Skip to content

Commit

Permalink
Support aggregate data types (#4)
Browse files Browse the repository at this point in the history
  • Loading branch information
p3psi-boo authored Mar 12, 2024
1 parent 0d94598 commit 916fdd9
Show file tree
Hide file tree
Showing 16 changed files with 266 additions and 61 deletions.
67 changes: 66 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
[Tasker](https://tasker.joaoapps.com/) plugin to interface with [Health Connect](https://developer.android.com/health-connect) on Android

## Current Features
- Get last X days step count
- ~~Get last X days step count~~ Retrieve all [Aggregate data](https://developer.android.com/health-and-fitness/guides/health-connect/develop/aggregate-data) for the last X days

## Installation
- Updates are currently released only on GitHub
Expand All @@ -14,6 +14,71 @@
- Run the app, it will check to make sure that Health Connect is installed and will prompt for required permissions
- Open Tasker, and look for 'Tasker Health Connect' inside Action -> Plugins

### Example Output

```json
[
{
"startTime":"2024-02-06T16:00:36.910Z",
"endTime":"2024-02-07T16:00:36.910Z",
"result":{
"Height_height_min":1.55,
"Height_height_avg":1.55,
"Height_height_max":1.55,
"Weight_weight_min":63,
"Weight_weight_avg":63.5,
"Weight_weight_max":64,
"BasalCaloriesBurned_energy_total":1523.29049265625,
"Nutrition_sugar_total":0,
"Nutrition_cholesterol_total":0,
"Nutrition_totalCarbohydrate_total":49.30000114440918,
"Nutrition_totalFat_total":1.0000000298023224,
"Nutrition_protein_total":1.600000023841858,
"Nutrition_dietaryFiber_total":5.5,
"Nutrition_iron_total":3.000000044703484E-4,
"Nutrition_vitaminA_total":2.0070000076293946E-4,
"Nutrition_vitaminC_total":0.0525,
"Nutrition_calcium_total":0.2390999984741211,
"Nutrition_calories_total":201,
"Nutrition_potassium_total":0.458,
"Nutrition_sodium_total":0.006,
"Nutrition_transFat_total":0,
"Nutrition_vitaminD_total":0,
"Nutrition_saturatedFat_total":0,
"TotalCaloriesBurned_energy_total":1523.29049265625,
"HeartRateSeries_bpm_min":68,
"HeartRate_bpm_min":68,
"HeartRateSeries_bpm_avg":79,
"HeartRate_bpm_avg":79,
"HeartRateSeries_bpm_max":92,
"HeartRate_bpm_max":92,
"Steps_count_total":5660,
"HeartRateSeries_count":48,
"HeartRate_count":48,
"SleepSession_duration":17040000
}
},
{
"startTime":"2024-02-05T16:00:36.910Z",
"endTime":"2024-02-06T16:00:36.910Z",
"result":{
"BasalCaloriesBurned_energy_total":1564.5,
"TotalCaloriesBurned_energy_total":1564.5,
"Steps_count_total":2
}
},
{
"startTime":"2024-02-07T16:00:36.910Z",
"endTime":"2024-02-08T12:01:36.910Z",
"result":{
"BasalCaloriesBurned_energy_total":1266.8881944444443,
"TotalCaloriesBurned_energy_total":1266.8881944444443,
"Steps_count_total":1423
}
}
]
```

## Building
- Clone the repository: `git clone https://github.com/RafhaanShah/TaskerHealthConnect`
- Build with gradle: `./gradlew assembleDebug`
Expand Down
1 change: 1 addition & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ android {
}

dependencies {
implementation "org.jetbrains.kotlin:kotlin-reflect:1.7.10"
implementation 'androidx.core:core-ktx:1.9.0'
implementation 'androidx.appcompat:appcompat:1.5.1'
implementation 'com.google.android.material:material:1.8.0-alpha02'
Expand Down
41 changes: 39 additions & 2 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,43 @@
package="com.rafapps.taskerhealthconnect">

<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.health.READ_ACTIVE_CALORIES_BURNED"/>
<uses-permission android:name="android.permission.health.READ_BASAL_BODY_TEMPERATURE"/>
<uses-permission android:name="android.permission.health.READ_BASAL_METABOLIC_RATE"/>
<uses-permission android:name="android.permission.health.READ_BLOOD_GLUCOSE"/>
<uses-permission android:name="android.permission.health.READ_BLOOD_PRESSURE"/>
<uses-permission android:name="android.permission.health.READ_BODY_FAT"/>
<uses-permission android:name="android.permission.health.READ_BODY_TEMPERATURE"/>
<uses-permission android:name="android.permission.health.READ_BODY_WATER_MASS"/>
<uses-permission android:name="android.permission.health.READ_BONE_MASS"/>
<uses-permission android:name="android.permission.health.READ_CERVICAL_MUCUS"/>
<uses-permission android:name="android.permission.health.READ_EXERCISE"/>
<uses-permission android:name="android.permission.health.READ_DISTANCE"/>
<uses-permission android:name="android.permission.health.READ_ELEVATION_GAINED"/>
<uses-permission android:name="android.permission.health.READ_EXERCISE"/>
<uses-permission android:name="android.permission.health.READ_FLOORS_CLIMBED"/>
<uses-permission android:name="android.permission.health.READ_HEART_RATE"/>
<uses-permission android:name="android.permission.health.READ_HEART_RATE_VARIABILITY"/>
<uses-permission android:name="android.permission.health.READ_HEIGHT"/>
<uses-permission android:name="android.permission.health.READ_HYDRATION"/>
<uses-permission android:name="android.permission.health.READ_INTERMENSTRUAL_BLEEDING"/>
<uses-permission android:name="android.permission.health.READ_LEAN_BODY_MASS"/>
<uses-permission android:name="android.permission.health.READ_MENSTRUATION"/>
<uses-permission android:name="android.permission.health.READ_NUTRITION"/>
<uses-permission android:name="android.permission.health.READ_OVULATION_TEST"/>
<uses-permission android:name="android.permission.health.READ_OXYGEN_SATURATION"/>
<uses-permission android:name="android.permission.health.READ_POWER"/>
<uses-permission android:name="android.permission.health.READ_RESPIRATORY_RATE"/>
<uses-permission android:name="android.permission.health.READ_RESTING_HEART_RATE"/>
<uses-permission android:name="android.permission.health.READ_SEXUAL_ACTIVITY"/>
<uses-permission android:name="android.permission.health.READ_SLEEP"/>
<uses-permission android:name="android.permission.health.READ_SPEED"/>
<uses-permission android:name="android.permission.health.READ_STEPS"/>
<uses-permission android:name="android.permission.health.READ_TOTAL_CALORIES_BURNED"/>
<uses-permission android:name="android.permission.health.READ_VO2_MAX"/>
<uses-permission android:name="android.permission.health.READ_WEIGHT"/>
<uses-permission android:name="android.permission.health.READ_WHEELCHAIR_PUSHES"/>


<queries>
<package android:name="com.google.android.apps.healthdata" />
Expand Down Expand Up @@ -38,9 +75,9 @@
</activity>

<activity
android:name=".getsteps.GetStepsActivity"
android:name=".tasker.GetHealthDataActivity"
android:exported="true"
android:label="@string/get_steps">
android:label="@string/get_data">
<intent-filter>
<action android:name="com.twofortyfouram.locale.intent.action.EDIT_SETTING" />
</intent-filter>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,101 @@ package com.rafapps.taskerhealthconnect
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.util.Log
import androidx.health.connect.client.HealthConnectClient
import androidx.health.connect.client.aggregate.AggregateMetric
import androidx.health.connect.client.aggregate.AggregationResult
import androidx.health.connect.client.permission.HealthPermission
import androidx.health.connect.client.records.StepsRecord
import androidx.health.connect.client.records.*
import androidx.health.connect.client.request.AggregateGroupByDurationRequest
import androidx.health.connect.client.time.TimeRangeFilter
import org.json.JSONArray
import org.json.JSONObject
import java.lang.Exception
import java.time.Duration
import java.time.Instant
import kotlin.reflect.KClass
import kotlin.reflect.full.companionObject
import kotlin.reflect.full.declaredMemberProperties
import kotlin.reflect.full.memberProperties
import kotlin.reflect.jvm.isAccessible
import kotlin.reflect.typeOf

class HealthConnectRepository(private val context: Context) {

companion object {
val TAG = "HealthConnectRepository"
}

private val playStoreUri =
"https://play.google.com/store/apps/details?id=com.google.android.apps.healthdata"

private val client by lazy { HealthConnectClient.getOrCreate(context) }

val permissions = setOf(
HealthPermission.createReadPermission(StepsRecord::class)
val recordTypes = setOf<KClass<out Record>>(
ActiveCaloriesBurnedRecord::class,
BasalBodyTemperatureRecord::class,
BasalMetabolicRateRecord::class,
BloodGlucoseRecord::class,
BloodPressureRecord::class,
BodyFatRecord::class,
BodyTemperatureRecord::class,
BodyWaterMassRecord::class,
BoneMassRecord::class,
CervicalMucusRecord::class,
CyclingPedalingCadenceRecord::class,
DistanceRecord::class,
ElevationGainedRecord::class,
ExerciseSessionRecord::class,
FloorsClimbedRecord::class,
HeartRateRecord::class,
HeartRateVariabilityRmssdRecord::class,
HeightRecord::class,
HydrationRecord::class,
LeanBodyMassRecord::class,
MenstruationFlowRecord::class,
NutritionRecord::class,
OvulationTestRecord::class,
OxygenSaturationRecord::class,
PowerRecord::class,
RespiratoryRateRecord::class,
RestingHeartRateRecord::class,
SexualActivityRecord::class,
SleepSessionRecord::class,
SpeedRecord::class,
StepsCadenceRecord::class,
StepsRecord::class,
TotalCaloriesBurnedRecord::class,
Vo2MaxRecord::class,
WeightRecord::class,
WheelchairPushesRecord::class,
)

val permissions by lazy { recordTypes.map { HealthPermission.createReadPermission(it) }.toSet() }

fun getAllAggregateMetrics(): Set<AggregateMetric<*>> {
val aggregateMetrics = mutableSetOf<AggregateMetric<*>>()
val targetType = typeOf<AggregateMetric<*>>()

recordTypes.forEach { recordType ->
try {
val companionObj = recordType.companionObject?.objectInstance ?: return@forEach
val properties = companionObj::class.declaredMemberProperties.filter {
it.returnType.classifier == targetType.classifier
}

properties.forEach { property ->
val value = property.getter.call(companionObj) as AggregateMetric<*>
aggregateMetrics.add(value)
}
}
catch (e: Exception) {
Log.e(TAG, "getAllAggregateMetrics: ", e)
}
}
return aggregateMetrics.toSet()
}

fun installHealthConnect() {
val intent = Intent(Intent.ACTION_VIEW).apply {
data = Uri.parse(playStoreUri)
Expand All @@ -38,23 +114,46 @@ class HealthConnectRepository(private val context: Context) {
return granted.containsAll(permissions)
}

suspend fun countSteps(
suspend fun getData(
startTime: Instant,
endTime: Instant
): Long {
val response = client.aggregateGroupByDuration(
AggregateGroupByDurationRequest(
metrics = setOf(StepsRecord.COUNT_TOTAL),
timeRangeFilter = TimeRangeFilter.between(startTime, endTime),
timeRangeSlicer = Duration.ofDays(1)
): String {
val result: JSONArray = JSONArray()
val metrics = getAllAggregateMetrics()

try {
val response = client.aggregateGroupByDuration(
AggregateGroupByDurationRequest(
metrics = metrics,
timeRangeFilter = TimeRangeFilter.between(startTime, endTime),
timeRangeSlicer = Duration.ofDays(1)
)
)
)
response.forEach {
val data = mutableMapOf<String, Any?>()
val properties = AggregationResult::class.memberProperties
properties.forEach { p ->
run {
p.isAccessible = true;
if (p.name == "doubleValues" || p.name == "longValues") {
val values = p.call(it.result)

var count = 0L
for (record in response) {
count += record.result[StepsRecord.COUNT_TOTAL] ?: 0L
data.putAll(values as Map<out String, Any?>)
}
}
}
result.put(JSONObject(mapOf(
"startTime" to it.startTime,
"endTime" to it.endTime,
"result" to data
)))
}
return result.toString()
}
catch (e: Exception) {
Log.e(TAG, "getData: ", e)
return "[]"
}

return count
}
}

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.rafapps.taskerhealthconnect.tasker

import com.joaomgcd.taskerpluginlibrary.config.TaskerPluginConfig
import com.joaomgcd.taskerpluginlibrary.config.TaskerPluginConfigHelper

class GetHealthDataActionHelper(config: TaskerPluginConfig<GetHealthDataInput>) :
TaskerPluginConfigHelper<GetHealthDataInput, GetHealthDataOutput, GetHealthDataActionRunner>(config) {
override val inputClass = GetHealthDataInput::class.java
override val outputClass = GetHealthDataOutput::class.java
override val runnerClass = GetHealthDataActionRunner::class.java
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.rafapps.taskerhealthconnect.getsteps
package com.rafapps.taskerhealthconnect.tasker

import android.content.Context
import com.joaomgcd.taskerpluginlibrary.action.TaskerPluginRunnerAction
Expand All @@ -11,14 +11,14 @@ import kotlinx.coroutines.runBlocking
import java.time.Instant
import java.time.ZonedDateTime

class GetStepsActionRunner : TaskerPluginRunnerAction<GetStepsInput, GetStepsOutput>() {
class GetHealthDataActionRunner : TaskerPluginRunnerAction<GetHealthDataInput, GetHealthDataOutput>() {
override val notificationProperties
get() = NotificationProperties(iconResId = R.drawable.ic_launcher_foreground)

override fun run(
context: Context,
input: TaskerInput<GetStepsInput>
): TaskerPluginResult<GetStepsOutput> {
input: TaskerInput<GetHealthDataInput>
): TaskerPluginResult<GetHealthDataOutput> {
val repository = HealthConnectRepository(context)
val daysOffset = input.regular.days
val now = Instant.now()
Expand All @@ -28,7 +28,7 @@ class GetStepsActionRunner : TaskerPluginRunnerAction<GetStepsInput, GetStepsOut
.minusMinutes(zonedDateTime.minute.toLong())
.toInstant()

val steps = runBlocking { repository.countSteps(startTime = offsetTime, endTime = now) }
return TaskerPluginResultSucess(GetStepsOutput(steps))
val data = runBlocking { repository.getData(startTime = offsetTime, endTime = now) }
return TaskerPluginResultSucess(GetHealthDataOutput(data))
}
}
Loading

0 comments on commit 916fdd9

Please sign in to comment.