From f470656b839e0eb3fde79c2acb6e6b5d105dcd30 Mon Sep 17 00:00:00 2001 From: DrDisagree Date: Wed, 4 Oct 2023 23:26:33 +0600 Subject: [PATCH] Battery Styles: Add 15 more battery styles --- .../Fonts/SanFranciscoText-Semibold.otf | Bin 0 -> 211156 bytes .../iconify/common/Preferences.java | 15 + .../xposed/mods/BatteryStyleManager.java | 86 +- .../mods/batterystyles/LandscapeBatteryA.kt | 694 +++++++++++ .../mods/batterystyles/LandscapeBatteryB.kt | 694 +++++++++++ .../mods/batterystyles/LandscapeBatteryC.kt | 692 +++++++++++ .../mods/batterystyles/LandscapeBatteryD.kt | 696 +++++++++++ .../mods/batterystyles/LandscapeBatteryE.kt | 694 +++++++++++ .../mods/batterystyles/LandscapeBatteryF.kt | 623 ++++++++++ .../mods/batterystyles/LandscapeBatteryG.kt | 662 ++++++++++ .../mods/batterystyles/LandscapeBatteryH.kt | 694 +++++++++++ .../mods/batterystyles/LandscapeBatteryI.kt | 1087 +++++++++++++++++ .../mods/batterystyles/LandscapeBatteryJ.kt | 655 ++++++++++ .../mods/batterystyles/LandscapeBatteryK.kt | 695 +++++++++++ .../mods/batterystyles/LandscapeBatteryL.kt | 686 +++++++++++ .../mods/batterystyles/LandscapeBatteryM.kt | 628 ++++++++++ .../mods/batterystyles/LandscapeBatteryN.kt | 711 +++++++++++ .../mods/batterystyles/LandscapeBatteryO.kt | 683 +++++++++++ app/src/main/res/values/arrays.xml | 15 + app/src/main/res/values/battery_strings.xml | 127 ++ 20 files changed, 10832 insertions(+), 5 deletions(-) create mode 100644 app/src/main/assets/Fonts/SanFranciscoText-Semibold.otf create mode 100644 app/src/main/java/com/drdisagree/iconify/xposed/mods/batterystyles/LandscapeBatteryA.kt create mode 100644 app/src/main/java/com/drdisagree/iconify/xposed/mods/batterystyles/LandscapeBatteryB.kt create mode 100644 app/src/main/java/com/drdisagree/iconify/xposed/mods/batterystyles/LandscapeBatteryC.kt create mode 100644 app/src/main/java/com/drdisagree/iconify/xposed/mods/batterystyles/LandscapeBatteryD.kt create mode 100644 app/src/main/java/com/drdisagree/iconify/xposed/mods/batterystyles/LandscapeBatteryE.kt create mode 100644 app/src/main/java/com/drdisagree/iconify/xposed/mods/batterystyles/LandscapeBatteryF.kt create mode 100644 app/src/main/java/com/drdisagree/iconify/xposed/mods/batterystyles/LandscapeBatteryG.kt create mode 100644 app/src/main/java/com/drdisagree/iconify/xposed/mods/batterystyles/LandscapeBatteryH.kt create mode 100644 app/src/main/java/com/drdisagree/iconify/xposed/mods/batterystyles/LandscapeBatteryI.kt create mode 100644 app/src/main/java/com/drdisagree/iconify/xposed/mods/batterystyles/LandscapeBatteryJ.kt create mode 100644 app/src/main/java/com/drdisagree/iconify/xposed/mods/batterystyles/LandscapeBatteryK.kt create mode 100644 app/src/main/java/com/drdisagree/iconify/xposed/mods/batterystyles/LandscapeBatteryL.kt create mode 100644 app/src/main/java/com/drdisagree/iconify/xposed/mods/batterystyles/LandscapeBatteryM.kt create mode 100644 app/src/main/java/com/drdisagree/iconify/xposed/mods/batterystyles/LandscapeBatteryN.kt create mode 100644 app/src/main/java/com/drdisagree/iconify/xposed/mods/batterystyles/LandscapeBatteryO.kt diff --git a/app/src/main/assets/Fonts/SanFranciscoText-Semibold.otf b/app/src/main/assets/Fonts/SanFranciscoText-Semibold.otf new file mode 100644 index 0000000000000000000000000000000000000000..fc4de752af7c4b5f81f1f9346d3589118630cc0b GIT binary patch literal 211156 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%0Dhh0|s2Z5(;0eD(N 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 Unit = { + invalidateSelf() + } + + open var criticalLevel: Int = 5 + + var charging = false + set(value) { + field = value + postInvalidate() + } + + override fun setChargingEnabled(charging: Boolean) { + this.charging = charging + postInvalidate() + } + + var powerSaveEnabled = false + set(value) { + field = value + postInvalidate() + } + + override fun setPowerSavingEnabled(powerSaveEnabled: Boolean) { + this.powerSaveEnabled = powerSaveEnabled + postInvalidate() + } + + var showPercent = false + set(value) { + field = value + postInvalidate() + } + + override fun setShowPercentEnabled(showPercent: Boolean) { + this.showPercent = showPercent + postInvalidate() + } + + var customChargingIcon = false + set(value) { + field = value + postInvalidate() + } + + open fun customizeBatteryDrawable( + isRotation: Boolean, + scaledPerimeterAlpha: Boolean, + scaledFillAlpha: Boolean, + customBlendColor: Boolean, + customFillColor: Int, + customFillGradColor: Int, + chargingColor: Int, + powerSaveColor: Int, + powerSaveFillColor: Int + ) { + this.isRotation = isRotation + this.scaledPerimeterAlpha = scaledPerimeterAlpha + this.scaledFillAlpha = scaledFillAlpha + this.customBlendColor = customBlendColor + this.customFillColor = customFillColor + this.customFillGradColor = customFillGradColor + this.chargingColor = chargingColor + this.powerSaveColor = powerSaveColor + this.powerSaveFillColor = powerSaveFillColor + } + + private val fillColorStrokePaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + p.alpha = 255 + p.isDither = true + p.strokeWidth = 5f + p.style = Paint.Style.STROKE + p.blendMode = BlendMode.SRC + p.strokeMiter = 5f + p.strokeJoin = Paint.Join.ROUND + } + + private val fillColorStrokeProtection = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.isDither = true + p.strokeWidth = 5f + p.style = Paint.Style.STROKE + p.blendMode = BlendMode.CLEAR + p.strokeMiter = 5f + p.strokeJoin = Paint.Join.ROUND + } + + private val fillPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + p.alpha = 255 + p.isDither = true + p.strokeWidth = 0f + p.style = Paint.Style.FILL_AND_STROKE + } + + private val errorPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = context.resources.getColorStateList( + context.resources.getIdentifier( + "batterymeter_plus_color", "color", context.packageName + ), context.theme + ).defaultColor + p.alpha = 255 + p.isDither = true + p.strokeWidth = 0f + p.style = Paint.Style.FILL_AND_STROKE + p.blendMode = BlendMode.SRC + } + + private val chargingPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + } + + private val customFillPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + } + + private val powerSavePaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + } + + private val powerSaveFillPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + } + + private val scaledFillPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + } + + private val scaledPerimeterPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + } + + private val scaledPerimeterPaintDef = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + } + + // Only used if dualTone is set to true + private val dualToneBackgroundFill = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + p.alpha = 85 // ~0.3 alpha by default + p.isDither = true + p.strokeWidth = 0f + p.style = Paint.Style.FILL_AND_STROKE + } + + private val textPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.typeface = Typeface.create("sans-serif-condensed", Typeface.BOLD) + p.textAlign = Paint.Align.CENTER + } + + init { + val density = context.resources.displayMetrics.density + intrinsicHeight = (HEIGHT * density).toInt() + intrinsicWidth = (WIDTH * density).toInt() + + val res = context.resources + val levels = res.obtainTypedArray( + res.getIdentifier( + "batterymeter_color_levels", "array", context.packageName + ) + ) + val colors = res.obtainTypedArray( + res.getIdentifier( + "batterymeter_color_values", "array", context.packageName + ) + ) + val n = levels.length() + colorLevels = IntArray(2 * n) + for (i in 0 until n) { + colorLevels[2 * i] = levels.getInt(i, 0) + if (colors.getType(i) == TypedValue.TYPE_ATTRIBUTE) { + colorLevels[2 * i + 1] = SettingsLibUtils.getColorAttrDefaultColor( + colors.getResourceId(i, 0), context + ) + } else { + colorLevels[2 * i + 1] = colors.getColor(i, 0) + } + } + levels.recycle() + colors.recycle() + + loadPaths() + } + + override fun draw(c: Canvas) { + c.saveLayer(null, null) + unifiedPath.reset() + levelPath.reset() + levelRect.set(fillRect) + val fillFraction = batteryLevel / 100f + val fillRight = + if (batteryLevel == 100) + fillRect.right + 0.61f + else + fillRect.right - (fillRect.width() * (1 - fillFraction)) + + levelRect.right = floor(fillRight.toDouble()).toFloat() + levelPath.addRoundRect( + levelRect, + floatArrayOf( + 3.8f, + 3.8f, + 3.8f, + 3.8f, + 3.8f, + 3.8f, + 3.8f, + 3.8f + ), Path.Direction.CCW + ) + + scaledFillPaint.alpha = if (scaledFillAlpha) 100 else 0 + scaledPerimeterPaint.alpha = + if (scaledPerimeterAlpha) 100 else scaledPerimeterPaintDef.alpha + + // The perimeter should never change + c.drawPath(scaledFill, scaledFillPaint) + c.drawPath(scaledPerimeter, scaledPerimeterPaint) + // If drawing dual tone, the level is used only to clip the whole drawable path + if (!dualTone) { + unifiedPath.op(levelPath, Path.Op.UNION) + } + + fillPaint.color = levelColor + val black = Color.BLACK + + // Deal with unifiedPath clipping before it draws + if (charging && !customChargingIcon) { + // Clip out the bolt shape + unifiedPath.op(scaledBolt, Path.Op.DIFFERENCE) + if (!invertFillIcon) { + c.drawPath(scaledBolt, fillPaint) + } + } + + if (dualTone) { + // Dual tone means we draw the shape again, clipped to the charge level + c.drawPath(unifiedPath, dualToneBackgroundFill) + c.save() + c.clipRect( + bounds.left.toFloat(), + 0f, + bounds.right + bounds.width() * fillFraction, + bounds.left.toFloat() + ) + c.drawPath(unifiedPath, fillPaint) + c.restore() + } else { + // Non dual-tone means we draw the perimeter (with the level fill), and potentially + // draw the fill again with a critical color + if (customBlendColor) { + if (charging) { + fillPaint.color = fillColor + c.drawPath(unifiedPath, fillPaint) + fillPaint.color = levelColor + } else { + // Show colorError below this level + if (batteryLevel <= CRITICAL_LEVEL) { + c.save() + c.clipPath(scaledFill) + c.drawPath(levelPath, fillPaint) + c.restore() + } else { + customFillPaint.color = customFillColor + customFillPaint.shader = + if (customFillColor != black && customFillGradColor != black) LinearGradient( + levelRect.right, 0f, 0f, levelRect.bottom, + customFillColor, customFillGradColor, + Shader.TileMode.CLAMP + ) else null + c.drawPath( + levelPath, + if (customFillColor == black) fillPaint else customFillPaint + ) + } + } + } else { + // Show colorError below this level + if (batteryLevel <= CRITICAL_LEVEL && !charging) { + c.save() + c.clipPath(scaledFill) + c.drawPath(levelPath, fillPaint) + c.restore() + } else { + fillPaint.color = fillColor + c.drawPath(unifiedPath, fillPaint) + fillPaint.color = levelColor + } + } + } + + if (customBlendColor) { + chargingPaint.color = + if (chargingColor == black) Color.TRANSPARENT else chargingColor + + powerSavePaint.color = + if (powerSaveColor == black) context.resources.getColorStateList( + context.resources.getIdentifier( + "batterymeter_plus_color", "color", context.packageName + ), context.theme + ).defaultColor else powerSaveColor + + powerSaveFillPaint.color = + if (powerSaveFillColor == black) Color.TRANSPARENT else powerSaveFillColor + } else { + chargingPaint.color = Color.TRANSPARENT + powerSavePaint.color = context.resources.getColorStateList( + context.resources.getIdentifier( + "batterymeter_plus_color", "color", context.packageName + ), context.theme + ).defaultColor + powerSaveFillPaint.color = Color.TRANSPARENT + } + + if (charging) { + if (!customChargingIcon) { + c.clipOutPath(scaledBolt) + c.drawPath(levelPath, chargingPaint) + if (invertFillIcon) { + c.drawPath(scaledBolt, fillColorStrokePaint) + } else { + c.drawPath(scaledBolt, fillColorStrokeProtection) + } + } else { + c.drawPath(levelPath, chargingPaint) + } + } else if (powerSaveEnabled) { + // If power save is enabled draw the perimeter path with colorError + c.drawPath(scaledErrorPerimeter, powerSavePaint) + c.drawPath(levelPath, powerSaveFillPaint) + // And draw the plus sign on top of the fill + if (!showPercent) { + c.drawPath(scaledPlus, powerSavePaint) + } + } + c.restore() + + var state = false + if (customChargingIcon && charging) { + state = true + } else if (customChargingIcon && !charging) { + state = true + } else if (!customChargingIcon && charging) { + state = false + } else if (!customChargingIcon && !charging) { + state = true + } + + if (showPercent && state) { + textPaint.textSize = bounds.width() * 0.25f + val textHeight = +textPaint.fontMetrics.ascent + val pctX = (bounds.width() + textHeight) * 0.56f + val pctY = bounds.height() * 0.66f + + if (isRotation) { + c.rotate(180f, pctX + 0.8f, pctY * 0.76f) + } + if (customBlendColor) { + if (powerSaveEnabled && powerSaveFillColor == black) { + textPaint.color = fillColor + c.drawText(batteryLevel.toString(), pctX, pctY, textPaint) + + textPaint.color = fillColor.toInt().inv() or 0xFF000000.toInt() + c.save() + c.clipRect( + if (isRotation) fillRect.left + (fillRect.width() * (1 - fillFraction)) else fillRect.left, + fillRect.top, + if (isRotation) fillRect.right else fillRect.right - (fillRect.width() * (1 - fillFraction)), + fillRect.bottom + ) + c.drawText(batteryLevel.toString(), pctX, pctY, textPaint) + } else if (!powerSaveEnabled && customFillColor == black) { + textPaint.color = fillColor + c.drawText(batteryLevel.toString(), pctX, pctY, textPaint) + + textPaint.color = fillColor.toInt().inv() or 0xFF000000.toInt() + c.save() + c.clipRect( + if (isRotation) fillRect.left + (fillRect.width() * (1 - fillFraction)) else fillRect.left, + fillRect.top, + if (isRotation) fillRect.right else fillRect.right - (fillRect.width() * (1 - fillFraction)), + fillRect.bottom + ) + c.drawText(batteryLevel.toString(), pctX, pctY, textPaint) + } else { + textPaint.color = fillColor + c.drawText(batteryLevel.toString(), pctX, pctY, textPaint) + } + } else { + textPaint.color = fillColor + c.drawText(batteryLevel.toString(), pctX, pctY, textPaint) + + textPaint.color = fillColor.toInt().inv() or 0xFF000000.toInt() + c.save() + c.clipRect( + if (isRotation) fillRect.left + (fillRect.width() * (1 - fillFraction)) else fillRect.left, + fillRect.top, + if (isRotation) fillRect.right else fillRect.right - (fillRect.width() * (1 - fillFraction)), + fillRect.bottom + ) + c.drawText(batteryLevel.toString(), pctX, pctY, textPaint) + } + c.restore() + } + } + + private fun batteryColorForLevel(level: Int): Int { + return when { + charging || powerSaveEnabled -> fillColor + else -> getColorForLevel(level) + } + } + + private fun getColorForLevel(level: Int): Int { + var thresh: Int + var color = 0 + var i = 0 + while (i < colorLevels.size) { + thresh = colorLevels[i] + color = colorLevels[i + 1] + if (level <= thresh) { + + // Respect tinting for "normal" level + return if (i == colorLevels.size - 2) { + fillColor + } else { + color + } + } + i += 2 + } + return color + } + + /** + * Alpha is unused internally, and should be defined in the colors passed to {@link setColors}. + * Further, setting an alpha for a dual tone battery meter doesn't make sense without bounds + * defining the minimum background fill alpha. This is because fill + background must be equal + * to the net alpha passed in here. + */ + override fun setAlpha(alpha: Int) { + } + + override fun setColorFilter(colorFilter: ColorFilter?) { + fillPaint.colorFilter = colorFilter + fillColorStrokePaint.colorFilter = colorFilter + dualToneBackgroundFill.colorFilter = colorFilter + } + + /** + * Deprecated, but required by Drawable + */ + @Deprecated( + "Deprecated in Java", + ReplaceWith("PixelFormat.OPAQUE", "android.graphics.PixelFormat"), + ) + override fun getOpacity(): Int { + return PixelFormat.OPAQUE + } + + override fun getIntrinsicHeight(): Int { + return intrinsicHeight + } + + override fun getIntrinsicWidth(): Int { + return intrinsicWidth + } + + /** + * Set the fill level + */ + @Suppress("PARAMETER_NAME_CHANGED_ON_OVERRIDE") + override fun setBatteryLevel(l: Int) { + // invertFillIcon = if (l >= 67) true else if (l <= 33) false else invertFillIcon + batteryLevel = l + levelColor = batteryColorForLevel(batteryLevel) + invalidateSelf() + } + + fun getBatteryLevel(): Int { + return batteryLevel + } + + override fun onBoundsChange(bounds: Rect) { + super.onBoundsChange(bounds) + updateSize() + } + + fun setPadding(left: Int, top: Int, right: Int, bottom: Int) { + padding.left = left + padding.top = top + padding.right = right + padding.bottom = bottom + + updateSize() + } + + override fun setColors(fgColor: Int, bgColor: Int, singleToneColor: Int) { + fillColor = if (dualTone) fgColor else singleToneColor + + fillPaint.color = fillColor + fillColorStrokePaint.color = fillColor + + scaledFillPaint.color = fillColor + scaledPerimeterPaint.color = fillColor + scaledPerimeterPaintDef.color = fillColor + + backgroundColor = bgColor + dualToneBackgroundFill.color = bgColor + + // Also update the level color, since fillColor may have changed + levelColor = batteryColorForLevel(batteryLevel) + + invalidateSelf() + } + + private fun postInvalidate() { + unscheduleSelf(invalidateRunnable) + scheduleSelf(invalidateRunnable, 0) + } + + private fun updateSize() { + val b = bounds + if (b.isEmpty) { + scaleMatrix.setScale(1f, 1f) + } else { + scaleMatrix.setScale((b.right / WIDTH), (b.bottom / HEIGHT)) + } + + perimeterPath.transform(scaleMatrix, scaledPerimeter) + errorPerimeterPath.transform(scaleMatrix, scaledErrorPerimeter) + fillMask.transform(scaleMatrix, scaledFill) + scaledFill.computeBounds(fillRect, true) + boltPath.transform(scaleMatrix, scaledBolt) + plusPath.transform(scaleMatrix, scaledPlus) + + // It is expected that this view only ever scale by the same factor in each dimension, so + // just pick one to scale the strokeWidths + val scaledStrokeWidth = + (b.right / WIDTH * PROTECTION_STROKE_WIDTH).coerceAtLeast(PROTECTION_MIN_STROKE_WIDTH) + + fillColorStrokePaint.strokeWidth = scaledStrokeWidth + fillColorStrokeProtection.strokeWidth = scaledStrokeWidth + } + + @SuppressLint("RestrictedApi") + private fun loadPaths() { + val pathString = modRes.getString(R.string.config_landscapeBatteryPerimeterPathA) + perimeterPath.set(PathParser.createPathFromPathData(pathString)) + perimeterPath.computeBounds(RectF(), true) + + val errorPathString = modRes.getString(R.string.config_landscapeBatteryErrorPerimeterPathA) + errorPerimeterPath.set(PathParser.createPathFromPathData(errorPathString)) + errorPerimeterPath.computeBounds(RectF(), true) + + val fillMaskString = modRes.getString(R.string.config_landscapeBatteryFillMaskA) + fillMask.set(PathParser.createPathFromPathData(fillMaskString)) + // Set the fill rect so we can calculate the fill properly + fillMask.computeBounds(fillRect, true) + + val boltPathString = modRes.getString(R.string.config_landscapeBatteryBoltPathA) + boltPath.set(PathParser.createPathFromPathData(boltPathString)) + + val plusPathString = modRes.getString(R.string.config_landscapeBatteryPowersavePathA) + plusPath.set(PathParser.createPathFromPathData(plusPathString)) + + dualTone = false + } + + companion object { + private const val TAG = "LandscapeBatteryA" + private const val WIDTH = 18f + private const val HEIGHT = 10f + private const val CRITICAL_LEVEL = 15 + + // On a 18x10 grid, how wide to make the fill protection stroke. + // Scales when our size changes + private const val PROTECTION_STROKE_WIDTH = 1.5f + + // Arbitrarily chosen for visibility at small sizes + private const val PROTECTION_MIN_STROKE_WIDTH = 3f + } +} diff --git a/app/src/main/java/com/drdisagree/iconify/xposed/mods/batterystyles/LandscapeBatteryB.kt b/app/src/main/java/com/drdisagree/iconify/xposed/mods/batterystyles/LandscapeBatteryB.kt new file mode 100644 index 000000000..40a474692 --- /dev/null +++ b/app/src/main/java/com/drdisagree/iconify/xposed/mods/batterystyles/LandscapeBatteryB.kt @@ -0,0 +1,694 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.drdisagree.iconify.xposed.mods.batterystyles + +import android.annotation.SuppressLint +import android.content.Context +import android.graphics.BlendMode +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.ColorFilter +import android.graphics.LinearGradient +import android.graphics.Matrix +import android.graphics.Paint +import android.graphics.Path +import android.graphics.PixelFormat +import android.graphics.Rect +import android.graphics.RectF +import android.graphics.Shader +import android.graphics.Typeface +import android.util.TypedValue +import androidx.core.graphics.PathParser +import com.drdisagree.iconify.R +import com.drdisagree.iconify.xposed.HookRes.modRes +import com.drdisagree.iconify.xposed.utils.SettingsLibUtils +import kotlin.math.floor + +/** + * A battery meter drawable that respects paths configured in + * frameworks/base/core/res/res/values/config.xml to allow for an easily overrideable battery icon + */ +@SuppressLint("DiscouragedApi") +open class LandscapeBatteryB(private val context: Context, frameColor: Int) : BatteryDrawable() { + + // Need to load: + // 1. perimeter shape + // 2. fill mask (if smaller than perimeter, this would create a fill that + // doesn't touch the walls + private val perimeterPath = Path() + private val scaledPerimeter = Path() + private val errorPerimeterPath = Path() + private val scaledErrorPerimeter = Path() + + // Fill will cover the whole bounding rect of the fillMask, and be masked by the path + private val fillMask = Path() + private val scaledFill = Path() + + // Based off of the mask, the fill will interpolate across this space + private val fillRect = RectF() + + // Top of this rect changes based on level, 100% == fillRect + private val levelRect = RectF() + private val levelPath = Path() + + // Updates the transform of the paths when our bounds change + private val scaleMatrix = Matrix() + private val padding = Rect() + + // The net result of fill + perimeter paths + private val unifiedPath = Path() + + // Bolt path (used while charging) + private val boltPath = Path() + private val scaledBolt = Path() + + // Plus sign (used for power save mode) + private val plusPath = Path() + private val scaledPlus = Path() + + private var intrinsicHeight: Int + private var intrinsicWidth: Int + + // To implement hysteresis, keep track of the need to invert the interior icon of the battery + private var invertFillIcon = false + + // Colors can be configured based on battery level (see res/values/arrays.xml) + private var colorLevels: IntArray + + private var fillColor: Int = Color.MAGENTA + private var backgroundColor: Int = Color.MAGENTA + + // updated whenever level changes + private var levelColor: Int = Color.MAGENTA + + // Dual tone implies that battery level is a clipped overlay over top of the whole shape + private var dualTone = false + + private var batteryLevel = 0 + + private var isRotation = false + private var scaledFillAlpha = false + private var scaledPerimeterAlpha = false + private var customBlendColor = false + + private var chargingColor: Int = Color.TRANSPARENT + private var customFillColor: Int = Color.BLACK + private var customFillGradColor: Int = Color.BLACK + private var powerSaveColor: Int = Color.TRANSPARENT + private var powerSaveFillColor: Int = Color.TRANSPARENT + + private val invalidateRunnable: () -> Unit = { + invalidateSelf() + } + + open var criticalLevel: Int = 5 + + var charging = false + set(value) { + field = value + postInvalidate() + } + + override fun setChargingEnabled(charging: Boolean) { + this.charging = charging + postInvalidate() + } + + var powerSaveEnabled = false + set(value) { + field = value + postInvalidate() + } + + override fun setPowerSavingEnabled(powerSaveEnabled: Boolean) { + this.powerSaveEnabled = powerSaveEnabled + postInvalidate() + } + + var showPercent = false + set(value) { + field = value + postInvalidate() + } + + override fun setShowPercentEnabled(showPercent: Boolean) { + this.showPercent = showPercent + postInvalidate() + } + + var customChargingIcon = false + set(value) { + field = value + postInvalidate() + } + + open fun customizeBatteryDrawable( + isRotation: Boolean, + scaledPerimeterAlpha: Boolean, + scaledFillAlpha: Boolean, + customBlendColor: Boolean, + customFillColor: Int, + customFillGradColor: Int, + chargingColor: Int, + powerSaveColor: Int, + powerSaveFillColor: Int + ) { + this.isRotation = isRotation + this.scaledPerimeterAlpha = scaledPerimeterAlpha + this.scaledFillAlpha = scaledFillAlpha + this.customBlendColor = customBlendColor + this.customFillColor = customFillColor + this.customFillGradColor = customFillGradColor + this.chargingColor = chargingColor + this.powerSaveColor = powerSaveColor + this.powerSaveFillColor = powerSaveFillColor + } + + private val fillColorStrokePaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + p.alpha = 255 + p.isDither = true + p.strokeWidth = 5f + p.style = Paint.Style.STROKE + p.blendMode = BlendMode.SRC + p.strokeMiter = 5f + p.strokeJoin = Paint.Join.ROUND + } + + private val fillColorStrokeProtection = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.isDither = true + p.strokeWidth = 5f + p.style = Paint.Style.STROKE + p.blendMode = BlendMode.CLEAR + p.strokeMiter = 5f + p.strokeJoin = Paint.Join.ROUND + } + + private val fillPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + p.alpha = 255 + p.isDither = true + p.strokeWidth = 0f + p.style = Paint.Style.FILL_AND_STROKE + } + + private val errorPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = context.resources.getColorStateList( + context.resources.getIdentifier( + "batterymeter_plus_color", "color", context.packageName + ), context.theme + ).defaultColor + p.alpha = 255 + p.isDither = true + p.strokeWidth = 0f + p.style = Paint.Style.FILL_AND_STROKE + p.blendMode = BlendMode.SRC + } + + private val chargingPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + } + + private val customFillPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + } + + private val powerSavePaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + } + + private val powerSaveFillPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + } + + private val scaledFillPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + } + + private val scaledPerimeterPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + } + + private val scaledPerimeterPaintDef = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + } + + // Only used if dualTone is set to true + private val dualToneBackgroundFill = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + p.alpha = 85 // ~0.3 alpha by default + p.isDither = true + p.strokeWidth = 0f + p.style = Paint.Style.FILL_AND_STROKE + } + + private val textPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.typeface = Typeface.create("sans-serif-condensed", Typeface.BOLD) + p.textAlign = Paint.Align.CENTER + } + + init { + val density = context.resources.displayMetrics.density + intrinsicHeight = (HEIGHT * density).toInt() + intrinsicWidth = (WIDTH * density).toInt() + + val res = context.resources + val levels = res.obtainTypedArray( + res.getIdentifier( + "batterymeter_color_levels", "array", context.packageName + ) + ) + val colors = res.obtainTypedArray( + res.getIdentifier( + "batterymeter_color_values", "array", context.packageName + ) + ) + val n = levels.length() + colorLevels = IntArray(2 * n) + for (i in 0 until n) { + colorLevels[2 * i] = levels.getInt(i, 0) + if (colors.getType(i) == TypedValue.TYPE_ATTRIBUTE) { + colorLevels[2 * i + 1] = SettingsLibUtils.getColorAttrDefaultColor( + colors.getResourceId(i, 0), context + ) + } else { + colorLevels[2 * i + 1] = colors.getColor(i, 0) + } + } + levels.recycle() + colors.recycle() + + loadPaths() + } + + override fun draw(c: Canvas) { + c.saveLayer(null, null) + unifiedPath.reset() + levelPath.reset() + levelRect.set(fillRect) + val fillFraction = batteryLevel / 100f + val fillRight = + if (batteryLevel == 100) + fillRect.right + 1 + else + fillRect.right - (fillRect.width() * (1 - fillFraction)) + + levelRect.right = floor(fillRight.toDouble()).toFloat() + levelPath.addRoundRect( + levelRect, + floatArrayOf( + 4.5f, + 4.5f, + 4.5f, + 4.5f, + 4.5f, + 4.5f, + 4.5f, + 4.5f + ), Path.Direction.CCW + ) + + scaledFillPaint.alpha = if (scaledFillAlpha) 100 else 0 + scaledPerimeterPaint.alpha = + if (scaledPerimeterAlpha) 100 else scaledPerimeterPaintDef.alpha + + // The perimeter should never change + c.drawPath(scaledFill, scaledFillPaint) + c.drawPath(scaledPerimeter, scaledPerimeterPaint) + // If drawing dual tone, the level is used only to clip the whole drawable path + if (!dualTone) { + unifiedPath.op(levelPath, Path.Op.UNION) + } + + fillPaint.color = levelColor + val black = Color.BLACK + + // Deal with unifiedPath clipping before it draws + if (charging && !customChargingIcon) { + // Clip out the bolt shape + unifiedPath.op(scaledBolt, Path.Op.DIFFERENCE) + if (!invertFillIcon) { + c.drawPath(scaledBolt, fillPaint) + } + } + + if (dualTone) { + // Dual tone means we draw the shape again, clipped to the charge level + c.drawPath(unifiedPath, dualToneBackgroundFill) + c.save() + c.clipRect( + bounds.left.toFloat(), + 0f, + bounds.right + bounds.width() * fillFraction, + bounds.left.toFloat() + ) + c.drawPath(unifiedPath, fillPaint) + c.restore() + } else { + // Non dual-tone means we draw the perimeter (with the level fill), and potentially + // draw the fill again with a critical color + if (customBlendColor) { + if (charging) { + fillPaint.color = fillColor + c.drawPath(unifiedPath, fillPaint) + fillPaint.color = levelColor + } else { + // Show colorError below this level + if (batteryLevel <= CRITICAL_LEVEL) { + c.save() + c.clipPath(scaledFill) + c.drawPath(levelPath, fillPaint) + c.restore() + } else { + customFillPaint.color = customFillColor + customFillPaint.shader = + if (customFillColor != black && customFillGradColor != black) LinearGradient( + levelRect.right, 0f, 0f, levelRect.bottom, + customFillColor, customFillGradColor, + Shader.TileMode.CLAMP + ) else null + c.drawPath( + levelPath, + if (customFillColor == black) fillPaint else customFillPaint + ) + } + } + } else { + // Show colorError below this level + if (batteryLevel <= CRITICAL_LEVEL && !charging) { + c.save() + c.clipPath(scaledFill) + c.drawPath(levelPath, fillPaint) + c.restore() + } else { + fillPaint.color = fillColor + c.drawPath(unifiedPath, fillPaint) + fillPaint.color = levelColor + } + } + } + + if (customBlendColor) { + chargingPaint.color = + if (chargingColor == black) Color.TRANSPARENT else chargingColor + + powerSavePaint.color = + if (powerSaveColor == black) context.resources.getColorStateList( + context.resources.getIdentifier( + "batterymeter_plus_color", "color", context.packageName + ), context.theme + ).defaultColor else powerSaveColor + + powerSaveFillPaint.color = + if (powerSaveFillColor == black) Color.TRANSPARENT else powerSaveFillColor + } else { + chargingPaint.color = Color.TRANSPARENT + powerSavePaint.color = context.resources.getColorStateList( + context.resources.getIdentifier( + "batterymeter_plus_color", "color", context.packageName + ), context.theme + ).defaultColor + powerSaveFillPaint.color = Color.TRANSPARENT + } + + if (charging) { + if (!customChargingIcon) { + c.clipOutPath(scaledBolt) + c.drawPath(levelPath, chargingPaint) + if (invertFillIcon) { + c.drawPath(scaledBolt, fillColorStrokePaint) + } else { + c.drawPath(scaledBolt, fillColorStrokeProtection) + } + } else { + c.drawPath(levelPath, chargingPaint) + } + } else if (powerSaveEnabled) { + // If power save is enabled draw the perimeter path with colorError + c.drawPath(scaledErrorPerimeter, powerSavePaint) + c.drawPath(levelPath, powerSaveFillPaint) + // And draw the plus sign on top of the fill + if (!showPercent) { + c.drawPath(scaledPlus, powerSavePaint) + } + } + c.restore() + + var state = false + if (customChargingIcon && charging) { + state = true + } else if (customChargingIcon && !charging) { + state = true + } else if (!customChargingIcon && charging) { + state = false + } else if (!customChargingIcon && !charging) { + state = true + } + + if (showPercent && state) { + textPaint.textSize = bounds.width() * 0.25f + val textHeight = +textPaint.fontMetrics.ascent + val pctX = (bounds.width() + textHeight) * 0.64f + val pctY = bounds.height() * 0.70f + + if (isRotation) { + c.rotate(180f, pctX + 0.5f, pctY * 0.79f) + } + if (customBlendColor) { + if (powerSaveEnabled && powerSaveFillColor == black) { + textPaint.color = fillColor + c.drawText(batteryLevel.toString(), pctX, pctY, textPaint) + + textPaint.color = fillColor.toInt().inv() or 0xFF000000.toInt() + c.save() + c.clipRect( + if (isRotation) fillRect.left + (fillRect.width() * (1 - fillFraction)) else fillRect.left, + fillRect.top, + if (isRotation) fillRect.right else fillRect.right - (fillRect.width() * (1 - fillFraction)), + fillRect.bottom + ) + c.drawText(batteryLevel.toString(), pctX, pctY, textPaint) + } else if (!powerSaveEnabled && customFillColor == black) { + textPaint.color = fillColor + c.drawText(batteryLevel.toString(), pctX, pctY, textPaint) + + textPaint.color = fillColor.toInt().inv() or 0xFF000000.toInt() + c.save() + c.clipRect( + if (isRotation) fillRect.left + (fillRect.width() * (1 - fillFraction)) else fillRect.left, + fillRect.top, + if (isRotation) fillRect.right else fillRect.right - (fillRect.width() * (1 - fillFraction)), + fillRect.bottom + ) + c.drawText(batteryLevel.toString(), pctX, pctY, textPaint) + } else { + textPaint.color = fillColor + c.drawText(batteryLevel.toString(), pctX, pctY, textPaint) + } + } else { + textPaint.color = fillColor + c.drawText(batteryLevel.toString(), pctX, pctY, textPaint) + + textPaint.color = fillColor.toInt().inv() or 0xFF000000.toInt() + c.save() + c.clipRect( + if (isRotation) fillRect.left + (fillRect.width() * (1 - fillFraction)) else fillRect.left, + fillRect.top, + if (isRotation) fillRect.right else fillRect.right - (fillRect.width() * (1 - fillFraction)), + fillRect.bottom + ) + c.drawText(batteryLevel.toString(), pctX, pctY, textPaint) + } + c.restore() + } + } + + private fun batteryColorForLevel(level: Int): Int { + return when { + charging || powerSaveEnabled -> fillColor + else -> getColorForLevel(level) + } + } + + private fun getColorForLevel(level: Int): Int { + var thresh: Int + var color = 0 + var i = 0 + while (i < colorLevels.size) { + thresh = colorLevels[i] + color = colorLevels[i + 1] + if (level <= thresh) { + + // Respect tinting for "normal" level + return if (i == colorLevels.size - 2) { + fillColor + } else { + color + } + } + i += 2 + } + return color + } + + /** + * Alpha is unused internally, and should be defined in the colors passed to {@link setColors}. + * Further, setting an alpha for a dual tone battery meter doesn't make sense without bounds + * defining the minimum background fill alpha. This is because fill + background must be equal + * to the net alpha passed in here. + */ + override fun setAlpha(alpha: Int) { + } + + override fun setColorFilter(colorFilter: ColorFilter?) { + fillPaint.colorFilter = colorFilter + fillColorStrokePaint.colorFilter = colorFilter + dualToneBackgroundFill.colorFilter = colorFilter + } + + /** + * Deprecated, but required by Drawable + */ + @Deprecated( + "Deprecated in Java", + ReplaceWith("PixelFormat.OPAQUE", "android.graphics.PixelFormat"), + ) + override fun getOpacity(): Int { + return PixelFormat.OPAQUE + } + + override fun getIntrinsicHeight(): Int { + return intrinsicHeight + } + + override fun getIntrinsicWidth(): Int { + return intrinsicWidth + } + + /** + * Set the fill level + */ + @Suppress("PARAMETER_NAME_CHANGED_ON_OVERRIDE") + override fun setBatteryLevel(l: Int) { + // invertFillIcon = if (l >= 67) true else if (l <= 33) false else invertFillIcon + batteryLevel = l + levelColor = batteryColorForLevel(batteryLevel) + invalidateSelf() + } + + fun getBatteryLevel(): Int { + return batteryLevel + } + + override fun onBoundsChange(bounds: Rect) { + super.onBoundsChange(bounds) + updateSize() + } + + fun setPadding(left: Int, top: Int, right: Int, bottom: Int) { + padding.left = left + padding.top = top + padding.right = right + padding.bottom = bottom + + updateSize() + } + + override fun setColors(fgColor: Int, bgColor: Int, singleToneColor: Int) { + fillColor = if (dualTone) fgColor else singleToneColor + + fillPaint.color = fillColor + fillColorStrokePaint.color = fillColor + + scaledFillPaint.color = fillColor + scaledPerimeterPaint.color = fillColor + scaledPerimeterPaintDef.color = fillColor + + backgroundColor = bgColor + dualToneBackgroundFill.color = bgColor + + // Also update the level color, since fillColor may have changed + levelColor = batteryColorForLevel(batteryLevel) + + invalidateSelf() + } + + private fun postInvalidate() { + unscheduleSelf(invalidateRunnable) + scheduleSelf(invalidateRunnable, 0) + } + + private fun updateSize() { + val b = bounds + if (b.isEmpty) { + scaleMatrix.setScale(1f, 1f) + } else { + scaleMatrix.setScale((b.right / WIDTH), (b.bottom / HEIGHT)) + } + + perimeterPath.transform(scaleMatrix, scaledPerimeter) + errorPerimeterPath.transform(scaleMatrix, scaledErrorPerimeter) + fillMask.transform(scaleMatrix, scaledFill) + scaledFill.computeBounds(fillRect, true) + boltPath.transform(scaleMatrix, scaledBolt) + plusPath.transform(scaleMatrix, scaledPlus) + + // It is expected that this view only ever scale by the same factor in each dimension, so + // just pick one to scale the strokeWidths + val scaledStrokeWidth = + (b.right / WIDTH * PROTECTION_STROKE_WIDTH).coerceAtLeast(PROTECTION_MIN_STROKE_WIDTH) + + fillColorStrokePaint.strokeWidth = scaledStrokeWidth + fillColorStrokeProtection.strokeWidth = scaledStrokeWidth + } + + @SuppressLint("RestrictedApi") + private fun loadPaths() { + val pathString = modRes.getString(R.string.config_landscapeBatteryPerimeterPathB) + perimeterPath.set(PathParser.createPathFromPathData(pathString)) + perimeterPath.computeBounds(RectF(), true) + + val errorPathString = modRes.getString(R.string.config_landscapeBatteryErrorPerimeterPathB) + errorPerimeterPath.set(PathParser.createPathFromPathData(errorPathString)) + errorPerimeterPath.computeBounds(RectF(), true) + + val fillMaskString = modRes.getString(R.string.config_landscapeBatteryFillMaskB) + fillMask.set(PathParser.createPathFromPathData(fillMaskString)) + // Set the fill rect so we can calculate the fill properly + fillMask.computeBounds(fillRect, true) + + val boltPathString = modRes.getString(R.string.config_landscapeBatteryBoltPathB) + boltPath.set(PathParser.createPathFromPathData(boltPathString)) + + val plusPathString = modRes.getString(R.string.config_landscapeBatteryPowersavePathB) + plusPath.set(PathParser.createPathFromPathData(plusPathString)) + + dualTone = false + } + + companion object { + private const val TAG = "LandscapeBatteryB" + private const val WIDTH = 20f + private const val HEIGHT = 12f + private const val CRITICAL_LEVEL = 15 + + // On a 20x12 grid, how wide to make the fill protection stroke. + // Scales when our size changes + private const val PROTECTION_STROKE_WIDTH = 3f + + // Arbitrarily chosen for visibility at small sizes + private const val PROTECTION_MIN_STROKE_WIDTH = 6f + } +} diff --git a/app/src/main/java/com/drdisagree/iconify/xposed/mods/batterystyles/LandscapeBatteryC.kt b/app/src/main/java/com/drdisagree/iconify/xposed/mods/batterystyles/LandscapeBatteryC.kt new file mode 100644 index 000000000..e6354b3ed --- /dev/null +++ b/app/src/main/java/com/drdisagree/iconify/xposed/mods/batterystyles/LandscapeBatteryC.kt @@ -0,0 +1,692 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.drdisagree.iconify.xposed.mods.batterystyles + +import android.annotation.SuppressLint +import android.content.Context +import android.graphics.BlendMode +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.ColorFilter +import android.graphics.LinearGradient +import android.graphics.Matrix +import android.graphics.Paint +import android.graphics.Path +import android.graphics.PixelFormat +import android.graphics.Rect +import android.graphics.RectF +import android.graphics.Shader +import android.graphics.Typeface +import android.util.TypedValue +import androidx.core.graphics.PathParser +import com.drdisagree.iconify.R +import com.drdisagree.iconify.xposed.HookRes.modRes +import com.drdisagree.iconify.xposed.utils.SettingsLibUtils +import kotlin.math.floor + +/** + * A battery meter drawable that respects paths configured in + * frameworks/base/core/res/res/values/config.xml to allow for an easily overrideable battery icon + */ +@SuppressLint("DiscouragedApi") +open class LandscapeBatteryC(private val context: Context, frameColor: Int) : BatteryDrawable() { + + // Need to load: + // 1. perimeter shape + // 2. fill mask (if smaller than perimeter, this would create a fill that + // doesn't touch the walls + private val perimeterPath = Path() + private val scaledPerimeter = Path() + private val errorPerimeterPath = Path() + private val scaledErrorPerimeter = Path() + + // Fill will cover the whole bounding rect of the fillMask, and be masked by the path + private val fillMask = Path() + private val scaledFill = Path() + + // Based off of the mask, the fill will interpolate across this space + private val fillRect = RectF() + + // Top of this rect changes based on level, 100% == fillRect + private val levelRect = RectF() + private val levelPath = Path() + + // Updates the transform of the paths when our bounds change + private val scaleMatrix = Matrix() + private val padding = Rect() + + // The net result of fill + perimeter paths + private val unifiedPath = Path() + + // Bolt path (used while charging) + private val boltPath = Path() + private val scaledBolt = Path() + + // Plus sign (used for power save mode) + private val plusPath = Path() + private val scaledPlus = Path() + + private var intrinsicHeight: Int + private var intrinsicWidth: Int + + // To implement hysteresis, keep track of the need to invert the interior icon of the battery + private var invertFillIcon = false + + // Colors can be configured based on battery level (see res/values/arrays.xml) + private var colorLevels: IntArray + + private var fillColor: Int = Color.MAGENTA + private var backgroundColor: Int = Color.MAGENTA + + // updated whenever level changes + private var levelColor: Int = Color.MAGENTA + + // Dual tone implies that battery level is a clipped overlay over top of the whole shape + private var dualTone = false + + private var batteryLevel = 0 + + private var isRotation = false + private var scaledFillAlpha = false + private var scaledPerimeterAlpha = false + private var customBlendColor = false + + private var chargingColor: Int = Color.TRANSPARENT + private var customFillColor: Int = Color.BLACK + private var customFillGradColor: Int = Color.BLACK + private var powerSaveColor: Int = Color.TRANSPARENT + private var powerSaveFillColor: Int = Color.TRANSPARENT + + private val invalidateRunnable: () -> Unit = { + invalidateSelf() + } + + open var criticalLevel: Int = 5 + + var charging = false + set(value) { + field = value + postInvalidate() + } + + override fun setChargingEnabled(charging: Boolean) { + this.charging = charging + postInvalidate() + } + + var powerSaveEnabled = false + set(value) { + field = value + postInvalidate() + } + + override fun setPowerSavingEnabled(powerSaveEnabled: Boolean) { + this.powerSaveEnabled = powerSaveEnabled + postInvalidate() + } + + var showPercent = false + set(value) { + field = value + postInvalidate() + } + + override fun setShowPercentEnabled(showPercent: Boolean) { + this.showPercent = showPercent + postInvalidate() + } + + var customChargingIcon = false + set(value) { + field = value + postInvalidate() + } + + open fun customizeBatteryDrawable( + isRotation: Boolean, + scaledPerimeterAlpha: Boolean, + scaledFillAlpha: Boolean, + customBlendColor: Boolean, + customFillColor: Int, + customFillGradColor: Int, + chargingColor: Int, + powerSaveColor: Int, + powerSaveFillColor: Int + ) { + this.isRotation = isRotation + this.scaledPerimeterAlpha = scaledPerimeterAlpha + this.scaledFillAlpha = scaledFillAlpha + this.customBlendColor = customBlendColor + this.customFillColor = customFillColor + this.customFillGradColor = customFillGradColor + this.chargingColor = chargingColor + this.powerSaveColor = powerSaveColor + this.powerSaveFillColor = powerSaveFillColor + } + + private val fillColorStrokePaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + p.alpha = 255 + p.isDither = true + p.strokeWidth = 5f + p.style = Paint.Style.STROKE + p.blendMode = BlendMode.SRC + p.strokeMiter = 5f + p.strokeJoin = Paint.Join.ROUND + } + + private val fillColorStrokeProtection = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.isDither = true + p.strokeWidth = 5f + p.style = Paint.Style.STROKE + p.blendMode = BlendMode.CLEAR + p.strokeMiter = 5f + p.strokeJoin = Paint.Join.ROUND + } + + private val fillPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + p.alpha = 255 + p.isDither = true + p.strokeWidth = 0f + p.style = Paint.Style.FILL_AND_STROKE + } + + @SuppressLint("DiscouragedApi") + private val errorPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = context.resources.getColorStateList( + context.resources.getIdentifier( + "batterymeter_plus_color", "color", context.packageName + ), context.theme + ).defaultColor + p.alpha = 255 + p.isDither = true + p.strokeWidth = 0f + p.style = Paint.Style.FILL_AND_STROKE + p.blendMode = BlendMode.SRC + } + + private val chargingPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + } + + private val customFillPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + } + + private val fillPercentPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + } + + private val powerSavePaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + } + + private val powerSaveFillPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + } + + private val scaledFillPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + } + + private val scaledPerimeterPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + } + + private val scaledPerimeterPaintDef = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + } + + // Only used if dualTone is set to true + private val dualToneBackgroundFill = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + p.alpha = 85 // ~0.3 alpha by default + p.isDither = true + p.strokeWidth = 0f + p.style = Paint.Style.FILL_AND_STROKE + } + + private val textPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.typeface = Typeface.create("sans-serif-condensed", Typeface.BOLD) + p.textAlign = Paint.Align.CENTER + } + + init { + val density = context.resources.displayMetrics.density + intrinsicHeight = (HEIGHT * density).toInt() + intrinsicWidth = (WIDTH * density).toInt() + + val res = context.resources + val levels = res.obtainTypedArray( + res.getIdentifier( + "batterymeter_color_levels", "array", context.packageName + ) + ) + val colors = res.obtainTypedArray( + res.getIdentifier( + "batterymeter_color_values", "array", context.packageName + ) + ) + val n = levels.length() + colorLevels = IntArray(2 * n) + for (i in 0 until n) { + colorLevels[2 * i] = levels.getInt(i, 0) + if (colors.getType(i) == TypedValue.TYPE_ATTRIBUTE) { + colorLevels[2 * i + 1] = SettingsLibUtils.getColorAttrDefaultColor( + colors.getResourceId(i, 0), context + ) + } else { + colorLevels[2 * i + 1] = colors.getColor(i, 0) + } + } + levels.recycle() + colors.recycle() + + loadPaths() + } + + override fun draw(c: Canvas) { + c.saveLayer(null, null) + unifiedPath.reset() + levelPath.reset() + levelRect.set(fillRect) + val fillFraction = batteryLevel / 100f + val fillRight = + if (batteryLevel == 100) + fillRect.right + 1 + else + fillRect.right - (fillRect.width() * (1 - fillFraction)) + + levelRect.right = floor(fillRight.toDouble()).toFloat() + levelPath.addRoundRect( + levelRect, + floatArrayOf( + 3.0f, + 3.0f, + 3.0f, + 3.0f, + 3.0f, + 3.0f, + 3.0f, + 3.0f + ), Path.Direction.CCW + ) + + scaledFillPaint.alpha = if (scaledFillAlpha) 100 else 0 + scaledPerimeterPaint.alpha = + if (scaledPerimeterAlpha) 100 else scaledPerimeterPaintDef.alpha + + // The perimeter should never change + c.drawPath(scaledFill, scaledFillPaint) + c.drawPath(scaledPerimeter, scaledPerimeterPaint) + // If drawing dual tone, the level is used only to clip the whole drawable path + if (!dualTone) { + unifiedPath.op(levelPath, Path.Op.UNION) + } + + fillPaint.color = levelColor + val black = Color.BLACK + + // Deal with unifiedPath clipping before it draws + if (!showPercent && !customChargingIcon) { + if (charging) { + // Clip out the bolt shape + unifiedPath.op(scaledBolt, Path.Op.DIFFERENCE) + if (!invertFillIcon) { + c.drawPath(scaledBolt, fillPaint) + } + } + } + + if (dualTone) { + // Dual tone means we draw the shape again, clipped to the charge level + c.drawPath(unifiedPath, dualToneBackgroundFill) + c.save() + c.clipRect( + bounds.left.toFloat(), + 0f, + bounds.right + bounds.width() * fillFraction, + bounds.left.toFloat() + ) + c.drawPath(unifiedPath, fillPaint) + c.restore() + } else { + // Non dual-tone means we draw the perimeter (with the level fill), and potentially + // draw the fill again with a critical color + if (customBlendColor) { + if (showPercent) { + if (charging) { + fillPaint.color = fillColor + c.drawPath(unifiedPath, fillPercentPaint) + fillPaint.color = levelColor + } else { + // Show colorError below this level + if (batteryLevel <= CRITICAL_LEVEL && !charging) { + c.save() + c.clipPath(scaledFill) + c.drawPath(levelPath, fillPaint) + c.restore() + } else { + customFillPaint.color = customFillColor + customFillPaint.shader = + if (customFillColor != black && customFillGradColor != black) LinearGradient( + levelRect.right, 0f, 0f, levelRect.bottom, + customFillColor, customFillGradColor, + Shader.TileMode.CLAMP + ) else null + c.drawPath( + levelPath, + if (customFillColor == black) fillPercentPaint else customFillPaint + ) + } + } + } else { + if (charging) { + fillPaint.color = fillColor + c.drawPath(unifiedPath, fillPaint) + fillPaint.color = levelColor + } else { + // Show colorError below this level + if (batteryLevel <= CRITICAL_LEVEL && !charging) { + c.save() + c.clipPath(scaledFill) + c.drawPath(levelPath, fillPaint) + c.restore() + } else { + customFillPaint.color = customFillColor + customFillPaint.shader = + if (customFillColor != black && customFillGradColor != black) LinearGradient( + levelRect.right, 0f, 0f, levelRect.bottom, + customFillColor, customFillGradColor, + Shader.TileMode.CLAMP + ) else null + c.drawPath( + levelPath, + if (customFillColor == black) fillPercentPaint else customFillPaint + ) + } + } + } + } else { + if (showPercent) { + // Show colorError below this level + if (batteryLevel <= CRITICAL_LEVEL && !charging) { + c.save() + c.clipPath(scaledFill) + c.drawPath(levelPath, fillPaint) + c.restore() + } else { + fillPaint.color = fillColor + c.drawPath(unifiedPath, fillPercentPaint) + fillPaint.color = levelColor + } + } else { + // Show colorError below this level + if (batteryLevel <= CRITICAL_LEVEL && !charging) { + c.save() + c.clipPath(scaledFill) + c.drawPath(levelPath, fillPaint) + c.restore() + } else { + fillPaint.color = fillColor + c.drawPath(unifiedPath, fillPaint) + fillPaint.color = levelColor + } + } + } + } + + if (customBlendColor) { + chargingPaint.color = + if (chargingColor == black) Color.TRANSPARENT else chargingColor + + powerSavePaint.color = + if (powerSaveColor == black) context.resources.getColorStateList( + context.resources.getIdentifier( + "batterymeter_plus_color", "color", context.packageName + ), context.theme + ).defaultColor else powerSaveColor + + powerSaveFillPaint.color = + if (powerSaveFillColor == black) Color.TRANSPARENT else powerSaveFillColor + } else { + chargingPaint.color = Color.TRANSPARENT + powerSavePaint.color = context.resources.getColorStateList( + context.resources.getIdentifier( + "batterymeter_plus_color", "color", context.packageName + ), context.theme + ).defaultColor + powerSaveFillPaint.color = Color.TRANSPARENT + } + + if (charging) { + if (!showPercent && !isRotation && !customChargingIcon) { + c.clipOutPath(scaledBolt) + c.drawPath(levelPath, chargingPaint) + if (invertFillIcon) { + c.drawPath(scaledBolt, fillColorStrokePaint) + } else { + c.drawPath(scaledBolt, fillColorStrokeProtection) + } + } else { + c.drawPath(levelPath, chargingPaint) + } + } else if (powerSaveEnabled) { + // If power save is enabled draw the perimeter path with colorError + c.drawPath(scaledErrorPerimeter, powerSavePaint) + c.drawPath(levelPath, powerSaveFillPaint) + // And draw the plus sign on top of the fill + if (!showPercent) { + c.drawPath(scaledPlus, powerSavePaint) + } + } + c.restore() + + if (showPercent) { + textPaint.textSize = bounds.width() * 0.26f + val textHeight = +textPaint.fontMetrics.ascent + val pctX = (bounds.width() + textHeight) * 0.6f + val pctY = bounds.height() * 0.66f + + textPaint.color = fillColor + if (isRotation) { + c.rotate(180f, pctX + 0.7f, pctY * 0.76f) + } + c.drawText(batteryLevel.toString(), pctX, pctY, textPaint) + c.restore() + } + } + + private fun batteryColorForLevel(level: Int): Int { + return when { + charging || powerSaveEnabled -> fillColor + else -> getColorForLevel(level) + } + } + + private fun getColorForLevel(level: Int): Int { + var thresh: Int + var color = 0 + var i = 0 + while (i < colorLevels.size) { + thresh = colorLevels[i] + color = colorLevels[i + 1] + if (level <= thresh) { + + // Respect tinting for "normal" level + return if (i == colorLevels.size - 2) { + fillColor + } else { + color + } + } + i += 2 + } + return color + } + + /** + * Alpha is unused internally, and should be defined in the colors passed to {@link setColors}. + * Further, setting an alpha for a dual tone battery meter doesn't make sense without bounds + * defining the minimum background fill alpha. This is because fill + background must be equal + * to the net alpha passed in here. + */ + override fun setAlpha(alpha: Int) { + } + + override fun setColorFilter(colorFilter: ColorFilter?) { + fillPaint.colorFilter = colorFilter + fillColorStrokePaint.colorFilter = colorFilter + dualToneBackgroundFill.colorFilter = colorFilter + } + + /** + * Deprecated, but required by Drawable + */ + @Deprecated( + "Deprecated in Java", + ReplaceWith("PixelFormat.OPAQUE", "android.graphics.PixelFormat"), + ) + override fun getOpacity(): Int { + return PixelFormat.OPAQUE + } + + override fun getIntrinsicHeight(): Int { + return intrinsicHeight + } + + override fun getIntrinsicWidth(): Int { + return intrinsicWidth + } + + /** + * Set the fill level + */ + @Suppress("PARAMETER_NAME_CHANGED_ON_OVERRIDE") + override fun setBatteryLevel(l: Int) { + // invertFillIcon = if (l >= 67) true else if (l <= 33) false else invertFillIcon + batteryLevel = l + levelColor = batteryColorForLevel(batteryLevel) + invalidateSelf() + } + + fun getBatteryLevel(): Int { + return batteryLevel + } + + override fun onBoundsChange(bounds: Rect) { + super.onBoundsChange(bounds) + updateSize() + } + + fun setPadding(left: Int, top: Int, right: Int, bottom: Int) { + padding.left = left + padding.top = top + padding.right = right + padding.bottom = bottom + + updateSize() + } + + override fun setColors(fgColor: Int, bgColor: Int, singleToneColor: Int) { + fillColor = if (dualTone) fgColor else singleToneColor + + fillPaint.color = fillColor + fillColorStrokePaint.color = fillColor + + fillPercentPaint.color = fillColor + fillPercentPaint.alpha = 75 + + scaledFillPaint.color = fillColor + scaledPerimeterPaint.color = fillColor + scaledPerimeterPaintDef.color = fillColor + + backgroundColor = bgColor + dualToneBackgroundFill.color = bgColor + + // Also update the level color, since fillColor may have changed + levelColor = batteryColorForLevel(batteryLevel) + + invalidateSelf() + } + + private fun postInvalidate() { + unscheduleSelf(invalidateRunnable) + scheduleSelf(invalidateRunnable, 0) + } + + private fun updateSize() { + val b = bounds + if (b.isEmpty) { + scaleMatrix.setScale(1f, 1f) + } else { + scaleMatrix.setScale((b.right / WIDTH), (b.bottom / HEIGHT)) + } + + perimeterPath.transform(scaleMatrix, scaledPerimeter) + errorPerimeterPath.transform(scaleMatrix, scaledErrorPerimeter) + fillMask.transform(scaleMatrix, scaledFill) + scaledFill.computeBounds(fillRect, true) + boltPath.transform(scaleMatrix, scaledBolt) + plusPath.transform(scaleMatrix, scaledPlus) + + // It is expected that this view only ever scale by the same factor in each dimension, so + // just pick one to scale the strokeWidths + val scaledStrokeWidth = + (b.right / WIDTH * PROTECTION_STROKE_WIDTH).coerceAtLeast(PROTECTION_MIN_STROKE_WIDTH) + + fillColorStrokePaint.strokeWidth = scaledStrokeWidth + fillColorStrokeProtection.strokeWidth = scaledStrokeWidth + } + + @SuppressLint("RestrictedApi") + private fun loadPaths() { + val pathString = modRes.getString(R.string.config_landscapeBatteryPerimeterPathC) + perimeterPath.set(PathParser.createPathFromPathData(pathString)) + perimeterPath.computeBounds(RectF(), true) + + val errorPathString = modRes.getString(R.string.config_landscapeBatteryErrorPerimeterPathC) + errorPerimeterPath.set(PathParser.createPathFromPathData(errorPathString)) + errorPerimeterPath.computeBounds(RectF(), true) + + val fillMaskString = modRes.getString(R.string.config_landscapeBatteryFillMaskC) + fillMask.set(PathParser.createPathFromPathData(fillMaskString)) + // Set the fill rect so we can calculate the fill properly + fillMask.computeBounds(fillRect, true) + + val boltPathString = modRes.getString(R.string.config_landscapeBatteryBoltPathC) + boltPath.set(PathParser.createPathFromPathData(boltPathString)) + + val plusPathString = modRes.getString(R.string.config_landscapeBatteryPowersavePathC) + plusPath.set(PathParser.createPathFromPathData(plusPathString)) + + dualTone = false + } + + companion object { + private const val TAG = "LandscapeBatteryC" + private const val WIDTH = 20f + private const val HEIGHT = 12f + private const val CRITICAL_LEVEL = 15 + + // On a 20x12 grid, how wide to make the fill protection stroke. + // Scales when our size changes + private const val PROTECTION_STROKE_WIDTH = 3f + + // Arbitrarily chosen for visibility at small sizes + private const val PROTECTION_MIN_STROKE_WIDTH = 6f + } +} diff --git a/app/src/main/java/com/drdisagree/iconify/xposed/mods/batterystyles/LandscapeBatteryD.kt b/app/src/main/java/com/drdisagree/iconify/xposed/mods/batterystyles/LandscapeBatteryD.kt new file mode 100644 index 000000000..c56b0ca89 --- /dev/null +++ b/app/src/main/java/com/drdisagree/iconify/xposed/mods/batterystyles/LandscapeBatteryD.kt @@ -0,0 +1,696 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.drdisagree.iconify.xposed.mods.batterystyles + +import android.annotation.SuppressLint +import android.content.Context +import android.graphics.BlendMode +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.ColorFilter +import android.graphics.LinearGradient +import android.graphics.Matrix +import android.graphics.Paint +import android.graphics.Path +import android.graphics.PixelFormat +import android.graphics.Rect +import android.graphics.RectF +import android.graphics.Shader +import android.graphics.Typeface +import android.util.TypedValue +import androidx.core.graphics.PathParser +import com.drdisagree.iconify.R +import com.drdisagree.iconify.xposed.HookRes.modRes +import com.drdisagree.iconify.xposed.utils.SettingsLibUtils +import kotlin.math.floor + +/** + * A battery meter drawable that respects paths configured in + * frameworks/base/core/res/res/values/config.xml to allow for an easily overrideable battery icon + */ +@SuppressLint("DiscouragedApi") +open class LandscapeBatteryD(private val context: Context, frameColor: Int) : BatteryDrawable() { + + // Need to load: + // 1. perimeter shape + // 2. fill mask (if smaller than perimeter, this would create a fill that + // doesn't touch the walls + private val perimeterPath = Path() + private val scaledPerimeter = Path() + private val errorPerimeterPath = Path() + private val scaledErrorPerimeter = Path() + + // Fill will cover the whole bounding rect of the fillMask, and be masked by the path + private val fillMask = Path() + private val scaledFill = Path() + private val fillOutlinePath = Path() + private val scaledfillOutline = Path() + + // Based off of the mask, the fill will interpolate across this space + private val fillRect = RectF() + + // Top of this rect changes based on level, 100% == fillRect + private val levelRect = RectF() + private val levelPath = Path() + + // Updates the transform of the paths when our bounds change + private val scaleMatrix = Matrix() + private val padding = Rect() + + // The net result of fill + perimeter paths + private val unifiedPath = Path() + + // Bolt path (used while charging) + private val boltPath = Path() + private val scaledBolt = Path() + + // Plus sign (used for power save mode) + private val plusPath = Path() + private val scaledPlus = Path() + + private var intrinsicHeight: Int + private var intrinsicWidth: Int + + // To implement hysteresis, keep track of the need to invert the interior icon of the battery + private var invertFillIcon = false + + // Colors can be configured based on battery level (see res/values/arrays.xml) + private var colorLevels: IntArray + + private var fillColor: Int = Color.MAGENTA + private var backgroundColor: Int = Color.MAGENTA + + // updated whenever level changes + private var levelColor: Int = Color.MAGENTA + + // Dual tone implies that battery level is a clipped overlay over top of the whole shape + private var dualTone = false + + private var batteryLevel = 0 + + private var scaledFillAlpha = false + private var scaledPerimeterAlpha = false + private var customBlendColor = false + + private var chargingColor: Int = Color.TRANSPARENT + private var customFillColor: Int = Color.BLACK + private var customFillGradColor: Int = Color.BLACK + private var powerSaveColor: Int = Color.TRANSPARENT + private var powerSaveFillColor: Int = Color.TRANSPARENT + + private val invalidateRunnable: () -> Unit = { + invalidateSelf() + } + + open var criticalLevel: Int = 5 + + var charging = false + set(value) { + field = value + postInvalidate() + } + + override fun setChargingEnabled(charging: Boolean) { + this.charging = charging + postInvalidate() + } + + var powerSaveEnabled = false + set(value) { + field = value + postInvalidate() + } + + override fun setPowerSavingEnabled(powerSaveEnabled: Boolean) { + this.powerSaveEnabled = powerSaveEnabled + postInvalidate() + } + + var showPercent = false + set(value) { + field = value + postInvalidate() + } + + override fun setShowPercentEnabled(showPercent: Boolean) { + this.showPercent = showPercent + postInvalidate() + } + + var customChargingIcon = false + set(value) { + field = value + postInvalidate() + } + + open fun customizeBatteryDrawable( + scaledPerimeterAlpha: Boolean, + scaledFillAlpha: Boolean, + customBlendColor: Boolean, + customFillColor: Int, + customFillGradColor: Int, + chargingColor: Int, + powerSaveColor: Int, + powerSaveFillColor: Int + ) { + this.scaledPerimeterAlpha = scaledPerimeterAlpha + this.scaledFillAlpha = scaledFillAlpha + this.customBlendColor = customBlendColor + this.customFillColor = customFillColor + this.customFillGradColor = customFillGradColor + this.chargingColor = chargingColor + this.powerSaveColor = powerSaveColor + this.powerSaveFillColor = powerSaveFillColor + } + + private val fillColorStrokePaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + p.alpha = 255 + p.isDither = true + p.strokeWidth = 5f + p.style = Paint.Style.STROKE + p.blendMode = BlendMode.SRC + p.strokeMiter = 5f + p.strokeJoin = Paint.Join.ROUND + } + + private val fillColorStrokeProtection = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.isDither = true + p.strokeWidth = 5f + p.style = Paint.Style.STROKE + p.blendMode = BlendMode.CLEAR + p.strokeMiter = 5f + p.strokeJoin = Paint.Join.ROUND + } + + private val fillPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + p.alpha = 255 + p.isDither = true + p.strokeWidth = 0f + p.style = Paint.Style.FILL_AND_STROKE + } + + private val errorPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = context.resources.getColorStateList( + context.resources.getIdentifier( + "batterymeter_plus_color", "color", context.packageName + ), context.theme + ).defaultColor + p.alpha = 255 + p.isDither = true + p.strokeWidth = 0f + p.style = Paint.Style.FILL_AND_STROKE + p.blendMode = BlendMode.SRC + } + + private val chargingPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + } + + private val customFillPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + } + + private val powerSavePaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + } + + private val powerSaveFillPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + } + + private val scaledFillPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + } + + private val scaledPerimeterPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + } + + private val scaledPerimeterPaintDef = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + } + + // Only used if dualTone is set to true + private val dualToneBackgroundFill = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + p.alpha = 85 // ~0.3 alpha by default + p.isDither = true + p.strokeWidth = 0f + p.style = Paint.Style.FILL_AND_STROKE + } + + private val textPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.typeface = Typeface.create("sans-serif-condensed", Typeface.BOLD) + p.textAlign = Paint.Align.CENTER + } + + init { + val density = context.resources.displayMetrics.density + intrinsicHeight = (HEIGHT * density).toInt() + intrinsicWidth = (WIDTH * density).toInt() + + val res = context.resources + val levels = res.obtainTypedArray( + res.getIdentifier( + "batterymeter_color_levels", "array", context.packageName + ) + ) + val colors = res.obtainTypedArray( + res.getIdentifier( + "batterymeter_color_values", "array", context.packageName + ) + ) + val n = levels.length() + colorLevels = IntArray(2 * n) + for (i in 0 until n) { + colorLevels[2 * i] = levels.getInt(i, 0) + if (colors.getType(i) == TypedValue.TYPE_ATTRIBUTE) { + colorLevels[2 * i + 1] = SettingsLibUtils.getColorAttrDefaultColor( + colors.getResourceId(i, 0), context + ) + } else { + colorLevels[2 * i + 1] = colors.getColor(i, 0) + } + } + levels.recycle() + colors.recycle() + + loadPaths() + } + + override fun draw(c: Canvas) { + c.saveLayer(null, null) + unifiedPath.reset() + levelPath.reset() + levelRect.set(fillRect) + val fillFraction = batteryLevel / 100f + val fillRight = + if (batteryLevel == 100) + fillRect.right + 1f + else + fillRect.right - (fillRect.width() * (1 - fillFraction)) + val fillBottom = fillRect.bottom + 1f + + levelRect.right = floor(fillRight.toDouble()).toFloat() + levelRect.bottom = floor(fillBottom.toDouble()).toFloat() + levelPath.addRect(levelRect, Path.Direction.CCW) + + scaledFillPaint.alpha = if (scaledFillAlpha) 100 else 0 + scaledPerimeterPaint.alpha = + if (scaledPerimeterAlpha) 100 else scaledPerimeterPaintDef.alpha + + // The perimeter should never change + c.drawPath(scaledFill, scaledFillPaint) + c.drawPath(scaledPerimeter, scaledPerimeterPaint) + // If drawing dual tone, the level is used only to clip the whole drawable path + if (!dualTone) { + unifiedPath.op(levelPath, Path.Op.UNION) + } + + fillPaint.color = levelColor + val black = Color.BLACK + + if (customBlendColor) { + chargingPaint.color = + if (chargingColor == black) Color.TRANSPARENT else chargingColor + + powerSavePaint.color = + if (powerSaveColor == black) context.resources.getColorStateList( + context.resources.getIdentifier( + "batterymeter_plus_color", "color", context.packageName + ), context.theme + ).defaultColor else powerSaveColor + + powerSaveFillPaint.color = + if (powerSaveFillColor == black) Color.TRANSPARENT else powerSaveFillColor + } else { + chargingPaint.color = Color.TRANSPARENT + powerSavePaint.color = context.resources.getColorStateList( + context.resources.getIdentifier( + "batterymeter_plus_color", "color", context.packageName + ), context.theme + ).defaultColor + powerSaveFillPaint.color = Color.TRANSPARENT + } + + // Deal with unifiedPath clipping before it draws + if (charging && !customChargingIcon) { + // Clip out the bolt shape + unifiedPath.op(scaledBolt, Path.Op.DIFFERENCE) + if (!invertFillIcon) { + c.drawPath(scaledBolt, fillPaint) + } + } + + if (dualTone) { + // Dual tone means we draw the shape again, clipped to the charge level + c.drawPath(unifiedPath, dualToneBackgroundFill) + c.save() + c.clipRect( + bounds.left.toFloat(), + 0f, + bounds.right + bounds.width() * fillFraction, + bounds.left.toFloat() + ) + c.drawPath(unifiedPath, fillPaint) + c.restore() + } else { + // Non dual-tone means we draw the perimeter (with the level fill), and potentially + // draw the fill again with a critical color + if (customBlendColor) { + if (charging) { + fillPaint.color = fillColor + c.clipOutPath(scaledfillOutline) + c.drawPath(unifiedPath, fillPaint) + fillPaint.color = levelColor + } else if (powerSaveEnabled) { + c.drawPath(scaledErrorPerimeter, powerSavePaint) + c.clipOutPath(scaledfillOutline) + c.drawPath(levelPath, fillPaint) + c.drawPath(levelPath, powerSaveFillPaint) + } else { + c.clipOutPath(scaledfillOutline) + customFillPaint.color = customFillColor + customFillPaint.shader = + if (customFillColor != black && customFillGradColor != black) LinearGradient( + levelRect.right, 0f, 0f, levelRect.bottom, + customFillColor, customFillGradColor, + Shader.TileMode.CLAMP + ) else null + c.drawPath( + levelPath, + if (customFillColor == black) fillPaint else customFillPaint + ) + } + } else { + if (charging) { + fillPaint.color = fillColor + c.clipOutPath(scaledfillOutline) + c.drawPath(unifiedPath, fillPaint) + fillPaint.color = levelColor + } else if (powerSaveEnabled) { + c.drawPath(scaledErrorPerimeter, errorPaint) + c.clipOutPath(scaledfillOutline) + c.drawPath(levelPath, fillPaint) + } else { + fillPaint.color = fillColor + c.clipOutPath(scaledfillOutline) + c.drawPath(unifiedPath, fillPaint) + fillPaint.color = levelColor + } + } + + // Show colorError below this level + if (batteryLevel <= CRITICAL_LEVEL && !charging) { + c.save() + c.clipPath(scaledFill) + c.drawPath(levelPath, fillPaint) + c.restore() + } + } + + if (charging) { + if (!customChargingIcon) { + c.clipOutPath(scaledBolt) + c.drawPath(levelPath, chargingPaint) + if (invertFillIcon) { + c.drawPath(scaledBolt, fillColorStrokePaint) + } else { + c.drawPath(scaledBolt, fillColorStrokeProtection) + } + } else { + c.drawPath(levelPath, chargingPaint) + } + } else if (powerSaveEnabled) { + // If power save is enabled draw the perimeter path with colorError + c.drawPath(scaledErrorPerimeter, powerSavePaint) + c.drawPath(levelPath, powerSaveFillPaint) + // And draw the plus sign on top of the fill + if (!showPercent) { + c.drawPath(scaledPlus, powerSavePaint) + } + } + c.restore() + + var state = false + if (customChargingIcon && charging) { + state = true + } else if (customChargingIcon && !charging) { + state = true + } else if (!customChargingIcon && charging) { + state = false + } else if (!customChargingIcon && !charging) { + state = true + } + + if (showPercent && state) { + textPaint.textSize = bounds.width() * 0.20f + val textHeight = +textPaint.fontMetrics.ascent + val pctX = (bounds.width() + textHeight) * 0.42f + val pctY = bounds.height() * 0.54f + + if (customBlendColor) { + if (powerSaveEnabled && powerSaveFillColor == black) { + textPaint.color = fillColor + c.drawText(batteryLevel.toString(), pctX, pctY, textPaint) + + textPaint.color = fillColor.toInt().inv() or 0xFF000000.toInt() + c.save() + c.clipRect( + fillRect.left, + fillRect.top, + fillRect.right - (fillRect.width() * (1 - fillFraction)), + fillRect.bottom + ) + c.drawText(batteryLevel.toString(), pctX, pctY, textPaint) + } else if (!powerSaveEnabled && customFillColor == black) { + textPaint.color = fillColor + c.drawText(batteryLevel.toString(), pctX, pctY, textPaint) + + textPaint.color = fillColor.toInt().inv() or 0xFF000000.toInt() + c.save() + c.clipRect( + fillRect.left, + fillRect.top, + fillRect.right - (fillRect.width() * (1 - fillFraction)), + fillRect.bottom + ) + c.drawText(batteryLevel.toString(), pctX, pctY, textPaint) + } else { + textPaint.color = fillColor + c.drawText(batteryLevel.toString(), pctX, pctY, textPaint) + } + } else { + textPaint.color = fillColor + c.drawText(batteryLevel.toString(), pctX, pctY, textPaint) + + textPaint.color = fillColor.toInt().inv() or 0xFF000000.toInt() + c.save() + c.clipRect( + fillRect.left, + fillRect.top, + fillRect.right - (fillRect.width() * (1 - fillFraction)), + fillRect.bottom + ) + c.drawText(batteryLevel.toString(), pctX, pctY, textPaint) + } + c.restore() + } + } + + private fun batteryColorForLevel(level: Int): Int { + return when { + charging || powerSaveEnabled -> fillColor + else -> getColorForLevel(level) + } + } + + private fun getColorForLevel(level: Int): Int { + var thresh: Int + var color = 0 + var i = 0 + while (i < colorLevels.size) { + thresh = colorLevels[i] + color = colorLevels[i + 1] + if (level <= thresh) { + + // Respect tinting for "normal" level + return if (i == colorLevels.size - 2) { + fillColor + } else { + color + } + } + i += 2 + } + return color + } + + /** + * Alpha is unused internally, and should be defined in the colors passed to {@link setColors}. + * Further, setting an alpha for a dual tone battery meter doesn't make sense without bounds + * defining the minimum background fill alpha. This is because fill + background must be equal + * to the net alpha passed in here. + */ + override fun setAlpha(alpha: Int) { + } + + override fun setColorFilter(colorFilter: ColorFilter?) { + fillPaint.colorFilter = colorFilter + fillColorStrokePaint.colorFilter = colorFilter + dualToneBackgroundFill.colorFilter = colorFilter + } + + /** + * Deprecated, but required by Drawable + */ + @Deprecated( + "Deprecated in Java", + ReplaceWith("PixelFormat.OPAQUE", "android.graphics.PixelFormat"), + ) + override fun getOpacity(): Int { + return PixelFormat.OPAQUE + } + + override fun getIntrinsicHeight(): Int { + return intrinsicHeight + } + + override fun getIntrinsicWidth(): Int { + return intrinsicWidth + } + + /** + * Set the fill level + */ + @Suppress("PARAMETER_NAME_CHANGED_ON_OVERRIDE") + override fun setBatteryLevel(l: Int) { + // invertFillIcon = if (l >= 67) true else if (l <= 33) false else invertFillIcon + batteryLevel = l + levelColor = batteryColorForLevel(batteryLevel) + invalidateSelf() + } + + fun getBatteryLevel(): Int { + return batteryLevel + } + + override fun onBoundsChange(bounds: Rect) { + super.onBoundsChange(bounds) + updateSize() + } + + fun setPadding(left: Int, top: Int, right: Int, bottom: Int) { + padding.left = left + padding.top = top + padding.right = right + padding.bottom = bottom + + updateSize() + } + + override fun setColors(fgColor: Int, bgColor: Int, singleToneColor: Int) { + fillColor = if (dualTone) fgColor else singleToneColor + + fillPaint.color = fillColor + fillColorStrokePaint.color = fillColor + + scaledFillPaint.color = fillColor + scaledPerimeterPaint.color = fillColor + scaledPerimeterPaintDef.color = fillColor + + backgroundColor = bgColor + dualToneBackgroundFill.color = bgColor + + // Also update the level color, since fillColor may have changed + levelColor = batteryColorForLevel(batteryLevel) + + invalidateSelf() + } + + private fun postInvalidate() { + unscheduleSelf(invalidateRunnable) + scheduleSelf(invalidateRunnable, 0) + } + + private fun updateSize() { + val b = bounds + if (b.isEmpty) { + scaleMatrix.setScale(1f, 1f) + } else { + scaleMatrix.setScale((b.right / WIDTH), (b.bottom / HEIGHT)) + } + + perimeterPath.transform(scaleMatrix, scaledPerimeter) + errorPerimeterPath.transform(scaleMatrix, scaledErrorPerimeter) + fillMask.transform(scaleMatrix, scaledFill) + scaledFill.computeBounds(fillRect, true) + fillOutlinePath.transform(scaleMatrix, scaledfillOutline) + boltPath.transform(scaleMatrix, scaledBolt) + plusPath.transform(scaleMatrix, scaledPlus) + + // It is expected that this view only ever scale by the same factor in each dimension, so + // just pick one to scale the strokeWidths + val scaledStrokeWidth = + (b.right / WIDTH * PROTECTION_STROKE_WIDTH).coerceAtLeast(PROTECTION_MIN_STROKE_WIDTH) + + fillColorStrokePaint.strokeWidth = scaledStrokeWidth + fillColorStrokeProtection.strokeWidth = scaledStrokeWidth + } + + @SuppressLint("RestrictedApi") + private fun loadPaths() { + val pathString = modRes.getString(R.string.config_landscapeBatteryPerimeterPathD) + perimeterPath.set(PathParser.createPathFromPathData(pathString)) + perimeterPath.computeBounds(RectF(), true) + + val errorPathString = modRes.getString(R.string.config_landscapeBatteryErrorPerimeterPathD) + errorPerimeterPath.set(PathParser.createPathFromPathData(errorPathString)) + errorPerimeterPath.computeBounds(RectF(), true) + + val fillMaskString = modRes.getString(R.string.config_landscapeBatteryFillMaskD) + fillMask.set(PathParser.createPathFromPathData(fillMaskString)) + // Set the fill rect so we can calculate the fill properly + fillMask.computeBounds(fillRect, true) + + val fillOutlinePathString = modRes.getString(R.string.config_landscapeBatteryFillOutlineD) + fillOutlinePath.set(PathParser.createPathFromPathData(fillOutlinePathString)) + fillOutlinePath.computeBounds(RectF(), true) + + val boltPathString = modRes.getString(R.string.config_landscapeBatteryBoltPathD) + boltPath.set(PathParser.createPathFromPathData(boltPathString)) + + val plusPathString = modRes.getString(R.string.config_landscapeBatteryPowersavePathD) + plusPath.set(PathParser.createPathFromPathData(plusPathString)) + + dualTone = false + } + + companion object { + private const val TAG = "LandscapeBatteryD" + private const val WIDTH = 20f + private const val HEIGHT = 14f + private const val CRITICAL_LEVEL = 15 + + // On a 20x14 grid, how wide to make the fill protection stroke. + // Scales when our size changes + private const val PROTECTION_STROKE_WIDTH = 2f + + // Arbitrarily chosen for visibility at small sizes + private const val PROTECTION_MIN_STROKE_WIDTH = 5f + } +} diff --git a/app/src/main/java/com/drdisagree/iconify/xposed/mods/batterystyles/LandscapeBatteryE.kt b/app/src/main/java/com/drdisagree/iconify/xposed/mods/batterystyles/LandscapeBatteryE.kt new file mode 100644 index 000000000..c4f4b0311 --- /dev/null +++ b/app/src/main/java/com/drdisagree/iconify/xposed/mods/batterystyles/LandscapeBatteryE.kt @@ -0,0 +1,694 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.drdisagree.iconify.xposed.mods.batterystyles + +import android.annotation.SuppressLint +import android.content.Context +import android.graphics.BlendMode +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.ColorFilter +import android.graphics.LinearGradient +import android.graphics.Matrix +import android.graphics.Paint +import android.graphics.Path +import android.graphics.PixelFormat +import android.graphics.Rect +import android.graphics.RectF +import android.graphics.Shader +import android.graphics.Typeface +import android.util.TypedValue +import androidx.core.graphics.PathParser +import com.drdisagree.iconify.R +import com.drdisagree.iconify.xposed.HookRes.modRes +import com.drdisagree.iconify.xposed.utils.SettingsLibUtils +import kotlin.math.floor + +/** + * A battery meter drawable that respects paths configured in + * frameworks/base/core/res/res/values/config.xml to allow for an easily overrideable battery icon + */ +@SuppressLint("DiscouragedApi") +open class LandscapeBatteryE(private val context: Context, frameColor: Int) : BatteryDrawable() { + + // Need to load: + // 1. perimeter shape + // 2. fill mask (if smaller than perimeter, this would create a fill that + // doesn't touch the walls + private val perimeterPath = Path() + private val scaledPerimeter = Path() + private val errorPerimeterPath = Path() + private val scaledErrorPerimeter = Path() + + // Fill will cover the whole bounding rect of the fillMask, and be masked by the path + private val fillMask = Path() + private val scaledFill = Path() + private val fillOutlinePath = Path() + private val scaledfillOutline = Path() + + // Based off of the mask, the fill will interpolate across this space + private val fillRect = RectF() + + // Top of this rect changes based on level, 100% == fillRect + private val levelRect = RectF() + private val levelPath = Path() + + // Updates the transform of the paths when our bounds change + private val scaleMatrix = Matrix() + private val padding = Rect() + + // The net result of fill + perimeter paths + private val unifiedPath = Path() + + // Bolt path (used while charging) + private val boltPath = Path() + private val scaledBolt = Path() + + // Plus sign (used for power save mode) + private val plusPath = Path() + private val scaledPlus = Path() + + private var intrinsicHeight: Int + private var intrinsicWidth: Int + + // To implement hysteresis, keep track of the need to invert the interior icon of the battery + private var invertFillIcon = false + + // Colors can be configured based on battery level (see res/values/arrays.xml) + private var colorLevels: IntArray + + private var fillColor: Int = Color.MAGENTA + private var backgroundColor: Int = Color.MAGENTA + + // updated whenever level changes + private var levelColor: Int = Color.MAGENTA + + // Dual tone implies that battery level is a clipped overlay over top of the whole shape + private var dualTone = false + + private var batteryLevel = 0 + + private var scaledFillAlpha = false + private var scaledPerimeterAlpha = false + private var customBlendColor = false + + private var chargingColor: Int = Color.TRANSPARENT + private var customFillColor: Int = Color.BLACK + private var customFillGradColor: Int = Color.BLACK + private var powerSaveColor: Int = Color.TRANSPARENT + private var powerSaveFillColor: Int = Color.TRANSPARENT + + private val invalidateRunnable: () -> Unit = { + invalidateSelf() + } + + open var criticalLevel: Int = 5 + + var charging = false + set(value) { + field = value + postInvalidate() + } + + override fun setChargingEnabled(charging: Boolean) { + this.charging = charging + postInvalidate() + } + + var powerSaveEnabled = false + set(value) { + field = value + postInvalidate() + } + + override fun setPowerSavingEnabled(powerSaveEnabled: Boolean) { + this.powerSaveEnabled = powerSaveEnabled + postInvalidate() + } + + var showPercent = false + set(value) { + field = value + postInvalidate() + } + + override fun setShowPercentEnabled(showPercent: Boolean) { + this.showPercent = showPercent + postInvalidate() + } + + var customChargingIcon = false + set(value) { + field = value + postInvalidate() + } + + open fun customizeBatteryDrawable( + scaledPerimeterAlpha: Boolean, + scaledFillAlpha: Boolean, + customBlendColor: Boolean, + customFillColor: Int, + customFillGradColor: Int, + chargingColor: Int, + powerSaveColor: Int, + powerSaveFillColor: Int + ) { + this.scaledPerimeterAlpha = scaledPerimeterAlpha + this.scaledFillAlpha = scaledFillAlpha + this.customBlendColor = customBlendColor + this.customFillColor = customFillColor + this.customFillGradColor = customFillGradColor + this.chargingColor = chargingColor + this.powerSaveColor = powerSaveColor + this.powerSaveFillColor = powerSaveFillColor + } + + private val fillColorStrokePaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + p.alpha = 255 + p.isDither = true + p.strokeWidth = 5f + p.style = Paint.Style.STROKE + p.blendMode = BlendMode.SRC + p.strokeMiter = 5f + p.strokeJoin = Paint.Join.ROUND + } + + private val fillColorStrokeProtection = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.isDither = true + p.strokeWidth = 5f + p.style = Paint.Style.STROKE + p.blendMode = BlendMode.CLEAR + p.strokeMiter = 5f + p.strokeJoin = Paint.Join.ROUND + } + + private val fillPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + p.alpha = 255 + p.isDither = true + p.strokeWidth = 0f + p.style = Paint.Style.FILL_AND_STROKE + } + + private val errorPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = context.resources.getColorStateList( + context.resources.getIdentifier( + "batterymeter_plus_color", "color", context.packageName + ), context.theme + ).defaultColor + p.alpha = 255 + p.isDither = true + p.strokeWidth = 0f + p.style = Paint.Style.FILL_AND_STROKE + p.blendMode = BlendMode.SRC + } + + private val chargingPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + } + + private val customFillPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + } + + private val powerSavePaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + } + + private val powerSaveFillPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + } + + private val scaledFillPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + } + + private val scaledPerimeterPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + } + + private val scaledPerimeterPaintDef = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + } + + // Only used if dualTone is set to true + private val dualToneBackgroundFill = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + p.alpha = 85 // ~0.3 alpha by default + p.isDither = true + p.strokeWidth = 0f + p.style = Paint.Style.FILL_AND_STROKE + } + + private val textPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.typeface = Typeface.create("sans-serif-condensed", Typeface.BOLD) + p.textAlign = Paint.Align.CENTER + } + + init { + val density = context.resources.displayMetrics.density + intrinsicHeight = (HEIGHT * density).toInt() + intrinsicWidth = (WIDTH * density).toInt() + + val res = context.resources + val levels = res.obtainTypedArray( + res.getIdentifier( + "batterymeter_color_levels", "array", context.packageName + ) + ) + val colors = res.obtainTypedArray( + res.getIdentifier( + "batterymeter_color_values", "array", context.packageName + ) + ) + val n = levels.length() + colorLevels = IntArray(2 * n) + for (i in 0 until n) { + colorLevels[2 * i] = levels.getInt(i, 0) + if (colors.getType(i) == TypedValue.TYPE_ATTRIBUTE) { + colorLevels[2 * i + 1] = SettingsLibUtils.getColorAttrDefaultColor( + colors.getResourceId(i, 0), context + ) + } else { + colorLevels[2 * i + 1] = colors.getColor(i, 0) + } + } + levels.recycle() + colors.recycle() + + loadPaths() + } + + override fun draw(c: Canvas) { + c.saveLayer(null, null) + unifiedPath.reset() + levelPath.reset() + levelRect.set(fillRect) + val fillFraction = batteryLevel / 100f + val fillLeft = + if (batteryLevel == 100) + fillRect.left + else + fillRect.left + (fillRect.width() * (1 - fillFraction)) + + levelRect.left = floor(fillLeft.toDouble()).toFloat() + levelPath.addRect(levelRect, Path.Direction.CCW) + + scaledFillPaint.alpha = if (scaledFillAlpha) 100 else 0 + scaledPerimeterPaint.alpha = + if (scaledPerimeterAlpha) 100 else scaledPerimeterPaintDef.alpha + + // The perimeter should never change + c.drawPath(scaledFill, scaledFillPaint) + c.drawPath(scaledPerimeter, scaledPerimeterPaint) + // If drawing dual tone, the level is used only to clip the whole drawable path + if (!dualTone) { + unifiedPath.op(levelPath, Path.Op.UNION) + } + + fillPaint.color = levelColor + val black = Color.BLACK + + if (customBlendColor) { + chargingPaint.color = + if (chargingColor == Color.BLACK) Color.TRANSPARENT else chargingColor + + powerSavePaint.color = + if (powerSaveColor == Color.BLACK) context.resources.getColorStateList( + context.resources.getIdentifier( + "batterymeter_plus_color", "color", context.packageName + ), context.theme + ).defaultColor else powerSaveColor + + powerSaveFillPaint.color = + if (powerSaveFillColor == Color.BLACK) Color.TRANSPARENT else powerSaveFillColor + } else { + chargingPaint.color = Color.TRANSPARENT + powerSavePaint.color = context.resources.getColorStateList( + context.resources.getIdentifier( + "batterymeter_plus_color", "color", context.packageName + ), context.theme + ).defaultColor + powerSaveFillPaint.color = Color.TRANSPARENT + } + + // Deal with unifiedPath clipping before it draws + if (charging && !customChargingIcon) { + // Clip out the bolt shape + unifiedPath.op(scaledBolt, Path.Op.DIFFERENCE) + if (!invertFillIcon) { + c.drawPath(scaledBolt, fillPaint) + } + } + + if (dualTone) { + // Dual tone means we draw the shape again, clipped to the charge level + c.drawPath(unifiedPath, dualToneBackgroundFill) + c.save() + c.clipRect( + 0f, + bounds.left - bounds.width() * fillFraction, + bounds.right.toFloat(), + bounds.left.toFloat() + ) + c.drawPath(unifiedPath, fillPaint) + c.restore() + } else { + // Non dual-tone means we draw the perimeter (with the level fill), and potentially + // draw the fill again with a critical color + if (customBlendColor) { + if (charging) { + fillPaint.color = fillColor + c.clipOutPath(scaledfillOutline) + c.drawPath(unifiedPath, fillPaint) + fillPaint.color = levelColor + } else if (powerSaveEnabled) { + c.drawPath(scaledErrorPerimeter, powerSavePaint) + c.clipOutPath(scaledfillOutline) + c.drawPath(levelPath, fillPaint) + c.drawPath(levelPath, powerSaveFillPaint) + } else { + c.clipOutPath(scaledfillOutline) + customFillPaint.color = customFillColor + customFillPaint.shader = + if (customFillColor != black && customFillGradColor != black) LinearGradient( + levelRect.left, 0f, 0f, levelRect.bottom, + customFillGradColor, customFillColor, + Shader.TileMode.CLAMP + ) else null + c.drawPath( + levelPath, + if (customFillColor == Color.BLACK) fillPaint else customFillPaint + ) + } + } else { + if (charging) { + fillPaint.color = fillColor + c.clipOutPath(scaledfillOutline) + c.drawPath(unifiedPath, fillPaint) + fillPaint.color = levelColor + } else if (powerSaveEnabled) { + c.drawPath(scaledErrorPerimeter, errorPaint) + c.clipOutPath(scaledfillOutline) + c.drawPath(levelPath, fillPaint) + } else { + fillPaint.color = fillColor + c.clipOutPath(scaledfillOutline) + c.drawPath(unifiedPath, fillPaint) + fillPaint.color = levelColor + } + } + + // Show colorError below this level + if (batteryLevel <= CRITICAL_LEVEL && !charging) { + c.save() + c.clipPath(scaledFill) + c.drawPath(levelPath, fillPaint) + c.restore() + } + } + + if (charging) { + if (!customChargingIcon) { + c.clipOutPath(scaledBolt) + c.drawPath(levelPath, chargingPaint) + if (invertFillIcon) { + c.drawPath(scaledBolt, fillColorStrokePaint) + } else { + c.drawPath(scaledBolt, fillColorStrokeProtection) + } + } else { + c.drawPath(levelPath, chargingPaint) + } + } else if (powerSaveEnabled) { + // If power save is enabled draw the perimeter path with colorError + c.drawPath(scaledErrorPerimeter, powerSavePaint) + c.drawPath(levelPath, powerSaveFillPaint) + // And draw the plus sign on top of the fill + if (!showPercent) { + c.drawPath(scaledPlus, powerSavePaint) + } + } + c.restore() + + var state = false + if (customChargingIcon && charging) { + state = true + } else if (customChargingIcon && !charging) { + state = true + } else if (!customChargingIcon && charging) { + state = false + } else if (!customChargingIcon && !charging) { + state = true + } + + if (showPercent && state) { + textPaint.textSize = bounds.width() * 0.20f + val textHeight = +textPaint.fontMetrics.ascent + val pctX = (bounds.width() + textHeight) * 0.84f + val pctY = bounds.height() * 0.61f + + if (customBlendColor) { + if (powerSaveEnabled && powerSaveFillColor == Color.BLACK) { + textPaint.color = fillColor + c.drawText(batteryLevel.toString(), pctX, pctY, textPaint) + + textPaint.color = fillColor.toInt().inv() or 0xFF000000.toInt() + c.save() + c.clipRect( + fillRect.left + (fillRect.width() * (1 - fillFraction)), + fillRect.top, + fillRect.right, + fillRect.bottom + ) + c.drawText(batteryLevel.toString(), pctX, pctY, textPaint) + } else if (!powerSaveEnabled && customFillColor == Color.BLACK) { + textPaint.color = fillColor + c.drawText(batteryLevel.toString(), pctX, pctY, textPaint) + + textPaint.color = fillColor.toInt().inv() or 0xFF000000.toInt() + c.save() + c.clipRect( + fillRect.left + (fillRect.width() * (1 - fillFraction)), + fillRect.top, + fillRect.right, + fillRect.bottom + ) + c.drawText(batteryLevel.toString(), pctX, pctY, textPaint) + } else { + textPaint.color = fillColor + c.drawText(batteryLevel.toString(), pctX, pctY, textPaint) + } + } else { + textPaint.color = fillColor + c.drawText(batteryLevel.toString(), pctX, pctY, textPaint) + + textPaint.color = fillColor.toInt().inv() or 0xFF000000.toInt() + c.save() + c.clipRect( + fillRect.left + (fillRect.width() * (1 - fillFraction)), + fillRect.top, + fillRect.right, + fillRect.bottom + ) + c.drawText(batteryLevel.toString(), pctX, pctY, textPaint) + } + c.restore() + } + } + + private fun batteryColorForLevel(level: Int): Int { + return when { + charging || powerSaveEnabled -> fillColor + else -> getColorForLevel(level) + } + } + + private fun getColorForLevel(level: Int): Int { + var thresh: Int + var color = 0 + var i = 0 + while (i < colorLevels.size) { + thresh = colorLevels[i] + color = colorLevels[i + 1] + if (level <= thresh) { + + // Respect tinting for "normal" level + return if (i == colorLevels.size - 2) { + fillColor + } else { + color + } + } + i += 2 + } + return color + } + + /** + * Alpha is unused internally, and should be defined in the colors passed to {@link setColors}. + * Further, setting an alpha for a dual tone battery meter doesn't make sense without bounds + * defining the minimum background fill alpha. This is because fill + background must be equal + * to the net alpha passed in here. + */ + override fun setAlpha(alpha: Int) { + } + + override fun setColorFilter(colorFilter: ColorFilter?) { + fillPaint.colorFilter = colorFilter + fillColorStrokePaint.colorFilter = colorFilter + dualToneBackgroundFill.colorFilter = colorFilter + } + + /** + * Deprecated, but required by Drawable + */ + @Deprecated( + "Deprecated in Java", + ReplaceWith("PixelFormat.OPAQUE", "android.graphics.PixelFormat"), + ) + override fun getOpacity(): Int { + return PixelFormat.OPAQUE + } + + override fun getIntrinsicHeight(): Int { + return intrinsicHeight + } + + override fun getIntrinsicWidth(): Int { + return intrinsicWidth + } + + /** + * Set the fill level + */ + @Suppress("PARAMETER_NAME_CHANGED_ON_OVERRIDE") + override fun setBatteryLevel(l: Int) { + // invertFillIcon = if (l >= 67) true else if (l <= 33) false else invertFillIcon + batteryLevel = l + levelColor = batteryColorForLevel(batteryLevel) + invalidateSelf() + } + + fun getBatteryLevel(): Int { + return batteryLevel + } + + override fun onBoundsChange(bounds: Rect) { + super.onBoundsChange(bounds) + updateSize() + } + + fun setPadding(left: Int, top: Int, right: Int, bottom: Int) { + padding.left = left + padding.top = top + padding.right = right + padding.bottom = bottom + + updateSize() + } + + override fun setColors(fgColor: Int, bgColor: Int, singleToneColor: Int) { + fillColor = if (dualTone) fgColor else singleToneColor + + fillPaint.color = fillColor + fillColorStrokePaint.color = fillColor + + scaledFillPaint.color = fillColor + scaledPerimeterPaint.color = fillColor + scaledPerimeterPaintDef.color = fillColor + + backgroundColor = bgColor + dualToneBackgroundFill.color = bgColor + + // Also update the level color, since fillColor may have changed + levelColor = batteryColorForLevel(batteryLevel) + + invalidateSelf() + } + + private fun postInvalidate() { + unscheduleSelf(invalidateRunnable) + scheduleSelf(invalidateRunnable, 0) + } + + private fun updateSize() { + val b = bounds + if (b.isEmpty) { + scaleMatrix.setScale(1f, 1f) + } else { + scaleMatrix.setScale((b.right / WIDTH), (b.bottom / HEIGHT)) + } + + perimeterPath.transform(scaleMatrix, scaledPerimeter) + errorPerimeterPath.transform(scaleMatrix, scaledErrorPerimeter) + fillMask.transform(scaleMatrix, scaledFill) + scaledFill.computeBounds(fillRect, true) + fillOutlinePath.transform(scaleMatrix, scaledfillOutline) + boltPath.transform(scaleMatrix, scaledBolt) + plusPath.transform(scaleMatrix, scaledPlus) + + // It is expected that this view only ever scale by the same factor in each dimension, so + // just pick one to scale the strokeWidths + val scaledStrokeWidth = + (b.right / WIDTH * PROTECTION_STROKE_WIDTH).coerceAtLeast(PROTECTION_MIN_STROKE_WIDTH) + + fillColorStrokePaint.strokeWidth = scaledStrokeWidth + fillColorStrokeProtection.strokeWidth = scaledStrokeWidth + } + + @SuppressLint("RestrictedApi") + private fun loadPaths() { + val pathString = modRes.getString(R.string.config_landscapeBatteryPerimeterPathE) + perimeterPath.set(PathParser.createPathFromPathData(pathString)) + perimeterPath.computeBounds(RectF(), true) + + val errorPathString = modRes.getString(R.string.config_landscapeBatteryErrorPerimeterPathE) + errorPerimeterPath.set(PathParser.createPathFromPathData(errorPathString)) + errorPerimeterPath.computeBounds(RectF(), true) + + val fillMaskString = modRes.getString(R.string.config_landscapeBatteryFillMaskE) + fillMask.set(PathParser.createPathFromPathData(fillMaskString)) + // Set the fill rect so we can calculate the fill properly + fillMask.computeBounds(fillRect, true) + + val fillOutlinePathString = modRes.getString(R.string.config_landscapeBatteryFillOutlineE) + fillOutlinePath.set(PathParser.createPathFromPathData(fillOutlinePathString)) + fillOutlinePath.computeBounds(RectF(), true) + + val boltPathString = modRes.getString(R.string.config_landscapeBatteryBoltPathE) + boltPath.set(PathParser.createPathFromPathData(boltPathString)) + + val plusPathString = modRes.getString(R.string.config_landscapeBatteryPowersavePathE) + plusPath.set(PathParser.createPathFromPathData(plusPathString)) + + dualTone = false + } + + companion object { + private const val TAG = "LandscapeBatteryE" + private const val WIDTH = 20f + private const val HEIGHT = 13f + private const val CRITICAL_LEVEL = 15 + + // On a 20x13 grid, how wide to make the fill protection stroke. + // Scales when our size changes + private const val PROTECTION_STROKE_WIDTH = 2f + + // Arbitrarily chosen for visibility at small sizes + private const val PROTECTION_MIN_STROKE_WIDTH = 5f + } +} diff --git a/app/src/main/java/com/drdisagree/iconify/xposed/mods/batterystyles/LandscapeBatteryF.kt b/app/src/main/java/com/drdisagree/iconify/xposed/mods/batterystyles/LandscapeBatteryF.kt new file mode 100644 index 000000000..544e1aa8e --- /dev/null +++ b/app/src/main/java/com/drdisagree/iconify/xposed/mods/batterystyles/LandscapeBatteryF.kt @@ -0,0 +1,623 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.drdisagree.iconify.xposed.mods.batterystyles + +import android.annotation.SuppressLint +import android.content.Context +import android.graphics.BlendMode +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.ColorFilter +import android.graphics.LinearGradient +import android.graphics.Matrix +import android.graphics.Paint +import android.graphics.Path +import android.graphics.PixelFormat +import android.graphics.Rect +import android.graphics.RectF +import android.graphics.Shader +import android.graphics.Typeface +import android.util.TypedValue +import androidx.core.graphics.PathParser +import com.drdisagree.iconify.R +import com.drdisagree.iconify.xposed.HookRes.modRes +import com.drdisagree.iconify.xposed.utils.SettingsLibUtils +import kotlin.math.floor + +/** + * A battery meter drawable that respects paths configured in + * frameworks/base/core/res/res/values/config.xml to allow for an easily overrideable battery icon + */ +@SuppressLint("DiscouragedApi") +open class LandscapeBatteryF(private val context: Context, frameColor: Int) : BatteryDrawable() { + + // Need to load: + // 1. perimeter shape + // 2. fill mask (if smaller than perimeter, this would create a fill that + // doesn't touch the walls + private val perimeterPath = Path() + private val scaledPerimeter = Path() + private val errorPerimeterPath = Path() + private val scaledErrorPerimeter = Path() + + // Fill will cover the whole bounding rect of the fillMask, and be masked by the path + private val fillMask = Path() + private val scaledFill = Path() + + // Based off of the mask, the fill will interpolate across this space + private val fillRect = RectF() + + // Top of this rect changes based on level, 100% == fillRect + private val levelRect = RectF() + private val levelPath = Path() + + // Updates the transform of the paths when our bounds change + private val scaleMatrix = Matrix() + private val padding = Rect() + + // The net result of fill + perimeter paths + private val unifiedPath = Path() + + // Bolt path (used while charging) + private val boltPath = Path() + private val scaledBolt = Path() + + // Plus sign (used for power save mode) + private val plusPath = Path() + private val scaledPlus = Path() + + private var intrinsicHeight: Int + private var intrinsicWidth: Int + + // To implement hysteresis, keep track of the need to invert the interior icon of the battery + private var invertFillIcon = false + + // Colors can be configured based on battery level (see res/values/arrays.xml) + private var colorLevels: IntArray + + private var fillColor: Int = Color.MAGENTA + private var backgroundColor: Int = Color.MAGENTA + + // updated whenever level changes + private var levelColor: Int = Color.MAGENTA + + // Dual tone implies that battery level is a clipped overlay over top of the whole shape + private var dualTone = false + + private var batteryLevel = 0 + + private var scaledFillAlpha = false + private var scaledPerimeterAlpha = false + private var customBlendColor = false + + private var chargingColor: Int = Color.TRANSPARENT + private var customFillColor: Int = Color.BLACK + private var customFillGradColor: Int = Color.BLACK + private var powerSaveColor: Int = Color.TRANSPARENT + private var powerSaveFillColor: Int = Color.TRANSPARENT + + private val invalidateRunnable: () -> Unit = { + invalidateSelf() + } + + open var criticalLevel: Int = 5 + + var charging = false + set(value) { + field = value + postInvalidate() + } + + override fun setChargingEnabled(charging: Boolean) { + this.charging = charging + postInvalidate() + } + + var powerSaveEnabled = false + set(value) { + field = value + postInvalidate() + } + + override fun setPowerSavingEnabled(powerSaveEnabled: Boolean) { + this.powerSaveEnabled = powerSaveEnabled + postInvalidate() + } + + var showPercent = false + set(value) { + field = value + postInvalidate() + } + + override fun setShowPercentEnabled(showPercent: Boolean) { + this.showPercent = showPercent + postInvalidate() + } + + var customChargingIcon = false + set(value) { + field = value + postInvalidate() + } + + open fun customizeBatteryDrawable( + scaledPerimeterAlpha: Boolean, + scaledFillAlpha: Boolean, + customBlendColor: Boolean, + customFillColor: Int, + customFillGradColor: Int, + chargingColor: Int, + powerSaveColor: Int, + powerSaveFillColor: Int + ) { + this.scaledPerimeterAlpha = scaledPerimeterAlpha + this.scaledFillAlpha = scaledFillAlpha + this.customBlendColor = customBlendColor + this.customFillColor = customFillColor + this.customFillGradColor = customFillGradColor + this.chargingColor = chargingColor + this.powerSaveColor = powerSaveColor + this.powerSaveFillColor = powerSaveFillColor + } + + private val fillColorStrokePaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + p.alpha = 255 + p.isDither = true + p.strokeWidth = 5f + p.style = Paint.Style.STROKE + p.blendMode = BlendMode.SRC + p.strokeMiter = 5f + p.strokeJoin = Paint.Join.ROUND + } + + private val fillColorStrokeProtection = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.isDither = true + p.strokeWidth = 5f + p.style = Paint.Style.STROKE + p.blendMode = BlendMode.CLEAR + p.strokeMiter = 5f + p.strokeJoin = Paint.Join.ROUND + } + + private val fillPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + p.alpha = 255 + p.isDither = true + p.strokeWidth = 0f + p.style = Paint.Style.FILL_AND_STROKE + } + + private val errorPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = context.resources.getColorStateList( + context.resources.getIdentifier( + "batterymeter_plus_color", "color", context.packageName + ), context.theme + ).defaultColor + p.alpha = 255 + p.isDither = true + p.strokeWidth = 0f + p.style = Paint.Style.FILL_AND_STROKE + p.blendMode = BlendMode.SRC + } + + private val chargingPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + } + + private val customFillPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + } + + private val powerSavePaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + } + + private val powerSaveFillPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + } + + private val scaledFillPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + } + + private val scaledPerimeterPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + } + + private val scaledPerimeterPaintDef = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + } + + // Only used if dualTone is set to true + private val dualToneBackgroundFill = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + p.alpha = 85 // ~0.3 alpha by default + p.isDither = true + p.strokeWidth = 0f + p.style = Paint.Style.FILL_AND_STROKE + } + + private val textPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.typeface = Typeface.create("sans-serif-condensed", Typeface.BOLD) + p.textAlign = Paint.Align.CENTER + } + + init { + val density = context.resources.displayMetrics.density + intrinsicHeight = (HEIGHT * density).toInt() + intrinsicWidth = (WIDTH * density).toInt() + + val res = context.resources + val levels = res.obtainTypedArray( + res.getIdentifier( + "batterymeter_color_levels", "array", context.packageName + ) + ) + val colors = res.obtainTypedArray( + res.getIdentifier( + "batterymeter_color_values", "array", context.packageName + ) + ) + val n = levels.length() + colorLevels = IntArray(2 * n) + for (i in 0 until n) { + colorLevels[2 * i] = levels.getInt(i, 0) + if (colors.getType(i) == TypedValue.TYPE_ATTRIBUTE) { + colorLevels[2 * i + 1] = SettingsLibUtils.getColorAttrDefaultColor( + colors.getResourceId(i, 0), context + ) + } else { + colorLevels[2 * i + 1] = colors.getColor(i, 0) + } + } + levels.recycle() + colors.recycle() + + loadPaths() + } + + override fun draw(c: Canvas) { + c.saveLayer(null, null) + unifiedPath.reset() + levelPath.reset() + levelRect.set(fillRect) + val fillFraction = batteryLevel / 100f + val fillRight = + if (batteryLevel == 100) + fillRect.right + 0.67f + else + fillRect.right - (fillRect.width() * (1 - fillFraction)) + + levelRect.right = floor(fillRight.toDouble()).toFloat() + levelPath.addRoundRect( + levelRect, + floatArrayOf( + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 3.5f, + 3.5f, + 3.5f, + 3.5f + ), Path.Direction.CCW + ) + + scaledFillPaint.alpha = if (scaledFillAlpha) 100 else 0 + scaledPerimeterPaint.alpha = + if (scaledPerimeterAlpha) 100 else scaledPerimeterPaintDef.alpha + + // The perimeter should never change + c.drawPath(scaledFill, scaledFillPaint) + c.drawPath(scaledPerimeter, scaledPerimeterPaint) + // If drawing dual tone, the level is used only to clip the whole drawable path + if (!dualTone) { + unifiedPath.op(levelPath, Path.Op.UNION) + } + + fillPaint.color = levelColor + val black = Color.BLACK + + // Deal with unifiedPath clipping before it draws + if (charging && !customChargingIcon) { + // Clip out the bolt shape + unifiedPath.op(scaledBolt, Path.Op.DIFFERENCE) + if (!invertFillIcon) { + c.drawPath(scaledBolt, fillPaint) + } + } + + if (dualTone) { + // Dual tone means we draw the shape again, clipped to the charge level + c.drawPath(unifiedPath, dualToneBackgroundFill) + c.save() + c.clipRect( + bounds.left.toFloat(), + 0f, + bounds.right + bounds.width() * fillFraction, + bounds.left.toFloat() + ) + c.drawPath(unifiedPath, fillPaint) + c.restore() + } else { + // Non dual-tone means we draw the perimeter (with the level fill), and potentially + // draw the fill again with a critical color + if (customBlendColor) { + if (charging) { + fillPaint.color = fillColor + c.drawPath(unifiedPath, fillPaint) + fillPaint.color = levelColor + } else { + // Show colorError below this level + if (batteryLevel <= CRITICAL_LEVEL) { + c.save() + c.clipPath(scaledFill) + c.drawPath(levelPath, fillPaint) + c.restore() + } else { + customFillPaint.color = customFillColor + customFillPaint.shader = + if (customFillColor != black && customFillGradColor != black) LinearGradient( + levelRect.right, 0f, 0f, levelRect.bottom, + customFillColor, customFillGradColor, + Shader.TileMode.CLAMP + ) else null + c.drawPath( + levelPath, + if (customFillColor == Color.BLACK) fillPaint else customFillPaint + ) + } + } + } else { + // Show colorError below this level + if (batteryLevel <= CRITICAL_LEVEL && !charging) { + c.save() + c.clipPath(scaledFill) + c.drawPath(levelPath, fillPaint) + c.restore() + } else { + fillPaint.color = fillColor + c.drawPath(unifiedPath, fillPaint) + fillPaint.color = levelColor + } + } + } + + if (customBlendColor) { + chargingPaint.color = + if (chargingColor == Color.BLACK) Color.TRANSPARENT else chargingColor + + powerSavePaint.color = + if (powerSaveColor == Color.BLACK) context.resources.getColorStateList( + context.resources.getIdentifier( + "batterymeter_plus_color", "color", context.packageName + ), context.theme + ).defaultColor else powerSaveColor + + powerSaveFillPaint.color = + if (powerSaveFillColor == Color.BLACK) Color.TRANSPARENT else powerSaveFillColor + } else { + chargingPaint.color = Color.TRANSPARENT + powerSavePaint.color = context.resources.getColorStateList( + context.resources.getIdentifier( + "batterymeter_plus_color", "color", context.packageName + ), context.theme + ).defaultColor + powerSaveFillPaint.color = Color.TRANSPARENT + } + + if (charging) { + if (!customChargingIcon) { + c.clipOutPath(scaledBolt) + c.drawPath(levelPath, chargingPaint) + if (invertFillIcon) { + c.drawPath(scaledBolt, fillColorStrokePaint) + } else { + c.drawPath(scaledBolt, fillColorStrokeProtection) + } + } else { + c.drawPath(levelPath, chargingPaint) + } + } else if (powerSaveEnabled) { + // If power save is enabled draw the perimeter path with colorError + c.drawPath(scaledErrorPerimeter, powerSavePaint) + c.drawPath(levelPath, powerSaveFillPaint) + // And draw the plus sign on top of the fill + if (!showPercent) { + c.drawPath(scaledPlus, powerSavePaint) + } + } + c.restore() + } + + private fun batteryColorForLevel(level: Int): Int { + return when { + charging || powerSaveEnabled -> fillColor + else -> getColorForLevel(level) + } + } + + private fun getColorForLevel(level: Int): Int { + var thresh: Int + var color = 0 + var i = 0 + while (i < colorLevels.size) { + thresh = colorLevels[i] + color = colorLevels[i + 1] + if (level <= thresh) { + + // Respect tinting for "normal" level + return if (i == colorLevels.size - 2) { + fillColor + } else { + color + } + } + i += 2 + } + return color + } + + /** + * Alpha is unused internally, and should be defined in the colors passed to {@link setColors}. + * Further, setting an alpha for a dual tone battery meter doesn't make sense without bounds + * defining the minimum background fill alpha. This is because fill + background must be equal + * to the net alpha passed in here. + */ + override fun setAlpha(alpha: Int) { + } + + override fun setColorFilter(colorFilter: ColorFilter?) { + fillPaint.colorFilter = colorFilter + fillColorStrokePaint.colorFilter = colorFilter + dualToneBackgroundFill.colorFilter = colorFilter + } + + /** + * Deprecated, but required by Drawable + */ + @Deprecated( + "Deprecated in Java", + ReplaceWith("PixelFormat.OPAQUE", "android.graphics.PixelFormat"), + ) + override fun getOpacity(): Int { + return PixelFormat.OPAQUE + } + + override fun getIntrinsicHeight(): Int { + return intrinsicHeight + } + + override fun getIntrinsicWidth(): Int { + return intrinsicWidth + } + + /** + * Set the fill level + */ + @Suppress("PARAMETER_NAME_CHANGED_ON_OVERRIDE") + override fun setBatteryLevel(l: Int) { + // invertFillIcon = if (l >= 67) true else if (l <= 33) false else invertFillIcon + batteryLevel = l + levelColor = batteryColorForLevel(batteryLevel) + invalidateSelf() + } + + fun getBatteryLevel(): Int { + return batteryLevel + } + + override fun onBoundsChange(bounds: Rect) { + super.onBoundsChange(bounds) + updateSize() + } + + fun setPadding(left: Int, top: Int, right: Int, bottom: Int) { + padding.left = left + padding.top = top + padding.right = right + padding.bottom = bottom + + updateSize() + } + + override fun setColors(fgColor: Int, bgColor: Int, singleToneColor: Int) { + fillColor = if (dualTone) fgColor else singleToneColor + + fillPaint.color = fillColor + fillColorStrokePaint.color = fillColor + + scaledFillPaint.color = fillColor + scaledPerimeterPaint.color = fillColor + scaledPerimeterPaintDef.color = fillColor + + backgroundColor = bgColor + dualToneBackgroundFill.color = bgColor + + // Also update the level color, since fillColor may have changed + levelColor = batteryColorForLevel(batteryLevel) + + invalidateSelf() + } + + private fun postInvalidate() { + unscheduleSelf(invalidateRunnable) + scheduleSelf(invalidateRunnable, 0) + } + + private fun updateSize() { + val b = bounds + if (b.isEmpty) { + scaleMatrix.setScale(1f, 1f) + } else { + scaleMatrix.setScale((b.right / WIDTH), (b.bottom / HEIGHT)) + } + + perimeterPath.transform(scaleMatrix, scaledPerimeter) + errorPerimeterPath.transform(scaleMatrix, scaledErrorPerimeter) + fillMask.transform(scaleMatrix, scaledFill) + scaledFill.computeBounds(fillRect, true) + boltPath.transform(scaleMatrix, scaledBolt) + plusPath.transform(scaleMatrix, scaledPlus) + + // It is expected that this view only ever scale by the same factor in each dimension, so + // just pick one to scale the strokeWidths + val scaledStrokeWidth = + (b.right / WIDTH * PROTECTION_STROKE_WIDTH).coerceAtLeast(PROTECTION_MIN_STROKE_WIDTH) + + fillColorStrokePaint.strokeWidth = scaledStrokeWidth + fillColorStrokeProtection.strokeWidth = scaledStrokeWidth + } + + @SuppressLint("RestrictedApi") + private fun loadPaths() { + val pathString = modRes.getString(R.string.config_landscapeBatteryPerimeterPathF) + perimeterPath.set(PathParser.createPathFromPathData(pathString)) + perimeterPath.computeBounds(RectF(), true) + + val errorPathString = modRes.getString(R.string.config_landscapeBatteryErrorPerimeterPathF) + errorPerimeterPath.set(PathParser.createPathFromPathData(errorPathString)) + errorPerimeterPath.computeBounds(RectF(), true) + + val fillMaskString = modRes.getString(R.string.config_landscapeBatteryFillMaskF) + fillMask.set(PathParser.createPathFromPathData(fillMaskString)) + // Set the fill rect so we can calculate the fill properly + fillMask.computeBounds(fillRect, true) + + val boltPathString = modRes.getString(R.string.config_landscapeBatteryBoltPathF) + boltPath.set(PathParser.createPathFromPathData(boltPathString)) + + val plusPathString = modRes.getString(R.string.config_landscapeBatteryPowersavePathF) + plusPath.set(PathParser.createPathFromPathData(plusPathString)) + + dualTone = false + } + + companion object { + private const val TAG = "LandscapeBatteryF" + private const val WIDTH = 24f + private const val HEIGHT = 12f + private const val CRITICAL_LEVEL = 15 + + // On a 24x12 grid, how wide to make the fill protection stroke. + // Scales when our size changes + private const val PROTECTION_STROKE_WIDTH = 1.5f + + // Arbitrarily chosen for visibility at small sizes + private const val PROTECTION_MIN_STROKE_WIDTH = 3f + } +} diff --git a/app/src/main/java/com/drdisagree/iconify/xposed/mods/batterystyles/LandscapeBatteryG.kt b/app/src/main/java/com/drdisagree/iconify/xposed/mods/batterystyles/LandscapeBatteryG.kt new file mode 100644 index 000000000..76f5347f2 --- /dev/null +++ b/app/src/main/java/com/drdisagree/iconify/xposed/mods/batterystyles/LandscapeBatteryG.kt @@ -0,0 +1,662 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.drdisagree.iconify.xposed.mods.batterystyles + +import android.annotation.SuppressLint +import android.content.Context +import android.graphics.BlendMode +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.ColorFilter +import android.graphics.LinearGradient +import android.graphics.Matrix +import android.graphics.Paint +import android.graphics.Path +import android.graphics.PixelFormat +import android.graphics.Rect +import android.graphics.RectF +import android.graphics.Shader +import android.graphics.Typeface +import android.util.TypedValue +import androidx.core.graphics.PathParser +import com.drdisagree.iconify.R +import com.drdisagree.iconify.xposed.HookRes.modRes +import com.drdisagree.iconify.xposed.utils.SettingsLibUtils +import kotlin.math.floor + +/** + * A battery meter drawable that respects paths configured in + * frameworks/base/core/res/res/values/config.xml to allow for an easily overrideable battery icon + */ +@SuppressLint("DiscouragedApi") +open class LandscapeBatteryG(private val context: Context, frameColor: Int) : BatteryDrawable() { + + // Need to load: + // 1. perimeter shape + // 2. fill mask (if smaller than perimeter, this would create a fill that + // doesn't touch the walls + private val perimeterPath = Path() + private val scaledPerimeter = Path() + private val errorPerimeterPath = Path() + private val scaledErrorPerimeter = Path() + + // Fill will cover the whole bounding rect of the fillMask, and be masked by the path + private val fillMask = Path() + private val scaledFill = Path() + private val fillOutlinePath = Path() + private val scaledfillOutline = Path() + + // Based off of the mask, the fill will interpolate across this space + private val fillRect = RectF() + + // Top of this rect changes based on level, 100% == fillRect + private val levelRect = RectF() + private val levelPath = Path() + + // Updates the transform of the paths when our bounds change + private val scaleMatrix = Matrix() + private val padding = Rect() + + // The net result of fill + perimeter paths + private val unifiedPath = Path() + + // Bolt path (used while charging) + private val boltPath = Path() + private val scaledBolt = Path() + + // Plus sign (used for power save mode) + private val plusPath = Path() + private val scaledPlus = Path() + + private var intrinsicHeight: Int + private var intrinsicWidth: Int + + // To implement hysteresis, keep track of the need to invert the interior icon of the battery + private var invertFillIcon = false + + // Colors can be configured based on battery level (see res/values/arrays.xml) + private var colorLevels: IntArray + + private var fillColor: Int = Color.MAGENTA + private var backgroundColor: Int = Color.MAGENTA + + // updated whenever level changes + private var levelColor: Int = Color.MAGENTA + + // Dual tone implies that battery level is a clipped overlay over top of the whole shape + private var dualTone = false + + private var batteryLevel = 0 + + private var isRotation = false + private var scaledFillAlpha = false + private var scaledPerimeterAlpha = false + private var customBlendColor = false + + private var chargingColor: Int = Color.TRANSPARENT + private var customFillColor: Int = Color.BLACK + private var customFillGradColor: Int = Color.BLACK + private var powerSaveColor: Int = Color.TRANSPARENT + private var powerSaveFillColor: Int = Color.TRANSPARENT + + private val invalidateRunnable: () -> Unit = { + invalidateSelf() + } + + open var criticalLevel: Int = 5 + + var charging = false + set(value) { + field = value + postInvalidate() + } + + override fun setChargingEnabled(charging: Boolean) { + this.charging = charging + postInvalidate() + } + + var powerSaveEnabled = false + set(value) { + field = value + postInvalidate() + } + + override fun setPowerSavingEnabled(powerSaveEnabled: Boolean) { + this.powerSaveEnabled = powerSaveEnabled + postInvalidate() + } + + var showPercent = false + set(value) { + field = value + postInvalidate() + } + + override fun setShowPercentEnabled(showPercent: Boolean) { + this.showPercent = showPercent + postInvalidate() + } + + var customChargingIcon = false + set(value) { + field = value + postInvalidate() + } + + open fun customizeBatteryDrawable( + isRotation: Boolean, + scaledPerimeterAlpha: Boolean, + scaledFillAlpha: Boolean, + customBlendColor: Boolean, + customFillColor: Int, + customFillGradColor: Int, + chargingColor: Int, + powerSaveColor: Int, + powerSaveFillColor: Int + ) { + this.isRotation = isRotation + this.scaledPerimeterAlpha = scaledPerimeterAlpha + this.scaledFillAlpha = scaledFillAlpha + this.customBlendColor = customBlendColor + this.customFillColor = customFillColor + this.customFillGradColor = customFillGradColor + this.chargingColor = chargingColor + this.powerSaveColor = powerSaveColor + this.powerSaveFillColor = powerSaveFillColor + } + + private val fillColorStrokePaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + p.alpha = 255 + p.isDither = true + p.strokeWidth = 5f + p.style = Paint.Style.STROKE + p.blendMode = BlendMode.SRC + p.strokeMiter = 5f + p.strokeJoin = Paint.Join.ROUND + } + + private val fillColorStrokeProtection = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.isDither = true + p.strokeWidth = 5f + p.style = Paint.Style.STROKE + p.blendMode = BlendMode.CLEAR + p.strokeMiter = 5f + p.strokeJoin = Paint.Join.ROUND + } + + private val fillPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + p.alpha = 255 + p.isDither = true + p.strokeWidth = 0f + p.style = Paint.Style.FILL_AND_STROKE + } + + private val errorPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = context.resources.getColorStateList( + context.resources.getIdentifier( + "batterymeter_plus_color", "color", context.packageName + ), context.theme + ).defaultColor + p.alpha = 255 + p.isDither = true + p.strokeWidth = 0f + p.style = Paint.Style.FILL_AND_STROKE + p.blendMode = BlendMode.SRC + } + + private val chargingPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + } + + private val customFillPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + } + + private val fillPercentPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + } + + private val powerSavePaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + } + + private val powerSaveFillPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + } + + private val scaledFillPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + } + + private val scaledPerimeterPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + } + + private val scaledPerimeterPaintDef = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + } + + // Only used if dualTone is set to true + private val dualToneBackgroundFill = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + p.alpha = 85 // ~0.3 alpha by default + p.isDither = true + p.strokeWidth = 0f + p.style = Paint.Style.FILL_AND_STROKE + } + + private val textPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.typeface = Typeface.create("sans-serif-condensed", Typeface.BOLD) + p.textAlign = Paint.Align.CENTER + } + + init { + val density = context.resources.displayMetrics.density + intrinsicHeight = (HEIGHT * density).toInt() + intrinsicWidth = (WIDTH * density).toInt() + + val res = context.resources + val levels = res.obtainTypedArray( + res.getIdentifier( + "batterymeter_color_levels", "array", context.packageName + ) + ) + val colors = res.obtainTypedArray( + res.getIdentifier( + "batterymeter_color_values", "array", context.packageName + ) + ) + val n = levels.length() + colorLevels = IntArray(2 * n) + for (i in 0 until n) { + colorLevels[2 * i] = levels.getInt(i, 0) + if (colors.getType(i) == TypedValue.TYPE_ATTRIBUTE) { + colorLevels[2 * i + 1] = SettingsLibUtils.getColorAttrDefaultColor( + colors.getResourceId(i, 0), context + ) + } else { + colorLevels[2 * i + 1] = colors.getColor(i, 0) + } + } + levels.recycle() + colors.recycle() + + loadPaths() + } + + override fun draw(c: Canvas) { + c.saveLayer(null, null) + unifiedPath.reset() + levelPath.reset() + levelRect.set(fillRect) + val fillFraction = batteryLevel / 100f + val fillRight = + if (batteryLevel == 100) + fillRect.right + 1f + else + fillRect.right - (fillRect.width() * (1 - fillFraction)) + val fillBottom = fillRect.bottom + 1f + + levelRect.right = floor(fillRight.toDouble()).toFloat() + levelRect.bottom = floor(fillBottom.toDouble()).toFloat() + levelPath.addRect(levelRect, Path.Direction.CCW) + + scaledFillPaint.alpha = if (scaledFillAlpha) 100 else 0 + scaledPerimeterPaint.alpha = + if (scaledPerimeterAlpha) 100 else scaledPerimeterPaintDef.alpha + + // The perimeter should never change + c.drawPath(scaledFill, scaledFillPaint) + c.drawPath(scaledPerimeter, scaledPerimeterPaint) + // If drawing dual tone, the level is used only to clip the whole drawable path + if (!dualTone) { + unifiedPath.op(levelPath, Path.Op.UNION) + } + + fillPaint.color = levelColor + val black = Color.BLACK + + if (customBlendColor) { + chargingPaint.color = + if (chargingColor == black) Color.TRANSPARENT else chargingColor + + powerSavePaint.color = + if (powerSaveColor == black) context.resources.getColorStateList( + context.resources.getIdentifier( + "batterymeter_plus_color", "color", context.packageName + ), context.theme + ).defaultColor else powerSaveColor + + powerSaveFillPaint.color = + if (powerSaveFillColor == black) Color.TRANSPARENT else powerSaveFillColor + } else { + chargingPaint.color = Color.TRANSPARENT + powerSavePaint.color = context.resources.getColorStateList( + context.resources.getIdentifier( + "batterymeter_plus_color", "color", context.packageName + ), context.theme + ).defaultColor + powerSaveFillPaint.color = Color.TRANSPARENT + } + + // Deal with unifiedPath clipping before it draws + if (charging && !customChargingIcon) { + // Clip out the bolt shape + unifiedPath.op(scaledBolt, Path.Op.DIFFERENCE) + if (!invertFillIcon) { + c.drawPath(scaledBolt, fillPaint) + } + } + + if (dualTone) { + // Dual tone means we draw the shape again, clipped to the charge level + c.drawPath(unifiedPath, dualToneBackgroundFill) + c.save() + c.clipRect( + bounds.left.toFloat(), + 0f, + bounds.right + bounds.width() * fillFraction, + bounds.left.toFloat() + ) + c.drawPath(unifiedPath, fillPaint) + c.restore() + } else { + // Non dual-tone means we draw the perimeter (with the level fill), and potentially + // draw the fill again with a critical color + if (customBlendColor) { + if (charging) { + fillPaint.color = fillColor + c.clipOutPath(scaledfillOutline) + c.drawPath(unifiedPath, fillPaint) + fillPaint.color = levelColor + } else if (powerSaveEnabled) { + c.drawPath(scaledErrorPerimeter, powerSavePaint) + c.clipOutPath(scaledfillOutline) + c.drawPath(levelPath, fillPaint) + c.drawPath(levelPath, powerSaveFillPaint) + } else { + c.clipOutPath(scaledfillOutline) + customFillPaint.color = customFillColor + customFillPaint.shader = + if (customFillColor != black && customFillGradColor != black) LinearGradient( + levelRect.right, 0f, 0f, levelRect.bottom, + customFillColor, customFillGradColor, + Shader.TileMode.CLAMP + ) else null + c.drawPath( + levelPath, + if (customFillColor == black) fillPercentPaint else customFillPaint + ) + } + } else { + if (charging) { + c.clipOutPath(scaledfillOutline) + c.drawPath(unifiedPath, fillPercentPaint) + } else if (powerSaveEnabled) { + c.drawPath(scaledErrorPerimeter, errorPaint) + c.clipOutPath(scaledfillOutline) + c.drawPath(levelPath, fillPercentPaint) + } else { + c.clipOutPath(scaledfillOutline) + c.drawPath(unifiedPath, fillPercentPaint) + } + } + + // Show colorError below this level + if (batteryLevel <= CRITICAL_LEVEL && !charging) { + c.save() + c.clipPath(scaledFill) + c.drawPath(levelPath, fillPaint) + c.restore() + } + } + + if (charging) { + if (!customChargingIcon) { + c.clipOutPath(scaledBolt) + c.drawPath(levelPath, chargingPaint) + if (invertFillIcon) { + c.drawPath(scaledBolt, fillColorStrokePaint) + } else { + c.drawPath(scaledBolt, fillColorStrokeProtection) + } + } else { + c.drawPath(levelPath, chargingPaint) + } + } else if (powerSaveEnabled) { + // If power save is enabled draw the perimeter path with colorError + c.drawPath(scaledErrorPerimeter, powerSavePaint) + c.drawPath(levelPath, powerSaveFillPaint) + // And draw the plus sign on top of the fill + if (!showPercent) { + c.drawPath(scaledPlus, powerSavePaint) + } + } + c.restore() + + var state = false + if (customChargingIcon && charging) { + state = true + } else if (customChargingIcon && !charging) { + state = true + } else if (!customChargingIcon && charging) { + state = false + } else if (!customChargingIcon && !charging) { + state = true + } + + if (showPercent && state) { + textPaint.textSize = bounds.width() * 0.26f + val textHeight = +textPaint.fontMetrics.ascent + val pctX = (bounds.width() + textHeight) * 0.54f + val pctY = bounds.height() * 0.66f + + textPaint.color = fillColor + if (isRotation) { + c.rotate(180f, pctX, pctY * 0.76f) + } + c.drawText(batteryLevel.toString(), pctX, pctY, textPaint) + c.restore() + } + } + + private fun batteryColorForLevel(level: Int): Int { + return when { + charging || powerSaveEnabled -> fillColor + else -> getColorForLevel(level) + } + } + + private fun getColorForLevel(level: Int): Int { + var thresh: Int + var color = 0 + var i = 0 + while (i < colorLevels.size) { + thresh = colorLevels[i] + color = colorLevels[i + 1] + if (level <= thresh) { + + // Respect tinting for "normal" level + return if (i == colorLevels.size - 2) { + fillColor + } else { + color + } + } + i += 2 + } + return color + } + + /** + * Alpha is unused internally, and should be defined in the colors passed to {@link setColors}. + * Further, setting an alpha for a dual tone battery meter doesn't make sense without bounds + * defining the minimum background fill alpha. This is because fill + background must be equal + * to the net alpha passed in here. + */ + override fun setAlpha(alpha: Int) { + } + + override fun setColorFilter(colorFilter: ColorFilter?) { + fillPaint.colorFilter = colorFilter + fillColorStrokePaint.colorFilter = colorFilter + dualToneBackgroundFill.colorFilter = colorFilter + } + + /** + * Deprecated, but required by Drawable + */ + @Deprecated( + "Deprecated in Java", + ReplaceWith("PixelFormat.OPAQUE", "android.graphics.PixelFormat"), + ) + override fun getOpacity(): Int { + return PixelFormat.OPAQUE + } + + override fun getIntrinsicHeight(): Int { + return intrinsicHeight + } + + override fun getIntrinsicWidth(): Int { + return intrinsicWidth + } + + /** + * Set the fill level + */ + @Suppress("PARAMETER_NAME_CHANGED_ON_OVERRIDE") + override fun setBatteryLevel(l: Int) { + // invertFillIcon = if (l >= 67) true else if (l <= 33) false else invertFillIcon + batteryLevel = l + levelColor = batteryColorForLevel(batteryLevel) + invalidateSelf() + } + + fun getBatteryLevel(): Int { + return batteryLevel + } + + override fun onBoundsChange(bounds: Rect) { + super.onBoundsChange(bounds) + updateSize() + } + + fun setPadding(left: Int, top: Int, right: Int, bottom: Int) { + padding.left = left + padding.top = top + padding.right = right + padding.bottom = bottom + + updateSize() + } + + override fun setColors(fgColor: Int, bgColor: Int, singleToneColor: Int) { + fillColor = if (dualTone) fgColor else singleToneColor + + fillPaint.color = fillColor + fillColorStrokePaint.color = fillColor + + fillPercentPaint.color = fillColor + fillPercentPaint.alpha = 75 + + scaledFillPaint.color = fillColor + scaledPerimeterPaint.color = fillColor + scaledPerimeterPaintDef.color = fillColor + + backgroundColor = bgColor + dualToneBackgroundFill.color = bgColor + + // Also update the level color, since fillColor may have changed + levelColor = batteryColorForLevel(batteryLevel) + + invalidateSelf() + } + + private fun postInvalidate() { + unscheduleSelf(invalidateRunnable) + scheduleSelf(invalidateRunnable, 0) + } + + private fun updateSize() { + val b = bounds + if (b.isEmpty) { + scaleMatrix.setScale(1f, 1f) + } else { + scaleMatrix.setScale((b.right / WIDTH), (b.bottom / HEIGHT)) + } + + perimeterPath.transform(scaleMatrix, scaledPerimeter) + errorPerimeterPath.transform(scaleMatrix, scaledErrorPerimeter) + fillMask.transform(scaleMatrix, scaledFill) + scaledFill.computeBounds(fillRect, true) + fillOutlinePath.transform(scaleMatrix, scaledfillOutline) + boltPath.transform(scaleMatrix, scaledBolt) + plusPath.transform(scaleMatrix, scaledPlus) + + // It is expected that this view only ever scale by the same factor in each dimension, so + // just pick one to scale the strokeWidths + val scaledStrokeWidth = + (b.right / WIDTH * PROTECTION_STROKE_WIDTH).coerceAtLeast(PROTECTION_MIN_STROKE_WIDTH) + + fillColorStrokePaint.strokeWidth = scaledStrokeWidth + fillColorStrokeProtection.strokeWidth = scaledStrokeWidth + } + + @SuppressLint("RestrictedApi") + private fun loadPaths() { + val pathString = modRes.getString(R.string.config_landscapeBatteryPerimeterPathG) + perimeterPath.set(PathParser.createPathFromPathData(pathString)) + perimeterPath.computeBounds(RectF(), true) + + val errorPathString = modRes.getString(R.string.config_landscapeBatteryErrorPerimeterPathG) + errorPerimeterPath.set(PathParser.createPathFromPathData(errorPathString)) + errorPerimeterPath.computeBounds(RectF(), true) + + val fillMaskString = modRes.getString(R.string.config_landscapeBatteryFillMaskG) + fillMask.set(PathParser.createPathFromPathData(fillMaskString)) + // Set the fill rect so we can calculate the fill properly + fillMask.computeBounds(fillRect, true) + + val fillOutlinePathString = modRes.getString(R.string.config_landscapeBatteryFillOutlineG) + fillOutlinePath.set(PathParser.createPathFromPathData(fillOutlinePathString)) + fillOutlinePath.computeBounds(RectF(), true) + + val boltPathString = modRes.getString(R.string.config_landscapeBatteryBoltPathG) + boltPath.set(PathParser.createPathFromPathData(boltPathString)) + + val plusPathString = modRes.getString(R.string.config_landscapeBatteryPowersavePathG) + plusPath.set(PathParser.createPathFromPathData(plusPathString)) + + dualTone = false + } + + companion object { + private const val TAG = "LandscapeBatteryG" + private const val WIDTH = 20f + private const val HEIGHT = 13f + private const val CRITICAL_LEVEL = 15 + + // On a 20x13 grid, how wide to make the fill protection stroke. + // Scales when our size changes + private const val PROTECTION_STROKE_WIDTH = 2f + + // Arbitrarily chosen for visibility at small sizes + private const val PROTECTION_MIN_STROKE_WIDTH = 5f + } +} diff --git a/app/src/main/java/com/drdisagree/iconify/xposed/mods/batterystyles/LandscapeBatteryH.kt b/app/src/main/java/com/drdisagree/iconify/xposed/mods/batterystyles/LandscapeBatteryH.kt new file mode 100644 index 000000000..276f5f05e --- /dev/null +++ b/app/src/main/java/com/drdisagree/iconify/xposed/mods/batterystyles/LandscapeBatteryH.kt @@ -0,0 +1,694 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.drdisagree.iconify.xposed.mods.batterystyles + +import android.annotation.SuppressLint +import android.content.Context +import android.graphics.BlendMode +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.ColorFilter +import android.graphics.LinearGradient +import android.graphics.Matrix +import android.graphics.Paint +import android.graphics.Path +import android.graphics.PixelFormat +import android.graphics.Rect +import android.graphics.RectF +import android.graphics.Shader +import android.graphics.Typeface +import android.util.TypedValue +import androidx.core.graphics.PathParser +import com.drdisagree.iconify.R +import com.drdisagree.iconify.xposed.HookRes.modRes +import com.drdisagree.iconify.xposed.utils.SettingsLibUtils +import kotlin.math.floor + +/** + * A battery meter drawable that respects paths configured in + * frameworks/base/core/res/res/values/config.xml to allow for an easily overrideable battery icon + */ +@SuppressLint("DiscouragedApi") +open class LandscapeBatteryH(private val context: Context, frameColor: Int) : BatteryDrawable() { + + // Need to load: + // 1. perimeter shape + // 2. fill mask (if smaller than perimeter, this would create a fill that + // doesn't touch the walls + private val perimeterPath = Path() + private val scaledPerimeter = Path() + private val errorPerimeterPath = Path() + private val scaledErrorPerimeter = Path() + + // Fill will cover the whole bounding rect of the fillMask, and be masked by the path + private val fillMask = Path() + private val scaledFill = Path() + + // Based off of the mask, the fill will interpolate across this space + private val fillRect = RectF() + + // Top of this rect changes based on level, 100% == fillRect + private val levelRect = RectF() + private val levelPath = Path() + + // Updates the transform of the paths when our bounds change + private val scaleMatrix = Matrix() + private val padding = Rect() + + // The net result of fill + perimeter paths + private val unifiedPath = Path() + + // Bolt path (used while charging) + private val boltPath = Path() + private val scaledBolt = Path() + + // Plus sign (used for power save mode) + private val plusPath = Path() + private val scaledPlus = Path() + + private var intrinsicHeight: Int + private var intrinsicWidth: Int + + // To implement hysteresis, keep track of the need to invert the interior icon of the battery + private var invertFillIcon = false + + // Colors can be configured based on battery level (see res/values/arrays.xml) + private var colorLevels: IntArray + + private var fillColor: Int = Color.MAGENTA + private var backgroundColor: Int = Color.MAGENTA + + // updated whenever level changes + private var levelColor: Int = Color.MAGENTA + + // Dual tone implies that battery level is a clipped overlay over top of the whole shape + private var dualTone = false + + private var batteryLevel = 0 + + private var isRotation = false + private var scaledFillAlpha = false + private var scaledPerimeterAlpha = false + private var customBlendColor = false + + private var chargingColor: Int = Color.TRANSPARENT + private var customFillColor: Int = Color.BLACK + private var customFillGradColor: Int = Color.BLACK + private var powerSaveColor: Int = Color.TRANSPARENT + private var powerSaveFillColor: Int = Color.TRANSPARENT + + private val invalidateRunnable: () -> Unit = { + invalidateSelf() + } + + open var criticalLevel: Int = 5 + + var charging = false + set(value) { + field = value + postInvalidate() + } + + override fun setChargingEnabled(charging: Boolean) { + this.charging = charging + postInvalidate() + } + + var powerSaveEnabled = false + set(value) { + field = value + postInvalidate() + } + + override fun setPowerSavingEnabled(powerSaveEnabled: Boolean) { + this.powerSaveEnabled = powerSaveEnabled + postInvalidate() + } + + var showPercent = false + set(value) { + field = value + postInvalidate() + } + + override fun setShowPercentEnabled(showPercent: Boolean) { + this.showPercent = showPercent + postInvalidate() + } + + var customChargingIcon = false + set(value) { + field = value + postInvalidate() + } + + open fun customizeBatteryDrawable( + isRotation: Boolean, + scaledPerimeterAlpha: Boolean, + scaledFillAlpha: Boolean, + customBlendColor: Boolean, + customFillColor: Int, + customFillGradColor: Int, + chargingColor: Int, + powerSaveColor: Int, + powerSaveFillColor: Int + ) { + this.isRotation = isRotation + this.scaledPerimeterAlpha = scaledPerimeterAlpha + this.scaledFillAlpha = scaledFillAlpha + this.customBlendColor = customBlendColor + this.customFillColor = customFillColor + this.customFillGradColor = customFillGradColor + this.chargingColor = chargingColor + this.powerSaveColor = powerSaveColor + this.powerSaveFillColor = powerSaveFillColor + } + + private val fillColorStrokePaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + p.alpha = 255 + p.isDither = true + p.strokeWidth = 5f + p.style = Paint.Style.STROKE + p.blendMode = BlendMode.SRC + p.strokeMiter = 5f + p.strokeJoin = Paint.Join.ROUND + } + + private val fillColorStrokeProtection = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.isDither = true + p.strokeWidth = 5f + p.style = Paint.Style.STROKE + p.blendMode = BlendMode.CLEAR + p.strokeMiter = 5f + p.strokeJoin = Paint.Join.ROUND + } + + private val fillPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + p.alpha = 255 + p.isDither = true + p.strokeWidth = 0f + p.style = Paint.Style.FILL_AND_STROKE + } + + private val errorPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = context.resources.getColorStateList( + context.resources.getIdentifier( + "batterymeter_plus_color", "color", context.packageName + ), context.theme + ).defaultColor + p.alpha = 255 + p.isDither = true + p.strokeWidth = 0f + p.style = Paint.Style.FILL_AND_STROKE + p.blendMode = BlendMode.SRC + } + + private val chargingPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + } + + private val customFillPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + } + + private val powerSavePaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + } + + private val powerSaveFillPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + } + + private val scaledFillPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + } + + private val scaledPerimeterPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + } + + private val scaledPerimeterPaintDef = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + } + + // Only used if dualTone is set to true + private val dualToneBackgroundFill = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + p.alpha = 85 // ~0.3 alpha by default + p.isDither = true + p.strokeWidth = 0f + p.style = Paint.Style.FILL_AND_STROKE + } + + private val textPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.typeface = Typeface.create("sans-serif-condensed", Typeface.BOLD) + p.textAlign = Paint.Align.CENTER + } + + init { + val density = context.resources.displayMetrics.density + intrinsicHeight = (HEIGHT * density).toInt() + intrinsicWidth = (WIDTH * density).toInt() + + val res = context.resources + val levels = res.obtainTypedArray( + res.getIdentifier( + "batterymeter_color_levels", "array", context.packageName + ) + ) + val colors = res.obtainTypedArray( + res.getIdentifier( + "batterymeter_color_values", "array", context.packageName + ) + ) + val n = levels.length() + colorLevels = IntArray(2 * n) + for (i in 0 until n) { + colorLevels[2 * i] = levels.getInt(i, 0) + if (colors.getType(i) == TypedValue.TYPE_ATTRIBUTE) { + colorLevels[2 * i + 1] = SettingsLibUtils.getColorAttrDefaultColor( + colors.getResourceId(i, 0), context + ) + } else { + colorLevels[2 * i + 1] = colors.getColor(i, 0) + } + } + levels.recycle() + colors.recycle() + + loadPaths() + } + + override fun draw(c: Canvas) { + c.saveLayer(null, null) + unifiedPath.reset() + levelPath.reset() + levelRect.set(fillRect) + val fillFraction = batteryLevel / 100f + val fillRight = + if (batteryLevel == 100) + fillRect.right + 0.59f + else + fillRect.right - (fillRect.width() * (1 - fillFraction)) + + levelRect.right = floor(fillRight.toDouble()).toFloat() + levelPath.addRoundRect( + levelRect, + floatArrayOf( + 3.0f, + 3.0f, + 3.0f, + 3.0f, + 3.0f, + 3.0f, + 3.0f, + 3.0f + ), Path.Direction.CCW + ) + + scaledFillPaint.alpha = if (scaledFillAlpha) 100 else 0 + scaledPerimeterPaint.alpha = + if (scaledPerimeterAlpha) 100 else scaledPerimeterPaintDef.alpha + + // The perimeter should never change + c.drawPath(scaledFill, scaledFillPaint) + c.drawPath(scaledPerimeter, scaledPerimeterPaint) + // If drawing dual tone, the level is used only to clip the whole drawable path + if (!dualTone) { + unifiedPath.op(levelPath, Path.Op.UNION) + } + + fillPaint.color = levelColor + val black = Color.BLACK + + // Deal with unifiedPath clipping before it draws + if (charging && !customChargingIcon) { + // Clip out the bolt shape + unifiedPath.op(scaledBolt, Path.Op.DIFFERENCE) + if (!invertFillIcon) { + c.drawPath(scaledBolt, fillPaint) + } + } + + if (dualTone) { + // Dual tone means we draw the shape again, clipped to the charge level + c.drawPath(unifiedPath, dualToneBackgroundFill) + c.save() + c.clipRect( + bounds.left.toFloat(), + 0f, + bounds.right + bounds.width() * fillFraction, + bounds.left.toFloat() + ) + c.drawPath(unifiedPath, fillPaint) + c.restore() + } else { + // Non dual-tone means we draw the perimeter (with the level fill), and potentially + // draw the fill again with a critical color + if (customBlendColor) { + if (charging) { + fillPaint.color = fillColor + c.drawPath(unifiedPath, fillPaint) + fillPaint.color = levelColor + } else { + // Show colorError below this level + if (batteryLevel <= CRITICAL_LEVEL) { + c.save() + c.clipPath(scaledFill) + c.drawPath(levelPath, fillPaint) + c.restore() + } else { + customFillPaint.color = customFillColor + customFillPaint.shader = + if (customFillColor != black && customFillGradColor != black) LinearGradient( + levelRect.right, 0f, 0f, levelRect.bottom, + customFillColor, customFillGradColor, + Shader.TileMode.CLAMP + ) else null + c.drawPath( + levelPath, + if (customFillColor == black) fillPaint else customFillPaint + ) + } + } + } else { + // Show colorError below this level + if (batteryLevel <= CRITICAL_LEVEL && !charging) { + c.save() + c.clipPath(scaledFill) + c.drawPath(levelPath, fillPaint) + c.restore() + } else { + fillPaint.color = fillColor + c.drawPath(unifiedPath, fillPaint) + fillPaint.color = levelColor + } + } + } + + if (customBlendColor) { + chargingPaint.color = + if (chargingColor == black) Color.TRANSPARENT else chargingColor + + powerSavePaint.color = + if (powerSaveColor == black) context.resources.getColorStateList( + context.resources.getIdentifier( + "batterymeter_plus_color", "color", context.packageName + ), context.theme + ).defaultColor else powerSaveColor + + powerSaveFillPaint.color = + if (powerSaveFillColor == black) Color.TRANSPARENT else powerSaveFillColor + } else { + chargingPaint.color = Color.TRANSPARENT + powerSavePaint.color = context.resources.getColorStateList( + context.resources.getIdentifier( + "batterymeter_plus_color", "color", context.packageName + ), context.theme + ).defaultColor + powerSaveFillPaint.color = Color.TRANSPARENT + } + + if (charging) { + if (!customChargingIcon) { + c.clipOutPath(scaledBolt) + c.drawPath(levelPath, chargingPaint) + if (invertFillIcon) { + c.drawPath(scaledBolt, fillColorStrokePaint) + } else { + c.drawPath(scaledBolt, fillColorStrokeProtection) + } + } else { + c.drawPath(levelPath, chargingPaint) + } + } else if (powerSaveEnabled) { + // If power save is enabled draw the perimeter path with colorError + c.drawPath(scaledErrorPerimeter, powerSavePaint) + c.drawPath(levelPath, powerSaveFillPaint) + // And draw the plus sign on top of the fill + if (!showPercent) { + c.drawPath(scaledPlus, powerSavePaint) + } + } + c.restore() + + var state = false + if (customChargingIcon && charging) { + state = true + } else if (customChargingIcon && !charging) { + state = true + } else if (!customChargingIcon && charging) { + state = false + } else if (!customChargingIcon && !charging) { + state = true + } + + if (showPercent && state) { + textPaint.textSize = bounds.width() * 0.26f + val textHeight = +textPaint.fontMetrics.ascent + val pctX = (bounds.width() + textHeight) * 0.61f + val pctY = bounds.height() * 0.69f + + if (isRotation) { + c.rotate(180f, pctX + 0.43f, pctY * 0.73f) + } + if (customBlendColor) { + if (powerSaveEnabled && powerSaveFillColor == black) { + textPaint.color = fillColor + c.drawText(batteryLevel.toString(), pctX, pctY, textPaint) + + textPaint.color = fillColor.toInt().inv() or 0xFF000000.toInt() + c.save() + c.clipRect( + if (isRotation) fillRect.left + (fillRect.width() * (1 - fillFraction)) else fillRect.left, + fillRect.top, + if (isRotation) fillRect.right else fillRect.right - (fillRect.width() * (1 - fillFraction)), + fillRect.bottom + ) + c.drawText(batteryLevel.toString(), pctX, pctY, textPaint) + } else if (!powerSaveEnabled && customFillColor == black) { + textPaint.color = fillColor + c.drawText(batteryLevel.toString(), pctX, pctY, textPaint) + + textPaint.color = fillColor.toInt().inv() or 0xFF000000.toInt() + c.save() + c.clipRect( + if (isRotation) fillRect.left + (fillRect.width() * (1 - fillFraction)) else fillRect.left, + fillRect.top, + if (isRotation) fillRect.right else fillRect.right - (fillRect.width() * (1 - fillFraction)), + fillRect.bottom + ) + c.drawText(batteryLevel.toString(), pctX, pctY, textPaint) + } else { + textPaint.color = fillColor + c.drawText(batteryLevel.toString(), pctX, pctY, textPaint) + } + } else { + textPaint.color = fillColor + c.drawText(batteryLevel.toString(), pctX, pctY, textPaint) + + textPaint.color = fillColor.toInt().inv() or 0xFF000000.toInt() + c.save() + c.clipRect( + if (isRotation) fillRect.left + (fillRect.width() * (1 - fillFraction)) else fillRect.left, + fillRect.top, + if (isRotation) fillRect.right else fillRect.right - (fillRect.width() * (1 - fillFraction)), + fillRect.bottom + ) + c.drawText(batteryLevel.toString(), pctX, pctY, textPaint) + } + c.restore() + } + } + + private fun batteryColorForLevel(level: Int): Int { + return when { + charging || powerSaveEnabled -> fillColor + else -> getColorForLevel(level) + } + } + + private fun getColorForLevel(level: Int): Int { + var thresh: Int + var color = 0 + var i = 0 + while (i < colorLevels.size) { + thresh = colorLevels[i] + color = colorLevels[i + 1] + if (level <= thresh) { + + // Respect tinting for "normal" level + return if (i == colorLevels.size - 2) { + fillColor + } else { + color + } + } + i += 2 + } + return color + } + + /** + * Alpha is unused internally, and should be defined in the colors passed to {@link setColors}. + * Further, setting an alpha for a dual tone battery meter doesn't make sense without bounds + * defining the minimum background fill alpha. This is because fill + background must be equal + * to the net alpha passed in here. + */ + override fun setAlpha(alpha: Int) { + } + + override fun setColorFilter(colorFilter: ColorFilter?) { + fillPaint.colorFilter = colorFilter + fillColorStrokePaint.colorFilter = colorFilter + dualToneBackgroundFill.colorFilter = colorFilter + } + + /** + * Deprecated, but required by Drawable + */ + @Deprecated( + "Deprecated in Java", + ReplaceWith("PixelFormat.OPAQUE", "android.graphics.PixelFormat"), + ) + override fun getOpacity(): Int { + return PixelFormat.OPAQUE + } + + override fun getIntrinsicHeight(): Int { + return intrinsicHeight + } + + override fun getIntrinsicWidth(): Int { + return intrinsicWidth + } + + /** + * Set the fill level + */ + @Suppress("PARAMETER_NAME_CHANGED_ON_OVERRIDE") + override fun setBatteryLevel(l: Int) { + // invertFillIcon = if (l >= 67) true else if (l <= 33) false else invertFillIcon + batteryLevel = l + levelColor = batteryColorForLevel(batteryLevel) + invalidateSelf() + } + + fun getBatteryLevel(): Int { + return batteryLevel + } + + override fun onBoundsChange(bounds: Rect) { + super.onBoundsChange(bounds) + updateSize() + } + + fun setPadding(left: Int, top: Int, right: Int, bottom: Int) { + padding.left = left + padding.top = top + padding.right = right + padding.bottom = bottom + + updateSize() + } + + override fun setColors(fgColor: Int, bgColor: Int, singleToneColor: Int) { + fillColor = if (dualTone) fgColor else singleToneColor + + fillPaint.color = fillColor + fillColorStrokePaint.color = fillColor + + scaledFillPaint.color = fillColor + scaledPerimeterPaint.color = fillColor + scaledPerimeterPaintDef.color = fillColor + + backgroundColor = bgColor + dualToneBackgroundFill.color = bgColor + + // Also update the level color, since fillColor may have changed + levelColor = batteryColorForLevel(batteryLevel) + + invalidateSelf() + } + + private fun postInvalidate() { + unscheduleSelf(invalidateRunnable) + scheduleSelf(invalidateRunnable, 0) + } + + private fun updateSize() { + val b = bounds + if (b.isEmpty) { + scaleMatrix.setScale(1f, 1f) + } else { + scaleMatrix.setScale((b.right / WIDTH), (b.bottom / HEIGHT)) + } + + perimeterPath.transform(scaleMatrix, scaledPerimeter) + errorPerimeterPath.transform(scaleMatrix, scaledErrorPerimeter) + fillMask.transform(scaleMatrix, scaledFill) + scaledFill.computeBounds(fillRect, true) + boltPath.transform(scaleMatrix, scaledBolt) + plusPath.transform(scaleMatrix, scaledPlus) + + // It is expected that this view only ever scale by the same factor in each dimension, so + // just pick one to scale the strokeWidths + val scaledStrokeWidth = + (b.right / WIDTH * PROTECTION_STROKE_WIDTH).coerceAtLeast(PROTECTION_MIN_STROKE_WIDTH) + + fillColorStrokePaint.strokeWidth = scaledStrokeWidth + fillColorStrokeProtection.strokeWidth = scaledStrokeWidth + } + + @SuppressLint("RestrictedApi") + private fun loadPaths() { + val pathString = modRes.getString(R.string.config_landscapeBatteryPerimeterPathH) + perimeterPath.set(PathParser.createPathFromPathData(pathString)) + perimeterPath.computeBounds(RectF(), true) + + val errorPathString = modRes.getString(R.string.config_landscapeBatteryErrorPerimeterPathH) + errorPerimeterPath.set(PathParser.createPathFromPathData(errorPathString)) + errorPerimeterPath.computeBounds(RectF(), true) + + val fillMaskString = modRes.getString(R.string.config_landscapeBatteryFillMaskH) + fillMask.set(PathParser.createPathFromPathData(fillMaskString)) + // Set the fill rect so we can calculate the fill properly + fillMask.computeBounds(fillRect, true) + + val boltPathString = modRes.getString(R.string.config_landscapeBatteryBoltPathH) + boltPath.set(PathParser.createPathFromPathData(boltPathString)) + + val plusPathString = modRes.getString(R.string.config_landscapeBatteryPowersavePathH) + plusPath.set(PathParser.createPathFromPathData(plusPathString)) + + dualTone = false + } + + companion object { + private const val TAG = "LandscapeBatteryH" + private const val WIDTH = 24f + private const val HEIGHT = 12f + private const val CRITICAL_LEVEL = 15 + + // On a 24x12 grid, how wide to make the fill protection stroke. + // Scales when our size changes + private const val PROTECTION_STROKE_WIDTH = 2f + + // Arbitrarily chosen for visibility at small sizes + private const val PROTECTION_MIN_STROKE_WIDTH = 5f + } +} diff --git a/app/src/main/java/com/drdisagree/iconify/xposed/mods/batterystyles/LandscapeBatteryI.kt b/app/src/main/java/com/drdisagree/iconify/xposed/mods/batterystyles/LandscapeBatteryI.kt new file mode 100644 index 000000000..f7f298d2e --- /dev/null +++ b/app/src/main/java/com/drdisagree/iconify/xposed/mods/batterystyles/LandscapeBatteryI.kt @@ -0,0 +1,1087 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.drdisagree.iconify.xposed.mods.batterystyles + +import android.annotation.SuppressLint +import android.content.Context +import android.graphics.BlendMode +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.ColorFilter +import android.graphics.LinearGradient +import android.graphics.Matrix +import android.graphics.Paint +import android.graphics.Path +import android.graphics.PixelFormat +import android.graphics.Rect +import android.graphics.RectF +import android.graphics.Shader +import android.graphics.Typeface +import android.util.TypedValue +import androidx.core.graphics.PathParser +import com.drdisagree.iconify.R +import com.drdisagree.iconify.xposed.HookRes.modRes +import com.drdisagree.iconify.xposed.utils.SettingsLibUtils +import kotlin.math.floor + +/** + * A battery meter drawable that respects paths configured in + * frameworks/base/core/res/res/values/config.xml to allow for an easily overrideable battery icon + */ +@SuppressLint("DiscouragedApi") +open class LandscapeBatteryI(private val context: Context, frameColor: Int) : BatteryDrawable() { + + // Need to load: + // 1. perimeter shape + // 2. fill mask (if smaller than perimeter, this would create a fill that + // doesn't touch the walls + private val perimeterPath = Path() + private val scaledPerimeter = Path() + private val errorPerimeterPath = Path() + private val scaledErrorPerimeter = Path() + + // Fill will cover the whole bounding rect of the fillMask, and be masked by the path + private val fillMask = Path() + private val scaledFill = Path() + + // custompath + private val fillOutlinePath = Path() + private val scaledfillOutline = Path() + private val fillMask1 = Path() + private val scaledFill1 = Path() + private val fillMask2 = Path() + private val scaledFill2 = Path() + private val fillMask3 = Path() + private val scaledFill3 = Path() + private val fillMask4 = Path() + private val scaledFill4 = Path() + private val fillMask5 = Path() + private val scaledFill5 = Path() + private val fillMask6 = Path() + private val scaledFill6 = Path() + private val fillMask7 = Path() + private val scaledFill7 = Path() + private val fillMask8 = Path() + private val scaledFill8 = Path() + private val fillMask9 = Path() + private val scaledFill9 = Path() + private val fillMask10 = Path() + private val scaledFill10 = Path() + private val fillNgguyu = Path() + private val scaledNgguyu = Path() + private val fillMingkem = Path() + private val scaledMingkem = Path() + private val fillMrengut = Path() + private val scaledMrengut = Path() + + // Based off of the mask, the fill will interpolate across this space + private val fillRect = RectF() + + // Top of this rect changes based on level, 100% == fillRect + private val levelRect = RectF() + private val levelPath = Path() + + // Updates the transform of the paths when our bounds change + private val scaleMatrix = Matrix() + private val padding = Rect() + + // The net result of fill + perimeter paths + private val unifiedPath = Path() + + // Bolt path (used while charging) + private val boltPath = Path() + private val scaledBolt = Path() + + // Plus sign (used for power save mode) + private val plusPath = Path() + private val scaledPlus = Path() + + private var intrinsicHeight: Int + private var intrinsicWidth: Int + + // To implement hysteresis, keep track of the need to invert the interior icon of the battery + private var invertFillIcon = false + + // Colors can be configured based on battery level (see res/values/arrays.xml) + private var colorLevels: IntArray + + private var fillColor: Int = Color.MAGENTA + private var backgroundColor: Int = Color.MAGENTA + + // updated whenever level changes + private var levelColor: Int = Color.MAGENTA + + // Dual tone implies that battery level is a clipped overlay over top of the whole shape + private var dualTone = false + + private var batteryLevel = 0 + + private var isRotation = false + private var customBlendColor = false + private var customFillRainbow = false + private var scaledFillAlpha = false + private var scaledPerimeterAlpha = false + + private var chargingColor: Int = Color.TRANSPARENT + private var customFillColor: Int = Color.BLACK + private var customFillGradColor: Int = Color.BLACK + private var powerSaveColor: Int = Color.TRANSPARENT + private var powerSaveFillColor: Int = Color.TRANSPARENT + + private val invalidateRunnable: () -> Unit = { + invalidateSelf() + } + + open var criticalLevel: Int = 5 + + var charging = false + set(value) { + field = value + postInvalidate() + } + + override fun setChargingEnabled(charging: Boolean) { + this.charging = charging + postInvalidate() + } + + var powerSaveEnabled = false + set(value) { + field = value + postInvalidate() + } + + override fun setPowerSavingEnabled(powerSaveEnabled: Boolean) { + this.powerSaveEnabled = powerSaveEnabled + postInvalidate() + } + + var showPercent = false + set(value) { + field = value + postInvalidate() + } + + override fun setShowPercentEnabled(showPercent: Boolean) { + this.showPercent = showPercent + postInvalidate() + } + + open fun customizeBatteryDrawable( + isRotation: Boolean, + scaledPerimeterAlpha: Boolean, + scaledFillAlpha: Boolean, + customBlendColor: Boolean, + customFillRainbow: Boolean, + customFillColor: Int, + customFillGradColor: Int, + chargingColor: Int, + powerSaveColor: Int, + powerSaveFillColor: Int + ) { + this.isRotation = isRotation + this.scaledPerimeterAlpha = scaledPerimeterAlpha + this.scaledFillAlpha = scaledFillAlpha + this.customBlendColor = customBlendColor + this.customFillRainbow = customFillRainbow + this.customFillColor = customFillColor + this.customFillGradColor = customFillGradColor + this.chargingColor = chargingColor + this.powerSaveColor = powerSaveColor + this.powerSaveFillColor = powerSaveFillColor + } + + private val fillColorStrokePaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + p.alpha = 255 + p.isDither = true + p.strokeWidth = 5f + p.style = Paint.Style.STROKE + p.blendMode = BlendMode.SRC + p.strokeMiter = 5f + p.strokeJoin = Paint.Join.ROUND + } + + private val fillColorStrokeProtection = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.isDither = true + p.strokeWidth = 5f + p.style = Paint.Style.STROKE + p.blendMode = BlendMode.CLEAR + p.strokeMiter = 5f + p.strokeJoin = Paint.Join.ROUND + } + + private val fillPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + p.alpha = 255 + p.isDither = true + p.strokeWidth = 0f + p.style = Paint.Style.FILL_AND_STROKE + } + + private val errorPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = context.resources.getColorStateList( + context.resources.getIdentifier( + "batterymeter_plus_color", "color", context.packageName + ), context.theme + ).defaultColor + p.alpha = 255 + p.isDither = true + p.strokeWidth = 0f + p.style = Paint.Style.FILL_AND_STROKE + p.blendMode = BlendMode.SRC + } + + private var fillPaintColor1: Int = 0xffff4931.toInt() + private val customFillPaint1 = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = fillPaintColor1 + p.alpha = 110 + p.isDither = true + p.strokeWidth = 0f + p.style = Paint.Style.FILL_AND_STROKE + p.blendMode = BlendMode.SRC + } + + private var fillPaintColor2: Int = 0xffff4931.toInt() + private val customFillPaint2 = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = fillPaintColor2 + p.alpha = 110 + p.isDither = true + p.strokeWidth = 0f + p.style = Paint.Style.FILL_AND_STROKE + p.blendMode = BlendMode.SRC + } + + private var fillPaintColor3: Int = 0xffff9e19.toInt() + private val customFillPaint3 = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = fillPaintColor3 + p.alpha = 110 + p.isDither = true + p.strokeWidth = 0f + p.style = Paint.Style.FILL_AND_STROKE + p.blendMode = BlendMode.SRC + } + + private var fillPaintColor4: Int = 0xffff9e19.toInt() + private val customFillPaint4 = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = fillPaintColor4 + p.alpha = 110 + p.isDither = true + p.strokeWidth = 0f + p.style = Paint.Style.FILL_AND_STROKE + p.blendMode = BlendMode.SRC + } + + private var fillPaintColor5: Int = 0xfffff32f.toInt() + private val customFillPaint5 = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = fillPaintColor5 + p.alpha = 110 + p.isDither = true + p.strokeWidth = 0f + p.style = Paint.Style.FILL_AND_STROKE + p.blendMode = BlendMode.SRC + } + + private var fillPaintColor6: Int = 0xfffff32f.toInt() + private val customFillPaint6 = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = fillPaintColor6 + p.alpha = 110 + p.isDither = true + p.strokeWidth = 0f + p.style = Paint.Style.FILL_AND_STROKE + p.blendMode = BlendMode.SRC + } + + private var fillPaintColor7: Int = 0xff9aff00.toInt() + private val customFillPaint7 = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = fillPaintColor7 + p.alpha = 110 + p.isDither = true + p.strokeWidth = 0f + p.style = Paint.Style.FILL_AND_STROKE + p.blendMode = BlendMode.SRC + } + + private var fillPaintColor8: Int = 0xff9aff00.toInt() + private val customFillPaint8 = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = fillPaintColor8 + p.alpha = 110 + p.isDither = true + p.strokeWidth = 0f + p.style = Paint.Style.FILL_AND_STROKE + p.blendMode = BlendMode.SRC + } + + private var fillPaintColor9: Int = 0xff2ed200.toInt() + private val customFillPaint9 = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = fillPaintColor9 + p.alpha = 110 + p.isDither = true + p.strokeWidth = 0f + p.style = Paint.Style.FILL_AND_STROKE + p.blendMode = BlendMode.SRC + } + + private var fillPaintColor10: Int = 0xff2ed200.toInt() + private val customFillPaint10 = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = fillPaintColor10 + p.alpha = 110 + p.isDither = true + p.strokeWidth = 0f + p.style = Paint.Style.FILL_AND_STROKE + p.blendMode = BlendMode.SRC + } + + private val defaultFillPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + p.alpha = 255 + p.isDither = true + p.strokeWidth = 0f + p.style = Paint.Style.FILL_AND_STROKE + p.blendMode = BlendMode.SRC + } + + private val chargingPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + } + + private val customFillPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + } + + private val powerSavePaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + } + + private val powerSaveFillPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + } + + private val scaledFillPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + } + + private val scaledPerimeterPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + } + + private val scaledPerimeterPaintDef = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + } + + // Only used if dualTone is set to true + private val dualToneBackgroundFill = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + p.alpha = 85 // ~0.3 alpha by default + p.isDither = true + p.strokeWidth = 0f + p.style = Paint.Style.FILL_AND_STROKE + } + + private val textPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.typeface = Typeface.create("sans-serif-condensed", Typeface.BOLD) + p.textAlign = Paint.Align.CENTER + } + + init { + val density = context.resources.displayMetrics.density + intrinsicHeight = (HEIGHT * density).toInt() + intrinsicWidth = (WIDTH * density).toInt() + + val res = context.resources + val levels = res.obtainTypedArray( + res.getIdentifier( + "batterymeter_color_levels", "array", context.packageName + ) + ) + val colors = res.obtainTypedArray( + res.getIdentifier( + "batterymeter_color_values", "array", context.packageName + ) + ) + val n = levels.length() + colorLevels = IntArray(2 * n) + for (i in 0 until n) { + colorLevels[2 * i] = levels.getInt(i, 0) + if (colors.getType(i) == TypedValue.TYPE_ATTRIBUTE) { + colorLevels[2 * i + 1] = SettingsLibUtils.getColorAttrDefaultColor( + colors.getResourceId(i, 0), context + ) + } else { + colorLevels[2 * i + 1] = colors.getColor(i, 0) + } + } + levels.recycle() + colors.recycle() + + loadPaths() + } + + override fun draw(c: Canvas) { + c.saveLayer(null, null) + unifiedPath.reset() + levelPath.reset() + levelRect.set(fillRect) + val fillFraction = batteryLevel / 100f + val fillRight = + if (batteryLevel == 100) + fillRect.right + 1f + else + fillRect.right - (fillRect.width() * (1 - fillFraction)) + + levelRect.right = floor(fillRight.toDouble()).toFloat() + levelPath.addRoundRect( + levelRect, + floatArrayOf( + 4.2f, + 4.2f, + 4.2f, + 4.2f, + 4.2f, + 4.2f, + 4.2f, + 4.2f + ), Path.Direction.CCW + ) + + scaledFillPaint.alpha = if (scaledFillAlpha) 100 else 0 + scaledPerimeterPaint.alpha = + if (scaledPerimeterAlpha) 100 else scaledPerimeterPaintDef.alpha + + // The perimeter should never change + c.drawPath(scaledFill, scaledFillPaint) + unifiedPath.addPath(scaledPerimeter) + // If drawing dual tone, the level is used only to clip the whole drawable path + if (!dualTone) { + unifiedPath.op(levelPath, Path.Op.UNION) + } + + fillPaint.color = levelColor + val black = Color.BLACK + + powerSavePaint.color = + if (customBlendColor && powerSaveColor != black) powerSaveColor else context.resources.getColorStateList( + context.resources.getIdentifier( + "batterymeter_plus_color", "color", context.packageName + ), context.theme + ).defaultColor + + // Deal with unifiedPath clipping before it draws + if (!showPercent) { + if (charging) { + // Clip out the bolt shape + unifiedPath.op(scaledBolt, Path.Op.DIFFERENCE) + if (!invertFillIcon) { + c.drawPath(scaledBolt, fillPaint) + } + } + } + + if (dualTone) { + // Dual tone means we draw the shape again, clipped to the charge level + c.drawPath(unifiedPath, dualToneBackgroundFill) + c.save() + c.clipRect( + bounds.left.toFloat(), + 0f, + bounds.right + bounds.width() * fillFraction, + bounds.left.toFloat() + ) + c.drawPath(unifiedPath, fillPaint) + c.restore() + } else { + // Non dual-tone means we draw the perimeter (with the level fill), and potentially + // draw the fill again with a critical color + if (customFillRainbow) { + if (charging) { + c.drawPath(scaledPerimeter, scaledPerimeterPaint) + c.clipOutPath(scaledfillOutline) + if (batteryLevel in 91..100) { + c.drawPath(scaledFill10, customFillPaint10) + c.drawPath(scaledFill9, customFillPaint9) + c.drawPath(scaledFill8, customFillPaint8) + c.drawPath(scaledFill7, customFillPaint7) + c.drawPath(scaledFill6, customFillPaint6) + c.drawPath(scaledFill5, customFillPaint5) + c.drawPath(scaledFill4, customFillPaint4) + c.drawPath(scaledFill3, customFillPaint3) + c.drawPath(scaledFill2, customFillPaint2) + c.drawPath(scaledFill1, customFillPaint1) + } + if (batteryLevel in 81..90) { + c.drawPath(scaledFill9, customFillPaint9) + c.drawPath(scaledFill8, customFillPaint8) + c.drawPath(scaledFill7, customFillPaint7) + c.drawPath(scaledFill6, customFillPaint6) + c.drawPath(scaledFill5, customFillPaint5) + c.drawPath(scaledFill4, customFillPaint4) + c.drawPath(scaledFill3, customFillPaint3) + c.drawPath(scaledFill2, customFillPaint2) + c.drawPath(scaledFill1, customFillPaint1) + } + if (batteryLevel in 71..80) { + c.drawPath(scaledFill8, customFillPaint8) + c.drawPath(scaledFill7, customFillPaint7) + c.drawPath(scaledFill6, customFillPaint6) + c.drawPath(scaledFill5, customFillPaint5) + c.drawPath(scaledFill4, customFillPaint4) + c.drawPath(scaledFill3, customFillPaint3) + c.drawPath(scaledFill2, customFillPaint2) + c.drawPath(scaledFill1, customFillPaint1) + } + if (batteryLevel in 61..70) { + c.drawPath(scaledFill7, customFillPaint7) + c.drawPath(scaledFill6, customFillPaint6) + c.drawPath(scaledFill5, customFillPaint5) + c.drawPath(scaledFill4, customFillPaint4) + c.drawPath(scaledFill3, customFillPaint3) + c.drawPath(scaledFill2, customFillPaint2) + c.drawPath(scaledFill1, customFillPaint1) + } + if (batteryLevel in 51..60) { + c.drawPath(scaledFill6, customFillPaint6) + c.drawPath(scaledFill5, customFillPaint5) + c.drawPath(scaledFill4, customFillPaint4) + c.drawPath(scaledFill3, customFillPaint3) + c.drawPath(scaledFill2, customFillPaint2) + c.drawPath(scaledFill1, customFillPaint1) + } + if (batteryLevel in 41..50) { + c.drawPath(scaledFill5, customFillPaint5) + c.drawPath(scaledFill4, customFillPaint4) + c.drawPath(scaledFill3, customFillPaint3) + c.drawPath(scaledFill2, customFillPaint2) + c.drawPath(scaledFill1, customFillPaint1) + } + if (batteryLevel in 31..40) { + c.drawPath(scaledFill4, customFillPaint4) + c.drawPath(scaledFill3, customFillPaint3) + c.drawPath(scaledFill2, customFillPaint2) + c.drawPath(scaledFill1, customFillPaint1) + } + if (batteryLevel in 21..30) { + c.drawPath(scaledFill3, customFillPaint3) + c.drawPath(scaledFill2, customFillPaint2) + c.drawPath(scaledFill1, customFillPaint1) + } + if (batteryLevel in 11..20) { + c.drawPath(scaledFill2, customFillPaint2) + c.drawPath(scaledFill1, customFillPaint1) + } + if (batteryLevel in 0..10) { + c.drawPath(scaledFill1, customFillPaint1) + } + c.drawPath(scaledBolt, fillPaint) + c.clipPath(scaledFill) + } else if (powerSaveEnabled) { + c.drawPath(scaledErrorPerimeter, powerSavePaint) + c.clipOutPath(scaledfillOutline) + if (batteryLevel in 91..100) { + c.drawPath(scaledFill10, customFillPaint10) + c.drawPath(scaledFill9, customFillPaint9) + c.drawPath(scaledFill8, customFillPaint8) + c.drawPath(scaledFill7, customFillPaint7) + c.drawPath(scaledFill6, customFillPaint6) + c.drawPath(scaledFill5, customFillPaint5) + c.drawPath(scaledFill4, customFillPaint4) + c.drawPath(scaledFill3, customFillPaint3) + c.drawPath(scaledFill2, customFillPaint2) + c.drawPath(scaledFill1, customFillPaint1) + } + if (batteryLevel in 81..90) { + c.drawPath(scaledFill9, customFillPaint9) + c.drawPath(scaledFill8, customFillPaint8) + c.drawPath(scaledFill7, customFillPaint7) + c.drawPath(scaledFill6, customFillPaint6) + c.drawPath(scaledFill5, customFillPaint5) + c.drawPath(scaledFill4, customFillPaint4) + c.drawPath(scaledFill3, customFillPaint3) + c.drawPath(scaledFill2, customFillPaint2) + c.drawPath(scaledFill1, customFillPaint1) + } + if (batteryLevel in 71..80) { + c.drawPath(scaledFill8, customFillPaint8) + c.drawPath(scaledFill7, customFillPaint7) + c.drawPath(scaledFill6, customFillPaint6) + c.drawPath(scaledFill5, customFillPaint5) + c.drawPath(scaledFill4, customFillPaint4) + c.drawPath(scaledFill3, customFillPaint3) + c.drawPath(scaledFill2, customFillPaint2) + c.drawPath(scaledFill1, customFillPaint1) + } + if (batteryLevel in 61..70) { + c.drawPath(scaledFill7, customFillPaint7) + c.drawPath(scaledFill6, customFillPaint6) + c.drawPath(scaledFill5, customFillPaint5) + c.drawPath(scaledFill4, customFillPaint4) + c.drawPath(scaledFill3, customFillPaint3) + c.drawPath(scaledFill2, customFillPaint2) + c.drawPath(scaledFill1, customFillPaint1) + } + if (batteryLevel in 51..60) { + c.drawPath(scaledFill6, customFillPaint6) + c.drawPath(scaledFill5, customFillPaint5) + c.drawPath(scaledFill4, customFillPaint4) + c.drawPath(scaledFill3, customFillPaint3) + c.drawPath(scaledFill2, customFillPaint2) + c.drawPath(scaledFill1, customFillPaint1) + } + if (batteryLevel in 41..50) { + c.drawPath(scaledFill5, customFillPaint5) + c.drawPath(scaledFill4, customFillPaint4) + c.drawPath(scaledFill3, customFillPaint3) + c.drawPath(scaledFill2, customFillPaint2) + c.drawPath(scaledFill1, customFillPaint1) + } + if (batteryLevel in 31..40) { + c.drawPath(scaledFill4, customFillPaint4) + c.drawPath(scaledFill3, customFillPaint3) + c.drawPath(scaledFill2, customFillPaint2) + c.drawPath(scaledFill1, customFillPaint1) + } + if (batteryLevel in 21..30) { + c.drawPath(scaledFill3, customFillPaint3) + c.drawPath(scaledFill2, customFillPaint2) + c.drawPath(scaledFill1, customFillPaint1) + } + if (batteryLevel in 11..20) { + c.drawPath(scaledFill2, customFillPaint2) + c.drawPath(scaledFill1, customFillPaint1) + } + if (batteryLevel in 0..10) { + c.drawPath(scaledFill1, customFillPaint1) + } + c.clipPath(scaledFill) + } else { + c.drawPath(scaledPerimeter, scaledPerimeterPaint) + c.clipOutPath(scaledfillOutline) + if (batteryLevel in 91..100) { + c.drawPath(scaledFill10, customFillPaint10) + c.drawPath(scaledFill9, customFillPaint9) + c.drawPath(scaledFill8, customFillPaint8) + c.drawPath(scaledFill7, customFillPaint7) + c.drawPath(scaledFill6, customFillPaint6) + c.drawPath(scaledFill5, customFillPaint5) + c.drawPath(scaledFill4, customFillPaint4) + c.drawPath(scaledFill3, customFillPaint3) + c.drawPath(scaledFill2, customFillPaint2) + c.drawPath(scaledFill1, customFillPaint1) + } + if (batteryLevel in 81..90) { + c.drawPath(scaledFill9, customFillPaint9) + c.drawPath(scaledFill8, customFillPaint8) + c.drawPath(scaledFill7, customFillPaint7) + c.drawPath(scaledFill6, customFillPaint6) + c.drawPath(scaledFill5, customFillPaint5) + c.drawPath(scaledFill4, customFillPaint4) + c.drawPath(scaledFill3, customFillPaint3) + c.drawPath(scaledFill2, customFillPaint2) + c.drawPath(scaledFill1, customFillPaint1) + } + if (batteryLevel in 71..80) { + c.drawPath(scaledFill8, customFillPaint8) + c.drawPath(scaledFill7, customFillPaint7) + c.drawPath(scaledFill6, customFillPaint6) + c.drawPath(scaledFill5, customFillPaint5) + c.drawPath(scaledFill4, customFillPaint4) + c.drawPath(scaledFill3, customFillPaint3) + c.drawPath(scaledFill2, customFillPaint2) + c.drawPath(scaledFill1, customFillPaint1) + } + if (batteryLevel in 61..70) { + c.drawPath(scaledFill7, customFillPaint7) + c.drawPath(scaledFill6, customFillPaint6) + c.drawPath(scaledFill5, customFillPaint5) + c.drawPath(scaledFill4, customFillPaint4) + c.drawPath(scaledFill3, customFillPaint3) + c.drawPath(scaledFill2, customFillPaint2) + c.drawPath(scaledFill1, customFillPaint1) + } + if (batteryLevel in 51..60) { + c.drawPath(scaledFill6, customFillPaint6) + c.drawPath(scaledFill5, customFillPaint5) + c.drawPath(scaledFill4, customFillPaint4) + c.drawPath(scaledFill3, customFillPaint3) + c.drawPath(scaledFill2, customFillPaint2) + c.drawPath(scaledFill1, customFillPaint1) + } + if (batteryLevel in 41..50) { + c.drawPath(scaledFill5, customFillPaint5) + c.drawPath(scaledFill4, customFillPaint4) + c.drawPath(scaledFill3, customFillPaint3) + c.drawPath(scaledFill2, customFillPaint2) + c.drawPath(scaledFill1, customFillPaint1) + } + if (batteryLevel in 31..40) { + c.drawPath(scaledFill4, customFillPaint4) + c.drawPath(scaledFill3, customFillPaint3) + c.drawPath(scaledFill2, customFillPaint2) + c.drawPath(scaledFill1, customFillPaint1) + } + if (batteryLevel in 21..30) { + c.drawPath(scaledFill3, customFillPaint3) + c.drawPath(scaledFill2, customFillPaint2) + c.drawPath(scaledFill1, customFillPaint1) + } + if (batteryLevel in 11..20) { + c.drawPath(scaledFill2, customFillPaint2) + c.drawPath(scaledFill1, customFillPaint1) + } + if (batteryLevel in 0..10) { + c.drawPath(scaledFill1, customFillPaint1) + } + c.clipPath(scaledFill) + } + } else { + // else rainbow + if (charging) { + c.drawPath(scaledPerimeter, scaledPerimeterPaint) + c.clipOutPath(scaledfillOutline) + chargingPaint.color = chargingColor + c.drawPath( + levelPath, + if (customBlendColor && chargingColor != black) chargingPaint else defaultFillPaint + ) + c.drawPath(scaledBolt, fillPaint) + c.clipPath(scaledFill) + } else if (powerSaveEnabled) { + c.drawPath(scaledErrorPerimeter, powerSavePaint) + c.clipOutPath(scaledfillOutline) + powerSaveFillPaint.color = powerSaveFillColor + c.drawPath( + levelPath, + if (customBlendColor && powerSaveFillColor != black) powerSaveFillPaint else defaultFillPaint + ) + c.clipPath(scaledFill) + } else { + c.drawPath(scaledPerimeter, scaledPerimeterPaint) + c.clipOutPath(scaledfillOutline) + customFillPaint.color = customFillColor + customFillPaint.shader = + if (customFillColor != black && customFillGradColor != black) LinearGradient( + levelRect.right, 0f, 0f, levelRect.bottom, + customFillColor, customFillGradColor, + Shader.TileMode.CLAMP + ) else null + c.drawPath( + levelPath, + if (customBlendColor && customFillColor != black) customFillPaint else defaultFillPaint + ) + c.clipPath(scaledFill) + } + } + if (!charging && !showPercent) { + if (batteryLevel in 71..100) { + c.drawPath(scaledNgguyu, fillPaint) + } + if (batteryLevel in 41..70) { + c.drawPath(scaledMingkem, fillPaint) + } + if (batteryLevel in 0..40) { + c.drawPath(scaledMrengut, fillPaint) + } + } + + // Show colorError below this level + if (batteryLevel <= CRITICAL_LEVEL && !charging && !customFillRainbow) { + c.save() + c.clipPath(scaledFill) + c.drawPath(levelPath, fillPaint) + c.restore() + } + } + + if (charging) { + c.clipOutPath(scaledBolt) + if (invertFillIcon) { + c.drawPath(scaledBolt, fillPaint) + } else { + c.drawPath(scaledBolt, fillPaint) + } + } else if (powerSaveEnabled) { + // If power save is enabled draw the perimeter path with colorError + c.drawPath(scaledErrorPerimeter, powerSavePaint) + // And draw the plus sign on top of the fill + if (!showPercent) { + c.drawPath(scaledPlus, powerSavePaint) + } + } + c.restore() + + if (showPercent && !charging) { + textPaint.textSize = bounds.width() * 0.24f + val textHeight = +textPaint.fontMetrics.ascent + val pctX = (bounds.width() + textHeight) * 0.56f + val pctY = bounds.height() * 0.65f + + textPaint.color = fillColor + if (isRotation) { + c.rotate(180f, pctX, pctY * 0.78f) + } + c.drawText(batteryLevel.toString(), pctX, pctY, textPaint) + c.restore() + } + } + + private fun batteryColorForLevel(level: Int): Int { + return when { + charging || powerSaveEnabled -> fillColor + else -> getColorForLevel(level) + } + } + + private fun getColorForLevel(level: Int): Int { + var thresh: Int + var color = 0 + var i = 0 + while (i < colorLevels.size) { + thresh = colorLevels[i] + color = colorLevels[i + 1] + if (level <= thresh) { + + // Respect tinting for "normal" level + return if (i == colorLevels.size - 2) { + fillColor + } else { + color + } + } + i += 2 + } + return color + } + + /** + * Alpha is unused internally, and should be defined in the colors passed to {@link setColors}. + * Further, setting an alpha for a dual tone battery meter doesn't make sense without bounds + * defining the minimum background fill alpha. This is because fill + background must be equal + * to the net alpha passed in here. + */ + override fun setAlpha(alpha: Int) { + } + + override fun setColorFilter(colorFilter: ColorFilter?) { + fillPaint.colorFilter = colorFilter + fillColorStrokePaint.colorFilter = colorFilter + dualToneBackgroundFill.colorFilter = colorFilter + } + + /** + * Deprecated, but required by Drawable + */ + @Deprecated( + "Deprecated in Java", + ReplaceWith("PixelFormat.OPAQUE", "android.graphics.PixelFormat"), + ) + override fun getOpacity(): Int { + return PixelFormat.OPAQUE + } + + override fun getIntrinsicHeight(): Int { + return intrinsicHeight + } + + override fun getIntrinsicWidth(): Int { + return intrinsicWidth + } + + /** + * Set the fill level + */ + @Suppress("PARAMETER_NAME_CHANGED_ON_OVERRIDE") + override fun setBatteryLevel(l: Int) { + // invertFillIcon = if (l >= 67) true else if (l <= 33) false else invertFillIcon + batteryLevel = l + levelColor = batteryColorForLevel(batteryLevel) + invalidateSelf() + } + + fun getBatteryLevel(): Int { + return batteryLevel + } + + override fun onBoundsChange(bounds: Rect) { + super.onBoundsChange(bounds) + updateSize() + } + + fun setPadding(left: Int, top: Int, right: Int, bottom: Int) { + padding.left = left + padding.top = top + padding.right = right + padding.bottom = bottom + + updateSize() + } + + override fun setColors(fgColor: Int, bgColor: Int, singleToneColor: Int) { + fillColor = if (dualTone) fgColor else singleToneColor + + fillPaint.color = fillColor + defaultFillPaint.color = fillColor + defaultFillPaint.alpha = 85 + fillColorStrokePaint.color = fillColor + + scaledFillPaint.color = fillColor + scaledPerimeterPaint.color = fillColor + scaledPerimeterPaintDef.color = fillColor + + backgroundColor = bgColor + dualToneBackgroundFill.color = bgColor + + // Also update the level color, since fillColor may have changed + levelColor = batteryColorForLevel(batteryLevel) + + invalidateSelf() + } + + private fun postInvalidate() { + unscheduleSelf(invalidateRunnable) + scheduleSelf(invalidateRunnable, 0) + } + + private fun updateSize() { + val b = bounds + if (b.isEmpty) { + scaleMatrix.setScale(1f, 1f) + } else { + scaleMatrix.setScale((b.right / WIDTH), (b.bottom / HEIGHT)) + } + + perimeterPath.transform(scaleMatrix, scaledPerimeter) + errorPerimeterPath.transform(scaleMatrix, scaledErrorPerimeter) + fillMask.transform(scaleMatrix, scaledFill) + scaledFill.computeBounds(fillRect, true) + fillOutlinePath.transform(scaleMatrix, scaledfillOutline) + fillMask1.transform(scaleMatrix, scaledFill1) + fillMask2.transform(scaleMatrix, scaledFill2) + fillMask3.transform(scaleMatrix, scaledFill3) + fillMask4.transform(scaleMatrix, scaledFill4) + fillMask5.transform(scaleMatrix, scaledFill5) + fillMask6.transform(scaleMatrix, scaledFill6) + fillMask7.transform(scaleMatrix, scaledFill7) + fillMask8.transform(scaleMatrix, scaledFill8) + fillMask9.transform(scaleMatrix, scaledFill9) + fillMask10.transform(scaleMatrix, scaledFill10) + fillNgguyu.transform(scaleMatrix, scaledNgguyu) + fillMingkem.transform(scaleMatrix, scaledMingkem) + fillMrengut.transform(scaleMatrix, scaledMrengut) + boltPath.transform(scaleMatrix, scaledBolt) + plusPath.transform(scaleMatrix, scaledPlus) + + // It is expected that this view only ever scale by the same factor in each dimension, so + // just pick one to scale the strokeWidths + val scaledStrokeWidth = + (b.right / WIDTH * PROTECTION_STROKE_WIDTH).coerceAtLeast(PROTECTION_MIN_STROKE_WIDTH) + + fillColorStrokePaint.strokeWidth = scaledStrokeWidth + fillColorStrokeProtection.strokeWidth = scaledStrokeWidth + } + + @SuppressLint("RestrictedApi") + private fun loadPaths() { + val pathString = modRes.getString(R.string.config_landscapeBatteryPerimeterPathI) + perimeterPath.set(PathParser.createPathFromPathData(pathString)) + perimeterPath.computeBounds(RectF(), true) + + val errorPathString = modRes.getString(R.string.config_landscapeBatteryErrorPerimeterPathI) + errorPerimeterPath.set(PathParser.createPathFromPathData(errorPathString)) + errorPerimeterPath.computeBounds(RectF(), true) + + val fillMaskString = modRes.getString(R.string.config_landscapeBatteryFillMaskI) + fillMask.set(PathParser.createPathFromPathData(fillMaskString)) + // Set the fill rect so we can calculate the fill properly + fillMask.computeBounds(fillRect, true) + + val fillOutlinePathString = + modRes.getString(R.string.config_landscapeBatteryFillOutlinePathI) + fillOutlinePath.set(PathParser.createPathFromPathData(fillOutlinePathString)) + fillOutlinePath.computeBounds(RectF(), true) + + val fillMask1String = modRes.getString(R.string.config_landscapeBatteryFillMaskI1) + fillMask1.set(PathParser.createPathFromPathData(fillMask1String)) + fillMask1.computeBounds(RectF(), true) + + val fillMask2String = modRes.getString(R.string.config_landscapeBatteryFillMaskI2) + fillMask2.set(PathParser.createPathFromPathData(fillMask2String)) + fillMask2.computeBounds(RectF(), true) + + val fillMask3String = modRes.getString(R.string.config_landscapeBatteryFillMaskI3) + fillMask3.set(PathParser.createPathFromPathData(fillMask3String)) + fillMask3.computeBounds(RectF(), true) + + val fillMask4String = modRes.getString(R.string.config_landscapeBatteryFillMaskI4) + fillMask4.set(PathParser.createPathFromPathData(fillMask4String)) + fillMask4.computeBounds(RectF(), true) + + val fillMask5String = modRes.getString(R.string.config_landscapeBatteryFillMaskI5) + fillMask5.set(PathParser.createPathFromPathData(fillMask5String)) + fillMask5.computeBounds(RectF(), true) + + val fillMask6String = modRes.getString(R.string.config_landscapeBatteryFillMaskI6) + fillMask6.set(PathParser.createPathFromPathData(fillMask6String)) + fillMask6.computeBounds(RectF(), true) + + val fillMask7String = modRes.getString(R.string.config_landscapeBatteryFillMaskI7) + fillMask7.set(PathParser.createPathFromPathData(fillMask7String)) + fillMask7.computeBounds(RectF(), true) + + val fillMask8String = modRes.getString(R.string.config_landscapeBatteryFillMaskI8) + fillMask8.set(PathParser.createPathFromPathData(fillMask8String)) + fillMask8.computeBounds(RectF(), true) + + val fillMask9String = modRes.getString(R.string.config_landscapeBatteryFillMaskI9) + fillMask9.set(PathParser.createPathFromPathData(fillMask9String)) + fillMask9.computeBounds(RectF(), true) + + val fillMask10String = modRes.getString(R.string.config_landscapeBatteryFillMaskI10) + fillMask10.set(PathParser.createPathFromPathData(fillMask10String)) + fillMask10.computeBounds(RectF(), true) + + val fillNgguyuString = modRes.getString(R.string.config_landscapeBatteryFillNgguyuI) + fillNgguyu.set(PathParser.createPathFromPathData(fillNgguyuString)) + fillNgguyu.computeBounds(RectF(), true) + + val fillMingkemString = modRes.getString(R.string.config_landscapeBatteryFillMingkemI) + fillMingkem.set(PathParser.createPathFromPathData(fillMingkemString)) + fillMingkem.computeBounds(RectF(), true) + + val fillMrengutString = modRes.getString(R.string.config_landscapeBatteryFillMrengutI) + fillMrengut.set(PathParser.createPathFromPathData(fillMrengutString)) + fillMrengut.computeBounds(RectF(), true) + + val boltPathString = modRes.getString(R.string.config_landscapeBatteryBoltPathI) + boltPath.set(PathParser.createPathFromPathData(boltPathString)) + + val plusPathString = modRes.getString(R.string.config_landscapeBatteryPowersavePathI) + plusPath.set(PathParser.createPathFromPathData(plusPathString)) + + dualTone = false + } + + companion object { + private const val TAG = "LandscapeBatteryI" + private const val WIDTH = 23f + private const val HEIGHT = 13f + private const val CRITICAL_LEVEL = 15 + + // On a 22x12 grid, how wide to make the fill protection stroke. + // Scales when our size changes + private const val PROTECTION_STROKE_WIDTH = 3f + + // Arbitrarily chosen for visibility at small sizes + private const val PROTECTION_MIN_STROKE_WIDTH = 6f + } +} diff --git a/app/src/main/java/com/drdisagree/iconify/xposed/mods/batterystyles/LandscapeBatteryJ.kt b/app/src/main/java/com/drdisagree/iconify/xposed/mods/batterystyles/LandscapeBatteryJ.kt new file mode 100644 index 000000000..8e0580c8e --- /dev/null +++ b/app/src/main/java/com/drdisagree/iconify/xposed/mods/batterystyles/LandscapeBatteryJ.kt @@ -0,0 +1,655 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.drdisagree.iconify.xposed.mods.batterystyles + +import android.annotation.SuppressLint +import android.content.Context +import android.graphics.BlendMode +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.ColorFilter +import android.graphics.LinearGradient +import android.graphics.Matrix +import android.graphics.Paint +import android.graphics.Path +import android.graphics.PixelFormat +import android.graphics.Rect +import android.graphics.RectF +import android.graphics.Shader +import android.graphics.Typeface +import android.util.TypedValue +import androidx.core.graphics.PathParser +import com.drdisagree.iconify.R +import com.drdisagree.iconify.xposed.HookRes.modRes +import com.drdisagree.iconify.xposed.utils.SettingsLibUtils +import kotlin.math.floor + +/** + * A battery meter drawable that respects paths configured in + * frameworks/base/core/res/res/values/config.xml to allow for an easily overrideable battery icon + */ +@SuppressLint("DiscouragedApi") +open class LandscapeBatteryJ(private val context: Context, frameColor: Int) : BatteryDrawable() { + + // Need to load: + // 1. perimeter shape + // 2. fill mask (if smaller than perimeter, this would create a fill that + // doesn't touch the walls + private val perimeterPath = Path() + private val scaledPerimeter = Path() + private val errorPerimeterPath = Path() + private val scaledErrorPerimeter = Path() + + // Fill will cover the whole bounding rect of the fillMask, and be masked by the path + private val fillMask = Path() + private val scaledFill = Path() + private val fillOutlinePath = Path() + private val scaledfillOutline = Path() + + // Based off of the mask, the fill will interpolate across this space + private val fillRect = RectF() + + // Top of this rect changes based on level, 100% == fillRect + private val levelRect = RectF() + private val levelPath = Path() + + // Updates the transform of the paths when our bounds change + private val scaleMatrix = Matrix() + private val padding = Rect() + + // The net result of fill + perimeter paths + private val unifiedPath = Path() + + // Bolt path (used while charging) + private val boltPath = Path() + private val scaledBolt = Path() + + // Plus sign (used for power save mode) + private val plusPath = Path() + private val scaledPlus = Path() + + private var intrinsicHeight: Int + private var intrinsicWidth: Int + + // To implement hysteresis, keep track of the need to invert the interior icon of the battery + private var invertFillIcon = false + + // Colors can be configured based on battery level (see res/values/arrays.xml) + private var colorLevels: IntArray + + private var fillColor: Int = Color.MAGENTA + private var backgroundColor: Int = Color.MAGENTA + + // updated whenever level changes + private var levelColor: Int = Color.MAGENTA + + // Dual tone implies that battery level is a clipped overlay over top of the whole shape + private var dualTone = false + + private var batteryLevel = 0 + + private var scaledFillAlpha = false + private var scaledPerimeterAlpha = false + private var customBlendColor = false + private var customFillRainbow = false + + private var chargingColor: Int = Color.TRANSPARENT + private var customFillColor: Int = Color.BLACK + private var customFillGradColor: Int = Color.BLACK + private var powerSaveColor: Int = Color.TRANSPARENT + private var powerSaveFillColor: Int = Color.TRANSPARENT + + private val invalidateRunnable: () -> Unit = { + invalidateSelf() + } + + open var criticalLevel: Int = 5 + + var charging = false + set(value) { + field = value + postInvalidate() + } + + override fun setChargingEnabled(charging: Boolean) { + this.charging = charging + postInvalidate() + } + + var powerSaveEnabled = false + set(value) { + field = value + postInvalidate() + } + + override fun setPowerSavingEnabled(powerSaveEnabled: Boolean) { + this.powerSaveEnabled = powerSaveEnabled + postInvalidate() + } + + var showPercent = false + set(value) { + field = value + postInvalidate() + } + + override fun setShowPercentEnabled(showPercent: Boolean) { + this.showPercent = showPercent + postInvalidate() + } + + var customChargingIcon = false + set(value) { + field = value + postInvalidate() + } + + open fun customizeBatteryDrawable( + scaledPerimeterAlpha: Boolean, + scaledFillAlpha: Boolean, + customBlendColor: Boolean, + customFillRainbow: Boolean, + customFillColor: Int, + customFillGradColor: Int, + chargingColor: Int, + powerSaveColor: Int, + powerSaveFillColor: Int + ) { + this.scaledPerimeterAlpha = scaledPerimeterAlpha + this.scaledFillAlpha = scaledFillAlpha + this.customBlendColor = customBlendColor + this.customFillRainbow = customFillRainbow + this.customFillColor = customFillColor + this.customFillGradColor = customFillGradColor + this.chargingColor = chargingColor + this.powerSaveColor = powerSaveColor + this.powerSaveFillColor = powerSaveFillColor + } + + private val fillColorStrokePaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + p.alpha = 255 + p.isDither = true + p.strokeWidth = 5f + p.style = Paint.Style.STROKE + p.blendMode = BlendMode.SRC + p.strokeMiter = 5f + p.strokeJoin = Paint.Join.ROUND + } + + private val fillColorStrokeProtection = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.isDither = true + p.strokeWidth = 5f + p.style = Paint.Style.STROKE + p.blendMode = BlendMode.CLEAR + p.strokeMiter = 5f + p.strokeJoin = Paint.Join.ROUND + } + + private val fillPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + p.alpha = 255 + p.isDither = true + p.strokeWidth = 0f + p.style = Paint.Style.FILL_AND_STROKE + } + + private val errorPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = context.resources.getColorStateList( + context.resources.getIdentifier( + "batterymeter_plus_color", "color", context.packageName + ), context.theme + ).defaultColor + p.alpha = 255 + p.isDither = true + p.strokeWidth = 0f + p.style = Paint.Style.FILL_AND_STROKE + p.blendMode = BlendMode.SRC + } + + private var coloringFill1: Int = 0xffff4931.toInt() + private var coloringFill2: Int = 0xffff9e19.toInt() + private var coloringFill3: Int = 0xffe7de51.toInt() + private var coloringFill4: Int = 0xff2ed200.toInt() + + private val coloringFillPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + } + + private val chargingPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + } + + private val customFillPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + } + + private val powerSavePaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + } + + private val powerSaveFillPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + } + + private val scaledFillPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + } + + private val scaledPerimeterPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + } + + private val scaledPerimeterPaintDef = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + } + + // Only used if dualTone is set to true + private val dualToneBackgroundFill = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + p.alpha = 85 // ~0.3 alpha by default + p.isDither = true + p.strokeWidth = 0f + p.style = Paint.Style.FILL_AND_STROKE + } + + private val textPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.typeface = Typeface.create("sans-serif-condensed", Typeface.BOLD) + p.textAlign = Paint.Align.CENTER + } + + init { + val density = context.resources.displayMetrics.density + intrinsicHeight = (HEIGHT * density).toInt() + intrinsicWidth = (WIDTH * density).toInt() + + val res = context.resources + val levels = res.obtainTypedArray( + res.getIdentifier( + "batterymeter_color_levels", "array", context.packageName + ) + ) + val colors = res.obtainTypedArray( + res.getIdentifier( + "batterymeter_color_values", "array", context.packageName + ) + ) + val n = levels.length() + colorLevels = IntArray(2 * n) + for (i in 0 until n) { + colorLevels[2 * i] = levels.getInt(i, 0) + if (colors.getType(i) == TypedValue.TYPE_ATTRIBUTE) { + colorLevels[2 * i + 1] = SettingsLibUtils.getColorAttrDefaultColor( + colors.getResourceId(i, 0), context + ) + } else { + colorLevels[2 * i + 1] = colors.getColor(i, 0) + } + } + levels.recycle() + colors.recycle() + + loadPaths() + } + + override fun draw(c: Canvas) { + c.saveLayer(null, null) + unifiedPath.reset() + levelPath.reset() + levelRect.set(fillRect) + val fillFraction = batteryLevel / 100f + val fillRight = + if (batteryLevel == 100) + fillRect.right + 1f + else + fillRect.right - (fillRect.width() * (1 - fillFraction)) + val fillBottom = fillRect.bottom + 1f + + levelRect.right = floor(fillRight.toDouble()).toFloat() + levelRect.bottom = floor(fillBottom.toDouble()).toFloat() + levelPath.addRect(levelRect, Path.Direction.CCW) + + scaledFillPaint.alpha = if (scaledFillAlpha) 100 else 0 + scaledPerimeterPaint.alpha = + if (scaledPerimeterAlpha) 100 else scaledPerimeterPaintDef.alpha + + // The perimeter should never change + c.drawPath(scaledFill, scaledFillPaint) + c.drawPath(scaledPerimeter, scaledPerimeterPaint) + // If drawing dual tone, the level is used only to clip the whole drawable path + if (!dualTone) { + unifiedPath.op(levelPath, Path.Op.UNION) + } + + fillPaint.color = levelColor + val black = Color.BLACK + + if (customBlendColor && !customFillRainbow) { + chargingPaint.color = + if (chargingColor == black) Color.TRANSPARENT else chargingColor + + powerSavePaint.color = + if (powerSaveColor == black) context.resources.getColorStateList( + context.resources.getIdentifier( + "batterymeter_plus_color", "color", context.packageName + ), context.theme + ).defaultColor else powerSaveColor + + powerSaveFillPaint.color = + if (powerSaveFillColor == black) Color.TRANSPARENT else powerSaveFillColor + } else { + chargingPaint.color = Color.TRANSPARENT + powerSavePaint.color = context.resources.getColorStateList( + context.resources.getIdentifier( + "batterymeter_plus_color", "color", context.packageName + ), context.theme + ).defaultColor + powerSaveFillPaint.color = Color.TRANSPARENT + } + + if (batteryLevel >= 83) { + coloringFillPaint.color = coloringFill4 + } else if (batteryLevel >= 55) { + coloringFillPaint.color = coloringFill3 + } else if (batteryLevel >= 28) { + coloringFillPaint.color = coloringFill2 + } else if (batteryLevel >= 0) { + coloringFillPaint.color = coloringFill1 + } + + // Deal with unifiedPath clipping before it draws + if (charging && !customChargingIcon) { + // Clip out the bolt shape + unifiedPath.op(scaledBolt, Path.Op.DIFFERENCE) + if (!invertFillIcon) { + c.drawPath(scaledBolt, fillPaint) + } + } + + if (dualTone) { + // Dual tone means we draw the shape again, clipped to the charge level + c.drawPath(unifiedPath, dualToneBackgroundFill) + c.save() + c.clipRect( + bounds.left.toFloat(), + 0f, + bounds.right + bounds.width() * fillFraction, + bounds.left.toFloat() + ) + c.drawPath(unifiedPath, fillPaint) + c.restore() + } else { + // Non dual-tone means we draw the perimeter (with the level fill), and potentially + // draw the fill again with a critical color + if (customBlendColor && !customFillRainbow) { + if (charging) { + fillPaint.color = fillColor + c.clipOutPath(scaledfillOutline) + c.drawPath(unifiedPath, fillPaint) + fillPaint.color = levelColor + } else if (powerSaveEnabled) { + c.drawPath(scaledErrorPerimeter, powerSavePaint) + c.clipOutPath(scaledfillOutline) + c.drawPath(levelPath, fillPaint) + c.drawPath(levelPath, powerSaveFillPaint) + } else { + c.clipOutPath(scaledfillOutline) + customFillPaint.color = customFillColor + customFillPaint.shader = + if (customFillColor != black && customFillGradColor != black) LinearGradient( + levelRect.right, 0f, 0f, levelRect.bottom, + customFillColor, customFillGradColor, + Shader.TileMode.CLAMP + ) else null + c.drawPath( + levelPath, + if (customFillColor == black) fillPaint else customFillPaint + ) + } + } else { + if (customFillRainbow) { + if (charging) { + c.clipOutPath(scaledfillOutline) + c.drawPath(unifiedPath, coloringFillPaint) + } else if (powerSaveEnabled) { + c.drawPath(scaledErrorPerimeter, errorPaint) + c.clipOutPath(scaledfillOutline) + c.drawPath(unifiedPath, coloringFillPaint) + } else { + c.clipOutPath(scaledfillOutline) + c.drawPath(unifiedPath, coloringFillPaint) + } + } else { + if (charging) { + c.clipOutPath(scaledfillOutline) + c.drawPath(unifiedPath, fillPaint) + } else if (powerSaveEnabled) { + c.drawPath(scaledErrorPerimeter, errorPaint) + c.clipOutPath(scaledfillOutline) + c.drawPath(unifiedPath, fillPaint) + } else { + c.clipOutPath(scaledfillOutline) + c.drawPath(unifiedPath, fillPaint) + } + } + } + } + + if (charging) { + if (!customChargingIcon) { + c.clipOutPath(scaledBolt) + c.drawPath(levelPath, chargingPaint) + if (invertFillIcon) { + c.drawPath(scaledBolt, fillColorStrokePaint) + } else { + c.drawPath(scaledBolt, fillColorStrokeProtection) + } + } else { + c.drawPath(levelPath, chargingPaint) + } + } else if (powerSaveEnabled) { + // If power save is enabled draw the perimeter path with colorError + c.drawPath(scaledErrorPerimeter, powerSavePaint) + c.drawPath(levelPath, powerSaveFillPaint) + // And draw the plus sign on top of the fill + if (!showPercent) { + c.drawPath(scaledPlus, powerSavePaint) + } + } + c.restore() + } + + private fun batteryColorForLevel(level: Int): Int { + return when { + charging || powerSaveEnabled -> fillColor + else -> getColorForLevel(level) + } + } + + private fun getColorForLevel(level: Int): Int { + var thresh: Int + var color = 0 + var i = 0 + while (i < colorLevels.size) { + thresh = colorLevels[i] + color = colorLevels[i + 1] + if (level <= thresh) { + + // Respect tinting for "normal" level + return if (i == colorLevels.size - 2) { + fillColor + } else { + color + } + } + i += 2 + } + return color + } + + /** + * Alpha is unused internally, and should be defined in the colors passed to {@link setColors}. + * Further, setting an alpha for a dual tone battery meter doesn't make sense without bounds + * defining the minimum background fill alpha. This is because fill + background must be equal + * to the net alpha passed in here. + */ + override fun setAlpha(alpha: Int) { + } + + override fun setColorFilter(colorFilter: ColorFilter?) { + fillPaint.colorFilter = colorFilter + fillColorStrokePaint.colorFilter = colorFilter + dualToneBackgroundFill.colorFilter = colorFilter + } + + /** + * Deprecated, but required by Drawable + */ + @Deprecated( + "Deprecated in Java", + ReplaceWith("PixelFormat.OPAQUE", "android.graphics.PixelFormat"), + ) + override fun getOpacity(): Int { + return PixelFormat.OPAQUE + } + + override fun getIntrinsicHeight(): Int { + return intrinsicHeight + } + + override fun getIntrinsicWidth(): Int { + return intrinsicWidth + } + + /** + * Set the fill level + */ + @Suppress("PARAMETER_NAME_CHANGED_ON_OVERRIDE") + override fun setBatteryLevel(l: Int) { + // invertFillIcon = if (l >= 67) true else if (l <= 33) false else invertFillIcon + batteryLevel = l + levelColor = batteryColorForLevel(batteryLevel) + invalidateSelf() + } + + fun getBatteryLevel(): Int { + return batteryLevel + } + + override fun onBoundsChange(bounds: Rect) { + super.onBoundsChange(bounds) + updateSize() + } + + fun setPadding(left: Int, top: Int, right: Int, bottom: Int) { + padding.left = left + padding.top = top + padding.right = right + padding.bottom = bottom + + updateSize() + } + + override fun setColors(fgColor: Int, bgColor: Int, singleToneColor: Int) { + fillColor = if (dualTone) fgColor else singleToneColor + + fillPaint.color = fillColor + fillColorStrokePaint.color = fillColor + + scaledFillPaint.color = fillColor + scaledPerimeterPaint.color = fillColor + scaledPerimeterPaintDef.color = fillColor + + backgroundColor = bgColor + dualToneBackgroundFill.color = bgColor + + // Also update the level color, since fillColor may have changed + levelColor = batteryColorForLevel(batteryLevel) + + invalidateSelf() + } + + private fun postInvalidate() { + unscheduleSelf(invalidateRunnable) + scheduleSelf(invalidateRunnable, 0) + } + + private fun updateSize() { + val b = bounds + if (b.isEmpty) { + scaleMatrix.setScale(1f, 1f) + } else { + scaleMatrix.setScale((b.right / WIDTH), (b.bottom / HEIGHT)) + } + + perimeterPath.transform(scaleMatrix, scaledPerimeter) + errorPerimeterPath.transform(scaleMatrix, scaledErrorPerimeter) + fillMask.transform(scaleMatrix, scaledFill) + scaledFill.computeBounds(fillRect, true) + fillOutlinePath.transform(scaleMatrix, scaledfillOutline) + boltPath.transform(scaleMatrix, scaledBolt) + plusPath.transform(scaleMatrix, scaledPlus) + + // It is expected that this view only ever scale by the same factor in each dimension, so + // just pick one to scale the strokeWidths + val scaledStrokeWidth = + (b.right / WIDTH * PROTECTION_STROKE_WIDTH).coerceAtLeast(PROTECTION_MIN_STROKE_WIDTH) + + fillColorStrokePaint.strokeWidth = scaledStrokeWidth + fillColorStrokeProtection.strokeWidth = scaledStrokeWidth + } + + @SuppressLint("RestrictedApi") + private fun loadPaths() { + val pathString = modRes.getString(R.string.config_landscapeBatteryPerimeterPathJ) + perimeterPath.set(PathParser.createPathFromPathData(pathString)) + perimeterPath.computeBounds(RectF(), true) + + val errorPathString = modRes.getString(R.string.config_landscapeBatteryErrorPerimeterPathJ) + errorPerimeterPath.set(PathParser.createPathFromPathData(errorPathString)) + errorPerimeterPath.computeBounds(RectF(), true) + + val fillMaskString = modRes.getString(R.string.config_landscapeBatteryFillMaskJ) + fillMask.set(PathParser.createPathFromPathData(fillMaskString)) + // Set the fill rect so we can calculate the fill properly + fillMask.computeBounds(fillRect, true) + + val fillOutlinePathString = modRes.getString(R.string.config_landscapeBatteryFillOutlineJ) + fillOutlinePath.set(PathParser.createPathFromPathData(fillOutlinePathString)) + fillOutlinePath.computeBounds(RectF(), true) + + val boltPathString = modRes.getString(R.string.config_landscapeBatteryBoltPathJ) + boltPath.set(PathParser.createPathFromPathData(boltPathString)) + + val plusPathString = modRes.getString(R.string.config_landscapeBatteryPowersavePathJ) + plusPath.set(PathParser.createPathFromPathData(plusPathString)) + + dualTone = false + } + + companion object { + private const val TAG = "LandscapeBatteryJ" + private const val WIDTH = 20f + private const val HEIGHT = 12f + private const val CRITICAL_LEVEL = 15 + + // On a 20x12 grid, how wide to make the fill protection stroke. + // Scales when our size changes + private const val PROTECTION_STROKE_WIDTH = 2f + + // Arbitrarily chosen for visibility at small sizes + private const val PROTECTION_MIN_STROKE_WIDTH = 5f + } +} diff --git a/app/src/main/java/com/drdisagree/iconify/xposed/mods/batterystyles/LandscapeBatteryK.kt b/app/src/main/java/com/drdisagree/iconify/xposed/mods/batterystyles/LandscapeBatteryK.kt new file mode 100644 index 000000000..6e662257c --- /dev/null +++ b/app/src/main/java/com/drdisagree/iconify/xposed/mods/batterystyles/LandscapeBatteryK.kt @@ -0,0 +1,695 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.drdisagree.iconify.xposed.mods.batterystyles + +import android.annotation.SuppressLint +import android.content.Context +import android.graphics.BlendMode +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.ColorFilter +import android.graphics.LinearGradient +import android.graphics.Matrix +import android.graphics.Paint +import android.graphics.Path +import android.graphics.PixelFormat +import android.graphics.Rect +import android.graphics.RectF +import android.graphics.Shader +import android.graphics.Typeface +import android.util.TypedValue +import androidx.core.graphics.PathParser +import com.drdisagree.iconify.R +import com.drdisagree.iconify.xposed.HookRes.modRes +import com.drdisagree.iconify.xposed.utils.SettingsLibUtils +import kotlin.math.floor + +/** + * A battery meter drawable that respects paths configured in + * frameworks/base/core/res/res/values/config.xml to allow for an easily overrideable battery icon + */ +@SuppressLint("DiscouragedApi") +open class LandscapeBatteryK(private val context: Context, frameColor: Int) : BatteryDrawable() { + + // Need to load: + // 1. perimeter shape + // 2. fill mask (if smaller than perimeter, this would create a fill that + // doesn't touch the walls + private val perimeterPath = Path() + private val scaledPerimeter = Path() + private val errorPerimeterPath = Path() + private val scaledErrorPerimeter = Path() + + // Fill will cover the whole bounding rect of the fillMask, and be masked by the path + private val fillMask = Path() + private val scaledFill = Path() + + // Based off of the mask, the fill will interpolate across this space + private val fillRect = RectF() + + // Top of this rect changes based on level, 100% == fillRect + private val levelRect = RectF() + private val levelPath = Path() + + // Updates the transform of the paths when our bounds change + private val scaleMatrix = Matrix() + private val padding = Rect() + + // The net result of fill + perimeter paths + private val unifiedPath = Path() + + // Bolt path (used while charging) + private val boltPath = Path() + private val scaledBolt = Path() + + // Plus sign (used for power save mode) + private val plusPath = Path() + private val scaledPlus = Path() + + private var intrinsicHeight: Int + private var intrinsicWidth: Int + + // To implement hysteresis, keep track of the need to invert the interior icon of the battery + private var invertFillIcon = false + + // Colors can be configured based on battery level (see res/values/arrays.xml) + private var colorLevels: IntArray + + private var fillColor: Int = Color.MAGENTA + private var backgroundColor: Int = Color.MAGENTA + + // updated whenever level changes + private var levelColor: Int = Color.MAGENTA + + // Dual tone implies that battery level is a clipped overlay over top of the whole shape + private var dualTone = false + + private var batteryLevel = 0 + + private var isRotation = false + private var scaledFillAlpha = false + private var scaledPerimeterAlpha = false + private var customBlendColor = false + + private var chargingColor: Int = Color.TRANSPARENT + private var customFillColor: Int = Color.BLACK + private var customFillGradColor: Int = Color.BLACK + private var powerSaveColor: Int = Color.TRANSPARENT + private var powerSaveFillColor: Int = Color.TRANSPARENT + + private val invalidateRunnable: () -> Unit = { + invalidateSelf() + } + + open var criticalLevel: Int = 5 + + var charging = false + set(value) { + field = value + postInvalidate() + } + + override fun setChargingEnabled(charging: Boolean) { + this.charging = charging + postInvalidate() + } + + var powerSaveEnabled = false + set(value) { + field = value + postInvalidate() + } + + override fun setPowerSavingEnabled(powerSaveEnabled: Boolean) { + this.powerSaveEnabled = powerSaveEnabled + postInvalidate() + } + + var showPercent = false + set(value) { + field = value + postInvalidate() + } + + override fun setShowPercentEnabled(showPercent: Boolean) { + this.showPercent = showPercent + postInvalidate() + } + + var customChargingIcon = false + set(value) { + field = value + postInvalidate() + } + + open fun customizeBatteryDrawable( + isRotation: Boolean, + scaledPerimeterAlpha: Boolean, + scaledFillAlpha: Boolean, + customBlendColor: Boolean, + customFillColor: Int, + customFillGradColor: Int, + chargingColor: Int, + powerSaveColor: Int, + powerSaveFillColor: Int + ) { + this.isRotation = isRotation + this.scaledPerimeterAlpha = scaledPerimeterAlpha + this.scaledFillAlpha = scaledFillAlpha + this.customBlendColor = customBlendColor + this.customFillColor = customFillColor + this.customFillGradColor = customFillGradColor + this.chargingColor = chargingColor + this.powerSaveColor = powerSaveColor + this.powerSaveFillColor = powerSaveFillColor + } + + private val fillColorStrokePaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + p.alpha = 255 + p.isDither = true + p.strokeWidth = 5f + p.style = Paint.Style.STROKE + p.blendMode = BlendMode.SRC + p.strokeMiter = 5f + p.strokeJoin = Paint.Join.ROUND + } + + private val fillColorStrokeProtection = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.isDither = true + p.strokeWidth = 5f + p.style = Paint.Style.STROKE + p.blendMode = BlendMode.CLEAR + p.strokeMiter = 5f + p.strokeJoin = Paint.Join.ROUND + } + + private val fillPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + p.alpha = 255 + p.isDither = true + p.strokeWidth = 0f + p.style = Paint.Style.FILL_AND_STROKE + } + + private val errorPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = context.resources.getColorStateList( + context.resources.getIdentifier( + "batterymeter_plus_color", "color", context.packageName + ), context.theme + ).defaultColor + p.alpha = 255 + p.isDither = true + p.strokeWidth = 0f + p.style = Paint.Style.FILL_AND_STROKE + p.blendMode = BlendMode.SRC + } + + private val chargingPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + } + + private val customFillPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + } + + private val powerSavePaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + } + + private val powerSaveFillPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + } + + private val scaledFillPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + } + + private val scaledPerimeterPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + } + + private val scaledPerimeterPaintDef = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + } + + // Only used if dualTone is set to true + private val dualToneBackgroundFill = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + p.alpha = 85 // ~0.3 alpha by default + p.isDither = true + p.strokeWidth = 0f + p.style = Paint.Style.FILL_AND_STROKE + } + + private val textPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.typeface = percentTypeface() + p.textAlign = Paint.Align.CENTER + } + + private fun percentTypeface(): Typeface { + val typefaceBuilder: Typeface.Builder? + return try { + typefaceBuilder = Typeface.Builder(modRes.assets, "Fonts/SanFranciscoText-Semibold.otf") + typefaceBuilder.build() ?: Typeface.create("sans-serif-condensed", Typeface.BOLD) + } catch (e: Exception) { + Typeface.DEFAULT + } + } + + init { + val density = context.resources.displayMetrics.density + intrinsicHeight = (HEIGHT * density).toInt() + intrinsicWidth = (WIDTH * density).toInt() + + val res = context.resources + val levels = res.obtainTypedArray( + res.getIdentifier( + "batterymeter_color_levels", "array", context.packageName + ) + ) + val colors = res.obtainTypedArray( + res.getIdentifier( + "batterymeter_color_values", "array", context.packageName + ) + ) + val n = levels.length() + colorLevels = IntArray(2 * n) + for (i in 0 until n) { + colorLevels[2 * i] = levels.getInt(i, 0) + if (colors.getType(i) == TypedValue.TYPE_ATTRIBUTE) { + colorLevels[2 * i + 1] = SettingsLibUtils.getColorAttrDefaultColor( + colors.getResourceId(i, 0), context + ) + } else { + colorLevels[2 * i + 1] = colors.getColor(i, 0) + } + } + levels.recycle() + colors.recycle() + + loadPaths() + } + + override fun draw(c: Canvas) { + c.saveLayer(null, null) + unifiedPath.reset() + levelPath.reset() + levelRect.set(fillRect) + val fillFraction = batteryLevel / 100f + val fillRight = + if (batteryLevel == 100) + fillRect.right + 0.4f + else + fillRect.right - (fillRect.width() * (1 - fillFraction)) + + levelRect.right = floor(fillRight.toDouble()).toFloat() + levelPath.addRoundRect( + levelRect, + floatArrayOf( + 3.6f, + 3.6f, + 3.6f, + 3.6f, + 3.6f, + 3.6f, + 3.6f, + 3.6f + ), Path.Direction.CCW + ) + + scaledFillPaint.alpha = if (scaledFillAlpha) 100 else 0 + scaledPerimeterPaint.alpha = + if (scaledPerimeterAlpha) 100 else scaledPerimeterPaintDef.alpha + + // The perimeter should never change + c.drawPath(scaledFill, scaledFillPaint) + c.drawPath(scaledPerimeter, scaledPerimeterPaint) + // If drawing dual tone, the level is used only to clip the whole drawable path + if (!dualTone) { + unifiedPath.op(levelPath, Path.Op.UNION) + } + + fillPaint.color = levelColor + val black = Color.BLACK + + // Deal with unifiedPath clipping before it draws + if (charging && !customChargingIcon) { + // Clip out the bolt shape + unifiedPath.op(scaledBolt, Path.Op.DIFFERENCE) + if (!invertFillIcon) { + c.drawPath(scaledBolt, fillPaint) + } + } + + if (dualTone) { + // Dual tone means we draw the shape again, clipped to the charge level + c.drawPath(unifiedPath, dualToneBackgroundFill) + c.save() + c.clipRect( + bounds.left.toFloat(), + 0f, + bounds.right + bounds.width() * fillFraction, + bounds.left.toFloat() + ) + c.drawPath(unifiedPath, fillPaint) + c.restore() + } else { + // Non dual-tone means we draw the perimeter (with the level fill), and potentially + // draw the fill again with a critical color + if (customBlendColor) { + if (charging) { + fillPaint.color = fillColor + c.drawPath(unifiedPath, fillPaint) + fillPaint.color = levelColor + } else { + // Show colorError below this level + if (batteryLevel <= CRITICAL_LEVEL) { + c.save() + c.clipPath(scaledFill) + c.drawPath(levelPath, fillPaint) + c.restore() + } else { + customFillPaint.color = customFillColor + customFillPaint.shader = + if (customFillColor != black && customFillGradColor != black) LinearGradient( + levelRect.right, 0f, 0f, levelRect.bottom, + customFillColor, customFillGradColor, + Shader.TileMode.CLAMP + ) else null + c.drawPath( + levelPath, + if (customFillColor == black) fillPaint else customFillPaint + ) + } + } + } else { + // Show colorError below this level + if (batteryLevel <= CRITICAL_LEVEL && !charging) { + c.save() + c.clipPath(scaledFill) + c.drawPath(levelPath, fillPaint) + c.restore() + } else { + fillPaint.color = fillColor + c.drawPath(unifiedPath, fillPaint) + fillPaint.color = levelColor + } + } + } + + if (customBlendColor) { + chargingPaint.color = + if (chargingColor == black) Color.TRANSPARENT else chargingColor + + powerSavePaint.color = + if (powerSaveColor == black) context.resources.getColorStateList( + context.resources.getIdentifier( + "batterymeter_plus_color", "color", context.packageName + ), context.theme + ).defaultColor else powerSaveColor + + powerSaveFillPaint.color = + if (powerSaveFillColor == black) Color.TRANSPARENT else powerSaveFillColor + } else { + chargingPaint.color = Color.TRANSPARENT + powerSavePaint.color = context.resources.getColorStateList( + context.resources.getIdentifier( + "batterymeter_plus_color", "color", context.packageName + ), context.theme + ).defaultColor + powerSaveFillPaint.color = Color.TRANSPARENT + } + + if (charging) { + if (!customChargingIcon) { + c.clipOutPath(scaledBolt) + c.drawPath(levelPath, chargingPaint) + if (invertFillIcon) { + c.drawPath(scaledBolt, fillColorStrokePaint) + } else { + c.drawPath(scaledBolt, fillColorStrokeProtection) + } + } else { + c.drawPath(levelPath, chargingPaint) + } + } else if (powerSaveEnabled) { + // If power save is enabled draw the perimeter path with colorError + if (!customBlendColor && powerSaveFillColor != black) { + c.drawPath(scaledErrorPerimeter, powerSavePaint) + } + c.drawPath(levelPath, powerSaveFillPaint) + // And draw the plus sign on top of the fill + if (!showPercent) { + c.drawPath(scaledPlus, powerSavePaint) + } + } + c.restore() + + if (showPercent && !charging) { + textPaint.textSize = bounds.width() * 0.26f + val textHeight = +textPaint.fontMetrics.ascent + val pctX = (bounds.width() + textHeight) * 0.58f + val pctY = bounds.height() * 0.66f + + if (isRotation) { + c.rotate(180f, pctX + 0.4f, pctY * 0.76f) + } + if (customBlendColor) { + if (powerSaveEnabled && powerSaveFillColor == black) { + textPaint.color = fillColor + c.drawText(batteryLevel.toString(), pctX, pctY, textPaint) + + textPaint.color = fillColor.toInt().inv() or 0xFF000000.toInt() + c.save() + c.clipRect( + if (isRotation) fillRect.left + (fillRect.width() * (1 - fillFraction)) else fillRect.left, + fillRect.top, + if (isRotation) fillRect.right else fillRect.right - (fillRect.width() * (1 - fillFraction)), + fillRect.bottom + ) + c.drawText(batteryLevel.toString(), pctX, pctY, textPaint) + } else if (!powerSaveEnabled && customFillColor == black) { + textPaint.color = fillColor + c.drawText(batteryLevel.toString(), pctX, pctY, textPaint) + + textPaint.color = fillColor.toInt().inv() or 0xFF000000.toInt() + c.save() + c.clipRect( + if (isRotation) fillRect.left + (fillRect.width() * (1 - fillFraction)) else fillRect.left, + fillRect.top, + if (isRotation) fillRect.right else fillRect.right - (fillRect.width() * (1 - fillFraction)), + fillRect.bottom + ) + c.drawText(batteryLevel.toString(), pctX, pctY, textPaint) + } else { + textPaint.color = fillColor + c.drawText(batteryLevel.toString(), pctX, pctY, textPaint) + } + } else { + textPaint.color = fillColor + c.drawText(batteryLevel.toString(), pctX, pctY, textPaint) + + textPaint.color = fillColor.toInt().inv() or 0xFF000000.toInt() + c.save() + c.clipRect( + if (isRotation) fillRect.left + (fillRect.width() * (1 - fillFraction)) else fillRect.left, + fillRect.top, + if (isRotation) fillRect.right else fillRect.right - (fillRect.width() * (1 - fillFraction)), + fillRect.bottom + ) + c.drawText(batteryLevel.toString(), pctX, pctY, textPaint) + } + c.restore() + } + } + + private fun batteryColorForLevel(level: Int): Int { + return when { + charging || powerSaveEnabled -> fillColor + else -> getColorForLevel(level) + } + } + + private fun getColorForLevel(level: Int): Int { + var thresh: Int + var color = 0 + var i = 0 + while (i < colorLevels.size) { + thresh = colorLevels[i] + color = colorLevels[i + 1] + if (level <= thresh) { + + // Respect tinting for "normal" level + return if (i == colorLevels.size - 2) { + fillColor + } else { + color + } + } + i += 2 + } + return color + } + + /** + * Alpha is unused internally, and should be defined in the colors passed to {@link setColors}. + * Further, setting an alpha for a dual tone battery meter doesn't make sense without bounds + * defining the minimum background fill alpha. This is because fill + background must be equal + * to the net alpha passed in here. + */ + override fun setAlpha(alpha: Int) { + } + + override fun setColorFilter(colorFilter: ColorFilter?) { + fillPaint.colorFilter = colorFilter + fillColorStrokePaint.colorFilter = colorFilter + dualToneBackgroundFill.colorFilter = colorFilter + } + + /** + * Deprecated, but required by Drawable + */ + @Deprecated( + "Deprecated in Java", + ReplaceWith("PixelFormat.OPAQUE", "android.graphics.PixelFormat"), + ) + override fun getOpacity(): Int { + return PixelFormat.OPAQUE + } + + override fun getIntrinsicHeight(): Int { + return intrinsicHeight + } + + override fun getIntrinsicWidth(): Int { + return intrinsicWidth + } + + /** + * Set the fill level + */ + @Suppress("PARAMETER_NAME_CHANGED_ON_OVERRIDE") + override fun setBatteryLevel(l: Int) { + // invertFillIcon = if (l >= 67) true else if (l <= 33) false else invertFillIcon + batteryLevel = l + levelColor = batteryColorForLevel(batteryLevel) + invalidateSelf() + } + + fun getBatteryLevel(): Int { + return batteryLevel + } + + override fun onBoundsChange(bounds: Rect) { + super.onBoundsChange(bounds) + updateSize() + } + + fun setPadding(left: Int, top: Int, right: Int, bottom: Int) { + padding.left = left + padding.top = top + padding.right = right + padding.bottom = bottom + + updateSize() + } + + override fun setColors(fgColor: Int, bgColor: Int, singleToneColor: Int) { + fillColor = if (dualTone) fgColor else singleToneColor + + fillPaint.color = fillColor + fillColorStrokePaint.color = fillColor + + scaledFillPaint.color = fillColor + scaledPerimeterPaint.color = fillColor + scaledPerimeterPaintDef.color = fillColor + + backgroundColor = bgColor + dualToneBackgroundFill.color = bgColor + + // Also update the level color, since fillColor may have changed + levelColor = batteryColorForLevel(batteryLevel) + + invalidateSelf() + } + + private fun postInvalidate() { + unscheduleSelf(invalidateRunnable) + scheduleSelf(invalidateRunnable, 0) + } + + private fun updateSize() { + val b = bounds + if (b.isEmpty) { + scaleMatrix.setScale(1f, 1f) + } else { + scaleMatrix.setScale((b.right / WIDTH), (b.bottom / HEIGHT)) + } + + perimeterPath.transform(scaleMatrix, scaledPerimeter) + errorPerimeterPath.transform(scaleMatrix, scaledErrorPerimeter) + fillMask.transform(scaleMatrix, scaledFill) + scaledFill.computeBounds(fillRect, true) + boltPath.transform(scaleMatrix, scaledBolt) + plusPath.transform(scaleMatrix, scaledPlus) + + // It is expected that this view only ever scale by the same factor in each dimension, so + // just pick one to scale the strokeWidths + val scaledStrokeWidth = + (b.right / WIDTH * PROTECTION_STROKE_WIDTH).coerceAtLeast(PROTECTION_MIN_STROKE_WIDTH) + + fillColorStrokePaint.strokeWidth = scaledStrokeWidth + fillColorStrokeProtection.strokeWidth = scaledStrokeWidth + } + + @SuppressLint("RestrictedApi") + private fun loadPaths() { + val pathString = modRes.getString(R.string.config_landscapeBatteryPerimeterPathK) + perimeterPath.set(PathParser.createPathFromPathData(pathString)) + perimeterPath.computeBounds(RectF(), true) + + val errorPathString = modRes.getString(R.string.config_landscapeBatteryErrorPerimeterPathK) + errorPerimeterPath.set(PathParser.createPathFromPathData(errorPathString)) + errorPerimeterPath.computeBounds(RectF(), true) + + val fillMaskString = modRes.getString(R.string.config_landscapeBatteryFillMaskK) + fillMask.set(PathParser.createPathFromPathData(fillMaskString)) + // Set the fill rect so we can calculate the fill properly + fillMask.computeBounds(fillRect, true) + + val boltPathString = modRes.getString(R.string.config_landscapeBatteryBoltPathK) + boltPath.set(PathParser.createPathFromPathData(boltPathString)) + + val plusPathString = modRes.getString(R.string.config_landscapeBatteryPowersavePathK) + plusPath.set(PathParser.createPathFromPathData(plusPathString)) + + dualTone = false + } + + companion object { + private const val TAG = "LandscapeBatteryK" + private const val WIDTH = 22f + private const val HEIGHT = 12f + private const val CRITICAL_LEVEL = 15 + + // On a 22x12 grid, how wide to make the fill protection stroke. + // Scales when our size changes + private const val PROTECTION_STROKE_WIDTH = 2f + + // Arbitrarily chosen for visibility at small sizes + private const val PROTECTION_MIN_STROKE_WIDTH = 5f + } +} diff --git a/app/src/main/java/com/drdisagree/iconify/xposed/mods/batterystyles/LandscapeBatteryL.kt b/app/src/main/java/com/drdisagree/iconify/xposed/mods/batterystyles/LandscapeBatteryL.kt new file mode 100644 index 000000000..f940ca3fc --- /dev/null +++ b/app/src/main/java/com/drdisagree/iconify/xposed/mods/batterystyles/LandscapeBatteryL.kt @@ -0,0 +1,686 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.drdisagree.iconify.xposed.mods.batterystyles + +import android.annotation.SuppressLint +import android.content.Context +import android.graphics.BlendMode +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.ColorFilter +import android.graphics.LinearGradient +import android.graphics.Matrix +import android.graphics.Paint +import android.graphics.Path +import android.graphics.PixelFormat +import android.graphics.PorterDuff +import android.graphics.PorterDuffXfermode +import android.graphics.Rect +import android.graphics.RectF +import android.graphics.Shader +import android.graphics.Typeface +import android.util.TypedValue +import androidx.core.graphics.PathParser +import com.drdisagree.iconify.R +import com.drdisagree.iconify.xposed.HookRes.modRes +import com.drdisagree.iconify.xposed.utils.SettingsLibUtils +import kotlin.math.floor + +/** + * A battery meter drawable that respects paths configured in + * frameworks/base/core/res/res/values/config.xml to allow for an easily overrideable battery icon + */ +@SuppressLint("DiscouragedApi") +open class LandscapeBatteryL(private val context: Context, frameColor: Int) : BatteryDrawable() { + + // Need to load: + // 1. perimeter shape + // 2. fill mask (if smaller than perimeter, this would create a fill that + // doesn't touch the walls + private val perimeterPath = Path() + private val scaledPerimeter = Path() + private val errorPerimeterPath = Path() + private val scaledErrorPerimeter = Path() + + // Fill will cover the whole bounding rect of the fillMask, and be masked by the path + private val fillMask = Path() + private val scaledFill = Path() + private val fillOutlinePath = Path() + private val scaledfillOutline = Path() + + // Based off of the mask, the fill will interpolate across this space + private val fillRect = RectF() + + // Top of this rect changes based on level, 100% == fillRect + private val levelRect = RectF() + private val levelPath = Path() + + // Updates the transform of the paths when our bounds change + private val scaleMatrix = Matrix() + private val padding = Rect() + + // The net result of fill + perimeter paths + private val unifiedPath = Path() + + // Bolt path (used while charging) + private val boltPath = Path() + private val scaledBolt = Path() + + // Plus sign (used for power save mode) + private val plusPath = Path() + private val scaledPlus = Path() + + private var intrinsicHeight: Int + private var intrinsicWidth: Int + + // To implement hysteresis, keep track of the need to invert the interior icon of the battery + private var invertFillIcon = false + + // Colors can be configured based on battery level (see res/values/arrays.xml) + private var colorLevels: IntArray + + private var fillColor: Int = Color.MAGENTA + private var backgroundColor: Int = Color.MAGENTA + + // updated whenever level changes + private var levelColor: Int = Color.MAGENTA + + // Dual tone implies that battery level is a clipped overlay over top of the whole shape + private var dualTone = false + + private var batteryLevel = 0 + + private var isRotation = false + private var customBlendColor = false + + private var chargingColor: Int = Color.TRANSPARENT + private var customFillColor: Int = Color.BLACK + private var customFillGradColor: Int = Color.BLACK + private var powerSaveColor: Int = Color.TRANSPARENT + private var powerSaveFillColor: Int = Color.TRANSPARENT + + private val invalidateRunnable: () -> Unit = { + invalidateSelf() + } + + open var criticalLevel: Int = 5 + + var charging = false + set(value) { + field = value + postInvalidate() + } + + override fun setChargingEnabled(charging: Boolean) { + this.charging = charging + postInvalidate() + } + + var powerSaveEnabled = false + set(value) { + field = value + postInvalidate() + } + + override fun setPowerSavingEnabled(powerSaveEnabled: Boolean) { + this.powerSaveEnabled = powerSaveEnabled + postInvalidate() + } + + var showPercent = false + set(value) { + field = value + postInvalidate() + } + + override fun setShowPercentEnabled(showPercent: Boolean) { + this.showPercent = showPercent + postInvalidate() + } + + private var isQsPercent = false + set(value) { + field = value + postInvalidate() + } + + var customChargingIcon = false + set(value) { + field = value + postInvalidate() + } + + open fun customizeBatteryDrawable( + isRotation: Boolean, + customBlendColor: Boolean, + customFillColor: Int, + customFillGradColor: Int, + chargingColor: Int, + powerSaveColor: Int, + powerSaveFillColor: Int + ) { + this.isRotation = isRotation + this.customBlendColor = customBlendColor + this.customFillColor = customFillColor + this.customFillGradColor = customFillGradColor + this.chargingColor = chargingColor + this.powerSaveColor = powerSaveColor + this.powerSaveFillColor = powerSaveFillColor + } + + private val fillColorStrokePaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + p.alpha = 255 + p.isDither = true + p.strokeWidth = 5f + p.style = Paint.Style.STROKE + p.blendMode = BlendMode.SRC + p.strokeMiter = 5f + p.strokeJoin = Paint.Join.ROUND + } + + private val fillColorStrokeProtection = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.isDither = true + p.strokeWidth = 5f + p.style = Paint.Style.STROKE + p.blendMode = BlendMode.CLEAR + p.strokeMiter = 5f + p.strokeJoin = Paint.Join.ROUND + } + + private val fillPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + p.alpha = 255 + p.isDither = true + p.strokeWidth = 0f + p.style = Paint.Style.FILL_AND_STROKE + } + + private val errorPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = context.resources.getColorStateList( + context.resources.getIdentifier( + "batterymeter_plus_color", "color", context.packageName + ), context.theme + ).defaultColor + p.alpha = 255 + p.isDither = true + p.strokeWidth = 0f + p.style = Paint.Style.FILL_AND_STROKE + p.blendMode = BlendMode.SRC + } + + private val boltPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = Color.WHITE + } + + private val chargingAlphaPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + } + + private val chargingPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + } + + private val customFillAlphaPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + } + + private val customFillPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + } + + private val powerSavePaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + } + + private val powerSaveFillPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + } + + private val scaledPerimeterPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + } + + // Only used if dualTone is set to true + private val dualToneBackgroundFill = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + p.alpha = 85 // ~0.3 alpha by default + p.isDither = true + p.strokeWidth = 0f + p.style = Paint.Style.FILL_AND_STROKE + } + + private val textPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.typeface = percentTypeface() + p.textAlign = Paint.Align.CENTER + p.xfermode = PorterDuffXfermode(PorterDuff.Mode.DST_OUT) + } + + private val textChargingPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.typeface = percentTypeface() + p.textAlign = Paint.Align.CENTER + p.color = Color.WHITE + } + + private val textQsPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.typeface = percentTypeface() + p.textAlign = Paint.Align.CENTER + } + + private fun percentTypeface(): Typeface { + val typefaceBuilder: Typeface.Builder? + return try { + typefaceBuilder = Typeface.Builder(modRes.assets, "Fonts/SanFranciscoText-Semibold.otf") + typefaceBuilder.build() ?: Typeface.create("sans-serif-condensed", Typeface.BOLD) + } catch (e: Exception) { + Typeface.DEFAULT + } + } + + init { + val density = context.resources.displayMetrics.density + intrinsicHeight = (HEIGHT * density).toInt() + intrinsicWidth = (WIDTH * density).toInt() + + val res = context.resources + val levels = res.obtainTypedArray( + res.getIdentifier( + "batterymeter_color_levels", "array", context.packageName + ) + ) + val colors = res.obtainTypedArray( + res.getIdentifier( + "batterymeter_color_values", "array", context.packageName + ) + ) + val n = levels.length() + colorLevels = IntArray(2 * n) + for (i in 0 until n) { + colorLevels[2 * i] = levels.getInt(i, 0) + if (colors.getType(i) == TypedValue.TYPE_ATTRIBUTE) { + colorLevels[2 * i + 1] = SettingsLibUtils.getColorAttrDefaultColor( + colors.getResourceId(i, 0), context + ) + } else { + colorLevels[2 * i + 1] = colors.getColor(i, 0) + } + } + levels.recycle() + colors.recycle() + + loadPaths() + } + + override fun draw(c: Canvas) { + c.saveLayer(null, null) + unifiedPath.reset() + levelPath.reset() + levelRect.set(fillRect) + val fillFraction = batteryLevel / 100f + val fillRight = + if (batteryLevel == 100) + fillRect.right + 1f + else + fillRect.right - (fillRect.width() * (1 - fillFraction)) + + levelRect.right = floor(fillRight.toDouble()).toFloat() + levelPath.addRect(levelRect, Path.Direction.CCW) + + // If drawing dual tone, the level is used only to clip the whole drawable path + if (!dualTone) { + unifiedPath.op(levelPath, Path.Op.UNION) + } + + fillPaint.color = levelColor + val black = Color.BLACK + val chargingParseColor = Color.parseColor("#ff3ab74e") + val powerSaveParseColor = Color.parseColor("#fffdd015") + chargingAlphaPaint.color = + if (customBlendColor && chargingColor != black) chargingColor else chargingParseColor + chargingPaint.color = + if (customBlendColor && chargingColor != black) chargingColor else chargingParseColor + powerSavePaint.color = + if (customBlendColor && powerSaveColor != black) powerSaveColor else powerSaveParseColor + powerSaveFillPaint.color = + if (customBlendColor && powerSaveFillColor != black) powerSaveFillColor else powerSaveParseColor + + customFillAlphaPaint.alpha = 85 + chargingAlphaPaint.alpha = 85 + powerSavePaint.alpha = 85 + + // The perimeter should never change + if (charging) { + c.drawPath(scaledPerimeter, chargingAlphaPaint) + } else if (powerSaveEnabled) { + c.drawPath(scaledPerimeter, powerSavePaint) + c.drawPath(scaledPlus, powerSaveFillPaint) + } else { + customFillAlphaPaint.color = customFillColor + customFillAlphaPaint.shader = + if (customFillColor != black && customFillGradColor != black) LinearGradient( + levelRect.right, 0f, 0f, levelRect.bottom, + customFillColor, customFillGradColor, + Shader.TileMode.CLAMP + ) else null + customFillAlphaPaint.alpha = 85 + c.drawPath( + scaledPerimeter, + if (customBlendColor && customFillColor != black) customFillAlphaPaint else scaledPerimeterPaint + ) + } + + // Deal with unifiedPath clipping before it draws + if (charging && !customChargingIcon) { + // Clip out the bolt shape + unifiedPath.op(scaledBolt, Path.Op.DIFFERENCE) + if (!invertFillIcon) { + c.drawPath(scaledBolt, boltPaint) + } + } + + if (dualTone) { + // Dual tone means we draw the shape again, clipped to the charge level + c.drawPath(unifiedPath, dualToneBackgroundFill) + c.save() + c.clipRect( + bounds.left.toFloat(), + 0f, + bounds.right + bounds.width() * fillFraction, + bounds.left.toFloat() + ) + c.drawPath(unifiedPath, fillPaint) + c.restore() + } else { + // Non dual-tone means we draw the perimeter (with the level fill), and potentially + // draw the fill again with a critical color + if (charging) { + fillPaint.color = fillColor + c.clipOutPath(scaledfillOutline) + c.drawPath(unifiedPath, chargingPaint) + fillPaint.color = levelColor + } else if (powerSaveEnabled) { + c.drawPath(scaledErrorPerimeter, errorPaint) + c.clipOutPath(scaledfillOutline) + c.drawPath(levelPath, powerSaveFillPaint) + } else { + fillPaint.color = fillColor + customFillPaint.color = customFillColor + customFillPaint.shader = + if (customFillColor != black && customFillGradColor != black) LinearGradient( + levelRect.right, 0f, 0f, levelRect.bottom, + customFillColor, customFillGradColor, + Shader.TileMode.CLAMP + ) else null + c.clipOutPath(scaledfillOutline) + c.drawPath( + unifiedPath, + if (customBlendColor && customFillColor != black) customFillPaint else fillPaint + ) + fillPaint.color = levelColor + } + + // Show colorError below this level + if (batteryLevel <= CRITICAL_LEVEL && !charging) { + c.save() + c.clipPath(scaledFill) + c.drawPath(levelPath, fillPaint) + c.restore() + } + } + + if (charging) { + c.clipOutPath(scaledBolt) + } else if (powerSaveEnabled) { + // If power save is enabled draw the perimeter path with colorError + c.drawPath(scaledErrorPerimeter, errorPaint) + } + c.restore() + + if (charging || batteryLevel <= CRITICAL_LEVEL) { + textChargingPaint.textSize = bounds.width() * if (customChargingIcon) 0.42f else 0.38f + val textHeight = +textChargingPaint.fontMetrics.ascent + val pctXcharging = if (customChargingIcon) 0.76f else 0.59f + val pctX100 = if (customChargingIcon) 0.76f else 0.54f + val pctX = (bounds.width() + textHeight) * + (if (!charging) 0.72f /* discharging */ + else if (batteryLevel < 100) pctXcharging /* charging */ + else pctX100) /* level == 100 */ /* charging */ + val pctY = bounds.height() * if (customChargingIcon) 0.79f else 0.76f + + if (isRotation) { + c.rotate(180f, pctX, pctY * if (customChargingIcon) 0.63f else 0.66f) + } + c.save() + c.drawText(batteryLevel.toString(), pctX, pctY, textChargingPaint) + c.restore() + } else { + textPaint.textSize = bounds.width() * 0.42f + textQsPaint.textSize = textPaint.textSize + val textHeight = +textPaint.fontMetrics.ascent + val pctX = (bounds.width() + textHeight) * 0.76f + val pctY = bounds.height() * 0.79f + + textPaint.color = fillColor + textQsPaint.color = SettingsLibUtils.getColorAttrDefaultColor( + context, + android.R.attr.textColorPrimaryInverse + ) + if (isRotation) { + c.rotate(180f, pctX, pctY * 0.63f) + } + c.drawText( + batteryLevel.toString(), + pctX, + pctY, + if (isQsPercent) textQsPaint else textPaint + ) + + textPaint.color = fillColor.toInt().inv() + textQsPaint.color = SettingsLibUtils.getColorAttrDefaultColor( + context, + android.R.attr.textColorPrimaryInverse + ) + c.save() + c.drawText( + batteryLevel.toString(), + pctX, + pctY, + if (isQsPercent) textQsPaint else textPaint + ) + c.restore() + } + } + + private fun batteryColorForLevel(level: Int): Int { + return when { + charging || powerSaveEnabled -> fillColor + else -> getColorForLevel(level) + } + } + + private fun getColorForLevel(level: Int): Int { + var thresh: Int + var color = 0 + var i = 0 + while (i < colorLevels.size) { + thresh = colorLevels[i] + color = colorLevels[i + 1] + if (level <= thresh) { + + // Respect tinting for "normal" level + return if (i == colorLevels.size - 2) { + fillColor + } else { + color + } + } + i += 2 + } + return color + } + + /** + * Alpha is unused internally, and should be defined in the colors passed to {@link setColors}. + * Further, setting an alpha for a dual tone battery meter doesn't make sense without bounds + * defining the minimum background fill alpha. This is because fill + background must be equal + * to the net alpha passed in here. + */ + override fun setAlpha(alpha: Int) { + } + + override fun setColorFilter(colorFilter: ColorFilter?) { + fillPaint.colorFilter = colorFilter + fillColorStrokePaint.colorFilter = colorFilter + dualToneBackgroundFill.colorFilter = colorFilter + } + + /** + * Deprecated, but required by Drawable + */ + @Deprecated( + "Deprecated in Java", + ReplaceWith("PixelFormat.OPAQUE", "android.graphics.PixelFormat"), + ) + override fun getOpacity(): Int { + return PixelFormat.OPAQUE + } + + override fun getIntrinsicHeight(): Int { + return intrinsicHeight + } + + override fun getIntrinsicWidth(): Int { + return intrinsicWidth + } + + /** + * Set the fill level + */ + @Suppress("PARAMETER_NAME_CHANGED_ON_OVERRIDE") + override fun setBatteryLevel(l: Int) { + // invertFillIcon = if (l >= 67) true else if (l <= 33) false else invertFillIcon + batteryLevel = l + levelColor = batteryColorForLevel(batteryLevel) + invalidateSelf() + } + + fun getBatteryLevel(): Int { + return batteryLevel + } + + override fun onBoundsChange(bounds: Rect) { + super.onBoundsChange(bounds) + updateSize() + } + + fun setPadding(left: Int, top: Int, right: Int, bottom: Int) { + padding.left = left + padding.top = top + padding.right = right + padding.bottom = bottom + + updateSize() + } + + override fun setColors(fgColor: Int, bgColor: Int, singleToneColor: Int) { + fillColor = if (dualTone) fgColor else singleToneColor + + fillPaint.color = fillColor + fillColorStrokePaint.color = fillColor + + scaledPerimeterPaint.color = fillColor + scaledPerimeterPaint.alpha = 85 + + backgroundColor = bgColor + dualToneBackgroundFill.color = bgColor + + // Also update the level color, since fillColor may have changed + levelColor = batteryColorForLevel(batteryLevel) + + invalidateSelf() + } + + private fun postInvalidate() { + unscheduleSelf(invalidateRunnable) + scheduleSelf(invalidateRunnable, 0) + } + + private fun updateSize() { + val b = bounds + if (b.isEmpty) { + scaleMatrix.setScale(1f, 1f) + } else { + scaleMatrix.setScale((b.right / WIDTH), (b.bottom / HEIGHT)) + } + + perimeterPath.transform(scaleMatrix, scaledPerimeter) + errorPerimeterPath.transform(scaleMatrix, scaledErrorPerimeter) + fillMask.transform(scaleMatrix, scaledFill) + scaledFill.computeBounds(fillRect, true) + fillOutlinePath.transform(scaleMatrix, scaledfillOutline) + boltPath.transform(scaleMatrix, scaledBolt) + plusPath.transform(scaleMatrix, scaledPlus) + + // It is expected that this view only ever scale by the same factor in each dimension, so + // just pick one to scale the strokeWidths + val scaledStrokeWidth = + (b.right / WIDTH * PROTECTION_STROKE_WIDTH).coerceAtLeast(PROTECTION_MIN_STROKE_WIDTH) + + fillColorStrokePaint.strokeWidth = scaledStrokeWidth + fillColorStrokeProtection.strokeWidth = scaledStrokeWidth + } + + @SuppressLint("RestrictedApi") + private fun loadPaths() { + val pathString = modRes.getString(R.string.config_landscapeBatteryPerimeterPathL) + perimeterPath.set(PathParser.createPathFromPathData(pathString)) + perimeterPath.computeBounds(RectF(), true) + + val errorPathString = modRes.getString(R.string.config_landscapeBatteryErrorPerimeterPathL) + errorPerimeterPath.set(PathParser.createPathFromPathData(errorPathString)) + errorPerimeterPath.computeBounds(RectF(), true) + + val fillMaskString = modRes.getString(R.string.config_landscapeBatteryFillMaskL) + fillMask.set(PathParser.createPathFromPathData(fillMaskString)) + // Set the fill rect so we can calculate the fill properly + fillMask.computeBounds(fillRect, true) + + val fillOutlinePathString = modRes.getString(R.string.config_landscapeBatteryFillOutlineL) + fillOutlinePath.set(PathParser.createPathFromPathData(fillOutlinePathString)) + fillOutlinePath.computeBounds(RectF(), true) + + val boltPathString = modRes.getString(R.string.config_landscapeBatteryBoltPathL) + boltPath.set(PathParser.createPathFromPathData(boltPathString)) + + val plusPathString = modRes.getString(R.string.config_landscapeBatteryPowersavePathL) + plusPath.set(PathParser.createPathFromPathData(plusPathString)) + + dualTone = false + } + + companion object { + private const val TAG = "LandscapeBatteryL" + private const val WIDTH = 24f + private const val HEIGHT = 12f + private const val CRITICAL_LEVEL = 15 + + // On a 24x12 grid, how wide to make the fill protection stroke. + // Scales when our size changes + private const val PROTECTION_STROKE_WIDTH = 2f + + // Arbitrarily chosen for visibility at small sizes + private const val PROTECTION_MIN_STROKE_WIDTH = 5f + } +} diff --git a/app/src/main/java/com/drdisagree/iconify/xposed/mods/batterystyles/LandscapeBatteryM.kt b/app/src/main/java/com/drdisagree/iconify/xposed/mods/batterystyles/LandscapeBatteryM.kt new file mode 100644 index 000000000..4e14dd2d4 --- /dev/null +++ b/app/src/main/java/com/drdisagree/iconify/xposed/mods/batterystyles/LandscapeBatteryM.kt @@ -0,0 +1,628 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.drdisagree.iconify.xposed.mods.batterystyles + +import android.annotation.SuppressLint +import android.content.Context +import android.graphics.BlendMode +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.ColorFilter +import android.graphics.LinearGradient +import android.graphics.Matrix +import android.graphics.Paint +import android.graphics.Path +import android.graphics.PixelFormat +import android.graphics.Rect +import android.graphics.RectF +import android.graphics.Shader +import android.graphics.Typeface +import android.util.TypedValue +import androidx.core.graphics.PathParser +import com.drdisagree.iconify.R +import com.drdisagree.iconify.xposed.HookRes.modRes +import com.drdisagree.iconify.xposed.utils.SettingsLibUtils +import kotlin.math.floor + +/** + * A battery meter drawable that respects paths configured in + * frameworks/base/core/res/res/values/config.xml to allow for an easily overrideable battery icon + */ +@SuppressLint("DiscouragedApi") +open class LandscapeBatteryM(private val context: Context, frameColor: Int) : BatteryDrawable() { + + // Need to load: + // 1. perimeter shape + // 2. fill mask (if smaller than perimeter, this would create a fill that + // doesn't touch the walls + private val perimeterPath = Path() + private val scaledPerimeter = Path() + private val errorPerimeterPath = Path() + private val scaledErrorPerimeter = Path() + + // Fill will cover the whole bounding rect of the fillMask, and be masked by the path + private val fillMask = Path() + private val scaledFill = Path() + private val fillOutlinePath = Path() + private val scaledfillOutline = Path() + + // Based off of the mask, the fill will interpolate across this space + private val fillRect = RectF() + + // Top of this rect changes based on level, 100% == fillRect + private val levelRect = RectF() + private val levelPath = Path() + + // Updates the transform of the paths when our bounds change + private val scaleMatrix = Matrix() + private val padding = Rect() + + // The net result of fill + perimeter paths + private val unifiedPath = Path() + + // Bolt path (used while charging) + private val boltPath = Path() + private val scaledBolt = Path() + + // Plus sign (used for power save mode) + private val plusPath = Path() + private val scaledPlus = Path() + + private var intrinsicHeight: Int + private var intrinsicWidth: Int + + // To implement hysteresis, keep track of the need to invert the interior icon of the battery + private var invertFillIcon = false + + // Colors can be configured based on battery level (see res/values/arrays.xml) + private var colorLevels: IntArray + + private var fillColor: Int = Color.MAGENTA + private var backgroundColor: Int = Color.MAGENTA + + // updated whenever level changes + private var levelColor: Int = Color.MAGENTA + + // Dual tone implies that battery level is a clipped overlay over top of the whole shape + private var dualTone = false + + private var batteryLevel = 0 + + private var isRotation = false + private var customBlendColor = false + + private var chargingColor: Int = Color.TRANSPARENT + private var customFillColor: Int = Color.BLACK + private var customFillGradColor: Int = Color.BLACK + private var powerSaveColor: Int = Color.TRANSPARENT + private var powerSaveFillColor: Int = Color.TRANSPARENT + + private val invalidateRunnable: () -> Unit = { + invalidateSelf() + } + + open var criticalLevel: Int = 5 + + var charging = false + set(value) { + field = value + postInvalidate() + } + + override fun setChargingEnabled(charging: Boolean) { + this.charging = charging + postInvalidate() + } + + var powerSaveEnabled = false + set(value) { + field = value + postInvalidate() + } + + override fun setPowerSavingEnabled(powerSaveEnabled: Boolean) { + this.powerSaveEnabled = powerSaveEnabled + postInvalidate() + } + + var showPercent = false + set(value) { + field = value + postInvalidate() + } + + override fun setShowPercentEnabled(showPercent: Boolean) { + this.showPercent = showPercent + postInvalidate() + } + + var customChargingIcon = false + set(value) { + field = value + postInvalidate() + } + + open fun customizeBatteryDrawable( + isRotation: Boolean, + customBlendColor: Boolean, + customFillColor: Int, + customFillGradColor: Int, + chargingColor: Int, + powerSaveColor: Int, + powerSaveFillColor: Int + ) { + this.isRotation = isRotation + this.customBlendColor = customBlendColor + this.customFillColor = customFillColor + this.customFillGradColor = customFillGradColor + this.chargingColor = chargingColor + this.powerSaveColor = powerSaveColor + this.powerSaveFillColor = powerSaveFillColor + } + + private val fillColorStrokePaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + p.alpha = 255 + p.isDither = true + p.strokeWidth = 5f + p.style = Paint.Style.STROKE + p.blendMode = BlendMode.SRC + p.strokeMiter = 5f + p.strokeJoin = Paint.Join.ROUND + } + + private val fillColorStrokeProtection = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.isDither = true + p.strokeWidth = 5f + p.style = Paint.Style.STROKE + p.blendMode = BlendMode.CLEAR + p.strokeMiter = 5f + p.strokeJoin = Paint.Join.ROUND + } + + private val fillPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + p.alpha = 255 + p.isDither = true + p.strokeWidth = 0f + p.style = Paint.Style.FILL_AND_STROKE + } + + private val errorPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = context.resources.getColorStateList( + context.resources.getIdentifier( + "batterymeter_plus_color", "color", context.packageName + ), context.theme + ).defaultColor + p.alpha = 255 + p.isDither = true + p.strokeWidth = 0f + p.style = Paint.Style.FILL_AND_STROKE + p.blendMode = BlendMode.SRC + } + + private val chargingAlphaPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + } + + private val chargingPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + } + + private val customFillAlphaPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + } + + private val customFillPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + } + + private val powerSavePaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + } + + private val powerSaveFillPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + } + + private val scaledPerimeterPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + } + + // Only used if dualTone is set to true + private val dualToneBackgroundFill = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + p.alpha = 85 // ~0.3 alpha by default + p.isDither = true + p.strokeWidth = 0f + p.style = Paint.Style.FILL_AND_STROKE + } + + private val textPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.typeface = percentTypeface() + p.textAlign = Paint.Align.CENTER + } + + private fun percentTypeface(): Typeface { + val typefaceBuilder: Typeface.Builder? + return try { + typefaceBuilder = Typeface.Builder(modRes.assets, "Fonts/SanFranciscoText-Semibold.otf") + typefaceBuilder.build() ?: Typeface.create("sans-serif-condensed", Typeface.BOLD) + } catch (e: Exception) { + Typeface.DEFAULT + } + } + + init { + val density = context.resources.displayMetrics.density + intrinsicHeight = (HEIGHT * density).toInt() + intrinsicWidth = (WIDTH * density).toInt() + + val res = context.resources + val levels = res.obtainTypedArray( + res.getIdentifier( + "batterymeter_color_levels", "array", context.packageName + ) + ) + val colors = res.obtainTypedArray( + res.getIdentifier( + "batterymeter_color_values", "array", context.packageName + ) + ) + val n = levels.length() + colorLevels = IntArray(2 * n) + for (i in 0 until n) { + colorLevels[2 * i] = levels.getInt(i, 0) + if (colors.getType(i) == TypedValue.TYPE_ATTRIBUTE) { + colorLevels[2 * i + 1] = SettingsLibUtils.getColorAttrDefaultColor( + colors.getResourceId(i, 0), context + ) + } else { + colorLevels[2 * i + 1] = colors.getColor(i, 0) + } + } + levels.recycle() + colors.recycle() + + loadPaths() + } + + override fun draw(c: Canvas) { + c.saveLayer(null, null) + unifiedPath.reset() + levelPath.reset() + levelRect.set(fillRect) + val fillFraction = batteryLevel / 100f + val fillRight = + if (batteryLevel == 100) + fillRect.right + 1f + else + fillRect.right - (fillRect.width() * (1 - fillFraction)) + val fillBottom = fillRect.bottom + 1f + + levelRect.right = floor(fillRight.toDouble()).toFloat() + levelRect.bottom = floor(fillBottom.toDouble()).toFloat() + levelPath.addRect(levelRect, Path.Direction.CCW) + + // If drawing dual tone, the level is used only to clip the whole drawable path + if (!dualTone) { + unifiedPath.op(levelPath, Path.Op.UNION) + } + + fillPaint.color = levelColor + val black = Color.BLACK + val chargingParseColor = Color.parseColor("#ff3ab74e") + val powerSaveParseColor = Color.parseColor("#fffdd015") + chargingAlphaPaint.color = + if (customBlendColor && chargingColor != black) chargingColor else chargingParseColor + chargingPaint.color = + if (customBlendColor && chargingColor != black) chargingColor else chargingParseColor + powerSavePaint.color = + if (customBlendColor && powerSaveColor != black) powerSaveColor else powerSaveParseColor + powerSaveFillPaint.color = + if (customBlendColor && powerSaveFillColor != black) powerSaveFillColor else powerSaveParseColor + + customFillAlphaPaint.alpha = 85 + chargingAlphaPaint.alpha = 85 + powerSavePaint.alpha = 85 + + // The perimeter should never change + if (charging) { + c.drawPath(scaledPerimeter, chargingAlphaPaint) + } else if (powerSaveEnabled) { + c.drawPath(scaledPerimeter, powerSavePaint) + c.drawPath(scaledPlus, powerSaveFillPaint) + } else { + customFillAlphaPaint.color = customFillColor + customFillAlphaPaint.shader = + if (customFillColor != black && customFillGradColor != black) LinearGradient( + levelRect.right, 0f, 0f, levelRect.bottom, + customFillColor, customFillGradColor, + Shader.TileMode.CLAMP + ) else null + customFillAlphaPaint.alpha = 85 + c.drawPath( + scaledPerimeter, + if (customBlendColor && customFillColor != black) customFillAlphaPaint else scaledPerimeterPaint + ) + } + + // Deal with unifiedPath clipping before it draws + if (charging && !customChargingIcon) { + // Clip out the bolt shape + unifiedPath.op(scaledBolt, Path.Op.DIFFERENCE) + if (!invertFillIcon) { + c.drawPath(scaledBolt, fillPaint) + } + } + + if (dualTone) { + // Dual tone means we draw the shape again, clipped to the charge level + c.drawPath(unifiedPath, dualToneBackgroundFill) + c.save() + c.clipRect( + bounds.left.toFloat(), + 0f, + bounds.right + bounds.width() * fillFraction, + bounds.left.toFloat() + ) + c.drawPath(unifiedPath, fillPaint) + c.restore() + } else { + // Non dual-tone means we draw the perimeter (with the level fill), and potentially + // draw the fill again with a critical color + if (charging) { + fillPaint.color = fillColor + c.clipOutPath(scaledfillOutline) + c.drawPath(unifiedPath, chargingPaint) + fillPaint.color = levelColor + } else if (powerSaveEnabled) { + c.drawPath(scaledErrorPerimeter, errorPaint) + c.clipOutPath(scaledfillOutline) + c.drawPath(levelPath, powerSaveFillPaint) + } else { + fillPaint.color = fillColor + customFillPaint.color = customFillColor + customFillPaint.shader = + if (customFillColor != black && customFillGradColor != black) LinearGradient( + levelRect.right, 0f, 0f, levelRect.bottom, + customFillColor, customFillGradColor, + Shader.TileMode.CLAMP + ) else null + c.clipOutPath(scaledfillOutline) + c.drawPath( + unifiedPath, + if (customBlendColor && customFillColor != black) customFillPaint else fillPaint + ) + fillPaint.color = levelColor + } + + // Show colorError below this level + if (batteryLevel <= CRITICAL_LEVEL && !charging) { + c.save() + c.clipPath(scaledFill) + c.drawPath(levelPath, fillPaint) + c.restore() + } + } + + if (charging) { + c.clipOutPath(scaledBolt) + } else if (powerSaveEnabled) { + // If power save is enabled draw the perimeter path with colorError + c.drawPath(scaledErrorPerimeter, errorPaint) + } + c.restore() + + textPaint.textSize = bounds.width() * 0.26f + val textHeight = +textPaint.fontMetrics.ascent + val pctXcharging = if (customChargingIcon) 0.58f else 0.49f + val pctX100 = if (customChargingIcon) 0.58f else 0.46f + val pctX = (bounds.width() + textHeight) * + (if (!charging && !powerSaveEnabled) 0.58f /* discharging */ + else if (batteryLevel < 100) pctXcharging /* charging / powerSaveEnabled*/ + else pctX100) /* level == 100 */ /* charging / powerSaveEnabled*/ + val pctY = bounds.height() * 0.67f + + textPaint.color = fillColor + if (isRotation) { + c.rotate(180f, pctX, pctY * 0.74f) + } + c.save() + c.drawText(batteryLevel.toString(), pctX, pctY, textPaint) + c.restore() + } + + private fun batteryColorForLevel(level: Int): Int { + return when { + charging || powerSaveEnabled -> fillColor + else -> getColorForLevel(level) + } + } + + private fun getColorForLevel(level: Int): Int { + var thresh: Int + var color = 0 + var i = 0 + while (i < colorLevels.size) { + thresh = colorLevels[i] + color = colorLevels[i + 1] + if (level <= thresh) { + + // Respect tinting for "normal" level + return if (i == colorLevels.size - 2) { + fillColor + } else { + color + } + } + i += 2 + } + return color + } + + /** + * Alpha is unused internally, and should be defined in the colors passed to {@link setColors}. + * Further, setting an alpha for a dual tone battery meter doesn't make sense without bounds + * defining the minimum background fill alpha. This is because fill + background must be equal + * to the net alpha passed in here. + */ + override fun setAlpha(alpha: Int) { + } + + override fun setColorFilter(colorFilter: ColorFilter?) { + fillPaint.colorFilter = colorFilter + fillColorStrokePaint.colorFilter = colorFilter + dualToneBackgroundFill.colorFilter = colorFilter + } + + /** + * Deprecated, but required by Drawable + */ + @Deprecated( + "Deprecated in Java", + ReplaceWith("PixelFormat.OPAQUE", "android.graphics.PixelFormat"), + ) + override fun getOpacity(): Int { + return PixelFormat.OPAQUE + } + + override fun getIntrinsicHeight(): Int { + return intrinsicHeight + } + + override fun getIntrinsicWidth(): Int { + return intrinsicWidth + } + + /** + * Set the fill level + */ + @Suppress("PARAMETER_NAME_CHANGED_ON_OVERRIDE") + override fun setBatteryLevel(l: Int) { + // invertFillIcon = if (l >= 67) true else if (l <= 33) false else invertFillIcon + batteryLevel = l + levelColor = batteryColorForLevel(batteryLevel) + invalidateSelf() + } + + fun getBatteryLevel(): Int { + return batteryLevel + } + + override fun onBoundsChange(bounds: Rect) { + super.onBoundsChange(bounds) + updateSize() + } + + fun setPadding(left: Int, top: Int, right: Int, bottom: Int) { + padding.left = left + padding.top = top + padding.right = right + padding.bottom = bottom + + updateSize() + } + + override fun setColors(fgColor: Int, bgColor: Int, singleToneColor: Int) { + fillColor = if (dualTone) fgColor else singleToneColor + + fillPaint.color = fillColor + fillColorStrokePaint.color = fillColor + + scaledPerimeterPaint.color = fillColor + scaledPerimeterPaint.alpha = 85 + + backgroundColor = bgColor + dualToneBackgroundFill.color = bgColor + + // Also update the level color, since fillColor may have changed + levelColor = batteryColorForLevel(batteryLevel) + + invalidateSelf() + } + + private fun postInvalidate() { + unscheduleSelf(invalidateRunnable) + scheduleSelf(invalidateRunnable, 0) + } + + private fun updateSize() { + val b = bounds + if (b.isEmpty) { + scaleMatrix.setScale(1f, 1f) + } else { + scaleMatrix.setScale((b.right / WIDTH), (b.bottom / HEIGHT)) + } + + perimeterPath.transform(scaleMatrix, scaledPerimeter) + errorPerimeterPath.transform(scaleMatrix, scaledErrorPerimeter) + fillMask.transform(scaleMatrix, scaledFill) + scaledFill.computeBounds(fillRect, true) + fillOutlinePath.transform(scaleMatrix, scaledfillOutline) + boltPath.transform(scaleMatrix, scaledBolt) + plusPath.transform(scaleMatrix, scaledPlus) + + // It is expected that this view only ever scale by the same factor in each dimension, so + // just pick one to scale the strokeWidths + val scaledStrokeWidth = + (b.right / WIDTH * PROTECTION_STROKE_WIDTH).coerceAtLeast(PROTECTION_MIN_STROKE_WIDTH) + + fillColorStrokePaint.strokeWidth = scaledStrokeWidth + fillColorStrokeProtection.strokeWidth = scaledStrokeWidth + } + + @SuppressLint("RestrictedApi") + private fun loadPaths() { + val pathString = modRes.getString(R.string.config_landscapeBatteryPerimeterPathM) + perimeterPath.set(PathParser.createPathFromPathData(pathString)) + perimeterPath.computeBounds(RectF(), true) + + val errorPathString = modRes.getString(R.string.config_landscapeBatteryErrorPerimeterPathM) + errorPerimeterPath.set(PathParser.createPathFromPathData(errorPathString)) + errorPerimeterPath.computeBounds(RectF(), true) + + val fillMaskString = modRes.getString(R.string.config_landscapeBatteryFillMaskM) + fillMask.set(PathParser.createPathFromPathData(fillMaskString)) + // Set the fill rect so we can calculate the fill properly + fillMask.computeBounds(fillRect, true) + + val fillOutlinePathString = modRes.getString(R.string.config_landscapeBatteryFillOutlineM) + fillOutlinePath.set(PathParser.createPathFromPathData(fillOutlinePathString)) + fillOutlinePath.computeBounds(RectF(), true) + + val boltPathString = modRes.getString(R.string.config_landscapeBatteryBoltPathM) + boltPath.set(PathParser.createPathFromPathData(boltPathString)) + + val plusPathString = modRes.getString(R.string.config_landscapeBatteryPowersavePathM) + plusPath.set(PathParser.createPathFromPathData(plusPathString)) + + dualTone = false + } + + companion object { + private const val TAG = "LandscapeBatteryM" + private const val WIDTH = 24f + private const val HEIGHT = 12f + private const val CRITICAL_LEVEL = 15 + + // On a 24x12 grid, how wide to make the fill protection stroke. + // Scales when our size changes + private const val PROTECTION_STROKE_WIDTH = 2f + + // Arbitrarily chosen for visibility at small sizes + private const val PROTECTION_MIN_STROKE_WIDTH = 5f + } +} diff --git a/app/src/main/java/com/drdisagree/iconify/xposed/mods/batterystyles/LandscapeBatteryN.kt b/app/src/main/java/com/drdisagree/iconify/xposed/mods/batterystyles/LandscapeBatteryN.kt new file mode 100644 index 000000000..ae778e6a2 --- /dev/null +++ b/app/src/main/java/com/drdisagree/iconify/xposed/mods/batterystyles/LandscapeBatteryN.kt @@ -0,0 +1,711 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.drdisagree.iconify.xposed.mods.batterystyles + +import android.annotation.SuppressLint +import android.content.Context +import android.graphics.BlendMode +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.ColorFilter +import android.graphics.LinearGradient +import android.graphics.Matrix +import android.graphics.Paint +import android.graphics.Path +import android.graphics.PixelFormat +import android.graphics.Rect +import android.graphics.RectF +import android.graphics.Shader +import android.graphics.Typeface +import android.util.TypedValue +import androidx.core.graphics.PathParser +import com.drdisagree.iconify.R +import com.drdisagree.iconify.xposed.HookRes.modRes +import com.drdisagree.iconify.xposed.utils.SettingsLibUtils +import kotlin.math.floor + +/** + * A battery meter drawable that respects paths configured in + * frameworks/base/core/res/res/values/config.xml to allow for an easily overrideable battery icon + */ +@SuppressLint("DiscouragedApi") +open class LandscapeBatteryN(private val context: Context, frameColor: Int) : BatteryDrawable() { + + // Need to load: + // 1. perimeter shape + // 2. fill mask (if smaller than perimeter, this would create a fill that + // doesn't touch the walls + private val perimeterPath = Path() + private val scaledPerimeter = Path() + private val perimeterStripPath = Path() + private val scaledPerimeterStrip = Path() + private val errorPerimeterPath = Path() + private val scaledErrorPerimeter = Path() + + // Fill will cover the whole bounding rect of the fillMask, and be masked by the path + private val fillMask = Path() + private val scaledFill = Path() + + // Based off of the mask, the fill will interpolate across this space + private val fillRect = RectF() + + // Top of this rect changes based on level, 100% == fillRect + private val levelRect = RectF() + private val levelPath = Path() + + // Updates the transform of the paths when our bounds change + private val scaleMatrix = Matrix() + private val padding = Rect() + + // The net result of fill + perimeter paths + private val unifiedPath = Path() + + // Bolt path (used while charging) + private val boltPath = Path() + private val scaledBolt = Path() + + // Plus sign (used for power save mode) + private val plusPath = Path() + private val scaledPlus = Path() + + private var intrinsicHeight: Int + private var intrinsicWidth: Int + + // To implement hysteresis, keep track of the need to invert the interior icon of the battery + private var invertFillIcon = false + + // Colors can be configured based on battery level (see res/values/arrays.xml) + private var colorLevels: IntArray + + private var fillColor: Int = Color.MAGENTA + private var backgroundColor: Int = Color.MAGENTA + + // updated whenever level changes + private var levelColor: Int = Color.MAGENTA + + // Dual tone implies that battery level is a clipped overlay over top of the whole shape + private var dualTone = false + + private var batteryLevel = 0 + + private var isRotation = false + private var scaledFillAlpha = false + private var scaledPerimeterAlpha = false + private var customBlendColor = false + + private var chargingColor: Int = Color.TRANSPARENT + private var customFillColor: Int = Color.BLACK + private var customFillGradColor: Int = Color.BLACK + private var powerSaveColor: Int = Color.TRANSPARENT + private var powerSaveFillColor: Int = Color.TRANSPARENT + + private val invalidateRunnable: () -> Unit = { + invalidateSelf() + } + + open var criticalLevel: Int = 5 + + var charging = false + set(value) { + field = value + postInvalidate() + } + + override fun setChargingEnabled(charging: Boolean) { + this.charging = charging + postInvalidate() + } + + var powerSaveEnabled = false + set(value) { + field = value + postInvalidate() + } + + override fun setPowerSavingEnabled(powerSaveEnabled: Boolean) { + this.powerSaveEnabled = powerSaveEnabled + postInvalidate() + } + + var showPercent = false + set(value) { + field = value + postInvalidate() + } + + override fun setShowPercentEnabled(showPercent: Boolean) { + this.showPercent = showPercent + postInvalidate() + } + + var customChargingIcon = false + set(value) { + field = value + postInvalidate() + } + + open fun customizeBatteryDrawable( + isRotation: Boolean, + scaledPerimeterAlpha: Boolean, + scaledFillAlpha: Boolean, + customBlendColor: Boolean, + customFillColor: Int, + customFillGradColor: Int, + chargingColor: Int, + powerSaveColor: Int, + powerSaveFillColor: Int + ) { + this.isRotation = isRotation + this.scaledPerimeterAlpha = scaledPerimeterAlpha + this.scaledFillAlpha = scaledFillAlpha + this.customBlendColor = customBlendColor + this.customFillColor = customFillColor + this.customFillGradColor = customFillGradColor + this.chargingColor = chargingColor + this.powerSaveColor = powerSaveColor + this.powerSaveFillColor = powerSaveFillColor + } + + private val fillColorStrokePaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + p.alpha = 255 + p.isDither = true + p.strokeWidth = 5f + p.style = Paint.Style.STROKE + p.blendMode = BlendMode.SRC + p.strokeMiter = 5f + p.strokeJoin = Paint.Join.ROUND + } + + private val fillColorStrokeProtection = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.isDither = true + p.strokeWidth = 5f + p.style = Paint.Style.STROKE + p.blendMode = BlendMode.CLEAR + p.strokeMiter = 5f + p.strokeJoin = Paint.Join.ROUND + } + + private val fillPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + p.alpha = 255 + p.isDither = true + p.strokeWidth = 0f + p.style = Paint.Style.FILL_AND_STROKE + } + + private val errorPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = context.resources.getColorStateList( + context.resources.getIdentifier( + "batterymeter_plus_color", "color", context.packageName + ), context.theme + ).defaultColor + p.alpha = 255 + p.isDither = true + p.strokeWidth = 0f + p.style = Paint.Style.FILL_AND_STROKE + p.blendMode = BlendMode.SRC + } + + private val chargingPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + } + + private val customFillPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + } + + private val powerSavePaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + } + + private val powerSaveFillPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + } + + private val scaledFillPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + } + + private val scaledPerimeterPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + } + + private val scaledPerimeterPaintDef = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + } + + private val scaledPerimeterStripPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + } + + // Only used if dualTone is set to true + private val dualToneBackgroundFill = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + p.alpha = 85 // ~0.3 alpha by default + p.isDither = true + p.strokeWidth = 0f + p.style = Paint.Style.FILL_AND_STROKE + } + + private val textPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.typeface = Typeface.create("sans-serif-condensed", Typeface.BOLD) + p.textAlign = Paint.Align.CENTER + } + + init { + val density = context.resources.displayMetrics.density + intrinsicHeight = (HEIGHT * density).toInt() + intrinsicWidth = (WIDTH * density).toInt() + + val res = context.resources + val levels = res.obtainTypedArray( + res.getIdentifier( + "batterymeter_color_levels", "array", context.packageName + ) + ) + val colors = res.obtainTypedArray( + res.getIdentifier( + "batterymeter_color_values", "array", context.packageName + ) + ) + val n = levels.length() + colorLevels = IntArray(2 * n) + for (i in 0 until n) { + colorLevels[2 * i] = levels.getInt(i, 0) + if (colors.getType(i) == TypedValue.TYPE_ATTRIBUTE) { + colorLevels[2 * i + 1] = SettingsLibUtils.getColorAttrDefaultColor( + colors.getResourceId(i, 0), context + ) + } else { + colorLevels[2 * i + 1] = colors.getColor(i, 0) + } + } + levels.recycle() + colors.recycle() + + loadPaths() + } + + override fun draw(c: Canvas) { + c.saveLayer(null, null) + unifiedPath.reset() + levelPath.reset() + levelRect.set(fillRect) + val fillFraction = batteryLevel / 100f + val fillRight = + if (batteryLevel == 100) + fillRect.right + 0.6f + else + fillRect.right - (fillRect.width() * (1 - fillFraction)) + + levelRect.right = floor(fillRight.toDouble()).toFloat() + levelPath.addRoundRect( + levelRect, + floatArrayOf( + 5.0f, + 5.0f, + 5.0f, + 5.0f, + 5.0f, + 5.0f, + 5.0f, + 5.0f + ), Path.Direction.CCW + ) + + scaledFillPaint.alpha = if (scaledFillAlpha) 100 else 0 + scaledPerimeterPaint.alpha = + if (scaledPerimeterAlpha) 100 else scaledPerimeterPaintDef.alpha + + // The perimeter should never change + c.drawPath(scaledFill, scaledFillPaint) + c.drawPath(scaledPerimeter, scaledPerimeterPaint) + // If drawing dual tone, the level is used only to clip the whole drawable path + if (!dualTone) { + unifiedPath.op(levelPath, Path.Op.UNION) + } + + fillPaint.color = levelColor + val black = Color.BLACK + + // Deal with unifiedPath clipping before it draws + if (charging && !customChargingIcon) { + // Clip out the bolt shape + unifiedPath.op(scaledBolt, Path.Op.DIFFERENCE) + if (!invertFillIcon) { + c.drawPath(scaledBolt, fillPaint) + } + } + + if (dualTone) { + // Dual tone means we draw the shape again, clipped to the charge level + c.drawPath(unifiedPath, dualToneBackgroundFill) + c.save() + c.clipRect( + bounds.left.toFloat(), + 0f, + bounds.right + bounds.width() * fillFraction, + bounds.left.toFloat() + ) + c.drawPath(unifiedPath, fillPaint) + c.restore() + } else { + // Non dual-tone means we draw the perimeter (with the level fill), and potentially + // draw the fill again with a critical color + if (customBlendColor) { + if (charging) { + fillPaint.color = fillColor + c.drawPath(unifiedPath, fillPaint) + fillPaint.color = levelColor + } else { + // Show colorError below this level + if (batteryLevel <= CRITICAL_LEVEL) { + c.save() + c.clipPath(scaledFill) + c.drawPath(levelPath, fillPaint) + c.restore() + } else { + customFillPaint.color = customFillColor + customFillPaint.shader = + if (customFillColor != black && customFillGradColor != black) LinearGradient( + levelRect.right, 0f, 0f, levelRect.bottom, + customFillColor, customFillGradColor, + Shader.TileMode.CLAMP + ) else null + c.drawPath( + levelPath, + if (customFillColor == black) fillPaint else customFillPaint + ) + } + } + } else { + // Show colorError below this level + if (batteryLevel <= CRITICAL_LEVEL && !charging) { + c.save() + c.clipPath(scaledFill) + c.drawPath(levelPath, fillPaint) + c.restore() + } else { + fillPaint.color = fillColor + c.drawPath(unifiedPath, fillPaint) + fillPaint.color = levelColor + } + } + } + + if (customBlendColor) { + chargingPaint.color = + if (chargingColor == black) Color.TRANSPARENT else chargingColor + + powerSavePaint.color = + if (powerSaveColor == black) context.resources.getColorStateList( + context.resources.getIdentifier( + "batterymeter_plus_color", "color", context.packageName + ), context.theme + ).defaultColor else powerSaveColor + + powerSaveFillPaint.color = + if (powerSaveFillColor == black) Color.TRANSPARENT else powerSaveFillColor + } else { + chargingPaint.color = Color.TRANSPARENT + powerSavePaint.color = context.resources.getColorStateList( + context.resources.getIdentifier( + "batterymeter_plus_color", "color", context.packageName + ), context.theme + ).defaultColor + powerSaveFillPaint.color = Color.TRANSPARENT + } + + if (charging) { + c.clipOutPath(scaledBolt) + c.drawPath(levelPath, chargingPaint) + } else if (powerSaveEnabled) { + // If power save is enabled draw the perimeter path with colorError + c.drawPath(scaledErrorPerimeter, powerSavePaint) + c.drawPath(levelPath, powerSaveFillPaint) + // And draw the plus sign on top of the fill + if (!showPercent) { + c.drawPath(scaledPlus, powerSavePaint) + } + } + c.restore() + + if (charging && !customChargingIcon) { + scaledPerimeterStripPaint.color = fillColor + c.drawPath(scaledBolt, scaledPerimeterStripPaint) + scaledPerimeterStripPaint.color = fillColor.toInt().inv() or 0xFF000000.toInt() + c.save() + c.clipRect( + fillRect.left, + fillRect.top, + fillRect.right - (fillRect.width() * (1 - fillFraction)), + fillRect.bottom + ) + c.drawPath(scaledBolt, scaledPerimeterStripPaint) + c.restore() + } + scaledPerimeterStripPaint.color = fillColor + c.drawPath(scaledPerimeterStrip, scaledPerimeterStripPaint) + scaledPerimeterStripPaint.color = fillColor.toInt().inv() or 0xFF000000.toInt() + c.save() + c.clipRect( + fillRect.left, + fillRect.top, + fillRect.right - (fillRect.width() * (1 - fillFraction)), + fillRect.bottom + ) + c.drawPath(scaledPerimeterStrip, scaledPerimeterStripPaint) + c.restore() + if (showPercent && !charging) { + textPaint.textSize = bounds.width() * 0.19f + val textHeight = +textPaint.fontMetrics.ascent + val pctX = (bounds.width() + textHeight) * 0.71f + val pctY = bounds.height() * 0.76f + + if (isRotation) { + c.rotate(180f, pctX + 0.5f, pctY * 0.83f) + } + if (customBlendColor) { + if (powerSaveEnabled && powerSaveFillColor == black) { + textPaint.color = fillColor + c.drawText(batteryLevel.toString(), pctX, pctY, textPaint) + + textPaint.color = fillColor.toInt().inv() or 0xFF000000.toInt() + c.save() + c.clipRect( + if (isRotation) fillRect.left + (fillRect.width() * (1 - fillFraction)) else fillRect.left, + fillRect.top, + if (isRotation) fillRect.right else fillRect.right - (fillRect.width() * (1 - fillFraction)), + fillRect.bottom + ) + c.drawText(batteryLevel.toString(), pctX, pctY, textPaint) + } else if (!powerSaveEnabled && customFillColor == black) { + textPaint.color = fillColor + c.drawText(batteryLevel.toString(), pctX, pctY, textPaint) + + textPaint.color = fillColor.toInt().inv() or 0xFF000000.toInt() + c.save() + c.clipRect( + if (isRotation) fillRect.left + (fillRect.width() * (1 - fillFraction)) else fillRect.left, + fillRect.top, + if (isRotation) fillRect.right else fillRect.right - (fillRect.width() * (1 - fillFraction)), + fillRect.bottom + ) + c.drawText(batteryLevel.toString(), pctX, pctY, textPaint) + } else { + textPaint.color = fillColor + c.drawText(batteryLevel.toString(), pctX, pctY, textPaint) + } + } else { + textPaint.color = fillColor + c.drawText(batteryLevel.toString(), pctX, pctY, textPaint) + + textPaint.color = fillColor.toInt().inv() or 0xFF000000.toInt() + c.save() + c.clipRect( + if (isRotation) fillRect.left + (fillRect.width() * (1 - fillFraction)) else fillRect.left, + fillRect.top, + if (isRotation) fillRect.right else fillRect.right - (fillRect.width() * (1 - fillFraction)), + fillRect.bottom + ) + c.drawText(batteryLevel.toString(), pctX, pctY, textPaint) + } + c.restore() + } + } + + private fun batteryColorForLevel(level: Int): Int { + return when { + charging || powerSaveEnabled -> fillColor + else -> getColorForLevel(level) + } + } + + private fun getColorForLevel(level: Int): Int { + var thresh: Int + var color = 0 + var i = 0 + while (i < colorLevels.size) { + thresh = colorLevels[i] + color = colorLevels[i + 1] + if (level <= thresh) { + + // Respect tinting for "normal" level + return if (i == colorLevels.size - 2) { + fillColor + } else { + color + } + } + i += 2 + } + return color + } + + /** + * Alpha is unused internally, and should be defined in the colors passed to {@link setColors}. + * Further, setting an alpha for a dual tone battery meter doesn't make sense without bounds + * defining the minimum background fill alpha. This is because fill + background must be equal + * to the net alpha passed in here. + */ + override fun setAlpha(alpha: Int) { + } + + override fun setColorFilter(colorFilter: ColorFilter?) { + fillPaint.colorFilter = colorFilter + fillColorStrokePaint.colorFilter = colorFilter + dualToneBackgroundFill.colorFilter = colorFilter + } + + /** + * Deprecated, but required by Drawable + */ + @Deprecated( + "Deprecated in Java", + ReplaceWith("PixelFormat.OPAQUE", "android.graphics.PixelFormat"), + ) + override fun getOpacity(): Int { + return PixelFormat.OPAQUE + } + + override fun getIntrinsicHeight(): Int { + return intrinsicHeight + } + + override fun getIntrinsicWidth(): Int { + return intrinsicWidth + } + + /** + * Set the fill level + */ + @Suppress("PARAMETER_NAME_CHANGED_ON_OVERRIDE") + override fun setBatteryLevel(l: Int) { + // invertFillIcon = if (l >= 67) true else if (l <= 33) false else invertFillIcon + batteryLevel = l + levelColor = batteryColorForLevel(batteryLevel) + invalidateSelf() + } + + fun getBatteryLevel(): Int { + return batteryLevel + } + + override fun onBoundsChange(bounds: Rect) { + super.onBoundsChange(bounds) + updateSize() + } + + fun setPadding(left: Int, top: Int, right: Int, bottom: Int) { + padding.left = left + padding.top = top + padding.right = right + padding.bottom = bottom + + updateSize() + } + + override fun setColors(fgColor: Int, bgColor: Int, singleToneColor: Int) { + fillColor = if (dualTone) fgColor else singleToneColor + + fillPaint.color = fillColor + fillColorStrokePaint.color = fillColor + + scaledFillPaint.color = fillColor + scaledPerimeterPaint.color = fillColor + scaledPerimeterPaintDef.color = fillColor + + backgroundColor = bgColor + dualToneBackgroundFill.color = bgColor + + // Also update the level color, since fillColor may have changed + levelColor = batteryColorForLevel(batteryLevel) + + invalidateSelf() + } + + private fun postInvalidate() { + unscheduleSelf(invalidateRunnable) + scheduleSelf(invalidateRunnable, 0) + } + + private fun updateSize() { + val b = bounds + if (b.isEmpty) { + scaleMatrix.setScale(1f, 1f) + } else { + scaleMatrix.setScale((b.right / WIDTH), (b.bottom / HEIGHT)) + } + + perimeterPath.transform(scaleMatrix, scaledPerimeter) + perimeterStripPath.transform(scaleMatrix, scaledPerimeterStrip) + errorPerimeterPath.transform(scaleMatrix, scaledErrorPerimeter) + fillMask.transform(scaleMatrix, scaledFill) + scaledFill.computeBounds(fillRect, true) + boltPath.transform(scaleMatrix, scaledBolt) + plusPath.transform(scaleMatrix, scaledPlus) + + // It is expected that this view only ever scale by the same factor in each dimension, so + // just pick one to scale the strokeWidths + val scaledStrokeWidth = + (b.right / WIDTH * PROTECTION_STROKE_WIDTH).coerceAtLeast(PROTECTION_MIN_STROKE_WIDTH) + + fillColorStrokePaint.strokeWidth = scaledStrokeWidth + fillColorStrokeProtection.strokeWidth = scaledStrokeWidth + } + + @SuppressLint("RestrictedApi") + private fun loadPaths() { + val pathString = modRes.getString(R.string.config_landscapeBatteryPerimeterPathN) + perimeterPath.set(PathParser.createPathFromPathData(pathString)) + perimeterPath.computeBounds(RectF(), true) + + val pathStripString = modRes.getString(R.string.config_landscapeBatteryPerimeterStripPathN) + perimeterStripPath.set(PathParser.createPathFromPathData(pathStripString)) + perimeterStripPath.computeBounds(RectF(), true) + + val errorPathString = modRes.getString(R.string.config_landscapeBatteryErrorPerimeterPathN) + errorPerimeterPath.set(PathParser.createPathFromPathData(errorPathString)) + errorPerimeterPath.computeBounds(RectF(), true) + + val fillMaskString = modRes.getString(R.string.config_landscapeBatteryFillMaskN) + fillMask.set(PathParser.createPathFromPathData(fillMaskString)) + // Set the fill rect so we can calculate the fill properly + fillMask.computeBounds(fillRect, true) + + val boltPathString = modRes.getString(R.string.config_landscapeBatteryBoltPathN) + boltPath.set(PathParser.createPathFromPathData(boltPathString)) + + val plusPathString = modRes.getString(R.string.config_landscapeBatteryPowersavePathN) + plusPath.set(PathParser.createPathFromPathData(plusPathString)) + + dualTone = false + } + + companion object { + private const val TAG = "LandscapeBatteryN" + private const val WIDTH = 24f + private const val HEIGHT = 12f + private const val CRITICAL_LEVEL = 15 + + // On a 24x12 grid, how wide to make the fill protection stroke. + // Scales when our size changes + private const val PROTECTION_STROKE_WIDTH = 1f + + // Arbitrarily chosen for visibility at small sizes + private const val PROTECTION_MIN_STROKE_WIDTH = 3f + } +} diff --git a/app/src/main/java/com/drdisagree/iconify/xposed/mods/batterystyles/LandscapeBatteryO.kt b/app/src/main/java/com/drdisagree/iconify/xposed/mods/batterystyles/LandscapeBatteryO.kt new file mode 100644 index 000000000..ee1ea38ca --- /dev/null +++ b/app/src/main/java/com/drdisagree/iconify/xposed/mods/batterystyles/LandscapeBatteryO.kt @@ -0,0 +1,683 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.drdisagree.iconify.xposed.mods.batterystyles + +import android.annotation.SuppressLint +import android.content.Context +import android.graphics.BlendMode +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.ColorFilter +import android.graphics.LinearGradient +import android.graphics.Matrix +import android.graphics.Paint +import android.graphics.Path +import android.graphics.PixelFormat +import android.graphics.Rect +import android.graphics.RectF +import android.graphics.Shader +import android.graphics.Typeface +import android.util.TypedValue +import androidx.core.graphics.PathParser +import com.drdisagree.iconify.R +import com.drdisagree.iconify.xposed.HookRes.modRes +import com.drdisagree.iconify.xposed.utils.SettingsLibUtils +import kotlin.math.floor + +/** + * A battery meter drawable that respects paths configured in + * frameworks/base/core/res/res/values/config.xml to allow for an easily overrideable battery icon + */ +@SuppressLint("DiscouragedApi") +open class LandscapeBatteryO(private val context: Context, frameColor: Int) : BatteryDrawable() { + + // Need to load: + // 1. perimeter shape + // 2. fill mask (if smaller than perimeter, this would create a fill that + // doesn't touch the walls + private val perimeterPath = Path() + private val scaledPerimeter = Path() + private val errorPerimeterPath = Path() + private val scaledErrorPerimeter = Path() + + // Fill will cover the whole bounding rect of the fillMask, and be masked by the path + private val fillMask = Path() + private val scaledFill = Path() + + // Based off of the mask, the fill will interpolate across this space + private val fillRect = RectF() + + // Top of this rect changes based on level, 100% == fillRect + private val levelRect = RectF() + private val levelPath = Path() + + // Updates the transform of the paths when our bounds change + private val scaleMatrix = Matrix() + private val padding = Rect() + + // The net result of fill + perimeter paths + private val unifiedPath = Path() + + // Bolt path (used while charging) + private val boltPath = Path() + private val scaledBolt = Path() + + // Plus sign (used for power save mode) + private val plusPath = Path() + private val scaledPlus = Path() + + private var intrinsicHeight: Int + private var intrinsicWidth: Int + + // To implement hysteresis, keep track of the need to invert the interior icon of the battery + private var invertFillIcon = false + + // Colors can be configured based on battery level (see res/values/arrays.xml) + private var colorLevels: IntArray + + private var fillColor: Int = Color.MAGENTA + private var backgroundColor: Int = Color.MAGENTA + + // updated whenever level changes + private var levelColor: Int = Color.MAGENTA + + // Dual tone implies that battery level is a clipped overlay over top of the whole shape + private var dualTone = false + + private var batteryLevel = 0 + + private var isRotation = false + private var scaledFillAlpha = false + private var scaledPerimeterAlpha = false + private var customBlendColor = false + + private var chargingColor: Int = Color.TRANSPARENT + private var customFillColor: Int = Color.BLACK + private var customFillGradColor: Int = Color.BLACK + private var powerSaveColor: Int = Color.TRANSPARENT + private var powerSaveFillColor: Int = Color.TRANSPARENT + + private val invalidateRunnable: () -> Unit = { + invalidateSelf() + } + + open var criticalLevel: Int = 5 + + var charging = false + set(value) { + field = value + postInvalidate() + } + + override fun setChargingEnabled(charging: Boolean) { + this.charging = charging + postInvalidate() + } + + var powerSaveEnabled = false + set(value) { + field = value + postInvalidate() + } + + override fun setPowerSavingEnabled(powerSaveEnabled: Boolean) { + this.powerSaveEnabled = powerSaveEnabled + postInvalidate() + } + + var showPercent = false + set(value) { + field = value + postInvalidate() + } + + override fun setShowPercentEnabled(showPercent: Boolean) { + this.showPercent = showPercent + postInvalidate() + } + + var customChargingIcon = false + set(value) { + field = value + postInvalidate() + } + + open fun customizeBatteryDrawable( + isRotation: Boolean, + scaledPerimeterAlpha: Boolean, + scaledFillAlpha: Boolean, + customBlendColor: Boolean, + customFillColor: Int, + customFillGradColor: Int, + chargingColor: Int, + powerSaveColor: Int, + powerSaveFillColor: Int + ) { + this.isRotation = isRotation + this.scaledPerimeterAlpha = scaledPerimeterAlpha + this.scaledFillAlpha = scaledFillAlpha + this.customBlendColor = customBlendColor + this.customFillColor = customFillColor + this.customFillGradColor = customFillGradColor + this.chargingColor = chargingColor + this.powerSaveColor = powerSaveColor + this.powerSaveFillColor = powerSaveFillColor + } + + private val fillColorStrokePaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + p.alpha = 255 + p.isDither = true + p.strokeWidth = 5f + p.style = Paint.Style.STROKE + p.blendMode = BlendMode.SRC + p.strokeMiter = 5f + p.strokeJoin = Paint.Join.ROUND + } + + private val fillColorStrokeProtection = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.isDither = true + p.strokeWidth = 5f + p.style = Paint.Style.STROKE + p.blendMode = BlendMode.CLEAR + p.strokeMiter = 5f + p.strokeJoin = Paint.Join.ROUND + } + + private val fillPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + p.alpha = 255 + p.isDither = true + p.strokeWidth = 0f + p.style = Paint.Style.FILL_AND_STROKE + } + + private val errorPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = context.resources.getColorStateList( + context.resources.getIdentifier( + "batterymeter_plus_color", "color", context.packageName + ), context.theme + ).defaultColor + p.alpha = 255 + p.isDither = true + p.strokeWidth = 0f + p.style = Paint.Style.FILL_AND_STROKE + p.blendMode = BlendMode.SRC + } + + private val chargingPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + } + + private val customFillPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + } + + private val powerSavePaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + } + + private val powerSaveFillPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + } + + private val scaledFillPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + } + + private val scaledPerimeterPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + } + + private val scaledPerimeterPaintDef = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + } + + // Only used if dualTone is set to true + private val dualToneBackgroundFill = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = frameColor + p.alpha = 85 // ~0.3 alpha by default + p.isDither = true + p.strokeWidth = 0f + p.style = Paint.Style.FILL_AND_STROKE + } + + private val textPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.typeface = Typeface.create("sans-serif-condensed", Typeface.BOLD) + p.textAlign = Paint.Align.CENTER + } + + init { + val density = context.resources.displayMetrics.density + intrinsicHeight = (HEIGHT * density).toInt() + intrinsicWidth = (WIDTH * density).toInt() + + val res = context.resources + val levels = res.obtainTypedArray( + res.getIdentifier( + "batterymeter_color_levels", "array", context.packageName + ) + ) + val colors = res.obtainTypedArray( + res.getIdentifier( + "batterymeter_color_values", "array", context.packageName + ) + ) + val n = levels.length() + colorLevels = IntArray(2 * n) + for (i in 0 until n) { + colorLevels[2 * i] = levels.getInt(i, 0) + if (colors.getType(i) == TypedValue.TYPE_ATTRIBUTE) { + colorLevels[2 * i + 1] = SettingsLibUtils.getColorAttrDefaultColor( + colors.getResourceId(i, 0), context + ) + } else { + colorLevels[2 * i + 1] = colors.getColor(i, 0) + } + } + levels.recycle() + colors.recycle() + + loadPaths() + } + + override fun draw(c: Canvas) { + c.saveLayer(null, null) + unifiedPath.reset() + levelPath.reset() + levelRect.set(fillRect) + val fillFraction = batteryLevel / 100f + val fillRight = + if (batteryLevel == 100) + fillRect.right + 1 + else + fillRect.right - (fillRect.width() * (1 - fillFraction)) + + levelRect.right = floor(fillRight.toDouble()).toFloat() + levelPath.addRoundRect( + levelRect, + floatArrayOf( + 2.0f, + 2.0f, + 2.0f, + 2.0f, + 2.0f, + 2.0f, + 2.0f, + 2.0f + ), Path.Direction.CCW + ) + + scaledFillPaint.alpha = if (scaledFillAlpha) 100 else 0 + scaledPerimeterPaint.alpha = + if (scaledPerimeterAlpha) 100 else scaledPerimeterPaintDef.alpha + + // The perimeter should never change + c.drawPath(scaledFill, scaledFillPaint) + c.drawPath(scaledPerimeter, scaledPerimeterPaint) + // If drawing dual tone, the level is used only to clip the whole drawable path + if (!dualTone) { + unifiedPath.op(levelPath, Path.Op.UNION) + } + + fillPaint.color = levelColor + val black = Color.BLACK + + // Deal with unifiedPath clipping before it draws + if (charging && !customChargingIcon) { + // Clip out the bolt shape + unifiedPath.op(scaledBolt, Path.Op.DIFFERENCE) + if (!invertFillIcon) { + c.drawPath(scaledBolt, fillPaint) + } + } + + if (dualTone) { + // Dual tone means we draw the shape again, clipped to the charge level + c.drawPath(unifiedPath, dualToneBackgroundFill) + c.save() + c.clipRect( + bounds.left.toFloat(), + 0f, + bounds.right + bounds.width() * fillFraction, + bounds.left.toFloat() + ) + c.drawPath(unifiedPath, fillPaint) + c.restore() + } else { + // Non dual-tone means we draw the perimeter (with the level fill), and potentially + // draw the fill again with a critical color + if (customBlendColor) { + if (charging) { + fillPaint.color = fillColor + c.drawPath(unifiedPath, fillPaint) + fillPaint.color = levelColor + } else { + // Show colorError below this level + if (batteryLevel <= CRITICAL_LEVEL) { + c.save() + c.clipPath(scaledFill) + c.drawPath(levelPath, fillPaint) + c.restore() + } else { + customFillPaint.color = customFillColor + customFillPaint.shader = + if (customFillColor != black && customFillGradColor != black) LinearGradient( + levelRect.right, 0f, 0f, levelRect.bottom, + customFillColor, customFillGradColor, + Shader.TileMode.CLAMP + ) else null + c.drawPath( + levelPath, + if (customFillColor == black) fillPaint else customFillPaint + ) + } + } + } else { + // Show colorError below this level + if (batteryLevel <= CRITICAL_LEVEL && !charging) { + c.save() + c.clipPath(scaledFill) + c.drawPath(levelPath, fillPaint) + c.restore() + } else { + fillPaint.color = fillColor + c.drawPath(unifiedPath, fillPaint) + fillPaint.color = levelColor + } + } + } + + if (customBlendColor) { + chargingPaint.color = + if (chargingColor == black) Color.TRANSPARENT else chargingColor + + powerSavePaint.color = + if (powerSaveColor == black) context.resources.getColorStateList( + context.resources.getIdentifier( + "batterymeter_plus_color", "color", context.packageName + ), context.theme + ).defaultColor else powerSaveColor + + powerSaveFillPaint.color = + if (powerSaveFillColor == black) Color.TRANSPARENT else powerSaveFillColor + } else { + chargingPaint.color = Color.TRANSPARENT + powerSavePaint.color = context.resources.getColorStateList( + context.resources.getIdentifier( + "batterymeter_plus_color", "color", context.packageName + ), context.theme + ).defaultColor + powerSaveFillPaint.color = Color.TRANSPARENT + } + + if (charging) { + if (!customChargingIcon) { + c.clipOutPath(scaledBolt) + c.drawPath(levelPath, chargingPaint) + if (invertFillIcon) { + c.drawPath(scaledBolt, fillColorStrokePaint) + } else { + c.drawPath(scaledBolt, fillColorStrokeProtection) + } + } else { + c.drawPath(levelPath, chargingPaint) + } + } else if (powerSaveEnabled) { + // If power save is enabled draw the perimeter path with colorError + c.drawPath(scaledErrorPerimeter, powerSavePaint) + c.drawPath(levelPath, powerSaveFillPaint) + // And draw the plus sign on top of the fill + if (!showPercent) { + c.drawPath(scaledPlus, powerSavePaint) + } + } + c.restore() + + if (showPercent && !charging) { + textPaint.textSize = bounds.width() * 0.25f + val textHeight = +textPaint.fontMetrics.ascent + val pctX = (bounds.width() + textHeight) * 0.59f + val pctY = bounds.height() * 0.65f + + if (isRotation) { + c.rotate(180f, pctX, pctY * 0.77f) + } + if (customBlendColor) { + if (powerSaveEnabled && powerSaveFillColor == black) { + textPaint.color = fillColor + c.drawText(batteryLevel.toString(), pctX, pctY, textPaint) + + textPaint.color = fillColor.toInt().inv() or 0xFF000000.toInt() + c.save() + c.clipRect( + if (isRotation) fillRect.left + (fillRect.width() * (1 - fillFraction)) else fillRect.left, + fillRect.top, + if (isRotation) fillRect.right else fillRect.right - (fillRect.width() * (1 - fillFraction)), + fillRect.bottom + ) + c.drawText(batteryLevel.toString(), pctX, pctY, textPaint) + } else if (!powerSaveEnabled && customFillColor == black) { + textPaint.color = fillColor + c.drawText(batteryLevel.toString(), pctX, pctY, textPaint) + + textPaint.color = fillColor.toInt().inv() or 0xFF000000.toInt() + c.save() + c.clipRect( + if (isRotation) fillRect.left + (fillRect.width() * (1 - fillFraction)) else fillRect.left, + fillRect.top, + if (isRotation) fillRect.right else fillRect.right - (fillRect.width() * (1 - fillFraction)), + fillRect.bottom + ) + c.drawText(batteryLevel.toString(), pctX, pctY, textPaint) + } else { + textPaint.color = fillColor + c.drawText(batteryLevel.toString(), pctX, pctY, textPaint) + } + } else { + textPaint.color = fillColor + c.drawText(batteryLevel.toString(), pctX, pctY, textPaint) + + textPaint.color = fillColor.toInt().inv() or 0xFF000000.toInt() + c.save() + c.clipRect( + if (isRotation) fillRect.left + (fillRect.width() * (1 - fillFraction)) else fillRect.left, + fillRect.top, + if (isRotation) fillRect.right else fillRect.right - (fillRect.width() * (1 - fillFraction)), + fillRect.bottom + ) + c.drawText(batteryLevel.toString(), pctX, pctY, textPaint) + } + c.restore() + } + } + + private fun batteryColorForLevel(level: Int): Int { + return when { + charging || powerSaveEnabled -> fillColor + else -> getColorForLevel(level) + } + } + + private fun getColorForLevel(level: Int): Int { + var thresh: Int + var color = 0 + var i = 0 + while (i < colorLevels.size) { + thresh = colorLevels[i] + color = colorLevels[i + 1] + if (level <= thresh) { + + // Respect tinting for "normal" level + return if (i == colorLevels.size - 2) { + fillColor + } else { + color + } + } + i += 2 + } + return color + } + + /** + * Alpha is unused internally, and should be defined in the colors passed to {@link setColors}. + * Further, setting an alpha for a dual tone battery meter doesn't make sense without bounds + * defining the minimum background fill alpha. This is because fill + background must be equal + * to the net alpha passed in here. + */ + override fun setAlpha(alpha: Int) { + } + + override fun setColorFilter(colorFilter: ColorFilter?) { + fillPaint.colorFilter = colorFilter + fillColorStrokePaint.colorFilter = colorFilter + dualToneBackgroundFill.colorFilter = colorFilter + } + + /** + * Deprecated, but required by Drawable + */ + @Deprecated( + "Deprecated in Java", + ReplaceWith("PixelFormat.OPAQUE", "android.graphics.PixelFormat"), + ) + override fun getOpacity(): Int { + return PixelFormat.OPAQUE + } + + override fun getIntrinsicHeight(): Int { + return intrinsicHeight + } + + override fun getIntrinsicWidth(): Int { + return intrinsicWidth + } + + /** + * Set the fill level + */ + @Suppress("PARAMETER_NAME_CHANGED_ON_OVERRIDE") + override fun setBatteryLevel(l: Int) { + // invertFillIcon = if (l >= 67) true else if (l <= 33) false else invertFillIcon + batteryLevel = l + levelColor = batteryColorForLevel(batteryLevel) + invalidateSelf() + } + + fun getBatteryLevel(): Int { + return batteryLevel + } + + override fun onBoundsChange(bounds: Rect) { + super.onBoundsChange(bounds) + updateSize() + } + + fun setPadding(left: Int, top: Int, right: Int, bottom: Int) { + padding.left = left + padding.top = top + padding.right = right + padding.bottom = bottom + + updateSize() + } + + override fun setColors(fgColor: Int, bgColor: Int, singleToneColor: Int) { + fillColor = if (dualTone) fgColor else singleToneColor + + fillPaint.color = fillColor + fillColorStrokePaint.color = fillColor + + scaledFillPaint.color = fillColor + scaledPerimeterPaint.color = fillColor + scaledPerimeterPaintDef.color = fillColor + + backgroundColor = bgColor + dualToneBackgroundFill.color = bgColor + + // Also update the level color, since fillColor may have changed + levelColor = batteryColorForLevel(batteryLevel) + + invalidateSelf() + } + + private fun postInvalidate() { + unscheduleSelf(invalidateRunnable) + scheduleSelf(invalidateRunnable, 0) + } + + private fun updateSize() { + val b = bounds + if (b.isEmpty) { + scaleMatrix.setScale(1f, 1f) + } else { + scaleMatrix.setScale((b.right / WIDTH), (b.bottom / HEIGHT)) + } + + perimeterPath.transform(scaleMatrix, scaledPerimeter) + errorPerimeterPath.transform(scaleMatrix, scaledErrorPerimeter) + fillMask.transform(scaleMatrix, scaledFill) + scaledFill.computeBounds(fillRect, true) + boltPath.transform(scaleMatrix, scaledBolt) + plusPath.transform(scaleMatrix, scaledPlus) + + // It is expected that this view only ever scale by the same factor in each dimension, so + // just pick one to scale the strokeWidths + val scaledStrokeWidth = + (b.right / WIDTH * PROTECTION_STROKE_WIDTH).coerceAtLeast(PROTECTION_MIN_STROKE_WIDTH) + + fillColorStrokePaint.strokeWidth = scaledStrokeWidth + fillColorStrokeProtection.strokeWidth = scaledStrokeWidth + } + + @SuppressLint("RestrictedApi") + private fun loadPaths() { + val pathString = modRes.getString(R.string.config_landscapeBatteryPerimeterPathO) + perimeterPath.set(PathParser.createPathFromPathData(pathString)) + perimeterPath.computeBounds(RectF(), true) + + val errorPathString = modRes.getString(R.string.config_landscapeBatteryErrorPerimeterPathO) + errorPerimeterPath.set(PathParser.createPathFromPathData(errorPathString)) + errorPerimeterPath.computeBounds(RectF(), true) + + val fillMaskString = modRes.getString(R.string.config_landscapeBatteryFillMaskO) + fillMask.set(PathParser.createPathFromPathData(fillMaskString)) + // Set the fill rect so we can calculate the fill properly + fillMask.computeBounds(fillRect, true) + + val boltPathString = modRes.getString(R.string.config_landscapeBatteryBoltPathO) + boltPath.set(PathParser.createPathFromPathData(boltPathString)) + + val plusPathString = modRes.getString(R.string.config_landscapeBatteryPowersavePathO) + plusPath.set(PathParser.createPathFromPathData(plusPathString)) + + dualTone = false + } + + companion object { + private const val TAG = "LandscapeBatteryO" + private const val WIDTH = 20f + private const val HEIGHT = 12f + private const val CRITICAL_LEVEL = 15 + + // On a 20x12 grid, how wide to make the fill protection stroke. + // Scales when our size changes + private const val PROTECTION_STROKE_WIDTH = 2f + + // Arbitrarily chosen for visibility at small sizes + private const val PROTECTION_MIN_STROKE_WIDTH = 3f + } +} diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml index afaf7f266..c04edc9b1 100644 --- a/app/src/main/res/values/arrays.xml +++ b/app/src/main/res/values/arrays.xml @@ -96,6 +96,21 @@ Landscape MIUI Pill Landscape L ColorOS Landscape R ColorOS + Landscape Battery A + Landscape Battery B + Landscape Battery C + Landscape Battery D + Landscape Battery E + Landscape Battery F + Landscape Battery G + Landscape Battery H + Landscape Battery I + Landscape Battery J + Landscape Battery K + Landscape Battery L + Landscape Battery M + Landscape Battery N + Landscape Battery O @string/sbclock_color_follow_system diff --git a/app/src/main/res/values/battery_strings.xml b/app/src/main/res/values/battery_strings.xml index 5df222106..57dc2e8b5 100644 --- a/app/src/main/res/values/battery_strings.xml +++ b/app/src/main/res/values/battery_strings.xml @@ -121,4 +121,131 @@ M5.79,2.00L17.33,2.00A2.67 2.67 0 0 1 20.00,4.67L20.00,7.33A2.67 2.67 0 0 1 17.33,10.00L5.79,10.00A2.67 2.67 0 0 1 3.13,7.33L3.13,4.67A2.67 2.67 0 0 1 5.79,2.00z M9.43,6.08L12.70,2.57C12.86,2.37,13.11,2.53,12.97,2.83L12.05,4.93C11.78,5.57,12.12,5.58,12.57,5.56L13.39,5.54C13.76,5.54,13.76,5.74,13.57,5.92L10.30,9.43C10.14,9.63,9.89,9.47,10.03,9.17L10.95,7.07C11.22,6.43,10.88,6.42,10.43,6.44L9.61,6.46C9.24,6.46,9.24,6.26,9.43,6.08z @string/config_landscapeBatteryPerimeterRColorOS + + + M3.387,0.676C3.375,0.676 3.313,0.684 3.246,0.691C2.676,0.746 2.094,0.992 1.641,1.371C1.063,1.855 0.699,2.508 0.578,3.27L0.555,3.414L0.555,6.586L0.578,6.73C0.664,7.285 0.875,7.766 1.219,8.199C1.313,8.32 1.555,8.563 1.68,8.66C2.172,9.051 2.75,9.277 3.367,9.32C3.477,9.328 4.922,9.328 8.074,9.328C12.32,9.324 12.633,9.324 12.742,9.309C12.984,9.273 13.207,9.215 13.43,9.133C13.563,9.086 13.844,8.945 13.969,8.863C14.512,8.52 14.938,8.012 15.168,7.43L15.191,7.371L15.234,7.371C15.336,7.371 15.586,7.324 15.746,7.277C16.539,7.047 17.156,6.414 17.367,5.617C17.422,5.43 17.441,5.281 17.445,5.078C17.461,4.656 17.379,4.289 17.188,3.918C16.871,3.293 16.281,2.84 15.594,2.684C15.5,2.664 15.266,2.633 15.203,2.629C15.195,2.629 15.184,2.605 15.168,2.57C15.039,2.25 14.84,1.93 14.59,1.656C14.176,1.195 13.578,0.855 12.973,0.734C12.66,0.672 13.082,0.676 8.016,0.676C5.48,0.672 3.395,0.676 3.387,0.676ZM12.441,1.875C12.684,1.914 12.863,1.973 13.055,2.066C13.68,2.367 14.109,2.977 14.191,3.676C14.195,3.73 14.199,4.223 14.199,5.004C14.199,6.059 14.199,6.258 14.184,6.355C14.094,7.059 13.66,7.652 13.023,7.949C12.84,8.039 12.664,8.09 12.43,8.129C12.32,8.145 12.117,8.145 7.969,8.145C3.848,8.145 3.617,8.145 3.508,8.129C3.199,8.082 2.941,7.984 2.688,7.828C2.25,7.559 1.922,7.109 1.797,6.602C1.734,6.355 1.734,6.398 1.734,4.996C1.734,3.59 1.734,3.648 1.797,3.398C2.012,2.535 2.766,1.914 3.66,1.859C3.73,1.855 5.711,1.852 8.059,1.855C12.125,1.855 12.34,1.855 12.441,1.875ZM15.582,3.828C16.219,4.066 16.547,4.742 16.34,5.391C16.203,5.809 15.859,6.121 15.418,6.223L15.391,6.23L15.391,3.773L15.457,3.789C15.496,3.801 15.551,3.816 15.582,3.828ZM15.582,3.828 + M3.387,0.676C3.375,0.676 3.313,0.684 3.246,0.691C2.676,0.746 2.094,0.992 1.641,1.371C1.063,1.855 0.699,2.508 0.578,3.27L0.555,3.414L0.555,6.586L0.578,6.73C0.664,7.285 0.875,7.766 1.219,8.199C1.313,8.32 1.555,8.563 1.68,8.66C2.172,9.051 2.75,9.277 3.367,9.32C3.477,9.328 4.922,9.328 8.074,9.328C12.32,9.324 12.633,9.324 12.742,9.309C12.984,9.273 13.207,9.215 13.43,9.133C13.563,9.086 13.844,8.945 13.969,8.863C14.512,8.52 14.938,8.012 15.168,7.43L15.191,7.371L15.234,7.371C15.336,7.371 15.586,7.324 15.746,7.277C16.539,7.047 17.156,6.414 17.367,5.617C17.422,5.43 17.441,5.281 17.445,5.078C17.461,4.656 17.379,4.289 17.188,3.918C16.871,3.293 16.281,2.84 15.594,2.684C15.5,2.664 15.266,2.633 15.203,2.629C15.195,2.629 15.184,2.605 15.168,2.57C15.039,2.25 14.84,1.93 14.59,1.656C14.176,1.195 13.578,0.855 12.973,0.734C12.66,0.672 13.082,0.676 8.016,0.676C5.48,0.672 3.395,0.676 3.387,0.676ZM12.441,1.875C12.684,1.914 12.863,1.973 13.055,2.066C13.68,2.367 14.109,2.977 14.191,3.676C14.195,3.73 14.199,4.223 14.199,5.004C14.199,6.059 14.199,6.258 14.184,6.355C14.094,7.059 13.66,7.652 13.023,7.949C12.84,8.039 12.664,8.09 12.43,8.129C12.32,8.145 12.117,8.145 7.969,8.145C3.848,8.145 3.617,8.145 3.508,8.129C3.199,8.082 2.941,7.984 2.688,7.828C2.25,7.559 1.922,7.109 1.797,6.602C1.734,6.355 1.734,6.398 1.734,4.996C1.734,3.59 1.734,3.648 1.797,3.398C2.012,2.535 2.766,1.914 3.66,1.859C3.73,1.855 5.711,1.852 8.059,1.855C12.125,1.855 12.34,1.855 12.441,1.875ZM15.582,3.828C16.219,4.066 16.547,4.742 16.34,5.391C16.203,5.809 15.859,6.121 15.418,6.223L15.391,6.23L15.391,3.773L15.457,3.789C15.496,3.801 15.551,3.816 15.582,3.828ZM15.582,3.828 + M3.871,2.742C3.734,2.75 3.629,2.773 3.512,2.816C3.328,2.883 3.172,2.98 3.031,3.113C2.801,3.34 2.66,3.617 2.621,3.934C2.602,4.078 2.605,5.953 2.625,6.09C2.68,6.496 2.914,6.855 3.266,7.07C3.43,7.172 3.629,7.238 3.836,7.262C3.906,7.27 5.078,7.27 8.035,7.27L12.137,7.266L12.227,7.242C12.57,7.164 12.84,6.996 13.035,6.746C13.211,6.52 13.297,6.289 13.32,5.984C13.332,5.828 13.332,4.184 13.32,4.02C13.309,3.871 13.289,3.766 13.25,3.656C13.086,3.184 12.691,2.848 12.188,2.754C12.102,2.734 11.844,2.734 8.035,2.734C5.801,2.734 3.93,2.734 3.871,2.742ZM3.871,2.742 + M9.23,2.371C9.176,2.383 9.129,2.41 9.09,2.461C9.07,2.48 8.836,2.867 8.566,3.313C8.297,3.762 8.07,4.129 8.07,4.129C8.066,4.129 8.012,3.996 7.945,3.836C7.879,3.676 7.813,3.52 7.797,3.488C7.734,3.363 7.59,3.316 7.461,3.371C7.402,3.395 7.348,3.453 7.324,3.508C7.297,3.57 7.305,3.652 7.375,3.984C7.41,4.16 7.441,4.309 7.438,4.316C7.438,4.32 7.301,4.262 7.094,4.16C5.773,3.516 5.672,3.469 5.625,3.465C5.461,3.445 5.32,3.582 5.332,3.746C5.34,3.813 5.375,3.883 5.422,3.922C5.438,3.938 5.82,4.168 6.266,4.438C6.711,4.703 7.082,4.93 7.086,4.934C7.098,4.941 7.016,4.977 6.758,5.07C6.563,5.141 6.395,5.207 6.371,5.227C6.344,5.242 6.316,5.273 6.297,5.309C6.238,5.414 6.262,5.563 6.355,5.641C6.398,5.676 6.469,5.699 6.516,5.699C6.535,5.699 6.715,5.664 6.91,5.625C7.109,5.582 7.273,5.551 7.277,5.555C7.277,5.555 7.09,5.949 6.859,6.426C6.438,7.297 6.434,7.297 6.434,7.367C6.43,7.43 6.43,7.449 6.453,7.496C6.535,7.66 6.758,7.688 6.883,7.555C6.898,7.531 7.137,7.145 7.41,6.691C7.684,6.242 7.906,5.875 7.91,5.879C7.914,5.883 7.969,6.023 8.031,6.191C8.156,6.531 8.164,6.551 8.211,6.598C8.293,6.676 8.402,6.691 8.508,6.641C8.602,6.594 8.648,6.52 8.648,6.41C8.648,6.383 8.621,6.215 8.586,6.035C8.551,5.859 8.523,5.715 8.523,5.711C8.527,5.711 8.895,5.879 9.336,6.09C10.371,6.574 10.266,6.531 10.344,6.531C10.535,6.531 10.66,6.328 10.574,6.156C10.563,6.125 10.535,6.09 10.523,6.078C10.508,6.066 10.129,5.832 9.68,5.559C9.172,5.25 8.867,5.063 8.875,5.059C8.883,5.055 9.02,5.004 9.18,4.949C9.34,4.895 9.492,4.84 9.516,4.824C9.688,4.723 9.68,4.461 9.496,4.367C9.422,4.328 9.379,4.332 9.027,4.406C8.848,4.441 8.695,4.469 8.695,4.469C8.691,4.465 8.879,4.074 9.109,3.598C9.34,3.125 9.531,2.719 9.539,2.695C9.566,2.613 9.531,2.488 9.465,2.434C9.402,2.379 9.313,2.355 9.23,2.371ZM9.23,2.371 + M7.906,3.32C7.816,3.344 7.754,3.395 7.719,3.477L7.695,3.523L7.691,4.129L7.688,4.73L7.086,4.73C6.504,4.73 6.484,4.73 6.441,4.75C6.387,4.777 6.336,4.82 6.309,4.875C6.277,4.93 6.277,5.066 6.305,5.117C6.332,5.176 6.383,5.223 6.434,5.246L6.484,5.27L7.688,5.27L7.691,5.871L7.695,6.477L7.719,6.527C7.813,6.734 8.105,6.738 8.215,6.535C8.234,6.496 8.234,6.488 8.238,5.883L8.242,5.27L8.859,5.27L9.477,5.266L9.52,5.238C9.613,5.176 9.652,5.105 9.652,5C9.652,4.922 9.629,4.859 9.574,4.809C9.496,4.73 9.535,4.734 8.852,4.73L8.242,4.73L8.238,4.117C8.234,3.508 8.234,3.504 8.215,3.465C8.184,3.406 8.133,3.359 8.082,3.34C8.031,3.316 7.949,3.309 7.906,3.32ZM7.906,3.32 + + + M4.64,1.85L15.36,1.85A3.69 3.69 0 0 1 19.05,5.54L19.05,7.85A3.69 3.69 0 0 1 15.36,11.54L4.64,11.54A3.69 3.69 0 0 1 0.95,7.85L0.95,5.54A3.69 3.69 0 0 1 4.64,1.85zM2.38,5.54L2.38,7.85A2.31 2.31 0 0 0 4.69,10.15L15.31,10.15A2.31 2.31 0 0 0 17.62,7.85L17.62,5.54A2.31 2.31 0 0 0 15.31,3.23L4.69,3.23A2.31 2.31 0 0 0 2.38,5.54zM14.29,1.85C14.23,1.29,14.18,0.74,13.81,0.46C13.44,0.18,12.75,0.18,12.38,0.46C12.01,0.74,11.96,1.29,11.90,1.85M8.10,1.85C8.04,1.29,7.99,0.74,7.62,0.46C7.25,0.18,6.56,0.18,6.19,0.46C5.82,0.74,5.77,1.29,5.71,1.85 + M4.64,1.85L15.36,1.85A3.69 3.69 0 0 1 19.05,5.54L19.05,7.85A3.69 3.69 0 0 1 15.36,11.54L4.64,11.54A3.69 3.69 0 0 1 0.95,7.85L0.95,5.54A3.69 3.69 0 0 1 4.64,1.85zM2.38,5.54L2.38,7.85A2.31 2.31 0 0 0 4.69,10.15L15.31,10.15A2.31 2.31 0 0 0 17.62,7.85L17.62,5.54A2.31 2.31 0 0 0 15.31,3.23L4.69,3.23A2.31 2.31 0 0 0 2.38,5.54zM14.29,1.85C14.23,1.29,14.18,0.74,13.81,0.46C13.44,0.18,12.75,0.18,12.38,0.46C12.01,0.74,11.96,1.29,11.90,1.85M8.10,1.85C8.04,1.29,7.99,0.74,7.62,0.46C7.25,0.18,6.56,0.18,6.19,0.46C5.82,0.74,5.77,1.29,5.71,1.85 + M5.18,4.15L14.82,4.15A1.85 1.85 0 0 1 16.67,6.00L16.67,7.38A1.85 1.85 0 0 1 14.82,9.23L5.18,9.23A1.85 1.85 0 0 1 3.33,7.38L3.33,6.00A1.85 1.85 0 0 1 5.18,4.15z + M 10.125 2.203 C 10.305 2.145 10.508 2.16 10.68 2.238 C 10.762 2.289 10.855 2.328 10.906 2.414 C 11 2.512 11.02 2.652 11.043 2.777 C 11.023 3.016 10.906 3.23 10.855 3.461 C 10.84 3.516 10.816 3.566 10.801 3.621 C 10.773 3.742 10.711 3.855 10.684 3.98 C 10.648 4.102 10.59 4.219 10.563 4.344 C 10.527 4.445 10.496 4.543 10.469 4.648 C 10.441 4.758 10.375 4.859 10.363 4.973 L 11.422 4.973 C 11.73 4.965 12.047 5.094 12.246 5.336 C 12.305 5.445 12.383 5.551 12.406 5.68 C 12.438 5.82 12.43 5.965 12.391 6.102 C 12.348 6.23 12.273 6.344 12.199 6.457 C 11.984 6.762 11.801 7.086 11.586 7.387 C 11.531 7.5 11.445 7.594 11.383 7.703 C 11.324 7.797 11.25 7.883 11.203 7.98 C 11.109 8.125 11.012 8.27 10.91 8.41 C 10.844 8.547 10.742 8.66 10.672 8.789 C 10.547 8.953 10.453 9.133 10.336 9.297 C 10.285 9.355 10.258 9.43 10.207 9.484 C 10.156 9.547 10.141 9.629 10.066 9.668 C 9.996 9.703 9.938 9.762 9.863 9.781 C 9.633 9.875 9.352 9.844 9.152 9.691 C 9.086 9.652 9.043 9.586 8.992 9.531 C 8.945 9.434 8.918 9.328 8.895 9.223 C 8.898 9.098 8.965 8.98 8.988 8.859 C 9.027 8.703 9.094 8.559 9.121 8.398 C 9.129 8.32 9.172 8.254 9.191 8.18 C 9.223 8.008 9.297 7.848 9.336 7.676 C 9.445 7.375 9.504 7.055 9.617 6.754 L 8.98 6.754 C 8.785 6.734 8.59 6.758 8.398 6.738 C 8.277 6.703 8.145 6.695 8.043 6.617 C 7.965 6.57 7.895 6.523 7.828 6.469 C 7.797 6.43 7.766 6.391 7.73 6.352 C 7.688 6.277 7.648 6.199 7.613 6.121 C 7.551 5.91 7.563 5.676 7.668 5.484 C 7.781 5.262 7.957 5.078 8.086 4.867 C 8.125 4.793 8.188 4.738 8.227 4.668 C 8.266 4.598 8.328 4.547 8.355 4.477 C 8.391 4.434 8.422 4.395 8.445 4.348 C 8.551 4.199 8.656 4.055 8.766 3.906 C 8.816 3.793 8.914 3.707 8.977 3.598 C 9.004 3.543 9.055 3.5 9.086 3.449 C 9.156 3.324 9.254 3.211 9.328 3.09 C 9.438 2.973 9.504 2.824 9.602 2.703 C 9.699 2.586 9.777 2.453 9.875 2.336 C 9.961 2.293 10.031 2.227 10.125 2.203 Z M 10.125 2.203 + M10.48,4.85L10.48,8.52A0.47 0.47 0 0 1 10.01,9.00L10.01,9.00A0.47 0.47 0 0 1 9.53,8.52L9.54,4.85A0.47 0.47 0 0 1 10.01,4.38L10.01,4.38A0.47 0.47 0 0 1 10.48,4.85zM8.09,6.23L11.93,6.23A0.46 0.46 0 0 1 12.39,6.69L12.39,6.69A0.46 0.46 0 0 1 11.93,7.15L8.09,7.15A0.46 0.46 0 0 1 7.63,6.69L7.63,6.69A0.46 0.46 0 0 1 8.09,6.23z + + + M3.32,0.906C2.984,0.938 2.848,0.965 2.594,1.047C2.297,1.145 2.012,1.297 1.746,1.5C1.621,1.594 1.359,1.852 1.258,1.98C1.004,2.305 0.824,2.676 0.727,3.074C0.684,3.254 0.664,3.383 0.648,3.59C0.637,3.813 0.637,8.176 0.648,8.406C0.68,8.934 0.836,9.398 1.125,9.836C1.238,10.008 1.352,10.141 1.512,10.297C1.992,10.766 2.605,11.035 3.289,11.09C3.395,11.098 5.102,11.102 9.496,11.098L15.555,11.094L15.691,11.074C15.902,11.039 16.051,11.004 16.23,10.941C17.145,10.629 17.844,9.859 18.07,8.914C18.121,8.707 18.141,8.535 18.148,8.277L18.152,8.039L18.242,7.988C18.43,7.879 18.648,7.695 18.797,7.52C19.008,7.266 19.18,6.926 19.262,6.59C19.316,6.363 19.324,6.277 19.324,6.004C19.324,5.742 19.32,5.676 19.273,5.469C19.195,5.113 19.02,4.75 18.797,4.48C18.652,4.309 18.426,4.117 18.238,4.008L18.152,3.957L18.148,3.723C18.145,3.563 18.133,3.438 18.121,3.344C17.98,2.414 17.414,1.621 16.578,1.203C16.305,1.066 16.012,0.977 15.668,0.922C15.563,0.906 15.215,0.906 9.453,0.902C6.102,0.902 3.34,0.906 3.32,0.906ZM15.434,2.078C16.113,2.137 16.676,2.574 16.891,3.219C16.938,3.359 16.957,3.449 16.969,3.594C16.984,3.746 16.984,8.254 16.969,8.406C16.934,8.805 16.766,9.152 16.484,9.434C16.199,9.723 15.844,9.887 15.438,9.922C15.266,9.934 3.527,9.934 3.359,9.922C2.953,9.887 2.59,9.719 2.313,9.441C2.086,9.219 1.938,8.953 1.863,8.645C1.813,8.449 1.816,8.535 1.816,6C1.816,3.465 1.813,3.551 1.863,3.355C2.027,2.684 2.578,2.184 3.254,2.09C3.301,2.086 3.359,2.078 3.387,2.078C3.523,2.066 15.328,2.07 15.434,2.078ZM15.434,2.078 + M3.32,0.906C2.984,0.938 2.848,0.965 2.594,1.047C2.297,1.145 2.012,1.297 1.746,1.5C1.621,1.594 1.359,1.852 1.258,1.98C1.004,2.305 0.824,2.676 0.727,3.074C0.684,3.254 0.664,3.383 0.648,3.59C0.637,3.813 0.637,8.176 0.648,8.406C0.68,8.934 0.836,9.398 1.125,9.836C1.238,10.008 1.352,10.141 1.512,10.297C1.992,10.766 2.605,11.035 3.289,11.09C3.395,11.098 5.102,11.102 9.496,11.098L15.555,11.094L15.691,11.074C15.902,11.039 16.051,11.004 16.23,10.941C17.145,10.629 17.844,9.859 18.07,8.914C18.121,8.707 18.141,8.535 18.148,8.277L18.152,8.039L18.242,7.988C18.43,7.879 18.648,7.695 18.797,7.52C19.008,7.266 19.18,6.926 19.262,6.59C19.316,6.363 19.324,6.277 19.324,6.004C19.324,5.742 19.32,5.676 19.273,5.469C19.195,5.113 19.02,4.75 18.797,4.48C18.652,4.309 18.426,4.117 18.238,4.008L18.152,3.957L18.148,3.723C18.145,3.563 18.133,3.438 18.121,3.344C17.98,2.414 17.414,1.621 16.578,1.203C16.305,1.066 16.012,0.977 15.668,0.922C15.563,0.906 15.215,0.906 9.453,0.902C6.102,0.902 3.34,0.906 3.32,0.906ZM15.434,2.078C16.113,2.137 16.676,2.574 16.891,3.219C16.938,3.359 16.957,3.449 16.969,3.594C16.984,3.746 16.984,8.254 16.969,8.406C16.934,8.805 16.766,9.152 16.484,9.434C16.199,9.723 15.844,9.887 15.438,9.922C15.266,9.934 3.527,9.934 3.359,9.922C2.953,9.887 2.59,9.719 2.313,9.441C2.086,9.219 1.938,8.953 1.863,8.645C1.813,8.449 1.816,8.535 1.816,6C1.816,3.465 1.813,3.551 1.863,3.355C2.027,2.684 2.578,2.184 3.254,2.09C3.301,2.086 3.359,2.078 3.387,2.078C3.523,2.066 15.328,2.07 15.434,2.078ZM15.434,2.078 + M3.328,2.738C2.938,2.805 2.598,3.121 2.5,3.508C2.488,3.551 2.477,3.633 2.469,3.695C2.457,3.848 2.457,8.137 2.469,8.305C2.492,8.574 2.598,8.797 2.785,8.98C2.91,9.102 3.047,9.18 3.23,9.238L3.316,9.266L15.465,9.266L15.555,9.238C15.738,9.18 15.895,9.086 16.02,8.953C16.141,8.824 16.223,8.688 16.277,8.512L16.305,8.426L16.309,6.051C16.309,4.285 16.309,3.656 16.301,3.602C16.266,3.383 16.16,3.18 15.992,3.02C15.867,2.898 15.734,2.82 15.551,2.762L15.465,2.734L9.422,2.734C6.094,2.734 3.352,2.734 3.328,2.738ZM3.328,2.738 + M9.629,2.238C9.59,2.25 9.543,2.266 9.523,2.273C9.508,2.281 9.492,2.289 9.488,2.289C9.48,2.289 9.445,2.313 9.406,2.34C9.332,2.391 9.254,2.48 9.164,2.602C9.141,2.641 9.078,2.727 9.031,2.793C8.949,2.91 8.883,3.008 8.852,3.055C8.832,3.078 8.762,3.18 8.688,3.281C8.66,3.32 8.617,3.379 8.598,3.414C8.574,3.445 8.531,3.512 8.496,3.555C8.453,3.617 8.168,4.023 8.09,4.137C8.082,4.148 8.035,4.211 7.988,4.277C7.941,4.344 7.898,4.406 7.887,4.422C7.879,4.434 7.84,4.488 7.801,4.547C7.762,4.602 7.707,4.68 7.676,4.727C7.605,4.828 7.547,4.914 7.465,5.023C7.27,5.297 7.156,5.469 7.137,5.52C7.125,5.547 7.113,5.574 7.109,5.59C7.074,5.676 7.059,5.75 7.059,5.863C7.059,5.984 7.07,6.047 7.117,6.176C7.18,6.359 7.391,6.566 7.598,6.656C7.762,6.723 7.813,6.73 8.172,6.738C8.348,6.742 8.625,6.746 8.781,6.75C8.941,6.75 9.07,6.754 9.07,6.758C9.07,6.762 9.059,6.809 9.039,6.859C9.023,6.91 8.988,7.035 8.961,7.129C8.871,7.434 8.789,7.699 8.75,7.832C8.727,7.902 8.691,8.023 8.672,8.098C8.648,8.172 8.609,8.313 8.578,8.406C8.551,8.496 8.516,8.629 8.496,8.691C8.477,8.754 8.441,8.867 8.422,8.945C8.363,9.133 8.359,9.188 8.395,9.32C8.43,9.457 8.445,9.488 8.543,9.586C8.68,9.723 8.844,9.789 9.047,9.789C9.27,9.789 9.477,9.695 9.598,9.531C9.664,9.445 9.914,9.066 9.93,9.035C9.934,9.027 9.957,8.992 9.984,8.953C10.008,8.914 10.051,8.852 10.078,8.809C10.105,8.77 10.18,8.656 10.242,8.559C10.305,8.465 10.398,8.324 10.445,8.25C10.492,8.176 10.574,8.055 10.621,7.98C10.672,7.91 10.711,7.848 10.711,7.844C10.711,7.844 10.734,7.809 10.762,7.77C10.789,7.734 10.809,7.699 10.809,7.695C10.809,7.691 10.84,7.648 10.871,7.598C10.906,7.547 10.938,7.5 10.941,7.492C10.941,7.488 10.98,7.43 11.023,7.367C11.07,7.301 11.109,7.242 11.113,7.23C11.156,7.164 11.348,6.875 11.379,6.828C11.398,6.801 11.426,6.762 11.434,6.746C11.441,6.727 11.457,6.703 11.469,6.688C11.496,6.645 11.559,6.547 11.59,6.508C11.602,6.484 11.621,6.461 11.625,6.453C11.633,6.441 11.652,6.41 11.676,6.375C11.738,6.281 11.758,6.246 11.777,6.195C11.785,6.168 11.797,6.141 11.801,6.137C11.805,6.129 11.813,6.094 11.824,6.059C11.855,5.934 11.84,5.656 11.801,5.605C11.797,5.598 11.785,5.57 11.777,5.543C11.734,5.41 11.578,5.234 11.422,5.137C11.32,5.074 11.246,5.047 11.09,5.016C10.977,4.988 10.977,4.988 10.387,4.988L9.801,4.988L9.836,4.887C9.887,4.734 9.977,4.465 10.102,4.078C10.16,3.891 10.23,3.676 10.254,3.605C10.277,3.531 10.309,3.445 10.316,3.41C10.328,3.375 10.352,3.305 10.371,3.254C10.387,3.203 10.41,3.137 10.422,3.098C10.434,3.059 10.449,2.996 10.461,2.957C10.492,2.844 10.484,2.77 10.43,2.605C10.406,2.543 10.348,2.461 10.281,2.398C10.246,2.363 10.137,2.289 10.121,2.289C10.117,2.289 10.102,2.281 10.086,2.273C9.992,2.223 9.77,2.207 9.629,2.238ZM9.629,2.238 + M9.285,3.539C9.121,3.574 8.988,3.695 8.93,3.859L8.906,3.926L8.898,5.52L8.156,5.52C7.68,5.52 7.383,5.523 7.336,5.531C6.945,5.586 6.785,6.059 7.063,6.336C7.121,6.395 7.176,6.426 7.27,6.457C7.309,6.473 7.418,6.473 8.105,6.477L8.898,6.48L8.906,8.074L8.93,8.141C9,8.344 9.168,8.465 9.379,8.465C9.59,8.465 9.758,8.344 9.832,8.145L9.855,8.086L9.859,7.281L9.859,6.48L10.664,6.477C11.379,6.473 11.469,6.473 11.512,6.457C11.652,6.41 11.754,6.324 11.809,6.207C11.949,5.914 11.766,5.574 11.445,5.531C11.395,5.523 11.098,5.52 10.613,5.52L9.859,5.52L9.859,4.762C9.859,3.988 9.855,3.926 9.82,3.828C9.777,3.715 9.648,3.598 9.531,3.559C9.461,3.535 9.352,3.527 9.285,3.539ZM9.285,3.539 + + + M3.14,0.59C0.53,0.89,-0.25,4.2,1.49,7.59C2.42,9.43,3.9,11.02,5.84,12.3C6.66,12.84,6.7,12.84,7.15,12.57C9.42,11.2,11.24,9.23,12.16,7.14C12.21,7.03,12.25,6.94,12.25,6.94C12.25,6.94,12.34,7.02,12.45,7.13C13.05,7.73,13.69,8.21,14.54,8.69C14.85,8.87,14.96,8.87,15.27,8.69C16.78,7.83,17.89,6.78,18.61,5.54C19.59,3.87,19.62,2.09,18.7,1.16C17.85,0.32,16.45,0.38,14.92,1.33C14.91,1.34,14.87,1.32,14.78,1.27C13.48,0.49,12.27,0.36,11.45,0.89L11.39,0.93L11.3,0.89C10.14,0.27,8.43,0.59,6.79,1.75C6.71,1.8,6.72,1.8,6.63,1.74C5.42,0.9,4.11,0.47,3.14,0.59M3.84,1.6C4.56,1.7,5.45,2.11,6.3,2.73C6.63,2.97,6.8,2.98,7.1,2.75C8.45,1.76,9.77,1.36,10.65,1.7C12,2.21,12.26,4.45,11.24,6.74C10.67,8,9.77,9.21,8.64,10.23C8.09,10.73,7.25,11.35,6.75,11.64L6.71,11.66L6.65,11.62C4.88,10.54,3.41,9.03,2.48,7.34C1.86,6.2,1.52,4.88,1.57,3.86C1.66,2.24,2.49,1.41,3.84,1.6M12.76,1.58C13.29,1.62,13.84,1.85,14.54,2.3C14.73,2.43,14.76,2.43,14.91,2.43C15.05,2.43,15.08,2.43,15.27,2.3C17.03,1.16,18.25,1.38,18.36,2.85C18.48,4.43,17.13,6.4,15.06,7.65C14.88,7.76,14.93,7.76,14.75,7.65C13.94,7.16,13.21,6.55,12.68,5.91L12.62,5.84L12.65,5.75C13.06,4.22,12.9,2.68,12.23,1.72C12.17,1.64,12.17,1.64,12.3,1.61C12.46,1.57,12.57,1.57,12.76,1.58M14.32,9.62C13.75,9.81,13.8,10.66,14.39,10.76C14.54,10.78,18.88,10.77,18.95,10.75C19.46,10.57,19.52,9.9,19.05,9.66C18.92,9.59,19.11,9.6,16.64,9.6L14.39,9.6L14.32,9.62M10.06,11.65C9.4,11.75,9.39,12.73,10.05,12.84C10.16,12.87,14.79,12.87,14.9,12.85C15.52,12.73,15.58,11.9,14.99,11.67L14.91,11.64L12.52,11.64C11.2,11.64,10.09,11.64,10.06,11.65 + M3.14,0.59C0.53,0.89,-0.25,4.2,1.49,7.59C2.42,9.43,3.9,11.02,5.84,12.3C6.66,12.84,6.7,12.84,7.15,12.57C9.42,11.2,11.24,9.23,12.16,7.14C12.21,7.03,12.25,6.94,12.25,6.94C12.25,6.94,12.34,7.02,12.45,7.13C13.05,7.73,13.69,8.21,14.54,8.69C14.85,8.87,14.96,8.87,15.27,8.69C16.78,7.83,17.89,6.78,18.61,5.54C19.59,3.87,19.62,2.09,18.7,1.16C17.85,0.32,16.45,0.38,14.92,1.33C14.91,1.34,14.87,1.32,14.78,1.27C13.48,0.49,12.27,0.36,11.45,0.89L11.39,0.93L11.3,0.89C10.14,0.27,8.43,0.59,6.79,1.75C6.71,1.8,6.72,1.8,6.63,1.74C5.42,0.9,4.11,0.47,3.14,0.59M3.84,1.6C4.56,1.7,5.45,2.11,6.3,2.73C6.63,2.97,6.8,2.98,7.1,2.75C8.45,1.76,9.77,1.36,10.65,1.7C12,2.21,12.26,4.45,11.24,6.74C10.67,8,9.77,9.21,8.64,10.23C8.09,10.73,7.25,11.35,6.75,11.64L6.71,11.66L6.65,11.62C4.88,10.54,3.41,9.03,2.48,7.34C1.86,6.2,1.52,4.88,1.57,3.86C1.66,2.24,2.49,1.41,3.84,1.6M12.76,1.58C13.29,1.62,13.84,1.85,14.54,2.3C14.73,2.43,14.76,2.43,14.91,2.43C15.05,2.43,15.08,2.43,15.27,2.3C17.03,1.16,18.25,1.38,18.36,2.85C18.48,4.43,17.13,6.4,15.06,7.65C14.88,7.76,14.93,7.76,14.75,7.65C13.94,7.16,13.21,6.55,12.68,5.91L12.62,5.84L12.65,5.75C13.06,4.22,12.9,2.68,12.23,1.72C12.17,1.64,12.17,1.64,12.3,1.61C12.46,1.57,12.57,1.57,12.76,1.58M14.32,9.62C13.75,9.81,13.8,10.66,14.39,10.76C14.54,10.78,18.88,10.77,18.95,10.75C19.46,10.57,19.52,9.9,19.05,9.66C18.92,9.59,19.11,9.6,16.64,9.6L14.39,9.6L14.32,9.62M10.06,11.65C9.4,11.75,9.39,12.73,10.05,12.84C10.16,12.87,14.79,12.87,14.9,12.85C15.52,12.73,15.58,11.9,14.99,11.67L14.91,11.64L12.52,11.64C11.2,11.64,10.09,11.64,10.06,11.65 + M3.2,1.59C2.17,1.73,1.59,2.58,1.56,3.97C1.51,6.63,3.61,9.79,6.68,11.65L6.71,11.67L6.93,11.53C9.97,9.58,11.92,6.59,11.87,3.97C11.81,1.33,9.77,0.8,7.11,2.74C6.79,2.97,6.64,2.97,6.29,2.71C5.13,1.89,4,1.47,3.2,1.59M12.39,1.59C12.32,1.6,12.17,1.64,12.17,1.64C12.17,1.65,12.2,1.68,12.22,1.73C12.88,2.69,13.04,4.09,12.67,5.63L12.62,5.84L12.66,5.89C13.18,6.54,13.95,7.18,14.8,7.69C14.91,7.76,14.89,7.76,15.07,7.65C17.02,6.47,18.32,4.67,18.37,3.1C18.41,1.42,17.08,1.08,15.26,2.3C14.99,2.48,14.83,2.48,14.58,2.32C13.7,1.75,12.93,1.49,12.39,1.59 + M0,6.5L0,13L20,13L20,0L0,0L0,6.5M3.64,1.58C4.4,1.63,5.37,2.05,6.3,2.73C6.63,2.97,6.8,2.98,7.1,2.75C8.97,1.37,10.67,1.2,11.41,2.32C12.36,3.74,11.8,6.42,10.1,8.65C9.24,9.77,7.89,10.98,6.75,11.64L6.71,11.66L6.65,11.62C4.23,10.14,2.39,7.86,1.78,5.59C1.19,3.38,1.83,1.69,3.31,1.58C3.46,1.57,3.48,1.57,3.64,1.58M12.76,1.58C13.28,1.62,13.84,1.85,14.54,2.3C14.83,2.49,14.98,2.49,15.25,2.31C17.01,1.16,18.25,1.37,18.36,2.85C18.48,4.43,17.12,6.4,15.06,7.65C14.88,7.76,14.93,7.76,14.75,7.65C13.95,7.16,13.23,6.56,12.69,5.92C12.62,5.84,12.62,5.85,12.66,5.72C13.05,4.14,12.89,2.68,12.23,1.72C12.17,1.64,12.17,1.64,12.3,1.61C12.45,1.57,12.57,1.57,12.76,1.58M12.4,7.09C12.45,7.14,12.49,7.18,12.49,7.18C12.49,7.18,12.44,7.14,12.39,7.09C12.34,7.03,12.3,6.99,12.3,6.99C12.3,6.99,12.35,7.03,12.4,7.09M17.43,7.07C17.43,7.07,17.4,7.1,17.37,7.13L17.32,7.18L17.37,7.13C17.42,7.07,17.43,7.07,17.43,7.07M17.72,9.6C17.13,9.6,16.15,9.6,15.56,9.6C14.96,9.6,15.45,9.59,16.64,9.59C17.83,9.59,18.32,9.6,17.72,9.6M3.65,10.52C3.67,10.54,3.68,10.56,3.68,10.56C3.68,10.56,3.66,10.54,3.64,10.52C3.62,10.5,3.61,10.49,3.61,10.49C3.61,10.49,3.63,10.5,3.65,10.52M9.79,10.52C9.79,10.52,9.77,10.54,9.75,10.57L9.7,10.61L9.75,10.56C9.78,10.52,9.79,10.52,9.79,10.52 + M5.02,3.89C3.62,4.12,3.25,5.94,4.23,7.72C4.69,8.56,5.37,9.28,6.27,9.87C6.69,10.14,6.78,10.13,7.41,9.67C9.13,8.42,10.07,6.42,9.59,4.98C9.21,3.83,8.07,3.55,6.81,4.31C6.68,4.39,6.72,4.39,6.56,4.3C6.02,3.97,5.45,3.82,5.02,3.89 + M3.14,0.59C0.53,0.89,-0.25,4.2,1.49,7.59C2.42,9.43,3.9,11.02,5.84,12.3C6.66,12.84,6.7,12.84,7.15,12.57C9.42,11.2,11.24,9.23,12.16,7.14C12.21,7.03,12.25,6.94,12.25,6.94C12.25,6.94,12.34,7.02,12.45,7.13C13.05,7.73,13.69,8.21,14.54,8.69C14.85,8.87,14.96,8.87,15.27,8.69C16.78,7.83,17.89,6.78,18.61,5.54C19.59,3.87,19.62,2.09,18.7,1.16C17.85,0.32,16.45,0.38,14.92,1.33C14.91,1.34,14.87,1.32,14.78,1.27C13.48,0.49,12.27,0.36,11.45,0.89L11.39,0.93L11.3,0.89C10.14,0.27,8.43,0.59,6.79,1.75C6.71,1.8,6.72,1.8,6.63,1.74C5.42,0.9,4.11,0.47,3.14,0.59M3.84,1.6C4.56,1.7,5.45,2.11,6.3,2.73C6.63,2.97,6.8,2.98,7.1,2.75C8.45,1.76,9.77,1.36,10.65,1.7C12,2.21,12.26,4.45,11.24,6.74C10.67,8,9.77,9.21,8.64,10.23C8.09,10.73,7.25,11.35,6.75,11.64L6.71,11.66L6.65,11.62C4.88,10.54,3.41,9.03,2.48,7.34C1.86,6.2,1.52,4.88,1.57,3.86C1.66,2.24,2.49,1.41,3.84,1.6M12.76,1.58C13.29,1.62,13.84,1.85,14.54,2.3C14.73,2.43,14.76,2.43,14.91,2.43C15.05,2.43,15.08,2.43,15.27,2.3C17.03,1.16,18.25,1.38,18.36,2.85C18.48,4.43,17.13,6.4,15.06,7.65C14.88,7.76,14.93,7.76,14.75,7.65C13.94,7.16,13.21,6.55,12.68,5.91L12.62,5.84L12.65,5.75C13.06,4.22,12.9,2.68,12.23,1.72C12.17,1.64,12.17,1.64,12.3,1.61C12.46,1.57,12.57,1.57,12.76,1.58M14.32,9.62C13.75,9.81,13.8,10.66,14.39,10.76C14.54,10.78,18.88,10.77,18.95,10.75C19.46,10.57,19.52,9.9,19.05,9.66C18.92,9.59,19.11,9.6,16.64,9.6L14.39,9.6L14.32,9.62M10.06,11.65C9.4,11.75,9.39,12.73,10.05,12.84C10.16,12.87,14.79,12.87,14.9,12.85C15.52,12.73,15.58,11.9,14.99,11.67L14.91,11.64L12.52,11.64C11.2,11.64,10.09,11.64,10.06,11.65 + + + M5.77,0.55C1.84,0.82,-0.75,4.85,0.62,8.55C2.02,12.32,6.68,13.66,9.86,11.2C9.98,11.1,10,11.09,10.02,11.11C10.05,11.18,10.63,11.57,10.91,11.72C14.42,13.64,18.8,11.65,19.63,7.76C20.48,3.76,17.17,0.11,13.11,0.57C12,0.7,10.75,1.23,10.03,1.88L10,1.9L9.89,1.8C8.73,0.89,7.26,0.45,5.77,0.55M14.02,1.55C17.58,1.72,19.81,5.45,18.27,8.65C16.79,11.69,12.8,12.41,10.36,10.07C10.26,9.97,10.22,9.95,10.1,9.87C9.87,9.74,9.64,9.57,9.45,9.41L9.41,9.36L9.27,9.49C8.14,10.64,6.44,11.05,4.92,10.55C2.35,9.7,1.21,6.72,2.55,4.37C3.55,2.63,5.65,1.83,7.55,2.45C8.21,2.66,8.78,3.01,9.27,3.5L9.41,3.64L9.45,3.59C9.64,3.43,9.92,3.23,10.13,3.11C10.21,3.07,10.26,3.03,10.34,2.95C11.23,2.09,12.34,1.61,13.55,1.55C13.79,1.54,13.8,1.54,14.02,1.55 + M5.77,0.55C1.84,0.82,-0.75,4.85,0.62,8.55C2.02,12.32,6.68,13.66,9.86,11.2C9.98,11.1,10,11.09,10.02,11.11C10.05,11.18,10.63,11.57,10.91,11.72C14.42,13.64,18.8,11.65,19.63,7.76C20.48,3.76,17.17,0.11,13.11,0.57C12,0.7,10.75,1.23,10.03,1.88L10,1.9L9.89,1.8C8.73,0.89,7.26,0.45,5.77,0.55M14.02,1.55C17.58,1.72,19.81,5.45,18.27,8.65C16.79,11.69,12.8,12.41,10.36,10.07C10.26,9.97,10.22,9.95,10.1,9.87C9.87,9.74,9.64,9.57,9.45,9.41L9.41,9.36L9.27,9.49C8.14,10.64,6.44,11.05,4.92,10.55C2.35,9.7,1.21,6.72,2.55,4.37C3.55,2.63,5.65,1.83,7.55,2.45C8.21,2.66,8.78,3.01,9.27,3.5L9.41,3.64L9.45,3.59C9.64,3.43,9.92,3.23,10.13,3.11C10.21,3.07,10.26,3.03,10.34,2.95C11.23,2.09,12.34,1.61,13.55,1.55C13.79,1.54,13.8,1.54,14.02,1.55 + M13.3,2.61C12.83,2.66,12.42,2.76,12,2.95C11.38,3.23,10.82,3.7,10.41,4.28L10.33,4.39L9.54,4.39C8.54,4.39,8.48,4.39,8.19,4.53L8.12,4.56L8.05,4.5C6.81,3.32,4.81,3.55,3.86,4.98C3.31,5.8,3.25,6.9,3.71,7.77C4.56,9.38,6.66,9.75,8.01,8.54L8.12,8.44L8.19,8.47C8.48,8.61,8.48,8.61,9.48,8.61L10.34,8.61L10.41,8.72C11.61,10.46,13.99,10.92,15.76,9.76C17.13,8.86,17.79,7.22,17.42,5.61C17.15,4.45,16.31,3.42,15.21,2.94C14.83,2.77,14.52,2.68,14.07,2.62C13.93,2.61,13.44,2.6,13.3,2.61 + M5.77,0.55C1.84,0.82,-0.75,4.85,0.62,8.55C2.02,12.32,6.68,13.66,9.86,11.2C9.98,11.1,10,11.09,10.02,11.11C10.05,11.18,10.63,11.57,10.91,11.72C14.42,13.64,18.8,11.65,19.63,7.76C20.48,3.76,17.17,0.11,13.11,0.57C12,0.7,10.75,1.23,10.03,1.88L10,1.9L9.89,1.8C8.73,0.89,7.26,0.45,5.77,0.55M13.81,2.61C16.99,2.79,18.64,6.45,16.66,8.93C14.89,11.16,11.38,10.77,10.12,8.21C8.9,5.71,10.62,2.76,13.41,2.61C13.62,2.6,13.62,2.6,13.81,2.61M6.54,3.77C8.56,4.09,9.57,6.34,8.44,8.04C7.42,9.57,5.15,9.66,4.01,8.22C2.64,6.49,3.71,3.98,5.93,3.76C6.02,3.75,6.44,3.76,6.54,3.77 + M5.84,5.67C4.95,6.06,4.22,6.39,4.22,6.39C4.21,6.39,4.94,6.72,5.83,7.12C6.72,7.52,7.46,7.84,7.46,7.84C7.46,7.83,7.34,7.59,7.2,7.3C7.05,7.01,6.93,6.77,6.93,6.76C6.94,6.75,6.77,6.76,11.2,6.59C13.46,6.51,15.35,6.44,15.4,6.43L15.5,6.43L15.5,6.39C15.5,6.38,15.5,6.36,15.49,6.36C15.49,6.36,13.56,6.28,11.21,6.19C8.86,6.1,6.93,6.03,6.93,6.02C6.93,6.02,7.05,5.78,7.2,5.49C7.47,4.95,7.46,4.95,7.45,4.95C7.45,4.95,6.72,5.27,5.84,5.67 + M5.77,0.55C1.84,0.82,-0.75,4.85,0.62,8.55C2.02,12.32,6.68,13.66,9.86,11.2C9.98,11.1,10,11.09,10.02,11.11C10.05,11.18,10.63,11.57,10.91,11.72C14.42,13.64,18.8,11.65,19.63,7.76C20.48,3.76,17.17,0.11,13.11,0.57C12,0.7,10.75,1.23,10.03,1.88L10,1.9L9.89,1.8C8.73,0.89,7.26,0.45,5.77,0.55M14.02,1.55C17.58,1.72,19.81,5.45,18.27,8.65C16.79,11.69,12.8,12.41,10.36,10.07C10.26,9.97,10.22,9.95,10.1,9.87C9.87,9.74,9.64,9.57,9.45,9.41L9.41,9.36L9.27,9.49C8.14,10.64,6.44,11.05,4.92,10.55C2.35,9.7,1.21,6.72,2.55,4.37C3.55,2.63,5.65,1.83,7.55,2.45C8.21,2.66,8.78,3.01,9.27,3.5L9.41,3.64L9.45,3.59C9.64,3.43,9.92,3.23,10.13,3.11C10.21,3.07,10.26,3.03,10.34,2.95C11.23,2.09,12.34,1.61,13.55,1.55C13.79,1.54,13.8,1.54,14.02,1.55 + + + M21.797,3.984C21.613,4.031 21.434,4.176 21.352,4.34C21.27,4.512 21.273,4.363 21.273,6.094L21.273,7.637L21.297,7.715C21.41,8.109 21.816,8.316 22.203,8.172C22.324,8.125 22.465,8.008 22.531,7.887C22.555,7.855 22.582,7.789 22.598,7.746L22.625,7.664L22.625,4.523L22.602,4.457C22.535,4.254 22.418,4.121 22.234,4.035C22.188,4.012 22.117,3.988 22.086,3.98C22.004,3.965 21.867,3.969 21.797,3.984ZM21.797,3.984M3.578,1C2.93,1.063 2.328,1.348 1.879,1.809C1.742,1.945 1.715,1.977 1.633,2.086C1.34,2.473 1.152,2.945 1.094,3.43C1.074,3.617 1.074,8.383 1.094,8.57C1.145,8.98 1.285,9.383 1.504,9.73C1.621,9.914 1.703,10.012 1.879,10.191C2.309,10.633 2.867,10.906 3.504,10.992C3.59,11.004 4.836,11.004 10.656,11.004L17.703,11.004L17.852,10.98C18.461,10.887 18.973,10.629 19.402,10.199C19.809,9.793 20.066,9.297 20.168,8.73C20.211,8.5 20.211,8.449 20.207,5.91L20.203,3.477L20.184,3.34C20.082,2.73 19.824,2.223 19.402,1.801C19.164,1.563 18.922,1.391 18.625,1.254C18.379,1.137 18.156,1.07 17.859,1.02L17.727,0.996L10.695,0.992C6.828,0.992 3.625,0.996 3.578,1ZM17.223,2.266C17.527,2.301 17.789,2.398 18.059,2.578C18.148,2.637 18.211,2.688 18.32,2.801C18.492,2.973 18.574,3.082 18.672,3.281C18.785,3.504 18.848,3.715 18.871,3.953C18.883,4.105 18.883,7.91 18.871,8.051C18.844,8.289 18.781,8.504 18.672,8.719C18.582,8.902 18.496,9.023 18.355,9.168C18.09,9.441 17.813,9.605 17.449,9.695C17.215,9.754 17.738,9.75 10.648,9.75C6.176,9.75 4.137,9.746 4.078,9.738C3.371,9.652 2.793,9.207 2.535,8.555C2.488,8.434 2.445,8.254 2.426,8.113C2.402,7.941 2.402,4.059 2.426,3.887C2.539,3.027 3.227,2.352 4.082,2.262C4.129,2.258 7.004,2.254 10.629,2.25C16.266,2.25 17.109,2.254 17.223,2.266ZM17.223,2.266M4.637,3.199C4.426,3.219 4.211,3.285 4.047,3.387C3.656,3.621 3.406,4.012 3.359,4.461C3.355,4.516 3.352,4.777 3.352,5.047L3.352,5.531L5.59,5.531L5.59,4.371L7.391,4.371L7.391,5.531L10.645,5.527L13.895,5.523L13.898,4.922L13.898,4.32L15.691,4.32L15.691,4.922L15.695,5.523L17.934,5.523L17.938,5.102C17.941,4.66 17.934,4.438 17.902,4.309C17.816,3.895 17.531,3.527 17.152,3.34C17.043,3.285 16.891,3.234 16.77,3.215C16.68,3.195 16.516,3.195 14.109,3.191L11.539,3.191L11.539,4.371L9.738,4.371L9.738,3.191L7.227,3.191C5.848,3.191 4.68,3.195 4.637,3.199ZM4.637,3.199 + M21.797,3.984C21.613,4.031 21.434,4.176 21.352,4.34C21.27,4.512 21.273,4.363 21.273,6.094L21.273,7.637L21.297,7.715C21.41,8.109 21.816,8.316 22.203,8.172C22.324,8.125 22.465,8.008 22.531,7.887C22.555,7.855 22.582,7.789 22.598,7.746L22.625,7.664L22.625,4.523L22.602,4.457C22.535,4.254 22.418,4.121 22.234,4.035C22.188,4.012 22.117,3.988 22.086,3.98C22.004,3.965 21.867,3.969 21.797,3.984ZM21.797,3.984M3.578,1C2.93,1.063 2.328,1.348 1.879,1.809C1.742,1.945 1.715,1.977 1.633,2.086C1.34,2.473 1.152,2.945 1.094,3.43C1.074,3.617 1.074,8.383 1.094,8.57C1.145,8.98 1.285,9.383 1.504,9.73C1.621,9.914 1.703,10.012 1.879,10.191C2.309,10.633 2.867,10.906 3.504,10.992C3.59,11.004 4.836,11.004 10.656,11.004L17.703,11.004L17.852,10.98C18.461,10.887 18.973,10.629 19.402,10.199C19.809,9.793 20.066,9.297 20.168,8.73C20.211,8.5 20.211,8.449 20.207,5.91L20.203,3.477L20.184,3.34C20.082,2.73 19.824,2.223 19.402,1.801C19.164,1.563 18.922,1.391 18.625,1.254C18.379,1.137 18.156,1.07 17.859,1.02L17.727,0.996L10.695,0.992C6.828,0.992 3.625,0.996 3.578,1ZM17.223,2.266C17.527,2.301 17.789,2.398 18.059,2.578C18.148,2.637 18.211,2.688 18.32,2.801C18.492,2.973 18.574,3.082 18.672,3.281C18.785,3.504 18.848,3.715 18.871,3.953C18.883,4.105 18.883,7.91 18.871,8.051C18.844,8.289 18.781,8.504 18.672,8.719C18.582,8.902 18.496,9.023 18.355,9.168C18.09,9.441 17.813,9.605 17.449,9.695C17.215,9.754 17.738,9.75 10.648,9.75C6.176,9.75 4.137,9.746 4.078,9.738C3.371,9.652 2.793,9.207 2.535,8.555C2.488,8.434 2.445,8.254 2.426,8.113C2.402,7.941 2.402,4.059 2.426,3.887C2.539,3.027 3.227,2.352 4.082,2.262C4.129,2.258 7.004,2.254 10.629,2.25C16.266,2.25 17.109,2.254 17.223,2.266ZM17.223,2.266 + M3.352,6.945C3.352,7.211 3.355,7.473 3.359,7.535C3.402,7.98 3.652,8.379 4.047,8.613C4.223,8.719 4.43,8.781 4.656,8.801C4.746,8.809 6.633,8.809 10.73,8.809C16.336,8.805 16.68,8.805 16.77,8.785C17.277,8.691 17.695,8.336 17.859,7.855C17.93,7.645 17.941,7.492 17.938,6.898L17.934,6.477L10.645,6.473L3.352,6.469ZM3.352,6.945 + M12.176,2.867C12.152,2.879 12.121,2.902 12.109,2.918C12.094,2.934 11.785,3.441 11.422,4.047C11.055,4.652 10.754,5.148 10.75,5.148C10.746,5.148 10.656,4.93 10.547,4.66C10.426,4.363 10.336,4.156 10.316,4.137C10.281,4.09 10.215,4.059 10.148,4.059C10.023,4.063 9.93,4.152 9.93,4.277C9.93,4.305 9.977,4.555 10.035,4.836C10.094,5.113 10.141,5.344 10.137,5.348C10.137,5.348 9.613,5.098 8.977,4.785C8.336,4.477 7.801,4.219 7.781,4.211C7.676,4.176 7.566,4.223 7.516,4.324C7.477,4.402 7.484,4.496 7.539,4.563C7.563,4.59 7.906,4.801 8.688,5.273C9.305,5.641 9.805,5.949 9.801,5.949C9.797,5.953 9.57,6.039 9.297,6.141C9.02,6.238 8.777,6.332 8.758,6.344C8.609,6.426 8.605,6.629 8.75,6.727C8.816,6.773 8.828,6.77 9.422,6.645C9.727,6.582 9.977,6.531 9.977,6.535C9.98,6.535 9.73,7.055 9.422,7.688C9.113,8.32 8.855,8.859 8.852,8.879C8.824,8.973 8.871,9.074 8.961,9.121C9.051,9.172 9.18,9.152 9.234,9.074C9.246,9.059 9.555,8.551 9.918,7.949C10.281,7.348 10.586,6.852 10.59,6.848C10.594,6.844 10.676,7.051 10.77,7.309C10.965,7.836 10.973,7.855 11.004,7.887C11.086,7.977 11.238,7.977 11.32,7.895C11.363,7.852 11.391,7.785 11.391,7.734C11.391,7.719 11.344,7.477 11.285,7.199C11.23,6.922 11.184,6.695 11.184,6.691C11.188,6.691 11.695,6.93 12.316,7.223C12.938,7.512 13.465,7.762 13.488,7.77C13.594,7.816 13.719,7.77 13.773,7.668C13.816,7.586 13.793,7.465 13.727,7.402C13.715,7.391 13.219,7.086 12.621,6.727C12.023,6.363 11.527,6.063 11.523,6.055C11.512,6.047 11.609,6.012 11.988,5.879C12.25,5.789 12.477,5.711 12.492,5.703C12.504,5.695 12.531,5.672 12.551,5.652C12.676,5.512 12.574,5.285 12.387,5.289C12.359,5.289 12.113,5.336 11.844,5.391C11.574,5.445 11.355,5.488 11.352,5.484C11.352,5.484 11.605,4.953 11.922,4.305C12.465,3.184 12.492,3.125 12.492,3.07C12.492,3.012 12.473,2.953 12.43,2.91C12.375,2.852 12.254,2.832 12.176,2.867ZM12.176,2.867 + M4.637,3.199C4.426,3.219 4.211,3.285 4.047,3.387C3.656,3.621 3.406,4.012 3.359,4.461C3.355,4.516 3.352,4.777 3.352,5.047L3.352,5.531L5.59,5.531L5.59,4.371L7.391,4.371L7.391,5.531L10.645,5.527L13.895,5.523L13.898,4.922L13.898,4.32L15.691,4.32L15.691,4.922L15.695,5.523L17.934,5.523L17.938,5.102C17.941,4.66 17.934,4.438 17.902,4.309C17.816,3.895 17.531,3.527 17.152,3.34C17.043,3.285 16.891,3.234 16.77,3.215C16.68,3.195 16.516,3.195 14.109,3.191L11.539,3.191L11.539,4.371L9.738,4.371L9.738,3.191L7.227,3.191C5.848,3.191 4.68,3.195 4.637,3.199ZM4.637,3.199 + + + M4.594,0.09C3.738,0.16 2.895,0.488 2.156,1.039C1.227,1.73 0.586,2.672 0.379,3.648C0.344,3.828 0.324,3.961 0.301,4.266C0.18,5.754 0.18,7.258 0.301,8.746C0.34,9.223 0.387,9.465 0.508,9.813C1.02,11.301 2.555,12.574 4.172,12.863C4.543,12.93 4.188,12.926 8.559,12.926C12.332,12.926 12.523,12.926 12.672,12.906C13.512,12.809 14.297,12.48 15.016,11.934C15.219,11.781 15.387,11.633 15.582,11.43C15.938,11.066 16.203,10.699 16.422,10.273C16.465,10.188 16.5,10.117 16.504,10.109C16.504,10.102 16.574,10.09 16.68,10.074C17.336,9.984 17.43,9.969 17.637,9.898C18.574,9.586 19.324,8.699 19.617,7.547C19.711,7.184 19.742,6.91 19.742,6.504C19.746,6.102 19.715,5.848 19.625,5.484C19.348,4.352 18.633,3.477 17.715,3.129C17.5,3.051 17.367,3.02 16.887,2.957C16.699,2.93 16.535,2.906 16.523,2.906C16.508,2.902 16.484,2.863 16.43,2.758C16.117,2.129 15.613,1.531 15.016,1.074C14.246,0.492 13.395,0.156 12.512,0.09C12.348,0.078 4.746,0.078 4.594,0.09ZM12.559,1.684C13.262,1.789 13.906,2.133 14.445,2.68C14.871,3.117 15.133,3.594 15.215,4.074C15.23,4.18 15.262,4.547 15.285,4.898C15.355,6.055 15.352,7.117 15.273,8.27C15.254,8.602 15.223,8.902 15.203,8.996C15.109,9.457 14.855,9.906 14.453,10.32C13.91,10.879 13.242,11.23 12.547,11.328C12.422,11.344 12.211,11.344 8.57,11.344C5.465,11.344 4.703,11.344 4.621,11.332C3.926,11.246 3.25,10.902 2.703,10.355C2.395,10.043 2.176,9.727 2.035,9.383C1.941,9.148 1.91,8.996 1.879,8.637C1.766,7.258 1.766,5.754 1.879,4.375C1.91,4.016 1.941,3.863 2.035,3.629C2.176,3.285 2.395,2.965 2.703,2.656C3.297,2.066 4.008,1.723 4.762,1.668C4.785,1.664 6.523,1.664 8.625,1.664C12.207,1.664 12.453,1.668 12.559,1.684ZM17.031,4.578C17.57,4.684 18.035,5.344 18.145,6.172C18.184,6.438 18.164,6.793 18.105,7.07C17.953,7.75 17.547,8.285 17.086,8.41C17.016,8.43 16.859,8.453 16.852,8.445C16.852,8.445 16.855,8.344 16.863,8.219C16.938,7.113 16.938,5.895 16.863,4.797C16.855,4.676 16.852,4.57 16.852,4.563C16.852,4.547 16.902,4.551 17.031,4.578ZM17.031,4.578M5.074,2.586C4.289,2.664 3.543,3.113 3.094,3.785C2.961,3.984 2.828,4.289 2.781,4.512C2.727,4.773 2.719,4.949 2.762,5.086C2.836,5.328 3.012,5.504 3.262,5.582C3.367,5.613 3.555,5.613 3.66,5.582C3.961,5.488 4.156,5.246 4.191,4.934C4.195,4.879 4.207,4.813 4.219,4.785C4.313,4.492 4.637,4.199 4.984,4.086C5.027,4.074 5.074,4.059 5.09,4.055C5.105,4.051 5.211,4.047 5.328,4.039C5.508,4.031 5.559,4.027 5.613,4.012C5.859,3.934 6.035,3.758 6.109,3.512C6.145,3.402 6.145,3.219 6.109,3.109C6.035,2.859 5.863,2.688 5.613,2.609C5.555,2.59 5.523,2.586 5.328,2.586C5.211,2.582 5.098,2.586 5.074,2.586ZM5.074,2.586 + M4.594,0.09C3.738,0.16 2.895,0.488 2.156,1.039C1.227,1.73 0.586,2.672 0.379,3.648C0.344,3.828 0.324,3.961 0.301,4.266C0.18,5.754 0.18,7.258 0.301,8.746C0.34,9.223 0.387,9.465 0.508,9.813C1.02,11.301 2.555,12.574 4.172,12.863C4.543,12.93 4.188,12.926 8.559,12.926C12.332,12.926 12.523,12.926 12.672,12.906C13.512,12.809 14.297,12.48 15.016,11.934C15.219,11.781 15.387,11.633 15.582,11.43C15.938,11.066 16.203,10.699 16.422,10.273C16.465,10.188 16.5,10.117 16.504,10.109C16.504,10.102 16.574,10.09 16.68,10.074C17.336,9.984 17.43,9.969 17.637,9.898C18.574,9.586 19.324,8.699 19.617,7.547C19.711,7.184 19.742,6.91 19.742,6.504C19.746,6.102 19.715,5.848 19.625,5.484C19.348,4.352 18.633,3.477 17.715,3.129C17.5,3.051 17.367,3.02 16.887,2.957C16.699,2.93 16.535,2.906 16.523,2.906C16.508,2.902 16.484,2.863 16.43,2.758C16.117,2.129 15.613,1.531 15.016,1.074C14.246,0.492 13.395,0.156 12.512,0.09C12.348,0.078 4.746,0.078 4.594,0.09ZM12.559,1.684C13.262,1.789 13.906,2.133 14.445,2.68C14.871,3.117 15.133,3.594 15.215,4.074C15.23,4.18 15.262,4.547 15.285,4.898C15.355,6.055 15.352,7.117 15.273,8.27C15.254,8.602 15.223,8.902 15.203,8.996C15.109,9.457 14.855,9.906 14.453,10.32C13.91,10.879 13.242,11.23 12.547,11.328C12.422,11.344 12.211,11.344 8.57,11.344C5.465,11.344 4.703,11.344 4.621,11.332C3.926,11.246 3.25,10.902 2.703,10.355C2.395,10.043 2.176,9.727 2.035,9.383C1.941,9.148 1.91,8.996 1.879,8.637C1.766,7.258 1.766,5.754 1.879,4.375C1.91,4.016 1.941,3.863 2.035,3.629C2.176,3.285 2.395,2.965 2.703,2.656C3.297,2.066 4.008,1.723 4.762,1.668C4.785,1.664 6.523,1.664 8.625,1.664C12.207,1.664 12.453,1.668 12.559,1.684ZM17.031,4.578C17.57,4.684 18.035,5.344 18.145,6.172C18.184,6.438 18.164,6.793 18.105,7.07C17.953,7.75 17.547,8.285 17.086,8.41C17.016,8.43 16.859,8.453 16.852,8.445C16.852,8.445 16.855,8.344 16.863,8.219C16.938,7.113 16.938,5.895 16.863,4.797C16.855,4.676 16.852,4.57 16.852,4.563C16.852,4.547 16.902,4.551 17.031,4.578ZM17.031,4.578M5.074,2.586C4.289,2.664 3.543,3.113 3.094,3.785C2.961,3.984 2.828,4.289 2.781,4.512C2.727,4.773 2.719,4.949 2.762,5.086C2.836,5.328 3.012,5.504 3.262,5.582C3.367,5.613 3.555,5.613 3.66,5.582C3.961,5.488 4.156,5.246 4.191,4.934C4.195,4.879 4.207,4.813 4.219,4.785C4.313,4.492 4.637,4.199 4.984,4.086C5.027,4.074 5.074,4.059 5.09,4.055C5.105,4.051 5.211,4.047 5.328,4.039C5.508,4.031 5.559,4.027 5.613,4.012C5.859,3.934 6.035,3.758 6.109,3.512C6.145,3.402 6.145,3.219 6.109,3.109C6.035,2.859 5.863,2.688 5.613,2.609C5.555,2.59 5.523,2.586 5.328,2.586C5.211,2.582 5.098,2.586 5.074,2.586ZM5.074,2.586 + M4.969,2.074C4.652,2.105 4.422,2.16 4.16,2.262C3.449,2.539 2.844,3.082 2.527,3.727C2.379,4.023 2.32,4.242 2.281,4.66C2.156,5.918 2.16,7.219 2.289,8.434C2.34,8.887 2.449,9.207 2.695,9.586C2.875,9.855 3.176,10.164 3.453,10.363C3.867,10.66 4.309,10.84 4.801,10.918C4.91,10.934 5.113,10.934 8.523,10.934C11.953,10.934 12.141,10.934 12.25,10.918C12.703,10.848 13.105,10.691 13.492,10.434C14.012,10.09 14.422,9.59 14.617,9.055C14.699,8.836 14.73,8.699 14.758,8.426C14.895,7.168 14.895,5.84 14.758,4.586C14.719,4.215 14.656,3.996 14.496,3.684C14.328,3.355 14.117,3.09 13.82,2.828C13.367,2.43 12.836,2.184 12.227,2.09C12.16,2.078 11.371,2.074 8.574,2.074C6.613,2.074 4.988,2.074 4.969,2.074ZM4.969,2.074 + M0,6.5L0,13L20,13L20,0L0,0ZM12.227,2.09C12.836,2.184 13.367,2.43 13.82,2.828C14.117,3.09 14.328,3.355 14.496,3.684C14.656,3.996 14.719,4.215 14.758,4.586C14.863,5.582 14.887,6.637 14.824,7.637C14.793,8.117 14.75,8.555 14.719,8.707C14.617,9.211 14.324,9.715 13.91,10.102C13.434,10.547 12.879,10.816 12.25,10.918C12.141,10.934 11.953,10.934 8.523,10.934C5.113,10.934 4.91,10.934 4.801,10.918C4.125,10.813 3.523,10.5 3.035,10C2.664,9.613 2.426,9.188 2.332,8.723C2.305,8.602 2.277,8.355 2.25,8.004C2.16,6.875 2.172,5.785 2.281,4.66C2.305,4.418 2.32,4.316 2.359,4.164C2.57,3.355 3.281,2.605 4.16,2.262C4.422,2.16 4.652,2.105 4.969,2.074C4.988,2.074 6.613,2.074 8.574,2.074C11.371,2.074 12.16,2.078 12.227,2.09ZM12.227,2.09M5.078,2.586C4.848,2.613 4.637,2.66 4.461,2.723C3.902,2.926 3.418,3.305 3.094,3.785C3.008,3.914 2.883,4.176 2.836,4.316C2.797,4.434 2.758,4.609 2.738,4.754C2.688,5.141 2.895,5.469 3.262,5.582C3.367,5.613 3.551,5.613 3.66,5.582C3.961,5.488 4.156,5.246 4.191,4.938C4.199,4.832 4.223,4.758 4.258,4.684C4.395,4.414 4.699,4.168 5.023,4.074C5.105,4.051 5.141,4.047 5.328,4.039C5.512,4.035 5.555,4.027 5.613,4.012C5.863,3.934 6.031,3.758 6.113,3.512C6.145,3.402 6.145,3.219 6.113,3.109C6.031,2.859 5.863,2.691 5.613,2.609C5.551,2.59 5.523,2.586 5.328,2.586C5.211,2.582 5.098,2.586 5.078,2.586ZM5.078,2.586 + M8.434,2.73C8.219,2.766 8.016,2.883 7.895,3.047C7.809,3.156 7.816,3.141 7.547,3.805C7.387,4.199 7.309,4.383 7.238,4.555C7.055,5.012 6.965,5.23 6.863,5.477C6.586,6.156 6.59,6.148 6.582,6.367C6.578,6.484 6.582,6.535 6.594,6.602C6.68,7.016 6.949,7.313 7.344,7.414L7.445,7.441L8.129,7.441C8.672,7.441 8.809,7.441 8.809,7.453C8.809,7.461 8.797,7.516 8.781,7.574C8.766,7.637 8.719,7.813 8.68,7.965C8.641,8.117 8.59,8.313 8.57,8.395C8.551,8.477 8.504,8.652 8.469,8.785C8.438,8.918 8.395,9.074 8.379,9.137C8.32,9.371 8.316,9.535 8.367,9.699C8.441,9.922 8.586,10.09 8.797,10.191C8.922,10.254 9.023,10.273 9.16,10.273C9.512,10.27 9.797,10.063 9.941,9.707C10.004,9.555 10.102,9.313 10.164,9.156C10.258,8.93 10.328,8.754 10.387,8.621C10.41,8.559 10.461,8.434 10.496,8.348C10.531,8.262 10.59,8.117 10.625,8.031C10.66,7.945 10.711,7.824 10.734,7.762C11.09,6.895 11.082,6.914 11.105,6.766C11.16,6.426 11.051,6.082 10.813,5.848C10.691,5.727 10.602,5.672 10.426,5.605C10.227,5.531 10.223,5.531 9.516,5.531C8.922,5.531 8.891,5.527 8.895,5.512C8.895,5.504 8.918,5.422 8.938,5.336C8.961,5.246 9.008,5.07 9.039,4.941C9.074,4.813 9.117,4.637 9.141,4.551C9.16,4.465 9.215,4.254 9.262,4.086C9.352,3.727 9.375,3.598 9.363,3.488C9.324,3.066 9.004,2.75 8.586,2.727C8.535,2.723 8.469,2.727 8.434,2.73ZM8.434,2.73 + M4.594,0.09C3.738,0.16 2.895,0.488 2.156,1.039C1.227,1.73 0.586,2.672 0.379,3.648C0.344,3.828 0.324,3.961 0.301,4.266C0.18,5.754 0.18,7.258 0.301,8.746C0.34,9.223 0.387,9.465 0.508,9.813C1.02,11.301 2.555,12.574 4.172,12.863C4.543,12.93 4.188,12.926 8.559,12.926C12.332,12.926 12.523,12.926 12.672,12.906C13.512,12.809 14.297,12.48 15.016,11.934C15.219,11.781 15.387,11.633 15.582,11.43C15.938,11.066 16.203,10.699 16.422,10.273C16.465,10.188 16.5,10.117 16.504,10.109C16.504,10.102 16.574,10.09 16.68,10.074C17.336,9.984 17.43,9.969 17.637,9.898C18.574,9.586 19.324,8.699 19.617,7.547C19.711,7.184 19.742,6.91 19.742,6.504C19.746,6.102 19.715,5.848 19.625,5.484C19.348,4.352 18.633,3.477 17.715,3.129C17.5,3.051 17.367,3.02 16.887,2.957C16.699,2.93 16.535,2.906 16.523,2.906C16.508,2.902 16.484,2.863 16.43,2.758C16.117,2.129 15.613,1.531 15.016,1.074C14.246,0.492 13.395,0.156 12.512,0.09C12.348,0.078 4.746,0.078 4.594,0.09ZM12.559,1.684C13.262,1.789 13.906,2.133 14.445,2.68C14.871,3.117 15.133,3.594 15.215,4.074C15.23,4.18 15.262,4.547 15.285,4.898C15.355,6.055 15.352,7.117 15.273,8.27C15.254,8.602 15.223,8.902 15.203,8.996C15.109,9.457 14.855,9.906 14.453,10.32C13.91,10.879 13.242,11.23 12.547,11.328C12.422,11.344 12.211,11.344 8.57,11.344C5.465,11.344 4.703,11.344 4.621,11.332C3.926,11.246 3.25,10.902 2.703,10.355C2.395,10.043 2.176,9.727 2.035,9.383C1.941,9.148 1.91,8.996 1.879,8.637C1.766,7.258 1.766,5.754 1.879,4.375C1.91,4.016 1.941,3.863 2.035,3.629C2.176,3.285 2.395,2.965 2.703,2.656C3.297,2.066 4.008,1.723 4.762,1.668C4.785,1.664 6.523,1.664 8.625,1.664C12.207,1.664 12.453,1.668 12.559,1.684ZM17.031,4.578C17.57,4.684 18.035,5.344 18.145,6.172C18.184,6.438 18.164,6.793 18.105,7.07C17.953,7.75 17.547,8.285 17.086,8.41C17.016,8.43 16.859,8.453 16.852,8.445C16.852,8.445 16.855,8.344 16.863,8.219C16.938,7.113 16.938,5.895 16.863,4.797C16.855,4.676 16.852,4.57 16.852,4.563C16.852,4.547 16.902,4.551 17.031,4.578ZM17.031,4.578M5.074,2.586C4.289,2.664 3.543,3.113 3.094,3.785C2.961,3.984 2.828,4.289 2.781,4.512C2.727,4.773 2.719,4.949 2.762,5.086C2.836,5.328 3.012,5.504 3.262,5.582C3.367,5.613 3.555,5.613 3.66,5.582C3.961,5.488 4.156,5.246 4.191,4.934C4.195,4.879 4.207,4.813 4.219,4.785C4.313,4.492 4.637,4.199 4.984,4.086C5.027,4.074 5.074,4.059 5.09,4.055C5.105,4.051 5.211,4.047 5.328,4.039C5.508,4.031 5.559,4.027 5.613,4.012C5.859,3.934 6.035,3.758 6.109,3.512C6.145,3.402 6.145,3.219 6.109,3.109C6.035,2.859 5.863,2.688 5.613,2.609C5.555,2.59 5.523,2.586 5.328,2.586C5.211,2.582 5.098,2.586 5.074,2.586ZM5.074,2.586 + + + M22.34,4.387C22.145,4.422 21.992,4.527 21.906,4.684C21.836,4.824 21.84,4.75 21.84,6.012C21.84,7.27 21.836,7.199 21.906,7.336C21.977,7.465 22.109,7.574 22.254,7.617C22.336,7.641 22.492,7.641 22.574,7.617C22.719,7.574 22.855,7.465 22.922,7.336C22.992,7.199 22.988,7.262 22.988,6.008C22.988,5.008 22.988,4.871 22.973,4.816C22.926,4.625 22.773,4.465 22.586,4.41C22.523,4.391 22.387,4.379 22.34,4.387ZM22.34,4.387M3.59,1.168C3.273,1.176 2.977,1.238 2.68,1.355C2.469,1.441 2.262,1.559 2.07,1.699C1.945,1.789 1.68,2.047 1.59,2.164C1.348,2.469 1.184,2.801 1.09,3.176C1.008,3.496 1.016,3.266 1.016,6C1.016,8.734 1.008,8.504 1.09,8.824C1.211,9.289 1.426,9.68 1.762,10.023C1.984,10.258 2.211,10.422 2.492,10.559C2.77,10.695 3.027,10.773 3.352,10.816C3.453,10.832 4.102,10.836 10.91,10.84C15.902,10.84 18.426,10.84 18.57,10.832C18.852,10.816 19.016,10.789 19.27,10.715C19.469,10.652 19.746,10.523 19.926,10.406C20.516,10.02 20.938,9.406 21.078,8.719C21.137,8.438 21.137,8.594 21.137,6C21.137,3.773 21.133,3.617 21.117,3.5C21.031,2.91 20.816,2.445 20.422,2.016C20.012,1.566 19.441,1.273 18.801,1.184C18.695,1.168 18.051,1.164 11.18,1.164C7.051,1.164 3.637,1.164 3.59,1.168ZM18.457,2.332C18.734,2.359 19,2.457 19.23,2.609C19.566,2.828 19.824,3.199 19.93,3.605C19.98,3.813 19.98,3.754 19.977,6.055C19.973,8.344 19.977,8.215 19.926,8.41C19.848,8.715 19.68,8.996 19.457,9.203C19.262,9.391 19.07,9.504 18.824,9.59C18.68,9.637 18.59,9.656 18.434,9.672C18.352,9.68 16.078,9.68 10.984,9.676C4.309,9.676 3.648,9.672 3.578,9.656C3.184,9.574 2.918,9.438 2.664,9.184C2.426,8.945 2.266,8.656 2.199,8.336C2.16,8.141 2.16,8.016 2.164,5.887C2.168,3.695 2.164,3.789 2.215,3.605C2.328,3.164 2.598,2.805 2.977,2.57C3.168,2.449 3.441,2.355 3.672,2.332C3.785,2.316 18.34,2.316 18.457,2.332ZM18.457,2.332 + M22.34,4.387C22.145,4.422 21.992,4.527 21.906,4.684C21.836,4.824 21.84,4.75 21.84,6.012C21.84,7.27 21.836,7.199 21.906,7.336C21.977,7.465 22.109,7.574 22.254,7.617C22.336,7.641 22.492,7.641 22.574,7.617C22.719,7.574 22.855,7.465 22.922,7.336C22.992,7.199 22.988,7.262 22.988,6.008C22.988,5.008 22.988,4.871 22.973,4.816C22.926,4.625 22.773,4.465 22.586,4.41C22.523,4.391 22.387,4.379 22.34,4.387ZM22.34,4.387M3.59,1.168C3.273,1.176 2.977,1.238 2.68,1.355C2.469,1.441 2.262,1.559 2.07,1.699C1.945,1.789 1.68,2.047 1.59,2.164C1.348,2.469 1.184,2.801 1.09,3.176C1.008,3.496 1.016,3.266 1.016,6C1.016,8.734 1.008,8.504 1.09,8.824C1.211,9.289 1.426,9.68 1.762,10.023C1.984,10.258 2.211,10.422 2.492,10.559C2.77,10.695 3.027,10.773 3.352,10.816C3.453,10.832 4.102,10.836 10.91,10.84C15.902,10.84 18.426,10.84 18.57,10.832C18.852,10.816 19.016,10.789 19.27,10.715C19.469,10.652 19.746,10.523 19.926,10.406C20.516,10.02 20.938,9.406 21.078,8.719C21.137,8.438 21.137,8.594 21.137,6C21.137,3.773 21.133,3.617 21.117,3.5C21.031,2.91 20.816,2.445 20.422,2.016C20.012,1.566 19.441,1.273 18.801,1.184C18.695,1.168 18.051,1.164 11.18,1.164C7.051,1.164 3.637,1.164 3.59,1.168ZM18.457,2.332C18.734,2.359 19,2.457 19.23,2.609C19.566,2.828 19.824,3.199 19.93,3.605C19.98,3.813 19.98,3.754 19.977,6.055C19.973,8.344 19.977,8.215 19.926,8.41C19.848,8.715 19.68,8.996 19.457,9.203C19.262,9.391 19.07,9.504 18.824,9.59C18.68,9.637 18.59,9.656 18.434,9.672C18.352,9.68 16.078,9.68 10.984,9.676C4.309,9.676 3.648,9.672 3.578,9.656C3.184,9.574 2.918,9.438 2.664,9.184C2.426,8.945 2.266,8.656 2.199,8.336C2.16,8.141 2.16,8.016 2.164,5.887C2.168,3.695 2.164,3.789 2.215,3.605C2.328,3.164 2.598,2.805 2.977,2.57C3.168,2.449 3.441,2.355 3.672,2.332C3.785,2.316 18.34,2.316 18.457,2.332ZM18.457,2.332 + M3.926,3.23C3.801,3.258 3.719,3.285 3.617,3.34C3.305,3.5 3.098,3.813 3.059,4.172C3.047,4.309 3.047,7.676 3.059,7.82C3.094,8.168 3.285,8.473 3.582,8.641C3.711,8.719 3.832,8.758 3.992,8.777C4.105,8.797 18.031,8.793 18.156,8.777C18.559,8.73 18.902,8.453 19.031,8.074C19.094,7.891 19.09,7.977 19.09,6C19.09,4.055 19.09,4.102 19.039,3.945C18.91,3.563 18.598,3.297 18.191,3.227C18.113,3.211 17.344,3.211 11.059,3.211C4.195,3.211 4.012,3.211 3.926,3.23ZM3.926,3.23 + M12.277,1.238C12.258,1.258 12.094,1.445 11.914,1.656C11.734,1.863 11.477,2.164 11.34,2.32C11.203,2.477 10.898,2.828 10.66,3.105C10.422,3.379 10.117,3.73 9.984,3.887C9.578,4.355 9.406,4.551 9.09,4.922C8.922,5.113 8.746,5.316 8.699,5.375C8.648,5.43 8.609,5.477 8.609,5.484C8.609,5.488 8.664,5.488 8.727,5.484C8.895,5.477 10.559,5.469 10.559,5.473C10.559,5.477 10.516,5.535 10.461,5.602C10.406,5.672 10.305,5.801 10.23,5.895C10.156,5.988 10.023,6.156 9.934,6.266C9.785,6.453 9.664,6.609 9.359,6.988C9.297,7.07 9.203,7.188 9.156,7.25C9.105,7.313 8.992,7.453 8.91,7.559C8.824,7.668 8.703,7.816 8.645,7.895C8.582,7.973 8.48,8.098 8.422,8.176C8.359,8.254 8.309,8.316 8.309,8.32C8.309,8.328 8.816,8.328 9.441,8.328L10.57,8.328L10.539,8.41C10.523,8.457 10.379,8.898 10.215,9.395C10.051,9.891 9.883,10.402 9.84,10.531C9.797,10.664 9.762,10.777 9.762,10.785C9.762,10.793 9.773,10.785 9.793,10.762C9.809,10.742 9.836,10.711 9.852,10.695C9.867,10.68 9.973,10.555 10.09,10.422C10.207,10.285 10.406,10.059 10.531,9.914C11.066,9.301 11.258,9.078 11.305,9.023C11.379,8.941 11.949,8.285 12.23,7.965C12.359,7.816 12.609,7.527 12.789,7.32C12.969,7.113 13.223,6.82 13.355,6.672L13.598,6.395L12.715,6.391C12.23,6.387 11.836,6.383 11.832,6.383C11.832,6.379 12.25,5.797 12.387,5.613C12.414,5.574 12.469,5.5 12.512,5.441C12.594,5.324 12.754,5.105 12.852,4.973C12.887,4.926 12.988,4.777 13.086,4.648C13.176,4.52 13.27,4.391 13.289,4.363C13.324,4.316 13.359,4.27 13.566,3.984C13.68,3.824 13.719,3.773 13.793,3.672L13.844,3.605L12.836,3.602L11.828,3.602L11.836,3.563C11.84,3.543 11.855,3.465 11.871,3.391C11.887,3.316 11.969,2.922 12.051,2.516C12.133,2.109 12.211,1.738 12.219,1.691C12.23,1.645 12.246,1.563 12.254,1.516C12.266,1.465 12.281,1.375 12.297,1.316C12.309,1.258 12.32,1.207 12.316,1.203C12.316,1.203 12.297,1.219 12.277,1.238ZM12.277,1.238 + M10.992,3.691C10.879,3.715 10.762,3.797 10.707,3.895C10.652,3.992 10.656,3.949 10.648,4.789L10.645,5.574L9.859,5.578C8.984,5.586 9.043,5.582 8.934,5.656C8.691,5.82 8.699,6.199 8.949,6.355C9.051,6.418 9.004,6.414 9.859,6.422L10.645,6.426L10.648,7.211C10.656,8.051 10.652,8.008 10.707,8.105C10.789,8.254 10.969,8.336 11.137,8.309C11.32,8.281 11.465,8.133 11.492,7.941C11.496,7.906 11.5,7.547 11.5,7.148L11.5,6.422L12.27,6.422C13.09,6.422 13.07,6.422 13.172,6.371C13.227,6.344 13.313,6.258 13.34,6.203C13.391,6.102 13.402,5.98 13.371,5.883C13.332,5.758 13.258,5.672 13.141,5.617L13.074,5.586L12.289,5.582L11.5,5.578L11.5,4.852C11.5,4.453 11.496,4.094 11.492,4.059C11.469,3.879 11.336,3.73 11.16,3.695C11.09,3.68 11.055,3.68 10.992,3.691ZM10.992,3.691 + + + M21.352,6.5L21.352,8.402L21.457,8.398C21.836,8.375 22.234,8.172 22.496,7.867C22.762,7.563 22.914,7.223 22.98,6.785C22.996,6.68 22.996,6.34 22.98,6.227C22.922,5.805 22.773,5.461 22.512,5.148C22.25,4.836 21.844,4.625 21.465,4.602L21.352,4.598ZM21.352,6.5M3.023,0.945C2.578,0.973 2.07,1.117 1.684,1.332C0.723,1.863 0.102,2.82 0.012,3.914C-0.004,4.078 -0.004,8.922 0.012,9.086C0.094,10.086 0.613,10.969 1.449,11.523C1.762,11.73 2.074,11.871 2.457,11.965C2.586,11.996 2.652,12.008 2.879,12.043C3.004,12.063 17.047,12.063 17.172,12.043C17.395,12.008 17.465,11.996 17.594,11.965C17.984,11.867 18.32,11.719 18.637,11.504C19.449,10.941 19.953,10.078 20.039,9.086C20.055,8.926 20.055,4.082 20.039,3.914C19.957,2.918 19.43,2.02 18.602,1.473C18.188,1.203 17.773,1.051 17.227,0.965C17.121,0.945 16.668,0.945 10.094,0.945C6.234,0.945 3.055,0.945 3.023,0.945ZM16.754,2.125C17.023,2.152 17.289,2.23 17.535,2.352C18.281,2.711 18.781,3.438 18.859,4.266C18.867,4.336 18.871,5.125 18.871,6.5C18.871,8.77 18.871,8.746 18.82,8.984C18.727,9.441 18.504,9.855 18.176,10.184C18.051,10.313 17.957,10.387 17.801,10.488C17.512,10.68 17.191,10.801 16.836,10.855C16.73,10.875 16.363,10.875 10.02,10.875C3.676,10.875 3.309,10.875 3.203,10.855C2.844,10.801 2.527,10.68 2.23,10.48C2.008,10.336 1.852,10.191 1.684,9.984C1.484,9.734 1.324,9.41 1.242,9.094C1.172,8.801 1.176,8.992 1.176,6.496C1.176,4.379 1.176,4.25 1.191,4.141C1.32,3.348 1.797,2.695 2.504,2.348C2.742,2.234 3.02,2.152 3.273,2.125C3.371,2.113 4.609,2.113 10.004,2.109C15.73,2.109 16.633,2.113 16.754,2.125ZM16.754,2.125 + M21.352,6.5L21.352,8.402L21.457,8.398C21.836,8.375 22.234,8.172 22.496,7.867C22.762,7.563 22.914,7.223 22.98,6.785C22.996,6.68 22.996,6.34 22.98,6.227C22.922,5.805 22.773,5.461 22.512,5.148C22.25,4.836 21.844,4.625 21.465,4.602L21.352,4.598ZM21.352,6.5M3.023,0.945C2.578,0.973 2.07,1.117 1.684,1.332C0.723,1.863 0.102,2.82 0.012,3.914C-0.004,4.078 -0.004,8.922 0.012,9.086C0.094,10.086 0.613,10.969 1.449,11.523C1.762,11.73 2.074,11.871 2.457,11.965C2.586,11.996 2.652,12.008 2.879,12.043C3.004,12.063 17.047,12.063 17.172,12.043C17.395,12.008 17.465,11.996 17.594,11.965C17.984,11.867 18.32,11.719 18.637,11.504C19.449,10.941 19.953,10.078 20.039,9.086C20.055,8.926 20.055,4.082 20.039,3.914C19.957,2.918 19.43,2.02 18.602,1.473C18.188,1.203 17.773,1.051 17.227,0.965C17.121,0.945 16.668,0.945 10.094,0.945C6.234,0.945 3.055,0.945 3.023,0.945ZM16.754,2.125C17.023,2.152 17.289,2.23 17.535,2.352C18.281,2.711 18.781,3.438 18.859,4.266C18.867,4.336 18.871,5.125 18.871,6.5C18.871,8.77 18.871,8.746 18.82,8.984C18.727,9.441 18.504,9.855 18.176,10.184C18.051,10.313 17.957,10.387 17.801,10.488C17.512,10.68 17.191,10.801 16.836,10.855C16.73,10.875 16.363,10.875 10.02,10.875C3.676,10.875 3.309,10.875 3.203,10.855C2.844,10.801 2.527,10.68 2.23,10.48C2.008,10.336 1.852,10.191 1.684,9.984C1.484,9.734 1.324,9.41 1.242,9.094C1.172,8.801 1.176,8.992 1.176,6.496C1.176,4.379 1.176,4.25 1.191,4.141C1.32,3.348 1.797,2.695 2.504,2.348C2.742,2.234 3.02,2.152 3.273,2.125C3.371,2.113 4.609,2.113 10.004,2.109C15.73,2.109 16.633,2.113 16.754,2.125ZM16.754,2.125 + M3.727,3.289C3.426,3.313 3.172,3.41 2.922,3.602C2.609,3.836 2.406,4.203 2.359,4.605C2.352,4.676 2.352,5.238 2.352,6.559C2.355,8.625 2.352,8.465 2.426,8.688C2.605,9.227 3.059,9.605 3.629,9.691C3.699,9.703 4.891,9.703 10.035,9.703C15.961,9.703 16.359,9.703 16.449,9.688C17.055,9.578 17.508,9.16 17.652,8.582C17.668,8.527 17.684,8.441 17.688,8.387C17.703,8.254 17.703,4.719 17.688,4.594C17.648,4.254 17.492,3.941 17.254,3.711C17.023,3.488 16.777,3.363 16.453,3.301C16.371,3.285 15.961,3.285 10.102,3.285C6.652,3.281 3.785,3.285 3.727,3.289ZM3.727,3.289 + M2.965,3.297C2.688,3.352 2.469,3.555 2.379,3.84L2.355,3.914L2.355,9.066L2.379,9.141C2.445,9.355 2.582,9.52 2.773,9.613C2.902,9.68 2.973,9.695 3.113,9.695C3.262,9.695 3.328,9.68 3.453,9.613C3.648,9.516 3.785,9.355 3.852,9.141L3.875,9.066L3.875,3.914L3.852,3.84C3.727,3.445 3.355,3.219 2.965,3.297ZM2.965,3.297 + M2.965,3.297C2.688,3.352 2.469,3.555 2.379,3.84L2.355,3.914L2.355,9.066L2.379,9.141C2.445,9.355 2.582,9.52 2.773,9.613C2.945,9.699 2.883,9.695 3.879,9.695C4.879,9.695 4.816,9.699 4.984,9.613C5.18,9.516 5.313,9.355 5.383,9.141L5.406,9.066L5.406,3.914L5.383,3.84C5.289,3.555 5.07,3.348 4.789,3.297C4.676,3.273 3.074,3.273 2.965,3.297ZM2.965,3.297 + M2.965,3.297C2.688,3.352 2.469,3.555 2.379,3.84L2.355,3.914L2.355,9.066L2.379,9.141C2.445,9.355 2.582,9.52 2.773,9.613C2.949,9.703 2.785,9.695 4.645,9.695C6.508,9.695 6.34,9.703 6.516,9.613C6.711,9.516 6.844,9.355 6.91,9.141L6.934,9.066L6.934,3.914L6.91,3.84C6.84,3.605 6.68,3.43 6.465,3.336C6.324,3.277 6.387,3.281 4.637,3.281C3.316,3.281 3.023,3.285 2.965,3.297ZM2.965,3.297 + M2.965,3.297C2.688,3.352 2.469,3.555 2.379,3.84L2.355,3.914L2.355,9.066L2.379,9.141C2.445,9.355 2.582,9.52 2.773,9.613C2.953,9.703 2.688,9.695 5.41,9.695C8.133,9.695 7.871,9.703 8.047,9.613C8.238,9.516 8.375,9.355 8.441,9.141L8.465,9.066L8.465,3.914L8.441,3.84C8.367,3.605 8.211,3.43 7.992,3.336C7.852,3.277 7.992,3.281 5.402,3.281C3.434,3.281 3.023,3.285 2.965,3.297ZM2.965,3.297 + M2.965,3.297C2.688,3.352 2.469,3.555 2.379,3.84L2.355,3.914L2.355,9.066L2.379,9.141C2.445,9.355 2.582,9.52 2.773,9.613C2.953,9.703 2.59,9.695 6.176,9.695C9.762,9.695 9.398,9.703 9.574,9.613C9.77,9.516 9.902,9.355 9.973,9.141L9.996,9.066L9.996,3.914L9.973,3.84C9.898,3.605 9.742,3.43 9.523,3.336C9.379,3.277 9.598,3.281 6.164,3.281C3.547,3.281 3.023,3.285 2.965,3.297ZM2.965,3.297 + M2.965,3.297C2.688,3.352 2.469,3.555 2.379,3.84L2.355,3.914L2.355,9.066L2.379,9.141C2.445,9.355 2.582,9.52 2.773,9.613C2.824,9.641 2.895,9.668 2.93,9.676C2.992,9.695 3.215,9.695 6.941,9.695C10.66,9.695 10.891,9.695 10.949,9.68C10.984,9.668 11.055,9.641 11.105,9.613C11.301,9.516 11.434,9.355 11.504,9.141L11.523,9.066L11.523,3.914L11.504,3.84C11.43,3.605 11.27,3.43 11.055,3.336C10.91,3.277 11.203,3.281 6.93,3.281C3.66,3.281 3.023,3.285 2.965,3.297ZM2.965,3.297 + M2.965,3.297C2.688,3.352 2.469,3.555 2.379,3.84L2.355,3.914L2.355,9.066L2.379,9.141C2.445,9.355 2.582,9.52 2.773,9.613C2.824,9.641 2.895,9.668 2.93,9.676C2.992,9.695 3.25,9.695 7.703,9.695C12.152,9.695 12.418,9.695 12.48,9.68C12.516,9.668 12.586,9.641 12.637,9.613C12.828,9.516 12.965,9.355 13.031,9.141L13.055,9.066L13.055,3.914L13.031,3.84C12.941,3.555 12.719,3.348 12.438,3.297C12.367,3.281 11.863,3.281 7.695,3.281C3.777,3.281 3.023,3.285 2.965,3.297ZM2.965,3.297 + M2.965,3.297C2.688,3.352 2.469,3.555 2.379,3.84L2.355,3.914L2.355,9.066L2.379,9.141C2.445,9.355 2.582,9.52 2.773,9.613C2.824,9.641 2.895,9.668 2.93,9.676C2.992,9.695 3.285,9.695 8.469,9.695C13.645,9.695 13.949,9.695 14.012,9.68C14.047,9.668 14.117,9.641 14.164,9.613C14.359,9.516 14.492,9.355 14.563,9.141L14.586,9.066L14.586,3.914L14.563,3.84C14.473,3.555 14.25,3.348 13.969,3.297C13.895,3.281 13.316,3.281 8.461,3.281C3.891,3.281 3.023,3.285 2.965,3.297ZM2.965,3.297 + M2.965,3.297C2.688,3.352 2.469,3.555 2.379,3.84L2.355,3.914L2.355,9.066L2.379,9.141C2.445,9.355 2.582,9.52 2.773,9.613C2.824,9.641 2.895,9.668 2.93,9.676C2.992,9.695 3.32,9.695 9.234,9.695C15.137,9.695 15.48,9.695 15.539,9.68C15.574,9.668 15.645,9.641 15.695,9.613C15.891,9.516 16.023,9.355 16.094,9.141L16.113,9.066L16.113,3.914L16.094,3.84C16,3.555 15.777,3.348 15.5,3.297C15.426,3.281 14.773,3.281 9.227,3.281C4.004,3.281 3.023,3.285 2.965,3.297ZM2.965,3.297 + M2.965,3.297C2.688,3.352 2.469,3.555 2.379,3.84L2.355,3.914L2.355,9.066L2.379,9.141C2.445,9.355 2.582,9.52 2.773,9.613C2.824,9.641 2.891,9.668 2.926,9.676C2.98,9.695 3.344,9.695 9.969,9.699C15.227,9.699 16.973,9.699 17.031,9.688C17.305,9.648 17.539,9.461 17.641,9.207C17.703,9.047 17.699,9.223 17.699,6.449L17.695,3.926L17.668,3.852C17.613,3.688 17.551,3.586 17.434,3.48C17.359,3.414 17.23,3.344 17.121,3.309L17.047,3.285L10.039,3.281C4.113,3.281 3.023,3.285 2.965,3.297ZM2.965,3.297 + M14.113,5.551C13.902,5.582 13.676,5.711 13.539,5.871C13.246,6.223 13.234,6.703 13.508,7.066C13.641,7.238 13.824,7.359 14.051,7.414C14.152,7.441 14.375,7.441 14.469,7.414C14.707,7.352 14.879,7.242 15.012,7.066C15.379,6.582 15.211,5.895 14.664,5.633C14.613,5.609 14.535,5.582 14.492,5.57C14.402,5.547 14.207,5.535 14.113,5.551ZM14.113,5.551M5.637,5.551C5.426,5.582 5.195,5.711 5.059,5.871C4.77,6.223 4.754,6.703 5.027,7.066C5.16,7.238 5.344,7.359 5.57,7.414C5.672,7.441 5.895,7.441 5.988,7.414C6.227,7.352 6.398,7.242 6.531,7.066C6.898,6.582 6.73,5.895 6.184,5.633C6.133,5.609 6.059,5.582 6.016,5.57C5.922,5.547 5.727,5.535 5.637,5.551ZM5.637,5.551M9.02,6.23C8.93,6.258 8.879,6.289 8.816,6.355C8.707,6.465 8.664,6.598 8.676,6.77C8.699,7.129 8.82,7.426 9.039,7.652C9.391,8.023 9.945,8.156 10.441,7.988C10.988,7.809 11.34,7.313 11.34,6.723C11.34,6.641 11.336,6.598 11.32,6.547C11.277,6.414 11.164,6.289 11.031,6.238C10.961,6.211 10.828,6.203 10.75,6.219C10.523,6.27 10.371,6.477 10.371,6.734C10.371,6.801 10.348,6.879 10.316,6.934C10.23,7.078 10.004,7.133 9.836,7.051C9.707,6.988 9.656,6.895 9.641,6.691C9.637,6.617 9.625,6.555 9.609,6.516C9.563,6.387 9.438,6.273 9.305,6.23C9.219,6.203 9.098,6.203 9.02,6.23ZM9.02,6.23 + M14.113,5.551C13.902,5.582 13.676,5.711 13.539,5.871C13.246,6.223 13.234,6.703 13.508,7.066C13.641,7.238 13.824,7.359 14.051,7.414C14.152,7.441 14.375,7.441 14.469,7.414C14.707,7.352 14.879,7.242 15.012,7.066C15.379,6.582 15.211,5.895 14.664,5.633C14.613,5.609 14.535,5.582 14.492,5.57C14.402,5.547 14.207,5.535 14.113,5.551ZM14.113,5.551M5.637,5.551C5.426,5.582 5.195,5.711 5.059,5.871C4.77,6.223 4.754,6.703 5.027,7.066C5.16,7.238 5.344,7.359 5.57,7.414C5.672,7.441 5.895,7.441 5.988,7.414C6.227,7.352 6.398,7.242 6.531,7.066C6.898,6.582 6.73,5.895 6.184,5.633C6.133,5.609 6.059,5.582 6.016,5.57C5.922,5.547 5.727,5.535 5.637,5.551ZM5.637,5.551M8.66,6.5C8.602,6.512 8.5,6.563 8.453,6.602C8.254,6.777 8.23,7.086 8.41,7.281C8.461,7.34 8.551,7.395 8.629,7.422C8.691,7.441 8.723,7.441 10.02,7.441C11.316,7.441 11.348,7.441 11.41,7.422C11.566,7.371 11.691,7.25 11.734,7.102C11.758,7.023 11.758,6.898 11.73,6.824C11.695,6.711 11.602,6.598 11.5,6.547C11.387,6.488 11.438,6.488 10.016,6.492C9.297,6.492 8.684,6.496 8.66,6.5ZM8.66,6.5 + M14.113,5.551C13.902,5.582 13.676,5.711 13.539,5.871C13.246,6.223 13.234,6.703 13.508,7.066C13.641,7.238 13.824,7.359 14.051,7.414C14.152,7.441 14.375,7.441 14.469,7.414C14.707,7.352 14.879,7.242 15.012,7.066C15.379,6.582 15.211,5.895 14.664,5.633C14.613,5.609 14.535,5.582 14.492,5.57C14.402,5.547 14.207,5.535 14.113,5.551ZM14.113,5.551M5.637,5.551C5.426,5.582 5.195,5.711 5.059,5.871C4.77,6.223 4.754,6.703 5.027,7.066C5.16,7.238 5.344,7.359 5.57,7.414C5.672,7.441 5.895,7.441 5.988,7.414C6.227,7.352 6.398,7.242 6.531,7.066C6.898,6.582 6.73,5.895 6.184,5.633C6.133,5.609 6.059,5.582 6.016,5.57C5.922,5.547 5.727,5.535 5.637,5.551ZM5.637,5.551M9.875,6.008C9.699,6.031 9.555,6.078 9.406,6.152C9.039,6.34 8.789,6.691 8.723,7.121C8.676,7.402 8.711,7.57 8.844,7.703C8.879,7.738 8.93,7.777 8.957,7.793C9.141,7.891 9.367,7.859 9.52,7.719C9.617,7.621 9.656,7.527 9.672,7.359C9.684,7.203 9.703,7.141 9.781,7.066C9.855,6.992 9.941,6.965 10.066,6.973C10.273,6.984 10.398,7.125 10.398,7.34C10.398,7.578 10.547,7.777 10.766,7.836C10.934,7.879 11.117,7.824 11.234,7.699C11.344,7.582 11.375,7.48 11.367,7.289C11.34,6.707 11,6.246 10.473,6.07C10.324,6.023 10.246,6.008 10.07,6.004C9.98,6.004 9.891,6.004 9.875,6.008ZM9.875,6.008 + M0,6.5L0,13L23,13L23,0L0,0ZM16.453,3.301C16.777,3.363 17.023,3.488 17.254,3.711C17.449,3.898 17.586,4.137 17.656,4.41C17.668,4.461 17.684,4.547 17.688,4.594C17.703,4.719 17.703,8.254 17.688,8.387C17.652,8.707 17.508,9.023 17.289,9.246C17.059,9.484 16.781,9.629 16.449,9.688C16.359,9.703 15.961,9.703 10.035,9.703C4.891,9.703 3.699,9.703 3.629,9.691C3.301,9.641 3.004,9.492 2.777,9.262C2.621,9.105 2.496,8.902 2.426,8.688C2.352,8.465 2.355,8.625 2.352,6.559C2.352,5.238 2.352,4.676 2.359,4.605C2.441,3.891 3.012,3.34 3.727,3.289C3.785,3.285 6.652,3.281 10.102,3.285C15.961,3.285 16.371,3.285 16.453,3.301ZM16.453,3.301 + M14.113,5.551C13.902,5.582 13.676,5.711 13.539,5.871C13.246,6.223 13.234,6.703 13.508,7.066C13.641,7.238 13.824,7.359 14.051,7.414C14.152,7.441 14.375,7.441 14.469,7.414C14.707,7.352 14.879,7.242 15.012,7.066C15.379,6.582 15.211,5.895 14.664,5.633C14.613,5.609 14.535,5.582 14.492,5.57C14.402,5.547 14.207,5.535 14.113,5.551ZM14.113,5.551M5.637,5.551C5.426,5.582 5.195,5.711 5.059,5.871C4.77,6.223 4.754,6.703 5.027,7.066C5.16,7.238 5.344,7.359 5.57,7.414C5.672,7.441 5.895,7.441 5.988,7.414C6.227,7.352 6.398,7.242 6.531,7.066C6.898,6.582 6.73,5.895 6.184,5.633C6.133,5.609 6.059,5.582 6.016,5.57C5.922,5.547 5.727,5.535 5.637,5.551ZM5.637,5.551M9.02,6.23C8.93,6.258 8.879,6.289 8.816,6.355C8.707,6.465 8.664,6.598 8.676,6.77C8.699,7.129 8.82,7.426 9.039,7.652C9.391,8.023 9.945,8.156 10.441,7.988C10.988,7.809 11.34,7.313 11.34,6.723C11.34,6.641 11.336,6.598 11.32,6.547C11.277,6.414 11.164,6.289 11.031,6.238C10.961,6.211 10.828,6.203 10.75,6.219C10.523,6.27 10.371,6.477 10.371,6.734C10.371,6.801 10.348,6.879 10.316,6.934C10.23,7.078 10.004,7.133 9.836,7.051C9.707,6.988 9.656,6.895 9.641,6.691C9.637,6.617 9.625,6.555 9.609,6.516C9.563,6.387 9.438,6.273 9.305,6.23C9.219,6.203 9.098,6.203 9.02,6.23ZM9.02,6.23 + M21.352,6.5L21.352,8.402L21.457,8.398C21.836,8.375 22.234,8.172 22.496,7.867C22.762,7.563 22.914,7.223 22.98,6.785C22.996,6.68 22.996,6.34 22.98,6.227C22.922,5.805 22.773,5.461 22.512,5.148C22.25,4.836 21.844,4.625 21.465,4.602L21.352,4.598ZM21.352,6.5M3.023,0.945C2.578,0.973 2.07,1.117 1.684,1.332C0.723,1.863 0.102,2.82 0.012,3.914C-0.004,4.078 -0.004,8.922 0.012,9.086C0.094,10.086 0.613,10.969 1.449,11.523C1.762,11.73 2.074,11.871 2.457,11.965C2.586,11.996 2.652,12.008 2.879,12.043C3.004,12.063 17.047,12.063 17.172,12.043C17.395,12.008 17.465,11.996 17.594,11.965C17.984,11.867 18.32,11.719 18.637,11.504C19.449,10.941 19.953,10.078 20.039,9.086C20.055,8.926 20.055,4.082 20.039,3.914C19.957,2.918 19.43,2.02 18.602,1.473C18.188,1.203 17.773,1.051 17.227,0.965C17.121,0.945 16.668,0.945 10.094,0.945C6.234,0.945 3.055,0.945 3.023,0.945ZM16.754,2.125C17.023,2.152 17.289,2.23 17.535,2.352C18.281,2.711 18.781,3.438 18.859,4.266C18.867,4.336 18.871,5.125 18.871,6.5C18.871,8.77 18.871,8.746 18.82,8.984C18.727,9.441 18.504,9.855 18.176,10.184C18.051,10.313 17.957,10.387 17.801,10.488C17.512,10.68 17.191,10.801 16.836,10.855C16.73,10.875 16.363,10.875 10.02,10.875C3.676,10.875 3.309,10.875 3.203,10.855C2.844,10.801 2.527,10.68 2.23,10.48C2.008,10.336 1.852,10.191 1.684,9.984C1.484,9.734 1.324,9.41 1.242,9.094C1.172,8.801 1.176,8.992 1.176,6.496C1.176,4.379 1.176,4.25 1.191,4.141C1.32,3.348 1.797,2.695 2.504,2.348C2.742,2.234 3.02,2.152 3.273,2.125C3.371,2.113 4.609,2.113 10.004,2.109C15.73,2.109 16.633,2.113 16.754,2.125ZM16.754,2.125 + + + M2.641,1.352C2.246,1.391 1.84,1.555 1.531,1.801C1.121,2.125 0.871,2.551 0.77,3.09L0.746,3.203L0.742,5.922C0.738,7.816 0.742,8.672 0.75,8.75C0.82,9.488 1.262,10.125 1.922,10.445C2.152,10.559 2.402,10.625 2.66,10.648C2.82,10.664 14.949,10.664 15.109,10.648C15.953,10.57 16.656,10.016 16.922,9.219C17,8.98 17.031,8.781 17.031,8.5L17.031,8.34L17.672,8.336C18.246,8.336 18.324,8.332 18.391,8.316C18.621,8.262 18.813,8.156 18.969,8C19.133,7.836 19.234,7.645 19.285,7.41C19.305,7.328 19.305,7.242 19.305,6.004C19.305,4.766 19.305,4.68 19.285,4.602C19.195,4.176 18.906,3.855 18.5,3.727C18.332,3.672 18.293,3.672 17.641,3.672L17.031,3.672L17.027,3.441C17.02,3.102 16.973,2.875 16.855,2.605C16.551,1.902 15.887,1.422 15.109,1.352C14.965,1.336 2.781,1.34 2.641,1.352ZM14.883,2.488C15.395,2.594 15.77,2.965 15.879,3.469C15.895,3.551 15.895,3.672 15.895,6C15.895,8.328 15.895,8.449 15.879,8.531C15.773,9.012 15.426,9.375 14.953,9.492C14.922,9.504 14.848,9.516 14.789,9.52C14.645,9.535 3.125,9.535 2.98,9.52C2.438,9.465 2.008,9.074 1.895,8.531C1.875,8.449 1.875,8.328 1.875,6C1.875,3.672 1.875,3.551 1.895,3.469C2.012,2.918 2.449,2.527 3.004,2.477C3.027,2.477 5.695,2.473 8.934,2.477C13.504,2.477 14.84,2.48 14.883,2.488ZM17.906,4.82C18.023,4.867 18.117,4.961 18.16,5.066L18.184,5.125L18.184,6C18.184,6.82 18.184,6.879 18.168,6.926C18.141,7.004 18.07,7.09 18,7.137C17.898,7.203 17.863,7.211 17.414,7.211L17.031,7.211L17.031,4.801L17.449,4.801C17.813,4.805 17.871,4.809 17.906,4.82ZM17.906,4.82 + M2.641,1.352C2.246,1.391 1.84,1.555 1.531,1.801C1.121,2.125 0.871,2.551 0.77,3.09L0.746,3.203L0.742,5.922C0.738,7.816 0.742,8.672 0.75,8.75C0.82,9.488 1.262,10.125 1.922,10.445C2.152,10.559 2.402,10.625 2.66,10.648C2.82,10.664 14.949,10.664 15.109,10.648C15.953,10.57 16.656,10.016 16.922,9.219C17,8.98 17.031,8.781 17.031,8.5L17.031,8.34L17.672,8.336C18.246,8.336 18.324,8.332 18.391,8.316C18.621,8.262 18.813,8.156 18.969,8C19.133,7.836 19.234,7.645 19.285,7.41C19.305,7.328 19.305,7.242 19.305,6.004C19.305,4.766 19.305,4.68 19.285,4.602C19.195,4.176 18.906,3.855 18.5,3.727C18.332,3.672 18.293,3.672 17.641,3.672L17.031,3.672L17.027,3.441C17.02,3.102 16.973,2.875 16.855,2.605C16.551,1.902 15.887,1.422 15.109,1.352C14.965,1.336 2.781,1.34 2.641,1.352ZM14.883,2.488C15.395,2.594 15.77,2.965 15.879,3.469C15.895,3.551 15.895,3.672 15.895,6C15.895,8.328 15.895,8.449 15.879,8.531C15.773,9.012 15.426,9.375 14.953,9.492C14.922,9.504 14.848,9.516 14.789,9.52C14.645,9.535 3.125,9.535 2.98,9.52C2.438,9.465 2.008,9.074 1.895,8.531C1.875,8.449 1.875,8.328 1.875,6C1.875,3.672 1.875,3.551 1.895,3.469C2.012,2.918 2.449,2.527 3.004,2.477C3.027,2.477 5.695,2.473 8.934,2.477C13.504,2.477 14.84,2.48 14.883,2.488ZM17.906,4.82C18.023,4.867 18.117,4.961 18.16,5.066L18.184,5.125L18.184,6C18.184,6.82 18.184,6.879 18.168,6.926C18.141,7.004 18.07,7.09 18,7.137C17.898,7.203 17.863,7.211 17.414,7.211L17.031,7.211L17.031,4.801L17.449,4.801C17.813,4.805 17.871,4.809 17.906,4.82ZM17.906,4.82 + M12.391,6L12.391,8.398L13.359,8.398C14.438,8.398 14.414,8.402 14.527,8.336C14.648,8.262 14.734,8.141 14.762,8.012C14.766,7.977 14.77,7.258 14.77,5.992C14.77,3.848 14.773,3.977 14.715,3.859C14.68,3.789 14.578,3.691 14.512,3.656C14.402,3.602 14.406,3.602 13.359,3.602L12.391,3.602ZM12.391,6M3.371,3.613C3.203,3.652 3.063,3.789 3.016,3.969C3.004,4.016 3,4.281 3,5.992C3,7.258 3.004,7.977 3.012,8.012C3.035,8.141 3.121,8.262 3.242,8.336C3.355,8.402 3.332,8.398 4.41,8.398L5.379,8.398L5.379,3.602L4.406,3.602C3.629,3.602 3.422,3.605 3.371,3.613ZM3.371,3.613M6.129,6L6.129,8.398L8.512,8.398L8.512,3.602L6.129,3.602ZM6.129,6M9.262,6L9.262,8.398L11.641,8.398L11.641,3.602L9.262,3.602ZM9.262,6 + M3.004,2.477C2.789,2.496 2.59,2.566 2.41,2.688C2.145,2.867 1.965,3.141 1.895,3.469C1.875,3.551 1.875,3.672 1.875,6C1.875,8.328 1.875,8.449 1.895,8.531C1.949,8.785 2.086,9.035 2.262,9.195C2.469,9.383 2.711,9.492 2.98,9.52C3.125,9.535 14.645,9.535 14.789,9.52C15.328,9.465 15.762,9.07 15.879,8.531C15.895,8.449 15.895,8.328 15.895,6C15.895,3.672 15.895,3.551 15.879,3.469C15.77,2.965 15.395,2.594 14.883,2.488C14.84,2.48 13.504,2.477 8.934,2.477C5.695,2.473 3.027,2.477 3.004,2.477ZM5.379,6L5.379,8.398L4.41,8.398C3.332,8.398 3.355,8.402 3.242,8.336C3.121,8.262 3.035,8.141 3.012,8.012C3.004,7.977 3,7.258 3,5.992C3,3.852 2.996,3.977 3.055,3.859C3.09,3.789 3.18,3.699 3.25,3.66C3.363,3.602 3.344,3.602 4.406,3.602L5.379,3.602ZM8.512,6L8.512,8.398L6.129,8.398L6.129,3.602L8.512,3.602ZM11.641,6L11.641,8.398L9.262,8.398L9.262,3.602L11.641,3.602ZM14.391,3.613C14.43,3.621 14.48,3.641 14.512,3.656C14.578,3.691 14.68,3.789 14.715,3.859C14.773,3.977 14.77,3.848 14.77,5.992C14.77,7.258 14.766,7.977 14.762,8.012C14.734,8.141 14.648,8.262 14.527,8.336C14.414,8.402 14.438,8.398 13.359,8.398L12.391,8.398L12.391,3.602L13.359,3.602C14.148,3.602 14.336,3.602 14.391,3.613ZM14.391,3.613 + M8.066,4.191C8.113,4.242 8.281,4.426 8.441,4.605C8.523,4.695 8.656,4.844 8.738,4.934C8.816,5.023 8.93,5.145 8.984,5.203L9.086,5.313L8.512,5.309C8.195,5.305 7.832,5.297 7.703,5.297C7.578,5.293 7.223,5.285 6.91,5.281C6.598,5.273 6.176,5.266 5.965,5.266L5.586,5.258L5.609,5.285C5.625,5.301 5.672,5.352 5.715,5.398C5.895,5.59 6.285,6.012 6.289,6.02C6.289,6.027 6.168,6.035 5.586,6.055C5.355,6.063 5.047,6.074 4.898,6.078C4.754,6.086 4.496,6.094 4.328,6.102C4.16,6.105 3.969,6.113 3.898,6.117L3.773,6.125L3.773,6.152C3.773,6.184 3.777,6.184 3.898,6.219C4.863,6.512 7.773,7.383 8.27,7.527C8.328,7.547 8.379,7.559 8.387,7.559C8.395,7.559 8.406,7.547 8.41,7.535C8.422,7.512 8.406,7.492 7.902,6.988C7.617,6.703 7.391,6.469 7.398,6.469C7.406,6.469 7.52,6.484 7.648,6.5C7.895,6.531 8.527,6.609 9.629,6.75C10.633,6.879 11.375,6.969 11.387,6.969C11.398,6.969 11.398,6.969 11.387,6.953C11.383,6.941 11.129,6.676 10.824,6.363C10.52,6.051 10.262,5.781 10.246,5.766L10.223,5.734L10.289,5.738C10.535,5.75 13.063,5.871 13.57,5.895C13.738,5.902 13.906,5.91 13.945,5.91C14.059,5.91 14.059,5.898 13.941,5.789C13.891,5.738 13.676,5.531 13.465,5.328C12.883,4.773 12.527,4.43 12.395,4.301L12.273,4.184L11.586,4.176C11.207,4.172 10.254,4.168 9.465,4.164L8.035,4.156ZM8.066,4.191 + M2.641,1.352C2.246,1.391 1.84,1.555 1.531,1.801C1.121,2.125 0.871,2.551 0.77,3.09L0.746,3.203L0.742,5.922C0.738,7.816 0.742,8.672 0.75,8.75C0.82,9.488 1.262,10.125 1.922,10.445C2.152,10.559 2.402,10.625 2.66,10.648C2.82,10.664 14.949,10.664 15.109,10.648C15.953,10.57 16.656,10.016 16.922,9.219C17,8.98 17.031,8.781 17.031,8.5L17.031,8.34L17.672,8.336C18.246,8.336 18.324,8.332 18.391,8.316C18.621,8.262 18.813,8.156 18.969,8C19.133,7.836 19.234,7.645 19.285,7.41C19.305,7.328 19.305,7.242 19.305,6.004C19.305,4.766 19.305,4.68 19.285,4.602C19.195,4.176 18.906,3.855 18.5,3.727C18.332,3.672 18.293,3.672 17.641,3.672L17.031,3.672L17.027,3.441C17.02,3.102 16.973,2.875 16.855,2.605C16.551,1.902 15.887,1.422 15.109,1.352C14.965,1.336 2.781,1.34 2.641,1.352ZM14.883,2.488C15.395,2.594 15.77,2.965 15.879,3.469C15.895,3.551 15.895,3.672 15.895,6C15.895,8.328 15.895,8.449 15.879,8.531C15.773,9.012 15.426,9.375 14.953,9.492C14.922,9.504 14.848,9.516 14.789,9.52C14.645,9.535 3.125,9.535 2.98,9.52C2.438,9.465 2.008,9.074 1.895,8.531C1.875,8.449 1.875,8.328 1.875,6C1.875,3.672 1.875,3.551 1.895,3.469C2.012,2.918 2.449,2.527 3.004,2.477C3.027,2.477 5.695,2.473 8.934,2.477C13.504,2.477 14.84,2.48 14.883,2.488ZM17.906,4.82C18.023,4.867 18.117,4.961 18.16,5.066L18.184,5.125L18.184,6C18.184,6.82 18.184,6.879 18.168,6.926C18.141,7.004 18.07,7.09 18,7.137C17.898,7.203 17.863,7.211 17.414,7.211L17.031,7.211L17.031,4.801L17.449,4.801C17.813,4.805 17.871,4.809 17.906,4.82ZM17.906,4.82 + + + M20.418,4.176C20.414,4.18 20.41,5.004 20.41,6.008L20.41,7.828L20.445,7.828C20.594,7.828 20.809,7.785 20.961,7.719C21.711,7.398 22.141,6.473 21.953,5.578C21.809,4.863 21.301,4.313 20.684,4.195C20.582,4.176 20.43,4.164 20.418,4.176ZM20.418,4.176M2.578,0.988C2.113,1.004 1.676,1.133 1.273,1.379C0.848,1.637 0.504,2.012 0.273,2.473C0.148,2.727 0.07,2.98 0.023,3.289C0.008,3.398 0.004,3.551 0.004,6C0.004,8.867 0,8.641 0.066,8.941C0.125,9.199 0.254,9.523 0.395,9.75C0.551,10 0.824,10.305 1.039,10.465C1.254,10.629 1.516,10.77 1.75,10.855C2.012,10.945 2.254,10.996 2.543,11.012C2.785,11.023 16.613,11.023 16.855,11.012C17.254,10.988 17.586,10.902 17.934,10.727C18.211,10.59 18.398,10.453 18.621,10.23C19.035,9.813 19.281,9.32 19.379,8.711C19.395,8.602 19.395,8.449 19.395,6C19.395,3.133 19.398,3.359 19.336,3.059C19.191,2.422 18.781,1.816 18.246,1.453C17.871,1.199 17.508,1.063 17.031,0.996C16.965,0.988 15.242,0.984 9.805,0.984C5.879,0.984 2.629,0.984 2.578,0.988ZM16.734,1.93C17.176,1.973 17.59,2.172 17.898,2.477C18.063,2.645 18.172,2.797 18.27,3.004C18.387,3.242 18.445,3.469 18.461,3.738C18.473,3.949 18.473,8.051 18.461,8.262C18.449,8.445 18.422,8.594 18.367,8.754C18.203,9.254 17.852,9.656 17.387,9.887C17.199,9.977 17.047,10.023 16.836,10.059C16.73,10.074 16.371,10.074 9.699,10.074C3.027,10.074 2.672,10.074 2.566,10.059C2.352,10.023 2.199,9.977 2.016,9.887C1.547,9.656 1.195,9.254 1.031,8.754C0.977,8.594 0.949,8.445 0.941,8.262C0.926,8.051 0.93,3.949 0.941,3.738C0.957,3.453 1.02,3.227 1.148,2.969C1.406,2.457 1.875,2.094 2.441,1.965C2.504,1.953 2.598,1.938 2.645,1.934C2.781,1.918 16.598,1.918 16.734,1.93ZM16.734,1.93 + M20.418,4.176C20.414,4.18 20.41,5.004 20.41,6.008L20.41,7.828L20.445,7.828C20.594,7.828 20.809,7.785 20.961,7.719C21.711,7.398 22.141,6.473 21.953,5.578C21.809,4.863 21.301,4.313 20.684,4.195C20.582,4.176 20.43,4.164 20.418,4.176ZM20.418,4.176M2.578,0.988C2.113,1.004 1.676,1.133 1.273,1.379C0.848,1.637 0.504,2.012 0.273,2.473C0.148,2.727 0.07,2.98 0.023,3.289C0.008,3.398 0.004,3.551 0.004,6C0.004,8.867 0,8.641 0.066,8.941C0.125,9.199 0.254,9.523 0.395,9.75C0.551,10 0.824,10.305 1.039,10.465C1.254,10.629 1.516,10.77 1.75,10.855C2.012,10.945 2.254,10.996 2.543,11.012C2.785,11.023 16.613,11.023 16.855,11.012C17.254,10.988 17.586,10.902 17.934,10.727C18.211,10.59 18.398,10.453 18.621,10.23C19.035,9.813 19.281,9.32 19.379,8.711C19.395,8.602 19.395,8.449 19.395,6C19.395,3.133 19.398,3.359 19.336,3.059C19.191,2.422 18.781,1.816 18.246,1.453C17.871,1.199 17.508,1.063 17.031,0.996C16.965,0.988 15.242,0.984 9.805,0.984C5.879,0.984 2.629,0.984 2.578,0.988ZM16.734,1.93C17.176,1.973 17.59,2.172 17.898,2.477C18.063,2.645 18.172,2.797 18.27,3.004C18.387,3.242 18.445,3.469 18.461,3.738C18.473,3.949 18.473,8.051 18.461,8.262C18.449,8.445 18.422,8.594 18.367,8.754C18.203,9.254 17.852,9.656 17.387,9.887C17.199,9.977 17.047,10.023 16.836,10.059C16.73,10.074 16.371,10.074 9.699,10.074C3.027,10.074 2.672,10.074 2.566,10.059C2.352,10.023 2.199,9.977 2.016,9.887C1.547,9.656 1.195,9.254 1.031,8.754C0.977,8.594 0.949,8.445 0.941,8.262C0.926,8.051 0.93,3.949 0.941,3.738C0.957,3.453 1.02,3.227 1.148,2.969C1.406,2.457 1.875,2.094 2.441,1.965C2.504,1.953 2.598,1.938 2.645,1.934C2.781,1.918 16.598,1.918 16.734,1.93ZM16.734,1.93 + M3.035,2.859C2.523,2.898 2.086,3.242 1.93,3.734C1.867,3.93 1.871,3.844 1.871,6C1.871,7.652 1.871,7.98 1.887,8.066C1.941,8.477 2.211,8.844 2.586,9.02C2.738,9.094 2.859,9.125 3.059,9.141C3.219,9.152 16.184,9.152 16.344,9.141C16.539,9.125 16.66,9.094 16.816,9.02C17.188,8.844 17.457,8.477 17.516,8.066C17.535,7.914 17.535,4.086 17.516,3.934C17.469,3.617 17.293,3.313 17.039,3.121C16.883,3 16.723,2.926 16.512,2.879L16.414,2.855L9.773,2.855C6.121,2.852 3.09,2.855 3.035,2.859ZM3.035,2.859 + M11.023,0.992C10.953,1 10.875,1.039 10.813,1.094C10.789,1.117 9.828,2.305 8.688,3.734C7.09,5.73 6.602,6.348 6.574,6.398C6.539,6.48 6.527,6.582 6.555,6.66C6.578,6.727 6.645,6.801 6.711,6.832L6.766,6.855L8.078,6.859C8.98,6.859 9.391,6.863 9.391,6.871C9.391,6.875 9.086,7.707 8.715,8.715C8.285,9.879 8.035,10.578 8.027,10.621C7.988,10.84 8.129,11.016 8.34,11.012C8.453,11.012 8.559,10.957 8.652,10.848C8.746,10.738 12.742,5.738 12.785,5.672C12.809,5.637 12.836,5.586 12.844,5.559C12.863,5.492 12.863,5.395 12.844,5.336C12.824,5.277 12.762,5.207 12.695,5.172L12.645,5.145L11.328,5.141C10.438,5.141 10.012,5.137 10.012,5.129C10.012,5.125 10.313,4.301 10.68,3.301C11.051,2.305 11.359,1.457 11.367,1.426C11.387,1.348 11.383,1.23 11.352,1.168C11.297,1.039 11.172,0.973 11.023,0.992ZM11.023,0.992 + M20.95,11.54L21.21,11.54L21.21,11.77L20.95,11.77zM20.95,11.77L21.21,11.77L21.21,11.54L20.95,11.54z + + + M2.977,0.285C2.344,0.324 1.711,0.57 1.211,0.969C1.086,1.066 0.816,1.34 0.715,1.461C0.52,1.707 0.332,2.043 0.219,2.344C0.129,2.59 0.063,2.887 0.039,3.156C0.031,3.246 0.031,4.094 0.031,6.086L0.035,8.887L0.059,9.031C0.113,9.371 0.211,9.672 0.359,9.977C0.668,10.598 1.168,11.094 1.797,11.398C2.105,11.555 2.449,11.652 2.82,11.699C2.906,11.715 4.266,11.715 10.953,11.715C18.605,11.715 18.992,11.715 19.109,11.695C19.477,11.645 19.781,11.555 20.094,11.398C20.52,11.191 20.871,10.914 21.172,10.547C21.5,10.141 21.73,9.629 21.82,9.109C21.867,8.832 21.863,8.934 21.863,5.988C21.863,2.922 21.867,3.133 21.805,2.813C21.66,2.102 21.289,1.477 20.734,1.012C20.406,0.738 20,0.52 19.598,0.406C19.449,0.363 19.34,0.34 19.145,0.309L19.004,0.285L11.016,0.285C6.621,0.285 3.004,0.285 2.977,0.285ZM2.977,0.285M22.672,6.008L22.672,7.762L22.746,7.758C22.906,7.742 23.016,7.711 23.16,7.633C23.336,7.543 23.508,7.391 23.641,7.203C23.75,7.051 23.867,6.797 23.926,6.59C24.148,5.762 23.859,4.824 23.258,4.434C23.094,4.324 22.934,4.273 22.734,4.258L22.672,4.25ZM22.672,6.008 + M20.95,11.54L21.21,11.54L21.21,11.77L20.95,11.77zM20.95,11.77L21.21,11.77L21.21,11.54L20.95,11.54z + M2.977,0.285C2.344,0.324 1.711,0.57 1.211,0.969C1.086,1.066 0.816,1.34 0.715,1.461C0.52,1.707 0.332,2.043 0.219,2.344C0.129,2.59 0.063,2.887 0.039,3.156C0.031,3.246 0.031,4.094 0.031,6.086L0.035,8.887L0.059,9.031C0.113,9.371 0.211,9.672 0.359,9.977C0.668,10.598 1.168,11.094 1.797,11.398C2.105,11.555 2.449,11.652 2.82,11.699C2.906,11.715 4.266,11.715 10.953,11.715C18.605,11.715 18.992,11.715 19.109,11.695C19.477,11.645 19.781,11.555 20.094,11.398C20.52,11.191 20.871,10.914 21.172,10.547C21.5,10.141 21.73,9.629 21.82,9.109C21.867,8.832 21.863,8.934 21.863,5.988C21.863,2.922 21.867,3.133 21.805,2.813C21.66,2.102 21.289,1.477 20.734,1.012C20.406,0.738 20,0.52 19.598,0.406C19.449,0.363 19.34,0.34 19.145,0.309L19.004,0.285L11.016,0.285C6.621,0.285 3.004,0.285 2.977,0.285ZM2.977,0.285M22.672,6.008L22.672,7.762L22.746,7.758C22.906,7.742 23.016,7.711 23.16,7.633C23.336,7.543 23.508,7.391 23.641,7.203C23.75,7.051 23.867,6.797 23.926,6.59C24.148,5.762 23.859,4.824 23.258,4.434C23.094,4.324 22.934,4.273 22.734,4.258L22.672,4.25ZM22.672,6.008 + M0,6L0,12L24,12L24,9.066C23.996,7.313 23.992,6.172 23.988,6.23C23.965,6.488 23.891,6.75 23.77,6.984C23.621,7.277 23.406,7.508 23.16,7.633C23.016,7.711 22.906,7.742 22.746,7.758L22.672,7.762L22.672,4.25L22.734,4.258C23.047,4.281 23.313,4.422 23.531,4.676C23.781,4.961 23.934,5.324 23.984,5.762C23.996,5.848 23.996,5.324 24,2.934L24,0L0,0ZM19.145,0.309C19.527,0.371 19.781,0.445 20.086,0.594C20.977,1.023 21.609,1.84 21.805,2.813C21.867,3.133 21.863,2.922 21.863,5.988C21.863,8.934 21.867,8.832 21.82,9.109C21.707,9.785 21.359,10.422 20.855,10.883C20.359,11.332 19.789,11.602 19.109,11.695C18.992,11.715 18.605,11.715 10.953,11.715C4.266,11.715 2.906,11.715 2.82,11.699C2.07,11.605 1.434,11.289 0.918,10.766C0.684,10.523 0.512,10.277 0.359,9.977C0.211,9.672 0.113,9.371 0.059,9.031L0.035,8.887L0.031,6.086C0.031,4.094 0.031,3.246 0.039,3.156C0.063,2.887 0.129,2.59 0.219,2.344C0.332,2.043 0.52,1.707 0.715,1.461C0.816,1.34 1.086,1.066 1.211,0.969C1.711,0.57 2.344,0.324 2.977,0.285C3.004,0.285 6.621,0.285 11.016,0.285L19.004,0.285ZM19.145,0.309 + M18.934,2.48C18.902,2.496 18.859,2.523 18.84,2.547C18.746,2.641 15.887,6.23 15.863,6.277C15.828,6.352 15.82,6.414 15.848,6.477C15.879,6.539 15.895,6.559 15.953,6.586L16.004,6.609L16.93,6.609C17.805,6.609 17.852,6.609 17.848,6.629C17.844,6.637 17.633,7.223 17.371,7.93C17.109,8.637 16.895,9.238 16.887,9.27C16.859,9.418 16.961,9.551 17.105,9.551C17.168,9.551 17.258,9.508 17.305,9.457C17.332,9.43 19.484,6.738 20.094,5.977C20.297,5.719 20.305,5.707 20.305,5.609C20.305,5.539 20.301,5.527 20.273,5.488C20.258,5.465 20.223,5.434 20.199,5.422L20.156,5.395L19.223,5.391C18.344,5.391 18.289,5.391 18.293,5.371C18.297,5.355 18.977,3.516 19.156,3.035C19.27,2.727 19.281,2.672 19.238,2.578C19.184,2.461 19.059,2.418 18.934,2.48ZM18.934,2.48 + M20.95,11.54L21.21,11.54L21.21,11.77L20.95,11.77zM20.95,11.77L21.21,11.77L21.21,11.54L20.95,11.54z + + + M22.102,6L22.102,7.914L22.199,7.906C22.785,7.871 23.309,7.492 23.59,6.906C23.684,6.715 23.738,6.523 23.773,6.289C23.793,6.164 23.797,5.875 23.781,5.746C23.707,5.16 23.402,4.648 22.961,4.352C22.738,4.203 22.461,4.109 22.207,4.094L22.102,4.086ZM22.102,6M3.629,0.578C3.012,0.621 2.465,0.789 1.953,1.098C1.148,1.586 0.563,2.375 0.324,3.285C0.258,3.543 0.227,3.75 0.211,4.031C0.195,4.262 0.195,7.738 0.211,7.969C0.238,8.496 0.363,8.965 0.59,9.422C0.945,10.125 1.5,10.684 2.211,11.039C2.598,11.234 2.938,11.34 3.398,11.402L3.566,11.426L10.535,11.426C17.195,11.426 17.512,11.426 17.648,11.406C17.945,11.371 18.258,11.297 18.496,11.207C19.227,10.934 19.832,10.453 20.262,9.813C20.539,9.402 20.734,8.922 20.82,8.441C20.875,8.117 20.871,8.242 20.879,6.102C20.883,4.047 20.879,3.973 20.84,3.684C20.781,3.273 20.629,2.82 20.434,2.469C19.984,1.656 19.273,1.059 18.398,0.758C18.215,0.695 17.922,0.629 17.66,0.594C17.543,0.578 17.102,0.574 10.605,0.574C6.793,0.574 3.656,0.574 3.629,0.578ZM17.34,2.301C17.789,2.355 18.203,2.547 18.523,2.852C18.852,3.16 19.059,3.559 19.137,4.02C19.152,4.117 19.156,4.246 19.156,6C19.156,7.77 19.152,7.883 19.137,7.984C19.066,8.406 18.883,8.777 18.594,9.078C18.266,9.426 17.816,9.645 17.336,9.699C17.262,9.707 15.391,9.711 10.48,9.707L3.727,9.703L3.59,9.676C3.074,9.574 2.633,9.301 2.332,8.895C2.133,8.625 2.016,8.355 1.945,7.996C1.926,7.887 1.926,7.855 1.922,6.09C1.918,4.27 1.922,4.145 1.953,3.965C2.063,3.402 2.375,2.934 2.852,2.621C3.117,2.445 3.445,2.328 3.77,2.297C3.863,2.285 17.262,2.289 17.34,2.301ZM17.34,2.301 + M20.95,11.54L21.21,11.54L21.21,11.77L20.95,11.77zM20.95,11.77L21.21,11.77L21.21,11.54L20.95,11.54z + M3.629,0.578C3.012,0.621 2.465,0.789 1.953,1.098C1.148,1.586 0.563,2.375 0.324,3.285C0.258,3.543 0.227,3.75 0.211,4.031C0.195,4.262 0.195,7.738 0.211,7.969C0.238,8.496 0.363,8.965 0.59,9.422C0.945,10.125 1.5,10.684 2.211,11.039C2.598,11.234 2.938,11.34 3.398,11.402L3.566,11.426L10.535,11.426C17.195,11.426 17.512,11.426 17.648,11.406C17.945,11.371 18.258,11.297 18.496,11.207C19.227,10.934 19.832,10.453 20.262,9.813C20.539,9.402 20.734,8.922 20.82,8.441C20.875,8.117 20.871,8.242 20.879,6.102C20.883,4.047 20.879,3.973 20.84,3.684C20.781,3.273 20.629,2.82 20.434,2.469C19.984,1.656 19.273,1.059 18.398,0.758C18.215,0.695 17.922,0.629 17.66,0.594C17.543,0.578 17.102,0.574 10.605,0.574C6.793,0.574 3.656,0.574 3.629,0.578ZM3.629,0.578 + M0,6L0,12L24,12L24,0L0,0ZM17.66,0.594C18.129,0.656 18.5,0.77 18.863,0.953C19.258,1.152 19.555,1.371 19.852,1.676C20.449,2.297 20.805,3.094 20.871,3.977C20.879,4.078 20.879,4.73 20.879,6.102C20.871,8.242 20.875,8.117 20.82,8.441C20.645,9.43 20.035,10.328 19.184,10.867C18.719,11.16 18.219,11.336 17.648,11.406C17.512,11.426 17.195,11.426 10.535,11.426L3.566,11.426L3.398,11.402C2.938,11.34 2.598,11.234 2.211,11.039C1.641,10.754 1.188,10.355 0.836,9.84C0.512,9.359 0.309,8.828 0.234,8.262C0.203,8.023 0.199,7.781 0.199,6C0.199,4.219 0.203,3.977 0.234,3.738C0.379,2.645 1.016,1.668 1.953,1.098C2.465,0.789 3.012,0.621 3.629,0.578C3.656,0.574 6.793,0.574 10.605,0.574C17.102,0.574 17.543,0.578 17.66,0.594ZM17.66,0.594M3.77,2.297C2.98,2.379 2.316,2.895 2.043,3.637C1.98,3.809 1.945,3.973 1.93,4.168C1.922,4.258 1.922,4.848 1.922,6.09C1.926,7.855 1.926,7.887 1.945,7.996C1.977,8.145 2.004,8.254 2.043,8.363C2.293,9.047 2.863,9.531 3.59,9.676L3.727,9.703L10.48,9.707C15.391,9.711 17.262,9.707 17.336,9.699C17.816,9.645 18.266,9.426 18.594,9.078C18.883,8.777 19.066,8.406 19.137,7.984C19.152,7.883 19.156,7.77 19.156,6C19.156,4.246 19.152,4.117 19.137,4.02C19.059,3.559 18.852,3.16 18.523,2.852C18.203,2.547 17.789,2.355 17.34,2.301C17.262,2.289 3.863,2.285 3.77,2.297ZM3.77,2.297 + M16.633,2.934C16.613,2.941 16.578,2.961 16.559,2.98C16.488,3.035 13.969,6.195 13.941,6.258C13.895,6.363 13.934,6.465 14.031,6.512C14.074,6.527 14.109,6.531 14.871,6.531C15.473,6.531 15.672,6.531 15.672,6.543C15.672,6.547 15.629,6.672 15.574,6.813C15.52,6.957 15.336,7.465 15.16,7.941C14.984,8.414 14.84,8.824 14.836,8.848C14.824,8.914 14.852,8.988 14.898,9.035C14.945,9.074 14.973,9.086 15.043,9.078C15.156,9.07 15.078,9.16 16.465,7.426C17.164,6.551 17.746,5.816 17.762,5.797C17.84,5.68 17.809,5.539 17.695,5.488C17.656,5.473 17.609,5.469 16.859,5.469C16.227,5.469 16.059,5.469 16.059,5.457C16.059,5.449 16.137,5.242 16.227,4.996C16.316,4.746 16.504,4.242 16.641,3.871C16.777,3.504 16.891,3.18 16.895,3.152C16.906,3.063 16.859,2.965 16.781,2.934C16.742,2.918 16.676,2.918 16.633,2.934ZM16.633,2.934 + M15.762,4.004C15.734,4.012 15.691,4.027 15.672,4.039C15.617,4.066 15.547,4.148 15.512,4.215L15.484,4.273L15.48,4.945L15.48,5.621L14.848,5.621C14.133,5.621 14.117,5.621 14.023,5.688C13.797,5.84 13.797,6.16 14.02,6.313C14.117,6.379 14.082,6.375 14.813,6.379L15.477,6.383L15.484,7.039C15.488,7.547 15.492,7.707 15.504,7.742C15.535,7.84 15.609,7.926 15.703,7.973C15.891,8.059 16.117,7.977 16.207,7.785L16.234,7.727L16.238,7.051L16.242,6.379L16.867,6.379C17.215,6.379 17.52,6.375 17.555,6.371C17.637,6.355 17.695,6.324 17.762,6.258C17.828,6.191 17.859,6.117 17.867,6.02C17.879,5.891 17.809,5.758 17.695,5.684C17.598,5.621 17.578,5.621 16.875,5.621L16.242,5.621L16.238,4.961C16.23,4.238 16.23,4.25 16.164,4.156C16.078,4.027 15.906,3.965 15.762,4.004ZM15.762,4.004 + + + M4.273,0.121C3.691,0.148 3.133,0.324 2.637,0.633C1.723,1.203 1.133,2.176 1.039,3.246C1.027,3.41 1.027,8.59 1.039,8.754C1.141,9.941 1.848,10.988 2.922,11.523C3.359,11.746 3.801,11.859 4.309,11.879C4.582,11.891 19.418,11.891 19.691,11.879C20.477,11.848 21.164,11.578 21.77,11.07C22.348,10.586 22.754,9.895 22.902,9.141C22.969,8.813 22.965,9.039 22.965,6C22.965,2.961 22.969,3.188 22.902,2.859C22.641,1.539 21.625,0.5 20.316,0.199C20.121,0.156 19.926,0.133 19.695,0.121C19.441,0.109 4.531,0.109 4.273,0.121ZM19.715,1.66C19.926,1.68 20.094,1.719 20.27,1.793C20.863,2.039 21.273,2.539 21.402,3.18L21.426,3.297L21.426,8.715L21.398,8.84C21.305,9.273 21.086,9.641 20.766,9.91C20.469,10.156 20.105,10.305 19.719,10.34C19.563,10.355 4.438,10.355 4.281,10.34C3.895,10.305 3.531,10.156 3.234,9.91C2.914,9.641 2.695,9.273 2.602,8.84L2.574,8.715L2.574,3.285L2.602,3.16C2.617,3.09 2.648,2.98 2.672,2.914C2.91,2.219 3.531,1.73 4.266,1.66C4.398,1.648 19.578,1.648 19.715,1.66ZM19.715,1.66 + M13.281,2.879C13.285,4.25 13.281,4.137 13.359,4.297C13.453,4.488 13.633,4.633 13.84,4.688C13.93,4.711 14.168,4.711 14.262,4.688C14.469,4.633 14.645,4.488 14.742,4.297C14.82,4.133 14.813,4.242 14.816,2.871L14.82,1.648L13.277,1.648ZM13.281,2.879M17.383,3.902C17.387,6.039 17.387,6.156 17.402,6.215C17.469,6.43 17.605,6.594 17.797,6.684C17.914,6.742 18.008,6.762 18.156,6.762C18.516,6.762 18.785,6.559 18.898,6.203C18.914,6.16 18.914,5.926 18.918,3.902L18.922,1.648L17.379,1.648ZM17.383,3.902M5.102,3.391L5.105,5.137L5.129,5.211C5.215,5.488 5.426,5.68 5.703,5.734C5.824,5.762 6.02,5.754 6.125,5.715C6.25,5.672 6.324,5.625 6.426,5.527C6.496,5.453 6.523,5.418 6.559,5.352C6.641,5.176 6.633,5.363 6.637,3.398L6.641,1.648L5.102,1.648ZM5.102,3.391M9.203,3.902C9.203,5.926 9.207,6.16 9.223,6.203C9.309,6.484 9.5,6.672 9.766,6.742C9.867,6.766 10.051,6.77 10.16,6.746C10.418,6.688 10.633,6.488 10.711,6.23L10.734,6.156L10.738,3.902L10.738,1.648L9.199,1.648ZM9.203,3.902 + M4.273,0.121C3.691,0.148 3.133,0.324 2.637,0.633C1.723,1.203 1.133,2.176 1.039,3.246C1.027,3.41 1.027,8.59 1.039,8.754C1.141,9.941 1.848,10.988 2.922,11.523C3.359,11.746 3.801,11.859 4.309,11.879C4.582,11.891 19.418,11.891 19.691,11.879C20.477,11.848 21.164,11.578 21.77,11.07C22.348,10.586 22.754,9.895 22.902,9.141C22.969,8.813 22.965,9.039 22.965,6C22.965,2.961 22.969,3.188 22.902,2.859C22.641,1.539 21.625,0.5 20.316,0.199C20.121,0.156 19.926,0.133 19.695,0.121C19.441,0.109 4.531,0.109 4.273,0.121ZM19.715,1.66C19.926,1.68 20.094,1.719 20.27,1.793C20.863,2.039 21.273,2.539 21.402,3.18L21.426,3.297L21.426,8.715L21.398,8.84C21.305,9.273 21.086,9.641 20.766,9.91C20.469,10.156 20.105,10.305 19.719,10.34C19.563,10.355 4.438,10.355 4.281,10.34C3.895,10.305 3.531,10.156 3.234,9.91C2.914,9.641 2.695,9.273 2.602,8.84L2.574,8.715L2.574,3.285L2.602,3.16C2.617,3.09 2.648,2.98 2.672,2.914C2.91,2.219 3.531,1.73 4.266,1.66C4.398,1.648 19.578,1.648 19.715,1.66ZM19.715,1.66 + M4.266,1.66C3.848,1.699 3.461,1.875 3.152,2.164C2.875,2.426 2.688,2.766 2.602,3.16L2.574,3.285L2.574,8.715L2.602,8.84C2.695,9.273 2.914,9.641 3.234,9.91C3.531,10.156 3.895,10.305 4.281,10.34C4.438,10.355 19.563,10.355 19.719,10.34C20.105,10.305 20.469,10.156 20.766,9.91C21.086,9.641 21.305,9.273 21.398,8.84L21.426,8.715L21.426,3.297L21.402,3.18C21.273,2.539 20.863,2.039 20.27,1.793C20.094,1.719 19.926,1.68 19.715,1.66C19.578,1.648 4.398,1.648 4.266,1.66ZM4.266,1.66 + M11.906,6.141C11.742,6.176 11.602,6.281 11.527,6.426C11.469,6.551 11.469,6.508 11.469,7.555C11.469,8.078 11.465,8.512 11.461,8.508C11.457,8.508 11.012,8.281 10.469,8.012C9.801,7.68 9.465,7.516 9.422,7.504C9.332,7.48 9.199,7.488 9.109,7.523C8.898,7.598 8.75,7.813 8.75,8.031C8.75,8.195 8.844,8.383 8.973,8.469C8.996,8.484 9.648,8.813 10.414,9.199C11.363,9.672 11.836,9.906 11.879,9.914C11.961,9.938 12.113,9.93 12.184,9.906C12.336,9.852 12.465,9.727 12.523,9.574L12.547,9.516L12.547,8.535C12.551,7.906 12.555,7.559 12.559,7.559C12.566,7.559 13.012,7.781 13.551,8.051C14.086,8.316 14.555,8.547 14.586,8.555C14.656,8.578 14.773,8.578 14.852,8.563C15.031,8.52 15.18,8.379 15.242,8.203C15.273,8.109 15.273,7.965 15.242,7.863C15.215,7.77 15.137,7.66 15.059,7.602C14.992,7.555 12.281,6.195 12.18,6.156C12.117,6.137 11.969,6.125 11.906,6.141ZM11.906,6.141 + M11.906,6.141C11.742,6.176 11.602,6.281 11.527,6.426C11.469,6.551 11.469,6.508 11.469,7.555C11.469,8.078 11.465,8.512 11.461,8.508C11.457,8.508 11.012,8.281 10.469,8.012C9.801,7.68 9.465,7.516 9.422,7.504C9.332,7.48 9.199,7.488 9.109,7.523C8.898,7.598 8.75,7.813 8.75,8.031C8.75,8.195 8.844,8.383 8.973,8.469C8.996,8.484 9.648,8.813 10.414,9.199C11.363,9.672 11.836,9.906 11.879,9.914C11.961,9.938 12.113,9.93 12.184,9.906C12.336,9.852 12.465,9.727 12.523,9.574L12.547,9.516L12.547,8.535C12.551,7.906 12.555,7.559 12.559,7.559C12.566,7.559 13.012,7.781 13.551,8.051C14.086,8.316 14.555,8.547 14.586,8.555C14.656,8.578 14.773,8.578 14.852,8.563C15.031,8.52 15.18,8.379 15.242,8.203C15.273,8.109 15.273,7.965 15.242,7.863C15.215,7.77 15.137,7.66 15.059,7.602C14.992,7.555 12.281,6.195 12.18,6.156C12.117,6.137 11.969,6.125 11.906,6.141ZM11.906,6.141 + + + M3.18,0.43C2.836,0.449 2.527,0.527 2.227,0.676C1.977,0.797 1.77,0.938 1.578,1.121C1.168,1.512 0.914,2.004 0.824,2.59C0.805,2.699 0.805,2.867 0.805,5.996C0.805,9.484 0.805,9.324 0.852,9.566C0.969,10.156 1.316,10.703 1.793,11.063C1.938,11.176 2.063,11.246 2.234,11.332C2.547,11.48 2.828,11.551 3.199,11.57C3.453,11.582 14.648,11.582 14.902,11.57C15.289,11.551 15.586,11.473 15.914,11.305C16.656,10.93 17.137,10.266 17.277,9.422C17.289,9.328 17.293,9.215 17.297,8.598L17.301,7.879L18.035,7.879C18.719,7.879 18.77,7.879 18.84,7.859C19.012,7.816 19.141,7.684 19.184,7.512C19.199,7.465 19.199,7.258 19.199,6C19.199,4.723 19.199,4.535 19.184,4.484C19.141,4.309 18.988,4.168 18.813,4.133C18.777,4.125 18.523,4.121 18.031,4.121L17.301,4.121L17.301,3.473C17.301,2.836 17.297,2.695 17.266,2.516C17.156,1.891 16.809,1.313 16.305,0.934C16.156,0.824 16.051,0.758 15.887,0.68C15.555,0.52 15.277,0.449 14.895,0.43C14.695,0.418 3.383,0.422 3.18,0.43ZM14.879,1.863C15.074,1.91 15.238,1.984 15.375,2.086C15.633,2.277 15.805,2.555 15.867,2.883C15.883,2.969 15.887,3.176 15.887,6C15.887,8.824 15.883,9.031 15.867,9.117C15.816,9.387 15.699,9.609 15.516,9.797C15.309,10 15.07,10.113 14.77,10.148C14.652,10.164 3.445,10.164 3.328,10.148C3.031,10.113 2.789,10 2.586,9.793C2.398,9.609 2.281,9.387 2.23,9.117C2.215,9.031 2.215,8.824 2.215,6C2.215,3.176 2.215,2.969 2.23,2.883C2.332,2.359 2.719,1.961 3.227,1.867C3.27,1.859 3.313,1.848 3.324,1.848C3.336,1.844 5.926,1.844 9.074,1.844C14.563,1.844 14.809,1.848 14.879,1.863ZM14.879,1.863 + M3.18,0.43C2.836,0.449 2.527,0.527 2.227,0.676C1.977,0.797 1.77,0.938 1.578,1.121C1.168,1.512 0.914,2.004 0.824,2.59C0.805,2.699 0.805,2.867 0.805,5.996C0.805,9.484 0.805,9.324 0.852,9.566C0.969,10.156 1.316,10.703 1.793,11.063C1.938,11.176 2.063,11.246 2.234,11.332C2.547,11.48 2.828,11.551 3.199,11.57C3.453,11.582 14.648,11.582 14.902,11.57C15.289,11.551 15.586,11.473 15.914,11.305C16.656,10.93 17.137,10.266 17.277,9.422C17.289,9.328 17.293,9.215 17.297,8.598L17.301,7.879L18.035,7.879C18.719,7.879 18.77,7.879 18.84,7.859C19.012,7.816 19.141,7.684 19.184,7.512C19.199,7.465 19.199,7.258 19.199,6C19.199,4.723 19.199,4.535 19.184,4.484C19.141,4.309 18.988,4.168 18.813,4.133C18.777,4.125 18.523,4.121 18.031,4.121L17.301,4.121L17.301,3.473C17.301,2.836 17.297,2.695 17.266,2.516C17.156,1.891 16.809,1.313 16.305,0.934C16.156,0.824 16.051,0.758 15.887,0.68C15.555,0.52 15.277,0.449 14.895,0.43C14.695,0.418 3.383,0.422 3.18,0.43ZM14.879,1.863C15.074,1.91 15.238,1.984 15.375,2.086C15.633,2.277 15.805,2.555 15.867,2.883C15.883,2.969 15.887,3.176 15.887,6C15.887,8.824 15.883,9.031 15.867,9.117C15.816,9.387 15.699,9.609 15.516,9.797C15.309,10 15.07,10.113 14.77,10.148C14.652,10.164 3.445,10.164 3.328,10.148C3.031,10.113 2.789,10 2.586,9.793C2.398,9.609 2.281,9.387 2.23,9.117C2.215,9.031 2.215,8.824 2.215,6C2.215,3.176 2.215,2.969 2.23,2.883C2.332,2.359 2.719,1.961 3.227,1.867C3.27,1.859 3.313,1.848 3.324,1.848C3.336,1.844 5.926,1.844 9.074,1.844C14.563,1.844 14.809,1.848 14.879,1.863ZM14.879,1.863 + M4.379,3.262C4.215,3.273 4.07,3.336 3.934,3.441C3.805,3.547 3.707,3.691 3.656,3.867L3.637,3.945L3.637,6C3.637,7.965 3.637,8.059 3.652,8.121C3.719,8.344 3.848,8.516 4.035,8.629C4.145,8.691 4.254,8.73 4.387,8.738C4.52,8.754 13.621,8.754 13.754,8.738C13.887,8.73 13.992,8.691 14.105,8.629C14.289,8.52 14.422,8.348 14.48,8.133L14.504,8.055L14.504,3.945L14.48,3.867C14.434,3.691 14.336,3.547 14.207,3.445C14.059,3.328 13.934,3.277 13.75,3.262C13.613,3.246 4.512,3.25 4.379,3.262ZM4.379,3.262 + M8.801,2.09C8.672,2.129 8.551,2.207 8.465,2.313C8.434,2.352 8.191,2.816 7.578,4.043C7.117,4.965 6.727,5.758 6.711,5.797C6.688,5.867 6.684,5.891 6.684,6C6.684,6.109 6.688,6.133 6.715,6.207C6.758,6.34 6.84,6.449 6.945,6.523C7.023,6.578 7.09,6.613 7.172,6.637C7.23,6.652 7.297,6.656 8.391,6.656L9.551,6.66L8.961,7.84C8.637,8.488 8.359,9.047 8.348,9.082C8.328,9.133 8.324,9.168 8.324,9.27C8.324,9.383 8.328,9.402 8.355,9.484C8.418,9.68 8.559,9.824 8.758,9.898C8.848,9.93 9.031,9.941 9.129,9.914C9.266,9.883 9.406,9.789 9.492,9.68C9.543,9.613 11.207,6.301 11.242,6.195C11.273,6.094 11.273,5.906 11.242,5.805C11.16,5.559 10.953,5.387 10.695,5.352C10.645,5.344 10.246,5.34 9.508,5.34L8.398,5.34L8.988,4.16C9.313,3.512 9.59,2.953 9.602,2.918C9.629,2.828 9.637,2.688 9.617,2.59C9.563,2.348 9.387,2.156 9.148,2.09C9.055,2.063 8.895,2.063 8.801,2.09ZM8.801,2.09 + M8.973,4.133C8.887,4.16 8.813,4.234 8.777,4.32C8.762,4.367 8.762,4.406 8.762,5.031L8.762,5.691L8.102,5.691C7.465,5.691 7.438,5.691 7.387,5.711C7.273,5.754 7.191,5.875 7.191,6C7.191,6.129 7.277,6.254 7.402,6.297C7.438,6.309 7.547,6.309 8.102,6.309L8.762,6.309L8.762,6.969C8.762,7.605 8.762,7.633 8.781,7.684C8.805,7.746 8.871,7.82 8.938,7.852C9.004,7.887 9.137,7.887 9.203,7.852C9.27,7.82 9.336,7.746 9.359,7.684C9.379,7.633 9.379,7.605 9.379,6.969L9.379,6.309L10.039,6.309C10.762,6.309 10.738,6.313 10.824,6.25C10.875,6.215 10.922,6.145 10.941,6.078C10.98,5.934 10.891,5.762 10.754,5.711C10.699,5.691 10.676,5.691 10.039,5.691L9.379,5.691L9.379,5.031C9.379,4.395 9.379,4.367 9.359,4.316C9.336,4.254 9.27,4.18 9.203,4.148C9.145,4.121 9.035,4.113 8.973,4.133ZM8.973,4.133 + \ No newline at end of file