Skip to content
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

Does Google Fit SDK work on WearOS? #70

Open
ZhEgor opened this issue Dec 9, 2022 · 13 comments
Open

Does Google Fit SDK work on WearOS? #70

ZhEgor opened this issue Dec 9, 2022 · 13 comments

Comments

@ZhEgor
Copy link

ZhEgor commented Dec 9, 2022

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}

Devices: Fossil Gen 6 (real device API 30, API 28), WearOS Small Round (Android Studio Emulator API 30)

I tried to run the same code on a phone app and I didin't receive an error. So I wonder, maybe didn't Google Fit API suppose to work on watches at all? If so, what workarounds can I use? Should I use Google Fit REST API?
Maybe someone has already encountered this problem, it would be useful to hear how they managed to deal with this.
Thanks in advance!

@Deepika1498
Copy link

Hi @ZhEgor have you been able to achieve a communication between phone app and watch to read heart rate data?

@ZhEgor
Copy link
Author

ZhEgor commented Apr 19, 2023

@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().

@Deepika1498
Copy link

Deepika1498 commented Apr 20, 2023

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.

@ZhEgor
Copy link
Author

ZhEgor commented Apr 20, 2023

@Deepika1498

GoogleFitPermissionsManager.kt

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,
        )
    }
}

GoogleFitRepository.kt


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"))
                    }
                }
            }
        }
    }
}

AndroidManifest.xml

    <!-- 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" />

@ZhEgor
Copy link
Author

ZhEgor commented Apr 20, 2023

you can also use HealthServices to collect heart rate or steps.

@Deepika1498
Copy link

Are these the complete set of files required for the project?

@ZhEgor
Copy link
Author

ZhEgor commented Apr 24, 2023

For the setting up Google Fit - yes, if we don't mention dependency for Google Fit.

@Deepika1498
Copy link

Do you have a repo or something which has entire code that's necessary?

@ZhEgor
Copy link
Author

ZhEgor commented Apr 25, 2023

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.

@phamtrungkt
Copy link

phamtrungkt commented May 10, 2023 via email

@Deepika1498
Copy link

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 sensor = sensorManager.getDefaultSensor(Sensor.TYPE_HEART_RATE)
var heartRateValue: Float? = null // Use a nullable Float to store the value

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?

@ZhEgor
Copy link
Author

ZhEgor commented May 26, 2023

@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) {}
}

@Deepika1498
Copy link

thank you very much @ZhEgor it works now

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants