diff --git a/Corona-Warn-App/build.gradle b/Corona-Warn-App/build.gradle index 761db73a44df54c8143232bc19ada8a6b7d459c3..310ecaab8c2c7de378a364e7c8a5d9f03df861d6 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 279bcbd92dfbf3f4fa48a3ebf707092cde5130f2..3c74b38013665b234f286bf142c3e5b6e4fc2f81 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 0000000000000000000000000000000000000000..a20b1f8ed8c1e28d18361e34119bb0c63c77241e --- /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 bc8252d1b7b923721b0e77cd6c85c08f43c4e678..8dbdda01a81613628f5c0ad3173d64397918f5d0 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 0000000000000000000000000000000000000000..3a04905ccfd3d5dc2e72ddae701529c783d7d621 --- /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 + } +}