Skip to content
Closed
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
4 changes: 2 additions & 2 deletions packages/health/android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -66,11 +66,11 @@ 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-fitness:21.3.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")
implementation("androidx.health.connect:connect-client:1.1.0-alpha11")
def fragment_version = "1.6.2"
implementation "androidx.fragment:fragment-ktx:$fragment_version"

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package cachet.plugins.health

import android.Manifest
import android.annotation.SuppressLint
import android.app.Activity
import android.app.ActivityManager
import android.app.Service
Expand All @@ -17,11 +18,13 @@ import androidx.activity.ComponentActivity
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.NonNull
import androidx.annotation.RequiresApi
import androidx.core.content.ContextCompat
import androidx.health.connect.client.HealthConnectClient
import androidx.health.connect.client.HealthConnectFeatures
import androidx.health.connect.client.PermissionController
import androidx.health.connect.client.feature.ExperimentalFeatureAvailabilityApi
import androidx.health.connect.client.permission.HealthPermission
import androidx.health.connect.client.permission.HealthPermission.Companion.PERMISSION_READ_HEALTH_DATA_IN_BACKGROUND
import androidx.health.connect.client.records.*
import androidx.health.connect.client.records.MealType.MEAL_TYPE_BREAKFAST
import androidx.health.connect.client.records.MealType.MEAL_TYPE_DINNER
Expand All @@ -35,18 +38,24 @@ 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.common.ConnectionResult
import com.google.android.gms.common.GoogleApiAvailability
import com.google.android.gms.fitness.Fitness
import com.google.android.gms.fitness.FitnessActivities
import com.google.android.gms.fitness.FitnessLocal
import com.google.android.gms.fitness.FitnessOptions
import com.google.android.gms.fitness.LocalRecordingClient
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.LocalDataReadRequest
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 com.google.android.gms.tasks.Tasks
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 @@ -55,7 +64,6 @@ 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.*
import java.time.temporal.ChronoUnit
import java.util.*
Expand All @@ -78,6 +86,7 @@ class HealthPlugin(private var channel: MethodChannel? = null) :
private var context: Context? = null
private var threadPoolExecutor: ExecutorService? = null
private var useHealthConnectIfAvailable: Boolean = false
private var stepSensorActive: Boolean = false
private var healthConnectRequestPermissionsLauncher: ActivityResultLauncher<Set<String>>? =
null
private var activityRecognitionPermissionLauncher: ActivityResultLauncher<Array<String>>? =
Expand Down Expand Up @@ -517,15 +526,6 @@ class HealthPlugin(private var channel: MethodChannel? = null) :
// 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)
}

const val PERMISSIONS_REQUEST_ACTIVITY_RECOGNITION = 10
}

