diff --git a/.circleci/config.yml b/.circleci/config.yml index e368d3a47e261dc557e1e2802beb3da54d7ac4e7..20f3f4a0c66333127c4e0480a8e3b4a3daca7be0 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -396,7 +396,7 @@ jobs: - run: name: Download Environment Properties command: | - curl --header "Authorization: token $keystore_download_token" --header "Accept: application/vnd.github.v3.raw" --remote-name --location "$keystore_download_url$env_prop_download_filename" + curl --header "Authorization: token $keystore_download_token" --header "Accept: application/vnd.github.v3.raw" --remote-name --location "$environments_download_url$env_prop_download_filename" - run: name: Decrypt Keystore command: openssl enc -aes-256-cbc -d -pbkdf2 -iter 100000 -in $keystore_download_filename -out $keystore_filename -k $keystore_encrypt_key diff --git a/.reuse/dep5 b/.reuse/dep5 index f04f0807befd63a90e2e5b9c54f6c1fdb7d3d465..b1088ff6d57f1f228fdbe25522ebe3b70c80941e 100644 --- a/.reuse/dep5 +++ b/.reuse/dep5 @@ -40,6 +40,14 @@ Files: Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/viewmodel/ViewMod Copyright: 2018 The Android Open Source Project License: Apache-2.0 +Files: Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/coroutine/ListenableFuture.kt +Copyright: 2018 The Android Open Source Project +License: Apache-2.0 + Files: gradlew gradlew.bat Copyright: Copyright 2015 the original author or authors. +License: Apache-2.0 + +Files: Corona-Warn-App/libs/play-services-nearby-exposurenotification-18.0.3.aar +Copyright: Copyright 2020 Google LLC License: Apache-2.0 \ No newline at end of file diff --git a/Corona-Warn-App/schemas/de.rki.coronawarnapp.eventregistration.checkins.riskcalculation.PresenceTracingDatabase/1.json b/Corona-Warn-App/schemas/de.rki.coronawarnapp.eventregistration.checkins.riskcalculation.PresenceTracingDatabase/1.json deleted file mode 100644 index 1109cd70c0a4675c7b28f92bdd500ac9ca638fad..0000000000000000000000000000000000000000 --- a/Corona-Warn-App/schemas/de.rki.coronawarnapp.eventregistration.checkins.riskcalculation.PresenceTracingDatabase/1.json +++ /dev/null @@ -1,64 +0,0 @@ -{ - "formatVersion": 1, - "database": { - "version": 1, - "identityHash": "751c249cb9c836add4b0cb663ed13954", - "entities": [ - { - "tableName": "TraceTimeIntervalMatchEntity", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `checkInId` INTEGER NOT NULL, `traceWarningPackageId` INTEGER NOT NULL, `transmissionRiskLevel` INTEGER NOT NULL, `startTimeMillis` INTEGER NOT NULL, `endTimeMillis` INTEGER NOT NULL)", - "fields": [ - { - "fieldPath": "id", - "columnName": "id", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "checkInId", - "columnName": "checkInId", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "traceWarningPackageId", - "columnName": "traceWarningPackageId", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "transmissionRiskLevel", - "columnName": "transmissionRiskLevel", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "startTimeMillis", - "columnName": "startTimeMillis", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "endTimeMillis", - "columnName": "endTimeMillis", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "id" - ], - "autoGenerate": true - }, - "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, '751c249cb9c836add4b0cb663ed13954')" - ] - } -} \ No newline at end of file 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 index 9e78f2a34c5721273cb56e001bd388c5b50decf5..befd229435a5e47f296ec11550b0b1534c07e0cb 100644 --- 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 @@ -2,11 +2,11 @@ "formatVersion": 1, "database": { "version": 1, - "identityHash": "1dc5f8a56361d50b8bb18050bce59d20", + "identityHash": "5117ed4caaa7ecd70051902d844cc665", "entities": [ { "tableName": "checkin", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `traceLocationIdBase64` TEXT NOT NULL, `traceLocationIdHashBase64` 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, `cryptographicSeedBase64` TEXT NOT NULL, `cnPublicKey` TEXT NOT NULL, `checkInStart` TEXT NOT NULL, `checkInEnd` TEXT NOT NULL, `completed` INTEGER NOT NULL, `createJournalEntry` INTEGER NOT NULL)", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `traceLocationIdBase64` 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, `cryptographicSeedBase64` TEXT NOT NULL, `cnPublicKey` TEXT NOT NULL, `checkInStart` TEXT NOT NULL, `checkInEnd` TEXT NOT NULL, `completed` INTEGER NOT NULL, `createJournalEntry` INTEGER NOT NULL)", "fields": [ { "fieldPath": "id", @@ -20,12 +20,6 @@ "affinity": "TEXT", "notNull": true }, - { - "fieldPath": "traceLocationIdHashBase64", - "columnName": "traceLocationIdHashBase64", - "affinity": "TEXT", - "notNull": true - }, { "fieldPath": "version", "columnName": "version", @@ -192,7 +186,7 @@ "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, '1dc5f8a56361d50b8bb18050bce59d20')" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '5117ed4caaa7ecd70051902d844cc665')" ] } } \ No newline at end of file diff --git a/Corona-Warn-App/schemas/de.rki.coronawarnapp.presencetracing.risk.PresenceTracingRiskDatabase/1.json b/Corona-Warn-App/schemas/de.rki.coronawarnapp.presencetracing.risk.storage.PresenceTracingRiskDatabase/1.json similarity index 100% rename from Corona-Warn-App/schemas/de.rki.coronawarnapp.presencetracing.risk.PresenceTracingRiskDatabase/1.json rename to Corona-Warn-App/schemas/de.rki.coronawarnapp.presencetracing.risk.storage.PresenceTracingRiskDatabase/1.json diff --git a/Corona-Warn-App/schemas/de.rki.coronawarnapp.presencetracing.warning.storage.TraceWarningDatabase/1.json b/Corona-Warn-App/schemas/de.rki.coronawarnapp.presencetracing.warning.storage.TraceWarningDatabase/1.json new file mode 100644 index 0000000000000000000000000000000000000000..71c7f0b1df463c61c0d7ce53f985d3ad898907d1 --- /dev/null +++ b/Corona-Warn-App/schemas/de.rki.coronawarnapp.presencetracing.warning.storage.TraceWarningDatabase/1.json @@ -0,0 +1,76 @@ +{ + "formatVersion": 1, + "database": { + "version": 1, + "identityHash": "1775e362b403962906c6d384338b2708", + "entities": [ + { + "tableName": "TraceWarningPackageMetadata", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `createdAt` TEXT NOT NULL, `location` TEXT NOT NULL, `hourInterval` INTEGER NOT NULL, `eTag` TEXT, `downloaded` INTEGER NOT NULL, `emptyPkg` INTEGER NOT NULL, `processed` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "packageId", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "location", + "columnName": "location", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "hourInterval", + "columnName": "hourInterval", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "eTag", + "columnName": "eTag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isDownloaded", + "columnName": "downloaded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isEmptyPkg", + "columnName": "emptyPkg", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isProcessed", + "columnName": "processed", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "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, '1775e362b403962906c6d384338b2708')" + ] + } +} \ No newline at end of file diff --git a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/VerifiedTraceLocationTest.kt b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/VerifiedTraceLocationTest.kt index 8bbe73ab7457b385ff6573a73f5ebfff6d902b8d..2e78d0912c047be24b4ab9c53a46da693f0deaf7 100644 --- a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/VerifiedTraceLocationTest.kt +++ b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/VerifiedTraceLocationTest.kt @@ -1,13 +1,10 @@ package de.rki.coronawarnapp.eventregistration.checkins.qrcode -import de.rki.coronawarnapp.environment.EnvironmentSetup +import android.os.Bundle +import android.os.Parcel import de.rki.coronawarnapp.server.protocols.internal.pt.TraceLocationOuterClass import io.kotest.matchers.shouldBe -import io.mockk.MockKAnnotations -import io.mockk.every -import io.mockk.impl.annotations.MockK import okio.ByteString.Companion.decodeBase64 -import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.JUnit4 @@ -15,254 +12,68 @@ import testhelpers.BaseTestInstrumentation @RunWith(JUnit4::class) class VerifiedTraceLocationTest : BaseTestInstrumentation() { - - @MockK lateinit var environmentSetup: EnvironmentSetup - - @Before - fun setUp() { - MockKAnnotations.init(this) - every { environmentSetup.appConfigVerificationKey } returns PUB_KEY - } - - // TODO: Ugly but kinda works @Test - fun verifyTraceLocationIdGenerationHash1() { - val base64Payload = "CAESLAgBEhFNeSBCaXJ0aGRheSBQYXJ0eRoLYXQgbXkgcGxhY2Uo04ekAT" + - "D3h6QBGmUIARJbMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEc7DEst" + - "cUIRcyk35OYDJ95/hTg3UVhsaDXKT0zK7NhHPXoyzipEnOp3GyNXDVpaPi3" + - "cAfQmxeuFMZAIX2+6A5XhoEMTIzNCIECAEQAg==" - val base64LocationID = "jNcJTCajd9Sen6Tbexl2Yb7O3J7ps47b6k4+QMT4xS0=" - - val qrCodePayload = - TraceLocationOuterClass.QRCodePayload.parseFrom(base64Payload.decodeBase64()!!.toByteArray()) - val instance = VerifiedTraceLocation(qrCodePayload) + fun verifyTraceLocationMapping1() { + val qrCodePayload = TraceLocationOuterClass.QRCodePayload.parseFrom( + BASE64_PAYLOAD_1.decodeBase64()!!.toByteArray() + ) - instance.traceLocationID.sha256().base64() shouldBe base64LocationID + VerifiedTraceLocation(qrCodePayload).traceLocation + .apply { + locationId.base64() shouldBe "jNcJTCajd9Sen6Tbexl2Yb7O3J7ps47b6k4+QMT4xS0=" + qrCodePayload() shouldBe qrCodePayload + } } @Test - fun verifyTraceLocationIdGenerationHash2() { - val base64Payload = "CAESIAgBEg1JY2VjcmVhbSBTaG9wGg1NYWluIFN0cmVldCAxGmUIARJ" + - "bMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEc7DEstcUIRcyk35OYDJ95/hTg3UVhsaDXKT" + - "0zK7NhHPXoyzipEnOp3GyNXDVpaPi3cAfQmxeuFMZAIX2+6A5XhoEMTIzNCIGCAEQARgK" - val base64LocationID = "GMuCjqNmOdYyrFhyvFNTVEeLaZh+uShgUoY0LYJo4YQ=" - - val qrCodePayload = - TraceLocationOuterClass.QRCodePayload.parseFrom(base64Payload.decodeBase64()!!.toByteArray()) - val instance = VerifiedTraceLocation(qrCodePayload) - - instance.traceLocationID.sha256().base64() shouldBe base64LocationID - } - - /* disabled because of incompatibilities due to latest tech spec changes... needs to be re-written anyway - -@Test -fun verifyEventSuccess() = runBlockingTest { - val instant = Instant.ofEpochMilli(2687960 * 1_000L) - shouldNotThrowAny { - val verifyResult = traceLocationQRCodeVerifier.verify(ENCODED_EVENT1.decodeBase32().toByteArray()) - verifyResult.apply { - traceLocation.description shouldBe "My Birthday Party" - traceLocation.isBeforeStartTime(instant) shouldBe false - traceLocation.isAfterEndTime(instant) shouldBe false - } - } -} - -@Test -fun verifyParcelization() = runBlockingTest { - val verifyResult = traceLocationQRCodeVerifier.verify(ENCODED_EVENT1.decodeBase32().toByteArray()) - - val expectedTraceLocation = TraceLocation( - guid = "3055331c-2306-43f3-9742-6d8fab54e848", - version = 1, - type = TraceLocationOuterClass.TraceLocationType.LOCATION_TYPE_TEMPORARY_OTHER, - description = "My Birthday Party", - address = "at my place", - startDate = Instant.ofEpochSecond(2687955), - endDate = Instant.ofEpochSecond(2687991), - defaultCheckInLengthInMinutes = 0, - byteRepresentation = verifyResult.traceLocationBytes, - signature = verifyResult.signature.toByteArray().toByteString(), - ) - - verifyResult.traceLocation shouldBe expectedTraceLocation - - val bundle = Bundle().apply { - putParcelable("test", verifyResult.traceLocation) - } - - val parcelRaw = Parcel.obtain().apply { - writeBundle(bundle) - }.marshall() - - val restoredParcel = Parcel.obtain().apply { - unmarshall(parcelRaw, 0, parcelRaw.size) - setDataPosition(0) - } - - val restoredData = restoredParcel.readBundle()!!.run { - classLoader = TraceLocation::class.java.classLoader - getParcelable<TraceLocation>("test") - } - restoredData shouldBe expectedTraceLocation -} - -@Test -fun verifyEventStartTimeWaning() = runBlockingTest { - val instant = Instant.ofEpochMilli(2687940 * 1_000L) - shouldNotThrowAny { - val verifyResult = traceLocationQRCodeVerifier.verify(ENCODED_EVENT1.decodeBase32().toByteArray()) - verifyResult.apply { - traceLocation.description shouldBe "My Birthday Party" - traceLocation.isBeforeStartTime(instant) shouldBe true - traceLocation.isAfterEndTime(instant) shouldBe false - } - } -} + fun verifyTraceLocationMapping2() { + val qrCodePayload = TraceLocationOuterClass.QRCodePayload.parseFrom( + BASE64_PAYLOAD_2.decodeBase64()!!.toByteArray() + ) -@Test -fun verifyEventEndTimeWarning() = runBlockingTest { - val instant = Instant.now() - shouldNotThrowAny { - val verifyResult = traceLocationQRCodeVerifier.verify(ENCODED_EVENT1.decodeBase32().toByteArray()) - verifyResult.apply { - traceLocation.description shouldBe "My Birthday Party" - traceLocation.isBeforeStartTime(instant) shouldBe false - traceLocation.isAfterEndTime(instant) shouldBe true - } + VerifiedTraceLocation(qrCodePayload).traceLocation + .apply { + locationId.base64() shouldBe "GMuCjqNmOdYyrFhyvFNTVEeLaZh+uShgUoY0LYJo4YQ=" + qrCodePayload() shouldBe qrCodePayload + } } -} -@Test -fun verifyEventWithInvalidKey() = runBlockingTest { - every { environmentSetup.appConfigVerificationKey } returns INVALID_PUB_KEY - shouldThrow<InvalidQRCodeSignatureException> { - traceLocationQRCodeVerifier.verify(ENCODED_EVENT1.decodeBase32().toByteArray()) - } -} - -@Test -fun eventHasMalformedData() = runBlockingTest { - shouldThrow<InvalidQRCodeDataException> { - traceLocationQRCodeVerifier.verify( - INVALID_ENCODED_EVENT.decodeBase32().toByteArray() + @Test + fun parcelization() { + val qrCodePayload = TraceLocationOuterClass.QRCodePayload.parseFrom( + BASE64_PAYLOAD_2.decodeBase64()!!.toByteArray() ) - } -} -@Test -fun decodingTest1() = runBlockingTest { - val signedTraceLocation = TraceLocationOuterClass.SignedTraceLocation.parseFrom( - ENCODED_EVENT1.decodeBase32().toByteArray() - ) - val expectedSignature = - "MEQCIGVKfqPF2851IrEyDeVMazlRnIzLX16H6r1TB37PRzjbAiBGP13ADQcbQZsztKUCZMRcvnv5Mgdo0LY/v3qFMnrUkQ==" + val expectedVerifiedLocation = VerifiedTraceLocation(qrCodePayload) - val base32 = signedTraceLocation.toByteArray().toByteString().base32() - - shouldNotThrowAny { - val verifyResult = traceLocationQRCodeVerifier.verify(base32.decodeBase32().toByteArray()) - - verifyResult.apply { - traceLocation.description shouldBe "My Birthday Party" - signedTraceLocation.signature.toByteArray().toByteString().base64() shouldBe expectedSignature + val bundle = Bundle().apply { + putParcelable("verifiedTraceLocation", expectedVerifiedLocation) } - } -} -@Test -fun decodingTest2() = runBlockingTest { - val signedTraceLocation = TraceLocationOuterClass.SignedTraceLocation.parseFrom( - ENCODED_EVENT2.decodeBase32().toByteArray() - ) - val expectedSignature = - "MEQCIDWRTM4ujn1GFPuHlgpUnQIWwwzwI8abxSrF5Er2I5HaAiAbucxg+6d3nC/Iwzo7AXehJAS20TRX1S2rl0LO8kcYxA==" + val parcelRaw = Parcel.obtain().apply { + writeBundle(bundle) + }.marshall() - val base32 = signedTraceLocation.toByteArray().toByteString().base32() - - shouldNotThrowAny { - val verifyResult = traceLocationQRCodeVerifier.verify(base32.decodeBase32().toByteArray()) - - verifyResult.apply { - traceLocation.description shouldBe "Icecream Shop" - signedTraceLocation.signature.toByteArray().toByteString().base64() shouldBe expectedSignature + val restoredParcel = Parcel.obtain().apply { + unmarshall(parcelRaw, 0, parcelRaw.size) + setDataPosition(0) } - } -} -@Test -fun testVerifiedTraceLocationMapping() { - shouldNotThrowAny { - val signedTraceLocation = TraceLocationOuterClass.SignedTraceLocation.parseFrom( - ENCODED_EVENT1.decodeBase32().toByteArray() - ) - - val traceLocation = TraceLocationOuterClass.TraceLocation.parseFrom( - ENCODED_EVENT1_LOCATION.decodeBase32().toByteArray() - ) - val verifiedTraceLocation = VerifiedTraceLocation( - protoSignedTraceLocation = signedTraceLocation, - protoTraceLocation = traceLocation - ).traceLocation - - verifiedTraceLocation shouldBe TraceLocation( - guid = "3055331c-2306-43f3-9742-6d8fab54e848", - version = 1, - type = TraceLocationOuterClass.TraceLocationType.LOCATION_TYPE_TEMPORARY_OTHER, - description = "My Birthday Party", - address = "at my place", - startDate = Instant.ofEpochSecond(2687955), - endDate = Instant.ofEpochSecond(2687991), - defaultCheckInLengthInMinutes = 0, - byteRepresentation = signedTraceLocation.location.toByteArray().toByteString(), - signature = signedTraceLocation.signature.toByteArray().toByteString() - ) + val restoredData = restoredParcel.readBundle()!!.run { + classLoader = VerifiedTraceLocation::class.java.classLoader + getParcelable<VerifiedTraceLocation>("verifiedTraceLocation") + } + restoredData shouldBe expectedVerifiedLocation } -} - -*/ companion object { + private const val BASE64_PAYLOAD_1 = "CAESLAgBEhFNeSBCaXJ0aGRheSBQYXJ0eRoLYXQgbXkgcGxhY2Uo04ekAT" + + "D3h6QBGmUIARJbMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEc7DEst" + + "cUIRcyk35OYDJ95/hTg3UVhsaDXKT0zK7NhHPXoyzipEnOp3GyNXDVpaPi3" + + "cAfQmxeuFMZAIX2+6A5XhoEMTIzNCIECAEQAg==" - // "signedLocation": { - // "location": { - // "guid": "3055331c-2306-43f3-9742-6d8fab54e848", - // "version": 1, - // "type": 2, - // "description": "My Birthday Party", - // "address": "at my place", - // "startTimestamp": 2687955, - // "endTimestamp": 2687991, - // "defaultCheckInLengthInMinutes": 0 - // }, - // "signature": "MEQCIGVKfqPF2851IrEyDeVMazlRnIzLX16H6r1TB37PRzjbAiBGP13ADQcbQZsztKUCZMRcvnv5Mgdo0LY/v3qFMnrUkQ==" - private const val ENCODED_EVENT1 = - "BJLAUJBTGA2TKMZTGFRS2MRTGA3C2NBTMYZS2OJXGQZC2NTEHBTGCYRVGRSTQNBYCAARQARCCFGXSICCNFZHI2DEMF4SAUDBOJ2HSKQLMF2CA3LZEBYGYYLDMUYNHB5EAE4PPB5EAFAAAESGGBCAEIDFJJ7KHRO3ZZ2SFMJSBXSUY2ZZKGOIZS27L2D6VPKTA57M6RZY3MBCARR7LXAA2BY3IGNTHNFFAJSMIXF6PP4TEB3I2C3D7P32QUZHVVER" - private const val ENCODED_EVENT1_LOCATION = - "BISDGMBVGUZTGMLDFUZDGMBWFU2DGZRTFU4TONBSFU3GIODGMFRDKNDFHA2DQEABDABCEEKNPEQEE2LSORUGIYLZEBIGC4TUPEVAWYLUEBWXSIDQNRQWGZJQ2OD2IAJY66D2IAKAAA" - - // "signedLocation": { - // "location": { - // "guid": "fca84b37-61c0-4a7c-b2f8-825cadd506cf", - // "version": 1, - // "type": 1, - // "description": "Icecream Shop", - // "address": "Main Street 1", - // "startTimestamp": 0, - // "endTimestamp": 0, - // "defaultCheckInLengthInMinutes": 10 - // }, - // "signature": "MEQCIDWRTM4ujn1GFPuHlgpUnQIWwwzwI8abxSrF5Er2I5HaAiAbucxg+6d3nC/Iwzo7AXehJAS20TRX1S2rl0LO8kcYxA==" - private const val ENCODED_EVENT2 = - "BJHAUJDGMNQTQNDCGM3S2NRRMMYC2NDBG5RS2YRSMY4C2OBSGVRWCZDEGUYDMY3GCAARQAJCBVEWGZLDOJSWC3JAKNUG64BKBVGWC2LOEBJXI4TFMV2CAMJQAA4AAQAKCJDDARACEA2ZCTGOF2HH2RQU7ODZMCSUTUBBNQYM6AR4NG6FFLC6ISXWEOI5UARADO44YYH3U53ZYL6IYM5DWALXUESAJNWRGRL5KLNLS5BM54SHDDCA" - - private const val INVALID_ENCODED_EVENT = - "NB2HI4DTHIXS653XO4XHK4TCMFXGI2LDORUW63TBOJ4S4Y3PNUXWIZLGNFXGKLTQNBYD65DFOJWT2VDIMUSTEMCDN53GSZBFGIYDCOI=" - - private const val PUB_KEY = - "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEafIKZOiRPuJWjKOUmKv7OTJWTyii4oCQLcGn3FgYoLQaJIvAM3Pl7anFDPPY/jxfqqrLyGc0f6hWQ9JPR3QjBw==" - private const val INVALID_PUB_KEY = - "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEc7DEstcUIRcyk35OYDJ95/hTg3UVhsaDXKT0zK7NhHPXoyzipEnOp3GyNXDVpaPi3cAfQmxeuFMZAIX2+6A5Xg==" + private const val BASE64_PAYLOAD_2 = "CAESIAgBEg1JY2VjcmVhbSBTaG9wGg1NYWluIFN0cmVldCAxGmUIARJ" + + "bMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEc7DEstcUIRcyk35OYDJ95/hTg3UVhsaDXKT" + + "0zK7NhHPXoyzipEnOp3GyNXDVpaPi3cAfQmxeuFMZAIX2+6A5XhoEMTIzNCIGCAEQARgK" } } 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 index 500e914851840d734dbeb11f16f85fbd1dd5d42c..370276daf4fe3f5f3dae2b3a188cd403f52488b2 100644 --- 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 @@ -9,7 +9,6 @@ object CheckInDatabaseData { val testCheckIn = TraceLocationCheckInEntity( traceLocationIdBase64 = "traceLocationId1".encode().base64(), - traceLocationIdHashBase64 = "traceLocationIdHash1".encode().base64(), version = 1, type = TraceLocationOuterClass.TraceLocationType.LOCATION_TYPE_TEMPORARY_OTHER.number, description = "testDescription1", @@ -27,7 +26,6 @@ object CheckInDatabaseData { val testCheckInWithoutCheckOutTime = TraceLocationCheckInEntity( traceLocationIdBase64 = "traceLocationId1".encode().base64(), - traceLocationIdHashBase64 = "traceLocationIdHash1".encode().base64(), version = 1, type = TraceLocationOuterClass.TraceLocationType.LOCATION_TYPE_TEMPORARY_OTHER.number, description = "testDescription2", diff --git a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/color/ColorTest.kt b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/color/ColorTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..039b5d6bd7798fbdcd59c860b03e1f9b9dc08356 --- /dev/null +++ b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/color/ColorTest.kt @@ -0,0 +1,25 @@ +package de.rki.coronawarnapp.ui.color + +import android.graphics.Color +import io.kotest.matchers.shouldBe +import org.junit.Test + +import testhelpers.BaseTestInstrumentation + +class ColorTest : BaseTestInstrumentation() { + + @Test + fun parseValidColor() { + "#FFFFFF".parseColor() shouldBe Color.WHITE + } + + @Test + fun parseInvalidColor() { + "000000".parseColor() shouldBe Color.BLACK + } + + @Test + fun defaultColor() { + "00".parseColor(Color.GRAY) shouldBe Color.GRAY + } +} diff --git a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/contactdiary/ContactDiaryOverviewFragmentTest.kt b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/contactdiary/ContactDiaryOverviewFragmentTest.kt index 3d7bc4e5fbd13ea12bd820192049a5576089e59d..4038ad7448406bca7ff8339defe6b6089175da6f 100644 --- a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/contactdiary/ContactDiaryOverviewFragmentTest.kt +++ b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/contactdiary/ContactDiaryOverviewFragmentTest.kt @@ -16,6 +16,7 @@ import de.rki.coronawarnapp.contactdiary.ui.overview.adapter.DiaryOverviewItem import de.rki.coronawarnapp.contactdiary.ui.overview.adapter.day.DayOverviewItem import de.rki.coronawarnapp.contactdiary.ui.overview.adapter.day.contact.ContactItem import de.rki.coronawarnapp.contactdiary.ui.overview.adapter.subheader.OverviewSubHeaderItem +import de.rki.coronawarnapp.eventregistration.checkins.CheckInRepository import de.rki.coronawarnapp.risk.storage.RiskLevelStorage import de.rki.coronawarnapp.task.TaskController import de.rki.coronawarnapp.util.TimeStamper @@ -47,6 +48,7 @@ class ContactDiaryOverviewFragmentTest : BaseUITest() { @MockK lateinit var riskLevelStorage: RiskLevelStorage @MockK lateinit var timeStamper: TimeStamper @MockK lateinit var exporter: ContactDiaryExporter + @MockK lateinit var checkInRepository: CheckInRepository private lateinit var viewModel: ContactDiaryOverviewViewModel @@ -63,6 +65,7 @@ class ContactDiaryOverviewFragmentTest : BaseUITest() { contactDiaryRepository = contactDiaryRepository, riskLevelStorage = riskLevelStorage, timeStamper = timeStamper, + checkInRepository = checkInRepository, exporter = exporter ) ) diff --git a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/contactdiary/DiaryData.kt b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/contactdiary/DiaryData.kt index 49c71ca142afd1334e8f68d8f1e24e26d6de6996..eddad5048b6c161ac5d076c2d3ace5972b35a58e 100644 --- a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/contactdiary/DiaryData.kt +++ b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/contactdiary/DiaryData.kt @@ -85,12 +85,14 @@ object DiaryData { val HIGH_RISK_EVENT = RiskEventItem.Event( name = HIGH_RISK_EVENT_LOCATION.name, + description = "2", bulledPointColor = R.color.colorBulletPointHighRisk, riskInfoAddition = R.string.contact_diary_trace_location_risk_high ) val LOW_RISK_EVENT = RiskEventItem.Event( name = LOW_RISK_EVENT_LOCATION.name, + description = "1", bulledPointColor = R.color.colorBulletPointLowRisk, riskInfoAddition = R.string.contact_diary_trace_location_risk_low ) diff --git a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/util/security/VerificationKeysTest.kt b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/util/security/VerificationKeysTest.kt index 8e8f547182033c77982535486201654ff27f7619..600842aae293c6ced478cda49a7f683f0c290715 100644 --- a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/util/security/VerificationKeysTest.kt +++ b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/util/security/VerificationKeysTest.kt @@ -20,7 +20,7 @@ class VerificationKeysTest { @Before fun setup() { MockKAnnotations.init(this) - every { environmentSetup.appConfigVerificationKey } returns PUB_KEY + every { environmentSetup.appConfigPublicKey } returns PUB_KEY } private fun createTool() = SignatureValidation(environmentSetup) diff --git a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/worker/DiagnosisKeyRetrievalPeriodicWorkerTest.kt b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/worker/DiagnosisKeyRetrievalPeriodicWorkerTest.kt deleted file mode 100644 index bc8b3025f2bb742d1bb7835ecdfd64c675c0e2ff..0000000000000000000000000000000000000000 --- a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/worker/DiagnosisKeyRetrievalPeriodicWorkerTest.kt +++ /dev/null @@ -1,150 +0,0 @@ -package de.rki.coronawarnapp.worker - -import android.content.Context -import androidx.test.core.app.ApplicationProvider -import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.work.WorkInfo -import androidx.work.WorkManager -import androidx.work.WorkRequest -import androidx.work.testing.TestDriver -import androidx.work.testing.WorkManagerTestInitHelper -import io.mockk.Runs -import io.mockk.every -import io.mockk.just -import io.mockk.mockkObject -import org.hamcrest.CoreMatchers.`is` -import org.hamcrest.CoreMatchers.notNullValue -import org.hamcrest.CoreMatchers.nullValue -import org.hamcrest.MatcherAssert.assertThat -import org.junit.After -import org.junit.Before -import org.junit.Ignore -import org.junit.Test -import org.junit.runner.RunWith -import testhelpers.BaseTestInstrumentation - -/** - * DiagnosisKeyRetrievalPeriodicWorker test. - */ -@Ignore("FixMe:DiagnosisKeyRetrievalPeriodicWorkerTest") -@RunWith(AndroidJUnit4::class) -class DiagnosisKeyRetrievalPeriodicWorkerTest : BaseTestInstrumentation() { - private lateinit var context: Context - private lateinit var workManager: WorkManager - private lateinit var request: WorkRequest - private lateinit var request2: WorkRequest - - // small delay because WorkManager does not run work instantly when delay is off - private val delay = 500L - - @Before - fun setUp() { - mockkObject(BackgroundWorkScheduler) - // do not init Test WorkManager instance again between tests - // leads to all tests instead of first one to fail - context = ApplicationProvider.getApplicationContext() - if (WorkManager.getInstance(context) !is TestDriver) { - WorkManagerTestInitHelper.initializeTestWorkManager(context) - } - workManager = WorkManager.getInstance(context) - - every { BackgroundWorkScheduler["buildDiagnosisKeyRetrievalPeriodicWork"]() } answers { - request = this.callOriginal() as WorkRequest - request - } - } - - /** - * Test worker for success. - */ - @Test - fun testDiagnosisKeyRetrievalPeriodicWorkerSuccess() { - every { BackgroundWorkScheduler.scheduleDiagnosisKeyOneTimeWork() } just Runs - - BackgroundWorkScheduler.scheduleDiagnosisKeyPeriodicWork() - - assertThat(request, notNullValue()) - - var workInfo = workManager.getWorkInfoById(request.id).get() - assertThat(workInfo, notNullValue()) - assertThat(workInfo.state, `is`((WorkInfo.State.ENQUEUED))) - - runPeriodicJobInitialDelayMet() - assertThat(request, notNullValue()) - workInfo = workManager.getWorkInfoById(request.id).get() - assertThat(workInfo.runAttemptCount, `is`(0)) - - runPeriodicJobPeriodDelayMet() - assertThat(request, notNullValue()) - workInfo = workManager.getWorkInfoById(request.id).get() - assertThat(workInfo.runAttemptCount, `is`(0)) - } - - /** - * Test worker for retries and fail. - */ - @Test - fun testDiagnosisKeyRetrievalPeriodicWorkerRetryAndFail() { - every { BackgroundWorkScheduler.scheduleDiagnosisKeyOneTimeWork() } throws Exception("test exception") - - BackgroundWorkScheduler.scheduleDiagnosisKeyPeriodicWork() - - assertThat(request, notNullValue()) - var workInfo = workManager.getWorkInfoById(request.id).get() - assertThat(workInfo, notNullValue()) - assertThat(workInfo.state, `is`((WorkInfo.State.ENQUEUED))) - - for (i in 1..BackgroundConstants.WORKER_RETRY_COUNT_THRESHOLD + 2) { - // run job i times - when (i) { - BackgroundConstants.WORKER_RETRY_COUNT_THRESHOLD + 2 -> { - every { BackgroundWorkScheduler["buildDiagnosisKeyRetrievalPeriodicWork"]() } answers { - request2 = this.callOriginal() as WorkRequest - request2 - } - runPeriodicJobInitialDelayMet() - } - else -> { - runPeriodicJobInitialDelayMet() - } - } - - // get job run #i result - when (i) { - BackgroundConstants.WORKER_RETRY_COUNT_THRESHOLD + 2 -> { - assertThat(request, notNullValue()) - assertThat(request2, notNullValue()) - workInfo = workManager.getWorkInfoById(request.id).get() - val workInfo2 = workManager.getWorkInfoById(request2.id).get() - assertThat(workInfo, nullValue()) - assertThat(workInfo2.state, `is`(WorkInfo.State.ENQUEUED)) - assertThat(workInfo2.runAttemptCount, `is`(0)) - } - else -> { - assertThat(request, notNullValue()) - workInfo = workManager.getWorkInfoById(request.id).get() - assertThat(workInfo.runAttemptCount, `is`(i)) - } - } - } - } - - @After - fun cleanUp() { - workManager.cancelAllWork() - } - - private fun runPeriodicJobInitialDelayMet() { - val testDriver = WorkManagerTestInitHelper.getTestDriver(context) - testDriver?.setAllConstraintsMet(request.id) - testDriver?.setInitialDelayMet(request.id) - Thread.sleep(delay) - } - - private fun runPeriodicJobPeriodDelayMet() { - val testDriver = WorkManagerTestInitHelper.getTestDriver(context) - testDriver?.setAllConstraintsMet(request.id) - testDriver?.setPeriodDelayMet(request.id) - Thread.sleep(delay) - } -} diff --git a/Corona-Warn-App/src/device/java/de/rki/coronawarnapp/risk/storage/DefaultRiskLevelStorage.kt b/Corona-Warn-App/src/device/java/de/rki/coronawarnapp/risk/storage/DefaultRiskLevelStorage.kt index c88e6445be335789386094922c591394762337bf..f8ac9f20dbc3afc5118bb6e262863d392401a67c 100644 --- a/Corona-Warn-App/src/device/java/de/rki/coronawarnapp/risk/storage/DefaultRiskLevelStorage.kt +++ b/Corona-Warn-App/src/device/java/de/rki/coronawarnapp/risk/storage/DefaultRiskLevelStorage.kt @@ -1,6 +1,6 @@ package de.rki.coronawarnapp.risk.storage -import de.rki.coronawarnapp.presencetracing.risk.PresenceTracingRiskRepository +import de.rki.coronawarnapp.presencetracing.risk.storage.PresenceTracingRiskRepository import de.rki.coronawarnapp.risk.EwRiskLevelResult import de.rki.coronawarnapp.risk.storage.internal.RiskResultDatabase import de.rki.coronawarnapp.util.coroutine.AppScope diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/risk/storage/DefaultRiskLevelStorage.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/risk/storage/DefaultRiskLevelStorage.kt index ee15ef5abd872e3749674aa965c61f40a35986a2..7fbb56a67d86ed6d29371dec56f158905536068e 100644 --- a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/risk/storage/DefaultRiskLevelStorage.kt +++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/risk/storage/DefaultRiskLevelStorage.kt @@ -1,6 +1,6 @@ package de.rki.coronawarnapp.risk.storage -import de.rki.coronawarnapp.presencetracing.risk.PresenceTracingRiskRepository +import de.rki.coronawarnapp.presencetracing.risk.storage.PresenceTracingRiskRepository import de.rki.coronawarnapp.risk.EwRiskLevelResult import de.rki.coronawarnapp.risk.storage.internal.RiskResultDatabase import de.rki.coronawarnapp.risk.storage.internal.windows.PersistedExposureWindowDao.PersistedScanInstance diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/debugoptions/ui/DebugOptionsFragment.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/debugoptions/ui/DebugOptionsFragment.kt index 42200d5b710b1ddab8e1923a2b5b16f45693aa37..638755b2c4ddae1e3f0355209ccd4f635023816a 100644 --- a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/debugoptions/ui/DebugOptionsFragment.kt +++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/debugoptions/ui/DebugOptionsFragment.kt @@ -70,7 +70,9 @@ class DebugOptionsFragment : Fragment(R.layout.fragment_test_debugoptions), Auto environmentCdnurlSubmission.text = "Submission CDN:\n${state.urlSubmission}" environmentCdnurlVerification.text = "Verification CDN:\n${state.urlVerification}" environmentUrlDatadonation.text = "DataDonation:\n${state.urlDataDonation}" - environmentUrlQrcodePosterTemplate.text = "QR-Code Poster Template:\n${state.urlQrCodePosterTemplate}" + environmentUrlLogUpload.text = "LogUpload:\n${state.urlLogUpload}" + environmentPubkeyCrowdnotifier.text = "CrowdNotifierPubKey:\n${state.pubKeyCrowdNotifier}" + environmentPubkeyAppconfig.text = "AppConfigPubKey:\n${state.pubKeyAppConfig}" } } vm.environmentChangeEvent.observe2(this) { diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/debugoptions/ui/EnvironmentState.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/debugoptions/ui/EnvironmentState.kt index 3d72f93a074ce0bdeffa9323f695944f54aac039..c9b907793ab6812a252f519c71a9bee66af2387a 100644 --- a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/debugoptions/ui/EnvironmentState.kt +++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/debugoptions/ui/EnvironmentState.kt @@ -9,8 +9,9 @@ data class EnvironmentState( val urlDownload: String, val urlVerification: String, val urlDataDonation: String, - val urlQrCodePosterTemplate: String - + val urlLogUpload: String, + val pubKeyCrowdNotifier: String, + val pubKeyAppConfig: String, ) { companion object { internal fun EnvironmentSetup.toEnvironmentState() = EnvironmentState( @@ -20,7 +21,9 @@ data class EnvironmentState( urlDownload = downloadCdnUrl, urlVerification = verificationCdnUrl, urlDataDonation = dataDonationCdnUrl, - urlQrCodePosterTemplate = qrCodePosterTemplateCdnUrl + urlLogUpload = logUploadServerUrl, + pubKeyCrowdNotifier = crowdNotifierPublicKey, + pubKeyAppConfig = appConfigPublicKey, ) } } 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 index beb72df52a2358f5258e42221e0db7f1237d8737..95d522e04234c83bbafcb5d659bf3b60cca610e0 100644 --- 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 @@ -2,13 +2,19 @@ package de.rki.coronawarnapp.test.eventregistration.ui import android.annotation.SuppressLint import android.os.Bundle +import android.text.SpannedString import android.view.View -import android.widget.Toast +import androidx.core.text.bold +import androidx.core.text.buildSpannedString +import androidx.core.text.color +import androidx.core.text.scale +import androidx.core.view.isVisible 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.eventregistration.checkins.qrcode.TraceLocation import de.rki.coronawarnapp.test.menu.ui.TestMenuItem +import de.rki.coronawarnapp.util.ContextExtensions.getColorCompat import de.rki.coronawarnapp.util.di.AutoInject import de.rki.coronawarnapp.util.ui.doNavigate import de.rki.coronawarnapp.util.ui.observe2 @@ -28,62 +34,96 @@ class EventRegistrationTestFragment : Fragment(R.layout.fragment_test_eventregis 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) - } - - generateTestTraceLocations.setOnClickListener { - viewModel.generateTestTraceLocations() - } - } - binding.runMatcher.setOnClickListener { - viewModel.runMatcher() - } - - binding.downloadReportedCheckIns.setOnClickListener { - Toast.makeText(context, "Not implemented", Toast.LENGTH_SHORT).show() + binding.resetProcessedWarningPackages.setOnClickListener { + viewModel.resetProcessedWarningPackages() } binding.calculateRisk.setOnClickListener { viewModel.runRiskCalculationPerCheckInDay() } - viewModel.checkInOverlapsText.observe2(this) { - binding.matchingResultText.text = it + viewModel.presenceTracingWarningTaskResult.observe2(this) { + binding.tracingWarningTaskResult.text = it } viewModel.checkInRiskPerDayText.observe2(this) { binding.riskCalculationResultText.text = it } - viewModel.matchingRuntime.observe2(this) { - binding.matchingRuntimeText.text = "Matching runtime in millis: $it" + viewModel.taskRunTime.observe2(this) { + binding.taskRunTime.text = "Task finished in ${it}ms" } viewModel.riskCalculationRuntime.observe2(this) { binding.riskCalculationRuntimeText.text = "Risk calculation runtime in millis: $it" } + + binding.runPtWarningTask.setOnClickListener { + viewModel.runPresenceTracingWarningTask() + } + + viewModel.lastOrganiserLocation.observe(viewLifecycleOwner) { + binding.lastOrganiserLocationCard.isVisible = it != null + it?.let { traceLocation -> + with(binding) { + lastOrganiserLocation.text = traceLocationText(traceLocation) + lastOrganiserLocationId.text = styleText("ID", traceLocation.locationId.base64()) + lastOrganiserLocationUrl.text = styleText("URL", traceLocation.locationUrl) + qrcodeButton.setOnClickListener { + doNavigate( + EventRegistrationTestFragmentDirections + .actionEventRegistrationTestFragmentToQrCodePosterFragmentTest(traceLocation.id) + ) + } + } + } + } + + viewModel.lastAttendeeLocation.observe(viewLifecycleOwner) { + binding.lastAttendeeLocationCard.isVisible = it != null + it?.let { traceLocation -> + with(binding) { + lastAttendeeLocation.text = traceLocationText(traceLocation) + lastAttendeeLocationId.text = styleText("ID", traceLocation.locationId.base64()) + lastAttendeeLocationUrl.text = styleText("URL", traceLocation.locationUrl) + } + } + } + } + + private fun traceLocationText(traceLocation: TraceLocation): SpannedString = with(traceLocation) { + buildSpannedString { + append("TraceLocation [\n") + append(styleText("Id", id)) + append(styleText("type", type)) + append(styleText("version", version)) + append(styleText("address", address)) + append(styleText("description", description)) + append(styleText("startDate", startDate)) + append(styleText("endDate", endDate)) + append(styleText("defaultCheckInLengthInMinutes", defaultCheckInLengthInMinutes)) + append(styleText("cnPublicKey", cnPublicKey)) + append(styleText("cryptographicSeed", cryptographicSeed.base64())) + append("]") + } } + private fun styleText(key: String, value: Any?): SpannedString = + buildSpannedString { + bold { + color(requireContext().getColorCompat(R.color.colorAccent)) { + append("$key=") + } + } + + scale(0.85f) { + color(requireContext().getColorCompat(R.color.colorTextPrimary1)) { + append(value.toString()) + } + } + append("\n") + } + companion object { val MENU_ITEM = TestMenuItem( title = "Event Registration", 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 index a87c13dc9c2aadbd6399aad83b2add45605cad64..3913503fe7082eb9597ce0f5b8d136bd9d0513e1 100644 --- 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 @@ -1,65 +1,94 @@ package de.rki.coronawarnapp.test.eventregistration.ui +import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.asLiveData import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject +import de.rki.coronawarnapp.eventregistration.checkins.CheckIn +import de.rki.coronawarnapp.eventregistration.checkins.CheckInRepository import de.rki.coronawarnapp.eventregistration.checkins.qrcode.TraceLocation import de.rki.coronawarnapp.eventregistration.storage.repo.TraceLocationRepository -import de.rki.coronawarnapp.presencetracing.risk.CheckInWarningMatcher -import de.rki.coronawarnapp.presencetracing.risk.CheckInWarningOverlap -import de.rki.coronawarnapp.presencetracing.risk.PresenceTracingRiskCalculator +import de.rki.coronawarnapp.presencetracing.risk.calculation.PresenceTracingRiskCalculator +import de.rki.coronawarnapp.presencetracing.risk.execution.PresenceTracingWarningTask +import de.rki.coronawarnapp.presencetracing.risk.storage.PresenceTracingRiskRepository +import de.rki.coronawarnapp.presencetracing.warning.storage.TraceWarningRepository import de.rki.coronawarnapp.server.protocols.internal.pt.TraceLocationOuterClass +import de.rki.coronawarnapp.task.TaskController +import de.rki.coronawarnapp.task.common.DefaultTaskRequest +import de.rki.coronawarnapp.task.submitBlocking import de.rki.coronawarnapp.util.coroutine.DispatcherProvider import de.rki.coronawarnapp.util.debug.measureTime import de.rki.coronawarnapp.util.viewmodel.CWAViewModel import de.rki.coronawarnapp.util.viewmodel.SimpleCWAViewModelFactory -import okio.ByteString.Companion.encode -import org.joda.time.DateTime +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.map import timber.log.Timber +import kotlin.system.measureTimeMillis class EventRegistrationTestFragmentViewModel @AssistedInject constructor( - private val dispatcherProvider: DispatcherProvider, - private val traceLocationRepository: TraceLocationRepository, - private val checkInWarningMatcher: CheckInWarningMatcher, - private val presenceTracingRiskCalculator: PresenceTracingRiskCalculator + dispatcherProvider: DispatcherProvider, + traceLocationRepository: TraceLocationRepository, + checkInRepository: CheckInRepository, + private val presenceTracingRiskCalculator: PresenceTracingRiskCalculator, + private val taskController: TaskController, + private val presenceTracingRiskRepository: PresenceTracingRiskRepository, + private val traceWarningRepository: TraceWarningRepository ) : CWAViewModel(dispatcherProvider = dispatcherProvider) { - private val checkInWarningOverlaps = mutableListOf<CheckInWarningOverlap>() - val checkInOverlapsText = MutableLiveData<String>() - val matchingRuntime = MutableLiveData<Long>() - val riskCalculationRuntime = MutableLiveData<Long>() + val lastOrganiserLocation: LiveData<TraceLocation?> = + traceLocationRepository.allTraceLocations + .map { lastLocationData(it) } + .asLiveData(dispatcherProvider.Default) - val checkInRiskPerDayText = MutableLiveData<String>() + val lastAttendeeLocation: LiveData<TraceLocation?> = + checkInRepository.allCheckIns + .map { lastAttendeeLocationData(it) } + .asLiveData(dispatcherProvider.Default) - fun runMatcher() { - launch { - measureTime( - { - Timber.d("Time to find matches: $it millis") - matchingRuntime.postValue(it) - }, - { - checkInWarningOverlaps.clear() - val matches = checkInWarningMatcher.execute() + val presenceTracingWarningTaskResult = MutableLiveData<String>() + val taskRunTime = MutableLiveData<Long>() + val riskCalculationRuntime = MutableLiveData<Long>() - checkInWarningOverlaps.addAll(matches) + val checkInRiskPerDayText = MutableLiveData<String>() - if (checkInWarningOverlaps.size < 100) { - val text = checkInWarningOverlaps.fold(StringBuilder()) { stringBuilder, checkInOverlap -> - stringBuilder - .append("CheckIn Id ${checkInOverlap.checkInId}, ") - .append("Date ${checkInOverlap.localDateUtc}, ") - .append("Min. ${checkInOverlap.overlap.standardMinutes}") - .append("\n") - } - if (text.isBlank()) checkInOverlapsText.postValue("No matches found") - else checkInOverlapsText.postValue(text.toString()) - } else { - checkInOverlapsText.postValue("Output too large. ${checkInWarningOverlaps.size} lines") - } - } + fun runPresenceTracingWarningTask() = launch { + Timber.d("runWarningPackageTask()") + presenceTracingWarningTaskResult.postValue("Running") + taskRunTime.postValue(-1L) + + val duration = measureTimeMillis { + taskController.submitBlocking( + DefaultTaskRequest( + PresenceTracingWarningTask::class, + originTag = "EventRegistrationTestFragmentViewModel" + ) ) } + taskRunTime.postValue(duration) + + val warningPackages = traceWarningRepository.allMetaData.first() + val overlaps = presenceTracingRiskRepository.checkInWarningOverlaps.first() + val lastResult = presenceTracingRiskRepository.latestEntries(1).first().singleOrNull() + + val infoText = when { + !lastResult!!.wasSuccessfullyCalculated -> "Last calculation failed" + overlaps.isEmpty() -> "No matches found (${warningPackages.size} warning packages)." + overlaps.size > 100 -> "Output too large. ${overlaps.size} lines" + overlaps.isNotEmpty() -> overlaps.fold(StringBuilder()) { stringBuilder, checkInOverlap -> + stringBuilder + .append("CheckIn Id ${checkInOverlap.checkInId}, ") + .append("Date ${checkInOverlap.localDateUtc}, ") + .append("Min. ${checkInOverlap.overlap.standardMinutes}") + .appendLine() + }.toString() + else -> "Unknown state" + } + presenceTracingWarningTaskResult.postValue(infoText) + } + + fun resetProcessedWarningPackages() = launch { + traceWarningRepository.clear() } fun runRiskCalculationPerCheckInDay() { @@ -70,6 +99,7 @@ class EventRegistrationTestFragmentViewModel @AssistedInject constructor( riskCalculationRuntime.postValue(it) }, { + val checkInWarningOverlaps = presenceTracingRiskRepository.checkInWarningOverlaps.first() val normalizedTimePerCheckInDayList = presenceTracingRiskCalculator.calculateNormalizedTime(checkInWarningOverlaps) val riskStates = @@ -93,92 +123,24 @@ class EventRegistrationTestFragmentViewModel @AssistedInject constructor( } } - fun generateTestTraceLocations() { - launch { - val permanent = TraceLocation( - type = TraceLocationOuterClass.TraceLocationType.LOCATION_TYPE_PERMANENT_FOOD_SERVICE, - description = "SAP Kantine WDF20", - address = "Hauptstr. 3, 69115 Heidelberg", - startDate = null, - endDate = null, - defaultCheckInLengthInMinutes = 60, - cryptographicSeed = "".encode(), - cnPublicKey = "" - ) - traceLocationRepository.addTraceLocation(permanent) - - val oneDayEvent = TraceLocation( - type = TraceLocationOuterClass.TraceLocationType.LOCATION_TYPE_TEMPORARY_CULTURAL_EVENT, - description = "Jahrestreffen der deutschen SAP Anwendergruppe (one day)", - address = "Hauptstr. 3, 69115 Heidelberg", - startDate = DateTime.now().plusHours(2).toInstant(), - endDate = DateTime.now().plusHours(3).toInstant(), - defaultCheckInLengthInMinutes = 60, - cryptographicSeed = "".encode(), - cnPublicKey = "" - ) - traceLocationRepository.addTraceLocation(oneDayEvent) - - val partyHardEvent = TraceLocation( - type = TraceLocationOuterClass.TraceLocationType.LOCATION_TYPE_TEMPORARY_CLUB_ACTIVITY, - description = "Jahrestreffen der deutschen SAP Anwendergruppe (many days)", - address = "Hauptstr. 3, 69115 Heidelberg", - startDate = DateTime.now().plusHours(2).toInstant(), - endDate = DateTime.now().plusDays(5).plusHours(2).toInstant(), - defaultCheckInLengthInMinutes = 60, - cryptographicSeed = "".encode(), - cnPublicKey = "" - ) - traceLocationRepository.addTraceLocation(partyHardEvent) - - val oldPermanent = TraceLocation( - type = TraceLocationOuterClass.TraceLocationType.LOCATION_TYPE_PERMANENT_FOOD_SERVICE, - description = "SAP Kantine MOW07", - address = "Moscow, Kosmodomianskaya 52/7", - startDate = null, - endDate = null, - defaultCheckInLengthInMinutes = 60, - cryptographicSeed = "".encode(), - cnPublicKey = "" - ) - traceLocationRepository.addTraceLocation(oldPermanent) - - val oldTemporaryOne = TraceLocation( - type = TraceLocationOuterClass.TraceLocationType.LOCATION_TYPE_TEMPORARY_CLUB_ACTIVITY, - description = "Old temporary 1", - address = "Hauptstr. 3, 69115 Heidelberg", - startDate = DateTime.now().minusSeconds(16 * 86400).toInstant(), - endDate = DateTime.now().minusSeconds(15 * 86400 - 10).toInstant(), - defaultCheckInLengthInMinutes = 60, - cryptographicSeed = "".encode(), - cnPublicKey = "" - ) - traceLocationRepository.addTraceLocation(oldTemporaryOne) - - val oldTemporaryTwo = TraceLocation( - type = TraceLocationOuterClass.TraceLocationType.LOCATION_TYPE_TEMPORARY_CLUB_ACTIVITY, - description = "Old temporary 2", - address = "Hauptstr. 3, 69115 Heidelberg", - startDate = DateTime.now().minusSeconds(16 * 86400).toInstant(), - endDate = DateTime.now().minusSeconds(15 * 86400).toInstant(), - defaultCheckInLengthInMinutes = 60, - cryptographicSeed = "".encode(), - cnPublicKey = "" - ) - traceLocationRepository.addTraceLocation(oldTemporaryTwo) - - val oldTemporaryThree = TraceLocation( - type = TraceLocationOuterClass.TraceLocationType.LOCATION_TYPE_TEMPORARY_CLUB_ACTIVITY, - description = "Old temporary 3", - address = "Hauptstr. 3, 69115 Heidelberg", - startDate = DateTime.now().minusSeconds(16 * 86400).toInstant(), - endDate = DateTime.now().minusSeconds(15 * 86400 + 10).toInstant(), - defaultCheckInLengthInMinutes = 60, - cryptographicSeed = "".encode(), - cnPublicKey = "" - ) - traceLocationRepository.addTraceLocation(oldTemporaryThree) - } + private fun lastLocationData(it: List<TraceLocation>): TraceLocation? = + it.maxByOrNull { traceLocation -> traceLocation.id } + + private fun lastAttendeeLocationData(it: List<CheckIn>): TraceLocation? { + val checkIn = it.maxByOrNull { checkIn -> checkIn.id } ?: return null + + return TraceLocation( + id = checkIn.id, + type = TraceLocationOuterClass.TraceLocationType.forNumber(checkIn.type), + description = checkIn.description, + address = checkIn.address, + startDate = checkIn.traceLocationStart, + endDate = checkIn.traceLocationEnd, + defaultCheckInLengthInMinutes = checkIn.defaultCheckInLengthInMinutes, + cryptographicSeed = checkIn.cryptographicSeed, + cnPublicKey = checkIn.cnPublicKey, + version = checkIn.version + ) } @AssistedFactory 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 deleted file mode 100644 index 74d32337209ea60d3fe48e5c0b1f13fa15bb110e..0000000000000000000000000000000000000000 --- a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/createevent/CreateEventTestFragment.kt +++ /dev/null @@ -1,92 +0,0 @@ -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 { - createEvent() - it.hideKeyboard() - } - } - - private fun FragmentTestCreateeventBinding.createEvent() { - vm.createEvent( - eventOrLocationSpinner.editText!!.text.toString(), - eventDescription.text.toString(), - eventAddress.text.toString(), - eventStartEditText.text.toString(), - eventEndEditText.text.toString(), - eventDefaultCheckinLengthInMinutes.text.toString() - ) - } - - 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 deleted file mode 100644 index 258a96c7ad463471d8cac73695fcb3e546f3ae5a..0000000000000000000000000000000000000000 --- a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/createevent/CreateEventTestFragmentModule.kt +++ /dev/null @@ -1,19 +0,0 @@ -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 deleted file mode 100644 index 66a5cabd01d3776d7d5137ff0a2f189b25243e70..0000000000000000000000000000000000000000 --- a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/createevent/CreateEventTestViewModel.kt +++ /dev/null @@ -1,73 +0,0 @@ -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.checkins.qrcode.TraceLocation -import de.rki.coronawarnapp.eventregistration.events.TraceLocationCreator -import de.rki.coronawarnapp.eventregistration.events.TraceLocationUserInput -import de.rki.coronawarnapp.server.protocols.internal.pt.TraceLocationOuterClass.TraceLocationType.LOCATION_TYPE_PERMANENT_OTHER -import de.rki.coronawarnapp.server.protocols.internal.pt.TraceLocationOuterClass.TraceLocationType.LOCATION_TYPE_TEMPORARY_OTHER -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 - -class CreateEventTestViewModel @AssistedInject constructor( - dispatcherProvider: DispatcherProvider, - private val traceLocationCreator: TraceLocationCreator -) : 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")) - - val traceLocationType = - if (type == "Event") LOCATION_TYPE_TEMPORARY_OTHER else LOCATION_TYPE_PERMANENT_OTHER - - val userInput = TraceLocationUserInput( - traceLocationType, - description, - address, - startDate?.toInstant(), - endDate?.toInstant(), - defaultCheckInLengthInMinutes.toInt() - ) - - launch { - try { - val traceLocation = traceLocationCreator.createTraceLocation(userInput) - result.postValue(Result.Success(traceLocation)) - } catch (exception: Exception) { - Timber.d("Something went wrong when creating the trace location $exception") - result.postValue(Result.Error) - } - } - } catch (exception: Exception) { - Timber.d("Something went wrong when creating the trace location $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 deleted file mode 100644 index 0124e0857c06ebe0e76039d8c3cd7bcdefb838f8..0000000000000000000000000000000000000000 --- a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/qrcode/QrCodeCreationTestFragment.kt +++ /dev/null @@ -1,92 +0,0 @@ -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()) - } - - binding.downloadQrCodePosterTemplate.setOnClickListener { - viewModel.downloadQrCodePosterTemplate() - } - - viewModel.qrCodePosterTemplate.observe2(this) { vectorDrawableBytes -> - binding.downloadedQrCodePoster.text = vectorDrawableBytes.utf8() - } - } -} 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 deleted file mode 100644 index 336f7fc486d6159c2e1e61d17920851a7e55a3aa..0000000000000000000000000000000000000000 --- a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/qrcode/QrCodeCreationTestViewModel.kt +++ /dev/null @@ -1,104 +0,0 @@ -package de.rki.coronawarnapp.test.eventregistration.ui.qrcode - -import android.content.Context -import android.graphics.Bitmap -import android.graphics.pdf.PdfDocument -import android.view.View -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject -import de.rki.coronawarnapp.eventregistration.events.server.qrcodepostertemplate.QrCodePosterTemplateServer -import de.rki.coronawarnapp.ui.eventregistration.organizer.details.QrCodeGenerator -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 okio.ByteString -import okio.ByteString.Companion.toByteString -import timber.log.Timber -import java.io.File -import java.io.FileOutputStream - -class QrCodeCreationTestViewModel @AssistedInject constructor( - private val dispatcher: DispatcherProvider, - private val fileSharing: FileSharing, - private val qrCodeGenerator: QrCodeGenerator, - @AppContext private val context: Context, - private val posterTemplateServer: QrCodePosterTemplateServer -) : CWAViewModel(dispatcher) { - - val qrCodeBitmap = SingleLiveEvent<Bitmap>() - val errorMessage = SingleLiveEvent<String>() - val sharingIntent = SingleLiveEvent<FileSharing.FileIntentProvider>() - val qrCodePosterTemplate = SingleLiveEvent<ByteString>() - - /** - * Creates a QR Code [Bitmap] ,result is delivered by [qrCodeBitmap] - */ - fun createQrCode(input: String) = launch(context = dispatcher.IO) { - - try { - qrCodeBitmap.postValue(qrCodeGenerator.createQrCode(input)) - } catch (e: Exception) { - Timber.d(e, "Qr code creation failed") - errorMessage.postValue(e.localizedMessage ?: "QR code creation failed") - } - } - - /** - * 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") - } - - fun downloadQrCodePosterTemplate() { - launch { - try { - val posterTemplate = posterTemplateServer.downloadQrCodePosterTemplate() - qrCodePosterTemplate.postValue(posterTemplate.template.toByteArray().toByteString()) - } catch (exception: Exception) { - errorMessage.postValue("Downloading Poster Template failed: ${exception.message}") - } - } - } - - @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 deleted file mode 100644 index 77b7135aeeff685a1ea950674c655f2a1b006681..0000000000000000000000000000000000000000 --- a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/showevents/ShowStoredEventsTestFragment.kt +++ /dev/null @@ -1,48 +0,0 @@ -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.checkins.qrcode.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( - "id = $id", - "type = $type", - "description = $description", - "location = $address", - "startTime = $startDate", - "endTime = $endDate", - "defaultCheckInLengthInMinutes = $defaultCheckInLengthInMinutes", - "cryptographicSeed = $cryptographicSeed", - "cnPublicKey = $cnPublicKey" - ).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 deleted file mode 100644 index 45e3c4763e437a8cc30a17c144e77d7e1af7174c..0000000000000000000000000000000000000000 --- a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/showevents/ShowStoredEventsTestFragmentModule.kt +++ /dev/null @@ -1,19 +0,0 @@ -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 deleted file mode 100644 index f85513da0a4ec166c8df5c5e13973a0b7a2acfc5..0000000000000000000000000000000000000000 --- a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/showevents/ShowStoredEventsTestViewModel.kt +++ /dev/null @@ -1,24 +0,0 @@ -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 10ba1a3b2352801b7cc95471eeb4cb0283f0cc6d..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 @@ -37,7 +37,6 @@ class TestMenuFragmentViewModel @AssistedInject constructor() : CWAViewModel() { DataDonationTestFragment.MENU_ITEM, DeltaonboardingFragment.MENU_ITEM, EventRegistrationTestFragment.MENU_ITEM, - DeltaonboardingFragment.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 4010a857cd5dba62c8bc247485600db5e8b733aa..9728bc8fd07c67ac0dec8dfe6185f066692b91e9 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 @@ -16,12 +16,6 @@ import de.rki.coronawarnapp.test.deltaonboarding.ui.DeltaOnboardingFragmentModul 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 @@ -79,15 +73,6 @@ abstract class MainActivityTestModule { @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 - @ContributesAndroidInjector(modules = [QrCodeDetailFragmentModule::class]) abstract fun showEventDetail(): QrCodeDetailFragment } diff --git a/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_debugoptions.xml b/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_debugoptions.xml index 224249114efbd69384ab29ac449aa49cae7f585c..165e76a5873ac28d6015f1d63ec99802ff2c4a4f 100644 --- a/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_debugoptions.xml +++ b/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_debugoptions.xml @@ -54,7 +54,8 @@ app:layout_constraintTop_toBottomOf="@+id/new_debuglog_screen_explanation" /> </androidx.constraintlayout.widget.ConstraintLayout> - <androidx.constraintlayout.widget.ConstraintLayout + <LinearLayout + android:orientation="vertical" android:id="@+id/environment_container" style="@style/Card" android:layout_width="match_parent" @@ -66,77 +67,71 @@ style="@style/headline6" android:layout_width="match_parent" android:layout_height="wrap_content" - android:text="Server environment" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="parent" /> + android:text="Server environments" /> <TextView android:id="@+id/environment_cdnurl_download" - style="@style/body2" + style="@style/TextAppearance.MaterialComponents.Caption" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="@dimen/spacing_tiny" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@+id/environment_title" tools:text="Download: ?" /> <TextView android:id="@+id/environment_cdnurl_submission" - style="@style/body2" + style="@style/TextAppearance.MaterialComponents.Caption" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_marginTop="@dimen/spacing_tiny" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@+id/environment_cdnurl_download" + android:layout_marginTop="4dp" tools:text="Submission: ?" /> <TextView android:id="@+id/environment_cdnurl_verification" - style="@style/body2" + style="@style/TextAppearance.MaterialComponents.Caption" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_marginTop="@dimen/spacing_tiny" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@+id/environment_cdnurl_submission" + android:layout_marginTop="4dp" tools:text="Verification: ?" /> <TextView android:id="@+id/environment_url_datadonation" - style="@style/body2" + style="@style/TextAppearance.MaterialComponents.Caption" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_marginTop="@dimen/spacing_tiny" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@+id/environment_cdnurl_verification" + android:layout_marginTop="4dp" tools:text="DataDonation: ?" /> <TextView - android:id="@+id/environment_url_qrcode_poster_template" - style="@style/body2" + android:id="@+id/environment_url_log_upload" + style="@style/TextAppearance.MaterialComponents.Caption" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_marginTop="@dimen/spacing_tiny" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@+id/environment_url_datadonation" - tools:text="QR-Code poster template: ?" /> + android:layout_marginTop="4dp" + tools:text="LogUpload: ?" /> + + <TextView + android:id="@+id/environment_pubkey_appconfig" + style="@style/TextAppearance.MaterialComponents.Caption" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="4dp" + tools:text="AppConfigPubKey: ?" /> + + <TextView + android:id="@+id/environment_pubkey_crowdnotifier" + style="@style/TextAppearance.MaterialComponents.Caption" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="4dp" + tools:text="CrowdNotifierPubKey: ?" /> <RadioGroup android:id="@+id/environment_toggle_group" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_marginTop="@dimen/spacing_tiny" - android:orientation="vertical" - app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@+id/environment_url_qrcode_poster_template" /> - </androidx.constraintlayout.widget.ConstraintLayout> + android:layout_marginTop="16dp" + android:orientation="vertical" /> + </LinearLayout> </LinearLayout> </androidx.core.widget.NestedScrollView> 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 index 7cde04273f12098aaf69f92af3986360c3ae31cf..1e4441e000d99e3810c34fcc01a9b9c9c4dd31ff 100644 --- a/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_eventregistration.xml +++ b/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_eventregistration.xml @@ -1,8 +1,8 @@ <?xml version="1.0" encoding="utf-8"?> - <androidx.core.widget.NestedScrollView 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/scroller" android:layout_width="match_parent" android:layout_height="match_parent" tools:ignore="HardcodedText"> @@ -12,49 +12,6 @@ android:layout_height="wrap_content" android:orientation="vertical"> - <LinearLayout - style="@style/Card" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:layout_margin="@dimen/spacing_tiny" - android:orientation="vertical"> - - <TextView - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:text="QRCode Creation, Poster Template download, PDF creation/printing/sharing" /> - - <com.google.android.material.button.MaterialButton - android:id="@+id/testQrCodeCreation" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:layout_marginTop="@dimen/spacing_tiny" - android:text="QR Code and Poster Creation" /> - - </LinearLayout> - - <LinearLayout - style="@style/Card" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:layout_marginHorizontal="@dimen/spacing_tiny" - android:layout_marginBottom="@dimen/spacing_tiny" - 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:layout_marginTop="@dimen/spacing_tiny" - android:text="Scan check in QR code" /> - - </LinearLayout> - <LinearLayout style="@style/Card" android:layout_width="match_parent" @@ -68,31 +25,30 @@ android:text="Download, matching & risk calculation" /> <com.google.android.material.button.MaterialButton - android:id="@+id/downloadReportedCheckIns" + android:id="@+id/reset_processed_warning_packages" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="@dimen/spacing_tiny" - android:text="download check-ins" /> + android:text="Reset processed packages" /> <com.google.android.material.button.MaterialButton - android:id="@+id/runMatcher" + android:id="@+id/run_pt_warning_task" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="@dimen/spacing_small" - android:text="Run matcher" /> + android:text="Run task (download+matching)" /> <TextView - android:id="@+id/matchingRuntimeText" + android:id="@+id/task_run_time" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="@dimen/spacing_small" android:text="Matching runtime:" /> <TextView - android:id="@+id/matchingResultText" + android:id="@+id/tracing_warning_task_result" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_marginTop="@dimen/spacing_small" android:text="Matching result:" /> <com.google.android.material.button.MaterialButton @@ -113,72 +69,103 @@ android:id="@+id/riskCalculationResultText" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_marginTop="@dimen/spacing_small" android:text="Risk calculation result:" /> </LinearLayout> - <androidx.constraintlayout.widget.ConstraintLayout - android:id="@+id/event_container" + <LinearLayout + android:id="@+id/lastOrganiserLocationCard" style="@style/Card" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_margin="@dimen/spacing_tiny"> + android:layout_marginHorizontal="@dimen/spacing_tiny" + android:layout_marginTop="10dp" + android:orientation="vertical"> <TextView - android:id="@+id/event_title" style="@style/headline6" - android:layout_width="0dp" + android:layout_width="match_parent" 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" /> + android:text="Last organiser location" /> <TextView - android:id="@+id/events_body" - android:layout_width="match_parent" + android:id="@+id/lastOrganiserLocation" + android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_marginTop="@dimen/spacing_tiny" - android:text="After creating an event in the app, it is sent to the server and returned together with a guid and a signature." + android:layout_margin="10dp" android:textIsSelectable="true" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@+id/event_title" /> + tools:text="Location" /> - <com.google.android.material.button.MaterialButton - android:id="@+id/create_event_button" - android:layout_width="0dp" + <TextView + android:id="@+id/lastOrganiserLocationUrl" + android:layout_width="wrap_content" 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" /> + android:layout_margin="10dp" + android:autoLink="web" + android:textIsSelectable="true" + tools:text="URL" /> - <com.google.android.material.button.MaterialButton - android:id="@+id/show_events_button" - android:layout_width="0dp" + <TextView + android:id="@+id/lastOrganiserLocationId" + android:layout_width="wrap_content" 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" /> + android:layout_margin="10dp" + android:textIsSelectable="true" + tools:text="ID" /> <com.google.android.material.button.MaterialButton - android:id="@+id/generate_test_trace_locations" - android:layout_width="0dp" + android:id="@+id/qrcode_button" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center" + android:text="Qr Code" + android:textColor="@android:color/white" + app:backgroundTint="@color/colorAccent" + app:icon="@drawable/ic_qrcode" + app:iconTint="@android:color/white" /> + </LinearLayout> + + <LinearLayout + android:id="@+id/lastAttendeeLocationCard" + style="@style/Card" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginHorizontal="@dimen/spacing_tiny" + android:layout_marginTop="10dp" + + android:orientation="vertical"> + + <TextView + style="@style/headline6" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:text="Last attendee location" /> + + <TextView + android:id="@+id/lastAttendeeLocation" + android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_marginTop="8dp" - android:text="Generate test trace locations" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="@id/show_events_button" - app:layout_constraintTop_toBottomOf="@id/show_events_button" /> + android:layout_margin="10dp" + android:textIsSelectable="true" + tools:text="Location" /> + + <TextView + android:id="@+id/lastAttendeeLocationUrl" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_margin="10dp" + android:autoLink="web" + android:textIsSelectable="true" + tools:text="URL" /> - </androidx.constraintlayout.widget.ConstraintLayout> + <TextView + android:id="@+id/lastAttendeeLocationId" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_margin="10dp" + android:textIsSelectable="true" + tools:text="ID" /> + </LinearLayout> </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 deleted file mode 100644 index 7f8e4b10c71683b4cc812dcf7835de7076e9f037..0000000000000000000000000000000000000000 --- a/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_qrcode_creation.xml +++ /dev/null @@ -1,115 +0,0 @@ -<?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.button.MaterialButton - android:id="@+id/downloadQrCodePosterTemplate" - android:layout_width="0dp" - android:layout_height="wrap_content" - android:layout_marginTop="16dp" - android:text="Download Poster Template (result below qr code)" - app:layout_constraintStart_toStartOf="@id/generateQrCode" - app:layout_constraintEnd_toEndOf="@id/printPDF" - app:layout_constraintTop_toBottomOf="@id/generateQrCode" /> - - <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/downloadQrCodePosterTemplate"> - <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> - - <TextView - android:id="@+id/downloadedQrCodePoster" - 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/pdfPage" /> - - </androidx.constraintlayout.widget.ConstraintLayout> -</ScrollView> 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 09b620c70da305446400ea9db26d267a8ae447c1..1586ca6972c5d45da2f811603c2bce38e0fcf812 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 @@ -133,40 +133,17 @@ 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" /> - + android:id="@+id/action_eventRegistrationTestFragment_to_qrCodePosterFragmentTest" + app:destination="@id/qrCodePosterFragmentTest" /> </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" /> + android:id="@+id/qrCodePosterFragmentTest" + android:name="de.rki.coronawarnapp.ui.eventregistration.organizer.poster.QrCodePosterFragment" + android:label="qr_code_poster_fragment" + tools:layout="@layout/qr_code_poster_fragment"> + <argument + android:name="traceLocationId" + app:argType="long" /> + </fragment> </navigation> diff --git a/Corona-Warn-App/src/main/AndroidManifest.xml b/Corona-Warn-App/src/main/AndroidManifest.xml index d0ebbbe7886e8262f3367976a3aa836bf26d0f93..2cefbd599e3a740713e55a733c3b04b38949d234 100644 --- a/Corona-Warn-App/src/main/AndroidManifest.xml +++ b/Corona-Warn-App/src/main/AndroidManifest.xml @@ -85,7 +85,6 @@ <data android:host="e.coronawarn.app" - android:pathPrefix="/" android:scheme="https" /> </intent-filter> </activity> diff --git a/Corona-Warn-App/src/main/assets/default_app_config_android.bin b/Corona-Warn-App/src/main/assets/default_app_config_android.bin index aeac5e2d45c54db350f6219cbc4487ee8c691af0..04b460df4f9c35969a5d8f6cbfdd90da3b7fa170 100644 Binary files a/Corona-Warn-App/src/main/assets/default_app_config_android.bin and b/Corona-Warn-App/src/main/assets/default_app_config_android.bin differ diff --git a/Corona-Warn-App/src/main/assets/default_app_config_android.sha256 b/Corona-Warn-App/src/main/assets/default_app_config_android.sha256 index bd4fdf7c0da4ec5b13788ea1547f8d91c8fdb278..d5dbe5290741deddf302bd3a00f664f504ea97ce 100644 --- a/Corona-Warn-App/src/main/assets/default_app_config_android.sha256 +++ b/Corona-Warn-App/src/main/assets/default_app_config_android.sha256 @@ -1 +1 @@ -3d108b3fee7d1b4c227087c82bb804048de8d0542c3f2b26cf507a918201124d \ No newline at end of file +f10bbfb50eae9f304114bded52972832346279776e9b43f9fdf1a39557497119 \ No newline at end of file diff --git a/Corona-Warn-App/src/main/assets/default_qr_code_poster_template_android.bin b/Corona-Warn-App/src/main/assets/default_qr_code_poster_template_android.bin new file mode 100644 index 0000000000000000000000000000000000000000..7194fc2074fa06be83064e9d79d62d3f110b25df Binary files /dev/null and b/Corona-Warn-App/src/main/assets/default_qr_code_poster_template_android.bin differ diff --git a/Corona-Warn-App/src/main/assets/default_qr_code_poster_template_android.sha256 b/Corona-Warn-App/src/main/assets/default_qr_code_poster_template_android.sha256 new file mode 100644 index 0000000000000000000000000000000000000000..5a7b398d9c407b66b4f26eb4e51ec18ed96056e1 --- /dev/null +++ b/Corona-Warn-App/src/main/assets/default_qr_code_poster_template_android.sha256 @@ -0,0 +1 @@ +1e972018100828aa63bc2559713cffa22d8db62f3ce56b3edbe72ad8cb7adb16 \ No newline at end of file diff --git a/Corona-Warn-App/src/main/assets/technical.html b/Corona-Warn-App/src/main/assets/technical.html index ab77f6194cdfd05a32003406119f4b122e70df65..25a8b08dfe43ff4c3830ac6c8d3c347a0ee07510 100644 --- a/Corona-Warn-App/src/main/assets/technical.html +++ b/Corona-Warn-App/src/main/assets/technical.html @@ -6,74 +6,104 @@ to our attention by contacting us via this email: corona-warn-app.opensource@sap.com</p> <h2>Components:</h2> -<p>Component: android-database-sqlcipher<br/> +<p> + Component: android-database-sqlcipher<br/> Licensor: Zetetic LLC<br/> Website: https://www.zetetic.net/sqlcipher/<br/> License: BSD-style license -<p>Component: guava<br/> +</p> +<p> + Component: guava<br/> Licensor: Google<br/> Website: https://github.com/google/guava<br/> License: Apache 2.0 +</p> <p>Component: joda-time<br/> Licensor: Joda.org<br/> Website: https://www.joda.org/joda-time/<br/> License: Apache 2.0 -<p>Component: MockK<br/> +</p> +<p> + Component: MockK<br/> Licensor: github.com/mockk<br/> Website: https://github.com/mockk/mockk<br/> License: Apache 2.0 -<p>Component: Robolectric<br/> +</p> +<p> + Component: Robolectric<br/> Licensor: Xtreme Labs, Pivotal Labs and Google Inc.<br/> Website: https://robolectric.org/<br/> License: MIT -<p>Component: Zxing<br/> +</p> +<p> + Component: Zxing<br/> Licensor: zxing<br/> Website: https://github.com/zxing/zxing/<br/> License: Apache 2.0 -<p>Component: ZXing Android Embedded<br/> +</p> +<p> + Component: ZXing Android Embedded<br/> Licensor: ZXing authors, Journey Mobile<br/> Website: https://github.com/journeyapps/zxing-android-embedded<br/> License: Apache 2.0 -<p>Component: Detekt<br/> +</p> +<p> + Component: Detekt<br/> Licensor: detekt<br/> Website: https://detekt.github.io/detekt/<br/> License: Apache 2.0 -<p>Component: gson<br/> +</p> +<p> + Component: gson<br/> Licensor: Google<br/> Website: https://github.com/google/gson<br/> License: Apache 2.0 +</p> <p>Component: okhttp<br/> Licensor: square<br/> Website: https://square.github.io/okhttp/<br/> License: Apache 2.0 +</p> <p>Component: kotlinx.coroutines<br/> Licensor: Kotlin<br/> Website: https://github.com/Kotlin/kotlinx.coroutines<br/> License: Apache 2.0 +</p> <p>Component: Ktlint Gradle<br/> Licensor: Jonathan Leitschuh<br/> Website: https://github.com/JLLeitschuh/ktlint-gradle<br/> License: MIT -<p>Component: Retrofit<br/> +</p> +<p> + Component: Retrofit<br/> Licensor: square<br/> Website: https://square.github.io/retrofit/<br/> License: Apache 2.0 -<p>Component: Protobuf gradle plugin<br/> +</p> +<p> + Component: Protobuf gradle plugin<br/> Licensor: Google<br/> Website: https://github.com/google/protobuf-gradle-plugin<br/> License: BSD 3-Clause -<p>Component: Dagger<br/> +</p> +<p> + Component: Dagger<br/> Licensor: Google<br/> Website: https://github.com/google/dagger<br/> License: Apache 2.0 -<p>Component: Conscrypt<br/> +</p> +<p> + Component: Conscrypt<br/> Licensor: Conscrypt<br/> Website: https://github.com/google/conscrypt<br/> License: Apache 2.0 -<p>Component: Lottie<br/> +</p> +<p> + Component: Lottie<br/> Licensor: Airbnb, Inc.<br/> Website: https://github.com/airbnb/lottie-android<br/> License: Apache 2.0 +</p> <hr/> <p>Copyright (c) 2008-2020 Zetetic LLC All rights reserved.</p> diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/CoronaWarnApplication.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/CoronaWarnApplication.kt index f364e27d43f8878067e4a2d0e028fd2048b4091f..cee7752cbc1bf088f981eabd22a6d506d28c62b2 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/CoronaWarnApplication.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/CoronaWarnApplication.kt @@ -65,6 +65,7 @@ class CoronaWarnApplication : Application(), HasAndroidInjector { @Inject lateinit var onboardingSettings: OnboardingSettings @Inject lateinit var autoCheckOut: AutoCheckOut @Inject lateinit var traceLocationDbCleanupScheduler: TraceLocationDbCleanUpScheduler + @Inject lateinit var backgroundWorkScheduler: BackgroundWorkScheduler @LogHistoryTree @Inject lateinit var rollingLogHistory: Timber.Tree @@ -83,8 +84,6 @@ class CoronaWarnApplication : Application(), HasAndroidInjector { compPreview.inject(this) } - BackgroundWorkScheduler.init(component) - Timber.plant(rollingLogHistory) Timber.v("onCreate(): WorkManager setup done: $workManager") diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/KeyDownloadConfig.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/KeyDownloadConfig.kt index 3c0a31697f3e0e87529481961e4c98943b3dc1ed..d61dd15258becc5339bc199e07035c57cb7abf83 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/KeyDownloadConfig.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/KeyDownloadConfig.kt @@ -16,6 +16,8 @@ interface KeyDownloadConfig { val revokedHourPackages: Collection<RevokedKeyPackage.Hour> + val revokedTraceWarningPackages: Collection<RevokedKeyPackage.TraceWarning> + interface RevokedKeyPackage { val etag: String val region: LocationCode @@ -27,6 +29,8 @@ interface KeyDownloadConfig { interface Hour : Day, RevokedKeyPackage { val hour: LocalTime } + + interface TraceWarning : RevokedKeyPackage } interface Mapper : ConfigMapper<KeyDownloadConfig> diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/PresenceTracingConfig.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/PresenceTracingConfig.kt index 7b73dbd495e8be74db6c1ad731ecf06d760e0c7a..699e95b6fd724340c2819eba9140ecc0c6071424 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/PresenceTracingConfig.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/PresenceTracingConfig.kt @@ -2,6 +2,7 @@ package de.rki.coronawarnapp.appconfig import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel import de.rki.coronawarnapp.appconfig.mapping.ConfigMapper +import de.rki.coronawarnapp.server.protocols.internal.v2.PresenceTracingParametersOuterClass.PresenceTracingQRCodeDescriptorOrBuilder interface PresenceTracingConfig { val qrCodeErrorCorrectionLevel: ErrorCorrectionLevel @@ -9,6 +10,7 @@ interface PresenceTracingConfig { val riskCalculationParameters: PresenceTracingRiskCalculationParamContainer val submissionParameters: PresenceTracingSubmissionParamContainer val plausibleDeniabilityParameters: PlausibleDeniabilityParametersContainer + val qrCodeDescriptors: List<PresenceTracingQRCodeDescriptorOrBuilder> interface Mapper : ConfigMapper<PresenceTracingConfig> } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/PresenceTracingConfigContainer.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/PresenceTracingConfigContainer.kt index 91e17852764dea900fcb19f5ce569f0730ef6c93..5957fcdd91063e0ddeef7020db73994aa2f6367a 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/PresenceTracingConfigContainer.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/PresenceTracingConfigContainer.kt @@ -1,6 +1,7 @@ package de.rki.coronawarnapp.appconfig import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel +import de.rki.coronawarnapp.server.protocols.internal.v2.PresenceTracingParametersOuterClass.PresenceTracingQRCodeDescriptorOrBuilder data class PresenceTracingConfigContainer( override val qrCodeErrorCorrectionLevel: ErrorCorrectionLevel = ErrorCorrectionLevel.H, @@ -10,5 +11,6 @@ data class PresenceTracingConfigContainer( override val submissionParameters: PresenceTracingSubmissionParamContainer = PresenceTracingSubmissionParamContainer(), override val plausibleDeniabilityParameters: PlausibleDeniabilityParametersContainer = - PlausibleDeniabilityParametersContainer() + PlausibleDeniabilityParametersContainer(), + override val qrCodeDescriptors: List<PresenceTracingQRCodeDescriptorOrBuilder> = emptyList() ) : PresenceTracingConfig diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/KeyDownloadParametersMapper.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/KeyDownloadParametersMapper.kt index f1a5a2671e661788c9f5086b18aefd016af84bb0..f9c95b41853a9293c6aae7dca11767c15ff554df 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/KeyDownloadParametersMapper.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/KeyDownloadParametersMapper.kt @@ -26,7 +26,8 @@ class KeyDownloadParametersMapper @Inject constructor() : KeyDownloadConfig.Mapp individualDownloadTimeout = rawParameters.individualTimeout(), overallDownloadTimeout = rawParameters.overAllTimeout(), revokedDayPackages = rawParameters.mapDayEtags(), - revokedHourPackages = rawParameters.mapHourEtags() + revokedHourPackages = rawParameters.mapHourEtags(), + revokedTraceWarningPackages = rawParameters.mapTraceWarningEtags() ) } @@ -81,11 +82,27 @@ class KeyDownloadParametersMapper @Inject constructor() : KeyDownloadConfig.Mapp } } + private fun KeyDownloadParametersAndroid?.mapTraceWarningEtags(): List<RevokedKeyPackage.TraceWarning> { + if (this == null) return emptyList() + + return this.revokedTraceWarningPackagesList.mapNotNull { + if (it.etag == null) { + Timber.e("TraceWarningPackageMeta data had no ETAG: %s", it) + return@mapNotNull null + } + RevokedKeyPackage.TraceWarning( + etag = it.etag, + region = LocationCode("DE"), + ) + } + } + data class KeyDownloadConfigContainer( override val individualDownloadTimeout: Duration, override val overallDownloadTimeout: Duration, override val revokedDayPackages: Collection<KeyDownloadConfig.RevokedKeyPackage.Day>, - override val revokedHourPackages: Collection<KeyDownloadConfig.RevokedKeyPackage.Hour> + override val revokedHourPackages: Collection<KeyDownloadConfig.RevokedKeyPackage.Hour>, + override val revokedTraceWarningPackages: Collection<KeyDownloadConfig.RevokedKeyPackage.TraceWarning> ) : KeyDownloadConfig companion object { @@ -109,4 +126,9 @@ internal sealed class RevokedKeyPackage : KeyDownloadConfig.RevokedKeyPackage { override val day: LocalDate, override val hour: LocalTime ) : RevokedKeyPackage(), KeyDownloadConfig.RevokedKeyPackage.Hour + + data class TraceWarning( + override val etag: String, + override val region: LocationCode + ) : RevokedKeyPackage(), KeyDownloadConfig.RevokedKeyPackage.TraceWarning } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/PresenceTracingConfigMapper.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/PresenceTracingConfigMapper.kt index bdba0734eca097fa3d2cbbf037a13c4762aef169..739a0e6fd00d27db3cd02e7f396e076eefeaa0dd 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/PresenceTracingConfigMapper.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/PresenceTracingConfigMapper.kt @@ -8,14 +8,10 @@ import de.rki.coronawarnapp.appconfig.PresenceTracingConfigContainer import de.rki.coronawarnapp.appconfig.PresenceTracingRiskCalculationParamContainer import de.rki.coronawarnapp.appconfig.PresenceTracingSubmissionParamContainer import de.rki.coronawarnapp.server.protocols.internal.v2.AppConfigAndroid -import de.rki.coronawarnapp.server.protocols.internal.v2 - .PresenceTracingParametersOuterClass.PresenceTracingSubmissionParameters -import de.rki.coronawarnapp.server.protocols.internal.v2 - .PresenceTracingParametersOuterClass.PresenceTracingRiskCalculationParameters -import de.rki.coronawarnapp.server.protocols.internal.v2 - .PresenceTracingParametersOuterClass.PresenceTracingParameters.QRCodeErrorCorrectionLevel -import de.rki.coronawarnapp.server.protocols.internal.v2 - .PresenceTracingParametersOuterClass.PresenceTracingPlausibleDeniabilityParameters +import de.rki.coronawarnapp.server.protocols.internal.v2.PresenceTracingParametersOuterClass.PresenceTracingSubmissionParameters +import de.rki.coronawarnapp.server.protocols.internal.v2.PresenceTracingParametersOuterClass.PresenceTracingRiskCalculationParameters +import de.rki.coronawarnapp.server.protocols.internal.v2.PresenceTracingParametersOuterClass.PresenceTracingParameters.QRCodeErrorCorrectionLevel +import de.rki.coronawarnapp.server.protocols.internal.v2.PresenceTracingParametersOuterClass.PresenceTracingPlausibleDeniabilityParameters import timber.log.Timber import javax.inject.Inject @@ -95,7 +91,8 @@ class PresenceTracingConfigMapper @Inject constructor() : PresenceTracingConfig. revokedTraceLocationVersions = revokedTraceLocationVersionsList.orEmpty(), riskCalculationParameters = riskCalculationParameters, submissionParameters = submissionParameters, - plausibleDeniabilityParameters = plausibleDeniabilityParameters + plausibleDeniabilityParameters = plausibleDeniabilityParameters, + qrCodeDescriptors = qrCodeDescriptorsOrBuilderList ) } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/overview/ContactDiaryOverviewViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/overview/ContactDiaryOverviewViewModel.kt index 3409ad4b6bea470d8213773a93070efb7081697a..bb81b52052a6ab2d2583276dd2bb9046d4b162f9 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/overview/ContactDiaryOverviewViewModel.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/overview/ContactDiaryOverviewViewModel.kt @@ -19,8 +19,10 @@ import de.rki.coronawarnapp.contactdiary.ui.overview.adapter.day.contact.Contact import de.rki.coronawarnapp.contactdiary.ui.overview.adapter.day.riskenf.RiskEnfItem import de.rki.coronawarnapp.contactdiary.ui.overview.adapter.day.riskevent.RiskEventItem import de.rki.coronawarnapp.contactdiary.ui.overview.adapter.subheader.OverviewSubHeaderItem +import de.rki.coronawarnapp.eventregistration.checkins.CheckIn +import de.rki.coronawarnapp.eventregistration.checkins.CheckInRepository +import de.rki.coronawarnapp.presencetracing.risk.TraceLocationCheckInRisk import de.rki.coronawarnapp.risk.RiskState -import de.rki.coronawarnapp.risk.TraceLocationCheckInRisk import de.rki.coronawarnapp.risk.result.ExposureWindowDayRisk import de.rki.coronawarnapp.risk.storage.RiskLevelStorage import de.rki.coronawarnapp.server.protocols.internal.v2.RiskCalculationParametersOuterClass @@ -29,10 +31,10 @@ import de.rki.coronawarnapp.task.common.DefaultTaskRequest import de.rki.coronawarnapp.util.TimeAndDateExtensions.toLocalDateUtc import de.rki.coronawarnapp.util.TimeStamper import de.rki.coronawarnapp.util.coroutine.DispatcherProvider +import de.rki.coronawarnapp.util.flow.combine import de.rki.coronawarnapp.util.ui.SingleLiveEvent import de.rki.coronawarnapp.util.viewmodel.CWAViewModel import de.rki.coronawarnapp.util.viewmodel.SimpleCWAViewModelFactory -import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flowOf import org.joda.time.LocalDate @@ -44,6 +46,7 @@ class ContactDiaryOverviewViewModel @AssistedInject constructor( contactDiaryRepository: ContactDiaryRepository, riskLevelStorage: RiskLevelStorage, timeStamper: TimeStamper, + checkInRepository: CheckInRepository, private val exporter: ContactDiaryExporter ) : CWAViewModel(dispatcherProvider = dispatcherProvider) { @@ -57,23 +60,25 @@ class ContactDiaryOverviewViewModel @AssistedInject constructor( private val riskLevelPerDateFlow = riskLevelStorage.ewDayRiskStates private val traceLocationCheckInRiskFlow = riskLevelStorage.traceLocationCheckInRiskStates + private val allCheckInsFlow = checkInRepository.allCheckIns val listItems = combine( flowOf(dates), locationVisitsFlow, personEncountersFlow, riskLevelPerDateFlow, - traceLocationCheckInRiskFlow - ) { dateList, locationVisists, personEncounters, riskLevelPerDateList, traceLocationCheckInRiskList -> + traceLocationCheckInRiskFlow, + allCheckInsFlow + ) { dateList, locationVisists, personEncounters, riskLevelPerDateList, traceLocationCheckInRiskList, checkInList -> mutableListOf<DiaryOverviewItem>().apply { add(OverviewSubHeaderItem) addAll( - createListItemList( - dateList, + dateList.createListItemList( locationVisists, personEncounters, riskLevelPerDateList, - traceLocationCheckInRiskList + traceLocationCheckInRiskList, + checkInList ) ) }.toList() @@ -88,12 +93,12 @@ class ContactDiaryOverviewViewModel @AssistedInject constructor( ) } - private fun createListItemList( - dateList: List<LocalDate>, + private fun List<LocalDate>.createListItemList( visits: List<ContactDiaryLocationVisit>, encounters: List<ContactDiaryPersonEncounter>, riskLevelPerDateList: List<ExposureWindowDayRisk>, - traceLocationCheckInRiskList: List<TraceLocationCheckInRisk> + traceLocationCheckInRiskList: List<TraceLocationCheckInRisk>, + checkInList: List<CheckIn> ): List<DiaryOverviewItem> { Timber.v( "createListItemList(" + @@ -101,14 +106,16 @@ class ContactDiaryOverviewViewModel @AssistedInject constructor( "visits=%s, " + "encounters=%s, " + "riskLevelPerDateList=%s, " + - "traceLocationCheckInRiskList=%s", - dateList, + "traceLocationCheckInRiskList=%s," + + "checkInList=%s", + this, visits, encounters, riskLevelPerDateList, - traceLocationCheckInRiskList + traceLocationCheckInRiskList, + checkInList ) - return dateList.map { date -> + return map { date -> val visitsForDate = visits.filter { it.date == date } val encountersForDate = encounters.filter { it.date == date } @@ -125,16 +132,16 @@ class ContactDiaryOverviewViewModel @AssistedInject constructor( .firstOrNull { riskLevelPerDate -> riskLevelPerDate.localDateUtc == date } ?.toRisk(coreItemData.isNotEmpty()) - val riskEventItem = visitsForDate - .map { - it to traceLocationCheckInRisksForDate.find { - checkInRisk -> - checkInRisk.checkInId == it.checkInID + val riskEventItem = traceLocationCheckInRisksForDate + .mapNotNull { + val locationVisit = visitsForDate.find { visit -> visit.checkInID == it.checkInId } + val checkIn = checkInList.find { checkIn -> checkIn.id == it.checkInId } + + return@mapNotNull when (locationVisit != null && checkIn != null) { + true -> RiskEventDataHolder(it, locationVisit, checkIn) + else -> null } - } - .toMap() - .filter { it.value != null } - .toRiskEventItem() + }.toRiskEventItem() DayOverviewItem( date = date, @@ -187,10 +194,16 @@ class ContactDiaryOverviewViewModel @AssistedInject constructor( type = ContactItem.Type.LOCATION ) - private fun Map<ContactDiaryLocationVisit, TraceLocationCheckInRisk?>.toRiskEventItem(): RiskEventItem? { + private data class RiskEventDataHolder( + val traceLocationCheckInRisk: TraceLocationCheckInRisk, + val locationVisit: ContactDiaryLocationVisit, + val checkIn: CheckIn + ) + + private fun List<RiskEventDataHolder>.toRiskEventItem(): RiskEventItem? { if (isEmpty()) return null - val isHighRisk = values.any { it?.riskState == RiskState.INCREASED_RISK } + val isHighRisk = any { it.traceLocationCheckInRisk.riskState == RiskState.INCREASED_RISK } val body: Int = R.string.contact_diary_trace_location_risk_body val drawableID: Int @@ -207,15 +220,13 @@ class ContactDiaryOverviewViewModel @AssistedInject constructor( } } - val events = mapNotNull { entry -> - if (entry.value == null) return null - - val name = entry.key.contactDiaryLocation.locationName + val events = map { data -> + val name = data.locationVisit.contactDiaryLocation.locationName val bulletPointColor: Int var riskInfoAddition: Int? - when (entry.value?.riskState == RiskState.INCREASED_RISK) { + when (data.traceLocationCheckInRisk.riskState == RiskState.INCREASED_RISK) { true -> { bulletPointColor = R.color.colorBulletPointHighRisk riskInfoAddition = R.string.contact_diary_trace_location_risk_high @@ -228,8 +239,11 @@ class ContactDiaryOverviewViewModel @AssistedInject constructor( if (size < 2) riskInfoAddition = null + val description = data.checkIn.description + RiskEventItem.Event( name = name, + description = description, bulledPointColor = bulletPointColor, riskInfoAddition = riskInfoAddition ) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/overview/adapter/day/riskevent/RiskEventAdapter.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/overview/adapter/day/riskevent/RiskEventAdapter.kt index 157b2cbc6f48a67dd8f1fbe6a04425069781f9e1..ed4eed0a1ccf2dbc33ed196ad76e01336025e92f 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/overview/adapter/day/riskevent/RiskEventAdapter.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/overview/adapter/day/riskevent/RiskEventAdapter.kt @@ -1,8 +1,9 @@ package de.rki.coronawarnapp.contactdiary.ui.overview.adapter.day.riskevent import android.view.ViewGroup +import androidx.recyclerview.widget.SortedList +import androidx.recyclerview.widget.SortedListAdapterCallback import de.rki.coronawarnapp.R -import de.rki.coronawarnapp.contactdiary.util.clearAndAddAll import de.rki.coronawarnapp.databinding.ContactDiaryOverviewDayListItemRiskEventListItemBinding import de.rki.coronawarnapp.ui.lists.BaseAdapter import de.rki.coronawarnapp.util.ContextExtensions.getColorCompat @@ -10,18 +11,38 @@ import de.rki.coronawarnapp.util.lists.BindableVH class RiskEventAdapter : BaseAdapter<RiskEventAdapter.RiskEventListItemVH>() { - private val events: MutableList<RiskEventItem.Event> = mutableListOf() + private val events: SortedList<RiskEventItem.Event> = SortedList( + RiskEventItem.Event::class.java, + SortedList.BatchedCallback( + object : SortedListAdapterCallback<RiskEventItem.Event>( + this + ) { + override fun compare(o1: RiskEventItem.Event, o2: RiskEventItem.Event): Int = + o1.description.compareTo(o2.description) + + override fun areContentsTheSame(oldItem: RiskEventItem.Event?, newItem: RiskEventItem.Event?): Boolean = + oldItem == newItem + + override fun areItemsTheSame(item1: RiskEventItem.Event?, item2: RiskEventItem.Event?): Boolean = + item1 == item2 + } + ) + ) override fun onCreateBaseVH(parent: ViewGroup, viewType: Int): RiskEventListItemVH = RiskEventListItemVH(parent) override fun onBindBaseVH(holder: RiskEventListItemVH, position: Int, payloads: MutableList<Any>) = holder.bind(events[position], payloads) - override fun getItemCount(): Int = events.size + override fun getItemCount(): Int = events.size() fun setItems(events: List<RiskEventItem.Event>) { - this.events.clearAndAddAll(events) - notifyDataSetChanged() + this.events.apply { + beginBatchedUpdates() + clear() + addAll(events) + endBatchedUpdates() + } } inner class RiskEventListItemVH(parent: ViewGroup) : diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/overview/adapter/day/riskevent/RiskEventItem.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/overview/adapter/day/riskevent/RiskEventItem.kt index faa1f95d67bca1761c3f8a3204de02da3e9b3292..ea836cc33184e784385173a4f9154ab22e4b9212 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/overview/adapter/day/riskevent/RiskEventItem.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/overview/adapter/day/riskevent/RiskEventItem.kt @@ -17,6 +17,7 @@ data class RiskEventItem( data class Event( val name: String, + val description: String, @ColorRes val bulledPointColor: Int, @StringRes val riskInfoAddition: Int? = null ) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/diagnosiskeys/execution/DiagnosisKeyRetrievalWorkBuilder.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/diagnosiskeys/execution/DiagnosisKeyRetrievalWorkBuilder.kt new file mode 100644 index 0000000000000000000000000000000000000000..5e48acc14ec9537eee531c4fbd0bac77dc982b1a --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/diagnosiskeys/execution/DiagnosisKeyRetrievalWorkBuilder.kt @@ -0,0 +1,34 @@ +package de.rki.coronawarnapp.diagnosiskeys.execution + +import androidx.work.BackoffPolicy +import androidx.work.PeriodicWorkRequest +import androidx.work.PeriodicWorkRequestBuilder +import dagger.Reusable +import de.rki.coronawarnapp.worker.BackgroundConstants +import java.util.concurrent.TimeUnit +import javax.inject.Inject + +@Reusable +class DiagnosisKeyRetrievalWorkBuilder @Inject constructor() { + + /** + * This has no network constraints, because even if there is no internet, + * the worker+task will trigger diagnosis key submission to the ENF. + * We don't want to prevent that. + */ + fun createPeriodicWorkRequest(): PeriodicWorkRequest = + PeriodicWorkRequestBuilder<DiagnosisKeyRetrievalWorker>( + 60, + TimeUnit.MINUTES + ) + .setInitialDelay( + BackgroundConstants.KIND_DELAY, + TimeUnit.MINUTES + ) + .setBackoffCriteria( + BackoffPolicy.EXPONENTIAL, + BackgroundConstants.BACKOFF_INITIAL_DELAY, + TimeUnit.MINUTES + ) + .build() +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/DiagnosisKeyRetrievalOneTimeWorker.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/diagnosiskeys/execution/DiagnosisKeyRetrievalWorker.kt similarity index 50% rename from Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/DiagnosisKeyRetrievalOneTimeWorker.kt rename to Corona-Warn-App/src/main/java/de/rki/coronawarnapp/diagnosiskeys/execution/DiagnosisKeyRetrievalWorker.kt index f087a031badd867c65e3f983792b90dcc833f00d..a503798ac72814a326e6f32ca14a48c95f1533b6 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/DiagnosisKeyRetrievalOneTimeWorker.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/diagnosiskeys/execution/DiagnosisKeyRetrievalWorker.kt @@ -1,4 +1,4 @@ -package de.rki.coronawarnapp.worker +package de.rki.coronawarnapp.diagnosiskeys.execution import android.content.Context import androidx.work.CoroutineWorker @@ -6,6 +6,7 @@ import androidx.work.WorkerParameters import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject +import de.rki.coronawarnapp.bugreporting.reportProblem import de.rki.coronawarnapp.diagnosiskeys.download.DownloadDiagnosisKeysTask import de.rki.coronawarnapp.task.TaskController import de.rki.coronawarnapp.task.common.DefaultTaskRequest @@ -13,49 +14,44 @@ import de.rki.coronawarnapp.task.submitBlocking import de.rki.coronawarnapp.util.worker.InjectedWorkerFactory import timber.log.Timber -/** - * One time diagnosis key retrieval work - * Executes the retrieve diagnosis key transaction - * - * @see BackgroundWorkScheduler - */ -class DiagnosisKeyRetrievalOneTimeWorker @AssistedInject constructor( +class DiagnosisKeyRetrievalWorker @AssistedInject constructor( @Assisted val context: Context, @Assisted workerParams: WorkerParameters, private val taskController: TaskController ) : CoroutineWorker(context, workerParams) { - override suspend fun doWork(): Result { + override suspend fun doWork(): Result = try { Timber.tag(TAG).d("$id: doWork() started. Run attempt: $runAttemptCount") - var result = Result.success() - taskController.submitBlocking( + val taskState = taskController.submitBlocking( DefaultTaskRequest( DownloadDiagnosisKeysTask::class, DownloadDiagnosisKeysTask.Arguments(), - originTag = "DiagnosisKeyRetrievalOneTimeWorker" + originTag = "DiagnosisKeyRetrievalWorker" ) - ).error?.also { error: Throwable -> - Timber.tag(TAG).w(error, "$id: Error when submitting DownloadDiagnosisKeysTask.") + ) - if (runAttemptCount > BackgroundConstants.WORKER_RETRY_COUNT_THRESHOLD) { - Timber.tag(TAG).w(error, "$id: Retry attempts exceeded.") - - return Result.failure() - } else { - Timber.tag(TAG).d(error, "$id: Retrying.") - result = Result.retry() + when { + taskState.isSuccessful -> { + Timber.tag(TAG).d("$id: DownloadDiagnosisKeysTask finished successfully.") + Result.success() + } + else -> { + taskState.error?.let { + Timber.tag(TAG).w(it, "$id: Error during DownloadDiagnosisKeysTask.") + } + Result.retry() } } - - Timber.tag(TAG).d("$id: doWork() finished with %s", result) - return result + } catch (e: Exception) { + e.reportProblem(TAG, "DownloadDiagnosisKeysTask failed exceptionally, will retry.") + Result.retry() } @AssistedFactory - interface Factory : InjectedWorkerFactory<DiagnosisKeyRetrievalOneTimeWorker> + interface Factory : InjectedWorkerFactory<DiagnosisKeyRetrievalWorker> companion object { - private val TAG = DiagnosisKeyRetrievalOneTimeWorker::class.java.simpleName + private val TAG = DiagnosisKeyRetrievalWorker::class.java.simpleName } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/environment/EnvironmentSetup.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/environment/EnvironmentSetup.kt index 952449960c4e2acb337506fc4fc4316edc0eb7cb..4c72ad4158a2f32c908c91a610a66bbf612978c9 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/environment/EnvironmentSetup.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/environment/EnvironmentSetup.kt @@ -35,7 +35,6 @@ class EnvironmentSetup @Inject constructor( DOWNLOAD("DOWNLOAD_CDN_URL"), VERIFICATION_KEYS("PUB_KEYS_SIGNATURE_VERIFICATION"), DATA_DONATION("DATA_DONATION_CDN_URL"), - QRCODE_POSTER_TEMPLATE("QRCODE_POSTER_TEMPLATE_URL"), LOG_UPLOAD("LOG_UPLOAD_SERVER_URL"), SAFETYNET_API_KEY("SAFETYNET_API_KEY"), CROWD_NOTIFIER_PUBLIC_KEY("CROWD_NOTIFIER_PUBLIC_KEY") @@ -48,6 +47,7 @@ class EnvironmentSetup @Inject constructor( WRU("WRU"), WRU_XA("WRU-XA"), // (aka ACME), WRU_XD("WRU-XD"), // (aka Germany) + TESTER_MOCK("TESTER-MOCK"), // (aka Germany) LOCAL("LOCAL"); // Emulator/CLI tooling companion object { @@ -118,10 +118,8 @@ class EnvironmentSetup @Inject constructor( get() = getEnvironmentValue(DOWNLOAD).asString val dataDonationCdnUrl: String get() = getEnvironmentValue(DATA_DONATION).asString - val qrCodePosterTemplateCdnUrl: String - get() = getEnvironmentValue(EnvKey.QRCODE_POSTER_TEMPLATE).asString - val appConfigVerificationKey: String + val appConfigPublicKey: String get() = getEnvironmentValue(VERIFICATION_KEYS).asString val useEuropeKeyPackageFiles: Boolean diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/environment/eventregistration/qrcodeposter/QrCodePosterTemplateModule.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/environment/eventregistration/qrcodeposter/QrCodePosterTemplateModule.kt index 5d7a0d8a163fecdda2d4b87c66453551bc88cc68..9e987ba5e53ecf55760e01d47c1d155d467dffab 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/environment/eventregistration/qrcodeposter/QrCodePosterTemplateModule.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/environment/eventregistration/qrcodeposter/QrCodePosterTemplateModule.kt @@ -4,8 +4,8 @@ import android.content.Context import dagger.Module import dagger.Provides import de.rki.coronawarnapp.environment.BaseEnvironmentModule -import de.rki.coronawarnapp.environment.EnvironmentSetup import de.rki.coronawarnapp.environment.download.DownloadCDNHttpClient +import de.rki.coronawarnapp.environment.download.DownloadCDNServerUrl import de.rki.coronawarnapp.eventregistration.events.server.qrcodepostertemplate.QrCodePosterTemplateApiV1 import de.rki.coronawarnapp.util.di.AppContext import okhttp3.Cache @@ -23,7 +23,7 @@ class QrCodePosterTemplateModule : BaseEnvironmentModule() { @QrCodePosterTemplate fun cacheDir( @AppContext context: Context - ): File = File(context.cacheDir, "qrCodePoster") + ): File = File(context.cacheDir, "poster") @Singleton @Provides @@ -32,19 +32,11 @@ class QrCodePosterTemplateModule : BaseEnvironmentModule() { @QrCodePosterTemplate cacheDir: File ): Cache = Cache(File(cacheDir, "cache_http"), CACHE_SIZE_5MB) - @Singleton - @QrCodePosterTemplate - @Provides - fun provideQrCodePosterTemplateCDNServerUrl(environment: EnvironmentSetup): String { - val url = environment.qrCodePosterTemplateCdnUrl - return requireValidUrl(url) - } - @Singleton @Provides fun api( @DownloadCDNHttpClient client: OkHttpClient, - @QrCodePosterTemplate url: String, + @DownloadCDNServerUrl url: String, @QrCodePosterTemplate cache: Cache ): QrCodePosterTemplateApiV1 { val httpClient = client.newBuilder().apply { 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 index 2a1be8176e660b0e3b8c24a2b6af2739037812b3..2704dfe269b033a3425aa37e3d44df5738ce3a0c 100644 --- 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 @@ -3,23 +3,20 @@ package de.rki.coronawarnapp.eventregistration import dagger.Binds import dagger.Module import de.rki.coronawarnapp.environment.eventregistration.qrcodeposter.QrCodePosterTemplateModule -import de.rki.coronawarnapp.eventregistration.checkins.download.FakeTraceTimeIntervalWarningRepository -import de.rki.coronawarnapp.eventregistration.checkins.download.TraceTimeIntervalWarningRepository import de.rki.coronawarnapp.eventregistration.storage.repo.DefaultTraceLocationRepository import de.rki.coronawarnapp.eventregistration.storage.repo.TraceLocationRepository +import de.rki.coronawarnapp.presencetracing.warning.PresenceTracingWarningModule @Module( includes = [ - QrCodePosterTemplateModule::class + QrCodePosterTemplateModule::class, + PresenceTracingWarningModule::class, ] ) abstract class EventRegistrationModule { @Binds - abstract fun traceLocationRepository(defaultTraceLocationRepo: DefaultTraceLocationRepository): - TraceLocationRepository - - @Binds - abstract fun traceTimeIntervalWarningRepository(repository: FakeTraceTimeIntervalWarningRepository): - TraceTimeIntervalWarningRepository + 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 index eae4e6abe8ff65f534022fb2da42f9ca9903fef4..1f755923ad63b8c50438a965499c63fd4c005fda 100644 --- 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 @@ -1,15 +1,15 @@ package de.rki.coronawarnapp.eventregistration.checkins +import de.rki.coronawarnapp.eventregistration.checkins.qrcode.TraceLocationId +import de.rki.coronawarnapp.eventregistration.checkins.qrcode.toTraceLocationIdHash import de.rki.coronawarnapp.eventregistration.storage.entity.TraceLocationCheckInEntity import okio.ByteString -import okio.ByteString.Companion.encode import org.joda.time.Instant @Suppress("LongParameterList") data class CheckIn( val id: Long = 0L, - val traceLocationId: ByteString = "TODO: calculate".encode(), - val traceLocationIdHash: ByteString = "TODO: calculate".encode(), + val traceLocationId: TraceLocationId, val version: Int, val type: Int, val description: String, @@ -24,13 +24,18 @@ data class CheckIn( val completed: Boolean, val createJournalEntry: Boolean ) { - // val locationGuidHash: com.google.protobuf.ByteString by lazy { copyFromUtf8(guid.toSHA256()) } + /** + * Returns SHA-256 hash of [traceLocationId] which itself may also be SHA-256 hash. + * For privacy reasons TraceTimeIntervalWarnings only offer a hash of the actual locationId. + * + * @see [de.rki.coronawarnapp.eventregistration.checkins.qrcode.TraceLocation] + */ + val traceLocationIdHash by lazy { traceLocationId.toTraceLocationIdHash() } } fun CheckIn.toEntity() = TraceLocationCheckInEntity( id = id, traceLocationIdBase64 = traceLocationId.base64(), - traceLocationIdHashBase64 = traceLocationIdHash.base64(), version = version, type = type, description = description, diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/CheckInsTransformer.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/CheckInsTransformer.kt index 6f76b0b04c1c86b8894e7ec7182343b834dbb2ab..723ea4bd4761c9c5a550293d6f5bd79291590038 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/CheckInsTransformer.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/CheckInsTransformer.kt @@ -1,6 +1,5 @@ package de.rki.coronawarnapp.eventregistration.checkins -import com.google.protobuf.ByteString import de.rki.coronawarnapp.appconfig.AppConfigProvider import de.rki.coronawarnapp.eventregistration.checkins.derivetime.deriveTime import de.rki.coronawarnapp.eventregistration.checkins.split.splitByMidnightUTC @@ -14,6 +13,7 @@ import de.rki.coronawarnapp.util.TimeAndDateExtensions.seconds import de.rki.coronawarnapp.util.TimeAndDateExtensions.secondsToInstant import de.rki.coronawarnapp.util.TimeAndDateExtensions.toLocalDateUtc import de.rki.coronawarnapp.util.TimeStamper +import de.rki.coronawarnapp.util.toProtoByteString import org.joda.time.Days import org.joda.time.Instant import timber.log.Timber @@ -88,7 +88,7 @@ class CheckInsTransformer @Inject constructor( } return CheckInOuterClass.CheckIn.newBuilder() - // .locationId = TODO: Set calculated trace location + .setLocationId(traceLocationId.toProtoByteString()) .setStartIntervalNumber(checkInStart.derive10MinutesInterval().toInt()) .setEndIntervalNumber(checkInEnd.derive10MinutesInterval().toInt()) .setTransmissionRiskLevel(transmissionRiskLevel) @@ -107,5 +107,3 @@ fun CheckIn.determineRiskTransmission(now: Instant, transmissionVector: Transmis val ageInDays = Days.daysBetween(startMidnight, nowMidnight).days return transmissionVector.raw.getOrElse(ageInDays) { 1 } // Default value } - -private fun okio.ByteString.toProtoByteString() = ByteString.copyFrom(toByteArray()) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/download/TraceTimeIntervalWarningPackage.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/download/TraceTimeIntervalWarningPackage.kt deleted file mode 100644 index be16290c8c184f5986a714d65142612957917ab1..0000000000000000000000000000000000000000 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/download/TraceTimeIntervalWarningPackage.kt +++ /dev/null @@ -1,16 +0,0 @@ -package de.rki.coronawarnapp.eventregistration.checkins.download - -import de.rki.coronawarnapp.server.protocols.internal.pt.TraceWarning - -interface TraceTimeIntervalWarningPackage { - - /** - * Hides the file reading - */ - suspend fun extractTraceTimeIntervalWarnings(): List<TraceWarning.TraceTimeIntervalWarning> - - /** - * Numeric identifier representing the hour since epoch, used in the Api endpoint - */ - val warningPackageId: String -} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/download/TraceTimeIntervalWarningRepository.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/download/TraceTimeIntervalWarningRepository.kt deleted file mode 100644 index 70a8d633266b300d12a2f6ac76d52a4f7c63d2c5..0000000000000000000000000000000000000000 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/download/TraceTimeIntervalWarningRepository.kt +++ /dev/null @@ -1,59 +0,0 @@ -package de.rki.coronawarnapp.eventregistration.checkins.download - -import de.rki.coronawarnapp.server.protocols.internal.pt.TraceWarning -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.asFlow -import org.joda.time.Duration -import org.joda.time.Instant -import javax.inject.Inject - -interface TraceTimeIntervalWarningRepository { - - val allWarningPackages: Flow<List<TraceTimeIntervalWarningPackage>> - - fun addWarningPackages(list: List<TraceTimeIntervalWarningPackage>) - - fun removeWarningPackages(list: List<TraceTimeIntervalWarningPackage>) -} - -// proprietary dummy implementations -class FakeTraceTimeIntervalWarningRepository @Inject constructor() : TraceTimeIntervalWarningRepository { - override val allWarningPackages: Flow<List<TraceTimeIntervalWarningPackage>> - get() = listOf(listOf<TraceTimeIntervalWarningPackage>(DummyCheckInPackage)).asFlow() - - override fun addWarningPackages(list: List<TraceTimeIntervalWarningPackage>) { - // TODO("Not yet implemented") - } - - override fun removeWarningPackages(list: List<TraceTimeIntervalWarningPackage>) { - // TODO("Not yet implemented") - } -} - -object DummyCheckInPackage : TraceTimeIntervalWarningPackage { - override suspend fun extractTraceTimeIntervalWarnings(): List<TraceWarning.TraceTimeIntervalWarning> { - return warnings - } - - override val warningPackageId: String - get() = "id" -} - -val warnings = (1L..1000L).map { - createWarning( - startIntervalDateStr = "2021-03-04T10:00+01:00", - period = 6, - transmissionRiskLevel = 8 - ) -} - -fun createWarning( - startIntervalDateStr: String, - period: Int, - transmissionRiskLevel: Int -) = TraceWarning.TraceTimeIntervalWarning.newBuilder() - // .locationIdHash = TODO: set location Id hash - .setPeriod(period) - .setStartIntervalNumber((Duration(Instant.parse(startIntervalDateStr).millis).standardMinutes / 10).toInt()) - .setTransmissionRiskLevel(transmissionRiskLevel) - .build() 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 deleted file mode 100644 index 935e86b319ff3fc5ca3d685371c417fe706bf64a..0000000000000000000000000000000000000000 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/InvalidQRCodeDataException.kt +++ /dev/null @@ -1,6 +0,0 @@ -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 deleted file mode 100644 index eae77689d0aab837f5527c2682e72f55078c70ab..0000000000000000000000000000000000000000 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/InvalidQRCodeSignatureException.kt +++ /dev/null @@ -1,6 +0,0 @@ -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/PosterTemplateProvider.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/PosterTemplateProvider.kt new file mode 100644 index 0000000000000000000000000000000000000000..f00a0c22f14152f4909eabf4c93667c76e4a8a8a --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/PosterTemplateProvider.kt @@ -0,0 +1,67 @@ +package de.rki.coronawarnapp.eventregistration.checkins.qrcode + +import android.content.Context +import android.graphics.Bitmap +import android.graphics.pdf.PdfRenderer +import android.os.ParcelFileDescriptor +import de.rki.coronawarnapp.eventregistration.events.server.qrcodepostertemplate.QrCodePosterTemplateServer +import de.rki.coronawarnapp.server.protocols.internal.pt.QrCodePosterTemplate.QRCodePosterTemplateAndroid.QRCodeTextBoxAndroid +import de.rki.coronawarnapp.util.coroutine.DispatcherProvider +import de.rki.coronawarnapp.util.di.AppContext +import kotlinx.coroutines.withContext +import timber.log.Timber +import java.io.File +import java.io.FileOutputStream +import javax.inject.Inject +import kotlin.math.roundToInt + +class PosterTemplateProvider @Inject constructor( + private val posterTemplateServer: QrCodePosterTemplateServer, + private val dispatcherProvider: DispatcherProvider, + @AppContext private val context: Context +) { + @Suppress("BlockingMethodInNonBlockingContext") + suspend fun template(): Template = withContext(dispatcherProvider.IO) { + val templateData = posterTemplateServer.downloadQrCodePosterTemplate() + val file = File(context.cacheDir, "template.pdf") + FileOutputStream(file).use { it.write(templateData.template.toByteArray()) } + + val fileDescriptor = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY) + val renderer = PdfRenderer(fileDescriptor) + + val page = renderer.openPage(0) + val scale = (context.resources.displayMetrics.density / page.width * page.height).roundToInt() + Timber.d("scale=$scale") + val bitmap = Bitmap.createBitmap( + context.resources.displayMetrics, + page.width * scale, + page.height * scale, + Bitmap.Config.ARGB_8888 + ) + + page.render(bitmap, null, null, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY) + page.close() + renderer.close() + file.delete() + + Template( + bitmap = bitmap, + width = page.width, + height = page.height, + offsetX = templateData.offsetX, + offsetY = templateData.offsetY, + qrCodeLength = templateData.qrCodeSideLength, + textBox = templateData.descriptionTextBox + ) + } +} + +data class Template( + val bitmap: Bitmap?, + val width: Int, + val height: Int, + val offsetX: Float, + val offsetY: Float, + val qrCodeLength: Int, + val textBox: QRCodeTextBoxAndroid +) 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 index 1fb4c881a81819cd76149537923ad3956f8d942a..3df60ea22ecbc0d8683f5d5987475eb287373e7a 100644 --- 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 @@ -1,6 +1,21 @@ package de.rki.coronawarnapp.eventregistration.checkins.qrcode -open class QRCodeException constructor( +sealed class QRCodeException constructor( message: String? = null, cause: Throwable? = null ) : Exception(message, cause) + +class InvalidQrCodeUriException constructor( + message: String? = null, + cause: Throwable? = null +) : QRCodeException(message, cause) + +class InvalidQrCodePayloadException constructor( + message: String? = null, + cause: Throwable? = null +) : QRCodeException(message, cause) + +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/QRCodeUriParser.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/QRCodeUriParser.kt index 7d622c3425e2e986c9117edf50e179940ea1ecde..493d2bdb667c846e7189099574c2018b63fd5a57 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/QRCodeUriParser.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/QRCodeUriParser.kt @@ -1,46 +1,86 @@ package de.rki.coronawarnapp.eventregistration.checkins.qrcode +import com.google.common.io.BaseEncoding import dagger.Reusable +import de.rki.coronawarnapp.appconfig.AppConfigProvider +import de.rki.coronawarnapp.server.protocols.internal.pt.TraceLocationOuterClass.QRCodePayload +import de.rki.coronawarnapp.server.protocols.internal.v2.PresenceTracingParametersOuterClass.PresenceTracingQRCodeDescriptorOrBuilder +import de.rki.coronawarnapp.server.protocols.internal.v2.PresenceTracingParametersOuterClass.PresenceTracingQRCodeDescriptor.PayloadEncoding import de.rki.coronawarnapp.util.decodeBase32 -import okio.ByteString +import okio.ByteString.Companion.toByteString import timber.log.Timber import java.net.URI import javax.inject.Inject @Reusable -class QRCodeUriParser @Inject constructor() { +class QRCodeUriParser @Inject constructor( + private val configProvider: AppConfigProvider +) { /** - * 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 + * Parse [QRCodePayload] from [input] + * + * @throws [Exception] such as [QRCodeException], + * exceptions from [URI.create] + * and possible decoding exceptions */ - fun getQrCodePayload(maybeUri: String): ByteString? = URI.create(maybeUri).run { - if (!scheme.equals(SCHEME, true)) return@run null - if (!authority.equals(AUTHORITY, true)) return@run null + @Suppress("BlockingMethodInNonBlockingContext") + suspend fun getQrCodePayload(input: String): QRCodePayload { + Timber.d("input=$input") + try { + URI.create(input) // Verify it is a valid uri + } catch (e: Exception) { + Timber.d(e, "Invalid URI") + throw InvalidQrCodeUriException("Invalid URI", e) + } - if (!path.substringBeforeLast("/").equals(PATH_PREFIX, true)) return@run null + val descriptor = descriptor(input) + val groups = descriptor.matchedGroups(input) - val rawData = path.substringAfterLast("/") - val paddingDiff = 8 - (rawData.length % 8) - val maybeBase32 = rawData + createPadding(paddingDiff) + val payload = groups[descriptor.encodedPayloadGroupIndex] + Timber.d("payload=$payload") - if (!maybeBase32.matches(BASE32_REGEX)) return@run null + val encoding = PayloadEncoding.forNumber(descriptor.payloadEncoding.number) + Timber.d("encoding=$encoding") - return@run try { - maybeBase32.decodeBase32() + val rawPayload = try { + when (encoding) { + PayloadEncoding.BASE32 -> payload.decodeBase32() + PayloadEncoding.BASE64 -> BaseEncoding.base64Url().decode(payload).toByteString() + else -> null + } } catch (e: Exception) { - Timber.w(e, "Data wasn't base32: %s", maybeBase32) + Timber.d(e, "Payload decoding failed") null + } ?: throw InvalidQrCodeDataException("Payload decoding failed") + + return QRCodePayload.parseFrom(rawPayload.toByteArray()) + } + + private suspend fun descriptor(input: String): PresenceTracingQRCodeDescriptorOrBuilder { + val descriptors = configProvider.getAppConfig().presenceTracing.qrCodeDescriptors + Timber.d("descriptors=$descriptors") + val descriptor = descriptors.find { it.regexPattern.toRegex(RegexOption.IGNORE_CASE).matches(input) } + if (descriptor == null) { + Timber.d("Invalid URI - no matchedDescriptor") + throw InvalidQrCodeUriException("Invalid URI - no matchedDescriptor") } + Timber.d("descriptor=$descriptor") + return descriptor } - companion object { - private fun createPadding(length: Int) = (0 until length).joinToString(separator = "") { "=" } + private fun PresenceTracingQRCodeDescriptorOrBuilder.matchedGroups( + input: String + ): List<String> { + val groups = regexPattern + .toRegex(RegexOption.IGNORE_CASE).find(input) // Find matched result [MatchResult] + ?.destructured?.toList().orEmpty() // Destructured groups - excluding the zeroth group (Whole String) + Timber.d("groups=$groups") - private const val SCHEME = "https" - private const val AUTHORITY = "e.coronawarn.app" - private const val PATH_PREFIX = "/c1" - private val BASE32_REGEX = "^([A-Z2-7=]{8})+$".toRegex(RegexOption.IGNORE_CASE) + if (encodedPayloadGroupIndex !in groups.indices) { + Timber.d("Invalid payload - group index is out of bounds") + throw InvalidQrCodePayloadException("Invalid payload - group index is out of bounds") + } + return groups } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/QrCodeGenerator.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/QrCodeGenerator.kt index 6dac184d63cd303b1ea1cd36d45bdaa16d01d6f6..96a244814db3cb17550f44466aa76668e58d0294 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/QrCodeGenerator.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/QrCodeGenerator.kt @@ -1,4 +1,4 @@ -package de.rki.coronawarnapp.ui.eventregistration.organizer.details +package de.rki.coronawarnapp.eventregistration.checkins.qrcode import android.content.Context import android.graphics.Bitmap @@ -6,6 +6,7 @@ import android.graphics.Color import com.google.zxing.BarcodeFormat import com.google.zxing.EncodeHintType import com.google.zxing.MultiFormatWriter +import com.google.zxing.WriterException import com.google.zxing.common.BitMatrix import dagger.Reusable import de.rki.coronawarnapp.appconfig.AppConfigProvider @@ -19,22 +20,31 @@ class QrCodeGenerator @Inject constructor( @AppContext private val context: Context, ) { - suspend fun createQrCode(input: String, size: Int = 1000): Bitmap? { - - val qrCodeErrorCorrectionLevel = appConfigProvider + /** + * Decodes input String into a QR Code [Bitmap] + * @param input [String] + * @param length [Int] QR Code side length + * @param margin [Int] QR Code side's margin + * + * @throws [Exception] it could throw [IllegalArgumentException] , [WriterException] + * or exception while creating the bitmap + */ + suspend fun createQrCode(input: String, length: Int = 1000, margin: Int = 1): Bitmap { + val correctionLevel = appConfigProvider .getAppConfig() .presenceTracing .qrCodeErrorCorrectionLevel - Timber.i("QrCodeErrorCorrectionLevel: $qrCodeErrorCorrectionLevel") + Timber.i("correctionLevel=$correctionLevel") + val hints = mapOf( - EncodeHintType.ERROR_CORRECTION to qrCodeErrorCorrectionLevel + EncodeHintType.ERROR_CORRECTION to correctionLevel, + EncodeHintType.MARGIN to margin ) - return MultiFormatWriter().encode( input, BarcodeFormat.QR_CODE, - size, - size, + length, + length, hints ).toBitmap() } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/QrCodePayload.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/QrCodePayload.kt new file mode 100644 index 0000000000000000000000000000000000000000..7ac7412993b6bea0ea8e9c666d013bb41dac2dbe --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/QrCodePayload.kt @@ -0,0 +1,57 @@ +package de.rki.coronawarnapp.eventregistration.checkins.qrcode + +import de.rki.coronawarnapp.server.protocols.internal.pt.TraceLocationOuterClass +import de.rki.coronawarnapp.server.protocols.internal.pt.TraceLocationOuterClass.CWALocationData +import de.rki.coronawarnapp.server.protocols.internal.pt.TraceLocationOuterClass.CrowdNotifierData +import de.rki.coronawarnapp.server.protocols.internal.pt.TraceLocationOuterClass.QRCodePayload +import de.rki.coronawarnapp.util.TimeAndDateExtensions.seconds +import de.rki.coronawarnapp.util.TimeAndDateExtensions.secondsToInstant +import de.rki.coronawarnapp.util.toOkioByteString +import de.rki.coronawarnapp.util.toProtoByteString +import okio.ByteString.Companion.decodeBase64 +import org.joda.time.Instant + +fun TraceLocation.qrCodePayload(): QRCodePayload { + val vendorData = CWALocationData.newBuilder() + .setType(type) + .setDefaultCheckInLengthInMinutes(defaultCheckInLengthInMinutes ?: 0) + .setVersion(TraceLocation.VERSION) + .build() + + val crowdNotifierData = CrowdNotifierData.newBuilder() + .setCryptographicSeed(cryptographicSeed.toProtoByteString()) + .setPublicKey(cnPublicKey.decodeBase64()!!.toProtoByteString()) + .setVersion(TraceLocation.VERSION) + + val locationData = TraceLocationOuterClass.TraceLocation.newBuilder() + .setDescription(description) + .setAddress(address) + .setStartTimestamp(startDate?.seconds ?: 0) + .setEndTimestamp(endDate?.seconds ?: 0) + .setVersion(TraceLocation.VERSION) + .build() + + return QRCodePayload.newBuilder() + .setVendorData(vendorData.toByteString()) + .setCrowdNotifierData(crowdNotifierData) + .setLocationData(locationData) + .setVersion(TraceLocation.VERSION) + .build() +} + +fun QRCodePayload.traceLocation(): TraceLocation { + val cwaLocationData = CWALocationData.parseFrom(vendorData) + return TraceLocation( + version = version, + type = cwaLocationData.type, + defaultCheckInLengthInMinutes = cwaLocationData.defaultCheckInLengthInMinutes, + description = locationData.description, + address = locationData.address, + startDate = locationData.startTimestamp.instant(), + endDate = locationData.endTimestamp.instant(), + cryptographicSeed = crowdNotifierData.cryptographicSeed.toOkioByteString(), + cnPublicKey = crowdNotifierData.publicKey.toOkioByteString().base64() + ) +} + +private fun Long.instant(): Instant? = if (this == 0L) null else secondsToInstant() diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/TraceLocation.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/TraceLocation.kt index 92409f9bf5c1ad34c00b28d063b9be8a17f03edd..bccfcf4b1dcf3768f8bfab1e48bf267cc71f4e4c 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/TraceLocation.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/TraceLocation.kt @@ -1,15 +1,16 @@ package de.rki.coronawarnapp.eventregistration.checkins.qrcode import android.os.Parcelable +import com.google.common.io.BaseEncoding import de.rki.coronawarnapp.eventregistration.storage.entity.TraceLocationEntity import de.rki.coronawarnapp.server.protocols.internal.pt.TraceLocationOuterClass +import kotlinx.parcelize.IgnoredOnParcel import kotlinx.parcelize.Parcelize import okio.ByteString import okio.ByteString.Companion.decodeBase64 +import okio.ByteString.Companion.toByteString import org.joda.time.Instant -const val TRACE_LOCATION_VERSION = 1 - @Parcelize data class TraceLocation( val id: Long = 0L, @@ -21,14 +22,60 @@ data class TraceLocation( val defaultCheckInLengthInMinutes: Int?, val cryptographicSeed: ByteString, val cnPublicKey: String, - val version: Int = TRACE_LOCATION_VERSION, + val version: Int = VERSION, ) : Parcelable { + /** + * Return a url for [TraceLocation] to be used as an input for [QrCodeGenerator] + * URL format https://e.coronawarn.app?v=1#QR_CODE_PAYLOAD_BASE64URL + */ + @IgnoredOnParcel + val locationUrl: String by lazy { + val payloadBytes = qrCodePayload().toByteArray() + val base64Url = BaseEncoding.base64Url().omitPadding().encode(payloadBytes) + AUTHORITY.plus(base64Url) + } + + /** + * Returns a byte sequence that serves as an identifier for the trace location. + * The ID is the byte representation of SHA-256 hash. + */ + @IgnoredOnParcel + val locationId: TraceLocationId by lazy { + val cwaDomain = CWA_GUID.toByteArray() + val payloadBytes = qrCodePayload().toByteArray() + val totalByteSequence = cwaDomain + payloadBytes + totalByteSequence.toByteString().sha256() + } + + /** + * Returns SHA-256 hash of [locationId] which itself may also be SHA-256 hash. + * For privacy reasons TraceTimeIntervalWarnings only offer a hash of the actual locationId. + * + * @see [de.rki.coronawarnapp.eventregistration.checkins.CheckIn] + */ + @IgnoredOnParcel + val locationIdHash: ByteString by lazy { locationId.toTraceLocationIdHash() } + fun isBeforeStartTime(now: Instant): Boolean = startDate?.isAfter(now) ?: false fun isAfterEndTime(now: Instant): Boolean = endDate?.isBefore(now) ?: false + + companion object { + /** + * Trace location version. This is a static data and not calculated from [TraceLocation] + */ + const val VERSION = 1 + + private const val AUTHORITY = "https://e.coronawarn.app?v=$VERSION#" + private const val CWA_GUID = "CWA-GUID" + } } +typealias TraceLocationId = ByteString + +fun TraceLocationId.toTraceLocationIdHash() = sha256() + fun List<TraceLocationEntity>.toTraceLocations() = this.map { it.toTraceLocation() } fun TraceLocationEntity.toTraceLocation() = TraceLocation( diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/VerifiedTraceLocation.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/VerifiedTraceLocation.kt index 6eb4f36452e936ca31e30161c025c074eb3a9948..b524577b5626e51801189188cb5a4db447ff6f69 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/VerifiedTraceLocation.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/VerifiedTraceLocation.kt @@ -7,12 +7,6 @@ import kotlinx.parcelize.IgnoredOnParcel import kotlinx.parcelize.Parceler import kotlinx.parcelize.Parcelize import kotlinx.parcelize.TypeParceler -import okio.Buffer -import okio.ByteString -import okio.ByteString.Companion.encode -import okio.ByteString.Companion.toByteString -import org.joda.time.Instant -import java.util.concurrent.TimeUnit @Parcelize @TypeParceler<TraceLocationOuterClass.TraceLocation, TraceLocationParceler>() @@ -20,42 +14,7 @@ import java.util.concurrent.TimeUnit data class VerifiedTraceLocation( private val protoQrCodePayload: TraceLocationOuterClass.QRCodePayload ) : Parcelable { - - @IgnoredOnParcel private val vendorData by lazy { - TraceLocationOuterClass.CWALocationData.parseFrom(protoQrCodePayload.vendorData) - } - - @IgnoredOnParcel val traceLocation: TraceLocation by lazy { - - TraceLocation( - version = protoQrCodePayload.version, - type = vendorData.type, - description = protoQrCodePayload.locationData.description, - address = protoQrCodePayload.locationData.address, - startDate = protoQrCodePayload.locationData.startTimestamp.toInstant(), - endDate = protoQrCodePayload.locationData.endTimestamp.toInstant(), - defaultCheckInLengthInMinutes = vendorData.defaultCheckInLengthInMinutes, - cryptographicSeed = protoQrCodePayload.crowdNotifierData.cryptographicSeed.toByteArray().toByteString(), - cnPublicKey = protoQrCodePayload.crowdNotifierData.publicKey.toStringUtf8() - ) - } - - @IgnoredOnParcel private val traceLocationHeader: ByteString by lazy { - "CWA-GUID".encode(Charsets.UTF_8) - } - - @IgnoredOnParcel val traceLocationID: ByteString by lazy { - Buffer() - .write(traceLocationHeader) - .write(protoQrCodePayload.toByteArray()) - .readByteString() - } - - /** - * Converts time in seconds into [Instant] - */ - private fun Long.toInstant() = - if (this == 0L) null else Instant.ofEpochMilli(TimeUnit.SECONDS.toMillis(this)) + @IgnoredOnParcel val traceLocation: TraceLocation = protoQrCodePayload.traceLocation() } private object TraceLocationParceler : Parceler<TraceLocationOuterClass.TraceLocation> { diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/events/server/qrcodepostertemplate/DefaultQrCodePosterTemplateSource.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/events/server/qrcodepostertemplate/DefaultQrCodePosterTemplateSource.kt new file mode 100644 index 0000000000000000000000000000000000000000..eac888cca0185974c5ba86be10fccb7b96b0f742 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/events/server/qrcodepostertemplate/DefaultQrCodePosterTemplateSource.kt @@ -0,0 +1,13 @@ +package de.rki.coronawarnapp.eventregistration.events.server.qrcodepostertemplate + +import android.content.Context +import dagger.Reusable +import de.rki.coronawarnapp.util.di.AppContext +import javax.inject.Inject + +@Reusable +class DefaultQrCodePosterTemplateSource @Inject constructor(@AppContext private val context: Context) { + + fun getDefaultQrCodePosterTemplate() = + context.assets.open("default_qr_code_poster_template_android.bin").readBytes() +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/events/server/qrcodepostertemplate/QrCodePosterTemplateServer.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/events/server/qrcodepostertemplate/QrCodePosterTemplateServer.kt index dccb0432de5badcba1c0a4277a4ddd161284f24d..9e24711b07252248f5d4fe9b932168efe87db3cd 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/events/server/qrcodepostertemplate/QrCodePosterTemplateServer.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/events/server/qrcodepostertemplate/QrCodePosterTemplateServer.kt @@ -1,11 +1,11 @@ package de.rki.coronawarnapp.eventregistration.events.server.qrcodepostertemplate +import androidx.annotation.VisibleForTesting import com.google.protobuf.InvalidProtocolBufferException import de.rki.coronawarnapp.server.protocols.internal.pt.QrCodePosterTemplate import de.rki.coronawarnapp.util.ZipHelper.readIntoMap import de.rki.coronawarnapp.util.ZipHelper.unzip import de.rki.coronawarnapp.util.security.SignatureValidation -import retrofit2.HttpException import timber.log.Timber import javax.inject.Inject import javax.inject.Singleton @@ -13,48 +13,54 @@ import javax.inject.Singleton @Singleton class QrCodePosterTemplateServer @Inject constructor( private val api: QrCodePosterTemplateApiV1, + private val defaultTemplateSource: DefaultQrCodePosterTemplateSource, private val signatureValidation: SignatureValidation ) { suspend fun downloadQrCodePosterTemplate(): QrCodePosterTemplate.QRCodePosterTemplateAndroid { Timber.d("Start download of QR-Code poster template.") + val binaryTemplate = getTemplateFromApiOrCache() - val response = api.getQrCodePosterTemplate() - Timber.d("Received: %s", response) - - if (!response.isSuccessful) { - // TODO return cached or default response - throw HttpException(response) - } - if (response.body() == null) { - throw IllegalStateException("Response is successful, but body is empty.") + return try { + QrCodePosterTemplate.QRCodePosterTemplateAndroid.parseFrom(binaryTemplate) + } catch (exception: InvalidProtocolBufferException) { + throw QrCodePosterTemplateInvalidResponseException( + message = "QR Code poster template could not be parsed", + cause = exception + ) } + } - val fileMap = response.body()!!.byteStream().unzip().readIntoMap() - - val exportBinary = fileMap[EXPORT_BINARY_FILE_NAME] - val exportSignature = fileMap[EXPORT_SIGNATURE_FILE_NAME] + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + suspend fun getTemplateFromApiOrCache(): ByteArray { + return try { + val response = api.getQrCodePosterTemplate() + if (response.body() == null) { + throw IllegalStateException("Response is successful, but body is empty.") + } - if (exportBinary == null || exportSignature == null) { - throw QrCodePosterTemplateInvalidResponseException(message = "Unknown files: ${fileMap.keys}") - } + val fileMap = response.body()!!.byteStream().unzip().readIntoMap() - val hasValidSignature = signatureValidation.hasValidSignature( - exportBinary, - SignatureValidation.parseTEKStyleSignature(exportSignature) - ) + val exportBinary = fileMap[EXPORT_BINARY_FILE_NAME] + val exportSignature = fileMap[EXPORT_SIGNATURE_FILE_NAME] - if (!hasValidSignature) { - throw QrCodePosterTemplateInvalidResponseException(message = "Invalid Signature!") - } + if (exportBinary == null || exportSignature == null) { + throw QrCodePosterTemplateInvalidResponseException(message = "Unknown files: ${fileMap.keys}") + } - return try { - QrCodePosterTemplate.QRCodePosterTemplateAndroid.parseFrom(exportBinary) - } catch (exception: InvalidProtocolBufferException) { - throw QrCodePosterTemplateInvalidResponseException( - message = "QR Code poster template could not be parsed", - cause = exception + val hasValidSignature = signatureValidation.hasValidSignature( + exportBinary, + SignatureValidation.parseTEKStyleSignature(exportSignature) ) + + if (!hasValidSignature) { + throw QrCodePosterTemplateInvalidResponseException(message = "Invalid Signature!") + } + + exportBinary + } catch (exception: Exception) { + Timber.d(exception, "Response is not successful, trying to load template from cache") + defaultTemplateSource.getDefaultQrCodePosterTemplate() } } 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 index 2df991c639036753b347014adcd46b1fb17f337b..2616685c8a0a15690dc2aa1d472b806d31b1019e 100644 --- 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 @@ -11,7 +11,6 @@ import org.joda.time.Instant data class TraceLocationCheckInEntity( @PrimaryKey(autoGenerate = true) @ColumnInfo(name = "id") val id: Long = 0L, @ColumnInfo(name = "traceLocationIdBase64") val traceLocationIdBase64: String, - @ColumnInfo(name = "traceLocationIdHashBase64") val traceLocationIdHashBase64: String, @ColumnInfo(name = "version") val version: Int, @ColumnInfo(name = "type") val type: Int, @ColumnInfo(name = "description") val description: String, @@ -30,7 +29,6 @@ data class TraceLocationCheckInEntity( fun TraceLocationCheckInEntity.toCheckIn() = CheckIn( id = id, traceLocationId = traceLocationIdBase64.decodeBase64()!!, - traceLocationIdHash = traceLocationIdHashBase64.decodeBase64()!!, version = version, type = type, description = description, diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/playbook/BackgroundNoise.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/playbook/BackgroundNoise.kt index cce89d6506f7ccdb7fb2bc872f11f5a76aa5991e..57791940bf1a983cda707f6804b8c0d6aee22db4 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/playbook/BackgroundNoise.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/playbook/BackgroundNoise.kt @@ -10,11 +10,12 @@ import kotlin.random.Random @Singleton class BackgroundNoise @Inject constructor( private val submissionSettings: SubmissionSettings, - private val playbook: Playbook + private val playbook: Playbook, + private val backgroundWorkScheduler: BackgroundWorkScheduler ) { fun scheduleDummyPattern() { if (BackgroundConstants.NUMBER_OF_DAYS_TO_RUN_PLAYBOOK > 0) - BackgroundWorkScheduler.scheduleBackgroundNoisePeriodicWork() + backgroundWorkScheduler.scheduleBackgroundNoisePeriodicWork() } suspend fun foregroundScheduleCheck() { diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/checkins/checkout/CheckOutNotification.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/checkins/checkout/CheckOutNotification.kt index 0a169254e27d98d1d747e0f9c2a09d9594f3edf6..419cc7637bc798614d8c50fe7fca07e156cdaa29 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/checkins/checkout/CheckOutNotification.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/checkins/checkout/CheckOutNotification.kt @@ -5,7 +5,7 @@ import androidx.core.app.NotificationCompat import dagger.Reusable import de.rki.coronawarnapp.R import de.rki.coronawarnapp.notification.NotificationConstants -import de.rki.coronawarnapp.presencetracing.common.TraceLocationNotifications +import de.rki.coronawarnapp.presencetracing.common.PresenceTracingNotifications import de.rki.coronawarnapp.ui.launcher.LauncherActivity import de.rki.coronawarnapp.util.device.ForegroundState import de.rki.coronawarnapp.util.di.AppContext @@ -19,7 +19,7 @@ import javax.inject.Inject class CheckOutNotification @Inject constructor( @AppContext private val context: Context, private val foregroundState: ForegroundState, - private val notificationHelper: TraceLocationNotifications, + private val notificationHelper: PresenceTracingNotifications, private val deepLinkBuilderFactory: NavDeepLinkBuilderFactory, ) { diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/common/TraceLocationNotifications.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/common/PresenceTracingNotifications.kt similarity index 96% rename from Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/common/TraceLocationNotifications.kt rename to Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/common/PresenceTracingNotifications.kt index 349096d2d218959b8970ac24f639db9c5068489c..7fd3c1f255bdec1cec5d065dfe584bae79f4521f 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/common/TraceLocationNotifications.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/common/PresenceTracingNotifications.kt @@ -19,18 +19,18 @@ import timber.log.Timber import javax.inject.Inject /** - * Helper to send notifications on the notification channel for trace location related events. + * Helper to send notifications on the notification channel for presence tracing related events. * * Also see **[de.rki.coronawarnapp.notification.GeneralNotifications]** */ @Reusable -class TraceLocationNotifications @Inject constructor( +class PresenceTracingNotifications @Inject constructor( @AppContext private val context: Context, private val apiLevel: ApiLevel, private val notificationManagerCompat: NotificationManagerCompat, ) { - private val channelId = "${context.packageName}.notification.traceLocationChannelId" + private val channelId = "${context.packageName}.notification.presenceTracingChannelId" private var isNotificationChannelSetup = false @TargetApi(Build.VERSION_CODES.O) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/CheckInWarningMatcher.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/CheckInWarningMatcher.kt deleted file mode 100644 index 66f82291fa3d6e0bf3148bebacf473626cfcb8ae..0000000000000000000000000000000000000000 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/CheckInWarningMatcher.kt +++ /dev/null @@ -1,156 +0,0 @@ -package de.rki.coronawarnapp.presencetracing.risk - -import androidx.annotation.VisibleForTesting -import de.rki.coronawarnapp.eventregistration.checkins.CheckIn -import de.rki.coronawarnapp.eventregistration.checkins.CheckInRepository -import de.rki.coronawarnapp.eventregistration.checkins.download.TraceTimeIntervalWarningPackage -import de.rki.coronawarnapp.eventregistration.checkins.download.TraceTimeIntervalWarningRepository -import de.rki.coronawarnapp.eventregistration.checkins.split.splitByMidnightUTC -import de.rki.coronawarnapp.server.protocols.internal.pt.TraceWarning -import de.rki.coronawarnapp.util.coroutine.DispatcherProvider -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Deferred -import kotlinx.coroutines.async -import kotlinx.coroutines.awaitAll -import kotlinx.coroutines.flow.firstOrNull -import kotlinx.coroutines.withContext -import okio.ByteString.Companion.toByteString -import org.joda.time.Instant -import timber.log.Timber -import java.lang.reflect.Modifier.PRIVATE -import javax.inject.Inject -import kotlin.coroutines.CoroutineContext - -class CheckInWarningMatcher @Inject constructor( - private val checkInsRepository: CheckInRepository, - private val traceTimeIntervalWarningRepository: TraceTimeIntervalWarningRepository, - private val presenceTracingRiskRepository: PresenceTracingRiskRepository, - private val dispatcherProvider: DispatcherProvider -) { - suspend fun execute(): List<CheckInWarningOverlap> { - - presenceTracingRiskRepository.deleteStaleData() - - val checkIns = checkInsRepository.allCheckIns.firstOrNull() - if (checkIns.isNullOrEmpty()) { - Timber.i("No check-ins available. Deleting all matches.") - presenceTracingRiskRepository.deleteAllMatches() - presenceTracingRiskRepository.reportSuccessfulCalculation(emptyList()) - return emptyList() - } - - val warningPackages = traceTimeIntervalWarningRepository.allWarningPackages.firstOrNull() - - if (warningPackages.isNullOrEmpty()) { - // nothing to be done here - return emptyList() - } - - val splitCheckIns = checkIns.flatMap { it.splitByMidnightUTC() } - - val matchLists = createMatchingLaunchers( - splitCheckIns, - warningPackages, - dispatcherProvider.IO - ) - .awaitAll() - - if (matchLists.contains(null)) { - Timber.e("Error occurred during matching. Abort calculation.") - presenceTracingRiskRepository.reportFailedCalculation() - return emptyList() - } - - // delete stale matches from new packages and mark packages as processed - warningPackages.forEach { - presenceTracingRiskRepository.deleteMatchesOfPackage(it.warningPackageId) - presenceTracingRiskRepository.markPackageProcessed(it.warningPackageId) - } - val matches = matchLists.filterNotNull().flatten() - - presenceTracingRiskRepository.reportSuccessfulCalculation(matches) - - return matches - } -} - -@VisibleForTesting(otherwise = PRIVATE) -internal suspend fun createMatchingLaunchers( - checkIns: List<CheckIn>, - warningPackages: List<TraceTimeIntervalWarningPackage>, - coroutineContext: CoroutineContext -): Collection<Deferred<List<CheckInWarningOverlap>?>> { - - val launcher: CoroutineScope.( - List<CheckIn>, - List<TraceTimeIntervalWarningPackage> - ) -> Deferred<List<CheckInWarningOverlap>?> = - { list, packageChunk -> - async { - try { - packageChunk.flatMap { - findMatches(list, it) - } - } catch (e: Throwable) { - Timber.e("Failed to process packages $packageChunk") - null - } - } - } - - // at most 4 parallel processes - val chunkSize = (checkIns.size / 4) + 1 - - return warningPackages.chunked(chunkSize).map { packageChunk -> - withContext(context = coroutineContext) { - launcher(checkIns, packageChunk) - } - } -} - -@VisibleForTesting(otherwise = PRIVATE) -internal suspend fun findMatches( - checkIns: List<CheckIn>, - warningPackage: TraceTimeIntervalWarningPackage -): List<CheckInWarningOverlap> { - return warningPackage - .extractTraceTimeIntervalWarnings() - .flatMap { warning -> - checkIns - .mapNotNull { checkIn -> - checkIn.calculateOverlap(warning, warningPackage.warningPackageId).also { overlap -> - if (overlap == null) { - Timber.d("No match/overlap found for $checkIn and $warning") - } else { - Timber.i("Overlap found $overlap") - } - } - } - } -} - -@VisibleForTesting(otherwise = PRIVATE) -internal fun CheckIn.calculateOverlap( - warning: TraceWarning.TraceTimeIntervalWarning, - traceWarningPackageId: String -): CheckInWarningOverlap? { - - if (warning.locationIdHash.toByteArray().toByteString() != traceLocationIdHash) return null - - val warningStartMillis = warning.startIntervalNumber.tenMinIntervalToMillis() - val warningEndMillis = (warning.startIntervalNumber + warning.period).tenMinIntervalToMillis() - - val overlapStartMillis = kotlin.math.max(checkInStart.millis, warningStartMillis) - val overlapEndMillis = kotlin.math.min(checkInEnd.millis, warningEndMillis) - val overlapMillis = overlapEndMillis - overlapStartMillis - - if (overlapMillis <= 0) return null - - return CheckInWarningOverlap( - checkInId = id, - transmissionRiskLevel = warning.transmissionRiskLevel, - traceWarningPackageId = traceWarningPackageId, - startTime = Instant.ofEpochMilli(overlapStartMillis), - endTime = Instant.ofEpochMilli(overlapEndMillis) - ) -} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/PresenceTracingRiskRepository.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/PresenceTracingRiskRepository.kt deleted file mode 100644 index 0d0f7add0e2639932e641501a87abccfd259fa74..0000000000000000000000000000000000000000 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/PresenceTracingRiskRepository.kt +++ /dev/null @@ -1,224 +0,0 @@ -package de.rki.coronawarnapp.presencetracing.risk - -import androidx.room.ColumnInfo -import androidx.room.Dao -import androidx.room.Entity -import androidx.room.ForeignKey -import androidx.room.Insert -import androidx.room.OnConflictStrategy.REPLACE -import androidx.room.PrimaryKey -import androidx.room.Query -import androidx.room.TypeConverter -import de.rki.coronawarnapp.eventregistration.storage.entity.TraceLocationCheckInEntity -import de.rki.coronawarnapp.risk.RiskState -import de.rki.coronawarnapp.risk.TraceLocationCheckInRisk -import de.rki.coronawarnapp.util.TimeAndDateExtensions.toLocalDateUtc -import de.rki.coronawarnapp.util.TimeStamper -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.map -import org.joda.time.Days -import org.joda.time.Instant -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class PresenceTracingRiskRepository @Inject constructor( - private val presenceTracingRiskCalculator: PresenceTracingRiskCalculator, - private val databaseFactory: PresenceTracingRiskDatabase.Factory, - private val timeStamper: TimeStamper -) { - - private val database by lazy { - databaseFactory.create() - } - - private val traceTimeIntervalMatchDao by lazy { - database.traceTimeIntervalMatchDao() - } - - private val riskLevelResultDao by lazy { - database.presenceTracingRiskLevelResultDao() - } - - private val allMatches = traceTimeIntervalMatchDao.allMatches().map { list -> - list.map { - it.toModel() - } - } - - private val normalizedTime = allMatches.map { - presenceTracingRiskCalculator.calculateNormalizedTime(it) - } - - private val fifteenDaysAgo: Instant - get() = timeStamper.nowUTC.minus(Days.days(15).toStandardDuration()) - - val traceLocationCheckInRiskStates: Flow<List<TraceLocationCheckInRisk>> = - normalizedTime.map { - presenceTracingRiskCalculator.calculateCheckInRiskPerDay(it) - } - - val presenceTracingDayRisk: Flow<List<PresenceTracingDayRisk>> = - normalizedTime.map { - presenceTracingRiskCalculator.calculateAggregatedRiskPerDay(it) - } - - internal suspend fun reportSuccessfulCalculation(list: List<CheckInWarningOverlap>) { - traceTimeIntervalMatchDao.insert(list.map { it.toEntity() }) - val last14days = normalizedTime.first().filter { it.localDateUtc.isAfter(fifteenDaysAgo.toLocalDateUtc()) } - val risk = presenceTracingRiskCalculator.calculateTotalRisk(last14days) - add( - PtRiskLevelResult( - timeStamper.nowUTC, - risk - ) - ) - } - - internal suspend fun deleteStaleData() { - traceTimeIntervalMatchDao.deleteOlderThan(fifteenDaysAgo.millis) - riskLevelResultDao.deleteOlderThan(fifteenDaysAgo.millis) - } - - internal suspend fun markPackageProcessed(warningPackageId: String) { - // TODO - } - - internal suspend fun deleteMatchesOfPackage(warningPackageId: String) { - traceTimeIntervalMatchDao.deleteMatchesForPackage(warningPackageId) - } - - suspend fun deleteAllMatches() { - traceTimeIntervalMatchDao.deleteAll() - } - - fun latestAndLastSuccessful() = riskLevelResultDao.latestAndLastSuccessful().map { list -> - list.map { entity -> - entity.toModel() - } - } - - fun latestEntries(limit: Int) = riskLevelResultDao.latestEntries(limit).map { list -> - list.map { entity -> - entity.toModel() - } - } - - fun add(riskLevelResult: PtRiskLevelResult) { - riskLevelResultDao.insert(riskLevelResult.toEntity()) - } - - fun reportFailedCalculation() { - add( - PtRiskLevelResult( - timeStamper.nowUTC, - RiskState.CALCULATION_FAILED - ) - ) - } -} - -@Dao -interface TraceTimeIntervalMatchDao { - - @Query("SELECT * FROM TraceTimeIntervalMatchEntity") - fun allMatches(): Flow<List<TraceTimeIntervalMatchEntity>> - - @Query("DELETE FROM TraceTimeIntervalMatchEntity") - suspend fun deleteAll() - - @Query("DELETE FROM TraceTimeIntervalMatchEntity WHERE endTimeMillis < :endTimeMillis") - suspend fun deleteOlderThan(endTimeMillis: Long) - - @Query("DELETE FROM TraceTimeIntervalMatchEntity WHERE traceWarningPackageId = :warningPackageId") - suspend fun deleteMatchesForPackage(warningPackageId: String) - - @Insert - suspend fun insert(entities: List<TraceTimeIntervalMatchEntity>) -} - -@Entity -data class TraceTimeIntervalMatchEntity( - @PrimaryKey(autoGenerate = true) @ColumnInfo(name = "id") val id: Long? = null, - @ForeignKey( - entity = TraceLocationCheckInEntity::class, - parentColumns = ["id"], - childColumns = ["checkInId"], - onDelete = ForeignKey.CASCADE - ) - @ColumnInfo(name = "checkInId") val checkInId: Long, - @ColumnInfo(name = "traceWarningPackageId") val traceWarningPackageId: String, - @ColumnInfo(name = "transmissionRiskLevel") val transmissionRiskLevel: Int, - @ColumnInfo(name = "startTimeMillis") val startTimeMillis: Long, - @ColumnInfo(name = "endTimeMillis") val endTimeMillis: Long -) - -private fun CheckInWarningOverlap.toEntity() = TraceTimeIntervalMatchEntity( - checkInId = checkInId, - traceWarningPackageId = traceWarningPackageId, - transmissionRiskLevel = transmissionRiskLevel, - startTimeMillis = startTime.millis, - endTimeMillis = endTime.millis -) - -private fun TraceTimeIntervalMatchEntity.toModel() = CheckInWarningOverlap( - checkInId = checkInId, - traceWarningPackageId = traceWarningPackageId, - transmissionRiskLevel = transmissionRiskLevel, - startTime = Instant.ofEpochMilli(startTimeMillis), - endTime = Instant.ofEpochMilli(endTimeMillis) -) - -@Suppress("MaxLineLength") -@Dao -interface PresenceTracingRiskLevelResultDao { - @Query("SELECT * FROM (SELECT * FROM PresenceTracingRiskLevelResultEntity ORDER BY calculatedAtMillis DESC LIMIT 1) UNION ALL SELECT * FROM (SELECT * FROM PresenceTracingRiskLevelResultEntity where riskStateCode is not 0 ORDER BY calculatedAtMillis DESC LIMIT 1)") - fun latestAndLastSuccessful(): Flow<List<PresenceTracingRiskLevelResultEntity>> - - @Query("SELECT * FROM PresenceTracingRiskLevelResultEntity ORDER BY calculatedAtMillis DESC LIMIT :limit") - fun latestEntries(limit: Int): Flow<List<PresenceTracingRiskLevelResultEntity>> - - @Insert(onConflict = REPLACE) - fun insert(entity: PresenceTracingRiskLevelResultEntity) - - @Query("DELETE FROM PresenceTracingRiskLevelResultEntity WHERE calculatedAtMillis < :calculatedAtMillis") - suspend fun deleteOlderThan(calculatedAtMillis: Long) -} - -@Entity -data class PresenceTracingRiskLevelResultEntity( - @PrimaryKey @ColumnInfo(name = "calculatedAtMillis") val calculatedAtMillis: Long, - @ColumnInfo(name = "riskStateCode")val riskState: RiskState -) - -private fun PresenceTracingRiskLevelResultEntity.toModel() = PtRiskLevelResult( - calculatedAt = Instant.ofEpochMilli((calculatedAtMillis)), - riskState = riskState -) - -private fun PtRiskLevelResult.toEntity() = PresenceTracingRiskLevelResultEntity( - calculatedAtMillis = calculatedAt.millis, - riskState = riskState -) - -class RiskStateConverter { - @TypeConverter - fun toRiskStateCode(value: Int?): RiskState? = value?.toRiskState() - - @TypeConverter - fun fromRiskStateCode(code: RiskState?): Int? = code?.toCode() - - private fun RiskState.toCode() = when (this) { - RiskState.CALCULATION_FAILED -> 0 - RiskState.LOW_RISK -> 1 - RiskState.INCREASED_RISK -> 2 - } - - private fun Int.toRiskState() = when (this) { - 0 -> RiskState.CALCULATION_FAILED - 1 -> RiskState.LOW_RISK - 2 -> RiskState.INCREASED_RISK - else -> null - } -} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/PtRiskLevelResult.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/PtRiskLevelResult.kt index 1fa338aab48560329f4776711fde3e6439c59272..62b25b76061904b9d6a11001752fdce79f8f3dda 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/PtRiskLevelResult.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/PtRiskLevelResult.kt @@ -1,5 +1,6 @@ package de.rki.coronawarnapp.presencetracing.risk +import de.rki.coronawarnapp.presencetracing.risk.calculation.PresenceTracingDayRisk import de.rki.coronawarnapp.risk.RiskState import org.joda.time.Instant import org.joda.time.LocalDate @@ -7,6 +8,7 @@ import org.joda.time.LocalDate data class PtRiskLevelResult( val calculatedAt: Instant, val riskState: RiskState, + // only available for the last calculation if successful, otherwise null val presenceTracingDayRisk: List<PresenceTracingDayRisk>? = null ) { diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/TraceLocationCheckInRisk.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/TraceLocationCheckInRisk.kt similarity index 62% rename from Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/TraceLocationCheckInRisk.kt rename to Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/TraceLocationCheckInRisk.kt index a43306755b43665da40c4d218969c7025d7fa16a..a9d2d3d016e37984cf5174100bc2d3d60d995801 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/TraceLocationCheckInRisk.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/TraceLocationCheckInRisk.kt @@ -1,5 +1,6 @@ -package de.rki.coronawarnapp.risk +package de.rki.coronawarnapp.presencetracing.risk +import de.rki.coronawarnapp.risk.RiskState import org.joda.time.LocalDate interface TraceLocationCheckInRisk { diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/calculation/CheckInWarningMatcher.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/calculation/CheckInWarningMatcher.kt new file mode 100644 index 0000000000000000000000000000000000000000..8c318df97711ea7ca4108ee90bcce03a155b7540 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/calculation/CheckInWarningMatcher.kt @@ -0,0 +1,143 @@ +package de.rki.coronawarnapp.presencetracing.risk.calculation + +import androidx.annotation.VisibleForTesting +import de.rki.coronawarnapp.eventregistration.checkins.CheckIn +import de.rki.coronawarnapp.eventregistration.checkins.split.splitByMidnightUTC +import de.rki.coronawarnapp.presencetracing.warning.storage.TraceWarningPackage +import de.rki.coronawarnapp.server.protocols.internal.pt.TraceWarning +import de.rki.coronawarnapp.util.coroutine.DispatcherProvider +import de.rki.coronawarnapp.util.toOkioByteString +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.withContext +import org.joda.time.Instant +import timber.log.Timber +import java.lang.reflect.Modifier.PRIVATE +import javax.inject.Inject +import kotlin.coroutines.CoroutineContext + +class CheckInWarningMatcher @Inject constructor( + private val dispatcherProvider: DispatcherProvider +) { + suspend fun process( + checkIns: List<CheckIn>, + warningPackages: List<TraceWarningPackage> + ): Result { + val splitCheckIns = checkIns.flatMap { it.splitByMidnightUTC() } + + val matchLists: List<List<MatchesPerPackage>?> = runMatchingLaunchers( + splitCheckIns, + warningPackages, + dispatcherProvider.IO + ) + + val successful = if (matchLists.contains(null)) { + Timber.e("Calculation partially failed.") + false + } else { + Timber.d("Matching was successful.") + true + } + + return Result( + successful = successful, + processedPackages = matchLists.filterNotNull().flatten() + ) + } + + data class Result( + val successful: Boolean, + val processedPackages: Collection<MatchesPerPackage> = emptyList(), + ) + + @VisibleForTesting(otherwise = PRIVATE) + internal suspend fun runMatchingLaunchers( + checkIns: List<CheckIn>, + warningPackages: List<TraceWarningPackage>, + coroutineContext: CoroutineContext + ): List<List<MatchesPerPackage>?> { + + val launcher: CoroutineScope.( + List<CheckIn>, + List<TraceWarningPackage> + ) -> Deferred<List<MatchesPerPackage>?> = { list, packageChunk -> + async { + try { + packageChunk.map { + val overlaps = findMatches(list, it) + Timber.d("%d overlaps for %s", overlaps.size, it.packageId) + MatchesPerPackage(warningPackage = it, overlaps = overlaps) + } + } catch (e: Throwable) { + Timber.e(e, "Failed to process packages $packageChunk") + null + } + } + } + + // at most 4 parallel processes + val chunkSize = (checkIns.size / 4) + 1 + + return warningPackages.chunked(chunkSize).map { packageChunk -> + withContext(context = coroutineContext) { + launcher(checkIns, packageChunk) + } + }.awaitAll() + } + + data class MatchesPerPackage( + val warningPackage: TraceWarningPackage, + val overlaps: List<CheckInWarningOverlap>, + ) +} + +@VisibleForTesting(otherwise = PRIVATE) +internal suspend fun findMatches( + checkIns: List<CheckIn>, + warningPackage: TraceWarningPackage +): List<CheckInWarningOverlap> { + return warningPackage + .extractWarnings() + .flatMap { warning -> + checkIns + .mapNotNull { checkIn -> + checkIn.calculateOverlap(warning, warningPackage.packageId).also { overlap -> + if (overlap == null) { + Timber.v("No match found for $checkIn and $warning") + } else { + Timber.w("Overlap was found $overlap") + } + } + } + } +} + +@VisibleForTesting(otherwise = PRIVATE) +internal fun CheckIn.calculateOverlap( + warning: TraceWarning.TraceTimeIntervalWarning, + traceWarningPackageId: String +): CheckInWarningOverlap? { + if (warning.locationIdHash.toOkioByteString() != traceLocationIdHash) return null + + val warningStartMillis = warning.startIntervalNumber.tenMinIntervalToMillis() + val warningEndMillis = (warning.startIntervalNumber + warning.period).tenMinIntervalToMillis() + + val overlapStartMillis = kotlin.math.max(checkInStart.millis, warningStartMillis) + val overlapEndMillis = kotlin.math.min(checkInEnd.millis, warningEndMillis) + val overlapMillis = overlapEndMillis - overlapStartMillis + + if (overlapMillis <= 0) { + Timber.i("No overlap (%dms) with match %s (%s)", overlapMillis, description, traceLocationIdHash) + return null + } + + return CheckInWarningOverlap( + checkInId = id, + transmissionRiskLevel = warning.transmissionRiskLevel, + traceWarningPackageId = traceWarningPackageId, + startTime = Instant.ofEpochMilli(overlapStartMillis), + endTime = Instant.ofEpochMilli(overlapEndMillis) + ) +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/PresenceTracingConversions.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/calculation/PresenceTracingConversions.kt similarity index 73% rename from Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/PresenceTracingConversions.kt rename to Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/calculation/PresenceTracingConversions.kt index 1d5c986675ec528039fef44acda4d9ef162a67f6..4e529779dc78875644dda7905f5d250866b606f0 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/PresenceTracingConversions.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/calculation/PresenceTracingConversions.kt @@ -1,12 +1,11 @@ -package de.rki.coronawarnapp.presencetracing.risk +package de.rki.coronawarnapp.presencetracing.risk.calculation import de.rki.coronawarnapp.risk.RiskState import de.rki.coronawarnapp.server.protocols.internal.v2.RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping.RiskLevel +import java.util.concurrent.TimeUnit // converts number of 10min intervals into milliseconds -internal fun Int.tenMinIntervalToMillis() = this * MILLIS_IN_MIN - -private const val MILLIS_IN_MIN = 600L * 1000L +internal fun Int.tenMinIntervalToMillis() = this * TimeUnit.MINUTES.toMillis(10L) fun RiskLevel.mapToRiskState(): RiskState { return when (this) { diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/PresenceTracingRiskCalculator.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/calculation/PresenceTracingRiskCalculator.kt similarity index 97% rename from Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/PresenceTracingRiskCalculator.kt rename to Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/calculation/PresenceTracingRiskCalculator.kt index 1c2bd002f2aa3bdb6499247ce107f69fa68151b4..568273ff125bcc1716ec8bc64d9ead1a201bc339 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/PresenceTracingRiskCalculator.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/calculation/PresenceTracingRiskCalculator.kt @@ -1,4 +1,4 @@ -package de.rki.coronawarnapp.presencetracing.risk +package de.rki.coronawarnapp.presencetracing.risk.calculation import de.rki.coronawarnapp.risk.RiskState import javax.inject.Inject diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/PresenceTracingRiskMapper.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/calculation/PresenceTracingRiskMapper.kt similarity index 97% rename from Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/PresenceTracingRiskMapper.kt rename to Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/calculation/PresenceTracingRiskMapper.kt index 6558491cac825c86b3d8a09b44fd17349e46ebf1..1cebd0aec5de5430225dbe41568f31b4e326b5a4 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/PresenceTracingRiskMapper.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/calculation/PresenceTracingRiskMapper.kt @@ -1,4 +1,4 @@ -package de.rki.coronawarnapp.presencetracing.risk +package de.rki.coronawarnapp.presencetracing.risk.calculation import de.rki.coronawarnapp.appconfig.AppConfigProvider import de.rki.coronawarnapp.appconfig.PresenceTracingRiskCalculationParamContainer diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/PresenceTracingRiskModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/calculation/PresenceTracingRiskModel.kt similarity index 90% rename from Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/PresenceTracingRiskModel.kt rename to Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/calculation/PresenceTracingRiskModel.kt index 91591e74a3c4a833682c6482dab82a7fa8c15412..4fc9b892d8004c7ceb6fdbcb08ce64a728ce3c5f 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/PresenceTracingRiskModel.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/calculation/PresenceTracingRiskModel.kt @@ -1,7 +1,7 @@ -package de.rki.coronawarnapp.presencetracing.risk +package de.rki.coronawarnapp.presencetracing.risk.calculation +import de.rki.coronawarnapp.presencetracing.risk.TraceLocationCheckInRisk import de.rki.coronawarnapp.risk.RiskState -import de.rki.coronawarnapp.risk.TraceLocationCheckInRisk import de.rki.coronawarnapp.util.TimeAndDateExtensions.toLocalDateUtc import org.joda.time.DateTimeConstants import org.joda.time.Duration diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/execution/PresenceTracingWarningTask.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/execution/PresenceTracingWarningTask.kt new file mode 100644 index 0000000000000000000000000000000000000000..e0d9556af8f02e0ee7577eeebdcbee4a9c0f4a5b --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/execution/PresenceTracingWarningTask.kt @@ -0,0 +1,158 @@ +package de.rki.coronawarnapp.presencetracing.risk.execution + +import de.rki.coronawarnapp.appconfig.AppConfigProvider +import de.rki.coronawarnapp.bugreporting.reportProblem +import de.rki.coronawarnapp.eventregistration.checkins.CheckInRepository +import de.rki.coronawarnapp.exception.ExceptionCategory +import de.rki.coronawarnapp.exception.reporting.report +import de.rki.coronawarnapp.presencetracing.risk.calculation.CheckInWarningMatcher +import de.rki.coronawarnapp.presencetracing.risk.storage.PresenceTracingRiskRepository +import de.rki.coronawarnapp.presencetracing.warning.download.TraceWarningPackageSyncTool +import de.rki.coronawarnapp.presencetracing.warning.storage.TraceWarningRepository +import de.rki.coronawarnapp.task.Task +import de.rki.coronawarnapp.task.TaskCancellationException +import de.rki.coronawarnapp.task.TaskFactory +import de.rki.coronawarnapp.task.common.DefaultProgress +import de.rki.coronawarnapp.util.TimeStamper +import kotlinx.coroutines.channels.ConflatedBroadcastChannel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.asFlow +import kotlinx.coroutines.flow.firstOrNull +import org.joda.time.Duration +import org.joda.time.Instant +import timber.log.Timber +import javax.inject.Inject +import javax.inject.Provider + +class PresenceTracingWarningTask @Inject constructor( + private val timeStamper: TimeStamper, + private val syncTool: TraceWarningPackageSyncTool, + private val checkInWarningMatcher: CheckInWarningMatcher, + private val presenceTracingRiskRepository: PresenceTracingRiskRepository, + private val traceWarningRepository: TraceWarningRepository, + private val checkInsRepository: CheckInRepository, +) : Task<DefaultProgress, PresenceTracingWarningTask.Result> { + + private val internalProgress = ConflatedBroadcastChannel<DefaultProgress>() + override val progress: Flow<DefaultProgress> = internalProgress.asFlow() + + private var isCanceled = false + + override suspend fun run(arguments: Task.Arguments): Result = try { + Timber.d("Running with arguments=%s", arguments) + + try { + doWork() + } catch (e: Exception) { + // We need to reported a failed calculation to update the risk card state + presenceTracingRiskRepository.reportCalculation(successful = false) + throw e + } + } catch (error: Exception) { + Timber.tag(TAG).e(error) + error.report(ExceptionCategory.EXPOSURENOTIFICATION) + throw error + } finally { + Timber.i("Finished (isCanceled=$isCanceled).") + internalProgress.close() + } + + private suspend fun doWork(): Result { + val nowUTC = timeStamper.nowUTC + + Timber.tag(TAG).d("Running package sync.") + syncTool.syncPackages() + + checkCancel() + + presenceTracingRiskRepository.deleteStaleData() + + val checkIns = checkInsRepository.allCheckIns.firstOrNull() ?: emptyList() + Timber.tag(TAG).d("There are %d check-ins to match against.", checkIns.size) + + if (checkIns.isEmpty()) { + Timber.tag(TAG).i("No check-ins available. Deleting all matches.") + presenceTracingRiskRepository.deleteAllMatches() + + presenceTracingRiskRepository.reportCalculation(successful = true) + + return Result(calculatedAt = nowUTC) + } + + val unprocessedPackages = traceWarningRepository.unprocessedWarningPackages.firstOrNull() ?: emptyList() + Timber.tag(TAG).d("There are %d unprocessed warning packages.", unprocessedPackages.size) + + if (unprocessedPackages.isEmpty()) { + Timber.tag(TAG).i("No new warning packages available.") + + presenceTracingRiskRepository.reportCalculation(successful = true) + + return Result(calculatedAt = nowUTC) + } + + Timber.tag(TAG).d("Running check-in matcher.") + val matcherResult = checkInWarningMatcher.process( + checkIns = checkIns, + warningPackages = unprocessedPackages, + ) + Timber.tag(TAG).i("Check-in matcher result: %s", matcherResult) + + val overlaps = matcherResult.processedPackages.flatMap { it.overlaps } + val overlapsDistinct = overlaps.distinct() + if (overlaps.size != overlapsDistinct.size) { + IllegalArgumentException("Matched overlaps are not distinct").also { + it.reportProblem(TAG, "CheckInWarningMatcher results are not distinct.") + } + } + + // Partial processing: if calculation was not successful, but some packages were processed, we still save them + presenceTracingRiskRepository.reportCalculation( + successful = matcherResult.successful, + overlaps = overlapsDistinct, + ) + + // markPackagesProcessed only after reportCalculation, if there is an exception, then we can process again. + traceWarningRepository.markPackagesProcessed( + matcherResult.processedPackages.map { it.warningPackage.packageId } + ) + + return Result(calculatedAt = nowUTC) + } + + private fun checkCancel() { + if (isCanceled) throw TaskCancellationException() + } + + override suspend fun cancel() { + Timber.w("cancel() called.") + isCanceled = true + } + + data class Result( + val calculatedAt: Instant + ) : Task.Result + + data class Config( + override val executionTimeout: Duration = Duration.standardMinutes(9), + override val collisionBehavior: TaskFactory.Config.CollisionBehavior = + TaskFactory.Config.CollisionBehavior.SKIP_IF_SIBLING_RUNNING + ) : TaskFactory.Config + + class Factory @Inject constructor( + private val taskByDagger: Provider<PresenceTracingWarningTask>, + private val appConfigProvider: AppConfigProvider + ) : TaskFactory<DefaultProgress, Task.Result> { + + override suspend fun createConfig(): TaskFactory.Config = Config( + executionTimeout = appConfigProvider.getAppConfig().overallDownloadTimeout + ) + + override val taskProvider: () -> Task<DefaultProgress, Task.Result> = { + taskByDagger.get() + } + } + + companion object { + private const val TAG = "TracingWarningTask" + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/execution/PresenceTracingWarningWorkBuilder.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/execution/PresenceTracingWarningWorkBuilder.kt new file mode 100644 index 0000000000000000000000000000000000000000..1479650a56f13e0cdba642c58a176baadb74e009 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/execution/PresenceTracingWarningWorkBuilder.kt @@ -0,0 +1,37 @@ +package de.rki.coronawarnapp.presencetracing.risk.execution + +import androidx.work.BackoffPolicy +import androidx.work.Constraints +import androidx.work.NetworkType +import androidx.work.PeriodicWorkRequest +import androidx.work.PeriodicWorkRequestBuilder +import dagger.Reusable +import de.rki.coronawarnapp.worker.BackgroundConstants +import java.util.concurrent.TimeUnit +import javax.inject.Inject + +@Reusable +class PresenceTracingWarningWorkBuilder @Inject constructor() { + + fun createPeriodicWorkRequest(): PeriodicWorkRequest = + PeriodicWorkRequestBuilder<PresenceTracingWarningWorker>( + 60, + TimeUnit.MINUTES + ) + .setInitialDelay( + BackgroundConstants.KIND_DELAY, + TimeUnit.MINUTES + ) + .setBackoffCriteria( + BackoffPolicy.EXPONENTIAL, + BackgroundConstants.BACKOFF_INITIAL_DELAY, + TimeUnit.MINUTES + ) + .setConstraints(buildConstraints()) + .build() + + private fun buildConstraints() = + Constraints.Builder() + .setRequiredNetworkType(NetworkType.CONNECTED) + .build() +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/execution/PresenceTracingWarningWorker.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/execution/PresenceTracingWarningWorker.kt new file mode 100644 index 0000000000000000000000000000000000000000..300cab25a65daca21a1fbcc90ec7e4e6177e0faa --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/execution/PresenceTracingWarningWorker.kt @@ -0,0 +1,52 @@ +package de.rki.coronawarnapp.presencetracing.risk.execution + +import android.content.Context +import androidx.work.CoroutineWorker +import androidx.work.WorkerParameters +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import de.rki.coronawarnapp.bugreporting.reportProblem +import de.rki.coronawarnapp.task.TaskController +import de.rki.coronawarnapp.task.common.DefaultTaskRequest +import de.rki.coronawarnapp.task.submitBlocking +import de.rki.coronawarnapp.util.worker.InjectedWorkerFactory +import timber.log.Timber + +class PresenceTracingWarningWorker @AssistedInject constructor( + @Assisted val context: Context, + @Assisted workerParams: WorkerParameters, + private val taskController: TaskController +) : CoroutineWorker(context, workerParams) { + + override suspend fun doWork(): Result = try { + Timber.tag(TAG).v("$id: doWork() started. Run attempt: $runAttemptCount") + + val taskState = taskController.submitBlocking( + DefaultTaskRequest(PresenceTracingWarningTask::class, originTag = TAG) + ) + + when { + taskState.isSuccessful -> { + Timber.tag(TAG).d("$id: PresenceTracingWarningTask finished successfully.") + Result.success() + } + else -> { + taskState.error?.let { + Timber.tag(TAG).w(it, "$id: Error during PresenceTracingWarningTask.") + } + Result.retry() + } + } + } catch (e: Exception) { + e.reportProblem(TAG, "PresenceTracingWarningTask failed exceptionally, will retry.") + Result.retry() + } + + @AssistedFactory + interface Factory : InjectedWorkerFactory<PresenceTracingWarningWorker> + + companion object { + private val TAG = PresenceTracingWarningWorker::class.java.simpleName + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/PresenceTracingRiskDatabase.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/storage/PresenceTracingRiskDatabase.kt similarity index 85% rename from Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/PresenceTracingRiskDatabase.kt rename to Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/storage/PresenceTracingRiskDatabase.kt index 2d39bd5eaa6ab3ca9b186aacca859ae0b747a7e8..b80b28fbfb8fb9e687d1f9d608e6a64075e90652 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/PresenceTracingRiskDatabase.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/storage/PresenceTracingRiskDatabase.kt @@ -1,4 +1,4 @@ -package de.rki.coronawarnapp.presencetracing.risk +package de.rki.coronawarnapp.presencetracing.risk.storage import android.content.Context import androidx.room.Database @@ -27,6 +27,8 @@ abstract class PresenceTracingRiskDatabase : RoomDatabase() { .databaseBuilder(context, PresenceTracingRiskDatabase::class.java, DATABASE_NAME) .build() } -} -private const val DATABASE_NAME = "PresenceTracingRisk_db" + companion object { + private const val DATABASE_NAME = "PresenceTracingRisk_db" + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/storage/PresenceTracingRiskRepository.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/storage/PresenceTracingRiskRepository.kt new file mode 100644 index 0000000000000000000000000000000000000000..daf5c60bcd39437e7d54a410654397d5855bbbc9 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/storage/PresenceTracingRiskRepository.kt @@ -0,0 +1,283 @@ +package de.rki.coronawarnapp.presencetracing.risk.storage + +import androidx.room.ColumnInfo +import androidx.room.Dao +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.Insert +import androidx.room.OnConflictStrategy.REPLACE +import androidx.room.PrimaryKey +import androidx.room.Query +import androidx.room.TypeConverter +import de.rki.coronawarnapp.eventregistration.storage.entity.TraceLocationCheckInEntity +import de.rki.coronawarnapp.presencetracing.risk.PtRiskLevelResult +import de.rki.coronawarnapp.presencetracing.risk.TraceLocationCheckInRisk +import de.rki.coronawarnapp.presencetracing.risk.calculation.CheckInWarningOverlap +import de.rki.coronawarnapp.presencetracing.risk.calculation.PresenceTracingDayRisk +import de.rki.coronawarnapp.presencetracing.risk.calculation.PresenceTracingRiskCalculator +import de.rki.coronawarnapp.risk.RiskState +import de.rki.coronawarnapp.util.TimeAndDateExtensions.toLocalDateUtc +import de.rki.coronawarnapp.util.TimeStamper +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.map +import org.joda.time.Days +import org.joda.time.Instant +import timber.log.Timber +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class PresenceTracingRiskRepository @Inject constructor( + private val presenceTracingRiskCalculator: PresenceTracingRiskCalculator, + private val databaseFactory: PresenceTracingRiskDatabase.Factory, + private val timeStamper: TimeStamper, +) { + + private val database by lazy { + databaseFactory.create() + } + + private val traceTimeIntervalMatchDao by lazy { + database.traceTimeIntervalMatchDao() + } + + private val riskLevelResultDao by lazy { + database.presenceTracingRiskLevelResultDao() + } + + private val matchesOfLast14DaysPlusToday = traceTimeIntervalMatchDao.allMatches() + .map { timeIntervalMatchEntities -> + timeIntervalMatchEntities + .map { it.toCheckInWarningOverlap() } + .filter { it.localDateUtc.isAfter(fifteenDaysAgo.toLocalDateUtc()) } + } + + val checkInWarningOverlaps: Flow<List<CheckInWarningOverlap>> = + traceTimeIntervalMatchDao.allMatches().map { matchEntities -> + matchEntities.map { + it.toCheckInWarningOverlap() + } + } + + private val normalizedTimeOfLast14DaysPlusToday = matchesOfLast14DaysPlusToday.map { + presenceTracingRiskCalculator.calculateNormalizedTime(it) + } + + private val fifteenDaysAgo: Instant + get() = timeStamper.nowUTC.minus(Days.days(15).toStandardDuration()) + + val traceLocationCheckInRiskStates: Flow<List<TraceLocationCheckInRisk>> = + normalizedTimeOfLast14DaysPlusToday.map { + presenceTracingRiskCalculator.calculateCheckInRiskPerDay(it) + } + + val presenceTracingDayRisk: Flow<List<PresenceTracingDayRisk>> = + normalizedTimeOfLast14DaysPlusToday.map { + presenceTracingRiskCalculator.calculateAggregatedRiskPerDay(it) + } + + /** + * We delete warning packages after processing, we need to store the latest matches independent of success state + * For a future update we should look into partial processing. + */ + internal suspend fun reportCalculation( + successful: Boolean, + overlaps: List<CheckInWarningOverlap> = emptyList() + ) { + Timber.v("reportCalculation(successful=%b, overlaps=%s)", successful, overlaps) + + val nowUTC = timeStamper.nowUTC + + // delete stale matches from new packages, old matches are superseeded + overlaps.map { it.traceWarningPackageId }.forEach { + traceTimeIntervalMatchDao.deleteMatchesForPackage(it) + } + + if (overlaps.isNotEmpty()) { + traceTimeIntervalMatchDao.insert(overlaps.map { it.toTraceTimeIntervalMatchEntity() }) + } + + val result = if (successful) { + val last14daysPlusToday = normalizedTimeOfLast14DaysPlusToday.first() + val risk = presenceTracingRiskCalculator.calculateTotalRisk(last14daysPlusToday) + PtRiskLevelResult(nowUTC, risk) + } else { + PtRiskLevelResult(nowUTC, RiskState.CALCULATION_FAILED) + } + addResult(result) + } + + internal suspend fun deleteStaleData() { + Timber.d("deleteStaleData()") + traceTimeIntervalMatchDao.deleteOlderThan(fifteenDaysAgo.millis) + riskLevelResultDao.deleteOlderThan(fifteenDaysAgo.millis) + } + + suspend fun deleteAllMatches() { + Timber.d("deleteAllMatches()") + traceTimeIntervalMatchDao.deleteAll() + } + + fun latestEntries(limit: Int) = riskLevelResultDao.latestEntries(limit).map { list -> + var lastSuccessfulFound = false + list.sortedByDescending { + it.calculatedAtMillis + } + .map { entity -> + if (!lastSuccessfulFound && entity.riskState != RiskState.CALCULATION_FAILED) { + lastSuccessfulFound = true + // add risk per day to the last successful result + entity.toCheckInWarningOverlap(presenceTracingDayRisk.first()) + } else { + entity.toCheckInWarningOverlap(null) + } + } + } + + fun allEntries() = riskLevelResultDao.allEntries().map { list -> + var lastSuccessfulFound = false + list.sortedByDescending { + it.calculatedAtMillis + } + .map { entity -> + if (!lastSuccessfulFound && entity.riskState != RiskState.CALCULATION_FAILED) { + lastSuccessfulFound = true + // add risk per day to the last successful result + entity.toCheckInWarningOverlap(presenceTracingDayRisk.first()) + } else { + entity.toCheckInWarningOverlap(null) + } + } + } + + private fun addResult(result: PtRiskLevelResult) { + Timber.i("Saving risk calculation from ${result.calculatedAt} with result ${result.riskState}.") + riskLevelResultDao.insert(result.toTraceTimeIntervalMatchEntity()) + } + + suspend fun clearAllTables() { + traceTimeIntervalMatchDao.deleteAll() + riskLevelResultDao.deleteAll() + } +} + +/* +* Stores matches from the last successful execution +* */ +@Dao +interface TraceTimeIntervalMatchDao { + + @Query("SELECT * FROM TraceTimeIntervalMatchEntity") + fun allMatches(): Flow<List<TraceTimeIntervalMatchEntity>> + + @Query("DELETE FROM TraceTimeIntervalMatchEntity") + suspend fun deleteAll() + + @Query("DELETE FROM TraceTimeIntervalMatchEntity WHERE endTimeMillis < :endTimeMillis") + suspend fun deleteOlderThan(endTimeMillis: Long) + + @Query("DELETE FROM TraceTimeIntervalMatchEntity WHERE traceWarningPackageId = :warningPackageId") + suspend fun deleteMatchesForPackage(warningPackageId: String) + + @Insert + suspend fun insert(entities: List<TraceTimeIntervalMatchEntity>) +} + +@Entity +data class TraceTimeIntervalMatchEntity( + @PrimaryKey(autoGenerate = true) @ColumnInfo(name = "id") val id: Long? = null, + @ForeignKey( + entity = TraceLocationCheckInEntity::class, + parentColumns = ["id"], + childColumns = ["checkInId"], + onDelete = ForeignKey.CASCADE + ) + @ColumnInfo(name = "checkInId") val checkInId: Long, + @ColumnInfo(name = "traceWarningPackageId") val traceWarningPackageId: String, + @ColumnInfo(name = "transmissionRiskLevel") val transmissionRiskLevel: Int, + @ColumnInfo(name = "startTimeMillis") val startTimeMillis: Long, + @ColumnInfo(name = "endTimeMillis") val endTimeMillis: Long +) + +internal fun CheckInWarningOverlap.toTraceTimeIntervalMatchEntity() = TraceTimeIntervalMatchEntity( + checkInId = checkInId, + traceWarningPackageId = traceWarningPackageId, + transmissionRiskLevel = transmissionRiskLevel, + startTimeMillis = startTime.millis, + endTimeMillis = endTime.millis +) + +internal fun TraceTimeIntervalMatchEntity.toCheckInWarningOverlap() = CheckInWarningOverlap( + checkInId = checkInId, + traceWarningPackageId = traceWarningPackageId, + transmissionRiskLevel = transmissionRiskLevel, + startTime = Instant.ofEpochMilli(startTimeMillis), + endTime = Instant.ofEpochMilli(endTimeMillis) +) + +@Suppress("MaxLineLength") +@Dao +interface PresenceTracingRiskLevelResultDao { + + @Query("SELECT * FROM PresenceTracingRiskLevelResultEntity ORDER BY calculatedAtMillis DESC LIMIT :limit") + fun latestEntries(limit: Int): Flow<List<PresenceTracingRiskLevelResultEntity>> + + @Query("SELECT * FROM PresenceTracingRiskLevelResultEntity") + fun allEntries(): Flow<List<PresenceTracingRiskLevelResultEntity>> + + @Insert(onConflict = REPLACE) + fun insert(entity: PresenceTracingRiskLevelResultEntity) + + @Query("DELETE FROM PresenceTracingRiskLevelResultEntity WHERE calculatedAtMillis < :calculatedAtMillis") + suspend fun deleteOlderThan(calculatedAtMillis: Long) + + @Query("DELETE FROM PresenceTracingRiskLevelResultEntity") + suspend fun deleteAll() +} + +@Entity +data class PresenceTracingRiskLevelResultEntity( + @PrimaryKey @ColumnInfo(name = "calculatedAtMillis") val calculatedAtMillis: Long, + @ColumnInfo(name = "riskStateCode") val riskState: RiskState +) + +private fun PresenceTracingRiskLevelResultEntity.toCheckInWarningOverlap( + presenceTracingDayRisk: List<PresenceTracingDayRisk>? +) = PtRiskLevelResult( + calculatedAt = Instant.ofEpochMilli((calculatedAtMillis)), + riskState = riskState, + presenceTracingDayRisk = presenceTracingDayRisk +) + +private fun PtRiskLevelResult.toTraceTimeIntervalMatchEntity() = PresenceTracingRiskLevelResultEntity( + calculatedAtMillis = calculatedAt.millis, + riskState = riskState +) + +class RiskStateConverter { + @TypeConverter + fun toRiskStateCode(value: Int?): RiskState? = value?.toRiskState() + + @TypeConverter + fun fromRiskStateCode(code: RiskState?): Int? = code?.toCode() + + private fun RiskState.toCode() = when (this) { + RiskState.CALCULATION_FAILED -> CALCULATION_FAILED + RiskState.LOW_RISK -> LOW_RISK + RiskState.INCREASED_RISK -> INCREASED_RISK + } + + private fun Int.toRiskState() = when (this) { + CALCULATION_FAILED -> RiskState.CALCULATION_FAILED + LOW_RISK -> RiskState.LOW_RISK + INCREASED_RISK -> RiskState.INCREASED_RISK + else -> null + } + + companion object { + private const val CALCULATION_FAILED = 0 + private const val LOW_RISK = 1 + private const val INCREASED_RISK = 2 + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/warning/PresenceTracingWarningModule.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/warning/PresenceTracingWarningModule.kt new file mode 100644 index 0000000000000000000000000000000000000000..49ced6787c86ec1438eb887d7b021ebe7e1f6448 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/warning/PresenceTracingWarningModule.kt @@ -0,0 +1,43 @@ +package de.rki.coronawarnapp.presencetracing.warning + +import dagger.Module +import dagger.Provides +import dagger.multibindings.IntoMap +import de.rki.coronawarnapp.environment.download.DownloadCDNHttpClient +import de.rki.coronawarnapp.environment.download.DownloadCDNServerUrl +import de.rki.coronawarnapp.presencetracing.risk.execution.PresenceTracingWarningTask +import de.rki.coronawarnapp.presencetracing.warning.download.server.TraceWarningApiV1 +import de.rki.coronawarnapp.task.Task +import de.rki.coronawarnapp.task.TaskFactory +import de.rki.coronawarnapp.task.TaskTypeKey +import okhttp3.OkHttpClient +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory +import javax.inject.Singleton + +@Module +class PresenceTracingWarningModule { + + @Provides + @IntoMap + @TaskTypeKey(PresenceTracingWarningTask::class) + fun taskFactory( + factory: PresenceTracingWarningTask.Factory + ): TaskFactory<out Task.Progress, out Task.Result> = factory + + @Singleton + @Provides + fun api( + @DownloadCDNHttpClient client: OkHttpClient, + @DownloadCDNServerUrl url: String, + gsonConverterFactory: GsonConverterFactory, + ): TraceWarningApiV1 { + + return Retrofit.Builder() + .client(client) + .baseUrl(url) + .addConverterFactory(gsonConverterFactory) + .build() + .create(TraceWarningApiV1::class.java) + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/warning/download/TraceWarningPackageDownloader.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/warning/download/TraceWarningPackageDownloader.kt new file mode 100644 index 0000000000000000000000000000000000000000..581df13ed6c0afec399e579fc8a42a67da801d47 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/warning/download/TraceWarningPackageDownloader.kt @@ -0,0 +1,164 @@ +package de.rki.coronawarnapp.presencetracing.warning.download + +import dagger.Reusable +import de.rki.coronawarnapp.diagnosiskeys.server.LocationCode +import de.rki.coronawarnapp.presencetracing.warning.download.server.TraceWarningServer +import de.rki.coronawarnapp.presencetracing.warning.storage.TraceWarningPackageMetadata +import de.rki.coronawarnapp.presencetracing.warning.storage.TraceWarningRepository +import de.rki.coronawarnapp.util.HourInterval +import de.rki.coronawarnapp.util.ZipHelper.readIntoMap +import de.rki.coronawarnapp.util.ZipHelper.unzip +import de.rki.coronawarnapp.util.coroutine.DispatcherProvider +import de.rki.coronawarnapp.util.security.SignatureValidation +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.withContext +import kotlinx.coroutines.withTimeout +import org.joda.time.Duration +import timber.log.Timber +import javax.inject.Inject + +@Reusable +class TraceWarningPackageDownloader @Inject constructor( + private val repository: TraceWarningRepository, + private val dispatcherProvider: DispatcherProvider, + private val server: TraceWarningServer, + private val signatureValidation: SignatureValidation, +) { + + data class DownloadResult( + val successful: Boolean, + val newPackages: Collection<TraceWarningPackageMetadata> + ) { + override fun toString(): String { + return "DownloadResult(successful=$successful, newPackages.size=${newPackages.size})" + } + } + + suspend fun launchDownloads( + location: LocationCode, + hourIntervals: List<HourInterval>, + downloadTimeout: Duration + ): DownloadResult { + val launcher: CoroutineScope.(HourInterval) -> Deferred<TraceWarningPackageMetadata?> = { hourInterval -> + async { + val metadata = repository.createMetadata(location, hourInterval) + withTimeout(downloadTimeout.millis) { + downloadPackageForMetaData(metadata) + } + } + } + + Timber.tag(TAG).d("Launching %d downloads.", hourIntervals.size) + + val launchedDownloads: Collection<Deferred<TraceWarningPackageMetadata?>> = + hourIntervals.map { warningPackageId -> + withContext(context = dispatcherProvider.IO) { + launcher(warningPackageId) + } + } + + val successfulDownloads = launchedDownloads.awaitAll() + .filterNotNull() + .also { + Timber.tag(TAG).v("Downloaded keyfile: %s", it.joinToString("\n")) + } + Timber.tag(TAG).i("Download success: ${successfulDownloads.size}/${launchedDownloads.size}") + + return DownloadResult( + successful = launchedDownloads.size == successfulDownloads.size, + newPackages = successfulDownloads + ) + } + + private suspend fun downloadPackageForMetaData( + metaData: TraceWarningPackageMetadata + ): TraceWarningPackageMetadata? = try { + val downloadInfo = server.downloadPackage( + location = metaData.location, + hourInterval = metaData.hourInterval + ) + + if (!downloadInfo.isEmptyPkg) { + val fileMap = downloadInfo.readBody().unzip().readIntoMap() + val rawProtoBuf = getValidatedBinary(metaData, fileMap) + writeProtoBufToFile(metaData, rawProtoBuf) + } else { + Timber.tag(TAG).v("Empty package for %s", metaData) + } + + Timber.tag(TAG).v("Download finished: %s -> %s", metaData, downloadInfo) + + val eTag = requireNotNull(downloadInfo.etag) { "Server provided no ETAG!" } + + repository.markDownloadComplete(metaData, eTag, downloadInfo.isEmptyPkg) + } catch (e: Exception) { + Timber.tag(TAG).e(e, "Download failed: %s", metaData) + null + } + + private fun writeProtoBufToFile( + metaData: TraceWarningPackageMetadata, + rawProtoBuf: ByteArray, + ) { + if (rawProtoBuf.isEmpty()) { + Timber.tag(TAG).d("rawProtoBuf was empty for %s", metaData.packageId) + return + } + + val saveTo = repository.getPathForMetaData(metaData) + if (saveTo.exists()) { + Timber.tag(TAG).w("File existed, overwriting: %s", saveTo) + if (saveTo.delete()) { + Timber.tag(TAG).e("%s exists, but can't be deleted.", saveTo) + } + } + try { + saveTo.parentFile?.let { + if (!it.exists() && it.mkdir()) { + Timber.w("Had to create parent dir: %s", it) + } + } + saveTo.writeBytes(rawProtoBuf) + } catch (e: Exception) { + Timber.tag(TAG).e(e, "Failed to write %s to %s", metaData, saveTo) + saveTo.delete() + throw e + } + Timber.tag(TAG).v("%d bytes written to %s.", rawProtoBuf.size, saveTo) + } + + private fun getValidatedBinary( + metaData: TraceWarningPackageMetadata, + fileMap: Map<String, ByteArray> + ): ByteArray { + val signature = fileMap[EXPORT_SIGNATURE_NAME] ?: throw TraceWarningPackageValidationException( + message = "Signature was null for ${metaData.packageId}(${metaData.eTag})." + ) + + val binary = fileMap[EXPORT_BINARY_NAME] ?: throw TraceWarningPackageValidationException( + message = "Binary was null for ${metaData.packageId}(${metaData.eTag})." + ) + + val hasValidSignature = signatureValidation.hasValidSignature( + binary, + SignatureValidation.parseTEKStyleSignature(signature) + ) + + if (!hasValidSignature) { + throw TraceWarningPackageValidationException( + message = "Signature didn't match for ${metaData.packageId}(${metaData.eTag})." + ) + } + + return binary + } + + companion object { + private const val TAG = "TraceWarningDownloader" + private const val EXPORT_BINARY_NAME = "export.bin" + private const val EXPORT_SIGNATURE_NAME = "export.sig" + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/warning/download/TraceWarningPackageSyncTool.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/warning/download/TraceWarningPackageSyncTool.kt new file mode 100644 index 0000000000000000000000000000000000000000..ea2d87f227017e3399c5c4b12468448f47914ec4 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/warning/download/TraceWarningPackageSyncTool.kt @@ -0,0 +1,178 @@ +package de.rki.coronawarnapp.presencetracing.warning.download + +import androidx.annotation.VisibleForTesting +import dagger.Reusable +import de.rki.coronawarnapp.appconfig.AppConfigProvider +import de.rki.coronawarnapp.appconfig.KeyDownloadConfig +import de.rki.coronawarnapp.diagnosiskeys.server.LocationCode +import de.rki.coronawarnapp.eventregistration.checkins.CheckInRepository +import de.rki.coronawarnapp.presencetracing.warning.download.server.TraceWarningApiV1 +import de.rki.coronawarnapp.presencetracing.warning.download.server.TraceWarningServer +import de.rki.coronawarnapp.presencetracing.warning.storage.TraceWarningPackageMetadata +import de.rki.coronawarnapp.presencetracing.warning.storage.TraceWarningRepository +import de.rki.coronawarnapp.storage.DeviceStorage +import de.rki.coronawarnapp.util.HourInterval +import de.rki.coronawarnapp.util.TimeAndDateExtensions.deriveHourInterval +import de.rki.coronawarnapp.util.debug.measureTime +import kotlinx.coroutines.flow.first +import timber.log.Timber +import javax.inject.Inject +import kotlin.math.max + +@Reusable +class TraceWarningPackageSyncTool @Inject constructor( + private val deviceStorage: DeviceStorage, + private val server: TraceWarningServer, + private val repository: TraceWarningRepository, + private val configProvider: AppConfigProvider, + private val checkInRepository: CheckInRepository, + private val downloader: TraceWarningPackageDownloader +) { + + suspend fun syncPackages(): SyncResult { + repository.cleanMetadata() + return measureTime( + { Timber.tag(TAG).d("syncPackagesForLocation(DE), took %dms", it) }, + { syncPackagesForLocation(LocationCode("DE")) } + ) + } + + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + internal suspend fun syncPackagesForLocation(location: LocationCode): SyncResult { + Timber.tag(TAG).d("syncTraceWarningPackages(location=%s)", location) + + val oldestCheckIn = checkInRepository.allCheckIns.first().minByOrNull { it.checkInStart }.also { + Timber.tag(TAG).d("Our oldest check-in is %s", it) + } + + if (oldestCheckIn == null) { + Timber.tag(TAG).w("There were no checkins, cleaning up package metadata, aborting early.") + val metaDataForLocation = repository.getMetaDataForLocation(location) + repository.delete(metaDataForLocation) + return SyncResult(successful = true) + } + + val downloadConfig: KeyDownloadConfig = configProvider.getAppConfig() + + cleanUpRevokedPackages(downloadConfig) + + val intervalDiscovery: TraceWarningApiV1.DiscoveryResult = try { + server.getAvailableIds(location) + } catch (e: Exception) { + Timber.tag(TAG).w(e, "Failed to discover available IDs.") + return SyncResult(successful = false) + } + + val firstRelevantInterval: HourInterval = max( + oldestCheckIn.checkInStart.deriveHourInterval(), + intervalDiscovery.oldest + ) + + cleanUpIrrelevantPackages(location, firstRelevantInterval) + + if (firstRelevantInterval > intervalDiscovery.latest) { + Timber.tag(TAG).d("Known server IDs are older then ours newest, aborting early.") + return SyncResult(successful = true) + } + + val missingHourIntervals = determineIntervalsToDownload( + location = location, + firstRelevant = oldestCheckIn.checkInStart.deriveHourInterval(), + lastRelevant = intervalDiscovery.latest + ) + + if (missingHourIntervals.isEmpty()) { + Timber.tag(TAG).d("There are no missing intervals for %s", location) + return SyncResult(successful = true) + } + + requireStorageSpaceFor(missingHourIntervals.size) + + val downloadResult = downloader.launchDownloads( + location = location, + hourIntervals = missingHourIntervals, + downloadTimeout = downloadConfig.individualDownloadTimeout + ) + Timber.tag(TAG).i("Download result: %s", downloadResult) + + return SyncResult( + successful = downloadResult.successful, + newPackages = downloadResult.newPackages, + ) + } + + /** + * Returns true if any of our cached keys were revoked + */ + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + internal suspend fun cleanUpRevokedPackages( + config: KeyDownloadConfig + ): List<TraceWarningPackageMetadata> { + val revokedKeyPackages = config.revokedTraceWarningPackages + + if (revokedKeyPackages.isEmpty()) { + Timber.tag(TAG).d("No revoked key packages to delete.") + return emptyList() + } + + val badEtags = revokedKeyPackages.map { it.etag } + val toDelete = repository.allMetaData.first().filter { badEtags.contains(it.eTag) } + Timber.tag(TAG).d("Revoked key packages matched %s", toDelete) + + repository.delete(toDelete) + + return toDelete.also { + Timber.tag(TAG).d("Cleaned up TraceWarning ids: %s", it) + } + } + + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + internal suspend fun cleanUpIrrelevantPackages( + location: LocationCode, + oldestRelevantInterval: HourInterval + ): List<TraceWarningPackageMetadata> { + val downloaded = repository.getMetaDataForLocation(location) + val toDelete = downloaded.filter { it.hourInterval < oldestRelevantInterval } + Timber.tag(TAG).d("Removing irrelevant ids older than %d: %s", oldestRelevantInterval, toDelete) + + repository.delete(toDelete) + + return toDelete.also { + Timber.tag(TAG).d("Removed irrelevant packages: %s", it) + } + } + + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + internal suspend fun determineIntervalsToDownload( + location: LocationCode, + firstRelevant: HourInterval, + lastRelevant: HourInterval + ): List<HourInterval> { + val metadatas = repository.getMetaDataForLocation(location) + + return (firstRelevant..lastRelevant).filter { interval -> + // If there is no metadata, it's unknown, so we want to download it + metadatas.none { it.hourInterval == interval } + } + } + + private suspend fun requireStorageSpaceFor(size: Int): DeviceStorage.CheckResult { + val requiredBytes: Long = APPROX_FILE_SIZE * size + Timber.tag(TAG).d("%dB are required for %d files", requiredBytes, size) + return deviceStorage.requireSpacePrivateStorage(requiredBytes).also { + Timber.tag(TAG).d("Storage check result: %s", it) + } + } + + data class SyncResult( + val successful: Boolean, + val newPackages: Collection<TraceWarningPackageMetadata> = emptyList() + ) + + companion object { + private const val TAG = "TraceWarningSyncTool" + + // TODO check size + private const val APPROX_FILE_SIZE = 22 * 1024L // ~22KB + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/warning/download/TraceWarningPackageValidationException.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/warning/download/TraceWarningPackageValidationException.kt new file mode 100644 index 0000000000000000000000000000000000000000..b05d7ac349fe3d0885f709c4e4bcc6b2fffee6e5 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/warning/download/TraceWarningPackageValidationException.kt @@ -0,0 +1,9 @@ +package de.rki.coronawarnapp.presencetracing.warning.download + +import de.rki.coronawarnapp.exception.reporting.ErrorCodes +import de.rki.coronawarnapp.util.security.InvalidSignatureException + +class TraceWarningPackageValidationException(message: String) : InvalidSignatureException( + code = ErrorCodes.APPLICATION_CONFIGURATION_INVALID.code, + message = message +) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/warning/download/server/TraceWarningApiV1.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/warning/download/server/TraceWarningApiV1.kt new file mode 100644 index 0000000000000000000000000000000000000000..0c99daef7570bf8beb5dc8b3015b7280804449ae --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/warning/download/server/TraceWarningApiV1.kt @@ -0,0 +1,31 @@ +package de.rki.coronawarnapp.presencetracing.warning.download.server + +import androidx.annotation.Keep +import com.google.gson.annotations.SerializedName +import de.rki.coronawarnapp.util.HourInterval +import okhttp3.ResponseBody +import retrofit2.Response +import retrofit2.http.GET +import retrofit2.http.Path +import retrofit2.http.Streaming + +interface TraceWarningApiV1 { + + @Keep + data class DiscoveryResult( + @SerializedName("oldest") val oldest: HourInterval, + @SerializedName("latest") val latest: HourInterval + ) + + @GET("/version/v1/twp/country/{region}/hour") + suspend fun getWarningPackageIds( + @Path("region") region: String + ): DiscoveryResult + + @Streaming + @GET("/version/v1/twp/country/{region}/hour/{timeId}") + suspend fun downloadKeyFileForHour( + @Path("region") region: String, + @Path("timeId") timeId: Long + ): Response<ResponseBody> +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/warning/download/server/TraceWarningPackageDownload.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/warning/download/server/TraceWarningPackageDownload.kt new file mode 100644 index 0000000000000000000000000000000000000000..02e5d44480f76ebbcd58e5a1621bbd2783b1ce25 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/warning/download/server/TraceWarningPackageDownload.kt @@ -0,0 +1,16 @@ +package de.rki.coronawarnapp.presencetracing.warning.download.server + +import okhttp3.ResponseBody +import retrofit2.Response +import java.io.InputStream + +data class TraceWarningPackageDownload(val response: Response<ResponseBody>) { + + private val headers = response.headers() + + val etag by lazy { headers.values("ETag").singleOrNull() } + + val isEmptyPkg by lazy { headers.values("cwa-empty-pkg").singleOrNull() == "1" } + + fun readBody(): InputStream = requireNotNull(response.body()) { "Response body was null" }.byteStream() +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/warning/download/server/TraceWarningServer.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/warning/download/server/TraceWarningServer.kt new file mode 100644 index 0000000000000000000000000000000000000000..3cc3c8bb40e34e4ac7a2856a093d0ab2a3e71e94 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/warning/download/server/TraceWarningServer.kt @@ -0,0 +1,54 @@ +package de.rki.coronawarnapp.presencetracing.warning.download.server + +import dagger.Lazy +import de.rki.coronawarnapp.diagnosiskeys.server.LocationCode +import de.rki.coronawarnapp.util.HourInterval +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import retrofit2.HttpException +import timber.log.Timber +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class TraceWarningServer @Inject constructor( + private val traceWarningApi: Lazy<TraceWarningApiV1> +) { + + private val warningApi: TraceWarningApiV1 + get() = traceWarningApi.get() + + suspend fun getAvailableIds( + location: LocationCode + ): TraceWarningApiV1.DiscoveryResult = withContext(Dispatchers.IO) { + warningApi.getWarningPackageIds(location.identifier).also { + Timber.d("getAvailableIds(location=%s): %s", location, it) + } + } + + suspend fun downloadPackage( + location: LocationCode, + hourInterval: HourInterval + ): TraceWarningPackageDownload = withContext(Dispatchers.IO) { + Timber.tag(TAG).v("downloadPackage(location=%s, hourInterval=%s)", location, hourInterval) + + val response = warningApi.downloadKeyFileForHour( + location.identifier, + hourInterval + ) + + val downloadInfo = TraceWarningPackageDownload(response) + + if (response.isSuccessful) { + Timber.tag(TAG).v("TraceTimeWarning download available: %s", downloadInfo) + + return@withContext downloadInfo + } else { + throw HttpException(response) + } + } + + companion object { + private val TAG = TraceWarningServer::class.java.simpleName + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/warning/storage/TraceWarningPackage.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/warning/storage/TraceWarningPackage.kt new file mode 100644 index 0000000000000000000000000000000000000000..91e655f43ba78c1cafbf4ec571e56a09fe7b33b6 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/warning/storage/TraceWarningPackage.kt @@ -0,0 +1,14 @@ +package de.rki.coronawarnapp.presencetracing.warning.storage + +import de.rki.coronawarnapp.presencetracing.warning.WarningPackageId +import de.rki.coronawarnapp.server.protocols.internal.pt.TraceWarning + +interface TraceWarningPackage { + + val packageId: WarningPackageId + + /** + * May throw an exception if there is an issue with the protobuf + */ + suspend fun extractWarnings(): List<TraceWarning.TraceTimeIntervalWarning> +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/warning/storage/TraceWarningPackageContainer.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/warning/storage/TraceWarningPackageContainer.kt new file mode 100644 index 0000000000000000000000000000000000000000..e16436f59241d064d0225cd15b8a2717932a8f77 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/warning/storage/TraceWarningPackageContainer.kt @@ -0,0 +1,23 @@ +package de.rki.coronawarnapp.presencetracing.warning.storage + +import de.rki.coronawarnapp.presencetracing.warning.WarningPackageId +import de.rki.coronawarnapp.server.protocols.internal.pt.TraceWarning +import java.io.File + +data class TraceWarningPackageContainer( + override val packageId: WarningPackageId, + private val packagePath: File, +) : TraceWarningPackage { + + private val warningPackage by lazy<TraceWarning.TraceWarningPackage> { + if (packagePath.exists()) { + TraceWarning.TraceWarningPackage.parseFrom(packagePath.readBytes()) + } else { + TraceWarning.TraceWarningPackage.getDefaultInstance() + } + } + + override suspend fun extractWarnings(): List<TraceWarning.TraceTimeIntervalWarning> { + return warningPackage.timeIntervalWarningsList + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/warning/storage/TraceWarningPackageDatabase.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/warning/storage/TraceWarningPackageDatabase.kt new file mode 100644 index 0000000000000000000000000000000000000000..5939360ce43edaf791ae2eaeedb59cd78982bce5 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/warning/storage/TraceWarningPackageDatabase.kt @@ -0,0 +1,62 @@ +package de.rki.coronawarnapp.presencetracing.warning.storage + +import android.content.Context +import androidx.room.Dao +import androidx.room.Database +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import androidx.room.Room +import androidx.room.RoomDatabase +import androidx.room.TypeConverters +import androidx.room.Update +import de.rki.coronawarnapp.presencetracing.warning.WarningPackageId +import de.rki.coronawarnapp.util.database.CommonConverters +import de.rki.coronawarnapp.util.di.AppContext +import kotlinx.coroutines.flow.Flow +import javax.inject.Inject + +@Dao +interface TraceWarningPackageDao { + + @Query("SELECT * FROM TraceWarningPackageMetadata") + fun getAllMetaData(): Flow<List<TraceWarningPackageMetadata>> + + @Query("SELECT * FROM TraceWarningPackageMetadata WHERE location = :location") + suspend fun getAllMetaDataForLocation(location: String): List<TraceWarningPackageMetadata> + + @Query("SELECT * FROM TraceWarningPackageMetadata WHERE id = :packageId") + suspend fun get(packageId: WarningPackageId): TraceWarningPackageMetadata? + + @Insert(onConflict = OnConflictStrategy.IGNORE) + suspend fun insert(entity: TraceWarningPackageMetadata) + + @Update(entity = TraceWarningPackageMetadata::class) + suspend fun updateMetaData(update: TraceWarningPackageMetadata.UpdateDownload) + + @Update(entity = TraceWarningPackageMetadata::class) + suspend fun updateMetaData(update: TraceWarningPackageMetadata.UpdateProcessed) + + @Query("DELETE FROM TraceWarningPackageMetadata WHERE id in (:packageIds)") + suspend fun deleteByIds(packageIds: List<WarningPackageId>) + + @Query("DELETE FROM TraceWarningPackageMetadata") + suspend fun clear() +} + +@Database( + entities = [TraceWarningPackageMetadata::class], + version = 1, + exportSchema = true +) +@TypeConverters(CommonConverters::class) +abstract class TraceWarningDatabase : RoomDatabase() { + + abstract fun traceWarningPackageDao(): TraceWarningPackageDao + + class Factory @Inject constructor(@AppContext private val context: Context) { + fun create() = Room + .databaseBuilder(context, TraceWarningDatabase::class.java, "TraceWarning_db") + .build() + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/warning/storage/TraceWarningPackageMetadata.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/warning/storage/TraceWarningPackageMetadata.kt new file mode 100644 index 0000000000000000000000000000000000000000..54725874a1c3c8931c8f9f9196786d861076456b --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/warning/storage/TraceWarningPackageMetadata.kt @@ -0,0 +1,58 @@ +package de.rki.coronawarnapp.presencetracing.warning.storage + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey +import de.rki.coronawarnapp.diagnosiskeys.server.LocationCode +import de.rki.coronawarnapp.presencetracing.warning.WarningPackageId +import de.rki.coronawarnapp.util.HourInterval +import org.joda.time.Instant + +@Entity(tableName = "TraceWarningPackageMetadata") +data class TraceWarningPackageMetadata( + @PrimaryKey @ColumnInfo(name = "id") val packageId: WarningPackageId, + @ColumnInfo(name = "createdAt") val createdAt: Instant, + @ColumnInfo(name = "location") val location: LocationCode, // i.e. "DE" + @ColumnInfo(name = "hourInterval") val hourInterval: HourInterval, + @ColumnInfo(name = "eTag") val eTag: String? = null, + @ColumnInfo(name = "downloaded") val isDownloaded: Boolean = false, + @ColumnInfo(name = "emptyPkg") val isEmptyPkg: Boolean = false, + @ColumnInfo(name = "processed") val isProcessed: Boolean = false +) { + + constructor( + location: LocationCode, + hourInterval: HourInterval, + createdAt: Instant + ) : this( + packageId = calcluateId(location, hourInterval), + location = location, + hourInterval = hourInterval, + createdAt = createdAt, + ) + + @Transient + val fileName: String = "$packageId.bin" + + companion object { + fun calcluateId( + location: LocationCode, + hourInterval: HourInterval + ): WarningPackageId = "${location.identifier}_$hourInterval" + } + + @Entity + data class UpdateDownload( + @PrimaryKey @ColumnInfo(name = "id") val packageId: WarningPackageId, + @ColumnInfo(name = "eTag") val eTag: String?, + @ColumnInfo(name = "downloaded") val isDownloaded: Boolean, + @ColumnInfo(name = "emptyPkg") val isEmptyPkg: Boolean, + @ColumnInfo(name = "processed") val isProcessed: Boolean, + ) + + @Entity + data class UpdateProcessed( + @PrimaryKey @ColumnInfo(name = "id") val packageId: WarningPackageId, + @ColumnInfo(name = "processed") val isProcessed: Boolean, + ) +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/warning/storage/TraceWarningRepository.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/warning/storage/TraceWarningRepository.kt new file mode 100644 index 0000000000000000000000000000000000000000..4af922212d36bfd5c4ae94950d2156abec57b3ca --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/warning/storage/TraceWarningRepository.kt @@ -0,0 +1,182 @@ +package de.rki.coronawarnapp.presencetracing.warning.storage + +import android.content.Context +import de.rki.coronawarnapp.diagnosiskeys.server.LocationCode +import de.rki.coronawarnapp.presencetracing.warning.WarningPackageId +import de.rki.coronawarnapp.util.HourInterval +import de.rki.coronawarnapp.util.TimeStamper +import de.rki.coronawarnapp.util.di.AppContext +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.map +import timber.log.Timber +import java.io.File +import java.io.IOException +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class TraceWarningRepository @Inject constructor( + @AppContext private val context: Context, + private val factory: TraceWarningDatabase.Factory, + private val timeStamper: TimeStamper +) { + private val database by lazy { factory.create() } + private val dao: TraceWarningPackageDao by lazy { database.traceWarningPackageDao() } + + private val storageDir by lazy { + File(context.cacheDir, "trace_warning_packages").apply { + if (!exists()) { + if (mkdirs()) { + Timber.tag(TAG).d("Trace warning package directory created: %s", this) + } else { + throw IOException("Trace warning package directory creation failed: $this") + } + } + } + } + + val unprocessedWarningPackages: Flow<List<TraceWarningPackage>> = dao.getAllMetaData() + .map { metadatas -> + Timber.tag(TAG).v("Known packages: ${metadatas.size}") + metadatas.filter { !it.isProcessed } + } + .map { unprocessed -> + Timber.tag(TAG).v("Unprocessed packages: ${unprocessed.size}") + unprocessed.filter { !it.isEmptyPkg } + } + .map { metaDatas -> + Timber.tag(TAG).v("There are ${metaDatas.size} unprocessed non-empty warning packages.") + metaDatas.map { metaData -> + TraceWarningPackageContainer( + packageId = metaData.packageId, + packagePath = getPathForMetaData(metaData) + ) + } + } + + fun getPathForMetaData(metaData: TraceWarningPackageMetadata): File { + return File(storageDir, metaData.fileName) + } + + val allMetaData = dao.getAllMetaData() + + suspend fun createMetadata(location: LocationCode, hourInterval: HourInterval): TraceWarningPackageMetadata { + val metadata = TraceWarningPackageMetadata( + location = location, + hourInterval = hourInterval, + createdAt = timeStamper.nowUTC + ) + dao.insert(metadata) + Timber.tag(TAG).d("Inserted new Metadata: %s", metadata) + return metadata + } + + suspend fun getMetaDataForLocation(location: LocationCode): List<TraceWarningPackageMetadata> { + return dao.getAllMetaDataForLocation(location.identifier) + } + + suspend fun markDownloadComplete( + metadata: TraceWarningPackageMetadata, + eTag: String, + isEmptyPkg: Boolean + ): TraceWarningPackageMetadata { + Timber.tag(TAG).d("markDownloadComplete(metaData=%s, eTag=%s)", metadata, eTag) + val update = TraceWarningPackageMetadata.UpdateDownload( + packageId = metadata.packageId, + eTag = eTag, + isDownloaded = true, + isProcessed = false, + isEmptyPkg = isEmptyPkg, + ) + Timber.tag(TAG).d("Metadata marked as complete: %s", update) + dao.updateMetaData(update) + return metadata.copy( + eTag = eTag, + isDownloaded = true, + isProcessed = false, + isEmptyPkg = isEmptyPkg, + ) + } + + suspend fun markPackagesProcessed(packageIds: List<WarningPackageId>) { + Timber.tag(TAG).v("markPackagesProcessed(packageIds=%s)", packageIds) + + packageIds.forEach { packageId -> + Timber.tag(TAG).d("markPackageProcessed(packageId=%s)", packageId) + val update = TraceWarningPackageMetadata.UpdateProcessed( + packageId = packageId, + isProcessed = true, + ) + dao.updateMetaData(update) + + dao.get(packageId)?.also { + val file = getPathForMetaData(it) + if (file.delete()) { + Timber.tag(TAG).v("Deleted processed file: %s", file) + } + } + } + } + + suspend fun delete(metadata: List<TraceWarningPackageMetadata>) { + Timber.tag(TAG).d("delete(metaData=%s)", metadata.map { it.packageId }) + dao.deleteByIds(metadata.map { it.packageId }) + metadata.map { getPathForMetaData(it) }.forEach { + if (it.exists()) { + if (it.delete()) { + Timber.tag(TAG).d("Delete TraceWarningPackage file.") + } else { + Timber.tag(TAG).w("Failed to delete TraceWarningPackage file: %s", it) + } + } + } + } + + suspend fun clear() { + Timber.tag(TAG).d("clear()") + dao.clear() + + if (!storageDir.deleteRecursively()) { + Timber.tag(TAG).e("Failed to delete all TraceWarningPackage files.") + } + } + + suspend fun cleanMetadata() { + Timber.tag(TAG).d("cleanMetadata()") + val allMetadata = allMetaData.first() + + // Lost files, system deleted cache? + run { + val shouldHaveFile = allMetadata.filter { it.isDownloaded && !it.isProcessed && !it.isEmptyPkg } + val toDelete = shouldHaveFile.filter { !getPathForMetaData(it).exists() } + if (toDelete.isNotEmpty()) { + Timber.tag(TAG).w("%d Metadata items lost their file", toDelete.size) + } + delete(toDelete) + } + + // Shouldn't have a file, but has one? Gremlins? + run { + val shouldNotHaveFile = allMetadata.filter { it.isDownloaded && (it.isProcessed || it.isEmptyPkg) } + val toDelete = shouldNotHaveFile.filter { getPathForMetaData(it).exists() } + if (toDelete.isNotEmpty()) { + Timber.tag(TAG).w("%d Metadata items have unexpected files", toDelete.size) + } + delete(toDelete) + } + + // File without owner? + storageDir.listFiles()?.forEach { file -> + val orphan = allMetadata.none { getPathForMetaData(it) == file } + + if (orphan && file.delete()) { + Timber.tag(TAG).w("Deleted orphaned file: %s", file) + } + } + } + + companion object { + private val TAG = TraceWarningRepository::class.java.simpleName + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/warning/storage/WarningPackageId.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/warning/storage/WarningPackageId.kt new file mode 100644 index 0000000000000000000000000000000000000000..af612ce8bad3c3eb934fa92b3bc9b264108ce1ac --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/warning/storage/WarningPackageId.kt @@ -0,0 +1,3 @@ +package de.rki.coronawarnapp.presencetracing.warning + +typealias WarningPackageId = String diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/CombinedEwPtRisk.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/CombinedEwPtRisk.kt new file mode 100644 index 0000000000000000000000000000000000000000..b4e6305fb069fdd90d9136baa9a2549a2250023a --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/CombinedEwPtRisk.kt @@ -0,0 +1,64 @@ +package de.rki.coronawarnapp.risk + +import de.rki.coronawarnapp.presencetracing.risk.PtRiskLevelResult +import de.rki.coronawarnapp.risk.storage.combine +import de.rki.coronawarnapp.risk.storage.max +import de.rki.coronawarnapp.util.TimeAndDateExtensions.toLocalDateUtc +import org.joda.time.Instant +import org.joda.time.LocalDate + +data class CombinedEwPtDayRisk( + val localDate: LocalDate, + val riskState: RiskState +) + +data class CombinedEwPtRiskLevelResult( + val ptRiskLevelResult: PtRiskLevelResult, + val ewRiskLevelResult: EwRiskLevelResult +) { + + val riskState: RiskState by lazy { + combine(ptRiskLevelResult.riskState, ewRiskLevelResult.riskState) + } + + val wasSuccessfullyCalculated: Boolean by lazy { + riskState != RiskState.CALCULATION_FAILED + } + + val calculatedAt: Instant by lazy { + max(ewRiskLevelResult.calculatedAt, ptRiskLevelResult.calculatedAt) + } + + val daysWithEncounters: Int by lazy { + when (riskState) { + RiskState.INCREASED_RISK -> { + (ewRiskLevelResult.ewAggregatedRiskResult?.numberOfDaysWithHighRisk ?: 0) + + ptRiskLevelResult.numberOfDaysWithHighRisk + } + RiskState.LOW_RISK -> { + (ewRiskLevelResult.ewAggregatedRiskResult?.numberOfDaysWithLowRisk ?: 0) + + ptRiskLevelResult.numberOfDaysWithLowRisk + } + else -> 0 + } + } + + val lastRiskEncounterAt: LocalDate? by lazy { + when (riskState) { + RiskState.INCREASED_RISK -> max( + ewRiskLevelResult.ewAggregatedRiskResult?.mostRecentDateWithHighRisk?.toLocalDateUtc(), + ptRiskLevelResult.mostRecentDateWithHighRisk + ) + RiskState.LOW_RISK -> max( + ewRiskLevelResult.ewAggregatedRiskResult?.mostRecentDateWithLowRisk?.toLocalDateUtc(), + ptRiskLevelResult.mostRecentDateWithLowRisk + ) + else -> null + } + } +} + +data class LastCombinedRiskResults( + val lastCalculated: CombinedEwPtRiskLevelResult, + val lastSuccessfullyCalculated: CombinedEwPtRiskLevelResult +) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskLevelChangeDetector.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskLevelChangeDetector.kt index d09e184b9cd11199166917a2337a23bf200aaf48..123f45edceef714e0fefff56dff06bb8dc7f6f73 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskLevelChangeDetector.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskLevelChangeDetector.kt @@ -8,7 +8,6 @@ import de.rki.coronawarnapp.datadonation.analytics.storage.TestResultDonorSettin import de.rki.coronawarnapp.datadonation.survey.Surveys import de.rki.coronawarnapp.notification.GeneralNotifications import de.rki.coronawarnapp.notification.NotificationConstants.NEW_MESSAGE_RISK_LEVEL_SCORE_NOTIFICATION_ID -import de.rki.coronawarnapp.risk.storage.CombinedEwPtRiskLevelResult import de.rki.coronawarnapp.risk.storage.RiskLevelStorage import de.rki.coronawarnapp.storage.TracingSettings import de.rki.coronawarnapp.submission.SubmissionSettings @@ -49,7 +48,7 @@ class RiskLevelChangeDetector @Inject constructor( } .filter { it.size == 2 } .onEach { - Timber.v("Checking for risklevel change.") + Timber.v("Checking for ew risklevel change.") checkEwRiskForStateChanges(it) } .catch { Timber.e(it, "App config change checks failed.") } @@ -61,7 +60,7 @@ class RiskLevelChangeDetector @Inject constructor( } .filter { it.size == 2 } .onEach { - Timber.v("Checking for risklevel change.") + Timber.v("Checking for combined risklevel change.") checkCombinedRiskForStateChanges(it) } .catch { Timber.e(it, "App config change checks failed.") } @@ -69,7 +68,6 @@ class RiskLevelChangeDetector @Inject constructor( } private suspend fun checkCombinedRiskForStateChanges(results: List<CombinedEwPtRiskLevelResult>) { - // TODO refactor val oldResult = results.first() val newResult = results.last() @@ -164,7 +162,6 @@ class RiskLevelChangeDetector @Inject constructor( } else { Timber.d("App is in foreground, not sending notifications") } - Timber.d("Risk level changed and notification sent. Current Risk level is $newRiskState") } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskLevelResultExtensions.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskLevelResultExtensions.kt index c47ff8c40e84d723cf7d1585d83c48174aa81c66..1d0ea0988e7b013f220062d5ffd009ccf30f65c7 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskLevelResultExtensions.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskLevelResultExtensions.kt @@ -1,9 +1,7 @@ package de.rki.coronawarnapp.risk import com.google.android.gms.nearby.exposurenotification.ExposureWindow -import de.rki.coronawarnapp.presencetracing.risk.PtRiskLevelResult import de.rki.coronawarnapp.risk.result.EwAggregatedRiskResult -import de.rki.coronawarnapp.risk.storage.CombinedEwPtRiskLevelResult import org.joda.time.Instant fun List<EwRiskLevelResult>.tryLatestEwResultsWithDefaults(): DisplayableEwRiskResults { @@ -24,41 +22,8 @@ data class DisplayableEwRiskResults( val lastSuccessfullyCalculated: EwRiskLevelResult ) -fun List<CombinedEwPtRiskLevelResult>.tryLatestResultsWithDefaults(): DisplayableRiskResults { - val latestCalculation = this.maxByOrNull { it.calculatedAt } - ?: initialLowLevelEwRiskLevelResult - - val lastSuccessfullyCalculated = this.filter { it.wasSuccessfullyCalculated } - .maxByOrNull { it.calculatedAt } ?: undeterminedEwRiskLevelResult - - return DisplayableRiskResults( - lastCalculated = latestCalculation, - lastSuccessfullyCalculated = lastSuccessfullyCalculated - ) -} - -data class DisplayableRiskResults( - val lastCalculated: CombinedEwPtRiskLevelResult, - val lastSuccessfullyCalculated: CombinedEwPtRiskLevelResult -) - -private val undeterminedEwRiskLevelResult = CombinedEwPtRiskLevelResult( - PtRiskLevelResult( - calculatedAt = Instant.EPOCH, - riskState = RiskState.CALCULATION_FAILED - ), - EwUndeterminedRiskLevelResult -) - -private val initialLowLevelEwRiskLevelResult = CombinedEwPtRiskLevelResult( - PtRiskLevelResult( - calculatedAt = Instant.now(), - riskState = RiskState.LOW_RISK - ), - EwInitialLowRiskLevelResult -) - private object EwInitialLowRiskLevelResult : EwRiskLevelResult { + // TODO this causes flaky tests as this is set in memory, once. override val calculatedAt: Instant = Instant.now() override val riskState: RiskState = RiskState.LOW_RISK override val failureReason: EwRiskLevelResult.FailureReason? = null diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/execution/RiskWorkScheduler.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/execution/RiskWorkScheduler.kt new file mode 100644 index 0000000000000000000000000000000000000000..ba05ce2a89622ce6cb12e207ae02095270fd799a --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/execution/RiskWorkScheduler.kt @@ -0,0 +1,109 @@ +package de.rki.coronawarnapp.risk.execution + +import androidx.work.ExistingPeriodicWorkPolicy +import androidx.work.PeriodicWorkRequest +import androidx.work.WorkInfo +import androidx.work.WorkManager +import de.rki.coronawarnapp.diagnosiskeys.download.DownloadDiagnosisKeysTask +import de.rki.coronawarnapp.diagnosiskeys.execution.DiagnosisKeyRetrievalWorkBuilder +import de.rki.coronawarnapp.presencetracing.risk.execution.PresenceTracingWarningTask +import de.rki.coronawarnapp.presencetracing.risk.execution.PresenceTracingWarningWorkBuilder +import de.rki.coronawarnapp.task.TaskController +import de.rki.coronawarnapp.task.TaskState +import de.rki.coronawarnapp.task.common.DefaultTaskRequest +import de.rki.coronawarnapp.task.submitBlocking +import de.rki.coronawarnapp.util.coroutine.AppScope +import de.rki.coronawarnapp.util.coroutine.await +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import timber.log.Timber +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class RiskWorkScheduler @Inject constructor( + @AppScope private val appScope: CoroutineScope, + private val workManager: WorkManager, + private val taskController: TaskController, + private val presenceWorkBuilder: PresenceTracingWarningWorkBuilder, + private val diagnosisWorkBuilder: DiagnosisKeyRetrievalWorkBuilder, +) { + + suspend fun runRiskTasksNow(): List<TaskState> { + val diagnosisKeysState = appScope.async { + Timber.tag(TAG).d("Running DownloadDiagnosisKeysTask") + val result = taskController.submitBlocking( + DefaultTaskRequest( + DownloadDiagnosisKeysTask::class, + DownloadDiagnosisKeysTask.Arguments(), + originTag = "RiskWorkScheduler-runRiskTasksNow" + ) + ) + Timber.tag(TAG).d("DownloadDiagnosisKeysTask finished with %s", result) + result + } + val presenceWarningState = appScope.async { + Timber.tag(TAG).d("Running PresenceTracingWarningTask") + val result = taskController.submitBlocking( + DefaultTaskRequest( + PresenceTracingWarningTask::class, + originTag = "RiskWorkScheduler-runRiskTasksNow" + ) + ) + Timber.tag(TAG).d("PresenceTracingWarningTask finished with %s", result) + result + } + return listOf(diagnosisKeysState, presenceWarningState).awaitAll() + } + + suspend fun isScheduled(): Boolean { + val diagnosisWorkerInfos = appScope.async { + workManager.getWorkInfosForUniqueWork(WORKER_ID_PRESENCE_TRACING).await() + } + val warningWorkerInfos = appScope.async { + workManager.getWorkInfosForUniqueWork(WORKER_ID_DIAGNOSIS_KEY_DOWNLOAD).await() + } + return listOf(diagnosisWorkerInfos, warningWorkerInfos).awaitAll().all { perWorkerInfos -> + perWorkerInfos.any { it.isScheduled } + } + } + + fun setPeriodicRiskCalculation(enabled: Boolean) { + Timber.tag(TAG).i("setPeriodicRiskCalculation(enabled=$enabled)") + + if (enabled) { + val diagnosisRequest = diagnosisWorkBuilder.createPeriodicWorkRequest() + queueWorker(WORKER_ID_DIAGNOSIS_KEY_DOWNLOAD, diagnosisRequest) + + val warningRequest = presenceWorkBuilder.createPeriodicWorkRequest() + queueWorker(WORKER_ID_PRESENCE_TRACING, warningRequest) + } else { + cancelWorker(WORKER_ID_DIAGNOSIS_KEY_DOWNLOAD) + cancelWorker(WORKER_ID_PRESENCE_TRACING) + } + } + + private fun queueWorker(workerId: String, request: PeriodicWorkRequest) { + Timber.tag(TAG).d("queueWorker(workerId=%s, request=%s)", workerId, request) + workManager.enqueueUniquePeriodicWork( + workerId, + ExistingPeriodicWorkPolicy.KEEP, + request, + ) + } + + private fun cancelWorker(workerId: String) { + Timber.tag(TAG).d("cancelWorker(workerId=$workerId") + workManager.cancelUniqueWork(workerId) + } + + private val WorkInfo.isScheduled: Boolean + get() = state == WorkInfo.State.RUNNING || state == WorkInfo.State.ENQUEUED + + companion object { + private const val WORKER_ID_DIAGNOSIS_KEY_DOWNLOAD = "DiagnosisKeyRetrievalWorker" + private const val WORKER_ID_PRESENCE_TRACING = "PresenceTracingWarningWorker" + private const val TAG = "RiskWorkScheduler" + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/storage/BaseRiskLevelStorage.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/storage/BaseRiskLevelStorage.kt index a0844cd0b8f1415b491cdf1fd3dfc16133335a3e..d5d0d71f03319705140b0a456969d7e9c2289b4f 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/storage/BaseRiskLevelStorage.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/storage/BaseRiskLevelStorage.kt @@ -1,14 +1,19 @@ package de.rki.coronawarnapp.risk.storage import androidx.annotation.VisibleForTesting -import de.rki.coronawarnapp.presencetracing.risk.PresenceTracingDayRisk -import de.rki.coronawarnapp.presencetracing.risk.PresenceTracingRiskRepository +import com.google.android.gms.nearby.exposurenotification.ExposureWindow import de.rki.coronawarnapp.presencetracing.risk.PtRiskLevelResult -import de.rki.coronawarnapp.presencetracing.risk.mapToRiskState +import de.rki.coronawarnapp.presencetracing.risk.TraceLocationCheckInRisk +import de.rki.coronawarnapp.presencetracing.risk.calculation.PresenceTracingDayRisk +import de.rki.coronawarnapp.presencetracing.risk.calculation.mapToRiskState +import de.rki.coronawarnapp.presencetracing.risk.storage.PresenceTracingRiskRepository +import de.rki.coronawarnapp.risk.CombinedEwPtDayRisk +import de.rki.coronawarnapp.risk.CombinedEwPtRiskLevelResult import de.rki.coronawarnapp.risk.EwRiskLevelResult import de.rki.coronawarnapp.risk.EwRiskLevelTaskResult +import de.rki.coronawarnapp.risk.LastCombinedRiskResults import de.rki.coronawarnapp.risk.RiskState -import de.rki.coronawarnapp.risk.TraceLocationCheckInRisk +import de.rki.coronawarnapp.risk.result.EwAggregatedRiskResult import de.rki.coronawarnapp.risk.result.ExposureWindowDayRisk import de.rki.coronawarnapp.risk.storage.internal.RiskResultDatabase import de.rki.coronawarnapp.risk.storage.internal.riskresults.PersistedRiskLevelResultDao @@ -30,7 +35,7 @@ import de.rki.coronawarnapp.util.flow.combine as flowCombine abstract class BaseRiskLevelStorage constructor( private val riskResultDatabaseFactory: RiskResultDatabase.Factory, - presenceTracingRiskRepository: PresenceTracingRiskRepository, + private val presenceTracingRiskRepository: PresenceTracingRiskRepository, scope: CoroutineScope ) : RiskLevelStorage { @@ -185,44 +190,20 @@ abstract class BaseRiskLevelStorage constructor( } .shareLatest(tag = TAG, scope = scope) - private val latestAndLastSuccessfulPtRiskLevelResult: Flow<List<PtRiskLevelResult>> = - presenceTracingRiskRepository - .latestAndLastSuccessful() - .shareLatest(tag = TAG, scope = scope) - - // TODO maybe refactor - override val latestAndLastSuccessfulCombinedEwPtRiskLevelResult: Flow<List<CombinedEwPtRiskLevelResult>> + // used for risk state in tracing state/details + override val latestAndLastSuccessfulCombinedEwPtRiskLevelResult: Flow<LastCombinedRiskResults> get() = combine( - latestAndLastSuccessfulEwRiskLevelResult, - latestAndLastSuccessfulPtRiskLevelResult + allEwRiskLevelResults, + presenceTracingRiskRepository.allEntries() ) { ewRiskLevelResults, ptRiskLevelResults -> - val latestEwResult = ewRiskLevelResults.maxByOrNull { it.calculatedAt } - val latestPtResult = ptRiskLevelResults.maxByOrNull { it.calculatedAt } - val combinedList = mutableListOf<CombinedEwPtRiskLevelResult>() - if (latestEwResult != null && latestPtResult != null) { - combinedList.add( - CombinedEwPtRiskLevelResult( - ewRiskLevelResult = latestEwResult, - ptRiskLevelResult = latestPtResult - ) - ) - } - val lastSuccessfulEwResult = ewRiskLevelResults - .filter { it.wasSuccessfullyCalculated }.maxByOrNull { it.calculatedAt } - val lastSuccessfulPtResult = ptRiskLevelResults - .filter { it.wasSuccessfullyCalculated }.maxByOrNull { it.calculatedAt } - if (lastSuccessfulEwResult != null && lastSuccessfulPtResult != null) { - combinedList.add( - CombinedEwPtRiskLevelResult( - ewRiskLevelResult = lastSuccessfulEwResult, - // current ptDayRiskStates belong to the last successful calculation - ugly - ptRiskLevelResult = lastSuccessfulPtResult.copy( - presenceTracingDayRisk = ptDayRiskStates.first() - ) - ) - ) - } - combinedList + + val combinedResults = combineEwPtRiskLevelResults(ptRiskLevelResults, ewRiskLevelResults) + .sortedByDescending { it.calculatedAt } + + LastCombinedRiskResults( + lastCalculated = combinedResults.firstOrNull() ?: currentCombinedLowRisk, + lastSuccessfullyCalculated = combinedResults.find { it.wasSuccessfullyCalculated } ?: initialCombined + ) } private val latestPtRiskLevelResults: Flow<List<PtRiskLevelResult>> = @@ -230,33 +211,15 @@ abstract class BaseRiskLevelStorage constructor( .latestEntries(2) .shareLatest(tag = TAG, scope = scope) + // used for risk level change detector to trigger notification override val latestCombinedEwPtRiskLevelResults: Flow<List<CombinedEwPtRiskLevelResult>> get() = combine( latestEwRiskLevelResults, latestPtRiskLevelResults ) { ewRiskLevelResults, ptRiskLevelResults -> - val latestEwResult = ewRiskLevelResults.maxByOrNull { it.calculatedAt } - val latestPtResult = ptRiskLevelResults.maxByOrNull { it.calculatedAt } - val olderEwResult = ewRiskLevelResults.maxByOrNull { it.calculatedAt } - val olderPtResult = ptRiskLevelResults.maxByOrNull { it.calculatedAt } - val combinedList = mutableListOf<CombinedEwPtRiskLevelResult>() - if (latestEwResult != null && latestPtResult != null) { - combinedList.add( - CombinedEwPtRiskLevelResult( - ewRiskLevelResult = latestEwResult, - ptRiskLevelResult = latestPtResult - ) - ) - } - if (olderEwResult != null && olderPtResult != null) { - combinedList.add( - CombinedEwPtRiskLevelResult( - ewRiskLevelResult = olderEwResult, - ptRiskLevelResult = olderPtResult - ) - ) - } - combinedList + combineEwPtRiskLevelResults(ptRiskLevelResults, ewRiskLevelResults) + .sortedByDescending { it.calculatedAt } + .take(2) } internal abstract suspend fun storeExposureWindows(storedResultId: String, resultEw: EwRiskLevelResult) @@ -266,6 +229,7 @@ abstract class BaseRiskLevelStorage constructor( override suspend fun clear() { Timber.w("clear() - Clearing stored risklevel/exposure-detection results.") database.clearAllTables() + presenceTracingRiskRepository.clearAllTables() } companion object { @@ -284,7 +248,7 @@ internal fun combineRisk( val ewRisk = exposureWindowDayRiskList.find { it.localDateUtc == date } CombinedEwPtDayRisk( date, - max( + combine( ptRisk?.riskState, ewRisk?.riskLevel?.mapToRiskState() ) @@ -292,7 +256,7 @@ internal fun combineRisk( } } -internal fun max(left: RiskState?, right: RiskState?): RiskState { +internal fun combine(left: RiskState?, right: RiskState?): RiskState { return if (left == RiskState.INCREASED_RISK || right == RiskState.INCREASED_RISK) RiskState.INCREASED_RISK else if (left == RiskState.LOW_RISK || right == RiskState.LOW_RISK) RiskState.LOW_RISK else RiskState.CALCULATION_FAILED @@ -308,3 +272,66 @@ internal fun max(left: LocalDate?, right: LocalDate?): LocalDate? { return if (left.isAfter(right)) left else right } + +@VisibleForTesting(otherwise = PRIVATE) +internal fun combineEwPtRiskLevelResults( + ptRiskResults: List<PtRiskLevelResult>, + ewRiskResults: List<EwRiskLevelResult> +): List<CombinedEwPtRiskLevelResult> { + val allDates = ptRiskResults.map { it.calculatedAt }.plus(ewRiskResults.map { it.calculatedAt }).distinct() + val sortedPtResults = ptRiskResults.sortedByDescending { it.calculatedAt } + val sortedEwResults = ewRiskResults.sortedByDescending { it.calculatedAt } + return allDates.map { date -> + val ptRisk = sortedPtResults.find { it.calculatedAt <= date } ?: ptInitialRiskLevelResult + val ewRisk = sortedEwResults.find { it.calculatedAt <= date } ?: EwInitialRiskLevelResult + CombinedEwPtRiskLevelResult( + ptRisk, + ewRisk + ) + } +} + +private object EwInitialRiskLevelResult : EwRiskLevelResult { + override val calculatedAt: Instant = Instant.EPOCH + override val riskState: RiskState = RiskState.CALCULATION_FAILED + override val failureReason: EwRiskLevelResult.FailureReason? = null + override val ewAggregatedRiskResult: EwAggregatedRiskResult? = null + override val exposureWindows: List<ExposureWindow>? = null + override val matchedKeyCount: Int = 0 + override val daysWithEncounters: Int = 0 +} + +private val ptInitialRiskLevelResult: PtRiskLevelResult by lazy { + PtRiskLevelResult( + calculatedAt = Instant.EPOCH, + riskState = RiskState.CALCULATION_FAILED + ) +} + +private val ewCurrentLowRiskLevelResult + get() = object : EwRiskLevelResult { + override val calculatedAt: Instant = Instant.now() + override val riskState: RiskState = RiskState.LOW_RISK + override val failureReason: EwRiskLevelResult.FailureReason? = null + override val ewAggregatedRiskResult: EwAggregatedRiskResult? = null + override val exposureWindows: List<ExposureWindow>? = null + override val matchedKeyCount: Int = 0 + override val daysWithEncounters: Int = 0 + } + +private val ptCurrentLowRiskLevelResult: PtRiskLevelResult + get() = PtRiskLevelResult( + calculatedAt = Instant.now(), + riskState = RiskState.LOW_RISK + ) + +private val initialCombined = CombinedEwPtRiskLevelResult( + ptInitialRiskLevelResult, + EwInitialRiskLevelResult +) + +private val currentCombinedLowRisk: CombinedEwPtRiskLevelResult + get() = CombinedEwPtRiskLevelResult( + ptCurrentLowRiskLevelResult, + ewCurrentLowRiskLevelResult + ) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/storage/RiskLevelStorage.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/storage/RiskLevelStorage.kt index 46d4366ef381eaf6cd2d4d992692f80ffebec0d6..a72b458ba00fde938c47d01db704dc33001864d5 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/storage/RiskLevelStorage.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/storage/RiskLevelStorage.kt @@ -1,15 +1,13 @@ package de.rki.coronawarnapp.risk.storage -import de.rki.coronawarnapp.presencetracing.risk.PresenceTracingDayRisk -import de.rki.coronawarnapp.presencetracing.risk.PtRiskLevelResult +import de.rki.coronawarnapp.presencetracing.risk.TraceLocationCheckInRisk +import de.rki.coronawarnapp.presencetracing.risk.calculation.PresenceTracingDayRisk +import de.rki.coronawarnapp.risk.CombinedEwPtDayRisk +import de.rki.coronawarnapp.risk.CombinedEwPtRiskLevelResult import de.rki.coronawarnapp.risk.EwRiskLevelResult -import de.rki.coronawarnapp.risk.RiskState -import de.rki.coronawarnapp.risk.TraceLocationCheckInRisk +import de.rki.coronawarnapp.risk.LastCombinedRiskResults import de.rki.coronawarnapp.risk.result.ExposureWindowDayRisk -import de.rki.coronawarnapp.util.TimeAndDateExtensions.toLocalDateUtc import kotlinx.coroutines.flow.Flow -import org.joda.time.Instant -import org.joda.time.LocalDate interface RiskLevelStorage { @@ -51,7 +49,7 @@ interface RiskLevelStorage { * Can be 0-2 entries. * Newest item first. */ - val latestAndLastSuccessfulCombinedEwPtRiskLevelResult: Flow<List<CombinedEwPtRiskLevelResult>> + val latestAndLastSuccessfulCombinedEwPtRiskLevelResult: Flow<LastCombinedRiskResults> /** EXPOSURE WINDOW RISK RESULT * Risk level per date/day @@ -82,48 +80,3 @@ interface RiskLevelStorage { suspend fun clear() } - -data class CombinedEwPtDayRisk( - val localDate: LocalDate, - val riskState: RiskState -) - -data class CombinedEwPtRiskLevelResult( - val ptRiskLevelResult: PtRiskLevelResult, - val ewRiskLevelResult: EwRiskLevelResult -) { - - val riskState: RiskState = max(ptRiskLevelResult.riskState, ewRiskLevelResult.riskState) - - val wasSuccessfullyCalculated: Boolean - get() = ewRiskLevelResult.ewAggregatedRiskResult != null && - ptRiskLevelResult.riskState != RiskState.CALCULATION_FAILED - - val calculatedAt: Instant = max(ewRiskLevelResult.calculatedAt, ptRiskLevelResult.calculatedAt) - - val daysWithEncounters: Int - get() = when (riskState) { - RiskState.INCREASED_RISK -> { - (ewRiskLevelResult.ewAggregatedRiskResult?.numberOfDaysWithHighRisk ?: 0) + - ptRiskLevelResult.numberOfDaysWithHighRisk - } - RiskState.LOW_RISK -> { - (ewRiskLevelResult.ewAggregatedRiskResult?.numberOfDaysWithLowRisk ?: 0) + - ptRiskLevelResult.numberOfDaysWithLowRisk - } - else -> 0 - } - - val lastRiskEncounterAt: LocalDate? - get() = if (riskState == RiskState.INCREASED_RISK) { - max( - ewRiskLevelResult.ewAggregatedRiskResult?.mostRecentDateWithHighRisk?.toLocalDateUtc(), - ptRiskLevelResult.mostRecentDateWithHighRisk - ) - } else { - max( - ewRiskLevelResult.ewAggregatedRiskResult?.mostRecentDateWithLowRisk?.toLocalDateUtc(), - ptRiskLevelResult.mostRecentDateWithLowRisk - ) - } -} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/SubmissionRepository.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/SubmissionRepository.kt index 016ce630f2d69af7780e42975c20d1a6403692cc..29a64f31616a0e3b6f3e0ee5ca98b6d8b5007a8f 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/SubmissionRepository.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/SubmissionRepository.kt @@ -40,7 +40,8 @@ class SubmissionRepository @Inject constructor( private val backgroundNoise: BackgroundNoise, private val analyticsKeySubmissionCollector: AnalyticsKeySubmissionCollector, private val tracingSettings: TracingSettings, - private val testResultDataCollector: TestResultDataCollector + private val testResultDataCollector: TestResultDataCollector, + private val backgroundWorkScheduler: BackgroundWorkScheduler, ) { private val testResultReceivedDateFlowInternal = MutableStateFlow((submissionSettings.initialTestResultReceivedAt ?: timeStamper.nowUTC).toDate()) @@ -192,7 +193,7 @@ class SubmissionRepository @Inject constructor( submissionSettings.initialTestResultReceivedAt = currentTime testResultReceivedDateFlowInternal.value = currentTime.toDate() if (testResult == TestResult.PENDING) { - BackgroundWorkScheduler.startWorkScheduler() + backgroundWorkScheduler.startWorkScheduler() } } else { testResultReceivedDateFlowInternal.value = initialTestResultReceivedTimestamp.toDate() diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/task/SubmissionTask.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/task/SubmissionTask.kt index 7265d2bb6e59b24541b4034d7e8b3029d269ef52..a860a2edb3b0e5692ea03bcb94157c091882638f 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/task/SubmissionTask.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/task/SubmissionTask.kt @@ -41,7 +41,8 @@ class SubmissionTask @Inject constructor( private val testResultAvailableNotificationService: TestResultAvailableNotificationService, private val checkInsRepository: CheckInRepository, private val checkInsTransformer: CheckInsTransformer, - private val analyticsKeySubmissionCollector: AnalyticsKeySubmissionCollector + private val analyticsKeySubmissionCollector: AnalyticsKeySubmissionCollector, + private val backgroundWorkScheduler: BackgroundWorkScheduler, ) : Task<DefaultProgress, SubmissionTask.Result> { private val internalProgress = ConflatedBroadcastChannel<DefaultProgress>() @@ -176,9 +177,9 @@ class SubmissionTask @Inject constructor( private fun setSubmissionFinished() { Timber.tag(TAG).d("setSubmissionFinished()") - BackgroundWorkScheduler.stopWorkScheduler() + backgroundWorkScheduler.stopWorkScheduler() submissionSettings.isSubmissionSuccessful = true - BackgroundWorkScheduler.startWorkScheduler() + backgroundWorkScheduler.startWorkScheduler() shareTestResultNotificationService.cancelSharePositiveTestResultNotification() testResultAvailableNotificationService.cancelTestResultAvailableNotification() diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/tracing/states/TracingStateProvider.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/tracing/states/TracingStateProvider.kt index c3d9018fc884b95d644643eeb8f0f713e0fcc0c8..5aa675aae2f4b9182fce26433476fb2427f97a6c 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/tracing/states/TracingStateProvider.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/tracing/states/TracingStateProvider.kt @@ -8,7 +8,6 @@ import de.rki.coronawarnapp.nearby.modules.detectiontracker.ExposureDetectionTra import de.rki.coronawarnapp.nearby.modules.detectiontracker.latestSubmission import de.rki.coronawarnapp.risk.RiskState import de.rki.coronawarnapp.risk.storage.RiskLevelStorage -import de.rki.coronawarnapp.risk.tryLatestResultsWithDefaults import de.rki.coronawarnapp.storage.TracingRepository import de.rki.coronawarnapp.tracing.GeneralTracingStatus import de.rki.coronawarnapp.tracing.TracingProgress @@ -51,15 +50,13 @@ class TracingStateProvider @AssistedInject constructor( latestSubmission, isBackgroundJobEnabled -> - val ( - latestCalc, - latestSuccessfulCalc - ) = riskLevelResults.tryLatestResultsWithDefaults() + val latestCalc = riskLevelResults.lastCalculated + val lastSuccessfullyCalc = riskLevelResults.lastSuccessfullyCalculated return@combine when { tracingStatus == GeneralTracingStatus.Status.TRACING_INACTIVE -> TracingDisabled( isInDetailsMode = isDetailsMode, - riskState = latestSuccessfulCalc.riskState, + riskState = lastSuccessfullyCalc.riskState, lastExposureDetectionTime = latestSubmission?.startedAt ) tracingProgress != TracingProgress.Idle -> TracingInProgress( @@ -86,7 +83,7 @@ class TracingStateProvider @AssistedInject constructor( ) else -> TracingFailed( isInDetailsMode = isDetailsMode, - riskState = latestSuccessfulCalc.riskState, + riskState = lastSuccessfullyCalc.riskState, lastExposureDetectionTime = latestSubmission?.startedAt ) } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/tracing/ui/details/TracingDetailsFragmentViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/tracing/ui/details/TracingDetailsFragmentViewModel.kt index eba70d84b142b805a168c7253e5c67b18fd0779e..dd52cdbc939d76e6c95e680b4982fe33ebd93500 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/tracing/ui/details/TracingDetailsFragmentViewModel.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/tracing/ui/details/TracingDetailsFragmentViewModel.kt @@ -9,7 +9,6 @@ import de.rki.coronawarnapp.datadonation.survey.Surveys.ConsentResult.AlreadyGiv import de.rki.coronawarnapp.datadonation.survey.Surveys.ConsentResult.Needed import de.rki.coronawarnapp.risk.RiskState import de.rki.coronawarnapp.risk.storage.RiskLevelStorage -import de.rki.coronawarnapp.risk.tryLatestResultsWithDefaults import de.rki.coronawarnapp.storage.TracingRepository import de.rki.coronawarnapp.tracing.GeneralTracingStatus import de.rki.coronawarnapp.tracing.states.IncreasedRisk @@ -81,7 +80,7 @@ class TracingDetailsFragmentViewModel @AssistedInject constructor( riskLevelResults, isBackgroundJobEnabled -> - val (latestCalc, _) = riskLevelResults.tryLatestResultsWithDefaults() + val latestCalc = riskLevelResults.lastCalculated val isRestartButtonEnabled = !isBackgroundJobEnabled || latestCalc.riskState == RiskState.CALCULATION_FAILED diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/tracing/ui/details/TracingDetailsItemProvider.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/tracing/ui/details/TracingDetailsItemProvider.kt index f58268644a91e315a23dce10bd13513da1be2568..f47238e681de2d5ec7077d10bf0ba2e1d60aa649 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/tracing/ui/details/TracingDetailsItemProvider.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/tracing/ui/details/TracingDetailsItemProvider.kt @@ -5,7 +5,6 @@ import de.rki.coronawarnapp.datadonation.survey.Surveys import de.rki.coronawarnapp.installTime.InstallTimeProvider import de.rki.coronawarnapp.risk.RiskState import de.rki.coronawarnapp.risk.storage.RiskLevelStorage -import de.rki.coronawarnapp.risk.tryLatestResultsWithDefaults import de.rki.coronawarnapp.tracing.GeneralTracingStatus import de.rki.coronawarnapp.tracing.GeneralTracingStatus.Status import de.rki.coronawarnapp.tracing.ui.details.items.DetailsItem @@ -43,7 +42,7 @@ class TracingDetailsItemProvider @Inject constructor( riskLevelResults, availableSurveys -> - val (latestCalc, _) = riskLevelResults.tryLatestResultsWithDefaults() + val latestCalc = riskLevelResults.lastCalculated mutableListOf<DetailsItem>().apply { if (status != Status.TRACING_INACTIVE && diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/tracing/ui/details/items/riskdetails/DetailsLowRiskBox.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/tracing/ui/details/items/riskdetails/DetailsLowRiskBox.kt index 3628d6addafda4ec484a6d988aa7a0142a4d3ab8..574698d978b25369e7610465708645e40beb0f21 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/tracing/ui/details/items/riskdetails/DetailsLowRiskBox.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/tracing/ui/details/items/riskdetails/DetailsLowRiskBox.kt @@ -50,6 +50,7 @@ class DetailsLowRiskBox( ) : RiskDetailsStateItem { fun getRiskDetailsRiskLevelBody(c: Context): String { + // TODO consider pt encounters? return c.getString( if (matchedKeyCount > 0) R.string.risk_details_information_body_low_risk_with_encounter else R.string.risk_details_information_body_low_risk diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/tracing/ui/settings/SettingsTracingFragmentViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/tracing/ui/settings/SettingsTracingFragmentViewModel.kt index 76af2958e4c24c74ad19e65dc40976470bb64161..571ae77c03533982ee966b7edbd1709094b7a9fe 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/tracing/ui/settings/SettingsTracingFragmentViewModel.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/tracing/ui/settings/SettingsTracingFragmentViewModel.kt @@ -34,7 +34,8 @@ class SettingsTracingFragmentViewModel @AssistedInject constructor( tracingStatus: GeneralTracingStatus, installTimeProvider: InstallTimeProvider, private val backgroundStatus: BackgroundModeStatus, - tracingPermissionHelperFactory: TracingPermissionHelper.Factory + tracingPermissionHelperFactory: TracingPermissionHelper.Factory, + private val backgroundWorkScheduler: BackgroundWorkScheduler ) : CWAViewModel(dispatcherProvider = dispatcherProvider) { val loggingPeriod: LiveData<PeriodLoggedBox.Item> = @@ -75,7 +76,7 @@ class SettingsTracingFragmentViewModel @AssistedInject constructor( if (!backgroundStatus.isIgnoringBatteryOptimizations.first()) { events.postValue(Event.ManualCheckingDialog) } - BackgroundWorkScheduler.startWorkScheduler() + backgroundWorkScheduler.startWorkScheduler() } isTracingSwitchChecked.postValue(isTracingEnabled) } @@ -109,7 +110,7 @@ class SettingsTracingFragmentViewModel @AssistedInject constructor( launch { if (InternalExposureNotificationClient.asyncIsEnabled()) { InternalExposureNotificationClient.asyncStop() - BackgroundWorkScheduler.stopWorkScheduler() + backgroundWorkScheduler.stopWorkScheduler() } } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/color/Color.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/color/Color.kt new file mode 100644 index 0000000000000000000000000000000000000000..3ae8e15318e8231b5d52e460ce4227db18d75817 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/color/Color.kt @@ -0,0 +1,17 @@ +package de.rki.coronawarnapp.ui.color + +import android.graphics.Color +import androidx.annotation.ColorInt +import timber.log.Timber + +/** + * Parse color from String - default color is a fallback + * @param defaultColor [Int] color + */ +fun String.parseColor(@ColorInt defaultColor: Int = Color.BLACK): Int = + try { + Color.parseColor(this) + } catch (e: Exception) { + Timber.d(e, "Parsing color failed") + defaultColor + } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/EventRegistrationUIModule.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/EventRegistrationUIModule.kt index 8840ff0adfeffac890939bad167637d87be50db1..e6f0123e69c4734517875010ea148e6006d78206 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/EventRegistrationUIModule.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/EventRegistrationUIModule.kt @@ -6,10 +6,10 @@ import de.rki.coronawarnapp.ui.eventregistration.attendee.checkins.CheckInsFragm import de.rki.coronawarnapp.ui.eventregistration.attendee.checkins.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.onboarding.CheckInOnboardingFragment -import de.rki.coronawarnapp.ui.eventregistration.attendee.onboarding.CheckInOnboardingModule import de.rki.coronawarnapp.ui.eventregistration.attendee.edit.EditCheckInFragment import de.rki.coronawarnapp.ui.eventregistration.attendee.edit.EditCheckInModule +import de.rki.coronawarnapp.ui.eventregistration.attendee.onboarding.CheckInOnboardingFragment +import de.rki.coronawarnapp.ui.eventregistration.attendee.onboarding.CheckInOnboardingModule import de.rki.coronawarnapp.ui.eventregistration.attendee.scan.ScanCheckInQrCodeFragment import de.rki.coronawarnapp.ui.eventregistration.attendee.scan.ScanCheckInQrCodeModule import de.rki.coronawarnapp.ui.eventregistration.organizer.category.TraceLocationCategoryFragment @@ -18,6 +18,8 @@ import de.rki.coronawarnapp.ui.eventregistration.organizer.create.TraceLocationC import de.rki.coronawarnapp.ui.eventregistration.organizer.create.TraceLocationCreateFragmentModule import de.rki.coronawarnapp.ui.eventregistration.organizer.list.TraceLocationsFragment import de.rki.coronawarnapp.ui.eventregistration.organizer.list.TraceLocationsFragmentModule +import de.rki.coronawarnapp.ui.eventregistration.organizer.poster.QrCodePosterFragment +import de.rki.coronawarnapp.ui.eventregistration.organizer.poster.QrCodePosterFragmentModule import de.rki.coronawarnapp.ui.eventregistration.organizer.qrinfo.TraceLocationQRInfoFragment import de.rki.coronawarnapp.ui.eventregistration.organizer.qrinfo.TraceLocationQRInfoFragmentModule @@ -50,4 +52,7 @@ internal abstract class EventRegistrationUIModule { @ContributesAndroidInjector(modules = [TraceLocationsFragmentModule::class]) abstract fun traceLocationsFragment(): TraceLocationsFragment + + @ContributesAndroidInjector(modules = [QrCodePosterFragmentModule::class]) + abstract fun qrCodePosterFragment(): QrCodePosterFragment } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/attendee/checkins/CheckInsFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/attendee/checkins/CheckInsFragment.kt index eaa1800739549ee6f5c44a1a7331dc3e6d91a371..b39c06703c9b30c4dd27fd5f0a4802e59f545375 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/attendee/checkins/CheckInsFragment.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/attendee/checkins/CheckInsFragment.kt @@ -12,7 +12,6 @@ import androidx.appcompat.widget.Toolbar import androidx.core.net.toUri import androidx.core.view.isGone import androidx.fragment.app.Fragment -import androidx.navigation.NavOptions import androidx.navigation.fragment.FragmentNavigatorExtras import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs @@ -24,7 +23,6 @@ import de.rki.coronawarnapp.databinding.TraceLocationAttendeeCheckinsFragmentBin import de.rki.coronawarnapp.eventregistration.checkins.CheckIn import de.rki.coronawarnapp.ui.eventregistration.attendee.checkins.items.CameraPermissionVH import de.rki.coronawarnapp.ui.eventregistration.attendee.checkins.items.CheckInsItem -import de.rki.coronawarnapp.util.CWADebug import de.rki.coronawarnapp.util.di.AutoInject import de.rki.coronawarnapp.util.list.isSwipeable import de.rki.coronawarnapp.util.list.onSwipeItem @@ -38,6 +36,8 @@ import de.rki.coronawarnapp.util.ui.viewBindingLazy import de.rki.coronawarnapp.util.viewmodel.CWAViewModelFactoryProvider import de.rki.coronawarnapp.util.viewmodel.cwaViewModelsAssisted import timber.log.Timber +import java.lang.Exception +import java.net.URLEncoder import javax.inject.Inject class CheckInsFragment : Fragment(R.layout.trace_location_attendee_checkins_fragment), AutoInject { @@ -149,18 +149,6 @@ class CheckInsFragment : Fragment(R.layout.trace_location_attendee_checkins_frag FragmentNavigatorExtras(this to transitionName) ) } - // TODO Remove once feature is done - if (CWADebug.isDeviceForTestersBuild) { - setOnLongClickListener { - findNavController().navigate( - createCheckInUri(DEBUG_CHECKINS.random()), - NavOptions.Builder().apply { - setLaunchSingleTop(true) - }.build() - ) - true - } - } } } @@ -224,15 +212,14 @@ class CheckInsFragment : Fragment(R.layout.trace_location_attendee_checkins_frag } companion object { - fun createCheckInUri(rootUri: String): Uri = "coronawarnapp://check-ins/$rootUri".toUri() - - @Suppress("MaxLineLength") - private val DEBUG_CHECKINS = listOf( - "HTTPS://E.CORONAWARN.APP/C1/BJHAUJDFMNSTKMJYGY3S2NJYHA4S2NBRG5QS2YLGMM3C2ZDDHFRTSNRSGZTGIYZWCAARQAJCBVEWGZLDOJSWC3JAKNUG64BKBVGWC2LOEBJXI4TFMV2CAMJQAA4AAQAKCJDTARICEBFRIDICXSP4QTNMBRDF7EOJ3EIJD6AWT24YDOWWXQI22KCUD7R7WARBAC7ONBRPJDB2KK6QKZLF4RE3PXU7PMON4IOZVIHCYPJGBZ27FF5S4", - "HTTPS://E.CORONAWARN.APP/C1/BJHAUJDEMVRDGZTGMU2C2MZUGQ2C2NBWGZQS2YLCHEYC2NJQHBRDCMBRMVTDIZBTCAARQAJCBVEWGZLDOJSWC3JAKNUG64BKBVGWC2LOEBJXI4TFMV2CAMJQAA4AAQAKCJDTARICEEAJRWAYJARF3V4AS5OVBODPLPX2V3IJFMFU4O2CAKRH6HGHHWCDMJYCEBH7BO2IU2EEGRKEXBZT2DAOFIMXES5ETUT45QIWDCX64APY7C2ME", - "HTTPS://E.CORONAWARN.APP/C1/BJHAUJDBMNQWIMLFHA3S2NZQGVTC2NDDGY3C2ODGGBTC2ZBWGQYDCZJUMRTDEN3FCAARQAJCBVEWGZLDOJSWC3JAKNUG64BKBVGWC2LOEBJXI4TFMV2CAMJQAA4AAQAKCJDTARICEEAKJM3RPYMM2VVCE2GLVK6OKY36F64FRNSI6DWYV7WW6MGESFCDNNQCEA44UHS2GEWHJYHTIJ3AJYM6BC3HEIYHY2HRMPIP7ZF62YBAUKOIY", - "HTTPS://E.CORONAWARN.APP/C1/BJHAUJBQGZRDOMJXHEYC2NBRG44C2NBWGZRC2YRQGA4S2MJTGRRDQOJZMU4DMMRQCAARQAJCBVEWGZLDOJSWC3JAKNUG64BKBVGWC2LOEBJXI4TFMV2CAMJQAA4AAQAKCJDTARICEB365TX5SEWICC3JUOAZCQX5YUK2LZZA7RGRTNBXTSEBXTD2766CAARBADXEYUJHQSE7QRQOIPEMSSPLCVC5D4I3FOBDRX64NASE47XKKK5EY", - "HTTPS://E.CORONAWARN.APP/C1/BJHAUJDGGE4WKMTEMQZC2OJRMUYC2NBQGNTC2OJZMZRC2MTEG4ZWGMJTGA3GEOBTCAARQAJCBVEWGZLDOJSWC3JAKNUG64BKBVGWC2LOEBJXI4TFMV2CAMJQAA4AAQAKCJDTARICEEANT4HDNB7V5DWCKKUV22YQ7NYOBCTOZ2QUFBOUDZS6V5J2VRVLVSICEBU2YHAEBPQSLWTR75VFC6OEFIS22V6KU4NRDYZHTIBMHS4FDADG6", - ) + fun createCheckInUri(rootUri: String): Uri { + val encodedUrl = try { + URLEncoder.encode(rootUri, Charsets.UTF_8.name()) + } catch (e: Exception) { + Timber.d(e, "URL Encoding failed url($rootUri)") + rootUri // Pass original + } + return "coronawarnapp://check-ins/$encodedUrl".toUri() + } } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/attendee/checkins/CheckInsViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/attendee/checkins/CheckInsViewModel.kt index dbfafa23aac8f2e6410cc218b13eceb6d12170c5..e78ae163bec10c1d32b585fc1482ed5ea204597f 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/attendee/checkins/CheckInsViewModel.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/attendee/checkins/CheckInsViewModel.kt @@ -8,13 +8,11 @@ import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import de.rki.coronawarnapp.eventregistration.checkins.CheckIn import de.rki.coronawarnapp.eventregistration.checkins.CheckInRepository -import de.rki.coronawarnapp.eventregistration.checkins.qrcode.InvalidQRCodeDataException import de.rki.coronawarnapp.eventregistration.checkins.qrcode.QRCodeUriParser import de.rki.coronawarnapp.eventregistration.checkins.qrcode.VerifiedTraceLocation import de.rki.coronawarnapp.exception.ExceptionCategory import de.rki.coronawarnapp.exception.reporting.report import de.rki.coronawarnapp.presencetracing.checkins.checkout.CheckOutHandler -import de.rki.coronawarnapp.server.protocols.internal.pt.TraceLocationOuterClass import de.rki.coronawarnapp.ui.eventregistration.attendee.checkins.items.ActiveCheckInVH import de.rki.coronawarnapp.ui.eventregistration.attendee.checkins.items.CameraPermissionVH import de.rki.coronawarnapp.ui.eventregistration.attendee.checkins.items.CheckInsItem @@ -138,15 +136,7 @@ class CheckInsViewModel @AssistedInject constructor( private fun verifyUri(uri: String) = launch { try { Timber.i("uri: $uri") - val qrCodePayloadRaw = qrCodeUriParser.getQrCodePayload(uri)?.toByteArray() - ?: throw IllegalArgumentException("Invalid uri: $uri") - - val qrCodePayload = try { - TraceLocationOuterClass.QRCodePayload.parseFrom(qrCodePayloadRaw) - } catch (e: Exception) { - throw InvalidQRCodeDataException(cause = e, message = "QR-code data could not be parsed.") - } - + val qrCodePayload = qrCodeUriParser.getQrCodePayload(uri) val verifiedTraceLocation = VerifiedTraceLocation(qrCodePayload) events.postValue(CheckInEvent.ConfirmCheckIn(verifiedTraceLocation)) } catch (e: Exception) { 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 index bd50ffcbe58afd1376da965d07776a3d8d8d0864..840682e97c68268d05e0cfc5ce38376f7c1a3eba 100644 --- 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 @@ -88,23 +88,25 @@ class ConfirmCheckInViewModel @AssistedInject constructor( ), completed: Boolean = false, createJournalEntry: Boolean = true - ): CheckIn = CheckIn( - traceLocationId = verifiedTraceLocation.traceLocationID, - traceLocationIdHash = verifiedTraceLocation.traceLocationID.sha256(), - version = traceLocation.version, - type = traceLocation.type.number, - description = traceLocation.description, - address = traceLocation.address, - traceLocationStart = traceLocation.startDate, - traceLocationEnd = traceLocation.endDate, - defaultCheckInLengthInMinutes = traceLocation.defaultCheckInLengthInMinutes, - cryptographicSeed = traceLocation.cryptographicSeed, - cnPublicKey = traceLocation.cnPublicKey, - checkInStart = checkInStart, - checkInEnd = checkInEnd, - completed = completed, - createJournalEntry = createJournalEntry, - ) + ): CheckIn { + val traceLocation = verifiedTraceLocation.traceLocation + return CheckIn( + traceLocationId = traceLocation.locationId, + version = traceLocation.version, + type = traceLocation.type.number, + description = traceLocation.description, + address = traceLocation.address, + traceLocationStart = traceLocation.startDate, + traceLocationEnd = traceLocation.endDate, + defaultCheckInLengthInMinutes = traceLocation.defaultCheckInLengthInMinutes, + cryptographicSeed = traceLocation.cryptographicSeed, + cnPublicKey = traceLocation.cnPublicKey, + checkInStart = checkInStart, + checkInEnd = checkInEnd, + completed = completed, + createJournalEntry = createJournalEntry, + ) + } @AssistedFactory interface Factory : CWAViewModelFactory<ConfirmCheckInViewModel> { diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/details/QrCodeDetailFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/details/QrCodeDetailFragment.kt index a300e882482f4a88c8bbdb9c2b5ea7fe924dbd68..bd2763bc00c30b84cdc58a650948906d7fa478dd 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/details/QrCodeDetailFragment.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/details/QrCodeDetailFragment.kt @@ -11,10 +11,12 @@ import androidx.fragment.app.Fragment import androidx.navigation.fragment.navArgs import com.google.android.material.appbar.AppBarLayout import com.google.android.material.appbar.AppBarLayout.OnOffsetChangedListener +import com.google.android.material.transition.MaterialContainerTransform import de.rki.coronawarnapp.R import de.rki.coronawarnapp.databinding.TraceLocationOrganizerQrCodeDetailFragmentBinding import de.rki.coronawarnapp.util.ContextExtensions.getDrawableCompat import de.rki.coronawarnapp.util.di.AutoInject +import de.rki.coronawarnapp.util.ui.doNavigate import de.rki.coronawarnapp.util.ui.observe2 import de.rki.coronawarnapp.util.ui.popBackStack import de.rki.coronawarnapp.util.ui.viewBindingLazy @@ -29,7 +31,7 @@ class QrCodeDetailFragment : Fragment(R.layout.trace_location_organizer_qr_code_ private val navArgs by navArgs<QrCodeDetailFragmentArgs>() - private val vm: QrCodeDetailViewModel by cwaViewModelsAssisted( + private val viewModel: QrCodeDetailViewModel by cwaViewModelsAssisted( factoryProducer = { viewModelFactory }, constructorCall = { factory, _ -> factory as QrCodeDetailViewModel.Factory @@ -39,6 +41,13 @@ class QrCodeDetailFragment : Fragment(R.layout.trace_location_organizer_qr_code_ private val binding: TraceLocationOrganizerQrCodeDetailFragmentBinding by viewBindingLazy() + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + sharedElementEnterTransition = MaterialContainerTransform() + sharedElementReturnTransition = MaterialContainerTransform() + } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) @@ -59,11 +68,16 @@ class QrCodeDetailFragment : Fragment(R.layout.trace_location_organizer_qr_code_ toolbar.apply { navigationIcon = context.getDrawableCompat(R.drawable.ic_close_white) navigationContentDescription = getString(R.string.accessibility_close) - setNavigationOnClickListener { vm.onBackButtonPress() } + setNavigationOnClickListener { viewModel.onBackButtonPress() } + } + + qrCodePrintButton.setOnClickListener { + viewModel.onPrintQrCode() } } - vm.qrCodeBitmap.observe2(this) { + viewModel.qrCodeBitmap.observe2(this) { + binding.progressBar.hide() binding.qrCodeImage.apply { val resourceId = RoundedBitmapDrawableFactory.create(resources, it) resourceId.cornerRadius = it.width * 0.1f @@ -71,19 +85,20 @@ class QrCodeDetailFragment : Fragment(R.layout.trace_location_organizer_qr_code_ } } - vm.routeToScreen.observe2(this) { + viewModel.routeToScreen.observe2(this) { when (it) { - QrCodeDetailNavigationEvents.NavigateBack -> { - popBackStack() - } - QrCodeDetailNavigationEvents.NavigateToPrintFragment -> { /* TODO */ - } + QrCodeDetailNavigationEvents.NavigateBack -> popBackStack() + QrCodeDetailNavigationEvents.NavigateToDuplicateFragment -> { /* TODO */ } + + is QrCodeDetailNavigationEvents.NavigateToQrCodePosterFragment -> doNavigate( + QrCodeDetailFragmentDirections.actionQrCodeDetailFragmentToQrCodePosterFragment(it.locationId) + ) } } - vm.uiState.observe2(this) { uiState -> + viewModel.uiState.observe2(this) { uiState -> with(binding) { title.text = uiState.description subtitle.text = uiState.address diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/details/QrCodeDetailNavigationEvents.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/details/QrCodeDetailNavigationEvents.kt index b91b99b27a5868544414d5093dc60311e97a2244..68027a1fd55345d94dd670307491137c2f9b8a04 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/details/QrCodeDetailNavigationEvents.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/details/QrCodeDetailNavigationEvents.kt @@ -2,6 +2,6 @@ package de.rki.coronawarnapp.ui.eventregistration.organizer.details sealed class QrCodeDetailNavigationEvents { object NavigateBack : QrCodeDetailNavigationEvents() - object NavigateToPrintFragment : QrCodeDetailNavigationEvents() + data class NavigateToQrCodePosterFragment(val locationId: Long) : QrCodeDetailNavigationEvents() object NavigateToDuplicateFragment : QrCodeDetailNavigationEvents() } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/details/QrCodeDetailViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/details/QrCodeDetailViewModel.kt index d9a58e3871a4c6ce7db771476f2ca273b5683b00..739078b1e3d5e2716236e7fca68f3bf355c1d141 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/details/QrCodeDetailViewModel.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/details/QrCodeDetailViewModel.kt @@ -1,12 +1,17 @@ package de.rki.coronawarnapp.ui.eventregistration.organizer.details import android.graphics.Bitmap +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData import androidx.lifecycle.asLiveData import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject +import de.rki.coronawarnapp.eventregistration.checkins.qrcode.QrCodeGenerator import de.rki.coronawarnapp.eventregistration.checkins.qrcode.TraceLocation import de.rki.coronawarnapp.eventregistration.storage.repo.DefaultTraceLocationRepository +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.ui.SingleLiveEvent import de.rki.coronawarnapp.util.viewmodel.CWAViewModel @@ -18,7 +23,7 @@ import org.joda.time.Instant import timber.log.Timber class QrCodeDetailViewModel @AssistedInject constructor( - @Assisted private val traceLocationId: Long?, + @Assisted private val traceLocationId: Long, private val dispatcher: DispatcherProvider, private val qrCodeGenerator: QrCodeGenerator, private val traceLocationRepository: DefaultTraceLocationRepository @@ -29,11 +34,12 @@ class QrCodeDetailViewModel @AssistedInject constructor( private val subtitleFlow = MutableStateFlow<String?>(null) private val startTimeFlow = MutableStateFlow<Instant?>(null) private val endTimeFlow = MutableStateFlow<Instant?>(null) + private val bitmapLiveData = MutableLiveData<Bitmap>() init { launch { - val traceLocation = traceLocationRepository.traceLocationForId(traceLocationId ?: 0L) + val traceLocation = traceLocationRepository.traceLocationForId(traceLocationId) if (titleFlow.value == null) { titleFlow.value = traceLocation.description @@ -53,11 +59,7 @@ class QrCodeDetailViewModel @AssistedInject constructor( traceLocationFlow.value = traceLocation - createQrCode( - "HTTPS://E.CORONAWARN.APP/C1/BIYAUEDBZY6EIWF7QX6JOKSRPAGEB3H7CIIEGV2BEBGGC5LOMNUCAUD" + - "BOJ2HSGGTQ6SACIHXQ6SACKA6CJEDARQCEEAPHGEZ5JI2K2T422L5U3SMZY5DGCPUZ2RQACAYEJ3HQYMAFF" + - "BU2SQCEEAJAUCJSQJ7WDM675MCMOD3L2UL7ECJU7TYERH23B746RQTABO3CTI=" - ) + createQrCode(traceLocation) } } @@ -84,20 +86,20 @@ class QrCodeDetailViewModel @AssistedInject constructor( val endDateTime: Instant? get() = endInstant } - val qrCodeBitmap = SingleLiveEvent<Bitmap>() - val errorMessage = SingleLiveEvent<String>() + val qrCodeBitmap: LiveData<Bitmap> = bitmapLiveData val routeToScreen: SingleLiveEvent<QrCodeDetailNavigationEvents> = SingleLiveEvent() /** * Creates a QR Code [Bitmap] ,result is delivered by [qrCodeBitmap] */ - private fun createQrCode(input: String) = launch(context = dispatcher.IO) { - + private fun createQrCode(traceLocation: TraceLocation) = launch(context = dispatcher.IO) { try { - qrCodeBitmap.postValue(qrCodeGenerator.createQrCode(input)) + val input = traceLocation.locationUrl + Timber.d("input=$input") + bitmapLiveData.postValue(qrCodeGenerator.createQrCode(input)) } catch (e: Exception) { Timber.d(e, "Qr code creation failed") - errorMessage.postValue(e.localizedMessage ?: "QR code creation failed") + e.report(ExceptionCategory.INTERNAL) } } @@ -105,10 +107,16 @@ class QrCodeDetailViewModel @AssistedInject constructor( routeToScreen.postValue(QrCodeDetailNavigationEvents.NavigateBack) } + fun onPrintQrCode() { + routeToScreen.postValue( + QrCodeDetailNavigationEvents.NavigateToQrCodePosterFragment(traceLocationId) + ) + } + @AssistedFactory interface Factory : CWAViewModelFactory<QrCodeDetailViewModel> { fun create( - traceLocationId: Long? + traceLocationId: Long ): QrCodeDetailViewModel } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/list/TraceLocationEvent.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/list/TraceLocationEvent.kt index d66aa6361215365a3402c5b72f4052ed329db2ca..5909c982933269feaeeb47a6fa3d1f3aebc0e2f8 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/list/TraceLocationEvent.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/list/TraceLocationEvent.kt @@ -10,5 +10,7 @@ sealed class TraceLocationEvent { data class ConfirmSwipeItem(val traceLocation: TraceLocation, val position: Int) : TraceLocationEvent() - data class StartQrCodeDetailFragment(val id: Long) : TraceLocationEvent() + data class StartQrCodeDetailFragment(val id: Long, val position: Int) : TraceLocationEvent() + + data class StartQrCodePosterFragment(val traceLocation: TraceLocation) : TraceLocationEvent() } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/list/TraceLocationsFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/list/TraceLocationsFragment.kt index f9edf14cac9155606064e68f1d6e5564209f8ba3..1847e8f7b3912ec7198198b0986705753f2e1952 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/list/TraceLocationsFragment.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/list/TraceLocationsFragment.kt @@ -14,6 +14,7 @@ import de.rki.coronawarnapp.R import de.rki.coronawarnapp.databinding.TraceLocationOrganizerTraceLocationsListFragmentBinding import de.rki.coronawarnapp.eventregistration.checkins.qrcode.TraceLocation import de.rki.coronawarnapp.ui.eventregistration.organizer.category.adapter.category.traceLocationCategories +import de.rki.coronawarnapp.ui.eventregistration.organizer.details.QrCodeDetailFragmentArgs import de.rki.coronawarnapp.util.DialogHelper import de.rki.coronawarnapp.util.di.AutoInject import de.rki.coronawarnapp.util.list.isSwipeable @@ -60,7 +61,7 @@ class TraceLocationsFragment : Fragment(R.layout.trace_location_organizer_trace_ } } - binding.toolbar.setOnClickListener { + binding.toolbar.setNavigationOnClickListener { popBackStack() } @@ -81,22 +82,36 @@ class TraceLocationsFragment : Fragment(R.layout.trace_location_organizer_trace_ showDeleteSingleDialog(it.traceLocation, it.position) } is TraceLocationEvent.StartQrCodeDetailFragment -> { - doNavigate( - TraceLocationsFragmentDirections.actionTraceLocationOrganizerListFragmentToQrCodeDetailFragment( - traceLocationId = it.id, - ) + + val navigatorExtras = binding.recyclerView.findViewHolderForAdapterPosition(it.position)?.itemView + ?.run { + // Set it on the fly to avoid confusion of recycler's items + this.transitionName = "trace_location_container_transition" + FragmentNavigatorExtras(this to this.transitionName) + } + + findNavController().navigate( + R.id.action_traceLocationsFragment_to_qrCodeDetailFragment, + QrCodeDetailFragmentArgs(traceLocationId = it.id).toBundle(), + null, + navigatorExtras ) } is TraceLocationEvent.DuplicateItem -> { openCreateEventFragment(it.traceLocation) } + is TraceLocationEvent.StartQrCodePosterFragment -> doNavigate( + TraceLocationsFragmentDirections.actionTraceLocationsFragmentToQrCodePosterFragment( + it.traceLocation.id + ) + ) } } binding.qrCodeFab.apply { setOnClickListener { findNavController().navigate( - R.id.action_traceLocationOrganizerListFragment_to_traceLocationOrganizerCategoriesFragment, + R.id.action_traceLocationsFragment_to_traceLocationCategoryFragment, null, null, FragmentNavigatorExtras(this to transitionName) @@ -116,7 +131,7 @@ class TraceLocationsFragment : Fragment(R.layout.trace_location_organizer_trace_ when (it.itemId) { R.id.menu_information -> { findNavController().navigate( - R.id.action_traceLocationOrganizerListFragment_to_traceLocationOrganizerQRInfoFragment + R.id.action_traceLocationOrganizerListFragment_to_traceLocationInfoFragment ) true } @@ -149,11 +164,10 @@ class TraceLocationsFragment : Fragment(R.layout.trace_location_organizer_trace_ Timber.e("Category not found, traceLocation = $traceLocation") } else { findNavController().navigate( - TraceLocationsFragmentDirections - .actionTraceLocationOrganizerListFragmentToTraceLocationCreateFragment( - category, - traceLocation - ) + TraceLocationsFragmentDirections.actionTraceLocationsFragmentToTraceLocationCreateFragment( + category, + traceLocation + ) ) } } @@ -166,18 +180,10 @@ class TraceLocationsFragment : Fragment(R.layout.trace_location_organizer_trace_ viewModel.deleteSingleTraceLocation(traceLocation) } setNegativeButton(R.string.trace_location_organiser_list_delete_all_popup_negative_button) { _, _ -> - position?.let { - traceLocationsAdapter.notifyItemChanged( - position - ) - } + position?.let { traceLocationsAdapter.notifyItemChanged(position) } } setOnCancelListener { - position?.let { - traceLocationsAdapter.notifyItemChanged( - position - ) - } + position?.let { traceLocationsAdapter.notifyItemChanged(position) } } }.show() } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/list/TraceLocationsViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/list/TraceLocationsViewModel.kt index 8c57430618bbc290e76ab55e63b36da8264db358..d4167ccd56634c989b0e015f00cffb71b9aeebaa 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/list/TraceLocationsViewModel.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/list/TraceLocationsViewModel.kt @@ -35,14 +35,18 @@ class TraceLocationsViewModel @AssistedInject constructor( traceLocation = traceLocation, onCheckIn = { /* TODO */ }, onDuplicate = { events.postValue(TraceLocationEvent.DuplicateItem(it)) }, - onShowPrint = { /* TODO */ }, - onClearItem = { events.postValue(TraceLocationEvent.ConfirmDeleteItem(it)) }, - onSwipeItem = { traceLocation, position -> + onShowPrint = { events.postValue(TraceLocationEvent.StartQrCodePosterFragment(it)) }, + onDeleteItem = { events.postValue(TraceLocationEvent.ConfirmDeleteItem(it)) }, + onSwipeItem = { location, position -> events.postValue( - TraceLocationEvent.ConfirmSwipeItem(traceLocation, position) + TraceLocationEvent.ConfirmSwipeItem(location, position) + ) + }, + onCardClicked = { traceLocation, position -> + events.postValue( + TraceLocationEvent.StartQrCodeDetailFragment(traceLocation.id, position) ) }, - onCardClicked = { events.postValue(TraceLocationEvent.StartQrCodeDetailFragment(it.id)) }, ) } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/list/items/TraceLocationVH.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/list/items/TraceLocationVH.kt index 8cc28cf70dd8585d2c5f46e3e5df50f371388a26..da8cca4747fd735671ea02fdfed288116addb87a 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/list/items/TraceLocationVH.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/list/items/TraceLocationVH.kt @@ -57,14 +57,13 @@ class TraceLocationVH(parent: ViewGroup) : when (it.itemId) { R.id.menu_duplicate -> item.onDuplicate(item.traceLocation).let { true } R.id.menu_show_print -> item.onShowPrint(item.traceLocation).let { true } - R.id.menu_clear -> item.onClearItem(item.traceLocation).let { true } + R.id.menu_clear -> item.onDeleteItem(item.traceLocation).let { true } else -> false } } checkinAction.setOnClickListener { item.onCheckIn(item.traceLocation) } - - itemView.setOnClickListener { item.onCardClicked(item.traceLocation) } + itemView.setOnClickListener { item.onCardClicked(item.traceLocation, adapterPosition) } } data class Item( @@ -72,9 +71,9 @@ class TraceLocationVH(parent: ViewGroup) : val onCheckIn: (TraceLocation) -> Unit, val onDuplicate: (TraceLocation) -> Unit, val onShowPrint: (TraceLocation) -> Unit, - val onClearItem: (TraceLocation) -> Unit, + val onDeleteItem: (TraceLocation) -> Unit, val onSwipeItem: (TraceLocation, Int) -> Unit, - val onCardClicked: (TraceLocation) -> Unit + val onCardClicked: (TraceLocation, Int) -> Unit ) : TraceLocationItem, SwipeConsumer { override val stableId: Long = traceLocation.id.hashCode().toLong() override fun onSwipe(position: Int, direction: Int) = onSwipeItem(traceLocation, position) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/poster/QrCodePosterFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/poster/QrCodePosterFragment.kt new file mode 100644 index 0000000000000000000000000000000000000000..b5840444875465bb97ae4112c546b037c8686b89 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/poster/QrCodePosterFragment.kt @@ -0,0 +1,156 @@ +package de.rki.coronawarnapp.ui.eventregistration.organizer.poster + +import android.os.Bundle +import android.print.PrintAttributes +import android.print.PrintManager +import android.util.TypedValue +import android.view.View +import android.view.ViewTreeObserver.OnGlobalLayoutListener +import android.widget.Toast +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.core.content.getSystemService +import androidx.core.widget.TextViewCompat +import androidx.fragment.app.Fragment +import androidx.navigation.fragment.navArgs +import de.rki.coronawarnapp.R +import de.rki.coronawarnapp.databinding.QrCodePosterFragmentBinding +import de.rki.coronawarnapp.exception.ExceptionCategory +import de.rki.coronawarnapp.exception.reporting.report +import de.rki.coronawarnapp.server.protocols.internal.pt.QrCodePosterTemplate.QRCodePosterTemplateAndroid.QRCodeTextBoxAndroid +import de.rki.coronawarnapp.ui.color.parseColor +import de.rki.coronawarnapp.ui.print.PrintingAdapter +import de.rki.coronawarnapp.util.di.AutoInject +import de.rki.coronawarnapp.util.files.FileSharing +import de.rki.coronawarnapp.util.ui.popBackStack +import de.rki.coronawarnapp.util.ui.viewBindingLazy +import de.rki.coronawarnapp.util.viewmodel.CWAViewModelFactoryProvider +import de.rki.coronawarnapp.util.viewmodel.cwaViewModelsAssisted +import timber.log.Timber +import java.io.File +import javax.inject.Inject + +class QrCodePosterFragment : Fragment(R.layout.qr_code_poster_fragment), AutoInject { + + @Inject lateinit var viewModelFactory: CWAViewModelFactoryProvider.Factory + + private val args by navArgs<QrCodePosterFragmentArgs>() + private val viewModel: QrCodePosterViewModel by cwaViewModelsAssisted( + factoryProducer = { viewModelFactory }, + constructorCall = { factory, _ -> + factory as QrCodePosterViewModel.Factory + factory.create(args.traceLocationId) + } + ) + + private val binding: QrCodePosterFragmentBinding by viewBindingLazy() + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + with(binding) { + toolbar.setNavigationOnClickListener { popBackStack() } + viewModel.poster.observe(viewLifecycleOwner) { poster -> + bindPoster(poster) + // Avoid creating blank PDF + if (poster.hasImages()) onPosterDrawn() + } + } + + viewModel.sharingIntent.observe(viewLifecycleOwner) { + onShareIntent(it) + } + } + + private fun QrCodePosterFragmentBinding.bindPoster(poster: Poster) { + Timber.d("poster=$poster") + progressBar.hide() + + val template = poster.template ?: return // Exit early + Timber.d("template=$template") + + // Adjust poster image dimensions ratio to have a proper printing preview + val posterLayoutParam = posterImage.layoutParams as ConstraintLayout.LayoutParams + val dimensionRatio = template.run { "$width:$height" } // W:H + Timber.d("dimensionRatio=$dimensionRatio") + posterLayoutParam.dimensionRatio = dimensionRatio + + // Display images + qrCodeImage.setImageBitmap(poster.qrCode) + posterImage.setImageBitmap(template.bitmap) + + // Position QR Code image based on data provided by server + topGuideline.setGuidelinePercent(template.offsetY) + startGuideline.setGuidelinePercent(template.offsetX) + endGuideline.setGuidelinePercent(1 - template.offsetX) + + // Bind text info + bindTextBox(poster.infoText, poster.template.textBox) + } + + private fun onPosterDrawn() = with(binding.qrCodePoster) { + viewTreeObserver.addOnGlobalLayoutListener( + object : OnGlobalLayoutListener { + override fun onGlobalLayout() { + viewModel.createPDF(binding.qrCodePoster) + viewTreeObserver.removeOnGlobalLayoutListener(this) + } + } + ) + } + + private fun QrCodePosterFragmentBinding.bindTextBox( + infoText: String, + textBox: QRCodeTextBoxAndroid + ) = with(infoTextView) { + text = infoText + val minFontSize = textBox.fontSize - 6 + val maxFontSize = textBox.fontSize + TextViewCompat.setAutoSizeTextTypeUniformWithConfiguration( + infoTextView, + minFontSize, + maxFontSize, + 1, + TypedValue.COMPLEX_UNIT_SP + ) + setTextSize(TypedValue.COMPLEX_UNIT_SP, maxFontSize.toFloat()) + setTextColor(textBox.fontColor.parseColor()) + textEndGuideline.setGuidelinePercent(1 - textBox.offsetX) + textStartGuideline.setGuidelinePercent(textBox.offsetX) + textTopGuideline.setGuidelinePercent(textBox.offsetY) + // TODO setTypeface() + } + + private fun onShareIntent(fileIntent: FileSharing.FileIntentProvider) { + binding.toolbar.setOnMenuItemClickListener { + when (it.itemId) { + R.id.action_print -> printFile(fileIntent.file).run { true } + R.id.action_share -> startActivity(fileIntent.intent(requireActivity())).run { true } + else -> false + } + } + } + + private fun printFile(file: File) { + val printingManger = context?.getSystemService<PrintManager>() + Timber.i("PrintingManager=$printingManger") + if (printingManger == null) { + Toast.makeText(requireContext(), R.string.errors_generic_headline, Toast.LENGTH_LONG).show() + return + } + + try { + val job = printingManger.print( + getString(R.string.app_name), + PrintingAdapter(file), + PrintAttributes.Builder() + .setMediaSize(PrintAttributes.MediaSize.ISO_A3) + .build() + ) + + Timber.d("JobState=%s", job.info.state) + } catch (e: Exception) { + Timber.d(e, "Printing job failed") + e.report(ExceptionCategory.INTERNAL) + } + } +} diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/qrcode/QrCodeCreationTestFragmentModule.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/poster/QrCodePosterFragmentModule.kt similarity index 56% rename from Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/qrcode/QrCodeCreationTestFragmentModule.kt rename to Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/poster/QrCodePosterFragmentModule.kt index 33fa4c48d9b99cc6df1ef0c3801310a0dccda239..a64e78afba4fc2e0735838b97974b6c494306f97 100644 --- a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/qrcode/QrCodeCreationTestFragmentModule.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/poster/QrCodePosterFragmentModule.kt @@ -1,4 +1,4 @@ -package de.rki.coronawarnapp.test.eventregistration.ui.qrcode +package de.rki.coronawarnapp.ui.eventregistration.organizer.poster import dagger.Binds import dagger.Module @@ -8,11 +8,11 @@ import de.rki.coronawarnapp.util.viewmodel.CWAViewModelFactory import de.rki.coronawarnapp.util.viewmodel.CWAViewModelKey @Module -abstract class QrCodeCreationTestFragmentModule { +abstract class QrCodePosterFragmentModule { @Binds @IntoMap - @CWAViewModelKey(QrCodeCreationTestViewModel::class) - abstract fun qrCodeCreation( - factory: QrCodeCreationTestViewModel.Factory + @CWAViewModelKey(QrCodePosterViewModel::class) + abstract fun qrCodePosterFragment( + factory: QrCodePosterViewModel.Factory ): CWAViewModelFactory<out CWAViewModel> } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/poster/QrCodePosterViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/poster/QrCodePosterViewModel.kt new file mode 100644 index 0000000000000000000000000000000000000000..3a5a36a36e0e9ad166cd5b192aecaf9117572998 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/poster/QrCodePosterViewModel.kt @@ -0,0 +1,119 @@ +package de.rki.coronawarnapp.ui.eventregistration.organizer.poster + +import android.graphics.Bitmap +import android.graphics.pdf.PdfDocument +import android.view.View +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import de.rki.coronawarnapp.eventregistration.checkins.qrcode.PosterTemplateProvider +import de.rki.coronawarnapp.eventregistration.checkins.qrcode.QrCodeGenerator +import de.rki.coronawarnapp.eventregistration.checkins.qrcode.Template +import de.rki.coronawarnapp.eventregistration.storage.repo.TraceLocationRepository +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.files.FileSharing +import de.rki.coronawarnapp.util.ui.SingleLiveEvent +import de.rki.coronawarnapp.util.viewmodel.CWAViewModel +import de.rki.coronawarnapp.util.viewmodel.CWAViewModelFactory +import timber.log.Timber +import java.io.File +import java.io.FileOutputStream +import java.lang.ref.WeakReference + +class QrCodePosterViewModel @AssistedInject constructor( + @Assisted private val traceLocationId: Long, + private val dispatcher: DispatcherProvider, + private val qrCodeGenerator: QrCodeGenerator, + private val posterTemplateProvider: PosterTemplateProvider, + private val traceLocationRepository: TraceLocationRepository, + private val fileSharing: FileSharing +) : CWAViewModel(dispatcher) { + + private val posterLiveData = MutableLiveData<Poster>() + val poster: LiveData<Poster> = posterLiveData + val sharingIntent = SingleLiveEvent<FileSharing.FileIntentProvider>() + + init { + generatePoster() + } + + /** + * Create a new PDF file and result is delivered by [sharingIntent] + * as a sharing [FileSharing.ShareIntentProvider] + */ + @Suppress("BlockingMethodInNonBlockingContext") + fun createPDF(view: View) = launch(context = dispatcher.IO) { + try { + val weakViewRef = WeakReference(view) // Accessing view in background thread + val directory = File(view.context.cacheDir, "poster").apply { if (!exists()) mkdirs() } + val file = File(directory, "cwa-qr-code.pdf") + + val weakView = weakViewRef.get() ?: return@launch // View is not existing anymore + val pageInfo = PdfDocument.PageInfo.Builder(weakView.width, weakView.height, 1).create() + + PdfDocument().apply { + startPage(pageInfo).apply { + weakView.draw(canvas) + finishPage(this) + } + + FileOutputStream(file).use { + writeTo(it) + close() + } + } + + sharingIntent.postValue(fileSharing.getFileIntentProvider(file, traceLocation().description)) + } catch (e: Exception) { + Timber.d(e, "Creating pdf failed") + e.report(ExceptionCategory.INTERNAL) + } + } + + private fun generatePoster() = launch(context = dispatcher.IO) { + try { + val traceLocation = traceLocation() + val template = posterTemplateProvider.template() + Timber.d("template=$template") + val qrCode = qrCodeGenerator.createQrCode( + input = traceLocation.locationUrl, + length = template.qrCodeLength, + margin = 0 + ) + + val textInfo = buildString { + append(traceLocation.description) + appendLine() + append(traceLocation.address) + } + posterLiveData.postValue( + Poster(qrCode, template, textInfo) + ) + } catch (e: Exception) { + Timber.d(e, "Generating poster failed") + posterLiveData.postValue(Poster()) + e.report(ExceptionCategory.INTERNAL) + } + } + + private suspend fun traceLocation() = traceLocationRepository.traceLocationForId(traceLocationId) + + @AssistedFactory + interface Factory : CWAViewModelFactory<QrCodePosterViewModel> { + fun create( + traceLocationId: Long + ): QrCodePosterViewModel + } +} + +data class Poster( + val qrCode: Bitmap? = null, + val template: Template? = null, + val infoText: String = "" +) { + fun hasImages(): Boolean = qrCode != null && template?.bitmap != null +} 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 8722443459668785f042e1fa85b15038a8aae2b2..798abdfcc35bce36c4b9e698e84572ffdb6191d3 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 @@ -80,6 +80,7 @@ class MainActivity : AppCompatActivity(), HasAndroidInjector { @Inject lateinit var contactDiaryWorkScheduler: ContactDiaryWorkScheduler @Inject lateinit var dataDonationAnalyticsScheduler: DataDonationAnalyticsScheduler @Inject lateinit var submissionSettings: SubmissionSettings + @Inject lateinit var backgroundWorkScheduler: BackgroundWorkScheduler override fun onCreate(savedInstanceState: Bundle?) { AppInjector.setup(this) @@ -117,6 +118,7 @@ class MainActivity : AppCompatActivity(), HasAndroidInjector { if (count > 0) { val badge = getOrCreateBadge(targetId) badge.number = count + badge.badgeTextColor = getColor(android.R.color.white) } else { removeBadge(targetId) } @@ -189,7 +191,7 @@ class MainActivity : AppCompatActivity(), HasAndroidInjector { */ override fun onResume() { super.onResume() - scheduleWork() + backgroundWorkScheduler.startWorkScheduler() vm.doBackgroundNoiseCheck() contactDiaryWorkScheduler.schedulePeriodic() dataDonationAnalyticsScheduler.schedulePeriodic() @@ -270,9 +272,4 @@ class MainActivity : AppCompatActivity(), HasAndroidInjector { fun goBack() { onBackPressed() } - - /** - * Scheduling for a download of keys every hour. - */ - private fun scheduleWork() = BackgroundWorkScheduler.startWorkScheduler() } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/home/HomeFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/home/HomeFragment.kt index a51ff449508c687a9819f04bab261ed78f4082e9..f2596fefb4c970957ce572717bbec87fc3df2cb2 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/home/HomeFragment.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/home/HomeFragment.kt @@ -35,7 +35,7 @@ import javax.inject.Inject class HomeFragment : Fragment(R.layout.home_fragment_layout), AutoInject { @Inject lateinit var viewModelFactory: CWAViewModelFactoryProvider.Factory - private val vm: HomeFragmentViewModel by cwaViewModels( + private val viewModel: HomeFragmentViewModel by cwaViewModels( ownerProducer = { requireActivity().viewModelStore }, factoryProducer = { viewModelFactory } ) @@ -53,7 +53,7 @@ class HomeFragment : Fragment(R.layout.home_fragment_layout), AutoInject { homeMenu.setupMenu(binding.toolbar) - vm.tracingHeaderState.observe2(this) { + viewModel.tracingHeaderState.observe2(this) { binding.tracingHeader = it } @@ -64,11 +64,11 @@ class HomeFragment : Fragment(R.layout.home_fragment_layout), AutoInject { adapter = homeAdapter } - vm.homeItems.observe2(this) { + viewModel.homeItems.observe2(this) { homeAdapter.update(it) } - vm.routeToScreen.observe2(this) { + viewModel.routeToScreen.observe2(this) { doNavigate(it) } @@ -76,28 +76,25 @@ class HomeFragment : Fragment(R.layout.home_fragment_layout), AutoInject { doNavigate(HomeFragmentDirections.actionMainFragmentToSettingsTracingFragment()) } - vm.openFAQUrlEvent.observe2(this) { + viewModel.openFAQUrlEvent.observe2(this) { ExternalActionHelper.openUrl(this@HomeFragment, getString(R.string.main_about_link)) } - vm.openTraceLocationOrganizerFlow.observe2(this) { - vm.wasQRInfoWasAcknowledged() - val nestedGraph = findNavController().graph.findNode(R.id.trace_location_organizer_nav_graph) as NavGraph - - if (vm.wasQRInfoWasAcknowledged()) { - nestedGraph.startDestination = R.id.traceLocationOrganizerListFragment - } else { - nestedGraph.startDestination = R.id.traceLocationOrganizerQRInfoFragment + viewModel.openTraceLocationOrganizerFlow.observe2(this) { + if (viewModel.wasQRInfoWasAcknowledged()) { + val nestedGraph = + findNavController().graph.findNode(R.id.trace_location_organizer_nav_graph) as NavGraph + nestedGraph.startDestination = R.id.traceLocationsFragment } doNavigate(HomeFragmentDirections.actionMainFragmentToTraceLocationOrganizerNavGraph()) } - vm.popupEvents.observe2(this) { event -> + viewModel.popupEvents.observe2(this) { event -> when (event) { HomeFragmentEvents.ShowErrorResetDialog -> { RecoveryByResetDialogFactory(this).showDialog( detailsLink = R.string.errors_generic_text_catastrophic_error_encryption_failure, - onPositive = { vm.errorResetDialogDismissed() } + onPositive = { viewModel.errorResetDialogDismissed() } ) } HomeFragmentEvents.ShowDeleteTestDialog -> showRemoveTestDialog() @@ -109,29 +106,29 @@ class HomeFragment : Fragment(R.layout.home_fragment_layout), AutoInject { } HomeFragmentEvents.ShowTracingExplanation -> { tracingExplanationDialog.show { - vm.tracingExplanationWasShown() + viewModel.tracingExplanationWasShown() } } } } - vm.showPopUps() + viewModel.showPopUps() - vm.showLoweredRiskLevelDialog.observe2(this) { + viewModel.showLoweredRiskLevelDialog.observe2(this) { if (it) showRiskLevelLoweredDialog() } - vm.showIncorrectDeviceTimeDialog.observe2(this) { showDialog -> + viewModel.showIncorrectDeviceTimeDialog.observe2(this) { showDialog -> if (!showDialog) return@observe2 - deviceTimeIncorrectDialog.show { vm.userHasAcknowledgedIncorrectDeviceTime() } + deviceTimeIncorrectDialog.show { viewModel.userHasAcknowledgedIncorrectDeviceTime() } } - vm.observeTestResultToSchedulePositiveTestResultReminder() + viewModel.observeTestResultToSchedulePositiveTestResultReminder() } override fun onResume() { super.onResume() - vm.refreshRequiredData() - vm.restoreAppShortcuts() + viewModel.refreshRequiredData() + viewModel.restoreAppShortcuts() binding.container.sendAccessibilityEvent(AccessibilityEvent.TYPE_ANNOUNCEMENT) } @@ -143,7 +140,7 @@ class HomeFragment : Fragment(R.layout.home_fragment_layout), AutoInject { R.string.submission_test_result_dialog_remove_test_button_positive, R.string.submission_test_result_dialog_remove_test_button_negative, positiveButtonFunction = { - vm.deregisterWarningAccepted() + viewModel.deregisterWarningAccepted() } ) DialogHelper.showDialog(removeTestDialog).apply { @@ -160,7 +157,7 @@ class HomeFragment : Fragment(R.layout.home_fragment_layout), AutoInject { R.string.dialog_reactivate_risk_calculation_button_positive, R.string.dialog_reactivate_risk_calculation_button_negative, positiveButtonFunction = { - vm.reenableRiskCalculation() + viewModel.reenableRiskCalculation() } ) DialogHelper.showDialog(removeTestDialog).apply { @@ -177,7 +174,7 @@ class HomeFragment : Fragment(R.layout.home_fragment_layout), AutoInject { positiveButton = R.string.risk_lowered_dialog_button_confirm, negativeButton = null, cancelable = false, - positiveButtonFunction = { vm.userHasAcknowledgedTheLoweredRiskLevel() } + positiveButtonFunction = { viewModel.userHasAcknowledgedTheLoweredRiskLevel() } ) DialogHelper.showDialog(riskLevelLoweredDialog).apply { diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/PrintingAdapter.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/print/PrintingAdapter.kt similarity index 97% rename from Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/PrintingAdapter.kt rename to Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/print/PrintingAdapter.kt index 2d20c87a1d4b6f1d63abc4e59f62c813740ff26a..622afae9fb3bf8b300896511ebd45baee8c847f1 100644 --- a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/PrintingAdapter.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/print/PrintingAdapter.kt @@ -1,4 +1,4 @@ -package de.rki.coronawarnapp.test.eventregistration.ui +package de.rki.coronawarnapp.ui.print import android.os.Bundle import android.os.CancellationSignal diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/settings/SettingsResetViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/settings/SettingsResetViewModel.kt index e61dfcc8a84099ece7ece5ed6709fa90abd34c4f..14459385664ce333363ac8b4bf4a86769f0f920b 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/settings/SettingsResetViewModel.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/settings/SettingsResetViewModel.kt @@ -19,7 +19,8 @@ class SettingsResetViewModel @AssistedInject constructor( dispatcherProvider: DispatcherProvider, private val dataReset: DataReset, private val shareTestResultNotificationService: ShareTestResultNotificationService, - private val shortcutsHelper: AppShortcutsHelper + private val shortcutsHelper: AppShortcutsHelper, + private val backgroundWorkScheduler: BackgroundWorkScheduler, ) : CWAViewModel(dispatcherProvider = dispatcherProvider) { val clickEvent: SingleLiveEvent<SettingsEvents> = SingleLiveEvent() @@ -39,7 +40,7 @@ class SettingsResetViewModel @AssistedInject constructor( // only stop tracing if it is currently enabled if (isTracingEnabled) { InternalExposureNotificationClient.asyncStop() - BackgroundWorkScheduler.stopWorkScheduler() + backgroundWorkScheduler.stopWorkScheduler() } } catch (apiException: ApiException) { apiException.report( diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/ProtoBuf.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/ProtoBuf.kt new file mode 100644 index 0000000000000000000000000000000000000000..c0a185307df427ba46134ef4d13afadf0c7c3816 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/ProtoBuf.kt @@ -0,0 +1,7 @@ +package de.rki.coronawarnapp.util + +import com.google.protobuf.ByteString +import okio.ByteString.Companion.toByteString + +fun okio.ByteString.toProtoByteString(): ByteString = ByteString.copyFrom(toByteArray()) +fun ByteString.toOkioByteString() = toByteArray().toByteString() diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/TimeAndDateExtensions.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/TimeAndDateExtensions.kt index 30c03c4a5c0a85f8d25f4758df640727cbed5197..f7f9f38cfecdbeb0fe260e791236ceb6d3cb8e9b 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/TimeAndDateExtensions.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/TimeAndDateExtensions.kt @@ -74,6 +74,11 @@ object TimeAndDateExtensions { fun Instant.derive10MinutesInterval(): Long = seconds / TimeUnit.MINUTES.toSeconds(10) // 10 min in seconds + /** + * Derive a UNIX timestamp (in seconds) and returns the corresponding 10-minute interval + */ + fun Instant.deriveHourInterval(): HourInterval = millis / 3600000 + /** * Converts milliseconds to human readable format hh:mm:ss * @@ -110,3 +115,5 @@ object TimeAndDateExtensions { fun Instant.toUserTimeZone() = this.toDateTime(DateTimeZone.forTimeZone(TimeZone.getDefault())) } + +typealias HourInterval = Long diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/WatchdogService.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/WatchdogService.kt index 6ead4ba58ab9d791885e0f583891dcad02328f09..442df0780ea00459160a7c9471bfec09a44293eb 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/WatchdogService.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/WatchdogService.kt @@ -5,11 +5,9 @@ import android.net.wifi.WifiManager import android.os.PowerManager import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.lifecycleScope -import de.rki.coronawarnapp.diagnosiskeys.download.DownloadDiagnosisKeysTask +import de.rki.coronawarnapp.risk.execution.RiskWorkScheduler import de.rki.coronawarnapp.storage.OnboardingSettings import de.rki.coronawarnapp.task.TaskController -import de.rki.coronawarnapp.task.common.DefaultTaskRequest -import de.rki.coronawarnapp.task.submitBlocking import de.rki.coronawarnapp.util.device.BackgroundModeStatus import de.rki.coronawarnapp.util.di.AppContext import de.rki.coronawarnapp.util.di.ProcessLifecycle @@ -28,7 +26,9 @@ class WatchdogService @Inject constructor( private val taskController: TaskController, private val backgroundModeStatus: BackgroundModeStatus, @ProcessLifecycle private val processLifecycleOwner: LifecycleOwner, - private val onboardingSettings: OnboardingSettings + private val onboardingSettings: OnboardingSettings, + private val backgroundWorkScheduler: BackgroundWorkScheduler, + private val riskWorkScheduler: RiskWorkScheduler, ) { private val powerManager by lazy { @@ -55,18 +55,8 @@ class WatchdogService @Inject constructor( Timber.tag(TAG).d("Automatic mode is on, check if we have downloaded keys already today") - val state = taskController.submitBlocking( - DefaultTaskRequest( - DownloadDiagnosisKeysTask::class, - DownloadDiagnosisKeysTask.Arguments(), - originTag = "WatchdogService" - ) - ) - if (state.isFailed) { - Timber.tag(TAG).e(state.error, "RetrieveDiagnosisKeysTransaction failed") - // retry the key retrieval in case of an error with a scheduled work - BackgroundWorkScheduler.scheduleDiagnosisKeyOneTimeWork() - } + val results = riskWorkScheduler.runRiskTasksNow() + Timber.tag(TAG).d("runRiskTasksNow() results: %s", results) if (wifiLock.isHeld) wifiLock.release() if (wakeLock.isHeld) wakeLock.release() @@ -75,7 +65,7 @@ class WatchdogService @Inject constructor( // if the user is onboarded we will schedule period background jobs // in case the app was force stopped and woken up again by the Google WakeUpService if (onboardingSettings.isOnboarded) { - BackgroundWorkScheduler.startWorkScheduler() + backgroundWorkScheduler.startWorkScheduler() } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/coroutine/ListenableFuture.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/coroutine/ListenableFuture.kt new file mode 100644 index 0000000000000000000000000000000000000000..553f18903372bc8c15238c88cb9ca688eaae28c1 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/coroutine/ListenableFuture.kt @@ -0,0 +1,57 @@ +package de.rki.coronawarnapp.util.coroutine + +/* + * Copyright 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import com.google.common.util.concurrent.ListenableFuture +import com.google.common.util.concurrent.MoreExecutors +import kotlinx.coroutines.suspendCancellableCoroutine +import java.util.concurrent.CancellationException +import java.util.concurrent.ExecutionException +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException + +/** + * Awaits for the completion of the [ListenableFuture] without blocking a thread. + * + * @return R The result from the [ListenableFuture] + * + */ +@Suppress("BlockingMethodInNonBlockingContext") +suspend inline fun <R> ListenableFuture<R>.await(): R { + // Fast path + if (isDone) { + try { + return get() + } catch (e: ExecutionException) { + throw e.cause ?: e + } + } + return suspendCancellableCoroutine { cancellableContinuation -> + val action = Runnable { + try { + cancellableContinuation.resume(get()) + } catch (throwable: Throwable) { + val cause = throwable.cause ?: throwable + when (throwable) { + is CancellationException -> cancellableContinuation.cancel(cause) + else -> cancellableContinuation.resumeWithException(cause) + } + } + } + addListener(action, MoreExecutors.directExecutor()) + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/formatter/FormatterStatistics.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/formatter/FormatterStatistics.kt index 8605f6a5a6f8a31b2d8261979b797112c7054eba..377c7491c7a41b32f2d8990c86451b58d844b215 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/formatter/FormatterStatistics.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/formatter/FormatterStatistics.kt @@ -8,31 +8,32 @@ import de.rki.coronawarnapp.statistics.InfectionStats import de.rki.coronawarnapp.statistics.KeySubmissionsStats import de.rki.coronawarnapp.statistics.SevenDayRValue import de.rki.coronawarnapp.statistics.StatsItem +import de.rki.coronawarnapp.util.TimeAndDateExtensions.toUserTimeZone import org.joda.time.LocalDate import org.joda.time.format.DateTimeFormat fun StatsItem.getPrimaryLabel(context: Context): String { val today = LocalDate() val yesterday = today.minusDays(1) - val day = LocalDate(updatedAt) + val updatedAtDate = LocalDate(updatedAt.toUserTimeZone()) val dateTimeFormatter = DateTimeFormat.mediumDate().withLocale(context.getLocale()) return when (this) { is InfectionStats, - is KeySubmissionsStats -> when (day) { + is KeySubmissionsStats -> when (updatedAtDate) { today -> context.getString(R.string.statistics_primary_value_today) yesterday -> context.getString(R.string.statistics_primary_value_yesterday) - else -> dateTimeFormatter.print(day) + else -> dateTimeFormatter.print(updatedAtDate) } - is IncidenceStats -> when (day) { + is IncidenceStats -> when (updatedAtDate) { today -> context.getString(R.string.statistics_primary_value_until_today) yesterday -> context.getString(R.string.statistics_primary_value_until_yesterday) - else -> context.getString(R.string.statistics_primary_value_until, dateTimeFormatter.print(day)) + else -> context.getString(R.string.statistics_primary_value_until, dateTimeFormatter.print(updatedAtDate)) } - is SevenDayRValue -> when (day) { + is SevenDayRValue -> when (updatedAtDate) { today -> context.getString(R.string.statistics_primary_value_current) yesterday -> context.getString(R.string.statistics_primary_value_yesterday) - else -> context.getString(R.string.statistics_primary_value_until, dateTimeFormatter.print(day)) + else -> context.getString(R.string.statistics_primary_value_until, dateTimeFormatter.print(updatedAtDate)) } } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/security/SignatureValidation.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/security/SignatureValidation.kt index a3221d6d27e661752b4161e9d2dc81855f36fb32..3167ba6655cc6c7bc2230250b9f14508b07667b8 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/security/SignatureValidation.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/security/SignatureValidation.kt @@ -26,7 +26,7 @@ class SignatureValidation @Inject constructor( // Public keys within this server environment private val publicKeys by lazy { - environmentSetup.appConfigVerificationKey.split(KEY_DELIMITER) + environmentSetup.appConfigPublicKey.split(KEY_DELIMITER) .mapNotNull { pubKeyBase64 -> Base64.decode(pubKeyBase64, Base64.DEFAULT) } .map { pubKeyBinary -> keyFactory.generatePublic(X509EncodedKeySpec(pubKeyBinary)) } .onEach { Timber.tag(TAG).v("ENV PubKey: %s", it) } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/worker/CWAWorkerFactory.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/worker/CWAWorkerFactory.kt index 2088d2fbed238c49289934158195088b78a2a889..de0c40995b889326071304923fbf63169564967a 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/worker/CWAWorkerFactory.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/worker/CWAWorkerFactory.kt @@ -27,19 +27,29 @@ class CWAWorkerFactory @Inject constructor( ): ListenableWorker? { Timber.v("Checking in known worker factories for %s", workerClassName) val ourWorkerFactories = factories.entries.find { - Class.forName(workerClassName).isAssignableFrom(it.key) + try { + Class.forName(workerClassName).isAssignableFrom(it.key) + } catch (e: ClassNotFoundException) { + Timber.e(e, "Failed to create worker class $workerClassName") + false + } }?.value - return if (ourWorkerFactories != null) { + if (ourWorkerFactories != null) { Timber.v("It's one of ours, creating worker for %s with %s", workerClassName, workerParameters) - ourWorkerFactories.get().create(appContext, workerParameters).also { + return ourWorkerFactories.get().create(appContext, workerParameters).also { Timber.i("Our worker was created: %s", it) } - } else { + } + + return try { Timber.w("Unknown worker class, trying direct instantiation on %s", workerClassName) workerClassName.toNewWorkerInstance(appContext, workerParameters).also { Timber.i("Unknown worker was created: %s", it) } + } catch (e: ClassNotFoundException) { + Timber.w(e, "Failed to create unknown worker class: %s", workerClassName) + null } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/worker/WorkerBinder.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/worker/WorkerBinder.kt index 3cc59eabfb2c1f84bd892cc528138e4fc15149f9..f93c59b45ba5d0cbd6c0501bb84be8e5c6ccb16d 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/worker/WorkerBinder.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/worker/WorkerBinder.kt @@ -8,14 +8,14 @@ import de.rki.coronawarnapp.contactdiary.retention.ContactDiaryRetentionWorker import de.rki.coronawarnapp.datadonation.analytics.worker.DataDonationAnalyticsPeriodicWorker import de.rki.coronawarnapp.deadman.DeadmanNotificationOneTimeWorker import de.rki.coronawarnapp.deadman.DeadmanNotificationPeriodicWorker +import de.rki.coronawarnapp.diagnosiskeys.execution.DiagnosisKeyRetrievalWorker import de.rki.coronawarnapp.eventregistration.storage.retention.TraceLocationDbCleanUpPeriodicWorker import de.rki.coronawarnapp.nearby.ExposureStateUpdateWorker import de.rki.coronawarnapp.presencetracing.checkins.checkout.auto.AutoCheckOutWorker +import de.rki.coronawarnapp.presencetracing.risk.execution.PresenceTracingWarningWorker import de.rki.coronawarnapp.submission.auto.SubmissionWorker import de.rki.coronawarnapp.worker.BackgroundNoiseOneTimeWorker import de.rki.coronawarnapp.worker.BackgroundNoisePeriodicWorker -import de.rki.coronawarnapp.worker.DiagnosisKeyRetrievalOneTimeWorker -import de.rki.coronawarnapp.worker.DiagnosisKeyRetrievalPeriodicWorker import de.rki.coronawarnapp.worker.DiagnosisTestResultRetrievalPeriodicWorker @Module @@ -44,16 +44,9 @@ abstract class WorkerBinder { @Binds @IntoMap - @WorkerKey(DiagnosisKeyRetrievalOneTimeWorker::class) + @WorkerKey(DiagnosisKeyRetrievalWorker::class) abstract fun diagnosisKeyRetrievalOneTime( - factory: DiagnosisKeyRetrievalOneTimeWorker.Factory - ): InjectedWorkerFactory<out ListenableWorker> - - @Binds - @IntoMap - @WorkerKey(DiagnosisKeyRetrievalPeriodicWorker::class) - abstract fun diagnosisKeyRetrievalPeriodic( - factory: DiagnosisKeyRetrievalPeriodicWorker.Factory + factory: DiagnosisKeyRetrievalWorker.Factory ): InjectedWorkerFactory<out ListenableWorker> @Binds @@ -111,4 +104,11 @@ abstract class WorkerBinder { abstract fun traceLocationCleanUpWorker( factory: TraceLocationDbCleanUpPeriodicWorker.Factory ): InjectedWorkerFactory<out ListenableWorker> + + @Binds + @IntoMap + @WorkerKey(PresenceTracingWarningWorker::class) + abstract fun traceWarningWorker( + factory: PresenceTracingWarningWorker.Factory + ): InjectedWorkerFactory<out ListenableWorker> } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/BackgroundConstants.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/BackgroundConstants.kt index 75b991b690e2f25d6c7d474257765028129c0c80..7e7340ebb38c7a76df1ccdbfdc4059cadf3b6057 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/BackgroundConstants.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/BackgroundConstants.kt @@ -9,16 +9,6 @@ import java.util.concurrent.TimeUnit */ object BackgroundConstants { - /** - * Tag for diagnosis key retrieval one time work - */ - const val DIAGNOSIS_KEY_ONE_TIME_WORKER_TAG = "DIAGNOSIS_KEY_ONE_TIME_WORKER" - - /** - * Tag for diagnosis key retrieval periodic work - */ - const val DIAGNOSIS_KEY_PERIODIC_WORKER_TAG = "DIAGNOSIS_KEY_PERIODIC_WORKER" - /** * Tag for background polling tp check test result periodic work */ @@ -34,16 +24,6 @@ object BackgroundConstants { */ const val BACKGROUND_NOISE_ONE_TIME_WORKER_TAG = "BACKGROUND_NOISE_PERIODIC_WORKER" - /** - * Unique name for diagnosis key retrieval one time work - */ - const val DIAGNOSIS_KEY_ONE_TIME_WORK_NAME = "DiagnosisKeyBackgroundOneTimeWork" - - /** - * Unique name for diagnosis key retrieval periodic work - */ - const val DIAGNOSIS_KEY_PERIODIC_WORK_NAME = "DiagnosisKeyBackgroundPeriodicWork" - /** * Unique name for diagnosis test result retrieval periodic work */ diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/BackgroundNoisePeriodicWorker.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/BackgroundNoisePeriodicWorker.kt index 31922abf4120e6341508e056d82b84386aec82ab..2168b02875a0b162f33c781faa418839b1dd000f 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/BackgroundNoisePeriodicWorker.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/BackgroundNoisePeriodicWorker.kt @@ -9,7 +9,6 @@ import dagger.assisted.AssistedInject import de.rki.coronawarnapp.submission.SubmissionSettings import de.rki.coronawarnapp.util.TimeStamper import de.rki.coronawarnapp.util.worker.InjectedWorkerFactory -import de.rki.coronawarnapp.worker.BackgroundWorkScheduler.stop import org.joda.time.Duration import org.joda.time.Instant import timber.log.Timber @@ -23,7 +22,8 @@ class BackgroundNoisePeriodicWorker @AssistedInject constructor( @Assisted val context: Context, @Assisted workerParams: WorkerParameters, private val submissionSettings: SubmissionSettings, - private val timeStamper: TimeStamper + private val timeStamper: TimeStamper, + private val backgroundWorkScheduler: BackgroundWorkScheduler, ) : CoroutineWorker(context, workerParams) { /** @@ -45,7 +45,7 @@ class BackgroundNoisePeriodicWorker @AssistedInject constructor( return result } - BackgroundWorkScheduler.scheduleBackgroundNoiseOneTimeWork() + backgroundWorkScheduler.scheduleBackgroundNoiseOneTimeWork() } catch (e: Exception) { result = if (runAttemptCount > BackgroundConstants.WORKER_RETRY_COUNT_THRESHOLD) { Result.failure() @@ -58,7 +58,7 @@ class BackgroundNoisePeriodicWorker @AssistedInject constructor( } private fun stopWorker() { - BackgroundWorkScheduler.WorkType.BACKGROUND_NOISE_PERIODIC_WORK.stop() + backgroundWorkScheduler.stopBackgroundNoisePeriodicWork() Timber.tag(TAG).d("$id: worker stopped") } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/BackgroundWorkBuilder.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/BackgroundWorkBuilder.kt index b109e8ba261979dc70e6760555a0a1c772bcf301..2c03e9544d7eb89824da44430afdf47ad39f0eb8 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/BackgroundWorkBuilder.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/BackgroundWorkBuilder.kt @@ -5,131 +5,84 @@ import androidx.work.OneTimeWorkRequestBuilder import androidx.work.PeriodicWorkRequestBuilder import de.rki.coronawarnapp.worker.BackgroundWorkScheduler.WorkTag import java.util.concurrent.TimeUnit +import javax.inject.Inject +import javax.inject.Singleton -/** - * Build diagnosis key periodic work request - * Set "kind delay" for accessibility reason. - * Backoff criteria set to Linear type. - * - * The launchInterval is 60 minutes as we want to check every hour, for new hour packages on the CDN. - * - * @see WorkTag.DIAGNOSIS_KEY_RETRIEVAL_PERIODIC_WORKER - * @see BackgroundConstants.KIND_DELAY - * @see BackgroundConstants.BACKOFF_INITIAL_DELAY - * @see BackoffPolicy.LINEAR - */ -fun buildDiagnosisKeyRetrievalPeriodicWork() = - PeriodicWorkRequestBuilder<DiagnosisKeyRetrievalPeriodicWorker>(60, TimeUnit.MINUTES) - .addTag(WorkTag.DIAGNOSIS_KEY_RETRIEVAL_PERIODIC_WORKER.tag) - .setInitialDelay( - BackgroundConstants.KIND_DELAY, - TimeUnit.MINUTES - ) - .setBackoffCriteria( - BackoffPolicy.EXPONENTIAL, - BackgroundConstants.BACKOFF_INITIAL_DELAY, - TimeUnit.MINUTES - ) - .build() +@Singleton +class BackgroundWorkBuilder @Inject constructor() { -/** - * Build diagnosis key one time work request - * Set random initial delay for security reason. - * Backoff criteria set to Linear type. - * - * @return OneTimeWorkRequest - * - * @see WorkTag.DIAGNOSIS_KEY_RETRIEVAL_ONE_TIME_WORKER - * @see buildDiagnosisKeyRetrievalOneTimeWork - * @see BackgroundConstants.BACKOFF_INITIAL_DELAY - * @see BackoffPolicy.LINEAR - */ -fun buildDiagnosisKeyRetrievalOneTimeWork() = - OneTimeWorkRequestBuilder<DiagnosisKeyRetrievalOneTimeWorker>() - .addTag(WorkTag.DIAGNOSIS_KEY_RETRIEVAL_ONE_TIME_WORKER.tag) - .setConstraints(BackgroundWorkHelper.getConstraintsForDiagnosisKeyOneTimeBackgroundWork()) - .setInitialDelay( - BackgroundConstants.KIND_DELAY, - TimeUnit.MINUTES - ) - .setBackoffCriteria( - BackoffPolicy.EXPONENTIAL, - BackgroundConstants.BACKOFF_INITIAL_DELAY, + /** + * Build diagnosis Test Result periodic work request + * Set "kind delay" for accessibility reason. + * + * @return PeriodicWorkRequest + * + * @see WorkTag.DIAGNOSIS_TEST_RESULT_RETRIEVAL_PERIODIC_WORKER + * @see BackgroundConstants.KIND_DELAY + */ + fun buildDiagnosisTestResultRetrievalPeriodicWork() = + PeriodicWorkRequestBuilder<DiagnosisTestResultRetrievalPeriodicWorker>( + BackgroundWorkHelper.getDiagnosisTestResultRetrievalPeriodicWorkTimeInterval(), TimeUnit.MINUTES ) - .build() + .addTag(WorkTag.DIAGNOSIS_TEST_RESULT_RETRIEVAL_PERIODIC_WORKER.tag) + .setConstraints(BackgroundWorkHelper.getConstraintsForDiagnosisKeyOneTimeBackgroundWork()) + .setInitialDelay( + BackgroundConstants.DIAGNOSIS_TEST_RESULT_PERIODIC_INITIAL_DELAY, + TimeUnit.SECONDS + ).setBackoffCriteria( + BackoffPolicy.LINEAR, + BackgroundConstants.KIND_DELAY, + TimeUnit.MINUTES + ) + .build() -/** - * Build diagnosis Test Result periodic work request - * Set "kind delay" for accessibility reason. - * - * @return PeriodicWorkRequest - * - * @see WorkTag.DIAGNOSIS_TEST_RESULT_RETRIEVAL_PERIODIC_WORKER - * @see BackgroundConstants.KIND_DELAY - */ -fun buildDiagnosisTestResultRetrievalPeriodicWork() = - PeriodicWorkRequestBuilder<DiagnosisTestResultRetrievalPeriodicWorker>( - BackgroundWorkHelper.getDiagnosisTestResultRetrievalPeriodicWorkTimeInterval(), - TimeUnit.MINUTES - ) - .addTag(WorkTag.DIAGNOSIS_TEST_RESULT_RETRIEVAL_PERIODIC_WORKER.tag) - .setConstraints(BackgroundWorkHelper.getConstraintsForDiagnosisKeyOneTimeBackgroundWork()) - .setInitialDelay( - BackgroundConstants.DIAGNOSIS_TEST_RESULT_PERIODIC_INITIAL_DELAY, - TimeUnit.SECONDS - ).setBackoffCriteria( - BackoffPolicy.LINEAR, - BackgroundConstants.KIND_DELAY, - TimeUnit.MINUTES - ) - .build() + /** + * Build background noise one time work request + * Set BackgroundNoiseOneTimeWorkDelay for timing randomness. + * + * @return PeriodicWorkRequest + * + * @see WorkTag.BACKGROUND_NOISE_ONE_TIME_WORKER + * @see BackgroundWorkHelper.getBackgroundNoiseOneTimeWorkDelay + */ + fun buildBackgroundNoiseOneTimeWork() = + OneTimeWorkRequestBuilder<BackgroundNoiseOneTimeWorker>() + .addTag(WorkTag.BACKGROUND_NOISE_ONE_TIME_WORKER.tag) + .setConstraints(BackgroundWorkHelper.getConstraintsForDiagnosisKeyOneTimeBackgroundWork()) + .setInitialDelay( + BackgroundWorkHelper.getBackgroundNoiseOneTimeWorkDelay(), + TimeUnit.HOURS + ).setBackoffCriteria( + BackoffPolicy.LINEAR, + BackgroundConstants.KIND_DELAY, + TimeUnit.MINUTES + ) + .build() -/** - * Build background noise one time work request - * Set BackgroundNoiseOneTimeWorkDelay for timing randomness. - * - * @return PeriodicWorkRequest - * - * @see WorkTag.BACKGROUND_NOISE_ONE_TIME_WORKER - * @see BackgroundWorkHelper.getBackgroundNoiseOneTimeWorkDelay - */ -fun buildBackgroundNoiseOneTimeWork() = - OneTimeWorkRequestBuilder<BackgroundNoiseOneTimeWorker>() - .addTag(WorkTag.BACKGROUND_NOISE_ONE_TIME_WORKER.tag) - .setConstraints(BackgroundWorkHelper.getConstraintsForDiagnosisKeyOneTimeBackgroundWork()) - .setInitialDelay( - BackgroundWorkHelper.getBackgroundNoiseOneTimeWorkDelay(), + /** + * Build background noise periodic work request + * Set "kind delay" for accessibility reason. + * + * @return PeriodicWorkRequest + * + * @see BackgroundConstants.MIN_HOURS_TO_NEXT_BACKGROUND_NOISE_EXECUTION + * @see WorkTag.BACKGROUND_NOISE_PERIODIC_WORKER + * @see BackgroundConstants.KIND_DELAY + */ + fun buildBackgroundNoisePeriodicWork() = + PeriodicWorkRequestBuilder<BackgroundNoisePeriodicWorker>( + BackgroundConstants.MIN_HOURS_TO_NEXT_BACKGROUND_NOISE_EXECUTION, TimeUnit.HOURS - ).setBackoffCriteria( - BackoffPolicy.LINEAR, - BackgroundConstants.KIND_DELAY, - TimeUnit.MINUTES - ) - .build() - -/** - * Build background noise periodic work request - * Set "kind delay" for accessibility reason. - * - * @return PeriodicWorkRequest - * - * @see BackgroundConstants.MIN_HOURS_TO_NEXT_BACKGROUND_NOISE_EXECUTION - * @see WorkTag.BACKGROUND_NOISE_PERIODIC_WORKER - * @see BackgroundConstants.KIND_DELAY - */ -fun buildBackgroundNoisePeriodicWork() = - PeriodicWorkRequestBuilder<BackgroundNoisePeriodicWorker>( - BackgroundConstants.MIN_HOURS_TO_NEXT_BACKGROUND_NOISE_EXECUTION, - TimeUnit.HOURS - ) - .addTag(WorkTag.BACKGROUND_NOISE_PERIODIC_WORKER.tag) - .setInitialDelay( - BackgroundConstants.KIND_DELAY, - TimeUnit.SECONDS - ).setBackoffCriteria( - BackoffPolicy.LINEAR, - BackgroundConstants.KIND_DELAY, - TimeUnit.MINUTES ) - .build() + .addTag(WorkTag.BACKGROUND_NOISE_PERIODIC_WORKER.tag) + .setInitialDelay( + BackgroundConstants.KIND_DELAY, + TimeUnit.SECONDS + ).setBackoffCriteria( + BackoffPolicy.LINEAR, + BackgroundConstants.KIND_DELAY, + TimeUnit.MINUTES + ) + .build() +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/BackgroundWorkScheduler.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/BackgroundWorkScheduler.kt index aaf6317d9be63bf12856a97c46cc9665512e65b7..4f83979c39c974861e80c04df100c1439be3b50e 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/BackgroundWorkScheduler.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/BackgroundWorkScheduler.kt @@ -6,9 +6,13 @@ import androidx.work.Operation import androidx.work.WorkInfo import androidx.work.WorkManager import de.rki.coronawarnapp.CoronaWarnApplication -import de.rki.coronawarnapp.util.di.ApplicationComponent +import de.rki.coronawarnapp.risk.execution.RiskWorkScheduler +import de.rki.coronawarnapp.storage.TracingSettings +import de.rki.coronawarnapp.submission.SubmissionSettings import timber.log.Timber import java.util.concurrent.ExecutionException +import javax.inject.Inject +import javax.inject.Singleton /** * Singleton class for background work handling @@ -17,26 +21,24 @@ import java.util.concurrent.ExecutionException * @see BackgroundConstants * @see BackgroundWorkHelper */ -object BackgroundWorkScheduler : BackgroundWorkSchedulerBase() { - - fun init(component: ApplicationComponent) { - component.inject(this) - } +@Singleton +class BackgroundWorkScheduler @Inject constructor( + private val backgroundWorkBuilder: BackgroundWorkBuilder, + private val submissionSettings: SubmissionSettings, + private val tracingSettings: TracingSettings, + private val riskWorkScheduler: RiskWorkScheduler +) { /** * Enum class for work tags * * @param tag the tag of the worker * - * @see BackgroundConstants.DIAGNOSIS_KEY_ONE_TIME_WORKER_TAG - * @see BackgroundConstants.DIAGNOSIS_KEY_PERIODIC_WORKER_TAG * @see BackgroundConstants.DIAGNOSIS_TEST_RESULT_PERIODIC_WORKER_TAG * @see BackgroundConstants.BACKGROUND_NOISE_ONE_TIME_WORKER_TAG * @see BackgroundConstants.BACKGROUND_NOISE_PERIODIC_WORKER_TAG */ enum class WorkTag(val tag: String) { - DIAGNOSIS_KEY_RETRIEVAL_ONE_TIME_WORKER(BackgroundConstants.DIAGNOSIS_KEY_ONE_TIME_WORKER_TAG), - DIAGNOSIS_KEY_RETRIEVAL_PERIODIC_WORKER(BackgroundConstants.DIAGNOSIS_KEY_PERIODIC_WORKER_TAG), DIAGNOSIS_TEST_RESULT_RETRIEVAL_PERIODIC_WORKER(BackgroundConstants.DIAGNOSIS_TEST_RESULT_PERIODIC_WORKER_TAG), BACKGROUND_NOISE_ONE_TIME_WORKER(BackgroundConstants.BACKGROUND_NOISE_ONE_TIME_WORKER_TAG), BACKGROUND_NOISE_PERIODIC_WORKER(BackgroundConstants.BACKGROUND_NOISE_PERIODIC_WORKER_TAG) @@ -47,15 +49,11 @@ object BackgroundWorkScheduler : BackgroundWorkSchedulerBase() { * * @param uniqueName the unique name of specified work * - * @see BackgroundConstants.DIAGNOSIS_KEY_ONE_TIME_WORK_NAME - * @see BackgroundConstants.DIAGNOSIS_KEY_PERIODIC_WORK_NAME * @see BackgroundConstants.DIAGNOSIS_TEST_RESULT_PERIODIC_WORK_NAME * @see BackgroundConstants.BACKGROUND_NOISE_PERIODIC_WORK_NAME * @see BackgroundConstants.BACKGROUND_NOISE_ONE_TIME_WORK_NAME */ enum class WorkType(val uniqueName: String) { - DIAGNOSIS_KEY_BACKGROUND_ONE_TIME_WORK(BackgroundConstants.DIAGNOSIS_KEY_ONE_TIME_WORK_NAME), - DIAGNOSIS_KEY_BACKGROUND_PERIODIC_WORK(BackgroundConstants.DIAGNOSIS_KEY_PERIODIC_WORK_NAME), DIAGNOSIS_TEST_RESULT_PERIODIC_WORKER(BackgroundConstants.DIAGNOSIS_TEST_RESULT_PERIODIC_WORK_NAME), BACKGROUND_NOISE_PERIODIC_WORK(BackgroundConstants.BACKGROUND_NOISE_PERIODIC_WORK_NAME), BACKGROUND_NOISE_ONE_TIME_WORK(BackgroundConstants.BACKGROUND_NOISE_ONE_TIME_WORK_NAME) @@ -75,17 +73,9 @@ object BackgroundWorkScheduler : BackgroundWorkSchedulerBase() { * @see isWorkActive */ fun startWorkScheduler() { - val notificationBody = StringBuilder() - notificationBody.append("Jobs starting: ") - val isPeriodicWorkActive = isWorkActive(WorkTag.DIAGNOSIS_KEY_RETRIEVAL_PERIODIC_WORKER.tag) - logWorkActiveStatus( - WorkTag.DIAGNOSIS_KEY_RETRIEVAL_PERIODIC_WORKER.tag, - isPeriodicWorkActive - ) - if (!isPeriodicWorkActive) { - WorkType.DIAGNOSIS_KEY_BACKGROUND_PERIODIC_WORK.start() - notificationBody.append("[DIAGNOSIS_KEY_BACKGROUND_PERIODIC_WORK] ") - } + Timber.d("startWorkScheduler()") + riskWorkScheduler.setPeriodicRiskCalculation(enabled = true) + if (!submissionSettings.isSubmissionSuccessful) { if (!isWorkActive(WorkTag.DIAGNOSIS_TEST_RESULT_RETRIEVAL_PERIODIC_WORKER.tag) && submissionSettings.registrationToken.value != null && @@ -93,10 +83,22 @@ object BackgroundWorkScheduler : BackgroundWorkSchedulerBase() { ) { WorkType.DIAGNOSIS_TEST_RESULT_PERIODIC_WORKER.start() tracingSettings.initialPollingForTestResultTimeStamp = System.currentTimeMillis() - notificationBody.append("[DIAGNOSIS_TEST_RESULT_PERIODIC_WORKER]") + Timber.d("Starting DIAGNOSIS_TEST_RESULT_PERIODIC_WORKER") } } - Timber.d("Background Job Starting: %s", notificationBody) + } + + /** + * Stop work scheduler + * Stops all background work by tag. + */ + fun stopWorkScheduler() { + WorkTag.values().map { workTag: WorkTag -> + workManager.cancelAllWorkByTag(workTag.tag) + .also { it.logOperationCancelByTag(workTag) } + } + riskWorkScheduler.setPeriodicRiskCalculation(enabled = false) + Timber.d("All Background Jobs Stopped") } /** @@ -139,34 +141,12 @@ object BackgroundWorkScheduler : BackgroundWorkSchedulerBase() { return result } - /** - * Stop work scheduler - * Stops all background work by tag. - */ - fun stopWorkScheduler() { - WorkTag.values().map { workTag: WorkTag -> - workManager.cancelAllWorkByTag(workTag.tag) - .also { it.logOperationCancelByTag(workTag) } - } - Timber.d("All Background Jobs Stopped") + fun scheduleDiagnosisTestResultPeriodicWork() { + WorkType.DIAGNOSIS_TEST_RESULT_PERIODIC_WORKER.start() } - /** - * Schedule diagnosis key periodic time work - * - * @see WorkType.DIAGNOSIS_KEY_BACKGROUND_PERIODIC_WORK - */ - fun scheduleDiagnosisKeyPeriodicWork() { - WorkType.DIAGNOSIS_KEY_BACKGROUND_PERIODIC_WORK.start() - } - - /** - * Schedule diagnosis key one time work - * - * @see WorkType.DIAGNOSIS_KEY_BACKGROUND_ONE_TIME_WORK - */ - fun scheduleDiagnosisKeyOneTimeWork() { - WorkType.DIAGNOSIS_KEY_BACKGROUND_ONE_TIME_WORK.start() + fun stopDiagnosisTestResultPeriodicWork() { + WorkType.DIAGNOSIS_TEST_RESULT_PERIODIC_WORKER.stop() } /** @@ -178,6 +158,10 @@ object BackgroundWorkScheduler : BackgroundWorkSchedulerBase() { WorkType.BACKGROUND_NOISE_PERIODIC_WORK.start() } + fun stopBackgroundNoisePeriodicWork() { + WorkType.BACKGROUND_NOISE_PERIODIC_WORK.start() + } + /** * Schedule background noise one time work * @@ -195,41 +179,11 @@ object BackgroundWorkScheduler : BackgroundWorkSchedulerBase() { * @see WorkType */ private fun WorkType.start(): Operation = when (this) { - WorkType.DIAGNOSIS_KEY_BACKGROUND_PERIODIC_WORK -> enqueueDiagnosisKeyBackgroundPeriodicWork() - WorkType.DIAGNOSIS_KEY_BACKGROUND_ONE_TIME_WORK -> enqueueDiagnosisKeyBackgroundOneTimeWork() WorkType.DIAGNOSIS_TEST_RESULT_PERIODIC_WORKER -> enqueueDiagnosisTestResultBackgroundPeriodicWork() WorkType.BACKGROUND_NOISE_PERIODIC_WORK -> enqueueBackgroundNoisePeriodicWork() WorkType.BACKGROUND_NOISE_ONE_TIME_WORK -> enqueueBackgroundNoiseOneTimeWork() } - /** - * Enqueue diagnosis key periodic work and log it - * Replace with new if older work exists. - * - * @return Operation - * - * @see WorkType.DIAGNOSIS_KEY_BACKGROUND_PERIODIC_WORK - */ - private fun enqueueDiagnosisKeyBackgroundPeriodicWork() = workManager.enqueueUniquePeriodicWork( - WorkType.DIAGNOSIS_KEY_BACKGROUND_PERIODIC_WORK.uniqueName, - ExistingPeriodicWorkPolicy.REPLACE, - buildDiagnosisKeyRetrievalPeriodicWork() - ).also { it.logOperationSchedule(WorkType.DIAGNOSIS_KEY_BACKGROUND_PERIODIC_WORK) } - - /** - * Enqueue diagnosis key one time work and log it - * Replace with new if older work exists. - * - * @return Operation - * - * @see WorkType.DIAGNOSIS_KEY_BACKGROUND_PERIODIC_WORK - */ - private fun enqueueDiagnosisKeyBackgroundOneTimeWork() = workManager.enqueueUniqueWork( - WorkType.DIAGNOSIS_KEY_BACKGROUND_ONE_TIME_WORK.uniqueName, - ExistingWorkPolicy.REPLACE, - buildDiagnosisKeyRetrievalOneTimeWork() - ).also { it.logOperationSchedule(WorkType.DIAGNOSIS_KEY_BACKGROUND_ONE_TIME_WORK) } - /** * Enqueue diagnosis Test Result periodic * Show a Notification when new Test Results are in. @@ -243,7 +197,7 @@ object BackgroundWorkScheduler : BackgroundWorkSchedulerBase() { workManager.enqueueUniquePeriodicWork( WorkType.DIAGNOSIS_TEST_RESULT_PERIODIC_WORKER.uniqueName, ExistingPeriodicWorkPolicy.REPLACE, - buildDiagnosisTestResultRetrievalPeriodicWork() + backgroundWorkBuilder.buildDiagnosisTestResultRetrievalPeriodicWork() ).also { it.logOperationSchedule(WorkType.DIAGNOSIS_TEST_RESULT_PERIODIC_WORKER) } /** @@ -258,7 +212,7 @@ object BackgroundWorkScheduler : BackgroundWorkSchedulerBase() { workManager.enqueueUniquePeriodicWork( WorkType.BACKGROUND_NOISE_PERIODIC_WORK.uniqueName, ExistingPeriodicWorkPolicy.REPLACE, - buildBackgroundNoisePeriodicWork() + backgroundWorkBuilder.buildBackgroundNoisePeriodicWork() ).also { it.logOperationSchedule(WorkType.BACKGROUND_NOISE_PERIODIC_WORK) } /** @@ -273,7 +227,7 @@ object BackgroundWorkScheduler : BackgroundWorkSchedulerBase() { workManager.enqueueUniqueWork( WorkType.BACKGROUND_NOISE_ONE_TIME_WORK.uniqueName, ExistingWorkPolicy.REPLACE, - buildBackgroundNoiseOneTimeWork() + backgroundWorkBuilder.buildBackgroundNoiseOneTimeWork() ).also { it.logOperationSchedule(WorkType.BACKGROUND_NOISE_ONE_TIME_WORK) } /** @@ -293,11 +247,4 @@ object BackgroundWorkScheduler : BackgroundWorkSchedulerBase() { { Timber.d("All work with tag ${workTag.tag} canceled.") }, { it.run() } ).also { Timber.d("Canceling all work with tag ${workTag.tag}") } - - /** - * Log work active status - */ - private fun logWorkActiveStatus(tag: String, active: Boolean) { - Timber.d("Work type $tag is active: $active") - } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/BackgroundWorkSchedulerBase.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/BackgroundWorkSchedulerBase.kt deleted file mode 100644 index 9085a1645570aa546c4f8294904096753b46b17c..0000000000000000000000000000000000000000 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/BackgroundWorkSchedulerBase.kt +++ /dev/null @@ -1,11 +0,0 @@ -package de.rki.coronawarnapp.worker - -import de.rki.coronawarnapp.storage.TracingSettings -import de.rki.coronawarnapp.submission.SubmissionSettings -import javax.inject.Inject - -@Suppress("UnnecessaryAbstractClass") -abstract class BackgroundWorkSchedulerBase { - @Inject internal lateinit var submissionSettings: SubmissionSettings - @Inject internal lateinit var tracingSettings: TracingSettings -} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/DiagnosisKeyRetrievalPeriodicWorker.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/DiagnosisKeyRetrievalPeriodicWorker.kt deleted file mode 100644 index 5a59a5b2f2d0952471b30f81ad39e3ade4ef2b1d..0000000000000000000000000000000000000000 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/DiagnosisKeyRetrievalPeriodicWorker.kt +++ /dev/null @@ -1,60 +0,0 @@ -package de.rki.coronawarnapp.worker - -import android.content.Context -import androidx.work.CoroutineWorker -import androidx.work.WorkerParameters -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject -import de.rki.coronawarnapp.util.worker.InjectedWorkerFactory -import timber.log.Timber - -/** - * Periodic diagnosis key retrieval work - * Executes the scheduling of one time diagnosis key retrieval work - * - * @see BackgroundWorkScheduler - * @see DiagnosisKeyRetrievalOneTimeWorker - */ -class DiagnosisKeyRetrievalPeriodicWorker @AssistedInject constructor( - @Assisted val context: Context, - @Assisted workerParams: WorkerParameters -) : CoroutineWorker(context, workerParams) { - - /** - * @see BackgroundWorkScheduler.scheduleDiagnosisKeyPeriodicWork() - * @see BackgroundWorkScheduler.scheduleDiagnosisKeyOneTimeWork() - */ - override suspend fun doWork(): Result { - Timber.tag(TAG).d("$id: doWork() started. Run attempt: $runAttemptCount") - - var result = Result.success() - try { - BackgroundWorkScheduler.scheduleDiagnosisKeyOneTimeWork() - } catch (e: Exception) { - Timber.tag(TAG).w( - e, - "$id: Error during BackgroundWorkScheduler.scheduleDiagnosisKeyOneTimeWork()." - ) - - if (runAttemptCount > BackgroundConstants.WORKER_RETRY_COUNT_THRESHOLD) { - Timber.tag(TAG).w(e, "$id: Retry attempts exceeded.") - - return Result.failure() - } else { - Timber.tag(TAG).d(e, "$id: Retrying.") - result = Result.retry() - } - } - - Timber.tag(TAG).d("$id: doWork() finished with %s", result) - return result - } - - @AssistedFactory - interface Factory : InjectedWorkerFactory<DiagnosisKeyRetrievalPeriodicWorker> - - companion object { - private val TAG = DiagnosisKeyRetrievalPeriodicWorker::class.java.simpleName - } -} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/DiagnosisTestResultRetrievalPeriodicWorker.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/DiagnosisTestResultRetrievalPeriodicWorker.kt index cd80fd7100a57719ab97efd62e2f622383e56aa5..348777db1e7947374017e72c7a262459deeee0a5 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/DiagnosisTestResultRetrievalPeriodicWorker.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/DiagnosisTestResultRetrievalPeriodicWorker.kt @@ -17,7 +17,6 @@ import de.rki.coronawarnapp.util.TimeAndDateExtensions import de.rki.coronawarnapp.util.TimeStamper import de.rki.coronawarnapp.util.formatter.TestResult import de.rki.coronawarnapp.util.worker.InjectedWorkerFactory -import de.rki.coronawarnapp.worker.BackgroundWorkScheduler.stop import timber.log.Timber /** @@ -34,6 +33,7 @@ class DiagnosisTestResultRetrievalPeriodicWorker @AssistedInject constructor( private val submissionService: SubmissionService, private val timeStamper: TimeStamper, private val tracingSettings: TracingSettings, + private val backgroundWorkScheduler: BackgroundWorkScheduler, ) : CoroutineWorker(context, workerParams) { override suspend fun doWork(): Result { @@ -42,7 +42,7 @@ class DiagnosisTestResultRetrievalPeriodicWorker @AssistedInject constructor( if (runAttemptCount > BackgroundConstants.WORKER_RETRY_COUNT_THRESHOLD) { Timber.tag(TAG).d("$id doWork() failed after $runAttemptCount attempts. Rescheduling") - BackgroundWorkScheduler.scheduleDiagnosisKeyPeriodicWork() + backgroundWorkScheduler.scheduleDiagnosisTestResultPeriodicWork() Timber.tag(TAG).d("$id Rescheduled background worker") return Result.failure() @@ -117,7 +117,7 @@ class DiagnosisTestResultRetrievalPeriodicWorker @AssistedInject constructor( private fun stopWorker() { tracingSettings.initialPollingForTestResultTimeStamp = 0L - BackgroundWorkScheduler.WorkType.DIAGNOSIS_TEST_RESULT_PERIODIC_WORKER.stop() + backgroundWorkScheduler.stopDiagnosisTestResultPeriodicWork() Timber.tag(TAG).d("$id: Background worker stopped") } diff --git a/Corona-Warn-App/src/main/res/drawable/ic_print.xml b/Corona-Warn-App/src/main/res/drawable/ic_print.xml new file mode 100644 index 0000000000000000000000000000000000000000..90691b5cc2a20beb3c772e4d5e0e3142582f0634 --- /dev/null +++ b/Corona-Warn-App/src/main/res/drawable/ic_print.xml @@ -0,0 +1,9 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="20dp" + android:height="18dp" + android:viewportWidth="20" + android:viewportHeight="18"> + <path + android:pathData="M17,5H3C1.34,5 0,6.34 0,8V14H4V18H16V14H20V8C20,6.34 18.66,5 17,5ZM14,16H6V11H14V16ZM17,9C16.45,9 16,8.55 16,8C16,7.45 16.45,7 17,7C17.55,7 18,7.45 18,8C18,8.55 17.55,9 17,9ZM16,0H4V4H16V0Z" + android:fillColor="#757575" /> +</vector> diff --git a/Corona-Warn-App/src/main/res/drawable/ic_share.xml b/Corona-Warn-App/src/main/res/drawable/ic_share.xml new file mode 100644 index 0000000000000000000000000000000000000000..2ed6672d49ee19a438ce2f1744fd7a4d54ccbc63 --- /dev/null +++ b/Corona-Warn-App/src/main/res/drawable/ic_share.xml @@ -0,0 +1,9 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="18dp" + android:height="20dp" + android:viewportWidth="18" + android:viewportHeight="20"> + <path + android:pathData="M15,14.08C14.24,14.08 13.56,14.38 13.04,14.85L5.91,10.7C5.96,10.47 6,10.24 6,10C6,9.76 5.96,9.53 5.91,9.3L12.96,5.19C13.5,5.69 14.21,6 15,6C16.66,6 18,4.66 18,3C18,1.34 16.66,0 15,0C13.34,0 12,1.34 12,3C12,3.24 12.04,3.47 12.09,3.7L5.04,7.81C4.5,7.31 3.79,7 3,7C1.34,7 0,8.34 0,10C0,11.66 1.34,13 3,13C3.79,13 4.5,12.69 5.04,12.19L12.16,16.35C12.11,16.56 12.08,16.78 12.08,17C12.08,18.61 13.39,19.92 15,19.92C16.61,19.92 17.92,18.61 17.92,17C17.92,15.39 16.61,14.08 15,14.08Z" + android:fillColor="#757575" /> +</vector> diff --git a/Corona-Warn-App/src/main/res/drawable/trace_location_stay.xml b/Corona-Warn-App/src/main/res/drawable/trace_location_stay.xml deleted file mode 100644 index ddd2f57d763a62e290f50999165a7e5cfbb227fa..0000000000000000000000000000000000000000 --- a/Corona-Warn-App/src/main/res/drawable/trace_location_stay.xml +++ /dev/null @@ -1,9 +0,0 @@ -<vector xmlns:android="http://schemas.android.com/apk/res/android" - android:width="18dp" - android:height="21dp" - android:viewportWidth="18" - android:viewportHeight="21"> - <path - android:pathData="M13,11C10.24,11 8,13.24 8,16C8,18.76 10.24,21 13,21C15.76,21 18,18.76 18,16C18,13.24 15.76,11 13,11ZM14.65,18.35L12.5,16.2V13H13.5V15.79L15.35,17.64L14.65,18.35ZM14,2H10.82C10.4,0.84 9.3,0 8,0C6.7,0 5.6,0.84 5.18,2H2C0.9,2 0,2.9 0,4V19C0,20.1 0.9,21 2,21H8.11C7.52,20.43 7.04,19.75 6.69,19H2V4H4V7H12V4H14V9.08C14.71,9.18 15.38,9.39 16,9.68V4C16,2.9 15.1,2 14,2ZM8,4C7.45,4 7,3.55 7,3C7,2.45 7.45,2 8,2C8.55,2 9,2.45 9,3C9,3.55 8.55,4 8,4Z" - android:fillColor="#007FAD"/> -</vector> diff --git a/Corona-Warn-App/src/main/res/drawable/trace_location_view_cardhighlight_gradient.xml b/Corona-Warn-App/src/main/res/drawable/trace_location_view_cardhighlight_gradient.xml new file mode 100644 index 0000000000000000000000000000000000000000..2b5115ceca76d8b517354f8a1a65e00c4d2d8d4c --- /dev/null +++ b/Corona-Warn-App/src/main/res/drawable/trace_location_view_cardhighlight_gradient.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<shape xmlns:android="http://schemas.android.com/apk/res/android" + android:shape="rectangle"> + <gradient + android:angle="135" + android:centerColor="#6C648C" + android:endColor="#3C8CBB" + android:startColor="#A93F45" /> +</shape> diff --git a/Corona-Warn-App/src/main/res/drawable/trace_location_view_cardhighlight_gradient_all_corners.xml b/Corona-Warn-App/src/main/res/drawable/trace_location_view_cardhighlight_gradient_all_corners.xml index 2b5115ceca76d8b517354f8a1a65e00c4d2d8d4c..3f18b2b7a68fee245d4e02e9d2453257a8d19292 100644 --- a/Corona-Warn-App/src/main/res/drawable/trace_location_view_cardhighlight_gradient_all_corners.xml +++ b/Corona-Warn-App/src/main/res/drawable/trace_location_view_cardhighlight_gradient_all_corners.xml @@ -1,6 +1,12 @@ <?xml version="1.0" encoding="utf-8"?> <shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle"> + <corners + android:radius="0dp" + android:bottomLeftRadius="@dimen/radius_card" + android:bottomRightRadius="@dimen/radius_card" + android:topLeftRadius="@dimen/radius_card" + android:topRightRadius="@dimen/radius_card" /> <gradient android:angle="135" android:centerColor="#6C648C" diff --git a/Corona-Warn-App/src/main/res/drawable/trace_location_warning.xml b/Corona-Warn-App/src/main/res/drawable/trace_location_warning.xml deleted file mode 100644 index 82e171873cd2a0887136c8c6043c7c823e0e155d..0000000000000000000000000000000000000000 --- a/Corona-Warn-App/src/main/res/drawable/trace_location_warning.xml +++ /dev/null @@ -1,28 +0,0 @@ -<vector xmlns:android="http://schemas.android.com/apk/res/android" - android:width="32dp" - android:height="32dp" - android:viewportWidth="32" - android:viewportHeight="32"> - <path - android:pathData="M16,16m-16,0a16,16 0,1 1,32 0a16,16 0,1 1,-32 0" - android:fillColor="#F5F5F5"/> - <path - android:pathData="M22.4593,15.7042C22.4593,19.3782 19.4813,22.3562 15.8073,22.3562C12.1333,22.3562 9.1553,19.3782 9.1553,15.7042C9.1553,12.0302 12.1333,9.0522 15.8073,9.0522C19.4813,9.0522 22.4593,12.0302 22.4593,15.7042Z" - android:strokeWidth="1.5" - android:fillColor="#00000000" - android:fillType="evenOdd" - android:strokeColor="#007FAD"/> - <group> - <clip-path - android:pathData="M5,9.0332H8.9876V23.3061H5V9.0332Z" - android:fillType="evenOdd"/> - <path - android:pathData="M8.2726,23.3062C1.6166,16.4932 7.1646,9.1062 7.2216,9.0332L8.0146,9.6422C7.8046,9.9152 2.9456,16.4222 8.9876,22.6072L8.2726,23.3062Z" - android:fillColor="#007FAD" - android:fillType="evenOdd"/> - </group> - <path - android:pathData="M23.3117,23.273C29.9677,16.46 24.4197,9.073 24.3627,9L23.5697,9.609C23.7797,9.882 28.6387,16.389 22.5967,22.574L23.3117,23.273Z" - android:fillColor="#007FAD" - android:fillType="evenOdd"/> -</vector> diff --git a/Corona-Warn-App/src/main/res/layout/contact_diary_location_list_fragment.xml b/Corona-Warn-App/src/main/res/layout/contact_diary_location_list_fragment.xml index ad69901e4fea98bc046bc90a73e74cc59e165abf..ea59fb9e0b29f95ecfe8689bef0ecfc0824d7edb 100644 --- a/Corona-Warn-App/src/main/res/layout/contact_diary_location_list_fragment.xml +++ b/Corona-Warn-App/src/main/res/layout/contact_diary_location_list_fragment.xml @@ -16,7 +16,7 @@ app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" - android:paddingBottom="@dimen/spacing_huge" + android:paddingBottom="@dimen/spacing_fab_padding" android:clipToPadding="false" tools:listitem="@layout/contact_diary_location_list_item" /> diff --git a/Corona-Warn-App/src/main/res/layout/contact_diary_person_list_fragment.xml b/Corona-Warn-App/src/main/res/layout/contact_diary_person_list_fragment.xml index 7553db974231d2afcde0fe9a93e58ac0b1b09177..37cf73437b095a12485cb8d39825bbbf51492352 100644 --- a/Corona-Warn-App/src/main/res/layout/contact_diary_person_list_fragment.xml +++ b/Corona-Warn-App/src/main/res/layout/contact_diary_person_list_fragment.xml @@ -16,7 +16,7 @@ app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" - android:paddingBottom="@dimen/spacing_huge" + android:paddingBottom="@dimen/spacing_fab_padding" android:clipToPadding="false" tools:listitem="@layout/contact_diary_person_list_item" /> 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 index 09b1a08305859ef5c3bd2a69132f8d7cf56911e2..b179c9fcb9fb183b95db937bf6908e7f289ee40b 100644 --- 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 @@ -36,7 +36,7 @@ android:layout_width="match_parent" android:layout_height="270dp" app:layout_collapseMode="parallax" - app:srcCompat="@drawable/trace_location_view_cardhighlight_gradient_all_corners" /> + app:srcCompat="@drawable/trace_location_view_cardhighlight_gradient" /> <LinearLayout android:layout_width="match_parent" diff --git a/Corona-Warn-App/src/main/res/layout/fragment_edit_check_in.xml b/Corona-Warn-App/src/main/res/layout/fragment_edit_check_in.xml index 15edd5621dd232084da2e2a6bb9302796dea86d0..89f49d4ca8894f916057eb240bc49c8f5f93f857 100644 --- a/Corona-Warn-App/src/main/res/layout/fragment_edit_check_in.xml +++ b/Corona-Warn-App/src/main/res/layout/fragment_edit_check_in.xml @@ -37,7 +37,7 @@ android:layout_width="match_parent" android:layout_height="270dp" app:layout_collapseMode="parallax" - app:srcCompat="@drawable/trace_location_view_cardhighlight_gradient_all_corners" /> + app:srcCompat="@drawable/trace_location_view_cardhighlight_gradient" /> <LinearLayout android:layout_width="match_parent" diff --git a/Corona-Warn-App/src/main/res/layout/fragment_trace_location_onboarding.xml b/Corona-Warn-App/src/main/res/layout/fragment_trace_location_onboarding.xml index d34935e17da592457de742da36b484d4aa7fdfa7..8100354a0dc61451dc5d49bc5adf28237a694085 100644 --- a/Corona-Warn-App/src/main/res/layout/fragment_trace_location_onboarding.xml +++ b/Corona-Warn-App/src/main/res/layout/fragment_trace_location_onboarding.xml @@ -77,12 +77,13 @@ <ImageView android:id="@+id/check_in_warning_image" - android:layout_width="32dp" - android:layout_height="32dp" - android:layout_marginTop="24dp" - android:src="@drawable/trace_location_warning" - android:layout_marginStart="24dp" - app:layout_constraintTop_toBottomOf="@+id/check_in_onboarding_subtitle" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginStart="@dimen/spacing_normal" + android:layout_marginTop="@dimen/spacing_large" + android:importantForAccessibility="no" + android:src="@drawable/ic_qr_tracing_static" + app:layout_constraintTop_toBottomOf="@id/check_in_onboarding_subtitle" app:layout_constraintStart_toStartOf="parent" /> <TextView @@ -98,12 +99,13 @@ <ImageView android:id="@+id/check_in_stay_image" - android:layout_width="32dp" - android:layout_height="32dp" - android:layout_marginTop="24dp" - android:src="@drawable/trace_location_stay" - android:layout_marginStart="24dp" - app:layout_constraintTop_toBottomOf="@+id/check_in_onboarding_warning" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginStart="@dimen/spacing_normal" + android:layout_marginTop="@dimen/spacing_medium" + android:importantForAccessibility="no" + android:src="@drawable/ic_qr_time" + app:layout_constraintTop_toBottomOf="@id/check_in_onboarding_warning" app:layout_constraintStart_toStartOf="parent" /> <TextView @@ -148,7 +150,7 @@ android:layout_marginTop="4dp" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintTop_toBottomOf="@+id/check_in_onboarding_card_title" /> + app:layout_constraintTop_toBottomOf="@id/check_in_onboarding_card_title" /> <ImageView android:id="@+id/check_in_onboarding_bulletpoint1" @@ -156,7 +158,7 @@ android:layout_height="8dp" android:src="@drawable/bullet_point" android:layout_marginTop="24dp" - app:layout_constraintTop_toBottomOf="@+id/check_in_onboarding_card_subtitle" + app:layout_constraintTop_toBottomOf="@id/check_in_onboarding_card_subtitle" app:layout_constraintStart_toStartOf="parent" /> <TextView @@ -192,7 +194,7 @@ android:layout_height="8dp" android:src="@drawable/bullet_point" android:layout_marginTop="22dp" - app:layout_constraintTop_toBottomOf="@+id/check_in_onboarding_body3" + app:layout_constraintTop_toBottomOf="@id/check_in_onboarding_body3" app:layout_constraintStart_toStartOf="parent" /> <TextView @@ -215,7 +217,7 @@ android:layout_height="8dp" android:src="@drawable/bullet_point" android:layout_marginTop="22dp" - app:layout_constraintTop_toBottomOf="@+id/check_in_onboarding_body4" + app:layout_constraintTop_toBottomOf="@id/check_in_onboarding_body4" app:layout_constraintStart_toStartOf="parent" /> <TextView diff --git a/Corona-Warn-App/src/main/res/layout/qr_code_poster_fragment.xml b/Corona-Warn-App/src/main/res/layout/qr_code_poster_fragment.xml new file mode 100644 index 0000000000000000000000000000000000000000..f2b6d1d139a858278c8c7abcbd497482a39e5a76 --- /dev/null +++ b/Corona-Warn-App/src/main/res/layout/qr_code_poster_fragment.xml @@ -0,0 +1,122 @@ +<?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:background="@android:color/white" + tools:context=".ui.eventregistration.organizer.poster.QrCodePosterFragment"> + + <com.google.android.material.appbar.MaterialToolbar + android:id="@+id/toolbar" + style="@style/CWAToolbar.BackArrow" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:elevation="2dp" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + app:menu="@menu/menu_trace_location_qr_code_poster" + app:title="@string/trace_location_organiser_poster_title" /> + + <androidx.constraintlayout.widget.ConstraintLayout + android:id="@+id/qr_code_poster" + android:layout_width="match_parent" + android:layout_height="wrap_content" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/toolbar"> + + <ImageView + android:id="@+id/poster_image" + android:layout_width="0dp" + android:layout_height="0dp" + android:adjustViewBounds="true" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + tools:layout_constraintDimensionRatio="595:841" /> + + <ImageView + android:id="@+id/qr_code_image" + android:layout_width="0dp" + android:layout_height="0dp" + android:scaleType="fitXY" + app:layout_constraintDimensionRatio="1:1" + app:layout_constraintEnd_toEndOf="@id/end_guideline" + app:layout_constraintStart_toStartOf="@id/start_guideline" + app:layout_constraintTop_toTopOf="@id/top_guideline" + tools:src="@drawable/ic_qrcode" + tools:tint="@android:color/black" /> + + <androidx.constraintlayout.widget.Guideline + android:id="@+id/start_guideline" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:orientation="vertical" + app:layout_constraintGuide_percent="0.16" /> + + <androidx.constraintlayout.widget.Guideline + android:id="@+id/end_guideline" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:orientation="vertical" + app:layout_constraintGuide_percent="0.84" /> + + <androidx.constraintlayout.widget.Guideline + android:id="@+id/top_guideline" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:orientation="horizontal" + app:layout_constraintGuide_percent="0.095" /> + + <androidx.constraintlayout.widget.Guideline + android:id="@+id/text_start_guideline" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:orientation="vertical" + app:layout_constraintGuide_percent="0.132" /> + + <androidx.constraintlayout.widget.Guideline + android:id="@+id/text_end_guideline" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:orientation="vertical" + app:layout_constraintGuide_percent="0.87" /> + + <androidx.constraintlayout.widget.Guideline + android:id="@+id/text_top_guideline" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:orientation="horizontal" + app:layout_constraintGuide_percent="0.61" /> + + <TextView + android:id="@+id/info_text_view" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:maxLines="2" + app:layout_constraintEnd_toEndOf="@id/text_end_guideline" + app:layout_constraintStart_toStartOf="@id/text_start_guideline" + app:layout_constraintTop_toTopOf="@id/text_top_guideline" + tools:ignore="SmallSp" + tools:text="Vereinsaktivität: Jahrestreffen der deutschen SAP Anwendergruppe\nHauptstr 3, 69115 Heidelberg" + tools:textColor="#000000" + tools:textSize="10sp" /> + + </androidx.constraintlayout.widget.ConstraintLayout> + + <com.google.android.material.progressindicator.LinearProgressIndicator + android:id="@+id/progress_bar" + android:layout_width="150dp" + android:layout_height="wrap_content" + android:indeterminate="true" + app:hideAnimationBehavior="inward" + app:indicatorColor="@color/colorAccent" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + + +</androidx.constraintlayout.widget.ConstraintLayout> \ No newline at end of file diff --git a/Corona-Warn-App/src/main/res/layout/trace_location_attendee_checkins_fragment.xml b/Corona-Warn-App/src/main/res/layout/trace_location_attendee_checkins_fragment.xml index cb2c9478f11425e75d037c5b97d1204f6b54cf8b..0d86c8e3555782de8c1716a1d36f5ea1d37e271a 100644 --- a/Corona-Warn-App/src/main/res/layout/trace_location_attendee_checkins_fragment.xml +++ b/Corona-Warn-App/src/main/res/layout/trace_location_attendee_checkins_fragment.xml @@ -20,6 +20,8 @@ android:id="@+id/check_ins_list" android:layout_width="0dp" android:layout_height="0dp" + android:clipToPadding="false" + android:paddingBottom="@dimen/spacing_fab_padding" android:visibility="gone" app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" app:layout_constraintBottom_toBottomOf="parent" diff --git a/Corona-Warn-App/src/main/res/layout/trace_location_create_fragment.xml b/Corona-Warn-App/src/main/res/layout/trace_location_create_fragment.xml index 08c5fcb7ba98c27c26a51e42e6e74ee487c8bf86..3a19e3c3b4f64b8066c9f51219504968a1929175 100644 --- a/Corona-Warn-App/src/main/res/layout/trace_location_create_fragment.xml +++ b/Corona-Warn-App/src/main/res/layout/trace_location_create_fragment.xml @@ -9,24 +9,16 @@ <com.google.android.material.appbar.MaterialToolbar android:id="@+id/toolbar" - android:layout_width="match_parent" + style="@style/CWAToolbar.BackArrow" + android:layout_width="0dp" android:layout_height="wrap_content" + android:elevation="2dp" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" - app:navigationIcon="@drawable/ic_back" app:title="@string/tracelocation_organizer_category_title" tools:subtitle="@string/tracelocation_organizer_category_craft_title" /> - <View - android:id="@+id/toolbar_divider" - android:layout_width="0dp" - android:layout_height="@dimen/card_divider" - android:background="@color/colorHairline" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@+id/toolbar" /> - <ScrollView android:layout_width="0dp" android:layout_height="0dp" @@ -35,7 +27,7 @@ app:layout_constraintBottom_toTopOf="@id/button_submit" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@id/toolbar_divider"> + app:layout_constraintTop_toBottomOf="@id/toolbar"> <LinearLayout android:layout_width="match_parent" diff --git a/Corona-Warn-App/src/main/res/layout/trace_location_organizer_qr_code_detail_fragment.xml b/Corona-Warn-App/src/main/res/layout/trace_location_organizer_qr_code_detail_fragment.xml index 3d67480093e2f75751b8a0ee35c49f4ac29332ba..58782363908fa6bf30e610630d4ad2c63b2b6fcc 100644 --- a/Corona-Warn-App/src/main/res/layout/trace_location_organizer_qr_code_detail_fragment.xml +++ b/Corona-Warn-App/src/main/res/layout/trace_location_organizer_qr_code_detail_fragment.xml @@ -4,9 +4,10 @@ xmlns:tools="http://schemas.android.com/tools" android:id="@+id/content_container" android:layout_width="match_parent" - android:contentDescription="@string/trace_location_event_detail_title_accessibility" + android:layout_height="match_parent" android:background="@drawable/trace_location_gradient_background" - android:layout_height="match_parent"> + android:contentDescription="@string/trace_location_event_detail_title_accessibility" + android:transitionName="trace_location_container_transition"> <androidx.coordinatorlayout.widget.CoordinatorLayout android:id="@+id/coordinator_layout" @@ -36,7 +37,7 @@ android:id="@+id/expandedImage" android:layout_width="match_parent" android:layout_height="match_parent" - android:src="@drawable/trace_location_view_cardhighlight_gradient_all_corners" + android:src="@drawable/trace_location_view_cardhighlight_gradient" app:layout_collapseMode="parallax" /> <LinearLayout @@ -63,8 +64,8 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" - android:gravity="center" android:layout_marginBottom="8dp" + android:gravity="center" android:textColor="@android:color/white" android:textSize="18sp" android:textStyle="normal" @@ -118,31 +119,44 @@ android:id="@+id/qrCodeImage" android:layout_width="0dp" android:layout_height="0dp" - android:contentDescription="@string/trace_location_event_detail_qr_code_accessibility" android:layout_marginHorizontal="24dp" android:layout_marginVertical="12dp" android:layout_marginTop="16dp" android:background="@drawable/trace_location_qr_code_background" + android:contentDescription="@string/trace_location_event_detail_qr_code_accessibility" app:layout_constraintDimensionRatio="H,1:1" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent" - tools:src="@drawable/ic_illustrations_qr_code_scan_info" /> + tools:src="@drawable/ic_qrcode" + tools:tint="@android:color/black" /> + + <com.google.android.material.progressindicator.LinearProgressIndicator + android:id="@+id/progress_bar" + android:layout_width="150dp" + android:layout_height="wrap_content" + android:indeterminate="true" + app:hideAnimationBehavior="inward" + app:indicatorColor="@color/colorAccent" + app:layout_constraintBottom_toBottomOf="@id/qrCodeImage" + app:layout_constraintEnd_toEndOf="@id/qrCodeImage" + app:layout_constraintStart_toStartOf="@id/qrCodeImage" + app:layout_constraintTop_toTopOf="@id/qrCodeImage" /> <TextView android:id="@+id/eventDate" style="@style/headline6Sixteen" android:layout_width="match_parent" android:layout_height="match_parent" - app:layout_constraintTop_toBottomOf="@id/qrCodeImage" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintVertical_chainStyle="packed" - android:layout_marginTop="12dp" android:layout_marginHorizontal="24dp" - app:layout_constraintEnd_toEndOf="parent" + android:layout_marginTop="12dp" android:gravity="center" android:textSize="16sp" android:textStyle="normal" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/qrCodeImage" + app:layout_constraintVertical_chainStyle="packed" tools:text="21.01.2021, 18:00 - 21:00 Uhr"> </TextView> diff --git a/Corona-Warn-App/src/main/res/layout/trace_location_organizer_trace_locations_list_fragment.xml b/Corona-Warn-App/src/main/res/layout/trace_location_organizer_trace_locations_list_fragment.xml index 1ba929717c19f4e0d71a7ff950615d2d35717bd1..2c3516cab07bf0aa2e3e00d29b07c8fdc6ab8556 100644 --- a/Corona-Warn-App/src/main/res/layout/trace_location_organizer_trace_locations_list_fragment.xml +++ b/Corona-Warn-App/src/main/res/layout/trace_location_organizer_trace_locations_list_fragment.xml @@ -13,7 +13,6 @@ android:layout_width="0dp" android:layout_height="wrap_content" android:background="@drawable/contact_diary_background" - android:elevation="@dimen/elevation_weak" android:focusable="true" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" @@ -24,10 +23,10 @@ <androidx.recyclerview.widget.RecyclerView android:id="@+id/recycler_view" - android:clipToPadding="false" android:layout_width="0dp" android:layout_height="0dp" - android:paddingBottom="@dimen/spacing_huge" + android:clipToPadding="false" + android:paddingBottom="@dimen/spacing_fab_padding" android:visibility="gone" app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" app:layout_constraintBottom_toBottomOf="parent" diff --git a/Corona-Warn-App/src/main/res/layout/trace_location_privacy_card.xml b/Corona-Warn-App/src/main/res/layout/trace_location_privacy_card.xml index 3d36130cbf9dcd3f3fcc5a25eeb1cc2abbf20c5b..cd37450cf9ba6b4a96d717d4efff30f5775ee0b2 100644 --- a/Corona-Warn-App/src/main/res/layout/trace_location_privacy_card.xml +++ b/Corona-Warn-App/src/main/res/layout/trace_location_privacy_card.xml @@ -41,7 +41,8 @@ <TextView android:id="@+id/first_bulletpoint_title" - style="@style/subtitleBoldSixteen" + style="@style/subtitle" + android:textStyle="bold" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginStart="@dimen/spacing_normal" @@ -71,7 +72,8 @@ <TextView android:id="@+id/second_bulletpoint_title" - style="@style/subtitleBoldSixteen" + style="@style/subtitle" + android:textStyle="bold" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginTop="@dimen/spacing_normal" diff --git a/Corona-Warn-App/src/main/res/menu/menu_trace_location_qr_code_poster.xml b/Corona-Warn-App/src/main/res/menu/menu_trace_location_qr_code_poster.xml new file mode 100644 index 0000000000000000000000000000000000000000..0158331ca362e23687471d693014ffbcb708d5f9 --- /dev/null +++ b/Corona-Warn-App/src/main/res/menu/menu_trace_location_qr_code_poster.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="utf-8"?> +<menu xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto"> + + <item + android:id="@+id/action_print" + android:icon="@drawable/ic_print" + android:title="@string/trace_location_organiser_poster_print" + app:iconTint="@color/colorTextPrimary1" + app:showAsAction="always" /> + <item + android:id="@+id/action_share" + android:icon="@drawable/ic_share" + android:title="@string/trace_location_organiser_poster_share" + app:iconTint="@color/colorTextPrimary1" + app:showAsAction="ifRoom" /> +</menu> \ No newline at end of file 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 5389cde771a0e757cfd1cdea5fac05d58d8aff7d..6a025e8397fc03d2239e57a6cfd02a906f346b41 100644 --- a/Corona-Warn-App/src/main/res/navigation/nav_graph.xml +++ b/Corona-Warn-App/src/main/res/navigation/nav_graph.xml @@ -153,7 +153,7 @@ tools:layout="@layout/fragment_settings_reset"> <action android:id="@+id/action_settingsResetFragment_to_mainFragment" - app:destination="@id/trace_location_organizer_nav_graph" /> + app:destination="@id/mainFragment" /> </fragment> <!-- Information --> @@ -267,7 +267,7 @@ tools:layout="@layout/fragment_submission_no_consent_positive_other_warning"> <action android:id="@+id/action_submissionResultPositiveOtherWarningNoConsentFragment_to_mainFragment" - app:destination="@id/trace_location_organizer_nav_graph" + app:destination="@id/mainFragment" app:popUpTo="@id/nav_graph" app:popUpToInclusive="true" /> <action @@ -371,7 +371,7 @@ tools:layout="@layout/fragment_submission_result_ready"> <action android:id="@+id/action_submissionResultReadyFragment_to_mainFragment" - app:destination="@id/trace_location_organizer_nav_graph" + app:destination="@id/mainFragment" app:popUpTo="@id/nav_graph" app:popUpToInclusive="true" /> <action @@ -395,7 +395,7 @@ app:destination="@id/submissionSymptomCalendarFragment" /> <action android:id="@+id/action_submissionSymptomIntroductionFragment_to_mainFragment" - app:destination="@id/trace_location_organizer_nav_graph" + app:destination="@id/mainFragment" app:popUpTo="@id/nav_graph" app:popUpToInclusive="true" /> </fragment> @@ -414,7 +414,7 @@ app:destination="@id/submissionResultPositiveOtherWarningNoConsentFragment" /> <action android:id="@+id/action_submissionSymptomCalendarFragment_to_mainFragment" - app:destination="@id/trace_location_organizer_nav_graph" + app:destination="@id/mainFragment" app:popUpTo="@id/nav_graph" app:popUpToInclusive="true" /> </fragment> @@ -437,7 +437,7 @@ android:label="SubmissionTestResultConsentFragment"> <action android:id="@+id/action_submissionTestResultConsentGivenFragment_to_homeFragment" - app:destination="@id/trace_location_organizer_nav_graph" + app:destination="@id/mainFragment" app:popUpTo="@id/nav_graph" app:popUpToInclusive="true" /> <action @@ -450,7 +450,7 @@ android:label="SubmissionTestResultNoConsentFragment"> <action android:id="@+id/action_submissionTestResultNoConsentFragment_to_homeFragment" - app:destination="@id/trace_location_organizer_nav_graph" + app:destination="@id/mainFragment" app:popUpTo="@id/nav_graph" app:popUpToInclusive="true" /> <action @@ -477,7 +477,7 @@ tools:layout="@layout/fragment_submission_test_result_available"> <action android:id="@+id/action_submissionTestResultAvailableFragment_to_mainFragment" - app:destination="@id/trace_location_organizer_nav_graph" + app:destination="@id/mainFragment" app:popUpTo="@id/nav_graph" app:popUpToInclusive="true" /> <action diff --git a/Corona-Warn-App/src/main/res/navigation/trace_location_organizer_nav_graph.xml b/Corona-Warn-App/src/main/res/navigation/trace_location_organizer_nav_graph.xml index a80ae6d058433de49909ad8ee20c52612f24e3d3..aa52e6cb304971cded06b59023927f8fe76811eb 100644 --- a/Corona-Warn-App/src/main/res/navigation/trace_location_organizer_nav_graph.xml +++ b/Corona-Warn-App/src/main/res/navigation/trace_location_organizer_nav_graph.xml @@ -3,10 +3,10 @@ xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/trace_location_organizer_nav_graph" - app:startDestination="@id/traceLocationOrganizerQRInfoFragment"> + app:startDestination="@id/traceLocationInfoFragment"> <fragment - android:id="@+id/traceLocationOrganizerQRInfoFragment" + android:id="@+id/traceLocationInfoFragment" android:name="de.rki.coronawarnapp.ui.eventregistration.organizer.qrinfo.TraceLocationQRInfoFragment" android:label="TraceLocationQRInfoFragment" tools:layout="@layout/trace_location_organizer_qr_code_info_fragment"> @@ -14,13 +14,13 @@ <action android:id="@+id/action_traceLocationOrganizerQRInfoFragment_to_traceLocationOrganizerListFragment" - app:destination="@id/traceLocationOrganizerListFragment" - app:popUpTo="@id/traceLocationOrganizerQRInfoFragment" + app:destination="@id/traceLocationsFragment" + app:popUpTo="@id/traceLocationInfoFragment" app:popUpToInclusive="true" /> </fragment> <fragment - android:id="@+id/traceLocationOrganizerCategoriesFragment" + android:id="@+id/traceLocationCategoryFragment" android:name="de.rki.coronawarnapp.ui.eventregistration.organizer.category.TraceLocationCategoryFragment" android:label="TraceLocationCategoryFragment" tools:layout="@layout/trace_location_organizer_category_fragment"> @@ -43,27 +43,33 @@ app:nullable="true" /> <action android:id="@+id/action_traceLocationCreateFragment_to_traceLocationOrganizerListFragment" - app:destination="@id/traceLocationOrganizerListFragment" + app:destination="@id/traceLocationsFragment" app:popUpTo="@id/mainFragment" app:popUpToInclusive="false" /> </fragment> <fragment - android:id="@+id/traceLocationOrganizerListFragment" + android:id="@+id/traceLocationsFragment" android:name="de.rki.coronawarnapp.ui.eventregistration.organizer.list.TraceLocationsFragment" - android:label="TraceLocationCategoryFragment" + android:label="TraceLocationsFragment" tools:layout="@layout/trace_location_organizer_trace_locations_list_fragment"> <action - android:id="@+id/action_traceLocationOrganizerListFragment_to_traceLocationOrganizerCategoriesFragment" - app:destination="@id/traceLocationOrganizerCategoriesFragment" /> + android:id="@+id/action_traceLocationsFragment_to_traceLocationCategoryFragment" + app:destination="@id/traceLocationCategoryFragment" /> <action android:id="@+id/action_traceLocationOrganizerListFragment_to_qrCodeDetailFragment" app:destination="@id/qrCodeDetailFragment" /> <action - android:id="@+id/action_traceLocationOrganizerListFragment_to_traceLocationCreateFragment" + android:id="@+id/action_traceLocationsFragment_to_qrCodeDetailFragment" + app:destination="@id/qrCodeDetailFragment" /> + <action + android:id="@+id/action_traceLocationsFragment_to_qrCodePosterFragment" + app:destination="@id/qrCodePosterFragment" /> + <action + android:id="@+id/action_traceLocationsFragment_to_traceLocationCreateFragment" app:destination="@id/traceLocationCreateFragment" /> <action - android:id="@+id/action_traceLocationOrganizerListFragment_to_traceLocationOrganizerQRInfoFragment" - app:destination="@id/traceLocationOrganizerQRInfoFragment" /> + android:id="@+id/action_traceLocationOrganizerListFragment_to_traceLocationInfoFragment" + app:destination="@id/traceLocationInfoFragment" /> </fragment> <fragment android:id="@+id/qrCodeDetailFragment" @@ -73,6 +79,19 @@ <argument android:name="traceLocationId" app:argType="long" /> + <action + android:id="@+id/action_qrCodeDetailFragment_to_qrCodePosterFragment" + app:destination="@id/qrCodePosterFragment" /> + </fragment> + + <fragment + android:id="@+id/qrCodePosterFragment" + android:name="de.rki.coronawarnapp.ui.eventregistration.organizer.poster.QrCodePosterFragment" + android:label="qr_code_poster_fragment" + tools:layout="@layout/qr_code_poster_fragment"> + <argument + android:name="traceLocationId" + app:argType="long" /> </fragment> </navigation> \ No newline at end of file diff --git a/Corona-Warn-App/src/main/res/values-de/event_registration_strings.xml b/Corona-Warn-App/src/main/res/values-de/event_registration_strings.xml index 588d5bff5cb0d7be5af478ea91bd2350d9b7596d..2c21432140c2dd68198ee608f07da3079797bd9e 100644 --- a/Corona-Warn-App/src/main/res/values-de/event_registration_strings.xml +++ b/Corona-Warn-App/src/main/res/values-de/event_registration_strings.xml @@ -151,9 +151,9 @@ <!-- XTXT: Text for qr icon of qr info screen --> <string name="trace_location_qr_info_qr_code_text">"Stellen Sie den QR-Code Ihren Gästen entweder über Ihr Smartphone oder in ausgedruckter Form zur Verfügung."</string> <!-- XTXT: Text for time sheet icon of qr info screen --> - <string name="trace_location_qr_info_time_text">"Wenn Sie dauerhaft einen QR-Code verwenden, sollten Sie diesen einmal täglich neu erstellen außerhalb der Öffnungszeiten."</string> + <string name="trace_location_qr_info_time_text">"Wenn Sie dauerhaft einen QR-Code verwenden, sollten Sie diesen einmal täglich außerhalb der Öffnungszeiten neu erstellen."</string> <!-- XTXT: Text for I understand button --> - <string name="acknowledge_button">"Enverstanden"</string> + <string name="acknowledge_button">"Weiter"</string> <!-- XHED: Title of the notification channel for event registration related notifications. --> <string name="tracelocation_notification_channel_title">"Check-ins"</string> @@ -178,7 +178,7 @@ <string name="trace_location_organiser_list_no_codes_subtitle">"Hier werden alle QR-Codes angezeigt, die Sie für ein Ort oder Event erstellt haben. Sie können die QR-Codes löschen, wenn diese nicht mehr verwendet werden sollen."</string> <!-- XBUT: Event organiser qr codes list: menu: information button --> - <string name="trace_location_organizer_list_menu_information_btn">"Information"</string> + <string name="trace_location_organizer_list_menu_information_btn">"Informationen"</string> <!-- XBUT: Event organiser qr codes list: menu: remove all button --> <string name="trace_location_organizer_list_menu_remove_all_btn">"Alle entfernen"</string> diff --git a/Corona-Warn-App/src/main/res/values-de/release_info_strings.xml b/Corona-Warn-App/src/main/res/values-de/release_info_strings.xml index 1762d8bec5dd3f1da51cfd486526cabf898ac3b8..b1493e50e3f95cd0b0fa920142b2f9b4a9e49baa 100644 --- a/Corona-Warn-App/src/main/res/values-de/release_info_strings.xml +++ b/Corona-Warn-App/src/main/res/values-de/release_info_strings.xml @@ -22,7 +22,7 @@ <!-- XTXT: Text bodies for the release info screen bullet points --> <string-array name="new_release_body"> - <item>Die CWA ermöglicht nun einen Check-in für Events und Orte. Personen, die Events veranstalten oder ein Geschäft haben, können über die App einen QR-Code erstellen. Durch Scannen des QR-Codes können sich Gäste bei Ankunft einchecken, um so ihre Anwesenheit zu registrieren. Auf Wunsch legt die App außerdem einen entsprechenden Tagebuch-Eintrag an. Wird eine eingecheckte Person später positiv auf das Coronavirus getestet, werden alle Personen automatisch gewarnt, die zur selben Zeit eingescheckt waren.</item> + <item>Die CWA ermöglicht nun einen Check-in für Events und Orte. Personen, die Events veranstalten oder ein Geschäft haben, können über die App einen QR-Code erstellen. Durch Scannen des QR-Codes können sich Gäste bei Ankunft einchecken, um so ihre Anwesenheit zu registrieren. Auf Wunsch legt die App außerdem einen entsprechenden Tagebuch-Eintrag an. Wird eine eingecheckte Person später positiv auf das Coronavirus getestet, können andere Personen gewarnt werden, die zur selben Zeit eingescheckt waren.</item> </string-array> <!-- XTXT: Text labels that will be converted to Links --> 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 676b78bd3a9fe4abafd7a39160a23b4f48c237cb..5e76c5740c29d83e7f88e404d4fd7c668277a559 100644 --- a/Corona-Warn-App/src/main/res/values-de/strings.xml +++ b/Corona-Warn-App/src/main/res/values-de/strings.xml @@ -308,7 +308,7 @@ <!-- XTXT: risk details - infection period logged information body, under 14 days --> <string name="risk_details_information_body_period_logged_assessment_under_14_days">"Die Corona-Warn-App ist seit %s Tagen installiert. Das Infektionsrisiko wird für Zeiträume berechnet, in denen die Risiko-Ermittlung aktiv ist. Wenn Sie andere Personen getroffen haben und die Risiko-Ermittlung aktiv war, wird Ihr Infektions-Risiko berechnet."</string> <!-- XTXT: risk details - infection period logged information body, over 14 days --> - <string name="risk_details_information_body_period_logged_assessment_over_14_days">"Wenn die Risiko-Ermittlung zu Zeiten in denen sie andere Personen getroffen haben aktiv war, kann die Berechnung des Infektionsrisikos für diesen Zeitraum erfolgen."</string> + <string name="risk_details_information_body_period_logged_assessment_over_14_days">"Wenn die Risiko-Ermittlung zu Zeiten, in denen Sie andere Personen getroffen haben, aktiv war, kann die Berechnung des Infektionsrisikos für diesen Zeitraum erfolgen."</string> <!-- XHED: risk details - infection period logged information body, below behaviors --> <!-- XTXT: risk details - infection period logged information body, under 14 days --> <string name="risk_details_information_body_period_logged_assessment">"Ältere Tage werden automatisch gelöscht, da sie aus Sicht des Infektionsschutzes nicht mehr relevant sind."</string> <!-- XTXT: risk details - infection period days logged/14 --> @@ -1968,4 +1968,10 @@ <string name="trace_location_attendee_camera_card_button">Einstellungen öffnen</string> <!-- YMSG: Trace location onboarding image description--> <string name="trace_location_onboarding_content_description">Drei Personen an einem Stehtisch, zwei von ihnen schauen auf ihr Smartphone.</string> + <!-- XMIT: Trace location poster print --> + <string name="trace_location_organiser_poster_print">"Drucken"</string> + <!-- XMIT: Trace location poster share --> + <string name="trace_location_organiser_poster_share">"Teilen"</string> + <!-- XHED: Trace location poster title --> + <string name="trace_location_organiser_poster_title">"Druckversion"</string> </resources> diff --git a/Corona-Warn-App/src/main/res/values/dimens.xml b/Corona-Warn-App/src/main/res/values/dimens.xml index 79e9dae773d6b9c36433d66e9ca90ca824b522d7..627ff2bc15f763c7b66a6cf4fe450a048b09694c 100644 --- a/Corona-Warn-App/src/main/res/values/dimens.xml +++ b/Corona-Warn-App/src/main/res/values/dimens.xml @@ -156,4 +156,8 @@ <!-- Swipe to delete icons left or right margin from the edge--> <dimen name="swipe_icon_margin">33dp</dimen> + + <!-- Padding for the collapsed floating action button --> + <dimen name="spacing_fab_padding">72dp</dimen> + </resources> diff --git a/Corona-Warn-App/src/main/res/values/strings.xml b/Corona-Warn-App/src/main/res/values/strings.xml index e369ddf69e91d8cb7be66023fd85174623dc6c3f..ced53c0830fe16fd8fffe0e7348008df28fb0198 100644 --- a/Corona-Warn-App/src/main/res/values/strings.xml +++ b/Corona-Warn-App/src/main/res/values/strings.xml @@ -1977,4 +1977,10 @@ <string name="trace_location_attendee_camera_card_button">Einstellungen öffnen</string> <!-- YMSG: Trace location onboarding image description--> <string name="trace_location_onboarding_content_description">Drei Personen an einem Stehtisch, zwei von ihnen schauen auf ihr Smartphone.</string> + <!-- XMIT: Trace location poster print --> + <string name="trace_location_organiser_poster_print">"Print"</string> + <!-- XMIT: Trace location poster share --> + <string name="trace_location_organiser_poster_share">"Share"</string> + <!-- XHED: Trace location poster title --> + <string name="trace_location_organiser_poster_title">"Print version"</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 104e4132425efe495666ece2264a7b4e24aa5dc4..823eb81820e952965917ed99e4ae20099ae4dcca 100644 --- a/Corona-Warn-App/src/main/res/xml/provider_paths.xml +++ b/Corona-Warn-App/src/main/res/xml/provider_paths.xml @@ -6,7 +6,8 @@ <cache-path name="shared_logs" path="debuglog/shared/" /> - <files-path - name="Events" - path="events/" /> + + <cache-path + name="Poster" + path="poster/" /> </paths> \ No newline at end of file diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/sources/fallback/DefaultAppConfigSanityCheck.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/sources/fallback/DefaultAppConfigTest.kt similarity index 93% rename from Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/sources/fallback/DefaultAppConfigSanityCheck.kt rename to Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/sources/fallback/DefaultAppConfigTest.kt index 67679862b65b1b2560d1b91ba942016cbc58e787..4760d3f098dad3186517c9cba5de6c80db3fa6d7 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/sources/fallback/DefaultAppConfigSanityCheck.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/sources/fallback/DefaultAppConfigTest.kt @@ -20,7 +20,7 @@ import testhelpers.EmptyApplication @Config(sdk = [Build.VERSION_CODES.P], application = EmptyApplication::class) @RunWith(RobolectricTestRunner::class) -class DefaultAppConfigSanityCheck : BaseTest() { +class DefaultAppConfigTest : BaseTest() { private val configName = "default_app_config_android.bin" private val checkSumName = "default_app_config_android.sha256" @@ -37,7 +37,7 @@ class DefaultAppConfigSanityCheck : BaseTest() { fun `current default matches checksum`() { val config = context.assets.open(configName).readBytes() val sha256 = context.assets.open(checkSumName).readBytes().toString(Charsets.UTF_8) - sha256 shouldBe "3d108b3fee7d1b4c227087c82bb804048de8d0542c3f2b26cf507a918201124d" + sha256 shouldBe "f10bbfb50eae9f304114bded52972832346279776e9b43f9fdf1a39557497119" config.toSHA256() shouldBe sha256 } diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/contactdiary/ui/overview/ContactDiaryOverviewViewModelTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/contactdiary/ui/overview/ContactDiaryOverviewViewModelTest.kt index bd06b47d299e49aeb0b611eac9e98b24534209ce..7c0754dd1a75fa657fe35e47ee9156b1cc95ef31 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/contactdiary/ui/overview/ContactDiaryOverviewViewModelTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/contactdiary/ui/overview/ContactDiaryOverviewViewModelTest.kt @@ -15,8 +15,10 @@ import de.rki.coronawarnapp.contactdiary.ui.overview.adapter.day.riskevent.RiskE import de.rki.coronawarnapp.contactdiary.ui.overview.adapter.subheader.OverviewSubHeaderItem import de.rki.coronawarnapp.contactdiary.util.ContactDiaryData import de.rki.coronawarnapp.contactdiary.util.mockStringsForContactDiaryExporterTests +import de.rki.coronawarnapp.eventregistration.checkins.CheckIn +import de.rki.coronawarnapp.eventregistration.checkins.CheckInRepository +import de.rki.coronawarnapp.presencetracing.risk.TraceLocationCheckInRisk import de.rki.coronawarnapp.risk.RiskState -import de.rki.coronawarnapp.risk.TraceLocationCheckInRisk import de.rki.coronawarnapp.risk.result.ExposureWindowDayRisk import de.rki.coronawarnapp.risk.storage.RiskLevelStorage import de.rki.coronawarnapp.server.protocols.internal.v2.RiskCalculationParametersOuterClass @@ -29,6 +31,7 @@ import io.mockk.MockKAnnotations import io.mockk.every import io.mockk.impl.annotations.MockK import io.mockk.just +import io.mockk.mockk import io.mockk.runs import io.mockk.verify import kotlinx.coroutines.flow.flowOf @@ -51,9 +54,10 @@ open class ContactDiaryOverviewViewModelTest { @MockK lateinit var riskLevelStorage: RiskLevelStorage @MockK lateinit var timeStamper: TimeStamper @MockK lateinit var context: Context + @MockK lateinit var checkInRepository: CheckInRepository private val testDispatcherProvider = TestDispatcherProvider() - private val date = LocalDate.now() + private val date = LocalDate.parse("2021-04-07") private val dateMillis = date.toDateTimeAtStartOfDay(DateTimeZone.UTC).millis @BeforeEach @@ -65,9 +69,10 @@ open class ContactDiaryOverviewViewModelTest { every { contactDiaryRepository.personEncounters } returns flowOf(emptyList()) every { riskLevelStorage.ewDayRiskStates } returns flowOf(emptyList()) every { riskLevelStorage.traceLocationCheckInRiskStates } returns flowOf(emptyList()) + every { checkInRepository.allCheckIns } returns flowOf(emptyList()) mockStringsForContactDiaryExporterTests(context) - every { timeStamper.nowUTC } returns Instant.now() + every { timeStamper.nowUTC } returns Instant.ofEpochMilli(dateMillis) } private val person = DefaultContactDiaryPerson(123, "Romeo") @@ -113,6 +118,16 @@ open class ContactDiaryOverviewViewModelTest { override val riskState: RiskState = RiskState.INCREASED_RISK } + private val checkInLow = mockk<CheckIn>().apply { + every { id } returns traceLocationCheckInRiskLow.checkInId + every { description } returns "I can make orange rhyme with banana... Bornana" + } + + private val checkInHigh = mockk<CheckIn>().apply { + every { id } returns traceLocationCheckInRiskHigh.checkInId + every { description } returns "I'm the bad guy cause I caused the high risk" + } + private val aggregatedRiskPerDateResultLowRisk = ExposureWindowDayRisk( dateMillisSinceEpoch = dateMillis, riskLevel = RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping.RiskLevel.LOW, @@ -139,8 +154,9 @@ open class ContactDiaryOverviewViewModelTest { dispatcherProvider = testDispatcherProvider, contactDiaryRepository = contactDiaryRepository, riskLevelStorage = riskLevelStorage, - timeStamper, - ContactDiaryExporter( + timeStamper = timeStamper, + checkInRepository = checkInRepository, + exporter = ContactDiaryExporter( context, timeStamper, testDispatcherProvider @@ -345,6 +361,7 @@ open class ContactDiaryOverviewViewModelTest { every { riskLevelStorage.ewDayRiskStates } returns flowOf(listOf(aggregatedRiskPerDateResultLowRisk)) every { riskLevelStorage.traceLocationCheckInRiskStates } returns flowOf(listOf(traceLocationCheckInRiskHigh)) every { contactDiaryRepository.locationVisits } returns flowOf(listOf(locationEventHighRiskVisit)) + every { checkInRepository.allCheckIns } returns flowOf(listOf(checkInHigh)) var item = createInstance().listItems.getOrAwaitValue().first { it is DayOverviewItem && it.date == date @@ -360,9 +377,14 @@ open class ContactDiaryOverviewViewModelTest { riskEventItem!!.validate(highRisk = true) } - every { riskLevelStorage.ewDayRiskStates } returns flowOf(listOf(aggregatedRiskPerDateResultHighRiskDueToLowRiskEncounter)) + every { riskLevelStorage.ewDayRiskStates } returns flowOf( + listOf( + aggregatedRiskPerDateResultHighRiskDueToLowRiskEncounter + ) + ) every { riskLevelStorage.traceLocationCheckInRiskStates } returns flowOf(listOf(traceLocationCheckInRiskLow)) every { contactDiaryRepository.locationVisits } returns flowOf(listOf(locationEventLowRiskVisit)) + every { checkInRepository.allCheckIns } returns flowOf(listOf(checkInLow)) item = createInstance().listItems.getOrAwaitValue().first { it is DayOverviewItem && it.date == date @@ -392,6 +414,7 @@ open class ContactDiaryOverviewViewModelTest { fun `low risk event by attending event with low risk`() { every { contactDiaryRepository.locationVisits } returns flowOf(listOf(locationEventLowRiskVisit)) every { riskLevelStorage.traceLocationCheckInRiskStates } returns flowOf(listOf(traceLocationCheckInRiskLow)) + every { checkInRepository.allCheckIns } returns flowOf(listOf(checkInLow)) val item = createInstance().listItems.getOrAwaitValue().first { it is DayOverviewItem && it.date == date @@ -404,6 +427,7 @@ open class ContactDiaryOverviewViewModelTest { name shouldBe locationEventLowRisk.locationName riskInfoAddition shouldBe null bulledPointColor shouldBe R.color.colorBulletPointLowRisk + description shouldBe checkInLow.description } } } @@ -412,6 +436,7 @@ open class ContactDiaryOverviewViewModelTest { fun `high risk event by attending event with high risk`() { every { contactDiaryRepository.locationVisits } returns flowOf(listOf(locationEventHighRiskVisit)) every { riskLevelStorage.traceLocationCheckInRiskStates } returns flowOf(listOf(traceLocationCheckInRiskHigh)) + every { checkInRepository.allCheckIns } returns flowOf(listOf(checkInHigh)) val item = createInstance().listItems.getOrAwaitValue().first { it is DayOverviewItem && it.date == date @@ -424,6 +449,7 @@ open class ContactDiaryOverviewViewModelTest { name shouldBe locationEventHighRisk.locationName riskInfoAddition shouldBe null bulledPointColor shouldBe R.color.colorBulletPointHighRisk + description shouldBe checkInHigh.description } } } @@ -443,6 +469,8 @@ open class ContactDiaryOverviewViewModelTest { ) ) + every { checkInRepository.allCheckIns } returns flowOf(listOf(checkInHigh, checkInLow)) + val item = createInstance().listItems.getOrAwaitValue().first { it is DayOverviewItem && it.date == date } as DayOverviewItem diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/environment/EnvironmentSetupTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/environment/EnvironmentSetupTest.kt index fa335d5e8129fd17e4575868872077471183314a..cfbee9a6393b9464a3c8b85d14e5dd9158516988 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/environment/EnvironmentSetupTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/environment/EnvironmentSetupTest.kt @@ -63,11 +63,10 @@ class EnvironmentSetupTest : BaseTest() { downloadCdnUrl shouldBe "https://download-${env.rawKey}" submissionCdnUrl shouldBe "https://submission-${env.rawKey}" verificationCdnUrl shouldBe "https://verification-${env.rawKey}" - appConfigVerificationKey shouldBe "12345678-${env.rawKey}" + appConfigPublicKey shouldBe "12345678-${env.rawKey}" safetyNetApiKey shouldBe "placeholder-${env.rawKey}" dataDonationCdnUrl shouldBe "https://datadonation-${env.rawKey}" logUploadServerUrl shouldBe "https://logupload-${env.rawKey}" - qrCodePosterTemplateCdnUrl shouldBe "https://qrcodepostertemplate-${env.rawKey}" crowdNotifierPublicKey shouldBe "123_abc-${env.rawKey}" } } @@ -115,8 +114,9 @@ class EnvironmentSetupTest : BaseTest() { EnvironmentSetup.Type.WRU.rawKey shouldBe "WRU" EnvironmentSetup.Type.WRU_XA.rawKey shouldBe "WRU-XA" EnvironmentSetup.Type.WRU_XD.rawKey shouldBe "WRU-XD" + EnvironmentSetup.Type.TESTER_MOCK.rawKey shouldBe "TESTER-MOCK" EnvironmentSetup.Type.LOCAL.rawKey shouldBe "LOCAL" - EnvironmentSetup.Type.values().size shouldBe 7 + EnvironmentSetup.Type.values().size shouldBe 8 EnvironmentSetup.EnvKey.USE_EUR_KEY_PKGS.rawKey shouldBe "USE_EUR_KEY_PKGS" EnvironmentSetup.EnvKey.SUBMISSION.rawKey shouldBe "SUBMISSION_CDN_URL" @@ -126,9 +126,8 @@ class EnvironmentSetupTest : BaseTest() { EnvironmentSetup.EnvKey.DATA_DONATION.rawKey shouldBe "DATA_DONATION_CDN_URL" EnvironmentSetup.EnvKey.LOG_UPLOAD.rawKey shouldBe "LOG_UPLOAD_SERVER_URL" EnvironmentSetup.EnvKey.SAFETYNET_API_KEY.rawKey shouldBe "SAFETYNET_API_KEY" - EnvironmentSetup.EnvKey.QRCODE_POSTER_TEMPLATE.rawKey shouldBe "QRCODE_POSTER_TEMPLATE_URL" EnvironmentSetup.EnvKey.CROWD_NOTIFIER_PUBLIC_KEY.rawKey shouldBe "CROWD_NOTIFIER_PUBLIC_KEY" - EnvironmentSetup.EnvKey.values().size shouldBe 10 + EnvironmentSetup.EnvKey.values().size shouldBe 9 } companion object { @@ -137,6 +136,7 @@ class EnvironmentSetupTest : BaseTest() { EnvironmentSetup.Type.PRODUCTION, EnvironmentSetup.Type.WRU_XD, EnvironmentSetup.Type.WRU_XA, + EnvironmentSetup.Type.TESTER_MOCK, EnvironmentSetup.Type.LOCAL ) private const val GOOD_JSON = @@ -149,7 +149,6 @@ class EnvironmentSetupTest : BaseTest() { "VERIFICATION_CDN_URL": "https://verification-PROD", "DATA_DONATION_CDN_URL": "https://datadonation-PROD", "LOG_UPLOAD_SERVER_URL": "https://logupload-PROD", - "QRCODE_POSTER_TEMPLATE_URL": "https://qrcodepostertemplate-PROD", "SAFETYNET_API_KEY": "placeholder-PROD", "PUB_KEYS_SIGNATURE_VERIFICATION": "12345678-PROD", "CROWD_NOTIFIER_PUBLIC_KEY": "123_abc-PROD" @@ -161,7 +160,6 @@ class EnvironmentSetupTest : BaseTest() { "VERIFICATION_CDN_URL": "https://verification-DEV", "DATA_DONATION_CDN_URL": "https://datadonation-DEV", "LOG_UPLOAD_SERVER_URL": "https://logupload-DEV", - "QRCODE_POSTER_TEMPLATE_URL": "https://qrcodepostertemplate-DEV", "SAFETYNET_API_KEY": "placeholder-DEV", "PUB_KEYS_SIGNATURE_VERIFICATION": "12345678-DEV", "CROWD_NOTIFIER_PUBLIC_KEY": "123_abc-DEV" @@ -173,7 +171,6 @@ class EnvironmentSetupTest : BaseTest() { "VERIFICATION_CDN_URL": "https://verification-INT", "DATA_DONATION_CDN_URL": "https://datadonation-INT", "LOG_UPLOAD_SERVER_URL": "https://logupload-INT", - "QRCODE_POSTER_TEMPLATE_URL": "https://qrcodepostertemplate-INT", "SAFETYNET_API_KEY": "placeholder-INT", "PUB_KEYS_SIGNATURE_VERIFICATION": "12345678-INT", "CROWD_NOTIFIER_PUBLIC_KEY": "123_abc-INT" @@ -185,7 +182,6 @@ class EnvironmentSetupTest : BaseTest() { "VERIFICATION_CDN_URL": "https://verification-WRU", "DATA_DONATION_CDN_URL": "https://datadonation-WRU", "LOG_UPLOAD_SERVER_URL": "https://logupload-WRU", - "QRCODE_POSTER_TEMPLATE_URL": "https://qrcodepostertemplate-WRU", "SAFETYNET_API_KEY": "placeholder-WRU", "PUB_KEYS_SIGNATURE_VERIFICATION": "12345678-WRU", "CREATE_TRACELOCATION_URL": "https://tracelocation-WRU", @@ -198,7 +194,6 @@ class EnvironmentSetupTest : BaseTest() { "VERIFICATION_CDN_URL": "https://verification-WRU-XD", "DATA_DONATION_CDN_URL": "https://datadonation-WRU-XD", "LOG_UPLOAD_SERVER_URL": "https://logupload-WRU-XD", - "QRCODE_POSTER_TEMPLATE_URL": "https://qrcodepostertemplate-WRU-XD", "SAFETYNET_API_KEY": "placeholder-WRU-XD", "PUB_KEYS_SIGNATURE_VERIFICATION": "12345678-WRU-XD", "CROWD_NOTIFIER_PUBLIC_KEY": "123_abc-WRU-XD" @@ -210,11 +205,21 @@ class EnvironmentSetupTest : BaseTest() { "VERIFICATION_CDN_URL": "https://verification-WRU-XA", "DATA_DONATION_CDN_URL": "https://datadonation-WRU-XA", "LOG_UPLOAD_SERVER_URL": "https://logupload-WRU-XA", - "QRCODE_POSTER_TEMPLATE_URL": "https://qrcodepostertemplate-WRU-XA", "SAFETYNET_API_KEY": "placeholder-WRU-XA", "PUB_KEYS_SIGNATURE_VERIFICATION": "12345678-WRU-XA", "CROWD_NOTIFIER_PUBLIC_KEY": "123_abc-WRU-XA" }, + "TESTER-MOCK": { + "USE_EUR_KEY_PKGS" : true, + "SUBMISSION_CDN_URL": "https://submission-TESTER-MOCK", + "DOWNLOAD_CDN_URL": "https://download-TESTER-MOCK", + "VERIFICATION_CDN_URL": "https://verification-TESTER-MOCK", + "DATA_DONATION_CDN_URL": "https://datadonation-TESTER-MOCK", + "LOG_UPLOAD_SERVER_URL": "https://logupload-TESTER-MOCK", + "SAFETYNET_API_KEY": "placeholder-TESTER-MOCK", + "PUB_KEYS_SIGNATURE_VERIFICATION": "12345678-TESTER-MOCK", + "CROWD_NOTIFIER_PUBLIC_KEY": "123_abc-TESTER-MOCK" + }, "LOCAL": { "USE_EUR_KEY_PKGS" : true, "SUBMISSION_CDN_URL": "https://submission-LOCAL", @@ -222,7 +227,6 @@ class EnvironmentSetupTest : BaseTest() { "VERIFICATION_CDN_URL": "https://verification-LOCAL", "DATA_DONATION_CDN_URL": "https://datadonation-LOCAL", "LOG_UPLOAD_SERVER_URL": "https://logupload-LOCAL", - "QRCODE_POSTER_TEMPLATE_URL": "https://qrcodepostertemplate-LOCAL", "SAFETYNET_API_KEY": "placeholder-LOCAL", "PUB_KEYS_SIGNATURE_VERIFICATION": "12345678-LOCAL", "CROWD_NOTIFIER_PUBLIC_KEY": "123_abc-LOCAL" 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 index 054e7b9473651d136d6f07c03b5a4c5f5f244652..aee24dd7643b2551114c25038649cc427269aad3 100644 --- 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 @@ -3,13 +3,18 @@ package de.rki.coronawarnapp.eventregistration.attendee.confirm import de.rki.coronawarnapp.eventregistration.checkins.CheckInRepository import de.rki.coronawarnapp.eventregistration.checkins.qrcode.TraceLocation import de.rki.coronawarnapp.eventregistration.checkins.qrcode.VerifiedTraceLocation +import de.rki.coronawarnapp.server.protocols.internal.pt.TraceLocationOuterClass import de.rki.coronawarnapp.ui.eventregistration.attendee.confirm.ConfirmCheckInNavigation import de.rki.coronawarnapp.ui.eventregistration.attendee.confirm.ConfirmCheckInViewModel +import de.rki.coronawarnapp.util.TimeAndDateExtensions.secondsToInstant import de.rki.coronawarnapp.util.TimeStamper import io.kotest.matchers.shouldBe import io.mockk.MockKAnnotations +import io.mockk.coEvery import io.mockk.every import io.mockk.impl.annotations.MockK +import okio.ByteString.Companion.decodeBase64 +import org.joda.time.Instant import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith @@ -20,19 +25,32 @@ import testhelpers.extensions.getOrAwaitValue @ExtendWith(InstantExecutorExtension::class) class ConfirmCheckInViewModelTest : BaseTest() { - @MockK lateinit var traceLocation: TraceLocation @MockK lateinit var verifiedTraceLocation: VerifiedTraceLocation @MockK lateinit var checkInRepository: CheckInRepository @MockK lateinit var timeStamper: TimeStamper private lateinit var viewModel: ConfirmCheckInViewModel + private val traceLocation = TraceLocation( + id = 1, + type = TraceLocationOuterClass.TraceLocationType.LOCATION_TYPE_TEMPORARY_OTHER, + description = "My Birthday Party", + address = "at my place", + startDate = 2687955L.secondsToInstant(), + endDate = 2687991L.secondsToInstant(), + defaultCheckInLengthInMinutes = null, + cryptographicSeed = "CRYPTOGRAPHIC_SEED".decodeBase64()!!, + cnPublicKey = "PUB_KEY", + version = TraceLocation.VERSION + ) + @BeforeEach fun setUp() { MockKAnnotations.init(this) + coEvery { checkInRepository.addCheckIn(any()) } returns 1L every { verifiedTraceLocation.traceLocation } returns traceLocation - every { traceLocation.defaultCheckInLengthInMinutes } returns 10 + every { timeStamper.nowUTC } returns Instant.parse("2021-03-04T10:30:00Z") viewModel = ConfirmCheckInViewModel( verifiedTraceLocation = verifiedTraceLocation, @@ -49,8 +67,7 @@ class ConfirmCheckInViewModelTest : BaseTest() { @Test fun onConfirmEvent() { - // TODO -// viewModel.onConfirmTraceLocation() -// viewModel.events.getOrAwaitValue() shouldBe ConfirmCheckInNavigation.ConfirmNavigation + viewModel.onConfirmTraceLocation() + viewModel.events.getOrAwaitValue() shouldBe ConfirmCheckInNavigation.ConfirmNavigation } } diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/CheckInRepositoryTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/CheckInRepositoryTest.kt index e869c2869ed6fc16b1bc2b9a7e91b94f41f9e1dc..0298f03aec66f49a628fb48937b3f1106e802387 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/CheckInRepositoryTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/CheckInRepositoryTest.kt @@ -48,7 +48,6 @@ class CheckInRepositoryTest : BaseTest() { val checkIn = CheckIn( id = 1L, traceLocationId = "41da2115-eba2-49bd-bf17-adb3d635ddaf".encode(), - traceLocationIdHash = "41da2115-eba2-49bd-bf17-adb3d635ddaf".encode(), version = 1, type = 2, description = "brothers birthday", @@ -77,7 +76,6 @@ class CheckInRepositoryTest : BaseTest() { CheckIn( id = 0L, traceLocationId = "41da2115-eba2-49bd-bf17-adb3d635ddaf".encode(), - traceLocationIdHash = "41da2115-eba2-49bd-bf17-adb3d635ddaf".encode(), version = 1, type = 2, description = "brothers birthday", @@ -98,7 +96,6 @@ class CheckInRepositoryTest : BaseTest() { TraceLocationCheckInEntity( id = 0L, traceLocationIdBase64 = "41da2115-eba2-49bd-bf17-adb3d635ddaf".encode().base64(), - traceLocationIdHashBase64 = "41da2115-eba2-49bd-bf17-adb3d635ddaf".encode().base64(), version = 1, type = 2, description = "brothers birthday", @@ -143,7 +140,6 @@ class CheckInRepositoryTest : BaseTest() { TraceLocationCheckInEntity( id = 1L, traceLocationIdBase64 = "41da2115-eba2-49bd-bf17-adb3d635ddaf".encode().base64(), - traceLocationIdHashBase64 = "41da2115-eba2-49bd-bf17-adb3d635ddaf".encode().base64(), version = 1, type = 2, description = "sisters birthday", @@ -164,7 +160,6 @@ class CheckInRepositoryTest : BaseTest() { CheckIn( id = 1L, traceLocationId = "41da2115-eba2-49bd-bf17-adb3d635ddaf".encode(), - traceLocationIdHash = "41da2115-eba2-49bd-bf17-adb3d635ddaf".encode(), version = 1, type = 2, description = "sisters birthday", diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/CheckInTransmissionRiskLevelTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/CheckInTransmissionRiskLevelTest.kt index bafd28dd1ac1725c8978e0c714238c97e6f0b1af..6439da3b9157a29a38ea87c89127bfdd2f6496f3 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/CheckInTransmissionRiskLevelTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/CheckInTransmissionRiskLevelTest.kt @@ -12,7 +12,6 @@ class CheckInTransmissionRiskLevelTest : BaseTest() { private val checkIn = CheckIn( id = 1L, traceLocationId = "41da2115-eba2-49bd-bf17-adb3d635ddaf".encode(), - traceLocationIdHash = "41da2115-eba2-49bd-bf17-adb3d635ddaf".encode(), version = 1, type = 2, description = "restaurant_1", diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/CheckInsTransformerTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/CheckInsTransformerTest.kt index 14d50535d1815d99ca6dc88917933c06885a6b97..1f188c3ec580271c013d8d0e963e9533accecc8e 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/CheckInsTransformerTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/CheckInsTransformerTest.kt @@ -1,12 +1,10 @@ package de.rki.coronawarnapp.eventregistration.checkins -import com.google.protobuf.ByteString import de.rki.coronawarnapp.appconfig.AppConfigProvider import de.rki.coronawarnapp.appconfig.ConfigData import de.rki.coronawarnapp.appconfig.PresenceTracingConfigContainer import de.rki.coronawarnapp.appconfig.PresenceTracingRiskCalculationParamContainer import de.rki.coronawarnapp.appconfig.PresenceTracingSubmissionParamContainer -import de.rki.coronawarnapp.server.protocols.internal.pt.TraceLocationOuterClass import de.rki.coronawarnapp.server.protocols.internal.v2.PresenceTracingParametersOuterClass.PresenceTracingSubmissionParameters.AerosoleDecayFunctionLinear import de.rki.coronawarnapp.server.protocols.internal.v2.PresenceTracingParametersOuterClass.PresenceTracingSubmissionParameters.DurationFilter import de.rki.coronawarnapp.server.protocols.internal.v2.RiskCalculationParametersOuterClass.Range @@ -16,6 +14,7 @@ import de.rki.coronawarnapp.submission.task.TransmissionRiskVectorDeterminator import de.rki.coronawarnapp.util.TimeAndDateExtensions.seconds import de.rki.coronawarnapp.util.TimeAndDateExtensions.toLocalDateUtc import de.rki.coronawarnapp.util.TimeStamper +import de.rki.coronawarnapp.util.toOkioByteString import io.kotest.matchers.shouldBe import io.mockk.MockKAnnotations import io.mockk.coEvery @@ -42,7 +41,6 @@ class CheckInsTransformerTest : BaseTest() { private val checkIn1 = CheckIn( id = 1L, traceLocationId = "traceLocationId1".encode(), - traceLocationIdHash = "traceLocationIdHash1".encode(), version = 1, type = 1, description = "restaurant_1", @@ -67,7 +65,6 @@ class CheckInsTransformerTest : BaseTest() { private val checkIn2 = CheckIn( id = 2L, traceLocationId = "traceLocationId2".encode(), - traceLocationIdHash = "traceLocationIdHash2".encode(), version = 1, type = 2, description = "restaurant_2", @@ -87,7 +84,6 @@ class CheckInsTransformerTest : BaseTest() { private val checkIn3 = CheckIn( id = 3L, traceLocationId = "traceLocationId3".encode(), - traceLocationIdHash = "traceLocationIdHash3".encode(), version = 1, type = 3, description = "restaurant_3", @@ -201,175 +197,63 @@ class CheckInsTransformerTest : BaseTest() { // Check In 1 is excluded from submission due to time deriving // Check In 2 mapping and transformation get(0).apply { - /* - id = 2L, // Not mapped - client specific - guid = "trace_location_2", - guidHash = EMPTY, // Not mapped - client specific - version = 1, - type = 2, - description = "restaurant_2", - address = "address_2", - traceLocationStart = null, - traceLocationEnd = null, - defaultCheckInLengthInMinutes = null, - traceLocationBytes = EMPTY, - signature = "c2lnbmF0dXJlMQ==".decodeBase64()!!, - checkInStart = Instant.parse("2021-03-04T10:20:00Z"), - checkInEnd = Instant.parse("2021-03-04T10:30:00Z"), - completed = false, // Not mapped - client specific - createJournalEntry = false // Not mapped - client specific - */ - + locationId.toOkioByteString() shouldBe checkIn2.traceLocationId // New derived start time startIntervalNumber shouldBe Instant.parse("2021-03-04T10:20:00Z").seconds / TEN_MINUTES_IN_SECONDS // New derived end time endIntervalNumber shouldBe Instant.parse("2021-03-04T10:40:00Z").seconds / TEN_MINUTES_IN_SECONDS - /*signedLocation.signature shouldBe ByteString.copyFrom("signature1".toByteArray()) - parseLocation(signedLocation.location).apply { - guid shouldBe "trace_location_2" - version shouldBe 1 - type shouldBe TraceLocationOuterClass.TraceLocationType.LOCATION_TYPE_TEMPORARY_OTHER - description shouldBe "restaurant_2" - address shouldBe "address_2" - startTimestamp shouldBe 0 - endTimestamp shouldBe 0 - defaultCheckInLengthInMinutes shouldBe 0 - transmissionRiskLevel shouldBe 1 - }*/ } // Check-In 3 mappings and transformation - /* - id = 3L, // Not mapped - client specific - guid = "trace_location_3", - guidHash = EMPTY, // Not mapped - client specific - version = 1, - type = 3, - description = "restaurant_3", - address = "address_3", - traceLocationStart = Instant.parse("2021-03-04T09:00:00Z"), - traceLocationEnd = Instant.parse("2021-03-10T11:00:00Z") - defaultCheckInLengthInMinutes = 10, - traceLocationBytes = EMPTY, - signature = "c2lnbmF0dXJlMQ==".decodeBase64()!!, - checkInStart = Instant.parse("2021-03-04T09:30:00Z"), - checkInEnd = Instant.parse("2021-03-10T09:45:00Z"), - completed = false, // Not mapped - client specific - createJournalEntry = false // Not mapped - client specific - */ - // Splitted CheckIn 1 get(1).apply { + locationId.toOkioByteString() shouldBe checkIn3.traceLocationId // Start time from original check-in startIntervalNumber shouldBe Instant.parse("2021-03-04T09:30:00Z").seconds / TEN_MINUTES_IN_SECONDS // End time for splitted check-in 1 endIntervalNumber shouldBe Instant.parse("2021-03-05T00:00:00Z").seconds / TEN_MINUTES_IN_SECONDS - /*signedLocation.signature shouldBe ByteString.copyFrom("signature1".toByteArray()) - parseLocation(signedLocation.location).apply { - guid shouldBe "trace_location_3" - version shouldBe 1 - type shouldBe TraceLocationOuterClass.TraceLocationType.LOCATION_TYPE_PERMANENT_RETAIL - description shouldBe "restaurant_3" - address shouldBe "address_3" - startTimestamp shouldBe Instant.parse("2021-03-04T09:00:00Z").seconds - endTimestamp shouldBe Instant.parse("2021-03-10T11:00:00Z").seconds - defaultCheckInLengthInMinutes shouldBe 10 - transmissionRiskLevel shouldBe 1 - }*/ } // Splitted CheckIn 2 get(2).apply { + locationId.toOkioByteString() shouldBe checkIn3.traceLocationId + // Start time for splitted check-in 2 startIntervalNumber shouldBe Instant.parse("2021-03-05T00:00:00Z").seconds / TEN_MINUTES_IN_SECONDS // End time for splitted check-in 2 endIntervalNumber shouldBe Instant.parse("2021-03-06T00:00:00Z").seconds / TEN_MINUTES_IN_SECONDS - /*signedLocation.signature shouldBe ByteString.copyFrom("signature1".toByteArray()) - parseLocation(signedLocation.location).apply { - guid shouldBe "trace_location_3" - version shouldBe 1 - type shouldBe TraceLocationOuterClass.TraceLocationType.LOCATION_TYPE_PERMANENT_RETAIL - description shouldBe "restaurant_3" - address shouldBe "address_3" - startTimestamp shouldBe Instant.parse("2021-03-04T09:00:00Z").seconds - endTimestamp shouldBe Instant.parse("2021-03-10T11:00:00Z").seconds - defaultCheckInLengthInMinutes shouldBe 10 - transmissionRiskLevel shouldBe 1 - }*/ } // Splitted CheckIn 3 get(3).apply { + locationId.toOkioByteString() shouldBe checkIn3.traceLocationId // Start time from splitted check-in 3 startIntervalNumber shouldBe Instant.parse("2021-03-06T00:00:00Z").seconds / TEN_MINUTES_IN_SECONDS // End time for splitted check-in 3 endIntervalNumber shouldBe Instant.parse("2021-03-07T00:00:00Z").seconds / TEN_MINUTES_IN_SECONDS - /*signedLocation.signature shouldBe ByteString.copyFrom("signature1".toByteArray()) - parseLocation(signedLocation.location).apply { - guid shouldBe "trace_location_3" - version shouldBe 1 - type shouldBe TraceLocationOuterClass.TraceLocationType.LOCATION_TYPE_PERMANENT_RETAIL - description shouldBe "restaurant_3" - address shouldBe "address_3" - startTimestamp shouldBe Instant.parse("2021-03-04T09:00:00Z").seconds - endTimestamp shouldBe Instant.parse("2021-03-10T11:00:00Z").seconds - defaultCheckInLengthInMinutes shouldBe 10 - transmissionRiskLevel shouldBe 2 - }*/ } // Splitted CheckIn 4 get(4).apply { + locationId.toOkioByteString() shouldBe checkIn3.traceLocationId // Start time from splitted check-in 4 startIntervalNumber shouldBe Instant.parse("2021-03-07T00:00:00Z").seconds / TEN_MINUTES_IN_SECONDS // End time for splitted check-in 4 endIntervalNumber shouldBe Instant.parse("2021-03-08T00:00:00Z").seconds / TEN_MINUTES_IN_SECONDS - /*signedLocation.signature shouldBe ByteString.copyFrom("signature1".toByteArray()) - parseLocation(signedLocation.location).apply { - guid shouldBe "trace_location_3" - version shouldBe 1 - type shouldBe TraceLocationOuterClass.TraceLocationType.LOCATION_TYPE_PERMANENT_RETAIL - description shouldBe "restaurant_3" - address shouldBe "address_3" - startTimestamp shouldBe Instant.parse("2021-03-04T09:00:00Z").seconds - endTimestamp shouldBe Instant.parse("2021-03-10T11:00:00Z").seconds - defaultCheckInLengthInMinutes shouldBe 10 - transmissionRiskLevel shouldBe 4 - }*/ } // Splitted CheckIn 5 get(5).apply { + locationId.toOkioByteString() shouldBe checkIn3.traceLocationId // Start time from splitted check-in 5 startIntervalNumber shouldBe Instant.parse("2021-03-10T00:00:00Z").seconds / TEN_MINUTES_IN_SECONDS // End time for splitted check-in 5 endIntervalNumber shouldBe Instant.parse("2021-03-10T10:20:00Z").seconds / TEN_MINUTES_IN_SECONDS - /*signedLocation.signature shouldBe ByteString.copyFrom("signature1".toByteArray()) - parseLocation(signedLocation.location).apply { - guid shouldBe "trace_location_3" - version shouldBe 1 - type shouldBe TraceLocationOuterClass.TraceLocationType.LOCATION_TYPE_PERMANENT_RETAIL - description shouldBe "restaurant_3" - address shouldBe "address_3" - startTimestamp shouldBe Instant.parse("2021-03-04T09:00:00Z").seconds - endTimestamp shouldBe Instant.parse("2021-03-10T11:00:00Z").seconds - defaultCheckInLengthInMinutes shouldBe 10 - transmissionRiskLevel shouldBe 8 - }*/ } } } - private fun parseLocation(bytes: ByteString): TraceLocationOuterClass.TraceLocation = - TraceLocationOuterClass.TraceLocation.parseFrom(bytes) - companion object { private val TEN_MINUTES_IN_SECONDS = TimeUnit.MINUTES.toSeconds(10) - - // Base64 Strings of trace locations - private const val TRACE_LOCATION_2 = - "ChB0cmFjZV9sb2NhdGlvbl8yEAEYAiIMcmVzdGF1cmFudF8yKglhZGRyZXNzXzI=" - private const val TRACE_LOCATION_3 = - "ChB0cmFjZV9sb2NhdGlvbl8zEAEYAyIMcmVzdGF1cmFudF8zKglhZGRyZXNzXzMwkMOCggY4sM2iggZACg==" } } diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/Base32UrlProvider.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/Base32UrlProvider.kt new file mode 100644 index 0000000000000000000000000000000000000000..036343e6dcf6c681a7f35f89809b365b11da401e --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/Base32UrlProvider.kt @@ -0,0 +1,81 @@ +package de.rki.coronawarnapp.eventregistration.checkins.qrcode + +import de.rki.coronawarnapp.server.protocols.internal.pt.TraceLocationOuterClass +import de.rki.coronawarnapp.server.protocols.internal.pt.TraceLocationOuterClass.CWALocationData +import de.rki.coronawarnapp.server.protocols.internal.pt.TraceLocationOuterClass.TraceLocation +import de.rki.coronawarnapp.server.protocols.internal.pt.TraceLocationOuterClass.QRCodePayload +import de.rki.coronawarnapp.server.protocols.internal.pt.TraceLocationOuterClass.CrowdNotifierData +import de.rki.coronawarnapp.util.toProtoByteString +import okio.ByteString.Companion.decodeBase64 +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 Base32UrlProvider : ArgumentsProvider { + override fun provideArguments(context: ExtensionContext?): Stream<out Arguments> { + return Stream.of( + Arguments.of( + "https://e.coronawarn.app?v=1#BAARELAIAEJBCTLZEBBGS4TUNBSGC6JAKBQXE5DZDIFWC5BANV4SA4DMMFRWKKGTQ6SA" + + "CMHXQ6SACGTFBAAREWZQLEYBGBQHFKDERTR5AIAQMCBKQZEM4PIDAEDQGQQAARZ3BRFS24KCCFZSSN7E4YBSPXT7QU4DO" + + "UKYNRUDLSSPJTFOZWCHHV5DFTRKISOOU5Y3ENLQ2WS2HYW5YAPUE3C6XBJRSAEF6352AOK6DICDCMRTGQRAICABCABA", + QRCodePayload.newBuilder() + .setVersion(1) + .setCrowdNotifierData( + CrowdNotifierData.newBuilder() + .setCryptographicSeed(CRYPTOGRAPHIC_SEED.decodeBase64()!!.toProtoByteString()) + .setPublicKey(PUB_KEY.decodeBase64()!!.toProtoByteString()) + .setVersion(1) + ) + .setVendorData("CAEQAg==".decodeBase64()!!.toProtoByteString()) + .setLocationData( + TraceLocation.newBuilder() + .setDescription("My Birthday Party") + .setAddress("at my place") + .setStartTimestamp(2687955) + .setEndTimestamp(2687991) + .setVersion(1) + .build() + ) + .build(), + CWALocationData.newBuilder() + .setType(TraceLocationOuterClass.TraceLocationType.LOCATION_TYPE_TEMPORARY_OTHER) + .setVersion(1) + .build() + ), + Arguments.of( + "https://e.coronawarn.app?v=1#BAAREIAIAEJA2SLDMVRXEZLBNUQFG2DPOANA2TLBNFXCAU3UOJSWK5BAGENGKCABCJNTA" + + "WJQCMDAOKUGJDHD2AQBAYECVBSIZY6QGAIHANBAABDTWDCLFVYUEELTFE36JZQDE7PH7BJYG5IVQ3DIGXFE6TGK5TMEOPL" + + "2GLHCURE45J3RWI2XBVNFUPRN3QA7IJWF5OCTDEAIL5X3UA4V4GQEGEZDGNBCAYEACEABDAFA", + QRCodePayload.newBuilder() + .setVersion(1) + .setCrowdNotifierData( + CrowdNotifierData.newBuilder() + .setCryptographicSeed(CRYPTOGRAPHIC_SEED.decodeBase64()!!.toProtoByteString()) + .setPublicKey(PUB_KEY.decodeBase64()!!.toProtoByteString()) + .setVersion(1) + ) + .setVendorData("CAEQARgK".decodeBase64()!!.toProtoByteString()) + .setLocationData( + TraceLocation.newBuilder() + .setDescription("Icecream Shop") + .setAddress("Main Street 1") + .setVersion(1) + .build() + ) + .build(), + CWALocationData.newBuilder() + .setType(TraceLocationOuterClass.TraceLocationType.LOCATION_TYPE_PERMANENT_OTHER) + .setVersion(1) + .setDefaultCheckInLengthInMinutes(10) + .build() + ) + ) + } + + companion object { + const val CRYPTOGRAPHIC_SEED = "MTIzNA==" + const val PUB_KEY = + "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEc7DEstcUIRcyk35OYDJ95/hTg3UVhsaDXKT0zK7NhHPXoyzipEnOp3GyNXDVpaPi3cAfQmxeuFMZAIX2+6A5Xg==" + } +} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/Base64UrlProvider.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/Base64UrlProvider.kt new file mode 100644 index 0000000000000000000000000000000000000000..2d92ae6bc967dc070b483590991c231b79295626 --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/Base64UrlProvider.kt @@ -0,0 +1,82 @@ +package de.rki.coronawarnapp.eventregistration.checkins.qrcode + +import de.rki.coronawarnapp.server.protocols.internal.pt.TraceLocationOuterClass +import de.rki.coronawarnapp.server.protocols.internal.pt.TraceLocationOuterClass.CWALocationData +import de.rki.coronawarnapp.server.protocols.internal.pt.TraceLocationOuterClass.TraceLocation +import de.rki.coronawarnapp.server.protocols.internal.pt.TraceLocationOuterClass.QRCodePayload +import de.rki.coronawarnapp.server.protocols.internal.pt.TraceLocationOuterClass.CrowdNotifierData +import de.rki.coronawarnapp.util.toProtoByteString +import okio.ByteString.Companion.decodeBase64 +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 Base64UrlProvider : ArgumentsProvider { + override fun provideArguments(context: ExtensionContext?): Stream<out Arguments> { + return Stream.of( + Arguments.of( + "https://e.coronawarn.app?v=1#CAESLAgBEhFNeSBCaXJ0aGRheSBQYXJ0eRoLYXQgbXkgcGxhY2Uo04ekATD3h6QBGmoIARJgO" + + "MTa6eYSiaDv8lW13xdYEvGHOZ1EYTiFSxt51HEoPCD7CNnvCUiIYPhax1MpkN0UfNClCm9ZWYy0JH01CDVD9eq-vox" + + "Q1EcFJQkEIujVwoCNK0MNGuDK1ayjGxeDc4UDGgQxMjM0IgQIARAC", + QRCodePayload.newBuilder() + .setVersion(1) + .setCrowdNotifierData( + CrowdNotifierData.newBuilder() + .setCryptographicSeed(CRYPTOGRAPHIC_SEED.decodeBase64()!!.toProtoByteString()) + .setPublicKey(PUB_KEY.decodeBase64()!!.toProtoByteString()) + .setVersion(1) + ) + .setVendorData("CAEQAg==".decodeBase64()!!.toProtoByteString()) + .setLocationData( + TraceLocation.newBuilder() + .setDescription("My Birthday Party") + .setAddress("at my place") + .setStartTimestamp(2687955) + .setEndTimestamp(2687991) + .setVersion(1) + .build() + ) + .build(), + CWALocationData.newBuilder() + .setType(TraceLocationOuterClass.TraceLocationType.LOCATION_TYPE_TEMPORARY_OTHER) + .setVersion(1) + .build() + ), + Arguments.of( + "https://e.coronawarn.app?v=1#CAESIAgBEg1JY2VjcmVhbSBTaG9wGg1NYWluIFN0cmVldCAxGmoIARJgOMTa6eYSiaDv8lW1" + + "3xdYEvGHOZ1EYTiFSxt51HEoPCD7CNnvCUiIYPhax1MpkN0UfNClCm9ZWYy0JH01CDVD9eq-voxQ" + + "1EcFJQkEIujVwoCNK0MNGuDK1ayjGxeDc4UDGgQxMjM0IgYIARABGAo", + QRCodePayload.newBuilder() + .setVersion(1) + .setCrowdNotifierData( + CrowdNotifierData.newBuilder() + .setCryptographicSeed(CRYPTOGRAPHIC_SEED.decodeBase64()!!.toProtoByteString()) + .setPublicKey(PUB_KEY.decodeBase64()!!.toProtoByteString()) + .setVersion(1) + ) + .setVendorData("CAEQARgK".decodeBase64()!!.toProtoByteString()) + .setLocationData( + TraceLocation.newBuilder() + .setDescription("Icecream Shop") + .setAddress("Main Street 1") + .setVersion(1) + .build() + ) + .build(), + CWALocationData.newBuilder() + .setType(TraceLocationOuterClass.TraceLocationType.LOCATION_TYPE_PERMANENT_OTHER) + .setVersion(1) + .setDefaultCheckInLengthInMinutes(10) + .build() + ) + ) + } + + companion object { + const val CRYPTOGRAPHIC_SEED = "MTIzNA==" + const val PUB_KEY = + "OMTa6eYSiaDv8lW13xdYEvGHOZ1EYTiFSxt51HEoPCD7CNnvCUiIYPhax1MpkN0UfNClCm9ZWYy0JH01CDVD9" + + "eq+voxQ1EcFJQkEIujVwoCNK0MNGuDK1ayjGxeDc4UD" + } +} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/DefaultQRCodeVerifierTest2.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/DefaultQRCodeVerifierTest2.kt deleted file mode 100644 index 5a767d467dcc7696076c8e28082bf2f79dee8074..0000000000000000000000000000000000000000 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/DefaultQRCodeVerifierTest2.kt +++ /dev/null @@ -1,64 +0,0 @@ -package de.rki.coronawarnapp.eventregistration.checkins.qrcode - -import testhelpers.BaseTest - -class DefaultQRCodeVerifierTest2 : BaseTest() { - - /* Disabled, because new protobuf doesn't include signed traceLocation, we should write tests for the parsing of - QrCodePayload protobuf ... - - @Test - fun `protobuf decoding 1`() { - val signedTraceLocation = - TraceLocationOuterClass.SignedTraceLocation.parseFrom( - "BJLAUJBTGA2TKMZTGFRS2MRTGA3C2NBTMYZS2OJXGQZC2NTEHBTGCYRVGRSTQNBYCAARQARCCFGXSICCNFZHI2DEMF4SAUDBOJ2HSKQLMF2CA3LZEBYGYYLDMUYNHB5EAE4PPB5EAFAAAESGGBCAEIDFJJ7KHRO3ZZ2SFMJSBXSUY2ZZKGOIZS27L2D6VPKTA57M6RZY3MBCARR7LXAA2BY3IGNTHNFFAJSMIXF6PP4TEB3I2C3D7P32QUZHVVER" - .decodeBase32().toByteArray() - ) - - signedTraceLocation.apply { - TraceLocationOuterClass.TraceLocation.parseFrom(location).apply { - guid shouldBe "3055331c-2306-43f3-9742-6d8fab54e848" - version shouldBe 1 - typeValue shouldBe 2 - description shouldBe "My Birthday Party" - address shouldBe "at my place" - startTimestamp shouldBe 2687955 - endTimestamp shouldBe 2687991 - defaultCheckInLengthInMinutes shouldBe 0 - } - signature.toByteArray().toByteString() - .base64() shouldBe "MEQCIGVKfqPF2851IrEyDeVMazlRnIzLX16H6r1TB37PRzjbAiBGP13ADQcbQZsztKUCZMRcvnv5Mgdo0LY/v3qFMnrUkQ==" - } - - signedTraceLocation.location.toByteArray().toByteString() - .base64() shouldBe "CiQzMDU1MzMxYy0yMzA2LTQzZjMtOTc0Mi02ZDhmYWI1NGU4NDgQARgCIhFNeSBCaXJ0aGRheSBQYXJ0eSoLYXQgbXkgcGxhY2Uw04ekATj3h6QBQAA=" - } - - @Test - fun `protobuf decoding 2`() { - val signedTraceLocation = TraceLocationOuterClass.SignedTraceLocation.parseFrom( - "BJHAUJDGMNQTQNDCGM3S2NRRMMYC2NDBG5RS2YRSMY4C2OBSGVRWCZDEGUYDMY3GCAARQAJCBVEWGZLDOJSWC3JAKNUG64BKBVGWC2LOEBJXI4TFMV2CAMJQAA4AAQAKCJDDARACEA2ZCTGOF2HH2RQU7ODZMCSUTUBBNQYM6AR4NG6FFLC6ISXWEOI5UARADO44YYH3U53ZYL6IYM5DWALXUESAJNWRGRL5KLNLS5BM54SHDDCA" - .decodeBase32().toByteArray() - ) - - signedTraceLocation.apply { - TraceLocationOuterClass.TraceLocation.parseFrom(location).apply { - guid shouldBe "fca84b37-61c0-4a7c-b2f8-825cadd506cf" - version shouldBe 1 - typeValue shouldBe 1 - description shouldBe "Icecream Shop" - address shouldBe "Main Street 1" - startTimestamp shouldBe 0 - endTimestamp shouldBe 0 - defaultCheckInLengthInMinutes shouldBe 10 - } - signature.toByteArray().toByteString() - .base64() shouldBe "MEQCIDWRTM4ujn1GFPuHlgpUnQIWwwzwI8abxSrF5Er2I5HaAiAbucxg+6d3nC/Iwzo7AXehJAS20TRX1S2rl0LO8kcYxA==" - } - - signedTraceLocation.location.toByteArray().toByteString() - .base64() shouldBe "CiRmY2E4NGIzNy02MWMwLTRhN2MtYjJmOC04MjVjYWRkNTA2Y2YQARgBIg1JY2VjcmVhbSBTaG9wKg1NYWluIFN0cmVldCAxMAA4AEAK" - } - - */ -} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/QRCodeUriParserTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/QRCodeUriParserTest.kt index 71a1c8722df6ebbcae83bb335bebdd1176f56d30..c01a15b2b532b8da63ff40271b1c22e9ed49910f 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/QRCodeUriParserTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/QRCodeUriParserTest.kt @@ -1,24 +1,91 @@ package de.rki.coronawarnapp.eventregistration.checkins.qrcode +import de.rki.coronawarnapp.appconfig.AppConfigProvider +import de.rki.coronawarnapp.appconfig.ConfigData +import de.rki.coronawarnapp.appconfig.PresenceTracingConfigContainer +import de.rki.coronawarnapp.server.protocols.internal.pt.TraceLocationOuterClass.CWALocationData +import de.rki.coronawarnapp.server.protocols.internal.pt.TraceLocationOuterClass.QRCodePayload +import de.rki.coronawarnapp.server.protocols.internal.v2.PresenceTracingParametersOuterClass.PresenceTracingQRCodeDescriptor +import de.rki.coronawarnapp.server.protocols.internal.v2.PresenceTracingParametersOuterClass.PresenceTracingQRCodeDescriptor.PayloadEncoding +import io.kotest.assertions.throwables.shouldThrow import io.kotest.matchers.shouldBe -import io.kotest.matchers.shouldNotBe +import io.mockk.MockKAnnotations +import io.mockk.coEvery +import io.mockk.every +import io.mockk.impl.annotations.MockK +import io.mockk.mockk +import kotlinx.coroutines.test.runBlockingTest +import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.ArgumentsSource import testhelpers.BaseTest +@Suppress("BlockingMethodInNonBlockingContext") class QRCodeUriParserTest : BaseTest() { - fun createInstance() = QRCodeUriParser() + @MockK lateinit var configProvider: AppConfigProvider + + @BeforeEach + fun setup() { + MockKAnnotations.init(this) + coEvery { configProvider.getAppConfig() } returns mockk<ConfigData>().apply { + every { presenceTracing } returns PresenceTracingConfigContainer( + qrCodeDescriptors = listOf( + PresenceTracingQRCodeDescriptor.newBuilder() + .setVersionGroupIndex(0) + .setEncodedPayloadGroupIndex(1) + .setPayloadEncoding(PayloadEncoding.BASE64) + .setRegexPattern("https://e\\.coronawarn\\.app\\?v=(\\d+)\\#(.+)") + .build() + ) + ) + } + } + + fun createInstance() = QRCodeUriParser(configProvider) @ParameterizedTest - @ArgumentsSource(ValidUrlProvider::class) - fun `Valid URLs`(input: String) { - createInstance().getQrCodePayload(input) shouldNotBe null + @ArgumentsSource(Base64UrlProvider::class) + fun `Base64 Valid URLs`( + input: String, + expectedPayload: QRCodePayload, + expectedVendorData: CWALocationData + ) = runBlockingTest { + val qrCodePayload = createInstance().getQrCodePayload(input) + qrCodePayload shouldBe expectedPayload + CWALocationData.parseFrom(qrCodePayload.vendorData) shouldBe expectedVendorData + } + + @ParameterizedTest + @ArgumentsSource(Base32UrlProvider::class) + fun `Base32 Valid URLs`( + input: String, + expectedPayload: QRCodePayload, + expectedVendorData: CWALocationData + ) = runBlockingTest { + coEvery { configProvider.getAppConfig() } returns mockk<ConfigData>().apply { + every { presenceTracing } returns PresenceTracingConfigContainer( + qrCodeDescriptors = listOf( + PresenceTracingQRCodeDescriptor.newBuilder() + .setVersionGroupIndex(0) + .setEncodedPayloadGroupIndex(1) + .setPayloadEncoding(PayloadEncoding.BASE32) + .setRegexPattern("https://e\\.coronawarn\\.app\\?v=(\\d+)\\#(.+)") + .build() + ) + ) + } + + val qrCodePayload = createInstance().getQrCodePayload(input) + qrCodePayload shouldBe expectedPayload + CWALocationData.parseFrom(qrCodePayload.vendorData) shouldBe expectedVendorData } @ParameterizedTest @ArgumentsSource(InvalidUrlProvider::class) - fun `Invalid URLs`(input: String) { - createInstance().getQrCodePayload(input) shouldBe null + fun `Invalid URLs`(input: String) = runBlockingTest { + shouldThrow<InvalidQrCodeUriException> { + createInstance().getQrCodePayload(input) + } } } 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 deleted file mode 100644 index f7e0bf42fae6a0c6d362ad86c1ffdb14bf97fc9f..0000000000000000000000000000000000000000 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/ValidUrlProvider.kt +++ /dev/null @@ -1,38 +0,0 @@ -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=" - ), - Arguments.of( - "https://e.coronawarn.app/c1/BJLAUJBTGA2TKMZTGFRS2MRTGA3C2NBTMYZS2OJXGQZC2NTEHBTGCYRVG" + - "RSTQNBYCAARQARCCFGXSICCNFZHI2DEMF4SAUDBOJ2HSKQLMF2CA3LZEBYGYYLDMUYNHB5EAE4PPB5EAF" + - "AAAESIGBDAEIIARVENF6QT6XZATJ5GSDHL77BCAGR6QKDEUJRP2RDCTKTS7QECWMFAEIIA47MT2EA7MQK" + - "GNQU2XCY3Y2ZOZXCILDPC65PBUO4JJHT5LQQWDQSA" - ), - Arguments.of( - "https://e.coronawarn.app/c1/BJHAUJDGMNQTQNDCGM3S2NRRMMYC2NDBG5RS2YRSMY4C2OBSGVRWCZDEG" + - "UYDMY3GCAARQAJCBVEWGZLDOJSWC3JAKNUG64BKBVGWC2LOEBJXI4TFMV2CAMJQAA4AAQAKCJDTARICEB" + - "NEPPKKTAAIH5BSV45EPOINHOASARJLYYSHNTUUHLNGVYUZXZEBWARBACD53WYEGYXYQS3STOFLSOVM3XX" + - "D5A5HKMFQR7WYYARKKVOFGYGHO" - ), - Arguments.of("https://e.coronawarn.app/c1/JBSWY3DPEBLW64TMMQQQ===="), - Arguments.of("https://e.coronawarn.app/c1/JBSWY3DPEBLW64TMMQQQ"), - Arguments.of("https://e.coronawarn.app/c1/JBSWY3DPEBLW64TMMQQQ========"), - ) - } -} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/split/CheckInSplitterTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/split/CheckInSplitterTest.kt index dc037d1caee0f5ac21a14b0f269ead33e909e035..8c76d64b9b633b1c8fcde91572d7ce43c9f1a47f 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/split/CheckInSplitterTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/split/CheckInSplitterTest.kt @@ -16,7 +16,6 @@ class CheckInSplitterTest : BaseTest() { private val defaultCheckIn = CheckIn( id = 1L, traceLocationId = "traceLocationId1".encode(), - traceLocationIdHash = "traceLocationIdHash1".encode(), version = 1, type = 1, description = "Restaurant", diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/events/QrCodePayloadTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/events/QrCodePayloadTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..683a645639868de09b9607ee724e21801bfac02c --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/events/QrCodePayloadTest.kt @@ -0,0 +1,67 @@ +package de.rki.coronawarnapp.eventregistration.events + +import de.rki.coronawarnapp.eventregistration.checkins.qrcode.TraceLocation +import de.rki.coronawarnapp.eventregistration.checkins.qrcode.qrCodePayload +import de.rki.coronawarnapp.server.protocols.internal.pt.TraceLocationOuterClass +import de.rki.coronawarnapp.util.TimeAndDateExtensions.secondsToInstant +import io.kotest.matchers.shouldBe +import okio.ByteString.Companion.decodeBase64 +import org.junit.jupiter.api.Test +import testhelpers.BaseTest + +class QrCodePayloadTest : BaseTest() { + + @Test + fun `Trace location to QrCodePayload 1`() { + val traceLocation = TraceLocation( + id = 1, + type = TraceLocationOuterClass.TraceLocationType.LOCATION_TYPE_TEMPORARY_OTHER, + description = "My Birthday Party", + address = "at my place", + startDate = 2687955L.secondsToInstant(), + endDate = 2687991L.secondsToInstant(), + defaultCheckInLengthInMinutes = null, + cryptographicSeed = CRYPTOGRAPHIC_SEED.decodeBase64()!!, + cnPublicKey = PUB_KEY, + version = TraceLocation.VERSION + ) + + traceLocation.qrCodePayload() shouldBe + TraceLocationOuterClass.QRCodePayload.parseFrom(PAYLOAD_1.decodeBase64()!!.toByteArray()) + } + + @Test + fun `Trace location to QrCodePayload 2`() { + val traceLocation = TraceLocation( + id = 2, + type = TraceLocationOuterClass.TraceLocationType.LOCATION_TYPE_PERMANENT_OTHER, + description = "Icecream Shop", + address = "Main Street 1", + startDate = null, + endDate = null, + defaultCheckInLengthInMinutes = 10, + cryptographicSeed = CRYPTOGRAPHIC_SEED.decodeBase64()!!, + cnPublicKey = PUB_KEY, + version = TraceLocation.VERSION + ) + + traceLocation.qrCodePayload() shouldBe + TraceLocationOuterClass.QRCodePayload.parseFrom(PAYLOAD_2.decodeBase64()!!.toByteArray()) + } + + companion object { + private const val PAYLOAD_1 = + "CAESLAgBEhFNeSBCaXJ0aGRheSBQYXJ0eRoLYXQgbXkgcGxhY2Uo04ekATD3h6QBGmUIARJbMFkwEwYHKoZIzj0CAQYIKo" + + "ZIzj0DAQcDQgAEc7DEstcUIRcyk35OYDJ95/hTg3UVhsaDXKT0zK7NhHPXoyzipEnOp3GyNXDVpaPi3cAfQmxe" + + "uFMZAIX2+6A5XhoEMTIzNCIECAEQAg==" + + private const val PAYLOAD_2 = + "CAESIAgBEg1JY2VjcmVhbSBTaG9wGg1NYWluIFN0cmVldCAxGmUIARJbMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEc7DEstcUIR" + + "cyk35OYDJ95/hTg3UVhsaDXKT0zK7NhHPXoyzipEnOp3GyNXDVpaPi3cAfQmxeuFMZAIX2+6A5XhoEMTIzNCIGCAEQARgK" + + private const val CRYPTOGRAPHIC_SEED = "MTIzNA==" + private const val PUB_KEY = + "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEc7DEstcUIRcyk35OYDJ95/hTg3UVhsaDXKT0z" + + "K7NhHPXoyzipEnOp3GyNXDVpaPi3cAfQmxeuFMZAIX2+6A5Xg==" + } +} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/events/TraceLocationIdTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/events/TraceLocationIdTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..f92345e30e279cf4183212ddf5f99bbf93a72007 --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/events/TraceLocationIdTest.kt @@ -0,0 +1,245 @@ +package de.rki.coronawarnapp.eventregistration.events + +import de.rki.coronawarnapp.eventregistration.checkins.qrcode.TraceLocation +import de.rki.coronawarnapp.eventregistration.checkins.qrcode.TraceLocationId +import de.rki.coronawarnapp.eventregistration.checkins.qrcode.qrCodePayload +import de.rki.coronawarnapp.eventregistration.checkins.qrcode.toTraceLocationIdHash +import de.rki.coronawarnapp.eventregistration.checkins.qrcode.traceLocation +import de.rki.coronawarnapp.server.protocols.internal.pt.TraceLocationOuterClass +import de.rki.coronawarnapp.util.TimeAndDateExtensions.secondsToInstant +import io.kotest.matchers.shouldBe +import okio.ByteString +import okio.ByteString.Companion.decodeBase64 +import okio.ByteString.Companion.decodeHex +import okio.ByteString.Companion.toByteString +import org.joda.time.Instant +import org.junit.jupiter.api.Test +import testhelpers.BaseTest + +@Suppress("MaxLineLength") +class TraceLocationIdTest : BaseTest() { + @Test + fun `locationId from qrCodePayloadBase64 - 1`() { + val qrCodePayloadBase64 = + "CAESLAgBEhFNeSBCaXJ0aGRheSBQYXJ0eRoLYXQgbXkgcGxhY2Uo04ekATD3h6QBGmUIARJbMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEc7DEstcUIRcyk35OYDJ95/hTg3UVhsaDXKT0zK7NhHPXoyzipEnOp3GyNXDVpaPi3cAfQmxeuFMZAIX2+6A5XhoEMTIzNCIECAEQAg==" + val qrCodePayload = TraceLocationOuterClass.QRCodePayload.parseFrom( + qrCodePayloadBase64.decodeBase64()!!.toByteArray() + ) + qrCodePayload.traceLocation().apply { + locationId.base64() shouldBe "jNcJTCajd9Sen6Tbexl2Yb7O3J7ps47b6k4+QMT4xS0=" + } + } + + @Test + fun `locationId from qrCodePayloadBase64 - 2`() { + val qrCodePayloadBase64 = + "CAESIAgBEg1JY2VjcmVhbSBTaG9wGg1NYWluIFN0cmVldCAxGmUIARJbMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEc7DEstcUIRcyk35OYDJ95/hTg3UVhsaDXKT0zK7NhHPXoyzipEnOp3GyNXDVpaPi3cAfQmxeuFMZAIX2+6A5XhoEMTIzNCIGCAEQARgK" + val qrCodePayload = TraceLocationOuterClass.QRCodePayload.parseFrom( + qrCodePayloadBase64.decodeBase64()!!.toByteArray() + ) + qrCodePayload.traceLocation().apply { + locationId.base64() shouldBe "GMuCjqNmOdYyrFhyvFNTVEeLaZh+uShgUoY0LYJo4YQ=" + } + } + + @Test + fun `locationId from traceLocation - 1`() { + val traceLocation = TraceLocation( + type = TraceLocationOuterClass.TraceLocationType.LOCATION_TYPE_TEMPORARY_OTHER, + description = "My Birthday Party", + address = "at my place", + startDate = 2687955L.secondsToInstant(), + endDate = 2687991L.secondsToInstant(), + defaultCheckInLengthInMinutes = null, + cryptographicSeed = "MTIzNA==".decodeBase64()!!, + cnPublicKey = "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEc7DEstcUIRcyk35OYDJ95/hTg3UVhsaDXKT0zK7NhHPXoyzipEnOp3GyNXDVpaPi3cAfQmxeuFMZAIX2+6A5Xg==", + version = TraceLocation.VERSION + ) + traceLocation.locationId.base64() shouldBe "jNcJTCajd9Sen6Tbexl2Yb7O3J7ps47b6k4+QMT4xS0=" + } + + @Test + fun `locationId from traceLocation - 2`() { + val traceLocation = TraceLocation( + type = TraceLocationOuterClass.TraceLocationType.LOCATION_TYPE_PERMANENT_OTHER, + description = "Icecream Shop", + address = "Main Street 1", + startDate = null, + endDate = null, + defaultCheckInLengthInMinutes = 10, + cryptographicSeed = "MTIzNA==".decodeBase64()!!, + cnPublicKey = "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEc7DEstcUIRcyk35OYDJ95/hTg3UVhsaDXKT0zK7NhHPXoyzipEnOp3GyNXDVpaPi3cAfQmxeuFMZAIX2+6A5Xg==", + version = TraceLocation.VERSION + ) + + traceLocation.locationId.base64() shouldBe "GMuCjqNmOdYyrFhyvFNTVEeLaZh+uShgUoY0LYJo4YQ=" + } + + /** + * Match server calculation + * https://github.com/corona-warn-app/cwa-server/blob/5ce7d27a74fbf4f2ed560772f97ac17e2189ad33/common/persistence/src/test/java/app/coronawarn/server/common/persistence/service/TraceTimeIntervalWarningServiceTest.java#L141 + */ + @Test + fun `test trace location hash generation`() { + val locationId = "afa27b44d43b02a9fea41d13cedc2e4016cfcf87c5dbf990e593669aa8ce286d" + val locationIdByte: ByteString = locationId.decodeHex() + val hashedLocationId: ByteString = locationIdByte.sha256() + + val expectedLocationIdHash = "0f37dac11d1b8118ea0b44303400faa5e3b876da9d758058b5ff7dc2e5da8230" + + hashedLocationId.hex() shouldBe expectedLocationIdHash + hashedLocationId shouldBe expectedLocationIdHash.decodeHex() + } + + @Test + fun `test trace location hash generation - 2`() { + val id: TraceLocationId = "1b02111da7c0799df6ad67deb7b397bdfb07e63da0fdea30fae335762826e34f".decodeHex() + id.toTraceLocationIdHash().hex() shouldBe "394db434a2e9c2ca9f32eed266d30bc037b4314cb3d53249fada68de45450cbb" + } + + @Test + fun `test trace location hash generation - 3`() { + val id: TraceLocationId = "14c7a20ed81ebabdc32c8521382c56b851af15ccd8d13c86cd91a0620e78d664".decodeHex() + id.toTraceLocationIdHash().hex() shouldBe "852475fa271a29c67ad85578bb86ff4922dabf9f2b081353e1b5cdf99442889d" + } + + @Test + fun `turn location ID into location ID hash`() { + val traceLocationIdHex = "afa27b44d43b02a9fea41d13cedc2e4016cfcf87c5dbf990e593669aa8ce286d" + val expectedLocationIdHashHex = "0f37dac11d1b8118ea0b44303400faa5e3b876da9d758058b5ff7dc2e5da8230" + + val traceLocationId: TraceLocationId = traceLocationIdHex.decodeHex() + traceLocationId.toTraceLocationIdHash() shouldBe expectedLocationIdHashHex.decodeHex() + } + + @Test + fun `mock server test data - 1`() { + val qrCodePayloadBase64 = + "CAESKAgBEhRBcHBsZSBDb21wdXRlciwgSW5jLhoOMTk1OCBGb2hlIFJvYWQadggBEmA4xNrp5hKJoO_yVbXfF1gS8Yc5nURhOIVLG3nUcSg8IPsI2e8JSIhg-FrHUymQ3RR80KUKb1lZjLQkfTUINUP16r6-jFDURwUlCQQi6NXCgI0rQw0a4MrVrKMbF4NzhQMaENXiVYke5XY0HddkDmj-3HYiBwgBEAQYqQE" + val expectedLocationIdHex = "fc925439f45417a14403b25c95fdc6d8711653f8aa08c0d0967bd30a6348c7fc" + val expectedLocationIdBase64 = "/JJUOfRUF6FEA7Jclf3G2HEWU/iqCMDQlnvTCmNIx/w=" + + val qrCodePayload = TraceLocationOuterClass.QRCodePayload.parseFrom( + qrCodePayloadBase64.decodeBase64()!!.toByteArray() + ) + + // Payload -> locationId (direct from raw) + val payloadBytes = qrCodePayloadBase64.decodeBase64()!!.toByteArray() + val totalByteSequence = "CWA-GUID".toByteArray() + payloadBytes + totalByteSequence.toByteString().sha256().base64() shouldBe expectedLocationIdBase64 + + // TraceLocation -> locationId (manual reconstruction) + val traceLocation = TraceLocation( + type = TraceLocationOuterClass.TraceLocationType.LOCATION_TYPE_PERMANENT_FOOD_SERVICE, + description = "Apple Computer, Inc.", + address = "1958 Fohe Road", + startDate = null, + endDate = null, + defaultCheckInLengthInMinutes = 169, + cryptographicSeed = "1eJViR7ldjQd12QOaP7cdg==".decodeBase64()!!, + cnPublicKey = "OMTa6eYSiaDv8lW13xdYEvGHOZ1EYTiFSxt51HEoPCD7CNnvCUiIYPhax1MpkN0UfNClCm9ZWYy0JH01CDVD9eq+voxQ1EcFJQkEIujVwoCNK0MNGuDK1ayjGxeDc4UD", + version = TraceLocation.VERSION + ).apply { + locationId.base64() shouldBe expectedLocationIdBase64 + locationId.hex() shouldBe expectedLocationIdHex + } + + // Full circle: Payload -> tracelocation -> qrpayload -> bytearray -> locationId + qrCodePayload.traceLocation().apply { + description shouldBe "Apple Computer, Inc." + locationId.base64() shouldBe expectedLocationIdBase64 + locationId.hex() shouldBe expectedLocationIdHex + } + + // ByteArrays actually match, we construct the data the same way. + qrCodePayload.toByteArray() shouldBe traceLocation.qrCodePayload().toByteArray() + } + + @Test + fun `mock server test data - 2`() { + val qrCodePayloadBase64 = + "CAESKQgBEhRXYXN0ZSBNYW5hZ2VtZW50IEluYxoPMzc5IE96Zm9jIE1hbm9yGnYIARJgOMTa6eYSiaDv8lW13xdYEvGHOZ1EYTiFSxt51HEoPCD7CNnvCUiIYPhax1MpkN0UfNClCm9ZWYy0JH01CDVD9eq-voxQ1EcFJQkEIujVwoCNK0MNGuDK1ayjGxeDc4UDGhCUpoAjtgZJPU4suuQZ6dVUIgcIARAIGJ4E" + val expectedLocationIdHex = "2c4e7a3be61004ed952cc189e85039e01be65f3d82439c8c7fe0f23b12ffa523" + val expectedLocationIdBase64 = "LE56O+YQBO2VLMGJ6FA54BvmXz2CQ5yMf+DyOxL/pSM=" + + // Payload -> locationId (direct from raw) + val payloadBytes = qrCodePayloadBase64.decodeBase64()!!.toByteArray() + val totalByteSequence = "CWA-GUID".toByteArray() + payloadBytes + totalByteSequence.toByteString().sha256().base64() shouldBe expectedLocationIdBase64 + + // TraceLocation -> locationId (manual reconstruction) + val traceLocation = TraceLocation( + type = TraceLocationOuterClass.TraceLocationType.LOCATION_TYPE_PERMANENT_PUBLIC_BUILDING, + description = "Waste Management Inc", + address = "379 Ozfoc Manor", + startDate = null, + endDate = null, + defaultCheckInLengthInMinutes = 542, + cryptographicSeed = "lKaAI7YGST1OLLrkGenVVA==".decodeBase64()!!, + cnPublicKey = "OMTa6eYSiaDv8lW13xdYEvGHOZ1EYTiFSxt51HEoPCD7CNnvCUiIYPhax1MpkN0UfNClCm9ZWYy0JH01CDVD9eq+voxQ1EcFJQkEIujVwoCNK0MNGuDK1ayjGxeDc4UD", + version = TraceLocation.VERSION + ).apply { + locationId.base64() shouldBe expectedLocationIdBase64 + locationId.hex() shouldBe expectedLocationIdHex + } + + // Full circle: Payload -> tracelocation -> qrpayload -> bytearray -> locationId + val qrCodePayload = TraceLocationOuterClass.QRCodePayload.parseFrom( + qrCodePayloadBase64.decodeBase64()!!.toByteArray() + ) + + qrCodePayload.traceLocation().apply { + description shouldBe "Waste Management Inc" + locationId.base64() shouldBe expectedLocationIdBase64 + locationId.hex() shouldBe expectedLocationIdHex + } + + // ByteArrays actually match, we construct the data the same way. + qrCodePayload.toByteArray() shouldBe traceLocation.qrCodePayload().toByteArray() + } + + @Test + fun `mock server test data - 3`() { + val qrCodePayloadBase64 = + "CAESMggBEg9NZXRhbHMgVVNBIEluYy4aETk2NiBEaXZ1ZCBIZWlnaHRzKLD6qYMGMNDZtoMGGnYIARJgOMTa6eYSiaDv8lW13xdYEvGHOZ1EYTiFSxt51HEoPCD7CNnvCUiIYPhax1MpkN0UfNClCm9ZWYy0JH01CDVD9eq-voxQ1EcFJQkEIujVwoCNK0MNGuDK1ayjGxeDc4UDGhD_mjsnT4b74d8M1P4cUflpIgcIARAKGOkJ" + val expectedLocationIdHex = "7051e04206ef9caf3a3165e82d0fec4cfe7ade770a2e01d9c0e456add760934d" + val expectedLocationIdBase64 = "cFHgQgbvnK86MWXoLQ/sTP563ncKLgHZwORWrddgk00=" + + // Payload -> locationId (direct from raw) + val payloadBytes = qrCodePayloadBase64.decodeBase64()!!.toByteArray() + val totalByteSequence = "CWA-GUID".toByteArray() + payloadBytes + totalByteSequence.toByteString().sha256().apply { + base64() shouldBe expectedLocationIdBase64 + } + + // TraceLocation -> locationId (manual reconstruction) + val traceLocation = TraceLocation( + type = TraceLocationOuterClass.TraceLocationType.LOCATION_TYPE_TEMPORARY_CLUB_ACTIVITY, + description = "Metals USA Inc.", + address = "966 Divud Heights", + startDate = Instant.ofEpochSecond(1617591600), + endDate = Instant.ofEpochSecond(1617800400), + defaultCheckInLengthInMinutes = 1257, + cryptographicSeed = "/5o7J0+G++HfDNT+HFH5aQ==".decodeBase64()!!, + cnPublicKey = "OMTa6eYSiaDv8lW13xdYEvGHOZ1EYTiFSxt51HEoPCD7CNnvCUiIYPhax1MpkN0UfNClCm9ZWYy0JH01CDVD9eq+voxQ1EcFJQkEIujVwoCNK0MNGuDK1ayjGxeDc4UD", + version = TraceLocation.VERSION + ).apply { + locationId.base64() shouldBe expectedLocationIdBase64 + locationId.hex() shouldBe expectedLocationIdHex + } + + // Full circle: Payload -> tracelocation -> qrpayload -> bytearray -> locationId + val qrCodePayload = TraceLocationOuterClass.QRCodePayload.parseFrom( + qrCodePayloadBase64.decodeBase64()!!.toByteArray() + ) + + qrCodePayload.traceLocation().apply { + description shouldBe "Metals USA Inc." + locationId.base64() shouldBe expectedLocationIdBase64 + locationId.hex() shouldBe expectedLocationIdHex + } + + // ByteArrays actually match, we construct the data the same way. + qrCodePayload.toByteArray() shouldBe traceLocation.qrCodePayload().toByteArray() + } +} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/events/TraceLocationUrlTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/events/TraceLocationUrlTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..aee6965e78df1ed7e771ec7b6bb041c3fbaa6ec6 --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/events/TraceLocationUrlTest.kt @@ -0,0 +1,62 @@ +package de.rki.coronawarnapp.eventregistration.events + +import de.rki.coronawarnapp.eventregistration.checkins.qrcode.TraceLocation +import de.rki.coronawarnapp.server.protocols.internal.pt.TraceLocationOuterClass +import de.rki.coronawarnapp.util.TimeAndDateExtensions.secondsToInstant +import io.kotest.matchers.shouldBe +import kotlinx.coroutines.test.runBlockingTest +import okio.ByteString.Companion.decodeBase64 +import org.junit.jupiter.api.Test +import testhelpers.BaseTest + +class TraceLocationUrlTest : BaseTest() { + + @Test + fun `locationUrl 1`() = runBlockingTest { + val traceLocation = TraceLocation( + id = 1, + type = TraceLocationOuterClass.TraceLocationType.LOCATION_TYPE_TEMPORARY_OTHER, + description = "My Birthday Party", + address = "at my place", + startDate = 2687955L.secondsToInstant(), + endDate = 2687991L.secondsToInstant(), + defaultCheckInLengthInMinutes = null, + cryptographicSeed = CRYPTOGRAPHIC_SEED.decodeBase64()!!, + cnPublicKey = PUB_KEY, + version = TraceLocation.VERSION + ) + + traceLocation.locationUrl shouldBe + "https://e.coronawarn.app?v=1#CAESLAgBEhFNeSBCaXJ0aGRheSBQYXJ0eRoLYXQgbXkgcGxhY2Uo04ekATD3h6QBGmoIAR" + + "JgOMTa6eYSiaDv8lW13xdYEvGHOZ1EYTiFSxt51HEoPCD7CNnvCUiIYPhax1MpkN0UfNClCm9ZWYy0JH01CDVD9eq-voxQ1EcFJ" + + "QkEIujVwoCNK0MNGuDK1ayjGxeDc4UDGgQxMjM0IgQIARAC" + } + + @Test + fun `locationUrl 2`() = runBlockingTest { + val traceLocation = TraceLocation( + id = 2, + type = TraceLocationOuterClass.TraceLocationType.LOCATION_TYPE_PERMANENT_OTHER, + description = "Icecream Shop", + address = "Main Street 1", + startDate = null, + endDate = null, + defaultCheckInLengthInMinutes = 10, + cryptographicSeed = CRYPTOGRAPHIC_SEED.decodeBase64()!!, + cnPublicKey = PUB_KEY, + version = TraceLocation.VERSION + ) + + traceLocation.locationUrl shouldBe + "https://e.coronawarn.app?v=1#CAESIAgBEg1JY2VjcmVhbSBTaG9wGg1NYWluIFN0cmVldCAxGmoIARJgOMTa6eYSiaDv8l" + + "W13xdYEvGHOZ1EYTiFSxt51HEoPCD7CNnvCUiIYPhax1MpkN0UfNClCm9ZWYy0JH01CDVD9eq-voxQ1EcFJQkEIujVwoCNK0MNG" + + "uDK1ayjGxeDc4UDGgQxMjM0IgYIARABGAo" + } + + companion object { + private const val CRYPTOGRAPHIC_SEED = "MTIzNA==" + private const val PUB_KEY = + "OMTa6eYSiaDv8lW13xdYEvGHOZ1EYTiFSxt51HEoPCD7CNnvCUiIYPhax1MpkN0UfNClCm9ZWYy0JH01CDVD9" + + "eq+voxQ1EcFJQkEIujVwoCNK0MNGuDK1ayjGxeDc4UD" + } +} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/events/server/qrcodepostertemplate/DefaultQrCodePosterTemplateTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/events/server/qrcodepostertemplate/DefaultQrCodePosterTemplateTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..bac5aeb8f0611dc7c43bab07185fb0d492ea00df --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/events/server/qrcodepostertemplate/DefaultQrCodePosterTemplateTest.kt @@ -0,0 +1,39 @@ +package de.rki.coronawarnapp.eventregistration.events.server.qrcodepostertemplate + +import android.content.Context +import android.os.Build +import androidx.test.core.app.ApplicationProvider +import de.rki.coronawarnapp.util.HashExtensions.toSHA256 +import io.kotest.matchers.shouldBe +import io.mockk.MockKAnnotations +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config +import testhelpers.BaseTest +import testhelpers.EmptyApplication + +@Config(sdk = [Build.VERSION_CODES.P], application = EmptyApplication::class) +@RunWith(RobolectricTestRunner::class) +class DefaultQrCodePosterTemplateTest : BaseTest() { + + private val templateName = "default_qr_code_poster_template_android.bin" + private val checkSumName = "default_qr_code_poster_template_android.sha256" + + @Before + fun setup() { + MockKAnnotations.init(this) + } + + val context: Context + get() = ApplicationProvider.getApplicationContext() + + @Test + fun `current default matches checksum`() { + val template = context.assets.open(templateName).readBytes() + val sha256 = context.assets.open(checkSumName).readBytes().toString(Charsets.UTF_8) + sha256 shouldBe "1e972018100828aa63bc2559713cffa22d8db62f3ce56b3edbe72ad8cb7adb16" + template.toSHA256() shouldBe sha256 + } +} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/events/server/qrcodepostertemplate/QrCodePosterTemplateServerTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/events/server/qrcodepostertemplate/QrCodePosterTemplateServerTest.kt index 6f2a1b4b719753cf830a3f455dcfb938cc6a9b3d..15083e52e544f6bc9586f5963ca5f7ba1071eb78 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/events/server/qrcodepostertemplate/QrCodePosterTemplateServerTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/events/server/qrcodepostertemplate/QrCodePosterTemplateServerTest.kt @@ -20,6 +20,7 @@ internal class QrCodePosterTemplateServerTest : BaseTest() { @MockK lateinit var api: QrCodePosterTemplateApiV1 @MockK lateinit var signatureValidation: SignatureValidation + @MockK lateinit var defaultTemplateSource: DefaultQrCodePosterTemplateSource /** * Info: [QrCodePosterTemplateApiV1Test] is testing if the ETag is set correctly @@ -30,11 +31,13 @@ internal class QrCodePosterTemplateServerTest : BaseTest() { MockKAnnotations.init(this) every { signatureValidation.hasValidSignature(any(), any()) } returns true + every { defaultTemplateSource.getDefaultQrCodePosterTemplate() } returns "CACHE".toByteArray() } private fun createInstance() = QrCodePosterTemplateServer( api = api, - signatureValidation = signatureValidation + signatureValidation = signatureValidation, + defaultTemplateSource = defaultTemplateSource ) @Test @@ -45,12 +48,12 @@ internal class QrCodePosterTemplateServerTest : BaseTest() { createInstance().downloadQrCodePosterTemplate().apply { template.toStringUtf8().substring(0, 22) shouldBe "<vector xmlns:android=" - offsetX shouldBe 10 - offsetY shouldBe 10 + offsetX shouldBe 10.0f + offsetY shouldBe 10.0f qrCodeSideLength shouldBe 100 with(descriptionTextBox) { - offsetX shouldBe 10 - offsetY shouldBe 50 + offsetX shouldBe 0.0f + offsetY shouldBe 0.0f width shouldBe 100 height shouldBe 20 fontSize shouldBe 10 @@ -62,16 +65,14 @@ internal class QrCodePosterTemplateServerTest : BaseTest() { } @Test - fun `should throw exception if signature is invalid`() = runBlockingTest { + fun `should fallback to cached or default template if signature is invalid`() = runBlockingTest { every { signatureValidation.hasValidSignature(any(), any()) } returns false coEvery { api.getQrCodePosterTemplate() } returns Response.success(POSTER_BUNDLE.toResponseBody()) - shouldThrow<QrCodePosterTemplateInvalidResponseException> { - createInstance().downloadQrCodePosterTemplate() - } + createInstance().getTemplateFromApiOrCache() shouldBe "CACHE".toByteArray() } @Test @@ -86,13 +87,12 @@ internal class QrCodePosterTemplateServerTest : BaseTest() { } @Test - fun `should return default poster template when response is not successful`() = runBlockingTest { - // TODO - } + fun `should fallback to cached or default template when response is not successful`() = runBlockingTest { + coEvery { + api.getQrCodePosterTemplate() + } returns Response.error(404, "ERROR".toResponseBody()) - @Test - fun `should return latest cached template when response is not successful`() = runBlockingTest { - // TODO + createInstance().getTemplateFromApiOrCache() shouldBe "CACHE".toByteArray() } companion object { @@ -127,6 +127,7 @@ internal class QrCodePosterTemplateServerTest : BaseTest() { "</vector>\n""")) .build()*/ + // TODO update this bundle to send PDF file not XML private val POSTER_BUNDLE = ( "504b03040a000000080014867d52008c85fefb000000ab0100000a0000006578706f72742e62" + "696e7d90cf4bc33014c7071e949c0415bc0825bbc8685f9676edbad216440fbb78f61c9a6a8ae912da9089ff80ffb66957700e" + diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/storage/retention/CheckInCleanerTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/storage/retention/CheckInCleanerTest.kt index 0dc18d47738d237a87375ba55922621dbe29814a..c7661962153098c69e209b30e0ed0f02563f3cb5 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/storage/retention/CheckInCleanerTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/storage/retention/CheckInCleanerTest.kt @@ -71,7 +71,6 @@ internal class CheckInCleanerTest : BaseTest() { private fun createCheckIn(checkOutDate: Instant) = CheckIn( traceLocationId = "traceLocationId1".encode(), - traceLocationIdHash = "traceLocationIdHash1".encode(), version = 1, type = 1, description = "", diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/checkins/checkout/CheckOutHandlerTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/checkins/checkout/CheckOutHandlerTest.kt index e420e844c1b1e06c1880db9355d784c32ebcad02..04fbb3a38e611423435fd717cc6e31d76f76e096 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/checkins/checkout/CheckOutHandlerTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/checkins/checkout/CheckOutHandlerTest.kt @@ -25,7 +25,6 @@ class CheckOutHandlerTest : BaseTest() { private val testCheckIn = CheckIn( id = 42L, traceLocationId = "traceLocationId1".encode(), - traceLocationIdHash = "traceLocationIdHash1".encode(), version = 1, type = 1, description = "Restaurant", diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/checkins/checkout/auto/AutoCheckOutTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/checkins/checkout/auto/AutoCheckOutTest.kt index 41c1f47c9691f7f5051f9d7c976e79754d490eda..4958a8cb294eb4916e23de2f9ca1fe5d385ccd3f 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/checkins/checkout/auto/AutoCheckOutTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/checkins/checkout/auto/AutoCheckOutTest.kt @@ -38,7 +38,6 @@ class AutoCheckOutTest : BaseTest() { private val baseCheckin = CheckIn( id = 0L, traceLocationId = "traceLocationId1".encode(), - traceLocationIdHash = "traceLocationIdHash1".encode(), version = 1, type = 2, description = "brothers birthday", diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/common/TraceLocationNotificationsTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/common/TraceLocationNotificationsTest.kt index 92c141796ec390d5d61f83b87ebec7c51474608f..f87f49945884faad710d647b00742aa3b4dfcd6c 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/common/TraceLocationNotificationsTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/common/TraceLocationNotificationsTest.kt @@ -37,7 +37,7 @@ class TraceLocationNotificationsTest : BaseTest() { } } - fun createInstance() = TraceLocationNotifications( + fun createInstance() = PresenceTracingNotifications( context = context, apiLevel = apiLevel, notificationManagerCompat = notificationManager diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/CheckInWarningMatcherTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/CheckInWarningMatcherTest.kt deleted file mode 100644 index 1a3dcc0488041e444d3c299a2ac98abf1084791f..0000000000000000000000000000000000000000 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/CheckInWarningMatcherTest.kt +++ /dev/null @@ -1,255 +0,0 @@ -package de.rki.coronawarnapp.presencetracing.risk - -import de.rki.coronawarnapp.eventregistration.checkins.CheckInRepository -import de.rki.coronawarnapp.eventregistration.checkins.download.TraceTimeIntervalWarningPackage -import de.rki.coronawarnapp.eventregistration.checkins.download.TraceTimeIntervalWarningRepository -import de.rki.coronawarnapp.server.protocols.internal.pt.TraceWarning -import de.rki.coronawarnapp.util.debug.measureTime -import io.mockk.MockKAnnotations -import io.mockk.Runs -import io.mockk.coEvery -import io.mockk.coVerify -import io.mockk.every -import io.mockk.impl.annotations.MockK -import io.mockk.just -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 -import testhelpers.TestDispatcherProvider -import timber.log.Timber - -class CheckInWarningMatcherTest : BaseTest() { - - @MockK lateinit var checkInsRepository: CheckInRepository - @MockK lateinit var traceTimeIntervalWarningRepository: TraceTimeIntervalWarningRepository - @MockK lateinit var presenceTracingRiskRepository: PresenceTracingRiskRepository - - @BeforeEach - fun setup() { - MockKAnnotations.init(this) - coEvery { presenceTracingRiskRepository.reportSuccessfulCalculation(any()) } just Runs - coEvery { presenceTracingRiskRepository.deleteAllMatches() } just Runs - coEvery { presenceTracingRiskRepository.deleteStaleData() } just Runs - // TODO tests - coEvery { presenceTracingRiskRepository.deleteMatchesOfPackage(any()) } just Runs - coEvery { presenceTracingRiskRepository.markPackageProcessed(any()) } just Runs - } - - @Test - fun `reports new matches`() { - val checkIn1 = createCheckIn( - id = 2L, - traceLocationId = "fe84394e73838590cc7707aba0350c130f6d0fb6f0f2535f9735f481dee61871", - startDateStr = "2021-03-04T10:15+01:00", - endDateStr = "2021-03-04T10:17+01:00" - ) - val checkIn2 = createCheckIn( - id = 3L, - traceLocationId = "69eb427e1a48133970486244487e31b3f1c5bde47415db9b52cc5a2ece1e0060", - startDateStr = "2021-03-04T09:15+01:00", - endDateStr = "2021-03-04T10:12+01:00" - ) - - val warning1 = createWarning( - traceLocationId = "fe84394e73838590cc7707aba0350c130f6d0fb6f0f2535f9735f481dee61871", - startIntervalDateStr = "2021-03-04T10:00+01:00", - period = 6, - transmissionRiskLevel = 8 - ) - - val warning2 = createWarning( - traceLocationId = "69eb427e1a48133970486244487e31b3f1c5bde47415db9b52cc5a2ece1e0060", - startIntervalDateStr = "2021-03-04T10:00+01:00", - period = 6, - transmissionRiskLevel = 8 - ) - - every { checkInsRepository.allCheckIns } returns flowOf(listOf(checkIn1, checkIn2)) - - val warningPackage = object : TraceTimeIntervalWarningPackage { - override suspend fun extractTraceTimeIntervalWarnings(): List<TraceWarning.TraceTimeIntervalWarning> { - return listOf(warning1, warning2) - } - - override val warningPackageId: String - get() = "id" - } - - every { traceTimeIntervalWarningRepository.allWarningPackages } returns flowOf(listOf(warningPackage)) - - runBlockingTest { - createInstance().execute() - coVerify(exactly = 1) { presenceTracingRiskRepository.reportSuccessfulCalculation(any()) } - coVerify(exactly = 0) { presenceTracingRiskRepository.deleteAllMatches() } - } - } - - @Test - fun `report empty list if no matches found`() { - val checkIn1 = createCheckIn( - id = 2L, - traceLocationId = "fe84394e73838590cc7707aba0350c130f6d0fb6f0f2535f9735f481dee61871", - startDateStr = "2021-03-04T10:15+01:00", - endDateStr = "2021-03-04T10:17+01:00" - ) - val checkIn2 = createCheckIn( - id = 3L, - traceLocationId = "fe84394e73838590cc7707aba0350c130f6d0fb6f0f2535f9735f481dee61871", - startDateStr = "2021-03-04T09:15+01:00", - endDateStr = "2021-03-04T10:12+01:00" - ) - - val warning1 = createWarning( - traceLocationId = "69eb427e1a48133970486244487e31b3f1c5bde47415db9b52cc5a2ece1e0060", - startIntervalDateStr = "2021-03-04T10:00+01:00", - period = 6, - transmissionRiskLevel = 8 - ) - - val warning2 = createWarning( - traceLocationId = "69eb427e1a48133970486244487e31b3f1c5bde47415db9b52cc5a2ece1e0060", - startIntervalDateStr = "2021-03-04T10:00+01:00", - period = 6, - transmissionRiskLevel = 8 - ) - - every { checkInsRepository.allCheckIns } returns flowOf(listOf(checkIn1, checkIn2)) - - val warningPackage = object : TraceTimeIntervalWarningPackage { - override suspend fun extractTraceTimeIntervalWarnings(): List<TraceWarning.TraceTimeIntervalWarning> { - return listOf(warning1, warning2) - } - - override val warningPackageId: String - get() = "id" - } - - every { traceTimeIntervalWarningRepository.allWarningPackages } returns flowOf(listOf(warningPackage)) - - runBlockingTest { - createInstance().execute() - coVerify(exactly = 1) { presenceTracingRiskRepository.reportSuccessfulCalculation(emptyList()) } - coVerify(exactly = 0) { presenceTracingRiskRepository.deleteAllMatches() } - } - } - - @Test - fun `report empty list if package is empty`() { - val checkIn1 = createCheckIn( - id = 2L, - traceLocationId = "fe84394e73838590cc7707aba0350c130f6d0fb6f0f2535f9735f481dee61871", - startDateStr = "2021-03-04T10:15+01:00", - endDateStr = "2021-03-04T10:17+01:00" - ) - val checkIn2 = createCheckIn( - id = 3L, - traceLocationId = "fe84394e73838590cc7707aba0350c130f6d0fb6f0f2535f9735f481dee61871", - startDateStr = "2021-03-04T09:15+01:00", - endDateStr = "2021-03-04T10:12+01:00" - ) - - every { checkInsRepository.allCheckIns } returns flowOf(listOf(checkIn1, checkIn2)) - - val warningPackage = object : TraceTimeIntervalWarningPackage { - override suspend fun extractTraceTimeIntervalWarnings(): List<TraceWarning.TraceTimeIntervalWarning> { - return listOf() - } - - override val warningPackageId: String - get() = "id" - } - - every { traceTimeIntervalWarningRepository.allWarningPackages } returns flowOf(listOf(warningPackage)) - - runBlockingTest { - createInstance().execute() - coVerify(exactly = 1) { presenceTracingRiskRepository.reportSuccessfulCalculation(emptyList()) } - coVerify(exactly = 0) { presenceTracingRiskRepository.deleteAllMatches() } - } - } - - @Test - fun `deletes all matches if no check-ins`() { - - val warning1 = createWarning( - traceLocationId = "69eb427e1a48133970486244487e31b3f1c5bde47415db9b52cc5a2ece1e0060", - startIntervalDateStr = "2021-03-04T10:00+01:00", - period = 6, - transmissionRiskLevel = 8 - ) - - val warning2 = createWarning( - traceLocationId = "69eb427e1a48133970486244487e31b3f1c5bde47415db9b52cc5a2ece1e0060", - startIntervalDateStr = "2021-03-04T10:00+01:00", - period = 6, - transmissionRiskLevel = 8 - ) - - every { checkInsRepository.allCheckIns } returns flowOf(listOf()) - - val warningPackage = object : TraceTimeIntervalWarningPackage { - override suspend fun extractTraceTimeIntervalWarnings(): List<TraceWarning.TraceTimeIntervalWarning> { - return listOf(warning1, warning2) - } - - override val warningPackageId: String - get() = "id" - } - - every { traceTimeIntervalWarningRepository.allWarningPackages } returns flowOf(listOf(warningPackage)) - - runBlockingTest { - createInstance().execute() - coVerify(exactly = 1) { presenceTracingRiskRepository.reportSuccessfulCalculation(emptyList()) } - coVerify(exactly = 1) { presenceTracingRiskRepository.deleteAllMatches() } - } - } - - @Test - fun `test mass data`() { - val checkIns = (1L..100L).map { - createCheckIn( - id = it, - traceLocationId = it.toString(), - startDateStr = "2021-03-04T09:50+01:00", - endDateStr = "2021-03-04T10:05:15+01:00" - ) - } - val warnings = (1L..1000L).map { - createWarning( - traceLocationId = it.toString(), - startIntervalDateStr = "2021-03-04T10:00+01:00", - period = 6, - transmissionRiskLevel = 8 - ) - } - - val warningPackage = object : TraceTimeIntervalWarningPackage { - override suspend fun extractTraceTimeIntervalWarnings(): List<TraceWarning.TraceTimeIntervalWarning> { - return warnings - } - - override val warningPackageId: String - get() = "id" - } - - every { checkInsRepository.allCheckIns } returns flowOf(checkIns) - every { traceTimeIntervalWarningRepository.allWarningPackages } returns flowOf(listOf(warningPackage)) - - runBlockingTest { - measureTime( - { Timber.d("Time to compare 200 checkIns with 1000 warnings: $it millis") }, - { createInstance().execute() } - ) - } - } - - private fun createInstance() = CheckInWarningMatcher( - checkInsRepository, - traceTimeIntervalWarningRepository, - presenceTracingRiskRepository, - TestDispatcherProvider() - ) -} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/calculation/CheckInWarningMatcherTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/calculation/CheckInWarningMatcherTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..b8beb8b1106ba1b21dc47c0596ea523728adc571 --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/calculation/CheckInWarningMatcherTest.kt @@ -0,0 +1,255 @@ +package de.rki.coronawarnapp.presencetracing.risk.calculation + +import de.rki.coronawarnapp.presencetracing.warning.WarningPackageId +import de.rki.coronawarnapp.presencetracing.warning.storage.TraceWarningPackage +import de.rki.coronawarnapp.server.protocols.internal.pt.TraceWarning +import io.kotest.matchers.shouldBe +import io.mockk.MockKAnnotations +import kotlinx.coroutines.test.runBlockingTest +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import testhelpers.BaseTest +import testhelpers.TestDispatcherProvider + +class CheckInWarningMatcherTest : BaseTest() { + + @BeforeEach + fun setup() { + MockKAnnotations.init(this) + } + + private fun createInstance() = CheckInWarningMatcher( + TestDispatcherProvider() + ) + + @Test + fun `reports new matches`() { + val checkIn1 = createCheckIn( + id = 2L, + traceLocationId = "fe84394e73838590cc7707aba0350c130f6d0fb6f0f2535f9735f481dee61871", + startDateStr = "2021-03-04T10:15+01:00", + endDateStr = "2021-03-04T10:17+01:00" + ) + val checkIn2 = createCheckIn( + id = 3L, + traceLocationId = "69eb427e1a48133970486244487e31b3f1c5bde47415db9b52cc5a2ece1e0060", + startDateStr = "2021-03-04T09:15+01:00", + endDateStr = "2021-03-04T10:12+01:00" + ) + + val warning1 = createWarning( + traceLocationId = "fe84394e73838590cc7707aba0350c130f6d0fb6f0f2535f9735f481dee61871", + startIntervalDateStr = "2021-03-04T10:00+01:00", + period = 6, + transmissionRiskLevel = 8 + ) + + val warning2 = createWarning( + traceLocationId = "69eb427e1a48133970486244487e31b3f1c5bde47415db9b52cc5a2ece1e0060", + startIntervalDateStr = "2021-03-04T10:00+01:00", + period = 6, + transmissionRiskLevel = 8 + ) + + val warningPackage = object : TraceWarningPackage { + override suspend fun extractWarnings(): List<TraceWarning.TraceTimeIntervalWarning> { + return listOf(warning1, warning2) + } + + override val packageId: WarningPackageId + get() = "id" + } + + runBlockingTest { + val result = createInstance().process( + checkIns = listOf(checkIn1, checkIn2), + warningPackages = listOf(warningPackage) + ) + result.apply { + successful shouldBe true + processedPackages.single().warningPackage shouldBe warningPackage + processedPackages.single().apply { + overlaps.size shouldBe 2 + overlaps.any { it.checkInId == 2L } shouldBe true + overlaps.any { it.checkInId == 3L } shouldBe true + } + } + } + } + + @Test + fun `report empty list if no matches found`() { + val checkIn1 = createCheckIn( + id = 2L, + traceLocationId = "fe84394e73838590cc7707aba0350c130f6d0fb6f0f2535f9735f481dee61871", + startDateStr = "2021-03-04T10:15+01:00", + endDateStr = "2021-03-04T10:17+01:00" + ) + val checkIn2 = createCheckIn( + id = 3L, + traceLocationId = "fe84394e73838590cc7707aba0350c130f6d0fb6f0f2535f9735f481dee61871", + startDateStr = "2021-03-04T09:15+01:00", + endDateStr = "2021-03-04T10:12+01:00" + ) + + val warning1 = createWarning( + traceLocationId = "69eb427e1a48133970486244487e31b3f1c5bde47415db9b52cc5a2ece1e0060", + startIntervalDateStr = "2021-03-04T10:00+01:00", + period = 6, + transmissionRiskLevel = 8 + ) + + val warning2 = createWarning( + traceLocationId = "69eb427e1a48133970486244487e31b3f1c5bde47415db9b52cc5a2ece1e0060", + startIntervalDateStr = "2021-03-04T10:00+01:00", + period = 6, + transmissionRiskLevel = 8 + ) + + val warningPackage = object : TraceWarningPackage { + override suspend fun extractWarnings(): List<TraceWarning.TraceTimeIntervalWarning> { + return listOf(warning1, warning2) + } + + override val packageId: WarningPackageId + get() = "id" + } + + runBlockingTest { + val result = createInstance().process( + checkIns = listOf(checkIn1, checkIn2), + warningPackages = listOf(warningPackage), + ) + + result.apply { + successful shouldBe true + processedPackages.single().warningPackage shouldBe warningPackage + processedPackages.single().overlaps.size shouldBe 0 + } + } + } + + @Test + fun `report empty list if package is empty`() { + val checkIn1 = createCheckIn( + id = 2L, + traceLocationId = "fe84394e73838590cc7707aba0350c130f6d0fb6f0f2535f9735f481dee61871", + startDateStr = "2021-03-04T10:15+01:00", + endDateStr = "2021-03-04T10:17+01:00" + ) + val checkIn2 = createCheckIn( + id = 3L, + traceLocationId = "fe84394e73838590cc7707aba0350c130f6d0fb6f0f2535f9735f481dee61871", + startDateStr = "2021-03-04T09:15+01:00", + endDateStr = "2021-03-04T10:12+01:00" + ) + + val warningPackage = object : TraceWarningPackage { + override suspend fun extractWarnings(): List<TraceWarning.TraceTimeIntervalWarning> { + return listOf() + } + + override val packageId: WarningPackageId + get() = "id" + } + + runBlockingTest { + val result = createInstance().process( + warningPackages = listOf(warningPackage), + checkIns = listOf(checkIn1, checkIn2), + ) + + result.apply { + successful shouldBe true + processedPackages.single().warningPackage shouldBe warningPackage + processedPackages.single().overlaps.size shouldBe 0 + } + } + } + + @Test + fun `report failure if matching throws exception`() { + val checkIn1 = createCheckIn( + id = 2L, + traceLocationId = "fe84394e73838590cc7707aba0350c130f6d0fb6f0f2535f9735f481dee61871", + startDateStr = "2021-03-04T10:15+01:00", + endDateStr = "2021-03-04T10:17+01:00" + ) + val checkIn2 = createCheckIn( + id = 3L, + traceLocationId = "69eb427e1a48133970486244487e31b3f1c5bde47415db9b52cc5a2ece1e0060", + startDateStr = "2021-03-04T09:15+01:00", + endDateStr = "2021-03-04T10:12+01:00" + ) + + val warningPackage = object : TraceWarningPackage { + override suspend fun extractWarnings(): List<TraceWarning.TraceTimeIntervalWarning> { + throw Exception() + } + + override val packageId: String + get() = "id" + } + + runBlockingTest { + val result = createInstance().process( + checkIns = listOf(checkIn1, checkIn2), + warningPackages = listOf(warningPackage), + ) + + result.apply { + successful shouldBe false + processedPackages shouldBe emptyList() + } + } + } + + @Test + fun `partial processing is possible on exceptions`() { + val checkIn1 = createCheckIn( + id = 2L, + traceLocationId = "fe84394e73838590cc7707aba0350c130f6d0fb6f0f2535f9735f481dee61871", + startDateStr = "2021-03-04T10:15+01:00", + endDateStr = "2021-03-04T10:17+01:00" + ) + val checkIn2 = createCheckIn( + id = 3L, + traceLocationId = "69eb427e1a48133970486244487e31b3f1c5bde47415db9b52cc5a2ece1e0060", + startDateStr = "2021-03-04T09:15+01:00", + endDateStr = "2021-03-04T10:12+01:00" + ) + + val warningPackage1 = object : TraceWarningPackage { + override suspend fun extractWarnings() = throw Exception() + + override val packageId: WarningPackageId = "id1" + } + val warningPackage2 = object : TraceWarningPackage { + override suspend fun extractWarnings() = listOf( + createWarning( + traceLocationId = "69eb427e1a48133970486244487e31b3f1c5bde47415db9b52cc5a2ece1e0060", + startIntervalDateStr = "2021-03-04T10:00+01:00", + period = 6, + transmissionRiskLevel = 8 + ) + ) + + override val packageId: WarningPackageId = "id2" + } + + runBlockingTest { + val result = createInstance().process( + checkIns = listOf(checkIn1, checkIn2), + warningPackages = listOf(warningPackage1, warningPackage2), + ) + + result.apply { + successful shouldBe false + processedPackages.single().apply { + warningPackage shouldBe warningPackage2 + overlaps.single().checkInId shouldBe checkIn2.id + } + } + } + } +} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/FindMatchesTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/calculation/FindMatchesTest.kt similarity index 79% rename from Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/FindMatchesTest.kt rename to Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/calculation/FindMatchesTest.kt index cc9d5e58028909f8a18c7185d3cf704340f61f87..3ddd887cdcf12de4c9a628bf24d0498350e62840 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/FindMatchesTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/calculation/FindMatchesTest.kt @@ -1,6 +1,7 @@ -package de.rki.coronawarnapp.presencetracing.risk +package de.rki.coronawarnapp.presencetracing.risk.calculation -import de.rki.coronawarnapp.eventregistration.checkins.download.TraceTimeIntervalWarningPackage +import de.rki.coronawarnapp.presencetracing.warning.WarningPackageId +import de.rki.coronawarnapp.presencetracing.warning.storage.TraceWarningPackage import de.rki.coronawarnapp.server.protocols.internal.pt.TraceWarning import io.kotest.matchers.shouldBe import kotlinx.coroutines.test.runBlockingTest @@ -36,11 +37,12 @@ class FindMatchesTest { period = 6, transmissionRiskLevel = 8 ) - val warningPackage = object : TraceTimeIntervalWarningPackage { - override suspend fun extractTraceTimeIntervalWarnings(): List<TraceWarning.TraceTimeIntervalWarning> { + val warningPackage = object : TraceWarningPackage { + override suspend fun extractWarnings(): List<TraceWarning.TraceTimeIntervalWarning> { return listOf(warning1, warning2) } - override val warningPackageId: String + + override val packageId: WarningPackageId get() = "id" } runBlockingTest { diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/OverlapTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/calculation/OverlapTest.kt similarity index 72% rename from Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/OverlapTest.kt rename to Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/calculation/OverlapTest.kt index fa1af775d15e0d2b44a4281f642e9a3d828f738a..67cdb5a057bde725542c8cd8712ade2d4aeb3ba0 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/OverlapTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/calculation/OverlapTest.kt @@ -1,24 +1,51 @@ -package de.rki.coronawarnapp.presencetracing.risk +package de.rki.coronawarnapp.presencetracing.risk.calculation -import com.google.protobuf.ByteString.copyFromUtf8 import de.rki.coronawarnapp.eventregistration.checkins.CheckIn import de.rki.coronawarnapp.server.protocols.internal.pt.TraceWarning -import de.rki.coronawarnapp.util.HashExtensions.toSHA256 +import de.rki.coronawarnapp.util.toOkioByteString +import de.rki.coronawarnapp.util.toProtoByteString import io.kotest.matchers.shouldBe +import okio.ByteString.Companion.decodeHex import okio.ByteString.Companion.encode import org.joda.time.Duration import org.joda.time.Instant -import org.junit.Test +import org.junit.jupiter.api.Test import testhelpers.BaseTest class OverlapTest : BaseTest() { - val id = "id" + private val id = "id" + + private val locationId = "afa27b44d43b02a9fea41d13cedc2e4016cfcf87c5dbf990e593669aa8ce286d" + private val locationIdHash = "0f37dac11d1b8118ea0b44303400faa5e3b876da9d758058b5ff7dc2e5da8230" + + @Test + fun `test helper method createCheckIn`() { + val checkIn = createCheckIn( + traceLocationId = locationId, + startDateStr = "2021-03-04T09:30+01:00", + endDateStr = "2021-03-04T09:45+01:00" + ) + + checkIn.traceLocationId shouldBe locationId.decodeHex() + checkIn.traceLocationIdHash shouldBe locationIdHash.decodeHex() + } + + @Test + fun `test helper method createWarning`() { + val warning = createWarning( + traceLocationId = locationId, + startIntervalDateStr = "2021-03-04T10:00+01:00", + period = 6, + transmissionRiskLevel = 8 + ) + warning.locationIdHash.toOkioByteString().hex() shouldBe locationIdHash + } @Test fun `returns null if guids do not match`() { createCheckIn( - traceLocationId = "fe84394e73838590cc7707aba0350c130f6d0fb6f0f2535f9735f481dee61871", + traceLocationId = locationId, startDateStr = "2021-03-04T09:30+01:00", endDateStr = "2021-03-04T09:45+01:00" ).calculateOverlap( @@ -35,12 +62,12 @@ class OverlapTest : BaseTest() { @Test fun `returns null if check-in precedes warning`() { createCheckIn( - traceLocationId = "fe84394e73838590cc7707aba0350c130f6d0fb6f0f2535f9735f481dee61871", + traceLocationId = locationId, startDateStr = "2021-03-04T09:30+01:00", endDateStr = "2021-03-04T09:45+01:00" ).calculateOverlap( createWarning( - traceLocationId = "fe84394e73838590cc7707aba0350c130f6d0fb6f0f2535f9735f481dee61871", + traceLocationId = locationId, startIntervalDateStr = "2021-03-04T10:00+01:00", period = 6, transmissionRiskLevel = 8 @@ -52,12 +79,12 @@ class OverlapTest : BaseTest() { @Test fun `returns null if check-in is preceded by warning`() { createCheckIn( - traceLocationId = "fe84394e73838590cc7707aba0350c130f6d0fb6f0f2535f9735f481dee61871", + traceLocationId = locationId, startDateStr = "2021-03-04T11:15+01:00", endDateStr = "2021-03-04T11:20+01:00" ).calculateOverlap( createWarning( - traceLocationId = "fe84394e73838590cc7707aba0350c130f6d0fb6f0f2535f9735f481dee61871", + traceLocationId = locationId, startIntervalDateStr = "2021-03-04T10:00+01:00", period = 6, transmissionRiskLevel = 8 @@ -69,12 +96,12 @@ class OverlapTest : BaseTest() { @Test fun `returns null if check-in meets warning at the start`() { createCheckIn( - traceLocationId = "fe84394e73838590cc7707aba0350c130f6d0fb6f0f2535f9735f481dee61871", + traceLocationId = locationId, startDateStr = "2021-03-04T09:30+01:00", endDateStr = "2021-03-04T10:00+01:00" ).calculateOverlap( createWarning( - traceLocationId = "fe84394e73838590cc7707aba0350c130f6d0fb6f0f2535f9735f481dee61871", + traceLocationId = locationId, startIntervalDateStr = "2021-03-04T10:00+01:00", period = 6, transmissionRiskLevel = 8 @@ -86,12 +113,12 @@ class OverlapTest : BaseTest() { @Test fun `returns null if check-in meets warning at the end`() { createCheckIn( - traceLocationId = "fe84394e73838590cc7707aba0350c130f6d0fb6f0f2535f9735f481dee61871", + traceLocationId = locationId, startDateStr = "2021-03-04T11:00+01:00", endDateStr = "2021-03-04T11:10+01:00" ).calculateOverlap( createWarning( - traceLocationId = "fe84394e73838590cc7707aba0350c130f6d0fb6f0f2535f9735f481dee61871", + traceLocationId = locationId, startIntervalDateStr = "2021-03-04T10:00+01:00", period = 6, transmissionRiskLevel = 8 @@ -103,12 +130,12 @@ class OverlapTest : BaseTest() { @Test fun `returns overlap if check-in overlaps warning at the start`() { createCheckIn( - traceLocationId = "fe84394e73838590cc7707aba0350c130f6d0fb6f0f2535f9735f481dee61871", + traceLocationId = locationId, startDateStr = "2021-03-04T09:30+01:00", endDateStr = "2021-03-04T10:12+01:00" ).calculateOverlap( createWarning( - traceLocationId = "fe84394e73838590cc7707aba0350c130f6d0fb6f0f2535f9735f481dee61871", + traceLocationId = locationId, startIntervalDateStr = "2021-03-04T10:00+01:00", period = 6, transmissionRiskLevel = 8 @@ -120,12 +147,12 @@ class OverlapTest : BaseTest() { @Test fun `returns overlap if check-in overlaps warning at the end`() { createCheckIn( - traceLocationId = "fe84394e73838590cc7707aba0350c130f6d0fb6f0f2535f9735f481dee61871", + traceLocationId = locationId, startDateStr = "2021-03-04T10:45+01:00", endDateStr = "2021-03-04T11:12+01:00" ).calculateOverlap( createWarning( - traceLocationId = "fe84394e73838590cc7707aba0350c130f6d0fb6f0f2535f9735f481dee61871", + traceLocationId = locationId, startIntervalDateStr = "2021-03-04T10:00+01:00", period = 6, transmissionRiskLevel = 8 @@ -137,12 +164,12 @@ class OverlapTest : BaseTest() { @Test fun `returns overlap if check-in starts warning`() { createCheckIn( - traceLocationId = "fe84394e73838590cc7707aba0350c130f6d0fb6f0f2535f9735f481dee61871", + traceLocationId = locationId, startDateStr = "2021-03-04T10:00+01:00", endDateStr = "2021-03-04T10:13+01:00" ).calculateOverlap( createWarning( - traceLocationId = "fe84394e73838590cc7707aba0350c130f6d0fb6f0f2535f9735f481dee61871", + traceLocationId = locationId, startIntervalDateStr = "2021-03-04T10:00+01:00", period = 6, transmissionRiskLevel = 8 @@ -154,12 +181,12 @@ class OverlapTest : BaseTest() { @Test fun `returns overlap if check-in during warning`() { createCheckIn( - traceLocationId = "fe84394e73838590cc7707aba0350c130f6d0fb6f0f2535f9735f481dee61871", + traceLocationId = locationId, startDateStr = "2021-03-04T10:15+01:00", endDateStr = "2021-03-04T10:17+01:00" ).calculateOverlap( createWarning( - traceLocationId = "fe84394e73838590cc7707aba0350c130f6d0fb6f0f2535f9735f481dee61871", + traceLocationId = locationId, startIntervalDateStr = "2021-03-04T10:00+01:00", period = 6, transmissionRiskLevel = 8 @@ -171,12 +198,12 @@ class OverlapTest : BaseTest() { @Test fun `returns overlap if check-in finishes warning`() { createCheckIn( - traceLocationId = "fe84394e73838590cc7707aba0350c130f6d0fb6f0f2535f9735f481dee61871", + traceLocationId = locationId, startDateStr = "2021-03-04T10:30+01:00", endDateStr = "2021-03-04T11:00+01:00" ).calculateOverlap( createWarning( - traceLocationId = "fe84394e73838590cc7707aba0350c130f6d0fb6f0f2535f9735f481dee61871", + traceLocationId = locationId, startIntervalDateStr = "2021-03-04T10:00+01:00", period = 6, transmissionRiskLevel = 8 @@ -188,12 +215,12 @@ class OverlapTest : BaseTest() { @Test fun `returns overlap if check-in equals warning`() { createCheckIn( - traceLocationId = "fe84394e73838590cc7707aba0350c130f6d0fb6f0f2535f9735f481dee61871", + traceLocationId = locationId, startDateStr = "2021-03-04T10:00+01:00", endDateStr = "2021-03-04T11:00+01:00" ).calculateOverlap( createWarning( - traceLocationId = "fe84394e73838590cc7707aba0350c130f6d0fb6f0f2535f9735f481dee61871", + traceLocationId = locationId, startIntervalDateStr = "2021-03-04T10:00+01:00", period = 6, transmissionRiskLevel = 8 @@ -205,12 +232,12 @@ class OverlapTest : BaseTest() { @Test fun `returns overlap after rounding (up)`() { createCheckIn( - traceLocationId = "fe84394e73838590cc7707aba0350c130f6d0fb6f0f2535f9735f481dee61871", + traceLocationId = locationId, startDateStr = "2021-03-04T09:50+01:00", endDateStr = "2021-03-04T10:05:45+01:00" ).calculateOverlap( createWarning( - traceLocationId = "fe84394e73838590cc7707aba0350c130f6d0fb6f0f2535f9735f481dee61871", + traceLocationId = locationId, startIntervalDateStr = "2021-03-04T10:00+01:00", period = 6, transmissionRiskLevel = 8 @@ -222,12 +249,12 @@ class OverlapTest : BaseTest() { @Test fun `returns overlap after rounding (down)`() { createCheckIn( - traceLocationId = "fe84394e73838590cc7707aba0350c130f6d0fb6f0f2535f9735f481dee61871", + traceLocationId = locationId, startDateStr = "2021-03-04T09:50+01:00", endDateStr = "2021-03-04T10:05:15+01:00" ).calculateOverlap( createWarning( - traceLocationId = "fe84394e73838590cc7707aba0350c130f6d0fb6f0f2535f9735f481dee61871", + traceLocationId = locationId, startIntervalDateStr = "2021-03-04T10:00+01:00", period = 6, transmissionRiskLevel = 8 @@ -244,8 +271,7 @@ fun createCheckIn( endDateStr: String ) = CheckIn( id = id, - traceLocationId = traceLocationId.toSHA256().encode(), - traceLocationIdHash = traceLocationId.toSHA256().encode(), + traceLocationId = traceLocationId.decodeHex(), version = 1, type = 2, description = "My birthday party", @@ -266,8 +292,8 @@ fun createWarning( startIntervalDateStr: String, period: Int, transmissionRiskLevel: Int -) = TraceWarning.TraceTimeIntervalWarning.newBuilder() - .setLocationIdHash(copyFromUtf8(traceLocationId.toSHA256())) +): TraceWarning.TraceTimeIntervalWarning = TraceWarning.TraceTimeIntervalWarning.newBuilder() + .setLocationIdHash(traceLocationId.decodeHex().sha256().toProtoByteString()) .setPeriod(period) .setStartIntervalNumber((Duration(Instant.parse(startIntervalDateStr).millis).standardMinutes / 10).toInt()) .setTransmissionRiskLevel(transmissionRiskLevel) diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/PresenceTracingRiskCalculatorTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/calculation/PresenceTracingRiskCalculatorTest.kt similarity index 98% rename from Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/PresenceTracingRiskCalculatorTest.kt rename to Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/calculation/PresenceTracingRiskCalculatorTest.kt index 17c8b74c36daa1ad362f75f499299ef4b758650e..f59cd16228473db4a9530fa15857c09c758b048a 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/PresenceTracingRiskCalculatorTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/calculation/PresenceTracingRiskCalculatorTest.kt @@ -1,4 +1,4 @@ -package de.rki.coronawarnapp.presencetracing.risk +package de.rki.coronawarnapp.presencetracing.risk.calculation import de.rki.coronawarnapp.risk.RiskState import de.rki.coronawarnapp.util.TimeAndDateExtensions.toLocalDateUtc diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/PresenceTracingRiskMapperTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/calculation/PresenceTracingRiskMapperTest.kt similarity index 98% rename from Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/PresenceTracingRiskMapperTest.kt rename to Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/calculation/PresenceTracingRiskMapperTest.kt index e5334348dd56ede52dde06a5ab8106d777ac3d98..dd07536e331c18350214ed1ed23f8f70ca1898bf 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/PresenceTracingRiskMapperTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/calculation/PresenceTracingRiskMapperTest.kt @@ -1,4 +1,4 @@ -package de.rki.coronawarnapp.presencetracing.risk +package de.rki.coronawarnapp.presencetracing.risk.calculation import de.rki.coronawarnapp.appconfig.AppConfigProvider import de.rki.coronawarnapp.appconfig.ConfigData diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/execution/PresenceTracingWarningTaskTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/execution/PresenceTracingWarningTaskTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..e44656741f40654af7de030c9812598a591047f4 --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/execution/PresenceTracingWarningTaskTest.kt @@ -0,0 +1,223 @@ +package de.rki.coronawarnapp.presencetracing.risk.execution + +import de.rki.coronawarnapp.eventregistration.checkins.CheckInRepository +import de.rki.coronawarnapp.presencetracing.risk.calculation.CheckInWarningMatcher +import de.rki.coronawarnapp.presencetracing.risk.calculation.createCheckIn +import de.rki.coronawarnapp.presencetracing.risk.calculation.createWarning +import de.rki.coronawarnapp.presencetracing.risk.storage.PresenceTracingRiskRepository +import de.rki.coronawarnapp.presencetracing.warning.WarningPackageId +import de.rki.coronawarnapp.presencetracing.warning.download.TraceWarningPackageSyncTool +import de.rki.coronawarnapp.presencetracing.warning.storage.TraceWarningPackage +import de.rki.coronawarnapp.presencetracing.warning.storage.TraceWarningRepository +import de.rki.coronawarnapp.server.protocols.internal.pt.TraceWarning +import de.rki.coronawarnapp.util.TimeStamper +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.comparables.shouldBeLessThan +import io.kotest.matchers.shouldNotBe +import io.mockk.MockKAnnotations +import io.mockk.Runs +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.coVerifySequence +import io.mockk.every +import io.mockk.impl.annotations.MockK +import io.mockk.just +import io.mockk.mockk +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.runBlockingTest +import org.joda.time.Duration +import org.joda.time.Instant +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import testhelpers.BaseTest +import java.io.IOException + +class PresenceTracingWarningTaskTest : BaseTest() { + + @MockK lateinit var timeStamper: TimeStamper + @MockK lateinit var syncTool: TraceWarningPackageSyncTool + @MockK lateinit var checkInWarningMatcher: CheckInWarningMatcher + @MockK lateinit var presenceTracingRiskRepository: PresenceTracingRiskRepository + @MockK lateinit var traceWarningRepository: TraceWarningRepository + @MockK lateinit var checkInsRepository: CheckInRepository + + @BeforeEach + fun setup() { + MockKAnnotations.init(this) + + every { timeStamper.nowUTC } returns Instant.ofEpochMilli(9000) + coEvery { syncTool.syncPackages() } returns mockk() + coEvery { checkInWarningMatcher.process(any(), any()) } answers { + CheckInWarningMatcher.Result( + successful = true, + processedPackages = listOf( + CheckInWarningMatcher.MatchesPerPackage( + warningPackage = WARNING_PKG, + overlaps = listOf(mockk()) + ) + ) + ) + } + + traceWarningRepository.apply { + coEvery { unprocessedWarningPackages } returns flowOf(listOf(WARNING_PKG)) + coEvery { markPackagesProcessed(any()) } just Runs + } + + coEvery { checkInsRepository.allCheckIns } returns flowOf(listOf(CHECKIN_1, CHECKIN_2)) + + presenceTracingRiskRepository.apply { + coEvery { deleteAllMatches() } just Runs + coEvery { deleteStaleData() } just Runs + coEvery { reportCalculation(any(), any()) } just Runs + } + } + + private fun createInstance() = PresenceTracingWarningTask( + timeStamper = timeStamper, + syncTool = syncTool, + checkInWarningMatcher = checkInWarningMatcher, + presenceTracingRiskRepository = presenceTracingRiskRepository, + traceWarningRepository = traceWarningRepository, + checkInsRepository = checkInsRepository, + ) + + @Test + fun `happy path, match result is reported successfully`() = runBlockingTest { + createInstance().run(mockk()) shouldNotBe null + + coVerifySequence { + syncTool.syncPackages() + presenceTracingRiskRepository.deleteStaleData() + checkInsRepository.allCheckIns + traceWarningRepository.unprocessedWarningPackages + + checkInWarningMatcher.process(any(), any()) + + presenceTracingRiskRepository.reportCalculation( + successful = true, + overlaps = any() + ) + traceWarningRepository.markPackagesProcessed(listOf(WARNING_PKG.packageId)) + } + } + + @Test + fun `overall task errors lead to a reported failed calculation`() = runBlockingTest { + coEvery { syncTool.syncPackages() } throws IOException("Unexpected") + + shouldThrow<IOException> { + createInstance().run(mockk()) + } + + coVerify { + presenceTracingRiskRepository.reportCalculation( + successful = false, + overlaps = emptyList() + ) + } + } + + @Test + fun `there are no check-ins to match against`() = runBlockingTest { + coEvery { checkInsRepository.allCheckIns } returns flowOf(emptyList()) + + createInstance().run(mockk()) shouldNotBe null + + coVerifySequence { + syncTool.syncPackages() + presenceTracingRiskRepository.deleteStaleData() + checkInsRepository.allCheckIns + + presenceTracingRiskRepository.deleteAllMatches() + presenceTracingRiskRepository.reportCalculation(successful = true) + } + } + + @Test + fun `there are no warning packages to process`() = runBlockingTest { + coEvery { traceWarningRepository.unprocessedWarningPackages } returns flowOf(emptyList()) + + createInstance().run(mockk()) shouldNotBe null + + coVerifySequence { + syncTool.syncPackages() + presenceTracingRiskRepository.deleteStaleData() + checkInsRepository.allCheckIns + traceWarningRepository.unprocessedWarningPackages + + presenceTracingRiskRepository.reportCalculation(successful = true) + } + } + + @Test + fun `report failure if matching throws exception`() = runBlockingTest { + coEvery { checkInWarningMatcher.process(any(), any()) } throws IllegalArgumentException() + shouldThrow<IllegalArgumentException> { + createInstance().run(mockk()) shouldNotBe null + } + + coVerifySequence { + syncTool.syncPackages() + presenceTracingRiskRepository.deleteStaleData() + checkInsRepository.allCheckIns + traceWarningRepository.unprocessedWarningPackages + + checkInWarningMatcher.process(any(), any()) + + presenceTracingRiskRepository.reportCalculation( + successful = false, + overlaps = any() + ) + } + + coVerify(exactly = 0) { + traceWarningRepository.markPackagesProcessed(any()) + } + } + + @Test + fun `task timeout is constrained to less than 9min`() { + // Worker execution time + val maxDuration = Duration.standardMinutes(9).plus(1) + PresenceTracingWarningTask.Config().executionTimeout shouldBeLessThan maxDuration + } + + companion object { + val CHECKIN_1 = createCheckIn( + id = 2L, + traceLocationId = "fe84394e73838590cc7707aba0350c130f6d0fb6f0f2535f9735f481dee61871", + startDateStr = "2021-03-04T10:15+01:00", + endDateStr = "2021-03-04T10:17+01:00" + ) + val CHECKIN_2 = createCheckIn( + id = 3L, + traceLocationId = "69eb427e1a48133970486244487e31b3f1c5bde47415db9b52cc5a2ece1e0060", + startDateStr = "2021-03-04T09:15+01:00", + endDateStr = "2021-03-04T10:12+01:00" + ) + + val WARNING_1 = createWarning( + traceLocationId = "fe84394e73838590cc7707aba0350c130f6d0fb6f0f2535f9735f481dee61871", + startIntervalDateStr = "2021-03-04T10:00+01:00", + period = 6, + transmissionRiskLevel = 8 + ) + + val WARNING_2 = createWarning( + traceLocationId = "69eb427e1a48133970486244487e31b3f1c5bde47415db9b52cc5a2ece1e0060", + startIntervalDateStr = "2021-03-04T10:00+01:00", + period = 6, + transmissionRiskLevel = 8 + ) + + val WARNING_PKG = object : TraceWarningPackage { + override suspend fun extractWarnings(): List<TraceWarning.TraceTimeIntervalWarning> { + return listOf(WARNING_1, WARNING_2) + } + + override val packageId: WarningPackageId + get() = "id" + } + } +} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/execution/PresenceTracingWarningWorkerTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/execution/PresenceTracingWarningWorkerTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..dee16970f1d0b8e9b4ff4621650b6167b1e062ed --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/execution/PresenceTracingWarningWorkerTest.kt @@ -0,0 +1,95 @@ +package de.rki.coronawarnapp.presencetracing.risk.execution + +import android.content.Context +import androidx.work.ListenableWorker +import androidx.work.WorkerParameters +import de.rki.coronawarnapp.task.TaskController +import de.rki.coronawarnapp.task.TaskRequest +import de.rki.coronawarnapp.task.TaskState +import de.rki.coronawarnapp.task.common.DefaultTaskRequest +import de.rki.coronawarnapp.task.submitBlocking +import io.kotest.matchers.shouldBe +import io.mockk.MockKAnnotations +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.every +import io.mockk.impl.annotations.MockK +import io.mockk.impl.annotations.RelaxedMockK +import io.mockk.mockkStatic +import io.mockk.slot +import kotlinx.coroutines.test.runBlockingTest +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import testhelpers.BaseTest + +class PresenceTracingWarningWorkerTest : BaseTest() { + + @RelaxedMockK lateinit var workerParams: WorkerParameters + @MockK lateinit var context: Context + @MockK lateinit var taskController: TaskController + @MockK(relaxed = true) lateinit var taskResult: TaskState + + @BeforeEach + fun setup() { + MockKAnnotations.init(this) + + mockkStatic("de.rki.coronawarnapp.task.TaskControllerExtensionsKt") + + coEvery { taskController.submitBlocking(any()) } returns taskResult + + taskResult.apply { + every { isSuccessful } returns true + every { error } returns null + } + } + + private fun createWorker() = PresenceTracingWarningWorker( + context = context, + workerParams = workerParams, + taskController = taskController + ) + + @Test + fun `worker runs task`() = runBlockingTest { + val slot = slot<TaskRequest>() + coEvery { taskController.submitBlocking(capture(slot)) } returns taskResult + + val worker = createWorker() + + worker.doWork() shouldBe ListenableWorker.Result.success() + + slot.captured shouldBe DefaultTaskRequest( + id = slot.captured.id, + arguments = slot.captured.arguments, + type = PresenceTracingWarningTask::class, + originTag = "PresenceTracingWarningWorker", + ) + } + + @Test + fun `task errors lead to retry`() = runBlockingTest { + every { taskResult.isSuccessful } returns false + every { taskResult.error } returns Exception() + + val worker = createWorker() + + worker.doWork() shouldBe ListenableWorker.Result.retry() + + coVerify { + taskController.submitBlocking(any()) + } + } + + @Test + fun `taskcontroller errors lead to retry`() = runBlockingTest { + coEvery { taskController.submitBlocking(any()) } throws Exception() + + val worker = createWorker() + + worker.doWork() shouldBe ListenableWorker.Result.retry() + + coVerify { + taskController.submitBlocking(any()) + } + } +} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/warning/download/TraceWarningPackageDownloaderTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/warning/download/TraceWarningPackageDownloaderTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..8bd44b79f7ebd3b74f884746953427d4c110e934 --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/warning/download/TraceWarningPackageDownloaderTest.kt @@ -0,0 +1,12 @@ +package de.rki.coronawarnapp.presencetracing.warning.download + +import org.junit.jupiter.api.Test +import testhelpers.BaseTest + +class TraceWarningPackageDownloaderTest : BaseTest() { + + @Test + fun `errors during writeProtoBufToFile cause download to be marked as failed`() { + // TODO + } +} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/EwRiskLevelResultExtensionsTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/EwRiskLevelResultExtensionsTest.kt index f1b801552258d43dfbaa690f7beebcca753efdba..18acf7c6352c7cb688064d5e52989cf29d984c2c 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/EwRiskLevelResultExtensionsTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/EwRiskLevelResultExtensionsTest.kt @@ -2,9 +2,9 @@ package de.rki.coronawarnapp.risk import com.google.android.gms.nearby.exposurenotification.ExposureWindow import de.rki.coronawarnapp.risk.result.EwAggregatedRiskResult -import io.kotest.matchers.longs.shouldBeInRange import io.kotest.matchers.shouldBe import io.mockk.mockk +import org.joda.time.Duration import org.joda.time.Instant import org.junit.jupiter.api.Test import testhelpers.BaseTest @@ -31,11 +31,12 @@ class EwRiskLevelResultExtensionsTest : BaseTest() { emptyResults.tryLatestEwResultsWithDefaults().apply { lastCalculated.apply { riskState shouldBe RiskState.LOW_RISK - val now = Instant.now().millis - calculatedAt.millis shouldBeInRange ((now - 60 * 1000L)..now + 60 * 1000L) - } - lastSuccessfullyCalculated.apply { - riskState shouldBe RiskState.CALCULATION_FAILED + + calculatedAt.isAfter(Instant.EPOCH) shouldBe true + calculatedAt.isBefore(Instant.now().plus(Duration.standardHours(1))) shouldBe true + lastSuccessfullyCalculated.apply { + riskState shouldBe RiskState.CALCULATION_FAILED + } } } } diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/RiskLevelChangeDetectorTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/RiskLevelChangeDetectorTest.kt index d391e5cc2a9f7d54390d3eb777452ba6b2de1459..225b325d98625085679e70b2392d65944b53f471 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/RiskLevelChangeDetectorTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/RiskLevelChangeDetectorTest.kt @@ -1,11 +1,14 @@ package de.rki.coronawarnapp.risk +import android.app.Notification import android.content.Context +import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import com.google.android.gms.nearby.exposurenotification.ExposureWindow import de.rki.coronawarnapp.datadonation.analytics.storage.TestResultDonorSettings import de.rki.coronawarnapp.datadonation.survey.Surveys import de.rki.coronawarnapp.notification.GeneralNotifications +import de.rki.coronawarnapp.presencetracing.risk.PtRiskLevelResult import de.rki.coronawarnapp.risk.RiskState.CALCULATION_FAILED import de.rki.coronawarnapp.risk.RiskState.INCREASED_RISK import de.rki.coronawarnapp.risk.RiskState.LOW_RISK @@ -15,6 +18,7 @@ import de.rki.coronawarnapp.storage.TracingSettings import de.rki.coronawarnapp.submission.SubmissionSettings import de.rki.coronawarnapp.util.TimeStamper import de.rki.coronawarnapp.util.device.ForegroundState +import de.rki.coronawarnapp.util.notifications.setContentTextExpandable import io.kotest.matchers.shouldBe import io.mockk.Called import io.mockk.MockKAnnotations @@ -47,18 +51,24 @@ class RiskLevelChangeDetectorTest : BaseTest() { @MockK lateinit var tracingSettings: TracingSettings @MockK lateinit var testResultDonorSettings: TestResultDonorSettings + @MockK lateinit var builder: NotificationCompat.Builder + @MockK lateinit var notification: Notification + @BeforeEach fun setup() { MockKAnnotations.init(this) every { tracingSettings.isUserToBeNotifiedOfLoweredRiskLevel } returns mockFlowPreference(false) every { submissionSettings.isSubmissionSuccessful } returns false - every { foregroundState.isInForeground } returns flowOf(true) + every { foregroundState.isInForeground } returns flowOf(false) every { notificationManagerCompat.areNotificationsEnabled() } returns true every { riskLevelSettings.lastChangeCheckedRiskLevelTimestamp = any() } just Runs every { riskLevelSettings.lastChangeCheckedRiskLevelTimestamp } returns null + every { riskLevelSettings.lastChangeCheckedRiskLevelCombinedTimestamp = any() } just Runs + every { riskLevelSettings.lastChangeCheckedRiskLevelCombinedTimestamp } returns null + every { riskLevelSettings.lastChangeToHighRiskLevelTimestamp = any() } just Runs every { riskLevelSettings.lastChangeToHighRiskLevelTimestamp } returns null @@ -66,10 +76,17 @@ class RiskLevelChangeDetectorTest : BaseTest() { every { testResultDonorSettings.riskLevelTurnedRedTime } returns mockFlowPreference(null) every { testResultDonorSettings.mostRecentDateWithHighOrLowRiskLevel } returns mockFlowPreference(null) - every { riskLevelStorage.latestCombinedEwPtRiskLevelResults } returns flowOf(listOf()) + + every { builder.build() } returns notification + every { builder.setContentTitle(any()) } returns builder + every { builder.setContentTextExpandable(any()) } returns builder + every { builder.setContentText(any()) } returns builder + every { builder.setStyle(any()) } returns builder + every { notificationHelper.newBaseBuilder() } returns builder + every { context.getString(any()) } returns "" } - private fun createRiskLevel( + private fun createEwRiskLevel( riskState: RiskState, calculatedAt: Instant = Instant.EPOCH, ewAggregatedRiskResult: EwAggregatedRiskResult? = null @@ -83,6 +100,23 @@ class RiskLevelChangeDetectorTest : BaseTest() { override val daysWithEncounters: Int = 0 } + private fun createPtRiskLevel( + riskState: RiskState, + calculatedAt: Instant = Instant.EPOCH + ): PtRiskLevelResult = PtRiskLevelResult( + calculatedAt = calculatedAt, + riskState = riskState + ) + + private fun createCombinedRiskLevel( + riskState: RiskState, + calculatedAt: Instant = Instant.EPOCH, + ewAggregatedRiskResult: EwAggregatedRiskResult? = null + ): CombinedEwPtRiskLevelResult = CombinedEwPtRiskLevelResult( + ewRiskLevelResult = createEwRiskLevel(riskState, calculatedAt, ewAggregatedRiskResult), + ptRiskLevelResult = createPtRiskLevel(riskState, calculatedAt) + ) + private fun createInstance(scope: CoroutineScope) = RiskLevelChangeDetector( context = context, appScope = scope, @@ -99,7 +133,9 @@ class RiskLevelChangeDetectorTest : BaseTest() { @Test fun `nothing happens if there is only one result yet`() { - every { riskLevelStorage.latestEwRiskLevelResults } returns flowOf(listOf(createRiskLevel(LOW_RISK))) + every { riskLevelStorage.latestCombinedEwPtRiskLevelResults } returns + flowOf(listOf(createCombinedRiskLevel(LOW_RISK))) + every { riskLevelStorage.latestEwRiskLevelResults } returns flowOf(listOf(createEwRiskLevel(LOW_RISK))) runBlockingTest { val instance = createInstance(scope = this) @@ -110,19 +146,28 @@ class RiskLevelChangeDetectorTest : BaseTest() { coVerifySequence { notificationManagerCompat wasNot Called surveys wasNot Called + testResultDonorSettings wasNot Called } } } @Test - fun `no risklevel change, nothing should happen`() { + fun `no risk level change, nothing should happen`() { every { riskLevelStorage.latestEwRiskLevelResults } returns flowOf( listOf( - createRiskLevel(LOW_RISK), - createRiskLevel(LOW_RISK) + createEwRiskLevel(LOW_RISK), + createEwRiskLevel(LOW_RISK) ) ) + every { riskLevelStorage.latestCombinedEwPtRiskLevelResults } returns + flowOf( + listOf( + createCombinedRiskLevel(LOW_RISK), + createCombinedRiskLevel(LOW_RISK) + ) + ) + runBlockingTest { val instance = createInstance(scope = this) instance.launch() @@ -136,17 +181,53 @@ class RiskLevelChangeDetectorTest : BaseTest() { } } - // TODO test if risk level change for combined risk triggers notification + @Test + fun `combined risk state change from HIGH to LOW triggers notification`() { + every { riskLevelStorage.latestEwRiskLevelResults } returns flowOf( + listOf( + createEwRiskLevel(LOW_RISK) + ) + ) + + every { riskLevelStorage.latestCombinedEwPtRiskLevelResults } returns + flowOf( + listOf( + createCombinedRiskLevel(LOW_RISK, calculatedAt = Instant.EPOCH.plus(1)), + createCombinedRiskLevel(INCREASED_RISK, calculatedAt = Instant.EPOCH) + ) + ) + + runBlockingTest { + val instance = createInstance(scope = this) + instance.launch() + + advanceUntilIdle() + + coVerifySequence { + submissionSettings.isSubmissionSuccessful + foregroundState.isInForeground + notificationHelper.newBaseBuilder() + notificationHelper.sendNotification(any(), any()) + } + } + } @Test - fun `risklevel went from HIGH to LOW`() { + fun `combined risk state change from LOW to HIGH triggers notification`() { every { riskLevelStorage.latestEwRiskLevelResults } returns flowOf( listOf( - createRiskLevel(LOW_RISK, calculatedAt = Instant.EPOCH.plus(1)), - createRiskLevel(INCREASED_RISK, calculatedAt = Instant.EPOCH) + createEwRiskLevel(LOW_RISK) ) ) + every { riskLevelStorage.latestCombinedEwPtRiskLevelResults } returns + flowOf( + listOf( + createCombinedRiskLevel(INCREASED_RISK, calculatedAt = Instant.EPOCH.plus(1)), + createCombinedRiskLevel(LOW_RISK, calculatedAt = Instant.EPOCH) + ) + ) + runBlockingTest { val instance = createInstance(scope = this) instance.launch() @@ -154,21 +235,46 @@ class RiskLevelChangeDetectorTest : BaseTest() { advanceUntilIdle() coVerifySequence { - surveys.resetSurvey(Surveys.Type.HIGH_RISK_ENCOUNTER) + submissionSettings.isSubmissionSuccessful + foregroundState.isInForeground + notificationHelper.newBaseBuilder() + notificationHelper.sendNotification(any(), any()) } } } - // TODO test if risk level change for combined risk triggers notification + @Test + fun `risk level went from HIGH to LOW resets survey`() { + every { riskLevelStorage.latestEwRiskLevelResults } returns flowOf( + listOf( + createEwRiskLevel(LOW_RISK, calculatedAt = Instant.EPOCH.plus(1)), + createEwRiskLevel(INCREASED_RISK, calculatedAt = Instant.EPOCH) + ) + ) + every { riskLevelStorage.latestCombinedEwPtRiskLevelResults } returns + flowOf(listOf(createCombinedRiskLevel(LOW_RISK))) + runBlockingTest { + val instance = createInstance(scope = this) + instance.launch() + + advanceUntilIdle() + + coVerifySequence { + surveys.resetSurvey(Surveys.Type.HIGH_RISK_ENCOUNTER) + } + } + } @Test - fun `risklevel went from LOW to HIGH`() { + fun `risk level went from LOW to HIGH`() { every { riskLevelStorage.latestEwRiskLevelResults } returns flowOf( listOf( - createRiskLevel(INCREASED_RISK, calculatedAt = Instant.EPOCH.plus(1)), - createRiskLevel(LOW_RISK, calculatedAt = Instant.EPOCH) + createEwRiskLevel(INCREASED_RISK, calculatedAt = Instant.EPOCH.plus(1)), + createEwRiskLevel(LOW_RISK, calculatedAt = Instant.EPOCH) ) ) + every { riskLevelStorage.latestCombinedEwPtRiskLevelResults } returns + flowOf(listOf(createCombinedRiskLevel(LOW_RISK))) runBlockingTest { val instance = createInstance(scope = this) @@ -183,13 +289,15 @@ class RiskLevelChangeDetectorTest : BaseTest() { } @Test - fun `risklevel went from LOW to HIGH but it is has already been processed`() { + fun `risk level went from LOW to HIGH but it is has already been processed`() { every { riskLevelStorage.latestEwRiskLevelResults } returns flowOf( listOf( - createRiskLevel(INCREASED_RISK, calculatedAt = Instant.EPOCH.plus(1)), - createRiskLevel(LOW_RISK, calculatedAt = Instant.EPOCH) + createEwRiskLevel(INCREASED_RISK, calculatedAt = Instant.EPOCH.plus(1)), + createEwRiskLevel(LOW_RISK, calculatedAt = Instant.EPOCH) ) ) + every { riskLevelStorage.latestCombinedEwPtRiskLevelResults } returns + flowOf(listOf(createCombinedRiskLevel(LOW_RISK))) every { riskLevelSettings.lastChangeCheckedRiskLevelTimestamp } returns Instant.EPOCH.plus(1) runBlockingTest { @@ -205,23 +313,57 @@ class RiskLevelChangeDetectorTest : BaseTest() { } } + @Test + fun `combined risk level went from LOW to HIGH but it is has already been processed`() { + every { riskLevelStorage.latestEwRiskLevelResults } returns flowOf( + listOf( + createEwRiskLevel(LOW_RISK) + ) + ) + + every { riskLevelStorage.latestCombinedEwPtRiskLevelResults } returns + flowOf( + listOf( + createCombinedRiskLevel(INCREASED_RISK, calculatedAt = Instant.EPOCH.plus(1)), + createCombinedRiskLevel(LOW_RISK, calculatedAt = Instant.EPOCH) + ) + ) + + every { riskLevelSettings.lastChangeCheckedRiskLevelCombinedTimestamp } returns Instant.EPOCH.plus(1) + + runBlockingTest { + val instance = createInstance(scope = this) + instance.launch() + + advanceUntilIdle() + + coVerifySequence { + notificationManagerCompat wasNot Called + surveys wasNot Called + } + } + } + @Test fun `riskLevelTurnedRedTime is only set once`() { testResultDonorSettings.riskLevelTurnedRedTime.update { Instant.EPOCH.plus(1) } every { riskLevelStorage.latestEwRiskLevelResults } returns flowOf( listOf( - createRiskLevel( + createEwRiskLevel( INCREASED_RISK, calculatedAt = Instant.EPOCH.plus(2), ewAggregatedRiskResult = mockk<EwAggregatedRiskResult>().apply { every { isIncreasedRisk() } returns true } ), - createRiskLevel(LOW_RISK, calculatedAt = Instant.EPOCH) + createEwRiskLevel(LOW_RISK, calculatedAt = Instant.EPOCH) ) ) + every { riskLevelStorage.latestCombinedEwPtRiskLevelResults } returns + flowOf(listOf(createCombinedRiskLevel(LOW_RISK))) + runBlockingTest { val instance = createInstance(scope = this) instance.launch() @@ -245,7 +387,7 @@ class RiskLevelChangeDetectorTest : BaseTest() { fun `mostRecentDateWithHighOrLowRiskLevel is updated every time`() { every { riskLevelStorage.latestEwRiskLevelResults } returns flowOf( listOf( - createRiskLevel( + createEwRiskLevel( INCREASED_RISK, calculatedAt = Instant.EPOCH.plus(1), ewAggregatedRiskResult = mockk<EwAggregatedRiskResult>().apply { @@ -253,9 +395,11 @@ class RiskLevelChangeDetectorTest : BaseTest() { every { isIncreasedRisk() } returns true } ), - createRiskLevel(LOW_RISK, calculatedAt = Instant.EPOCH) + createEwRiskLevel(LOW_RISK, calculatedAt = Instant.EPOCH) ) ) + every { riskLevelStorage.latestCombinedEwPtRiskLevelResults } returns + flowOf(listOf(createCombinedRiskLevel(LOW_RISK))) runBlockingTest { val instance = createInstance(scope = this) @@ -267,7 +411,7 @@ class RiskLevelChangeDetectorTest : BaseTest() { every { riskLevelStorage.latestEwRiskLevelResults } returns flowOf( listOf( - createRiskLevel( + createEwRiskLevel( INCREASED_RISK, calculatedAt = Instant.EPOCH.plus(1), ewAggregatedRiskResult = mockk<EwAggregatedRiskResult>().apply { @@ -275,7 +419,7 @@ class RiskLevelChangeDetectorTest : BaseTest() { every { isIncreasedRisk() } returns false } ), - createRiskLevel(LOW_RISK, calculatedAt = Instant.EPOCH) + createEwRiskLevel(LOW_RISK, calculatedAt = Instant.EPOCH) ) ) diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/storage/BaseRiskLevelStorageTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/storage/BaseRiskLevelStorageTest.kt index 099afd385a811e046f64a17a1cc4e9a742d1cbaa..03c6758900c2520d87a5dbf7fa83f0224ad034a7 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/storage/BaseRiskLevelStorageTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/storage/BaseRiskLevelStorageTest.kt @@ -1,7 +1,11 @@ package de.rki.coronawarnapp.risk.storage -import de.rki.coronawarnapp.presencetracing.risk.PresenceTracingRiskRepository +import de.rki.coronawarnapp.presencetracing.risk.PtRiskLevelResult +import de.rki.coronawarnapp.presencetracing.risk.calculation.PresenceTracingDayRisk +import de.rki.coronawarnapp.presencetracing.risk.storage.PresenceTracingRiskRepository import de.rki.coronawarnapp.risk.EwRiskLevelResult +import de.rki.coronawarnapp.risk.RiskState +import de.rki.coronawarnapp.risk.storage.RiskStorageTestData.ewCalculatedAt import de.rki.coronawarnapp.risk.storage.RiskStorageTestData.testAggregatedRiskPerDateResult import de.rki.coronawarnapp.risk.storage.RiskStorageTestData.testExposureWindow import de.rki.coronawarnapp.risk.storage.RiskStorageTestData.testExposureWindowDaoWrapper @@ -14,6 +18,9 @@ import de.rki.coronawarnapp.risk.storage.internal.RiskResultDatabase.AggregatedR import de.rki.coronawarnapp.risk.storage.internal.RiskResultDatabase.ExposureWindowsDao import de.rki.coronawarnapp.risk.storage.internal.RiskResultDatabase.Factory import de.rki.coronawarnapp.risk.storage.internal.RiskResultDatabase.RiskResultsDao +import de.rki.coronawarnapp.risk.storage.internal.riskresults.PersistedRiskLevelResultDao +import de.rki.coronawarnapp.server.protocols.internal.v2.RiskCalculationParametersOuterClass +import de.rki.coronawarnapp.util.TimeAndDateExtensions.toLocalDateUtc import io.kotest.assertions.throwables.shouldThrow import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldNotBe @@ -32,6 +39,7 @@ import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.TestCoroutineScope import kotlinx.coroutines.test.runBlockingTest +import org.joda.time.Instant import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import testhelpers.BaseTest @@ -70,8 +78,9 @@ class BaseRiskLevelStorageTest : BaseTest() { // TODO proper tests coEvery { presenceTracingRiskRepository.traceLocationCheckInRiskStates } returns emptyFlow() coEvery { presenceTracingRiskRepository.presenceTracingDayRisk } returns emptyFlow() - coEvery { presenceTracingRiskRepository.latestAndLastSuccessful() } returns emptyFlow() + coEvery { presenceTracingRiskRepository.allEntries() } returns emptyFlow() coEvery { presenceTracingRiskRepository.latestEntries(any()) } returns emptyFlow() + coEvery { presenceTracingRiskRepository.clearAllTables() } just Runs } private fun createInstance( @@ -104,7 +113,9 @@ class BaseRiskLevelStorageTest : BaseTest() { val instance = createInstance() val allEntries = instance.aggregatedRiskPerDateResultTables.allEntries() allEntries shouldBe testPersistedAggregatedRiskPerDateResultFlow - allEntries.first().map { it.toAggregatedRiskPerDateResult() } shouldBe listOf(testAggregatedRiskPerDateResult) + allEntries.first().map { it.toAggregatedRiskPerDateResult() } shouldBe listOf( + testAggregatedRiskPerDateResult + ) val aggregatedRiskPerDateResults = instance.ewDayRiskStates.first() aggregatedRiskPerDateResults shouldNotBe listOf(testPersistedAggregatedRiskPerDateResult) @@ -112,6 +123,19 @@ class BaseRiskLevelStorageTest : BaseTest() { } } + @Test + fun `ptDayRiskStates are returned from database`() { + val testPresenceTracingDayRiskFlow = flowOf(listOf(testPresenceTracingDayRisk)) + every { presenceTracingRiskRepository.presenceTracingDayRisk } returns testPresenceTracingDayRiskFlow + + runBlockingTest { + val instance = createInstance() + + val states = instance.ptDayRiskStates.first() + states shouldBe listOf(testPresenceTracingDayRisk) + } + } + @Test fun `exposureWindows are returned from database and mapped`() { val testDaoWrappers = flowOf(listOf(testExposureWindowDaoWrapper)) @@ -170,6 +194,142 @@ class BaseRiskLevelStorageTest : BaseTest() { } } + @Test + fun `latestCombinedEwPtRiskLevelResults works when 2 pt result and 1 ew result are available`() { + every { riskResultTables.latestEntries(any()) } returns flowOf(listOf(testRiskLevelResultDao)) + every { exposureWindowTables.getWindowsForResult(any()) } returns flowOf(listOf(testExposureWindowDaoWrapper)) + val calculatedAt = ewCalculatedAt.plus(6000L) + every { presenceTracingRiskRepository.latestEntries(2) } returns flowOf( + listOf( + PtRiskLevelResult( + calculatedAt = calculatedAt, + presenceTracingDayRisk = null, + riskState = RiskState.CALCULATION_FAILED + ), + PtRiskLevelResult( + calculatedAt = ewCalculatedAt.minus(1000L), + presenceTracingDayRisk = listOf(testPresenceTracingDayRisk), + riskState = RiskState.INCREASED_RISK + ) + ) + ) + runBlockingTest2(ignoreActive = true) { + val instance = createInstance(scope = this) + + val riskLevelResults = instance.latestCombinedEwPtRiskLevelResults.first() + riskLevelResults.size shouldBe 2 + + riskLevelResults[0].calculatedAt shouldBe calculatedAt + riskLevelResults[0].riskState shouldBe RiskState.INCREASED_RISK + riskLevelResults[1].calculatedAt shouldBe ewCalculatedAt + riskLevelResults[1].riskState shouldBe RiskState.INCREASED_RISK + + verify { + riskResultTables.latestEntries(2) + presenceTracingRiskRepository.latestEntries(2) + } + } + } + + @Test + fun `latestCombinedEwPtRiskLevelResults works when only one calc each is available`() { + every { riskResultTables.latestEntries(any()) } returns flowOf(listOf(testRiskLevelResultDao)) + every { exposureWindowTables.getWindowsForResult(any()) } returns flowOf(listOf(testExposureWindowDaoWrapper)) + val calculatedAt = ewCalculatedAt.minus(400L) + every { presenceTracingRiskRepository.latestEntries(2) } returns flowOf( + listOf( + PtRiskLevelResult( + calculatedAt = calculatedAt, + presenceTracingDayRisk = null, + riskState = RiskState.CALCULATION_FAILED + ) + ) + ) + runBlockingTest2(ignoreActive = true) { + val instance = createInstance(scope = this) + + val riskLevelResults = instance.latestCombinedEwPtRiskLevelResults.first() + riskLevelResults.size shouldBe 2 + + riskLevelResults[0].calculatedAt shouldBe ewCalculatedAt + riskLevelResults[0].riskState shouldBe RiskState.INCREASED_RISK + + // result from the combination with initial ew low risk result + riskLevelResults[1].calculatedAt shouldBe ewCalculatedAt.minus(400L) + riskLevelResults[1].riskState shouldBe RiskState.CALCULATION_FAILED + + verify { + riskResultTables.latestEntries(2) + presenceTracingRiskRepository.latestEntries(2) + } + } + } + + @Test + fun `latestCombinedEwPtRiskLevelResults works when two calc each are available`() { + val ewResultDao1 = PersistedRiskLevelResultDao( + id = "id1", + calculatedAt = ewCalculatedAt, + failureReason = null, + aggregatedRiskResult = PersistedRiskLevelResultDao.PersistedAggregatedRiskResult( + totalRiskLevel = RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping.RiskLevel.LOW, + totalMinimumDistinctEncountersWithLowRisk = 1, + totalMinimumDistinctEncountersWithHighRisk = 2, + mostRecentDateWithLowRisk = Instant.ofEpochMilli(3), + mostRecentDateWithHighRisk = Instant.ofEpochMilli(4), + numberOfDaysWithLowRisk = 5, + numberOfDaysWithHighRisk = 6 + ) + ) + val ewResultDao2 = PersistedRiskLevelResultDao( + id = "id2", + calculatedAt = ewCalculatedAt.minus(200L), + failureReason = null, + aggregatedRiskResult = PersistedRiskLevelResultDao.PersistedAggregatedRiskResult( + totalRiskLevel = RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping.RiskLevel.HIGH, + totalMinimumDistinctEncountersWithLowRisk = 1, + totalMinimumDistinctEncountersWithHighRisk = 2, + mostRecentDateWithLowRisk = Instant.ofEpochMilli(3), + mostRecentDateWithHighRisk = Instant.ofEpochMilli(4), + numberOfDaysWithLowRisk = 5, + numberOfDaysWithHighRisk = 6 + ) + ) + every { riskResultTables.latestEntries(any()) } returns flowOf(listOf(ewResultDao1, ewResultDao2)) + every { exposureWindowTables.getWindowsForResult(any()) } returns flowOf(listOf(testExposureWindowDaoWrapper)) + val calculatedAt = ewCalculatedAt.minus(400L) + every { presenceTracingRiskRepository.latestEntries(2) } returns flowOf( + listOf( + PtRiskLevelResult( + calculatedAt = calculatedAt, + presenceTracingDayRisk = null, + riskState = RiskState.CALCULATION_FAILED + ), + PtRiskLevelResult( + calculatedAt = calculatedAt.minus(400L), + presenceTracingDayRisk = null, + riskState = RiskState.LOW_RISK + ) + ) + ) + runBlockingTest2(ignoreActive = true) { + val instance = createInstance(scope = this) + + val riskLevelResults = instance.latestCombinedEwPtRiskLevelResults.first() + riskLevelResults.size shouldBe 2 + + riskLevelResults[0].calculatedAt shouldBe ewCalculatedAt + riskLevelResults[0].riskState shouldBe RiskState.LOW_RISK + riskLevelResults[1].calculatedAt shouldBe ewCalculatedAt.minus(200L) + riskLevelResults[1].riskState shouldBe RiskState.INCREASED_RISK + + verify { + riskResultTables.latestEntries(2) + presenceTracingRiskRepository.latestEntries(2) + } + } + } + // This just tests the mapping, the correctness of the SQL statement is validated in an instrumentation test @Test fun `latestAndLastSuccessful with exposure windows are returned from database and mapped`() { @@ -190,7 +350,214 @@ class BaseRiskLevelStorageTest : BaseTest() { } @Test - fun `errors when storing risklevel result are rethrown`() = runBlockingTest { + fun `latestAndLastSuccessfulCombinedEwPtRiskLevelResult are combined`() { + val calculatedAt = ewCalculatedAt.plus(6000L) + every { presenceTracingRiskRepository.allEntries() } returns flowOf( + listOf( + PtRiskLevelResult( + calculatedAt = calculatedAt, + presenceTracingDayRisk = null, + riskState = RiskState.CALCULATION_FAILED + ), + PtRiskLevelResult( + calculatedAt = ewCalculatedAt.minus(1000L), + presenceTracingDayRisk = listOf(testPresenceTracingDayRisk), + riskState = RiskState.INCREASED_RISK + ) + ) + ) + every { presenceTracingRiskRepository.presenceTracingDayRisk } returns flowOf(listOf(testPresenceTracingDayRisk)) + + every { riskResultTables.latestAndLastSuccessful() } returns flowOf(listOf(testRiskLevelResultDao)) + every { exposureWindowTables.getWindowsForResult(any()) } returns flowOf(listOf(testExposureWindowDaoWrapper)) + + runBlockingTest2(ignoreActive = true) { + val instance = createInstance(scope = this) + + val riskLevelResult = instance.latestAndLastSuccessfulCombinedEwPtRiskLevelResult.first() + + riskLevelResult.lastCalculated.calculatedAt shouldBe calculatedAt + riskLevelResult.lastCalculated.riskState shouldBe RiskState.INCREASED_RISK + riskLevelResult.lastSuccessfullyCalculated.calculatedAt shouldBe calculatedAt + riskLevelResult.lastSuccessfullyCalculated.riskState shouldBe RiskState.INCREASED_RISK + + verify { + presenceTracingRiskRepository.allEntries() + presenceTracingRiskRepository.presenceTracingDayRisk + } + } + } + + @Test + fun `latestAndLastSuccessfulCombinedEwPtRiskLevelResult are combined 2`() { + every { presenceTracingRiskRepository.allEntries() } returns flowOf( + listOf( + PtRiskLevelResult( + calculatedAt = ewCalculatedAt.plus(6000L), + presenceTracingDayRisk = null, + riskState = RiskState.CALCULATION_FAILED + ), + PtRiskLevelResult( + calculatedAt = ewCalculatedAt.minus(1000L), + presenceTracingDayRisk = listOf(testPresenceTracingDayRisk), + riskState = RiskState.INCREASED_RISK + ) + ) + ) + + val ewResultDao1 = PersistedRiskLevelResultDao( + id = "id1", + calculatedAt = ewCalculatedAt, + failureReason = EwRiskLevelResult.FailureReason.UNKNOWN, + aggregatedRiskResult = null + ) + val ewResultDao2 = PersistedRiskLevelResultDao( + id = "id2", + calculatedAt = ewCalculatedAt.minus(2000L), + failureReason = null, + aggregatedRiskResult = PersistedRiskLevelResultDao.PersistedAggregatedRiskResult( + totalRiskLevel = RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping.RiskLevel.LOW, + totalMinimumDistinctEncountersWithLowRisk = 1, + totalMinimumDistinctEncountersWithHighRisk = 0, + mostRecentDateWithLowRisk = Instant.ofEpochMilli(3), + mostRecentDateWithHighRisk = Instant.ofEpochMilli(0), + numberOfDaysWithLowRisk = 5, + numberOfDaysWithHighRisk = 6 + ) + ) + every { presenceTracingRiskRepository.presenceTracingDayRisk } returns flowOf(listOf(testPresenceTracingDayRisk)) + + every { riskResultTables.latestAndLastSuccessful() } returns flowOf(listOf(ewResultDao1, ewResultDao2)) + every { exposureWindowTables.getWindowsForResult(any()) } returns flowOf(listOf(testExposureWindowDaoWrapper)) + + runBlockingTest2(ignoreActive = true) { + val instance = createInstance(scope = this) + + val riskLevelResult = instance.latestAndLastSuccessfulCombinedEwPtRiskLevelResult.first() + + riskLevelResult.lastCalculated.calculatedAt shouldBe ewCalculatedAt.plus(6000L) + riskLevelResult.lastCalculated.riskState shouldBe RiskState.CALCULATION_FAILED + riskLevelResult.lastSuccessfullyCalculated.calculatedAt shouldBe ewCalculatedAt + riskLevelResult.lastSuccessfullyCalculated.riskState shouldBe RiskState.INCREASED_RISK + + verify { + presenceTracingRiskRepository.allEntries() + presenceTracingRiskRepository.presenceTracingDayRisk + } + } + } + + @Test + fun `latestAndLastSuccessfulCombinedEwPtRiskLevelResult are combined 3`() { + every { presenceTracingRiskRepository.allEntries() } returns flowOf( + listOf( + PtRiskLevelResult( + calculatedAt = ewCalculatedAt.plus(6000L), + presenceTracingDayRisk = null, + riskState = RiskState.CALCULATION_FAILED + ), + PtRiskLevelResult( + calculatedAt = ewCalculatedAt.minus(100L), + presenceTracingDayRisk = listOf(testPresenceTracingDayRisk), + riskState = RiskState.LOW_RISK + ) + ) + ) + + val ewResultDao1 = PersistedRiskLevelResultDao( + id = "id1", + calculatedAt = ewCalculatedAt, + failureReason = EwRiskLevelResult.FailureReason.UNKNOWN, + aggregatedRiskResult = null + ) + val ewResultDao2 = PersistedRiskLevelResultDao( + id = "id2", + calculatedAt = ewCalculatedAt.minus(200L), + failureReason = null, + aggregatedRiskResult = PersistedRiskLevelResultDao.PersistedAggregatedRiskResult( + totalRiskLevel = RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping.RiskLevel.HIGH, + totalMinimumDistinctEncountersWithLowRisk = 1, + totalMinimumDistinctEncountersWithHighRisk = 2, + mostRecentDateWithLowRisk = Instant.ofEpochMilli(3), + mostRecentDateWithHighRisk = Instant.ofEpochMilli(4), + numberOfDaysWithLowRisk = 5, + numberOfDaysWithHighRisk = 6 + ) + ) + every { presenceTracingRiskRepository.presenceTracingDayRisk } returns flowOf(listOf(testPresenceTracingDayRisk)) + + every { riskResultTables.latestAndLastSuccessful() } returns flowOf(listOf(ewResultDao1, ewResultDao2)) + every { exposureWindowTables.getWindowsForResult(any()) } returns flowOf(listOf(testExposureWindowDaoWrapper)) + + runBlockingTest2(ignoreActive = true) { + val instance = createInstance(scope = this) + + val riskLevelResult = instance.latestAndLastSuccessfulCombinedEwPtRiskLevelResult.first() + + riskLevelResult.lastCalculated.calculatedAt shouldBe ewCalculatedAt.plus(6000L) + riskLevelResult.lastCalculated.riskState shouldBe RiskState.CALCULATION_FAILED + riskLevelResult.lastSuccessfullyCalculated.calculatedAt shouldBe ewCalculatedAt + riskLevelResult.lastSuccessfullyCalculated.riskState shouldBe RiskState.LOW_RISK + + verify { + presenceTracingRiskRepository.allEntries() + presenceTracingRiskRepository.presenceTracingDayRisk + } + } + } + + @Test + fun `latestAndLastSuccessfulCombinedEwPtRiskLevelResult works when no pt result yet`() { + + every { presenceTracingRiskRepository.allEntries() } returns flowOf(listOf()) + every { presenceTracingRiskRepository.presenceTracingDayRisk } returns flowOf(listOf()) + + val ewResultDao1 = PersistedRiskLevelResultDao( + id = "id1", + calculatedAt = ewCalculatedAt, + failureReason = null, + aggregatedRiskResult = PersistedRiskLevelResultDao.PersistedAggregatedRiskResult( + totalRiskLevel = RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping.RiskLevel.LOW, + totalMinimumDistinctEncountersWithLowRisk = 1, + totalMinimumDistinctEncountersWithHighRisk = 2, + mostRecentDateWithLowRisk = Instant.ofEpochMilli(3), + mostRecentDateWithHighRisk = Instant.ofEpochMilli(4), + numberOfDaysWithLowRisk = 5, + numberOfDaysWithHighRisk = 6 + ) + ) + val ewResultDao2 = PersistedRiskLevelResultDao( + id = "id2", + calculatedAt = ewCalculatedAt.minus(200L), + failureReason = null, + aggregatedRiskResult = PersistedRiskLevelResultDao.PersistedAggregatedRiskResult( + totalRiskLevel = RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping.RiskLevel.HIGH, + totalMinimumDistinctEncountersWithLowRisk = 1, + totalMinimumDistinctEncountersWithHighRisk = 2, + mostRecentDateWithLowRisk = Instant.ofEpochMilli(3), + mostRecentDateWithHighRisk = Instant.ofEpochMilli(4), + numberOfDaysWithLowRisk = 5, + numberOfDaysWithHighRisk = 6 + ) + ) + + every { riskResultTables.latestAndLastSuccessful() } returns flowOf(listOf(ewResultDao1, ewResultDao2)) + every { exposureWindowTables.getWindowsForResult(any()) } returns flowOf(listOf(testExposureWindowDaoWrapper)) + + runBlockingTest2(ignoreActive = true) { + val instance = createInstance(scope = this) + + val riskLevelResult = instance.latestAndLastSuccessfulCombinedEwPtRiskLevelResult.first() + + riskLevelResult.lastCalculated.calculatedAt shouldBe ewCalculatedAt + riskLevelResult.lastCalculated.riskState shouldBe RiskState.LOW_RISK + riskLevelResult.lastSuccessfullyCalculated.calculatedAt shouldBe ewCalculatedAt + riskLevelResult.lastSuccessfullyCalculated.riskState shouldBe RiskState.LOW_RISK + } + } + + @Test + fun `errors when storing risk level result are rethrown`() = runBlockingTest { coEvery { riskResultTables.insertEntry(any()) } throws IllegalStateException("No body expects the...") val instance = createInstance() shouldThrow<java.lang.IllegalStateException> { @@ -242,6 +609,14 @@ class BaseRiskLevelStorageTest : BaseTest() { @Test fun `clear works`() = runBlockingTest { createInstance().clear() - verify { database.clearAllTables() } + coVerify { + database.clearAllTables() + presenceTracingRiskRepository.clearAllTables() + } } } + +private val testPresenceTracingDayRisk = PresenceTracingDayRisk( + Instant.now().toLocalDateUtc(), + RiskState.INCREASED_RISK +) diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/storage/CombineRiskTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/storage/CombineRiskTest.kt index 824be93cc148c67284dc133d0de47990018141c5..a1099568aa19e32b4143ce8da45613664c804346 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/storage/CombineRiskTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/storage/CombineRiskTest.kt @@ -1,13 +1,17 @@ package de.rki.coronawarnapp.risk.storage -import de.rki.coronawarnapp.presencetracing.risk.PresenceTracingDayRisk +import com.google.android.gms.nearby.exposurenotification.ExposureWindow +import de.rki.coronawarnapp.presencetracing.risk.PtRiskLevelResult +import de.rki.coronawarnapp.presencetracing.risk.calculation.PresenceTracingDayRisk +import de.rki.coronawarnapp.risk.EwRiskLevelResult import de.rki.coronawarnapp.risk.RiskState +import de.rki.coronawarnapp.risk.result.EwAggregatedRiskResult import de.rki.coronawarnapp.risk.result.ExposureWindowDayRisk import de.rki.coronawarnapp.server.protocols.internal.v2.RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping.RiskLevel import io.kotest.matchers.shouldBe +import org.joda.time.Instant import org.joda.time.LocalDate import org.junit.jupiter.api.Test -import java.time.Instant class CombineRiskTest { @@ -26,33 +30,33 @@ class CombineRiskTest { riskState = RiskState.CALCULATION_FAILED ) val ewRisk = ExposureWindowDayRisk( - dateMillisSinceEpoch = Instant.parse("2021-03-22T14:00:00.000Z").toEpochMilli(), + dateMillisSinceEpoch = Instant.parse("2021-03-22T14:00:00.000Z").millis, riskLevel = RiskLevel.HIGH, 0, 0 ) val ewRisk2 = ExposureWindowDayRisk( - dateMillisSinceEpoch = Instant.parse("2021-03-19T14:00:00.000Z").toEpochMilli(), + dateMillisSinceEpoch = Instant.parse("2021-03-19T14:00:00.000Z").millis, riskLevel = RiskLevel.LOW, 0, 0 ) val ewRisk3 = ExposureWindowDayRisk( - dateMillisSinceEpoch = Instant.parse("2021-03-20T14:00:00.000Z").toEpochMilli(), + dateMillisSinceEpoch = Instant.parse("2021-03-20T14:00:00.000Z").millis, riskLevel = RiskLevel.UNSPECIFIED, 0, 0 ) val ewRisk4 = ExposureWindowDayRisk( - dateMillisSinceEpoch = Instant.parse("2021-03-15T14:00:00.000Z").toEpochMilli(), + dateMillisSinceEpoch = Instant.parse("2021-03-15T14:00:00.000Z").millis, riskLevel = RiskLevel.UNSPECIFIED, 0, 0 ) - val ptRiskList: List<PresenceTracingDayRisk> = listOf(ptRisk, ptRisk2, ptRisk3) - val exposureWindowDayRiskList: List<ExposureWindowDayRisk> = listOf(ewRisk, ewRisk2, ewRisk3, ewRisk4) - val result = combineRisk(ptRiskList, exposureWindowDayRiskList) + val ptDayRiskList: List<PresenceTracingDayRisk> = listOf(ptRisk, ptRisk2, ptRisk3) + val ewDayRiskList: List<ExposureWindowDayRisk> = listOf(ewRisk, ewRisk2, ewRisk3, ewRisk4) + val result = combineRisk(ptDayRiskList, ewDayRiskList) result.size shouldBe 5 result.find { it.localDate == LocalDate(2021, 3, 15) @@ -71,15 +75,87 @@ class CombineRiskTest { }!!.riskState shouldBe RiskState.INCREASED_RISK } + @Test + fun `combineEwPtRiskLevelResults works`() { + val startInstant = Instant.ofEpochMilli(10000) + + val ptResult = PtRiskLevelResult( + calculatedAt = startInstant.plus(1000L), + riskState = RiskState.LOW_RISK + ) + val ptResult2 = PtRiskLevelResult( + calculatedAt = startInstant.plus(3000L), + riskState = RiskState.CALCULATION_FAILED + ) + val ptResult3 = PtRiskLevelResult( + calculatedAt = startInstant.plus(6000L), + riskState = RiskState.CALCULATION_FAILED + ) + val ptResult4 = PtRiskLevelResult( + calculatedAt = startInstant.plus(7000L), + riskState = RiskState.CALCULATION_FAILED + ) + + val ptResults = listOf(ptResult, ptResult2, ptResult4, ptResult3) + val ewResult = createEwRiskLevelResult( + calculatedAt = startInstant.plus(2000L), + riskState = RiskState.CALCULATION_FAILED + ) + val ewResult2 = createEwRiskLevelResult( + calculatedAt = startInstant.plus(4000L), + riskState = RiskState.INCREASED_RISK + ) + val ewResult3 = createEwRiskLevelResult( + calculatedAt = startInstant.plus(5000L), + riskState = RiskState.CALCULATION_FAILED + ) + val ewResult4 = createEwRiskLevelResult( + calculatedAt = startInstant.plus(8000L), + riskState = RiskState.CALCULATION_FAILED + ) + val ewResults = listOf(ewResult, ewResult4, ewResult2, ewResult3) + val result = combineEwPtRiskLevelResults(ptResults, ewResults).sortedByDescending { it.calculatedAt } + result.size shouldBe 8 + result[0].riskState shouldBe RiskState.CALCULATION_FAILED + result[0].calculatedAt shouldBe startInstant.plus(8000L) + result[1].riskState shouldBe RiskState.CALCULATION_FAILED + result[1].calculatedAt shouldBe startInstant.plus(7000L) + result[2].riskState shouldBe RiskState.CALCULATION_FAILED + result[2].calculatedAt shouldBe startInstant.plus(6000L) + result[3].riskState shouldBe RiskState.CALCULATION_FAILED + result[3].calculatedAt shouldBe startInstant.plus(5000L) + result[4].riskState shouldBe RiskState.INCREASED_RISK + result[4].calculatedAt shouldBe startInstant.plus(4000L) + result[5].riskState shouldBe RiskState.CALCULATION_FAILED + result[5].calculatedAt shouldBe startInstant.plus(3000L) + result[6].riskState shouldBe RiskState.LOW_RISK + result[6].calculatedAt shouldBe startInstant.plus(2000L) + result[7].riskState shouldBe RiskState.LOW_RISK + result[7].calculatedAt shouldBe startInstant.plus(1000L) + } + @Test fun `max RiskState works`() { - max(RiskState.INCREASED_RISK, RiskState.INCREASED_RISK) shouldBe RiskState.INCREASED_RISK - max(RiskState.INCREASED_RISK, RiskState.LOW_RISK) shouldBe RiskState.INCREASED_RISK - max(RiskState.INCREASED_RISK, RiskState.CALCULATION_FAILED) shouldBe RiskState.INCREASED_RISK - max(RiskState.LOW_RISK, RiskState.INCREASED_RISK) shouldBe RiskState.INCREASED_RISK - max(RiskState.CALCULATION_FAILED, RiskState.INCREASED_RISK) shouldBe RiskState.INCREASED_RISK - max(RiskState.LOW_RISK, RiskState.LOW_RISK) shouldBe RiskState.LOW_RISK - max(RiskState.CALCULATION_FAILED, RiskState.LOW_RISK) shouldBe RiskState.LOW_RISK - max(RiskState.CALCULATION_FAILED, RiskState.CALCULATION_FAILED) shouldBe RiskState.CALCULATION_FAILED + combine(RiskState.INCREASED_RISK, RiskState.INCREASED_RISK) shouldBe RiskState.INCREASED_RISK + combine(RiskState.INCREASED_RISK, RiskState.LOW_RISK) shouldBe RiskState.INCREASED_RISK + combine(RiskState.INCREASED_RISK, RiskState.CALCULATION_FAILED) shouldBe RiskState.INCREASED_RISK + combine(RiskState.LOW_RISK, RiskState.INCREASED_RISK) shouldBe RiskState.INCREASED_RISK + combine(RiskState.CALCULATION_FAILED, RiskState.INCREASED_RISK) shouldBe RiskState.INCREASED_RISK + combine(RiskState.LOW_RISK, RiskState.LOW_RISK) shouldBe RiskState.LOW_RISK + combine(RiskState.CALCULATION_FAILED, RiskState.LOW_RISK) shouldBe RiskState.LOW_RISK + combine(RiskState.CALCULATION_FAILED, RiskState.CALCULATION_FAILED) shouldBe RiskState.CALCULATION_FAILED } } + +private fun createEwRiskLevelResult( + calculatedAt: Instant, + riskState: RiskState +): EwRiskLevelResult = object : EwRiskLevelResult { + override val calculatedAt: Instant = calculatedAt + override val riskState: RiskState = riskState + override val failureReason: EwRiskLevelResult.FailureReason? = null + override val ewAggregatedRiskResult: EwAggregatedRiskResult? = null + override val exposureWindows: List<ExposureWindow>? = null + override val matchedKeyCount: Int = 0 + override val daysWithEncounters: Int = 0 +} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/storage/RiskStorageTestData.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/storage/RiskStorageTestData.kt index 34c79dac15a342d5194b7f53f23d8db624fad6e8..5cb8e090ce739ed51abda4b3e1dc11f3327d2983 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/storage/RiskStorageTestData.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/storage/RiskStorageTestData.kt @@ -14,9 +14,11 @@ import org.joda.time.Instant object RiskStorageTestData { + val ewCalculatedAt = Instant.ofEpochMilli(9999L) + val testRiskLevelResultDao = PersistedRiskLevelResultDao( id = "riskresult-id", - calculatedAt = Instant.ofEpochMilli(9999L), + calculatedAt = ewCalculatedAt, failureReason = null, aggregatedRiskResult = PersistedRiskLevelResultDao.PersistedAggregatedRiskResult( totalRiskLevel = RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping.RiskLevel.HIGH, @@ -40,7 +42,7 @@ object RiskStorageTestData { ) val testRisklevelResult = EwRiskLevelTaskResult( - calculatedAt = Instant.ofEpochMilli(9999L), + calculatedAt = ewCalculatedAt, ewAggregatedRiskResult = testAggregatedRiskResult, exposureWindows = null ) @@ -76,14 +78,14 @@ object RiskStorageTestData { }.build() val testAggregatedRiskPerDateResult = ExposureWindowDayRisk( - dateMillisSinceEpoch = 9999L, + dateMillisSinceEpoch = ewCalculatedAt.millis, riskLevel = RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping.RiskLevel.HIGH, minimumDistinctEncountersWithLowRisk = 0, minimumDistinctEncountersWithHighRisk = 0 ) val testPersistedAggregatedRiskPerDateResult = PersistedAggregatedRiskPerDateResult( - dateMillisSinceEpoch = 9999L, + dateMillisSinceEpoch = ewCalculatedAt.millis, riskLevel = RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping.RiskLevel.HIGH, minimumDistinctEncountersWithLowRisk = 0, minimumDistinctEncountersWithHighRisk = 0 diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/storage/SubmissionRepositoryTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/storage/SubmissionRepositoryTest.kt index 30de3e9892402fc4c54632028487059802e9ea8c..38282aaf96e40847a257eaca7047f53b675770d1 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/storage/SubmissionRepositoryTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/storage/SubmissionRepositoryTest.kt @@ -18,6 +18,7 @@ import de.rki.coronawarnapp.util.di.ApplicationComponent import de.rki.coronawarnapp.util.encryptionmigration.EncryptedPreferencesFactory import de.rki.coronawarnapp.util.encryptionmigration.EncryptionErrorResetTool import de.rki.coronawarnapp.util.formatter.TestResult +import de.rki.coronawarnapp.worker.BackgroundWorkScheduler import io.kotest.matchers.shouldBe import io.mockk.MockKAnnotations import io.mockk.Runs @@ -57,6 +58,7 @@ class SubmissionRepositoryTest : BaseTest() { @MockK lateinit var analyticsKeySubmissionCollector: AnalyticsKeySubmissionCollector @MockK lateinit var tracingSettings: TracingSettings @MockK lateinit var testResultDataCollector: TestResultDataCollector + @MockK lateinit var backgroundWorkScheduler: BackgroundWorkScheduler private val guid = "123456-12345678-1234-4DA7-B166-B86D85475064" private val tan = "123456-12345678-1234-4DA7-B166-B86D85475064" @@ -99,6 +101,10 @@ class SubmissionRepositoryTest : BaseTest() { every { testResultDataCollector.updatePendingTestResultReceivedTime(any()) } just Runs coEvery { testResultDataCollector.saveTestResultAnalyticsSettings(any()) } just Runs every { testResultDataCollector.clear() } just Runs + + backgroundWorkScheduler.apply { + every { startWorkScheduler() } just Runs + } } fun createInstance(scope: CoroutineScope) = SubmissionRepository( @@ -111,7 +117,8 @@ class SubmissionRepositoryTest : BaseTest() { backgroundNoise = backgroundNoise, analyticsKeySubmissionCollector = analyticsKeySubmissionCollector, tracingSettings = tracingSettings, - testResultDataCollector = testResultDataCollector + testResultDataCollector = testResultDataCollector, + backgroundWorkScheduler = backgroundWorkScheduler ) @Test diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/submission/task/SubmissionTaskTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/submission/task/SubmissionTaskTest.kt index 0cb6a4ad75326870cbe5b8c7487448a2a64689bd..2802029978aae963e691b4abaa25e40ec5cac9cd 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/submission/task/SubmissionTaskTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/submission/task/SubmissionTaskTest.kt @@ -30,7 +30,6 @@ 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.verify import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.flowOf @@ -63,6 +62,7 @@ class SubmissionTaskTest : BaseTest() { @MockK lateinit var analyticsKeySubmissionCollector: AnalyticsKeySubmissionCollector @MockK lateinit var checkInsTransformer: CheckInsTransformer @MockK lateinit var checkInRepository: CheckInRepository + @MockK lateinit var backgroundWorkScheduler: BackgroundWorkScheduler private lateinit var settingSymptomsPreference: FlowPreference<Symptoms?> @@ -80,9 +80,8 @@ class SubmissionTaskTest : BaseTest() { every { submissionSettings.registrationToken } returns registrationToken every { submissionSettings.isSubmissionSuccessful = any() } just Runs - mockkObject(BackgroundWorkScheduler) - every { BackgroundWorkScheduler.stopWorkScheduler() } just Runs - every { BackgroundWorkScheduler.startWorkScheduler() } just Runs + every { backgroundWorkScheduler.stopWorkScheduler() } just Runs + every { backgroundWorkScheduler.startWorkScheduler() } just Runs every { tekBatch.keys } returns listOf(tek) every { tekHistoryStorage.tekData } returns flowOf(listOf(tekBatch)) @@ -131,7 +130,8 @@ class SubmissionTaskTest : BaseTest() { testResultAvailableNotificationService = testResultAvailableNotificationService, analyticsKeySubmissionCollector = analyticsKeySubmissionCollector, checkInsRepository = checkInRepository, - checkInsTransformer = checkInsTransformer + checkInsTransformer = checkInsTransformer, + backgroundWorkScheduler = backgroundWorkScheduler, ) @Test @@ -183,9 +183,9 @@ class SubmissionTaskTest : BaseTest() { autoSubmission.updateMode(AutoSubmission.Mode.DISABLED) - BackgroundWorkScheduler.stopWorkScheduler() + backgroundWorkScheduler.stopWorkScheduler() submissionSettings.isSubmissionSuccessful = true - BackgroundWorkScheduler.startWorkScheduler() + backgroundWorkScheduler.startWorkScheduler() shareTestResultNotificationService.cancelSharePositiveTestResultNotification() testResultAvailableNotificationService.cancelTestResultAvailableNotification() diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/tracing/ui/details/TracingDetailsItemProviderTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/tracing/ui/details/TracingDetailsItemProviderTest.kt index 27175e36d1849b303abc331b0bf1ebbe25b838ce..daf12a7211a3c53bb238e1cb84de4f73bda21e4c 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/tracing/ui/details/TracingDetailsItemProviderTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/tracing/ui/details/TracingDetailsItemProviderTest.kt @@ -5,12 +5,11 @@ import android.content.res.Resources import com.google.android.gms.nearby.exposurenotification.ExposureWindow import de.rki.coronawarnapp.datadonation.survey.Surveys import de.rki.coronawarnapp.installTime.InstallTimeProvider -import de.rki.coronawarnapp.presencetracing.risk.PtRiskLevelResult +import de.rki.coronawarnapp.risk.CombinedEwPtRiskLevelResult import de.rki.coronawarnapp.risk.EwRiskLevelTaskResult -import de.rki.coronawarnapp.risk.ProtoRiskLevel +import de.rki.coronawarnapp.risk.LastCombinedRiskResults import de.rki.coronawarnapp.risk.RiskState import de.rki.coronawarnapp.risk.result.EwAggregatedRiskResult -import de.rki.coronawarnapp.risk.storage.CombinedEwPtRiskLevelResult import de.rki.coronawarnapp.risk.storage.RiskLevelStorage import de.rki.coronawarnapp.tracing.GeneralTracingStatus import de.rki.coronawarnapp.tracing.ui.details.items.additionalinfos.AdditionalInfoLowRiskBox @@ -46,6 +45,8 @@ class TracingDetailsItemProviderTest : BaseTest() { @MockK lateinit var installTimeProvider: InstallTimeProvider @MockK lateinit var surveys: Surveys + @MockK(relaxed = true) lateinit var combinedResult: CombinedEwPtRiskLevelResult + @BeforeEach fun setup() { MockKAnnotations.init(this) @@ -61,22 +62,23 @@ class TracingDetailsItemProviderTest : BaseTest() { private fun prepare( status: GeneralTracingStatus.Status, - riskLevel: ProtoRiskLevel, + riskState: RiskState, matchedKeyCount: Int, daysSinceInstallation: Long, availableSurveys: List<Surveys.Type> = emptyList() ) { every { tracingStatus.generalStatus } returns flowOf(status) - every { ewAggregatedRiskResult.totalRiskLevel } returns riskLevel every { installTimeProvider.daysSinceInstallation } returns daysSinceInstallation every { surveys.availableSurveys } returns flowOf(availableSurveys) - if (riskLevel == ProtoRiskLevel.LOW) { + if (riskState == RiskState.LOW_RISK) { every { ewAggregatedRiskResult.isLowRisk() } returns true - } else if (riskLevel == ProtoRiskLevel.HIGH) { + } else if (riskState == RiskState.INCREASED_RISK) { every { ewAggregatedRiskResult.isIncreasedRisk() } returns true } + every { combinedResult.riskState } returns riskState + val exposureWindow: ExposureWindow = mockk() val ewRiskLevelTaskResult = EwRiskLevelTaskResult( @@ -85,18 +87,16 @@ class TracingDetailsItemProviderTest : BaseTest() { exposureWindows = listOf(exposureWindow) ) - val ptRiskLevelResult = PtRiskLevelResult( - calculatedAt = Instant.EPOCH, - riskState = RiskState.CALCULATION_FAILED - ) - val combined = CombinedEwPtRiskLevelResult( - ewRiskLevelResult = ewRiskLevelTaskResult, - ptRiskLevelResult = ptRiskLevelResult + every { combinedResult.ewRiskLevelResult } returns ewRiskLevelTaskResult + + val lastCombined = LastCombinedRiskResults( + lastCalculated = combinedResult, + lastSuccessfullyCalculated = combinedResult ) every { ewRiskLevelTaskResult.matchedKeyCount } returns matchedKeyCount - every { riskLevelStorage.latestAndLastSuccessfulEwRiskLevelResult } returns flowOf(listOf(ewRiskLevelTaskResult)) - // TODO tests - every { riskLevelStorage.latestAndLastSuccessfulCombinedEwPtRiskLevelResult } returns flowOf(listOf(combined)) + every { riskLevelStorage.latestAndLastSuccessfulEwRiskLevelResult } returns + flowOf(listOf(ewRiskLevelTaskResult)) + every { riskLevelStorage.latestAndLastSuccessfulCombinedEwPtRiskLevelResult } returns flowOf(lastCombined) } @Test @@ -104,7 +104,7 @@ class TracingDetailsItemProviderTest : BaseTest() { prepare( status = GeneralTracingStatus.Status.TRACING_ACTIVE, - riskLevel = ProtoRiskLevel.LOW, + riskState = RiskState.LOW_RISK, daysSinceInstallation = 4, matchedKeyCount = 1 ) @@ -121,7 +121,7 @@ class TracingDetailsItemProviderTest : BaseTest() { prepare( status = GeneralTracingStatus.Status.TRACING_ACTIVE, - riskLevel = ProtoRiskLevel.LOW, + riskState = RiskState.LOW_RISK, daysSinceInstallation = 4, matchedKeyCount = 0 ) @@ -138,7 +138,7 @@ class TracingDetailsItemProviderTest : BaseTest() { prepare( status = GeneralTracingStatus.Status.TRACING_ACTIVE, - riskLevel = ProtoRiskLevel.HIGH, + riskState = RiskState.INCREASED_RISK, daysSinceInstallation = 4, matchedKeyCount = 0 ) @@ -155,7 +155,7 @@ class TracingDetailsItemProviderTest : BaseTest() { prepare( status = GeneralTracingStatus.Status.TRACING_ACTIVE, - riskLevel = ProtoRiskLevel.HIGH, + riskState = RiskState.INCREASED_RISK, daysSinceInstallation = 4, matchedKeyCount = 0 ) @@ -174,7 +174,7 @@ class TracingDetailsItemProviderTest : BaseTest() { prepare( status = GeneralTracingStatus.Status.TRACING_ACTIVE, - riskLevel = ProtoRiskLevel.LOW, + riskState = RiskState.LOW_RISK, daysSinceInstallation = 4, matchedKeyCount = 0 ) @@ -193,7 +193,7 @@ class TracingDetailsItemProviderTest : BaseTest() { prepare( status = GeneralTracingStatus.Status.TRACING_ACTIVE, - riskLevel = ProtoRiskLevel.LOW, + riskState = RiskState.LOW_RISK, daysSinceInstallation = 4, matchedKeyCount = 0, ) @@ -213,7 +213,7 @@ class TracingDetailsItemProviderTest : BaseTest() { prepare( status = GeneralTracingStatus.Status.TRACING_ACTIVE, - riskLevel = ProtoRiskLevel.HIGH, + riskState = RiskState.INCREASED_RISK, daysSinceInstallation = 4, matchedKeyCount = 0 ) @@ -233,7 +233,7 @@ class TracingDetailsItemProviderTest : BaseTest() { prepare( status = GeneralTracingStatus.Status.TRACING_ACTIVE, - riskLevel = ProtoRiskLevel.UNRECOGNIZED, + riskState = RiskState.CALCULATION_FAILED, daysSinceInstallation = 4, matchedKeyCount = 0 ) @@ -253,7 +253,7 @@ class TracingDetailsItemProviderTest : BaseTest() { prepare( status = GeneralTracingStatus.Status.TRACING_INACTIVE, - riskLevel = ProtoRiskLevel.LOW, + riskState = RiskState.LOW_RISK, daysSinceInstallation = 4, matchedKeyCount = 0 ) @@ -273,7 +273,7 @@ class TracingDetailsItemProviderTest : BaseTest() { prepare( status = GeneralTracingStatus.Status.TRACING_INACTIVE, - riskLevel = ProtoRiskLevel.LOW, + riskState = RiskState.LOW_RISK, daysSinceInstallation = 4, matchedKeyCount = 0 ) @@ -296,7 +296,7 @@ class TracingDetailsItemProviderTest : BaseTest() { prepare( status = GeneralTracingStatus.Status.TRACING_ACTIVE, - riskLevel = ProtoRiskLevel.UNRECOGNIZED, + riskState = RiskState.CALCULATION_FAILED, daysSinceInstallation = 4, matchedKeyCount = 0 ) @@ -319,7 +319,7 @@ class TracingDetailsItemProviderTest : BaseTest() { prepare( status = GeneralTracingStatus.Status.TRACING_ACTIVE, - riskLevel = ProtoRiskLevel.LOW, + riskState = RiskState.LOW_RISK, daysSinceInstallation = 4, matchedKeyCount = 0 ) @@ -342,7 +342,7 @@ class TracingDetailsItemProviderTest : BaseTest() { prepare( status = GeneralTracingStatus.Status.TRACING_ACTIVE, - riskLevel = ProtoRiskLevel.HIGH, + riskState = RiskState.INCREASED_RISK, daysSinceInstallation = 4, matchedKeyCount = 0 ) @@ -365,7 +365,7 @@ class TracingDetailsItemProviderTest : BaseTest() { prepare( status = GeneralTracingStatus.Status.TRACING_ACTIVE, - riskLevel = ProtoRiskLevel.LOW, + riskState = RiskState.LOW_RISK, matchedKeyCount = 0, daysSinceInstallation = 4, availableSurveys = listOf(Surveys.Type.HIGH_RISK_ENCOUNTER) @@ -386,7 +386,7 @@ class TracingDetailsItemProviderTest : BaseTest() { prepare( status = GeneralTracingStatus.Status.TRACING_ACTIVE, - riskLevel = ProtoRiskLevel.HIGH, + riskState = RiskState.INCREASED_RISK, matchedKeyCount = 0, daysSinceInstallation = 4, availableSurveys = emptyList() @@ -407,7 +407,7 @@ class TracingDetailsItemProviderTest : BaseTest() { prepare( status = GeneralTracingStatus.Status.TRACING_ACTIVE, - riskLevel = ProtoRiskLevel.HIGH, + riskState = RiskState.INCREASED_RISK, matchedKeyCount = 0, daysSinceInstallation = 4, availableSurveys = listOf(Surveys.Type.HIGH_RISK_ENCOUNTER) diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/eventregistration/attendee/checkins/CheckInsViewModelTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/eventregistration/attendee/checkins/CheckInsViewModelTest.kt index 77d37b6285bcc0b9d4981ce580d366e7440d0ed8..bfbd074700697d48cbfc6eebf01d6c887164530c 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/eventregistration/attendee/checkins/CheckInsViewModelTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/eventregistration/attendee/checkins/CheckInsViewModelTest.kt @@ -5,6 +5,7 @@ import de.rki.coronawarnapp.eventregistration.checkins.CheckIn import de.rki.coronawarnapp.eventregistration.checkins.CheckInRepository import de.rki.coronawarnapp.eventregistration.checkins.qrcode.QRCodeUriParser import de.rki.coronawarnapp.presencetracing.checkins.checkout.CheckOutHandler +import de.rki.coronawarnapp.server.protocols.internal.pt.TraceLocationOuterClass import de.rki.coronawarnapp.ui.eventregistration.attendee.checkins.items.ActiveCheckInVH import de.rki.coronawarnapp.ui.eventregistration.attendee.checkins.items.CameraPermissionVH import de.rki.coronawarnapp.ui.eventregistration.attendee.checkins.items.PastCheckInVH @@ -23,7 +24,6 @@ import io.mockk.verify import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runBlockingTest -import okio.ByteString import org.joda.time.Instant import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test @@ -80,11 +80,12 @@ class CheckInsViewModelTest : BaseTest() { @Test fun `DeepLink verification`() = runBlockingTest { every { savedState.get<String>(any()) } returns null - every { qrCodeUriParser.getQrCodePayload(any()) } returns ByteString.EMPTY + coEvery { qrCodeUriParser.getQrCodePayload(any()) } returns + TraceLocationOuterClass.QRCodePayload.newBuilder().build() createInstance(deepLink = DEEP_LINK, scope = this).apply { events.getOrAwaitValue().shouldBeInstanceOf<CheckInEvent.ConfirmCheckIn>() - verify { + coVerify { savedState.get<String>(any()) qrCodeUriParser.getQrCodePayload(any()) savedState.set(any(), any<String>()) diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/eventregistration/organizer/poster/QrCodePosterViewModelTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/eventregistration/organizer/poster/QrCodePosterViewModelTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..7ead8a9e213ea9be2b9fb76e8e05cfda3975a3ed --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/eventregistration/organizer/poster/QrCodePosterViewModelTest.kt @@ -0,0 +1,77 @@ +package de.rki.coronawarnapp.ui.eventregistration.organizer.poster + +import android.graphics.Bitmap +import de.rki.coronawarnapp.eventregistration.checkins.qrcode.PosterTemplateProvider +import de.rki.coronawarnapp.eventregistration.checkins.qrcode.QrCodeGenerator +import de.rki.coronawarnapp.eventregistration.checkins.qrcode.Template +import de.rki.coronawarnapp.eventregistration.checkins.qrcode.TraceLocation +import de.rki.coronawarnapp.eventregistration.storage.repo.TraceLocationRepository +import de.rki.coronawarnapp.server.protocols.internal.pt.QrCodePosterTemplate.QRCodePosterTemplateAndroid.QRCodeTextBoxAndroid +import de.rki.coronawarnapp.util.files.FileSharing +import io.kotest.matchers.shouldBe +import io.mockk.MockKAnnotations +import io.mockk.coEvery +import io.mockk.every +import io.mockk.impl.annotations.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.TestDispatcherProvider +import testhelpers.extensions.InstantExecutorExtension +import testhelpers.extensions.getOrAwaitValue + +@ExtendWith(InstantExecutorExtension::class) +class QrCodePosterViewModelTest : BaseTest() { + + @MockK lateinit var qrCodeGenerator: QrCodeGenerator + @MockK lateinit var posterTemplateProvider: PosterTemplateProvider + @MockK lateinit var traceLocationRepository: TraceLocationRepository + @MockK lateinit var fileSharing: FileSharing + @MockK lateinit var qrCodeBitmap: Bitmap + @MockK lateinit var templateBitmap: Bitmap + @MockK lateinit var textBox: QRCodeTextBoxAndroid + @MockK lateinit var traceLocation: TraceLocation + private lateinit var template: Template + + @BeforeEach + fun setup() { + MockKAnnotations.init(this) + + template = Template( + bitmap = templateBitmap, + width = 500, + height = 600, + offsetX = 0.15f, + offsetY = 0.14f, + qrCodeLength = 500, + textBox = textBox + ) + + coEvery { qrCodeGenerator.createQrCode("locationUrl", any(), any()) } returns qrCodeBitmap + coEvery { posterTemplateProvider.template() } returns template + coEvery { traceLocationRepository.traceLocationForId(any()) } returns traceLocation.apply { + every { description } returns "description" + every { address } returns "address" + every { locationUrl } returns "locationUrl" + } + } + + @Test + fun `Poster is requested in init`() { + createInstance().poster.getOrAwaitValue() shouldBe Poster( + qrCode = qrCodeBitmap, + template = template, + infoText = "description\naddress" + ) + } + + private fun createInstance() = QrCodePosterViewModel( + traceLocationId = 1, + dispatcher = TestDispatcherProvider(), + qrCodeGenerator = qrCodeGenerator, + posterTemplateProvider = posterTemplateProvider, + traceLocationRepository = traceLocationRepository, + fileSharing = fileSharing + ) +} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/ProtoBufKtTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/ProtoBufKtTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..b1678a5b59974ee94f11483bd2965ec4fc1c0008 --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/ProtoBufKtTest.kt @@ -0,0 +1,28 @@ +package de.rki.coronawarnapp.util + +import com.google.protobuf.ByteString +import io.kotest.matchers.shouldBe +import okio.ByteString.Companion.toByteString +import org.junit.jupiter.api.Test +import testhelpers.BaseTest + +class ProtoBufKtTest : BaseTest() { + + @Test + fun toProtoByteString() { + val okioByteString = KEY.toByteArray().toByteString() + + okioByteString.toProtoByteString() shouldBe ByteString.copyFromUtf8(KEY) + } + + @Test + fun toOkioByteString() { + val protoByteString = ByteString.copyFromUtf8(KEY) + + protoByteString.toOkioByteString() shouldBe KEY.toByteArray().toByteString() + } + + companion object { + private const val KEY = "No generated key" + } +} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/TimeAndDateExtensionsTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/TimeAndDateExtensionsTest.kt index a02df66dd129ca32d506ebb2ae18343436000264..4c7411bca6a60422e7bdd71bb58d47e0c792f4e1 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/TimeAndDateExtensionsTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/TimeAndDateExtensionsTest.kt @@ -4,6 +4,7 @@ import de.rki.coronawarnapp.CoronaWarnApplication import de.rki.coronawarnapp.util.TimeAndDateExtensions.ageInDays import de.rki.coronawarnapp.util.TimeAndDateExtensions.calculateDays import de.rki.coronawarnapp.util.TimeAndDateExtensions.derive10MinutesInterval +import de.rki.coronawarnapp.util.TimeAndDateExtensions.deriveHourInterval import de.rki.coronawarnapp.util.TimeAndDateExtensions.getCurrentHourUTC import de.rki.coronawarnapp.util.TimeAndDateExtensions.seconds import de.rki.coronawarnapp.util.TimeAndDateExtensions.secondsToInstant @@ -99,4 +100,16 @@ class TimeAndDateExtensionsTest : BaseTest() { Instant.parse("1970-01-01T00:00:00.000Z") .derive10MinutesInterval() shouldBe 0 } + + @Test + fun `derive 1 hour interval should be 0`() { + Instant.parse("1970-01-01T00:00:00.000Z") + .deriveHourInterval() shouldBe 0 + } + + @Test + fun `derive 1 hour interval`() { + Instant.parse("2021-02-15T13:52:05+00:00") + .deriveHourInterval() shouldBe 448165 + } } diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/formatter/FormatterStatisticsHelperTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/formatter/FormatterStatisticsHelperTest.kt index 32de838776c99e74a17e6837df6e5165e24a7b8f..9a63024547371943a6998ac85d9ed57d096590b1 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/formatter/FormatterStatisticsHelperTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/formatter/FormatterStatisticsHelperTest.kt @@ -13,10 +13,10 @@ import io.mockk.every import io.mockk.impl.annotations.MockK import io.mockk.mockkStatic import io.mockk.slot -import org.joda.time.DateTime +import org.joda.time.Duration import org.joda.time.Instant -import org.junit.Before -import org.junit.Test +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test import testhelpers.BaseTest import java.util.Locale @@ -26,9 +26,9 @@ class FormatterStatisticsHelperTest : BaseTest() { private lateinit var context: Context private val today = Instant() - private val yesterday = DateTime().minusDays(1).toInstant() + private val yesterday = today.minus(Duration.standardDays(1)) - @Before + @BeforeEach fun setUp() { val slot = slot<String>() MockKAnnotations.init(this) diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/worker/CWAWorkerFactoryTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/worker/CWAWorkerFactoryTest.kt index 4fdcc480084e1ab05272eeff493aa49a5880a4ca..ef18b7fd626748acc031103cd5e3adf605803a74 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/worker/CWAWorkerFactoryTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/worker/CWAWorkerFactoryTest.kt @@ -4,7 +4,7 @@ import android.content.Context import androidx.work.ListenableWorker import androidx.work.WorkerParameters import androidx.work.impl.workers.DiagnosticsWorker -import de.rki.coronawarnapp.worker.DiagnosisKeyRetrievalOneTimeWorker +import de.rki.coronawarnapp.diagnosiskeys.execution.DiagnosisKeyRetrievalWorker import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldNotBe import io.mockk.MockKAnnotations @@ -22,15 +22,15 @@ class CWAWorkerFactoryTest : BaseTest() { @MockK lateinit var context: Context @MockK lateinit var workerParameters: WorkerParameters - @MockK lateinit var ourWorker: DiagnosisKeyRetrievalOneTimeWorker - @MockK lateinit var ourFactory: DiagnosisKeyRetrievalOneTimeWorker.Factory + @MockK lateinit var ourWorker: DiagnosisKeyRetrievalWorker + @MockK lateinit var ourFactory: DiagnosisKeyRetrievalWorker.Factory @BeforeEach fun setup() { MockKAnnotations.init(this) every { ourFactory.create(context, workerParameters) } returns ourWorker - workerFactories[DiagnosisKeyRetrievalOneTimeWorker::class.java] = Provider { ourFactory } + workerFactories[DiagnosisKeyRetrievalWorker::class.java] = Provider { ourFactory } } fun createInstance() = CWAWorkerFactory( @@ -42,7 +42,7 @@ class CWAWorkerFactoryTest : BaseTest() { val instance = createInstance() instance.createWorker( context, - DiagnosisKeyRetrievalOneTimeWorker::class.qualifiedName!!, + DiagnosisKeyRetrievalWorker::class.qualifiedName!!, workerParameters ) shouldBe ourWorker } @@ -55,4 +55,20 @@ class CWAWorkerFactoryTest : BaseTest() { val worker2 = instance.createWorker(context, DiagnosticsWorker::class.qualifiedName!!, workerParameters) worker1 shouldNotBe worker2 } + + /** + * Workers are initialized based on their class name. + * That class name is stored as part of the worker arguments. + * If we refactor a worker, the previously used classname will be unknown. + * Returning null will cause the periodic worker to be dequeued. + */ + @Test + fun `class names that can not be instantiated are treated like an unknown worker`() { + val instance = createInstance() + instance.createWorker( + context, + "abc.im.a.ghost.def", + workerParameters + ) shouldBe null + } } diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/worker/WorkerBinderTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/worker/WorkerBinderTest.kt index 4c7049e9fff4c1c9efa7929b797834418261c971..67bdaf6650019a2e6368f755d05c613af186b8d9 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/worker/WorkerBinderTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/worker/WorkerBinderTest.kt @@ -18,6 +18,7 @@ import de.rki.coronawarnapp.notification.TestResultAvailableNotificationService import de.rki.coronawarnapp.playbook.Playbook import de.rki.coronawarnapp.presencetracing.checkins.checkout.CheckOutNotification import de.rki.coronawarnapp.presencetracing.checkins.checkout.auto.AutoCheckOut +import de.rki.coronawarnapp.risk.execution.RiskWorkScheduler import de.rki.coronawarnapp.risk.storage.RiskLevelStorage import de.rki.coronawarnapp.task.TaskController import de.rki.coronawarnapp.util.di.AppContext @@ -143,4 +144,7 @@ class MockProvider { @Provides fun checkOutNotification(): CheckOutNotification = mockk() + + @Provides + fun riskWorkScheduler(): RiskWorkScheduler = mockk() } diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/worker/BackgroundWorkBuilderTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/worker/BackgroundWorkBuilderTest.kt deleted file mode 100644 index f77fd6a7e8ebd20758a1949af5652484ac69ac48..0000000000000000000000000000000000000000 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/worker/BackgroundWorkBuilderTest.kt +++ /dev/null @@ -1,16 +0,0 @@ -package de.rki.coronawarnapp.worker - -import io.kotest.matchers.shouldBe -import org.joda.time.Duration -import org.junit.jupiter.api.Test -import testhelpers.BaseTest - -class BackgroundWorkBuilderTest : BaseTest() { - - @Test - fun `worker interval for key retrieval is 60 minutes, once every hour`() { - buildDiagnosisKeyRetrievalPeriodicWork().apply { - workSpec.intervalDuration shouldBe Duration.standardMinutes(60).millis - } - } -} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/worker/DiagnosisTestResultRetrievalPeriodicWorkerTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/worker/DiagnosisTestResultRetrievalPeriodicWorkerTest.kt index 499dada21cc74a244210f6dacbb57dec888023ba..a59702bd5d771dbc90c8fe80cf09ec523e6f83bb 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/worker/DiagnosisTestResultRetrievalPeriodicWorkerTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/worker/DiagnosisTestResultRetrievalPeriodicWorkerTest.kt @@ -2,7 +2,6 @@ package de.rki.coronawarnapp.worker import android.content.Context import androidx.work.ListenableWorker -import androidx.work.Operation import androidx.work.WorkRequest import androidx.work.WorkerParameters import de.rki.coronawarnapp.notification.GeneralNotifications @@ -18,7 +17,6 @@ import de.rki.coronawarnapp.util.di.ApplicationComponent import de.rki.coronawarnapp.util.encryptionmigration.EncryptedPreferencesFactory import de.rki.coronawarnapp.util.encryptionmigration.EncryptionErrorResetTool import de.rki.coronawarnapp.util.formatter.TestResult -import de.rki.coronawarnapp.worker.BackgroundWorkScheduler.stop import io.kotest.matchers.shouldBe import io.mockk.MockKAnnotations import io.mockk.Runs @@ -48,9 +46,10 @@ class DiagnosisTestResultRetrievalPeriodicWorkerTest : BaseTest() { @MockK lateinit var appComponent: ApplicationComponent @MockK lateinit var encryptedPreferencesFactory: EncryptedPreferencesFactory @MockK lateinit var encryptionErrorResetTool: EncryptionErrorResetTool - @MockK lateinit var operation: Operation @MockK lateinit var timeStamper: TimeStamper @MockK lateinit var tracingSettings: TracingSettings + @MockK lateinit var backgroundWorkScheduler: BackgroundWorkScheduler + @RelaxedMockK lateinit var workerParams: WorkerParameters private val currentInstant = Instant.ofEpochSecond(1611764225) private val registrationToken = "test token" @@ -72,8 +71,7 @@ class DiagnosisTestResultRetrievalPeriodicWorkerTest : BaseTest() { every { submissionSettings.registrationToken } returns mockFlowPreference(registrationToken) - mockkObject(BackgroundWorkScheduler) - every { BackgroundWorkScheduler.WorkType.DIAGNOSIS_TEST_RESULT_PERIODIC_WORKER.stop() } returns operation + every { backgroundWorkScheduler.stopDiagnosisTestResultPeriodicWork() } just Runs } @Test @@ -83,7 +81,7 @@ class DiagnosisTestResultRetrievalPeriodicWorkerTest : BaseTest() { val worker = createWorker() val result = worker.doWork() coVerify(exactly = 0) { submissionService.asyncRequestTestResult(any()) } - verify(exactly = 1) { BackgroundWorkScheduler.WorkType.DIAGNOSIS_TEST_RESULT_PERIODIC_WORKER.stop() } + verify(exactly = 1) { backgroundWorkScheduler.stopDiagnosisTestResultPeriodicWork() } result shouldBe ListenableWorker.Result.success() } } @@ -95,7 +93,7 @@ class DiagnosisTestResultRetrievalPeriodicWorkerTest : BaseTest() { val worker = createWorker() val result = worker.doWork() coVerify(exactly = 0) { submissionService.asyncRequestTestResult(any()) } - verify(exactly = 1) { BackgroundWorkScheduler.WorkType.DIAGNOSIS_TEST_RESULT_PERIODIC_WORKER.stop() } + verify(exactly = 1) { backgroundWorkScheduler.stopDiagnosisTestResultPeriodicWork() } result shouldBe ListenableWorker.Result.success() } } @@ -109,7 +107,7 @@ class DiagnosisTestResultRetrievalPeriodicWorkerTest : BaseTest() { val worker = createWorker() val result = worker.doWork() coVerify(exactly = 0) { submissionService.asyncRequestTestResult(any()) } - verify(exactly = 1) { BackgroundWorkScheduler.WorkType.DIAGNOSIS_TEST_RESULT_PERIODIC_WORKER.stop() } + verify(exactly = 1) { backgroundWorkScheduler.stopDiagnosisTestResultPeriodicWork() } result shouldBe ListenableWorker.Result.success() } } @@ -228,7 +226,7 @@ class DiagnosisTestResultRetrievalPeriodicWorkerTest : BaseTest() { NotificationConstants.NEW_MESSAGE_RISK_LEVEL_SCORE_NOTIFICATION_ID ) } - coVerify(exactly = 0) { BackgroundWorkScheduler.WorkType.DIAGNOSIS_TEST_RESULT_PERIODIC_WORKER.stop() } + coVerify(exactly = 0) { backgroundWorkScheduler.stopDiagnosisTestResultPeriodicWork() } result shouldBe ListenableWorker.Result.success() } } @@ -240,7 +238,7 @@ class DiagnosisTestResultRetrievalPeriodicWorkerTest : BaseTest() { val worker = createWorker() val result = worker.doWork() coVerify(exactly = 1) { submissionService.asyncRequestTestResult(any()) } - coVerify(exactly = 0) { BackgroundWorkScheduler.WorkType.DIAGNOSIS_TEST_RESULT_PERIODIC_WORKER.stop() } + coVerify(exactly = 0) { backgroundWorkScheduler.stopDiagnosisTestResultPeriodicWork() } result shouldBe ListenableWorker.Result.retry() } } @@ -253,6 +251,7 @@ class DiagnosisTestResultRetrievalPeriodicWorkerTest : BaseTest() { submissionSettings, submissionService, timeStamper, - tracingSettings + tracingSettings, + backgroundWorkScheduler, ) } diff --git a/Corona-Warn-App/src/testDevice/java/de/rki/coronawarnapp/test/risk/storage/DefaultRiskLevelStorageTest.kt b/Corona-Warn-App/src/testDevice/java/de/rki/coronawarnapp/test/risk/storage/DefaultRiskLevelStorageTest.kt index 8c676d6ab2f8cd5353ff3445e8919809507c5722..274a5e5f941e266f6db258ecf57cad71bd51e806 100644 --- a/Corona-Warn-App/src/testDevice/java/de/rki/coronawarnapp/test/risk/storage/DefaultRiskLevelStorageTest.kt +++ b/Corona-Warn-App/src/testDevice/java/de/rki/coronawarnapp/test/risk/storage/DefaultRiskLevelStorageTest.kt @@ -1,7 +1,7 @@ package de.rki.coronawarnapp.test.risk.storage import com.google.android.gms.nearby.exposurenotification.ExposureWindow -import de.rki.coronawarnapp.presencetracing.risk.PresenceTracingRiskRepository +import de.rki.coronawarnapp.presencetracing.risk.storage.PresenceTracingRiskRepository import de.rki.coronawarnapp.risk.EwRiskLevelTaskResult import de.rki.coronawarnapp.risk.result.EwAggregatedRiskResult import de.rki.coronawarnapp.risk.storage.DefaultRiskLevelStorage @@ -88,7 +88,7 @@ class DefaultRiskLevelStorageTest : BaseTest() { every { presenceTracingRiskRepository.traceLocationCheckInRiskStates } returns emptyFlow() every { presenceTracingRiskRepository.presenceTracingDayRisk } returns emptyFlow() - every { presenceTracingRiskRepository.latestAndLastSuccessful() } returns emptyFlow() + every { presenceTracingRiskRepository.allEntries() } returns emptyFlow() every { presenceTracingRiskRepository.latestEntries(any()) } returns emptyFlow() } diff --git a/Corona-Warn-App/src/testDeviceForTesters/java/de/rki/coronawarnapp/test/debugoptions/ui/DebugOptionsFragmentViewModelTest.kt b/Corona-Warn-App/src/testDeviceForTesters/java/de/rki/coronawarnapp/test/debugoptions/ui/DebugOptionsFragmentViewModelTest.kt index ed0342077389eb08c9590844454473015094eb94..bc5ae9e45a58d362c2de5a11464488638ac300c2 100644 --- a/Corona-Warn-App/src/testDeviceForTesters/java/de/rki/coronawarnapp/test/debugoptions/ui/DebugOptionsFragmentViewModelTest.kt +++ b/Corona-Warn-App/src/testDeviceForTesters/java/de/rki/coronawarnapp/test/debugoptions/ui/DebugOptionsFragmentViewModelTest.kt @@ -36,7 +36,9 @@ class DebugOptionsFragmentViewModelTest : BaseTestInstrumentation() { every { environmentSetup.downloadCdnUrl } returns "downloadUrl" every { environmentSetup.verificationCdnUrl } returns "verificationUrl" every { environmentSetup.dataDonationCdnUrl } returns "dataDonationUrl" - every { environmentSetup.qrCodePosterTemplateCdnUrl } returns "qrCodePosterTemplateUrl" + every { environmentSetup.logUploadServerUrl } returns "logUploadServerUrl" + every { environmentSetup.crowdNotifierPublicKey } returns "crowdNotifierPublicKey" + every { environmentSetup.appConfigPublicKey } returns "appConfigPublicKey" every { environmentSetup.currentEnvironment = any() } answers { currentEnvironment = arg(0) diff --git a/Corona-Warn-App/src/testDeviceForTesters/java/de/rki/coronawarnapp/test/risk/storage/DefaultRiskLevelStorageTest.kt b/Corona-Warn-App/src/testDeviceForTesters/java/de/rki/coronawarnapp/test/risk/storage/DefaultRiskLevelStorageTest.kt index 24478cb801c98a653901968debb4b0b5e1374967..72eae29914ebfe630caea23002104922563ca4dd 100644 --- a/Corona-Warn-App/src/testDeviceForTesters/java/de/rki/coronawarnapp/test/risk/storage/DefaultRiskLevelStorageTest.kt +++ b/Corona-Warn-App/src/testDeviceForTesters/java/de/rki/coronawarnapp/test/risk/storage/DefaultRiskLevelStorageTest.kt @@ -1,7 +1,7 @@ package de.rki.coronawarnapp.test.risk.storage import com.google.android.gms.nearby.exposurenotification.ExposureWindow -import de.rki.coronawarnapp.presencetracing.risk.PresenceTracingRiskRepository +import de.rki.coronawarnapp.presencetracing.risk.storage.PresenceTracingRiskRepository import de.rki.coronawarnapp.risk.EwRiskLevelTaskResult import de.rki.coronawarnapp.risk.result.EwAggregatedRiskResult import de.rki.coronawarnapp.risk.storage.DefaultRiskLevelStorage @@ -87,7 +87,7 @@ class DefaultRiskLevelStorageTest : BaseTestInstrumentation() { every { presenceTracingRiskRepository.traceLocationCheckInRiskStates } returns emptyFlow() every { presenceTracingRiskRepository.presenceTracingDayRisk } returns emptyFlow() - every { presenceTracingRiskRepository.latestAndLastSuccessful() } returns emptyFlow() + every { presenceTracingRiskRepository.allEntries() } returns emptyFlow() every { presenceTracingRiskRepository.latestEntries(any()) } returns emptyFlow() } diff --git a/Server-Protocol-Buffer/src/main/proto/internal/pt/qr_code_poster_template.proto b/Server-Protocol-Buffer/src/main/proto/internal/pt/qr_code_poster_template.proto index 76a0b921cabfb724875d5ea13db33ca63dc39a51..6fdbbcdd9957131b034b0d8c5c6cecda1b1d447d 100644 --- a/Server-Protocol-Buffer/src/main/proto/internal/pt/qr_code_poster_template.proto +++ b/Server-Protocol-Buffer/src/main/proto/internal/pt/qr_code_poster_template.proto @@ -15,8 +15,8 @@ message QRCodePosterTemplateAndroid { QRCodeTextBoxAndroid descriptionTextBox = 5; message QRCodeTextBoxAndroid { - uint32 offsetX = 1; - uint32 offsetY = 2; + float offsetX = 1; + float offsetY = 2; uint32 width = 3; uint32 height = 4; diff --git a/Server-Protocol-Buffer/src/main/proto/internal/pt/trace_location.proto b/Server-Protocol-Buffer/src/main/proto/internal/pt/trace_location.proto index baf364df29eddd47d9dde80956f7b884b7225a20..d169f942a70706d1243379d263f70f69f657029d 100644 --- a/Server-Protocol-Buffer/src/main/proto/internal/pt/trace_location.proto +++ b/Server-Protocol-Buffer/src/main/proto/internal/pt/trace_location.proto @@ -2,6 +2,7 @@ syntax = "proto3"; package de.rki.coronawarnapp.server.protocols.internal.pt; + message QRCodePayload { uint32 version = 1; TraceLocation locationData = 2; diff --git a/build.gradle b/build.gradle index 533ae7cd654a253ff06be9890da5be66db25f37b..271da8127e5db7a2db945d1c3a4ada30695be281 100644 --- a/build.gradle +++ b/build.gradle @@ -18,7 +18,7 @@ buildscript { // Can be upgraded once new Android version is released or // the specific version is available via maven classpath 'com.android.tools:r8:2.0.88' - classpath 'com.android.tools.build:gradle:4.1.2' + classpath 'com.android.tools.build:gradle:4.1.3' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "com.google.protobuf:protobuf-gradle-plugin:$protobufVersion" classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$navVersion" diff --git a/gradle.properties b/gradle.properties index 85d774408956be697bebcd055517b68670eb71cb..de45338cd079f8b39f38ac723b6539fac91ac262 100644 --- a/gradle.properties +++ b/gradle.properties @@ -20,4 +20,4 @@ org.gradle.dependency.verification.console=verbose VERSION_MAJOR=2 VERSION_MINOR=1 VERSION_PATCH=0 -VERSION_BUILD=0 +VERSION_BUILD=3 diff --git a/prod_environments.json b/prod_environments.json index f500312f51d59c251fd51267c5ed8ff6f2713fbf..303606b6bddcec348ec65236a6204446b6f51ef3 100644 --- a/prod_environments.json +++ b/prod_environments.json @@ -5,8 +5,9 @@ "DOWNLOAD_CDN_URL": "https://svc90.main.px.t-online.de", "VERIFICATION_CDN_URL": "https://verification.coronawarn.app", "DATA_DONATION_CDN_URL": "https://data.coronawarn.app", - "LOG_UPLOAD_SERVER_URL": "https://logupload.coronawarn.app", + "LOG_UPLOAD_SERVER_URL": "https://data.coronawarn.app", "SAFETYNET_API_KEY": "placeholder", - "PUB_KEYS_SIGNATURE_VERIFICATION": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEc7DEstcUIRcyk35OYDJ95/hTg3UVhsaDXKT0zK7NhHPXoyzipEnOp3GyNXDVpaPi3cAfQmxeuFMZAIX2+6A5Xg==" + "PUB_KEYS_SIGNATURE_VERIFICATION": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEc7DEstcUIRcyk35OYDJ95/hTg3UVhsaDXKT0zK7NhHPXoyzipEnOp3GyNXDVpaPi3cAfQmxeuFMZAIX2+6A5Xg==", + "CROWD_NOTIFIER_PUBLIC_KEY": "gwLMzE153tQwAOf2MZoUXXfzWTdlSpfS99iZffmcmxOG9njSK4RTimFOFwDh6t0Tyw8XR01ugDYjtuKwjjuK49Oh83FWct6XpefPi9Skjxvvz53i9gaMmUEc96pbtoaA" } }