Skip to content
This repository has been archived by the owner on Jun 20, 2023. It is now read-only.

Move risk calculation unrelated functions into task #1682

Merged
merged 2 commits into from
Nov 20, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,167 +2,32 @@ package de.rki.coronawarnapp.risk

import android.text.TextUtils
import androidx.annotation.VisibleForTesting
import androidx.core.app.NotificationManagerCompat
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.R
import de.rki.coronawarnapp.appconfig.ConfigData
import de.rki.coronawarnapp.exception.RiskLevelCalculationException
import de.rki.coronawarnapp.notification.NotificationHelper
import de.rki.coronawarnapp.risk.RiskLevel.UNKNOWN_RISK_INITIAL
import de.rki.coronawarnapp.risk.RiskLevel.UNKNOWN_RISK_OUTDATED_RESULTS
import de.rki.coronawarnapp.appconfig.ExposureWindowRiskCalculationConfig
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.v2.RiskCalculationParametersOuterClass
import de.rki.coronawarnapp.storage.LocalData
import de.rki.coronawarnapp.storage.RiskLevelRepository
import de.rki.coronawarnapp.util.TimeAndDateExtensions.millisecondsToHours
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import org.joda.time.Instant
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Singleton

typealias ProtoRiskLevel = RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping.RiskLevel

