fit-samples icon indicating copy to clipboard operation
fit-samples copied to clipboard

Does Google Fit SDK work on WearOS?

Open ZhEgor opened this issue 2 years ago • 13 comments

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!

ZhEgor avatar Dec 09 '22 13:12 ZhEgor

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

Deepika1498 avatar Apr 19 '23 09:04 Deepika1498

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

ZhEgor avatar Apr 19 '23 10:04 ZhEgor

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 avatar Apr 20 '23 03:04 Deepika1498

@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 avatar Apr 20 '23 07:04 ZhEgor

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

ZhEgor avatar Apr 20 '23 07:04 ZhEgor

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

Deepika1498 avatar Apr 24 '23 03:04 Deepika1498

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

ZhEgor avatar Apr 24 '23 07:04 ZhEgor

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

Deepika1498 avatar Apr 24 '23 11:04 Deepika1498

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.

ZhEgor avatar Apr 25 '23 05:04 ZhEgor

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!

phamtrungkt avatar May 10 '23 08:05 phamtrungkt

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<SensorManager>()!! 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?

Deepika1498 avatar May 26 '23 05:05 Deepika1498

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

ZhEgor avatar May 26 '23 06:05 ZhEgor

thank you very much @ZhEgor it works now

Deepika1498 avatar May 29 '23 06:05 Deepika1498