From 025c0b3ee87fe3f7481a7e9a74b9cb47c3f0a5c0 Mon Sep 17 00:00:00 2001 From: pachi81 Date: Fri, 20 Dec 2024 17:17:55 +0100 Subject: [PATCH 01/17] add chart to phone --- common/build.gradle | 1 + .../glucodatahandler/common/ReceiveData.kt | 16 + .../common/chart/ChartData.kt | 23 ++ .../common/chart/ChartViewer.kt | 328 ++++++++++++++++++ .../common/chart/CustomBubbleMarker.kt | 125 +++++++ .../common/chart/CustomMarkerView.kt | 36 ++ .../common/chart/GlucoseFormatter.kt | 18 + .../common/chart/TimeAxisRenderer.kt | 150 ++++++++ .../common/chart/TimeValueFormatter.kt | 106 ++++++ .../common/notifier/NotifySource.kt | 3 +- .../common/utils/DummyGraphData.kt | 24 ++ common/src/main/res/drawable-nodpi/marker.png | Bin 0 -> 3389 bytes .../res/layout/custom_marker_view_layout.xml | 39 +++ common/src/main/res/layout/marker_layout.xml | 31 ++ common/src/main/res/values-night/colors.xml | 20 ++ common/src/main/res/values/colors.xml | 21 ++ mobile/build.gradle | 1 + .../glucodatahandler/MainActivity.kt | 20 +- mobile/src/main/res/layout/activity_main.xml | 9 +- settings.gradle | 1 + 20 files changed, 969 insertions(+), 3 deletions(-) create mode 100644 common/src/main/java/de/michelinside/glucodatahandler/common/chart/ChartData.kt create mode 100644 common/src/main/java/de/michelinside/glucodatahandler/common/chart/ChartViewer.kt create mode 100644 common/src/main/java/de/michelinside/glucodatahandler/common/chart/CustomBubbleMarker.kt create mode 100644 common/src/main/java/de/michelinside/glucodatahandler/common/chart/CustomMarkerView.kt create mode 100644 common/src/main/java/de/michelinside/glucodatahandler/common/chart/GlucoseFormatter.kt create mode 100644 common/src/main/java/de/michelinside/glucodatahandler/common/chart/TimeAxisRenderer.kt create mode 100644 common/src/main/java/de/michelinside/glucodatahandler/common/chart/TimeValueFormatter.kt create mode 100644 common/src/main/java/de/michelinside/glucodatahandler/common/utils/DummyGraphData.kt create mode 100644 common/src/main/res/drawable-nodpi/marker.png create mode 100644 common/src/main/res/layout/custom_marker_view_layout.xml create mode 100644 common/src/main/res/layout/marker_layout.xml create mode 100644 common/src/main/res/values-night/colors.xml create mode 100644 common/src/main/res/values/colors.xml diff --git a/common/build.gradle b/common/build.gradle index b6a16a7c7..d36868f92 100644 --- a/common/build.gradle +++ b/common/build.gradle @@ -48,6 +48,7 @@ dependencies { implementation 'com.google.android.gms:play-services-wearable:19.0.0' implementation 'androidx.work:work-runtime:2.9.1' implementation 'androidx.preference:preference-ktx:1.2.1' + implementation 'com.github.PhilJay:MPAndroidChart:v3.1.0' testImplementation 'junit:junit:4.13.2' testImplementation 'io.mockk:mockk:1.13.12' androidTestImplementation 'androidx.test.ext:junit:1.2.1' diff --git a/common/src/main/java/de/michelinside/glucodatahandler/common/ReceiveData.kt b/common/src/main/java/de/michelinside/glucodatahandler/common/ReceiveData.kt index 939797194..544cb8d20 100644 --- a/common/src/main/java/de/michelinside/glucodatahandler/common/ReceiveData.kt +++ b/common/src/main/java/de/michelinside/glucodatahandler/common/ReceiveData.kt @@ -64,6 +64,7 @@ object ReceiveData: SharedPreferences.OnSharedPreferenceChangeListener { var cob: Float = Float.NaN var iobCobTime: Long = 0 private var lowValue: Float = 70F + val lowRaw: Float get() = lowValue val low: Float get() { if(isMmol && lowValue > 0F) // mmol/l { @@ -72,6 +73,7 @@ object ReceiveData: SharedPreferences.OnSharedPreferenceChangeListener { return lowValue } private var highValue: Float = 240F + val highRaw: Float get() = highValue val high: Float get() { if(isMmol && highValue > 0F) // mmol/l { @@ -80,6 +82,7 @@ object ReceiveData: SharedPreferences.OnSharedPreferenceChangeListener { return highValue } private var targetMinValue = 90F + val targetMinRaw: Float get() = targetMinValue val targetMin: Float get() { if(isMmol) // mmol/l { @@ -88,6 +91,7 @@ object ReceiveData: SharedPreferences.OnSharedPreferenceChangeListener { return targetMinValue } private var targetMaxValue = 165F + val targetMaxRaw: Float get() = targetMaxValue val targetMax: Float get() { if(isMmol) // mmol/l { @@ -319,6 +323,18 @@ object ReceiveData: SharedPreferences.OnSharedPreferenceChangeListener { return AlarmType.OK } + fun getValueColor(rawValue: Int): Int { + if(highValue>0F && rawValue >= highValue) + return getAlarmTypeColor(AlarmType.VERY_HIGH) + if(lowValue>0F && rawValue <= lowValue) + return getAlarmTypeColor(AlarmType.VERY_LOW) + if(targetMinValue>0F && rawValue < targetMinValue) + return getAlarmTypeColor(AlarmType.LOW) + if(targetMaxValue>0F && rawValue > targetMaxValue) + return getAlarmTypeColor(AlarmType.HIGH) + return getAlarmTypeColor(AlarmType.OK) + } + private fun calculateAlarm(): Int { alarm = 0 // reset to calculate again val curAlarmType = getAlarmType() diff --git a/common/src/main/java/de/michelinside/glucodatahandler/common/chart/ChartData.kt b/common/src/main/java/de/michelinside/glucodatahandler/common/chart/ChartData.kt new file mode 100644 index 000000000..07a184203 --- /dev/null +++ b/common/src/main/java/de/michelinside/glucodatahandler/common/chart/ChartData.kt @@ -0,0 +1,23 @@ +package de.michelinside.glucodatahandler.common.chart + +import com.github.mikephil.charting.data.Entry + +object ChartData { + fun getData(count: Int): ArrayList { + val entries = ArrayList() + val min = 50F + val max = 300F + var value = min + var delta = 10F + for (i in 0 until count) { + entries.add(Entry(i.toFloat(), value)) + value += delta + if(value >= max) + delta = -10F + if(value <= min) + delta = +10F + } + return entries + } + +} \ No newline at end of file diff --git a/common/src/main/java/de/michelinside/glucodatahandler/common/chart/ChartViewer.kt b/common/src/main/java/de/michelinside/glucodatahandler/common/chart/ChartViewer.kt new file mode 100644 index 000000000..3a248b93a --- /dev/null +++ b/common/src/main/java/de/michelinside/glucodatahandler/common/chart/ChartViewer.kt @@ -0,0 +1,328 @@ +package de.michelinside.glucodatahandler.common.chart + +import android.content.Context +import android.content.SharedPreferences +import android.graphics.Bitmap +import android.os.Bundle +import android.util.Log +import com.github.mikephil.charting.charts.LineChart +import com.github.mikephil.charting.components.LimitLine +import com.github.mikephil.charting.components.XAxis +import com.github.mikephil.charting.components.YAxis +import com.github.mikephil.charting.data.Entry +import com.github.mikephil.charting.data.LineData +import com.github.mikephil.charting.data.LineDataSet +import de.michelinside.glucodatahandler.common.Constants +import de.michelinside.glucodatahandler.common.R +import de.michelinside.glucodatahandler.common.ReceiveData +import de.michelinside.glucodatahandler.common.notifier.InternalNotifier +import de.michelinside.glucodatahandler.common.notifier.NotifierInterface +import de.michelinside.glucodatahandler.common.notifier.NotifySource +import de.michelinside.glucodatahandler.common.utils.DummyGraphData +import java.util.concurrent.TimeUnit + + +open class ChartViewer(private val chart: LineChart, val context: Context, private val forBitmap: Boolean = false): NotifierInterface, + SharedPreferences.OnSharedPreferenceChangeListener { + private val LOG_ID = "GDH.Chart.Handler" + private var created = false + private val sharedPref = context.getSharedPreferences(Constants.SHARED_PREF_TAG, Context.MODE_PRIVATE) + private var init = false + + private var graphPrefList = mutableSetOf( + Constants.SHARED_PREF_LOW_GLUCOSE, + Constants.SHARED_PREF_HIGH_GLUCOSE, + Constants.SHARED_PREF_TARGET_MIN, + Constants.SHARED_PREF_TARGET_MAX, + Constants.SHARED_PREF_COLOR_ALARM, + Constants.SHARED_PREF_COLOR_OUT_OF_RANGE, + Constants.SHARED_PREF_COLOR_OK, + Constants.SHARED_PREF_SHOW_OTHER_UNIT + ) + + private fun init() { + if(!init) { + Log.d(LOG_ID, "init") + sharedPref.registerOnSharedPreferenceChangeListener(this) + InternalNotifier.addNotifier(context, this, mutableSetOf(NotifySource.MESSAGECLIENT, NotifySource.BROADCAST)) + } + } + + fun create() { + Log.d(LOG_ID, "create") + try { + init() + resetChart() + initChart() + initXaxis() + initYaxis() + initData() + created = true + //demo() + } catch (exc: Exception) { + Log.e(LOG_ID, "create exception: " + exc.message.toString() ) + } + } + + fun close() { + Log.d(LOG_ID, "close") + if(init) { + InternalNotifier.remNotifier(context, this) + sharedPref.unregisterOnSharedPreferenceChangeListener(this) + init = false + } + + } + + fun isRight(): Boolean { + return chart.highestVisibleX.toInt() == chart.xChartMax.toInt() + } + + fun isLeft(): Boolean { + return chart.lowestVisibleX.toInt() == chart.xChartMin.toInt() + } + + private fun resetChart() { + Log.v(LOG_ID, "resetChart") + chart.fitScreen() + chart.data?.clearValues() + chart.axisRight.removeAllLimitLines() + chart.xAxis.valueFormatter = null + chart.axisRight.valueFormatter = null + chart.axisLeft.valueFormatter = null + chart.marker = null + chart.notifyDataSetChanged() + chart.clear() + chart.invalidate() + } + + private fun initXaxis() { + Log.v(LOG_ID, "initXaxis") + chart.xAxis.isEnabled = !forBitmap + if(chart.xAxis.isEnabled) { + chart.xAxis.position = XAxis.XAxisPosition.BOTTOM + chart.xAxis.setDrawGridLines(true) + chart.xAxis.enableGridDashedLine(10F, 10F, 0F) + chart.xAxis.valueFormatter = TimeValueFormatter(chart) + chart.setXAxisRenderer(TimeAxisRenderer(chart)) + } + } + + private fun initYaxis() { + Log.v(LOG_ID, "initYaxis") + chart.axisRight.setDrawAxisLine(!forBitmap) + chart.axisRight.setDrawLabels(!forBitmap) + if(!forBitmap) + chart.axisRight.valueFormatter = GlucoseFormatter() + chart.axisRight.setDrawZeroLine(false) + chart.axisRight.setDrawGridLines(false) + chart.axisLeft.isEnabled = !forBitmap && showOtherUnit() + if(chart.axisLeft.isEnabled) { + chart.axisLeft.valueFormatter = GlucoseFormatter(true) + chart.axisLeft.setDrawZeroLine(false) + chart.axisLeft.setDrawGridLines(false) + } + } + + private fun createLimitLine(limit: Float): LimitLine { + val line = LimitLine(limit) + line.lineColor = context.resources.getColor(R.color.gray) + return line + } + + private fun initChart() { + Log.v(LOG_ID, "initChart") + chart.setTouchEnabled(!forBitmap) + //chart.setPinchZoom(true) + val dataSet = LineDataSet(ArrayList(), "Glucose Values") + dataSet.valueFormatter = GlucoseFormatter() + dataSet.colors = mutableListOf() + dataSet.circleColors = mutableListOf() + dataSet.lineWidth = 2F + dataSet.circleRadius = 3F + dataSet.setDrawValues(false) + dataSet.setDrawCircleHole(false) + dataSet.axisDependency = YAxis.AxisDependency.RIGHT + chart.data = LineData(dataSet) + + chart.isAutoScaleMinMaxEnabled = false + chart.legend.isEnabled = false + chart.description.isEnabled = false + chart.isScaleYEnabled = false + chart.xAxis.textColor = context.resources.getColor(R.color.text_color) + if(!forBitmap) { + val mMarker = CustomBubbleMarker(context) + mMarker.chartView = chart + chart.marker = mMarker + } + chart.axisRight.removeAllLimitLines() + + if(ReceiveData.highRaw > 0F) + chart.axisRight.addLimitLine(createLimitLine(ReceiveData.highRaw)) + if(ReceiveData.lowRaw > 0F) + chart.axisRight.addLimitLine(createLimitLine(ReceiveData.lowRaw)) + if(ReceiveData.targetMinRaw > 0F) + chart.axisRight.addLimitLine(createLimitLine(ReceiveData.targetMinRaw)) + if(ReceiveData.targetMaxRaw > 0F) + chart.axisRight.addLimitLine(createLimitLine(ReceiveData.targetMaxRaw)) + chart.axisRight.textColor = context.resources.getColor(R.color.text_color) + chart.axisRight.axisMinimum = Constants.GLUCOSE_MIN_VALUE.toFloat()-10F + chart.axisRight.axisMaximum = ReceiveData.highRaw+10 + chart.axisLeft.axisMinimum = chart.axisRight.axisMinimum + chart.axisLeft.axisMaximum = chart.axisRight.axisMaximum + chart.isScaleXEnabled = false + chart.invalidate() + } + + private fun initData() { + val graphData = DummyGraphData.create(4, min = 160, max = 260, stepMinute = 5) + if(graphData.values.isNotEmpty()) { + val values: ArrayList = ArrayList() + graphData.forEach { (t, u) -> + values.add(Entry(TimeValueFormatter.to_chart_x(t), u.toFloat())) + } + addEntries(values) + } else if(ReceiveData.time > 0) { + update() + } + } + + private fun addEntries(values: ArrayList) { + Log.d(LOG_ID, "Add ${values.size} entries") + if(values.isEmpty()) + return + + val dataSet = chart.data.getDataSetByIndex(0) as LineDataSet + val right = isRight() + val left = isLeft() + Log.v(LOG_ID, "Min: ${chart.xAxis.axisMinimum} - visible: ${chart.lowestVisibleX} - Max: ${chart.xAxis.axisMaximum} - visible: ${chart.highestVisibleX} - isLeft: ${left} - isRight: ${right}" ) + var diffTimeMin = TimeUnit.MILLISECONDS.toMinutes(TimeValueFormatter.from_chart_x(chart.highestVisibleX).time - TimeValueFormatter.from_chart_x(chart.lowestVisibleX).time) + Log.d(LOG_ID, "Diff-Time: ${diffTimeMin} minutes") + var added = false + for (i in 0 until values.size) { + val entry = values[i] + if(dataSet.values.isEmpty() || dataSet.values.last().x < entry.x) { + added = true + dataSet.addEntry(entry) + val color = ReceiveData.getValueColor(entry.y.toInt()) + dataSet.addColor(color) + dataSet.circleColors.add(color) + dataSet.notifyDataSetChanged() + + if(chart.axisRight.axisMinimum > (entry.y-10F)) { + chart.axisRight.axisMinimum = entry.y-10F + chart.axisLeft.axisMinimum = chart.axisRight.axisMinimum + } + if(chart.axisRight.axisMaximum < (entry.y+10F)) { + chart.axisRight.axisMaximum = entry.y+10F + chart.axisLeft.axisMaximum = chart.axisRight.axisMaximum + } + } + } + if(added) { + chart.data = LineData(dataSet) + chart.notifyDataSetChanged() + val newDiffTime = TimeUnit.MILLISECONDS.toMinutes( TimeValueFormatter.from_chart_x(values.last().x).time - TimeValueFormatter.from_chart_x(chart.lowestVisibleX).time) + + var setXRange = false + if(!forBitmap && !chart.isScaleXEnabled && newDiffTime >= 90) { + Log.d(LOG_ID, "Enable X scale") + chart.isScaleXEnabled = true + setXRange = true + } + + if(right && left && diffTimeMin < 240L && newDiffTime >= 240L) { + Log.v(LOG_ID, "Set 4 hours diff time") + diffTimeMin = 240L + } + + if(right) { + if(diffTimeMin >= 240L || !left) { + Log.d(LOG_ID, "Fix interval: $diffTimeMin") + chart.setVisibleXRangeMaximum(diffTimeMin.toFloat()) + setXRange = true + } + Log.v(LOG_ID, "moveViewToX ${values.last().x}") + chart.moveViewToX(values.last().x) + } else { + Log.v(LOG_ID, "Invalidate chart") + chart.setVisibleXRangeMaximum(diffTimeMin.toFloat()) + setXRange = true + chart.invalidate() + } + + if(setXRange) { + chart.setVisibleXRangeMinimum(60F) + chart.setVisibleXRangeMaximum(60F*24F) + } + notifyChanged() + } + } + + private fun update() { + // update limit lines + if(ReceiveData.time > 0) { + val entry = Entry(TimeValueFormatter.to_chart_x(ReceiveData.time), ReceiveData.rawValue.toFloat()) + addEntries(arrayListOf(entry)) + } + } + + override fun OnNotifyData(context: Context, dataSource: NotifySource, extras: Bundle?) { + try { + Log.d(LOG_ID, "OnNotifyData: $dataSource") + update() + } catch (exc: Exception) { + Log.e(LOG_ID, "OnNotifyData exception: " + exc.message.toString() + " - " + exc.stackTraceToString() ) + } + } + + private fun demo() { + Log.w(LOG_ID, "Start demo") + Thread { + try { + val demoData = DummyGraphData.create(5, min = 40, max = 260, stepMinute = 5) + Log.w(LOG_ID, "Running demo for ${demoData.size} entries") + demoData.forEach { (t, u) -> + addEntries(arrayListOf(Entry(TimeValueFormatter.to_chart_x(t), u.toFloat()))) + Thread.sleep(1000) + } + Log.w(LOG_ID, "Demo finished") + create() + } catch (exc: Exception) { + Log.e(LOG_ID, "demo exception: " + exc.message.toString() + " - " + exc.stackTraceToString() ) + } + }.start() + } + + fun getBitmap(): Bitmap? { + try { + if(chart.width > 0 && chart.height > 0) + return chart.chartBitmap + } catch (exc: Exception) { + Log.e(LOG_ID, "getBitmap exception: " + exc.message.toString() ) + } + return null + } + + private fun notifyChanged() { + Log.d(LOG_ID, "notifyChanged") + InternalNotifier.notify(context, NotifySource.GRAPH_CHANGED, null) + } + + private fun showOtherUnit(): Boolean { + return sharedPref.getBoolean(Constants.SHARED_PREF_SHOW_OTHER_UNIT, false) + } + + override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) { + Log.d(LOG_ID, "onSharedPreferenceChanged: $key") + try { + if (graphPrefList.contains(key)) { + Log.i(LOG_ID, "re create graph after settings changed for key: $key") + ReceiveData.updateSettings(sharedPref) + create() + } + } catch (exc: Exception) { + Log.e(LOG_ID, "onSharedPreferenceChanged exception: " + exc.message.toString() ) + } + } +} diff --git a/common/src/main/java/de/michelinside/glucodatahandler/common/chart/CustomBubbleMarker.kt b/common/src/main/java/de/michelinside/glucodatahandler/common/chart/CustomBubbleMarker.kt new file mode 100644 index 000000000..1e80c5bf4 --- /dev/null +++ b/common/src/main/java/de/michelinside/glucodatahandler/common/chart/CustomBubbleMarker.kt @@ -0,0 +1,125 @@ +package de.michelinside.glucodatahandler.common.chart + +import android.content.Context +import android.graphics.Canvas +import android.graphics.Paint +import android.graphics.Path +import android.util.Log +import android.widget.TextView +import androidx.core.content.ContextCompat +import com.github.mikephil.charting.components.MarkerView +import com.github.mikephil.charting.data.Entry +import com.github.mikephil.charting.highlight.Highlight +import com.github.mikephil.charting.utils.MPPointF +import de.michelinside.glucodatahandler.common.R +import java.text.DateFormat + +class CustomBubbleMarker(context: Context) : MarkerView(context, R.layout.marker_layout) { + private val LOG_ID = "GDH.Chart.MarkerView" + private val arrowSize = 35 + private val arrowCircleOffset = 0f + + override fun refreshContent(e: Entry?, highlight: Highlight?) { + try { + e?.let { + val time: TextView = this.findViewById(R.id.time) + val glucose: TextView = this.findViewById(R.id.glucose) + time.text = DateFormat.getTimeInstance(DateFormat.DEFAULT).format(TimeValueFormatter.from_chart_x(e.x)) + glucose.text = GlucoseFormatter.getValueAsString(e.y) + } + } catch (exc: Exception) { + Log.e(LOG_ID, "Exception in refreshContent", exc) + } + super.refreshContent(e, highlight) + } + + override fun getOffsetForDrawingAtPoint(posX: Float, posY: Float): MPPointF { + val offset = offset + try { + val chart = chartView + val width = width.toFloat() + val height = height.toFloat() + + if (posY <= height + arrowSize) { + offset.y = arrowSize.toFloat() + } else { + offset.y = -height - arrowSize + } + + if (posX > chart.width - width) { + offset.x = -width + } else { + offset.x = 0f + if (posX > width / 2) { + offset.x = -(width / 2) + } + } + } catch (exc: Exception) { + Log.e(LOG_ID, "Exception in getOffsetForDrawingAtPoint", exc) + } + + return offset + } + + override fun draw(canvas: Canvas, posX: Float, posY: Float) { + try { + val paint = Paint().apply { + style = Paint.Style.FILL + strokeJoin = Paint.Join.ROUND + color = ContextCompat.getColor(context, R.color.transparent_widget_background) + } + + val chart = chartView + val width = width.toFloat() + val height = height.toFloat() + val offset = getOffsetForDrawingAtPoint(posX, posY) + val saveId: Int = canvas.save() + val path = Path() + + if (posY < height + arrowSize) { + if (posX > chart.width - width) { + path.moveTo(width - (2 * arrowSize), 2f) + path.lineTo(width, -arrowSize + arrowCircleOffset) + path.lineTo(width - arrowSize, 2f) + } else { + if (posX > width / 2) { + path.moveTo(width / 2 - arrowSize / 2, 2f) + path.lineTo(width / 2, -arrowSize + arrowCircleOffset) + path.lineTo(width / 2 + arrowSize / 2, 2f) + } else { + path.moveTo(0f, -arrowSize + arrowCircleOffset) + path.lineTo(0f + arrowSize, 2f) + path.lineTo(0f, 2f) + path.lineTo(0f, -arrowSize + arrowCircleOffset) + } + } + path.offset(posX + offset.x, posY + offset.y) + } else { + if (posX > chart.width - width) { + path.moveTo(width, (height - 2) + arrowSize - arrowCircleOffset) + path.lineTo(width - arrowSize, height - 2) + path.lineTo(width - (2 * arrowSize), height - 2) + } else { + if (posX > width / 2) { + path.moveTo(width / 2 + arrowSize / 2, height - 2) + path.lineTo(width / 2, (height - 2) + arrowSize - arrowCircleOffset) + path.lineTo(width / 2 - arrowSize / 2, height - 2) + path.lineTo(0f, height - 2) + } else { + path.moveTo(0f + (arrowSize * 2), height - 2) + path.lineTo(0f, (height - 2) + arrowSize - arrowCircleOffset) + path.lineTo(0f, height - 2) + path.lineTo(0f + arrowSize, height - 2) + } + } + path.offset(posX + offset.x, posY + offset.y) + } + canvas.drawPath(path, paint) + canvas.translate(posX + offset.x, posY + offset.y) + draw(canvas) + canvas.restoreToCount(saveId) + } catch (exc: Exception) { + Log.e(LOG_ID, "Exception in getOffsetForDrawingAtPoint", exc) + } + } +} \ No newline at end of file diff --git a/common/src/main/java/de/michelinside/glucodatahandler/common/chart/CustomMarkerView.kt b/common/src/main/java/de/michelinside/glucodatahandler/common/chart/CustomMarkerView.kt new file mode 100644 index 000000000..2b1a81f80 --- /dev/null +++ b/common/src/main/java/de/michelinside/glucodatahandler/common/chart/CustomMarkerView.kt @@ -0,0 +1,36 @@ +package de.michelinside.glucodatahandler.common.chart + +import android.annotation.SuppressLint +import android.content.Context +import android.util.Log +import android.view.View +import android.widget.TextView +import com.github.mikephil.charting.components.MarkerView +import com.github.mikephil.charting.data.Entry +import com.github.mikephil.charting.highlight.Highlight +import com.github.mikephil.charting.utils.MPPointF +import de.michelinside.glucodatahandler.common.R +import java.text.DateFormat + + +@SuppressLint("ViewConstructor") +class CustomMarkerView(context: Context): MarkerView(context, R.layout.custom_marker_view_layout) { + private val LOG_ID = "GDH.Chart.MarkerView" + // this markerview only displays a textview + private val time = findViewById(R.id.time) as TextView + private val glucose = findViewById(R.id.glucose) as TextView + + // callbacks everytime the MarkerView is redrawn, can be used to update the + // content (user-interface) + override fun refreshContent(e: Entry?, highlight: Highlight?) { + Log.v(LOG_ID, "refreshContent for $e") + super.refreshContent(e, highlight) + time.text = DateFormat.getTimeInstance(DateFormat.DEFAULT).format(TimeValueFormatter.from_chart_x(e!!.x)) + glucose.text = GlucoseFormatter.getValueAsString(e.y) + } + + override fun getOffset(): MPPointF { + return MPPointF((-(width / 2)).toFloat(), (-height).toFloat()) + } + +} \ No newline at end of file diff --git a/common/src/main/java/de/michelinside/glucodatahandler/common/chart/GlucoseFormatter.kt b/common/src/main/java/de/michelinside/glucodatahandler/common/chart/GlucoseFormatter.kt new file mode 100644 index 000000000..0468a5785 --- /dev/null +++ b/common/src/main/java/de/michelinside/glucodatahandler/common/chart/GlucoseFormatter.kt @@ -0,0 +1,18 @@ +package de.michelinside.glucodatahandler.common.chart + +import com.github.mikephil.charting.formatter.ValueFormatter +import de.michelinside.glucodatahandler.common.ReceiveData +import de.michelinside.glucodatahandler.common.utils.GlucoDataUtils + +class GlucoseFormatter(val inverted: Boolean = false): ValueFormatter() { + companion object { + fun getValueAsString(value: Float, inverted: Boolean = false): String { + if((!inverted && ReceiveData.isMmol) || (inverted && !ReceiveData.isMmol)) + return GlucoDataUtils.mgToMmol(value).toString() + return value.toInt().toString() + } + } + override fun getFormattedValue(value: Float): String { + return getValueAsString(value, inverted) + } +} \ No newline at end of file diff --git a/common/src/main/java/de/michelinside/glucodatahandler/common/chart/TimeAxisRenderer.kt b/common/src/main/java/de/michelinside/glucodatahandler/common/chart/TimeAxisRenderer.kt new file mode 100644 index 000000000..ed1648046 --- /dev/null +++ b/common/src/main/java/de/michelinside/glucodatahandler/common/chart/TimeAxisRenderer.kt @@ -0,0 +1,150 @@ +package de.michelinside.glucodatahandler.common.chart + +import android.util.Log +import com.github.mikephil.charting.charts.LineChart +import com.github.mikephil.charting.renderer.XAxisRenderer +import com.github.mikephil.charting.utils.Utils +import kotlin.math.ceil +import kotlin.math.floor +import kotlin.math.log10 +import kotlin.math.pow +class TimeAxisRenderer(chart: LineChart) : + XAxisRenderer(chart.viewPortHandler, chart.xAxis, chart.rendererXAxis.transformer) { + private val LOG_ID = "GDH.Chart.TimeAxisRenderer" + + + override fun computeAxisValues(min: Float, max: Float) { + computeNiceAxisValues(min, max) + super.computeSize() + } + + // Custom method for calculating a "nice" interval at the + // desired display time unit + private fun calculateInterval(min: Float, max: Float, count: Int): Float { + val range = (max - min).toDouble() + + val unit = TimeValueFormatter.getUnit(min, max) + val axisScale = unit.factor + + //Log.d(LOG_ID, "Calculate interval: $unit - scale: $axisScale - min: $min - max: $max - range: $range - count: $count") + + // Find out how much spacing (in y value space) between axis values + val rawInterval = range / count / axisScale + var interval: Float = Utils.roundToNextSignificant(rawInterval) + // If granularity is enabled, then do not allow the interval to go below specified granularity. + // This is used to avoid repeated values when rounding values for display. + if (mAxis.isGranularityEnabled) { + val gran = mAxis.granularity * axisScale + Log.d(LOG_ID, "Calculate interval: $interval - gran: $gran") + interval = if (interval < gran) gran else interval + } + + // Normalize interval + val intervalMagnitude: Float = + Utils.roundToNextSignificant(10.0.pow(log10(interval).toInt().toDouble())) + val intervalSigDigit = (interval / intervalMagnitude).toInt() + if (intervalSigDigit > 5) { + // Use one order of magnitude higher, to avoid intervals like 0.9 or + // 90 + interval = floor(10 * intervalMagnitude) + } + + interval *= axisScale + //Log.d(LOG_ID, "Calculate interval: $interval - scale: $axisScale - intervalMagnitude: $intervalMagnitude - intervalSigDigit: $intervalSigDigit") + return (interval/10).toInt()*10F + } + + private fun computeNiceAxisValues(xMin: Float, xMax: Float) { + val labelCount = mAxis.labelCount + val range = (xMax - xMin).toDouble() + + if (labelCount == 0 || range <= 0 || java.lang.Double.isInfinite(range)) { + mAxis.mEntries = floatArrayOf() + mAxis.mCenteredEntries = floatArrayOf() + mAxis.mEntryCount = 0 + return + } + + // == Use a time-aware interval calculation + var interval = calculateInterval(xMin, xMax, labelCount) + + // == Below here copied from AxisRenderer::computeAxisValues unchanged ============ + var n = if (mAxis.isCenterAxisLabelsEnabled) 1 else 0 + + // force label count + if (mAxis.isForceLabelsEnabled) { + interval = (range.toFloat() / (labelCount - 1).toFloat()) + mAxis.mEntryCount = labelCount + + if (mAxis.mEntries.size < labelCount) { + // Ensure stops contains at least numStops elements. + mAxis.mEntries = FloatArray(labelCount) + } + + var v = xMin + + for (i in 0.. { + val data = mutableMapOf() + if(hours<=0) + return data + val startTime = System.currentTimeMillis() - hours * 60 * 60 * 1000 + val timeDiff = 60L * 1000L * stepMinute + var lastValue = min + var deltaFactor = 1 + for (i in startTime until System.currentTimeMillis() step timeDiff) { + if(deltaFactor > 0 && lastValue >= max) + deltaFactor = -1 + else if(deltaFactor < 0 && lastValue <= min) + deltaFactor = 1 + lastValue += Random.nextInt(10) * deltaFactor + data[i] = lastValue + } + return data + } +} \ No newline at end of file diff --git a/common/src/main/res/drawable-nodpi/marker.png b/common/src/main/res/drawable-nodpi/marker.png new file mode 100644 index 0000000000000000000000000000000000000000..580542829b4b935710fa7f836e71e932c693e6b2 GIT binary patch literal 3389 zcmbW4ikgaHu&K~j(o;ed$bIEX6WP;9W^I40D#UwU)$nBKU{<>CHciF z`%A3wLa2Q8ZT$hDVg3^&An)Z>0I1D8VX#}bJbVIt{5^bp`3zt%KHrBv?w>ul^B#S9Em9xOxf_NmElJ zQ&`Q#X`fP0QY`boNsW1%__X(AA?&T!c+=tcf%Ccvjn(o!bS<8ug_1T)U(Q@1lC~7e zd*xe9cSqO4vZPiNwU94hq^!Nm?|&vp0*)fo)NTs2P<#R;!J{;kpv45$BF&vZWY~b_ zI*>%hlC%V*|7AoO$pW-OQ=vsbOP3@Pog-`s3dn#{pR3CfxFrFcM4o&f1(E2Z+%OVg z|AL>Dq#zyeF}o#d0}oYDj<qOa8O88MA5R$y2g9hw3SrnZw zIp=9fwuR(!$VSOCeB0{>pdg*~qT0Q);6eJz!9m4TEd3pcjnCvn0T-90^A$YCR||lJ zfQaF9aj9C)NLBJk@AEg;x5+$i(-b^9Om?qf(5eFk>(hum-#@+4dHoDGHMO$5JZabg zwRi4Cgr6hZ?zAAz?*EQZJ3T!3_GwWlO4cz-kL>7MTmQasAvgXxRjkX*_cXoJdK%&> z&!|9$feX$?jAhB3$uCtqFJ(`*P#_8V{6^>X5#rs&dGQlcjd=x7eNTfVEM20+(UtU< zkqDzrK+EDG0K1JotzShcNh3XC7VtsDpPFZS1=m5OyTNlm0B&mwNy2+;HM=PR&@PA) ztA=pxw_X)*A?I$rJkv^Z>ZBM873ye%GD4~EMsh!Nx=|ht6^$RL<`Hv}`l`hv-(v0& zpW#a{*=F^TUfq}B*qJiFRkSIJiVV_2$?VKOok-&H)J8Csl07%>h`&{zj44@&?@JM4q^SBEej4AW^Eux{y=wW}0z9rvXryV{}e{}QLlaP509P)>{K z;;gnVCv8kRaK*n5B4diuX`xai3xe`F^Fs7(jBQLO^-K7;h2*bHQz=oA#+tW@2xOEP zeGqQA^1I`zi>yq7jlQ_xI8z$46k}K-tV2nNDG4Skf_rw!;Qc%J2-Aq}$O8+(b@5V^ zygsL3#+T{4UNscTFU0w7_QWltm)n=+mbsSsckQXsE?R!?jw}~NEqd=6a;z|}$ghyQ z<;X&;-sV}%mb`{vQAo3cR=mbqRGBA~Fv~y6Qyz$6GcYQwc#}EG>A~)C=vlQE&~Rlo zlh-tR2r<&PuD6!GM!FwFOBc@?)18PE}WET*NV&%+U>(R+8DwSSZw{tX@|7S{Wv1D2J%E*n8owNwJjUcl0vAlxkJ9ID0SR=^V??8;du% zisX%PGHwoDWm(x-HW+=3IYw`@S+R1f7+ut?(`b7VMGxsV6mbyIL#7R0Mz2~8SF`uw1HJ176s?Z|v%pW?ZY=G9~FaH%u+nfGz=fgY_5c`dCF zIe~WYs#>RlpN|@kR5xMA9Ol@r1uJyUbk1IgO~(zZy&cp2TbCilv&&&gXvw?FyX&nm zxiG)5k;#V1cAkX^uINUQsm!x+ZNhb**VtgR-h}ck2h8{7GEjGnP!+`^2d?w)jQZP*wT8A>Xk`y zLM7H48}5vBVMo??R<>_vG-rf2g#Ue}PC=J2MUUaJsB5u{J(L;r!h>xjlkbBrYKi5wDr*n`py1ER(GeG&}?IpVvOt&eEPkA!W>z zt0W8+#5Bqz@2O2IHOMz;c1gP1_K(@1Z4B-kaVtxTRVqHX|7reoYcF9Dy-2eZ#-1b9 zVB#fp?ZYci%Yo<*tTv*iWydCRJ+G|R+1Pk-hIE#o)I zz#=!0-`xZI%H85>&O09@*GutcQhf2MeO7+iR)QJ}En0)yiB5=7)F9mu?w<#edX=*< z`8Nl)`fGa$&y-&Nd_~-1YGYb>jxCHgLP+q~d~Ytlr(C1v7eb(3V!eDb-kQ8z?c-Cd znD>|&W($#B8PO~pYT^}jyR|pEwT{GVlg5gAB`$n(W@5|H%tp#~zRvkL9?hk}n)I}i zg}|Q8`eagcCU}J_!Ke3g?#zo1rI-%fUA+5<`}+4eiF|wdtg`Iy+4K+Srlvp8&0Bxi zI{2kSkjfqqB{arhVYs)S)jl@46@M8VP}PLLgYUzS;fmWMvvtlBM-PU#xx$)$$^RH5 z*f$-XX?*!aup+TC8FL?VJ25z^J=CmBDqBCwI`MX_F!lD^>Lwi_J);n$$4=nBj`57B z!!6RrZ*J<*cVGK-j{X@WL8nM(_4muJML}QfkmdD5s`*1+I+f|bl(6yziqUIx`pgAH4C9h|?%;PbPAF<@athnT#o+N3(@u=_D!w}!C7jQEJit5Uh+y%DgXcAhb6 z*4_BBF|QKm;Dy@`ZLSM*KV0X^R~nq%Zw}dEBaV;dE33~o?}hptb{-zgv&}|46geC0 zYn%!a*{*0ZYr3+Id^jL+VW3(q!M zHaV&YU2R>wpS}dk$=u$zKG|vwnhE0h`F5VcZ22+Y6H%ha@An6mL&YZ<92przGSZyA zQ&%|&!o_=&&sE>T1c1=%07S+B@SAv{>i|5G24KSp0F@j7*nOVg>C^?_l81pd1Rgy7 zW7;hF>m$&;bJ)up1`L9CI07~%Np0}YoeY1p^0ixzIIj!vLf^SElFth4v6!qr|K}YU zq=<=kCZ>q}z$tn?cye~q!oT)x;95wtY5R|VPFj(3)${17xSe3`Ch@==0BKLLn+cZY z{QFZyASzZl;E|`NCw@M z1(1I+n*4uwvH4&8_uL=6@?Q%%MDSnB|KWBQT$|``eR+(T?KClqu-&Q|U=FmdGTS*v zl+8EM5$_#%LOy$CBhT4XH496stbE}0&2)o9gcb9;ss_2Zuxl)}1qr@575RlQ-il93 z%>@a&RtGz=+mDJ=oF76Mw%Rv-v>bkfOQ7tAGIj$W`4@Q2SN%c+4T!H9;H;bFhUyp) z?8X<#Nu>&QVB+-6@yv(l{H(OHVMWKy;tll->d@&d!oI{9&9Up5Lj?Y1bUt<-Q(vY@ z>{g(+{+wemRJ-Wlb#1jd(X6b>M0Cy8zot$KcAk6B;4P~}84uNNKYp~VT%~+F+*J&MPd3Bg^LJ{^>s$mNc$DB6o*4X~r>g(T ze!+JAuiyJ!njGpS^zeFP!L^VBuY>dBrOXYA3|gOIvKSS|W;?WO=UwrXZr7XNdPpv2 i)IP3$@ev{tNkOW$$#B2HKKI4_1O_^$+LchJ=>GsoT~~?# literal 0 HcmV?d00001 diff --git a/common/src/main/res/layout/custom_marker_view_layout.xml b/common/src/main/res/layout/custom_marker_view_layout.xml new file mode 100644 index 000000000..72f687172 --- /dev/null +++ b/common/src/main/res/layout/custom_marker_view_layout.xml @@ -0,0 +1,39 @@ + + + + + + + + + \ No newline at end of file diff --git a/common/src/main/res/layout/marker_layout.xml b/common/src/main/res/layout/marker_layout.xml new file mode 100644 index 000000000..1080688b6 --- /dev/null +++ b/common/src/main/res/layout/marker_layout.xml @@ -0,0 +1,31 @@ + + + + + + + + \ No newline at end of file diff --git a/common/src/main/res/values-night/colors.xml b/common/src/main/res/values-night/colors.xml new file mode 100644 index 000000000..c03d1134a --- /dev/null +++ b/common/src/main/res/values-night/colors.xml @@ -0,0 +1,20 @@ + + + #FFBB86FC + #FF6200EE + #FF3700B3 + #FF03DAC5 + #FF018786 + #FF000000 + #FFFFFFFF + #FF005CCC + #FFFF0000 + #FFFFFF00 + #FF00FF00 + #FF888888 + @color/white + #FF333333 + #FF666666 + #55000000 + @color/white + \ No newline at end of file diff --git a/common/src/main/res/values/colors.xml b/common/src/main/res/values/colors.xml new file mode 100644 index 000000000..0167d98f1 --- /dev/null +++ b/common/src/main/res/values/colors.xml @@ -0,0 +1,21 @@ + + + #FFBB86FC + #FF6200EE + #FF3700B3 + #FF03DAC5 + #FF018786 + #FF005CCC + #FF000000 + #FFFFFFFF + #FFFF0000 + #FFFFFF00 + #FFFF7700 + #FF00FF00 + #FF888888 + @color/black + #FFDDDDDD + #FFF0F0F0 + #55000000 + @color/white + \ No newline at end of file diff --git a/mobile/build.gradle b/mobile/build.gradle index 1e0c8c474..d7d89d6f1 100644 --- a/mobile/build.gradle +++ b/mobile/build.gradle @@ -94,6 +94,7 @@ dependencies { implementation "com.ncorti:slidetoact:0.11.0" implementation 'com.takisoft.preferencex:preferencex-datetimepicker:1.1.0' implementation 'androidx.coordinatorlayout:coordinatorlayout:1.2.0' + implementation 'com.github.PhilJay:MPAndroidChart:v3.1.0' testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.2.1' androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1' diff --git a/mobile/src/main/java/de/michelinside/glucodatahandler/MainActivity.kt b/mobile/src/main/java/de/michelinside/glucodatahandler/MainActivity.kt index c8925cd4c..7a8282bb4 100644 --- a/mobile/src/main/java/de/michelinside/glucodatahandler/MainActivity.kt +++ b/mobile/src/main/java/de/michelinside/glucodatahandler/MainActivity.kt @@ -29,6 +29,7 @@ import androidx.core.content.ContextCompat import androidx.core.view.MenuCompat import androidx.core.view.setPadding import androidx.preference.PreferenceManager +import com.github.mikephil.charting.charts.LineChart import de.michelinside.glucodatahandler.android_auto.CarModeReceiver import de.michelinside.glucodatahandler.common.Constants import de.michelinside.glucodatahandler.common.GlucoDataService @@ -36,6 +37,7 @@ import de.michelinside.glucodatahandler.common.ReceiveData import de.michelinside.glucodatahandler.common.SourceState import de.michelinside.glucodatahandler.common.SourceStateData import de.michelinside.glucodatahandler.common.WearPhoneConnection +import de.michelinside.glucodatahandler.common.chart.ChartViewer import de.michelinside.glucodatahandler.common.notification.AlarmHandler import de.michelinside.glucodatahandler.common.notification.AlarmState import de.michelinside.glucodatahandler.common.notification.AlarmType @@ -47,6 +49,8 @@ import de.michelinside.glucodatahandler.common.notifier.NotifierInterface import de.michelinside.glucodatahandler.common.notifier.NotifySource import de.michelinside.glucodatahandler.common.ui.Dialogs import de.michelinside.glucodatahandler.common.utils.BitmapUtils +import de.michelinside.glucodatahandler.common.utils.DummyGraphData +import de.michelinside.glucodatahandler.common.utils.GlucoDataUtils import de.michelinside.glucodatahandler.common.utils.PackageUtils import de.michelinside.glucodatahandler.common.utils.TextToSpeechUtils import de.michelinside.glucodatahandler.common.utils.Utils @@ -75,12 +79,14 @@ class MainActivity : AppCompatActivity(), NotifierInterface { private lateinit var btnSources: Button private lateinit var sharedPref: SharedPreferences private lateinit var optionsMenu: Menu + private lateinit var chart: LineChart private var alarmIcon: MenuItem? = null private var snoozeMenu: MenuItem? = null private var floatingWidgetItem: MenuItem? = null private val LOG_ID = "GDH.Main" private var requestNotificationPermission = false private var doNotUpdate = false + private lateinit var chartHandler: ChartViewer override fun onCreate(savedInstanceState: Bundle?) { try { @@ -103,6 +109,7 @@ class MainActivity : AppCompatActivity(), NotifierInterface { tableAlarms = findViewById(R.id.tableAlarms) tableDetails = findViewById(R.id.tableDetails) tableNotes = findViewById(R.id.tableNotes) + chart = findViewById(R.id.chart) PreferenceManager.setDefaultValues(this, R.xml.preferences, false) sharedPref = this.getSharedPreferences(Constants.SHARED_PREF_TAG, Context.MODE_PRIVATE) @@ -126,6 +133,9 @@ class MainActivity : AppCompatActivity(), NotifierInterface { if (requestPermission()) GlucoDataServiceMobile.start(this) TextToSpeechUtils.initTextToSpeech(this) + //createChart() + chartHandler = ChartViewer(chart, this) + chartHandler.create() } catch (exc: Exception) { Log.e(LOG_ID, "onCreate exception: " + exc.message.toString() ) } @@ -174,6 +184,13 @@ class MainActivity : AppCompatActivity(), NotifierInterface { Log.e(LOG_ID, "onResume exception: " + exc.message.toString() ) } } + + override fun onDestroy() { + Log.v(LOG_ID, "onDestroy called") + super.onDestroy() + chartHandler.close() + } + private val requestPermissionLauncher = registerForActivityResult( ActivityResultContracts.RequestPermission() @@ -539,7 +556,7 @@ class MainActivity : AppCompatActivity(), NotifierInterface { } else { btnSources.visibility = View.GONE } - + //chartHandler.update() updateNotesTable() updateAlarmsTable() updateConnectionsTable() @@ -547,6 +564,7 @@ class MainActivity : AppCompatActivity(), NotifierInterface { updateAlarmIcon() updateMenuItems() + } catch (exc: Exception) { Log.e(LOG_ID, "update exception: " + exc.message.toString() ) } diff --git a/mobile/src/main/res/layout/activity_main.xml b/mobile/src/main/res/layout/activity_main.xml index e339784e6..d8f39e3fc 100644 --- a/mobile/src/main/res/layout/activity_main.xml +++ b/mobile/src/main/res/layout/activity_main.xml @@ -133,8 +133,15 @@ android:text="@string/menu_sources" /> + + + diff --git a/settings.gradle b/settings.gradle index 39e8d61e9..5de0e55b8 100644 --- a/settings.gradle +++ b/settings.gradle @@ -10,6 +10,7 @@ dependencyResolutionManagement { repositories { google() mavenCentral() + maven { url "https://jitpack.io" } } } rootProject.name = "GlucoDataHandler" From bf193fa171363dda70781bcbe9cc03fc807ac29a Mon Sep 17 00:00:00 2001 From: pachi81 Date: Sun, 19 Jan 2025 23:29:03 +0100 Subject: [PATCH 02/17] chart as bitmap --- .../glucodatahandler/common/Constants.kt | 4 + .../common/chart/ChartBitmap.kt | 42 ++++ .../common/chart/ChartBitmapCreator.kt | 68 ++++++ .../common/chart/ChartData.kt | 24 +-- .../common/chart/ChartViewer.kt | 204 ++++++++++-------- .../common/chart/GlucoseChart.kt | 40 ++++ .../glucodatahandler/MainActivity.kt | 36 ++-- mobile/src/main/res/layout/activity_main.xml | 5 + 8 files changed, 298 insertions(+), 125 deletions(-) create mode 100644 common/src/main/java/de/michelinside/glucodatahandler/common/chart/ChartBitmap.kt create mode 100644 common/src/main/java/de/michelinside/glucodatahandler/common/chart/ChartBitmapCreator.kt create mode 100644 common/src/main/java/de/michelinside/glucodatahandler/common/chart/GlucoseChart.kt diff --git a/common/src/main/java/de/michelinside/glucodatahandler/common/Constants.kt b/common/src/main/java/de/michelinside/glucodatahandler/common/Constants.kt index 17378091b..a6d50b4cb 100644 --- a/common/src/main/java/de/michelinside/glucodatahandler/common/Constants.kt +++ b/common/src/main/java/de/michelinside/glucodatahandler/common/Constants.kt @@ -284,4 +284,8 @@ object Constants { const val AA_MEDIA_PLAYER_DURATION = "aa_media_player_duration" const val PATIENT_NAME = "patient_name" + + // graph + const val GRAPH_ID = "graph_id" + } diff --git a/common/src/main/java/de/michelinside/glucodatahandler/common/chart/ChartBitmap.kt b/common/src/main/java/de/michelinside/glucodatahandler/common/chart/ChartBitmap.kt new file mode 100644 index 000000000..0a47e4f07 --- /dev/null +++ b/common/src/main/java/de/michelinside/glucodatahandler/common/chart/ChartBitmap.kt @@ -0,0 +1,42 @@ +package de.michelinside.glucodatahandler.common.chart + +import android.content.Context +import android.os.Bundle +import android.util.Log +import android.view.View +import android.widget.ImageView +import de.michelinside.glucodatahandler.common.Constants +import de.michelinside.glucodatahandler.common.notifier.InternalNotifier +import de.michelinside.glucodatahandler.common.notifier.NotifierInterface +import de.michelinside.glucodatahandler.common.notifier.NotifySource +import de.michelinside.glucodatahandler.common.utils.Utils + +class ChartBitmap(val imageView: ImageView, val context: Context): NotifierInterface { + + private val LOG_ID = "GDH.Chart.Bitmap" + + private var chartViewer: ChartBitmapCreator + private var chart: GlucoseChart = GlucoseChart(context) + init { + Log.v(LOG_ID, "init") + chart.measure (View.MeasureSpec.makeMeasureSpec (1000, View.MeasureSpec.EXACTLY), View.MeasureSpec.makeMeasureSpec (400, View.MeasureSpec.EXACTLY)) + chart.layout (0, 0, chart.getMeasuredWidth(), chart.getMeasuredHeight()) + + InternalNotifier.addNotifier(context, this, mutableSetOf(NotifySource.GRAPH_CHANGED)) + chartViewer = ChartBitmapCreator(chart, context) + chartViewer.create() + } + + fun close() { + Log.v(LOG_ID, "close") + InternalNotifier.remNotifier(context, this) + } + + override fun OnNotifyData(context: Context, dataSource: NotifySource, extras: Bundle?) { + Log.d(LOG_ID, "OnNotifyData for $dataSource - id ${chart.id} - extras: ${Utils.dumpBundle(extras)}") + if(dataSource == NotifySource.GRAPH_CHANGED && extras?.getInt(Constants.GRAPH_ID) == chart.id) { + Log.i(LOG_ID, "Update bitmap") + imageView.setImageBitmap(chartViewer.getBitmap()) + } + } +} \ No newline at end of file diff --git a/common/src/main/java/de/michelinside/glucodatahandler/common/chart/ChartBitmapCreator.kt b/common/src/main/java/de/michelinside/glucodatahandler/common/chart/ChartBitmapCreator.kt new file mode 100644 index 000000000..44a1957cb --- /dev/null +++ b/common/src/main/java/de/michelinside/glucodatahandler/common/chart/ChartBitmapCreator.kt @@ -0,0 +1,68 @@ +package de.michelinside.glucodatahandler.common.chart + +import android.content.Context +import android.util.Log +import com.github.mikephil.charting.charts.LineChart +import com.github.mikephil.charting.data.LineData +import com.github.mikephil.charting.data.LineDataSet +import de.michelinside.glucodatahandler.common.ReceiveData + +class ChartBitmapCreator(chart: LineChart, context: Context): ChartViewer(chart, context) { + private val LOG_ID = "GDH.Chart.BitmapCreator" + + override fun initXaxis() { + Log.v(LOG_ID, "initXaxis") + chart.xAxis.isEnabled = false + } + + override fun initYaxis() { + Log.v(LOG_ID, "initYaxis") + chart.axisRight.setDrawAxisLine(false) + chart.axisRight.setDrawLabels(false) + chart.axisRight.setDrawZeroLine(false) + chart.axisRight.setDrawGridLines(false) + chart.axisLeft.isEnabled = false + } + + override fun initChart(touchEnabled: Boolean) { + super.initChart(false) + chart.isDrawingCacheEnabled = false + } + + override fun getDefaultRange(): Long { + return 120L + } + + override fun getMinTime(): Long { + return System.currentTimeMillis() - getDefaultRange()*60*1000L + } + + override fun updateChart(dataSet: LineDataSet) { + Log.v(LOG_ID, "Update chart for ${dataSet.values.size} entries and ${dataSet.circleColors.size} colors") + chart.data = LineData(dataSet) + chart.notifyDataSetChanged() + chart.invalidate() + } + + private fun createBitmap() { + Log.d(LOG_ID, "createBitmap") + // reset bitmap and create it + chart.fitScreen() + chart.data?.clearValues() + chart.data?.notifyDataChanged() + chart.notifyDataSetChanged() + //chart.clear() + initData() + } + + override fun update() { + // update limit lines + if(ReceiveData.time > 0) { + ChartData.addData(ReceiveData.time, ReceiveData.rawValue) + createBitmap() + } + } + + + +} \ No newline at end of file diff --git a/common/src/main/java/de/michelinside/glucodatahandler/common/chart/ChartData.kt b/common/src/main/java/de/michelinside/glucodatahandler/common/chart/ChartData.kt index 07a184203..5c9bcbcb4 100644 --- a/common/src/main/java/de/michelinside/glucodatahandler/common/chart/ChartData.kt +++ b/common/src/main/java/de/michelinside/glucodatahandler/common/chart/ChartData.kt @@ -1,23 +1,23 @@ package de.michelinside.glucodatahandler.common.chart import com.github.mikephil.charting.data.Entry +import de.michelinside.glucodatahandler.common.utils.DummyGraphData object ChartData { - fun getData(count: Int): ArrayList { + private var graphData = DummyGraphData.create(48, min = 40, max = 260, stepMinute = 5).toMutableMap() + + fun getData(minTime: Long = 0L): ArrayList { val entries = ArrayList() - val min = 50F - val max = 300F - var value = min - var delta = 10F - for (i in 0 until count) { - entries.add(Entry(i.toFloat(), value)) - value += delta - if(value >= max) - delta = -10F - if(value <= min) - delta = +10F + graphData.forEach { (t, u) -> + if(t >= minTime) { + entries.add(Entry(TimeValueFormatter.to_chart_x(t), u.toFloat())) + } } return entries } + fun addData(time: Long, value: Int) { + graphData[time] = value + } + } \ No newline at end of file diff --git a/common/src/main/java/de/michelinside/glucodatahandler/common/chart/ChartViewer.kt b/common/src/main/java/de/michelinside/glucodatahandler/common/chart/ChartViewer.kt index 3a248b93a..569cb7620 100644 --- a/common/src/main/java/de/michelinside/glucodatahandler/common/chart/ChartViewer.kt +++ b/common/src/main/java/de/michelinside/glucodatahandler/common/chart/ChartViewer.kt @@ -3,8 +3,10 @@ package de.michelinside.glucodatahandler.common.chart import android.content.Context import android.content.SharedPreferences import android.graphics.Bitmap +import android.graphics.Color import android.os.Bundle import android.util.Log +import androidx.core.view.drawToBitmap import com.github.mikephil.charting.charts.LineChart import com.github.mikephil.charting.components.LimitLine import com.github.mikephil.charting.components.XAxis @@ -12,6 +14,7 @@ import com.github.mikephil.charting.components.YAxis import com.github.mikephil.charting.data.Entry import com.github.mikephil.charting.data.LineData import com.github.mikephil.charting.data.LineDataSet +import com.github.mikephil.charting.utils.Utils import de.michelinside.glucodatahandler.common.Constants import de.michelinside.glucodatahandler.common.R import de.michelinside.glucodatahandler.common.ReceiveData @@ -22,12 +25,14 @@ import de.michelinside.glucodatahandler.common.utils.DummyGraphData import java.util.concurrent.TimeUnit -open class ChartViewer(private val chart: LineChart, val context: Context, private val forBitmap: Boolean = false): NotifierInterface, +open class ChartViewer(protected val chart: LineChart, protected val context: Context): NotifierInterface, SharedPreferences.OnSharedPreferenceChangeListener { - private val LOG_ID = "GDH.Chart.Handler" - private var created = false - private val sharedPref = context.getSharedPreferences(Constants.SHARED_PREF_TAG, Context.MODE_PRIVATE) - private var init = false + private val LOG_ID = "GDH.Chart.Viewer" + protected var created = false + protected val sharedPref = context.getSharedPreferences(Constants.SHARED_PREF_TAG, Context.MODE_PRIVATE) + protected var init = false + + private val demoMode = false private var graphPrefList = mutableSetOf( Constants.SHARED_PREF_LOW_GLUCOSE, @@ -43,8 +48,10 @@ open class ChartViewer(private val chart: LineChart, val context: Context, priva private fun init() { if(!init) { Log.d(LOG_ID, "init") + Utils.init(context) sharedPref.registerOnSharedPreferenceChangeListener(this) - InternalNotifier.addNotifier(context, this, mutableSetOf(NotifySource.MESSAGECLIENT, NotifySource.BROADCAST)) + if(!demoMode) + InternalNotifier.addNotifier(context, this, mutableSetOf(NotifySource.MESSAGECLIENT, NotifySource.BROADCAST)) } } @@ -53,12 +60,14 @@ open class ChartViewer(private val chart: LineChart, val context: Context, priva try { init() resetChart() - initChart() initXaxis() initYaxis() - initData() + initChart() + if(!demoMode) + initData() created = true - //demo() + if(demoMode) + demo() } catch (exc: Exception) { Log.e(LOG_ID, "create exception: " + exc.message.toString() ) } @@ -96,31 +105,30 @@ open class ChartViewer(private val chart: LineChart, val context: Context, priva chart.invalidate() } - private fun initXaxis() { + protected open fun initXaxis() { Log.v(LOG_ID, "initXaxis") - chart.xAxis.isEnabled = !forBitmap - if(chart.xAxis.isEnabled) { - chart.xAxis.position = XAxis.XAxisPosition.BOTTOM - chart.xAxis.setDrawGridLines(true) - chart.xAxis.enableGridDashedLine(10F, 10F, 0F) - chart.xAxis.valueFormatter = TimeValueFormatter(chart) - chart.setXAxisRenderer(TimeAxisRenderer(chart)) - } + chart.xAxis.position = XAxis.XAxisPosition.BOTTOM + chart.xAxis.setDrawGridLines(true) + chart.xAxis.enableGridDashedLine(10F, 10F, 0F) + chart.xAxis.valueFormatter = TimeValueFormatter(chart) + chart.setXAxisRenderer(TimeAxisRenderer(chart)) + chart.xAxis.textColor = context.resources.getColor(R.color.text_color) } - private fun initYaxis() { + protected open fun initYaxis() { Log.v(LOG_ID, "initYaxis") - chart.axisRight.setDrawAxisLine(!forBitmap) - chart.axisRight.setDrawLabels(!forBitmap) - if(!forBitmap) - chart.axisRight.valueFormatter = GlucoseFormatter() + chart.axisRight.valueFormatter = GlucoseFormatter() chart.axisRight.setDrawZeroLine(false) + chart.axisRight.setDrawAxisLine(false) chart.axisRight.setDrawGridLines(false) - chart.axisLeft.isEnabled = !forBitmap && showOtherUnit() + chart.axisRight.textColor = context.resources.getColor(R.color.text_color) + chart.axisLeft.isEnabled = showOtherUnit() if(chart.axisLeft.isEnabled) { chart.axisLeft.valueFormatter = GlucoseFormatter(true) chart.axisLeft.setDrawZeroLine(false) + chart.axisLeft.setDrawAxisLine(false) chart.axisLeft.setDrawGridLines(false) + chart.axisLeft.textColor = context.resources.getColor(R.color.text_color) } } @@ -130,27 +138,33 @@ open class ChartViewer(private val chart: LineChart, val context: Context, priva return line } - private fun initChart() { - Log.v(LOG_ID, "initChart") - chart.setTouchEnabled(!forBitmap) - //chart.setPinchZoom(true) - val dataSet = LineDataSet(ArrayList(), "Glucose Values") - dataSet.valueFormatter = GlucoseFormatter() - dataSet.colors = mutableListOf() - dataSet.circleColors = mutableListOf() - dataSet.lineWidth = 2F - dataSet.circleRadius = 3F - dataSet.setDrawValues(false) - dataSet.setDrawCircleHole(false) - dataSet.axisDependency = YAxis.AxisDependency.RIGHT - chart.data = LineData(dataSet) + protected fun initDataSet() { + if(chart.data == null || chart.data.entryCount == 0) { + Log.v(LOG_ID, "initDataSet") + val dataSet = LineDataSet(ArrayList(), "Glucose Values") + //dataSet.valueFormatter = GlucoseFormatter() + //dataSet.colors = mutableListOf() + dataSet.circleColors = mutableListOf() + //dataSet.lineWidth = 1F + dataSet.circleRadius = 3F + dataSet.setDrawValues(false) + dataSet.setDrawCircleHole(false) + dataSet.axisDependency = YAxis.AxisDependency.RIGHT + dataSet.enableDashedLine(0F, 1F, 0F) + chart.data = LineData(dataSet) + } + } + protected open fun initChart(touchEnabled: Boolean = true) { + Log.v(LOG_ID, "initChart - touchEnabled: $touchEnabled") + chart.setTouchEnabled(touchEnabled) + initDataSet() + chart.setBackgroundColor(Color.TRANSPARENT) chart.isAutoScaleMinMaxEnabled = false chart.legend.isEnabled = false chart.description.isEnabled = false chart.isScaleYEnabled = false - chart.xAxis.textColor = context.resources.getColor(R.color.text_color) - if(!forBitmap) { + if(touchEnabled) { val mMarker = CustomBubbleMarker(context) mMarker.chartView = chart chart.marker = mMarker @@ -165,7 +179,6 @@ open class ChartViewer(private val chart: LineChart, val context: Context, priva chart.axisRight.addLimitLine(createLimitLine(ReceiveData.targetMinRaw)) if(ReceiveData.targetMaxRaw > 0F) chart.axisRight.addLimitLine(createLimitLine(ReceiveData.targetMaxRaw)) - chart.axisRight.textColor = context.resources.getColor(R.color.text_color) chart.axisRight.axisMinimum = Constants.GLUCOSE_MIN_VALUE.toFloat()-10F chart.axisRight.axisMaximum = ReceiveData.highRaw+10 chart.axisLeft.axisMinimum = chart.axisRight.axisMinimum @@ -174,30 +187,25 @@ open class ChartViewer(private val chart: LineChart, val context: Context, priva chart.invalidate() } - private fun initData() { - val graphData = DummyGraphData.create(4, min = 160, max = 260, stepMinute = 5) - if(graphData.values.isNotEmpty()) { - val values: ArrayList = ArrayList() - graphData.forEach { (t, u) -> - values.add(Entry(TimeValueFormatter.to_chart_x(t), u.toFloat())) - } - addEntries(values) - } else if(ReceiveData.time > 0) { - update() - } + protected fun initData() { + initDataSet() + addEntries(ChartData.getData(getMinTime())) } - private fun addEntries(values: ArrayList) { + protected open fun getMinTime(): Long { + return 0L + } + + protected open fun getDefaultRange(): Long { + return 240L + } + + protected open fun addEntries(values: ArrayList) { Log.d(LOG_ID, "Add ${values.size} entries") if(values.isEmpty()) return val dataSet = chart.data.getDataSetByIndex(0) as LineDataSet - val right = isRight() - val left = isLeft() - Log.v(LOG_ID, "Min: ${chart.xAxis.axisMinimum} - visible: ${chart.lowestVisibleX} - Max: ${chart.xAxis.axisMaximum} - visible: ${chart.highestVisibleX} - isLeft: ${left} - isRight: ${right}" ) - var diffTimeMin = TimeUnit.MILLISECONDS.toMinutes(TimeValueFormatter.from_chart_x(chart.highestVisibleX).time - TimeValueFormatter.from_chart_x(chart.lowestVisibleX).time) - Log.d(LOG_ID, "Diff-Time: ${diffTimeMin} minutes") var added = false for (i in 0 until values.size) { val entry = values[i] @@ -205,7 +213,7 @@ open class ChartViewer(private val chart: LineChart, val context: Context, priva added = true dataSet.addEntry(entry) val color = ReceiveData.getValueColor(entry.y.toInt()) - dataSet.addColor(color) + //dataSet.addColor(color) dataSet.circleColors.add(color) dataSet.notifyDataSetChanged() @@ -219,49 +227,60 @@ open class ChartViewer(private val chart: LineChart, val context: Context, priva } } } + if(added) { - chart.data = LineData(dataSet) - chart.notifyDataSetChanged() - val newDiffTime = TimeUnit.MILLISECONDS.toMinutes( TimeValueFormatter.from_chart_x(values.last().x).time - TimeValueFormatter.from_chart_x(chart.lowestVisibleX).time) + updateChart(dataSet) + } + } - var setXRange = false - if(!forBitmap && !chart.isScaleXEnabled && newDiffTime >= 90) { - Log.d(LOG_ID, "Enable X scale") - chart.isScaleXEnabled = true - setXRange = true - } + protected open fun updateChart(dataSet: LineDataSet) { + val defaultRange = getDefaultRange() + val right = isRight() + val left = isLeft() + Log.v(LOG_ID, "Min: ${chart.xAxis.axisMinimum} - visible: ${chart.lowestVisibleX} - Max: ${chart.xAxis.axisMaximum} - visible: ${chart.highestVisibleX} - isLeft: ${left} - isRight: ${right}" ) + var diffTimeMin = TimeUnit.MILLISECONDS.toMinutes(TimeValueFormatter.from_chart_x(chart.highestVisibleX).time - TimeValueFormatter.from_chart_x(chart.lowestVisibleX).time) + Log.d(LOG_ID, "Diff-Time: ${diffTimeMin} minutes") + chart.data = LineData(dataSet) + chart.notifyDataSetChanged() + val newDiffTime = TimeUnit.MILLISECONDS.toMinutes( TimeValueFormatter.from_chart_x(dataSet.values.last().x).time - TimeValueFormatter.from_chart_x(chart.lowestVisibleX).time) - if(right && left && diffTimeMin < 240L && newDiffTime >= 240L) { - Log.v(LOG_ID, "Set 4 hours diff time") - diffTimeMin = 240L - } + var setXRange = false + if(!chart.isScaleXEnabled && newDiffTime >= 90) { + Log.d(LOG_ID, "Enable X scale") + chart.isScaleXEnabled = true + setXRange = true + } - if(right) { - if(diffTimeMin >= 240L || !left) { - Log.d(LOG_ID, "Fix interval: $diffTimeMin") - chart.setVisibleXRangeMaximum(diffTimeMin.toFloat()) - setXRange = true - } - Log.v(LOG_ID, "moveViewToX ${values.last().x}") - chart.moveViewToX(values.last().x) - } else { - Log.v(LOG_ID, "Invalidate chart") + if((right && left && diffTimeMin < getDefaultRange() && newDiffTime >= defaultRange)) { + Log.v(LOG_ID, "Set ${defaultRange/60} hours diff time") + diffTimeMin = defaultRange + } + + if(right) { + if(diffTimeMin >= defaultRange || !left) { + Log.d(LOG_ID, "Fix interval: $diffTimeMin") chart.setVisibleXRangeMaximum(diffTimeMin.toFloat()) setXRange = true - chart.invalidate() } + Log.v(LOG_ID, "moveViewToX ${dataSet.values.last().x}") + chart.moveViewToX(dataSet.values.last().x) + } else { + Log.v(LOG_ID, "Invalidate chart") + chart.setVisibleXRangeMaximum(diffTimeMin.toFloat()) + setXRange = true + chart.invalidate() + } - if(setXRange) { - chart.setVisibleXRangeMinimum(60F) - chart.setVisibleXRangeMaximum(60F*24F) - } - notifyChanged() + if(setXRange) { + chart.setVisibleXRangeMinimum(60F) + chart.setVisibleXRangeMaximum(60F*24F) } } - private fun update() { + protected open fun update() { // update limit lines if(ReceiveData.time > 0) { + ChartData.addData(ReceiveData.time, ReceiveData.rawValue) val entry = Entry(TimeValueFormatter.to_chart_x(ReceiveData.time), ReceiveData.rawValue.toFloat()) addEntries(arrayListOf(entry)) } @@ -297,18 +316,13 @@ open class ChartViewer(private val chart: LineChart, val context: Context, priva fun getBitmap(): Bitmap? { try { if(chart.width > 0 && chart.height > 0) - return chart.chartBitmap + return chart.drawToBitmap() } catch (exc: Exception) { Log.e(LOG_ID, "getBitmap exception: " + exc.message.toString() ) } return null } - private fun notifyChanged() { - Log.d(LOG_ID, "notifyChanged") - InternalNotifier.notify(context, NotifySource.GRAPH_CHANGED, null) - } - private fun showOtherUnit(): Boolean { return sharedPref.getBoolean(Constants.SHARED_PREF_SHOW_OTHER_UNIT, false) } diff --git a/common/src/main/java/de/michelinside/glucodatahandler/common/chart/GlucoseChart.kt b/common/src/main/java/de/michelinside/glucodatahandler/common/chart/GlucoseChart.kt new file mode 100644 index 000000000..cde90b2b7 --- /dev/null +++ b/common/src/main/java/de/michelinside/glucodatahandler/common/chart/GlucoseChart.kt @@ -0,0 +1,40 @@ +package de.michelinside.glucodatahandler.common.chart + +import android.content.Context +import android.os.Bundle +import android.util.AttributeSet +import android.util.Log +import android.view.View +import com.github.mikephil.charting.charts.LineChart +import de.michelinside.glucodatahandler.common.Constants +import de.michelinside.glucodatahandler.common.notifier.InternalNotifier +import de.michelinside.glucodatahandler.common.notifier.NotifySource + +class GlucoseChart: LineChart { + private val LOG_ID = "GDH.Chart.GlucoseChart" + + constructor(context: Context?) : super(context) + + constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) + + constructor(context: Context?, attrs: AttributeSet?, defStyle: Int) : super( + context, + attrs, + defStyle + ) + + override fun invalidate() { + if(id == View.NO_ID) { + id = generateViewId() + } + Log.v(LOG_ID, "start invalidate - notify ID: $id") + super.invalidate() + Log.i(LOG_ID, "invalidate finished - notify ID: $id") + InternalNotifier.notify(context, NotifySource.GRAPH_CHANGED, Bundle().apply { putInt(Constants.GRAPH_ID, id) }) + } + + override fun onDetachedFromWindow() { + super.onDetachedFromWindow() + Log.d(LOG_ID, "onDetachedFromWindow for ID: $id") + } +} \ No newline at end of file diff --git a/mobile/src/main/java/de/michelinside/glucodatahandler/MainActivity.kt b/mobile/src/main/java/de/michelinside/glucodatahandler/MainActivity.kt index 7a8282bb4..5004ee695 100644 --- a/mobile/src/main/java/de/michelinside/glucodatahandler/MainActivity.kt +++ b/mobile/src/main/java/de/michelinside/glucodatahandler/MainActivity.kt @@ -37,6 +37,7 @@ import de.michelinside.glucodatahandler.common.ReceiveData import de.michelinside.glucodatahandler.common.SourceState import de.michelinside.glucodatahandler.common.SourceStateData import de.michelinside.glucodatahandler.common.WearPhoneConnection +import de.michelinside.glucodatahandler.common.chart.ChartBitmap import de.michelinside.glucodatahandler.common.chart.ChartViewer import de.michelinside.glucodatahandler.common.notification.AlarmHandler import de.michelinside.glucodatahandler.common.notification.AlarmState @@ -49,8 +50,6 @@ import de.michelinside.glucodatahandler.common.notifier.NotifierInterface import de.michelinside.glucodatahandler.common.notifier.NotifySource import de.michelinside.glucodatahandler.common.ui.Dialogs import de.michelinside.glucodatahandler.common.utils.BitmapUtils -import de.michelinside.glucodatahandler.common.utils.DummyGraphData -import de.michelinside.glucodatahandler.common.utils.GlucoDataUtils import de.michelinside.glucodatahandler.common.utils.PackageUtils import de.michelinside.glucodatahandler.common.utils.TextToSpeechUtils import de.michelinside.glucodatahandler.common.utils.Utils @@ -80,13 +79,15 @@ class MainActivity : AppCompatActivity(), NotifierInterface { private lateinit var sharedPref: SharedPreferences private lateinit var optionsMenu: Menu private lateinit var chart: LineChart + private lateinit var chartImage: ImageView private var alarmIcon: MenuItem? = null private var snoozeMenu: MenuItem? = null private var floatingWidgetItem: MenuItem? = null private val LOG_ID = "GDH.Main" private var requestNotificationPermission = false private var doNotUpdate = false - private lateinit var chartHandler: ChartViewer + private lateinit var chartViewer: ChartViewer + private lateinit var chartBitmap: ChartBitmap override fun onCreate(savedInstanceState: Bundle?) { try { @@ -110,6 +111,7 @@ class MainActivity : AppCompatActivity(), NotifierInterface { tableDetails = findViewById(R.id.tableDetails) tableNotes = findViewById(R.id.tableNotes) chart = findViewById(R.id.chart) + chartImage = findViewById(R.id.graphImage) PreferenceManager.setDefaultValues(this, R.xml.preferences, false) sharedPref = this.getSharedPreferences(Constants.SHARED_PREF_TAG, Context.MODE_PRIVATE) @@ -133,9 +135,9 @@ class MainActivity : AppCompatActivity(), NotifierInterface { if (requestPermission()) GlucoDataServiceMobile.start(this) TextToSpeechUtils.initTextToSpeech(this) - //createChart() - chartHandler = ChartViewer(chart, this) - chartHandler.create() + chartViewer = ChartViewer(chart, this) + chartViewer.create() + chartBitmap = ChartBitmap(chartImage, this) } catch (exc: Exception) { Log.e(LOG_ID, "onCreate exception: " + exc.message.toString() ) } @@ -188,7 +190,8 @@ class MainActivity : AppCompatActivity(), NotifierInterface { override fun onDestroy() { Log.v(LOG_ID, "onDestroy called") super.onDestroy() - chartHandler.close() + chartViewer.close() + chartBitmap.close() } private val requestPermissionLauncher = @@ -224,17 +227,15 @@ class MainActivity : AppCompatActivity(), NotifierInterface { .setTitle(CR.string.request_exact_alarm_title) .setMessage(CR.string.request_exact_alarm_summary) .setPositiveButton(CR.string.button_ok) { dialog, which -> - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - try { - startActivity( - Intent( - ACTION_REQUEST_SCHEDULE_EXACT_ALARM, - Uri.parse("package:$packageName") - ) + try { + startActivity( + Intent( + ACTION_REQUEST_SCHEDULE_EXACT_ALARM, + Uri.parse("package:$packageName") ) - } catch (exc: Exception) { - Log.e(LOG_ID, "requestExactAlarmPermission exception: " + exc.message.toString() ) - } + ) + } catch (exc: Exception) { + Log.e(LOG_ID, "requestExactAlarmPermission exception: " + exc.message.toString() ) } } .setNegativeButton(CR.string.button_cancel) { dialog, which -> @@ -564,7 +565,6 @@ class MainActivity : AppCompatActivity(), NotifierInterface { updateAlarmIcon() updateMenuItems() - } catch (exc: Exception) { Log.e(LOG_ID, "update exception: " + exc.message.toString() ) } diff --git a/mobile/src/main/res/layout/activity_main.xml b/mobile/src/main/res/layout/activity_main.xml index d8f39e3fc..802b38b3d 100644 --- a/mobile/src/main/res/layout/activity_main.xml +++ b/mobile/src/main/res/layout/activity_main.xml @@ -140,6 +140,11 @@ android:layout_height="200dp" /> + + Date: Mon, 20 Jan 2025 09:38:13 +0100 Subject: [PATCH 03/17] Rename class --- .../common/chart/ChartBitmapCreator.kt | 2 +- .../common/chart/{ChartViewer.kt => ChartCreator.kt} | 4 ++-- .../de/michelinside/glucodatahandler/MainActivity.kt | 10 +++++----- 3 files changed, 8 insertions(+), 8 deletions(-) rename common/src/main/java/de/michelinside/glucodatahandler/common/chart/{ChartViewer.kt => ChartCreator.kt} (98%) diff --git a/common/src/main/java/de/michelinside/glucodatahandler/common/chart/ChartBitmapCreator.kt b/common/src/main/java/de/michelinside/glucodatahandler/common/chart/ChartBitmapCreator.kt index 44a1957cb..1fdf4a875 100644 --- a/common/src/main/java/de/michelinside/glucodatahandler/common/chart/ChartBitmapCreator.kt +++ b/common/src/main/java/de/michelinside/glucodatahandler/common/chart/ChartBitmapCreator.kt @@ -7,7 +7,7 @@ import com.github.mikephil.charting.data.LineData import com.github.mikephil.charting.data.LineDataSet import de.michelinside.glucodatahandler.common.ReceiveData -class ChartBitmapCreator(chart: LineChart, context: Context): ChartViewer(chart, context) { +class ChartBitmapCreator(chart: LineChart, context: Context): ChartCreator(chart, context) { private val LOG_ID = "GDH.Chart.BitmapCreator" override fun initXaxis() { diff --git a/common/src/main/java/de/michelinside/glucodatahandler/common/chart/ChartViewer.kt b/common/src/main/java/de/michelinside/glucodatahandler/common/chart/ChartCreator.kt similarity index 98% rename from common/src/main/java/de/michelinside/glucodatahandler/common/chart/ChartViewer.kt rename to common/src/main/java/de/michelinside/glucodatahandler/common/chart/ChartCreator.kt index 569cb7620..5c388c1c2 100644 --- a/common/src/main/java/de/michelinside/glucodatahandler/common/chart/ChartViewer.kt +++ b/common/src/main/java/de/michelinside/glucodatahandler/common/chart/ChartCreator.kt @@ -25,9 +25,9 @@ import de.michelinside.glucodatahandler.common.utils.DummyGraphData import java.util.concurrent.TimeUnit -open class ChartViewer(protected val chart: LineChart, protected val context: Context): NotifierInterface, +open class ChartCreator(protected val chart: LineChart, protected val context: Context): NotifierInterface, SharedPreferences.OnSharedPreferenceChangeListener { - private val LOG_ID = "GDH.Chart.Viewer" + private val LOG_ID = "GDH.Chart.Creator" protected var created = false protected val sharedPref = context.getSharedPreferences(Constants.SHARED_PREF_TAG, Context.MODE_PRIVATE) protected var init = false diff --git a/mobile/src/main/java/de/michelinside/glucodatahandler/MainActivity.kt b/mobile/src/main/java/de/michelinside/glucodatahandler/MainActivity.kt index 5004ee695..cc6e6bb2e 100644 --- a/mobile/src/main/java/de/michelinside/glucodatahandler/MainActivity.kt +++ b/mobile/src/main/java/de/michelinside/glucodatahandler/MainActivity.kt @@ -38,7 +38,7 @@ import de.michelinside.glucodatahandler.common.SourceState import de.michelinside.glucodatahandler.common.SourceStateData import de.michelinside.glucodatahandler.common.WearPhoneConnection import de.michelinside.glucodatahandler.common.chart.ChartBitmap -import de.michelinside.glucodatahandler.common.chart.ChartViewer +import de.michelinside.glucodatahandler.common.chart.ChartCreator import de.michelinside.glucodatahandler.common.notification.AlarmHandler import de.michelinside.glucodatahandler.common.notification.AlarmState import de.michelinside.glucodatahandler.common.notification.AlarmType @@ -86,7 +86,7 @@ class MainActivity : AppCompatActivity(), NotifierInterface { private val LOG_ID = "GDH.Main" private var requestNotificationPermission = false private var doNotUpdate = false - private lateinit var chartViewer: ChartViewer + private lateinit var chartCreator: ChartCreator private lateinit var chartBitmap: ChartBitmap override fun onCreate(savedInstanceState: Bundle?) { @@ -135,8 +135,8 @@ class MainActivity : AppCompatActivity(), NotifierInterface { if (requestPermission()) GlucoDataServiceMobile.start(this) TextToSpeechUtils.initTextToSpeech(this) - chartViewer = ChartViewer(chart, this) - chartViewer.create() + chartCreator = ChartCreator(chart, this) + chartCreator.create() chartBitmap = ChartBitmap(chartImage, this) } catch (exc: Exception) { Log.e(LOG_ID, "onCreate exception: " + exc.message.toString() ) @@ -190,7 +190,7 @@ class MainActivity : AppCompatActivity(), NotifierInterface { override fun onDestroy() { Log.v(LOG_ID, "onDestroy called") super.onDestroy() - chartViewer.close() + chartCreator.close() chartBitmap.close() } From fd5cb42b27485d05e09b88eae81f59feb4dd0bb3 Mon Sep 17 00:00:00 2001 From: pachi81 Date: Mon, 20 Jan 2025 18:58:40 +0100 Subject: [PATCH 04/17] Chart time line added --- build.gradle | 2 +- .../common/chart/ChartBitmapCreator.kt | 27 ++- .../common/chart/ChartCreator.kt | 159 ++++++++++++++---- .../common/chart/ChartData.kt | 11 +- .../common/chart/CustomBubbleMarker.kt | 23 ++- common/src/main/res/layout/marker_layout.xml | 1 + 6 files changed, 178 insertions(+), 45 deletions(-) diff --git a/build.gradle b/build.gradle index 4347938ed..170ddaf3f 100644 --- a/build.gradle +++ b/build.gradle @@ -9,7 +9,7 @@ Properties properties = new Properties() properties.load(project.rootProject.file('local.properties').newDataInputStream()) project.ext.set("versionCode", 98) -project.ext.set("versionName", "1.3") +project.ext.set("versionName", "1.3.1") project.ext.set("compileSdk", 34) project.ext.set("targetSdk", 34) project.ext.set("minSdk", 26) diff --git a/common/src/main/java/de/michelinside/glucodatahandler/common/chart/ChartBitmapCreator.kt b/common/src/main/java/de/michelinside/glucodatahandler/common/chart/ChartBitmapCreator.kt index 1fdf4a875..f46b07408 100644 --- a/common/src/main/java/de/michelinside/glucodatahandler/common/chart/ChartBitmapCreator.kt +++ b/common/src/main/java/de/michelinside/glucodatahandler/common/chart/ChartBitmapCreator.kt @@ -30,16 +30,20 @@ class ChartBitmapCreator(chart: LineChart, context: Context): ChartCreator(chart } override fun getDefaultRange(): Long { - return 120L + return 60L } override fun getMinTime(): Long { - return System.currentTimeMillis() - getDefaultRange()*60*1000L + return System.currentTimeMillis() - (getDefaultRange()*60*1000L) } override fun updateChart(dataSet: LineDataSet) { Log.v(LOG_ID, "Update chart for ${dataSet.values.size} entries and ${dataSet.circleColors.size} colors") - chart.data = LineData(dataSet) + if(dataSet.values.isNotEmpty()) + chart.data = LineData(dataSet) + else + chart.data = LineData() + addEmptyTimeData() chart.notifyDataSetChanged() chart.invalidate() } @@ -51,18 +55,23 @@ class ChartBitmapCreator(chart: LineChart, context: Context): ChartCreator(chart chart.data?.clearValues() chart.data?.notifyDataChanged() chart.notifyDataSetChanged() - //chart.clear() - initData() + if(ChartData.hasData(getMinTime())) { + initData() + } else { + Log.d(LOG_ID, "no data available for bitmap - reset chart") + create() + updateNotifier() + } } override fun update() { // update limit lines - if(ReceiveData.time > 0) { - ChartData.addData(ReceiveData.time, ReceiveData.rawValue) - createBitmap() - } + createBitmap() } + override fun updateTimeElapsed() { + update() + } } \ No newline at end of file diff --git a/common/src/main/java/de/michelinside/glucodatahandler/common/chart/ChartCreator.kt b/common/src/main/java/de/michelinside/glucodatahandler/common/chart/ChartCreator.kt index 5c388c1c2..88545ac17 100644 --- a/common/src/main/java/de/michelinside/glucodatahandler/common/chart/ChartCreator.kt +++ b/common/src/main/java/de/michelinside/glucodatahandler/common/chart/ChartCreator.kt @@ -14,7 +14,9 @@ import com.github.mikephil.charting.components.YAxis import com.github.mikephil.charting.data.Entry import com.github.mikephil.charting.data.LineData import com.github.mikephil.charting.data.LineDataSet -import com.github.mikephil.charting.utils.Utils +import com.github.mikephil.charting.highlight.Highlight +import com.github.mikephil.charting.listener.OnChartValueSelectedListener +import com.github.mikephil.charting.utils.Utils as ChartUtils import de.michelinside.glucodatahandler.common.Constants import de.michelinside.glucodatahandler.common.R import de.michelinside.glucodatahandler.common.ReceiveData @@ -22,6 +24,7 @@ import de.michelinside.glucodatahandler.common.notifier.InternalNotifier import de.michelinside.glucodatahandler.common.notifier.NotifierInterface import de.michelinside.glucodatahandler.common.notifier.NotifySource import de.michelinside.glucodatahandler.common.utils.DummyGraphData +import de.michelinside.glucodatahandler.common.utils.Utils import java.util.concurrent.TimeUnit @@ -31,6 +34,7 @@ open class ChartCreator(protected val chart: LineChart, protected val context: C protected var created = false protected val sharedPref = context.getSharedPreferences(Constants.SHARED_PREF_TAG, Context.MODE_PRIVATE) protected var init = false + protected var hasTimeNotifier = false private val demoMode = false @@ -48,10 +52,21 @@ open class ChartCreator(protected val chart: LineChart, protected val context: C private fun init() { if(!init) { Log.d(LOG_ID, "init") - Utils.init(context) + ChartUtils.init(context) sharedPref.registerOnSharedPreferenceChangeListener(this) if(!demoMode) - InternalNotifier.addNotifier(context, this, mutableSetOf(NotifySource.MESSAGECLIENT, NotifySource.BROADCAST)) + updateNotifier() + } + } + + protected fun updateNotifier() { + Log.d(LOG_ID, "updateNotifier - has data: ${ChartData.hasData(getMinTime())} - xAxisEnabled: ${chart.xAxis.isEnabled}") + if(ChartData.hasData(getMinTime()) || chart.xAxis.isEnabled) { + InternalNotifier.addNotifier(context, this, mutableSetOf(NotifySource.MESSAGECLIENT, NotifySource.BROADCAST, NotifySource.TIME_VALUE)) + hasTimeNotifier = true + } else { + InternalNotifier.addNotifier(context, this, mutableSetOf(NotifySource.MESSAGECLIENT, NotifySource.BROADCAST)) + hasTimeNotifier = false } } @@ -93,6 +108,7 @@ open class ChartCreator(protected val chart: LineChart, protected val context: C private fun resetChart() { Log.v(LOG_ID, "resetChart") + //chart.highlightValue(null) chart.fitScreen() chart.data?.clearValues() chart.axisRight.removeAllLimitLines() @@ -168,6 +184,18 @@ open class ChartCreator(protected val chart: LineChart, protected val context: C val mMarker = CustomBubbleMarker(context) mMarker.chartView = chart chart.marker = mMarker + chart.setOnChartValueSelectedListener(object: OnChartValueSelectedListener { + override fun onValueSelected(e: Entry?, h: Highlight?) { + Log.v(LOG_ID, "onValueSelected: ${e?.x} - ${e?.y} - index: ${h?.dataSetIndex}") + if(h?.dataSetIndex == 1) { + // do not select time line + chart.highlightValue(null) + } + } + override fun onNothingSelected() { + Log.v(LOG_ID, "onNothingSelected") + } + }) } chart.axisRight.removeAllLimitLines() @@ -202,34 +230,91 @@ open class ChartCreator(protected val chart: LineChart, protected val context: C protected open fun addEntries(values: ArrayList) { Log.d(LOG_ID, "Add ${values.size} entries") - if(values.isEmpty()) - return - val dataSet = chart.data.getDataSetByIndex(0) as LineDataSet var added = false - for (i in 0 until values.size) { - val entry = values[i] - if(dataSet.values.isEmpty() || dataSet.values.last().x < entry.x) { - added = true - dataSet.addEntry(entry) - val color = ReceiveData.getValueColor(entry.y.toInt()) - //dataSet.addColor(color) - dataSet.circleColors.add(color) - dataSet.notifyDataSetChanged() - - if(chart.axisRight.axisMinimum > (entry.y-10F)) { - chart.axisRight.axisMinimum = entry.y-10F - chart.axisLeft.axisMinimum = chart.axisRight.axisMinimum - } - if(chart.axisRight.axisMaximum < (entry.y+10F)) { - chart.axisRight.axisMaximum = entry.y+10F - chart.axisLeft.axisMaximum = chart.axisRight.axisMaximum + if(values.isNotEmpty()) { + for (i in 0 until values.size) { + val entry = values[i] + if (dataSet.values.isEmpty() || dataSet.values.last().x < entry.x) { + added = true + dataSet.addEntry(entry) + val color = ReceiveData.getValueColor(entry.y.toInt()) + //dataSet.addColor(color) + dataSet.circleColors.add(color) + dataSet.notifyDataSetChanged() + + if (chart.axisRight.axisMinimum > (entry.y - 10F)) { + chart.axisRight.axisMinimum = entry.y - 10F + chart.axisLeft.axisMinimum = chart.axisRight.axisMinimum + } + if (chart.axisRight.axisMaximum < (entry.y + 10F)) { + chart.axisRight.axisMaximum = entry.y + 10F + chart.axisLeft.axisMaximum = chart.axisRight.axisMaximum + } } } } if(added) { updateChart(dataSet) + } else { + addEmptyTimeData() + } + } + + protected fun addEmptyTimeData() { + Log.v(LOG_ID, "addEmptyTimeData - entries: ${chart.data?.entryCount}") + if(chart.data?.entryCount == 0 && !chart.xAxis.isEnabled) { + if(hasTimeNotifier) + updateNotifier() + return // no data and not xAxis, do not add the time line, as it is not needed + } + if(!hasTimeNotifier) + updateNotifier() + var minTime = getMinTime() + if (minTime == 0L) // at least one hour should be shown + minTime = System.currentTimeMillis() - (60 * 60 * 1000L) + + var minValue = 0L + var maxValue = 0L + + if(chart.data != null && chart.data.dataSetCount > 0) { + val dataSet = chart.data.getDataSetByIndex(0) as LineDataSet + if (dataSet.values.isEmpty()) { + Log.v(LOG_ID, "No values, set min and max") + minValue = minTime + maxValue = System.currentTimeMillis() + } else { + Log.v( + LOG_ID, + "Min time: ${Utils.getUiTimeStamp(minTime)} - first value: ${ + Utils.getUiTimeStamp(TimeValueFormatter.from_chart_x(dataSet.values.first().x).time) + } - last value: ${Utils.getUiTimeStamp(TimeValueFormatter.from_chart_x(dataSet.values.last().x).time)}" + ) + if (TimeValueFormatter.from_chart_x(dataSet.values.first().x).time > minTime) { + minValue = minTime + } + if (Utils.getElapsedTimeMinute(TimeValueFormatter.from_chart_x(dataSet.values.last().x).time) >= 1) { + maxValue = System.currentTimeMillis() + } + } + } + if(minValue > 0 || maxValue > 0) { + Log.w(LOG_ID, "Creating extra data set from ${Utils.getUiTimeStamp(minValue)} - ${Utils.getUiTimeStamp(maxValue)}") + val entries = ArrayList() + if(minValue > 0) + entries.add(Entry(TimeValueFormatter.to_chart_x(minValue), 120F)) + if(maxValue > 0) + entries.add(Entry(TimeValueFormatter.to_chart_x(maxValue), 120F)) + + val timeSet = LineDataSet(entries, "Time line") + timeSet.lineWidth = 1F + timeSet.circleRadius = 1F + timeSet.setDrawValues(false) + timeSet.setDrawCircleHole(false) + timeSet.setCircleColors(0) + timeSet.setColors(0) + chart.data.addDataSet(timeSet) } } @@ -239,11 +324,15 @@ open class ChartCreator(protected val chart: LineChart, protected val context: C val left = isLeft() Log.v(LOG_ID, "Min: ${chart.xAxis.axisMinimum} - visible: ${chart.lowestVisibleX} - Max: ${chart.xAxis.axisMaximum} - visible: ${chart.highestVisibleX} - isLeft: ${left} - isRight: ${right}" ) var diffTimeMin = TimeUnit.MILLISECONDS.toMinutes(TimeValueFormatter.from_chart_x(chart.highestVisibleX).time - TimeValueFormatter.from_chart_x(chart.lowestVisibleX).time) - Log.d(LOG_ID, "Diff-Time: ${diffTimeMin} minutes") + if(!chart.highlighted.isNullOrEmpty() && chart.highlighted[0].dataSetIndex != 0) { + Log.v(LOG_ID, "Unset current highlighter") + chart.highlightValue(null) + } chart.data = LineData(dataSet) + addEmptyTimeData() chart.notifyDataSetChanged() - val newDiffTime = TimeUnit.MILLISECONDS.toMinutes( TimeValueFormatter.from_chart_x(dataSet.values.last().x).time - TimeValueFormatter.from_chart_x(chart.lowestVisibleX).time) - + val newDiffTime = TimeUnit.MILLISECONDS.toMinutes( TimeValueFormatter.from_chart_x(chart.xChartMax).time - TimeValueFormatter.from_chart_x(chart.lowestVisibleX).time) + Log.d(LOG_ID, "Diff-Time: ${diffTimeMin} minutes - newDiffTime: ${newDiffTime} minutes") var setXRange = false if(!chart.isScaleXEnabled && newDiffTime >= 90) { Log.d(LOG_ID, "Enable X scale") @@ -262,8 +351,8 @@ open class ChartCreator(protected val chart: LineChart, protected val context: C chart.setVisibleXRangeMaximum(diffTimeMin.toFloat()) setXRange = true } - Log.v(LOG_ID, "moveViewToX ${dataSet.values.last().x}") - chart.moveViewToX(dataSet.values.last().x) + Log.v(LOG_ID, "moveViewToX ${chart.xChartMax}") + chart.moveViewToX(chart.xChartMax) } else { Log.v(LOG_ID, "Invalidate chart") chart.setVisibleXRangeMaximum(diffTimeMin.toFloat()) @@ -280,16 +369,26 @@ open class ChartCreator(protected val chart: LineChart, protected val context: C protected open fun update() { // update limit lines if(ReceiveData.time > 0) { - ChartData.addData(ReceiveData.time, ReceiveData.rawValue) val entry = Entry(TimeValueFormatter.to_chart_x(ReceiveData.time), ReceiveData.rawValue.toFloat()) addEntries(arrayListOf(entry)) } } + protected open fun updateTimeElapsed() { + Log.v(LOG_ID, "update time elapsed") + updateChart(chart.data.getDataSetByIndex(0) as LineDataSet) + } + override fun OnNotifyData(context: Context, dataSource: NotifySource, extras: Bundle?) { try { Log.d(LOG_ID, "OnNotifyData: $dataSource") - update() + if(dataSource == NotifySource.TIME_VALUE) { + Log.d(LOG_ID, "time elapsed: ${ReceiveData.getElapsedTimeMinute()}") + if(ReceiveData.getElapsedTimeMinute().mod(2) == 0) + updateTimeElapsed() + } else { + update() + } } catch (exc: Exception) { Log.e(LOG_ID, "OnNotifyData exception: " + exc.message.toString() + " - " + exc.stackTraceToString() ) } diff --git a/common/src/main/java/de/michelinside/glucodatahandler/common/chart/ChartData.kt b/common/src/main/java/de/michelinside/glucodatahandler/common/chart/ChartData.kt index 5c9bcbcb4..e73606cc4 100644 --- a/common/src/main/java/de/michelinside/glucodatahandler/common/chart/ChartData.kt +++ b/common/src/main/java/de/michelinside/glucodatahandler/common/chart/ChartData.kt @@ -1,12 +1,16 @@ package de.michelinside.glucodatahandler.common.chart +import android.util.Log import com.github.mikephil.charting.data.Entry import de.michelinside.glucodatahandler.common.utils.DummyGraphData +import de.michelinside.glucodatahandler.common.utils.Utils object ChartData { - private var graphData = DummyGraphData.create(48, min = 40, max = 260, stepMinute = 5).toMutableMap() + private val LOG_ID = "GDH.Chart.Data" + private var graphData = DummyGraphData.create(0, min = 40, max = 260, stepMinute = 5).toMutableMap() fun getData(minTime: Long = 0L): ArrayList { + Log.d(LOG_ID, "getData - minTime: ${Utils.getUiTimeStamp(minTime)}") val entries = ArrayList() graphData.forEach { (t, u) -> if(t >= minTime) { @@ -16,7 +20,12 @@ object ChartData { return entries } + fun hasData(minTime: Long = 0L): Boolean { + return graphData.keys.any { it >= minTime } + } + fun addData(time: Long, value: Int) { + Log.v(LOG_ID, "addData - time: ${Utils.getUiTimeStamp(time)} - value: $value") graphData[time] = value } diff --git a/common/src/main/java/de/michelinside/glucodatahandler/common/chart/CustomBubbleMarker.kt b/common/src/main/java/de/michelinside/glucodatahandler/common/chart/CustomBubbleMarker.kt index 1e80c5bf4..c10c09577 100644 --- a/common/src/main/java/de/michelinside/glucodatahandler/common/chart/CustomBubbleMarker.kt +++ b/common/src/main/java/de/michelinside/glucodatahandler/common/chart/CustomBubbleMarker.kt @@ -6,6 +6,7 @@ import android.graphics.Paint import android.graphics.Path import android.util.Log import android.widget.TextView +import androidx.appcompat.widget.LinearLayoutCompat import androidx.core.content.ContextCompat import com.github.mikephil.charting.components.MarkerView import com.github.mikephil.charting.data.Entry @@ -18,14 +19,26 @@ class CustomBubbleMarker(context: Context) : MarkerView(context, R.layout.marker private val LOG_ID = "GDH.Chart.MarkerView" private val arrowSize = 35 private val arrowCircleOffset = 0f + private var isGlucose = true - override fun refreshContent(e: Entry?, highlight: Highlight?) { + override fun refreshContent(e: Entry?, highlight: Highlight) { + Log.v(LOG_ID, "refreshContent - index ${highlight.dataSetIndex}") try { + isGlucose = highlight.dataSetIndex == 0 e?.let { val time: TextView = this.findViewById(R.id.time) val glucose: TextView = this.findViewById(R.id.glucose) - time.text = DateFormat.getTimeInstance(DateFormat.DEFAULT).format(TimeValueFormatter.from_chart_x(e.x)) - glucose.text = GlucoseFormatter.getValueAsString(e.y) + val layout: LinearLayoutCompat = this.findViewById(R.id.marker_layout) + if(isGlucose) { + time.text = DateFormat.getTimeInstance(DateFormat.DEFAULT) + .format(TimeValueFormatter.from_chart_x(e.x)) + glucose.text = GlucoseFormatter.getValueAsString(e.y) + layout.visibility = VISIBLE + } else { + time.text = "" + glucose.text = "" + layout.visibility = GONE + } } } catch (exc: Exception) { Log.e(LOG_ID, "Exception in refreshContent", exc) @@ -34,6 +47,7 @@ class CustomBubbleMarker(context: Context) : MarkerView(context, R.layout.marker } override fun getOffsetForDrawingAtPoint(posX: Float, posY: Float): MPPointF { + Log.v(LOG_ID, "getOffsetForDrawingAtPoint - isGlucose: $isGlucose") val offset = offset try { val chart = chartView @@ -62,11 +76,12 @@ class CustomBubbleMarker(context: Context) : MarkerView(context, R.layout.marker } override fun draw(canvas: Canvas, posX: Float, posY: Float) { + Log.v(LOG_ID, "draw - isGlucose: $isGlucose") try { val paint = Paint().apply { style = Paint.Style.FILL strokeJoin = Paint.Join.ROUND - color = ContextCompat.getColor(context, R.color.transparent_widget_background) + color = if(isGlucose) ContextCompat.getColor(context, R.color.transparent_widget_background) else 0 } val chart = chartView diff --git a/common/src/main/res/layout/marker_layout.xml b/common/src/main/res/layout/marker_layout.xml index 1080688b6..0431d490d 100644 --- a/common/src/main/res/layout/marker_layout.xml +++ b/common/src/main/res/layout/marker_layout.xml @@ -1,6 +1,7 @@ Date: Tue, 21 Jan 2025 16:56:12 +0100 Subject: [PATCH 05/17] Create chart data from LibreLinkUp --- .../glucodatahandler/common/ReceiveData.kt | 12 +++-- .../common/chart/ChartData.kt | 18 +++++-- .../common/notifier/NotifySource.kt | 3 +- .../common/tasks/LibreLinkSourceTask.kt | 47 ++++++++++++++----- 4 files changed, 60 insertions(+), 20 deletions(-) diff --git a/common/src/main/java/de/michelinside/glucodatahandler/common/ReceiveData.kt b/common/src/main/java/de/michelinside/glucodatahandler/common/ReceiveData.kt index 544cb8d20..3ad90d1ab 100644 --- a/common/src/main/java/de/michelinside/glucodatahandler/common/ReceiveData.kt +++ b/common/src/main/java/de/michelinside/glucodatahandler/common/ReceiveData.kt @@ -5,6 +5,7 @@ import android.content.SharedPreferences import android.graphics.Color import android.os.Bundle import android.util.Log +import de.michelinside.glucodatahandler.common.chart.ChartData import de.michelinside.glucodatahandler.common.notification.AlarmHandler import de.michelinside.glucodatahandler.common.notification.AlarmNotificationBase import de.michelinside.glucodatahandler.common.notification.AlarmType @@ -324,13 +325,14 @@ object ReceiveData: SharedPreferences.OnSharedPreferenceChangeListener { } fun getValueColor(rawValue: Int): Int { - if(highValue>0F && rawValue >= highValue) + val customValue = if(isMmol) GlucoDataUtils.mgToMmol(rawValue.toFloat()) else rawValue.toFloat() + if(high>0F && customValue >= high) return getAlarmTypeColor(AlarmType.VERY_HIGH) - if(lowValue>0F && rawValue <= lowValue) + if(low>0F && customValue <= low) return getAlarmTypeColor(AlarmType.VERY_LOW) - if(targetMinValue>0F && rawValue < targetMinValue) + if(targetMin>0F && customValue < targetMin) return getAlarmTypeColor(AlarmType.LOW) - if(targetMaxValue>0F && rawValue > targetMaxValue) + if(targetMax>0F && customValue > targetMax) return getAlarmTypeColor(AlarmType.HIGH) return getAlarmTypeColor(AlarmType.OK) } @@ -557,6 +559,8 @@ object ReceiveData: SharedPreferences.OnSharedPreferenceChangeListener { } } + ChartData.addData(time, rawValue) + val notifySource = if(interApp) NotifySource.MESSAGECLIENT else NotifySource.BROADCAST InternalNotifier.notify(context, notifySource, createExtras()) // re-create extras to have all changed value inside... diff --git a/common/src/main/java/de/michelinside/glucodatahandler/common/chart/ChartData.kt b/common/src/main/java/de/michelinside/glucodatahandler/common/chart/ChartData.kt index e73606cc4..beedd6c49 100644 --- a/common/src/main/java/de/michelinside/glucodatahandler/common/chart/ChartData.kt +++ b/common/src/main/java/de/michelinside/glucodatahandler/common/chart/ChartData.kt @@ -7,16 +7,18 @@ import de.michelinside.glucodatahandler.common.utils.Utils object ChartData { private val LOG_ID = "GDH.Chart.Data" - private var graphData = DummyGraphData.create(0, min = 40, max = 260, stepMinute = 5).toMutableMap() + private var graphData = mutableMapOf() // DummyGraphData.create(0, min = 40, max = 260, stepMinute = 5).toMutableMap() fun getData(minTime: Long = 0L): ArrayList { Log.d(LOG_ID, "getData - minTime: ${Utils.getUiTimeStamp(minTime)}") val entries = ArrayList() - graphData.forEach { (t, u) -> + graphData.toSortedMap().forEach { (t, u) -> if(t >= minTime) { entries.add(Entry(TimeValueFormatter.to_chart_x(t), u.toFloat())) } } + if(entries.isNotEmpty()) + Log.v(LOG_ID, "getData - entries: ${entries.size} from ${Utils.getUiTimeStamp(TimeValueFormatter.from_chart_x(entries.first().x).time)} to ${Utils.getUiTimeStamp(TimeValueFormatter.from_chart_x(entries.last().x).time)}") return entries } @@ -25,8 +27,16 @@ object ChartData { } fun addData(time: Long, value: Int) { - Log.v(LOG_ID, "addData - time: ${Utils.getUiTimeStamp(time)} - value: $value") - graphData[time] = value + try { + Log.v(LOG_ID, "addData - time: ${Utils.getUiTimeStamp(time)} - value: $value") + graphData[time] = value + } catch (exc: Exception) { + Log.e(LOG_ID, "addData exception: " + exc.toString() ) + } + } + + fun needsData(): Boolean { + return graphData.size < 5 } } \ No newline at end of file diff --git a/common/src/main/java/de/michelinside/glucodatahandler/common/notifier/NotifySource.kt b/common/src/main/java/de/michelinside/glucodatahandler/common/notifier/NotifySource.kt index 12dc169da..ee49e1584 100644 --- a/common/src/main/java/de/michelinside/glucodatahandler/common/notifier/NotifySource.kt +++ b/common/src/main/java/de/michelinside/glucodatahandler/common/notifier/NotifySource.kt @@ -27,5 +27,6 @@ enum class NotifySource { NEW_VERSION_AVAILABLE, TTS_STATE_CHANGED, DISPLAY_STATE_CHANGED, - GRAPH_CHANGED; + GRAPH_CHANGED, + GRAPH_DATA_CHANGED; } \ No newline at end of file diff --git a/common/src/main/java/de/michelinside/glucodatahandler/common/tasks/LibreLinkSourceTask.kt b/common/src/main/java/de/michelinside/glucodatahandler/common/tasks/LibreLinkSourceTask.kt index b80894ca3..75a04ee90 100644 --- a/common/src/main/java/de/michelinside/glucodatahandler/common/tasks/LibreLinkSourceTask.kt +++ b/common/src/main/java/de/michelinside/glucodatahandler/common/tasks/LibreLinkSourceTask.kt @@ -10,6 +10,7 @@ import de.michelinside.glucodatahandler.common.GlucoDataService import de.michelinside.glucodatahandler.common.R import de.michelinside.glucodatahandler.common.ReceiveData import de.michelinside.glucodatahandler.common.SourceState +import de.michelinside.glucodatahandler.common.chart.ChartData import de.michelinside.glucodatahandler.common.notifier.DataSource import de.michelinside.glucodatahandler.common.notifier.InternalNotifier import de.michelinside.glucodatahandler.common.notifier.NotifySource @@ -404,12 +405,18 @@ class LibreLinkSourceTask : DataSourceTask(Constants.SHARED_PREF_LIBRE_ENABLED, val jsonObject = checkResponse(body) if (jsonObject != null && jsonObject.has("data")) { val data = jsonObject.optJSONObject("data") - if(data != null && data.has("connection")) { - val connection = data.optJSONObject("connection") - if(connection!=null) - return parseConnectionData(connection) + if(data != null) { + if(ChartData.needsData() && data.has("graphData")) { + parseGraphData(data.optJSONArray("graphData")) + } + if(data.has("connection")) { + val connection = data.optJSONObject("connection") + if(connection!=null) + return parseConnectionData(connection) + } } } + Log.e(LOG_ID, "No data found in response: ${replaceSensitiveData(body!!)}") setLastError("Invalid response! Please send logs to developer.") reset() @@ -417,6 +424,28 @@ class LibreLinkSourceTask : DataSourceTask(Constants.SHARED_PREF_LIBRE_ENABLED, return false } + private fun parseGraphData(graphData: JSONArray?) { + try { + if(graphData != null && graphData.length() > 0) { + Log.d(LOG_ID, "Parse graph data for ${graphData.length()} entries") + for (i in 0 until graphData.length()) { + val glucoseData = graphData.optJSONObject(i) + if(glucoseData != null) { + val parsedUtc = parseUtcTimestamp(glucoseData.optString("FactoryTimestamp")) + val parsedLocal = parseLocalTimestamp(glucoseData.optString("Timestamp")) + val time = if (parsedUtc > 0) parsedUtc else parsedLocal + val value = glucoseData.optInt("ValueInMgPerDl") + if(value > 0 && time > 0) + ChartData.addData(time, value) + } + } + InternalNotifier.notify(GlucoDataService.context!!, NotifySource.GRAPH_DATA_CHANGED, null) + } + } catch (exc: Exception) { + Log.e(LOG_ID, "parseGraphData exception: " + exc.toString() ) + } + } + private fun handleGlucoseResponse(body: String?): Boolean { /* { @@ -464,6 +493,9 @@ class LibreLinkSourceTask : DataSourceTask(Constants.SHARED_PREF_LIBRE_ENABLED, setState(SourceState.NO_NEW_VALUE, GlucoDataService.context!!.resources.getString(R.string.no_data_in_server_response)) return false } + if(ChartData.needsData() && patientId.isNotEmpty() && patientData.isNotEmpty()) { + return handleGraphResponse(httpGet(getUrl(GRAPH_ENDPOINT.format(patientId)), getHeader())) + } return parseConnectionData(data) } else { Log.e(LOG_ID, "No data array found in response: ${replaceSensitiveData(body!!)}") @@ -490,13 +522,6 @@ class LibreLinkSourceTask : DataSourceTask(Constants.SHARED_PREF_LIBRE_ENABLED, glucoExtras.putFloat(ReceiveData.GLUCOSECUSTOM, glucoseData.optDouble("Value").toFloat()) glucoExtras.putInt(ReceiveData.MGDL, glucoseData.optInt("ValueInMgPerDl")) glucoExtras.putFloat(ReceiveData.RATE, getRateFromTrend(glucoseData.optInt("TrendArrow"))) - if (glucoseData.optBoolean("isHigh")) - glucoExtras.putInt(ReceiveData.ALARM, 6) - else if (glucoseData.optBoolean("isLow")) - glucoExtras.putInt(ReceiveData.ALARM, 7) - else - glucoExtras.putInt(ReceiveData.ALARM, 0) - glucoExtras.putString(ReceiveData.SERIAL, "LibreLink") if (data.has("sensor")) { val sensor = data.optJSONObject("sensor") From e0609a75dbcf10459a3974f29473489d36773362 Mon Sep 17 00:00:00 2001 From: pachi81 Date: Tue, 21 Jan 2025 16:58:15 +0100 Subject: [PATCH 06/17] Reset chart for GraphData changed --- .../common/chart/ChartBitmapCreator.kt | 18 +++----- .../common/chart/ChartCreator.kt | 44 +++++++++++++++---- .../common/chart/GlucoseChart.kt | 43 +++++++++++++++--- .../glucodatahandler/MainActivity.kt | 4 +- mobile/src/main/res/layout/activity_main.xml | 2 +- 5 files changed, 79 insertions(+), 32 deletions(-) diff --git a/common/src/main/java/de/michelinside/glucodatahandler/common/chart/ChartBitmapCreator.kt b/common/src/main/java/de/michelinside/glucodatahandler/common/chart/ChartBitmapCreator.kt index f46b07408..b25bf8b6a 100644 --- a/common/src/main/java/de/michelinside/glucodatahandler/common/chart/ChartBitmapCreator.kt +++ b/common/src/main/java/de/michelinside/glucodatahandler/common/chart/ChartBitmapCreator.kt @@ -2,12 +2,10 @@ package de.michelinside.glucodatahandler.common.chart import android.content.Context import android.util.Log -import com.github.mikephil.charting.charts.LineChart import com.github.mikephil.charting.data.LineData import com.github.mikephil.charting.data.LineDataSet -import de.michelinside.glucodatahandler.common.ReceiveData -class ChartBitmapCreator(chart: LineChart, context: Context): ChartCreator(chart, context) { +class ChartBitmapCreator(chart: GlucoseChart, context: Context): ChartCreator(chart, context) { private val LOG_ID = "GDH.Chart.BitmapCreator" override fun initXaxis() { @@ -30,7 +28,7 @@ class ChartBitmapCreator(chart: LineChart, context: Context): ChartCreator(chart } override fun getDefaultRange(): Long { - return 60L + return 120L } override fun getMinTime(): Long { @@ -51,17 +49,11 @@ class ChartBitmapCreator(chart: LineChart, context: Context): ChartCreator(chart private fun createBitmap() { Log.d(LOG_ID, "createBitmap") // reset bitmap and create it - chart.fitScreen() + /*chart.fitScreen() chart.data?.clearValues() chart.data?.notifyDataChanged() - chart.notifyDataSetChanged() - if(ChartData.hasData(getMinTime())) { - initData() - } else { - Log.d(LOG_ID, "no data available for bitmap - reset chart") - create() - updateNotifier() - } + chart.notifyDataSetChanged()*/ + resetData() } override fun update() { diff --git a/common/src/main/java/de/michelinside/glucodatahandler/common/chart/ChartCreator.kt b/common/src/main/java/de/michelinside/glucodatahandler/common/chart/ChartCreator.kt index 88545ac17..463f66b58 100644 --- a/common/src/main/java/de/michelinside/glucodatahandler/common/chart/ChartCreator.kt +++ b/common/src/main/java/de/michelinside/glucodatahandler/common/chart/ChartCreator.kt @@ -7,7 +7,6 @@ import android.graphics.Color import android.os.Bundle import android.util.Log import androidx.core.view.drawToBitmap -import com.github.mikephil.charting.charts.LineChart import com.github.mikephil.charting.components.LimitLine import com.github.mikephil.charting.components.XAxis import com.github.mikephil.charting.components.YAxis @@ -28,9 +27,9 @@ import de.michelinside.glucodatahandler.common.utils.Utils import java.util.concurrent.TimeUnit -open class ChartCreator(protected val chart: LineChart, protected val context: Context): NotifierInterface, +open class ChartCreator(protected val chart: GlucoseChart, protected val context: Context): NotifierInterface, SharedPreferences.OnSharedPreferenceChangeListener { - private val LOG_ID = "GDH.Chart.Creator" + private var LOG_ID = "GDH.Chart.Creator" protected var created = false protected val sharedPref = context.getSharedPreferences(Constants.SHARED_PREF_TAG, Context.MODE_PRIVATE) protected var init = false @@ -51,6 +50,7 @@ open class ChartCreator(protected val chart: LineChart, protected val context: C private fun init() { if(!init) { + LOG_ID = "GDH.Chart.Creator." + chart.id.toString() Log.d(LOG_ID, "init") ChartUtils.init(context) sharedPref.registerOnSharedPreferenceChangeListener(this) @@ -62,10 +62,10 @@ open class ChartCreator(protected val chart: LineChart, protected val context: C protected fun updateNotifier() { Log.d(LOG_ID, "updateNotifier - has data: ${ChartData.hasData(getMinTime())} - xAxisEnabled: ${chart.xAxis.isEnabled}") if(ChartData.hasData(getMinTime()) || chart.xAxis.isEnabled) { - InternalNotifier.addNotifier(context, this, mutableSetOf(NotifySource.MESSAGECLIENT, NotifySource.BROADCAST, NotifySource.TIME_VALUE)) + InternalNotifier.addNotifier(context, this, mutableSetOf(NotifySource.MESSAGECLIENT, NotifySource.BROADCAST, NotifySource.GRAPH_DATA_CHANGED, NotifySource.TIME_VALUE)) hasTimeNotifier = true } else { - InternalNotifier.addNotifier(context, this, mutableSetOf(NotifySource.MESSAGECLIENT, NotifySource.BROADCAST)) + InternalNotifier.addNotifier(context, this, mutableSetOf(NotifySource.MESSAGECLIENT, NotifySource.BROADCAST, NotifySource.GRAPH_DATA_CHANGED)) hasTimeNotifier = false } } @@ -111,6 +111,7 @@ open class ChartCreator(protected val chart: LineChart, protected val context: C //chart.highlightValue(null) chart.fitScreen() chart.data?.clearValues() + chart.data?.notifyDataChanged() chart.axisRight.removeAllLimitLines() chart.xAxis.valueFormatter = null chart.axisRight.valueFormatter = null @@ -119,6 +120,7 @@ open class ChartCreator(protected val chart: LineChart, protected val context: C chart.notifyDataSetChanged() chart.clear() chart.invalidate() + chart.waitForInvalidate() } protected open fun initXaxis() { @@ -155,7 +157,7 @@ open class ChartCreator(protected val chart: LineChart, protected val context: C } protected fun initDataSet() { - if(chart.data == null || chart.data.entryCount == 0) { + if(chart.data == null || chart.data.dataSetCount == 0) { Log.v(LOG_ID, "initDataSet") val dataSet = LineDataSet(ArrayList(), "Glucose Values") //dataSet.valueFormatter = GlucoseFormatter() @@ -168,6 +170,8 @@ open class ChartCreator(protected val chart: LineChart, protected val context: C dataSet.axisDependency = YAxis.AxisDependency.RIGHT dataSet.enableDashedLine(0F, 1F, 0F) chart.data = LineData(dataSet) + chart.notifyDataSetChanged() + Log.v(LOG_ID, "Min: ${chart.xAxis.axisMinimum} - visible: ${chart.lowestVisibleX} - Max: ${chart.xAxis.axisMaximum} - visible: ${chart.highestVisibleX}") } } @@ -213,6 +217,7 @@ open class ChartCreator(protected val chart: LineChart, protected val context: C chart.axisLeft.axisMaximum = chart.axisRight.axisMaximum chart.isScaleXEnabled = false chart.invalidate() + chart.waitForInvalidate() } protected fun initData() { @@ -233,6 +238,10 @@ open class ChartCreator(protected val chart: LineChart, protected val context: C val dataSet = chart.data.getDataSetByIndex(0) as LineDataSet var added = false if(values.isNotEmpty()) { + if(dataSet.values.isEmpty()) { + Log.v(LOG_ID, "Reset colors") + dataSet.resetCircleColors() + } for (i in 0 until values.size) { val entry = values[i] if (dataSet.values.isEmpty() || dataSet.values.last().x < entry.x) { @@ -241,7 +250,6 @@ open class ChartCreator(protected val chart: LineChart, protected val context: C val color = ReceiveData.getValueColor(entry.y.toInt()) //dataSet.addColor(color) dataSet.circleColors.add(color) - dataSet.notifyDataSetChanged() if (chart.axisRight.axisMinimum > (entry.y - 10F)) { chart.axisRight.axisMinimum = entry.y - 10F @@ -256,6 +264,7 @@ open class ChartCreator(protected val chart: LineChart, protected val context: C } if(added) { + dataSet.notifyDataSetChanged() updateChart(dataSet) } else { addEmptyTimeData() @@ -379,6 +388,19 @@ open class ChartCreator(protected val chart: LineChart, protected val context: C updateChart(chart.data.getDataSetByIndex(0) as LineDataSet) } + fun resetData() { + if(chart.data != null) { + Log.w(LOG_ID, "Reset data") + val dataSet = chart.data.getDataSetByIndex(0) as LineDataSet + dataSet.clear() + dataSet.setColors(0) + dataSet.setCircleColor(0) + chart.data.notifyDataChanged() + chart.notifyDataSetChanged() + } + initData() + } + override fun OnNotifyData(context: Context, dataSource: NotifySource, extras: Bundle?) { try { Log.d(LOG_ID, "OnNotifyData: $dataSource") @@ -386,6 +408,9 @@ open class ChartCreator(protected val chart: LineChart, protected val context: C Log.d(LOG_ID, "time elapsed: ${ReceiveData.getElapsedTimeMinute()}") if(ReceiveData.getElapsedTimeMinute().mod(2) == 0) updateTimeElapsed() + } else if(dataSource == NotifySource.GRAPH_DATA_CHANGED) { + Log.d(LOG_ID, "graph data changed") + resetData() // recreate chart with new graph data } else { update() } @@ -405,7 +430,7 @@ open class ChartCreator(protected val chart: LineChart, protected val context: C Thread.sleep(1000) } Log.w(LOG_ID, "Demo finished") - create() + resetData() } catch (exc: Exception) { Log.e(LOG_ID, "demo exception: " + exc.message.toString() + " - " + exc.stackTraceToString() ) } @@ -432,7 +457,8 @@ open class ChartCreator(protected val chart: LineChart, protected val context: C if (graphPrefList.contains(key)) { Log.i(LOG_ID, "re create graph after settings changed for key: $key") ReceiveData.updateSettings(sharedPref) - create() + if(chart.data != null && chart.data.getDataSetByIndex(0).entryCount > 0) + create() // recreate chart with new graph data } } catch (exc: Exception) { Log.e(LOG_ID, "onSharedPreferenceChanged exception: " + exc.message.toString() ) diff --git a/common/src/main/java/de/michelinside/glucodatahandler/common/chart/GlucoseChart.kt b/common/src/main/java/de/michelinside/glucodatahandler/common/chart/GlucoseChart.kt index cde90b2b7..9344ce417 100644 --- a/common/src/main/java/de/michelinside/glucodatahandler/common/chart/GlucoseChart.kt +++ b/common/src/main/java/de/michelinside/glucodatahandler/common/chart/GlucoseChart.kt @@ -1,6 +1,7 @@ package de.michelinside.glucodatahandler.common.chart import android.content.Context +import android.graphics.Canvas import android.os.Bundle import android.util.AttributeSet import android.util.Log @@ -23,18 +24,46 @@ class GlucoseChart: LineChart { defStyle ) + private var processing = false + override fun invalidate() { - if(id == View.NO_ID) { - id = generateViewId() + processing = true + try { + if(id == View.NO_ID) { + id = generateViewId() + } + //Log.v(LOG_ID, "start invalidate - notify ID: $id") + super.invalidate() + //Log.i(LOG_ID, "invalidate finished - notify ID: $id") + InternalNotifier.notify(context, NotifySource.GRAPH_CHANGED, Bundle().apply { putInt(Constants.GRAPH_ID, id) }) + } catch (exc: Exception) { + Log.e(LOG_ID, "invalidate exception: ${exc.message}\n${exc.stackTraceToString()}") + } + processing = false + } + + + fun waitForInvalidate() { + while(processing) { + Thread.sleep(10) } - Log.v(LOG_ID, "start invalidate - notify ID: $id") - super.invalidate() - Log.i(LOG_ID, "invalidate finished - notify ID: $id") - InternalNotifier.notify(context, NotifySource.GRAPH_CHANGED, Bundle().apply { putInt(Constants.GRAPH_ID, id) }) } override fun onDetachedFromWindow() { - super.onDetachedFromWindow() + try { + super.onDetachedFromWindow() + } catch (exc: Exception) { + Log.e(LOG_ID, "onDetachedFromWindow exception: ${exc.message}\n${exc.stackTraceToString()}") + } Log.d(LOG_ID, "onDetachedFromWindow for ID: $id") } + + override fun onDraw(canvas: Canvas) { + try { + super.onDraw(canvas) + } catch (exc: Exception) { + Log.e(LOG_ID, "onDraw exception: ${exc.message}\n${exc.stackTraceToString()}") + } + } + } \ No newline at end of file diff --git a/mobile/src/main/java/de/michelinside/glucodatahandler/MainActivity.kt b/mobile/src/main/java/de/michelinside/glucodatahandler/MainActivity.kt index cc6e6bb2e..a2b1e01ee 100644 --- a/mobile/src/main/java/de/michelinside/glucodatahandler/MainActivity.kt +++ b/mobile/src/main/java/de/michelinside/glucodatahandler/MainActivity.kt @@ -29,7 +29,6 @@ import androidx.core.content.ContextCompat import androidx.core.view.MenuCompat import androidx.core.view.setPadding import androidx.preference.PreferenceManager -import com.github.mikephil.charting.charts.LineChart import de.michelinside.glucodatahandler.android_auto.CarModeReceiver import de.michelinside.glucodatahandler.common.Constants import de.michelinside.glucodatahandler.common.GlucoDataService @@ -39,6 +38,7 @@ import de.michelinside.glucodatahandler.common.SourceStateData import de.michelinside.glucodatahandler.common.WearPhoneConnection import de.michelinside.glucodatahandler.common.chart.ChartBitmap import de.michelinside.glucodatahandler.common.chart.ChartCreator +import de.michelinside.glucodatahandler.common.chart.GlucoseChart import de.michelinside.glucodatahandler.common.notification.AlarmHandler import de.michelinside.glucodatahandler.common.notification.AlarmState import de.michelinside.glucodatahandler.common.notification.AlarmType @@ -78,7 +78,7 @@ class MainActivity : AppCompatActivity(), NotifierInterface { private lateinit var btnSources: Button private lateinit var sharedPref: SharedPreferences private lateinit var optionsMenu: Menu - private lateinit var chart: LineChart + private lateinit var chart: GlucoseChart private lateinit var chartImage: ImageView private var alarmIcon: MenuItem? = null private var snoozeMenu: MenuItem? = null diff --git a/mobile/src/main/res/layout/activity_main.xml b/mobile/src/main/res/layout/activity_main.xml index 802b38b3d..63844bb02 100644 --- a/mobile/src/main/res/layout/activity_main.xml +++ b/mobile/src/main/res/layout/activity_main.xml @@ -134,7 +134,7 @@ /> - Date: Thu, 23 Jan 2025 15:31:08 +0100 Subject: [PATCH 07/17] Update gradle --- build.gradle | 6 +++--- gradle/wrapper/gradle-wrapper.properties | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/build.gradle b/build.gradle index 170ddaf3f..10111ed4e 100644 --- a/build.gradle +++ b/build.gradle @@ -1,14 +1,14 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. plugins { - id 'com.android.application' version '8.6.1' apply false - id 'com.android.library' version '8.6.1' apply false + id 'com.android.application' version '8.8.0' apply false + id 'com.android.library' version '8.8.0' apply false id 'org.jetbrains.kotlin.android' version '1.9.23' apply false } Properties properties = new Properties() properties.load(project.rootProject.file('local.properties').newDataInputStream()) -project.ext.set("versionCode", 98) +project.ext.set("versionCode", 99) project.ext.set("versionName", "1.3.1") project.ext.set("compileSdk", 34) project.ext.set("targetSdk", 34) diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index dddffa93c..1869f4165 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Wed Dec 14 15:11:00 CET 2022 distributionBase=GRADLE_USER_HOME -distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip distributionPath=wrapper/dists zipStorePath=wrapper/dists zipStoreBase=GRADLE_USER_HOME From 264f3099dd264f57e7dab1793bb433ff36fa472c Mon Sep 17 00:00:00 2001 From: pachi81 Date: Thu, 23 Jan 2025 18:55:32 +0100 Subject: [PATCH 08/17] Chart creation asynch --- .../common/chart/ChartCreator.kt | 86 +++++++++++++------ 1 file changed, 59 insertions(+), 27 deletions(-) diff --git a/common/src/main/java/de/michelinside/glucodatahandler/common/chart/ChartCreator.kt b/common/src/main/java/de/michelinside/glucodatahandler/common/chart/ChartCreator.kt index 463f66b58..61cf69069 100644 --- a/common/src/main/java/de/michelinside/glucodatahandler/common/chart/ChartCreator.kt +++ b/common/src/main/java/de/michelinside/glucodatahandler/common/chart/ChartCreator.kt @@ -24,6 +24,12 @@ import de.michelinside.glucodatahandler.common.notifier.NotifierInterface import de.michelinside.glucodatahandler.common.notifier.NotifySource import de.michelinside.glucodatahandler.common.utils.DummyGraphData import de.michelinside.glucodatahandler.common.utils.Utils +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking import java.util.concurrent.TimeUnit @@ -34,6 +40,8 @@ open class ChartCreator(protected val chart: GlucoseChart, protected val context protected val sharedPref = context.getSharedPreferences(Constants.SHARED_PREF_TAG, Context.MODE_PRIVATE) protected var init = false protected var hasTimeNotifier = false + private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default) + private var currentJob: Job? = null private val demoMode = false @@ -71,20 +79,28 @@ open class ChartCreator(protected val chart: GlucoseChart, protected val context } fun create() { - Log.d(LOG_ID, "create") - try { - init() - resetChart() - initXaxis() - initYaxis() - initChart() - if(!demoMode) - initData() - created = true - if(demoMode) - demo() - } catch (exc: Exception) { - Log.e(LOG_ID, "create exception: " + exc.message.toString() ) + if(currentJob?.isActive == true) { + runBlocking { + Log.d(LOG_ID, "create - wait for current execution") + currentJob!!.join() + } + } + currentJob = scope.launch { + Log.d(LOG_ID, "create") + try { + init() + resetChart() + initXaxis() + initYaxis() + initChart() + if(!demoMode) + initData() + created = true + //if(demoMode) + // demo() + } catch (exc: Exception) { + Log.e(LOG_ID, "create exception: " + exc.message.toString() ) + } } } @@ -94,6 +110,12 @@ open class ChartCreator(protected val chart: GlucoseChart, protected val context InternalNotifier.remNotifier(context, this) sharedPref.unregisterOnSharedPreferenceChangeListener(this) init = false + if(currentJob?.isActive == true) { + runBlocking { + Log.d(LOG_ID, "close - wait for current execution") + currentJob!!.join() + } + } } } @@ -391,6 +413,7 @@ open class ChartCreator(protected val chart: GlucoseChart, protected val context fun resetData() { if(chart.data != null) { Log.w(LOG_ID, "Reset data") + chart.highlightValue(null) val dataSet = chart.data.getDataSetByIndex(0) as LineDataSet dataSet.clear() dataSet.setColors(0) @@ -402,21 +425,30 @@ open class ChartCreator(protected val chart: GlucoseChart, protected val context } override fun OnNotifyData(context: Context, dataSource: NotifySource, extras: Bundle?) { - try { - Log.d(LOG_ID, "OnNotifyData: $dataSource") - if(dataSource == NotifySource.TIME_VALUE) { - Log.d(LOG_ID, "time elapsed: ${ReceiveData.getElapsedTimeMinute()}") - if(ReceiveData.getElapsedTimeMinute().mod(2) == 0) - updateTimeElapsed() - } else if(dataSource == NotifySource.GRAPH_DATA_CHANGED) { - Log.d(LOG_ID, "graph data changed") - resetData() // recreate chart with new graph data - } else { - update() + if(currentJob?.isActive == true) { + runBlocking { + Log.d(LOG_ID, "OnNotifyData - wait for current execution") + currentJob!!.join() + } + } + currentJob = scope.launch { + try { + Log.d(LOG_ID, "OnNotifyData: $dataSource") + if(dataSource == NotifySource.TIME_VALUE) { + Log.d(LOG_ID, "time elapsed: ${ReceiveData.getElapsedTimeMinute()}") + if(ReceiveData.getElapsedTimeMinute().mod(2) == 0) + updateTimeElapsed() + } else if(dataSource == NotifySource.GRAPH_DATA_CHANGED) { + Log.d(LOG_ID, "graph data changed") + resetData() // recreate chart with new graph data + } else { + update() + } + } catch (exc: Exception) { + Log.e(LOG_ID, "OnNotifyData exception: " + exc.message.toString() + " - " + exc.stackTraceToString() ) } - } catch (exc: Exception) { - Log.e(LOG_ID, "OnNotifyData exception: " + exc.message.toString() + " - " + exc.stackTraceToString() ) } + //currentJob!!.start() } private fun demo() { From 5b238b6c77aba70113c6fb3480d77a50429e5195 Mon Sep 17 00:00:00 2001 From: pachi81 Date: Fri, 24 Jan 2025 10:09:22 +0100 Subject: [PATCH 09/17] Using database --- .gitignore | 1 + build.gradle | 3 +- common/build.gradle | 7 + .../glucodatahandler/common/ReceiveData.kt | 5 +- .../common/WearPhoneConnection.kt | 4 +- .../common/chart/ChartBitmap.kt | 1 + .../common/chart/ChartBitmapCreator.kt | 24 +-- .../common/chart/ChartCreator.kt | 152 +++++++++++------- .../common/chart/ChartData.kt | 42 ----- .../common/database/Database.kt | 8 + .../common/database/GlucoseValue.kt | 9 ++ .../common/database/GlucoseValueDao.kt | 46 ++++++ .../common/database/dbAccess.kt | 107 ++++++++++++ .../common/tasks/LibreLinkSourceTask.kt | 64 ++++---- 14 files changed, 312 insertions(+), 161 deletions(-) delete mode 100644 common/src/main/java/de/michelinside/glucodatahandler/common/chart/ChartData.kt create mode 100644 common/src/main/java/de/michelinside/glucodatahandler/common/database/Database.kt create mode 100644 common/src/main/java/de/michelinside/glucodatahandler/common/database/GlucoseValue.kt create mode 100644 common/src/main/java/de/michelinside/glucodatahandler/common/database/GlucoseValueDao.kt create mode 100644 common/src/main/java/de/michelinside/glucodatahandler/common/database/dbAccess.kt diff --git a/.gitignore b/.gitignore index dddde1381..7ab6a3c49 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ release/ dev_release/ debug/ second/ +/.kotlin/ diff --git a/build.gradle b/build.gradle index 10111ed4e..f78539417 100644 --- a/build.gradle +++ b/build.gradle @@ -2,7 +2,8 @@ plugins { id 'com.android.application' version '8.8.0' apply false id 'com.android.library' version '8.8.0' apply false - id 'org.jetbrains.kotlin.android' version '1.9.23' apply false + id 'org.jetbrains.kotlin.android' version '2.1.0' apply false + id("com.google.devtools.ksp") version "2.1.0-1.0.29" apply false } Properties properties = new Properties() diff --git a/common/build.gradle b/common/build.gradle index d36868f92..562592670 100644 --- a/common/build.gradle +++ b/common/build.gradle @@ -1,6 +1,7 @@ plugins { id 'com.android.library' id 'org.jetbrains.kotlin.android' + id 'com.google.devtools.ksp' } android { @@ -55,4 +56,10 @@ dependencies { androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1' testImplementation 'org.mockito:mockito-core:4.11.0' testImplementation 'org.mockito.kotlin:mockito-kotlin:4.0.0' + + //Room + def roomVersion = "2.6.1" + implementation("androidx.room:room-runtime:$roomVersion") + implementation("androidx.room:room-ktx:$roomVersion") + ksp("androidx.room:room-compiler:$roomVersion") } \ No newline at end of file diff --git a/common/src/main/java/de/michelinside/glucodatahandler/common/ReceiveData.kt b/common/src/main/java/de/michelinside/glucodatahandler/common/ReceiveData.kt index 3ad90d1ab..5aa22119a 100644 --- a/common/src/main/java/de/michelinside/glucodatahandler/common/ReceiveData.kt +++ b/common/src/main/java/de/michelinside/glucodatahandler/common/ReceiveData.kt @@ -5,7 +5,7 @@ import android.content.SharedPreferences import android.graphics.Color import android.os.Bundle import android.util.Log -import de.michelinside.glucodatahandler.common.chart.ChartData +import de.michelinside.glucodatahandler.common.database.dbAccess import de.michelinside.glucodatahandler.common.notification.AlarmHandler import de.michelinside.glucodatahandler.common.notification.AlarmNotificationBase import de.michelinside.glucodatahandler.common.notification.AlarmType @@ -144,6 +144,7 @@ object ReceiveData: SharedPreferences.OnSharedPreferenceChangeListener { initialized = true unitMgDl = context.resources.getString(R.string.unit_mgdl) unitMmol = context.resources.getString(R.string.unit_mmol) + dbAccess.init(context) AlarmHandler.initData(context) readTargets(context) loadExtras(context) @@ -559,7 +560,7 @@ object ReceiveData: SharedPreferences.OnSharedPreferenceChangeListener { } } - ChartData.addData(time, rawValue) + dbAccess.addGlucoseValue(time, rawValue) val notifySource = if(interApp) NotifySource.MESSAGECLIENT else NotifySource.BROADCAST diff --git a/common/src/main/java/de/michelinside/glucodatahandler/common/WearPhoneConnection.kt b/common/src/main/java/de/michelinside/glucodatahandler/common/WearPhoneConnection.kt index d8b06099c..abcacb8f2 100644 --- a/common/src/main/java/de/michelinside/glucodatahandler/common/WearPhoneConnection.kt +++ b/common/src/main/java/de/michelinside/glucodatahandler/common/WearPhoneConnection.kt @@ -79,7 +79,7 @@ class WearPhoneConnection : MessageClient.OnMessageReceivedListener, CapabilityC } else if (addMissing) { batterLevels.add(-1) - }) + } else {}) } return batterLevels } @@ -102,7 +102,7 @@ class WearPhoneConnection : MessageClient.OnMessageReceivedListener, CapabilityC } else if (addMissing) { connectionStates[getDisplayName(node.value)] = context.getString(R.string.state_await_data) - }) + } else {}) } return connectionStates diff --git a/common/src/main/java/de/michelinside/glucodatahandler/common/chart/ChartBitmap.kt b/common/src/main/java/de/michelinside/glucodatahandler/common/chart/ChartBitmap.kt index 0a47e4f07..0048b6dfd 100644 --- a/common/src/main/java/de/michelinside/glucodatahandler/common/chart/ChartBitmap.kt +++ b/common/src/main/java/de/michelinside/glucodatahandler/common/chart/ChartBitmap.kt @@ -30,6 +30,7 @@ class ChartBitmap(val imageView: ImageView, val context: Context): NotifierInter fun close() { Log.v(LOG_ID, "close") InternalNotifier.remNotifier(context, this) + chartViewer.close() } override fun OnNotifyData(context: Context, dataSource: NotifySource, extras: Bundle?) { diff --git a/common/src/main/java/de/michelinside/glucodatahandler/common/chart/ChartBitmapCreator.kt b/common/src/main/java/de/michelinside/glucodatahandler/common/chart/ChartBitmapCreator.kt index b25bf8b6a..a371e117d 100644 --- a/common/src/main/java/de/michelinside/glucodatahandler/common/chart/ChartBitmapCreator.kt +++ b/common/src/main/java/de/michelinside/glucodatahandler/common/chart/ChartBitmapCreator.kt @@ -4,6 +4,7 @@ import android.content.Context import android.util.Log import com.github.mikephil.charting.data.LineData import com.github.mikephil.charting.data.LineDataSet +import de.michelinside.glucodatahandler.common.database.dbAccess class ChartBitmapCreator(chart: GlucoseChart, context: Context): ChartCreator(chart, context) { private val LOG_ID = "GDH.Chart.BitmapCreator" @@ -31,8 +32,8 @@ class ChartBitmapCreator(chart: GlucoseChart, context: Context): ChartCreator(ch return 120L } - override fun getMinTime(): Long { - return System.currentTimeMillis() - (getDefaultRange()*60*1000L) + override fun getMaxRange(): Long { + return getDefaultRange() } override fun updateChart(dataSet: LineDataSet) { @@ -46,24 +47,7 @@ class ChartBitmapCreator(chart: GlucoseChart, context: Context): ChartCreator(ch chart.invalidate() } - private fun createBitmap() { - Log.d(LOG_ID, "createBitmap") - // reset bitmap and create it - /*chart.fitScreen() - chart.data?.clearValues() - chart.data?.notifyDataChanged() - chart.notifyDataSetChanged()*/ - resetData() - } - - override fun update() { - // update limit lines - createBitmap() - } - override fun updateTimeElapsed() { - update() + update(dbAccess.getGlucoseValues(getMinTime())) } - - } \ No newline at end of file diff --git a/common/src/main/java/de/michelinside/glucodatahandler/common/chart/ChartCreator.kt b/common/src/main/java/de/michelinside/glucodatahandler/common/chart/ChartCreator.kt index 61cf69069..2648bf6b3 100644 --- a/common/src/main/java/de/michelinside/glucodatahandler/common/chart/ChartCreator.kt +++ b/common/src/main/java/de/michelinside/glucodatahandler/common/chart/ChartCreator.kt @@ -19,15 +19,17 @@ import com.github.mikephil.charting.utils.Utils as ChartUtils import de.michelinside.glucodatahandler.common.Constants import de.michelinside.glucodatahandler.common.R import de.michelinside.glucodatahandler.common.ReceiveData +import de.michelinside.glucodatahandler.common.database.GlucoseValue +import de.michelinside.glucodatahandler.common.database.dbAccess import de.michelinside.glucodatahandler.common.notifier.InternalNotifier import de.michelinside.glucodatahandler.common.notifier.NotifierInterface import de.michelinside.glucodatahandler.common.notifier.NotifySource -import de.michelinside.glucodatahandler.common.utils.DummyGraphData import de.michelinside.glucodatahandler.common.utils.Utils import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancelAndJoin import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import java.util.concurrent.TimeUnit @@ -42,8 +44,7 @@ open class ChartCreator(protected val chart: GlucoseChart, protected val context protected var hasTimeNotifier = false private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default) private var currentJob: Job? = null - - private val demoMode = false + private var dataSyncJob: Job? = null private var graphPrefList = mutableSetOf( Constants.SHARED_PREF_LOW_GLUCOSE, @@ -62,18 +63,19 @@ open class ChartCreator(protected val chart: GlucoseChart, protected val context Log.d(LOG_ID, "init") ChartUtils.init(context) sharedPref.registerOnSharedPreferenceChangeListener(this) - if(!demoMode) - updateNotifier() + updateNotifier() + init = true } } protected fun updateNotifier() { - Log.d(LOG_ID, "updateNotifier - has data: ${ChartData.hasData(getMinTime())} - xAxisEnabled: ${chart.xAxis.isEnabled}") - if(ChartData.hasData(getMinTime()) || chart.xAxis.isEnabled) { - InternalNotifier.addNotifier(context, this, mutableSetOf(NotifySource.MESSAGECLIENT, NotifySource.BROADCAST, NotifySource.GRAPH_DATA_CHANGED, NotifySource.TIME_VALUE)) + val hasData = dbAccess.hasGlucoseValues(getMinTime()) + Log.d(LOG_ID, "updateNotifier - has data: $hasData - xAxisEnabled: ${chart.xAxis.isEnabled}") + if(hasData || chart.xAxis.isEnabled) { + InternalNotifier.addNotifier(context, this, mutableSetOf(NotifySource.TIME_VALUE)) hasTimeNotifier = true } else { - InternalNotifier.addNotifier(context, this, mutableSetOf(NotifySource.MESSAGECLIENT, NotifySource.BROADCAST, NotifySource.GRAPH_DATA_CHANGED)) + InternalNotifier.remNotifier(context, this) hasTimeNotifier = false } } @@ -93,11 +95,8 @@ open class ChartCreator(protected val chart: GlucoseChart, protected val context initXaxis() initYaxis() initChart() - if(!demoMode) - initData() + initData() created = true - //if(demoMode) - // demo() } catch (exc: Exception) { Log.e(LOG_ID, "create exception: " + exc.message.toString() ) } @@ -105,19 +104,28 @@ open class ChartCreator(protected val chart: GlucoseChart, protected val context } fun close() { - Log.d(LOG_ID, "close") - if(init) { - InternalNotifier.remNotifier(context, this) - sharedPref.unregisterOnSharedPreferenceChangeListener(this) - init = false - if(currentJob?.isActive == true) { - runBlocking { - Log.d(LOG_ID, "close - wait for current execution") - currentJob!!.join() + Log.d(LOG_ID, "close init: $init") + try { + if(init) { + InternalNotifier.remNotifier(context, this) + sharedPref.unregisterOnSharedPreferenceChangeListener(this) + if(dataSyncJob != null) { + runBlocking { + Log.d(LOG_ID, "stop data sync - wait for current execution") + dataSyncJob!!.cancelAndJoin() + } + } + if(currentJob?.isActive == true) { + runBlocking { + Log.d(LOG_ID, "close current job - wait for current execution") + currentJob!!.join() + } } + init = false } + } catch (exc: Exception) { + Log.e(LOG_ID, "close exception: " + exc.message.toString() ) } - } fun isRight(): Boolean { @@ -242,12 +250,32 @@ open class ChartCreator(protected val chart: GlucoseChart, protected val context chart.waitForInvalidate() } - protected fun initData() { + private fun initData() { initDataSet() - addEntries(ChartData.getData(getMinTime())) + dataSyncJob = scope.launch { + dataSync() + } } - protected open fun getMinTime(): Long { + private suspend fun dataSync() { + Log.d(LOG_ID, "dataSync running") + try { + dbAccess.getLiveValuesByTimeSpan(getMaxRange().toInt()/60).collect{ values -> + update(values) + } + Log.d(LOG_ID, "dataSync done") + } catch (exc: Exception) { + Log.e(LOG_ID, "dataSync exception: " + exc.message.toString() ) + } + } + + protected open fun getMaxRange(): Long { + return 0L + } + + protected fun getMinTime(): Long { + if(getMaxRange() > 0L) + return System.currentTimeMillis() - (getMaxRange()*60*1000L) return 0L } @@ -255,7 +283,7 @@ open class ChartCreator(protected val chart: GlucoseChart, protected val context return 240L } - protected open fun addEntries(values: ArrayList) { + protected open fun addEntries(values: List) { Log.d(LOG_ID, "Add ${values.size} entries") val dataSet = chart.data.getDataSetByIndex(0) as LineDataSet var added = false @@ -264,8 +292,8 @@ open class ChartCreator(protected val chart: GlucoseChart, protected val context Log.v(LOG_ID, "Reset colors") dataSet.resetCircleColors() } - for (i in 0 until values.size) { - val entry = values[i] + for (element in values) { + val entry = Entry(TimeValueFormatter.to_chart_x(element.timestamp), element.value.toFloat()) if (dataSet.values.isEmpty() || dataSet.values.last().x < entry.x) { added = true dataSet.addEntry(entry) @@ -331,7 +359,7 @@ open class ChartCreator(protected val chart: GlucoseChart, protected val context } } if(minValue > 0 || maxValue > 0) { - Log.w(LOG_ID, "Creating extra data set from ${Utils.getUiTimeStamp(minValue)} - ${Utils.getUiTimeStamp(maxValue)}") + Log.d(LOG_ID, "Creating extra data set from ${Utils.getUiTimeStamp(minValue)} - ${Utils.getUiTimeStamp(maxValue)}") val entries = ArrayList() if(minValue > 0) entries.add(Entry(TimeValueFormatter.to_chart_x(minValue), 120F)) @@ -397,11 +425,35 @@ open class ChartCreator(protected val chart: GlucoseChart, protected val context } } - protected open fun update() { - // update limit lines - if(ReceiveData.time > 0) { - val entry = Entry(TimeValueFormatter.to_chart_x(ReceiveData.time), ReceiveData.rawValue.toFloat()) - addEntries(arrayListOf(entry)) + private fun getFirstTimestamp(): Long { + if(chart.data != null && chart.data.dataSetCount > 0) { + val dataSet = chart.data.getDataSetByIndex(0) as LineDataSet + if (dataSet.values.isNotEmpty()) { + return TimeValueFormatter.from_chart_x(dataSet.values.first().x).time + } + } + return 0L + } + + private fun getLastTimestamp(): Long { + if(chart.data != null && chart.data.dataSetCount > 0) { + val dataSet = chart.data.getDataSetByIndex(0) as LineDataSet + if (dataSet.values.isNotEmpty()) { + return TimeValueFormatter.from_chart_x(dataSet.values.last().x).time + } + } + return 0L + } + + protected fun update(values: List) { + Log.d(LOG_ID, "update called for ${values.size} value") + if(values.isNotEmpty()) { + if(values.first().timestamp != getFirstTimestamp()) { + resetData(values) + return + } + val newValues = values.filter { data -> data.timestamp > getLastTimestamp() } + addEntries(newValues) } } @@ -410,9 +462,9 @@ open class ChartCreator(protected val chart: GlucoseChart, protected val context updateChart(chart.data.getDataSetByIndex(0) as LineDataSet) } - fun resetData() { + protected fun resetData(values: List) { + Log.d(LOG_ID, "Reset data") if(chart.data != null) { - Log.w(LOG_ID, "Reset data") chart.highlightValue(null) val dataSet = chart.data.getDataSetByIndex(0) as LineDataSet dataSet.clear() @@ -421,7 +473,8 @@ open class ChartCreator(protected val chart: GlucoseChart, protected val context chart.data.notifyDataChanged() chart.notifyDataSetChanged() } - initData() + initDataSet() + addEntries(values) } override fun OnNotifyData(context: Context, dataSource: NotifySource, extras: Bundle?) { @@ -438,11 +491,6 @@ open class ChartCreator(protected val chart: GlucoseChart, protected val context Log.d(LOG_ID, "time elapsed: ${ReceiveData.getElapsedTimeMinute()}") if(ReceiveData.getElapsedTimeMinute().mod(2) == 0) updateTimeElapsed() - } else if(dataSource == NotifySource.GRAPH_DATA_CHANGED) { - Log.d(LOG_ID, "graph data changed") - resetData() // recreate chart with new graph data - } else { - update() } } catch (exc: Exception) { Log.e(LOG_ID, "OnNotifyData exception: " + exc.message.toString() + " - " + exc.stackTraceToString() ) @@ -451,24 +499,6 @@ open class ChartCreator(protected val chart: GlucoseChart, protected val context //currentJob!!.start() } - private fun demo() { - Log.w(LOG_ID, "Start demo") - Thread { - try { - val demoData = DummyGraphData.create(5, min = 40, max = 260, stepMinute = 5) - Log.w(LOG_ID, "Running demo for ${demoData.size} entries") - demoData.forEach { (t, u) -> - addEntries(arrayListOf(Entry(TimeValueFormatter.to_chart_x(t), u.toFloat()))) - Thread.sleep(1000) - } - Log.w(LOG_ID, "Demo finished") - resetData() - } catch (exc: Exception) { - Log.e(LOG_ID, "demo exception: " + exc.message.toString() + " - " + exc.stackTraceToString() ) - } - }.start() - } - fun getBitmap(): Bitmap? { try { if(chart.width > 0 && chart.height > 0) diff --git a/common/src/main/java/de/michelinside/glucodatahandler/common/chart/ChartData.kt b/common/src/main/java/de/michelinside/glucodatahandler/common/chart/ChartData.kt deleted file mode 100644 index beedd6c49..000000000 --- a/common/src/main/java/de/michelinside/glucodatahandler/common/chart/ChartData.kt +++ /dev/null @@ -1,42 +0,0 @@ -package de.michelinside.glucodatahandler.common.chart - -import android.util.Log -import com.github.mikephil.charting.data.Entry -import de.michelinside.glucodatahandler.common.utils.DummyGraphData -import de.michelinside.glucodatahandler.common.utils.Utils - -object ChartData { - private val LOG_ID = "GDH.Chart.Data" - private var graphData = mutableMapOf() // DummyGraphData.create(0, min = 40, max = 260, stepMinute = 5).toMutableMap() - - fun getData(minTime: Long = 0L): ArrayList { - Log.d(LOG_ID, "getData - minTime: ${Utils.getUiTimeStamp(minTime)}") - val entries = ArrayList() - graphData.toSortedMap().forEach { (t, u) -> - if(t >= minTime) { - entries.add(Entry(TimeValueFormatter.to_chart_x(t), u.toFloat())) - } - } - if(entries.isNotEmpty()) - Log.v(LOG_ID, "getData - entries: ${entries.size} from ${Utils.getUiTimeStamp(TimeValueFormatter.from_chart_x(entries.first().x).time)} to ${Utils.getUiTimeStamp(TimeValueFormatter.from_chart_x(entries.last().x).time)}") - return entries - } - - fun hasData(minTime: Long = 0L): Boolean { - return graphData.keys.any { it >= minTime } - } - - fun addData(time: Long, value: Int) { - try { - Log.v(LOG_ID, "addData - time: ${Utils.getUiTimeStamp(time)} - value: $value") - graphData[time] = value - } catch (exc: Exception) { - Log.e(LOG_ID, "addData exception: " + exc.toString() ) - } - } - - fun needsData(): Boolean { - return graphData.size < 5 - } - -} \ No newline at end of file diff --git a/common/src/main/java/de/michelinside/glucodatahandler/common/database/Database.kt b/common/src/main/java/de/michelinside/glucodatahandler/common/database/Database.kt new file mode 100644 index 000000000..1424041ed --- /dev/null +++ b/common/src/main/java/de/michelinside/glucodatahandler/common/database/Database.kt @@ -0,0 +1,8 @@ +package de.michelinside.glucodatahandler.common.database +import androidx.room.Database +import androidx.room.RoomDatabase + +@Database(entities = [GlucoseValue::class], version = 1) +abstract class Database: RoomDatabase() { + abstract fun glucoseValuesDao(): GlucoseValueDao +} \ No newline at end of file diff --git a/common/src/main/java/de/michelinside/glucodatahandler/common/database/GlucoseValue.kt b/common/src/main/java/de/michelinside/glucodatahandler/common/database/GlucoseValue.kt new file mode 100644 index 000000000..9e7890d9a --- /dev/null +++ b/common/src/main/java/de/michelinside/glucodatahandler/common/database/GlucoseValue.kt @@ -0,0 +1,9 @@ +package de.michelinside.glucodatahandler.common.database +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity(tableName = "glucose_values") +class GlucoseValue ( + @PrimaryKey val timestamp: Long, + val value: Int +) \ No newline at end of file diff --git a/common/src/main/java/de/michelinside/glucodatahandler/common/database/GlucoseValueDao.kt b/common/src/main/java/de/michelinside/glucodatahandler/common/database/GlucoseValueDao.kt new file mode 100644 index 000000000..f64ce11df --- /dev/null +++ b/common/src/main/java/de/michelinside/glucodatahandler/common/database/GlucoseValueDao.kt @@ -0,0 +1,46 @@ +package de.michelinside.glucodatahandler.common.database + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import kotlinx.coroutines.flow.Flow + +@Dao +interface GlucoseValueDao { + @Insert(onConflict = OnConflictStrategy.IGNORE) + fun insertValue(values: GlucoseValue) + + @Insert(onConflict = OnConflictStrategy.IGNORE) + fun insertValues(events: List) + + @Query("SELECT * FROM glucose_values ORDER BY timestamp") + fun getValues(): List + + @Query("SELECT * FROM glucose_values WHERE timestamp >= :minTime ORDER BY timestamp") + fun getValuesByTime(minTime: Long): List + + @Query("SELECT * FROM glucose_values ORDER BY timestamp") + fun getLiveValues(): Flow> + + @Query("SELECT * FROM glucose_values WHERE timestamp >= strftime('%s', 'now', '-' || :hours || ' hours') * 1000") + fun getLiveValuesByTimeSpan(hours: Int): Flow> + + @Query("SELECT timestamp from glucose_values ORDER BY timestamp DESC LIMIT 1") + fun getLastTimestamp(): Long + + @Query("SELECT timestamp from glucose_values ORDER BY timestamp ASC LIMIT 1") + fun getFirstTimestamp(): Long + + @Query("SELECT COUNT(*) from glucose_values") + fun getCount(): Int + + @Query("SELECT COUNT(*) from glucose_values WHERE timestamp >= :minTime") + fun getCountByTime(minTime: Long): Int + + @Query("DELETE FROM glucose_values WHERE timestamp < :minTime") + fun deleteOldValues(minTime: Long) + + @Query("DELETE FROM glucose_values") + fun deleteAllValues() +} \ No newline at end of file diff --git a/common/src/main/java/de/michelinside/glucodatahandler/common/database/dbAccess.kt b/common/src/main/java/de/michelinside/glucodatahandler/common/database/dbAccess.kt new file mode 100644 index 000000000..4a8e3860f --- /dev/null +++ b/common/src/main/java/de/michelinside/glucodatahandler/common/database/dbAccess.kt @@ -0,0 +1,107 @@ +package de.michelinside.glucodatahandler.common.database + +import android.content.Context +import android.util.Log +import androidx.room.Room +import de.michelinside.glucodatahandler.common.GlucoDataService +import de.michelinside.glucodatahandler.common.notifier.InternalNotifier +import de.michelinside.glucodatahandler.common.notifier.NotifySource +import de.michelinside.glucodatahandler.common.utils.Utils +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.async +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking + +object dbAccess { + private val LOG_ID = "GDH.dbAccess" + private var database: Database? = null + private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO) + fun init(context: Context) { + Log.v(LOG_ID, "init") + try { + database = Room.databaseBuilder( + context.applicationContext, + Database::class.java, + "gdh_database" + ).build() + } catch (exc: Exception) { + Log.e(LOG_ID, "init exception: " + exc.toString() + ": " + exc.stackTraceToString() ) + } + } + + fun getGlucoseValues(minTime: Long = 0L): List = runBlocking { + scope.async { + if(database != null) { + Log.v(LOG_ID, "getGlucoseValues - minTime: ${Utils.getUiTimeStamp(minTime)}") + database!!.glucoseValuesDao().getValuesByTime(minTime) + } else { + Log.e(LOG_ID, "getGlucoseValues - database is null") + emptyList() + } + }.await() + } + + fun getLiveGlucoseValues(): Flow> { + return database!!.glucoseValuesDao().getLiveValues() + } + + fun getLiveValuesByTimeSpan(hours: Int): Flow> { + if(hours > 0) + return database!!.glucoseValuesDao().getLiveValuesByTimeSpan(hours) + return database!!.glucoseValuesDao().getLiveValues() + } + + fun hasGlucoseValues(minTime: Long = 0L): Boolean = runBlocking { + scope.async { + if(database != null) { + Log.v(LOG_ID, "hasGlucoseValues - minTime: ${Utils.getUiTimeStamp(minTime)}") + database!!.glucoseValuesDao().getCountByTime(minTime) > 0 + } else { + false + } + }.await() + } + + fun addGlucoseValue(time: Long, value: Int) { + if(database != null) { + scope.launch { + Log.v(LOG_ID, "Add new value $value at ${Utils.getUiTimeStamp(time)}") + database!!.glucoseValuesDao().insertValue(GlucoseValue(time, value)) + } + } + } + + fun addGlucoseValues(values: List) { + if(database != null && values.isNotEmpty()) { + scope.launch { + Log.v(LOG_ID, "Add ${values.size} values") + database!!.glucoseValuesDao().insertValues(values) + InternalNotifier.notify(GlucoDataService.context!!, NotifySource.GRAPH_DATA_CHANGED, null) + } + } + } + + fun getFirstLastTimestamp(): Pair = runBlocking { + scope.async { + if(database != null) { + val first = database!!.glucoseValuesDao().getFirstTimestamp() + val last = database!!.glucoseValuesDao().getLastTimestamp() + Pair(first, last) + } else { + Pair(0L, 0L) + } + }.await() + } + + fun deleteAllValues() = runBlocking { + scope.async { + scope.launch { + Log.v(LOG_ID, "deleteAllValues") + database!!.glucoseValuesDao().deleteAllValues() + } + } + } +} \ No newline at end of file diff --git a/common/src/main/java/de/michelinside/glucodatahandler/common/tasks/LibreLinkSourceTask.kt b/common/src/main/java/de/michelinside/glucodatahandler/common/tasks/LibreLinkSourceTask.kt index 75a04ee90..44af69441 100644 --- a/common/src/main/java/de/michelinside/glucodatahandler/common/tasks/LibreLinkSourceTask.kt +++ b/common/src/main/java/de/michelinside/glucodatahandler/common/tasks/LibreLinkSourceTask.kt @@ -9,8 +9,8 @@ import de.michelinside.glucodatahandler.common.Constants import de.michelinside.glucodatahandler.common.GlucoDataService import de.michelinside.glucodatahandler.common.R import de.michelinside.glucodatahandler.common.ReceiveData -import de.michelinside.glucodatahandler.common.SourceState -import de.michelinside.glucodatahandler.common.chart.ChartData +import de.michelinside.glucodatahandler.common.database.GlucoseValue +import de.michelinside.glucodatahandler.common.database.dbAccess import de.michelinside.glucodatahandler.common.notifier.DataSource import de.michelinside.glucodatahandler.common.notifier.InternalNotifier import de.michelinside.glucodatahandler.common.notifier.NotifySource @@ -370,10 +370,10 @@ class LibreLinkSourceTask : DataSourceTask(Constants.SHARED_PREF_LIBRE_ENABLED, } override fun getValue(): Boolean { - if(patientId.isNotEmpty() && patientData.isNotEmpty()) { + if((patientId.isNotEmpty() && patientData.isNotEmpty()) || handleConnectionResponse(httpGet(getUrl(CONNECTION_ENDPOINT), getHeader()))) { return handleGraphResponse(httpGet(getUrl(GRAPH_ENDPOINT.format(patientId)), getHeader())) } - return handleGlucoseResponse(httpGet(getUrl(CONNECTION_ENDPOINT), getHeader())) + return false } private fun getRateFromTrend(trend: Int): Float { @@ -406,13 +406,13 @@ class LibreLinkSourceTask : DataSourceTask(Constants.SHARED_PREF_LIBRE_ENABLED, if (jsonObject != null && jsonObject.has("data")) { val data = jsonObject.optJSONObject("data") if(data != null) { - if(ChartData.needsData() && data.has("graphData")) { + if(data.has("graphData")) { parseGraphData(data.optJSONArray("graphData")) } if(data.has("connection")) { val connection = data.optJSONObject("connection") if(connection!=null) - return parseConnectionData(connection) + return parseGlucoseData(connection) } } } @@ -428,26 +428,32 @@ class LibreLinkSourceTask : DataSourceTask(Constants.SHARED_PREF_LIBRE_ENABLED, try { if(graphData != null && graphData.length() > 0) { Log.d(LOG_ID, "Parse graph data for ${graphData.length()} entries") - for (i in 0 until graphData.length()) { - val glucoseData = graphData.optJSONObject(i) - if(glucoseData != null) { - val parsedUtc = parseUtcTimestamp(glucoseData.optString("FactoryTimestamp")) - val parsedLocal = parseLocalTimestamp(glucoseData.optString("Timestamp")) - val time = if (parsedUtc > 0) parsedUtc else parsedLocal - val value = glucoseData.optInt("ValueInMgPerDl") - if(value > 0 && time > 0) - ChartData.addData(time, value) + val firstLastPair = dbAccess.getFirstLastTimestamp() + if(Utils.getElapsedTimeMinute(firstLastPair.first) < (8*60) || Utils.getElapsedTimeMinute(firstLastPair.second) > (30)) { + val values = mutableListOf() + for (i in 0 until graphData.length()) { + val glucoseData = graphData.optJSONObject(i) + if(glucoseData != null) { + val parsedUtc = parseUtcTimestamp(glucoseData.optString("FactoryTimestamp")) + val parsedLocal = parseLocalTimestamp(glucoseData.optString("Timestamp")) + val time = if (parsedUtc > 0) parsedUtc else parsedLocal + if(time < firstLastPair.first || time > firstLastPair.second) { + val value = glucoseData.optInt("ValueInMgPerDl") + if(value > 0 && time > 0) + values.add(GlucoseValue(time, value)) + } + } } + dbAccess.addGlucoseValues(values) } - InternalNotifier.notify(GlucoDataService.context!!, NotifySource.GRAPH_DATA_CHANGED, null) } } catch (exc: Exception) { Log.e(LOG_ID, "parseGraphData exception: " + exc.toString() ) } } - private fun handleGlucoseResponse(body: String?): Boolean { - /* + private fun handleConnectionResponse(body: String?): Boolean { + /* used for getting patient id { "status": 0, "data": [ @@ -488,15 +494,7 @@ class LibreLinkSourceTask : DataSourceTask(Constants.SHARED_PREF_LIBRE_ENABLED, } return false } - val data = getPatientData(array) - if (data == null) { - setState(SourceState.NO_NEW_VALUE, GlucoDataService.context!!.resources.getString(R.string.no_data_in_server_response)) - return false - } - if(ChartData.needsData() && patientId.isNotEmpty() && patientData.isNotEmpty()) { - return handleGraphResponse(httpGet(getUrl(GRAPH_ENDPOINT.format(patientId)), getHeader())) - } - return parseConnectionData(data) + return getPatientData(array) } else { Log.e(LOG_ID, "No data array found in response: ${replaceSensitiveData(body!!)}") setLastError("Invalid response! Please send logs to developer.") @@ -507,7 +505,7 @@ class LibreLinkSourceTask : DataSourceTask(Constants.SHARED_PREF_LIBRE_ENABLED, return false } - private fun parseConnectionData(data: JSONObject): Boolean { + private fun parseGlucoseData(data: JSONObject): Boolean { if(data.has("glucoseMeasurement")) { val glucoseData = data.optJSONObject("glucoseMeasurement") if (glucoseData != null) { @@ -538,7 +536,7 @@ class LibreLinkSourceTask : DataSourceTask(Constants.SHARED_PREF_LIBRE_ENABLED, return false } - private fun getPatientData(dataArray: JSONArray): JSONObject? { + private fun getPatientData(dataArray: JSONArray): Boolean { if(dataArray.length() > patientData.size) { // create patientData map val checkPatienId = patientData.isEmpty() && patientId.isNotEmpty() @@ -569,16 +567,16 @@ class LibreLinkSourceTask : DataSourceTask(Constants.SHARED_PREF_LIBRE_ENABLED, for (i in 0 until dataArray.length()) { val data = dataArray.getJSONObject(i) if (data.has("patientId") && data.getString("patientId") == patientId) { - return data + return true } } - return null + return false } else if (patientData.isNotEmpty()) { patientId = patientData.keys.first() Log.d(LOG_ID, "Using patient ID $patientId") + return true } - // default: use first one - return dataArray.optJSONObject(0) + return false // no patient found } override fun interrupt() { From 6faf1018db28fc35cc37518f20a2d1b12640c0d6 Mon Sep 17 00:00:00 2001 From: pachi81 Date: Sun, 26 Jan 2025 21:16:54 +0100 Subject: [PATCH 10/17] Fixes for chart --- .../common/chart/ChartBitmapCreator.kt | 16 +++++ .../common/chart/ChartCreator.kt | 70 +++++++++++-------- .../common/chart/CustomBubbleMarker.kt | 2 +- .../common/chart/GlucoseChart.kt | 17 ++--- .../common/notifier/InternalNotifier.kt | 22 ++++-- common/src/main/res/layout/marker_layout.xml | 2 +- common/src/main/res/values-night/colors.xml | 1 + common/src/main/res/values/colors.xml | 1 + .../main/res/layout-land/activity_main.xml | 10 +++ 9 files changed, 95 insertions(+), 46 deletions(-) diff --git a/common/src/main/java/de/michelinside/glucodatahandler/common/chart/ChartBitmapCreator.kt b/common/src/main/java/de/michelinside/glucodatahandler/common/chart/ChartBitmapCreator.kt index a371e117d..8a9029915 100644 --- a/common/src/main/java/de/michelinside/glucodatahandler/common/chart/ChartBitmapCreator.kt +++ b/common/src/main/java/de/michelinside/glucodatahandler/common/chart/ChartBitmapCreator.kt @@ -1,13 +1,20 @@ package de.michelinside.glucodatahandler.common.chart import android.content.Context +import android.graphics.Bitmap +import android.os.Bundle import android.util.Log import com.github.mikephil.charting.data.LineData import com.github.mikephil.charting.data.LineDataSet +import de.michelinside.glucodatahandler.common.Constants import de.michelinside.glucodatahandler.common.database.dbAccess +import de.michelinside.glucodatahandler.common.notifier.InternalNotifier +import de.michelinside.glucodatahandler.common.notifier.NotifySource class ChartBitmapCreator(chart: GlucoseChart, context: Context): ChartCreator(chart, context) { private val LOG_ID = "GDH.Chart.BitmapCreator" + private var bitmap: Bitmap? = null + override val resetChart = true override fun initXaxis() { Log.v(LOG_ID, "initXaxis") @@ -45,9 +52,18 @@ class ChartBitmapCreator(chart: GlucoseChart, context: Context): ChartCreator(ch addEmptyTimeData() chart.notifyDataSetChanged() chart.invalidate() + chart.waitForInvalidate() + bitmap = createBitmap() + InternalNotifier.notify(context, NotifySource.GRAPH_CHANGED, Bundle().apply { putInt(Constants.GRAPH_ID, chart.id) }) } override fun updateTimeElapsed() { update(dbAccess.getGlucoseValues(getMinTime())) } + + fun getBitmap(): Bitmap? { + if(bitmap == null) + bitmap = createBitmap() + return bitmap + } } \ No newline at end of file diff --git a/common/src/main/java/de/michelinside/glucodatahandler/common/chart/ChartCreator.kt b/common/src/main/java/de/michelinside/glucodatahandler/common/chart/ChartCreator.kt index 2648bf6b3..f4ae03331 100644 --- a/common/src/main/java/de/michelinside/glucodatahandler/common/chart/ChartCreator.kt +++ b/common/src/main/java/de/michelinside/glucodatahandler/common/chart/ChartCreator.kt @@ -25,6 +25,7 @@ import de.michelinside.glucodatahandler.common.notifier.InternalNotifier import de.michelinside.glucodatahandler.common.notifier.NotifierInterface import de.michelinside.glucodatahandler.common.notifier.NotifySource import de.michelinside.glucodatahandler.common.utils.Utils +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job @@ -33,6 +34,7 @@ import kotlinx.coroutines.cancelAndJoin import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import java.util.concurrent.TimeUnit +import kotlin.math.abs open class ChartCreator(protected val chart: GlucoseChart, protected val context: Context): NotifierInterface, @@ -43,8 +45,9 @@ open class ChartCreator(protected val chart: GlucoseChart, protected val context protected var init = false protected var hasTimeNotifier = false private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default) - private var currentJob: Job? = null + private var createJob: Job? = null private var dataSyncJob: Job? = null + protected open val resetChart = false private var graphPrefList = mutableSetOf( Constants.SHARED_PREF_LOW_GLUCOSE, @@ -80,14 +83,18 @@ open class ChartCreator(protected val chart: GlucoseChart, protected val context } } - fun create() { - if(currentJob?.isActive == true) { + private fun waitForCreation() { + if(createJob?.isActive == true) { + Log.d(LOG_ID, "waitForCreation - wait for current execution") runBlocking { - Log.d(LOG_ID, "create - wait for current execution") - currentJob!!.join() + createJob!!.join() } } - currentJob = scope.launch { + } + + fun create() { + waitForCreation() + createJob = scope.launch { Log.d(LOG_ID, "create") try { init() @@ -98,7 +105,7 @@ open class ChartCreator(protected val chart: GlucoseChart, protected val context initData() created = true } catch (exc: Exception) { - Log.e(LOG_ID, "create exception: " + exc.message.toString() ) + Log.e(LOG_ID, "create exception: " + exc.message + " - " + exc.stackTraceToString()) } } } @@ -115,12 +122,7 @@ open class ChartCreator(protected val chart: GlucoseChart, protected val context dataSyncJob!!.cancelAndJoin() } } - if(currentJob?.isActive == true) { - runBlocking { - Log.d(LOG_ID, "close current job - wait for current execution") - currentJob!!.join() - } - } + waitForCreation() init = false } } catch (exc: Exception) { @@ -264,6 +266,8 @@ open class ChartCreator(protected val chart: GlucoseChart, protected val context update(values) } Log.d(LOG_ID, "dataSync done") + } catch (exc: CancellationException) { + Log.d(LOG_ID, "dataSync cancelled") } catch (exc: Exception) { Log.e(LOG_ID, "dataSync exception: " + exc.message.toString() ) } @@ -435,6 +439,14 @@ open class ChartCreator(protected val chart: GlucoseChart, protected val context return 0L } + private fun getEntryCount(): Int { + if(chart.data != null && chart.data.dataSetCount > 0) { + val dataSet = chart.data.getDataSetByIndex(0) as LineDataSet + return dataSet.entryCount + } + return 0 + } + private fun getLastTimestamp(): Long { if(chart.data != null && chart.data.dataSetCount > 0) { val dataSet = chart.data.getDataSetByIndex(0) as LineDataSet @@ -448,7 +460,7 @@ open class ChartCreator(protected val chart: GlucoseChart, protected val context protected fun update(values: List) { Log.d(LOG_ID, "update called for ${values.size} value") if(values.isNotEmpty()) { - if(values.first().timestamp != getFirstTimestamp()) { + if(resetChart || values.first().timestamp != getFirstTimestamp() || (abs(values.size-getEntryCount()) > 5)) { resetData(values) return } @@ -470,6 +482,7 @@ open class ChartCreator(protected val chart: GlucoseChart, protected val context dataSet.clear() dataSet.setColors(0) dataSet.setCircleColor(0) + chart.data.clearValues() chart.data.notifyDataChanged() chart.notifyDataSetChanged() } @@ -478,28 +491,27 @@ open class ChartCreator(protected val chart: GlucoseChart, protected val context } override fun OnNotifyData(context: Context, dataSource: NotifySource, extras: Bundle?) { - if(currentJob?.isActive == true) { - runBlocking { - Log.d(LOG_ID, "OnNotifyData - wait for current execution") - currentJob!!.join() - } + Log.d(LOG_ID, "OnNotifyData: $dataSource") + if(!init) { + Log.w(LOG_ID, "Chart still not init - create!") + create() } - currentJob = scope.launch { - try { - Log.d(LOG_ID, "OnNotifyData: $dataSource") - if(dataSource == NotifySource.TIME_VALUE) { - Log.d(LOG_ID, "time elapsed: ${ReceiveData.getElapsedTimeMinute()}") - if(ReceiveData.getElapsedTimeMinute().mod(2) == 0) + if(dataSource == NotifySource.TIME_VALUE) { + Log.d(LOG_ID, "time elapsed: ${ReceiveData.getElapsedTimeMinute()}") + if(ReceiveData.getElapsedTimeMinute().mod(2) == 0) { + waitForCreation() + createJob = scope.launch { + try { updateTimeElapsed() + } catch (exc: Exception) { + Log.e(LOG_ID, "OnNotifyData exception: " + exc.message.toString() + " - " + exc.stackTraceToString() ) + } } - } catch (exc: Exception) { - Log.e(LOG_ID, "OnNotifyData exception: " + exc.message.toString() + " - " + exc.stackTraceToString() ) } } - //currentJob!!.start() } - fun getBitmap(): Bitmap? { + protected fun createBitmap(): Bitmap? { try { if(chart.width > 0 && chart.height > 0) return chart.drawToBitmap() diff --git a/common/src/main/java/de/michelinside/glucodatahandler/common/chart/CustomBubbleMarker.kt b/common/src/main/java/de/michelinside/glucodatahandler/common/chart/CustomBubbleMarker.kt index c10c09577..c324a403b 100644 --- a/common/src/main/java/de/michelinside/glucodatahandler/common/chart/CustomBubbleMarker.kt +++ b/common/src/main/java/de/michelinside/glucodatahandler/common/chart/CustomBubbleMarker.kt @@ -81,7 +81,7 @@ class CustomBubbleMarker(context: Context) : MarkerView(context, R.layout.marker val paint = Paint().apply { style = Paint.Style.FILL strokeJoin = Paint.Join.ROUND - color = if(isGlucose) ContextCompat.getColor(context, R.color.transparent_widget_background) else 0 + color = if(isGlucose) ContextCompat.getColor(context, R.color.transparent_marker_background) else 0 } val chart = chartView diff --git a/common/src/main/java/de/michelinside/glucodatahandler/common/chart/GlucoseChart.kt b/common/src/main/java/de/michelinside/glucodatahandler/common/chart/GlucoseChart.kt index 9344ce417..7b4f6aaab 100644 --- a/common/src/main/java/de/michelinside/glucodatahandler/common/chart/GlucoseChart.kt +++ b/common/src/main/java/de/michelinside/glucodatahandler/common/chart/GlucoseChart.kt @@ -2,14 +2,10 @@ package de.michelinside.glucodatahandler.common.chart import android.content.Context import android.graphics.Canvas -import android.os.Bundle import android.util.AttributeSet import android.util.Log import android.view.View import com.github.mikephil.charting.charts.LineChart -import de.michelinside.glucodatahandler.common.Constants -import de.michelinside.glucodatahandler.common.notifier.InternalNotifier -import de.michelinside.glucodatahandler.common.notifier.NotifySource class GlucoseChart: LineChart { private val LOG_ID = "GDH.Chart.GlucoseChart" @@ -26,16 +22,17 @@ class GlucoseChart: LineChart { private var processing = false + override fun getId(): Int { + if(super.getId() == View.NO_ID) { + id = generateViewId() + } + return super.getId() + } + override fun invalidate() { processing = true try { - if(id == View.NO_ID) { - id = generateViewId() - } - //Log.v(LOG_ID, "start invalidate - notify ID: $id") super.invalidate() - //Log.i(LOG_ID, "invalidate finished - notify ID: $id") - InternalNotifier.notify(context, NotifySource.GRAPH_CHANGED, Bundle().apply { putInt(Constants.GRAPH_ID, id) }) } catch (exc: Exception) { Log.e(LOG_ID, "invalidate exception: ${exc.message}\n${exc.stackTraceToString()}") } diff --git a/common/src/main/java/de/michelinside/glucodatahandler/common/notifier/InternalNotifier.kt b/common/src/main/java/de/michelinside/glucodatahandler/common/notifier/InternalNotifier.kt index e780e44fd..8a915c42e 100644 --- a/common/src/main/java/de/michelinside/glucodatahandler/common/notifier/InternalNotifier.kt +++ b/common/src/main/java/de/michelinside/glucodatahandler/common/notifier/InternalNotifier.kt @@ -39,10 +39,18 @@ object InternalNotifier { return notifiers.contains(notifier) } + private fun getNotifiers(): MutableMap?> { + Log.d(LOG_ID, "getNotifiers") + synchronized(notifiers) { + Log.d(LOG_ID, "getNotifiers synchronized") + return notifiers.toMutableMap() + } + } + fun notify(context: Context, notifySource: NotifySource, extras: Bundle?) { Log.d(LOG_ID, "Sending new data from " + notifySource.toString() + " to " + getNotifierCount(notifySource) + " notifier(s).") - val curNotifiers = notifiers.toMutableMap() + val curNotifiers = getNotifiers() curNotifiers.forEach{ try { if (it.value == null || it.value!!.contains(notifySource)) { @@ -57,8 +65,8 @@ object InternalNotifier { fun getNotifierCount(notifySource: NotifySource): Int { var count = 0 - val curNotifiers = notifiers.toMutableMap() - curNotifiers.forEach { + val curNotifiers = getNotifiers() + curNotifiers.forEach{ if (it.value == null || it.value!!.contains(notifySource)) { Log.v(LOG_ID, "Notifier " + it.toString() + " has source " + notifySource.toString()) count++ @@ -92,8 +100,12 @@ object InternalNotifier { private fun hasSource(notifier: NotifierInterface, notifySource: NotifySource): Boolean { if(hasNotifier(notifier)) { - Log.v(LOG_ID, "Check notifier ${notifier} has source $notifySource: ${notifiers[notifier]}") - return notifiers[notifier]!!.contains(notifySource) + synchronized(notifiers) { + if(notifiers.contains(notifier) && notifiers[notifier] != null) { + Log.v(LOG_ID, "Check notifier ${notifier} has source $notifySource: ${notifiers[notifier]}") + return notifiers[notifier]!!.contains(notifySource) + } + } } return false } diff --git a/common/src/main/res/layout/marker_layout.xml b/common/src/main/res/layout/marker_layout.xml index 0431d490d..419eebd2a 100644 --- a/common/src/main/res/layout/marker_layout.xml +++ b/common/src/main/res/layout/marker_layout.xml @@ -5,7 +5,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="vertical" - android:background="@color/transparent_widget_background" + android:background="@color/transparent_marker_background" android:paddingHorizontal="12dp" android:paddingVertical="8dp"> diff --git a/common/src/main/res/values-night/colors.xml b/common/src/main/res/values-night/colors.xml index c03d1134a..0bfe5801f 100644 --- a/common/src/main/res/values-night/colors.xml +++ b/common/src/main/res/values-night/colors.xml @@ -17,4 +17,5 @@ #FF666666 #55000000 @color/white + #AA000000 \ No newline at end of file diff --git a/common/src/main/res/values/colors.xml b/common/src/main/res/values/colors.xml index 0167d98f1..fed988e7a 100644 --- a/common/src/main/res/values/colors.xml +++ b/common/src/main/res/values/colors.xml @@ -18,4 +18,5 @@ #FFF0F0F0 #55000000 @color/white + #77000000 \ No newline at end of file diff --git a/mobile/src/main/res/layout-land/activity_main.xml b/mobile/src/main/res/layout-land/activity_main.xml index f9cbf0bd4..2633a9ccf 100644 --- a/mobile/src/main/res/layout-land/activity_main.xml +++ b/mobile/src/main/res/layout-land/activity_main.xml @@ -143,6 +143,16 @@ android:layout_marginTop="20dp" android:text="@string/menu_sources" /> + + + From feb99db8f3d96574374c7b2a978f0e9eeb2b3e5c Mon Sep 17 00:00:00 2001 From: pachi81 Date: Tue, 28 Jan 2025 10:54:27 +0100 Subject: [PATCH 11/17] DB sync --- build.gradle | 2 +- common/build.gradle | 1 + .../glucodatahandler/common/Constants.kt | 1 + .../common/WearPhoneConnection.kt | 39 +++- .../common/database/dbSync.kt | 181 ++++++++++++++++++ 5 files changed, 215 insertions(+), 9 deletions(-) create mode 100644 common/src/main/java/de/michelinside/glucodatahandler/common/database/dbSync.kt diff --git a/build.gradle b/build.gradle index f78539417..0fa0fb0a2 100644 --- a/build.gradle +++ b/build.gradle @@ -3,7 +3,7 @@ plugins { id 'com.android.application' version '8.8.0' apply false id 'com.android.library' version '8.8.0' apply false id 'org.jetbrains.kotlin.android' version '2.1.0' apply false - id("com.google.devtools.ksp") version "2.1.0-1.0.29" apply false + id 'com.google.devtools.ksp' version "2.1.0-1.0.29" apply false } Properties properties = new Properties() diff --git a/common/build.gradle b/common/build.gradle index 562592670..9f8844349 100644 --- a/common/build.gradle +++ b/common/build.gradle @@ -56,6 +56,7 @@ dependencies { androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1' testImplementation 'org.mockito:mockito-core:4.11.0' testImplementation 'org.mockito.kotlin:mockito-kotlin:4.0.0' + implementation 'com.google.code.gson:gson:2.10.1' //Room def roomVersion = "2.6.1" diff --git a/common/src/main/java/de/michelinside/glucodatahandler/common/Constants.kt b/common/src/main/java/de/michelinside/glucodatahandler/common/Constants.kt index a6d50b4cb..173179ef7 100644 --- a/common/src/main/java/de/michelinside/glucodatahandler/common/Constants.kt +++ b/common/src/main/java/de/michelinside/glucodatahandler/common/Constants.kt @@ -11,6 +11,7 @@ object Constants { const val REQUEST_DATA_MESSAGE_PATH = "/request_data_intent" const val REQUEST_LOGCAT_MESSAGE_PATH = "/request_logcat_intent" const val LOGCAT_CHANNEL_PATH = "/logcat_intent" + const val DB_SYNC_CHANNEL_PATH = "/db_sync_intent" const val COMMAND_PATH = "/command_intent" const val GLUCODATA_BROADCAST_ACTION = "glucodata.Minute" const val SETTINGS_BUNDLE = "settings_bundle" diff --git a/common/src/main/java/de/michelinside/glucodatahandler/common/WearPhoneConnection.kt b/common/src/main/java/de/michelinside/glucodatahandler/common/WearPhoneConnection.kt index abcacb8f2..6175e7ced 100644 --- a/common/src/main/java/de/michelinside/glucodatahandler/common/WearPhoneConnection.kt +++ b/common/src/main/java/de/michelinside/glucodatahandler/common/WearPhoneConnection.kt @@ -6,6 +6,7 @@ import android.os.Bundle import android.util.Log import com.google.android.gms.tasks.Tasks import com.google.android.gms.wearable.* +import de.michelinside.glucodatahandler.common.database.dbSync import de.michelinside.glucodatahandler.common.notification.AlarmHandler import de.michelinside.glucodatahandler.common.notification.AlarmNotificationBase import de.michelinside.glucodatahandler.common.notification.AlarmType @@ -29,7 +30,8 @@ enum class Command { DISABLE_INACTIVE_TIME, PAUSE_NODE, RESUME_NODE, - FORCE_UPDATE + FORCE_UPDATE, + DB_SYNC } class WearPhoneConnection : MessageClient.OnMessageReceivedListener, CapabilityClient.OnCapabilityChangedListener, NotifierInterface { @@ -384,9 +386,10 @@ class WearPhoneConnection : MessageClient.OnMessageReceivedListener, CapabilityC LOG_ID, dataSource.toString() + " data send to node " + node.toString() ) - noDataSend.remove(node.id) - if(notConnectedNodes.isEmpty()) - removeTimer() + if(noDataSend.contains(node.id)) { + noDataSend.remove(node.id) + checkNodeConnect(node.id) + } } addOnFailureListener { error -> if (retryCount < 2) { @@ -412,6 +415,20 @@ class WearPhoneConnection : MessageClient.OnMessageReceivedListener, CapabilityC } } + private fun checkNodeConnect(nodeId: String) { + Log.d(LOG_ID, "check node connect $nodeId") + if(!noDataReceived.contains(nodeId) && !noDataSend.contains(nodeId)) { + Log.i(LOG_ID, "Node with id " + nodeId + " connected!") + if(notConnectedNodes.isEmpty()) + removeTimer() + dbSync.requestDbSync(context) + } else if(noDataReceived.contains(nodeId)) { + Log.i(LOG_ID, "Node with id " + nodeId + " still waiting for receiving data!") + } else if(noDataSend.contains(nodeId)) { + Log.i(LOG_ID, "Node with id " + nodeId + " still sending data!") + } + } + fun sendCommand(command: Command, extras: Bundle?) { // Send command to all nodes in parallel Log.d(LOG_ID, "sendCommand called for $command with extras: ${Utils.dumpBundle(extras)}") @@ -444,9 +461,10 @@ class WearPhoneConnection : MessageClient.OnMessageReceivedListener, CapabilityC try { Log.i(LOG_ID, "onMessageReceived from " + p0.sourceNodeId + " with path " + p0.path) checkConnectedNode(p0.sourceNodeId) - noDataReceived.remove(p0.sourceNodeId) - if (notConnectedNodes.isEmpty()) - removeTimer() + if(noDataReceived.contains(p0.sourceNodeId)) { + noDataReceived.remove(p0.sourceNodeId) + checkNodeConnect(p0.sourceNodeId) + } val extras = Utils.bytesToBundle(p0.data) //Log.v(LOG_ID, "Received extras for path ${p0.path}: ${Utils.dumpBundle(extras)}") if(extras!= null) { @@ -562,6 +580,10 @@ class WearPhoneConnection : MessageClient.OnMessageReceivedListener, CapabilityC if(p0.path == Constants.REQUEST_DATA_MESSAGE_PATH || forceSend) { Log.d(LOG_ID, "Data request received from " + p0.sourceNodeId) + if (p0.path == Constants.REQUEST_DATA_MESSAGE_PATH) { + // new data request -> new connection on other side -> reset connection + noDataSend.add(p0.sourceNodeId) // add to trigger db sync after connection established + } var bundle = ReceiveData.createExtras() var source = NotifySource.BROADCAST if( bundle == null && BatteryReceiver.batteryPercentage >= 0) { @@ -583,7 +605,7 @@ class WearPhoneConnection : MessageClient.OnMessageReceivedListener, CapabilityC if(p0.path == Constants.REQUEST_LOGCAT_MESSAGE_PATH) { sendLogcat(p0.sourceNodeId) } - if (noDataSend.contains(p0.sourceNodeId)) { + if (p0.path != Constants.REQUEST_DATA_MESSAGE_PATH && noDataSend.contains(p0.sourceNodeId)) { sendDataRequest() } } catch (exc: Exception) { @@ -621,6 +643,7 @@ class WearPhoneConnection : MessageClient.OnMessageReceivedListener, CapabilityC InternalNotifier.notify(context, NotifySource.MESSAGECLIENT, bundle) } } + Command.DB_SYNC -> dbSync.sendData(context, nodeId) } } catch (exc: Exception) { diff --git a/common/src/main/java/de/michelinside/glucodatahandler/common/database/dbSync.kt b/common/src/main/java/de/michelinside/glucodatahandler/common/database/dbSync.kt new file mode 100644 index 000000000..e2f7e8f5e --- /dev/null +++ b/common/src/main/java/de/michelinside/glucodatahandler/common/database/dbSync.kt @@ -0,0 +1,181 @@ +package de.michelinside.glucodatahandler.common.database + +import android.content.Context +import android.util.Log +import com.google.android.gms.tasks.Tasks +import com.google.android.gms.wearable.ChannelClient +import com.google.android.gms.wearable.Wearable +import com.google.gson.Gson +import de.michelinside.glucodatahandler.common.Command +import de.michelinside.glucodatahandler.common.Constants +import de.michelinside.glucodatahandler.common.GlucoDataService +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.launch +import java.io.InputStream +import java.io.InputStreamReader +import java.io.StringWriter + +object dbSync : ChannelClient.ChannelCallback() { + private val LOG_ID = "GDH.dbSync" + private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO) + private var finished = true + private var channel: ChannelClient.Channel? = null + private var retryCount = 0 + + private fun registerChannel(context: Context) { + Log.d(LOG_ID, "registerChannel called") + Wearable.getChannelClient(context).registerChannelCallback(this) + } + + private fun getStringFromInputStream(stream: InputStream?): String { + var n = 0 + val buffer = CharArray(1024 * 4) + val reader = InputStreamReader(stream, "UTF8") + val writer = StringWriter() + while (-1 != (reader.read(buffer).also { n = it })) writer.write(buffer, 0, n) + return writer.toString() + } + + override fun onChannelOpened(p0: ChannelClient.Channel) { + try { + super.onChannelOpened(p0) + Log.d(LOG_ID, "onChannelOpened") + channel = p0 + scope.launch { + try { + Log.d(LOG_ID, "receiving...") + val inputStream = Tasks.await(Wearable.getChannelClient(GlucoDataService.context!!).getInputStream(p0)) + Log.d(LOG_ID, "received - read") + val received = getStringFromInputStream(inputStream) + Log.v(LOG_ID, "received data: $received") + val gson = Gson() + val data = gson.fromJson(received, Array::class.java).toList() + Log.i(LOG_ID, "${data.size} values received") + dbAccess.addGlucoseValues(data) + Log.d(LOG_ID, "db data saved") + } catch (exc: Exception) { + Log.e(LOG_ID, "reading input exception: " + exc.message.toString() ) + } + } + } catch (exc: Exception) { + Log.e(LOG_ID, "onChannelOpened exception: " + exc.message.toString() ) + } + } + + override fun onInputClosed(p0: ChannelClient.Channel, i: Int, i1: Int) { + try { + super.onInputClosed(p0, i, i1) + Log.d(LOG_ID, "onInputClosed") + Wearable.getChannelClient(GlucoDataService.context!!).close(p0) + channel = null + finished = true + } catch (exc: Exception) { + Log.e(LOG_ID, "onInputClosed exception: " + exc.message.toString() ) + } + } + + override fun onOutputClosed(p0: ChannelClient.Channel, p1: Int, p2: Int) { + try { + super.onOutputClosed(p0, p1, p2) + Log.d(LOG_ID, "onOutputClosed") + Wearable.getChannelClient(GlucoDataService.context!!).close(p0) + channel = null + finished = true + } catch (exc: Exception) { + Log.e(LOG_ID, "onInputClosed exception: " + exc.message.toString() ) + } + } + + fun sendData(context: Context, nodeId: String) { + try { + Log.i(LOG_ID, "send db data to $nodeId") + finished = false + val channelClient = Wearable.getChannelClient(context) + val channelTask = + channelClient.openChannel(nodeId, Constants.DB_SYNC_CHANNEL_PATH) + channelTask.addOnSuccessListener { channel -> + Thread { + try { + val outputStream = Tasks.await(channelClient.getOutputStream(channel)) + val data = dbAccess.getGlucoseValues() + Log.i(LOG_ID, "sending ${data.size} values") + val gson = Gson() + val string = gson.toJson(data) + Log.v(LOG_ID, string) + outputStream.write(string.toByteArray()) + outputStream.flush() + outputStream.close() + channelClient.close(channel) + Log.d(LOG_ID, "db data sent") + } catch (exc: Exception) { + Log.e(LOG_ID, "sendData exception: " + exc.toString()) + } + }.start() + } + } catch (exc: Exception) { + Log.e(LOG_ID, "sendData exception: " + exc.toString()) + } + } + + private fun close(context: Context) { + try { + if(channel != null) { + Log.d(LOG_ID, "close channel") + val channelClient = Wearable.getChannelClient(context) + channelClient.close(channel!!) + channelClient.unregisterChannelCallback(this) + finished = true + channel = null + Log.d(LOG_ID, "channel closed") + } + } catch (exc: Exception) { + Log.e(LOG_ID, "close exception: " + exc.toString()) + } + } + + private fun waitFor(context: Context) { + Thread { + try { + Log.v(LOG_ID, "Waiting for receiving db data") + var count = 0 + while (!finished && count < 10) { + Thread.sleep(1000) + count++ + } + var success: Boolean + if (!finished) { + Log.w(LOG_ID, "Receiving still not finished!") + success = false + } else { + Log.d(LOG_ID, "Receiving finished!") + success = true + } + close(context) + if (success) { + Log.i(LOG_ID, "db sync succeeded") + } else { + Log.w(LOG_ID, "db sync failed") + if(retryCount < 3) { + retryCount++ + requestDbSync(context, retryCount) + } + } + } catch (exc: Exception) { + Log.e(LOG_ID, "waitFor exception: " + exc.message.toString() ) + } + }.start() + } + + fun requestDbSync(context: Context, curRetry: Int = 0) { + Log.d(LOG_ID, "request db sync - finished: $finished - retry: $curRetry") + if (finished) { + finished = false + retryCount = curRetry + registerChannel(context) + GlucoDataService.sendCommand(Command.DB_SYNC) + waitFor(context) + } + } +} \ No newline at end of file From 83741a4ea5e3503a486f533530a9a827518450d2 Mon Sep 17 00:00:00 2001 From: pachi81 Date: Tue, 28 Jan 2025 19:59:51 +0100 Subject: [PATCH 12/17] Wear Graph --- .../common/chart/ChartBitmap.kt | 15 ++++++- .../common/chart/ChartCreator.kt | 42 +++++++++++-------- wear/build.gradle | 1 + .../glucodatahandler/WearActivity.kt | 12 ++++++ .../glucodatahandler/WearChartCreator.kt | 30 +++++++++++++ wear/src/main/res/layout/activity_wear.xml | 6 +++ 6 files changed, 86 insertions(+), 20 deletions(-) create mode 100644 wear/src/main/java/de/michelinside/glucodatahandler/WearChartCreator.kt diff --git a/common/src/main/java/de/michelinside/glucodatahandler/common/chart/ChartBitmap.kt b/common/src/main/java/de/michelinside/glucodatahandler/common/chart/ChartBitmap.kt index 0048b6dfd..9b3c2132f 100644 --- a/common/src/main/java/de/michelinside/glucodatahandler/common/chart/ChartBitmap.kt +++ b/common/src/main/java/de/michelinside/glucodatahandler/common/chart/ChartBitmap.kt @@ -10,6 +10,10 @@ import de.michelinside.glucodatahandler.common.notifier.InternalNotifier import de.michelinside.glucodatahandler.common.notifier.NotifierInterface import de.michelinside.glucodatahandler.common.notifier.NotifySource import de.michelinside.glucodatahandler.common.utils.Utils +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch class ChartBitmap(val imageView: ImageView, val context: Context): NotifierInterface { @@ -33,11 +37,18 @@ class ChartBitmap(val imageView: ImageView, val context: Context): NotifierInter chartViewer.close() } + @OptIn(DelicateCoroutinesApi::class) override fun OnNotifyData(context: Context, dataSource: NotifySource, extras: Bundle?) { Log.d(LOG_ID, "OnNotifyData for $dataSource - id ${chart.id} - extras: ${Utils.dumpBundle(extras)}") if(dataSource == NotifySource.GRAPH_CHANGED && extras?.getInt(Constants.GRAPH_ID) == chart.id) { - Log.i(LOG_ID, "Update bitmap") - imageView.setImageBitmap(chartViewer.getBitmap()) + GlobalScope.launch(Dispatchers.Main) { + try { + Log.i(LOG_ID, "Update bitmap") + imageView.setImageBitmap(chartViewer.getBitmap()) + } catch (exc: Exception) { + Log.e(LOG_ID, "Update bitmap exception: " + exc.toString()) + } + } } } } \ No newline at end of file diff --git a/common/src/main/java/de/michelinside/glucodatahandler/common/chart/ChartCreator.kt b/common/src/main/java/de/michelinside/glucodatahandler/common/chart/ChartCreator.kt index f4ae03331..5b0a009ae 100644 --- a/common/src/main/java/de/michelinside/glucodatahandler/common/chart/ChartCreator.kt +++ b/common/src/main/java/de/michelinside/glucodatahandler/common/chart/ChartCreator.kt @@ -84,29 +84,27 @@ open class ChartCreator(protected val chart: GlucoseChart, protected val context } private fun waitForCreation() { - if(createJob?.isActive == true) { + /*if(createJob?.isActive == true) { Log.d(LOG_ID, "waitForCreation - wait for current execution") runBlocking { createJob!!.join() } - } + }*/ } fun create() { waitForCreation() - createJob = scope.launch { - Log.d(LOG_ID, "create") - try { - init() - resetChart() - initXaxis() - initYaxis() - initChart() - initData() - created = true - } catch (exc: Exception) { - Log.e(LOG_ID, "create exception: " + exc.message + " - " + exc.stackTraceToString()) - } + Log.d(LOG_ID, "create") + try { + init() + resetChart() + initXaxis() + initYaxis() + initChart() + initData() + created = true + } catch (exc: Exception) { + Log.e(LOG_ID, "create exception: " + exc.message + " - " + exc.stackTraceToString()) } } @@ -171,6 +169,7 @@ open class ChartCreator(protected val chart: GlucoseChart, protected val context chart.axisRight.setDrawZeroLine(false) chart.axisRight.setDrawAxisLine(false) chart.axisRight.setDrawGridLines(false) + chart.axisRight.xOffset = -15F chart.axisRight.textColor = context.resources.getColor(R.color.text_color) chart.axisLeft.isEnabled = showOtherUnit() if(chart.axisLeft.isEnabled) { @@ -178,6 +177,7 @@ open class ChartCreator(protected val chart: GlucoseChart, protected val context chart.axisLeft.setDrawZeroLine(false) chart.axisLeft.setDrawAxisLine(false) chart.axisLeft.setDrawGridLines(false) + chart.axisLeft.xOffset = -15F chart.axisLeft.textColor = context.resources.getColor(R.color.text_color) } } @@ -216,6 +216,8 @@ open class ChartCreator(protected val chart: GlucoseChart, protected val context chart.legend.isEnabled = false chart.description.isEnabled = false chart.isScaleYEnabled = false + chart.minOffset = 0F + chart.setExtraOffsets(4F, 4F, 4F, 4F) if(touchEnabled) { val mMarker = CustomBubbleMarker(context) mMarker.chartView = chart @@ -232,6 +234,10 @@ open class ChartCreator(protected val chart: GlucoseChart, protected val context Log.v(LOG_ID, "onNothingSelected") } }) + chart.setOnLongClickListener { + chart.highlightValue(null) + true + } } chart.axisRight.removeAllLimitLines() @@ -500,13 +506,13 @@ open class ChartCreator(protected val chart: GlucoseChart, protected val context Log.d(LOG_ID, "time elapsed: ${ReceiveData.getElapsedTimeMinute()}") if(ReceiveData.getElapsedTimeMinute().mod(2) == 0) { waitForCreation() - createJob = scope.launch { + //createJob = scope.launch { try { updateTimeElapsed() } catch (exc: Exception) { Log.e(LOG_ID, "OnNotifyData exception: " + exc.message.toString() + " - " + exc.stackTraceToString() ) } - } + //} } } } @@ -521,7 +527,7 @@ open class ChartCreator(protected val chart: GlucoseChart, protected val context return null } - private fun showOtherUnit(): Boolean { + protected open fun showOtherUnit(): Boolean { return sharedPref.getBoolean(Constants.SHARED_PREF_SHOW_OTHER_UNIT, false) } diff --git a/wear/build.gradle b/wear/build.gradle index 43b5f5b1c..cefa78960 100644 --- a/wear/build.gradle +++ b/wear/build.gradle @@ -104,6 +104,7 @@ dependencies { implementation 'androidx.appcompat:appcompat:1.7.0' implementation 'androidx.work:work-runtime:2.9.1' implementation "androidx.core:core-splashscreen:1.1.0-rc01" + implementation 'com.github.PhilJay:MPAndroidChart:v3.1.0' } afterEvaluate { diff --git a/wear/src/main/java/de/michelinside/glucodatahandler/WearActivity.kt b/wear/src/main/java/de/michelinside/glucodatahandler/WearActivity.kt index eecb05f9c..87fc5b000 100644 --- a/wear/src/main/java/de/michelinside/glucodatahandler/WearActivity.kt +++ b/wear/src/main/java/de/michelinside/glucodatahandler/WearActivity.kt @@ -25,6 +25,7 @@ import de.michelinside.glucodatahandler.common.utils.Utils import de.michelinside.glucodatahandler.databinding.ActivityWearBinding import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.core.view.setPadding +import de.michelinside.glucodatahandler.common.chart.GlucoseChart import de.michelinside.glucodatahandler.common.notification.AlarmHandler import de.michelinside.glucodatahandler.common.notification.AlarmState import de.michelinside.glucodatahandler.common.notification.AlarmType @@ -61,8 +62,10 @@ class WearActivity : AppCompatActivity(), NotifierInterface { private lateinit var btnSettings: Button private lateinit var btnSources: Button private lateinit var btnAlarms: Button + private lateinit var chart: GlucoseChart private var doNotUpdate = false private var requestNotificationPermission = false + private lateinit var chartCreator: WearChartCreator override fun onCreate(savedInstanceState: Bundle?) { try { @@ -87,6 +90,7 @@ class WearActivity : AppCompatActivity(), NotifierInterface { tableAlarms = findViewById(R.id.tableAlarms) tableDetails = findViewById(R.id.tableDetails) tableNotes = findViewById(R.id.tableNotes) + chart = findViewById(R.id.chart) txtVersion = findViewById(R.id.txtVersion) txtVersion.text = BuildConfig.VERSION_NAME @@ -118,6 +122,8 @@ class WearActivity : AppCompatActivity(), NotifierInterface { val intent = Intent(this, AlarmsActivity::class.java) startActivity(intent) } + chartCreator = WearChartCreator(chart, this) + chartCreator.create() if(requestPermission()) GlucoDataServiceWear.start(this) PackageUtils.updatePackages(this) @@ -170,6 +176,12 @@ class WearActivity : AppCompatActivity(), NotifierInterface { } } + override fun onDestroy() { + Log.v(LOG_ID, "onDestroy called") + super.onDestroy() + chartCreator.close() + } + fun requestPermission() : Boolean { requestNotificationPermission = false if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { diff --git a/wear/src/main/java/de/michelinside/glucodatahandler/WearChartCreator.kt b/wear/src/main/java/de/michelinside/glucodatahandler/WearChartCreator.kt new file mode 100644 index 000000000..1c6b56960 --- /dev/null +++ b/wear/src/main/java/de/michelinside/glucodatahandler/WearChartCreator.kt @@ -0,0 +1,30 @@ +package de.michelinside.glucodatahandler + +import android.content.Context +import android.util.Log +import de.michelinside.glucodatahandler.common.chart.ChartCreator +import de.michelinside.glucodatahandler.common.chart.GlucoseChart + +class WearChartCreator(chart: GlucoseChart, context: Context) : ChartCreator(chart, context) { + private val LOG_ID = "GDH.Chart.WearCreator" + + override fun initXaxis() { + Log.v(LOG_ID, "initXaxis") + super.initXaxis() + chart.xAxis.setLabelCount(4) + } + + override fun showOtherUnit(): Boolean = false + + override fun initYaxis() { + Log.v(LOG_ID, "initYaxis") + super.initYaxis() + /* + chart.axisRight.setDrawAxisLine(false) + chart.axisRight.setDrawLabels(false) + chart.axisRight.setDrawZeroLine(false) + chart.axisRight.setDrawGridLines(false) + chart.axisLeft.isEnabled = false + */ + } +} \ No newline at end of file diff --git a/wear/src/main/res/layout/activity_wear.xml b/wear/src/main/res/layout/activity_wear.xml index 70f9249a9..0aab81ebb 100644 --- a/wear/src/main/res/layout/activity_wear.xml +++ b/wear/src/main/res/layout/activity_wear.xml @@ -153,6 +153,12 @@ android:textColor="#FFFFFF" android:textSize="14sp" /> + + From b27bfd8c21e3a29a74501d6fff471d08ce81040c Mon Sep 17 00:00:00 2001 From: pachi81 Date: Wed, 29 Jan 2025 07:42:51 +0100 Subject: [PATCH 13/17] Refactor chart --- .../common/chart/ChartBitmap.kt | 43 +++------- .../common/chart/ChartBitmapCreator.kt | 4 +- .../common/chart/ChartBitmapView.kt | 49 +++++++++++ .../common/chart/ChartCreator.kt | 83 +++++++++++++++---- .../common/chart/CustomBubbleMarker.kt | 11 ++- common/src/main/res/layout/marker_layout.xml | 10 +++ common/src/main/res/values-night/colors.xml | 1 + common/src/main/res/values/colors.xml | 1 + .../glucodatahandler/MainActivity.kt | 24 +++++- mobile/src/main/res/layout/activity_main.xml | 7 ++ .../glucodatahandler/WearActivity.kt | 13 ++- .../glucodatahandler/WearChartCreator.kt | 4 + wear/src/main/res/layout/activity_wear.xml | 7 +- 13 files changed, 190 insertions(+), 67 deletions(-) create mode 100644 common/src/main/java/de/michelinside/glucodatahandler/common/chart/ChartBitmapView.kt diff --git a/common/src/main/java/de/michelinside/glucodatahandler/common/chart/ChartBitmap.kt b/common/src/main/java/de/michelinside/glucodatahandler/common/chart/ChartBitmap.kt index 9b3c2132f..b2b9d0a2f 100644 --- a/common/src/main/java/de/michelinside/glucodatahandler/common/chart/ChartBitmap.kt +++ b/common/src/main/java/de/michelinside/glucodatahandler/common/chart/ChartBitmap.kt @@ -1,54 +1,35 @@ package de.michelinside.glucodatahandler.common.chart import android.content.Context -import android.os.Bundle +import android.graphics.Bitmap import android.util.Log import android.view.View -import android.widget.ImageView -import de.michelinside.glucodatahandler.common.Constants -import de.michelinside.glucodatahandler.common.notifier.InternalNotifier -import de.michelinside.glucodatahandler.common.notifier.NotifierInterface -import de.michelinside.glucodatahandler.common.notifier.NotifySource -import de.michelinside.glucodatahandler.common.utils.Utils -import kotlinx.coroutines.DelicateCoroutinesApi -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.launch - -class ChartBitmap(val imageView: ImageView, val context: Context): NotifierInterface { + +class ChartBitmap(val context: Context, durationPref: String = "", width: Int = 1000, height: Int = 0) { private val LOG_ID = "GDH.Chart.Bitmap" private var chartViewer: ChartBitmapCreator private var chart: GlucoseChart = GlucoseChart(context) init { - Log.v(LOG_ID, "init") - chart.measure (View.MeasureSpec.makeMeasureSpec (1000, View.MeasureSpec.EXACTLY), View.MeasureSpec.makeMeasureSpec (400, View.MeasureSpec.EXACTLY)) + val viewHeight = if(height > 0) height else width/3 + Log.v(LOG_ID, "init - width: $width - durationPref: $durationPref") + chart.measure (View.MeasureSpec.makeMeasureSpec (width, View.MeasureSpec.EXACTLY), View.MeasureSpec.makeMeasureSpec (viewHeight, View.MeasureSpec.EXACTLY)) chart.layout (0, 0, chart.getMeasuredWidth(), chart.getMeasuredHeight()) - InternalNotifier.addNotifier(context, this, mutableSetOf(NotifySource.GRAPH_CHANGED)) - chartViewer = ChartBitmapCreator(chart, context) + chartViewer = ChartBitmapCreator(chart, context, durationPref) chartViewer.create() } fun close() { Log.v(LOG_ID, "close") - InternalNotifier.remNotifier(context, this) chartViewer.close() } - @OptIn(DelicateCoroutinesApi::class) - override fun OnNotifyData(context: Context, dataSource: NotifySource, extras: Bundle?) { - Log.d(LOG_ID, "OnNotifyData for $dataSource - id ${chart.id} - extras: ${Utils.dumpBundle(extras)}") - if(dataSource == NotifySource.GRAPH_CHANGED && extras?.getInt(Constants.GRAPH_ID) == chart.id) { - GlobalScope.launch(Dispatchers.Main) { - try { - Log.i(LOG_ID, "Update bitmap") - imageView.setImageBitmap(chartViewer.getBitmap()) - } catch (exc: Exception) { - Log.e(LOG_ID, "Update bitmap exception: " + exc.toString()) - } - } - } + fun getBitmap(): Bitmap? { + return chartViewer.getBitmap() } + + val chartId: Int get() = chart.id + } \ No newline at end of file diff --git a/common/src/main/java/de/michelinside/glucodatahandler/common/chart/ChartBitmapCreator.kt b/common/src/main/java/de/michelinside/glucodatahandler/common/chart/ChartBitmapCreator.kt index 8a9029915..953e1e867 100644 --- a/common/src/main/java/de/michelinside/glucodatahandler/common/chart/ChartBitmapCreator.kt +++ b/common/src/main/java/de/michelinside/glucodatahandler/common/chart/ChartBitmapCreator.kt @@ -11,7 +11,7 @@ import de.michelinside.glucodatahandler.common.database.dbAccess import de.michelinside.glucodatahandler.common.notifier.InternalNotifier import de.michelinside.glucodatahandler.common.notifier.NotifySource -class ChartBitmapCreator(chart: GlucoseChart, context: Context): ChartCreator(chart, context) { +class ChartBitmapCreator(chart: GlucoseChart, context: Context, durationPref: String = ""): ChartCreator(chart, context, durationPref) { private val LOG_ID = "GDH.Chart.BitmapCreator" private var bitmap: Bitmap? = null override val resetChart = true @@ -36,6 +36,8 @@ class ChartBitmapCreator(chart: GlucoseChart, context: Context): ChartCreator(ch } override fun getDefaultRange(): Long { + if(durationPref.isNotEmpty()) + return super.getDefaultRange() return 120L } diff --git a/common/src/main/java/de/michelinside/glucodatahandler/common/chart/ChartBitmapView.kt b/common/src/main/java/de/michelinside/glucodatahandler/common/chart/ChartBitmapView.kt new file mode 100644 index 000000000..0ae0cfe1c --- /dev/null +++ b/common/src/main/java/de/michelinside/glucodatahandler/common/chart/ChartBitmapView.kt @@ -0,0 +1,49 @@ +package de.michelinside.glucodatahandler.common.chart + +import android.content.Context +import android.os.Bundle +import android.util.Log +import android.view.View +import android.widget.ImageView +import de.michelinside.glucodatahandler.common.Constants +import de.michelinside.glucodatahandler.common.notifier.InternalNotifier +import de.michelinside.glucodatahandler.common.notifier.NotifierInterface +import de.michelinside.glucodatahandler.common.notifier.NotifySource +import de.michelinside.glucodatahandler.common.utils.Utils +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch + +class ChartBitmapView(val imageView: ImageView, val context: Context, durationPref: String = ""): NotifierInterface { + + private val LOG_ID = "GDH.Chart.BitmapView" + + private var chartBitmap: ChartBitmap + init { + Log.v(LOG_ID, "init") + InternalNotifier.addNotifier(context, this, mutableSetOf(NotifySource.GRAPH_CHANGED)) + chartBitmap = ChartBitmap(context, durationPref) + } + + fun close() { + Log.v(LOG_ID, "close") + InternalNotifier.remNotifier(context, this) + chartBitmap.close() + } + + @OptIn(DelicateCoroutinesApi::class) + override fun OnNotifyData(context: Context, dataSource: NotifySource, extras: Bundle?) { + Log.d(LOG_ID, "OnNotifyData for $dataSource - id ${chartBitmap.chartId} - extras: ${Utils.dumpBundle(extras)}") + if(dataSource == NotifySource.GRAPH_CHANGED && extras?.getInt(Constants.GRAPH_ID) == chartBitmap.chartId) { + GlobalScope.launch(Dispatchers.Main) { + try { + Log.i(LOG_ID, "Update bitmap") + imageView.setImageBitmap(chartBitmap.getBitmap()) + } catch (exc: Exception) { + Log.e(LOG_ID, "Update bitmap exception: " + exc.toString()) + } + } + } + } +} \ No newline at end of file diff --git a/common/src/main/java/de/michelinside/glucodatahandler/common/chart/ChartCreator.kt b/common/src/main/java/de/michelinside/glucodatahandler/common/chart/ChartCreator.kt index 5b0a009ae..030c1b6ed 100644 --- a/common/src/main/java/de/michelinside/glucodatahandler/common/chart/ChartCreator.kt +++ b/common/src/main/java/de/michelinside/glucodatahandler/common/chart/ChartCreator.kt @@ -33,11 +33,12 @@ import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancelAndJoin import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking +import java.math.RoundingMode import java.util.concurrent.TimeUnit import kotlin.math.abs -open class ChartCreator(protected val chart: GlucoseChart, protected val context: Context): NotifierInterface, +open class ChartCreator(protected val chart: GlucoseChart, protected val context: Context, protected val durationPref: String = ""): NotifierInterface, SharedPreferences.OnSharedPreferenceChangeListener { private var LOG_ID = "GDH.Chart.Creator" protected var created = false @@ -45,9 +46,10 @@ open class ChartCreator(protected val chart: GlucoseChart, protected val context protected var init = false protected var hasTimeNotifier = false private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default) - private var createJob: Job? = null + //private var createJob: Job? = null private var dataSyncJob: Job? = null protected open val resetChart = false + protected var durationHours = 4 private var graphPrefList = mutableSetOf( Constants.SHARED_PREF_LOW_GLUCOSE, @@ -65,6 +67,9 @@ open class ChartCreator(protected val chart: GlucoseChart, protected val context LOG_ID = "GDH.Chart.Creator." + chart.id.toString() Log.d(LOG_ID, "init") ChartUtils.init(context) + if(durationPref.isNotEmpty()) { + durationHours = sharedPref.getInt(durationPref, 4) + } sharedPref.registerOnSharedPreferenceChangeListener(this) updateNotifier() init = true @@ -114,12 +119,7 @@ open class ChartCreator(protected val chart: GlucoseChart, protected val context if(init) { InternalNotifier.remNotifier(context, this) sharedPref.unregisterOnSharedPreferenceChangeListener(this) - if(dataSyncJob != null) { - runBlocking { - Log.d(LOG_ID, "stop data sync - wait for current execution") - dataSyncJob!!.cancelAndJoin() - } - } + stopDataSync() waitForCreation() init = false } @@ -171,6 +171,7 @@ open class ChartCreator(protected val chart: GlucoseChart, protected val context chart.axisRight.setDrawGridLines(false) chart.axisRight.xOffset = -15F chart.axisRight.textColor = context.resources.getColor(R.color.text_color) + chart.axisLeft.isEnabled = showOtherUnit() if(chart.axisLeft.isEnabled) { chart.axisLeft.valueFormatter = GlucoseFormatter(true) @@ -196,7 +197,7 @@ open class ChartCreator(protected val chart: GlucoseChart, protected val context //dataSet.colors = mutableListOf() dataSet.circleColors = mutableListOf() //dataSet.lineWidth = 1F - dataSet.circleRadius = 3F + dataSet.circleRadius = 2F dataSet.setDrawValues(false) dataSet.setDrawCircleHole(false) dataSet.axisDependency = YAxis.AxisDependency.RIGHT @@ -207,6 +208,22 @@ open class ChartCreator(protected val chart: GlucoseChart, protected val context } } + protected open fun getYAxisInterval(): Float { + return 50F + } + + protected fun updateYAxisLabelCount() { + if(chart.axisRight.isDrawLabelsEnabled && getYAxisInterval() > 0F) { + val count = Utils.round(chart.axisRight.axisMaximum / getYAxisInterval(), 0, RoundingMode.DOWN).toInt() + if(count != chart.axisRight.labelCount) { + Log.v(LOG_ID, "update y-axis label count: $count for ${chart.axisRight.axisMaximum}") + chart.axisRight.setLabelCount(count) + if(chart.axisLeft.isEnabled) + chart.axisLeft.setLabelCount(count) + } + } + } + protected open fun initChart(touchEnabled: Boolean = true) { Log.v(LOG_ID, "initChart - touchEnabled: $touchEnabled") chart.setTouchEnabled(touchEnabled) @@ -219,7 +236,7 @@ open class ChartCreator(protected val chart: GlucoseChart, protected val context chart.minOffset = 0F chart.setExtraOffsets(4F, 4F, 4F, 4F) if(touchEnabled) { - val mMarker = CustomBubbleMarker(context) + val mMarker = CustomBubbleMarker(context, true) mMarker.chartView = chart chart.marker = mMarker chart.setOnChartValueSelectedListener(object: OnChartValueSelectedListener { @@ -253,13 +270,24 @@ open class ChartCreator(protected val chart: GlucoseChart, protected val context chart.axisRight.axisMaximum = ReceiveData.highRaw+10 chart.axisLeft.axisMinimum = chart.axisRight.axisMinimum chart.axisLeft.axisMaximum = chart.axisRight.axisMaximum + updateYAxisLabelCount() chart.isScaleXEnabled = false chart.invalidate() chart.waitForInvalidate() } + protected fun stopDataSync() { + if(dataSyncJob != null) { + runBlocking { + Log.d(LOG_ID, "stop data sync - wait for current execution") + dataSyncJob!!.cancelAndJoin() + } + } + } + private fun initData() { initDataSet() + stopDataSync() dataSyncJob = scope.launch { dataSync() } @@ -290,7 +318,7 @@ open class ChartCreator(protected val chart: GlucoseChart, protected val context } protected open fun getDefaultRange(): Long { - return 240L + return durationHours * 60L } protected open fun addEntries(values: List) { @@ -314,10 +342,12 @@ open class ChartCreator(protected val chart: GlucoseChart, protected val context if (chart.axisRight.axisMinimum > (entry.y - 10F)) { chart.axisRight.axisMinimum = entry.y - 10F chart.axisLeft.axisMinimum = chart.axisRight.axisMinimum + updateYAxisLabelCount() } if (chart.axisRight.axisMaximum < (entry.y + 10F)) { chart.axisRight.axisMaximum = entry.y + 10F chart.axisLeft.axisMaximum = chart.axisRight.axisMaximum + updateYAxisLabelCount() } } } @@ -388,7 +418,6 @@ open class ChartCreator(protected val chart: GlucoseChart, protected val context } protected open fun updateChart(dataSet: LineDataSet) { - val defaultRange = getDefaultRange() val right = isRight() val left = isLeft() Log.v(LOG_ID, "Min: ${chart.xAxis.axisMinimum} - visible: ${chart.lowestVisibleX} - Max: ${chart.xAxis.axisMaximum} - visible: ${chart.highestVisibleX} - isLeft: ${left} - isRight: ${right}" ) @@ -400,16 +429,22 @@ open class ChartCreator(protected val chart: GlucoseChart, protected val context chart.data = LineData(dataSet) addEmptyTimeData() chart.notifyDataSetChanged() + invalidateChart(diffTimeMin, right, left) + } + + protected open fun invalidateChart(diffTime: Long, right: Boolean, left: Boolean) { + val defaultRange = getDefaultRange() + var diffTimeMin = diffTime val newDiffTime = TimeUnit.MILLISECONDS.toMinutes( TimeValueFormatter.from_chart_x(chart.xChartMax).time - TimeValueFormatter.from_chart_x(chart.lowestVisibleX).time) Log.d(LOG_ID, "Diff-Time: ${diffTimeMin} minutes - newDiffTime: ${newDiffTime} minutes") var setXRange = false - if(!chart.isScaleXEnabled && newDiffTime >= 90) { + if(!chart.isScaleXEnabled && newDiffTime >= minOf(defaultRange, 90)) { Log.d(LOG_ID, "Enable X scale") chart.isScaleXEnabled = true setXRange = true } - if((right && left && diffTimeMin < getDefaultRange() && newDiffTime >= defaultRange)) { + if((right && left && diffTimeMin < defaultRange && newDiffTime >= defaultRange)) { Log.v(LOG_ID, "Set ${defaultRange/60} hours diff time") diffTimeMin = defaultRange } @@ -435,6 +470,10 @@ open class ChartCreator(protected val chart: GlucoseChart, protected val context } } + protected open fun durationChanged() { + invalidateChart(getDefaultRange(), true, true) + } + private fun getFirstTimestamp(): Long { if(chart.data != null && chart.data.dataSetCount > 0) { val dataSet = chart.data.getDataSetByIndex(0) as LineDataSet @@ -480,9 +519,9 @@ open class ChartCreator(protected val chart: GlucoseChart, protected val context updateChart(chart.data.getDataSetByIndex(0) as LineDataSet) } - protected fun resetData(values: List) { - Log.d(LOG_ID, "Reset data") + protected fun resetChartData() { if(chart.data != null) { + Log.d(LOG_ID, "Reset chart data") chart.highlightValue(null) val dataSet = chart.data.getDataSetByIndex(0) as LineDataSet dataSet.clear() @@ -492,6 +531,11 @@ open class ChartCreator(protected val chart: GlucoseChart, protected val context chart.data.notifyDataChanged() chart.notifyDataSetChanged() } + } + + protected fun resetData(values: List) { + Log.d(LOG_ID, "Reset data") + resetChartData() initDataSet() addEntries(values) } @@ -534,7 +578,12 @@ open class ChartCreator(protected val chart: GlucoseChart, protected val context override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) { Log.d(LOG_ID, "onSharedPreferenceChanged: $key") try { - if (graphPrefList.contains(key)) { + if(key == durationPref) { + durationHours = sharedPref.getInt(durationPref, durationHours) + resetChartData() + chart.fitScreen() + initData() + } else if (graphPrefList.contains(key)) { Log.i(LOG_ID, "re create graph after settings changed for key: $key") ReceiveData.updateSettings(sharedPref) if(chart.data != null && chart.data.getDataSetByIndex(0).entryCount > 0) diff --git a/common/src/main/java/de/michelinside/glucodatahandler/common/chart/CustomBubbleMarker.kt b/common/src/main/java/de/michelinside/glucodatahandler/common/chart/CustomBubbleMarker.kt index c324a403b..46a737a1e 100644 --- a/common/src/main/java/de/michelinside/glucodatahandler/common/chart/CustomBubbleMarker.kt +++ b/common/src/main/java/de/michelinside/glucodatahandler/common/chart/CustomBubbleMarker.kt @@ -4,6 +4,7 @@ import android.content.Context import android.graphics.Canvas import android.graphics.Paint import android.graphics.Path +import android.text.format.DateUtils import android.util.Log import android.widget.TextView import androidx.appcompat.widget.LinearLayoutCompat @@ -15,7 +16,7 @@ import com.github.mikephil.charting.utils.MPPointF import de.michelinside.glucodatahandler.common.R import java.text.DateFormat -class CustomBubbleMarker(context: Context) : MarkerView(context, R.layout.marker_layout) { +class CustomBubbleMarker(context: Context, private val showDate: Boolean = false) : MarkerView(context, R.layout.marker_layout) { private val LOG_ID = "GDH.Chart.MarkerView" private val arrowSize = 35 private val arrowCircleOffset = 0f @@ -26,15 +27,19 @@ class CustomBubbleMarker(context: Context) : MarkerView(context, R.layout.marker try { isGlucose = highlight.dataSetIndex == 0 e?.let { + val dateValue = TimeValueFormatter.from_chart_x(e.x) + val date: TextView = this.findViewById(R.id.date) + date.visibility = if(showDate && !DateUtils.isToday(dateValue.time)) VISIBLE else GONE val time: TextView = this.findViewById(R.id.time) val glucose: TextView = this.findViewById(R.id.glucose) val layout: LinearLayoutCompat = this.findViewById(R.id.marker_layout) if(isGlucose) { - time.text = DateFormat.getTimeInstance(DateFormat.DEFAULT) - .format(TimeValueFormatter.from_chart_x(e.x)) + date.text = DateFormat.getDateInstance(DateFormat.SHORT).format(dateValue) + time.text = DateFormat.getTimeInstance(DateFormat.DEFAULT).format(dateValue) glucose.text = GlucoseFormatter.getValueAsString(e.y) layout.visibility = VISIBLE } else { + date.text = "" time.text = "" glucose.text = "" layout.visibility = GONE diff --git a/common/src/main/res/layout/marker_layout.xml b/common/src/main/res/layout/marker_layout.xml index 419eebd2a..fd17bbb22 100644 --- a/common/src/main/res/layout/marker_layout.xml +++ b/common/src/main/res/layout/marker_layout.xml @@ -9,6 +9,16 @@ android:paddingHorizontal="12dp" android:paddingVertical="8dp"> + + #55000000 @color/white #AA000000 + @color/white \ No newline at end of file diff --git a/common/src/main/res/values/colors.xml b/common/src/main/res/values/colors.xml index fed988e7a..0891a61ae 100644 --- a/common/src/main/res/values/colors.xml +++ b/common/src/main/res/values/colors.xml @@ -19,4 +19,5 @@ #55000000 @color/white #77000000 + @color/black \ No newline at end of file diff --git a/mobile/src/main/java/de/michelinside/glucodatahandler/MainActivity.kt b/mobile/src/main/java/de/michelinside/glucodatahandler/MainActivity.kt index a2b1e01ee..de3ca8472 100644 --- a/mobile/src/main/java/de/michelinside/glucodatahandler/MainActivity.kt +++ b/mobile/src/main/java/de/michelinside/glucodatahandler/MainActivity.kt @@ -19,6 +19,8 @@ import android.view.View import android.view.View.OnClickListener import android.widget.Button import android.widget.ImageView +import android.widget.SeekBar +import android.widget.SeekBar.OnSeekBarChangeListener import android.widget.TableLayout import android.widget.TableRow import android.widget.TextView @@ -36,7 +38,7 @@ import de.michelinside.glucodatahandler.common.ReceiveData import de.michelinside.glucodatahandler.common.SourceState import de.michelinside.glucodatahandler.common.SourceStateData import de.michelinside.glucodatahandler.common.WearPhoneConnection -import de.michelinside.glucodatahandler.common.chart.ChartBitmap +import de.michelinside.glucodatahandler.common.chart.ChartBitmapView import de.michelinside.glucodatahandler.common.chart.ChartCreator import de.michelinside.glucodatahandler.common.chart.GlucoseChart import de.michelinside.glucodatahandler.common.notification.AlarmHandler @@ -80,6 +82,7 @@ class MainActivity : AppCompatActivity(), NotifierInterface { private lateinit var optionsMenu: Menu private lateinit var chart: GlucoseChart private lateinit var chartImage: ImageView + private var chartDuration: SeekBar? = null private var alarmIcon: MenuItem? = null private var snoozeMenu: MenuItem? = null private var floatingWidgetItem: MenuItem? = null @@ -87,7 +90,7 @@ class MainActivity : AppCompatActivity(), NotifierInterface { private var requestNotificationPermission = false private var doNotUpdate = false private lateinit var chartCreator: ChartCreator - private lateinit var chartBitmap: ChartBitmap + private lateinit var chartBitmap: ChartBitmapView override fun onCreate(savedInstanceState: Bundle?) { try { @@ -112,6 +115,7 @@ class MainActivity : AppCompatActivity(), NotifierInterface { tableNotes = findViewById(R.id.tableNotes) chart = findViewById(R.id.chart) chartImage = findViewById(R.id.graphImage) + chartDuration = findViewById(R.id.chartDuration) PreferenceManager.setDefaultValues(this, R.xml.preferences, false) sharedPref = this.getSharedPreferences(Constants.SHARED_PREF_TAG, Context.MODE_PRIVATE) @@ -132,12 +136,24 @@ class MainActivity : AppCompatActivity(), NotifierInterface { } Dialogs.updateColorScheme(this) + if(chartDuration != null) { + chartDuration!!.progress = sharedPref.getInt("test_chart_duration", 4) + chartDuration!!.setOnSeekBarChangeListener(object: OnSeekBarChangeListener { + override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) { + Log.i(LOG_ID, "Chart duration changed to $progress") + sharedPref.edit().putInt("test_chart_duration", progress).apply() + } + override fun onStartTrackingTouch(seekBar: SeekBar?) {} + override fun onStopTrackingTouch(seekBar: SeekBar?) {} + }) + } + if (requestPermission()) GlucoDataServiceMobile.start(this) TextToSpeechUtils.initTextToSpeech(this) - chartCreator = ChartCreator(chart, this) + chartCreator = ChartCreator(chart, this, "test_chart_duration") chartCreator.create() - chartBitmap = ChartBitmap(chartImage, this) + chartBitmap = ChartBitmapView(chartImage, this, "test_chart_duration") } catch (exc: Exception) { Log.e(LOG_ID, "onCreate exception: " + exc.message.toString() ) } diff --git a/mobile/src/main/res/layout/activity_main.xml b/mobile/src/main/res/layout/activity_main.xml index 63844bb02..c7f09078d 100644 --- a/mobile/src/main/res/layout/activity_main.xml +++ b/mobile/src/main/res/layout/activity_main.xml @@ -140,6 +140,13 @@ android:layout_height="200dp" /> + - + android:layout_height="100dp" /> Date: Wed, 29 Jan 2025 19:24:12 +0100 Subject: [PATCH 14/17] Chart complication --- .../common/chart/ChartBitmap.kt | 4 +- .../common/chart/ChartBitmapCreator.kt | 8 +- .../common/chart/ChartCreator.kt | 14 +- common/src/main/res/values/strings.xml | 1 + wear/src/main/AndroidManifest.xml | 17 ++ .../glucodatahandler/GlucoDataServiceWear.kt | 1 + .../complications/ChartComplication.kt | 198 ++++++++++++++++++ 7 files changed, 233 insertions(+), 10 deletions(-) create mode 100644 wear/src/main/java/de/michelinside/glucodatahandler/complications/ChartComplication.kt diff --git a/common/src/main/java/de/michelinside/glucodatahandler/common/chart/ChartBitmap.kt b/common/src/main/java/de/michelinside/glucodatahandler/common/chart/ChartBitmap.kt index b2b9d0a2f..cdd6b2e8c 100644 --- a/common/src/main/java/de/michelinside/glucodatahandler/common/chart/ChartBitmap.kt +++ b/common/src/main/java/de/michelinside/glucodatahandler/common/chart/ChartBitmap.kt @@ -5,7 +5,7 @@ import android.graphics.Bitmap import android.util.Log import android.view.View -class ChartBitmap(val context: Context, durationPref: String = "", width: Int = 1000, height: Int = 0) { +class ChartBitmap(val context: Context, durationPref: String = "", width: Int = 1000, height: Int = 0, forComplication: Boolean = false) { private val LOG_ID = "GDH.Chart.Bitmap" @@ -17,7 +17,7 @@ class ChartBitmap(val context: Context, durationPref: String = "", width: Int = chart.measure (View.MeasureSpec.makeMeasureSpec (width, View.MeasureSpec.EXACTLY), View.MeasureSpec.makeMeasureSpec (viewHeight, View.MeasureSpec.EXACTLY)) chart.layout (0, 0, chart.getMeasuredWidth(), chart.getMeasuredHeight()) - chartViewer = ChartBitmapCreator(chart, context, durationPref) + chartViewer = ChartBitmapCreator(chart, context, durationPref, forComplication) chartViewer.create() } diff --git a/common/src/main/java/de/michelinside/glucodatahandler/common/chart/ChartBitmapCreator.kt b/common/src/main/java/de/michelinside/glucodatahandler/common/chart/ChartBitmapCreator.kt index 953e1e867..17c6878db 100644 --- a/common/src/main/java/de/michelinside/glucodatahandler/common/chart/ChartBitmapCreator.kt +++ b/common/src/main/java/de/michelinside/glucodatahandler/common/chart/ChartBitmapCreator.kt @@ -11,7 +11,7 @@ import de.michelinside.glucodatahandler.common.database.dbAccess import de.michelinside.glucodatahandler.common.notifier.InternalNotifier import de.michelinside.glucodatahandler.common.notifier.NotifySource -class ChartBitmapCreator(chart: GlucoseChart, context: Context, durationPref: String = ""): ChartCreator(chart, context, durationPref) { +class ChartBitmapCreator(chart: GlucoseChart, context: Context, durationPref: String = "", val forComplication: Boolean = false): ChartCreator(chart, context, durationPref) { private val LOG_ID = "GDH.Chart.BitmapCreator" private var bitmap: Bitmap? = null override val resetChart = true @@ -45,6 +45,12 @@ class ChartBitmapCreator(chart: GlucoseChart, context: Context, durationPref: St return getDefaultRange() } + override fun getDefaultMaxValue(): Float { + if(forComplication) + return maxOf(super.getDefaultMaxValue(), 310F) + return super.getDefaultMaxValue() + } + override fun updateChart(dataSet: LineDataSet) { Log.v(LOG_ID, "Update chart for ${dataSet.values.size} entries and ${dataSet.circleColors.size} colors") if(dataSet.values.isNotEmpty()) diff --git a/common/src/main/java/de/michelinside/glucodatahandler/common/chart/ChartCreator.kt b/common/src/main/java/de/michelinside/glucodatahandler/common/chart/ChartCreator.kt index 030c1b6ed..bda310dc3 100644 --- a/common/src/main/java/de/michelinside/glucodatahandler/common/chart/ChartCreator.kt +++ b/common/src/main/java/de/michelinside/glucodatahandler/common/chart/ChartCreator.kt @@ -197,7 +197,7 @@ open class ChartCreator(protected val chart: GlucoseChart, protected val context //dataSet.colors = mutableListOf() dataSet.circleColors = mutableListOf() //dataSet.lineWidth = 1F - dataSet.circleRadius = 2F + dataSet.circleRadius = 3F dataSet.setDrawValues(false) dataSet.setDrawCircleHole(false) dataSet.axisDependency = YAxis.AxisDependency.RIGHT @@ -267,7 +267,7 @@ open class ChartCreator(protected val chart: GlucoseChart, protected val context if(ReceiveData.targetMaxRaw > 0F) chart.axisRight.addLimitLine(createLimitLine(ReceiveData.targetMaxRaw)) chart.axisRight.axisMinimum = Constants.GLUCOSE_MIN_VALUE.toFloat()-10F - chart.axisRight.axisMaximum = ReceiveData.highRaw+10 + chart.axisRight.axisMaximum = getDefaultMaxValue() chart.axisLeft.axisMinimum = chart.axisRight.axisMinimum chart.axisLeft.axisMaximum = chart.axisRight.axisMaximum updateYAxisLabelCount() @@ -276,6 +276,10 @@ open class ChartCreator(protected val chart: GlucoseChart, protected val context chart.waitForInvalidate() } + protected open fun getDefaultMaxValue() : Float { + return ReceiveData.highRaw + 10 + } + protected fun stopDataSync() { if(dataSyncJob != null) { runBlocking { @@ -421,7 +425,7 @@ open class ChartCreator(protected val chart: GlucoseChart, protected val context val right = isRight() val left = isLeft() Log.v(LOG_ID, "Min: ${chart.xAxis.axisMinimum} - visible: ${chart.lowestVisibleX} - Max: ${chart.xAxis.axisMaximum} - visible: ${chart.highestVisibleX} - isLeft: ${left} - isRight: ${right}" ) - var diffTimeMin = TimeUnit.MILLISECONDS.toMinutes(TimeValueFormatter.from_chart_x(chart.highestVisibleX).time - TimeValueFormatter.from_chart_x(chart.lowestVisibleX).time) + val diffTimeMin = TimeUnit.MILLISECONDS.toMinutes(TimeValueFormatter.from_chart_x(chart.highestVisibleX).time - TimeValueFormatter.from_chart_x(chart.lowestVisibleX).time) if(!chart.highlighted.isNullOrEmpty() && chart.highlighted[0].dataSetIndex != 0) { Log.v(LOG_ID, "Unset current highlighter") chart.highlightValue(null) @@ -470,10 +474,6 @@ open class ChartCreator(protected val chart: GlucoseChart, protected val context } } - protected open fun durationChanged() { - invalidateChart(getDefaultRange(), true, true) - } - private fun getFirstTimestamp(): Long { if(chart.data != null && chart.data.dataSetCount > 0) { val dataSet = chart.data.getDataSetByIndex(0) as LineDataSet diff --git a/common/src/main/res/values/strings.xml b/common/src/main/res/values/strings.xml index f1ab78457..304b1d555 100644 --- a/common/src/main/res/values/strings.xml +++ b/common/src/main/res/values/strings.xml @@ -770,5 +770,6 @@ Choose up to 3 snooze durations to display as buttons on the alarm notification. Show IOB/COB values Show IOB and COB values, if available. + Graph diff --git a/wear/src/main/AndroidManifest.xml b/wear/src/main/AndroidManifest.xml index 4a7e50514..30a4ae786 100644 --- a/wear/src/main/AndroidManifest.xml +++ b/wear/src/main/AndroidManifest.xml @@ -804,6 +804,23 @@ android:name="android.support.wearable.complications.UPDATE_PERIOD_SECONDS" android:value="0" /> + + + + + + + + diff --git a/wear/src/main/java/de/michelinside/glucodatahandler/GlucoDataServiceWear.kt b/wear/src/main/java/de/michelinside/glucodatahandler/GlucoDataServiceWear.kt index ee5a6789d..f6f80af69 100644 --- a/wear/src/main/java/de/michelinside/glucodatahandler/GlucoDataServiceWear.kt +++ b/wear/src/main/java/de/michelinside/glucodatahandler/GlucoDataServiceWear.kt @@ -110,6 +110,7 @@ class GlucoDataServiceWear: GlucoDataService(AppSource.WEAR_APP), NotifierInterf InternalNotifier.addNotifier(this, this, filter) updateComplicationNotifier() ActiveComplicationHandler.OnNotifyData(this, NotifySource.CAPILITY_INFO, null) + ChartComplicationUpdater.OnNotifyData(this, NotifySource.GRAPH_CHANGED, null) } catch (ex: Exception) { Log.e(LOG_ID, "onCreate exception: " + ex) } diff --git a/wear/src/main/java/de/michelinside/glucodatahandler/complications/ChartComplication.kt b/wear/src/main/java/de/michelinside/glucodatahandler/complications/ChartComplication.kt new file mode 100644 index 000000000..f8d3f7292 --- /dev/null +++ b/wear/src/main/java/de/michelinside/glucodatahandler/complications/ChartComplication.kt @@ -0,0 +1,198 @@ +package de.michelinside.glucodatahandler + +import android.annotation.SuppressLint +import android.app.PendingIntent +import android.content.ComponentName +import android.content.Context +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.Matrix +import android.graphics.Paint +import android.graphics.drawable.Icon +import android.os.Bundle +import android.util.Log +import androidx.wear.watchface.complications.data.ComplicationData +import androidx.wear.watchface.complications.data.ComplicationType +import androidx.wear.watchface.complications.data.PlainComplicationText +import androidx.wear.watchface.complications.data.SmallImage +import androidx.wear.watchface.complications.data.SmallImageComplicationData +import androidx.wear.watchface.complications.data.SmallImageType +import androidx.wear.watchface.complications.datasource.ComplicationDataSourceUpdateRequester +import androidx.wear.watchface.complications.datasource.ComplicationRequest +import androidx.wear.watchface.complications.datasource.SuspendingComplicationDataSourceService +import de.michelinside.glucodatahandler.common.GlucoDataService +import de.michelinside.glucodatahandler.common.chart.ChartBitmap +import de.michelinside.glucodatahandler.common.notifier.InternalNotifier +import de.michelinside.glucodatahandler.common.notifier.NotifierInterface +import de.michelinside.glucodatahandler.common.notifier.NotifySource +import de.michelinside.glucodatahandler.common.utils.PackageUtils + + +open class ChartComplication(): SuspendingComplicationDataSourceService() { + + companion object { + protected val LOG_ID = "GDH.Chart.Complication" + @SuppressLint("StaticFieldLeak") + private var chartBitmap: ChartBitmap? = null + private var complications = mutableSetOf() + private val size = 600 + + private fun addComplication(id: Int) { + if(!complications.contains(id)) { + Log.d(LOG_ID, "Add complication $id") + complications.add(id) + createBitmap() + } + } + + private fun remComplication(id: Int) { + if(complications.contains(id)) { + Log.d(LOG_ID, "Remove complication $id") + complications.remove(id) + if(complications.isEmpty()) + removeBitmap() + } + } + + private fun createBitmap() { + if(chartBitmap == null) { + Log.i(LOG_ID, "Create bitmap") + chartBitmap = ChartBitmap(GlucoDataService.context!!, "", size, 275, true) + InternalNotifier.addNotifier(GlucoDataService.context!!, ChartComplicationUpdater, mutableSetOf(NotifySource.GRAPH_CHANGED)) + } + } + + private fun removeBitmap() { + if(chartBitmap != null) { + Log.i(LOG_ID, "Remove bitmap") + InternalNotifier.remNotifier(GlucoDataService.context!!, ChartComplicationUpdater) + chartBitmap!!.close() + chartBitmap = null + } + } + + } + + override suspend fun onComplicationRequest(request: ComplicationRequest): ComplicationData? { + try { + Log.d( + LOG_ID, + "onComplicationRequest called for ID " + request.complicationInstanceId.toString() + " with type " + request.complicationType.toString() + ) + GlucoDataServiceWear.start(this) + addComplication(request.complicationInstanceId) + return getComplicationData(request) + } catch (exc: Exception) { + Log.e(LOG_ID, "onComplicationRequest exception: " + exc.message.toString()) + } + return null + } + + override fun onComplicationDeactivated(complicationInstanceId: Int) { + try { + Log.d(LOG_ID, "onComplicationDeactivated called for id " + complicationInstanceId) + super.onComplicationDeactivated(complicationInstanceId) + remComplication(complicationInstanceId) + } catch (exc: Exception) { + Log.e(LOG_ID, "onComplicationDeactivated exception: " + exc.message.toString()) + } + } + + override fun getPreviewData(type: ComplicationType): ComplicationData { + Log.d(LOG_ID, "onComplicationRequest called for " + type.toString()) + return getComplicationData(ComplicationRequest(0, type, false))!! + } + + private fun getComplicationData(request: ComplicationRequest): ComplicationData? { + return when (request.complicationType) { + ComplicationType.SMALL_IMAGE -> getSmallImageComplicationData(request.complicationInstanceId) + else -> { + Log.e( + LOG_ID, + "Unsupported type for char: " + request.complicationType.toString() + ) + null + } + } + } + + private fun getImage(graph: Bitmap): SmallImage { + val icon = Icon.createWithBitmap(resize(graph)) + return SmallImage.Builder( + image = icon, + type = SmallImageType.PHOTO + ).setAmbientImage(icon) + .build() + } + + private fun resize(originalImage: Bitmap): Bitmap { + val background = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888) + + val originalWidth: Float = originalImage.getWidth().toFloat() + val originalHeight: Float = originalImage.getHeight().toFloat() + + val canvas = Canvas(background) + + val scale = size / originalWidth + + val xTranslation = 0.0f + val yTranslation = (size - originalHeight * scale) / 1.9f + + Log.v(LOG_ID, "scale: $scale, xTranslation: $xTranslation, yTranslation: $yTranslation") + + val transformation = Matrix() + transformation.postTranslate(xTranslation, yTranslation) + transformation.preScale(scale, scale) + + val paint = Paint() + paint.isFilterBitmap = true + + canvas.drawBitmap(originalImage, transformation, paint) + + return background + } + + private fun getSmallImageComplicationData(complicationInstanceId: Int): ComplicationData? { + val graph = chartBitmap?.getBitmap() + if(graph == null) { + return null + } + // else + return SmallImageComplicationData.Builder ( + smallImage = getImage(graph), + contentDescription = PlainComplicationText.Builder("glucose graph").build() + ) + .setTapAction(getTapAction(complicationInstanceId)) + .build() + } + + private fun getTapAction( + complicationInstanceId: Int + ): PendingIntent { + return PackageUtils.getAppIntent( + applicationContext, + WearActivity::class.java, + complicationInstanceId + ) + } + +} + +object ChartComplicationUpdater: NotifierInterface { + private var complicationClasses = mutableListOf(ChartComplication::class.java) + val LOG_ID = "GDH.Chart.ComplicationUpdater" + override fun OnNotifyData(context: Context, dataSource: NotifySource, extras: Bundle?) { + Log.d(LOG_ID, "OnNotifyData called for source " + dataSource.toString() ) + complicationClasses.forEach { + ComplicationDataSourceUpdateRequester + .create( + context = context, + complicationDataSourceComponent = ComponentName( + context, + it + ) + ) + .requestUpdateAll() + } + } +} \ No newline at end of file From cedf8c17629b89abe9735432677ec02521de3787 Mon Sep 17 00:00:00 2001 From: pachi81 Date: Thu, 30 Jan 2025 19:39:36 +0100 Subject: [PATCH 15/17] Fix chart exceptions --- .../common/chart/ChartBitmapCreator.kt | 5 +-- .../common/chart/ChartBitmapView.kt | 3 +- .../common/chart/ChartCreator.kt | 29 +++++++------- .../common/chart/GlucoseChart.kt | 40 +++++++++++++++---- common/src/main/res/values-night/colors.xml | 1 + common/src/main/res/values/colors.xml | 1 + mobile/src/main/res/values-night/colors.xml | 19 --------- mobile/src/main/res/values/colors.xml | 20 ---------- .../complications/ChartComplication.kt | 4 +- wear/src/main/res/layout/activity_wear.xml | 2 +- wear/src/main/res/values/colors.xml | 2 + 11 files changed, 57 insertions(+), 69 deletions(-) delete mode 100644 mobile/src/main/res/values-night/colors.xml delete mode 100644 mobile/src/main/res/values/colors.xml diff --git a/common/src/main/java/de/michelinside/glucodatahandler/common/chart/ChartBitmapCreator.kt b/common/src/main/java/de/michelinside/glucodatahandler/common/chart/ChartBitmapCreator.kt index 17c6878db..ba8f8b0f5 100644 --- a/common/src/main/java/de/michelinside/glucodatahandler/common/chart/ChartBitmapCreator.kt +++ b/common/src/main/java/de/michelinside/glucodatahandler/common/chart/ChartBitmapCreator.kt @@ -59,9 +59,8 @@ class ChartBitmapCreator(chart: GlucoseChart, context: Context, durationPref: St chart.data = LineData() addEmptyTimeData() chart.notifyDataSetChanged() - chart.invalidate() - chart.waitForInvalidate() - bitmap = createBitmap() + bitmap = null // reset + chart.postInvalidate() InternalNotifier.notify(context, NotifySource.GRAPH_CHANGED, Bundle().apply { putInt(Constants.GRAPH_ID, chart.id) }) } diff --git a/common/src/main/java/de/michelinside/glucodatahandler/common/chart/ChartBitmapView.kt b/common/src/main/java/de/michelinside/glucodatahandler/common/chart/ChartBitmapView.kt index 0ae0cfe1c..2671621b6 100644 --- a/common/src/main/java/de/michelinside/glucodatahandler/common/chart/ChartBitmapView.kt +++ b/common/src/main/java/de/michelinside/glucodatahandler/common/chart/ChartBitmapView.kt @@ -3,7 +3,6 @@ package de.michelinside.glucodatahandler.common.chart import android.content.Context import android.os.Bundle import android.util.Log -import android.view.View import android.widget.ImageView import de.michelinside.glucodatahandler.common.Constants import de.michelinside.glucodatahandler.common.notifier.InternalNotifier @@ -38,7 +37,7 @@ class ChartBitmapView(val imageView: ImageView, val context: Context, durationPr if(dataSource == NotifySource.GRAPH_CHANGED && extras?.getInt(Constants.GRAPH_ID) == chartBitmap.chartId) { GlobalScope.launch(Dispatchers.Main) { try { - Log.i(LOG_ID, "Update bitmap") + Log.i(LOG_ID, "Update bitmap for id ${chartBitmap.chartId}") imageView.setImageBitmap(chartBitmap.getBitmap()) } catch (exc: Exception) { Log.e(LOG_ID, "Update bitmap exception: " + exc.toString()) diff --git a/common/src/main/java/de/michelinside/glucodatahandler/common/chart/ChartCreator.kt b/common/src/main/java/de/michelinside/glucodatahandler/common/chart/ChartCreator.kt index bda310dc3..9bb50391d 100644 --- a/common/src/main/java/de/michelinside/glucodatahandler/common/chart/ChartCreator.kt +++ b/common/src/main/java/de/michelinside/glucodatahandler/common/chart/ChartCreator.kt @@ -30,7 +30,6 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.cancelAndJoin import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import java.math.RoundingMode @@ -149,8 +148,6 @@ open class ChartCreator(protected val chart: GlucoseChart, protected val context chart.marker = null chart.notifyDataSetChanged() chart.clear() - chart.invalidate() - chart.waitForInvalidate() } protected open fun initXaxis() { @@ -185,7 +182,7 @@ open class ChartCreator(protected val chart: GlucoseChart, protected val context private fun createLimitLine(limit: Float): LimitLine { val line = LimitLine(limit) - line.lineColor = context.resources.getColor(R.color.gray) + line.lineColor = context.resources.getColor(R.color.chart_limit_line_color) return line } @@ -272,8 +269,6 @@ open class ChartCreator(protected val chart: GlucoseChart, protected val context chart.axisLeft.axisMaximum = chart.axisRight.axisMaximum updateYAxisLabelCount() chart.isScaleXEnabled = false - chart.invalidate() - chart.waitForInvalidate() } protected open fun getDefaultMaxValue() : Float { @@ -281,10 +276,13 @@ open class ChartCreator(protected val chart: GlucoseChart, protected val context } protected fun stopDataSync() { - if(dataSyncJob != null) { - runBlocking { - Log.d(LOG_ID, "stop data sync - wait for current execution") - dataSyncJob!!.cancelAndJoin() + if(dataSyncJob != null && dataSyncJob!!.isActive) { + dataSyncJob!!.cancel() + if(dataSyncJob!!.isActive) { + runBlocking { + Log.d(LOG_ID, "stop data sync - wait for current execution") + dataSyncJob!!.join() + } } } } @@ -465,7 +463,7 @@ open class ChartCreator(protected val chart: GlucoseChart, protected val context Log.v(LOG_ID, "Invalidate chart") chart.setVisibleXRangeMaximum(diffTimeMin.toFloat()) setXRange = true - chart.invalidate() + chart.postInvalidate() } if(setXRange) { @@ -505,7 +503,7 @@ open class ChartCreator(protected val chart: GlucoseChart, protected val context protected fun update(values: List) { Log.d(LOG_ID, "update called for ${values.size} value") if(values.isNotEmpty()) { - if(resetChart || values.first().timestamp != getFirstTimestamp() || (abs(values.size-getEntryCount()) > 5)) { + if(resetChart || values.first().timestamp != getFirstTimestamp() || (getEntryCount() > 0 && abs(values.size-getEntryCount()) > 5)) { resetData(values) return } @@ -522,7 +520,7 @@ open class ChartCreator(protected val chart: GlucoseChart, protected val context protected fun resetChartData() { if(chart.data != null) { Log.d(LOG_ID, "Reset chart data") - chart.highlightValue(null) + //chart.highlightValue(null) val dataSet = chart.data.getDataSetByIndex(0) as LineDataSet dataSet.clear() dataSet.setColors(0) @@ -563,8 +561,11 @@ open class ChartCreator(protected val chart: GlucoseChart, protected val context protected fun createBitmap(): Bitmap? { try { - if(chart.width > 0 && chart.height > 0) + chart.waitForInvalidate() + if(chart.width > 0 && chart.height > 0) { + Log.d(LOG_ID, "Draw bitmap") return chart.drawToBitmap() + } } catch (exc: Exception) { Log.e(LOG_ID, "getBitmap exception: " + exc.message.toString() ) } diff --git a/common/src/main/java/de/michelinside/glucodatahandler/common/chart/GlucoseChart.kt b/common/src/main/java/de/michelinside/glucodatahandler/common/chart/GlucoseChart.kt index 7b4f6aaab..bd6f6f9c5 100644 --- a/common/src/main/java/de/michelinside/glucodatahandler/common/chart/GlucoseChart.kt +++ b/common/src/main/java/de/michelinside/glucodatahandler/common/chart/GlucoseChart.kt @@ -2,13 +2,14 @@ package de.michelinside.glucodatahandler.common.chart import android.content.Context import android.graphics.Canvas +import android.os.Looper import android.util.AttributeSet import android.util.Log import android.view.View import com.github.mikephil.charting.charts.LineChart class GlucoseChart: LineChart { - private val LOG_ID = "GDH.Chart.GlucoseChart" + private var LOG_ID = "GDH.Chart.GlucoseChart" constructor(context: Context?) : super(context) @@ -20,7 +21,10 @@ class GlucoseChart: LineChart { defStyle ) - private var processing = false + private var isInvalidating = false + init { + LOG_ID = "GDH.Chart.GlucoseChart." + id.toString() + } override fun getId(): Int { if(super.getId() == View.NO_ID) { @@ -29,20 +33,30 @@ class GlucoseChart: LineChart { return super.getId() } + override fun postInvalidate() { + Log.v(LOG_ID, "postInvalidate") + isInvalidating = true + super.postInvalidate() + Log.v(LOG_ID, "postInvalidate done") + } + override fun invalidate() { - processing = true + isInvalidating = true try { + Log.v(LOG_ID, "invalidate - shown: $isShown") super.invalidate() } catch (exc: Exception) { Log.e(LOG_ID, "invalidate exception: ${exc.message}\n${exc.stackTraceToString()}") } - processing = false + isInvalidating = false } - fun waitForInvalidate() { - while(processing) { - Thread.sleep(10) + if(isInvalidating && Looper.myLooper() != Looper.getMainLooper()) { + Log.d(LOG_ID, "waitForDrawing") + while(isInvalidating) { + Thread.sleep(10) + } } } @@ -57,9 +71,19 @@ class GlucoseChart: LineChart { override fun onDraw(canvas: Canvas) { try { + Log.v(LOG_ID, "onDraw - min: ${xAxis.axisMinimum} - max: ${xAxis.axisMaximum}") + if(xAxis.axisMinimum > xAxis.axisMaximum) { + Log.w(LOG_ID, "Skip drawing as min: ${xAxis.axisMinimum} > max: ${xAxis.axisMaximum}") + return + } super.onDraw(canvas) + Log.v(LOG_ID, "drawn") } catch (exc: Exception) { - Log.e(LOG_ID, "onDraw exception: ${exc.message}\n${exc.stackTraceToString()}") + if(xAxis.axisMinimum > xAxis.axisMaximum) { // ignore this, as it is caused during reset and draw... + Log.w(LOG_ID, "onDraw exception: ${exc.message}") + } else { + Log.e(LOG_ID, "onDraw exception: ${exc.message}\n${exc.stackTraceToString()}") + } } } diff --git a/common/src/main/res/values-night/colors.xml b/common/src/main/res/values-night/colors.xml index c20c4119f..c90a1f813 100644 --- a/common/src/main/res/values-night/colors.xml +++ b/common/src/main/res/values-night/colors.xml @@ -19,4 +19,5 @@ @color/white #AA000000 @color/white + #FFCCCCCC \ No newline at end of file diff --git a/common/src/main/res/values/colors.xml b/common/src/main/res/values/colors.xml index 0891a61ae..7f99a6161 100644 --- a/common/src/main/res/values/colors.xml +++ b/common/src/main/res/values/colors.xml @@ -20,4 +20,5 @@ @color/white #77000000 @color/black + #FF444444 \ No newline at end of file diff --git a/mobile/src/main/res/values-night/colors.xml b/mobile/src/main/res/values-night/colors.xml deleted file mode 100644 index 8b6022102..000000000 --- a/mobile/src/main/res/values-night/colors.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - #FFBB86FC - #FF6200EE - #FF3700B3 - #FF03DAC5 - #FF018786 - #FF000000 - #FFFFFFFF - #FFFF0000 - #FFFFFF00 - #FF00FF00 - #FF888888 - @color/white - #FF333333 - #FF666666 - #55000000 - @color/white - \ No newline at end of file diff --git a/mobile/src/main/res/values/colors.xml b/mobile/src/main/res/values/colors.xml deleted file mode 100644 index cf85e9a46..000000000 --- a/mobile/src/main/res/values/colors.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - #FFBB86FC - #FF6200EE - #FF3700B3 - #FF03DAC5 - #FF018786 - #FF000000 - #FFFFFFFF - #FFFF0000 - #FFFFFF00 - #FFFF7700 - #FF00FF00 - #FF888888 - @color/black - #FFDDDDDD - #FFF0F0F0 - #55000000 - @color/white - \ No newline at end of file diff --git a/wear/src/main/java/de/michelinside/glucodatahandler/complications/ChartComplication.kt b/wear/src/main/java/de/michelinside/glucodatahandler/complications/ChartComplication.kt index f8d3f7292..dadaa6aac 100644 --- a/wear/src/main/java/de/michelinside/glucodatahandler/complications/ChartComplication.kt +++ b/wear/src/main/java/de/michelinside/glucodatahandler/complications/ChartComplication.kt @@ -57,7 +57,7 @@ open class ChartComplication(): SuspendingComplicationDataSourceService() { private fun createBitmap() { if(chartBitmap == null) { Log.i(LOG_ID, "Create bitmap") - chartBitmap = ChartBitmap(GlucoDataService.context!!, "", size, 275, true) + chartBitmap = ChartBitmap(GlucoDataService.context!!, "", size, 250, true) InternalNotifier.addNotifier(GlucoDataService.context!!, ChartComplicationUpdater, mutableSetOf(NotifySource.GRAPH_CHANGED)) } } @@ -136,7 +136,7 @@ open class ChartComplication(): SuspendingComplicationDataSourceService() { val scale = size / originalWidth val xTranslation = 0.0f - val yTranslation = (size - originalHeight * scale) / 1.9f + val yTranslation = (size - originalHeight * scale) / 2f Log.v(LOG_ID, "scale: $scale, xTranslation: $xTranslation, yTranslation: $yTranslation") diff --git a/wear/src/main/res/layout/activity_wear.xml b/wear/src/main/res/layout/activity_wear.xml index 773111fe7..9b2f358b6 100644 --- a/wear/src/main/res/layout/activity_wear.xml +++ b/wear/src/main/res/layout/activity_wear.xml @@ -156,7 +156,7 @@ + android:layout_height="70dp" /> #FF666666 #55000000 @color/white + @color/white + #FFCCCCCC \ No newline at end of file From cf3902e035f3364747bbaec2b71d481d88bfa514 Mon Sep 17 00:00:00 2001 From: pachi81 Date: Thu, 30 Jan 2025 20:14:26 +0100 Subject: [PATCH 16/17] Wear chart duration setting --- .../glucodatahandler/common/Constants.kt | 1 + common/src/main/res/values/strings.xml | 4 +- wear/src/main/AndroidManifest.xml | 2 +- .../glucodatahandler/WearActivity.kt | 2 +- .../complications/ChartComplication.kt | 3 +- .../settings/SettingsActivity.kt | 60 +++++++++++++++++++ .../src/main/res/layout/activity_settings.xml | 47 +++++++++++++++ 7 files changed, 115 insertions(+), 4 deletions(-) diff --git a/common/src/main/java/de/michelinside/glucodatahandler/common/Constants.kt b/common/src/main/java/de/michelinside/glucodatahandler/common/Constants.kt index 173179ef7..1a2c92877 100644 --- a/common/src/main/java/de/michelinside/glucodatahandler/common/Constants.kt +++ b/common/src/main/java/de/michelinside/glucodatahandler/common/Constants.kt @@ -288,5 +288,6 @@ object Constants { // graph const val GRAPH_ID = "graph_id" + const val SHARED_PREF_GRAPH_DURATION_WEAR_COMPLICATION = "graph_duration_wear_complication" } diff --git a/common/src/main/res/values/strings.xml b/common/src/main/res/values/strings.xml index 304b1d555..e96d50812 100644 --- a/common/src/main/res/values/strings.xml +++ b/common/src/main/res/values/strings.xml @@ -770,6 +770,8 @@ Choose up to 3 snooze durations to display as buttons on the alarm notification. Show IOB/COB values Show IOB and COB values, if available. - Graph + Graph + Graph duration + Number of hours shown in graph in complication and on main screen. diff --git a/wear/src/main/AndroidManifest.xml b/wear/src/main/AndroidManifest.xml index 30a4ae786..7f1fd3bb1 100644 --- a/wear/src/main/AndroidManifest.xml +++ b/wear/src/main/AndroidManifest.xml @@ -808,7 +808,7 @@ android:name=".ChartComplication" android:exported="true" android:icon="@drawable/icon_rate" - android:label="@string/chart" + android:label="@string/graph" android:permission="com.google.android.wearable.permission.BIND_COMPLICATION_PROVIDER"> diff --git a/wear/src/main/java/de/michelinside/glucodatahandler/WearActivity.kt b/wear/src/main/java/de/michelinside/glucodatahandler/WearActivity.kt index c3bc8f161..8c8320dc8 100644 --- a/wear/src/main/java/de/michelinside/glucodatahandler/WearActivity.kt +++ b/wear/src/main/java/de/michelinside/glucodatahandler/WearActivity.kt @@ -122,7 +122,7 @@ class WearActivity : AppCompatActivity(), NotifierInterface { val intent = Intent(this, AlarmsActivity::class.java) startActivity(intent) } - chartBitmap = ChartBitmapView(chartImage, this) + chartBitmap = ChartBitmapView(chartImage, this, Constants.SHARED_PREF_GRAPH_DURATION_WEAR_COMPLICATION) if(requestPermission()) GlucoDataServiceWear.start(this) PackageUtils.updatePackages(this) diff --git a/wear/src/main/java/de/michelinside/glucodatahandler/complications/ChartComplication.kt b/wear/src/main/java/de/michelinside/glucodatahandler/complications/ChartComplication.kt index dadaa6aac..d00d14fb4 100644 --- a/wear/src/main/java/de/michelinside/glucodatahandler/complications/ChartComplication.kt +++ b/wear/src/main/java/de/michelinside/glucodatahandler/complications/ChartComplication.kt @@ -20,6 +20,7 @@ import androidx.wear.watchface.complications.data.SmallImageType import androidx.wear.watchface.complications.datasource.ComplicationDataSourceUpdateRequester import androidx.wear.watchface.complications.datasource.ComplicationRequest import androidx.wear.watchface.complications.datasource.SuspendingComplicationDataSourceService +import de.michelinside.glucodatahandler.common.Constants import de.michelinside.glucodatahandler.common.GlucoDataService import de.michelinside.glucodatahandler.common.chart.ChartBitmap import de.michelinside.glucodatahandler.common.notifier.InternalNotifier @@ -57,7 +58,7 @@ open class ChartComplication(): SuspendingComplicationDataSourceService() { private fun createBitmap() { if(chartBitmap == null) { Log.i(LOG_ID, "Create bitmap") - chartBitmap = ChartBitmap(GlucoDataService.context!!, "", size, 250, true) + chartBitmap = ChartBitmap(GlucoDataService.context!!, Constants.SHARED_PREF_GRAPH_DURATION_WEAR_COMPLICATION, size, 250, true) InternalNotifier.addNotifier(GlucoDataService.context!!, ChartComplicationUpdater, mutableSetOf(NotifySource.GRAPH_CHANGED)) } } diff --git a/wear/src/main/java/de/michelinside/glucodatahandler/settings/SettingsActivity.kt b/wear/src/main/java/de/michelinside/glucodatahandler/settings/SettingsActivity.kt index 42b252b86..f0a281ed1 100644 --- a/wear/src/main/java/de/michelinside/glucodatahandler/settings/SettingsActivity.kt +++ b/wear/src/main/java/de/michelinside/glucodatahandler/settings/SettingsActivity.kt @@ -6,7 +6,10 @@ import android.content.SharedPreferences import android.os.Bundle import android.util.Log import android.widget.Button +import android.widget.SeekBar +import android.widget.TextView import androidx.appcompat.app.AppCompatActivity +import androidx.appcompat.widget.AppCompatSeekBar import androidx.appcompat.widget.SwitchCompat import de.michelinside.glucodatahandler.ActiveComplicationHandler import de.michelinside.glucodatahandler.GlucoDataServiceWear @@ -24,6 +27,8 @@ class SettingsActivity : AppCompatActivity() { private lateinit var switchRelativeTime: SwitchCompat private lateinit var switchBatteryLevel: SwitchCompat private lateinit var btnComplicationTapAction: Button + private lateinit var seekGraphDuration: AppCompatSeekBar + private lateinit var txtGraphDurationLevel: TextView override fun onCreate(savedInstanceState: Bundle?) { try { Log.v(LOG_ID, "onCreate called") @@ -122,6 +127,15 @@ class SettingsActivity : AppCompatActivity() { Log.e(LOG_ID, "Changing battery level exception: " + exc.message.toString() ) } } + + + seekGraphDuration = findViewById(R.id.seekGraphDuration) + txtGraphDurationLevel = findViewById(R.id.txtGraphDurationLevel) + + seekGraphDuration.progress = sharedPref.getInt(Constants.SHARED_PREF_GRAPH_DURATION_WEAR_COMPLICATION, 2) + txtGraphDurationLevel.text = getGraphDurationString() + seekGraphDuration.setOnSeekBarChangeListener(SeekBarChangeListener(Constants.SHARED_PREF_GRAPH_DURATION_WEAR_COMPLICATION, txtGraphDurationLevel)) + } catch( exc: Exception ) { Log.e(LOG_ID, exc.message + "\n" + exc.stackTraceToString()) } @@ -153,4 +167,50 @@ class SettingsActivity : AppCompatActivity() { Log.e(LOG_ID, exc.message + "\n" + exc.stackTraceToString()) } } + + + private fun getGraphDurationString(): String { + return "${seekGraphDuration.progress}h" + } + + inner class SeekBarChangeListener(val preference: String, val txtLevel: TextView) : SeekBar.OnSeekBarChangeListener { + private val LOG_ID = "GDH.Main.Settings.SeekBar" + override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) { + try { + Log.v(LOG_ID, "onProgressChanged called for $preference with progress=$progress - fromUser=$fromUser") + if(fromUser) { + txtLevel.text = getGraphDurationString() + with(sharedPref.edit()) { + putInt(preference, progress) + apply() + } + } + } catch( exc: Exception ) { + Log.e(LOG_ID, exc.message + "\n" + exc.stackTraceToString()) + } + } + + override fun onStartTrackingTouch(seekBar: SeekBar?) { + try { + Log.v(LOG_ID, "onStartTrackingTouch called") + } catch( exc: Exception ) { + Log.e(LOG_ID, exc.message + "\n" + exc.stackTraceToString()) + } + } + + override fun onStopTrackingTouch(seekBar: SeekBar?) { + try { + if(seekBar != null) { + Log.v(LOG_ID, "onStopTrackingTouch called with current progress: ${seekBar.progress}") + with(sharedPref.edit()) { + putInt(preference, seekBar.progress) + apply() + } + } + } catch( exc: Exception ) { + Log.e(LOG_ID, exc.message + "\n" + exc.stackTraceToString()) + } + } + } + } \ No newline at end of file diff --git a/wear/src/main/res/layout/activity_settings.xml b/wear/src/main/res/layout/activity_settings.xml index c7a2adbcf..02a98f4f3 100644 --- a/wear/src/main/res/layout/activity_settings.xml +++ b/wear/src/main/res/layout/activity_settings.xml @@ -81,6 +81,53 @@ android:layout_height="wrap_content" android:layout_width="match_parent" android:text="@string/wear_complication_tap_action" /> + + + + + + + + + + + + From ae13c452ea4a0ec4fd6c06029aac32e68d97250d Mon Sep 17 00:00:00 2001 From: pachi81 Date: Thu, 30 Jan 2025 20:46:04 +0100 Subject: [PATCH 17/17] Wear chart activity --- .../common/chart/ChartBitmapCreator.kt | 1 + .../common/chart/ChartCreator.kt | 10 +++- common/src/main/res/values-night/colors.xml | 2 +- wear/src/main/AndroidManifest.xml | 11 ++++ .../glucodatahandler/GraphActivity.kt | 57 +++++++++++++++++++ .../glucodatahandler/WearActivity.kt | 6 ++ .../glucodatahandler/WearChartCreator.kt | 7 ++- .../complications/ChartComplication.kt | 2 +- wear/src/main/res/layout/activity_graph.xml | 17 ++++++ wear/src/main/res/values/colors.xml | 1 + wear/src/main/res/values/styles.xml | 1 + wear/src/main/res/values/themes.xml | 7 ++- 12 files changed, 114 insertions(+), 8 deletions(-) create mode 100644 wear/src/main/java/de/michelinside/glucodatahandler/GraphActivity.kt create mode 100644 wear/src/main/res/layout/activity_graph.xml diff --git a/common/src/main/java/de/michelinside/glucodatahandler/common/chart/ChartBitmapCreator.kt b/common/src/main/java/de/michelinside/glucodatahandler/common/chart/ChartBitmapCreator.kt index ba8f8b0f5..54bc7f6d0 100644 --- a/common/src/main/java/de/michelinside/glucodatahandler/common/chart/ChartBitmapCreator.kt +++ b/common/src/main/java/de/michelinside/glucodatahandler/common/chart/ChartBitmapCreator.kt @@ -15,6 +15,7 @@ class ChartBitmapCreator(chart: GlucoseChart, context: Context, durationPref: St private val LOG_ID = "GDH.Chart.BitmapCreator" private var bitmap: Bitmap? = null override val resetChart = true + override val circleRadius = 3F override fun initXaxis() { Log.v(LOG_ID, "initXaxis") diff --git a/common/src/main/java/de/michelinside/glucodatahandler/common/chart/ChartCreator.kt b/common/src/main/java/de/michelinside/glucodatahandler/common/chart/ChartCreator.kt index 9bb50391d..298ac4a30 100644 --- a/common/src/main/java/de/michelinside/glucodatahandler/common/chart/ChartCreator.kt +++ b/common/src/main/java/de/michelinside/glucodatahandler/common/chart/ChartCreator.kt @@ -49,6 +49,8 @@ open class ChartCreator(protected val chart: GlucoseChart, protected val context private var dataSyncJob: Job? = null protected open val resetChart = false protected var durationHours = 4 + protected open val yAxisOffset = -15F + protected open val circleRadius = 2F private var graphPrefList = mutableSetOf( Constants.SHARED_PREF_LOW_GLUCOSE, @@ -166,7 +168,7 @@ open class ChartCreator(protected val chart: GlucoseChart, protected val context chart.axisRight.setDrawZeroLine(false) chart.axisRight.setDrawAxisLine(false) chart.axisRight.setDrawGridLines(false) - chart.axisRight.xOffset = -15F + chart.axisRight.xOffset = yAxisOffset chart.axisRight.textColor = context.resources.getColor(R.color.text_color) chart.axisLeft.isEnabled = showOtherUnit() @@ -175,7 +177,7 @@ open class ChartCreator(protected val chart: GlucoseChart, protected val context chart.axisLeft.setDrawZeroLine(false) chart.axisLeft.setDrawAxisLine(false) chart.axisLeft.setDrawGridLines(false) - chart.axisLeft.xOffset = -15F + chart.axisLeft.xOffset = yAxisOffset chart.axisLeft.textColor = context.resources.getColor(R.color.text_color) } } @@ -194,7 +196,7 @@ open class ChartCreator(protected val chart: GlucoseChart, protected val context //dataSet.colors = mutableListOf() dataSet.circleColors = mutableListOf() //dataSet.lineWidth = 1F - dataSet.circleRadius = 3F + dataSet.circleRadius = circleRadius dataSet.setDrawValues(false) dataSet.setDrawCircleHole(false) dataSet.axisDependency = YAxis.AxisDependency.RIGHT @@ -269,6 +271,8 @@ open class ChartCreator(protected val chart: GlucoseChart, protected val context chart.axisLeft.axisMaximum = chart.axisRight.axisMaximum updateYAxisLabelCount() chart.isScaleXEnabled = false + chart.postInvalidate() + chart.waitForInvalidate() } protected open fun getDefaultMaxValue() : Float { diff --git a/common/src/main/res/values-night/colors.xml b/common/src/main/res/values-night/colors.xml index c90a1f813..dee375197 100644 --- a/common/src/main/res/values-night/colors.xml +++ b/common/src/main/res/values-night/colors.xml @@ -17,7 +17,7 @@ #FF666666 #55000000 @color/white - #AA000000 + #BB000000 @color/white #FFCCCCCC \ No newline at end of file diff --git a/wear/src/main/AndroidManifest.xml b/wear/src/main/AndroidManifest.xml index 7f1fd3bb1..c84f7ea34 100644 --- a/wear/src/main/AndroidManifest.xml +++ b/wear/src/main/AndroidManifest.xml @@ -50,6 +50,17 @@ + + + + + + + diff --git a/wear/src/main/res/values/colors.xml b/wear/src/main/res/values/colors.xml index 3b9f342b0..c05bb3c4e 100644 --- a/wear/src/main/res/values/colors.xml +++ b/wear/src/main/res/values/colors.xml @@ -16,6 +16,7 @@ #FF666666 #55000000 @color/white + #BB000000 @color/white #FFCCCCCC \ No newline at end of file diff --git a/wear/src/main/res/values/styles.xml b/wear/src/main/res/values/styles.xml index a29ffa5fc..b2f9a03aa 100644 --- a/wear/src/main/res/values/styles.xml +++ b/wear/src/main/res/values/styles.xml @@ -12,4 +12,5 @@ @style/Theme.GlucoDataHandler + \ No newline at end of file diff --git a/wear/src/main/res/values/themes.xml b/wear/src/main/res/values/themes.xml index 9b703da84..42ad8ddf9 100644 --- a/wear/src/main/res/values/themes.xml +++ b/wear/src/main/res/values/themes.xml @@ -1,4 +1,4 @@ - + + + + \ No newline at end of file