From c582918f0f79f279bebe422d7f7162079b6f1f60 Mon Sep 17 00:00:00 2001 From: Aamir Farooq Date: Tue, 6 Aug 2024 14:18:51 +0200 Subject: [PATCH 01/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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 bd37cd56b5e8fc168cb1fe58331a56934231b63c Mon Sep 17 00:00:00 2001 From: bardram Date: Wed, 21 Aug 2024 16:44:54 +0200 Subject: [PATCH 19/21] small updates to the README --- packages/health/README.md | 67 +++++++++---------- packages/health/lib/health.g.dart | 106 +++++++++++++++--------------- 2 files changed, 85 insertions(+), 88 deletions(-) diff --git a/packages/health/README.md b/packages/health/README.md index e7cf8c16d..ccd2d913c 100644 --- a/packages/health/README.md +++ b/packages/health/README.md @@ -2,8 +2,7 @@ Enables reading and writing health data from/to Apple Health and 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. +> **NOTE:** Google has deprecated the Google Fit API. According to the [documentation](https://developers.google.com/fit/android), as of **May 1st 2014** developers cannot sign up for using the API. 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: @@ -26,7 +25,7 @@ See the tables below for supported health and workout data types. ### Apple Health (iOS) -Step 1: Append the `Info.plist` with the following 2 entries +First, add the following 2 entries to the `Info.plist`: ```xml NSHealthShareUsageDescription @@ -35,24 +34,9 @@ Step 1: Append the `Info.plist` with the following 2 entries We will sync your data with the Apple Health app to give you better insights ``` -Step 2: Open your Flutter project in Xcode by right clicking on the "ios" folder and selecting "Open in Xcode". Next, enable "HealthKit" by adding a capability inside the "Signing & Capabilities" tab of the Runner target's settings. +Then, open your Flutter project in Xcode by right clicking on the "ios" folder and selecting "Open in Xcode". Next, enable "HealthKit" by adding a capability inside the "Signing & Capabilities" tab of the Runner target's settings. -### Android - -Starting from API level 28 (Android 9.0) accessing some fitness data (e.g. Steps) requires a special permission. To set it add the following line to your `AndroidManifest.xml` file. - -```xml - -``` - -Additionally, for workouts, if the distance of a workout is requested then the location permissions below are needed. - -```xml - - -``` - -#### Health Connect +### Google Health Connect (Android) Health Connect requires the following lines in the `AndroidManifest.xml` file (see also the example app): @@ -81,17 +65,38 @@ In the Health Connect permissions activity there is a link to your privacy polic ``` -Health Connect on Android it requires special permissions in the `AndroidManifest.xml` file. The permissions can be found here: +For each data type you want to access, the READ and WRITE permissions need to be added to the `AndroidManifest.xml` file. The list of [permissions](https://developer.android.com/health-and-fitness/guides/health-connect/plan/data-types#permissions) can be found here on the [data types](https://developer.android.com/health-and-fitness/guides/health-connect/plan/data-types) page. -Example shown here (can also be found in the example app): +An example of asking for permission to read and write heart rate data is shown below and more examples can also be found in the example app. ```xml -... ``` -Furthermore, an `intent-filter` needs to be added to the `.MainActivity` activity. +Accessing fitness data (e.g. Steps) requires permission to access the "Activity Recognition" API. To set it add the following line to your `AndroidManifest.xml` file. + +```xml + +``` + +Additionally, for workouts, if the distance of a workout is requested then the location permissions below are needed. + +```xml + + +``` + +Because this is labeled as a `dangerous` protection level, the permission system will not grant it automatically and it requires the user's action. +You can prompt the user for it using the [permission_handler](https://pub.dev/packages/permission_handler) plugin. +Follow the plugin setup instructions and add the following line before requesting the data: + +```dart +await Permission.activityRecognition.request(); +await Permission.location.request(); +``` + +Finally, an `intent-filter` needs to be added to the `.MainActivity` activity. ```xml Date: Thu, 22 Aug 2024 11:09:24 +0200 Subject: [PATCH 20/21] Fix an issue in generated file --- packages/health/lib/health.g.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/health/lib/health.g.dart b/packages/health/lib/health.g.dart index cd4da26b0..104be599d 100644 --- a/packages/health/lib/health.g.dart +++ b/packages/health/lib/health.g.dart @@ -324,7 +324,6 @@ const _$HealthWorkoutActivityTypeEnumMap = { HealthWorkoutActivityType.BOXING: 'BOXING', HealthWorkoutActivityType.CRICKET: 'CRICKET', HealthWorkoutActivityType.CROSS_COUNTRY_SKIING: 'CROSS_COUNTRY_SKIING', - HealthWorkoutActivityType.CROSS_TRAINING: 'CROSS_TRAINING', HealthWorkoutActivityType.CURLING: 'CURLING', HealthWorkoutActivityType.DOWNHILL_SKIING: 'DOWNHILL_SKIING', HealthWorkoutActivityType.ELLIPTICAL: 'ELLIPTICAL', From dd1f0352edcfff3dd76272f44ce8d9bef0e059d6 Mon Sep 17 00:00:00 2001 From: Aamir Farooq Date: Thu, 22 Aug 2024 11:11:04 +0200 Subject: [PATCH 21/21] When writing data, check if the type is available on the requested platform --- packages/health/example/lib/main.dart | 5 ----- packages/health/lib/src/health_plugin.dart | 4 ++++ 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/health/example/lib/main.dart b/packages/health/example/lib/main.dart index 316222413..3912bda41 100644 --- a/packages/health/example/lib/main.dart +++ b/packages/health/example/lib/main.dart @@ -217,11 +217,6 @@ 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); success &= await Health().writeHealthData( value: 37, type: HealthDataType.BODY_TEMPERATURE, diff --git a/packages/health/lib/src/health_plugin.dart b/packages/health/lib/src/health_plugin.dart index 87de5bcba..a55bc49e4 100644 --- a/packages/health/lib/src/health_plugin.dart +++ b/packages/health/lib/src/health_plugin.dart @@ -310,6 +310,10 @@ class Health { throw ArgumentError( "Adding workouts should be done using the writeWorkoutData method."); } + // If not implemented on platform, throw an exception + if (!isDataTypeAvailable(type)) { + throw HealthException(type, 'Not available on platform $platformType'); + } endTime ??= startTime; if (startTime.isAfter(endTime)) { throw ArgumentError("startTime must be equal or earlier than endTime");