Expand Down Expand Up @@ -1314,6 +1314,20 @@ class HealthPlugin(private var channel: MethodChannel? = null) :
/** Get all datapoints of the DataType within the given time range */
private fun getData(call: MethodCall, result: Result) {

if (stepSensorActive) {
CoroutineScope(Dispatchers.IO).launch {
try {
fetchCacheData(10)
} catch (e: Exception) {
// ignore
}
withContext(Dispatchers.Main) {
getSensorData(call, result)
}
}
return
}

if (StepCounterService.initiated()) {
getSensorData(call, result)
return
Expand Down Expand Up @@ -1476,15 +1490,19 @@ class HealthPlugin(private var channel: MethodChannel? = null) :

val healthConnectData = mutableListOf<Map<String, Any?>>()

if(dataType == STEPS && StepCounterService.initiated()) {
val items = StepCounterService.box.boxFor(SensorStep::class.java).query(SensorStep_.startTime.between(startTime.toEpochMilli(),endTime.toEpochMilli())).build().find()
if (dataType == STEPS) {
val items = StepCounterService.box.boxFor(SensorStep::class.java).query(
SensorStep_.startTime.between(
startTime.toEpochMilli(),
endTime.toEpochMilli()
)
).build().find()
healthConnectData.addAll(items.map {
mapOf<String, Any>(
"value" to
it.count,
"date_from" to
it.startTime!!
,
it.startTime!!,
"date_to" to
it.endTime!!,
"source_id" to "",
Expand All @@ -1502,7 +1520,9 @@ class HealthPlugin(private var channel: MethodChannel? = null) :
val startTime = Instant.ofEpochMilli(call.argument<Long>("startTime")!!)
val endTime = Instant.ofEpochMilli(call.argument<Long>("endTime")!!)

val items = StepCounterService.box.boxFor(SensorStep::class.java).query(SensorStep_.startTime.between(startTime.toEpochMilli(),endTime.toEpochMilli())).build().find()
val items = StepCounterService.box.boxFor(SensorStep::class.java)
.query(SensorStep_.startTime.between(startTime.toEpochMilli(), endTime.toEpochMilli()))
.build().find()
var totalResult = 0.0
items.forEach {
totalResult += it.count;
Expand Down Expand Up @@ -2297,10 +2317,87 @@ class HealthPlugin(private var channel: MethodChannel? = null) :
}
}

private fun lastSyncTime(): ZonedDateTime {
return Instant.ofEpochSecond(
activity!!.getSharedPreferences("my_prefs", Context.MODE_PRIVATE)
.getLong(
"last_q_epoch",
LocalDateTime.now().atZone(ZoneId.systemDefault()).minusHours(1).toEpochSecond()
)
).atZone(ZoneId.systemDefault())
}

private fun updateLastSyncTime(atZone: ZonedDateTime) {
activity!!.getSharedPreferences("my_prefs", Context.MODE_PRIVATE)
.edit()
.putLong("last_q_epoch", atZone.toEpochSecond())
.apply()
}

private suspend fun fetchCacheData(duration: Int = 10) {

val queryEndTime = LocalDateTime.now().atZone(ZoneId.systemDefault())
val queryStartTime = lastSyncTime()
val readRequest =
LocalDataReadRequest.Builder()
.aggregate(LocalDataType.TYPE_STEP_COUNT_DELTA)
.bucketByTime(duration, TimeUnit.SECONDS)
.setTimeRange(
queryStartTime.toEpochSecond(),
queryEndTime.toEpochSecond(),
TimeUnit.SECONDS
)
.build()
val localRecordingClient = FitnessLocal.getLocalRecordingClient(activity!!)
val result = Tasks.await(localRecordingClient.readData(readRequest))
val data = result.buckets.flatMap { bucket -> bucket.dataSets.flatMap { it.dataPoints } }
val sensorStepData = data.map { each ->
SensorStep().apply {
startTime = each.getStartTime(TimeUnit.MILLISECONDS)
endTime = each.getEndTime(TimeUnit.MILLISECONDS)
count = each.getValue(LocalField.FIELD_STEPS).asInt().toDouble()
}
}
if (sensorStepData.isNotEmpty()) {
updateLastSyncTime(LocalDateTime.now().atZone(ZoneId.systemDefault()))
Log.d(
"HealthPlugin",
"Sensor step data received: ${sensorStepData.size} records. Updating query and storing data."
)

StepCounterService.box.boxFor(SensorStep::class.java).put(sensorStepData)
} else {
Log.d(
"HealthPlugin",
"No sensor step data found. Extending query duration."
)
}
}

private fun getTotalStepsInInterval(call: MethodCall, result: Result) {
val start = call.argument<Long>("startTime")!!
val end = call.argument<Long>("endTime")!!

if (stepSensorActive) {

if (!StepCounterService.initiated()) {
StepCounterService.box = MyObjectBox.builder()
.androidContext(context)
.build();
}

CoroutineScope(Dispatchers.IO).launch {
try {
fetchCacheData(10)
} catch (e: Exception) {
// ignore
}
withContext(Dispatchers.Main) {
getSensorDataCount(call, result)
}
}
return
}

if (StepCounterService.initiated()) {
getSensorDataCount(call, result)
Expand Down Expand Up @@ -2465,6 +2562,7 @@ class HealthPlugin(private var channel: MethodChannel? = null) :
"hasPermissions" -> hasPermissions(call, result)
"requestAuthorization" -> requestAuthorization(call, result)
"revokePermissions" -> revokePermissions(call, result)
"requestBackgroundFetchIfAvailable" -> requestBackgroundFetchIfAvailable(call, result)
"getData" -> getData(call, result)
"getIntervalData" -> getIntervalData(call, result)
"writeData" -> writeData(call, result)
Expand All @@ -2485,6 +2583,25 @@ class HealthPlugin(private var channel: MethodChannel? = null) :
}
}

@OptIn(ExperimentalFeatureAvailabilityApi::class)
private fun requestBackgroundFetchIfAvailable(call: MethodCall, result: Result) {
if (healthConnectClient
.features
.getFeatureStatus(
HealthConnectFeatures.FEATURE_READ_HEALTH_DATA_IN_BACKGROUND
) == HealthConnectFeatures.FEATURE_STATUS_AVAILABLE
) {
mResult = result
healthConnectRequestPermissionsLauncher!!.launch(
setOf(PERMISSION_READ_HEALTH_DATA_IN_BACKGROUND)
)
return
}

result.success(false)
}


private fun isForegroundServiceRunning(context: Context, serviceClass: Class<*>): Boolean {
val activityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
for (service in activityManager.getRunningServices(Int.MAX_VALUE)) {
Expand All @@ -2496,41 +2613,76 @@ class HealthPlugin(private var channel: MethodChannel? = null) :
}

private fun isStepSensorRunning(call: MethodCall, result: Result) {
result.success(isForegroundServiceRunning(context!!,StepCounterService::class.java))
result.success(
isForegroundServiceRunning(
context!!,
StepCounterService::class.java
) || stepSensorActive
)
}

private fun clearStepSensorData(call: MethodCall, result: Result) {
StepCounterService.box.boxFor(SensorStep::class.java).removeAll()
result.success(true)
}

private fun stopStepSensorBackgroundService(call: MethodCall, result: Result) {
val serviceIntent = Intent(activity!!, StepCounterService::class.java)
activity!!.stopService(serviceIntent)
}

private fun startStepSensorBackgroundService(call: MethodCall, result: Result) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
if (ContextCompat.checkSelfPermission(
activity!!,
Manifest.permission.ACTIVITY_RECOGNITION
)
!= PackageManager.PERMISSION_GRANTED
) {
mResult = result
activityRecognitionPermissionLauncher!!.launch(arrayOf(Manifest.permission.ACTIVITY_RECOGNITION))
try {
if (activity == null || context == null) {
result.success(false)
return
}
val serviceIntent = Intent(activity!!, StepCounterService::class.java)
val stopped = activity!!.stopService(serviceIntent)

result.success(stopped)
} catch (e: Exception) {
Log.e("FLUTTER_HEALTH::ERROR", "Failed to stop step sensor service: ${e.message}")
Log.e("FLUTTER_HEALTH::ERROR", e.stackTrace.toString())
result.success(false)
}
}

startStepSenor()
result.success(true)
private fun startStepSensorBackgroundService(call: MethodCall, result: Result) {
if (ContextCompat.checkSelfPermission(
activity!!,
Manifest.permission.ACTIVITY_RECOGNITION
)
!= PackageManager.PERMISSION_GRANTED
) {
mResult = result
activityRecognitionPermissionLauncher!!.launch(arrayOf(Manifest.permission.ACTIVITY_RECOGNITION))
return
} else {
startStepSenor()
result.success(true)
}
}

@SuppressLint("MissingPermission")
private fun startStepSenor() {
try {
val serviceIntent = Intent(activity!!, StepCounterService::class.java)
ContextCompat.startForegroundService(context!!, serviceIntent)
} catch (e:Exception) {
val hasMinPlayServices = GoogleApiAvailability.getInstance()
.isGooglePlayServicesAvailable(
context!!,
LocalRecordingClient.LOCAL_RECORDING_CLIENT_MIN_VERSION_CODE
)
if (hasMinPlayServices == ConnectionResult.SUCCESS) {
val localRecordingClient = FitnessLocal.getLocalRecordingClient(activity!!)
localRecordingClient.subscribe(LocalDataType.TYPE_STEP_COUNT_DELTA)
.addOnSuccessListener {
stepSensorActive = true
Log.d("HealthPlugin", "FitnessLocal subscribed");
}
.addOnFailureListener { e ->
Log.e("HealthPlugin", "FitnessLocal subscription failed: $e");
}

} else {
val serviceIntent = Intent(activity!!, StepCounterService::class.java)
ContextCompat.startForegroundService(context!!, serviceIntent)
}
} catch (e: Exception) {
Log.d("HealthPlugin", e.toString());
}
}
Expand Down Expand Up @@ -2559,7 +2711,7 @@ class HealthPlugin(private var channel: MethodChannel? = null) :
(activity as ComponentActivity).registerForActivityResult(
ActivityResultContracts.RequestMultiplePermissions()
) { granted ->
if(granted.isNotEmpty() && granted.values.first()) {
if (granted.isNotEmpty() && granted.values.first()) {
startStepSenor()
mResult?.success(true)
return@registerForActivityResult
Expand Down Expand Up @@ -2787,7 +2939,7 @@ class HealthPlugin(private var channel: MethodChannel? = null) :
val records = mutableListOf<Record>()

val granted = healthConnectClient.permissionController.getGrantedPermissions()
if(!granted.contains(HealthPermission.getReadPermission(classType))) {
if (!granted.contains(HealthPermission.getReadPermission(classType))) {
Log.d("Health Plugin", "No Permission granted for $dataType")
return@let
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ import android.os.SystemClock
import android.util.Log
import androidx.core.app.NotificationCompat
import androidx.core.app.ServiceCompat
import com.google.android.gms.common.ConnectionResult
import com.google.android.gms.common.GoogleApiAvailability
import com.google.android.gms.fitness.FitnessLocal
import com.google.android.gms.fitness.LocalRecordingClient
import com.google.android.gms.fitness.data.LocalDataType
import io.objectbox.BoxStore


Expand Down
Loading
Loading