From d6beb58705c09eac7b2e52aad86c21f25430d224 Mon Sep 17 00:00:00 2001 From: Basler182 Date: Thu, 15 Aug 2024 14:40:44 +0200 Subject: [PATCH 01/31] renamed to AppScreenEvents Signed-off-by: Basler182 --- ...ottomSheetEvents.kt => AppScreenEvents.kt} | 4 ++- .../bdh/engagehf/health/HealthViewModel.kt | 12 ++++---- .../engagehf/health/weight/WeightViewModel.kt | 8 ++--- .../AddWeightBottomSheetViewModel.kt | 6 ++-- .../navigation/screens/AppScreenViewModel.kt | 29 ++++++++++++------- 5 files changed, 35 insertions(+), 24 deletions(-) rename app/src/main/kotlin/edu/stanford/bdh/engagehf/bluetooth/component/{BottomSheetEvents.kt => AppScreenEvents.kt} (84%) 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 84% 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 8da9bfd3c..0423cdb3e 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) @@ -28,5 +29,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 0b2280c63..cd455bdb2 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/weight/WeightViewModel.kt b/app/src/main/kotlin/edu/stanford/bdh/engagehf/health/weight/WeightViewModel.kt index af1dd6acf..75904028e 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() { @@ -56,7 +56,7 @@ class WeightViewModel @Inject internal constructor( fun onAction(healthAction: HealthAction) { when (healthAction) { HealthAction.AddRecord -> { - bottomSheetEvents.emit(BottomSheetEvents.Event.AddWeightRecord) + appScreenEvents.emit(AppScreenEvents.Event.AddWeightRecord) } is HealthAction.DeleteRecord -> { @@ -70,7 +70,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 7c29d67bb..6455f4468 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 @@ -2,7 +2,7 @@ package edu.stanford.bdh.engagehf.health.weight.bottomsheet 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 edu.stanford.spezi.core.logging.speziLogger import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow @@ -14,7 +14,7 @@ import javax.inject.Inject @HiltViewModel class AddWeightBottomSheetViewModel @Inject internal constructor( - private val bottomSheetEvents: BottomSheetEvents, + private val appScreenEvents: AppScreenEvents, private val uiStateMapper: AddWeightBottomSheetUiStateMapper, ) : ViewModel() { private val logger by speziLogger() @@ -33,7 +33,7 @@ class AddWeightBottomSheetViewModel @Inject internal constructor( } Action.SaveWeight -> { - bottomSheetEvents.emit(BottomSheetEvents.Event.CloseBottomSheet) + appScreenEvents.emit(AppScreenEvents.Event.CloseBottomSheet) _uiState.update { uiStateMapper.mapSaveWeightActionToUiState(_uiState.value) } logger.i { "Save weight: ${uiState.value}" } } 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 4f8bb2e63..95b364467 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,7 @@ 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 kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update @@ -16,7 +16,7 @@ import edu.stanford.spezi.core.design.R.drawable as DesignR @HiltViewModel class AppScreenViewModel @Inject constructor( - private val bottomSheetEvents: BottomSheetEvents, + private val appScreenEvents: AppScreenEvents, ) : ViewModel() { private val _uiState = MutableStateFlow( AppUiState( @@ -33,32 +33,40 @@ class AppScreenViewModel @Inject constructor( private fun setup() { viewModelScope.launch { - bottomSheetEvents.events.collect { event -> + appScreenEvents.events.collect { event -> val (isExpanded, content) = when (event) { - BottomSheetEvents.Event.NewMeasurementAction -> { + is AppScreenEvents.Event.NavigateToTab -> { + _uiState.update { + it.copy(selectedItem = event.bottomBarItem) + } + return@collect + } + + AppScreenEvents.Event.NewMeasurementAction -> { true to BottomSheetContent.NEW_MEASUREMENT_RECEIVED } - BottomSheetEvents.Event.DoNewMeasurement -> { + AppScreenEvents.Event.DoNewMeasurement -> { true to BottomSheetContent.DO_NEW_MEASUREMENT } - BottomSheetEvents.Event.CloseBottomSheet -> { + AppScreenEvents.Event.CloseBottomSheet -> { false to null } - BottomSheetEvents.Event.WeightDescriptionBottomSheet -> { + AppScreenEvents.Event.WeightDescriptionBottomSheet -> { true to BottomSheetContent.WEIGHT_DESCRIPTION_INFO } - BottomSheetEvents.Event.AddWeightRecord -> { + AppScreenEvents.Event.AddWeightRecord -> { true to BottomSheetContent.ADD_WEIGHT_RECORD } - BottomSheetEvents.Event.AddBloodPressureRecord -> { + AppScreenEvents.Event.AddBloodPressureRecord -> { false to null } - BottomSheetEvents.Event.AddHeartRateRecord -> { + + AppScreenEvents.Event.AddHeartRateRecord -> { false to null } } @@ -74,6 +82,7 @@ class AppScreenViewModel @Inject constructor( is Action.UpdateSelectedBottomBarItem -> { _uiState.update { it.copy(selectedItem = action.selectedBottomBarItem) } } + is Action.UpdateBottomSheetState -> { _uiState.update { it.copy(isBottomSheetExpanded = action.isExpanded) } } From 5335d6a8ceb9fd765923178ceb733e69bb6952d3 Mon Sep 17 00:00:00 2001 From: Basler182 Date: Thu, 15 Aug 2024 14:46:51 +0200 Subject: [PATCH 02/31] added HealthSummaryRepository Signed-off-by: Basler182 --- .../messages/HealthSummaryRepository.kt | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 app/src/main/kotlin/edu/stanford/bdh/engagehf/messages/HealthSummaryRepository.kt 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..94abc68a5 --- /dev/null +++ b/app/src/main/kotlin/edu/stanford/bdh/engagehf/messages/HealthSummaryRepository.kt @@ -0,0 +1,36 @@ +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.module.account.manager.UserSessionManager +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.tasks.await +import kotlinx.coroutines.withContext +import javax.inject.Inject + +internal 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 findHealthSummaryByUserId(): Result = withContext(ioDispatcher) { + runCatching { + val uid = userSessionManager.getUserUid() + ?: throw IllegalStateException("User not authenticated") + val result = firebaseFunctions.getHttpsCallable("exportHealthSummary") + .call(mapOf("userId" to uid)) + .await() + val resultData = result.data as? Map<*, *> + ?: throw IllegalStateException("Invalid function response") + val pdfBase64 = resultData["content"] as? String + ?: throw IllegalStateException("No content found in function response") + val pdfBytes = android.util.Base64.decode(pdfBase64, android.util.Base64.DEFAULT) + pdfBytes + }.onFailure { + logger.e(it) { "Error while fetching health summary" } + } + } +} From d4da4a6210c4eefeb2aedf47376d8c91e11f0cf9 Mon Sep 17 00:00:00 2001 From: Basler182 Date: Thu, 15 Aug 2024 15:13:51 +0200 Subject: [PATCH 03/31] added file provider and file paths Signed-off-by: Basler182 --- app/src/main/AndroidManifest.xml | 9 +++++++++ app/src/main/res/xml/file_paths.xml | 6 ++++++ 2 files changed, 15 insertions(+) create mode 100644 app/src/main/res/xml/file_paths.xml 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/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 @@ + + + + From 6b288ddaca662a51ffa853870c1de192909540de Mon Sep 17 00:00:00 2001 From: Basler182 Date: Thu, 15 Aug 2024 15:14:41 +0200 Subject: [PATCH 04/31] finish rename tests Signed-off-by: Basler182 --- .../engagehf/health/HealthViewModelTest.kt | 14 ++++----- .../AddWeightBottomSheetViewModelTest.kt | 8 ++--- .../screens/AppScreenViewModelTest.kt | 30 +++++++++---------- 3 files changed, 26 insertions(+), 26 deletions(-) diff --git a/app/src/test/kotlin/edu/stanford/bdh/engagehf/health/HealthViewModelTest.kt b/app/src/test/kotlin/edu/stanford/bdh/engagehf/health/HealthViewModelTest.kt index 33910a456..96bb74c0d 100644 --- a/app/src/test/kotlin/edu/stanford/bdh/engagehf/health/HealthViewModelTest.kt +++ b/app/src/test/kotlin/edu/stanford/bdh/engagehf/health/HealthViewModelTest.kt @@ -1,7 +1,7 @@ package edu.stanford.bdh.engagehf.health 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.spezi.core.testing.verifyNever import io.mockk.mockk import io.mockk.verify @@ -9,10 +9,10 @@ import org.junit.Test class HealthViewModelTest { - private val bottomSheetEvents: BottomSheetEvents = mockk(relaxed = true) + private val appScreenEvents: AppScreenEvents = mockk(relaxed = true) private val tabs = HealthTab.entries.filter { it != HealthTab.Symptoms } - private val viewModel: HealthViewModel = HealthViewModel(bottomSheetEvents) + private val viewModel: HealthViewModel = HealthViewModel(appScreenEvents) @Test fun `it should have the correct initial state`() { @@ -50,7 +50,7 @@ class HealthViewModelTest { viewModel.onAction(action) // then - verify { bottomSheetEvents.emit(BottomSheetEvents.Event.AddWeightRecord) } + verify { appScreenEvents.emit(AppScreenEvents.Event.AddWeightRecord) } } @Test @@ -62,7 +62,7 @@ class HealthViewModelTest { viewModel.onAction(action) // then - verify { bottomSheetEvents.emit(BottomSheetEvents.Event.AddBloodPressureRecord) } + verify { appScreenEvents.emit(AppScreenEvents.Event.AddBloodPressureRecord) } } @Test @@ -74,7 +74,7 @@ class HealthViewModelTest { viewModel.onAction(action) // then - verify { bottomSheetEvents.emit(BottomSheetEvents.Event.AddHeartRateRecord) } + verify { appScreenEvents.emit(AppScreenEvents.Event.AddHeartRateRecord) } } @Test @@ -86,6 +86,6 @@ class HealthViewModelTest { viewModel.onAction(action) // then - verifyNever { bottomSheetEvents.emit(any()) } + verifyNever { appScreenEvents.emit(any()) } } } diff --git a/app/src/test/kotlin/edu/stanford/bdh/engagehf/health/weight/bottomsheet/AddWeightBottomSheetViewModelTest.kt b/app/src/test/kotlin/edu/stanford/bdh/engagehf/health/weight/bottomsheet/AddWeightBottomSheetViewModelTest.kt index 78a9dca24..013419fea 100644 --- a/app/src/test/kotlin/edu/stanford/bdh/engagehf/health/weight/bottomsheet/AddWeightBottomSheetViewModelTest.kt +++ b/app/src/test/kotlin/edu/stanford/bdh/engagehf/health/weight/bottomsheet/AddWeightBottomSheetViewModelTest.kt @@ -1,7 +1,7 @@ package edu.stanford.bdh.engagehf.health.weight.bottomsheet 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.spezi.core.testing.CoroutineTestRule import edu.stanford.spezi.core.testing.runTestUnconfined import io.mockk.coVerify @@ -15,10 +15,10 @@ class AddWeightBottomSheetViewModelTest { @get:Rule val coroutineTestRule = CoroutineTestRule() - private var bottomSheetEvents: BottomSheetEvents = mockk(relaxed = true) + private var appScreenEvents: AppScreenEvents = mockk(relaxed = true) private var viewModel: AddWeightBottomSheetViewModel = AddWeightBottomSheetViewModel( - bottomSheetEvents, + appScreenEvents, AddWeightBottomSheetUiStateMapper() ) @@ -86,7 +86,7 @@ class AddWeightBottomSheetViewModelTest { viewModel.onAction(AddWeightBottomSheetViewModel.Action.SaveWeight) // Then - coVerify { bottomSheetEvents.emit(BottomSheetEvents.Event.CloseBottomSheet) } + coVerify { appScreenEvents.emit(AppScreenEvents.Event.CloseBottomSheet) } } @Test diff --git a/app/src/test/kotlin/edu/stanford/bdh/engagehf/navigation/screens/AppScreenViewModelTest.kt b/app/src/test/kotlin/edu/stanford/bdh/engagehf/navigation/screens/AppScreenViewModelTest.kt index f40e6c51f..ad8bdbfaf 100644 --- a/app/src/test/kotlin/edu/stanford/bdh/engagehf/navigation/screens/AppScreenViewModelTest.kt +++ b/app/src/test/kotlin/edu/stanford/bdh/engagehf/navigation/screens/AppScreenViewModelTest.kt @@ -1,7 +1,7 @@ package edu.stanford.bdh.engagehf.navigation.screens 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.spezi.core.testing.CoroutineTestRule import edu.stanford.spezi.core.testing.runTestUnconfined import io.mockk.every @@ -16,16 +16,16 @@ class AppScreenViewModelTest { @get:Rule val coroutineTestRule = CoroutineTestRule() - private val bottomSheetEvents: BottomSheetEvents = mockk(relaxed = true) - private val bottomSheetEventsFlow = MutableSharedFlow() + private val appScreenEvents: AppScreenEvents = mockk(relaxed = true) + private val appScreenEventsFlow = MutableSharedFlow() private lateinit var viewModel: AppScreenViewModel @Before fun setup() { - every { bottomSheetEvents.events } returns bottomSheetEventsFlow + every { appScreenEvents.events } returns appScreenEventsFlow viewModel = AppScreenViewModel( - bottomSheetEvents = bottomSheetEvents + appScreenEvents = appScreenEvents ) } @@ -56,10 +56,10 @@ class AppScreenViewModelTest { fun `given NewMeasurementAction is received then uiState should be updated`() = runTestUnconfined { // Given - val event = BottomSheetEvents.Event.NewMeasurementAction + val event = AppScreenEvents.Event.NewMeasurementAction // When - bottomSheetEventsFlow.emit(event) + appScreenEventsFlow.emit(event) // Then val updatedUiState = viewModel.uiState.value @@ -71,10 +71,10 @@ class AppScreenViewModelTest { fun `given DoNewMeasurement is received then uiState should be updated`() = runTestUnconfined { // Given - val event = BottomSheetEvents.Event.DoNewMeasurement + val event = AppScreenEvents.Event.DoNewMeasurement // When - bottomSheetEventsFlow.emit(event) + appScreenEventsFlow.emit(event) // Then val updatedUiState = viewModel.uiState.value @@ -86,10 +86,10 @@ class AppScreenViewModelTest { fun `given CloseBottomSheet is received then uiState should be updated`() = runTestUnconfined { // Given - val event = BottomSheetEvents.Event.CloseBottomSheet + val event = AppScreenEvents.Event.CloseBottomSheet // When - bottomSheetEventsFlow.emit(event) + appScreenEventsFlow.emit(event) // Then val updatedUiState = viewModel.uiState.value @@ -101,10 +101,10 @@ class AppScreenViewModelTest { fun `given WeightDescriptionBottomSheet is received then uiState should be updated`() = runTestUnconfined { // Given - val event = BottomSheetEvents.Event.WeightDescriptionBottomSheet + val event = AppScreenEvents.Event.WeightDescriptionBottomSheet // When - bottomSheetEventsFlow.emit(event) + appScreenEventsFlow.emit(event) // Then val updatedUiState = viewModel.uiState.value @@ -116,10 +116,10 @@ class AppScreenViewModelTest { fun `given AddWeightRecord is received then uiState should be updated`() = runTestUnconfined { // Given - val event = BottomSheetEvents.Event.AddWeightRecord + val event = AppScreenEvents.Event.AddWeightRecord // When - bottomSheetEventsFlow.emit(event) + appScreenEventsFlow.emit(event) // Then val updatedUiState = viewModel.uiState.value From 46e712c79cfe285d308483e80707ebb902dac6e1 Mon Sep 17 00:00:00 2001 From: Basler182 Date: Thu, 15 Aug 2024 15:15:17 +0200 Subject: [PATCH 05/31] added firebase functions Signed-off-by: Basler182 --- app/build.gradle.kts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 20fa72305..57d8b4d01 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) From aab7120fc1fa4a8ed146b0eac994f15d0605245d Mon Sep 17 00:00:00 2001 From: Basler182 Date: Thu, 15 Aug 2024 15:39:37 +0200 Subject: [PATCH 06/31] added HealthSummaryService Signed-off-by: Basler182 --- .../engagehf/messages/HealthSummaryService.kt | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 app/src/main/kotlin/edu/stanford/bdh/engagehf/messages/HealthSummaryService.kt 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..304b42799 --- /dev/null +++ b/app/src/main/kotlin/edu/stanford/bdh/engagehf/messages/HealthSummaryService.kt @@ -0,0 +1,60 @@ +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.logging.speziLogger +import edu.stanford.spezi.core.utils.MessageNotifier +import java.io.File +import java.io.FileOutputStream +import javax.inject.Inject + +internal class HealthSummaryService @Inject constructor( + private val healthSummaryRepository: HealthSummaryRepository, + private val messageNotifier: MessageNotifier, + @ApplicationContext private val context: Context, +) { + private val logger by speziLogger() + + companion object { + const val FILE_NAME = "health_summary.pdf" + const val MIME_TYPE_PDF = "application/pdf" + } + + suspend fun generateHealthSummaryPdf() { + val pdfByteArrayResult = + healthSummaryRepository.findHealthSummaryByUserId() + pdfByteArrayResult.onSuccess { + val savePdfToFile = savePdfToFile(it) + val pdfUri = FileProvider.getUriForFile( + context, + "${context.packageName}.provider", + savePdfToFile + ) + val intent = Intent(Intent.ACTION_VIEW) + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + intent.setDataAndType(pdfUri, MIME_TYPE_PDF) + context.startActivity(intent) + }.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 + } +} From cd71635caf31dc148dc9d06db0083ad71acb9b32 Mon Sep 17 00:00:00 2001 From: Basler182 Date: Thu, 15 Aug 2024 15:42:56 +0200 Subject: [PATCH 07/31] fixed and expanded existing BluetoothViewModelTests Signed-off-by: Basler182 --- .../bluetooth/BluetoothViewModelTest.kt | 55 ++++++++++++++++--- 1 file changed, 47 insertions(+), 8 deletions(-) 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 9e736e671..331161fae 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,11 +12,13 @@ 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.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 @@ -48,11 +50,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" @@ -240,7 +243,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 @@ -391,8 +394,6 @@ class BluetoothViewModelTest { // given val action = Action.MessageItemClicked(message = message) val todoActions = listOf( - MessagesAction.HealthSummaryAction, - MessagesAction.MedicationsAction, MessagesAction.QuestionnaireAction(questionnaire = mockk()), ) createViewModel() @@ -422,7 +423,7 @@ class BluetoothViewModelTest { bluetoothViewModel.onAction(action = action) // then - verify { bottomSheetEvents.emit(BottomSheetEvents.Event.DoNewMeasurement) } + verify { appScreenEvents.emit(AppScreenEvents.Event.DoNewMeasurement) } } @Test @@ -451,6 +452,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 @@ -466,7 +499,12 @@ class BluetoothViewModelTest { ) every { this@BluetoothViewModelTest.message.id } returns "new-id" - coEvery { messageRepository.observeUserMessages() } returns flowOf(listOf(message, this.message)) + coEvery { messageRepository.observeUserMessages() } returns flowOf( + listOf( + message, + this.message + ) + ) createViewModel() // when @@ -492,9 +530,10 @@ class BluetoothViewModelTest { uiStateMapper = uiStateMapper, measurementsRepository = measurementsRepository, messageRepository = messageRepository, - bottomSheetEvents = bottomSheetEvents, + appScreenEvents = appScreenEvents, navigator = navigator, engageEducationRepository = engageEducationRepository, + healthSummaryService = healthSummaryService ) } } From 8e4246f87f80c721be89b37140f627ab4b237c57 Mon Sep 17 00:00:00 2001 From: Basler182 Date: Thu, 15 Aug 2024 15:44:40 +0200 Subject: [PATCH 08/31] added MedicationsAction and HealthSummaryAction Signed-off-by: Basler182 --- .../engagehf/bluetooth/BluetoothViewModel.kt | 57 +++++++++++++------ 1 file changed, 40 insertions(+), 17 deletions(-) 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 b53fcd839..c978fc608 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,15 +3,17 @@ 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.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 @@ -33,9 +35,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() @@ -93,7 +96,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( @@ -130,7 +133,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 + ) + } } } } @@ -152,14 +159,20 @@ class BluetoothViewModel @Inject internal constructor( val mappingResult = uiStateMapper.mapMessagesAction(action.message.action) if (mappingResult.isSuccess) { when (val mappedAction = mappingResult.getOrNull()!!) { - is MessagesAction.HealthSummaryAction -> { /* TODO */ + is MessagesAction.HealthSummaryAction -> { + handleHealthSummaryAction() } is MessagesAction.MeasurementsAction -> { - bottomSheetEvents.emit(BottomSheetEvents.Event.DoNewMeasurement) + appScreenEvents.emit(AppScreenEvents.Event.DoNewMeasurement) } - is MessagesAction.MedicationsAction -> { /* TODO */ + is MessagesAction.MedicationsAction -> { + appScreenEvents.emit( + AppScreenEvents.Event.NavigateToTab( + BottomBarItem.MEDICATION + ) + ) } is MessagesAction.QuestionnaireAction -> { /* TODO */ @@ -167,16 +180,7 @@ class BluetoothViewModel @Inject internal constructor( is MessagesAction.VideoSectionAction -> { viewModelScope.launch { - engageEducationRepository.getVideoBySectionAndVideoId( - mappedAction.videoSectionVideo.videoSectionId, - mappedAction.videoSectionVideo.videoId - ).getOrNull()?.let { video -> - navigator.navigateTo( - EducationNavigationEvent.VideoSectionClicked( - video = video - ) - ) - } + handleVideoSectionAction(mappedAction) } } } @@ -197,6 +201,25 @@ class BluetoothViewModel @Inject internal constructor( } } + private suspend fun handleVideoSectionAction(mappedAction: MessagesAction.VideoSectionAction) { + engageEducationRepository.getVideoBySectionAndVideoId( + mappedAction.videoSectionVideo.videoSectionId, + mappedAction.videoSectionVideo.videoId + ).getOrNull()?.let { video -> + navigator.navigateTo( + EducationNavigationEvent.VideoSectionClicked( + video = video + ) + ) + } + } + + private fun handleHealthSummaryAction() { + viewModelScope.launch { + healthSummaryService.generateHealthSummaryPdf() + } + } + public override fun onCleared() { super.onCleared() bleService.stop() From 59dce139ebcdb178537455a288abc01cb8dcd03e Mon Sep 17 00:00:00 2001 From: Basler182 Date: Thu, 15 Aug 2024 18:48:31 +0200 Subject: [PATCH 09/31] tested HealthSummaryRepository Signed-off-by: Basler182 --- .../messages/HealthSummaryRepositoryTest.kt | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 app/src/test/kotlin/edu/stanford/bdh/engagehf/messages/HealthSummaryRepositoryTest.kt diff --git a/app/src/test/kotlin/edu/stanford/bdh/engagehf/messages/HealthSummaryRepositoryTest.kt b/app/src/test/kotlin/edu/stanford/bdh/engagehf/messages/HealthSummaryRepositoryTest.kt new file mode 100644 index 000000000..bd7b33d15 --- /dev/null +++ b/app/src/test/kotlin/edu/stanford/bdh/engagehf/messages/HealthSummaryRepositoryTest.kt @@ -0,0 +1,56 @@ +package edu.stanford.bdh.engagehf.messages + +import com.google.common.truth.Truth.assertThat +import com.google.firebase.functions.FirebaseFunctions +import edu.stanford.spezi.core.testing.runTestUnconfined +import edu.stanford.spezi.module.account.manager.UserSessionManager +import io.mockk.coEvery +import io.mockk.every +import io.mockk.mockk +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import org.junit.Test + +class HealthSummaryRepositoryTest { + private val uid = "some-uid" + private val userSessionManager: UserSessionManager = mockk { + every { getUserUid() } returns uid + } + private val firebaseFunctions: FirebaseFunctions = mockk() + private val ioDispatcher = UnconfinedTestDispatcher() + + private val repository = HealthSummaryRepository( + userSessionManager = userSessionManager, + firebaseFunctions = firebaseFunctions, + ioDispatcher = ioDispatcher + ) + + @Test + fun `findHealthSummaryByUserId returns failure when user is not authenticated`() = + runTestUnconfined { + // given + every { userSessionManager.getUserUid() } returns null + + // when + val result = repository.findHealthSummaryByUserId() + + // then + assertThat(result.isFailure).isTrue() + assertThat(result.exceptionOrNull()).isInstanceOf(IllegalStateException::class.java) + } + + @Test + fun `findHealthSummaryByUserId returns failure when function call fails`() = runTestUnconfined { + // given + val exception = Exception("Function call failed") + coEvery { + firebaseFunctions.getHttpsCallable(any()) + } throws exception + + // when + val result = repository.findHealthSummaryByUserId() + + // then + assertThat(result.isFailure).isTrue() + assertThat(result.exceptionOrNull()).isEqualTo(exception) + } +} From cf471487d752809fe65d7ac603d1a07ffd04ebb4 Mon Sep 17 00:00:00 2001 From: Basler182 Date: Thu, 15 Aug 2024 19:46:13 +0200 Subject: [PATCH 10/31] added loading spinner while generating health summary Signed-off-by: Basler182 --- .../engagehf/bluetooth/BluetoothViewModel.kt | 31 +++++++++++++++---- .../stanford/bdh/engagehf/messages/Message.kt | 1 + .../bdh/engagehf/messages/MessageItem.kt | 14 ++++++--- 3 files changed, 36 insertions(+), 10 deletions(-) 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 c978fc608..8f17d937a 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 @@ -158,9 +158,10 @@ class BluetoothViewModel @Inject internal constructor( viewModelScope.launch { val mappingResult = uiStateMapper.mapMessagesAction(action.message.action) if (mappingResult.isSuccess) { + val messageId = action.message.id when (val mappedAction = mappingResult.getOrNull()!!) { is MessagesAction.HealthSummaryAction -> { - handleHealthSummaryAction() + handleHealthSummaryAction(messageId) } is MessagesAction.MeasurementsAction -> { @@ -184,11 +185,7 @@ class BluetoothViewModel @Inject internal constructor( } } } - val messageId = action.message.id messageRepository.completeMessage(messageId = messageId) - _uiState.update { - it.copy(messages = it.messages.filter { message -> message.id != messageId }) - } } else { logger.e { "Error while mapping action: ${mappingResult.exceptionOrNull()}" } } @@ -214,9 +211,31 @@ class BluetoothViewModel @Inject internal constructor( } } - private fun handleHealthSummaryAction() { + private fun handleHealthSummaryAction(messageId: String) { viewModelScope.launch { + _uiState.update { + it.copy( + messages = it.messages.map { message -> + if (message.id == messageId) { + message.copy(isLoading = true) + } else { + message + } + } + ) + } healthSummaryService.generateHealthSummaryPdf() + _uiState.update { + it.copy( + messages = it.messages.map { message -> + if (message.id == messageId) { + message.copy(isLoading = false) + } else { + message + } + } + ) + } } } 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, + ) + } } } } From 0f308467de9f9bb327c38b6deefff7b7274379f6 Mon Sep 17 00:00:00 2001 From: Basler182 Date: Fri, 16 Aug 2024 18:33:12 +0200 Subject: [PATCH 11/31] added account top app bar item Signed-off-by: Basler182 --- .../messages/HealthSummaryRepository.kt | 2 +- .../engagehf/messages/HealthSummaryService.kt | 2 +- .../navigation/components/AccountDialog.kt | 191 ++++++++++++++++++ .../components/AccountTopAppBarButton.kt | 37 ++++ .../engagehf/navigation/screens/AppScreen.kt | 41 +++- .../navigation/screens/AppScreenViewModel.kt | 47 +++++ app/src/main/res/values-de/strings.xml | 5 + app/src/main/res/values/strings.xml | 3 + .../account/manager/UserSessionManager.kt | 29 ++- 9 files changed, 336 insertions(+), 21 deletions(-) create mode 100644 app/src/main/kotlin/edu/stanford/bdh/engagehf/navigation/components/AccountDialog.kt create mode 100644 app/src/main/kotlin/edu/stanford/bdh/engagehf/navigation/components/AccountTopAppBarButton.kt 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 index 94abc68a5..c38fe85c8 100644 --- a/app/src/main/kotlin/edu/stanford/bdh/engagehf/messages/HealthSummaryRepository.kt +++ b/app/src/main/kotlin/edu/stanford/bdh/engagehf/messages/HealthSummaryRepository.kt @@ -9,7 +9,7 @@ import kotlinx.coroutines.tasks.await import kotlinx.coroutines.withContext import javax.inject.Inject -internal class HealthSummaryRepository @Inject constructor( +class HealthSummaryRepository @Inject constructor( private val userSessionManager: UserSessionManager, private val firebaseFunctions: FirebaseFunctions, @Dispatching.IO private val ioDispatcher: CoroutineDispatcher, 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 index 304b42799..ef20cfcc7 100644 --- a/app/src/main/kotlin/edu/stanford/bdh/engagehf/messages/HealthSummaryService.kt +++ b/app/src/main/kotlin/edu/stanford/bdh/engagehf/messages/HealthSummaryService.kt @@ -11,7 +11,7 @@ import java.io.File import java.io.FileOutputStream import javax.inject.Inject -internal class HealthSummaryService @Inject constructor( +class HealthSummaryService @Inject constructor( private val healthSummaryRepository: HealthSummaryRepository, private val messageNotifier: MessageNotifier, @ApplicationContext private val context: Context, 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..22d777b75 --- /dev/null +++ b/app/src/main/kotlin/edu/stanford/bdh/engagehf/navigation/components/AccountDialog.kt @@ -0,0 +1,191 @@ +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.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +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.Action +import edu.stanford.bdh.engagehf.navigation.screens.AppTopBar +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.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.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(appTopBar: AppTopBar, onAction: (Action) -> Unit) { + Dialog( + onDismissRequest = { + if (!appTopBar.isHealthSummaryLoading) { + onAction(Action.ShowDialog(false)) + } + }, + properties = DialogProperties() + ) { + Surface( + shape = MaterialTheme.shapes.medium, + color = MaterialTheme.colorScheme.surface.lighten(), + modifier = Modifier + .fillMaxWidth() + .padding(Spacings.medium) + ) { + Column( + modifier = Modifier.padding(Spacings.medium), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Box( + modifier = Modifier.fillMaxWidth(), + contentAlignment = Alignment.TopStart + ) { + IconButton( + enabled = !appTopBar.isHealthSummaryLoading, + onClick = { onAction(Action.ShowDialog(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) + ) + } + appTopBar.initials?.let { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .align(Alignment.Center) + .size(Sizes.Icon.large) + .background(primary, shape = CircleShape) + ) { + Text( + text = appTopBar.initials, + style = headlineMedium, + color = onPrimary, + ) + } + } + + if (appTopBar.initials == null) { + Icon( + imageVector = Icons.Default.AccountCircle, + contentDescription = null, + modifier = Modifier + .align(Alignment.Center) + .size(Sizes.Icon.large), + tint = primary + ) + } + } + VerticalSpacer() + appTopBar.userName?.let { + Text(text = appTopBar.userName, style = headlineMedium) + VerticalSpacer(height = Spacings.small) + } + Text(text = appTopBar.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 (appTopBar.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( + AppTopBar( + userName = "John Doe", + initials = "JD", + email = "john@doe.de", + isHealthSummaryLoading = false + ), + AppTopBar( + userName = "Jane Smith", + email = "jane@smith.com", + isHealthSummaryLoading = true + ), + AppTopBar( + userName = null, + email = "john@doe.de", + ) + ) +} + +@ThemePreviews +@Composable +fun AccountDialogPreview( + @PreviewParameter(AppTopBarProvider::class) appTopBar: AppTopBar, +) { + SpeziTheme { + AccountDialog( + appTopBar = appTopBar, + 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..e9c3d861b --- /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.Action +import edu.stanford.bdh.engagehf.navigation.screens.AppTopBar +import edu.stanford.spezi.core.design.theme.Colors +import edu.stanford.spezi.core.design.theme.Sizes + +@Composable +fun AccountTopAppBarButton(appTopBar: AppTopBar, onAction: (Action) -> Unit) { + IconButton(onClick = { + onAction(Action.ShowDialog(true)) + }) { + Icon( + imageVector = Icons.Default.AccountCircle, + contentDescription = "Account", + tint = Colors.onPrimary, + modifier = Modifier.size(Sizes.Icon.medium) + ) + } + AnimatedVisibility( + visible = appTopBar.showDialog, + enter = fadeIn(), + exit = fadeOut() + ) { + AccountDialog(appTopBar = appTopBar, 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 7f3f7d9aa..e9934233b 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,9 @@ package edu.stanford.bdh.engagehf.navigation.screens 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.material3.BottomSheetScaffold import androidx.compose.material3.ExperimentalMaterial3Api @@ -18,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 @@ -30,7 +34,9 @@ import edu.stanford.bdh.engagehf.health.HealthScreen 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.launch @@ -71,7 +77,6 @@ fun AppScreen( onAction(Action.UpdateBottomSheetState(isExpanded = state == SheetValue.Expanded)) } } - BottomSheetScaffold( scaffoldState = bottomSheetScaffoldState, sheetContent = { @@ -90,12 +95,21 @@ fun AppScreen( 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 + ) ) - ) + Spacer(modifier = Modifier.weight(1f)) + AccountTopAppBarButton(uiState.appTopBar, onAction = onAction) + } }) }, bottomBar = { @@ -114,11 +128,22 @@ fun AppScreen( ), 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 ) }, - label = { Text(text = stringResource(id = item.label), textAlign = TextAlign.Center) }, + label = { + Text( + text = stringResource(id = item.label), + textAlign = TextAlign.Center + ) + }, selected = uiState.selectedItem == item, onClick = { onAction(Action.UpdateSelectedBottomBarItem(item)) 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 95b364467..0b23a28c6 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 @@ -7,6 +7,8 @@ import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import edu.stanford.bdh.engagehf.R 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 @@ -17,6 +19,8 @@ import edu.stanford.spezi.core.design.R.drawable as DesignR @HiltViewModel class AppScreenViewModel @Inject constructor( private val appScreenEvents: AppScreenEvents, + private val userSessionManager: UserSessionManager, + private val healthSummaryService: HealthSummaryService, ) : ViewModel() { private val _uiState = MutableStateFlow( AppUiState( @@ -33,6 +37,13 @@ class AppScreenViewModel @Inject constructor( private fun setup() { viewModelScope.launch { + _uiState.update { it -> + val userName = userSessionManager.getUserName()?.takeIf { it.isNotBlank() } + it.copy(appTopBar = it.appTopBar.copy(userName = userName)) + } + userSessionManager.getUserEmail()?.let { email -> + _uiState.update { it.copy(appTopBar = it.appTopBar.copy(email = email)) } + } appScreenEvents.events.collect { event -> val (isExpanded, content) = when (event) { is AppScreenEvents.Event.NavigateToTab -> { @@ -86,6 +97,30 @@ class AppScreenViewModel @Inject constructor( is Action.UpdateBottomSheetState -> { _uiState.update { it.copy(isBottomSheetExpanded = action.isExpanded) } } + + is Action.ShowDialog -> { + _uiState.update { it.copy(appTopBar = it.appTopBar.copy(showDialog = action.showDialog)) } + } + + Action.ShowHealthSummary -> { + viewModelScope.launch { + _uiState.update { it.copy(appTopBar = it.appTopBar.copy(isHealthSummaryLoading = true)) } + healthSummaryService.generateHealthSummaryPdf() + _uiState.update { + it.copy( + appTopBar = it.appTopBar.copy( + isHealthSummaryLoading = false, + showDialog = false + ) + ) + } + } + } + + Action.SignOut -> { + _uiState.update { it.copy(appTopBar = it.appTopBar.copy(showDialog = false)) } + /* TODO: Implement sign out */ + } } } } @@ -95,6 +130,15 @@ data class AppUiState( val selectedItem: BottomBarItem, val isBottomSheetExpanded: Boolean = false, val bottomSheetContent: BottomSheetContent? = null, + val appTopBar: AppTopBar = AppTopBar(), +) + +data class AppTopBar( + val showDialog: Boolean = false, + val userName: String? = null, + val initials: String? = null, + val email: String = "", + val isHealthSummaryLoading: Boolean = false, ) enum class BottomSheetContent { @@ -107,6 +151,9 @@ 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 ShowDialog(val showDialog: Boolean) : 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 59fd9080a..49aabf9f9 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -18,8 +18,10 @@ Info Icon Blutdruck Icon Bitte nehmen Sie eine neue Messung vor. + Keine Empfehlungen für Medikamente Aktuelle Dosis: Ziel Dosis: + k.A. Aktuell Ziel Täglich @@ -36,4 +38,7 @@ Gewicht BD Herzfrequenz + Dialog schließen + Ausloggen + Gesundheitszusammenfassung diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c8805f1b6..1375d20f9 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -37,4 +37,7 @@ Weight BP Heart Rate + Close dialog + Sign Out + Health Summary diff --git a/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/manager/UserSessionManager.kt b/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/manager/UserSessionManager.kt index a672b4234..bf8faad68 100644 --- a/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/manager/UserSessionManager.kt +++ b/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/manager/UserSessionManager.kt @@ -21,6 +21,8 @@ interface UserSessionManager { suspend fun getUserState(): UserState fun observeUserState(): Flow fun getUserUid(): String? + fun getUserName(): String? + fun getUserEmail(): String? } @Singleton @@ -32,19 +34,20 @@ internal class UserSessionManagerImpl @Inject constructor( ) : UserSessionManager { private val logger by speziLogger() - override suspend fun uploadConsentPdf(pdfBytes: ByteArray): Result = withContext(ioDispatcher) { - runCatching { - val currentUser = firebaseAuth.currentUser ?: error("User not available") - val inputStream = ByteArrayInputStream(pdfBytes) - logger.i { "Uploading file to Firebase Storage" } - val uploaded = firebaseStorage - .getReference("users/${currentUser.uid}/consent/consent.pdf") - .putStream(inputStream) - .await().task.isSuccessful + override suspend fun uploadConsentPdf(pdfBytes: ByteArray): Result = + withContext(ioDispatcher) { + runCatching { + val currentUser = firebaseAuth.currentUser ?: error("User not available") + val inputStream = ByteArrayInputStream(pdfBytes) + logger.i { "Uploading file to Firebase Storage" } + val uploaded = firebaseStorage + .getReference("users/${currentUser.uid}/consent/consent.pdf") + .putStream(inputStream) + .await().task.isSuccessful - if (!uploaded) error("Failed to upload consent.pdf") + if (!uploaded) error("Failed to upload consent.pdf") + } } - } override suspend fun getUserState(): UserState { val user = firebaseAuth.currentUser @@ -69,6 +72,10 @@ internal class UserSessionManagerImpl @Inject constructor( override fun getUserUid(): String? = firebaseAuth.uid + override fun getUserName(): String? = firebaseAuth.currentUser?.displayName + + override fun getUserEmail(): String? = firebaseAuth.currentUser?.email + private suspend fun hasConsented(): Boolean = withContext(ioDispatcher) { runCatching { val uid = getUserUid() ?: error("No uid available") From 56056f8bb4d03b078fa48fd4027e2a0bb66ca3bf Mon Sep 17 00:00:00 2001 From: Basler182 Date: Fri, 16 Aug 2024 18:44:03 +0200 Subject: [PATCH 12/31] added actions to TopAppBar Signed-off-by: Basler182 --- .../bdh/engagehf/navigation/screens/AppScreen.kt | 9 +++++---- .../stanford/spezi/core/design/component/TopAppBar.kt | 5 ++++- 2 files changed, 9 insertions(+), 5 deletions(-) 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 e9934233b..4ba6395b4 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 @@ -2,7 +2,6 @@ package edu.stanford.bdh.engagehf.navigation.screens 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.material3.BottomSheetScaffold @@ -107,10 +106,12 @@ fun AppScreen( AppScreenTestIdentifier.TOP_APP_BAR_TITLE ) ) - Spacer(modifier = Modifier.weight(1f)) - AccountTopAppBarButton(uiState.appTopBar, onAction = onAction) } - }) + }, + actions = { + AccountTopAppBarButton(uiState.appTopBar, onAction = onAction) + } + ) }, bottomBar = { Column { diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/component/TopAppBar.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/component/TopAppBar.kt index a2dd68f69..d2297fe1b 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/component/TopAppBar.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/component/TopAppBar.kt @@ -1,5 +1,6 @@ package edu.stanford.spezi.core.design.component +import androidx.compose.foundation.layout.RowScope import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults @@ -14,6 +15,7 @@ fun AppTopAppBar( modifier: Modifier = Modifier, title: @Composable () -> Unit, navigationIcon: @Composable () -> Unit = {}, + actions: @Composable RowScope.() -> Unit = {}, ) { TopAppBar( modifier = modifier, @@ -25,6 +27,7 @@ fun AppTopAppBar( actionIconContentColor = onPrimary ), title = title, - navigationIcon = navigationIcon + navigationIcon = navigationIcon, + actions = actions, ) } From 3610d393f9c7db7c2265f040f1ecebd3e569c43d Mon Sep 17 00:00:00 2001 From: Basler182 Date: Fri, 16 Aug 2024 18:46:17 +0200 Subject: [PATCH 13/31] adjusted AppScreenViewModelTests Signed-off-by: Basler182 --- .../engagehf/navigation/screens/AppScreenViewModelTest.kt | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/app/src/test/kotlin/edu/stanford/bdh/engagehf/navigation/screens/AppScreenViewModelTest.kt b/app/src/test/kotlin/edu/stanford/bdh/engagehf/navigation/screens/AppScreenViewModelTest.kt index ad8bdbfaf..316471064 100644 --- a/app/src/test/kotlin/edu/stanford/bdh/engagehf/navigation/screens/AppScreenViewModelTest.kt +++ b/app/src/test/kotlin/edu/stanford/bdh/engagehf/navigation/screens/AppScreenViewModelTest.kt @@ -2,8 +2,10 @@ package edu.stanford.bdh.engagehf.navigation.screens import com.google.common.truth.Truth.assertThat import edu.stanford.bdh.engagehf.bluetooth.component.AppScreenEvents +import edu.stanford.bdh.engagehf.messages.HealthSummaryService import edu.stanford.spezi.core.testing.CoroutineTestRule import edu.stanford.spezi.core.testing.runTestUnconfined +import edu.stanford.spezi.module.account.manager.UserSessionManager import io.mockk.every import io.mockk.mockk import kotlinx.coroutines.flow.MutableSharedFlow @@ -17,6 +19,8 @@ class AppScreenViewModelTest { val coroutineTestRule = CoroutineTestRule() private val appScreenEvents: AppScreenEvents = mockk(relaxed = true) + private val userSessionManager: UserSessionManager = mockk(relaxed = true) + private val healthSummaryService: HealthSummaryService = mockk(relaxed = true) private val appScreenEventsFlow = MutableSharedFlow() private lateinit var viewModel: AppScreenViewModel @@ -25,7 +29,9 @@ class AppScreenViewModelTest { fun setup() { every { appScreenEvents.events } returns appScreenEventsFlow viewModel = AppScreenViewModel( - appScreenEvents = appScreenEvents + appScreenEvents = appScreenEvents, + userSessionManager = userSessionManager, + healthSummaryService = healthSummaryService ) } From 85b5ce365eaf54ecbd4446dff72314bbbe320851 Mon Sep 17 00:00:00 2001 From: Basler182 Date: Sun, 18 Aug 2024 19:44:52 +0200 Subject: [PATCH 14/31] renamed ShowDialog to ShowAccountDialog Signed-off-by: Basler182 --- .../bdh/engagehf/navigation/components/AccountDialog.kt | 4 ++-- .../engagehf/navigation/components/AccountTopAppBarButton.kt | 2 +- .../bdh/engagehf/navigation/screens/AppScreenViewModel.kt | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) 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 index 22d777b75..d5e7bece0 100644 --- 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 @@ -47,7 +47,7 @@ fun AccountDialog(appTopBar: AppTopBar, onAction: (Action) -> Unit) { Dialog( onDismissRequest = { if (!appTopBar.isHealthSummaryLoading) { - onAction(Action.ShowDialog(false)) + onAction(Action.ShowAccountDialog(false)) } }, properties = DialogProperties() @@ -69,7 +69,7 @@ fun AccountDialog(appTopBar: AppTopBar, onAction: (Action) -> Unit) { ) { IconButton( enabled = !appTopBar.isHealthSummaryLoading, - onClick = { onAction(Action.ShowDialog(false)) }, + onClick = { onAction(Action.ShowAccountDialog(false)) }, modifier = Modifier.align(Alignment.CenterStart) ) { Icon( 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 index e9c3d861b..0c522f377 100644 --- 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 @@ -18,7 +18,7 @@ import edu.stanford.spezi.core.design.theme.Sizes @Composable fun AccountTopAppBarButton(appTopBar: AppTopBar, onAction: (Action) -> Unit) { IconButton(onClick = { - onAction(Action.ShowDialog(true)) + onAction(Action.ShowAccountDialog(true)) }) { Icon( imageVector = Icons.Default.AccountCircle, 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 0b23a28c6..0ce60c377 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 @@ -98,7 +98,7 @@ class AppScreenViewModel @Inject constructor( _uiState.update { it.copy(isBottomSheetExpanded = action.isExpanded) } } - is Action.ShowDialog -> { + is Action.ShowAccountDialog -> { _uiState.update { it.copy(appTopBar = it.appTopBar.copy(showDialog = action.showDialog)) } } @@ -153,7 +153,7 @@ sealed interface Action { data class UpdateBottomSheetState(val isExpanded: Boolean) : Action data object ShowHealthSummary : Action data object SignOut : Action - data class ShowDialog(val showDialog: Boolean) : Action + data class ShowAccountDialog(val showDialog: Boolean) : Action } enum class BottomBarItem( From 14bd1019e7687b550b99f1545313eec3c660f618 Mon Sep 17 00:00:00 2001 From: Basler182 Date: Sun, 18 Aug 2024 19:46:36 +0200 Subject: [PATCH 15/31] renamed AppTopBar to AccountUiState Signed-off-by: Basler182 --- .../navigation/components/AccountDialog.kt | 36 +++++++++---------- .../components/AccountTopAppBarButton.kt | 8 ++--- .../engagehf/navigation/screens/AppScreen.kt | 2 +- .../navigation/screens/AppScreenViewModel.kt | 22 +++++++----- 4 files changed, 37 insertions(+), 31 deletions(-) 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 index d5e7bece0..3406c8479 100644 --- 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 @@ -28,8 +28,8 @@ 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.bdh.engagehf.navigation.screens.AppTopBar 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 @@ -43,10 +43,10 @@ import edu.stanford.spezi.core.design.theme.ThemePreviews import edu.stanford.spezi.core.design.theme.lighten @Composable -fun AccountDialog(appTopBar: AppTopBar, onAction: (Action) -> Unit) { +fun AccountDialog(accountUiState: AccountUiState, onAction: (Action) -> Unit) { Dialog( onDismissRequest = { - if (!appTopBar.isHealthSummaryLoading) { + if (!accountUiState.isHealthSummaryLoading) { onAction(Action.ShowAccountDialog(false)) } }, @@ -68,7 +68,7 @@ fun AccountDialog(appTopBar: AppTopBar, onAction: (Action) -> Unit) { contentAlignment = Alignment.TopStart ) { IconButton( - enabled = !appTopBar.isHealthSummaryLoading, + enabled = !accountUiState.isHealthSummaryLoading, onClick = { onAction(Action.ShowAccountDialog(false)) }, modifier = Modifier.align(Alignment.CenterStart) ) { @@ -79,7 +79,7 @@ fun AccountDialog(appTopBar: AppTopBar, onAction: (Action) -> Unit) { modifier = Modifier.size(Sizes.Icon.small) ) } - appTopBar.initials?.let { + accountUiState.initials?.let { Box( contentAlignment = Alignment.Center, modifier = Modifier @@ -88,14 +88,14 @@ fun AccountDialog(appTopBar: AppTopBar, onAction: (Action) -> Unit) { .background(primary, shape = CircleShape) ) { Text( - text = appTopBar.initials, + text = accountUiState.initials, style = headlineMedium, color = onPrimary, ) } } - if (appTopBar.initials == null) { + if (accountUiState.initials == null) { Icon( imageVector = Icons.Default.AccountCircle, contentDescription = null, @@ -107,11 +107,11 @@ fun AccountDialog(appTopBar: AppTopBar, onAction: (Action) -> Unit) { } } VerticalSpacer() - appTopBar.userName?.let { - Text(text = appTopBar.userName, style = headlineMedium) + accountUiState.userName?.let { + Text(text = accountUiState.userName, style = headlineMedium) VerticalSpacer(height = Spacings.small) } - Text(text = appTopBar.email, style = bodyMedium) + Text(text = accountUiState.email, style = bodyMedium) VerticalSpacer() HorizontalDivider() TextButton( @@ -130,7 +130,7 @@ fun AccountDialog(appTopBar: AppTopBar, onAction: (Action) -> Unit) { style = bodyMedium, modifier = Modifier.weight(1f) ) - if (appTopBar.isHealthSummaryLoading) { + if (accountUiState.isHealthSummaryLoading) { CircularProgressIndicator( modifier = Modifier.size(Sizes.Icon.small), color = primary @@ -157,20 +157,20 @@ fun AccountDialog(appTopBar: AppTopBar, onAction: (Action) -> Unit) { } } -class AppTopBarProvider : PreviewParameterProvider { - override val values: Sequence = sequenceOf( - AppTopBar( +class AppTopBarProvider : PreviewParameterProvider { + override val values: Sequence = sequenceOf( + AccountUiState( userName = "John Doe", initials = "JD", email = "john@doe.de", isHealthSummaryLoading = false ), - AppTopBar( + AccountUiState( userName = "Jane Smith", email = "jane@smith.com", isHealthSummaryLoading = true ), - AppTopBar( + AccountUiState( userName = null, email = "john@doe.de", ) @@ -180,11 +180,11 @@ class AppTopBarProvider : PreviewParameterProvider { @ThemePreviews @Composable fun AccountDialogPreview( - @PreviewParameter(AppTopBarProvider::class) appTopBar: AppTopBar, + @PreviewParameter(AppTopBarProvider::class) accountUiState: AccountUiState, ) { SpeziTheme { AccountDialog( - appTopBar = appTopBar, + 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 index 0c522f377..533390c9c 100644 --- 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 @@ -10,13 +10,13 @@ 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.bdh.engagehf.navigation.screens.AppTopBar import edu.stanford.spezi.core.design.theme.Colors import edu.stanford.spezi.core.design.theme.Sizes @Composable -fun AccountTopAppBarButton(appTopBar: AppTopBar, onAction: (Action) -> Unit) { +fun AccountTopAppBarButton(accountUiState: AccountUiState, onAction: (Action) -> Unit) { IconButton(onClick = { onAction(Action.ShowAccountDialog(true)) }) { @@ -28,10 +28,10 @@ fun AccountTopAppBarButton(appTopBar: AppTopBar, onAction: (Action) -> Unit) { ) } AnimatedVisibility( - visible = appTopBar.showDialog, + visible = accountUiState.showDialog, enter = fadeIn(), exit = fadeOut() ) { - AccountDialog(appTopBar = appTopBar, onAction = onAction) + 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 4ba6395b4..358ce7170 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 @@ -109,7 +109,7 @@ fun AppScreen( } }, actions = { - AccountTopAppBarButton(uiState.appTopBar, onAction = onAction) + AccountTopAppBarButton(uiState.accountUiState, onAction = onAction) } ) }, 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 0ce60c377..bd233e73f 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 @@ -39,10 +39,10 @@ class AppScreenViewModel @Inject constructor( viewModelScope.launch { _uiState.update { it -> val userName = userSessionManager.getUserName()?.takeIf { it.isNotBlank() } - it.copy(appTopBar = it.appTopBar.copy(userName = userName)) + it.copy(accountUiState = it.accountUiState.copy(userName = userName)) } userSessionManager.getUserEmail()?.let { email -> - _uiState.update { it.copy(appTopBar = it.appTopBar.copy(email = email)) } + _uiState.update { it.copy(accountUiState = it.accountUiState.copy(email = email)) } } appScreenEvents.events.collect { event -> val (isExpanded, content) = when (event) { @@ -99,16 +99,22 @@ class AppScreenViewModel @Inject constructor( } is Action.ShowAccountDialog -> { - _uiState.update { it.copy(appTopBar = it.appTopBar.copy(showDialog = action.showDialog)) } + _uiState.update { it.copy(accountUiState = it.accountUiState.copy(showDialog = action.showDialog)) } } Action.ShowHealthSummary -> { viewModelScope.launch { - _uiState.update { it.copy(appTopBar = it.appTopBar.copy(isHealthSummaryLoading = true)) } + _uiState.update { + it.copy( + accountUiState = it.accountUiState.copy( + isHealthSummaryLoading = true + ) + ) + } healthSummaryService.generateHealthSummaryPdf() _uiState.update { it.copy( - appTopBar = it.appTopBar.copy( + accountUiState = it.accountUiState.copy( isHealthSummaryLoading = false, showDialog = false ) @@ -118,7 +124,7 @@ class AppScreenViewModel @Inject constructor( } Action.SignOut -> { - _uiState.update { it.copy(appTopBar = it.appTopBar.copy(showDialog = false)) } + _uiState.update { it.copy(accountUiState = it.accountUiState.copy(showDialog = false)) } /* TODO: Implement sign out */ } } @@ -130,10 +136,10 @@ data class AppUiState( val selectedItem: BottomBarItem, val isBottomSheetExpanded: Boolean = false, val bottomSheetContent: BottomSheetContent? = null, - val appTopBar: AppTopBar = AppTopBar(), + val accountUiState: AccountUiState = AccountUiState(), ) -data class AppTopBar( +data class AccountUiState( val showDialog: Boolean = false, val userName: String? = null, val initials: String? = null, From 68a8ee5f365bffd98276948f0653afb60ad81cce Mon Sep 17 00:00:00 2001 From: Basler182 Date: Sun, 18 Aug 2024 19:51:47 +0200 Subject: [PATCH 16/31] switched to typealias for generic map with wildcard types Signed-off-by: Basler182 --- .../stanford/bdh/engagehf/messages/HealthSummaryRepository.kt | 3 ++- .../main/kotlin/edu/stanford/spezi/core/utils/Typealiases.kt | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) 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 index c38fe85c8..55994a02e 100644 --- a/app/src/main/kotlin/edu/stanford/bdh/engagehf/messages/HealthSummaryRepository.kt +++ b/app/src/main/kotlin/edu/stanford/bdh/engagehf/messages/HealthSummaryRepository.kt @@ -3,6 +3,7 @@ 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 @@ -23,7 +24,7 @@ class HealthSummaryRepository @Inject constructor( val result = firebaseFunctions.getHttpsCallable("exportHealthSummary") .call(mapOf("userId" to uid)) .await() - val resultData = result.data as? Map<*, *> + val resultData = result.data as? JsonMap ?: throw IllegalStateException("Invalid function response") val pdfBase64 = resultData["content"] as? String ?: throw IllegalStateException("No content found in function response") diff --git a/core/utils/src/main/kotlin/edu/stanford/spezi/core/utils/Typealiases.kt b/core/utils/src/main/kotlin/edu/stanford/spezi/core/utils/Typealiases.kt index fee80057f..b6cee33ed 100644 --- a/core/utils/src/main/kotlin/edu/stanford/spezi/core/utils/Typealiases.kt +++ b/core/utils/src/main/kotlin/edu/stanford/spezi/core/utils/Typealiases.kt @@ -35,4 +35,4 @@ typealias TestIdentifier = Enum<*> /** * A typealias for kotlin.Map with String keys and any values */ -typealias JsonMap = Map +typealias JsonMap = Map<*, *> From c4a59995b5dddef47143ef93774b87148316721f Mon Sep 17 00:00:00 2001 From: Basler182 Date: Sun, 18 Aug 2024 19:58:05 +0200 Subject: [PATCH 17/31] removed MaterialTheme import Signed-off-by: Basler182 --- .../bdh/engagehf/navigation/components/AccountDialog.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 index 3406c8479..9a243b94e 100644 --- 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 @@ -34,6 +34,7 @@ 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 @@ -54,7 +55,7 @@ fun AccountDialog(accountUiState: AccountUiState, onAction: (Action) -> Unit) { ) { Surface( shape = MaterialTheme.shapes.medium, - color = MaterialTheme.colorScheme.surface.lighten(), + color = surface.lighten(), modifier = Modifier .fillMaxWidth() .padding(Spacings.medium) From faa21042a1e39c946e4ab93d7adc7d15ef4327f8 Mon Sep 17 00:00:00 2001 From: Basler182 Date: Sun, 18 Aug 2024 20:22:00 +0200 Subject: [PATCH 18/31] added title to dialog Signed-off-by: Basler182 --- .../navigation/components/AccountDialog.kt | 62 ++++++++++++------- app/src/main/res/values-de/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + 3 files changed, 42 insertions(+), 22 deletions(-) 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 index 9a243b94e..40f819718 100644 --- 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 @@ -4,9 +4,11 @@ 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 @@ -38,6 +40,7 @@ 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 @@ -64,27 +67,39 @@ fun AccountDialog(accountUiState: AccountUiState, onAction: (Action) -> Unit) { modifier = Modifier.padding(Spacings.medium), horizontalAlignment = Alignment.CenterHorizontally, ) { - Box( - modifier = Modifier.fillMaxWidth(), - contentAlignment = Alignment.TopStart - ) { - IconButton( - enabled = !accountUiState.isHealthSummaryLoading, - onClick = { onAction(Action.ShowAccountDialog(false)) }, - modifier = Modifier.align(Alignment.CenterStart) + Row { + Box( + modifier = Modifier.fillMaxWidth(), + contentAlignment = Alignment.TopStart ) { - Icon( - imageVector = Icons.Default.Close, - contentDescription = stringResource(R.string.close_dialog_content_description), - tint = MaterialTheme.colorScheme.primary, - modifier = Modifier.size(Sizes.Icon.small) + 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 - .align(Alignment.Center) .size(Sizes.Icon.large) .background(primary, shape = CircleShape) ) { @@ -101,19 +116,22 @@ fun AccountDialog(accountUiState: AccountUiState, onAction: (Action) -> Unit) { imageVector = Icons.Default.AccountCircle, contentDescription = null, modifier = Modifier - .align(Alignment.Center) .size(Sizes.Icon.large), tint = primary ) } + Spacer(modifier = Modifier.width(Spacings.medium)) + Column { + VerticalSpacer() + accountUiState.userName?.let { + Text(text = accountUiState.userName, style = headlineMedium) + VerticalSpacer(height = Spacings.small) + } + Text(text = accountUiState.email, style = bodyMedium) + VerticalSpacer() + } } - VerticalSpacer() - accountUiState.userName?.let { - Text(text = accountUiState.userName, style = headlineMedium) - VerticalSpacer(height = Spacings.small) - } - Text(text = accountUiState.email, style = bodyMedium) - VerticalSpacer() + HorizontalDivider() TextButton( onClick = { diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 49aabf9f9..1d3c4d15d 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -41,4 +41,5 @@ 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 1375d20f9..9d5c3e6cb 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -40,4 +40,5 @@ Close dialog Sign Out Health Summary + Account From 5ff42d195250a7a0b752e57a7a61d2bd9e09d571 Mon Sep 17 00:00:00 2001 From: Basler182 Date: Sun, 18 Aug 2024 20:25:20 +0200 Subject: [PATCH 19/31] generateHealthSummary with ioDispatcher Signed-off-by: Basler182 --- .../engagehf/messages/HealthSummaryService.kt | 38 +++++++++++-------- 1 file changed, 22 insertions(+), 16 deletions(-) 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 index ef20cfcc7..9e042739d 100644 --- a/app/src/main/kotlin/edu/stanford/bdh/engagehf/messages/HealthSummaryService.kt +++ b/app/src/main/kotlin/edu/stanford/bdh/engagehf/messages/HealthSummaryService.kt @@ -5,8 +5,11 @@ 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 @@ -14,6 +17,7 @@ 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() @@ -24,22 +28,24 @@ class HealthSummaryService @Inject constructor( } suspend fun generateHealthSummaryPdf() { - val pdfByteArrayResult = - healthSummaryRepository.findHealthSummaryByUserId() - pdfByteArrayResult.onSuccess { - val savePdfToFile = savePdfToFile(it) - val pdfUri = FileProvider.getUriForFile( - context, - "${context.packageName}.provider", - savePdfToFile - ) - val intent = Intent(Intent.ACTION_VIEW) - intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - intent.setDataAndType(pdfUri, MIME_TYPE_PDF) - context.startActivity(intent) - }.onFailure { - messageNotifier.notify("Failed to generate Health Summary") + withContext(ioDispatcher) { + val pdfByteArrayResult = + healthSummaryRepository.findHealthSummaryByUserId() + pdfByteArrayResult.onSuccess { + val savePdfToFile = savePdfToFile(it) + val pdfUri = FileProvider.getUriForFile( + context, + "${context.packageName}.provider", + savePdfToFile + ) + val intent = Intent(Intent.ACTION_VIEW) + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + intent.setDataAndType(pdfUri, MIME_TYPE_PDF) + context.startActivity(intent) + }.onFailure { + messageNotifier.notify("Failed to generate Health Summary") + } } } From 346ae96d84b0d744d984554a7896abda43835189 Mon Sep 17 00:00:00 2001 From: Basler182 Date: Sun, 18 Aug 2024 20:29:24 +0200 Subject: [PATCH 20/31] use lambda for state changing Signed-off-by: Basler182 --- .../engagehf/bluetooth/BluetoothViewModel.kt | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) 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 8f17d937a..1b4f5a750 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 @@ -212,30 +212,23 @@ class BluetoothViewModel @Inject internal constructor( } private fun handleHealthSummaryAction(messageId: String) { - viewModelScope.launch { + val setLoading = { loading: Boolean -> _uiState.update { it.copy( messages = it.messages.map { message -> if (message.id == messageId) { - message.copy(isLoading = true) + message.copy(isLoading = loading) } else { message } } ) } + } + viewModelScope.launch { + setLoading(true) healthSummaryService.generateHealthSummaryPdf() - _uiState.update { - it.copy( - messages = it.messages.map { message -> - if (message.id == messageId) { - message.copy(isLoading = false) - } else { - message - } - } - ) - } + setLoading(false) } } From 3b5b0dbd66488ea8d8ea48721f56e38a87782f3d Mon Sep 17 00:00:00 2001 From: Basler182 Date: Sun, 18 Aug 2024 21:00:36 +0200 Subject: [PATCH 21/31] changed name and email to user info Signed-off-by: Basler182 --- .../navigation/components/AccountDialog.kt | 27 ++++++++++++------- .../navigation/screens/AppScreenViewModel.kt | 16 +++++------ .../spezi/module/account/manager/UserInfo.kt | 6 +++++ .../account/manager/UserSessionManager.kt | 13 +++++---- 4 files changed, 39 insertions(+), 23 deletions(-) create mode 100644 modules/account/src/main/kotlin/edu/stanford/spezi/module/account/manager/UserInfo.kt 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 index 40f819718..359af4f55 100644 --- 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 @@ -45,6 +45,7 @@ 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 +import edu.stanford.spezi.module.account.manager.UserInfo @Composable fun AccountDialog(accountUiState: AccountUiState, onAction: (Action) -> Unit) { @@ -123,11 +124,11 @@ fun AccountDialog(accountUiState: AccountUiState, onAction: (Action) -> Unit) { Spacer(modifier = Modifier.width(Spacings.medium)) Column { VerticalSpacer() - accountUiState.userName?.let { - Text(text = accountUiState.userName, style = headlineMedium) + accountUiState.userInfo.name?.let { + Text(text = it, style = headlineMedium) VerticalSpacer(height = Spacings.small) } - Text(text = accountUiState.email, style = bodyMedium) + Text(text = accountUiState.userInfo.email, style = bodyMedium) VerticalSpacer() } } @@ -179,19 +180,25 @@ fun AccountDialog(accountUiState: AccountUiState, onAction: (Action) -> Unit) { class AppTopBarProvider : PreviewParameterProvider { override val values: Sequence = sequenceOf( AccountUiState( - userName = "John Doe", initials = "JD", - email = "john@doe.de", - isHealthSummaryLoading = false + isHealthSummaryLoading = false, + userInfo = UserInfo( + name = "John Doe", + email = "john@doe.de" + ), ), AccountUiState( - userName = "Jane Smith", - email = "jane@smith.com", + userInfo = UserInfo( + name = "John Doe", + email = "" + ), isHealthSummaryLoading = true ), AccountUiState( - userName = null, - email = "john@doe.de", + userInfo = UserInfo( + name = null, + email = "john@doe.de" + ) ) ) } 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 bd233e73f..84f20db34 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 @@ -8,6 +8,7 @@ import dagger.hilt.android.lifecycle.HiltViewModel import edu.stanford.bdh.engagehf.R import edu.stanford.bdh.engagehf.bluetooth.component.AppScreenEvents import edu.stanford.bdh.engagehf.messages.HealthSummaryService +import edu.stanford.spezi.module.account.manager.UserInfo import edu.stanford.spezi.module.account.manager.UserSessionManager import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow @@ -37,12 +38,12 @@ class AppScreenViewModel @Inject constructor( private fun setup() { viewModelScope.launch { - _uiState.update { it -> - val userName = userSessionManager.getUserName()?.takeIf { it.isNotBlank() } - it.copy(accountUiState = it.accountUiState.copy(userName = userName)) - } - userSessionManager.getUserEmail()?.let { email -> - _uiState.update { it.copy(accountUiState = it.accountUiState.copy(email = email)) } + userSessionManager.getUserInfo().let { userInfo -> + _uiState.update { + it.copy( + accountUiState = it.accountUiState.copy(userInfo = userInfo) + ) + } } appScreenEvents.events.collect { event -> val (isExpanded, content) = when (event) { @@ -141,9 +142,8 @@ data class AppUiState( data class AccountUiState( val showDialog: Boolean = false, - val userName: String? = null, + val userInfo: UserInfo = UserInfo("", null), val initials: String? = null, - val email: String = "", val isHealthSummaryLoading: Boolean = false, ) diff --git a/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/manager/UserInfo.kt b/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/manager/UserInfo.kt new file mode 100644 index 000000000..35165ddef --- /dev/null +++ b/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/manager/UserInfo.kt @@ -0,0 +1,6 @@ +package edu.stanford.spezi.module.account.manager + +data class UserInfo( + val email: String, + val name: String?, +) diff --git a/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/manager/UserSessionManager.kt b/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/manager/UserSessionManager.kt index bf8faad68..7ec99b3ee 100644 --- a/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/manager/UserSessionManager.kt +++ b/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/manager/UserSessionManager.kt @@ -21,8 +21,7 @@ interface UserSessionManager { suspend fun getUserState(): UserState fun observeUserState(): Flow fun getUserUid(): String? - fun getUserName(): String? - fun getUserEmail(): String? + fun getUserInfo(): UserInfo } @Singleton @@ -72,9 +71,13 @@ internal class UserSessionManagerImpl @Inject constructor( override fun getUserUid(): String? = firebaseAuth.uid - override fun getUserName(): String? = firebaseAuth.currentUser?.displayName - - override fun getUserEmail(): String? = firebaseAuth.currentUser?.email + override fun getUserInfo(): UserInfo { + val user = firebaseAuth.currentUser ?: error("User not available") + return UserInfo( + email = user.email ?: "", + name = user.displayName?.takeIf { it.isNotBlank() }, + ) + } private suspend fun hasConsented(): Boolean = withContext(ioDispatcher) { runCatching { From 26801e94f5b8a374d3f808c25d7197f26f7fd884 Mon Sep 17 00:00:00 2001 From: Basler182 Date: Sun, 18 Aug 2024 21:07:17 +0200 Subject: [PATCH 22/31] improved catching of intent failures Signed-off-by: Basler182 --- .../engagehf/messages/HealthSummaryService.kt | 20 +++++++++---------- 1 file changed, 9 insertions(+), 11 deletions(-) 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 index 9e042739d..79e0d6725 100644 --- a/app/src/main/kotlin/edu/stanford/bdh/engagehf/messages/HealthSummaryService.kt +++ b/app/src/main/kotlin/edu/stanford/bdh/engagehf/messages/HealthSummaryService.kt @@ -27,26 +27,24 @@ class HealthSummaryService @Inject constructor( const val MIME_TYPE_PDF = "application/pdf" } - suspend fun generateHealthSummaryPdf() { - withContext(ioDispatcher) { - val pdfByteArrayResult = - healthSummaryRepository.findHealthSummaryByUserId() - pdfByteArrayResult.onSuccess { + suspend fun generateHealthSummaryPdf(): Result = withContext(ioDispatcher) { + healthSummaryRepository.findHealthSummaryByUserId() + .mapCatching { val savePdfToFile = savePdfToFile(it) val pdfUri = FileProvider.getUriForFile( context, "${context.packageName}.provider", savePdfToFile ) - val intent = Intent(Intent.ACTION_VIEW) - intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - intent.setDataAndType(pdfUri, MIME_TYPE_PDF) - context.startActivity(intent) + 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 { From c7e3aab36a85c44612e9df8100e756a8e34b230d Mon Sep 17 00:00:00 2001 From: Basler182 Date: Sun, 18 Aug 2024 21:47:27 +0200 Subject: [PATCH 23/31] removed unneeded isBottomSheetExpanded flag Signed-off-by: Basler182 --- .../engagehf/navigation/screens/AppScreen.kt | 13 +++-- .../navigation/screens/AppScreenViewModel.kt | 58 +++++++------------ 2 files changed, 29 insertions(+), 42 deletions(-) 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 358ce7170..7e11b5efb 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 @@ -38,6 +38,8 @@ 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 +62,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) } } BottomSheetScaffold( 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 84f20db34..5b3aadb8f 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 @@ -46,45 +46,30 @@ class AppScreenViewModel @Inject constructor( } } appScreenEvents.events.collect { event -> - val (isExpanded, content) = when (event) { - is AppScreenEvents.Event.NavigateToTab -> { - _uiState.update { - it.copy(selectedItem = event.bottomBarItem) - } - return@collect - } - - AppScreenEvents.Event.NewMeasurementAction -> { - true to BottomSheetContent.NEW_MEASUREMENT_RECEIVED - } - - AppScreenEvents.Event.DoNewMeasurement -> { - true to BottomSheetContent.DO_NEW_MEASUREMENT + 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 - AppScreenEvents.Event.CloseBottomSheet -> { - false to null - } + AppScreenEvents.Event.DoNewMeasurement -> + BottomSheetContent.DO_NEW_MEASUREMENT - AppScreenEvents.Event.WeightDescriptionBottomSheet -> { - true to BottomSheetContent.WEIGHT_DESCRIPTION_INFO - } + AppScreenEvents.Event.WeightDescriptionBottomSheet -> + BottomSheetContent.WEIGHT_DESCRIPTION_INFO - AppScreenEvents.Event.AddWeightRecord -> { - true to BottomSheetContent.ADD_WEIGHT_RECORD - } + AppScreenEvents.Event.AddWeightRecord -> + BottomSheetContent.ADD_WEIGHT_RECORD - AppScreenEvents.Event.AddBloodPressureRecord -> { - false to null + else -> null } - - AppScreenEvents.Event.AddHeartRateRecord -> { - false to null + _uiState.update { + it.copy(bottomSheetContent = bottomSheetContent) } } - _uiState.update { - it.copy(isBottomSheetExpanded = isExpanded, bottomSheetContent = content) - } } } } @@ -95,10 +80,6 @@ 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)) } } @@ -128,6 +109,10 @@ class AppScreenViewModel @Inject constructor( _uiState.update { it.copy(accountUiState = it.accountUiState.copy(showDialog = false)) } /* TODO: Implement sign out */ } + + Action.DismissBottomSheet -> { + _uiState.update { it.copy(bottomSheetContent = null) } + } } } } @@ -135,7 +120,6 @@ 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(), ) @@ -156,10 +140,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( From a837a54385088bedd71f360de58fd50818acda5f Mon Sep 17 00:00:00 2001 From: Basler182 Date: Sun, 18 Aug 2024 22:08:39 +0200 Subject: [PATCH 24/31] removed bottom sheet expanded tests Signed-off-by: Basler182 --- .../engagehf/navigation/screens/AppScreenViewModelTest.kt | 5 ----- 1 file changed, 5 deletions(-) diff --git a/app/src/test/kotlin/edu/stanford/bdh/engagehf/navigation/screens/AppScreenViewModelTest.kt b/app/src/test/kotlin/edu/stanford/bdh/engagehf/navigation/screens/AppScreenViewModelTest.kt index 316471064..cd2c28977 100644 --- a/app/src/test/kotlin/edu/stanford/bdh/engagehf/navigation/screens/AppScreenViewModelTest.kt +++ b/app/src/test/kotlin/edu/stanford/bdh/engagehf/navigation/screens/AppScreenViewModelTest.kt @@ -69,7 +69,6 @@ class AppScreenViewModelTest { // Then val updatedUiState = viewModel.uiState.value - assertThat(updatedUiState.isBottomSheetExpanded).isTrue() assertThat(updatedUiState.bottomSheetContent).isEqualTo(BottomSheetContent.NEW_MEASUREMENT_RECEIVED) } @@ -84,7 +83,6 @@ class AppScreenViewModelTest { // Then val updatedUiState = viewModel.uiState.value - assertThat(updatedUiState.isBottomSheetExpanded).isTrue() assertThat(updatedUiState.bottomSheetContent).isEqualTo(BottomSheetContent.DO_NEW_MEASUREMENT) } @@ -99,7 +97,6 @@ class AppScreenViewModelTest { // Then val updatedUiState = viewModel.uiState.value - assertThat(updatedUiState.isBottomSheetExpanded).isFalse() assertThat(updatedUiState.bottomSheetContent).isNull() } @@ -114,7 +111,6 @@ class AppScreenViewModelTest { // Then val updatedUiState = viewModel.uiState.value - assertThat(updatedUiState.isBottomSheetExpanded).isTrue() assertThat(updatedUiState.bottomSheetContent).isEqualTo(BottomSheetContent.WEIGHT_DESCRIPTION_INFO) } @@ -129,7 +125,6 @@ class AppScreenViewModelTest { // Then val updatedUiState = viewModel.uiState.value - assertThat(updatedUiState.isBottomSheetExpanded).isTrue() assertThat(updatedUiState.bottomSheetContent).isEqualTo(BottomSheetContent.ADD_WEIGHT_RECORD) } } From 61f3feff16b96cfb9aef62b3ef38b33676f130aa Mon Sep 17 00:00:00 2001 From: Basler182 Date: Sun, 18 Aug 2024 22:15:19 +0200 Subject: [PATCH 25/31] fixed VideoSectionDocumentToVideoSectionMapperTest Signed-off-by: Basler182 --- .../VideoSectionDocumentToVideoSectionMapperTest.kt | 6 +++--- .../kotlin/edu/stanford/spezi/core/utils/Typealiases.kt | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) 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..baf53f035 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 @@ -35,7 +35,7 @@ class VideoSectionDocumentToVideoSectionMapperTest { // given val jsonMap: JsonMap = mockk() val document: DocumentSnapshot = mockk { - every { data } returns jsonMap + every { data } returns jsonMap as Map every { getLong("orderIndex") } returns 1L val collectionReference: CollectionReference = mockk { every { get() } returns mockk { @@ -69,7 +69,7 @@ class VideoSectionDocumentToVideoSectionMapperTest { // given val videoJsonMap: JsonMap = mockk() val videoDocument: DocumentSnapshot = mockk { - every { data } returns videoJsonMap + every { data } returns videoJsonMap as Map? every { exists() } returns true every { this@mockk.get("youtubeId") } returns "youtube123" @@ -80,7 +80,7 @@ class VideoSectionDocumentToVideoSectionMapperTest { val videoSectionJsonMap: JsonMap = mockk() val document: DocumentSnapshot = mockk { - every { data } returns videoSectionJsonMap + every { data } returns videoSectionJsonMap as Map? every { getLong("orderIndex") } returns 1L val collectionReference: CollectionReference = mockk { diff --git a/core/utils/src/main/kotlin/edu/stanford/spezi/core/utils/Typealiases.kt b/core/utils/src/main/kotlin/edu/stanford/spezi/core/utils/Typealiases.kt index b6cee33ed..f4740a9a8 100644 --- a/core/utils/src/main/kotlin/edu/stanford/spezi/core/utils/Typealiases.kt +++ b/core/utils/src/main/kotlin/edu/stanford/spezi/core/utils/Typealiases.kt @@ -33,6 +33,6 @@ typealias ComposableBlock = @Composable () -> Unit typealias TestIdentifier = Enum<*> /** - * A typealias for kotlin.Map with String keys and any values + * A typealias for kotlin.Map with generic keys and any values */ typealias JsonMap = Map<*, *> From cc0c2ccdc341d5f0e5df1e8ff8f1823e00848b07 Mon Sep 17 00:00:00 2001 From: Basler182 Date: Sun, 18 Aug 2024 22:42:37 +0200 Subject: [PATCH 26/31] fixed unit tests Signed-off-by: Basler182 --- .../spezi/module/account/manager/UserSessionManager.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/manager/UserSessionManager.kt b/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/manager/UserSessionManager.kt index 7ec99b3ee..63be9dca5 100644 --- a/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/manager/UserSessionManager.kt +++ b/modules/account/src/main/kotlin/edu/stanford/spezi/module/account/manager/UserSessionManager.kt @@ -72,10 +72,10 @@ internal class UserSessionManagerImpl @Inject constructor( override fun getUserUid(): String? = firebaseAuth.uid override fun getUserInfo(): UserInfo { - val user = firebaseAuth.currentUser ?: error("User not available") + val user = firebaseAuth.currentUser return UserInfo( - email = user.email ?: "", - name = user.displayName?.takeIf { it.isNotBlank() }, + email = user?.email ?: "", + name = user?.displayName?.takeIf { it.isNotBlank() }, ) } From bff808ec7dea9dcf9e9b37ad273255a7c5f5867c Mon Sep 17 00:00:00 2001 From: Basler182 Date: Mon, 19 Aug 2024 20:54:51 +0200 Subject: [PATCH 27/31] adjusted domain model changes Signed-off-by: Basler182 --- .../navigation/components/AccountDialog.kt | 23 +++++++------------ .../navigation/screens/AppScreenViewModel.kt | 15 ++++++++---- 2 files changed, 18 insertions(+), 20 deletions(-) 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 index 359af4f55..a65e715e5 100644 --- 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 @@ -45,7 +45,6 @@ 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 -import edu.stanford.spezi.module.account.manager.UserInfo @Composable fun AccountDialog(accountUiState: AccountUiState, onAction: (Action) -> Unit) { @@ -124,11 +123,11 @@ fun AccountDialog(accountUiState: AccountUiState, onAction: (Action) -> Unit) { Spacer(modifier = Modifier.width(Spacings.medium)) Column { VerticalSpacer() - accountUiState.userInfo.name?.let { + accountUiState.name?.let { Text(text = it, style = headlineMedium) VerticalSpacer(height = Spacings.small) } - Text(text = accountUiState.userInfo.email, style = bodyMedium) + Text(text = accountUiState.email, style = bodyMedium) VerticalSpacer() } } @@ -182,23 +181,17 @@ class AppTopBarProvider : PreviewParameterProvider { AccountUiState( initials = "JD", isHealthSummaryLoading = false, - userInfo = UserInfo( - name = "John Doe", - email = "john@doe.de" - ), + name = "John Doe", + email = "john@doe.de" ), AccountUiState( - userInfo = UserInfo( - name = "John Doe", - email = "" - ), + name = "John Doe", + email = "", isHealthSummaryLoading = true ), AccountUiState( - userInfo = UserInfo( - name = null, - email = "john@doe.de" - ) + name = null, + email = "john@doe.de" ) ) } 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 5b3aadb8f..6d4963ae7 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 @@ -8,7 +8,6 @@ import dagger.hilt.android.lifecycle.HiltViewModel import edu.stanford.bdh.engagehf.R import edu.stanford.bdh.engagehf.bluetooth.component.AppScreenEvents import edu.stanford.bdh.engagehf.messages.HealthSummaryService -import edu.stanford.spezi.module.account.manager.UserInfo import edu.stanford.spezi.module.account.manager.UserSessionManager import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow @@ -39,9 +38,14 @@ class AppScreenViewModel @Inject constructor( private fun setup() { viewModelScope.launch { userSessionManager.getUserInfo().let { userInfo -> - _uiState.update { - it.copy( - accountUiState = it.accountUiState.copy(userInfo = userInfo) + _uiState.update { uiState -> + uiState.copy( + accountUiState = uiState.accountUiState.copy( + email = userInfo.email, + name = userInfo.name, + initials = userInfo.name?.split(" ") + ?.mapNotNull { it.firstOrNull()?.toString() }?.joinToString(""), + ) ) } } @@ -126,7 +130,8 @@ data class AppUiState( data class AccountUiState( val showDialog: Boolean = false, - val userInfo: UserInfo = UserInfo("", null), + val email: String = "", + val name: String? = null, val initials: String? = null, val isHealthSummaryLoading: Boolean = false, ) From 960ae3078effda33b3b0675a8733690d59a1f9be Mon Sep 17 00:00:00 2001 From: Basler182 Date: Mon, 19 Aug 2024 21:01:00 +0200 Subject: [PATCH 28/31] tested UserSessionManager Signed-off-by: Basler182 --- .../account/manager/UserSessionManagerTest.kt | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/modules/account/src/test/java/edu/stanford/spezi/module/account/manager/UserSessionManagerTest.kt b/modules/account/src/test/java/edu/stanford/spezi/module/account/manager/UserSessionManagerTest.kt index 00dd51cfc..a4179ef82 100644 --- a/modules/account/src/test/java/edu/stanford/spezi/module/account/manager/UserSessionManagerTest.kt +++ b/modules/account/src/test/java/edu/stanford/spezi/module/account/manager/UserSessionManagerTest.kt @@ -207,6 +207,26 @@ class UserSessionManagerTest { assertThat(result).isEqualTo(uid) } + @Test + fun `it should return the correct user info`() { + // given + val eMail = "test@test.de" + val name = "Test User" + val firebaseUser: FirebaseUser = mockk { + every { email } returns eMail + every { displayName } returns name + } + every { firebaseAuth.currentUser } returns firebaseUser + createUserSessionManager() + + // when + val result = userSessionManager.getUserInfo() + + // then + assertThat(result.email).isEqualTo(eMail) + assertThat(result.name).isEqualTo(name) + } + private suspend fun assertObservedState(userState: UserState, scope: TestScope) { val slot = slot() var capturedState: UserState? = null From 9a3c44bca3a51ad6e5350f9aead78ae5d2a9a5de Mon Sep 17 00:00:00 2001 From: Basler182 Date: Mon, 19 Aug 2024 21:14:41 +0200 Subject: [PATCH 29/31] tested AppScreenViewModel Signed-off-by: Basler182 --- .../screens/AppScreenViewModelTest.kt | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/app/src/test/kotlin/edu/stanford/bdh/engagehf/navigation/screens/AppScreenViewModelTest.kt b/app/src/test/kotlin/edu/stanford/bdh/engagehf/navigation/screens/AppScreenViewModelTest.kt index cd2c28977..9ba08507b 100644 --- a/app/src/test/kotlin/edu/stanford/bdh/engagehf/navigation/screens/AppScreenViewModelTest.kt +++ b/app/src/test/kotlin/edu/stanford/bdh/engagehf/navigation/screens/AppScreenViewModelTest.kt @@ -6,6 +6,7 @@ import edu.stanford.bdh.engagehf.messages.HealthSummaryService import edu.stanford.spezi.core.testing.CoroutineTestRule import edu.stanford.spezi.core.testing.runTestUnconfined import edu.stanford.spezi.module.account.manager.UserSessionManager +import io.mockk.coVerify import io.mockk.every import io.mockk.mockk import kotlinx.coroutines.flow.MutableSharedFlow @@ -127,4 +128,45 @@ class AppScreenViewModelTest { val updatedUiState = viewModel.uiState.value assertThat(updatedUiState.bottomSheetContent).isEqualTo(BottomSheetContent.ADD_WEIGHT_RECORD) } + + @Test + fun `given ShowAccountDialog is received then uiState should be updated`() = + runTestUnconfined { + // Given + val event = Action.ShowAccountDialog(showDialog = true) + + // When + viewModel.onAction(event) + + // Then + val updatedUiState = viewModel.uiState.value + assertThat(updatedUiState.accountUiState.showDialog).isTrue() + } + + @Test + fun `given SignOut is received then uiState should be updated`() = + runTestUnconfined { + // Given + val event = Action.SignOut + + // When + viewModel.onAction(event) + + // Then + val updatedUiState = viewModel.uiState.value + assertThat(updatedUiState.accountUiState.showDialog).isFalse() + } + + @Test + fun `given ShowHealthSummary is received then healthSummaryService should be called`() = + runTestUnconfined { + // Given + val event = Action.ShowHealthSummary + + // When + viewModel.onAction(event) + + // Then + coVerify { healthSummaryService.generateHealthSummaryPdf() } + } } From ff8168f9e82c96b54d597ad34dc5c619a5b4352b Mon Sep 17 00:00:00 2001 From: Basler182 Date: Sat, 24 Aug 2024 10:23:32 +0200 Subject: [PATCH 30/31] added deletion of health summary Signed-off-by: Basler182 --- .../bdh/engagehf/messages/HealthSummaryService.kt | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) 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 index 79e0d6725..cd6bb9ad9 100644 --- a/app/src/main/kotlin/edu/stanford/bdh/engagehf/messages/HealthSummaryService.kt +++ b/app/src/main/kotlin/edu/stanford/bdh/engagehf/messages/HealthSummaryService.kt @@ -23,7 +23,7 @@ class HealthSummaryService @Inject constructor( private val logger by speziLogger() companion object { - const val FILE_NAME = "health_summary.pdf" + const val FILE_NAME = "engage_hf_health_summary.pdf" const val MIME_TYPE_PDF = "application/pdf" } @@ -61,4 +61,17 @@ class HealthSummaryService @Inject constructor( logger.i { "PDF saved to file: ${pdfFile.absolutePath}" } return pdfFile } + + fun deletePdfFile(): Result { + return runCatching { + logger.i { "Deleting PDF file" } + val storageDir = + Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) + val pdfFile = File(storageDir, FILE_NAME) + if (pdfFile.exists()) { + pdfFile.delete() + logger.i { "PDF file deleted: ${pdfFile.absolutePath}" } + } + } + } } From 547525b1df338d7552d636a784599dde5a308810 Mon Sep 17 00:00:00 2001 From: Eldi Cano Date: Sun, 25 Aug 2024 21:04:59 +0200 Subject: [PATCH 31/31] minor adjustments --- .../engagehf/bluetooth/BluetoothViewModel.kt | 65 ++++++++++--------- .../messages/HealthSummaryRepository.kt | 10 ++- .../engagehf/messages/HealthSummaryService.kt | 21 ++---- .../navigation/screens/AppScreenViewModel.kt | 20 +++--- ...SectionDocumentToVideoSectionMapperTest.kt | 17 +++-- .../messages/HealthSummaryRepositoryTest.kt | 8 +-- .../stanford/spezi/core/utils/Typealiases.kt | 4 +- 7 files changed, 65 insertions(+), 80 deletions(-) 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 6f0b5cbdb..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 @@ -157,44 +157,45 @@ class BluetoothViewModel @Inject internal constructor( is Action.MessageItemClicked -> { viewModelScope.launch { - val mappingResult = uiStateMapper.mapMessagesAction(action.message.action) - if (mappingResult.isSuccess) { - val messageId = action.message.id - when (val mappedAction = mappingResult.getOrNull()!!) { - is MessagesAction.HealthSummaryAction -> { - handleHealthSummaryAction(messageId) - } + 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.MeasurementsAction -> { - appScreenEvents.emit(AppScreenEvents.Event.DoNewMeasurement) - } + is MessagesAction.MeasurementsAction -> { + appScreenEvents.emit(AppScreenEvents.Event.DoNewMeasurement) + } - is MessagesAction.MedicationsAction -> { - appScreenEvents.emit( - AppScreenEvents.Event.NavigateToTab( - BottomBarItem.MEDICATION + is MessagesAction.MedicationsAction -> { + appScreenEvents.emit( + AppScreenEvents.Event.NavigateToTab( + BottomBarItem.MEDICATION + ) ) - ) - } + } - is MessagesAction.QuestionnaireAction -> { - navigator.navigateTo( - AppNavigationEvent.QuestionnaireScreen( - mappedAction.questionnaireId + is MessagesAction.QuestionnaireAction -> { + navigator.navigateTo( + AppNavigationEvent.QuestionnaireScreen( + mappedAction.questionnaireId + ) ) - ) - } + } - is MessagesAction.VideoSectionAction -> { - viewModelScope.launch { - handleVideoSectionAction(mappedAction) + is MessagesAction.VideoSectionAction -> { + viewModelScope.launch { + handleVideoSectionAction(mappedAction) + } } } + messageRepository.completeMessage(messageId = messageId) } - messageRepository.completeMessage(messageId = messageId) - } else { - logger.e { "Error while mapping action: ${mappingResult.exceptionOrNull()}" } - } } } @@ -204,10 +205,10 @@ class BluetoothViewModel @Inject internal constructor( } } - private suspend fun handleVideoSectionAction(mappedAction: MessagesAction.VideoSectionAction) { + private suspend fun handleVideoSectionAction(messageAction: MessagesAction.VideoSectionAction) { engageEducationRepository.getVideoBySectionAndVideoId( - mappedAction.videoSectionVideo.videoSectionId, - mappedAction.videoSectionVideo.videoId + messageAction.videoSectionVideo.videoSectionId, + messageAction.videoSectionVideo.videoId ).getOrNull()?.let { video -> navigator.navigateTo( EducationNavigationEvent.VideoSectionClicked( 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 index 55994a02e..2a0959283 100644 --- a/app/src/main/kotlin/edu/stanford/bdh/engagehf/messages/HealthSummaryRepository.kt +++ b/app/src/main/kotlin/edu/stanford/bdh/engagehf/messages/HealthSummaryRepository.kt @@ -17,17 +17,15 @@ class HealthSummaryRepository @Inject constructor( ) { private val logger by speziLogger() - suspend fun findHealthSummaryByUserId(): Result = withContext(ioDispatcher) { + suspend fun getHealthSummary(): Result = withContext(ioDispatcher) { runCatching { val uid = userSessionManager.getUserUid() - ?: throw IllegalStateException("User not authenticated") + ?: error("User not authenticated") val result = firebaseFunctions.getHttpsCallable("exportHealthSummary") .call(mapOf("userId" to uid)) .await() - val resultData = result.data as? JsonMap - ?: throw IllegalStateException("Invalid function response") - val pdfBase64 = resultData["content"] as? String - ?: throw IllegalStateException("No content found in function response") + 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 { 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 index cd6bb9ad9..ed84519b6 100644 --- a/app/src/main/kotlin/edu/stanford/bdh/engagehf/messages/HealthSummaryService.kt +++ b/app/src/main/kotlin/edu/stanford/bdh/engagehf/messages/HealthSummaryService.kt @@ -22,13 +22,8 @@ class HealthSummaryService @Inject constructor( ) { private val logger by speziLogger() - companion object { - const val FILE_NAME = "engage_hf_health_summary.pdf" - const val MIME_TYPE_PDF = "application/pdf" - } - suspend fun generateHealthSummaryPdf(): Result = withContext(ioDispatcher) { - healthSummaryRepository.findHealthSummaryByUserId() + healthSummaryRepository.getHealthSummary() .mapCatching { val savePdfToFile = savePdfToFile(it) val pdfUri = FileProvider.getUriForFile( @@ -62,16 +57,8 @@ class HealthSummaryService @Inject constructor( return pdfFile } - fun deletePdfFile(): Result { - return runCatching { - logger.i { "Deleting PDF file" } - val storageDir = - Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) - val pdfFile = File(storageDir, FILE_NAME) - if (pdfFile.exists()) { - pdfFile.delete() - logger.i { "PDF file deleted: ${pdfFile.absolutePath}" } - } - } + 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/navigation/screens/AppScreenViewModel.kt b/app/src/main/kotlin/edu/stanford/bdh/engagehf/navigation/screens/AppScreenViewModel.kt index f05a9278a..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 @@ -37,18 +37,18 @@ class AppScreenViewModel @Inject constructor( private fun setup() { viewModelScope.launch { - userSessionManager.getUserInfo().let { userInfo -> - _uiState.update { uiState -> - uiState.copy( - accountUiState = uiState.accountUiState.copy( - email = userInfo.email, - name = userInfo.name, - initials = userInfo.name?.split(" ") - ?.mapNotNull { it.firstOrNull()?.toString() }?.joinToString(""), - ) + _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(""), ) - } + ) } + appScreenEvents.events.collect { event -> if (event is AppScreenEvents.Event.NavigateToTab) { _uiState.update { 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 baf53f035..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>() val document: DocumentSnapshot = mockk { - every { data } returns jsonMap as Map + every { data } returns jsonData every { getLong("orderIndex") } returns 1L val collectionReference: CollectionReference = mockk { every { get() } returns mockk { @@ -54,8 +53,8 @@ class VideoSectionDocumentToVideoSectionMapperTest { every { collection("videos") } returns collectionReference } } - every { localizedMapReader.get("title", jsonMap) } returns "Test Title" - every { localizedMapReader.get("description", jsonMap) } returns "Test Description" + every { localizedMapReader.get("title", jsonData) } returns "Test Title" + every { localizedMapReader.get("description", jsonData) } returns "Test Description" // when val result = mapper.map(document) @@ -67,9 +66,9 @@ class VideoSectionDocumentToVideoSectionMapperTest { @Test fun `it should return a valid VideoSection`() = runTest { // given - val videoJsonMap: JsonMap = mockk() + val videoJsonMap: Map = mockk() val videoDocument: DocumentSnapshot = mockk { - every { data } returns videoJsonMap as Map? + every { data } returns videoJsonMap every { exists() } returns true every { this@mockk.get("youtubeId") } returns "youtube123" @@ -78,9 +77,9 @@ class VideoSectionDocumentToVideoSectionMapperTest { every { localizedMapReader.get("title", videoJsonMap) } returns "Video Title" every { localizedMapReader.get("description", videoJsonMap) } returns "Video Description" - val videoSectionJsonMap: JsonMap = mockk() + val videoSectionJsonMap: Map = mockk() val document: DocumentSnapshot = mockk { - every { data } returns videoSectionJsonMap as Map? + every { data } returns videoSectionJsonMap every { getLong("orderIndex") } returns 1L val collectionReference: CollectionReference = mockk { diff --git a/app/src/test/kotlin/edu/stanford/bdh/engagehf/messages/HealthSummaryRepositoryTest.kt b/app/src/test/kotlin/edu/stanford/bdh/engagehf/messages/HealthSummaryRepositoryTest.kt index bd7b33d15..22e504e93 100644 --- a/app/src/test/kotlin/edu/stanford/bdh/engagehf/messages/HealthSummaryRepositoryTest.kt +++ b/app/src/test/kotlin/edu/stanford/bdh/engagehf/messages/HealthSummaryRepositoryTest.kt @@ -25,13 +25,13 @@ class HealthSummaryRepositoryTest { ) @Test - fun `findHealthSummaryByUserId returns failure when user is not authenticated`() = + fun `getHealthSummary returns failure when user is not authenticated`() = runTestUnconfined { // given every { userSessionManager.getUserUid() } returns null // when - val result = repository.findHealthSummaryByUserId() + val result = repository.getHealthSummary() // then assertThat(result.isFailure).isTrue() @@ -39,7 +39,7 @@ class HealthSummaryRepositoryTest { } @Test - fun `findHealthSummaryByUserId returns failure when function call fails`() = runTestUnconfined { + fun `getHealthSummary returns failure when function call fails`() = runTestUnconfined { // given val exception = Exception("Function call failed") coEvery { @@ -47,7 +47,7 @@ class HealthSummaryRepositoryTest { } throws exception // when - val result = repository.findHealthSummaryByUserId() + val result = repository.getHealthSummary() // then assertThat(result.isFailure).isTrue() diff --git a/core/utils/src/main/kotlin/edu/stanford/spezi/core/utils/Typealiases.kt b/core/utils/src/main/kotlin/edu/stanford/spezi/core/utils/Typealiases.kt index f4740a9a8..fee80057f 100644 --- a/core/utils/src/main/kotlin/edu/stanford/spezi/core/utils/Typealiases.kt +++ b/core/utils/src/main/kotlin/edu/stanford/spezi/core/utils/Typealiases.kt @@ -33,6 +33,6 @@ typealias ComposableBlock = @Composable () -> Unit typealias TestIdentifier = Enum<*> /** - * A typealias for kotlin.Map with generic keys and any values + * A typealias for kotlin.Map with String keys and any values */ -typealias JsonMap = Map<*, *> +typealias JsonMap = Map