diff --git a/Corona-Warn-App/build.gradle b/Corona-Warn-App/build.gradle index e7fda93ad22d7261d4db870b6dd3ad44bdeaccf0..fe696278452c74c83edf5b794619dc26d090a8f4 100644 --- a/Corona-Warn-App/build.gradle +++ b/Corona-Warn-App/build.gradle @@ -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" @@ -382,7 +382,6 @@ dependencies { androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' androidTestImplementation 'androidx.test:rules:1.3.0' androidTestImplementation 'androidx.test.ext:truth:1.3.0' - androidTestImplementation 'androidx.test.ext:junit:1.1.2' androidTestImplementation 'androidx.work:work-testing:2.5.0' androidTestImplementation "io.mockk:mockk-android:1.10.4" debugImplementation 'androidx.fragment:fragment-testing:1.2.5' diff --git a/Corona-Warn-App/schemas/de.rki.coronawarnapp.eventregistration.storage.TraceLocationDatabase/1.json b/Corona-Warn-App/schemas/de.rki.coronawarnapp.eventregistration.storage.TraceLocationDatabase/1.json new file mode 100644 index 0000000000000000000000000000000000000000..f5c14553bb62386c785fe0f4f551332968cc8ee0 --- /dev/null +++ b/Corona-Warn-App/schemas/de.rki.coronawarnapp.eventregistration.storage.TraceLocationDatabase/1.json @@ -0,0 +1,180 @@ +{ + "formatVersion": 1, + "database": { + "version": 1, + "identityHash": "48d71b0a00d0c7fe01ffe05d5fecb512", + "entities": [ + { + "tableName": "checkin", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `guid` TEXT NOT NULL, `version` INTEGER NOT NULL, `type` INTEGER NOT NULL, `description` TEXT NOT NULL, `address` TEXT NOT NULL, `traceLocationStart` TEXT, `traceLocationEnd` TEXT, `defaultCheckInLengthInMinutes` INTEGER, `signature` TEXT NOT NULL, `checkInStart` TEXT NOT NULL, `checkInEnd` TEXT, `targetCheckInEnd` TEXT, `createJournalEntry` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "guid", + "columnName": "guid", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "address", + "columnName": "address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "traceLocationStart", + "columnName": "traceLocationStart", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "traceLocationEnd", + "columnName": "traceLocationEnd", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "defaultCheckInLengthInMinutes", + "columnName": "defaultCheckInLengthInMinutes", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "signature", + "columnName": "signature", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "checkInStart", + "columnName": "checkInStart", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "checkInEnd", + "columnName": "checkInEnd", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "targetCheckInEnd", + "columnName": "targetCheckInEnd", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createJournalEntry", + "columnName": "createJournalEntry", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "traceLocations", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`guid` TEXT NOT NULL, `version` INTEGER NOT NULL, `type` INTEGER NOT NULL, `description` TEXT NOT NULL, `address` TEXT NOT NULL, `startDate` TEXT, `endDate` TEXT, `defaultCheckInLengthInMinutes` INTEGER, `signature` TEXT NOT NULL, PRIMARY KEY(`guid`))", + "fields": [ + { + "fieldPath": "guid", + "columnName": "guid", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "address", + "columnName": "address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "startDate", + "columnName": "startDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "endDate", + "columnName": "endDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "defaultCheckInLengthInMinutes", + "columnName": "defaultCheckInLengthInMinutes", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "signature", + "columnName": "signature", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "guid" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '48d71b0a00d0c7fe01ffe05d5fecb512')" + ] + } +} \ No newline at end of file diff --git a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/DefaultQRCodeVerifierTest.kt b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/DefaultQRCodeVerifierTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..5e837010548de435714ef67edef1c885c00c4b56 --- /dev/null +++ b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/DefaultQRCodeVerifierTest.kt @@ -0,0 +1,111 @@ +package de.rki.coronawarnapp.eventregistration.checkins.qrcode + +import de.rki.coronawarnapp.environment.EnvironmentSetup +import de.rki.coronawarnapp.eventregistration.common.decodeBase32 +import de.rki.coronawarnapp.server.protocols.internal.pt.TraceLocationOuterClass +import de.rki.coronawarnapp.util.security.SignatureValidation +import io.kotest.assertions.throwables.shouldNotThrowAny +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.shouldBe +import io.mockk.MockKAnnotations +import io.mockk.every +import io.mockk.impl.annotations.MockK +import kotlinx.coroutines.test.runBlockingTest +import org.joda.time.Instant +import org.junit.Before +import org.junit.Ignore +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import testhelpers.BaseTestInstrumentation + +@RunWith(JUnit4::class) +@Ignore("FIXME: Provide new encoded signed trace location samples") +class DefaultQRCodeVerifierTest : BaseTestInstrumentation() { + + @MockK lateinit var environmentSetup: EnvironmentSetup + private lateinit var qrCodeVerifier: QRCodeVerifier + + @Before + fun setUp() { + MockKAnnotations.init(this) + every { environmentSetup.appConfigVerificationKey } returns PUB_KEY + qrCodeVerifier = DefaultQRCodeVerifier(SignatureValidation(environmentSetup)) + } + + @Test + fun verifyEventSuccess() = runBlockingTest { + val time = 2687960 * 1_000L + val instant = Instant.ofEpochMilli(time) + shouldNotThrowAny { + val verifyResult = qrCodeVerifier.verify(ENCODED_EVENT) + verifyResult.apply { + singedTraceLocation.location.description shouldBe "CWA Launch Party" + verifyResult.isBeforeStartTime(instant) shouldBe false + verifyResult.isAfterEndTime(instant) shouldBe false + } + } + } + + @Test + fun verifyEventStartTimeWaning() = runBlockingTest { + val time = 2687940 * 1_000L + val instant = Instant.ofEpochMilli(time) + shouldNotThrowAny { + val verifyResult = qrCodeVerifier.verify(ENCODED_EVENT) + verifyResult.apply { + singedTraceLocation.location.description shouldBe "CWA Launch Party" + } + verifyResult.isBeforeStartTime(instant) shouldBe true + verifyResult.isAfterEndTime(instant) shouldBe false + } + } + + @Test + fun verifyEventEndTimeWarning() = runBlockingTest { + val instant = Instant.now() + shouldNotThrowAny { + val verifyResult = qrCodeVerifier.verify(ENCODED_EVENT) + verifyResult.apply { + singedTraceLocation.location.description shouldBe "CWA Launch Party" + } + verifyResult.isBeforeStartTime(instant) shouldBe false + verifyResult.isAfterEndTime(instant) shouldBe true + } + } + + @Test + fun verifyEventWithInvalidKey() = runBlockingTest { + every { environmentSetup.appConfigVerificationKey } returns INVALID_PUB_KEY + shouldThrow<InvalidQRCodeSignatureException> { + qrCodeVerifier.verify(ENCODED_EVENT) + } + } + + @Test + fun eventHasMalformedData() = runBlockingTest { + shouldThrow<InvalidQRCodeDataException> { + qrCodeVerifier.verify(INVALID_ENCODED_EVENT) + } + } + + companion object { + + private const val INVALID_PUB_KEY = "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEc7DEstcUIRcyk35OYDJ95/hTg" + + "3UVhsaDXKT0zK7NhHPXoyzipEnOp3GyNXDVpaPi3cAfQmxeuFMZAIX2+6A5Xg==" + + private const val PUB_KEY = "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEafIKZOiRPuJWjKOUmKv7OTJWTyii" + + "4oCQLcGn3FgYoLQaJIvAM3Pl7anFDPPY/jxfqqrLyGc0f6hWQ9JPR3QjBw==" + + private const val INVALID_ENCODED_EVENT = + "BIPEY33SMVWSA2LQON2W2IDEN5WG64RAONUXIIDBNVSXILBAMNXRBCM4UQARRKM6UQASAHRKCC7CTDWGQ" + + "4JCO7RVZSWVIMQK4UPA.GBCAEIA7TEORBTUA25QHBOCWT26BCA5PORBS2E4FFWMJ3U" + + "U3P6SXOL7SHUBCA7UEZBDDQ2R6VRJH7WBJKVF7GZYJA6YMRN27IPEP7NKGGJSWX3XQ" + + private const val ENCODED_EVENT = + "BIYAUEDBZY6EIWF7QX6JOKSRPAGEB3H7CIIEGV2BEBGGC5LOMNUCAUDBOJ2" + + "HSGGTQ6SACIHXQ6SACKA6CJEDARQCEEAPHGEZ5JI2K2T422L5U3SMZY5DGC" + + "PUZ2RQACAYEJ3HQYMAFFBU2SQCEEAJAUCJSQJ7WDM675MCMOD3L2UL7ECJU" + + "7TYERH23B746RQTABO3CTI=" + } +} diff --git a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/eventregistration/storage/CheckInDatabaseData.kt b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/eventregistration/storage/CheckInDatabaseData.kt new file mode 100644 index 0000000000000000000000000000000000000000..2797a59ee206d546406dc7f7e460bb2839dfe756 --- /dev/null +++ b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/eventregistration/storage/CheckInDatabaseData.kt @@ -0,0 +1,40 @@ +package de.rki.coronawarnapp.eventregistration.storage + +import de.rki.coronawarnapp.eventregistration.events.TraceLocation +import de.rki.coronawarnapp.eventregistration.storage.entity.TraceLocationCheckInEntity +import org.joda.time.Instant + +object CheckInDatabaseData { + + val testCheckIn = TraceLocationCheckInEntity( + guid = "testGuid1", + version = 1, + type = TraceLocation.Type.TEMPORARY_OTHER.value, + description = "testDescription1", + address = "testAddress1", + traceLocationStart = Instant.parse("2021-01-01T12:00:00.000Z"), + traceLocationEnd = Instant.parse("2021-01-01T15:00:00.000Z"), + defaultCheckInLengthInMinutes = 15, + signature = "Signature", + checkInStart = Instant.parse("2021-01-01T12:30:00.000Z"), + checkInEnd = Instant.parse("2021-01-01T14:00:00.000Z"), + targetCheckInEnd = Instant.parse("2021-01-01T12:45:00.000Z"), + createJournalEntry = true + ) + + val testCheckInWithoutCheckOutTime = TraceLocationCheckInEntity( + guid = "testGuid2", + version = 1, + type = TraceLocation.Type.TEMPORARY_OTHER.value, + description = "testDescription2", + address = "testAddress2", + traceLocationStart = null, + traceLocationEnd = null, + defaultCheckInLengthInMinutes = null, + signature = "Signature", + checkInStart = Instant.parse("2021-01-01T12:30:00.000Z"), + checkInEnd = null, + targetCheckInEnd = Instant.parse("2021-01-01T12:45:00.000Z"), + createJournalEntry = true + ) +} diff --git a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/eventregistration/storage/TraceLocationCheckInDaoTest.kt b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/eventregistration/storage/TraceLocationCheckInDaoTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..60223bea5e51e2caba73607eb59fa01dabca3b01 --- /dev/null +++ b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/eventregistration/storage/TraceLocationCheckInDaoTest.kt @@ -0,0 +1,85 @@ +package de.rki.coronawarnapp.eventregistration.storage + +import androidx.room.Room +import androidx.test.core.app.ApplicationProvider +import de.rki.coronawarnapp.eventregistration.storage.CheckInDatabaseData.testCheckIn +import de.rki.coronawarnapp.eventregistration.storage.CheckInDatabaseData.testCheckInWithoutCheckOutTime +import io.kotest.matchers.shouldBe +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.runBlocking +import org.joda.time.Instant +import org.junit.After +import org.junit.Test +import testhelpers.BaseTestInstrumentation + +class TraceLocationCheckInDaoTest : BaseTestInstrumentation() { + + private val traceLocationDatabase = Room.inMemoryDatabaseBuilder( + ApplicationProvider.getApplicationContext(), + TraceLocationDatabase::class.java + ).build() + + private val checkInDao = traceLocationDatabase.eventCheckInDao() + + @After + fun tearDown() { + traceLocationDatabase.clearAllTables() + } + + @Test + fun traceLocationCheckInDaoShouldReturnNoEntriesInitially() = runBlocking { + val checkInsFlow = checkInDao.allEntries() + + checkInsFlow.first() shouldBe emptyList() + } + + @Test + fun traceLocationCheckInDaoShouldSuccessfullyInsertCheckIn() = runBlocking { + val checkInsFlow = checkInDao.allEntries() + + val generatedId = checkInDao.insert(testCheckIn) + + checkInsFlow.first() shouldBe listOf(testCheckIn.copy(id = generatedId)) + } + + @Test + fun traceLocationCheckInDaoShouldSuccessfullyInsertMultipleCheckIns() = runBlocking { + val checkInsFlow = checkInDao.allEntries() + + val testCheckInGeneratedId = checkInDao.insert(testCheckIn) + val testCheckInWithoutCheckOutTimeGeneratedId = checkInDao.insert(testCheckInWithoutCheckOutTime) + + checkInsFlow.first() shouldBe listOf( + testCheckIn.copy(id = testCheckInGeneratedId), + testCheckInWithoutCheckOutTime.copy(id = testCheckInWithoutCheckOutTimeGeneratedId) + ) + } + + @Test + fun traceLocationCheckInDaoShouldSuccessfullyUpdateCheckIn() = runBlocking { + val checkInsFlow = checkInDao.allEntries() + + val testCheckInGeneratedId = checkInDao.insert(testCheckInWithoutCheckOutTime) + + val updatedCheckIn = testCheckInWithoutCheckOutTime.copy( + id = testCheckInGeneratedId, + checkInEnd = Instant.parse("2021-01-01T14:00:00.000Z") + ) + + checkInDao.update(updatedCheckIn) + + checkInsFlow.first() shouldBe listOf(updatedCheckIn) + } + + @Test + fun traceLocationCheckInDaoShouldSuccessfullyDeleteAllCheckIns() = runBlocking { + val checkInsFlow = checkInDao.allEntries() + + checkInDao.insert(testCheckIn) + checkInDao.insert(testCheckInWithoutCheckOutTime) + + checkInDao.deleteAll() + + checkInsFlow.first() shouldBe emptyList() + } +} diff --git a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/eventregistration/storage/TraceLocationDaoTest.kt b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/eventregistration/storage/TraceLocationDaoTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..ab29881320b63e7230c4aea57350bbe554ba4dad --- /dev/null +++ b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/eventregistration/storage/TraceLocationDaoTest.kt @@ -0,0 +1,72 @@ +package de.rki.coronawarnapp.eventregistration.storage + +import androidx.room.Room +import androidx.test.core.app.ApplicationProvider +import de.rki.coronawarnapp.eventregistration.storage.TraceLocationDatabaseData.testTraceLocation1 +import de.rki.coronawarnapp.eventregistration.storage.TraceLocationDatabaseData.testTraceLocation2 +import io.kotest.matchers.shouldBe +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.runBlocking +import org.junit.After +import org.junit.Test +import testhelpers.BaseTestInstrumentation + +class TraceLocationDaoTest : BaseTestInstrumentation() { + + private val traceLocationDatabase = Room.inMemoryDatabaseBuilder( + ApplicationProvider.getApplicationContext(), + TraceLocationDatabase::class.java + ).build() + + private val traceLocationDao = traceLocationDatabase.traceLocationDao() + + @After + fun tearDown() { + traceLocationDatabase.clearAllTables() + } + + @Test + fun traceLocationDaoShouldReturnNoEntriesInitially() = runBlocking { + val traceLocationsFlow = traceLocationDao.allEntries() + + traceLocationsFlow.first() shouldBe emptyList() + } + + @Test + fun traceLocationDaoShouldSuccessfullyInsertTraceLocation() = runBlocking { + val traceLocationsFlow = traceLocationDao.allEntries() + + traceLocationDao.insert(testTraceLocation1) + + traceLocationsFlow.first() shouldBe listOf(testTraceLocation1) + } + + @Test + fun traceLocationDaoShouldSuccessfullyInsertMultipleTraceLocations() = runBlocking { + val traceLocationsFlow = traceLocationDao.allEntries() + + traceLocationDao.insert(testTraceLocation1) + traceLocationDao.insert(testTraceLocation2) + + traceLocationsFlow.first() shouldBe listOf(testTraceLocation1, testTraceLocation2) + } + + @Test + fun traceLocationDaoShouldSuccessfullyDeleteSingleTraceLocation() = runBlocking { + val traceLocationsFlow = traceLocationDao.allEntries() + + traceLocationDao.insert(testTraceLocation1) + traceLocationDao.delete(testTraceLocation1) + traceLocationsFlow.first() shouldBe emptyList() + } + + @Test + fun traceLocationDaoShouldSuccessfullyDeleteAllTraceLocations() = runBlocking { + val traceLocationsFlow = traceLocationDao.allEntries() + + traceLocationDao.insert(testTraceLocation1) + traceLocationDao.insert(testTraceLocation2) + traceLocationDao.deleteAll() + traceLocationsFlow.first() shouldBe emptyList() + } +} diff --git a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/eventregistration/storage/TraceLocationDatabaseData.kt b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/eventregistration/storage/TraceLocationDatabaseData.kt new file mode 100644 index 0000000000000000000000000000000000000000..3891b6bdfd0e2d397f6208a0381a67b9ae360cce --- /dev/null +++ b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/eventregistration/storage/TraceLocationDatabaseData.kt @@ -0,0 +1,32 @@ +package de.rki.coronawarnapp.eventregistration.storage + +import de.rki.coronawarnapp.eventregistration.events.TraceLocation +import de.rki.coronawarnapp.eventregistration.storage.entity.TraceLocationEntity +import org.joda.time.Instant + +object TraceLocationDatabaseData { + + val testTraceLocation1 = TraceLocationEntity( + guid = "TestGuid1", + version = 1, + type = TraceLocation.Type.TEMPORARY_OTHER, + description = "TestTraceLocation1", + address = "TestTraceLocationAddress1", + startDate = Instant.parse("2021-01-01T12:00:00.000Z"), + endDate = Instant.parse("2021-01-01T18:00:00.000Z"), + defaultCheckInLengthInMinutes = null, + signature = "signature1" + ) + + val testTraceLocation2 = TraceLocationEntity( + guid = "TestGuid2", + version = 1, + type = TraceLocation.Type.PERMANENT_OTHER, + description = "TestTraceLocation2", + address = "TestTraceLocationAddress2", + startDate = null, + endDate = null, + defaultCheckInLengthInMinutes = 15, + signature = "signature2" + ) +} diff --git a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/eventregistration/attendee/checkin/VerifiedTraceLocationKtTest.kt b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/eventregistration/attendee/checkin/VerifiedTraceLocationKtTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..9935f9eed60890c9939c8cc860ee6cf9f603e5ef --- /dev/null +++ b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/eventregistration/attendee/checkin/VerifiedTraceLocationKtTest.kt @@ -0,0 +1,44 @@ +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.pt.TraceLocationOuterClass +import io.kotest.assertions.throwables.shouldNotThrowAny +import io.kotest.matchers.shouldBe +import org.joda.time.Instant +import org.junit.Ignore +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import testhelpers.BaseTestInstrumentation + +@RunWith(JUnit4::class) +@Ignore("FIXME: Provide new encoded signed trace location samples") +class VerifiedTraceLocationKtTest : BaseTestInstrumentation() { + + @Test + fun testVerifiedTraceLocationMapping() { + shouldNotThrowAny { + val signedTraceLocation = + TraceLocationOuterClass.SignedTraceLocation.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=" + } +} diff --git a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/launcher/LauncherActivityTest.kt b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/launcher/LauncherActivityTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..9d6924d1d165d12308513254e14a127f9e88793f --- /dev/null +++ b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/launcher/LauncherActivityTest.kt @@ -0,0 +1,98 @@ +package de.rki.coronawarnapp.ui.launcher + +import android.content.Intent +import android.net.Uri +import androidx.test.core.app.launchActivity +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import dagger.Module +import dagger.android.ContributesAndroidInjector +import de.rki.coronawarnapp.main.CWASettings +import de.rki.coronawarnapp.storage.LocalData +import de.rki.coronawarnapp.update.UpdateChecker +import de.rki.coronawarnapp.util.ui.SingleLiveEvent +import io.mockk.MockKAnnotations +import io.mockk.Runs +import io.mockk.coEvery +import io.mockk.every +import io.mockk.impl.annotations.MockK +import io.mockk.just +import io.mockk.mockk +import io.mockk.mockkObject +import io.mockk.spyk +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import testhelpers.BaseUITest +import testhelpers.TestDispatcherProvider + +@RunWith(AndroidJUnit4::class) +class LauncherActivityTest : BaseUITest() { + + @MockK lateinit var updateChecker: UpdateChecker + @MockK lateinit var cwaSettings: CWASettings + lateinit var viewModel: LauncherActivityViewModel + + @Before + fun setup() { + MockKAnnotations.init(this) + mockkObject(LocalData) + coEvery { updateChecker.checkForUpdate() } returns UpdateChecker.Result(isUpdateNeeded = false) + every { LocalData.isOnboarded() } returns false + viewModel = launcherActivityViewModel() + setupMockViewModel( + object : LauncherActivityViewModel.Factory { + override fun create(): LauncherActivityViewModel = viewModel + } + ) + + every { viewModel.events } returns mockk<SingleLiveEvent<LauncherEvent>>().apply { + every { observe(any(), any()) } just Runs + } + } + + @After + fun teardown() { + clearAllViewModels() + } + + @Test + fun testDeepLinkLowercase() { + val uri = Uri.parse("https://e.coronawarn.app/c1/SOME_PATH_GOES_HERE") + launchActivity<LauncherActivity>(getIntent(uri)) + } + + @Test(expected = RuntimeException::class) + fun testDeepLinkDoNotOpenOtherLinks() { + val uri = Uri.parse("https://www.rki.de") + launchActivity<LauncherActivity>(getIntent(uri)) + } + + @Test(expected = RuntimeException::class) + fun testDeepLinkUppercase() { + // Host is case sensitive and it should be only in lowercase + val uri = Uri.parse("HTTPS://CORONAWARN.APP/E1/SOME_PATH_GOES_HERE") + launchActivity<LauncherActivity>(getIntent(uri)) + } + + private fun getIntent(uri: Uri) = Intent(Intent.ACTION_VIEW, uri).apply { + setPackage(InstrumentationRegistry.getInstrumentation().targetContext.packageName) + addCategory(Intent.CATEGORY_BROWSABLE) + addCategory(Intent.CATEGORY_DEFAULT) + } + + private fun launcherActivityViewModel() = spyk( + LauncherActivityViewModel( + updateChecker, + TestDispatcherProvider(), + cwaSettings + ) + ) +} + +@Module +abstract class LauncherActivityTestModule { + @ContributesAndroidInjector + abstract fun launcherActivity(): LauncherActivity +} diff --git a/Corona-Warn-App/src/androidTest/java/testhelpers/TestAppComponent.kt b/Corona-Warn-App/src/androidTest/java/testhelpers/TestAppComponent.kt index 203a4d16b69a8e91d3845c315eb96b28eaf30e67..bafd923f25f69ba70ec07f32dba3d6c26cd72c6e 100644 --- a/Corona-Warn-App/src/androidTest/java/testhelpers/TestAppComponent.kt +++ b/Corona-Warn-App/src/androidTest/java/testhelpers/TestAppComponent.kt @@ -4,6 +4,7 @@ import dagger.BindsInstance import dagger.Component import dagger.android.AndroidInjector import dagger.android.support.AndroidSupportInjectionModule +import de.rki.coronawarnapp.ui.launcher.LauncherActivityTestModule import testhelpers.viewmodels.MockViewModelModule import javax.inject.Singleton @@ -13,6 +14,7 @@ import javax.inject.Singleton MockViewModelModule::class, FragmentTestModuleRegistrar::class, TestAndroidModule::class, + LauncherActivityTestModule::class, ] ) @Singleton diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/EventRegistrationTestFragment.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/EventRegistrationTestFragment.kt new file mode 100644 index 0000000000000000000000000000000000000000..7ccb3476a11a996eb1c0d6e3bf04f54b4b1c9646 --- /dev/null +++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/EventRegistrationTestFragment.kt @@ -0,0 +1,61 @@ +package de.rki.coronawarnapp.test.eventregistration.ui + +import android.annotation.SuppressLint +import android.os.Bundle +import android.view.View +import androidx.fragment.app.Fragment +import androidx.navigation.fragment.findNavController +import de.rki.coronawarnapp.R +import de.rki.coronawarnapp.databinding.FragmentTestEventregistrationBinding +import de.rki.coronawarnapp.test.menu.ui.TestMenuItem +import de.rki.coronawarnapp.util.di.AutoInject +import de.rki.coronawarnapp.util.ui.doNavigate +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 + +@SuppressLint("SetTextI18n") +class EventRegistrationTestFragment : Fragment(R.layout.fragment_test_eventregistration), AutoInject { + + @Inject lateinit var viewModelFactory: CWAViewModelFactoryProvider.Factory + private val viewModel: EventRegistrationTestFragmentViewModel by cwaViewModels { viewModelFactory } + + private val binding: FragmentTestEventregistrationBinding by viewBindingLazy() + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + with(binding) { + scanCheckInQrCode.setOnClickListener { + doNavigate( + EventRegistrationTestFragmentDirections + .actionEventRegistrationTestFragmentToScanCheckInQrCodeFragment() + ) + } + + testQrCodeCreation.setOnClickListener { + doNavigate( + EventRegistrationTestFragmentDirections + .actionEventRegistrationTestFragmentToTestQrCodeCreationFragment() + ) + } + + createEventButton.setOnClickListener { + findNavController().navigate(R.id.createEventTestFragment) + } + + showEventsButton.setOnClickListener { + findNavController().navigate(R.id.showStoredEventsTestFragment) + } + } + } + + companion object { + val MENU_ITEM = TestMenuItem( + title = "Event Registration", + description = "View & Control the event registration.", + targetId = R.id.eventRegistrationTestFragment + ) + } +} diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/EventRegistrationTestFragmentModule.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/EventRegistrationTestFragmentModule.kt new file mode 100644 index 0000000000000000000000000000000000000000..3c95a0a4a47d729182eca300cc9e5b7a70ad655f --- /dev/null +++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/EventRegistrationTestFragmentModule.kt @@ -0,0 +1,18 @@ +package de.rki.coronawarnapp.test.eventregistration.ui + +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 EventRegistrationTestFragmentModule { + @Binds + @IntoMap + @CWAViewModelKey(EventRegistrationTestFragmentViewModel::class) + abstract fun testEventRegistrationFragment( + factory: EventRegistrationTestFragmentViewModel.Factory + ): CWAViewModelFactory<out CWAViewModel> +} diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/EventRegistrationTestFragmentViewModel.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/EventRegistrationTestFragmentViewModel.kt new file mode 100644 index 0000000000000000000000000000000000000000..105a19bd4b837e72457a9be0033ae564a7a785e7 --- /dev/null +++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/EventRegistrationTestFragmentViewModel.kt @@ -0,0 +1,15 @@ +package de.rki.coronawarnapp.test.eventregistration.ui + +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import de.rki.coronawarnapp.util.coroutine.DispatcherProvider +import de.rki.coronawarnapp.util.viewmodel.CWAViewModel +import de.rki.coronawarnapp.util.viewmodel.SimpleCWAViewModelFactory + +class EventRegistrationTestFragmentViewModel @AssistedInject constructor( + dispatcherProvider: DispatcherProvider +) : CWAViewModel(dispatcherProvider = dispatcherProvider) { + + @AssistedFactory + interface Factory : SimpleCWAViewModelFactory<EventRegistrationTestFragmentViewModel> +} diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/PrintingAdapter.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/PrintingAdapter.kt new file mode 100644 index 0000000000000000000000000000000000000000..2d20c87a1d4b6f1d63abc4e59f62c813740ff26a --- /dev/null +++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/PrintingAdapter.kt @@ -0,0 +1,75 @@ +package de.rki.coronawarnapp.test.eventregistration.ui + +import android.os.Bundle +import android.os.CancellationSignal +import android.os.ParcelFileDescriptor +import android.print.PageRange +import android.print.PrintAttributes +import android.print.PrintDocumentAdapter +import android.print.PrintDocumentInfo +import timber.log.Timber +import java.io.File +import java.io.FileInputStream +import java.io.FileOutputStream + +/** + * Printing adapter for poster PDF files + */ +class PrintingAdapter( + private val file: File +) : PrintDocumentAdapter() { + + override fun onLayout( + oldAttributes: PrintAttributes?, + newAttributes: PrintAttributes?, + cancellationSignal: CancellationSignal, + callback: LayoutResultCallback, + extras: Bundle? + ) { + if (cancellationSignal.isCanceled) { + callback.onLayoutCancelled() + Timber.i("onLayoutCancelled") + return + } + + val info = PrintDocumentInfo.Builder(file.name) + .setContentType(PrintDocumentInfo.CONTENT_TYPE_DOCUMENT) + .setPageCount(PrintDocumentInfo.PAGE_COUNT_UNKNOWN) + .build() + Timber.i( + "onLayoutFinished(info:%s, oldAttributes:%s, newAttributes:%s)", + info, + oldAttributes, + newAttributes + ) + callback.onLayoutFinished(info, oldAttributes != newAttributes) + } + + override fun onWrite( + pages: Array<out PageRange>, + destination: ParcelFileDescriptor, + cancellationSignal: CancellationSignal, + callback: WriteResultCallback + ) = try { + FileInputStream(file).use { input -> + FileOutputStream(destination.fileDescriptor).use { output -> + val bytesCopied = input.copyTo(output) + Timber.i("bytesCopied:$bytesCopied") + } + } + + when { + cancellationSignal.isCanceled -> { + Timber.i("onWriteCancelled") + callback.onWriteCancelled() + } + else -> { + Timber.i("onWriteFinished") + callback.onWriteFinished(arrayOf(PageRange.ALL_PAGES)) + } + } + } catch (e: Exception) { + callback.onWriteFailed(e.message) + Timber.e(e, "Printing $file failed") + } +} diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/createevent/CreateEventTestFragment.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/createevent/CreateEventTestFragment.kt new file mode 100644 index 0000000000000000000000000000000000000000..3e6daef237373951dc50bf59d214b44f3b72232b --- /dev/null +++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/createevent/CreateEventTestFragment.kt @@ -0,0 +1,88 @@ +package de.rki.coronawarnapp.test.eventregistration.ui.createevent + +import android.os.Bundle +import android.view.View +import android.widget.ArrayAdapter +import android.widget.AutoCompleteTextView +import androidx.core.widget.doAfterTextChanged +import androidx.core.widget.doOnTextChanged +import androidx.fragment.app.Fragment +import de.rki.coronawarnapp.R +import de.rki.coronawarnapp.contactdiary.util.hideKeyboard +import de.rki.coronawarnapp.databinding.FragmentTestCreateeventBinding +import de.rki.coronawarnapp.util.di.AutoInject +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 timber.log.Timber +import javax.inject.Inject + +class CreateEventTestFragment : Fragment(R.layout.fragment_test_createevent), AutoInject { + + @Inject lateinit var viewModelFactory: CWAViewModelFactoryProvider.Factory + private val vm: CreateEventTestViewModel by cwaViewModels { viewModelFactory } + + private val binding: FragmentTestCreateeventBinding by viewBindingLazy() + + private val eventString = "Event" + private val locationString = "Location" + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + initSpinner() + initOnCreateEventClicked() + observeViewModelResult() + } + + private fun observeViewModelResult() { + vm.result.observe2(this) { + when (it) { + is CreateEventTestViewModel.Result.Success -> + binding.resultText.text = "Successfully stored: ${it.eventEntity}" + is CreateEventTestViewModel.Result.Error -> + binding.resultText.text = "There is something wrong with your input values, please check again." + } + } + } + + private fun initOnCreateEventClicked() = with(binding) { + createEventButton.setOnClickListener { + vm.createEvent( + eventOrLocationSpinner.editText!!.text.toString(), + eventDescription.text.toString(), + eventAddress.text.toString(), + eventStartEditText.text.toString(), + eventEndEditText.text.toString(), + eventDefaultCheckinLengthInMinutes.text.toString() + ) + it.hideKeyboard() + } + } + + private fun initSpinner() { + val items = listOf(eventString, locationString) + with(binding.eventOrLocationSpinner.editText as AutoCompleteTextView) { + setText(items.first(), false) + setAdapter(ArrayAdapter(requireContext(), android.R.layout.simple_dropdown_item_1line, items)) + doAfterTextChanged { } + doOnTextChanged { text, start, before, count -> + Timber.d("text: $text, start: $start, before: $before, count: $count") + + when (text.toString()) { + eventString -> { + binding.eventStart.visibility = View.VISIBLE + binding.eventEnd.visibility = View.VISIBLE + } + locationString -> { + binding.eventStart.visibility = View.GONE + binding.eventEnd.visibility = View.GONE + binding.eventStartEditText.text = null + binding.eventEndEditText.text = null + } + } + } + } + } +} diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/createevent/CreateEventTestFragmentModule.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/createevent/CreateEventTestFragmentModule.kt new file mode 100644 index 0000000000000000000000000000000000000000..258a96c7ad463471d8cac73695fcb3e546f3ae5a --- /dev/null +++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/createevent/CreateEventTestFragmentModule.kt @@ -0,0 +1,19 @@ +package de.rki.coronawarnapp.test.eventregistration.ui.createevent + +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 CreateEventTestFragmentModule { + + @Binds + @IntoMap + @CWAViewModelKey(CreateEventTestViewModel::class) + abstract fun testCreateEventFragment( + factory: CreateEventTestViewModel.Factory + ): CWAViewModelFactory<out CWAViewModel> +} diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/createevent/CreateEventTestViewModel.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/createevent/CreateEventTestViewModel.kt new file mode 100644 index 0000000000000000000000000000000000000000..3668a469297caae2efb4ecde3da605eb74d23d8e --- /dev/null +++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/createevent/CreateEventTestViewModel.kt @@ -0,0 +1,75 @@ +package de.rki.coronawarnapp.test.eventregistration.ui.createevent + +import androidx.lifecycle.MutableLiveData +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import de.rki.coronawarnapp.eventregistration.events.DefaultTraceLocation +import de.rki.coronawarnapp.eventregistration.events.TraceLocation +import de.rki.coronawarnapp.eventregistration.storage.repo.TraceLocationRepository +import de.rki.coronawarnapp.util.TimeAndDateExtensions.seconds +import de.rki.coronawarnapp.util.coroutine.DispatcherProvider +import de.rki.coronawarnapp.util.viewmodel.CWAViewModel +import de.rki.coronawarnapp.util.viewmodel.SimpleCWAViewModelFactory +import org.joda.time.DateTime +import org.joda.time.format.DateTimeFormat +import timber.log.Timber +import java.util.UUID + +class CreateEventTestViewModel @AssistedInject constructor( + dispatcherProvider: DispatcherProvider, + private val traceLocationRepository: TraceLocationRepository +) : CWAViewModel(dispatcherProvider = dispatcherProvider) { + + @AssistedFactory + interface Factory : SimpleCWAViewModelFactory<CreateEventTestViewModel> + + val result = MutableLiveData<Result>() + + fun createEvent( + type: String, + description: String, + address: String, + start: String, + end: String, + defaultCheckInLengthInMinutes: String + ) { + try { + val startDate = + if (start.isBlank()) null else DateTime.parse(start, DateTimeFormat.forPattern("yyyy-MM-dd HH:mm")) + val endDate = + if (end.isBlank()) null else DateTime.parse(end, DateTimeFormat.forPattern("yyyy-MM-dd HH:mm")) + + /* TODO: wait for new protobuf messages 'TraceLocation' and perform network request to get + 'SignedTraceLocation' */ + + // Backend needs UNIX timestamp in Seconds, not milliseconds + val startTimeStampSeconds = startDate?.toInstant()?.seconds ?: 0 + val endTimeStampSeconds = endDate?.toInstant()?.seconds ?: 0 + + val traceLocationType = + if (type == "Event") TraceLocation.Type.TEMPORARY_OTHER else TraceLocation.Type.PERMANENT_OTHER + + val traceLocation = DefaultTraceLocation( + UUID.randomUUID().toString(), // will be provided by the server when the endpoint is ready + traceLocationType, + description, + address, + startDate?.toInstant(), + endDate?.toInstant(), + defaultCheckInLengthInMinutes.toInt(), + "ServerSignature" + ) + + traceLocationRepository.addTraceLocation(traceLocation) + result.postValue(Result.Success(traceLocation)) + } catch (exception: Exception) { + Timber.d("Something went wrong when trying to create an event: $exception") + result.postValue(Result.Error) + } + } + + sealed class Result { + object Error : Result() + data class Success(val eventEntity: TraceLocation) : Result() + } +} diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/qrcode/QrCodeCreationTestFragment.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/qrcode/QrCodeCreationTestFragment.kt new file mode 100644 index 0000000000000000000000000000000000000000..abd5b1c1be9461567ee0dbf1fccc308797c2bb08 --- /dev/null +++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/qrcode/QrCodeCreationTestFragment.kt @@ -0,0 +1,84 @@ +package de.rki.coronawarnapp.test.eventregistration.ui.qrcode + +import android.annotation.SuppressLint +import android.os.Bundle +import android.print.PrintAttributes +import android.print.PrintManager +import android.view.View +import android.widget.Toast +import androidx.core.content.getSystemService +import androidx.core.view.isVisible +import androidx.fragment.app.Fragment +import de.rki.coronawarnapp.R +import de.rki.coronawarnapp.databinding.FragmentTestQrcodeCreationBinding +import de.rki.coronawarnapp.test.eventregistration.ui.PrintingAdapter +import de.rki.coronawarnapp.util.di.AutoInject +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 timber.log.Timber +import javax.inject.Inject + +class QrCodeCreationTestFragment : Fragment(R.layout.fragment_test_qrcode_creation), AutoInject { + + @Inject lateinit var viewModelFactory: CWAViewModelFactoryProvider.Factory + + 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) { fileIntent -> + + binding.printPDF.isVisible = true + binding.printPDF.setOnClickListener { + // Context must be an Activity context + val printingManger = context?.getSystemService<PrintManager>() + Timber.i("PrintingManager: $printingManger") + printingManger?.apply { + val printingJob = print( + "CoronaWarnApp", + PrintingAdapter(fileIntent.file), + PrintAttributes + .Builder() + .setMediaSize(PrintAttributes.MediaSize.ISO_A3) + .build() + ) + + Timber.i("PrintingJob:$printingJob") + Timber.i("PrintingJob isBlocked:${printingJob.isBlocked}") + Timber.i("PrintingJob isCancelled:${printingJob.isCancelled}") + Timber.i("PrintingJob isCompleted:${printingJob.isCompleted}") + Timber.i("PrintingJob isFailed:${printingJob.isFailed}") + Timber.i("PrintingJob info:${printingJob.info}") + } + } + binding.sharePDF.isVisible = true + binding.sharePDF.setOnClickListener { + startActivity(fileIntent.intent(requireActivity())) + } + } + + viewModel.qrCodeBitmap.observe2(this) { + binding.qrCodeImage.setImageBitmap(it) + if (it != null) { + viewModel.createPDF(binding.pdfPage) + } + } + + viewModel.errorMessage.observe2(this) { + 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()) + } + } +} diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/qrcode/QrCodeCreationTestFragmentModule.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/qrcode/QrCodeCreationTestFragmentModule.kt new file mode 100644 index 0000000000000000000000000000000000000000..33fa4c48d9b99cc6df1ef0c3801310a0dccda239 --- /dev/null +++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/qrcode/QrCodeCreationTestFragmentModule.kt @@ -0,0 +1,18 @@ +package de.rki.coronawarnapp.test.eventregistration.ui.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 QrCodeCreationTestFragmentModule { + @Binds + @IntoMap + @CWAViewModelKey(QrCodeCreationTestViewModel::class) + abstract fun qrCodeCreation( + factory: QrCodeCreationTestViewModel.Factory + ): CWAViewModelFactory<out CWAViewModel> +} diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/qrcode/QrCodeCreationTestViewModel.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/qrcode/QrCodeCreationTestViewModel.kt new file mode 100644 index 0000000000000000000000000000000000000000..6dd59f2e683ef8ba88781a3711a673e369c31b65 --- /dev/null +++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/qrcode/QrCodeCreationTestViewModel.kt @@ -0,0 +1,124 @@ +package de.rki.coronawarnapp.test.eventregistration.ui.qrcode + +import android.content.Context +import android.graphics.Bitmap +import android.graphics.Color.BLACK +import android.graphics.Color.WHITE +import android.graphics.pdf.PdfDocument +import android.view.View +import com.google.zxing.BarcodeFormat +import com.google.zxing.EncodeHintType +import com.google.zxing.MultiFormatWriter +import com.google.zxing.common.BitMatrix +import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import de.rki.coronawarnapp.util.coroutine.DispatcherProvider +import de.rki.coronawarnapp.util.di.AppContext +import de.rki.coronawarnapp.util.files.FileSharing +import de.rki.coronawarnapp.util.ui.SingleLiveEvent +import de.rki.coronawarnapp.util.viewmodel.CWAViewModel +import de.rki.coronawarnapp.util.viewmodel.SimpleCWAViewModelFactory +import timber.log.Timber +import java.io.File +import java.io.FileOutputStream + +class QrCodeCreationTestViewModel @AssistedInject constructor( + private val dispatcher: DispatcherProvider, + private val fileSharing: FileSharing, + @AppContext private val context: Context +) : CWAViewModel(dispatcher) { + + val qrCodeBitmap = SingleLiveEvent<Bitmap>() + val errorMessage = SingleLiveEvent<String>() + val sharingIntent = SingleLiveEvent<FileSharing.FileIntentProvider>() + + /** + * Creates a QR Code [Bitmap] ,result is delivered by [qrCodeBitmap] + */ + fun createQrCode(input: String) = launch(context = dispatcher.IO) { + qrCodeBitmap.postValue(encodeAsBitmap(input)) + } + + /** + * Create a new PDF file and result is delivered by [sharingIntent] + * as a sharing [FileSharing.ShareIntentProvider] + */ + fun createPDF( + view: View + ) = launch(context = dispatcher.IO) { + try { + val file = pdfFile() + val pageInfo = PdfDocument.PageInfo.Builder( + view.width, + view.height, + 1 + ).create() + + PdfDocument().apply { + startPage(pageInfo).apply { + view.draw(canvas) + finishPage(this) + } + + FileOutputStream(file).use { + writeTo(it) + close() + } + } + + sharingIntent.postValue( + fileSharing.getFileIntentProvider(file, "Scan and Help") + ) + } catch (e: Exception) { + errorMessage.postValue(e.localizedMessage ?: "Creating pdf failed") + Timber.d(e, "Creating pdf failed") + } + } + + private fun pdfFile(): File { + val dir = File(context.filesDir, "events") + if (!dir.exists()) dir.mkdirs() + return File(dir, "CoronaWarnApp-Event.pdf") + } + + private fun encodeAsBitmap(input: String, size: Int = 1000): Bitmap? { + return try { + val hints = mapOf( + EncodeHintType.ERROR_CORRECTION to ErrorCorrectionLevel.H + // This is not required in the specs and it should not be enabled + // it is causing crash on older Android versions ex:API 23 + // EncodeHintType.CHARACTER_SET to Charsets.UTF_8 + ) + MultiFormatWriter().encode( + input, + BarcodeFormat.QR_CODE, + size, + size, + hints + ).toBitmap() + } catch (e: Exception) { + Timber.d(e, "Qr code creation failed") + errorMessage.postValue(e.localizedMessage ?: "QR code creation failed") + null + } + } + + private fun BitMatrix.toBitmap() = + Bitmap.createBitmap( + context.resources.displayMetrics, + width, + height, + Bitmap.Config.ARGB_8888 + ).apply { + for (x in 0 until width) { + for (y in 0 until height) { + val color = if (get(x, y)) BLACK else WHITE + setPixel(x, y, color) + } + } + } + + @AssistedFactory + interface Factory : SimpleCWAViewModelFactory<QrCodeCreationTestViewModel> +} diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/showevents/ShowStoredEventsTestFragment.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/showevents/ShowStoredEventsTestFragment.kt new file mode 100644 index 0000000000000000000000000000000000000000..62bd7b04808a57d947b01fc3693ff8b70acdbbf4 --- /dev/null +++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/showevents/ShowStoredEventsTestFragment.kt @@ -0,0 +1,48 @@ +package de.rki.coronawarnapp.test.eventregistration.ui.showevents + +import android.os.Bundle +import android.view.View +import androidx.fragment.app.Fragment +import de.rki.coronawarnapp.R +import de.rki.coronawarnapp.databinding.FragmentTestShowstoredeventsBinding +import de.rki.coronawarnapp.eventregistration.events.TraceLocation +import de.rki.coronawarnapp.util.di.AutoInject +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 ShowStoredEventsTestFragment : Fragment(R.layout.fragment_test_showstoredevents), AutoInject { + + @Inject lateinit var viewModelFactory: CWAViewModelFactoryProvider.Factory + private val vm: ShowStoredEventsTestViewModel by cwaViewModels { viewModelFactory } + + private val binding: FragmentTestShowstoredeventsBinding by viewBindingLazy() + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + vm.storedEvents.observe2(this) { events -> + binding.storedEvents.text = events.joinToString(separator = "\n\n") { it.getSimpleUIString() } + } + + binding.deleteAllEvents.setOnClickListener { + vm.deleteAllEvents() + } + } + + private fun TraceLocation.getSimpleUIString(): String { + return listOf( + "guid = $guid", + "type = $type", + "description = $description", + "location = $address", + "startTime = $startDate", + "endTime = $endDate", + "defaultCheckInLengthInMinutes = $defaultCheckInLengthInMinutes", + "signature = $signature", + "version = $version" + ).joinToString(separator = "\n") + } +} diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/showevents/ShowStoredEventsTestFragmentModule.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/showevents/ShowStoredEventsTestFragmentModule.kt new file mode 100644 index 0000000000000000000000000000000000000000..45e3c4763e437a8cc30a17c144e77d7e1af7174c --- /dev/null +++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/showevents/ShowStoredEventsTestFragmentModule.kt @@ -0,0 +1,19 @@ +package de.rki.coronawarnapp.test.eventregistration.ui.showevents + +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 ShowStoredEventsTestFragmentModule { + + @Binds + @IntoMap + @CWAViewModelKey(ShowStoredEventsTestViewModel::class) + abstract fun testStoredEventsFragment( + factory: ShowStoredEventsTestViewModel.Factory + ): CWAViewModelFactory<out CWAViewModel> +} diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/showevents/ShowStoredEventsTestViewModel.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/showevents/ShowStoredEventsTestViewModel.kt new file mode 100644 index 0000000000000000000000000000000000000000..f85513da0a4ec166c8df5c5e13973a0b7a2acfc5 --- /dev/null +++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/showevents/ShowStoredEventsTestViewModel.kt @@ -0,0 +1,24 @@ +package de.rki.coronawarnapp.test.eventregistration.ui.showevents + +import androidx.lifecycle.asLiveData +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import de.rki.coronawarnapp.eventregistration.storage.repo.TraceLocationRepository +import de.rki.coronawarnapp.util.coroutine.DispatcherProvider +import de.rki.coronawarnapp.util.viewmodel.CWAViewModel +import de.rki.coronawarnapp.util.viewmodel.SimpleCWAViewModelFactory + +class ShowStoredEventsTestViewModel @AssistedInject constructor( + dispatcherProvider: DispatcherProvider, + private val traceLocationRepository: TraceLocationRepository +) : CWAViewModel(dispatcherProvider = dispatcherProvider) { + + @AssistedFactory + interface Factory : SimpleCWAViewModelFactory<ShowStoredEventsTestViewModel> + + val storedEvents = traceLocationRepository.allTraceLocations.asLiveData() + + fun deleteAllEvents() { + traceLocationRepository.deleteAllTraceLocations() + } +} diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/menu/ui/TestMenuFragmentViewModel.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/menu/ui/TestMenuFragmentViewModel.kt index 6d09d4e6bc5d38de5e6556b177aa116a3a37b415..e631969b4290fe76b22c48870f35bb55b96d1b9d 100644 --- a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/menu/ui/TestMenuFragmentViewModel.kt +++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/menu/ui/TestMenuFragmentViewModel.kt @@ -10,6 +10,7 @@ import de.rki.coronawarnapp.test.crash.ui.SettingsCrashReportFragment import de.rki.coronawarnapp.test.datadonation.ui.DataDonationTestFragment import de.rki.coronawarnapp.test.debugoptions.ui.DebugOptionsFragment import de.rki.coronawarnapp.test.deltaonboarding.ui.DeltaonboardingFragment +import de.rki.coronawarnapp.test.eventregistration.ui.EventRegistrationTestFragment import de.rki.coronawarnapp.test.keydownload.ui.KeyDownloadTestFragment import de.rki.coronawarnapp.test.playground.ui.PlaygroundFragment import de.rki.coronawarnapp.test.risklevel.ui.TestRiskLevelCalculationFragment @@ -34,7 +35,8 @@ class TestMenuFragmentViewModel @AssistedInject constructor() : CWAViewModel() { ContactDiaryTestFragment.MENU_ITEM, PlaygroundFragment.MENU_ITEM, DataDonationTestFragment.MENU_ITEM, - DeltaonboardingFragment.MENU_ITEM + DeltaonboardingFragment.MENU_ITEM, + EventRegistrationTestFragment.MENU_ITEM, ).let { MutableLiveData(it) } } val showTestScreenEvent = SingleLiveEvent<TestMenuItem>() diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/ui/main/MainActivityTestModule.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/ui/main/MainActivityTestModule.kt index da9c43ddb5f631cfcd805aea75a1852960dd46ff..e0ecef22020975d24885f70fd2549a8b96656083 100644 --- a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/ui/main/MainActivityTestModule.kt +++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/ui/main/MainActivityTestModule.kt @@ -14,6 +14,14 @@ import de.rki.coronawarnapp.test.debugoptions.ui.DebugOptionsFragment import de.rki.coronawarnapp.test.debugoptions.ui.DebugOptionsFragmentModule import de.rki.coronawarnapp.test.deltaonboarding.ui.DeltaOnboardingFragmentModule import de.rki.coronawarnapp.test.deltaonboarding.ui.DeltaonboardingFragment +import de.rki.coronawarnapp.test.eventregistration.ui.EventRegistrationTestFragment +import de.rki.coronawarnapp.test.eventregistration.ui.EventRegistrationTestFragmentModule +import de.rki.coronawarnapp.test.eventregistration.ui.createevent.CreateEventTestFragment +import de.rki.coronawarnapp.test.eventregistration.ui.createevent.CreateEventTestFragmentModule +import de.rki.coronawarnapp.test.eventregistration.ui.qrcode.QrCodeCreationTestFragment +import de.rki.coronawarnapp.test.eventregistration.ui.qrcode.QrCodeCreationTestFragmentModule +import de.rki.coronawarnapp.test.eventregistration.ui.showevents.ShowStoredEventsTestFragment +import de.rki.coronawarnapp.test.eventregistration.ui.showevents.ShowStoredEventsTestFragmentModule import de.rki.coronawarnapp.test.keydownload.ui.KeyDownloadTestFragment import de.rki.coronawarnapp.test.keydownload.ui.KeyDownloadTestFragmentModule import de.rki.coronawarnapp.test.menu.ui.TestMenuFragment @@ -65,4 +73,16 @@ abstract class MainActivityTestModule { @ContributesAndroidInjector(modules = [DeltaOnboardingFragmentModule::class]) abstract fun deltaOnboarding(): DeltaonboardingFragment + + @ContributesAndroidInjector(modules = [EventRegistrationTestFragmentModule::class]) + abstract fun eventRegistration(): EventRegistrationTestFragment + + @ContributesAndroidInjector(modules = [QrCodeCreationTestFragmentModule::class]) + abstract fun qrCodeCreation(): QrCodeCreationTestFragment + + @ContributesAndroidInjector(modules = [CreateEventTestFragmentModule::class]) + abstract fun createEvent(): CreateEventTestFragment + + @ContributesAndroidInjector(modules = [ShowStoredEventsTestFragmentModule::class]) + abstract fun showStoredEvents(): ShowStoredEventsTestFragment } diff --git a/Corona-Warn-App/src/deviceForTesters/res/drawable/ic_bug.xml b/Corona-Warn-App/src/deviceForTesters/res/drawable/ic_bug.xml index ed88658407baec3e8107d6281f279e9d0af2f86c..a9c970bdc30479e734b9bdaf08b5c9cf631e74ca 100644 --- a/Corona-Warn-App/src/deviceForTesters/res/drawable/ic_bug.xml +++ b/Corona-Warn-App/src/deviceForTesters/res/drawable/ic_bug.xml @@ -1,7 +1,8 @@ <?xml version="1.0" encoding="utf-8"?> <vector xmlns:android="http://schemas.android.com/apk/res/android" - android:height="24dp" android:width="24dp" + android:height="24dp" + android:tint="?attr/colorControlNormal" android:viewportWidth="24" android:viewportHeight="24"> <path diff --git a/Corona-Warn-App/src/deviceForTesters/res/drawable/ic_coffee.xml b/Corona-Warn-App/src/deviceForTesters/res/drawable/ic_coffee.xml index 95c65dbf1b3c2232925dc54ff1f98bc6e1c49567..d7fde78a86ca740f295e828894aa3c5f16ea3e7d 100644 --- a/Corona-Warn-App/src/deviceForTesters/res/drawable/ic_coffee.xml +++ b/Corona-Warn-App/src/deviceForTesters/res/drawable/ic_coffee.xml @@ -1,7 +1,8 @@ <?xml version="1.0" encoding="utf-8"?> <vector xmlns:android="http://schemas.android.com/apk/res/android" - android:height="24dp" android:width="24dp" + android:height="24dp" + android:tint="?attr/colorControlNormal" android:viewportWidth="24" android:viewportHeight="24"> <path diff --git a/Corona-Warn-App/src/deviceForTesters/res/drawable/qr_code_print_template.png b/Corona-Warn-App/src/deviceForTesters/res/drawable/qr_code_print_template.png new file mode 100644 index 0000000000000000000000000000000000000000..70ce653f099e59942922b03275beac482038fe04 Binary files /dev/null and b/Corona-Warn-App/src/deviceForTesters/res/drawable/qr_code_print_template.png differ diff --git a/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_createevent.xml b/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_createevent.xml new file mode 100644 index 0000000000000000000000000000000000000000..afd82d5c5bb5f5f2b149d587eae2549a9a3204ac --- /dev/null +++ b/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_createevent.xml @@ -0,0 +1,154 @@ +<?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:padding="@dimen/spacing_normal"> + + <ScrollView + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical"> + + <TextView + style="@style/headline5" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginBottom="@dimen/spacing_normal" + android:text="Create new event" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + tools:ignore="HardcodedText" /> + + <com.google.android.material.textfield.TextInputLayout + android:id="@+id/eventOrLocationSpinner" + style="@style/Widget.MaterialComponents.TextInputLayout.FilledBox.ExposedDropdownMenu" + android:layout_marginBottom="@dimen/spacing_tiny" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:hint="Type"> + + <AutoCompleteTextView + android:enabled="false" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:inputType="none" /> + + </com.google.android.material.textfield.TextInputLayout> + + <com.google.android.material.textfield.TextInputLayout + style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginBottom="@dimen/spacing_tiny"> + + <com.google.android.material.textfield.TextInputEditText + android:id="@+id/event_description" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:hint="Description" + android:maxLines="1" + android:padding="@dimen/spacing_tiny" + tools:ignore="HardcodedText" /> + + </com.google.android.material.textfield.TextInputLayout> + + <com.google.android.material.textfield.TextInputLayout + style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginBottom="@dimen/spacing_tiny"> + + <com.google.android.material.textfield.TextInputEditText + android:id="@+id/event_address" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:hint="Address" + android:maxLines="1" + android:padding="@dimen/spacing_tiny" + tools:ignore="HardcodedText" /> + + </com.google.android.material.textfield.TextInputLayout> + + <com.google.android.material.textfield.TextInputLayout + android:id="@+id/event_start" + style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginBottom="@dimen/spacing_tiny"> + + <com.google.android.material.textfield.TextInputEditText + android:id="@+id/event_start_edit_text" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:hint="Start (yyyy-MM-dd HH:mm)" + android:maxLines="1" + android:padding="@dimen/spacing_tiny" + tools:ignore="HardcodedText" /> + + </com.google.android.material.textfield.TextInputLayout> + + <com.google.android.material.textfield.TextInputLayout + android:id="@+id/event_end" + style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginBottom="@dimen/spacing_tiny"> + + <com.google.android.material.textfield.TextInputEditText + android:id="@+id/event_end_edit_text" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:hint="End (yyyy-MM-dd HH:mm)" + android:maxLines="1" + android:padding="@dimen/spacing_tiny" + tools:ignore="HardcodedText" /> + + </com.google.android.material.textfield.TextInputLayout> + + <com.google.android.material.textfield.TextInputLayout + style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox" + android:layout_width="match_parent" + android:layout_height="wrap_content" + + android:layout_marginBottom="@dimen/spacing_normal"> + + <com.google.android.material.textfield.TextInputEditText + android:id="@+id/event_default_checkin_length_in_minutes" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:hint="Default Check-In Lenght in Minutes - multiple of 10" + android:maxLines="1" + android:padding="@dimen/spacing_tiny" + android:inputType="numberDecimal" + tools:ignore="HardcodedText" /> + + </com.google.android.material.textfield.TextInputLayout> + + <com.google.android.material.button.MaterialButton + android:id="@+id/create_event_button" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:text="Create Event" + tools:ignore="HardcodedText" /> + + <TextView + android:id="@+id/resultText" + style="@style/subtitle" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginBottom="@dimen/spacing_normal" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + tools:ignore="HardcodedText" /> + + </LinearLayout> + + </ScrollView> + +</androidx.constraintlayout.widget.ConstraintLayout> \ No newline at end of file diff --git a/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_eventregistration.xml b/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_eventregistration.xml new file mode 100644 index 0000000000000000000000000000000000000000..390245bc42225eea7e9109b18285697cd8bcb503 --- /dev/null +++ b/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_eventregistration.xml @@ -0,0 +1,114 @@ +<?xml version="1.0" encoding="utf-8"?> + +<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="match_parent" + xmlns:app="http://schemas.android.com/apk/res-auto" + tools:ignore="HardcodedText"> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_margin="8dp" + android:orientation="vertical" + android:paddingBottom="32dp"> + + <LinearLayout + style="@style/Card" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_margin="@dimen/spacing_tiny" + android:layout_marginStart="8dp" + android:layout_marginEnd="8dp" + android:orientation="vertical"> + + <TextView + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:text="QRCode, PDF event registration" /> + + <com.google.android.material.button.MaterialButton + android:id="@+id/testQrCodeCreation" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="10dp" + android:text="QR Code Creation" /> + + </LinearLayout> + + <LinearLayout + style="@style/Card" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_margin="@dimen/spacing_tiny" + android:layout_marginStart="8dp" + android:layout_marginEnd="8dp" + android:orientation="vertical"> + <TextView + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:text="QRCode scanning" /> + + <com.google.android.material.button.MaterialButton + android:id="@+id/scanCheckInQrCode" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:text="Scan check in QR code" /> + + </LinearLayout> + + <androidx.constraintlayout.widget.ConstraintLayout + android:id="@+id/event_container" + style="@style/Card" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_margin="@dimen/spacing_tiny"> + + <TextView + android:id="@+id/event_title" + style="@style/headline6" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginEnd="8dp" + android:text="Events" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + + <TextView + android:id="@+id/events_body" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/spacing_tiny" + android:textIsSelectable="true" + android:text="After creating an event in the app, it is sent to the server and returned together with a guid and a signature." + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/event_title" /> + + <com.google.android.material.button.MaterialButton + android:id="@+id/create_event_button" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="8dp" + android:layout_marginEnd="8dp" + android:text="Create Event" + app:layout_constraintEnd_toStartOf="@+id/show_events_button" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/events_body" /> + + <com.google.android.material.button.MaterialButton + android:id="@+id/show_events_button" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="8dp" + android:text="Show stored Events" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toEndOf="@id/create_event_button" + app:layout_constraintTop_toBottomOf="@id/events_body" /> + </androidx.constraintlayout.widget.ConstraintLayout> + + + </LinearLayout> +</androidx.core.widget.NestedScrollView> diff --git a/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_qrcode_creation.xml b/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_qrcode_creation.xml new file mode 100644 index 0000000000000000000000000000000000000000..6ae5f29730ea651f8a8e8bc0d61f010427562518 --- /dev/null +++ b/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_qrcode_creation.xml @@ -0,0 +1,95 @@ +<?xml version="1.0" encoding="utf-8"?> +<ScrollView 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:fillViewport="true" + tools:ignore="HardcodedText"> + + <androidx.constraintlayout.widget.ConstraintLayout + android:layout_width="match_parent" + android:layout_height="wrap_content"> + + <com.google.android.material.button.MaterialButton + android:id="@+id/generateQrCode" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="24dp" + android:text="QR Code" + app:layout_constraintEnd_toStartOf="@+id/sharePDF" + app:layout_constraintHorizontal_bias="0.5" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + + <com.google.android.material.button.MaterialButton + android:id="@+id/sharePDF" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="24dp" + android:text="Share PDF" + android:visibility="invisible" + app:layout_constraintEnd_toStartOf="@+id/printPDF" + app:layout_constraintHorizontal_bias="0.5" + app:layout_constraintStart_toEndOf="@+id/generateQrCode" + app:layout_constraintTop_toTopOf="parent" /> + + <com.google.android.material.button.MaterialButton + android:id="@+id/printPDF" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="24dp" + android:text="Print PDF" + android:visibility="invisible" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintHorizontal_bias="0.5" + app:layout_constraintStart_toEndOf="@+id/sharePDF" + app:layout_constraintTop_toTopOf="parent" /> + + <com.google.android.material.textfield.TextInputLayout + android:id="@+id/qrCodeTextLayout" + style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_margin="16dp" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/generateQrCode"> + <com.google.android.material.textfield.TextInputEditText + android:id="@+id/qrCodeText" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:textSize="14sp" /> + </com.google.android.material.textfield.TextInputLayout> + + <androidx.constraintlayout.widget.ConstraintLayout + android:id="@+id/pdfPage" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="8dp" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/qrCodeTextLayout"> + <androidx.appcompat.widget.AppCompatImageView + android:id="@+id/pdfTemplateImageView" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:adjustViewBounds="true" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + app:srcCompat="@drawable/qr_code_print_template" /> + + <androidx.appcompat.widget.AppCompatImageView + android:id="@+id/qrCodeImage" + android:layout_width="250dp" + android:layout_height="250dp" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="@id/pdfTemplateImageView" + app:layout_constraintVertical_bias="0.25" /> + </androidx.constraintlayout.widget.ConstraintLayout> + </androidx.constraintlayout.widget.ConstraintLayout> +</ScrollView> diff --git a/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_showstoredevents.xml b/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_showstoredevents.xml new file mode 100644 index 0000000000000000000000000000000000000000..3521aafdae703228d9a98da5f2297dfdb2ef9a14 --- /dev/null +++ b/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_showstoredevents.xml @@ -0,0 +1,38 @@ +<?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:padding="@dimen/spacing_normal"> + + <ScrollView + android:layout_width="match_parent" + android:layout_height="wrap_content" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent"> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical"> + + <TextView + android:id="@+id/storedEvents" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginBottom="@dimen/spacing_normal" + android:text="Show stored Events" + tools:ignore="HardcodedText" /> + + <com.google.android.material.button.MaterialButton + android:id="@+id/deleteAllEvents" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="Delete All Events" /> + + </LinearLayout> + + </ScrollView> + +</androidx.constraintlayout.widget.ConstraintLayout> \ No newline at end of file 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 d4b7265dbf3cd6f7b563669a33c8f4319319a8b4..5f3d417bb90e61a607e9fb9a7950d147a8b2f30f 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 @@ -46,6 +46,9 @@ <action android:id="@+id/action_test_menu_fragment_to_deltaonboardingFragment" app:destination="@id/test_deltaonboarding_fragment" /> + <action + android:id="@+id/action_test_menu_fragment_to_eventRegistrationTestFragment" + app:destination="@id/eventRegistrationTestFragment" /> </fragment> <fragment @@ -124,5 +127,46 @@ android:name="de.rki.coronawarnapp.test.deltaonboarding.ui.DeltaonboardingFragment" android:label="DeltaonboardingFragment" tools:layout="@layout/fragment_test_deltaonboarding" /> + <fragment + android:id="@+id/eventRegistrationTestFragment" + android:name="de.rki.coronawarnapp.test.eventregistration.ui.EventRegistrationTestFragment" + android:label="EventRegistrationTestFragment" + tools:layout="@layout/fragment_test_eventregistration"> + <action + android:id="@+id/action_eventRegistrationTestFragment_to_test_qr_code_creation_fragment" + app:destination="@id/test_qr_code_creation_fragment" /> + <action + android:id="@+id/action_eventRegistrationTestFragment_to_scanCheckInQrCodeFragment" + app:destination="@id/scanCheckInQrCodeFragmentTest" /> + <action + android:id="@+id/action_eventRegistrationTestFragment_to_CreateEventTestFragment" + app:destination="@id/createEventTestFragment" /> + <action + android:id="@+id/action_eventRegistrationTestFragment_to_ShowStoredEventsTestFragment" + app:destination="@id/showStoredEventsTestFragment" /> + </fragment> + + <fragment + android:id="@+id/test_qr_code_creation_fragment" + android:name="de.rki.coronawarnapp.test.eventregistration.ui.qrcode.QrCodeCreationTestFragment" + android:label="QrCodeCreationTestFragment" + tools:layout="@layout/fragment_test_qrcode_creation" /> + <fragment + android:id="@+id/scanCheckInQrCodeFragmentTest" + android:name="de.rki.coronawarnapp.ui.eventregistration.attendee.scan.ScanCheckInQrCodeFragment" + android:label="ScanCheckInQrCodeFragment" + tools:layout="@layout/fragment_submission_qr_code_scan" /> + + <fragment + android:id="@+id/createEventTestFragment" + android:name="de.rki.coronawarnapp.test.eventregistration.ui.createevent.CreateEventTestFragment" + android:label="CreateEventTestFragment" + tools:layout="@layout/fragment_test_createevent" /> + <fragment + android:id="@+id/showStoredEventsTestFragment" + android:name="de.rki.coronawarnapp.test.eventregistration.ui.showevents.ShowStoredEventsTestFragment" + android:label="ShowStoredEventsTestFragment" + tools:layout="@layout/fragment_test_showstoredevents" /> + </navigation> diff --git a/Corona-Warn-App/src/main/AndroidManifest.xml b/Corona-Warn-App/src/main/AndroidManifest.xml index 8c7a9babcd15f82d582dc03215616e6d6047bcc0..2a68d8aca15fb7ea6f131f893d7c19a70979691d 100644 --- a/Corona-Warn-App/src/main/AndroidManifest.xml +++ b/Corona-Warn-App/src/main/AndroidManifest.xml @@ -54,12 +54,25 @@ <activity android:name=".ui.launcher.LauncherActivity" + android:exported="true" android:screenOrientation="portrait" android:theme="@style/AppTheme.Launcher"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> + + <intent-filter> + <action android:name="android.intent.action.VIEW" /> + + <category android:name="android.intent.category.DEFAULT" /> + <category android:name="android.intent.category.BROWSABLE" /> + + <data + android:host="e.coronawarn.app" + android:pathPrefix="/" + android:scheme="https" /> + </intent-filter> </activity> <activity android:name=".ui.main.MainActivity" diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/EventRegistrationModule.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/EventRegistrationModule.kt new file mode 100644 index 0000000000000000000000000000000000000000..2a082136720b9aaa1cc5007295b39be1345e57c3 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/EventRegistrationModule.kt @@ -0,0 +1,19 @@ +package de.rki.coronawarnapp.eventregistration + +import dagger.Binds +import dagger.Module +import de.rki.coronawarnapp.eventregistration.checkins.qrcode.DefaultQRCodeVerifier +import de.rki.coronawarnapp.eventregistration.checkins.qrcode.QRCodeVerifier +import de.rki.coronawarnapp.eventregistration.storage.repo.DefaultTraceLocationRepository +import de.rki.coronawarnapp.eventregistration.storage.repo.TraceLocationRepository + +@Module +abstract class EventRegistrationModule { + + @Binds + abstract fun qrCodeVerifier(qrCodeVerifier: DefaultQRCodeVerifier): QRCodeVerifier + + @Binds + abstract fun traceLocationRepository(defaultTraceLocationRepo: DefaultTraceLocationRepository): + TraceLocationRepository +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/CheckIn.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/CheckIn.kt new file mode 100644 index 0000000000000000000000000000000000000000..ae36ed6417ba73682c5f2c598daf837978b74027 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/CheckIn.kt @@ -0,0 +1,21 @@ +package de.rki.coronawarnapp.eventregistration.checkins + +import org.joda.time.Instant + +@Suppress("LongParameterList") +class CheckIn( + val id: Long, + val guid: String, + val version: Int, + val type: Int, + val description: String, + val address: String, + val traceLocationStart: Instant?, + val traceLocationEnd: Instant?, + val defaultCheckInLengthInMinutes: Int?, + val signature: String, + val checkInStart: Instant, + val checkInEnd: Instant?, + val targetCheckInEnd: Instant?, + val createJournalEntry: Boolean +) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/CheckInRepository.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/CheckInRepository.kt new file mode 100644 index 0000000000000000000000000000000000000000..46c7905290df6b0c35482962cf2c90e5faa641b4 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/CheckInRepository.kt @@ -0,0 +1,76 @@ +package de.rki.coronawarnapp.eventregistration.checkins + +import de.rki.coronawarnapp.eventregistration.storage.TraceLocationDatabase +import de.rki.coronawarnapp.eventregistration.storage.dao.CheckInDao +import de.rki.coronawarnapp.eventregistration.storage.entity.TraceLocationCheckInEntity +import de.rki.coronawarnapp.util.coroutine.AppScope +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch +import javax.inject.Inject + +class CheckInRepository @Inject constructor( + traceLocationDatabaseFactory: TraceLocationDatabase.Factory, + @AppScope private val appScope: CoroutineScope +) { + + private val traceLocationDatabase: TraceLocationDatabase by lazy { + traceLocationDatabaseFactory.create() + } + + private val checkInDao: CheckInDao by lazy { + traceLocationDatabase.eventCheckInDao() + } + + val allCheckIns: Flow<List<CheckIn>> = + checkInDao + .allEntries() + .map { list -> list.map { it.toCheckIn() } } + + fun addCheckIn(checkIn: CheckIn) { + appScope.launch { + checkInDao.insert(checkIn.toEntity()) + } + } + + fun updateCheckIn(checkIn: CheckIn) { + appScope.launch { + checkInDao.update(checkIn.toEntity()) + } + } +} + +private fun TraceLocationCheckInEntity.toCheckIn() = CheckIn( + id = id, + guid = guid, + version = version, + type = type, + description = description, + address = address, + traceLocationStart = traceLocationStart, + traceLocationEnd = traceLocationEnd, + defaultCheckInLengthInMinutes = defaultCheckInLengthInMinutes, + signature = signature, + checkInStart = checkInStart, + checkInEnd = checkInEnd, + targetCheckInEnd = targetCheckInEnd, + createJournalEntry = createJournalEntry +) + +private fun CheckIn.toEntity() = TraceLocationCheckInEntity( + id = id, + guid = guid, + version = version, + type = type, + description = description, + address = address, + traceLocationStart = traceLocationStart, + traceLocationEnd = traceLocationEnd, + defaultCheckInLengthInMinutes = defaultCheckInLengthInMinutes, + signature = signature, + checkInStart = checkInStart, + checkInEnd = checkInEnd, + targetCheckInEnd = targetCheckInEnd, + createJournalEntry = createJournalEntry +) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/download/CheckInsPackage.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/download/CheckInsPackage.kt new file mode 100644 index 0000000000000000000000000000000000000000..97f513181b57d32ee384dedf307409a778cf8adf --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/download/CheckInsPackage.kt @@ -0,0 +1,11 @@ +package de.rki.coronawarnapp.eventregistration.checkins.download + +import de.rki.coronawarnapp.eventregistration.checkins.CheckIn + +interface CheckInsPackage { + + /** + * Hides the file reading + */ + suspend fun extractCheckIns(): List<CheckIn> +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/download/DownloadedCheckInsRepo.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/download/DownloadedCheckInsRepo.kt new file mode 100644 index 0000000000000000000000000000000000000000..748cee11448f772318c85beecd0fb9e1542fcb33 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/download/DownloadedCheckInsRepo.kt @@ -0,0 +1,12 @@ +package de.rki.coronawarnapp.eventregistration.checkins.download + +import kotlinx.coroutines.flow.Flow + +interface DownloadedCheckInsRepo { + + val allCheckInsPackages: Flow<List<CheckInsPackage>> + + fun addCheckIns(checkins: List<CheckInsPackage>) + + fun removeCheckIns(checkins: List<CheckInsPackage>) +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/DefaultQRCodeVerifier.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/DefaultQRCodeVerifier.kt new file mode 100644 index 0000000000000000000000000000000000000000..db0a9559c20c59b0671a9bddb49cbd4dc1eac113 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/DefaultQRCodeVerifier.kt @@ -0,0 +1,42 @@ +package de.rki.coronawarnapp.eventregistration.checkins.qrcode + +import de.rki.coronawarnapp.eventregistration.common.decodeBase32 +import de.rki.coronawarnapp.server.protocols.internal.pt.TraceLocationOuterClass +import de.rki.coronawarnapp.util.security.SignatureValidation +import timber.log.Timber +import javax.inject.Inject + +class DefaultQRCodeVerifier @Inject constructor( + private val signatureValidation: SignatureValidation +) : QRCodeVerifier { + + override suspend fun verify(encodedTraceLocation: String): QRCodeVerifyResult { + Timber.tag(TAG).v("Verifying: %s", encodedTraceLocation) + + val signedTraceLocation = try { + TraceLocationOuterClass.SignedTraceLocation.parseFrom(encodedTraceLocation.decodeBase32().toByteArray()) + } catch (e: Exception) { + throw InvalidQRCodeDataException(cause = e, message = "QR-code data could not be parsed.") + } + Timber.tag(TAG).d("Parsed to signed location: %s", signedTraceLocation) + + val isValid = try { + signatureValidation.hasValidSignature( + signedTraceLocation.location.toByteArray(), + sequenceOf(signedTraceLocation.signature.toByteArray()) + ) + } catch (e: Exception) { + throw InvalidQRCodeDataException(cause = e, message = "Verification failed.") + } + + if (!isValid) { + throw InvalidQRCodeSignatureException(message = "QR-code did not match signature.") + } + + return QRCodeVerifyResult(signedTraceLocation) + } + + companion object { + private const val TAG = "DefaultQRCodeVerifier" + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/InvalidQRCodeDataException.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/InvalidQRCodeDataException.kt new file mode 100644 index 0000000000000000000000000000000000000000..935e86b319ff3fc5ca3d685371c417fe706bf64a --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/InvalidQRCodeDataException.kt @@ -0,0 +1,6 @@ +package de.rki.coronawarnapp.eventregistration.checkins.qrcode + +class InvalidQRCodeDataException constructor( + message: String? = null, + cause: Throwable? = null +) : QRCodeException(message, cause) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/InvalidQRCodeSignatureException.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/InvalidQRCodeSignatureException.kt new file mode 100644 index 0000000000000000000000000000000000000000..eae77689d0aab837f5527c2682e72f55078c70ab --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/InvalidQRCodeSignatureException.kt @@ -0,0 +1,6 @@ +package de.rki.coronawarnapp.eventregistration.checkins.qrcode + +class InvalidQRCodeSignatureException constructor( + message: String? = null, + cause: Throwable? = null +) : QRCodeException(message, cause) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/QRCodeException.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/QRCodeException.kt new file mode 100644 index 0000000000000000000000000000000000000000..1fb4c881a81819cd76149537923ad3956f8d942a --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/QRCodeException.kt @@ -0,0 +1,6 @@ +package de.rki.coronawarnapp.eventregistration.checkins.qrcode + +open class QRCodeException constructor( + message: String? = null, + cause: Throwable? = null +) : Exception(message, cause) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/QRCodeVerifier.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/QRCodeVerifier.kt new file mode 100644 index 0000000000000000000000000000000000000000..8e7e2452d58b283c699c89cc3864c3e517457636 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/QRCodeVerifier.kt @@ -0,0 +1,6 @@ +package de.rki.coronawarnapp.eventregistration.checkins.qrcode + +interface QRCodeVerifier { + + suspend fun verify(encodedEvent: String): QRCodeVerifyResult +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/QRCodeVerifyResult.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/QRCodeVerifyResult.kt new file mode 100644 index 0000000000000000000000000000000000000000..24b3a2430321fbcf9788b8cc5eea8556d2cfcc59 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/QRCodeVerifyResult.kt @@ -0,0 +1,19 @@ +package de.rki.coronawarnapp.eventregistration.checkins.qrcode + +import de.rki.coronawarnapp.server.protocols.internal.pt.TraceLocationOuterClass +import de.rki.coronawarnapp.util.TimeAndDateExtensions.seconds +import org.joda.time.Instant + +data class QRCodeVerifyResult( + val singedTraceLocation: TraceLocationOuterClass.SignedTraceLocation +) { + fun isBeforeStartTime(now: Instant): Boolean { + val startTimestamp = singedTraceLocation.location.startTimestamp + return startTimestamp != 0L && startTimestamp > now.seconds + } + + fun isAfterEndTime(now: Instant): Boolean { + val endTimestamp = singedTraceLocation.location.endTimestamp + return endTimestamp != 0L && endTimestamp < now.seconds + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/UriValidator.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/UriValidator.kt new file mode 100644 index 0000000000000000000000000000000000000000..48af21b14167f849d326fdcd9e1ccc5060dfacdb --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/UriValidator.kt @@ -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)) + } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/common/Base32.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/common/Base32.kt new file mode 100644 index 0000000000000000000000000000000000000000..21af3649a21a400ec221cc3c0b8a60733457e4d0 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/common/Base32.kt @@ -0,0 +1,30 @@ +package de.rki.coronawarnapp.eventregistration.common + +import com.google.common.io.BaseEncoding +import okio.ByteString +import okio.ByteString.Companion.toByteString + +/** + * Decodes [String] into [ByteString] using Base32 decoder + * @return [ByteString] + */ +fun String.decodeBase32(): ByteString = BaseEncoding.base32().decode(this).toByteString() + +/** + * Encodes [ByteString] into base32 [String] + * @return [String] + */ +fun ByteString.base32(padding: Boolean = true): String = when { + padding -> BaseEncoding.base32().encode(toByteArray()) + else -> BaseEncoding.base32().omitPadding().encode(toByteArray()) +} + +/** + * Returns Base32 encoded string using [Charsets.UTF_8] + * @param padding [Boolean] true by default ,Output will have '=' characters padding + * @return [String] + */ +fun String.base32(padding: Boolean = true): String = when { + padding -> BaseEncoding.base32().encode(toByteArray(Charsets.UTF_8)) + else -> BaseEncoding.base32().omitPadding().encode(toByteArray(Charsets.UTF_8)) +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/events/DefaultTraceLocation.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/events/DefaultTraceLocation.kt new file mode 100644 index 0000000000000000000000000000000000000000..f792ebf9ea52339be007226803ba26598b307ce8 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/events/DefaultTraceLocation.kt @@ -0,0 +1,45 @@ +package de.rki.coronawarnapp.eventregistration.events + +import de.rki.coronawarnapp.eventregistration.storage.entity.TraceLocationEntity +import org.joda.time.Instant + +const val TRACE_LOCATION_VERSION = 1 + +data class DefaultTraceLocation( + override val guid: String, + override val type: TraceLocation.Type, + override val description: String, + override val address: String, + override val startDate: Instant?, + override val endDate: Instant?, + override val defaultCheckInLengthInMinutes: Int?, + override val signature: String, + override val version: Int = TRACE_LOCATION_VERSION, +) : TraceLocation + +fun List<TraceLocationEntity>.toTraceLocations() = this.map { it.toTraceLocation() } + +fun TraceLocationEntity.toTraceLocation() = DefaultTraceLocation( + guid = guid, + type = type, + description = description, + address = address, + startDate = startDate, + endDate = endDate, + defaultCheckInLengthInMinutes = defaultCheckInLengthInMinutes, + signature = signature, + version = version +) + +/*fun SignedEventOuterClass.SignedEvent.toHostedEvent(): TraceLocation = + DefaultTraceLocation( + guid = event.guid.toString(), + type = enumValues<TraceLocation.Type>()[type], + description = event.description, + address = "hardcodedLocation", // event.location, + // backend needs UNIX timestamp in seconds, so we have to multiply it by 1000 to get milliseconds + startDate = Instant.ofEpochMilli(event.start.toLong() * 1000), + endDate = Instant.ofEpochMilli(event.end.toLong() * 1000), + defaultCheckInLengthInMinutes = event.defaultCheckInLengthInMinutes, + signature = signature.toString() + )*/ diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/events/TraceLocation.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/events/TraceLocation.kt new file mode 100644 index 0000000000000000000000000000000000000000..1316ea7996b67e139cf70ffe404017bf91c1e23c --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/events/TraceLocation.kt @@ -0,0 +1,21 @@ +package de.rki.coronawarnapp.eventregistration.events + +import org.joda.time.Instant + +interface TraceLocation { + val guid: String + val version: Int + val type: Type + val description: String + val address: String + val startDate: Instant? + val endDate: Instant? + val defaultCheckInLengthInMinutes: Int? + val signature: String + + enum class Type(val value: Int) { + UNSPECIFIED(0), + PERMANENT_OTHER(1), + TEMPORARY_OTHER(2) + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/storage/TraceLocationDatabase.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/storage/TraceLocationDatabase.kt new file mode 100644 index 0000000000000000000000000000000000000000..30e508cf4c45d0a9145c9551e64523c457f20a99 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/storage/TraceLocationDatabase.kt @@ -0,0 +1,38 @@ +package de.rki.coronawarnapp.eventregistration.storage + +import android.content.Context +import androidx.room.Database +import androidx.room.Room +import androidx.room.RoomDatabase +import androidx.room.TypeConverters +import de.rki.coronawarnapp.eventregistration.storage.dao.CheckInDao +import de.rki.coronawarnapp.eventregistration.storage.dao.TraceLocationDao +import de.rki.coronawarnapp.eventregistration.storage.entity.TraceLocationCheckInEntity +import de.rki.coronawarnapp.eventregistration.storage.entity.TraceLocationConverters +import de.rki.coronawarnapp.eventregistration.storage.entity.TraceLocationEntity +import de.rki.coronawarnapp.util.database.CommonConverters +import de.rki.coronawarnapp.util.di.AppContext +import javax.inject.Inject + +@Database( + entities = [ + TraceLocationCheckInEntity::class, + TraceLocationEntity::class + ], + version = 1, + exportSchema = true +) +@TypeConverters(CommonConverters::class, TraceLocationConverters::class) +abstract class TraceLocationDatabase : RoomDatabase() { + + abstract fun eventCheckInDao(): CheckInDao + abstract fun traceLocationDao(): TraceLocationDao + + class Factory @Inject constructor(@AppContext private val context: Context) { + fun create() = Room + .databaseBuilder(context, TraceLocationDatabase::class.java, TRACE_LOCATIONS_DATABASE_NAME) + .build() + } +} + +private const val TRACE_LOCATIONS_DATABASE_NAME = "TraceLocations_db" diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/storage/dao/CheckInDao.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/storage/dao/CheckInDao.kt new file mode 100644 index 0000000000000000000000000000000000000000..75f5cbd8987f0d8172c5217cc03154afe0b02984 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/storage/dao/CheckInDao.kt @@ -0,0 +1,24 @@ +package de.rki.coronawarnapp.eventregistration.storage.dao + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.Query +import androidx.room.Update +import de.rki.coronawarnapp.eventregistration.storage.entity.TraceLocationCheckInEntity +import kotlinx.coroutines.flow.Flow + +@Dao +interface CheckInDao { + + @Query("SELECT * FROM checkin") + fun allEntries(): Flow<List<TraceLocationCheckInEntity>> + + @Insert + suspend fun insert(entity: TraceLocationCheckInEntity): Long + + @Update + suspend fun update(entity: TraceLocationCheckInEntity) + + @Query("DELETE FROM checkin") + suspend fun deleteAll() +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/storage/dao/TraceLocationDao.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/storage/dao/TraceLocationDao.kt new file mode 100644 index 0000000000000000000000000000000000000000..888f5e3c4bc76f34eead4cf92d0ef1c10e72498b --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/storage/dao/TraceLocationDao.kt @@ -0,0 +1,25 @@ +package de.rki.coronawarnapp.eventregistration.storage.dao + +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Insert +import androidx.room.Query +import de.rki.coronawarnapp.eventregistration.storage.entity.TraceLocationEntity +import kotlinx.coroutines.flow.Flow + +@Dao +@Suppress("UnnecessaryAbstractClass") +abstract class TraceLocationDao { + + @Query("SELECT * FROM traceLocations") + abstract fun allEntries(): Flow<List<TraceLocationEntity>> + + @Insert + abstract suspend fun insert(traceLocation: TraceLocationEntity) + + @Delete + abstract suspend fun delete(traceLocation: TraceLocationEntity) + + @Query("DELETE FROM traceLocations") + abstract suspend fun deleteAll() +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/storage/entity/TraceLocationCheckInEntity.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/storage/entity/TraceLocationCheckInEntity.kt new file mode 100644 index 0000000000000000000000000000000000000000..354715f9cf6227ef1043339731b60ad421a45227 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/storage/entity/TraceLocationCheckInEntity.kt @@ -0,0 +1,24 @@ +package de.rki.coronawarnapp.eventregistration.storage.entity + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey +import org.joda.time.Instant + +@Entity(tableName = "checkin") +data class TraceLocationCheckInEntity( + @PrimaryKey(autoGenerate = true) @ColumnInfo(name = "id") val id: Long = 0L, + @ColumnInfo(name = "guid") val guid: String, + @ColumnInfo(name = "version") val version: Int, + @ColumnInfo(name = "type") val type: Int, + @ColumnInfo(name = "description") val description: String, + @ColumnInfo(name = "address") val address: String, + @ColumnInfo(name = "traceLocationStart") val traceLocationStart: Instant?, + @ColumnInfo(name = "traceLocationEnd") val traceLocationEnd: Instant?, + @ColumnInfo(name = "defaultCheckInLengthInMinutes") val defaultCheckInLengthInMinutes: Int?, + @ColumnInfo(name = "signature") val signature: String, + @ColumnInfo(name = "checkInStart") val checkInStart: Instant, + @ColumnInfo(name = "checkInEnd") val checkInEnd: Instant?, + @ColumnInfo(name = "targetCheckInEnd") val targetCheckInEnd: Instant?, + @ColumnInfo(name = "createJournalEntry") val createJournalEntry: Boolean +) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/storage/entity/TraceLocationConverters.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/storage/entity/TraceLocationConverters.kt new file mode 100644 index 0000000000000000000000000000000000000000..89546329e0ea2ac472ad58100bb89d733ec818dd --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/storage/entity/TraceLocationConverters.kt @@ -0,0 +1,13 @@ +package de.rki.coronawarnapp.eventregistration.storage.entity + +import androidx.room.TypeConverter +import de.rki.coronawarnapp.eventregistration.events.TraceLocation + +class TraceLocationConverters { + + @TypeConverter + fun toTraceLocationType(value: Int) = enumValues<TraceLocation.Type>().single { it.value == value } + + @TypeConverter + fun fromTraceLocationType(type: TraceLocation.Type) = type.value +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/storage/entity/TraceLocationEntity.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/storage/entity/TraceLocationEntity.kt new file mode 100644 index 0000000000000000000000000000000000000000..c681d3a92acf6c123ba70af40d17a86447ae178b --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/storage/entity/TraceLocationEntity.kt @@ -0,0 +1,35 @@ +package de.rki.coronawarnapp.eventregistration.storage.entity + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey +import de.rki.coronawarnapp.eventregistration.events.TraceLocation +import org.joda.time.Instant + +@Entity(tableName = "traceLocations") +data class TraceLocationEntity( + + @PrimaryKey @ColumnInfo(name = "guid") val guid: String, + @ColumnInfo(name = "version") val version: Int, + @ColumnInfo(name = "type") val type: TraceLocation.Type, + @ColumnInfo(name = "description") val description: String, + @ColumnInfo(name = "address") val address: String, + @ColumnInfo(name = "startDate") val startDate: Instant?, + @ColumnInfo(name = "endDate") val endDate: Instant?, + @ColumnInfo(name = "defaultCheckInLengthInMinutes") val defaultCheckInLengthInMinutes: Int?, + @ColumnInfo(name = "signature") val signature: String + +) + +fun TraceLocation.toTraceLocationEntity(): TraceLocationEntity = + TraceLocationEntity( + guid = guid, + type = type, + description = description, + address = address, + startDate = startDate, + endDate = endDate, + defaultCheckInLengthInMinutes = defaultCheckInLengthInMinutes, + signature = signature, + version = version + ) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/storage/repo/DefaultTraceLocationRepository.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/storage/repo/DefaultTraceLocationRepository.kt new file mode 100644 index 0000000000000000000000000000000000000000..ab931ee3c3f22c898054b1e4391b34213c6a6eae --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/storage/repo/DefaultTraceLocationRepository.kt @@ -0,0 +1,56 @@ +package de.rki.coronawarnapp.eventregistration.storage.repo + +import de.rki.coronawarnapp.eventregistration.events.TraceLocation +import de.rki.coronawarnapp.eventregistration.events.toTraceLocations +import de.rki.coronawarnapp.eventregistration.storage.TraceLocationDatabase +import de.rki.coronawarnapp.eventregistration.storage.dao.TraceLocationDao +import de.rki.coronawarnapp.eventregistration.storage.entity.toTraceLocationEntity +import de.rki.coronawarnapp.util.coroutine.AppScope +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch +import timber.log.Timber +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class DefaultTraceLocationRepository @Inject constructor( + traceLocationDatabaseFactory: TraceLocationDatabase.Factory, + @AppScope private val appScope: CoroutineScope +) : TraceLocationRepository { + + private val traceLocationDatabase: TraceLocationDatabase by lazy { + traceLocationDatabaseFactory.create() + } + + private val traceLocationDao: TraceLocationDao by lazy { + traceLocationDatabase.traceLocationDao() + } + + override val allTraceLocations: Flow<List<TraceLocation>> + get() = traceLocationDao.allEntries().map { it.toTraceLocations() } + + override fun addTraceLocation(event: TraceLocation) { + appScope.launch { + Timber.d("Add hosted event: $event") + val eventEntity = event.toTraceLocationEntity() + traceLocationDao.insert(eventEntity) + } + } + + override fun deleteTraceLocation(event: TraceLocation) { + appScope.launch { + Timber.d("Delete hosted event: $event") + val eventEntity = event.toTraceLocationEntity() + traceLocationDao.delete(eventEntity) + } + } + + override fun deleteAllTraceLocations() { + appScope.launch { + Timber.d("Delete all hosted events.") + traceLocationDao.deleteAll() + } + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/storage/repo/TraceLocationRepository.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/storage/repo/TraceLocationRepository.kt new file mode 100644 index 0000000000000000000000000000000000000000..88576a9cfc9eca4563f129d2f2820c7ab1e9ebee --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/storage/repo/TraceLocationRepository.kt @@ -0,0 +1,15 @@ +package de.rki.coronawarnapp.eventregistration.storage.repo + +import de.rki.coronawarnapp.eventregistration.events.TraceLocation +import kotlinx.coroutines.flow.Flow + +interface TraceLocationRepository { + + val allTraceLocations: Flow<List<TraceLocation>> + + fun addTraceLocation(event: TraceLocation) + + fun deleteTraceLocation(event: TraceLocation) + + fun deleteAllTraceLocations() +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/UIExtensions.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/UIExtensions.kt index 3863ee8015d64f51679e2a9f09645580fad0d187..4a8dea0d8b21ac5ecbcae1fd927a7b38c61d48a1 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/UIExtensions.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/UIExtensions.kt @@ -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 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 new file mode 100644 index 0000000000000000000000000000000000000000..a87ab34f421700514e26c8d5ce57a7357e9f973a --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/EventRegistrationUIModule.kt @@ -0,0 +1,23 @@ +package de.rki.coronawarnapp.ui.eventregistration + +import dagger.Module +import dagger.android.ContributesAndroidInjector +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 { + + @ContributesAndroidInjector(modules = [ScanCheckInQrCodeModule::class]) + abstract fun scanCheckInQrCodeFragment(): ScanCheckInQrCodeFragment + + @ContributesAndroidInjector(modules = [ConfirmCheckInModule::class]) + abstract fun confirmCheckInFragment(): ConfirmCheckInFragment + + @ContributesAndroidInjector(modules = [CheckInsModule::class]) + abstract fun checkInsFragment(): CheckInsFragment +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/attendee/checkin/CheckInsFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/attendee/checkin/CheckInsFragment.kt new file mode 100644 index 0000000000000000000000000000000000000000..2ab0eb9fa9a49a81953fbe7624ecbfdad17917db --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/attendee/checkin/CheckInsFragment.kt @@ -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() + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/attendee/checkin/CheckInsModule.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/attendee/checkin/CheckInsModule.kt new file mode 100644 index 0000000000000000000000000000000000000000..d4b68e13ea370f9e4ddfbee26a9deab8b45b392f --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/attendee/checkin/CheckInsModule.kt @@ -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> +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/attendee/checkin/CheckInsViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/attendee/checkin/CheckInsViewModel.kt new file mode 100644 index 0000000000000000000000000000000000000000..c72f034e320f2f289cdbbbb1f9e4889145e91d85 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/attendee/checkin/CheckInsViewModel.kt @@ -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> +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/attendee/checkin/VerifiedTraceLocation.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/attendee/checkin/VerifiedTraceLocation.kt new file mode 100644 index 0000000000000000000000000000000000000000..b8c61317dba996501964090f76d8caa5774aeb2d --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/attendee/checkin/VerifiedTraceLocation.kt @@ -0,0 +1,35 @@ +package de.rki.coronawarnapp.ui.eventregistration.attendee.checkin + +import android.os.Parcelable +import de.rki.coronawarnapp.eventregistration.checkins.qrcode.QRCodeVerifyResult +import kotlinx.parcelize.Parcelize +import okio.ByteString.Companion.toByteString +import org.joda.time.Instant +import java.util.concurrent.TimeUnit + +@Parcelize +data class VerifiedTraceLocation( + val guid: String, + val description: String?, + val start: Instant?, + val end: Instant?, + val defaultCheckInLengthInMinutes: Int, + // TODO add required properties to confirm check-in +) : Parcelable + +fun QRCodeVerifyResult.toVerifiedTraceLocation() = + with(singedTraceLocation.location) { + VerifiedTraceLocation( + guid = guid.toByteArray().toByteString().base64(), + start = startTimestamp.toInstant(), + end = endTimestamp.toInstant(), + description = description, + defaultCheckInLengthInMinutes = defaultCheckInLengthInMinutes + ) + } + +/** + * Converts time in seconds into [Instant] + */ +private fun Long.toInstant() = + if (this == 0L) null else Instant.ofEpochMilli(TimeUnit.SECONDS.toMillis(this)) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/attendee/confirm/ConfirmCheckInFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/attendee/confirm/ConfirmCheckInFragment.kt new file mode 100644 index 0000000000000000000000000000000000000000..804b57eb9cb2ae9b46e8412356bced243ea9f228 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/attendee/confirm/ConfirmCheckInFragment.kt @@ -0,0 +1,48 @@ +package de.rki.coronawarnapp.ui.eventregistration.attendee.confirm + +import android.os.Bundle +import android.view.View +import androidx.fragment.app.Fragment +import androidx.navigation.fragment.navArgs +import de.rki.coronawarnapp.R +import de.rki.coronawarnapp.databinding.FragmentConfirmCheckInBinding +import de.rki.coronawarnapp.util.di.AutoInject +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 javax.inject.Inject + +class ConfirmCheckInFragment : Fragment(R.layout.fragment_confirm_check_in), AutoInject { + + @Inject lateinit var viewModelFactory: CWAViewModelFactoryProvider.Factory + + private val viewModel: ConfirmCheckInViewModel by cwaViewModels { viewModelFactory } + private val binding: FragmentConfirmCheckInBinding by viewBindingLazy() + private val args by navArgs<ConfirmCheckInFragmentArgs>() + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + with(binding) { + toolbar.setNavigationOnClickListener { viewModel.onClose() } + confirmButton.setOnClickListener { viewModel.onConfirmTraceLocation() } + // TODO bind final UI + eventGuid.text = "GUID: %s".format(args.traceLocation.guid) + startTime.text = "Start time: %s".format(args.traceLocation.start) + endTime.text = "End time: %s".format(args.traceLocation.end) + description.text = "Description: %s".format(args.traceLocation.description) + } + + viewModel.events.observe2(this) { navEvent -> + when (navEvent) { + ConfirmCheckInNavigation.BackNavigation -> popBackStack() + ConfirmCheckInNavigation.ConfirmNavigation -> { + // TODO Navigate to the rightful destination + popBackStack() + } + } + } + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/attendee/confirm/ConfirmCheckInModule.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/attendee/confirm/ConfirmCheckInModule.kt new file mode 100644 index 0000000000000000000000000000000000000000..fa18d2e2e0caabeeac574cfb141687db6f4dfe75 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/attendee/confirm/ConfirmCheckInModule.kt @@ -0,0 +1,18 @@ +package de.rki.coronawarnapp.ui.eventregistration.attendee.confirm + +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 ConfirmCheckInModule { + @Binds + @IntoMap + @CWAViewModelKey(ConfirmCheckInViewModel::class) + abstract fun confirmCheckInFragment( + factory: ConfirmCheckInViewModel.Factory + ): CWAViewModelFactory<out CWAViewModel> +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/attendee/confirm/ConfirmCheckInNavigation.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/attendee/confirm/ConfirmCheckInNavigation.kt new file mode 100644 index 0000000000000000000000000000000000000000..0620735ed73a68cd34c6842eb3f909de36091ac0 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/attendee/confirm/ConfirmCheckInNavigation.kt @@ -0,0 +1,6 @@ +package de.rki.coronawarnapp.ui.eventregistration.attendee.confirm + +sealed class ConfirmCheckInNavigation { + object BackNavigation : ConfirmCheckInNavigation() + object ConfirmNavigation : ConfirmCheckInNavigation() +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/attendee/confirm/ConfirmCheckInViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/attendee/confirm/ConfirmCheckInViewModel.kt new file mode 100644 index 0000000000000000000000000000000000000000..b3931016005d6dbe248ab12e52d20af88c4e66bb --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/attendee/confirm/ConfirmCheckInViewModel.kt @@ -0,0 +1,22 @@ +package de.rki.coronawarnapp.ui.eventregistration.attendee.confirm + +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import de.rki.coronawarnapp.util.ui.SingleLiveEvent +import de.rki.coronawarnapp.util.viewmodel.CWAViewModel +import de.rki.coronawarnapp.util.viewmodel.SimpleCWAViewModelFactory + +class ConfirmCheckInViewModel @AssistedInject constructor() : CWAViewModel() { + val events = SingleLiveEvent<ConfirmCheckInNavigation>() + + fun onClose() { + events.value = ConfirmCheckInNavigation.BackNavigation + } + + fun onConfirmTraceLocation() { + events.value = ConfirmCheckInNavigation.ConfirmNavigation + } + + @AssistedFactory + interface Factory : SimpleCWAViewModelFactory<ConfirmCheckInViewModel> +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/attendee/scan/ScanCheckInQrCodeFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/attendee/scan/ScanCheckInQrCodeFragment.kt new file mode 100644 index 0000000000000000000000000000000000000000..c8d757c662b984b9c7eaf5636b934071740f2111 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/attendee/scan/ScanCheckInQrCodeFragment.kt @@ -0,0 +1,162 @@ +package de.rki.coronawarnapp.ui.eventregistration.attendee.scan + +import android.Manifest +import android.content.pm.PackageManager +import android.os.Bundle +import android.view.View +import android.view.accessibility.AccessibilityEvent.TYPE_ANNOUNCEMENT +import androidx.fragment.app.Fragment +import androidx.navigation.NavOptions +import androidx.navigation.fragment.findNavController +import com.google.android.material.transition.MaterialContainerTransform +import com.google.zxing.BarcodeFormat +import com.journeyapps.barcodescanner.DefaultDecoderFactory +import de.rki.coronawarnapp.R +import de.rki.coronawarnapp.databinding.FragmentScanCheckInQrCodeBinding +import de.rki.coronawarnapp.ui.eventregistration.attendee.checkin.CheckInsFragment +import de.rki.coronawarnapp.util.CameraPermissionHelper +import de.rki.coronawarnapp.util.DialogHelper +import de.rki.coronawarnapp.util.di.AutoInject +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 ScanCheckInQrCodeFragment : + Fragment(R.layout.fragment_scan_check_in_qr_code), + AutoInject { + + @Inject lateinit var viewModelFactory: CWAViewModelFactoryProvider.Factory + private val viewModel: ScanCheckInQrCodeViewModel by cwaViewModels { viewModelFactory } + + private val binding: FragmentScanCheckInQrCodeBinding by viewBindingLazy() + private var showsPermissionDialog = false + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + sharedElementEnterTransition = MaterialContainerTransform() + sharedElementReturnTransition = MaterialContainerTransform() + } + + override fun onViewCreated( + view: View, + savedInstanceState: Bundle? + ) { + with(binding) { + checkInQrCodeScanTorch.setOnCheckedChangeListener { _, isChecked -> + binding.checkInQrCodeScanPreview.setTorch(isChecked) + } + checkInQrCodeScanClose.setOnClickListener { viewModel.onNavigateUp() } + checkInQrCodeScanPreview.decoderFactory = DefaultDecoderFactory(listOf(BarcodeFormat.QR_CODE)) + checkInQrCodeScanViewfinderView.setCameraPreview(binding.checkInQrCodeScanPreview) + } + + viewModel.events.observe2(this) { navEvent -> + when (navEvent) { + is ScanCheckInQrCodeNavigation.BackNavigation -> popBackStack() + is ScanCheckInQrCodeNavigation.ScanResultNavigation -> { + Timber.i(navEvent.uri) + findNavController().navigate( + CheckInsFragment.uri(navEvent.uri), + NavOptions.Builder() + .setPopUpTo(R.id.checkInsFragment, true) + .build() + ) + } + } + } + } + + override fun onResume() { + super.onResume() + binding.checkInQrCodeScanContainer.sendAccessibilityEvent(TYPE_ANNOUNCEMENT) + if (CameraPermissionHelper.hasCameraPermission(requireActivity())) { + binding.checkInQrCodeScanPreview.resume() + startDecode() + return + } + if (showsPermissionDialog) return + + requestCameraPermission() + } + + override fun onRequestPermissionsResult( + requestCode: Int, + permissions: Array<String>, + grantResults: IntArray + ) { + if (requestCode == REQUEST_CAMERA_PERMISSION_CODE && + grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_DENIED + ) { + if (shouldShowRequestPermissionRationale(Manifest.permission.CAMERA)) { + showCameraPermissionRationaleDialog() + } else { + // User permanently denied access to the camera + showCameraPermissionDeniedDialog() + } + } + } + + private fun startDecode() = binding.checkInQrCodeScanPreview + .decodeSingle { barcodeResult -> + viewModel.onScanResult(barcodeResult) + } + + private fun showCameraPermissionDeniedDialog() { + val permissionDeniedDialog = DialogHelper.DialogInstance( + requireActivity(), + // TODO use strings for this screen + R.string.submission_qr_code_scan_permission_denied_dialog_headline, + R.string.submission_qr_code_scan_permission_denied_dialog_body, + R.string.submission_qr_code_scan_permission_denied_dialog_button, + cancelable = false, + positiveButtonFunction = { + showsPermissionDialog = false + viewModel.onNavigateUp() + } + ) + showsPermissionDialog = true + DialogHelper.showDialog(permissionDeniedDialog) + } + + private fun showCameraPermissionRationaleDialog() { + val cameraPermissionRationaleDialogInstance = DialogHelper.DialogInstance( + requireActivity(), + // TODO use strings for this screen + R.string.submission_qr_code_scan_permission_rationale_dialog_headline, + R.string.submission_qr_code_scan_permission_rationale_dialog_body, + R.string.submission_qr_code_scan_permission_rationale_dialog_button_positive, + R.string.submission_qr_code_scan_permission_rationale_dialog_button_negative, + false, + { + showsPermissionDialog = false + requestCameraPermission() + }, + { + showsPermissionDialog = false + viewModel.onNavigateUp() + } + ) + + showsPermissionDialog = true + DialogHelper.showDialog(cameraPermissionRationaleDialogInstance) + } + + private fun requestCameraPermission() = requestPermissions( + arrayOf(Manifest.permission.CAMERA), + REQUEST_CAMERA_PERMISSION_CODE + ) + + override fun onPause() { + super.onPause() + binding.checkInQrCodeScanPreview.pause() + } + + companion object { + private const val REQUEST_CAMERA_PERMISSION_CODE = 4000 + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/attendee/scan/ScanCheckInQrCodeModule.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/attendee/scan/ScanCheckInQrCodeModule.kt new file mode 100644 index 0000000000000000000000000000000000000000..37349adb9a52a9b8f936f64993e9395d8f43da8a --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/attendee/scan/ScanCheckInQrCodeModule.kt @@ -0,0 +1,18 @@ +package de.rki.coronawarnapp.ui.eventregistration.attendee.scan + +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 ScanCheckInQrCodeModule { + @Binds + @IntoMap + @CWAViewModelKey(ScanCheckInQrCodeViewModel::class) + abstract fun scanCheckInQrCodeFragment( + factory: ScanCheckInQrCodeViewModel.Factory + ): CWAViewModelFactory<out CWAViewModel> +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/attendee/scan/ScanCheckInQrCodeNavigation.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/attendee/scan/ScanCheckInQrCodeNavigation.kt new file mode 100644 index 0000000000000000000000000000000000000000..db3c562d8276579ab79d50043d0b347b67895760 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/attendee/scan/ScanCheckInQrCodeNavigation.kt @@ -0,0 +1,6 @@ +package de.rki.coronawarnapp.ui.eventregistration.attendee.scan + +sealed class ScanCheckInQrCodeNavigation { + object BackNavigation : ScanCheckInQrCodeNavigation() + data class ScanResultNavigation(val uri: String) : ScanCheckInQrCodeNavigation() +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/attendee/scan/ScanCheckInQrCodeViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/attendee/scan/ScanCheckInQrCodeViewModel.kt new file mode 100644 index 0000000000000000000000000000000000000000..5452829bd49a50f995ae826f1041798b12e52d84 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/attendee/scan/ScanCheckInQrCodeViewModel.kt @@ -0,0 +1,25 @@ +package de.rki.coronawarnapp.ui.eventregistration.attendee.scan + +import com.journeyapps.barcodescanner.BarcodeResult +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import de.rki.coronawarnapp.util.ui.SingleLiveEvent +import de.rki.coronawarnapp.util.viewmodel.CWAViewModel +import de.rki.coronawarnapp.util.viewmodel.SimpleCWAViewModelFactory + +class ScanCheckInQrCodeViewModel @AssistedInject constructor() : CWAViewModel() { + val events = SingleLiveEvent<ScanCheckInQrCodeNavigation>() + + fun onNavigateUp() { + events.value = ScanCheckInQrCodeNavigation.BackNavigation + } + + fun onScanResult(barcodeResult: BarcodeResult) { + events.value = ScanCheckInQrCodeNavigation.ScanResultNavigation( + barcodeResult.result.text + ) + } + + @AssistedFactory + interface Factory : SimpleCWAViewModelFactory<ScanCheckInQrCodeViewModel> +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/launcher/LauncherActivity.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/launcher/LauncherActivity.kt index 90c3fe4bce7c607f8ef654177a82015d632e8a30..27a61dabff03e3af8e5693db96de5f57c5c005fd 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/launcher/LauncherActivity.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/launcher/LauncherActivity.kt @@ -9,7 +9,6 @@ import de.rki.coronawarnapp.R import de.rki.coronawarnapp.ui.main.MainActivity import de.rki.coronawarnapp.ui.onboarding.OnboardingActivity import de.rki.coronawarnapp.util.di.AppInjector -import de.rki.coronawarnapp.util.shortcuts.AppShortcutsHelper import de.rki.coronawarnapp.util.viewmodel.CWAViewModelFactoryProvider import de.rki.coronawarnapp.util.viewmodel.cwaViewModels import javax.inject.Inject @@ -30,12 +29,12 @@ class LauncherActivity : AppCompatActivity() { vm.events.observe(this) { when (it) { LauncherEvent.GoToOnboarding -> { - OnboardingActivity.start(this, AppShortcutsHelper.getShortcutType(intent)) + OnboardingActivity.start(this, intent) this.overridePendingTransition(0, 0) finish() } LauncherEvent.GoToMainActivity -> { - MainActivity.start(this, AppShortcutsHelper.getShortcutType(intent)) + MainActivity.start(this, intent) this.overridePendingTransition(0, 0) finish() } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/MainActivity.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/MainActivity.kt index 545557ddd88760054a96560d618d8db498bc01f7..12ba28358a75be28d1cfa0d45252d25ffa996e32 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/MainActivity.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/MainActivity.kt @@ -24,6 +24,7 @@ import de.rki.coronawarnapp.datadonation.analytics.worker.DataDonationAnalyticsS import de.rki.coronawarnapp.deadman.DeadmanNotificationScheduler import de.rki.coronawarnapp.submission.SubmissionSettings import de.rki.coronawarnapp.ui.base.startActivitySafely +import de.rki.coronawarnapp.ui.eventregistration.attendee.checkin.CheckInsFragment import de.rki.coronawarnapp.ui.setupWithNavController2 import de.rki.coronawarnapp.util.AppShortcuts import de.rki.coronawarnapp.util.CWADebug @@ -31,11 +32,13 @@ import de.rki.coronawarnapp.util.ConnectivityHelper import de.rki.coronawarnapp.util.DialogHelper import de.rki.coronawarnapp.util.device.PowerManagement import de.rki.coronawarnapp.util.di.AppInjector +import de.rki.coronawarnapp.util.shortcuts.AppShortcutsHelper.Companion.getShortcutExtra import de.rki.coronawarnapp.util.ui.findNavController import de.rki.coronawarnapp.util.viewmodel.CWAViewModelFactoryProvider import de.rki.coronawarnapp.util.viewmodel.cwaViewModels import de.rki.coronawarnapp.worker.BackgroundWorkScheduler import org.joda.time.LocalDate +import timber.log.Timber import javax.inject.Inject /** @@ -47,24 +50,14 @@ import javax.inject.Inject */ class MainActivity : AppCompatActivity(), HasAndroidInjector { companion object { - private const val EXTRA_DATA = "shortcut" - - fun start(context: Context, shortcut: AppShortcuts? = null) { - val intent = Intent(context, MainActivity::class.java).apply { - if (shortcut != null) { - putExtra(EXTRA_DATA, shortcut.toString()) - flags = flags or Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK - } + fun start(context: Context, launchIntent: Intent) { + Intent(context, MainActivity::class.java).apply { + flags = flags or Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK + Timber.i("launchIntent:$launchIntent") + fillIn(launchIntent, Intent.FILL_IN_DATA) + Timber.i("filledIntent:$this") + context.startActivity(this) } - context.startActivity(intent) - } - - private fun getShortcutFromIntent(intent: Intent): AppShortcuts? { - val extra = intent.getStringExtra(EXTRA_DATA) - if (extra != null) { - return AppShortcuts.valueOf(extra) - } - return null } } @@ -80,6 +73,8 @@ class MainActivity : AppCompatActivity(), HasAndroidInjector { private val FragmentManager.currentNavigationFragment: Fragment? get() = primaryNavigationFragment?.childFragmentManager?.fragments?.first() + private val navController by lazy { supportFragmentManager.findNavController(R.id.nav_host_fragment) } + @Inject lateinit var powerManagement: PowerManagement @Inject lateinit var deadmanScheduler: DeadmanNotificationScheduler @Inject lateinit var contactDiaryWorkScheduler: ContactDiaryWorkScheduler @@ -106,7 +101,6 @@ class MainActivity : AppCompatActivity(), HasAndroidInjector { showEnergyOptimizedEnabledForBackground() } - val navController = supportFragmentManager.findNavController(R.id.nav_host_fragment) binding.mainBottomNavigation.setupWithNavController2(navController) { vm.onBottomNavSelected() } @@ -119,16 +113,21 @@ class MainActivity : AppCompatActivity(), HasAndroidInjector { } } + override fun onNewIntent(intent: Intent?) { + super.onNewIntent(intent) + Timber.i("onNewIntent:$intent") + navigateByIntentUri(intent) + } + private fun processExtraParameters() { - when (getShortcutFromIntent(intent)) { - AppShortcuts.CONTACT_DIARY -> { - goToContactJournal() - } + when (intent.getShortcutExtra()) { + AppShortcuts.CONTACT_DIARY -> goToContactJournal() } + + navigateByIntentUri(intent) } private fun goToContactJournal() { - val navController = supportFragmentManager.findNavController(R.id.nav_host_fragment) findViewById<BottomNavigationView>(R.id.main_bottom_navigation).selectedItemId = R.id.contact_diary_nav_graph val nestedGraph = navController.graph.findNode(R.id.contact_diary_nav_graph) as NavGraph @@ -155,6 +154,12 @@ class MainActivity : AppCompatActivity(), HasAndroidInjector { } } + private fun navigateByIntentUri(intent: Intent?) { + val uri = intent?.data ?: return + Timber.i("Uri:$uri") + navController.navigate(CheckInsFragment.uri(uri.toString())) + } + /** * Register callbacks. */ diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/MainActivityModule.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/MainActivityModule.kt index 72222d127801669f8442f1c624e67c85de13ce85..dc98543bbc48644fdbebc937c055cb39c33d74aa 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/MainActivityModule.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/MainActivityModule.kt @@ -8,6 +8,7 @@ import de.rki.coronawarnapp.datadonation.analytics.ui.AnalyticsUIModule import de.rki.coronawarnapp.release.NewReleaseInfoFragment import de.rki.coronawarnapp.release.NewReleaseInfoFragmentModule import de.rki.coronawarnapp.tracing.ui.details.TracingDetailsFragmentModule +import de.rki.coronawarnapp.ui.eventregistration.EventRegistrationUIModule import de.rki.coronawarnapp.ui.information.InformationFragmentModule import de.rki.coronawarnapp.ui.interoperability.InteroperabilityConfigurationFragment import de.rki.coronawarnapp.ui.interoperability.InteroperabilityConfigurationFragmentModule @@ -32,7 +33,8 @@ import de.rki.coronawarnapp.util.viewmodel.CWAViewModelKey SubmissionFragmentModule::class, InformationFragmentModule::class, NewReleaseInfoFragmentModule::class, - AnalyticsUIModule::class + AnalyticsUIModule::class, + EventRegistrationUIModule::class, ] ) abstract class MainActivityModule { diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/onboarding/OnboardingActivity.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/onboarding/OnboardingActivity.kt index 3888efc6398c11c741d3fc29bc6d796e1c36254e..1309c84def1530eaccad02f74ac8987bede1055f 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/onboarding/OnboardingActivity.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/onboarding/OnboardingActivity.kt @@ -15,9 +15,9 @@ import de.rki.coronawarnapp.environment.BuildConfigWrap import de.rki.coronawarnapp.main.CWASettings import de.rki.coronawarnapp.storage.OnboardingSettings import de.rki.coronawarnapp.ui.main.MainActivity -import de.rki.coronawarnapp.util.AppShortcuts import de.rki.coronawarnapp.util.TimeStamper import de.rki.coronawarnapp.util.di.AppInjector +import timber.log.Timber import javax.inject.Inject /** @@ -26,22 +26,16 @@ import javax.inject.Inject */ class OnboardingActivity : AppCompatActivity(), LifecycleObserver, HasAndroidInjector { companion object { - private val TAG: String? = OnboardingActivity::class.simpleName - private const val EXTRA_DATA = "shortcut" - fun start(context: Context, shortcut: AppShortcuts? = null) { - val intent = Intent(context, OnboardingActivity::class.java).apply { - putExtra(EXTRA_DATA, shortcut?.toString()) + fun start(context: Context, launchIntent: Intent? = null) { + val intent = Intent(context, OnboardingActivity::class.java) + Timber.i("launchIntent:$launchIntent") + launchIntent?.let { + intent.fillIn(it, Intent.FILL_IN_DATA) + Timber.i("filledIntent:$intent") } context.startActivity(intent) } - - fun getShortcutFromIntent(intent: Intent?): AppShortcuts? { - intent?.getStringExtra(EXTRA_DATA)?.let { - return AppShortcuts.valueOf(it) - } - return null - } } @Inject lateinit var dispatchingAndroidInjector: DispatchingAndroidInjector<Any> @@ -74,12 +68,9 @@ class OnboardingActivity : AppCompatActivity(), LifecycleObserver, HasAndroidInj fun completeOnboarding() { onboardingSettings.onboardingCompletedTimestamp = timeStamper.nowUTC - - settings.lastChangelogVersion.update { - BuildConfigWrap.VERSION_CODE - } - - MainActivity.start(this) + settings.lastChangelogVersion.update { BuildConfigWrap.VERSION_CODE } + settings.lastChangelogVersion.update { BuildConfigWrap.VERSION_CODE } + MainActivity.start(this, intent) finish() } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/onboarding/OnboardingLoadingFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/onboarding/OnboardingLoadingFragment.kt index 7d7c6475ff8122c86e7ae6b8e42260c447f8da71..e4d0d9a7ae1b209bb75af52955d50d84d02541ae 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/onboarding/OnboardingLoadingFragment.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/onboarding/OnboardingLoadingFragment.kt @@ -41,7 +41,7 @@ class OnboardingLoadingFragment : Fragment(R.layout.onboaring_loading_layout), A .actionLoadingFragmentToOnboardingFragment() ) OnboardingFragmentEvents.OnboardingDone -> { - MainActivity.start(requireContext(), OnboardingActivity.getShortcutFromIntent(activity?.intent)) + MainActivity.start(requireContext(), requireActivity().intent) activity?.overridePendingTransition(0, 0) activity?.finish() } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/DataReset.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/DataReset.kt index 08eec626f20d6005b8aec183c6fa79f9a2fc0a7c..37b1347df6e68a0c8022dea5dfb5b76c24dc8280 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/DataReset.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/DataReset.kt @@ -11,6 +11,7 @@ import de.rki.coronawarnapp.datadonation.analytics.storage.AnalyticsSettings import de.rki.coronawarnapp.datadonation.survey.SurveySettings import de.rki.coronawarnapp.diagnosiskeys.download.DownloadDiagnosisKeysSettings import de.rki.coronawarnapp.diagnosiskeys.storage.KeyCacheRepository +import de.rki.coronawarnapp.eventregistration.storage.repo.TraceLocationRepository import de.rki.coronawarnapp.main.CWASettings import de.rki.coronawarnapp.nearby.modules.detectiontracker.ExposureDetectionTracker import de.rki.coronawarnapp.risk.storage.RiskLevelStorage @@ -49,7 +50,8 @@ class DataReset @Inject constructor( private val bugReportingSettings: BugReportingSettings, private val tracingSettings: TracingSettings, private val onboardingSettings: OnboardingSettings, - private val submissionSettings: SubmissionSettings + private val submissionSettings: SubmissionSettings, + private val traceLocationRepository: TraceLocationRepository, ) { private val mutex = Mutex() @@ -86,6 +88,8 @@ class DataReset @Inject constructor( bugReportingSettings.clear() + traceLocationRepository.deleteAllTraceLocations() + Timber.w("CWA LOCAL DATA DELETION COMPLETED.") } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/di/ApplicationComponent.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/di/ApplicationComponent.kt index 35ee578c350d1feeaaaf43f1a4a3e9852c896171..a2c6ec3d39243cc97bae7269e18dbe1bb09fbde2 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/di/ApplicationComponent.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/di/ApplicationComponent.kt @@ -16,6 +16,7 @@ import de.rki.coronawarnapp.diagnosiskeys.DiagnosisKeysModule import de.rki.coronawarnapp.diagnosiskeys.DownloadDiagnosisKeysTaskModule import de.rki.coronawarnapp.diagnosiskeys.storage.KeyCacheRepository import de.rki.coronawarnapp.environment.EnvironmentModule +import de.rki.coronawarnapp.eventregistration.EventRegistrationModule import de.rki.coronawarnapp.http.HttpModule import de.rki.coronawarnapp.nearby.ENFClient import de.rki.coronawarnapp.nearby.ENFModule @@ -73,7 +74,8 @@ import javax.inject.Singleton WorkerBinder::class, StatisticsModule::class, DataDonationModule::class, - SecurityModule::class + SecurityModule::class, + EventRegistrationModule::class, ] ) interface ApplicationComponent : AndroidInjector<CoronaWarnApplication> { diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/files/FileExtensions.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/files/FileExtensions.kt index 8bf768b7824255e8be19e89883b8ae0b2172823d..f5b9f83495c522357b9f1c5b65e200c423cabdcd 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/files/FileExtensions.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/files/FileExtensions.kt @@ -4,5 +4,6 @@ import java.io.File fun File.determineMimeType(): String = when { name.endsWith(".zip") -> "application/zip" + name.endsWith(".pdf") -> "application/pdf" else -> throw UnsupportedOperationException("Unsupported MIME type: $path") } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/files/FileSharing.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/files/FileSharing.kt index 4303506d6e561362417e660cb3a0e2833f31af53..488c8a12f3a23a44cb9f8a7ad51cfb762d7f346b 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/files/FileSharing.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/files/FileSharing.kt @@ -50,6 +50,38 @@ class FileSharing @Inject constructor( } } + fun getFileIntentProvider( + path: File, + title: String, + @StringRes chooserTitle: Int? = null + ): FileIntentProvider = object : FileIntentProvider { + override fun intent(activity: Activity): Intent { + val builder = ShareCompat.IntentBuilder.from(activity).apply { + setType(path.determineMimeType()) + setStream(getFileUri(path)) + setSubject(title) + chooserTitle?.let { setChooserTitle(it) } + } + + val intent = if (chooserTitle != null) { + builder.createChooserIntent() + } else { + builder.intent + } + return intent.apply { + addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + Timber.tag(TAG).d("Intent created %s", this) + } + } + + override val file: File = path + } + + interface FileIntentProvider { + fun intent(activity: Activity): Intent + val file: File + } + interface ShareIntentProvider { fun get(activity: Activity): Intent } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/shortcuts/AppShortcutsHelper.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/shortcuts/AppShortcutsHelper.kt index 0aeb1345d353a2b058806d25494139d272b83d14..a12907e37134b29d20e21c7077ace62c2b588ed4 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/shortcuts/AppShortcutsHelper.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/shortcuts/AppShortcutsHelper.kt @@ -36,18 +36,17 @@ class AppShortcutsHelper @Inject constructor(@AppContext private val context: Co private fun createContactDiaryIntent() = Intent(context, LauncherActivity::class.java).apply { action = Intent.ACTION_VIEW - putExtra(SHORTCUT_EXTRA_ID, AppShortcuts.CONTACT_DIARY.toString()) + putExtra(SHORTCUT_EXTRA, AppShortcuts.CONTACT_DIARY.toString()) } companion object { private const val CONTACT_DIARY_SHORTCUT_ID = "contact_diary_id" - private const val SHORTCUT_EXTRA_ID = "shortcut_extra" + const val SHORTCUT_EXTRA = "shortcut_extra" - fun getShortcutType(intent: Intent): AppShortcuts? { - intent.getStringExtra(SHORTCUT_EXTRA_ID)?.let { + fun Intent.getShortcutExtra(): AppShortcuts? { + getStringExtra(SHORTCUT_EXTRA)?.let { return AppShortcuts.valueOf(it) } - return null } } diff --git a/Corona-Warn-App/src/main/res/drawable/ic_nav_check_in.xml b/Corona-Warn-App/src/main/res/drawable/ic_nav_check_in.xml new file mode 100644 index 0000000000000000000000000000000000000000..a0597989a33e9a8cfb17dc58612d6016840fc97e --- /dev/null +++ b/Corona-Warn-App/src/main/res/drawable/ic_nav_check_in.xml @@ -0,0 +1,9 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="23dp" + android:height="22dp" + android:viewportWidth="23" + android:viewportHeight="22"> + <path + android:pathData="M21.1465,7.581C21.7158,7.581 22.0166,7.2588 22.0166,6.7002V4.0684C22.0166,1.8555 20.8887,0.749 18.6436,0.749H16.0117C15.4531,0.749 15.1416,1.0498 15.1416,1.6084C15.1416,2.167 15.4531,2.4785 16.0117,2.4785H18.6113C19.6748,2.4785 20.2871,3.0478 20.2871,4.165V6.7002C20.2871,7.2588 20.5986,7.581 21.1465,7.581ZM1.8428,7.581C2.4121,7.581 2.7129,7.2588 2.7129,6.7002V4.165C2.7129,3.0478 3.3037,2.4785 4.3779,2.4785H6.9775C7.5469,2.4785 7.8584,2.167 7.8584,1.6084C7.8584,1.0498 7.5469,0.749 6.9775,0.749H4.3565C2.1113,0.749 0.9834,1.8555 0.9834,4.0684V6.7002C0.9834,7.2588 1.2949,7.581 1.8428,7.581ZM7.3106,17.0771H15.668C16.4199,17.0771 16.7637,16.7871 16.7637,16.0352V6.4746C16.7637,5.7227 16.4199,5.4326 15.668,5.4326H7.3106C6.5586,5.4326 6.2148,5.7227 6.2148,6.4746V16.0352C6.2148,16.7871 6.5586,17.0771 7.3106,17.0771ZM9.169,9.3857C8.8145,9.3857 8.5889,9.1494 8.5889,8.8164C8.5889,8.4619 8.8145,8.2256 9.169,8.2256H13.8203C14.1748,8.2256 14.4111,8.4619 14.4111,8.8164C14.4111,9.1494 14.1748,9.3857 13.8203,9.3857H9.169ZM9.169,11.6953C8.8145,11.6953 8.5889,11.4482 8.5889,11.1152C8.5889,10.7715 8.8145,10.5244 9.169,10.5244H11.7363C12.0801,10.5244 12.3164,10.7715 12.3164,11.1152C12.3164,11.4482 12.0801,11.6953 11.7363,11.6953H9.169ZM4.3565,21.7715H6.9775C7.5469,21.7715 7.8584,21.46 7.8584,20.9121C7.8584,20.3535 7.5469,20.042 6.9775,20.042H4.3779C3.3037,20.042 2.7129,19.4727 2.7129,18.3555V15.8203C2.7129,15.251 2.4014,14.9395 1.8428,14.9395C1.2842,14.9395 0.9834,15.251 0.9834,15.8203V18.4414C0.9834,20.665 2.1113,21.7715 4.3565,21.7715ZM16.0117,21.7715H18.6436C20.8887,21.7715 22.0166,20.6543 22.0166,18.4414V15.8203C22.0166,15.251 21.7051,14.9395 21.1465,14.9395C20.5879,14.9395 20.2871,15.251 20.2871,15.8203V18.3555C20.2871,19.4727 19.6748,20.042 18.6113,20.042H16.0117C15.4531,20.042 15.1416,20.3535 15.1416,20.9121C15.1416,21.46 15.4531,21.7715 16.0117,21.7715Z" + android:fillColor="#949494"/> +</vector> diff --git a/Corona-Warn-App/src/main/res/layout/fragment_check_ins.xml b/Corona-Warn-App/src/main/res/layout/fragment_check_ins.xml new file mode 100644 index 0000000000000000000000000000000000000000..1053da359f440f76e1861823d2946dea460a8370 --- /dev/null +++ b/Corona-Warn-App/src/main/res/layout/fragment_check_ins.xml @@ -0,0 +1,37 @@ +<?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" + android:id="@+id/content_container" + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <com.google.android.material.appbar.MaterialToolbar + android:id="@+id/toolbar" + android:layout_width="0dp" + android:layout_height="wrap_content" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + + <androidx.recyclerview.widget.RecyclerView + android:id="@+id/check_ins_recycler" + android:layout_width="match_parent" + android:layout_height="match_parent" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/toolbar" /> + + <com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton + android:id="@+id/scan_checkin_qrcode_fab" + style="@style/Widget.App.ExtendedFloatingActionButton" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_margin="@dimen/spacing_normal" + android:text="@string/scan_check_in_qr_code" + android:transitionName="shared_element_container" + app:icon="@drawable/ic_nav_check_in" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" /> + +</androidx.constraintlayout.widget.ConstraintLayout> \ No newline at end of file diff --git a/Corona-Warn-App/src/main/res/layout/fragment_confirm_check_in.xml b/Corona-Warn-App/src/main/res/layout/fragment_confirm_check_in.xml new file mode 100644 index 0000000000000000000000000000000000000000..b5b111f19b1de861eafe5a4e015ef7416d026c45 --- /dev/null +++ b/Corona-Warn-App/src/main/res/layout/fragment_confirm_check_in.xml @@ -0,0 +1,54 @@ +<?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" + tools:context=".ui.eventregistration.attendee.confirm.ConfirmCheckInFragment"> + + <androidx.appcompat.widget.Toolbar + android:id="@+id/toolbar" + style="@style/CWAToolbar.Close" + android:layout_width="0dp" + android:layout_height="wrap_content" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + + <!-- TODO implement actual UI --> + <LinearLayout + style="@style/Card" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_margin="10dp" + android:orientation="vertical" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/toolbar"> + <TextView + android:id="@+id/eventGuid" + android:layout_width="match_parent" + android:layout_height="wrap_content" /> + <TextView + android:id="@+id/startTime" + android:layout_width="match_parent" + android:layout_height="wrap_content" /> + + <TextView + android:id="@+id/endTime" + android:layout_width="match_parent" + android:layout_height="wrap_content" /> + + <TextView + android:id="@+id/description" + android:layout_width="match_parent" + android:layout_height="wrap_content" /> + + <com.google.android.material.button.MaterialButton + android:id="@+id/confirmButton" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:text="Confirm" /> + </LinearLayout> + +</androidx.constraintlayout.widget.ConstraintLayout> \ No newline at end of file diff --git a/Corona-Warn-App/src/main/res/layout/fragment_scan_check_in_qr_code.xml b/Corona-Warn-App/src/main/res/layout/fragment_scan_check_in_qr_code.xml new file mode 100644 index 0000000000000000000000000000000000000000..b14d08f71b9816b0b02be971c69885988077d54b --- /dev/null +++ b/Corona-Warn-App/src/main/res/layout/fragment_scan_check_in_qr_code.xml @@ -0,0 +1,97 @@ +<?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" + android:id="@+id/check_in_qr_code_scan_container" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:contentDescription="@string/submission_qr_code_scan_title" + android:transitionName="shared_element_container"> + + <com.journeyapps.barcodescanner.BarcodeView + android:id="@+id/check_in_qr_code_scan_preview" + android:layout_width="0dp" + android:layout_height="0dp" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + app:zxing_framing_rect_height="@dimen/submission_scan_qr_code_viewfinder_size" + app:zxing_framing_rect_width="@dimen/submission_scan_qr_code_viewfinder_size"> + + </com.journeyapps.barcodescanner.BarcodeView> + + <com.journeyapps.barcodescanner.ViewfinderView + android:id="@+id/check_in_qr_code_scan_viewfinder_view" + android:layout_width="0dp" + android:layout_height="0dp" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + app:zxing_viewfinder_laser_visibility="false" /> + + <TextView + android:id="@+id/check_in_qr_code_scan_body" + style="@style/registrationQRCodeScanBody" + android:layout_width="@dimen/submission_scan_qr_code_viewfinder_size" + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/submission_scan_qr_code_viewfinder_center_offset" + android:text="@string/submission_qr_code_scan_body" + app:layout_constraintEnd_toEndOf="@id/check_in_qr_code_scan_preview" + app:layout_constraintStart_toStartOf="@id/check_in_qr_code_scan_preview" + app:layout_constraintTop_toBottomOf="@id/check_in_qr_code_scan_guideline_center" /> + + + <androidx.constraintlayout.widget.ConstraintLayout + android:id="@+id/check_in_qr_code_scan_close" + style="@style/buttonIcon" + android:layout_width="@dimen/icon_size_button" + android:layout_height="@dimen/icon_size_button" + app:layout_constraintBottom_toTopOf="@id/check_in_qr_code_scan_guideline_top" + app:layout_constraintEnd_toStartOf="@id/guideline_start" + app:layout_constraintStart_toStartOf="@id/guideline_start" + app:layout_constraintTop_toTopOf="@id/check_in_qr_code_scan_guideline_top"> + + <androidx.appcompat.widget.AppCompatImageView + style="@style/iconStable" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:contentDescription="@string/accessibility_close" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + app:srcCompat="@drawable/ic_close" /> + + </androidx.constraintlayout.widget.ConstraintLayout> + + <ToggleButton + android:id="@+id/check_in_qr_code_scan_torch" + android:layout_width="@dimen/icon_size_button" + android:layout_height="@dimen/icon_size_button" + android:background="@drawable/ic_registration_qr_code_scan_torch_toggle" + android:backgroundTint="@color/colorStableLight" + android:textOff="" + android:textOn="" + app:layout_constraintBottom_toTopOf="@id/check_in_qr_code_scan_guideline_top" + app:layout_constraintEnd_toStartOf="@id/guideline_end" + app:layout_constraintStart_toStartOf="@id/guideline_end" + app:layout_constraintTop_toTopOf="@id/check_in_qr_code_scan_guideline_top" /> + + <androidx.constraintlayout.widget.Guideline + android:id="@+id/check_in_qr_code_scan_guideline_top" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:orientation="horizontal" + app:layout_constraintGuide_begin="@dimen/spacing_normal" /> + + <androidx.constraintlayout.widget.Guideline + android:id="@+id/check_in_qr_code_scan_guideline_center" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:orientation="horizontal" + app:layout_constraintGuide_percent="0.5" /> + + <include layout="@layout/merge_guidelines_side" /> + +</androidx.constraintlayout.widget.ConstraintLayout> \ No newline at end of file diff --git a/Corona-Warn-App/src/main/res/menu/menu_bottom_nav.xml b/Corona-Warn-App/src/main/res/menu/menu_bottom_nav.xml index 3c598525006aed7ca597de2bd57c5cc3a1b55016..bef4a123a6e4f1390db724399037facf57931f01 100644 --- a/Corona-Warn-App/src/main/res/menu/menu_bottom_nav.xml +++ b/Corona-Warn-App/src/main/res/menu/menu_bottom_nav.xml @@ -6,6 +6,11 @@ android:icon="@drawable/ic_nav_home" android:title="@string/bottom_nav_home_title" /> + <item + android:id="@+id/trace_location_attendee_nav_graph" + android:icon="@drawable/ic_nav_check_in" + android:title="@string/bottom_nav_check_ins_title" /> + <item android:id="@+id/contact_diary_nav_graph" android:icon="@drawable/ic_nav_diary" diff --git a/Corona-Warn-App/src/main/res/navigation/nav_graph.xml b/Corona-Warn-App/src/main/res/navigation/nav_graph.xml index 3f864197f4d746404191f1e46ff49d39f8d020f4..cc6efcc8ebfc90c536d5222489f7c5cd8e9ba67c 100644 --- a/Corona-Warn-App/src/main/res/navigation/nav_graph.xml +++ b/Corona-Warn-App/src/main/res/navigation/nav_graph.xml @@ -9,6 +9,9 @@ <include app:graph="@navigation/test_nav_graph" /> <!-- Contact Diary graph--> <include app:graph="@navigation/contact_diary_nav_graph" /> + <!-- Event registration graphs--> + <include app:graph="@navigation/trace_location_organizer_nav_graph" /> + <include app:graph="@navigation/trace_location_attendee_nav_graph" /> <!-- Main --> <fragment @@ -545,8 +548,7 @@ android:id="@+id/surveyConsentDetailFragment" android:name="de.rki.coronawarnapp.datadonation.survey.consent.SurveyConsentDetailFragment" android:label="survey_consent_detail_fragment" - tools:layout="@layout/survey_consent_detail_fragment"> - </fragment> + tools:layout="@layout/survey_consent_detail_fragment" /> <fragment android:id="@+id/analyticsUserInputFragment" android:name="de.rki.coronawarnapp.datadonation.analytics.ui.input.AnalyticsUserInputFragment" @@ -572,8 +574,7 @@ android:id="@+id/ppaMoreInfoFragment" android:name="de.rki.coronawarnapp.datadonation.analytics.ui.PpaMoreInfoFragment" android:label="PpaMoreInfoFragment" - tools:layout="@layout/fragment_ppa_more_info"> - </fragment> + tools:layout="@layout/fragment_ppa_more_info" /> <fragment android:id="@+id/settingsPrivacyPreservingAnalyticsFragment" android:name="de.rki.coronawarnapp.ui.settings.analytics.SettingsPrivacyPreservingAnalyticsFragment" @@ -595,7 +596,7 @@ android:id="@+id/debugLogUploadFragment" android:name="de.rki.coronawarnapp.bugreporting.debuglog.ui.upload.DebugLogUploadFragment" android:label="DebugLogUploadFragment" - tools:layout="@layout/bugreporting_debuglog_upload_fragment" > + tools:layout="@layout/bugreporting_debuglog_upload_fragment"> <action android:id="@+id/action_debugLogUploadFragment_to_debugLogLegalFragment" app:destination="@id/debugLogLegalFragment" /> diff --git a/Corona-Warn-App/src/main/res/navigation/trace_location_attendee_nav_graph.xml b/Corona-Warn-App/src/main/res/navigation/trace_location_attendee_nav_graph.xml new file mode 100644 index 0000000000000000000000000000000000000000..062c8064c0103be7b5f6b29c49ba31bcd12702f3 --- /dev/null +++ b/Corona-Warn-App/src/main/res/navigation/trace_location_attendee_nav_graph.xml @@ -0,0 +1,41 @@ +<?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/trace_location_attendee_nav_graph" + app:startDestination="@id/checkInsFragment"> + <fragment + android:id="@+id/confirmCheckInFragment" + android:name="de.rki.coronawarnapp.ui.eventregistration.attendee.confirm.ConfirmCheckInFragment" + android:label="fragment_confirm_check_in" + tools:layout="@layout/fragment_confirm_check_in"> + <argument + android:name="traceLocation" + app:argType="de.rki.coronawarnapp.ui.eventregistration.attendee.checkin.VerifiedTraceLocation" /> + </fragment> + <fragment + android:id="@+id/scanCheckInQrCodeFragment" + android:name="de.rki.coronawarnapp.ui.eventregistration.attendee.scan.ScanCheckInQrCodeFragment" + android:label="ScanCheckInQrCodeFragment" + tools:layout="@layout/fragment_scan_check_in_qr_code" /> + + <fragment + android:id="@+id/checkInsFragment" + android:name="de.rki.coronawarnapp.ui.eventregistration.attendee.checkin.CheckInsFragment" + android:label="CheckInsFragment" + tools:layout="@layout/fragment_check_ins"> + <deepLink app:uri="coronawarnapp://check-ins/{uri}" /> + <action + android:id="@+id/action_checkInsFragment_to_scanCheckInQrCodeFragment" + app:destination="@id/scanCheckInQrCodeFragment" /> + <action + android:id="@+id/action_checkInsFragment_to_confirmCheckInFragment" + app:destination="@id/confirmCheckInFragment" /> + <argument + android:name="uri" + android:defaultValue="@null" + app:argType="string" + app:nullable="true" /> + </fragment> + +</navigation> \ 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 new file mode 100644 index 0000000000000000000000000000000000000000..35744089a2654cb434804ecdceb6fc1bb55090bb --- /dev/null +++ b/Corona-Warn-App/src/main/res/navigation/trace_location_organizer_nav_graph.xml @@ -0,0 +1,6 @@ +<?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" + android:id="@+id/event_organizer_nav_graph"> + <!-- TODO add organiser screens --> +</navigation> \ No newline at end of file diff --git a/Corona-Warn-App/src/main/res/values-de/strings.xml b/Corona-Warn-App/src/main/res/values-de/strings.xml index 6e6d5106ce8aac361f155b78f0b1aa73f19296d9..2285da38b5227a2efaafe21ad36783185e3e2148 100644 --- a/Corona-Warn-App/src/main/res/values-de/strings.xml +++ b/Corona-Warn-App/src/main/res/values-de/strings.xml @@ -1802,6 +1802,8 @@ <string name="bottom_nav_home_title">Startseite</string> <!-- XHED: Title for BottomNav diary screen title --> <string name="bottom_nav_diary_title">Tagebuch</string> + <!-- XHED: Title for BottomNav check-in screen title --> + <string name="bottom_nav_check_ins_title">Check In</string> <!-- #################################### Data Donation & Survey @@ -1919,4 +1921,8 @@ <string name="duration_dialog_ok_button">OK</string> <!-- NOTR --> <string name="duration_dialog_default_value">00:00</string> + + <!-- Scan check in QR Code--> + <!-- XTXT: Scan check in QR-Code FAB text--> + <string name="scan_check_in_qr_code">QR-Code scannen</string> </resources> diff --git a/Corona-Warn-App/src/main/res/values/strings.xml b/Corona-Warn-App/src/main/res/values/strings.xml index 38eb2464b2fe251d64f1b1f0568a845cb2392325..75d293fde8e1638f48c051b11733c1cbbc02263a 100644 --- a/Corona-Warn-App/src/main/res/values/strings.xml +++ b/Corona-Warn-App/src/main/res/values/strings.xml @@ -1811,6 +1811,8 @@ <string name="bottom_nav_home_title">"Start Screen"</string> <!-- XHED: Title for BottomNav diary screen title --> <string name="bottom_nav_diary_title">"Journal"</string> + <!-- XHED: Title for BottomNav check-in screen title --> + <string name="bottom_nav_check_ins_title">Check In</string> <!-- #################################### Data Donation & Survey @@ -1928,4 +1930,8 @@ <string name="duration_dialog_ok_button">"OK"</string> <!-- NOTR --> <string name="duration_dialog_default_value">"00:00"</string> + + <!-- Scan check in QR Code--> + <!-- XTXT: Scan check in QR-Code FAB text--> + <string name="scan_check_in_qr_code">Scan QR-Code</string> </resources> diff --git a/Corona-Warn-App/src/main/res/xml/provider_paths.xml b/Corona-Warn-App/src/main/res/xml/provider_paths.xml index cc1e648e7f702573ec5da2f64b42a4ed4a409fb0..104e4132425efe495666ece2264a7b4e24aa5dc4 100644 --- a/Corona-Warn-App/src/main/res/xml/provider_paths.xml +++ b/Corona-Warn-App/src/main/res/xml/provider_paths.xml @@ -6,4 +6,7 @@ <cache-path name="shared_logs" path="debuglog/shared/" /> + <files-path + name="Events" + path="events/" /> </paths> \ No newline at end of file diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/Base32Test.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/Base32Test.kt new file mode 100644 index 0000000000000000000000000000000000000000..674f4b7654f169f9c8ab7f44a74b914c9b958768 --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/Base32Test.kt @@ -0,0 +1,50 @@ +package de.rki.coronawarnapp.eventregistration + +import de.rki.coronawarnapp.eventregistration.common.base32 +import de.rki.coronawarnapp.eventregistration.common.decodeBase32 +import io.kotest.matchers.shouldBe +import okio.ByteString.Companion.toByteString +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.ArgumentsSource +import testhelpers.BaseTest + +class Base32Test : BaseTest() { + + @ParameterizedTest + @ArgumentsSource(NoPaddingTestProvider::class) + fun `Encoding with base32NoPadding`(input: String, expected: String) { + input.base32(padding = false) shouldBe expected + } + + @ParameterizedTest + @ArgumentsSource(WithPaddingTestProvider::class) + fun `Encoding with base32WithPadding`(input: String, expected: String) { + input.base32() shouldBe expected + } + + @ParameterizedTest + @ArgumentsSource(NoPaddingTestProvider::class) + fun `Encoding ByteString with base32NoPadding`(input: String, expected: String) { + val byteString = input.toByteArray().toByteString() + byteString.base32(padding = false) shouldBe expected + } + + @ParameterizedTest + @ArgumentsSource(WithPaddingTestProvider::class) + fun `Encoding ByteString with base32WithPadding`(input: String, expected: String) { + val byteString = input.toByteArray().toByteString() + byteString.base32() shouldBe expected + } + + @ParameterizedTest + @ArgumentsSource(WithPaddingTestProvider::class) + fun `Decoding with base32WithPadding ByteString`(expected: String, input: String) { + input.decodeBase32().string(Charsets.UTF_8) shouldBe expected + } + + @ParameterizedTest + @ArgumentsSource(NoPaddingTestProvider::class) + fun `Decoding with base32NoPadding ByteString`(expected: String, input: String) { + input.decodeBase32().string(Charsets.UTF_8) shouldBe expected + } +} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/NoPaddingTestProvider.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/NoPaddingTestProvider.kt new file mode 100644 index 0000000000000000000000000000000000000000..69f2287a9c2b998162cac5e059fcf592345341e4 --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/NoPaddingTestProvider.kt @@ -0,0 +1,33 @@ +package de.rki.coronawarnapp.eventregistration + +import org.junit.jupiter.api.extension.ExtensionContext +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.ArgumentsProvider +import java.util.stream.Stream + +class NoPaddingTestProvider : ArgumentsProvider { + override fun provideArguments(context: ExtensionContext?): Stream<out Arguments> { + return Stream.of( + Arguments.of( + "the quick brown fox jumps over the lazy dog", + "ORUGKIDROVUWG2ZAMJZG653OEBTG66BANJ2W24DTEBXXMZLSEB2GQZJANRQXU6JAMRXWO" + ), + Arguments.of( + "THE QUICK BROWN FOX JUMPS OVER THE LAZY DOG", + "KREEKICRKVEUGSZAIJJE6V2OEBDE6WBAJJKU2UCTEBHVMRKSEBKEQRJAJRAVUWJAIRHUO" + ), + Arguments.of( + "die heiße zypernsonne quälte max und victoria ja böse auf dem weg bis zur küste.", + "MRUWKIDIMVU4HH3FEB5HS4DFOJXHG33ONZSSA4LVYOSGY5DFEBWWC6BAOVXGIIDWNFRXI33SNFQSA2TBEBRMHNTTMUQGC5LGEBSGK3JAO5SWOIDCNFZSA6TVOIQGXQ54ON2GKLQ" + ), + Arguments.of( + "DIE HEISSE ZYPERNSONNE QUÄLTE MAX UND VICTORIA JA BÖSE AUF DEM WEG BIS ZUR KÜSTE.", + "IREUKICIIVEVGU2FEBNFSUCFKJHFGT2OJZCSAUKVYOCEYVCFEBGUCWBAKVHEIICWJFBVIT2SJFASASSBEBBMHFSTIUQECVKGEBCEKTJAK5CUOICCJFJSAWSVKIQEXQ44KNKEKLQ" + ), + Arguments.of( + "Hello World!", + "JBSWY3DPEBLW64TMMQQQ" + ), + ) + } +} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/WithPaddingTestProvider.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/WithPaddingTestProvider.kt new file mode 100644 index 0000000000000000000000000000000000000000..c54ed7c290fc4c4267f12ff54535f2603b1f473c --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/WithPaddingTestProvider.kt @@ -0,0 +1,33 @@ +package de.rki.coronawarnapp.eventregistration + +import org.junit.jupiter.api.extension.ExtensionContext +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.ArgumentsProvider +import java.util.stream.Stream + +class WithPaddingTestProvider : ArgumentsProvider { + override fun provideArguments(context: ExtensionContext?): Stream<out Arguments> { + return Stream.of( + Arguments.of( + "the quick brown fox jumps over the lazy dog", + "ORUGKIDROVUWG2ZAMJZG653OEBTG66BANJ2W24DTEBXXMZLSEB2GQZJANRQXU6JAMRXWO===" + ), + Arguments.of( + "THE QUICK BROWN FOX JUMPS OVER THE LAZY DOG", + "KREEKICRKVEUGSZAIJJE6V2OEBDE6WBAJJKU2UCTEBHVMRKSEBKEQRJAJRAVUWJAIRHUO===" + ), + Arguments.of( + "die heiße zypernsonne quälte max und victoria ja böse auf dem weg bis zur küste.", + "MRUWKIDIMVU4HH3FEB5HS4DFOJXHG33ONZSSA4LVYOSGY5DFEBWWC6BAOVXGIIDWNFRXI33SNFQSA2TBEBRMHNTTMUQGC5LGEBSGK3JAO5SWOIDCNFZSA6TVOIQGXQ54ON2GKLQ=" + ), + Arguments.of( + "DIE HEISSE ZYPERNSONNE QUÄLTE MAX UND VICTORIA JA BÖSE AUF DEM WEG BIS ZUR KÜSTE.", + "IREUKICIIVEVGU2FEBNFSUCFKJHFGT2OJZCSAUKVYOCEYVCFEBGUCWBAKVHEIICWJFBVIT2SJFASASSBEBBMHFSTIUQECVKGEBCEKTJAK5CUOICCJFJSAWSVKIQEXQ44KNKEKLQ=" + ), + Arguments.of( + "Hello World!", + "JBSWY3DPEBLW64TMMQQQ====" + ), + ) + } +} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/attendee/confirm/ConfirmCheckInViewModelTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/attendee/confirm/ConfirmCheckInViewModelTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..1bb9410f2eeb38a6b5308ec6d19b52b06e144fd1 --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/attendee/confirm/ConfirmCheckInViewModelTest.kt @@ -0,0 +1,35 @@ +package de.rki.coronawarnapp.eventregistration.attendee.confirm + +import de.rki.coronawarnapp.ui.eventregistration.attendee.confirm.ConfirmCheckInNavigation +import de.rki.coronawarnapp.ui.eventregistration.attendee.confirm.ConfirmCheckInViewModel +import io.kotest.matchers.shouldBe +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test + +import org.junit.jupiter.api.extension.ExtendWith +import testhelpers.BaseTest +import testhelpers.extensions.InstantExecutorExtension +import testhelpers.extensions.getOrAwaitValue + +@ExtendWith(InstantExecutorExtension::class) +class ConfirmCheckInViewModelTest : BaseTest() { + + private lateinit var viewModel: ConfirmCheckInViewModel + + @BeforeEach + fun setUp() { + viewModel = ConfirmCheckInViewModel() + } + + @Test + fun onClose() { + viewModel.onClose() + viewModel.events.getOrAwaitValue() shouldBe ConfirmCheckInNavigation.BackNavigation + } + + @Test + fun onConfirmEvent() { + viewModel.onConfirmTraceLocation() + viewModel.events.getOrAwaitValue() shouldBe ConfirmCheckInNavigation.ConfirmNavigation + } +} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/InvalidUrlProvider.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/InvalidUrlProvider.kt new file mode 100644 index 0000000000000000000000000000000000000000..62e33419136ee48800c5d1ecd84315d289b4ed36 --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/InvalidUrlProvider.kt @@ -0,0 +1,32 @@ +package de.rki.coronawarnapp.eventregistration.checkins.qrcode + +import org.junit.jupiter.api.extension.ExtensionContext +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.ArgumentsProvider +import java.util.stream.Stream + +class InvalidUrlProvider : ArgumentsProvider { + override fun provideArguments(context: ExtensionContext?): Stream<out Arguments> { + return Stream.of( + Arguments.of( + "https://e.coronawarn.app/e1/BIYAUEDBZY6EIWF7QX6JOKSRPAGEB3H7CIIEGV2BEBGGC5LOMNUCAUDBO" + + "J2HSGGTQ6SACIHXQ6SACKA6CJEDARQCEEAPHGEZ5JI2K2T422L5U3SMZY5DGCPUZ2RQACAYEJ3HQYMAFFBU2" + + "SQCEEAJAUCJSQJ7WDM675MCMOD3L2UL7ECJU7TYERH23B746RQTABO3CTI=" + ), + Arguments.of( + "https://e.coronawarn.app/c1/SINGED_ENCODED_LOCATION" + ), + Arguments.of( + "HTTPS://E.CORONAWARN.APP/C1/BIPEY33SMVWSA2LQON2W2IDEN5WG64RAONUXIIDBNVSXILBAMNXRBCM4UQARRKM6UQASAHR" + + "KCC7CTDWGQ4JCO7RVZSWVIMQK4UPA.GBCAEIA7TEORBTUA25QHBOCWT26BCA5PORBS2E4FFWMJ3UU3P6SXOL" + + "7SHUBCA7UEZBDDQ2R6VRJH7WBJKVF7GZYJA6YMRN27IPEP7NKGGJSWX3XQ" + ), + Arguments.of( + "HTTPS://E.CORONAWARN.APP/C1/BIYAUEDBZY6EIWF7QX6JOKSRPAGEB3H7CIIEGV2BEBGGC5LOMNUCAUDBO" + + "J2HSGGTQ6SACIHXQ6SACKA6CJEDARQCEEAPHGEZ5JI2K2T422L5U3SMZY5DGCPUZ2RQACAYEJ3HQYMAFFBU2" + + "SQCEEAJAUCJSQJ7WDM675MCMOD3L2UL7ECJU7TYERH23B746RQTABO3CTI" + ), + Arguments.of("") + ) + } +} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/UriValidatorTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/UriValidatorTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..d0f30a69b74d3aba0aa7db9aae6fc6ebdb95de49 --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/UriValidatorTest.kt @@ -0,0 +1,29 @@ +package de.rki.coronawarnapp.eventregistration.checkins.qrcode + +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.shouldBe +import org.junit.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.ArgumentsSource + +class UriValidatorTest { + + @ParameterizedTest + @ArgumentsSource(ValidUrlProvider::class) + fun `Valid URLs`(input: String) { + input.isValidQRCodeUri() shouldBe true + } + + @ParameterizedTest + @ArgumentsSource(InvalidUrlProvider::class) + fun `Invalid URLs`(input: String) { + input.isValidQRCodeUri() shouldBe false + } + + @Test + fun `Invalid URL string`() { + shouldThrow<IllegalArgumentException> { + "Hello World!".isValidQRCodeUri() + } + } +} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/ValidUrlProvider.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/ValidUrlProvider.kt new file mode 100644 index 0000000000000000000000000000000000000000..6b8b83fbe79725ba9382671da81673ba7dd729c6 --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/ValidUrlProvider.kt @@ -0,0 +1,23 @@ +package de.rki.coronawarnapp.eventregistration.checkins.qrcode + +import org.junit.jupiter.api.extension.ExtensionContext +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.ArgumentsProvider +import java.util.stream.Stream + +class ValidUrlProvider : ArgumentsProvider { + override fun provideArguments(context: ExtensionContext?): Stream<out Arguments> { + return Stream.of( + Arguments.of( + "HTTPS://E.CORONAWARN.APP/C1/BIYAUEDBZY6EIWF7QX6JOKSRPAGEB3H7CIIEGV2BEBGGC5LOMNUCAUDBO" + + "J2HSGGTQ6SACIHXQ6SACKA6CJEDARQCEEAPHGEZ5JI2K2T422L5U3SMZY5DGCPUZ2RQACAYEJ3HQYMAFFBU2" + + "SQCEEAJAUCJSQJ7WDM675MCMOD3L2UL7ECJU7TYERH23B746RQTABO3CTI=" + ), + Arguments.of( + "https://e.coronawarn.app/c1/BIYAUEDBZY6EIWF7QX6JOKSRPAGEB3H7CIIEGV2BEBGGC5LOMNUCAUDBO" + + "J2HSGGTQ6SACIHXQ6SACKA6CJEDARQCEEAPHGEZ5JI2K2T422L5U3SMZY5DGCPUZ2RQACAYEJ3HQYMAFFBU2" + + "SQCEEAJAUCJSQJ7WDM675MCMOD3L2UL7ECJU7TYERH23B746RQTABO3CTI=" + ) + ) + } +} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/events/DefaultTraceLocationKtTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/events/DefaultTraceLocationKtTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..7aefa48ae7e14c6714a058884d6b3c1747d9cb24 --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/events/DefaultTraceLocationKtTest.kt @@ -0,0 +1,111 @@ +package de.rki.coronawarnapp.eventregistration.events + +import de.rki.coronawarnapp.eventregistration.storage.entity.TraceLocationEntity +import io.kotest.matchers.shouldBe +import org.joda.time.Instant +import org.junit.jupiter.api.Test +import testhelpers.BaseTest + +internal class DefaultTraceLocationKtTest : BaseTest() { + + @Test + fun `toTraceLocation() should map to correct object when providing all arguments`() { + TraceLocationEntity( + guid = "TestGuid", + version = 1, + type = TraceLocation.Type.TEMPORARY_OTHER, + description = "TestTraceLocation", + address = "TestTraceLocationAddress", + startDate = Instant.parse("2021-01-01T12:00:00.000Z"), + endDate = Instant.parse("2021-01-01T18:00:00.000Z"), + defaultCheckInLengthInMinutes = 15, + signature = "signature" + ).toTraceLocation() shouldBe DefaultTraceLocation( + guid = "TestGuid", + version = 1, + type = TraceLocation.Type.TEMPORARY_OTHER, + description = "TestTraceLocation", + address = "TestTraceLocationAddress", + startDate = Instant.parse("2021-01-01T12:00:00.000Z"), + endDate = Instant.parse("2021-01-01T18:00:00.000Z"), + defaultCheckInLengthInMinutes = 15, + signature = "signature" + ) + } + + @Test + fun `toTraceLocation() should map to correct object when providing only arguments that are required`() { + TraceLocationEntity( + guid = "TestGuid", + version = 1, + type = TraceLocation.Type.PERMANENT_OTHER, + description = "TestTraceLocation", + address = "TestTraceLocationAddress", + startDate = null, + endDate = null, + defaultCheckInLengthInMinutes = null, + signature = "signature" + ).toTraceLocation() shouldBe DefaultTraceLocation( + guid = "TestGuid", + version = 1, + type = TraceLocation.Type.PERMANENT_OTHER, + description = "TestTraceLocation", + address = "TestTraceLocationAddress", + startDate = null, + endDate = null, + defaultCheckInLengthInMinutes = null, + signature = "signature" + ) + } + + @Test + fun `toTraceLocations() should map a list of TraceLocationEntities correctly`() { + listOf( + TraceLocationEntity( + guid = "TestGuid1", + version = 1, + type = TraceLocation.Type.TEMPORARY_OTHER, + description = "TestTraceLocation1", + address = "TestTraceLocationAddress1", + startDate = Instant.parse("2021-01-01T12:00:00.000Z"), + endDate = Instant.parse("2021-01-01T18:00:00.000Z"), + defaultCheckInLengthInMinutes = 15, + signature = "signature" + ), + TraceLocationEntity( + guid = "TestGuid2", + version = 1, + type = TraceLocation.Type.PERMANENT_OTHER, + description = "TestTraceLocation2", + address = "TestTraceLocationAddress2", + startDate = null, + endDate = null, + defaultCheckInLengthInMinutes = null, + signature = "signature" + ) + ).toTraceLocations() shouldBe listOf( + DefaultTraceLocation( + guid = "TestGuid1", + version = 1, + type = TraceLocation.Type.TEMPORARY_OTHER, + description = "TestTraceLocation1", + address = "TestTraceLocationAddress1", + startDate = Instant.parse("2021-01-01T12:00:00.000Z"), + endDate = Instant.parse("2021-01-01T18:00:00.000Z"), + defaultCheckInLengthInMinutes = 15, + signature = "signature" + ), + DefaultTraceLocation( + guid = "TestGuid2", + version = 1, + type = TraceLocation.Type.PERMANENT_OTHER, + description = "TestTraceLocation2", + address = "TestTraceLocationAddress2", + startDate = null, + endDate = null, + defaultCheckInLengthInMinutes = null, + signature = "signature" + ) + ) + } +} \ No newline at end of file diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/storage/entity/TraceLocationConvertersTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/storage/entity/TraceLocationConvertersTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..b11ff01460ae53187c857905f50fe49f7e239a0c --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/storage/entity/TraceLocationConvertersTest.kt @@ -0,0 +1,29 @@ +package de.rki.coronawarnapp.eventregistration.storage.entity + +import de.rki.coronawarnapp.eventregistration.events.TraceLocation +import io.kotest.matchers.shouldBe +import org.junit.jupiter.api.Test +import testhelpers.BaseTest + +internal class TraceLocationConvertersTest : BaseTest() { + + private val converter = TraceLocationConverters() + + @Test + fun `toTraceLocationType() should convert different integer values to correct TraceLocation Types`() { + with(converter) { + toTraceLocationType(0) shouldBe TraceLocation.Type.UNSPECIFIED + toTraceLocationType(1) shouldBe TraceLocation.Type.PERMANENT_OTHER + toTraceLocationType(2) shouldBe TraceLocation.Type.TEMPORARY_OTHER + } + } + + @Test + fun `fromTraceLocationType() should convert TraceLocation Types to correct integer values`() { + with(converter) { + fromTraceLocationType(TraceLocation.Type.UNSPECIFIED) shouldBe 0 + fromTraceLocationType(TraceLocation.Type.PERMANENT_OTHER) shouldBe 1 + fromTraceLocationType(TraceLocation.Type.TEMPORARY_OTHER) shouldBe 2 + } + } +} \ No newline at end of file diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/storage/entity/TraceLocationEntityTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/storage/entity/TraceLocationEntityTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..e796c1838e3def3d812cdad83e767645c88b016b --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/storage/entity/TraceLocationEntityTest.kt @@ -0,0 +1,61 @@ +package de.rki.coronawarnapp.eventregistration.storage.entity + +import de.rki.coronawarnapp.eventregistration.events.DefaultTraceLocation +import de.rki.coronawarnapp.eventregistration.events.TraceLocation +import io.kotest.matchers.shouldBe +import org.joda.time.Instant +import org.junit.jupiter.api.Test +import testhelpers.BaseTest + +internal class TraceLocationEntityTest : BaseTest() { + + @Test + fun `toTraceLocationEntity() should map to TraceLocationEntity correctly with all arguments`() { + DefaultTraceLocation( + guid = "TestGuid", + version = 1, + type = TraceLocation.Type.TEMPORARY_OTHER, + description = "TestTraceLocation", + address = "TestTraceLocationAddress", + startDate = Instant.parse("2021-01-01T12:00:00.000Z"), + endDate = Instant.parse("2021-01-01T18:00:00.000Z"), + defaultCheckInLengthInMinutes = 15, + signature = "signature" + ).toTraceLocationEntity() shouldBe TraceLocationEntity( + guid = "TestGuid", + version = 1, + type = TraceLocation.Type.TEMPORARY_OTHER, + description = "TestTraceLocation", + address = "TestTraceLocationAddress", + startDate = Instant.parse("2021-01-01T12:00:00.000Z"), + endDate = Instant.parse("2021-01-01T18:00:00.000Z"), + defaultCheckInLengthInMinutes = 15, + signature = "signature" + ) + } + + @Test + fun `toTraceLocationEntity() should map to TraceLocationEntity correctly with some arguments as null`() { + DefaultTraceLocation( + guid = "TestGuid", + version = 1, + type = TraceLocation.Type.PERMANENT_OTHER, + description = "TestTraceLocation", + address = "TestTraceLocationAddress", + startDate = null, + endDate = null, + defaultCheckInLengthInMinutes = null, + signature = "signature" + ).toTraceLocationEntity() shouldBe TraceLocationEntity( + guid = "TestGuid", + version = 1, + type = TraceLocation.Type.PERMANENT_OTHER, + description = "TestTraceLocation", + address = "TestTraceLocationAddress", + startDate = null, + endDate = null, + defaultCheckInLengthInMinutes = null, + signature = "signature" + ) + } +} \ No newline at end of file diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/eventregistration/attendee/scan/ScanCheckInQrCodeViewModelTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/eventregistration/attendee/scan/ScanCheckInQrCodeViewModelTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..2dae524bbe0ed017588eb4f6de36e86b845573f7 --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/eventregistration/attendee/scan/ScanCheckInQrCodeViewModelTest.kt @@ -0,0 +1,42 @@ +package de.rki.coronawarnapp.ui.eventregistration.attendee.scan + +import com.google.zxing.Result +import com.journeyapps.barcodescanner.BarcodeResult +import io.kotest.matchers.shouldBe +import io.mockk.every +import io.mockk.mockk +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import testhelpers.BaseTest +import testhelpers.extensions.InstantExecutorExtension +import testhelpers.extensions.getOrAwaitValue + +@ExtendWith(InstantExecutorExtension::class) +class ScanCheckInQrCodeViewModelTest : BaseTest() { + + private lateinit var viewModel: ScanCheckInQrCodeViewModel + + @BeforeEach + fun setup() { + viewModel = ScanCheckInQrCodeViewModel() + } + + @Test + fun `onNavigateUp goes back`() { + viewModel.onNavigateUp() + viewModel.events.getOrAwaitValue() shouldBe ScanCheckInQrCodeNavigation.BackNavigation + } + + @Test + fun `onScanResult results in navigation url`() { + val mockedResult = mockk<BarcodeResult>().apply { + every { result } returns mockk<Result>().apply { + every { text } returns "https://coronawarn.app/E1/SOME_PATH_GOES_HERE" + } + } + viewModel.onScanResult(mockedResult) + viewModel.events.getOrAwaitValue() shouldBe + ScanCheckInQrCodeNavigation.ScanResultNavigation("https://coronawarn.app/E1/SOME_PATH_GOES_HERE") + } +} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/eventregistration/checkin/ConfirmCheckInViewModelTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/eventregistration/checkin/ConfirmCheckInViewModelTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391