diff --git a/.circleci/config.yml b/.circleci/config.yml index da41853c01b..ed0062ff57d 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,10 +1,12 @@ version: 2.1 orbs: android: circleci/android@0.2.1 + sonarcloud: sonarsource/sonarcloud@1.0.1 commands: install-ndk: android/install-ndk restore-build-cache: android/restore-build-cache save-build-cache: android/save-build-cache + scan-sonar: sonarcloud/scan jobs: quickBuildReleaseWithTestsAndChecks: executor: android/android @@ -22,6 +24,13 @@ jobs: environment: JVM_OPTS: -Xmx2048m GRADLE_OPTS: -Xmx1536m -XX:+HeapDumpOnOutOfMemoryError -Dorg.gradle.caching=true -Dorg.gradle.configureondemand=true -Dkotlin.compiler.execution.strategy=in-process -Dkotlin.incremental=false + - run: + name: JaCoCo report + command: ./gradlew :Corona-Warn-App:jacocoTestReportDeviceRelease + environment: + JVM_OPTS: -Xmx2048m + GRADLE_OPTS: -Xmx1536m -XX:+HeapDumpOnOutOfMemoryError -Dorg.gradle.caching=true -Dorg.gradle.configureondemand=true -Dkotlin.compiler.execution.strategy=in-process -Dkotlin.incremental=false + - scan-sonar - save-build-cache - save_cache: paths: @@ -36,4 +45,5 @@ workflows: version: 2 workflow: jobs: - - quickBuildReleaseWithTestsAndChecks + - quickBuildReleaseWithTestsAndChecks: + context: SonarCloud diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index fc9b825df03..83a0e36cd75 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -11,6 +11,8 @@ Before submitting, please take the time to check the points below and provide so * [ ] [Link your Pull Request to an issue](https://help.github.com/en/github/managing-your-work-on-github/linking-a-pull-request-to-an-issue) (if applicable) * [ ] Create Work In Progress [WIP] pull requests only if you need clarification or an explicit review before you can continue your work item. * [ ] Make sure that your PR is not introducing _unnecessary_ reformatting (e.g., introduced by on-save hooks in your IDE) +* [ ] Make sure that your PR does not contain changes in strings.xml (see issue #72) +* [ ] Make sure that your PR does not contain compiled sources (already set by the default .gitignore) and / or binary files ## Description diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 210103484f0..4ad6bc590d6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -50,6 +50,8 @@ The following rule governs documentation contributions: * Commits should be as small as possible while ensuring that each commit is correct independently (i.e., each commit should compile and pass tests). +* Pull requests must not contain compiled sources (already set by the default .gitignore) or binary files + * Test your changes as thoroughly as possible before you commit them. Preferably, automate your test by unit/integration tests. If tested manually, provide information about the test scope in the PR description (e.g. “Test passed: Upgrade version from 0.42 to 0.42.23.”). * Create _Work In Progress [WIP]_ pull requests only if you need clarification or an explicit review before you can continue your work item. diff --git a/Corona-Warn-App/build.gradle b/Corona-Warn-App/build.gradle index 8d2b3aa6ad8..a48d1106680 100644 --- a/Corona-Warn-App/build.gradle +++ b/Corona-Warn-App/build.gradle @@ -22,7 +22,6 @@ apply plugin: 'kotlin-android' apply plugin: 'kotlin-android-extensions' apply plugin: 'kotlin-kapt' apply plugin: "androidx.navigation.safeargs.kotlin" -apply plugin: 'jacoco' android { ndkVersion "21.2.6472646" @@ -33,8 +32,8 @@ android { applicationId 'de.rki.coronawarnapp' minSdkVersion 23 targetSdkVersion 29 - versionCode 12 - versionName "0.8.5" + versionCode 13 + versionName "0.8.6" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" buildConfigField "String", "DOWNLOAD_CDN_URL", "\"$DOWNLOAD_CDN_URL\"" @@ -62,7 +61,12 @@ android { def VERIFICATION_CDN_URL = properties.getProperty('VERIFICATION_CDN_URL') if (VERIFICATION_CDN_URL) buildConfigField "String", "VERIFICATION_CDN_URL", "\"$VERIFICATION_CDN_URL\"" + } + javaCompileOptions { + annotationProcessorOptions { + arguments += ["room.schemaLocation": "$projectDir/schemas".toString()] + } } } @@ -78,6 +82,7 @@ android { minifyEnabled true shrinkResources true proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + testCoverageEnabled true buildConfigField "String", "EXPORT_SIGNATURE_ID", "\"de.rki.coronawarnapp-dev\"" } } @@ -141,13 +146,6 @@ android { } -task jacocoTestReport(type: JacocoReport, dependsOn: ['testDeviceReleaseUnitTest']) { - reports { - xml.enabled = true - html.enabled = true - } -} - dependencies { // KOTLIN implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.7" @@ -209,7 +207,7 @@ dependencies { implementation "androidx.room:room-ktx:$room_version" implementation "androidx.room:room-guava:$room_version" kapt "androidx.room:room-compiler:$room_version" - implementation "androidx.sqlite:sqlite:2.0.1" + implementation "androidx.sqlite:sqlite:2.1.0" // UTILS implementation project(":Server-Protocol-Buffer") diff --git a/Corona-Warn-App/schemas/de.rki.coronawarnapp.storage.AppDatabase/1.json b/Corona-Warn-App/schemas/de.rki.coronawarnapp.storage.AppDatabase/1.json new file mode 100644 index 00000000000..006845e8274 --- /dev/null +++ b/Corona-Warn-App/schemas/de.rki.coronawarnapp.storage.AppDatabase/1.json @@ -0,0 +1,151 @@ +{ + "formatVersion": 1, + "database": { + "version": 1, + "identityHash": "fc62bfe144d17a099180c8023c340138", + "entities": [ + { + "tableName": "exposure_summary", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `daysSinceLastExposure` INTEGER NOT NULL, `matchedKeyCount` INTEGER NOT NULL, `maximumRiskScore` INTEGER NOT NULL, `summationRiskScore` INTEGER NOT NULL, `attenuationDurationsInMinutes` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "daysSinceLastExposure", + "columnName": "daysSinceLastExposure", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "matchedKeyCount", + "columnName": "matchedKeyCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "maximumRiskScore", + "columnName": "maximumRiskScore", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "summationRiskScore", + "columnName": "summationRiskScore", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "attenuationDurationsInMinutes", + "columnName": "attenuationDurationsInMinutes", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_exposure_summary_id", + "unique": false, + "columnNames": [ + "id" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_exposure_summary_id` ON `${TABLE_NAME}` (`id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "date", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `path` TEXT NOT NULL, `type` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "path", + "columnName": "path", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_date_id", + "unique": false, + "columnNames": [ + "id" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_date_id` ON `${TABLE_NAME}` (`id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "tracing_interval", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`from` INTEGER NOT NULL, `to` INTEGER NOT NULL, PRIMARY KEY(`from`, `to`))", + "fields": [ + { + "fieldPath": "from", + "columnName": "from", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "to", + "columnName": "to", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "from", + "to" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_tracing_interval_from_to", + "unique": false, + "columnNames": [ + "from", + "to" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_tracing_interval_from_to` ON `${TABLE_NAME}` (`from`, `to`)" + } + ], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'fc62bfe144d17a099180c8023c340138')" + ] + } +} \ No newline at end of file diff --git a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/util/security/DBPasswordTest.kt b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/util/security/DBPasswordTest.kt index bd9025f6272..5f1267524ea 100644 --- a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/util/security/DBPasswordTest.kt +++ b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/util/security/DBPasswordTest.kt @@ -20,10 +20,8 @@ package de.rki.coronawarnapp.util.security import android.content.Context -import android.database.sqlite.SQLiteDatabase import androidx.test.core.app.ApplicationProvider import de.rki.coronawarnapp.storage.AppDatabase -import de.rki.coronawarnapp.storage.DATABASE_NAME import de.rki.coronawarnapp.storage.keycache.KeyCacheEntity import kotlinx.coroutines.runBlocking import net.sqlcipher.database.SQLiteException @@ -35,7 +33,6 @@ import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.JUnit4 -import java.io.File import java.util.UUID import kotlin.random.Random diff --git a/Corona-Warn-App/src/main/assets/pins.properties b/Corona-Warn-App/src/main/assets/pins.properties deleted file mode 100644 index 198e2e9bbfd..00000000000 --- a/Corona-Warn-App/src/main/assets/pins.properties +++ /dev/null @@ -1,13 +0,0 @@ -# TODO add certificate pinning -# -# Intermediates will be encoded like this: -# openssl x509 -in CERTNAME -pubkey -noout | \ -# openssl pkey -pubin -outform der | \ -# openssl dgst -sha256 -binary | \ -# openssl enc -base64 -# -# Format is sha256/BASE64ENCODED -# Pins are delimited by "," -SUBMISSION_PINS= -DISTRIBUTION_PINS= -VERIFICATION_PINS= \ No newline at end of file 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 f1f40cb4af0..cae06737762 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 @@ -14,8 +14,8 @@ import androidx.lifecycle.LifecycleObserver import androidx.lifecycle.OnLifecycleEvent import androidx.lifecycle.ProcessLifecycleOwner import androidx.localbroadcastmanager.content.LocalBroadcastManager -import de.rki.coronawarnapp.exception.ErrorReportReceiver -import de.rki.coronawarnapp.exception.ReportingConstants.ERROR_REPORT_LOCAL_BROADCAST_CHANNEL +import de.rki.coronawarnapp.exception.reporting.ErrorReportReceiver +import de.rki.coronawarnapp.exception.reporting.ReportingConstants.ERROR_REPORT_LOCAL_BROADCAST_CHANNEL import de.rki.coronawarnapp.notification.NotificationHelper import org.conscrypt.Conscrypt import java.security.Security @@ -101,7 +101,8 @@ class CoronaWarnApplication : Application(), LifecycleObserver, } override fun onActivityResumed(activity: Activity) { - errorReceiver = ErrorReportReceiver(activity) + errorReceiver = + ErrorReportReceiver(activity) LocalBroadcastManager.getInstance(this) .registerReceiver(errorReceiver, IntentFilter(ERROR_REPORT_LOCAL_BROADCAST_CHANNEL)) } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/TestForAPIFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/TestForAPIFragment.kt index d1740c85eb6..3cd4ea57daf 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/TestForAPIFragment.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/TestForAPIFragment.kt @@ -35,7 +35,7 @@ import de.rki.coronawarnapp.databinding.FragmentTestForAPIBinding import de.rki.coronawarnapp.exception.ExceptionCategory import de.rki.coronawarnapp.exception.ExceptionCategory.INTERNAL import de.rki.coronawarnapp.exception.TransactionException -import de.rki.coronawarnapp.exception.report +import de.rki.coronawarnapp.exception.reporting.report import de.rki.coronawarnapp.nearby.InternalExposureNotificationClient import de.rki.coronawarnapp.nearby.InternalExposureNotificationPermissionHelper import de.rki.coronawarnapp.receiver.ExposureStateUpdateReceiver diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/TestRiskLevelCalculation.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/TestRiskLevelCalculation.kt index 3e288bd87dc..94df6e4ab1c 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/TestRiskLevelCalculation.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/TestRiskLevelCalculation.kt @@ -19,7 +19,7 @@ import com.google.zxing.integration.android.IntentResult import de.rki.coronawarnapp.databinding.FragmentTestRiskLevelCalculationBinding import de.rki.coronawarnapp.exception.ExceptionCategory import de.rki.coronawarnapp.exception.TransactionException -import de.rki.coronawarnapp.exception.report +import de.rki.coronawarnapp.exception.reporting.report import de.rki.coronawarnapp.nearby.InternalExposureNotificationClient import de.rki.coronawarnapp.risk.RiskLevel import de.rki.coronawarnapp.risk.TimeVariables @@ -258,7 +258,9 @@ class TestRiskLevelCalculation : Fragment() { "Tracing Duration: " + "${TimeUnit.MILLISECONDS.toDays(TimeVariables.getTimeActiveTracingDuration())} days \n" + "Tracing Duration in last 14 days: " + - "${TimeVariables.getActiveTracingDaysInRetentionPeriod()} days" + "${TimeVariables.getActiveTracingDaysInRetentionPeriod()} days \n" + + "Last time risk level calculation ${LocalData.lastTimeRiskLevelCalculation()}" + binding.labelRiskScore.text = riskAsString val lowClass = diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/exception/CwaWebSecurityException.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/exception/CwaWebSecurityException.kt new file mode 100644 index 00000000000..6dbbbca8d83 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/exception/CwaWebSecurityException.kt @@ -0,0 +1,8 @@ +package de.rki.coronawarnapp.exception + +import okio.IOException + +class CwaWebSecurityException(cause: Throwable) : IOException( + "an error occurred while trying to establish a secure connection to the server", + cause +) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/exception/ErrorReportReceiver.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/exception/reporting/ErrorReportReceiver.kt similarity index 95% rename from Corona-Warn-App/src/main/java/de/rki/coronawarnapp/exception/ErrorReportReceiver.kt rename to Corona-Warn-App/src/main/java/de/rki/coronawarnapp/exception/reporting/ErrorReportReceiver.kt index 958e8a1dd95..0213635c4cc 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/exception/ErrorReportReceiver.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/exception/reporting/ErrorReportReceiver.kt @@ -1,4 +1,4 @@ -package de.rki.coronawarnapp.exception +package de.rki.coronawarnapp.exception.reporting import android.app.Activity import android.content.BroadcastReceiver @@ -7,6 +7,7 @@ import android.content.Intent import android.util.Log import de.rki.coronawarnapp.CoronaWarnApplication import de.rki.coronawarnapp.R +import de.rki.coronawarnapp.exception.ExceptionCategory import de.rki.coronawarnapp.util.DialogHelper class ErrorReportReceiver(private val activity: Activity) : BroadcastReceiver() { diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/exception/ExceptionReporter.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/exception/reporting/ExceptionReporter.kt similarity index 93% rename from Corona-Warn-App/src/main/java/de/rki/coronawarnapp/exception/ExceptionReporter.kt rename to Corona-Warn-App/src/main/java/de/rki/coronawarnapp/exception/reporting/ExceptionReporter.kt index 3c58048be14..feaa6711f16 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/exception/ExceptionReporter.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/exception/reporting/ExceptionReporter.kt @@ -1,8 +1,9 @@ -package de.rki.coronawarnapp.exception +package de.rki.coronawarnapp.exception.reporting import android.content.Intent import androidx.localbroadcastmanager.content.LocalBroadcastManager import de.rki.coronawarnapp.CoronaWarnApplication +import de.rki.coronawarnapp.exception.ExceptionCategory import java.io.PrintWriter import java.io.StringWriter diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/exception/ReportingConstants.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/exception/reporting/ReportingConstants.kt similarity index 87% rename from Corona-Warn-App/src/main/java/de/rki/coronawarnapp/exception/ReportingConstants.kt rename to Corona-Warn-App/src/main/java/de/rki/coronawarnapp/exception/reporting/ReportingConstants.kt index 22f1c2393c8..d3eb21a2eb9 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/exception/ReportingConstants.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/exception/reporting/ReportingConstants.kt @@ -1,4 +1,4 @@ -package de.rki.coronawarnapp.exception +package de.rki.coronawarnapp.exception.reporting object ReportingConstants { const val ERROR_REPORT_LOCAL_BROADCAST_CHANNEL = "error-report" diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/http/CertificatePinnerFactory.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/http/CertificatePinnerFactory.kt deleted file mode 100644 index 7a103bea083..00000000000 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/http/CertificatePinnerFactory.kt +++ /dev/null @@ -1,23 +0,0 @@ -package de.rki.coronawarnapp.http - -import de.rki.coronawarnapp.util.PropertyLoader -import okhttp3.CertificatePinner - -class CertificatePinnerFactory { - fun getCertificatePinner(): CertificatePinner = PropertyLoader().run { - CertificatePinner.Builder() - .add( - DynamicURLs.DOWNLOAD_CDN_URL.removePrefix(DynamicURLs.PATTERN_PREFIX_HTTPS), - *this.getDistributionPins() - ) - .add( - DynamicURLs.SUBMISSION_CDN_URL.removePrefix(DynamicURLs.PATTERN_PREFIX_HTTPS), - *this.getSubmissionPins() - ) - .add( - DynamicURLs.VERIFICATION_CDN_URL.removePrefix(DynamicURLs.PATTERN_PREFIX_HTTPS), - *this.getVerificationPins() - ) - .build() - } -} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/http/ServiceFactory.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/http/ServiceFactory.kt index 42604a156a5..94c3fd63c68 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/http/ServiceFactory.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/http/ServiceFactory.kt @@ -4,15 +4,22 @@ import android.webkit.URLUtil import de.rki.coronawarnapp.BuildConfig import de.rki.coronawarnapp.CoronaWarnApplication import de.rki.coronawarnapp.exception.http.ServiceFactoryException +import de.rki.coronawarnapp.http.config.DynamicURLs +import de.rki.coronawarnapp.http.config.HTTPVariables +import de.rki.coronawarnapp.http.interceptor.OfflineCacheInterceptor +import de.rki.coronawarnapp.http.interceptor.WebSecurityVerificationInterceptor +import de.rki.coronawarnapp.http.interceptor.RetryInterceptor import de.rki.coronawarnapp.http.service.DistributionService import de.rki.coronawarnapp.http.service.SubmissionService import de.rki.coronawarnapp.http.service.VerificationService import de.rki.coronawarnapp.risk.TimeVariables import okhttp3.Cache +import okhttp3.CipherSuite import okhttp3.ConnectionPool import okhttp3.ConnectionSpec import okhttp3.Interceptor import okhttp3.OkHttpClient +import okhttp3.TlsVersion import okhttp3.logging.HttpLoggingInterceptor import retrofit2.Retrofit import retrofit2.converter.gson.GsonConverterFactory @@ -37,10 +44,13 @@ class ServiceFactory { * List of interceptors, e.g. logging */ private val mInterceptors: List = listOf( + WebSecurityVerificationInterceptor(), HttpLoggingInterceptor().also { if (BuildConfig.DEBUG) it.setLevel(HttpLoggingInterceptor.Level.BODY) }, - OfflineCacheInterceptor(CoronaWarnApplication.getAppContext()), + OfflineCacheInterceptor( + CoronaWarnApplication.getAppContext() + ), RetryInterceptor(), HttpErrorParser() ) @@ -64,41 +74,98 @@ class ServiceFactory { private val okHttpClient by lazy { val clientBuilder = OkHttpClient.Builder() - val timeoutMs = TimeVariables.getTransactionTimeout() - clientBuilder.connectTimeout(timeoutMs, TimeUnit.MILLISECONDS) - clientBuilder.readTimeout(timeoutMs, TimeUnit.MILLISECONDS) - clientBuilder.writeTimeout(timeoutMs, TimeUnit.MILLISECONDS) - clientBuilder.callTimeout(timeoutMs, TimeUnit.MILLISECONDS) + clientBuilder.connectTimeout( + HTTPVariables.getHTTPConnectionTimeout(), + TimeUnit.MILLISECONDS + ) + clientBuilder.readTimeout( + HTTPVariables.getHTTPReadTimeout(), + TimeUnit.MILLISECONDS + ) + clientBuilder.writeTimeout( + HTTPVariables.getHTTPWriteTimeout(), + TimeUnit.MILLISECONDS + ) + clientBuilder.callTimeout( + TimeVariables.getTransactionTimeout(), + TimeUnit.MILLISECONDS + ) clientBuilder.connectionPool(conPool) cache.evictAll() clientBuilder.cache(cache) - val spec: ConnectionSpec = ConnectionSpec.Builder(ConnectionSpec.RESTRICTED_TLS) - .allEnabledCipherSuites() // TODO clarify more concrete Ciphers - .build() - - clientBuilder.connectionSpecs(listOf(spec)) - - CertificatePinnerFactory().getCertificatePinner().run { - if (this.pins.isNotEmpty()) { - clientBuilder.certificatePinner(this) - } - } - mInterceptors.forEach { clientBuilder.addInterceptor(it) } clientBuilder.build() } + /** + * For the CDN we want to ensure maximum Compatibility. + */ + private fun getCDNSpecs(): List = listOf( + ConnectionSpec.Builder(ConnectionSpec.COMPATIBLE_TLS) + .tlsVersions( + TlsVersion.TLS_1_0, + TlsVersion.TLS_1_1, + TlsVersion.TLS_1_2, + TlsVersion.TLS_1_3 + ) + .allEnabledCipherSuites() + .build() + ) + + /** + * For Submission and Verification we want to limit our specifications for TLS. + */ + private fun getRestrictedSpecs(): List = listOf( + ConnectionSpec.Builder(ConnectionSpec.RESTRICTED_TLS) + .tlsVersions( + TlsVersion.TLS_1_2, + TlsVersion.TLS_1_3 + ) + .cipherSuites( + // TLS 1.2 with Perfect Forward Secrecy (BSI TR-02102-2) + CipherSuite.TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA256, + CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384, + CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, + CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256, + CipherSuite.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384, + CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + CipherSuite.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + CipherSuite.TLS_DHE_DSS_WITH_AES_128_CBC_SHA256, + CipherSuite.TLS_DHE_DSS_WITH_AES_256_CBC_SHA256, + CipherSuite.TLS_DHE_DSS_WITH_AES_128_GCM_SHA256, + CipherSuite.TLS_DHE_DSS_WITH_AES_256_GCM_SHA384, + CipherSuite.TLS_DHE_RSA_WITH_AES_128_CBC_SHA256, + CipherSuite.TLS_DHE_RSA_WITH_AES_256_CBC_SHA256, + CipherSuite.TLS_DHE_RSA_WITH_AES_128_GCM_SHA256, + CipherSuite.TLS_DHE_RSA_WITH_AES_256_GCM_SHA384, + // TLS 1.3 (BSI TR-02102-2) + CipherSuite.TLS_AES_128_GCM_SHA256, + CipherSuite.TLS_AES_256_GCM_SHA384, + CipherSuite.TLS_AES_128_CCM_SHA256 + ) + .build() + ) + + /** + * Helper function to create a new client from an existent Client with New Specs. + * + * @param specs + */ + private fun OkHttpClient.buildClientWithNewSpecs(specs: List) = + this.newBuilder().connectionSpecs(specs).build() + private val downloadCdnUrl get() = getValidUrl(DynamicURLs.DOWNLOAD_CDN_URL) fun distributionService(): DistributionService = distributionService private val distributionService by lazy { Retrofit.Builder() - .client(okHttpClient) + .client(okHttpClient.buildClientWithNewSpecs(getCDNSpecs())) .baseUrl(downloadCdnUrl) .addConverterFactory(gsonConverterFactory) .build() @@ -111,7 +178,7 @@ class ServiceFactory { fun verificationService(): VerificationService = verificationService private val verificationService by lazy { Retrofit.Builder() - .client(okHttpClient) + .client(okHttpClient.buildClientWithNewSpecs(getRestrictedSpecs())) .baseUrl(verificationCdnUrl) .addConverterFactory(gsonConverterFactory) .build() @@ -124,7 +191,7 @@ class ServiceFactory { fun submissionService(): SubmissionService = submissionService private val submissionService by lazy { Retrofit.Builder() - .client(okHttpClient) + .client(okHttpClient.buildClientWithNewSpecs(getRestrictedSpecs())) .baseUrl(submissionCdnUrl) .addConverterFactory(protoConverterFactory) .addConverterFactory(gsonConverterFactory) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/http/DynamicURLs.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/http/config/DynamicURLs.kt similarity index 92% rename from Corona-Warn-App/src/main/java/de/rki/coronawarnapp/http/DynamicURLs.kt rename to Corona-Warn-App/src/main/java/de/rki/coronawarnapp/http/config/DynamicURLs.kt index 20dd1781424..cc05f85afe6 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/http/DynamicURLs.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/http/config/DynamicURLs.kt @@ -1,4 +1,4 @@ -package de.rki.coronawarnapp.http +package de.rki.coronawarnapp.http.config import de.rki.coronawarnapp.BuildConfig diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/http/config/HTTPVariables.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/http/config/HTTPVariables.kt new file mode 100644 index 00000000000..4939bf67556 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/http/config/HTTPVariables.kt @@ -0,0 +1,45 @@ +package de.rki.coronawarnapp.http.config + +object HTTPVariables { + /** + * The maximal runtime of a transaction + * In milliseconds + */ + private const val HTTP_CONNECTION_TIMEOUT = 10000L + + /** + * Getter function for [HTTP_CONNECTION_TIMEOUT] + * + * @return timeout in milliseconds + */ + fun getHTTPConnectionTimeout(): Long = + HTTP_CONNECTION_TIMEOUT + + /** + * The maximal runtime of a transaction + * In milliseconds + */ + private const val HTTP_READ_TIMEOUT = 10000L + + /** + * Getter function for [HTTP_READ_TIMEOUT] + * + * @return timeout in milliseconds + */ + fun getHTTPReadTimeout(): Long = + HTTP_READ_TIMEOUT + + /** + * The maximal runtime of a transaction + * In milliseconds + */ + private const val HTTP_WRITE_TIMEOUT = 10000L + + /** + * Getter function for [HTTP_WRITE_TIMEOUT] + * + * @return timeout in milliseconds + */ + fun getHTTPWriteTimeout(): Long = + HTTP_WRITE_TIMEOUT +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/http/OfflineCacheInterceptor.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/http/interceptor/OfflineCacheInterceptor.kt similarity index 97% rename from Corona-Warn-App/src/main/java/de/rki/coronawarnapp/http/OfflineCacheInterceptor.kt rename to Corona-Warn-App/src/main/java/de/rki/coronawarnapp/http/interceptor/OfflineCacheInterceptor.kt index 15bf62893e8..e8d14fab803 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/http/OfflineCacheInterceptor.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/http/interceptor/OfflineCacheInterceptor.kt @@ -1,4 +1,4 @@ -package de.rki.coronawarnapp.http +package de.rki.coronawarnapp.http.interceptor import android.content.Context import de.rki.coronawarnapp.util.ConnectivityHelper.isNetworkEnabled diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/http/RetryInterceptor.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/http/interceptor/RetryInterceptor.kt similarity index 93% rename from Corona-Warn-App/src/main/java/de/rki/coronawarnapp/http/RetryInterceptor.kt rename to Corona-Warn-App/src/main/java/de/rki/coronawarnapp/http/interceptor/RetryInterceptor.kt index c4bfa179b98..91167efe8a8 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/http/RetryInterceptor.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/http/interceptor/RetryInterceptor.kt @@ -1,4 +1,4 @@ -package de.rki.coronawarnapp.http +package de.rki.coronawarnapp.http.interceptor import android.util.Log import okhttp3.Interceptor diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/http/interceptor/WebSecurityVerificationInterceptor.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/http/interceptor/WebSecurityVerificationInterceptor.kt new file mode 100644 index 00000000000..2af724e76a8 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/http/interceptor/WebSecurityVerificationInterceptor.kt @@ -0,0 +1,14 @@ +package de.rki.coronawarnapp.http.interceptor + +import de.rki.coronawarnapp.exception.CwaWebSecurityException +import okhttp3.Interceptor +import okhttp3.Response +import javax.net.ssl.SSLException + +class WebSecurityVerificationInterceptor : Interceptor { + override fun intercept(chain: Interceptor.Chain): Response { + try { return chain.proceed(chain.request()) } catch (e: SSLException) { + throw CwaWebSecurityException(e) + } + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/ExposureStateUpdateWorker.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/ExposureStateUpdateWorker.kt index bc97b2c7a0f..bf0fb12d918 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/ExposureStateUpdateWorker.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/ExposureStateUpdateWorker.kt @@ -9,7 +9,7 @@ import com.google.android.gms.nearby.exposurenotification.ExposureNotificationCl import de.rki.coronawarnapp.exception.ExceptionCategory import de.rki.coronawarnapp.exception.NoTokenException import de.rki.coronawarnapp.exception.TransactionException -import de.rki.coronawarnapp.exception.report +import de.rki.coronawarnapp.exception.reporting.report import de.rki.coronawarnapp.storage.ExposureSummaryRepository import de.rki.coronawarnapp.transaction.RiskLevelTransaction diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/InternalExposureNotificationPermissionHelper.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/InternalExposureNotificationPermissionHelper.kt index 61c56fa645b..7ee79021a3a 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/InternalExposureNotificationPermissionHelper.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/InternalExposureNotificationPermissionHelper.kt @@ -9,7 +9,7 @@ import com.google.android.gms.nearby.exposurenotification.ExposureNotificationSt import com.google.android.gms.nearby.exposurenotification.TemporaryExposureKey import de.rki.coronawarnapp.exception.ENPermissionException import de.rki.coronawarnapp.exception.ExceptionCategory -import de.rki.coronawarnapp.exception.report +import de.rki.coronawarnapp.exception.reporting.report import kotlinx.coroutines.launch /** diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/receiver/ExposureStateUpdateReceiver.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/receiver/ExposureStateUpdateReceiver.kt index 9a28ffa7b51..65f13604d17 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/receiver/ExposureStateUpdateReceiver.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/receiver/ExposureStateUpdateReceiver.kt @@ -10,7 +10,7 @@ import com.google.android.gms.nearby.exposurenotification.ExposureNotificationCl import de.rki.coronawarnapp.exception.ExceptionCategory.INTERNAL import de.rki.coronawarnapp.exception.NoTokenException import de.rki.coronawarnapp.exception.WrongReceiverException -import de.rki.coronawarnapp.exception.report +import de.rki.coronawarnapp.exception.reporting.report import de.rki.coronawarnapp.nearby.ExposureStateUpdateWorker /** diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/TimeVariables.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/TimeVariables.kt index 5a6537cc950..6f687165077 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/TimeVariables.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/TimeVariables.kt @@ -3,7 +3,7 @@ package de.rki.coronawarnapp.risk import com.google.android.gms.common.api.ApiException import de.rki.coronawarnapp.CoronaWarnApplication import de.rki.coronawarnapp.exception.ExceptionCategory -import de.rki.coronawarnapp.exception.report +import de.rki.coronawarnapp.exception.reporting.report import de.rki.coronawarnapp.nearby.InternalExposureNotificationClient import de.rki.coronawarnapp.storage.LocalData import de.rki.coronawarnapp.storage.tracing.TracingIntervalRepository diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/sharing/ExposureSharingService.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/sharing/ExposureSharingService.kt index 12711ab2c7d..b70a41978ce 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/sharing/ExposureSharingService.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/sharing/ExposureSharingService.kt @@ -10,7 +10,7 @@ import com.google.zxing.BarcodeFormat import com.google.zxing.qrcode.QRCodeWriter import de.rki.coronawarnapp.exception.ExceptionCategory.EXPOSURENOTIFICATION import de.rki.coronawarnapp.exception.ExceptionCategory.INTERNAL -import de.rki.coronawarnapp.exception.report +import de.rki.coronawarnapp.exception.reporting.report import de.rki.coronawarnapp.nearby.InternalExposureNotificationClient import de.rki.coronawarnapp.server.protocols.AppleLegacyKeyExchange diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/AppDatabase.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/AppDatabase.kt index 113427e38b8..632fef20885 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/AppDatabase.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/AppDatabase.kt @@ -18,7 +18,7 @@ import java.io.File @Database( entities = [ExposureSummaryEntity::class, KeyCacheEntity::class, TracingIntervalEntity::class], version = 1, - exportSchema = false + exportSchema = true ) @TypeConverters(Converters::class) abstract class AppDatabase : RoomDatabase() { @@ -52,6 +52,11 @@ abstract class AppDatabase : RoomDatabase() { private fun buildDatabase(context: Context): AppDatabase { return Room.databaseBuilder(context, AppDatabase::class.java, DATABASE_NAME) + /** + * The fallback behavior is to reset the app as we only store exposure summaries + * and cached references that are non-critical to app operation. + */ + .fallbackToDestructiveMigrationFrom() .openHelperFactory(SupportFactory(SecurityHelper.getDBPassword())) .build() } 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 c6c6bba9de2..fc0d79de672 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 @@ -276,7 +276,6 @@ object LocalData { * @param value timestamp as Date */ fun lastTimeDiagnosisKeysFromServerFetch(value: Date?) { - // TODO need this for nullable ref, shout not be goto for nullable storage getSharedPreferenceInstance().edit(true) { putLong( CoronaWarnApplication.getAppContext() @@ -286,6 +285,37 @@ object LocalData { } } + /** + * Gets the last time of successful risk level calculation as long + * from the EncryptedSharedPrefs + * + * @return Long + */ + fun lastTimeRiskLevelCalculation(): Long? { + val time = getSharedPreferenceInstance().getLong( + CoronaWarnApplication.getAppContext() + .getString(R.string.preference_timestamp_risk_level_calculation), + 0L + ) + return Date(time).time + } + + /** + * Sets the last time of successful risk level calculation as long + * from the EncryptedSharedPrefs + * + * @param value timestamp as Long + */ + fun lastTimeRiskLevelCalculation(value: Long?) { + getSharedPreferenceInstance().edit(true) { + putLong( + CoronaWarnApplication.getAppContext() + .getString(R.string.preference_timestamp_risk_level_calculation), + value ?: 0L + ) + } + } + /** * Gets the last timestamp the user manually triggered the key retrieval process * 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 cb84c5cc157..3beb1fb40b6 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 @@ -4,7 +4,7 @@ import android.widget.Toast import androidx.lifecycle.MutableLiveData import de.rki.coronawarnapp.CoronaWarnApplication import de.rki.coronawarnapp.exception.ExceptionCategory -import de.rki.coronawarnapp.exception.report +import de.rki.coronawarnapp.exception.reporting.report import de.rki.coronawarnapp.nearby.InternalExposureNotificationClient import de.rki.coronawarnapp.risk.TimeVariables.getActiveTracingDaysInRetentionPeriod import de.rki.coronawarnapp.transaction.RetrieveDiagnosisKeysTransaction diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/transaction/RiskLevelTransaction.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/transaction/RiskLevelTransaction.kt index df32874cc60..690595b1ff9 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/transaction/RiskLevelTransaction.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/transaction/RiskLevelTransaction.kt @@ -32,6 +32,7 @@ import de.rki.coronawarnapp.transaction.RiskLevelTransaction.RiskLevelTransactio import de.rki.coronawarnapp.transaction.RiskLevelTransaction.RiskLevelTransactionState.CLOSE import de.rki.coronawarnapp.transaction.RiskLevelTransaction.RiskLevelTransactionState.RETRIEVE_APPLICATION_CONFIG import de.rki.coronawarnapp.transaction.RiskLevelTransaction.RiskLevelTransactionState.RETRIEVE_EXPOSURE_SUMMARY +import de.rki.coronawarnapp.transaction.RiskLevelTransaction.RiskLevelTransactionState.RISK_CALCULATION_DATE_UPDATE import de.rki.coronawarnapp.transaction.RiskLevelTransaction.RiskLevelTransactionState.UPDATE_RISK_LEVEL import de.rki.coronawarnapp.util.TimeAndDateExtensions.millisecondsToHours import kotlinx.coroutines.Dispatchers @@ -108,7 +109,8 @@ import java.util.concurrent.atomic.AtomicReference * 6. [CHECK_INCREASED_RISK] * 7. [CHECK_UNKNOWN_RISK_INITIAL_TRACING_DURATION] * 8. [UPDATE_RISK_LEVEL] - * 9. [CLOSE] + * 9. [RISK_CALCULATION_DATE_UPDATE] + * 10. [CLOSE] * * This transaction will queue up any start calls and executes them in the given order (unlike the other defined * transactions (e.g. [RetrieveDiagnosisKeysTransaction]). This is necessary in order to respond to various trigger @@ -152,6 +154,9 @@ object RiskLevelTransaction : Transaction() { /** Update and persist the Risk Level Score with the calculated score */ UPDATE_RISK_LEVEL, + /** Update of the Date to reflect a complete Transaction State */ + RISK_CALCULATION_DATE_UPDATE, + /** Transaction Closure */ CLOSE } @@ -170,6 +175,9 @@ object RiskLevelTransaction : Transaction() { /** atomic reference for the rollback value for the last calculated risk level score */ private val lastCalculatedRiskLevelScoreForRollback = AtomicReference() + /** atomic reference for the rollback value for date of last risk level calculation */ + private val lastCalculatedRiskLevelDate = AtomicReference() + /** initiates the transaction. This suspend function guarantees a successful transaction once completed. */ suspend fun start() = lockAndExecute { /**************************************************** @@ -220,6 +228,7 @@ object RiskLevelTransaction : Transaction() { if (result == UNDETERMINED) { lastCalculatedRiskLevelScoreForRollback.set(RiskLevelRepository.getLastCalculatedScore()) executeUpdateRiskLevelScore(LOW_LEVEL_RISK) + executeRiskLevelCalculationDateUpdate() executeClose() return@lockAndExecute } else { @@ -233,6 +242,9 @@ object RiskLevelTransaction : Transaction() { if (UPDATE_RISK_LEVEL.isInStateStack()) { updateRiskLevelScore(lastCalculatedRiskLevelScoreForRollback.get()) } + if (RISK_CALCULATION_DATE_UPDATE.isInStateStack()) { + LocalData.lastTimeRiskLevelCalculation(lastCalculatedRiskLevelDate.get()) + } } catch (e: Exception) { // We handle every exception through a RollbackException to make sure that a single EntryPoint // is available for the caller. @@ -261,7 +273,10 @@ object RiskLevelTransaction : Transaction() { // if there was no key retrieval before, we return no calculation state TimeVariables.getLastTimeDiagnosisKeysFromServerFetch() ?: return@executeState UNKNOWN_RISK_INITIAL.also { - Log.v(TAG, "$transactionId - no last time diagnosis keys from server fetch timestamp was found") + Log.v( + TAG, + "$transactionId - no last time diagnosis keys from server fetch timestamp was found" + ) } Log.v(TAG, "$transactionId - CHECK_UNKNOWN_RISK_INITIAL_NO_KEYS not applicable") @@ -314,15 +329,16 @@ object RiskLevelTransaction : Transaction() { /** * Executes the [RETRIEVE_EXPOSURE_SUMMARY] Transaction State */ - private suspend fun executeRetrieveExposureSummary(): ExposureSummary = executeState(RETRIEVE_EXPOSURE_SUMMARY) { - val lastExposureSummary = getLastExposureSummary() ?: getNewExposureSummary() + private suspend fun executeRetrieveExposureSummary(): ExposureSummary = + executeState(RETRIEVE_EXPOSURE_SUMMARY) { + val lastExposureSummary = getLastExposureSummary() ?: getNewExposureSummary() - return@executeState lastExposureSummary.also { - // todo remove after testing sessions - recordedTransactionValuesForTestingOnly.exposureSummary = it - Log.v(TAG, "$transactionId - get the exposure summary for further calculation") + return@executeState lastExposureSummary.also { + // todo remove after testing sessions + recordedTransactionValuesForTestingOnly.exposureSummary = it + Log.v(TAG, "$transactionId - get the exposure summary for further calculation") + } } - } /** * Executes the [CHECK_INCREASED_RISK] Transaction State @@ -412,6 +428,7 @@ object RiskLevelTransaction : Transaction() { private suspend fun executeClose() = executeState(CLOSE) { Log.v(TAG, "$transactionId - transaction will close") lastCalculatedRiskLevelScoreForRollback.set(null) + lastCalculatedRiskLevelDate.set(null) } /**************************************************** @@ -435,6 +452,8 @@ object RiskLevelTransaction : Transaction() { ) lastCalculatedRiskLevelScoreForRollback.set(RiskLevelRepository.getLastCalculatedScore()) executeUpdateRiskLevelScore(riskLevel) + lastCalculatedRiskLevelDate.set(LocalData.lastTimeRiskLevelCalculation()) + executeRiskLevelCalculationDateUpdate() executeClose() return true } @@ -522,4 +541,14 @@ object RiskLevelTransaction : Transaction() { Log.v(TAG, "$transactionId - generated new exposure summary with $googleToken") } } + + /** + * Executes the CALCULATION_DATE_UPDATE Transaction State + */ + private suspend fun executeRiskLevelCalculationDateUpdate() { + val currentDate = System.currentTimeMillis() + executeState(RISK_CALCULATION_DATE_UPDATE) { + LocalData.lastTimeRiskLevelCalculation(currentDate) + } + } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/LauncherActivity.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/LauncherActivity.kt index 7a96494ec5f..938f0c3026c 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/LauncherActivity.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/LauncherActivity.kt @@ -6,7 +6,7 @@ import android.os.Bundle import android.widget.Toast import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.lifecycleScope -import de.rki.coronawarnapp.http.DynamicURLs +import de.rki.coronawarnapp.http.config.DynamicURLs import de.rki.coronawarnapp.storage.LocalData import de.rki.coronawarnapp.ui.main.MainActivity import de.rki.coronawarnapp.ui.onboarding.OnboardingActivity diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/information/InformationAboutFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/information/InformationAboutFragment.kt index 95a4182275b..4fb3f4ea0a4 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/information/InformationAboutFragment.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/information/InformationAboutFragment.kt @@ -39,7 +39,7 @@ class InformationAboutFragment : BaseFragment() { } private fun setButtonOnClickListener() { - binding.informationAboutHeader.headerButtonBack.buttonIcon.setOnClickListener { + binding.informationAboutHeader.headerToolbar.setNavigationOnClickListener { (activity as MainActivity).goBack() } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/information/InformationContactFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/information/InformationContactFragment.kt index 88f37c5fd8b..595fb2df009 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/information/InformationContactFragment.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/information/InformationContactFragment.kt @@ -41,7 +41,7 @@ class InformationContactFragment : BaseFragment() { } private fun setButtonOnClickListener() { - binding.informationContactHeader.headerButtonBack.buttonIcon.setOnClickListener { + binding.informationContactHeader.headerToolbar.setNavigationOnClickListener { (activity as MainActivity).goBack() } binding.informationContactNavigationRowPhone.navigationRow.setOnClickListener { diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/information/InformationFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/information/InformationFragment.kt index 80e7e541cd5..8e44bce61ff 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/information/InformationFragment.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/information/InformationFragment.kt @@ -74,7 +74,7 @@ class InformationFragment : BaseFragment() { InformationFragmentDirections.actionInformationFragmentToInformationTechnicalFragment() ) } - binding.informationHeader.headerButtonBack.buttonIcon.setOnClickListener { + binding.informationHeader.headerToolbar.setNavigationOnClickListener { (activity as MainActivity).goBack() } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/information/InformationLegalFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/information/InformationLegalFragment.kt index 16abadd8c53..298e5275cdb 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/information/InformationLegalFragment.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/information/InformationLegalFragment.kt @@ -39,7 +39,7 @@ class InformationLegalFragment : BaseFragment() { } private fun setButtonOnClickListener() { - binding.informationLegalHeader.headerButtonBack.buttonIcon.setOnClickListener { + binding.informationLegalHeader.headerToolbar.setNavigationOnClickListener { (activity as MainActivity).goBack() } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/information/InformationPrivacyFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/information/InformationPrivacyFragment.kt index b3ab39e4cc4..75e3d748329 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/information/InformationPrivacyFragment.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/information/InformationPrivacyFragment.kt @@ -39,7 +39,7 @@ class InformationPrivacyFragment : BaseFragment() { } private fun setButtonOnClickListener() { - binding.informationPrivacyHeader.headerButtonBack.buttonIcon.setOnClickListener { + binding.informationPrivacyHeader.headerToolbar.setNavigationOnClickListener { (activity as MainActivity).goBack() } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/information/InformationTechnicalFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/information/InformationTechnicalFragment.kt index 9ce43c29ff7..d28274e6a15 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/information/InformationTechnicalFragment.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/information/InformationTechnicalFragment.kt @@ -39,7 +39,7 @@ class InformationTechnicalFragment : BaseFragment() { } private fun setButtonOnClickListener() { - binding.informationTechnicalHeader.headerButtonBack.buttonIcon.setOnClickListener { + binding.informationTechnicalHeader.headerToolbar.setNavigationOnClickListener { (activity as MainActivity).goBack() } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/information/InformationTermsFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/information/InformationTermsFragment.kt index 8189f124017..1a96747418f 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/information/InformationTermsFragment.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/information/InformationTermsFragment.kt @@ -39,7 +39,7 @@ class InformationTermsFragment : BaseFragment() { } private fun setButtonOnClickListener() { - binding.informationTermsHeader.headerButtonBack.buttonIcon.setOnClickListener { + binding.informationTermsHeader.headerToolbar.setNavigationOnClickListener { (activity as MainActivity).goBack() } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/MainOverviewFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/MainOverviewFragment.kt index 40502e58607..28b9c00ba30 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/MainOverviewFragment.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/MainOverviewFragment.kt @@ -43,7 +43,7 @@ class MainOverviewFragment : BaseFragment() { } private fun setButtonOnClickListener() { - binding.mainOverviewHeader.headerButtonBack.buttonIcon.setOnClickListener { + binding.mainOverviewHeader.headerToolbar.setNavigationOnClickListener { (activity as MainActivity).goBack() } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/MainShareFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/MainShareFragment.kt index 8ba1bb2e6fc..d8cad38c46f 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/MainShareFragment.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/MainShareFragment.kt @@ -51,7 +51,7 @@ class MainShareFragment : BaseFragment() { binding.mainShareButton.setOnClickListener { ShareHelper.shareText(this, getString(R.string.main_share_message), null) } - binding.mainShareHeader.headerButtonBack.buttonIcon.setOnClickListener { + binding.mainShareHeader.headerToolbar.setNavigationOnClickListener { (activity as MainActivity).goBack() } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/onboarding/OnboardingTracingFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/onboarding/OnboardingTracingFragment.kt index 023b07e91f9..3ff6a0f5df3 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/onboarding/OnboardingTracingFragment.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/onboarding/OnboardingTracingFragment.kt @@ -6,11 +6,17 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.appcompat.app.AlertDialog +import androidx.lifecycle.lifecycleScope import de.rki.coronawarnapp.R import de.rki.coronawarnapp.databinding.FragmentOnboardingTracingBinding +import de.rki.coronawarnapp.exception.ExceptionCategory +import de.rki.coronawarnapp.exception.reporting.report +import de.rki.coronawarnapp.nearby.InternalExposureNotificationClient import de.rki.coronawarnapp.nearby.InternalExposureNotificationPermissionHelper +import de.rki.coronawarnapp.storage.LocalData import de.rki.coronawarnapp.ui.BaseFragment import de.rki.coronawarnapp.util.DialogHelper +import kotlinx.coroutines.launch /** * This fragment ask the user if he wants to enable tracing. @@ -59,6 +65,7 @@ class OnboardingTracingFragment : BaseFragment(), override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) setButtonOnClickListener() + resetTracing() } private fun setButtonOnClickListener() { @@ -100,4 +107,23 @@ class OnboardingTracingFragment : BaseFragment(), OnboardingTracingFragmentDirections.actionOnboardingTracingFragmentToOnboardingTestFragment() ) } + + private fun resetTracing() { + // Reset tracing state in onboarding + lifecycleScope.launch { + if (InternalExposureNotificationClient.asyncIsEnabled()) { + try { + InternalExposureNotificationClient.asyncStop() + // Reset initial activation timestamp + LocalData.initialTracingActivationTimestamp(0L) + } catch (exception: Exception) { + exception.report( + ExceptionCategory.EXPOSURENOTIFICATION, + OnboardingTracingFragment.TAG, + null + ) + } + } + } + } } 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 893a95287d2..3c753af5782 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 @@ -71,7 +71,7 @@ class RiskDetailsFragment : BaseFragment() { RiskDetailsFragmentDirections.actionRiskDetailsFragmentToSettingsTracingFragment() ) } - binding.riskDetailsRiskCard.riskCardHeader.riskCardHeaderButtonBack.setOnClickListener { + binding.riskDetailsToolbar.setNavigationOnClickListener { (activity as MainActivity).goBack() } } 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 f7aab9c197b..4f5c8b56026 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 @@ -63,7 +63,7 @@ class SettingsFragment : BaseFragment() { val tracingRow = binding.settingsTracing.settingsRow val notificationRow = binding.settingsNotifications.settingsRow val resetRow = binding.settingsReset - val goBack = binding.settingsHeader.headerButtonBack.buttonIcon + val goBack = binding.settingsHeader.headerToolbar resetRow.setOnClickListener { doNavigate( SettingsFragmentDirections.actionSettingsFragmentToSettingsResetFragment() diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/settings/SettingsNotificationFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/settings/SettingsNotificationFragment.kt index 2344f700960..862b943ef08 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/settings/SettingsNotificationFragment.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/settings/SettingsNotificationFragment.kt @@ -68,7 +68,7 @@ class SettingsNotificationFragment : Fragment() { // Settings val settingsRow = binding.settingsNavigationRowSystem.navigationRow val goBack = - binding.settingsNotificationsHeader.headerButtonBack.buttonIcon + binding.settingsNotificationsHeader.headerToolbar // Update Risk updateRiskNotificationSwitch.setOnCheckedChangeListener { _, _ -> // android calls this listener also on start, so it has to be verified if the user pressed the switch 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 6fb673f152f..13e05292e8c 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 @@ -10,7 +10,7 @@ import androidx.lifecycle.lifecycleScope import com.google.android.gms.common.api.ApiException import de.rki.coronawarnapp.databinding.FragmentSettingsResetBinding import de.rki.coronawarnapp.exception.ExceptionCategory -import de.rki.coronawarnapp.exception.report +import de.rki.coronawarnapp.exception.reporting.report import de.rki.coronawarnapp.nearby.InternalExposureNotificationClient import de.rki.coronawarnapp.ui.BaseFragment import de.rki.coronawarnapp.ui.main.MainActivity @@ -55,7 +55,7 @@ class SettingsResetFragment : BaseFragment() { binding.settingsResetButtonCancel.setOnClickListener { (activity as MainActivity).goBack() } - binding.settingsResetHeader.headerButtonBack.buttonIcon.setOnClickListener { + binding.settingsResetHeader.headerToolbar.setNavigationOnClickListener { (activity as MainActivity).goBack() } } 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 382d34f0557..0c97c73a4f9 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 @@ -8,16 +8,19 @@ import android.view.ViewGroup import android.widget.Toast import androidx.fragment.app.activityViewModels import androidx.lifecycle.lifecycleScope +import de.rki.coronawarnapp.R import de.rki.coronawarnapp.databinding.FragmentSettingsTracingBinding import de.rki.coronawarnapp.exception.ExceptionCategory -import de.rki.coronawarnapp.exception.report +import de.rki.coronawarnapp.exception.reporting.report import de.rki.coronawarnapp.nearby.InternalExposureNotificationClient import de.rki.coronawarnapp.nearby.InternalExposureNotificationPermissionHelper +import de.rki.coronawarnapp.storage.LocalData import de.rki.coronawarnapp.ui.BaseFragment import de.rki.coronawarnapp.ui.ViewBlocker import de.rki.coronawarnapp.ui.main.MainActivity import de.rki.coronawarnapp.ui.viewmodel.SettingsViewModel import de.rki.coronawarnapp.ui.viewmodel.TracingViewModel +import de.rki.coronawarnapp.util.DialogHelper import de.rki.coronawarnapp.util.SettingsNavigationHelper import de.rki.coronawarnapp.worker.BackgroundWorkScheduler import kotlinx.coroutines.launch @@ -108,7 +111,7 @@ class SettingsTracingFragment : BaseFragment(), } } } - binding.settingsTracingHeader.headerButtonBack.buttonIcon.setOnClickListener { + binding.settingsTracingHeader.headerToolbar.setNavigationOnClickListener { (activity as MainActivity).goBack() } binding.settingsTracingStatusBluetooth.tracingStatusCardButton.setOnClickListener { @@ -142,8 +145,32 @@ class SettingsTracingFragment : BaseFragment(), tracingViewModel.refreshIsTracingEnabled() BackgroundWorkScheduler.stopWorkScheduler() } else { - internalExposureNotificationPermissionHelper.requestPermissionToStartTracing() + // tracing was already activated + if (LocalData.initialTracingActivationTimestamp() != null) { + internalExposureNotificationPermissionHelper.requestPermissionToStartTracing() + } else { + // tracing was never activated + // ask for consent via dialog for initial tracing activation when tracing was not + // activated during onboarding + showConsentDialog() + } } } } + + private fun showConsentDialog() { + val dialog = DialogHelper.DialogInstance( + requireActivity(), + R.string.onboarding_tracing_headline_consent, + R.string.onboarding_tracing_body_consent, + R.string.onboarding_button_enable, + R.string.onboarding_button_cancel, + true, + { + internalExposureNotificationPermissionHelper.requestPermissionToStartTracing() + }, { + tracingViewModel.refreshIsTracingEnabled() + }) + DialogHelper.showDialog(dialog) + } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/SubmissionContactFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/SubmissionContactFragment.kt index 0d1693898f5..93bd99e04da 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/SubmissionContactFragment.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/SubmissionContactFragment.kt @@ -39,7 +39,7 @@ class SubmissionContactFragment : BaseFragment() { } private fun setButtonOnClickListener() { - binding.submissionContactHeader.headerButtonBack.buttonIcon.setOnClickListener { + binding.submissionContactHeader.headerToolbar.setNavigationOnClickListener { (activity as MainActivity).goBack() } binding.submissionContactButtonCall.setOnClickListener { @@ -51,7 +51,7 @@ class SubmissionContactFragment : BaseFragment() { } private fun dial() = context?.let { - val number = getString(R.string.submission_contact_number) + val number = getString(R.string.submission_contact_number_dial) CallHelper.call(this, "tel:$number") } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/SubmissionDispatcherFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/SubmissionDispatcherFragment.kt index 4178eea3bd4..73bf2aa3534 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/SubmissionDispatcherFragment.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/SubmissionDispatcherFragment.kt @@ -40,7 +40,7 @@ class SubmissionDispatcherFragment : BaseFragment() { } private fun setButtonOnClickListener() { - binding.submissionDispatcherHeader.headerButtonBack.buttonIcon.setOnClickListener { + binding.submissionDispatcherHeader.headerToolbar.setNavigationOnClickListener { (activity as MainActivity).goBack() } binding.submissionDispatcherQr.dispatcherCard.setOnClickListener { diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/SubmissionDoneFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/SubmissionDoneFragment.kt index 330c86d79ff..60c9300d935 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/SubmissionDoneFragment.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/SubmissionDoneFragment.kt @@ -36,7 +36,7 @@ class SubmissionDoneFragment : BaseFragment() { } private fun setButtonOnClickListener() { - binding.submissionDoneHeader.headerButtonBack.buttonIcon.setOnClickListener { + binding.submissionDoneHeader.headerToolbar.setNavigationOnClickListener { doNavigate( SubmissionDoneFragmentDirections.actionSubmissionDoneFragmentToMainFragment() ) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/SubmissionIntroFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/SubmissionIntroFragment.kt index 219457cb2e0..0dc26e99bb9 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/SubmissionIntroFragment.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/SubmissionIntroFragment.kt @@ -36,7 +36,7 @@ class SubmissionIntroFragment : BaseFragment() { } private fun setButtonOnClickListener() { - binding.submissionIntroHeader.headerButtonBack.buttonIcon.setOnClickListener { + binding.submissionIntroHeader.headerToolbar.setNavigationOnClickListener { doNavigate(SubmissionIntroFragmentDirections.actionSubmissionIntroFragmentToMainFragment()) } binding.submissionIntroButtonNext.setOnClickListener { diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/SubmissionResultPositiveOtherWarningFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/SubmissionResultPositiveOtherWarningFragment.kt index 02872234709..401edf1528a 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/SubmissionResultPositiveOtherWarningFragment.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/SubmissionResultPositiveOtherWarningFragment.kt @@ -148,7 +148,7 @@ class SubmissionResultPositiveOtherWarningFragment : BaseFragment(), binding.submissionPositiveOtherWarningButtonNext.setOnClickListener { initiateWarningOthers() } - binding.submissionPositiveOtherWarningHeader.headerButtonBack.buttonIcon.setOnClickListener { + binding.submissionPositiveOtherWarningHeader.headerToolbar.setNavigationOnClickListener { navigateToSubmissionResultFragment() } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/SubmissionTanFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/SubmissionTanFragment.kt index 62877109447..7a6036caaff 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/SubmissionTanFragment.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/SubmissionTanFragment.kt @@ -95,7 +95,7 @@ class SubmissionTanFragment : BaseFragment() { binding.submissionTanInput.listener = { tan -> viewModel.tan.value = tan } binding.submissionTanButtonEnter.setOnClickListener { storeTanAndContinue() } - binding.submissionTanHeader.headerButtonBack.buttonIcon.setOnClickListener { navigateToDispatchScreen() } + binding.submissionTanHeader.headerToolbar.setNavigationOnClickListener { navigateToDispatchScreen() } submissionViewModel.registrationState.observeEvent(viewLifecycleOwner, { if (ApiRequestState.SUCCESS == it) { diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/SubmissionTanViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/SubmissionTanViewModel.kt index c18d49a0153..cae07743b57 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/SubmissionTanViewModel.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/SubmissionTanViewModel.kt @@ -5,6 +5,7 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.Transformations import androidx.lifecycle.ViewModel import de.rki.coronawarnapp.storage.SubmissionRepository +import de.rki.coronawarnapp.util.TanHelper class SubmissionTanViewModel : ViewModel() { @@ -16,7 +17,21 @@ class SubmissionTanViewModel : ViewModel() { val isValidTanFormat = Transformations.map(tan) { - it != null && it.length == TanConstants.MAX_LENGTH + it != null && + it.length == TanConstants.MAX_LENGTH && + TanHelper.isChecksumValid(it) && + TanHelper.allCharactersValid(it) + } + + val tanChecksumValid = + Transformations.map(tan) { + ((it !== null && it.trim().length == TanConstants.MAX_LENGTH) && + TanHelper.isChecksumValid(it).not()).not() + } + + val tanCharactersValid = + Transformations.map(tan) { + !((it != null) && TanHelper.allCharactersValid(it).not()) } fun storeTeletan() { diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/SubmissionTestResultFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/SubmissionTestResultFragment.kt index 3037b5b61e0..85c53feadad 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/SubmissionTestResultFragment.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/SubmissionTestResultFragment.kt @@ -1,5 +1,6 @@ package de.rki.coronawarnapp.ui.submission +import android.app.AlertDialog import android.os.Bundle import android.view.LayoutInflater import android.view.View @@ -110,17 +111,11 @@ class SubmissionTestResultFragment : BaseFragment() { } binding.submissionTestResultButtonPendingRemoveTest.setOnClickListener { - submissionViewModel.deregisterTestFromDevice() - doNavigate( - SubmissionTestResultFragmentDirections.actionSubmissionResultFragmentToMainFragment() - ) + removeTestAfterConfirmation() } binding.submissionTestResultButtonNegativeRemoveTest.setOnClickListener { - submissionViewModel.deregisterTestFromDevice() - doNavigate( - SubmissionTestResultFragmentDirections.actionSubmissionResultFragmentToMainFragment() - ) + removeTestAfterConfirmation() } binding.submissionTestResultButtonPositiveContinue.setOnClickListener { @@ -128,13 +123,10 @@ class SubmissionTestResultFragment : BaseFragment() { } binding.submissionTestResultButtonInvalidRemoveTest.setOnClickListener { - submissionViewModel.deregisterTestFromDevice() - doNavigate( - SubmissionTestResultFragmentDirections.actionSubmissionResultFragmentToMainFragment() - ) + removeTestAfterConfirmation() } - binding.submissionTestResultHeader.headerButtonBack.buttonIcon.setOnClickListener { + binding.submissionTestResultHeader.headerToolbar.setNavigationOnClickListener { doNavigate( SubmissionTestResultFragmentDirections.actionSubmissionResultFragmentToMainFragment() ) @@ -158,4 +150,23 @@ class SubmissionTestResultFragment : BaseFragment() { .actionSubmissionResultFragmentToSubmissionResultPositiveOtherWarningFragment() ) } + + private fun removeTestAfterConfirmation() { + val removeTestDialog = DialogHelper.DialogInstance( + requireActivity(), + R.string.submission_test_result_dialog_remove_test_title, + R.string.submission_test_result_dialog_remove_test_message, + R.string.submission_test_result_dialog_remove_test_button_positive, + R.string.submission_test_result_dialog_remove_test_button_negative, + positiveButtonFunction = { + submissionViewModel.deregisterTestFromDevice() + doNavigate( + SubmissionTestResultFragmentDirections.actionSubmissionResultFragmentToMainFragment() + ) + } + ) + DialogHelper.showDialog(removeTestDialog).apply { + getButton(AlertDialog.BUTTON_POSITIVE).setTextColor(context.getColor(R.color.colorTextSemanticRed)) + } + } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/TanConstants.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/TanConstants.kt index b6be13be81d..644cf1ee0f3 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/TanConstants.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/TanConstants.kt @@ -1,6 +1,6 @@ package de.rki.coronawarnapp.ui.submission object TanConstants { - const val MAX_LENGTH = 7 + const val MAX_LENGTH = 10 val ALPHA_NUMERIC_CHARS = ('a'..'z').plus('A'..'Z').plus('0'..'9') } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/TanInput.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/TanInput.kt index 70cc48b6833..f95bafeb917 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/TanInput.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/TanInput.kt @@ -8,6 +8,9 @@ import android.view.inputmethod.InputMethodManager import android.widget.FrameLayout import androidx.core.widget.doOnTextChanged import de.rki.coronawarnapp.R +import de.rki.coronawarnapp.ui.submission.TanConstants.MAX_LENGTH +import de.rki.coronawarnapp.util.TanHelper +import java.util.Locale import kotlinx.android.synthetic.main.view_tan_input.view.tan_input_edittext import kotlinx.android.synthetic.main.view_tan_input.view.tan_input_textview_1 import kotlinx.android.synthetic.main.view_tan_input.view.tan_input_textview_2 @@ -16,6 +19,11 @@ import kotlinx.android.synthetic.main.view_tan_input.view.tan_input_textview_4 import kotlinx.android.synthetic.main.view_tan_input.view.tan_input_textview_5 import kotlinx.android.synthetic.main.view_tan_input.view.tan_input_textview_6 import kotlinx.android.synthetic.main.view_tan_input.view.tan_input_textview_7 +import kotlinx.android.synthetic.main.view_tan_input.view.tan_input_textview_8 +import kotlinx.android.synthetic.main.view_tan_input.view.tan_input_textview_9 +import kotlinx.android.synthetic.main.view_tan_input.view.tan_input_textview_10 +import kotlinx.android.synthetic.main.view_tan_input.view.dash_view_1 +import kotlinx.android.synthetic.main.view_tan_input.view.dash_view_2 class TanInput(context: Context, attrs: AttributeSet) : FrameLayout(context, attrs) { @@ -30,7 +38,7 @@ class TanInput(context: Context, attrs: AttributeSet) : FrameLayout(context, att TanConstants.ALPHA_NUMERIC_CHARS.contains(it) } } - private val lengthFilter = InputFilter.LengthFilter(TanConstants.MAX_LENGTH) + private var lengthFilter = InputFilter.LengthFilter(MAX_LENGTH) var listener: ((String?) -> Unit)? = null @@ -41,6 +49,9 @@ class TanInput(context: Context, attrs: AttributeSet) : FrameLayout(context, att tan_input_edittext.filters = arrayOf(whitespaceFilter, alphaNumericFilter, lengthFilter) + dash_view_1.text = "-" + dash_view_2.text = "-" + // register listener tan_input_edittext.doOnTextChanged { text, _, _, _ -> updateTan(text) } setOnClickListener { showKeyboard() } @@ -56,9 +67,20 @@ class TanInput(context: Context, attrs: AttributeSet) : FrameLayout(context, att } } + private fun limitLength(length: Int?) { + lengthFilter = InputFilter.LengthFilter(if (length != null) length else MAX_LENGTH) + tan_input_edittext.filters = arrayOf(whitespaceFilter, alphaNumericFilter, lengthFilter) + } + private fun updateTan(text: CharSequence?) { - this.tan = text?.toString() + this.tan = text?.toString()?.toUpperCase(Locale.getDefault()) updateDigits() + tan?.let { + limitLength( + if (TanHelper.allCharactersValid(it)) null + else it.length + ) + } notifyListener() } @@ -71,9 +93,24 @@ class TanInput(context: Context, attrs: AttributeSet) : FrameLayout(context, att tan_input_textview_4, tan_input_textview_5, tan_input_textview_6, - tan_input_textview_7 + tan_input_textview_7, + tan_input_textview_8, + tan_input_textview_9, + tan_input_textview_10 ).forEachIndexed { i, tanDigit -> tanDigit.text = digitAtIndex(i) + tanDigit.background = + if (digitAtIndex(i) == "") + resources.getDrawable(R.drawable.tan_input_digit, null) + else if (TanHelper.isTanCharacterValid(digitAtIndex(i))) + resources.getDrawable(R.drawable.tan_input_digit_entered, null) + else resources.getDrawable(R.drawable.tan_input_digit_error, null) + + tanDigit.setTextColor( + if (TanHelper.isTanCharacterValid(digitAtIndex(i))) + resources.getColor(R.color.colorTextSemanticNeutral, null) + else resources.getColor(R.color.colorTextSemanticRed, null) + ) } private fun digitAtIndex(index: Int): String = tan?.getOrNull(index)?.toString() ?: "" diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/viewmodel/SubmissionViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/viewmodel/SubmissionViewModel.kt index 60ab8b5cdc4..60376083e32 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/viewmodel/SubmissionViewModel.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/viewmodel/SubmissionViewModel.kt @@ -6,7 +6,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import de.rki.coronawarnapp.exception.ExceptionCategory import de.rki.coronawarnapp.exception.http.CwaWebException -import de.rki.coronawarnapp.exception.report +import de.rki.coronawarnapp.exception.reporting.report import de.rki.coronawarnapp.service.submission.SubmissionService import de.rki.coronawarnapp.storage.LocalData import de.rki.coronawarnapp.storage.SubmissionRepository diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/viewmodel/TracingViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/viewmodel/TracingViewModel.kt index ad8a5d362fc..4bccd807c69 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/viewmodel/TracingViewModel.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/viewmodel/TracingViewModel.kt @@ -6,7 +6,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import de.rki.coronawarnapp.exception.ExceptionCategory.INTERNAL import de.rki.coronawarnapp.exception.TransactionException -import de.rki.coronawarnapp.exception.report +import de.rki.coronawarnapp.exception.reporting.report import de.rki.coronawarnapp.storage.ExposureSummaryRepository import de.rki.coronawarnapp.storage.RiskLevelRepository import de.rki.coronawarnapp.storage.TracingRepository diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/update/UpdateChecker.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/update/UpdateChecker.kt index 2644430f991..3d56617f253 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/update/UpdateChecker.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/update/UpdateChecker.kt @@ -14,6 +14,7 @@ import com.google.android.play.core.install.model.AppUpdateType import com.google.android.play.core.install.model.UpdateAvailability import de.rki.coronawarnapp.BuildConfig import de.rki.coronawarnapp.R +import de.rki.coronawarnapp.exception.CwaSecurityException import de.rki.coronawarnapp.server.protocols.ApplicationConfigurationOuterClass import de.rki.coronawarnapp.service.applicationconfiguration.ApplicationConfigurationService import de.rki.coronawarnapp.ui.LauncherActivity @@ -31,12 +32,13 @@ class UpdateChecker(private val activity: LauncherActivity) { suspend fun checkForUpdate() { // check if an update is needed based on server config - // TODO replace with signature exception val updateNeededFromServer: Boolean = try { checkIfUpdatesNeededFromServer() + } catch (exception: CwaSecurityException) { + Log.e(TAG, "CwaSecurityException caught:" + exception.localizedMessage) + true } catch (exception: Exception) { - Log.e(TAG, exception.localizedMessage ?: "unknown error") - exception.printStackTrace() + Log.e(TAG, "Exception caught:" + exception.localizedMessage) false } 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 7cd591d3bbe..aba5ccaa6a5 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 @@ -13,7 +13,7 @@ import android.net.NetworkRequest import android.os.Build import android.util.Log import de.rki.coronawarnapp.exception.ExceptionCategory -import de.rki.coronawarnapp.exception.report +import de.rki.coronawarnapp.exception.reporting.report /** * Helper for connectivity statuses. diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/IndexHelper.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/IndexHelper.kt deleted file mode 100644 index 1ea82db6601..00000000000 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/IndexHelper.kt +++ /dev/null @@ -1,79 +0,0 @@ -/****************************************************************************** - * Corona-Warn-App * - * * - * SAP SE and all other contributors / * - * copyright owners license this file to you under the Apache * - * License, Version 2.0 (the "License"); you may not use this * - * file except in compliance with the License. * - * You may obtain a copy of the License at * - * * - * http://www.apache.org/licenses/LICENSE-2.0 * - * * - * Unless required by applicable law or agreed to in writing, * - * software distributed under the License is distributed on an * - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * - * KIND, either express or implied. See the License for the * - * specific language governing permissions and limitations * - * under the License. * - ******************************************************************************/ - -package de.rki.coronawarnapp.util - -import android.util.Log -import com.google.common.base.Splitter -import de.rki.coronawarnapp.BuildConfig -import java.util.regex.Pattern - -@Suppress("ComplexCondition", "TooGenericExceptionThrown") -object IndexHelper { - private val TAG: String? = IndexHelper::class.simpleName - private val WHITESPACE_SPLITTER: Splitter = - Splitter.onPattern("\\s+").trimResults().omitEmptyStrings() - - private val INDEX_ELEMENT_PATTERN: Pattern = Pattern.compile("Cwa-([A-Z]{2})/([0-9]{10})-([0-9]+).zip") - - private const val GROUP_COUNT = 3 - private const val COUNTRY_CODE_GROUP = 1 - private const val TIMESTAMP_GROUP = 2 - private const val BATCH_NUMBER_GROUP = 3 - - /** - * Converts a String to an index according to the Delimiter defined in [WHITESPACE_SPLITTER] and the - * Regular Expression of an index element defined in [INDEX_ELEMENT_PATTERN]. The Pattern has to define - * 3 Groups. - * 1. Country Code Information - * 2. Creation Epoch of the Batch - * 3. Batch Number - * - * @return Map of Batch Number to File Names (from the Index) - */ - fun String.convertToIndex(): Map = WHITESPACE_SPLITTER.splitToList(this).also { - if (BuildConfig.DEBUG) Log.d(TAG, "Index(${it.size} Elements):$it") - }.associateBy { indexElement -> - val matcher = INDEX_ELEMENT_PATTERN.matcher(indexElement) - if ( - !matcher.matches() || - matcher.groupCount() != GROUP_COUNT || - matcher.group(COUNTRY_CODE_GROUP) != null || - matcher.group(TIMESTAMP_GROUP) != null || - matcher.group(BATCH_NUMBER_GROUP) != null - ) throw RuntimeException("Failed to parse batch from $indexElement") - val isoCountryCode = - matcher.group(COUNTRY_CODE_GROUP) - ?: throw NullPointerException("Batch Regex Group 1 (Country Code) must not be null") - val timestampString = - matcher.group(TIMESTAMP_GROUP) - ?: throw NullPointerException("Batch Regex Group 2 (Timestamp) must not be null") - val batchNumberString = - matcher.group(BATCH_NUMBER_GROUP) - ?: throw NullPointerException("Batch Regex Group 3 (Batch Number) must not be null") - - if (BuildConfig.DEBUG) Log.d( - TAG, "index element " + - "$indexElement=(Timestamp:$timestampString, BatchNum:$batchNumberString, " + - "ISOCountryCode:$isoCountryCode" - ) - - timestampString.toLong() - } -} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/PropertyLoader.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/PropertyLoader.kt deleted file mode 100644 index c162ac72f29..00000000000 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/PropertyLoader.kt +++ /dev/null @@ -1,28 +0,0 @@ -package de.rki.coronawarnapp.util - -import android.util.Log -import de.rki.coronawarnapp.CoronaWarnApplication -import java.util.Properties - -class PropertyLoader { - companion object { - private const val PIN_PROPERTIES_FILE_NAME = "pins.properties" - private const val PIN_FILE_DELIMITER = "," - private const val DISTRIBUTION_PIN_PROPERTY_NAME = "DISTRIBUTION_PINS" - private const val SUBMISSION_PINS_PROPERTY_NAME = "SUBMISSION_PINS" - private const val VERIFICATION_PINS_PROPERTY_NAME = "VERIFICATION_PINS" - } - - fun getDistributionPins() = getCertificatePins(DISTRIBUTION_PIN_PROPERTY_NAME) - fun getSubmissionPins() = getCertificatePins(SUBMISSION_PINS_PROPERTY_NAME) - fun getVerificationPins() = getCertificatePins(VERIFICATION_PINS_PROPERTY_NAME) - - private fun getCertificatePins(key: String): Array = Properties().run { - this.load(CoronaWarnApplication.getAppContext().assets.open(PIN_PROPERTIES_FILE_NAME)) - this.getProperty(key) - .split(PIN_FILE_DELIMITER) - .filter { it.isNotEmpty() } - .also { Log.v(key, it.toString()) } - .toTypedArray() - } -} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/TanHelper.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/TanHelper.kt new file mode 100644 index 00000000000..9693fab69ac --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/TanHelper.kt @@ -0,0 +1,33 @@ +package de.rki.coronawarnapp.util + +import de.rki.coronawarnapp.ui.submission.TanConstants.MAX_LENGTH +import java.security.MessageDigest +import java.util.Locale + +object TanHelper { + private const val VALID_CHARACTERS = "23456789ABCDEFGHJKMNPQRSTUVWXYZ" + + fun isChecksumValid(tan: String): Boolean { + if (tan.trim().length != MAX_LENGTH) + return false + val subTan = tan.substring(0, MAX_LENGTH - 1).toUpperCase(Locale.getDefault()) + val tanDigest = MessageDigest.getInstance("SHA-256").digest(subTan.toByteArray()) + var checkChar = "%02x".format(tanDigest[0])[0] + if (checkChar == '0') checkChar = 'G' + if (checkChar == '1') checkChar = 'H' + + return checkChar.toUpperCase() == tan.last().toUpperCase() + } + + fun allCharactersValid(tan: String): Boolean { + for (character in tan) { + if (!isTanCharacterValid(character.toString())) + return false + } + return true + } + + fun isTanCharacterValid(character: String): Boolean { + return VALID_CHARACTERS.contains(character) + } +} 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 743c2af9459..9b0914336fa 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 @@ -352,12 +352,12 @@ fun formatStableIconColor(riskLevelScore: Int?): Int = * @param riskLevelScore * @return */ -fun formatStableBackButtonColor(riskLevelScore: Int?): ColorStateList? { +fun formatStableBackButtonIcon(riskLevelScore: Int?): Drawable? { val appContext = CoronaWarnApplication.getAppContext() - return if (!isTracingOffRiskLevel(riskLevelScore)) { - appContext.getColorStateList(R.color.button_back) + return if (isTracingOffRiskLevel(riskLevelScore)) { + appContext.getDrawable(R.drawable.ic_close_dark) } else { - appContext.getColorStateList(R.color.button_back_stable) + appContext.getDrawable(R.drawable.ic_close_light) } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/formatter/FormatterSubmissionHelper.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/formatter/FormatterSubmissionHelper.kt index dbe3e29ed71..c9502ce5d27 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/formatter/FormatterSubmissionHelper.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/formatter/FormatterSubmissionHelper.kt @@ -193,3 +193,8 @@ fun formatShowRiskStatusCard(deviceUiState: DeviceUIState?): Int = deviceUiState != DeviceUIState.PAIRED_POSITIVE_TELETAN && deviceUiState != DeviceUIState.SUBMITTED_FINAL ) + +fun formatShowTanCharacterError( + charactersValid: Boolean, + checksumValid: Boolean +): Int = formatVisibility(checksumValid && !charactersValid) diff --git a/Corona-Warn-App/src/main/res/drawable/ic_close_dark.xml b/Corona-Warn-App/src/main/res/drawable/ic_close_dark.xml new file mode 100644 index 00000000000..6b3c8307a45 --- /dev/null +++ b/Corona-Warn-App/src/main/res/drawable/ic_close_dark.xml @@ -0,0 +1,12 @@ + + + diff --git a/Corona-Warn-App/src/main/res/drawable/ic_close_light.xml b/Corona-Warn-App/src/main/res/drawable/ic_close_light.xml new file mode 100644 index 00000000000..a0fd695b693 --- /dev/null +++ b/Corona-Warn-App/src/main/res/drawable/ic_close_light.xml @@ -0,0 +1,12 @@ + + + diff --git a/Corona-Warn-App/src/main/res/drawable/tan_input_digit.xml b/Corona-Warn-App/src/main/res/drawable/tan_input_digit.xml index d06cdb9200e..48e3d0c51a8 100644 --- a/Corona-Warn-App/src/main/res/drawable/tan_input_digit.xml +++ b/Corona-Warn-App/src/main/res/drawable/tan_input_digit.xml @@ -1,11 +1,20 @@ - - - - \ No newline at end of file + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Corona-Warn-App/src/main/res/drawable/tan_input_digit_entered.xml b/Corona-Warn-App/src/main/res/drawable/tan_input_digit_entered.xml new file mode 100644 index 00000000000..0550164c034 --- /dev/null +++ b/Corona-Warn-App/src/main/res/drawable/tan_input_digit_entered.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/Corona-Warn-App/src/main/res/drawable/tan_input_digit_error.xml b/Corona-Warn-App/src/main/res/drawable/tan_input_digit_error.xml new file mode 100644 index 00000000000..01780eeb09b --- /dev/null +++ b/Corona-Warn-App/src/main/res/drawable/tan_input_digit_error.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + diff --git a/Corona-Warn-App/src/main/res/layout/fragment_information.xml b/Corona-Warn-App/src/main/res/layout/fragment_information.xml index da08ae6e6ea..931078c802b 100644 --- a/Corona-Warn-App/src/main/res/layout/fragment_information.xml +++ b/Corona-Warn-App/src/main/res/layout/fragment_information.xml @@ -25,7 +25,6 @@ app:title="@{@string/information_title}" /> + app:subtitle="@{@string/information_about_title}" /> + app:subtitle="@{@string/information_help_title}" /> + app:subtitle="@{@string/information_terms_title}" /> + app:subtitle="@{@string/information_privacy_title}" /> + app:subtitle="@{@string/information_technical_title}" /> + app:subtitle="@{@string/information_contact_title}" /> + app:subtitle="@{@string/information_legal_title}" /> - + app:layout_constraintTop_toBottomOf="@+id/information_legal_header_details" /> + + + + + + + + diff --git a/Corona-Warn-App/src/main/res/layout/fragment_information_privacy.xml b/Corona-Warn-App/src/main/res/layout/fragment_information_privacy.xml index 056e50cc3ee..637c7cee80d 100644 --- a/Corona-Warn-App/src/main/res/layout/fragment_information_privacy.xml +++ b/Corona-Warn-App/src/main/res/layout/fragment_information_privacy.xml @@ -26,7 +26,6 @@ app:title="@{@string/information_privacy_title}" /> - + app:layout_constraintTop_toBottomOf="@+id/information_privacy_header_details" /> diff --git a/Corona-Warn-App/src/main/res/layout/fragment_information_technical.xml b/Corona-Warn-App/src/main/res/layout/fragment_information_technical.xml index 19c05927253..50b2021336c 100644 --- a/Corona-Warn-App/src/main/res/layout/fragment_information_technical.xml +++ b/Corona-Warn-App/src/main/res/layout/fragment_information_technical.xml @@ -26,7 +26,6 @@ app:title="@{@string/information_technical_title}" /> - + app:layout_constraintTop_toBottomOf="@+id/information_technical_header_details" /> diff --git a/Corona-Warn-App/src/main/res/layout/fragment_information_terms.xml b/Corona-Warn-App/src/main/res/layout/fragment_information_terms.xml index 4fdffed28f0..17885b9567d 100644 --- a/Corona-Warn-App/src/main/res/layout/fragment_information_terms.xml +++ b/Corona-Warn-App/src/main/res/layout/fragment_information_terms.xml @@ -2,6 +2,14 @@ + + + + + + + + @@ -19,7 +27,6 @@ app:title="@{@string/information_terms_title}" /> - - - - + app:layout_constraintTop_toTopOf="parent"> @@ -117,9 +111,9 @@ @@ -128,7 +122,7 @@ layout="@layout/include_risk_card" android:layout_width="@dimen/match_constraint" android:layout_height="wrap_content" - android:layout_marginTop="@dimen/spacing_normal" + android:layout_marginTop="@dimen/spacing_small" android:visibility="@{FormatterSubmissionHelper.formatShowRiskStatusCard(submissionViewModel.deviceUiState)}" app:layout_constraintEnd_toStartOf="@+id/guideline_end" app:layout_constraintStart_toStartOf="@+id/guideline_start" @@ -140,7 +134,7 @@ + app:constraint_referenced_ids="main_test, main_test_done, main_test_positive, main_risk" /> + app:layout_constraintGuide_begin="@dimen/spacing_small" /> + app:layout_constraintGuide_end="@dimen/spacing_small" /> + app:layout_constraintGuide_end="@dimen/spacing_small" /> + app:layout_constraintGuide_begin="@dimen/spacing_small" /> diff --git a/Corona-Warn-App/src/main/res/layout/fragment_main_overview.xml b/Corona-Warn-App/src/main/res/layout/fragment_main_overview.xml index ea311e0253a..a2d3826e64a 100644 --- a/Corona-Warn-App/src/main/res/layout/fragment_main_overview.xml +++ b/Corona-Warn-App/src/main/res/layout/fragment_main_overview.xml @@ -19,7 +19,6 @@ app:title="@{@string/main_overview_title}" /> + app:subtitle="@{@string/main_overview_subtitle_increased_risk}" /> + app:subtitle="@{@string/main_overview_subtitle_low_risk}" /> + app:subtitle="@{@string/main_overview_subtitle_unknown_risk}" /> diff --git a/Corona-Warn-App/src/main/res/layout/fragment_risk_details.xml b/Corona-Warn-App/src/main/res/layout/fragment_risk_details.xml index 72b5d8ce565..3e5f3ad200d 100644 --- a/Corona-Warn-App/src/main/res/layout/fragment_risk_details.xml +++ b/Corona-Warn-App/src/main/res/layout/fragment_risk_details.xml @@ -24,8 +24,30 @@ android:layout_width="match_parent" android:layout_height="match_parent"> + + + + + + + app:layout_constraintTop_toBottomOf="@+id/risk_details_app_bar_layout"> + + + + + +