Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions packages/health/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ In the Health Connect permissions activity there is a link to your privacy polic

### Android Permissions

Starting from API level 28 (Android 9.0) acessing some fitness data (e.g. Steps) requires a special permission.
Starting from API level 28 (Android 9.0) accessing some fitness data (e.g. Steps) requires a special permission.

To set it add the following line to your `AndroidManifest.xml` file.

Expand All @@ -160,7 +160,7 @@ To set it add the following line to your `AndroidManifest.xml` file.

#### Health Connect

If using Health Connect on Android it requires speciel permissions in the `AndroidManifest.xml` file.
If using Health Connect on Android it requires special permissions in the `AndroidManifest.xml` file.
The permissions can be found here: https://developer.android.com/guide/health-and-fitness/health-connect/data-and-data-types/data-types

Example shown here (can also be found in the example app):
Expand All @@ -181,7 +181,7 @@ Additionally, for Workouts: If the distance of a workout is requested then the l

There's a `debug`, `main` and `profile` version which are chosen depending on how you start your app. In general, it's sufficient to add permission only to the `main` version.

Beacuse this is labled as a `dangerous` protection level, the permission system will not grant it automaticlly and it requires the user's action.
Because this is labeled as a `dangerous` protection level, the permission system will not grant it automaticlly and it requires the user's action.
You can prompt the user for it using the [permission_handler](https://pub.dev/packages/permission_handler) plugin.
Follow the plugin setup instructions and add the following line before requsting the data:

Expand All @@ -192,7 +192,9 @@ await Permission.location.request();

### Android 14

This plugin uses the new `registerForActivityResult` when requesting permissions from Health Connect. In order for that to work, the Main app's activity should extend `FlutterFragmentActivity` instead of `FlutterActivity`. This adjustment allows casting from `Activity` to `ComponentActivity` for accessing `registerForActivityResult`.
This plugin uses the new `registerForActivityResult` when requesting permissions from Health Connect.
In order for that to work, the Main app's activity should extend `FlutterFragmentActivity` instead of `FlutterActivity`.
This adjustment allows casting from `Activity` to `ComponentActivity` for accessing `registerForActivityResult`.

In your MainActivity.kt file, update the `MainActivity` class so that it extends `FlutterFragmentActivity` instead of the default `FlutterActivity`:

Expand Down
6 changes: 4 additions & 2 deletions packages/health/android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@ group 'cachet.plugins.health'
version '1.2'

buildscript {
ext.kotlin_version = '1.8.0'
ext.kotlin_version = '1.7.22'
repositories {
google()
mavenCentral()
}

dependencies {
classpath 'com.android.tools.build:gradle:7.4.0'
classpath 'com.android.tools.build:gradle:7.4.2'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
Expand All @@ -27,6 +27,8 @@ apply plugin: 'kotlin-android'
android {
compileSdkVersion 34

namespace "cachet.plugins.health"

sourceSets {
main.java.srcDirs += 'src/main/kotlin'
}
Expand Down
3 changes: 1 addition & 2 deletions packages/health/android/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="cachet.plugins.health">
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
</manifest>
Original file line number Diff line number Diff line change
Expand Up @@ -456,6 +456,9 @@ class HealthPlugin(private var channel: MethodChannel? = null) :
SLEEP_ASLEEP -> DataType.TYPE_SLEEP_SEGMENT
SLEEP_AWAKE -> DataType.TYPE_SLEEP_SEGMENT
SLEEP_IN_BED -> DataType.TYPE_SLEEP_SEGMENT
SLEEP_LIGHT -> DataType.TYPE_SLEEP_SEGMENT
SLEEP_REM -> DataType.TYPE_SLEEP_SEGMENT
SLEEP_DEEP -> DataType.TYPE_SLEEP_SEGMENT
WORKOUT -> DataType.TYPE_ACTIVITY_SEGMENT
else -> throw IllegalArgumentException("Unsupported dataType: $type")
}
Expand All @@ -480,6 +483,9 @@ class HealthPlugin(private var channel: MethodChannel? = null) :
SLEEP_ASLEEP -> Field.FIELD_SLEEP_SEGMENT_TYPE
SLEEP_AWAKE -> Field.FIELD_SLEEP_SEGMENT_TYPE
SLEEP_IN_BED -> Field.FIELD_SLEEP_SEGMENT_TYPE
SLEEP_LIGHT -> Field.FIELD_SLEEP_SEGMENT_TYPE
SLEEP_REM -> Field.FIELD_SLEEP_SEGMENT_TYPE
SLEEP_DEEP -> Field.FIELD_SLEEP_SEGMENT_TYPE
WORKOUT -> Field.FIELD_ACTIVITY
else -> throw IllegalArgumentException("Unsupported dataType: $type")
}
Expand Down Expand Up @@ -1641,12 +1647,25 @@ class HealthPlugin(private var channel: MethodChannel? = null) :
)
}
// Filter sleep stages for requested stage
} else if (classType == SleepStageRecord::class) {
}
else if (classType == SleepSessionRecord::class) {
for (rec in response.records) {
if (rec is SleepStageRecord) {
if (dataType == MapSleepStageToType[rec.stage]) {
if (rec is SleepSessionRecord) {
if (dataType == SLEEP_SESSION) {
healthConnectData.addAll(convertRecord(rec, dataType))
}
else {
for (recStage in rec.stages) {
if (dataType == MapSleepStageToType[recStage.stage]) {
healthConnectData.addAll(
convertRecordStage(
recStage, dataType,
rec.metadata.dataOrigin.packageName
)
)
}
}
}
}
}
}
Expand All @@ -1660,6 +1679,20 @@ class HealthPlugin(private var channel: MethodChannel? = null) :
}
}

