From 35f353d292e396ec8eed3584dc9835e5c883c8d5 Mon Sep 17 00:00:00 2001
From: Matthias Urhahn <matthias.urhahn@sap.com>
Date: Mon, 30 Nov 2020 15:43:54 +0100
Subject: [PATCH] Fix possible notification loop (EXPOSUREAPP-4036) (#1750)

* Adding missing log tags, otherwise displays "BaseContinuationImpl" etc.

* Remember the last risk level result for which we did a change check.
This fixed the issue that if no new risk level calculation happened, and the last one triggered a notification,
killing and restarting the app would also trigger a notification.

Co-authored-by: Ralf Gehrer <ralfgehrer@users.noreply.github.com>
Co-authored-by: BMItter <46747780+BMItter@users.noreply.github.com>
---
 .../appconfig/ConfigChangeDetector.kt         | 16 ++++++----
 .../risk/RiskLevelChangeDetector.kt           |  8 +++++
 .../coronawarnapp/risk/RiskLevelSettings.kt   | 10 +++++++
 .../rki/coronawarnapp/util/WatchdogService.kt |  9 +++---
 .../util/worker/WorkManagerProvider.kt        | 10 +++++--
 .../risk/RiskLevelChangeDetectorTest.kt       | 29 ++++++++++++++++++-
 6 files changed, 68 insertions(+), 14 deletions(-)

diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/ConfigChangeDetector.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/ConfigChangeDetector.kt
index da3440abb..9ba3aef29 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/ConfigChangeDetector.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/ConfigChangeDetector.kt
@@ -24,14 +24,14 @@ class ConfigChangeDetector @Inject constructor(
 ) {
 
     fun launch() {
-        Timber.v("Monitoring config changes.")
+        Timber.tag(TAG).v("Monitoring config changes.")
         appConfigProvider.currentConfig
             .distinctUntilChangedBy { it.identifier }
             .onEach {
-                Timber.v("Running app config change checks.")
+                Timber.tag(TAG).v("Running app config change checks.")
                 check(it.identifier)
             }
-            .catch { Timber.e(it, "App config change checks failed.") }
+            .catch { Timber.tag(TAG).e(it, "App config change checks failed.") }
             .launchIn(appScope)
     }
 
@@ -39,17 +39,21 @@ class ConfigChangeDetector @Inject constructor(
     internal suspend fun check(newIdentifier: String) {
         if (riskLevelSettings.lastUsedConfigIdentifier == null) {
             // No need to reset anything if we didn't calculate a risklevel yet.
-            Timber.d("Config changed, but no previous identifier is available.")
+            Timber.tag(TAG).d("Config changed, but no previous identifier is available.")
             return
         }
 
         val oldConfigId = riskLevelSettings.lastUsedConfigIdentifier
         if (newIdentifier != oldConfigId) {
-            Timber.i("New config id ($newIdentifier) differs from last one ($oldConfigId), resetting.")
+            Timber.tag(TAG).i("New config id ($newIdentifier) differs from last one ($oldConfigId), resetting.")
             riskLevelStorage.clear()
             taskController.submit(DefaultTaskRequest(RiskLevelTask::class, originTag = "ConfigChangeDetector"))
         } else {
-            Timber.v("Config identifier ($oldConfigId) didn't change, NOOP.")
+            Timber.tag(TAG).v("Config identifier ($oldConfigId) didn't change, NOOP.")
         }
     }
+
+    companion object {
+        private const val TAG = "ConfigChangeDetector"
+    }
 }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskLevelChangeDetector.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskLevelChangeDetector.kt
index 4829cc5b1..0a08ba9ef 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskLevelChangeDetector.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskLevelChangeDetector.kt
@@ -25,6 +25,7 @@ class RiskLevelChangeDetector @Inject constructor(
     @AppContext private val context: Context,
     @AppScope private val appScope: CoroutineScope,
     private val riskLevelStorage: RiskLevelStorage,
+    private val riskLevelSettings: RiskLevelSettings,
     private val notificationManagerCompat: NotificationManagerCompat,
     private val foregroundState: ForegroundState
 ) {
@@ -48,6 +49,13 @@ class RiskLevelChangeDetector @Inject constructor(
         val oldResult = changedLevels.first()
         val newResult = changedLevels.last()
 
+        val lastCheckedResult = riskLevelSettings.lastChangeCheckedRiskLevelTimestamp
+        if (lastCheckedResult == newResult.calculatedAt) {
+            Timber.d("We already checked this risk level change, skipping further checks.")
+            return
+        }
+        riskLevelSettings.lastChangeCheckedRiskLevelTimestamp = newResult.calculatedAt
+
         val oldRiskLevel = oldResult.riskLevel
         val newRiskLevel = newResult.riskLevel
 
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskLevelSettings.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskLevelSettings.kt
index 4af4d0c05..4e2eb9cf9 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskLevelSettings.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskLevelSettings.kt
@@ -3,6 +3,7 @@ package de.rki.coronawarnapp.risk
 import android.content.Context
 import androidx.core.content.edit
 import de.rki.coronawarnapp.util.di.AppContext
+import org.joda.time.Instant
 import javax.inject.Inject
 import javax.inject.Singleton
 
@@ -24,8 +25,17 @@ class RiskLevelSettings @Inject constructor(
             putString(PKEY_RISKLEVEL_CALC_LAST_CONFIG_ID, value)
         }
 
+    var lastChangeCheckedRiskLevelTimestamp: Instant?
+        get() = prefs.getLong(PKEY_LAST_CHANGE_CHECKED_RISKLEVEL_TIMESTAMP, 0L).let {
+            if (it != 0L) Instant.ofEpochMilli(it) else null
+        }
+        set(value) = prefs.edit {
+            putLong(PKEY_LAST_CHANGE_CHECKED_RISKLEVEL_TIMESTAMP, value?.millis ?: 0L)
+        }
+
     companion object {
         private const val NAME_SHARED_PREFS = "risklevel_localdata"
         private const val PKEY_RISKLEVEL_CALC_LAST_CONFIG_ID = "risklevel.config.identifier.last"
+        private const val PKEY_LAST_CHANGE_CHECKED_RISKLEVEL_TIMESTAMP = "PKEY_RISKLEVEL_CALC_LAST_CONFIG_ID"
     }
 }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/WatchdogService.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/WatchdogService.kt
index 8a5729554..f4f6f8cdf 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/WatchdogService.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/WatchdogService.kt
@@ -34,18 +34,18 @@ class WatchdogService @Inject constructor(
     fun launch() {
         // Only do this if the background jobs are enabled
         if (!ConnectivityHelper.autoModeEnabled(context)) {
-            Timber.d("Background jobs are not enabled, aborting.")
+            Timber.tag(TAG).d("Background jobs are not enabled, aborting.")
             return
         }
 
-        Timber.v("Acquiring wakelocks for watchdog routine.")
+        Timber.tag(TAG).v("Acquiring wakelocks for watchdog routine.")
         ProcessLifecycleOwner.get().lifecycleScope.launch {
             // A wakelock as the OS does not handle this for us like in the background job execution
             val wakeLock = createWakeLock()
             // A wifi lock to wake up the wifi connection in case the device is dozing
             val wifiLock = createWifiLock()
 
-            Timber.d("Automatic mode is on, check if we have downloaded keys already today")
+            Timber.tag(TAG).d("Automatic mode is on, check if we have downloaded keys already today")
 
             val state = taskController.submitBlocking(
                 DefaultTaskRequest(
@@ -55,7 +55,7 @@ class WatchdogService @Inject constructor(
                 )
             )
             if (state.isFailed) {
-                Timber.e(state.error, "RetrieveDiagnosisKeysTransaction failed")
+                Timber.tag(TAG).e(state.error, "RetrieveDiagnosisKeysTransaction failed")
                 // retry the key retrieval in case of an error with a scheduled work
                 BackgroundWorkScheduler.scheduleDiagnosisKeyOneTimeWork()
             }
@@ -78,6 +78,7 @@ class WatchdogService @Inject constructor(
         .apply { acquire() }
 
     companion object {
+        private const val TAG = "WatchdogService"
         private const val TEN_MINUTE_TIMEOUT_IN_MS = 10 * 60 * 1000L
     }
 }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/worker/WorkManagerProvider.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/worker/WorkManagerProvider.kt
index dff4a55e3..bd5b99c86 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/worker/WorkManagerProvider.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/worker/WorkManagerProvider.kt
@@ -15,17 +15,21 @@ class WorkManagerProvider @Inject constructor(
 ) {
 
     val workManager by lazy {
-        Timber.v("Setting up WorkManager.")
+        Timber.tag(TAG).v("Setting up WorkManager.")
         val configuration = Configuration.Builder().apply {
             setMinimumLoggingLevel(android.util.Log.DEBUG)
             setWorkerFactory(cwaWorkerFactory)
         }.build()
 
-        Timber.v("WorkManager initialize...")
+        Timber.tag(TAG).v("WorkManager initialize...")
         WorkManager.initialize(context, configuration)
 
         WorkManager.getInstance(context).also {
-            Timber.v("WorkManager setup done: %s", it)
+            Timber.tag(TAG).v("WorkManager setup done: %s", it)
         }
     }
+
+    companion object {
+        private const val TAG = "WorkManagerProvider"
+    }
 }
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/RiskLevelChangeDetectorTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/RiskLevelChangeDetectorTest.kt
index 025faf80b..37e6cd268 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/RiskLevelChangeDetectorTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/RiskLevelChangeDetectorTest.kt
@@ -37,6 +37,7 @@ class RiskLevelChangeDetectorTest : BaseTest() {
     @MockK lateinit var riskLevelStorage: RiskLevelStorage
     @MockK lateinit var notificationManagerCompat: NotificationManagerCompat
     @MockK lateinit var foregroundState: ForegroundState
+    @MockK lateinit var riskLevelSettings: RiskLevelSettings
 
     @BeforeEach
     fun setup() {
@@ -48,6 +49,8 @@ class RiskLevelChangeDetectorTest : BaseTest() {
         every { LocalData.submissionWasSuccessful() } returns false
         every { foregroundState.isInForeground } returns flowOf(true)
         every { notificationManagerCompat.areNotificationsEnabled() } returns true
+        every { riskLevelSettings.lastChangeCheckedRiskLevelTimestamp = any() } just Runs
+        every { riskLevelSettings.lastChangeCheckedRiskLevelTimestamp } returns null
     }
 
     @AfterEach
@@ -73,7 +76,8 @@ class RiskLevelChangeDetectorTest : BaseTest() {
         appScope = scope,
         riskLevelStorage = riskLevelStorage,
         notificationManagerCompat = notificationManagerCompat,
-        foregroundState = foregroundState
+        foregroundState = foregroundState,
+        riskLevelSettings = riskLevelSettings
     )
 
     @Test
@@ -160,6 +164,29 @@ class RiskLevelChangeDetectorTest : BaseTest() {
         }
     }
 
+    @Test
+    fun `risklevel went from LOW to HIGH but it is has already been processed`() {
+        every { riskLevelStorage.riskLevelResults } returns flowOf(
+            listOf(
+                createRiskLevel(INCREASED_RISK, calculatedAt = Instant.EPOCH.plus(1)),
+                createRiskLevel(LOW_LEVEL_RISK, calculatedAt = Instant.EPOCH)
+            )
+        )
+        every { riskLevelSettings.lastChangeCheckedRiskLevelTimestamp } returns Instant.EPOCH.plus(1)
+
+        runBlockingTest {
+            val instance = createInstance(scope = this)
+            instance.launch()
+
+            advanceUntilIdle()
+
+            coVerifySequence {
+                LocalData wasNot Called
+                notificationManagerCompat wasNot Called
+            }
+        }
+    }
+
     @Test
     fun `evaluate risk level change detection function`() {
         RiskLevelChangeDetector.hasHighLowLevelChanged(UNDETERMINED, UNDETERMINED) shouldBe false
-- 
GitLab