From 284195cba031ae2db871d39909c62cf243d739f1 Mon Sep 17 00:00:00 2001 From: Rituraj Sambherao <54317407+ritsam@users.noreply.github.com> Date: Tue, 9 Jun 2020 15:54:36 +0100 Subject: [PATCH] Background Polling for Test Result (#246) * Background Polling for Test Result --- .../de/rki/coronawarnapp/storage/LocalData.kt | 70 +++++++- .../storage/SubmissionRepository.kt | 4 + .../util/TimeAndDateExtensions.kt | 13 ++ .../worker/BackgroundConstants.kt | 32 +++- .../worker/BackgroundWorkHelper.kt | 73 +++++++++ .../worker/BackgroundWorkScheduler.kt | 153 ++++++++++-------- ...gnosisTestResultRetrievalPeriodicWorker.kt | 116 +++++++++++++ .../src/main/res/values/strings.xml | 8 + 8 files changed, 400 insertions(+), 69 deletions(-) create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/BackgroundWorkHelper.kt create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/DiagnosisTestResultRetrievalPeriodicWorker.kt 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 6010d2f33..3ce3ac2ef 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 @@ -143,6 +143,23 @@ object LocalData { ) } + /** + * Sets the total amount of time the tracing was not active + * from the EncryptedSharedPrefs + * + * @param value timestamp in ms + */ + fun totalNonActiveTracing(value: Long?) { + // TODO need this for nullable ref, shout not be goto for nullable storage + getSharedPreferenceInstance().edit(true) { + putLong( + CoronaWarnApplication.getAppContext() + .getString(R.string.preference_total_non_active_tracing), + value ?: 0L + ) + } + } + /** * Gets the total amount of time the tracing was not active * from the EncryptedSharedPrefs @@ -158,22 +175,65 @@ object LocalData { } /** - * Sets the total amount of time the tracing was not active + + * Gets the timestamp when the Background Polling Began initially + * from the EncryptedSharedPrefs + * + * @return timestamp in ms + */ + fun initialPollingForTestResultTimeStamp(): Long { + return getSharedPreferenceInstance().getLong( + CoronaWarnApplication.getAppContext() + .getString(R.string.preference_polling_test_result_started), + 0L + ) + } + + /** + * Sets the timestamp when the Background Polling Began initially * from the EncryptedSharedPrefs * * @param value timestamp in ms */ - fun totalNonActiveTracing(value: Long?) { - // TODO need this for nullable ref, shout not be goto for nullable storage + fun initialPollingForTestResultTimeStamp(value: Long) = getSharedPreferenceInstance().edit(true) { putLong( CoronaWarnApplication.getAppContext() - .getString(R.string.preference_total_non_active_tracing), - value ?: 0L + .getString(R.string.preference_polling_test_result_started), + value ) } + + /** + + * Gets the flag if notification is executed on Status Change + * from the EncryptedSharedPrefs + * + * @return boolean + */ + fun isTestResultNotificationSent(): Boolean { + return getSharedPreferenceInstance().getBoolean( + CoronaWarnApplication.getAppContext() + .getString(R.string.preference_test_result_notification), + false + ) } + /** + * Sets the flag if notification is executed on Status Change + * from the EncryptedSharedPrefs + * + * @param value boolean + */ + fun isTestResultNotificationSent(value: Boolean) = + getSharedPreferenceInstance().edit(true) { + putBoolean( + CoronaWarnApplication.getAppContext() + .getString(R.string.preference_test_result_notification), + value + ) + } + /**************************************************** * RISK LEVEL ****************************************************/ diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/SubmissionRepository.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/SubmissionRepository.kt index f6ab3dad8..424167ff5 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/SubmissionRepository.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/SubmissionRepository.kt @@ -5,6 +5,7 @@ import de.rki.coronawarnapp.exception.NoRegistrationTokenSetException import de.rki.coronawarnapp.service.submission.SubmissionService import de.rki.coronawarnapp.util.DeviceUIState import de.rki.coronawarnapp.util.formatter.TestResult +import de.rki.coronawarnapp.worker.BackgroundWorkScheduler import java.util.Date object SubmissionRepository { @@ -45,6 +46,9 @@ object SubmissionRepository { val currentTime = System.currentTimeMillis() LocalData.inititalTestResultReceivedTimestamp(currentTime) testResultReceivedDate.value = Date(currentTime) + if (testResult == TestResult.PENDING) { + BackgroundWorkScheduler.startWorkScheduler() + } } else { testResultReceivedDate.value = Date(initialTestResultReceivedTimestamp) } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/TimeAndDateExtensions.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/TimeAndDateExtensions.kt index 6206d2a13..717435f25 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/TimeAndDateExtensions.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/TimeAndDateExtensions.kt @@ -51,4 +51,17 @@ object TimeAndDateExtensions { TimeUnit.MILLISECONDS.toMinutes(this) % TimeUnit.HOURS.toMinutes(1), TimeUnit.MILLISECONDS.toSeconds(this) % TimeUnit.MINUTES.toSeconds(1) ) + + /** + * Calculates the difference between two timestamps in Days Units + * + * @return Long + * + * @see TimeUnit + */ + fun calculateDays(firstDate: Long, secondDate: Long): Long { + val millionSeconds = secondDate - firstDate + var days = TimeUnit.MILLISECONDS.toDays(millionSeconds) + return days + } } 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 a36174aea..30ad926c4 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 @@ -19,6 +19,11 @@ object BackgroundConstants { */ const val DIAGNOSIS_KEY_PERIODIC_WORKER_TAG = "DIAGNOSIS_KEY_PERIODIC_WORKER" + /** + * Tag for background polling tp check test result periodic work + */ + const val DIAGNOSIS_TEST_RESULT_PERIODIC_WORKER_TAG = "DIAGNOSIS_TEST_RESULT_PERIODIC_WORKER" + /** * Unique name for diagnosis key retrieval one time work */ @@ -29,6 +34,11 @@ object BackgroundConstants { */ const val DIAGNOSIS_KEY_PERIODIC_WORK_NAME = "DiagnosisKeyBackgroundPeriodicWork" + /** + * Unique name for diagnosis test result retrieval periodic work + */ + const val DIAGNOSIS_TEST_RESULT_PERIODIC_WORK_NAME = "DiagnosisTestResultBackgroundPeriodicWork" + /** * Total minutes in one day */ @@ -46,12 +56,25 @@ object BackgroundConstants { */ const val GOOGLE_API_MAX_CALLS_PER_DAY = 20 + /** + * Total tries count for diagnosis key retrieval per day + * Internal requirement + */ + const val DIAGNOSIS_TEST_RESULT_RETRIEVAL_TRIES_PER_DAY = 12 + /** * Kind initial delay in minutes for periodic work for accessibility reason * * @see TimeUnit.MINUTES */ - const val DIAGNOSIS_KEY_PERIODIC_KIND_DELAY = 1L + const val KIND_DELAY = 1L + + /** + * Kind initial delay in minutes for periodic work for accessibility reason + * + * @see TimeUnit.SECONDS + */ + const val DIAGNOSIS_TEST_RESULT_PERIODIC_INITIAL_DELAY = 10L /** * Minimum initial delay in minutes for diagnosis key retrieval one time work @@ -89,4 +112,11 @@ object BackgroundConstants { * Retries before work would set as FAILED */ const val WORKER_RETRY_COUNT_THRESHOLD = 3 + + /** + * The maximum validity in days for keeping Background polling active + * + * @see TimeUnit.DAYS + */ + const val POLLING_VALIDITY_MAX_DAYS = 21 } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/BackgroundWorkHelper.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/BackgroundWorkHelper.kt new file mode 100644 index 000000000..b981162c4 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/BackgroundWorkHelper.kt @@ -0,0 +1,73 @@ +package de.rki.coronawarnapp.worker + +import androidx.work.Constraints +import androidx.work.NetworkType + +/** + * Singleton class for background work helper functions + * The helper uses externalised constants for readability. + * + * @see BackgroundConstants + * @see BackgroundWorkScheduler + */ +object BackgroundWorkHelper { + + /** + * Calculate the time for diagnosis key retrieval periodic work + * + * @return Long + * + * @see BackgroundConstants.MINUTES_IN_DAY + * @see getDiagnosisKeyRetrievalMaximumCalls + */ + fun getDiagnosisKeyRetrievalPeriodicWorkTimeInterval(): Long = + (BackgroundConstants.MINUTES_IN_DAY / getDiagnosisKeyRetrievalMaximumCalls()).toLong() + + /** + * Calculate the time for diagnosis key retrieval periodic work + * + * @return Long + * + * @see BackgroundConstants.MINUTES_IN_DAY + */ + fun getDiagnosisTestResultRetrievalPeriodicWorkTimeInterval(): Long = + (BackgroundConstants.MINUTES_IN_DAY / + BackgroundConstants.DIAGNOSIS_TEST_RESULT_RETRIEVAL_TRIES_PER_DAY).toLong() + + /** + * Get maximum calls count to Google API + * + * @return Long + * + * @see BackgroundConstants.DIAGNOSIS_KEY_RETRIEVAL_TRIES_PER_DAY + * @see BackgroundConstants.GOOGLE_API_MAX_CALLS_PER_DAY + */ + fun getDiagnosisKeyRetrievalMaximumCalls() = + BackgroundConstants.DIAGNOSIS_KEY_RETRIEVAL_TRIES_PER_DAY + .coerceAtMost(BackgroundConstants.GOOGLE_API_MAX_CALLS_PER_DAY) + + /** + * Constraints for diagnosis key periodic work + * Do not execute background work if battery on low level. + * + * @return Constraints + */ + fun getConstraintsForDiagnosisKeyPeriodicBackgroundWork() = + Constraints.Builder().setRequiresBatteryNotLow(true).build() + + /** + * Constraints for diagnosis key one time work + * Requires battery not low and any network connection + * Mobile data usage is handled on OS level in application settings + * + * @return Constraints + * + * @see NetworkType.CONNECTED + */ + fun getConstraintsForDiagnosisKeyOneTimeBackgroundWork() = + Constraints + .Builder() + .setRequiresBatteryNotLow(true) + .setRequiredNetworkType(NetworkType.CONNECTED) + .build() +} 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 c4e946d98..bb7bf742c 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 @@ -2,10 +2,8 @@ package de.rki.coronawarnapp.worker import android.util.Log import androidx.work.BackoffPolicy -import androidx.work.Constraints import androidx.work.ExistingPeriodicWorkPolicy import androidx.work.ExistingWorkPolicy -import androidx.work.NetworkType import androidx.work.OneTimeWorkRequestBuilder import androidx.work.Operation import androidx.work.PeriodicWorkRequestBuilder @@ -13,6 +11,7 @@ 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 org.joda.time.DateTime import org.joda.time.DateTimeZone import org.joda.time.Instant @@ -21,9 +20,10 @@ import java.util.concurrent.TimeUnit /** * Singleton class for background work handling - * The helper uses externalised constants for readability. + * The helper uses externalised constants and helper for readability. * * @see BackgroundConstants + * @see BackgroundWorkHelper */ object BackgroundWorkScheduler { @@ -36,10 +36,12 @@ object BackgroundWorkScheduler { * * @see BackgroundConstants.DIAGNOSIS_KEY_ONE_TIME_WORKER_TAG * @see BackgroundConstants.DIAGNOSIS_KEY_PERIODIC_WORKER_TAG + * @see BackgroundConstants.DIAGNOSIS_TEST_RESULT_PERIODIC_WORKER_TAG */ enum class WorkTag(val tag: String) { DIAGNOSIS_KEY_RETRIEVAL_ONE_TIME_WORKER(BackgroundConstants.DIAGNOSIS_KEY_ONE_TIME_WORKER_TAG), - DIAGNOSIS_KEY_RETRIEVAL_PERIODIC_WORKER(BackgroundConstants.DIAGNOSIS_KEY_PERIODIC_WORKER_TAG) + DIAGNOSIS_KEY_RETRIEVAL_PERIODIC_WORKER(BackgroundConstants.DIAGNOSIS_KEY_PERIODIC_WORKER_TAG), + DIAGNOSIS_TEST_RESULT_RETRIEVAL_PERIODIC_WORKER(BackgroundConstants.DIAGNOSIS_TEST_RESULT_PERIODIC_WORKER_TAG) } /** @@ -49,35 +51,14 @@ object BackgroundWorkScheduler { * * @see BackgroundConstants.DIAGNOSIS_KEY_ONE_TIME_WORK_NAME * @see BackgroundConstants.DIAGNOSIS_KEY_PERIODIC_WORK_NAME + * @see BackgroundConstants.DIAGNOSIS_TEST_RESULT_PERIODIC_WORK_NAME */ enum class WorkType(val uniqueName: String) { DIAGNOSIS_KEY_BACKGROUND_ONE_TIME_WORK(BackgroundConstants.DIAGNOSIS_KEY_ONE_TIME_WORK_NAME), - DIAGNOSIS_KEY_BACKGROUND_PERIODIC_WORK(BackgroundConstants.DIAGNOSIS_KEY_PERIODIC_WORK_NAME) + DIAGNOSIS_KEY_BACKGROUND_PERIODIC_WORK(BackgroundConstants.DIAGNOSIS_KEY_PERIODIC_WORK_NAME), + DIAGNOSIS_TEST_RESULT_PERIODIC_WORKER(BackgroundConstants.DIAGNOSIS_TEST_RESULT_PERIODIC_WORK_NAME) } - /** - * Calculate the time for diagnosis key retrieval periodic work - * - * @return Long - * - * @see BackgroundConstants.MINUTES_IN_DAY - * @see getDiagnosisKeyRetrievalMaximumCalls - */ - private fun getDiagnosisKeyRetrievalPeriodicWorkTimeInterval(): Long = - (BackgroundConstants.MINUTES_IN_DAY / getDiagnosisKeyRetrievalMaximumCalls()).toLong() - - /** - * Get maximum calls count to Google API - * - * @return Long - * - * @see BackgroundConstants.DIAGNOSIS_KEY_RETRIEVAL_TRIES_PER_DAY - * @see BackgroundConstants.GOOGLE_API_MAX_CALLS_PER_DAY - */ - private fun getDiagnosisKeyRetrievalMaximumCalls() = - BackgroundConstants.DIAGNOSIS_KEY_RETRIEVAL_TRIES_PER_DAY - .coerceAtMost(BackgroundConstants.GOOGLE_API_MAX_CALLS_PER_DAY) - /** * Work manager instance */ @@ -86,7 +67,9 @@ object BackgroundWorkScheduler { /** * Start work scheduler * Checks if periodic worker was already scheduled. If not - reschedule it again. + * For [WorkTag.DIAGNOSIS_TEST_RESULT_RETRIEVAL_PERIODIC_WORKER] also checks if User is Registered * + * @see LocalData.registrationToken * @see isWorkActive */ fun startWorkScheduler() { @@ -95,9 +78,27 @@ object BackgroundWorkScheduler { WorkTag.DIAGNOSIS_KEY_RETRIEVAL_PERIODIC_WORKER.tag, isPeriodicWorkActive ) - if (!isPeriodicWorkActive) WorkType.DIAGNOSIS_KEY_BACKGROUND_PERIODIC_WORK.start() + if (!isPeriodicWorkActive) { + WorkType.DIAGNOSIS_KEY_BACKGROUND_PERIODIC_WORK.start() + } + if (!isWorkActive(WorkTag.DIAGNOSIS_TEST_RESULT_RETRIEVAL_PERIODIC_WORKER.tag) && + LocalData.registrationToken() != null && !LocalData.isTestResultNotificationSent() + ) { + WorkType.DIAGNOSIS_TEST_RESULT_PERIODIC_WORKER.start() + LocalData.initialPollingForTestResultTimeStamp(System.currentTimeMillis()) + } } + /** + * Stop work by unique name + * + * @return Operation + * + * @see WorkType + */ + fun WorkType.stop(): Operation = + workManager.cancelUniqueWork(this.uniqueName) + /** * Checks if defined work is active * Non-active means worker was Cancelled, Failed or have not been enqueued at all @@ -167,6 +168,7 @@ object BackgroundWorkScheduler { private fun WorkType.start(): Operation = when (this) { WorkType.DIAGNOSIS_KEY_BACKGROUND_PERIODIC_WORK -> enqueueDiagnosisKeyBackgroundPeriodicWork() WorkType.DIAGNOSIS_KEY_BACKGROUND_ONE_TIME_WORK -> enqueueDiagnosisKeyBackgroundOneTimeWork() + WorkType.DIAGNOSIS_TEST_RESULT_PERIODIC_WORKER -> enqueueDiagnosisTestResultBackgroundPeriodicWork() } /** @@ -197,6 +199,22 @@ object BackgroundWorkScheduler { buildDiagnosisKeyRetrievalOneTimeWork() ).also { it.logOperationSchedule(WorkType.DIAGNOSIS_KEY_BACKGROUND_ONE_TIME_WORK) } + /** + * Enqueue diagnosis Test Result periodic + * Show a Notification when new Test Results are in. + * Replace with new if older work exists. + * + * @return Operation + * + * @see WorkType.DIAGNOSIS_TEST_RESULT_PERIODIC_WORKER + */ + private fun enqueueDiagnosisTestResultBackgroundPeriodicWork() = + workManager.enqueueUniquePeriodicWork( + WorkType.DIAGNOSIS_TEST_RESULT_PERIODIC_WORKER.uniqueName, + ExistingPeriodicWorkPolicy.REPLACE, + buildDiagnosisTestResultRetrievalPeriodicWork() + ).also { it.logOperationSchedule(WorkType.DIAGNOSIS_TEST_RESULT_PERIODIC_WORKER) } + /** * Build diagnosis key periodic work request * Set "kind delay" for accessibility reason. @@ -205,22 +223,22 @@ object BackgroundWorkScheduler { * @return PeriodicWorkRequest * * @see WorkTag.DIAGNOSIS_KEY_RETRIEVAL_PERIODIC_WORKER - * @see BackgroundConstants.DIAGNOSIS_KEY_PERIODIC_KIND_DELAY + * @see BackgroundConstants.KIND_DELAY * @see BackoffPolicy.LINEAR */ private fun buildDiagnosisKeyRetrievalPeriodicWork() = PeriodicWorkRequestBuilder<DiagnosisKeyRetrievalPeriodicWorker>( - getDiagnosisKeyRetrievalPeriodicWorkTimeInterval(), TimeUnit.MINUTES + BackgroundWorkHelper.getDiagnosisKeyRetrievalPeriodicWorkTimeInterval(), TimeUnit.MINUTES ) .addTag(WorkTag.DIAGNOSIS_KEY_RETRIEVAL_PERIODIC_WORKER.tag) - .setConstraints(getConstraintsForDiagnosisKeyPeriodicBackgroundWork()) + .setConstraints(BackgroundWorkHelper.getConstraintsForDiagnosisKeyPeriodicBackgroundWork()) .setInitialDelay( - BackgroundConstants.DIAGNOSIS_KEY_PERIODIC_KIND_DELAY, + BackgroundConstants.KIND_DELAY, TimeUnit.MINUTES ) .setBackoffCriteria( BackoffPolicy.LINEAR, - BackgroundConstants.DIAGNOSIS_KEY_PERIODIC_KIND_DELAY, + BackgroundConstants.KIND_DELAY, TimeUnit.MINUTES ) .build() @@ -234,13 +252,13 @@ object BackgroundWorkScheduler { * * @see WorkTag.DIAGNOSIS_KEY_RETRIEVAL_ONE_TIME_WORKER * @see buildDiagnosisKeyRetrievalOneTimeWork - * @see BackgroundConstants.DIAGNOSIS_KEY_PERIODIC_KIND_DELAY + * @see BackgroundConstants.KIND_DELAY * @see BackoffPolicy.LINEAR */ private fun buildDiagnosisKeyRetrievalOneTimeWork() = OneTimeWorkRequestBuilder<DiagnosisKeyRetrievalOneTimeWorker>() .addTag(WorkTag.DIAGNOSIS_KEY_RETRIEVAL_ONE_TIME_WORKER.tag) - .setConstraints(getConstraintsForDiagnosisKeyOneTimeBackgroundWork()) + .setConstraints(BackgroundWorkHelper.getConstraintsForDiagnosisKeyOneTimeBackgroundWork()) .setInitialDelay( DiagnosisKeyRetrievalTimeCalculator.generateDiagnosisKeyRetrievalOneTimeWorkRandomDuration( DateTime( @@ -251,50 +269,59 @@ object BackgroundWorkScheduler { ) .setBackoffCriteria( BackoffPolicy.LINEAR, - BackgroundConstants.DIAGNOSIS_KEY_PERIODIC_KIND_DELAY, + BackgroundConstants.KIND_DELAY, TimeUnit.MINUTES ) .build() /** - * Constraints for diagnosis key periodic work - * Do not execute background work if battery on low level. - * - * @return Constraints - */ - private fun getConstraintsForDiagnosisKeyPeriodicBackgroundWork() = - Constraints.Builder().setRequiresBatteryNotLow(true).build() - - /** - * Constraints for diagnosis key one time work - * Requires battery not low and any network connection - * Mobile data usage is handled on OS level in application settings + * Build diagnosis Test Result periodic work request + * Set "kind delay" for accessibility reason. * - * @return Constraints + * @return PeriodicWorkRequest * - * @see NetworkType.CONNECTED + * @see WorkTag.DIAGNOSIS_TEST_RESULT_RETRIEVAL_PERIODIC_WORKER + * @see BackgroundConstants.KIND_DELAY */ - private fun getConstraintsForDiagnosisKeyOneTimeBackgroundWork() = - Constraints - .Builder() - .setRequiresBatteryNotLow(true) - .setRequiredNetworkType(NetworkType.CONNECTED) + private fun buildDiagnosisTestResultRetrievalPeriodicWork() = + PeriodicWorkRequestBuilder<DiagnosisTestResultRetrievalPeriodicWorker>( + BackgroundWorkHelper.getDiagnosisTestResultRetrievalPeriodicWorkTimeInterval(), TimeUnit.MINUTES + ) + .addTag(WorkTag.DIAGNOSIS_TEST_RESULT_RETRIEVAL_PERIODIC_WORKER.tag) + .setConstraints(BackgroundWorkHelper.getConstraintsForDiagnosisKeyOneTimeBackgroundWork()) + .setInitialDelay( + BackgroundConstants.DIAGNOSIS_TEST_RESULT_PERIODIC_INITIAL_DELAY, + TimeUnit.SECONDS + ).setBackoffCriteria( + BackoffPolicy.LINEAR, + BackgroundConstants.KIND_DELAY, + TimeUnit.MINUTES + ) .build() /** * Log operation schedule */ - private fun Operation.logOperationSchedule(workType: WorkType) = this.result.addListener({ - if (BuildConfig.DEBUG) Log.d(TAG, "${workType.uniqueName} completed.") - }, { it.run() }).also { if (BuildConfig.DEBUG) Log.d(TAG, "${workType.uniqueName} scheduled.") } + private fun Operation.logOperationSchedule(workType: WorkType) = + this.result.addListener({ + if (BuildConfig.DEBUG) Log.d( + TAG, + "${workType.uniqueName} completed." + ) + }, { it.run() }) + .also { if (BuildConfig.DEBUG) Log.d(TAG, "${workType.uniqueName} scheduled.") } /** * Log operation cancellation */ - private fun Operation.logOperationCancelByTag(workTag: WorkTag) = this.result.addListener({ - if (BuildConfig.DEBUG) Log.d(TAG, "All work with tag ${workTag.tag} canceled.") - }, { it.run() }) - .also { if (BuildConfig.DEBUG) Log.d(TAG, "Canceling all work with tag ${workTag.tag}") } + private fun Operation.logOperationCancelByTag(workTag: WorkTag) = + this.result.addListener({ + if (BuildConfig.DEBUG) Log.d( + TAG, + "All work with tag ${workTag.tag} canceled." + ) + }, { it.run() }) + .also { if (BuildConfig.DEBUG) Log.d(TAG, "Canceling all work with tag ${workTag.tag}") } /** * Log work active status diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/DiagnosisTestResultRetrievalPeriodicWorker.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/DiagnosisTestResultRetrievalPeriodicWorker.kt new file mode 100644 index 000000000..e1b40d62d --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/DiagnosisTestResultRetrievalPeriodicWorker.kt @@ -0,0 +1,116 @@ +package de.rki.coronawarnapp.worker + +import android.content.Context +import android.util.Log +import androidx.core.app.NotificationCompat +import androidx.work.CoroutineWorker +import androidx.work.WorkerParameters +import de.rki.coronawarnapp.BuildConfig +import de.rki.coronawarnapp.CoronaWarnApplication +import de.rki.coronawarnapp.R +import de.rki.coronawarnapp.notification.NotificationHelper +import de.rki.coronawarnapp.service.submission.SubmissionService +import de.rki.coronawarnapp.storage.LocalData +import de.rki.coronawarnapp.util.TimeAndDateExtensions +import de.rki.coronawarnapp.util.formatter.TestResult +import de.rki.coronawarnapp.worker.BackgroundWorkScheduler.stop + +/** + * Diagnosis Test Result Periodic retrieavl + * + * @see BackgroundWorkScheduler + */ +class DiagnosisTestResultRetrievalPeriodicWorker( + val context: Context, + workerParams: WorkerParameters +) : + CoroutineWorker(context, workerParams) { + + companion object { + private val TAG: String? = DiagnosisTestResultRetrievalPeriodicWorker::class.simpleName + } + + /** + * Work execution + * + * If background job is running for less than 21 days, testResult is checked. + * If the job is running for more than 21 days, the job will be stopped + * + * @return Result + * + * @see LocalData.isTestResultNotificationSent + * @see LocalData.initialPollingForTestResultTimeStamp + */ + override suspend fun doWork(): Result { + + 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 { + if (TimeAndDateExtensions.calculateDays( + LocalData.initialPollingForTestResultTimeStamp(), + System.currentTimeMillis() + ) < BackgroundConstants.POLLING_VALIDITY_MAX_DAYS + ) { + val testResult = SubmissionService.asyncRequestTestResult() + initiateNotification(testResult) + } else { + stopWorker() + } + } catch (e: Exception) { + result = Result.retry() + } + return result + } + + /** + * Notification Initiation + * + * If the returned Test Result is Negative, Positive or Invalid + * The Background polling will be stopped + * and a notification is shown, but only if the App is not in foreground + * + * @see LocalData.isTestResultNotificationSent + * @see LocalData.initialPollingForTestResultTimeStamp + * @see TestResult + */ + private fun initiateNotification(testResult: TestResult) { + if (testResult == TestResult.NEGATIVE || testResult == TestResult.POSITIVE || + testResult == TestResult.INVALID + ) { + if (!CoronaWarnApplication.isAppInForeground) { + NotificationHelper.sendNotification( + CoronaWarnApplication.getAppContext() + .getString(R.string.notification_name), CoronaWarnApplication.getAppContext() + .getString(R.string.notification_body), + NotificationCompat.PRIORITY_HIGH + ) + } + LocalData.isTestResultNotificationSent(true) + stopWorker() + } + } + + /** + * Stops the Background Polling worker + * + * @see LocalData.initialPollingForTestResultTimeStamp + * @see BackgroundWorkScheduler.stop + + */ + private fun stopWorker() { + LocalData.initialPollingForTestResultTimeStamp(0L) + BackgroundWorkScheduler.WorkType.DIAGNOSIS_TEST_RESULT_PERIODIC_WORKER.stop() + } +} diff --git a/Corona-Warn-App/src/main/res/values/strings.xml b/Corona-Warn-App/src/main/res/values/strings.xml index 62f103e42..3e6c8c43f 100644 --- a/Corona-Warn-App/src/main/res/values/strings.xml +++ b/Corona-Warn-App/src/main/res/values/strings.xml @@ -126,6 +126,14 @@ <xliff:g id="preference">preference_last_three_hours_from_server</xliff:g> </string> + <string name="preference_polling_test_result_started"> + <xliff:g id="preference">preference_polling_test_result_started</xliff:g> + </string> + + <string name="preference_test_result_notification"> + <xliff:g id="preference">preference_test_result_notification</xliff:g> + </string> + <!-- #################################### Generics ###################################### --> -- GitLab