diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/AppConfigModule.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/AppConfigModule.kt
index 82936e52b38862e1288652fe250952d4e5669375..87d675328c7d39af0f306e2d81860789efe4b094 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/AppConfigModule.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/AppConfigModule.kt
@@ -8,6 +8,7 @@ import de.rki.coronawarnapp.appconfig.download.AppConfigHttpCache
 import de.rki.coronawarnapp.appconfig.mapping.CWAConfigMapper
 import de.rki.coronawarnapp.appconfig.mapping.DownloadConfigMapper
 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.environment.download.DownloadCDNHttpClient
 import de.rki.coronawarnapp.environment.download.DownloadCDNServerUrl
@@ -61,17 +62,24 @@ class AppConfigModule {
     }
 
     @Provides
-    fun cwaMapper(mapper: CWAConfigMapper): CWAConfig.Mapper = mapper
+    fun cwaMapper(mapper: CWAConfigMapper):
+        CWAConfig.Mapper = mapper
 
     @Provides
-    fun downloadMapper(mapper: DownloadConfigMapper): KeyDownloadConfig.Mapper = mapper
+    fun downloadMapper(mapper: DownloadConfigMapper):
+        KeyDownloadConfig.Mapper = mapper
 
     @Provides
-    fun exposurMapper(mapper: ExposureDetectionConfigMapper): ExposureDetectionConfig.Mapper =
-        mapper
+    fun exposurMapper(mapper: ExposureDetectionConfigMapper):
+        ExposureDetectionConfig.Mapper = mapper
 
     @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 {
         private val HTTP_TIMEOUT_APPCONFIG = Duration.standardSeconds(10)
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/ExposureWindowRiskCalculationConfig.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/ExposureWindowRiskCalculationConfig.kt
new file mode 100644
index 0000000000000000000000000000000000000000..80eadd589d6bf68fc19648513195ab5e36841036
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/ExposureWindowRiskCalculationConfig.kt
@@ -0,0 +1,20 @@
+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
+    }
+}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/ConfigMapping.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/ConfigMapping.kt
index 9858ec812bfc0b74c37330b2d44704decd085a5d..1e9ccb2d404d3c6f0217e8f3b44fc33bbda68329 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/ConfigMapping.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/ConfigMapping.kt
@@ -2,6 +2,7 @@ package de.rki.coronawarnapp.appconfig.mapping
 
 import de.rki.coronawarnapp.appconfig.CWAConfig
 import de.rki.coronawarnapp.appconfig.ExposureDetectionConfig
+import de.rki.coronawarnapp.appconfig.ExposureWindowRiskCalculationConfig
 import de.rki.coronawarnapp.appconfig.KeyDownloadConfig
 import de.rki.coronawarnapp.appconfig.RiskCalculationConfig
 import de.rki.coronawarnapp.server.protocols.internal.AppConfig
@@ -10,7 +11,8 @@ interface ConfigMapping :
     CWAConfig,
     KeyDownloadConfig,
     ExposureDetectionConfig,
-    RiskCalculationConfig {
+    RiskCalculationConfig,
+    ExposureWindowRiskCalculationConfig {
 
     @Deprecated("Try to access a more specific config type, avoid the RAW variant.")
     val rawConfig: AppConfig.ApplicationConfiguration
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/ConfigParser.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/ConfigParser.kt
index 8449b81b7cf3c5e996dc8121f97de585bc0ef693..41988550a2cef0482119b04c1d2bbda3c5463c2c 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/ConfigParser.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/ConfigParser.kt
@@ -3,9 +3,11 @@ package de.rki.coronawarnapp.appconfig.mapping
 import dagger.Reusable
 import de.rki.coronawarnapp.appconfig.CWAConfig
 import de.rki.coronawarnapp.appconfig.ExposureDetectionConfig
+import de.rki.coronawarnapp.appconfig.ExposureWindowRiskCalculationConfig
 import de.rki.coronawarnapp.appconfig.KeyDownloadConfig
 import de.rki.coronawarnapp.appconfig.RiskCalculationConfig
 import de.rki.coronawarnapp.server.protocols.internal.AppConfig
+import de.rki.coronawarnapp.server.protocols.internal.v2.AppConfigAndroid
 import timber.log.Timber
 import javax.inject.Inject
 
@@ -14,17 +16,24 @@ class ConfigParser @Inject constructor(
     private val cwaConfigMapper: CWAConfig.Mapper,
     private val keyDownloadConfigMapper: KeyDownloadConfig.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 {
+        // TODO replace with actual v2 config
+        val dummyConfig = AppConfigAndroid
+            .ApplicationConfigurationAndroid
+            .newBuilder()
+            .build()
         parseRawArray(configBytes).let {
             DefaultConfigMapping(
                 rawConfig = it,
                 cwaConfig = cwaConfigMapper.map(it),
                 keyDownloadConfig = keyDownloadConfigMapper.map(it),
                 exposureDetectionConfig = exposureDetectionConfigMapper.map(it),
-                riskCalculationConfig = riskCalculationConfigMapper.map(it)
+                riskCalculationConfig = riskCalculationConfigMapper.map(it),
+                exposureWindowRiskCalculationConfig = exposureWindowRiskCalculationConfigMapper.map(dummyConfig)
             )
         }
     } catch (e: Exception) {
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/DefaultConfigMapping.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/DefaultConfigMapping.kt
index 783385ddfdd3e7c06198eaff8c0ef5ba5a2961ff..78bacc12c694bcbb9cb409e2a6ed590ed1e6120d 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/DefaultConfigMapping.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/DefaultConfigMapping.kt
@@ -2,6 +2,7 @@ package de.rki.coronawarnapp.appconfig.mapping
 
 import de.rki.coronawarnapp.appconfig.CWAConfig
 import de.rki.coronawarnapp.appconfig.ExposureDetectionConfig
+import de.rki.coronawarnapp.appconfig.ExposureWindowRiskCalculationConfig
 import de.rki.coronawarnapp.appconfig.KeyDownloadConfig
 import de.rki.coronawarnapp.appconfig.RiskCalculationConfig
 import de.rki.coronawarnapp.server.protocols.internal.AppConfig
@@ -11,9 +12,11 @@ data class DefaultConfigMapping(
     val cwaConfig: CWAConfig,
     val keyDownloadConfig: KeyDownloadConfig,
     val exposureDetectionConfig: ExposureDetectionConfig,
-    val riskCalculationConfig: RiskCalculationConfig
+    val riskCalculationConfig: RiskCalculationConfig,
+    val exposureWindowRiskCalculationConfig: ExposureWindowRiskCalculationConfig
 ) : ConfigMapping,
     CWAConfig by cwaConfig,
     KeyDownloadConfig by keyDownloadConfig,
     ExposureDetectionConfig by exposureDetectionConfig,
-    RiskCalculationConfig by riskCalculationConfig
+    RiskCalculationConfig by riskCalculationConfig,
+    ExposureWindowRiskCalculationConfig by exposureWindowRiskCalculationConfig
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/ExposureWindowRiskCalculationConfigMapper.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/ExposureWindowRiskCalculationConfigMapper.kt
new file mode 100644
index 0000000000000000000000000000000000000000..e64fdc2bad115bfb34b9812a032badcdb69c3799
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/ExposureWindowRiskCalculationConfigMapper.kt
@@ -0,0 +1,44 @@
+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
+}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/DefaultRiskLevels.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/DefaultRiskLevels.kt
index 4df5b3228daba8b66f098621e61dc89a8ff73ec1..e0fd8a80dae4a9ba7f5e9e93647e24686b105bfc 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/DefaultRiskLevels.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/DefaultRiskLevels.kt
@@ -1,19 +1,32 @@
 package de.rki.coronawarnapp.risk
 
+import android.text.TextUtils
 import androidx.annotation.VisibleForTesting
 import androidx.core.app.NotificationCompat
 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.R
 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.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.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.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.onEach
+import kotlinx.coroutines.runBlocking
+import org.joda.time.Instant
 import timber.log.Timber
 import javax.inject.Inject
 import javax.inject.Singleton
@@ -24,6 +37,17 @@ class DefaultRiskLevels @Inject constructor(
     private val appConfigProvider: AppConfigProvider
 ) : 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) {
         val rollbackItems = mutableListOf<RollbackItem>()
         try {
@@ -207,14 +231,311 @@ class DefaultRiskLevels @Inject constructor(
             )
         }
         if (lastCalculatedScore.raw == RiskLevelConstants.INCREASED_RISK &&
-            riskLevel.raw == RiskLevelConstants.LOW_LEVEL_RISK) {
+            riskLevel.raw == RiskLevelConstants.LOW_LEVEL_RISK
+        ) {
             LocalData.isUserToBeNotifiedOfLoweredRiskLevel = true
         }
         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 {
         private val TAG = DefaultRiskLevels::class.java.simpleName
         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
+            }
     }
 }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskLevelTask.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskLevelTask.kt
index 407fef631dd901e8995d64cff3047acc22beb5c6..29ab44cbd70cb8e8be0d268931571af9954c6ec5 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskLevelTask.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskLevelTask.kt
@@ -124,18 +124,18 @@ class RiskLevelTask @Inject constructor(
     }
 
     private suspend fun backgroundJobsEnabled() =
-            backgroundModeStatus.isAutoModeEnabled.first().also {
-                if (it) {
-                    Timber.tag(TAG)
-                            .v("diagnosis keys outdated and active tracing time is above threshold")
-                    Timber.tag(TAG)
-                            .v("manual mode not active (background jobs enabled)")
-                } else {
-                    Timber.tag(TAG)
-                            .v("diagnosis keys outdated and active tracing time is above threshold")
-                    Timber.tag(TAG).v("manual mode active (background jobs disabled)")
-                }
+        backgroundModeStatus.isAutoModeEnabled.first().also {
+            if (it) {
+                Timber.tag(TAG)
+                    .v("diagnosis keys outdated and active tracing time is above threshold")
+                Timber.tag(TAG)
+                    .v("manual mode not active (background jobs enabled)")
+            } else {
+                Timber.tag(TAG)
+                    .v("diagnosis keys outdated and active tracing time is above threshold")
+                Timber.tag(TAG).v("manual mode active (background jobs disabled)")
             }
+        }
 
     override suspend fun cancel() {
         Timber.w("cancel() called.")
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskLevels.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskLevels.kt
index b8cd2f00c6b4ea61636c4593b03d43b65520078d..28cdbf181ce883ce79a66412d4f0c4271a3f1cd0 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskLevels.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskLevels.kt
@@ -1,6 +1,9 @@
 package de.rki.coronawarnapp.risk
 
 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
 
 interface RiskLevels {
@@ -22,8 +25,17 @@ interface RiskLevels {
         time: Long
     )
 
+    @Deprecated("Switch to new calculation with Exposure Window")
     fun calculateRiskScore(
         attenuationParameters: AttenuationDurationOuterClass.AttenuationDuration,
         exposureSummary: ExposureSummary
     ): Double
+
+    fun calculateRisk(
+        exposureWindow: ExposureWindow
+    ): RiskResult?
+
+    fun aggregateResults(
+        exposureWindowsAndResult: Map<ExposureWindow, RiskResult>
+    ): AggregatedRiskResult
 }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/result/AggregatedRiskPerDateResult.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/result/AggregatedRiskPerDateResult.kt
new file mode 100644
index 0000000000000000000000000000000000000000..99c140888f23c365690f3671430e015cf966d96b
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/result/AggregatedRiskPerDateResult.kt
@@ -0,0 +1,10 @@
+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
+)
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/result/AggregatedRiskResult.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/result/AggregatedRiskResult.kt
new file mode 100644
index 0000000000000000000000000000000000000000..17e48ce7644a01996839269dce8988141d484efd
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/result/AggregatedRiskResult.kt
@@ -0,0 +1,12 @@
+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?
+)
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/result/RiskResult.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/result/RiskResult.kt
new file mode 100644
index 0000000000000000000000000000000000000000..e2b5c582b347c962ea92e47917e76b620b6a9143
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/result/RiskResult.kt
@@ -0,0 +1,10 @@
+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
+)
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/mapping/ConfigParserTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/mapping/ConfigParserTest.kt
index 18ee07f72f83cfdbfccae776f6df5edc21a23083..55e68f895f3defd027fe002af7aa113dc4b28aa7 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/mapping/ConfigParserTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/mapping/ConfigParserTest.kt
@@ -2,6 +2,7 @@ package de.rki.coronawarnapp.appconfig.mapping
 
 import de.rki.coronawarnapp.appconfig.CWAConfig
 import de.rki.coronawarnapp.appconfig.ExposureDetectionConfig
+import de.rki.coronawarnapp.appconfig.ExposureWindowRiskCalculationConfig
 import de.rki.coronawarnapp.appconfig.KeyDownloadConfig
 import de.rki.coronawarnapp.appconfig.RiskCalculationConfig
 import io.mockk.MockKAnnotations
@@ -21,6 +22,7 @@ class ConfigParserTest : BaseTest() {
     @MockK lateinit var keyDownloadConfigMapper: KeyDownloadConfig.Mapper
     @MockK lateinit var exposureDetectionConfigMapper: ExposureDetectionConfig.Mapper
     @MockK lateinit var riskCalculationConfigMapper: RiskCalculationConfig.Mapper
+    @MockK lateinit var exposureWindowRiskCalculationConfigMapper: ExposureWindowRiskCalculationConfig.Mapper
 
     @BeforeEach
     fun setup() {
@@ -30,6 +32,7 @@ class ConfigParserTest : BaseTest() {
         every { keyDownloadConfigMapper.map(any()) } returns mockk()
         every { exposureDetectionConfigMapper.map(any()) } returns mockk()
         every { riskCalculationConfigMapper.map(any()) } returns mockk()
+        every { exposureWindowRiskCalculationConfigMapper.map(any()) } returns mockk()
     }
 
     @AfterEach
@@ -41,7 +44,8 @@ class ConfigParserTest : BaseTest() {
         cwaConfigMapper = cwaConfigMapper,
         keyDownloadConfigMapper = keyDownloadConfigMapper,
         exposureDetectionConfigMapper = exposureDetectionConfigMapper,
-        riskCalculationConfigMapper = riskCalculationConfigMapper
+        riskCalculationConfigMapper = riskCalculationConfigMapper,
+        exposureWindowRiskCalculationConfigMapper = exposureWindowRiskCalculationConfigMapper
     )
 
     @Test
@@ -53,6 +57,7 @@ class ConfigParserTest : BaseTest() {
                 keyDownloadConfigMapper.map(any())
                 exposureDetectionConfigMapper.map(any())
                 riskCalculationConfigMapper.map(any())
+                exposureWindowRiskCalculationConfigMapper.map(any())
             }
         }
     }
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/RiskLevelsTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/RiskLevelsTest.kt
index 845722a2c23174f01becc23d183dc78053e30226..746c8acf70be3dedb4969ce5a0611bd7bbd09529 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/RiskLevelsTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/RiskLevelsTest.kt
@@ -5,7 +5,10 @@ import de.rki.coronawarnapp.appconfig.AppConfigProvider
 import de.rki.coronawarnapp.server.protocols.internal.AttenuationDurationOuterClass
 import io.kotest.matchers.shouldBe
 import io.mockk.MockKAnnotations
+import io.mockk.coEvery
+import io.mockk.every
 import io.mockk.impl.annotations.MockK
+import io.mockk.mockk
 import junit.framework.TestCase.assertEquals
 import org.junit.Before
 import org.junit.Test
@@ -19,6 +22,10 @@ class RiskLevelsTest : BaseTest() {
     @Before
     fun setUp() {
         MockKAnnotations.init(this)
+
+        coEvery { appConfigProvider.getAppConfig() } returns mockk()
+        every { appConfigProvider.currentConfig } returns mockk()
+
         riskLevels = DefaultRiskLevels(appConfigProvider)
     }