Skip to content

Commit

Permalink
Add raw health record Tasker activity #6
Browse files Browse the repository at this point in the history
  • Loading branch information
RafhaanShah committed Apr 27, 2024
1 parent 3d52789 commit aa04921
Show file tree
Hide file tree
Showing 9 changed files with 376 additions and 1 deletion.
26 changes: 25 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,31 @@
[Tasker](https://tasker.joaoapps.com/) plugin to interface with [Health Connect](https://developer.android.com/health-connect) on Android

## Current Features
- Retrieve [Aggregate data](https://developer.android.com/health-and-fitness/guides/health-connect/develop/aggregate-data) for the last X days as JSON. See [HealthConnectRepository](app/src/main/java/com/rafapps/taskerhealthconnect/HealthConnectRepository.kt) and [HealthConnectDataTypes](app/src/main/java/com/rafapps/taskerhealthconnect/HealthConnectDataTypes.kt) for all data types, units, and JSON keys.
- See [HealthConnectRepository](app/src/main/java/com/rafapps/taskerhealthconnect/HealthConnectRepository.kt) and [HealthConnectDataTypes](app/src/main/java/com/rafapps/taskerhealthconnect/HealthConnectDataTypes.kt) for all data types, units, and JSON keys.
- Retrieve [Raw Health Records](https://developer.android.com/health-and-fitness/guides/health-connect/develop/read-data) for the last X time period as JSON.
```json
[
{
"diastolic": 13,
"measurementLocation": 3,
"systolic": 59,
"time": "2024-04-27T13:02:50.265Z"
},
{
"diastolic": 134,
"measurementLocation": 3,
"systolic": 51,
"time": "2024-04-27T14:02:50.265Z"
},
{
"diastolic": 34,
"measurementLocation": 3,
"systolic": 54,
"time": "2024-04-27T15:02:50.265Z"
}
]
```
- Retrieve [Aggregate data](https://developer.android.com/health-and-fitness/guides/health-connect/develop/aggregate-data) for the last X days as JSON.
```json
[
{
Expand Down
9 changes: 9 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -92,5 +92,14 @@
<action android:name="com.twofortyfouram.locale.intent.action.EDIT_SETTING" />
</intent-filter>
</activity>

<activity
android:name=".healthdata.HealthDataActivity"
android:exported="true"
android:label="@string/health_data">
<intent-filter>
<action android:name="com.twofortyfouram.locale.intent.action.EDIT_SETTING" />
</intent-filter>
</activity>
</application>
</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.rafapps.taskerhealthconnect.healthdata

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

class HealthDataActionHelper(config: TaskerPluginConfig<HealthDataInput>) :
TaskerPluginConfigHelper<HealthDataInput, HealthDataOutput, HealthDataActionRunner>(config) {
override val inputClass = HealthDataInput::class.java
override val outputClass = HealthDataOutput::class.java
override val runnerClass = HealthDataActionRunner::class.java
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package com.rafapps.taskerhealthconnect.healthdata

import android.content.Context
import android.util.Log
import com.joaomgcd.taskerpluginlibrary.action.TaskerPluginRunnerAction
import com.joaomgcd.taskerpluginlibrary.input.TaskerInput
import com.joaomgcd.taskerpluginlibrary.runner.TaskerPluginResult
import com.joaomgcd.taskerpluginlibrary.runner.TaskerPluginResultErrorWithOutput
import com.joaomgcd.taskerpluginlibrary.runner.TaskerPluginResultSucess
import com.rafapps.taskerhealthconnect.HealthConnectRepository
import com.rafapps.taskerhealthconnect.R
import com.rafapps.taskerhealthconnect.aggregated.AggregatedHealthDataActionRunner.Companion.daysToOffsetTime
import kotlinx.coroutines.runBlocking
import java.time.Instant
import java.time.ZonedDateTime

class HealthDataActionRunner :
TaskerPluginRunnerAction<HealthDataInput, HealthDataOutput>() {

private val TAG = "HealthDataActionRunner"

override val notificationProperties
get() = NotificationProperties(iconResId = R.drawable.ic_launcher_foreground)

override fun run(
context: Context,
input: TaskerInput<HealthDataInput>
): TaskerPluginResult<HealthDataOutput> {
Log.d(TAG, "run: $input")
val repository = HealthConnectRepository(context)
val timeMs = runCatching { input.regular.fromTimeMillis.toLong() }.getOrElse {
Log.e(TAG, "invalid input: ${input.regular.fromTimeMillis}")
return TaskerPluginResultErrorWithOutput(Throwable(it))
}
val offsetTime = Instant.ofEpochMilli(timeMs)

if (!repository.isAvailable() || runBlocking { !repository.hasPermissions() }) {
val errMessage = context.getString(R.string.health_connect_unavailable_or_permissions)
Log.d(TAG, errMessage)
return TaskerPluginResultErrorWithOutput(Throwable(errMessage))
}

return try {
val data = runBlocking {
repository.getData(
input.regular.recordType,
startTime = offsetTime,
endTime = Instant.now()
)
}
TaskerPluginResultSucess(HealthDataOutput(healthData = data.toString()))
} catch (e: Exception) {
Log.e(TAG, "run error:", e)
TaskerPluginResultErrorWithOutput(e)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
package com.rafapps.taskerhealthconnect.healthdata

import android.content.Context
import android.os.Bundle
import android.util.Log
import android.view.inputmethod.InputMethodManager
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.WindowCompat
import androidx.core.view.isVisible
import androidx.health.connect.client.PermissionController
import androidx.lifecycle.lifecycleScope
import com.joaomgcd.taskerpluginlibrary.config.TaskerPluginConfig
import com.joaomgcd.taskerpluginlibrary.input.TaskerInput
import com.rafapps.taskerhealthconnect.BuildConfig
import com.rafapps.taskerhealthconnect.HealthConnectRepository
import com.rafapps.taskerhealthconnect.R
import com.rafapps.taskerhealthconnect.databinding.ActivityHealthDataBinding
import kotlinx.coroutines.launch
import java.time.Instant

class HealthDataActivity : AppCompatActivity(),
TaskerPluginConfig<HealthDataInput> {

private val TAG = "HealthDataActivity"
private lateinit var binding: ActivityHealthDataBinding
private val repository by lazy { HealthConnectRepository(this) }
private val taskerHelper by lazy { HealthDataActionHelper(this) }

private val permissionsLauncher =
registerForActivityResult(
PermissionController.createRequestPermissionResultContract()
) { granted ->
if (granted.containsAll(repository.permissions))
onPermissionGranted()
}

override val context: Context
get() = this

override val inputForTasker: TaskerInput<HealthDataInput>
get() = TaskerInput(
HealthDataInput(recordType = getInputRecordType(), fromTimeMillis = getInputFromTime())
)

override fun assignFromInput(input: TaskerInput<HealthDataInput>) {
binding.recordTypeText.editText?.setText(input.regular.recordType)
binding.fromTimeText.editText?.setText(input.regular.fromTimeMillis)
}

override fun onCreate(savedInstanceState: Bundle?) {
Log.d(TAG, "onCreate")
WindowCompat.setDecorFitsSystemWindows(window, false)
super.onCreate(savedInstanceState)

binding = ActivityHealthDataBinding.inflate(layoutInflater)
setContentView(binding.root)
setDebugButton()
taskerHelper.onCreate()
}

override fun onResume() {
super.onResume()
setButtonState()
}

private fun setButtonState() {
lifecycleScope.launch {
when {
!repository.isAvailable() -> {
binding.button.text = getString(R.string.install)
binding.button.setOnClickListener { repository.installHealthConnect() }
}

!repository.hasPermissions() -> {
binding.button.text = getString(R.string.grant_permissions)
binding.button.setOnClickListener {
permissionsLauncher.launch(repository.permissions)
}
}

else -> onPermissionGranted()
}
}
}

private fun onPermissionGranted() {
Log.d(TAG, "onPermissionGranted")
binding.button.text = getString(R.string.done)
binding.button.setOnClickListener {
hideKeyboard()
taskerHelper.finishForTasker()
}
}

private fun getInputRecordType(): String {
return binding.recordTypeText.editText?.text.toString()
}

private fun getInputFromTime(): String {
return binding.fromTimeText.editText?.text.toString()
}

private fun hideKeyboard() {
(getSystemService(INPUT_METHOD_SERVICE) as? InputMethodManager)?.hideSoftInputFromWindow(
binding.root.windowToken,
0
)
}

private fun setDebugButton() {
binding.debugButton.isVisible = BuildConfig.DEBUG
binding.debugButton.setOnClickListener {
lifecycleScope.launch {
val recordType = getInputRecordType()
val startTime =
runCatching { Instant.ofEpochMilli(getInputFromTime().toLong()) }.getOrElse {
Instant.now().minusSeconds(60 * 60)
}
val endTime = Instant.now()
runCatching {
val output = repository.getData(recordType, startTime, endTime)
Log.d(TAG, output.toString(2))
}.onFailure { err ->
Log.e(TAG, "Repository error:", err)
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.rafapps.taskerhealthconnect.healthdata

import android.annotation.SuppressLint
import com.joaomgcd.taskerpluginlibrary.input.TaskerInputField
import com.joaomgcd.taskerpluginlibrary.input.TaskerInputRoot
import com.rafapps.taskerhealthconnect.R
import java.time.Instant

@SuppressLint("NonConstantResourceId") // TODO: check with nonFinalResIds
@TaskerInputRoot
class HealthDataInput @JvmOverloads constructor(
@field:TaskerInputField(
key = "recordType",
labelResId = R.string.record_type,
descriptionResId = R.string.record_type_description
) var recordType: String = "StepsRecord",
@field:TaskerInputField(
key = "fromTimeMillis",
labelResId = R.string.from_time_milliseconds,
descriptionResId = R.string.from_time_milliseconds_description
) var fromTimeMillis: String = Instant.now()
.minusSeconds(60 * 60)
.toEpochMilli()
.toString()
) {
override fun toString(): String {
return "recordType: $recordType, fromTimeMillis: $fromTimeMillis"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.rafapps.taskerhealthconnect.healthdata

import android.annotation.SuppressLint
import com.joaomgcd.taskerpluginlibrary.output.TaskerOutputObject
import com.joaomgcd.taskerpluginlibrary.output.TaskerOutputVariable
import com.rafapps.taskerhealthconnect.R

@SuppressLint("NonConstantResourceId") // TODO: check with nonFinalResIds
@TaskerOutputObject
class HealthDataOutput(
@get:TaskerOutputVariable(
name = "healthData",
labelResId = R.string.health_data,
htmlLabelResId = R.string.health_data_description
) val healthData: String = "[]"
) {
override fun toString(): String {
return "healthData: $healthData"
}
}
90 changes: 90 additions & 0 deletions app/src/main/res/layout/activity_health_data.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/aggregatedHealthDataLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true"
android:fitsSystemWindows="true"
tools:context=".aggregated.AggregatedHealthDataActivity">

<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="?attr/actionBarSize">

<com.google.android.material.textview.MaterialTextView
android:id="@+id/recordTypeDescription"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="@dimen/margin"
android:text="@string/record_type_description"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />

<com.google.android.material.textfield.TextInputLayout
android:id="@+id/recordTypeText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="@dimen/margin"
android:hint="@string/record_type"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/recordTypeDescription">

<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</com.google.android.material.textfield.TextInputLayout>

<com.google.android.material.textview.MaterialTextView
android:id="@+id/fromTimeDescription"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="@dimen/margin"
android:text="@string/from_time_milliseconds_description"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/recordTypeText" />

<com.google.android.material.textfield.TextInputLayout
android:id="@+id/fromTimeText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="@dimen/margin"
android:hint="@string/from_time_milliseconds"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/fromTimeDescription">

<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</com.google.android.material.textfield.TextInputLayout>

<Button
android:id="@+id/debugButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="@dimen/margin"
android:text="@string/debug"
android:visibility="gone"
app:layout_constraintBottom_toTopOf="@id/button"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />

<Button
android:id="@+id/button"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="@dimen/margin"
android:visibility="visible"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
tools:text="@string/done" />

</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>
Loading

0 comments on commit aa04921

Please sign in to comment.