Skip to content

Add get HealthData by UUID method for Android #1194

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

Open
wants to merge 12 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
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,76 @@ class HealthDataReader(
}
}

/**
* Retrieves single health data point by given UUID and type.
*
* @param call Method call containing 'UUID' and 'dataTypeKey'
* @param result Flutter result callback returning list of health data maps
*/
fun getDataByUUID(call: MethodCall, result: Result) {
val dataType = call.argument<String>("dataTypeKey")!!
val uuid = call.argument<String>("uuid")!!
var healthPoint = mapOf<String, Any?>()

if (!HealthConstants.mapToType.containsKey(dataType)) {
Log.w("FLUTTER_HEALTH::ERROR", "Datatype $dataType not found in HC")
result.success(null)
return
}

val classType = HealthConstants.mapToType[dataType]!!

scope.launch {
try {

Log.i("FLUTTER_HEALTH", "Getting $uuid with $classType")

// Execute the request
val response = healthConnectClient.readRecord(classType, uuid)

// Find the record with the matching UUID
val matchingRecord = response.record

if (matchingRecord != null) {
// Handle special cases using shared logic
when (dataType) {
WORKOUT -> {
val tempData = mutableListOf<Map<String, Any?>>()
handleWorkoutData(listOf(matchingRecord), emptyList(), tempData)
healthPoint = if (tempData.isNotEmpty()) tempData[0] else mapOf()
}
SLEEP_SESSION, SLEEP_ASLEEP, SLEEP_AWAKE, SLEEP_AWAKE_IN_BED,
SLEEP_LIGHT, SLEEP_DEEP, SLEEP_REM, SLEEP_OUT_OF_BED, SLEEP_UNKNOWN -> {
if (matchingRecord is SleepSessionRecord) {
val tempData = mutableListOf<Map<String, Any?>>()
handleSleepData(listOf(matchingRecord), emptyList(), dataType, tempData)
healthPoint = if (tempData.isNotEmpty()) tempData[0] else mapOf()
}
}
else -> {
healthPoint = dataConverter.convertRecord(matchingRecord, dataType)[0]
}
}

Log.i(
"FLUTTER_HEALTH",
"Success: $healthPoint"
)

Handler(context.mainLooper).run { result.success(healthPoint) }
} else {
Log.e("FLUTTER_HEALTH::ERROR", "Record not found for UUID: $uuid")
result.success(null)
}
} catch (e: Exception) {
Log.e("FLUTTER_HEALTH::ERROR", "Error fetching record with UUID: $uuid")
Log.e("FLUTTER_HEALTH::ERROR", e.message ?: "unknown error")
Log.e("FLUTTER_HEALTH::ERROR", e.stackTraceToString())
result.success(null)
}
}
}

