From 343a5a3c826b7130f6ce78697f046ea461358d3f Mon Sep 17 00:00:00 2001 From: Michael Stillwell Date: Tue, 23 Jan 2024 17:30:11 +0000 Subject: [PATCH 1/2] Integrate FLP as a Location source --- .../ExerciseSampleCompose/app/build.gradle | 8 +- .../app/src/main/AndroidManifest.xml | 1 + .../data/ExerciseClientManager.kt | 177 +++++++++++++++++- .../exercisesamplecompose/di/MainModule.kt | 10 + .../gradle/libs.versions.toml | 2 +- 5 files changed, 191 insertions(+), 7 deletions(-) diff --git a/health-services/ExerciseSampleCompose/app/build.gradle b/health-services/ExerciseSampleCompose/app/build.gradle index c19a4832..136c86a0 100644 --- a/health-services/ExerciseSampleCompose/app/build.gradle +++ b/health-services/ExerciseSampleCompose/app/build.gradle @@ -87,7 +87,7 @@ dependencies { implementation libs.guava implementation libs.androidx.concurrent - //Wear OS Compose Navigation + // Wear OS Compose Navigation implementation libs.compose.wear.navigation implementation libs.androidx.compose.navigation implementation libs.horologist.compose.layout @@ -95,7 +95,7 @@ dependencies { implementation libs.horologist.health.composables implementation libs.horologist.health.service - //Wear Health Services + // Wear Health Services implementation libs.androidx.health.services // Lifecycle components @@ -110,12 +110,14 @@ dependencies { // Ongoing Activity implementation libs.wear.ongoing.activity + // Fused Location Provider + implementation libs.play.services.location + // Hilt implementation libs.hilt.navigation.compose implementation libs.dagger.hilt.android kapt libs.dagger.hilt.android.compiler - // Testing testImplementation libs.junit androidTestImplementation libs.test.ext.junit diff --git a/health-services/ExerciseSampleCompose/app/src/main/AndroidManifest.xml b/health-services/ExerciseSampleCompose/app/src/main/AndroidManifest.xml index 902f3aad..7d8f7d43 100644 --- a/health-services/ExerciseSampleCompose/app/src/main/AndroidManifest.xml +++ b/health-services/ExerciseSampleCompose/app/src/main/AndroidManifest.xml @@ -26,6 +26,7 @@ + diff --git a/health-services/ExerciseSampleCompose/app/src/main/java/com/example/exercisesamplecompose/data/ExerciseClientManager.kt b/health-services/ExerciseSampleCompose/app/src/main/java/com/example/exercisesamplecompose/data/ExerciseClientManager.kt index a0d2bef5..5e0ae386 100644 --- a/health-services/ExerciseSampleCompose/app/src/main/java/com/example/exercisesamplecompose/data/ExerciseClientManager.kt +++ b/health-services/ExerciseSampleCompose/app/src/main/java/com/example/exercisesamplecompose/data/ExerciseClientManager.kt @@ -15,21 +15,33 @@ */ package com.example.exercisesamplecompose.data +import android.Manifest import android.annotation.SuppressLint +import android.location.Location +import android.os.Looper +import android.util.Log +import androidx.annotation.RequiresPermission import androidx.health.services.client.ExerciseClient import androidx.health.services.client.ExerciseUpdateCallback import androidx.health.services.client.HealthServicesClient import androidx.health.services.client.data.Availability import androidx.health.services.client.data.ComparisonType +import androidx.health.services.client.data.DataPointContainer import androidx.health.services.client.data.DataType import androidx.health.services.client.data.DataTypeCondition import androidx.health.services.client.data.ExerciseConfig +import androidx.health.services.client.data.ExerciseEndReason import androidx.health.services.client.data.ExerciseGoal import androidx.health.services.client.data.ExerciseLapSummary +import androidx.health.services.client.data.ExerciseState +import androidx.health.services.client.data.ExerciseStateInfo import androidx.health.services.client.data.ExerciseType import androidx.health.services.client.data.ExerciseTypeCapabilities import androidx.health.services.client.data.ExerciseUpdate import androidx.health.services.client.data.LocationAvailability +import androidx.health.services.client.data.LocationData +import androidx.health.services.client.data.MilestoneMarkerSummary +import androidx.health.services.client.data.SampleDataPoint import androidx.health.services.client.data.WarmUpConfig import androidx.health.services.client.endExercise import androidx.health.services.client.getCapabilities @@ -39,9 +51,20 @@ import androidx.health.services.client.prepareExercise import androidx.health.services.client.resumeExercise import androidx.health.services.client.startExercise import com.example.exercisesamplecompose.service.ExerciseLogger +import com.google.android.gms.location.FusedLocationProviderClient +import com.google.android.gms.location.LocationCallback +import com.google.android.gms.location.LocationListener +import com.google.android.gms.location.LocationRequest +import com.google.android.gms.location.LocationResult +import com.google.android.gms.location.Priority +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.channels.trySendBlocking +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.callbackFlow +import java.time.Duration +import java.time.Instant import javax.inject.Inject import javax.inject.Singleton @@ -51,8 +74,9 @@ import javax.inject.Singleton @SuppressLint("RestrictedApi") @Singleton class ExerciseClientManager @Inject constructor( - val healthServicesClient: HealthServicesClient, - val logger: ExerciseLogger + private val healthServicesClient: HealthServicesClient, + private val flpClient: FusedLocationProviderClient, + private val logger: ExerciseLogger ) { val exerciseClient: ExerciseClient = healthServicesClient.exerciseClient @@ -168,6 +192,9 @@ class ExerciseClientManager @Inject constructor( * cancelled, this flow will unregister the listener. * [callbackFlow] is used to bridge between a callback-based API and Kotlin flows. */ + @OptIn(ExperimentalCoroutinesApi::class) + @SuppressLint("MissingPermission") + @RequiresPermission(Manifest.permission.ACCESS_FINE_LOCATION) val exerciseUpdateFlow = callbackFlow { val callback = object : ExerciseUpdateCallback { override fun onExerciseUpdateReceived(update: ExerciseUpdate) { @@ -194,7 +221,7 @@ class ExerciseClientManager @Inject constructor( } } - exerciseClient.setUpdateCallback(callback) + exerciseClient.setUpdateCallback(callback, flpClient) awaitClose { // Ignore async result exerciseClient.clearUpdateCallbackAsync(callback) @@ -215,5 +242,149 @@ sealed class ExerciseMessage { ExerciseMessage() } +@RequiresPermission(Manifest.permission.ACCESS_FINE_LOCATION) +@ExperimentalCoroutinesApi +fun FusedLocationProviderClient.locationUpdates(): Flow = callbackFlow { + val locationRequest = + LocationRequest.Builder(10000).setPriority(Priority.PRIORITY_HIGH_ACCURACY).build() +// val locationCallback = object : LocationCallback() { +// override fun onLocationResult(locationResult: LocationResult) { +// for (location in locationResult.locations) { +// trySend(location) +// } +// } +// } +// requestLocationUpdates(locationRequest, locationCallback, Looper.getMainLooper()) +// awaitClose { removeLocationUpdates(locationCallback) } + val locationListener = LocationListener { location -> trySend(location) } + + requestLocationUpdates(locationRequest, locationListener, Looper.getMainLooper()) // is this the right looper? + awaitClose { removeLocationUpdates(locationListener) } + + Log.d("qqqqqq", "requested location updates") +} + +@RequiresPermission(Manifest.permission.ACCESS_FINE_LOCATION) +fun FusedLocationProviderClient.locationUpdates(callback: (l: Location) -> Unit) { + val locationRequest = + LocationRequest.Builder(10000).setPriority(Priority.PRIORITY_HIGH_ACCURACY).build() + + val locationCallback = object : LocationCallback() { + override fun onLocationResult(locationResult: LocationResult) { + for (location in locationResult.locations) { + Log.d("qqqqq", "got location") + callback(location) + } + } + } + + requestLocationUpdates(locationRequest, locationCallback, Looper.getMainLooper()) +} + +@OptIn(ExperimentalCoroutinesApi::class, DelicateCoroutinesApi::class) +@RequiresPermission(Manifest.permission.ACCESS_FINE_LOCATION) +suspend fun ExerciseClient.setUpdateCallback( + callback: ExerciseUpdateCallback, + ftpClient: FusedLocationProviderClient +) { + Log.d("qqqqqq", "exerciseupdatecallback") + + val locationRequest = + LocationRequest.Builder(10000).setPriority(Priority.PRIORITY_HIGH_ACCURACY).build() + + val locationListener = LocationListener { location -> callback.onExerciseUpdateReceived( + exerciseUpdateFromLocation(location)) + } + + ftpClient.requestLocationUpdates(locationRequest, locationListener, Looper.getMainLooper()) + + ftpClient.locationUpdates { location -> callback.onExerciseUpdateReceived( + exerciseUpdateFromLocation(location)) + } + + class Proxy(val obj: ExerciseUpdateCallback) : ExerciseUpdateCallback by obj { + + override fun onExerciseUpdateReceived(update: ExerciseUpdate) { + + val hasLocation = update.latestMetrics.getData(DataType.LOCATION).isNotEmpty() + + if (hasLocation) { + Log.d("qqqqqq", "WHS is now providing location, switching to WHS") + ftpClient.removeLocationUpdates(locationListener) + } + + return obj.onExerciseUpdateReceived(update) + } + } + + val proxy = Proxy(callback) + + return setUpdateCallback(proxy) + +// val locationFlow = ftpClient.locationUpdates() +// locationFlow.collect { location -> +// Log.d("qqqqqq", "sending update from FLP") +// callback.onExerciseUpdateReceived(ExerciseUpdateFromLocation(location)) +// } +// return setUpdateCallback(callback) +} + +@SuppressLint("RestrictedApi") +@Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") +fun DataPointContainer.fromLocation(l: Location) { + DataPointContainer( + mapOf( + Pair( + DataType.LOCATION, + listOf( + SampleDataPoint( + DataType.LOCATION, + LocationData(l.latitude, l.longitude), + Duration.ZERO + ) + ) + ) + ) + ) +} + +//@SuppressLint("RestrictedApi") +@SuppressLint("RestrictedApi") +@Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") +fun exerciseUpdateFromLocation(l: Location): ExerciseUpdate { + val latestMetrics: DataPointContainer = DataPointContainer( + mapOf( + Pair( + DataType.LOCATION, + listOf( + SampleDataPoint( + DataType.LOCATION, + LocationData(l.latitude, l.longitude), + Duration.ZERO + ) + ) + ) + ) + ) + val latestAchievedGoals: Set> = emptySet() + val latestMilestoneMarkerSummaries: Set = emptySet() + val exerciseStateInfo = ExerciseStateInfo(ExerciseState.ACTIVE, ExerciseEndReason.UNKNOWN) + val exerciseConfig: ExerciseConfig? = null + val activeDurationCheckpoint: ExerciseUpdate.ActiveDurationCheckpoint? = null + val updateDurationFromBoot: Duration? = null + val startTime: Instant? = null + val activeDurationLegacy: Duration = Duration.ZERO + return ExerciseUpdate( + latestMetrics, + latestAchievedGoals, + latestMilestoneMarkerSummaries, + exerciseStateInfo, + exerciseConfig, + activeDurationCheckpoint, + updateDurationFromBoot, + startTime, + activeDurationLegacy + ) +} diff --git a/health-services/ExerciseSampleCompose/app/src/main/java/com/example/exercisesamplecompose/di/MainModule.kt b/health-services/ExerciseSampleCompose/app/src/main/java/com/example/exercisesamplecompose/di/MainModule.kt index 3fd1defe..b99855fc 100644 --- a/health-services/ExerciseSampleCompose/app/src/main/java/com/example/exercisesamplecompose/di/MainModule.kt +++ b/health-services/ExerciseSampleCompose/app/src/main/java/com/example/exercisesamplecompose/di/MainModule.kt @@ -17,10 +17,13 @@ package com.example.exercisesamplecompose.di import android.content.Context +import android.util.Log import androidx.health.services.client.HealthServices import androidx.health.services.client.HealthServicesClient import com.example.exercisesamplecompose.service.AndroidLogExerciseLogger import com.example.exercisesamplecompose.service.ExerciseLogger +import com.google.android.gms.location.FusedLocationProviderClient +import com.google.android.gms.location.LocationServices import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -51,4 +54,11 @@ class MainModule { @Singleton @Provides fun provideLogger(): ExerciseLogger = AndroidLogExerciseLogger() + + @Singleton + @Provides + fun provideFusedLocationProviderClient(@ApplicationContext context: Context): FusedLocationProviderClient { + Log.d("qqqqqq", "getting flpclient") + return LocationServices.getFusedLocationProviderClient(context) + } } diff --git a/health-services/ExerciseSampleCompose/gradle/libs.versions.toml b/health-services/ExerciseSampleCompose/gradle/libs.versions.toml index 168ef523..5bbf214f 100644 --- a/health-services/ExerciseSampleCompose/gradle/libs.versions.toml +++ b/health-services/ExerciseSampleCompose/gradle/libs.versions.toml @@ -47,7 +47,7 @@ test-ext-junit = "androidx.test.ext:junit:1.1.5" wear-compose-foundation = { module = "androidx.wear.compose:compose-foundation", version.ref = "androidx-wear-compose" } wear-compose-material = { module = "androidx.wear.compose:compose-material", version.ref = "androidx-wear-compose" } wear-ongoing-activity = "androidx.wear:wear-ongoing:1.0.0" - +play-services-location = "com.google.android.gms:play-services-location:21.0.1" [plugins] com-android-application = { id = "com.android.application", version.ref = "android-gradle-plugin" } From ddbf4bbb99b579e6ff25e8234546276182b7172d Mon Sep 17 00:00:00 2001 From: Michael Stillwell Date: Tue, 23 Jan 2024 18:07:59 +0000 Subject: [PATCH 2/2] Cleanups --- .../data/ExerciseClientManager.kt | 81 +------------------ 1 file changed, 4 insertions(+), 77 deletions(-) diff --git a/health-services/ExerciseSampleCompose/app/src/main/java/com/example/exercisesamplecompose/data/ExerciseClientManager.kt b/health-services/ExerciseSampleCompose/app/src/main/java/com/example/exercisesamplecompose/data/ExerciseClientManager.kt index 5e0ae386..e83331e8 100644 --- a/health-services/ExerciseSampleCompose/app/src/main/java/com/example/exercisesamplecompose/data/ExerciseClientManager.kt +++ b/health-services/ExerciseSampleCompose/app/src/main/java/com/example/exercisesamplecompose/data/ExerciseClientManager.kt @@ -242,68 +242,22 @@ sealed class ExerciseMessage { ExerciseMessage() } -@RequiresPermission(Manifest.permission.ACCESS_FINE_LOCATION) -@ExperimentalCoroutinesApi -fun FusedLocationProviderClient.locationUpdates(): Flow = callbackFlow { - val locationRequest = - LocationRequest.Builder(10000).setPriority(Priority.PRIORITY_HIGH_ACCURACY).build() - -// val locationCallback = object : LocationCallback() { -// override fun onLocationResult(locationResult: LocationResult) { -// for (location in locationResult.locations) { -// trySend(location) -// } -// } -// } -// requestLocationUpdates(locationRequest, locationCallback, Looper.getMainLooper()) -// awaitClose { removeLocationUpdates(locationCallback) } - - val locationListener = LocationListener { location -> trySend(location) } - - requestLocationUpdates(locationRequest, locationListener, Looper.getMainLooper()) // is this the right looper? - awaitClose { removeLocationUpdates(locationListener) } - - Log.d("qqqqqq", "requested location updates") -} - -@RequiresPermission(Manifest.permission.ACCESS_FINE_LOCATION) -fun FusedLocationProviderClient.locationUpdates(callback: (l: Location) -> Unit) { - val locationRequest = - LocationRequest.Builder(10000).setPriority(Priority.PRIORITY_HIGH_ACCURACY).build() - - val locationCallback = object : LocationCallback() { - override fun onLocationResult(locationResult: LocationResult) { - for (location in locationResult.locations) { - Log.d("qqqqq", "got location") - callback(location) - } - } - } - - requestLocationUpdates(locationRequest, locationCallback, Looper.getMainLooper()) -} - @OptIn(ExperimentalCoroutinesApi::class, DelicateCoroutinesApi::class) @RequiresPermission(Manifest.permission.ACCESS_FINE_LOCATION) suspend fun ExerciseClient.setUpdateCallback( callback: ExerciseUpdateCallback, ftpClient: FusedLocationProviderClient ) { - Log.d("qqqqqq", "exerciseupdatecallback") - val locationRequest = LocationRequest.Builder(10000).setPriority(Priority.PRIORITY_HIGH_ACCURACY).build() val locationListener = LocationListener { location -> callback.onExerciseUpdateReceived( - exerciseUpdateFromLocation(location)) + ExerciseUpdate.fromLocation(location)) + Log.d("qqqqqq", "Got location from FLP: $location") } ftpClient.requestLocationUpdates(locationRequest, locationListener, Looper.getMainLooper()) - ftpClient.locationUpdates { location -> callback.onExerciseUpdateReceived( - exerciseUpdateFromLocation(location)) - } - class Proxy(val obj: ExerciseUpdateCallback) : ExerciseUpdateCallback by obj { override fun onExerciseUpdateReceived(update: ExerciseUpdate) { @@ -311,7 +265,7 @@ suspend fun ExerciseClient.setUpdateCallback( val hasLocation = update.latestMetrics.getData(DataType.LOCATION).isNotEmpty() if (hasLocation) { - Log.d("qqqqqq", "WHS is now providing location, switching to WHS") + Log.d("qqqqqq", "Removing FLP") ftpClient.removeLocationUpdates(locationListener) } @@ -322,38 +276,11 @@ suspend fun ExerciseClient.setUpdateCallback( val proxy = Proxy(callback) return setUpdateCallback(proxy) - -// val locationFlow = ftpClient.locationUpdates() -// locationFlow.collect { location -> -// Log.d("qqqqqq", "sending update from FLP") -// callback.onExerciseUpdateReceived(ExerciseUpdateFromLocation(location)) -// } -// return setUpdateCallback(callback) -} - -@SuppressLint("RestrictedApi") -@Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") -fun DataPointContainer.fromLocation(l: Location) { - DataPointContainer( - mapOf( - Pair( - DataType.LOCATION, - listOf( - SampleDataPoint( - DataType.LOCATION, - LocationData(l.latitude, l.longitude), - Duration.ZERO - ) - ) - ) - ) - ) } -//@SuppressLint("RestrictedApi") @SuppressLint("RestrictedApi") @Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") -fun exerciseUpdateFromLocation(l: Location): ExerciseUpdate { +fun ExerciseUpdate.Companion.fromLocation(l: Location): ExerciseUpdate { val latestMetrics: DataPointContainer = DataPointContainer( mapOf( Pair(