diff --git a/Corona-Warn-App/src/deviceForTesters/res/navigation/test_nav_graph.xml b/Corona-Warn-App/src/deviceForTesters/res/navigation/test_nav_graph.xml index dc81ad56f31a02deeb6f63ab438364bd9fcb2018..cc69872aa26bd8731655d408d32d012990120089 100644 --- a/Corona-Warn-App/src/deviceForTesters/res/navigation/test_nav_graph.xml +++ b/Corona-Warn-App/src/deviceForTesters/res/navigation/test_nav_graph.xml @@ -171,6 +171,20 @@ android:id="@+id/traceLocationOrganizerCategoriesFragment" android:name="de.rki.coronawarnapp.ui.eventregistration.organizer.category.TraceLocationCategoryFragment" android:label="TraceLocationCategoryFragment" - tools:layout="@layout/trace_location_organizer_category_fragment" /> + tools:layout="@layout/trace_location_organizer_category_fragment"> + + <action + android:id="@+id/action_traceLocationOrganizerCategoriesFragment_to_traceLocationCreateFragment" + app:destination="@id/traceLocationCreateFragment" /> + </fragment> + <fragment + android:id="@+id/traceLocationCreateFragment" + android:name="de.rki.coronawarnapp.ui.eventregistration.organizer.create.TraceLocationCreateFragment" + android:label="trace_location_create_fragment" + tools:layout="@layout/trace_location_create_fragment"> + <argument + android:name="category" + app:argType="de.rki.coronawarnapp.ui.eventregistration.organizer.category.adapter.category.TraceLocationCategory" /> + </fragment> </navigation> diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/EventRegistrationUIModule.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/EventRegistrationUIModule.kt index c45fb6984e1ef191a696a6861ff3feb534be0911..bf5d601ee8db88ae0c3f494e8790fef6cab294bd 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/EventRegistrationUIModule.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/EventRegistrationUIModule.kt @@ -2,6 +2,7 @@ package de.rki.coronawarnapp.ui.eventregistration import dagger.Module import dagger.android.ContributesAndroidInjector +import de.rki.coronawarnapp.ui.eventregistration.organizer.create.TraceLocationCreateFragment import de.rki.coronawarnapp.ui.eventregistration.attendee.checkins.CheckInsFragment import de.rki.coronawarnapp.ui.eventregistration.attendee.checkins.CheckInsModule import de.rki.coronawarnapp.ui.eventregistration.attendee.confirm.ConfirmCheckInFragment @@ -9,7 +10,8 @@ import de.rki.coronawarnapp.ui.eventregistration.attendee.confirm.ConfirmCheckIn import de.rki.coronawarnapp.ui.eventregistration.attendee.scan.ScanCheckInQrCodeFragment import de.rki.coronawarnapp.ui.eventregistration.attendee.scan.ScanCheckInQrCodeModule import de.rki.coronawarnapp.ui.eventregistration.organizer.category.TraceLocationCategoryFragment -import de.rki.coronawarnapp.ui.eventregistration.organizer.category.TraceLocationCategoryModule +import de.rki.coronawarnapp.ui.eventregistration.organizer.category.TraceLocationCategoryFragmentModule +import de.rki.coronawarnapp.ui.eventregistration.organizer.create.TraceLocationCreateFragmentModule @Module internal abstract class EventRegistrationUIModule { @@ -23,6 +25,9 @@ internal abstract class EventRegistrationUIModule { @ContributesAndroidInjector(modules = [CheckInsModule::class]) abstract fun checkInsFragment(): CheckInsFragment - @ContributesAndroidInjector(modules = [TraceLocationCategoryModule::class]) + @ContributesAndroidInjector(modules = [TraceLocationCategoryFragmentModule::class]) abstract fun traceLocationCategoryFragment(): TraceLocationCategoryFragment + + @ContributesAndroidInjector(modules = [TraceLocationCreateFragmentModule::class]) + abstract fun traceLocationCreateFragment(): TraceLocationCreateFragment } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/category/TraceLocationCategoryFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/category/TraceLocationCategoryFragment.kt index 251a4a1a873f567cfed9936d4fb31b787b34c663..6c42c65b0795edd35d8c08a0f0ffa404d78ff6b7 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/category/TraceLocationCategoryFragment.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/category/TraceLocationCategoryFragment.kt @@ -8,12 +8,12 @@ import de.rki.coronawarnapp.R import de.rki.coronawarnapp.databinding.TraceLocationOrganizerCategoryFragmentBinding import de.rki.coronawarnapp.ui.eventregistration.organizer.category.adapter.TraceLocationCategoryAdapter import de.rki.coronawarnapp.util.di.AutoInject +import de.rki.coronawarnapp.util.ui.doNavigate import de.rki.coronawarnapp.util.ui.observe2 import de.rki.coronawarnapp.util.ui.popBackStack import de.rki.coronawarnapp.util.ui.viewBindingLazy import de.rki.coronawarnapp.util.viewmodel.CWAViewModelFactoryProvider import de.rki.coronawarnapp.util.viewmodel.cwaViewModels -import timber.log.Timber import javax.inject.Inject class TraceLocationCategoryFragment : Fragment(R.layout.trace_location_organizer_category_fragment), AutoInject { @@ -30,8 +30,10 @@ class TraceLocationCategoryFragment : Fragment(R.layout.trace_location_organizer vm.categoryItems.observe2(this) { categoryItems -> val adapter = TraceLocationCategoryAdapter(categoryItems) { - // TODO: Set click-listener - Continue with event creation flow in next PR - Timber.d("Clicked on TraceLocationCategory: $it") + doNavigate( + TraceLocationCategoryFragmentDirections + .actionTraceLocationOrganizerCategoriesFragmentToTraceLocationCreateFragment(it) + ) } binding.recyclerViewCategories.adapter = adapter } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/category/TraceLocationCategoryModule.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/category/TraceLocationCategoryFragmentModule.kt similarity index 91% rename from Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/category/TraceLocationCategoryModule.kt rename to Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/category/TraceLocationCategoryFragmentModule.kt index c410c0643c032220ae9282a22b9642a11aa45380..e2151c9274563b0fa69066f4f9e8188189b5ab7b 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/category/TraceLocationCategoryModule.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/category/TraceLocationCategoryFragmentModule.kt @@ -8,7 +8,7 @@ import de.rki.coronawarnapp.util.viewmodel.CWAViewModelFactory import de.rki.coronawarnapp.util.viewmodel.CWAViewModelKey @Module -abstract class TraceLocationCategoryModule { +abstract class TraceLocationCategoryFragmentModule { @Binds @IntoMap diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/category/adapter/category/TraceLocationCategory.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/category/adapter/category/TraceLocationCategory.kt index 39472f4af3e3d612e0d0b336527c3744c2354daf..9d9bc2ac3563fc669f9ec1b94c3aae0450ebfb78 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/category/adapter/category/TraceLocationCategory.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/category/adapter/category/TraceLocationCategory.kt @@ -1,5 +1,6 @@ package de.rki.coronawarnapp.ui.eventregistration.organizer.category.adapter.category +import android.os.Parcelable import androidx.annotation.StringRes import de.rki.coronawarnapp.R import de.rki.coronawarnapp.server.protocols.internal.pt.TraceLocationOuterClass @@ -18,13 +19,15 @@ import de.rki.coronawarnapp.server.protocols.internal.pt.TraceLocationOuterClass import de.rki.coronawarnapp.ui.eventregistration.organizer.category.adapter.CategoryItem import de.rki.coronawarnapp.ui.eventregistration.organizer.category.adapter.category.TraceLocationUIType.EVENT import de.rki.coronawarnapp.ui.eventregistration.organizer.category.adapter.category.TraceLocationUIType.LOCATION +import kotlinx.parcelize.Parcelize +@Parcelize data class TraceLocationCategory( val type: TraceLocationOuterClass.TraceLocationType, val uiType: TraceLocationUIType, @StringRes val title: Int, @StringRes val subtitle: Int? = null -) : CategoryItem { +) : CategoryItem, Parcelable { override val stableId = hashCode().toLong() } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/create/TraceLocationCreateFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/create/TraceLocationCreateFragment.kt new file mode 100644 index 0000000000000000000000000000000000000000..d3fc7232a848f13008dc4cb04691414584619053 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/create/TraceLocationCreateFragment.kt @@ -0,0 +1,189 @@ +package de.rki.coronawarnapp.ui.eventregistration.organizer.create + +import android.os.Bundle +import android.text.format.DateFormat.is24HourFormat +import android.view.View +import androidx.core.view.isVisible +import androidx.core.widget.doOnTextChanged +import androidx.fragment.app.Fragment +import androidx.navigation.fragment.navArgs +import com.google.android.material.datepicker.CalendarConstraints +import com.google.android.material.datepicker.DateValidatorPointForward +import com.google.android.material.datepicker.MaterialDatePicker +import com.google.android.material.timepicker.MaterialTimePicker +import com.google.android.material.timepicker.TimeFormat +import de.rki.coronawarnapp.R +import de.rki.coronawarnapp.contactdiary.util.getLocale +import de.rki.coronawarnapp.contactdiary.util.hideKeyboard +import de.rki.coronawarnapp.databinding.TraceLocationCreateFragmentBinding +import de.rki.coronawarnapp.ui.durationpicker.DurationPicker +import de.rki.coronawarnapp.ui.durationpicker.toContactDiaryFormat +import de.rki.coronawarnapp.util.di.AutoInject +import de.rki.coronawarnapp.util.ui.popBackStack +import de.rki.coronawarnapp.util.ui.viewBindingLazy +import de.rki.coronawarnapp.util.viewmodel.CWAViewModelFactoryProvider +import de.rki.coronawarnapp.util.viewmodel.cwaViewModelsAssisted +import org.joda.time.DateTimeZone +import org.joda.time.Duration +import org.joda.time.LocalDate +import org.joda.time.LocalDateTime +import org.joda.time.LocalTime +import javax.inject.Inject + +class TraceLocationCreateFragment : Fragment(R.layout.trace_location_create_fragment), AutoInject { + + private val binding: TraceLocationCreateFragmentBinding by viewBindingLazy() + private val navArgs by navArgs<TraceLocationCreateFragmentArgs>() + + @Inject lateinit var viewModelFactory: CWAViewModelFactoryProvider.Factory + private val viewModel: TraceLocationCreateViewModel by cwaViewModelsAssisted( + keyProducer = { navArgs.category.type.toString() }, + factoryProducer = { viewModelFactory }, + constructorCall = { factory, _ -> + factory as TraceLocationCreateViewModel.Factory + factory.create(navArgs.category) + } + ) + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + binding.toolbar.setNavigationOnClickListener { + it.hideKeyboard() + popBackStack() + } + + viewModel.uiState.observe(viewLifecycleOwner) { state -> + binding.apply { + toolbar.setSubtitle(state.title) + valueStart.text = state.getBegin(requireContext().getLocale()) + valueEnd.text = state.getEnd(requireContext().getLocale()) + layoutBegin.isVisible = state.isDateVisible + layoutEnd.isVisible = state.isDateVisible + valueLengthOfStay.text = state.getCheckInLength(resources) + buttonSubmit.isEnabled = state.isSendEnable + } + } + + binding.descriptionInputEdit.doOnTextChanged { text, _, _, _ -> + viewModel.description = text?.toString() + } + + binding.placeInputEdit.doOnTextChanged { text, _, _, _ -> + viewModel.address = text?.toString() + } + + binding.layoutBegin.setOnClickListener { + it.hideKeyboard() + showDatePicker(viewModel.begin) { value -> + viewModel.begin = value + } + } + + binding.layoutEnd.setOnClickListener { + it.hideKeyboard() + showDatePicker(viewModel.end, viewModel.begin) { value -> + viewModel.end = value + } + } + + binding.layoutLengthOfStay.setOnClickListener { + it.hideKeyboard() + showDurationPicker() + } + + binding.buttonSubmit.setOnClickListener { + viewModel.send() + } + } + + private fun showDatePicker( + defaultValue: LocalDateTime?, + minConstraint: LocalDateTime? = null, + callback: (LocalDateTime) -> Unit + ) { + MaterialDatePicker + .Builder + .datePicker() + .setSelection(defaultValue?.toDateTime(DateTimeZone.UTC)?.millis) + .apply { + if (minConstraint != null) { + setCalendarConstraints( + CalendarConstraints.Builder() + .setValidator( + DateValidatorPointForward + .from(minConstraint.withMillisOfDay(0).toDateTime(DateTimeZone.UTC).millis) + ) + .build() + ) + } + } + .build() + .apply { + addOnPositiveButtonClickListener { + showTimePicker(LocalDate(it), defaultValue?.hourOfDay, defaultValue?.minuteOfHour, callback) + } + } + .show(childFragmentManager, DATE_PICKER_TAG) + } + + private fun showTimePicker(date: LocalDate, hours: Int?, minutes: Int?, callback: (LocalDateTime) -> Unit) { + MaterialTimePicker + .Builder() + .setTimeFormat(if (is24HourFormat(requireContext())) TimeFormat.CLOCK_24H else TimeFormat.CLOCK_12H) + .apply { + if (hours != null && minutes != null) { + setHour(hours) + setMinute(minutes) + } + } + .build() + .apply { + addOnPositiveButtonClickListener { + callback(date.toLocalDateTime(LocalTime(this.hour, this.minute))) + } + } + .show(childFragmentManager, TIME_PICKER_TAG) + } + + private fun showDurationPicker() { + DurationPicker.Builder() + .duration(viewModel.checkInLength?.toContactDiaryFormat() ?: "") + .title(getString(R.string.tracelocation_organizer_add_event_length_of_stay)) + .build() + .apply { + setDurationChangeListener { + viewModel.checkInLength = it + } + } + .show(parentFragmentManager, DURATION_PICKER_TAG) + } + + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState( + outState.apply { + putLong(LENGTH_OF_STAY, viewModel.checkInLength?.standardMinutes ?: 0L) + putSerializable(BEGIN, viewModel.begin) + putSerializable(END, viewModel.end) + } + ) + } + + override fun onViewStateRestored(savedInstanceState: Bundle?) { + super.onViewStateRestored(savedInstanceState) + savedInstanceState?.getLong(LENGTH_OF_STAY)?.let { + viewModel.checkInLength = Duration.standardMinutes(it) + } + viewModel.begin = savedInstanceState?.getSerializable(BEGIN) as LocalDateTime? + viewModel.end = savedInstanceState?.getSerializable(END) as LocalDateTime? + } + + companion object { + private const val DURATION_PICKER_TAG = "duration_picker" + private const val DATE_PICKER_TAG = "date_picker" + private const val TIME_PICKER_TAG = "time_picker" + private const val LENGTH_OF_STAY = "length_of_stay" + private const val BEGIN = "begin" + private const val END = "end" + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/create/TraceLocationCreateFragmentModule.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/create/TraceLocationCreateFragmentModule.kt new file mode 100644 index 0000000000000000000000000000000000000000..b32ebf2fb923fb143c13849415915bfca475feb0 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/create/TraceLocationCreateFragmentModule.kt @@ -0,0 +1,18 @@ +package de.rki.coronawarnapp.ui.eventregistration.organizer.create + +import dagger.Binds +import dagger.Module +import dagger.multibindings.IntoMap +import de.rki.coronawarnapp.util.viewmodel.CWAViewModel +import de.rki.coronawarnapp.util.viewmodel.CWAViewModelFactory +import de.rki.coronawarnapp.util.viewmodel.CWAViewModelKey + +@Module +abstract class TraceLocationCreateFragmentModule { + + @Binds + @IntoMap + @CWAViewModelKey(TraceLocationCreateViewModel::class) + abstract fun traceLocationCreateViewModel(factory: TraceLocationCreateViewModel.Factory): + CWAViewModelFactory<out CWAViewModel> +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/create/TraceLocationCreateViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/create/TraceLocationCreateViewModel.kt new file mode 100644 index 0000000000000000000000000000000000000000..38ec57f8963e71b230490b96d3d48cfe83cf5d00 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/create/TraceLocationCreateViewModel.kt @@ -0,0 +1,121 @@ +package de.rki.coronawarnapp.ui.eventregistration.organizer.create + +import android.content.res.Resources +import androidx.annotation.StringRes +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import de.rki.coronawarnapp.R +import de.rki.coronawarnapp.contactdiary.util.CWADateTimeFormatPatternFactory.shortDatePattern +import de.rki.coronawarnapp.ui.durationpicker.toReadableDuration +import de.rki.coronawarnapp.ui.eventregistration.organizer.category.adapter.category.TraceLocationCategory +import de.rki.coronawarnapp.ui.eventregistration.organizer.category.adapter.category.TraceLocationUIType +import de.rki.coronawarnapp.util.viewmodel.CWAViewModel +import de.rki.coronawarnapp.util.viewmodel.CWAViewModelFactory +import org.joda.time.Duration +import org.joda.time.LocalDateTime +import java.util.Locale +import kotlin.properties.ReadWriteProperty +import kotlin.reflect.KProperty + +class TraceLocationCreateViewModel @AssistedInject constructor( + @Assisted private val category: TraceLocationCategory +) : CWAViewModel() { + + private val mutableUiState = MutableLiveData<UIState>() + val uiState: LiveData<UIState> + get() = mutableUiState + + var description: String? by UpdateDelegate() + var address: String? by UpdateDelegate() + var checkInLength: Duration? by UpdateDelegate() + var begin: LocalDateTime? by UpdateDelegate() + var end: LocalDateTime? by UpdateDelegate() + + init { + checkInLength = when (category.uiType) { + TraceLocationUIType.LOCATION -> { + Duration.standardHours(2) + } + TraceLocationUIType.EVENT -> { + Duration.ZERO + } + } + } + + fun send() { + // TODO: This will be implemented in another PR + } + + private fun updateState() { + mutableUiState.value = UIState( + begin = begin, + end = end, + checkInLength = checkInLength, + title = category.title, + isDateVisible = category.uiType == TraceLocationUIType.EVENT, + isSendEnable = when (category.uiType) { + TraceLocationUIType.LOCATION -> { + description?.trim()?.length in 1..100 && + address?.trim()?.length in 0..100 && + (checkInLength ?: Duration.ZERO) > Duration.ZERO + } + TraceLocationUIType.EVENT -> { + description?.trim()?.length in 1..100 && + address?.trim()?.length in 0..100 && + begin != null && + end != null && + end?.isAfter(begin) == true + } + } + ) + } + + data class UIState( + private val begin: LocalDateTime? = null, + private val end: LocalDateTime? = null, + private val checkInLength: Duration? = null, + @StringRes val title: Int, + val isDateVisible: Boolean, + val isSendEnable: Boolean + ) { + fun getBegin(locale: Locale) = getFormattedTime(begin, locale) + + fun getEnd(locale: Locale) = getFormattedTime(end, locale) + + fun getCheckInLength(resources: Resources): String? { + return checkInLength?.toReadableDuration( + suffix = resources.getString(R.string.tracelocation_organizer_duration_suffix) + ) + } + + private fun getFormattedTime(value: LocalDateTime?, locale: Locale) = + value?.toString("E, ${locale.shortDatePattern()} HH:mm", locale) + } + + private class UpdateDelegate<T> : ReadWriteProperty<TraceLocationCreateViewModel?, T?> { + var value: T? = null + + override fun setValue( + thisRef: TraceLocationCreateViewModel?, + property: KProperty<*>, + value: T? + ) { + if (value != null) { + this.value = value + } + thisRef?.updateState() + } + + override fun getValue(thisRef: TraceLocationCreateViewModel?, property: KProperty<*>): T? { + return this.value + } + } + + @AssistedFactory + interface Factory : CWAViewModelFactory<TraceLocationCreateViewModel> { + fun create(category: TraceLocationCategory): TraceLocationCreateViewModel + } +} diff --git a/Corona-Warn-App/src/main/res/layout/trace_location_create_fragment.xml b/Corona-Warn-App/src/main/res/layout/trace_location_create_fragment.xml new file mode 100644 index 0000000000000000000000000000000000000000..37ac4ec19d52d74afa1b3f0990ee0c1369dabd18 --- /dev/null +++ b/Corona-Warn-App/src/main/res/layout/trace_location_create_fragment.xml @@ -0,0 +1,217 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:contentDescription="@string/tracelocation_organizer_category_title" + tools:context=".ui.eventregistration.organizer.create.TraceLocationCreateFragment"> + + <com.google.android.material.appbar.MaterialToolbar + android:id="@+id/toolbar" + android:layout_width="match_parent" + android:layout_height="wrap_content" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + app:navigationIcon="@drawable/ic_back" + app:title="@string/tracelocation_organizer_category_title" + tools:subtitle="@string/tracelocation_organizer_category_craft_title" /> + + <View + android:id="@+id/toolbar_divider" + android:layout_width="0dp" + android:layout_height="@dimen/card_divider" + android:background="@color/colorHairline" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/toolbar" /> + + <ScrollView + android:layout_width="0dp" + android:layout_height="0dp" + android:layout_marginBottom="@dimen/spacing_small" + android:fillViewport="true" + app:layout_constraintBottom_toTopOf="@id/button_submit" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/toolbar_divider"> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical" + android:paddingBottom="@dimen/spacing_normal"> + + <com.google.android.material.textfield.TextInputLayout + android:id="@+id/description_input_layout" + style="@style/TextInputLayoutTheme" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginHorizontal="@dimen/spacing_normal" + android:layout_marginTop="@dimen/spacing_small" + android:hint="@string/tracelocation_organizer_add_event_description" + app:endIconMode="clear_text"> + + <com.google.android.material.textfield.TextInputEditText + android:id="@+id/description_input_edit" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:imeOptions="actionNext" + android:inputType="textCapWords" + android:maxLength="100" + android:textStyle="bold" /> + + </com.google.android.material.textfield.TextInputLayout> + + <com.google.android.material.textfield.TextInputLayout + android:id="@+id/place_input_layout" + style="@style/TextInputLayoutTheme" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginHorizontal="@dimen/spacing_normal" + android:layout_marginTop="@dimen/spacing_tiny" + android:layout_marginBottom="@dimen/spacing_normal" + android:hint="@string/tracelocation_organizer_add_event_location" + app:endIconMode="clear_text"> + + <com.google.android.material.textfield.TextInputEditText + android:id="@+id/place_input_edit" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:imeOptions="actionNext" + android:inputType="textCapWords" + android:maxLength="100" /> + + </com.google.android.material.textfield.TextInputLayout> + + <androidx.constraintlayout.widget.ConstraintLayout + android:id="@+id/layout_begin" + style="@style/ContactDiaryExpandableListItem" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginHorizontal="@dimen/spacing_normal" + android:layout_marginBottom="@dimen/spacing_tiny" + android:backgroundTint="@color/colorContactDiaryListItem" + android:clickable="true" + android:focusable="true" + android:padding="@dimen/spacing_small"> + + <TextView + android:id="@+id/label_start" + style="@style/subtitle" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:text="@string/tracelocation_organizer_add_event_begin" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toStartOf="@id/value_start" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + + <TextView + android:id="@+id/value_start" + style="@style/subtitleMedium" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toTopOf="parent" + tools:text="Do., 11.02.21 10:00" /> + + </androidx.constraintlayout.widget.ConstraintLayout> + + <androidx.constraintlayout.widget.ConstraintLayout + android:id="@+id/layout_end" + style="@style/ContactDiaryExpandableListItem" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginHorizontal="@dimen/spacing_normal" + android:layout_marginBottom="@dimen/spacing_tiny" + android:backgroundTint="@color/colorContactDiaryListItem" + android:clickable="true" + android:focusable="true" + android:padding="@dimen/spacing_small"> + + <TextView + android:id="@+id/label_end" + style="@style/subtitle" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:text="@string/tracelocation_organizer_add_event_end" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toStartOf="@id/value_end" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + + <TextView + android:id="@+id/value_end" + style="@style/subtitleMedium" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toTopOf="parent" + tools:text="Do., 11.02.21 10:00" /> + + </androidx.constraintlayout.widget.ConstraintLayout> + + <androidx.constraintlayout.widget.ConstraintLayout + android:id="@+id/layout_length_of_stay" + style="@style/ContactDiaryExpandableListItem" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginHorizontal="@dimen/spacing_normal" + android:backgroundTint="@color/colorContactDiaryListItem" + android:clickable="true" + android:focusable="true" + android:padding="@dimen/spacing_small"> + + <TextView + android:id="@+id/label_length_of_stay" + style="@style/subtitle" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:text="@string/tracelocation_organizer_add_event_length_of_stay" + app:layout_constraintEnd_toStartOf="@id/value_length_of_stay" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + + <TextView + android:id="@+id/value_length_of_stay" + style="@style/subtitleMedium" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toTopOf="parent" + tools:text="1:00 Std." /> + + <TextView + android:id="@+id/description_length_of_stay" + style="@style/subtitleMedium" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:paddingTop="@dimen/spacing_small" + android:text="@string/tracelocation_organizer_add_event_length_of_stay_description" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/value_length_of_stay" /> + + </androidx.constraintlayout.widget.ConstraintLayout> + + </LinearLayout> + </ScrollView> + + <Button + android:id="@+id/button_submit" + style="@style/buttonPrimary" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginHorizontal="@dimen/spacing_normal" + android:layout_marginBottom="@dimen/spacing_small" + android:text="@string/tracelocation_organizer_save" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" /> + +</androidx.constraintlayout.widget.ConstraintLayout> diff --git a/Corona-Warn-App/src/main/res/layout/trace_location_organizer_category_fragment.xml b/Corona-Warn-App/src/main/res/layout/trace_location_organizer_category_fragment.xml index 0aa9f86df11d785bdab0f55ce33802a6e6126df7..26baca4abe8b9f27655036ba9f52106bea3bd04a 100644 --- a/Corona-Warn-App/src/main/res/layout/trace_location_organizer_category_fragment.xml +++ b/Corona-Warn-App/src/main/res/layout/trace_location_organizer_category_fragment.xml @@ -1,6 +1,7 @@ <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" android:id="@+id/category_root" android:layout_width="match_parent" android:layout_height="match_parent" @@ -19,6 +20,7 @@ android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1" - app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" /> + app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" + tools:listitem="@layout/trace_location_organizer_category_item" /> </LinearLayout> \ No newline at end of file diff --git a/Corona-Warn-App/src/main/res/navigation/trace_location_organizer_nav_graph.xml b/Corona-Warn-App/src/main/res/navigation/trace_location_organizer_nav_graph.xml index 35744089a2654cb434804ecdceb6fc1bb55090bb..cb50196129082d1ab0f03a44b3fec3155fce1c6b 100644 --- a/Corona-Warn-App/src/main/res/navigation/trace_location_organizer_nav_graph.xml +++ b/Corona-Warn-App/src/main/res/navigation/trace_location_organizer_nav_graph.xml @@ -1,6 +1,27 @@ <?xml version="1.0" encoding="utf-8"?> <navigation xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" android:id="@+id/event_organizer_nav_graph"> - <!-- TODO add organiser screens --> + + <fragment + android:id="@+id/traceLocationOrganizerCategoriesFragment" + android:name="de.rki.coronawarnapp.ui.eventregistration.organizer.category.TraceLocationCategoryFragment" + android:label="TraceLocationCategoryFragment" + tools:layout="@layout/trace_location_organizer_category_fragment"> + + <action + android:id="@+id/action_traceLocationOrganizerCategoriesFragment_to_traceLocationCreateFragment" + app:destination="@id/traceLocationCreateFragment" /> + </fragment> + <fragment + android:id="@+id/traceLocationCreateFragment" + android:name="de.rki.coronawarnapp.ui.eventregistration.organizer.create.TraceLocationCreateFragment" + android:label="trace_location_create_fragment" + tools:layout="@layout/trace_location_create_fragment"> + <argument + android:name="category" + app:argType="de.rki.coronawarnapp.ui.eventregistration.organizer.category.adapter.category.TraceLocationCategory" /> + </fragment> + </navigation> \ No newline at end of file diff --git a/Corona-Warn-App/src/main/res/values-de/event_registration_strings.xml b/Corona-Warn-App/src/main/res/values-de/event_registration_strings.xml index 171b63159bb1769cc60e06ee8c0d60e58ecb2d4f..026406e04b1e4e8e1d01bf5c1ac71be41ff30648 100644 --- a/Corona-Warn-App/src/main/res/values-de/event_registration_strings.xml +++ b/Corona-Warn-App/src/main/res/values-de/event_registration_strings.xml @@ -107,6 +107,22 @@ <!-- XTXT: Title for other event --> <string name="tracelocation_organizer_category_other_event_title">anderes Event</string> + <!-- XTXT: Title for event description --> + <string name="tracelocation_organizer_add_event_description">"Bezeichnung"</string> + <!-- XTXT: Title for event location --> + <string name="tracelocation_organizer_add_event_location">"Ort"</string> + <!-- XTXT: Title for event length of stay --> + <string name="tracelocation_organizer_add_event_length_of_stay">"Typische Aufenthaltsdauer"</string> + <!-- XTXT: Description for event length of stay --> + <string name="tracelocation_organizer_add_event_length_of_stay_description">"Geben Sie an wie lange sich Gäste üblicherweise bei Ihnen aufhalten."</string> + <!-- XTXT: Title for event start time --> + <string name="tracelocation_organizer_add_event_begin">"Beginn"</string> + <!-- XTXT: Title for event end time --> + <string name="tracelocation_organizer_add_event_end">"Ende"</string> + <!-- XTXT: Duration suffix --> + <string name="tracelocation_organizer_duration_suffix">"Std."</string> + <!-- XTXT: Save button text --> + <string name="tracelocation_organizer_save">"Speichern"</string> <!-- Trace Location Homescreen Card--> <!-- XHED: Headline for trace location creation card --> diff --git a/Corona-Warn-App/src/main/res/values/event_registration_strings.xml b/Corona-Warn-App/src/main/res/values/event_registration_strings.xml index 020131edc17f8fce80983a087fdd569ab7fb463b..8616928eb5e8add101ccf750bbff8c34c9e4f67c 100644 --- a/Corona-Warn-App/src/main/res/values/event_registration_strings.xml +++ b/Corona-Warn-App/src/main/res/values/event_registration_strings.xml @@ -108,6 +108,22 @@ <!-- XTXT: Title for other event --> <string name="tracelocation_organizer_category_other_event_title">anderes Event</string> + <!-- XTXT: Title for event description --> + <string name="tracelocation_organizer_add_event_description">"Bezeichnung"</string> + <!-- XTXT: Title for event location --> + <string name="tracelocation_organizer_add_event_location">"Ort"</string> + <!-- XTXT: Title for event length of stay --> + <string name="tracelocation_organizer_add_event_length_of_stay">"Typische Aufenthaltsdauer"</string> + <!-- XTXT: Description for event length of stay --> + <string name="tracelocation_organizer_add_event_length_of_stay_description">"Geben Sie an wie lange sich Gäste üblicherweise bei Ihnen aufhalten."</string> + <!-- XTXT: Title for event start time --> + <string name="tracelocation_organizer_add_event_begin">"Beginn"</string> + <!-- XTXT: Title for event end time --> + <string name="tracelocation_organizer_add_event_end">"Ende"</string> + <!-- XTXT: Duration suffix --> + <string name="tracelocation_organizer_duration_suffix">"Std."</string> + <!-- XTXT: Save button text --> + <string name="tracelocation_organizer_save">"Speichern"</string> <!-- Trace Location Homescreen Card--> <!-- XHED: Headline for trace location creation card --> diff --git a/Corona-Warn-App/src/main/res/values/styles.xml b/Corona-Warn-App/src/main/res/values/styles.xml index 91e297f4f85c0a54f10f6c0fc713eb591bc12a28..7d60a1a669c5b100ff3d55981be6c6bbec2d2428 100644 --- a/Corona-Warn-App/src/main/res/values/styles.xml +++ b/Corona-Warn-App/src/main/res/values/styles.xml @@ -6,6 +6,17 @@ <item name="android:windowBackground">@color/colorBackground</item> <item name="alertDialogTheme">@style/DialogAlertTheme</item> <item name="android:actionOverflowButtonStyle">@style/CWAToolbar.Overflow</item> + + <item name="materialCalendarTheme">@style/ThemeOverlay.App.DatePicker</item> + <item name="materialTimePickerTheme">@style/ThemeOverlay.App.TimePicker</item> + </style> + + <style name="ThemeOverlay.App.DatePicker" parent="ThemeOverlay.MaterialComponents.MaterialCalendar"> + <item name="colorPrimary">@color/colorAccent</item> + </style> + + <style name="ThemeOverlay.App.TimePicker" parent="ThemeOverlay.MaterialComponents.TimePicker"> + <item name="colorPrimary">@color/colorAccent</item> </style> <style name="AppTheme.NoActionBar" parent="AppTheme"> diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/eventregistration/organizer/create/TraceLocationCreateViewModelTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/eventregistration/organizer/create/TraceLocationCreateViewModelTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..11bd8101964e816ef5d5435dd11ab62fbf7c74b1 --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/eventregistration/organizer/create/TraceLocationCreateViewModelTest.kt @@ -0,0 +1,165 @@ +package de.rki.coronawarnapp.ui.eventregistration.organizer.create + +import de.rki.coronawarnapp.R +import de.rki.coronawarnapp.server.protocols.internal.pt.TraceLocationOuterClass +import de.rki.coronawarnapp.ui.eventregistration.organizer.category.adapter.category.TraceLocationCategory +import de.rki.coronawarnapp.ui.eventregistration.organizer.category.adapter.category.TraceLocationUIType +import io.kotest.matchers.shouldBe +import org.joda.time.Duration +import org.joda.time.LocalDateTime +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.MethodSource +import testhelpers.BaseTest +import testhelpers.extensions.InstantExecutorExtension +import testhelpers.extensions.observeForTesting + +@ExtendWith(InstantExecutorExtension::class) +internal class TraceLocationCreateViewModelTest : BaseTest() { + + @ParameterizedTest + @MethodSource("provideArguments") + fun `send should not be enabled for empty form`(category: TraceLocationCategory) { + val viewModel = TraceLocationCreateViewModel(category = category) + viewModel.uiState.observeForTesting { + viewModel.uiState.value?.isSendEnable shouldBe false + } + } + + @ParameterizedTest + @MethodSource("provideArguments") + fun `title should be set according to the category item`(category: TraceLocationCategory) { + val viewModel = TraceLocationCreateViewModel(category = category) + viewModel.uiState.observeForTesting { + viewModel.uiState.value?.title shouldBe category.title + } + } + + @ParameterizedTest + @MethodSource("provideArguments") + fun `send should be enabled when all data are entered`(category: TraceLocationCategory) { + val viewModel = TraceLocationCreateViewModel(category = category) + + viewModel.address = "Address" + viewModel.description = "Description" + viewModel.begin = LocalDateTime() + viewModel.end = LocalDateTime().plusHours(1) + viewModel.checkInLength = Duration.standardMinutes(1) + + viewModel.uiState.observeForTesting { + viewModel.uiState.value?.isSendEnable shouldBe true + } + } + + @ParameterizedTest + @MethodSource("provideArguments") + fun `send should not not be enabled when description it too long`(category: TraceLocationCategory) { + val viewModel = TraceLocationCreateViewModel(category = category) + + viewModel.address = "Address" + viewModel.description = "A".repeat(101) + viewModel.begin = LocalDateTime() + viewModel.end = LocalDateTime().plusHours(1) + viewModel.checkInLength = Duration.standardMinutes(1) + + viewModel.uiState.observeForTesting { + viewModel.uiState.value?.isSendEnable shouldBe false + } + } + + @ParameterizedTest + @MethodSource("provideArguments") + fun `send should not not be enabled when address it too long`(category: TraceLocationCategory) { + val viewModel = TraceLocationCreateViewModel(category = category) + + viewModel.address = "A".repeat(101) + viewModel.description = "Description" + viewModel.begin = LocalDateTime() + viewModel.end = LocalDateTime().plusHours(1) + viewModel.checkInLength = Duration.standardMinutes(1) + + viewModel.uiState.observeForTesting { + viewModel.uiState.value?.isSendEnable shouldBe false + } + } + + @Test + fun `begin and end should be visible for EVENT`() { + val viewModel = TraceLocationCreateViewModel(category = categoryEvent) + viewModel.uiState.observeForTesting { + viewModel.uiState.value?.isDateVisible shouldBe true + } + } + + @Test + fun `begin and end should not be visible for LOCATION`() { + val viewModel = TraceLocationCreateViewModel(category = categoryLocation) + viewModel.uiState.observeForTesting { + viewModel.uiState.value?.isDateVisible shouldBe false + } + } + + @Test + fun `send should not be enabled when length of stay is ZERO and category is LOCATION`() { + val viewModel = TraceLocationCreateViewModel(category = categoryLocation) + + viewModel.address = "Address" + viewModel.description = "Description" + viewModel.checkInLength = Duration.ZERO + + viewModel.uiState.observeForTesting { + viewModel.uiState.value?.isSendEnable shouldBe false + } + } + + @Test + fun `send should be enabled when length of stay is ZERO and category is EVENT`() { + val viewModel = TraceLocationCreateViewModel(category = categoryEvent) + + viewModel.address = "Address" + viewModel.description = "Description" + viewModel.begin = LocalDateTime() + viewModel.end = LocalDateTime().plusHours(1) + viewModel.checkInLength = Duration.ZERO + + viewModel.uiState.observeForTesting { + viewModel.uiState.value?.isSendEnable shouldBe true + } + } + + @Test + fun `send should not be enabled when end is before begin and category is EVENT`() { + val viewModel = TraceLocationCreateViewModel(category = categoryEvent) + + viewModel.address = "Address" + viewModel.description = "Description" + viewModel.begin = LocalDateTime().plusHours(1) + viewModel.end = LocalDateTime() + viewModel.checkInLength = Duration.ZERO + + viewModel.uiState.observeForTesting { + viewModel.uiState.value?.isSendEnable shouldBe false + } + } + + companion object { + private val categoryLocation = TraceLocationCategory( + TraceLocationOuterClass.TraceLocationType.LOCATION_TYPE_PERMANENT_RETAIL, + TraceLocationUIType.LOCATION, + R.string.tracelocation_organizer_category_retail_title, + R.string.tracelocation_organizer_category_retail_subtitle + ) + + private val categoryEvent = TraceLocationCategory( + TraceLocationOuterClass.TraceLocationType.LOCATION_TYPE_TEMPORARY_CULTURAL_EVENT, + TraceLocationUIType.EVENT, + R.string.tracelocation_organizer_category_cultural_event_title, + R.string.tracelocation_organizer_category_cultural_event_subtitle + ) + + @Suppress("unused") + @JvmStatic + fun provideArguments() = listOf(categoryEvent, categoryLocation) + } +}