Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

When I update items in BarChartView with number of entries larger then before (e.g. from 7 to 24) I get IndexOutOfBoundsException #140

Open
dautovicharis opened this issue Aug 18, 2024 · 3 comments
Assignees

Comments

@dautovicharis
Copy link
Owner

          > Please check the readme on how to use snapshot releases. If you have any questions let me know.

Thanks for the explanation. I got 2.0.0-SNAPSHOT working without any issues.

Well, some issues, but they were also present in 1.3.1. When I update items in BarChartView with number of entries larger then before (e.g. from 7 to 24) I get IndexOutOfBoundsException

Originally posted by @pacjo in #97 (comment)

@pacjo
Copy link

pacjo commented Aug 18, 2024

Ok, yeah, it should have been a separate issue.

As I edited the comment, crash appears when number of entries changes in any way (not only increases) (in the logs below going from 30 to 7).

logs
FATAL EXCEPTION: main
Process: nodomain.pacjo.healthconnect.viewer, PID: 4274
java.lang.IndexOutOfBoundsException: Index 30 out of bounds for length 30
    at jdk.internal.util.Preconditions.outOfBounds(Preconditions.java:64)
    at jdk.internal.util.Preconditions.outOfBoundsCheckIndex(Preconditions.java:70)
    at jdk.internal.util.Preconditions.checkIndex(Preconditions.java:266)
    at java.util.Objects.checkIndex(Objects.java:359)
    at java.util.ArrayList.get(ArrayList.java:434)
    at io.github.dautovicharis.charts.internal.barchart.BarChartKt.drawBars-ks2KLzI(BarChart.kt:106)
    at io.github.dautovicharis.charts.internal.barchart.BarChartKt.access$drawBars-ks2KLzI(BarChart.kt:1)
    at io.github.dautovicharis.charts.internal.barchart.BarChartKt$BarChart$4.invoke(BarChart.kt:74)
    at io.github.dautovicharis.charts.internal.barchart.BarChartKt$BarChart$4.invoke(BarChart.kt:54)
    at androidx.compose.ui.draw.DrawBackgroundModifier.draw(DrawModifier.kt:116)
    at androidx.compose.ui.node.LayoutNodeDrawScope.drawDirect-x_KDEd0$ui_release(LayoutNodeDrawScope.kt:105)
    at androidx.compose.ui.node.LayoutNodeDrawScope.draw-x_KDEd0$ui_release(LayoutNodeDrawScope.kt:86)
    at androidx.compose.ui.node.NodeCoordinator.drawContainedDrawModifiers(NodeCoordinator.kt:364)
    at androidx.compose.ui.node.NodeCoordinator.draw(NodeCoordinator.kt:353)
    at androidx.compose.ui.node.LayoutModifierNodeCoordinator.performDraw(LayoutModifierNodeCoordinator.kt:176)
    at androidx.compose.ui.node.NodeCoordinator.drawContainedDrawModifiers(NodeCoordinator.kt:361)
    at androidx.compose.ui.node.NodeCoordinator.draw(NodeCoordinator.kt:353)
    at androidx.compose.ui.node.LayoutModifierNodeCoordinator.performDraw(LayoutModifierNodeCoordinator.kt:176)
    at androidx.compose.ui.node.NodeCoordinator.drawContainedDrawModifiers(NodeCoordinator.kt:361)
    at androidx.compose.ui.node.NodeCoordinator.draw(NodeCoordinator.kt:353)
    at androidx.compose.ui.node.LayoutModifierNodeCoordinator.performDraw(LayoutModifierNodeCoordinator.kt:176)
    at androidx.compose.ui.node.NodeCoordinator.drawContainedDrawModifiers(NodeCoordinator.kt:361)
    at androidx.compose.ui.node.NodeCoordinator.draw(NodeCoordinator.kt:353)
    at androidx.compose.ui.node.LayoutNode.draw$ui_release(LayoutNode.kt:926)
    at androidx.compose.ui.node.InnerNodeCoordinator.performDraw(InnerNodeCoordinator.kt:174)
    at androidx.compose.ui.node.NodeCoordinator.drawContainedDrawModifiers(NodeCoordinator.kt:361)
    at androidx.compose.ui.node.NodeCoordinator.draw(NodeCoordinator.kt:353)
    at androidx.compose.ui.node.LayoutModifierNodeCoordinator.performDraw(LayoutModifierNodeCoordinator.kt:176)
    at androidx.compose.ui.node.NodeCoordinator.drawContainedDrawModifiers(NodeCoordinator.kt:361)
    at androidx.compose.ui.node.NodeCoordinator.draw(NodeCoordinator.kt:353)
    at androidx.compose.ui.node.LayoutNode.draw$ui_release(LayoutNode.kt:926)
    at androidx.compose.ui.node.InnerNodeCoordinator.performDraw(InnerNodeCoordinator.kt:174)
    at androidx.compose.ui.node.NodeCoordinator.drawContainedDrawModifiers(NodeCoordinator.kt:361)
    at androidx.compose.ui.node.NodeCoordinator.draw(NodeCoordinator.kt:353)
    at androidx.compose.ui.node.LayoutModifierNodeCoordinator.performDraw(LayoutModifierNodeCoordinator.kt:176)
    at androidx.compose.ui.node.LayoutNodeDrawScope.drawContent(LayoutNodeDrawScope.kt:66)
    at androidx.compose.foundation.BackgroundNode.draw(Background.kt:159)
    at androidx.compose.ui.node.LayoutNodeDrawScope.drawDirect-x_KDEd0$ui_release(LayoutNodeDrawScope.kt:105)
    at androidx.compose.ui.node.LayoutNodeDrawScope.draw-x_KDEd0$ui_release(LayoutNodeDrawScope.kt:86)
    at androidx.compose.ui.node.NodeCoordinator.drawContainedDrawModifiers(NodeCoordinator.kt:364)
    at androidx.compose.ui.node.NodeCoordinator.access$drawContainedDrawModifiers(NodeCoordinator.kt:54)
    at androidx.compose.ui.node.NodeCoordinator$drawBlock$1$1.invoke(NodeCoordinator.kt:383)
    at androidx.compose.ui.node.NodeCoordinator$drawBlock$1$1.invoke(NodeCoordinator.kt:382)
    at androidx.compose.runtime.snapshots.Snapshot$Companion.observe(Snapshot.kt:2303)
    at androidx.compose.runtime.snapshots.SnapshotStateObserver$ObservedScopeMap.observe(SnapshotStateObserver.kt:500)
    at androidx.compose.runtime.snapshots.SnapshotStateObserver.observeReads(SnapshotStateObserver.kt:256)
    at androidx.compose.ui.node.OwnerSnapshotObserver.observeReads$ui_release(OwnerSnapshotObserver.kt:133)
    at androidx.compose.ui.node.NodeCoordinator$drawBlock$1.invoke(NodeCoordinator.kt:382)
    at androidx.compose.ui.node.NodeCoordinator$drawBlock$1.invoke(NodeCoordinator.kt:380)
    at androidx.compose.ui.platform.RenderNodeApi29.record(RenderNodeApi29.android.kt:209)
    at androidx.compose.ui.platform.RenderNodeLayer.updateDisplayList(RenderNodeLayer.android.kt:335)
    at androidx.compose.ui.platform.AndroidComposeView.dispatchDraw(AndroidComposeView.android.kt:1236)
    at android.view.View.draw(View.java:24630)
    at android.view.View.updateDisplayListIfDirty(View.java:23493)
    at android.view.ViewGroup.recreateChildDisplayList(ViewGroup.java:4566)
    at android.view.ViewGroup.dispatchGetDisplayList(ViewGroup.java:4539)
    at android.view.View.updateDisplayListIfDirty(View.java:23449)
    at android.view.ViewGroup.recreateChildDisplayList(ViewGroup.java:4566)
    at android.view.ViewGroup.dispatchGetDisplayList(ViewGroup.java:4539)
    at android.view.View.updateDisplayListIfDirty(View.java:23449)
    at android.view.ViewGroup.recreateChildDisplayList(ViewGroup.java:4566)
    at android.view.ViewGroup.dispatchGetDisplayList(ViewGroup.java:4539)
    at android.view.View.updateDisplayListIfDirty(View.java:23449)
    at android.view.ViewGroup.recreateChildDisplayList(ViewGroup.java:4566)
    at android.view.ViewGroup.dispatchGetDisplayList(ViewGroup.java:4539)
    at android.view.View.updateDisplayListIfDirty(View.java:23449)
    at android.view.ThreadedRenderer.updateViewTreeDisplayList(ThreadedRenderer.java:694)
    at android.view.ThreadedRenderer.updateRootDisplayList(ThreadedRenderer.java:700)
    at android.view.ThreadedRenderer.draw(ThreadedRenderer.java:798)
    at android.view.ViewRootImpl.draw(ViewRootImpl.java:5313)
    at android.view.ViewRootImpl.performDraw(ViewRootImpl.java:4975)
    at android.view.ViewRootImpl.performTraversals(ViewRootImpl.java:4093)
    at android.view.ViewRootImpl.doTraversal(ViewRootImpl.java:2718)
    at android.view.ViewRootImpl$TraversalRunnable.run(ViewRootImpl.java:9941)
    at android.view.Choreographer$CallbackRecord.run(Choreographer.java:1406)
    at android.view.Choreographer$CallbackRecord.run(Choreographer.java:1415)
    at android.view.Choreographer.doCallbacks(Choreographer.java:1015)
    at android.view.Choreographer.doFrame(Choreographer.java:945)
    at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:1389)
    at android.os.Handler.handleCallback(Handler.java:959)
    at android.os.Handler.dispatchMessage(Handler.java:100)
    at android.os.Looper.loopOnce(Looper.java:232)
    at android.os.Looper.loop(Looper.java:317)
    at android.app.ActivityThread.main(ActivityThread.java:8594)
    at java.lang.reflect.Method.invoke(Native Method)
    at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:583)
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:878)
code causing the issue (a bit of a mess since I'm just starting)
package nodomain.pacjo.healthconnect.viewer

import [...]

class MainActivity : ComponentActivity() {
    // TODO: move this out too
    private val requestPermissionActivityContract = PermissionController.createRequestPermissionResultContract()
    private val PERMISSIONS =
        setOf(
            HealthPermission.getReadPermission(StepsRecord::class),
            // TODO: add the rest
        )

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()

        // TODO: move this somewhere else
        val requestPermissions = registerForActivityResult(requestPermissionActivityContract) { granted ->
            if (granted.containsAll(PERMISSIONS)) {
                Toast.makeText(this, "Permissions granted!", Toast.LENGTH_SHORT).show()
            } else {
                Toast.makeText(this, "Missing permissions!", Toast.LENGTH_SHORT).show()
            }
        }

        suspend fun checkPermissionsAndRun(healthConnectClient: HealthConnectClient): Boolean {
            val granted = healthConnectClient.permissionController.getGrantedPermissions()

            if (!granted.containsAll(PERMISSIONS))
                requestPermissions.launch(PERMISSIONS)
            else
                return true

            return false
        }

        setContent {
            val healthConnectClient = getHealthConnectClient(LocalContext.current)
            var permissionsGranted = false

            LaunchedEffect(healthConnectClient) {
                if (healthConnectClient != null) {
                    permissionsGranted = checkPermissionsAndRun(healthConnectClient)
                }
            }

            AppTheme {
                Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
                    Column(
                        modifier = Modifier.padding(innerPadding)
                    ) {
                        Text(text = "Are permissions granted?: $permissionsGranted")

                        Graph(
                            healthConnectClient = healthConnectClient!!,     // TODO: check non-null
                            defaultPeriod = GraphPeriod.MONTH
                        )
                    }
                }
            }
        }
    }
}

