Skip to content
This repository has been archived by the owner on Jun 20, 2023. It is now read-only.

Commit

Permalink
Create Check-in confirmation screen (EXPOSUREAPP-5424) (#2536)
Browse files Browse the repository at this point in the history
* Refactoring

- Add basic setup for attendee and organizer
- Renaming and re-packing for already implemented screens

* Rename

* Add check in tab

* lint

* Connect scan fragment

* Navigate to confirm event

* Fix import

* lint

* Add FAB text

* Update MDC version

* Catch error

* Animate transition

* Add space

* Connect check-in flow

* Parse signed event

* Import SingleLiveData

* Add test

* Clean-up

* Delete ConfirmCheckInViewModel.kt

* Support new deeplink host and requirements

* Validate uri

* Update LauncherActivityTest.kt

* Renaming

* Trace location times are in seconds

* Delete redundants

* Remove destinations from main graph

those destinations are part of attendee graph now

* Use hard coded string

* Verify uri

* Pass QRCodeVerifyResult directly

* lint
  • Loading branch information
mtwalli authored Mar 11, 2021
1 parent e9e7635 commit b861799
Show file tree
Hide file tree
Showing 46 changed files with 569 additions and 242 deletions.
2 changes: 1 addition & 1 deletion Corona-Warn-App/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -307,7 +307,7 @@ dependencies {
def nav_version = "2.3.3"
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'androidx.core:core-ktx:1.3.2'
implementation 'com.google.android.material:material:1.2.1'
implementation 'com.google.android.material:material:1.3.0'
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"
implementation "androidx.navigation:navigation-ui-ktx:$nav_version"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package de.rki.coronawarnapp.ui.eventregistration.attendee.checkin

import de.rki.coronawarnapp.eventregistration.checkins.qrcode.QRCodeVerifyResult
import de.rki.coronawarnapp.eventregistration.common.decodeBase32
import de.rki.coronawarnapp.server.protocols.internal.evreg.SignedEventOuterClass
import io.kotest.assertions.throwables.shouldNotThrowAny
import io.kotest.matchers.shouldBe
import org.joda.time.Instant
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
import testhelpers.BaseTestInstrumentation

@RunWith(JUnit4::class)
class VerifiedTraceLocationKtTest : BaseTestInstrumentation() {

@Test
fun testVerifiedTraceLocationMapping() {
shouldNotThrowAny {
val signedTraceLocation =
SignedEventOuterClass.SignedEvent.parseFrom(DECODED_TRACE_LOCATION.decodeBase32().toByteArray())
val verifiedTraceLocation =
QRCodeVerifyResult(singedTraceLocation = signedTraceLocation).toVerifiedTraceLocation()
verifiedTraceLocation shouldBe VerifiedTraceLocation(
guid = "Yc48RFi/hfyXKlF4DEDs/w==",
start = Instant.parse("1970-02-01T02:39:15.000Z"),
end = Instant.parse("1970-02-01T02:39:51.000Z"),
defaultCheckInLengthInMinutes = 30,
description = "CWA Launch Party"
)
}
}

companion object {
private const val DECODED_TRACE_LOCATION =
"BIYAUEDBZY6EIWF7QX6JOKSRPAGEB3H7CIIEGV2BEBGGC5LOMNUCAUDBOJ2HSGGTQ6SACIHXQ6SAC" +
"KA6CJEDARQCEEAPHGEZ5JI2K2T422L5U3SMZY5DGCPUZ2RQACAYEJ3HQYMAFFBU2SQCEEAJAUCJSQJ7WDM6" +
"75MCMOD3L2UL7ECJU7TYERH23B746RQTABO3CTI="
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -59,13 +59,7 @@ class LauncherActivityTest : BaseUITest() {

@Test
fun testDeepLinkLowercase() {
val uri = Uri.parse("https://coronawarn.app/E1/SOME_PATH_GOES_HERE")
launchActivity<LauncherActivity>(getIntent(uri))
}

@Test
fun testDeepLinkLowercaseWww() {
val uri = Uri.parse("https://www.coronawarn.app/E1/SOME_PATH_GOES_HERE")
val uri = Uri.parse("https://e.coronawarn.app/c1/SOME_PATH_GOES_HERE")
launchActivity<LauncherActivity>(getIntent(uri))
}

Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package de.rki.coronawarnapp.test.eventregistration.ui.qrcode

import android.annotation.SuppressLint
import android.os.Bundle
import android.view.View
import android.widget.Toast
Expand All @@ -21,6 +22,7 @@ class QrCodeCreationTestFragment : Fragment(R.layout.fragment_test_qrcode_creati
private val viewModel: QrCodeCreationTestViewModel by cwaViewModels { viewModelFactory }
private val binding: FragmentTestQrcodeCreationBinding by viewBindingLazy()

@SuppressLint("SetTextI18n")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {

viewModel.sharingIntent.observe2(this) {
Expand All @@ -36,6 +38,11 @@ class QrCodeCreationTestFragment : Fragment(R.layout.fragment_test_qrcode_creati
Toast.makeText(requireContext(), it, Toast.LENGTH_LONG).show()
}

binding.qrCodeText.setText(
"HTTPS://E.CORONAWARN.APP/C1/BIYAUEDBZY6EIWF7QX6JOKSRPAGEB3H7CIIEGV2BEBGGC5LOMNUCAUD" +
"BOJ2HSGGTQ6SACIHXQ6SACKA6CJEDARQCEEAPHGEZ5JI2K2T422L5U3SMZY5DGCPUZ2RQACAYEJ3HQYMAFF" +
"BU2SQCEEAJAUCJSQJ7WDM675MCMOD3L2UL7ECJU7TYERH23B746RQTABO3CTI="
)
binding.generateQrCode.setOnClickListener {
viewModel.createQrCode(binding.qrCodeText.text.toString())
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@
android:id="@+id/qrCodeText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/qr_code_input"
android:textSize="14sp" />
</com.google.android.material.textfield.TextInputLayout>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@
tools:layout="@layout/fragment_test_qrcode_creation" />
<fragment
android:id="@+id/scanCheckInQrCodeFragmentTest"
android:name="de.rki.coronawarnapp.ui.eventregistration.scan.ScanCheckInQrCodeFragment"
android:name="de.rki.coronawarnapp.ui.eventregistration.attendee.scan.ScanCheckInQrCodeFragment"
android:label="ScanCheckInQrCodeFragment"
tools:layout="@layout/fragment_submission_qr_code_scan" />

Expand Down
4 changes: 0 additions & 4 deletions Corona-Warn-App/src/deviceForTesters/res/values/strings.xml

This file was deleted.

7 changes: 1 addition & 6 deletions Corona-Warn-App/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -69,12 +69,7 @@
<category android:name="android.intent.category.BROWSABLE" />

<data
android:host="coronawarn.app"
android:pathPrefix="/"
android:scheme="https" />

<data
android:host="www.coronawarn.app"
android:host="e.coronawarn.app"
android:pathPrefix="/"
android:scheme="https" />
</intent-filter>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import dagger.Module
import de.rki.coronawarnapp.eventregistration.checkins.qrcode.DefaultQRCodeVerifier
import de.rki.coronawarnapp.eventregistration.checkins.qrcode.QRCodeVerifier

@Suppress("EmptyClassBlock")
@Module
abstract class EventRegistrationModule {
@Binds
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package de.rki.coronawarnapp.eventregistration.checkins.qrcode

import java.net.URI

private const val SCHEME = "https"
private const val AUTHORITY = "e.coronawarn.app"
private const val PATH_PREFIX = "/c1"
private const val SIGNED_TRACE_LOCATION_BASE_32_REGEX =
"^(?:[A-Z2-7]{8})*(?:[A-Z2-7]{2}={6}|[A-Z2-7]{4}={4}|[A-Z2-7]{5}={3}|[A-Z2-7]{7}=)?\$"

/**
* Validate that QRCode scanned uri matches the following formulas:
* https://e.coronawarn.app/c1/SIGNED_TRACE_LOCATION_BASE32
* HTTPS://E.CORONAWARN.APP/C1/SIGNED_TRACE_LOCATION_BASE32
*/
fun String.isValidQRCodeUri(): Boolean =
URI.create(this).run {
scheme.equals(SCHEME, true) &&
authority.equals(AUTHORITY, true) &&
path.substringBeforeLast("/")
.equals(PATH_PREFIX, true) &&
path.substringAfterLast("/")
.matches(Regex(SIGNED_TRACE_LOCATION_BASE_32_REGEX))
}
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ fun BottomNavigationView.setupWithNavController2(
// For destinations that always show the bottom bar
val inShowList = destination.id in listOf(
R.id.mainFragment,
R.id.checkInsFragment,
R.id.contactDiaryOverviewFragment
)
// For destinations that can show or hide the bottom bar in different cases
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ package de.rki.coronawarnapp.ui.eventregistration

import dagger.Module
import dagger.android.ContributesAndroidInjector
import de.rki.coronawarnapp.ui.eventregistration.checkin.ConfirmCheckInFragment
import de.rki.coronawarnapp.ui.eventregistration.checkin.ConfirmCheckInModule
import de.rki.coronawarnapp.ui.eventregistration.scan.ScanCheckInQrCodeFragment
import de.rki.coronawarnapp.ui.eventregistration.scan.ScanCheckInQrCodeModule
import de.rki.coronawarnapp.ui.eventregistration.attendee.checkin.CheckInsFragment
import de.rki.coronawarnapp.ui.eventregistration.attendee.checkin.CheckInsModule
import de.rki.coronawarnapp.ui.eventregistration.attendee.confirm.ConfirmCheckInFragment
import de.rki.coronawarnapp.ui.eventregistration.attendee.confirm.ConfirmCheckInModule
import de.rki.coronawarnapp.ui.eventregistration.attendee.scan.ScanCheckInQrCodeFragment
import de.rki.coronawarnapp.ui.eventregistration.attendee.scan.ScanCheckInQrCodeModule

@Module
internal abstract class EventRegistrationUIModule {
Expand All @@ -15,4 +17,7 @@ internal abstract class EventRegistrationUIModule {

@ContributesAndroidInjector(modules = [ConfirmCheckInModule::class])
abstract fun confirmCheckInFragment(): ConfirmCheckInFragment

@ContributesAndroidInjector(modules = [CheckInsModule::class])
abstract fun checkInsFragment(): CheckInsFragment
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package de.rki.coronawarnapp.ui.eventregistration.attendee.checkin

import android.net.Uri
import android.os.Bundle
import androidx.fragment.app.Fragment
import android.view.View
import androidx.core.net.toUri
import androidx.navigation.fragment.FragmentNavigatorExtras
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import com.google.android.material.transition.Hold
import de.rki.coronawarnapp.R
import de.rki.coronawarnapp.databinding.FragmentCheckInsBinding
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.viewBindingLazy
import de.rki.coronawarnapp.util.viewmodel.CWAViewModelFactoryProvider
import de.rki.coronawarnapp.util.viewmodel.cwaViewModels
import javax.inject.Inject

class CheckInsFragment : Fragment(R.layout.fragment_check_ins), AutoInject {

@Inject lateinit var viewModelFactory: CWAViewModelFactoryProvider.Factory
private val viewModel: CheckInsViewModel by cwaViewModels { viewModelFactory }
private val binding: FragmentCheckInsBinding by viewBindingLazy()

// Encoded uri is a one-time use data and then cleared
private val uri: String?
get() = navArgs<CheckInsFragmentArgs>().value
.uri
.also { arguments?.clear() }

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
exitTransition = Hold()
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)

with(binding.scanCheckinQrcodeFab) {
setOnClickListener {
findNavController().navigate(
R.id.action_checkInsFragment_to_scanCheckInQrCodeFragment,
null,
null,
FragmentNavigatorExtras(this to transitionName)
)
}
}

uri?.let { viewModel.verifyUri(it) }
viewModel.verifyResult.observe2(this) {
doNavigate(
CheckInsFragmentDirections
.actionCheckInsFragmentToConfirmCheckInFragment(it.toVerifiedTraceLocation())
)
}
}

companion object {
fun uri(rootUri: String): Uri = "coronawarnapp://check-ins/$rootUri".toUri()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package de.rki.coronawarnapp.ui.eventregistration.attendee.checkin

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 CheckInsModule {
@Binds
@IntoMap
@CWAViewModelKey(CheckInsViewModel::class)
abstract fun checkInsFragment(
factory: CheckInsViewModel.Factory
): CWAViewModelFactory<out CWAViewModel>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package de.rki.coronawarnapp.ui.eventregistration.attendee.checkin

import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import de.rki.coronawarnapp.eventregistration.checkins.qrcode.QRCodeVerifier
import de.rki.coronawarnapp.eventregistration.checkins.qrcode.QRCodeVerifyResult
import de.rki.coronawarnapp.eventregistration.checkins.qrcode.isValidQRCodeUri
import de.rki.coronawarnapp.exception.ExceptionCategory
import de.rki.coronawarnapp.exception.reporting.report
import de.rki.coronawarnapp.util.coroutine.DispatcherProvider
import de.rki.coronawarnapp.util.viewmodel.CWAViewModel
import de.rki.coronawarnapp.util.viewmodel.SimpleCWAViewModelFactory
import timber.log.Timber

class CheckInsViewModel @AssistedInject constructor(
dispatcherProvider: DispatcherProvider,
private val qrCodeVerifier: QRCodeVerifier
) : CWAViewModel(dispatcherProvider) {

private val verifyResultData = MutableLiveData<QRCodeVerifyResult>()
val verifyResult: LiveData<QRCodeVerifyResult> = verifyResultData

fun verifyUri(uri: String) = launch {
try {
Timber.i("uri: $uri")
if (!uri.isValidQRCodeUri())
throw IllegalArgumentException("Invalid uri: $uri")

val encodedEvent = uri.substringAfterLast("/")
val verifyResult = qrCodeVerifier.verify(encodedEvent)
Timber.i("verifyResult: $verifyResult")
verifyResultData.postValue(verifyResult)
} catch (e: Exception) {
Timber.d(e, "TraceLocation verification failed")
e.report(ExceptionCategory.INTERNAL)
}
}

@AssistedFactory
interface Factory : SimpleCWAViewModelFactory<CheckInsViewModel>
}
Loading

0 comments on commit b861799

Please sign in to comment.