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