Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Health] Expose additional methods for HealthFactory #799

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions packages/health/android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,14 @@ apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'

android {
compileSdkVersion 34
compileSdk 34

sourceSets {
main.java.srcDirs += 'src/main/kotlin'
}
defaultConfig {
minSdkVersion 28
targetSdkVersion 33
targetSdkVersion 34
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
lintOptions {
Expand All @@ -46,7 +46,8 @@ dependencies {
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")
implementation("androidx.fragment:fragment:1.6.1")

// The new health connect api
implementation("androidx.health.connect:connect-client:1.1.0-alpha03")
implementation("androidx.health.connect:connect-client:1.1.0-alpha04")
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import android.content.pm.PackageManager
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
Expand All @@ -33,6 +35,7 @@ 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.android.FlutterFragmentActivity
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.embedding.engine.plugins.activity.ActivityAware
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
Expand All @@ -47,6 +50,7 @@ import java.time.*
import java.time.temporal.ChronoUnit
import java.util.*
import java.util.concurrent.*
import kotlin.reflect.KClass


const val GOOGLE_FIT_PERMISSIONS_REQUEST_CODE = 1111
Expand All @@ -68,7 +72,7 @@ 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 useHealthConnectIfAvailable: Boolean = true
private lateinit var healthConnectClient: HealthConnectClient
private lateinit var scope: CoroutineScope

Expand All @@ -91,6 +95,8 @@ class HealthPlugin(private var channel: MethodChannel? = null) :
private var BASAL_ENERGY_BURNED = "BASAL_ENERGY_BURNED"
private var FLIGHTS_CLIMBED = "FLIGHTS_CLIMBED"
private var RESPIRATORY_RATE = "RESPIRATORY_RATE"
private var HEART_RATE_VARIABILITY_SDNN = "HEART_RATE_VARIABILITY_SDNN"
private var LEAN_BODY_MASS = "LEAN_BODY_MASS"

// TODO support unknown?
private var SLEEP_ASLEEP = "SLEEP_ASLEEP"
Expand Down Expand Up @@ -343,10 +349,6 @@ class HealthPlugin(private var channel: MethodChannel? = null) :
context = flutterPluginBinding.applicationContext
threadPoolExecutor = Executors.newFixedThreadPool(4)
checkAvailability()
if (healthConnectAvailable) {
healthConnectClient =
HealthConnectClient.getOrCreate(flutterPluginBinding.applicationContext)
}
}

override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
Expand Down Expand Up @@ -1229,27 +1231,7 @@ class HealthPlugin(private var channel: MethodChannel? = null) :
}
mResult = result

if (useHealthConnectIfAvailable && healthConnectAvailable) {
requestAuthorizationHC(call, result)
return
}

val optionsToRegister = callToHealthTypes(call)

// Set to false due to bug described in https://github.com/cph-cachet/flutter-plugins/issues/640#issuecomment-1366830132
val isGranted = false

// If not granted then ask for permission
if (!isGranted && activity != null) {
GoogleSignIn.requestPermissions(
activity!!,
GOOGLE_FIT_PERMISSIONS_REQUEST_CODE,
GoogleSignIn.getLastSignedInAccount(context!!),
optionsToRegister,
)
} else { // / Permission already granted
result?.success(true)
}
requestAuthorizationHC(call, result)
}

/**
Expand Down Expand Up @@ -1392,6 +1374,8 @@ class HealthPlugin(private var channel: MethodChannel? = null) :
override fun onMethodCall(call: MethodCall, result: Result) {
when (call.method) {
"useHealthConnectIfAvailable" -> useHealthConnectIfAvailable(call, result)
"checkAvailability" -> checkAvailability(call, result)
"openSystemSettings" -> openSystemSettings(call, result)
"hasPermissions" -> hasPermissions(call, result)
"requestAuthorization" -> requestAuthorization(call, result)
"revokePermissions" -> revokePermissions(call, result)
Expand All @@ -1412,6 +1396,10 @@ class HealthPlugin(private var channel: MethodChannel? = null) :
}
binding.addActivityResultListener(this)
activity = binding.activity

permissionsLauncher = (activity as ComponentActivity).registerForActivityResult(
PermissionController.createRequestPermissionResultContract()
) { granted -> onPermissionsResult(granted) }
}

override fun onDetachedFromActivityForConfigChanges() {
Expand All @@ -1435,9 +1423,25 @@ class HealthPlugin(private var channel: MethodChannel? = null) :
var healthConnectAvailable = false
var healthConnectStatus = HealthConnectClient.SDK_UNAVAILABLE

fun checkAvailability() {
fun checkAvailability(call: MethodCall? = null, result: Result? = null) {
healthConnectStatus = HealthConnectClient.getSdkStatus(context!!)
healthConnectAvailable = healthConnectStatus == HealthConnectClient.SDK_AVAILABLE
if (healthConnectAvailable) {
healthConnectClient = HealthConnectClient.getOrCreate(context!!)
}
result?.success(healthConnectAvailable)
}

fun openSystemSettings(call: MethodCall, result: Result) {
try {
val intent = Intent()
intent.action = HealthConnectClient.ACTION_HEALTH_CONNECT_SETTINGS
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
context?.startActivity(intent)
result.success(true)
} catch (e: Throwable) {
result.error("UNABLE_TO_START_ACTIVITY", e.message, e)
}
}

fun useHealthConnectIfAvailable(call: MethodCall, result: Result) {
Expand Down Expand Up @@ -1495,6 +1499,13 @@ class HealthPlugin(private var channel: MethodChannel? = null) :
}
}

private lateinit var requestedPermissions: Set<String>
private lateinit var permissionsLauncher: ActivityResultLauncher<Set<String>>

private fun onPermissionsResult(granted: Set<String>) {
mResult?.success(granted.containsAll(requestedPermissions))
}

private fun requestAuthorizationHC(call: MethodCall, result: Result) {
val args = call.arguments as HashMap<*, *>
val types = (args["types"] as? ArrayList<*>)?.filterIsInstance<String>()!!
Expand Down Expand Up @@ -1537,9 +1548,8 @@ class HealthPlugin(private var channel: MethodChannel? = null) :
}
}
}
val contract = PermissionController.createRequestPermissionResultContract()
val intent = contract.createIntent(activity!!, permList.toSet())
activity!!.startActivityForResult(intent, HEALTH_CONNECT_RESULT_CODE)
requestedPermissions = permList.toSet()
permissionsLauncher.launch(permList.toSet())
}

fun getHCData(call: MethodCall, result: Result) {
Expand Down Expand Up @@ -1795,6 +1805,24 @@ class HealthPlugin(private var channel: MethodChannel? = null) :
"source_name" to metadata.dataOrigin.packageName,
)
)
is HeartRateVariabilityRmssdRecord -> return listOf(
mapOf<String, Any>(
"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 LeanBodyMassRecord -> return listOf(
mapOf<String, Any>(
"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 ExerciseSessionRecord -> return listOf(mapOf<String, Any>("value" to ,
// "date_from" to ,
// "date_to" to ,
Expand Down Expand Up @@ -1952,6 +1980,16 @@ class HealthPlugin(private var channel: MethodChannel? = null) :
rate = value,
zoneOffset = null,
)
HEART_RATE_VARIABILITY_SDNN -> HeartRateVariabilityRmssdRecord(
time = Instant.ofEpochMilli(startTime),
heartRateVariabilityMillis = value,
zoneOffset = null,
)
LEAN_BODY_MASS -> LeanBodyMassRecord(
time = Instant.ofEpochMilli(startTime),
mass = Mass.kilograms(value),
zoneOffset = null,
)
// AGGREGATE_STEP_COUNT -> StepsRecord()
BLOOD_PRESSURE_SYSTOLIC -> throw IllegalArgumentException("You must use the [writeBloodPressure] API ")
BLOOD_PRESSURE_DIASTOLIC -> throw IllegalArgumentException("You must use the [writeBloodPressure] API ")
Expand Down Expand Up @@ -2091,6 +2129,13 @@ class HealthPlugin(private var channel: MethodChannel? = null) :
6 to SLEEP_REM,
)

fun sleepRecordTypeForOS(): KClass<out Record> {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
SleepSessionRecord::class
else
SleepStageRecord::class
}

val MapToHCType = hashMapOf(
BODY_FAT_PERCENTAGE to BodyFatRecord::class,
HEIGHT to HeightRecord::class,
Expand All @@ -2106,18 +2151,20 @@ class HealthPlugin(private var channel: MethodChannel? = null) :
BLOOD_GLUCOSE to BloodGlucoseRecord::class,
DISTANCE_DELTA to DistanceRecord::class,
WATER to HydrationRecord::class,
SLEEP_ASLEEP to SleepStageRecord::class,
SLEEP_AWAKE to SleepStageRecord::class,
SLEEP_LIGHT to SleepStageRecord::class,
SLEEP_DEEP to SleepStageRecord::class,
SLEEP_REM to SleepStageRecord::class,
SLEEP_OUT_OF_BED to SleepStageRecord::class,
SLEEP_ASLEEP to sleepRecordTypeForOS(),
SLEEP_AWAKE to sleepRecordTypeForOS(),
SLEEP_LIGHT to sleepRecordTypeForOS(),
SLEEP_DEEP to sleepRecordTypeForOS(),
SLEEP_REM to sleepRecordTypeForOS(),
SLEEP_OUT_OF_BED to sleepRecordTypeForOS(),
SLEEP_SESSION to SleepSessionRecord::class,
WORKOUT to ExerciseSessionRecord::class,
RESTING_HEART_RATE to RestingHeartRateRecord::class,
BASAL_ENERGY_BURNED to BasalMetabolicRateRecord::class,
FLIGHTS_CLIMBED to FloorsClimbedRecord::class,
RESPIRATORY_RATE to RespiratoryRateRecord::class,
HEART_RATE_VARIABILITY_SDNN to HeartRateVariabilityRmssdRecord::class,
LEAN_BODY_MASS to LeanBodyMassRecord::class,
// MOVE_MINUTES to TODO: Find alternative?
// TODO: Implement remaining types
// "ActiveCaloriesBurned" to ActiveCaloriesBurnedRecord::class,
Expand All @@ -2137,7 +2184,6 @@ class HealthPlugin(private var channel: MethodChannel? = null) :
// "HeartRate" to HeartRateRecord::class,
// "Height" to HeightRecord::class,
// "Hydration" to HydrationRecord::class,
// "LeanBodyMass" to LeanBodyMassRecord::class,
// "MenstruationFlow" to MenstruationFlowRecord::class,
// "MenstruationPeriod" to MenstruationPeriodRecord::class,
// "Nutrition" to NutritionRecord::class,
Expand Down
10 changes: 10 additions & 0 deletions packages/health/example/android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@
<uses-permission android:name="android.permission.health.READ_BASAL_METABOLIC_RATE"/>
<uses-permission android:name="android.permission.health.READ_RESPIRATORY_RATE"/>
<uses-permission android:name="android.permission.health.WRITE_RESPIRATORY_RATE"/>
<uses-permission android:name="android.permission.health.READ_HEART_RATE_VARIABILITY"/>
<uses-permission android:name="android.permission.health.WRITE_HEART_RATE_VARIABILITY"/>
<uses-permission android:name="android.permission.health.READ_LEAN_BODY_MASS"/>
<uses-permission android:name="android.permission.health.WRITE_LEAN_BODY_MASS"/>


<application android:label="health_example"
Expand All @@ -78,6 +82,12 @@
<intent-filter>
<action android:name="androidx.health.ACTION_SHOW_PERMISSIONS_RATIONALE" />
</intent-filter>

<!-- Health Permission handling for Android 14 -->
<intent-filter>
<action android:name="android.intent.action.VIEW_PERMISSION_USAGE"/>
<category android:name="android.intent.category.HEALTH_PERMISSIONS"/>
</intent-filter>
</activity>
<meta-data android:name="flutterEmbedding"
android:value="2"/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package cachet.plugins.example_app

import android.os.Bundle

import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.android.FlutterFragmentActivity

class MainActivity: FlutterActivity() {
class MainActivity: FlutterFragmentActivity() {
}
30 changes: 30 additions & 0 deletions packages/health/example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,22 @@ class _HealthAppState extends State<HealthApp> {
}
}

Future<void> openSettings() async {
try {
await health.openSystemSettings();
} catch (error) {
print("Caught exception in openSettings: $error");
}
}

Future<void> checkAvailability() async {
try {
await health.checkAvailability();
} catch (error) {
print("Caught exception in checkAvailability: $error");
}
}

Widget _contentFetchingData() {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
Expand Down Expand Up @@ -412,6 +428,20 @@ class _HealthAppState extends State<HealthApp> {
style: ButtonStyle(
backgroundColor:
MaterialStatePropertyAll(Colors.blue))),
TextButton(
onPressed: checkAvailability,
child: Text("Check availability",
style: TextStyle(color: Colors.white)),
style: ButtonStyle(
backgroundColor:
MaterialStatePropertyAll(Colors.blue))),
TextButton(
onPressed: openAppSettings,
child: Text("Open settings",
style: TextStyle(color: Colors.white)),
style: ButtonStyle(
backgroundColor:
MaterialStatePropertyAll(Colors.blue))),
],
),
Divider(thickness: 3),
Expand Down
Loading