diff --git a/health-services/ExerciseSampleCompose/app/src/main/java/com/example/exercisesamplecompose/data/HealthServicesRepository.kt b/health-services/ExerciseSampleCompose/app/src/main/java/com/example/exercisesamplecompose/data/HealthServicesRepository.kt index 4def12c6..31ff0259 100644 --- a/health-services/ExerciseSampleCompose/app/src/main/java/com/example/exercisesamplecompose/data/HealthServicesRepository.kt +++ b/health-services/ExerciseSampleCompose/app/src/main/java/com/example/exercisesamplecompose/data/HealthServicesRepository.kt @@ -20,38 +20,45 @@ package com.example.exercisesamplecompose.data import android.content.Context import androidx.health.services.client.data.LocationAvailability import com.example.exercisesamplecompose.di.bindService +import com.example.exercisesamplecompose.service.ExerciseLogger import com.example.exercisesamplecompose.service.ExerciseService import com.example.exercisesamplecompose.service.ExerciseServiceState import dagger.hilt.android.ActivityRetainedLifecycle import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.scopes.ActivityRetainedScoped +import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch -import javax.inject.Inject @ActivityRetainedScoped class HealthServicesRepository @Inject constructor( @ApplicationContext private val applicationContext: Context, val exerciseClientManager: ExerciseClientManager, + val logger: ExerciseLogger, val coroutineScope: CoroutineScope, val lifecycle: ActivityRetainedLifecycle ) { private val binderConnection = lifecycle.bindService(applicationContext) - val serviceState: StateFlow = - binderConnection.flowWhenConnected(ExerciseService.LocalBinder::exerciseServiceState).map { - ServiceState.Connected(it) - }.stateIn( - coroutineScope, - started = SharingStarted.Eagerly, - initialValue = ServiceState.Disconnected - ) + private val exerciseServiceStateUpdates: Flow = binderConnection.flowWhenConnected(ExerciseService.LocalBinder::exerciseServiceState) + + private var errorState: MutableStateFlow = MutableStateFlow(null) + + val serviceState: StateFlow = exerciseServiceStateUpdates.combine(errorState) { exerciseServiceState, errorString -> + ServiceState.Connected(exerciseServiceState.copy(error = errorString)) + }.stateIn( + coroutineScope, + started = SharingStarted.Eagerly, + initialValue = ServiceState.Disconnected + ) suspend fun hasExerciseCapability(): Boolean = getExerciseCapabilities() != null @@ -71,7 +78,16 @@ class HealthServicesRepository @Inject constructor( } } - fun startExercise() = serviceCall { startExercise() } + fun startExercise() = serviceCall { + try { + errorState.value = null + startExercise() + } catch (e: Exception) { + errorState.value = e.message + logger.error(e.toString()) + e.printStackTrace() + } + } fun pauseExercise() = serviceCall { pauseExercise() } fun endExercise() = serviceCall { endExercise() } fun resumeExercise() = serviceCall { resumeExercise() } @@ -80,7 +96,8 @@ class HealthServicesRepository @Inject constructor( /** Store exercise values in the service state. While the service is connected, * the values will persist.**/ sealed class ServiceState { - object Disconnected : ServiceState() + data object Disconnected : ServiceState() + data class Connected( val exerciseServiceState: ExerciseServiceState, ) : ServiceState() { diff --git a/health-services/ExerciseSampleCompose/app/src/main/java/com/example/exercisesamplecompose/presentation/ExerciseSampleApp.kt b/health-services/ExerciseSampleCompose/app/src/main/java/com/example/exercisesamplecompose/presentation/ExerciseSampleApp.kt index 539b6690..2041a9c8 100644 --- a/health-services/ExerciseSampleCompose/app/src/main/java/com/example/exercisesamplecompose/presentation/ExerciseSampleApp.kt +++ b/health-services/ExerciseSampleCompose/app/src/main/java/com/example/exercisesamplecompose/presentation/ExerciseSampleApp.kt @@ -98,7 +98,11 @@ fun ExerciseSampleApp( columnState = columnState, onSummary = { navController.navigateToTopLevel(Summary, Summary.buildRoute(it)) - } + }, + onRestart = { + navController.navigateToTopLevel(PreparingExercise) + }, + onFinishActivity = onFinishActivity ) } } diff --git a/health-services/ExerciseSampleCompose/app/src/main/java/com/example/exercisesamplecompose/presentation/exercise/ExerciseScreen.kt b/health-services/ExerciseSampleCompose/app/src/main/java/com/example/exercisesamplecompose/presentation/exercise/ExerciseScreen.kt index 2512bbff..298fcda5 100644 --- a/health-services/ExerciseSampleCompose/app/src/main/java/com/example/exercisesamplecompose/presentation/exercise/ExerciseScreen.kt +++ b/health-services/ExerciseSampleCompose/app/src/main/java/com/example/exercisesamplecompose/presentation/exercise/ExerciseScreen.kt @@ -36,7 +36,9 @@ import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.wear.compose.material.Icon import androidx.wear.compose.material.Text +import androidx.wear.compose.ui.tooling.preview.WearPreviewDevices import com.example.exercisesamplecompose.R +import com.example.exercisesamplecompose.data.ServiceState import com.example.exercisesamplecompose.presentation.component.CaloriesText import com.example.exercisesamplecompose.presentation.component.DistanceText import com.example.exercisesamplecompose.presentation.component.HRText @@ -46,10 +48,13 @@ import com.example.exercisesamplecompose.presentation.component.StartButton import com.example.exercisesamplecompose.presentation.component.StopButton import com.example.exercisesamplecompose.presentation.component.formatElapsedTime import com.example.exercisesamplecompose.presentation.summary.SummaryScreenState +import com.example.exercisesamplecompose.presentation.theme.ThemePreview +import com.example.exercisesamplecompose.service.ExerciseServiceState import com.google.android.horologist.annotations.ExperimentalHorologistApi import com.google.android.horologist.compose.ambient.AmbientState import com.google.android.horologist.compose.layout.ScalingLazyColumn import com.google.android.horologist.compose.layout.ScalingLazyColumnState +import com.google.android.horologist.compose.material.AlertDialog import com.google.android.horologist.health.composables.ActiveDurationText @Composable @@ -57,7 +62,9 @@ fun ExerciseRoute( ambientState: AmbientState, columnState: ScalingLazyColumnState, modifier: Modifier = Modifier, - onSummary: (SummaryScreenState) -> Unit + onSummary: (SummaryScreenState) -> Unit, + onRestart: () -> Unit, + onFinishActivity: () -> Unit, ) { val viewModel = hiltViewModel() val uiState by viewModel.uiState.collectAsStateWithLifecycle() @@ -68,7 +75,13 @@ fun ExerciseRoute( } } - if (ambientState is AmbientState.Interactive) { + if (uiState.error != null) { + ErrorStartingExerciseScreen( + onRestart = onRestart, + onFinishActivity = onFinishActivity, + uiState = uiState + ) + } else if (ambientState is AmbientState.Interactive) { ExerciseScreen( onPauseClick = { viewModel.pauseExercise() }, onEndClick = { viewModel.endExercise() }, @@ -81,6 +94,24 @@ fun ExerciseRoute( } } +/** + * Shows an error that occured when starting an exercise + */ +@Composable +fun ErrorStartingExerciseScreen( + onRestart: () -> Unit, + onFinishActivity: () -> Unit, + uiState: ExerciseScreenState +) { + AlertDialog( + title = stringResource(id = R.string.error_starting_exercise), + message = "${uiState.error ?: stringResource(id = R.string.unknown_error)}. ${stringResource(id = R.string.try_again)}", + onCancel = onFinishActivity, + onOk = onRestart, + showDialog = true, + ) +} + /** * Shows while an exercise is in progress */ @@ -220,10 +251,44 @@ private fun DurationRow(uiState: ExerciseScreenState) { } } +@WearPreviewDevices +@Composable +fun ExerciseScreenPreview() { + ThemePreview { + ExerciseScreen( + onPauseClick = {}, + onEndClick = {}, + onResumeClick = {}, + onStartClick = {}, + uiState = ExerciseScreenState( + hasExerciseCapabilities = true, + isTrackingAnotherExercise = false, + serviceState = ServiceState.Connected( + ExerciseServiceState() + ), + exerciseState = ExerciseServiceState() + ), + columnState = ScalingLazyColumnState(), + modifier = Modifier + ) + } +} - - - - - - +@WearPreviewDevices +@Composable +fun ErrorStartingExerciseScreenPreview() { + ThemePreview { + ErrorStartingExerciseScreen( + onRestart = {}, + onFinishActivity = {}, + uiState = ExerciseScreenState( + hasExerciseCapabilities = true, + isTrackingAnotherExercise = false, + serviceState = ServiceState.Connected( + ExerciseServiceState() + ), + exerciseState = ExerciseServiceState() + ) + ) + } +} diff --git a/health-services/ExerciseSampleCompose/app/src/main/java/com/example/exercisesamplecompose/presentation/exercise/ExerciseScreenState.kt b/health-services/ExerciseSampleCompose/app/src/main/java/com/example/exercisesamplecompose/presentation/exercise/ExerciseScreenState.kt index 51a7510b..8d051d21 100644 --- a/health-services/ExerciseSampleCompose/app/src/main/java/com/example/exercisesamplecompose/presentation/exercise/ExerciseScreenState.kt +++ b/health-services/ExerciseSampleCompose/app/src/main/java/com/example/exercisesamplecompose/presentation/exercise/ExerciseScreenState.kt @@ -28,4 +28,10 @@ data class ExerciseScreenState( val isPaused: Boolean get() = exerciseState?.exerciseState?.isPaused == true + + val error: String? + get() = when(serviceState) { + is ServiceState.Connected -> serviceState.exerciseServiceState.error + else -> null + } } \ No newline at end of file diff --git a/health-services/ExerciseSampleCompose/app/src/main/java/com/example/exercisesamplecompose/presentation/exercise/ExerciseViewModel.kt b/health-services/ExerciseSampleCompose/app/src/main/java/com/example/exercisesamplecompose/presentation/exercise/ExerciseViewModel.kt index 741eb11e..d82be641 100644 --- a/health-services/ExerciseSampleCompose/app/src/main/java/com/example/exercisesamplecompose/presentation/exercise/ExerciseViewModel.kt +++ b/health-services/ExerciseSampleCompose/app/src/main/java/com/example/exercisesamplecompose/presentation/exercise/ExerciseViewModel.kt @@ -20,11 +20,11 @@ import androidx.lifecycle.viewModelScope import com.example.exercisesamplecompose.data.HealthServicesRepository import com.example.exercisesamplecompose.data.ServiceState import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn -import javax.inject.Inject @HiltViewModel class ExerciseViewModel @Inject constructor( diff --git a/health-services/ExerciseSampleCompose/app/src/main/java/com/example/exercisesamplecompose/service/ExerciseLogger.kt b/health-services/ExerciseSampleCompose/app/src/main/java/com/example/exercisesamplecompose/service/ExerciseLogger.kt index def25bb3..2578354d 100644 --- a/health-services/ExerciseSampleCompose/app/src/main/java/com/example/exercisesamplecompose/service/ExerciseLogger.kt +++ b/health-services/ExerciseSampleCompose/app/src/main/java/com/example/exercisesamplecompose/service/ExerciseLogger.kt @@ -2,12 +2,19 @@ package com.example.exercisesamplecompose.service import android.util.Log +private const val TAG = "ExerciseSample" + interface ExerciseLogger { + fun error(message: String) fun log(message: String) } class AndroidLogExerciseLogger : ExerciseLogger { + override fun error(message: String) { + Log.e(TAG, message) + } + override fun log(message: String) { - Log.i("ExerciseSample", message) + Log.i(TAG, message) } } \ No newline at end of file diff --git a/health-services/ExerciseSampleCompose/app/src/main/java/com/example/exercisesamplecompose/service/ExerciseState.kt b/health-services/ExerciseSampleCompose/app/src/main/java/com/example/exercisesamplecompose/service/ExerciseState.kt index 9f532d3b..a5a137d8 100644 --- a/health-services/ExerciseSampleCompose/app/src/main/java/com/example/exercisesamplecompose/service/ExerciseState.kt +++ b/health-services/ExerciseSampleCompose/app/src/main/java/com/example/exercisesamplecompose/service/ExerciseState.kt @@ -30,5 +30,6 @@ data class ExerciseServiceState( val exerciseMetrics: ExerciseMetrics = ExerciseMetrics(), val exerciseLaps: Int = 0, val activeDurationCheckpoint: ActiveDurationCheckpoint? = null, - val locationAvailability: LocationAvailability = LocationAvailability.UNKNOWN + val locationAvailability: LocationAvailability = LocationAvailability.UNKNOWN, + val error: String? = null, ) \ No newline at end of file diff --git a/health-services/ExerciseSampleCompose/app/src/main/res/values/strings.xml b/health-services/ExerciseSampleCompose/app/src/main/res/values/strings.xml index d6b89a8c..1c1798e5 100644 --- a/health-services/ExerciseSampleCompose/app/src/main/res/values/strings.xml +++ b/health-services/ExerciseSampleCompose/app/src/main/res/values/strings.xml @@ -21,6 +21,9 @@ Start Starting up Exercise not available on this device + Error starting exercise + Unknown error + Would you like to try again? GPS Acquiring