/***
 * Gets [HealthConnectClient] or returns `null` if unsupported
 */
private fun getHealthConnectClient(context: Context): HealthConnectClient? {
    val providerPackageName = "com.google.android.apps.healthdata"
    val availabilityStatus = HealthConnectClient.getSdkStatus(context, providerPackageName)

    if (availabilityStatus == HealthConnectClient.SDK_UNAVAILABLE) {
        return null
    }

    if (availabilityStatus == HealthConnectClient.SDK_UNAVAILABLE_PROVIDER_UPDATE_REQUIRED) {
        // Optionally redirect to package installer to find a provider, for example:
        val uriString = "market://details?id=$providerPackageName&url=healthconnect%3A%2F%2Fonboarding"
        context.startActivity(
            Intent(Intent.ACTION_VIEW).apply {
                data = Uri.parse(uriString)
                putExtra("overlay", true)
                putExtra("callerId", context.packageName)
            }
        )
        return null
    }

    return HealthConnectClient.getOrCreate(context)
}

enum class GraphPeriod {
    DAY,
    WEEK,
    MONTH
}

@Composable
fun Graph(healthConnectClient: HealthConnectClient, defaultPeriod: GraphPeriod = GraphPeriod.WEEK) {
    var period by remember { mutableStateOf(defaultPeriod) }

    Column(
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        PeriodSelector(defaultPeriod) { selection ->
            period = selection
        }
        GenericGraph(healthConnectClient, period)
    }
}

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun PeriodSelector(startPeriod: GraphPeriod, onClick: (GraphPeriod) -> Unit) {
    val options = GraphPeriod.entries

    SingleChoiceSegmentedButtonRow {
        options.forEachIndexed { index, period ->
            SegmentedButton(
                selected = (period == startPeriod),
                onClick = { onClick(period) },      // TODO: simplify
                shape = SegmentedButtonDefaults.itemShape(index = index, count = options.size),
            ) {
                Text(
                    text = when (period) {
                        GraphPeriod.DAY -> "Day"
                        GraphPeriod.WEEK -> "Week"
                        GraphPeriod.MONTH -> "Month"
                    }
                )
            }
        }
    }
}

