From c582918f0f79f279bebe422d7f7162079b6f1f60 Mon Sep 17 00:00:00 2001 From: Aamir Farooq Date: Tue, 6 Aug 2024 14:18:51 +0200 Subject: [PATCH 01/32] Remove Google Fit and imports from Android code --- .../cachet/plugins/health/HealthPlugin.kt | 2984 +++-------------- 1 file changed, 533 insertions(+), 2451 deletions(-) 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 0455c9c6b..aaa78fae1 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 @@ -3,15 +3,12 @@ package cachet.plugins.health import android.app.Activity import android.content.Context import android.content.Intent -import android.content.pm.PackageManager 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.annotation.NonNull -import androidx.core.content.ContextCompat import androidx.health.connect.client.HealthConnectClient import androidx.health.connect.client.PermissionController import androidx.health.connect.client.permission.HealthPermission @@ -26,27 +23,12 @@ import androidx.health.connect.client.request.AggregateRequest import androidx.health.connect.client.request.ReadRecordsRequest import androidx.health.connect.client.time.TimeRangeFilter import androidx.health.connect.client.units.* -import com.google.android.gms.auth.api.signin.GoogleSignIn -import com.google.android.gms.auth.api.signin.GoogleSignInOptions -import com.google.android.gms.fitness.Fitness -import com.google.android.gms.fitness.FitnessActivities -import com.google.android.gms.fitness.FitnessOptions -import com.google.android.gms.fitness.data.* -import com.google.android.gms.fitness.request.DataDeleteRequest -import com.google.android.gms.fitness.request.DataReadRequest -import com.google.android.gms.fitness.request.SessionInsertRequest -import com.google.android.gms.fitness.request.SessionReadRequest -import com.google.android.gms.fitness.result.DataReadResponse -import com.google.android.gms.fitness.result.SessionReadResponse -import com.google.android.gms.tasks.OnFailureListener -import com.google.android.gms.tasks.OnSuccessListener import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.embedding.engine.plugins.activity.ActivityAware import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel.MethodCallHandler -import io.flutter.plugin.common.MethodChannel.Result import io.flutter.plugin.common.PluginRegistry.ActivityResultListener import io.flutter.plugin.common.PluginRegistry.Registrar import java.time.* @@ -55,13 +37,49 @@ import java.util.* import java.util.concurrent.* import kotlinx.coroutines.* -const val GOOGLE_FIT_PERMISSIONS_REQUEST_CODE = 1111 -const val HEALTH_CONNECT_RESULT_CODE = 16969 const val CHANNEL_NAME = "flutter_health" -const val MMOLL_2_MGDL = 18.0 // 1 mmoll= 18 mgdl -// The minimum android level that can use Health Connect -const val MIN_SUPPORTED_SDK = Build.VERSION_CODES.O_MR1 +const val BODY_FAT_PERCENTAGE = "BODY_FAT_PERCENTAGE" +const val HEIGHT = "HEIGHT" +const val WEIGHT = "WEIGHT" +const val STEPS = "STEPS" +const val AGGREGATE_STEP_COUNT = "AGGREGATE_STEP_COUNT" +const val ACTIVE_ENERGY_BURNED = "ACTIVE_ENERGY_BURNED" +const val HEART_RATE = "HEART_RATE" +const val BODY_TEMPERATURE = "BODY_TEMPERATURE" +const val BODY_WATER_MASS = "BODY_WATER_MASS" +const val BLOOD_PRESSURE_SYSTOLIC = "BLOOD_PRESSURE_SYSTOLIC" +const val BLOOD_PRESSURE_DIASTOLIC = "BLOOD_PRESSURE_DIASTOLIC" +const val BLOOD_OXYGEN = "BLOOD_OXYGEN" +const val BLOOD_GLUCOSE = "BLOOD_GLUCOSE" +const val HEART_RATE_VARIABILITY_RMSSD = "HEART_RATE_VARIABILITY_RMSSD" +const val DISTANCE_DELTA = "DISTANCE_DELTA" +const val WATER = "WATER" +const val RESTING_HEART_RATE = "RESTING_HEART_RATE" +const val BASAL_ENERGY_BURNED = "BASAL_ENERGY_BURNED" +const val FLIGHTS_CLIMBED = "FLIGHTS_CLIMBED" +const val RESPIRATORY_RATE = "RESPIRATORY_RATE" +const val MENSTRUATION_FLOW = "MENSTRUATION_FLOW" + +// TODO support unknown? +const val SLEEP_ASLEEP = "SLEEP_ASLEEP" +const val SLEEP_AWAKE = "SLEEP_AWAKE" +const val SLEEP_IN_BED = "SLEEP_IN_BED" +const val SLEEP_SESSION = "SLEEP_SESSION" +const val SLEEP_LIGHT = "SLEEP_LIGHT" +const val SLEEP_DEEP = "SLEEP_DEEP" +const val SLEEP_REM = "SLEEP_REM" +const val SLEEP_OUT_OF_BED = "SLEEP_OUT_OF_BED" +const val WORKOUT = "WORKOUT" +const val NUTRITION = "NUTRITION" +const val BREAKFAST = "BREAKFAST" +const val LUNCH = "LUNCH" +const val DINNER = "DINNER" +const val SNACK = "SNACK" +const val MEAL_UNKNOWN = "UNKNOWN" + +const val TOTAL_CALORIES_BURNED = "TOTAL_CALORIES_BURNED" + class HealthPlugin(private var channel: MethodChannel? = null) : MethodCallHandler, ActivityResultListener, Result, ActivityAware, FlutterPlugin { @@ -76,403 +94,7 @@ class HealthPlugin(private var channel: MethodChannel? = 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 HEART_RATE_VARIABILITY_RMSSD = "HEART_RATE_VARIABILITY_RMSSD" - 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" - private var MENSTRUATION_FLOW = "MENSTRUATION_FLOW" - - // 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 @@ -515,1990 +137,242 @@ class HealthPlugin(private var channel: MethodChannel? = null) : fun registerWith(registrar: Registrar) { val channel = MethodChannel(registrar.messenger(), CHANNEL_NAME) val plugin = HealthPlugin(channel) - registrar.addActivityResultListener(plugin) - channel.setMethodCallHandler(plugin) - } - } - - 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()) - } - } - - /** 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) - - 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) - } - } - - /** 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 - } - - 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 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) - } - .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("start_time")!!) - val endTime = Instant.ofEpochMilli(call.argument("end_time")!!) - val calories = call.argument("calories") - val protein = call.argument("protein") as Double? - val carbs = call.argument("carbs") as Double? - val fat = call.argument("fat") as Double? - val caffeine = call.argument("caffeine") as Double? - val vitaminA = call.argument("vitamin_a") as Double? - val b1Thiamine = call.argument("b1_thiamine") as Double? - val b2Riboflavin = call.argument("b2_riboflavin") as Double? - val b3Niacin = call.argument("b3_niacin") as Double? - val b5PantothenicAcid = call.argument("b5_pantothenic_acid") as Double? - val b6Pyridoxine = call.argument("b6_pyridoxine") as Double? - val b7Biotin = call.argument("b7_biotin") as Double? - val b9Folate = call.argument("b9_folate") as Double? - val b12Cobalamin = call.argument("b12_cobalamin") as Double? - val vitaminC = call.argument("vitamin_c") as Double? - val vitaminD = call.argument("vitamin_d") as Double? - val vitaminE = call.argument("vitamin_e") as Double? - val vitaminK = call.argument("vitamin_k") as Double? - val calcium = call.argument("calcium") as Double? - val chloride = call.argument("chloride") as Double? - val cholesterol = call.argument("cholesterol") as Double? - // Choline is not yet supported by Health Connect - // val choline = call.argument("choline") as Double? - val chromium = call.argument("chromium") as Double? - val copper = call.argument("copper") as Double? - val fatUnsaturated = call.argument("fat_unsaturated") as Double? - val fatMonounsaturated = call.argument("fat_monounsaturated") as Double? - val fatPolyunsaturated = call.argument("fat_polyunsaturated") as Double? - val fatSaturated = call.argument("fat_saturated") as Double? - val fatTransMonoenoic = call.argument("fat_trans_monoenoic") as Double? - val fiber = call.argument("fiber") as Double? - val iodine = call.argument("iodine") as Double? - val iron = call.argument("iron") as Double? - val magnesium = call.argument("magnesium") as Double? - val manganese = call.argument("manganese") as Double? - val molybdenum = call.argument("molybdenum") as Double? - val phosphorus = call.argument("phosphorus") as Double? - val potassium = call.argument("potassium") as Double? - val selenium = call.argument("selenium") as Double? - val sodium = call.argument("sodium") as Double? - val sugar = call.argument("sugar") as Double? - // Water is not support on a food in Health Connect - // val water = call.argument("water") as Double? - val zinc = call.argument("zinc") as Double? - - val name = call.argument("name") - val mealType = call.argument("meal_type")!! - - 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, - vitaminA = vitaminA?.grams, - thiamin = b1Thiamine?.grams, - riboflavin = b2Riboflavin?.grams, - niacin = b3Niacin?.grams, - pantothenicAcid = b5PantothenicAcid?.grams, - vitaminB6 = b6Pyridoxine?.grams, - biotin = b7Biotin?.grams, - folate = b9Folate?.grams, - vitaminB12 = b12Cobalamin?.grams, - vitaminC = vitaminC?.grams, - vitaminD = vitaminD?.grams, - vitaminE = vitaminE?.grams, - vitaminK = vitaminK?.grams, - calcium = calcium?.grams, - chloride = chloride?.grams, - cholesterol = cholesterol?.grams, - chromium = chromium?.grams, - copper = copper?.grams, - unsaturatedFat = fatUnsaturated?.grams, - monounsaturatedFat = fatMonounsaturated?.grams, - polyunsaturatedFat = fatPolyunsaturated?.grams, - saturatedFat = fatSaturated?.grams, - transFat = fatTransMonoenoic?.grams, - dietaryFiber = fiber?.grams, - iodine = iodine?.grams, - iron = iron?.grams, - magnesium = magnesium?.grams, - manganese = manganese?.grams, - molybdenum = molybdenum?.grams, - phosphorus = phosphorus?.grams, - potassium = potassium?.grams, - selenium = selenium?.grams, - sodium = sodium?.grams, - sugar = sugar?.grams, - zinc = zinc?.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) - } - } - } - - /** 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 - } - - val startTime = call.argument("start_time")!! - val endTime = call.argument("end_time")!! - val calories = call.argument("calories") - val carbs = call.argument("carbs") as Double? - val protein = call.argument("protein") as Double? - val fat = call.argument("fat") as Double? - - - val name = call.argument("name") - val mealType = call.argument("meal_type")!! - - 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() - } - - if (protein != null) { - nutrients[Field.NUTRIENT_PROTEIN] = protein.toFloat() - } - - if (fat != null) { - nutrients[Field.NUTRIENT_TOTAL_FAT] = fat.toFloat() - } - - val dataBuilder = - DataPoint.builder(dataSource) - .setTimeInterval( - startTime, - endTime, - TimeUnit.MILLISECONDS - ) - .setField( - Field.FIELD_NUTRIENTS, - // Remove null values - nutrients.filterValues { it != null }.toMutableMap(), - ) - - if (name != null) { - dataBuilder.setField(Field.FIELD_FOOD_ITEM, name as String) - } - - dataBuilder.setField( - Field.FIELD_MEAL_TYPE, - MapMealTypeToType[mealType] ?: Field.MEAL_TYPE_UNKNOWN - ) - - val dataPoint = dataBuilder.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", - "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 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 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 dataSet = DataSet.builder(dataSource).add(dataPoint).build() - - 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) - } - } - - /** - * Save menstrual flow data - */ - private fun writeMenstruationFlow(call: MethodCall, result: Result) { - if (useHealthConnectIfAvailable && healthConnectAvailable) { - writeHCData(call, result) - return - } - } - - /** - * 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 - } - - 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) - } - } - - /** 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 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() - } - - // 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() - - 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() - - 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) - } - } - - /** 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 - } - - if (context == null) { - result.success(null) - return - } - - 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 - ) - - // 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 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 - ) - - 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!" - ) - ) - } - - private fun getAggregateData(call: MethodCall, result: Result) { - if (context == null) { - result.success(null) - return - } - - 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 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 - ) - - for (type in types) { - val dataType = keyToHealthDataType(type) - readWorkoutsRequest.aggregate(dataType) - } - - 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()) - } - - 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 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) } - } - - 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) } - } - - 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() - } - - private fun hasPermissions(call: MethodCall, result: Result) { - if (useHealthConnectIfAvailable && healthConnectAvailable) { - hasPermissionsHC(call, result) - return - } - if (context == null) { - result.success(false) - return - } - - 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 - - if (useHealthConnectIfAvailable && healthConnectAvailable) { - requestAuthorizationHC(call, result) - return + registrar.addActivityResultListener(plugin) + channel.setMethodCallHandler(plugin) } + } - 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 + override fun success(p0: Any?) { + handler?.post { mResult?.success(p0) } + } - // 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 notImplemented() { + handler?.post { mResult?.notImplemented() } } - /** - * Revokes access to Health Connect using `revokeAllPermissions` and 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. - * Note: When using `revokePermissions` with Health Connect, the app must be completely killed - * for it to take effect. - */ - private fun revokePermissions(call: MethodCall, result: Result) { - if (useHealthConnectIfAvailable && healthConnectAvailable) { - scope.launch { - Log.i("Health", "Disabling Health Connect") - healthConnectClient.permissionController.revokeAllPermissions() - } - result.success(true) - } - 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) - } + override fun error( + errorCode: String, + errorMessage: String?, + errorDetails: Any?, + ) { + handler?.post { mResult?.error(errorCode, errorMessage, errorDetails) } } - private fun getTotalStepsInInterval(call: MethodCall, result: Result) { - val start = call.argument("startTime")!! - val end = call.argument("endTime")!! - val includeManualEntry = call.argument("includeManualEntry")!! + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean { + return false + } - if (useHealthConnectIfAvailable && healthConnectAvailable) { - getStepsHealthConnect(start, end, result) - return + 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)!") } + } - 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() + /** Save a Nutrition measurement with calories, carbs, protein, fat, name and mealType */ + private fun writeMeal(call: MethodCall, result: Result) { + val startTime = Instant.ofEpochMilli(call.argument("start_time")!!) + val endTime = Instant.ofEpochMilli(call.argument("end_time")!!) + val calories = call.argument("calories") + val protein = call.argument("protein") as Double? + val carbs = call.argument("carbs") as Double? + val fat = call.argument("fat") as Double? + val caffeine = call.argument("caffeine") as Double? + val vitaminA = call.argument("vitamin_a") as Double? + val b1Thiamine = call.argument("b1_thiamine") as Double? + val b2Riboflavin = call.argument("b2_riboflavin") as Double? + val b3Niacin = call.argument("b3_niacin") as Double? + val b5PantothenicAcid = call.argument("b5_pantothenic_acid") as Double? + val b6Pyridoxine = call.argument("b6_pyridoxine") as Double? + val b7Biotin = call.argument("b7_biotin") as Double? + val b9Folate = call.argument("b9_folate") as Double? + val b12Cobalamin = call.argument("b12_cobalamin") as Double? + val vitaminC = call.argument("vitamin_c") as Double? + val vitaminD = call.argument("vitamin_d") as Double? + val vitaminE = call.argument("vitamin_e") as Double? + val vitaminK = call.argument("vitamin_k") as Double? + val calcium = call.argument("calcium") as Double? + val chloride = call.argument("chloride") as Double? + val cholesterol = call.argument("cholesterol") as Double? + // Choline is not yet supported by Health Connect + // val choline = call.argument("choline") as Double? + val chromium = call.argument("chromium") as Double? + val copper = call.argument("copper") as Double? + val fatUnsaturated = call.argument("fat_unsaturated") as Double? + val fatMonounsaturated = call.argument("fat_monounsaturated") as Double? + val fatPolyunsaturated = call.argument("fat_polyunsaturated") as Double? + val fatSaturated = call.argument("fat_saturated") as Double? + val fatTransMonoenoic = call.argument("fat_trans_monoenoic") as Double? + val fiber = call.argument("fiber") as Double? + val iodine = call.argument("iodine") as Double? + val iron = call.argument("iron") as Double? + val magnesium = call.argument("magnesium") as Double? + val manganese = call.argument("manganese") as Double? + val molybdenum = call.argument("molybdenum") as Double? + val phosphorus = call.argument("phosphorus") as Double? + val potassium = call.argument("potassium") as Double? + val selenium = call.argument("selenium") as Double? + val sodium = call.argument("sodium") as Double? + val sugar = call.argument("sugar") as Double? + // Water is not support on a food in Health Connect + // val water = call.argument("water") as Double? + val zinc = call.argument("zinc") as Double? - val request = - DataReadRequest.Builder() - .read(ds) - .bucketByTime(duration, TimeUnit.MILLISECONDS) - .setTimeRange(start, end, TimeUnit.MILLISECONDS) - .build() + val name = call.argument("name") + val mealType = call.argument("meal_type")!! - Fitness.getHistoryClient(context, gsa) - .readData(request) - .addOnFailureListener( - errHandler( - result, - "There was an error getting the total steps in the interval!", + 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, + vitaminA = vitaminA?.grams, + thiamin = b1Thiamine?.grams, + riboflavin = b2Riboflavin?.grams, + niacin = b3Niacin?.grams, + pantothenicAcid = b5PantothenicAcid?.grams, + vitaminB6 = b6Pyridoxine?.grams, + biotin = b7Biotin?.grams, + folate = b9Folate?.grams, + vitaminB12 = b12Cobalamin?.grams, + vitaminC = vitaminC?.grams, + vitaminD = vitaminD?.grams, + vitaminE = vitaminE?.grams, + vitaminK = vitaminK?.grams, + calcium = calcium?.grams, + chloride = chloride?.grams, + cholesterol = cholesterol?.grams, + chromium = chromium?.grams, + copper = copper?.grams, + unsaturatedFat = fatUnsaturated?.grams, + monounsaturatedFat = fatMonounsaturated?.grams, + polyunsaturatedFat = fatPolyunsaturated?.grams, + saturatedFat = fatSaturated?.grams, + transFat = fatTransMonoenoic?.grams, + dietaryFiber = fiber?.grams, + iodine = iodine?.grams, + iron = iron?.grams, + magnesium = magnesium?.grams, + manganese = manganese?.grams, + molybdenum = molybdenum?.grams, + phosphorus = phosphorus?.grams, + potassium = potassium?.grams, + selenium = selenium?.grams, + sodium = sodium?.grams, + sugar = sugar?.grams, + zinc = zinc?.grams, + startTime = startTime, + startZoneOffset = null, + endTime = endTime, + endZoneOffset = null, + mealType = + mapMealTypeToType[ + mealType] + ?: MEAL_TYPE_UNKNOWN, ), ) - .addOnSuccessListener( - threadPoolExecutor!!, - getStepsInRange( - start, - end, - includeManualEntry, - result - ), + healthConnectClient.insertRecords( + list, ) - } - - 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.e("FLUTTER_HEALTH::ERROR", "Unable to return steps due to the following exception:") - Log.e("FLUTTER_HEALTH::ERROR", Log.getStackTraceString(e)) - result.success(null) - } - } - - private fun getStepsInRange( - start: Long, - end: Long, - includeManualEntry: Boolean, - result: Result - ) = OnSuccessListener { response: DataReadResponse -> - var totalSteps = 0 // Variable to accumulate the total steps. - - for (bucket in response.buckets) { - for (dataSet in bucket.dataSets) { - var dataPoints = dataSet.dataPoints - if (!includeManualEntry) { - dataPoints = - dataPoints.filterIndexed { _, dataPoint -> - !dataPoint.originalDataSource - .streamName - .contains( - "user_input" - ) - } - } - for (dp in dataPoints) { - val streamName = dp.originalDataSource.streamName - if (!includeManualEntry && streamName.contains("user_input")) { - // Skip this data point if manual entry is not included - Log.i("FLUTTER_HEALTH::SKIPPED", "Skipping manual entry data point with stream name $streamName") - continue - } - - val count = dp.getValue(Field.FIELD_STEPS) - totalSteps += count.asInt() - - val startTime = dp.getStartTime(TimeUnit.MILLISECONDS) - val startDate = Date(startTime) - val endDate = Date(dp.getEndTime(TimeUnit.MILLISECONDS)) - Log.i( - "FLUTTER_HEALTH::INFO", - "adding $count steps for $startDate - $endDate. Total so far: $totalSteps", - ) - } + 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) } } + } - if (totalSteps == 0) { - val startDay = Date(start) - val endDay = Date(end) - Log.i("FLUTTER_HEALTH::ERROR", "no steps for $startDay - $endDay") - } - Log.i("FLUTTER_HEALTH::SUCCESS", "Final total steps in interval: $totalSteps") + /** + * Save menstrual flow data + */ + private fun writeMenstruationFlow(call: MethodCall, result: Result) { + writeData(call, result) + } - Handler(context!!.mainLooper).run { result.success(totalSteps) } + /** + * Save the blood oxygen saturation, without supplemental flow rate + */ + private fun writeBloodOxygen(call: MethodCall, result: Result) { + writeData(call, result) } - /// Disconnect Google fit - private fun disconnect(call: MethodCall, result: Result) { - if (activity == null) { - result.success(false) - return - } - val context = activity!!.applicationContext + private fun getIntervalData(call: MethodCall, result: Result) { + getAggregateData(call, result) + } - 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) + /** + * Revokes access to Health Connect using `revokeAllPermissions`. + * + * Note: When using `revokePermissions` with Health Connect, the app must be completely killed + * for it to take effect. + */ + private fun revokePermissions(call: MethodCall, result: Result) { + scope.launch { + Log.i("Health", "Disabling Health Connect") + healthConnectClient.permissionController.revokeAllPermissions() } + result.success(true) } - private fun getActivityType(type: String): String { - return workoutTypeMap[type] ?: FitnessActivities.UNKNOWN + private fun getTotalStepsInInterval(call: MethodCall, result: Result) { + val start = call.argument("startTime")!! + val end = call.argument("endTime")!! + + 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.e( + "FLUTTER_HEALTH::ERROR", + "Unable to return steps due to the following exception:" + ) + Log.e("FLUTTER_HEALTH::ERROR", Log.getStackTraceString(e)) + result.success(null) + } + } } /** Handle calls from the MethodChannel */ @@ -2513,7 +387,7 @@ class HealthPlugin(private var channel: MethodChannel? = null) : "getData" -> getData(call, result) "getIntervalData" -> getIntervalData(call, result) "writeData" -> writeData(call, result) - "delete" -> delete(call, result) + "delete" -> deleteData(call, result) "getAggregateData" -> getAggregateData(call, result) "getTotalStepsInInterval" -> getTotalStepsInInterval(call, result) "writeWorkoutData" -> writeWorkoutData(call, result) @@ -2521,14 +395,13 @@ class HealthPlugin(private var channel: MethodChannel? = null) : "writeBloodOxygen" -> writeBloodOxygen(call, result) "writeMenstruationFlow" -> writeMenstruationFlow(call, result) "writeMeal" -> writeMeal(call, result) - "disconnect" -> disconnect(call, result) else -> result.notImplemented() } } override fun onAttachedToActivity(binding: ActivityPluginBinding) { if (channel == null) { - return + return } binding.addActivityResultListener(this) activity = binding.activity @@ -2559,10 +432,10 @@ class HealthPlugin(private var channel: MethodChannel? = null) : } /** HEALTH CONNECT BELOW */ - var healthConnectAvailable = false - var healthConnectStatus = HealthConnectClient.SDK_UNAVAILABLE + private var healthConnectAvailable = false + private var healthConnectStatus = HealthConnectClient.SDK_UNAVAILABLE - fun checkAvailability() { + private fun checkAvailability() { healthConnectStatus = HealthConnectClient.getSdkStatus(context!!) healthConnectAvailable = healthConnectStatus == HealthConnectClient.SDK_AVAILABLE } @@ -2582,7 +455,7 @@ class HealthPlugin(private var channel: MethodChannel? = null) : result.success(null) } - fun useHealthConnectIfAvailable(call: MethodCall, result: Result) { + private fun useHealthConnectIfAvailable(call: MethodCall, result: Result) { useHealthConnectIfAvailable = true result.success(null) } @@ -2598,23 +471,23 @@ class HealthPlugin(private var channel: MethodChannel? = null) : result.success(healthConnectStatus) } - private fun hasPermissionsHC(call: MethodCall, result: Result) { + private fun hasPermissions(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() + val permList = mutableListOf() for ((i, typeKey) in types.withIndex()) { - if (!MapToHCType.containsKey(typeKey)) { + if (!mapToType.containsKey(typeKey)) { Log.w( "FLUTTER_HEALTH::ERROR", - "Datatype " + typeKey + " not found in HC" + "Datatype $typeKey not found in HC" ) result.success(false) return } val access = permissions[i] - val dataType = MapToHCType[typeKey]!! + val dataType = mapToType[typeKey]!! if (access == 0) { permList.add( HealthPermission.getReadPermission(dataType), @@ -2674,23 +547,27 @@ class HealthPlugin(private var channel: MethodChannel? = null) : } } - private fun requestAuthorizationHC(call: MethodCall, result: Result) { + /** + * Requests authorization for the HealthDataTypes with the the READ or READ_WRITE permission + * type. + */ + private fun requestAuthorization(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() + val permList = mutableListOf() for ((i, typeKey) in types.withIndex()) { - if (!MapToHCType.containsKey(typeKey)) { + if (!mapToType.containsKey(typeKey)) { Log.w( "FLUTTER_HEALTH::ERROR", - "Datatype " + typeKey + " not found in HC" + "Datatype $typeKey not found in HC" ) result.success(false) return } val access = permissions[i]!! - val dataType = MapToHCType[typeKey]!! + val dataType = mapToType[typeKey]!! if (access == 0) { permList.add( HealthPermission.getReadPermission(dataType), @@ -2749,14 +626,15 @@ class HealthPlugin(private var channel: MethodChannel? = null) : healthConnectRequestPermissionsLauncher!!.launch(permList.toSet()) } - fun getHCData(call: MethodCall, result: Result) { + /** Get all datapoints of the DataType within the given time range */ + private fun getData(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 { try { - MapToHCType[dataType]?.let { classType -> + mapToType[dataType]?.let { classType -> val records = mutableListOf() // Set up the initial request to read health records with specified @@ -2865,7 +743,7 @@ class HealthPlugin(private var channel: MethodChannel? = null) : // mapOf( mapOf( "workoutActivityType" to - (workoutTypeMapHealthConnect + (workoutTypeMap .filterValues { it == record.exerciseType @@ -2928,7 +806,7 @@ class HealthPlugin(private var channel: MethodChannel? = null) : } else { for (recStage in rec.stages) { if (dataType == - MapSleepStageToType[ + mapSleepStageToType[ recStage.stage] ) { healthConnectData @@ -2962,7 +840,7 @@ class HealthPlugin(private var channel: MethodChannel? = null) : } } - fun convertRecordStage( + private fun convertRecordStage( stage: SleepSessionRecord.Stage, dataType: String, sourceName: String @@ -2983,7 +861,7 @@ class HealthPlugin(private var channel: MethodChannel? = null) : ) } - fun getAggregateHCData(call: MethodCall, result: Result) { + private fun getAggregateData(call: MethodCall, result: Result) { val dataType = call.argument("dataTypeKey")!! val interval = call.argument("interval")!! val startTime = Instant.ofEpochMilli(call.argument("startTime")!!) @@ -2991,7 +869,7 @@ class HealthPlugin(private var channel: MethodChannel? = null) : val healthConnectData = mutableListOf>() scope.launch { try { - MapToHCAggregateMetric[dataType]?.let { metricClassType -> + mapToAggregateMetric[dataType]?.let { metricClassType -> val request = AggregateGroupByDurationRequest( metrics = setOf(metricClassType), @@ -3020,7 +898,7 @@ class HealthPlugin(private var channel: MethodChannel? = null) : val packageNames = durationResult.result.dataOrigins .joinToString { origin -> - "${origin.packageName}" + origin.packageName } val data = @@ -3055,7 +933,7 @@ class HealthPlugin(private var channel: MethodChannel? = null) : } // TODO: Find alternative to SOURCE_ID or make it nullable? - fun convertRecord(record: Any, dataType: String): List> { + private fun convertRecord(record: Any, dataType: String): List> { val metadata = (record as Record).metadata when (record) { is WeightRecord -> @@ -3382,24 +1260,6 @@ class HealthPlugin(private var channel: MethodChannel? = null) : .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( @@ -3479,7 +1339,7 @@ class HealthPlugin(private var channel: MethodChannel? = null) : "zinc" to record.zinc?.inGrams, "name" to record.name!!, "meal_type" to - (MapTypeToMealTypeHC[ + (mapTypeToMealType[ record.mealType] ?: MEAL_TYPE_UNKNOWN), "date_from" to @@ -3522,7 +1382,7 @@ class HealthPlugin(private var channel: MethodChannel? = null) : // 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) { + private fun writeData(call: MethodCall, result: Result) { val type = call.argument("dataTypeKey")!! val startTime = call.argument("startTime")!! val endTime = call.argument("endTime")!! @@ -3607,8 +1467,7 @@ class HealthPlugin(private var channel: MethodChannel? = null) : endTime ), samples = - listOf< - HeartRateRecord.Sample>( + listOf( HeartRateRecord.Sample( time = Instant.ofEpochMilli( @@ -3677,7 +1536,7 @@ class HealthPlugin(private var channel: MethodChannel? = null) : ), heartRateVariabilityMillis = value, - + zoneOffset = null, ) DISTANCE_DELTA -> @@ -3988,13 +1847,14 @@ class HealthPlugin(private var channel: MethodChannel? = null) : } } - fun writeWorkoutHCData(call: MethodCall, result: Result) { + /** Save a Workout session with options for distance and calories expended */ + private fun writeWorkoutData(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) { + if (!workoutTypeMap.containsKey(type)) { result.success(false) Log.w( "FLUTTER_HEALTH::ERROR", @@ -4002,7 +1862,7 @@ class HealthPlugin(private var channel: MethodChannel? = null) : ) return } - val workoutType = workoutTypeMapHealthConnect[type]!! + val workoutType = workoutTypeMap[type]!! val title = call.argument("title") ?: type scope.launch { @@ -4067,11 +1927,11 @@ class HealthPlugin(private var channel: MethodChannel? = null) : } } - fun writeBloodPressureHC(call: MethodCall, result: Result) { + /** Save a Blood Pressure measurement with systolic and diastolic values */ + private fun writeBloodPressure(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 { @@ -4108,16 +1968,17 @@ class HealthPlugin(private var channel: MethodChannel? = null) : } } - fun deleteHCData(call: MethodCall, result: Result) { + /** Delete records of the given type in the time range */ + private fun deleteData(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") + if (!mapToType.containsKey(type)) { + Log.w("FLUTTER_HEALTH::ERROR", "Datatype $type not found in HC") result.success(false) return } - val classType = MapToHCType[type]!! + val classType = mapToType[type]!! scope.launch { try { @@ -4136,8 +1997,8 @@ class HealthPlugin(private var channel: MethodChannel? = null) : } } - val MapSleepStageToType = - hashMapOf( + private val mapSleepStageToType = + hashMapOf( 1 to SLEEP_AWAKE, 2 to SLEEP_ASLEEP, 3 to SLEEP_OUT_OF_BED, @@ -4146,8 +2007,8 @@ class HealthPlugin(private var channel: MethodChannel? = null) : 6 to SLEEP_REM, ) - private val MapMealTypeToTypeHC = - hashMapOf( + private val mapMealTypeToType = + hashMapOf( BREAKFAST to MEAL_TYPE_BREAKFAST, LUNCH to MEAL_TYPE_LUNCH, DINNER to MEAL_TYPE_DINNER, @@ -4155,8 +2016,8 @@ class HealthPlugin(private var channel: MethodChannel? = null) : MEAL_UNKNOWN to MEAL_TYPE_UNKNOWN, ) - private val MapTypeToMealTypeHC = - hashMapOf( + private val mapTypeToMealType = + hashMapOf( MEAL_TYPE_BREAKFAST to BREAKFAST, MEAL_TYPE_LUNCH to LUNCH, MEAL_TYPE_DINNER to DINNER, @@ -4164,16 +2025,8 @@ class HealthPlugin(private var channel: MethodChannel? = null) : 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 = + private val mapToType = hashMapOf( BODY_FAT_PERCENTAGE to BodyFatRecord::class, HEIGHT to HeightRecord::class, @@ -4249,7 +2102,7 @@ class HealthPlugin(private var channel: MethodChannel? = null) : // "WheelchairPushes" to WheelchairPushesRecord::class, ) - val MapToHCAggregateMetric = + private val mapToAggregateMetric = hashMapOf( HEIGHT to HeightRecord.HEIGHT_AVG, WEIGHT to WeightRecord.WEIGHT_AVG, @@ -4267,4 +2120,233 @@ class HealthPlugin(private var channel: MethodChannel? = null) : TOTAL_CALORIES_BURNED to TotalCaloriesBurnedRecord.ENERGY_TOTAL ) + + // TODO: Update with new workout types when Health Connect becomes the standard. + private val workoutTypeMap = + 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, + ) } From 2590ffe119e5fc4fa8def6a43699e5e6e0c9ccd8 Mon Sep 17 00:00:00 2001 From: Aamir Farooq Date: Tue, 6 Aug 2024 14:29:52 +0200 Subject: [PATCH 02/32] Formatting --- .../cachet/plugins/health/HealthPlugin.kt | 4023 +++++++++-------- 1 file changed, 2039 insertions(+), 1984 deletions(-) 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 aaa78fae1..58608ad39 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 @@ -39,2087 +39,2142 @@ import kotlinx.coroutines.* const val CHANNEL_NAME = "flutter_health" -const val BODY_FAT_PERCENTAGE = "BODY_FAT_PERCENTAGE" -const val HEIGHT = "HEIGHT" -const val WEIGHT = "WEIGHT" -const val STEPS = "STEPS" -const val AGGREGATE_STEP_COUNT = "AGGREGATE_STEP_COUNT" const val ACTIVE_ENERGY_BURNED = "ACTIVE_ENERGY_BURNED" -const val HEART_RATE = "HEART_RATE" +const val AGGREGATE_STEP_COUNT = "AGGREGATE_STEP_COUNT" +const val BASAL_ENERGY_BURNED = "BASAL_ENERGY_BURNED" +const val BLOOD_GLUCOSE = "BLOOD_GLUCOSE" +const val BLOOD_OXYGEN = "BLOOD_OXYGEN" +const val BLOOD_PRESSURE_DIASTOLIC = "BLOOD_PRESSURE_DIASTOLIC" +const val BLOOD_PRESSURE_SYSTOLIC = "BLOOD_PRESSURE_SYSTOLIC" +const val BODY_FAT_PERCENTAGE = "BODY_FAT_PERCENTAGE" const val BODY_TEMPERATURE = "BODY_TEMPERATURE" const val BODY_WATER_MASS = "BODY_WATER_MASS" -const val BLOOD_PRESSURE_SYSTOLIC = "BLOOD_PRESSURE_SYSTOLIC" -const val BLOOD_PRESSURE_DIASTOLIC = "BLOOD_PRESSURE_DIASTOLIC" -const val BLOOD_OXYGEN = "BLOOD_OXYGEN" -const val BLOOD_GLUCOSE = "BLOOD_GLUCOSE" -const val HEART_RATE_VARIABILITY_RMSSD = "HEART_RATE_VARIABILITY_RMSSD" const val DISTANCE_DELTA = "DISTANCE_DELTA" -const val WATER = "WATER" -const val RESTING_HEART_RATE = "RESTING_HEART_RATE" -const val BASAL_ENERGY_BURNED = "BASAL_ENERGY_BURNED" const val FLIGHTS_CLIMBED = "FLIGHTS_CLIMBED" -const val RESPIRATORY_RATE = "RESPIRATORY_RATE" +const val HEART_RATE = "HEART_RATE" +const val HEART_RATE_VARIABILITY_RMSSD = "HEART_RATE_VARIABILITY_RMSSD" +const val HEIGHT = "HEIGHT" const val MENSTRUATION_FLOW = "MENSTRUATION_FLOW" +const val RESPIRATORY_RATE = "RESPIRATORY_RATE" +const val RESTING_HEART_RATE = "RESTING_HEART_RATE" +const val STEPS = "STEPS" +const val WATER = "WATER" +const val WEIGHT = "WEIGHT" // TODO support unknown? +const val BREAKFAST = "BREAKFAST" +const val DINNER = "DINNER" +const val LUNCH = "LUNCH" +const val MEAL_UNKNOWN = "UNKNOWN" +const val NUTRITION = "NUTRITION" const val SLEEP_ASLEEP = "SLEEP_ASLEEP" const val SLEEP_AWAKE = "SLEEP_AWAKE" +const val SLEEP_DEEP = "SLEEP_DEEP" const val SLEEP_IN_BED = "SLEEP_IN_BED" -const val SLEEP_SESSION = "SLEEP_SESSION" const val SLEEP_LIGHT = "SLEEP_LIGHT" -const val SLEEP_DEEP = "SLEEP_DEEP" -const val SLEEP_REM = "SLEEP_REM" const val SLEEP_OUT_OF_BED = "SLEEP_OUT_OF_BED" -const val WORKOUT = "WORKOUT" -const val NUTRITION = "NUTRITION" -const val BREAKFAST = "BREAKFAST" -const val LUNCH = "LUNCH" -const val DINNER = "DINNER" +const val SLEEP_REM = "SLEEP_REM" +const val SLEEP_SESSION = "SLEEP_SESSION" const val SNACK = "SNACK" -const val MEAL_UNKNOWN = "UNKNOWN" +const val WORKOUT = "WORKOUT" const val TOTAL_CALORIES_BURNED = "TOTAL_CALORIES_BURNED" 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 - - - - 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 - ) - } + 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 + + + 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 + } + + 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) } - - // 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) - } + } + + 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 { + 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)!") } + } - override fun success(p0: Any?) { - handler?.post { mResult?.success(p0) } + /** Save a Nutrition measurement with calories, carbs, protein, fat, name and mealType */ + private fun writeMeal(call: MethodCall, result: Result) { + val startTime = Instant.ofEpochMilli(call.argument("start_time")!!) + val endTime = Instant.ofEpochMilli(call.argument("end_time")!!) + val calories = call.argument("calories") + val protein = call.argument("protein") as Double? + val carbs = call.argument("carbs") as Double? + val fat = call.argument("fat") as Double? + val caffeine = call.argument("caffeine") as Double? + val vitaminA = call.argument("vitamin_a") as Double? + val b1Thiamine = call.argument("b1_thiamine") as Double? + val b2Riboflavin = call.argument("b2_riboflavin") as Double? + val b3Niacin = call.argument("b3_niacin") as Double? + val b5PantothenicAcid = call.argument("b5_pantothenic_acid") as Double? + val b6Pyridoxine = call.argument("b6_pyridoxine") as Double? + val b7Biotin = call.argument("b7_biotin") as Double? + val b9Folate = call.argument("b9_folate") as Double? + val b12Cobalamin = call.argument("b12_cobalamin") as Double? + val vitaminC = call.argument("vitamin_c") as Double? + val vitaminD = call.argument("vitamin_d") as Double? + val vitaminE = call.argument("vitamin_e") as Double? + val vitaminK = call.argument("vitamin_k") as Double? + val calcium = call.argument("calcium") as Double? + val chloride = call.argument("chloride") as Double? + val cholesterol = call.argument("cholesterol") as Double? + // Choline is not yet supported by Health Connect + // val choline = call.argument("choline") as Double? + val chromium = call.argument("chromium") as Double? + val copper = call.argument("copper") as Double? + val fatUnsaturated = call.argument("fat_unsaturated") as Double? + val fatMonounsaturated = call.argument("fat_monounsaturated") as Double? + val fatPolyunsaturated = call.argument("fat_polyunsaturated") as Double? + val fatSaturated = call.argument("fat_saturated") as Double? + val fatTransMonoenoic = call.argument("fat_trans_monoenoic") as Double? + val fiber = call.argument("fiber") as Double? + val iodine = call.argument("iodine") as Double? + val iron = call.argument("iron") as Double? + val magnesium = call.argument("magnesium") as Double? + val manganese = call.argument("manganese") as Double? + val molybdenum = call.argument("molybdenum") as Double? + val phosphorus = call.argument("phosphorus") as Double? + val potassium = call.argument("potassium") as Double? + val selenium = call.argument("selenium") as Double? + val sodium = call.argument("sodium") as Double? + val sugar = call.argument("sugar") as Double? + // Water is not support on a food in Health Connect + // val water = call.argument("water") as Double? + val zinc = call.argument("zinc") as Double? + + val name = call.argument("name") + val mealType = call.argument("meal_type")!! + + 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, + vitaminA = vitaminA?.grams, + thiamin = b1Thiamine?.grams, + riboflavin = b2Riboflavin?.grams, + niacin = b3Niacin?.grams, + pantothenicAcid = b5PantothenicAcid?.grams, + vitaminB6 = b6Pyridoxine?.grams, + biotin = b7Biotin?.grams, + folate = b9Folate?.grams, + vitaminB12 = b12Cobalamin?.grams, + vitaminC = vitaminC?.grams, + vitaminD = vitaminD?.grams, + vitaminE = vitaminE?.grams, + vitaminK = vitaminK?.grams, + calcium = calcium?.grams, + chloride = chloride?.grams, + cholesterol = cholesterol?.grams, + chromium = chromium?.grams, + copper = copper?.grams, + unsaturatedFat = fatUnsaturated?.grams, + monounsaturatedFat = fatMonounsaturated?.grams, + polyunsaturatedFat = fatPolyunsaturated?.grams, + saturatedFat = fatSaturated?.grams, + transFat = fatTransMonoenoic?.grams, + dietaryFiber = fiber?.grams, + iodine = iodine?.grams, + iron = iron?.grams, + magnesium = magnesium?.grams, + manganese = manganese?.grams, + molybdenum = molybdenum?.grams, + phosphorus = phosphorus?.grams, + potassium = potassium?.grams, + selenium = selenium?.grams, + sodium = sodium?.grams, + sugar = sugar?.grams, + zinc = zinc?.grams, + startTime = startTime, + startZoneOffset = null, + endTime = endTime, + endZoneOffset = null, + mealType = + mapMealTypeToType[ + 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) + } } + } - override fun notImplemented() { - handler?.post { mResult?.notImplemented() } - } - override fun error( - errorCode: String, - errorMessage: String?, - errorDetails: Any?, - ) { - handler?.post { mResult?.error(errorCode, errorMessage, errorDetails) } - } + /** + * Save menstrual flow data + */ + private fun writeMenstruationFlow(call: MethodCall, result: Result) { + writeData(call, result) + } - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean { - return false - } + /** + * Save the blood oxygen saturation, without supplemental flow rate + */ + private fun writeBloodOxygen(call: MethodCall, result: Result) { + writeData(call, result) + } - 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 getIntervalData(call: MethodCall, result: Result) { + getAggregateData(call, result) + } + + /** + * Revokes access to Health Connect using `revokeAllPermissions`. + * + * Note: When using `revokePermissions` with Health Connect, the app must be completely killed + * for it to take effect. + */ + private fun revokePermissions(call: MethodCall, result: Result) { + scope.launch { + Log.i("Health", "Disabling Health Connect") + healthConnectClient.permissionController.revokeAllPermissions() } + result.success(true) + } + + private fun getTotalStepsInInterval(call: MethodCall, result: Result) { + val start = call.argument("startTime")!! + val end = call.argument("endTime")!! + + 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.e( + "FLUTTER_HEALTH::ERROR", + "Unable to return steps due to the following exception:" + ) + Log.e("FLUTTER_HEALTH::ERROR", Log.getStackTraceString(e)) + result.success(null) + } + } + } + + /** 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" -> deleteData(call, result) + "getAggregateData" -> getAggregateData(call, result) + "getTotalStepsInInterval" -> getTotalStepsInInterval(call, result) + "writeWorkoutData" -> writeWorkoutData(call, result) + "writeBloodPressure" -> writeBloodPressure(call, result) + "writeBloodOxygen" -> writeBloodOxygen(call, result) + "writeMenstruationFlow" -> writeMenstruationFlow(call, result) + "writeMeal" -> writeMeal(call, result) + else -> result.notImplemented() + } + } - /** Save a Nutrition measurement with calories, carbs, protein, fat, name and mealType */ - private fun writeMeal(call: MethodCall, result: Result) { - val startTime = Instant.ofEpochMilli(call.argument("start_time")!!) - val endTime = Instant.ofEpochMilli(call.argument("end_time")!!) - val calories = call.argument("calories") - val protein = call.argument("protein") as Double? - val carbs = call.argument("carbs") as Double? - val fat = call.argument("fat") as Double? - val caffeine = call.argument("caffeine") as Double? - val vitaminA = call.argument("vitamin_a") as Double? - val b1Thiamine = call.argument("b1_thiamine") as Double? - val b2Riboflavin = call.argument("b2_riboflavin") as Double? - val b3Niacin = call.argument("b3_niacin") as Double? - val b5PantothenicAcid = call.argument("b5_pantothenic_acid") as Double? - val b6Pyridoxine = call.argument("b6_pyridoxine") as Double? - val b7Biotin = call.argument("b7_biotin") as Double? - val b9Folate = call.argument("b9_folate") as Double? - val b12Cobalamin = call.argument("b12_cobalamin") as Double? - val vitaminC = call.argument("vitamin_c") as Double? - val vitaminD = call.argument("vitamin_d") as Double? - val vitaminE = call.argument("vitamin_e") as Double? - val vitaminK = call.argument("vitamin_k") as Double? - val calcium = call.argument("calcium") as Double? - val chloride = call.argument("chloride") as Double? - val cholesterol = call.argument("cholesterol") as Double? - // Choline is not yet supported by Health Connect - // val choline = call.argument("choline") as Double? - val chromium = call.argument("chromium") as Double? - val copper = call.argument("copper") as Double? - val fatUnsaturated = call.argument("fat_unsaturated") as Double? - val fatMonounsaturated = call.argument("fat_monounsaturated") as Double? - val fatPolyunsaturated = call.argument("fat_polyunsaturated") as Double? - val fatSaturated = call.argument("fat_saturated") as Double? - val fatTransMonoenoic = call.argument("fat_trans_monoenoic") as Double? - val fiber = call.argument("fiber") as Double? - val iodine = call.argument("iodine") as Double? - val iron = call.argument("iron") as Double? - val magnesium = call.argument("magnesium") as Double? - val manganese = call.argument("manganese") as Double? - val molybdenum = call.argument("molybdenum") as Double? - val phosphorus = call.argument("phosphorus") as Double? - val potassium = call.argument("potassium") as Double? - val selenium = call.argument("selenium") as Double? - val sodium = call.argument("sodium") as Double? - val sugar = call.argument("sugar") as Double? - // Water is not support on a food in Health Connect - // val water = call.argument("water") as Double? - val zinc = call.argument("zinc") as Double? - - val name = call.argument("name") - val mealType = call.argument("meal_type")!! - - 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, - vitaminA = vitaminA?.grams, - thiamin = b1Thiamine?.grams, - riboflavin = b2Riboflavin?.grams, - niacin = b3Niacin?.grams, - pantothenicAcid = b5PantothenicAcid?.grams, - vitaminB6 = b6Pyridoxine?.grams, - biotin = b7Biotin?.grams, - folate = b9Folate?.grams, - vitaminB12 = b12Cobalamin?.grams, - vitaminC = vitaminC?.grams, - vitaminD = vitaminD?.grams, - vitaminE = vitaminE?.grams, - vitaminK = vitaminK?.grams, - calcium = calcium?.grams, - chloride = chloride?.grams, - cholesterol = cholesterol?.grams, - chromium = chromium?.grams, - copper = copper?.grams, - unsaturatedFat = fatUnsaturated?.grams, - monounsaturatedFat = fatMonounsaturated?.grams, - polyunsaturatedFat = fatPolyunsaturated?.grams, - saturatedFat = fatSaturated?.grams, - transFat = fatTransMonoenoic?.grams, - dietaryFiber = fiber?.grams, - iodine = iodine?.grams, - iron = iron?.grams, - magnesium = magnesium?.grams, - manganese = manganese?.grams, - molybdenum = molybdenum?.grams, - phosphorus = phosphorus?.grams, - potassium = potassium?.grams, - selenium = selenium?.grams, - sodium = sodium?.grams, - sugar = sugar?.grams, - zinc = zinc?.grams, - startTime = startTime, - startZoneOffset = null, - endTime = endTime, - endZoneOffset = null, - mealType = - mapMealTypeToType[ - 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) - } - } + override fun onAttachedToActivity(binding: ActivityPluginBinding) { + if (channel == null) { + return } + binding.addActivityResultListener(this) + activity = binding.activity + val requestPermissionActivityContract = + PermissionController.createRequestPermissionResultContract() - /** - * Save menstrual flow data - */ - private fun writeMenstruationFlow(call: MethodCall, result: Result) { - writeData(call, result) - } + healthConnectRequestPermissionsLauncher = + (activity as ComponentActivity).registerForActivityResult( + requestPermissionActivityContract + ) { granted -> onHealthConnectPermissionCallback(granted) } + } - /** - * Save the blood oxygen saturation, without supplemental flow rate - */ - private fun writeBloodOxygen(call: MethodCall, result: Result) { - writeData(call, result) - } + override fun onDetachedFromActivityForConfigChanges() { + onDetachedFromActivity() + } - private fun getIntervalData(call: MethodCall, result: Result) { - getAggregateData(call, result) - } + override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) { + onAttachedToActivity(binding) + } - /** - * Revokes access to Health Connect using `revokeAllPermissions`. - * - * Note: When using `revokePermissions` with Health Connect, the app must be completely killed - * for it to take effect. - */ - private fun revokePermissions(call: MethodCall, result: Result) { - scope.launch { - Log.i("Health", "Disabling Health Connect") - healthConnectClient.permissionController.revokeAllPermissions() - } - result.success(true) + override fun onDetachedFromActivity() { + if (channel == null) { + return } - - private fun getTotalStepsInInterval(call: MethodCall, result: Result) { - val start = call.argument("startTime")!! - val end = call.argument("endTime")!! - - 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 - ), + activity = null + healthConnectRequestPermissionsLauncher = null + } + + /** HEALTH CONNECT BELOW */ + private var healthConnectAvailable = false + private var healthConnectStatus = HealthConnectClient.SDK_UNAVAILABLE + + private 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) + } + + private 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 hasPermissions(call: MethodCall, result: Result) { + val args = call.arguments as HashMap<*, *> + val types = (args["types"] as? ArrayList<*>)?.filterIsInstance()!! + val permissions = (args["permissions"] as? ArrayList<*>)?.filterIsInstance()!! + + val permList = mutableListOf() + for ((i, typeKey) in types.withIndex()) { + if (!mapToType.containsKey(typeKey)) { + Log.w( + "FLUTTER_HEALTH::ERROR", + "Datatype $typeKey not found in HC" + ) + result.success(false) + return + } + val access = permissions[i] + val dataType = mapToType[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 ), - ) - // 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" + HealthPermission.getReadPermission( + TotalCaloriesBurnedRecord::class + ), + ), ) - result.success(stepsInInterval) - } catch (e: Exception) { - Log.e( - "FLUTTER_HEALTH::ERROR", - "Unable to return steps due to the following exception:" + } else { + permList.addAll( + listOf( + HealthPermission.getReadPermission( + DistanceRecord::class + ), + HealthPermission.getReadPermission( + TotalCaloriesBurnedRecord::class + ), + HealthPermission.getWritePermission( + DistanceRecord::class + ), + HealthPermission.getWritePermission( + TotalCaloriesBurnedRecord::class + ), + ), ) - Log.e("FLUTTER_HEALTH::ERROR", Log.getStackTraceString(e)) - result.success(null) } } } - - /** 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" -> deleteData(call, result) - "getAggregateData" -> getAggregateData(call, result) - "getTotalStepsInInterval" -> getTotalStepsInInterval(call, result) - "writeWorkoutData" -> writeWorkoutData(call, result) - "writeBloodPressure" -> writeBloodPressure(call, result) - "writeBloodOxygen" -> writeBloodOxygen(call, result) - "writeMenstruationFlow" -> writeMenstruationFlow(call, result) - "writeMeal" -> writeMeal(call, result) - else -> result.notImplemented() - } + scope.launch { + result.success( + healthConnectClient + .permissionController + .getGrantedPermissions() + .containsAll(permList), + ) } + } - override fun onAttachedToActivity(binding: ActivityPluginBinding) { - if (channel == null) { - return + /** + * Requests authorization for the HealthDataTypes with the the READ or READ_WRITE permission + * type. + */ + private fun requestAuthorization(call: MethodCall, result: Result) { + val args = call.arguments as HashMap<*, *> + val types = (args["types"] as? ArrayList<*>)?.filterIsInstance()!! + val permissions = (args["permissions"] as? ArrayList<*>)?.filterIsInstance()!! + + val permList = mutableListOf() + for ((i, typeKey) in types.withIndex()) { + if (!mapToType.containsKey(typeKey)) { + Log.w( + "FLUTTER_HEALTH::ERROR", + "Datatype $typeKey not found in HC" + ) + result.success(false) + return + } + val access = permissions[i]!! + val dataType = mapToType[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 + ), + ), + ) } - binding.addActivityResultListener(this) - activity = binding.activity - - val requestPermissionActivityContract = - PermissionController.createRequestPermissionResultContract() - - healthConnectRequestPermissionsLauncher = - (activity as ComponentActivity).registerForActivityResult( - requestPermissionActivityContract - ) { granted -> onHealthConnectPermissionCallback(granted) } + } } - - override fun onDetachedFromActivityForConfigChanges() { - onDetachedFromActivity() + if (healthConnectRequestPermissionsLauncher == null) { + result.success(false) + Log.i("FLUTTER_HEALTH", "Permission launcher not found") + return } - override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) { - onAttachedToActivity(binding) - } + healthConnectRequestPermissionsLauncher!!.launch(permList.toSet()) + } + + /** Get all datapoints of the DataType within the given time range */ + private fun getData(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 { + try { + mapToType[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 + ), + ) - override fun onDetachedFromActivity() { - if (channel == null) { - return - } - activity = null - healthConnectRequestPermissionsLauncher = null - } + var response = healthConnectClient.readRecords(request) + var pageToken = response.pageToken - /** HEALTH CONNECT BELOW */ - private var healthConnectAvailable = false - private var healthConnectStatus = HealthConnectClient.SDK_UNAVAILABLE + // Add the records from the initial response to the records list + records.addAll(response.records) - private fun checkAvailability() { - healthConnectStatus = HealthConnectClient.getSdkStatus(context!!) - healthConnectAvailable = healthConnectStatus == HealthConnectClient.SDK_AVAILABLE - } + // 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) - 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) + pageToken = response.pageToken + records.addAll(response.records) } - ) - result.success(null) - } - - private 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 hasPermissions(call: MethodCall, result: Result) { - val args = call.arguments as HashMap<*, *> - val types = (args["types"] as? ArrayList<*>)?.filterIsInstance()!! - val permissions = (args["permissions"] as? ArrayList<*>)?.filterIsInstance()!! - - val permList = mutableListOf() - for ((i, typeKey) in types.withIndex()) { - if (!mapToType.containsKey(typeKey)) { - Log.w( - "FLUTTER_HEALTH::ERROR", - "Datatype $typeKey not found in HC" + // 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, + ), + ), ) - result.success(false) - return - } - val access = permissions[i] - val dataType = mapToType[typeKey]!! - if (access == 0) { - permList.add( - HealthPermission.getReadPermission(dataType), + 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, + ), + ), ) - } else { - permList.addAll( - listOf( - HealthPermission.getReadPermission( - dataType - ), - HealthPermission.getWritePermission( - dataType - ), - ), + 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 + (workoutTypeMap + .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, + ), + ) } - // 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 - ), - ), + // 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 { - permList.addAll( - listOf( - HealthPermission.getReadPermission( - DistanceRecord::class - ), - HealthPermission.getReadPermission( - TotalCaloriesBurnedRecord::class - ), - HealthPermission.getWritePermission( - DistanceRecord::class - ), - HealthPermission.getWritePermission( - TotalCaloriesBurnedRecord::class - ), - ), - ) + 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) + ) + } + } } - scope.launch { - result.success( - healthConnectClient - .permissionController - .getGrantedPermissions() - .containsAll(permList), - ) - } + Handler(context!!.mainLooper).run { result.success(healthConnectData) } + } catch (e: Exception) { + Log.i( + "FLUTTER_HEALTH::ERROR", + "Unable to return $dataType due to the following exception:" + ) + Log.e("FLUTTER_HEALTH::ERROR", Log.getStackTraceString(e)) + result.success(null) + } } - - /** - * Requests authorization for the HealthDataTypes with the the READ or READ_WRITE permission - * type. - */ - private fun requestAuthorization(call: MethodCall, result: Result) { - val args = call.arguments as HashMap<*, *> - val types = (args["types"] as? ArrayList<*>)?.filterIsInstance()!! - val permissions = (args["permissions"] as? ArrayList<*>)?.filterIsInstance()!! - - val permList = mutableListOf() - for ((i, typeKey) in types.withIndex()) { - if (!mapToType.containsKey(typeKey)) { - Log.w( - "FLUTTER_HEALTH::ERROR", - "Datatype $typeKey not found in HC" - ) - result.success(false) - return - } - val access = permissions[i]!! - val dataType = mapToType[typeKey]!! - if (access == 0) { - permList.add( - HealthPermission.getReadPermission(dataType), - ) - } else { - permList.addAll( - listOf( - HealthPermission.getReadPermission( - dataType - ), - HealthPermission.getWritePermission( - dataType - ), - ), - ) + } + + private 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, + ), + ) + } + + private fun getAggregateData(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 { + try { + mapToAggregateMetric[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 } - // 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 - ), - ), - ) + + val packageNames = + durationResult.result.dataOrigins + .joinToString { origin -> + origin.packageName } - } - } - if (healthConnectRequestPermissionsLauncher == null) { - result.success(false) - Log.i("FLUTTER_HEALTH", "Permission launcher not found") - return - } - healthConnectRequestPermissionsLauncher!!.launch(permList.toSet()) + 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) } + } catch (e: Exception) { + Log.i( + "FLUTTER_HEALTH::ERROR", + "Unable to return $dataType due to the following exception:" + ) + Log.e("FLUTTER_HEALTH::ERROR", Log.getStackTraceString(e)) + result.success(null) + } } + } - /** Get all datapoints of the DataType within the given time range */ - private fun getData(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 { - try { - mapToType[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 - ), - ) - - 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) - } + // TODO: Find alternative to SOURCE_ID or make it nullable? + private 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, + ), + ) - // 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 - (workoutTypeMap - .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) } - } catch (e: Exception) { - Log.i("FLUTTER_HEALTH::ERROR", "Unable to return $dataType due to the following exception:") - Log.e("FLUTTER_HEALTH::ERROR", Log.getStackTraceString(e)) - result.success(null) - } - } - } + 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, + ), + ) - private fun convertRecordStage( - stage: SleepSessionRecord.Stage, - dataType: String, - sourceName: String - ): List> { + is BodyFatRecord -> 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.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 getAggregateData(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 { - try { - mapToAggregateMetric[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 - } + 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, + ), + ) - 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) } - } catch (e: Exception) { - Log.i("FLUTTER_HEALTH::ERROR", "Unable to return $dataType due to the following exception:") - Log.e("FLUTTER_HEALTH::ERROR", Log.getStackTraceString(e)) - result.success(null) - } - } - } + 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, + ), + ) - // TODO: Find alternative to SOURCE_ID or make it nullable? - private 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 HeartRateVariabilityRmssdRecord -> - return listOf( - mapOf( - "value" to - record.heartRateVariabilityMillis, - "date_from" to - record.time - .toEpochMilli(), - "date_to" to - record.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 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, - "caffeine" to record.caffeine?.inGrams, - "vitamin_a" to record.vitaminA?.inGrams, - "b1_thiamine" to record.thiamin?.inGrams, - "b2_riboflavin" to record.riboflavin?.inGrams, - "b3_niacin" to record.niacin?.inGrams, - "b5_pantothenic_acid" to record.pantothenicAcid?.inGrams, - "b6_pyridoxine" to record.vitaminB6?.inGrams, - "b7_biotin" to record.biotin?.inGrams, - "b9_folate" to record.folate?.inGrams, - "b12_cobalamin" to record.vitaminB12?.inGrams, - "vitamin_c" to record.vitaminC?.inGrams, - "vitamin_d" to record.vitaminD?.inGrams, - "vitamin_e" to record.vitaminE?.inGrams, - "vitamin_k" to record.vitaminK?.inGrams, - "calcium" to record.calcium?.inGrams, - "chloride" to record.chloride?.inGrams, - "cholesterol" to record.cholesterol?.inGrams, - "choline" to null, - "chromium" to record.chromium?.inGrams, - "copper" to record.copper?.inGrams, - "fat_unsaturated" to record.unsaturatedFat?.inGrams, - "fat_monounsaturated" to record.monounsaturatedFat?.inGrams, - "fat_polyunsaturated" to record.polyunsaturatedFat?.inGrams, - "fat_saturated" to record.saturatedFat?.inGrams, - "fat_trans_monoenoic" to record.transFat?.inGrams, - "fiber" to record.dietaryFiber?.inGrams, - "iodine" to record.iodine?.inGrams, - "iron" to record.iron?.inGrams, - "magnesium" to record.magnesium?.inGrams, - "manganese" to record.manganese?.inGrams, - "molybdenum" to record.molybdenum?.inGrams, - "phosphorus" to record.phosphorus?.inGrams, - "potassium" to record.potassium?.inGrams, - "selenium" to record.selenium?.inGrams, - "sodium" to record.sodium?.inGrams, - "sugar" to record.sugar?.inGrams, - "water" to null, - "zinc" to record.zinc?.inGrams, - "name" to record.name!!, - "meal_type" to - (mapTypeToMealType[ - 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 MenstruationFlowRecord -> - return listOf( - mapOf( - "value" to record.flow, - "date_from" to record.time.toEpochMilli(), - "date_to" to record.time.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 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, + ) } - } - // 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 - private fun writeData(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( - 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, - ) - HEART_RATE_VARIABILITY_RMSSD -> - HeartRateVariabilityRmssdRecord( - time = - Instant.ofEpochMilli( - startTime - ), - heartRateVariabilityMillis = - 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, - ) - MENSTRUATION_FLOW -> MenstruationFlowRecord( - time = Instant.ofEpochMilli(startTime), - flow = value.toInt(), - zoneOffset = 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 HeartRateVariabilityRmssdRecord -> + return listOf( + mapOf( + "value" to + record.heartRateVariabilityMillis, + "date_from" to + record.time + .toEpochMilli(), + "date_to" to + record.time + .toEpochMilli(), + "source_id" to "", + "source_name" to + metadata.dataOrigin + .packageName, + ), + ) - /** Save a Workout session with options for distance and calories expended */ - private fun writeWorkoutData(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 (!workoutTypeMap.containsKey(type)) { - result.success(false) - Log.w( - "FLUTTER_HEALTH::ERROR", - "[Health Connect] Workout type not supported" - ) - return - } - val workoutType = workoutTypeMap[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, - ), + 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 ) - 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, + 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 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, + "caffeine" to record.caffeine?.inGrams, + "vitamin_a" to record.vitaminA?.inGrams, + "b1_thiamine" to record.thiamin?.inGrams, + "b2_riboflavin" to record.riboflavin?.inGrams, + "b3_niacin" to record.niacin?.inGrams, + "b5_pantothenic_acid" to record.pantothenicAcid?.inGrams, + "b6_pyridoxine" to record.vitaminB6?.inGrams, + "b7_biotin" to record.biotin?.inGrams, + "b9_folate" to record.folate?.inGrams, + "b12_cobalamin" to record.vitaminB12?.inGrams, + "vitamin_c" to record.vitaminC?.inGrams, + "vitamin_d" to record.vitaminD?.inGrams, + "vitamin_e" to record.vitaminE?.inGrams, + "vitamin_k" to record.vitaminK?.inGrams, + "calcium" to record.calcium?.inGrams, + "chloride" to record.chloride?.inGrams, + "cholesterol" to record.cholesterol?.inGrams, + "choline" to null, + "chromium" to record.chromium?.inGrams, + "copper" to record.copper?.inGrams, + "fat_unsaturated" to record.unsaturatedFat?.inGrams, + "fat_monounsaturated" to record.monounsaturatedFat?.inGrams, + "fat_polyunsaturated" to record.polyunsaturatedFat?.inGrams, + "fat_saturated" to record.saturatedFat?.inGrams, + "fat_trans_monoenoic" to record.transFat?.inGrams, + "fiber" to record.dietaryFiber?.inGrams, + "iodine" to record.iodine?.inGrams, + "iron" to record.iron?.inGrams, + "magnesium" to record.magnesium?.inGrams, + "manganese" to record.manganese?.inGrams, + "molybdenum" to record.molybdenum?.inGrams, + "phosphorus" to record.phosphorus?.inGrams, + "potassium" to record.potassium?.inGrams, + "selenium" to record.selenium?.inGrams, + "sodium" to record.sodium?.inGrams, + "sugar" to record.sugar?.inGrams, + "water" to null, + "zinc" to record.zinc?.inGrams, + "name" to record.name!!, + "meal_type" to + (mapTypeToMealType[ + 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 MenstruationFlowRecord -> + return listOf( + mapOf( + "value" to record.flow, + "date_from" to record.time.toEpochMilli(), + "date_to" to record.time.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? + } + } + + // 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 + private fun writeData(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( + 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, + ) + + HEART_RATE_VARIABILITY_RMSSD -> + HeartRateVariabilityRmssdRecord( + time = + Instant.ofEpochMilli( + startTime + ), + heartRateVariabilityMillis = + 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) - } - } - } + ), + ) - /** Save a Blood Pressure measurement with systolic and diastolic values */ - private fun writeBloodPressure(call: MethodCall, result: Result) { - val systolic = call.argument("systolic")!! - val diastolic = call.argument("diastolic")!! - val startTime = Instant.ofEpochMilli(call.argument("startTime")!!) - - 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, + ) + + MENSTRUATION_FLOW -> MenstruationFlowRecord( + time = Instant.ofEpochMilli(startTime), + flow = value.toInt(), + zoneOffset = 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 " + ) - /** Delete records of the given type in the time range */ - private fun deleteData(call: MethodCall, result: Result) { - val type = call.argument("dataTypeKey")!! - val startTime = Instant.ofEpochMilli(call.argument("startTime")!!) - val endTime = Instant.ofEpochMilli(call.argument("endTime")!!) - if (!mapToType.containsKey(type)) { - Log.w("FLUTTER_HEALTH::ERROR", "Datatype $type not found in HC") - result.success(false) - return + 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) + } + } + } + + /** Save a Workout session with options for distance and calories expended */ + private fun writeWorkoutData(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 (!workoutTypeMap.containsKey(type)) { + result.success(false) + Log.w( + "FLUTTER_HEALTH::ERROR", + "[Health Connect] Workout type not supported" + ) + return + } + val workoutType = workoutTypeMap[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 = mapToType[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) + } } + } + + /** Save a Blood Pressure measurement with systolic and diastolic values */ + private fun writeBloodPressure(call: MethodCall, result: Result) { + val systolic = call.argument("systolic")!! + val diastolic = call.argument("diastolic")!! + val startTime = Instant.ofEpochMilli(call.argument("startTime")!!) + + 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) + } + } + } + + /** Delete records of the given type in the time range */ + private fun deleteData(call: MethodCall, result: Result) { + val type = call.argument("dataTypeKey")!! + val startTime = Instant.ofEpochMilli(call.argument("startTime")!!) + val endTime = Instant.ofEpochMilli(call.argument("endTime")!!) + if (!mapToType.containsKey(type)) { + Log.w("FLUTTER_HEALTH::ERROR", "Datatype $type not found in HC") + result.success(false) + return + } + val classType = mapToType[type]!! + + scope.launch { + try { + healthConnectClient.deleteRecords( + recordType = classType, + timeRangeFilter = + TimeRangeFilter.between( + startTime, + endTime + ), + ) + result.success(true) + } catch (e: Exception) { + result.success(false) + } + } + } + + private 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 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 mapMealTypeToType = - 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 mapMealTypeToType = + 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 mapTypeToMealType = - 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 mapTypeToMealType = + 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 mapToType = - 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, - HEART_RATE_VARIABILITY_RMSSD to HeartRateVariabilityRmssdRecord::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, - MENSTRUATION_FLOW to MenstruationFlowRecord::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, - // "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, - ) + private val mapToType = + 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, + HEART_RATE_VARIABILITY_RMSSD to HeartRateVariabilityRmssdRecord::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, + MENSTRUATION_FLOW to MenstruationFlowRecord::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, + // "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, + ) - private val mapToAggregateMetric = - 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 - ) + private val mapToAggregateMetric = + 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 + ) // TODO: Update with new workout types when Health Connect becomes the standard. private val workoutTypeMap = From 401fa75dc6b2ae4704891860c22a45d010774ba0 Mon Sep 17 00:00:00 2001 From: Aamir Farooq Date: Tue, 6 Aug 2024 14:33:41 +0200 Subject: [PATCH 03/32] Remove Google Fit column from readme --- packages/health/README.md | 382 +++++++++++++++++++------------------- 1 file changed, 191 insertions(+), 191 deletions(-) diff --git a/packages/health/README.md b/packages/health/README.md index f5d4a6437..03b01b12d 100644 --- a/packages/health/README.md +++ b/packages/health/README.md @@ -312,199 +312,199 @@ points = Health().removeDuplicates(points); The plugin supports the following [`HealthDataType`](https://pub.dev/documentation/health/latest/health/HealthDataType.html). -| **Data Type** | **Unit** | **Apple Health** | **Google Fit** | **Google Health Connect** | **Comments** | -| ---------------------------- | ----------------------- | ---------------- | -------------- | ------------------------- | -------------------------------------- | -| ACTIVE_ENERGY_BURNED | CALORIES | yes | yes | yes | | -| BASAL_ENERGY_BURNED | CALORIES | yes | | yes | | -| BLOOD_GLUCOSE | MILLIGRAM_PER_DECILITER | yes | yes | yes | | -| BLOOD_OXYGEN | PERCENTAGE | yes | yes | yes | | -| BLOOD_PRESSURE_DIASTOLIC | MILLIMETER_OF_MERCURY | yes | yes | yes | | -| BLOOD_PRESSURE_SYSTOLIC | MILLIMETER_OF_MERCURY | yes | yes | yes | | -| BODY_FAT_PERCENTAGE | PERCENTAGE | yes | yes | yes | | -| BODY_MASS_INDEX | NO_UNIT | yes | yes | yes | | -| BODY_TEMPERATURE | DEGREE_CELSIUS | yes | yes | yes | | -| BODY_WATER_MASS | KILOGRAMS | | | yes | | -| ELECTRODERMAL_ACTIVITY | SIEMENS | yes | | | | -| HEART_RATE | BEATS_PER_MINUTE | yes | yes | yes | | -| HEIGHT | METERS | yes | yes | yes | | -| RESTING_HEART_RATE | BEATS_PER_MINUTE | yes | | yes | | -| RESPIRATORY_RATE | RESPIRATIONS_PER_MINUTE | yes | | yes | | -| PERIPHERAL_PERFUSION_INDEX | PERCENTAGE | yes | | | | -| STEPS | COUNT | yes | yes | yes | | -| WAIST_CIRCUMFERENCE | METERS | yes | | | | -| WALKING_HEART_RATE | BEATS_PER_MINUTE | yes | | | | -| WEIGHT | KILOGRAMS | yes | yes | yes | | -| DISTANCE_WALKING_RUNNING | METERS | yes | | | | -| FLIGHTS_CLIMBED | COUNT | yes | | yes | | -| MOVE_MINUTES | MINUTES | | yes | | | -| DISTANCE_DELTA | METERS | | yes | yes | | -| MINDFULNESS | MINUTES | yes | | | | -| SLEEP_IN_BED | MINUTES | yes | | | | -| SLEEP_ASLEEP | MINUTES | yes | | yes | | -| SLEEP_AWAKE | MINUTES | yes | | yes | | -| SLEEP_DEEP | MINUTES | yes | | yes | | -| SLEEP_LIGHT | MINUTES | | | yes | | -| SLEEP_REM | MINUTES | yes | | yes | | -| SLEEP_OUT_OF_BED | MINUTES | | | yes | | -| SLEEP_SESSION | MINUTES | | | yes | | -| WATER | LITER | yes | yes | yes | | -| EXERCISE_TIME | MINUTES | yes | | | | -| WORKOUT | NO_UNIT | yes | yes | yes | See table below | -| HIGH_HEART_RATE_EVENT | NO_UNIT | yes | | | Requires Apple Watch to write the data | -| LOW_HEART_RATE_EVENT | NO_UNIT | yes | | | Requires Apple Watch to write the data | -| IRREGULAR_HEART_RATE_EVENT | NO_UNIT | yes | | | Requires Apple Watch to write the data | -| HEART_RATE_VARIABILITY_RMSSD | MILLISECONDS | | | yes | | -| HEART_RATE_VARIABILITY_SDNN | MILLISECONDS | yes | | | Requires Apple Watch to write the data | -| HEADACHE_NOT_PRESENT | MINUTES | yes | | | | -| HEADACHE_MILD | MINUTES | yes | | | | -| HEADACHE_MODERATE | MINUTES | yes | | | | -| HEADACHE_SEVERE | MINUTES | yes | | | | -| HEADACHE_UNSPECIFIED | MINUTES | yes | | | | -| AUDIOGRAM | DECIBEL_HEARING_LEVEL | yes | | | | -| ELECTROCARDIOGRAM | VOLT | yes | | | Requires Apple Watch to write the data | -| NUTRITION | NO_UNIT | yes | yes | yes | | -| INSULIN_DELIVERY | INTERNATIONAL_UNIT | yes | | | | +| **Data Type** | **Unit** | **Apple Health** | **Google Health Connect** | **Comments** | +| ---------------------------- | ----------------------- | ---------------- | ------------------------- | -------------------------------------- | +| ACTIVE_ENERGY_BURNED | CALORIES | yes | yes | | +| BASAL_ENERGY_BURNED | CALORIES | yes | yes | | +| BLOOD_GLUCOSE | MILLIGRAM_PER_DECILITER | yes | yes | | +| BLOOD_OXYGEN | PERCENTAGE | yes | yes | | +| BLOOD_PRESSURE_DIASTOLIC | MILLIMETER_OF_MERCURY | yes | yes | | +| BLOOD_PRESSURE_SYSTOLIC | MILLIMETER_OF_MERCURY | yes | yes | | +| BODY_FAT_PERCENTAGE | PERCENTAGE | yes | yes | | +| BODY_MASS_INDEX | NO_UNIT | yes | yes | | +| BODY_TEMPERATURE | DEGREE_CELSIUS | yes | yes | | +| BODY_WATER_MASS | KILOGRAMS | | yes | | +| ELECTRODERMAL_ACTIVITY | SIEMENS | yes | | | +| HEART_RATE | BEATS_PER_MINUTE | yes | yes | | +| HEIGHT | METERS | yes | yes | | +| RESTING_HEART_RATE | BEATS_PER_MINUTE | yes | yes | | +| RESPIRATORY_RATE | RESPIRATIONS_PER_MINUTE | yes | yes | | +| PERIPHERAL_PERFUSION_INDEX | PERCENTAGE | yes | | | +| STEPS | COUNT | yes | yes | | +| WAIST_CIRCUMFERENCE | METERS | yes | | | +| WALKING_HEART_RATE | BEATS_PER_MINUTE | yes | | | +| WEIGHT | KILOGRAMS | yes | yes | | +| DISTANCE_WALKING_RUNNING | METERS | yes | | | +| FLIGHTS_CLIMBED | COUNT | yes | yes | | +| MOVE_MINUTES | MINUTES | | | | +| DISTANCE_DELTA | METERS | | yes | | +| MINDFULNESS | MINUTES | yes | | | +| SLEEP_IN_BED | MINUTES | yes | | | +| SLEEP_ASLEEP | MINUTES | yes | yes | | +| SLEEP_AWAKE | MINUTES | yes | yes | | +| SLEEP_DEEP | MINUTES | yes | yes | | +| SLEEP_LIGHT | MINUTES | | yes | | +| SLEEP_REM | MINUTES | yes | yes | | +| SLEEP_OUT_OF_BED | MINUTES | | yes | | +| SLEEP_SESSION | MINUTES | | yes | | +| WATER | LITER | yes | yes | | +| EXERCISE_TIME | MINUTES | yes | | | +| WORKOUT | NO_UNIT | yes | yes | See table below | +| HIGH_HEART_RATE_EVENT | NO_UNIT | yes | | Requires Apple Watch to write the data | +| LOW_HEART_RATE_EVENT | NO_UNIT | yes | | Requires Apple Watch to write the data | +| IRREGULAR_HEART_RATE_EVENT | NO_UNIT | yes | | Requires Apple Watch to write the data | +| HEART_RATE_VARIABILITY_RMSSD | MILLISECONDS | | yes | | +| HEART_RATE_VARIABILITY_SDNN | MILLISECONDS | yes | | Requires Apple Watch to write the data | +| HEADACHE_NOT_PRESENT | MINUTES | yes | | | +| HEADACHE_MILD | MINUTES | yes | | | +| HEADACHE_MODERATE | MINUTES | yes | | | +| HEADACHE_SEVERE | MINUTES | yes | | | +| HEADACHE_UNSPECIFIED | MINUTES | yes | | | +| AUDIOGRAM | DECIBEL_HEARING_LEVEL | yes | | | +| ELECTROCARDIOGRAM | VOLT | yes | | Requires Apple Watch to write the data | +| NUTRITION | NO_UNIT | yes | yes | | +| INSULIN_DELIVERY | INTERNATIONAL_UNIT | yes | | | ## Workout Types The plugin supports the following [`HealthWorkoutActivityType`](https://pub.dev/documentation/health/latest/health/HealthWorkoutActivityType.html). -| **Workout Type** | **Apple Health** | **Google Fit** | **Google Health Connect** | **Comments** | -| -------------------------------- | ---------------- | -------------- | ------------------------- | ----------------------------------------------------------------- | -| ARCHERY | yes | yes | | | -| BADMINTON | yes | yes | yes | | -| BASEBALL | yes | yes | yes | | -| BASKETBALL | yes | yes | yes | | -| BIKING | yes | yes | yes | on iOS this is CYCLING, but name changed here to fit with Android | -| BOXING | yes | yes | yes | | -| CRICKET | yes | yes | yes | | -| CURLING | yes | yes | | | -| ELLIPTICAL | yes | yes | yes | | -| FENCING | yes | yes | yes | | -| AMERICAN_FOOTBALL | yes | yes | yes | | -| AUSTRALIAN_FOOTBALL | yes | yes | yes | | -| SOCCER | yes | yes | | | -| GOLF | yes | yes | yes | | -| GYMNASTICS | yes | yes | yes | | -| HANDBALL | yes | yes | yes | | -| HIGH_INTENSITY_INTERVAL_TRAINING | yes | yes | yes | | -| HIKING | yes | yes | yes | | -| HOCKEY | yes | yes | | | -| SKATING | yes | yes | yes | On iOS this is skating_sports | -| JUMP_ROPE | yes | yes | | | -| KICKBOXING | yes | yes | | | -| MARTIAL_ARTS | yes | yes | yes | | -| PILATES | yes | yes | yes | | -| RACQUETBALL | yes | yes | yes | | -| RUGBY | yes | yes | yes | | -| RUNNING | yes | yes | yes | | -| ROWING | yes | yes | yes | | -| SAILING | yes | yes | yes | | -| CROSS_COUNTRY_SKIING | yes | yes | | | -| DOWNHILL_SKIING | yes | yes | | | -| SNOWBOARDING | yes | yes | yes | | -| SOFTBALL | yes | yes | yes | | -| SQUASH | yes | yes | yes | | -| STAIR_CLIMBING | yes | yes | yes | | -| SWIMMING | yes | yes | | | -| TABLE_TENNIS | yes | yes | yes | | -| TENNIS | yes | yes | yes | | -| VOLLEYBALL | yes | yes | yes | | -| WALKING | yes | yes | yes | | -| WATER_POLO | yes | yes | yes | | -| YOGA | yes | yes | yes | | -| BOWLING | yes | | | | -| CROSS_TRAINING | yes | | | | -| TRACK_AND_FIELD | yes | | | | -| DISC_SPORTS | yes | | | | -| LACROSSE | yes | | | | -| PREPARATION_AND_RECOVERY | yes | | | | -| FLEXIBILITY | yes | | | | -| COOLDOWN | yes | | | | -| WHEELCHAIR_WALK_PACE | yes | | | | -| WHEELCHAIR_RUN_PACE | yes | | | | -| HAND_CYCLING | yes | | | | -| CORE_TRAINING | yes | | | | -| FUNCTIONAL_STRENGTH_TRAINING | yes | | | | -| TRADITIONAL_STRENGTH_TRAINING | yes | | | | -| MIXED_CARDIO | yes | | | | -| STAIRS | yes | | | | -| STEP_TRAINING | yes | | | | -| FITNESS_GAMING | yes | | | | -| BARRE | yes | | | | -| CARDIO_DANCE | yes | | | | -| SOCIAL_DANCE | yes | | | | -| MIND_AND_BODY | yes | | | | -| PICKLEBALL | yes | | | | -| CLIMBING | yes | | | | -| EQUESTRIAN_SPORTS | yes | | | | -| FISHING | yes | | | | -| HUNTING | yes | | | | -| PLAY | yes | | | | -| SNOW_SPORTS | yes | | | | -| PADDLE_SPORTS | yes | | | | -| SURFING_SPORTS | yes | | | | -| WATER_FITNESS | yes | | | | -| WATER_SPORTS | yes | | | | -| TAI_CHI | yes | | | | -| WRESTLING | yes | | | | -| AEROBICS | | yes | | | -| BIATHLON | | yes | | | -| CALISTHENICS | | yes | yes | | -| CIRCUIT_TRAINING | | yes | | | -| CROSS_FIT | | yes | | | -| DANCING | | yes | yes | | -| DIVING | | yes | | | -| ELEVATOR | | yes | | | -| ERGOMETER | | yes | | | -| ESCALATOR | | yes | | | -| FRISBEE_DISC | | yes | yes | | -| GARDENING | | yes | | | -| GUIDED_BREATHING | | yes | yes | | -| HORSEBACK_RIDING | | yes | | | -| HOUSEWORK | | yes | | | -| INTERVAL_TRAINING | | yes | | | -| IN_VEHICLE | | yes | | | -| KAYAKING | | yes | | | -| KETTLEBELL_TRAINING | | yes | | | -| KICK_SCOOTER | | yes | | | -| KITE_SURFING | | yes | | | -| MEDITATION | | yes | | | -| MIXED_MARTIAL_ARTS | | yes | | | -| P90X | | yes | | | -| PARAGLIDING | | yes | yes | | -| POLO | | yes | | | -| ROCK_CLIMBING | (yes) | yes | yes | on iOS this will be stored as CLIMBING | -| RUNNING_JOGGING | (yes) | yes | | on iOS this will be stored as RUNNING | -| RUNNING_SAND | (yes) | yes | | on iOS this will be stored as RUNNING | -| RUNNING_TREADMILL | (yes) | yes | yes | on iOS this will be stored as RUNNING | -| SCUBA_DIVING | | yes | yes | | -| SKATING_CROSS | (yes) | yes | | on iOS this will be stored as SKATING | -| SKATING_INDOOR | (yes) | yes | | on iOS this will be stored as SKATING | -| SKATING_INLINE | (yes) | yes | | on iOS this will be stored as SKATING | -| SKIING_BACK_COUNTRY | | yes | | | -| SKIING_KITE | | yes | | | -| SKIING_ROLLER | | yes | | | -| SLEDDING | | yes | | | -| STAIR_CLIMBING_MACHINE | | yes | yes | | -| STANDUP_PADDLEBOARDING | | yes | | | -| STILL | | yes | | | -| STRENGTH_TRAINING | | yes | yes | | -| SURFING | | yes | yes | | -| SWIMMING_OPEN_WATER | | yes | yes | | -| SWIMMING_POOL | | yes | yes | | -| TEAM_SPORTS | | yes | | | -| TILTING | | yes | | | -| TREADMILL | | yes | | | -| VOLLEYBALL_BEACH | | yes | | | -| VOLLEYBALL_INDOOR | | yes | | | -| WAKEBOARDING | | yes | | | -| WALKING_FITNESS | | yes | | | -| WALKING_NORDIC | | yes | | | -| WALKING_STROLLER | | yes | | | -| WALKING_TREADMILL | | yes | | | -| WEIGHTLIFTING | | yes | yes | | -| WHEELCHAIR | | yes | yes | | -| WINDSURFING | | yes | | | -| ZUMBA | | yes | | | -| OTHER | yes | yes | | | +| **Workout Type** | **Apple Health** | **Google Health Connect** | **Comments** | +| -------------------------------- | ---------------- | ------------------------- | ----------------------------------------------------------------- | +| AEROBICS | | | | +| AMERICAN_FOOTBALL | yes | yes | | +| ARCHERY | yes | | | +| AUSTRALIAN_FOOTBALL | yes | yes | | +| BADMINTON | yes | yes | | +| BARRE | yes | | | +| BASEBALL | yes | yes | | +| BASKETBALL | yes | yes | | +| BIATHLON | | | | +| BIKING | yes | yes | on iOS this is CYCLING, but name changed here to fit with Android | +| BOWLING | yes | | | +| BOXING | yes | yes | | +| CALISTHENICS | | yes | | +| CARDIO_DANCE | yes | | | +| CIRCUIT_TRAINING | | | | +| CLIMBING | yes | | | +| COOLDOWN | yes | | | +| CORE_TRAINING | yes | | | +| CRICKET | yes | yes | | +| CROSS_COUNTRY_SKIING | yes | | | +| CROSS_FIT | | | | +| CROSS_TRAINING | yes | | | +| CURLING | yes | | | +| DANCING | | yes | | +| DISC_SPORTS | yes | | | +| DIVING | | | | +| DOWNHILL_SKIING | yes | | | +| ELEVATOR | | | | +| ELLIPTICAL | yes | yes | | +| EQUESTRIAN_SPORTS | yes | | | +| ERGOMETER | | | | +| ESCALATOR | | | | +| FENCING | yes | yes | | +| FISHING | yes | | | +| FITNESS_GAMING | yes | | | +| FLEXIBILITY | yes | | | +| FRISBEE_DISC | | yes | | +| FUNCTIONAL_STRENGTH_TRAINING | yes | | | +| GARDENING | | | | +| GOLF | yes | yes | | +| GUIDED_BREATHING | | yes | | +| GYMNASTICS | yes | yes | | +| HAND_CYCLING | yes | | | +| HANDBALL | yes | yes | | +| HIGH_INTENSITY_INTERVAL_TRAINING | yes | yes | | +| HIKING | yes | yes | | +| HOCKEY | yes | | | +| HORSEBACK_RIDING | | | | +| HOUSEWORK | | | | +| HUNTING | yes | | | +| IN_VEHICLE | | | | +| INTERVAL_TRAINING | | | | +| JUMP_ROPE | yes | | | +| KAYAKING | | | | +| KETTLEBELL_TRAINING | | | | +| KICK_SCOOTER | | | | +| KICKBOXING | yes | | | +| KITE_SURFING | | | | +| LACROSSE | yes | | | +| MARTIAL_ARTS | yes | yes | | +| MEDITATION | | | | +| MIND_AND_BODY | yes | | | +| MIXED_CARDIO | yes | | | +| MIXED_MARTIAL_ARTS | | | | +| P90X | | | | +| PADDLE_SPORTS | yes | | | +| PARAGLIDING | | yes | | +| PICKLEBALL | yes | | | +| PILATES | yes | yes | | +| PLAY | yes | | | +| POLO | | | | +| PREPARATION_AND_RECOVERY | yes | | | +| RACQUETBALL | yes | yes | | +| ROCK_CLIMBING | (yes) | yes | on iOS this will be stored as CLIMBING | +| ROWING | yes | yes | | +| RUGBY | yes | yes | | +| RUNNING | yes | yes | | +| RUNNING_JOGGING | (yes) | | on iOS this will be stored as RUNNING | +| RUNNING_SAND | (yes) | | on iOS this will be stored as RUNNING | +| RUNNING_TREADMILL | (yes) | yes | on iOS this will be stored as RUNNING | +| SAILING | yes | yes | | +| SCUBA_DIVING | | yes | | +| SKATING | yes | yes | On iOS this is skating_sports | +| SKATING_CROSS | (yes) | | on iOS this will be stored as SKATING | +| SKATING_INDOOR | (yes) | | on iOS this will be stored as SKATING | +| SKATING_INLINE | (yes) | | on iOS this will be stored as SKATING | +| SKIING_BACK_COUNTRY | | | | +| SKIING_KITE | | | | +| SKIING_ROLLER | | | | +| SLEDDING | | | | +| SNOW_SPORTS | yes | | | +| SNOWBOARDING | yes | yes | | +| SOCCER | yes | | | +| SOCIAL_DANCE | yes | | | +| SOFTBALL | yes | yes | | +| SQUASH | yes | yes | | +| STAIR_CLIMBING | yes | yes | | +| STAIR_CLIMBING_MACHINE | | yes | | +| STAIRS | yes | | | +| STANDUP_PADDLEBOARDING | | | | +| STEP_TRAINING | yes | | | +| STILL | | | | +| STRENGTH_TRAINING | | yes | | +| SURFING | | yes | | +| SURFING_SPORTS | yes | | | +| SWIMMING | yes | | | +| SWIMMING_OPEN_WATER | | yes | | +| SWIMMING_POOL | | yes | | +| TABLE_TENNIS | yes | yes | | +| TAI_CHI | yes | | | +| TEAM_SPORTS | | | | +| TENNIS | yes | yes | | +| TILTING | | | | +| TRACK_AND_FIELD | yes | | | +| TRADITIONAL_STRENGTH_TRAINING | yes | | | +| TREADMILL | | | | +| VOLLEYBALL | yes | yes | | +| VOLLEYBALL_BEACH | | | | +| VOLLEYBALL_INDOOR | | | | +| WAKEBOARDING | | | | +| WALKING | yes | yes | | +| WALKING_FITNESS | | | | +| WALKING_NORDIC | | | | +| WALKING_STROLLER | | | | +| WALKING_TREADMILL | | | | +| WATER_FITNESS | yes | | | +| WATER_POLO | yes | yes | | +| WATER_SPORTS | yes | | | +| WEIGHTLIFTING | | yes | | +| WHEELCHAIR | | yes | | +| WHEELCHAIR_RUN_PACE | yes | | | +| WHEELCHAIR_WALK_PACE | yes | | | +| WINDSURFING | | | | +| WRESTLING | yes | | | +| YOGA | yes | yes | | +| ZUMBA | | | | +| OTHER | yes | | | From aa4c76391431efb5a4c27e1817dfd5f1ff5e0456 Mon Sep 17 00:00:00 2001 From: Aamir Farooq Date: Tue, 6 Aug 2024 15:04:34 +0200 Subject: [PATCH 04/32] Remove support for Google Fit types not supported by Health Connect --- packages/health/README.md | 33 +--- .../cachet/plugins/health/HealthPlugin.kt | 65 +------ packages/health/example/lib/util.dart | 1 - packages/health/lib/health.g.dart | 173 +++++++----------- packages/health/lib/health.json.dart | 1 - packages/health/lib/src/health_plugin.dart | 122 +++++------- packages/health/lib/src/heath_data_types.dart | 87 +++------ 7 files changed, 155 insertions(+), 327 deletions(-) diff --git a/packages/health/README.md b/packages/health/README.md index 03b01b12d..d2294f901 100644 --- a/packages/health/README.md +++ b/packages/health/README.md @@ -336,7 +336,6 @@ The plugin supports the following [`HealthDataType`](https://pub.dev/documentati | WEIGHT | KILOGRAMS | yes | yes | | | DISTANCE_WALKING_RUNNING | METERS | yes | | | | FLIGHTS_CLIMBED | COUNT | yes | yes | | -| MOVE_MINUTES | MINUTES | | | | | DISTANCE_DELTA | METERS | | yes | | | MINDFULNESS | MINUTES | yes | | | | SLEEP_IN_BED | MINUTES | yes | | | @@ -371,7 +370,6 @@ The plugin supports the following [`HealthWorkoutActivityType`](https://pub.dev/ | **Workout Type** | **Apple Health** | **Google Health Connect** | **Comments** | | -------------------------------- | ---------------- | ------------------------- | ----------------------------------------------------------------- | -| AEROBICS | | | | | AMERICAN_FOOTBALL | yes | yes | | | ARCHERY | yes | | | | AUSTRALIAN_FOOTBALL | yes | yes | | @@ -379,37 +377,29 @@ The plugin supports the following [`HealthWorkoutActivityType`](https://pub.dev/ | BARRE | yes | | | | BASEBALL | yes | yes | | | BASKETBALL | yes | yes | | -| BIATHLON | | | | | BIKING | yes | yes | on iOS this is CYCLING, but name changed here to fit with Android | | BOWLING | yes | | | | BOXING | yes | yes | | | CALISTHENICS | | yes | | | CARDIO_DANCE | yes | | | -| CIRCUIT_TRAINING | | | | | CLIMBING | yes | | | | COOLDOWN | yes | | | | CORE_TRAINING | yes | | | | CRICKET | yes | yes | | | CROSS_COUNTRY_SKIING | yes | | | -| CROSS_FIT | | | | | CROSS_TRAINING | yes | | | | CURLING | yes | | | | DANCING | | yes | | | DISC_SPORTS | yes | | | -| DIVING | | | | | DOWNHILL_SKIING | yes | | | -| ELEVATOR | | | | | ELLIPTICAL | yes | yes | | | EQUESTRIAN_SPORTS | yes | | | -| ERGOMETER | | | | -| ESCALATOR | | | | | FENCING | yes | yes | | | FISHING | yes | | | | FITNESS_GAMING | yes | | | | FLEXIBILITY | yes | | | | FRISBEE_DISC | | yes | | | FUNCTIONAL_STRENGTH_TRAINING | yes | | | -| GARDENING | | | | | GOLF | yes | yes | | | GUIDED_BREATHING | | yes | | | GYMNASTICS | yes | yes | | @@ -418,30 +408,18 @@ The plugin supports the following [`HealthWorkoutActivityType`](https://pub.dev/ | HIGH_INTENSITY_INTERVAL_TRAINING | yes | yes | | | HIKING | yes | yes | | | HOCKEY | yes | | | -| HORSEBACK_RIDING | | | | -| HOUSEWORK | | | | | HUNTING | yes | | | -| IN_VEHICLE | | | | -| INTERVAL_TRAINING | | | | | JUMP_ROPE | yes | | | -| KAYAKING | | | | -| KETTLEBELL_TRAINING | | | | -| KICK_SCOOTER | | | | | KICKBOXING | yes | | | -| KITE_SURFING | | | | | LACROSSE | yes | | | | MARTIAL_ARTS | yes | yes | | -| MEDITATION | | | | | MIND_AND_BODY | yes | | | | MIXED_CARDIO | yes | | | -| MIXED_MARTIAL_ARTS | | | | -| P90X | | | | | PADDLE_SPORTS | yes | | | | PARAGLIDING | | yes | | | PICKLEBALL | yes | | | | PILATES | yes | yes | | | PLAY | yes | | | -| POLO | | | | | PREPARATION_AND_RECOVERY | yes | | | | RACQUETBALL | yes | yes | | | ROCK_CLIMBING | (yes) | yes | on iOS this will be stored as CLIMBING | @@ -460,7 +438,6 @@ The plugin supports the following [`HealthWorkoutActivityType`](https://pub.dev/ | SKIING_BACK_COUNTRY | | | | | SKIING_KITE | | | | | SKIING_ROLLER | | | | -| SLEDDING | | | | | SNOW_SPORTS | yes | | | | SNOWBOARDING | yes | yes | | | SOCCER | yes | | | @@ -472,7 +449,6 @@ The plugin supports the following [`HealthWorkoutActivityType`](https://pub.dev/ | STAIRS | yes | | | | STANDUP_PADDLEBOARDING | | | | | STEP_TRAINING | yes | | | -| STILL | | | | | STRENGTH_TRAINING | | yes | | | SURFING | | yes | | | SURFING_SPORTS | yes | | | @@ -481,7 +457,6 @@ The plugin supports the following [`HealthWorkoutActivityType`](https://pub.dev/ | SWIMMING_POOL | | yes | | | TABLE_TENNIS | yes | yes | | | TAI_CHI | yes | | | -| TEAM_SPORTS | | | | | TENNIS | yes | yes | | | TILTING | | | | | TRACK_AND_FIELD | yes | | | @@ -492,10 +467,6 @@ The plugin supports the following [`HealthWorkoutActivityType`](https://pub.dev/ | VOLLEYBALL_INDOOR | | | | | WAKEBOARDING | | | | | WALKING | yes | yes | | -| WALKING_FITNESS | | | | -| WALKING_NORDIC | | | | -| WALKING_STROLLER | | | | -| WALKING_TREADMILL | | | | | WATER_FITNESS | yes | | | | WATER_POLO | yes | yes | | | WATER_SPORTS | yes | | | @@ -503,8 +474,6 @@ The plugin supports the following [`HealthWorkoutActivityType`](https://pub.dev/ | WHEELCHAIR | | yes | | | WHEELCHAIR_RUN_PACE | yes | | | | WHEELCHAIR_WALK_PACE | yes | | | -| WINDSURFING | | | | | WRESTLING | yes | | | | YOGA | yes | yes | | -| ZUMBA | | | | -| OTHER | yes | | | +| OTHER | yes | yes | | 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 58608ad39..5eafc3c13 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 @@ -2114,7 +2114,6 @@ class HealthPlugin(private var channel: MethodChannel? = null) : RESPIRATORY_RATE to RespiratoryRateRecord::class, TOTAL_CALORIES_BURNED to TotalCaloriesBurnedRecord::class, MENSTRUATION_FLOW to MenstruationFlowRecord::class, - // MOVE_MINUTES to TODO: Find alternative? // TODO: Implement remaining types // "ActiveCaloriesBurned" to // ActiveCaloriesBurnedRecord::class, @@ -2179,8 +2178,11 @@ class HealthPlugin(private var channel: MethodChannel? = null) : // TODO: Update with new workout types when Health Connect becomes the standard. private val workoutTypeMap = mapOf( - // "AEROBICS" to - // ExerciseSessionRecord.EXERCISE_TYPE_AEROBICS, + // TODO: add skiing + // TODO: add skating + // TODO: add soccer + // TOOD: look into paddling + // TODO: add runnning_treadmill "AMERICAN_FOOTBALL" to ExerciseSessionRecord .EXERCISE_TYPE_FOOTBALL_AMERICAN, @@ -2195,8 +2197,6 @@ class HealthPlugin(private var channel: MethodChannel? = null) : "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, @@ -2214,33 +2214,20 @@ class HealthPlugin(private var channel: MethodChannel? = null) : "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 @@ -2254,42 +2241,20 @@ class HealthPlugin(private var channel: MethodChannel? = null) : .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, @@ -2327,8 +2292,6 @@ class HealthPlugin(private var channel: MethodChannel? = null) : // 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, @@ -2349,7 +2312,6 @@ class HealthPlugin(private var channel: MethodChannel? = null) : .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, @@ -2365,8 +2327,6 @@ class HealthPlugin(private var channel: MethodChannel? = null) : "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 @@ -2378,16 +2338,6 @@ class HealthPlugin(private var channel: MethodChannel? = null) : .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 @@ -2398,10 +2348,7 @@ class HealthPlugin(private var channel: MethodChannel? = null) : "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, + "OTHER" to ExerciseSessionRecord.OTHER_WORKOUT, ) } diff --git a/packages/health/example/lib/util.dart b/packages/health/example/lib/util.dart index f1bfcb44f..d2b4028ba 100644 --- a/packages/health/example/lib/util.dart +++ b/packages/health/example/lib/util.dart @@ -79,7 +79,6 @@ const List dataTypesAndroid = [ HealthDataType.HEART_RATE, HealthDataType.HEART_RATE_VARIABILITY_RMSSD, HealthDataType.STEPS, - // HealthDataType.MOVE_MINUTES, // TODO: Find alternative for Health Connect HealthDataType.DISTANCE_DELTA, HealthDataType.RESPIRATORY_RATE, HealthDataType.SLEEP_AWAKE, diff --git a/packages/health/lib/health.g.dart b/packages/health/lib/health.g.dart index fbbfcc13c..d043cd59e 100644 --- a/packages/health/lib/health.g.dart +++ b/packages/health/lib/health.g.dart @@ -118,7 +118,6 @@ const _$HealthDataTypeEnumMap = { HealthDataType.DISTANCE_SWIMMING: 'DISTANCE_SWIMMING', HealthDataType.DISTANCE_CYCLING: 'DISTANCE_CYCLING', HealthDataType.FLIGHTS_CLIMBED: 'FLIGHTS_CLIMBED', - HealthDataType.MOVE_MINUTES: 'MOVE_MINUTES', HealthDataType.DISTANCE_DELTA: 'DISTANCE_DELTA', HealthDataType.MINDFULNESS: 'MINDFULNESS', HealthDataType.WATER: 'WATER', @@ -316,156 +315,126 @@ Map _$WorkoutHealthValueToJson(WorkoutHealthValue instance) { } const _$HealthWorkoutActivityTypeEnumMap = { + HealthWorkoutActivityType.AMERICAN_FOOTBALL: 'AMERICAN_FOOTBALL', HealthWorkoutActivityType.ARCHERY: 'ARCHERY', + HealthWorkoutActivityType.AUSTRALIAN_FOOTBALL: 'AUSTRALIAN_FOOTBALL', HealthWorkoutActivityType.BADMINTON: 'BADMINTON', + HealthWorkoutActivityType.BARRE: 'BARRE', HealthWorkoutActivityType.BASEBALL: 'BASEBALL', HealthWorkoutActivityType.BASKETBALL: 'BASKETBALL', + HealthWorkoutActivityType.BIKING_HAND: 'BIKING_HAND', + HealthWorkoutActivityType.BIKING_MOUNTAIN: 'BIKING_MOUNTAIN', + HealthWorkoutActivityType.BIKING_ROAD: 'BIKING_ROAD', + HealthWorkoutActivityType.BIKING_SPINNING: 'BIKING_SPINNING', + HealthWorkoutActivityType.BIKING_STATIONARY: 'BIKING_STATIONARY', + HealthWorkoutActivityType.BIKING_UTILITY: 'BIKING_UTILITY', HealthWorkoutActivityType.BIKING: 'BIKING', + HealthWorkoutActivityType.BOWLING: 'BOWLING', HealthWorkoutActivityType.BOXING: 'BOXING', + HealthWorkoutActivityType.CALISTHENICS: 'CALISTHENICS', + HealthWorkoutActivityType.CARDIO_DANCE: 'CARDIO_DANCE', + HealthWorkoutActivityType.CLIMBING: 'CLIMBING', + HealthWorkoutActivityType.COOLDOWN: 'COOLDOWN', + HealthWorkoutActivityType.CORE_TRAINING: 'CORE_TRAINING', HealthWorkoutActivityType.CRICKET: 'CRICKET', + HealthWorkoutActivityType.CROSS_COUNTRY_SKIING: 'CROSS_COUNTRY_SKIING', + HealthWorkoutActivityType.CROSS_TRAINING: 'CROSS_TRAINING', HealthWorkoutActivityType.CURLING: 'CURLING', + HealthWorkoutActivityType.DANCING: 'DANCING', + HealthWorkoutActivityType.DISC_SPORTS: 'DISC_SPORTS', + HealthWorkoutActivityType.DOWNHILL_SKIING: 'DOWNHILL_SKIING', HealthWorkoutActivityType.ELLIPTICAL: 'ELLIPTICAL', + HealthWorkoutActivityType.EQUESTRIAN_SPORTS: 'EQUESTRIAN_SPORTS', HealthWorkoutActivityType.FENCING: 'FENCING', - HealthWorkoutActivityType.AMERICAN_FOOTBALL: 'AMERICAN_FOOTBALL', - HealthWorkoutActivityType.AUSTRALIAN_FOOTBALL: 'AUSTRALIAN_FOOTBALL', - HealthWorkoutActivityType.SOCCER: 'SOCCER', + HealthWorkoutActivityType.FISHING: 'FISHING', + HealthWorkoutActivityType.FITNESS_GAMING: 'FITNESS_GAMING', + HealthWorkoutActivityType.FLEXIBILITY: 'FLEXIBILITY', + HealthWorkoutActivityType.FRISBEE_DISC: 'FRISBEE_DISC', + HealthWorkoutActivityType.FUNCTIONAL_STRENGTH_TRAINING: + 'FUNCTIONAL_STRENGTH_TRAINING', HealthWorkoutActivityType.GOLF: 'GOLF', + HealthWorkoutActivityType.GUIDED_BREATHING: 'GUIDED_BREATHING', HealthWorkoutActivityType.GYMNASTICS: 'GYMNASTICS', + HealthWorkoutActivityType.HAND_CYCLING: 'HAND_CYCLING', HealthWorkoutActivityType.HANDBALL: 'HANDBALL', HealthWorkoutActivityType.HIGH_INTENSITY_INTERVAL_TRAINING: 'HIGH_INTENSITY_INTERVAL_TRAINING', HealthWorkoutActivityType.HIKING: 'HIKING', HealthWorkoutActivityType.HOCKEY: 'HOCKEY', - HealthWorkoutActivityType.SKATING: 'SKATING', + HealthWorkoutActivityType.HUNTING: 'HUNTING', + HealthWorkoutActivityType.ICE_SKATING: 'ICE_SKATING', HealthWorkoutActivityType.JUMP_ROPE: 'JUMP_ROPE', HealthWorkoutActivityType.KICKBOXING: 'KICKBOXING', - HealthWorkoutActivityType.MARTIAL_ARTS: 'MARTIAL_ARTS', - HealthWorkoutActivityType.PILATES: 'PILATES', - HealthWorkoutActivityType.RACQUETBALL: 'RACQUETBALL', - HealthWorkoutActivityType.ROWING: 'ROWING', - HealthWorkoutActivityType.RUGBY: 'RUGBY', - HealthWorkoutActivityType.RUNNING: 'RUNNING', - HealthWorkoutActivityType.SAILING: 'SAILING', - HealthWorkoutActivityType.CROSS_COUNTRY_SKIING: 'CROSS_COUNTRY_SKIING', - HealthWorkoutActivityType.DOWNHILL_SKIING: 'DOWNHILL_SKIING', - HealthWorkoutActivityType.SNOWBOARDING: 'SNOWBOARDING', - HealthWorkoutActivityType.SOFTBALL: 'SOFTBALL', - HealthWorkoutActivityType.SQUASH: 'SQUASH', - HealthWorkoutActivityType.STAIR_CLIMBING: 'STAIR_CLIMBING', - HealthWorkoutActivityType.SWIMMING: 'SWIMMING', - HealthWorkoutActivityType.TABLE_TENNIS: 'TABLE_TENNIS', - HealthWorkoutActivityType.TENNIS: 'TENNIS', - HealthWorkoutActivityType.VOLLEYBALL: 'VOLLEYBALL', - HealthWorkoutActivityType.WALKING: 'WALKING', - HealthWorkoutActivityType.WATER_POLO: 'WATER_POLO', - HealthWorkoutActivityType.YOGA: 'YOGA', - HealthWorkoutActivityType.BOWLING: 'BOWLING', - HealthWorkoutActivityType.CROSS_TRAINING: 'CROSS_TRAINING', - HealthWorkoutActivityType.TRACK_AND_FIELD: 'TRACK_AND_FIELD', - HealthWorkoutActivityType.DISC_SPORTS: 'DISC_SPORTS', HealthWorkoutActivityType.LACROSSE: 'LACROSSE', - HealthWorkoutActivityType.PREPARATION_AND_RECOVERY: - 'PREPARATION_AND_RECOVERY', - HealthWorkoutActivityType.FLEXIBILITY: 'FLEXIBILITY', - HealthWorkoutActivityType.COOLDOWN: 'COOLDOWN', - HealthWorkoutActivityType.WHEELCHAIR_WALK_PACE: 'WHEELCHAIR_WALK_PACE', - HealthWorkoutActivityType.WHEELCHAIR_RUN_PACE: 'WHEELCHAIR_RUN_PACE', - HealthWorkoutActivityType.HAND_CYCLING: 'HAND_CYCLING', - HealthWorkoutActivityType.CORE_TRAINING: 'CORE_TRAINING', - HealthWorkoutActivityType.FUNCTIONAL_STRENGTH_TRAINING: - 'FUNCTIONAL_STRENGTH_TRAINING', - HealthWorkoutActivityType.TRADITIONAL_STRENGTH_TRAINING: - 'TRADITIONAL_STRENGTH_TRAINING', - HealthWorkoutActivityType.MIXED_CARDIO: 'MIXED_CARDIO', - HealthWorkoutActivityType.STAIRS: 'STAIRS', - HealthWorkoutActivityType.STEP_TRAINING: 'STEP_TRAINING', - HealthWorkoutActivityType.FITNESS_GAMING: 'FITNESS_GAMING', - HealthWorkoutActivityType.BARRE: 'BARRE', - HealthWorkoutActivityType.CARDIO_DANCE: 'CARDIO_DANCE', - HealthWorkoutActivityType.SOCIAL_DANCE: 'SOCIAL_DANCE', + HealthWorkoutActivityType.MARTIAL_ARTS: 'MARTIAL_ARTS', HealthWorkoutActivityType.MIND_AND_BODY: 'MIND_AND_BODY', - HealthWorkoutActivityType.PICKLEBALL: 'PICKLEBALL', - HealthWorkoutActivityType.CLIMBING: 'CLIMBING', - HealthWorkoutActivityType.EQUESTRIAN_SPORTS: 'EQUESTRIAN_SPORTS', - HealthWorkoutActivityType.FISHING: 'FISHING', - HealthWorkoutActivityType.HUNTING: 'HUNTING', - HealthWorkoutActivityType.PLAY: 'PLAY', - HealthWorkoutActivityType.SNOW_SPORTS: 'SNOW_SPORTS', + HealthWorkoutActivityType.MIXED_CARDIO: 'MIXED_CARDIO', HealthWorkoutActivityType.PADDLE_SPORTS: 'PADDLE_SPORTS', - HealthWorkoutActivityType.SURFING_SPORTS: 'SURFING_SPORTS', - HealthWorkoutActivityType.WATER_FITNESS: 'WATER_FITNESS', - HealthWorkoutActivityType.WATER_SPORTS: 'WATER_SPORTS', - HealthWorkoutActivityType.TAI_CHI: 'TAI_CHI', - HealthWorkoutActivityType.WRESTLING: 'WRESTLING', - HealthWorkoutActivityType.AEROBICS: 'AEROBICS', - HealthWorkoutActivityType.BIATHLON: 'BIATHLON', - HealthWorkoutActivityType.BIKING_HAND: 'BIKING_HAND', - HealthWorkoutActivityType.BIKING_MOUNTAIN: 'BIKING_MOUNTAIN', - HealthWorkoutActivityType.BIKING_ROAD: 'BIKING_ROAD', - HealthWorkoutActivityType.BIKING_SPINNING: 'BIKING_SPINNING', - HealthWorkoutActivityType.BIKING_STATIONARY: 'BIKING_STATIONARY', - HealthWorkoutActivityType.BIKING_UTILITY: 'BIKING_UTILITY', - HealthWorkoutActivityType.CALISTHENICS: 'CALISTHENICS', - HealthWorkoutActivityType.CIRCUIT_TRAINING: 'CIRCUIT_TRAINING', - HealthWorkoutActivityType.CROSS_FIT: 'CROSS_FIT', - HealthWorkoutActivityType.DANCING: 'DANCING', - HealthWorkoutActivityType.DIVING: 'DIVING', - HealthWorkoutActivityType.ELEVATOR: 'ELEVATOR', - HealthWorkoutActivityType.ERGOMETER: 'ERGOMETER', - HealthWorkoutActivityType.ESCALATOR: 'ESCALATOR', - HealthWorkoutActivityType.FRISBEE_DISC: 'FRISBEE_DISC', - HealthWorkoutActivityType.GARDENING: 'GARDENING', - HealthWorkoutActivityType.GUIDED_BREATHING: 'GUIDED_BREATHING', - HealthWorkoutActivityType.HORSEBACK_RIDING: 'HORSEBACK_RIDING', - HealthWorkoutActivityType.HOUSEWORK: 'HOUSEWORK', - HealthWorkoutActivityType.INTERVAL_TRAINING: 'INTERVAL_TRAINING', - HealthWorkoutActivityType.IN_VEHICLE: 'IN_VEHICLE', - HealthWorkoutActivityType.ICE_SKATING: 'ICE_SKATING', - HealthWorkoutActivityType.KAYAKING: 'KAYAKING', - HealthWorkoutActivityType.KETTLEBELL_TRAINING: 'KETTLEBELL_TRAINING', - HealthWorkoutActivityType.KICK_SCOOTER: 'KICK_SCOOTER', - HealthWorkoutActivityType.KITE_SURFING: 'KITE_SURFING', - HealthWorkoutActivityType.MEDITATION: 'MEDITATION', - HealthWorkoutActivityType.MIXED_MARTIAL_ARTS: 'MIXED_MARTIAL_ARTS', - HealthWorkoutActivityType.P90X: 'P90X', HealthWorkoutActivityType.PARAGLIDING: 'PARAGLIDING', - HealthWorkoutActivityType.POLO: 'POLO', + HealthWorkoutActivityType.PICKLEBALL: 'PICKLEBALL', + HealthWorkoutActivityType.PILATES: 'PILATES', + HealthWorkoutActivityType.PLAY: 'PLAY', + HealthWorkoutActivityType.PREPARATION_AND_RECOVERY: + 'PREPARATION_AND_RECOVERY', + HealthWorkoutActivityType.RACQUETBALL: 'RACQUETBALL', HealthWorkoutActivityType.ROCK_CLIMBING: 'ROCK_CLIMBING', HealthWorkoutActivityType.ROWING_MACHINE: 'ROWING_MACHINE', + HealthWorkoutActivityType.ROWING: 'ROWING', + HealthWorkoutActivityType.RUGBY: 'RUGBY', HealthWorkoutActivityType.RUNNING_JOGGING: 'RUNNING_JOGGING', HealthWorkoutActivityType.RUNNING_SAND: 'RUNNING_SAND', HealthWorkoutActivityType.RUNNING_TREADMILL: 'RUNNING_TREADMILL', + HealthWorkoutActivityType.RUNNING: 'RUNNING', + HealthWorkoutActivityType.SAILING: 'SAILING', HealthWorkoutActivityType.SCUBA_DIVING: 'SCUBA_DIVING', HealthWorkoutActivityType.SKATING_CROSS: 'SKATING_CROSS', HealthWorkoutActivityType.SKATING_INDOOR: 'SKATING_INDOOR', HealthWorkoutActivityType.SKATING_INLINE: 'SKATING_INLINE', - HealthWorkoutActivityType.SKIING: 'SKIING', + HealthWorkoutActivityType.SKATING: 'SKATING', HealthWorkoutActivityType.SKIING_BACK_COUNTRY: 'SKIING_BACK_COUNTRY', HealthWorkoutActivityType.SKIING_KITE: 'SKIING_KITE', HealthWorkoutActivityType.SKIING_ROLLER: 'SKIING_ROLLER', - HealthWorkoutActivityType.SLEDDING: 'SLEDDING', + HealthWorkoutActivityType.SKIING: 'SKIING', + HealthWorkoutActivityType.SNOW_SPORTS: 'SNOW_SPORTS', + HealthWorkoutActivityType.SNOWBOARDING: 'SNOWBOARDING', HealthWorkoutActivityType.SNOWMOBILE: 'SNOWMOBILE', HealthWorkoutActivityType.SNOWSHOEING: 'SNOWSHOEING', + HealthWorkoutActivityType.SOCCER: 'SOCCER', + HealthWorkoutActivityType.SOCIAL_DANCE: 'SOCIAL_DANCE', + HealthWorkoutActivityType.SOFTBALL: 'SOFTBALL', + HealthWorkoutActivityType.SQUASH: 'SQUASH', HealthWorkoutActivityType.STAIR_CLIMBING_MACHINE: 'STAIR_CLIMBING_MACHINE', + HealthWorkoutActivityType.STAIR_CLIMBING: 'STAIR_CLIMBING', + HealthWorkoutActivityType.STAIRS: 'STAIRS', HealthWorkoutActivityType.STANDUP_PADDLEBOARDING: 'STANDUP_PADDLEBOARDING', - HealthWorkoutActivityType.STILL: 'STILL', + HealthWorkoutActivityType.STEP_TRAINING: 'STEP_TRAINING', HealthWorkoutActivityType.STRENGTH_TRAINING: 'STRENGTH_TRAINING', + HealthWorkoutActivityType.SURFING_SPORTS: 'SURFING_SPORTS', HealthWorkoutActivityType.SURFING: 'SURFING', HealthWorkoutActivityType.SWIMMING_OPEN_WATER: 'SWIMMING_OPEN_WATER', HealthWorkoutActivityType.SWIMMING_POOL: 'SWIMMING_POOL', - HealthWorkoutActivityType.TEAM_SPORTS: 'TEAM_SPORTS', + HealthWorkoutActivityType.SWIMMING: 'SWIMMING', + HealthWorkoutActivityType.TABLE_TENNIS: 'TABLE_TENNIS', + HealthWorkoutActivityType.TAI_CHI: 'TAI_CHI', + HealthWorkoutActivityType.TENNIS: 'TENNIS', HealthWorkoutActivityType.TILTING: 'TILTING', + HealthWorkoutActivityType.TRACK_AND_FIELD: 'TRACK_AND_FIELD', + HealthWorkoutActivityType.TRADITIONAL_STRENGTH_TRAINING: + 'TRADITIONAL_STRENGTH_TRAINING', HealthWorkoutActivityType.VOLLEYBALL_BEACH: 'VOLLEYBALL_BEACH', HealthWorkoutActivityType.VOLLEYBALL_INDOOR: 'VOLLEYBALL_INDOOR', + HealthWorkoutActivityType.VOLLEYBALL: 'VOLLEYBALL', HealthWorkoutActivityType.WAKEBOARDING: 'WAKEBOARDING', - HealthWorkoutActivityType.WALKING_FITNESS: 'WALKING_FITNESS', - HealthWorkoutActivityType.WALKING_NORDIC: 'WALKING_NORDIC', - HealthWorkoutActivityType.WALKING_STROLLER: 'WALKING_STROLLER', - HealthWorkoutActivityType.WALKING_TREADMILL: 'WALKING_TREADMILL', + HealthWorkoutActivityType.WALKING: 'WALKING', + HealthWorkoutActivityType.WATER_FITNESS: 'WATER_FITNESS', + HealthWorkoutActivityType.WATER_POLO: 'WATER_POLO', + HealthWorkoutActivityType.WATER_SPORTS: 'WATER_SPORTS', HealthWorkoutActivityType.WEIGHTLIFTING: 'WEIGHTLIFTING', + HealthWorkoutActivityType.WHEELCHAIR_RUN_PACE: 'WHEELCHAIR_RUN_PACE', + HealthWorkoutActivityType.WHEELCHAIR_WALK_PACE: 'WHEELCHAIR_WALK_PACE', HealthWorkoutActivityType.WHEELCHAIR: 'WHEELCHAIR', - HealthWorkoutActivityType.WINDSURFING: 'WINDSURFING', - HealthWorkoutActivityType.ZUMBA: 'ZUMBA', + HealthWorkoutActivityType.WRESTLING: 'WRESTLING', + HealthWorkoutActivityType.YOGA: 'YOGA', HealthWorkoutActivityType.OTHER: 'OTHER', }; diff --git a/packages/health/lib/health.json.dart b/packages/health/lib/health.json.dart index 72a43924b..351098a28 100644 --- a/packages/health/lib/health.json.dart +++ b/packages/health/lib/health.json.dart @@ -15,7 +15,6 @@ void _registerFromJsonFunctions() { leftEarSensitivities: [], rightEarSensitivities: [], ), - WorkoutHealthValue(workoutActivityType: HealthWorkoutActivityType.AEROBICS), ElectrocardiogramHealthValue(voltageValues: []), ElectrocardiogramVoltageValue(voltage: 12, timeSinceSampleStart: 0), NutritionHealthValue(), diff --git a/packages/health/lib/src/health_plugin.dart b/packages/health/lib/src/health_plugin.dart index 6fe05ef05..6a6586031 100644 --- a/packages/health/lib/src/health_plugin.dart +++ b/packages/health/lib/src/health_plugin.dart @@ -1106,84 +1106,84 @@ class Health { bool _isOnIOS(HealthWorkoutActivityType type) { // Returns true if the type is part of the iOS set return { + HealthWorkoutActivityType.AMERICAN_FOOTBALL, HealthWorkoutActivityType.ARCHERY, + HealthWorkoutActivityType.AUSTRALIAN_FOOTBALL, HealthWorkoutActivityType.BADMINTON, + HealthWorkoutActivityType.BARRE, HealthWorkoutActivityType.BASEBALL, HealthWorkoutActivityType.BASKETBALL, HealthWorkoutActivityType.BIKING, + HealthWorkoutActivityType.BOWLING, HealthWorkoutActivityType.BOXING, + HealthWorkoutActivityType.CARDIO_DANCE, + HealthWorkoutActivityType.CLIMBING, + HealthWorkoutActivityType.COOLDOWN, + HealthWorkoutActivityType.CORE_TRAINING, HealthWorkoutActivityType.CRICKET, + HealthWorkoutActivityType.CROSS_COUNTRY_SKIING, + HealthWorkoutActivityType.CROSS_TRAINING, HealthWorkoutActivityType.CURLING, + HealthWorkoutActivityType.DISC_SPORTS, + HealthWorkoutActivityType.DOWNHILL_SKIING, HealthWorkoutActivityType.ELLIPTICAL, + HealthWorkoutActivityType.EQUESTRIAN_SPORTS, HealthWorkoutActivityType.FENCING, - HealthWorkoutActivityType.AMERICAN_FOOTBALL, - HealthWorkoutActivityType.AUSTRALIAN_FOOTBALL, - HealthWorkoutActivityType.SOCCER, + HealthWorkoutActivityType.FISHING, + HealthWorkoutActivityType.FITNESS_GAMING, + HealthWorkoutActivityType.FLEXIBILITY, + HealthWorkoutActivityType.FUNCTIONAL_STRENGTH_TRAINING, HealthWorkoutActivityType.GOLF, HealthWorkoutActivityType.GYMNASTICS, + HealthWorkoutActivityType.HAND_CYCLING, HealthWorkoutActivityType.HANDBALL, HealthWorkoutActivityType.HIGH_INTENSITY_INTERVAL_TRAINING, HealthWorkoutActivityType.HIKING, HealthWorkoutActivityType.HOCKEY, - HealthWorkoutActivityType.SKATING, + HealthWorkoutActivityType.HUNTING, HealthWorkoutActivityType.JUMP_ROPE, HealthWorkoutActivityType.KICKBOXING, + HealthWorkoutActivityType.LACROSSE, HealthWorkoutActivityType.MARTIAL_ARTS, + HealthWorkoutActivityType.MIND_AND_BODY, + HealthWorkoutActivityType.MIXED_CARDIO, + HealthWorkoutActivityType.OTHER, + HealthWorkoutActivityType.PADDLE_SPORTS, + HealthWorkoutActivityType.PICKLEBALL, HealthWorkoutActivityType.PILATES, + HealthWorkoutActivityType.PLAY, + HealthWorkoutActivityType.PREPARATION_AND_RECOVERY, HealthWorkoutActivityType.RACQUETBALL, HealthWorkoutActivityType.ROWING, HealthWorkoutActivityType.RUGBY, HealthWorkoutActivityType.RUNNING, HealthWorkoutActivityType.SAILING, - HealthWorkoutActivityType.CROSS_COUNTRY_SKIING, - HealthWorkoutActivityType.DOWNHILL_SKIING, + HealthWorkoutActivityType.SKATING, + HealthWorkoutActivityType.SNOW_SPORTS, HealthWorkoutActivityType.SNOWBOARDING, + HealthWorkoutActivityType.SOCCER, + HealthWorkoutActivityType.SOCIAL_DANCE, HealthWorkoutActivityType.SOFTBALL, HealthWorkoutActivityType.SQUASH, HealthWorkoutActivityType.STAIR_CLIMBING, + HealthWorkoutActivityType.STAIRS, + HealthWorkoutActivityType.STEP_TRAINING, + HealthWorkoutActivityType.SURFING_SPORTS, HealthWorkoutActivityType.SWIMMING, HealthWorkoutActivityType.TABLE_TENNIS, + HealthWorkoutActivityType.TAI_CHI, HealthWorkoutActivityType.TENNIS, - HealthWorkoutActivityType.VOLLEYBALL, - HealthWorkoutActivityType.WALKING, - HealthWorkoutActivityType.WATER_POLO, - HealthWorkoutActivityType.YOGA, - HealthWorkoutActivityType.BOWLING, - HealthWorkoutActivityType.CROSS_TRAINING, HealthWorkoutActivityType.TRACK_AND_FIELD, - HealthWorkoutActivityType.DISC_SPORTS, - HealthWorkoutActivityType.LACROSSE, - HealthWorkoutActivityType.PREPARATION_AND_RECOVERY, - HealthWorkoutActivityType.FLEXIBILITY, - HealthWorkoutActivityType.COOLDOWN, - HealthWorkoutActivityType.WHEELCHAIR_WALK_PACE, - HealthWorkoutActivityType.WHEELCHAIR_RUN_PACE, - HealthWorkoutActivityType.HAND_CYCLING, - HealthWorkoutActivityType.CORE_TRAINING, - HealthWorkoutActivityType.FUNCTIONAL_STRENGTH_TRAINING, HealthWorkoutActivityType.TRADITIONAL_STRENGTH_TRAINING, - HealthWorkoutActivityType.MIXED_CARDIO, - HealthWorkoutActivityType.STAIRS, - HealthWorkoutActivityType.STEP_TRAINING, - HealthWorkoutActivityType.FITNESS_GAMING, - HealthWorkoutActivityType.BARRE, - HealthWorkoutActivityType.CARDIO_DANCE, - HealthWorkoutActivityType.SOCIAL_DANCE, - HealthWorkoutActivityType.MIND_AND_BODY, - HealthWorkoutActivityType.PICKLEBALL, - HealthWorkoutActivityType.CLIMBING, - HealthWorkoutActivityType.EQUESTRIAN_SPORTS, - HealthWorkoutActivityType.FISHING, - HealthWorkoutActivityType.HUNTING, - HealthWorkoutActivityType.PLAY, - HealthWorkoutActivityType.SNOW_SPORTS, - HealthWorkoutActivityType.PADDLE_SPORTS, - HealthWorkoutActivityType.SURFING_SPORTS, + HealthWorkoutActivityType.VOLLEYBALL, + HealthWorkoutActivityType.WALKING, HealthWorkoutActivityType.WATER_FITNESS, + HealthWorkoutActivityType.WATER_POLO, HealthWorkoutActivityType.WATER_SPORTS, - HealthWorkoutActivityType.TAI_CHI, + HealthWorkoutActivityType.WHEELCHAIR_RUN_PACE, + HealthWorkoutActivityType.WHEELCHAIR_WALK_PACE, HealthWorkoutActivityType.WRESTLING, - HealthWorkoutActivityType.OTHER, + HealthWorkoutActivityType.YOGA, }.contains(type); } @@ -1192,26 +1192,26 @@ class Health { // Returns true if the type is part of the Android set return { // Both + HealthWorkoutActivityType.AMERICAN_FOOTBALL, HealthWorkoutActivityType.ARCHERY, + HealthWorkoutActivityType.AUSTRALIAN_FOOTBALL, HealthWorkoutActivityType.BADMINTON, HealthWorkoutActivityType.BASEBALL, HealthWorkoutActivityType.BASKETBALL, HealthWorkoutActivityType.BIKING, HealthWorkoutActivityType.BOXING, HealthWorkoutActivityType.CRICKET, + HealthWorkoutActivityType.CROSS_COUNTRY_SKIING, HealthWorkoutActivityType.CURLING, + HealthWorkoutActivityType.DOWNHILL_SKIING, HealthWorkoutActivityType.ELLIPTICAL, HealthWorkoutActivityType.FENCING, - HealthWorkoutActivityType.AMERICAN_FOOTBALL, - HealthWorkoutActivityType.AUSTRALIAN_FOOTBALL, - HealthWorkoutActivityType.SOCCER, HealthWorkoutActivityType.GOLF, HealthWorkoutActivityType.GYMNASTICS, HealthWorkoutActivityType.HANDBALL, HealthWorkoutActivityType.HIGH_INTENSITY_INTERVAL_TRAINING, HealthWorkoutActivityType.HIKING, HealthWorkoutActivityType.HOCKEY, - HealthWorkoutActivityType.SKATING, HealthWorkoutActivityType.JUMP_ROPE, HealthWorkoutActivityType.KICKBOXING, HealthWorkoutActivityType.MARTIAL_ARTS, @@ -1221,9 +1221,9 @@ class Health { HealthWorkoutActivityType.RUGBY, HealthWorkoutActivityType.RUNNING, HealthWorkoutActivityType.SAILING, - HealthWorkoutActivityType.CROSS_COUNTRY_SKIING, - HealthWorkoutActivityType.DOWNHILL_SKIING, + HealthWorkoutActivityType.SKATING, HealthWorkoutActivityType.SNOWBOARDING, + HealthWorkoutActivityType.SOCCER, HealthWorkoutActivityType.SOFTBALL, HealthWorkoutActivityType.SQUASH, HealthWorkoutActivityType.STAIR_CLIMBING, @@ -1237,8 +1237,6 @@ class Health { // Android only // Once Google Fit is removed, this list needs to be changed - HealthWorkoutActivityType.AEROBICS, - HealthWorkoutActivityType.BIATHLON, HealthWorkoutActivityType.BIKING_HAND, HealthWorkoutActivityType.BIKING_MOUNTAIN, HealthWorkoutActivityType.BIKING_ROAD, @@ -1246,30 +1244,11 @@ class Health { HealthWorkoutActivityType.BIKING_STATIONARY, HealthWorkoutActivityType.BIKING_UTILITY, HealthWorkoutActivityType.CALISTHENICS, - HealthWorkoutActivityType.CIRCUIT_TRAINING, - HealthWorkoutActivityType.CROSS_FIT, HealthWorkoutActivityType.DANCING, - HealthWorkoutActivityType.DIVING, - HealthWorkoutActivityType.ELEVATOR, - HealthWorkoutActivityType.ERGOMETER, - HealthWorkoutActivityType.ESCALATOR, HealthWorkoutActivityType.FRISBEE_DISC, - HealthWorkoutActivityType.GARDENING, HealthWorkoutActivityType.GUIDED_BREATHING, - HealthWorkoutActivityType.HORSEBACK_RIDING, - HealthWorkoutActivityType.HOUSEWORK, - HealthWorkoutActivityType.INTERVAL_TRAINING, - HealthWorkoutActivityType.IN_VEHICLE, HealthWorkoutActivityType.ICE_SKATING, - HealthWorkoutActivityType.KAYAKING, - HealthWorkoutActivityType.KETTLEBELL_TRAINING, - HealthWorkoutActivityType.KICK_SCOOTER, - HealthWorkoutActivityType.KITE_SURFING, - HealthWorkoutActivityType.MEDITATION, - HealthWorkoutActivityType.MIXED_MARTIAL_ARTS, - HealthWorkoutActivityType.P90X, HealthWorkoutActivityType.PARAGLIDING, - HealthWorkoutActivityType.POLO, HealthWorkoutActivityType.ROCK_CLIMBING, HealthWorkoutActivityType.ROWING_MACHINE, HealthWorkoutActivityType.RUNNING_JOGGING, @@ -1279,21 +1258,18 @@ class Health { HealthWorkoutActivityType.SKATING_CROSS, HealthWorkoutActivityType.SKATING_INDOOR, HealthWorkoutActivityType.SKATING_INLINE, - HealthWorkoutActivityType.SKIING, HealthWorkoutActivityType.SKIING_BACK_COUNTRY, HealthWorkoutActivityType.SKIING_KITE, HealthWorkoutActivityType.SKIING_ROLLER, - HealthWorkoutActivityType.SLEDDING, + HealthWorkoutActivityType.SKIING, HealthWorkoutActivityType.SNOWMOBILE, HealthWorkoutActivityType.SNOWSHOEING, HealthWorkoutActivityType.STAIR_CLIMBING_MACHINE, HealthWorkoutActivityType.STANDUP_PADDLEBOARDING, - HealthWorkoutActivityType.STILL, HealthWorkoutActivityType.STRENGTH_TRAINING, HealthWorkoutActivityType.SURFING, HealthWorkoutActivityType.SWIMMING_OPEN_WATER, HealthWorkoutActivityType.SWIMMING_POOL, - HealthWorkoutActivityType.TEAM_SPORTS, HealthWorkoutActivityType.TILTING, HealthWorkoutActivityType.VOLLEYBALL_BEACH, HealthWorkoutActivityType.VOLLEYBALL_INDOOR, @@ -1304,8 +1280,6 @@ class Health { HealthWorkoutActivityType.WALKING_TREADMILL, HealthWorkoutActivityType.WEIGHTLIFTING, HealthWorkoutActivityType.WHEELCHAIR, - HealthWorkoutActivityType.WINDSURFING, - HealthWorkoutActivityType.ZUMBA, HealthWorkoutActivityType.OTHER, }.contains(type); } diff --git a/packages/health/lib/src/heath_data_types.dart b/packages/health/lib/src/heath_data_types.dart index ba04a21e2..4864eb4e3 100644 --- a/packages/health/lib/src/heath_data_types.dart +++ b/packages/health/lib/src/heath_data_types.dart @@ -68,7 +68,6 @@ enum HealthDataType { DISTANCE_SWIMMING, DISTANCE_CYCLING, FLIGHTS_CLIMBED, - MOVE_MINUTES, DISTANCE_DELTA, MINDFULNESS, WATER, @@ -226,7 +225,6 @@ const List dataTypeKeysAndroid = [ HealthDataType.HEIGHT, HealthDataType.STEPS, HealthDataType.WEIGHT, - HealthDataType.MOVE_MINUTES, HealthDataType.DISTANCE_DELTA, HealthDataType.SLEEP_AWAKE, HealthDataType.SLEEP_ASLEEP, @@ -315,7 +313,6 @@ const Map dataTypeToUnit = { HealthDataType.DISTANCE_SWIMMING: HealthDataUnit.METER, HealthDataType.DISTANCE_CYCLING: HealthDataUnit.METER, HealthDataType.FLIGHTS_CLIMBED: HealthDataUnit.COUNT, - HealthDataType.MOVE_MINUTES: HealthDataUnit.MINUTE, HealthDataType.DISTANCE_DELTA: HealthDataUnit.METER, HealthDataType.WATER: HealthDataUnit.LITER, @@ -451,26 +448,26 @@ enum HealthWorkoutActivityType { // Commented for which platform the type are supported // Both + AMERICAN_FOOTBALL, ARCHERY, + AUSTRALIAN_FOOTBALL, BADMINTON, BASEBALL, BASKETBALL, BIKING, // This also entails the iOS version where it is called CYCLING BOXING, CRICKET, + CROSS_COUNTRY_SKIING, CURLING, + DOWNHILL_SKIING, ELLIPTICAL, FENCING, - AMERICAN_FOOTBALL, - AUSTRALIAN_FOOTBALL, - SOCCER, GOLF, GYMNASTICS, HANDBALL, HIGH_INTENSITY_INTERVAL_TRAINING, HIKING, HOCKEY, - SKATING, JUMP_ROPE, KICKBOXING, MARTIAL_ARTS, @@ -480,9 +477,9 @@ enum HealthWorkoutActivityType { RUGBY, RUNNING, SAILING, - CROSS_COUNTRY_SKIING, - DOWNHILL_SKIING, + SKATING, SNOWBOARDING, + SOCCER, SOFTBALL, SQUASH, STAIR_CLIMBING, @@ -495,45 +492,43 @@ enum HealthWorkoutActivityType { YOGA, // iOS only + BARRE, BOWLING, + CARDIO_DANCE, + CLIMBING, + COOLDOWN, + CORE_TRAINING, CROSS_TRAINING, - TRACK_AND_FIELD, DISC_SPORTS, - LACROSSE, - PREPARATION_AND_RECOVERY, + EQUESTRIAN_SPORTS, + FISHING, + FITNESS_GAMING, FLEXIBILITY, - COOLDOWN, - WHEELCHAIR_WALK_PACE, - WHEELCHAIR_RUN_PACE, - HAND_CYCLING, - CORE_TRAINING, FUNCTIONAL_STRENGTH_TRAINING, - TRADITIONAL_STRENGTH_TRAINING, - MIXED_CARDIO, - STAIRS, - STEP_TRAINING, - FITNESS_GAMING, - BARRE, - CARDIO_DANCE, - SOCIAL_DANCE, + HAND_CYCLING, + HUNTING, + LACROSSE, MIND_AND_BODY, + MIXED_CARDIO, + PADDLE_SPORTS, PICKLEBALL, - CLIMBING, - EQUESTRIAN_SPORTS, - FISHING, - HUNTING, PLAY, + PREPARATION_AND_RECOVERY, SNOW_SPORTS, - PADDLE_SPORTS, + SOCIAL_DANCE, + STAIRS, + STEP_TRAINING, SURFING_SPORTS, + TAI_CHI, + TRACK_AND_FIELD, + TRADITIONAL_STRENGTH_TRAINING, WATER_FITNESS, WATER_SPORTS, - TAI_CHI, + WHEELCHAIR_RUN_PACE, + WHEELCHAIR_WALK_PACE, WRESTLING, // Android only - AEROBICS, - BIATHLON, BIKING_HAND, BIKING_MOUNTAIN, BIKING_ROAD, @@ -541,30 +536,11 @@ enum HealthWorkoutActivityType { BIKING_STATIONARY, BIKING_UTILITY, CALISTHENICS, - CIRCUIT_TRAINING, - CROSS_FIT, DANCING, - DIVING, - ELEVATOR, - ERGOMETER, - ESCALATOR, FRISBEE_DISC, - GARDENING, GUIDED_BREATHING, - HORSEBACK_RIDING, - HOUSEWORK, - INTERVAL_TRAINING, - IN_VEHICLE, ICE_SKATING, - KAYAKING, - KETTLEBELL_TRAINING, - KICK_SCOOTER, - KITE_SURFING, - MEDITATION, - MIXED_MARTIAL_ARTS, - P90X, PARAGLIDING, - POLO, ROCK_CLIMBING, // on iOS this is the same as CLIMBING ROWING_MACHINE, RUNNING_JOGGING, // on iOS this is the same as RUNNING @@ -574,21 +550,18 @@ enum HealthWorkoutActivityType { SKATING_CROSS, // on iOS this is the same as SKATING SKATING_INDOOR, // on iOS this is the same as SKATING SKATING_INLINE, // on iOS this is the same as SKATING - SKIING, SKIING_BACK_COUNTRY, SKIING_KITE, SKIING_ROLLER, - SLEDDING, + SKIING, SNOWMOBILE, SNOWSHOEING, STAIR_CLIMBING_MACHINE, STANDUP_PADDLEBOARDING, - STILL, STRENGTH_TRAINING, SURFING, SWIMMING_OPEN_WATER, SWIMMING_POOL, - TEAM_SPORTS, TILTING, VOLLEYBALL_BEACH, VOLLEYBALL_INDOOR, @@ -599,8 +572,6 @@ enum HealthWorkoutActivityType { WALKING_TREADMILL, WEIGHTLIFTING, WHEELCHAIR, - WINDSURFING, - ZUMBA, // OTHER, From 2acaaf6c8bdfccb79d2834480fb0960d07b62bce Mon Sep 17 00:00:00 2001 From: Aamir Farooq Date: Tue, 6 Aug 2024 15:35:25 +0200 Subject: [PATCH 05/32] Remove more Google Fit workout types --- packages/health/README.md | 3 - .../cachet/plugins/health/HealthPlugin.kt | 75 ++++++------------- packages/health/lib/health.g.dart | 4 - packages/health/lib/src/health_plugin.dart | 7 -- packages/health/lib/src/heath_data_types.dart | 4 - 5 files changed, 22 insertions(+), 71 deletions(-) diff --git a/packages/health/README.md b/packages/health/README.md index d2294f901..6a5a7dce1 100644 --- a/packages/health/README.md +++ b/packages/health/README.md @@ -447,7 +447,6 @@ The plugin supports the following [`HealthWorkoutActivityType`](https://pub.dev/ | STAIR_CLIMBING | yes | yes | | | STAIR_CLIMBING_MACHINE | | yes | | | STAIRS | yes | | | -| STANDUP_PADDLEBOARDING | | | | | STEP_TRAINING | yes | | | | STRENGTH_TRAINING | | yes | | | SURFING | | yes | | @@ -458,14 +457,12 @@ The plugin supports the following [`HealthWorkoutActivityType`](https://pub.dev/ | TABLE_TENNIS | yes | yes | | | TAI_CHI | yes | | | | TENNIS | yes | yes | | -| TILTING | | | | | TRACK_AND_FIELD | yes | | | | TRADITIONAL_STRENGTH_TRAINING | yes | | | | TREADMILL | | | | | VOLLEYBALL | yes | yes | | | VOLLEYBALL_BEACH | | | | | VOLLEYBALL_INDOOR | | | | -| WAKEBOARDING | | | | | WALKING | yes | yes | | | WATER_FITNESS | yes | | | | WATER_POLO | yes | yes | | 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 5eafc3c13..35a2e742a 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 @@ -2182,7 +2182,9 @@ class HealthPlugin(private var channel: MethodChannel? = null) : // TODO: add skating // TODO: add soccer // TOOD: look into paddling - // TODO: add runnning_treadmill + // TODO: add runnning + // TODO: look into hockey + // TODO: look into volleyball "AMERICAN_FOOTBALL" to ExerciseSessionRecord .EXERCISE_TYPE_FOOTBALL_AMERICAN, @@ -2198,29 +2200,20 @@ class HealthPlugin(private var channel: MethodChannel? = null) : ExerciseSessionRecord .EXERCISE_TYPE_BASKETBALL, "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, + // "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, "CRICKET" to ExerciseSessionRecord.EXERCISE_TYPE_CRICKET, - // "CROSS_COUNTRY_SKIING" to - // ExerciseSessionRecord.EXERCISE_TYPE_SKIING_CROSS_COUNTRY, - // "CURLING" to ExerciseSessionRecord.EXERCISE_TYPE_CURLING, + // "CROSS_COUNTRY_SKIING" to ExerciseSessionRecord.EXERCISE_TYPE_SKIING_CROSS_COUNTRY, "DANCING" to ExerciseSessionRecord.EXERCISE_TYPE_DANCING, - // "DOWNHILL_SKIING" to - // ExerciseSessionRecord.EXERCISE_TYPE_SKIING_DOWNHILL, + // "DOWNHILL_SKIING" to ExerciseSessionRecord.EXERCISE_TYPE_SKIING_DOWNHILL, "ELLIPTICAL" to ExerciseSessionRecord .EXERCISE_TYPE_ELLIPTICAL, @@ -2244,10 +2237,6 @@ class HealthPlugin(private var channel: MethodChannel? = null) : "ICE_SKATING" to ExerciseSessionRecord .EXERCISE_TYPE_ICE_SKATING, - // "JUMP_ROPE" to - // ExerciseSessionRecord.EXERCISE_TYPE_JUMP_ROPE, - // "KICKBOXING" to - // ExerciseSessionRecord.EXERCISE_TYPE_KICKBOXING, "MARTIAL_ARTS" to ExerciseSessionRecord .EXERCISE_TYPE_MARTIAL_ARTS, @@ -2266,10 +2255,8 @@ class HealthPlugin(private var channel: MethodChannel? = null) : 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_JOGGING" to ExerciseSessionRecord.EXERCISE_TYPE_RUNNING_JOGGING, + // "RUNNING_SAND" to ExerciseSessionRecord.EXERCISE_TYPE_RUNNING_SAND, "RUNNING_TREADMILL" to ExerciseSessionRecord .EXERCISE_TYPE_RUNNING_TREADMILL, @@ -2278,30 +2265,21 @@ class HealthPlugin(private var channel: MethodChannel? = null) : "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_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, + // "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, "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, + // "SOCCER" to ExerciseSessionRecord.EXERCISE_TYPE_FOOTBALL_SOCCER, "SOFTBALL" to ExerciseSessionRecord.EXERCISE_TYPE_SOFTBALL, "SQUASH" to ExerciseSessionRecord.EXERCISE_TYPE_SQUASH, "STAIR_CLIMBING_MACHINE" to @@ -2310,8 +2288,6 @@ class HealthPlugin(private var channel: MethodChannel? = null) : "STAIR_CLIMBING" to ExerciseSessionRecord .EXERCISE_TYPE_STAIR_CLIMBING, - // "STANDUP_PADDLEBOARDING" to - // ExerciseSessionRecord.EXERCISE_TYPE_STANDUP_PADDLEBOARDING, "STRENGTH_TRAINING" to ExerciseSessionRecord .EXERCISE_TYPE_STRENGTH_TRAINING, @@ -2322,22 +2298,15 @@ class HealthPlugin(private var channel: MethodChannel? = null) : "SWIMMING_POOL" to ExerciseSessionRecord .EXERCISE_TYPE_SWIMMING_POOL, - // "SWIMMING" to - // ExerciseSessionRecord.EXERCISE_TYPE_SWIMMING, "TABLE_TENNIS" to ExerciseSessionRecord .EXERCISE_TYPE_TABLE_TENNIS, "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_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" to ExerciseSessionRecord.EXERCISE_TYPE_WALKING, "WATER_POLO" to ExerciseSessionRecord diff --git a/packages/health/lib/health.g.dart b/packages/health/lib/health.g.dart index d043cd59e..49d91c7c6 100644 --- a/packages/health/lib/health.g.dart +++ b/packages/health/lib/health.g.dart @@ -397,7 +397,6 @@ const _$HealthWorkoutActivityTypeEnumMap = { HealthWorkoutActivityType.SKIING: 'SKIING', HealthWorkoutActivityType.SNOW_SPORTS: 'SNOW_SPORTS', HealthWorkoutActivityType.SNOWBOARDING: 'SNOWBOARDING', - HealthWorkoutActivityType.SNOWMOBILE: 'SNOWMOBILE', HealthWorkoutActivityType.SNOWSHOEING: 'SNOWSHOEING', HealthWorkoutActivityType.SOCCER: 'SOCCER', HealthWorkoutActivityType.SOCIAL_DANCE: 'SOCIAL_DANCE', @@ -406,7 +405,6 @@ const _$HealthWorkoutActivityTypeEnumMap = { HealthWorkoutActivityType.STAIR_CLIMBING_MACHINE: 'STAIR_CLIMBING_MACHINE', HealthWorkoutActivityType.STAIR_CLIMBING: 'STAIR_CLIMBING', HealthWorkoutActivityType.STAIRS: 'STAIRS', - HealthWorkoutActivityType.STANDUP_PADDLEBOARDING: 'STANDUP_PADDLEBOARDING', HealthWorkoutActivityType.STEP_TRAINING: 'STEP_TRAINING', HealthWorkoutActivityType.STRENGTH_TRAINING: 'STRENGTH_TRAINING', HealthWorkoutActivityType.SURFING_SPORTS: 'SURFING_SPORTS', @@ -417,14 +415,12 @@ const _$HealthWorkoutActivityTypeEnumMap = { HealthWorkoutActivityType.TABLE_TENNIS: 'TABLE_TENNIS', HealthWorkoutActivityType.TAI_CHI: 'TAI_CHI', HealthWorkoutActivityType.TENNIS: 'TENNIS', - HealthWorkoutActivityType.TILTING: 'TILTING', HealthWorkoutActivityType.TRACK_AND_FIELD: 'TRACK_AND_FIELD', HealthWorkoutActivityType.TRADITIONAL_STRENGTH_TRAINING: 'TRADITIONAL_STRENGTH_TRAINING', HealthWorkoutActivityType.VOLLEYBALL_BEACH: 'VOLLEYBALL_BEACH', HealthWorkoutActivityType.VOLLEYBALL_INDOOR: 'VOLLEYBALL_INDOOR', HealthWorkoutActivityType.VOLLEYBALL: 'VOLLEYBALL', - HealthWorkoutActivityType.WAKEBOARDING: 'WAKEBOARDING', HealthWorkoutActivityType.WALKING: 'WALKING', HealthWorkoutActivityType.WATER_FITNESS: 'WATER_FITNESS', HealthWorkoutActivityType.WATER_POLO: 'WATER_POLO', diff --git a/packages/health/lib/src/health_plugin.dart b/packages/health/lib/src/health_plugin.dart index 6a6586031..6b9673332 100644 --- a/packages/health/lib/src/health_plugin.dart +++ b/packages/health/lib/src/health_plugin.dart @@ -1212,8 +1212,6 @@ class Health { HealthWorkoutActivityType.HIGH_INTENSITY_INTERVAL_TRAINING, HealthWorkoutActivityType.HIKING, HealthWorkoutActivityType.HOCKEY, - HealthWorkoutActivityType.JUMP_ROPE, - HealthWorkoutActivityType.KICKBOXING, HealthWorkoutActivityType.MARTIAL_ARTS, HealthWorkoutActivityType.PILATES, HealthWorkoutActivityType.RACQUETBALL, @@ -1227,7 +1225,6 @@ class Health { HealthWorkoutActivityType.SOFTBALL, HealthWorkoutActivityType.SQUASH, HealthWorkoutActivityType.STAIR_CLIMBING, - HealthWorkoutActivityType.SWIMMING, HealthWorkoutActivityType.TABLE_TENNIS, HealthWorkoutActivityType.TENNIS, HealthWorkoutActivityType.VOLLEYBALL, @@ -1262,18 +1259,14 @@ class Health { HealthWorkoutActivityType.SKIING_KITE, HealthWorkoutActivityType.SKIING_ROLLER, HealthWorkoutActivityType.SKIING, - HealthWorkoutActivityType.SNOWMOBILE, HealthWorkoutActivityType.SNOWSHOEING, HealthWorkoutActivityType.STAIR_CLIMBING_MACHINE, - HealthWorkoutActivityType.STANDUP_PADDLEBOARDING, HealthWorkoutActivityType.STRENGTH_TRAINING, HealthWorkoutActivityType.SURFING, HealthWorkoutActivityType.SWIMMING_OPEN_WATER, HealthWorkoutActivityType.SWIMMING_POOL, - HealthWorkoutActivityType.TILTING, HealthWorkoutActivityType.VOLLEYBALL_BEACH, HealthWorkoutActivityType.VOLLEYBALL_INDOOR, - HealthWorkoutActivityType.WAKEBOARDING, HealthWorkoutActivityType.WALKING_FITNESS, HealthWorkoutActivityType.WALKING_NORDIC, HealthWorkoutActivityType.WALKING_STROLLER, diff --git a/packages/health/lib/src/heath_data_types.dart b/packages/health/lib/src/heath_data_types.dart index 4864eb4e3..b1554dd2f 100644 --- a/packages/health/lib/src/heath_data_types.dart +++ b/packages/health/lib/src/heath_data_types.dart @@ -554,18 +554,14 @@ enum HealthWorkoutActivityType { SKIING_KITE, SKIING_ROLLER, SKIING, - SNOWMOBILE, SNOWSHOEING, STAIR_CLIMBING_MACHINE, - STANDUP_PADDLEBOARDING, STRENGTH_TRAINING, SURFING, SWIMMING_OPEN_WATER, SWIMMING_POOL, - TILTING, VOLLEYBALL_BEACH, VOLLEYBALL_INDOOR, - WAKEBOARDING, WALKING_FITNESS, WALKING_NORDIC, WALKING_STROLLER, From 58a6ae7c7101a1ec796f83fdc7ee69dba6d55efd Mon Sep 17 00:00:00 2001 From: Aamir Farooq Date: Wed, 7 Aug 2024 10:36:29 +0200 Subject: [PATCH 06/32] Remove references to Google Fit, remove `useHealthConnectIfAvailable` --- packages/health/README.md | 4 +-- .../cachet/plugins/health/HealthPlugin.kt | 7 ---- packages/health/example/lib/main.dart | 6 ++-- packages/health/example/lib/util.dart | 2 +- packages/health/ios/health.podspec | 4 +-- packages/health/lib/health.g.dart | 1 - .../health/lib/src/health_data_point.dart | 4 +-- packages/health/lib/src/health_plugin.dart | 33 +++++-------------- .../health/lib/src/health_value_types.dart | 2 +- packages/health/pubspec.yaml | 2 +- 10 files changed, 19 insertions(+), 46 deletions(-) diff --git a/packages/health/README.md b/packages/health/README.md index 6a5a7dce1..63c36dab6 100644 --- a/packages/health/README.md +++ b/packages/health/README.md @@ -190,7 +190,7 @@ Below is a simplified flow of how to use the plugin. ```dart // configure the health plugin before use. - Health().configure(useHealthConnectIfAvailable: true); + Health().configure(); // define the types to get @@ -389,7 +389,7 @@ The plugin supports the following [`HealthWorkoutActivityType`](https://pub.dev/ | CROSS_COUNTRY_SKIING | yes | | | | CROSS_TRAINING | yes | | | | CURLING | yes | | | -| DANCING | | yes | | +| DANCING | yes | yes | on iOS this is DANCE, but name changed here to fit with Android | | DISC_SPORTS | yes | | | | DOWNHILL_SKIING | yes | | | | ELLIPTICAL | yes | yes | | 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 35a2e742a..80f4723cd 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 @@ -88,7 +88,6 @@ class HealthPlugin(private var channel: MethodChannel? = 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 @@ -378,7 +377,6 @@ class HealthPlugin(private var channel: MethodChannel? = null) : 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) @@ -454,11 +452,6 @@ class HealthPlugin(private var channel: MethodChannel? = null) : result.success(null) } - private fun useHealthConnectIfAvailable(call: MethodCall, result: Result) { - useHealthConnectIfAvailable = true - result.success(null) - } - private fun getHealthConnectSdkStatus(call: MethodCall, result: Result) { checkAvailability() if (healthConnectAvailable) { diff --git a/packages/health/example/lib/main.dart b/packages/health/example/lib/main.dart index 59eb830c9..56a70c320 100644 --- a/packages/health/example/lib/main.dart +++ b/packages/health/example/lib/main.dart @@ -81,7 +81,7 @@ class _HealthAppState extends State { void initState() { // configure the health plugin before use. - Health().configure(useHealthConnectIfAvailable: true); + Health().configure(); super.initState(); } @@ -185,7 +185,7 @@ class _HealthAppState extends State { // Add data for supported types // NOTE: These are only the ones supported on Androids new API Health Connect. - // Both Android's Google Fit and iOS' HealthKit have more types that we support in the enum list [HealthDataType] + // Both Android's Health Connect and iOS' HealthKit have more types that we support in the enum list [HealthDataType] // Add more - like AUDIOGRAM, HEADACHE_SEVERE etc. to try them. bool success = true; @@ -591,8 +591,6 @@ class _HealthAppState extends State { Widget _authorizationNotGranted = const Column( children: [ const Text('Authorization not given.'), - const Text( - 'For Google Fit please check your OAUTH2 client ID is correct in Google Developer Console.'), const Text( 'For Google Health Connect please check if you have added the right permissions and services to the manifest file.'), const Text('For Apple Health check your permissions in Apple Health.'), diff --git a/packages/health/example/lib/util.dart b/packages/health/example/lib/util.dart index d2b4028ba..e135a30f6 100644 --- a/packages/health/example/lib/util.dart +++ b/packages/health/example/lib/util.dart @@ -62,7 +62,7 @@ const List dataTypesIOS = [ /// List of data types available on Android. /// /// Note that these are only the ones supported on Android's Health Connect API. -/// Android's Google Fit have more types that we support in the [HealthDataType] +/// Android's Health Connect has more types that we support in the [HealthDataType] /// enumeration. const List dataTypesAndroid = [ HealthDataType.ACTIVE_ENERGY_BURNED, diff --git a/packages/health/ios/health.podspec b/packages/health/ios/health.podspec index 443b44c7c..3a8d89370 100644 --- a/packages/health/ios/health.podspec +++ b/packages/health/ios/health.podspec @@ -4,9 +4,9 @@ Pod::Spec.new do |s| s.name = 'health' s.version = '1.0.4' - s.summary = 'Wrapper for the iOS HealthKit and Android GoogleFit services.' + s.summary = 'Wrapper for Apple\'s HealthKit on iOS and Google\'s Health Connect on Android.' s.description = <<-DESC -Wrapper for the iOS HealthKit and Android GoogleFit services. +Wrapper for Apple's HealthKit on iOS and Google's Health Connect on Android. DESC s.homepage = 'https://pub.dev/packages/health' s.license = { :file => '../LICENSE' } diff --git a/packages/health/lib/health.g.dart b/packages/health/lib/health.g.dart index 49d91c7c6..6a365a135 100644 --- a/packages/health/lib/health.g.dart +++ b/packages/health/lib/health.g.dart @@ -206,7 +206,6 @@ const _$HealthDataUnitEnumMap = { const _$HealthPlatformTypeEnumMap = { HealthPlatformType.appleHealth: 'appleHealth', - HealthPlatformType.googleFit: 'googleFit', HealthPlatformType.googleHealthConnect: 'googleHealthConnect', }; diff --git a/packages/health/lib/src/health_data_point.dart b/packages/health/lib/src/health_data_point.dart index 417991fb2..b98734c5a 100644 --- a/packages/health/lib/src/health_data_point.dart +++ b/packages/health/lib/src/health_data_point.dart @@ -1,10 +1,10 @@ part of '../health.dart'; /// Types of health platforms. -enum HealthPlatformType { appleHealth, googleFit, googleHealthConnect } +enum HealthPlatformType { appleHealth, googleHealthConnect } /// A [HealthDataPoint] object corresponds to a data point capture from -/// Apple HealthKit or Google Fit or Google Health Connect with a [HealthValue] +/// Apple HealthKit or Google Health Connect with a [HealthValue] /// as value. @JsonSerializable(fieldRename: FieldRename.snake, includeIfNull: false) class HealthDataPoint { diff --git a/packages/health/lib/src/health_plugin.dart b/packages/health/lib/src/health_plugin.dart index 6b9673332..cd3f60ebb 100644 --- a/packages/health/lib/src/health_plugin.dart +++ b/packages/health/lib/src/health_plugin.dart @@ -27,7 +27,6 @@ class Health { String? _deviceId; final _deviceInfo = DeviceInfoPlugin(); - bool _useHealthConnectIfAvailable = false; Health._() { _registerFromJsonFunctions(); @@ -39,9 +38,7 @@ class Health { /// The type of platform of this device. HealthPlatformType get platformType => Platform.isIOS ? HealthPlatformType.appleHealth - : useHealthConnectIfAvailable - ? HealthPlatformType.googleHealthConnect - : HealthPlatformType.googleFit; + : HealthPlatformType.googleHealthConnect; /// The id of this device. /// @@ -50,24 +47,12 @@ class Health { String get deviceId => _deviceId ?? 'unknown'; /// Configure the health plugin. Must be called before using the plugin. - /// - /// If [useHealthConnectIfAvailable] is true, Google Health Connect on - /// Android will be used. Has no effect on iOS. - Future configure({bool useHealthConnectIfAvailable = false}) async { - if (Platform.isAndroid) { - _deviceId = (await _deviceInfo.androidInfo).id; - _useHealthConnectIfAvailable = useHealthConnectIfAvailable; - await _channel.invokeMethod('useHealthConnectIfAvailable'); - } else { - _deviceId = (await _deviceInfo.iosInfo).identifierForVendor; - } + Future configure() async { + _deviceId = Platform.isAndroid + ? (await _deviceInfo.androidInfo).id + : (await _deviceInfo.iosInfo).identifierForVendor; } - /// Is this plugin using Health Connect (true) or Google Fit (false)? - /// - /// This is set in the [configure] method. - bool get useHealthConnectIfAvailable => _useHealthConnectIfAvailable; - /// Check if a given data type is available on the platform bool isDataTypeAvailable(HealthDataType dataType) => Platform.isAndroid ? dataTypeKeysAndroid.contains(dataType) @@ -121,9 +106,7 @@ class Health { }); } - /// Revokes permissions of all types. - /// - /// Uses `disableFit()` on Google Fit. + /// Revokes Android permissions of all types. /// /// Not implemented on iOS as there is no way to programmatically remove access. Future revokePermissions() async { @@ -507,7 +490,7 @@ class Health { return success ?? false; } - /// Saves meal record into Apple Health or Google Fit / Health Connect. + /// Saves meal record into Apple Health or Health Connect. /// /// Returns true if successful, false otherwise. /// @@ -1055,7 +1038,7 @@ class Health { "HealthDataType was not aligned correctly - please report bug at https://github.com/cph-cachet/flutter-plugins/issues"), }; - /// Write workout data to Apple Health or Google Fit or Google Health Connect. + /// Write workout data to Apple Health or Google Health Connect. /// /// Returns true if the workout data was successfully added. /// diff --git a/packages/health/lib/src/health_value_types.dart b/packages/health/lib/src/health_value_types.dart index d3d364141..6a217ea93 100644 --- a/packages/health/lib/src/health_value_types.dart +++ b/packages/health/lib/src/health_value_types.dart @@ -13,7 +13,7 @@ class HealthValue extends Serializable { Map toJson() => _$HealthValueToJson(this); } -/// A numerical value from Apple HealthKit or Google Fit +/// A numerical value from Apple HealthKit or Google Health Connect /// such as integer or double. E.g. 1, 2.9, -3 /// /// Parameters: diff --git a/packages/health/pubspec.yaml b/packages/health/pubspec.yaml index cc118c503..dbfe4d751 100644 --- a/packages/health/pubspec.yaml +++ b/packages/health/pubspec.yaml @@ -1,5 +1,5 @@ name: health -description: Wrapper for HealthKit on iOS and Google Fit and Health Connect on Android. +description: Wrapper for Apple's HealthKit on iOS and Google's Health Connect on Android. version: 10.2.0 homepage: https://github.com/cph-cachet/flutter-plugins/tree/master/packages/health From f8bbc429f456d4c14b49dbe40e60cfeedcbd9ccf Mon Sep 17 00:00:00 2001 From: Aamir Farooq Date: Wed, 7 Aug 2024 10:40:19 +0200 Subject: [PATCH 07/32] Remove `disconect` method channel --- packages/health/README.md | 41 ++----------------- .../ios/Classes/SwiftHealthPlugin.swift | 7 ---- packages/health/lib/src/health_plugin.dart | 27 ------------ 3 files changed, 3 insertions(+), 72 deletions(-) diff --git a/packages/health/README.md b/packages/health/README.md index 63c36dab6..cbda8af61 100644 --- a/packages/health/README.md +++ b/packages/health/README.md @@ -17,7 +17,7 @@ The plugin supports: - cleaning up duplicate data points via the `removeDuplicates` method. - removing data of a given type in a selected period of time using the `delete` method. -Note that for Android, the target phone **needs** to have [Google Fit](https://www.google.com/fit/) or [Health Connect](https://health.google/health-connect-android/) (which is currently in beta) installed and have access to the internet, otherwise this plugin will not work. +Note that for Android, the target phone **needs** to have [Health Connect](https://health.google/health-connect-android/) (which is currently in beta) installed and have access to the internet, otherwise this plugin will not work. See the tables below for supported health and workout data types. @@ -51,42 +51,7 @@ Additionally, for workouts, if the distance of a workout is requested then the l ``` -#### Google Fit (Android option 1) - -Follow the guide at . Below is an example of following the guide. - -Change directory to your key-store directory (MacOS): - -`cd ~/.android/` - -Get your keystore SHA1 fingerprint: - -`keytool -list -v -keystore ~/.android/debug.keystore -alias androiddebugkey -storepass android -keypass android` - -Example output: - -```bash -Alias name: androiddebugkey -Creation date: Jan 01, 2013 -Entry type: PrivateKeyEntry -Certificate chain length: 1 -Certificate[1]: -Owner: CN=Android Debug, O=Android, C=US -Issuer: CN=Android Debug, O=Android, C=US -Serial number: 4aa9b300 -Valid from: Mon Jan 01 08:04:04 UTC 2013 until: Mon Jan 01 18:04:04 PST 2033 -Certificate fingerprints: - MD5: AE:9F:95:D0:A6:86:89:BC:A8:70:BA:34:FF:6A:AC:F9 - SHA1: BB:0D:AC:74:D3:21:E1:43:07:71:9B:62:90:AF:A1:66:6E:44:5D:75 - Signature algorithm name: SHA1withRSA - Version: 3 -``` - -Follow the instructions at for setting up an OAuth2 Client ID for a Google project, and adding the SHA1 fingerprint to that OAuth2 credential. - -The client id will look something like `YOUR_CLIENT_ID.apps.googleusercontent.com`. - -#### Health Connect (Android option 2) +#### Health Connect Health Connect requires the following lines in the `AndroidManifest.xml` file (see also the example app): @@ -185,7 +150,7 @@ android.useAndroidX=true See the example app for detailed examples of how to use the Health API. -The Health plugin is used via the `Health()` singleton using the different methods for handling permissions and getting and adding data to Apple Health, Google Fit, or Google Health Connect. +The Health plugin is used via the `Health()` singleton using the different methods for handling permissions and getting and adding data to Apple Health or Google Health Connect. Below is a simplified flow of how to use the plugin. ```dart diff --git a/packages/health/ios/Classes/SwiftHealthPlugin.swift b/packages/health/ios/Classes/SwiftHealthPlugin.swift index 996c8dc32..d5f1dc509 100644 --- a/packages/health/ios/Classes/SwiftHealthPlugin.swift +++ b/packages/health/ios/Classes/SwiftHealthPlugin.swift @@ -291,13 +291,6 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { else if call.method.elementsEqual("delete") { try! delete(call: call, result: result) } - - /// Disconnect - else if (call.method.elementsEqual("disconnect")){ - // Do nothing. - result(true) - } - } func checkIfHealthDataAvailable(call: FlutterMethodCall, result: @escaping FlutterResult) { diff --git a/packages/health/lib/src/health_plugin.dart b/packages/health/lib/src/health_plugin.dart index cd3f60ebb..814775d1e 100644 --- a/packages/health/lib/src/health_plugin.dart +++ b/packages/health/lib/src/health_plugin.dart @@ -158,33 +158,6 @@ class Health { } } - /// Disconnect from Google fit. - /// - /// Not supported on iOS and Google Health Connect, and the method does nothing. - Future disconnect( - List types, { - List? permissions, - }) async { - if (permissions != null && permissions.length != types.length) { - throw ArgumentError( - 'The length of [types] must be same as that of [permissions].'); - } - - final mTypes = List.from(types, growable: true); - final mPermissions = permissions == null - ? List.filled(types.length, HealthDataAccess.READ.index, - growable: true) - : permissions.map((permission) => permission.index).toList(); - - // on Android, if BMI is requested, then also ask for weight and height - if (Platform.isAndroid) _handleBMI(mTypes, mPermissions); - - List keys = mTypes.map((dataType) => dataType.name).toList(); - - return await _channel.invokeMethod( - 'disconnect', {'types': keys, "permissions": mPermissions}); - } - /// Requests permissions to access health data [types]. /// /// Returns true if successful, false otherwise. From 99eae3398ffb27e5b7eaa6446806a89ee11a7f12 Mon Sep 17 00:00:00 2001 From: Aamir Farooq Date: Wed, 7 Aug 2024 13:11:16 +0200 Subject: [PATCH 08/32] Remove `flowRate` from `writeBloodOxygen` as it is not supported in Health Connect --- .../src/main/kotlin/cachet/plugins/health/HealthPlugin.kt | 2 +- packages/health/example/lib/main.dart | 1 - packages/health/lib/src/health_plugin.dart | 4 ---- 3 files changed, 1 insertion(+), 6 deletions(-) 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 80f4723cd..fd47eade2 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 @@ -307,7 +307,7 @@ class HealthPlugin(private var channel: MethodChannel? = null) : } /** - * Save the blood oxygen saturation, without supplemental flow rate + * Save the blood oxygen saturation */ private fun writeBloodOxygen(call: MethodCall, result: Result) { writeData(call, result) diff --git a/packages/health/example/lib/main.dart b/packages/health/example/lib/main.dart index 56a70c320..316222413 100644 --- a/packages/health/example/lib/main.dart +++ b/packages/health/example/lib/main.dart @@ -265,7 +265,6 @@ class _HealthAppState extends State { saturation: 98, startTime: earlier, endTime: now, - flowRate: 1.0, ); success &= await Health().writeWorkoutData( activityType: HealthWorkoutActivityType.AMERICAN_FOOTBALL, diff --git a/packages/health/lib/src/health_plugin.dart b/packages/health/lib/src/health_plugin.dart index 814775d1e..51da0010b 100644 --- a/packages/health/lib/src/health_plugin.dart +++ b/packages/health/lib/src/health_plugin.dart @@ -424,8 +424,6 @@ class Health { /// /// Parameters: /// * [saturation] - the saturation of the blood oxygen in percentage - /// * [flowRate] - optional supplemental oxygen flow rate, only supported on - /// Google Fit (default 0.0) /// * [startTime] - the start time when this [saturation] is measured. /// Must be equal to or earlier than [endTime]. /// * [endTime] - the end time when this [saturation] is measured. @@ -434,7 +432,6 @@ class Health { /// is measured only at a specific point in time (default). Future writeBloodOxygen({ required double saturation, - double flowRate = 0.0, required DateTime startTime, DateTime? endTime, }) async { @@ -453,7 +450,6 @@ class Health { } else if (Platform.isAndroid) { Map args = { 'value': saturation, - 'flowRate': flowRate, 'startTime': startTime.millisecondsSinceEpoch, 'endTime': endTime.millisecondsSinceEpoch, 'dataTypeKey': HealthDataType.BLOOD_OXYGEN.name, From 99ae5dde1628574a082031c62afed948504cbda2 Mon Sep 17 00:00:00 2001 From: Aamir Farooq Date: Wed, 7 Aug 2024 14:08:17 +0200 Subject: [PATCH 09/32] Remove more unsupported workout types --- packages/health/README.md | 11 ----------- .../cachet/plugins/health/HealthPlugin.kt | 18 ------------------ .../health/ios/Classes/SwiftHealthPlugin.swift | 5 ----- packages/health/lib/health.g.dart | 10 ---------- packages/health/lib/src/health_plugin.dart | 10 ---------- packages/health/lib/src/heath_data_types.dart | 10 ---------- 6 files changed, 64 deletions(-) diff --git a/packages/health/README.md b/packages/health/README.md index cbda8af61..09be1f012 100644 --- a/packages/health/README.md +++ b/packages/health/README.md @@ -391,18 +391,10 @@ The plugin supports the following [`HealthWorkoutActivityType`](https://pub.dev/ | ROWING | yes | yes | | | RUGBY | yes | yes | | | RUNNING | yes | yes | | -| RUNNING_JOGGING | (yes) | | on iOS this will be stored as RUNNING | -| RUNNING_SAND | (yes) | | on iOS this will be stored as RUNNING | | RUNNING_TREADMILL | (yes) | yes | on iOS this will be stored as RUNNING | | SAILING | yes | yes | | | SCUBA_DIVING | | yes | | | SKATING | yes | yes | On iOS this is skating_sports | -| SKATING_CROSS | (yes) | | on iOS this will be stored as SKATING | -| SKATING_INDOOR | (yes) | | on iOS this will be stored as SKATING | -| SKATING_INLINE | (yes) | | on iOS this will be stored as SKATING | -| SKIING_BACK_COUNTRY | | | | -| SKIING_KITE | | | | -| SKIING_ROLLER | | | | | SNOW_SPORTS | yes | | | | SNOWBOARDING | yes | yes | | | SOCCER | yes | | | @@ -424,10 +416,7 @@ The plugin supports the following [`HealthWorkoutActivityType`](https://pub.dev/ | TENNIS | yes | yes | | | TRACK_AND_FIELD | yes | | | | TRADITIONAL_STRENGTH_TRAINING | yes | | | -| TREADMILL | | | | | VOLLEYBALL | yes | yes | | -| VOLLEYBALL_BEACH | | | | -| VOLLEYBALL_INDOOR | | | | | WALKING | yes | yes | | | WATER_FITNESS | yes | | | | WATER_POLO | yes | yes | | 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 fd47eade2..31801aa5e 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 @@ -428,7 +428,6 @@ class HealthPlugin(private var channel: MethodChannel? = null) : healthConnectRequestPermissionsLauncher = null } - /** HEALTH CONNECT BELOW */ private var healthConnectAvailable = false private var healthConnectStatus = HealthConnectClient.SDK_UNAVAILABLE @@ -2177,11 +2176,9 @@ class HealthPlugin(private var channel: MethodChannel? = null) : // TOOD: look into paddling // TODO: add runnning // TODO: look into hockey - // TODO: look into volleyball "AMERICAN_FOOTBALL" to ExerciseSessionRecord .EXERCISE_TYPE_FOOTBALL_AMERICAN, - // "ARCHERY" to ExerciseSessionRecord.EXERCISE_TYPE_ARCHERY, "AUSTRALIAN_FOOTBALL" to ExerciseSessionRecord .EXERCISE_TYPE_FOOTBALL_AUSTRALIAN, @@ -2193,12 +2190,7 @@ class HealthPlugin(private var channel: MethodChannel? = null) : ExerciseSessionRecord .EXERCISE_TYPE_BASKETBALL, "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 @@ -2248,8 +2240,6 @@ class HealthPlugin(private var channel: MethodChannel? = null) : 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, @@ -2258,14 +2248,8 @@ class HealthPlugin(private var channel: MethodChannel? = null) : "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, "SNOWBOARDING" to ExerciseSessionRecord .EXERCISE_TYPE_SNOWBOARDING, @@ -2295,8 +2279,6 @@ class HealthPlugin(private var channel: MethodChannel? = null) : ExerciseSessionRecord .EXERCISE_TYPE_TABLE_TENNIS, "TENNIS" to ExerciseSessionRecord.EXERCISE_TYPE_TENNIS, - // "VOLLEYBALL_BEACH" to ExerciseSessionRecord.EXERCISE_TYPE_VOLLEYBALL_BEACH, - // "VOLLEYBALL_INDOOR" to ExerciseSessionRecord.EXERCISE_TYPE_VOLLEYBALL_INDOOR, "VOLLEYBALL" to ExerciseSessionRecord .EXERCISE_TYPE_VOLLEYBALL, diff --git a/packages/health/ios/Classes/SwiftHealthPlugin.swift b/packages/health/ios/Classes/SwiftHealthPlugin.swift index d5f1dc509..9b00a3c2b 100644 --- a/packages/health/ios/Classes/SwiftHealthPlugin.swift +++ b/packages/health/ios/Classes/SwiftHealthPlugin.swift @@ -1296,8 +1296,6 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { workoutActivityTypeMap["FLEXIBILITY"] = .flexibility workoutActivityTypeMap["WALKING"] = .walking workoutActivityTypeMap["RUNNING"] = .running - workoutActivityTypeMap["RUNNING_JOGGING"] = .running // Supported due to combining with Android naming - workoutActivityTypeMap["RUNNING_SAND"] = .running // Supported due to combining with Android naming workoutActivityTypeMap["RUNNING_TREADMILL"] = .running // Supported due to combining with Android naming workoutActivityTypeMap["WHEELCHAIR_WALK_PACE"] = .wheelchairWalkPace workoutActivityTypeMap["WHEELCHAIR_RUN_PACE"] = .wheelchairRunPace @@ -1338,9 +1336,6 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { workoutActivityTypeMap["SNOW_SPORTS"] = .snowSports workoutActivityTypeMap["SNOWBOARDING"] = .snowboarding workoutActivityTypeMap["SKATING"] = .skatingSports - workoutActivityTypeMap["SKATING_CROSS,"] = .skatingSports // Supported due to combining with Android naming - workoutActivityTypeMap["SKATING_INDOOR,"] = .skatingSports // Supported due to combining with Android naming - workoutActivityTypeMap["SKATING_INLINE,"] = .skatingSports // Supported due to combining with Android naming workoutActivityTypeMap["PADDLE_SPORTS"] = .paddleSports workoutActivityTypeMap["ROWING"] = .rowing workoutActivityTypeMap["SAILING"] = .sailing diff --git a/packages/health/lib/health.g.dart b/packages/health/lib/health.g.dart index 6a365a135..7aea4b361 100644 --- a/packages/health/lib/health.g.dart +++ b/packages/health/lib/health.g.dart @@ -380,19 +380,11 @@ const _$HealthWorkoutActivityTypeEnumMap = { HealthWorkoutActivityType.ROWING_MACHINE: 'ROWING_MACHINE', HealthWorkoutActivityType.ROWING: 'ROWING', HealthWorkoutActivityType.RUGBY: 'RUGBY', - HealthWorkoutActivityType.RUNNING_JOGGING: 'RUNNING_JOGGING', - HealthWorkoutActivityType.RUNNING_SAND: 'RUNNING_SAND', HealthWorkoutActivityType.RUNNING_TREADMILL: 'RUNNING_TREADMILL', HealthWorkoutActivityType.RUNNING: 'RUNNING', HealthWorkoutActivityType.SAILING: 'SAILING', HealthWorkoutActivityType.SCUBA_DIVING: 'SCUBA_DIVING', - HealthWorkoutActivityType.SKATING_CROSS: 'SKATING_CROSS', - HealthWorkoutActivityType.SKATING_INDOOR: 'SKATING_INDOOR', - HealthWorkoutActivityType.SKATING_INLINE: 'SKATING_INLINE', HealthWorkoutActivityType.SKATING: 'SKATING', - HealthWorkoutActivityType.SKIING_BACK_COUNTRY: 'SKIING_BACK_COUNTRY', - HealthWorkoutActivityType.SKIING_KITE: 'SKIING_KITE', - HealthWorkoutActivityType.SKIING_ROLLER: 'SKIING_ROLLER', HealthWorkoutActivityType.SKIING: 'SKIING', HealthWorkoutActivityType.SNOW_SPORTS: 'SNOW_SPORTS', HealthWorkoutActivityType.SNOWBOARDING: 'SNOWBOARDING', @@ -417,8 +409,6 @@ const _$HealthWorkoutActivityTypeEnumMap = { HealthWorkoutActivityType.TRACK_AND_FIELD: 'TRACK_AND_FIELD', HealthWorkoutActivityType.TRADITIONAL_STRENGTH_TRAINING: 'TRADITIONAL_STRENGTH_TRAINING', - HealthWorkoutActivityType.VOLLEYBALL_BEACH: 'VOLLEYBALL_BEACH', - HealthWorkoutActivityType.VOLLEYBALL_INDOOR: 'VOLLEYBALL_INDOOR', HealthWorkoutActivityType.VOLLEYBALL: 'VOLLEYBALL', HealthWorkoutActivityType.WALKING: 'WALKING', HealthWorkoutActivityType.WATER_FITNESS: 'WATER_FITNESS', diff --git a/packages/health/lib/src/health_plugin.dart b/packages/health/lib/src/health_plugin.dart index 51da0010b..85242ca4e 100644 --- a/packages/health/lib/src/health_plugin.dart +++ b/packages/health/lib/src/health_plugin.dart @@ -1200,16 +1200,8 @@ class Health { HealthWorkoutActivityType.PARAGLIDING, HealthWorkoutActivityType.ROCK_CLIMBING, HealthWorkoutActivityType.ROWING_MACHINE, - HealthWorkoutActivityType.RUNNING_JOGGING, - HealthWorkoutActivityType.RUNNING_SAND, HealthWorkoutActivityType.RUNNING_TREADMILL, HealthWorkoutActivityType.SCUBA_DIVING, - HealthWorkoutActivityType.SKATING_CROSS, - HealthWorkoutActivityType.SKATING_INDOOR, - HealthWorkoutActivityType.SKATING_INLINE, - HealthWorkoutActivityType.SKIING_BACK_COUNTRY, - HealthWorkoutActivityType.SKIING_KITE, - HealthWorkoutActivityType.SKIING_ROLLER, HealthWorkoutActivityType.SKIING, HealthWorkoutActivityType.SNOWSHOEING, HealthWorkoutActivityType.STAIR_CLIMBING_MACHINE, @@ -1217,8 +1209,6 @@ class Health { HealthWorkoutActivityType.SURFING, HealthWorkoutActivityType.SWIMMING_OPEN_WATER, HealthWorkoutActivityType.SWIMMING_POOL, - HealthWorkoutActivityType.VOLLEYBALL_BEACH, - HealthWorkoutActivityType.VOLLEYBALL_INDOOR, HealthWorkoutActivityType.WALKING_FITNESS, HealthWorkoutActivityType.WALKING_NORDIC, HealthWorkoutActivityType.WALKING_STROLLER, diff --git a/packages/health/lib/src/heath_data_types.dart b/packages/health/lib/src/heath_data_types.dart index b1554dd2f..eef93c4f9 100644 --- a/packages/health/lib/src/heath_data_types.dart +++ b/packages/health/lib/src/heath_data_types.dart @@ -543,16 +543,8 @@ enum HealthWorkoutActivityType { PARAGLIDING, ROCK_CLIMBING, // on iOS this is the same as CLIMBING ROWING_MACHINE, - RUNNING_JOGGING, // on iOS this is the same as RUNNING - RUNNING_SAND, // on iOS this is the same as RUNNING RUNNING_TREADMILL, // on iOS this is the same as RUNNING SCUBA_DIVING, - SKATING_CROSS, // on iOS this is the same as SKATING - SKATING_INDOOR, // on iOS this is the same as SKATING - SKATING_INLINE, // on iOS this is the same as SKATING - SKIING_BACK_COUNTRY, - SKIING_KITE, - SKIING_ROLLER, SKIING, SNOWSHOEING, STAIR_CLIMBING_MACHINE, @@ -560,8 +552,6 @@ enum HealthWorkoutActivityType { SURFING, SWIMMING_OPEN_WATER, SWIMMING_POOL, - VOLLEYBALL_BEACH, - VOLLEYBALL_INDOOR, WALKING_FITNESS, WALKING_NORDIC, WALKING_STROLLER, From d604507fc5e33934fa4751193a281119388ac005 Mon Sep 17 00:00:00 2001 From: Aamir Farooq Date: Thu, 8 Aug 2024 11:23:19 +0200 Subject: [PATCH 10/32] Add missing import --- packages/health/README.md | 6 +- .../cachet/plugins/health/HealthPlugin.kt | 159 +++++++++--------- 2 files changed, 83 insertions(+), 82 deletions(-) diff --git a/packages/health/README.md b/packages/health/README.md index 09be1f012..21c03c085 100644 --- a/packages/health/README.md +++ b/packages/health/README.md @@ -51,7 +51,7 @@ Additionally, for workouts, if the distance of a workout is requested then the l ``` -#### Health Connect +#### Health Connect Health Connect requires the following lines in the `AndroidManifest.xml` file (see also the example app): @@ -342,7 +342,7 @@ The plugin supports the following [`HealthWorkoutActivityType`](https://pub.dev/ | BARRE | yes | | | | BASEBALL | yes | yes | | | BASKETBALL | yes | yes | | -| BIKING | yes | yes | on iOS this is CYCLING, but name changed here to fit with Android | +| BIKING | yes | yes | on iOS this is CYCLING, but name changed here to fit with Android | | BOWLING | yes | | | | BOXING | yes | yes | | | CALISTHENICS | | yes | | @@ -354,7 +354,7 @@ The plugin supports the following [`HealthWorkoutActivityType`](https://pub.dev/ | CROSS_COUNTRY_SKIING | yes | | | | CROSS_TRAINING | yes | | | | CURLING | yes | | | -| DANCING | yes | yes | on iOS this is DANCE, but name changed here to fit with Android | +| DANCING | yes | yes | on iOS this is DANCE, but name changed here to fit with Android | | DISC_SPORTS | yes | | | | DOWNHILL_SKIING | yes | | | | ELLIPTICAL | yes | yes | | 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 31801aa5e..07e8faa54 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 @@ -29,6 +29,7 @@ import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel.MethodCallHandler +import io.flutter.plugin.common.MethodChannel.Result import io.flutter.plugin.common.PluginRegistry.ActivityResultListener import io.flutter.plugin.common.PluginRegistry.Registrar import java.time.* @@ -160,6 +161,84 @@ class HealthPlugin(private var channel: MethodChannel? = null) : return false } + /** Handle calls from the MethodChannel */ + override fun onMethodCall(call: MethodCall, result: Result) { + when (call.method) { + "installHealthConnect" -> installHealthConnect(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" -> deleteData(call, result) + "getAggregateData" -> getAggregateData(call, result) + "getTotalStepsInInterval" -> getTotalStepsInInterval(call, result) + "writeWorkoutData" -> writeWorkoutData(call, result) + "writeBloodPressure" -> writeBloodPressure(call, result) + "writeBloodOxygen" -> writeBloodOxygen(call, result) + "writeMenstruationFlow" -> writeMenstruationFlow(call, result) + "writeMeal" -> writeMeal(call, result) + else -> result.notImplemented() + } + } + + 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) } + } + + override fun onDetachedFromActivityForConfigChanges() { + onDetachedFromActivity() + } + + override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) { + onAttachedToActivity(binding) + } + + override fun onDetachedFromActivity() { + if (channel == null) { + return + } + activity = null + healthConnectRequestPermissionsLauncher = null + } + + private var healthConnectAvailable = false + private var healthConnectStatus = HealthConnectClient.SDK_UNAVAILABLE + + private 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) + } + private fun onHealthConnectPermissionCallback(permissionGranted: Set) { if (permissionGranted.isEmpty()) { mResult?.success(false) @@ -298,7 +377,6 @@ class HealthPlugin(private var channel: MethodChannel? = null) : } } - /** * Save menstrual flow data */ @@ -373,83 +451,6 @@ class HealthPlugin(private var channel: MethodChannel? = null) : } } - /** Handle calls from the MethodChannel */ - override fun onMethodCall(call: MethodCall, result: Result) { - when (call.method) { - "installHealthConnect" -> installHealthConnect(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" -> deleteData(call, result) - "getAggregateData" -> getAggregateData(call, result) - "getTotalStepsInInterval" -> getTotalStepsInInterval(call, result) - "writeWorkoutData" -> writeWorkoutData(call, result) - "writeBloodPressure" -> writeBloodPressure(call, result) - "writeBloodOxygen" -> writeBloodOxygen(call, result) - "writeMenstruationFlow" -> writeMenstruationFlow(call, result) - "writeMeal" -> writeMeal(call, result) - else -> result.notImplemented() - } - } - - 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) } - } - - override fun onDetachedFromActivityForConfigChanges() { - onDetachedFromActivity() - } - - override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) { - onAttachedToActivity(binding) - } - - override fun onDetachedFromActivity() { - if (channel == null) { - return - } - activity = null - healthConnectRequestPermissionsLauncher = null - } - - private var healthConnectAvailable = false - private var healthConnectStatus = HealthConnectClient.SDK_UNAVAILABLE - - private 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) - } private fun getHealthConnectSdkStatus(call: MethodCall, result: Result) { checkAvailability() @@ -2293,6 +2294,6 @@ class HealthPlugin(private var channel: MethodChannel? = null) : ExerciseSessionRecord .EXERCISE_TYPE_WHEELCHAIR, "YOGA" to ExerciseSessionRecord.EXERCISE_TYPE_YOGA, - "OTHER" to ExerciseSessionRecord.OTHER_WORKOUT, + "OTHER" to ExerciseSessionRecord.EXERCISE_TYPE_OTHER_WORKOUT, ) } From 458d323e0824604a0d48e4b2340d6c5e43aa35c6 Mon Sep 17 00:00:00 2001 From: Aamir Farooq Date: Thu, 8 Aug 2024 11:36:28 +0200 Subject: [PATCH 11/32] Remove Google Fit as dependency --- packages/health/README.md | 4 ++-- packages/health/android/build.gradle | 6 +----- packages/health/example/android/app/build.gradle | 4 ---- packages/health/lib/src/health_plugin.dart | 3 --- 4 files changed, 3 insertions(+), 14 deletions(-) diff --git a/packages/health/README.md b/packages/health/README.md index 21c03c085..7aae7e22d 100644 --- a/packages/health/README.md +++ b/packages/health/README.md @@ -1,6 +1,6 @@ # Health -Enables reading and writing health data from/to Apple Health, Google Fit and Health Connect. +Enables reading and writing health data from/to Apple Health and Health Connect. > Google Fitness API is deprecated and will be turned down in 2024, thus this package will also transition to only support Health Connect. @@ -80,7 +80,7 @@ In the Health Connect permissions activity there is a link to your privacy polic ``` -If using Health Connect on Android it requires special permissions in the `AndroidManifest.xml` file. The permissions can be found here: +Health Connect on Android it requires special permissions in the `AndroidManifest.xml` file. The permissions can be found here: Example shown here (can also be found in the example app): diff --git a/packages/health/android/build.gradle b/packages/health/android/build.gradle index ee0b26c7f..9d9c8e7ed 100644 --- a/packages/health/android/build.gradle +++ b/packages/health/android/build.gradle @@ -50,14 +50,10 @@ android { namespace "cachet.plugins.health" } -dependencies { - def composeBom = platform('androidx.compose:compose-bom:2022.10.00') +dependencies { def composeBom = platform('androidx.compose:compose-bom:2022.10.00') implementation(composeBom) implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" - implementation("com.google.android.gms:play-services-fitness:21.1.0") - implementation("com.google.android.gms:play-services-auth:20.2.0") - // The new health connect api implementation("androidx.health.connect:connect-client:1.1.0-alpha07") def fragment_version = "1.6.2" implementation "androidx.fragment:fragment-ktx:$fragment_version" diff --git a/packages/health/example/android/app/build.gradle b/packages/health/example/android/app/build.gradle index 95636d301..63e07ba82 100644 --- a/packages/health/example/android/app/build.gradle +++ b/packages/health/example/android/app/build.gradle @@ -64,10 +64,6 @@ dependencies { implementation(composeBom) implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" testImplementation 'junit:junit:4.12' - implementation("com.google.android.gms:play-services-fitness:21.1.0") - implementation("com.google.android.gms:play-services-auth:20.2.0") androidTestImplementation 'androidx.test:runner:1.1.1' androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' - // The new health connect api - // implementation("androidx.health.connect:connect-client:1.0.0-alpha11") } diff --git a/packages/health/lib/src/health_plugin.dart b/packages/health/lib/src/health_plugin.dart index 85242ca4e..1213a585d 100644 --- a/packages/health/lib/src/health_plugin.dart +++ b/packages/health/lib/src/health_plugin.dart @@ -972,8 +972,6 @@ class Health { /// Get the total number of steps within a specific time period. /// Returns null if not successful. - /// - /// Is a fix according to https://stackoverflow.com/questions/29414386/step-count-retrieved-through-google-fit-api-does-not-match-step-count-displayed/29415091#29415091 Future getTotalStepsInInterval(DateTime startTime, DateTime endTime, {bool includeManualEntry = true}) async { final args = { @@ -1185,7 +1183,6 @@ class Health { HealthWorkoutActivityType.YOGA, // Android only - // Once Google Fit is removed, this list needs to be changed HealthWorkoutActivityType.BIKING_HAND, HealthWorkoutActivityType.BIKING_MOUNTAIN, HealthWorkoutActivityType.BIKING_ROAD, From c732a9e6d702d74fd1835816f1ee98c416568bfa Mon Sep 17 00:00:00 2001 From: Aamir Farooq Date: Thu, 8 Aug 2024 11:40:43 +0200 Subject: [PATCH 12/32] Add notice in README --- packages/health/README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/health/README.md b/packages/health/README.md index 7aae7e22d..e7cf8c16d 100644 --- a/packages/health/README.md +++ b/packages/health/README.md @@ -2,7 +2,8 @@ Enables reading and writing health data from/to Apple Health and Health Connect. -> Google Fitness API is deprecated and will be turned down in 2024, thus this package will also transition to only support Health Connect. +> [!IMPORTANT] +> Google has deprecated the Google Fit API. According to the [documentation](https://developers.google.com/fit/android), the API will no longer be available after **June 30, 2025**. As such, this package has removed support for Google Fit as of version 11.0.0 and users are urged to upgrade as soon as possible. The plugin supports: From 0e248a686bd2bedb27d1c38cbbed795d52ed7e9d Mon Sep 17 00:00:00 2001 From: Aamir Farooq Date: Thu, 8 Aug 2024 11:58:29 +0200 Subject: [PATCH 13/32] Improve logging for HC permission callback --- .../src/main/kotlin/cachet/plugins/health/HealthPlugin.kt | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) 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 07e8faa54..331fbd4c6 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 @@ -242,10 +242,13 @@ class HealthPlugin(private var channel: MethodChannel? = null) : private fun onHealthConnectPermissionCallback(permissionGranted: Set) { if (permissionGranted.isEmpty()) { mResult?.success(false) - Log.i("FLUTTER_HEALTH", "Access Denied (to Health Connect)!") + Log.i("FLUTTER_HEALTH", "Health Connect permissions were not granted! Make sure to declare the required permissions in the AndroidManifest.xml file.") } else { mResult?.success(true) - Log.i("FLUTTER_HEALTH", "Access Granted (to Health Connect)!") + Log.i("FLUTTER_HEALTH", "${permissionGranted.size} Health Connect permissions were granted!") + + // log the permissions granted for debugging + Log.i("FLUTTER_HEALTH", "Permissions granted: $permissionGranted") } } From 0f5fb5a1c299cf72e7a5376b7a18d3773a26cd71 Mon Sep 17 00:00:00 2001 From: Aamir Farooq Date: Thu, 8 Aug 2024 12:04:07 +0200 Subject: [PATCH 14/32] Update some documentation --- .../src/main/kotlin/cachet/plugins/health/HealthPlugin.kt | 5 +---- packages/health/lib/src/health_plugin.dart | 3 ++- 2 files changed, 3 insertions(+), 5 deletions(-) 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 331fbd4c6..1154bfeff 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 @@ -62,7 +62,6 @@ const val STEPS = "STEPS" const val WATER = "WATER" const val WEIGHT = "WEIGHT" -// TODO support unknown? const val BREAKFAST = "BREAKFAST" const val DINNER = "DINNER" const val LUNCH = "LUNCH" @@ -1402,8 +1401,7 @@ class HealthPlugin(private var channel: MethodChannel? = null) : } // 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 + // not adopt a single type with attached stages approach private fun writeData(call: MethodCall, result: Result) { val type = call.argument("dataTypeKey")!! val startTime = call.argument("startTime")!! @@ -2171,7 +2169,6 @@ class HealthPlugin(private var channel: MethodChannel? = null) : TotalCaloriesBurnedRecord.ENERGY_TOTAL ) - // TODO: Update with new workout types when Health Connect becomes the standard. private val workoutTypeMap = mapOf( // TODO: add skiing diff --git a/packages/health/lib/src/health_plugin.dart b/packages/health/lib/src/health_plugin.dart index 1213a585d..a7a874f24 100644 --- a/packages/health/lib/src/health_plugin.dart +++ b/packages/health/lib/src/health_plugin.dart @@ -106,8 +106,9 @@ class Health { }); } - /// Revokes Android permissions of all types. + /// Revokes Google Health Connect permissions on Android of all types. /// + /// NOTE: The app must be completely killed and restarted for the changes to take effect. /// Not implemented on iOS as there is no way to programmatically remove access. Future revokePermissions() async { try { From 3979573270282224d4e11eca2344cd2fffe8e9bf Mon Sep 17 00:00:00 2001 From: Aamir Farooq Date: Thu, 8 Aug 2024 12:36:10 +0200 Subject: [PATCH 15/32] Android: Fix `requestAuthorization` not returning a result on success --- packages/health/android/build.gradle | 3 ++- .../main/kotlin/cachet/plugins/health/HealthPlugin.kt | 11 +++++++---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/packages/health/android/build.gradle b/packages/health/android/build.gradle index 9d9c8e7ed..1f96a1fb1 100644 --- a/packages/health/android/build.gradle +++ b/packages/health/android/build.gradle @@ -50,7 +50,8 @@ android { namespace "cachet.plugins.health" } -dependencies { def composeBom = platform('androidx.compose:compose-bom:2022.10.00') +dependencies { + def composeBom = platform('androidx.compose:compose-bom:2022.10.00') implementation(composeBom) implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 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 1154bfeff..369497662 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 @@ -156,10 +156,6 @@ class HealthPlugin(private var channel: MethodChannel? = null) : handler?.post { mResult?.error(errorCode, errorMessage, errorDetails) } } - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean { - return false - } - /** Handle calls from the MethodChannel */ override fun onMethodCall(call: MethodCall, result: Result) { when (call.method) { @@ -546,6 +542,11 @@ class HealthPlugin(private var channel: MethodChannel? = null) : * type. */ private fun requestAuthorization(call: MethodCall, result: Result) { + if (context == null) { + result.success(false) + return + } + val args = call.arguments as HashMap<*, *> val types = (args["types"] as? ArrayList<*>)?.filterIsInstance()!! val permissions = (args["permissions"] as? ArrayList<*>)?.filterIsInstance()!! @@ -617,6 +618,8 @@ class HealthPlugin(private var channel: MethodChannel? = null) : return } + // Store the result to be called in [onHealthConnectPermissionCallback] + mResult = result healthConnectRequestPermissionsLauncher!!.launch(permList.toSet()) } From d663b25baa03c3b13b3b69126655df2dfe4bf05d Mon Sep 17 00:00:00 2001 From: Aamir Farooq Date: Thu, 8 Aug 2024 15:21:16 +0200 Subject: [PATCH 16/32] Remove additional workout types that are not supported --- packages/health/lib/health.g.dart | 5 ----- packages/health/lib/src/health_plugin.dart | 7 ------- packages/health/lib/src/heath_data_types.dart | 7 ------- 3 files changed, 19 deletions(-) diff --git a/packages/health/lib/health.g.dart b/packages/health/lib/health.g.dart index 7aea4b361..89ded9d4c 100644 --- a/packages/health/lib/health.g.dart +++ b/packages/health/lib/health.g.dart @@ -321,12 +321,7 @@ const _$HealthWorkoutActivityTypeEnumMap = { HealthWorkoutActivityType.BARRE: 'BARRE', HealthWorkoutActivityType.BASEBALL: 'BASEBALL', HealthWorkoutActivityType.BASKETBALL: 'BASKETBALL', - HealthWorkoutActivityType.BIKING_HAND: 'BIKING_HAND', - HealthWorkoutActivityType.BIKING_MOUNTAIN: 'BIKING_MOUNTAIN', - HealthWorkoutActivityType.BIKING_ROAD: 'BIKING_ROAD', - HealthWorkoutActivityType.BIKING_SPINNING: 'BIKING_SPINNING', HealthWorkoutActivityType.BIKING_STATIONARY: 'BIKING_STATIONARY', - HealthWorkoutActivityType.BIKING_UTILITY: 'BIKING_UTILITY', HealthWorkoutActivityType.BIKING: 'BIKING', HealthWorkoutActivityType.BOWLING: 'BOWLING', HealthWorkoutActivityType.BOXING: 'BOXING', diff --git a/packages/health/lib/src/health_plugin.dart b/packages/health/lib/src/health_plugin.dart index a7a874f24..712508408 100644 --- a/packages/health/lib/src/health_plugin.dart +++ b/packages/health/lib/src/health_plugin.dart @@ -1184,12 +1184,7 @@ class Health { HealthWorkoutActivityType.YOGA, // Android only - HealthWorkoutActivityType.BIKING_HAND, - HealthWorkoutActivityType.BIKING_MOUNTAIN, - HealthWorkoutActivityType.BIKING_ROAD, - HealthWorkoutActivityType.BIKING_SPINNING, HealthWorkoutActivityType.BIKING_STATIONARY, - HealthWorkoutActivityType.BIKING_UTILITY, HealthWorkoutActivityType.CALISTHENICS, HealthWorkoutActivityType.DANCING, HealthWorkoutActivityType.FRISBEE_DISC, @@ -1207,8 +1202,6 @@ class Health { HealthWorkoutActivityType.SURFING, HealthWorkoutActivityType.SWIMMING_OPEN_WATER, HealthWorkoutActivityType.SWIMMING_POOL, - HealthWorkoutActivityType.WALKING_FITNESS, - HealthWorkoutActivityType.WALKING_NORDIC, HealthWorkoutActivityType.WALKING_STROLLER, HealthWorkoutActivityType.WALKING_TREADMILL, HealthWorkoutActivityType.WEIGHTLIFTING, diff --git a/packages/health/lib/src/heath_data_types.dart b/packages/health/lib/src/heath_data_types.dart index eef93c4f9..76192eeb0 100644 --- a/packages/health/lib/src/heath_data_types.dart +++ b/packages/health/lib/src/heath_data_types.dart @@ -529,12 +529,7 @@ enum HealthWorkoutActivityType { WRESTLING, // Android only - BIKING_HAND, - BIKING_MOUNTAIN, - BIKING_ROAD, - BIKING_SPINNING, BIKING_STATIONARY, - BIKING_UTILITY, CALISTHENICS, DANCING, FRISBEE_DISC, @@ -552,8 +547,6 @@ enum HealthWorkoutActivityType { SURFING, SWIMMING_OPEN_WATER, SWIMMING_POOL, - WALKING_FITNESS, - WALKING_NORDIC, WALKING_STROLLER, WALKING_TREADMILL, WEIGHTLIFTING, From 1bb375cd38554639920cede9213e11f54231a88c Mon Sep 17 00:00:00 2001 From: Aamir Farooq Date: Thu, 8 Aug 2024 15:24:14 +0200 Subject: [PATCH 17/32] Remove another workout type --- packages/health/lib/src/health_plugin.dart | 1 - packages/health/lib/src/heath_data_types.dart | 1 - 2 files changed, 2 deletions(-) diff --git a/packages/health/lib/src/health_plugin.dart b/packages/health/lib/src/health_plugin.dart index 712508408..87de5bcba 100644 --- a/packages/health/lib/src/health_plugin.dart +++ b/packages/health/lib/src/health_plugin.dart @@ -1202,7 +1202,6 @@ class Health { HealthWorkoutActivityType.SURFING, HealthWorkoutActivityType.SWIMMING_OPEN_WATER, HealthWorkoutActivityType.SWIMMING_POOL, - HealthWorkoutActivityType.WALKING_STROLLER, HealthWorkoutActivityType.WALKING_TREADMILL, HealthWorkoutActivityType.WEIGHTLIFTING, HealthWorkoutActivityType.WHEELCHAIR, diff --git a/packages/health/lib/src/heath_data_types.dart b/packages/health/lib/src/heath_data_types.dart index 76192eeb0..014cfe1da 100644 --- a/packages/health/lib/src/heath_data_types.dart +++ b/packages/health/lib/src/heath_data_types.dart @@ -547,7 +547,6 @@ enum HealthWorkoutActivityType { SURFING, SWIMMING_OPEN_WATER, SWIMMING_POOL, - WALKING_STROLLER, WALKING_TREADMILL, WEIGHTLIFTING, WHEELCHAIR, From 94aceae980de81f027b1356150dd75b374af54c6 Mon Sep 17 00:00:00 2001 From: Aamir Farooq Date: Thu, 8 Aug 2024 16:42:36 +0200 Subject: [PATCH 18/32] Add missing unimplemented method --- .../src/main/kotlin/cachet/plugins/health/HealthPlugin.kt | 4 ++++ 1 file changed, 4 insertions(+) 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 369497662..b80b2b53d 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 @@ -155,6 +155,10 @@ class HealthPlugin(private var channel: MethodChannel? = null) : ) { handler?.post { mResult?.error(errorCode, errorMessage, errorDetails) } } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean { + return false + } /** Handle calls from the MethodChannel */ override fun onMethodCall(call: MethodCall, result: Result) { From 240d4273e1a4c5e0bdabae510eeb098dfe92c56a Mon Sep 17 00:00:00 2001 From: Aamir Farooq Date: Wed, 14 Aug 2024 15:32:15 +0200 Subject: [PATCH 19/32] Include recording method from Android metadata in HealthDataPoint --- packages/health/README.md | 2 +- .../cachet/plugins/health/HealthPlugin.kt | 130 +++++++++++++++++- packages/health/example/lib/main.dart | 8 +- packages/health/lib/health.g.dart | 120 ++++++++-------- .../health/lib/src/health_data_point.dart | 35 ++++- packages/health/lib/src/health_plugin.dart | 2 +- 6 files changed, 224 insertions(+), 73 deletions(-) diff --git a/packages/health/README.md b/packages/health/README.md index e7cf8c16d..cb9b14db6 100644 --- a/packages/health/README.md +++ b/packages/health/README.md @@ -205,7 +205,7 @@ HealthPlatformType sourcePlatform; String sourceDeviceId; String sourceId; String sourceName; -bool isManualEntry; +RecordingMethod recordingMethod; WorkoutSummary? workoutSummary; ``` 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 b80b2b53d..9093083f6 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 @@ -18,6 +18,7 @@ import androidx.health.connect.client.records.MealType.MEAL_TYPE_DINNER import androidx.health.connect.client.records.MealType.MEAL_TYPE_LUNCH import androidx.health.connect.client.records.MealType.MEAL_TYPE_SNACK import androidx.health.connect.client.records.MealType.MEAL_TYPE_UNKNOWN +import androidx.health.connect.client.records.metadata.Metadata import androidx.health.connect.client.request.AggregateGroupByDurationRequest import androidx.health.connect.client.request.AggregateRequest import androidx.health.connect.client.request.ReadRecordsRequest @@ -414,17 +415,26 @@ class HealthPlugin(private var channel: MethodChannel? = null) : private fun getTotalStepsInInterval(call: MethodCall, result: Result) { val start = call.argument("startTime")!! val end = call.argument("endTime")!! + val includeManualEntry = call.argument("includeManualEntry")!! + if (includeManualEntry) { + getAggregatedStepCount(start, end, result) + } else { + getStepCountManual(start, end, result) + } + } + + private fun getAggregatedStepCount(start: Long, end: Long, result: Result) { + val startInstant = Instant.ofEpochMilli(start) + val endInstant = Instant.ofEpochMilli(end) scope.launch { try { - val startInstant = Instant.ofEpochMilli(start) - val endInstant = Instant.ofEpochMilli(end) val response = healthConnectClient.aggregate( AggregateRequest( metrics = setOf( - StepsRecord.COUNT_TOTAL + StepsRecord.COUNT_TOTAL, ), timeRangeFilter = TimeRangeFilter.between( @@ -437,6 +447,7 @@ class HealthPlugin(private var channel: MethodChannel? = null) : // time range. val stepsInInterval = response[StepsRecord.COUNT_TOTAL] ?: 0L + Log.i( "FLUTTER_HEALTH::SUCCESS", "returning $stepsInInterval steps" @@ -453,6 +464,41 @@ class HealthPlugin(private var channel: MethodChannel? = null) : } } + /** get the step records manually and filter out manual entries **/ + private fun getStepCountManual(start: Long, end: Long, result: Result) { + scope.launch { + try { + val request = + ReadRecordsRequest( + recordType = StepsRecord::class, + timeRangeFilter = + TimeRangeFilter.between( + Instant.ofEpochMilli(start), + Instant.ofEpochMilli(end) + ), + ) + val response = healthConnectClient.readRecords(request) + val filteredRecords = filterManualEntry( + false, + response.records + ) + val totalSteps = filteredRecords.sumOf { (it as StepsRecord).count.toInt() } + Log.i( + "FLUTTER_HEALTH::SUCCESS", + "returning $totalSteps steps (excluding manual entries)" + ) + result.success(totalSteps) + } catch (e: Exception) { + Log.e( + "FLUTTER_HEALTH::ERROR", + "Unable to return steps due to the following exception:" + ) + Log.e("FLUTTER_HEALTH::ERROR", Log.getStackTraceString(e)) + result.success(null) + } + } + } + private fun getHealthConnectSdkStatus(call: MethodCall, result: Result) { checkAvailability() @@ -627,12 +673,32 @@ class HealthPlugin(private var channel: MethodChannel? = null) : healthConnectRequestPermissionsLauncher!!.launch(permList.toSet()) } + /** Filter manually recorded data */ + private fun filterManualEntry( + includeManualEntry: Boolean, + records: List + ): List { + if (includeManualEntry) { + return records + } + + return records.filter { record -> + return@filter isManualEntry(record) + } + } + + private fun isManualEntry(record: Record): Boolean { + return record.metadata.recordingMethod == Metadata.RECORDING_METHOD_MANUAL_ENTRY + } + /** Get all datapoints of the DataType within the given time range */ private fun getData(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 includeManualEntry = call.argument("includeManualEntry")!! + scope.launch { try { mapToType[dataType]?.let { classType -> @@ -656,8 +722,13 @@ class HealthPlugin(private var channel: MethodChannel? = null) : var response = healthConnectClient.readRecords(request) var pageToken = response.pageToken + var filteredRecords = filterManualEntry( + includeManualEntry, + response.records + ) + // Add the records from the initial response to the records list - records.addAll(response.records) + records.addAll(filteredRecords) // Continue making requests and fetching records while there is a // page token @@ -675,7 +746,12 @@ class HealthPlugin(private var channel: MethodChannel? = null) : response = healthConnectClient.readRecords(request) pageToken = response.pageToken - records.addAll(response.records) + filteredRecords = filterManualEntry( + includeManualEntry, + response.records + ) + + records.addAll(filteredRecords) } // Workout needs distance and total calories burned too @@ -959,6 +1035,8 @@ class HealthPlugin(private var channel: MethodChannel? = null) : "source_name" to metadata.dataOrigin .packageName, + "recording_method" to + metadata.recordingMethod ), ) @@ -978,6 +1056,8 @@ class HealthPlugin(private var channel: MethodChannel? = null) : "source_name" to metadata.dataOrigin .packageName, + "recording_method" to + metadata.recordingMethod ), ) @@ -997,6 +1077,8 @@ class HealthPlugin(private var channel: MethodChannel? = null) : "source_name" to metadata.dataOrigin .packageName, + "recording_method" to + metadata.recordingMethod ), ) @@ -1014,6 +1096,8 @@ class HealthPlugin(private var channel: MethodChannel? = null) : "source_name" to metadata.dataOrigin .packageName, + "recording_method" to + metadata.recordingMethod ), ) @@ -1033,6 +1117,8 @@ class HealthPlugin(private var channel: MethodChannel? = null) : "source_name" to metadata.dataOrigin .packageName, + "recording_method" to + metadata.recordingMethod ), ) @@ -1047,6 +1133,8 @@ class HealthPlugin(private var channel: MethodChannel? = null) : "source_name" to metadata.dataOrigin .packageName, + "recording_method" to + metadata.recordingMethod ) } @@ -1065,6 +1153,8 @@ class HealthPlugin(private var channel: MethodChannel? = null) : "source_name" to metadata.dataOrigin .packageName, + "recording_method" to + metadata.recordingMethod ), ) @@ -1084,6 +1174,8 @@ class HealthPlugin(private var channel: MethodChannel? = null) : "source_name" to metadata.dataOrigin .packageName, + "recording_method" to + metadata.recordingMethod ), ) @@ -1103,6 +1195,8 @@ class HealthPlugin(private var channel: MethodChannel? = null) : "source_name" to metadata.dataOrigin .packageName, + "recording_method" to + metadata.recordingMethod ), ) @@ -1128,6 +1222,8 @@ class HealthPlugin(private var channel: MethodChannel? = null) : "source_name" to metadata.dataOrigin .packageName, + "recording_method" to + metadata.recordingMethod ), ) @@ -1147,6 +1243,8 @@ class HealthPlugin(private var channel: MethodChannel? = null) : "source_name" to metadata.dataOrigin .packageName, + "recording_method" to + metadata.recordingMethod ), ) @@ -1166,6 +1264,8 @@ class HealthPlugin(private var channel: MethodChannel? = null) : "source_name" to metadata.dataOrigin .packageName, + "recording_method" to + metadata.recordingMethod ), ) @@ -1185,6 +1285,8 @@ class HealthPlugin(private var channel: MethodChannel? = null) : "source_name" to metadata.dataOrigin .packageName, + "recording_method" to + metadata.recordingMethod ), ) @@ -1204,6 +1306,8 @@ class HealthPlugin(private var channel: MethodChannel? = null) : "source_name" to metadata.dataOrigin .packageName, + "recording_method" to + metadata.recordingMethod ), ) @@ -1223,6 +1327,8 @@ class HealthPlugin(private var channel: MethodChannel? = null) : "source_name" to metadata.dataOrigin .packageName, + "recording_method" to + metadata.recordingMethod ), ) @@ -1242,6 +1348,8 @@ class HealthPlugin(private var channel: MethodChannel? = null) : "source_name" to metadata.dataOrigin .packageName, + "recording_method" to + metadata.recordingMethod ), ) @@ -1264,6 +1372,8 @@ class HealthPlugin(private var channel: MethodChannel? = null) : "source_name" to metadata.dataOrigin .packageName, + "recording_method" to + metadata.recordingMethod ), ) @@ -1282,6 +1392,8 @@ class HealthPlugin(private var channel: MethodChannel? = null) : "source_name" to metadata.dataOrigin .packageName, + "recording_method" to + metadata.recordingMethod ) ) @@ -1299,6 +1411,8 @@ class HealthPlugin(private var channel: MethodChannel? = null) : "source_name" to metadata.dataOrigin .packageName, + "recording_method" to + metadata.recordingMethod ) ) @@ -1316,6 +1430,8 @@ class HealthPlugin(private var channel: MethodChannel? = null) : "source_name" to metadata.dataOrigin .packageName, + "recording_method" to + metadata.recordingMethod ) ) @@ -1379,6 +1495,8 @@ class HealthPlugin(private var channel: MethodChannel? = null) : "source_name" to metadata.dataOrigin .packageName, + "recording_method" to + metadata.recordingMethod ) ) @@ -1392,6 +1510,8 @@ class HealthPlugin(private var channel: MethodChannel? = null) : "source_name" to metadata.dataOrigin .packageName, + "recording_method" to + metadata.recordingMethod ) ) // is ExerciseSessionRecord -> return listOf(mapOf("value" to , diff --git a/packages/health/example/lib/main.dart b/packages/health/example/lib/main.dart index 316222413..fa7ad6b10 100644 --- a/packages/health/example/lib/main.dart +++ b/packages/health/example/lib/main.dart @@ -548,7 +548,7 @@ class _HealthAppState extends State { return ListTile( title: Text("${p.typeString}: ${p.value}"), trailing: Text('${p.unitString}'), - subtitle: Text('${p.dateFrom} - ${p.dateTo}'), + subtitle: Text('${p.dateFrom} - ${p.dateTo}\n${p.recordingMethod}'), ); } if (p.value is WorkoutHealthValue) { @@ -557,7 +557,7 @@ class _HealthAppState extends State { "${p.typeString}: ${(p.value as WorkoutHealthValue).totalEnergyBurned} ${(p.value as WorkoutHealthValue).totalEnergyBurnedUnit?.name}"), trailing: Text( '${(p.value as WorkoutHealthValue).workoutActivityType.name}'), - subtitle: Text('${p.dateFrom} - ${p.dateTo}'), + subtitle: Text('${p.dateFrom} - ${p.dateTo}\n${p.recordingMethod}'), ); } if (p.value is NutritionHealthValue) { @@ -566,13 +566,13 @@ class _HealthAppState extends State { "${p.typeString} ${(p.value as NutritionHealthValue).mealType}: ${(p.value as NutritionHealthValue).name}"), trailing: Text('${(p.value as NutritionHealthValue).calories} kcal'), - subtitle: Text('${p.dateFrom} - ${p.dateTo}'), + subtitle: Text('${p.dateFrom} - ${p.dateTo}\n${p.recordingMethod}'), ); } return ListTile( title: Text("${p.typeString}: ${p.value}"), trailing: Text('${p.unitString}'), - subtitle: Text('${p.dateFrom} - ${p.dateTo}'), + subtitle: Text('${p.dateFrom} - ${p.dateTo}\n${p.recordingMethod}'), ); }); diff --git a/packages/health/lib/health.g.dart b/packages/health/lib/health.g.dart index 89ded9d4c..995fb4261 100644 --- a/packages/health/lib/health.g.dart +++ b/packages/health/lib/health.g.dart @@ -18,7 +18,9 @@ HealthDataPoint _$HealthDataPointFromJson(Map json) => sourceDeviceId: json['source_device_id'] as String, sourceId: json['source_id'] as String, sourceName: json['source_name'] as String, - isManualEntry: json['is_manual_entry'] as bool? ?? false, + recordingMethod: $enumDecodeNullable( + _$RecordingMethodEnumMap, json['recording_method']) ?? + RecordingMethod.unknown, workoutSummary: json['workout_summary'] == null ? null : WorkoutSummary.fromJson( @@ -37,7 +39,7 @@ Map _$HealthDataPointToJson(HealthDataPoint instance) { 'source_device_id': instance.sourceDeviceId, 'source_id': instance.sourceId, 'source_name': instance.sourceName, - 'is_manual_entry': instance.isManualEntry, + 'recording_method': _$RecordingMethodEnumMap[instance.recordingMethod]!, }; void writeNotNull(String key, dynamic value) { @@ -209,6 +211,13 @@ const _$HealthPlatformTypeEnumMap = { HealthPlatformType.googleHealthConnect: 'googleHealthConnect', }; +const _$RecordingMethodEnumMap = { + RecordingMethod.active: 'active', + RecordingMethod.automatic: 'automatic', + RecordingMethod.manual: 'manual', + RecordingMethod.unknown: 'unknown', +}; + HealthValue _$HealthValueFromJson(Map json) => HealthValue()..$type = json['__type'] as String?; @@ -318,103 +327,104 @@ const _$HealthWorkoutActivityTypeEnumMap = { HealthWorkoutActivityType.ARCHERY: 'ARCHERY', HealthWorkoutActivityType.AUSTRALIAN_FOOTBALL: 'AUSTRALIAN_FOOTBALL', HealthWorkoutActivityType.BADMINTON: 'BADMINTON', - HealthWorkoutActivityType.BARRE: 'BARRE', HealthWorkoutActivityType.BASEBALL: 'BASEBALL', HealthWorkoutActivityType.BASKETBALL: 'BASKETBALL', - HealthWorkoutActivityType.BIKING_STATIONARY: 'BIKING_STATIONARY', HealthWorkoutActivityType.BIKING: 'BIKING', - HealthWorkoutActivityType.BOWLING: 'BOWLING', HealthWorkoutActivityType.BOXING: 'BOXING', - HealthWorkoutActivityType.CALISTHENICS: 'CALISTHENICS', - HealthWorkoutActivityType.CARDIO_DANCE: 'CARDIO_DANCE', - HealthWorkoutActivityType.CLIMBING: 'CLIMBING', - HealthWorkoutActivityType.COOLDOWN: 'COOLDOWN', - HealthWorkoutActivityType.CORE_TRAINING: 'CORE_TRAINING', HealthWorkoutActivityType.CRICKET: 'CRICKET', HealthWorkoutActivityType.CROSS_COUNTRY_SKIING: 'CROSS_COUNTRY_SKIING', - HealthWorkoutActivityType.CROSS_TRAINING: 'CROSS_TRAINING', HealthWorkoutActivityType.CURLING: 'CURLING', - HealthWorkoutActivityType.DANCING: 'DANCING', - HealthWorkoutActivityType.DISC_SPORTS: 'DISC_SPORTS', HealthWorkoutActivityType.DOWNHILL_SKIING: 'DOWNHILL_SKIING', HealthWorkoutActivityType.ELLIPTICAL: 'ELLIPTICAL', - HealthWorkoutActivityType.EQUESTRIAN_SPORTS: 'EQUESTRIAN_SPORTS', HealthWorkoutActivityType.FENCING: 'FENCING', - HealthWorkoutActivityType.FISHING: 'FISHING', - HealthWorkoutActivityType.FITNESS_GAMING: 'FITNESS_GAMING', - HealthWorkoutActivityType.FLEXIBILITY: 'FLEXIBILITY', - HealthWorkoutActivityType.FRISBEE_DISC: 'FRISBEE_DISC', - HealthWorkoutActivityType.FUNCTIONAL_STRENGTH_TRAINING: - 'FUNCTIONAL_STRENGTH_TRAINING', HealthWorkoutActivityType.GOLF: 'GOLF', - HealthWorkoutActivityType.GUIDED_BREATHING: 'GUIDED_BREATHING', HealthWorkoutActivityType.GYMNASTICS: 'GYMNASTICS', - HealthWorkoutActivityType.HAND_CYCLING: 'HAND_CYCLING', HealthWorkoutActivityType.HANDBALL: 'HANDBALL', HealthWorkoutActivityType.HIGH_INTENSITY_INTERVAL_TRAINING: 'HIGH_INTENSITY_INTERVAL_TRAINING', HealthWorkoutActivityType.HIKING: 'HIKING', HealthWorkoutActivityType.HOCKEY: 'HOCKEY', - HealthWorkoutActivityType.HUNTING: 'HUNTING', - HealthWorkoutActivityType.ICE_SKATING: 'ICE_SKATING', HealthWorkoutActivityType.JUMP_ROPE: 'JUMP_ROPE', HealthWorkoutActivityType.KICKBOXING: 'KICKBOXING', - HealthWorkoutActivityType.LACROSSE: 'LACROSSE', HealthWorkoutActivityType.MARTIAL_ARTS: 'MARTIAL_ARTS', - HealthWorkoutActivityType.MIND_AND_BODY: 'MIND_AND_BODY', - HealthWorkoutActivityType.MIXED_CARDIO: 'MIXED_CARDIO', - HealthWorkoutActivityType.PADDLE_SPORTS: 'PADDLE_SPORTS', - HealthWorkoutActivityType.PARAGLIDING: 'PARAGLIDING', - HealthWorkoutActivityType.PICKLEBALL: 'PICKLEBALL', HealthWorkoutActivityType.PILATES: 'PILATES', - HealthWorkoutActivityType.PLAY: 'PLAY', - HealthWorkoutActivityType.PREPARATION_AND_RECOVERY: - 'PREPARATION_AND_RECOVERY', HealthWorkoutActivityType.RACQUETBALL: 'RACQUETBALL', - HealthWorkoutActivityType.ROCK_CLIMBING: 'ROCK_CLIMBING', - HealthWorkoutActivityType.ROWING_MACHINE: 'ROWING_MACHINE', HealthWorkoutActivityType.ROWING: 'ROWING', HealthWorkoutActivityType.RUGBY: 'RUGBY', - HealthWorkoutActivityType.RUNNING_TREADMILL: 'RUNNING_TREADMILL', HealthWorkoutActivityType.RUNNING: 'RUNNING', HealthWorkoutActivityType.SAILING: 'SAILING', - HealthWorkoutActivityType.SCUBA_DIVING: 'SCUBA_DIVING', HealthWorkoutActivityType.SKATING: 'SKATING', - HealthWorkoutActivityType.SKIING: 'SKIING', - HealthWorkoutActivityType.SNOW_SPORTS: 'SNOW_SPORTS', HealthWorkoutActivityType.SNOWBOARDING: 'SNOWBOARDING', - HealthWorkoutActivityType.SNOWSHOEING: 'SNOWSHOEING', HealthWorkoutActivityType.SOCCER: 'SOCCER', - HealthWorkoutActivityType.SOCIAL_DANCE: 'SOCIAL_DANCE', HealthWorkoutActivityType.SOFTBALL: 'SOFTBALL', HealthWorkoutActivityType.SQUASH: 'SQUASH', - HealthWorkoutActivityType.STAIR_CLIMBING_MACHINE: 'STAIR_CLIMBING_MACHINE', HealthWorkoutActivityType.STAIR_CLIMBING: 'STAIR_CLIMBING', + HealthWorkoutActivityType.SWIMMING: 'SWIMMING', + HealthWorkoutActivityType.TABLE_TENNIS: 'TABLE_TENNIS', + HealthWorkoutActivityType.TENNIS: 'TENNIS', + HealthWorkoutActivityType.VOLLEYBALL: 'VOLLEYBALL', + HealthWorkoutActivityType.WALKING: 'WALKING', + HealthWorkoutActivityType.WATER_POLO: 'WATER_POLO', + HealthWorkoutActivityType.YOGA: 'YOGA', + HealthWorkoutActivityType.BARRE: 'BARRE', + HealthWorkoutActivityType.BOWLING: 'BOWLING', + HealthWorkoutActivityType.CARDIO_DANCE: 'CARDIO_DANCE', + HealthWorkoutActivityType.CLIMBING: 'CLIMBING', + HealthWorkoutActivityType.COOLDOWN: 'COOLDOWN', + HealthWorkoutActivityType.CORE_TRAINING: 'CORE_TRAINING', + HealthWorkoutActivityType.CROSS_TRAINING: 'CROSS_TRAINING', + HealthWorkoutActivityType.DISC_SPORTS: 'DISC_SPORTS', + HealthWorkoutActivityType.EQUESTRIAN_SPORTS: 'EQUESTRIAN_SPORTS', + HealthWorkoutActivityType.FISHING: 'FISHING', + HealthWorkoutActivityType.FITNESS_GAMING: 'FITNESS_GAMING', + HealthWorkoutActivityType.FLEXIBILITY: 'FLEXIBILITY', + HealthWorkoutActivityType.FUNCTIONAL_STRENGTH_TRAINING: + 'FUNCTIONAL_STRENGTH_TRAINING', + HealthWorkoutActivityType.HAND_CYCLING: 'HAND_CYCLING', + HealthWorkoutActivityType.HUNTING: 'HUNTING', + HealthWorkoutActivityType.LACROSSE: 'LACROSSE', + HealthWorkoutActivityType.MIND_AND_BODY: 'MIND_AND_BODY', + HealthWorkoutActivityType.MIXED_CARDIO: 'MIXED_CARDIO', + HealthWorkoutActivityType.PADDLE_SPORTS: 'PADDLE_SPORTS', + HealthWorkoutActivityType.PICKLEBALL: 'PICKLEBALL', + HealthWorkoutActivityType.PLAY: 'PLAY', + HealthWorkoutActivityType.PREPARATION_AND_RECOVERY: + 'PREPARATION_AND_RECOVERY', + HealthWorkoutActivityType.SNOW_SPORTS: 'SNOW_SPORTS', + HealthWorkoutActivityType.SOCIAL_DANCE: 'SOCIAL_DANCE', HealthWorkoutActivityType.STAIRS: 'STAIRS', HealthWorkoutActivityType.STEP_TRAINING: 'STEP_TRAINING', - HealthWorkoutActivityType.STRENGTH_TRAINING: 'STRENGTH_TRAINING', HealthWorkoutActivityType.SURFING_SPORTS: 'SURFING_SPORTS', - HealthWorkoutActivityType.SURFING: 'SURFING', - HealthWorkoutActivityType.SWIMMING_OPEN_WATER: 'SWIMMING_OPEN_WATER', - HealthWorkoutActivityType.SWIMMING_POOL: 'SWIMMING_POOL', - HealthWorkoutActivityType.SWIMMING: 'SWIMMING', - HealthWorkoutActivityType.TABLE_TENNIS: 'TABLE_TENNIS', HealthWorkoutActivityType.TAI_CHI: 'TAI_CHI', - HealthWorkoutActivityType.TENNIS: 'TENNIS', HealthWorkoutActivityType.TRACK_AND_FIELD: 'TRACK_AND_FIELD', HealthWorkoutActivityType.TRADITIONAL_STRENGTH_TRAINING: 'TRADITIONAL_STRENGTH_TRAINING', - HealthWorkoutActivityType.VOLLEYBALL: 'VOLLEYBALL', - HealthWorkoutActivityType.WALKING: 'WALKING', HealthWorkoutActivityType.WATER_FITNESS: 'WATER_FITNESS', - HealthWorkoutActivityType.WATER_POLO: 'WATER_POLO', HealthWorkoutActivityType.WATER_SPORTS: 'WATER_SPORTS', - HealthWorkoutActivityType.WEIGHTLIFTING: 'WEIGHTLIFTING', HealthWorkoutActivityType.WHEELCHAIR_RUN_PACE: 'WHEELCHAIR_RUN_PACE', HealthWorkoutActivityType.WHEELCHAIR_WALK_PACE: 'WHEELCHAIR_WALK_PACE', - HealthWorkoutActivityType.WHEELCHAIR: 'WHEELCHAIR', HealthWorkoutActivityType.WRESTLING: 'WRESTLING', - HealthWorkoutActivityType.YOGA: 'YOGA', + HealthWorkoutActivityType.BIKING_STATIONARY: 'BIKING_STATIONARY', + HealthWorkoutActivityType.CALISTHENICS: 'CALISTHENICS', + HealthWorkoutActivityType.DANCING: 'DANCING', + HealthWorkoutActivityType.FRISBEE_DISC: 'FRISBEE_DISC', + HealthWorkoutActivityType.GUIDED_BREATHING: 'GUIDED_BREATHING', + HealthWorkoutActivityType.ICE_SKATING: 'ICE_SKATING', + HealthWorkoutActivityType.PARAGLIDING: 'PARAGLIDING', + HealthWorkoutActivityType.ROCK_CLIMBING: 'ROCK_CLIMBING', + HealthWorkoutActivityType.ROWING_MACHINE: 'ROWING_MACHINE', + HealthWorkoutActivityType.RUNNING_TREADMILL: 'RUNNING_TREADMILL', + HealthWorkoutActivityType.SCUBA_DIVING: 'SCUBA_DIVING', + HealthWorkoutActivityType.SKIING: 'SKIING', + HealthWorkoutActivityType.SNOWSHOEING: 'SNOWSHOEING', + HealthWorkoutActivityType.STAIR_CLIMBING_MACHINE: 'STAIR_CLIMBING_MACHINE', + HealthWorkoutActivityType.STRENGTH_TRAINING: 'STRENGTH_TRAINING', + HealthWorkoutActivityType.SURFING: 'SURFING', + HealthWorkoutActivityType.SWIMMING_OPEN_WATER: 'SWIMMING_OPEN_WATER', + HealthWorkoutActivityType.SWIMMING_POOL: 'SWIMMING_POOL', + HealthWorkoutActivityType.WALKING_TREADMILL: 'WALKING_TREADMILL', + HealthWorkoutActivityType.WEIGHTLIFTING: 'WEIGHTLIFTING', + HealthWorkoutActivityType.WHEELCHAIR: 'WHEELCHAIR', HealthWorkoutActivityType.OTHER: 'OTHER', }; diff --git a/packages/health/lib/src/health_data_point.dart b/packages/health/lib/src/health_data_point.dart index b98734c5a..187a6024f 100644 --- a/packages/health/lib/src/health_data_point.dart +++ b/packages/health/lib/src/health_data_point.dart @@ -3,6 +3,8 @@ part of '../health.dart'; /// Types of health platforms. enum HealthPlatformType { appleHealth, googleHealthConnect } +enum RecordingMethod { active, automatic, manual, unknown } + /// A [HealthDataPoint] object corresponds to a data point capture from /// Apple HealthKit or Google Health Connect with a [HealthValue] /// as value. @@ -41,8 +43,10 @@ class HealthDataPoint { /// The name of the source from which the data point was fetched. String sourceName; - /// The user entered state of the data point. - bool isManualEntry; + /// How the data point was recorded + /// (on Android: https://developer.android.com/reference/kotlin/androidx/health/connect/client/records/metadata/Metadata#summary) + /// on iOS: either user entered or manual https://developer.apple.com/documentation/healthkit/hkmetadatakeywasuserentered) + RecordingMethod recordingMethod; /// The summary of the workout data point, if available. WorkoutSummary? workoutSummary; @@ -60,7 +64,7 @@ class HealthDataPoint { required this.sourceDeviceId, required this.sourceId, required this.sourceName, - this.isManualEntry = false, + this.recordingMethod = RecordingMethod.unknown, this.workoutSummary, this.metadata, }) { @@ -124,7 +128,6 @@ class HealthDataPoint { DateTime.fromMillisecondsSinceEpoch(dataPoint['date_to'] as int); final String sourceId = dataPoint["source_id"] as String; final String sourceName = dataPoint["source_name"] as String; - final bool isManualEntry = dataPoint["is_manual_entry"] as bool? ?? false; final Map? metadata = dataPoint["metadata"] == null ? null : Map.from(dataPoint['metadata'] as Map); @@ -139,6 +142,8 @@ class HealthDataPoint { workoutSummary = WorkoutSummary.fromHealthDataPoint(dataPoint); } + var recordingMethod = dataPoint["recording_method"] as int?; + return HealthDataPoint( value: value, type: dataType, @@ -149,12 +154,28 @@ class HealthDataPoint { sourceDeviceId: Health().deviceId, sourceId: sourceId, sourceName: sourceName, - isManualEntry: isManualEntry, + recordingMethod: _alignRecordingMethod(recordingMethod), workoutSummary: workoutSummary, metadata: metadata, ); } + /// align recording method with the platform + static RecordingMethod _alignRecordingMethod(int? recordingMethod) { + switch (recordingMethod) { + case 0: + return RecordingMethod.unknown; + case 1: + return RecordingMethod.active; + case 2: + return RecordingMethod.automatic; + case 3: + return RecordingMethod.manual; + default: + return RecordingMethod.unknown; + } + } + @override String toString() => """$runtimeType - value: ${value.toString()}, @@ -166,7 +187,7 @@ class HealthDataPoint { deviceId: $sourceDeviceId, sourceId: $sourceId, sourceName: $sourceName - isManualEntry: $isManualEntry + recordingMethod: $recordingMethod workoutSummary: $workoutSummary metadata: $metadata"""; @@ -182,7 +203,7 @@ class HealthDataPoint { sourceDeviceId == other.sourceDeviceId && sourceId == other.sourceId && sourceName == other.sourceName && - isManualEntry == other.isManualEntry && + recordingMethod == other.recordingMethod && metadata == other.metadata; @override diff --git a/packages/health/lib/src/health_plugin.dart b/packages/health/lib/src/health_plugin.dart index 87de5bcba..62675f657 100644 --- a/packages/health/lib/src/health_plugin.dart +++ b/packages/health/lib/src/health_plugin.dart @@ -274,7 +274,7 @@ class Health { sourceDeviceId: _deviceId!, sourceId: '', sourceName: '', - isManualEntry: !includeManualEntry, + recordingMethod: RecordingMethod.unknown, ); bmiHealthPoints.add(x); From fa69767c88bea448ae6cd6736c79a703da2d3113 Mon Sep 17 00:00:00 2001 From: Aamir Farooq Date: Wed, 14 Aug 2024 16:46:23 +0200 Subject: [PATCH 20/32] Support writing data with custom recording method on Android --- .../cachet/plugins/health/HealthPlugin.kt | 95 +++++++++++++++++++ packages/health/lib/health.g.dart | 2 +- .../health/lib/src/health_data_point.dart | 7 +- packages/health/lib/src/health_plugin.dart | 23 ++++- 4 files changed, 123 insertions(+), 4 deletions(-) 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 9093083f6..0e014afdc 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 @@ -1534,6 +1534,8 @@ class HealthPlugin(private var channel: MethodChannel? = null) : val startTime = call.argument("startTime")!! val endTime = call.argument("endTime")!! val value = call.argument("value")!! + val recordingMethod = call.argument("recordingMethod")!! + val record = when (type) { BODY_FAT_PERCENTAGE -> @@ -1547,6 +1549,9 @@ class HealthPlugin(private var channel: MethodChannel? = null) : value ), zoneOffset = null, + metadata = Metadata( + recordingMethod = recordingMethod, + ), ) HEIGHT -> @@ -1560,6 +1565,9 @@ class HealthPlugin(private var channel: MethodChannel? = null) : value ), zoneOffset = null, + metadata = Metadata( + recordingMethod = recordingMethod, + ), ) WEIGHT -> @@ -1573,6 +1581,9 @@ class HealthPlugin(private var channel: MethodChannel? = null) : value ), zoneOffset = null, + metadata = Metadata( + recordingMethod = recordingMethod, + ), ) STEPS -> @@ -1588,6 +1599,9 @@ class HealthPlugin(private var channel: MethodChannel? = null) : count = value.toLong(), startZoneOffset = null, endZoneOffset = null, + metadata = Metadata( + recordingMethod = recordingMethod, + ), ) ACTIVE_ENERGY_BURNED -> @@ -1606,6 +1620,9 @@ class HealthPlugin(private var channel: MethodChannel? = null) : ), startZoneOffset = null, endZoneOffset = null, + metadata = Metadata( + recordingMethod = recordingMethod, + ), ) HEART_RATE -> @@ -1631,6 +1648,9 @@ class HealthPlugin(private var channel: MethodChannel? = null) : ), startZoneOffset = null, endZoneOffset = null, + metadata = Metadata( + recordingMethod = recordingMethod, + ), ) BODY_TEMPERATURE -> @@ -1644,6 +1664,9 @@ class HealthPlugin(private var channel: MethodChannel? = null) : value ), zoneOffset = null, + metadata = Metadata( + recordingMethod = recordingMethod, + ), ) BODY_WATER_MASS -> @@ -1657,6 +1680,9 @@ class HealthPlugin(private var channel: MethodChannel? = null) : value ), zoneOffset = null, + metadata = Metadata( + recordingMethod = recordingMethod, + ), ) BLOOD_OXYGEN -> @@ -1670,6 +1696,9 @@ class HealthPlugin(private var channel: MethodChannel? = null) : value ), zoneOffset = null, + metadata = Metadata( + recordingMethod = recordingMethod, + ), ) BLOOD_GLUCOSE -> @@ -1683,6 +1712,9 @@ class HealthPlugin(private var channel: MethodChannel? = null) : value ), zoneOffset = null, + metadata = Metadata( + recordingMethod = recordingMethod, + ), ) HEART_RATE_VARIABILITY_RMSSD -> @@ -1695,6 +1727,9 @@ class HealthPlugin(private var channel: MethodChannel? = null) : value, zoneOffset = null, + metadata = Metadata( + recordingMethod = recordingMethod, + ), ) DISTANCE_DELTA -> @@ -1713,6 +1748,9 @@ class HealthPlugin(private var channel: MethodChannel? = null) : ), startZoneOffset = null, endZoneOffset = null, + metadata = Metadata( + recordingMethod = recordingMethod, + ), ) WATER -> @@ -1731,6 +1769,9 @@ class HealthPlugin(private var channel: MethodChannel? = null) : ), startZoneOffset = null, endZoneOffset = null, + metadata = Metadata( + recordingMethod = recordingMethod, + ), ) SLEEP_ASLEEP -> @@ -1759,6 +1800,9 @@ class HealthPlugin(private var channel: MethodChannel? = null) : .STAGE_TYPE_SLEEPING ) ), + metadata = Metadata( + recordingMethod = recordingMethod, + ), ) SLEEP_LIGHT -> @@ -1787,6 +1831,9 @@ class HealthPlugin(private var channel: MethodChannel? = null) : .STAGE_TYPE_LIGHT ) ), + metadata = Metadata( + recordingMethod = recordingMethod, + ), ) SLEEP_DEEP -> @@ -1815,6 +1862,9 @@ class HealthPlugin(private var channel: MethodChannel? = null) : .STAGE_TYPE_DEEP ) ), + metadata = Metadata( + recordingMethod = recordingMethod, + ), ) SLEEP_REM -> @@ -1843,6 +1893,9 @@ class HealthPlugin(private var channel: MethodChannel? = null) : .STAGE_TYPE_REM ) ), + metadata = Metadata( + recordingMethod = recordingMethod, + ), ) SLEEP_OUT_OF_BED -> @@ -1871,6 +1924,9 @@ class HealthPlugin(private var channel: MethodChannel? = null) : .STAGE_TYPE_OUT_OF_BED ) ), + metadata = Metadata( + recordingMethod = recordingMethod, + ), ) SLEEP_AWAKE -> @@ -1899,6 +1955,9 @@ class HealthPlugin(private var channel: MethodChannel? = null) : .STAGE_TYPE_AWAKE ) ), + metadata = Metadata( + recordingMethod = recordingMethod, + ), ) SLEEP_SESSION -> @@ -1913,6 +1972,9 @@ class HealthPlugin(private var channel: MethodChannel? = null) : ), startZoneOffset = null, endZoneOffset = null, + metadata = Metadata( + recordingMethod = recordingMethod, + ), ) RESTING_HEART_RATE -> @@ -1924,6 +1986,9 @@ class HealthPlugin(private var channel: MethodChannel? = null) : beatsPerMinute = value.toLong(), zoneOffset = null, + metadata = Metadata( + recordingMethod = recordingMethod, + ), ) BASAL_ENERGY_BURNED -> @@ -1937,6 +2002,9 @@ class HealthPlugin(private var channel: MethodChannel? = null) : value ), zoneOffset = null, + metadata = Metadata( + recordingMethod = recordingMethod, + ), ) FLIGHTS_CLIMBED -> @@ -1952,6 +2020,9 @@ class HealthPlugin(private var channel: MethodChannel? = null) : floors = value, startZoneOffset = null, endZoneOffset = null, + metadata = Metadata( + recordingMethod = recordingMethod, + ), ) RESPIRATORY_RATE -> @@ -1962,6 +2033,9 @@ class HealthPlugin(private var channel: MethodChannel? = null) : ), rate = value, zoneOffset = null, + metadata = Metadata( + recordingMethod = recordingMethod, + ), ) // AGGREGATE_STEP_COUNT -> StepsRecord() TOTAL_CALORIES_BURNED -> @@ -1980,12 +2054,18 @@ class HealthPlugin(private var channel: MethodChannel? = null) : ), startZoneOffset = null, endZoneOffset = null, + metadata = Metadata( + recordingMethod = recordingMethod, + ), ) MENSTRUATION_FLOW -> MenstruationFlowRecord( time = Instant.ofEpochMilli(startTime), flow = value.toInt(), zoneOffset = null, + metadata = Metadata( + recordingMethod = recordingMethod, + ), ) BLOOD_PRESSURE_SYSTOLIC -> @@ -2030,6 +2110,8 @@ class HealthPlugin(private var channel: MethodChannel? = null) : val endTime = Instant.ofEpochMilli(call.argument("endTime")!!) val totalEnergyBurned = call.argument("totalEnergyBurned") val totalDistance = call.argument("totalDistance") + val recordingMethod = call.argument("recordingMethod")!! + if (!workoutTypeMap.containsKey(type)) { result.success(false) Log.w( @@ -2052,6 +2134,9 @@ class HealthPlugin(private var channel: MethodChannel? = null) : endZoneOffset = null, exerciseType = workoutType, title = title, + metadata = Metadata( + recordingMethod = recordingMethod, + ), ), ) if (totalDistance != null) { @@ -2065,6 +2150,9 @@ class HealthPlugin(private var channel: MethodChannel? = null) : Length.meters( totalDistance.toDouble() ), + metadata = Metadata( + recordingMethod = recordingMethod, + ), ), ) } @@ -2080,6 +2168,9 @@ class HealthPlugin(private var channel: MethodChannel? = null) : totalEnergyBurned .toDouble() ), + metadata = Metadata( + recordingMethod = recordingMethod, + ), ), ) } @@ -2108,6 +2199,7 @@ class HealthPlugin(private var channel: MethodChannel? = null) : val systolic = call.argument("systolic")!! val diastolic = call.argument("diastolic")!! val startTime = Instant.ofEpochMilli(call.argument("startTime")!!) + val recordingMethod = call.argument("recordingMethod")!! scope.launch { try { @@ -2124,6 +2216,9 @@ class HealthPlugin(private var channel: MethodChannel? = null) : diastolic ), zoneOffset = null, + metadata = Metadata( + recordingMethod = recordingMethod, + ), ), ), ) diff --git a/packages/health/lib/health.g.dart b/packages/health/lib/health.g.dart index 995fb4261..ccf6c8ddc 100644 --- a/packages/health/lib/health.g.dart +++ b/packages/health/lib/health.g.dart @@ -212,10 +212,10 @@ const _$HealthPlatformTypeEnumMap = { }; const _$RecordingMethodEnumMap = { + RecordingMethod.unknown: 'unknown', RecordingMethod.active: 'active', RecordingMethod.automatic: 'automatic', RecordingMethod.manual: 'manual', - RecordingMethod.unknown: 'unknown', }; HealthValue _$HealthValueFromJson(Map json) => diff --git a/packages/health/lib/src/health_data_point.dart b/packages/health/lib/src/health_data_point.dart index 187a6024f..f10d52694 100644 --- a/packages/health/lib/src/health_data_point.dart +++ b/packages/health/lib/src/health_data_point.dart @@ -3,7 +3,12 @@ part of '../health.dart'; /// Types of health platforms. enum HealthPlatformType { appleHealth, googleHealthConnect } -enum RecordingMethod { active, automatic, manual, unknown } +enum RecordingMethod { + unknown, + active, + automatic, + manual; +} /// A [HealthDataPoint] object corresponds to a data point capture from /// Apple HealthKit or Google Health Connect with a [HealthValue] diff --git a/packages/health/lib/src/health_plugin.dart b/packages/health/lib/src/health_plugin.dart index 62675f657..3f534e5dd 100644 --- a/packages/health/lib/src/health_plugin.dart +++ b/packages/health/lib/src/health_plugin.dart @@ -305,6 +305,7 @@ class Health { required HealthDataType type, required DateTime startTime, DateTime? endTime, + RecordingMethod recordingMethod = RecordingMethod.unknown, }) async { if (type == HealthDataType.WORKOUT) { throw ArgumentError( @@ -351,7 +352,8 @@ class Health { 'dataTypeKey': type.name, 'dataUnitKey': unit.name, 'startTime': startTime.millisecondsSinceEpoch, - 'endTime': endTime.millisecondsSinceEpoch + 'endTime': endTime.millisecondsSinceEpoch, + 'recordingMethod': recordingMethod.index, }; bool? success = await _channel.invokeMethod('writeData', args); return success ?? false; @@ -367,6 +369,7 @@ class Health { /// Must be equal to or earlier than [endTime]. /// * [endTime] - the end time when this [value] is measured. /// Must be equal to or later than [startTime]. + /// * [recordingMethod] - the recording method of the data point. Future delete({ required HealthDataType type, required DateTime startTime, @@ -399,11 +402,13 @@ class Health { /// Must be equal to or later than [startTime]. /// Simply set [endTime] equal to [startTime] if the blood pressure is measured /// only at a specific point in time. If omitted, [endTime] is set to [startTime]. + /// * [recordingMethod] - the recording method of the data point. Future writeBloodPressure({ required int systolic, required int diastolic, required DateTime startTime, DateTime? endTime, + RecordingMethod recordingMethod = RecordingMethod.unknown, }) async { endTime ??= startTime; if (startTime.isAfter(endTime)) { @@ -414,7 +419,8 @@ class Health { 'systolic': systolic, 'diastolic': diastolic, 'startTime': startTime.millisecondsSinceEpoch, - 'endTime': endTime.millisecondsSinceEpoch + 'endTime': endTime.millisecondsSinceEpoch, + 'recordingMethod': recordingMethod.index, }; return await _channel.invokeMethod('writeBloodPressure', args) == true; } @@ -431,10 +437,12 @@ class Health { /// Must be equal to or later than [startTime]. /// Simply set [endTime] equal to [startTime] if the blood oxygen saturation /// is measured only at a specific point in time (default). + /// * [recordingMethod] - the recording method of the data point. Future writeBloodOxygen({ required double saturation, required DateTime startTime, DateTime? endTime, + RecordingMethod recordingMethod = RecordingMethod.unknown, }) async { endTime ??= startTime; if (startTime.isAfter(endTime)) { @@ -454,6 +462,7 @@ class Health { 'startTime': startTime.millisecondsSinceEpoch, 'endTime': endTime.millisecondsSinceEpoch, 'dataTypeKey': HealthDataType.BLOOD_OXYGEN.name, + 'recordingMethod': recordingMethod.index, }; success = await _channel.invokeMethod('writeBloodOxygen', args); } @@ -512,6 +521,7 @@ class Health { /// * [sugar] - optional sugar information. /// * [water] - optional water information. /// * [zinc] - optional zinc information. + /// * [recordingMethod] - the recording method of the data point. Future writeMeal({ required MealType mealType, required DateTime startTime, @@ -558,6 +568,7 @@ class Health { double? sugar, double? water, double? zinc, + RecordingMethod recordingMethod = RecordingMethod.unknown, }) async { if (startTime.isAfter(endTime)) { throw ArgumentError("startTime must be equal or earlier than endTime"); @@ -609,6 +620,7 @@ class Health { 'sugar': sugar, 'water': water, 'zinc': zinc, + 'recordingMethod': recordingMethod.index, }; bool? success = await _channel.invokeMethod('writeMeal', args); return success ?? false; @@ -624,11 +636,13 @@ class Health { /// * [endTime] - the start time when the menstrual flow is measured. /// * [isStartOfCycle] - A bool that indicates whether the sample represents /// the start of a menstrual cycle. + /// * [recordingMethod] - the recording method of the data point. Future writeMenstruationFlow({ required MenstrualFlow flow, required DateTime startTime, required DateTime endTime, required bool isStartOfCycle, + RecordingMethod recordingMethod = RecordingMethod.unknown, }) async { var value = Platform.isAndroid ? MenstrualFlow.toHealthConnect(flow) : flow.index; @@ -644,6 +658,7 @@ class Health { 'endTime': endTime.millisecondsSinceEpoch, 'isStartOfCycle': isStartOfCycle, 'dataTypeKey': HealthDataType.MENSTRUATION_FLOW.name, + 'recordingMethod': recordingMethod.index, }; return await _channel.invokeMethod('writeMenstruationFlow', args) == true; } @@ -664,6 +679,7 @@ class Health { /// only at a specific point in time (default). /// * [metadata] - optional map of keys, both HKMetadataKeyExternalUUID /// and HKMetadataKeyDeviceName are required + /// * [recordingMethod] - the recording method of the data point. Future writeAudiogram({ required List frequencies, required List leftEarSensitivities, @@ -1022,6 +1038,7 @@ class Health { /// *ONLY FOR IOS* Default value is METER. /// - [title] The title of the workout. /// *ONLY FOR HEALTH CONNECT* Default value is the [activityType], e.g. "STRENGTH_TRAINING". + /// - [recorrdingMethod] The recording method of the data point. Future writeWorkoutData({ required HealthWorkoutActivityType activityType, required DateTime start, @@ -1031,6 +1048,7 @@ class Health { int? totalDistance, HealthDataUnit totalDistanceUnit = HealthDataUnit.METER, String? title, + RecordingMethod recordingMethod = RecordingMethod.unknown, }) async { // Check that value is on the current Platform if (Platform.isIOS && !_isOnIOS(activityType)) { @@ -1049,6 +1067,7 @@ class Health { 'totalDistance': totalDistance, 'totalDistanceUnit': totalDistanceUnit.name, 'title': title, + 'recordingMethod': recordingMethod.index, }; return await _channel.invokeMethod('writeWorkoutData', args) == true; } From cedfd0bc2db6a756ed3fdcd64add992b80191768 Mon Sep 17 00:00:00 2001 From: Aamir Farooq Date: Wed, 14 Aug 2024 16:52:23 +0200 Subject: [PATCH 21/32] Improve RecordingMethod enum --- .../health/lib/src/health_data_point.dart | 25 +----------- packages/health/lib/src/health_plugin.dart | 12 +++--- .../health/lib/src/health_value_types.dart | 40 +++++++++++++++++++ 3 files changed, 47 insertions(+), 30 deletions(-) diff --git a/packages/health/lib/src/health_data_point.dart b/packages/health/lib/src/health_data_point.dart index f10d52694..aa1bc7765 100644 --- a/packages/health/lib/src/health_data_point.dart +++ b/packages/health/lib/src/health_data_point.dart @@ -3,13 +3,6 @@ part of '../health.dart'; /// Types of health platforms. enum HealthPlatformType { appleHealth, googleHealthConnect } -enum RecordingMethod { - unknown, - active, - automatic, - manual; -} - /// A [HealthDataPoint] object corresponds to a data point capture from /// Apple HealthKit or Google Health Connect with a [HealthValue] /// as value. @@ -159,28 +152,12 @@ class HealthDataPoint { sourceDeviceId: Health().deviceId, sourceId: sourceId, sourceName: sourceName, - recordingMethod: _alignRecordingMethod(recordingMethod), + recordingMethod: RecordingMethod.fromInt(recordingMethod), workoutSummary: workoutSummary, metadata: metadata, ); } - /// align recording method with the platform - static RecordingMethod _alignRecordingMethod(int? recordingMethod) { - switch (recordingMethod) { - case 0: - return RecordingMethod.unknown; - case 1: - return RecordingMethod.active; - case 2: - return RecordingMethod.automatic; - case 3: - return RecordingMethod.manual; - default: - return RecordingMethod.unknown; - } - } - @override String toString() => """$runtimeType - value: ${value.toString()}, diff --git a/packages/health/lib/src/health_plugin.dart b/packages/health/lib/src/health_plugin.dart index 3f534e5dd..31fab3941 100644 --- a/packages/health/lib/src/health_plugin.dart +++ b/packages/health/lib/src/health_plugin.dart @@ -353,7 +353,7 @@ class Health { 'dataUnitKey': unit.name, 'startTime': startTime.millisecondsSinceEpoch, 'endTime': endTime.millisecondsSinceEpoch, - 'recordingMethod': recordingMethod.index, + 'recordingMethod': recordingMethod.toInt(), }; bool? success = await _channel.invokeMethod('writeData', args); return success ?? false; @@ -420,7 +420,7 @@ class Health { 'diastolic': diastolic, 'startTime': startTime.millisecondsSinceEpoch, 'endTime': endTime.millisecondsSinceEpoch, - 'recordingMethod': recordingMethod.index, + 'recordingMethod': recordingMethod.toInt(), }; return await _channel.invokeMethod('writeBloodPressure', args) == true; } @@ -462,7 +462,7 @@ class Health { 'startTime': startTime.millisecondsSinceEpoch, 'endTime': endTime.millisecondsSinceEpoch, 'dataTypeKey': HealthDataType.BLOOD_OXYGEN.name, - 'recordingMethod': recordingMethod.index, + 'recordingMethod': recordingMethod.toInt(), }; success = await _channel.invokeMethod('writeBloodOxygen', args); } @@ -620,7 +620,7 @@ class Health { 'sugar': sugar, 'water': water, 'zinc': zinc, - 'recordingMethod': recordingMethod.index, + 'recordingMethod': recordingMethod.toInt(), }; bool? success = await _channel.invokeMethod('writeMeal', args); return success ?? false; @@ -658,7 +658,7 @@ class Health { 'endTime': endTime.millisecondsSinceEpoch, 'isStartOfCycle': isStartOfCycle, 'dataTypeKey': HealthDataType.MENSTRUATION_FLOW.name, - 'recordingMethod': recordingMethod.index, + 'recordingMethod': recordingMethod.toInt(), }; return await _channel.invokeMethod('writeMenstruationFlow', args) == true; } @@ -1067,7 +1067,7 @@ class Health { 'totalDistance': totalDistance, 'totalDistanceUnit': totalDistanceUnit.name, 'title': title, - 'recordingMethod': recordingMethod.index, + 'recordingMethod': recordingMethod.toInt(), }; return await _channel.invokeMethod('writeWorkoutData', args) == true; } diff --git a/packages/health/lib/src/health_value_types.dart b/packages/health/lib/src/health_value_types.dart index 6a217ea93..c0b7a77d7 100644 --- a/packages/health/lib/src/health_value_types.dart +++ b/packages/health/lib/src/health_value_types.dart @@ -816,6 +816,46 @@ enum MenstrualFlow { } } +enum RecordingMethod { + unknown, + active, + automatic, + manual; + + /// Create a [RecordingMethod] from an integer. + /// 0: unknown, 1: active, 2: automatic, 3: manual + /// If the integer is not in the range of 0-3, [RecordingMethod.unknown] is returned. + /// This is used to align the recording method with the platform. + static RecordingMethod fromInt(int? recordingMethod) { + switch (recordingMethod) { + case 0: + return RecordingMethod.unknown; + case 1: + return RecordingMethod.active; + case 2: + return RecordingMethod.automatic; + case 3: + return RecordingMethod.manual; + default: + return RecordingMethod.unknown; + } + } + + /// Convert this [RecordingMethod] to an integer. + int toInt() { + switch (this) { + case RecordingMethod.unknown: + return 0; + case RecordingMethod.active: + return 1; + case RecordingMethod.automatic: + return 2; + case RecordingMethod.manual: + return 3; + } + } +} + /// A [HealthValue] object for menstrual flow. /// /// Parameters: From 2267115ddc8e68e25af79ed96e07274bc0bae5c9 Mon Sep 17 00:00:00 2001 From: Aamir Farooq Date: Wed, 14 Aug 2024 17:44:57 +0200 Subject: [PATCH 22/32] Support filtering by recording method when fetching data --- .../cachet/plugins/health/HealthPlugin.kt | 76 +++++++++++-------- packages/health/example/lib/main.dart | 14 +++- packages/health/lib/src/health_plugin.dart | 31 +++++--- 3 files changed, 75 insertions(+), 46 deletions(-) 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 0e014afdc..f0c7d9384 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 @@ -415,12 +415,12 @@ class HealthPlugin(private var channel: MethodChannel? = null) : private fun getTotalStepsInInterval(call: MethodCall, result: Result) { val start = call.argument("startTime")!! val end = call.argument("endTime")!! - val includeManualEntry = call.argument("includeManualEntry")!! + val recordingMethodsToFilter = call.argument>("recordingMethodsToFilter")!! - if (includeManualEntry) { + if (recordingMethodsToFilter.isEmpty()) { getAggregatedStepCount(start, end, result) } else { - getStepCountManual(start, end, result) + getStepCountFiltered(start, end, recordingMethodsToFilter, result) } } @@ -465,7 +465,7 @@ class HealthPlugin(private var channel: MethodChannel? = null) : } /** get the step records manually and filter out manual entries **/ - private fun getStepCountManual(start: Long, end: Long, result: Result) { + private fun getStepCountFiltered(start: Long, end: Long, recordingMethodsToFilter: List, result: Result) { scope.launch { try { val request = @@ -478,8 +478,8 @@ class HealthPlugin(private var channel: MethodChannel? = null) : ), ) val response = healthConnectClient.readRecords(request) - val filteredRecords = filterManualEntry( - false, + val filteredRecords = filterRecordsByRecordingMethods( + recordingMethodsToFilter, response.records ) val totalSteps = filteredRecords.sumOf { (it as StepsRecord).count.toInt() } @@ -673,31 +673,36 @@ class HealthPlugin(private var channel: MethodChannel? = null) : healthConnectRequestPermissionsLauncher!!.launch(permList.toSet()) } - /** Filter manually recorded data */ - private fun filterManualEntry( - includeManualEntry: Boolean, + /** Filter records by recording methods */ + private fun filterRecordsByRecordingMethods( + recordingMethodsToFilter: List, records: List ): List { - if (includeManualEntry) { + if (recordingMethodsToFilter.isEmpty()) { return records } return records.filter { record -> - return@filter isManualEntry(record) + Log.i( + "FLUTTER_HEALTH", + "Filtering record with recording method ${record.metadata.recordingMethod}, filtering by $recordingMethodsToFilter. Result: ${recordingMethodsToFilter.contains(record.metadata.recordingMethod)}" + ) + return@filter !recordingMethodsToFilter.contains(record.metadata.recordingMethod) } } - private fun isManualEntry(record: Record): Boolean { - return record.metadata.recordingMethod == Metadata.RECORDING_METHOD_MANUAL_ENTRY - } - /** Get all datapoints of the DataType within the given time range */ private fun getData(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 includeManualEntry = call.argument("includeManualEntry")!! + val recordingMethodsToFilter = call.argument>("recordingMethodsToFilter")!! + + Log.i( + "FLUTTER_HEALTH", + "Getting data for $dataType between $startTime and $endTime, filtering by $recordingMethodsToFilter" + ) scope.launch { try { @@ -722,13 +727,8 @@ class HealthPlugin(private var channel: MethodChannel? = null) : var response = healthConnectClient.readRecords(request) var pageToken = response.pageToken - var filteredRecords = filterManualEntry( - includeManualEntry, - response.records - ) - // Add the records from the initial response to the records list - records.addAll(filteredRecords) + records.addAll(response.records) // Continue making requests and fetching records while there is a // page token @@ -746,17 +746,18 @@ class HealthPlugin(private var channel: MethodChannel? = null) : response = healthConnectClient.readRecords(request) pageToken = response.pageToken - filteredRecords = filterManualEntry( - includeManualEntry, - response.records - ) - records.addAll(filteredRecords) + records.addAll(response.records) } // Workout needs distance and total calories burned too if (dataType == WORKOUT) { - for (rec in records) { + var filteredRecords = filterRecordsByRecordingMethods( + recordingMethodsToFilter, + records + ) + + for (rec in filteredRecords) { val record = rec as ExerciseSessionRecord val distanceRequest = healthConnectClient.readRecords( @@ -871,7 +872,12 @@ class HealthPlugin(private var channel: MethodChannel? = null) : } // Filter sleep stages for requested stage } else if (classType == SleepSessionRecord::class) { - for (rec in response.records) { + val filteredRecords = filterRecordsByRecordingMethods( + recordingMethodsToFilter, + response.records + ) + + for (rec in filteredRecords) { if (rec is SleepSessionRecord) { if (dataType == SLEEP_SESSION) { healthConnectData.addAll( @@ -901,13 +907,18 @@ class HealthPlugin(private var channel: MethodChannel? = null) : } } } else { - for (rec in records) { + val filteredRecords = filterRecordsByRecordingMethods( + recordingMethodsToFilter, + records + ) + for (rec in filteredRecords) { healthConnectData.addAll( convertRecord(rec, dataType) ) } } } + Handler(context!!.mainLooper).run { result.success(healthConnectData) } } catch (e: Exception) { Log.i( @@ -1536,6 +1547,11 @@ class HealthPlugin(private var channel: MethodChannel? = null) : val value = call.argument("value")!! val recordingMethod = call.argument("recordingMethod")!! + Log.i( + "FLUTTER_HEALTH", + "Writing data for $type between $startTime and $endTime, value: $value, recording method: $recordingMethod" + ) + val record = when (type) { BODY_FAT_PERCENTAGE -> diff --git a/packages/health/example/lib/main.dart b/packages/health/example/lib/main.dart index fa7ad6b10..674897e9e 100644 --- a/packages/health/example/lib/main.dart +++ b/packages/health/example/lib/main.dart @@ -194,9 +194,13 @@ class _HealthAppState extends State { value: 1.925, type: HealthDataType.HEIGHT, startTime: earlier, - endTime: now); + endTime: now, + recordingMethod: RecordingMethod.manual); success &= await Health().writeHealthData( - value: 90, type: HealthDataType.WEIGHT, startTime: now); + value: 90, + type: HealthDataType.WEIGHT, + startTime: now, + recordingMethod: RecordingMethod.manual); success &= await Health().writeHealthData( value: 90, type: HealthDataType.HEART_RATE, @@ -206,12 +210,14 @@ class _HealthAppState extends State { value: 90, type: HealthDataType.STEPS, startTime: earlier, - endTime: now); + endTime: now, + recordingMethod: RecordingMethod.active); success &= await Health().writeHealthData( value: 200, type: HealthDataType.ACTIVE_ENERGY_BURNED, startTime: earlier, - endTime: now); + endTime: now, + recordingMethod: RecordingMethod.automatic); success &= await Health().writeHealthData( value: 70, type: HealthDataType.HEART_RATE, diff --git a/packages/health/lib/src/health_plugin.dart b/packages/health/lib/src/health_plugin.dart index 31fab3941..a53ccce51 100644 --- a/packages/health/lib/src/health_plugin.dart +++ b/packages/health/lib/src/health_plugin.dart @@ -241,17 +241,17 @@ class Health { Future> _computeAndroidBMI( DateTime startTime, DateTime endTime, - bool includeManualEntry, + List recordingMethodsToFilter, ) async { List heights = await _prepareQuery( - startTime, endTime, HealthDataType.HEIGHT, includeManualEntry); + startTime, endTime, HealthDataType.HEIGHT, recordingMethodsToFilter); if (heights.isEmpty) { return []; } List weights = await _prepareQuery( - startTime, endTime, HealthDataType.WEIGHT, includeManualEntry); + startTime, endTime, HealthDataType.WEIGHT, recordingMethodsToFilter); double h = (heights.last.value as NumericHealthValue).numericValue.toDouble(); @@ -761,17 +761,19 @@ class Health { } /// Fetch a list of health data points based on [types]. + /// You can also specify the [recordingMethodsToFilter] to filter the data points. + /// If not specified, all data points will be included. Future> getHealthDataFromTypes({ required List types, required DateTime startTime, required DateTime endTime, - bool includeManualEntry = true, + List recordingMethodsToFilter = const [], }) async { List dataPoints = []; for (var type in types) { - final result = - await _prepareQuery(startTime, endTime, type, includeManualEntry); + final result = await _prepareQuery( + startTime, endTime, type, recordingMethodsToFilter); dataPoints.addAll(result); } @@ -823,7 +825,7 @@ class Health { DateTime startTime, DateTime endTime, HealthDataType dataType, - bool includeManualEntry, + List recordingMethodsToFilter, ) async { // Ask for device ID only once _deviceId ??= Platform.isAndroid @@ -838,9 +840,10 @@ class Health { // If BodyMassIndex is requested on Android, calculate this manually if (dataType == HealthDataType.BODY_MASS_INDEX && Platform.isAndroid) { - return _computeAndroidBMI(startTime, endTime, includeManualEntry); + return _computeAndroidBMI(startTime, endTime, recordingMethodsToFilter); } - return await _dataQuery(startTime, endTime, dataType, includeManualEntry); + return await _dataQuery( + startTime, endTime, dataType, recordingMethodsToFilter); } /// Prepares an interval query, i.e. checks if the types are available, etc. @@ -889,14 +892,18 @@ class Health { } /// Fetches data points from Android/iOS native code. - Future> _dataQuery(DateTime startTime, DateTime endTime, - HealthDataType dataType, bool includeManualEntry) async { + Future> _dataQuery( + DateTime startTime, + DateTime endTime, + HealthDataType dataType, + List recordingMethodsToFilter) async { final args = { 'dataTypeKey': dataType.name, 'dataUnitKey': dataTypeToUnit[dataType]!.name, 'startTime': startTime.millisecondsSinceEpoch, 'endTime': endTime.millisecondsSinceEpoch, - 'includeManualEntry': includeManualEntry + 'recordingMethodsToFilter': + recordingMethodsToFilter.map((e) => e.toInt()).toList(), }; final fetchedDataPoints = await _channel.invokeMethod('getData', args); From f8a3017d03bd74d64da82af9b822a2cfd0bbb83e Mon Sep 17 00:00:00 2001 From: Aamir Farooq Date: Thu, 15 Aug 2024 10:07:51 +0200 Subject: [PATCH 23/32] Fix `includeManualEntry` for `getTotalStepsInInterval` --- packages/health/example/lib/main.dart | 2 +- packages/health/lib/src/health_plugin.dart | 17 ++++++++++------- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/packages/health/example/lib/main.dart b/packages/health/example/lib/main.dart index 674897e9e..50246c3bc 100644 --- a/packages/health/example/lib/main.dart +++ b/packages/health/example/lib/main.dart @@ -211,7 +211,7 @@ class _HealthAppState extends State { type: HealthDataType.STEPS, startTime: earlier, endTime: now, - recordingMethod: RecordingMethod.active); + recordingMethod: RecordingMethod.manual); success &= await Health().writeHealthData( value: 200, type: HealthDataType.ACTIVE_ENERGY_BURNED, diff --git a/packages/health/lib/src/health_plugin.dart b/packages/health/lib/src/health_plugin.dart index a53ccce51..62c4b801e 100644 --- a/packages/health/lib/src/health_plugin.dart +++ b/packages/health/lib/src/health_plugin.dart @@ -791,12 +791,12 @@ class Health { required DateTime endDate, required List types, required int interval, - bool includeManualEntry = true}) async { + List recordingMethodsToFilter = const []}) async { List dataPoints = []; for (var type in types) { final result = await _prepareIntervalQuery( - startDate, endDate, type, interval, includeManualEntry); + startDate, endDate, type, interval, recordingMethodsToFilter); dataPoints.addAll(result); } @@ -852,7 +852,7 @@ class Health { DateTime endDate, HealthDataType dataType, int interval, - bool includeManualEntry) async { + List recordingMethodsToFilter) async { // Ask for device ID only once _deviceId ??= Platform.isAndroid ? (await _deviceInfo.androidInfo).id @@ -865,7 +865,7 @@ class Health { } return await _dataIntervalQuery( - startDate, endDate, dataType, interval, includeManualEntry); + startDate, endDate, dataType, interval, recordingMethodsToFilter); } /// Prepares an aggregate query, i.e. checks if the types are available, etc. @@ -930,14 +930,15 @@ class Health { DateTime endDate, HealthDataType dataType, int interval, - bool includeManualEntry) async { + List recordingMethodsToFilter) async { final args = { 'dataTypeKey': dataType.name, 'dataUnitKey': dataTypeToUnit[dataType]!.name, 'startTime': startDate.millisecondsSinceEpoch, 'endTime': endDate.millisecondsSinceEpoch, 'interval': interval, - 'includeManualEntry': includeManualEntry + 'recordingMethodsToFilter': + recordingMethodsToFilter.map((e) => e.toInt()).toList(), }; final fetchedDataPoints = @@ -1001,7 +1002,9 @@ class Health { final args = { 'startTime': startTime.millisecondsSinceEpoch, 'endTime': endTime.millisecondsSinceEpoch, - 'includeManualEntry': includeManualEntry + 'recordingMethodsToFilter': includeManualEntry + ? [] + : [RecordingMethod.manual.toInt()], }; final stepsCount = await _channel.invokeMethod( 'getTotalStepsInInterval', From 373799a0f595215b5b3c8c108c7312532ea18a00 Mon Sep 17 00:00:00 2001 From: Aamir Farooq Date: Thu, 15 Aug 2024 10:43:28 +0200 Subject: [PATCH 24/32] Support recording method on iOS --- packages/health/example/lib/main.dart | 18 +++++++---- .../ios/Classes/SwiftHealthPlugin.swift | 30 ++++++++++++------- 2 files changed, 33 insertions(+), 15 deletions(-) diff --git a/packages/health/example/lib/main.dart b/packages/health/example/lib/main.dart index 50246c3bc..3c7ab8b27 100644 --- a/packages/health/example/lib/main.dart +++ b/packages/health/example/lib/main.dart @@ -223,11 +223,19 @@ class _HealthAppState extends State { type: HealthDataType.HEART_RATE, startTime: earlier, endTime: now); - success &= await Health().writeHealthData( - value: 30, - type: HealthDataType.HEART_RATE_VARIABILITY_RMSSD, - startTime: earlier, - endTime: now); + if (Platform.isIOS) { + success &= await Health().writeHealthData( + value: 30, + type: HealthDataType.HEART_RATE_VARIABILITY_SDNN, + startTime: earlier, + endTime: now); + } else { + success &= await Health().writeHealthData( + value: 30, + type: HealthDataType.HEART_RATE_VARIABILITY_RMSSD, + startTime: earlier, + endTime: now); + } success &= await Health().writeHealthData( value: 37, type: HealthDataType.BODY_TEMPERATURE, diff --git a/packages/health/ios/Classes/SwiftHealthPlugin.swift b/packages/health/ios/Classes/SwiftHealthPlugin.swift index 9b00a3c2b..b6dce5a9f 100644 --- a/packages/health/ios/Classes/SwiftHealthPlugin.swift +++ b/packages/health/ios/Classes/SwiftHealthPlugin.swift @@ -2,6 +2,13 @@ import Flutter import HealthKit import UIKit +enum RecordingMethod: Int { + case unknown = 0 // RECORDING_METHOD_UNKNOWN + case active = 1 // RECORDING_METHOD_ACTIVELY_RECORDED + case automatic = 2 // RECORDING_METHOD_AUTOMATICALLY_RECORDED + case manual = 3 // RECORDING_METHOD_MANUAL_ENTRY +} + public class SwiftHealthPlugin: NSObject, FlutterPlugin { let healthStore = HKHealthStore() @@ -743,7 +750,8 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { let startTime = (arguments?["startTime"] as? NSNumber) ?? 0 let endTime = (arguments?["endTime"] as? NSNumber) ?? 0 let limit = (arguments?["limit"] as? Int) ?? HKObjectQueryNoLimit - let includeManualEntry = (arguments?["includeManualEntry"] as? Bool) ?? true + let recordingMethodsToFilter = (arguments?["recordingMethodsToFilter"] as? [Int]) ?? [] + let includeManualEntry = recordingMethodsToFilter.contains(RecordingMethod.manual.rawValue) // Convert dates from milliseconds to Date() let dateFrom = Date(timeIntervalSince1970: startTime.doubleValue / 1000) @@ -768,7 +776,7 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { "date_to": Int(dateTo.timeIntervalSince1970 * 1000), "source_id": sourceIdForCharacteristic, "source_name": sourceNameForCharacteristic, - "is_manual_entry": true + "recording_method": RecordingMethod.manual.rawValue ] ]) return @@ -781,7 +789,7 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { "date_to": Int(dateTo.timeIntervalSince1970 * 1000), "source_id": sourceIdForCharacteristic, "source_name": sourceNameForCharacteristic, - "is_manual_entry": true + "recording_method": RecordingMethod.manual.rawValue ] ]) return @@ -794,7 +802,7 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { "date_to": Int(dateTo.timeIntervalSince1970 * 1000), "source_id": sourceIdForCharacteristic, "source_name": sourceNameForCharacteristic, - "is_manual_entry": true + "recording_method": RecordingMethod.manual.rawValue ] ]) return @@ -826,7 +834,7 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { "date_to": Int(sample.endDate.timeIntervalSince1970 * 1000), "source_id": sample.sourceRevision.source.bundleIdentifier, "source_name": sample.sourceRevision.source.name, - "is_manual_entry": sample.metadata?[HKMetadataKeyWasUserEntered] != nil, + "recording_method": (sample.metadata?[HKMetadataKeyWasUserEntered] as? Bool == true) ? RecordingMethod.manual.rawValue : RecordingMethod.automatic.rawValue, "metadata": dataTypeKey == INSULIN_DELIVERY ? sample.metadata : nil ] } @@ -891,7 +899,7 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { "date_to": Int(sample.endDate.timeIntervalSince1970 * 1000), "source_id": sample.sourceRevision.source.bundleIdentifier, "source_name": sample.sourceRevision.source.name, - "is_manual_entry": sample.metadata?[HKMetadataKeyWasUserEntered] != nil, + "recording_method": (sample.metadata?[HKMetadataKeyWasUserEntered] as? Bool == true) ? RecordingMethod.manual.rawValue : RecordingMethod.automatic.rawValue, "metadata": metadata ] } @@ -915,7 +923,7 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { "date_to": Int(sample.endDate.timeIntervalSince1970 * 1000), "source_id": sample.sourceRevision.source.bundleIdentifier, "source_name": sample.sourceRevision.source.name, - "is_manual_entry": sample.metadata?[HKMetadataKeyWasUserEntered] != nil, + "recording_method": (sample.metadata?[HKMetadataKeyWasUserEntered] as? Bool == true) ? RecordingMethod.manual.rawValue : RecordingMethod.automatic.rawValue, "workout_type": self.getWorkoutType(type: sample.workoutActivityType), "total_distance": sample.totalDistance != nil ? Int(sample.totalDistance!.doubleValue(for: HKUnit.meter())) : 0, "total_energy_burned": sample.totalEnergyBurned != nil ? Int(sample.totalEnergyBurned!.doubleValue(for: HKUnit.kilocalorie())) : 0 @@ -1047,7 +1055,8 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { let startDate = (arguments?["startTime"] as? NSNumber) ?? 0 let endDate = (arguments?["endTime"] as? NSNumber) ?? 0 let intervalInSecond = (arguments?["interval"] as? Int) ?? 1 - let includeManualEntry = (arguments?["includeManualEntry"] as? Bool) ?? true + let recordingMethodsToFilter = (arguments?["recordingMethodsToFilter"] as? [Int]) ?? [] + let includeManualEntry = recordingMethodsToFilter.contains(RecordingMethod.manual.rawValue) // Set interval in seconds. var interval = DateComponents() @@ -1131,7 +1140,8 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { let arguments = call.arguments as? NSDictionary let startTime = (arguments?["startTime"] as? NSNumber) ?? 0 let endTime = (arguments?["endTime"] as? NSNumber) ?? 0 - let includeManualEntry = (arguments?["includeManualEntry"] as? Bool) ?? true + let recordingMethodsToFilter = (arguments?["recordingMethodsToFilter"] as? [Int]) ?? [] + let includeManualEntry = recordingMethodsToFilter.contains(RecordingMethod.manual.rawValue) // Convert dates from milliseconds to Date() let dateFrom = Date(timeIntervalSince1970: startTime.doubleValue / 1000) @@ -1741,5 +1751,5 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { default: return "other" } - } + } } From 885a929f57a61d8cf2dd0dae7ebf2dfa019c52e9 Mon Sep 17 00:00:00 2001 From: Aamir Farooq Date: Thu, 15 Aug 2024 16:10:25 +0200 Subject: [PATCH 25/32] Recording method when writing on iOS (WIP) --- .../ios/Classes/SwiftHealthPlugin.swift | 51 ++++++++++++----- packages/health/lib/src/health_plugin.dart | 55 +++++++++++++++---- 2 files changed, 83 insertions(+), 23 deletions(-) diff --git a/packages/health/ios/Classes/SwiftHealthPlugin.swift b/packages/health/ios/Classes/SwiftHealthPlugin.swift index b6dce5a9f..a7d837380 100644 --- a/packages/health/ios/Classes/SwiftHealthPlugin.swift +++ b/packages/health/ios/Classes/SwiftHealthPlugin.swift @@ -421,26 +421,33 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { let type = (arguments["dataTypeKey"] as? String), let unit = (arguments["dataUnitKey"] as? String), let startTime = (arguments["startTime"] as? NSNumber), - let endTime = (arguments["endTime"] as? NSNumber) + let endTime = (arguments["endTime"] as? NSNumber), + let recordingMethod = (arguments["recordingMethod"] as? Int) else { throw PluginError(message: "Invalid Arguments") } let dateFrom = Date(timeIntervalSince1970: startTime.doubleValue / 1000) let dateTo = Date(timeIntervalSince1970: endTime.doubleValue / 1000) + + let isManualEntry = recordingMethod == RecordingMethod.manual.rawValue + let metadata: [String: Any] = [ + HKMetadataKeyWasUserEntered: NSNumber(value: isManualEntry) + ] let sample: HKObject if dataTypeLookUp(key: type).isKind(of: HKCategoryType.self) { sample = HKCategorySample( type: dataTypeLookUp(key: type) as! HKCategoryType, value: Int(value), start: dateFrom, - end: dateTo) + end: dateTo, metadata: metadata) } else { let quantity = HKQuantity(unit: unitDict[unit]!, doubleValue: value) sample = HKQuantitySample( type: dataTypeLookUp(key: type) as! HKQuantityType, quantity: quantity, start: dateFrom, - end: dateTo) + end: dateTo, metadata: metadata) } + print(sample.metadata?["HKWasUserEntered"] ?? "No metadata") HKHealthStore().save( sample, @@ -513,21 +520,27 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { let systolic = (arguments["systolic"] as? Double), let diastolic = (arguments["diastolic"] as? Double), let startTime = (arguments["startTime"] as? NSNumber), - let endTime = (arguments["endTime"] as? NSNumber) + let endTime = (arguments["endTime"] as? NSNumber), + let recordingMethod = (arguments["recordingMethod"] as? Int) else { throw PluginError(message: "Invalid Arguments") } let dateFrom = Date(timeIntervalSince1970: startTime.doubleValue / 1000) let dateTo = Date(timeIntervalSince1970: endTime.doubleValue / 1000) + + let isManualEntry = recordingMethod == RecordingMethod.manual.rawValue + let metadata = [ + HKMetadataKeyWasUserEntered: NSNumber(value: isManualEntry) + ] let systolic_sample = HKQuantitySample( type: HKSampleType.quantityType(forIdentifier: .bloodPressureSystolic)!, quantity: HKQuantity(unit: HKUnit.millimeterOfMercury(), doubleValue: systolic), - start: dateFrom, end: dateTo) + start: dateFrom, end: dateTo, metadata: metadata) let diastolic_sample = HKQuantitySample( type: HKSampleType.quantityType(forIdentifier: .bloodPressureDiastolic)!, quantity: HKQuantity(unit: HKUnit.millimeterOfMercury(), doubleValue: diastolic), - start: dateFrom, end: dateTo) + start: dateFrom, end: dateTo, metadata: metadata) let bpCorrelationType = HKCorrelationType.correlationType(forIdentifier: .bloodPressure)! let bpCorrelation = Set(arrayLiteral: systolic_sample, diastolic_sample) let blood_pressure_sample = HKCorrelation(type: bpCorrelationType , start: dateFrom, end: dateTo, objects: bpCorrelation) @@ -549,7 +562,8 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { let name = (arguments["name"] as? String?), let startTime = (arguments["start_time"] as? NSNumber), let endTime = (arguments["end_time"] as? NSNumber), - let mealType = (arguments["meal_type"] as? String?) + let mealType = (arguments["meal_type"] as? String?), + let recordingMethod = arguments["recordingMethod"] as? Int else { throw PluginError(message: "Invalid Arguments") } @@ -558,12 +572,14 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { let dateTo = Date(timeIntervalSince1970: endTime.doubleValue / 1000) let mealTypeString = mealType ?? "UNKNOWN" - var metadata = ["HKFoodMeal": "\(mealTypeString)"] + + let isManualEntry = recordingMethod == RecordingMethod.manual.rawValue - if(name != nil) { + var metadata = ["HKFoodMeal": mealTypeString, HKMetadataKeyWasUserEntered: NSNumber(value: isManualEntry)] as [String : Any] + if (name != nil) { metadata[HKMetadataKeyFoodType] = "\(name!)" } - + var nutrition = Set() for (key, identifier) in NUTRITION_KEYS { let value = arguments[key] as? Double @@ -622,7 +638,8 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { guard let arguments = call.arguments as? NSDictionary, let flow = (arguments["value"] as? Int), let endTime = (arguments["endTime"] as? NSNumber), - let isStartOfCycle = (arguments["isStartOfCycle"] as? NSNumber) + let isStartOfCycle = (arguments["isStartOfCycle"] as? NSNumber), + let recordingMethod = (arguments["recordingMethod"] as? Int) else { throw PluginError(message: "Invalid Arguments - value, startTime, endTime or isStartOfCycle invalid") } @@ -632,11 +649,13 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { let dateTime = Date(timeIntervalSince1970: endTime.doubleValue / 1000) + let isManualEntry = recordingMethod == RecordingMethod.manual.rawValue + guard let categoryType = HKSampleType.categoryType(forIdentifier: .menstrualFlow) else { throw PluginError(message: "Invalid Menstrual Flow Type") } - let metadata = [HKMetadataKeyMenstrualCycleStart: isStartOfCycle] + let metadata = [HKMetadataKeyMenstrualCycleStart: isStartOfCycle, HKMetadataKeyWasUserEntered: NSNumber(value: isManualEntry)] as [String : Any] let sample = HKCategorySample( type: categoryType, @@ -827,6 +846,7 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { switch samplesOrNil { case let (samples as [HKQuantitySample]) as Any: let dictionaries = samples.map { sample -> NSDictionary in + print(sample.metadata?[HKMetadataKeyWasUserEntered] as? Bool) return [ "uuid": "\(sample.uuid)", "value": sample.quantity.doubleValue(for: unit ?? HKUnit.internationalUnit()), @@ -834,7 +854,9 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { "date_to": Int(sample.endDate.timeIntervalSince1970 * 1000), "source_id": sample.sourceRevision.source.bundleIdentifier, "source_name": sample.sourceRevision.source.name, - "recording_method": (sample.metadata?[HKMetadataKeyWasUserEntered] as? Bool == true) ? RecordingMethod.manual.rawValue : RecordingMethod.automatic.rawValue, + "recording_method": (sample.metadata?[HKMetadataKeyWasUserEntered] as? Bool == true) + ? RecordingMethod.manual.rawValue + : RecordingMethod.automatic.rawValue, "metadata": dataTypeKey == INSULIN_DELIVERY ? sample.metadata : nil ] } @@ -977,6 +999,9 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { "date_to": Int(sample.endDate.timeIntervalSince1970 * 1000), "source_id": sample.sourceRevision.source.bundleIdentifier, "source_name": sample.sourceRevision.source.name, + "recording_method": (sample.metadata?[HKMetadataKeyWasUserEntered] as? Bool == true) + ? RecordingMethod.manual.rawValue + : RecordingMethod.automatic.rawValue ] for sample in samples { if let quantitySample = sample as? HKQuantitySample { diff --git a/packages/health/lib/src/health_plugin.dart b/packages/health/lib/src/health_plugin.dart index 62c4b801e..718a45ea9 100644 --- a/packages/health/lib/src/health_plugin.dart +++ b/packages/health/lib/src/health_plugin.dart @@ -305,8 +305,14 @@ class Health { required HealthDataType type, required DateTime startTime, DateTime? endTime, - RecordingMethod recordingMethod = RecordingMethod.unknown, + RecordingMethod recordingMethod = RecordingMethod.automatic, }) async { + if (Platform.isIOS && + [RecordingMethod.active, RecordingMethod.unknown] + .contains(recordingMethod)) { + throw ArgumentError("recordingMethod must be manual or automatic on iOS"); + } + if (type == HealthDataType.WORKOUT) { throw ArgumentError( "Adding workouts should be done using the writeWorkoutData method."); @@ -369,7 +375,6 @@ class Health { /// Must be equal to or earlier than [endTime]. /// * [endTime] - the end time when this [value] is measured. /// Must be equal to or later than [startTime]. - /// * [recordingMethod] - the recording method of the data point. Future delete({ required HealthDataType type, required DateTime startTime, @@ -408,8 +413,14 @@ class Health { required int diastolic, required DateTime startTime, DateTime? endTime, - RecordingMethod recordingMethod = RecordingMethod.unknown, + RecordingMethod recordingMethod = RecordingMethod.automatic, }) async { + if (Platform.isIOS && + [RecordingMethod.active, RecordingMethod.unknown] + .contains(recordingMethod)) { + throw ArgumentError("recordingMethod must be manual or automatic on iOS"); + } + endTime ??= startTime; if (startTime.isAfter(endTime)) { throw ArgumentError("startTime must be equal or earlier than endTime"); @@ -442,8 +453,14 @@ class Health { required double saturation, required DateTime startTime, DateTime? endTime, - RecordingMethod recordingMethod = RecordingMethod.unknown, + RecordingMethod recordingMethod = RecordingMethod.automatic, }) async { + if (Platform.isIOS && + [RecordingMethod.active, RecordingMethod.unknown] + .contains(recordingMethod)) { + throw ArgumentError("recordingMethod must be manual or automatic on iOS"); + } + endTime ??= startTime; if (startTime.isAfter(endTime)) { throw ArgumentError("startTime must be equal or earlier than endTime"); @@ -455,7 +472,8 @@ class Health { value: saturation, type: HealthDataType.BLOOD_OXYGEN, startTime: startTime, - endTime: endTime); + endTime: endTime, + recordingMethod: recordingMethod); } else if (Platform.isAndroid) { Map args = { 'value': saturation, @@ -568,8 +586,14 @@ class Health { double? sugar, double? water, double? zinc, - RecordingMethod recordingMethod = RecordingMethod.unknown, + RecordingMethod recordingMethod = RecordingMethod.automatic, }) async { + if (Platform.isIOS && + [RecordingMethod.active, RecordingMethod.unknown] + .contains(recordingMethod)) { + throw ArgumentError("recordingMethod must be manual or automatic on iOS"); + } + if (startTime.isAfter(endTime)) { throw ArgumentError("startTime must be equal or earlier than endTime"); } @@ -642,8 +666,14 @@ class Health { required DateTime startTime, required DateTime endTime, required bool isStartOfCycle, - RecordingMethod recordingMethod = RecordingMethod.unknown, + RecordingMethod recordingMethod = RecordingMethod.automatic, }) async { + if (Platform.isIOS && + [RecordingMethod.active, RecordingMethod.unknown] + .contains(recordingMethod)) { + throw ArgumentError("recordingMethod must be manual or automatic on iOS"); + } + var value = Platform.isAndroid ? MenstrualFlow.toHealthConnect(flow) : flow.index; @@ -679,7 +709,6 @@ class Health { /// only at a specific point in time (default). /// * [metadata] - optional map of keys, both HKMetadataKeyExternalUUID /// and HKMetadataKeyDeviceName are required - /// * [recordingMethod] - the recording method of the data point. Future writeAudiogram({ required List frequencies, required List leftEarSensitivities, @@ -1048,7 +1077,7 @@ class Health { /// *ONLY FOR IOS* Default value is METER. /// - [title] The title of the workout. /// *ONLY FOR HEALTH CONNECT* Default value is the [activityType], e.g. "STRENGTH_TRAINING". - /// - [recorrdingMethod] The recording method of the data point. + /// - [recordingMethod] The recording method of the data point. Future writeWorkoutData({ required HealthWorkoutActivityType activityType, required DateTime start, @@ -1058,8 +1087,14 @@ class Health { int? totalDistance, HealthDataUnit totalDistanceUnit = HealthDataUnit.METER, String? title, - RecordingMethod recordingMethod = RecordingMethod.unknown, + RecordingMethod recordingMethod = RecordingMethod.automatic, }) async { + if (Platform.isIOS && + [RecordingMethod.active, RecordingMethod.unknown] + .contains(recordingMethod)) { + throw ArgumentError("recordingMethod must be manual or automatic on iOS"); + } + // Check that value is on the current Platform if (Platform.isIOS && !_isOnIOS(activityType)) { throw HealthException(activityType, From 468eaa1ce3ccb9b7a24d5dfb17670f5f0cd2109c Mon Sep 17 00:00:00 2001 From: Aamir Farooq Date: Thu, 15 Aug 2024 16:27:25 +0200 Subject: [PATCH 26/32] Fix filtering manual entries when fetching data on iOS --- packages/health/example/lib/main.dart | 183 +++++++++++++----- .../ios/Classes/SwiftHealthPlugin.swift | 7 +- 2 files changed, 138 insertions(+), 52 deletions(-) diff --git a/packages/health/example/lib/main.dart b/packages/health/example/lib/main.dart index 3c7ab8b27..06ab843d6 100644 --- a/packages/health/example/lib/main.dart +++ b/packages/health/example/lib/main.dart @@ -36,6 +36,7 @@ class _HealthAppState extends State { List _healthDataList = []; AppState _state = AppState.DATA_NOT_FETCHED; int _nofSteps = 0; + List filteredEntries = []; // All types available depending on platform (iOS ot Android). List get types => (Platform.isAndroid) @@ -158,6 +159,9 @@ class _HealthAppState extends State { debugPrint('Total number of data points: ${healthData.length}. ' '${healthData.length > 100 ? 'Only showing the first 100.' : ''}'); + // sort the data points by date + healthData.sort((a, b) => b.dateTo.compareTo(a.dateTo)); + // save all the new data points (only the first 100) _healthDataList.addAll( (healthData.length < 100) ? healthData : healthData.sublist(0, 100)); @@ -205,7 +209,8 @@ class _HealthAppState extends State { value: 90, type: HealthDataType.HEART_RATE, startTime: earlier, - endTime: now); + endTime: now, + recordingMethod: RecordingMethod.manual); success &= await Health().writeHealthData( value: 90, type: HealthDataType.STEPS, @@ -294,52 +299,52 @@ class _HealthAppState extends State { startTime: now, ); success &= await Health().writeMeal( - mealType: MealType.SNACK, - startTime: earlier, - endTime: now, - caloriesConsumed: 1000, - carbohydrates: 50, - protein: 25, - fatTotal: 50, - name: "Banana", - caffeine: 0.002, - vitaminA: 0.001, - vitaminC: 0.002, - vitaminD: 0.003, - vitaminE: 0.004, - vitaminK: 0.005, - b1Thiamin: 0.006, - b2Riboflavin: 0.007, - b3Niacin: 0.008, - b5PantothenicAcid: 0.009, - b6Pyridoxine: 0.010, - b7Biotin: 0.011, - b9Folate: 0.012, - b12Cobalamin: 0.013, - calcium: 0.015, - copper: 0.016, - iodine: 0.017, - iron: 0.018, - magnesium: 0.019, - manganese: 0.020, - phosphorus: 0.021, - potassium: 0.022, - selenium: 0.023, - sodium: 0.024, - zinc: 0.025, - water: 0.026, - molybdenum: 0.027, - chloride: 0.028, - chromium: 0.029, - cholesterol: 0.030, - fiber: 0.031, - fatMonounsaturated: 0.032, - fatPolyunsaturated: 0.033, - fatUnsaturated: 0.065, - fatTransMonoenoic: 0.65, - fatSaturated: 066, - sugar: 0.067, - ); + mealType: MealType.SNACK, + startTime: earlier, + endTime: now, + caloriesConsumed: 1000, + carbohydrates: 50, + protein: 25, + fatTotal: 50, + name: "Banana", + caffeine: 0.002, + vitaminA: 0.001, + vitaminC: 0.002, + vitaminD: 0.003, + vitaminE: 0.004, + vitaminK: 0.005, + b1Thiamin: 0.006, + b2Riboflavin: 0.007, + b3Niacin: 0.008, + b5PantothenicAcid: 0.009, + b6Pyridoxine: 0.010, + b7Biotin: 0.011, + b9Folate: 0.012, + b12Cobalamin: 0.013, + calcium: 0.015, + copper: 0.016, + iodine: 0.017, + iron: 0.018, + magnesium: 0.019, + manganese: 0.020, + phosphorus: 0.021, + potassium: 0.022, + selenium: 0.023, + sodium: 0.024, + zinc: 0.025, + water: 0.026, + molybdenum: 0.027, + chloride: 0.028, + chromium: 0.029, + cholesterol: 0.030, + fiber: 0.031, + fatMonounsaturated: 0.032, + fatPolyunsaturated: 0.033, + fatUnsaturated: 0.065, + fatTransMonoenoic: 0.65, + fatSaturated: 066, + sugar: 0.067, + recordingMethod: RecordingMethod.manual); // Store an Audiogram - only available on iOS // const frequencies = [125.0, 500.0, 1000.0, 2000.0, 4000.0, 8000.0]; @@ -405,7 +410,9 @@ class _HealthAppState extends State { if (stepsPermission) { try { - steps = await Health().getTotalStepsInInterval(midnight, now); + steps = await Health().getTotalStepsInInterval(midnight, now, + includeManualEntry: + !filteredEntries.contains(RecordingMethod.manual)); } catch (error) { debugPrint("Exception in getTotalStepsInInterval: $error"); } @@ -427,6 +434,7 @@ class _HealthAppState extends State { setState(() => _state = AppState.PERMISSIONS_REVOKING); bool success = false; + try { await Health().revokePermissions(); success = true; @@ -517,6 +525,8 @@ class _HealthAppState extends State { ], ), Divider(thickness: 3), + if (_state == AppState.DATA_READY) _dataFiltration, + if (_state == AppState.STEPS_READY) _stepsFiltration, Expanded(child: Center(child: _content)) ], ), @@ -525,6 +535,78 @@ class _HealthAppState extends State { ); } + Widget get _dataFiltration => Column( + children: [ + Wrap( + children: [ + for (final method in [ + RecordingMethod.manual, + RecordingMethod.automatic, + RecordingMethod.active, + RecordingMethod.unknown, + ]) + SizedBox( + width: 150, + child: CheckboxListTile( + title: Text( + '${method.name[0].toUpperCase()}${method.name.substring(1)} entries'), + value: !filteredEntries.contains(method), + onChanged: (value) { + setState(() { + if (value!) { + filteredEntries.remove(method); + } else { + filteredEntries.add(method); + } + }); + }, + controlAffinity: ListTileControlAffinity.leading, + contentPadding: EdgeInsets.zero, + dense: true, + ), + ), + // Add other entries here if needed + ], + ), + Divider(thickness: 3), + ], + ); + + Widget get _stepsFiltration => Column( + children: [ + Wrap( + children: [ + for (final method in [ + RecordingMethod.manual, + ]) + SizedBox( + width: 150, + child: CheckboxListTile( + title: Text( + '${method.name[0].toUpperCase()}${method.name.substring(1)} entries'), + value: !filteredEntries.contains(method), + onChanged: (value) { + setState(() { + if (value!) { + filteredEntries.remove(method); + } else { + filteredEntries.add(method); + } + fetchStepData(); + }); + }, + controlAffinity: ListTileControlAffinity.leading, + contentPadding: EdgeInsets.zero, + dense: true, + ), + ), + // Add other entries here if needed + ], + ), + Divider(thickness: 3), + ], + ); + Widget get _permissionsRevoking => Column( mainAxisAlignment: MainAxisAlignment.center, children: [ @@ -557,6 +639,11 @@ class _HealthAppState extends State { Widget get _contentDataReady => ListView.builder( itemCount: _healthDataList.length, itemBuilder: (_, index) { + // filter out manual entires if not wanted + if (filteredEntries.contains(_healthDataList[index].recordingMethod)) { + return Container(); + } + HealthDataPoint p = _healthDataList[index]; if (p.value is AudiogramHealthValue) { return ListTile( diff --git a/packages/health/ios/Classes/SwiftHealthPlugin.swift b/packages/health/ios/Classes/SwiftHealthPlugin.swift index a7d837380..c87069dab 100644 --- a/packages/health/ios/Classes/SwiftHealthPlugin.swift +++ b/packages/health/ios/Classes/SwiftHealthPlugin.swift @@ -770,7 +770,7 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { let endTime = (arguments?["endTime"] as? NSNumber) ?? 0 let limit = (arguments?["limit"] as? Int) ?? HKObjectQueryNoLimit let recordingMethodsToFilter = (arguments?["recordingMethodsToFilter"] as? [Int]) ?? [] - let includeManualEntry = recordingMethodsToFilter.contains(RecordingMethod.manual.rawValue) + let includeManualEntry = !recordingMethodsToFilter.contains(RecordingMethod.manual.rawValue) // Convert dates from milliseconds to Date() let dateFrom = Date(timeIntervalSince1970: startTime.doubleValue / 1000) @@ -846,7 +846,6 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { switch samplesOrNil { case let (samples as [HKQuantitySample]) as Any: let dictionaries = samples.map { sample -> NSDictionary in - print(sample.metadata?[HKMetadataKeyWasUserEntered] as? Bool) return [ "uuid": "\(sample.uuid)", "value": sample.quantity.doubleValue(for: unit ?? HKUnit.internationalUnit()), @@ -1081,7 +1080,7 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { let endDate = (arguments?["endTime"] as? NSNumber) ?? 0 let intervalInSecond = (arguments?["interval"] as? Int) ?? 1 let recordingMethodsToFilter = (arguments?["recordingMethodsToFilter"] as? [Int]) ?? [] - let includeManualEntry = recordingMethodsToFilter.contains(RecordingMethod.manual.rawValue) + let includeManualEntry = !recordingMethodsToFilter.contains(RecordingMethod.manual.rawValue) // Set interval in seconds. var interval = DateComponents() @@ -1166,7 +1165,7 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { let startTime = (arguments?["startTime"] as? NSNumber) ?? 0 let endTime = (arguments?["endTime"] as? NSNumber) ?? 0 let recordingMethodsToFilter = (arguments?["recordingMethodsToFilter"] as? [Int]) ?? [] - let includeManualEntry = recordingMethodsToFilter.contains(RecordingMethod.manual.rawValue) + let includeManualEntry = !recordingMethodsToFilter.contains(RecordingMethod.manual.rawValue) // Convert dates from milliseconds to Date() let dateFrom = Date(timeIntervalSince1970: startTime.doubleValue / 1000) From 9c7b1b88f4af1dc5b396d3190cf710bc6a264a1a Mon Sep 17 00:00:00 2001 From: Aamir Farooq Date: Thu, 15 Aug 2024 16:28:38 +0200 Subject: [PATCH 27/32] Rename variable in example app --- packages/health/example/lib/main.dart | 29 ++++++++++--------- .../ios/Classes/SwiftHealthPlugin.swift | 1 - 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/packages/health/example/lib/main.dart b/packages/health/example/lib/main.dart index 06ab843d6..840d4cda9 100644 --- a/packages/health/example/lib/main.dart +++ b/packages/health/example/lib/main.dart @@ -36,7 +36,7 @@ class _HealthAppState extends State { List _healthDataList = []; AppState _state = AppState.DATA_NOT_FETCHED; int _nofSteps = 0; - List filteredEntries = []; + List recordingMethodsToFilter = []; // All types available depending on platform (iOS ot Android). List get types => (Platform.isAndroid) @@ -218,11 +218,11 @@ class _HealthAppState extends State { endTime: now, recordingMethod: RecordingMethod.manual); success &= await Health().writeHealthData( - value: 200, - type: HealthDataType.ACTIVE_ENERGY_BURNED, - startTime: earlier, - endTime: now, - recordingMethod: RecordingMethod.automatic); + value: 200, + type: HealthDataType.ACTIVE_ENERGY_BURNED, + startTime: earlier, + endTime: now, + ); success &= await Health().writeHealthData( value: 70, type: HealthDataType.HEART_RATE, @@ -412,7 +412,7 @@ class _HealthAppState extends State { try { steps = await Health().getTotalStepsInInterval(midnight, now, includeManualEntry: - !filteredEntries.contains(RecordingMethod.manual)); + !recordingMethodsToFilter.contains(RecordingMethod.manual)); } catch (error) { debugPrint("Exception in getTotalStepsInInterval: $error"); } @@ -550,13 +550,13 @@ class _HealthAppState extends State { child: CheckboxListTile( title: Text( '${method.name[0].toUpperCase()}${method.name.substring(1)} entries'), - value: !filteredEntries.contains(method), + value: !recordingMethodsToFilter.contains(method), onChanged: (value) { setState(() { if (value!) { - filteredEntries.remove(method); + recordingMethodsToFilter.remove(method); } else { - filteredEntries.add(method); + recordingMethodsToFilter.add(method); } }); }, @@ -584,13 +584,13 @@ class _HealthAppState extends State { child: CheckboxListTile( title: Text( '${method.name[0].toUpperCase()}${method.name.substring(1)} entries'), - value: !filteredEntries.contains(method), + value: !recordingMethodsToFilter.contains(method), onChanged: (value) { setState(() { if (value!) { - filteredEntries.remove(method); + recordingMethodsToFilter.remove(method); } else { - filteredEntries.add(method); + recordingMethodsToFilter.add(method); } fetchStepData(); }); @@ -640,7 +640,8 @@ class _HealthAppState extends State { itemCount: _healthDataList.length, itemBuilder: (_, index) { // filter out manual entires if not wanted - if (filteredEntries.contains(_healthDataList[index].recordingMethod)) { + if (recordingMethodsToFilter + .contains(_healthDataList[index].recordingMethod)) { return Container(); } diff --git a/packages/health/ios/Classes/SwiftHealthPlugin.swift b/packages/health/ios/Classes/SwiftHealthPlugin.swift index c87069dab..6bc60d7b0 100644 --- a/packages/health/ios/Classes/SwiftHealthPlugin.swift +++ b/packages/health/ios/Classes/SwiftHealthPlugin.swift @@ -447,7 +447,6 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin { type: dataTypeLookUp(key: type) as! HKQuantityType, quantity: quantity, start: dateFrom, end: dateTo, metadata: metadata) } - print(sample.metadata?["HKWasUserEntered"] ?? "No metadata") HKHealthStore().save( sample, From 041bb22fa4897a5d37d7f9d6cf01845db16eb280 Mon Sep 17 00:00:00 2001 From: Aamir Farooq Date: Thu, 15 Aug 2024 16:49:24 +0200 Subject: [PATCH 28/32] Update documentation --- packages/health/README.md | 4 ++-- packages/health/lib/src/health_plugin.dart | 6 +++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/health/README.md b/packages/health/README.md index cb9b14db6..68d955f71 100644 --- a/packages/health/README.md +++ b/packages/health/README.md @@ -227,7 +227,7 @@ A `HealthDataPoint` object can be serialized to and from JSON using the `toJson( "source_device_id": "F74938B9-C011-4DE4-AA5E-CF41B60B96E7", "source_id": "com.apple.health.81AE7156-EC05-47E3-AC93-2D6F65C717DF", "source_name": "iPhone12.bardram.net", - "is_manual_entry": false + "recordingMethod": 3 "value": { "__type": "NumericHealthValue", "numeric_value": 141.0 @@ -240,7 +240,7 @@ A `HealthDataPoint` object can be serialized to and from JSON using the `toJson( "source_device_id": "F74938B9-C011-4DE4-AA5E-CF41B60B96E7", "source_id": "com.apple.health.81AE7156-EC05-47E3-AC93-2D6F65C717DF", "source_name": "iPhone12.bardram.net", - "is_manual_entry": false + "recording_method": 2 } ``` diff --git a/packages/health/lib/src/health_plugin.dart b/packages/health/lib/src/health_plugin.dart index 718a45ea9..ab70b53b3 100644 --- a/packages/health/lib/src/health_plugin.dart +++ b/packages/health/lib/src/health_plugin.dart @@ -296,6 +296,8 @@ class Health { /// It must be equal to or later than [startTime]. /// Simply set [endTime] equal to [startTime] if the [value] is measured /// only at a specific point in time (default). + /// * [recordingMethod] - the recording method of the data point, automatic by default. + /// (on iOS this must be manual or automatic) /// /// Values for Sleep and Headache are ignored and will be automatically assigned /// the default value. @@ -815,6 +817,8 @@ class Health { } /// Fetch a list of health data points based on [types]. + /// You can also specify the [recordingMethodsToFilter] to filter the data points. + /// If not specified, all data points will be included.Vkk Future> getHealthIntervalDataFromTypes( {required DateTime startDate, required DateTime endDate, @@ -1077,7 +1081,7 @@ class Health { /// *ONLY FOR IOS* Default value is METER. /// - [title] The title of the workout. /// *ONLY FOR HEALTH CONNECT* Default value is the [activityType], e.g. "STRENGTH_TRAINING". - /// - [recordingMethod] The recording method of the data point. + /// - [recordingMethod] The recording method of the data point, automatic by default (on iOS this can only be automatic or manual). Future writeWorkoutData({ required HealthWorkoutActivityType activityType, required DateTime start, From d5f235c7fe05569c4910822543eaee6e3c881755 Mon Sep 17 00:00:00 2001 From: Aamir Farooq Date: Thu, 15 Aug 2024 17:08:58 +0200 Subject: [PATCH 29/32] Improvements to example app --- packages/health/example/lib/main.dart | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/packages/health/example/lib/main.dart b/packages/health/example/lib/main.dart index 840d4cda9..038b4cf67 100644 --- a/packages/health/example/lib/main.dart +++ b/packages/health/example/lib/main.dart @@ -154,6 +154,7 @@ class _HealthAppState extends State { types: types, startTime: yesterday, endTime: now, + recordingMethodsToFilter: recordingMethodsToFilter, ); debugPrint('Total number of data points: ${healthData.length}. ' @@ -539,12 +540,17 @@ class _HealthAppState extends State { children: [ Wrap( children: [ - for (final method in [ - RecordingMethod.manual, - RecordingMethod.automatic, - RecordingMethod.active, - RecordingMethod.unknown, - ]) + for (final method in Platform.isAndroid + ? [ + RecordingMethod.manual, + RecordingMethod.automatic, + RecordingMethod.active, + RecordingMethod.unknown, + ] + : [ + RecordingMethod.automatic, + RecordingMethod.manual, + ]) SizedBox( width: 150, child: CheckboxListTile( @@ -558,6 +564,7 @@ class _HealthAppState extends State { } else { recordingMethodsToFilter.add(method); } + fetchData(); }); }, controlAffinity: ListTileControlAffinity.leading, From f3a7276bec279d3b12011bb2e5a40b8ed9875556 Mon Sep 17 00:00:00 2001 From: Aamir Farooq Date: Thu, 29 Aug 2024 16:55:30 +0200 Subject: [PATCH 30/32] Quick fix --- .../src/main/kotlin/cachet/plugins/health/HealthPlugin.kt | 1 + 1 file changed, 1 insertion(+) 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 db84ea2be..a1633f336 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 @@ -2168,6 +2168,7 @@ class HealthPlugin(private var channel: MethodChannel? = null) : val endTime = Instant.ofEpochMilli(call.argument("endTime")!!) val totalEnergyBurned = call.argument("totalEnergyBurned") val totalDistance = call.argument("totalDistance") + val recordingMethod = call.argument("recordingMethod")!! if (!workoutTypeMap.containsKey(type)) { result.success(false) Log.w( From cac7736fcfb673f8de698357790685d522901f0e Mon Sep 17 00:00:00 2001 From: Aamir Farooq Date: Thu, 29 Aug 2024 17:12:51 +0200 Subject: [PATCH 31/32] Update documentation --- packages/health/README.md | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/packages/health/README.md b/packages/health/README.md index 9b7f4b549..1e47314a3 100644 --- a/packages/health/README.md +++ b/packages/health/README.md @@ -181,6 +181,11 @@ Below is a simplified flow of how to use the plugin. bool success = await Health().writeHealthData(10, HealthDataType.STEPS, now, now); success = await Health().writeHealthData(3.1, HealthDataType.BLOOD_GLUCOSE, now, now); + // you can also specify the recording method to store in the metadata (default is RecordingMethod.automatic) + // on iOS only `RecordingMethod.automatic` and `RecordingMethod.manual` are supported + // Android additionally supports `RecordingMethod.active` and `RecordingMethod.unknown` + success &= await Health().writeHealthData(10, HealthDataType.STEPS, now, now, recordingMethod: RecordingMethod.manual); + // get the number of steps for today var midnight = DateTime(now.year, now.month, now.day); int? steps = await Health().getTotalStepsInInterval(midnight, now); @@ -223,7 +228,7 @@ A `HealthDataPoint` object can be serialized to and from JSON using the `toJson( "source_device_id": "F74938B9-C011-4DE4-AA5E-CF41B60B96E7", "source_id": "com.apple.health.81AE7156-EC05-47E3-AC93-2D6F65C717DF", "source_name": "iPhone12.bardram.net", - "recordingMethod": 3 + "recording_method": 3 "value": { "__type": "NumericHealthValue", "numeric_value": 141.0 @@ -251,6 +256,28 @@ flutter: Health Plugin Error: flutter: PlatformException(FlutterHealth, Results are null, Optional(Error Domain=com.apple.healthkit Code=6 "Protected health data is inaccessible" UserInfo={NSLocalizedDescription=Protected health data is inaccessible})) ``` +### Filtering by recording method +Google Health Connect and Apple HealthKit both provide ways to distinguish samples collected "automatically" and manually entered data by the user. + +- Android provides an enum with 4 variations: https://developer.android.com/reference/kotlin/androidx/health/connect/client/records/metadata/Metadata#summary +- iOS has a boolean value: https://developer.apple.com/documentation/healthkit/hkmetadatakeywasuserentered + +As such, when fetching data you have the option to filter the fetched data by recording method as such: + +```dart +List healthData = await Health().getHealthDataFromTypes( + types: types, + startTime: yesterday, + endTime: now, + recordingMethodsToFilter: [RecordingMethod.manual, RecordingMethod.unknown], +); +``` + +**Note that for this to work, the information needs to have been provided when writing the data to Health Connect or Apple Health**. For example, steps added manually through the Apple Health App will set `HKWasUserEntered` to true (corresponding to `RecordingMethod.manual`), however it seems that adding steps manually to Google Fit does not write the data with the `RecordingMethod.manual` in the metadata, instead it shows up as `RecordingMethod.unknown`. This is an open issue, and as such filtering manual entries when querying step count on Android with `getTotalStepsInInterval(includeManualEntries: false)` does not necessarily filter out manual steps. + +**NOTE**: On iOS, you can only filter by `RecordingMethod.automatic` and `RecordingMethod`.manual` as it is stored `HKMetadataKeyWasUserEntered` is a boolean value in the metadata. + + ### Filtering out duplicates If the same data is requested multiple times and saved in the same array duplicates will occur. From 8d398bac9d515059af88eb326509cb4ab4119874 Mon Sep 17 00:00:00 2001 From: Aamir Farooq Date: Fri, 30 Aug 2024 10:37:40 +0200 Subject: [PATCH 32/32] Update iOS docs --- packages/health/ios/Classes/SwiftHealthPlugin.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/health/ios/Classes/SwiftHealthPlugin.swift b/packages/health/ios/Classes/SwiftHealthPlugin.swift index 5c04ecdfe..654f89991 100644 --- a/packages/health/ios/Classes/SwiftHealthPlugin.swift +++ b/packages/health/ios/Classes/SwiftHealthPlugin.swift @@ -3,10 +3,10 @@ import HealthKit import UIKit enum RecordingMethod: Int { - case unknown = 0 // RECORDING_METHOD_UNKNOWN - case active = 1 // RECORDING_METHOD_ACTIVELY_RECORDED - case automatic = 2 // RECORDING_METHOD_AUTOMATICALLY_RECORDED - case manual = 3 // RECORDING_METHOD_MANUAL_ENTRY + case unknown = 0 // RECORDING_METHOD_UNKNOWN (not supported on iOS) + case active = 1 // RECORDING_METHOD_ACTIVELY_RECORDED (not supported on iOS) + case automatic = 2 // RECORDING_METHOD_AUTOMATICALLY_RECORDED + case manual = 3 // RECORDING_METHOD_MANUAL_ENTRY } public class SwiftHealthPlugin: NSObject, FlutterPlugin {