Skip to content

Commit

Permalink
Remove series records from Exercise detail screen; add new Records sc…
Browse files Browse the repository at this point in the history
…reen (#279)

* Add new RecordList files

* Remove unused imports

* Removed RecordList from navbar and updated screen formatting
  • Loading branch information
cy245 authored Aug 5, 2024
1 parent df496b8 commit 62c5caa
Show file tree
Hide file tree
Showing 10 changed files with 473 additions and 147 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,8 @@
*/
package com.example.healthconnectsample.data

import androidx.health.connect.client.records.HeartRateRecord
import androidx.health.connect.client.records.SpeedRecord
import androidx.health.connect.client.units.Energy
import androidx.health.connect.client.units.Length
import androidx.health.connect.client.units.Velocity
import java.time.Duration

/**
Expand All @@ -35,9 +32,4 @@ data class ExerciseSessionData(
val minHeartRate: Long? = null,
val maxHeartRate: Long? = null,
val avgHeartRate: Long? = null,
val heartRateSeries: List<HeartRateRecord> = listOf(),
val minSpeed: Velocity? = null,
val maxSpeed: Velocity? = null,
val avgSpeed: Velocity? = null,
val speedRecord: List<SpeedRecord> = listOf()
)
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ import androidx.health.connect.client.records.SpeedRecord
import androidx.health.connect.client.records.StepsRecord
import androidx.health.connect.client.records.TotalCaloriesBurnedRecord
import androidx.health.connect.client.records.WeightRecord
import androidx.health.connect.client.records.metadata.DataOrigin
import androidx.health.connect.client.request.AggregateRequest
import androidx.health.connect.client.request.ChangesTokenRequest
import androidx.health.connect.client.request.ReadRecordsRequest
Expand All @@ -44,11 +43,11 @@ import androidx.health.connect.client.time.TimeRangeFilter
import androidx.health.connect.client.units.Energy
import androidx.health.connect.client.units.Length
import androidx.health.connect.client.units.Mass
import androidx.health.connect.client.units.Velocity
import com.example.healthconnectsample.R
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import java.io.IOException
import java.io.InvalidObjectException
import java.time.Instant
import java.time.ZonedDateTime
import java.time.temporal.ChronoUnit
Expand All @@ -58,9 +57,7 @@ import kotlin.reflect.KClass
// The minimum android level that can use Health Connect
const val MIN_SUPPORTED_SDK = Build.VERSION_CODES.O_MR1

/**
* Demonstrates reading and writing from Health Connect.
*/
/** Demonstrates reading and writing from Health Connect. */
class HealthConnectManager(private val context: Context) {
private val healthConnectClient by lazy { HealthConnectClient.getOrCreate(context) }

Expand Down Expand Up @@ -178,9 +175,9 @@ class HealthConnectManager(private val context: Context) {
startZoneOffset = start.offset,
endTime = end.toInstant(),
endZoneOffset = end.offset,
energy = Energy.calories((140 + Random.nextInt(20)) * 0.01)
energy = Energy.calories(140 + (Random.nextInt(20)) * 0.01)
)
) + buildHeartRateSeries(start, end) + buildSpeedSeries(start, end)
) + buildHeartRateSeries(start, end)
)
}

Expand Down Expand Up @@ -238,11 +235,8 @@ class HealthConnectManager(private val context: Context) {
val aggregateRequest = AggregateRequest(
metrics = aggregateDataTypes,
timeRangeFilter = timeRangeFilter,
dataOriginFilter = dataOriginFilter
)
dataOriginFilter = dataOriginFilter)
val aggregateData = healthConnectClient.aggregate(aggregateRequest)
val speedData = readData<SpeedRecord>(timeRangeFilter, dataOriginFilter)
val heartRateData = readData<HeartRateRecord>(timeRangeFilter, dataOriginFilter)

return ExerciseSessionData(
uid = uid,
Expand All @@ -253,8 +247,6 @@ class HealthConnectManager(private val context: Context) {
minHeartRate = aggregateData[HeartRateRecord.BPM_MIN],
maxHeartRate = aggregateData[HeartRateRecord.BPM_MAX],
avgHeartRate = aggregateData[HeartRateRecord.BPM_AVG],
heartRateSeries = heartRateData,
speedRecord = speedData,
)
}

Expand Down Expand Up @@ -416,9 +408,7 @@ class HealthConnectManager(private val context: Context) {
emit(ChangesMessage.NoMoreChanges(nextChangesToken))
}