@Singleton
class DefaultRiskLevels @Inject constructor(
private val exposureResultStore: ExposureResultStore
) : RiskLevels {
override fun updateRepository(riskLevel: RiskLevel, time: Long) {
val rollbackItems = mutableListOf<RollbackItem>()
try {
Timber.tag(TAG).v("Update the risk level with $riskLevel")
val lastCalculatedRiskLevelScoreForRollback =
RiskLevelRepository.getLastCalculatedScore()
updateRiskLevelScore(riskLevel)
rollbackItems.add {
updateRiskLevelScore(lastCalculatedRiskLevelScoreForRollback)
}

// risk level calculation date update
val lastCalculatedRiskLevelDate = LocalData.lastTimeRiskLevelCalculation()
LocalData.lastTimeRiskLevelCalculation(time)
rollbackItems.add {
LocalData.lastTimeRiskLevelCalculation(lastCalculatedRiskLevelDate)
}
} catch (error: Exception) {
Timber.tag(TAG).e(error, "Updating the RiskLevelRepository failed.")

try {
Timber.tag(TAG).d("Initiate Rollback")
for (rollbackItem: RollbackItem in rollbackItems) rollbackItem.invoke()
} catch (rollbackException: Exception) {
Timber.tag(TAG).e(rollbackException, "RiskLevelRepository rollback failed.")
}

throw error
}
}

override fun calculationNotPossibleBecauseOfOutdatedResults(): Boolean {
// if the last calculation is longer in the past as the defined threshold we return the stale state
val timeSinceLastDiagnosisKeyFetchFromServer =
TimeVariables.getTimeSinceLastDiagnosisKeyFetchFromServer()
?: throw RiskLevelCalculationException(
IllegalArgumentException(
"Time since last exposure calculation is null"
)
)
/** we only return outdated risk level if the threshold is reached AND the active tracing time is above the
defined threshold because [UNKNOWN_RISK_INITIAL] overrules [UNKNOWN_RISK_OUTDATED_RESULTS] */
return timeSinceLastDiagnosisKeyFetchFromServer.millisecondsToHours() >
TimeVariables.getMaxStaleExposureRiskRange() && isActiveTracingTimeAboveThreshold()
}
class DefaultRiskLevels @Inject constructor() : RiskLevels {

override fun calculationNotPossibleBecauseOfNoKeys() =
(TimeVariables.getLastTimeDiagnosisKeysFromServerFetch() == null).also {
if (it) {
Timber.tag(TAG)
.v("No last time diagnosis keys from server fetch timestamp was found")
}
}

override fun isIncreasedRisk(appConfig: ConfigData, exposureWindows: List<ExposureWindow>): Boolean {
override fun determineRisk(
appConfig: ExposureWindowRiskCalculationConfig,
exposureWindows: List<ExposureWindow>
): AggregatedRiskResult {
val riskResultsPerWindow =
exposureWindows.mapNotNull { window ->
calculateRisk(appConfig, window)?.let { window to it }
}.toMap()

val aggregatedResult = aggregateResults(appConfig, riskResultsPerWindow)

exposureResultStore.entities.value = ExposureResult(exposureWindows, aggregatedResult)

val highRisk = aggregatedResult.totalRiskLevel == ProtoRiskLevel.HIGH

if (highRisk) {
internalMatchedKeyCount.value = aggregatedResult.totalMinimumDistinctEncountersWithHighRisk
internalDaysSinceLastExposure.value = aggregatedResult.numberOfDaysWithHighRisk
} else {
internalMatchedKeyCount.value = aggregatedResult.totalMinimumDistinctEncountersWithLowRisk
internalDaysSinceLastExposure.value = aggregatedResult.numberOfDaysWithLowRisk
}

return highRisk
}

override fun isActiveTracingTimeAboveThreshold(): Boolean {
val durationTracingIsActive = TimeVariables.getTimeActiveTracingDuration()
val activeTracingDurationInHours = durationTracingIsActive.millisecondsToHours()
val durationTracingIsActiveThreshold =
TimeVariables.getMinActivatedTracingTime().toLong()

return (activeTracingDurationInHours >= durationTracingIsActiveThreshold).also {
Timber.tag(TAG).v(
"Active tracing time ($activeTracingDurationInHours h) is above threshold " +
"($durationTracingIsActiveThreshold h): $it"
)
}
}

@VisibleForTesting
internal fun withinDefinedLevelThreshold(riskScore: Double, min: Int, max: Int) =
riskScore >= min && riskScore <= max

/**
* Updates the Risk Level Score in the repository with the calculated Risk Level
*
* @param riskLevel
*/
@VisibleForTesting
internal fun updateRiskLevelScore(riskLevel: RiskLevel) {
val lastCalculatedScore = RiskLevelRepository.getLastCalculatedScore()
Timber.d("last CalculatedS core is ${lastCalculatedScore.raw} and Current Risk Level is ${riskLevel.raw}")

if (RiskLevel.riskLevelChangedBetweenLowAndHigh(
lastCalculatedScore,
riskLevel
) && !LocalData.submissionWasSuccessful()
) {
Timber.d(
"Notification Permission = ${
NotificationManagerCompat.from(CoronaWarnApplication.getAppContext()).areNotificationsEnabled()
}"
)

NotificationHelper.sendNotification(
CoronaWarnApplication.getAppContext().getString(R.string.notification_body)
)

Timber.d("Risk level changed and notification sent. Current Risk level is ${riskLevel.raw}")
}
if (lastCalculatedScore.raw == RiskLevelConstants.INCREASED_RISK &&
riskLevel.raw == RiskLevelConstants.LOW_LEVEL_RISK
) {
LocalData.isUserToBeNotifiedOfLoweredRiskLevel = true

Timber.d("Risk level changed LocalData is updated. Current Risk level is ${riskLevel.raw}")
}
RiskLevelRepository.setRiskLevelScore(riskLevel)
return aggregateResults(appConfig, riskResultsPerWindow)
}

private fun ExposureWindow.dropDueToMinutesAtAttenuation(
Expand Down Expand Up @@ -233,8 +98,9 @@ class DefaultRiskLevels @Inject constructor(
.map { it.riskLevel }
.firstOrNull()

override fun calculateRisk(
appConfig: ConfigData,
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
internal fun calculateRisk(
appConfig: ExposureWindowRiskCalculationConfig,
exposureWindow: ExposureWindow
): RiskResult? {
if (exposureWindow.dropDueToMinutesAtAttenuation(appConfig.minutesAtAttenuationFilters)) {
Expand Down Expand Up @@ -289,8 +155,9 @@ class DefaultRiskLevels @Inject constructor(
)
}

override fun aggregateResults(
appConfig: ConfigData,
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
internal fun aggregateResults(
appConfig: ExposureWindowRiskCalculationConfig,
exposureWindowsAndResult: Map<ExposureWindow, RiskResult>
): AggregatedRiskResult {
val uniqueDatesMillisSinceEpoch = exposureWindowsAndResult.keys
Expand Down Expand Up @@ -378,7 +245,7 @@ class DefaultRiskLevels @Inject constructor(
.size

private fun aggregateRiskPerDate(
appConfig: ConfigData,
appConfig: ExposureWindowRiskCalculationConfig,
dateMillisSinceEpoch: Long,
exposureWindowsAndResult: Map<ExposureWindow, RiskResult>
): AggregatedRiskPerDateResult {
Expand Down Expand Up @@ -431,8 +298,6 @@ class DefaultRiskLevels @Inject constructor(
.size

companion object {
private val TAG = DefaultRiskLevels::class.java.simpleName
private const val DECIMAL_MULTIPLIER = 100

open class RiskLevelMappingMissingException(msg: String) : Exception(msg)

Expand All @@ -456,11 +321,5 @@ class DefaultRiskLevels @Inject constructor(
!maxExclusive && value.toDouble() > max -> false
else -> true
}

private val internalMatchedKeyCount = MutableStateFlow(0)
val matchedKeyCount: Flow<Int> = internalMatchedKeyCount

private val internalDaysSinceLastExposure = MutableStateFlow(0)
val daysSinceLastExposure: Flow<Int> = internalDaysSinceLastExposure
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package de.rki.coronawarnapp.risk

import com.google.android.gms.nearby.exposurenotification.ExposureWindow
import de.rki.coronawarnapp.risk.result.AggregatedRiskResult
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import javax.inject.Inject
import javax.inject.Singleton
Expand All @@ -15,6 +16,12 @@ class ExposureResultStore @Inject constructor() {
aggregatedRiskResult = null
)
)

internal val internalMatchedKeyCount = MutableStateFlow(0)
val matchedKeyCount: Flow<Int> = internalMatchedKeyCount

internal val internalDaysSinceLastExposure = MutableStateFlow(0)
val daysSinceLastExposure: Flow<Int> = internalDaysSinceLastExposure
}

data class ExposureResult(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package de.rki.coronawarnapp.risk

import de.rki.coronawarnapp.server.protocols.internal.v2.RiskCalculationParametersOuterClass

typealias ProtoRiskLevel = RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping.RiskLevel
Loading