@Composable
fun GenericGraph(healthConnectClient: HealthConnectClient, period: GraphPeriod) {
    val data = remember { mutableListOf<Float>() }

    val endTime = Instant.now().truncatedTo(ChronoUnit.DAYS)

    val startTime = when (period) {
        GraphPeriod.DAY -> endTime
        GraphPeriod.WEEK -> endTime.minus(6, ChronoUnit.DAYS)
        GraphPeriod.MONTH -> endTime.minus(30, ChronoUnit.DAYS)     // or 31, 28, 29? i fucking hate calendars
    }

    val range = when (period) {
        GraphPeriod.DAY -> 0L..23L
        GraphPeriod.WEEK -> 0L..6L
        GraphPeriod.MONTH -> 0L..29L
    }

    val unit = when (period) {
        GraphPeriod.DAY -> ChronoUnit.HOURS
        GraphPeriod.WEEK -> ChronoUnit.DAYS
        GraphPeriod.MONTH -> ChronoUnit.DAYS
    }

    for (i in range) {
        val dayStartTime = startTime.plus(i, unit)                    // TODO: rename
        val dayEndTime = dayStartTime.plus(1, unit)       // TODO: rename

        var dataPoint: Long?
        runBlocking {
            dataPoint = readDataPoint(healthConnectClient, StepsRecord.COUNT_TOTAL, dayStartTime, dayEndTime)
        }


        if (dataPoint != null) {
            data.add(dataPoint!!.toFloat())
        } else {
            data.add(0f)
        }
    }

    BarChartView(
        dataSet = ChartDataSet(
            items = data,
            title = "Steps"
        )
    )
}

suspend fun <T : Number> readDataPoint(
    healthConnectClient: HealthConnectClient,
    aggregateMetric: AggregateMetric<T>,
    startTime: Instant,
    endTime: Instant
): T? {
    try {
        val response = healthConnectClient.aggregate(
            AggregateRequest(
                metrics = setOf(aggregateMetric),
                timeRangeFilter = TimeRangeFilter.between(startTime, endTime)
            )
        )

        return response[aggregateMetric]
    } catch (_: Exception) {
        return null
    }
}

@dautovicharis
Copy link
Owner Author

Thank you for the additional information! I'll fix this as soon as I find the time. Currently, I'm working on a new feature that will most likely be available next week.

@dautovicharis dautovicharis self-assigned this Aug 18, 2024
@pacjo
Copy link

pacjo commented Aug 18, 2024

Awesome! Take you're time.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants