From a5630bed85c2f20cb81bf43a3a3ce3568876c05f Mon Sep 17 00:00:00 2001
From: BMItter <46747780+BMItter@users.noreply.github.com>
Date: Thu, 12 Nov 2020 16:35:34 +0100
Subject: [PATCH] Enhance enf client (EXPOSUREAPP-3540) (#1584)

* Implemented exposure window provider, adjusted ENF Client

* Implemented new provideDiagnosisKeys to activate ExposureWindow mode, deprecated old diagnosis related content

* Adjusted ENFClientTest

* conflict fix - wip

* test fix
---
 .../de/rki/coronawarnapp/nearby/ENFClient.kt  | 21 +++++++++++++++-
 .../de/rki/coronawarnapp/nearby/ENFModule.kt  |  7 ++++++
 .../DefaultDiagnosisKeyProvider.kt            | 25 +++++++++++++++++++
 .../DiagnosisKeyProvider.kt                   |  3 +++
 .../diagnosiskeyprovider/SubmissionQuota.kt   |  4 +++
 .../DefaultExposureWindowProvider.kt          | 20 +++++++++++++++
 .../exposurewindow/ExposureWindowProvider.kt  |  7 ++++++
 .../coronawarnapp/util/GoogleAPIVersion.kt    |  1 +
 .../rki/coronawarnapp/nearby/ENFClientTest.kt | 18 +++++++++++++
 9 files changed, 105 insertions(+), 1 deletion(-)
 create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/exposurewindow/DefaultExposureWindowProvider.kt
 create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/exposurewindow/ExposureWindowProvider.kt

diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/ENFClient.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/ENFClient.kt
index c3de7abd0..38d4f40b1 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/ENFClient.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/ENFClient.kt
@@ -4,9 +4,11 @@ package de.rki.coronawarnapp.nearby
 
 import com.google.android.gms.nearby.exposurenotification.ExposureConfiguration
 import com.google.android.gms.nearby.exposurenotification.ExposureNotificationClient
+import com.google.android.gms.nearby.exposurenotification.ExposureWindow
 import de.rki.coronawarnapp.nearby.modules.detectiontracker.ExposureDetectionTracker
 import de.rki.coronawarnapp.nearby.modules.detectiontracker.TrackedExposureDetection
 import de.rki.coronawarnapp.nearby.modules.diagnosiskeyprovider.DiagnosisKeyProvider
+import de.rki.coronawarnapp.nearby.modules.exposurewindow.ExposureWindowProvider
 import de.rki.coronawarnapp.nearby.modules.locationless.ScanningSupport
 import de.rki.coronawarnapp.nearby.modules.tracing.TracingStatus
 import kotlinx.coroutines.flow.Flow
@@ -23,8 +25,10 @@ class ENFClient @Inject constructor(
     private val diagnosisKeyProvider: DiagnosisKeyProvider,
     private val tracingStatus: TracingStatus,
     private val scanningSupport: ScanningSupport,
+    private val exposureWindowProvider: ExposureWindowProvider,
+
     private val exposureDetectionTracker: ExposureDetectionTracker
-) : DiagnosisKeyProvider, TracingStatus, ScanningSupport {
+) : DiagnosisKeyProvider, TracingStatus, ScanningSupport, ExposureWindowProvider {
 
     // TODO Remove this once we no longer need direct access to the ENF Client,
     // i.e. in **[InternalExposureNotificationClient]**
@@ -51,6 +55,19 @@ class ENFClient @Inject constructor(
         }
     }
 
+    override suspend fun provideDiagnosisKeys(keyFiles: Collection<File>): Boolean {
+        Timber.d("asyncProvideDiagnosisKeys(keyFiles=$keyFiles)")
+
+        return if (keyFiles.isEmpty()) {
+            Timber.d("No key files submitted, returning early.")
+            true
+        } else {
+            Timber.d("Forwarding %d key files to our DiagnosisKeyProvider.", keyFiles.size)
+            TODO("Call calculationTracker.trackNewCalaculation with an UUID as replacement for token?")
+            diagnosisKeyProvider.provideDiagnosisKeys(keyFiles)
+        }
+    }
+
     override val isLocationLessScanningSupported: Flow<Boolean>
         get() = scanningSupport.isLocationLessScanningSupported
 
@@ -72,4 +89,6 @@ class ENFClient @Inject constructor(
                 .filter { !it.isCalculating && it.isSuccessful }
                 .maxByOrNull { it.finishedAt ?: Instant.EPOCH }
         }
+
+    override suspend fun exposureWindows(): List<ExposureWindow> = exposureWindowProvider.exposureWindows()
 }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/ENFModule.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/ENFModule.kt
index 9d98d5b33..ec99ff4e7 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/ENFModule.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/ENFModule.kt
@@ -9,6 +9,8 @@ import de.rki.coronawarnapp.nearby.modules.detectiontracker.DefaultExposureDetec
 import de.rki.coronawarnapp.nearby.modules.detectiontracker.ExposureDetectionTracker
 import de.rki.coronawarnapp.nearby.modules.diagnosiskeyprovider.DefaultDiagnosisKeyProvider
 import de.rki.coronawarnapp.nearby.modules.diagnosiskeyprovider.DiagnosisKeyProvider
+import de.rki.coronawarnapp.nearby.modules.exposurewindow.DefaultExposureWindowProvider
+import de.rki.coronawarnapp.nearby.modules.exposurewindow.ExposureWindowProvider
 import de.rki.coronawarnapp.nearby.modules.locationless.DefaultScanningSupport
 import de.rki.coronawarnapp.nearby.modules.locationless.ScanningSupport
 import de.rki.coronawarnapp.nearby.modules.tracing.DefaultTracingStatus
@@ -39,6 +41,11 @@ class ENFModule {
     fun scanningSupport(scanningSupport: DefaultScanningSupport): ScanningSupport =
         scanningSupport
 
+    @Singleton
+    @Provides
+    fun exposureWindowProvider(exposureWindowProvider: DefaultExposureWindowProvider): ExposureWindowProvider =
+        exposureWindowProvider
+
     @Singleton
     @Provides
     fun calculationTracker(exposureDetectionTracker: DefaultExposureDetectionTracker): ExposureDetectionTracker =
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/diagnosiskeyprovider/DefaultDiagnosisKeyProvider.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/diagnosiskeyprovider/DefaultDiagnosisKeyProvider.kt
index 59114d58d..a0cbf8b53 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/diagnosiskeyprovider/DefaultDiagnosisKeyProvider.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/diagnosiskeyprovider/DefaultDiagnosisKeyProvider.kt
@@ -54,6 +54,31 @@ class DefaultDiagnosisKeyProvider @Inject constructor(
         }
     }
 