/**
* Retrieves aggregated health data grouped by time intervals.
* Calculates totals, averages, or counts over specified time periods.
Expand Down Expand Up @@ -289,18 +359,22 @@ class HealthDataReader(
* by querying related records within the workout time period.
*
* @param records List of ExerciseSessionRecord objects
* @param recordingMethodsToFilter Recording methods to exclude
* @param recordingMethodsToFilter Recording methods to exclude (empty list means no filtering)
* @param healthConnectData Mutable list to append processed workout data
*/
private suspend fun handleWorkoutData(
records: List<Record>,
recordingMethodsToFilter: List<Int>,
recordingMethodsToFilter: List<Int> = emptyList(),
healthConnectData: MutableList<Map<String, Any?>>
) {
val filteredRecords = recordingFilter.filterRecordsByRecordingMethods(
recordingMethodsToFilter,
val filteredRecords = if (recordingMethodsToFilter.isEmpty()) {
records
)
} else {
recordingFilter.filterRecordsByRecordingMethods(
recordingMethodsToFilter,
records
)
}

for (rec in filteredRecords) {
val record = rec as ExerciseSessionRecord
Expand Down Expand Up @@ -366,8 +440,8 @@ class HealthDataReader(
"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(),
"date_from" to record.startTime.toEpochMilli(),
"date_to" to record.endTime.toEpochMilli(),
"source_id" to "",
"source_name" to record.metadata.dataOrigin.packageName,
),
Expand All @@ -381,20 +455,24 @@ class HealthDataReader(
* Converts sleep stage enumerations to meaningful duration and type information.
*
* @param records List of SleepSessionRecord objects
* @param recordingMethodsToFilter Recording methods to exclude
* @param recordingMethodsToFilter Recording methods to exclude (empty list means no filtering)
* @param dataType Specific sleep data type being requested
* @param healthConnectData Mutable list to append processed sleep data
*/
private fun handleSleepData(
records: List<Record>,
recordingMethodsToFilter: List<Int>,
recordingMethodsToFilter: List<Int> = emptyList(),
dataType: String,
healthConnectData: MutableList<Map<String, Any?>>
) {
val filteredRecords = recordingFilter.filterRecordsByRecordingMethods(
recordingMethodsToFilter,
val filteredRecords = if (recordingMethodsToFilter.isEmpty()) {
records
)
} else {
recordingFilter.filterRecordsByRecordingMethods(
recordingMethodsToFilter,
records
)
}

for (rec in filteredRecords) {
if (rec is SleepSessionRecord) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ class HealthPlugin(private var channel: MethodChannel? = null) :

// Reading data
"getData" -> dataReader.getData(call, result)
"getDataByUUID" -> dataReader.getDataByUUID(call, result)
"getIntervalData" -> dataReader.getIntervalData(call, result)
"getAggregateData" -> dataReader.getAggregateData(call, result)
"getTotalStepsInInterval" -> dataReader.getTotalStepsInInterval(call, result)
Expand Down
4 changes: 2 additions & 2 deletions packages/health/example/.metadata
Original file line number Diff line number Diff line change
Expand Up @@ -41,5 +41,5 @@ migration:
#
# Files that are not part of the templates will be ignored by default.
unmanaged_files:
- 'lib/main.dart'
- 'ios/Runner.xcodeproj/project.pbxproj'
- "lib/main.dart"
- "ios/Runner.xcodeproj/project.pbxproj"
12 changes: 6 additions & 6 deletions packages/health/example/README.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
# health_example
# example_new

Demonstrates how to use the health plugin.
A new Flutter project.

## Getting Started

This project is a starting point for a Flutter application.

A few resources to get you started if this is your first Flutter project:

- [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab)
- [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook)
- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)

For help getting started with Flutter, view our
[online documentation](https://flutter.dev/docs), which offers tutorials,
For help getting started with Flutter development, view the
[online documentation](https://docs.flutter.dev/), which offers tutorials,
samples, guidance on mobile development, and a full API reference.
1 change: 0 additions & 1 deletion packages/health/example/analysis_options.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,5 @@ linter:
rules:
# avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule

# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options
44 changes: 44 additions & 0 deletions packages/health/example/android/app/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
plugins {
id "com.android.application"
id "kotlin-android"
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
id "dev.flutter.flutter-gradle-plugin"
}

android {
namespace = "cachet.plugins.health.health_example"
compileSdk = 35
ndkVersion = '25.1.8937393'

compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}

kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8
}

defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "cachet.plugins.health.health_example"
// You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = 26
targetSdk = 35
versionCode = flutter.versionCode
versionName = flutter.versionName
}

buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig = signingConfigs.debug
}
}
}

flutter {
source = "../.."
}
4 changes: 2 additions & 2 deletions packages/health/example/android/app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ plugins {

android {
namespace = "cachet.plugins.health.health_example"
compileSdk = flutter.compileSdkVersion
compileSdk = 35
// ndkVersion = flutter.ndkVersion

compileOptions {
Expand All @@ -25,7 +25,7 @@ android {
// You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = 26
targetSdk = flutter.targetSdkVersion
targetSdk = 35
versionCode = flutter.versionCode
versionName = flutter.versionName
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@
<action android:name="androidx.health.ACTION_SHOW_PERMISSIONS_RATIONALE" />
</intent-filter>
</activity>

<activity-alias
android:name="ViewPermissionUsageActivity"
android:exported="true"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
package cachet.plugins.health.health_example

import android.os.Bundle

import io.flutter.embedding.android.FlutterFragmentActivity

class MainActivity: FlutterFragmentActivity() {
Expand Down
18 changes: 18 additions & 0 deletions packages/health/example/android/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
allprojects {
repositories {
google()
mavenCentral()
}
}

rootProject.buildDir = "../build"
subprojects {
project.buildDir = "${rootProject.buildDir}/${project.name}"
}
subprojects {
project.evaluationDependsOn(":app")
}

tasks.register("clean", Delete) {
delete rootProject.buildDir
}
25 changes: 25 additions & 0 deletions packages/health/example/android/settings.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
pluginManagement {
def flutterSdkPath = {
def properties = new Properties()
file("local.properties").withInputStream { properties.load(it) }
def flutterSdkPath = properties.getProperty("flutter.sdk")
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
return flutterSdkPath
}()

includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")

repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}

plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
id "com.android.application" version "8.3.0" apply false
id "org.jetbrains.kotlin.android" version "1.9.10" apply false
}

include ":app"
Loading