diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 5dab6e7c5..c80295aa8 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -13,7 +13,8 @@ android {
defaultConfig {
applicationId = "edu.stanford.bdh.engagehf"
- versionCode = (project.findProperty("android.injected.version.code") as? String)?.toInt() ?: 1
+ versionCode =
+ (project.findProperty("android.injected.version.code") as? String)?.toInt() ?: 1
versionName = (project.findProperty("android.injected.version.name") as? String) ?: "1.0.0"
targetSdk = libs.versions.targetSdk.get().toInt()
@@ -49,6 +50,7 @@ dependencies {
implementation(project(":modules:onboarding"))
implementation(libs.firebase.firestore.ktx)
+ implementation(libs.firebase.functions.ktx)
implementation(libs.androidx.core.i18n)
implementation(libs.androidx.core.ktx)
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 3a4e7b1e3..3c3cf8b7a 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -24,6 +24,15 @@
+
+
+
diff --git a/app/src/main/kotlin/edu/stanford/bdh/engagehf/bluetooth/BluetoothViewModel.kt b/app/src/main/kotlin/edu/stanford/bdh/engagehf/bluetooth/BluetoothViewModel.kt
index 394ebefff..b69cf0d7d 100644
--- a/app/src/main/kotlin/edu/stanford/bdh/engagehf/bluetooth/BluetoothViewModel.kt
+++ b/app/src/main/kotlin/edu/stanford/bdh/engagehf/bluetooth/BluetoothViewModel.kt
@@ -3,16 +3,18 @@ package edu.stanford.bdh.engagehf.bluetooth
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
-import edu.stanford.bdh.engagehf.bluetooth.component.BottomSheetEvents
+import edu.stanford.bdh.engagehf.bluetooth.component.AppScreenEvents
import edu.stanford.bdh.engagehf.bluetooth.data.mapper.BluetoothUiStateMapper
import edu.stanford.bdh.engagehf.bluetooth.data.models.Action
import edu.stanford.bdh.engagehf.bluetooth.data.models.BluetoothUiState
import edu.stanford.bdh.engagehf.bluetooth.data.models.UiState
import edu.stanford.bdh.engagehf.bluetooth.measurements.MeasurementsRepository
import edu.stanford.bdh.engagehf.education.EngageEducationRepository
+import edu.stanford.bdh.engagehf.messages.HealthSummaryService
import edu.stanford.bdh.engagehf.messages.MessageRepository
import edu.stanford.bdh.engagehf.messages.MessagesAction
import edu.stanford.bdh.engagehf.navigation.AppNavigationEvent
+import edu.stanford.bdh.engagehf.navigation.screens.BottomBarItem
import edu.stanford.spezi.core.bluetooth.api.BLEService
import edu.stanford.spezi.core.bluetooth.data.model.BLEServiceEvent
import edu.stanford.spezi.core.bluetooth.data.model.BLEServiceState
@@ -34,9 +36,10 @@ class BluetoothViewModel @Inject internal constructor(
private val uiStateMapper: BluetoothUiStateMapper,
private val measurementsRepository: MeasurementsRepository,
private val messageRepository: MessageRepository,
- private val bottomSheetEvents: BottomSheetEvents,
+ private val appScreenEvents: AppScreenEvents,
private val navigator: Navigator,
private val engageEducationRepository: EngageEducationRepository,
+ private val healthSummaryService: HealthSummaryService,
) : ViewModel() {
private val logger by speziLogger()
@@ -94,7 +97,7 @@ class BluetoothViewModel @Inject internal constructor(
}
is BLEServiceEvent.MeasurementReceived -> {
- bottomSheetEvents.emit(BottomSheetEvents.Event.CloseBottomSheet)
+ appScreenEvents.emit(AppScreenEvents.Event.CloseBottomSheet)
_uiState.update {
it.copy(
measurementDialog = uiStateMapper.mapToMeasurementDialogUiState(
@@ -131,7 +134,11 @@ class BluetoothViewModel @Inject internal constructor(
private fun observeMessages() {
viewModelScope.launch {
messageRepository.observeUserMessages().collect { messages ->
- _uiState.update { it.copy(messages = messages) }
+ _uiState.update {
+ it.copy(
+ messages = messages
+ )
+ }
}
}
}
@@ -150,47 +157,45 @@ class BluetoothViewModel @Inject internal constructor(
is Action.MessageItemClicked -> {
viewModelScope.launch {
- val mappingResult = uiStateMapper.mapMessagesAction(action.message.action)
- if (mappingResult.isSuccess) {
- when (val mappedAction = mappingResult.getOrNull()!!) {
- is MessagesAction.HealthSummaryAction -> { /* TODO */
- }
-
- is MessagesAction.MeasurementsAction -> {
- bottomSheetEvents.emit(BottomSheetEvents.Event.DoNewMeasurement)
- }
+ uiStateMapper.mapMessagesAction(action.message.action)
+ .onFailure { error ->
+ logger.e(error) { "Error while mapping action: ${action.message.action}" }
+ }
+ .onSuccess { mappedAction ->
+ val messageId = action.message.id
+ when (mappedAction) {
+ is MessagesAction.HealthSummaryAction -> {
+ handleHealthSummaryAction(messageId)
+ }
- is MessagesAction.MedicationsAction -> { /* TODO */
- }
+ is MessagesAction.MeasurementsAction -> {
+ appScreenEvents.emit(AppScreenEvents.Event.DoNewMeasurement)
+ }
- is MessagesAction.QuestionnaireAction -> {
- navigator.navigateTo(
- AppNavigationEvent.QuestionnaireScreen(
- mappedAction.questionnaireId
+ is MessagesAction.MedicationsAction -> {
+ appScreenEvents.emit(
+ AppScreenEvents.Event.NavigateToTab(
+ BottomBarItem.MEDICATION
+ )
)
- )
- }
+ }
- is MessagesAction.VideoSectionAction -> {
- viewModelScope.launch {
- engageEducationRepository.getVideoBySectionAndVideoId(
- mappedAction.videoSectionVideo.videoSectionId,
- mappedAction.videoSectionVideo.videoId
- ).getOrNull()?.let { video ->
- navigator.navigateTo(
- EducationNavigationEvent.VideoSectionClicked(
- video = video
- )
+ is MessagesAction.QuestionnaireAction -> {
+ navigator.navigateTo(
+ AppNavigationEvent.QuestionnaireScreen(
+ mappedAction.questionnaireId
)
+ )
+ }
+
+ is MessagesAction.VideoSectionAction -> {
+ viewModelScope.launch {
+ handleVideoSectionAction(mappedAction)
}
}
}
+ messageRepository.completeMessage(messageId = messageId)
}
- val messageId = action.message.id
- messageRepository.completeMessage(messageId = messageId)
- } else {
- logger.e { "Error while mapping action: ${mappingResult.exceptionOrNull()}" }
- }
}
}
@@ -200,6 +205,40 @@ class BluetoothViewModel @Inject internal constructor(
}
}
+ private suspend fun handleVideoSectionAction(messageAction: MessagesAction.VideoSectionAction) {
+ engageEducationRepository.getVideoBySectionAndVideoId(
+ messageAction.videoSectionVideo.videoSectionId,
+ messageAction.videoSectionVideo.videoId
+ ).getOrNull()?.let { video ->
+ navigator.navigateTo(
+ EducationNavigationEvent.VideoSectionClicked(
+ video = video
+ )
+ )
+ }
+ }
+
+ private fun handleHealthSummaryAction(messageId: String) {
+ val setLoading = { loading: Boolean ->
+ _uiState.update {
+ it.copy(
+ messages = it.messages.map { message ->
+ if (message.id == messageId) {
+ message.copy(isLoading = loading)
+ } else {
+ message
+ }
+ }
+ )
+ }
+ }
+ viewModelScope.launch {
+ setLoading(true)
+ healthSummaryService.generateHealthSummaryPdf()
+ setLoading(false)
+ }
+ }
+
public override fun onCleared() {
super.onCleared()
bleService.stop()
diff --git a/app/src/main/kotlin/edu/stanford/bdh/engagehf/bluetooth/component/BottomSheetEvents.kt b/app/src/main/kotlin/edu/stanford/bdh/engagehf/bluetooth/component/AppScreenEvents.kt
similarity index 86%
rename from app/src/main/kotlin/edu/stanford/bdh/engagehf/bluetooth/component/BottomSheetEvents.kt
rename to app/src/main/kotlin/edu/stanford/bdh/engagehf/bluetooth/component/AppScreenEvents.kt
index c97765ec6..d8cef33b7 100644
--- a/app/src/main/kotlin/edu/stanford/bdh/engagehf/bluetooth/component/BottomSheetEvents.kt
+++ b/app/src/main/kotlin/edu/stanford/bdh/engagehf/bluetooth/component/AppScreenEvents.kt
@@ -1,5 +1,6 @@
package edu.stanford.bdh.engagehf.bluetooth.component
+import edu.stanford.bdh.engagehf.navigation.screens.BottomBarItem
import edu.stanford.spezi.core.coroutines.di.Dispatching
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableSharedFlow
@@ -10,7 +11,7 @@ import javax.inject.Inject
import javax.inject.Singleton
@Singleton
-class BottomSheetEvents @Inject constructor(
+class AppScreenEvents @Inject constructor(
@Dispatching.IO private val scope: CoroutineScope,
) {
private val _events = MutableSharedFlow(replay = 1)
@@ -30,5 +31,6 @@ class BottomSheetEvents @Inject constructor(
data object AddWeightRecord : Event
data object AddBloodPressureRecord : Event
data object AddHeartRateRecord : Event
+ data class NavigateToTab(val bottomBarItem: BottomBarItem) : Event
}
}
diff --git a/app/src/main/kotlin/edu/stanford/bdh/engagehf/health/HealthViewModel.kt b/app/src/main/kotlin/edu/stanford/bdh/engagehf/health/HealthViewModel.kt
index 299136dc4..43edea210 100644
--- a/app/src/main/kotlin/edu/stanford/bdh/engagehf/health/HealthViewModel.kt
+++ b/app/src/main/kotlin/edu/stanford/bdh/engagehf/health/HealthViewModel.kt
@@ -2,7 +2,7 @@ package edu.stanford.bdh.engagehf.health
import androidx.lifecycle.ViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
-import edu.stanford.bdh.engagehf.bluetooth.component.BottomSheetEvents
+import edu.stanford.bdh.engagehf.bluetooth.component.AppScreenEvents
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
@@ -10,7 +10,7 @@ import javax.inject.Inject
@HiltViewModel
class HealthViewModel @Inject constructor(
- private val bottomSheetEvents: BottomSheetEvents,
+ private val appScreenEvents: AppScreenEvents,
) : ViewModel() {
private val _uiState = MutableStateFlow(UiState())
val uiState = _uiState.asStateFlow()
@@ -19,12 +19,12 @@ class HealthViewModel @Inject constructor(
when (action) {
is Action.AddRecord -> {
val event = when (action.tab) {
- HealthTab.Weight -> BottomSheetEvents.Event.AddWeightRecord
- HealthTab.BloodPressure -> BottomSheetEvents.Event.AddBloodPressureRecord
- HealthTab.HeartRate -> BottomSheetEvents.Event.AddHeartRateRecord
+ HealthTab.Weight -> AppScreenEvents.Event.AddWeightRecord
+ HealthTab.BloodPressure -> AppScreenEvents.Event.AddBloodPressureRecord
+ HealthTab.HeartRate -> AppScreenEvents.Event.AddHeartRateRecord
else -> return
}
- bottomSheetEvents.emit(event)
+ appScreenEvents.emit(event)
}
is Action.UpdateTab -> {
diff --git a/app/src/main/kotlin/edu/stanford/bdh/engagehf/health/bloodpressure/BloodPressureViewModel.kt b/app/src/main/kotlin/edu/stanford/bdh/engagehf/health/bloodpressure/BloodPressureViewModel.kt
index 9360dbdaa..f13ba8f07 100644
--- a/app/src/main/kotlin/edu/stanford/bdh/engagehf/health/bloodpressure/BloodPressureViewModel.kt
+++ b/app/src/main/kotlin/edu/stanford/bdh/engagehf/health/bloodpressure/BloodPressureViewModel.kt
@@ -3,7 +3,7 @@ package edu.stanford.bdh.engagehf.health.bloodpressure
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
-import edu.stanford.bdh.engagehf.bluetooth.component.BottomSheetEvents
+import edu.stanford.bdh.engagehf.bluetooth.component.AppScreenEvents
import edu.stanford.bdh.engagehf.health.HealthAction
import edu.stanford.bdh.engagehf.health.HealthRepository
import edu.stanford.bdh.engagehf.health.HealthUiState
@@ -19,7 +19,7 @@ import javax.inject.Inject
class BloodPressureViewModel @Inject internal constructor(
private val uiStateMapper: HealthUiStateMapper,
private val healthRepository: HealthRepository,
- private val bottomSheetEvents: BottomSheetEvents,
+ private val appScreenEvents: AppScreenEvents,
) : ViewModel() {
private val _uiState = MutableStateFlow(HealthUiState.Loading)
val uiState = _uiState.asStateFlow()
@@ -56,7 +56,7 @@ class BloodPressureViewModel @Inject internal constructor(
}
HealthAction.DescriptionBottomSheet -> {
- bottomSheetEvents.emit(BottomSheetEvents.Event.BloodPressureDescriptionBottomSheet)
+ appScreenEvents.emit(AppScreenEvents.Event.BloodPressureDescriptionBottomSheet)
}
is HealthAction.ToggleTimeRangeDropdown -> {
diff --git a/app/src/main/kotlin/edu/stanford/bdh/engagehf/health/bloodpressure/bottomsheet/AddBloodPressureBottomSheetViewModel.kt b/app/src/main/kotlin/edu/stanford/bdh/engagehf/health/bloodpressure/bottomsheet/AddBloodPressureBottomSheetViewModel.kt
index 99a778a12..f1469e2ae 100644
--- a/app/src/main/kotlin/edu/stanford/bdh/engagehf/health/bloodpressure/bottomsheet/AddBloodPressureBottomSheetViewModel.kt
+++ b/app/src/main/kotlin/edu/stanford/bdh/engagehf/health/bloodpressure/bottomsheet/AddBloodPressureBottomSheetViewModel.kt
@@ -5,7 +5,7 @@ import androidx.health.connect.client.units.Pressure
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
-import edu.stanford.bdh.engagehf.bluetooth.component.BottomSheetEvents
+import edu.stanford.bdh.engagehf.bluetooth.component.AppScreenEvents
import edu.stanford.bdh.engagehf.health.HealthRepository
import edu.stanford.spezi.core.utils.MessageNotifier
import kotlinx.coroutines.flow.MutableStateFlow
@@ -19,7 +19,7 @@ import javax.inject.Inject
@HiltViewModel
internal class AddBloodPressureBottomSheetViewModel @Inject constructor(
- private val bottomSheetEvents: BottomSheetEvents,
+ private val appScreenEvents: AppScreenEvents,
private val addBloodPressureBottomSheetUiStateMapper: AddBloodPressureBottomSheetUiStateMapper,
private val healthRepository: HealthRepository,
private val notifier: MessageNotifier,
@@ -62,7 +62,7 @@ internal class AddBloodPressureBottomSheetViewModel @Inject constructor(
}
Action.CloseSheet -> {
- bottomSheetEvents.emit(BottomSheetEvents.Event.CloseBottomSheet)
+ appScreenEvents.emit(AppScreenEvents.Event.CloseBottomSheet)
}
Action.CloseUpdateDate -> {
@@ -112,7 +112,7 @@ internal class AddBloodPressureBottomSheetViewModel @Inject constructor(
healthRepository.saveRecord(bloodPressureRecord).onFailure {
notifier.notify("Failed to save blood pressure record")
}.onSuccess {
- bottomSheetEvents.emit(BottomSheetEvents.Event.CloseBottomSheet)
+ appScreenEvents.emit(AppScreenEvents.Event.CloseBottomSheet)
}
}
}
diff --git a/app/src/main/kotlin/edu/stanford/bdh/engagehf/health/heartrate/HeartRateViewModel.kt b/app/src/main/kotlin/edu/stanford/bdh/engagehf/health/heartrate/HeartRateViewModel.kt
index b81f0d0f5..e694e0362 100644
--- a/app/src/main/kotlin/edu/stanford/bdh/engagehf/health/heartrate/HeartRateViewModel.kt
+++ b/app/src/main/kotlin/edu/stanford/bdh/engagehf/health/heartrate/HeartRateViewModel.kt
@@ -3,7 +3,7 @@ package edu.stanford.bdh.engagehf.health.heartrate
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
-import edu.stanford.bdh.engagehf.bluetooth.component.BottomSheetEvents
+import edu.stanford.bdh.engagehf.bluetooth.component.AppScreenEvents
import edu.stanford.bdh.engagehf.health.HealthAction
import edu.stanford.bdh.engagehf.health.HealthRepository
import edu.stanford.bdh.engagehf.health.HealthUiState
@@ -20,7 +20,7 @@ import javax.inject.Inject
class HeartRateViewModel @Inject internal constructor(
private val uiStateMapper: HealthUiStateMapper,
private val healthRepository: HealthRepository,
- private val bottomSheetEvents: BottomSheetEvents,
+ private val appScreenEvents: AppScreenEvents,
) : ViewModel() {
private val logger by speziLogger()
@@ -60,7 +60,7 @@ class HeartRateViewModel @Inject internal constructor(
}
is HealthAction.DescriptionBottomSheet -> {
- bottomSheetEvents.emit(BottomSheetEvents.Event.HeartRateDescriptionBottomSheet)
+ appScreenEvents.emit(AppScreenEvents.Event.HeartRateDescriptionBottomSheet)
}
is HealthAction.ToggleTimeRangeDropdown -> {
diff --git a/app/src/main/kotlin/edu/stanford/bdh/engagehf/health/heartrate/bottomsheet/AddHeartRateBottomSheetViewModel.kt b/app/src/main/kotlin/edu/stanford/bdh/engagehf/health/heartrate/bottomsheet/AddHeartRateBottomSheetViewModel.kt
index b4b13119c..8e79289bd 100644
--- a/app/src/main/kotlin/edu/stanford/bdh/engagehf/health/heartrate/bottomsheet/AddHeartRateBottomSheetViewModel.kt
+++ b/app/src/main/kotlin/edu/stanford/bdh/engagehf/health/heartrate/bottomsheet/AddHeartRateBottomSheetViewModel.kt
@@ -4,7 +4,7 @@ import androidx.health.connect.client.records.HeartRateRecord
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
-import edu.stanford.bdh.engagehf.bluetooth.component.BottomSheetEvents
+import edu.stanford.bdh.engagehf.bluetooth.component.AppScreenEvents
import edu.stanford.bdh.engagehf.health.HealthRepository
import edu.stanford.spezi.core.utils.MessageNotifier
import kotlinx.coroutines.flow.MutableStateFlow
@@ -18,7 +18,7 @@ import javax.inject.Inject
@HiltViewModel
internal class AddHeartRateBottomSheetViewModel @Inject constructor(
- private val bottomSheetEvents: BottomSheetEvents,
+ private val appScreenEvents: AppScreenEvents,
private val healthRepository: HealthRepository,
private val addHeartRateBottomSheetUiStateMapper: AddHeartRateBottomSheetUiStateMapper,
private val notifier: MessageNotifier,
@@ -30,7 +30,7 @@ internal class AddHeartRateBottomSheetViewModel @Inject constructor(
fun onAction(action: Action) {
when (action) {
Action.CloseSheet -> {
- bottomSheetEvents.emit(BottomSheetEvents.Event.CloseBottomSheet)
+ appScreenEvents.emit(AppScreenEvents.Event.CloseBottomSheet)
}
Action.SaveHeartRate -> {
@@ -83,7 +83,7 @@ internal class AddHeartRateBottomSheetViewModel @Inject constructor(
healthRepository.saveRecord(heartRate).onFailure {
notifier.notify("Failed to save heart rate record")
}.onSuccess {
- bottomSheetEvents.emit(BottomSheetEvents.Event.CloseBottomSheet)
+ appScreenEvents.emit(AppScreenEvents.Event.CloseBottomSheet)
}
}
}
diff --git a/app/src/main/kotlin/edu/stanford/bdh/engagehf/health/weight/WeightViewModel.kt b/app/src/main/kotlin/edu/stanford/bdh/engagehf/health/weight/WeightViewModel.kt
index 86dc4d8ad..e37d89987 100644
--- a/app/src/main/kotlin/edu/stanford/bdh/engagehf/health/weight/WeightViewModel.kt
+++ b/app/src/main/kotlin/edu/stanford/bdh/engagehf/health/weight/WeightViewModel.kt
@@ -5,7 +5,7 @@ package edu.stanford.bdh.engagehf.health.weight
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
-import edu.stanford.bdh.engagehf.bluetooth.component.BottomSheetEvents
+import edu.stanford.bdh.engagehf.bluetooth.component.AppScreenEvents
import edu.stanford.bdh.engagehf.health.HealthAction
import edu.stanford.bdh.engagehf.health.HealthRepository
import edu.stanford.bdh.engagehf.health.HealthUiState
@@ -20,7 +20,7 @@ import javax.inject.Inject
@HiltViewModel
class WeightViewModel @Inject internal constructor(
- private val bottomSheetEvents: BottomSheetEvents,
+ private val appScreenEvents: AppScreenEvents,
private val uiStateMapper: HealthUiStateMapper,
private val healthRepository: HealthRepository,
) : ViewModel() {
@@ -68,7 +68,7 @@ class WeightViewModel @Inject internal constructor(
}
HealthAction.DescriptionBottomSheet -> {
- bottomSheetEvents.emit(BottomSheetEvents.Event.WeightDescriptionBottomSheet)
+ appScreenEvents.emit(AppScreenEvents.Event.WeightDescriptionBottomSheet)
}
is HealthAction.ToggleTimeRangeDropdown -> {
diff --git a/app/src/main/kotlin/edu/stanford/bdh/engagehf/health/weight/bottomsheet/AddWeightBottomSheetViewModel.kt b/app/src/main/kotlin/edu/stanford/bdh/engagehf/health/weight/bottomsheet/AddWeightBottomSheetViewModel.kt
index b1e3bce52..587fa4028 100644
--- a/app/src/main/kotlin/edu/stanford/bdh/engagehf/health/weight/bottomsheet/AddWeightBottomSheetViewModel.kt
+++ b/app/src/main/kotlin/edu/stanford/bdh/engagehf/health/weight/bottomsheet/AddWeightBottomSheetViewModel.kt
@@ -5,7 +5,7 @@ import androidx.health.connect.client.units.Mass
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
-import edu.stanford.bdh.engagehf.bluetooth.component.BottomSheetEvents
+import edu.stanford.bdh.engagehf.bluetooth.component.AppScreenEvents
import edu.stanford.bdh.engagehf.health.HealthRepository
import edu.stanford.spezi.core.utils.MessageNotifier
import kotlinx.coroutines.flow.MutableStateFlow
@@ -19,7 +19,7 @@ import javax.inject.Inject
@HiltViewModel
class AddWeightBottomSheetViewModel @Inject internal constructor(
- private val bottomSheetEvents: BottomSheetEvents,
+ private val appScreenEvents: AppScreenEvents,
private val uiStateMapper: AddWeightBottomSheetUiStateMapper,
private val healthRepository: HealthRepository,
private val notifier: MessageNotifier,
@@ -35,7 +35,7 @@ class AddWeightBottomSheetViewModel @Inject internal constructor(
}
Action.CloseSheet -> {
- bottomSheetEvents.emit(BottomSheetEvents.Event.CloseBottomSheet)
+ appScreenEvents.emit(AppScreenEvents.Event.CloseBottomSheet)
}
is Action.UpdateDate -> {
@@ -79,7 +79,7 @@ class AddWeightBottomSheetViewModel @Inject internal constructor(
healthRepository.saveRecord(it).onFailure {
notifier.notify("Failed to save weight record")
}.onSuccess {
- bottomSheetEvents.emit(BottomSheetEvents.Event.CloseBottomSheet)
+ appScreenEvents.emit(AppScreenEvents.Event.CloseBottomSheet)
}
}
}
diff --git a/app/src/main/kotlin/edu/stanford/bdh/engagehf/messages/HealthSummaryRepository.kt b/app/src/main/kotlin/edu/stanford/bdh/engagehf/messages/HealthSummaryRepository.kt
new file mode 100644
index 000000000..2a0959283
--- /dev/null
+++ b/app/src/main/kotlin/edu/stanford/bdh/engagehf/messages/HealthSummaryRepository.kt
@@ -0,0 +1,35 @@
+package edu.stanford.bdh.engagehf.messages
+
+import com.google.firebase.functions.FirebaseFunctions
+import edu.stanford.spezi.core.coroutines.di.Dispatching
+import edu.stanford.spezi.core.logging.speziLogger
+import edu.stanford.spezi.core.utils.JsonMap
+import edu.stanford.spezi.module.account.manager.UserSessionManager
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.tasks.await
+import kotlinx.coroutines.withContext
+import javax.inject.Inject
+
+class HealthSummaryRepository @Inject constructor(
+ private val userSessionManager: UserSessionManager,
+ private val firebaseFunctions: FirebaseFunctions,
+ @Dispatching.IO private val ioDispatcher: CoroutineDispatcher,
+) {
+ private val logger by speziLogger()
+
+ suspend fun getHealthSummary(): Result = withContext(ioDispatcher) {
+ runCatching {
+ val uid = userSessionManager.getUserUid()
+ ?: error("User not authenticated")
+ val result = firebaseFunctions.getHttpsCallable("exportHealthSummary")
+ .call(mapOf("userId" to uid))
+ .await()
+ val pdfBase64 = (result.data as? JsonMap)?.get("content") as? String
+ ?: error("Invalid function response")
+ val pdfBytes = android.util.Base64.decode(pdfBase64, android.util.Base64.DEFAULT)
+ pdfBytes
+ }.onFailure {
+ logger.e(it) { "Error while fetching health summary" }
+ }
+ }
+}
diff --git a/app/src/main/kotlin/edu/stanford/bdh/engagehf/messages/HealthSummaryService.kt b/app/src/main/kotlin/edu/stanford/bdh/engagehf/messages/HealthSummaryService.kt
new file mode 100644
index 000000000..ed84519b6
--- /dev/null
+++ b/app/src/main/kotlin/edu/stanford/bdh/engagehf/messages/HealthSummaryService.kt
@@ -0,0 +1,64 @@
+package edu.stanford.bdh.engagehf.messages
+
+import android.content.Context
+import android.content.Intent
+import android.os.Environment
+import androidx.core.content.FileProvider
+import dagger.hilt.android.qualifiers.ApplicationContext
+import edu.stanford.spezi.core.coroutines.di.Dispatching
+import edu.stanford.spezi.core.logging.speziLogger
+import edu.stanford.spezi.core.utils.MessageNotifier
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.withContext
+import java.io.File
+import java.io.FileOutputStream
+import javax.inject.Inject
+
+class HealthSummaryService @Inject constructor(
+ private val healthSummaryRepository: HealthSummaryRepository,
+ private val messageNotifier: MessageNotifier,
+ @Dispatching.IO private val ioDispatcher: CoroutineDispatcher,
+ @ApplicationContext private val context: Context,
+) {
+ private val logger by speziLogger()
+
+ suspend fun generateHealthSummaryPdf(): Result = withContext(ioDispatcher) {
+ healthSummaryRepository.getHealthSummary()
+ .mapCatching {
+ val savePdfToFile = savePdfToFile(it)
+ val pdfUri = FileProvider.getUriForFile(
+ context,
+ "${context.packageName}.provider",
+ savePdfToFile
+ )
+ Intent(Intent.ACTION_VIEW).run {
+ addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
+ addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ setDataAndType(pdfUri, MIME_TYPE_PDF)
+ context.startActivity(this)
+ }
+ }.onFailure {
+ messageNotifier.notify("Failed to generate Health Summary")
+ }
+ }
+
+ private fun savePdfToFile(pdfBytes: ByteArray): File {
+ logger.i { "PDF size: ${pdfBytes.size}" }
+ val storageDir =
+ Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
+ if (!storageDir.exists()) {
+ storageDir.mkdirs()
+ }
+ val pdfFile = File(storageDir, FILE_NAME)
+ FileOutputStream(pdfFile).use { fos ->
+ fos.write(pdfBytes)
+ }
+ logger.i { "PDF saved to file: ${pdfFile.absolutePath}" }
+ return pdfFile
+ }
+
+ companion object {
+ const val FILE_NAME = "engage_hf_health_summary.pdf"
+ const val MIME_TYPE_PDF = "application/pdf"
+ }
+}
diff --git a/app/src/main/kotlin/edu/stanford/bdh/engagehf/messages/Message.kt b/app/src/main/kotlin/edu/stanford/bdh/engagehf/messages/Message.kt
index dc3e41687..320bee974 100644
--- a/app/src/main/kotlin/edu/stanford/bdh/engagehf/messages/Message.kt
+++ b/app/src/main/kotlin/edu/stanford/bdh/engagehf/messages/Message.kt
@@ -14,6 +14,7 @@ data class Message(
val title: String,
val description: String,
val action: String,
+ val isLoading: Boolean = false,
val isExpanded: Boolean = false,
) {
diff --git a/app/src/main/kotlin/edu/stanford/bdh/engagehf/messages/MessageItem.kt b/app/src/main/kotlin/edu/stanford/bdh/engagehf/messages/MessageItem.kt
index d8a976765..a6f39977a 100644
--- a/app/src/main/kotlin/edu/stanford/bdh/engagehf/messages/MessageItem.kt
+++ b/app/src/main/kotlin/edu/stanford/bdh/engagehf/messages/MessageItem.kt
@@ -17,6 +17,7 @@ import androidx.compose.material.icons.filled.KeyboardArrowUp
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.CardDefaults
+import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ElevatedCard
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
@@ -137,14 +138,19 @@ fun MessageItem(
Button(
modifier = Modifier.testIdentifier(MessageItemTestIdentifiers.ACTION_BUTTON),
colors = ButtonDefaults.buttonColors(containerColor = primary),
+ enabled = !message.isLoading,
onClick = {
onAction(Action.MessageItemClicked(message))
},
) {
- Text(
- text = stringResource(R.string.message_item_button_action_text),
- color = Colors.onPrimary,
- )
+ if (message.isLoading) {
+ CircularProgressIndicator(modifier = Modifier.size(Sizes.Content.small))
+ } else {
+ Text(
+ text = stringResource(R.string.message_item_button_action_text),
+ color = Colors.onPrimary,
+ )
+ }
}
}
}
diff --git a/app/src/main/kotlin/edu/stanford/bdh/engagehf/navigation/components/AccountDialog.kt b/app/src/main/kotlin/edu/stanford/bdh/engagehf/navigation/components/AccountDialog.kt
new file mode 100644
index 000000000..a65e715e5
--- /dev/null
+++ b/app/src/main/kotlin/edu/stanford/bdh/engagehf/navigation/components/AccountDialog.kt
@@ -0,0 +1,210 @@
+package edu.stanford.bdh.engagehf.navigation.components
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.AccountCircle
+import androidx.compose.material.icons.filled.Close
+import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.material3.HorizontalDivider
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.PreviewParameter
+import androidx.compose.ui.tooling.preview.PreviewParameterProvider
+import androidx.compose.ui.window.Dialog
+import androidx.compose.ui.window.DialogProperties
+import edu.stanford.bdh.engagehf.R
+import edu.stanford.bdh.engagehf.navigation.screens.AccountUiState
+import edu.stanford.bdh.engagehf.navigation.screens.Action
+import edu.stanford.spezi.core.design.component.VerticalSpacer
+import edu.stanford.spezi.core.design.theme.Colors
+import edu.stanford.spezi.core.design.theme.Colors.onPrimary
+import edu.stanford.spezi.core.design.theme.Colors.primary
+import edu.stanford.spezi.core.design.theme.Colors.surface
+import edu.stanford.spezi.core.design.theme.Sizes
+import edu.stanford.spezi.core.design.theme.Spacings
+import edu.stanford.spezi.core.design.theme.SpeziTheme
+import edu.stanford.spezi.core.design.theme.TextStyles
+import edu.stanford.spezi.core.design.theme.TextStyles.bodyMedium
+import edu.stanford.spezi.core.design.theme.TextStyles.headlineMedium
+import edu.stanford.spezi.core.design.theme.ThemePreviews
+import edu.stanford.spezi.core.design.theme.lighten
+
+@Composable
+fun AccountDialog(accountUiState: AccountUiState, onAction: (Action) -> Unit) {
+ Dialog(
+ onDismissRequest = {
+ if (!accountUiState.isHealthSummaryLoading) {
+ onAction(Action.ShowAccountDialog(false))
+ }
+ },
+ properties = DialogProperties()
+ ) {
+ Surface(
+ shape = MaterialTheme.shapes.medium,
+ color = surface.lighten(),
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(Spacings.medium)
+ ) {
+ Column(
+ modifier = Modifier.padding(Spacings.medium),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ Row {
+ Box(
+ modifier = Modifier.fillMaxWidth(),
+ contentAlignment = Alignment.TopStart
+ ) {
+ IconButton(
+ enabled = !accountUiState.isHealthSummaryLoading,
+ onClick = { onAction(Action.ShowAccountDialog(false)) },
+ modifier = Modifier.align(Alignment.CenterStart)
+ ) {
+ Icon(
+ imageVector = Icons.Default.Close,
+ contentDescription = stringResource(R.string.close_dialog_content_description),
+ tint = MaterialTheme.colorScheme.primary,
+ modifier = Modifier.size(Sizes.Icon.small)
+ )
+ }
+ Text(
+ text = stringResource(R.string.account), style = TextStyles.titleMedium,
+ modifier = Modifier.align(
+ Alignment.Center
+ )
+ )
+ }
+ }
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ accountUiState.initials?.let {
+ Box(
+ contentAlignment = Alignment.Center,
+ modifier = Modifier
+ .size(Sizes.Icon.large)
+ .background(primary, shape = CircleShape)
+ ) {
+ Text(
+ text = accountUiState.initials,
+ style = headlineMedium,
+ color = onPrimary,
+ )
+ }
+ }
+
+ if (accountUiState.initials == null) {
+ Icon(
+ imageVector = Icons.Default.AccountCircle,
+ contentDescription = null,
+ modifier = Modifier
+ .size(Sizes.Icon.large),
+ tint = primary
+ )
+ }
+ Spacer(modifier = Modifier.width(Spacings.medium))
+ Column {
+ VerticalSpacer()
+ accountUiState.name?.let {
+ Text(text = it, style = headlineMedium)
+ VerticalSpacer(height = Spacings.small)
+ }
+ Text(text = accountUiState.email, style = bodyMedium)
+ VerticalSpacer()
+ }
+ }
+
+ HorizontalDivider()
+ TextButton(
+ onClick = {
+ onAction(Action.ShowHealthSummary)
+ },
+ modifier = Modifier
+ .align(Alignment.Start),
+ ) {
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier.fillMaxWidth()
+ ) {
+ Text(
+ text = stringResource(R.string.health_summary),
+ style = bodyMedium,
+ modifier = Modifier.weight(1f)
+ )
+ if (accountUiState.isHealthSummaryLoading) {
+ CircularProgressIndicator(
+ modifier = Modifier.size(Sizes.Icon.small),
+ color = primary
+ )
+ }
+ }
+ }
+ HorizontalDivider()
+ VerticalSpacer()
+ TextButton(
+ onClick = { onAction(Action.SignOut) },
+ modifier = Modifier
+ .align(Alignment.Start),
+ ) {
+ Text(
+ text = stringResource(R.string.sign_out),
+ style = bodyMedium,
+ color = Colors.error,
+ modifier = Modifier.fillMaxWidth()
+ )
+ }
+ }
+ }
+ }
+}
+
+class AppTopBarProvider : PreviewParameterProvider {
+ override val values: Sequence = sequenceOf(
+ AccountUiState(
+ initials = "JD",
+ isHealthSummaryLoading = false,
+ name = "John Doe",
+ email = "john@doe.de"
+ ),
+ AccountUiState(
+ name = "John Doe",
+ email = "",
+ isHealthSummaryLoading = true
+ ),
+ AccountUiState(
+ name = null,
+ email = "john@doe.de"
+ )
+ )
+}
+
+@ThemePreviews
+@Composable
+fun AccountDialogPreview(
+ @PreviewParameter(AppTopBarProvider::class) accountUiState: AccountUiState,
+) {
+ SpeziTheme {
+ AccountDialog(
+ accountUiState = accountUiState,
+ onAction = {}
+ )
+ }
+}
diff --git a/app/src/main/kotlin/edu/stanford/bdh/engagehf/navigation/components/AccountTopAppBarButton.kt b/app/src/main/kotlin/edu/stanford/bdh/engagehf/navigation/components/AccountTopAppBarButton.kt
new file mode 100644
index 000000000..533390c9c
--- /dev/null
+++ b/app/src/main/kotlin/edu/stanford/bdh/engagehf/navigation/components/AccountTopAppBarButton.kt
@@ -0,0 +1,37 @@
+package edu.stanford.bdh.engagehf.navigation.components
+
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
+import androidx.compose.foundation.layout.size
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.AccountCircle
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import edu.stanford.bdh.engagehf.navigation.screens.AccountUiState
+import edu.stanford.bdh.engagehf.navigation.screens.Action
+import edu.stanford.spezi.core.design.theme.Colors
+import edu.stanford.spezi.core.design.theme.Sizes
+
+@Composable
+fun AccountTopAppBarButton(accountUiState: AccountUiState, onAction: (Action) -> Unit) {
+ IconButton(onClick = {
+ onAction(Action.ShowAccountDialog(true))
+ }) {
+ Icon(
+ imageVector = Icons.Default.AccountCircle,
+ contentDescription = "Account",
+ tint = Colors.onPrimary,
+ modifier = Modifier.size(Sizes.Icon.medium)
+ )
+ }
+ AnimatedVisibility(
+ visible = accountUiState.showDialog,
+ enter = fadeIn(),
+ exit = fadeOut()
+ ) {
+ AccountDialog(accountUiState = accountUiState, onAction = onAction)
+ }
+}
diff --git a/app/src/main/kotlin/edu/stanford/bdh/engagehf/navigation/screens/AppScreen.kt b/app/src/main/kotlin/edu/stanford/bdh/engagehf/navigation/screens/AppScreen.kt
index d1885ae75..82cb5529e 100644
--- a/app/src/main/kotlin/edu/stanford/bdh/engagehf/navigation/screens/AppScreen.kt
+++ b/app/src/main/kotlin/edu/stanford/bdh/engagehf/navigation/screens/AppScreen.kt
@@ -1,6 +1,8 @@
package edu.stanford.bdh.engagehf.navigation.screens
import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.BottomSheetScaffold
import androidx.compose.material3.BottomSheetScaffoldState
@@ -19,6 +21,7 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.snapshotFlow
+import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
@@ -35,9 +38,13 @@ import edu.stanford.bdh.engagehf.health.heartrate.bottomsheet.HeartRateDescripti
import edu.stanford.bdh.engagehf.health.weight.bottomsheet.AddWeightBottomSheet
import edu.stanford.bdh.engagehf.health.weight.bottomsheet.WeightDescriptionBottomSheet
import edu.stanford.bdh.engagehf.medication.ui.MedicationScreen
+import edu.stanford.bdh.engagehf.navigation.components.AccountTopAppBarButton
import edu.stanford.spezi.core.design.component.AppTopAppBar
+import edu.stanford.spezi.core.design.theme.Spacings
import edu.stanford.spezi.core.utils.extensions.testIdentifier
import edu.stanford.spezi.modules.education.videos.EducationScreen
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.launch
@Composable
@@ -60,20 +67,21 @@ fun AppScreen(
bottomSheetState = rememberModalBottomSheetState()
)
- LaunchedEffect(key1 = uiState.isBottomSheetExpanded) {
+ LaunchedEffect(key1 = uiState.bottomSheetContent) {
launch {
- if (uiState.isBottomSheetExpanded) {
+ if (uiState.bottomSheetContent != null) {
bottomSheetScaffoldState.bottomSheetState.expand()
} else {
bottomSheetScaffoldState.bottomSheetState.hide()
}
}
}
-
LaunchedEffect(bottomSheetScaffoldState.bottomSheetState) {
snapshotFlow { bottomSheetScaffoldState.bottomSheetState.currentValue }
- .collect { state ->
- onAction(Action.UpdateBottomSheetState(isExpanded = state == SheetValue.Expanded))
+ .filter { it == SheetValue.Hidden }
+ .distinctUntilChanged()
+ .collect {
+ onAction(Action.DismissBottomSheet)
}
}
@@ -104,13 +112,24 @@ fun BottomSheetScaffoldContent(
AppTopAppBar(
modifier = Modifier.testIdentifier(identifier = AppScreenTestIdentifier.TOP_APP_BAR),
title = {
- Text(
- text = stringResource(id = uiState.selectedItem.label),
- modifier = Modifier.testIdentifier(
- AppScreenTestIdentifier.TOP_APP_BAR_TITLE
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(end = Spacings.small),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Text(
+ text = stringResource(id = uiState.selectedItem.label),
+ modifier = Modifier.testIdentifier(
+ AppScreenTestIdentifier.TOP_APP_BAR_TITLE
+ )
)
- )
- })
+ }
+ },
+ actions = {
+ AccountTopAppBarButton(uiState.accountUiState, onAction = onAction)
+ }
+ )
},
bottomBar = {
Column {
@@ -128,7 +147,13 @@ fun BottomSheetScaffoldContent(
),
icon = {
Icon(
- painter = painterResource(id = if (uiState.selectedItem == item) item.selectedIcon else item.icon),
+ painter = painterResource(
+ id = if (uiState.selectedItem == item) {
+ item.selectedIcon
+ } else {
+ item.icon
+ }
+ ),
contentDescription = null
)
},
diff --git a/app/src/main/kotlin/edu/stanford/bdh/engagehf/navigation/screens/AppScreenViewModel.kt b/app/src/main/kotlin/edu/stanford/bdh/engagehf/navigation/screens/AppScreenViewModel.kt
index da0cfda65..4aa52e35c 100644
--- a/app/src/main/kotlin/edu/stanford/bdh/engagehf/navigation/screens/AppScreenViewModel.kt
+++ b/app/src/main/kotlin/edu/stanford/bdh/engagehf/navigation/screens/AppScreenViewModel.kt
@@ -6,7 +6,9 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import edu.stanford.bdh.engagehf.R
-import edu.stanford.bdh.engagehf.bluetooth.component.BottomSheetEvents
+import edu.stanford.bdh.engagehf.bluetooth.component.AppScreenEvents
+import edu.stanford.bdh.engagehf.messages.HealthSummaryService
+import edu.stanford.spezi.module.account.manager.UserSessionManager
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
@@ -16,7 +18,9 @@ import edu.stanford.spezi.core.design.R.drawable as DesignR
@HiltViewModel
class AppScreenViewModel @Inject constructor(
- private val bottomSheetEvents: BottomSheetEvents,
+ private val appScreenEvents: AppScreenEvents,
+ private val userSessionManager: UserSessionManager,
+ private val healthSummaryService: HealthSummaryService,
) : ViewModel() {
private val _uiState = MutableStateFlow(
AppUiState(
@@ -33,46 +37,42 @@ class AppScreenViewModel @Inject constructor(
private fun setup() {
viewModelScope.launch {
- bottomSheetEvents.events.collect { event ->
- val (isExpanded, content) = when (event) {
- BottomSheetEvents.Event.NewMeasurementAction -> {
- true to BottomSheetContent.NEW_MEASUREMENT_RECEIVED
- }
+ _uiState.update { uiState ->
+ val userInfo = userSessionManager.getUserInfo()
+ uiState.copy(
+ accountUiState = uiState.accountUiState.copy(
+ email = userInfo.email,
+ name = userInfo.name,
+ initials = userInfo.name?.split(" ")
+ ?.mapNotNull { it.firstOrNull()?.toString() }?.joinToString(""),
+ )
+ )
+ }
- BottomSheetEvents.Event.DoNewMeasurement -> {
- true to BottomSheetContent.DO_NEW_MEASUREMENT
+ appScreenEvents.events.collect { event ->
+ if (event is AppScreenEvents.Event.NavigateToTab) {
+ _uiState.update {
+ it.copy(selectedItem = event.bottomBarItem)
}
+ } else {
+ val bottomSheetContent = when (event) {
+ AppScreenEvents.Event.NewMeasurementAction ->
+ BottomSheetContent.NEW_MEASUREMENT_RECEIVED
- BottomSheetEvents.Event.CloseBottomSheet -> {
- false to null
- }
+ AppScreenEvents.Event.DoNewMeasurement ->
+ BottomSheetContent.DO_NEW_MEASUREMENT
- BottomSheetEvents.Event.WeightDescriptionBottomSheet -> {
- true to BottomSheetContent.WEIGHT_DESCRIPTION_INFO
- }
+ AppScreenEvents.Event.WeightDescriptionBottomSheet ->
+ BottomSheetContent.WEIGHT_DESCRIPTION_INFO
- BottomSheetEvents.Event.AddWeightRecord -> {
- true to BottomSheetContent.ADD_WEIGHT_RECORD
- }
+ AppScreenEvents.Event.AddWeightRecord ->
+ BottomSheetContent.ADD_WEIGHT_RECORD
- BottomSheetEvents.Event.AddBloodPressureRecord -> {
- true to BottomSheetContent.ADD_BLOOD_PRESSURE_RECORD
+ else -> null
}
-
- BottomSheetEvents.Event.AddHeartRateRecord -> {
- true to BottomSheetContent.ADD_HEART_RATE_RECORD
+ _uiState.update {
+ it.copy(bottomSheetContent = bottomSheetContent)
}
-
- BottomSheetEvents.Event.BloodPressureDescriptionBottomSheet -> {
- true to BottomSheetContent.BLOOD_PRESSURE_DESCRIPTION_INFO
- }
-
- BottomSheetEvents.Event.HeartRateDescriptionBottomSheet -> {
- true to BottomSheetContent.HEART_RATE_DESCRIPTION_INFO
- }
- }
- _uiState.update {
- it.copy(isBottomSheetExpanded = isExpanded, bottomSheetContent = content)
}
}
}
@@ -84,8 +84,38 @@ class AppScreenViewModel @Inject constructor(
_uiState.update { it.copy(selectedItem = action.selectedBottomBarItem) }
}
- is Action.UpdateBottomSheetState -> {
- _uiState.update { it.copy(isBottomSheetExpanded = action.isExpanded) }
+ is Action.ShowAccountDialog -> {
+ _uiState.update { it.copy(accountUiState = it.accountUiState.copy(showDialog = action.showDialog)) }
+ }
+
+ Action.ShowHealthSummary -> {
+ viewModelScope.launch {
+ _uiState.update {
+ it.copy(
+ accountUiState = it.accountUiState.copy(
+ isHealthSummaryLoading = true
+ )
+ )
+ }
+ healthSummaryService.generateHealthSummaryPdf()
+ _uiState.update {
+ it.copy(
+ accountUiState = it.accountUiState.copy(
+ isHealthSummaryLoading = false,
+ showDialog = false
+ )
+ )
+ }
+ }
+ }
+
+ Action.SignOut -> {
+ _uiState.update { it.copy(accountUiState = it.accountUiState.copy(showDialog = false)) }
+ /* TODO: Implement sign out */
+ }
+
+ Action.DismissBottomSheet -> {
+ _uiState.update { it.copy(bottomSheetContent = null) }
}
}
}
@@ -94,8 +124,16 @@ class AppScreenViewModel @Inject constructor(
data class AppUiState(
val items: List,
val selectedItem: BottomBarItem,
- val isBottomSheetExpanded: Boolean = false,
val bottomSheetContent: BottomSheetContent? = null,
+ val accountUiState: AccountUiState = AccountUiState(),
+)
+
+data class AccountUiState(
+ val showDialog: Boolean = false,
+ val email: String = "",
+ val name: String? = null,
+ val initials: String? = null,
+ val isHealthSummaryLoading: Boolean = false,
)
enum class BottomSheetContent {
@@ -111,7 +149,10 @@ enum class BottomSheetContent {
sealed interface Action {
data class UpdateSelectedBottomBarItem(val selectedBottomBarItem: BottomBarItem) : Action
- data class UpdateBottomSheetState(val isExpanded: Boolean) : Action
+ data object ShowHealthSummary : Action
+ data object SignOut : Action
+ data class ShowAccountDialog(val showDialog: Boolean) : Action
+ data object DismissBottomSheet : Action
}
enum class BottomBarItem(
diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml
index 38941c7ea..ef357dc0a 100644
--- a/app/src/main/res/values-de/strings.xml
+++ b/app/src/main/res/values-de/strings.xml
@@ -21,7 +21,7 @@
Keine Empfehlungen für Medikamente
Aktuelle Dosis:
Ziel Dosis:
- K.A.
+ k.A.
Aktuell
Ziel
Täglich
@@ -58,4 +58,8 @@
Blutdruck
ANGABEN ZUR MESSUNG
Herzfrequenz
+ Dialog schließen
+ Ausloggen
+ Gesundheitszusammenfassung
+ Account
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 200abe983..5adb02aac 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -37,6 +37,10 @@
Weight
BP
Heart Rate
+ Close dialog
+ Sign Out
+ Health Summary
+ Account
Cancel
OK
Time
diff --git a/app/src/main/res/xml/file_paths.xml b/app/src/main/res/xml/file_paths.xml
new file mode 100644
index 000000000..a075ef96b
--- /dev/null
+++ b/app/src/main/res/xml/file_paths.xml
@@ -0,0 +1,6 @@
+
+
+
+
diff --git a/app/src/test/kotlin/edu/stanford/bdh/engagehf/bluetooth/BluetoothViewModelTest.kt b/app/src/test/kotlin/edu/stanford/bdh/engagehf/bluetooth/BluetoothViewModelTest.kt
index 196d7ff33..a93d8f389 100644
--- a/app/src/test/kotlin/edu/stanford/bdh/engagehf/bluetooth/BluetoothViewModelTest.kt
+++ b/app/src/test/kotlin/edu/stanford/bdh/engagehf/bluetooth/BluetoothViewModelTest.kt
@@ -4,7 +4,7 @@ import androidx.health.connect.client.records.BloodPressureRecord
import androidx.health.connect.client.records.HeartRateRecord
import androidx.health.connect.client.records.WeightRecord
import com.google.common.truth.Truth.assertThat
-import edu.stanford.bdh.engagehf.bluetooth.component.BottomSheetEvents
+import edu.stanford.bdh.engagehf.bluetooth.component.AppScreenEvents
import edu.stanford.bdh.engagehf.bluetooth.data.mapper.BluetoothUiStateMapper
import edu.stanford.bdh.engagehf.bluetooth.data.models.Action
import edu.stanford.bdh.engagehf.bluetooth.data.models.BluetoothUiState
@@ -12,12 +12,14 @@ import edu.stanford.bdh.engagehf.bluetooth.data.models.MeasurementDialogUiState
import edu.stanford.bdh.engagehf.bluetooth.data.models.VitalDisplayData
import edu.stanford.bdh.engagehf.bluetooth.measurements.MeasurementsRepository
import edu.stanford.bdh.engagehf.education.EngageEducationRepository
+import edu.stanford.bdh.engagehf.messages.HealthSummaryService
import edu.stanford.bdh.engagehf.messages.Message
import edu.stanford.bdh.engagehf.messages.MessageRepository
import edu.stanford.bdh.engagehf.messages.MessageType
import edu.stanford.bdh.engagehf.messages.MessagesAction
import edu.stanford.bdh.engagehf.messages.VideoSectionVideo
import edu.stanford.bdh.engagehf.navigation.AppNavigationEvent
+import edu.stanford.bdh.engagehf.navigation.screens.BottomBarItem
import edu.stanford.spezi.core.bluetooth.api.BLEService
import edu.stanford.spezi.core.bluetooth.data.model.BLEServiceEvent
import edu.stanford.spezi.core.bluetooth.data.model.BLEServiceState
@@ -49,11 +51,12 @@ class BluetoothViewModelTest {
private val measurementsRepository = mockk(relaxed = true)
private val messageRepository = mockk(relaxed = true)
private val engageEducationRepository = mockk(relaxed = true)
+ private val healthSummaryService = mockk(relaxed = true)
private val bleServiceState = MutableStateFlow(BLEServiceState.Idle)
private val bleServiceEvents = MutableSharedFlow()
private val readyUiState: BluetoothUiState.Ready = mockk()
- private val bottomSheetEvents = mockk(relaxed = true)
+ private val appScreenEvents = mockk(relaxed = true)
private val navigator = mockk(relaxed = true)
private val messageAction = "some-action"
private val messageId = "some-id"
@@ -241,7 +244,7 @@ class BluetoothViewModelTest {
bleServiceEvents.emit(event)
// then
- verify { bottomSheetEvents.emit(BottomSheetEvents.Event.CloseBottomSheet) }
+ verify { appScreenEvents.emit(AppScreenEvents.Event.CloseBottomSheet) }
assertBluetothUiState(state = BluetoothUiState.Idle)
assertThat(bluetoothViewModel.uiState.value.measurementDialog).isEqualTo(
measurementDialog
@@ -387,28 +390,6 @@ class BluetoothViewModelTest {
coVerify { messageRepository.completeMessage(messageId = messageId) }
}
- @Test
- fun `it should do nothing on MessageItemClicked with for TODO actions`() {
- // given
- val action = Action.MessageItemClicked(message = message)
- val todoActions = listOf(
- MessagesAction.HealthSummaryAction,
- MessagesAction.MedicationsAction,
- )
- createViewModel()
- val initialState = bluetoothViewModel.uiState.value
-
- todoActions.forEach {
- every { uiStateMapper.mapMessagesAction(messageAction) } returns Result.success(it)
-
- // when
- bluetoothViewModel.onAction(action = action)
-
- // then
- assertThat(bluetoothViewModel.uiState.value).isEqualTo(initialState)
- }
- }
-
@Test
fun `it should handle MeasurementsAction correctly`() {
// given
@@ -422,7 +403,7 @@ class BluetoothViewModelTest {
bluetoothViewModel.onAction(action = action)
// then
- verify { bottomSheetEvents.emit(BottomSheetEvents.Event.DoNewMeasurement) }
+ verify { appScreenEvents.emit(AppScreenEvents.Event.DoNewMeasurement) }
}
@Test
@@ -468,6 +449,38 @@ class BluetoothViewModelTest {
verify { navigator.navigateTo(EducationNavigationEvent.VideoSectionClicked(video)) }
}
+ @Test
+ fun `it should handle health summary action correctly`() = runTestUnconfined {
+ // given
+ val action = Action.MessageItemClicked(message = message)
+ every {
+ uiStateMapper.mapMessagesAction(messageAction)
+ } returns Result.success(MessagesAction.HealthSummaryAction)
+ createViewModel()
+
+ // when
+ bluetoothViewModel.onAction(action = action)
+
+ // then
+ coVerify { healthSummaryService.generateHealthSummaryPdf() }
+ }
+
+ @Test
+ fun `it should handle medication change action correctly`() {
+ // given
+ val action = Action.MessageItemClicked(message = message)
+ every {
+ uiStateMapper.mapMessagesAction(messageAction)
+ } returns Result.success(MessagesAction.MedicationsAction)
+ createViewModel()
+
+ // when
+ bluetoothViewModel.onAction(action = action)
+
+ // then
+ verify { appScreenEvents.emit(AppScreenEvents.Event.NavigateToTab(BottomBarItem.MEDICATION)) }
+ }
+
@Test
fun `it should handle toggle expand action correctly`() {
// given
@@ -514,9 +527,10 @@ class BluetoothViewModelTest {
uiStateMapper = uiStateMapper,
measurementsRepository = measurementsRepository,
messageRepository = messageRepository,
- bottomSheetEvents = bottomSheetEvents,
+ appScreenEvents = appScreenEvents,
navigator = navigator,
engageEducationRepository = engageEducationRepository,
+ healthSummaryService = healthSummaryService
)
}
}
diff --git a/app/src/test/kotlin/edu/stanford/bdh/engagehf/education/VideoSectionDocumentToVideoSectionMapperTest.kt b/app/src/test/kotlin/edu/stanford/bdh/engagehf/education/VideoSectionDocumentToVideoSectionMapperTest.kt
index 5ad6da20f..cb3b7fa0a 100644
--- a/app/src/test/kotlin/edu/stanford/bdh/engagehf/education/VideoSectionDocumentToVideoSectionMapperTest.kt
+++ b/app/src/test/kotlin/edu/stanford/bdh/engagehf/education/VideoSectionDocumentToVideoSectionMapperTest.kt
@@ -4,7 +4,6 @@ import com.google.common.truth.Truth.assertThat
import com.google.firebase.firestore.CollectionReference
import com.google.firebase.firestore.DocumentSnapshot
import edu.stanford.bdh.engagehf.localization.LocalizedMapReader
-import edu.stanford.spezi.core.utils.JsonMap
import io.mockk.every
import io.mockk.mockk
import kotlinx.coroutines.test.runTest
@@ -33,9 +32,9 @@ class VideoSectionDocumentToVideoSectionMapperTest {
@Test
fun `it should return null if no videos found`() = runTest {
// given
- val jsonMap: JsonMap = mockk()
+ val jsonData = mockk