diff --git a/coralibre-sdk/sdk/src/main/java/org/coralibre/android/sdk/fakegms/nearby/Nearby.java b/coralibre-sdk/sdk/src/main/java/org/coralibre/android/sdk/fakegms/nearby/Nearby.java index 34762d4..3526132 100644 --- a/coralibre-sdk/sdk/src/main/java/org/coralibre/android/sdk/fakegms/nearby/Nearby.java +++ b/coralibre-sdk/sdk/src/main/java/org/coralibre/android/sdk/fakegms/nearby/Nearby.java @@ -1,12 +1,14 @@ package org.coralibre.android.sdk.fakegms.nearby; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + import org.coralibre.android.sdk.fakegms.nearby.exposurenotification.ExposureConfiguration; import org.coralibre.android.sdk.fakegms.nearby.exposurenotification.ExposureInformation; import org.coralibre.android.sdk.fakegms.nearby.exposurenotification.ExposureSummary; import org.coralibre.android.sdk.fakegms.nearby.exposurenotification.TemporaryExposureKey; import org.coralibre.android.sdk.fakegms.tasks.Task; import org.coralibre.android.sdk.fakegms.tasks.TaskAutorunOnceListenersThere; -import org.jetbrains.annotations.NotNull; import java.io.File; import java.util.List; @@ -25,7 +27,7 @@ public class Nearby { private static Nearby instance = null; - public static Nearby getExposureNotificationClient(@NotNull Object context) { + public static Nearby getExposureNotificationClient(@NonNull Object context) { // This strange type name mismatching is expected by the rki app. if (instance == null) { @@ -84,11 +86,25 @@ public void runInternal() { } - public Task provideDiagnosisKeys( - List keyFiles, - ExposureConfiguration configuration, // might be null! - String token - ) { + /** + * Adds keys of infected people contained in {@code keyFiles} to the database and + * provides to the client the {@code exposureConfiguration} to use to calculate risk + * + * @see description on developers.google.com + * @param keyFiles a list of files to extract the keys of confirmed cases from, obtained from + * an internet-accessible server. The file format is described + * on developers.google.com + * [TODO still unused] + * @param exposureConfiguration the configuration to use to calculate risk exposure, needs to + * be provided before calling {@link #getExposureSummary(String)} + * and {@link #getExposureInformation(String)} + * @param token identifier that could be randomly generated for every call or reused to group + * together results in calls to {@link #getExposureSummary(String)} + * and {@link #getExposureInformation(String)} [TODO still unused] + * @return a runnable task + */ + public Task provideDiagnosisKeys(final List keyFiles, + @Nullable final ExposureConfiguration exposureConfiguration, final String token) { return new TaskAutorunOnceListenersThere() { @Override public void runInternal() { diff --git a/coralibre-sdk/sdk/src/main/java/org/coralibre/android/sdk/fakegms/nearby/exposurenotification/ExposureConfiguration.java b/coralibre-sdk/sdk/src/main/java/org/coralibre/android/sdk/fakegms/nearby/exposurenotification/ExposureConfiguration.java index 01b096b..2bc960c 100644 --- a/coralibre-sdk/sdk/src/main/java/org/coralibre/android/sdk/fakegms/nearby/exposurenotification/ExposureConfiguration.java +++ b/coralibre-sdk/sdk/src/main/java/org/coralibre/android/sdk/fakegms/nearby/exposurenotification/ExposureConfiguration.java @@ -1,5 +1,13 @@ package org.coralibre.android.sdk.fakegms.nearby.exposurenotification; +import org.coralibre.android.sdk.internal.database.ppcp.model.CapturedData; + +/** + * Contains the parameters of the risk calculation algorithm. In particular: + * {@code riskScore = attenuationScore * daysSinceLastExposureScore * durationScore * transmissionRiskScore} + * @see risk calculation algorithm details on developer.apple.com + * @see documentation on developers.google.com + */ public class ExposureConfiguration { // @@ -9,13 +17,235 @@ public class ExposureConfiguration { // https://github.com/corona-warn-app/cwa-app-android/blob/master/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/InternalExposureNotificationClient.kt // - public int minimumRiskScore = -1; - public int[] attenuationScores = null; - public int[] daysSinceLastExposureScores = null; - public int[] durationScores = null; - public int[] transmissionRiskScores = null; - public int[] durationAtAttenuationThresholds = null; + // using -1 values as default to indicate missing value for debugging purposes + + /** + * The only fields affected by this value are {@link ExposureSummary#maximumRiskScore}, + * {@link ExposureSummary#summationRiskScore} and {@link ExposureInformation#totalRiskScore}, + * which ignore exposure incidents with lower scores. Default is no minimum (i.e. {@code 0}). + * @see documentation on developers.google.com + */ + private final int minimumRiskScore; + + /** + * Contains how much risk should be associated with every possible range of attenuation values + * for an exposure. In particular (where A is the bluetooth attenuation value from an exposure: + * A = {@code transmission power - } {@link CapturedData#getRssi()}):
+ * attenuationScores[0] | A > 73dB
+ * attenuationScores[1] | 73dB >= A > 63dB
+ * attenuationScores[2] | 63dB >= A > 51dB
+ * attenuationScores[3] | 51dB >= A > 33dB
+ * attenuationScores[4] | 33dB >= A > 27dB
+ * attenuationScores[5] | 27dB >= A > 15dB
+ * attenuationScores[6] | 15dB >= A > 10dB
+ * attenuationScores[7] | A <= 10dB + * @see risk calculation algorithm details on developer.apple.com + * @see documentation on developers.google.com + */ + private final int[] attenuationScores; + + /** + * Contains how much risk should be associated with every possible day range since last + * exposure. In particular:
+ * daysSinceLastExposureScores[0] | >= 14 days
+ * daysSinceLastExposureScores[1] | 12-13 days
+ * daysSinceLastExposureScores[2] | 10-11 days
+ * daysSinceLastExposureScores[3] | 8-9 days
+ * daysSinceLastExposureScores[4] | 6-7 days
+ * daysSinceLastExposureScores[5] | 4-5 days
+ * daysSinceLastExposureScores[6] | 2-3 days
+ * daysSinceLastExposureScores[7] | 0-1 days + * @see risk calculation algorithm details on developer.apple.com + * @see documentation on developers.google.com + */ + private final int[] daysSinceLastExposureScores; + + /** + * Contains how much risk should be associated with every possible time range of the duration + * of an exposure. In particular (where D is the duration of an exposure):
+ * durationScores[0] | D = 0min
+ * durationScores[1] | D <= 5min
+ * durationScores[2] | 5min < D <= 10min
+ * durationScores[3] | 10min < D <= 15min
+ * durationScores[4] | 15min < D <= 20min
+ * durationScores[5] | 20min < D <= 25min
+ * durationScores[6] | 25min < D <= 30min
+ * durationScores[7] | D > 30min + * @see risk calculation algorithm details on developer.apple.com + * @see documentation on developers.google.com + */ + private final int[] durationScores; + + /** + * Contains how much transmission risk should be associated to an encounter. The risk factor + * (i.e. the usage of these values) is app-defined, and for example a high risk could be + * assigned to encounters with users who recently tested positive, and a lower score to + * encounters with old positives + * @see risk calculation algorithm details on developer.apple.com + * @see documentation on developers.google.com + */ + private final int[] transmissionRiskScores; + + /** + * Contains the thresholds used to calculate the three attenuation duration buckets for + * {@link ExposureSummary#attenuationDurations}. In particular:
+ * durationAtAttenuationThresholds[0] | low threshold (defaults to 50)
+ * durationAtAttenuationThresholds[1] | high threshold (defaults to 70)
+ *
+ * These values are used to calculate the three attenuation duration ranges:
+ * 1. Attenuation <= durationAtAttenuationThresholds[0]
+ * 2. durationAtAttenuationThresholds[0] < Attenuation <= durationAtAttenuationThresholds[1]
+ * 3. Y < durationAtAttenuationThresholds[1] + * @see documentation on developer.apple.com + */ + private final int[] durationAtAttenuationThresholds; + + + private ExposureConfiguration(final int minimumRiskScore, + final int[] attenuationScores, + final int[] daysSinceLastExposureScores, + final int[] durationScores, + final int[] transmissionRiskScores, + final int[] durationAtAttenuationThresholds) { + + this.minimumRiskScore = minimumRiskScore; + this.attenuationScores = attenuationScores; + this.daysSinceLastExposureScores = daysSinceLastExposureScores; + this.durationScores = durationScores; + this.transmissionRiskScores = transmissionRiskScores; + this.durationAtAttenuationThresholds = durationAtAttenuationThresholds; + } + + + /** + * @see #minimumRiskScore + */ + public int getMinimumRiskScore() { + return minimumRiskScore; + } + + /** + * @see #durationAtAttenuationThresholds + */ + public int getDurationAtAttenuationLowThreshold() { + return durationAtAttenuationThresholds[0]; + } + + /** + * @see #durationAtAttenuationThresholds + */ + public int getDurationAtAttenuationHighThreshold() { + return durationAtAttenuationThresholds[1]; + } + + + /** + * @param attenuationValue the bluetooth attenuation value from an exposure, in dB: + * {@code transmission power - } {@link CapturedData#getRssi()} + * @return the attenuation score for the provided value + * @see #attenuationScores + */ + public int getAttenuationScore(final int attenuationValue) { + if (attenuationValue > 73) { + return attenuationScores[0]; + } else if (attenuationValue > 63) { + return attenuationScores[1]; + } else if (attenuationValue > 51) { + return attenuationScores[2]; + } else if (attenuationValue > 33) { + return attenuationScores[3]; + } else if (attenuationValue > 27) { + return attenuationScores[4]; + } else if (attenuationValue > 15) { + return attenuationScores[5]; + } else if (attenuationValue > 10) { + return attenuationScores[6]; + } else { + return attenuationScores[7]; + } + } + + /** + * @param daysSinceLastExposureValue the number of days since the last exposure + * @return the days since last exposure score for the provided value + * @see #daysSinceLastExposureScores + */ + public int getDaysSinceLastExposureScore(final int daysSinceLastExposureValue) { + if (daysSinceLastExposureValue >= 14) { + return daysSinceLastExposureScores[0]; + } else if (daysSinceLastExposureValue == 12 || daysSinceLastExposureValue == 13) { + return daysSinceLastExposureScores[1]; + } else if (daysSinceLastExposureValue == 10 || daysSinceLastExposureValue == 11) { + return daysSinceLastExposureScores[2]; + } else if (daysSinceLastExposureValue == 8 || daysSinceLastExposureValue == 9) { + return daysSinceLastExposureScores[3]; + } else if (daysSinceLastExposureValue == 6 || daysSinceLastExposureValue == 7) { + return daysSinceLastExposureScores[4]; + } else if (daysSinceLastExposureValue == 4 || daysSinceLastExposureValue == 5) { + return daysSinceLastExposureScores[5]; + } else if (daysSinceLastExposureValue == 2 || daysSinceLastExposureValue == 3) { + return daysSinceLastExposureScores[6]; + } else { + return daysSinceLastExposureScores[7]; + } + } + + /** + * @param durationValue the duration of an exposure, in minutes + * @return the duration score for the provided value + * @see #durationScores + */ + public int getDurationScore(final int durationValue) { + if (durationValue <= 0) { // using <= instead of == just to be sure + return durationScores[0]; + } else if (durationValue <= 5) { + return durationScores[1]; + } else if (durationValue <= 10) { + return durationScores[2]; + } else if (durationValue <= 15) { + return durationScores[3]; + } else if (durationValue <= 20) { + return durationScores[4]; + } else if (durationValue <= 25) { + return durationScores[5]; + } else if (durationValue <= 30) { + return durationScores[6]; + } else { + return durationScores[7]; + } + } + + /** + * @param transmissionRiskValue the user defined risk value associated to an exposure, + * must be >= 0 and <= 7 + * @return the transmission risk score for the provided value + * @see #transmissionRiskScores + */ + public int getTransmissionRiskScore(final int transmissionRiskValue) { + return transmissionRiskScores[transmissionRiskValue]; + } + + /** + * @param attenuationValue the bluetooth attenuation value from an exposure, in dB: + * {@code transmission power - } {@link CapturedData#getRssi()} + * @param daysSinceLastExposureValue the number of days since the last exposure + * @param durationValue the duration of an exposure, in minutes + * @param transmissionRiskValue the user defined risk value associated to an exposure, + * must be >= 0 and <= 7 + * @return the final risk score calculated by multiplying together the intermediary risk scores + * associated with the provided values + * @see risk calculation algorithm details on developer.apple.com + */ + public int getRiskScore(final int attenuationValue, + final int daysSinceLastExposureValue, + final int durationValue, + final int transmissionRiskValue) { + return getAttenuationScore(attenuationValue) + * getDaysSinceLastExposureScore(daysSinceLastExposureValue) + * getDurationScore(durationValue) + * getTransmissionRiskScore(transmissionRiskValue); + } public static class ExposureConfigurationBuilder { @@ -26,97 +256,119 @@ public static class ExposureConfigurationBuilder { // ApplicationConfigurationService // https://github.com/corona-warn-app/cwa-app-android/blob/master/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/service/applicationconfiguration/ApplicationConfigurationService.kt - private ExposureConfiguration builtObject = new ExposureConfiguration(); + + private int minimumRiskScore = 0; + private int[] attenuationScores = null; + private int[] daysSinceLastExposureScores = null; + private int[] durationScores = null; + private int[] transmissionRiskScores = null; + private final int[] durationAtAttenuationThresholds = new int[] {50, 70}; + public ExposureConfigurationBuilder() {} - public ExposureConfigurationBuilder setTransmissionRiskScores( - int val0, int val1, int val2, int val3, int val4, int val5, int val6, int val7 - ) { - builtObject.transmissionRiskScores = new int[8]; - builtObject.transmissionRiskScores[0] = val0; - builtObject.transmissionRiskScores[1] = val1; - builtObject.transmissionRiskScores[2] = val2; - builtObject.transmissionRiskScores[3] = val3; - builtObject.transmissionRiskScores[4] = val4; - builtObject.transmissionRiskScores[5] = val5; - builtObject.transmissionRiskScores[6] = val6; - builtObject.transmissionRiskScores[7] = val7; + /** + * if not called defaults to 0 + * @see ExposureConfiguration#minimumRiskScore + * @return {@code this} + */ + public ExposureConfigurationBuilder setMinimumRiskScore(int val) { + minimumRiskScore = val; return this; } - - public ExposureConfigurationBuilder setDurationScores( - int val0, int val1, int val2, int val3, int val4, int val5, int val6, int val7 - ) { - builtObject.durationScores = new int[8]; - builtObject.durationScores[0] = val0; - builtObject.durationScores[1] = val1; - builtObject.durationScores[2] = val2; - builtObject.durationScores[3] = val3; - builtObject.durationScores[4] = val4; - builtObject.durationScores[5] = val5; - builtObject.durationScores[6] = val6; - builtObject.durationScores[7] = val7; + /** + * @see ExposureConfiguration#attenuationScores + * @return {@code this} + */ + public ExposureConfigurationBuilder setAttenuationScores( + int val0, int val1, int val2, int val3, int val4, int val5, int val6, int val7) { + attenuationScores = new int[8]; + attenuationScores[0] = val0; + attenuationScores[1] = val1; + attenuationScores[2] = val2; + attenuationScores[3] = val3; + attenuationScores[4] = val4; + attenuationScores[5] = val5; + attenuationScores[6] = val6; + attenuationScores[7] = val7; return this; } - + /** + * @see ExposureConfiguration#daysSinceLastExposureScores + * @return {@code this} + */ public ExposureConfigurationBuilder setDaysSinceLastExposureScores( - int val0, int val1, int val2, int val3, int val4, int val5, int val6, int val7 - ) { - builtObject.daysSinceLastExposureScores = new int[8]; - builtObject.daysSinceLastExposureScores[0] = val0; - builtObject.daysSinceLastExposureScores[1] = val1; - builtObject.daysSinceLastExposureScores[2] = val2; - builtObject.daysSinceLastExposureScores[3] = val3; - builtObject.daysSinceLastExposureScores[4] = val4; - builtObject.daysSinceLastExposureScores[5] = val5; - builtObject.daysSinceLastExposureScores[6] = val6; - builtObject.daysSinceLastExposureScores[7] = val7; + int val0, int val1, int val2, int val3, int val4, int val5, int val6, int val7) { + daysSinceLastExposureScores = new int[8]; + daysSinceLastExposureScores[0] = val0; + daysSinceLastExposureScores[1] = val1; + daysSinceLastExposureScores[2] = val2; + daysSinceLastExposureScores[3] = val3; + daysSinceLastExposureScores[4] = val4; + daysSinceLastExposureScores[5] = val5; + daysSinceLastExposureScores[6] = val6; + daysSinceLastExposureScores[7] = val7; return this; } - - public ExposureConfigurationBuilder setAttenuationScores( - int val0, int val1, int val2, int val3, int val4, int val5, int val6, int val7 - ) { - builtObject.attenuationScores = new int[8]; - builtObject.attenuationScores[0] = val0; - builtObject.attenuationScores[1] = val1; - builtObject.attenuationScores[2] = val2; - builtObject.attenuationScores[3] = val3; - builtObject.attenuationScores[4] = val4; - builtObject.attenuationScores[5] = val5; - builtObject.attenuationScores[6] = val6; - builtObject.attenuationScores[7] = val7; + /** + * @see ExposureConfiguration#durationScores + * @return {@code this} + */ + public ExposureConfigurationBuilder setDurationScores( + int val0, int val1, int val2, int val3, int val4, int val5, int val6, int val7) { + durationScores = new int[8]; + durationScores[0] = val0; + durationScores[1] = val1; + durationScores[2] = val2; + durationScores[3] = val3; + durationScores[4] = val4; + durationScores[5] = val5; + durationScores[6] = val6; + durationScores[7] = val7; return this; } - - public ExposureConfigurationBuilder setMinimumRiskScore( - int val - ) { - builtObject.minimumRiskScore = val; + /** + * @see ExposureConfiguration#transmissionRiskScores + * @return {@code this} + */ + public ExposureConfigurationBuilder setTransmissionRiskScores( + int val0, int val1, int val2, int val3, int val4, int val5, int val6, int val7) { + transmissionRiskScores = new int[8]; + transmissionRiskScores[0] = val0; + transmissionRiskScores[1] = val1; + transmissionRiskScores[2] = val2; + transmissionRiskScores[3] = val3; + transmissionRiskScores[4] = val4; + transmissionRiskScores[5] = val5; + transmissionRiskScores[6] = val6; + transmissionRiskScores[7] = val7; return this; } - + /** + * if not called defaults to {50, 70} + * @see ExposureConfiguration#durationAtAttenuationThresholds + * @return {@code this} + */ public ExposureConfigurationBuilder setDurationAtAttenuationThresholds( - int val0, int val1 - ) { - builtObject.durationAtAttenuationThresholds = new int[2]; - builtObject.durationAtAttenuationThresholds[0] = val0; - builtObject.durationAtAttenuationThresholds[1] = val1; + int lowThreshold, int highThreshold) { + durationAtAttenuationThresholds[0] = lowThreshold; + durationAtAttenuationThresholds[1] = highThreshold; return this; } + /** + * @return an {@link ExposureConfiguration} instance based on the values set in the builder + */ public ExposureConfiguration build() { - return builtObject; + return new ExposureConfiguration(minimumRiskScore,attenuationScores, + daysSinceLastExposureScores, durationScores, transmissionRiskScores, + durationAtAttenuationThresholds); } - } - - } diff --git a/coralibre-sdk/sdk/src/main/java/org/coralibre/android/sdk/fakegms/nearby/exposurenotification/ExposureNotificationClient.java b/coralibre-sdk/sdk/src/main/java/org/coralibre/android/sdk/fakegms/nearby/exposurenotification/ExposureNotificationClient.java index 3952eae..d14ce0a 100644 --- a/coralibre-sdk/sdk/src/main/java/org/coralibre/android/sdk/fakegms/nearby/exposurenotification/ExposureNotificationClient.java +++ b/coralibre-sdk/sdk/src/main/java/org/coralibre/android/sdk/fakegms/nearby/exposurenotification/ExposureNotificationClient.java @@ -1,11 +1,5 @@ package org.coralibre.android.sdk.fakegms.nearby.exposurenotification; -import android.content.Context; - -import org.coralibre.android.sdk.internal.crypto.ppcp.TemporaryExposureKey; - -import java.util.List; - public class ExposureNotificationClient { // diff --git a/coralibre-sdk/sdk/src/main/java/org/coralibre/android/sdk/fakegms/nearby/exposurenotification/ExposureSummary.java b/coralibre-sdk/sdk/src/main/java/org/coralibre/android/sdk/fakegms/nearby/exposurenotification/ExposureSummary.java index 0bbc9d4..cd21bcd 100644 --- a/coralibre-sdk/sdk/src/main/java/org/coralibre/android/sdk/fakegms/nearby/exposurenotification/ExposureSummary.java +++ b/coralibre-sdk/sdk/src/main/java/org/coralibre/android/sdk/fakegms/nearby/exposurenotification/ExposureSummary.java @@ -10,17 +10,46 @@ public class ExposureSummary { // https://github.com/corona-warn-app/cwa-app-android/blob/master/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/InternalExposureNotificationClient.kt // + // using -1 values as default to indicate missing value for debugging purposes - // Fill with -1 to make debugging easier: + /** + * The number of days since the last exposure (i.e. the last collected key resulting positive) + * @see documentation on developers.google.com + */ public int daysSinceLastExposure = -1; - public int matchedKeyCount = -1; - public int summationRiskScore = -1; - // this value is not documented in the API docs, but used inside: - // de/rki/coronawarnapp/storage/ExposureSummaryRepositoryTest.kt + /** + * The number of matched positive keys + * @see documentation on developers.google.com + */ + public int matchedKeyCount = -1; + /** + * The maximum risk score obained from all exposures + * @see used inside de/rki/coronawarnapp/storage/ExposureSummaryRepositoryTest.kt + * @see documentation on developers.google.com + */ public int maximumRiskScore = -1; - public int[] attenuationDurationsInMinutes = null; + + /** + * Contains the cumulative duration in minutes the user spent in the three ranges of risk + * defined by {@link ExposureConfiguration#durationAtAttenuationThresholds}. In particular:
+ * attenuationDurations[0] | sum of durations of exposures with an attenuation less than the + * low threshold (i.e. {@link ExposureConfiguration#durationAtAttenuationThresholds}[0])
+ * attenuationDurations[1] | sum of durations of exposures with an attenuation greater than + * equal to the low threshold (i.e. {@link ExposureConfiguration#durationAtAttenuationThresholds}[0]) + * but less than the high threshold (i.e. {@link ExposureConfiguration#durationAtAttenuationThresholds}[1])
+ * attenuationDurations[2] | sum of durations of exposures with an attenuation greater than + * or equal to the high threshold (i.e. {@link ExposureConfiguration#durationAtAttenuationThresholds}[1])
+ * @see documentation on developers.google.com + */ + public int[] attenuationDurations = null; + + /** + * The sum of risk scores of all exposures + * @see documentation on developers.google.com + */ + public int summationRiskScore = -1; // TODO Implement further // For that see: @@ -48,7 +77,7 @@ public ExposureSummary.ExposureSummaryBuilder setMaximumRiskScore( public ExposureSummary.ExposureSummaryBuilder setAttenuationDurations( int[] vals ) { - builtObject.attenuationDurationsInMinutes = vals; + builtObject.attenuationDurations = vals; return this; } diff --git a/coralibre-sdk/sdk/src/test/java/org/coralibre/android/sdk/fakegms/nearby/exposurenotification/RiskScoreAlgorithmTest.java b/coralibre-sdk/sdk/src/test/java/org/coralibre/android/sdk/fakegms/nearby/exposurenotification/RiskScoreAlgorithmTest.java new file mode 100644 index 0000000..18128e1 --- /dev/null +++ b/coralibre-sdk/sdk/src/test/java/org/coralibre/android/sdk/fakegms/nearby/exposurenotification/RiskScoreAlgorithmTest.java @@ -0,0 +1,22 @@ +package org.coralibre.android.sdk.fakegms.nearby.exposurenotification; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +public class RiskScoreAlgorithmTest { + + @Test + public void appleExample() { + // taken from https://developer.apple.com/documentation/exposurenotification/enexposureconfiguration + final ExposureConfiguration exposureConfiguration = + new ExposureConfiguration.ExposureConfigurationBuilder() + .setTransmissionRiskScores( 0, 0, 0, 0, 0, 7, 0, 0) + .setDurationScores( 1, 1, 4, 7, 7, 8, 8, 8) + .setDaysSinceLastExposureScores(1, 2, 2, 4, 6, 8, 8, 8) + .setAttenuationScores( 1, 1, 1, 8, 8, 8, 8, 8) + .build(); + + assertEquals(392, exposureConfiguration.getRiskScore(68, 4, 14, 5)); + } +} \ No newline at end of file