Open ZhEgor opened 1 year ago
Hi @ZhEgor have you been able to achieve a communication between phone app and watch to read heart rate data?
@Deepika1498 Yes, I was able to get the heart rate data from Google fit on a watch in the end, but I don't know what fixed it. Maybe I verified the google console account or I added google-service.json from firebase to the project, or I added DataReadRequest.BuilderenableServerQueries().
Do you happen to have the code to that project? It'll be very useful if you could pls share . I am doing this as a part of college project. I've been trying to achieve this for the past two months, haven't been able to. I'd really appreciate if you could please help.
@Deepika1498
import androidx.core.app.ComponentActivity
import com.google.android.gms.auth.api.signin.GoogleSignIn
import com.google.android.gms.auth.api.signin.GoogleSignInAccount
import com.google.android.gms.fitness.FitnessOptions
import com.google.android.gms.fitness.data.DataType
interface GoogleFitPermissionsManager {
fun requestSleepPermission()
}
internal fun requestFitnessPermissions(
activity: ComponentActivity,
account: GoogleSignInAccount,
fitnessOptions: FitnessOptions
) {
val fitnessPermissionRequestCode = 1011
GoogleSignIn.requestPermissions(
activity,
fitnessPermissionRequestCode,
account,
fitnessOptions
)
}
fun requestSleepPermission(activity: ComponentActivity) {
val fitnessOptions = FitnessOptions.builder()
.accessSleepSessions(FitnessOptions.ACCESS_READ)
.addDataType(DataType.AGGREGATE_STEP_COUNT_DELTA, FitnessOptions.ACCESS_READ)
.addDataType(DataType.TYPE_DISTANCE_DELTA, FitnessOptions.ACCESS_READ)
.addDataType(DataType.TYPE_STEP_COUNT_DELTA, FitnessOptions.ACCESS_READ)
.addDataType(DataType.TYPE_HEART_RATE_BPM, FitnessOptions.ACCESS_READ)
.addDataType(DataType.AGGREGATE_HEART_RATE_SUMMARY, FitnessOptions.ACCESS_READ)
.build()
val account = GoogleSignIn.getAccountForExtension(activity, fitnessOptions)
if (!GoogleSignIn.hasPermissions(account, fitnessOptions)) {
requestFitnessPermissions(
activity = activity,
account = account,
fitnessOptions = fitnessOptions,
)
}
}
import android.content.Context
import android.util.Log
import com.google.android.gms.auth.api.signin.GoogleSignIn
import com.google.android.gms.fitness.Fitness
import com.google.android.gms.fitness.FitnessOptions
import com.google.android.gms.fitness.data.DataType
import com.google.android.gms.fitness.data.Field
import com.google.android.gms.fitness.request.DataReadRequest
import com.google.android.gms.fitness.request.SessionReadRequest
import kotlinx.coroutines.suspendCancellableCoroutine
import java.util.concurrent.TimeUnit
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
interface GoogleFitRepository {
suspend fun getSleepSegment(): Result<Int>
suspend fun getStepsData(): Result<Int>
suspend fun getHeartRateData(): Result<Int>
}
class GoogleFitRepositoryImpl(
private val activity: Context
) : GoogleFitRepository {
private val SLEEP_STAGE_NAMES = arrayOf(
"Unused",
"Awake (during sleep)",
"Sleep",
"Out-of-bed",
"Light sleep",
"Deep sleep",
"REM sleep"
)
override suspend fun getStepsData(): Result<Int> {
return runCatching {
val now = System.currentTimeMillis()
val yesterday = now - 6 * 24 * 60 * 60 * 1000
val fitnessOptions = FitnessOptions.builder()
.addDataType(DataType.TYPE_STEP_COUNT_DELTA, FitnessOptions.ACCESS_READ)
.build()
val googleSignInAccount = GoogleSignIn.getAccountForExtension(activity, fitnessOptions)
if (!GoogleSignIn.hasPermissions(googleSignInAccount, fitnessOptions)) {
throw Exception("no permission")
}
val request = DataReadRequest.Builder()
.aggregate(
DataType.TYPE_STEP_COUNT_DELTA,
DataType.AGGREGATE_STEP_COUNT_DELTA
) // .read(DataType.TYPE_STEP_COUNT_DELTA)
.bucketByTime(8, TimeUnit.DAYS)
.enableServerQueries()
.setTimeRange(yesterday, now, TimeUnit.MILLISECONDS)
.build()
// val request = DataReadRequest.Builder()
// .aggregate(DataType.TYPE_HEART_RATE_BPM)
// .aggregate(DataType.AGGREGATE_HEART_RATE_SUMMARY)
// .setTimeRange(yesterday, now, TimeUnit.MILLISECONDS)
// .bucketByTime(1, TimeUnit.HOURS)
// .build()
val response = Fitness.getHistoryClient(activity, googleSignInAccount).readData(request)
suspendCancellableCoroutine { continuation ->
response.addOnCompleteListener {
if (it.isSuccessful) {
val stepsDataSet = HashMap<String, Int>()
for (bucket in it.result.buckets) {
val totalSteps = bucket.dataSets
.flatMap { it.dataPoints }
.sumBy { it.getValue(Field.FIELD_STEPS).asInt() }
println("test___total steps $totalSteps")
}
val summary = it.result.getDataSet(DataType.TYPE_STEP_COUNT_DELTA)
println("test___ $summary")
continuation.resume(120)
} else {
it.exception?.printStackTrace()
continuation.resumeWithException(
it.exception
?: RuntimeException("Unknown exception requesting heart rate data")
)
}
}
}
}
}
override suspend fun getSleepSegment(): Result<Int> {
return runCatching {
val now = System.currentTimeMillis()
val yesterday = now - 30L * 24 * 60 * 60 * 1000
val fitnessOptions = FitnessOptions.builder()
.addDataType(DataType.TYPE_SLEEP_SEGMENT, FitnessOptions.ACCESS_READ)
.build()
val googleSignInAccount = GoogleSignIn.getAccountForExtension(activity, fitnessOptions)
if (!GoogleSignIn.hasPermissions(googleSignInAccount, fitnessOptions)) {
throw Exception("no permission")
}
val request = SessionReadRequest.Builder()
.readSessionsFromAllApps()
.includeSleepSessions()
.read(DataType.TYPE_SLEEP_SEGMENT)
.setTimeInterval(yesterday, now, TimeUnit.MILLISECONDS)
.enableServerQueries()
.build()
val response = Fitness.getSessionsClient(activity, googleSignInAccount).readSession(request)
suspendCancellableCoroutine { continuation ->
response.addOnCompleteListener { result ->
if (result.isSuccessful) {
println("test___ sleep session ${result.result.sessions}")
for (session in result.result.sessions) {
val sessionStart = session.getStartTime(TimeUnit.MILLISECONDS)
val sessionEnd = session.getEndTime(TimeUnit.MILLISECONDS)
Log.d("TAG!", "Sleep between $sessionStart and $sessionEnd")
// If the sleep session has finer granularity sub-components, extract them:
val dataSets = result.result.getDataSet(session)
for (dataSet in dataSets) {
for (point in dataSet.dataPoints) {
val sleepStageVal = point.getValue(Field.FIELD_SLEEP_SEGMENT_TYPE).asInt()
val sleepStage = SLEEP_STAGE_NAMES[sleepStageVal]
val segmentStart = point.getStartTime(TimeUnit.MILLISECONDS)
val segmentEnd = point.getEndTime(TimeUnit.MILLISECONDS)
Log.d("TAG!", "\t* Type $sleepStage between $segmentStart and $segmentEnd")
}
}
}
continuation.resume(120)
} else {
result.exception?.printStackTrace()
continuation.resumeWithException(result.exception ?: RuntimeException("Unknown exception requesting heart rate data"))
}
}
}
}
}
override suspend fun getHeartRateData(): Result<Int> {
return runCatching {
val now = System.currentTimeMillis()
val yesterday = now - 30L * 24 * 60 * 60 * 1000
val fitnessOptions = FitnessOptions.builder()
.addDataType(DataType.TYPE_HEART_RATE_BPM, FitnessOptions.ACCESS_READ)
.addDataType(DataType.AGGREGATE_HEART_RATE_SUMMARY, FitnessOptions.ACCESS_READ)
.build()
val googleSignInAccount = GoogleSignIn.getAccountForExtension(activity, fitnessOptions)
if (!GoogleSignIn.hasPermissions(googleSignInAccount, fitnessOptions)) {
throw Exception("no permission")
}
val request = DataReadRequest.Builder()
.aggregate(DataType.TYPE_HEART_RATE_BPM, DataType.AGGREGATE_HEART_RATE_SUMMARY)
.enableServerQueries()
.setTimeRange(yesterday, now, TimeUnit.MILLISECONDS)
.bucketByTime(8, TimeUnit.DAYS)
.build()
val response = Fitness.getHistoryClient(activity, googleSignInAccount).readData(request)
suspendCancellableCoroutine { continuation ->
response.addOnCompleteListener {
if (it.isSuccessful) {
val summary = it.result.getDataSet(DataType.AGGREGATE_HEART_RATE_SUMMARY)
println("test___ heart rate summary $summary")
for ( bucket in it.result.buckets){
for (dataSet in bucket.dataSets){
when (dataSet.dataType){
DataType.AGGREGATE_HEART_RATE_SUMMARY ->{
for (dataPoint in dataSet.dataPoints){
println("test___ heart rate summary ${dataPoint.getValue(Field.FIELD_AVERAGE).asFloat()}")
}
}
}
}
}
continuation.resume(120)
} else {
it.exception?.printStackTrace()
continuation.resumeWithException(it.exception ?: RuntimeException("Unknown exception requesting heart rate data"))
}
}
}
}
}
}
<!-- For receiving heart rate data. -->
<uses-permission android:name="android.permission.BODY_SENSORS" />
<!-- For receiving steps data. -->
<uses-permission android:name="android.permission.ACTIVITY_RECOGNITION" />
you can also use HealthServices to collect heart rate or steps.
Are these the complete set of files required for the project?
For the setting up Google Fit - yes, if we don't mention dependency for Google Fit.
Do you have a repo or something which has entire code that's necessary?
That's full code, I extracted this code from a module, which is fully dedicated to Google Fit, the module contains only two files. Alas I am not allowed to share the repo.
Hi
On Wed, 19 Apr 2023 at 16:06 Deepika1498 @.***> wrote:
Hi @ZhEgor https://github.com/ZhEgor have you been able to achieve a communication between phone app and watch to read heart rate data?
— Reply to this email directly, view it on GitHub https://github.com/android/fit-samples/issues/70#issuecomment-1514387957, or unsubscribe https://github.com/notifications/unsubscribe-auth/AWZELFYYPYKCF5WBOJBJ5UDXB6THXANCNFSM6AAAAAASZLGN6Y . You are receiving this because you are subscribed to this thread.Message ID: @.***>
-- Mr Trung Pham ,Architect Work : +84 985 515 288 +1 317-344 9869 (sms) Private : +84 909 044 888 Home : +84-28-3 636 8848 https://linktr.ee/trungphamfile Please let text message, i will call back soon!
This is the code I have written to get heart rate data from the sensor on watch:
@RequiresPermission(Manifest.permission.BODY_SENSORS)
fun Context.sensorSummary(): String = runBlocking {
val sensorManager = getSystemService
val sensorEventListener = object : SensorEventListener {
override fun onSensorChanged(event: SensorEvent) {
if (event.sensor.type == Sensor.TYPE_HEART_RATE) {
heartRateValue = event.values[0] // Assign the value to the shared variable
}
}
override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {
// Handle accuracy changes if needed
}
}
val job = GlobalScope.launch(Dispatchers.IO) {
sensorManager.registerListener(sensorEventListener, sensor, SensorManager.SENSOR_DELAY_NORMAL)
}
// Wait for the onSensorChanged callback to finish
job.join()
// Unregister the listener after the join to ensure it has finished processing
sensorManager.unregisterListener(sensorEventListener)
return@runBlocking heartRateValue?.toString() ?: "No heart rate data"
} But it always return no heart rate data. Where have I made a mistake?
@Deepika1498 it seems you unregister the listener before it even manages to collect any data. And right after this function returns empty value. Try this code:
@RequiresPermission(Manifest.permission.BODY_SENSORS)
private suspend fun Context.getInstantHeartRate(): Int? = suspendCoroutine { continuation ->
val sensorManager = getSystemService(Context.SENSOR_SERVICE) as SensorManager
val sensorListener = HeartRateEventListener { heartRate ->
sensorManager.unregisterListener(this@HeartRateEventListener)
continuation.resumeWith(Result.success(heartRate))
}
val sensorHeartRate: Sensor = sensorManager.getDefaultSensor(Sensor.TYPE_HEART_RATE)
val isSuccessiveHeartRate = sensorManager.registerListener(
sensorListener,
sensorHeartRate,
SensorManager.SENSOR_DELAY_NORMAL
)
if (!isSuccessiveHeartRate) {
Log.d("SENSOR_TAG", "failed to register a listener")
sensorManager.unregisterListener(sensorListener)
continuation.resumeWith(Result.success(null))
}
}
class HeartRateEventListener(
private val onHeartRateReceived: SensorEventListener.(Int) -> Unit
) : SensorEventListener {
override fun onSensorChanged(event: SensorEvent?) {
if (event?.sensor?.type == Sensor.TYPE_HEART_RATE) {
val heartRate = event.values.getOrNull(0)?.toInt()
if (heartRate != null && heartRate != 0) {
onHeartRateReceived.invoke(this, heartRate)
}
}
}
override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {}
}
thank you very much @ZhEgor it works now
Hi guys, I have two apps one for wearable and one for handheld device and we have mutual source of fit data - Google Fit. So when I request data on watches, I catch this exception on
57th line
:17: API: Fitness.CLIENT is not available on this device. Connection failed with: ConnectionResult{statusCode=INVALID_ACCOUNT, resolution=null, message=null}