diff --git a/packages/health/android/build.gradle b/packages/health/android/build.gradle index ee0b26c7f..cd5e3e2aa 100644 --- a/packages/health/android/build.gradle +++ b/packages/health/android/build.gradle @@ -3,6 +3,7 @@ version '1.2' buildscript { ext.kotlin_version = '1.9.22' + ext.objectboxVersion = "4.0.3" repositories { google() mavenCentral() @@ -11,6 +12,7 @@ buildscript { dependencies { classpath 'com.android.tools.build:gradle:8.1.4' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + classpath("io.objectbox:objectbox-gradle-plugin:$objectboxVersion") } } @@ -22,7 +24,10 @@ rootProject.allprojects { } apply plugin: 'com.android.library' -apply plugin: 'kotlin-android' +apply plugin: "kotlin-android" +apply plugin: "kotlin-kapt" +apply plugin: "io.objectbox" + android { compileSdkVersion 34 @@ -47,9 +52,16 @@ android { lintOptions { disable 'InvalidPackage' } + namespace "cachet.plugins.health" } +kapt { + arguments { + arg("objectbox.modelPath", "$projectDir/schemas/objectbox.json") + } +} + dependencies { def composeBom = platform('androidx.compose:compose-bom:2022.10.00') implementation(composeBom) diff --git a/packages/health/android/schemas/objectbox.json b/packages/health/android/schemas/objectbox.json new file mode 100644 index 000000000..4934d20cf --- /dev/null +++ b/packages/health/android/schemas/objectbox.json @@ -0,0 +1,47 @@ +{ + "_note1": "KEEP THIS FILE! Check it into a version control system (VCS) like git.", + "_note2": "ObjectBox manages crucial IDs for your object model. See docs for details.", + "_note3": "If you have VCS merge conflicts, you must resolve them according to ObjectBox docs.", + "entities": [ + { + "id": "1:594514540867167445", + "lastPropertyId": "4:7527286885730290448", + "name": "SensorStep", + "properties": [ + { + "id": "1:998565355754980530", + "name": "id", + "type": 6, + "flags": 1 + }, + { + "id": "2:874476595718356043", + "name": "startTime", + "type": 12 + }, + { + "id": "3:8917239542853691220", + "name": "endTime", + "type": 12 + }, + { + "id": "4:7527286885730290448", + "name": "count", + "type": 8 + } + ], + "relations": [] + } + ], + "lastEntityId": "1:594514540867167445", + "lastIndexId": "0:0", + "lastRelationId": "0:0", + "lastSequenceId": "0:0", + "modelVersion": 5, + "modelVersionParserMinimum": 5, + "retiredEntityUids": [], + "retiredIndexUids": [], + "retiredPropertyUids": [], + "retiredRelationUids": [], + "version": 1 +} \ No newline at end of file diff --git a/packages/health/android/src/main/AndroidManifest.xml b/packages/health/android/src/main/AndroidManifest.xml index a2f47b605..79b8699ff 100644 --- a/packages/health/android/src/main/AndroidManifest.xml +++ b/packages/health/android/src/main/AndroidManifest.xml @@ -1,2 +1,9 @@ + + + + + + + diff --git a/packages/health/android/src/main/kotlin/cachet/plugins/health/HealthPlugin.kt b/packages/health/android/src/main/kotlin/cachet/plugins/health/HealthPlugin.kt index 323c157c4..b51e83afa 100644 --- a/packages/health/android/src/main/kotlin/cachet/plugins/health/HealthPlugin.kt +++ b/packages/health/android/src/main/kotlin/cachet/plugins/health/HealthPlugin.kt @@ -1,16 +1,23 @@ package cachet.plugins.health +import android.Manifest import android.app.Activity +import android.app.ActivityManager +import android.app.Service import android.content.Context import android.content.Intent import android.content.pm.PackageManager +import android.hardware.Sensor +import android.hardware.SensorManager import android.net.Uri import android.os.Build import android.os.Handler import android.util.Log import androidx.activity.ComponentActivity import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.contract.ActivityResultContracts import androidx.annotation.NonNull +import androidx.annotation.RequiresApi import androidx.core.content.ContextCompat import androidx.health.connect.client.HealthConnectClient import androidx.health.connect.client.PermissionController @@ -64,3995 +71,4192 @@ const val MMOLL_2_MGDL = 18.0 // 1 mmoll= 18 mgdl const val MIN_SUPPORTED_SDK = Build.VERSION_CODES.O_MR1 class HealthPlugin(private var channel: MethodChannel? = null) : - MethodCallHandler, ActivityResultListener, Result, ActivityAware, FlutterPlugin { - private var mResult: Result? = null - private var handler: Handler? = null - private var activity: Activity? = null - private var context: Context? = null - private var threadPoolExecutor: ExecutorService? = null - private var useHealthConnectIfAvailable: Boolean = false - private var healthConnectRequestPermissionsLauncher: ActivityResultLauncher>? = - null - private lateinit var healthConnectClient: HealthConnectClient - private lateinit var scope: CoroutineScope - - private var BODY_FAT_PERCENTAGE = "BODY_FAT_PERCENTAGE" - private var HEIGHT = "HEIGHT" - private var WEIGHT = "WEIGHT" - private var STEPS = "STEPS" - private var AGGREGATE_STEP_COUNT = "AGGREGATE_STEP_COUNT" - private var ACTIVE_ENERGY_BURNED = "ACTIVE_ENERGY_BURNED" - private var HEART_RATE = "HEART_RATE" - private var BODY_TEMPERATURE = "BODY_TEMPERATURE" - private var BODY_WATER_MASS = "BODY_WATER_MASS" - private var BLOOD_PRESSURE_SYSTOLIC = "BLOOD_PRESSURE_SYSTOLIC" - private var BLOOD_PRESSURE_DIASTOLIC = "BLOOD_PRESSURE_DIASTOLIC" - private var BLOOD_OXYGEN = "BLOOD_OXYGEN" - private var BLOOD_GLUCOSE = "BLOOD_GLUCOSE" - private var MOVE_MINUTES = "MOVE_MINUTES" - private var DISTANCE_DELTA = "DISTANCE_DELTA" - private var WATER = "WATER" - private var RESTING_HEART_RATE = "RESTING_HEART_RATE" - private var BASAL_ENERGY_BURNED = "BASAL_ENERGY_BURNED" - private var FLIGHTS_CLIMBED = "FLIGHTS_CLIMBED" - private var RESPIRATORY_RATE = "RESPIRATORY_RATE" - - // TODO support unknown? - private var SLEEP_ASLEEP = "SLEEP_ASLEEP" - private var SLEEP_AWAKE = "SLEEP_AWAKE" - private var SLEEP_IN_BED = "SLEEP_IN_BED" - private var SLEEP_SESSION = "SLEEP_SESSION" - private var SLEEP_LIGHT = "SLEEP_LIGHT" - private var SLEEP_DEEP = "SLEEP_DEEP" - private var SLEEP_REM = "SLEEP_REM" - private var SLEEP_OUT_OF_BED = "SLEEP_OUT_OF_BED" - private var WORKOUT = "WORKOUT" - private var NUTRITION = "NUTRITION" - private var BREAKFAST = "BREAKFAST" - private var LUNCH = "LUNCH" - private var DINNER = "DINNER" - private var SNACK = "SNACK" - private var MEAL_UNKNOWN = "UNKNOWN" - - private var TOTAL_CALORIES_BURNED = "TOTAL_CALORIES_BURNED" - - val workoutTypeMap = - mapOf( - "AEROBICS" to FitnessActivities.AEROBICS, - "AMERICAN_FOOTBALL" to FitnessActivities.FOOTBALL_AMERICAN, - "ARCHERY" to FitnessActivities.ARCHERY, - "AUSTRALIAN_FOOTBALL" to - FitnessActivities.FOOTBALL_AUSTRALIAN, - "BADMINTON" to FitnessActivities.BADMINTON, - "BASEBALL" to FitnessActivities.BASEBALL, - "BASKETBALL" to FitnessActivities.BASKETBALL, - "BIATHLON" to FitnessActivities.BIATHLON, - "BIKING" to FitnessActivities.BIKING, - "BIKING_HAND" to FitnessActivities.BIKING_HAND, - "BIKING_MOUNTAIN" to FitnessActivities.BIKING_MOUNTAIN, - "BIKING_ROAD" to FitnessActivities.BIKING_ROAD, - "BIKING_SPINNING" to FitnessActivities.BIKING_SPINNING, - "BIKING_STATIONARY" to FitnessActivities.BIKING_STATIONARY, - "BIKING_UTILITY" to FitnessActivities.BIKING_UTILITY, - "BOXING" to FitnessActivities.BOXING, - "CALISTHENICS" to FitnessActivities.CALISTHENICS, - "CIRCUIT_TRAINING" to FitnessActivities.CIRCUIT_TRAINING, - "CRICKET" to FitnessActivities.CRICKET, - "CROSS_COUNTRY_SKIING" to - FitnessActivities.SKIING_CROSS_COUNTRY, - "CROSS_FIT" to FitnessActivities.CROSSFIT, - "CURLING" to FitnessActivities.CURLING, - "DANCING" to FitnessActivities.DANCING, - "DIVING" to FitnessActivities.DIVING, - "DOWNHILL_SKIING" to FitnessActivities.SKIING_DOWNHILL, - "ELEVATOR" to FitnessActivities.ELEVATOR, - "ELLIPTICAL" to FitnessActivities.ELLIPTICAL, - "ERGOMETER" to FitnessActivities.ERGOMETER, - "ESCALATOR" to FitnessActivities.ESCALATOR, - "FENCING" to FitnessActivities.FENCING, - "FRISBEE_DISC" to FitnessActivities.FRISBEE_DISC, - "GARDENING" to FitnessActivities.GARDENING, - "GOLF" to FitnessActivities.GOLF, - "GUIDED_BREATHING" to FitnessActivities.GUIDED_BREATHING, - "GYMNASTICS" to FitnessActivities.GYMNASTICS, - "HANDBALL" to FitnessActivities.HANDBALL, - "HIGH_INTENSITY_INTERVAL_TRAINING" to - FitnessActivities - .HIGH_INTENSITY_INTERVAL_TRAINING, - "HIKING" to FitnessActivities.HIKING, - "HOCKEY" to FitnessActivities.HOCKEY, - "HORSEBACK_RIDING" to FitnessActivities.HORSEBACK_RIDING, - "HOUSEWORK" to FitnessActivities.HOUSEWORK, - "IN_VEHICLE" to FitnessActivities.IN_VEHICLE, - "ICE_SKATING" to FitnessActivities.ICE_SKATING, - "INTERVAL_TRAINING" to FitnessActivities.INTERVAL_TRAINING, - "JUMP_ROPE" to FitnessActivities.JUMP_ROPE, - "KAYAKING" to FitnessActivities.KAYAKING, - "KETTLEBELL_TRAINING" to - FitnessActivities.KETTLEBELL_TRAINING, - "KICK_SCOOTER" to FitnessActivities.KICK_SCOOTER, - "KICKBOXING" to FitnessActivities.KICKBOXING, - "KITE_SURFING" to FitnessActivities.KITESURFING, - "MARTIAL_ARTS" to FitnessActivities.MARTIAL_ARTS, - "MEDITATION" to FitnessActivities.MEDITATION, - "MIXED_MARTIAL_ARTS" to - FitnessActivities.MIXED_MARTIAL_ARTS, - "P90X" to FitnessActivities.P90X, - "PARAGLIDING" to FitnessActivities.PARAGLIDING, - "PILATES" to FitnessActivities.PILATES, - "POLO" to FitnessActivities.POLO, - "RACQUETBALL" to FitnessActivities.RACQUETBALL, - "ROCK_CLIMBING" to FitnessActivities.ROCK_CLIMBING, - "ROWING" to FitnessActivities.ROWING, - "ROWING_MACHINE" to FitnessActivities.ROWING_MACHINE, - "RUGBY" to FitnessActivities.RUGBY, - "RUNNING_JOGGING" to FitnessActivities.RUNNING_JOGGING, - "RUNNING_SAND" to FitnessActivities.RUNNING_SAND, - "RUNNING_TREADMILL" to FitnessActivities.RUNNING_TREADMILL, - "RUNNING" to FitnessActivities.RUNNING, - "SAILING" to FitnessActivities.SAILING, - "SCUBA_DIVING" to FitnessActivities.SCUBA_DIVING, - "SKATING_CROSS" to FitnessActivities.SKATING_CROSS, - "SKATING_INDOOR" to FitnessActivities.SKATING_INDOOR, - "SKATING_INLINE" to FitnessActivities.SKATING_INLINE, - "SKATING" to FitnessActivities.SKATING, - "SKIING" to FitnessActivities.SKIING, - "SKIING_BACK_COUNTRY" to - FitnessActivities.SKIING_BACK_COUNTRY, - "SKIING_KITE" to FitnessActivities.SKIING_KITE, - "SKIING_ROLLER" to FitnessActivities.SKIING_ROLLER, - "SLEDDING" to FitnessActivities.SLEDDING, - "SNOWBOARDING" to FitnessActivities.SNOWBOARDING, - "SNOWMOBILE" to FitnessActivities.SNOWMOBILE, - "SNOWSHOEING" to FitnessActivities.SNOWSHOEING, - "SOCCER" to FitnessActivities.FOOTBALL_SOCCER, - "SOFTBALL" to FitnessActivities.SOFTBALL, - "SQUASH" to FitnessActivities.SQUASH, - "STAIR_CLIMBING_MACHINE" to - FitnessActivities.STAIR_CLIMBING_MACHINE, - "STAIR_CLIMBING" to FitnessActivities.STAIR_CLIMBING, - "STANDUP_PADDLEBOARDING" to - FitnessActivities.STANDUP_PADDLEBOARDING, - "STILL" to FitnessActivities.STILL, - "STRENGTH_TRAINING" to FitnessActivities.STRENGTH_TRAINING, - "SURFING" to FitnessActivities.SURFING, - "SWIMMING_OPEN_WATER" to - FitnessActivities.SWIMMING_OPEN_WATER, - "SWIMMING_POOL" to FitnessActivities.SWIMMING_POOL, - "SWIMMING" to FitnessActivities.SWIMMING, - "TABLE_TENNIS" to FitnessActivities.TABLE_TENNIS, - "TEAM_SPORTS" to FitnessActivities.TEAM_SPORTS, - "TENNIS" to FitnessActivities.TENNIS, - "TILTING" to FitnessActivities.TILTING, - "VOLLEYBALL_BEACH" to FitnessActivities.VOLLEYBALL_BEACH, - "VOLLEYBALL_INDOOR" to FitnessActivities.VOLLEYBALL_INDOOR, - "VOLLEYBALL" to FitnessActivities.VOLLEYBALL, - "WAKEBOARDING" to FitnessActivities.WAKEBOARDING, - "WALKING_FITNESS" to FitnessActivities.WALKING_FITNESS, - "WALKING_PACED" to FitnessActivities.WALKING_PACED, - "WALKING_NORDIC" to FitnessActivities.WALKING_NORDIC, - "WALKING_STROLLER" to FitnessActivities.WALKING_STROLLER, - "WALKING_TREADMILL" to FitnessActivities.WALKING_TREADMILL, - "WALKING" to FitnessActivities.WALKING, - "WATER_POLO" to FitnessActivities.WATER_POLO, - "WEIGHTLIFTING" to FitnessActivities.WEIGHTLIFTING, - "WHEELCHAIR" to FitnessActivities.WHEELCHAIR, - "WINDSURFING" to FitnessActivities.WINDSURFING, - "YOGA" to FitnessActivities.YOGA, - "ZUMBA" to FitnessActivities.ZUMBA, - "OTHER" to FitnessActivities.OTHER, - ) + MethodCallHandler, ActivityResultListener, Result, ActivityAware, FlutterPlugin { + private var mResult: Result? = null + private var handler: Handler? = null + private var activity: Activity? = null + private var context: Context? = null + private var threadPoolExecutor: ExecutorService? = null + private var useHealthConnectIfAvailable: Boolean = false + private var healthConnectRequestPermissionsLauncher: ActivityResultLauncher>? = + null + private var activityRecognitionPermissionLauncher: ActivityResultLauncher>? = + null + private lateinit var healthConnectClient: HealthConnectClient + private lateinit var scope: CoroutineScope + + private var BODY_FAT_PERCENTAGE = "BODY_FAT_PERCENTAGE" + private var HEIGHT = "HEIGHT" + private var WEIGHT = "WEIGHT" + private var STEPS = "STEPS" + private var AGGREGATE_STEP_COUNT = "AGGREGATE_STEP_COUNT" + private var ACTIVE_ENERGY_BURNED = "ACTIVE_ENERGY_BURNED" + private var HEART_RATE = "HEART_RATE" + private var BODY_TEMPERATURE = "BODY_TEMPERATURE" + private var BODY_WATER_MASS = "BODY_WATER_MASS" + private var BLOOD_PRESSURE_SYSTOLIC = "BLOOD_PRESSURE_SYSTOLIC" + private var BLOOD_PRESSURE_DIASTOLIC = "BLOOD_PRESSURE_DIASTOLIC" + private var BLOOD_OXYGEN = "BLOOD_OXYGEN" + private var BLOOD_GLUCOSE = "BLOOD_GLUCOSE" + private var MOVE_MINUTES = "MOVE_MINUTES" + private var DISTANCE_DELTA = "DISTANCE_DELTA" + private var WATER = "WATER" + private var RESTING_HEART_RATE = "RESTING_HEART_RATE" + private var BASAL_ENERGY_BURNED = "BASAL_ENERGY_BURNED" + private var FLIGHTS_CLIMBED = "FLIGHTS_CLIMBED" + private var RESPIRATORY_RATE = "RESPIRATORY_RATE" + + // TODO support unknown? + private var SLEEP_ASLEEP = "SLEEP_ASLEEP" + private var SLEEP_AWAKE = "SLEEP_AWAKE" + private var SLEEP_IN_BED = "SLEEP_IN_BED" + private var SLEEP_SESSION = "SLEEP_SESSION" + private var SLEEP_LIGHT = "SLEEP_LIGHT" + private var SLEEP_DEEP = "SLEEP_DEEP" + private var SLEEP_REM = "SLEEP_REM" + private var SLEEP_OUT_OF_BED = "SLEEP_OUT_OF_BED" + private var WORKOUT = "WORKOUT" + private var NUTRITION = "NUTRITION" + private var BREAKFAST = "BREAKFAST" + private var LUNCH = "LUNCH" + private var DINNER = "DINNER" + private var SNACK = "SNACK" + private var MEAL_UNKNOWN = "UNKNOWN" + + private var TOTAL_CALORIES_BURNED = "TOTAL_CALORIES_BURNED" + + val workoutTypeMap = + mapOf( + "AEROBICS" to FitnessActivities.AEROBICS, + "AMERICAN_FOOTBALL" to FitnessActivities.FOOTBALL_AMERICAN, + "ARCHERY" to FitnessActivities.ARCHERY, + "AUSTRALIAN_FOOTBALL" to + FitnessActivities.FOOTBALL_AUSTRALIAN, + "BADMINTON" to FitnessActivities.BADMINTON, + "BASEBALL" to FitnessActivities.BASEBALL, + "BASKETBALL" to FitnessActivities.BASKETBALL, + "BIATHLON" to FitnessActivities.BIATHLON, + "BIKING" to FitnessActivities.BIKING, + "BIKING_HAND" to FitnessActivities.BIKING_HAND, + "BIKING_MOUNTAIN" to FitnessActivities.BIKING_MOUNTAIN, + "BIKING_ROAD" to FitnessActivities.BIKING_ROAD, + "BIKING_SPINNING" to FitnessActivities.BIKING_SPINNING, + "BIKING_STATIONARY" to FitnessActivities.BIKING_STATIONARY, + "BIKING_UTILITY" to FitnessActivities.BIKING_UTILITY, + "BOXING" to FitnessActivities.BOXING, + "CALISTHENICS" to FitnessActivities.CALISTHENICS, + "CIRCUIT_TRAINING" to FitnessActivities.CIRCUIT_TRAINING, + "CRICKET" to FitnessActivities.CRICKET, + "CROSS_COUNTRY_SKIING" to + FitnessActivities.SKIING_CROSS_COUNTRY, + "CROSS_FIT" to FitnessActivities.CROSSFIT, + "CURLING" to FitnessActivities.CURLING, + "DANCING" to FitnessActivities.DANCING, + "DIVING" to FitnessActivities.DIVING, + "DOWNHILL_SKIING" to FitnessActivities.SKIING_DOWNHILL, + "ELEVATOR" to FitnessActivities.ELEVATOR, + "ELLIPTICAL" to FitnessActivities.ELLIPTICAL, + "ERGOMETER" to FitnessActivities.ERGOMETER, + "ESCALATOR" to FitnessActivities.ESCALATOR, + "FENCING" to FitnessActivities.FENCING, + "FRISBEE_DISC" to FitnessActivities.FRISBEE_DISC, + "GARDENING" to FitnessActivities.GARDENING, + "GOLF" to FitnessActivities.GOLF, + "GUIDED_BREATHING" to FitnessActivities.GUIDED_BREATHING, + "GYMNASTICS" to FitnessActivities.GYMNASTICS, + "HANDBALL" to FitnessActivities.HANDBALL, + "HIGH_INTENSITY_INTERVAL_TRAINING" to + FitnessActivities + .HIGH_INTENSITY_INTERVAL_TRAINING, + "HIKING" to FitnessActivities.HIKING, + "HOCKEY" to FitnessActivities.HOCKEY, + "HORSEBACK_RIDING" to FitnessActivities.HORSEBACK_RIDING, + "HOUSEWORK" to FitnessActivities.HOUSEWORK, + "IN_VEHICLE" to FitnessActivities.IN_VEHICLE, + "ICE_SKATING" to FitnessActivities.ICE_SKATING, + "INTERVAL_TRAINING" to FitnessActivities.INTERVAL_TRAINING, + "JUMP_ROPE" to FitnessActivities.JUMP_ROPE, + "KAYAKING" to FitnessActivities.KAYAKING, + "KETTLEBELL_TRAINING" to + FitnessActivities.KETTLEBELL_TRAINING, + "KICK_SCOOTER" to FitnessActivities.KICK_SCOOTER, + "KICKBOXING" to FitnessActivities.KICKBOXING, + "KITE_SURFING" to FitnessActivities.KITESURFING, + "MARTIAL_ARTS" to FitnessActivities.MARTIAL_ARTS, + "MEDITATION" to FitnessActivities.MEDITATION, + "MIXED_MARTIAL_ARTS" to + FitnessActivities.MIXED_MARTIAL_ARTS, + "P90X" to FitnessActivities.P90X, + "PARAGLIDING" to FitnessActivities.PARAGLIDING, + "PILATES" to FitnessActivities.PILATES, + "POLO" to FitnessActivities.POLO, + "RACQUETBALL" to FitnessActivities.RACQUETBALL, + "ROCK_CLIMBING" to FitnessActivities.ROCK_CLIMBING, + "ROWING" to FitnessActivities.ROWING, + "ROWING_MACHINE" to FitnessActivities.ROWING_MACHINE, + "RUGBY" to FitnessActivities.RUGBY, + "RUNNING_JOGGING" to FitnessActivities.RUNNING_JOGGING, + "RUNNING_SAND" to FitnessActivities.RUNNING_SAND, + "RUNNING_TREADMILL" to FitnessActivities.RUNNING_TREADMILL, + "RUNNING" to FitnessActivities.RUNNING, + "SAILING" to FitnessActivities.SAILING, + "SCUBA_DIVING" to FitnessActivities.SCUBA_DIVING, + "SKATING_CROSS" to FitnessActivities.SKATING_CROSS, + "SKATING_INDOOR" to FitnessActivities.SKATING_INDOOR, + "SKATING_INLINE" to FitnessActivities.SKATING_INLINE, + "SKATING" to FitnessActivities.SKATING, + "SKIING" to FitnessActivities.SKIING, + "SKIING_BACK_COUNTRY" to + FitnessActivities.SKIING_BACK_COUNTRY, + "SKIING_KITE" to FitnessActivities.SKIING_KITE, + "SKIING_ROLLER" to FitnessActivities.SKIING_ROLLER, + "SLEDDING" to FitnessActivities.SLEDDING, + "SNOWBOARDING" to FitnessActivities.SNOWBOARDING, + "SNOWMOBILE" to FitnessActivities.SNOWMOBILE, + "SNOWSHOEING" to FitnessActivities.SNOWSHOEING, + "SOCCER" to FitnessActivities.FOOTBALL_SOCCER, + "SOFTBALL" to FitnessActivities.SOFTBALL, + "SQUASH" to FitnessActivities.SQUASH, + "STAIR_CLIMBING_MACHINE" to + FitnessActivities.STAIR_CLIMBING_MACHINE, + "STAIR_CLIMBING" to FitnessActivities.STAIR_CLIMBING, + "STANDUP_PADDLEBOARDING" to + FitnessActivities.STANDUP_PADDLEBOARDING, + "STILL" to FitnessActivities.STILL, + "STRENGTH_TRAINING" to FitnessActivities.STRENGTH_TRAINING, + "SURFING" to FitnessActivities.SURFING, + "SWIMMING_OPEN_WATER" to + FitnessActivities.SWIMMING_OPEN_WATER, + "SWIMMING_POOL" to FitnessActivities.SWIMMING_POOL, + "SWIMMING" to FitnessActivities.SWIMMING, + "TABLE_TENNIS" to FitnessActivities.TABLE_TENNIS, + "TEAM_SPORTS" to FitnessActivities.TEAM_SPORTS, + "TENNIS" to FitnessActivities.TENNIS, + "TILTING" to FitnessActivities.TILTING, + "VOLLEYBALL_BEACH" to FitnessActivities.VOLLEYBALL_BEACH, + "VOLLEYBALL_INDOOR" to FitnessActivities.VOLLEYBALL_INDOOR, + "VOLLEYBALL" to FitnessActivities.VOLLEYBALL, + "WAKEBOARDING" to FitnessActivities.WAKEBOARDING, + "WALKING_FITNESS" to FitnessActivities.WALKING_FITNESS, + "WALKING_PACED" to FitnessActivities.WALKING_PACED, + "WALKING_NORDIC" to FitnessActivities.WALKING_NORDIC, + "WALKING_STROLLER" to FitnessActivities.WALKING_STROLLER, + "WALKING_TREADMILL" to FitnessActivities.WALKING_TREADMILL, + "WALKING" to FitnessActivities.WALKING, + "WATER_POLO" to FitnessActivities.WATER_POLO, + "WEIGHTLIFTING" to FitnessActivities.WEIGHTLIFTING, + "WHEELCHAIR" to FitnessActivities.WHEELCHAIR, + "WINDSURFING" to FitnessActivities.WINDSURFING, + "YOGA" to FitnessActivities.YOGA, + "ZUMBA" to FitnessActivities.ZUMBA, + "OTHER" to FitnessActivities.OTHER, + ) + + // TODO: Update with new workout types when Health Connect becomes the standard. + val workoutTypeMapHealthConnect = + mapOf( + // "AEROBICS" to + // ExerciseSessionRecord.EXERCISE_TYPE_AEROBICS, + "AMERICAN_FOOTBALL" to + ExerciseSessionRecord + .EXERCISE_TYPE_FOOTBALL_AMERICAN, + // "ARCHERY" to ExerciseSessionRecord.EXERCISE_TYPE_ARCHERY, + "AUSTRALIAN_FOOTBALL" to + ExerciseSessionRecord + .EXERCISE_TYPE_FOOTBALL_AUSTRALIAN, + "BADMINTON" to + ExerciseSessionRecord + .EXERCISE_TYPE_BADMINTON, + "BASEBALL" to ExerciseSessionRecord.EXERCISE_TYPE_BASEBALL, + "BASKETBALL" to + ExerciseSessionRecord + .EXERCISE_TYPE_BASKETBALL, + // "BIATHLON" to + // ExerciseSessionRecord.EXERCISE_TYPE_BIATHLON, + "BIKING" to ExerciseSessionRecord.EXERCISE_TYPE_BIKING, + // "BIKING_HAND" to + // ExerciseSessionRecord.EXERCISE_TYPE_BIKING_HAND, + // "BIKING_MOUNTAIN" to + // ExerciseSessionRecord.EXERCISE_TYPE_BIKING_MOUNTAIN, + // "BIKING_ROAD" to + // ExerciseSessionRecord.EXERCISE_TYPE_BIKING_ROAD, + // "BIKING_SPINNING" to + // ExerciseSessionRecord.EXERCISE_TYPE_BIKING_SPINNING, + // "BIKING_STATIONARY" to + // ExerciseSessionRecord.EXERCISE_TYPE_BIKING_STATIONARY, + // "BIKING_UTILITY" to + // ExerciseSessionRecord.EXERCISE_TYPE_BIKING_UTILITY, + "BOXING" to ExerciseSessionRecord.EXERCISE_TYPE_BOXING, + "CALISTHENICS" to + ExerciseSessionRecord + .EXERCISE_TYPE_CALISTHENICS, + // "CIRCUIT_TRAINING" to + // ExerciseSessionRecord.EXERCISE_TYPE_CIRCUIT_TRAINING, + "CRICKET" to ExerciseSessionRecord.EXERCISE_TYPE_CRICKET, + // "CROSS_COUNTRY_SKIING" to + // ExerciseSessionRecord.EXERCISE_TYPE_SKIING_CROSS_COUNTRY, + // "CROSS_FIT" to + // ExerciseSessionRecord.EXERCISE_TYPE_CROSSFIT, + // "CURLING" to ExerciseSessionRecord.EXERCISE_TYPE_CURLING, + "DANCING" to ExerciseSessionRecord.EXERCISE_TYPE_DANCING, + // "DIVING" to ExerciseSessionRecord.EXERCISE_TYPE_DIVING, + // "DOWNHILL_SKIING" to + // ExerciseSessionRecord.EXERCISE_TYPE_SKIING_DOWNHILL, + // "ELEVATOR" to + // ExerciseSessionRecord.EXERCISE_TYPE_ELEVATOR, + "ELLIPTICAL" to + ExerciseSessionRecord + .EXERCISE_TYPE_ELLIPTICAL, + // "ERGOMETER" to + // ExerciseSessionRecord.EXERCISE_TYPE_ERGOMETER, + // "ESCALATOR" to + // ExerciseSessionRecord.EXERCISE_TYPE_ESCALATOR, + "FENCING" to ExerciseSessionRecord.EXERCISE_TYPE_FENCING, + "FRISBEE_DISC" to + ExerciseSessionRecord + .EXERCISE_TYPE_FRISBEE_DISC, + // "GARDENING" to + // ExerciseSessionRecord.EXERCISE_TYPE_GARDENING, + "GOLF" to ExerciseSessionRecord.EXERCISE_TYPE_GOLF, + "GUIDED_BREATHING" to + ExerciseSessionRecord + .EXERCISE_TYPE_GUIDED_BREATHING, + "GYMNASTICS" to + ExerciseSessionRecord + .EXERCISE_TYPE_GYMNASTICS, + "HANDBALL" to ExerciseSessionRecord.EXERCISE_TYPE_HANDBALL, + "HIGH_INTENSITY_INTERVAL_TRAINING" to + ExerciseSessionRecord + .EXERCISE_TYPE_HIGH_INTENSITY_INTERVAL_TRAINING, + "HIKING" to ExerciseSessionRecord.EXERCISE_TYPE_HIKING, + // "HOCKEY" to ExerciseSessionRecord.EXERCISE_TYPE_HOCKEY, + // "HORSEBACK_RIDING" to + // ExerciseSessionRecord.EXERCISE_TYPE_HORSEBACK_RIDING, + // "HOUSEWORK" to + // ExerciseSessionRecord.EXERCISE_TYPE_HOUSEWORK, + // "IN_VEHICLE" to + // ExerciseSessionRecord.EXERCISE_TYPE_IN_VEHICLE, + "ICE_SKATING" to + ExerciseSessionRecord + .EXERCISE_TYPE_ICE_SKATING, + // "INTERVAL_TRAINING" to + // ExerciseSessionRecord.EXERCISE_TYPE_INTERVAL_TRAINING, + // "JUMP_ROPE" to + // ExerciseSessionRecord.EXERCISE_TYPE_JUMP_ROPE, + // "KAYAKING" to + // ExerciseSessionRecord.EXERCISE_TYPE_KAYAKING, + // "KETTLEBELL_TRAINING" to + // ExerciseSessionRecord.EXERCISE_TYPE_KETTLEBELL_TRAINING, + // "KICK_SCOOTER" to + // ExerciseSessionRecord.EXERCISE_TYPE_KICK_SCOOTER, + // "KICKBOXING" to + // ExerciseSessionRecord.EXERCISE_TYPE_KICKBOXING, + // "KITE_SURFING" to + // ExerciseSessionRecord.EXERCISE_TYPE_KITESURFING, + "MARTIAL_ARTS" to + ExerciseSessionRecord + .EXERCISE_TYPE_MARTIAL_ARTS, + // "MEDITATION" to + // ExerciseSessionRecord.EXERCISE_TYPE_MEDITATION, + // "MIXED_MARTIAL_ARTS" to + // ExerciseSessionRecord.EXERCISE_TYPE_MIXED_MARTIAL_ARTS, + // "P90X" to ExerciseSessionRecord.EXERCISE_TYPE_P90X, + "PARAGLIDING" to + ExerciseSessionRecord + .EXERCISE_TYPE_PARAGLIDING, + "PILATES" to ExerciseSessionRecord.EXERCISE_TYPE_PILATES, + // "POLO" to ExerciseSessionRecord.EXERCISE_TYPE_POLO, + "RACQUETBALL" to + ExerciseSessionRecord + .EXERCISE_TYPE_RACQUETBALL, + "ROCK_CLIMBING" to + ExerciseSessionRecord + .EXERCISE_TYPE_ROCK_CLIMBING, + "ROWING" to ExerciseSessionRecord.EXERCISE_TYPE_ROWING, + "ROWING_MACHINE" to + ExerciseSessionRecord + .EXERCISE_TYPE_ROWING_MACHINE, + "RUGBY" to ExerciseSessionRecord.EXERCISE_TYPE_RUGBY, + // "RUNNING_JOGGING" to + // ExerciseSessionRecord.EXERCISE_TYPE_RUNNING_JOGGING, + // "RUNNING_SAND" to + // ExerciseSessionRecord.EXERCISE_TYPE_RUNNING_SAND, + "RUNNING_TREADMILL" to + ExerciseSessionRecord + .EXERCISE_TYPE_RUNNING_TREADMILL, + "RUNNING" to ExerciseSessionRecord.EXERCISE_TYPE_RUNNING, + "SAILING" to ExerciseSessionRecord.EXERCISE_TYPE_SAILING, + "SCUBA_DIVING" to + ExerciseSessionRecord + .EXERCISE_TYPE_SCUBA_DIVING, + // "SKATING_CROSS" to + // ExerciseSessionRecord.EXERCISE_TYPE_SKATING_CROSS, + // "SKATING_INDOOR" to + // ExerciseSessionRecord.EXERCISE_TYPE_SKATING_INDOOR, + // "SKATING_INLINE" to + // ExerciseSessionRecord.EXERCISE_TYPE_SKATING_INLINE, + "SKATING" to ExerciseSessionRecord.EXERCISE_TYPE_SKATING, + "SKIING" to ExerciseSessionRecord.EXERCISE_TYPE_SKIING, + // "SKIING_BACK_COUNTRY" to + // ExerciseSessionRecord.EXERCISE_TYPE_SKIING_BACK_COUNTRY, + // "SKIING_KITE" to + // ExerciseSessionRecord.EXERCISE_TYPE_SKIING_KITE, + // "SKIING_ROLLER" to + // ExerciseSessionRecord.EXERCISE_TYPE_SKIING_ROLLER, + // "SLEDDING" to + // ExerciseSessionRecord.EXERCISE_TYPE_SLEDDING, + "SNOWBOARDING" to + ExerciseSessionRecord + .EXERCISE_TYPE_SNOWBOARDING, + // "SNOWMOBILE" to + // ExerciseSessionRecord.EXERCISE_TYPE_SNOWMOBILE, + "SNOWSHOEING" to + ExerciseSessionRecord + .EXERCISE_TYPE_SNOWSHOEING, + // "SOCCER" to + // ExerciseSessionRecord.EXERCISE_TYPE_FOOTBALL_SOCCER, + "SOFTBALL" to ExerciseSessionRecord.EXERCISE_TYPE_SOFTBALL, + "SQUASH" to ExerciseSessionRecord.EXERCISE_TYPE_SQUASH, + "STAIR_CLIMBING_MACHINE" to + ExerciseSessionRecord + .EXERCISE_TYPE_STAIR_CLIMBING_MACHINE, + "STAIR_CLIMBING" to + ExerciseSessionRecord + .EXERCISE_TYPE_STAIR_CLIMBING, + // "STANDUP_PADDLEBOARDING" to + // ExerciseSessionRecord.EXERCISE_TYPE_STANDUP_PADDLEBOARDING, + // "STILL" to ExerciseSessionRecord.EXERCISE_TYPE_STILL, + "STRENGTH_TRAINING" to + ExerciseSessionRecord + .EXERCISE_TYPE_STRENGTH_TRAINING, + "SURFING" to ExerciseSessionRecord.EXERCISE_TYPE_SURFING, + "SWIMMING_OPEN_WATER" to + ExerciseSessionRecord + .EXERCISE_TYPE_SWIMMING_OPEN_WATER, + "SWIMMING_POOL" to + ExerciseSessionRecord + .EXERCISE_TYPE_SWIMMING_POOL, + // "SWIMMING" to + // ExerciseSessionRecord.EXERCISE_TYPE_SWIMMING, + "TABLE_TENNIS" to + ExerciseSessionRecord + .EXERCISE_TYPE_TABLE_TENNIS, + // "TEAM_SPORTS" to + // ExerciseSessionRecord.EXERCISE_TYPE_TEAM_SPORTS, + "TENNIS" to ExerciseSessionRecord.EXERCISE_TYPE_TENNIS, + // "TILTING" to ExerciseSessionRecord.EXERCISE_TYPE_TILTING, + // "VOLLEYBALL_BEACH" to + // ExerciseSessionRecord.EXERCISE_TYPE_VOLLEYBALL_BEACH, + // "VOLLEYBALL_INDOOR" to + // ExerciseSessionRecord.EXERCISE_TYPE_VOLLEYBALL_INDOOR, + "VOLLEYBALL" to + ExerciseSessionRecord + .EXERCISE_TYPE_VOLLEYBALL, + // "WAKEBOARDING" to + // ExerciseSessionRecord.EXERCISE_TYPE_WAKEBOARDING, + // "WALKING_FITNESS" to + // ExerciseSessionRecord.EXERCISE_TYPE_WALKING_FITNESS, + // "WALKING_PACED" to + // ExerciseSessionRecord.EXERCISE_TYPE_WALKING_PACED, + // "WALKING_NORDIC" to + // ExerciseSessionRecord.EXERCISE_TYPE_WALKING_NORDIC, + // "WALKING_STROLLER" to + // ExerciseSessionRecord.EXERCISE_TYPE_WALKING_STROLLER, + // "WALKING_TREADMILL" to + // ExerciseSessionRecord.EXERCISE_TYPE_WALKING_TREADMILL, + "WALKING" to ExerciseSessionRecord.EXERCISE_TYPE_WALKING, + "WATER_POLO" to + ExerciseSessionRecord + .EXERCISE_TYPE_WATER_POLO, + "WEIGHTLIFTING" to + ExerciseSessionRecord + .EXERCISE_TYPE_WEIGHTLIFTING, + "WHEELCHAIR" to + ExerciseSessionRecord + .EXERCISE_TYPE_WHEELCHAIR, + // "WINDSURFING" to + // ExerciseSessionRecord.EXERCISE_TYPE_WINDSURFING, + "YOGA" to ExerciseSessionRecord.EXERCISE_TYPE_YOGA, + // "ZUMBA" to ExerciseSessionRecord.EXERCISE_TYPE_ZUMBA, + // "OTHER" to ExerciseSessionRecord.EXERCISE_TYPE_OTHER, + ) + + override fun onAttachedToEngine( + @NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding + ) { + scope = CoroutineScope(SupervisorJob() + Dispatchers.Main) + channel = MethodChannel(flutterPluginBinding.binaryMessenger, CHANNEL_NAME) + channel?.setMethodCallHandler(this) + context = flutterPluginBinding.applicationContext + threadPoolExecutor = Executors.newFixedThreadPool(4) + checkAvailability() + if (healthConnectAvailable) { + healthConnectClient = + HealthConnectClient.getOrCreate( + flutterPluginBinding.applicationContext + ) + } + } + + override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { + channel = null + activity = null + threadPoolExecutor!!.shutdown() + threadPoolExecutor = null + } + + // This static function is optional and equivalent to onAttachedToEngine. It supports the + // old + // pre-Flutter-1.12 Android projects. You are encouraged to continue supporting + // plugin registration via this function while apps migrate to use the new Android APIs + // post-flutter-1.12 via https://flutter.dev/go/android-project-migration. + // + // It is encouraged to share logic between onAttachedToEngine and registerWith to keep + // them functionally equivalent. Only one of onAttachedToEngine or registerWith will be + // called + // depending on the user's project. onAttachedToEngine or registerWith must both be defined + // in the same class. + companion object { + @Suppress("unused") + @JvmStatic + fun registerWith(registrar: Registrar) { + val channel = MethodChannel(registrar.messenger(), CHANNEL_NAME) + val plugin = HealthPlugin(channel) + registrar.addActivityResultListener(plugin) + channel.setMethodCallHandler(plugin) + } - // TODO: Update with new workout types when Health Connect becomes the standard. - val workoutTypeMapHealthConnect = - mapOf( - // "AEROBICS" to - // ExerciseSessionRecord.EXERCISE_TYPE_AEROBICS, - "AMERICAN_FOOTBALL" to - ExerciseSessionRecord - .EXERCISE_TYPE_FOOTBALL_AMERICAN, - // "ARCHERY" to ExerciseSessionRecord.EXERCISE_TYPE_ARCHERY, - "AUSTRALIAN_FOOTBALL" to - ExerciseSessionRecord - .EXERCISE_TYPE_FOOTBALL_AUSTRALIAN, - "BADMINTON" to - ExerciseSessionRecord - .EXERCISE_TYPE_BADMINTON, - "BASEBALL" to ExerciseSessionRecord.EXERCISE_TYPE_BASEBALL, - "BASKETBALL" to - ExerciseSessionRecord - .EXERCISE_TYPE_BASKETBALL, - // "BIATHLON" to - // ExerciseSessionRecord.EXERCISE_TYPE_BIATHLON, - "BIKING" to ExerciseSessionRecord.EXERCISE_TYPE_BIKING, - // "BIKING_HAND" to - // ExerciseSessionRecord.EXERCISE_TYPE_BIKING_HAND, - // "BIKING_MOUNTAIN" to - // ExerciseSessionRecord.EXERCISE_TYPE_BIKING_MOUNTAIN, - // "BIKING_ROAD" to - // ExerciseSessionRecord.EXERCISE_TYPE_BIKING_ROAD, - // "BIKING_SPINNING" to - // ExerciseSessionRecord.EXERCISE_TYPE_BIKING_SPINNING, - // "BIKING_STATIONARY" to - // ExerciseSessionRecord.EXERCISE_TYPE_BIKING_STATIONARY, - // "BIKING_UTILITY" to - // ExerciseSessionRecord.EXERCISE_TYPE_BIKING_UTILITY, - "BOXING" to ExerciseSessionRecord.EXERCISE_TYPE_BOXING, - "CALISTHENICS" to - ExerciseSessionRecord - .EXERCISE_TYPE_CALISTHENICS, - // "CIRCUIT_TRAINING" to - // ExerciseSessionRecord.EXERCISE_TYPE_CIRCUIT_TRAINING, - "CRICKET" to ExerciseSessionRecord.EXERCISE_TYPE_CRICKET, - // "CROSS_COUNTRY_SKIING" to - // ExerciseSessionRecord.EXERCISE_TYPE_SKIING_CROSS_COUNTRY, - // "CROSS_FIT" to - // ExerciseSessionRecord.EXERCISE_TYPE_CROSSFIT, - // "CURLING" to ExerciseSessionRecord.EXERCISE_TYPE_CURLING, - "DANCING" to ExerciseSessionRecord.EXERCISE_TYPE_DANCING, - // "DIVING" to ExerciseSessionRecord.EXERCISE_TYPE_DIVING, - // "DOWNHILL_SKIING" to - // ExerciseSessionRecord.EXERCISE_TYPE_SKIING_DOWNHILL, - // "ELEVATOR" to - // ExerciseSessionRecord.EXERCISE_TYPE_ELEVATOR, - "ELLIPTICAL" to - ExerciseSessionRecord - .EXERCISE_TYPE_ELLIPTICAL, - // "ERGOMETER" to - // ExerciseSessionRecord.EXERCISE_TYPE_ERGOMETER, - // "ESCALATOR" to - // ExerciseSessionRecord.EXERCISE_TYPE_ESCALATOR, - "FENCING" to ExerciseSessionRecord.EXERCISE_TYPE_FENCING, - "FRISBEE_DISC" to - ExerciseSessionRecord - .EXERCISE_TYPE_FRISBEE_DISC, - // "GARDENING" to - // ExerciseSessionRecord.EXERCISE_TYPE_GARDENING, - "GOLF" to ExerciseSessionRecord.EXERCISE_TYPE_GOLF, - "GUIDED_BREATHING" to - ExerciseSessionRecord - .EXERCISE_TYPE_GUIDED_BREATHING, - "GYMNASTICS" to - ExerciseSessionRecord - .EXERCISE_TYPE_GYMNASTICS, - "HANDBALL" to ExerciseSessionRecord.EXERCISE_TYPE_HANDBALL, - "HIGH_INTENSITY_INTERVAL_TRAINING" to - ExerciseSessionRecord - .EXERCISE_TYPE_HIGH_INTENSITY_INTERVAL_TRAINING, - "HIKING" to ExerciseSessionRecord.EXERCISE_TYPE_HIKING, - // "HOCKEY" to ExerciseSessionRecord.EXERCISE_TYPE_HOCKEY, - // "HORSEBACK_RIDING" to - // ExerciseSessionRecord.EXERCISE_TYPE_HORSEBACK_RIDING, - // "HOUSEWORK" to - // ExerciseSessionRecord.EXERCISE_TYPE_HOUSEWORK, - // "IN_VEHICLE" to - // ExerciseSessionRecord.EXERCISE_TYPE_IN_VEHICLE, - "ICE_SKATING" to - ExerciseSessionRecord - .EXERCISE_TYPE_ICE_SKATING, - // "INTERVAL_TRAINING" to - // ExerciseSessionRecord.EXERCISE_TYPE_INTERVAL_TRAINING, - // "JUMP_ROPE" to - // ExerciseSessionRecord.EXERCISE_TYPE_JUMP_ROPE, - // "KAYAKING" to - // ExerciseSessionRecord.EXERCISE_TYPE_KAYAKING, - // "KETTLEBELL_TRAINING" to - // ExerciseSessionRecord.EXERCISE_TYPE_KETTLEBELL_TRAINING, - // "KICK_SCOOTER" to - // ExerciseSessionRecord.EXERCISE_TYPE_KICK_SCOOTER, - // "KICKBOXING" to - // ExerciseSessionRecord.EXERCISE_TYPE_KICKBOXING, - // "KITE_SURFING" to - // ExerciseSessionRecord.EXERCISE_TYPE_KITESURFING, - "MARTIAL_ARTS" to - ExerciseSessionRecord - .EXERCISE_TYPE_MARTIAL_ARTS, - // "MEDITATION" to - // ExerciseSessionRecord.EXERCISE_TYPE_MEDITATION, - // "MIXED_MARTIAL_ARTS" to - // ExerciseSessionRecord.EXERCISE_TYPE_MIXED_MARTIAL_ARTS, - // "P90X" to ExerciseSessionRecord.EXERCISE_TYPE_P90X, - "PARAGLIDING" to - ExerciseSessionRecord - .EXERCISE_TYPE_PARAGLIDING, - "PILATES" to ExerciseSessionRecord.EXERCISE_TYPE_PILATES, - // "POLO" to ExerciseSessionRecord.EXERCISE_TYPE_POLO, - "RACQUETBALL" to - ExerciseSessionRecord - .EXERCISE_TYPE_RACQUETBALL, - "ROCK_CLIMBING" to - ExerciseSessionRecord - .EXERCISE_TYPE_ROCK_CLIMBING, - "ROWING" to ExerciseSessionRecord.EXERCISE_TYPE_ROWING, - "ROWING_MACHINE" to - ExerciseSessionRecord - .EXERCISE_TYPE_ROWING_MACHINE, - "RUGBY" to ExerciseSessionRecord.EXERCISE_TYPE_RUGBY, - // "RUNNING_JOGGING" to - // ExerciseSessionRecord.EXERCISE_TYPE_RUNNING_JOGGING, - // "RUNNING_SAND" to - // ExerciseSessionRecord.EXERCISE_TYPE_RUNNING_SAND, - "RUNNING_TREADMILL" to - ExerciseSessionRecord - .EXERCISE_TYPE_RUNNING_TREADMILL, - "RUNNING" to ExerciseSessionRecord.EXERCISE_TYPE_RUNNING, - "SAILING" to ExerciseSessionRecord.EXERCISE_TYPE_SAILING, - "SCUBA_DIVING" to - ExerciseSessionRecord - .EXERCISE_TYPE_SCUBA_DIVING, - // "SKATING_CROSS" to - // ExerciseSessionRecord.EXERCISE_TYPE_SKATING_CROSS, - // "SKATING_INDOOR" to - // ExerciseSessionRecord.EXERCISE_TYPE_SKATING_INDOOR, - // "SKATING_INLINE" to - // ExerciseSessionRecord.EXERCISE_TYPE_SKATING_INLINE, - "SKATING" to ExerciseSessionRecord.EXERCISE_TYPE_SKATING, - "SKIING" to ExerciseSessionRecord.EXERCISE_TYPE_SKIING, - // "SKIING_BACK_COUNTRY" to - // ExerciseSessionRecord.EXERCISE_TYPE_SKIING_BACK_COUNTRY, - // "SKIING_KITE" to - // ExerciseSessionRecord.EXERCISE_TYPE_SKIING_KITE, - // "SKIING_ROLLER" to - // ExerciseSessionRecord.EXERCISE_TYPE_SKIING_ROLLER, - // "SLEDDING" to - // ExerciseSessionRecord.EXERCISE_TYPE_SLEDDING, - "SNOWBOARDING" to - ExerciseSessionRecord - .EXERCISE_TYPE_SNOWBOARDING, - // "SNOWMOBILE" to - // ExerciseSessionRecord.EXERCISE_TYPE_SNOWMOBILE, - "SNOWSHOEING" to - ExerciseSessionRecord - .EXERCISE_TYPE_SNOWSHOEING, - // "SOCCER" to - // ExerciseSessionRecord.EXERCISE_TYPE_FOOTBALL_SOCCER, - "SOFTBALL" to ExerciseSessionRecord.EXERCISE_TYPE_SOFTBALL, - "SQUASH" to ExerciseSessionRecord.EXERCISE_TYPE_SQUASH, - "STAIR_CLIMBING_MACHINE" to - ExerciseSessionRecord - .EXERCISE_TYPE_STAIR_CLIMBING_MACHINE, - "STAIR_CLIMBING" to - ExerciseSessionRecord - .EXERCISE_TYPE_STAIR_CLIMBING, - // "STANDUP_PADDLEBOARDING" to - // ExerciseSessionRecord.EXERCISE_TYPE_STANDUP_PADDLEBOARDING, - // "STILL" to ExerciseSessionRecord.EXERCISE_TYPE_STILL, - "STRENGTH_TRAINING" to - ExerciseSessionRecord - .EXERCISE_TYPE_STRENGTH_TRAINING, - "SURFING" to ExerciseSessionRecord.EXERCISE_TYPE_SURFING, - "SWIMMING_OPEN_WATER" to - ExerciseSessionRecord - .EXERCISE_TYPE_SWIMMING_OPEN_WATER, - "SWIMMING_POOL" to - ExerciseSessionRecord - .EXERCISE_TYPE_SWIMMING_POOL, - // "SWIMMING" to - // ExerciseSessionRecord.EXERCISE_TYPE_SWIMMING, - "TABLE_TENNIS" to - ExerciseSessionRecord - .EXERCISE_TYPE_TABLE_TENNIS, - // "TEAM_SPORTS" to - // ExerciseSessionRecord.EXERCISE_TYPE_TEAM_SPORTS, - "TENNIS" to ExerciseSessionRecord.EXERCISE_TYPE_TENNIS, - // "TILTING" to ExerciseSessionRecord.EXERCISE_TYPE_TILTING, - // "VOLLEYBALL_BEACH" to - // ExerciseSessionRecord.EXERCISE_TYPE_VOLLEYBALL_BEACH, - // "VOLLEYBALL_INDOOR" to - // ExerciseSessionRecord.EXERCISE_TYPE_VOLLEYBALL_INDOOR, - "VOLLEYBALL" to - ExerciseSessionRecord - .EXERCISE_TYPE_VOLLEYBALL, - // "WAKEBOARDING" to - // ExerciseSessionRecord.EXERCISE_TYPE_WAKEBOARDING, - // "WALKING_FITNESS" to - // ExerciseSessionRecord.EXERCISE_TYPE_WALKING_FITNESS, - // "WALKING_PACED" to - // ExerciseSessionRecord.EXERCISE_TYPE_WALKING_PACED, - // "WALKING_NORDIC" to - // ExerciseSessionRecord.EXERCISE_TYPE_WALKING_NORDIC, - // "WALKING_STROLLER" to - // ExerciseSessionRecord.EXERCISE_TYPE_WALKING_STROLLER, - // "WALKING_TREADMILL" to - // ExerciseSessionRecord.EXERCISE_TYPE_WALKING_TREADMILL, - "WALKING" to ExerciseSessionRecord.EXERCISE_TYPE_WALKING, - "WATER_POLO" to - ExerciseSessionRecord - .EXERCISE_TYPE_WATER_POLO, - "WEIGHTLIFTING" to - ExerciseSessionRecord - .EXERCISE_TYPE_WEIGHTLIFTING, - "WHEELCHAIR" to - ExerciseSessionRecord - .EXERCISE_TYPE_WHEELCHAIR, - // "WINDSURFING" to - // ExerciseSessionRecord.EXERCISE_TYPE_WINDSURFING, - "YOGA" to ExerciseSessionRecord.EXERCISE_TYPE_YOGA, - // "ZUMBA" to ExerciseSessionRecord.EXERCISE_TYPE_ZUMBA, - // "OTHER" to ExerciseSessionRecord.EXERCISE_TYPE_OTHER, - ) + const val PERMISSIONS_REQUEST_ACTIVITY_RECOGNITION = 10 + } + + override fun success(p0: Any?) { + handler?.post { mResult?.success(p0) } + } + + override fun notImplemented() { + handler?.post { mResult?.notImplemented() } + } + + override fun error( + errorCode: String, + errorMessage: String?, + errorDetails: Any?, + ) { + handler?.post { mResult?.error(errorCode, errorMessage, errorDetails) } + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean { + if (requestCode == GOOGLE_FIT_PERMISSIONS_REQUEST_CODE) { + if (resultCode == Activity.RESULT_OK) { + Log.i("FLUTTER_HEALTH", "Access Granted!") + mResult?.success(true) + } else if (resultCode == Activity.RESULT_CANCELED) { + Log.i("FLUTTER_HEALTH", "Access Denied!") + mResult?.success(false) + } + } + return false + } + + private fun onHealthConnectPermissionCallback(permissionGranted: Set) { + if (permissionGranted.isEmpty()) { + mResult?.success(false) + Log.i("FLUTTER_HEALTH", "Access Denied (to Health Connect)!") + } else { + mResult?.success(true) + Log.i("FLUTTER_HEALTH", "Access Granted (to Health Connect)!") + } + } + + private fun keyToHealthDataType(type: String): DataType { + return when (type) { + BODY_FAT_PERCENTAGE -> DataType.TYPE_BODY_FAT_PERCENTAGE + HEIGHT -> DataType.TYPE_HEIGHT + WEIGHT -> DataType.TYPE_WEIGHT + STEPS -> DataType.TYPE_STEP_COUNT_DELTA + AGGREGATE_STEP_COUNT -> DataType.AGGREGATE_STEP_COUNT_DELTA + ACTIVE_ENERGY_BURNED -> DataType.TYPE_CALORIES_EXPENDED + HEART_RATE -> DataType.TYPE_HEART_RATE_BPM + BODY_TEMPERATURE -> HealthDataTypes.TYPE_BODY_TEMPERATURE + BLOOD_PRESSURE_SYSTOLIC -> HealthDataTypes.TYPE_BLOOD_PRESSURE + BLOOD_PRESSURE_DIASTOLIC -> HealthDataTypes.TYPE_BLOOD_PRESSURE + BLOOD_OXYGEN -> HealthDataTypes.TYPE_OXYGEN_SATURATION + BLOOD_GLUCOSE -> HealthDataTypes.TYPE_BLOOD_GLUCOSE + MOVE_MINUTES -> DataType.TYPE_MOVE_MINUTES + DISTANCE_DELTA -> DataType.TYPE_DISTANCE_DELTA + WATER -> DataType.TYPE_HYDRATION + 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 + NUTRITION -> DataType.TYPE_NUTRITION + else -> throw IllegalArgumentException("Unsupported dataType: $type") + } + } + + private fun getField(type: String): Field { + return when (type) { + BODY_FAT_PERCENTAGE -> Field.FIELD_PERCENTAGE + HEIGHT -> Field.FIELD_HEIGHT + WEIGHT -> Field.FIELD_WEIGHT + STEPS -> Field.FIELD_STEPS + ACTIVE_ENERGY_BURNED -> Field.FIELD_CALORIES + HEART_RATE -> Field.FIELD_BPM + BODY_TEMPERATURE -> HealthFields.FIELD_BODY_TEMPERATURE + BLOOD_PRESSURE_SYSTOLIC -> HealthFields.FIELD_BLOOD_PRESSURE_SYSTOLIC + BLOOD_PRESSURE_DIASTOLIC -> HealthFields.FIELD_BLOOD_PRESSURE_DIASTOLIC + BLOOD_OXYGEN -> HealthFields.FIELD_OXYGEN_SATURATION + BLOOD_GLUCOSE -> HealthFields.FIELD_BLOOD_GLUCOSE_LEVEL + MOVE_MINUTES -> Field.FIELD_DURATION + DISTANCE_DELTA -> Field.FIELD_DISTANCE + WATER -> Field.FIELD_VOLUME + 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 + NUTRITION -> Field.FIELD_NUTRIENTS + else -> throw IllegalArgumentException("Unsupported dataType: $type") + } + } + + private fun isIntField(dataSource: DataSource, unit: Field): Boolean { + val dataPoint = DataPoint.builder(dataSource).build() + val value = dataPoint.getValue(unit) + return value.format == Field.FORMAT_INT32 + } + + // / Extracts the (numeric) value from a Health Data Point + private fun getHealthDataValue(dataPoint: DataPoint, field: Field): Any { + val value = dataPoint.getValue(field) + // Conversion is needed because glucose is stored as mmoll in Google Fit; + // while mgdl is used for glucose in this plugin. + val isGlucose = field == HealthFields.FIELD_BLOOD_GLUCOSE_LEVEL + return when (value.format) { + Field.FORMAT_FLOAT -> + if (!isGlucose) value.asFloat() + else value.asFloat() * MMOLL_2_MGDL + + Field.FORMAT_INT32 -> value.asInt() + Field.FORMAT_STRING -> value.asString() + else -> Log.e("Unsupported format:", value.format.toString()) + } + } - override fun onAttachedToEngine( - @NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding - ) { - scope = CoroutineScope(SupervisorJob() + Dispatchers.Main) - channel = MethodChannel(flutterPluginBinding.binaryMessenger, CHANNEL_NAME) - channel?.setMethodCallHandler(this) - context = flutterPluginBinding.applicationContext - threadPoolExecutor = Executors.newFixedThreadPool(4) - checkAvailability() - if (healthConnectAvailable) { - healthConnectClient = - HealthConnectClient.getOrCreate( - flutterPluginBinding.applicationContext - ) - } + /** Delete records of the given type in the time range */ + private fun delete(call: MethodCall, result: Result) { + if (useHealthConnectIfAvailable && healthConnectAvailable) { + deleteHCData(call, result) + return } + if (context == null) { + result.success(false) + return + } + + val type = call.argument("dataTypeKey")!! + val startTime = call.argument("startTime")!! + val endTime = call.argument("endTime")!! + + // Look up data type and unit for the type key + val dataType = keyToHealthDataType(type) + val field = getField(type) + + val typesBuilder = FitnessOptions.builder() + typesBuilder.addDataType(dataType, FitnessOptions.ACCESS_WRITE) - override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { - channel = null - activity = null - threadPoolExecutor!!.shutdown() - threadPoolExecutor = null - } - - // This static function is optional and equivalent to onAttachedToEngine. It supports the - // old - // pre-Flutter-1.12 Android projects. You are encouraged to continue supporting - // plugin registration via this function while apps migrate to use the new Android APIs - // post-flutter-1.12 via https://flutter.dev/go/android-project-migration. - // - // It is encouraged to share logic between onAttachedToEngine and registerWith to keep - // them functionally equivalent. Only one of onAttachedToEngine or registerWith will be - // called - // depending on the user's project. onAttachedToEngine or registerWith must both be defined - // in the same class. - companion object { - @Suppress("unused") - @JvmStatic - fun registerWith(registrar: Registrar) { - val channel = MethodChannel(registrar.messenger(), CHANNEL_NAME) - val plugin = HealthPlugin(channel) - registrar.addActivityResultListener(plugin) - channel.setMethodCallHandler(plugin) + val dataSource = + DataDeleteRequest.Builder() + .setTimeInterval( + startTime, + endTime, + TimeUnit.MILLISECONDS + ) + .addDataType(dataType) + .deleteAllSessions() + .build() + + val fitnessOptions = typesBuilder.build() + + try { + val googleSignInAccount = + GoogleSignIn.getAccountForExtension( + context!!.applicationContext, + fitnessOptions + ) + Fitness.getHistoryClient(context!!.applicationContext, googleSignInAccount) + .deleteData(dataSource) + .addOnSuccessListener { + Log.i( + "FLUTTER_HEALTH::SUCCESS", + "Dataset deleted successfully!" + ) + result.success(true) } + .addOnFailureListener( + errHandler( + result, + "There was an error deleting the dataset" + ) + ) + } catch (e3: Exception) { + result.success(false) } + } - override fun success(p0: Any?) { - handler?.post { mResult?.success(p0) } + /** Save a Blood Pressure measurement with systolic and diastolic values */ + private fun writeBloodPressure(call: MethodCall, result: Result) { + if (useHealthConnectIfAvailable && healthConnectAvailable) { + writeBloodPressureHC(call, result) + return } - - override fun notImplemented() { - handler?.post { mResult?.notImplemented() } + if (context == null) { + result.success(false) + return } - override fun error( - errorCode: String, - errorMessage: String?, - errorDetails: Any?, - ) { - handler?.post { mResult?.error(errorCode, errorMessage, errorDetails) } - } + val dataType = HealthDataTypes.TYPE_BLOOD_PRESSURE + val systolic = call.argument("systolic")!! + val diastolic = call.argument("diastolic")!! + val startTime = call.argument("startTime")!! + val endTime = call.argument("endTime")!! + + val typesBuilder = FitnessOptions.builder() + typesBuilder.addDataType(dataType, FitnessOptions.ACCESS_WRITE) + + val dataSource = + DataSource.Builder() + .setDataType(dataType) + .setType(DataSource.TYPE_RAW) + .setDevice( + Device.getLocalDevice( + context!!.applicationContext + ) + ) + .setAppPackageName(context!!.applicationContext) + .build() + + val builder = + DataPoint.builder(dataSource) + .setTimeInterval( + startTime, + endTime, + TimeUnit.MILLISECONDS + ) + .setField( + HealthFields.FIELD_BLOOD_PRESSURE_SYSTOLIC, + systolic + ) + .setField( + HealthFields.FIELD_BLOOD_PRESSURE_DIASTOLIC, + diastolic + ) + .build() - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean { - if (requestCode == GOOGLE_FIT_PERMISSIONS_REQUEST_CODE) { - if (resultCode == Activity.RESULT_OK) { - Log.i("FLUTTER_HEALTH", "Access Granted!") - mResult?.success(true) - } else if (resultCode == Activity.RESULT_CANCELED) { - Log.i("FLUTTER_HEALTH", "Access Denied!") - mResult?.success(false) - } + val dataPoint = builder + val dataSet = DataSet.builder(dataSource).add(dataPoint).build() + + val fitnessOptions = typesBuilder.build() + try { + val googleSignInAccount = + GoogleSignIn.getAccountForExtension( + context!!.applicationContext, + fitnessOptions + ) + Fitness.getHistoryClient(context!!.applicationContext, googleSignInAccount) + .insertData(dataSet) + .addOnSuccessListener { + Log.i( + "FLUTTER_HEALTH::SUCCESS", + "Blood Pressure added successfully!" + ) + result.success(true) } - return false + .addOnFailureListener( + errHandler( + result, + "There was an error adding the blood pressure data!", + ), + ) + } catch (e3: Exception) { + result.success(false) + } + } + + private fun writeMealHC(call: MethodCall, result: Result) { + val startTime = Instant.ofEpochMilli(call.argument("startTime")!!) + val endTime = Instant.ofEpochMilli(call.argument("endTime")!!) + val calories = call.argument("caloriesConsumed") + val carbs = call.argument("carbohydrates") as Double? + val protein = call.argument("protein") as Double? + val fat = call.argument("fatTotal") as Double? + val caffeine = call.argument("caffeine") as Double? + val name = call.argument("name") + val mealType = call.argument("mealType")!! + + scope.launch { + try { + val list = mutableListOf() + list.add( + NutritionRecord( + name = name, + energy = calories?.kilocalories, + totalCarbohydrate = carbs?.grams, + protein = protein?.grams, + totalFat = fat?.grams, + caffeine = caffeine?.grams, + startTime = startTime, + startZoneOffset = null, + endTime = endTime, + endZoneOffset = null, + mealType = + MapMealTypeToTypeHC[ + mealType] + ?: MEAL_TYPE_UNKNOWN, + ), + ) + healthConnectClient.insertRecords( + list, + ) + result.success(true) + Log.i( + "FLUTTER_HEALTH::SUCCESS", + "[Health Connect] Meal was successfully added!" + ) + } catch (e: Exception) { + Log.w( + "FLUTTER_HEALTH::ERROR", + "[Health Connect] There was an error adding the meal", + ) + Log.w("FLUTTER_HEALTH::ERROR", e.message ?: "unknown error") + Log.w("FLUTTER_HEALTH::ERROR", e.stackTrace.toString()) + result.success(false) + } } + } - private fun onHealthConnectPermissionCallback(permissionGranted: Set) { - if (permissionGranted.isEmpty()) { - mResult?.success(false) - Log.i("FLUTTER_HEALTH", "Access Denied (to Health Connect)!") - } else { - mResult?.success(true) - Log.i("FLUTTER_HEALTH", "Access Granted (to Health Connect)!") - } + /** Save a Nutrition measurement with calories, carbs, protein, fat, name and mealType */ + private fun writeMeal(call: MethodCall, result: Result) { + if (useHealthConnectIfAvailable && healthConnectAvailable) { + writeMealHC(call, result) + return } - private fun keyToHealthDataType(type: String): DataType { - return when (type) { - BODY_FAT_PERCENTAGE -> DataType.TYPE_BODY_FAT_PERCENTAGE - HEIGHT -> DataType.TYPE_HEIGHT - WEIGHT -> DataType.TYPE_WEIGHT - STEPS -> DataType.TYPE_STEP_COUNT_DELTA - AGGREGATE_STEP_COUNT -> DataType.AGGREGATE_STEP_COUNT_DELTA - ACTIVE_ENERGY_BURNED -> DataType.TYPE_CALORIES_EXPENDED - HEART_RATE -> DataType.TYPE_HEART_RATE_BPM - BODY_TEMPERATURE -> HealthDataTypes.TYPE_BODY_TEMPERATURE - BLOOD_PRESSURE_SYSTOLIC -> HealthDataTypes.TYPE_BLOOD_PRESSURE - BLOOD_PRESSURE_DIASTOLIC -> HealthDataTypes.TYPE_BLOOD_PRESSURE - BLOOD_OXYGEN -> HealthDataTypes.TYPE_OXYGEN_SATURATION - BLOOD_GLUCOSE -> HealthDataTypes.TYPE_BLOOD_GLUCOSE - MOVE_MINUTES -> DataType.TYPE_MOVE_MINUTES - DISTANCE_DELTA -> DataType.TYPE_DISTANCE_DELTA - WATER -> DataType.TYPE_HYDRATION - 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 - NUTRITION -> DataType.TYPE_NUTRITION - else -> throw IllegalArgumentException("Unsupported dataType: $type") - } + if (context == null) { + result.success(false) + return } - private fun getField(type: String): Field { - return when (type) { - BODY_FAT_PERCENTAGE -> Field.FIELD_PERCENTAGE - HEIGHT -> Field.FIELD_HEIGHT - WEIGHT -> Field.FIELD_WEIGHT - STEPS -> Field.FIELD_STEPS - ACTIVE_ENERGY_BURNED -> Field.FIELD_CALORIES - HEART_RATE -> Field.FIELD_BPM - BODY_TEMPERATURE -> HealthFields.FIELD_BODY_TEMPERATURE - BLOOD_PRESSURE_SYSTOLIC -> HealthFields.FIELD_BLOOD_PRESSURE_SYSTOLIC - BLOOD_PRESSURE_DIASTOLIC -> HealthFields.FIELD_BLOOD_PRESSURE_DIASTOLIC - BLOOD_OXYGEN -> HealthFields.FIELD_OXYGEN_SATURATION - BLOOD_GLUCOSE -> HealthFields.FIELD_BLOOD_GLUCOSE_LEVEL - MOVE_MINUTES -> Field.FIELD_DURATION - DISTANCE_DELTA -> Field.FIELD_DISTANCE - WATER -> Field.FIELD_VOLUME - 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 - NUTRITION -> Field.FIELD_NUTRIENTS - else -> throw IllegalArgumentException("Unsupported dataType: $type") - } + val startTime = call.argument("startTime")!! + val endTime = call.argument("endTime")!! + val calories = call.argument("caloriesConsumed") + val carbs = call.argument("carbohydrates") as Double? + val protein = call.argument("protein") as Double? + val fat = call.argument("fatTotal") as Double? + val name = call.argument("name") + val mealType = call.argument("mealType")!! + + val dataType = DataType.TYPE_NUTRITION + + val typesBuilder = FitnessOptions.builder() + typesBuilder.addDataType(dataType, FitnessOptions.ACCESS_WRITE) + + val dataSource = + DataSource.Builder() + .setDataType(dataType) + .setType(DataSource.TYPE_RAW) + .setDevice( + Device.getLocalDevice( + context!!.applicationContext + ) + ) + .setAppPackageName(context!!.applicationContext) + .build() + + val nutrients = mutableMapOf(Field.NUTRIENT_CALORIES to calories?.toFloat()) + + if (carbs != null) { + nutrients[Field.NUTRIENT_TOTAL_CARBS] = carbs.toFloat() } - private fun isIntField(dataSource: DataSource, unit: Field): Boolean { - val dataPoint = DataPoint.builder(dataSource).build() - val value = dataPoint.getValue(unit) - return value.format == Field.FORMAT_INT32 - } - - // / Extracts the (numeric) value from a Health Data Point - private fun getHealthDataValue(dataPoint: DataPoint, field: Field): Any { - val value = dataPoint.getValue(field) - // Conversion is needed because glucose is stored as mmoll in Google Fit; - // while mgdl is used for glucose in this plugin. - val isGlucose = field == HealthFields.FIELD_BLOOD_GLUCOSE_LEVEL - return when (value.format) { - Field.FORMAT_FLOAT -> - if (!isGlucose) value.asFloat() - else value.asFloat() * MMOLL_2_MGDL - Field.FORMAT_INT32 -> value.asInt() - Field.FORMAT_STRING -> value.asString() - else -> Log.e("Unsupported format:", value.format.toString()) - } + if (protein != null) { + nutrients[Field.NUTRIENT_PROTEIN] = protein.toFloat() } - /** Delete records of the given type in the time range */ - private fun delete(call: MethodCall, result: Result) { - if (useHealthConnectIfAvailable && healthConnectAvailable) { - deleteHCData(call, result) - return - } - if (context == null) { - result.success(false) - return - } + if (fat != null) { + nutrients[Field.NUTRIENT_TOTAL_FAT] = fat.toFloat() + } - val type = call.argument("dataTypeKey")!! - val startTime = call.argument("startTime")!! - val endTime = call.argument("endTime")!! + val dataBuilder = + DataPoint.builder(dataSource) + .setTimeInterval( + startTime, + endTime, + TimeUnit.MILLISECONDS + ) + .setField(Field.FIELD_NUTRIENTS, nutrients) - // Look up data type and unit for the type key - val dataType = keyToHealthDataType(type) - val field = getField(type) + if (name != null) { + dataBuilder.setField(Field.FIELD_FOOD_ITEM, name as String) + } - val typesBuilder = FitnessOptions.builder() - typesBuilder.addDataType(dataType, FitnessOptions.ACCESS_WRITE) + dataBuilder.setField( + Field.FIELD_MEAL_TYPE, + MapMealTypeToType[mealType] ?: Field.MEAL_TYPE_UNKNOWN + ) - val dataSource = - DataDeleteRequest.Builder() - .setTimeInterval( - startTime, - endTime, - TimeUnit.MILLISECONDS - ) - .addDataType(dataType) - .deleteAllSessions() - .build() + val dataPoint = dataBuilder.build() - val fitnessOptions = typesBuilder.build() + val dataSet = DataSet.builder(dataSource).add(dataPoint).build() - try { - val googleSignInAccount = - GoogleSignIn.getAccountForExtension( - context!!.applicationContext, - fitnessOptions - ) - Fitness.getHistoryClient(context!!.applicationContext, googleSignInAccount) - .deleteData(dataSource) - .addOnSuccessListener { - Log.i( - "FLUTTER_HEALTH::SUCCESS", - "Dataset deleted successfully!" - ) - result.success(true) - } - .addOnFailureListener( - errHandler( - result, - "There was an error deleting the dataset" - ) - ) - } catch (e3: Exception) { - result.success(false) + val fitnessOptions = typesBuilder.build() + try { + val googleSignInAccount = + GoogleSignIn.getAccountForExtension( + context!!.applicationContext, + fitnessOptions + ) + Fitness.getHistoryClient(context!!.applicationContext, googleSignInAccount) + .insertData(dataSet) + .addOnSuccessListener { + Log.i( + "FLUTTER_HEALTH::SUCCESS", + "Meal added successfully!" + ) + result.success(true) } + .addOnFailureListener( + errHandler( + result, + "There was an error adding the meal data!" + ) + ) + } catch (e3: Exception) { + result.success(false) } + } - /** Save a Blood Pressure measurement with systolic and diastolic values */ - private fun writeBloodPressure(call: MethodCall, result: Result) { - if (useHealthConnectIfAvailable && healthConnectAvailable) { - writeBloodPressureHC(call, result) - return - } - if (context == null) { - result.success(false) - return - } + /** Save a data type in Google Fit */ + private fun writeData(call: MethodCall, result: Result) { + if (useHealthConnectIfAvailable && healthConnectAvailable) { + writeHCData(call, result) + return + } + if (context == null) { + result.success(false) + return + } - val dataType = HealthDataTypes.TYPE_BLOOD_PRESSURE - val systolic = call.argument("systolic")!! - val diastolic = call.argument("diastolic")!! - val startTime = call.argument("startTime")!! - val endTime = call.argument("endTime")!! - - val typesBuilder = FitnessOptions.builder() - typesBuilder.addDataType(dataType, FitnessOptions.ACCESS_WRITE) - - val dataSource = - DataSource.Builder() - .setDataType(dataType) - .setType(DataSource.TYPE_RAW) - .setDevice( - Device.getLocalDevice( - context!!.applicationContext - ) - ) - .setAppPackageName(context!!.applicationContext) - .build() - - val builder = - DataPoint.builder(dataSource) - .setTimeInterval( - startTime, - endTime, - TimeUnit.MILLISECONDS - ) - .setField( - HealthFields.FIELD_BLOOD_PRESSURE_SYSTOLIC, - systolic - ) - .setField( - HealthFields.FIELD_BLOOD_PRESSURE_DIASTOLIC, - diastolic - ) - .build() + val type = call.argument("dataTypeKey")!! + val startTime = call.argument("startTime")!! + val endTime = call.argument("endTime")!! + val value = call.argument("value")!! + + // Look up data type and unit for the type key + val dataType = keyToHealthDataType(type) + val field = getField(type) + + val typesBuilder = FitnessOptions.builder() + typesBuilder.addDataType(dataType, FitnessOptions.ACCESS_WRITE) + + val dataSource = + DataSource.Builder() + .setDataType(dataType) + .setType(DataSource.TYPE_RAW) + .setDevice( + Device.getLocalDevice( + context!!.applicationContext + ) + ) + .setAppPackageName(context!!.applicationContext) + .build() + + val builder = + if (startTime == endTime) { + DataPoint.builder(dataSource) + .setTimestamp( + startTime, + TimeUnit.MILLISECONDS + ) + } else { + DataPoint.builder(dataSource) + .setTimeInterval( + startTime, + endTime, + TimeUnit.MILLISECONDS + ) + } + + // Conversion is needed because glucose is stored as mmoll in Google Fit; + // while mgdl is used for glucose in this plugin. + val isGlucose = field == HealthFields.FIELD_BLOOD_GLUCOSE_LEVEL + val dataPoint = + if (!isIntField(dataSource, field)) { + builder.setField( + field, + (if (!isGlucose) value + else + (value / + MMOLL_2_MGDL) + .toFloat()) + ) + .build() + } else { + builder.setField(field, value.toInt()).build() + } - val dataPoint = builder - val dataSet = DataSet.builder(dataSource).add(dataPoint).build() + val dataSet = DataSet.builder(dataSource).add(dataPoint).build() - val fitnessOptions = typesBuilder.build() - try { - val googleSignInAccount = - GoogleSignIn.getAccountForExtension( - context!!.applicationContext, - fitnessOptions - ) - Fitness.getHistoryClient(context!!.applicationContext, googleSignInAccount) - .insertData(dataSet) - .addOnSuccessListener { - Log.i( - "FLUTTER_HEALTH::SUCCESS", - "Blood Pressure added successfully!" - ) - result.success(true) - } - .addOnFailureListener( - errHandler( - result, - "There was an error adding the blood pressure data!", - ), - ) - } catch (e3: Exception) { - result.success(false) - } + if (dataType == DataType.TYPE_SLEEP_SEGMENT) { + typesBuilder.accessSleepSessions(FitnessOptions.ACCESS_READ) } - - private fun writeMealHC(call: MethodCall, result: Result) { - val startTime = Instant.ofEpochMilli(call.argument("startTime")!!) - val endTime = Instant.ofEpochMilli(call.argument("endTime")!!) - val calories = call.argument("caloriesConsumed") - val carbs = call.argument("carbohydrates") as Double? - val protein = call.argument("protein") as Double? - val fat = call.argument("fatTotal") as Double? - val caffeine = call.argument("caffeine") as Double? - val name = call.argument("name") - val mealType = call.argument("mealType")!! - - scope.launch { - try { - val list = mutableListOf() - list.add( - NutritionRecord( - name = name, - energy = calories?.kilocalories, - totalCarbohydrate = carbs?.grams, - protein = protein?.grams, - totalFat = fat?.grams, - caffeine = caffeine?.grams, - startTime = startTime, - startZoneOffset = null, - endTime = endTime, - endZoneOffset = null, - mealType = - MapMealTypeToTypeHC[ - mealType] - ?: MEAL_TYPE_UNKNOWN, - ), - ) - healthConnectClient.insertRecords( - list, - ) - result.success(true) - Log.i( - "FLUTTER_HEALTH::SUCCESS", - "[Health Connect] Meal was successfully added!" - ) - } catch (e: Exception) { - Log.w( - "FLUTTER_HEALTH::ERROR", - "[Health Connect] There was an error adding the meal", - ) - Log.w("FLUTTER_HEALTH::ERROR", e.message ?: "unknown error") - Log.w("FLUTTER_HEALTH::ERROR", e.stackTrace.toString()) - result.success(false) - } + val fitnessOptions = typesBuilder.build() + try { + val googleSignInAccount = + GoogleSignIn.getAccountForExtension( + context!!.applicationContext, + fitnessOptions + ) + Fitness.getHistoryClient(context!!.applicationContext, googleSignInAccount) + .insertData(dataSet) + .addOnSuccessListener { + Log.i( + "FLUTTER_HEALTH::SUCCESS", + "Dataset added successfully!" + ) + result.success(true) } + .addOnFailureListener( + errHandler( + result, + "There was an error adding the dataset" + ) + ) + } catch (e3: Exception) { + result.success(false) + } + } + + /** + * Save the blood oxygen saturation, in Google Fit with the supplemental flow rate, in + * HealthConnect without + */ + private fun writeBloodOxygen(call: MethodCall, result: Result) { + // Health Connect does not support supplemental flow rate, thus it is ignored + if (useHealthConnectIfAvailable && healthConnectAvailable) { + writeHCData(call, result) + return } - /** Save a Nutrition measurement with calories, carbs, protein, fat, name and mealType */ - private fun writeMeal(call: MethodCall, result: Result) { - if (useHealthConnectIfAvailable && healthConnectAvailable) { - writeMealHC(call, result) - return - } + if (context == null) { + result.success(false) + return + } - if (context == null) { - result.success(false) - return + val dataType = HealthDataTypes.TYPE_OXYGEN_SATURATION + val startTime = call.argument("startTime")!! + val endTime = call.argument("endTime")!! + val saturation = call.argument("value")!! + val flowRate = call.argument("flowRate")!! + + val typesBuilder = FitnessOptions.builder() + typesBuilder.addDataType(dataType, FitnessOptions.ACCESS_WRITE) + + val dataSource = + DataSource.Builder() + .setDataType(dataType) + .setType(DataSource.TYPE_RAW) + .setDevice( + Device.getLocalDevice( + context!!.applicationContext + ) + ) + .setAppPackageName(context!!.applicationContext) + .build() + + val builder = + if (startTime == endTime) { + DataPoint.builder(dataSource) + .setTimestamp( + startTime, + TimeUnit.MILLISECONDS + ) + } else { + DataPoint.builder(dataSource) + .setTimeInterval( + startTime, + endTime, + TimeUnit.MILLISECONDS + ) + } + + builder.setField(HealthFields.FIELD_SUPPLEMENTAL_OXYGEN_FLOW_RATE, flowRate) + builder.setField(HealthFields.FIELD_OXYGEN_SATURATION, saturation) + + val dataPoint = builder.build() + val dataSet = DataSet.builder(dataSource).add(dataPoint).build() + + val fitnessOptions = typesBuilder.build() + try { + val googleSignInAccount = + GoogleSignIn.getAccountForExtension( + context!!.applicationContext, + fitnessOptions + ) + Fitness.getHistoryClient(context!!.applicationContext, googleSignInAccount) + .insertData(dataSet) + .addOnSuccessListener { + Log.i( + "FLUTTER_HEALTH::SUCCESS", + "Blood Oxygen added successfully!" + ) + result.success(true) } + .addOnFailureListener( + errHandler( + result, + "There was an error adding the blood oxygen data!", + ), + ) + } catch (e3: Exception) { + result.success(false) + } + } - val startTime = call.argument("startTime")!! - val endTime = call.argument("endTime")!! - val calories = call.argument("caloriesConsumed") - val carbs = call.argument("carbohydrates") as Double? - val protein = call.argument("protein") as Double? - val fat = call.argument("fatTotal") as Double? - val name = call.argument("name") - val mealType = call.argument("mealType")!! - - val dataType = DataType.TYPE_NUTRITION - - val typesBuilder = FitnessOptions.builder() - typesBuilder.addDataType(dataType, FitnessOptions.ACCESS_WRITE) - - val dataSource = - DataSource.Builder() - .setDataType(dataType) - .setType(DataSource.TYPE_RAW) - .setDevice( - Device.getLocalDevice( - context!!.applicationContext - ) - ) - .setAppPackageName(context!!.applicationContext) - .build() + /** Save a Workout session with options for distance and calories expended */ + private fun writeWorkoutData(call: MethodCall, result: Result) { + if (useHealthConnectIfAvailable && healthConnectAvailable) { + writeWorkoutHCData(call, result) + return + } + if (context == null) { + result.success(false) + return + } - val nutrients = mutableMapOf(Field.NUTRIENT_CALORIES to calories?.toFloat()) + val type = call.argument("activityType")!! + val startTime = call.argument("startTime")!! + val endTime = call.argument("endTime")!! + val totalEnergyBurned = call.argument("totalEnergyBurned") + val totalDistance = call.argument("totalDistance") + + val activityType = getActivityType(type) + // Create the Activity Segment DataSource + val activitySegmentDataSource = + DataSource.Builder() + .setAppPackageName(context!!.packageName) + .setDataType(DataType.TYPE_ACTIVITY_SEGMENT) + .setStreamName("FLUTTER_HEALTH - Activity") + .setType(DataSource.TYPE_RAW) + .build() + // Create the Activity Segment + val activityDataPoint = + DataPoint.builder(activitySegmentDataSource) + .setTimeInterval( + startTime, + endTime, + TimeUnit.MILLISECONDS + ) + .setActivityField( + Field.FIELD_ACTIVITY, + activityType + ) + .build() + // Add DataPoint to DataSet + val activitySegments = + DataSet.builder(activitySegmentDataSource) + .add(activityDataPoint) + .build() + + // If distance is provided + var distanceDataSet: DataSet? = null + if (totalDistance != null) { + // Create a data source + val distanceDataSource = + DataSource.Builder() + .setAppPackageName(context!!.packageName) + .setDataType(DataType.TYPE_DISTANCE_DELTA) + .setStreamName("FLUTTER_HEALTH - Distance") + .setType(DataSource.TYPE_RAW) + .build() + + val distanceDataPoint = + DataPoint.builder(distanceDataSource) + .setTimeInterval( + startTime, + endTime, + TimeUnit.MILLISECONDS + ) + .setField( + Field.FIELD_DISTANCE, + totalDistance.toFloat() + ) + .build() + // Create a data set + distanceDataSet = + DataSet.builder(distanceDataSource) + .add(distanceDataPoint) + .build() + } + // If energyBurned is provided + var energyDataSet: DataSet? = null + if (totalEnergyBurned != null) { + // Create a data source + val energyDataSource = + DataSource.Builder() + .setAppPackageName(context!!.packageName) + .setDataType( + DataType.TYPE_CALORIES_EXPENDED + ) + .setStreamName("FLUTTER_HEALTH - Calories") + .setType(DataSource.TYPE_RAW) + .build() + + val energyDataPoint = + DataPoint.builder(energyDataSource) + .setTimeInterval( + startTime, + endTime, + TimeUnit.MILLISECONDS + ) + .setField( + Field.FIELD_CALORIES, + totalEnergyBurned.toFloat() + ) + .build() + // Create a data set + energyDataSet = + DataSet.builder(energyDataSource) + .add(energyDataPoint) + .build() + } - if (carbs != null) { - nutrients[Field.NUTRIENT_TOTAL_CARBS] = carbs.toFloat() - } + // Finish session setup + val session = + Session.Builder() + .setName( + activityType + ) // TODO: Make a sensible name / allow user to set + // name + .setDescription("") + .setIdentifier(UUID.randomUUID().toString()) + .setActivity(activityType) + .setStartTime(startTime, TimeUnit.MILLISECONDS) + .setEndTime(endTime, TimeUnit.MILLISECONDS) + .build() + // Build a session and add the values provided + val sessionInsertRequestBuilder = + SessionInsertRequest.Builder() + .setSession(session) + .addDataSet(activitySegments) + if (totalDistance != null) { + sessionInsertRequestBuilder.addDataSet(distanceDataSet!!) + } + if (totalEnergyBurned != null) { + sessionInsertRequestBuilder.addDataSet(energyDataSet!!) + } + val insertRequest = sessionInsertRequestBuilder.build() - if (protein != null) { - nutrients[Field.NUTRIENT_PROTEIN] = protein.toFloat() - } + val fitnessOptionsBuilder = + FitnessOptions.builder() + .addDataType( + DataType.TYPE_ACTIVITY_SEGMENT, + FitnessOptions.ACCESS_WRITE + ) + if (totalDistance != null) { + fitnessOptionsBuilder.addDataType( + DataType.TYPE_DISTANCE_DELTA, + FitnessOptions.ACCESS_WRITE, + ) + } + if (totalEnergyBurned != null) { + fitnessOptionsBuilder.addDataType( + DataType.TYPE_CALORIES_EXPENDED, + FitnessOptions.ACCESS_WRITE, + ) + } + val fitnessOptions = fitnessOptionsBuilder.build() - if (fat != null) { - nutrients[Field.NUTRIENT_TOTAL_FAT] = fat.toFloat() + try { + val googleSignInAccount = + GoogleSignIn.getAccountForExtension( + context!!.applicationContext, + fitnessOptions + ) + Fitness.getSessionsClient( + context!!.applicationContext, + googleSignInAccount, + ) + .insertSession(insertRequest) + .addOnSuccessListener { + Log.i( + "FLUTTER_HEALTH::SUCCESS", + "Workout was successfully added!" + ) + result.success(true) } + .addOnFailureListener( + errHandler( + result, + "There was an error adding the workout" + ) + ) + } catch (e: Exception) { + result.success(false) + } + } - val dataBuilder = - DataPoint.builder(dataSource) - .setTimeInterval( - startTime, - endTime, - TimeUnit.MILLISECONDS - ) - .setField(Field.FIELD_NUTRIENTS, nutrients) - - if (name != null) { - dataBuilder.setField(Field.FIELD_FOOD_ITEM, name as String) - } + /** Get all datapoints of the DataType within the given time range */ + private fun getData(call: MethodCall, result: Result) { - dataBuilder.setField( - Field.FIELD_MEAL_TYPE, - MapMealTypeToType[mealType] ?: Field.MEAL_TYPE_UNKNOWN - ) + if (StepCounterService.initiated()) { + getSensorData(call, result) + return + } - val dataPoint = dataBuilder.build() + if (useHealthConnectIfAvailable && healthConnectAvailable) { + getHCData(call, result) + return + } - val dataSet = DataSet.builder(dataSource).add(dataPoint).build() + if (context == null) { + result.success(null) + return + } - val fitnessOptions = typesBuilder.build() - try { - val googleSignInAccount = - GoogleSignIn.getAccountForExtension( - context!!.applicationContext, - fitnessOptions - ) - Fitness.getHistoryClient(context!!.applicationContext, googleSignInAccount) - .insertData(dataSet) - .addOnSuccessListener { - Log.i( - "FLUTTER_HEALTH::SUCCESS", - "Meal added successfully!" - ) - result.success(true) - } - .addOnFailureListener( - errHandler( - result, - "There was an error adding the meal data!" - ) - ) - } catch (e3: Exception) { - result.success(false) - } + val type = call.argument("dataTypeKey")!! + val startTime = call.argument("startTime")!! + val endTime = call.argument("endTime")!! + val includeManualEntry = call.argument("includeManualEntry")!! + // Look up data type and unit for the type key + val dataType = keyToHealthDataType(type) + val field = getField(type) + val typesBuilder = FitnessOptions.builder() + typesBuilder.addDataType(dataType) + + // Add special cases for accessing workouts or sleep data. + if (dataType == DataType.TYPE_SLEEP_SEGMENT) { + typesBuilder.accessSleepSessions(FitnessOptions.ACCESS_READ) + } else if (dataType == DataType.TYPE_ACTIVITY_SEGMENT) { + typesBuilder.accessActivitySessions(FitnessOptions.ACCESS_READ) + .addDataType( + DataType.TYPE_CALORIES_EXPENDED, + FitnessOptions.ACCESS_READ + ) + .addDataType( + DataType.TYPE_DISTANCE_DELTA, + FitnessOptions.ACCESS_READ + ) } + val fitnessOptions = typesBuilder.build() + val googleSignInAccount = + GoogleSignIn.getAccountForExtension( + context!!.applicationContext, + fitnessOptions + ) + // Handle data types + when (dataType) { + DataType.TYPE_SLEEP_SEGMENT -> { + // request to the sessions for sleep data + val request = + SessionReadRequest.Builder() + .setTimeInterval( + startTime, + endTime, + TimeUnit.MILLISECONDS + ) + .enableServerQueries() + .readSessionsFromAllApps() + .includeSleepSessions() + .build() + Fitness.getSessionsClient( + context!!.applicationContext, + googleSignInAccount + ) + .readSession(request) + .addOnSuccessListener( + threadPoolExecutor!!, + sleepDataHandler(type, result) + ) + .addOnFailureListener( + errHandler( + result, + "There was an error getting the sleeping data!", + ), + ) + } + + DataType.TYPE_ACTIVITY_SEGMENT -> { + val readRequest: SessionReadRequest + val readRequestBuilder = + SessionReadRequest.Builder() + .setTimeInterval( + startTime, + endTime, + TimeUnit.MILLISECONDS + ) + .enableServerQueries() + .readSessionsFromAllApps() + .includeActivitySessions() + .read(dataType) + .read( + DataType.TYPE_CALORIES_EXPENDED + ) - /** Save a data type in Google Fit */ - private fun writeData(call: MethodCall, result: Result) { - if (useHealthConnectIfAvailable && healthConnectAvailable) { - writeHCData(call, result) - return - } - if (context == null) { - result.success(false) - return + // If fine location is enabled, read distance data + if (ContextCompat.checkSelfPermission( + context!!.applicationContext, + android.Manifest.permission + .ACCESS_FINE_LOCATION, + ) == PackageManager.PERMISSION_GRANTED + ) { + readRequestBuilder.read(DataType.TYPE_DISTANCE_DELTA) } + readRequest = readRequestBuilder.build() + Fitness.getSessionsClient( + context!!.applicationContext, + googleSignInAccount + ) + .readSession(readRequest) + .addOnSuccessListener( + threadPoolExecutor!!, + workoutDataHandler(type, result) + ) + .addOnFailureListener( + errHandler( + result, + "There was an error getting the workout data!", + ), + ) + } + + else -> { + Fitness.getHistoryClient( + context!!.applicationContext, + googleSignInAccount + ) + .readData( + DataReadRequest.Builder() + .read(dataType) + .setTimeRange( + startTime, + endTime, + TimeUnit.MILLISECONDS + ) + .build(), + ) + .addOnSuccessListener( + threadPoolExecutor!!, + dataHandler( + dataType, + field, + includeManualEntry, + result + ), + ) + .addOnFailureListener( + errHandler( + result, + "There was an error getting the data!", + ), + ) + } + } + } + + private fun getSensorData(call: MethodCall, result: Result) { + val dataType = call.argument("dataTypeKey")!! + val startTime = Instant.ofEpochMilli(call.argument("startTime")!!) + val endTime = Instant.ofEpochMilli(call.argument("endTime")!!) + + val healthConnectData = mutableListOf>() + + if(dataType == STEPS && StepCounterService.initiated()) { + val items = StepCounterService.box.boxFor(SensorStep::class.java).query(SensorStep_.startTime.between(startTime.toEpochMilli(),endTime.toEpochMilli())).build().find() + healthConnectData.addAll(items.map { + mapOf( + "value" to + it.count, + "date_from" to + it.startTime!! + , + "date_to" to + it.endTime!!, + "source_id" to "", + "source_name" to + "sensor_step", + ) + }) + } - val type = call.argument("dataTypeKey")!! - val startTime = call.argument("startTime")!! - val endTime = call.argument("endTime")!! - val value = call.argument("value")!! - - // Look up data type and unit for the type key - val dataType = keyToHealthDataType(type) - val field = getField(type) - - val typesBuilder = FitnessOptions.builder() - typesBuilder.addDataType(dataType, FitnessOptions.ACCESS_WRITE) - - val dataSource = - DataSource.Builder() - .setDataType(dataType) - .setType(DataSource.TYPE_RAW) - .setDevice( - Device.getLocalDevice( - context!!.applicationContext - ) - ) - .setAppPackageName(context!!.applicationContext) - .build() - - val builder = - if (startTime == endTime) { - DataPoint.builder(dataSource) - .setTimestamp( - startTime, - TimeUnit.MILLISECONDS - ) - } else { - DataPoint.builder(dataSource) - .setTimeInterval( - startTime, - endTime, - TimeUnit.MILLISECONDS - ) - } + Handler(context!!.mainLooper).run { result.success(healthConnectData) } + } - // Conversion is needed because glucose is stored as mmoll in Google Fit; - // while mgdl is used for glucose in this plugin. - val isGlucose = field == HealthFields.FIELD_BLOOD_GLUCOSE_LEVEL - val dataPoint = - if (!isIntField(dataSource, field)) { - builder.setField( - field, - (if (!isGlucose) value - else - (value / - MMOLL_2_MGDL) - .toFloat()) - ) - .build() - } else { - builder.setField(field, value.toInt()).build() - } - val dataSet = DataSet.builder(dataSource).add(dataPoint).build() + private fun getSensorDataCount(call: MethodCall, result: Result) { + val startTime = Instant.ofEpochMilli(call.argument("startTime")!!) + val endTime = Instant.ofEpochMilli(call.argument("endTime")!!) - if (dataType == DataType.TYPE_SLEEP_SEGMENT) { - typesBuilder.accessSleepSessions(FitnessOptions.ACCESS_READ) - } - val fitnessOptions = typesBuilder.build() - try { - val googleSignInAccount = - GoogleSignIn.getAccountForExtension( - context!!.applicationContext, - fitnessOptions - ) - Fitness.getHistoryClient(context!!.applicationContext, googleSignInAccount) - .insertData(dataSet) - .addOnSuccessListener { - Log.i( - "FLUTTER_HEALTH::SUCCESS", - "Dataset added successfully!" - ) - result.success(true) - } - .addOnFailureListener( - errHandler( - result, - "There was an error adding the dataset" - ) - ) - } catch (e3: Exception) { - result.success(false) - } + val items = StepCounterService.box.boxFor(SensorStep::class.java).query(SensorStep_.startTime.between(startTime.toEpochMilli(),endTime.toEpochMilli())).build().find() + var totalResult = 0.0 + items.forEach { + totalResult += it.count; } - /** - * Save the blood oxygen saturation, in Google Fit with the supplemental flow rate, in - * HealthConnect without - */ - private fun writeBloodOxygen(call: MethodCall, result: Result) { - // Health Connect does not support supplemental flow rate, thus it is ignored - if (useHealthConnectIfAvailable && healthConnectAvailable) { - writeHCData(call, result) - return - } + Handler(context!!.mainLooper).run { result.success(totalResult.toInt()) } + } - if (context == null) { - result.success(false) - return - } + private fun getIntervalData(call: MethodCall, result: Result) { + if (useHealthConnectIfAvailable && healthConnectAvailable) { + getAggregateHCData(call, result) + return + } - val dataType = HealthDataTypes.TYPE_OXYGEN_SATURATION - val startTime = call.argument("startTime")!! - val endTime = call.argument("endTime")!! - val saturation = call.argument("value")!! - val flowRate = call.argument("flowRate")!! - - val typesBuilder = FitnessOptions.builder() - typesBuilder.addDataType(dataType, FitnessOptions.ACCESS_WRITE) - - val dataSource = - DataSource.Builder() - .setDataType(dataType) - .setType(DataSource.TYPE_RAW) - .setDevice( - Device.getLocalDevice( - context!!.applicationContext - ) - ) - .setAppPackageName(context!!.applicationContext) - .build() - - val builder = - if (startTime == endTime) { - DataPoint.builder(dataSource) - .setTimestamp( - startTime, - TimeUnit.MILLISECONDS - ) - } else { - DataPoint.builder(dataSource) - .setTimeInterval( - startTime, - endTime, - TimeUnit.MILLISECONDS - ) - } + if (context == null) { + result.success(null) + return + } + + val type = call.argument("dataTypeKey")!! + val startTime = call.argument("startTime")!! + val endTime = call.argument("endTime")!! + val interval = call.argument("interval")!! + val includeManualEntry = call.argument("includeManualEntry")!! + + // Look up data type and unit for the type key + val dataType = keyToHealthDataType(type) + val field = getField(type) + val typesBuilder = FitnessOptions.builder() + typesBuilder.addDataType(dataType) + if (dataType == DataType.TYPE_SLEEP_SEGMENT) { + typesBuilder.accessSleepSessions(FitnessOptions.ACCESS_READ) + } + val fitnessOptions = typesBuilder.build() + val googleSignInAccount = + GoogleSignIn.getAccountForExtension( + context!!.applicationContext, + fitnessOptions + ) + + Fitness.getHistoryClient(context!!.applicationContext, googleSignInAccount) + .readData( + DataReadRequest.Builder() + .aggregate(dataType) + .bucketByTime( + interval, + TimeUnit.SECONDS + ) + .setTimeRange( + startTime, + endTime, + TimeUnit.MILLISECONDS + ) + .build() + ) + .addOnSuccessListener( + threadPoolExecutor!!, + intervalDataHandler( + dataType, + field, + includeManualEntry, + result + ) + ) + .addOnFailureListener( + errHandler( + result, + "There was an error getting the interval data!" + ) + ) + } - builder.setField(HealthFields.FIELD_SUPPLEMENTAL_OXYGEN_FLOW_RATE, flowRate) - builder.setField(HealthFields.FIELD_OXYGEN_SATURATION, saturation) + private fun getAggregateData(call: MethodCall, result: Result) { + if (context == null) { + result.success(null) + return + } - val dataPoint = builder.build() - val dataSet = DataSet.builder(dataSource).add(dataPoint).build() + val types = call.argument>("dataTypeKeys")!! + val startTime = call.argument("startTime")!! + val endTime = call.argument("endTime")!! + val activitySegmentDuration = call.argument("activitySegmentDuration")!! + val includeManualEntry = call.argument("includeManualEntry")!! - val fitnessOptions = typesBuilder.build() - try { - val googleSignInAccount = - GoogleSignIn.getAccountForExtension( - context!!.applicationContext, - fitnessOptions - ) - Fitness.getHistoryClient(context!!.applicationContext, googleSignInAccount) - .insertData(dataSet) - .addOnSuccessListener { - Log.i( - "FLUTTER_HEALTH::SUCCESS", - "Blood Oxygen added successfully!" - ) - result.success(true) - } - .addOnFailureListener( - errHandler( - result, - "There was an error adding the blood oxygen data!", - ), - ) - } catch (e3: Exception) { - result.success(false) - } + val typesBuilder = FitnessOptions.builder() + for (type in types) { + val dataType = keyToHealthDataType(type) + typesBuilder.addDataType(dataType) } + val fitnessOptions = typesBuilder.build() + val googleSignInAccount = + GoogleSignIn.getAccountForExtension( + context!!.applicationContext, + fitnessOptions + ) + + val readWorkoutsRequest = + DataReadRequest.Builder() + .bucketByActivitySegment( + activitySegmentDuration, + TimeUnit.SECONDS + ) + .setTimeRange( + startTime, + endTime, + TimeUnit.MILLISECONDS + ) - /** Save a Workout session with options for distance and calories expended */ - private fun writeWorkoutData(call: MethodCall, result: Result) { - if (useHealthConnectIfAvailable && healthConnectAvailable) { - writeWorkoutHCData(call, result) - return - } - if (context == null) { - result.success(false) - return - } + for (type in types) { + val dataType = keyToHealthDataType(type) + readWorkoutsRequest.aggregate(dataType) + } - val type = call.argument("activityType")!! - val startTime = call.argument("startTime")!! - val endTime = call.argument("endTime")!! - val totalEnergyBurned = call.argument("totalEnergyBurned") - val totalDistance = call.argument("totalDistance") - - val activityType = getActivityType(type) - // Create the Activity Segment DataSource - val activitySegmentDataSource = - DataSource.Builder() - .setAppPackageName(context!!.packageName) - .setDataType(DataType.TYPE_ACTIVITY_SEGMENT) - .setStreamName("FLUTTER_HEALTH - Activity") - .setType(DataSource.TYPE_RAW) - .build() - // Create the Activity Segment - val activityDataPoint = - DataPoint.builder(activitySegmentDataSource) - .setTimeInterval( - startTime, - endTime, - TimeUnit.MILLISECONDS - ) - .setActivityField( - Field.FIELD_ACTIVITY, - activityType - ) - .build() - // Add DataPoint to DataSet - val activitySegments = - DataSet.builder(activitySegmentDataSource) - .add(activityDataPoint) - .build() - - // If distance is provided - var distanceDataSet: DataSet? = null - if (totalDistance != null) { - // Create a data source - val distanceDataSource = - DataSource.Builder() - .setAppPackageName(context!!.packageName) - .setDataType(DataType.TYPE_DISTANCE_DELTA) - .setStreamName("FLUTTER_HEALTH - Distance") - .setType(DataSource.TYPE_RAW) - .build() - - val distanceDataPoint = - DataPoint.builder(distanceDataSource) - .setTimeInterval( - startTime, - endTime, - TimeUnit.MILLISECONDS - ) - .setField( - Field.FIELD_DISTANCE, - totalDistance.toFloat() - ) - .build() - // Create a data set - distanceDataSet = - DataSet.builder(distanceDataSource) - .add(distanceDataPoint) - .build() - } - // If energyBurned is provided - var energyDataSet: DataSet? = null - if (totalEnergyBurned != null) { - // Create a data source - val energyDataSource = - DataSource.Builder() - .setAppPackageName(context!!.packageName) - .setDataType( - DataType.TYPE_CALORIES_EXPENDED - ) - .setStreamName("FLUTTER_HEALTH - Calories") - .setType(DataSource.TYPE_RAW) - .build() - - val energyDataPoint = - DataPoint.builder(energyDataSource) - .setTimeInterval( - startTime, - endTime, - TimeUnit.MILLISECONDS - ) - .setField( - Field.FIELD_CALORIES, - totalEnergyBurned.toFloat() - ) - .build() - // Create a data set - energyDataSet = - DataSet.builder(energyDataSource) - .add(energyDataPoint) - .build() + Fitness.getHistoryClient(context!!.applicationContext, googleSignInAccount) + .readData(readWorkoutsRequest.build()) + .addOnSuccessListener( + threadPoolExecutor!!, + aggregateDataHandler(includeManualEntry, result) + ) + .addOnFailureListener( + errHandler( + result, + "There was an error getting the aggregate data!" + ) + ) + } + + private fun dataHandler( + dataType: DataType, + field: Field, + includeManualEntry: Boolean, + result: Result + ) = OnSuccessListener { response: DataReadResponse -> + // / Fetch all data points for the specified DataType + val dataSet = response.getDataSet(dataType) + /// For each data point, extract the contents and send them to Flutter, along with + // date and unit. + var dataPoints = dataSet.dataPoints + if (!includeManualEntry) { + dataPoints = + dataPoints.filterIndexed { _, dataPoint -> + !dataPoint.originalDataSource.streamName.contains( + "user_input" + ) } + } + // For each data point, extract the contents and send them to Flutter, along with + // date and unit. + val healthData = + dataPoints.mapIndexed { _, dataPoint -> + return@mapIndexed hashMapOf( + "value" to + getHealthDataValue( + dataPoint, + field + ), + "date_from" to + dataPoint.getStartTime( + TimeUnit.MILLISECONDS + ), + "date_to" to + dataPoint.getEndTime( + TimeUnit.MILLISECONDS + ), + "source_name" to + (dataPoint.originalDataSource + .appPackageName + ?: (dataPoint.originalDataSource + .device + ?.model + ?: "")), + "source_id" to + dataPoint.originalDataSource + .streamIdentifier, + ) + } + Handler(context!!.mainLooper).run { result.success(healthData) } + } + + private fun errHandler(result: Result, addMessage: String) = + OnFailureListener { exception -> + Handler(context!!.mainLooper).run { result.success(null) } + Log.w("FLUTTER_HEALTH::ERROR", addMessage) + Log.w("FLUTTER_HEALTH::ERROR", exception.message ?: "unknown error") + Log.w("FLUTTER_HEALTH::ERROR", exception.stackTrace.toString()) + } - // Finish session setup - val session = - Session.Builder() - .setName( - activityType - ) // TODO: Make a sensible name / allow user to set - // name - .setDescription("") - .setIdentifier(UUID.randomUUID().toString()) - .setActivity(activityType) - .setStartTime(startTime, TimeUnit.MILLISECONDS) - .setEndTime(endTime, TimeUnit.MILLISECONDS) - .build() - // Build a session and add the values provided - val sessionInsertRequestBuilder = - SessionInsertRequest.Builder() - .setSession(session) - .addDataSet(activitySegments) - if (totalDistance != null) { - sessionInsertRequestBuilder.addDataSet(distanceDataSet!!) - } - if (totalEnergyBurned != null) { - sessionInsertRequestBuilder.addDataSet(energyDataSet!!) + private fun sleepDataHandler(type: String, result: Result) = + OnSuccessListener { response: SessionReadResponse -> + val healthData: MutableList> = mutableListOf() + for (session in response.sessions) { + // Return sleep time in Minutes if requested ASLEEP data + if (type == SLEEP_ASLEEP) { + healthData.add( + hashMapOf( + "value" to + session.getEndTime( + TimeUnit.MINUTES + ) - + session.getStartTime( + TimeUnit.MINUTES, + ), + "date_from" to + session.getStartTime( + TimeUnit.MILLISECONDS + ), + "date_to" to + session.getEndTime( + TimeUnit.MILLISECONDS + ), + "unit" to "MINUTES", + "source_name" to + session.appPackageName, + "source_id" to + session.identifier, + ), + ) } - val insertRequest = sessionInsertRequestBuilder.build() - val fitnessOptionsBuilder = - FitnessOptions.builder() - .addDataType( - DataType.TYPE_ACTIVITY_SEGMENT, - FitnessOptions.ACCESS_WRITE - ) - if (totalDistance != null) { - fitnessOptionsBuilder.addDataType( - DataType.TYPE_DISTANCE_DELTA, - FitnessOptions.ACCESS_WRITE, - ) - } - if (totalEnergyBurned != null) { - fitnessOptionsBuilder.addDataType( - DataType.TYPE_CALORIES_EXPENDED, - FitnessOptions.ACCESS_WRITE, + if (type == SLEEP_IN_BED) { + val dataSets = response.getDataSet(session) + + // If the sleep session has finer granularity + // sub-components, extract them: + if (dataSets.isNotEmpty()) { + for (dataSet in dataSets) { + for (dataPoint in + dataSet.dataPoints) { + // searching OUT OF BED data + if (dataPoint.getValue( + Field.FIELD_SLEEP_SEGMENT_TYPE + ) + .asInt() != + 3 + ) { + healthData.add( + hashMapOf( + "value" to + dataPoint.getEndTime( + TimeUnit.MINUTES + ) - + dataPoint.getStartTime( + TimeUnit.MINUTES, + ), + "date_from" to + dataPoint.getStartTime( + TimeUnit.MILLISECONDS + ), + "date_to" to + dataPoint.getEndTime( + TimeUnit.MILLISECONDS + ), + "unit" to + "MINUTES", + "source_name" to + (dataPoint.originalDataSource + .appPackageName + ?: (dataPoint.originalDataSource + .device + ?.model + ?: "unknown")), + "source_id" to + dataPoint.originalDataSource + .streamIdentifier, + ), + ) + } + } + } + } else { + healthData.add( + hashMapOf( + "value" to + session.getEndTime( + TimeUnit.MINUTES + ) - + session.getStartTime( + TimeUnit.MINUTES, + ), + "date_from" to + session.getStartTime( + TimeUnit.MILLISECONDS + ), + "date_to" to + session.getEndTime( + TimeUnit.MILLISECONDS + ), + "unit" to + "MINUTES", + "source_name" to + session.appPackageName, + "source_id" to + session.identifier, + ), ) + } } - val fitnessOptions = fitnessOptionsBuilder.build() - try { - val googleSignInAccount = - GoogleSignIn.getAccountForExtension( - context!!.applicationContext, - fitnessOptions - ) - Fitness.getSessionsClient( - context!!.applicationContext, - googleSignInAccount, - ) - .insertSession(insertRequest) - .addOnSuccessListener { - Log.i( - "FLUTTER_HEALTH::SUCCESS", - "Workout was successfully added!" - ) - result.success(true) - } - .addOnFailureListener( - errHandler( - result, - "There was an error adding the workout" - ) - ) - } catch (e: Exception) { - result.success(false) + if (type == SLEEP_AWAKE) { + val dataSets = response.getDataSet(session) + for (dataSet in dataSets) { + for (dataPoint in dataSet.dataPoints) { + // searching SLEEP AWAKE data + if (dataPoint.getValue( + Field.FIELD_SLEEP_SEGMENT_TYPE + ) + .asInt() == + 1 + ) { + healthData.add( + hashMapOf( + "value" to + dataPoint.getEndTime( + TimeUnit.MINUTES + ) - + dataPoint.getStartTime( + TimeUnit.MINUTES, + ), + "date_from" to + dataPoint.getStartTime( + TimeUnit.MILLISECONDS + ), + "date_to" to + dataPoint.getEndTime( + TimeUnit.MILLISECONDS + ), + "unit" to + "MINUTES", + "source_name" to + (dataPoint.originalDataSource + .appPackageName + ?: (dataPoint.originalDataSource + .device + ?.model + ?: "unknown")), + "source_id" to + dataPoint.originalDataSource + .streamIdentifier, + ), + ) + } + } + } } + } + Handler(context!!.mainLooper).run { result.success(healthData) } } - /** Get all datapoints of the DataType within the given time range */ - private fun getData(call: MethodCall, result: Result) { - if (useHealthConnectIfAvailable && healthConnectAvailable) { - getHCData(call, result) - return + private fun intervalDataHandler( + dataType: DataType, + field: Field, + includeManualEntry: Boolean, + result: Result + ) = OnSuccessListener { response: DataReadResponse -> + val healthData = mutableListOf>() + for (bucket in response.buckets) { + /// Fetch all data points for the specified DataType + // val dataSet = response.getDataSet(dataType) + for (dataSet in bucket.dataSets) { + /// For each data point, extract the contents and send them to + // Flutter, along with + // date and unit. + var dataPoints = dataSet.dataPoints + if (!includeManualEntry) { + dataPoints = + dataPoints.filterIndexed { _, dataPoint -> + !dataPoint.originalDataSource + .streamName + .contains( + "user_input" + ) + } } - - if (context == null) { - result.success(null) - return + for (dataPoint in dataPoints) { + for (field in dataPoint.dataType.fields) { + val healthDataItems = + dataPoints.mapIndexed { _, dataPoint + -> + return@mapIndexed hashMapOf( + "value" to + getHealthDataValue( + dataPoint, + field + ), + "date_from" to + dataPoint.getStartTime( + TimeUnit.MILLISECONDS + ), + "date_to" to + dataPoint.getEndTime( + TimeUnit.MILLISECONDS + ), + "source_name" to + (dataPoint.originalDataSource + .appPackageName + ?: (dataPoint.originalDataSource + .device + ?.model + ?: "")), + "source_id" to + dataPoint.originalDataSource + .streamIdentifier, + "is_manual_entry" to + dataPoint.originalDataSource + .streamName + .contains( + "user_input" + ) + ) + } + healthData.addAll(healthDataItems) + } } + } + } + Handler(context!!.mainLooper).run { result.success(healthData) } + } + + private fun aggregateDataHandler(includeManualEntry: Boolean, result: Result) = + OnSuccessListener { response: DataReadResponse -> + val healthData = mutableListOf>() + for (bucket in response.buckets) { + var sourceName: Any = "" + var sourceId: Any = "" + var isManualEntry: Any = false + var totalSteps: Any = 0 + var totalDistance: Any = 0 + var totalEnergyBurned: Any = 0 + /// Fetch all data points for the specified DataType + for (dataSet in bucket.dataSets) { + /// For each data point, extract the contents and + // send them to Flutter, + // along with date and unit. + var dataPoints = dataSet.dataPoints + if (!includeManualEntry) { + dataPoints = + dataPoints.filterIndexed { _, + dataPoint -> + !dataPoint.originalDataSource + .streamName + .contains( + "user_input" + ) + } + } + for (dataPoint in dataPoints) { + sourceName = + (dataPoint.originalDataSource + .appPackageName + ?: (dataPoint.originalDataSource + .device + ?.model + ?: "")) + sourceId = + dataPoint.originalDataSource + .streamIdentifier + isManualEntry = + dataPoint.originalDataSource + .streamName + .contains( + "user_input" + ) + for (field in dataPoint.dataType.fields) { + when (field) { + getField(STEPS) -> { + totalSteps = + getHealthDataValue( + dataPoint, + field + ) + } - val type = call.argument("dataTypeKey")!! - val startTime = call.argument("startTime")!! - val endTime = call.argument("endTime")!! - val includeManualEntry = call.argument("includeManualEntry")!! - // Look up data type and unit for the type key - val dataType = keyToHealthDataType(type) - val field = getField(type) - val typesBuilder = FitnessOptions.builder() - typesBuilder.addDataType(dataType) - - // Add special cases for accessing workouts or sleep data. - if (dataType == DataType.TYPE_SLEEP_SEGMENT) { - typesBuilder.accessSleepSessions(FitnessOptions.ACCESS_READ) - } else if (dataType == DataType.TYPE_ACTIVITY_SEGMENT) { - typesBuilder.accessActivitySessions(FitnessOptions.ACCESS_READ) - .addDataType( - DataType.TYPE_CALORIES_EXPENDED, - FitnessOptions.ACCESS_READ + getField( + DISTANCE_DELTA + ) -> { + totalDistance = + getHealthDataValue( + dataPoint, + field ) - .addDataType( - DataType.TYPE_DISTANCE_DELTA, - FitnessOptions.ACCESS_READ + } + + getField( + ACTIVE_ENERGY_BURNED + ) -> { + totalEnergyBurned = + getHealthDataValue( + dataPoint, + field ) - } - val fitnessOptions = typesBuilder.build() - val googleSignInAccount = - GoogleSignIn.getAccountForExtension( - context!!.applicationContext, - fitnessOptions - ) - // Handle data types - when (dataType) { - DataType.TYPE_SLEEP_SEGMENT -> { - // request to the sessions for sleep data - val request = - SessionReadRequest.Builder() - .setTimeInterval( - startTime, - endTime, - TimeUnit.MILLISECONDS - ) - .enableServerQueries() - .readSessionsFromAllApps() - .includeSleepSessions() - .build() - Fitness.getSessionsClient( - context!!.applicationContext, - googleSignInAccount - ) - .readSession(request) - .addOnSuccessListener( - threadPoolExecutor!!, - sleepDataHandler(type, result) - ) - .addOnFailureListener( - errHandler( - result, - "There was an error getting the sleeping data!", - ), - ) - } - DataType.TYPE_ACTIVITY_SEGMENT -> { - val readRequest: SessionReadRequest - val readRequestBuilder = - SessionReadRequest.Builder() - .setTimeInterval( - startTime, - endTime, - TimeUnit.MILLISECONDS - ) - .enableServerQueries() - .readSessionsFromAllApps() - .includeActivitySessions() - .read(dataType) - .read( - DataType.TYPE_CALORIES_EXPENDED - ) - - // If fine location is enabled, read distance data - if (ContextCompat.checkSelfPermission( - context!!.applicationContext, - android.Manifest.permission - .ACCESS_FINE_LOCATION, - ) == PackageManager.PERMISSION_GRANTED - ) { - readRequestBuilder.read(DataType.TYPE_DISTANCE_DELTA) } - readRequest = readRequestBuilder.build() - Fitness.getSessionsClient( - context!!.applicationContext, - googleSignInAccount - ) - .readSession(readRequest) - .addOnSuccessListener( - threadPoolExecutor!!, - workoutDataHandler(type, result) - ) - .addOnFailureListener( - errHandler( - result, - "There was an error getting the workout data!", - ), - ) - } - else -> { - Fitness.getHistoryClient( - context!!.applicationContext, - googleSignInAccount - ) - .readData( - DataReadRequest.Builder() - .read(dataType) - .setTimeRange( - startTime, - endTime, - TimeUnit.MILLISECONDS - ) - .build(), - ) - .addOnSuccessListener( - threadPoolExecutor!!, - dataHandler( - dataType, - field, - includeManualEntry, - result - ), - ) - .addOnFailureListener( - errHandler( - result, - "There was an error getting the data!", - ), - ) + } } + } } + val healthDataItems = + hashMapOf( + "value" to + bucket.getEndTime( + TimeUnit.MINUTES + ) - + bucket.getStartTime( + TimeUnit.MINUTES + ), + "date_from" to + bucket.getStartTime( + TimeUnit.MILLISECONDS + ), + "date_to" to + bucket.getEndTime( + TimeUnit.MILLISECONDS + ), + "source_name" to sourceName, + "source_id" to sourceId, + "is_manual_entry" to + isManualEntry, + "workout_type" to + bucket.activity + .toLowerCase(), + "total_steps" to totalSteps, + "total_distance" to + totalDistance, + "total_energy_burned" to + totalEnergyBurned + ) + healthData.add(healthDataItems) + } + Handler(context!!.mainLooper).run { result.success(healthData) } } - private fun getIntervalData(call: MethodCall, result: Result) { - if (useHealthConnectIfAvailable && healthConnectAvailable) { - getAggregateHCData(call, result) - return - } - - if (context == null) { - result.success(null) - return - } - - val type = call.argument("dataTypeKey")!! - val startTime = call.argument("startTime")!! - val endTime = call.argument("endTime")!! - val interval = call.argument("interval")!! - val includeManualEntry = call.argument("includeManualEntry")!! - - // Look up data type and unit for the type key - val dataType = keyToHealthDataType(type) - val field = getField(type) - val typesBuilder = FitnessOptions.builder() - typesBuilder.addDataType(dataType) - if (dataType == DataType.TYPE_SLEEP_SEGMENT) { - typesBuilder.accessSleepSessions(FitnessOptions.ACCESS_READ) - } - val fitnessOptions = typesBuilder.build() - val googleSignInAccount = - GoogleSignIn.getAccountForExtension( - context!!.applicationContext, - fitnessOptions + private fun workoutDataHandler(type: String, result: Result) = + OnSuccessListener { response: SessionReadResponse -> + val healthData: MutableList> = mutableListOf() + for (session in response.sessions) { + // Look for calories and distance if they + var totalEnergyBurned = 0.0 + var totalDistance = 0.0 + for (dataSet in response.getDataSet(session)) { + if (dataSet.dataType == + DataType.TYPE_CALORIES_EXPENDED + ) { + for (dataPoint in dataSet.dataPoints) { + totalEnergyBurned += + dataPoint.getValue( + Field.FIELD_CALORIES ) - - Fitness.getHistoryClient(context!!.applicationContext, googleSignInAccount) - .readData( - DataReadRequest.Builder() - .aggregate(dataType) - .bucketByTime( - interval, - TimeUnit.SECONDS - ) - .setTimeRange( - startTime, - endTime, - TimeUnit.MILLISECONDS - ) - .build() + .toString() + .toDouble() + } + } + if (dataSet.dataType == DataType.TYPE_DISTANCE_DELTA + ) { + for (dataPoint in dataSet.dataPoints) { + totalDistance += + dataPoint.getValue( + Field.FIELD_DISTANCE ) - .addOnSuccessListener( - threadPoolExecutor!!, - intervalDataHandler( - dataType, - field, - includeManualEntry, - result - ) + .toString() + .toDouble() + } + } + } + healthData.add( + hashMapOf( + "workoutActivityType" to + (workoutTypeMap + .filterValues { + it == + session.activity + } + .keys + .firstOrNull() + ?: "OTHER"), + "totalEnergyBurned" to + if (totalEnergyBurned == + 0.0 ) - .addOnFailureListener( - errHandler( - result, - "There was an error getting the interval data!" - ) + null + else + totalEnergyBurned, + "totalEnergyBurnedUnit" to + "KILOCALORIE", + "totalDistance" to + if (totalDistance == + 0.0 ) + null + else + totalDistance, + "totalDistanceUnit" to + "METER", + "date_from" to + session.getStartTime( + TimeUnit.MILLISECONDS + ), + "date_to" to + session.getEndTime( + TimeUnit.MILLISECONDS + ), + "unit" to "MINUTES", + "source_name" to + session.appPackageName, + "source_id" to + session.identifier, + ), + ) + } + Handler(context!!.mainLooper).run { result.success(healthData) } } - private fun getAggregateData(call: MethodCall, result: Result) { - if (context == null) { - result.success(null) - return + private fun callToHealthTypes(call: MethodCall): FitnessOptions { + val typesBuilder = FitnessOptions.builder() + val args = call.arguments as HashMap<*, *> + val types = (args["types"] as? ArrayList<*>)?.filterIsInstance() + val permissions = (args["permissions"] as? ArrayList<*>)?.filterIsInstance() + + assert(types != null) + assert(permissions != null) + assert(types!!.count() == permissions!!.count()) + + for ((i, typeKey) in types.withIndex()) { + val access = permissions[i] + val dataType = keyToHealthDataType(typeKey) + when (access) { + 0 -> typesBuilder.addDataType(dataType, FitnessOptions.ACCESS_READ) + 1 -> typesBuilder.addDataType(dataType, FitnessOptions.ACCESS_WRITE) + 2 -> { + typesBuilder.addDataType( + dataType, + FitnessOptions.ACCESS_READ + ) + typesBuilder.addDataType( + dataType, + FitnessOptions.ACCESS_WRITE + ) } - val types = call.argument>("dataTypeKeys")!! - val startTime = call.argument("startTime")!! - val endTime = call.argument("endTime")!! - val activitySegmentDuration = call.argument("activitySegmentDuration")!! - val includeManualEntry = call.argument("includeManualEntry")!! + else -> + throw IllegalArgumentException( + "Unknown access type $access" + ) + } + if (typeKey == SLEEP_ASLEEP || + typeKey == SLEEP_AWAKE || + typeKey == SLEEP_IN_BED + ) { + typesBuilder.accessSleepSessions(FitnessOptions.ACCESS_READ) + when (access) { + 0 -> + typesBuilder.accessSleepSessions( + FitnessOptions.ACCESS_READ + ) - val typesBuilder = FitnessOptions.builder() - for (type in types) { - val dataType = keyToHealthDataType(type) - typesBuilder.addDataType(dataType) - } - val fitnessOptions = typesBuilder.build() - val googleSignInAccount = - GoogleSignIn.getAccountForExtension( - context!!.applicationContext, - fitnessOptions - ) + 1 -> + typesBuilder.accessSleepSessions( + FitnessOptions.ACCESS_WRITE + ) - val readWorkoutsRequest = - DataReadRequest.Builder() - .bucketByActivitySegment( - activitySegmentDuration, - TimeUnit.SECONDS - ) - .setTimeRange( - startTime, - endTime, - TimeUnit.MILLISECONDS - ) + 2 -> { + typesBuilder.accessSleepSessions( + FitnessOptions.ACCESS_READ + ) + typesBuilder.accessSleepSessions( + FitnessOptions.ACCESS_WRITE + ) + } - for (type in types) { - val dataType = keyToHealthDataType(type) - readWorkoutsRequest.aggregate(dataType) + else -> + throw IllegalArgumentException( + "Unknown access type $access" + ) } + } + if (typeKey == WORKOUT) { + when (access) { + 0 -> + typesBuilder.accessActivitySessions( + FitnessOptions.ACCESS_READ + ) - Fitness.getHistoryClient(context!!.applicationContext, googleSignInAccount) - .readData(readWorkoutsRequest.build()) - .addOnSuccessListener( - threadPoolExecutor!!, - aggregateDataHandler(includeManualEntry, result) - ) - .addOnFailureListener( - errHandler( - result, - "There was an error getting the aggregate data!" - ) - ) - } + 1 -> + typesBuilder.accessActivitySessions( + FitnessOptions.ACCESS_WRITE + ) - private fun dataHandler( - dataType: DataType, - field: Field, - includeManualEntry: Boolean, - result: Result - ) = OnSuccessListener { response: DataReadResponse -> - // / Fetch all data points for the specified DataType - val dataSet = response.getDataSet(dataType) - /// For each data point, extract the contents and send them to Flutter, along with - // date and unit. - var dataPoints = dataSet.dataPoints - if (!includeManualEntry) { - dataPoints = - dataPoints.filterIndexed { _, dataPoint -> - !dataPoint.originalDataSource.streamName.contains( - "user_input" - ) - } + 2 -> { + typesBuilder.accessActivitySessions( + FitnessOptions.ACCESS_READ + ) + typesBuilder.accessActivitySessions( + FitnessOptions.ACCESS_WRITE + ) + } + + else -> + throw IllegalArgumentException( + "Unknown access type $access" + ) } - // For each data point, extract the contents and send them to Flutter, along with - // date and unit. - val healthData = - dataPoints.mapIndexed { _, dataPoint -> - return@mapIndexed hashMapOf( - "value" to - getHealthDataValue( - dataPoint, - field - ), - "date_from" to - dataPoint.getStartTime( - TimeUnit.MILLISECONDS - ), - "date_to" to - dataPoint.getEndTime( - TimeUnit.MILLISECONDS - ), - "source_name" to - (dataPoint.originalDataSource - .appPackageName - ?: (dataPoint.originalDataSource - .device - ?.model - ?: "")), - "source_id" to - dataPoint.originalDataSource - .streamIdentifier, - ) - } - Handler(context!!.mainLooper).run { result.success(healthData) } + } } + return typesBuilder.build() + } - private fun errHandler(result: Result, addMessage: String) = - OnFailureListener { exception -> - Handler(context!!.mainLooper).run { result.success(null) } - Log.w("FLUTTER_HEALTH::ERROR", addMessage) - Log.w("FLUTTER_HEALTH::ERROR", exception.message ?: "unknown error") - Log.w("FLUTTER_HEALTH::ERROR", exception.stackTrace.toString()) - } - - private fun sleepDataHandler(type: String, result: Result) = - OnSuccessListener { response: SessionReadResponse -> - val healthData: MutableList> = mutableListOf() - for (session in response.sessions) { - // Return sleep time in Minutes if requested ASLEEP data - if (type == SLEEP_ASLEEP) { - healthData.add( - hashMapOf( - "value" to - session.getEndTime( - TimeUnit.MINUTES - ) - - session.getStartTime( - TimeUnit.MINUTES, - ), - "date_from" to - session.getStartTime( - TimeUnit.MILLISECONDS - ), - "date_to" to - session.getEndTime( - TimeUnit.MILLISECONDS - ), - "unit" to "MINUTES", - "source_name" to - session.appPackageName, - "source_id" to - session.identifier, - ), - ) - } - - if (type == SLEEP_IN_BED) { - val dataSets = response.getDataSet(session) - - // If the sleep session has finer granularity - // sub-components, extract them: - if (dataSets.isNotEmpty()) { - for (dataSet in dataSets) { - for (dataPoint in - dataSet.dataPoints) { - // searching OUT OF BED data - if (dataPoint.getValue( - Field.FIELD_SLEEP_SEGMENT_TYPE - ) - .asInt() != - 3 - ) { - healthData.add( - hashMapOf( - "value" to - dataPoint.getEndTime( - TimeUnit.MINUTES - ) - - dataPoint.getStartTime( - TimeUnit.MINUTES, - ), - "date_from" to - dataPoint.getStartTime( - TimeUnit.MILLISECONDS - ), - "date_to" to - dataPoint.getEndTime( - TimeUnit.MILLISECONDS - ), - "unit" to - "MINUTES", - "source_name" to - (dataPoint.originalDataSource - .appPackageName - ?: (dataPoint.originalDataSource - .device - ?.model - ?: "unknown")), - "source_id" to - dataPoint.originalDataSource - .streamIdentifier, - ), - ) - } - } - } - } else { - healthData.add( - hashMapOf( - "value" to - session.getEndTime( - TimeUnit.MINUTES - ) - - session.getStartTime( - TimeUnit.MINUTES, - ), - "date_from" to - session.getStartTime( - TimeUnit.MILLISECONDS - ), - "date_to" to - session.getEndTime( - TimeUnit.MILLISECONDS - ), - "unit" to - "MINUTES", - "source_name" to - session.appPackageName, - "source_id" to - session.identifier, - ), - ) - } - } - - if (type == SLEEP_AWAKE) { - val dataSets = response.getDataSet(session) - for (dataSet in dataSets) { - for (dataPoint in dataSet.dataPoints) { - // searching SLEEP AWAKE data - if (dataPoint.getValue( - Field.FIELD_SLEEP_SEGMENT_TYPE - ) - .asInt() == - 1 - ) { - healthData.add( - hashMapOf( - "value" to - dataPoint.getEndTime( - TimeUnit.MINUTES - ) - - dataPoint.getStartTime( - TimeUnit.MINUTES, - ), - "date_from" to - dataPoint.getStartTime( - TimeUnit.MILLISECONDS - ), - "date_to" to - dataPoint.getEndTime( - TimeUnit.MILLISECONDS - ), - "unit" to - "MINUTES", - "source_name" to - (dataPoint.originalDataSource - .appPackageName - ?: (dataPoint.originalDataSource - .device - ?.model - ?: "unknown")), - "source_id" to - dataPoint.originalDataSource - .streamIdentifier, - ), - ) - } - } - } - } - } - Handler(context!!.mainLooper).run { result.success(healthData) } - } + private fun hasPermissions(call: MethodCall, result: Result) { + if (useHealthConnectIfAvailable && healthConnectAvailable) { + hasPermissionsHC(call, result) + return + } + if (context == null) { + result.success(false) + return + } - private fun intervalDataHandler( - dataType: DataType, - field: Field, - includeManualEntry: Boolean, - result: Result - ) = OnSuccessListener { response: DataReadResponse -> - val healthData = mutableListOf>() - for (bucket in response.buckets) { - /// Fetch all data points for the specified DataType - // val dataSet = response.getDataSet(dataType) - for (dataSet in bucket.dataSets) { - /// For each data point, extract the contents and send them to - // Flutter, along with - // date and unit. - var dataPoints = dataSet.dataPoints - if (!includeManualEntry) { - dataPoints = - dataPoints.filterIndexed { _, dataPoint -> - !dataPoint.originalDataSource - .streamName - .contains( - "user_input" - ) - } - } - for (dataPoint in dataPoints) { - for (field in dataPoint.dataType.fields) { - val healthDataItems = - dataPoints.mapIndexed { _, dataPoint - -> - return@mapIndexed hashMapOf( - "value" to - getHealthDataValue( - dataPoint, - field - ), - "date_from" to - dataPoint.getStartTime( - TimeUnit.MILLISECONDS - ), - "date_to" to - dataPoint.getEndTime( - TimeUnit.MILLISECONDS - ), - "source_name" to - (dataPoint.originalDataSource - .appPackageName - ?: (dataPoint.originalDataSource - .device - ?.model - ?: "")), - "source_id" to - dataPoint.originalDataSource - .streamIdentifier, - "is_manual_entry" to - dataPoint.originalDataSource - .streamName - .contains( - "user_input" - ) - ) - } - healthData.addAll(healthDataItems) - } - } - } - } - Handler(context!!.mainLooper).run { result.success(healthData) } - } - - private fun aggregateDataHandler(includeManualEntry: Boolean, result: Result) = - OnSuccessListener { response: DataReadResponse -> - val healthData = mutableListOf>() - for (bucket in response.buckets) { - var sourceName: Any = "" - var sourceId: Any = "" - var isManualEntry: Any = false - var totalSteps: Any = 0 - var totalDistance: Any = 0 - var totalEnergyBurned: Any = 0 - /// Fetch all data points for the specified DataType - for (dataSet in bucket.dataSets) { - /// For each data point, extract the contents and - // send them to Flutter, - // along with date and unit. - var dataPoints = dataSet.dataPoints - if (!includeManualEntry) { - dataPoints = - dataPoints.filterIndexed { - _, - dataPoint -> - !dataPoint.originalDataSource - .streamName - .contains( - "user_input" - ) - } - } - for (dataPoint in dataPoints) { - sourceName = - (dataPoint.originalDataSource - .appPackageName - ?: (dataPoint.originalDataSource - .device - ?.model - ?: "")) - sourceId = - dataPoint.originalDataSource - .streamIdentifier - isManualEntry = - dataPoint.originalDataSource - .streamName - .contains( - "user_input" - ) - for (field in dataPoint.dataType.fields) { - when (field) { - getField(STEPS) -> { - totalSteps = - getHealthDataValue( - dataPoint, - field - ) - } - getField( - DISTANCE_DELTA - ) -> { - totalDistance = - getHealthDataValue( - dataPoint, - field - ) - } - getField( - ACTIVE_ENERGY_BURNED - ) -> { - totalEnergyBurned = - getHealthDataValue( - dataPoint, - field - ) - } - } - } - } - } - val healthDataItems = - hashMapOf( - "value" to - bucket.getEndTime( - TimeUnit.MINUTES - ) - - bucket.getStartTime( - TimeUnit.MINUTES - ), - "date_from" to - bucket.getStartTime( - TimeUnit.MILLISECONDS - ), - "date_to" to - bucket.getEndTime( - TimeUnit.MILLISECONDS - ), - "source_name" to sourceName, - "source_id" to sourceId, - "is_manual_entry" to - isManualEntry, - "workout_type" to - bucket.activity - .toLowerCase(), - "total_steps" to totalSteps, - "total_distance" to - totalDistance, - "total_energy_burned" to - totalEnergyBurned - ) - healthData.add(healthDataItems) - } - Handler(context!!.mainLooper).run { result.success(healthData) } - } + val optionsToRegister = callToHealthTypes(call) + + val isGranted = + GoogleSignIn.hasPermissions( + GoogleSignIn.getLastSignedInAccount(context!!), + optionsToRegister, + ) + + result?.success(isGranted) + } + + /** + * Requests authorization for the HealthDataTypes with the the READ or READ_WRITE permission + * type. + */ + private fun requestAuthorization(call: MethodCall, result: Result) { + if (context == null) { + result.success(false) + return + } + mResult = result - private fun workoutDataHandler(type: String, result: Result) = - OnSuccessListener { response: SessionReadResponse -> - val healthData: MutableList> = mutableListOf() - for (session in response.sessions) { - // Look for calories and distance if they - var totalEnergyBurned = 0.0 - var totalDistance = 0.0 - for (dataSet in response.getDataSet(session)) { - if (dataSet.dataType == - DataType.TYPE_CALORIES_EXPENDED - ) { - for (dataPoint in dataSet.dataPoints) { - totalEnergyBurned += - dataPoint.getValue( - Field.FIELD_CALORIES - ) - .toString() - .toDouble() - } - } - if (dataSet.dataType == DataType.TYPE_DISTANCE_DELTA - ) { - for (dataPoint in dataSet.dataPoints) { - totalDistance += - dataPoint.getValue( - Field.FIELD_DISTANCE - ) - .toString() - .toDouble() - } - } - } - healthData.add( - hashMapOf( - "workoutActivityType" to - (workoutTypeMap - .filterValues { - it == - session.activity - } - .keys - .firstOrNull() - ?: "OTHER"), - "totalEnergyBurned" to - if (totalEnergyBurned == - 0.0 - ) - null - else - totalEnergyBurned, - "totalEnergyBurnedUnit" to - "KILOCALORIE", - "totalDistance" to - if (totalDistance == - 0.0 - ) - null - else - totalDistance, - "totalDistanceUnit" to - "METER", - "date_from" to - session.getStartTime( - TimeUnit.MILLISECONDS - ), - "date_to" to - session.getEndTime( - TimeUnit.MILLISECONDS - ), - "unit" to "MINUTES", - "source_name" to - session.appPackageName, - "source_id" to - session.identifier, - ), - ) - } - Handler(context!!.mainLooper).run { result.success(healthData) } - } + if (useHealthConnectIfAvailable && healthConnectAvailable) { + requestAuthorizationHC(call, result) + return + } - private fun callToHealthTypes(call: MethodCall): FitnessOptions { - val typesBuilder = FitnessOptions.builder() - val args = call.arguments as HashMap<*, *> - val types = (args["types"] as? ArrayList<*>)?.filterIsInstance() - val permissions = (args["permissions"] as? ArrayList<*>)?.filterIsInstance() - - assert(types != null) - assert(permissions != null) - assert(types!!.count() == permissions!!.count()) - - for ((i, typeKey) in types.withIndex()) { - val access = permissions[i] - val dataType = keyToHealthDataType(typeKey) - when (access) { - 0 -> typesBuilder.addDataType(dataType, FitnessOptions.ACCESS_READ) - 1 -> typesBuilder.addDataType(dataType, FitnessOptions.ACCESS_WRITE) - 2 -> { - typesBuilder.addDataType( - dataType, - FitnessOptions.ACCESS_READ - ) - typesBuilder.addDataType( - dataType, - FitnessOptions.ACCESS_WRITE - ) - } - else -> - throw IllegalArgumentException( - "Unknown access type $access" - ) - } - if (typeKey == SLEEP_ASLEEP || - typeKey == SLEEP_AWAKE || - typeKey == SLEEP_IN_BED - ) { - typesBuilder.accessSleepSessions(FitnessOptions.ACCESS_READ) - when (access) { - 0 -> - typesBuilder.accessSleepSessions( - FitnessOptions.ACCESS_READ - ) - 1 -> - typesBuilder.accessSleepSessions( - FitnessOptions.ACCESS_WRITE - ) - 2 -> { - typesBuilder.accessSleepSessions( - FitnessOptions.ACCESS_READ - ) - typesBuilder.accessSleepSessions( - FitnessOptions.ACCESS_WRITE - ) - } - else -> - throw IllegalArgumentException( - "Unknown access type $access" - ) - } - } - if (typeKey == WORKOUT) { - when (access) { - 0 -> - typesBuilder.accessActivitySessions( - FitnessOptions.ACCESS_READ - ) - 1 -> - typesBuilder.accessActivitySessions( - FitnessOptions.ACCESS_WRITE - ) - 2 -> { - typesBuilder.accessActivitySessions( - FitnessOptions.ACCESS_READ - ) - typesBuilder.accessActivitySessions( - FitnessOptions.ACCESS_WRITE - ) - } - else -> - throw IllegalArgumentException( - "Unknown access type $access" - ) - } - } - } - return typesBuilder.build() + val optionsToRegister = callToHealthTypes(call) + + // Set to false due to bug described in + // https://github.com/cph-cachet/flutter-plugins/issues/640#issuecomment-1366830132 + val isGranted = false + + // If not granted then ask for permission + if (!isGranted && activity != null) { + GoogleSignIn.requestPermissions( + activity!!, + GOOGLE_FIT_PERMISSIONS_REQUEST_CODE, + GoogleSignIn.getLastSignedInAccount(context!!), + optionsToRegister, + ) + } else { // / Permission already granted + result?.success(true) + } + } + + /** + * Revokes access to Google Fit using the `disableFit`-method. + * + * Note: Using the `revokeAccess` creates a bug on android when trying to reapply for + * permissions afterwards, hence `disableFit` was used. + */ + private fun revokePermissions(call: MethodCall, result: Result) { + if (useHealthConnectIfAvailable && healthConnectAvailable) { + result.notImplemented() + return + } + if (context == null) { + result.success(false) + return } + Fitness.getConfigClient( + activity!!, + GoogleSignIn.getLastSignedInAccount(context!!)!! + ) + .disableFit() + .addOnSuccessListener { + Log.i("Health", "Disabled Google Fit") + result.success(true) + } + .addOnFailureListener { e -> + Log.w( + "Health", + "There was an error disabling Google Fit", + e + ) + result.success(false) + } + } - private fun hasPermissions(call: MethodCall, result: Result) { - if (useHealthConnectIfAvailable && healthConnectAvailable) { - hasPermissionsHC(call, result) - return - } - if (context == null) { - result.success(false) - return - } + private fun getTotalStepsInInterval(call: MethodCall, result: Result) { + val start = call.argument("startTime")!! + val end = call.argument("endTime")!! - val optionsToRegister = callToHealthTypes(call) - val isGranted = - GoogleSignIn.hasPermissions( - GoogleSignIn.getLastSignedInAccount(context!!), - optionsToRegister, - ) + if (StepCounterService.initiated()) { + getSensorDataCount(call, result) + return + } - result?.success(isGranted) + if (useHealthConnectIfAvailable && healthConnectAvailable) { + getStepsHealthConnect(start, end, result) + return } - /** - * Requests authorization for the HealthDataTypes with the the READ or READ_WRITE permission - * type. - */ - private fun requestAuthorization(call: MethodCall, result: Result) { - if (context == null) { - result.success(false) - return - } + val context = context ?: return + + val stepsDataType = keyToHealthDataType(STEPS) + val aggregatedDataType = keyToHealthDataType(AGGREGATE_STEP_COUNT) + + val fitnessOptions = + FitnessOptions.builder() + .addDataType(stepsDataType) + .addDataType(aggregatedDataType) + .build() + val gsa = GoogleSignIn.getAccountForExtension(context, fitnessOptions) + + val ds = + DataSource.Builder() + .setAppPackageName("com.google.android.gms") + .setDataType(stepsDataType) + .setType(DataSource.TYPE_DERIVED) + .setStreamName("estimated_steps") + .build() + + val duration = (end - start).toInt() + + val request = + DataReadRequest.Builder() + .aggregate(ds) + .bucketByTime(duration, TimeUnit.MILLISECONDS) + .setTimeRange(start, end, TimeUnit.MILLISECONDS) + .build() + + Fitness.getHistoryClient(context, gsa) + .readData(request) + .addOnFailureListener( + errHandler( + result, + "There was an error getting the total steps in the interval!", + ), + ) + .addOnSuccessListener( + threadPoolExecutor!!, + getStepsInRange( + start, + end, + aggregatedDataType, + result + ), + ) + } + + private fun getStepsHealthConnect(start: Long, end: Long, result: Result) = + scope.launch { + try { + val startInstant = Instant.ofEpochMilli(start) + val endInstant = Instant.ofEpochMilli(end) + val response = + healthConnectClient.aggregate( + AggregateRequest( + metrics = + setOf( + StepsRecord.COUNT_TOTAL + ), + timeRangeFilter = + TimeRangeFilter.between( + startInstant, + endInstant + ), + ), + ) + // The result may be null if no data is available in the + // time range. + val stepsInInterval = + response[StepsRecord.COUNT_TOTAL] ?: 0L + Log.i( + "FLUTTER_HEALTH::SUCCESS", + "returning $stepsInInterval steps" + ) + result.success(stepsInInterval) + } catch (e: Exception) { + Log.i("FLUTTER_HEALTH::ERROR", "unable to return steps") + result.success(null) + } + } + + private fun getStepsInRange( + start: Long, + end: Long, + aggregatedDataType: DataType, + result: Result, + ) = OnSuccessListener { response: DataReadResponse -> + val map = HashMap() // need to return to Dart so can't use sparse array + for (bucket in response.buckets) { + val dp = bucket.dataSets.firstOrNull()?.dataPoints?.firstOrNull() + if (dp != null) { + val count = dp.getValue(aggregatedDataType.fields[0]) + + val startTime = dp.getStartTime(TimeUnit.MILLISECONDS) + val startDate = Date(startTime) + val endDate = Date(dp.getEndTime(TimeUnit.MILLISECONDS)) + Log.i( + "FLUTTER_HEALTH::SUCCESS", + "returning $count steps for $startDate - $endDate", + ) + map[startTime] = count.asInt() + } else { + val startDay = Date(start) + val endDay = Date(end) + Log.i("FLUTTER_HEALTH::ERROR", "no steps for $startDay - $endDay") + } + } + + assert(map.size <= 1) { + "getTotalStepsInInterval should return only one interval. Found: ${map.size}" + } + Handler(context!!.mainLooper).run { result.success(map.values.firstOrNull()) } + } + + /// Disconnect Google fit + private fun disconnect(call: MethodCall, result: Result) { + if (activity == null) { + result.success(false) + return + } + val context = activity!!.applicationContext + + val fitnessOptions = callToHealthTypes(call) + val googleAccount = GoogleSignIn.getAccountForExtension(context, fitnessOptions) + Fitness.getConfigClient(context, googleAccount).disableFit().continueWith { + val signinOption = + GoogleSignInOptions.Builder( + GoogleSignInOptions + .DEFAULT_SIGN_IN + ) + .requestId() + .requestEmail() + .build() + val googleSignInClient = GoogleSignIn.getClient(context, signinOption) + googleSignInClient.signOut() + result.success(true) + } + } + + private fun getActivityType(type: String): String { + return workoutTypeMap[type] ?: FitnessActivities.UNKNOWN + } + + /** Handle calls from the MethodChannel */ + override fun onMethodCall(call: MethodCall, result: Result) { + when (call.method) { + "installHealthConnect" -> installHealthConnect(call, result) + "useHealthConnectIfAvailable" -> useHealthConnectIfAvailable(call, result) + "getHealthConnectSdkStatus" -> getHealthConnectSdkStatus(call, result) + "hasPermissions" -> hasPermissions(call, result) + "requestAuthorization" -> requestAuthorization(call, result) + "revokePermissions" -> revokePermissions(call, result) + "getData" -> getData(call, result) + "getIntervalData" -> getIntervalData(call, result) + "writeData" -> writeData(call, result) + "delete" -> delete(call, result) + "getAggregateData" -> getAggregateData(call, result) + "getTotalStepsInInterval" -> getTotalStepsInInterval(call, result) + "writeWorkoutData" -> writeWorkoutData(call, result) + "writeBloodPressure" -> writeBloodPressure(call, result) + "writeBloodOxygen" -> writeBloodOxygen(call, result) + "writeMeal" -> writeMeal(call, result) + "disconnect" -> disconnect(call, result) + "doseStepSensorIsAvailable" -> doseStepSensorIsAvailable(call, result) + "isStepSensorRunning" -> isStepSensorRunning(call, result) + "clearStepSensorData" -> clearStepSensorData(call, result) + "startStepSensorBackgroundService" -> startStepSensorBackgroundService(call, result) + "stopStepSensorBackgroundService" -> stopStepSensorBackgroundService(call, result) + else -> result.notImplemented() + } + } + + private fun isForegroundServiceRunning(context: Context, serviceClass: Class<*>): Boolean { + val activityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager + for (service in activityManager.getRunningServices(Int.MAX_VALUE)) { + if (serviceClass.name == service.service.className && service.foreground) { + return true + } + } + return false + } + + private fun isStepSensorRunning(call: MethodCall, result: Result) { + result.success(isForegroundServiceRunning(context!!,StepCounterService::class.java)) + } + private fun clearStepSensorData(call: MethodCall, result: Result) { + StepCounterService.box.boxFor(SensorStep::class.java).removeAll() + result.success(true) + } + + private fun stopStepSensorBackgroundService(call: MethodCall, result: Result) { + val serviceIntent = Intent(activity!!, StepCounterService::class.java) + activity!!.stopService(serviceIntent) + } + + private fun startStepSensorBackgroundService(call: MethodCall, result: Result) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + if (ContextCompat.checkSelfPermission( + activity!!, + Manifest.permission.ACTIVITY_RECOGNITION + ) + != PackageManager.PERMISSION_GRANTED + ) { mResult = result + activityRecognitionPermissionLauncher!!.launch(arrayOf(Manifest.permission.ACTIVITY_RECOGNITION)) + return + } + } - if (useHealthConnectIfAvailable && healthConnectAvailable) { - requestAuthorizationHC(call, result) - return - } + startStepSenor() + result.success(true) + } - val optionsToRegister = callToHealthTypes(call) + private fun startStepSenor() { + try { + val serviceIntent = Intent(activity!!, StepCounterService::class.java) + ContextCompat.startForegroundService(context!!, serviceIntent) + } catch (e:Exception) { + Log.d("HealthPlugin", e.toString()); + } + } - // Set to false due to bug described in - // https://github.com/cph-cachet/flutter-plugins/issues/640#issuecomment-1366830132 - val isGranted = false + private fun doseStepSensorIsAvailable(call: MethodCall, result: Result) { + val sensorManager = context?.getSystemService(Service.SENSOR_SERVICE) as SensorManager + result.success(sensorManager.getDefaultSensor(Sensor.TYPE_STEP_COUNTER) != null) + } - // If not granted then ask for permission - if (!isGranted && activity != null) { - GoogleSignIn.requestPermissions( - activity!!, - GOOGLE_FIT_PERMISSIONS_REQUEST_CODE, - GoogleSignIn.getLastSignedInAccount(context!!), - optionsToRegister, - ) - } else { // / Permission already granted - result?.success(true) - } + override fun onAttachedToActivity(binding: ActivityPluginBinding) { + if (channel == null) { + return } + binding.addActivityResultListener(this) + activity = binding.activity + + val requestPermissionActivityContract = + PermissionController.createRequestPermissionResultContract() + + healthConnectRequestPermissionsLauncher = + (activity as ComponentActivity).registerForActivityResult( + requestPermissionActivityContract + ) { granted -> onHealthConnectPermissionCallback(granted) } + + activityRecognitionPermissionLauncher = + (activity as ComponentActivity).registerForActivityResult( + ActivityResultContracts.RequestMultiplePermissions() + ) { granted -> + if(granted.isNotEmpty() && granted.values.first()) { + startStepSenor() + mResult?.success(true) + return@registerForActivityResult + } + + mResult?.success(false) + } + } + + override fun onDetachedFromActivityForConfigChanges() { + onDetachedFromActivity() + } + + override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) { + onAttachedToActivity(binding) + } - /** - * Revokes access to Google Fit using the `disableFit`-method. - * - * Note: Using the `revokeAccess` creates a bug on android when trying to reapply for - * permissions afterwards, hence `disableFit` was used. - */ - private fun revokePermissions(call: MethodCall, result: Result) { - if (useHealthConnectIfAvailable && healthConnectAvailable) { - result.notImplemented() - return + override fun onDetachedFromActivity() { + if (channel == null) { + return + } + activity = null + healthConnectRequestPermissionsLauncher = null + } + + /** HEALTH CONNECT BELOW */ + var healthConnectAvailable = false + var healthConnectStatus = HealthConnectClient.SDK_UNAVAILABLE + + fun checkAvailability() { + healthConnectStatus = HealthConnectClient.getSdkStatus(context!!) + healthConnectAvailable = healthConnectStatus == HealthConnectClient.SDK_AVAILABLE + } + + private fun installHealthConnect(call: MethodCall, result: Result) { + val uriString = + "market://details?id=com.google.android.apps.healthdata&url=healthconnect%3A%2F%2Fonboarding" + context!!.startActivity( + Intent(Intent.ACTION_VIEW).apply { + setPackage("com.android.vending") + data = Uri.parse(uriString) + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + putExtra("overlay", true) + putExtra("callerId", context!!.packageName) + } + ) + result.success(null) + } + + fun useHealthConnectIfAvailable(call: MethodCall, result: Result) { + useHealthConnectIfAvailable = true + result.success(null) + } + + private fun getHealthConnectSdkStatus(call: MethodCall, result: Result) { + checkAvailability() + if (healthConnectAvailable) { + healthConnectClient = + HealthConnectClient.getOrCreate( + context!! + ) + } + result.success(healthConnectStatus) + } + + private fun hasPermissionsHC(call: MethodCall, result: Result) { + val args = call.arguments as HashMap<*, *> + val types = (args["types"] as? ArrayList<*>)?.filterIsInstance()!! + val permissions = (args["permissions"] as? ArrayList<*>)?.filterIsInstance()!! + + var permList = mutableListOf() + for ((i, typeKey) in types.withIndex()) { + if (!MapToHCType.containsKey(typeKey)) { + Log.w( + "FLUTTER_HEALTH::ERROR", + "Datatype " + typeKey + " not found in HC" + ) + result.success(false) + return + } + val access = permissions[i] + val dataType = MapToHCType[typeKey]!! + if (access == 0) { + permList.add( + HealthPermission.getReadPermission(dataType), + ) + } else { + permList.addAll( + listOf( + HealthPermission.getReadPermission( + dataType + ), + HealthPermission.getWritePermission( + dataType + ), + ), + ) + } + // Workout also needs distance and total energy burned too + if (typeKey == WORKOUT) { + if (access == 0) { + permList.addAll( + listOf( + HealthPermission.getReadPermission( + DistanceRecord::class + ), + HealthPermission.getReadPermission( + TotalCaloriesBurnedRecord::class + ), + ), + ) + } else { + permList.addAll( + listOf( + HealthPermission.getReadPermission( + DistanceRecord::class + ), + HealthPermission.getReadPermission( + TotalCaloriesBurnedRecord::class + ), + HealthPermission.getWritePermission( + DistanceRecord::class + ), + HealthPermission.getWritePermission( + TotalCaloriesBurnedRecord::class + ), + ), + ) } - if (context == null) { - result.success(false) - return + } + } + scope.launch { + result.success( + healthConnectClient + .permissionController + .getGrantedPermissions() + .containsAll(permList), + ) + } + } + + private fun requestAuthorizationHC(call: MethodCall, result: Result) { + val args = call.arguments as HashMap<*, *> + val types = (args["types"] as? ArrayList<*>)?.filterIsInstance()!! + val permissions = (args["permissions"] as? ArrayList<*>)?.filterIsInstance()!! + + var permList = mutableListOf() + for ((i, typeKey) in types.withIndex()) { + if (!MapToHCType.containsKey(typeKey)) { + Log.w( + "FLUTTER_HEALTH::ERROR", + "Datatype " + typeKey + " not found in HC" + ) + result.success(false) + return + } + val access = permissions[i]!! + val dataType = MapToHCType[typeKey]!! + if (access == 0) { + permList.add( + HealthPermission.getReadPermission(dataType), + ) + } else { + permList.addAll( + listOf( + HealthPermission.getReadPermission( + dataType + ), + HealthPermission.getWritePermission( + dataType + ), + ), + ) + } + // Workout also needs distance and total energy burned too + if (typeKey == WORKOUT) { + if (access == 0) { + permList.addAll( + listOf( + HealthPermission.getReadPermission( + DistanceRecord::class + ), + HealthPermission.getReadPermission( + TotalCaloriesBurnedRecord::class + ), + ), + ) + } else { + permList.addAll( + listOf( + HealthPermission.getReadPermission( + DistanceRecord::class + ), + HealthPermission.getReadPermission( + TotalCaloriesBurnedRecord::class + ), + HealthPermission.getWritePermission( + DistanceRecord::class + ), + HealthPermission.getWritePermission( + TotalCaloriesBurnedRecord::class + ), + ), + ) } - Fitness.getConfigClient( - activity!!, - GoogleSignIn.getLastSignedInAccount(context!!)!! - ) - .disableFit() - .addOnSuccessListener { - Log.i("Health", "Disabled Google Fit") - result.success(true) - } - .addOnFailureListener { e -> - Log.w( - "Health", - "There was an error disabling Google Fit", - e - ) - result.success(false) - } + } + } + if (healthConnectRequestPermissionsLauncher == null) { + result.success(false) + Log.i("FLUTTER_HEALTH", "Permission launcher not found") + return } - private fun getTotalStepsInInterval(call: MethodCall, result: Result) { - val start = call.argument("startTime")!! - val end = call.argument("endTime")!! + healthConnectRequestPermissionsLauncher!!.launch(permList.toSet()) + } - if (useHealthConnectIfAvailable && healthConnectAvailable) { - getStepsHealthConnect(start, end, result) - return - } + fun getHCData(call: MethodCall, result: Result) { + val dataType = call.argument("dataTypeKey")!! + val startTime = Instant.ofEpochMilli(call.argument("startTime")!!) + val endTime = Instant.ofEpochMilli(call.argument("endTime")!!) + val healthConnectData = mutableListOf>() - val context = context ?: return + scope.launch { + MapToHCType[dataType]?.let { classType -> + val records = mutableListOf() - val stepsDataType = keyToHealthDataType(STEPS) - val aggregatedDataType = keyToHealthDataType(AGGREGATE_STEP_COUNT) + val granted = healthConnectClient.permissionController.getGrantedPermissions() + if(!granted.contains(HealthPermission.getReadPermission(classType))) { + Log.d("Health Plugin", "No Permission granted for $dataType") + return@let + } - val fitnessOptions = - FitnessOptions.builder() - .addDataType(stepsDataType) - .addDataType(aggregatedDataType) - .build() - val gsa = GoogleSignIn.getAccountForExtension(context, fitnessOptions) + // Set up the initial request to read health records with specified + // parameters + var request = + ReadRecordsRequest( + recordType = classType, + // Define the maximum amount of data + // that HealthConnect can return + // in a single request + timeRangeFilter = + TimeRangeFilter.between( + startTime, + endTime + ), + ) + + var response = healthConnectClient.readRecords(request) + var pageToken = response.pageToken + + // Add the records from the initial response to the records list + records.addAll(response.records) + + // Continue making requests and fetching records while there is a + // page token + while (!pageToken.isNullOrEmpty()) { + request = + ReadRecordsRequest( + recordType = classType, + timeRangeFilter = + TimeRangeFilter.between( + startTime, + endTime + ), + pageToken = pageToken + ) + response = healthConnectClient.readRecords(request) - val ds = - DataSource.Builder() - .setAppPackageName("com.google.android.gms") - .setDataType(stepsDataType) - .setType(DataSource.TYPE_DERIVED) - .setStreamName("estimated_steps") - .build() + pageToken = response.pageToken + records.addAll(response.records) + } - val duration = (end - start).toInt() + // Workout needs distance and total calories burned too + if (dataType == WORKOUT) { + for (rec in records) { + val record = rec as ExerciseSessionRecord + val distanceRequest = + healthConnectClient.readRecords( + ReadRecordsRequest( + recordType = + DistanceRecord::class, + timeRangeFilter = + TimeRangeFilter.between( + record.startTime, + record.endTime, + ), + ), + ) + var totalDistance = 0.0 + for (distanceRec in distanceRequest.records) { + totalDistance += + distanceRec.distance + .inMeters + } - val request = - DataReadRequest.Builder() - .aggregate(ds) - .bucketByTime(duration, TimeUnit.MILLISECONDS) - .setTimeRange(start, end, TimeUnit.MILLISECONDS) - .build() - - Fitness.getHistoryClient(context, gsa) - .readData(request) - .addOnFailureListener( - errHandler( - result, - "There was an error getting the total steps in the interval!", - ), - ) - .addOnSuccessListener( - threadPoolExecutor!!, - getStepsInRange( - start, - end, - aggregatedDataType, - result - ), - ) - } + val energyBurnedRequest = + healthConnectClient.readRecords( + ReadRecordsRequest( + recordType = + TotalCaloriesBurnedRecord::class, + timeRangeFilter = + TimeRangeFilter.between( + record.startTime, + record.endTime, + ), + ), + ) + var totalEnergyBurned = 0.0 + for (energyBurnedRec in + energyBurnedRequest.records) { + totalEnergyBurned += + energyBurnedRec.energy + .inKilocalories + } - private fun getStepsHealthConnect(start: Long, end: Long, result: Result) = - scope.launch { - try { - val startInstant = Instant.ofEpochMilli(start) - val endInstant = Instant.ofEpochMilli(end) - val response = - healthConnectClient.aggregate( - AggregateRequest( - metrics = - setOf( - StepsRecord.COUNT_TOTAL - ), - timeRangeFilter = - TimeRangeFilter.between( - startInstant, - endInstant - ), - ), - ) - // The result may be null if no data is available in the - // time range. - val stepsInInterval = - response[StepsRecord.COUNT_TOTAL] ?: 0L - Log.i( - "FLUTTER_HEALTH::SUCCESS", - "returning $stepsInInterval steps" - ) - result.success(stepsInInterval) - } catch (e: Exception) { - Log.i("FLUTTER_HEALTH::ERROR", "unable to return steps") - result.success(null) - } + val stepRequest = + healthConnectClient.readRecords( + ReadRecordsRequest( + recordType = + StepsRecord::class, + timeRangeFilter = + TimeRangeFilter.between( + record.startTime, + record.endTime + ), + ), + ) + var totalSteps = 0.0 + for (stepRec in stepRequest.records) { + totalSteps += stepRec.count } - private fun getStepsInRange( - start: Long, - end: Long, - aggregatedDataType: DataType, - result: Result, - ) = OnSuccessListener { response: DataReadResponse -> - val map = HashMap() // need to return to Dart so can't use sparse array - for (bucket in response.buckets) { - val dp = bucket.dataSets.firstOrNull()?.dataPoints?.firstOrNull() - if (dp != null) { - val count = dp.getValue(aggregatedDataType.fields[0]) - - val startTime = dp.getStartTime(TimeUnit.MILLISECONDS) - val startDate = Date(startTime) - val endDate = Date(dp.getEndTime(TimeUnit.MILLISECONDS)) - Log.i( - "FLUTTER_HEALTH::SUCCESS", - "returning $count steps for $startDate - $endDate", + // val metadata = (rec as Record).metadata + // Add final datapoint + healthConnectData.add( + // mapOf( + mapOf( + "workoutActivityType" to + (workoutTypeMapHealthConnect + .filterValues { + it == + record.exerciseType + } + .keys + .firstOrNull() + ?: "OTHER"), + "totalDistance" to + if (totalDistance == + 0.0 + ) + null + else + totalDistance, + "totalDistanceUnit" to + "METER", + "totalEnergyBurned" to + if (totalEnergyBurned == + 0.0 + ) + null + else + totalEnergyBurned, + "totalEnergyBurnedUnit" to + "KILOCALORIE", + "totalSteps" to + if (totalSteps == + 0.0 + ) + null + else + totalSteps, + "totalStepsUnit" to + "COUNT", + "unit" to "MINUTES", + "date_from" to + rec.startTime + .toEpochMilli(), + "date_to" to + rec.endTime.toEpochMilli(), + "source_id" to "", + "source_name" to + record.metadata + .dataOrigin + .packageName, + ), + ) + } + // Filter sleep stages for requested stage + } else if (classType == SleepSessionRecord::class) { + for (rec in response.records) { + if (rec is SleepSessionRecord) { + if (dataType == SLEEP_SESSION) { + healthConnectData.addAll( + convertRecord( + rec, + dataType + ) ) - map[startTime] = count.asInt() - } else { - val startDay = Date(start) - val endDay = Date(end) - Log.i("FLUTTER_HEALTH::ERROR", "no steps for $startDay - $endDay") + } else { + for (recStage in rec.stages) { + if (dataType == + MapSleepStageToType[ + recStage.stage] + ) { + healthConnectData + .addAll( + convertRecordStage( + recStage, + dataType, + rec.metadata.dataOrigin + .packageName + ) + ) + } + } + } } + } + } else { + for (rec in records) { + healthConnectData.addAll( + convertRecord(rec, dataType) + ) + } } + } - assert(map.size <= 1) { - "getTotalStepsInInterval should return only one interval. Found: ${map.size}" - } - Handler(context!!.mainLooper).run { result.success(map.values.firstOrNull()) } - } - - /// Disconnect Google fit - private fun disconnect(call: MethodCall, result: Result) { - if (activity == null) { - result.success(false) - return - } - val context = activity!!.applicationContext - - val fitnessOptions = callToHealthTypes(call) - val googleAccount = GoogleSignIn.getAccountForExtension(context, fitnessOptions) - Fitness.getConfigClient(context, googleAccount).disableFit().continueWith { - val signinOption = - GoogleSignInOptions.Builder( - GoogleSignInOptions - .DEFAULT_SIGN_IN - ) - .requestId() - .requestEmail() - .build() - val googleSignInClient = GoogleSignIn.getClient(context, signinOption) - googleSignInClient.signOut() - result.success(true) - } + Handler(context!!.mainLooper).run { result.success(healthConnectData) } } + } + + fun convertRecordStage( + stage: SleepSessionRecord.Stage, + dataType: String, + sourceName: String + ): List> { + return listOf( + mapOf( + "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, + ), + ) + } + + fun getAggregateHCData(call: MethodCall, result: Result) { + val dataType = call.argument("dataTypeKey")!! + val interval = call.argument("interval")!! + val startTime = Instant.ofEpochMilli(call.argument("startTime")!!) + val endTime = Instant.ofEpochMilli(call.argument("endTime")!!) + val healthConnectData = mutableListOf>() + scope.launch { + MapToHCAggregateMetric[dataType]?.let { metricClassType -> + val request = + AggregateGroupByDurationRequest( + metrics = setOf(metricClassType), + timeRangeFilter = + TimeRangeFilter.between( + startTime, + endTime + ), + timeRangeSlicer = + Duration.ofSeconds( + interval + ) + ) + val response = healthConnectClient.aggregateGroupByDuration(request) + + for (durationResult in response) { + // The result may be null if no data is available in the + // time range + var totalValue = durationResult.result[metricClassType] + if (totalValue is Length) { + totalValue = totalValue.inMeters + } else if (totalValue is Energy) { + totalValue = totalValue.inKilocalories + } - private fun getActivityType(type: String): String { - return workoutTypeMap[type] ?: FitnessActivities.UNKNOWN - } - - /** Handle calls from the MethodChannel */ - override fun onMethodCall(call: MethodCall, result: Result) { - when (call.method) { - "installHealthConnect" -> installHealthConnect(call, result) - "useHealthConnectIfAvailable" -> useHealthConnectIfAvailable(call, result) - "getHealthConnectSdkStatus" -> getHealthConnectSdkStatus(call, result) - "hasPermissions" -> hasPermissions(call, result) - "requestAuthorization" -> requestAuthorization(call, result) - "revokePermissions" -> revokePermissions(call, result) - "getData" -> getData(call, result) - "getIntervalData" -> getIntervalData(call, result) - "writeData" -> writeData(call, result) - "delete" -> delete(call, result) - "getAggregateData" -> getAggregateData(call, result) - "getTotalStepsInInterval" -> getTotalStepsInInterval(call, result) - "writeWorkoutData" -> writeWorkoutData(call, result) - "writeBloodPressure" -> writeBloodPressure(call, result) - "writeBloodOxygen" -> writeBloodOxygen(call, result) - "writeMeal" -> writeMeal(call, result) - "disconnect" -> disconnect(call, result) - else -> result.notImplemented() + val packageNames = + durationResult.result.dataOrigins + .joinToString { origin -> + "${origin.packageName}" + } + + val data = + mapOf( + "value" to + (totalValue + ?: 0), + "date_from" to + durationResult.startTime + .toEpochMilli(), + "date_to" to + durationResult.endTime + .toEpochMilli(), + "source_name" to + packageNames, + "source_id" to "", + "is_manual_entry" to + packageNames.contains( + "user_input" + ) + ) + healthConnectData.add(data) } + } + Handler(context!!.mainLooper).run { result.success(healthConnectData) } } + } - override fun onAttachedToActivity(binding: ActivityPluginBinding) { - if (channel == null) { - return - } - binding.addActivityResultListener(this) - activity = binding.activity + // TODO: Find alternative to SOURCE_ID or make it nullable? + fun convertRecord(record: Any, dataType: String): List> { + val metadata = (record as Record).metadata + when (record) { + is WeightRecord -> + return listOf( + mapOf( + "value" to + record.weight + .inKilograms, + "date_from" to + record.time + .toEpochMilli(), + "date_to" to + record.time + .toEpochMilli(), + "source_id" to "", + "source_name" to + metadata.dataOrigin + .packageName, + ), + ) - val requestPermissionActivityContract = - PermissionController.createRequestPermissionResultContract() + is HeightRecord -> + return listOf( + mapOf( + "value" to + record.height + .inMeters, + "date_from" to + record.time + .toEpochMilli(), + "date_to" to + record.time + .toEpochMilli(), + "source_id" to "", + "source_name" to + metadata.dataOrigin + .packageName, + ), + ) - healthConnectRequestPermissionsLauncher = - (activity as ComponentActivity).registerForActivityResult( - requestPermissionActivityContract - ) { granted -> onHealthConnectPermissionCallback(granted) } - } + is BodyFatRecord -> + return listOf( + mapOf( + "value" to + record.percentage + .value, + "date_from" to + record.time + .toEpochMilli(), + "date_to" to + record.time + .toEpochMilli(), + "source_id" to "", + "source_name" to + metadata.dataOrigin + .packageName, + ), + ) - override fun onDetachedFromActivityForConfigChanges() { - onDetachedFromActivity() - } + is StepsRecord -> + return listOf( + mapOf( + "value" to record.count, + "date_from" to + record.startTime + .toEpochMilli(), + "date_to" to + record.endTime + .toEpochMilli(), + "source_id" to "", + "source_name" to + metadata.dataOrigin + .packageName, + ), + ) - override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) { - onAttachedToActivity(binding) - } + is ActiveCaloriesBurnedRecord -> + return listOf( + mapOf( + "value" to + record.energy + .inKilocalories, + "date_from" to + record.startTime + .toEpochMilli(), + "date_to" to + record.endTime + .toEpochMilli(), + "source_id" to "", + "source_name" to + metadata.dataOrigin + .packageName, + ), + ) - override fun onDetachedFromActivity() { - if (channel == null) { - return + is HeartRateRecord -> + return record.samples.map { + mapOf( + "value" to it.beatsPerMinute, + "date_from" to + it.time.toEpochMilli(), + "date_to" to it.time.toEpochMilli(), + "source_id" to "", + "source_name" to + metadata.dataOrigin + .packageName, + ) } - activity = null - healthConnectRequestPermissionsLauncher = null - } - /** HEALTH CONNECT BELOW */ - var healthConnectAvailable = false - var healthConnectStatus = HealthConnectClient.SDK_UNAVAILABLE - - fun checkAvailability() { - healthConnectStatus = HealthConnectClient.getSdkStatus(context!!) - healthConnectAvailable = healthConnectStatus == HealthConnectClient.SDK_AVAILABLE - } + is BodyTemperatureRecord -> + return listOf( + mapOf( + "value" to + record.temperature + .inCelsius, + "date_from" to + record.time + .toEpochMilli(), + "date_to" to + record.time + .toEpochMilli(), + "source_id" to "", + "source_name" to + metadata.dataOrigin + .packageName, + ), + ) - private fun installHealthConnect(call: MethodCall, result: Result) { - val uriString = - "market://details?id=com.google.android.apps.healthdata&url=healthconnect%3A%2F%2Fonboarding" - context!!.startActivity( - Intent(Intent.ACTION_VIEW).apply { - setPackage("com.android.vending") - data = Uri.parse(uriString) - addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - putExtra("overlay", true) - putExtra("callerId", context!!.packageName) - } + is BodyWaterMassRecord -> + return listOf( + mapOf( + "value" to + record.mass + .inKilograms, + "date_from" to + record.time + .toEpochMilli(), + "date_to" to + record.time + .toEpochMilli(), + "source_id" to "", + "source_name" to + metadata.dataOrigin + .packageName, + ), ) - result.success(null) - } - fun useHealthConnectIfAvailable(call: MethodCall, result: Result) { - useHealthConnectIfAvailable = true - result.success(null) - } + is BloodPressureRecord -> + return listOf( + mapOf( + "value" to + if (dataType == + BLOOD_PRESSURE_DIASTOLIC + ) + record.diastolic + .inMillimetersOfMercury + else + record.systolic + .inMillimetersOfMercury, + "date_from" to + record.time + .toEpochMilli(), + "date_to" to + record.time + .toEpochMilli(), + "source_id" to "", + "source_name" to + metadata.dataOrigin + .packageName, + ), + ) - private fun getHealthConnectSdkStatus(call: MethodCall, result: Result) { - checkAvailability() - if (healthConnectAvailable) { - healthConnectClient = - HealthConnectClient.getOrCreate( - context!! - ) - } - result.success(healthConnectStatus) - } + is OxygenSaturationRecord -> + return listOf( + mapOf( + "value" to + record.percentage + .value, + "date_from" to + record.time + .toEpochMilli(), + "date_to" to + record.time + .toEpochMilli(), + "source_id" to "", + "source_name" to + metadata.dataOrigin + .packageName, + ), + ) - private fun hasPermissionsHC(call: MethodCall, result: Result) { - val args = call.arguments as HashMap<*, *> - val types = (args["types"] as? ArrayList<*>)?.filterIsInstance()!! - val permissions = (args["permissions"] as? ArrayList<*>)?.filterIsInstance()!! + is BloodGlucoseRecord -> + return listOf( + mapOf( + "value" to + record.level + .inMilligramsPerDeciliter, + "date_from" to + record.time + .toEpochMilli(), + "date_to" to + record.time + .toEpochMilli(), + "source_id" to "", + "source_name" to + metadata.dataOrigin + .packageName, + ), + ) - var permList = mutableListOf() - for ((i, typeKey) in types.withIndex()) { - if (!MapToHCType.containsKey(typeKey)) { - Log.w( - "FLUTTER_HEALTH::ERROR", - "Datatype " + typeKey + " not found in HC" - ) - result.success(false) - return - } - val access = permissions[i] - val dataType = MapToHCType[typeKey]!! - if (access == 0) { - permList.add( - HealthPermission.getReadPermission(dataType), - ) - } else { - permList.addAll( - listOf( - HealthPermission.getReadPermission( - dataType - ), - HealthPermission.getWritePermission( - dataType - ), - ), - ) - } - // Workout also needs distance and total energy burned too - if (typeKey == WORKOUT) { - if (access == 0) { - permList.addAll( - listOf( - HealthPermission.getReadPermission( - DistanceRecord::class - ), - HealthPermission.getReadPermission( - TotalCaloriesBurnedRecord::class - ), - ), - ) - } else { - permList.addAll( - listOf( - HealthPermission.getReadPermission( - DistanceRecord::class - ), - HealthPermission.getReadPermission( - TotalCaloriesBurnedRecord::class - ), - HealthPermission.getWritePermission( - DistanceRecord::class - ), - HealthPermission.getWritePermission( - TotalCaloriesBurnedRecord::class - ), - ), - ) - } - } - } - scope.launch { - result.success( - healthConnectClient - .permissionController - .getGrantedPermissions() - .containsAll(permList), - ) - } - } + is DistanceRecord -> + return listOf( + mapOf( + "value" to + record.distance + .inMeters, + "date_from" to + record.startTime + .toEpochMilli(), + "date_to" to + record.endTime + .toEpochMilli(), + "source_id" to "", + "source_name" to + metadata.dataOrigin + .packageName, + ), + ) - private fun requestAuthorizationHC(call: MethodCall, result: Result) { - val args = call.arguments as HashMap<*, *> - val types = (args["types"] as? ArrayList<*>)?.filterIsInstance()!! - val permissions = (args["permissions"] as? ArrayList<*>)?.filterIsInstance()!! + is HydrationRecord -> + return listOf( + mapOf( + "value" to + record.volume + .inLiters, + "date_from" to + record.startTime + .toEpochMilli(), + "date_to" to + record.endTime + .toEpochMilli(), + "source_id" to "", + "source_name" to + metadata.dataOrigin + .packageName, + ), + ) - var permList = mutableListOf() - for ((i, typeKey) in types.withIndex()) { - if (!MapToHCType.containsKey(typeKey)) { - Log.w( - "FLUTTER_HEALTH::ERROR", - "Datatype " + typeKey + " not found in HC" - ) - result.success(false) - return - } - val access = permissions[i]!! - val dataType = MapToHCType[typeKey]!! - if (access == 0) { - permList.add( - HealthPermission.getReadPermission(dataType), - ) - } else { - permList.addAll( - listOf( - HealthPermission.getReadPermission( - dataType - ), - HealthPermission.getWritePermission( - dataType - ), - ), - ) - } - // Workout also needs distance and total energy burned too - if (typeKey == WORKOUT) { - if (access == 0) { - permList.addAll( - listOf( - HealthPermission.getReadPermission( - DistanceRecord::class - ), - HealthPermission.getReadPermission( - TotalCaloriesBurnedRecord::class - ), - ), - ) - } else { - permList.addAll( - listOf( - HealthPermission.getReadPermission( - DistanceRecord::class - ), - HealthPermission.getReadPermission( - TotalCaloriesBurnedRecord::class - ), - HealthPermission.getWritePermission( - DistanceRecord::class - ), - HealthPermission.getWritePermission( - TotalCaloriesBurnedRecord::class - ), - ), - ) - } - } - } - if (healthConnectRequestPermissionsLauncher == null) { - result.success(false) - Log.i("FLUTTER_HEALTH", "Permission launcher not found") - return - } + is TotalCaloriesBurnedRecord -> + return listOf( + mapOf( + "value" to + record.energy + .inKilocalories, + "date_from" to + record.startTime + .toEpochMilli(), + "date_to" to + record.endTime + .toEpochMilli(), + "source_id" to "", + "source_name" to + metadata.dataOrigin + .packageName, + ), + ) - healthConnectRequestPermissionsLauncher!!.launch(permList.toSet()) - } - - fun getHCData(call: MethodCall, result: Result) { - val dataType = call.argument("dataTypeKey")!! - val startTime = Instant.ofEpochMilli(call.argument("startTime")!!) - val endTime = Instant.ofEpochMilli(call.argument("endTime")!!) - val healthConnectData = mutableListOf>() - scope.launch { - MapToHCType[dataType]?.let { classType -> - val records = mutableListOf() - - // Set up the initial request to read health records with specified - // parameters - var request = - ReadRecordsRequest( - recordType = classType, - // Define the maximum amount of data - // that HealthConnect can return - // in a single request - timeRangeFilter = - TimeRangeFilter.between( - startTime, - endTime - ), - ) + is BasalMetabolicRateRecord -> + return listOf( + mapOf( + "value" to + record.basalMetabolicRate + .inKilocaloriesPerDay, + "date_from" to + record.time + .toEpochMilli(), + "date_to" to + record.time + .toEpochMilli(), + "source_id" to "", + "source_name" to + metadata.dataOrigin + .packageName, + ), + ) - var response = healthConnectClient.readRecords(request) - var pageToken = response.pageToken - - // Add the records from the initial response to the records list - records.addAll(response.records) - - // Continue making requests and fetching records while there is a - // page token - while (!pageToken.isNullOrEmpty()) { - request = - ReadRecordsRequest( - recordType = classType, - timeRangeFilter = - TimeRangeFilter.between( - startTime, - endTime - ), - pageToken = pageToken - ) - response = healthConnectClient.readRecords(request) - - pageToken = response.pageToken - records.addAll(response.records) - } + is SleepSessionRecord -> + return listOf( + mapOf( + "date_from" to + record.startTime + .toEpochMilli(), + "date_to" to + record.endTime + .toEpochMilli(), + "value" to + ChronoUnit.MINUTES + .between( + record.startTime, + record.endTime + ), + "source_id" to "", + "source_name" to + metadata.dataOrigin + .packageName, + ), + ) - // Workout needs distance and total calories burned too - if (dataType == WORKOUT) { - for (rec in records) { - val record = rec as ExerciseSessionRecord - val distanceRequest = - healthConnectClient.readRecords( - ReadRecordsRequest( - recordType = - DistanceRecord::class, - timeRangeFilter = - TimeRangeFilter.between( - record.startTime, - record.endTime, - ), - ), - ) - var totalDistance = 0.0 - for (distanceRec in distanceRequest.records) { - totalDistance += - distanceRec.distance - .inMeters - } - - val energyBurnedRequest = - healthConnectClient.readRecords( - ReadRecordsRequest( - recordType = - TotalCaloriesBurnedRecord::class, - timeRangeFilter = - TimeRangeFilter.between( - record.startTime, - record.endTime, - ), - ), - ) - var totalEnergyBurned = 0.0 - for (energyBurnedRec in - energyBurnedRequest.records) { - totalEnergyBurned += - energyBurnedRec.energy - .inKilocalories - } - - val stepRequest = - healthConnectClient.readRecords( - ReadRecordsRequest( - recordType = - StepsRecord::class, - timeRangeFilter = - TimeRangeFilter.between( - record.startTime, - record.endTime - ), - ), - ) - var totalSteps = 0.0 - for (stepRec in stepRequest.records) { - totalSteps += stepRec.count - } - - // val metadata = (rec as Record).metadata - // Add final datapoint - healthConnectData.add( - // mapOf( - mapOf( - "workoutActivityType" to - (workoutTypeMapHealthConnect - .filterValues { - it == - record.exerciseType - } - .keys - .firstOrNull() - ?: "OTHER"), - "totalDistance" to - if (totalDistance == - 0.0 - ) - null - else - totalDistance, - "totalDistanceUnit" to - "METER", - "totalEnergyBurned" to - if (totalEnergyBurned == - 0.0 - ) - null - else - totalEnergyBurned, - "totalEnergyBurnedUnit" to - "KILOCALORIE", - "totalSteps" to - if (totalSteps == - 0.0 - ) - null - else - totalSteps, - "totalStepsUnit" to - "COUNT", - "unit" to "MINUTES", - "date_from" to - rec.startTime - .toEpochMilli(), - "date_to" to - rec.endTime.toEpochMilli(), - "source_id" to "", - "source_name" to - record.metadata - .dataOrigin - .packageName, - ), - ) - } - // Filter sleep stages for requested stage - } else if (classType == SleepSessionRecord::class) { - for (rec in response.records) { - 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 - ) - ) - } - } - } - } - } - } else { - for (rec in records) { - healthConnectData.addAll( - convertRecord(rec, dataType) - ) - } - } - } - Handler(context!!.mainLooper).run { result.success(healthConnectData) } - } - } + is RestingHeartRateRecord -> + return listOf( + mapOf( + "value" to + record.beatsPerMinute, + "date_from" to + record.time + .toEpochMilli(), + "date_to" to + record.time + .toEpochMilli(), + "source_id" to "", + "source_name" to + metadata.dataOrigin + .packageName, + ) + ) - fun convertRecordStage( - stage: SleepSessionRecord.Stage, - dataType: String, - sourceName: String - ): List> { + is BasalMetabolicRateRecord -> return listOf( - mapOf( - "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, - ), + mapOf( + "value" to + record.basalMetabolicRate + .inKilocaloriesPerDay, + "date_from" to + record.time + .toEpochMilli(), + "date_to" to + record.time + .toEpochMilli(), + "source_id" to "", + "source_name" to + metadata.dataOrigin + .packageName, + ) ) - } - fun getAggregateHCData(call: MethodCall, result: Result) { - val dataType = call.argument("dataTypeKey")!! - val interval = call.argument("interval")!! - val startTime = Instant.ofEpochMilli(call.argument("startTime")!!) - val endTime = Instant.ofEpochMilli(call.argument("endTime")!!) - val healthConnectData = mutableListOf>() - scope.launch { - MapToHCAggregateMetric[dataType]?.let { metricClassType -> - val request = - AggregateGroupByDurationRequest( - metrics = setOf(metricClassType), - timeRangeFilter = - TimeRangeFilter.between( - startTime, - endTime - ), - timeRangeSlicer = - Duration.ofSeconds( - interval - ) - ) - val response = healthConnectClient.aggregateGroupByDuration(request) - - for (durationResult in response) { - // The result may be null if no data is available in the - // time range - var totalValue = durationResult.result[metricClassType] - if (totalValue is Length) { - totalValue = totalValue.inMeters - } else if (totalValue is Energy) { - totalValue = totalValue.inKilocalories - } - - val packageNames = - durationResult.result.dataOrigins - .joinToString { origin -> - "${origin.packageName}" - } - - val data = - mapOf( - "value" to - (totalValue - ?: 0), - "date_from" to - durationResult.startTime - .toEpochMilli(), - "date_to" to - durationResult.endTime - .toEpochMilli(), - "source_name" to - packageNames, - "source_id" to "", - "is_manual_entry" to - packageNames.contains( - "user_input" - ) - ) - healthConnectData.add(data) - } - } - Handler(context!!.mainLooper).run { result.success(healthConnectData) } - } - } + is FloorsClimbedRecord -> + return listOf( + mapOf( + "value" to record.floors, + "date_from" to + record.startTime + .toEpochMilli(), + "date_to" to + record.endTime + .toEpochMilli(), + "source_id" to "", + "source_name" to + metadata.dataOrigin + .packageName, + ) + ) - // TODO: Find alternative to SOURCE_ID or make it nullable? - fun convertRecord(record: Any, dataType: String): List> { - val metadata = (record as Record).metadata - when (record) { - is WeightRecord -> - return listOf( - mapOf( - "value" to - record.weight - .inKilograms, - "date_from" to - record.time - .toEpochMilli(), - "date_to" to - record.time - .toEpochMilli(), - "source_id" to "", - "source_name" to - metadata.dataOrigin - .packageName, - ), - ) - is HeightRecord -> - return listOf( - mapOf( - "value" to - record.height - .inMeters, - "date_from" to - record.time - .toEpochMilli(), - "date_to" to - record.time - .toEpochMilli(), - "source_id" to "", - "source_name" to - metadata.dataOrigin - .packageName, - ), - ) - is BodyFatRecord -> - return listOf( - mapOf( - "value" to - record.percentage - .value, - "date_from" to - record.time - .toEpochMilli(), - "date_to" to - record.time - .toEpochMilli(), - "source_id" to "", - "source_name" to - metadata.dataOrigin - .packageName, - ), - ) - is StepsRecord -> - return listOf( - mapOf( - "value" to record.count, - "date_from" to - record.startTime - .toEpochMilli(), - "date_to" to - record.endTime - .toEpochMilli(), - "source_id" to "", - "source_name" to - metadata.dataOrigin - .packageName, - ), - ) - is ActiveCaloriesBurnedRecord -> - return listOf( - mapOf( - "value" to - record.energy - .inKilocalories, - "date_from" to - record.startTime - .toEpochMilli(), - "date_to" to - record.endTime - .toEpochMilli(), - "source_id" to "", - "source_name" to - metadata.dataOrigin - .packageName, - ), - ) - is HeartRateRecord -> - return record.samples.map { - mapOf( - "value" to it.beatsPerMinute, - "date_from" to - it.time.toEpochMilli(), - "date_to" to it.time.toEpochMilli(), - "source_id" to "", - "source_name" to - metadata.dataOrigin - .packageName, - ) - } - is BodyTemperatureRecord -> - return listOf( - mapOf( - "value" to - record.temperature - .inCelsius, - "date_from" to - record.time - .toEpochMilli(), - "date_to" to - record.time - .toEpochMilli(), - "source_id" to "", - "source_name" to - metadata.dataOrigin - .packageName, - ), - ) - is BodyWaterMassRecord -> - return listOf( - mapOf( - "value" to - record.mass - .inKilograms, - "date_from" to - record.time - .toEpochMilli(), - "date_to" to - record.time - .toEpochMilli(), - "source_id" to "", - "source_name" to - metadata.dataOrigin - .packageName, - ), - ) - is BloodPressureRecord -> - return listOf( - mapOf( - "value" to - if (dataType == - BLOOD_PRESSURE_DIASTOLIC - ) - record.diastolic - .inMillimetersOfMercury - else - record.systolic - .inMillimetersOfMercury, - "date_from" to - record.time - .toEpochMilli(), - "date_to" to - record.time - .toEpochMilli(), - "source_id" to "", - "source_name" to - metadata.dataOrigin - .packageName, - ), - ) - is OxygenSaturationRecord -> - return listOf( - mapOf( - "value" to - record.percentage - .value, - "date_from" to - record.time - .toEpochMilli(), - "date_to" to - record.time - .toEpochMilli(), - "source_id" to "", - "source_name" to - metadata.dataOrigin - .packageName, - ), - ) - is BloodGlucoseRecord -> - return listOf( - mapOf( - "value" to - record.level - .inMilligramsPerDeciliter, - "date_from" to - record.time - .toEpochMilli(), - "date_to" to - record.time - .toEpochMilli(), - "source_id" to "", - "source_name" to - metadata.dataOrigin - .packageName, - ), - ) - is DistanceRecord -> - return listOf( - mapOf( - "value" to - record.distance - .inMeters, - "date_from" to - record.startTime - .toEpochMilli(), - "date_to" to - record.endTime - .toEpochMilli(), - "source_id" to "", - "source_name" to - metadata.dataOrigin - .packageName, - ), - ) - is HydrationRecord -> - return listOf( - mapOf( - "value" to - record.volume - .inLiters, - "date_from" to - record.startTime - .toEpochMilli(), - "date_to" to - record.endTime - .toEpochMilli(), - "source_id" to "", - "source_name" to - metadata.dataOrigin - .packageName, - ), - ) - is TotalCaloriesBurnedRecord -> - return listOf( - mapOf( - "value" to - record.energy - .inKilocalories, - "date_from" to - record.startTime - .toEpochMilli(), - "date_to" to - record.endTime - .toEpochMilli(), - "source_id" to "", - "source_name" to - metadata.dataOrigin - .packageName, - ), - ) - is BasalMetabolicRateRecord -> - return listOf( - mapOf( - "value" to - record.basalMetabolicRate - .inKilocaloriesPerDay, - "date_from" to - record.time - .toEpochMilli(), - "date_to" to - record.time - .toEpochMilli(), - "source_id" to "", - "source_name" to - metadata.dataOrigin - .packageName, - ), - ) - is SleepSessionRecord -> - return listOf( - mapOf( - "date_from" to - record.startTime - .toEpochMilli(), - "date_to" to - record.endTime - .toEpochMilli(), - "value" to - ChronoUnit.MINUTES - .between( - record.startTime, - record.endTime - ), - "source_id" to "", - "source_name" to - metadata.dataOrigin - .packageName, - ), - ) - is RestingHeartRateRecord -> - return listOf( - mapOf( - "value" to - record.beatsPerMinute, - "date_from" to - record.time - .toEpochMilli(), - "date_to" to - record.time - .toEpochMilli(), - "source_id" to "", - "source_name" to - metadata.dataOrigin - .packageName, - ) - ) - is BasalMetabolicRateRecord -> - return listOf( - mapOf( - "value" to - record.basalMetabolicRate - .inKilocaloriesPerDay, - "date_from" to - record.time - .toEpochMilli(), - "date_to" to - record.time - .toEpochMilli(), - "source_id" to "", - "source_name" to - metadata.dataOrigin - .packageName, - ) - ) - is FloorsClimbedRecord -> - return listOf( - mapOf( - "value" to record.floors, - "date_from" to - record.startTime - .toEpochMilli(), - "date_to" to - record.endTime - .toEpochMilli(), - "source_id" to "", - "source_name" to - metadata.dataOrigin - .packageName, - ) - ) - is RespiratoryRateRecord -> - return listOf( - mapOf( - "value" to record.rate, - "date_from" to - record.time - .toEpochMilli(), - "date_to" to - record.time - .toEpochMilli(), - "source_id" to "", - "source_name" to - metadata.dataOrigin - .packageName, - ) - ) - is NutritionRecord -> - return listOf( - mapOf( - "calories" to - record.energy!!.inKilocalories, - "protein" to - record.protein!!.inGrams, - "carbs" to - record.totalCarbohydrate!! - .inGrams, - "fat" to - record.totalFat!! - .inGrams, - "name" to record.name!!, - "mealType" to - (MapTypeToMealTypeHC[ - record.mealType] - ?: MEAL_TYPE_UNKNOWN), - "date_from" to - record.startTime - .toEpochMilli(), - "date_to" to - record.endTime - .toEpochMilli(), - "source_id" to "", - "source_name" to - metadata.dataOrigin - .packageName, - ) - ) - // is ExerciseSessionRecord -> return listOf(mapOf("value" to , - // "date_from" to , - // "date_to" to , - // "source_id" to "", - // "source_name" to - // metadata.dataOrigin.packageName)) - else -> - throw IllegalArgumentException( - "Health data type not supported" - ) // TODO: Exception or error? - } - } + is RespiratoryRateRecord -> + return listOf( + mapOf( + "value" to record.rate, + "date_from" to + record.time + .toEpochMilli(), + "date_to" to + record.time + .toEpochMilli(), + "source_id" to "", + "source_name" to + metadata.dataOrigin + .packageName, + ) + ) - // TODO rewrite sleep to fit new update better --> compare with Apple and see if we should - // not - // adopt a single type with attached stages approach - fun writeHCData(call: MethodCall, result: Result) { - val type = call.argument("dataTypeKey")!! - val startTime = call.argument("startTime")!! - val endTime = call.argument("endTime")!! - val value = call.argument("value")!! - val record = - when (type) { - BODY_FAT_PERCENTAGE -> - BodyFatRecord( - time = - Instant.ofEpochMilli( - startTime - ), - percentage = - Percentage( - value - ), - zoneOffset = null, - ) - HEIGHT -> - HeightRecord( - time = - Instant.ofEpochMilli( - startTime - ), - height = - Length.meters( - value - ), - zoneOffset = null, - ) - WEIGHT -> - WeightRecord( - time = - Instant.ofEpochMilli( - startTime - ), - weight = - Mass.kilograms( - value - ), - zoneOffset = null, - ) - STEPS -> - StepsRecord( - startTime = - Instant.ofEpochMilli( - startTime - ), - endTime = - Instant.ofEpochMilli( - endTime - ), - count = value.toLong(), - startZoneOffset = null, - endZoneOffset = null, - ) - ACTIVE_ENERGY_BURNED -> - ActiveCaloriesBurnedRecord( - startTime = - Instant.ofEpochMilli( - startTime - ), - endTime = - Instant.ofEpochMilli( - endTime - ), - energy = - Energy.kilocalories( - value - ), - startZoneOffset = null, - endZoneOffset = null, - ) - HEART_RATE -> - HeartRateRecord( - startTime = - Instant.ofEpochMilli( - startTime - ), - endTime = - Instant.ofEpochMilli( - endTime - ), - samples = - listOf< - HeartRateRecord.Sample>( - HeartRateRecord.Sample( - time = - Instant.ofEpochMilli( - startTime - ), - beatsPerMinute = - value.toLong(), - ), - ), - startZoneOffset = null, - endZoneOffset = null, - ) - BODY_TEMPERATURE -> - BodyTemperatureRecord( - time = - Instant.ofEpochMilli( - startTime - ), - temperature = - Temperature.celsius( - value - ), - zoneOffset = null, - ) - BODY_WATER_MASS -> - BodyWaterMassRecord( - time = - Instant.ofEpochMilli( - startTime - ), - mass = - Mass.kilograms( - value - ), - zoneOffset = null, - ) - BLOOD_OXYGEN -> - OxygenSaturationRecord( - time = - Instant.ofEpochMilli( - startTime - ), - percentage = - Percentage( - value - ), - zoneOffset = null, - ) - BLOOD_GLUCOSE -> - BloodGlucoseRecord( - time = - Instant.ofEpochMilli( - startTime - ), - level = - BloodGlucose.milligramsPerDeciliter( - value - ), - zoneOffset = null, - ) - DISTANCE_DELTA -> - DistanceRecord( - startTime = - Instant.ofEpochMilli( - startTime - ), - endTime = - Instant.ofEpochMilli( - endTime - ), - distance = - Length.meters( - value - ), - startZoneOffset = null, - endZoneOffset = null, - ) - WATER -> - HydrationRecord( - startTime = - Instant.ofEpochMilli( - startTime - ), - endTime = - Instant.ofEpochMilli( - endTime - ), - volume = - Volume.liters( - value - ), - startZoneOffset = null, - endZoneOffset = null, - ) - SLEEP_ASLEEP -> - SleepSessionRecord( - startTime = - Instant.ofEpochMilli( - startTime - ), - endTime = - Instant.ofEpochMilli( - endTime - ), - startZoneOffset = null, - endZoneOffset = null, - stages = - listOf( - SleepSessionRecord - .Stage( - Instant.ofEpochMilli( - startTime - ), - Instant.ofEpochMilli( - endTime - ), - SleepSessionRecord - .STAGE_TYPE_SLEEPING - ) - ), - ) - SLEEP_LIGHT -> - SleepSessionRecord( - startTime = - Instant.ofEpochMilli( - startTime - ), - endTime = - Instant.ofEpochMilli( - endTime - ), - startZoneOffset = null, - endZoneOffset = null, - stages = - listOf( - SleepSessionRecord - .Stage( - Instant.ofEpochMilli( - startTime - ), - Instant.ofEpochMilli( - endTime - ), - SleepSessionRecord - .STAGE_TYPE_LIGHT - ) - ), - ) - SLEEP_DEEP -> - SleepSessionRecord( - startTime = - Instant.ofEpochMilli( - startTime - ), - endTime = - Instant.ofEpochMilli( - endTime - ), - startZoneOffset = null, - endZoneOffset = null, - stages = - listOf( - SleepSessionRecord - .Stage( - Instant.ofEpochMilli( - startTime - ), - Instant.ofEpochMilli( - endTime - ), - SleepSessionRecord - .STAGE_TYPE_DEEP - ) - ), - ) - SLEEP_REM -> - SleepSessionRecord( - startTime = - Instant.ofEpochMilli( - startTime - ), - endTime = - Instant.ofEpochMilli( - endTime - ), - startZoneOffset = null, - endZoneOffset = null, - stages = - listOf( - SleepSessionRecord - .Stage( - Instant.ofEpochMilli( - startTime - ), - Instant.ofEpochMilli( - endTime - ), - SleepSessionRecord - .STAGE_TYPE_REM - ) - ), - ) - SLEEP_OUT_OF_BED -> - SleepSessionRecord( - startTime = - Instant.ofEpochMilli( - startTime - ), - endTime = - Instant.ofEpochMilli( - endTime - ), - startZoneOffset = null, - endZoneOffset = null, - stages = - listOf( - SleepSessionRecord - .Stage( - Instant.ofEpochMilli( - startTime - ), - Instant.ofEpochMilli( - endTime - ), - SleepSessionRecord - .STAGE_TYPE_OUT_OF_BED - ) - ), - ) - SLEEP_AWAKE -> - SleepSessionRecord( - startTime = - Instant.ofEpochMilli( - startTime - ), - endTime = - Instant.ofEpochMilli( - endTime - ), - startZoneOffset = null, - endZoneOffset = null, - stages = - listOf( - SleepSessionRecord - .Stage( - Instant.ofEpochMilli( - startTime - ), - Instant.ofEpochMilli( - endTime - ), - SleepSessionRecord - .STAGE_TYPE_AWAKE - ) - ), - ) - SLEEP_SESSION -> - SleepSessionRecord( - startTime = - Instant.ofEpochMilli( - startTime - ), - endTime = - Instant.ofEpochMilli( - endTime - ), - startZoneOffset = null, - endZoneOffset = null, - ) - RESTING_HEART_RATE -> - RestingHeartRateRecord( - time = - Instant.ofEpochMilli( - startTime - ), - beatsPerMinute = - value.toLong(), - zoneOffset = null, - ) - BASAL_ENERGY_BURNED -> - BasalMetabolicRateRecord( - time = - Instant.ofEpochMilli( - startTime - ), - basalMetabolicRate = - Power.kilocaloriesPerDay( - value - ), - zoneOffset = null, - ) - FLIGHTS_CLIMBED -> - FloorsClimbedRecord( - startTime = - Instant.ofEpochMilli( - startTime - ), - endTime = - Instant.ofEpochMilli( - endTime - ), - floors = value, - startZoneOffset = null, - endZoneOffset = null, - ) - RESPIRATORY_RATE -> - RespiratoryRateRecord( - time = - Instant.ofEpochMilli( - startTime - ), - rate = value, - zoneOffset = null, - ) - // AGGREGATE_STEP_COUNT -> StepsRecord() - TOTAL_CALORIES_BURNED -> - TotalCaloriesBurnedRecord( - startTime = - Instant.ofEpochMilli( - startTime - ), - endTime = - Instant.ofEpochMilli( - endTime - ), - energy = - Energy.kilocalories( - value - ), - startZoneOffset = null, - endZoneOffset = null, - ) - BLOOD_PRESSURE_SYSTOLIC -> - throw IllegalArgumentException( - "You must use the [writeBloodPressure] API " - ) - BLOOD_PRESSURE_DIASTOLIC -> - throw IllegalArgumentException( - "You must use the [writeBloodPressure] API " - ) - WORKOUT -> - throw IllegalArgumentException( - "You must use the [writeWorkoutData] API " - ) - NUTRITION -> - throw IllegalArgumentException( - "You must use the [writeMeal] API " - ) - else -> - throw IllegalArgumentException( - "The type $type was not supported by the Health plugin or you must use another API " - ) - } - scope.launch { - try { - healthConnectClient.insertRecords(listOf(record)) - result.success(true) - } catch (e: Exception) { - result.success(false) - } - } + is NutritionRecord -> + return listOf( + mapOf( + "calories" to + record.energy!!.inKilocalories, + "protein" to + record.protein!!.inGrams, + "carbs" to + record.totalCarbohydrate!! + .inGrams, + "fat" to + record.totalFat!! + .inGrams, + "name" to record.name!!, + "mealType" to + (MapTypeToMealTypeHC[ + record.mealType] + ?: MEAL_TYPE_UNKNOWN), + "date_from" to + record.startTime + .toEpochMilli(), + "date_to" to + record.endTime + .toEpochMilli(), + "source_id" to "", + "source_name" to + metadata.dataOrigin + .packageName, + ) + ) + // is ExerciseSessionRecord -> return listOf(mapOf("value" to , + // "date_from" to , + // "date_to" to , + // "source_id" to "", + // "source_name" to + // metadata.dataOrigin.packageName)) + else -> + throw IllegalArgumentException( + "Health data type not supported" + ) // TODO: Exception or error? } - - fun writeWorkoutHCData(call: MethodCall, result: Result) { - val type = call.argument("activityType")!! - val startTime = Instant.ofEpochMilli(call.argument("startTime")!!) - val endTime = Instant.ofEpochMilli(call.argument("endTime")!!) - val totalEnergyBurned = call.argument("totalEnergyBurned") - val totalDistance = call.argument("totalDistance") - if (workoutTypeMapHealthConnect.containsKey(type) == false) { - result.success(false) - Log.w( - "FLUTTER_HEALTH::ERROR", - "[Health Connect] Workout type not supported" - ) - return - } - val workoutType = workoutTypeMapHealthConnect[type]!! - val title = call.argument("title") ?: type - - scope.launch { - try { - val list = mutableListOf() - list.add( - ExerciseSessionRecord( - startTime = startTime, - startZoneOffset = null, - endTime = endTime, - endZoneOffset = null, - exerciseType = workoutType, - title = title, - ), - ) - if (totalDistance != null) { - list.add( - DistanceRecord( - startTime = startTime, - startZoneOffset = null, - endTime = endTime, - endZoneOffset = null, - distance = - Length.meters( - totalDistance.toDouble() - ), - ), - ) - } - if (totalEnergyBurned != null) { - list.add( - TotalCaloriesBurnedRecord( - startTime = startTime, - startZoneOffset = null, - endTime = endTime, - endZoneOffset = null, - energy = - Energy.kilocalories( - totalEnergyBurned - .toDouble() - ), - ), - ) - } - healthConnectClient.insertRecords( - list, + } + + // TODO rewrite sleep to fit new update better --> compare with Apple and see if we should + // not + // adopt a single type with attached stages approach + fun writeHCData(call: MethodCall, result: Result) { + val type = call.argument("dataTypeKey")!! + val startTime = call.argument("startTime")!! + val endTime = call.argument("endTime")!! + val value = call.argument("value")!! + val record = + when (type) { + BODY_FAT_PERCENTAGE -> + BodyFatRecord( + time = + Instant.ofEpochMilli( + startTime + ), + percentage = + Percentage( + value + ), + zoneOffset = null, + ) + + HEIGHT -> + HeightRecord( + time = + Instant.ofEpochMilli( + startTime + ), + height = + Length.meters( + value + ), + zoneOffset = null, + ) + + WEIGHT -> + WeightRecord( + time = + Instant.ofEpochMilli( + startTime + ), + weight = + Mass.kilograms( + value + ), + zoneOffset = null, + ) + + STEPS -> + StepsRecord( + startTime = + Instant.ofEpochMilli( + startTime + ), + endTime = + Instant.ofEpochMilli( + endTime + ), + count = value.toLong(), + startZoneOffset = null, + endZoneOffset = null, + ) + + ACTIVE_ENERGY_BURNED -> + ActiveCaloriesBurnedRecord( + startTime = + Instant.ofEpochMilli( + startTime + ), + endTime = + Instant.ofEpochMilli( + endTime + ), + energy = + Energy.kilocalories( + value + ), + startZoneOffset = null, + endZoneOffset = null, + ) + + HEART_RATE -> + HeartRateRecord( + startTime = + Instant.ofEpochMilli( + startTime + ), + endTime = + Instant.ofEpochMilli( + endTime + ), + samples = + listOf< + HeartRateRecord.Sample>( + HeartRateRecord.Sample( + time = + Instant.ofEpochMilli( + startTime + ), + beatsPerMinute = + value.toLong(), + ), + ), + startZoneOffset = null, + endZoneOffset = null, + ) + + BODY_TEMPERATURE -> + BodyTemperatureRecord( + time = + Instant.ofEpochMilli( + startTime + ), + temperature = + Temperature.celsius( + value + ), + zoneOffset = null, + ) + + BODY_WATER_MASS -> + BodyWaterMassRecord( + time = + Instant.ofEpochMilli( + startTime + ), + mass = + Mass.kilograms( + value + ), + zoneOffset = null, + ) + + BLOOD_OXYGEN -> + OxygenSaturationRecord( + time = + Instant.ofEpochMilli( + startTime + ), + percentage = + Percentage( + value + ), + zoneOffset = null, + ) + + BLOOD_GLUCOSE -> + BloodGlucoseRecord( + time = + Instant.ofEpochMilli( + startTime + ), + level = + BloodGlucose.milligramsPerDeciliter( + value + ), + zoneOffset = null, + ) + + DISTANCE_DELTA -> + DistanceRecord( + startTime = + Instant.ofEpochMilli( + startTime + ), + endTime = + Instant.ofEpochMilli( + endTime + ), + distance = + Length.meters( + value + ), + startZoneOffset = null, + endZoneOffset = null, + ) + + WATER -> + HydrationRecord( + startTime = + Instant.ofEpochMilli( + startTime + ), + endTime = + Instant.ofEpochMilli( + endTime + ), + volume = + Volume.liters( + value + ), + startZoneOffset = null, + endZoneOffset = null, + ) + + SLEEP_ASLEEP -> + SleepSessionRecord( + startTime = + Instant.ofEpochMilli( + startTime + ), + endTime = + Instant.ofEpochMilli( + endTime + ), + startZoneOffset = null, + endZoneOffset = null, + stages = + listOf( + SleepSessionRecord + .Stage( + Instant.ofEpochMilli( + startTime + ), + Instant.ofEpochMilli( + endTime + ), + SleepSessionRecord + .STAGE_TYPE_SLEEPING ) - result.success(true) - Log.i( - "FLUTTER_HEALTH::SUCCESS", - "[Health Connect] Workout was successfully added!" + ), + ) + + SLEEP_LIGHT -> + SleepSessionRecord( + startTime = + Instant.ofEpochMilli( + startTime + ), + endTime = + Instant.ofEpochMilli( + endTime + ), + startZoneOffset = null, + endZoneOffset = null, + stages = + listOf( + SleepSessionRecord + .Stage( + Instant.ofEpochMilli( + startTime + ), + Instant.ofEpochMilli( + endTime + ), + SleepSessionRecord + .STAGE_TYPE_LIGHT ) - } catch (e: Exception) { - Log.w( - "FLUTTER_HEALTH::ERROR", - "[Health Connect] There was an error adding the workout", + ), + ) + + SLEEP_DEEP -> + SleepSessionRecord( + startTime = + Instant.ofEpochMilli( + startTime + ), + endTime = + Instant.ofEpochMilli( + endTime + ), + startZoneOffset = null, + endZoneOffset = null, + stages = + listOf( + SleepSessionRecord + .Stage( + Instant.ofEpochMilli( + startTime + ), + Instant.ofEpochMilli( + endTime + ), + SleepSessionRecord + .STAGE_TYPE_DEEP ) - Log.w("FLUTTER_HEALTH::ERROR", e.message ?: "unknown error") - Log.w("FLUTTER_HEALTH::ERROR", e.stackTrace.toString()) - result.success(false) - } - } - } - - fun writeBloodPressureHC(call: MethodCall, result: Result) { - val systolic = call.argument("systolic")!! - val diastolic = call.argument("diastolic")!! - val startTime = Instant.ofEpochMilli(call.argument("startTime")!!) - val endTime = Instant.ofEpochMilli(call.argument("endTime")!!) - - scope.launch { - try { - healthConnectClient.insertRecords( - listOf( - BloodPressureRecord( - time = startTime, - systolic = - Pressure.millimetersOfMercury( - systolic - ), - diastolic = - Pressure.millimetersOfMercury( - diastolic - ), - zoneOffset = null, - ), - ), + ), + ) + + SLEEP_REM -> + SleepSessionRecord( + startTime = + Instant.ofEpochMilli( + startTime + ), + endTime = + Instant.ofEpochMilli( + endTime + ), + startZoneOffset = null, + endZoneOffset = null, + stages = + listOf( + SleepSessionRecord + .Stage( + Instant.ofEpochMilli( + startTime + ), + Instant.ofEpochMilli( + endTime + ), + SleepSessionRecord + .STAGE_TYPE_REM ) - result.success(true) - Log.i( - "FLUTTER_HEALTH::SUCCESS", - "[Health Connect] Blood pressure was successfully added!", + ), + ) + + SLEEP_OUT_OF_BED -> + SleepSessionRecord( + startTime = + Instant.ofEpochMilli( + startTime + ), + endTime = + Instant.ofEpochMilli( + endTime + ), + startZoneOffset = null, + endZoneOffset = null, + stages = + listOf( + SleepSessionRecord + .Stage( + Instant.ofEpochMilli( + startTime + ), + Instant.ofEpochMilli( + endTime + ), + SleepSessionRecord + .STAGE_TYPE_OUT_OF_BED ) - } catch (e: Exception) { - Log.w( - "FLUTTER_HEALTH::ERROR", - "[Health Connect] There was an error adding the blood pressure", + ), + ) + + SLEEP_AWAKE -> + SleepSessionRecord( + startTime = + Instant.ofEpochMilli( + startTime + ), + endTime = + Instant.ofEpochMilli( + endTime + ), + startZoneOffset = null, + endZoneOffset = null, + stages = + listOf( + SleepSessionRecord + .Stage( + Instant.ofEpochMilli( + startTime + ), + Instant.ofEpochMilli( + endTime + ), + SleepSessionRecord + .STAGE_TYPE_AWAKE ) - Log.w("FLUTTER_HEALTH::ERROR", e.message ?: "unknown error") - Log.w("FLUTTER_HEALTH::ERROR", e.stackTrace.toString()) - result.success(false) - } - } + ), + ) + + SLEEP_SESSION -> + SleepSessionRecord( + startTime = + Instant.ofEpochMilli( + startTime + ), + endTime = + Instant.ofEpochMilli( + endTime + ), + startZoneOffset = null, + endZoneOffset = null, + ) + + RESTING_HEART_RATE -> + RestingHeartRateRecord( + time = + Instant.ofEpochMilli( + startTime + ), + beatsPerMinute = + value.toLong(), + zoneOffset = null, + ) + + BASAL_ENERGY_BURNED -> + BasalMetabolicRateRecord( + time = + Instant.ofEpochMilli( + startTime + ), + basalMetabolicRate = + Power.kilocaloriesPerDay( + value + ), + zoneOffset = null, + ) + + FLIGHTS_CLIMBED -> + FloorsClimbedRecord( + startTime = + Instant.ofEpochMilli( + startTime + ), + endTime = + Instant.ofEpochMilli( + endTime + ), + floors = value, + startZoneOffset = null, + endZoneOffset = null, + ) + + RESPIRATORY_RATE -> + RespiratoryRateRecord( + time = + Instant.ofEpochMilli( + startTime + ), + rate = value, + zoneOffset = null, + ) + // AGGREGATE_STEP_COUNT -> StepsRecord() + TOTAL_CALORIES_BURNED -> + TotalCaloriesBurnedRecord( + startTime = + Instant.ofEpochMilli( + startTime + ), + endTime = + Instant.ofEpochMilli( + endTime + ), + energy = + Energy.kilocalories( + value + ), + startZoneOffset = null, + endZoneOffset = null, + ) + + BLOOD_PRESSURE_SYSTOLIC -> + throw IllegalArgumentException( + "You must use the [writeBloodPressure] API " + ) + + BLOOD_PRESSURE_DIASTOLIC -> + throw IllegalArgumentException( + "You must use the [writeBloodPressure] API " + ) + + WORKOUT -> + throw IllegalArgumentException( + "You must use the [writeWorkoutData] API " + ) + + NUTRITION -> + throw IllegalArgumentException( + "You must use the [writeMeal] API " + ) + + else -> + throw IllegalArgumentException( + "The type $type was not supported by the Health plugin or you must use another API " + ) + } + scope.launch { + try { + healthConnectClient.insertRecords(listOf(record)) + result.success(true) + } catch (e: Exception) { + result.success(false) + } } - - fun deleteHCData(call: MethodCall, result: Result) { - val type = call.argument("dataTypeKey")!! - val startTime = Instant.ofEpochMilli(call.argument("startTime")!!) - val endTime = Instant.ofEpochMilli(call.argument("endTime")!!) - if (!MapToHCType.containsKey(type)) { - Log.w("FLUTTER_HEALTH::ERROR", "Datatype " + type + " not found in HC") - result.success(false) - return + } + + fun writeWorkoutHCData(call: MethodCall, result: Result) { + val type = call.argument("activityType")!! + val startTime = Instant.ofEpochMilli(call.argument("startTime")!!) + val endTime = Instant.ofEpochMilli(call.argument("endTime")!!) + val totalEnergyBurned = call.argument("totalEnergyBurned") + val totalDistance = call.argument("totalDistance") + if (workoutTypeMapHealthConnect.containsKey(type) == false) { + result.success(false) + Log.w( + "FLUTTER_HEALTH::ERROR", + "[Health Connect] Workout type not supported" + ) + return + } + val workoutType = workoutTypeMapHealthConnect[type]!! + val title = call.argument("title") ?: type + + scope.launch { + try { + val list = mutableListOf() + list.add( + ExerciseSessionRecord( + startTime = startTime, + startZoneOffset = null, + endTime = endTime, + endZoneOffset = null, + exerciseType = workoutType, + title = title, + ), + ) + if (totalDistance != null) { + list.add( + DistanceRecord( + startTime = startTime, + startZoneOffset = null, + endTime = endTime, + endZoneOffset = null, + distance = + Length.meters( + totalDistance.toDouble() + ), + ), + ) } - val classType = MapToHCType[type]!! - - scope.launch { - try { - healthConnectClient.deleteRecords( - recordType = classType, - timeRangeFilter = - TimeRangeFilter.between( - startTime, - endTime - ), - ) - result.success(true) - } catch (e: Exception) { - result.success(false) - } + if (totalEnergyBurned != null) { + list.add( + TotalCaloriesBurnedRecord( + startTime = startTime, + startZoneOffset = null, + endTime = endTime, + endZoneOffset = null, + energy = + Energy.kilocalories( + totalEnergyBurned + .toDouble() + ), + ), + ) } + healthConnectClient.insertRecords( + list, + ) + result.success(true) + Log.i( + "FLUTTER_HEALTH::SUCCESS", + "[Health Connect] Workout was successfully added!" + ) + } catch (e: Exception) { + Log.w( + "FLUTTER_HEALTH::ERROR", + "[Health Connect] There was an error adding the workout", + ) + Log.w("FLUTTER_HEALTH::ERROR", e.message ?: "unknown error") + Log.w("FLUTTER_HEALTH::ERROR", e.stackTrace.toString()) + result.success(false) + } } - - val MapSleepStageToType = - hashMapOf( - 1 to SLEEP_AWAKE, - 2 to SLEEP_ASLEEP, - 3 to SLEEP_OUT_OF_BED, - 4 to SLEEP_LIGHT, - 5 to SLEEP_DEEP, - 6 to SLEEP_REM, - ) - - private val MapMealTypeToTypeHC = - hashMapOf( - BREAKFAST to MEAL_TYPE_BREAKFAST, - LUNCH to MEAL_TYPE_LUNCH, - DINNER to MEAL_TYPE_DINNER, - SNACK to MEAL_TYPE_SNACK, - MEAL_UNKNOWN to MEAL_TYPE_UNKNOWN, - ) - - private val MapTypeToMealTypeHC = - hashMapOf( - MEAL_TYPE_BREAKFAST to BREAKFAST, - MEAL_TYPE_LUNCH to LUNCH, - MEAL_TYPE_DINNER to DINNER, - MEAL_TYPE_SNACK to SNACK, - MEAL_TYPE_UNKNOWN to MEAL_UNKNOWN, - ) - - private val MapMealTypeToType = - hashMapOf( - BREAKFAST to Field.MEAL_TYPE_BREAKFAST, - LUNCH to Field.MEAL_TYPE_LUNCH, - DINNER to Field.MEAL_TYPE_DINNER, - SNACK to Field.MEAL_TYPE_SNACK, - MEAL_UNKNOWN to Field.MEAL_TYPE_UNKNOWN, - ) - - val MapToHCType = - hashMapOf( - BODY_FAT_PERCENTAGE to BodyFatRecord::class, - HEIGHT to HeightRecord::class, - WEIGHT to WeightRecord::class, - STEPS to StepsRecord::class, - AGGREGATE_STEP_COUNT to StepsRecord::class, - ACTIVE_ENERGY_BURNED to ActiveCaloriesBurnedRecord::class, - HEART_RATE to HeartRateRecord::class, - BODY_TEMPERATURE to BodyTemperatureRecord::class, - BODY_WATER_MASS to BodyWaterMassRecord::class, - BLOOD_PRESSURE_SYSTOLIC to BloodPressureRecord::class, - BLOOD_PRESSURE_DIASTOLIC to BloodPressureRecord::class, - BLOOD_OXYGEN to OxygenSaturationRecord::class, - BLOOD_GLUCOSE to BloodGlucoseRecord::class, - DISTANCE_DELTA to DistanceRecord::class, - WATER to HydrationRecord::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, - NUTRITION to NutritionRecord::class, - RESTING_HEART_RATE to RestingHeartRateRecord::class, - BASAL_ENERGY_BURNED to BasalMetabolicRateRecord::class, - FLIGHTS_CLIMBED to FloorsClimbedRecord::class, - RESPIRATORY_RATE to RespiratoryRateRecord::class, - TOTAL_CALORIES_BURNED to TotalCaloriesBurnedRecord::class - // MOVE_MINUTES to TODO: Find alternative? - // TODO: Implement remaining types - // "ActiveCaloriesBurned" to - // ActiveCaloriesBurnedRecord::class, - // "BasalBodyTemperature" to - // BasalBodyTemperatureRecord::class, - // "BasalMetabolicRate" to BasalMetabolicRateRecord::class, - // "BloodGlucose" to BloodGlucoseRecord::class, - // "BloodPressure" to BloodPressureRecord::class, - // "BodyFat" to BodyFatRecord::class, - // "BodyTemperature" to BodyTemperatureRecord::class, - // "BoneMass" to BoneMassRecord::class, - // "CervicalMucus" to CervicalMucusRecord::class, - // "CyclingPedalingCadence" to - // CyclingPedalingCadenceRecord::class, - // "Distance" to DistanceRecord::class, - // "ElevationGained" to ElevationGainedRecord::class, - // "ExerciseSession" to ExerciseSessionRecord::class, - // "FloorsClimbed" to FloorsClimbedRecord::class, - // "HeartRate" to HeartRateRecord::class, - // "Height" to HeightRecord::class, - // "Hydration" to HydrationRecord::class, - // "LeanBodyMass" to LeanBodyMassRecord::class, - // "MenstruationFlow" to MenstruationFlowRecord::class, - // "MenstruationPeriod" to MenstruationPeriodRecord::class, - // "Nutrition" to NutritionRecord::class, - // "OvulationTest" to OvulationTestRecord::class, - // "OxygenSaturation" to OxygenSaturationRecord::class, - // "Power" to PowerRecord::class, - // "RespiratoryRate" to RespiratoryRateRecord::class, - // "RestingHeartRate" to RestingHeartRateRecord::class, - // "SexualActivity" to SexualActivityRecord::class, - // "SleepSession" to SleepSessionRecord::class, - // "SleepStage" to SleepStageRecord::class, - // "Speed" to SpeedRecord::class, - // "StepsCadence" to StepsCadenceRecord::class, - // "Steps" to StepsRecord::class, - // "TotalCaloriesBurned" to - // TotalCaloriesBurnedRecord::class, - // "Vo2Max" to Vo2MaxRecord::class, - // "Weight" to WeightRecord::class, - // "WheelchairPushes" to WheelchairPushesRecord::class, - ) - - val MapToHCAggregateMetric = - hashMapOf( - HEIGHT to HeightRecord.HEIGHT_AVG, - WEIGHT to WeightRecord.WEIGHT_AVG, - STEPS to StepsRecord.COUNT_TOTAL, - AGGREGATE_STEP_COUNT to StepsRecord.COUNT_TOTAL, - ACTIVE_ENERGY_BURNED to - ActiveCaloriesBurnedRecord - .ACTIVE_CALORIES_TOTAL, - HEART_RATE to HeartRateRecord.MEASUREMENTS_COUNT, - DISTANCE_DELTA to DistanceRecord.DISTANCE_TOTAL, - WATER to HydrationRecord.VOLUME_TOTAL, - SLEEP_ASLEEP to SleepSessionRecord.SLEEP_DURATION_TOTAL, - SLEEP_AWAKE to SleepSessionRecord.SLEEP_DURATION_TOTAL, - SLEEP_IN_BED to SleepSessionRecord.SLEEP_DURATION_TOTAL, - TOTAL_CALORIES_BURNED to - TotalCaloriesBurnedRecord.ENERGY_TOTAL - ) + } + + fun writeBloodPressureHC(call: MethodCall, result: Result) { + val systolic = call.argument("systolic")!! + val diastolic = call.argument("diastolic")!! + val startTime = Instant.ofEpochMilli(call.argument("startTime")!!) + val endTime = Instant.ofEpochMilli(call.argument("endTime")!!) + + scope.launch { + try { + healthConnectClient.insertRecords( + listOf( + BloodPressureRecord( + time = startTime, + systolic = + Pressure.millimetersOfMercury( + systolic + ), + diastolic = + Pressure.millimetersOfMercury( + diastolic + ), + zoneOffset = null, + ), + ), + ) + result.success(true) + Log.i( + "FLUTTER_HEALTH::SUCCESS", + "[Health Connect] Blood pressure was successfully added!", + ) + } catch (e: Exception) { + Log.w( + "FLUTTER_HEALTH::ERROR", + "[Health Connect] There was an error adding the blood pressure", + ) + Log.w("FLUTTER_HEALTH::ERROR", e.message ?: "unknown error") + Log.w("FLUTTER_HEALTH::ERROR", e.stackTrace.toString()) + result.success(false) + } + } + } + + fun deleteHCData(call: MethodCall, result: Result) { + val type = call.argument("dataTypeKey")!! + val startTime = Instant.ofEpochMilli(call.argument("startTime")!!) + val endTime = Instant.ofEpochMilli(call.argument("endTime")!!) + if (!MapToHCType.containsKey(type)) { + Log.w("FLUTTER_HEALTH::ERROR", "Datatype " + type + " not found in HC") + result.success(false) + return + } + val classType = MapToHCType[type]!! + + scope.launch { + try { + healthConnectClient.deleteRecords( + recordType = classType, + timeRangeFilter = + TimeRangeFilter.between( + startTime, + endTime + ), + ) + result.success(true) + } catch (e: Exception) { + result.success(false) + } + } + } + + val MapSleepStageToType = + hashMapOf( + 1 to SLEEP_AWAKE, + 2 to SLEEP_ASLEEP, + 3 to SLEEP_OUT_OF_BED, + 4 to SLEEP_LIGHT, + 5 to SLEEP_DEEP, + 6 to SLEEP_REM, + ) + + private val MapMealTypeToTypeHC = + hashMapOf( + BREAKFAST to MEAL_TYPE_BREAKFAST, + LUNCH to MEAL_TYPE_LUNCH, + DINNER to MEAL_TYPE_DINNER, + SNACK to MEAL_TYPE_SNACK, + MEAL_UNKNOWN to MEAL_TYPE_UNKNOWN, + ) + + private val MapTypeToMealTypeHC = + hashMapOf( + MEAL_TYPE_BREAKFAST to BREAKFAST, + MEAL_TYPE_LUNCH to LUNCH, + MEAL_TYPE_DINNER to DINNER, + MEAL_TYPE_SNACK to SNACK, + MEAL_TYPE_UNKNOWN to MEAL_UNKNOWN, + ) + + private val MapMealTypeToType = + hashMapOf( + BREAKFAST to Field.MEAL_TYPE_BREAKFAST, + LUNCH to Field.MEAL_TYPE_LUNCH, + DINNER to Field.MEAL_TYPE_DINNER, + SNACK to Field.MEAL_TYPE_SNACK, + MEAL_UNKNOWN to Field.MEAL_TYPE_UNKNOWN, + ) + + val MapToHCType = + hashMapOf( + BODY_FAT_PERCENTAGE to BodyFatRecord::class, + HEIGHT to HeightRecord::class, + WEIGHT to WeightRecord::class, + STEPS to StepsRecord::class, + AGGREGATE_STEP_COUNT to StepsRecord::class, + ACTIVE_ENERGY_BURNED to ActiveCaloriesBurnedRecord::class, + HEART_RATE to HeartRateRecord::class, + BODY_TEMPERATURE to BodyTemperatureRecord::class, + BODY_WATER_MASS to BodyWaterMassRecord::class, + BLOOD_PRESSURE_SYSTOLIC to BloodPressureRecord::class, + BLOOD_PRESSURE_DIASTOLIC to BloodPressureRecord::class, + BLOOD_OXYGEN to OxygenSaturationRecord::class, + BLOOD_GLUCOSE to BloodGlucoseRecord::class, + DISTANCE_DELTA to DistanceRecord::class, + WATER to HydrationRecord::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, + NUTRITION to NutritionRecord::class, + RESTING_HEART_RATE to RestingHeartRateRecord::class, + BASAL_ENERGY_BURNED to BasalMetabolicRateRecord::class, + FLIGHTS_CLIMBED to FloorsClimbedRecord::class, + RESPIRATORY_RATE to RespiratoryRateRecord::class, + TOTAL_CALORIES_BURNED to TotalCaloriesBurnedRecord::class + // MOVE_MINUTES to TODO: Find alternative? + // TODO: Implement remaining types + // "ActiveCaloriesBurned" to + // ActiveCaloriesBurnedRecord::class, + // "BasalBodyTemperature" to + // BasalBodyTemperatureRecord::class, + // "BasalMetabolicRate" to BasalMetabolicRateRecord::class, + // "BloodGlucose" to BloodGlucoseRecord::class, + // "BloodPressure" to BloodPressureRecord::class, + // "BodyFat" to BodyFatRecord::class, + // "BodyTemperature" to BodyTemperatureRecord::class, + // "BoneMass" to BoneMassRecord::class, + // "CervicalMucus" to CervicalMucusRecord::class, + // "CyclingPedalingCadence" to + // CyclingPedalingCadenceRecord::class, + // "Distance" to DistanceRecord::class, + // "ElevationGained" to ElevationGainedRecord::class, + // "ExerciseSession" to ExerciseSessionRecord::class, + // "FloorsClimbed" to FloorsClimbedRecord::class, + // "HeartRate" to HeartRateRecord::class, + // "Height" to HeightRecord::class, + // "Hydration" to HydrationRecord::class, + // "LeanBodyMass" to LeanBodyMassRecord::class, + // "MenstruationFlow" to MenstruationFlowRecord::class, + // "MenstruationPeriod" to MenstruationPeriodRecord::class, + // "Nutrition" to NutritionRecord::class, + // "OvulationTest" to OvulationTestRecord::class, + // "OxygenSaturation" to OxygenSaturationRecord::class, + // "Power" to PowerRecord::class, + // "RespiratoryRate" to RespiratoryRateRecord::class, + // "RestingHeartRate" to RestingHeartRateRecord::class, + // "SexualActivity" to SexualActivityRecord::class, + // "SleepSession" to SleepSessionRecord::class, + // "SleepStage" to SleepStageRecord::class, + // "Speed" to SpeedRecord::class, + // "StepsCadence" to StepsCadenceRecord::class, + // "Steps" to StepsRecord::class, + // "TotalCaloriesBurned" to + // TotalCaloriesBurnedRecord::class, + // "Vo2Max" to Vo2MaxRecord::class, + // "Weight" to WeightRecord::class, + // "WheelchairPushes" to WheelchairPushesRecord::class, + ) + + val MapToHCAggregateMetric = + hashMapOf( + HEIGHT to HeightRecord.HEIGHT_AVG, + WEIGHT to WeightRecord.WEIGHT_AVG, + STEPS to StepsRecord.COUNT_TOTAL, + AGGREGATE_STEP_COUNT to StepsRecord.COUNT_TOTAL, + ACTIVE_ENERGY_BURNED to + ActiveCaloriesBurnedRecord + .ACTIVE_CALORIES_TOTAL, + HEART_RATE to HeartRateRecord.MEASUREMENTS_COUNT, + DISTANCE_DELTA to DistanceRecord.DISTANCE_TOTAL, + WATER to HydrationRecord.VOLUME_TOTAL, + SLEEP_ASLEEP to SleepSessionRecord.SLEEP_DURATION_TOTAL, + SLEEP_AWAKE to SleepSessionRecord.SLEEP_DURATION_TOTAL, + SLEEP_IN_BED to SleepSessionRecord.SLEEP_DURATION_TOTAL, + TOTAL_CALORIES_BURNED to + TotalCaloriesBurnedRecord.ENERGY_TOTAL + ) } diff --git a/packages/health/android/src/main/kotlin/cachet/plugins/health/SensorStep.kt b/packages/health/android/src/main/kotlin/cachet/plugins/health/SensorStep.kt new file mode 100644 index 000000000..34e5b2765 --- /dev/null +++ b/packages/health/android/src/main/kotlin/cachet/plugins/health/SensorStep.kt @@ -0,0 +1,17 @@ +package cachet.plugins.health + +import io.objectbox.annotation.DatabaseType +import io.objectbox.annotation.Entity +import io.objectbox.annotation.Id +import io.objectbox.annotation.Type + +@Entity +class SensorStep { + @Id + var id: Long = 0 + @Type(DatabaseType.DateNano) + var startTime: Long? = 0 + @Type(DatabaseType.DateNano) + var endTime: Long? = 0 + var count: Double = 0.0 +} \ No newline at end of file diff --git a/packages/health/android/src/main/kotlin/cachet/plugins/health/StepCounterService.kt b/packages/health/android/src/main/kotlin/cachet/plugins/health/StepCounterService.kt new file mode 100644 index 000000000..380eec897 --- /dev/null +++ b/packages/health/android/src/main/kotlin/cachet/plugins/health/StepCounterService.kt @@ -0,0 +1,134 @@ +package cachet.plugins.health + +import android.app.Notification +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.Service +import android.content.Intent +import android.content.pm.ServiceInfo +import android.hardware.Sensor +import android.hardware.SensorEvent +import android.hardware.SensorEventListener +import android.hardware.SensorManager +import android.os.Build +import android.os.IBinder +import android.os.SystemClock +import android.util.Log +import androidx.core.app.NotificationCompat +import androidx.core.app.ServiceCompat +import io.objectbox.BoxStore + + +class StepCounterService : Service(), SensorEventListener { + + private lateinit var sensorManager: SensorManager + private lateinit var sensor: Sensor + + private var previousStepCount = 0 + private val maxStepsThreshold = Int.MAX_VALUE / 2 + + companion object { + lateinit var box: BoxStore; + const val LOG = "StepCounterService" + const val SENSOR_NAME = Sensor.TYPE_STEP_COUNTER + + const val CHANNEL_ID = "StepCounterServiceChannel" + const val CHANNEL_NAME = "Step Counter" + const val NOTIFICATION_ID = 1 + + fun initiated(): Boolean { + return this::box.isInitialized; + } + } + + override fun onBind(p0: Intent?): IBinder? { + return null + } + + override fun onCreate() { + super.onCreate() + if (!initiated()) { + box = MyObjectBox.builder() + .androidContext(applicationContext) + .build(); + } + + sensorManager = getSystemService(SENSOR_SERVICE) as SensorManager + sensor = sensorManager.getDefaultSensor(SENSOR_NAME)!! + } + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + startForegroundService() + + sensorManager.registerListener(this, sensor, SensorManager.SENSOR_DELAY_GAME) + + return START_STICKY + } + + private fun createNotificationChannel() { + val notificationChannel = NotificationChannel(CHANNEL_ID, CHANNEL_NAME, NotificationManager.IMPORTANCE_DEFAULT) + val notificationManager = applicationContext.getSystemService(NotificationManager::class.java) + notificationManager.createNotificationChannel(notificationChannel) + } + + private fun startForegroundService() { + createNotificationChannel() + + ServiceCompat.startForeground( + this, + NOTIFICATION_ID, + NotificationCompat.Builder(this, CHANNEL_ID).build(), + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + ServiceInfo.FOREGROUND_SERVICE_TYPE_HEALTH + } else { + 0 + } + ) + } + + override fun onDestroy() { + super.onDestroy() + sensorManager.unregisterListener(this) + } + + override fun onSensorChanged(event: SensorEvent?) { + if (event?.sensor?.type == SENSOR_NAME) { + val steps = event.values[0].toInt() + + if (previousStepCount == 0) { + previousStepCount = steps + } + + if (steps < previousStepCount && previousStepCount > maxStepsThreshold) { + Log.d(LOG, "Overflow detected. Resetting previousStepCount.") + previousStepCount = steps // Reset the previous count to avoid overflow issues + return + } + + val newSteps = steps - previousStepCount + previousStepCount = steps + + + val eventHappened = + System.currentTimeMillis() + ((event.timestamp - SystemClock.elapsedRealtimeNanos()) / 1000000L) + + if (newSteps > 0) { + val step = SensorStep().apply { + startTime = eventHappened + endTime = System.currentTimeMillis() + count = newSteps.toDouble() + } + + box.boxFor(SensorStep::class.java).put(step) + Log.d( + LOG, + "Steps: $newSteps at time: $eventHappened}-${System.currentTimeMillis()}" + ) + } + } + } + + override fun onAccuracyChanged(p0: Sensor?, p1: Int) { + Log.d(LOG, "onAccuracyChanged") + } +} \ No newline at end of file diff --git a/packages/health/android/src/main/res/drawable-hdpi/ic_notification.png b/packages/health/android/src/main/res/drawable-hdpi/ic_notification.png new file mode 100644 index 000000000..9f4f59403 Binary files /dev/null and b/packages/health/android/src/main/res/drawable-hdpi/ic_notification.png differ diff --git a/packages/health/android/src/main/res/drawable-mdpi/ic_notification.png b/packages/health/android/src/main/res/drawable-mdpi/ic_notification.png new file mode 100644 index 000000000..218b5b3df Binary files /dev/null and b/packages/health/android/src/main/res/drawable-mdpi/ic_notification.png differ diff --git a/packages/health/android/src/main/res/drawable-xhdpi/ic_notification.png b/packages/health/android/src/main/res/drawable-xhdpi/ic_notification.png new file mode 100644 index 000000000..11a0f11df Binary files /dev/null and b/packages/health/android/src/main/res/drawable-xhdpi/ic_notification.png differ diff --git a/packages/health/android/src/main/res/drawable-xxhdpi/ic_notification.png b/packages/health/android/src/main/res/drawable-xxhdpi/ic_notification.png new file mode 100644 index 000000000..0107da434 Binary files /dev/null and b/packages/health/android/src/main/res/drawable-xxhdpi/ic_notification.png differ diff --git a/packages/health/android/src/main/res/drawable-xxxhdpi/ic_notification.png b/packages/health/android/src/main/res/drawable-xxxhdpi/ic_notification.png new file mode 100644 index 000000000..3b79d2d90 Binary files /dev/null and b/packages/health/android/src/main/res/drawable-xxxhdpi/ic_notification.png differ diff --git a/packages/health/ios/Classes/SwiftHealthPlugin.swift b/packages/health/ios/Classes/SwiftHealthPlugin.swift index 7d5b3eb64..ac70dcb78 100644 --- a/packages/health/ios/Classes/SwiftHealthPlugin.swift +++ b/packages/health/ios/Classes/SwiftHealthPlugin.swift @@ -663,7 +663,7 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { samplesCategory = samplesCategory.filter { $0.value == 2 } } if dataTypeKey == self.SLEEP_ASLEEP { - samplesCategory = samplesCategory.filter { $0.value == 3 } + samplesCategory = samplesCategory.filter { $0.value == 3 || $0.value == 1 } } if dataTypeKey == self.SLEEP_DEEP { samplesCategory = samplesCategory.filter { $0.value == 4 } diff --git a/packages/health/lib/health.g.dart b/packages/health/lib/health.g.dart index 8b23ccc0e..109507403 100644 --- a/packages/health/lib/health.g.dart +++ b/packages/health/lib/health.g.dart @@ -110,6 +110,7 @@ const _$HealthDataTypeEnumMap = { HealthDataType.ELECTRODERMAL_ACTIVITY: 'ELECTRODERMAL_ACTIVITY', HealthDataType.ELECTROCARDIOGRAM: 'ELECTROCARDIOGRAM', HealthDataType.TOTAL_CALORIES_BURNED: 'TOTAL_CALORIES_BURNED', + HealthDataType.TIME_IN_DAYLIGHT: 'TIME_IN_DAYLIGHT', }; const _$HealthDataUnitEnumMap = { diff --git a/packages/health/lib/src/health_plugin.dart b/packages/health/lib/src/health_plugin.dart index cb755553c..d22c0cd58 100644 --- a/packages/health/lib/src/health_plugin.dart +++ b/packages/health/lib/src/health_plugin.dart @@ -793,6 +793,41 @@ class Health { return []; } + Future doseStepSensorIsAvailable() async { + final fetchedDataPoints = + await _channel.invokeMethod('doseStepSensorIsAvailable'); + + return fetchedDataPoints; + } + + Future startStepSensorBackgroundService() async { + final fetchedDataPoints = + await _channel.invokeMethod('startStepSensorBackgroundService'); + + return fetchedDataPoints; + } + + Future stopStepSensorBackgroundService() async { + final fetchedDataPoints = + await _channel.invokeMethod('stopStepSensorBackgroundService'); + + return fetchedDataPoints; + } + + Future isStepSensorRunning() async { + final fetchedDataPoints = + await _channel.invokeMethod('isStepSensorRunning'); + + return fetchedDataPoints; + } + + Future clearStepSensorData() async { + final clearStepSensorData = + await _channel.invokeMethod('clearStepSensorData'); + + return clearStepSensorData; + } + /// function for fetching statistic health data Future> _dataAggregateQuery( DateTime startDate,