Skip to content
Snippets Groups Projects
Unverified Commit d2f89e74 authored by Kolya Opahle's avatar Kolya Opahle Committed by GitHub
Browse files

Determine Risk Level for Exposure Windows & Aggregation (EXPOSUREAPP-3537,...

Determine Risk Level for Exposure Windows & Aggregation (EXPOSUREAPP-3537, EXPOSUREAPP-3518) (#1546)

* Split and hide the protobuf config behind interfaces with individual mappers responsible for creating the desired formats.

* Merge branch 'release/1.7.x' into feature/3455-more-frequent-riskscore-updates-configs

# Conflicts:
#	Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/risklevel/ui/TestRiskLevelCalculationFragmentCWAViewModel.kt
#	Corona-Warn-App/src/main/java/de/rki/coronawarnapp/transaction/RiskLevelTransaction.kt
#	Corona-Warn-App/src/test/java/de/rki/coronawarnapp/transaction/RiskLevelTransactionTest.kt

* Make the AppConfig observable.
Provide the server time offset.
Offer a lastUpdatedAt timestamp.
Add an app config specific test screen.
Clean up test screens a bit and move debug options out of API test options.

* Fix test regression due to refactoring (moved code around).

* Store the server timestamp and offset at retrieval.
Switch to config storage via json to be able to store additional meta data fields (i.e. time).

* KLint and Me have a hate relationship based on both mutual admiration.

* Fix time offset parsing being locale dependent.

* Fix broken unit tests.

* Improve offset accuracy, move before unzipping.

* Fix overly long livedata subscription to results (viewmodel scope vs observer scope)

* Add mapping for the new protobuf configs + tests.

* For cached (retrofit) response, we need to check the cacheResponse and its timestamps
to determine an accurate time offset.

* Exposure a boolean property to tell us when a fallback config is being used.

* Hide the observable flow<ConfigData> behind a method that can automatically triggers refreshes.

* Use a common mapper interface.

* set old risklevelcalculation deprecated

* Created skeleton for new risk calculation and aggregation

* Implementing steps to aggregate results form exposure windows - wip

* Address PR comments and KLints.

* Fix refactoring regression.

* ktlint

* Added ExposureWindowRiskLevelConfig and ExposureWindowRiskLevelConfigMapper for new config api (not yet introduced)

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

* Added first Implementation of exposure window based calculateRisk function

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

* Added generics to Range.inRange

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

* Added Ugly Hack to RiskLevelTransaction to allow for compilation during testing

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

* Linting and injecting RiskLevelCalculation into TestRiskLevelCalculationFragmentCWAViewModel, currently wont build because ExposureWindowRiskLevelConfig has no Provider

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

* Linting extravaganza

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

* Lint Wars Episode VI: Return of the trailing Comma

* Improve config unzipping code.

* Add flag to forward exception thrown during HotDataFlow.kt initialization.

* Don't specify a default context via singleton.

* Move download and fallback logic into it's own class just responsible for sourcing the config: "AppConfigSource".
"AppConfigProvider" is now only responsible for making it available.

* Simplify current concepts for making the app config observable until we have a default configuration.

* Improve app config test screen, delete options, better feedback.
Show toast instead of crash on errors.

* Fixed GSON serialization not encoding/decoding the byte array correctly.
Added specific type adapters for instant and duration to get cleaner json.

* Remove type adapters from base gson due to conflict with CalculationTrackerStorage.

* We want to default to forced serialization of instant by our converters, instead of using the default serialization which will differ
between Java8.Instant and JodaTime.Instant, to prevent future headaches there, register explicit converters by default,
and overwrite them if necessary (currently only needed for CalculationTrackerStorage.kt).

* Improve AppConfigServer code readability by moving code into extensions.

* Fix merge conflicts

* Added missing import to WorkerBinderTest

* fixed unit tests

* Removed auto formatting on unrelated files (revert + cherry pick in other commit)

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

* Implementing steps to aggregate results form exposure windows

* Renamed ExposureWindowRiskLevelConfig to ExposureWindowRiskCalculationConfig

* adjusted & refactored Windows aggregation

* removed example Values

* satisfy lint

* make Aggregation work with Instant now

* Use long while calculation

* Added normalizedTimePerDayToRiskLevelMappingList to AppConfig

* normalizedTimePerDayToRiskLevelMappingList from AppConfig

* satisfy lint

* Get AppConfig on init and listen for updates

* exposureData to aggregatedRiskPerDateResult

* Corrected name in ConfigParserTest

* use instant for specific aggregation logs

* satisfy CI

* satisfy detekt

* exposure history exception & log adjustment

* Fixed unittests for new config parser and risk levels

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

* Added some logging to the calculateRisk function and removed the suspend qualifiers as AppConfig is fetched during init

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

Co-authored-by: default avatarMatthias Urhahn <matthias.urhahn@sap.com>
Co-authored-by: default avatarBMItter <Berndus@gmx.de>
Co-authored-by: default avatarharambasicluka <64483219+harambasicluka@users.noreply.github.com>
parent 2f0eeeb2
No related branches found
No related tags found
No related merge requests found
Showing
with 486 additions and 23 deletions
...@@ -8,6 +8,7 @@ import de.rki.coronawarnapp.appconfig.download.AppConfigHttpCache ...@@ -8,6 +8,7 @@ import de.rki.coronawarnapp.appconfig.download.AppConfigHttpCache
import de.rki.coronawarnapp.appconfig.mapping.CWAConfigMapper import de.rki.coronawarnapp.appconfig.mapping.CWAConfigMapper
import de.rki.coronawarnapp.appconfig.mapping.DownloadConfigMapper import de.rki.coronawarnapp.appconfig.mapping.DownloadConfigMapper
import de.rki.coronawarnapp.appconfig.mapping.ExposureDetectionConfigMapper import de.rki.coronawarnapp.appconfig.mapping.ExposureDetectionConfigMapper
import de.rki.coronawarnapp.appconfig.mapping.ExposureWindowRiskCalculationConfigMapper
import de.rki.coronawarnapp.appconfig.mapping.RiskCalculationConfigMapper import de.rki.coronawarnapp.appconfig.mapping.RiskCalculationConfigMapper
import de.rki.coronawarnapp.environment.download.DownloadCDNHttpClient import de.rki.coronawarnapp.environment.download.DownloadCDNHttpClient
import de.rki.coronawarnapp.environment.download.DownloadCDNServerUrl import de.rki.coronawarnapp.environment.download.DownloadCDNServerUrl
...@@ -61,17 +62,24 @@ class AppConfigModule { ...@@ -61,17 +62,24 @@ class AppConfigModule {
} }
@Provides @Provides
fun cwaMapper(mapper: CWAConfigMapper): CWAConfig.Mapper = mapper fun cwaMapper(mapper: CWAConfigMapper):
CWAConfig.Mapper = mapper
@Provides @Provides
fun downloadMapper(mapper: DownloadConfigMapper): KeyDownloadConfig.Mapper = mapper fun downloadMapper(mapper: DownloadConfigMapper):
KeyDownloadConfig.Mapper = mapper
@Provides @Provides
fun exposurMapper(mapper: ExposureDetectionConfigMapper): ExposureDetectionConfig.Mapper = fun exposurMapper(mapper: ExposureDetectionConfigMapper):
mapper ExposureDetectionConfig.Mapper = mapper
@Provides @Provides
fun riskMapper(mapper: RiskCalculationConfigMapper): RiskCalculationConfig.Mapper = mapper fun riskMapper(mapper: RiskCalculationConfigMapper):
RiskCalculationConfig.Mapper = mapper
@Provides
fun windowRiskMapper(mapper: ExposureWindowRiskCalculationConfigMapper):
ExposureWindowRiskCalculationConfig.Mapper = mapper
companion object { companion object {
private val HTTP_TIMEOUT_APPCONFIG = Duration.standardSeconds(10) private val HTTP_TIMEOUT_APPCONFIG = Duration.standardSeconds(10)
......
package de.rki.coronawarnapp.appconfig
import de.rki.coronawarnapp.server.protocols.internal.v2.AppConfigAndroid
import de.rki.coronawarnapp.server.protocols.internal.v2.RiskCalculationParametersOuterClass
interface ExposureWindowRiskCalculationConfig {
val minutesAtAttenuationFilters: List<RiskCalculationParametersOuterClass.MinutesAtAttenuationFilter>
val minutesAtAttenuationWeights: List<RiskCalculationParametersOuterClass.MinutesAtAttenuationWeight>
val transmissionRiskLevelEncoding: RiskCalculationParametersOuterClass.TransmissionRiskLevelEncoding
val transmissionRiskLevelFilters: List<RiskCalculationParametersOuterClass.TrlFilter>
val transmissionRiskLevelMultiplier: Double
val normalizedTimePerExposureWindowToRiskLevelMapping:
List<RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping>
val normalizedTimePerDayToRiskLevelMappingList:
List<RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping>
interface Mapper {
fun map(rawConfig: AppConfigAndroid.ApplicationConfigurationAndroid): ExposureWindowRiskCalculationConfig
}
}
...@@ -2,6 +2,7 @@ package de.rki.coronawarnapp.appconfig.mapping ...@@ -2,6 +2,7 @@ package de.rki.coronawarnapp.appconfig.mapping
import de.rki.coronawarnapp.appconfig.CWAConfig import de.rki.coronawarnapp.appconfig.CWAConfig
import de.rki.coronawarnapp.appconfig.ExposureDetectionConfig import de.rki.coronawarnapp.appconfig.ExposureDetectionConfig
import de.rki.coronawarnapp.appconfig.ExposureWindowRiskCalculationConfig
import de.rki.coronawarnapp.appconfig.KeyDownloadConfig import de.rki.coronawarnapp.appconfig.KeyDownloadConfig
import de.rki.coronawarnapp.appconfig.RiskCalculationConfig import de.rki.coronawarnapp.appconfig.RiskCalculationConfig
import de.rki.coronawarnapp.server.protocols.internal.AppConfig import de.rki.coronawarnapp.server.protocols.internal.AppConfig
...@@ -10,7 +11,8 @@ interface ConfigMapping : ...@@ -10,7 +11,8 @@ interface ConfigMapping :
CWAConfig, CWAConfig,
KeyDownloadConfig, KeyDownloadConfig,
ExposureDetectionConfig, ExposureDetectionConfig,
RiskCalculationConfig { RiskCalculationConfig,
ExposureWindowRiskCalculationConfig {
@Deprecated("Try to access a more specific config type, avoid the RAW variant.") @Deprecated("Try to access a more specific config type, avoid the RAW variant.")
val rawConfig: AppConfig.ApplicationConfiguration val rawConfig: AppConfig.ApplicationConfiguration
......
...@@ -3,9 +3,11 @@ package de.rki.coronawarnapp.appconfig.mapping ...@@ -3,9 +3,11 @@ package de.rki.coronawarnapp.appconfig.mapping
import dagger.Reusable import dagger.Reusable
import de.rki.coronawarnapp.appconfig.CWAConfig import de.rki.coronawarnapp.appconfig.CWAConfig
import de.rki.coronawarnapp.appconfig.ExposureDetectionConfig import de.rki.coronawarnapp.appconfig.ExposureDetectionConfig
import de.rki.coronawarnapp.appconfig.ExposureWindowRiskCalculationConfig
import de.rki.coronawarnapp.appconfig.KeyDownloadConfig import de.rki.coronawarnapp.appconfig.KeyDownloadConfig
import de.rki.coronawarnapp.appconfig.RiskCalculationConfig import de.rki.coronawarnapp.appconfig.RiskCalculationConfig
import de.rki.coronawarnapp.server.protocols.internal.AppConfig import de.rki.coronawarnapp.server.protocols.internal.AppConfig
import de.rki.coronawarnapp.server.protocols.internal.v2.AppConfigAndroid
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
...@@ -14,17 +16,24 @@ class ConfigParser @Inject constructor( ...@@ -14,17 +16,24 @@ class ConfigParser @Inject constructor(
private val cwaConfigMapper: CWAConfig.Mapper, private val cwaConfigMapper: CWAConfig.Mapper,
private val keyDownloadConfigMapper: KeyDownloadConfig.Mapper, private val keyDownloadConfigMapper: KeyDownloadConfig.Mapper,
private val exposureDetectionConfigMapper: ExposureDetectionConfig.Mapper, private val exposureDetectionConfigMapper: ExposureDetectionConfig.Mapper,
private val riskCalculationConfigMapper: RiskCalculationConfig.Mapper private val riskCalculationConfigMapper: RiskCalculationConfig.Mapper,
private val exposureWindowRiskCalculationConfigMapper: ExposureWindowRiskCalculationConfig.Mapper
) { ) {
fun parse(configBytes: ByteArray): ConfigMapping = try { fun parse(configBytes: ByteArray): ConfigMapping = try {
// TODO replace with actual v2 config
val dummyConfig = AppConfigAndroid
.ApplicationConfigurationAndroid
.newBuilder()
.build()
parseRawArray(configBytes).let { parseRawArray(configBytes).let {
DefaultConfigMapping( DefaultConfigMapping(
rawConfig = it, rawConfig = it,
cwaConfig = cwaConfigMapper.map(it), cwaConfig = cwaConfigMapper.map(it),
keyDownloadConfig = keyDownloadConfigMapper.map(it), keyDownloadConfig = keyDownloadConfigMapper.map(it),
exposureDetectionConfig = exposureDetectionConfigMapper.map(it), exposureDetectionConfig = exposureDetectionConfigMapper.map(it),
riskCalculationConfig = riskCalculationConfigMapper.map(it) riskCalculationConfig = riskCalculationConfigMapper.map(it),
exposureWindowRiskCalculationConfig = exposureWindowRiskCalculationConfigMapper.map(dummyConfig)
) )
} }
} catch (e: Exception) { } catch (e: Exception) {
......
...@@ -2,6 +2,7 @@ package de.rki.coronawarnapp.appconfig.mapping ...@@ -2,6 +2,7 @@ package de.rki.coronawarnapp.appconfig.mapping
import de.rki.coronawarnapp.appconfig.CWAConfig import de.rki.coronawarnapp.appconfig.CWAConfig
import de.rki.coronawarnapp.appconfig.ExposureDetectionConfig import de.rki.coronawarnapp.appconfig.ExposureDetectionConfig
import de.rki.coronawarnapp.appconfig.ExposureWindowRiskCalculationConfig
import de.rki.coronawarnapp.appconfig.KeyDownloadConfig import de.rki.coronawarnapp.appconfig.KeyDownloadConfig
import de.rki.coronawarnapp.appconfig.RiskCalculationConfig import de.rki.coronawarnapp.appconfig.RiskCalculationConfig
import de.rki.coronawarnapp.server.protocols.internal.AppConfig import de.rki.coronawarnapp.server.protocols.internal.AppConfig
...@@ -11,9 +12,11 @@ data class DefaultConfigMapping( ...@@ -11,9 +12,11 @@ data class DefaultConfigMapping(
val cwaConfig: CWAConfig, val cwaConfig: CWAConfig,
val keyDownloadConfig: KeyDownloadConfig, val keyDownloadConfig: KeyDownloadConfig,
val exposureDetectionConfig: ExposureDetectionConfig, val exposureDetectionConfig: ExposureDetectionConfig,
val riskCalculationConfig: RiskCalculationConfig val riskCalculationConfig: RiskCalculationConfig,
val exposureWindowRiskCalculationConfig: ExposureWindowRiskCalculationConfig
) : ConfigMapping, ) : ConfigMapping,
CWAConfig by cwaConfig, CWAConfig by cwaConfig,
KeyDownloadConfig by keyDownloadConfig, KeyDownloadConfig by keyDownloadConfig,
ExposureDetectionConfig by exposureDetectionConfig, ExposureDetectionConfig by exposureDetectionConfig,
RiskCalculationConfig by riskCalculationConfig RiskCalculationConfig by riskCalculationConfig,
ExposureWindowRiskCalculationConfig by exposureWindowRiskCalculationConfig
package de.rki.coronawarnapp.appconfig.mapping
import dagger.Reusable
import de.rki.coronawarnapp.appconfig.ExposureWindowRiskCalculationConfig
import de.rki.coronawarnapp.server.protocols.internal.v2.AppConfigAndroid
import de.rki.coronawarnapp.server.protocols.internal.v2.RiskCalculationParametersOuterClass
import javax.inject.Inject
@Reusable
class ExposureWindowRiskCalculationConfigMapper @Inject constructor() :
ExposureWindowRiskCalculationConfig.Mapper {
override fun map(rawConfig: AppConfigAndroid.ApplicationConfigurationAndroid): ExposureWindowRiskCalculationConfig {
val riskCalculationParameters = rawConfig.riskCalculationParameters
return ExposureWindowRiskCalculationContainer(
minutesAtAttenuationFilters = riskCalculationParameters
.minutesAtAttenuationFiltersList,
minutesAtAttenuationWeights = riskCalculationParameters
.minutesAtAttenuationWeightsList,
transmissionRiskLevelEncoding = riskCalculationParameters
.trlEncoding,
transmissionRiskLevelFilters = riskCalculationParameters
.trlFiltersList,
transmissionRiskLevelMultiplier = riskCalculationParameters
.transmissionRiskLevelMultiplier,
normalizedTimePerExposureWindowToRiskLevelMapping = riskCalculationParameters
.normalizedTimePerEWToRiskLevelMappingList,
normalizedTimePerDayToRiskLevelMappingList = riskCalculationParameters
.normalizedTimePerDayToRiskLevelMappingList
)
}
data class ExposureWindowRiskCalculationContainer(
override val minutesAtAttenuationFilters: List<RiskCalculationParametersOuterClass.MinutesAtAttenuationFilter>,
override val minutesAtAttenuationWeights: List<RiskCalculationParametersOuterClass.MinutesAtAttenuationWeight>,
override val transmissionRiskLevelEncoding: RiskCalculationParametersOuterClass.TransmissionRiskLevelEncoding,
override val transmissionRiskLevelFilters: List<RiskCalculationParametersOuterClass.TrlFilter>,
override val transmissionRiskLevelMultiplier: Double,
override val normalizedTimePerExposureWindowToRiskLevelMapping:
List<RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping>,
override val normalizedTimePerDayToRiskLevelMappingList:
List<RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping>
) : ExposureWindowRiskCalculationConfig
}
package de.rki.coronawarnapp.risk package de.rki.coronawarnapp.risk
import android.text.TextUtils
import androidx.annotation.VisibleForTesting import androidx.annotation.VisibleForTesting
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import com.google.android.gms.nearby.exposurenotification.ExposureSummary import com.google.android.gms.nearby.exposurenotification.ExposureSummary
import com.google.android.gms.nearby.exposurenotification.ExposureWindow
import com.google.android.gms.nearby.exposurenotification.Infectiousness
import com.google.android.gms.nearby.exposurenotification.ReportType
import de.rki.coronawarnapp.CoronaWarnApplication import de.rki.coronawarnapp.CoronaWarnApplication
import de.rki.coronawarnapp.R import de.rki.coronawarnapp.R
import de.rki.coronawarnapp.appconfig.AppConfigProvider import de.rki.coronawarnapp.appconfig.AppConfigProvider
import de.rki.coronawarnapp.appconfig.ConfigData
import de.rki.coronawarnapp.appconfig.download.ApplicationConfigurationInvalidException
import de.rki.coronawarnapp.exception.RiskLevelCalculationException import de.rki.coronawarnapp.exception.RiskLevelCalculationException
import de.rki.coronawarnapp.notification.NotificationHelper import de.rki.coronawarnapp.notification.NotificationHelper
import de.rki.coronawarnapp.risk.RiskLevel.UNKNOWN_RISK_INITIAL import de.rki.coronawarnapp.risk.RiskLevel.UNKNOWN_RISK_INITIAL
import de.rki.coronawarnapp.risk.RiskLevel.UNKNOWN_RISK_OUTDATED_RESULTS import de.rki.coronawarnapp.risk.RiskLevel.UNKNOWN_RISK_OUTDATED_RESULTS
import de.rki.coronawarnapp.risk.result.AggregatedRiskPerDateResult
import de.rki.coronawarnapp.risk.result.AggregatedRiskResult
import de.rki.coronawarnapp.risk.result.RiskResult
import de.rki.coronawarnapp.server.protocols.internal.AttenuationDurationOuterClass.AttenuationDuration import de.rki.coronawarnapp.server.protocols.internal.AttenuationDurationOuterClass.AttenuationDuration
import de.rki.coronawarnapp.server.protocols.internal.v2.RiskCalculationParametersOuterClass
import de.rki.coronawarnapp.storage.LocalData import de.rki.coronawarnapp.storage.LocalData
import de.rki.coronawarnapp.storage.RiskLevelRepository import de.rki.coronawarnapp.storage.RiskLevelRepository
import de.rki.coronawarnapp.util.TimeAndDateExtensions.millisecondsToHours import de.rki.coronawarnapp.util.TimeAndDateExtensions.millisecondsToHours
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.runBlocking
import org.joda.time.Instant
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
...@@ -24,6 +37,17 @@ class DefaultRiskLevels @Inject constructor( ...@@ -24,6 +37,17 @@ class DefaultRiskLevels @Inject constructor(
private val appConfigProvider: AppConfigProvider private val appConfigProvider: AppConfigProvider
) : RiskLevels { ) : RiskLevels {
private var appConfig: ConfigData
init {
runBlocking {
appConfig = appConfigProvider.getAppConfig()
}
appConfigProvider.currentConfig
.onEach { if (it != null) appConfig = it }
}
override fun updateRepository(riskLevel: RiskLevel, time: Long) { override fun updateRepository(riskLevel: RiskLevel, time: Long) {
val rollbackItems = mutableListOf<RollbackItem>() val rollbackItems = mutableListOf<RollbackItem>()
try { try {
...@@ -207,14 +231,311 @@ class DefaultRiskLevels @Inject constructor( ...@@ -207,14 +231,311 @@ class DefaultRiskLevels @Inject constructor(
) )
} }
if (lastCalculatedScore.raw == RiskLevelConstants.INCREASED_RISK && if (lastCalculatedScore.raw == RiskLevelConstants.INCREASED_RISK &&
riskLevel.raw == RiskLevelConstants.LOW_LEVEL_RISK) { riskLevel.raw == RiskLevelConstants.LOW_LEVEL_RISK
) {
LocalData.isUserToBeNotifiedOfLoweredRiskLevel = true LocalData.isUserToBeNotifiedOfLoweredRiskLevel = true
} }
RiskLevelRepository.setRiskLevelScore(riskLevel) RiskLevelRepository.setRiskLevelScore(riskLevel)
} }
private fun dropDueToMinutesAtAttenuation(
exposureWindow: ExposureWindow,
attenuationFilters: List<RiskCalculationParametersOuterClass.MinutesAtAttenuationFilter>
) =
attenuationFilters.any { attenuationFilter ->
// Get total seconds at attenuation in exposure window
val secondsAtAttenuation = exposureWindow.scanInstances
.filter { attenuationFilter.attenuationRange.inRange(it.typicalAttenuationDb) }
.fold(0) { acc, scanInstance -> acc + scanInstance.secondsSinceLastScan }
val minutesAtAttenuation = secondsAtAttenuation / 60
return attenuationFilter.dropIfMinutesInRange.inRange(minutesAtAttenuation)
}
private fun determineTransmissionRiskLevel(
exposureWindow: ExposureWindow,
transmissionRiskLevelEncoding: RiskCalculationParametersOuterClass.TransmissionRiskLevelEncoding
): Int {
val reportTypeOffset = when (exposureWindow.reportType) {
ReportType.RECURSIVE -> transmissionRiskLevelEncoding
.reportTypeOffsetRecursive
ReportType.SELF_REPORT -> transmissionRiskLevelEncoding
.reportTypeOffsetSelfReport
ReportType.CONFIRMED_CLINICAL_DIAGNOSIS -> transmissionRiskLevelEncoding
.reportTypeOffsetConfirmedClinicalDiagnosis
ReportType.CONFIRMED_TEST -> transmissionRiskLevelEncoding
.reportTypeOffsetConfirmedTest
else -> throw UnknownReportTypeException()
}
val infectiousnessOffset = when (exposureWindow.infectiousness) {
Infectiousness.HIGH -> transmissionRiskLevelEncoding
.infectiousnessOffsetHigh
else -> transmissionRiskLevelEncoding
.infectiousnessOffsetStandard
}
return reportTypeOffset + infectiousnessOffset
}
private fun dropDueToTransmissionRiskLevel(
transmissionRiskLevel: Int,
transmissionRiskLevelFilters: List<RiskCalculationParametersOuterClass.TrlFilter>
) =
transmissionRiskLevelFilters.any {
it.dropIfTrlInRange.inRange(transmissionRiskLevel)
}
private fun determineWeightedSeconds(
exposureWindow: ExposureWindow,
minutesAtAttenuationWeight: List<RiskCalculationParametersOuterClass.MinutesAtAttenuationWeight>
): Double =
exposureWindow.scanInstances.fold(.0) { seconds, scanInstance ->
val weight =
minutesAtAttenuationWeight
.filter { it.attenuationRange.inRange(scanInstance.typicalAttenuationDb) }
.map { it.weight }
.firstOrNull() ?: .0
return seconds + scanInstance.secondsSinceLastScan * weight
}
private fun determineRiskLevel(
normalizedTime: Double,
timeToRiskLevelMapping: List<RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping>
) =
timeToRiskLevelMapping
.filter { it.normalizedTimeRange.inRange(normalizedTime) }
.map { it.riskLevel }
.firstOrNull()
override fun calculateRisk(
exposureWindow: ExposureWindow
): RiskResult? {
if (dropDueToMinutesAtAttenuation(exposureWindow, appConfig.minutesAtAttenuationFilters)) {
Timber.d(
"%s dropped due to minutes at attenuation filter",
exposureWindow
)
return null
}
val transmissionRiskLevel = determineTransmissionRiskLevel(
exposureWindow,
appConfig.transmissionRiskLevelEncoding
)
if (dropDueToTransmissionRiskLevel(transmissionRiskLevel, appConfig.transmissionRiskLevelFilters)) {
Timber.d(
"%s dropped due to transmission risk level filter, level is %s",
exposureWindow,
transmissionRiskLevel
)
return null
}
val transmissionRiskValue =
transmissionRiskLevel * appConfig.transmissionRiskLevelMultiplier
Timber.d(
"%s's transmissionRiskValue is: %s",
exposureWindow,
transmissionRiskValue
)
val weightedMinutes = determineWeightedSeconds(
exposureWindow,
appConfig.minutesAtAttenuationWeights
) / 60
Timber.d(
"%s's weightedMinutes are: %s",
exposureWindow,
weightedMinutes
)
val normalizedTime = transmissionRiskValue * weightedMinutes
Timber.d(
"%s's normalizedTime is: %s",
exposureWindow,
normalizedTime
)
val riskLevel = determineRiskLevel(
normalizedTime,
appConfig.normalizedTimePerExposureWindowToRiskLevelMapping
)
if (riskLevel == null) {
Timber.e("Exposure Window: $exposureWindow could not be mapped to a risk level")
throw NormalizedTimePerExposureWindowToRiskLevelMappingMissingException()
}
Timber.d(
"%s's riskLevel is: %s",
exposureWindow,
riskLevel
)
return RiskResult(transmissionRiskLevel, normalizedTime, riskLevel)
}
override fun aggregateResults(
exposureWindowsAndResult: Map<ExposureWindow, RiskResult>
): AggregatedRiskResult {
val uniqueDatesMillisSinceEpoch = exposureWindowsAndResult.keys
.map { it.dateMillisSinceEpoch }
.toSet()
Timber.d(
"uniqueDates: ${
TextUtils.join(System.lineSeparator(), uniqueDatesMillisSinceEpoch)
}"
)
val exposureHistory = uniqueDatesMillisSinceEpoch.map {
aggregateRiskPerDate(it, exposureWindowsAndResult)
}
Timber.d("exposureHistory size: ${exposureHistory.size}")
// 6. Determine `Total Risk`
val totalRiskLevel =
if (exposureHistory.any {
it.riskLevel == RiskCalculationParametersOuterClass
.NormalizedTimeToRiskLevelMapping
.RiskLevel
.HIGH
}) {
RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping.RiskLevel.HIGH
} else {
RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping.RiskLevel.LOW
}
Timber.d("totalRiskLevel: ${totalRiskLevel.name} (${totalRiskLevel.ordinal})")
// 7. Determine `Date of Most Recent Date with Low Risk`
val mostRecentDateWithLowRisk = mostRecentDateForRisk(
exposureHistory,
RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping.RiskLevel.LOW
)
Timber.d("mostRecentDateWithLowRisk: $mostRecentDateWithLowRisk")
// 8. Determine `Date of Most Recent Date with High Risk`
val mostRecentDateWithHighRisk = mostRecentDateForRisk(
exposureHistory,
RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping.RiskLevel.HIGH
)
Timber.d("mostRecentDateWithHighRisk: $mostRecentDateWithHighRisk")
// 9. Determine `Total Minimum Distinct Encounters With Low Risk`
val totalMinimumDistinctEncountersWithLowRisk = exposureHistory
.sumBy { it.minimumDistinctEncountersWithLowRisk }
Timber.d("totalMinimumDistinctEncountersWithLowRisk: $totalMinimumDistinctEncountersWithLowRisk")
// 10. Determine `Total Minimum Distinct Encounters With High Risk`
val totalMinimumDistinctEncountersWithHighRisk = exposureHistory
.sumBy { it.minimumDistinctEncountersWithHighRisk }
Timber.d("totalMinimumDistinctEncountersWithHighRisk: $totalMinimumDistinctEncountersWithHighRisk")
return AggregatedRiskResult(
totalRiskLevel,
totalMinimumDistinctEncountersWithLowRisk,
totalMinimumDistinctEncountersWithHighRisk,
mostRecentDateWithLowRisk,
mostRecentDateWithHighRisk
)
}
private fun mostRecentDateForRisk(
exposureHistory: List<AggregatedRiskPerDateResult>,
riskLevel: RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping.RiskLevel
): Instant? = exposureHistory
.filter { it.riskLevel == riskLevel }
.maxOfOrNull { it.dateMillisSinceEpoch }
?.let { Instant.ofEpochMilli(it) }
private fun aggregateRiskPerDate(
dateMillisSinceEpoch: Long,
exposureWindowsAndResult: Map<ExposureWindow, RiskResult>
): AggregatedRiskPerDateResult {
// 1. Group `Exposure Windows by Date`
val exposureWindowsAndResultForDate = exposureWindowsAndResult
.filter { it.key.dateMillisSinceEpoch == dateMillisSinceEpoch }
// 2. Determine `Normalized Time per Date`
val normalizedTime = exposureWindowsAndResultForDate.values
.sumOf { it.normalizedTime }
Timber.d("Aggregating result for date $dateMillisSinceEpoch - ${Instant.ofEpochMilli(dateMillisSinceEpoch)}")
// 3. Determine `Risk Level per Date`
val riskLevel = try {
appConfig.normalizedTimePerDayToRiskLevelMappingList
.filter { it.normalizedTimeRange.inRange(normalizedTime) }
.map { it.riskLevel }
.first()
} catch (e: Exception) {
throw ApplicationConfigurationInvalidException(
e,
"Invalid config for normalizedTimePerDayToRiskLevelMapping"
)
}
Timber.d("riskLevel: ${riskLevel.name} (${riskLevel.ordinal})")
// 4. Determine `Minimum Distinct Encounters With Low Risk per Date`
val minimumDistinctEncountersWithLowRisk = minimumDistinctEncountersForRisk(
exposureWindowsAndResultForDate,
RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping.RiskLevel.LOW
)
Timber.d("minimumDistinctEncountersWithLowRisk: $minimumDistinctEncountersWithLowRisk")
// 5. Determine `Minimum Distinct Encounters With High Risk per Date`
val minimumDistinctEncountersWithHighRisk = minimumDistinctEncountersForRisk(
exposureWindowsAndResultForDate,
RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping.RiskLevel.HIGH
)
Timber.d("minimumDistinctEncountersWithHighRisk: $minimumDistinctEncountersWithHighRisk")
return AggregatedRiskPerDateResult(
dateMillisSinceEpoch,
riskLevel,
minimumDistinctEncountersWithLowRisk,
minimumDistinctEncountersWithHighRisk
)
}
private fun minimumDistinctEncountersForRisk(
exposureWindowsAndResultForDate: Map<ExposureWindow, RiskResult>,
riskLevel: RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping.RiskLevel
): Int =
exposureWindowsAndResultForDate
.filter { it.value.riskLevel == riskLevel }
.map { "${it.value.transmissionRiskLevel}_${it.key.calibrationConfidence}" }
.distinct()
.size
companion object { companion object {
private val TAG = DefaultRiskLevels::class.java.simpleName private val TAG = DefaultRiskLevels::class.java.simpleName
private const val DECIMAL_MULTIPLIER = 100 private const val DECIMAL_MULTIPLIER = 100
class NormalizedTimePerExposureWindowToRiskLevelMappingMissingException : Exception()
class UnknownReportTypeException : Exception()
private fun <T : Number> RiskCalculationParametersOuterClass.Range.inRange(value: T): Boolean =
when {
minExclusive && value.toDouble() <= min -> false
!minExclusive && value.toDouble() < min -> false
maxExclusive && value.toDouble() >= max -> false
!maxExclusive && value.toDouble() > max -> false
else -> true
}
} }
} }
...@@ -124,18 +124,18 @@ class RiskLevelTask @Inject constructor( ...@@ -124,18 +124,18 @@ class RiskLevelTask @Inject constructor(
} }
private suspend fun backgroundJobsEnabled() = private suspend fun backgroundJobsEnabled() =
backgroundModeStatus.isAutoModeEnabled.first().also { backgroundModeStatus.isAutoModeEnabled.first().also {
if (it) { if (it) {
Timber.tag(TAG) Timber.tag(TAG)
.v("diagnosis keys outdated and active tracing time is above threshold") .v("diagnosis keys outdated and active tracing time is above threshold")
Timber.tag(TAG) Timber.tag(TAG)
.v("manual mode not active (background jobs enabled)") .v("manual mode not active (background jobs enabled)")
} else { } else {
Timber.tag(TAG) Timber.tag(TAG)
.v("diagnosis keys outdated and active tracing time is above threshold") .v("diagnosis keys outdated and active tracing time is above threshold")
Timber.tag(TAG).v("manual mode active (background jobs disabled)") Timber.tag(TAG).v("manual mode active (background jobs disabled)")
}
} }
}
override suspend fun cancel() { override suspend fun cancel() {
Timber.w("cancel() called.") Timber.w("cancel() called.")
......
package de.rki.coronawarnapp.risk package de.rki.coronawarnapp.risk
import com.google.android.gms.nearby.exposurenotification.ExposureSummary import com.google.android.gms.nearby.exposurenotification.ExposureSummary
import com.google.android.gms.nearby.exposurenotification.ExposureWindow
import de.rki.coronawarnapp.risk.result.AggregatedRiskResult
import de.rki.coronawarnapp.risk.result.RiskResult
import de.rki.coronawarnapp.server.protocols.internal.AttenuationDurationOuterClass import de.rki.coronawarnapp.server.protocols.internal.AttenuationDurationOuterClass
interface RiskLevels { interface RiskLevels {
...@@ -22,8 +25,17 @@ interface RiskLevels { ...@@ -22,8 +25,17 @@ interface RiskLevels {
time: Long time: Long
) )
@Deprecated("Switch to new calculation with Exposure Window")
fun calculateRiskScore( fun calculateRiskScore(
attenuationParameters: AttenuationDurationOuterClass.AttenuationDuration, attenuationParameters: AttenuationDurationOuterClass.AttenuationDuration,
exposureSummary: ExposureSummary exposureSummary: ExposureSummary
): Double ): Double
fun calculateRisk(
exposureWindow: ExposureWindow
): RiskResult?
fun aggregateResults(
exposureWindowsAndResult: Map<ExposureWindow, RiskResult>
): AggregatedRiskResult
} }
package de.rki.coronawarnapp.risk.result
import de.rki.coronawarnapp.server.protocols.internal.v2.RiskCalculationParametersOuterClass
data class AggregatedRiskPerDateResult(
val dateMillisSinceEpoch: Long,
val riskLevel: RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping.RiskLevel,
val minimumDistinctEncountersWithLowRisk: Int,
val minimumDistinctEncountersWithHighRisk: Int
)
package de.rki.coronawarnapp.risk.result
import de.rki.coronawarnapp.server.protocols.internal.v2.RiskCalculationParametersOuterClass
import org.joda.time.Instant
data class AggregatedRiskResult(
val totalRiskLevel: RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping.RiskLevel,
val totalMinimumDistinctEncountersWithLowRisk: Int,
val totalMinimumDistinctEncountersWithHighRisk: Int,
val mostRecentDateWithLowRisk: Instant?,
val mostRecentDateWithHighRisk: Instant?
)
package de.rki.coronawarnapp.risk.result
import de.rki.coronawarnapp.server.protocols.internal.v2.RiskCalculationParametersOuterClass
// TODO("Adjust Types")
data class RiskResult(
val transmissionRiskLevel: Int,
val normalizedTime: Double,
val riskLevel: RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping.RiskLevel
)
...@@ -2,6 +2,7 @@ package de.rki.coronawarnapp.appconfig.mapping ...@@ -2,6 +2,7 @@ package de.rki.coronawarnapp.appconfig.mapping
import de.rki.coronawarnapp.appconfig.CWAConfig import de.rki.coronawarnapp.appconfig.CWAConfig
import de.rki.coronawarnapp.appconfig.ExposureDetectionConfig import de.rki.coronawarnapp.appconfig.ExposureDetectionConfig
import de.rki.coronawarnapp.appconfig.ExposureWindowRiskCalculationConfig
import de.rki.coronawarnapp.appconfig.KeyDownloadConfig import de.rki.coronawarnapp.appconfig.KeyDownloadConfig
import de.rki.coronawarnapp.appconfig.RiskCalculationConfig import de.rki.coronawarnapp.appconfig.RiskCalculationConfig
import io.mockk.MockKAnnotations import io.mockk.MockKAnnotations
...@@ -21,6 +22,7 @@ class ConfigParserTest : BaseTest() { ...@@ -21,6 +22,7 @@ class ConfigParserTest : BaseTest() {
@MockK lateinit var keyDownloadConfigMapper: KeyDownloadConfig.Mapper @MockK lateinit var keyDownloadConfigMapper: KeyDownloadConfig.Mapper
@MockK lateinit var exposureDetectionConfigMapper: ExposureDetectionConfig.Mapper @MockK lateinit var exposureDetectionConfigMapper: ExposureDetectionConfig.Mapper
@MockK lateinit var riskCalculationConfigMapper: RiskCalculationConfig.Mapper @MockK lateinit var riskCalculationConfigMapper: RiskCalculationConfig.Mapper
@MockK lateinit var exposureWindowRiskCalculationConfigMapper: ExposureWindowRiskCalculationConfig.Mapper
@BeforeEach @BeforeEach
fun setup() { fun setup() {
...@@ -30,6 +32,7 @@ class ConfigParserTest : BaseTest() { ...@@ -30,6 +32,7 @@ class ConfigParserTest : BaseTest() {
every { keyDownloadConfigMapper.map(any()) } returns mockk() every { keyDownloadConfigMapper.map(any()) } returns mockk()
every { exposureDetectionConfigMapper.map(any()) } returns mockk() every { exposureDetectionConfigMapper.map(any()) } returns mockk()
every { riskCalculationConfigMapper.map(any()) } returns mockk() every { riskCalculationConfigMapper.map(any()) } returns mockk()
every { exposureWindowRiskCalculationConfigMapper.map(any()) } returns mockk()
} }
@AfterEach @AfterEach
...@@ -41,7 +44,8 @@ class ConfigParserTest : BaseTest() { ...@@ -41,7 +44,8 @@ class ConfigParserTest : BaseTest() {
cwaConfigMapper = cwaConfigMapper, cwaConfigMapper = cwaConfigMapper,
keyDownloadConfigMapper = keyDownloadConfigMapper, keyDownloadConfigMapper = keyDownloadConfigMapper,
exposureDetectionConfigMapper = exposureDetectionConfigMapper, exposureDetectionConfigMapper = exposureDetectionConfigMapper,
riskCalculationConfigMapper = riskCalculationConfigMapper riskCalculationConfigMapper = riskCalculationConfigMapper,
exposureWindowRiskCalculationConfigMapper = exposureWindowRiskCalculationConfigMapper
) )
@Test @Test
...@@ -53,6 +57,7 @@ class ConfigParserTest : BaseTest() { ...@@ -53,6 +57,7 @@ class ConfigParserTest : BaseTest() {
keyDownloadConfigMapper.map(any()) keyDownloadConfigMapper.map(any())
exposureDetectionConfigMapper.map(any()) exposureDetectionConfigMapper.map(any())
riskCalculationConfigMapper.map(any()) riskCalculationConfigMapper.map(any())
exposureWindowRiskCalculationConfigMapper.map(any())
} }
} }
} }
......
...@@ -5,7 +5,10 @@ import de.rki.coronawarnapp.appconfig.AppConfigProvider ...@@ -5,7 +5,10 @@ import de.rki.coronawarnapp.appconfig.AppConfigProvider
import de.rki.coronawarnapp.server.protocols.internal.AttenuationDurationOuterClass import de.rki.coronawarnapp.server.protocols.internal.AttenuationDurationOuterClass
import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldBe
import io.mockk.MockKAnnotations import io.mockk.MockKAnnotations
import io.mockk.coEvery
import io.mockk.every
import io.mockk.impl.annotations.MockK import io.mockk.impl.annotations.MockK
import io.mockk.mockk
import junit.framework.TestCase.assertEquals import junit.framework.TestCase.assertEquals
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
...@@ -19,6 +22,10 @@ class RiskLevelsTest : BaseTest() { ...@@ -19,6 +22,10 @@ class RiskLevelsTest : BaseTest() {
@Before @Before
fun setUp() { fun setUp() {
MockKAnnotations.init(this) MockKAnnotations.init(this)
coEvery { appConfigProvider.getAppConfig() } returns mockk()
every { appConfigProvider.currentConfig } returns mockk()
riskLevels = DefaultRiskLevels(appConfigProvider) riskLevels = DefaultRiskLevels(appConfigProvider)
} }
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment