From d848d7b46631b5bdc2bb290aaf10b88f3dea7cb5 Mon Sep 17 00:00:00 2001
From: Kolya Opahle <k.opahle@sap.com>
Date: Fri, 19 Feb 2021 09:55:30 +0100
Subject: [PATCH] PPA: Collect Client Metadata (EXPOSUREAPP-5147) (#2373)

* Implemented ClientMetadataDonor which uses ClientVersionParser to extract the major minor and patch version from the app VERSION_CODE

Signed-off-by: Kolya Opahle <k.opahle@sap.com>

* Restructured ClientMetadataDonor and added ClientMetadataDonorTests

Signed-off-by: Kolya Opahle <k.opahle@sap.com>

* Fixed comment in ClientMetadataDonor

Signed-off-by: Kolya Opahle <k.opahle@sap.com>

* Got rid of ClientVersionWrapper, switched all usages to ApiLevel
Added ability to generate version string to ClientVersionParser
Added ClientVersionParserTest that compares generated string with real one to make sure the parser is working
Changed ClientMetadataDonor to use currentConfig instead of getAppConfig

Signed-off-by: Kolya Opahle <k.opahle@sap.com>

* Removed ClientVersionParser and put MAJOR, MINOR and PATCH version directly in the BuildConfig

Signed-off-by: Kolya Opahle <k.opahle@sap.com>
---
 Corona-Warn-App/build.gradle                  |  4 +
 .../datadonation/analytics/AnalyticsModule.kt |  5 ++
 .../clientmetadata/ClientMetadataDonor.kt     | 69 +++++++++++++++
 .../environment/BuildConfigWrap.kt            |  4 +
 .../clientmetadata/ClientMetadataDonorTest.kt | 87 +++++++++++++++++++
 5 files changed, 169 insertions(+)
 create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/datadonation/analytics/modules/clientmetadata/ClientMetadataDonor.kt
 create mode 100644 Corona-Warn-App/src/test/java/de/rki/coronawarnapp/datadonation/analytics/modules/clientmetadata/ClientMetadataDonorTest.kt

diff --git a/Corona-Warn-App/build.gradle b/Corona-Warn-App/build.gradle
index 761db73a4..310ecaab8 100644
--- a/Corona-Warn-App/build.gradle
+++ b/Corona-Warn-App/build.gradle
@@ -64,6 +64,10 @@ android {
                 arguments += ["room.schemaLocation": "$projectDir/schemas".toString()]
             }
         }
+
+        buildConfigField "int", "VERSION_MAJOR", VERSION_MAJOR
+        buildConfigField "int", "VERSION_MINOR", VERSION_MINOR
+        buildConfigField "int", "VERSION_PATCH", VERSION_PATCH
     }
 
     def signingPropFile = file("../keystore.properties")
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/datadonation/analytics/AnalyticsModule.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/datadonation/analytics/AnalyticsModule.kt
index 279bcbd92..3c74b3801 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/datadonation/analytics/AnalyticsModule.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/datadonation/analytics/AnalyticsModule.kt
@@ -5,6 +5,7 @@ import dagger.Provides
 import dagger.Reusable
 import dagger.multibindings.IntoSet
 import de.rki.coronawarnapp.datadonation.analytics.modules.DonorModule
+import de.rki.coronawarnapp.datadonation.analytics.modules.clientmetadata.ClientMetadataDonor
 import de.rki.coronawarnapp.datadonation.analytics.modules.exposureriskmetadata.ExposureRiskMetadataDonor
 import de.rki.coronawarnapp.datadonation.analytics.modules.usermetadata.UserMetadataDonor
 import de.rki.coronawarnapp.datadonation.analytics.server.DataDonationAnalyticsApiV1
@@ -60,6 +61,10 @@ class AnalyticsModule {
     @Provides
     fun userMetadata(module: UserMetadataDonor): DonorModule = module
 
+    @IntoSet
+    @Provides
+    fun clientMetadata(module: ClientMetadataDonor): DonorModule = module
+
     @Provides
     @Singleton
     fun analyticsLogger(logger: DefaultLastAnalyticsSubmissionLogger): LastAnalyticsSubmissionLogger = logger
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/datadonation/analytics/modules/clientmetadata/ClientMetadataDonor.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/datadonation/analytics/modules/clientmetadata/ClientMetadataDonor.kt
new file mode 100644
index 000000000..a20b1f8ed
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/datadonation/analytics/modules/clientmetadata/ClientMetadataDonor.kt
@@ -0,0 +1,69 @@
+package de.rki.coronawarnapp.datadonation.analytics.modules.clientmetadata
+
+import de.rki.coronawarnapp.appconfig.AppConfigProvider
+import de.rki.coronawarnapp.datadonation.analytics.modules.DonorModule
+import de.rki.coronawarnapp.environment.BuildConfigWrap
+import de.rki.coronawarnapp.nearby.ENFClient
+import de.rki.coronawarnapp.server.protocols.internal.ppdd.PpaData
+import de.rki.coronawarnapp.util.ApiLevel
+import kotlinx.coroutines.flow.first
+import javax.inject.Inject
+import javax.inject.Singleton
+
+@Singleton
+class ClientMetadataDonor @Inject constructor(
+    private val apiLevel: ApiLevel,
+    private val appConfigProvider: AppConfigProvider,
+    private val enfClient: ENFClient
+) : DonorModule {
+
+    override suspend fun beginDonation(request: DonorModule.Request): DonorModule.Contribution {
+        val config = appConfigProvider.currentConfig.first()
+
+        val version = ClientVersion()
+
+        val clientMetadataBuilder = PpaData.PPAClientMetadataAndroid.newBuilder()
+            .setCwaVersion(version.toPPASemanticVersion())
+            .setAndroidApiLevel(apiLevel.currentLevel.toLong())
+            .setAppConfigETag(config.identifier)
+
+        enfClient.getENFClientVersion()?.let {
+            clientMetadataBuilder.setEnfVersion(it)
+        }
+
+        return ClientMetadataContribution(
+            contributionProto = clientMetadataBuilder.build()
+        )
+    }
+
+    override suspend fun deleteData() {
+        // Nothing to be deleted
+    }
+
+    data class ClientMetadataContribution(
+        val contributionProto: PpaData.PPAClientMetadataAndroid
+    ) : DonorModule.Contribution {
+        override suspend fun injectData(protobufContainer: PpaData.PPADataAndroid.Builder) {
+            protobufContainer.clientMetadata = contributionProto
+        }
+
+        override suspend fun finishDonation(successful: Boolean) {
+            // No post processing needed for Client Metadata
+        }
+    }
+
+    data class ClientVersion(val major: Int, val minor: Int, val patch: Int) {
+        constructor() : this(
+            BuildConfigWrap.VERSION_MAJOR,
+            BuildConfigWrap.VERSION_MINOR,
+            BuildConfigWrap.VERSION_PATCH
+        )
+
+        fun toPPASemanticVersion(): PpaData.PPASemanticVersion =
+            PpaData.PPASemanticVersion.newBuilder()
+                .setMajor(major)
+                .setMinor(minor)
+                .setPatch(patch)
+                .build()
+    }
+}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/environment/BuildConfigWrap.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/environment/BuildConfigWrap.kt
index bc8252d1b..8dbdda01a 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/environment/BuildConfigWrap.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/environment/BuildConfigWrap.kt
@@ -10,4 +10,8 @@ object BuildConfigWrap {
     val ENVIRONMENT_TYPE_DEFAULT = BuildConfig.ENVIRONMENT_TYPE_DEFAULT
 
     val VERSION_CODE: Long = BuildConfig.VERSION_CODE.toLong()
+
+    val VERSION_MAJOR: Int = BuildConfig.VERSION_MAJOR
+    val VERSION_MINOR: Int = BuildConfig.VERSION_MINOR
+    val VERSION_PATCH: Int = BuildConfig.VERSION_PATCH
 }
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/datadonation/analytics/modules/clientmetadata/ClientMetadataDonorTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/datadonation/analytics/modules/clientmetadata/ClientMetadataDonorTest.kt
new file mode 100644
index 000000000..3a04905cc
--- /dev/null
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/datadonation/analytics/modules/clientmetadata/ClientMetadataDonorTest.kt
@@ -0,0 +1,87 @@
+package de.rki.coronawarnapp.datadonation.analytics.modules.clientmetadata
+
+import de.rki.coronawarnapp.appconfig.AppConfigProvider
+import de.rki.coronawarnapp.appconfig.ConfigData
+import de.rki.coronawarnapp.datadonation.analytics.modules.DonorModule
+import de.rki.coronawarnapp.environment.BuildConfigWrap
+import de.rki.coronawarnapp.nearby.ENFClient
+import de.rki.coronawarnapp.server.protocols.internal.ppdd.PpaData
+import de.rki.coronawarnapp.util.ApiLevel
+import io.kotest.matchers.shouldBe
+import io.mockk.MockKAnnotations
+import io.mockk.clearAllMocks
+import io.mockk.coEvery
+import io.mockk.every
+import io.mockk.impl.annotations.MockK
+import io.mockk.mockkObject
+import kotlinx.coroutines.flow.flowOf
+import org.junit.jupiter.api.AfterEach
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+import testhelpers.BaseTest
+import testhelpers.coroutines.runBlockingTest2
+
+class ClientMetadataDonorTest : BaseTest() {
+    @MockK lateinit var apiLevel: ApiLevel
+    @MockK lateinit var appConfigProvider: AppConfigProvider
+    @MockK lateinit var configData: ConfigData
+    @MockK lateinit var enfClient: ENFClient
+
+    private val eTag = "testETag"
+    private val enfVersion = 1611L
+    private val androidVersionCode = 42L
+
+    private val versionMajor = 1
+    private val versionMinor = 11
+    private val versionPatch = 1
+
+    @BeforeEach
+    fun setup() {
+        MockKAnnotations.init(this)
+
+        mockkObject(BuildConfigWrap)
+        every { BuildConfigWrap.VERSION_MAJOR } returns versionMajor
+        every { BuildConfigWrap.VERSION_MINOR } returns versionMinor
+        every { BuildConfigWrap.VERSION_PATCH } returns versionPatch
+
+        every { apiLevel.currentLevel } returns androidVersionCode.toInt()
+        every { configData.identifier } returns eTag
+        coEvery { appConfigProvider.currentConfig } returns flowOf(configData)
+        coEvery { enfClient.getENFClientVersion() } returns enfVersion
+    }
+
+    @AfterEach
+    fun tearDown() {
+        clearAllMocks()
+    }
+
+    private fun createInstance() = ClientMetadataDonor(
+        apiLevel = apiLevel,
+        appConfigProvider = appConfigProvider,
+        enfClient = enfClient
+    )
+
+    @Test
+    fun `client metadata is properly collected`() {
+        val version = ClientMetadataDonor.ClientVersion().toPPASemanticVersion()
+
+        val expectedMetadata = PpaData.PPAClientMetadataAndroid.newBuilder()
+            .setAppConfigETag(eTag)
+            .setEnfVersion(enfVersion)
+            .setCwaVersion(version)
+            .setAndroidApiLevel(androidVersionCode)
+            .build()
+
+        val parentBuilder = PpaData.PPADataAndroid.newBuilder()
+
+        runBlockingTest2 {
+            val contribution = createInstance().beginDonation(object : DonorModule.Request {})
+            contribution.injectData(parentBuilder)
+            contribution.finishDonation(true)
+        }
+
+        val parentProto = parentBuilder.build()
+
+        parentProto.clientMetadata shouldBe expectedMetadata
+    }
+}
-- 
GitLab