diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/antigen/profile/RATProfileSettings.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/antigen/profile/RATProfileSettings.kt index a9169309856e9b587dcf33eda9217066c637d026..c432743863efcaa82a5a02e6ee639e9c6babf88d 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/antigen/profile/RATProfileSettings.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/antigen/profile/RATProfileSettings.kt @@ -1,10 +1,10 @@ package de.rki.coronawarnapp.coronatest.antigen.profile import android.content.Context +import androidx.core.content.edit import com.google.gson.Gson import dagger.Reusable import de.rki.coronawarnapp.util.di.AppContext -import de.rki.coronawarnapp.util.preferences.clearAndNotify import de.rki.coronawarnapp.util.preferences.createFlowPreference import de.rki.coronawarnapp.util.serialization.BaseGson import de.rki.coronawarnapp.util.serialization.fromJson @@ -35,9 +35,17 @@ class RATProfileSettings @Inject constructor( } ) - fun clear() = prefs.clearAndNotify() + val onboarded = prefs.createFlowPreference( + key = PREFS_KEY_ONBOARDED, + defaultValue = false + ) + + fun deleteProfile() = prefs.edit(commit = true) { + remove(PREFS_KEY_PROFILE) + } companion object { private const val PREFS_KEY_PROFILE = "ratprofile.settings.profile" + private const val PREFS_KEY_ONBOARDED = "ratprofile.settings.onboarded" } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/antigen/profile/VCard.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/antigen/profile/VCard.kt new file mode 100644 index 0000000000000000000000000000000000000000..619587bd95a11a71a285a4095499c1a5d1af4bf5 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/antigen/profile/VCard.kt @@ -0,0 +1,51 @@ +package de.rki.coronawarnapp.coronatest.antigen.profile + +import dagger.Reusable +import de.rki.coronawarnapp.util.TimeStamper +import org.joda.time.format.ISODateTimeFormat +import javax.inject.Inject + +@Reusable +class VCard @Inject constructor( + private val timeStamper: TimeStamper +) { + + /** + * Return V-Card format for [RATProfile] + * @return [String] + */ + fun create(ratProfile: RATProfile): String = ratProfile.run { + val lastName = lastName.escapeAll() + val firstName = firstName.escapeAll() + val fullName = buildString { + append(firstName) + if (lastName.isNotBlank()) { + append(" $lastName") + } + } + val city = city.escapeAll() + val street = street.escapeAll() + val zipCode = zipCode.escapeAll() + val phone = phone.escapeAll() + val email = email.escapeAll() + val birthDate = birthDate?.toString(ISODateTimeFormat.basicDate()).orEmpty() + val rev = timeStamper.nowUTC.toString(ISODateTimeFormat.basicDateTimeNoMillis()) // Time the vCard was updated + """ + BEGIN:VCARD + VERSION:4.0 + N:$lastName;$firstName;;; + FN:$fullName + BDAY:$birthDate + EMAIL;TYPE=home:$email + TEL;TYPE="cell,home":$phone + ADR;TYPE=home:;;$street;$city;;$zipCode + REV:$rev + END:VCARD + """.trimIndent() + } + + private fun String.escapeAll(): String = replace("\n", "") + .replace("\\", "\\\\") + .replace(",", "\\,") + .replace(";", "\\;") +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/coronatest/rat/profile/RATProfileUIModule.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/coronatest/rat/profile/RATProfileUIModule.kt index 21a76225622d3fe5375e956e10c0bd7507114bb4..61208100d9be7ddcd09c80be0639ca7773fda25b 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/coronatest/rat/profile/RATProfileUIModule.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/coronatest/rat/profile/RATProfileUIModule.kt @@ -4,10 +4,20 @@ import dagger.Module import dagger.android.ContributesAndroidInjector import de.rki.coronawarnapp.ui.coronatest.rat.profile.create.RATProfileCreateFragment import de.rki.coronawarnapp.ui.coronatest.rat.profile.create.RATProfileCreateFragmentModule +import de.rki.coronawarnapp.ui.coronatest.rat.profile.onboarding.RATProfileOnboardingFragment +import de.rki.coronawarnapp.ui.coronatest.rat.profile.onboarding.RATProfileOnboardingFragmentModule +import de.rki.coronawarnapp.ui.coronatest.rat.profile.qrcode.RATProfileQrCodeFragment +import de.rki.coronawarnapp.ui.coronatest.rat.profile.qrcode.RATProfileQrCodeFragmentModule @Module internal abstract class RATProfileUIModule { @ContributesAndroidInjector(modules = [RATProfileCreateFragmentModule::class]) abstract fun ratProfileCreateFragment(): RATProfileCreateFragment + + @ContributesAndroidInjector(modules = [RATProfileQrCodeFragmentModule::class]) + abstract fun ratProfileQrCodeFragment(): RATProfileQrCodeFragment + + @ContributesAndroidInjector(modules = [RATProfileOnboardingFragmentModule::class]) + abstract fun ratProfileOnboardingFragment(): RATProfileOnboardingFragment } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/coronatest/rat/profile/create/RATProfileCreateFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/coronatest/rat/profile/create/RATProfileCreateFragment.kt index 8d32df661e7f5e6e1d971edf6c3c842de2e9b356..2ef3dcaca4e697d9a5cdcfa6f27bbc309cea85eb 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/coronatest/rat/profile/create/RATProfileCreateFragment.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/coronatest/rat/profile/create/RATProfileCreateFragment.kt @@ -40,6 +40,11 @@ class RATProfileCreateFragment : Fragment(R.layout.rat_profile_create_fragment), // Birth date birthDateInputEdit.setOnClickListener { openDatePicker() } + birthDateInputEdit.doAfterTextChanged { + if (it.toString().isBlank()) { + viewModel.birthDateChanged(null) + } + } // Address streetInputEdit.doAfterTextChanged { viewModel.streetChanged(it.toString()) } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/coronatest/rat/profile/create/RATProfileCreateFragmentViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/coronatest/rat/profile/create/RATProfileCreateFragmentViewModel.kt index 36ca71e288a1a5d80f7ae3fc88c57e7161ca3473..27861b540f066645e23703f9d8a43c8c6c3ca957 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/coronatest/rat/profile/create/RATProfileCreateFragmentViewModel.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/coronatest/rat/profile/create/RATProfileCreateFragmentViewModel.kt @@ -41,7 +41,7 @@ class RATProfileCreateFragmentViewModel @AssistedInject constructor( } } - fun birthDateChanged(birthDate: LocalDate) { + fun birthDateChanged(birthDate: LocalDate?) { profileData.apply { value = value?.copy(birthDate = birthDate) } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/coronatest/rat/profile/onboarding/RATProfileOnboardingFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/coronatest/rat/profile/onboarding/RATProfileOnboardingFragment.kt index 62643cefb5f6d43906269e05f998ac422ad25a2d..bf3e0cb317ce6a714a2680a1b90f5282b3c18821 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/coronatest/rat/profile/onboarding/RATProfileOnboardingFragment.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/coronatest/rat/profile/onboarding/RATProfileOnboardingFragment.kt @@ -5,18 +5,27 @@ import android.view.View import androidx.fragment.app.Fragment import de.rki.coronawarnapp.R import de.rki.coronawarnapp.databinding.RatProfileOnboardingFragmentBinding +import de.rki.coronawarnapp.util.di.AutoInject import de.rki.coronawarnapp.util.ui.doNavigate 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 javax.inject.Inject -class RATProfileOnboardingFragment : Fragment(R.layout.rat_profile_onboarding_fragment) { +class RATProfileOnboardingFragment : Fragment(R.layout.rat_profile_onboarding_fragment), AutoInject { private val binding: RatProfileOnboardingFragmentBinding by viewBindingLazy() + @Inject lateinit var viewModelFactory: CWAViewModelFactoryProvider.Factory + + private val viewModel: RATProfileOnboardingFragmentViewModel by cwaViewModels { viewModelFactory } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) = with(binding) { toolbar.setNavigationOnClickListener { popBackStack() } nextButton.setOnClickListener { + viewModel.onNext() doNavigate( RATProfileOnboardingFragmentDirections .actionRatProfileOnboardingFragmentToRatProfileCreateFragment() diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/coronatest/rat/profile/onboarding/RATProfileOnboardingFragmentModule.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/coronatest/rat/profile/onboarding/RATProfileOnboardingFragmentModule.kt new file mode 100644 index 0000000000000000000000000000000000000000..1b2bdad689f7a2064187143ab0a38b478dd51dbc --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/coronatest/rat/profile/onboarding/RATProfileOnboardingFragmentModule.kt @@ -0,0 +1,18 @@ +package de.rki.coronawarnapp.ui.coronatest.rat.profile.onboarding + +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 RATProfileOnboardingFragmentModule { + @Binds + @IntoMap + @CWAViewModelKey(RATProfileOnboardingFragmentViewModel::class) + abstract fun ratProfileOnboardingFragment( + factory: RATProfileOnboardingFragmentViewModel.Factory + ): CWAViewModelFactory<out CWAViewModel> +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/coronatest/rat/profile/onboarding/RATProfileOnboardingFragmentViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/coronatest/rat/profile/onboarding/RATProfileOnboardingFragmentViewModel.kt new file mode 100644 index 0000000000000000000000000000000000000000..0160a51dc00649b821b570ef84af638e5063de50 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/coronatest/rat/profile/onboarding/RATProfileOnboardingFragmentViewModel.kt @@ -0,0 +1,19 @@ +package de.rki.coronawarnapp.ui.coronatest.rat.profile.onboarding + +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import de.rki.coronawarnapp.coronatest.antigen.profile.RATProfileSettings +import de.rki.coronawarnapp.util.viewmodel.CWAViewModel +import de.rki.coronawarnapp.util.viewmodel.SimpleCWAViewModelFactory + +class RATProfileOnboardingFragmentViewModel @AssistedInject constructor( + private val ratProfileSettings: RATProfileSettings, +) : CWAViewModel() { + + fun onNext() { + ratProfileSettings.onboarded.update { true } + } + + @AssistedFactory + interface Factory : SimpleCWAViewModelFactory<RATProfileOnboardingFragmentViewModel> +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/coronatest/rat/profile/qrcode/ProfileQrCodeNavigation.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/coronatest/rat/profile/qrcode/ProfileQrCodeNavigation.kt new file mode 100644 index 0000000000000000000000000000000000000000..030aa4d4a7b6e8d06c5aca636d7f9b12c224250d --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/coronatest/rat/profile/qrcode/ProfileQrCodeNavigation.kt @@ -0,0 +1,6 @@ +package de.rki.coronawarnapp.ui.coronatest.rat.profile.qrcode + +sealed class ProfileQrCodeNavigation { + object Back : ProfileQrCodeNavigation() + object SubmissionConsent : ProfileQrCodeNavigation() +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/coronatest/rat/profile/qrcode/RATProfileQrCodeFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/coronatest/rat/profile/qrcode/RATProfileQrCodeFragment.kt index 4bc5b2120fed99eae14aca2a4a0458e45357461a..cedefa8d72b7ed4319d2c6d1bcda846703dc9931 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/coronatest/rat/profile/qrcode/RATProfileQrCodeFragment.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/coronatest/rat/profile/qrcode/RATProfileQrCodeFragment.kt @@ -2,18 +2,106 @@ package de.rki.coronawarnapp.ui.coronatest.rat.profile.qrcode import android.os.Bundle import android.view.View +import android.widget.LinearLayout +import androidx.coordinatorlayout.widget.CoordinatorLayout +import androidx.core.text.bold +import androidx.core.text.buildSpannedString import androidx.fragment.app.Fragment +import androidx.navigation.fragment.findNavController +import com.google.android.material.appbar.AppBarLayout +import com.google.android.material.dialog.MaterialAlertDialogBuilder import de.rki.coronawarnapp.R +import de.rki.coronawarnapp.coronatest.antigen.profile.RATProfile import de.rki.coronawarnapp.databinding.RatProfileQrCodeFragmentBinding +import de.rki.coronawarnapp.util.di.AutoInject +import de.rki.coronawarnapp.util.joinToSpannable 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 javax.inject.Inject +import kotlin.math.abs -class RATProfileQrCodeFragment : Fragment(R.layout.rat_profile_qr_code_fragment) { +class RATProfileQrCodeFragment : Fragment(R.layout.rat_profile_qr_code_fragment), AutoInject { + @Inject lateinit var viewModelFactory: CWAViewModelFactoryProvider.Factory + + private val viewModel: RATProfileQrCodeFragmentViewModel by cwaViewModels { viewModelFactory } private val binding: RatProfileQrCodeFragmentBinding by viewBindingLazy() override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + setToolbarOverlay() + binding.apply { + appBarLayout.addOnOffsetChangedListener( + AppBarLayout.OnOffsetChangedListener { appBarLayout, verticalOffset -> + val alpha = 1.0f - abs(verticalOffset / (appBarLayout.totalScrollRange.toFloat() * 0.5f)) + title.alpha = alpha + } + ) + + nextButton.setOnClickListener { viewModel.onNext() } + toolbar.setNavigationOnClickListener { viewModel.onClose() } + toolbar.setOnMenuItemClickListener { + confirmDeletionDialog() + true + } + } + viewModel.profile.observe(viewLifecycleOwner) { personProfile -> + with(binding) { + progressBar.hide() + personProfile.profile?.let { bindPersonInfo(it) } + qrCodeImage.setImageBitmap(personProfile.bitmap) + } + } + + viewModel.events.observe(viewLifecycleOwner) { + when (it) { + ProfileQrCodeNavigation.Back -> popBackStack() + ProfileQrCodeNavigation.SubmissionConsent -> + findNavController().navigate(R.id.submissionConsentFragment) + } + } + } + + private fun confirmDeletionDialog() { + MaterialAlertDialogBuilder(requireContext()) + .setTitle(getString(R.string.rat_qr_code_profile_dialog_title)) + .setMessage(getString(R.string.rat_qr_code_profile_dialog_message)) + .setPositiveButton(getString(R.string.rat_qr_code_profile_dialog_positive_button)) { _, _ -> + viewModel.deleteProfile() + } + .setNegativeButton(getString(R.string.rat_qr_code_profile_dialog_negative_button)) { _, _ -> + // No-Op + } + .show() + } + + private fun bindPersonInfo(ratProfile: RATProfile) = with(ratProfile) { + val name = buildSpannedString { bold { append("$firstName $lastName") } } + val birthDate = birthDate?.let { + getString( + R.string.rat_qr_code_profile_birth_date, + birthDate.toString("dd.MM.yyyy").orEmpty() + ) + }.orEmpty() + + val address = "$zipCode $city" + binding.profileInfo.text = arrayOf(name, birthDate, street, address, phone, email) + .filter { it.isNotBlank() } + .joinToSpannable("\n") + } + + private fun setToolbarOverlay() { + val width = requireContext().resources.displayMetrics.widthPixels + + val params: CoordinatorLayout.LayoutParams = binding.nestedScrollView.layoutParams + as (CoordinatorLayout.LayoutParams) + + val textParams = binding.title.layoutParams as (LinearLayout.LayoutParams) + textParams.bottomMargin = (width / 2) - 24 /* 24 is space between screen border and QrCode */ + binding.title.requestLayout() /* 24 is space between screen border and QrCode */ - binding.closeButton.setOnClickListener { popBackStack() } + val behavior: AppBarLayout.ScrollingViewBehavior = params.behavior as (AppBarLayout.ScrollingViewBehavior) + behavior.overlayTop = (width / 2) - 24 } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/coronatest/rat/profile/qrcode/RATProfileQrCodeFragmentModule.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/coronatest/rat/profile/qrcode/RATProfileQrCodeFragmentModule.kt new file mode 100644 index 0000000000000000000000000000000000000000..b35a9dbd0d30474b9cc7f5a70f7ed867e4eb4c3c --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/coronatest/rat/profile/qrcode/RATProfileQrCodeFragmentModule.kt @@ -0,0 +1,18 @@ +package de.rki.coronawarnapp.ui.coronatest.rat.profile.qrcode + +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 RATProfileQrCodeFragmentModule { + @Binds + @IntoMap + @CWAViewModelKey(RATProfileQrCodeFragmentViewModel::class) + abstract fun ratProfileQrCodeFragmentViewModel( + factory: RATProfileQrCodeFragmentViewModel.Factory + ): CWAViewModelFactory<out CWAViewModel> +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/coronatest/rat/profile/qrcode/RATProfileQrCodeFragmentViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/coronatest/rat/profile/qrcode/RATProfileQrCodeFragmentViewModel.kt new file mode 100644 index 0000000000000000000000000000000000000000..fad1095b4c4fffbf13b19fec30be729de5101e5d --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/coronatest/rat/profile/qrcode/RATProfileQrCodeFragmentViewModel.kt @@ -0,0 +1,72 @@ +package de.rki.coronawarnapp.ui.coronatest.rat.profile.qrcode + +import android.graphics.Bitmap +import androidx.lifecycle.LiveData +import androidx.lifecycle.asLiveData +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import de.rki.coronawarnapp.coronatest.antigen.profile.RATProfile +import de.rki.coronawarnapp.coronatest.antigen.profile.RATProfileSettings +import de.rki.coronawarnapp.coronatest.antigen.profile.VCard +import de.rki.coronawarnapp.presencetracing.checkins.qrcode.QrCodeGenerator +import de.rki.coronawarnapp.util.coroutine.DispatcherProvider +import de.rki.coronawarnapp.util.ui.SingleLiveEvent +import de.rki.coronawarnapp.util.viewmodel.CWAViewModel +import de.rki.coronawarnapp.util.viewmodel.SimpleCWAViewModelFactory +import kotlinx.coroutines.flow.map +import timber.log.Timber + +class RATProfileQrCodeFragmentViewModel @AssistedInject constructor( + private val ratProfileSettings: RATProfileSettings, + private val qrCodeGenerator: QrCodeGenerator, + private val vCard: VCard, + dispatcherProvider: DispatcherProvider, +) : CWAViewModel() { + + val profile: LiveData<PersonProfile> = ratProfileSettings.profile.flow + .map { profile -> + PersonProfile( + profile, + profile.qrCode() + ) + }.asLiveData(context = dispatcherProvider.Default) + + val events = SingleLiveEvent<ProfileQrCodeNavigation>() + + fun deleteProfile() { + Timber.d("deleteProfile") + ratProfileSettings.deleteProfile() + events.postValue(ProfileQrCodeNavigation.Back) + } + + fun onClose() { + Timber.d("onClose") + events.postValue(ProfileQrCodeNavigation.Back) + } + + fun onNext() { + Timber.d("onNext") + events.postValue(ProfileQrCodeNavigation.SubmissionConsent) + } + + private suspend fun RATProfile?.qrCode(): Bitmap? = + try { + if (this != null) { + qrCodeGenerator.createQrCode(vCard.create(this)) + } else { + Timber.d("No Profile available") + null + } + } catch (e: Exception) { + Timber.e(e, "Failed to generate profile Qr Code") + null + } + + @AssistedFactory + interface Factory : SimpleCWAViewModelFactory<RATProfileQrCodeFragmentViewModel> +} + +data class PersonProfile( + val profile: RATProfile?, + val bitmap: Bitmap? +) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/fragment/SubmissionDispatcherFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/fragment/SubmissionDispatcherFragment.kt index 3b16c6f61832089e3e981cf1d006eee4ad428890..11f7585630af3e8e2d67608088233528a34872d7 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/fragment/SubmissionDispatcherFragment.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/fragment/SubmissionDispatcherFragment.kt @@ -48,11 +48,17 @@ class SubmissionDispatcherFragment : Fragment(R.layout.fragment_submission_dispa SubmissionDispatcherFragmentDirections .actionSubmissionDispatcherFragmentToSubmissionConsentFragment() ) - is SubmissionNavigationEvents.NavigateToCreateProfile -> + is SubmissionNavigationEvents.NavigateToCreateProfile -> { + val ratGraph = findNavController().graph.findNode(R.id.rapid_test_profile_nav_graph) as NavGraph + ratGraph.startDestination = if (it.onboarded) + R.id.ratProfileCreateFragment + else R.id.ratProfileOnboardingFragment + doNavigate( SubmissionDispatcherFragmentDirections .actionSubmissionDispatcherFragmentToRapidTestProfileNavGraph() ) + } is SubmissionNavigationEvents.NavigateToOpenProfile -> { val ratGraph = findNavController().graph.findNode(R.id.rapid_test_profile_nav_graph) as NavGraph diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/viewmodel/SubmissionDispatcherViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/viewmodel/SubmissionDispatcherViewModel.kt index c558190977261129100283f195602654df04c75f..09cffe765938860b3133b292988fee6779e76405 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/viewmodel/SubmissionDispatcherViewModel.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/viewmodel/SubmissionDispatcherViewModel.kt @@ -48,7 +48,7 @@ class SubmissionDispatcherViewModel @AssistedInject constructor( val event = if (ratProfileSettings.profile.value != null) { SubmissionNavigationEvents.NavigateToOpenProfile } else { - SubmissionNavigationEvents.NavigateToCreateProfile + SubmissionNavigationEvents.NavigateToCreateProfile(ratProfileSettings.onboarded.value) } routeToScreen.postValue(event) } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/viewmodel/SubmissionNavigationEvents.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/viewmodel/SubmissionNavigationEvents.kt index c9efab9912d83ee93da4bdb596965750dee6cbef..789a64d889cf8358ceb601e7694cb881073c22b3 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/viewmodel/SubmissionNavigationEvents.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/viewmodel/SubmissionNavigationEvents.kt @@ -21,9 +21,11 @@ sealed class SubmissionNavigationEvents { val coronaTestQRCode: CoronaTestQRCode, val consentGiven: Boolean ) : SubmissionNavigationEvents() + data class NavigateToDeletionWarningFragmentFromTan(val coronaTestTan: CoronaTestTAN, val consentGiven: Boolean) : SubmissionNavigationEvents() - object NavigateToCreateProfile : SubmissionNavigationEvents() + + data class NavigateToCreateProfile(val onboarded: Boolean = false) : SubmissionNavigationEvents() object NavigateToOpenProfile : SubmissionNavigationEvents() data class ResolvePlayServicesException(val exception: ApiException) : SubmissionNavigationEvents() } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/SpannableStringBuilder.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/SpannableStringBuilder.kt index 61c9b2698cb6e8d8a9177579ac5b843890111c94..576047ca22cf24f014ac205a98a159576a453bd9 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/SpannableStringBuilder.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/SpannableStringBuilder.kt @@ -1,10 +1,40 @@ package de.rki.coronawarnapp.util +import android.text.Spannable import android.text.SpannableStringBuilder import android.text.Spanned import android.text.style.URLSpan +import androidx.core.text.toSpannable fun SpannableStringBuilder.urlSpan(start: Int, end: Int, value: String): SpannableStringBuilder { setSpan(URLSpan(value), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) return this } + +/** + * Creates a [Spannable] from all the elements separated using [separator] + * and using the given [prefix] and [postfix] if supplied. + * + * If the collection could be huge, you can specify a non-negative value of [limit], + * in which case only the first [limit] + * elements will be appended, followed by the [truncated] string (which defaults to "..."). + **/ +@Suppress("LongParameterList") +fun <T> Iterable<T>.joinToSpannable( + separator: CharSequence = ", ", + prefix: CharSequence = "", + postfix: CharSequence = "", + limit: Int = -1, + truncated: CharSequence = "...", + transform: ((T) -> CharSequence)? = null +): Spannable { + return joinTo( + SpannableStringBuilder(), + separator, + prefix, + postfix, + limit, + truncated, + transform + ).toSpannable() +} diff --git a/Corona-Warn-App/src/main/res/drawable/rat_profile_gradient.xml b/Corona-Warn-App/src/main/res/drawable/rat_profile_gradient.xml new file mode 100644 index 0000000000000000000000000000000000000000..85fdf590f418b7008036246cbbc244b07b29de9a --- /dev/null +++ b/Corona-Warn-App/src/main/res/drawable/rat_profile_gradient.xml @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="utf-8"?> +<shape xmlns:android="http://schemas.android.com/apk/res/android"> + <gradient + android:angle="135" + android:endColor="#599BC4" + android:startColor="#29689B" /> +</shape> \ No newline at end of file diff --git a/Corona-Warn-App/src/main/res/drawable/trace_location_qr_code_background.xml b/Corona-Warn-App/src/main/res/drawable/rounded_white_background.xml similarity index 100% rename from Corona-Warn-App/src/main/res/drawable/trace_location_qr_code_background.xml rename to Corona-Warn-App/src/main/res/drawable/rounded_white_background.xml diff --git a/Corona-Warn-App/src/main/res/layout/rat_profile_qr_code_fragment.xml b/Corona-Warn-App/src/main/res/layout/rat_profile_qr_code_fragment.xml index b1467f8500be0855d3ac060e51f2163500da632c..3c990f83bc78fdc54fd39988a01e1aff7c8adcd6 100644 --- a/Corona-Warn-App/src/main/res/layout/rat_profile_qr_code_fragment.xml +++ b/Corona-Warn-App/src/main/res/layout/rat_profile_qr_code_fragment.xml @@ -1,26 +1,198 @@ <?xml version="1.0" encoding="utf-8"?> -<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" +<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:id="@+id/content_container" android:layout_width="match_parent" android:layout_height="match_parent" - tools:context=".ui.coronatest.rat.profile.qrcode.RATProfileQrCodeFragment"> + android:background="@drawable/trace_location_gradient_background" + android:contentDescription="@string/trace_location_event_detail_title_accessibility"> - <TextView - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_gravity="center" - android:text="🚧 Profile Screen" - android:textSize="40sp" /> + <androidx.coordinatorlayout.widget.CoordinatorLayout + android:id="@+id/coordinator_layout" + android:layout_width="0dp" + android:layout_height="0dp" + android:layout_marginBottom="12dp" + android:nestedScrollingEnabled="true" + app:layout_constraintBottom_toTopOf="@id/next_button" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + tools:context=".CollapsingToolbar"> + + <com.google.android.material.appbar.AppBarLayout + android:id="@+id/appBarLayout" + android:layout_width="match_parent" + android:layout_height="wrap_content"> + + <com.google.android.material.appbar.CollapsingToolbarLayout + android:id="@+id/collapsing_toolbar_layout" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:nestedScrollingEnabled="true" + app:layout_scrollFlags="scroll|exitUntilCollapsed"> + + <ImageView + android:id="@+id/expandedImage" + android:layout_width="match_parent" + android:layout_height="match_parent" + app:layout_collapseMode="parallax" + app:srcCompat="@drawable/rat_profile_gradient" /> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical" + app:layout_collapseMode="parallax"> + + <TextView + android:id="@+id/title" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginHorizontal="24dp" + android:layout_marginTop="90dp" + android:layout_marginBottom="12dp" + android:gravity="center" + android:text="@string/rat_profile_create_title" + android:textColor="@android:color/white" + android:textSize="20sp" + android:textStyle="bold" /> + + </LinearLayout> + + <com.google.android.material.appbar.MaterialToolbar + android:id="@+id/toolbar" + android:layout_width="match_parent" + android:layout_height="?attr/actionBarSize" + android:theme="@style/CWAToolbar.Theme" + app:layout_collapseMode="pin" + app:layout_scrollFlags="scroll|enterAlways" + app:menu="@menu/menu_rat_qr_code_profile" + app:navigationIcon="@drawable/ic_close" + app:navigationIconTint="@android:color/white"> + + <FrameLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginEnd="24dp"> + <ImageView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center" + android:importantForAccessibility="no" + android:scaleType="center" + app:srcCompat="@drawable/ic_cwa_logo_white" /> + </FrameLayout> + + </com.google.android.material.appbar.MaterialToolbar> + + </com.google.android.material.appbar.CollapsingToolbarLayout> + + </com.google.android.material.appbar.AppBarLayout> + + <androidx.core.widget.NestedScrollView + android:id="@+id/nested_scroll_view" + android:layout_width="match_parent" + android:layout_height="match_parent" + app:layout_behavior="@string/appbar_scrolling_view_behavior"> + + <androidx.constraintlayout.widget.ConstraintLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:animateLayoutChanges="true"> + + <ImageView + android:id="@+id/qrCodeImage" + android:layout_width="0dp" + android:layout_height="0dp" + android:layout_marginHorizontal="24dp" + android:layout_marginVertical="12dp" + android:layout_marginTop="16dp" + android:background="@drawable/rounded_white_background" + android:contentDescription="@string/trace_location_event_detail_qr_code_accessibility" + app:layout_constraintDimensionRatio="H,1:1" + app:layout_constraintLeft_toLeftOf="parent" + app:layout_constraintRight_toRightOf="parent" + app:layout_constraintTop_toTopOf="parent" + tools:src="@drawable/ic_qrcode" + tools:tint="@android:color/black" /> + + <com.google.android.material.progressindicator.LinearProgressIndicator + android:id="@+id/progress_bar" + android:layout_width="150dp" + android:layout_height="wrap_content" + android:indeterminate="true" + app:hideAnimationBehavior="inward" + app:indicatorColor="@color/colorAccent" + app:layout_constraintBottom_toBottomOf="@id/qrCodeImage" + app:layout_constraintEnd_toEndOf="@id/qrCodeImage" + app:layout_constraintStart_toStartOf="@id/qrCodeImage" + app:layout_constraintTop_toTopOf="@id/qrCodeImage" /> + + <LinearLayout + android:id="@+id/info_box" + style="@style/Card.NoElevation" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginHorizontal="24dp" + android:layout_marginTop="8dp" + android:orientation="vertical" + android:padding="16dp" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/qrCodeImage" + app:layout_constraintVertical_chainStyle="packed"> + + <TextView + style="@style/body2" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_marginTop="8dp" + android:text="@string/rat_qr_code_profile_description" /> + + </LinearLayout> + + <LinearLayout + android:id="@+id/person_data" + style="@style/Card.NoElevation" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginHorizontal="24dp" + android:layout_marginTop="8dp" + android:orientation="vertical" + android:padding="16dp" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/info_box" + app:layout_constraintVertical_chainStyle="packed"> + + <TextView + android:id="@+id/profile_info" + style="@style/materialSubtitleSixteen" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:lineSpacingExtra="8dp" + tools:text="Max Mustermann\ngeboren 17.11.1990\nLange Straße 51\n4471 Potsdam\n0151123456789\nmaxmustermann@web.de" /> + + </LinearLayout> + + </androidx.constraintlayout.widget.ConstraintLayout> + + </androidx.core.widget.NestedScrollView> + </androidx.coordinatorlayout.widget.CoordinatorLayout> <Button - android:id="@+id/closeButton" + android:id="@+id/next_button" style="@style/buttonPrimary" - android:layout_width="match_parent" + android:layout_width="0dp" android:layout_height="wrap_content" - android:layout_gravity="bottom|center" - android:layout_marginHorizontal="24dp" - android:layout_marginBottom="24dp" - android:text="Weiter" /> + android:layout_marginStart="@dimen/spacing_normal" + android:layout_marginEnd="@dimen/spacing_normal" + android:layout_marginBottom="8dp" + android:text="@string/rat_qr_code_profile_next_button" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" /> -</FrameLayout> \ No newline at end of file +</androidx.constraintlayout.widget.ConstraintLayout> \ No newline at end of file diff --git a/Corona-Warn-App/src/main/res/layout/trace_location_organizer_qr_code_detail_fragment.xml b/Corona-Warn-App/src/main/res/layout/trace_location_organizer_qr_code_detail_fragment.xml index 36589681fc501f9e38582a7790969b2071e1dab8..8ffc939f6e8957cdb5d1c5923692cc43418eb0f0 100644 --- a/Corona-Warn-App/src/main/res/layout/trace_location_organizer_qr_code_detail_fragment.xml +++ b/Corona-Warn-App/src/main/res/layout/trace_location_organizer_qr_code_detail_fragment.xml @@ -122,7 +122,7 @@ android:layout_marginHorizontal="24dp" android:layout_marginVertical="12dp" android:layout_marginTop="16dp" - android:background="@drawable/trace_location_qr_code_background" + android:background="@drawable/rounded_white_background" android:contentDescription="@string/trace_location_event_detail_qr_code_accessibility" app:layout_constraintDimensionRatio="H,1:1" app:layout_constraintLeft_toLeftOf="parent" diff --git a/Corona-Warn-App/src/main/res/menu/menu_rat_qr_code_profile.xml b/Corona-Warn-App/src/main/res/menu/menu_rat_qr_code_profile.xml new file mode 100644 index 0000000000000000000000000000000000000000..7ee450aee57a945098e8e29ce8ffaf3a4cfa7612 --- /dev/null +++ b/Corona-Warn-App/src/main/res/menu/menu_rat_qr_code_profile.xml @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="utf-8"?> +<menu xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto"> + + <item + android:title="@string/rat_qr_code_profile_delete_item" + app:showAsAction="never" /> +</menu> \ No newline at end of file diff --git a/Corona-Warn-App/src/main/res/navigation/rapid_antigen_test_profile_nav_graph.xml b/Corona-Warn-App/src/main/res/navigation/rapid_antigen_test_profile_nav_graph.xml index 8aae017026d6ffc20a3b66626ff26ab28a886610..09d3bd47f25919ae4124271e1b47b91f0eeb93e6 100644 --- a/Corona-Warn-App/src/main/res/navigation/rapid_antigen_test_profile_nav_graph.xml +++ b/Corona-Warn-App/src/main/res/navigation/rapid_antigen_test_profile_nav_graph.xml @@ -12,7 +12,9 @@ tools:layout="@layout/rat_profile_onboarding_fragment"> <action android:id="@+id/action_ratProfileOnboardingFragment_to_ratProfileCreateFragment" - app:destination="@id/ratProfileCreateFragment" /> + app:destination="@id/ratProfileCreateFragment" + app:popUpTo="@id/rapid_test_profile_nav_graph" + app:popUpToInclusive="false" /> <action android:id="@+id/action_ratProfileOnboardingFragment_to_privacyFragment" app:destination="@id/privacyFragment" /> diff --git a/Corona-Warn-App/src/main/res/values-de/antigen_strings.xml b/Corona-Warn-App/src/main/res/values-de/antigen_strings.xml index 0696cdf0c652ae2a38f623747588a5f92176ce8d..fee399d51a3ac30964ad7ff56e9bba103c8e4bf9 100644 --- a/Corona-Warn-App/src/main/res/values-de/antigen_strings.xml +++ b/Corona-Warn-App/src/main/res/values-de/antigen_strings.xml @@ -181,4 +181,20 @@ <string name="rat_profile_onboarding_next">Weiter</string> <!-- XTXT: Create RAT profile onboarding privacy --> <string name="rat_profile_onboarding_privacy">Ausführliche Informationen zur Datenverarbeitung und Ihrem Einverständnis.</string> + <!-- XTXT: Create RAT Qr code profile description--> + <string name="rat_qr_code_profile_description">Bitte zeigen Sie diesen QR-Code an der Teststelle vor, um Ihre persönlichen Daten schnell erfassen zu lassen. Bitte halten zusätzlich Sie Ihren Personalausweis bereit.</string> + <!-- XTXT: Create RAT Qr code profile birth date--> + <string name="rat_qr_code_profile_birth_date">geboren %1$s</string> + <!-- XBUT: Create RAT Qr code profile next button--> + <string name="rat_qr_code_profile_next_button">Weiter</string> + <!-- XTXT: Create RAT Qr code profile delete item--> + <string name="rat_qr_code_profile_delete_item">Entfernen</string> + <!-- XTXT: Create RAT Qr code profile delete dialog title--> + <string name="rat_qr_code_profile_dialog_title">Wollen Sie das Schnelltest-Profil wirklich entfernen?</string> + <!-- XTXT: Create RAT Qr code profile delete dialog message--> + <string name="rat_qr_code_profile_dialog_message">Bitte denken Sie daran, dass der QR-Code danach nicht mehr zum Registrieren verwendet werden kann.</string> + <!-- XTXT: Create RAT Qr code profile delete dialog positive button--> + <string name="rat_qr_code_profile_dialog_positive_button">Löschen</string> + <!-- XTXT: Create RAT Qr code profile delete dialog negative button--> + <string name="rat_qr_code_profile_dialog_negative_button">Abbrechen</string> </resources> diff --git a/Corona-Warn-App/src/main/res/values/antigen_strings.xml b/Corona-Warn-App/src/main/res/values/antigen_strings.xml index 1cd6f7682a8f13640dde151163ae19d68880cb93..00ff0072b458eb6408ecd1b5312108a9b6d00779 100644 --- a/Corona-Warn-App/src/main/res/values/antigen_strings.xml +++ b/Corona-Warn-App/src/main/res/values/antigen_strings.xml @@ -181,4 +181,20 @@ <string name="rat_profile_onboarding_next">Weiter</string> <!-- XTXT: Create RAT profile onboarding privacy --> <string name="rat_profile_onboarding_privacy">Ausführliche Informationen zur Datenverarbeitung und Ihrem Einverständnis.</string> + <!-- XTXT: Create RAT Qr code profile description--> + <string name="rat_qr_code_profile_description">Bitte zeigen Sie diesen QR-Code an der Teststelle vor, um Ihre persönlichen Daten schnell erfassen zu lassen. Bitte halten zusätzlich Sie Ihren Personalausweis bereit.</string> + <!-- XTXT: Create RAT Qr code profile birth date--> + <string name="rat_qr_code_profile_birth_date">geboren %1$s</string> + <!-- XBUT: Create RAT Qr code profile next button--> + <string name="rat_qr_code_profile_next_button">Weiter</string> + <!-- XTXT: Create RAT Qr code profile delete item--> + <string name="rat_qr_code_profile_delete_item">Entfernen</string> + <!-- XTXT: Create RAT Qr code profile delete dialog title--> + <string name="rat_qr_code_profile_dialog_title">Wollen Sie das Schnelltest-Profil wirklich entfernen?</string> + <!-- XTXT: Create RAT Qr code profile delete dialog message--> + <string name="rat_qr_code_profile_dialog_message">Bitte denken Sie daran, dass der QR-Code danach nicht mehr zum Registrieren verwendet werden kann.</string> + <!-- XTXT: Create RAT Qr code profile delete dialog positive button--> + <string name="rat_qr_code_profile_dialog_positive_button">Löschen</string> + <!-- XTXT: Create RAT Qr code profile delete dialog negative button--> + <string name="rat_qr_code_profile_dialog_negative_button">Abbrechen</string> </resources> diff --git a/Corona-Warn-App/src/main/res/values/styles.xml b/Corona-Warn-App/src/main/res/values/styles.xml index be7743ea4251a325806bf29993af49ddbaaeba5d..bc944522a40b87e9ab633b51e85237ac0e981cb3 100644 --- a/Corona-Warn-App/src/main/res/values/styles.xml +++ b/Corona-Warn-App/src/main/res/values/styles.xml @@ -83,6 +83,14 @@ <item name="android:background">@color/colorTransparent</item> </style> + <style name="CWAToolbar.Theme" parent="Widget.AppCompat.ActionBar"> + <item name="actionOverflowButtonStyle">@style/OverflowButtonStyle</item> + </style> + + <style name="OverflowButtonStyle" parent="Widget.AppCompat.Light.ActionButton.Overflow"> + <item name="android:tint">@android:color/white</item> + </style> + <!-- Dialog Theme--> <style name="DialogTheme" parent="Theme.MaterialComponents.DayNight.Dialog.Alert"> <item name="buttonBarPositiveButtonStyle">@style/DialogButtonTheme</item> @@ -283,6 +291,7 @@ <style name="headline4" parent="@style/TextAppearance.MaterialComponents.Headline4"> <item name="android:textColor">@color/colorTextPrimary1</item> </style> + <style name="headline4Bold" parent="@style/headline4"> <item name="android:textStyle">bold</item> </style> diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/antigen/profile/VCardTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/antigen/profile/VCardTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..9d8c91141da23d079fa53a580d3d6c369d1aaaa0 --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/antigen/profile/VCardTest.kt @@ -0,0 +1,133 @@ +package de.rki.coronawarnapp.coronatest.antigen.profile + +import de.rki.coronawarnapp.util.TimeStamper +import io.kotest.matchers.shouldBe +import io.mockk.MockKAnnotations +import io.mockk.every +import io.mockk.impl.annotations.MockK +import org.joda.time.Instant +import org.joda.time.format.ISODateTimeFormat +import org.junit.jupiter.api.Test + +import org.junit.jupiter.api.BeforeEach +import testhelpers.BaseTest + +class VCardTest : BaseTest() { + + @MockK lateinit var timeStamper: TimeStamper + + @BeforeEach + fun setup() { + MockKAnnotations.init(this) + + every { timeStamper.nowUTC } returns Instant.parse("1995-10-31T22:27:10Z") + } + + @Test + fun `Case 1`() { + val profile = RATProfile( + firstName = "Max", + lastName = "Mustermann", + birthDate = ISODateTimeFormat.basicDate().parseLocalDate("19800625"), + street = "Musterstrasse 14", + zipCode = "51466", + city = "Musterstadt", + phone = "0190 1234567", + email = "max@mustermann.de" + ) + VCard(timeStamper).create(profile) shouldBe + """ + BEGIN:VCARD + VERSION:4.0 + N:Mustermann;Max;;; + FN:Max Mustermann + BDAY:19800625 + EMAIL;TYPE=home:max@mustermann.de + TEL;TYPE="cell,home":0190 1234567 + ADR;TYPE=home:;;Musterstrasse 14;Musterstadt;;51466 + REV:19951031T222710Z + END:VCARD + """.trimIndent() + } + + @Test + fun `Case 2`() { + val profile = RATProfile( + firstName = "", + lastName = "", + birthDate = null, + street = "", + zipCode = "", + city = "", + phone = "", + email = "" + ) + VCard(timeStamper).create(profile) shouldBe + """ + BEGIN:VCARD + VERSION:4.0 + N:;;;; + FN: + BDAY: + EMAIL;TYPE=home: + TEL;TYPE="cell,home": + ADR;TYPE=home:;;;;; + REV:19951031T222710Z + END:VCARD + """.trimIndent() + } + + @Test + fun `Case 3`() { + val profile = RATProfile( + firstName = "Max", + lastName = "Mustermann", + birthDate = ISODateTimeFormat.basicDate().parseLocalDate("19800625"), + street = "Mu\\ster;stra,sse 14", + zipCode = "51466", + city = "Musterstadt", + phone = "0190 1234567", + email = "max@mustermann.de" + ) + VCard(timeStamper).create(profile) shouldBe + """ + BEGIN:VCARD + VERSION:4.0 + N:Mustermann;Max;;; + FN:Max Mustermann + BDAY:19800625 + EMAIL;TYPE=home:max@mustermann.de + TEL;TYPE="cell,home":0190 1234567 + ADR;TYPE=home:;;Mu\\ster\;stra\,sse 14;Musterstadt;;51466 + REV:19951031T222710Z + END:VCARD + """.trimIndent() + } + + @Test + fun `Case 4`() { + val profile = RATProfile( + firstName = "Max,", + lastName = "Mustermann;", + birthDate = ISODateTimeFormat.basicDate().parseLocalDate("19800625"), + street = "Mu\\\\ster;stra,sse 14\nA", + zipCode = "51466", + city = "Muster city \n Upper \\county, DC ; US", + phone = "0190 \\1234567", + email = "max@mustermann,;\\.de" + ) + VCard(timeStamper).create(profile) shouldBe + """ + BEGIN:VCARD + VERSION:4.0 + N:Mustermann\;;Max\,;;; + FN:Max\, Mustermann\; + BDAY:19800625 + EMAIL;TYPE=home:max@mustermann\,\;\\.de + TEL;TYPE="cell,home":0190 \\1234567 + ADR;TYPE=home:;;Mu\\\\ster\;stra\,sse 14A;Muster city Upper \\county\, DC \; US;;51466 + REV:19951031T222710Z + END:VCARD + """.trimIndent() + } +}