diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/weardata/CustomWatchfaceFormat.kt b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/weardata/CustomWatchfaceFormat.kt index d48be7b3f9..e629eada28 100644 --- a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/weardata/CustomWatchfaceFormat.kt +++ b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/weardata/CustomWatchfaceFormat.kt @@ -238,7 +238,20 @@ enum class JsonKeys(val key: String) { ALLCAPS("allCaps"), DAYNAMEFORMAT("dayNameFormat"), MONTHFORMAT("monthFormat"), - BACKGROUND("background") + BACKGROUND("background"), // Background image for textView + LEFTOFFSET("leftOffset"), + TOPOFFSET("topOffset"), + ROTATIONOFFSET("rotationOffset"), + DYNDATA("dynData"), //Bloc of DynDatas definition, and DynData keyValue within view + VALUEKEY("valueKey"), // Indentify which value (default is View Value) + MINDATA("minData"), // Min data Value (default defined for each value, note unit mg/dl for all bg, deltas) + MAXDATA("maxData"), // Max data idem min data (note all value below min or above max will be considered as equal min or mas) + MINVALUE("minValue"), // min returned value (when data value equals minData + MAXVALUE("maxValue"), // + INVALIDVALUE("invalidValue"), + IMAGE("image"), + INVALIDIMAGE("invalidImage"), + INVALIDCOLOR("invalidColor") } enum class JsonKeyValues(val key: String) { @@ -260,7 +273,8 @@ enum class JsonKeyValues(val key: String) { BOLD("bold"), BOLD_ITALIC("bold_italic"), ITALIC("italic"), - BGCOLOR("bgColor") + BGCOLOR("bgColor"), + SGVLEVEL("sgvLevel") } class ZipWatchfaceFormat { diff --git a/wear/src/main/kotlin/app/aaps/wear/watchfaces/CustomWatchface.kt b/wear/src/main/kotlin/app/aaps/wear/watchfaces/CustomWatchface.kt index 9e8f5da1e5..0c30914061 100644 --- a/wear/src/main/kotlin/app/aaps/wear/watchfaces/CustomWatchface.kt +++ b/wear/src/main/kotlin/app/aaps/wear/watchfaces/CustomWatchface.kt @@ -48,10 +48,12 @@ import app.aaps.core.interfaces.rx.weardata.isEquals import app.aaps.wear.R import app.aaps.wear.databinding.ActivityCustomBinding import app.aaps.wear.watchfaces.utils.BaseWatchFace +import org.joda.time.DateTime import org.joda.time.TimeOfDay import org.json.JSONObject import java.io.ByteArrayOutputStream import javax.inject.Inject +import kotlin.math.floor @SuppressLint("UseCompatLoadingForDrawables") class CustomWatchface : BaseWatchFace() { @@ -63,6 +65,7 @@ class CustomWatchface : BaseWatchFace() { private val TEMPLATE_RESOLUTION = 400 private var lowBatColor = Color.RED private var resDataMap: CwfResDataMap = mutableMapOf() + private var jsonString = "" private val bgColor: Int get() = when (singleBg.sgvLevel) { 1L -> highColor @@ -102,18 +105,20 @@ class CustomWatchface : BaseWatchFace() { override fun setColorDark() { setWatchfaceStyle() - binding.sgv.setTextColor(bgColor) - binding.direction2.colorFilter = changeDrawableColor(bgColor) - - if (ageLevel != 1) + if ((ViewMap.SGV.dynData?.stepColor ?: 0) == 0) + binding.sgv.setTextColor(bgColor) + if ((ViewMap.DIRECTION.dynData?.stepColor ?: 0) == 0) + binding.direction2.colorFilter = changeDrawableColor(bgColor) + if (ageLevel != 1 && (ViewMap.TIMESTAMP.dynData?.stepColor ?: 0) == 0) binding.timestamp.setTextColor(ContextCompat.getColor(this, R.color.dark_TimestampOld)) - if (status.batteryLevel != 1) + if (status.batteryLevel != 1 && (ViewMap.UPLOADER_BATTERY.dynData?.stepColor ?: 0) == 0) binding.uploaderBattery.setTextColor(lowBatColor) - when (loopLevel) { - -1 -> binding.loop.setBackgroundResource(R.drawable.loop_grey_25) - 1 -> binding.loop.setBackgroundResource(R.drawable.loop_green_25) - else -> binding.loop.setBackgroundResource(R.drawable.loop_red_25) - } + if ((ViewMap.LOOP.dynData?.stepDraw ?: 0) == 0) // Apply automatic background image only if no dynData or no step images + when (loopLevel) { + -1 -> binding.loop.setBackgroundResource(R.drawable.loop_grey_25) + 1 -> binding.loop.setBackgroundResource(R.drawable.loop_green_25) + else -> binding.loop.setBackgroundResource(R.drawable.loop_red_25) + } setupCharts() } @@ -147,11 +152,13 @@ class CustomWatchface : BaseWatchFace() { updatePref(it.customWatchfaceData.metadata) try { val json = JSONObject(it.customWatchfaceData.json) - if (!resDataMap.isEquals(it.customWatchfaceData.resDatas)) { + if (!resDataMap.isEquals(it.customWatchfaceData.resDatas) || jsonString != it.customWatchfaceData.json) { resDataMap = it.customWatchfaceData.resDatas + jsonString = it.customWatchfaceData.json FontMap.init(this) ViewMap.init(this) TrendArrowMap.init(this) + DynProvider.init(json.optJSONObject(DYNDATA.key)) } enableSecond = json.optBoolean(ENABLESECOND.key) && sp.getBoolean(R.string.key_show_seconds, true) highColor = getColor(json.optString(HIGHCOLOR.key), ContextCompat.getColor(this, R.color.dark_highColor)) @@ -174,6 +181,7 @@ class CustomWatchface : BaseWatchFace() { when (view) { is TextView -> viewMap.customizeTextView(view, viewJson) is ImageView -> viewMap.customizeImageView(view, viewJson) + is lecho.lib.hellocharts.view.LineChartView -> viewMap.customizeGraphView(view, viewJson) else -> viewMap.customizeViewCommon(view, viewJson) } } ?: apply { @@ -446,6 +454,7 @@ class CustomWatchface : BaseWatchFace() { var height = 0 var left = 0 var top = 0 + var dynData: DynProvider? = null var rangeCustom: Drawable? = null get() = field ?: customDrawable?.let { cd -> cwf.resDataMap[cd.fileName]?.toDrawable(cwf.resources).also { rangeCustom = it } } var highCustom: Drawable? = null @@ -454,7 +463,7 @@ class CustomWatchface : BaseWatchFace() { get() = field ?: customLow?.let { cd -> cwf.resDataMap[cd.fileName]?.toDrawable(cwf.resources).also { lowCustom = it } } var textDrawable: Drawable? = null val drawable: Drawable? - get() = when (cwf.singleBg.sgvLevel) { + get() = dynData?.getDrawable() ?: when (cwf.singleBg.sgvLevel) { 1L -> highCustom ?: rangeCustom 0L -> rangeCustom -1L -> lowCustom ?: rangeCustom @@ -472,58 +481,68 @@ class CustomWatchface : BaseWatchFace() { left = (viewJson.optInt(LEFTMARGIN.key) * cwf.zoomFactor).toInt() top = (viewJson.optInt(TOPMARGIN.key) * cwf.zoomFactor).toInt() val params = FrameLayout.LayoutParams(width, height) - params.topMargin = top - params.leftMargin = left + dynData = DynProvider.getDyn(cwf, viewJson.optString(DYNDATA.key),width, height, key) + val topOffset = if (viewJson.optBoolean(TOPOFFSET.key, false)) dynData?.getTopOffset() ?:0 else 0 + params.topMargin = top + topOffset + val leftOffset = if (viewJson.optBoolean(LEFTOFFSET.key, false)) dynData?.getLeftOffset() ?:0 else 0 + params.leftMargin = left + leftOffset view.layoutParams = params view.visibility = cwf.setVisibility(viewJson.optString(VISIBILITY.key, JsonKeyValues.GONE.key), visibility()) + val rotationOffset = if (viewJson.optBoolean(ROTATIONOFFSET.key, false)) dynData?.getRotationOffset()?.toFloat() ?:0F else 0F + view.rotation = viewJson.optInt(ROTATION.key).toFloat() + rotationOffset } fun customizeTextView(view: TextView, viewJson: JSONObject) { customizeViewCommon(view, viewJson) - view.rotation = viewJson.optInt(ROTATION.key).toFloat() view.setTextSize(TypedValue.COMPLEX_UNIT_PX, (viewJson.optInt(TEXTSIZE.key, 22) * cwf.zoomFactor).toFloat()) view.gravity = GravityMap.gravity(viewJson.optString(GRAVITY.key, GravityMap.CENTER.key)) view.setTypeface( FontMap.font(viewJson.optString(FONT.key, FontMap.DEFAULT.key)), StyleMap.style(viewJson.optString(FONTSTYLE.key, StyleMap.NORMAL.key)) ) - view.setTextColor(cwf.getColor(viewJson.optString(FONTCOLOR.key))) + view.setTextColor(dynData?.getColor() ?: cwf.getColor(viewJson.optString(FONTCOLOR.key))) view.isAllCaps = viewJson.optBoolean(ALLCAPS.key) if (viewJson.has(TEXTVALUE.key)) view.text = viewJson.optString(TEXTVALUE.key) - view.background = textDrawable(viewJson) + view.background = dynData?.getDrawable() ?: textDrawable(viewJson) } fun customizeImageView(view: ImageView, viewJson: JSONObject) { customizeViewCommon(view, viewJson) view.clearColorFilter() drawable?.let { - if (viewJson.has(COLOR.key)) // Note only works on bitmap (png or jpg) or xml included into res, not for svg files - it.colorFilter = cwf.changeDrawableColor(cwf.getColor(viewJson.optString(COLOR.key))) + if (viewJson.has(COLOR.key) || (dynData?.stepColor ?: 0) > 0) // Note only works on bitmap (png or jpg) or xml included into res, not for svg files + it.colorFilter = cwf.changeDrawableColor(dynData?.getColor() ?: cwf.getColor(viewJson.optString(COLOR.key))) else it.clearColorFilter() view.setImageDrawable(it) } ?: apply { view.setImageDrawable(defaultDrawable?.let { cwf.resources.getDrawable(it) }) - if (viewJson.has(COLOR.key)) - view.setColorFilter(cwf.getColor(viewJson.optString(COLOR.key))) + if (viewJson.has(COLOR.key) || (dynData?.stepColor ?: 0) > 0) + view.setColorFilter(dynData?.getColor() ?: cwf.getColor(viewJson.optString(COLOR.key))) else view.clearColorFilter() } } + + fun customizeGraphView(view: lecho.lib.hellocharts.view.LineChartView, viewJson: JSONObject) { + customizeViewCommon(view, viewJson) + view.setBackgroundColor(dynData?.getColor() ?: cwf.getColor(viewJson.optString(COLOR.key, "#0000000000"),Color.TRANSPARENT)) + view.background = dynData?.getDrawable() ?: textDrawable(viewJson) + } } - private enum class TrendArrowMap(val symbol: String, @DrawableRes val icon: Int, val customDrawable: ResFileMap?) { - NONE("??", R.drawable.ic_invalid, ResFileMap.ARROW_NONE), - TRIPLE_UP("X", R.drawable.ic_doubleup, ResFileMap.ARROW_DOUBLE_UP), - DOUBLE_UP("\u21c8", R.drawable.ic_doubleup, ResFileMap.ARROW_DOUBLE_UP), - SINGLE_UP("\u2191", R.drawable.ic_singleup, ResFileMap.ARROW_SINGLE_UP), - FORTY_FIVE_UP("\u2197", R.drawable.ic_fortyfiveup, ResFileMap.ARROW_FORTY_FIVE_UP), - FLAT("\u2192", R.drawable.ic_flat, ResFileMap.ARROW_FLAT), - FORTY_FIVE_DOWN("\u2198", R.drawable.ic_fortyfivedown, ResFileMap.ARROW_FORTY_FIVE_DOWN), - SINGLE_DOWN("\u2193", R.drawable.ic_singledown, ResFileMap.ARROW_SINGLE_DOWN), - DOUBLE_DOWN("\u21ca", R.drawable.ic_doubledown, ResFileMap.ARROW_DOUBLE_DOWN), - TRIPLE_DOWN("X", R.drawable.ic_doubledown, ResFileMap.ARROW_DOUBLE_DOWN); + private enum class TrendArrowMap(val symbol: String, @DrawableRes val icon: Int, val customDrawable: ResFileMap?, val dynValue: Double) { + NONE("??", R.drawable.ic_invalid, ResFileMap.ARROW_NONE, 0.0), + TRIPLE_UP("X", R.drawable.ic_doubleup, ResFileMap.ARROW_DOUBLE_UP, 7.0), + DOUBLE_UP("\u21c8", R.drawable.ic_doubleup, ResFileMap.ARROW_DOUBLE_UP, 7.0), + SINGLE_UP("\u2191", R.drawable.ic_singleup, ResFileMap.ARROW_SINGLE_UP, 6.0), + FORTY_FIVE_UP("\u2197", R.drawable.ic_fortyfiveup, ResFileMap.ARROW_FORTY_FIVE_UP, 5.0), + FLAT("\u2192", R.drawable.ic_flat, ResFileMap.ARROW_FLAT, 4.0), + FORTY_FIVE_DOWN("\u2198", R.drawable.ic_fortyfivedown, ResFileMap.ARROW_FORTY_FIVE_DOWN, 3.0), + SINGLE_DOWN("\u2193", R.drawable.ic_singledown, ResFileMap.ARROW_SINGLE_DOWN, 2.0), + DOUBLE_DOWN("\u21ca", R.drawable.ic_doubledown, ResFileMap.ARROW_DOUBLE_DOWN, 2.0), + TRIPLE_DOWN("X", R.drawable.ic_doubledown, ResFileMap.ARROW_DOUBLE_DOWN, 1.0); companion object { @@ -532,6 +551,7 @@ class CustomWatchface : BaseWatchFace() { it.arrowCustom = null } fun drawable() = values().firstOrNull { it.symbol == it.cwf.singleBg.slopeArrow }?.arrowCustom ?: NONE.arrowCustom + fun value() = values().firstOrNull { it.symbol == it.cwf.singleBg.slopeArrow }?.dynValue ?: NONE.dynValue } lateinit var cwf: CustomWatchface @@ -618,6 +638,129 @@ class CustomWatchface : BaseWatchFace() { SHOW_LOOP_STATUS(CwfMetadataKey.CWF_PREF_WATCH_SHOW_LOOP_STATUS.key, R.string.key_show_external_status), SHOW_WEEK_NUMBER(CwfMetadataKey.CWF_PREF_WATCH_SHOW_WEEK_NUMBER.key, R.string.key_show_week_number) } + + private enum class ValueMap(val key: String, val min: Double, val max: Double) { + SGV(ViewKeys.SGV.key, 39.0, 400.0), + SGVLEVEL(JsonKeyValues.SGVLEVEL.key, -1.0, 1.0), + DIRECTION(ViewKeys.DIRECTION.key, 1.0, 7.0), + DELTA(ViewKeys.DELTA.key, -25.0, 25.0), + AVG_DELTA(ViewKeys.AVG_DELTA.key, -25.0, 25.0), + UPLOADER_BATTERY(ViewKeys.UPLOADER_BATTERY.key, 0.0, 100.0), + RIG_BATTERY(ViewKeys.RIG_BATTERY.key, 0.0, 100.0), + TIMESTAMP(ViewKeys.TIMESTAMP.key, 0.0, 60.0), + LOOP(ViewKeys.LOOP.key, 0.0, 28.0), + DAY(ViewKeys.DAY.key, 1.0, 31.0), + DAY_NAME(ViewKeys.DAY_NAME.key, 1.0, 7.0), + MONTH(ViewKeys.MONTH.key, 1.0, 12.0), + WEEKNUMBER(ViewKeys.WEEKNUMBER.key, 1.0, 53.0); + + fun dynValue(dataValue: Double, dataRange: DataRange, valueRange: DataRange): Int = when { + dataValue < dataRange.minData -> dataRange.minData + dataValue > dataRange.maxData -> dataRange.maxData + else -> dataValue + }.let { + if (dataRange.minData != dataRange.maxData) + (valueRange.minData + (it - dataRange.minData) * (valueRange.maxData - valueRange.minData) / (dataRange.maxData - dataRange.minData)).toInt() + else it.toInt() + } + + fun stepValue(dataValue: Double, range: DataRange, step: Int): Int = step(dataValue, range, step) + private fun step(dataValue: Double, dataRange: DataRange, step: Int): Int = when { + dataValue < dataRange.minData -> dataRange.minData + dataValue >= dataRange.maxData -> dataRange.maxData * 0.9999 // to avoid dataValue == maxData and be out of range + else -> dataValue + }.let { if (dataRange.minData != dataRange.maxData) (1 + ((it - dataRange.minData) * step) / (dataRange.maxData - dataRange.minData)).toInt() else 0 } + + companion object { + + fun fromKey(key: String) = values().firstOrNull { it.key == key } + } + } + + private class DynProvider(val cwf: CustomWatchface, val dataJson: JSONObject, val valueMap: ValueMap, val width: Int, val height: Int) { + + private val dynDrawable = mutableMapOf() + private val dynColor = mutableMapOf() + private var dataRange: DataRange? = null + private var topRange: DataRange? = null + private var leftRange: DataRange? = null + private var rotationRange: DataRange? = null + val stepDraw: Int + get() = dynDrawable.size - 1 + val stepColor: Int + get() = dynColor.size - 1 + + val dataValue: Double? + get() = when (valueMap) { + ValueMap.SGV -> if (cwf.singleBg.sgvString != "---") cwf.singleBg.sgv else null + ValueMap.SGVLEVEL -> if (cwf.singleBg.sgvString != "---") cwf.singleBg.sgvLevel.toDouble() else null + ValueMap.DIRECTION -> TrendArrowMap.value() + ValueMap.DELTA -> cwf.singleBg.deltaMgdl + ValueMap.AVG_DELTA -> cwf.singleBg.avgDeltaMgdl + ValueMap.RIG_BATTERY -> cwf.status.rigBattery.replace("%", "").toDoubleOrNull() + ValueMap.UPLOADER_BATTERY -> cwf.status.battery.replace("%", "").toDoubleOrNull() + ValueMap.LOOP -> if (cwf.status.openApsStatus != -1L) ((System.currentTimeMillis() - cwf.status.openApsStatus) / 1000 / 60).toDouble() else null + ValueMap.TIMESTAMP -> if (cwf.singleBg.timeStamp != 0L) floor(cwf.timeSince() / (1000 * 60)) else null + ValueMap.DAY -> DateTime().dayOfMonth.toDouble() + ValueMap.DAY_NAME -> DateTime().dayOfWeek.toDouble() + ValueMap.MONTH -> DateTime().monthOfYear.toDouble() + ValueMap.WEEKNUMBER -> DateTime().weekOfWeekyear.toDouble() + } + + fun getTopOffset(): Int = dataRange?.let { dataRange -> topRange?.let { topRange -> dataValue?.let { valueMap.dynValue(it, dataRange, topRange) } ?: topRange.invalidData } } ?: 0 + fun getLeftOffset(): Int = dataRange?.let { dataRange -> leftRange?.let { leftRange -> dataValue?.let { valueMap.dynValue(it, dataRange, leftRange) } ?: leftRange.invalidData } } ?: 0 + fun getRotationOffset(): Int = dataRange?.let { dataRange -> rotationRange?.let { rotRange -> dataValue?.let { valueMap.dynValue(it, dataRange, rotRange) } ?: rotRange.invalidData } } ?: 0 + fun getDrawable() = dataRange?.let { dataRange -> dataValue?.let { dynDrawable[valueMap.stepValue(it, dataRange, stepDraw)] } ?: dynDrawable[0] } + fun getColor() = if (stepColor > 0) dataRange?.let { dataRange -> dataValue?.let { dynColor[valueMap.stepValue(it, dataRange, stepColor)] } ?: dynColor[0] } else null + private fun load() { + dynDrawable[0] = dataJson.optString(INVALIDIMAGE.key)?.let { cwf.resDataMap[it]?.toDrawable(cwf.resources, width, height) } + var idx = 1 + while (dataJson.has("${IMAGE.key}$idx")) { + cwf.resDataMap[dataJson.optString("${IMAGE.key}$idx")]?.toDrawable(cwf.resources, width, height).also { dynDrawable[idx] = it } + idx++ + } + dynColor[0] = cwf.getColor(dataJson.optString(INVALIDCOLOR.key)) + idx = 1 + while (dataJson.has("${COLOR.key}$idx")) { + dynColor[idx] = cwf.getColor(dataJson.optString("${COLOR.key}$idx")) + idx++ + } + DataRange(dataJson.optDouble(MINDATA.key, valueMap.min), dataJson.optDouble(MAXDATA.key, valueMap.max)).let { defaultRange -> + dataRange = defaultRange + topRange = parseDataRange(dataJson.optJSONObject(TOPOFFSET.key), defaultRange) + leftRange = parseDataRange(dataJson.optJSONObject(LEFTOFFSET.key), defaultRange) + rotationRange = parseDataRange(dataJson.optJSONObject(ROTATIONOFFSET.key), defaultRange) + } + } + + companion object { + + val dynData = mutableMapOf() + var dynJson: JSONObject? = null + fun init(dynJson: JSONObject?) { + this.dynJson = dynJson + dynData.clear() + } + + fun getDyn(cwf: CustomWatchface, key: String, width: Int, height: Int, defaultViewKey: String): DynProvider? = dynData["${defaultViewKey}_$key"] + ?: dynJson?.optJSONObject(key)?.let { dataJson -> + ValueMap.fromKey(dataJson.optString(VALUEKEY.key, defaultViewKey))?.let { valueMap -> + DynProvider(cwf, dataJson, valueMap, width, height).also { it.load() } + } + }?.also { dynData["${defaultViewKey}_$key"] = it } + + private fun parseDataRange(json: JSONObject?, defaultData: DataRange) = + json?.let { + DataRange( + minData = it.optDouble(MINVALUE.key, defaultData.minData), + maxData = it.optDouble(MAXVALUE.key, defaultData.maxData), + invalidData = it.optInt(INVALIDVALUE.key, defaultData.invalidData) + ) + } ?: defaultData + } + } + + private class DataRange (val minData: Double, val maxData: Double, val invalidData: Int = 0) }