From c166e4cb5d17337ea09739f760467f147e4cad32 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jakob=20M=C3=B6ller?= <jakob.moeller@sap.com>
Date: Fri, 5 Jun 2020 18:16:59 +0200
Subject: [PATCH] Background Job Rework (#193)

* Background work improved

* Allow background data usage checks
---
 .../de/rki/coronawarnapp/storage/LocalData.kt |   9 --
 .../storage/SettingsRepository.kt             |  38 +----
 .../rki/coronawarnapp/ui/main/MainActivity.kt |  10 +-
 .../rki/coronawarnapp/ui/main/MainFragment.kt |   1 -
 .../ui/riskdetails/RiskDetailsFragment.kt     |   1 -
 .../ui/settings/SettingsFragment.kt           |   2 -
 .../ui/settings/SettingsTracingFragment.kt    |   3 +-
 .../ui/viewmodel/SettingsViewModel.kt         |  27 +---
 .../coronawarnapp/util/ConnectivityHelper.kt  |  18 +++
 .../util/formatter/FormatterRiskHelper.kt     |   7 +-
 .../worker/BackgroundConstants.kt             |   7 +-
 .../worker/BackgroundWorkScheduler.kt         | 140 +++++++++---------
 .../DiagnosisKeyRetrievalOneTimeWorker.kt     |  15 +-
 .../DiagnosisKeyRetrievalPeriodicWorker.kt    |  17 ++-
 .../src/main/res/layout/include_risk_card.xml |   4 +-
 .../src/main/res/values/strings.xml           |   2 +-
 16 files changed, 133 insertions(+), 168 deletions(-)

diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/LocalData.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/LocalData.kt
index 7735bc4ce..c6c6bba9d 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/LocalData.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/LocalData.kt
@@ -585,13 +585,4 @@ object LocalData {
      ****************************************************/
 
     fun getSharedPreferenceInstance(): SharedPreferences = globalEncryptedSharedPreferencesInstance
-
-    fun getBackgroundWorkRelatedPreferences() = listOf(
-        CoronaWarnApplication.getAppContext().getString(R.string.preference_background_job_allowed),
-        CoronaWarnApplication.getAppContext().getString(R.string.preference_mobile_data_allowed)
-    )
-
-    fun getLastFetchDatePreference() =
-        CoronaWarnApplication.getAppContext()
-            .getString(R.string.preference_timestamp_diagnosis_keys_fetch)
 }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/SettingsRepository.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/SettingsRepository.kt
index 918f0ad3a..308385ca1 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/SettingsRepository.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/SettingsRepository.kt
@@ -24,7 +24,6 @@ object SettingsRepository {
     val isManualKeyRetrievalEnabled = MutableLiveData(true)
     val isConnectionEnabled = MutableLiveData(true)
     val isBluetoothEnabled = MutableLiveData(true)
-    val isMobileDataEnabled = MutableLiveData(true)
     val isBackgroundJobEnabled = MutableLiveData(true)
 
     // TODO should go to a formatter
@@ -97,40 +96,11 @@ object SettingsRepository {
     }
 
     /**
-     * Toggle mobile data in shared preferences and refresh it afterwards.
-     *
-     * @see LocalData
-     */
-    fun toggleMobileDataEnabled() {
-        LocalData.toggleMobileDataEnabled()
-        refreshMobileDataEnabled()
-    }
-
-    /**
-     * Refresh mobile data with the current shared preferences state.
-     *
-     * @see LocalData
-     */
-    fun refreshMobileDataEnabled() {
-        isMobileDataEnabled.value = LocalData.isMobileDataEnabled()
-    }
-
-    /**
-     * Toggle background job in shared preferences and refresh it afterwards.
-     *
-     * @see LocalData
-     */
-    fun toggleBackgroundJobEnabled() {
-        LocalData.toggleBackgroundJobEnabled()
-        refreshBackgroundJobEnabled()
-    }
-
-    /**
-     * Refresh background job with the current shared preferences state.
+     * Refresh global bluetooth state to point out that tracing isn't working
      *
-     * @see LocalData
+     * @see ConnectivityHelper
      */
-    fun refreshBackgroundJobEnabled() {
-        isBackgroundJobEnabled.value = LocalData.isBackgroundJobEnabled()
+    fun updateBackgroundJobEnabled(value: Boolean) {
+        isBackgroundJobEnabled.postValue(value)
     }
 }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/MainActivity.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/MainActivity.kt
index 091353254..1d03c732a 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/MainActivity.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/MainActivity.kt
@@ -2,6 +2,7 @@ package de.rki.coronawarnapp.ui.main
 
 import android.content.Intent
 import android.os.Bundle
+import android.util.Log
 import androidx.appcompat.app.AppCompatActivity
 import androidx.fragment.app.Fragment
 import androidx.fragment.app.FragmentManager
@@ -55,10 +56,6 @@ class MainActivity : AppCompatActivity() {
         }
     }
 
-    init {
-        scheduleWork()
-    }
-
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
         setContentView(R.layout.activity_main)
@@ -66,12 +63,15 @@ class MainActivity : AppCompatActivity() {
     }
 
     /**
-     * Register network and bluetooth callback.
+     * Register network, bluetooth and data saver callback.
      */
     override fun onResume() {
         super.onResume()
         ConnectivityHelper.registerNetworkStatusCallback(this, callbackNetwork)
         ConnectivityHelper.registerBluetoothStatusCallback(this, callbackBluetooth)
+        Log.d(TAG, "Background work is available: ${!ConnectivityHelper.isDataSaverEnabled(this)}")
+        settingsViewModel.updateBackgroundJobEnabled(!ConnectivityHelper.isDataSaverEnabled(this))
+        scheduleWork()
         showDialogWithStacktraceIfPreviouslyCrashed()
     }
 
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/MainFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/MainFragment.kt
index 53404e0f9..3634469be 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/MainFragment.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/MainFragment.kt
@@ -72,7 +72,6 @@ class MainFragment : BaseFragment() {
         tracingViewModel.refreshLastTimeDiagnosisKeysFetchedDate()
         tracingViewModel.refreshIsTracingEnabled()
         tracingViewModel.refreshActiveTracingDaysInRetentionPeriod()
-        settingsViewModel.refreshBackgroundJobEnabled()
         TimerHelper.checkManualKeyRetrievalTimer()
         submissionViewModel.refreshDeviceUIState()
     }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/riskdetails/RiskDetailsFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/riskdetails/RiskDetailsFragment.kt
index 13e9e6564..893a95287 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/riskdetails/RiskDetailsFragment.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/riskdetails/RiskDetailsFragment.kt
@@ -57,7 +57,6 @@ class RiskDetailsFragment : BaseFragment() {
         tracingViewModel.refreshRiskLevel()
         tracingViewModel.refreshExposureSummary()
         tracingViewModel.refreshLastTimeDiagnosisKeysFetchedDate()
-        settingsViewModel.refreshBackgroundJobEnabled()
         TimerHelper.checkManualKeyRetrievalTimer()
     }
 
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/settings/SettingsFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/settings/SettingsFragment.kt
index 55f10e046..f7aab9c19 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/settings/SettingsFragment.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/settings/SettingsFragment.kt
@@ -57,8 +57,6 @@ class SettingsFragment : BaseFragment() {
         settingsViewModel.refreshNotificationsEnabled(requireContext())
         settingsViewModel.refreshNotificationsRiskEnabled()
         settingsViewModel.refreshNotificationsTestEnabled()
-        settingsViewModel.refreshMobileDataEnabled()
-        settingsViewModel.refreshBackgroundJobEnabled()
     }
 
     private fun setButtonOnClickListener() {
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/settings/SettingsTracingFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/settings/SettingsTracingFragment.kt
index 7a4f6cbc0..382d34f05 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/settings/SettingsTracingFragment.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/settings/SettingsTracingFragment.kt
@@ -81,8 +81,7 @@ class SettingsTracingFragment : BaseFragment(),
 
     override fun onStartPermissionGranted() {
         tracingViewModel.refreshIsTracingEnabled()
-        // TODO
-        BackgroundWorkScheduler.checkStart()
+        BackgroundWorkScheduler.startWorkScheduler()
         Toast.makeText(requireContext(), "Tracing started successfully", Toast.LENGTH_SHORT).show()
     }
 
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/viewmodel/SettingsViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/viewmodel/SettingsViewModel.kt
index ada13e703..05809e74c 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/viewmodel/SettingsViewModel.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/viewmodel/SettingsViewModel.kt
@@ -22,10 +22,6 @@ class SettingsViewModel : ViewModel() {
     val isBluetoothEnabled: LiveData<Boolean> =
         SettingsRepository.isBluetoothEnabled
 
-    // Todo bind to os settings, change to general network availability, cannot be set within the app
-    // Will impact UI if no network connection is found, persistent storing is not necessary
-    val isMobileDataEnabled: LiveData<Boolean> = SettingsRepository.isMobileDataEnabled
-
     // Todo bind to os settings, care API 23 / API 24 onwards
     // Will impact UI if background activity is not permitted, persistent storing is not necessary
     val isBackgroundJobEnabled: LiveData<Boolean> = SettingsRepository.isBackgroundJobEnabled
@@ -102,24 +98,11 @@ class SettingsViewModel : ViewModel() {
     }
 
     /**
-     * Refresh & toggle mobile data enabled
-     */
-    fun refreshMobileDataEnabled() {
-        SettingsRepository.refreshMobileDataEnabled()
-    }
-
-    fun toggleMobileDataEnabled() {
-        SettingsRepository.toggleMobileDataEnabled()
-    }
-
-    /**
-     * Refresh & toggle background job enabled
+     * Update background job enabled
+     *
+     * @param value
      */
-    fun refreshBackgroundJobEnabled() {
-        SettingsRepository.refreshBackgroundJobEnabled()
-    }
-
-    fun toggleBackgroundJobEnabled() {
-        SettingsRepository.toggleBackgroundJobEnabled()
+    fun updateBackgroundJobEnabled(value: Boolean) {
+        SettingsRepository.updateBackgroundJobEnabled(value)
     }
 }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/ConnectivityHelper.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/ConnectivityHelper.kt
index 6118ea661..457de57a6 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/ConnectivityHelper.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/ConnectivityHelper.kt
@@ -9,6 +9,7 @@ import android.net.ConnectivityManager
 import android.net.Network
 import android.net.NetworkCapabilities
 import android.net.NetworkRequest
+import android.os.Build
 import android.util.Log
 import de.rki.coronawarnapp.exception.ExceptionCategory
 import de.rki.coronawarnapp.exception.report
@@ -120,6 +121,23 @@ object ConnectivityHelper {
         }
     }
 
+    /**
+     * For API level 24+ check if data saver is enabled
+     * Else always return false
+     *
+     * @param context the context
+     *
+     * @return Boolean
+     *
+     * @see ConnectivityManager.RESTRICT_BACKGROUND_STATUS_DISABLED
+     */
+    fun isDataSaverEnabled(context: Context): Boolean {
+        return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+            val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
+            connectivityManager.restrictBackgroundStatus != ConnectivityManager.RESTRICT_BACKGROUND_STATUS_DISABLED
+        } else false
+    }
+
     /**
      * Get bluetooth enabled status.
      *
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/formatter/FormatterRiskHelper.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/formatter/FormatterRiskHelper.kt
index 29adcd8d9..2b26669bf 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/formatter/FormatterRiskHelper.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/formatter/FormatterRiskHelper.kt
@@ -255,8 +255,7 @@ fun formatTimeFetched(
  */
 fun formatNextUpdate(
     riskLevelScore: Int?,
-    isBackgroundJobEnabled: Boolean?,
-    nextUpdate: Date
+    isBackgroundJobEnabled: Boolean?
 ): String {
     val appContext = CoronaWarnApplication.getAppContext()
     return if (isBackgroundJobEnabled != true) {
@@ -266,9 +265,7 @@ fun formatNextUpdate(
             RiskLevelConstants.UNKNOWN_RISK_INITIAL,
             RiskLevelConstants.LOW_LEVEL_RISK,
             RiskLevelConstants.INCREASED_RISK -> appContext.getString(
-                R.string.risk_card_body_next_update,
-                DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.MEDIUM)
-                    .format(nextUpdate)
+                R.string.risk_card_body_next_update
             )
             else -> ""
         }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/BackgroundConstants.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/BackgroundConstants.kt
index 3c5c8eb80..a36174aea 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/BackgroundConstants.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/BackgroundConstants.kt
@@ -38,7 +38,7 @@ object BackgroundConstants {
      * Total tries count for diagnosis key retrieval per day
      * Internal requirement
      */
-    const val DIAGNOSIS_KEY_RETRIEVAL_TRIES_PER_DAY = 12
+    const val DIAGNOSIS_KEY_RETRIEVAL_TRIES_PER_DAY = 1
 
     /**
      * Maximum tries count for diagnosis key retrieval per day
@@ -84,4 +84,9 @@ object BackgroundConstants {
      * @see TimeUnit.MINUTES
      */
     const val TIME_RANGE_MAX = 1439
+
+    /**
+     * Retries before work would set as FAILED
+     */
+    const val WORKER_RETRY_COUNT_THRESHOLD = 3
 }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/BackgroundWorkScheduler.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/BackgroundWorkScheduler.kt
index 88f2db0be..a40cef82d 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/BackgroundWorkScheduler.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/BackgroundWorkScheduler.kt
@@ -1,7 +1,7 @@
 package de.rki.coronawarnapp.worker
 
-import android.content.SharedPreferences.OnSharedPreferenceChangeListener
 import android.util.Log
+import androidx.work.BackoffPolicy
 import androidx.work.Constraints
 import androidx.work.ExistingPeriodicWorkPolicy
 import androidx.work.ExistingWorkPolicy
@@ -9,14 +9,14 @@ import androidx.work.NetworkType
 import androidx.work.OneTimeWorkRequestBuilder
 import androidx.work.Operation
 import androidx.work.PeriodicWorkRequestBuilder
+import androidx.work.WorkInfo
 import androidx.work.WorkManager
 import de.rki.coronawarnapp.BuildConfig
 import de.rki.coronawarnapp.CoronaWarnApplication
-import de.rki.coronawarnapp.storage.LocalData
-import de.rki.coronawarnapp.storage.TracingRepository
 import org.joda.time.DateTime
 import org.joda.time.DateTimeZone
 import org.joda.time.Instant
+import java.util.concurrent.ExecutionException
 import java.util.concurrent.TimeUnit
 
 /**
@@ -78,11 +78,6 @@ object BackgroundWorkScheduler {
         BackgroundConstants.DIAGNOSIS_KEY_RETRIEVAL_TRIES_PER_DAY
             .coerceAtMost(BackgroundConstants.GOOGLE_API_MAX_CALLS_PER_DAY)
 
-    /**
-     * Shared preferences listener
-     */
-    private var sharedPrefListener: OnSharedPreferenceChangeListener? = null
-
     /**
      * Work manager instance
      */
@@ -90,27 +85,44 @@ object BackgroundWorkScheduler {
 
     /**
      * Start work scheduler
-     * Subscribe shared preferences listener for changes. If any changes regarding background work
-     * occurred, then reschedule periodic work or stop it (depends on changes occurred).
-     * Two keys are monitored:
-     * - preference_background_jonboarding_allowed
-     * - preference_mobile_data_allowed
-     * @see LocalData.getBackgroundWorkRelatedPreferences()
+     * Checks if periodic worker was already scheduled. If not - reschedule it again.
+     *
+     * @see isWorkActive
      */
     fun startWorkScheduler() {
-        sharedPrefListener = OnSharedPreferenceChangeListener { _, key ->
-            if (LocalData.getBackgroundWorkRelatedPreferences().contains(key)) {
-                logSharedPreferencesChange(key)
-                checkStart()
-            } else if (key == LocalData.getLastFetchDatePreference()) {
-                TracingRepository.refreshLastTimeDiagnosisKeysFetchedDate()
+        val isPeriodicWorkActive = isWorkActive(WorkTag.DIAGNOSIS_KEY_RETRIEVAL_PERIODIC_WORKER.tag)
+        logWorkActiveStatus(WorkTag.DIAGNOSIS_KEY_RETRIEVAL_PERIODIC_WORKER.tag, isPeriodicWorkActive)
+        if (!isPeriodicWorkActive) WorkType.DIAGNOSIS_KEY_BACKGROUND_PERIODIC_WORK.start()
+    }
+
+    /**
+     * Checks if defined work is active
+     * Non-active means worker was Cancelled, Failed or have not been enqueued at all
+     *
+     * @param tag String tag of the worker
+     *
+     * @return Boolean
+     *
+     * @see WorkInfo.State.CANCELLED
+     * @see WorkInfo.State.FAILED
+     */
+    private fun isWorkActive(tag: String): Boolean {
+        val workStatus = workManager.getWorkInfosByTag(tag)
+        var result = true
+        try {
+            val workInfoList = workStatus.get()
+            if (workInfoList.size == 0) result = false
+            for (info in workInfoList) {
+                if (info.state == WorkInfo.State.CANCELLED || info.state == WorkInfo.State.FAILED) {
+                    result = false
+                }
             }
+        } catch (e: ExecutionException) {
+            result = false
+        } catch (e: InterruptedException) {
+            result = false
         }
-        LocalData.getSharedPreferenceInstance().registerOnSharedPreferenceChangeListener(
-            sharedPrefListener
-        )
-        // TODO: Reimplement after clarifications
-        WorkType.DIAGNOSIS_KEY_BACKGROUND_PERIODIC_WORK.start()
+        return result
     }
 
     /**
@@ -125,31 +137,21 @@ object BackgroundWorkScheduler {
     }
 
     /**
-     * Check start periodic work
-     * If background work is enabled, than reschedule it. else - stop it.
+     * Schedule diagnosis key one time work
      *
-     * @see LocalData.isBackgroundJobEnabled()
-     * @see WorkType.DIAGNOSIS_KEY_BACKGROUND_PERIODIC_WORK
+     * @see WorkType.DIAGNOSIS_KEY_BACKGROUND_ONE_TIME_WORK
      */
-    fun checkStart() {
-        if (LocalData.isBackgroundJobEnabled()) {
-            WorkType.DIAGNOSIS_KEY_BACKGROUND_ONE_TIME_WORK.stop()
-            WorkType.DIAGNOSIS_KEY_BACKGROUND_PERIODIC_WORK.start()
-        } else {
-            stopWorkScheduler()
-        }
+    fun scheduleDiagnosisKeyPeriodicWork() {
+        WorkType.DIAGNOSIS_KEY_BACKGROUND_PERIODIC_WORK.start()
     }
 
     /**
      * Schedule diagnosis key one time work
      *
-     * @see LocalData.isBackgroundJobEnabled()
      * @see WorkType.DIAGNOSIS_KEY_BACKGROUND_ONE_TIME_WORK
      */
     fun scheduleDiagnosisKeyOneTimeWork() {
-        if (LocalData.isBackgroundJobEnabled()) {
-            WorkType.DIAGNOSIS_KEY_BACKGROUND_ONE_TIME_WORK.start()
-        }
+        WorkType.DIAGNOSIS_KEY_BACKGROUND_ONE_TIME_WORK.start()
     }
 
     /**
@@ -164,15 +166,6 @@ object BackgroundWorkScheduler {
         WorkType.DIAGNOSIS_KEY_BACKGROUND_ONE_TIME_WORK -> enqueueDiagnosisKeyBackgroundOneTimeWork()
     }
 
-    /**
-     * Stop work by unique name
-     *
-     * @return Operation
-     *
-     * @see WorkType
-     */
-    private fun WorkType.stop(): Operation = workManager.cancelUniqueWork(this.uniqueName)
-
     /**
      * Enqueue diagnosis key periodic work and log it
      * Replace with new if older work exists.
@@ -204,11 +197,13 @@ object BackgroundWorkScheduler {
     /**
      * Build diagnosis key periodic work request
      * Set "kind delay" for accessibility reason.
+     * Backoff criteria set to Linear type.
      *
      * @return PeriodicWorkRequest
      *
      * @see WorkTag.DIAGNOSIS_KEY_RETRIEVAL_PERIODIC_WORKER
      * @see BackgroundConstants.DIAGNOSIS_KEY_PERIODIC_KIND_DELAY
+     * @see BackoffPolicy.LINEAR
      */
     private fun buildDiagnosisKeyRetrievalPeriodicWork() =
         PeriodicWorkRequestBuilder<DiagnosisKeyRetrievalPeriodicWorker>(
@@ -220,16 +215,24 @@ object BackgroundWorkScheduler {
                 BackgroundConstants.DIAGNOSIS_KEY_PERIODIC_KIND_DELAY,
                 TimeUnit.MINUTES
             )
+            .setBackoffCriteria(
+                BackoffPolicy.LINEAR,
+                BackgroundConstants.DIAGNOSIS_KEY_PERIODIC_KIND_DELAY,
+                TimeUnit.MINUTES
+            )
             .build()
 
     /**
      * Build diagnosis key one time work request
      * Set random initial delay for security reason.
+     * Backoff criteria set to Linear type.
      *
      * @return OneTimeWorkRequest
      *
      * @see WorkTag.DIAGNOSIS_KEY_RETRIEVAL_ONE_TIME_WORKER
      * @see buildDiagnosisKeyRetrievalOneTimeWork
+     * @see BackgroundConstants.DIAGNOSIS_KEY_PERIODIC_KIND_DELAY
+     * @see BackoffPolicy.LINEAR
      */
     private fun buildDiagnosisKeyRetrievalOneTimeWork() =
         OneTimeWorkRequestBuilder<DiagnosisKeyRetrievalOneTimeWorker>()
@@ -243,6 +246,11 @@ object BackgroundWorkScheduler {
                     )
                 ), TimeUnit.MINUTES
             )
+            .setBackoffCriteria(
+                BackoffPolicy.LINEAR,
+                BackgroundConstants.DIAGNOSIS_KEY_PERIODIC_KIND_DELAY,
+                TimeUnit.MINUTES
+            )
             .build()
 
     /**
@@ -256,29 +264,19 @@ object BackgroundWorkScheduler {
 
     /**
      * Constraints for diagnosis key one time work
-     * Depends on current application settings.
+     * Requires battery not low and any network connection
+     * Mobile data usage is handled on OS level in application settings
      *
      * @return Constraints
      *
-     * @see LocalData.isMobileDataEnabled()
+     * @see NetworkType.CONNECTED
      */
-    private fun getConstraintsForDiagnosisKeyOneTimeBackgroundWork(): Constraints {
-        val builder = Constraints.Builder()
-        if (LocalData.isMobileDataEnabled()) {
-            if (BuildConfig.DEBUG) Log.d(
-                TAG, "${WorkType.DIAGNOSIS_KEY_BACKGROUND_ONE_TIME_WORK}:" +
-                        "$BackgroundConstants.NETWORK_ROAMING_ALLOWED"
-            )
-            builder.setRequiredNetworkType(NetworkType.CONNECTED)
-        } else {
-            if (BuildConfig.DEBUG) Log.d(
-                TAG, "${WorkType.DIAGNOSIS_KEY_BACKGROUND_ONE_TIME_WORK}:" +
-                        "$BackgroundConstants.NETWORK_ROAMING_FORBIDDEN"
-            )
-            builder.setRequiredNetworkType(NetworkType.NOT_ROAMING)
-        }
-        return builder.build()
-    }
+    private fun getConstraintsForDiagnosisKeyOneTimeBackgroundWork() =
+        Constraints
+            .Builder()
+            .setRequiresBatteryNotLow(true)
+            .setRequiredNetworkType(NetworkType.CONNECTED)
+            .build()
 
     /**
      * Log operation schedule
@@ -296,9 +294,9 @@ object BackgroundWorkScheduler {
         .also { if (BuildConfig.DEBUG) Log.d(TAG, "Canceling all work with tag ${workTag.tag}") }
 
     /**
-     * Log shared preferences change
+     * Log work active status
      */
-    private fun logSharedPreferencesChange(key: String) {
-        if (BuildConfig.DEBUG) Log.d(TAG, "Shared preferences was changed in key: $key")
+    private fun logWorkActiveStatus(tag: String, active: Boolean) {
+        if (BuildConfig.DEBUG) Log.d(TAG, "Work type $tag is active: $active")
     }
 }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/DiagnosisKeyRetrievalOneTimeWorker.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/DiagnosisKeyRetrievalOneTimeWorker.kt
index 777edc0c3..5f31a256b 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/DiagnosisKeyRetrievalOneTimeWorker.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/DiagnosisKeyRetrievalOneTimeWorker.kt
@@ -5,8 +5,6 @@ import android.util.Log
 import androidx.work.CoroutineWorker
 import androidx.work.WorkerParameters
 import de.rki.coronawarnapp.BuildConfig
-import de.rki.coronawarnapp.exception.ExceptionCategory
-import de.rki.coronawarnapp.exception.report
 import de.rki.coronawarnapp.transaction.RetrieveDiagnosisKeysTransaction
 
 /**
@@ -30,13 +28,18 @@ class DiagnosisKeyRetrievalOneTimeWorker(val context: Context, workerParams: Wor
      * @see RetrieveDiagnosisKeysTransaction
      */
     override suspend fun doWork(): Result {
-        if (BuildConfig.DEBUG) Log.d(TAG, "Background job started...")
+        if (BuildConfig.DEBUG) Log.d(TAG, "Background job started. Run attempt: $runAttemptCount")
+
+        if (runAttemptCount > BackgroundConstants.WORKER_RETRY_COUNT_THRESHOLD) {
+            if (BuildConfig.DEBUG) Log.d(TAG, "Background job failed after $runAttemptCount attempts. Rescheduling")
+            return Result.failure()
+        }
+        var result = Result.success()
         try {
             RetrieveDiagnosisKeysTransaction.start()
         } catch (e: Exception) {
-            e.report(ExceptionCategory.JOB)
-            return Result.failure()
+            result = Result.retry()
         }
-        return Result.success()
+        return result
     }
 }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/DiagnosisKeyRetrievalPeriodicWorker.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/DiagnosisKeyRetrievalPeriodicWorker.kt
index 1caccef0b..f7d7eee4f 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/DiagnosisKeyRetrievalPeriodicWorker.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/DiagnosisKeyRetrievalPeriodicWorker.kt
@@ -5,8 +5,6 @@ import android.util.Log
 import androidx.work.CoroutineWorker
 import androidx.work.WorkerParameters
 import de.rki.coronawarnapp.BuildConfig
-import de.rki.coronawarnapp.exception.ExceptionCategory
-import de.rki.coronawarnapp.exception.report
 
 /**
  * Periodic diagnosis key retrieval work
@@ -27,16 +25,23 @@ class DiagnosisKeyRetrievalPeriodicWorker(val context: Context, workerParams: Wo
      *
      * @return Result
      *
+     * @see BackgroundWorkScheduler.scheduleDiagnosisKeyPeriodicWork()
      * @see BackgroundWorkScheduler.scheduleDiagnosisKeyOneTimeWork()
      */
     override suspend fun doWork(): Result {
-        if (BuildConfig.DEBUG) Log.d(TAG, "Background job started...")
+        if (BuildConfig.DEBUG) Log.d(TAG, "Background job started. Run attempt: $runAttemptCount")
+
+        if (runAttemptCount > BackgroundConstants.WORKER_RETRY_COUNT_THRESHOLD) {
+            if (BuildConfig.DEBUG) Log.d(TAG, "Background job failed after $runAttemptCount attempts. Rescheduling")
+            BackgroundWorkScheduler.scheduleDiagnosisKeyPeriodicWork()
+            return Result.failure()
+        }
+        var result = Result.success()
         try {
             BackgroundWorkScheduler.scheduleDiagnosisKeyOneTimeWork()
         } catch (e: Exception) {
-            e.report(ExceptionCategory.JOB)
-            return Result.failure()
+            result = Result.retry()
         }
-        return Result.success()
+        return result
     }
 }
diff --git a/Corona-Warn-App/src/main/res/layout/include_risk_card.xml b/Corona-Warn-App/src/main/res/layout/include_risk_card.xml
index 31f9e683f..6c7e4acf0 100644
--- a/Corona-Warn-App/src/main/res/layout/include_risk_card.xml
+++ b/Corona-Warn-App/src/main/res/layout/include_risk_card.xml
@@ -209,7 +209,7 @@
                 android:layout_width="0dp"
                 android:layout_height="wrap_content"
                 android:layout_marginTop="@dimen/spacing_normal"
-                android:visibility="@{FormatterHelper.formatVisibilityText(FormatterRiskHelper.formatNextUpdate(tracingViewModel.riskLevel, settingsViewModel.isBackgroundJobEnabled(), tracingViewModel.nextUpdate))}"
+                android:visibility="@{FormatterHelper.formatVisibilityText(FormatterRiskHelper.formatNextUpdate(tracingViewModel.riskLevel, settingsViewModel.isBackgroundJobEnabled()))}"
                 app:layout_constraintEnd_toEndOf="parent"
                 app:layout_constraintStart_toStartOf="parent"
                 app:layout_constraintTop_toBottomOf="@+id/risk_card_row_time_fetched">
@@ -227,7 +227,7 @@
                     android:layout_width="0dp"
                     android:layout_height="wrap_content"
                     android:layout_marginTop="@dimen/spacing_small"
-                    android:text="@{FormatterRiskHelper.formatNextUpdate(tracingViewModel.riskLevel, settingsViewModel.isBackgroundJobEnabled(), tracingViewModel.nextUpdate)}"
+                    android:text="@{FormatterRiskHelper.formatNextUpdate(tracingViewModel.riskLevel, settingsViewModel.isBackgroundJobEnabled())}"
                     android:textColor="@{FormatterRiskHelper.formatStableTextColor(tracingViewModel.riskLevel)}"
                     app:layout_constraintEnd_toEndOf="parent"
                     app:layout_constraintStart_toStartOf="parent"
diff --git a/Corona-Warn-App/src/main/res/values/strings.xml b/Corona-Warn-App/src/main/res/values/strings.xml
index 6f3c230cc..15c559b77 100644
--- a/Corona-Warn-App/src/main/res/values/strings.xml
+++ b/Corona-Warn-App/src/main/res/values/strings.xml
@@ -164,7 +164,7 @@
     <string name="risk_card_body_saved_days">%1$s von 14 Tagen aktiv</string>
     <string name="risk_card_body_not_yet_fetched">Kontakte wurden noch nicht überprüft.</string>
     <string name="risk_card_body_time_fetched">Aktualisiert: %1$s</string>
-    <string name="risk_card_body_next_update">Nächste Aktualisierung in: %1$s</string>
+    <string name="risk_card_body_next_update">Tägliche Aktualisierung</string>
     <!-- XBUT: risk card - update risk -->
     <string name="risk_card_button_update">Aktualisieren</string>
     <string name="risk_card_button_cooldown">Aktualisierung in %1$s</string>
-- 
GitLab