/**
* Creates a random sleep stage that spans the specified [start] to [end] time.
*/
/** Creates a random sleep stage that spans the specified [start] to [end] time. */
private fun generateSleepStages(
start: ZonedDateTime,
end: ZonedDateTime
Expand All @@ -432,26 +422,45 @@ class HealthConnectManager(private val context: Context) {
SleepSessionRecord.Stage(
stage = randomSleepStage(),
startTime = stageStart.toInstant(),
endTime = checkedEnd.toInstant()
)
)
endTime = checkedEnd.toInstant()))
stageStart = checkedEnd
}
return sleepStages
}

/**
* Convenience function to reuse code for reading data.
* Convenience function to fetch a time-based record and return series data based on the record.
* Record types compatible with this function must be declared in the
* [com.example.healthconnectsample.presentation.screen.recordlist.RecordType] enum.
*/
private suspend inline fun <reified T : Record> readData(
timeRangeFilter: TimeRangeFilter,
dataOriginFilter: Set<DataOrigin> = setOf()
): List<T> {
val request = ReadRecordsRequest(
recordType = T::class,
dataOriginFilter = dataOriginFilter,
timeRangeFilter = timeRangeFilter
)
suspend fun fetchSeriesRecordsFromUid(recordType: KClass<out Record>, uid: String, seriesRecordsType: KClass<out Record>): List<Record> {
val recordResponse = healthConnectClient.readRecord(recordType, uid)
// Use the start time and end time from the session, for reading raw and aggregate data.
val timeRangeFilter =
when (recordResponse.record) {
// Change to use series record instead
is ExerciseSessionRecord -> {
val record = recordResponse.record as ExerciseSessionRecord
TimeRangeFilter.between(startTime = record.startTime, endTime = record.endTime)
}
is SleepSessionRecord -> {
val record = recordResponse.record as SleepSessionRecord
TimeRangeFilter.between(startTime = record.startTime, endTime = record.endTime)
}
else -> {
throw InvalidObjectException("Record with unregistered data type returned")
}
}

// Limit the data read to just the application that wrote the session. This may or may not
// be desirable depending on the use case: In some cases, it may be useful to combine with
// data written by other apps.
val dataOriginFilter = setOf(recordResponse.record.metadata.dataOrigin)
val request =
ReadRecordsRequest(
recordType = seriesRecordsType,
dataOriginFilter = dataOriginFilter,
timeRangeFilter = timeRangeFilter)
return healthConnectClient.readRecords(request).records
}

Expand All @@ -464,48 +473,21 @@ class HealthConnectManager(private val context: Context) {
while (time.isBefore(sessionEndTime)) {
samples.add(
HeartRateRecord.Sample(
time = time.toInstant(),
beatsPerMinute = (80 + Random.nextInt(80)).toLong()
)
)
time = time.toInstant(), beatsPerMinute = (80 + Random.nextInt(80)).toLong()))
time = time.plusSeconds(30)
}
return HeartRateRecord(
startTime = sessionStartTime.toInstant(),
startZoneOffset = sessionStartTime.offset,
endTime = sessionEndTime.toInstant(),
endZoneOffset = sessionEndTime.offset,
samples = samples
)
samples = samples)
}

private fun buildSpeedSeries(
sessionStartTime: ZonedDateTime,
sessionEndTime: ZonedDateTime
) = SpeedRecord(
startTime = sessionStartTime.toInstant(),
startZoneOffset = sessionStartTime.offset,
endTime = sessionEndTime.toInstant(),
endZoneOffset = sessionEndTime.offset,
samples = listOf(
SpeedRecord.Sample(
time = sessionStartTime.toInstant(),
speed = Velocity.metersPerSecond(2.5)
),
SpeedRecord.Sample(
time = sessionStartTime.toInstant().plus(5, ChronoUnit.MINUTES),
speed = Velocity.metersPerSecond(2.7)
),
SpeedRecord.Sample(
time = sessionStartTime.toInstant().plus(10, ChronoUnit.MINUTES),
speed = Velocity.metersPerSecond(2.9)
)
)
)

