Skip to content

Commit

Permalink
Merge remote-tracking branch 'upstream/master' into 2561-fix-sqlite-c…
Browse files Browse the repository at this point in the history
…rash
  • Loading branch information
LZRS committed Jun 24, 2024
2 parents c73037d + 5dfb936 commit 0264816
Show file tree
Hide file tree
Showing 11 changed files with 669 additions and 244 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,7 @@ internal sealed interface QuestionnaireAdapterItem {
val responses: List<QuestionnaireResponse.QuestionnaireResponseItemComponent>,
val title: String,
) : QuestionnaireAdapterItem

data class Navigation(val questionnaireNavigationUIState: QuestionnaireNavigationUIState) :
QuestionnaireAdapterItem
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import androidx.recyclerview.widget.RecyclerView
import com.google.android.fhir.datacapture.contrib.views.PhoneNumberViewHolderFactory
import com.google.android.fhir.datacapture.extensions.inflate
import com.google.android.fhir.datacapture.extensions.itemControl
import com.google.android.fhir.datacapture.views.NavigationViewHolder
import com.google.android.fhir.datacapture.views.QuestionnaireViewItem
import com.google.android.fhir.datacapture.views.factories.AttachmentViewHolderFactory
import com.google.android.fhir.datacapture.views.factories.AutoCompleteViewHolderFactory
Expand Down Expand Up @@ -69,6 +70,13 @@ internal class QuestionnaireEditAdapter(
),
)
}
ViewType.Type.NAVIGATION -> {
ViewHolder.NavigationHolder(
NavigationViewHolder(
parent.inflate(R.layout.pagination_navigation_view),
),
)
}
}
}

Expand Down Expand Up @@ -122,6 +130,10 @@ internal class QuestionnaireEditAdapter(
holder as ViewHolder.RepeatedGroupHeaderHolder
holder.viewHolder.bind(item)
}
is QuestionnaireAdapterItem.Navigation -> {
holder as ViewHolder.NavigationHolder
holder.viewHolder.bind(item.questionnaireNavigationUIState)
}
}
}

Expand All @@ -143,6 +155,10 @@ internal class QuestionnaireEditAdapter(
// All of the repeated group headers will be rendered identically
subtype = 0
}
is QuestionnaireAdapterItem.Navigation -> {
type = ViewType.Type.NAVIGATION
subtype = 0xFFFFFF
}
}
return ViewType.from(type = type, subtype = subtype).viewType
}
Expand Down Expand Up @@ -174,6 +190,7 @@ internal class QuestionnaireEditAdapter(
enum class Type {
QUESTION,
REPEATED_GROUP_HEADER,
NAVIGATION,
}
}

Expand Down Expand Up @@ -269,6 +286,8 @@ internal class QuestionnaireEditAdapter(

class RepeatedGroupHeaderHolder(val viewHolder: RepeatedGroupHeaderItemViewHolder) :
ViewHolder(viewHolder.itemView)

class NavigationHolder(val viewHolder: NavigationViewHolder) : ViewHolder(viewHolder.itemView)
}

internal companion object {
Expand Down Expand Up @@ -296,6 +315,7 @@ internal object DiffCallbacks {
newItem is QuestionnaireAdapterItem.RepeatedGroupHeader &&
oldItem.index == newItem.index
}
is QuestionnaireAdapterItem.Navigation -> newItem is QuestionnaireAdapterItem.Navigation
}

override fun areContentsTheSame(
Expand All @@ -311,6 +331,10 @@ internal object DiffCallbacks {
newItem is QuestionnaireAdapterItem.RepeatedGroupHeader &&
oldItem.responses == newItem.responses
}
is QuestionnaireAdapterItem.Navigation -> {
newItem is QuestionnaireAdapterItem.Navigation &&
oldItem.questionnaireNavigationUIState == newItem.questionnaireNavigationUIState
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import androidx.annotation.VisibleForTesting
import androidx.appcompat.view.ContextThemeWrapper
import androidx.core.content.res.use
Expand All @@ -34,6 +33,7 @@ import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.android.fhir.datacapture.validation.Invalid
import com.google.android.fhir.datacapture.views.NavigationViewHolder
import com.google.android.fhir.datacapture.views.factories.QuestionnaireItemViewHolderFactory
import com.google.android.material.progressindicator.LinearProgressIndicator
import kotlinx.coroutines.launch
Expand Down Expand Up @@ -94,58 +94,44 @@ class QuestionnaireFragment : Fragment() {
view.findViewById<RecyclerView>(R.id.questionnaire_edit_recycler_view)
val questionnaireReviewRecyclerView =
view.findViewById<RecyclerView>(R.id.questionnaire_review_recycler_view)
val paginationPreviousButton = view.findViewById<View>(R.id.pagination_previous_button)
paginationPreviousButton.setOnClickListener { viewModel.goToPreviousPage() }
val paginationNextButton = view.findViewById<View>(R.id.pagination_next_button)
paginationNextButton.setOnClickListener { viewModel.goToNextPage() }
view.findViewById<Button>(R.id.cancel_questionnaire).setOnClickListener {

// 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)

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

view
.findViewById<Button>(R.id.submit_questionnaire)
.apply {
text =
requireArguments()
.getString(EXTRA_SUBMIT_BUTTON_TEXT, getString(R.string.submit_questionnaire))
}
.setOnClickListener {
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)
QuestionnaireValidationErrorMessageDialogFragment()
.show(
requireActivity().supportFragmentManager,
QuestionnaireValidationErrorMessageDialogFragment.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)
QuestionnaireValidationErrorMessageDialogFragment()
.show(
requireActivity().supportFragmentManager,
QuestionnaireValidationErrorMessageDialogFragment.TAG,
)
}
}
}
}
val questionnaireProgressIndicator: LinearProgressIndicator =
view.findViewById(R.id.questionnaire_progress_indicator)
val questionnaireEditAdapter =
QuestionnaireEditAdapter(questionnaireItemViewHolderFactoryMatchersProvider.get())
val questionnaireReviewAdapter = QuestionnaireReviewAdapter()

