Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions datacapture/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ dependencies {
implementation(libs.androidx.core)
implementation(libs.androidx.fragment)
implementation(libs.androidx.lifecycle.viewmodel)

implementation(libs.glide)
implementation(libs.kotlin.stdlib)
implementation(libs.kotlinx.coroutines.core)
Expand All @@ -113,6 +114,7 @@ dependencies {
implementation(libs.androidx.compose.ui)
implementation(libs.androidx.compose.ui.graphics)
implementation(libs.androidx.compose.ui.tooling.preview)
implementation(libs.androidx.compose.material.icons.core)
implementation(libs.androidx.compose.material3)
implementation(libs.androidx.navigation.compose)
implementation(libs.accompanist.themeadapter.material3)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,12 @@

package com.google.android.fhir.datacapture

import android.content.Context
import android.net.Uri
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.annotation.VisibleForTesting
import androidx.appcompat.view.ContextThemeWrapper
import androidx.compose.ui.platform.ComposeView
Expand All @@ -31,17 +31,14 @@ import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.fragment.app.setFragmentResult
import androidx.fragment.app.viewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.google.android.fhir.datacapture.QuestionnaireFragment.Companion.EXTRA_QUESTIONNAIRE_JSON_STRING
import com.google.android.fhir.datacapture.QuestionnaireFragment.Companion.EXTRA_QUESTIONNAIRE_JSON_URI
import com.google.android.fhir.datacapture.QuestionnaireFragment.Companion.EXTRA_QUESTIONNAIRE_RESPONSE_JSON_STRING
import com.google.android.fhir.datacapture.QuestionnaireFragment.Companion.EXTRA_QUESTIONNAIRE_RESPONSE_JSON_URI
import com.google.android.fhir.datacapture.validation.Invalid
import com.google.android.fhir.datacapture.views.NavigationViewHolder
import com.google.android.fhir.datacapture.validation.ValidationResult
import com.google.android.fhir.datacapture.views.factories.QuestionnaireItemViewHolderFactory
import com.google.android.material.progressindicator.LinearProgressIndicator
import kotlinx.coroutines.launch
import org.hl7.fhir.r4.model.Questionnaire
import timber.log.Timber
Expand All @@ -66,10 +63,24 @@ class QuestionnaireFragment : Fragment() {
@VisibleForTesting
val questionnaireItemViewHolderFactoryMatchersProvider:
QuestionnaireItemViewHolderFactoryMatchersProvider by lazy {
requireArguments().getString(EXTRA_MATCHERS_FACTORY)?.let {
DataCapture.getConfiguration(requireContext())
.questionnaireItemViewHolderFactoryMatchersProviderFactory
?.get(it)
requireArguments().getString(EXTRA_MATCHERS_FACTORY)?.let { factoryKey ->
val provider =
DataCapture.getConfiguration(requireContext())
.questionnaireItemViewHolderFactoryMatchersProviderFactory
?.get(factoryKey)

provider?.let {
object : QuestionnaireItemViewHolderFactoryMatchersProvider() {
override fun get(): List<QuestionnaireItemViewHolderFactoryMatcher> {
return it.get().map { matcher ->
QuestionnaireItemViewHolderFactoryMatcher(
factory = matcher.factory,
matches = matcher.matches,
)
}
}
}
}
}
?: EmptyQuestionnaireItemViewHolderFactoryMatchersProviderImpl
}
Expand All @@ -80,163 +91,65 @@ class QuestionnaireFragment : Fragment() {
container: ViewGroup?,
savedInstanceState: Bundle?,
): View {
inflater.context.obtainStyledAttributes(R.styleable.QuestionnaireTheme).use {
val themeId =
it.getResourceId(
// Use the custom questionnaire theme if it is specified
R.styleable.QuestionnaireTheme_questionnaire_theme,
// Otherwise, use the default questionnaire theme
R.style.Theme_Questionnaire,
val themeId = getQuestionnaireThemeId(inflater.context)
val themedContext = ContextThemeWrapper(inflater.context, themeId)

return ComposeView(themedContext).apply {
setContent {
QuestionnaireScreen(
viewModel = viewModel,
matchersProvider = questionnaireItemViewHolderFactoryMatchersProvider,
)
return inflater
.cloneInContext(ContextThemeWrapper(inflater.context, themeId))
.inflate(R.layout.questionnaire_fragment, container, false)
}
}
}

/** @suppress */
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val questionnaireEditComposeView =
view.findViewById<ComposeView>(R.id.questionnaire_edit_compose_view)
val questionnaireReviewComposeView =
view.findViewById<ComposeView>(R.id.questionnaire_review_recycler_view)
val questionnaireTitle = view.findViewById<TextView>(R.id.questionnaire_title)

// This container frame floats at the bottom of the view to make navigation controls visible at
// all times when the user scrolls. Use
// [QuestionnaireFragment.Builder.setShowNavigationInDefaultLongScroll] to disable this.
val bottomNavContainerFrame = view.findViewById<View>(R.id.bottom_nav_container_frame)
super.onViewCreated(view, savedInstanceState)
setupViewModelCallbacks()
setupFragmentResultListeners()
}

private fun setupViewModelCallbacks() {
viewModel.setOnCancelButtonClickListener {
QuestionnaireCancelDialogFragment()
.show(requireActivity().supportFragmentManager, QuestionnaireCancelDialogFragment.TAG)
}

viewModel.setOnSubmitButtonClickListener {
lifecycleScope.launch {
viewModel.validateQuestionnaireAndUpdateUI().let { validationMap ->
if (validationMap.values.flatten().filterIsInstance<Invalid>().isEmpty()) {
setFragmentResult(SUBMIT_REQUEST_KEY, Bundle.EMPTY)
} else {
val errorViewModel: QuestionnaireValidationErrorViewModel by activityViewModels()
errorViewModel.setQuestionnaireAndValidation(viewModel.questionnaire, validationMap)
val validationErrorMessageDialog = QuestionnaireValidationErrorMessageDialogFragment()
if (requireArguments().containsKey(EXTRA_SHOW_SUBMIT_ANYWAY_BUTTON)) {
validationErrorMessageDialog.arguments =
Bundle().apply {
putBoolean(
EXTRA_SHOW_SUBMIT_ANYWAY_BUTTON,
requireArguments()
.getBoolean(
EXTRA_SHOW_SUBMIT_ANYWAY_BUTTON,
),
)
}
}
validationErrorMessageDialog.show(
requireActivity().supportFragmentManager,
QuestionnaireValidationErrorMessageDialogFragment.TAG,
)
showValidationErrorDialog(validationMap)
}
}
}
}
val questionnaireProgressIndicator: LinearProgressIndicator =
view.findViewById(R.id.questionnaire_progress_indicator)

val reviewModeEditButton =
view.findViewById<View>(R.id.review_mode_edit_button).apply {
setOnClickListener { viewModel.setReviewMode(false) }
}
}

// Listen to updates from the view model.
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.CREATED) {
viewModel.questionnaireStateFlow.collect { state ->
when (val displayMode = state.displayMode) {
is DisplayMode.ReviewMode -> {
// Set items

questionnaireReviewComposeView.visibility = View.VISIBLE
questionnaireReviewComposeView.setContent { QuestionnaireReviewList(state.items) }
questionnaireEditComposeView.visibility = View.GONE

reviewModeEditButton.visibility =
if (displayMode.showEditButton) {
View.VISIBLE
} else {
View.GONE
}
questionnaireTitle.visibility = View.VISIBLE
questionnaireTitle.text = getString(R.string.questionnaire_review_mode_title)

// Set bottom navigation
if (state.bottomNavItem != null) {
bottomNavContainerFrame.visibility = View.VISIBLE
NavigationViewHolder(bottomNavContainerFrame)
.bind(state.bottomNavItem.questionnaireNavigationUIState)
} else {
bottomNavContainerFrame.visibility = View.GONE
}

// Hide progress indicator
questionnaireProgressIndicator.visibility = View.GONE
}
is DisplayMode.EditMode -> {
// Set items
questionnaireReviewComposeView.visibility = View.GONE
questionnaireEditComposeView.setContent {
QuestionnaireEditList(
items = state.items,
displayMode = displayMode,
questionnaireItemViewHolderMatchers =
questionnaireItemViewHolderFactoryMatchersProvider.get(),
onUpdateProgressIndicator = { currentPage, totalCount ->
questionnaireProgressIndicator.updateProgressIndicator(
calculateProgressPercentage(
count = (currentPage + 1),
totalCount = totalCount,
),
)
},
)
}
questionnaireEditComposeView.visibility = View.VISIBLE
reviewModeEditButton.visibility = View.GONE
questionnaireTitle.visibility = View.GONE

// Set bottom navigation
if (state.bottomNavItem != null) {
bottomNavContainerFrame.visibility = View.VISIBLE
NavigationViewHolder(bottomNavContainerFrame)
.bind(state.bottomNavItem.questionnaireNavigationUIState)
} else {
bottomNavContainerFrame.visibility = View.GONE
}

// Set progress indicator
questionnaireProgressIndicator.visibility = View.VISIBLE
if (displayMode.pagination.isPaginated) {
questionnaireProgressIndicator.updateProgressIndicator(
calculateProgressPercentage(
count =
(displayMode.pagination.currentPageIndex +
1), // incremented by 1 due to initialPageIndex starts with 0.
totalCount = displayMode.pagination.pages.size,
),
)
}
}
is DisplayMode.InitMode -> {
questionnaireReviewComposeView.visibility = View.GONE
questionnaireEditComposeView.visibility = View.GONE
questionnaireProgressIndicator.visibility = View.GONE
reviewModeEditButton.visibility = View.GONE
bottomNavContainerFrame.visibility = View.GONE
}
}
private fun showValidationErrorDialog(validationMap: Map<String, List<ValidationResult>>) {
val errorViewModel: QuestionnaireValidationErrorViewModel by activityViewModels()
errorViewModel.setQuestionnaireAndValidation(viewModel.questionnaire, validationMap)
val validationErrorMessageDialog = QuestionnaireValidationErrorMessageDialogFragment()
if (requireArguments().containsKey(EXTRA_SHOW_SUBMIT_ANYWAY_BUTTON)) {
validationErrorMessageDialog.arguments =
Bundle().apply {
putBoolean(
EXTRA_SHOW_SUBMIT_ANYWAY_BUTTON,
requireArguments().getBoolean(EXTRA_SHOW_SUBMIT_ANYWAY_BUTTON),
)
}
}
}
validationErrorMessageDialog.show(
requireActivity().supportFragmentManager,
QuestionnaireValidationErrorMessageDialogFragment.TAG,
)
}

private fun setupFragmentResultListeners() {
requireActivity().supportFragmentManager.setFragmentResultListener(
QuestionnaireValidationErrorMessageDialogFragment.RESULT_CALLBACK,
viewLifecycleOwner,
Expand All @@ -251,12 +164,10 @@ class QuestionnaireFragment : Fragment() {
QuestionnaireValidationErrorMessageDialogFragment.RESULT_VALUE_SUBMIT -> {
setFragmentResult(SUBMIT_REQUEST_KEY, Bundle.EMPTY)
}
else ->
Timber.e(
"Unknown fragment result $result",
)
else -> Timber.e("Unknown fragment result $result")
}
}

/** Listen to Button Clicks from the Cancel Dialog */
requireActivity().supportFragmentManager.setFragmentResultListener(
QuestionnaireCancelDialogFragment.REQUEST_KEY,
Expand All @@ -269,17 +180,18 @@ class QuestionnaireFragment : Fragment() {
QuestionnaireCancelDialogFragment.RESULT_YES -> {
setFragmentResult(CANCEL_REQUEST_KEY, Bundle.EMPTY)
}
else ->
Timber.e(
"Unknown fragment result $result",
)
else -> Timber.e("Unknown fragment result $result")
}
}
}

/** Calculates the progress percentage from given [count] and [totalCount] values. */
internal fun calculateProgressPercentage(count: Int, totalCount: Int): Int {
return if (totalCount == 0) 0 else (count * 100 / totalCount)
private fun getQuestionnaireThemeId(context: Context): Int {
return context.obtainStyledAttributes(R.styleable.QuestionnaireTheme).use {
it.getResourceId(
R.styleable.QuestionnaireTheme_questionnaire_theme,
R.style.Theme_Questionnaire,
)
}
}

/**
Expand All @@ -292,7 +204,6 @@ class QuestionnaireFragment : Fragment() {

/** Helper to create [QuestionnaireFragment] with appropriate [Bundle] arguments. */
class Builder {

private val args = mutableListOf<Pair<String, Any>>()

/**
Expand Down Expand Up @@ -587,15 +498,3 @@ class QuestionnaireFragment : Fragment() {
override fun get() = emptyList<QuestionnaireItemViewHolderFactoryMatcher>()
}
}

/**
* Updates the [LinearProgressIndicator] progress with given value.
*
* This method will also set max value of [LinearProgressIndicator] to 100.
*
* @param progress The new progress [Integer] value between 0 to 100.
*/
internal fun LinearProgressIndicator.updateProgressIndicator(progress: Int) {
setProgress(progress)
max = 100
}
Loading