+    override suspend fun provideDiagnosisKeys(keyFiles: Collection<File>): Boolean {
+        if (keyFiles.isEmpty()) {
+            Timber.d("No key files submitted, returning early.")
+            return true
+        }
+
+        if (!googleAPIVersion.isAtLeast(GoogleAPIVersion.V15)) {
+            // Actually this shouldn't happen
+            Timber.d("No key files submitted because client uses an old unsupported version")
+            return false
+        }
+
+        if (!submissionQuota.consumeQuota(1)) {
+            Timber.w("No key files submitted because not enough quota available.")
+        }
+
+        return suspendCoroutine { cont ->
+            Timber.d("Performing key submission.")
+            enfClient
+                .provideDiagnosisKeys(keyFiles.toList())
+                .addOnSuccessListener { cont.resume(true) }
+                .addOnFailureListener { cont.resumeWithException(it) }
+        }
+    }
+
     private suspend fun provideKeys(
         files: Collection<File>,
         configuration: ExposureConfiguration,
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/diagnosiskeyprovider/DiagnosisKeyProvider.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/diagnosiskeyprovider/DiagnosisKeyProvider.kt
index accedeed0..d4a2f00fa 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/diagnosiskeyprovider/DiagnosisKeyProvider.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/diagnosiskeyprovider/DiagnosisKeyProvider.kt
@@ -17,9 +17,12 @@ interface DiagnosisKeyProvider {
      * @param token
      * @return
      */
+    @Deprecated("Use provideDiagnosisKeys with only keyFiles as param to activate WindowExposure mode")
     suspend fun provideDiagnosisKeys(
         keyFiles: Collection<File>,
         configuration: ExposureConfiguration?,
         token: String
     ): Boolean
+
+    suspend fun provideDiagnosisKeys(keyFiles: Collection<File>): Boolean
 }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/diagnosiskeyprovider/SubmissionQuota.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/diagnosiskeyprovider/SubmissionQuota.kt
index d9bd53506..bf5ff885a 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/diagnosiskeyprovider/SubmissionQuota.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/diagnosiskeyprovider/SubmissionQuota.kt
@@ -86,6 +86,10 @@ class SubmissionQuota @Inject constructor(
 
     companion object {
         @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+        /**
+         * This quota changes when using ExposureWindow mode from 20 to 6 per day
+         * See: https://developers.google.com/android/exposure-notifications/release-notes
+         */
         internal const val DEFAULT_QUOTA = 20
     }
 }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/exposurewindow/DefaultExposureWindowProvider.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/exposurewindow/DefaultExposureWindowProvider.kt
new file mode 100644
index 000000000..5f83c9bba
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/exposurewindow/DefaultExposureWindowProvider.kt
@@ -0,0 +1,20 @@
+package de.rki.coronawarnapp.nearby.modules.exposurewindow
+
+import com.google.android.gms.nearby.exposurenotification.ExposureNotificationClient
+import com.google.android.gms.nearby.exposurenotification.ExposureWindow
+import javax.inject.Inject
+import javax.inject.Singleton
+import kotlin.coroutines.resume
+import kotlin.coroutines.resumeWithException
+import kotlin.coroutines.suspendCoroutine
+
+@Singleton
+class DefaultExposureWindowProvider @Inject constructor(
+    private val client: ExposureNotificationClient
+) : ExposureWindowProvider {
+    override suspend fun exposureWindows(): List<ExposureWindow> = suspendCoroutine { cont ->
+        client.exposureWindows
+            .addOnSuccessListener { cont.resume(it) }
+            .addOnFailureListener { cont.resumeWithException(it) }
+    }
+}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/exposurewindow/ExposureWindowProvider.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/exposurewindow/ExposureWindowProvider.kt
new file mode 100644
index 000000000..713715c87
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/exposurewindow/ExposureWindowProvider.kt
@@ -0,0 +1,7 @@
+package de.rki.coronawarnapp.nearby.modules.exposurewindow
+
+import com.google.android.gms.nearby.exposurenotification.ExposureWindow
+
+interface ExposureWindowProvider {
+    suspend fun exposureWindows(): List<ExposureWindow>
+}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/GoogleAPIVersion.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/GoogleAPIVersion.kt
index 6af00068b..8119586bc 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/GoogleAPIVersion.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/GoogleAPIVersion.kt
@@ -36,5 +36,6 @@ class GoogleAPIVersion @Inject constructor() {
     companion object {
         private const val GOOGLE_API_VERSION_FIELD_LENGTH = 8
         const val V16 = 16000000L
+        const val V15 = 15000000L
     }
 }
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/ENFClientTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/ENFClientTest.kt
index 4880b66aa..1f242c4c2 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/ENFClientTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/ENFClientTest.kt
@@ -2,9 +2,11 @@ package de.rki.coronawarnapp.nearby
 
 import com.google.android.gms.nearby.exposurenotification.ExposureConfiguration
 import com.google.android.gms.nearby.exposurenotification.ExposureNotificationClient
+import com.google.android.gms.nearby.exposurenotification.ExposureWindow
 import de.rki.coronawarnapp.nearby.modules.detectiontracker.ExposureDetectionTracker
 import de.rki.coronawarnapp.nearby.modules.detectiontracker.TrackedExposureDetection
 import de.rki.coronawarnapp.nearby.modules.diagnosiskeyprovider.DiagnosisKeyProvider
+import de.rki.coronawarnapp.nearby.modules.exposurewindow.ExposureWindowProvider
 import de.rki.coronawarnapp.nearby.modules.locationless.ScanningSupport
 import de.rki.coronawarnapp.nearby.modules.tracing.TracingStatus
 import io.kotest.matchers.shouldBe
@@ -37,6 +39,7 @@ class ENFClientTest : BaseTest() {
     @MockK lateinit var diagnosisKeyProvider: DiagnosisKeyProvider
     @MockK lateinit var tracingStatus: TracingStatus
     @MockK lateinit var scanningSupport: ScanningSupport
+    @MockK lateinit var exposureWindowProvider: ExposureWindowProvider
     @MockK lateinit var exposureDetectionTracker: ExposureDetectionTracker
 
     @BeforeEach
@@ -56,6 +59,8 @@ class ENFClientTest : BaseTest() {
         diagnosisKeyProvider = diagnosisKeyProvider,
         tracingStatus = tracingStatus,
         scanningSupport = scanningSupport,
+
+        exposureWindowProvider = exposureWindowProvider,
         exposureDetectionTracker = exposureDetectionTracker
     )
 
@@ -265,4 +270,17 @@ class ENFClientTest : BaseTest() {
             createClient().lastSuccessfulTrackedExposureDetection().first()!!.identifier shouldBe "1"
         }
     }
+
+    @Test
+    fun `exposure windows check is forwarded to the right module`() = runBlocking {
+        val exposureWindowList = emptyList<ExposureWindow>()
+        coEvery { exposureWindowProvider.exposureWindows() } returns exposureWindowList
+
+        val client = createClient()
+        client.exposureWindows() shouldBe exposureWindowList
+
+        coVerify(exactly = 1) {
+            exposureWindowProvider.exposureWindows()
+        }
+    }
 }
-- 
GitLab