fun convertRecordStage(stage: SleepSessionRecord.Stage, dataType: String, sourceName: String):
List<Map<String, Any>> {
return listOf(
mapOf<String, Any>(
"stage" to stage.stage,
"value" to ChronoUnit.MINUTES.between(stage.startTime, stage.endTime),
"date_from" to stage.startTime.toEpochMilli(),
"date_to" to stage.endTime.toEpochMilli(),
"source_id" to "",
"source_name" to sourceName,
),
);
}

// TODO: Find alternative to SOURCE_ID or make it nullable?
fun convertRecord(record: Any, dataType: String): List<Map<String, Any>> {
val metadata = (record as Record).metadata
Expand Down Expand Up @@ -1781,16 +1814,6 @@ class HealthPlugin(private var channel: MethodChannel? = null) :
"source_name" to metadata.dataOrigin.packageName,
),
)
is SleepStageRecord -> return listOf(
mapOf<String, Any>(
"stage" to record.stage,
"value" to ChronoUnit.MINUTES.between(record.startTime, record.endTime),
"date_from" to record.startTime.toEpochMilli(),
"date_to" to record.endTime.toEpochMilli(),
"source_id" to "",
"source_name" to metadata.dataOrigin.packageName,
),
)
is RestingHeartRateRecord -> return listOf(
mapOf<String, Any>(
"value" to record.beatsPerMinute,
Expand Down Expand Up @@ -2138,12 +2161,12 @@ class HealthPlugin(private var channel: MethodChannel? = null) :
BLOOD_GLUCOSE to BloodGlucoseRecord::class,
DISTANCE_DELTA to DistanceRecord::class,
WATER to HydrationRecord::class,
SLEEP_ASLEEP to SleepStageRecord::class,
SLEEP_AWAKE to SleepStageRecord::class,
SLEEP_LIGHT to SleepStageRecord::class,
SLEEP_DEEP to SleepStageRecord::class,
SLEEP_REM to SleepStageRecord::class,
SLEEP_OUT_OF_BED to SleepStageRecord::class,
SLEEP_ASLEEP to SleepSessionRecord::class,
SLEEP_AWAKE to SleepSessionRecord::class,
SLEEP_LIGHT to SleepSessionRecord::class,
SLEEP_DEEP to SleepSessionRecord::class,
SLEEP_REM to SleepSessionRecord::class,
SLEEP_OUT_OF_BED to SleepSessionRecord::class,
SLEEP_SESSION to SleepSessionRecord::class,
WORKOUT to ExerciseSessionRecord::class,
RESTING_HEART_RATE to RestingHeartRateRecord::class,
Expand Down
16 changes: 8 additions & 8 deletions packages/health/example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -160,14 +160,14 @@ class _HealthAppState extends State<HealthApp> {
totalDistance: 2430,
totalEnergyBurned: 400);
success &= await health.writeBloodPressure(90, 80, earlier, now);
// success &= await health.writeHealthData(
// 0.0, HealthDataType.SLEEP_REM, earlier, now);
// success &= await health.writeHealthData(
// 0.0, HealthDataType.SLEEP_ASLEEP, earlier, now);
// success &= await health.writeHealthData(
// 0.0, HealthDataType.SLEEP_AWAKE, earlier, now);
// success &= await health.writeHealthData(
// 0.0, HealthDataType.SLEEP_DEEP, earlier, now);
success &= await health.writeHealthData(
0.0, HealthDataType.SLEEP_REM, earlier, now);
success &= await health.writeHealthData(
0.0, HealthDataType.SLEEP_ASLEEP, earlier, now);
success &= await health.writeHealthData(
0.0, HealthDataType.SLEEP_AWAKE, earlier, now);
success &= await health.writeHealthData(
0.0, HealthDataType.SLEEP_DEEP, earlier, now);

// Store an Audiogram
// Uncomment these on iOS - only available on iOS
Expand Down
12 changes: 6 additions & 6 deletions packages/health/example/lib/util.dart
Original file line number Diff line number Diff line change
Expand Up @@ -68,12 +68,12 @@ const List<HealthDataType> dataTypesAndroid = [
// HealthDataType.MOVE_MINUTES, // TODO: Find alternative for Health Connect
HealthDataType.DISTANCE_DELTA,
HealthDataType.RESPIRATORY_RATE,
// HealthDataType.SLEEP_AWAKE,
// HealthDataType.SLEEP_ASLEEP,
// HealthDataType.SLEEP_LIGHT,
// HealthDataType.SLEEP_DEEP,
// HealthDataType.SLEEP_REM,
// HealthDataType.SLEEP_SESSION,
HealthDataType.SLEEP_AWAKE,
HealthDataType.SLEEP_ASLEEP,
HealthDataType.SLEEP_LIGHT,
HealthDataType.SLEEP_DEEP,
HealthDataType.SLEEP_REM,
HealthDataType.SLEEP_SESSION,
HealthDataType.WATER,
HealthDataType.WORKOUT,
HealthDataType.RESTING_HEART_RATE,
Expand Down