Skip to content
Snippets Groups Projects
Commit b5e38763 authored by Matthias Urhahn's avatar Matthias Urhahn
Browse files

Merge branch 'release/1.15.x' into chore/DEV-merge-1.5.x-into-2.0.x

# Conflicts:
#	Corona-Warn-App/src/main/res/values/strings.xml
#	gradle.properties
parents d7306206 12f453c9
No related branches found
No related tags found
No related merge requests found
Showing
with 289 additions and 131 deletions
......@@ -30,7 +30,7 @@ import de.rki.coronawarnapp.tracing.ui.statusbar.TracingHeaderState
import de.rki.coronawarnapp.ui.main.home.items.FAQCard
import de.rki.coronawarnapp.ui.main.home.items.HomeItem
import de.rki.coronawarnapp.ui.statistics.Statistics
import de.rki.coronawarnapp.util.security.EncryptionErrorResetTool
import de.rki.coronawarnapp.util.encryptionmigration.EncryptionErrorResetTool
import de.rki.coronawarnapp.util.shortcuts.AppShortcutsHelper
import de.rki.coronawarnapp.util.ui.SingleLiveEvent
import io.mockk.MockKAnnotations
......
......@@ -21,9 +21,8 @@ import de.rki.coronawarnapp.exception.reporting.ErrorReportReceiver
import de.rki.coronawarnapp.exception.reporting.ReportingConstants.ERROR_REPORT_LOCAL_BROADCAST_CHANNEL
import de.rki.coronawarnapp.notification.NotificationHelper
import de.rki.coronawarnapp.risk.RiskLevelChangeDetector
import de.rki.coronawarnapp.submission.SubmissionSettings
import de.rki.coronawarnapp.storage.OnboardingSettings
import de.rki.coronawarnapp.storage.preferences.EncryptedPreferencesMigration
import de.rki.coronawarnapp.submission.SubmissionSettings
import de.rki.coronawarnapp.submission.auto.AutoSubmission
import de.rki.coronawarnapp.task.TaskController
import de.rki.coronawarnapp.util.CWADebug
......@@ -62,7 +61,6 @@ class CoronaWarnApplication : Application(), HasAndroidInjector {
@Inject lateinit var autoSubmission: AutoSubmission
@Inject lateinit var submissionSettings: SubmissionSettings
@Inject lateinit var onboardingSettings: OnboardingSettings
@Inject lateinit var encryptedPreferencesMigration: EncryptedPreferencesMigration
@LogHistoryTree @Inject lateinit var rollingLogHistory: Timber.Tree
......@@ -71,12 +69,15 @@ class CoronaWarnApplication : Application(), HasAndroidInjector {
super.onCreate()
CWADebug.init(this)
Timber.v("onCreate(): Initializing Dagger")
AppInjector.init(this)
AppInjector.init(this).let { compPreview ->
Timber.v("Calling EncryptedPreferencesMigration.doMigration()")
compPreview.encryptedMigration.doMigration()
CWADebug.initAfterInjection(component)
CWADebug.initAfterInjection(compPreview)
encryptedPreferencesMigration.doMigration()
Timber.v("Completing application injection")
compPreview.inject(this)
}
BackgroundWorkScheduler.init(component)
......
......@@ -22,7 +22,11 @@ interface ContactDiaryRepository {
val locationVisits: Flow<List<ContactDiaryLocationVisit>>
fun locationVisitsForDate(date: LocalDate): Flow<List<ContactDiaryLocationVisit>>
suspend fun addLocationVisit(contactDiaryLocationVisit: ContactDiaryLocationVisit)
suspend fun updateLocationVisit(contactDiaryLocationVisit: ContactDiaryLocationVisit)
suspend fun updateLocationVisit(
visitId: Long,
update: (ContactDiaryLocationVisit) -> ContactDiaryLocationVisit
)
suspend fun deleteLocationVisit(contactDiaryLocationVisit: ContactDiaryLocationVisit)
suspend fun deleteLocationVisits(contactDiaryLocationVisits: List<ContactDiaryLocationVisit>)
suspend fun deleteAllLocationVisits()
......@@ -39,7 +43,11 @@ interface ContactDiaryRepository {
val personEncounters: Flow<List<ContactDiaryPersonEncounter>>
fun personEncountersForDate(date: LocalDate): Flow<List<ContactDiaryPersonEncounter>>
suspend fun addPersonEncounter(contactDiaryPersonEncounter: ContactDiaryPersonEncounter)
suspend fun updatePersonEncounter(contactDiaryPersonEncounter: ContactDiaryPersonEncounter)
suspend fun updatePersonEncounter(
encounterId: Long,
update: (ContactDiaryPersonEncounter) -> ContactDiaryPersonEncounter
)
suspend fun deletePersonEncounter(contactDiaryPersonEncounter: ContactDiaryPersonEncounter)
suspend fun deletePersonEncounters(contactDiaryPersonEncounters: List<ContactDiaryPersonEncounter>)
suspend fun deleteAllPersonEncounters()
......
......@@ -11,8 +11,10 @@ import de.rki.coronawarnapp.contactdiary.storage.dao.ContactDiaryLocationVisitDa
import de.rki.coronawarnapp.contactdiary.storage.dao.ContactDiaryPersonDao
import de.rki.coronawarnapp.contactdiary.storage.dao.ContactDiaryPersonEncounterDao
import de.rki.coronawarnapp.contactdiary.storage.entity.toContactDiaryLocationEntity
import de.rki.coronawarnapp.contactdiary.storage.entity.toContactDiaryLocationVisit
import de.rki.coronawarnapp.contactdiary.storage.entity.toContactDiaryLocationVisitEntity
import de.rki.coronawarnapp.contactdiary.storage.entity.toContactDiaryLocationVisitSortedList
import de.rki.coronawarnapp.contactdiary.storage.entity.toContactDiaryPersonEncounter
import de.rki.coronawarnapp.contactdiary.storage.entity.toContactDiaryPersonEncounterEntity
import de.rki.coronawarnapp.contactdiary.storage.entity.toContactDiaryPersonEncounterSortedList
import de.rki.coronawarnapp.contactdiary.storage.entity.toContactDiaryPersonEntity
......@@ -108,10 +110,14 @@ class DefaultContactDiaryRepository @Inject constructor(
contactDiaryLocationVisitDao.insert(contactDiaryLocationVisitEntity)
}
override suspend fun updateLocationVisit(contactDiaryLocationVisit: ContactDiaryLocationVisit) {
executeWhenIdNotDefault(contactDiaryLocationVisit.id) {
val contactDiaryLocationVisitEntity = contactDiaryLocationVisit.toContactDiaryLocationVisitEntity()
contactDiaryLocationVisitDao.update(contactDiaryLocationVisitEntity)
override suspend fun updateLocationVisit(
visitId: Long,
update: (ContactDiaryLocationVisit) -> ContactDiaryLocationVisit
) {
executeWhenIdNotDefault(visitId) {
val original = contactDiaryLocationVisitDao.entityForId(visitId)
val updatedVisit = update(original.toContactDiaryLocationVisit())
contactDiaryLocationVisitDao.update(updatedVisit.toContactDiaryLocationVisitEntity())
}
}
......@@ -202,10 +208,14 @@ class DefaultContactDiaryRepository @Inject constructor(
contactDiaryPersonEncounterDao.insert(contactDiaryPersonEncounterEntity)
}
override suspend fun updatePersonEncounter(contactDiaryPersonEncounter: ContactDiaryPersonEncounter) {
executeWhenIdNotDefault(contactDiaryPersonEncounter.id) {
val contactDiaryPersonEncounterEntity = contactDiaryPersonEncounter.toContactDiaryPersonEncounterEntity()
contactDiaryPersonEncounterDao.update(contactDiaryPersonEncounterEntity)
override suspend fun updatePersonEncounter(
encounterId: Long,
update: (ContactDiaryPersonEncounter) -> ContactDiaryPersonEncounter
) {
executeWhenIdNotDefault(encounterId) {
val original = contactDiaryPersonEncounterDao.entityForId(encounterId)
val updatedEncounter = update(original.toContactDiaryPersonEncounter())
contactDiaryPersonEncounterDao.update(updatedEncounter.toContactDiaryPersonEncounterEntity())
}
}
......
......@@ -99,9 +99,11 @@ class ContactDiaryLocationListViewModel @AssistedInject constructor(
item: DiaryLocationListItem,
duration: Duration?
) {
val visit = item.visit?.toEditableVariant() ?: return
val visit = item.visit ?: return
launchOnAppScope {
contactDiaryRepository.updateLocationVisit(visit.copy(duration = duration))
contactDiaryRepository.updateLocationVisit(visit.id) {
it.toEditableVariant().copy(duration = duration)
}
}
}
......@@ -109,10 +111,12 @@ class ContactDiaryLocationListViewModel @AssistedInject constructor(
item: DiaryLocationListItem,
circumstances: String
) {
val visit = item.visit?.toEditableVariant() ?: return
val visit = item.visit ?: return
val sanitized = circumstances.trim().trimToLength(250)
launchOnAppScope {
contactDiaryRepository.updateLocationVisit(visit.copy(circumstances = sanitized))
contactDiaryRepository.updateLocationVisit(visit.id) {
it.toEditableVariant().copy(circumstances = sanitized)
}
}
}
......
......@@ -95,9 +95,11 @@ class ContactDiaryPersonListViewModel @AssistedInject constructor(
duration: ContactDiaryPersonEncounter.DurationClassification?
) {
Timber.d("onDurationChanged(item=%s, duration=%s)", item, duration)
val encounter = item.personEncounter?.toEditableVariant() ?: return
val encounter = item.personEncounter ?: return
launchOnAppScope {
contactDiaryRepository.updatePersonEncounter(encounter.copy(durationClassification = duration))
contactDiaryRepository.updatePersonEncounter(encounter.id) {
it.toEditableVariant().copy(durationClassification = duration)
}
}
}
......@@ -112,9 +114,11 @@ class ContactDiaryPersonListViewModel @AssistedInject constructor(
withMask: Boolean?
) {
Timber.d("onWithmaskChanged(item=%s, withMask=%s)", item, withMask)
val encounter = item.personEncounter?.toEditableVariant() ?: return
val encounter = item.personEncounter ?: return
launchOnAppScope {
contactDiaryRepository.updatePersonEncounter(encounter.copy(withMask = withMask))
contactDiaryRepository.updatePersonEncounter(encounter.id) {
it.toEditableVariant().copy(withMask = withMask)
}
}
}
......@@ -123,9 +127,11 @@ class ContactDiaryPersonListViewModel @AssistedInject constructor(
wasOutside: Boolean?
) {
Timber.d("onWasOutsideChanged(item=%s, onWasOutside=%s)", item, wasOutside)
val encounter = item.personEncounter?.toEditableVariant() ?: return
val encounter = item.personEncounter ?: return
launchOnAppScope {
contactDiaryRepository.updatePersonEncounter(encounter.copy(wasOutside = wasOutside))
contactDiaryRepository.updatePersonEncounter(encounter.id) {
it.toEditableVariant().copy(wasOutside = wasOutside)
}
}
}
......@@ -134,10 +140,12 @@ class ContactDiaryPersonListViewModel @AssistedInject constructor(
circumstances: String
) {
Timber.d("onCircumstancesChanged(item=%s, circumstances=%s)", item, circumstances)
val encounter = item.personEncounter?.toEditableVariant() ?: return
val encounter = item.personEncounter ?: return
launchOnAppScope {
val sanitized = circumstances.trim().trimToLength(250)
contactDiaryRepository.updatePersonEncounter(encounter.copy(circumstances = sanitized))
contactDiaryRepository.updatePersonEncounter(encounter.id) {
it.toEditableVariant().copy(circumstances = sanitized)
}
}
}
......
......@@ -53,7 +53,7 @@ class DayDataNestedAdapter : BaseAdapter<DayDataNestedAdapter.NestedItemViewHold
mutableListOf<String>().apply {
duration?.run {
if (duration != Duration.ZERO) {
val durationSuffix = context.getString(R.string.contact_diary_overview_location_duration_suffix)
val durationSuffix = context.getString(R.string.contact_diary_location_visit_duration_hour)
add(toReadableDuration(suffix = durationSuffix))
}
}
......
......@@ -8,8 +8,9 @@ fun calculateDaysSinceMostRecentDateAtRiskLevelAtTestRegistration(
lastChangeCheckedRiskLevelTimestamp: Instant?,
testRegisteredAt: Instant?
): Int {
val lastChangeCheckedRiskLevelDate = lastChangeCheckedRiskLevelTimestamp?.toLocalDate() ?: return 0
val testRegisteredAtDate = testRegisteredAt?.toLocalDate() ?: return 0
val lastChangeCheckedRiskLevelDate = lastChangeCheckedRiskLevelTimestamp?.toLocalDate() ?: return -1
val testRegisteredAtDate = testRegisteredAt?.toLocalDate() ?: return -1
if (lastChangeCheckedRiskLevelDate.isAfter(testRegisteredAtDate)) return -1
return Days.daysBetween(
lastChangeCheckedRiskLevelDate,
testRegisteredAtDate
......
package de.rki.coronawarnapp.datadonation.analytics.modules.keysubmission
import de.rki.coronawarnapp.datadonation.analytics.common.calculateDaysSinceMostRecentDateAtRiskLevelAtTestRegistration
import de.rki.coronawarnapp.datadonation.analytics.common.toMetadataRiskLevel
import de.rki.coronawarnapp.datadonation.analytics.storage.AnalyticsSettings
import de.rki.coronawarnapp.risk.RiskLevelSettings
......@@ -54,6 +55,13 @@ class AnalyticsKeySubmissionCollector @Inject constructor(
}
}
}
analyticsKeySubmissionStorage.daysSinceMostRecentDateAtRiskLevelAtTestRegistration.update {
calculateDaysSinceMostRecentDateAtRiskLevelAtTestRegistration(
riskLevelSettings.lastChangeCheckedRiskLevelTimestamp,
testRegisteredAt
)
}
}
fun reportSubmitted() {
......
package de.rki.coronawarnapp.datadonation.analytics.modules.keysubmission
import de.rki.coronawarnapp.datadonation.analytics.common.calculateDaysSinceMostRecentDateAtRiskLevelAtTestRegistration
import de.rki.coronawarnapp.risk.RiskLevelSettings
import org.joda.time.Duration
import org.joda.time.Instant
import javax.inject.Inject
import kotlin.math.max
class AnalyticsKeySubmissionRepository @Inject constructor(
private val storage: AnalyticsKeySubmissionStorage,
private val riskLevelSettings: RiskLevelSettings
private val storage: AnalyticsKeySubmissionStorage
) {
val testResultReceivedAt: Long
get() = storage.testResultReceivedAt.value
......@@ -42,16 +37,23 @@ class AnalyticsKeySubmissionRepository @Inject constructor(
get() = storage.advancedConsentGiven.value
val hoursSinceTestResult: Int
get() = Duration.millis(max(submittedAt - testResultReceivedAt, 0)).toStandardHours().hours
get() {
if (submittedAt <= 0) return -1
if (testResultReceivedAt <= 0) return -1
if (submittedAt < testResultReceivedAt) return -1
return Duration.millis(submittedAt - testResultReceivedAt).toStandardHours().hours
}
val hoursSinceTestRegistration: Int
get() = Duration.millis(max(submittedAt - testRegisteredAt, 0L)).toStandardHours().hours
get() {
if (submittedAt <= 0) return -1
if (testRegisteredAt <= 0) return -1
if (submittedAt < testRegisteredAt) return -1
return Duration.millis(submittedAt - testRegisteredAt).toStandardHours().hours
}
val daysSinceMostRecentDateAtRiskLevelAtTestRegistration: Int
get() = calculateDaysSinceMostRecentDateAtRiskLevelAtTestRegistration(
riskLevelSettings.lastChangeCheckedRiskLevelTimestamp,
Instant.ofEpochMilli(testRegisteredAt)
)
get() = storage.daysSinceMostRecentDateAtRiskLevelAtTestRegistration.value
val hoursSinceHighRiskWarningAtTestRegistration: Int
get() = storage.hoursSinceHighRiskWarningAtTestRegistration.value
......
......@@ -73,6 +73,11 @@ class AnalyticsKeySubmissionStorage @Inject constructor(
defaultValue = -1
)
val daysSinceMostRecentDateAtRiskLevelAtTestRegistration = prefs.createFlowPreference(
key = "analytics_key_submission_daysSinceMostRecentDateAtRiskLevelAtTestRegistration",
defaultValue = -1
)
fun clear() {
prefs.clearAndNotify()
}
......
......@@ -3,13 +3,10 @@ package de.rki.coronawarnapp.datadonation.analytics.modules.registeredtest
import de.rki.coronawarnapp.datadonation.analytics.common.calculateDaysSinceMostRecentDateAtRiskLevelAtTestRegistration
import de.rki.coronawarnapp.datadonation.analytics.modules.DonorModule
import de.rki.coronawarnapp.datadonation.analytics.storage.TestResultDonorSettings
import de.rki.coronawarnapp.risk.RiskLevelSettings
import de.rki.coronawarnapp.risk.storage.RiskLevelStorage
import de.rki.coronawarnapp.server.protocols.internal.ppdd.PpaData
import de.rki.coronawarnapp.submission.SubmissionSettings
import de.rki.coronawarnapp.util.TimeStamper
import de.rki.coronawarnapp.util.formatter.TestResult
import kotlinx.coroutines.flow.first
import org.joda.time.Duration
import org.joda.time.Instant
import timber.log.Timber
......@@ -19,8 +16,6 @@ import javax.inject.Singleton
@Singleton
class TestResultDonor @Inject constructor(
private val testResultDonorSettings: TestResultDonorSettings,
private val riskLevelSettings: RiskLevelSettings,
private val riskLevelStorage: RiskLevelStorage,
private val timeStamper: TimeStamper,
private val submissionSettings: SubmissionSettings
) : DonorModule {
......@@ -28,42 +23,66 @@ class TestResultDonor @Inject constructor(
override suspend fun beginDonation(request: DonorModule.Request): DonorModule.Contribution {
val scannedAfterConsent = testResultDonorSettings.testScannedAfterConsent.value
if (!scannedAfterConsent) {
Timber.d("Skipping TestResultMetadata donation (testScannedAfterConsent=%s)", scannedAfterConsent)
Timber.d("Skipping TestResultMetadata donation (scannedAfterConsent=%s)", scannedAfterConsent)
return TestResultMetadataNoContribution
}
val timestampAtRegistration = submissionSettings.initialTestResultReceivedAt
if (timestampAtRegistration == null) {
Timber.d("Skipping TestResultMetadata donation timestampAtRegistration isn't found")
Timber.d("Skipping TestResultMetadata donation (timestampAtRegistration is missing)")
return TestResultMetadataNoContribution
}
val configHours = request
.currentConfig
.analytics
.hoursSinceTestRegistrationToSubmitTestResultMetadata
val hoursSinceTestRegistrationTime = Duration(timestampAtRegistration, timeStamper.nowUTC).standardHours.toInt()
val isDiffHoursMoreThanConfigHoursForPendingTest = hoursSinceTestRegistrationTime >= configHours
val testResultAtRegistration = testResultDonorSettings.testResultAtRegistration.value
if (testResultAtRegistration == null) {
Timber.d("Skipping TestResultMetadata donation (testResultAtRegistration is missing)")
return TestResultMetadataNoContribution
}
val testResultAtRegistration =
testResultDonorSettings.testResultAtRegistration.value ?: return TestResultMetadataNoContribution
val lastChangeCheckedRiskLevelTimestamp = testResultDonorSettings.mostRecentDateWithHighOrLowRiskLevel.value
if (lastChangeCheckedRiskLevelTimestamp == null) {
Timber.d("Skipping TestResultMetadata donation (lastChangeCheckedRiskLevelTimestamp is missing)")
return TestResultMetadataNoContribution
}
val daysSinceMostRecentDateAtRiskLevelAtTestRegistration =
calculateDaysSinceMostRecentDateAtRiskLevelAtTestRegistration(
riskLevelSettings.lastChangeCheckedRiskLevelTimestamp,
lastChangeCheckedRiskLevelTimestamp,
timestampAtRegistration
)
Timber.i(
"daysSinceMostRecentDateAtRiskLevelAtTestRegistration: %s",
daysSinceMostRecentDateAtRiskLevelAtTestRegistration
)
val riskLevelAtRegistration = testResultDonorSettings.riskLevelAtTestRegistration.value
val highRiskResultCalculatedAt = testResultDonorSettings.riskLevelTurnedRedTime.value
val hoursSinceHighRiskWarningAtTestRegistration =
if (riskLevelAtRegistration == PpaData.PPARiskLevel.RISK_LEVEL_LOW) {
DEFAULT_HOURS_SINCE_HIGH_RISK_WARNING
} else {
calculatedHoursSinceHighRiskWarning(timestampAtRegistration)
if (highRiskResultCalculatedAt == null) {
Timber.d("Skipping TestResultMetadata donation (highRiskResultCalculatedAt is missing)")
return TestResultMetadataNoContribution
}
Timber.i(
"highRiskResultCalculatedAt: %s, timestampAtRegistration: %s",
highRiskResultCalculatedAt,
timestampAtRegistration
)
calculatedHoursSinceHighRiskWarning(highRiskResultCalculatedAt, timestampAtRegistration)
}
Timber.i(
"hoursSinceHighRiskWarningAtTestRegistration: %s",
hoursSinceHighRiskWarningAtTestRegistration
)
val configHours = request.currentConfig.analytics.hoursSinceTestRegistrationToSubmitTestResultMetadata
val hoursSinceTestRegistrationTime = Duration(timestampAtRegistration, timeStamper.nowUTC).standardHours.toInt()
val isDiffHoursMoreThanConfigHoursForPendingTest = hoursSinceTestRegistrationTime >= configHours
return when {
/**
......@@ -134,8 +153,13 @@ class TestResultDonor @Inject constructor(
): DonorModule.Contribution {
val finalTestResultReceivedAt = testResultDonorSettings.finalTestResultReceivedAt.value
val hoursSinceTestRegistrationTime = if (finalTestResultReceivedAt != null) {
Duration(registrationTime, finalTestResultReceivedAt).standardHours.toInt()
Timber.i("finalTestResultReceivedAt: %s", finalTestResultReceivedAt)
Timber.i("registrationTime: %s", registrationTime)
Duration(registrationTime, finalTestResultReceivedAt).standardHours.toInt().also {
Timber.i("Calculated hoursSinceTestRegistrationTime: %s", it)
}
} else {
Timber.i("Default hoursSinceTestRegistrationTime")
DEFAULT_HOURS_SINCE_TEST_REGISTRATION_TIME
}
......@@ -153,14 +177,10 @@ class TestResultDonor @Inject constructor(
return TestResultMetadataContribution(testResultMetaData, ::cleanUp)
}
private suspend fun calculatedHoursSinceHighRiskWarning(registrationTime: Instant): Int {
val highRiskResultCalculatedAt = riskLevelStorage
.latestAndLastSuccessful
.first()
.filter { it.isIncreasedRisk }
.minByOrNull { it.calculatedAt }
?.calculatedAt ?: return DEFAULT_HOURS_SINCE_HIGH_RISK_WARNING
private fun calculatedHoursSinceHighRiskWarning(
highRiskResultCalculatedAt: Instant,
registrationTime: Instant
): Int {
return Duration(
highRiskResultCalculatedAt,
registrationTime
......
......@@ -65,6 +65,34 @@ class TestResultDonorSettings @Inject constructor(
}
)
val mostRecentDateWithHighOrLowRiskLevel = prefs.createFlowPreference(
key = PREFS_KEY_MOST_RECENT_WITH_HIGH_OR_LOW_RISK_LEVEL,
reader = { key ->
getLong(key, 0L).let {
if (it != 0L) {
Instant.ofEpochMilli(it)
} else null
}
},
writer = { key, value ->
putLong(key, value?.millis ?: 0L)
}
)
val riskLevelTurnedRedTime = prefs.createFlowPreference(
key = PREFS_KEY_RISK_LEVEL_TURNED_RED_TIME,
reader = { key ->
getLong(key, 0L).let {
if (it != 0L) {
Instant.ofEpochMilli(it)
} else null
}
},
writer = { key, value ->
putLong(key, value?.millis ?: 0L)
}
)
fun saveTestResultDonorDataAtRegistration(testResult: TestResult, lastRiskResult: RiskLevelResult) {
testScannedAfterConsent.update { true }
testResultAtRegistration.update { testResult }
......@@ -82,5 +110,8 @@ class TestResultDonorSettings @Inject constructor(
private const val PREFS_KEY_TEST_RESULT_AT_REGISTRATION = "testResultDonor.testResultAtRegistration"
private const val PREFS_KEY_RISK_LEVEL_AT_REGISTRATION = "testResultDonor.riskLevelAtRegistration"
private const val PREFS_KEY_FINAL_TEST_RESULT_RECEIVED_AT = "testResultDonor.finalTestResultReceivedAt"
private const val PREFS_KEY_RISK_LEVEL_TURNED_RED_TIME = "testResultDonor.riskLevelTurnedRedTime"
private const val PREFS_KEY_MOST_RECENT_WITH_HIGH_OR_LOW_RISK_LEVEL =
"testResultDonor.mostRecentWithHighOrLowRiskLevel"
}
}
......@@ -25,6 +25,10 @@ class CWASettings @Inject constructor(
get() = prefs.getBoolean(PKEY_DEVICE_TIME_INCORRECT_ACK, false)
set(value) = prefs.edit { putBoolean(PKEY_DEVICE_TIME_INCORRECT_ACK, value) }
var wasTracingExplanationDialogShown: Boolean
get() = prefs.getBoolean(PKEY_TRACING_DIALOG_SHOWN, false)
set(value) = prefs.edit { putBoolean(PKEY_TRACING_DIALOG_SHOWN, value) }
var wasInteroperabilityShownAtLeastOnce: Boolean
get() = prefs.getBoolean(PKEY_INTEROPERABILITY_SHOWED_AT_LEAST_ONCE, false)
set(value) = prefs.edit { putBoolean(PKEY_INTEROPERABILITY_SHOWED_AT_LEAST_ONCE, value) }
......@@ -50,12 +54,12 @@ class CWASettings @Inject constructor(
val isNotificationsRiskEnabled = prefs.createFlowPreference(
key = PKEY_NOTIFICATIONS_RISK_ENABLED,
defaultValue = false
defaultValue = true
)
val isNotificationsTestEnabled = prefs.createFlowPreference(
key = PKEY_NOTIFICATIONS_TEST_ENABLED,
defaultValue = false
defaultValue = true
)
val lastChangelogVersion = prefs.createFlowPreference(
......@@ -69,6 +73,7 @@ class CWASettings @Inject constructor(
companion object {
private const val PKEY_DEVICE_TIME_INCORRECT_ACK = "devicetime.incorrect.acknowledged"
private const val PKEY_TRACING_DIALOG_SHOWN = "tracing.dialog.shown"
private const val PKEY_INTEROPERABILITY_SHOWED_AT_LEAST_ONCE = "interoperability.showed"
private const val PKEY_DEVICE_TIME_FIRST_RELIABLE = "devicetime.correct.first"
private const val PKEY_DEVICE_TIME_LAST_STATE_CHANGE_TIME = "devicetime.laststatechange.timestamp"
......
......@@ -4,6 +4,7 @@ import android.content.Context
import androidx.annotation.VisibleForTesting
import androidx.core.app.NotificationManagerCompat
import de.rki.coronawarnapp.R
import de.rki.coronawarnapp.datadonation.analytics.storage.TestResultDonorSettings
import de.rki.coronawarnapp.datadonation.survey.Surveys
import de.rki.coronawarnapp.notification.NotificationConstants.NEW_MESSAGE_RISK_LEVEL_SCORE_NOTIFICATION_ID
import de.rki.coronawarnapp.notification.NotificationHelper
......@@ -34,7 +35,8 @@ class RiskLevelChangeDetector @Inject constructor(
private val notificationHelper: NotificationHelper,
private val surveys: Surveys,
private val submissionSettings: SubmissionSettings,
private val tracingSettings: TracingSettings
private val tracingSettings: TracingSettings,
private val testResultDonorSettings: TestResultDonorSettings
) {
fun launch() {
......@@ -65,9 +67,54 @@ class RiskLevelChangeDetector @Inject constructor(
val oldRiskState = oldResult.riskState
val newRiskState = newResult.riskState
Timber.d("Last state was $oldRiskState and current state is $newRiskState")
// Check sending a notification when risk level changes
checkSendingNotification(oldRiskState, newRiskState)
// Save Survey related data based on the risk state
saveSurveyRiskState(oldRiskState, newRiskState, newResult)
// Save TestDonor risk level timestamps
saveTestDonorRiskLevelAnalytics(newResult)
}
private fun saveTestDonorRiskLevelAnalytics(
newRiskState: RiskLevelResult
) {
// Save riskLevelTurnedRedTime if not already set before for high risk detection
Timber.i("riskLevelTurnedRedTime=%s", testResultDonorSettings.riskLevelTurnedRedTime.value)
if (testResultDonorSettings.riskLevelTurnedRedTime.value == null) {
if (newRiskState.isIncreasedRisk) {
testResultDonorSettings.riskLevelTurnedRedTime.update {
newRiskState.calculatedAt
}
Timber.i(
"riskLevelTurnedRedTime: newRiskState=%s, riskLevelTurnedRedTime=%s",
newRiskState.riskState,
newRiskState.calculatedAt
)
}
}
// Save most recent date of high or low risks
if (newRiskState.riskState in listOf(RiskState.INCREASED_RISK, RiskState.LOW_RISK)) {
Timber.i(
"mostRecentDateWithHighOrLowRiskLevel: newRiskState=%s, lastRiskEncounterAt=%s",
newRiskState.riskState,
newRiskState.lastRiskEncounterAt
)
testResultDonorSettings.mostRecentDateWithHighOrLowRiskLevel.update {
newRiskState.lastRiskEncounterAt
}
}
}
private suspend fun checkSendingNotification(
oldRiskState: RiskState,
newRiskState: RiskState
) {
if (hasHighLowLevelChanged(oldRiskState, newRiskState) && !submissionSettings.isSubmissionSuccessful) {
Timber.d("Notification Permission = ${notificationManagerCompat.areNotificationsEnabled()}")
......@@ -82,7 +129,13 @@ class RiskLevelChangeDetector @Inject constructor(
Timber.d("Risk level changed and notification sent. Current Risk level is $newRiskState")
}
}
private fun saveSurveyRiskState(
oldRiskState: RiskState,
newRiskState: RiskState,
newResult: RiskLevelResult
) {
if (oldRiskState == RiskState.INCREASED_RISK && newRiskState == RiskState.LOW_RISK) {
tracingSettings.isUserToBeNotifiedOfLoweredRiskLevel.update { true }
Timber.d("Risk level changed LocalData is updated. Current Risk level is $newRiskState")
......
......@@ -83,8 +83,8 @@ class RiskLevelTask @Inject constructor(
Timber.d("The current time is %s", it)
}
if (submissionSettings.isAllowedToSubmitKeys) {
Timber.i("Positive test result, skip risk calculation")
if (submissionSettings.isAllowedToSubmitKeys && submissionSettings.hasViewedTestResult.value) {
Timber.i("Positive test result and user has seen it, skip risk calculation")
return RiskLevelTaskResult(
calculatedAt = nowUTC,
failureReason = FailureReason.POSITIVE_TEST_RESULT
......
package de.rki.coronawarnapp.storage.preferences
import android.content.SharedPreferences
import android.content.pm.ApplicationInfo
import androidx.core.content.edit
import de.rki.coronawarnapp.exception.CwaSecurityException
import de.rki.coronawarnapp.util.security.EncryptedPreferencesFactory
import de.rki.coronawarnapp.util.security.EncryptionErrorResetTool
import de.rki.coronawarnapp.util.security.SecurityConstants
import java.io.File
import javax.inject.Inject
class EncryptedPreferencesHelper @Inject constructor(
private val applicationInfo: ApplicationInfo,
factory: EncryptedPreferencesFactory,
encryptionErrorResetTool: EncryptionErrorResetTool
) {
private val encryptedPreferencesFile by lazy {
File(applicationInfo.dataDir)
.resolve("shared_prefs/${SecurityConstants.ENCRYPTED_SHARED_PREFERENCES_FILE}.xml")
}
val encryptedSharedPreferencesInstance: SharedPreferences? by lazy {
withSecurityCatch {
try {
if (encryptedPreferencesFile.exists()) {
factory.create(SecurityConstants.ENCRYPTED_SHARED_PREFERENCES_FILE)
} else {
null
}
} catch (e: Exception) {
encryptionErrorResetTool.isResetNoticeToBeShown = true
null
}
}
}
fun clean() {
encryptedSharedPreferencesInstance?.edit(true) {
clear()
}
encryptedPreferencesFile.delete()
}
private fun <T> withSecurityCatch(doInCatch: () -> T) = try {
doInCatch.invoke()
} catch (e: Exception) {
throw CwaSecurityException(e)
}
}
......@@ -124,6 +124,7 @@ class SubmissionRepository @Inject constructor(
}
suspend fun asyncRegisterDeviceViaTAN(tan: String) {
analyticsKeySubmissionCollector.reset()
val registrationData = submissionService.asyncRegisterDeviceViaTAN(tan)
submissionSettings.registrationToken.update {
registrationData.registrationToken
......@@ -136,6 +137,7 @@ class SubmissionRepository @Inject constructor(
}
suspend fun asyncRegisterDeviceViaGUID(guid: String): TestResult {
analyticsKeySubmissionCollector.reset()
val registrationData = submissionService.asyncRegisterDeviceViaGUID(guid)
submissionSettings.registrationToken.update {
registrationData.registrationToken
......@@ -163,6 +165,16 @@ class SubmissionRepository @Inject constructor(
deadmanNotificationScheduler.cancelScheduledWork()
}
// https://jira-ibs.wbs.net.sap/browse/EXPOSUREAPP-4484
// User removed a test before 1.11 where due to a bug the timestamp was not removed.
if (submissionSettings.initialTestResultReceivedAt != null &&
submissionSettings.registrationToken.value != null &&
submissionSettings.devicePairingSuccessfulAt == null
) {
Timber.tag(TAG).w("User has stale initialTestResultReceivedAt, fixing EXPOSUREAPP-4484.")
submissionSettings.initialTestResultReceivedAt = null
}
val initialTestResultReceivedTimestamp = submissionSettings.initialTestResultReceivedAt
if (initialTestResultReceivedTimestamp == null) {
......@@ -188,7 +200,6 @@ class SubmissionRepository @Inject constructor(
fun removeTestFromDevice() {
submissionSettings.hasViewedTestResult.update { false }
submissionSettings.hasGivenConsent.update { false }
analyticsKeySubmissionCollector.reset()
revokeConsentToSubmission()
submissionSettings.registrationToken.update { null }
submissionSettings.devicePairingSuccessfulAt = null
......
package de.rki.coronawarnapp.tracing.ui
import android.content.Context
import de.rki.coronawarnapp.R
import de.rki.coronawarnapp.ui.main.home.HomeFragment
import de.rki.coronawarnapp.util.DialogHelper
import javax.inject.Inject
class TracingExplanationDialog @Inject constructor(
private val homeFragment: HomeFragment
) {
private val context: Context
get() = homeFragment.requireContext()
fun show(onPositive: () -> Unit) {
val infoPeriodLogged =
context.getString(R.string.risk_details_information_body_period_logged)
val infoPeriodLoggedAssessment =
context.getString(R.string.risk_details_information_body_period_logged_assessment)
val infoFAQ = context.getString(R.string.risk_details_explanation_dialog_faq_body)
val data = DialogHelper.DialogInstance(
context = context,
title = context.getString(R.string.risk_details_explanation_dialog_title),
message = "$infoPeriodLogged\n$infoPeriodLoggedAssessment\n\n$infoFAQ",
positiveButton = context.getString(R.string.errors_generic_button_positive),
negativeButton = null,
cancelable = null,
positiveButtonFunction = onPositive,
negativeButtonFunction = {}
)
DialogHelper.showDialog(data)
}
}
......@@ -9,6 +9,7 @@ import androidx.recyclerview.widget.DefaultItemAnimator
import androidx.recyclerview.widget.LinearLayoutManager
import de.rki.coronawarnapp.R
import de.rki.coronawarnapp.databinding.HomeFragmentLayoutBinding
import de.rki.coronawarnapp.tracing.ui.TracingExplanationDialog
import de.rki.coronawarnapp.ui.main.home.popups.DeviceTimeIncorrectDialog
import de.rki.coronawarnapp.util.ContextExtensions.getColorCompat
import de.rki.coronawarnapp.util.DialogHelper
......@@ -40,6 +41,7 @@ class HomeFragment : Fragment(R.layout.home_fragment_layout), AutoInject {
val binding: HomeFragmentLayoutBinding by viewBindingLazy()
@Inject lateinit var homeMenu: HomeMenu
@Inject lateinit var tracingExplanationDialog: TracingExplanationDialog
@Inject lateinit var deviceTimeIncorrectDialog: DeviceTimeIncorrectDialog
private val homeAdapter = HomeAdapter()
......@@ -91,6 +93,11 @@ class HomeFragment : Fragment(R.layout.home_fragment_layout), AutoInject {
HomeFragmentEvents.ShowReactivateRiskCheckDialog -> {
showReactivateRiskCheckDialog()
}
HomeFragmentEvents.ShowTracingExplanation -> {
tracingExplanationDialog.show {
vm.tracingExplanationWasShown()
}
}
}
}
......
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