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/bugreporting/BugReportingSharedModule.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/BugReportingSharedModule.kt index 70cf264d396708e97412548538bd50aafa167d42..a090b59fadb6860df156d971cc1056565ec12163 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/BugReportingSharedModule.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/BugReportingSharedModule.kt @@ -3,9 +3,12 @@ package de.rki.coronawarnapp.bugreporting import dagger.Module import dagger.Provides import dagger.Reusable +import dagger.multibindings.IntoSet import de.rki.coronawarnapp.bugreporting.censors.BugCensor +import de.rki.coronawarnapp.bugreporting.censors.DiaryEncounterCensor import de.rki.coronawarnapp.bugreporting.censors.DiaryLocationCensor import de.rki.coronawarnapp.bugreporting.censors.DiaryPersonCensor +import de.rki.coronawarnapp.bugreporting.censors.DiaryVisitCensor import de.rki.coronawarnapp.bugreporting.censors.QRCodeCensor import de.rki.coronawarnapp.bugreporting.censors.RegistrationTokenCensor import de.rki.coronawarnapp.bugreporting.debuglog.internal.DebugLoggerScope @@ -22,7 +25,6 @@ import okhttp3.OkHttpClient import retrofit2.Retrofit import retrofit2.converter.gson.GsonConverterFactory import retrofit2.converter.protobuf.ProtoConverterFactory -import timber.log.Timber import javax.inject.Singleton @Module @@ -67,19 +69,27 @@ class BugReportingSharedModule { @Provides fun scope(): CoroutineScope = DebugLoggerScope - @Singleton @Provides - fun censors( - registrationTokenCensor: RegistrationTokenCensor, - diaryPersonCensor: DiaryPersonCensor, - diaryLocationCensor: DiaryLocationCensor, - qrCodeCensor: QRCodeCensor - ): List<BugCensor> = listOf( - registrationTokenCensor, - diaryPersonCensor, - diaryLocationCensor, - qrCodeCensor - ).also { - Timber.d("Loaded BugCensors: %s", it) - } + @IntoSet + fun registrationTokenCensor(censor: RegistrationTokenCensor): BugCensor = censor + + @Provides + @IntoSet + fun qrCodeCensor(censor: QRCodeCensor): BugCensor = censor + + @Provides + @IntoSet + fun diaryPersonCensor(censor: DiaryPersonCensor): BugCensor = censor + + @Provides + @IntoSet + fun diaryEncounterCensor(censor: DiaryEncounterCensor): BugCensor = censor + + @Provides + @IntoSet + fun diaryLocationCensor(censor: DiaryLocationCensor): BugCensor = censor + + @Provides + @IntoSet + fun diaryVisitCensor(censor: DiaryVisitCensor): BugCensor = censor } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/censors/DiaryEncounterCensor.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/censors/DiaryEncounterCensor.kt new file mode 100644 index 0000000000000000000000000000000000000000..e8d6c96a8e23a2132a3f3f86d13ace255cfc1a9b --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/censors/DiaryEncounterCensor.kt @@ -0,0 +1,41 @@ +package de.rki.coronawarnapp.bugreporting.censors + +import dagger.Reusable +import de.rki.coronawarnapp.bugreporting.debuglog.LogLine +import de.rki.coronawarnapp.bugreporting.debuglog.internal.DebuggerScope +import de.rki.coronawarnapp.contactdiary.storage.repo.ContactDiaryRepository +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.stateIn +import javax.inject.Inject + +@Reusable +class DiaryEncounterCensor @Inject constructor( + @DebuggerScope debugScope: CoroutineScope, + diary: ContactDiaryRepository +) : BugCensor { + + private val encounters by lazy { + diary.personEncounters.stateIn( + scope = debugScope, + started = SharingStarted.Lazily, + initialValue = null + ).filterNotNull() + } + + override suspend fun checkLog(entry: LogLine): LogLine? { + val encountersNow = encounters.first().filter { !it.circumstances.isNullOrBlank() } + + if (encountersNow.isEmpty()) return null + + val newMessage = encountersNow.fold(entry.message) { orig, encounter -> + if (encounter.circumstances.isNullOrBlank()) return@fold orig + + orig.replace(encounter.circumstances!!, "Encounter#${encounter.id}/Circumstances") + } + + return entry.copy(message = newMessage) + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/censors/DiaryLocationCensor.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/censors/DiaryLocationCensor.kt index 61dc4cc8d637eb73fcb54388058daf3eab9ce3fe..acf70ef4c5dedf146165338d4c188d82488f51e2 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/censors/DiaryLocationCensor.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/censors/DiaryLocationCensor.kt @@ -4,7 +4,6 @@ import dagger.Reusable import de.rki.coronawarnapp.bugreporting.debuglog.LogLine import de.rki.coronawarnapp.bugreporting.debuglog.internal.DebuggerScope import de.rki.coronawarnapp.contactdiary.storage.repo.ContactDiaryRepository -import de.rki.coronawarnapp.util.CWADebug import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.filterNotNull @@ -31,12 +30,18 @@ class DiaryLocationCensor @Inject constructor( if (locationsNow.isEmpty()) return null - var newMessage = locationsNow.fold(entry.message) { oldMsg, location -> - oldMsg.replace(location.locationName, "Location#${location.locationId}") - } + val newMessage = locationsNow.fold(entry.message) { orig, location -> + var wip = orig.replace(location.locationName, "Location#${location.locationId}/Name") + + location.emailAddress?.let { + wip = wip.replace(it, "Location#${location.locationId}/EMail") + } + + location.phoneNumber?.let { + wip = wip.replace(it, "Location#${location.locationId}/PhoneNumber") + } - if (CWADebug.isDeviceForTestersBuild) { - newMessage = entry.message + wip } return entry.copy(message = newMessage) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/censors/DiaryPersonCensor.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/censors/DiaryPersonCensor.kt index 591c8db2cfc412e096e96393ac74ddc6e6669650..6f56e4bfe4d236a0145edd02ba54f7ddafa8a528 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/censors/DiaryPersonCensor.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/censors/DiaryPersonCensor.kt @@ -4,7 +4,6 @@ import dagger.Reusable import de.rki.coronawarnapp.bugreporting.debuglog.LogLine import de.rki.coronawarnapp.bugreporting.debuglog.internal.DebuggerScope import de.rki.coronawarnapp.contactdiary.storage.repo.ContactDiaryRepository -import de.rki.coronawarnapp.util.CWADebug import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.filterNotNull @@ -31,12 +30,18 @@ class DiaryPersonCensor @Inject constructor( if (personsNow.isEmpty()) return null - var newMessage = personsNow.fold(entry.message) { oldMsg, person -> - oldMsg.replace(person.fullName, "Person#${person.personId}") - } + val newMessage = personsNow.fold(entry.message) { orig, person -> + var wip = orig.replace(person.fullName, "Person#${person.personId}/Name") + + person.emailAddress?.let { + wip = wip.replace(it, "Person#${person.personId}/EMail") + } + + person.phoneNumber?.let { + wip = wip.replace(it, "Person#${person.personId}/PhoneNumber") + } - if (CWADebug.isDeviceForTestersBuild) { - newMessage = entry.message + wip } return entry.copy(message = newMessage) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/censors/DiaryVisitCensor.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/censors/DiaryVisitCensor.kt new file mode 100644 index 0000000000000000000000000000000000000000..266eff75ec6eb9c175fa2c55a6727a25bb87bccf --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/censors/DiaryVisitCensor.kt @@ -0,0 +1,41 @@ +package de.rki.coronawarnapp.bugreporting.censors + +import dagger.Reusable +import de.rki.coronawarnapp.bugreporting.debuglog.LogLine +import de.rki.coronawarnapp.bugreporting.debuglog.internal.DebuggerScope +import de.rki.coronawarnapp.contactdiary.storage.repo.ContactDiaryRepository +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.stateIn +import javax.inject.Inject + +@Reusable +class DiaryVisitCensor @Inject constructor( + @DebuggerScope debugScope: CoroutineScope, + diary: ContactDiaryRepository +) : BugCensor { + + private val visits by lazy { + diary.locationVisits.stateIn( + scope = debugScope, + started = SharingStarted.Lazily, + initialValue = null + ).filterNotNull() + } + + override suspend fun checkLog(entry: LogLine): LogLine? { + val visitsNow = visits.first().filter { !it.circumstances.isNullOrBlank() } + + if (visitsNow.isEmpty()) return null + + val newMessage = visitsNow.fold(entry.message) { orig, visit -> + if (visit.circumstances.isNullOrBlank()) return@fold orig + + orig.replace(visit.circumstances!!, "Visit#${visit.id}/Circumstances") + } + + return entry.copy(message = newMessage) + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/censors/QRCodeCensor.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/censors/QRCodeCensor.kt index 208ac88117faf57f908ffa012132c8111bbaab80..8b23a70048f81b0ec2713d76ff0c3d129b81c0ce 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/censors/QRCodeCensor.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/censors/QRCodeCensor.kt @@ -13,10 +13,10 @@ class QRCodeCensor @Inject constructor() : BugCensor { val guid = lastGUID ?: return null if (!entry.message.contains(guid)) return null - var newMessage = entry.message.replace(guid, PLACEHOLDER + guid.takeLast(4)) - - if (CWADebug.isDeviceForTestersBuild) { - newMessage = entry.message + val newMessage = if (CWADebug.isDeviceForTestersBuild) { + entry.message.replace(guid, PLACEHOLDER_TESTER + guid.takeLast(27)) + } else { + entry.message.replace(guid, PLACEHOLDER + guid.takeLast(4)) } return entry.copy(message = newMessage) @@ -24,6 +24,7 @@ class QRCodeCensor @Inject constructor() : BugCensor { companion object { var lastGUID: String? = null + private const val PLACEHOLDER_TESTER = "########-" private const val PLACEHOLDER = "########-####-####-####-########" } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/censors/RegistrationTokenCensor.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/censors/RegistrationTokenCensor.kt index 590df2a5aebfe0b81ac7292a61f37cae74bab5ea..843f67ff149bcd117d07e1cacebe4a2413075a24 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/censors/RegistrationTokenCensor.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/censors/RegistrationTokenCensor.kt @@ -12,16 +12,17 @@ class RegistrationTokenCensor @Inject constructor() : BugCensor { val token = LocalData.registrationToken() ?: return null if (!entry.message.contains(token)) return null - var newMessage = entry.message.replace(token, PLACEHOLDER + token.takeLast(4)) - - if (CWADebug.isDeviceForTestersBuild) { - newMessage = entry.message + val newMessage = if (CWADebug.isDeviceForTestersBuild) { + entry.message.replace(token, PLACEHOLDER_TESTER + token.takeLast(27)) + } else { + entry.message.replace(token, PLACEHOLDER + token.takeLast(4)) } return entry.copy(message = newMessage) } companion object { + private const val PLACEHOLDER_TESTER = "########-" private const val PLACEHOLDER = "########-####-####-####-########" } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/DebugLogger.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/DebugLogger.kt index 57f7e38a498fc264bd3d577753690e6c9bc0ea09..698f5b68c658e6a27e39089550f8c42b1997ab8b 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/DebugLogger.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/DebugLogger.kt @@ -90,6 +90,7 @@ class DebugLogger( Timber.tag(TAG).i("setInjectionIsReady()") component.inject(this) isDaggerReady = true + Timber.tag(TAG).d("Censors loaded: %s", bugCensors) } suspend fun start(): Unit = mutex.withLock { diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/DebugLoggerBase.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/DebugLoggerBase.kt index ee056fbd562c4e613e63aac0f8fd60173f38249b..0a152bfd5f48e453bc79bf6474bc3d119da47db4 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/DebugLoggerBase.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/DebugLoggerBase.kt @@ -8,5 +8,5 @@ import javax.inject.Inject */ @Suppress("UnnecessaryAbstractClass") abstract class DebugLoggerBase { - @Inject internal lateinit var bugCensors: dagger.Lazy<List<BugCensor>> + @Inject internal lateinit var bugCensors: dagger.Lazy<Set<BugCensor>> } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/ui/DebugLogFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/ui/DebugLogFragment.kt index 41f63891d30d37f5c88b944ffb680da7b35a4bef..a31c0431644e8efaa287d63fc51464b8d1d00b14 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/ui/DebugLogFragment.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/ui/DebugLogFragment.kt @@ -101,8 +101,8 @@ class DebugLogFragment : Fragment(R.layout.bugreporting_debuglog_fragment), Auto vm.events.observe2(this) { when (it) { - DebugLogViewModel.Event.ShowLogDeletedConfirmation -> { - showLogDeletionConfirmation() + DebugLogViewModel.Event.ShowLogDeletionRequest -> { + showLogDeletionRequest() } DebugLogViewModel.Event.NavigateToPrivacyFragment -> { doNavigate( @@ -151,10 +151,14 @@ class DebugLogFragment : Fragment(R.layout.bugreporting_debuglog_fragment), Auto ) } - private fun showLogDeletionConfirmation() { + private fun showLogDeletionRequest() { MaterialAlertDialogBuilder(requireContext()).apply { + setTitle(R.string.debugging_debuglog_stop_confirmation_title) setMessage(R.string.debugging_debuglog_stop_confirmation_message) - setPositiveButton(android.R.string.yes) { _, _ -> } + setPositiveButton(R.string.debugging_debuglog_stop_confirmation_confirmation_button) { _, _ -> + vm.stopAndDeleteDebugLog() + } + setNegativeButton(R.string.debugging_debuglog_stop_confirmation_discard_button) { _, _ -> /* dismiss */ } }.show() } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/ui/DebugLogViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/ui/DebugLogViewModel.kt index ec5b7e32885e1c8442c4ea6db27388008d247144..d9793e597bc6606b636e4d87e4eed7be0b0578fe 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/ui/DebugLogViewModel.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/ui/DebugLogViewModel.kt @@ -65,8 +65,7 @@ class DebugLogViewModel @AssistedInject constructor( fun onToggleRecording() = launchWithProgress { if (debugLogger.isLogging.value) { - debugLogger.stop() - events.postValue(Event.ShowLogDeletedConfirmation) + events.postValue(Event.ShowLogDeletionRequest) } else { if (debugLogger.storageCheck.isLowStorage(forceCheck = true)) { Timber.d("Low storage, not starting logger.") @@ -86,6 +85,14 @@ class DebugLogViewModel @AssistedInject constructor( } } + fun stopAndDeleteDebugLog() { + launchWithProgress { + if (debugLogger.isLogging.value) { + debugLogger.stop() + } + } + } + fun onStoreLog() = launchWithProgress(finishProgressAction = false) { Timber.d("storeLog()") val snapshot = logSnapshotter.snapshot() @@ -147,7 +154,7 @@ class DebugLogViewModel @AssistedInject constructor( object NavigateToPrivacyFragment : Event() object NavigateToUploadFragment : Event() object NavigateToUploadHistory : Event() - object ShowLogDeletedConfirmation : Event() + object ShowLogDeletionRequest : Event() object ShowLowStorageDialog : Event() data class ShowLocalExportError(val error: Throwable) : Event() data class Error(val error: Throwable) : Event() diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/upload/server/auth/LogUploadAuthApiV1.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/upload/server/auth/LogUploadAuthApiV1.kt index ef9ea5988795eccfa701d6bd790727d7c3ab69f7..8ae2ecc70a4d196c0179b1a82fe3d65dba89e32b 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/upload/server/auth/LogUploadAuthApiV1.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/upload/server/auth/LogUploadAuthApiV1.kt @@ -15,7 +15,7 @@ interface LogUploadAuthApiV1 { @SerializedName("errorCode") val errorCode: String? ) - @POST("version/v1/android/log") + @POST("version/v1/android/els") suspend fun authOTP( @Body requestBody: ElsOtpRequestAndroid.ELSOneTimePasswordRequestAndroid ): AuthResponse 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 f6984c21e47c7cae1c08ef02749d3d15df21926b..88b36e0a77f6312149eb127e16630db03a625d7b 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.storage.LocalData 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 @@ -105,7 +100,6 @@ class MainActivity : AppCompatActivity(), HasAndroidInjector { showEnergyOptimizedEnabledForBackground() } - val navController = supportFragmentManager.findNavController(R.id.nav_host_fragment) binding.mainBottomNavigation.setupWithNavController2(navController) { vm.onBottomNavSelected() } @@ -118,16 +112,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 @@ -154,6 +153,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 fe8a9252ff56112046101f42edf4e0a5c6ff8861..a020dfe537f3aaa4d7e26a4e6adeda9044225a14 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,8 +15,8 @@ import de.rki.coronawarnapp.environment.BuildConfigWrap import de.rki.coronawarnapp.main.CWASettings import de.rki.coronawarnapp.storage.LocalData import de.rki.coronawarnapp.ui.main.MainActivity -import de.rki.coronawarnapp.util.AppShortcuts 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,7 +68,7 @@ class OnboardingActivity : AppCompatActivity(), LifecycleObserver, HasAndroidInj LocalData.isOnboarded(true) LocalData.onboardingCompletedTimestamp(System.currentTimeMillis()) settings.lastChangelogVersion.update { BuildConfigWrap.VERSION_CODE } - MainActivity.start(this) + 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 d78661d2b7a2a4ac2d2152c02ade522a4c19440c..83729d06cdd034b7d6fe33ae0e7208543d37fbe3 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 @@ -45,6 +46,7 @@ class DataReset @Inject constructor( private val surveySettings: SurveySettings, private val analyticsSettings: AnalyticsSettings, private val analytics: Analytics, + private val traceLocationRepository: TraceLocationRepository, private val bugReportingSettings: BugReportingSettings ) { @@ -84,6 +86,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 da4d4e081a5b64cb8664754d5b950c536c7513f6..095a4afc79058ee6d1b99bfb34718173e85a8ae1 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 @@ -72,7 +73,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/legal_strings.xml b/Corona-Warn-App/src/main/res/values-de/legal_strings.xml index 506e8246cbd4ad11a37e2f42c35a1d6e2c77a493..e9500583d90380da150895e7cafd904c53608511 100644 --- a/Corona-Warn-App/src/main/res/values-de/legal_strings.xml +++ b/Corona-Warn-App/src/main/res/values-de/legal_strings.xml @@ -65,7 +65,7 @@ <!-- XHED: Title for the information box in the survey consent detail screen --> <string name="datadonation_survey_consent_details_title">"Prüfung der Echtheit und Drittlandsübermittlung"</string> <!-- XTXT: Text for the information box in the survey consent detail screen --> - <string name="datadonation_survey_consent_details_text">"Um die Echtheit Ihrer App zu bestätigen, erzeugt Ihr Smartphone eine eindeutige Kennung, die Informationen über die Version Ihres Smartphones und der App enthält. Das ist erforderlich, um zu verhindern, dass Nutzer mehrfach der Befragung teilnehmen und so die Ergebnisse der Befragung verfälschen. Die Kennung wird hier einmalig an Google übermittelt. Dabei kann es auch zu einer Datenübermittlung in die USA oder andere Drittländer kommen. Dort besteht möglicherweise kein dem europäischen Recht entsprechendes Datenschutzniveau und Ihre europäischen Datenschutzrechte können eventuell nicht durchgesetzt werden. Insbesondere besteht die Möglichkeit, dass Sicherheitsbehörden im Drittland, auch ohne einen konkreten Verdacht, auf die übermittelten Daten bei Google zugreifen und diese auswerten, beispielsweise indem sie Daten mit anderen Informationen verknüpfen. Dies betrifft nur die an Google übermittelte Kennung. Die weiteren Angaben über Ihre Teilnahme an der Befragung erhält Google nicht. Möglicherweise kann Google jedoch anhand der Kennung auf Ihre Identität schließen und nachvollziehen, dass die Echtheitsprüfung Ihres Smartphones stattgefunden hat.\n\nWenn Sie mit der Drittlandsübermittlung nicht einverstanden sind, tippen Sie bitte nicht „Einverstanden“ an. Sie können die App weiterhin nutzen, eine Teilnahme an dieser Befragung ist dann jedoch nicht möglich."</string> + <string name="datadonation_survey_consent_details_text">"Um die Echtheit Ihrer App zu bestätigen, erzeugt Ihr Smartphone eine eindeutige Kennung, die Informationen über die Version Ihres Smartphones und der App enthält. Das ist erforderlich, um zu verhindern, dass Nutzer mehrfach an der Befragung teilnehmen und so die Ergebnisse der Befragung verfälschen. Die Kennung wird hier einmalig an Google übermittelt. Dabei kann es auch zu einer Datenübermittlung in die USA oder andere Drittländer kommen. Dort besteht möglicherweise kein dem europäischen Recht entsprechendes Datenschutzniveau und Ihre europäischen Datenschutzrechte können eventuell nicht durchgesetzt werden. Insbesondere besteht die Möglichkeit, dass Sicherheitsbehörden im Drittland, auch ohne einen konkreten Verdacht, auf die übermittelten Daten bei Google zugreifen und diese auswerten, beispielsweise indem sie Daten mit anderen Informationen verknüpfen. Dies betrifft nur die an Google übermittelte Kennung. Die weiteren Angaben über Ihre Teilnahme an der Befragung erhält Google nicht. Möglicherweise kann Google jedoch anhand der Kennung auf Ihre Identität schließen und nachvollziehen, dass die Echtheitsprüfung Ihres Smartphones stattgefunden hat.\n\nWenn Sie mit der Drittlandsübermittlung nicht einverstanden sind, tippen Sie bitte nicht „Einverstanden“ an. Sie können die App weiterhin nutzen, eine Teilnahme an dieser Befragung ist dann jedoch nicht möglich."</string> <!-- XTXT: onboarding privacy preserving analytics (ppa) - consent title --> <string name="ppa_onboarding_consent_title" translatable="false">"Ihr Einverständnis"</string> 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 0055ef71a88b3db5ed9a851619a8fd2b25694e31..d12543303c42f7fb65217e17725db878b7192150 100644 --- a/Corona-Warn-App/src/main/res/values-de/strings.xml +++ b/Corona-Warn-App/src/main/res/values-de/strings.xml @@ -905,8 +905,16 @@ <string name="debugging_debuglog_status_not_recording">"Inaktiv"</string> <!-- YTXT: Describtion for current logging status text if a debug log is not being recorded --> <string name="debugging_debuglog_status_additional_infos">"Derzeitige Größe: %1$s (unkomprimiert)"</string> + + + <!-- YTXT: Dialog title if the log recording is stopped, and thus deleted. --> + <string name="debugging_debuglog_stop_confirmation_title">"Wollen Sie die Fehleranalyse wirklich stoppen?"</string> <!-- YTXT: Dialog message if the log recording is stopped, and thus deleted. --> - <string name="debugging_debuglog_stop_confirmation_message">"Der Fehlerbericht wurde gelöscht. Wenn Sie eine lokale Kopie des Fehlerberichts gespeichert haben, wurde diese nicht gelöscht."</string> + <string name="debugging_debuglog_stop_confirmation_message">"Hierbei werden alle bereits aufgezeichneten Daten gelöscht. Wenn Sie eine lokale Kopie des Fehlerberichts gespeichert haben, wird diese nicht gelöscht."</string> + <!-- YTXT: Dialog confirmation button if the log recording is stopped, and thus deleted. --> + <string name="debugging_debuglog_stop_confirmation_confirmation_button">"Analyse stoppen"</string> + <!-- YTXT: Dialog discard button if the log recording is stopped, and thus deleted. --> + <string name="debugging_debuglog_stop_confirmation_discard_button">"Analyse fortführen"</string> <!-- YTXT: Dialog message if there is not enough free storage to start a debug log --> <string name="debugging_debuglog_start_low_storage_error">"Sie brauchen mindestens 200 MB Speicherplatz, um die Fehleranalyse zu starten. Bitte geben Sie Speicherplatz frei."</string> <!-- XHED: Dialog title if a user has stored a debug log locally --> @@ -915,7 +923,6 @@ <string name="debugging_debuglog_localexport_message">"Die Fehleranalyse wurde lokal gespeichert."</string> <!-- YTXT: Dialog message if local export has failed --> <string name="debugging_debuglog_localexport_error_message">"Das Speichern des Fehlerberichts ist fehlgeschlagen. Bitte überprüfen Sie, ob genügend Speicherplatz zur Verfügung steht."</string> - <!-- XHED: Title for debug legal screen --> <string name="debugging_debuglog_legal_dialog_title">"Ausführliche Informationen zur Übersendung der Fehlerberichte"</string> <!-- YTXT: Section Title for debug legal screen --> @@ -1832,6 +1839,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 @@ -1949,4 +1958,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 ee5cad7b72738f11b52bc72fd7cc92e503250749..424c7c16a33a60c01eb7bedbfdf293a8d4391e72 100644 --- a/Corona-Warn-App/src/main/res/values/strings.xml +++ b/Corona-Warn-App/src/main/res/values/strings.xml @@ -923,8 +923,14 @@ <string name="debugging_debuglog_status_additional_infos">"Current size: %1$s (uncompressed)"</string> <!-- XHED: Title for native sharing dialog --> <string name="debugging_debuglog_sharing_dialog_title">"Share CWA Error Report"</string> + <!-- YTXT: Dialog title if the log recording is stopped, and thus deleted. --> + <string name="debugging_debuglog_stop_confirmation_title">"Wollen Sie die Fehleranalyse wirklich stoppen?"</string> <!-- YTXT: Dialog message if the log recording is stopped, and thus deleted. --> - <string name="debugging_debuglog_stop_confirmation_message">"Der Fehlerbericht wurde gelöscht. Wenn Sie eine lokale Kopie des Fehlerberichts gespeichert haben, wurde diese nicht gelöscht."</string> + <string name="debugging_debuglog_stop_confirmation_message">"Hierbei werden alle bereits aufgezeichneten Daten gelöscht. Wenn Sie eine lokale Kopie des Fehlerberichts gespeichert haben, wird diese nicht gelöscht."</string> + <!-- YTXT: Dialog confirmation button if the log recording is stopped, and thus deleted. --> + <string name="debugging_debuglog_stop_confirmation_confirmation_button">"Analyse stoppen"</string> + <!-- YTXT: Dialog discard button if the log recording is stopped, and thus deleted. --> + <string name="debugging_debuglog_stop_confirmation_discard_button">"Analyse fortführen"</string> <!-- YTXT: Dialog message if there is not enough free storage to start a debug log --> <string name="debugging_debuglog_start_low_storage_error">Sie brauchen mindestens 200 MB Speicherplatz, um die Fehleranalyse zu starten. Bitte geben Sie Speicherplatz frei.</string> <!-- XHED: Dialog title if a user has stored a debug log locally --> @@ -1851,6 +1857,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 @@ -1968,4 +1976,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/bugreporting/censors/CensorInjectionTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/censors/CensorInjectionTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..10c54df0a834451588d7daddb3610dd29a6cc673 --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/censors/CensorInjectionTest.kt @@ -0,0 +1,66 @@ +package de.rki.coronawarnapp.bugreporting.censors + +import dagger.Component +import dagger.Module +import dagger.Provides +import de.rki.coronawarnapp.bugreporting.BugReportingSharedModule +import de.rki.coronawarnapp.contactdiary.storage.repo.ContactDiaryRepository +import io.github.classgraph.ClassGraph +import io.kotest.matchers.collections.shouldContainAll +import io.kotest.matchers.shouldBe +import io.mockk.mockk +import org.junit.jupiter.api.Test +import testhelpers.BaseTest +import timber.log.Timber +import javax.inject.Singleton + +class CensorInjectionTest : BaseTest() { + + /** + * Scan our class graph, and compare it against what we inject via Dagger. + * Catch accidentally forgetting to add a bug censor. + */ + @Test + fun `all censors are injected`() { + val component = DaggerBugCensorTestComponent.factory().create() + val bugCensors = component.bugCensors + + Timber.v("We know %d censors.", bugCensors.size) + require(bugCensors.isNotEmpty()) + + val scanResult = ClassGraph() + .acceptPackages("de.rki.coronawarnapp") + .enableClassInfo() + .scan() + + val ourCensors = scanResult + .getClassesImplementing("de.rki.coronawarnapp.bugreporting.censors.BugCensor") + + Timber.v("Our project contains %d censor classes.", ourCensors.size) + + val injectedCensors = bugCensors.map { it.javaClass.name }.toSet() + val existingCensors = ourCensors.map { it.name }.toSet() + existingCensors.isEmpty() shouldBe false + injectedCensors shouldContainAll existingCensors + } +} + +@Singleton +@Component(modules = [MockProvider::class, BugReportingSharedModule::class]) +interface BugCensorTestComponent { + + val bugCensors: Set<BugCensor> + + @Component.Factory + interface Factory { + fun create(): BugCensorTestComponent + } +} + +@Module +class MockProvider { + + @Singleton + @Provides + fun diary(): ContactDiaryRepository = mockk() +} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/censors/DiaryEncounterCensorTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/censors/DiaryEncounterCensorTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..9ebaa58660f865e91be890d49886070ffe1aef28 --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/censors/DiaryEncounterCensorTest.kt @@ -0,0 +1,101 @@ +package de.rki.coronawarnapp.bugreporting.censors + +import de.rki.coronawarnapp.bugreporting.debuglog.LogLine +import de.rki.coronawarnapp.contactdiary.model.ContactDiaryPersonEncounter +import de.rki.coronawarnapp.contactdiary.storage.repo.ContactDiaryRepository +import io.kotest.matchers.shouldBe +import io.mockk.MockKAnnotations +import io.mockk.every +import io.mockk.impl.annotations.MockK +import io.mockk.mockk +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.runBlockingTest +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import testhelpers.BaseTest + +class DiaryEncounterCensorTest : BaseTest() { + + @MockK lateinit var diaryRepo: ContactDiaryRepository + + @BeforeEach + fun setup() { + MockKAnnotations.init(this) + } + + private fun createInstance(scope: CoroutineScope) = DiaryEncounterCensor( + debugScope = scope, + diary = diaryRepo + ) + + private fun mockEncounter( + _id: Long, + _circumstances: String + ) = mockk<ContactDiaryPersonEncounter>().apply { + every { id } returns _id + every { circumstances } returns _circumstances + } + + @Test + fun `censoring replaces the logline message`() = runBlockingTest { + every { diaryRepo.personEncounters } returns flowOf( + listOf( + mockEncounter(1, _circumstances = ""), + mockEncounter(2, _circumstances = "A rainy day"), + mockEncounter(3, "Spilled coffee on each others laptops") + ) + ) + + val instance = createInstance(this) + val censorMe = LogLine( + timestamp = 1, + priority = 3, + message = """ + On A rainy day, + two persons Spilled coffee on each others laptops, + everyone disliked that. + """.trimIndent(), + tag = "I'm a tag", + throwable = null + ) + + instance.checkLog(censorMe) shouldBe censorMe.copy( + message = """ + On Encounter#2/Circumstances, + two persons Encounter#3/Circumstances, + everyone disliked that. + """.trimIndent() + ) + } + + @Test + fun `censoring returns null if all circumstances are blank`() = runBlockingTest { + every { diaryRepo.personEncounters } returns flowOf(listOf(mockEncounter(1, _circumstances = ""))) + val instance = createInstance(this) + val notCensored = LogLine( + timestamp = 1, + priority = 3, + message = "That was strange.", + tag = "I'm a tag", + throwable = null + ) + instance.checkLog(notCensored) shouldBe null + } + + @Test + fun `censoring returns null if there are no locations no match`() = runBlockingTest { + every { diaryRepo.personEncounters } returns flowOf(emptyList()) + + val instance = createInstance(this) + + val notCensored = LogLine( + timestamp = 1, + priority = 3, + message = "Nothing ever happens.", + tag = "I'm a tag", + throwable = null + ) + instance.checkLog(notCensored) shouldBe null + } +} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/censors/DiaryLocationCensorTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/censors/DiaryLocationCensorTest.kt index a02d2f73cd405b14179730df07f4083f4d47dd63..eaa554089d8aed49074add3417232b0e12cab3c2 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/censors/DiaryLocationCensorTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/censors/DiaryLocationCensorTest.kt @@ -3,17 +3,14 @@ package de.rki.coronawarnapp.bugreporting.censors import de.rki.coronawarnapp.bugreporting.debuglog.LogLine import de.rki.coronawarnapp.contactdiary.model.ContactDiaryLocation import de.rki.coronawarnapp.contactdiary.storage.repo.ContactDiaryRepository -import de.rki.coronawarnapp.util.CWADebug import io.kotest.matchers.shouldBe import io.mockk.MockKAnnotations import io.mockk.every import io.mockk.impl.annotations.MockK import io.mockk.mockk -import io.mockk.mockkObject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runBlockingTest -import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import testhelpers.BaseTest @@ -25,14 +22,6 @@ class DiaryLocationCensorTest : BaseTest() { @BeforeEach fun setup() { MockKAnnotations.init(this) - - mockkObject(CWADebug) - every { CWADebug.isDeviceForTestersBuild } returns false - } - - @AfterEach - fun teardown() { - QRCodeCensor.lastGUID = null } private fun createInstance(scope: CoroutineScope) = DiaryLocationCensor( @@ -40,31 +29,45 @@ class DiaryLocationCensorTest : BaseTest() { diary = diaryRepo ) - private fun mockLocation(id: Long, name: String) = mockk<ContactDiaryLocation>().apply { + private fun mockLocation( + id: Long, + name: String, + phone: String?, + mail: String? + ) = mockk<ContactDiaryLocation>().apply { every { locationId } returns id every { locationName } returns name + every { phoneNumber } returns phone + every { emailAddress } returns mail } @Test fun `censoring replaces the logline message`() = runBlockingTest { every { diaryRepo.locations } returns flowOf( - listOf(mockLocation(1, "Berlin"), mockLocation(2, "Munich"), mockLocation(3, "Aachen")) + listOf( + mockLocation(1, "Munich", phone = "+49 089 3333", mail = "bürgermeister@münchen.de"), + mockLocation(2, "Bielefeld", phone = null, mail = null), + mockLocation(3, "Aachen", phone = "+49 0241 9999", mail = "karl@aachen.de") + ) ) val instance = createInstance(this) val censorMe = LogLine( timestamp = 1, priority = 3, - message = "Munich is nice, but Aachen is nice too.", + message = """ + Bürgermeister of Munich (+49 089 3333) and Karl of Aachen [+49 0241 9999] called each other. + Both agreed that their emails (bürgermeister@münchen.de|karl@aachen.de) are awesome, + and that Bielefeld doesn't exist as it has neither phonenumber (null) nor email (null). + """.trimIndent(), tag = "I'm a tag", throwable = null ) instance.checkLog(censorMe) shouldBe censorMe.copy( - message = "Location#2 is nice, but Location#3 is nice too." - ) - - every { CWADebug.isDeviceForTestersBuild } returns true - instance.checkLog(censorMe) shouldBe censorMe.copy( - message = "Munich is nice, but Aachen is nice too." + message = """ + Bürgermeister of Location#1/Name (Location#1/PhoneNumber) and Karl of Location#3/Name [Location#3/PhoneNumber] called each other. + Both agreed that their emails (Location#1/EMail|Location#3/EMail) are awesome, + and that Location#2/Name doesn't exist as it has neither phonenumber (null) nor email (null). + """.trimIndent() ) } diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/censors/DiaryPersonCensorTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/censors/DiaryPersonCensorTest.kt index 44b46c3d8685a849216a9287fd01fda88a802d99..75df6df3742ae82d6979d435cbc3b40f3cf55b41 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/censors/DiaryPersonCensorTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/censors/DiaryPersonCensorTest.kt @@ -3,17 +3,14 @@ package de.rki.coronawarnapp.bugreporting.censors import de.rki.coronawarnapp.bugreporting.debuglog.LogLine import de.rki.coronawarnapp.contactdiary.model.ContactDiaryPerson import de.rki.coronawarnapp.contactdiary.storage.repo.ContactDiaryRepository -import de.rki.coronawarnapp.util.CWADebug import io.kotest.matchers.shouldBe import io.mockk.MockKAnnotations import io.mockk.every import io.mockk.impl.annotations.MockK import io.mockk.mockk -import io.mockk.mockkObject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runBlockingTest -import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import testhelpers.BaseTest @@ -25,14 +22,6 @@ class DiaryPersonCensorTest : BaseTest() { @BeforeEach fun setup() { MockKAnnotations.init(this) - - mockkObject(CWADebug) - every { CWADebug.isDeviceForTestersBuild } returns false - } - - @AfterEach - fun teardown() { - QRCodeCensor.lastGUID = null } private fun createInstance(scope: CoroutineScope) = DiaryPersonCensor( @@ -40,31 +29,45 @@ class DiaryPersonCensorTest : BaseTest() { diary = diaryRepo ) - private fun mockPerson(id: Long, name: String) = mockk<ContactDiaryPerson>().apply { + private fun mockPerson( + id: Long, + name: String, + phone: String?, + mail: String? + ) = mockk<ContactDiaryPerson>().apply { every { personId } returns id every { fullName } returns name + every { phoneNumber } returns phone + every { emailAddress } returns mail } @Test fun `censoring replaces the logline message`() = runBlockingTest { every { diaryRepo.people } returns flowOf( - listOf(mockPerson(1, "Luka"), mockPerson(2, "Ralf"), mockPerson(3, "Matthias")) + listOf( + mockPerson(1, "Luka", phone = "+49 1234 7777", mail = "luka@sap.com"), + mockPerson(2, "Ralf", phone = null, mail = null), + mockPerson(3, "Matthias", phone = null, mail = "matthias@sap.com") + ) ) val instance = createInstance(this) val censorMe = LogLine( timestamp = 1, priority = 3, - message = "Ralf needs more coffee, but Matthias has had enough for today.", + message = """ + Ralf requested more coffee from +49 1234 7777, + but Matthias thought he had enough has had enough for today. + A quick mail to luka@sap.com confirmed this. + """.trimIndent(), tag = "I'm a tag", throwable = null ) instance.checkLog(censorMe) shouldBe censorMe.copy( - message = "Person#2 needs more coffee, but Person#3 has had enough for today." - ) - - every { CWADebug.isDeviceForTestersBuild } returns true - instance.checkLog(censorMe) shouldBe censorMe.copy( - message = "Ralf needs more coffee, but Matthias has had enough for today." + message = """ + Person#2/Name requested more coffee from Person#1/PhoneNumber, + but Person#3/Name thought he had enough has had enough for today. + A quick mail to Person#1/EMail confirmed this. + """.trimIndent() ) } diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/censors/DiaryVisitCensorTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/censors/DiaryVisitCensorTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..5f9d6b5411995a371b19ef6f6237cbf2667ab1a0 --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/censors/DiaryVisitCensorTest.kt @@ -0,0 +1,97 @@ +package de.rki.coronawarnapp.bugreporting.censors + +import de.rki.coronawarnapp.bugreporting.debuglog.LogLine +import de.rki.coronawarnapp.contactdiary.model.ContactDiaryLocationVisit +import de.rki.coronawarnapp.contactdiary.storage.repo.ContactDiaryRepository +import io.kotest.matchers.shouldBe +import io.mockk.MockKAnnotations +import io.mockk.every +import io.mockk.impl.annotations.MockK +import io.mockk.mockk +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.runBlockingTest +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import testhelpers.BaseTest + +class DiaryVisitCensorTest : BaseTest() { + + @MockK lateinit var diaryRepo: ContactDiaryRepository + + @BeforeEach + fun setup() { + MockKAnnotations.init(this) + } + + private fun createInstance(scope: CoroutineScope) = DiaryVisitCensor( + debugScope = scope, + diary = diaryRepo + ) + + private fun mockVisit( + _id: Long, + _circumstances: String + ) = mockk<ContactDiaryLocationVisit>().apply { + every { id } returns _id + every { circumstances } returns _circumstances + } + + @Test + fun `censoring replaces the logline message`() = runBlockingTest { + every { diaryRepo.locationVisits } returns flowOf( + listOf( + mockVisit(1, _circumstances = "Döner that was too spicy"), + mockVisit(2, _circumstances = "beard shaved without mask"), + mockVisit(3, _circumstances = "out of toiletpaper") + ) + ) + val instance = createInstance(this) + val censorMe = LogLine( + timestamp = 1, + priority = 3, + message = """ + After having a Döner that was too spicy, + I got my beard shaved without mask, + only to find out the supermarket was out of toiletpaper. + """.trimIndent(), + tag = "I'm a tag", + throwable = null + ) + instance.checkLog(censorMe) shouldBe censorMe.copy( + message = """ + After having a Visit#1/Circumstances, + I got my Visit#2/Circumstances, + only to find out the supermarket was Visit#3/Circumstances. + """.trimIndent() + ) + } + + @Test + fun `censoring returns null if all circumstances are blank`() = runBlockingTest { + every { diaryRepo.locationVisits } returns flowOf(listOf(mockVisit(1, _circumstances = ""))) + val instance = createInstance(this) + val notCensored = LogLine( + timestamp = 1, + priority = 3, + message = "So many places to visit, but no place like home!", + tag = "I'm a tag", + throwable = null + ) + instance.checkLog(notCensored) shouldBe null + } + + @Test + fun `censoring returns null if there are no visits no match`() = runBlockingTest { + every { diaryRepo.locationVisits } returns flowOf(emptyList()) + val instance = createInstance(this) + val notCensored = LogLine( + timestamp = 1, + priority = 3, + message = "So many places to visit, but no place like home!", + tag = "I'm a tag", + throwable = null + ) + instance.checkLog(notCensored) shouldBe null + } +} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/censors/QRCodeCensorTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/censors/QRCodeCensorTest.kt index 617bad4cab1ae22191aafd4b5261b98c8f19ab8b..62bf33f114bc5d0051b5d9fa0fafa92264c01417 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/censors/QRCodeCensorTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/censors/QRCodeCensorTest.kt @@ -48,7 +48,7 @@ class QRCodeCensorTest : BaseTest() { every { CWADebug.isDeviceForTestersBuild } returns true instance.checkLog(censored) shouldBe censored.copy( - message = "I'm a shy qrcode: 63b4d3ff-e0de-4bd4-90c1-17c2bb683a2f" + message = "I'm a shy qrcode: ########-e0de-4bd4-90c1-17c2bb683a2f" ) } diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/censors/RegistrationTokenCensorTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/censors/RegistrationTokenCensorTest.kt index 08978e545feb1e462f8e81f139c65cc52b7e2394..a1266ba8016ff84188722fbe1eee9020392cb410 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/censors/RegistrationTokenCensorTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/censors/RegistrationTokenCensorTest.kt @@ -46,7 +46,7 @@ class RegistrationTokenCensorTest : BaseTest() { every { CWADebug.isDeviceForTestersBuild } returns true instance.checkLog(filterMe) shouldBe filterMe.copy( - message = "I'm a shy registration token: 63b4d3ff-e0de-4bd4-90c1-17c2bb683a2f" + message = "I'm a shy registration token: ########-e0de-4bd4-90c1-17c2bb683a2f" ) verify { LocalData.registrationToken() } diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/debuglog/DebugLoggerTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/debuglog/DebugLoggerTest.kt index 61dbd80eafbcd892ac79d14ba4b32cd4f0eb0b6a..ace9d7c0a8bf6eacb9e3f12d265f0f0a157d593a 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/debuglog/DebugLoggerTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/debuglog/DebugLoggerTest.kt @@ -49,7 +49,7 @@ class DebugLoggerTest : BaseIOTest() { every { application.cacheDir } returns cacheDir every { component.inject(any<DebugLogger>()) } answers { val logger = arg<DebugLogger>(0) - logger.bugCensors = Lazy { listOf(registrationTokenCensor) } + logger.bugCensors = Lazy { setOf(registrationTokenCensor) } } coEvery { registrationTokenCensor.checkLog(any()) } returns null diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/debuglog/upload/server/auth/LogUploadAuthApiTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/debuglog/upload/server/auth/LogUploadAuthApiTest.kt index 46c4a59e9e75e3aca26e6b4b81030744f78e27e1..d4f34fa8717d49a34b76624ed0f4759ca9ab6809 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/debuglog/upload/server/auth/LogUploadAuthApiTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/debuglog/upload/server/auth/LogUploadAuthApiTest.kt @@ -77,7 +77,7 @@ class LogUploadAuthApiTest : BaseTest() { api.authOTP(requestBody = elsPayload) webServer.takeRequest(5, TimeUnit.SECONDS)!!.apply { - path shouldBe "/version/v1/android/log" + path shouldBe "/version/v1/android/els" body.readByteArray() shouldBe elsPayload.toByteArray() } } 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