// Represents the two types of messages that can be sent in a Changes flow.
sealed class ChangesMessage {
data class NoMoreChanges(val nextChangesToken: String) : ChangesMessage()

data class ChangeList(val changes: List<Change>) : ChangesMessage()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
package com.example.healthconnectsample.presentation.component

import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
Expand All @@ -35,9 +34,7 @@ fun ExerciseSessionDetailsMinMaxAvg(
maximum: String?,
average: String?
) {
Row(
modifier = Modifier.fillMaxWidth()
) {
Row {
Text(
modifier = Modifier
.weight(1f),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
import androidx.compose.material.icons.filled.Delete
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
Expand Down Expand Up @@ -72,6 +73,11 @@ fun ExerciseSessionRow(
) {
Icon(Icons.Default.Delete, stringResource(R.string.delete_button))
}
IconButton(
onClick = { onDetailsClick(uid) },
) {
Icon(Icons.AutoMirrored.Filled.KeyboardArrowRight, stringResource(R.string.details_button))
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,16 @@ import com.example.healthconnectsample.presentation.screen.inputreadings.InputRe
import com.example.healthconnectsample.presentation.screen.inputreadings.InputReadingsViewModel
import com.example.healthconnectsample.presentation.screen.inputreadings.InputReadingsViewModelFactory
import com.example.healthconnectsample.presentation.screen.privacypolicy.PrivacyPolicyScreen
import com.example.healthconnectsample.presentation.screen.recordlist.RecordType
import com.example.healthconnectsample.presentation.screen.recordlist.RecordListScreen
import com.example.healthconnectsample.presentation.screen.recordlist.RecordListScreenViewModel
import com.example.healthconnectsample.presentation.screen.recordlist.RecordListViewModelFactory
import com.example.healthconnectsample.presentation.screen.recordlist.SeriesRecordsType
import com.example.healthconnectsample.presentation.screen.sleepsession.SleepSessionScreen
import com.example.healthconnectsample.presentation.screen.sleepsession.SleepSessionViewModel
import com.example.healthconnectsample.presentation.screen.sleepsession.SleepSessionViewModelFactory
import com.example.healthconnectsample.showExceptionSnackbar
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking

/**
* Provides the navigation in the app.
Expand Down Expand Up @@ -139,6 +142,9 @@ fun HealthConnectNavigation(
permissionsGranted = permissionsGranted,
sessionMetrics = sessionMetrics,
uiState = viewModel.uiState,
onDetailsClick = { recordType, uid, seriesRecordsType ->
navController.navigate(Screen.RecordListScreen.route + "/" + recordType + "/"+ uid + "/" + seriesRecordsType)
},
onError = { exception ->
showExceptionSnackbar(scaffoldState, scope, exception)
},
Expand All @@ -149,6 +155,40 @@ fun HealthConnectNavigation(
permissionsLauncher.launch(values)}
)
}
composable(Screen.RecordListScreen.route + "/{$RECORD_TYPE}" + "/{$UID_NAV_ARGUMENT}" + "/{$SERIES_RECORDS_TYPE}") {
val uid = it.arguments?.getString(UID_NAV_ARGUMENT)!!
val recordTypeString = it.arguments?.getString(RECORD_TYPE)!!
val seriesRecordsTypeString = it.arguments?.getString(SERIES_RECORDS_TYPE)!!
val viewModel: RecordListScreenViewModel = viewModel(
factory = RecordListViewModelFactory(
uid = uid,
recordTypeString = recordTypeString,
seriesRecordsTypeString = seriesRecordsTypeString,
healthConnectManager = healthConnectManager
)
)
val permissionsGranted by viewModel.permissionsGranted
val recordList = viewModel.recordList
val permissions = viewModel.permissions
val onPermissionsResult = {viewModel.initialLoad()}
val permissionsLauncher =
rememberLauncherForActivityResult(viewModel.permissionsLauncher) {
onPermissionsResult()}
RecordListScreen(
uid = uid,
permissions = permissions,
permissionsGranted = permissionsGranted,
recordType = RecordType.valueOf(recordTypeString),
seriesRecordsType = SeriesRecordsType.valueOf(seriesRecordsTypeString),
recordList = recordList,
uiState = viewModel.uiState,
onPermissionsResult = {
viewModel.initialLoad()
},
onPermissionsLaunch = { values ->
permissionsLauncher.launch(values)}
)
}
composable(Screen.SleepSessions.route) {
val viewModel: SleepSessionViewModel = viewModel(
factory = SleepSessionViewModelFactory(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ package com.example.healthconnectsample.presentation.navigation
import com.example.healthconnectsample.R

const val UID_NAV_ARGUMENT = "uid"
const val RECORD_TYPE = "recordType"
const val SERIES_RECORDS_TYPE = "seriesRecordsType"

/**
* Represent all Screens in the app.
Expand All @@ -36,5 +38,6 @@ enum class Screen(val route: String, val titleId: Int, val hasMenuItem: Boolean
InputReadings("input_readings", R.string.input_readings),
DifferentialChanges("differential_changes", R.string.differential_changes),
PrivacyPolicy("privacy_policy", R.string.privacy_policy, false),
SettingsScreen("settings_screen", R.string.settings)
SettingsScreen("settings_screen", R.string.settings),
RecordListScreen("record_list", R.string.record_list, false),
}
Loading

0 comments on commit 62c5caa

Please sign in to comment.