diff --git a/Corona-Warn-App/build.gradle b/Corona-Warn-App/build.gradle index 4f12c7cb116..21a4222baf2 100644 --- a/Corona-Warn-App/build.gradle +++ b/Corona-Warn-App/build.gradle @@ -219,6 +219,7 @@ dependencies { androidTestImplementation 'androidx.test:rules:1.2.0' androidTestImplementation 'androidx.test.ext:truth:1.2.0' androidTestImplementation 'androidx.test.ext:junit:1.1.1' + androidTestImplementation 'androidx.work:work-testing:2.3.4' androidTestImplementation "io.mockk:mockk-android:1.10.0" debugImplementation 'androidx.fragment:fragment-testing:1.2.4' diff --git a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/worker/DiagnosisKeyRetrievalOneTimeWorkerTest.kt b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/worker/DiagnosisKeyRetrievalOneTimeWorkerTest.kt new file mode 100644 index 00000000000..92c26746805 --- /dev/null +++ b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/worker/DiagnosisKeyRetrievalOneTimeWorkerTest.kt @@ -0,0 +1,71 @@ +package de.rki.coronawarnapp.worker + +import android.content.Context +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.work.ListenableWorker + +import androidx.work.testing.TestListenableWorkerBuilder +import de.rki.coronawarnapp.transaction.RetrieveDiagnosisKeysTransaction +import io.mockk.Runs +import io.mockk.coEvery +import io.mockk.just +import io.mockk.mockkObject +import org.hamcrest.CoreMatchers.`is` +import org.hamcrest.MatcherAssert.assertThat +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +/** + * DiagnosisKeyRetrievalOneTimeWorker test. + */ +@RunWith(AndroidJUnit4::class) +class DiagnosisKeyRetrievalOneTimeWorkerTest { + private lateinit var context: Context + private lateinit var worker: ListenableWorker + + @Before + fun setUp() { + context = ApplicationProvider.getApplicationContext() + mockkObject(RetrieveDiagnosisKeysTransaction) + } + + /** + * Test worker result = [ListenableWorker.Result.success] + */ + @Test + fun testDiagnosisKeyRetrievalOneTimeWorkerSuccess() { + worker = + TestListenableWorkerBuilder(context).build() + coEvery { RetrieveDiagnosisKeysTransaction.start() } just Runs + val result = worker.startWork().get() + assertThat(result, `is`(ListenableWorker.Result.success())) + } + + /** + * Test worker result = [ListenableWorker.Result.retry] + */ + @Test + fun testDiagnosisKeyRetrievalOneTimeWorkerRetry() { + worker = + TestListenableWorkerBuilder(context).build() + coEvery { RetrieveDiagnosisKeysTransaction.start() } throws Exception("test exception") + val result = worker.startWork().get() + assertThat(result, `is`(ListenableWorker.Result.retry())) + } + + /** + * Test worker result = [ListenableWorker.Result.failure] + * Check [BackgroundConstants.WORKER_RETRY_COUNT_THRESHOLD] for proper attempt count. + * + * @see [BackgroundConstants.WORKER_RETRY_COUNT_THRESHOLD] + */ + @Test + fun testDiagnosisKeyRetrievalOneTimeWorkerFailed() { + worker = + TestListenableWorkerBuilder(context).setRunAttemptCount(5).build() + val result = worker.startWork().get() + assertThat(result, `is`(ListenableWorker.Result.failure())) + } +} diff --git a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/worker/DiagnosisKeyRetrievalPeriodicWorkerTest.kt b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/worker/DiagnosisKeyRetrievalPeriodicWorkerTest.kt new file mode 100644 index 00000000000..7005e07b3c0 --- /dev/null +++ b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/worker/DiagnosisKeyRetrievalPeriodicWorkerTest.kt @@ -0,0 +1,148 @@ +package de.rki.coronawarnapp.worker + +import android.content.Context +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.work.WorkInfo +import androidx.work.WorkManager +import androidx.work.WorkRequest +import androidx.work.testing.TestDriver +import androidx.work.testing.WorkManagerTestInitHelper +import io.mockk.Runs +import io.mockk.every +import io.mockk.just +import io.mockk.mockkObject +import io.mockk.unmockkAll +import org.hamcrest.CoreMatchers.`is` +import org.hamcrest.CoreMatchers.notNullValue +import org.hamcrest.CoreMatchers.nullValue +import org.hamcrest.MatcherAssert.assertThat +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +/** + * DiagnosisKeyRetrievalPeriodicWorker test. + */ +@RunWith(AndroidJUnit4::class) +class DiagnosisKeyRetrievalPeriodicWorkerTest { + private lateinit var context: Context + private lateinit var workManager: WorkManager + private lateinit var request: WorkRequest + private lateinit var request2: WorkRequest + // small delay because WorkManager does not run work instantly when delay is off + private val delay = 500L + + @Before + fun setUp() { + mockkObject(BackgroundWorkScheduler) + // do not init Test WorkManager instance again between tests + // leads to all tests instead of first one to fail + context = ApplicationProvider.getApplicationContext() + if (WorkManager.getInstance(context) !is TestDriver) { + WorkManagerTestInitHelper.initializeTestWorkManager(context) + } + workManager = WorkManager.getInstance(context) + + every { BackgroundWorkScheduler["buildDiagnosisKeyRetrievalPeriodicWork"]() } answers { + request = this.callOriginal() as WorkRequest + request + } + } + + /** + * Test worker for success. + */ + @Test + fun testDiagnosisKeyRetrievalPeriodicWorkerSuccess() { + every { BackgroundWorkScheduler.scheduleDiagnosisKeyOneTimeWork() } just Runs + + BackgroundWorkScheduler.scheduleDiagnosisKeyPeriodicWork() + + assertThat(request, notNullValue()) + + var workInfo = workManager.getWorkInfoById(request.id).get() + assertThat(workInfo, notNullValue()) + assertThat(workInfo.state, `is`((WorkInfo.State.ENQUEUED))) + + runPeriodicJobInitialDelayMet() + assertThat(request, notNullValue()) + workInfo = workManager.getWorkInfoById(request.id).get() + assertThat(workInfo.runAttemptCount, `is`(0)) + + runPeriodicJobPeriodDelayMet() + assertThat(request, notNullValue()) + workInfo = workManager.getWorkInfoById(request.id).get() + assertThat(workInfo.runAttemptCount, `is`(0)) + } + + /** + * Test worker for retries and fail. + */ + @Test + fun testDiagnosisKeyRetrievalPeriodicWorkerRetryAndFail() { + every { BackgroundWorkScheduler.scheduleDiagnosisKeyOneTimeWork() } throws Exception("test exception") + + BackgroundWorkScheduler.scheduleDiagnosisKeyPeriodicWork() + + assertThat(request, notNullValue()) + var workInfo = workManager.getWorkInfoById(request.id).get() + assertThat(workInfo, notNullValue()) + assertThat(workInfo.state, `is`((WorkInfo.State.ENQUEUED))) + + for (i in 1..BackgroundConstants.WORKER_RETRY_COUNT_THRESHOLD + 2) { + // run job i times + when (i) { + BackgroundConstants.WORKER_RETRY_COUNT_THRESHOLD + 2 -> { + every { BackgroundWorkScheduler["buildDiagnosisKeyRetrievalPeriodicWork"]() } answers { + request2 = this.callOriginal() as WorkRequest + request2 + } + runPeriodicJobInitialDelayMet() + } + else -> { + runPeriodicJobInitialDelayMet() + } + } + + // get job run #i result + when (i) { + BackgroundConstants.WORKER_RETRY_COUNT_THRESHOLD + 2 -> { + assertThat(request, notNullValue()) + assertThat(request2, notNullValue()) + workInfo = workManager.getWorkInfoById(request.id).get() + val workInfo2 = workManager.getWorkInfoById(request2.id).get() + assertThat(workInfo, nullValue()) + assertThat(workInfo2.state, `is`(WorkInfo.State.ENQUEUED)) + assertThat(workInfo2.runAttemptCount, `is`(0)) + } + else -> { + assertThat(request, notNullValue()) + workInfo = workManager.getWorkInfoById(request.id).get() + assertThat(workInfo.runAttemptCount, `is`(i)) + } + } + } + } + + @After + fun cleanUp() { + workManager.cancelAllWork() + unmockkAll() + } + + private fun runPeriodicJobInitialDelayMet() { + val testDriver = WorkManagerTestInitHelper.getTestDriver(context) + testDriver?.setAllConstraintsMet(request.id) + testDriver?.setInitialDelayMet(request.id) + Thread.sleep(delay) + } + + private fun runPeriodicJobPeriodDelayMet() { + val testDriver = WorkManagerTestInitHelper.getTestDriver(context) + testDriver?.setAllConstraintsMet(request.id) + testDriver?.setPeriodDelayMet(request.id) + Thread.sleep(delay) + } +} diff --git a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/worker/DiagnosisTestResultRetrievalPeriodicWorkerTest.kt b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/worker/DiagnosisTestResultRetrievalPeriodicWorkerTest.kt new file mode 100644 index 00000000000..c610d634556 --- /dev/null +++ b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/worker/DiagnosisTestResultRetrievalPeriodicWorkerTest.kt @@ -0,0 +1,202 @@ +package de.rki.coronawarnapp.worker + +import android.content.Context +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.work.WorkInfo +import androidx.work.WorkManager +import androidx.work.WorkRequest +import androidx.work.testing.TestDriver +import androidx.work.testing.WorkManagerTestInitHelper +import de.rki.coronawarnapp.service.submission.SubmissionService +import de.rki.coronawarnapp.storage.LocalData +import de.rki.coronawarnapp.util.TimeAndDateExtensions.daysToMilliseconds +import de.rki.coronawarnapp.util.formatter.TestResult +import io.mockk.coEvery +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkObject +import io.mockk.slot +import io.mockk.unmockkAll +import org.hamcrest.CoreMatchers.`is` +import org.hamcrest.CoreMatchers.notNullValue +import org.hamcrest.MatcherAssert.assertThat +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import java.util.Date + +/** + * DiagnosisTestResultRetrievalPeriodicWorker test. + */ +@RunWith(AndroidJUnit4::class) +class DiagnosisTestResultRetrievalPeriodicWorkerTest { + private lateinit var context: Context + private lateinit var workManager: WorkManager + private lateinit var request: WorkRequest + private lateinit var request2: WorkRequest + // small delay because WorkManager does not run work instantly when delay is off + private val delay = 500L + + @Before + fun setUp() { + LocalData.registrationToken("test token") + LocalData.isTestResultNotificationSent(false) + mockkObject(LocalData) + mockkObject(SubmissionService) + mockkObject(BackgroundWorkScheduler) + + // do not init Test WorkManager instance again between tests + // leads to all tests instead of first one to fail + context = ApplicationProvider.getApplicationContext() + if (WorkManager.getInstance(context) !is TestDriver) { + WorkManagerTestInitHelper.initializeTestWorkManager(context) + } + workManager = WorkManager.getInstance(context) + + every { BackgroundWorkScheduler["buildDiagnosisTestResultRetrievalPeriodicWork"]() } answers { + request = this.callOriginal() as WorkRequest + request + } + + val slot = slot() + every { BackgroundWorkScheduler["isWorkActive"](capture(slot)) } answers { + !(slot.isCaptured && slot.captured == + BackgroundWorkScheduler.WorkTag.DIAGNOSIS_TEST_RESULT_RETRIEVAL_PERIODIC_WORKER.tag) + } + } + + /** + * Test worker for running more than a set amount of days. + * + * @see [BackgroundConstants.POLLING_VALIDITY_MAX_DAYS] + */ + @Test + fun testDiagnosisTestResultRetrievalPeriodicWorkerCancel() { + val past = System.currentTimeMillis() - + (BackgroundConstants.POLLING_VALIDITY_MAX_DAYS.toLong() + 1).daysToMilliseconds() + testDiagnosisTestResultRetrievalPeriodicWorkerForResult(mockk(), past, true) + } + + /** + * Test worker for running less than a set amount of days, [TestResult.PENDING]. + * + * @see [BackgroundConstants.POLLING_VALIDITY_MAX_DAYS] + */ + @Test + fun testDiagnosisTestResultRetrievalPeriodicWorkerPending() { + val past = Date().time - (BackgroundConstants.POLLING_VALIDITY_MAX_DAYS.toLong() - 1).daysToMilliseconds() + testDiagnosisTestResultRetrievalPeriodicWorkerForResult(TestResult.PENDING, past) + } + + /** + * Test worker for running less than a set amount of days, [TestResult.NEGATIVE] . + * + * @see [BackgroundConstants.POLLING_VALIDITY_MAX_DAYS] + */ + @Test + fun testDiagnosisTestResultRetrievalPeriodicWorkerSuccessNegative() { + val past = Date().time - (BackgroundConstants.POLLING_VALIDITY_MAX_DAYS.toLong() - 1).daysToMilliseconds() + testDiagnosisTestResultRetrievalPeriodicWorkerForResult(TestResult.NEGATIVE, past) + } + + /** + * Test worker for running less than a set amount of days, [TestResult.POSITIVE] . + * + * @see [BackgroundConstants.POLLING_VALIDITY_MAX_DAYS] + */ + @Test + fun testDiagnosisTestResultRetrievalPeriodicWorkerSuccessPositive() { + val past = Date().time - (BackgroundConstants.POLLING_VALIDITY_MAX_DAYS.toLong() - 1).daysToMilliseconds() + testDiagnosisTestResultRetrievalPeriodicWorkerForResult(TestResult.POSITIVE, past) + } + + /** + * Test worker for retries and fail. + */ + @Test + fun testDiagnosisTestResultRetrievalPeriodicWorkerRetryAndFail() { + val past = Date().time - (BackgroundConstants.POLLING_VALIDITY_MAX_DAYS.toLong() - 1).daysToMilliseconds() + coEvery { SubmissionService.asyncRequestTestResult() } throws Exception("test exception") + every { LocalData.initialPollingForTestResultTimeStamp() } returns past + + BackgroundWorkScheduler.startWorkScheduler() + + assertThat(request, notNullValue()) + var workInfo = workManager.getWorkInfoById(request.id).get() + assertThat(workInfo, notNullValue()) + assertThat(workInfo.state, `is`((WorkInfo.State.ENQUEUED))) + + for (i in 1..BackgroundConstants.WORKER_RETRY_COUNT_THRESHOLD + 2) { + // run job i times + runPeriodicJobInitialDelayMet() + + // get job run #i result + when (i) { + BackgroundConstants.WORKER_RETRY_COUNT_THRESHOLD + 2 -> { + assertThat(request, notNullValue()) + workInfo = workManager.getWorkInfoById(request.id).get() + assertThat(workInfo.runAttemptCount, `is`(0)) + } + else -> { + assertThat(request, notNullValue()) + workInfo = workManager.getWorkInfoById(request.id).get() + assertThat(workInfo.runAttemptCount, `is`(i)) + } + } + } + } + + @After + fun cleanUp() { + workManager.cancelAllWork() + unmockkAll() + } + + private fun testDiagnosisTestResultRetrievalPeriodicWorkerForResult( + result: TestResult, + past: Long, + isCancelTest: Boolean = false + ) { + coEvery { SubmissionService.asyncRequestTestResult() } returns result + every { LocalData.initialPollingForTestResultTimeStamp() } returns past + + BackgroundWorkScheduler.startWorkScheduler() + + assertThat(request, notNullValue()) + + val workInfo = workManager.getWorkInfoById(request.id).get() + assertThat(workInfo, notNullValue()) + assertThat(workInfo.state, `is`((WorkInfo.State.ENQUEUED))) + runPeriodicJobInitialDelayMet() + verifyTestResult(result, isCancelTest) + } + + private fun verifyTestResult(result: TestResult, isCancelTest: Boolean) { + assertThat(request, notNullValue()) + val workInfo = workManager.getWorkInfoById(request.id).get() + if (isCancelTest) { + assertThat(workInfo.state, `is`((WorkInfo.State.CANCELLED))) + assertThat(LocalData.isTestResultNotificationSent(), `is`(false)) + } else { + when (result) { + TestResult.POSITIVE, TestResult.NEGATIVE, TestResult.INVALID -> { + assertThat(workInfo.state, `is`((WorkInfo.State.CANCELLED))) + assertThat(LocalData.isTestResultNotificationSent(), `is`(true)) + } + TestResult.PENDING -> { + assertThat(workInfo.runAttemptCount, `is`(0)) + assertThat(workInfo.state, `is`((WorkInfo.State.ENQUEUED))) + } + } + } + } + + private fun runPeriodicJobInitialDelayMet() { + val testDriver = WorkManagerTestInitHelper.getTestDriver(context) + testDriver?.setAllConstraintsMet(request.id) + testDriver?.setInitialDelayMet(request.id) + Thread.sleep(delay) + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/CoronaWarnApplication.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/CoronaWarnApplication.kt index 2dddb152716..3d2291df2fe 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/CoronaWarnApplication.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/CoronaWarnApplication.kt @@ -7,6 +7,7 @@ import android.content.Context import android.content.IntentFilter import android.content.pm.ActivityInfo import android.os.Bundle +import android.view.WindowManager import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleObserver import androidx.lifecycle.OnLifecycleEvent @@ -94,13 +95,15 @@ class CoronaWarnApplication : Application(), LifecycleObserver, @SuppressLint("SourceLockedOrientationActivity") override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) { - // prevents screenshot of the app for all activities - // TODO temporarily removed screenshot prevention for testing purposes - /* - activity.window.setFlags( - WindowManager.LayoutParams.FLAG_SECURE, - WindowManager.LayoutParams.FLAG_SECURE - )*/ + // prevents screenshot of the app for all activities, + // except for deviceForTesters build flavor, which is used for testing + if (BuildConfig.FLAVOR != "deviceForTesters") { + activity.window.setFlags( + WindowManager.LayoutParams.FLAG_SECURE, + WindowManager.LayoutParams.FLAG_SECURE + ) + } + // set screen orientation to portrait activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_USER_PORTRAIT } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/exception/NotEnoughSpaceOnDiskException.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/exception/NotEnoughSpaceOnDiskException.kt new file mode 100644 index 00000000000..28ae9a03995 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/exception/NotEnoughSpaceOnDiskException.kt @@ -0,0 +1,12 @@ +package de.rki.coronawarnapp.exception + +import de.rki.coronawarnapp.R +import de.rki.coronawarnapp.exception.reporting.ErrorCodes +import de.rki.coronawarnapp.exception.reporting.ReportedException + +class NotEnoughSpaceOnDiskException(cause: Throwable? = null) : ReportedException( + ErrorCodes.NOT_ENOUGH_AVAILABLE_SPACE_ON_DISK.code, + "the app detected that not enough storage space is available for the required operation", + cause, + R.string.errors_not_enough_device_storage +) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/exception/reporting/ErrorCodes.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/exception/reporting/ErrorCodes.kt index 9f62362d14a..5b89a01d3dd 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/exception/reporting/ErrorCodes.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/exception/reporting/ErrorCodes.kt @@ -20,5 +20,6 @@ enum class ErrorCodes(val code: Int) { // NONTECHNICAL NO_NETWORK_CONNECTIVITY(1), + NOT_ENOUGH_AVAILABLE_SPACE_ON_DISK(2), EXTERNAL_NAVIGATION(10), } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/FileStorageHelper.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/FileStorageHelper.kt index f1844b19fff..44442e9e61d 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/FileStorageHelper.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/FileStorageHelper.kt @@ -1,8 +1,13 @@ package de.rki.coronawarnapp.storage +import android.content.Context +import android.os.Build +import android.os.storage.StorageManager import de.rki.coronawarnapp.CoronaWarnApplication +import de.rki.coronawarnapp.exception.NotEnoughSpaceOnDiskException import timber.log.Timber import java.io.File +import java.util.UUID import java.util.concurrent.TimeUnit /** @@ -38,34 +43,23 @@ object FileStorageHelper { * Checks if internal store has free memory. * Threshold: FileStorageConstants.FREE_SPACE_THRESHOLD * Bound to .usableSpace due to API level restrictions (minimum required level - 23) - * - * TODO Check with UX team to handle insufficient space flow */ fun checkFileStorageFreeSpace() { - val availableSpace = (keyExportDirectory.usableSpace / BYTES) - .also { logAvailableSpace(it) } - + val availableSpace = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val storageManager = CoronaWarnApplication.getAppContext() + .getSystemService(Context.STORAGE_SERVICE) as StorageManager + val storageVolume = storageManager.primaryStorageVolume + val storageUUID = + UUID.fromString(storageVolume.uuid ?: StorageManager.UUID_DEFAULT.toString()) + storageManager.getAllocatableBytes(storageUUID) / BYTES + } else { + keyExportDirectory.usableSpace / BYTES + } if (availableSpace < FileStorageConstants.FREE_SPACE_THRESHOLD) { - logInsufficientSpace(availableSpace) + throw NotEnoughSpaceOnDiskException() } } - /** - * Remove outdated key files from internal storage. - * Threshold: FileStorageConstants.DAYS_TO_KEEP - */ - fun removeOutdatedFilesFromStorage() { - keyExportDirectory - .walk() - .filter(File::isDirectory) - .forEach { file: File -> - Unit - if (file != keyExportDirectory && file.isOutdated()) { - file.checkAndRemove().also { logFileRemovalResult(file.name, it) } - } - } - } - fun getAllFilesInKeyExportDirectory(): List { return keyExportDirectory .walk(FileWalkDirection.BOTTOM_UP) 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 3ce3ac2efdf..6be3628848a 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 @@ -542,7 +542,7 @@ object LocalData { } fun devicePairingSuccessfulTimestamp(value: Long) = - with(getSharedPreferenceInstance().edit()) { + getSharedPreferenceInstance().edit(true) { putLong( CoronaWarnApplication.getAppContext() .getString(R.string.preference_device_pairing_successful_time), diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/TracingRepository.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/TracingRepository.kt index c94cc83476a..c018540071a 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/TracingRepository.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/TracingRepository.kt @@ -2,6 +2,7 @@ package de.rki.coronawarnapp.storage import androidx.lifecycle.MutableLiveData import de.rki.coronawarnapp.exception.ExceptionCategory +import de.rki.coronawarnapp.exception.TransactionException import de.rki.coronawarnapp.exception.reporting.report import de.rki.coronawarnapp.nearby.InternalExposureNotificationClient import de.rki.coronawarnapp.risk.TimeVariables.getActiveTracingDaysInRetentionPeriod @@ -52,6 +53,8 @@ object TracingRepository { try { RetrieveDiagnosisKeysTransaction.start() RiskLevelTransaction.start() + } catch (e: TransactionException) { + e.cause?.report(ExceptionCategory.EXPOSURENOTIFICATION) } catch (e: Exception) { e.report(ExceptionCategory.EXPOSURENOTIFICATION) } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/settings/SettingsResetFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/settings/SettingsResetFragment.kt index 88c3d33e831..cb9ddd031d9 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/settings/SettingsResetFragment.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/settings/SettingsResetFragment.kt @@ -60,7 +60,6 @@ class SettingsResetFragment : Fragment() { } } - // TODO verify that all local data is deleted private fun deleteAllAppContent() { lifecycleScope.launch { try { diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/CachedKeyFileHolder.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/CachedKeyFileHolder.kt index 883d0d03e11..be5b2edf70b 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/CachedKeyFileHolder.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/CachedKeyFileHolder.kt @@ -23,6 +23,7 @@ import de.rki.coronawarnapp.BuildConfig import de.rki.coronawarnapp.CoronaWarnApplication import de.rki.coronawarnapp.http.WebRequestBuilder import de.rki.coronawarnapp.service.diagnosiskey.DiagnosisKeyConstants +import de.rki.coronawarnapp.storage.FileStorageHelper import de.rki.coronawarnapp.storage.LocalData import de.rki.coronawarnapp.storage.keycache.KeyCacheEntity import de.rki.coronawarnapp.storage.keycache.KeyCacheRepository @@ -67,6 +68,7 @@ object CachedKeyFileHolder { * @return list of all files from both the cache and the diff query */ suspend fun asyncFetchFiles(currentDate: Date): List = withContext(Dispatchers.IO) { + checkForFreeSpace() val serverDates = getDatesFromServer() // TODO remove last3HourFetch before Release if (BuildConfig.FLAVOR != "device" && isLast3HourFetchEnabled()) { @@ -114,6 +116,8 @@ object CachedKeyFileHolder { } } + private fun checkForFreeSpace() = FileStorageHelper.checkFileStorageFreeSpace() + /** * Calculates the missing days based on current missing entries in the cache */ diff --git a/Corona-Warn-App/src/main/res/drawable-night/ic_illustration_notification_on.xml b/Corona-Warn-App/src/main/res/drawable-night/ic_illustration_notification_on.xml index 1a20b06cdc8..24c871811f6 100644 --- a/Corona-Warn-App/src/main/res/drawable-night/ic_illustration_notification_on.xml +++ b/Corona-Warn-App/src/main/res/drawable-night/ic_illustration_notification_on.xml @@ -3,236 +3,204 @@ android:height="220dp" android:viewportWidth="360" android:viewportHeight="220"> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Corona-Warn-App/src/main/res/drawable-night/ic_illustration_test.xml b/Corona-Warn-App/src/main/res/drawable-night/ic_illustration_test.xml index 9472fc4720a..2dc4c4ee7ab 100644 --- a/Corona-Warn-App/src/main/res/drawable-night/ic_illustration_test.xml +++ b/Corona-Warn-App/src/main/res/drawable-night/ic_illustration_test.xml @@ -1,453 +1,684 @@ - - - - + + + + + android:fillType="evenOdd" + android:strokeColor="#00000000"/> - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + - - - - + + + + + android:fillType="evenOdd" + android:strokeColor="#00000000"/> - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + android:fillType="evenOdd" + android:strokeColor="#00000000"/> + android:pathData="M56.56,103.31C56.9,103.31 57.19,103.73 57.23,104.24L55.89,104.24C55.89,103.73 56.21,103.31 56.56,103.31M55.38,104.24L55.16,104.24C54.868,104.251 54.635,104.488 54.63,104.78L54.63,106.63C54.635,106.922 54.868,107.159 55.16,107.17L58,107.17C58.292,107.159 58.525,106.922 58.53,106.63L58.53,104.78C58.525,104.488 58.292,104.251 58,104.24L57.78,104.24C57.868,103.82 57.724,103.386 57.405,103.1C57.085,102.814 56.637,102.721 56.23,102.855C55.822,102.989 55.518,103.33 55.43,103.75C55.4,103.912 55.4,104.078 55.43,104.24" + android:strokeWidth="1" + android:fillColor="#FFFFFF" + android:fillType="evenOdd" + android:strokeColor="#00000000"/> + android:fillType="evenOdd" + android:strokeColor="#00000000"/> diff --git a/Corona-Warn-App/src/main/res/drawable-night/ic_submission_illustration_other_warning.xml b/Corona-Warn-App/src/main/res/drawable-night/ic_submission_illustration_other_warning.xml index 814323b348e..9463e9389e3 100644 --- a/Corona-Warn-App/src/main/res/drawable-night/ic_submission_illustration_other_warning.xml +++ b/Corona-Warn-App/src/main/res/drawable-night/ic_submission_illustration_other_warning.xml @@ -1,200 +1,306 @@ + android:pathData="M173.7,100.7L142.89,100.6C142.58,100.595 142.28,100.713 142.057,100.928C141.834,101.144 141.705,101.44 141.7,101.75L141.7,101.75L141.7,111.39C141.7,112.041 142.219,112.574 142.87,112.59L142.87,112.59L173.68,112.68C174.335,112.68 174.869,112.155 174.88,111.5L174.88,101.87C174.883,101.557 174.761,101.256 174.541,101.033C174.322,100.81 174.023,100.683 173.71,100.68L173.71,100.68" + android:strokeWidth="1" + android:fillColor="#B1BBC2" + android:fillType="evenOdd" + android:strokeColor="#00000000"/> + android:pathData="M173.76,84L143,83.85C142.682,83.834 142.371,83.947 142.137,84.163C141.904,84.38 141.768,84.682 141.76,85L141.76,85L141.76,85L141.76,94.64C141.755,94.95 141.873,95.25 142.088,95.473C142.304,95.696 142.6,95.825 142.91,95.83L142.91,95.83L173.71,95.93C174.023,95.935 174.325,95.816 174.55,95.599C174.775,95.381 174.905,95.083 174.91,94.77L174.91,94.77L174.91,85.14C174.899,84.519 174.401,84.016 173.78,84L173.78,84" + android:strokeWidth="1" + android:fillColor="#94A1AB" + android:fillType="evenOdd" + android:strokeColor="#00000000"/> + android:fillType="evenOdd" + android:strokeColor="#00000000"/> - - - - + android:fillType="evenOdd" + android:strokeColor="#00000000"/> + + + + + android:strokeWidth="1" + android:fillColor="#232324" + android:fillType="nonZero" + android:strokeColor="#00000000"/> + android:fillType="evenOdd" + android:strokeColor="#00000000"/> + android:pathData="M158.69,78.93C159.14,78.93 159.53,79.49 159.59,80.18L157.78,80.18C157.78,79.49 158.21,78.93 158.69,78.93M157.1,80.18L156.8,80.18C156.409,80.191 156.095,80.508 156.09,80.9L156.09,83.39C156.087,83.581 156.161,83.765 156.294,83.902C156.427,84.039 156.609,84.117 156.8,84.12L160.62,84.12C160.811,84.117 160.993,84.039 161.126,83.902C161.259,83.765 161.333,83.581 161.33,83.39L161.33,83.39L161.33,80.9C161.325,80.508 161.011,80.191 160.62,80.18L160.33,80.18C160.448,79.616 160.256,79.031 159.826,78.647C159.396,78.262 158.794,78.137 158.246,78.317C157.698,78.497 157.288,78.956 157.17,79.52L157.17,79.52C157.12,79.737 157.12,79.963 157.17,80.18" + android:strokeWidth="1" + android:fillColor="#FFFFFF" + android:fillType="evenOdd" + android:strokeColor="#00000000"/> - - - - - - - - - - - - - + + + + + + + + + + + + + - - - - + + + + + android:fillType="evenOdd" + android:strokeColor="#00000000"/> - - - - - - - - - - - - - - - - - - - - + android:fillType="evenOdd" + android:strokeColor="#00000000"/> + + + + + + + + + + + + + + + + + + + + diff --git a/Corona-Warn-App/src/main/res/drawable-night/ic_test_result_illustration_positive.xml b/Corona-Warn-App/src/main/res/drawable-night/ic_test_result_illustration_positive.xml index a44f4b52a50..0de3ca0c9fd 100644 --- a/Corona-Warn-App/src/main/res/drawable-night/ic_test_result_illustration_positive.xml +++ b/Corona-Warn-App/src/main/res/drawable-night/ic_test_result_illustration_positive.xml @@ -3,126 +3,126 @@ android:height="112dp" android:viewportWidth="80" android:viewportHeight="112"> - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + diff --git a/Corona-Warn-App/src/main/res/drawable/ic_settings_tracing_inactive.xml b/Corona-Warn-App/src/main/res/drawable/ic_settings_tracing_inactive.xml index 05c269e85b3..62c09555d8c 100644 --- a/Corona-Warn-App/src/main/res/drawable/ic_settings_tracing_inactive.xml +++ b/Corona-Warn-App/src/main/res/drawable/ic_settings_tracing_inactive.xml @@ -4,7 +4,7 @@ android:viewportWidth="40" android:viewportHeight="40"> @@ -102,7 +102,7 @@ layout="@layout/include_divider" android:layout_width="@dimen/match_constraint" android:layout_height="wrap_content" - android:layout_marginTop="@dimen/spacing_small" + android:layout_marginTop="@dimen/spacing_medium" app:layout_constraintEnd_toEndOf="@+id/guideline_end" app:layout_constraintStart_toStartOf="@+id/guideline_start" app:layout_constraintTop_toBottomOf="@+id/information_legal_subtitle_contact" /> diff --git a/Corona-Warn-App/src/main/res/layout/fragment_settings_notifications.xml b/Corona-Warn-App/src/main/res/layout/fragment_settings_notifications.xml index a85d1f4a1e2..0a62638a9e4 100644 --- a/Corona-Warn-App/src/main/res/layout/fragment_settings_notifications.xml +++ b/Corona-Warn-App/src/main/res/layout/fragment_settings_notifications.xml @@ -59,7 +59,7 @@ layout="@layout/include_settings_switch_row" android:layout_width="0dp" android:layout_height="wrap_content" - android:layout_marginTop="@dimen/spacing_tiny" + android:layout_marginTop="@dimen/spacing_small" android:visibility="@{FormatterHelper.formatVisibility(settingsViewModel.isNotificationsEnabled())}" app:enabled="@{true}" app:layout_constraintEnd_toEndOf="parent" diff --git a/Corona-Warn-App/src/main/res/layout/include_submission_status_card_content.xml b/Corona-Warn-App/src/main/res/layout/include_submission_status_card_content.xml index f7b9952c079..6d11a80ed55 100644 --- a/Corona-Warn-App/src/main/res/layout/include_submission_status_card_content.xml +++ b/Corona-Warn-App/src/main/res/layout/include_submission_status_card_content.xml @@ -36,7 +36,7 @@