android / fit-samples

Multiple samples showing the best practices using the Fit APIs on Android.
Apache License 2.0
152 stars 112 forks source link

Does Google Fit SDK work on WearOS? #70

Open ZhEgor opened 1 year ago

ZhEgor commented 1 year ago

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 commented 1 year ago

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

ZhEgor commented 1 year ago

@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 commented 1 year ago

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 commented 1 year ago

@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 commented 1 year ago

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

Deepika1498 commented 1 year ago

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

ZhEgor commented 1 year ago

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

Deepika1498 commented 1 year ago

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

ZhEgor commented 1 year ago

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 commented 1 year ago

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!

Deepika1498 commented 1 year ago

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 commented 1 year ago

@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 commented 1 year ago

thank you very much @ZhEgor it works now