val submitButton = requireView().findViewById<Button>(R.id.submit_questionnaire)
val cancelButton = requireView().findViewById<Button>(R.id.cancel_questionnaire)

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

val reviewModeButton =
view.findViewById<View>(R.id.review_mode_button).apply {
setOnClickListener { viewModel.setReviewMode(true) }
}

questionnaireEditRecyclerView.adapter = questionnaireEditAdapter
val linearLayoutManager = LinearLayoutManager(view.context)
questionnaireEditRecyclerView.layoutManager = linearLayoutManager
Expand All @@ -163,23 +149,20 @@ class QuestionnaireFragment : Fragment() {
// Set items
questionnaireEditRecyclerView.visibility = View.GONE
questionnaireReviewAdapter.submitList(
state.items.filterIsInstance<QuestionnaireAdapterItem.Question>(),
state.items,
)
questionnaireReviewRecyclerView.visibility = View.VISIBLE

// Set button visibility
submitButton.visibility = if (displayMode.showSubmitButton) View.VISIBLE else View.GONE
cancelButton.visibility = if (displayMode.showCancelButton) View.VISIBLE else View.GONE

reviewModeButton.visibility = View.GONE
reviewModeEditButton.visibility =
if (displayMode.showEditButton) {
View.VISIBLE
} else {
View.GONE
}
paginationPreviousButton.visibility = View.GONE
paginationNextButton.visibility = View.GONE

// Set bottom navigation
bottomNavContainerFrame.visibility = View.VISIBLE
NavigationViewHolder(bottomNavContainerFrame)
.bind(state.bottomNavItems.single().questionnaireNavigationUIState)

// Hide progress indicator
questionnaireProgressIndicator.visibility = View.GONE
Expand All @@ -189,25 +172,12 @@ class QuestionnaireFragment : Fragment() {
questionnaireReviewRecyclerView.visibility = View.GONE
questionnaireEditAdapter.submitList(state.items)
questionnaireEditRecyclerView.visibility = View.VISIBLE

// Set button visibility
submitButton.visibility =
if (displayMode.pagination.showSubmitButton) View.VISIBLE else View.GONE
cancelButton.visibility =
if (displayMode.pagination.showCancelButton) View.VISIBLE else View.GONE
reviewModeButton.visibility =
if (displayMode.pagination.showReviewButton) View.VISIBLE else View.GONE
reviewModeEditButton.visibility = View.GONE

if (displayMode.pagination.isPaginated) {
paginationPreviousButton.visibility =
if (displayMode.pagination.hasPreviousPage) View.VISIBLE else View.GONE
paginationNextButton.visibility =
if (displayMode.pagination.hasNextPage) View.VISIBLE else View.GONE
} else {
paginationPreviousButton.visibility = View.GONE
paginationNextButton.visibility = View.GONE
}
// Set bottom navigation
bottomNavContainerFrame.visibility = View.VISIBLE
NavigationViewHolder(bottomNavContainerFrame)
.bind(state.bottomNavItems.single().questionnaireNavigationUIState)

// Set progress indicator
questionnaireProgressIndicator.visibility = View.VISIBLE
Expand Down Expand Up @@ -241,13 +211,9 @@ class QuestionnaireFragment : Fragment() {
is DisplayMode.InitMode -> {
questionnaireReviewRecyclerView.visibility = View.GONE
questionnaireEditRecyclerView.visibility = View.GONE
paginationPreviousButton.visibility = View.GONE
paginationNextButton.visibility = View.GONE
questionnaireProgressIndicator.visibility = View.GONE
submitButton.visibility = View.GONE
cancelButton.visibility = View.GONE
reviewModeButton.visibility = View.GONE
reviewModeEditButton.visibility = View.GONE
bottomNavContainerFrame.visibility = View.GONE
}
}
}
Expand Down Expand Up @@ -425,6 +391,14 @@ class QuestionnaireFragment : Fragment() {
*/
fun setShowCancelButton(value: Boolean) = apply { args.add(EXTRA_SHOW_CANCEL_BUTTON to value) }

/**
* A [Boolean] extra to show questionnaire page as a default/long scroll with the
* previous/next/submit buttons anchored to bottom/end of page. Default is false.
*/
fun setShowNavigationInDefaultLongScroll(value: Boolean) = apply {
args.add(EXTRA_SHOW_NAVIGATION_IN_DEFAULT_LONG_SCROLL to value)
}

@VisibleForTesting fun buildArgs() = bundleOf(*args.toTypedArray())

/** @return A [QuestionnaireFragment] with provided [Bundle] arguments. */
Expand Down Expand Up @@ -524,6 +498,9 @@ class QuestionnaireFragment : Fragment() {

internal const val EXTRA_SUBMIT_BUTTON_TEXT = "submit-button-text"

internal const val EXTRA_SHOW_NAVIGATION_IN_DEFAULT_LONG_SCROLL =
"show-navigation-in-default-long-scroll"

fun builder() = Builder()
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
* Copyright 2023-2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.google.android.fhir.datacapture

sealed class QuestionnaireNavigationViewUIState(val isShown: Boolean, val isEnabled: Boolean) {
data object Hidden : QuestionnaireNavigationViewUIState(isShown = false, isEnabled = false)

data class Enabled(val labelText: String? = null, val onClickAction: () -> Unit) :
QuestionnaireNavigationViewUIState(isShown = true, isEnabled = true)
}

data class QuestionnaireNavigationUIState(
val navPrevious: QuestionnaireNavigationViewUIState = QuestionnaireNavigationViewUIState.Hidden,
val navNext: QuestionnaireNavigationViewUIState = QuestionnaireNavigationViewUIState.Hidden,
val navSubmit: QuestionnaireNavigationViewUIState = QuestionnaireNavigationViewUIState.Hidden,
val navCancel: QuestionnaireNavigationViewUIState = QuestionnaireNavigationViewUIState.Hidden,
val navReview: QuestionnaireNavigationViewUIState = QuestionnaireNavigationViewUIState.Hidden,
)
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2022-2023 Google LLC
* Copyright 2022-2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -16,21 +16,65 @@

package com.google.android.fhir.datacapture

import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.google.android.fhir.datacapture.views.NavigationViewHolder
import com.google.android.fhir.datacapture.views.factories.QuestionnaireItemViewHolder
import com.google.android.fhir.datacapture.views.factories.ReviewViewHolderFactory

/** List Adapter used to bind answers to [QuestionnaireItemViewHolder] in review mode. */
internal class QuestionnaireReviewAdapter :
ListAdapter<QuestionnaireAdapterItem.Question, QuestionnaireItemViewHolder>(
DiffCallbacks.QUESTIONS,
ListAdapter<QuestionnaireAdapterItem, RecyclerView.ViewHolder>(
DiffCallbacks.ITEMS,
) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): QuestionnaireItemViewHolder {
return ReviewViewHolderFactory.create(parent)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val typedViewType = QuestionnaireEditAdapter.ViewType.parse(viewType)
return when (typedViewType.type) {
QuestionnaireEditAdapter.ViewType.Type.QUESTION -> ReviewViewHolderFactory.create(parent)
QuestionnaireEditAdapter.ViewType.Type.NAVIGATION ->
NavigationViewHolder(
LayoutInflater.from(parent.context)
.inflate(R.layout.pagination_navigation_view, parent, false),
)
QuestionnaireEditAdapter.ViewType.Type.REPEATED_GROUP_HEADER -> TODO()
}
}

override fun onBindViewHolder(holder: QuestionnaireItemViewHolder, position: Int) {
holder.bind(getItem(position).item)
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (val item = getItem(position)) {
is QuestionnaireAdapterItem.Question -> {
holder as QuestionnaireItemViewHolder
holder.bind(item.item)
}
is QuestionnaireAdapterItem.Navigation -> {
holder as NavigationViewHolder
holder.bind(item.questionnaireNavigationUIState)
}
is QuestionnaireAdapterItem.RepeatedGroupHeader -> TODO()
}
}

override fun getItemViewType(position: Int): Int {
// Because we have multiple Item subtypes, we will pack two ints into the item view type.

// The first 8 bits will be represented by this type, which is unique for each Item subclass.
val type: QuestionnaireEditAdapter.ViewType.Type
// The last 24 bits will be represented by this subtype, which will further divide each Item
// subclass into more view types.
val subtype: Int
when (getItem(position)) {
is QuestionnaireAdapterItem.Question -> {
type = QuestionnaireEditAdapter.ViewType.Type.QUESTION
subtype = 0xFFFFFF
}
is QuestionnaireAdapterItem.Navigation -> {
type = QuestionnaireEditAdapter.ViewType.Type.NAVIGATION
subtype = 0xFFFFFF
}
is QuestionnaireAdapterItem.RepeatedGroupHeader -> TODO()
}
return QuestionnaireEditAdapter.ViewType.from(type = type, subtype = subtype).viewType
}
}
Loading

0 comments on commit 0264816

Please sign in to comment.