diff --git a/.circleci/config.yml b/.circleci/config.yml index 7d0ae9878ca..da41853c01b 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -11,8 +11,8 @@ jobs: steps: - checkout - install-ndk: - ndk-sha: "50250fcba479de477b45801e2699cca47f7e1267" - ndk-version: "android-ndk-r21b" + ndk-sha: "c81a5bcb4672a18d3647bf6898cd4dbcb978d0e8" + ndk-version: "android-ndk-r21c" - restore-build-cache - restore_cache: key: jars-{{ checksum "build.gradle" }}-{{ checksum "Corona-Warn-App/build.gradle" }}-{{ checksum "Server-Protocol-Buffer/build.gradle" }} @@ -36,4 +36,4 @@ workflows: version: 2 workflow: jobs: - - quickBuildReleaseWithTestsAndChecks \ No newline at end of file + - quickBuildReleaseWithTestsAndChecks diff --git a/Corona-Warn-App/build.gradle b/Corona-Warn-App/build.gradle index 42d44254931..ad0a14fdcb2 100644 --- a/Corona-Warn-App/build.gradle +++ b/Corona-Warn-App/build.gradle @@ -25,7 +25,7 @@ apply plugin: "androidx.navigation.safeargs.kotlin" apply plugin: 'jacoco' android { - ndkVersion "21.1.6352462" + ndkVersion "21.2.6472646" compileSdkVersion 29 buildToolsVersion "29.0.3" @@ -33,8 +33,8 @@ android { applicationId 'de.rki.coronawarnapp' minSdkVersion 23 targetSdkVersion 29 - versionCode 8 - versionName "0.8.1" + versionCode 9 + versionName "0.8.2" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" buildConfigField "String", "DOWNLOAD_CDN_URL", "\"$DOWNLOAD_CDN_URL\"" @@ -142,12 +142,11 @@ task jacocoTestReport(type: JacocoReport, dependsOn: ['testDeviceReleaseUnitTest } dependencies { - api fileTree(dir: 'libs', include: ['play-services-nearby-18.0.2-eap.aar']) - implementation project(":Server-Protocol-Buffer") - - + // KOTLIN implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.7" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + + // ANDROID STANDARD implementation 'androidx.appcompat:appcompat:1.1.0' implementation 'androidx.core:core-ktx:1.3.0' implementation 'com.google.android.material:material:1.1.0' @@ -158,15 +157,13 @@ dependencies { implementation 'androidx.preference:preference:1.1.1' implementation 'androidx.work:work-runtime-ktx:2.3.4' implementation 'android.arch.lifecycle:extensions:1.1.1' - implementation 'com.android.volley:volley:1.1.1' - implementation 'com.squareup.okhttp3:okhttp:4.7.2' - implementation 'com.google.android.play:core:1.7.3' - implementation 'com.google.code.gson:gson:2.8.6' - implementation 'com.google.guava:guava:29.0-android' + + // QR implementation('com.journeyapps:zxing-android-embedded:4.1.0') { transitive = false } // noinspection GradleDependency - needed for SDK 23 compatibility, in combination with com.journeyapps:zxing-android-embedded:4.1.0 implementation 'com.google.zxing:core:3.3.0' - // implementation 'com.google.android.gms:play-services-nearby:18.0.2-eap' + + // TESTING testImplementation 'junit:junit:4.13' testImplementation "org.mockito:mockito-core:3.3.3" testImplementation('org.robolectric:robolectric:4.3.1') { @@ -181,23 +178,41 @@ dependencies { androidTestImplementation 'androidx.test.ext:truth:1.2.0' androidTestImplementation 'androidx.test.ext:junit:1.1.1' - implementation 'joda-time:joda-time:2.10.6' + // Play Services + implementation 'com.google.android.play:core:1.7.3' implementation 'com.google.android.gms:play-services-base:17.2.1' implementation 'com.google.android.gms:play-services-basement:17.2.1' implementation 'com.google.android.gms:play-services-safetynet:17.0.0' implementation 'com.google.android.gms:play-services-tasks:17.0.2' + api fileTree(dir: 'libs', include: ['play-services-nearby-18.0.2-eap.aar']) - def room_version = "2.2.5" + // HTTP + implementation 'com.squareup.retrofit2:retrofit:2.9.0' + implementation 'com.squareup.retrofit2:converter-gson:2.9.0' + implementation ('com.squareup.retrofit2:converter-protobuf:2.9.0') { + exclude group: 'com.google.protobuf', module: 'protobuf-java' + } + implementation("com.squareup.okhttp3:logging-interceptor:4.7.2") + implementation 'com.squareup.okhttp3:okhttp:4.7.2' + // PERSISTENCE + def room_version = "2.2.5" implementation "androidx.room:room-runtime:$room_version" 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.security:security-crypto:1.0.0-rc02" + // UTILS + implementation project(":Server-Protocol-Buffer") + implementation 'com.google.code.gson:gson:2.8.6' + implementation 'com.google.guava:guava:29.0-android' + implementation 'joda-time:joda-time:2.10.6' + // SECURITY + implementation "androidx.security:security-crypto:1.0.0-rc02" implementation 'net.zetetic:android-database-sqlcipher:4.4.0' - implementation "androidx.sqlite:sqlite:2.0.1" + implementation 'org.conscrypt:conscrypt-android:2.4.0' } diff --git a/Corona-Warn-App/config/detekt.yml b/Corona-Warn-App/config/detekt.yml index 6780100fbf1..9cad3798d18 100644 --- a/Corona-Warn-App/config/detekt.yml +++ b/Corona-Warn-App/config/detekt.yml @@ -410,7 +410,7 @@ performance: excludes: ['**/test/**', '**/androidTest/**', '**/*.Test.kt', '**/*.Spec.kt', '**/*.Spek.kt'] SpreadOperator: active: true - excludes: ['**/test/**', '**/androidTest/**', '**/*.Test.kt', '**/*.Spec.kt', '**/*.Spek.kt'] + excludes: ['**/test/**', '**/androidTest/**', '**/*.Test.kt', '**/*.Spec.kt', '**/*.Spek.kt', '**/CertificatePinnerFactory.kt'] UnnecessaryTemporaryInstantiation: active: true diff --git a/Corona-Warn-App/proguard-rules.pro b/Corona-Warn-App/proguard-rules.pro index d7f94bd58f3..98d843e33e8 100644 --- a/Corona-Warn-App/proguard-rules.pro +++ b/Corona-Warn-App/proguard-rules.pro @@ -28,3 +28,33 @@ # If you keep the line number information, uncomment this to # hide the original source file name. #-renamesourcefileattribute SourceFile + +# Retrofit does reflection on generic parameters. InnerClasses is required to use Signature and +# EnclosingMethod is required to use InnerClasses. +-keepattributes Signature, InnerClasses, EnclosingMethod + +# Retrofit does reflection on method and parameter annotations. +-keepattributes RuntimeVisibleAnnotations, RuntimeVisibleParameterAnnotations + +# Retain service method parameters when optimizing. +-keepclassmembers,allowshrinking,allowobfuscation interface * { + @retrofit2.http.* ; +} + +# Ignore annotation used for build tooling. +-dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement + +# Ignore JSR 305 annotations for embedding nullability information. +-dontwarn javax.annotation.** + +# Guarded by a NoClassDefFoundError try/catch and only used when on the classpath. +-dontwarn kotlin.Unit + +# Top-level functions that can only be used by Kotlin. +-dontwarn retrofit2.KotlinExtensions +-dontwarn retrofit2.KotlinExtensions$* + +# With R8 full mode, it sees no subtypes of Retrofit interfaces since they are created with a Proxy +# and replaces all potential values with null. Explicitly keeping the interfaces prevents this. +-if interface * { @retrofit2.http.* ; } +-keep,allowobfuscation interface <1> diff --git a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/storage/ExposureSummaryDaoTest.kt b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/storage/ExposureSummaryDaoTest.kt new file mode 100644 index 00000000000..1fd325556b1 --- /dev/null +++ b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/storage/ExposureSummaryDaoTest.kt @@ -0,0 +1,75 @@ +package de.rki.coronawarnapp.storage + +import android.content.Context +import androidx.room.Room +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.runBlocking +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +/** + * ExposureSummaryDao test. + */ +@RunWith(AndroidJUnit4::class) +class ExposureSummaryDaoTest { + private lateinit var dao: ExposureSummaryDao + private lateinit var db: AppDatabase + + @Before + fun setUp() { + val context = ApplicationProvider.getApplicationContext() + db = Room.inMemoryDatabaseBuilder( + context, AppDatabase::class.java).build() + dao = db.exposureSummaryDao() + } + + /** + * Test Create / Read DB operations. + */ + @Test + fun testCROperations() { + runBlocking { + val testEntity1 = ExposureSummaryEntity().apply { + this.daysSinceLastExposure = 1 + this.matchedKeyCount = 1 + this.maximumRiskScore = 1 + this.summationRiskScore = 1 + } + + val testEntity2 = ExposureSummaryEntity().apply { + this.daysSinceLastExposure = 2 + this.matchedKeyCount = 2 + this.maximumRiskScore = 2 + this.summationRiskScore = 2 + } + + assertThat(dao.getExposureSummaryEntities().isEmpty()).isTrue() + + val id1 = dao.insertExposureSummaryEntity(testEntity1) + var selectAll = dao.getExposureSummaryEntities() + var selectLast = dao.getLatestExposureSummary() + assertThat(dao.getExposureSummaryEntities().isEmpty()).isFalse() + assertThat(selectAll.size).isEqualTo(1) + assertThat(selectAll[0].id).isEqualTo(id1) + assertThat(selectLast).isNotNull() + assertThat(selectLast?.id).isEqualTo(id1) + + val id2 = dao.insertExposureSummaryEntity(testEntity2) + selectAll = dao.getExposureSummaryEntities() + selectLast = dao.getLatestExposureSummary() + assertThat(selectAll.isEmpty()).isFalse() + assertThat(selectAll.size).isEqualTo(2) + assertThat(selectLast).isNotNull() + assertThat(selectLast?.id).isEqualTo(id2) + } + } + + @After + fun closeDb() { + db.close() + } +} diff --git a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/storage/keycache/KeyCacheDaoTest.kt b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/storage/keycache/KeyCacheDaoTest.kt new file mode 100644 index 00000000000..d47bd985d1e --- /dev/null +++ b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/storage/keycache/KeyCacheDaoTest.kt @@ -0,0 +1,87 @@ +package de.rki.coronawarnapp.storage.keycache + +import android.content.Context +import androidx.room.Room +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.common.truth.Truth.assertThat +import de.rki.coronawarnapp.storage.AppDatabase +import kotlinx.coroutines.runBlocking +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +/** + * KeyCacheDao test. + */ +@RunWith(AndroidJUnit4::class) +class KeyCacheDaoTest { + private lateinit var keyCacheDao: KeyCacheDao + private lateinit var db: AppDatabase + + @Before + fun setUp() { + val context = ApplicationProvider.getApplicationContext() + db = Room.inMemoryDatabaseBuilder( + context, AppDatabase::class.java).build() + keyCacheDao = db.dateDao() + } + + /** + * Test Create / Read / Delete DB operations. + */ + @Test + fun testCRDOperations() { + runBlocking { + val dates = KeyCacheEntity().apply { + this.id = "0" + this.path = "0" + this.type = 0 + } + val hours = KeyCacheEntity().apply { + this.id = "1" + this.path = "1" + this.type = 1 + } + + assertThat(keyCacheDao.getAllEntries().isEmpty()).isTrue() + + keyCacheDao.insertEntry(dates) + keyCacheDao.insertEntry(hours) + + var all = keyCacheDao.getAllEntries() + + assertThat(all.size).isEqualTo(2) + + val selectedDates = keyCacheDao.getDates() + assertThat(selectedDates.size).isEqualTo(1) + assertThat(selectedDates[0].type).isEqualTo(0) + assertThat(selectedDates[0].id).isEqualTo(dates.id) + + val selectedHours = keyCacheDao.getHours() + assertThat(selectedHours.size).isEqualTo(1) + assertThat(selectedHours[0].type).isEqualTo(1) + assertThat(selectedHours[0].id).isEqualTo(hours.id) + + keyCacheDao.clearHours() + + all = keyCacheDao.getAllEntries() + assertThat(all.size).isEqualTo(1) + assertThat(all[0].type).isEqualTo(0) + + keyCacheDao.insertEntry(hours) + + assertThat(keyCacheDao.getAllEntries().size).isEqualTo(2) + + keyCacheDao.clear() + + assertThat(keyCacheDao.getAllEntries().isEmpty()).isTrue() + } + } + + @After + fun closeDb() { + db.close() + } +} diff --git a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/storage/tracing/TracingIntervalDaoTest.kt b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/storage/tracing/TracingIntervalDaoTest.kt new file mode 100644 index 00000000000..797f5c58491 --- /dev/null +++ b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/storage/tracing/TracingIntervalDaoTest.kt @@ -0,0 +1,73 @@ +package de.rki.coronawarnapp.storage.tracing + +import android.content.Context +import androidx.room.Room +import androidx.test.core.app.ApplicationProvider + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.common.truth.Truth.assertThat +import de.rki.coronawarnapp.storage.AppDatabase +import kotlinx.coroutines.runBlocking +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import java.util.Date + +/** + * TracingIntervalDao test. + */ +@RunWith(AndroidJUnit4::class) +class TracingIntervalDaoTest { + private lateinit var dao: TracingIntervalDao + private lateinit var db: AppDatabase + + @Before + fun setUp() { + val context = ApplicationProvider.getApplicationContext() + db = Room.inMemoryDatabaseBuilder( + context, AppDatabase::class.java).build() + dao = db.tracingIntervalDao() + } + + /** + * Test Create / Read / Delete DB operations. + */ + @Test + fun testCRDOperations() { + runBlocking { + val oneDay = 24 * 60 * 60 * 1000 + val today = Date().time + val testEntity = TracingIntervalEntity().apply { + // minus 1 day + this.from = today - oneDay + this.to = today + } + + assertThat(dao.getAllIntervals().isEmpty()).isTrue() + + dao.insertInterval(testEntity) + + var select = dao.getAllIntervals() + assertThat(select.isEmpty()).isFalse() + assertThat(select.size).isEqualTo(1) + assertThat(select[0].from).isEqualTo(today - oneDay) + assertThat(select[0].to).isEqualTo(today) + + dao.deleteOutdatedIntervals(today - 1) + + select = dao.getAllIntervals() + assertThat(select.isEmpty()).isFalse() + assertThat(select.size).isEqualTo(1) + + dao.deleteOutdatedIntervals(today + 1) + select = dao.getAllIntervals() + assertThat(select.isEmpty()).isTrue() + } + } + + @After + fun closeDb() { + db.close() + } +} diff --git a/Corona-Warn-App/src/main/assets/pins.properties b/Corona-Warn-App/src/main/assets/pins.properties new file mode 100644 index 00000000000..198e2e9bbfd --- /dev/null +++ b/Corona-Warn-App/src/main/assets/pins.properties @@ -0,0 +1,13 @@ +# 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 3e151d6899f..c41efeec1fc 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 @@ -13,6 +13,8 @@ import androidx.lifecycle.LifecycleObserver import androidx.lifecycle.OnLifecycleEvent import androidx.lifecycle.ProcessLifecycleOwner import de.rki.coronawarnapp.notification.NotificationHelper +import org.conscrypt.Conscrypt +import java.security.Security class CoronaWarnApplication : Application(), LifecycleObserver, Application.ActivityLifecycleCallbacks { @@ -35,6 +37,8 @@ class CoronaWarnApplication : Application(), LifecycleObserver, override fun onCreate() { instance = this NotificationHelper.createNotificationChannel() + // Enable Conscrypt for TLS1.3 Support below API Level 29 + Security.insertProviderAt(Conscrypt.newProvider(), 1) super.onCreate() ProcessLifecycleOwner.get().lifecycle.addObserver(this) registerActivityLifecycleCallbacks(this) 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 907d9c8eea6..e755d25ccb3 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 @@ -32,6 +32,7 @@ import de.rki.coronawarnapp.nearby.InternalExposureNotificationPermissionHelper import de.rki.coronawarnapp.receiver.ExposureStateUpdateReceiver import de.rki.coronawarnapp.risk.TimeVariables import de.rki.coronawarnapp.server.protocols.AppleLegacyKeyExchange +import de.rki.coronawarnapp.service.applicationconfiguration.ApplicationConfigurationService import de.rki.coronawarnapp.sharing.ExposureSharingService import de.rki.coronawarnapp.storage.AppDatabase import de.rki.coronawarnapp.storage.ExposureSummaryRepository @@ -348,7 +349,7 @@ class TestForAPIFragment : Fragment(), InternalExposureNotificationPermissionHel // only testing implementation: this is used to wait for the broadcastreceiver of the OS / EN API InternalExposureNotificationClient.asyncProvideDiagnosisKeys( googleFileList, - getCustomConfig(), + ApplicationConfigurationService.asyncRetrieveExposureConfiguration(), token!! ) showToast("Provided ${appleKeyList.size} keys to Google API with token $token") 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 new file mode 100644 index 00000000000..b1f9739c044 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/TestRiskLevelCalculation.kt @@ -0,0 +1,262 @@ +package de.rki.coronawarnapp + +import android.content.Intent +import android.os.Bundle +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Toast +import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels +import androidx.lifecycle.Observer +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.viewModelScope +import com.google.android.gms.nearby.exposurenotification.ExposureSummary +import com.google.zxing.integration.android.IntentIntegrator +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.nearby.InternalExposureNotificationClient +import de.rki.coronawarnapp.risk.RiskLevel +import de.rki.coronawarnapp.risk.TimeVariables +import de.rki.coronawarnapp.server.protocols.AppleLegacyKeyExchange +import de.rki.coronawarnapp.server.protocols.ApplicationConfigurationOuterClass +import de.rki.coronawarnapp.service.applicationconfiguration.ApplicationConfigurationService +import de.rki.coronawarnapp.sharing.ExposureSharingService +import de.rki.coronawarnapp.storage.LocalData +import de.rki.coronawarnapp.transaction.RetrieveDiagnosisKeysTransaction +import de.rki.coronawarnapp.transaction.RiskLevelTransaction +import de.rki.coronawarnapp.ui.viewmodel.SettingsViewModel +import de.rki.coronawarnapp.ui.viewmodel.SubmissionViewModel +import de.rki.coronawarnapp.ui.viewmodel.TracingViewModel +import de.rki.coronawarnapp.util.KeyFileHelper +import kotlinx.coroutines.launch +import java.io.File +import java.util.UUID +import java.util.concurrent.TimeUnit + +@Suppress("MagicNumber") +class TestRiskLevelCalculation : Fragment() { + companion object { + val TAG: String? = TestRiskLevelCalculation::class.simpleName + } + + data class TransactionValues( + var appConfig: ApplicationConfigurationOuterClass.ApplicationConfiguration? = null, + var exposureSummary: ExposureSummary? = null, + var riskScore: Double? = null, + var riskLevel: RiskLevel? = null + ) + + private val tracingViewModel: TracingViewModel by activityViewModels() + private val settingsViewModel: SettingsViewModel by activityViewModels() + private val submissionViewModel: SubmissionViewModel by activityViewModels() + private lateinit var binding: FragmentTestRiskLevelCalculationBinding + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + binding = FragmentTestRiskLevelCalculationBinding.inflate(inflater) + binding.tracingViewModel = tracingViewModel + binding.settingsViewModel = settingsViewModel + binding.submissionViewModel = submissionViewModel + binding.lifecycleOwner = this + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + binding.buttonRetrieveDiagnosisKeys.setOnClickListener { + tracingViewModel.viewModelScope.launch { + retrieveDiagnosisKeys() + } + } + + binding.buttonProvideKeyViaQr.setOnClickListener { + scanLocalQRCodeAndProvide() + } + + binding.buttonCalculateRiskLevel.setOnClickListener { + tracingViewModel.viewModelScope.launch { + calculateRiskLevel() + } + } + + startObserving() + } + + override fun onResume() { + super.onResume() + tracingViewModel.viewModelScope.launch { + RiskLevelTransaction.recordedTransactionValuesForTestingOnly = TransactionValues() + calculateRiskLevel() + } + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + + val result: IntentResult? = + IntentIntegrator.parseActivityResult(requestCode, resultCode, data) + if (result != null) { + if (result.contents == null) { + Toast.makeText(requireContext(), "Cancelled", Toast.LENGTH_LONG).show() + } else { + ExposureSharingService.getOthersKeys(result.contents, onScannedKey) + } + } else { + super.onActivityResult(requestCode, resultCode, data) + } + } + + private suspend fun retrieveDiagnosisKeys() { + try { + RetrieveDiagnosisKeysTransaction.start() + } catch (e: TransactionException) { + e.report(ExceptionCategory.INTERNAL) + } + } + + private fun scanLocalQRCodeAndProvide() { + IntentIntegrator.forSupportFragment(this) + .setOrientationLocked(false) + .setBeepEnabled(false) + .initiateScan() + } + + private val onScannedKey = { key: AppleLegacyKeyExchange.Key? -> + Log.i(TestForAPIFragment.TAG, "keys scanned..") + provideDiagnosisKey(key) + } + + private fun provideDiagnosisKey(key: AppleLegacyKeyExchange.Key?) { + if (null == key) { + Toast.makeText(requireContext(), "No Key data found in QR code", Toast.LENGTH_SHORT) + .show() + } else { + val token = UUID.randomUUID().toString() + LocalData.googleApiToken(token) + + val appleKeyList = mutableListOf() + + appleKeyList.add( + AppleLegacyKeyExchange.Key.newBuilder() + .setKeyData(key.keyData) + .setRollingPeriod(144) + .setRollingStartNumber(key.rollingStartNumber) + .setTransmissionRiskLevel(1) + .build() + ) + + val appleFiles = listOf( + AppleLegacyKeyExchange.File.newBuilder() + .addAllKeys(appleKeyList) + .build() + ) + + val dir = + File(File(requireContext().getExternalFilesDir(null), "key-export"), token) + dir.mkdirs() + + var googleFileList: List + lifecycleScope.launch { + googleFileList = KeyFileHelper.asyncCreateExportFiles(appleFiles, dir) + + Log.i( + TAG, + "Provide ${googleFileList.count()} files with ${appleKeyList.size} keys with token $token" + ) + try { + // only testing implementation: this is used to wait for the broadcastreceiver of the OS / EN API + InternalExposureNotificationClient.asyncProvideDiagnosisKeys( + googleFileList, + ApplicationConfigurationService.asyncRetrieveExposureConfiguration(), + token + ) + Toast.makeText( + requireContext(), + "Provided ${appleKeyList.size} keys to Google API with token $token", + Toast.LENGTH_SHORT + ).show() + } catch (e: Exception) { + e.report(ExceptionCategory.EXPOSURENOTIFICATION) + } + } + } + } + + private suspend fun calculateRiskLevel() { + try { + RiskLevelTransaction.start() + } catch (e: TransactionException) { + e.report(ExceptionCategory.INTERNAL) + } + } + + private fun startObserving() { + RiskLevelTransaction.tempExposedTransactionValuesForTestingOnly.observe( + viewLifecycleOwner, + Observer { + tracingViewModel.viewModelScope.launch { + val riskAsString = "Level: ${it.riskLevel}\n" + + "Calc. Score: ${it.riskScore}\n" + + "Tracing Duration: " + + "${TimeUnit.MILLISECONDS.toDays(TimeVariables.getTimeActiveTracingDuration())} days \n" + + "Tracing Duration in last 14 days: " + + "${TimeVariables.getActiveTracingDaysInRetentionPeriod()} days" + binding.labelRiskScore.text = riskAsString + + val lowClass = + it.appConfig?.riskScoreClasses?.riskClassesList?.find { low -> low.label == "LOW" } + val highClass = + it.appConfig?.riskScoreClasses?.riskClassesList?.find { high -> high.label == "HIGH" } + + val configAsString = + "Attenuation Weight Low: ${it.appConfig?.attenuationDuration?.weights?.low}\n" + + "Attenuation Weight Mid: ${it.appConfig?.attenuationDuration?.weights?.mid}\n" + + "Attenuation Weight High: ${it.appConfig?.attenuationDuration?.weights?.high}\n\n" + + "Attenuation Offset: ${it.appConfig?.attenuationDuration?.defaultBucketOffset}\n" + + "Attenuation Normalization: " + + "${it.appConfig?.attenuationDuration?.riskScoreNormalizationDivisor}\n\n" + + "Risk Score Low Class: ${lowClass?.min ?: 0} - ${lowClass?.max ?: 0}\n" + + "Risk Score High Class: ${highClass?.min ?: 0} - ${highClass?.max ?: 0}" + + binding.labelBackendParameters.text = configAsString + + val summaryAsString = + "Days Since Last Exposure: ${it.exposureSummary?.daysSinceLastExposure}\n" + + "Matched Key Count: ${it.exposureSummary?.matchedKeyCount}\n" + + "Maximum Risk Score: ${it.exposureSummary?.maximumRiskScore}\n" + + "Attenuation Durations: [${it.exposureSummary?.attenuationDurationsInMinutes?.get( + 0 + )}," + + "${it.exposureSummary?.attenuationDurationsInMinutes?.get(1)}," + + "${it.exposureSummary?.attenuationDurationsInMinutes?.get(2)}]\n" + + "Summation Risk Score: ${it.exposureSummary?.summationRiskScore}" + + binding.labelExposureSummary.text = summaryAsString + + val maxRisk = it.exposureSummary?.maximumRiskScore + val atWeights = it.appConfig?.attenuationDuration?.weights + val attenuationDurationInMin = + it.exposureSummary?.attenuationDurationsInMinutes + val attenuationConfig = it.appConfig?.attenuationDuration + val formulaString = + "($maxRisk / ${attenuationConfig?.riskScoreNormalizationDivisor}) * " + + "(${attenuationDurationInMin?.get(0)} * ${atWeights?.low} " + + "+ ${attenuationDurationInMin?.get(1)} * ${atWeights?.mid} " + + "+ ${attenuationDurationInMin?.get(2)} * ${atWeights?.high} " + + "+ ${attenuationConfig?.defaultBucketOffset})" + + binding.labelFormula.text = formulaString + + binding.labelFullConfig.text = it.appConfig?.toString() + } + }) + } +} 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/ErrorReportReceiver.kt new file mode 100644 index 00000000000..f9d52f4f917 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/exception/ErrorReportReceiver.kt @@ -0,0 +1,53 @@ +package de.rki.coronawarnapp.exception + +import android.app.Activity +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.util.Log +import de.rki.coronawarnapp.CoronaWarnApplication +import de.rki.coronawarnapp.R +import de.rki.coronawarnapp.util.DialogHelper + +class ErrorReportReceiver(private val activity: Activity) : BroadcastReceiver() { + companion object { + private val TAG: String = ErrorReportReceiver::class.java.simpleName + } + override fun onReceive(context: Context, intent: Intent) { + val category = ExceptionCategory + .valueOf(intent.getStringExtra(ReportingConstants.ERROR_REPORT_CATEGORY_EXTRA) ?: "") + val prefix = intent.getStringExtra(ReportingConstants.ERROR_REPORT_PREFIX_EXTRA) + val suffix = intent.getStringExtra(ReportingConstants.ERROR_REPORT_SUFFIX_EXTRA) + val message = intent.getStringExtra(ReportingConstants.ERROR_REPORT_MESSAGE_EXTRA) + ?: context.resources.getString(R.string.errors_generic_text_unknown_error_cause) + val stack = intent.getStringExtra(ReportingConstants.ERROR_REPORT_STACK_EXTRA) + val title = context.resources.getString(R.string.errors_generic_headline) + val confirm = context.resources.getString(R.string.errors_generic_button_positive) + val details = context.resources.getString(R.string.errors_generic_button_negative) + val detailsTitle = context.resources.getString(R.string.errors_generic_details_headline) + if (CoronaWarnApplication.isAppInForeground) { + DialogHelper.showDialog(DialogHelper.DialogInstance( + activity, + title, + message, + confirm, + details, + null, + {}, + { + DialogHelper.showDialog( + DialogHelper.DialogInstance( + activity, + title, + "$detailsTitle:\n$stack", + confirm + )).run {} + } + )) + } + Log.e( + TAG, + "[$category]${(prefix ?: "")} $message${(suffix ?: "")}" + ) + } +} 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/ExceptionReporter.kt index 273b168eba8..65b1713f114 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/ExceptionReporter.kt @@ -1,11 +1,10 @@ package de.rki.coronawarnapp.exception -import android.util.Log -import android.widget.Toast +import android.content.Intent +import androidx.localbroadcastmanager.content.LocalBroadcastManager import de.rki.coronawarnapp.CoronaWarnApplication -import kotlinx.coroutines.runBlocking - -private const val TAG: String = "ExceptionHandler" +import java.io.PrintWriter +import java.io.StringWriter fun Throwable.report(exceptionCategory: ExceptionCategory) = this.report(exceptionCategory, null, null) @@ -15,16 +14,23 @@ fun Throwable.report( prefix: String?, suffix: String? ) { - runBlocking { - Toast.makeText( - CoronaWarnApplication.getAppContext(), - this@report.localizedMessage ?: "This should never happen.", - Toast.LENGTH_SHORT - ).show() - } + val intent = Intent(ReportingConstants.ERROR_REPORT_LOCAL_BROADCAST_CHANNEL) + intent.putExtra(ReportingConstants.ERROR_REPORT_CATEGORY_EXTRA, exceptionCategory.name) + intent.putExtra(ReportingConstants.ERROR_REPORT_PREFIX_EXTRA, prefix) + intent.putExtra(ReportingConstants.ERROR_REPORT_SUFFIX_EXTRA, suffix) + intent.putExtra(ReportingConstants.ERROR_REPORT_MESSAGE_EXTRA, this.message) + val sw = StringWriter() + this.printStackTrace() + this.printStackTrace(PrintWriter(sw)) + intent.putExtra(ReportingConstants.ERROR_REPORT_STACK_EXTRA, sw.toString()) + LocalBroadcastManager.getInstance(CoronaWarnApplication.getAppContext()).sendBroadcast(intent) +} - Log.e( - TAG, - "[${exceptionCategory.name}]${(prefix ?: "")} ${(this.message ?: "Error Text Unavailable")}${(suffix ?: "")}" - ) +fun Throwable.reportGeneric( + stackString: String +) { + val intent = Intent(ReportingConstants.ERROR_REPORT_LOCAL_BROADCAST_CHANNEL) + intent.putExtra("category", ExceptionCategory.INTERNAL.name) + intent.putExtra("stack", stackString) + LocalBroadcastManager.getInstance(CoronaWarnApplication.getAppContext()).sendBroadcast(intent) } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/exception/NoTokenException.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/exception/NoTokenException.kt new file mode 100644 index 00000000000..6140302afde --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/exception/NoTokenException.kt @@ -0,0 +1,8 @@ +package de.rki.coronawarnapp.exception + +class NoTokenException( + cause: Throwable +) : Exception( + "An error occurred during BroadcastReceiver onReceive function. No token found", + cause +) 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/ReportingConstants.kt new file mode 100644 index 00000000000..22f1c2393c8 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/exception/ReportingConstants.kt @@ -0,0 +1,10 @@ +package de.rki.coronawarnapp.exception + +object ReportingConstants { + const val ERROR_REPORT_LOCAL_BROADCAST_CHANNEL = "error-report" + const val ERROR_REPORT_CATEGORY_EXTRA = "category" + const val ERROR_REPORT_PREFIX_EXTRA = "prefix" + const val ERROR_REPORT_SUFFIX_EXTRA = "suffix" + const val ERROR_REPORT_MESSAGE_EXTRA = "message" + const val ERROR_REPORT_STACK_EXTRA = "stack" +} 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 new file mode 100644 index 00000000000..7a103bea083 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/http/CertificatePinnerFactory.kt @@ -0,0 +1,23 @@ +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/DynamicURLs.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/http/DynamicURLs.kt index 6f83c12e8d7..20dd1781424 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/DynamicURLs.kt @@ -3,6 +3,7 @@ package de.rki.coronawarnapp.http import de.rki.coronawarnapp.BuildConfig object DynamicURLs { + const val PATTERN_PREFIX_HTTPS = "https://" /** CDN URLs for querying against the Server from the Build Config for downloading keys */ var DOWNLOAD_CDN_URL = BuildConfig.DOWNLOAD_CDN_URL 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/OfflineCacheInterceptor.kt new file mode 100644 index 00000000000..15bf62893e8 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/http/OfflineCacheInterceptor.kt @@ -0,0 +1,48 @@ +package de.rki.coronawarnapp.http + +import android.content.Context +import de.rki.coronawarnapp.util.ConnectivityHelper.isNetworkEnabled +import okhttp3.Interceptor +import okhttp3.Response + +class OfflineCacheInterceptor(private val context: Context) : Interceptor { + companion object { + private const val MAX_AGE = 5 + private const val MAX_STALE_DAYS = 1 + private const val MAX_STALE = 60 * 60 * 24 * MAX_STALE_DAYS + } + override fun intercept(chain: Interceptor.Chain): Response { + // Get the request from the chain. + var request = chain.request() + /* + * Leveraging the advantage of using Kotlin, + * we initialize the request and change its header depending on whether + * the device is connected to Internet or not. + */ + request = if (isNetworkEnabled(context)) { + /* + * If there is Internet, get the cache that was stored 5 seconds ago. + * If the cache is older than 5 seconds, then discard it, + * and indicate an error in fetching the response. + * The 'max-age' attribute is responsible for this behavior. + */ + request.newBuilder().header( + "Cache-Control", + "public, max-age=$MAX_AGE" + ).build() + } else { + /* + * If there is no Internet, get the cache that was stored 1 days ago. + * If the cache is older than 1 day, then discard it, + * and indicate an error in fetching the response. + * The 'max-stale' attribute is responsible for this behavior. + * The 'only-if-cached' attribute indicates to not retrieve new data; fetch the cache only instead. + */ + request.newBuilder().header( + "Cache-Control", + "public, only-if-cached, max-stale=$MAX_STALE" + ).build() + } + return chain.proceed(request) + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/http/OkHttp3Stack.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/http/OkHttp3Stack.kt deleted file mode 100644 index 0e60fa00b1f..00000000000 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/http/OkHttp3Stack.kt +++ /dev/null @@ -1,190 +0,0 @@ -package de.rki.coronawarnapp.http - -import android.content.Context -import com.android.volley.Header -import com.android.volley.Request -import com.android.volley.toolbox.BaseHttpStack -import com.android.volley.toolbox.HttpResponse -import de.rki.coronawarnapp.risk.TimeVariables -import okhttp3.Cache -import okhttp3.ConnectionPool -import okhttp3.ConnectionSpec -import okhttp3.Headers -import okhttp3.Interceptor -import okhttp3.MediaType.Companion.toMediaType -import okhttp3.OkHttpClient -import okhttp3.RequestBody -import okhttp3.RequestBody.Companion.toRequestBody -import okhttp3.TlsVersion -import java.io.File -import java.util.concurrent.TimeUnit - -/** - * Convenience Wrapper used for accessing the OkHTTP Transport layer with the Volley Interfaces. - * Designed for: - * - in-memory connection management - * - disk based caching (10MB) - * - extension based on volleys BaseHttpStack - * - volley request queue - * - synchronous calls executed by asynchronous thread-pool - * - * @param context - * @param interceptors - */ -class OkHttp3Stack(context: Context, interceptors: List) : BaseHttpStack() { - constructor(context: Context) : this(context, emptyList()) - - /** - * List of interceptors, e.g. logging - */ - private val mInterceptors: List = interceptors - - /** - * connection pool held in-memory, especially useful for key retrieval - */ - private val conPool = ConnectionPool() - - /** - * Basic disk cache backed by LRU - */ - private val cache = Cache( - directory = File(context.cacheDir, HTTP_CACHE_NAME), - maxSize = HTTP_CACHE_SIZE - ) - - /** - * lazily initialized client instance of OkHTTP - */ - private val client by lazy { buildClient() } - - /** - * Convenience method to map headers from volley to OkHTTP style. - */ - private fun mapHeaders(responseHeaders: Headers): List
= - responseHeaders.map { Header(it.first, it.second) } - - companion object { - /** - * 10 MiB - */ - private const val HTTP_CACHE_SIZE = 50L * 1024L * 1024L - - /** - * Cache file name - */ - private const val HTTP_CACHE_NAME = "http_cache" - - /** - * Convenience method used for building the correct request type. - */ - private fun setConnectionParametersForRequest( - builder: okhttp3.Request.Builder, - request: Request<*> - ) { - when (request.method) { - Request.Method.DEPRECATED_GET_OR_POST -> { throw IllegalArgumentException("deprecated.") } - Request.Method.GET -> builder.get() - Request.Method.DELETE -> builder.delete(createRequestBody(request)) - Request.Method.POST -> builder.post(createRequestBody(request)!!) - Request.Method.PUT -> builder.put(createRequestBody(request)!!) - Request.Method.HEAD -> builder.head() - Request.Method.OPTIONS -> builder.method("OPTIONS", null) - Request.Method.TRACE -> builder.method("TRACE", null) - Request.Method.PATCH -> builder.patch(createRequestBody(request)!!) - else -> throw IllegalStateException("Unknown method type.") - } - } - - /** - * Convenience method to create a request body based on MediaType - * - * @param volleyRequest - */ - private fun createRequestBody(volleyRequest: Request<*>): RequestBody? = - volleyRequest.body.toRequestBody( - volleyRequest.bodyContentType.toMediaType(), - 0, - volleyRequest.body.size - ) - } - - override fun executeRequest( - request: Request<*>, - additionalHeaders: MutableMap? - ): HttpResponse { - val okHttpRequest = buildRequest(request, additionalHeaders) - - val okHttpCall = client.newCall(okHttpRequest) - val okHttpResponse = okHttpCall.execute() - - val code = okHttpResponse.code - val body = okHttpResponse.body - val content = body?.byteStream() - val contentLength = body?.contentLength()?.toInt() ?: 0 - val responseHeaders = mapHeaders(okHttpResponse.headers) - return HttpResponse(code, responseHeaders, contentLength, content) - } - - /** - * Wrapper around the OkHTTP request builder used to set header fields, url and connection params. - * - * @param request - * @param additionalHeaders - */ - private fun buildRequest(request: Request<*>, additionalHeaders: MutableMap?): - okhttp3.Request { - val okHttpRequestBuilder = okhttp3.Request.Builder() - okHttpRequestBuilder.url(request.url) - - val headers = request.headers - headers.forEach { - okHttpRequestBuilder.addHeader(it.key, it.value) - } - additionalHeaders?.forEach { - okHttpRequestBuilder.addHeader(it.key, it.value) - } - - setConnectionParametersForRequest(okHttpRequestBuilder, request) - - return okHttpRequestBuilder.build() - } - - /** - * Helper method used to build the client with connection pool, timeout values, caching, - * connection specs, interceptors and other generic meta-info. - */ - private fun buildClient(): OkHttpClient { - 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.connectionPool(conPool) - - cache.evictAll() - clientBuilder.cache(cache) - - val spec: ConnectionSpec = ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS) - .tlsVersions(TlsVersion.TLS_1_2, TlsVersion.TLS_1_3) - .allEnabledCipherSuites() // TODO clarify more concrete Ciphers - .build() - - clientBuilder.connectionSpecs(listOf(spec)) - - // TODO add certificate pinning -// val certificatePinner = CertificatePinner.Builder() -// .add( -// "x.de", -// "sha256/base64" -// ) -// .build() -// clientBuilder.certificatePinner(certificatePinner) - - mInterceptors.forEach { clientBuilder.addInterceptor(it) } - - return clientBuilder.build() - } -} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/http/RequestQueueHolder.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/http/RequestQueueHolder.kt deleted file mode 100644 index 2c16c7709b9..00000000000 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/http/RequestQueueHolder.kt +++ /dev/null @@ -1,32 +0,0 @@ -package de.rki.coronawarnapp.http - -import com.android.volley.Request -import com.android.volley.RequestQueue -import de.rki.coronawarnapp.CoronaWarnApplication -import de.rki.coronawarnapp.util.security.SecurityHelper - -/** - * Request queue holder used to reference a singleton of a Volley request queue for simple web requests. - * The Singleton is used here solely for Thread-Management, actual requests are executed by an OkHTTP - * Transport Layer - */ -object RequestQueueHolder { - /** - * lazily initialized singleton reference to a request queue. - */ - private val requestQueue: RequestQueue by lazy { - // applicationContext is key, it keeps you from leaking the - // Activity or BroadcastReceiver if someone passes one in. - SecurityHelper.getPinnedWebStack(CoronaWarnApplication.getAppContext()) - } - - /** - * Adds a request to the queue. - * - * @param T return type of the request - * @param req a given request - */ - fun addToRequestQueue(req: Request) { - requestQueue.add(req) - } -} 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/RetryInterceptor.kt new file mode 100644 index 00000000000..c4bfa179b98 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/http/RetryInterceptor.kt @@ -0,0 +1,23 @@ +package de.rki.coronawarnapp.http + +import android.util.Log +import okhttp3.Interceptor +import okhttp3.Request +import okhttp3.Response + +class RetryInterceptor : Interceptor { + companion object { + private const val MAX_RETRY_COUNT = 3 + } + override fun intercept(chain: Interceptor.Chain): Response { + val request: Request = chain.request() + var response = chain.proceed(request) + var tryCount = 0 + while (!response.isSuccessful && tryCount < MAX_RETRY_COUNT) { + Log.d(this.javaClass.simpleName, "Request is not successful - $tryCount") + tryCount++ + response = chain.proceed(request) + } + return response + } +} 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 new file mode 100644 index 00000000000..b17803daad1 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/http/ServiceFactory.kt @@ -0,0 +1,122 @@ +package de.rki.coronawarnapp.http + +import de.rki.coronawarnapp.BuildConfig +import de.rki.coronawarnapp.CoronaWarnApplication +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.ConnectionPool +import okhttp3.ConnectionSpec +import okhttp3.Interceptor +import okhttp3.OkHttpClient +import okhttp3.logging.HttpLoggingInterceptor +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory +import retrofit2.converter.protobuf.ProtoConverterFactory +import java.io.File +import java.util.concurrent.TimeUnit + +class ServiceFactory { + companion object { + /** + * 10 MiB + */ + private const val HTTP_CACHE_SIZE = 10L * 1024L * 1024L + + /** + * Cache file name + */ + private const val HTTP_CACHE_NAME = "http_cache" + } + + /** + * List of interceptors, e.g. logging + */ + private val mInterceptors: List = listOf( + HttpLoggingInterceptor().also { + if (BuildConfig.DEBUG) it.setLevel(HttpLoggingInterceptor.Level.BODY) + }, + OfflineCacheInterceptor(CoronaWarnApplication.getAppContext()), + RetryInterceptor() + ) + + /** + * connection pool held in-memory, especially useful for key retrieval + */ + private val conPool = ConnectionPool() + + /** + * Basic disk cache backed by LRU + */ + private val cache = Cache( + directory = File(CoronaWarnApplication.getAppContext().cacheDir, HTTP_CACHE_NAME), + maxSize = HTTP_CACHE_SIZE + ) + + private val gsonConverterFactory = GsonConverterFactory.create() + private val protoConverterFactory = ProtoConverterFactory.create() + + 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.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() + } + + fun distributionService(): DistributionService = distributionService + private val distributionService by lazy { + Retrofit.Builder() + .client(okHttpClient) + .baseUrl(DynamicURLs.DOWNLOAD_CDN_URL) + .addConverterFactory(gsonConverterFactory) + .build() + .create(DistributionService::class.java) + } + + fun verificationService(): VerificationService = verificationService + private val verificationService by lazy { + Retrofit.Builder() + .client(okHttpClient) + .baseUrl(DynamicURLs.VERIFICATION_CDN_URL) + .addConverterFactory(gsonConverterFactory) + .build() + .create(VerificationService::class.java) + } + + fun submissionService(): SubmissionService = submissionService + private val submissionService by lazy { + Retrofit.Builder() + .client(okHttpClient) + .baseUrl(DynamicURLs.SUBMISSION_CDN_URL) + .addConverterFactory(protoConverterFactory) + .addConverterFactory(gsonConverterFactory) + .build() + .create(SubmissionService::class.java) + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/http/WebRequestBuilder.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/http/WebRequestBuilder.kt index 68167454aaf..a5e68d8526f 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/http/WebRequestBuilder.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/http/WebRequestBuilder.kt @@ -21,245 +21,139 @@ package de.rki.coronawarnapp.http import KeyExportFormat import android.util.Log -import com.android.volley.DefaultRetryPolicy -import com.android.volley.Response -import com.android.volley.VolleyError -import com.android.volley.toolbox.JsonArrayRequest -import com.android.volley.toolbox.StringRequest -import de.rki.coronawarnapp.BuildConfig -import de.rki.coronawarnapp.exception.WebRequestException -import de.rki.coronawarnapp.exception.report -import de.rki.coronawarnapp.http.request.ApplicationConfigurationRequest -import de.rki.coronawarnapp.http.request.KeyFileRequest -import de.rki.coronawarnapp.http.request.KeySubmissionRequest -import de.rki.coronawarnapp.http.request.RegistrationTokenRequest -import de.rki.coronawarnapp.http.request.TanRequest -import de.rki.coronawarnapp.http.request.TestResultRequest -import de.rki.coronawarnapp.risk.TimeVariables +import de.rki.coronawarnapp.http.requests.RegistrationTokenRequest +import de.rki.coronawarnapp.http.requests.ReqistrationRequest +import de.rki.coronawarnapp.http.requests.TanRequestBody import de.rki.coronawarnapp.server.protocols.ApplicationConfigurationOuterClass.ApplicationConfiguration -import de.rki.coronawarnapp.util.IndexHelper.convertToIndex -import org.json.JSONArray +import de.rki.coronawarnapp.service.diagnosiskey.DiagnosisKeyConstants +import de.rki.coronawarnapp.service.submission.SubmissionConstants +import de.rki.coronawarnapp.storage.FileStorageHelper +import de.rki.coronawarnapp.util.TimeAndDateExtensions.toServerFormat +import de.rki.coronawarnapp.util.ZipHelper.unzip +import de.rki.coronawarnapp.util.security.SecurityHelper +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import java.io.File -import java.util.ArrayList +import java.util.Date import java.util.UUID -import kotlin.coroutines.Continuation -import kotlin.coroutines.resume -import kotlin.coroutines.resumeWithException -import kotlin.coroutines.suspendCoroutine object WebRequestBuilder { private val TAG: String? = WebRequestBuilder::class.simpleName - private const val MAX_RETRIES = 3 - private const val BACKOFF_MULTIPLIER = 2F + private val serviceFactory = ServiceFactory() - private val standardRetryPolicy by lazy { - DefaultRetryPolicy( - TimeVariables.getTransactionTimeout().toInt(), - MAX_RETRIES, - BACKOFF_MULTIPLIER - ) - } + private val distributionService = serviceFactory.distributionService() + private val verificationService = serviceFactory.verificationService() + private val submissionService = serviceFactory.submissionService() - suspend fun asyncGetArrayListFromGenericRequest( - url: String, - parser: (JSONArray) -> ArrayList - ) = - suspendCoroutine> { cont -> - val requestID = UUID.randomUUID() - val getArrayListRequest = JsonArrayRequest( - url, Response.Listener { response -> - Log.d(TAG, "$requestID: Response is $response") - cont.resume(parser(response)) - }, RequestErrorListener(requestID, cont) - ) - RequestQueueHolder.addToRequestQueue(getArrayListRequest) - Log.d(TAG, "$requestID: Added $url to queue.") - } + suspend fun asyncGetDateIndex(): List = withContext(Dispatchers.IO) { + return@withContext distributionService + .getDateIndex(DiagnosisKeyConstants.AVAILABLE_DATES_URL).toList() + } - suspend fun asyncGetIndexBasedQueryURLsFromServer(url: String) = - suspendCoroutine> { cont -> - val requestID = UUID.randomUUID() - val getArrayListRequest = StringRequest( - url, Response.Listener { response -> - if (BuildConfig.DEBUG) Log.d(TAG, "$requestID: Response is $response") - cont.resume(response.convertToIndex()) - }, RequestErrorListener(requestID, cont) + suspend fun asyncGetHourIndex(day: Date): List = withContext(Dispatchers.IO) { + return@withContext distributionService + .getHourIndex( + DiagnosisKeyConstants.AVAILABLE_DATES_URL + + "/${day.toServerFormat()}/${DiagnosisKeyConstants.HOUR}" ) - RequestQueueHolder.addToRequestQueue(getArrayListRequest) - Log.d(TAG, "$requestID: Added $url to queue.") - } + .toList() + } /** * Retrieves Key Files from the Server based on a URL * * @param url the given URL */ - suspend fun asyncGetKeyFilesFromServer(url: String) = - suspendCoroutine { cont -> - val requestID = UUID.randomUUID() - val request = KeyFileRequest( - url, - requestID, - null, - Response.Listener { response -> - Log.v(requestID.toString(), "key file request successful. (${response.length()}B)") - cont.resume(response) - }, - RequestErrorListener(requestID, cont) - ).also { keyFileRequest -> - keyFileRequest.retryPolicy = standardRetryPolicy - keyFileRequest.setShouldCache(false) - keyFileRequest.setShouldRetryServerErrors(true) - } - RequestQueueHolder.addToRequestQueue(request) + suspend fun asyncGetKeyFilesFromServer( + url: String + ): File = withContext(Dispatchers.IO) { + val requestID = UUID.randomUUID() + val fileName = "${UUID.nameUUIDFromBytes(url.toByteArray())}.zip" + val file = File(FileStorageHelper.keyExportDirectory, fileName) + file.outputStream().use { Log.v(requestID.toString(), "Added $url to queue.") + distributionService.getKeyFiles(url).byteStream().copyTo(it, DEFAULT_BUFFER_SIZE) + Log.v(requestID.toString(), "key file request successful.") } + return@withContext file + } - suspend fun asyncGetApplicationConfigurationFromServer(url: String) = - suspendCoroutine { cont -> - val requestID = UUID.randomUUID() - val getKeyBucketRequest = - ApplicationConfigurationRequest( - url, - requestID, - null, - Response.Listener { response -> cont.resume(response) }, - RequestErrorListener(requestID, cont) - ) - RequestQueueHolder.addToRequestQueue(getKeyBucketRequest) - Log.d(TAG, "$requestID: Added $url to queue.") + suspend fun asyncGetApplicationConfigurationFromServer(): ApplicationConfiguration = + withContext(Dispatchers.IO) { + var applicationConfiguration: ApplicationConfiguration? = null + distributionService.getApplicationConfiguration( + DiagnosisKeyConstants.COUNTRY_APPCONFIG_DOWNLOAD_URL + ).byteStream().unzip { entry, entryContent -> + if (entry.name == "export.bin") { + val appConfig = ApplicationConfiguration.parseFrom(entryContent) + applicationConfiguration = appConfig + } + if (entry.name == "export.sig") { + val signatures = KeyExportFormat.TEKSignatureList.parseFrom(entryContent) + signatures.signaturesList.forEach { + Log.d(TAG, it.signatureInfo.toString()) + } + } + } + if (applicationConfiguration == null) { + throw IllegalArgumentException("no file was found in the downloaded zip") + } + return@withContext applicationConfiguration!! } suspend fun asyncGetRegistrationToken( - url: String, key: String, keyType: String - ) = - suspendCoroutine { cont -> - val requestID = UUID.randomUUID() - val getRegistrationTokenRequest = - RegistrationTokenRequest( - url, - requestID, - keyType, - key, - false, - standardRetryPolicy, - Response.Listener { response -> - Log.d( - TAG, - "$requestID: Registration Token Request successful" - ) - cont.resume(response) - }, - RequestErrorListener(requestID, cont) - ) - RequestQueueHolder.addToRequestQueue(getRegistrationTokenRequest) - Log.d(TAG, "$requestID: Added $url to queue.") + ): String = withContext(Dispatchers.IO) { + val keyStr = if (keyType == SubmissionConstants.QR_CODE_KEY_TYPE) { + SecurityHelper.hash256(key) + } else { + key } + verificationService.getRegistrationToken( + SubmissionConstants.REGISTRATION_TOKEN_URL, + "0", + RegistrationTokenRequest(keyType, keyStr) + ).registrationToken + } suspend fun asyncGetTestResult( - url: String, registrationToken: String - ) = - suspendCoroutine { cont -> - val requestID = UUID.randomUUID() - val getTestResultRequest = - TestResultRequest( - url, - requestID, - registrationToken, - false, - standardRetryPolicy, - - Response.Listener { response -> - Log.d( - TAG, - "$requestID: Test Result Request successful" - ) - cont.resume(response) - }, - RequestErrorListener(requestID, cont) - ) - RequestQueueHolder.addToRequestQueue(getTestResultRequest) - Log.d(TAG, "$requestID: Added $url to queue.") - } + ): Int = withContext(Dispatchers.IO) { + verificationService.getTestResult( + SubmissionConstants.TEST_RESULT_URL, + "0", ReqistrationRequest(registrationToken) + ).testResult + } suspend fun asyncGetTan( - url: String, registrationToken: String - ) = - suspendCoroutine { cont -> - val requestID = UUID.randomUUID() - val getTANRequest = - TanRequest( - url, - requestID, - registrationToken, - false, - standardRetryPolicy, - - Response.Listener { response -> - Log.d( - TAG, - "$requestID: TAN Request successful" - ) - cont.resume(response) - }, - RequestErrorListener(requestID, cont) - ) - RequestQueueHolder.addToRequestQueue(getTANRequest) - Log.d(TAG, "$requestID: Added $url to queue.") - } + ): String = withContext(Dispatchers.IO) { + verificationService.getTAN(SubmissionConstants.TAN_REQUEST_URL, "0", + TanRequestBody( + registrationToken + ) + ).tan + } suspend fun asyncSubmitKeysToServer( - url: String, authCode: String, faked: Boolean, keyList: List - ) = - suspendCoroutine { cont -> - val requestID = UUID.randomUUID() - Log.d(TAG, "Writing ${keyList.size} Keys to the Submission Payload.") - val submissionPayload = KeyExportFormat.SubmissionPayload.newBuilder() - .addAllKeys(keyList) - .build() - .toByteArray() - val submitKeysRequest = - KeySubmissionRequest( - url, - requestID, - submissionPayload, - authCode, - faked, - standardRetryPolicy, - Response.Listener { response -> - Log.d( - TAG, - "$requestID: Key Submission Request successful." - ) - cont.resume(response) - }, - RequestErrorListener(requestID, cont) - ) - RequestQueueHolder.addToRequestQueue(submitKeysRequest) - Log.d(TAG, "$requestID: Added $url to queue.") - } - - private class RequestErrorListener( - private val requestID: UUID, - private val cont: Continuation - ) : - Response.ErrorListener { - override fun onErrorResponse(error: VolleyError?) { - if (error != null) { - val webRequestException = WebRequestException("an error occurred during a webrequest", error) - webRequestException.report(de.rki.coronawarnapp.exception.ExceptionCategory.HTTP) - cont.resumeWithException(webRequestException) - } else { - cont.resumeWithException(NullPointerException("the provided exception from volley was null")) - } - } + ) = withContext(Dispatchers.IO) { + Log.d(TAG, "Writing ${keyList.size} Keys to the Submission Payload.") + val submissionPayload = KeyExportFormat.SubmissionPayload.newBuilder() + .addAllKeys(keyList) + .build() + var fakeHeader = "0" + if (faked) fakeHeader = Math.random().toInt().toString() + submissionService.submitKeys( + DiagnosisKeyConstants.DIAGNOSIS_KEYS_SUBMISSION_URL, + authCode, + fakeHeader, + submissionPayload + ) + return@withContext } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/http/request/ApplicationConfigurationRequest.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/http/request/ApplicationConfigurationRequest.kt deleted file mode 100644 index a12c88fed40..00000000000 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/http/request/ApplicationConfigurationRequest.kt +++ /dev/null @@ -1,77 +0,0 @@ -package de.rki.coronawarnapp.http.request - -import KeyExportFormat -import android.util.Log -import com.android.volley.Cache.Entry -import com.android.volley.NetworkResponse -import com.android.volley.ParseError -import com.android.volley.Request -import com.android.volley.Response -import com.android.volley.toolbox.HttpHeaderParser -import de.rki.coronawarnapp.exception.ExceptionCategory.HTTP -import de.rki.coronawarnapp.exception.report -import de.rki.coronawarnapp.server.protocols.ApplicationConfigurationOuterClass.ApplicationConfiguration -import de.rki.coronawarnapp.util.ZipHelper.unzip - -class ApplicationConfigurationRequest( - url: String, - private val requestTag: Any?, - private val headers: MutableMap?, - private val listener: Response.Listener, - errorListener: Response.ErrorListener -) : Request(Method.GET, url, errorListener) { - - companion object { - private val TAG: String? = ApplicationConfigurationRequest::class.simpleName - - private const val SOFT_TTL = 5 * 60 * 1000 // in 5 minutes cache will be hit, but also refreshed on background - private const val TTL = 1 * 60 * 60 * 1000 // in 1 hours this cache entry expires completely - } - - override fun getTag(): Any = requestTag ?: super.getTag().also { this.addMarker("tag:$it") } - override fun deliverResponse(response: ApplicationConfiguration) = listener.onResponse(response) - override fun getHeaders(): MutableMap = headers ?: super.getHeaders() - - override fun parseNetworkResponse(response: NetworkResponse?): Response { - return try { - var cacheEntry = HttpHeaderParser.parseCacheHeaders(response) - if (cacheEntry == null && response != null) { - Log.v(TAG, "new cache entry.") - cacheEntry = Entry() - cacheEntry.data = response.data - cacheEntry.softTtl = SOFT_TTL.toLong() - cacheEntry.ttl = TTL.toLong() - cacheEntry.responseHeaders = response.headers - } else { - Log.v(TAG, "using cache entry") - } - - var applicationConfiguration: ApplicationConfiguration? = null - response!!.data.inputStream().unzip { entry, entryContent -> - if (entry.name == "export.bin") { - val appConfig = ApplicationConfiguration.parseFrom(entryContent) -// Log.d(TAG, "app config from zip: $appConfig") - applicationConfiguration = appConfig - } - if (entry.name == "export.sig") { - val signatures = KeyExportFormat.TEKSignatureList.parseFrom(entryContent) - signatures.signaturesList.forEach { - Log.d(TAG, it.signatureInfo.toString()) - } - } - } - if (applicationConfiguration == null) { - throw IllegalArgumentException("no file was found in the downloaded zip") - } - - Response.success(applicationConfiguration, cacheEntry) - } catch (e: Exception) { - e.report( - HTTP, - requestTag.toString(), - null - ) - Response.error(ParseError(e)) - } - } -} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/http/request/KeyFileRequest.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/http/request/KeyFileRequest.kt deleted file mode 100644 index 00966a2fb57..00000000000 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/http/request/KeyFileRequest.kt +++ /dev/null @@ -1,43 +0,0 @@ -package de.rki.coronawarnapp.http.request - -import android.util.Log -import com.android.volley.NetworkResponse -import com.android.volley.Request -import com.android.volley.Response -import com.android.volley.toolbox.HttpHeaderParser -import de.rki.coronawarnapp.storage.FileStorageHelper -import java.io.File -import java.util.UUID - -/** - * Request Class used for querying ZIP files containing the key files - * - * @property requestTag a given request tag to add as tag - * @property headers given header values - * @property listener given listener to call on response - * - * @param url given request URL - * @param errorListener given listener to call on error - */ -class KeyFileRequest( - url: String, - private val requestTag: Any?, - private val headers: MutableMap?, - private val listener: Response.Listener, - errorListener: Response.ErrorListener -) : Request(Method.GET, url, errorListener) { - - override fun getTag(): Any = requestTag ?: super.getTag().also { this.addMarker("tag:$it") } - override fun deliverResponse(response: File) = listener.onResponse(response) - override fun getHeaders(): MutableMap = headers ?: super.getHeaders() - - override fun parseNetworkResponse(response: NetworkResponse): Response { - val fileName = "${UUID.nameUUIDFromBytes(url.toByteArray())}.zip" - Log.v(tag.toString(), "$url results in file name $fileName") - val file = File(FileStorageHelper.keyExportDirectory, fileName) - file.outputStream().use { - response.data.inputStream().copyTo(it, DEFAULT_BUFFER_SIZE) - } - return Response.success(file, HttpHeaderParser.parseCacheHeaders(response)) - } -} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/http/request/KeySubmissionRequest.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/http/request/KeySubmissionRequest.kt deleted file mode 100644 index e1fd16f017f..00000000000 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/http/request/KeySubmissionRequest.kt +++ /dev/null @@ -1,69 +0,0 @@ -package de.rki.coronawarnapp.http.request - -import android.util.Log -import com.android.volley.DefaultRetryPolicy -import com.android.volley.NetworkResponse -import com.android.volley.ParseError -import com.android.volley.Request -import com.android.volley.Response -import com.android.volley.RetryPolicy -import com.android.volley.toolbox.HttpHeaderParser -import com.google.protobuf.InvalidProtocolBufferException -import de.rki.coronawarnapp.exception.ExceptionCategory.HTTP -import de.rki.coronawarnapp.exception.report -import java.io.UnsupportedEncodingException - -class KeySubmissionRequest( - url: String, - private val requestTag: Any?, - private val binaryBody: ByteArray, - private val authCode: String, - private val faked: Boolean, - private val retryPolicy: RetryPolicy?, - private val listener: Response.Listener, - errorListener: Response.ErrorListener -) : Request(Method.POST, url, errorListener) { - - companion object { - private val TAG: String? = KeySubmissionRequest::class.simpleName - } - - override fun getTag(): Any = requestTag ?: super.getTag().also { this.addMarker("tag:$it") } - - override fun getBodyContentType(): String = - "application/x-protobuf".also { this.addMarker("bodyContentType:$it") } - - override fun getBody(): ByteArray = binaryBody - - override fun deliverResponse(response: Int) = listener.onResponse(response) - - override fun getRetryPolicy(): RetryPolicy = - retryPolicy ?: (super.getRetryPolicy() ?: DefaultRetryPolicy()) - - override fun getHeaders(): MutableMap { - val headers = HashMap(super.getHeaders()) - if (faked) headers["cwa-fake"] = Math.random().toInt().toString() - else headers["cwa-fake"] = "0" - headers["cwa-authorization"] = authCode - this.addMarker("headers:$headers") - return headers - } - - override fun parseNetworkResponse(response: NetworkResponse?): Response { - return try { - if (response == null) throw NullPointerException("response is null, this should never be the case") - return Response.success( - response.statusCode, - HttpHeaderParser.parseCacheHeaders(response) - ) - } catch (e: UnsupportedEncodingException) { - e.report(HTTP) - Log.e(TAG, "invalid encoding found") - Response.error(ParseError(e)) - } catch (e: InvalidProtocolBufferException) { - Log.e(TAG, "invalid protobuf message, probably the object parse failed") - e.report(HTTP) - Response.error(ParseError(e)) - } - } -} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/http/request/RegistrationTokenRequest.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/http/request/RegistrationTokenRequest.kt deleted file mode 100644 index 1a76aac8ca8..00000000000 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/http/request/RegistrationTokenRequest.kt +++ /dev/null @@ -1,92 +0,0 @@ -package de.rki.coronawarnapp.http.request - -import android.util.Log -import com.android.volley.DefaultRetryPolicy -import com.android.volley.NetworkResponse -import com.android.volley.ParseError -import com.android.volley.Request -import com.android.volley.Response -import com.android.volley.RetryPolicy -import com.android.volley.toolbox.HttpHeaderParser -import de.rki.coronawarnapp.exception.ExceptionCategory.HTTP -import de.rki.coronawarnapp.exception.report -import de.rki.coronawarnapp.service.submission.SubmissionConstants -import org.json.JSONException -import org.json.JSONObject -import java.io.UnsupportedEncodingException -import java.security.MessageDigest - -class RegistrationTokenRequest( - url: String, - private val requestTag: Any?, - private val keyType: String, - private val key: String, - private val faked: Boolean, - private val retryPolicy: RetryPolicy?, - private val listener: Response.Listener, - errorListener: Response.ErrorListener -) : Request(Method.POST, url, errorListener) { - - companion object { - private val TAG: String? = RegistrationTokenRequest::class.simpleName - } - - override fun getTag(): Any = requestTag ?: super.getTag().also { this.addMarker("tag:$it") } - - override fun getBodyContentType(): String = - "application/json".also { this.addMarker("bodyContentType:$it") } - - override fun getBody(): ByteArray { - val body = JSONObject() - - var keyStr = "" - if (keyType == SubmissionConstants.QR_CODE_KEY_TYPE) { - val md = MessageDigest.getInstance("SHA-256") - val keyDigest = md.digest(key.toByteArray()) - - for (b in keyDigest) { - keyStr += String.format("%02x", b) - } - } else { - keyStr = key - } - - body.put("keyType", keyType) - body.put("key", keyStr) - return body.toString().toByteArray() - } - - override fun deliverResponse(response: String) = listener.onResponse(response) - - override fun getRetryPolicy(): RetryPolicy = - retryPolicy ?: (super.getRetryPolicy() ?: DefaultRetryPolicy()) - - override fun getHeaders(): MutableMap { - val headers = HashMap(super.getHeaders()) - if (faked) headers["cwa-fake"] = Math.random().toInt().toString() - else headers["cwa-fake"] = "0" - this.addMarker("headers:$headers") - return headers - } - - override fun parseNetworkResponse(response: NetworkResponse?): Response { - return try { - if (response == null) throw NullPointerException("response is null, this should never be the case") - val strResp = String(response.data) - val jsonObj = JSONObject(strResp) - val registrationToken = jsonObj.getString("registrationToken") - return Response.success( - registrationToken, - HttpHeaderParser.parseCacheHeaders(response) - ) - } catch (e: UnsupportedEncodingException) { - e.report(HTTP) - Log.e(TAG, "invalid encoding found") - Response.error(ParseError(e)) - } catch (e: JSONException) { - Log.e(TAG, "invalid JSON message, probably the object parse failed") - e.report(HTTP) - Response.error(ParseError(e)) - } - } -} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/http/request/TanRequest.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/http/request/TanRequest.kt deleted file mode 100644 index 553fb3cae10..00000000000 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/http/request/TanRequest.kt +++ /dev/null @@ -1,76 +0,0 @@ -package de.rki.coronawarnapp.http.request - -import android.util.Log -import com.android.volley.DefaultRetryPolicy -import com.android.volley.NetworkResponse -import com.android.volley.ParseError -import com.android.volley.Request -import com.android.volley.Response -import com.android.volley.RetryPolicy -import com.android.volley.toolbox.HttpHeaderParser -import de.rki.coronawarnapp.exception.ExceptionCategory.HTTP -import de.rki.coronawarnapp.exception.report -import org.json.JSONException -import org.json.JSONObject -import java.io.UnsupportedEncodingException - -class TanRequest( - url: String, - private val requestTag: Any?, - private val registrationToken: String, - private val faked: Boolean, - private val retryPolicy: RetryPolicy?, - private val listener: Response.Listener, - errorListener: Response.ErrorListener -) : Request(Method.POST, url, errorListener) { - - companion object { - private val TAG: String? = TanRequest::class.simpleName - } - - override fun getTag(): Any = requestTag ?: super.getTag().also { this.addMarker("tag:$it") } - - override fun getBodyContentType(): String = - "application/json".also { this.addMarker("bodyContentType:$it") } - - override fun getBody(): ByteArray { - val body = JSONObject() - - body.put("registrationToken", registrationToken) - return body.toString().toByteArray() - } - - override fun deliverResponse(response: String) = listener.onResponse(response) - - override fun getRetryPolicy(): RetryPolicy = - retryPolicy ?: (super.getRetryPolicy() ?: DefaultRetryPolicy()) - - override fun getHeaders(): MutableMap { - val headers = HashMap(super.getHeaders()) - if (faked) headers["cwa-fake"] = Math.random().toInt().toString() - else headers["cwa-fake"] = "0" - this.addMarker("headers:$headers") - return headers - } - - override fun parseNetworkResponse(response: NetworkResponse?): Response { - return try { - if (response == null) throw NullPointerException("response is null, this should never be the case") - val strResp = String(response.data) - val jsonObj = JSONObject(strResp) - val testResult = jsonObj.getString("tan") - return Response.success( - testResult, - HttpHeaderParser.parseCacheHeaders(response) - ) - } catch (e: UnsupportedEncodingException) { - e.report(HTTP) - Log.e(TAG, "invalid encoding found") - Response.error(ParseError(e)) - } catch (e: JSONException) { - Log.e(TAG, "invalid JSON message, probably the object parse failed") - e.report(HTTP) - Response.error(ParseError(e)) - } - } -} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/http/request/TestResultRequest.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/http/request/TestResultRequest.kt deleted file mode 100644 index 6634f8008e6..00000000000 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/http/request/TestResultRequest.kt +++ /dev/null @@ -1,76 +0,0 @@ -package de.rki.coronawarnapp.http.request - -import android.util.Log -import com.android.volley.DefaultRetryPolicy -import com.android.volley.NetworkResponse -import com.android.volley.ParseError -import com.android.volley.Request -import com.android.volley.Response -import com.android.volley.RetryPolicy -import com.android.volley.toolbox.HttpHeaderParser -import de.rki.coronawarnapp.exception.ExceptionCategory.HTTP -import de.rki.coronawarnapp.exception.report -import org.json.JSONException -import org.json.JSONObject -import java.io.UnsupportedEncodingException - -class TestResultRequest( - url: String, - private val requestTag: Any?, - private val registrationToken: String, - private val faked: Boolean, - private val retryPolicy: RetryPolicy?, - private val listener: Response.Listener, - errorListener: Response.ErrorListener -) : Request(Method.POST, url, errorListener) { - - companion object { - private val TAG: String? = TestResultRequest::class.simpleName - } - - override fun getTag(): Any = requestTag ?: super.getTag().also { this.addMarker("tag:$it") } - - override fun getBodyContentType(): String = - "application/json".also { this.addMarker("bodyContentType:$it") } - - override fun getBody(): ByteArray { - val body = JSONObject() - - body.put("registrationToken", registrationToken) - return body.toString().toByteArray() - } - - override fun deliverResponse(response: Int) = listener.onResponse(response) - - override fun getRetryPolicy(): RetryPolicy = - retryPolicy ?: (super.getRetryPolicy() ?: DefaultRetryPolicy()) - - override fun getHeaders(): MutableMap { - val headers = HashMap(super.getHeaders()) - if (faked) headers["cwa-fake"] = Math.random().toInt().toString() - else headers["cwa-fake"] = "0" - this.addMarker("headers:$headers") - return headers - } - - override fun parseNetworkResponse(response: NetworkResponse?): Response { - return try { - if (response == null) throw NullPointerException("response is null, this should never be the case") - val strResp = String(response.data) - val jsonObj = JSONObject(strResp) - val testResult = jsonObj.getInt("testResult") - return Response.success( - testResult, - HttpHeaderParser.parseCacheHeaders(response) - ) - } catch (e: UnsupportedEncodingException) { - e.report(HTTP) - Log.e(TAG, "invalid encoding found") - Response.error(ParseError(e)) - } catch (e: JSONException) { - Log.e(TAG, "invalid JSON message, probably the object parse failed") - e.report(HTTP) - Response.error(ParseError(e)) - } - } -} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/http/requests/RegistrationTokenRequest.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/http/requests/RegistrationTokenRequest.kt new file mode 100644 index 00000000000..64b51092bc8 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/http/requests/RegistrationTokenRequest.kt @@ -0,0 +1,10 @@ +package de.rki.coronawarnapp.http.requests + +import com.google.gson.annotations.SerializedName + +data class RegistrationTokenRequest( + @SerializedName("keyType") + val keyType: String, + @SerializedName("key") + val key: String +) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/http/requests/ReqistrationRequest.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/http/requests/ReqistrationRequest.kt new file mode 100644 index 00000000000..1c5dceb5504 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/http/requests/ReqistrationRequest.kt @@ -0,0 +1,8 @@ +package de.rki.coronawarnapp.http.requests + +import com.google.gson.annotations.SerializedName + +data class ReqistrationRequest( + @SerializedName("registrationToken") + val registrationToken: String +) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/http/requests/TanRequestBody.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/http/requests/TanRequestBody.kt new file mode 100644 index 00000000000..a06f7aa9b56 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/http/requests/TanRequestBody.kt @@ -0,0 +1,8 @@ +package de.rki.coronawarnapp.http.requests + +import com.google.gson.annotations.SerializedName + +data class TanRequestBody( + @SerializedName("registrationToken") + val registrationToken: String +) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/http/responses/RegistrationTokenResponse.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/http/responses/RegistrationTokenResponse.kt new file mode 100644 index 00000000000..4ec189715dc --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/http/responses/RegistrationTokenResponse.kt @@ -0,0 +1,8 @@ +package de.rki.coronawarnapp.http.responses + +import com.google.gson.annotations.SerializedName + +data class RegistrationTokenResponse( + @SerializedName("registrationToken") + val registrationToken: String +) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/http/responses/TanResponse.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/http/responses/TanResponse.kt new file mode 100644 index 00000000000..91f1ea9b7c0 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/http/responses/TanResponse.kt @@ -0,0 +1,8 @@ +package de.rki.coronawarnapp.http.responses + +import com.google.gson.annotations.SerializedName + +data class TanResponse( + @SerializedName("tan") + val tan: String +) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/http/responses/TestResultResponse.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/http/responses/TestResultResponse.kt new file mode 100644 index 00000000000..065a2356f20 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/http/responses/TestResultResponse.kt @@ -0,0 +1,8 @@ +package de.rki.coronawarnapp.http.responses + +import com.google.gson.annotations.SerializedName + +data class TestResultResponse( + @SerializedName("testResult") + val testResult: Int +) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/http/service/DistributionService.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/http/service/DistributionService.kt new file mode 100644 index 00000000000..9eef5b91ff6 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/http/service/DistributionService.kt @@ -0,0 +1,20 @@ +package de.rki.coronawarnapp.http.service + +import okhttp3.ResponseBody +import retrofit2.http.GET +import retrofit2.http.Url + +interface DistributionService { + + @GET + suspend fun getDateIndex(@Url url: String): List + + @GET + suspend fun getHourIndex(@Url url: String): List + + @GET + suspend fun getKeyFiles(@Url url: String): ResponseBody + + @GET + suspend fun getApplicationConfiguration(@Url url: String): ResponseBody +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/http/service/SubmissionService.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/http/service/SubmissionService.kt new file mode 100644 index 00000000000..e902a25b291 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/http/service/SubmissionService.kt @@ -0,0 +1,17 @@ +package de.rki.coronawarnapp.http.service + +import okhttp3.ResponseBody +import retrofit2.http.Body +import retrofit2.http.Header +import retrofit2.http.POST +import retrofit2.http.Url + +interface SubmissionService { + @POST + suspend fun submitKeys( + @Url url: String, + @Header("cwa-authorization") authCode: String, + @Header("cwa-fake") fake: String, + @Body requestBody: KeyExportFormat.SubmissionPayload + ): ResponseBody +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/http/service/VerificationService.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/http/service/VerificationService.kt new file mode 100644 index 00000000000..688ceba4c53 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/http/service/VerificationService.kt @@ -0,0 +1,36 @@ +package de.rki.coronawarnapp.http.service + +import de.rki.coronawarnapp.http.requests.RegistrationTokenRequest +import de.rki.coronawarnapp.http.requests.ReqistrationRequest +import de.rki.coronawarnapp.http.requests.TanRequestBody +import de.rki.coronawarnapp.http.responses.RegistrationTokenResponse +import de.rki.coronawarnapp.http.responses.TanResponse +import de.rki.coronawarnapp.http.responses.TestResultResponse +import retrofit2.http.Body +import retrofit2.http.Header +import retrofit2.http.POST +import retrofit2.http.Url + +interface VerificationService { + + @POST + suspend fun getRegistrationToken( + @Url url: String, + @Header("cwa-fake") fake: String, + @Body requestBody: RegistrationTokenRequest + ): RegistrationTokenResponse + + @POST + suspend fun getTestResult( + @Url url: String, + @Header("cwa-fake") fake: String, + @Body request: ReqistrationRequest + ): TestResultResponse + + @POST + suspend fun getTAN( + @Url url: String, + @Header("cwa-fake") fake: String, + @Body requestBody: TanRequestBody + ): TanResponse +} 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 new file mode 100644 index 00000000000..bc97b2c7a0f --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/ExposureStateUpdateWorker.kt @@ -0,0 +1,47 @@ +package de.rki.coronawarnapp.nearby + +import android.content.Context +import android.util.Log +import androidx.work.CoroutineWorker +import androidx.work.WorkerParameters +import com.google.android.gms.common.api.ApiException +import com.google.android.gms.nearby.exposurenotification.ExposureNotificationClient +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.storage.ExposureSummaryRepository +import de.rki.coronawarnapp.transaction.RiskLevelTransaction + +class ExposureStateUpdateWorker(val context: Context, workerParams: WorkerParameters) : + CoroutineWorker(context, workerParams) { + companion object { + private val TAG = ExposureStateUpdateWorker::class.simpleName + } + + override suspend fun doWork(): Result { + try { + Log.v(TAG, "worker to persist exposure summary started") + val token = inputData.getString(ExposureNotificationClient.EXTRA_TOKEN) + ?: throw NoTokenException(IllegalArgumentException("no token was found in the intent")) + + Log.v(TAG, "valid token $token retrieved") + + val exposureSummary = InternalExposureNotificationClient + .asyncGetExposureSummary(token) + + ExposureSummaryRepository.getExposureSummaryRepository() + .insertExposureSummaryEntity(exposureSummary) + Log.v(TAG, "exposure summary state updated: $exposureSummary") + + RiskLevelTransaction.start() + Log.v(TAG, "risk level calculation triggered") + } catch (e: ApiException) { + e.report(ExceptionCategory.EXPOSURENOTIFICATION) + } catch (e: TransactionException) { + e.report(ExceptionCategory.INTERNAL) + } + + return Result.success() + } +} 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 e41d7b94ed1..9a28ffa7b51 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 @@ -3,22 +3,15 @@ package de.rki.coronawarnapp.receiver import android.content.BroadcastReceiver import android.content.Context import android.content.Intent -import android.util.Log -import com.google.android.gms.common.api.ApiException +import androidx.work.Data +import androidx.work.OneTimeWorkRequest +import androidx.work.WorkManager import com.google.android.gms.nearby.exposurenotification.ExposureNotificationClient -import de.rki.coronawarnapp.exception.ExceptionCategory.EXPOSURENOTIFICATION import de.rki.coronawarnapp.exception.ExceptionCategory.INTERNAL -import de.rki.coronawarnapp.exception.TransactionException +import de.rki.coronawarnapp.exception.NoTokenException import de.rki.coronawarnapp.exception.WrongReceiverException import de.rki.coronawarnapp.exception.report -import de.rki.coronawarnapp.nearby.InternalExposureNotificationClient -import de.rki.coronawarnapp.storage.ExposureSummaryRepository -import de.rki.coronawarnapp.storage.LocalData -import de.rki.coronawarnapp.transaction.RiskLevelTransaction -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.launch -import java.util.UUID +import de.rki.coronawarnapp.nearby.ExposureStateUpdateWorker /** * Receiver to listen to the Exposure Notification Exposure State Updated event. This event will be triggered from the @@ -36,8 +29,6 @@ import java.util.UUID class ExposureStateUpdateReceiver : BroadcastReceiver() { companion object { private val TAG: String? = ExposureStateUpdateReceiver::class.simpleName - private const val EXPOSURE_STATE_UPDATE_PERMISSION = - "com.google.android.gms.nearby.exposurenotification.EXPOSURE_CALLBACK" } override fun onReceive(context: Context, intent: Intent) { @@ -51,32 +42,27 @@ class ExposureStateUpdateReceiver : BroadcastReceiver() { IllegalArgumentException("wrong action was received") ) } - val token = LocalData.googleApiToken() - val pendingRepository = goAsync() + val token = + intent.getStringExtra(ExposureNotificationClient.EXTRA_TOKEN) + ?: throw NoTokenException( + IllegalArgumentException("no token was found in the intent") + ) - GlobalScope.launch(Dispatchers.Default) { - try { - val exposureSummary = InternalExposureNotificationClient - .asyncGetExposureSummary(token ?: UUID.randomUUID().toString()) - - ExposureSummaryRepository.getExposureSummaryRepository() - .insertExposureSummaryEntity(exposureSummary) - pendingRepository.finish() - Log.v(TAG, "exposure summary state updated") - try { - RiskLevelTransaction.start() - } catch (e: TransactionException) { - e.report(INTERNAL) - } - Log.v(TAG, "exposure summary updated - trigger a new risk level calculation") - Log.v(TAG, exposureSummary.toString()) - } catch (e: ApiException) { - e.report(EXPOSURENOTIFICATION) - } - } + val workManager = WorkManager.getInstance(context) + workManager.enqueue( + OneTimeWorkRequest.Builder(ExposureStateUpdateWorker::class.java) + .setInputData( + Data.Builder() + .putString(ExposureNotificationClient.EXTRA_TOKEN, token) + .build() + ) + .build() + ) } catch (e: WrongReceiverException) { e.report(INTERNAL) + } catch (e: NoTokenException) { + e.report(INTERNAL) } } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskLevelCalculation.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskLevelCalculation.kt new file mode 100644 index 00000000000..0da900ecf9c --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskLevelCalculation.kt @@ -0,0 +1,65 @@ +package de.rki.coronawarnapp.risk + +import android.util.Log +import com.google.android.gms.nearby.exposurenotification.ExposureSummary +import de.rki.coronawarnapp.server.protocols.ApplicationConfigurationOuterClass +import kotlin.math.round + +object RiskLevelCalculation { + private var TAG = RiskLevelCalculation::class.simpleName + + private const val DECIMAL_MULTIPLIER = 100 + + fun calculateRiskScore( + attenuationParameters: ApplicationConfigurationOuterClass.AttenuationDuration, + exposureSummary: ExposureSummary + ): Double { + + /** all attenuation values are capped to [TimeVariables.MAX_ATTENUATION_DURATION] */ + val weightedAttenuationLow = + attenuationParameters.weights.low.capped() + .times(exposureSummary.attenuationDurationsInMinutes[0]) + val weightedAttenuationMid = + attenuationParameters.weights.mid.capped() + .times(exposureSummary.attenuationDurationsInMinutes[1]) + val weightedAttenuationHigh = + attenuationParameters.weights.high.capped() + .times(exposureSummary.attenuationDurationsInMinutes[2]) + + val maximumRiskScore = exposureSummary.maximumRiskScore.toDouble() + + val defaultBucketOffset = attenuationParameters.defaultBucketOffset.toDouble() + val normalizationDivisor = attenuationParameters.riskScoreNormalizationDivisor.toDouble() + + Log.v( + TAG, + "Weighted Attenuation: ($weightedAttenuationLow +" + + " $weightedAttenuationMid +" + + " $weightedAttenuationHigh +" + + " $defaultBucketOffset)" + ) + + val weightedAttenuationDuration = + weightedAttenuationLow + .plus(weightedAttenuationMid) + .plus(weightedAttenuationHigh) + .plus(defaultBucketOffset) + + Log.v( + TAG, + "Formula used: ($maximumRiskScore / $normalizationDivisor) * $weightedAttenuationDuration" + ) + + val riskScore = (maximumRiskScore / normalizationDivisor) * weightedAttenuationDuration + + return round(riskScore.times(DECIMAL_MULTIPLIER)).div(DECIMAL_MULTIPLIER) + } + + private fun Double.capped(): Double { + return if (this > TimeVariables.getMaxAttenuationDuration()) { + TimeVariables.getMaxAttenuationDuration() + } else { + this + } + } +} 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 1e87eb70dfb..2e90edd67d3 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 @@ -115,6 +115,19 @@ object TimeVariables { */ fun getManualKeyRetrievalDelay() = MANUAL_KEY_RETRIEVAL_DELAY + /** + * This is the maximum attenuation duration value for the risk level calculation + * in minutes + */ + private const val MAX_ATTENUATION_DURATION = 30.0 + + /** + * Getter function for [MAX_ATTENUATION_DURATION] + * + * @return max attenuation duration in minutes + */ + fun getMaxAttenuationDuration() = MAX_ATTENUATION_DURATION + /**************************************************** * STORED DATA ****************************************************/ diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/service/applicationconfiguration/ApplicationConfigurationService.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/service/applicationconfiguration/ApplicationConfigurationService.kt new file mode 100644 index 00000000000..596ba092533 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/service/applicationconfiguration/ApplicationConfigurationService.kt @@ -0,0 +1,68 @@ +package de.rki.coronawarnapp.service.applicationconfiguration + +import com.google.android.gms.nearby.exposurenotification.ExposureConfiguration +import de.rki.coronawarnapp.http.WebRequestBuilder +import de.rki.coronawarnapp.server.protocols.ApplicationConfigurationOuterClass + +object ApplicationConfigurationService { + suspend fun asyncRetrieveApplicationConfiguration(): ApplicationConfigurationOuterClass.ApplicationConfiguration { + return WebRequestBuilder + .asyncGetApplicationConfigurationFromServer() + } + + suspend fun asyncRetrieveExposureConfiguration(): ExposureConfiguration = + asyncRetrieveApplicationConfiguration() + .mapRiskScoreToExposureConfiguration() + + // todo double check that the weighted params are not used + private fun ApplicationConfigurationOuterClass.ApplicationConfiguration + .mapRiskScoreToExposureConfiguration(): ExposureConfiguration = + ExposureConfiguration + .ExposureConfigurationBuilder() + .setTransmissionRiskScores( + this.exposureConfig.transmission.appDefined1Value, + this.exposureConfig.transmission.appDefined2Value, + this.exposureConfig.transmission.appDefined3Value, + this.exposureConfig.transmission.appDefined4Value, + this.exposureConfig.transmission.appDefined5Value, + this.exposureConfig.transmission.appDefined6Value, + this.exposureConfig.transmission.appDefined7Value, + this.exposureConfig.transmission.appDefined8Value + ) + .setDurationScores( + this.exposureConfig.duration.eq0MinValue, + this.exposureConfig.duration.gt0Le5MinValue, + this.exposureConfig.duration.gt5Le10MinValue, + this.exposureConfig.duration.gt10Le15MinValue, + this.exposureConfig.duration.gt15Le20MinValue, + this.exposureConfig.duration.gt20Le25MinValue, + this.exposureConfig.duration.gt25Le30MinValue, + this.exposureConfig.duration.gt30MinValue + ) + .setDaysSinceLastExposureScores( + this.exposureConfig.daysSinceLastExposure.ge14DaysValue, + this.exposureConfig.daysSinceLastExposure.ge12Lt14DaysValue, + this.exposureConfig.daysSinceLastExposure.ge10Lt12DaysValue, + this.exposureConfig.daysSinceLastExposure.ge8Lt10DaysValue, + this.exposureConfig.daysSinceLastExposure.ge6Lt8DaysValue, + this.exposureConfig.daysSinceLastExposure.ge4Lt6DaysValue, + this.exposureConfig.daysSinceLastExposure.ge2Lt4DaysValue, + this.exposureConfig.daysSinceLastExposure.ge0Lt2DaysValue + ) + .setAttenuationScores( + this.exposureConfig.attenuation.gt73DbmValue, + this.exposureConfig.attenuation.gt63Le73DbmValue, + this.exposureConfig.attenuation.gt51Le63DbmValue, + this.exposureConfig.attenuation.gt33Le51DbmValue, + this.exposureConfig.attenuation.gt27Le33DbmValue, + this.exposureConfig.attenuation.gt15Le27DbmValue, + this.exposureConfig.attenuation.gt10Le15DbmValue, + this.exposureConfig.attenuation.lt10DbmValue + ) + .setMinimumRiskScore(this.minRiskScore) + .setDurationAtAttenuationThresholds( + this.attenuationDuration.thresholds.lower, + this.attenuationDuration.thresholds.upper + ) + .build() +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/service/diagnosiskey/DiagnosisKeyConstants.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/service/diagnosiskey/DiagnosisKeyConstants.kt index ce90f98ce95..beef2512531 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/service/diagnosiskey/DiagnosisKeyConstants.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/service/diagnosiskey/DiagnosisKeyConstants.kt @@ -19,8 +19,6 @@ package de.rki.coronawarnapp.service.diagnosiskey -import de.rki.coronawarnapp.http.DynamicURLs - /** * The Diagnosis Key constants are used inside the DiagnosisKeyService * @@ -53,10 +51,10 @@ object DiagnosisKeyConstants { private var CURRENT_COUNTRY = "DE" /** Distribution URL built from CDN URL's and REST resources */ - private var VERSIONED_DISTRIBUTION_CDN_URL = DynamicURLs.DOWNLOAD_CDN_URL + "/$VERSION/$CURRENT_VERSION" + private var VERSIONED_DISTRIBUTION_CDN_URL = "/$VERSION/$CURRENT_VERSION" /** Submission URL built from CDN URL's and REST resources */ - private var VERSIONED_SUBMISSION_CDN_URL = DynamicURLs.SUBMISSION_CDN_URL + "/$VERSION/$CURRENT_VERSION" + private var VERSIONED_SUBMISSION_CDN_URL = "/$VERSION/$CURRENT_VERSION" /** Parameter Download URL built from CDN URL's and REST resources */ private val PARAMETERS_DOWNLOAD_URL = "$VERSIONED_DISTRIBUTION_CDN_URL/$PARAMETERS" diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/service/diagnosiskey/DiagnosisKeyService.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/service/diagnosiskey/DiagnosisKeyService.kt index 7169d014ed7..24682d27e24 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/service/diagnosiskey/DiagnosisKeyService.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/service/diagnosiskey/DiagnosisKeyService.kt @@ -24,7 +24,6 @@ import android.util.Log import de.rki.coronawarnapp.exception.DiagnosisKeyRetrievalException import de.rki.coronawarnapp.exception.DiagnosisKeySubmissionException import de.rki.coronawarnapp.http.WebRequestBuilder -import de.rki.coronawarnapp.service.diagnosiskey.DiagnosisKeyConstants.DIAGNOSIS_KEYS_SUBMISSION_URL /** * The Diagnosis Key Service is used to interact with the Server to submit and retrieve keys through @@ -51,7 +50,6 @@ object DiagnosisKeyService { try { Log.d(TAG, "Diagnosis Keys will be submitted.") WebRequestBuilder.asyncSubmitKeysToServer( - DIAGNOSIS_KEYS_SUBMISSION_URL, authCode, false, keysToReport diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/service/riskscoreclassification/RiskScoreClassificationService.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/service/riskscoreclassification/RiskScoreClassificationService.kt deleted file mode 100644 index b29989b49c6..00000000000 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/service/riskscoreclassification/RiskScoreClassificationService.kt +++ /dev/null @@ -1,13 +0,0 @@ -package de.rki.coronawarnapp.service.riskscoreclassification - -import de.rki.coronawarnapp.http.WebRequestBuilder -import de.rki.coronawarnapp.server.protocols.ApplicationConfigurationOuterClass.RiskScoreClassification -import de.rki.coronawarnapp.service.diagnosiskey.DiagnosisKeyConstants - -object RiskScoreClassificationService { - suspend fun asyncRetrieveRiskScoreClassification(): RiskScoreClassification { - return WebRequestBuilder - .asyncGetApplicationConfigurationFromServer(DiagnosisKeyConstants.COUNTRY_APPCONFIG_DOWNLOAD_URL) - .riskScoreClasses - } -} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/service/riskscoreparameter/RiskScoreParameterService.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/service/riskscoreparameter/RiskScoreParameterService.kt deleted file mode 100644 index 6df6ffded62..00000000000 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/service/riskscoreparameter/RiskScoreParameterService.kt +++ /dev/null @@ -1,64 +0,0 @@ -package de.rki.coronawarnapp.service.riskscoreparameter - -import com.google.android.gms.nearby.exposurenotification.ExposureConfiguration -import de.rki.coronawarnapp.http.WebRequestBuilder -import de.rki.coronawarnapp.server.protocols.ApplicationConfigurationOuterClass.RiskScoreParameters -import de.rki.coronawarnapp.service.diagnosiskey.DiagnosisKeyConstants - -object RiskScoreParameterService { - suspend fun asyncRetrieveRiskScoreParameters(): ExposureConfiguration = - WebRequestBuilder - .asyncGetApplicationConfigurationFromServer(DiagnosisKeyConstants.COUNTRY_APPCONFIG_DOWNLOAD_URL) - .exposureConfig - .mapRiskScoreToExposureConfiguration() - - // TODO also add minimumRiskScore as soon as the backend will provide it in the correct proto - // right now it is available but only in the parent ApplicationConfiguration - private fun RiskScoreParameters.mapRiskScoreToExposureConfiguration(): ExposureConfiguration = - ExposureConfiguration - .ExposureConfigurationBuilder() - .setAttenuationWeight(this.attenuationWeight.toInt()) - .setAttenuationScores( - this.attenuation.gt73DbmValue, - this.attenuation.gt63Le73DbmValue, - this.attenuation.gt51Le63DbmValue, - this.attenuation.gt33Le51DbmValue, - this.attenuation.gt27Le33DbmValue, - this.attenuation.gt15Le27DbmValue, - this.attenuation.gt10Le15DbmValue, - this.attenuation.lt10DbmValue - ) - .setDaysSinceLastExposureWeight(this.daysWeight.toInt()) - .setDaysSinceLastExposureScores( - this.daysSinceLastExposure.ge14DaysValue, - this.daysSinceLastExposure.ge12Lt14DaysValue, - this.daysSinceLastExposure.ge10Lt12DaysValue, - this.daysSinceLastExposure.ge8Lt10DaysValue, - this.daysSinceLastExposure.ge6Lt8DaysValue, - this.daysSinceLastExposure.ge4Lt6DaysValue, - this.daysSinceLastExposure.ge2Lt4DaysValue, - this.daysSinceLastExposure.ge0Lt2DaysValue - ) - .setDurationWeight(this.durationWeight.toInt()) - .setDurationScores( - this.duration.eq0MinValue, - this.duration.gt0Le5MinValue, - this.duration.gt5Le10MinValue, - this.duration.gt10Le15MinValue, - this.duration.gt15Le20MinValue, - this.duration.gt20Le25MinValue, - this.duration.gt25Le30MinValue, - this.duration.gt30MinValue - ) - .setTransmissionRiskWeight(this.transmissionWeight.toInt()) - .setTransmissionRiskScores( - this.transmission.appDefined1Value, - this.transmission.appDefined2Value, - this.transmission.appDefined3Value, - this.transmission.appDefined4Value, - this.transmission.appDefined5Value, - this.transmission.appDefined6Value, - this.transmission.appDefined7Value, - this.transmission.appDefined8Value - ).build() -} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/service/submission/SubmissionConstants.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/service/submission/SubmissionConstants.kt index 6edc4ce1bfc..a99128c9013 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/service/submission/SubmissionConstants.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/service/submission/SubmissionConstants.kt @@ -1,7 +1,5 @@ package de.rki.coronawarnapp.service.submission -import de.rki.coronawarnapp.http.DynamicURLs - object SubmissionConstants { private const val VERSION = "version" private const val REGISTRATION_TOKEN = "registrationToken" @@ -10,8 +8,7 @@ object SubmissionConstants { private var CURRENT_VERSION = "v1" - private var VERIFICATION_CDN_URL = DynamicURLs.VERIFICATION_CDN_URL - private val VERSIONED_VERIFICATION_CDN_URL = "$VERIFICATION_CDN_URL/$VERSION/$CURRENT_VERSION" + private val VERSIONED_VERIFICATION_CDN_URL = "$VERSION/$CURRENT_VERSION" const val QR_CODE_KEY_TYPE = "GUID" const val TELE_TAN_KEY_TYPE = "TELETAN" @@ -19,4 +16,8 @@ object SubmissionConstants { val REGISTRATION_TOKEN_URL = "$VERSIONED_VERIFICATION_CDN_URL/$REGISTRATION_TOKEN" val TEST_RESULT_URL = "$VERSIONED_VERIFICATION_CDN_URL/$TEST_RESULT" val TAN_REQUEST_URL = "$VERSIONED_VERIFICATION_CDN_URL/$TAN" + + const val MAX_QR_CODE_LENGTH = 150 + const val MAX_GUID_LENGTH = 80 + const val GUID_SEPARATOR = '?' } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/service/submission/SubmissionService.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/service/submission/SubmissionService.kt index ad23d8a0d3b..c50399d451d 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/service/submission/SubmissionService.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/service/submission/SubmissionService.kt @@ -4,11 +4,10 @@ import de.rki.coronawarnapp.exception.NoGUIDOrTANSetException import de.rki.coronawarnapp.exception.NoRegistrationTokenSetException import de.rki.coronawarnapp.http.WebRequestBuilder import de.rki.coronawarnapp.service.submission.SubmissionConstants.QR_CODE_KEY_TYPE -import de.rki.coronawarnapp.service.submission.SubmissionConstants.REGISTRATION_TOKEN_URL -import de.rki.coronawarnapp.service.submission.SubmissionConstants.TAN_REQUEST_URL import de.rki.coronawarnapp.service.submission.SubmissionConstants.TELE_TAN_KEY_TYPE import de.rki.coronawarnapp.storage.LocalData import de.rki.coronawarnapp.transaction.SubmitDiagnosisKeysTransaction +import de.rki.coronawarnapp.util.formatter.TestResult object SubmissionService { suspend fun asyncRegisterDevice() { @@ -26,7 +25,6 @@ object SubmissionService { private suspend fun asyncRegisterDeviceViaGUID(guid: String) { val registrationToken = WebRequestBuilder.asyncGetRegistrationToken( - REGISTRATION_TOKEN_URL, guid, QR_CODE_KEY_TYPE ) @@ -38,7 +36,6 @@ object SubmissionService { private suspend fun asyncRegisterDeviceViaTAN(tan: String) { val registrationToken = WebRequestBuilder.asyncGetRegistrationToken( - REGISTRATION_TOKEN_URL, tan, TELE_TAN_KEY_TYPE ) @@ -47,11 +44,8 @@ object SubmissionService { deleteTeleTAN() } - suspend fun asyncRequestAuthCode(): String { - val registrationToken = - LocalData.registrationToken() ?: throw NoRegistrationTokenSetException() - - val authCode = WebRequestBuilder.asyncGetTan(TAN_REQUEST_URL, registrationToken) + suspend fun asyncRequestAuthCode(registrationToken: String): String { + val authCode = WebRequestBuilder.asyncGetTan(registrationToken) return authCode } @@ -61,17 +55,28 @@ object SubmissionService { SubmitDiagnosisKeysTransaction.start(registrationToken) } - /** - * extracts the GUID from [scanResult]. Returns null if it does not match the required pattern - */ - fun extractGUID(scanResult: String): String? { - val potentialGUID = scanResult.substringAfterLast("?", "") - return if (potentialGUID.isEmpty()) - null - else - potentialGUID + suspend fun asyncRequestTestResult(): TestResult { + val registrationToken = + LocalData.registrationToken() ?: throw NoRegistrationTokenSetException() + return TestResult.fromInt( + WebRequestBuilder.asyncGetTestResult(registrationToken) + ) + } + + fun containsValidGUID(scanResult: String): Boolean { + if (scanResult.length > SubmissionConstants.MAX_QR_CODE_LENGTH || + scanResult.count { it == SubmissionConstants.GUID_SEPARATOR } != 1 + ) + return false + + val potentialGUID = extractGUID(scanResult) + + return !(potentialGUID.isEmpty() || potentialGUID.length > SubmissionConstants.MAX_GUID_LENGTH) } + fun extractGUID(scanResult: String): String = + scanResult.substringAfterLast(SubmissionConstants.GUID_SEPARATOR, "") + fun storeTestGUID(guid: String) = LocalData.testGUID(guid) fun deleteTestGUID() { 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 1988d0d4d7a..734d06ac111 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 @@ -160,7 +160,7 @@ object LocalData { fun lastTimeDiagnosisKeysFromServerFetch(): Date? { val time = getSharedPreferenceInstance().getLong( CoronaWarnApplication.getAppContext() - .getString(R.string.preference_m_timestamp_diagnosis_keys_fetch), + .getString(R.string.preference_timestamp_diagnosis_keys_fetch), 0L ) // TODO need this for nullable ref, shout not be goto for nullable storage @@ -180,7 +180,7 @@ object LocalData { getSharedPreferenceInstance().edit(true) { putLong( CoronaWarnApplication.getAppContext() - .getString(R.string.preference_m_timestamp_diagnosis_keys_fetch), + .getString(R.string.preference_timestamp_diagnosis_keys_fetch), value?.time ?: 0L ) } @@ -193,7 +193,7 @@ object LocalData { */ fun lastTimeManualDiagnosisKeysRetrieved(): Long = getSharedPreferenceInstance().getLong( CoronaWarnApplication.getAppContext() - .getString(R.string.preference_m_timestamp_manual_diagnosis_keys_retrieval), + .getString(R.string.preference_timestamp_manual_diagnosis_keys_retrieval), 0L ) @@ -204,7 +204,7 @@ object LocalData { getSharedPreferenceInstance().edit(true) { putLong( CoronaWarnApplication.getAppContext() - .getString(R.string.preference_m_timestamp_manual_diagnosis_keys_retrieval), + .getString(R.string.preference_timestamp_manual_diagnosis_keys_retrieval), value ) } @@ -220,7 +220,7 @@ object LocalData { */ fun googleApiToken(): String? = getSharedPreferenceInstance().getString( CoronaWarnApplication.getAppContext() - .getString(R.string.preference_m_string_google_api_token), + .getString(R.string.preference_string_google_api_token), null ) @@ -232,7 +232,7 @@ object LocalData { fun googleApiToken(value: String?) = getSharedPreferenceInstance().edit(true) { putString( CoronaWarnApplication.getAppContext() - .getString(R.string.preference_m_string_google_api_token), + .getString(R.string.preference_string_google_api_token), value ) } @@ -333,7 +333,7 @@ object LocalData { */ fun registrationToken(): String? = getSharedPreferenceInstance().getString( CoronaWarnApplication.getAppContext() - .getString(R.string.preference_m_registration_token), + .getString(R.string.preference_registration_token), null ) @@ -346,7 +346,7 @@ object LocalData { getSharedPreferenceInstance().edit(true) { putString( CoronaWarnApplication.getAppContext() - .getString(R.string.preference_m_registration_token), + .getString(R.string.preference_registration_token), value ) } @@ -410,7 +410,7 @@ object LocalData { fun testGUID(): String? = getSharedPreferenceInstance().getString( CoronaWarnApplication.getAppContext() - .getString(R.string.preference_m_test_guid), + .getString(R.string.preference_test_guid), null ) @@ -418,7 +418,7 @@ object LocalData { getSharedPreferenceInstance().edit(true) { putString( CoronaWarnApplication.getAppContext() - .getString(R.string.preference_m_test_guid), + .getString(R.string.preference_test_guid), value ) } @@ -426,7 +426,7 @@ object LocalData { fun authCode(): String? = getSharedPreferenceInstance().getString( CoronaWarnApplication.getAppContext() - .getString(R.string.preference_m_auth_code), + .getString(R.string.preference_auth_code), null ) @@ -434,7 +434,7 @@ object LocalData { getSharedPreferenceInstance().edit(true) { putString( CoronaWarnApplication.getAppContext() - .getString(R.string.preference_m_auth_code), + .getString(R.string.preference_auth_code), value ) } @@ -444,7 +444,7 @@ object LocalData { getSharedPreferenceInstance().edit(true) { putBoolean( CoronaWarnApplication.getAppContext() - .getString(R.string.preference_m_is_allowed_to_submit_diagnosis_keys), + .getString(R.string.preference_is_allowed_to_submit_diagnosis_keys), isAllowedToSubmitDiagnosisKeys ) } @@ -453,7 +453,7 @@ object LocalData { fun isAllowedToSubmitDiagnosisKeys(): Boolean? { return getSharedPreferenceInstance().getBoolean( CoronaWarnApplication.getAppContext() - .getString(R.string.preference_m_is_allowed_to_submit_diagnosis_keys), + .getString(R.string.preference_is_allowed_to_submit_diagnosis_keys), false ) } @@ -482,5 +482,5 @@ object LocalData { fun getLastFetchDatePreference() = CoronaWarnApplication.getAppContext() - .getString(R.string.preference_m_timestamp_diagnosis_keys_fetch) + .getString(R.string.preference_timestamp_diagnosis_keys_fetch) } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/SubmissionRepository.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/SubmissionRepository.kt index 5d3546d6034..f6ab3dad85f 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/SubmissionRepository.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/SubmissionRepository.kt @@ -2,34 +2,61 @@ package de.rki.coronawarnapp.storage import androidx.lifecycle.MutableLiveData import de.rki.coronawarnapp.exception.NoRegistrationTokenSetException -import de.rki.coronawarnapp.http.WebRequestBuilder -import de.rki.coronawarnapp.service.submission.SubmissionConstants.TEST_RESULT_URL +import de.rki.coronawarnapp.service.submission.SubmissionService +import de.rki.coronawarnapp.util.DeviceUIState import de.rki.coronawarnapp.util.formatter.TestResult import java.util.Date object SubmissionRepository { private val TAG: String? = SubmissionRepository::class.simpleName - val testResult = MutableLiveData(TestResult.INVALID) val testResultReceivedDate = MutableLiveData(Date()) + val deviceUIState = MutableLiveData(DeviceUIState.UNPAIRED) - suspend fun refreshTestResult() { - val registrationToken = - LocalData.registrationToken() ?: throw NoRegistrationTokenSetException() - val testResultValue = - WebRequestBuilder.asyncGetTestResult(TEST_RESULT_URL, registrationToken) - testResult.value = TestResult.fromInt(testResultValue) - if (testResult == TestResult.POSITIVE) { - LocalData.isAllowedToSubmitDiagnosisKeys(true) - } - val initialTestResultReceivedTimestamp = LocalData.inititalTestResultReceivedTimestamp() + suspend fun refreshUIState() { + var uiState = DeviceUIState.UNPAIRED - if (initialTestResultReceivedTimestamp == null) { - val currentTime = System.currentTimeMillis() - LocalData.inititalTestResultReceivedTimestamp(currentTime) - testResultReceivedDate.value = Date(currentTime) + if (LocalData.numberOfSuccessfulSubmissions() == 1) { + uiState = DeviceUIState.SUBMITTED_FINAL } else { - testResultReceivedDate.value = Date(initialTestResultReceivedTimestamp) + if (LocalData.registrationToken() != null) { + uiState = when { + LocalData.isAllowedToSubmitDiagnosisKeys() == true -> { + DeviceUIState.PAIRED_POSITIVE + } + else -> fetchTestResult() + } + } + } + deviceUIState.value = uiState + } + + private suspend fun fetchTestResult(): DeviceUIState { + try { + val testResult = SubmissionService.asyncRequestTestResult() + + if (testResult == TestResult.POSITIVE) { + LocalData.isAllowedToSubmitDiagnosisKeys(true) + } + + val initialTestResultReceivedTimestamp = LocalData.inititalTestResultReceivedTimestamp() + + if (initialTestResultReceivedTimestamp == null) { + val currentTime = System.currentTimeMillis() + LocalData.inititalTestResultReceivedTimestamp(currentTime) + testResultReceivedDate.value = Date(currentTime) + } else { + testResultReceivedDate.value = Date(initialTestResultReceivedTimestamp) + } + + return when (testResult) { + TestResult.NEGATIVE -> DeviceUIState.PAIRED_NEGATIVE + TestResult.POSITIVE -> DeviceUIState.PAIRED_POSITIVE + TestResult.PENDING -> DeviceUIState.PAIRED_NO_RESULT + TestResult.INVALID -> DeviceUIState.PAIRED_ERROR + } + } catch (err: NoRegistrationTokenSetException) { + return DeviceUIState.UNPAIRED } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/transaction/RetrieveDiagnosisKeysTransaction.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/transaction/RetrieveDiagnosisKeysTransaction.kt index 9547187c67b..49e9ff6f798 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/transaction/RetrieveDiagnosisKeysTransaction.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/transaction/RetrieveDiagnosisKeysTransaction.kt @@ -23,7 +23,7 @@ import android.util.Log import com.google.android.gms.nearby.exposurenotification.ExposureConfiguration import de.rki.coronawarnapp.CoronaWarnApplication import de.rki.coronawarnapp.nearby.InternalExposureNotificationClient -import de.rki.coronawarnapp.service.riskscoreparameter.RiskScoreParameterService +import de.rki.coronawarnapp.service.applicationconfiguration.ApplicationConfigurationService import de.rki.coronawarnapp.storage.FileStorageHelper import de.rki.coronawarnapp.storage.LocalData import de.rki.coronawarnapp.storage.keycache.KeyCacheRepository @@ -215,7 +215,7 @@ object RetrieveDiagnosisKeysTransaction : Transaction() { */ private suspend fun executeRetrieveRiskScoreParams() = executeState(RETRIEVE_RISK_SCORE_PARAMS) { - RiskScoreParameterService.asyncRetrieveRiskScoreParameters() + ApplicationConfigurationService.asyncRetrieveExposureConfiguration() } /** 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 48c521cae4c..32e157f3771 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 @@ -2,9 +2,11 @@ package de.rki.coronawarnapp.transaction import android.util.Log import androidx.core.app.NotificationCompat +import androidx.lifecycle.MutableLiveData import com.google.android.gms.nearby.exposurenotification.ExposureSummary import de.rki.coronawarnapp.CoronaWarnApplication import de.rki.coronawarnapp.R +import de.rki.coronawarnapp.TestRiskLevelCalculation import de.rki.coronawarnapp.exception.RiskLevelCalculationException import de.rki.coronawarnapp.nearby.InternalExposureNotificationClient import de.rki.coronawarnapp.notification.NotificationHelper @@ -15,9 +17,10 @@ import de.rki.coronawarnapp.risk.RiskLevel.NO_CALCULATION_POSSIBLE_TRACING_OFF import de.rki.coronawarnapp.risk.RiskLevel.UNDETERMINED import de.rki.coronawarnapp.risk.RiskLevel.UNKNOWN_RISK_INITIAL import de.rki.coronawarnapp.risk.RiskLevel.UNKNOWN_RISK_OUTDATED_RESULTS +import de.rki.coronawarnapp.risk.RiskLevelCalculation import de.rki.coronawarnapp.risk.TimeVariables -import de.rki.coronawarnapp.server.protocols.ApplicationConfigurationOuterClass.RiskScoreClassification -import de.rki.coronawarnapp.service.riskscoreclassification.RiskScoreClassificationService +import de.rki.coronawarnapp.server.protocols.ApplicationConfigurationOuterClass +import de.rki.coronawarnapp.service.applicationconfiguration.ApplicationConfigurationService import de.rki.coronawarnapp.storage.ExposureSummaryRepository import de.rki.coronawarnapp.storage.LocalData import de.rki.coronawarnapp.storage.RiskLevelRepository @@ -27,8 +30,8 @@ import de.rki.coronawarnapp.transaction.RiskLevelTransaction.RiskLevelTransactio import de.rki.coronawarnapp.transaction.RiskLevelTransaction.RiskLevelTransactionState.CHECK_UNKNOWN_RISK_INITIAL_TRACING_DURATION import de.rki.coronawarnapp.transaction.RiskLevelTransaction.RiskLevelTransactionState.CHECK_UNKNOWN_RISK_OUTDATED 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.RETRIEVE_RISK_THRESHOLD import de.rki.coronawarnapp.transaction.RiskLevelTransaction.RiskLevelTransactionState.UPDATE_RISK_LEVEL import de.rki.coronawarnapp.util.TimeAndDateExtensions.millisecondsToDays import de.rki.coronawarnapp.util.TimeAndDateExtensions.millisecondsToHours @@ -101,7 +104,7 @@ import java.util.concurrent.atomic.AtomicReference * 1. [CHECK_TRACING] * 2. [CHECK_UNKNOWN_RISK_INITIAL_NO_KEYS] * 3. [CHECK_UNKNOWN_RISK_OUTDATED] - * 4. [RETRIEVE_RISK_THRESHOLD] + * 4. [RETRIEVE_APPLICATION_CONFIG] * 5. [RETRIEVE_EXPOSURE_SUMMARY] * 6. [CHECK_INCREASED_RISK] * 7. [CHECK_UNKNOWN_RISK_INITIAL_TRACING_DURATION] @@ -133,8 +136,9 @@ object RiskLevelTransaction : Transaction() { /** Check the conditions for the [UNKNOWN_RISK_OUTDATED_RESULTS] score */ CHECK_UNKNOWN_RISK_OUTDATED, - /** Retrieve the RiskThreshold values to define the thresholds for [INCREASED_RISK] and [LOW_LEVEL_RISK] */ - RETRIEVE_RISK_THRESHOLD, + /** Retrieve the Application Configuration values to calculate the Risk Score + * and determine the [INCREASED_RISK] and [LOW_LEVEL_RISK] */ + RETRIEVE_APPLICATION_CONFIG, /** Retrieve the last persisted [ExposureSummary] (if available) from the Google Exposure Notification API for * further calculation of the Risk Level Score */ @@ -153,6 +157,17 @@ object RiskLevelTransaction : Transaction() { CLOSE } + /** TESTING ONLY + * + * TODO remove asap, only used to display the used values for risk level calculation + */ + + val tempExposedTransactionValuesForTestingOnly = + MutableLiveData() + + var recordedTransactionValuesForTestingOnly = + TestRiskLevelCalculation.TransactionValues() + /** atomic reference for the rollback value for the last calculated risk level score */ private val lastCalculatedRiskLevelScoreForRollback = AtomicReference() @@ -178,9 +193,9 @@ object RiskLevelTransaction : Transaction() { if (isValidResult(result)) return@lockAndExecute /**************************************************** - * RETRIEVE RISK THRESHOLD + * RETRIEVE APPLICATION CONFIGURATION ****************************************************/ - val riskScoreClassification = executeRetrieveRiskThreshold() + val appConfiguration = executeRetrieveApplicationConfiguration() /**************************************************** * RETRIEVE EXPOSURE SUMMARY @@ -190,7 +205,7 @@ object RiskLevelTransaction : Transaction() { /**************************************************** * CHECK [INCREASED_RISK] CONDITIONS ****************************************************/ - result = executeCheckIncreasedRisk(riskScoreClassification, lastExposureSummary) + result = executeCheckIncreasedRisk(appConfiguration, lastExposureSummary) if (isValidResult(result)) return@lockAndExecute /**************************************************** @@ -204,7 +219,7 @@ object RiskLevelTransaction : Transaction() { * SET [LOW_LEVEL_RISK] LEVEL IF NONE ABOVE APPLIED ****************************************************/ if (result == UNDETERMINED) { - lastCalculatedRiskLevelScoreForRollback.set(getLastCalculatedRiskLevelScore()) + lastCalculatedRiskLevelScoreForRollback.set(RiskLevelRepository.getLastCalculatedScore()) executeUpdateRiskLevelScore(LOW_LEVEL_RISK) executeClose() return@lockAndExecute @@ -282,14 +297,19 @@ object RiskLevelTransaction : Transaction() { } /** - * Executes the [RETRIEVE_RISK_THRESHOLD] Transaction State + * Executes the [RETRIEVE_APPLICATION_CONFIG] Transaction State + * + * @return the values of the application configuration */ - private suspend fun executeRetrieveRiskThreshold(): RiskScoreClassification = - executeState(RETRIEVE_RISK_THRESHOLD) { - // these are the threshold values defined by RKI - return@executeState getRiskThreshold().also { - Log.v(TAG, "$transactionId - retrieved risk threshold") - } + private suspend fun executeRetrieveApplicationConfiguration(): + ApplicationConfigurationOuterClass.ApplicationConfiguration = + executeState(RETRIEVE_APPLICATION_CONFIG) { + return@executeState getApplicationConfiguration() + .also { + // todo remove after testing sessions + recordedTransactionValuesForTestingOnly.appConfig = it + Log.v(TAG, "$transactionId - retrieved configuration from backend") + } } /** @@ -299,6 +319,8 @@ object RiskLevelTransaction : Transaction() { 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") } } @@ -307,23 +329,47 @@ object RiskLevelTransaction : Transaction() { * Executes the [CHECK_INCREASED_RISK] Transaction State */ private suspend fun executeCheckIncreasedRisk( - riskScoreClassification: RiskScoreClassification, + appConfig: ApplicationConfigurationOuterClass.ApplicationConfiguration, exposureSummary: ExposureSummary ): RiskLevel = executeState(CHECK_INCREASED_RISK) { - val highRiskScoreClass = riskScoreClassification.riskClassesList.find { it.label == "HIGH" } - ?: throw RiskLevelCalculationException(IllegalStateException("no high risk score class found")) + // custom attenuation parameters to weight the attenuation + // values provided by the Google API + val attenuationParameters = appConfig.attenuationDuration + + // calculate the risk score based on the values collected by the Google EN API and + // the backend configuration + val riskScore = RiskLevelCalculation.calculateRiskScore( + attenuationParameters, + exposureSummary + ).also { + // todo remove after testing sessions + recordedTransactionValuesForTestingOnly.riskScore = it + Log.v(TAG, "calculated risk with the given config: $it") + } - // if the risk score is above the defined level threshold we always return the high level risk score - if (exposureSummary.maximumRiskScore >= highRiskScoreClass.min) return@executeState INCREASED_RISK - .also { - Log.v( - TAG, - "${exposureSummary.maximumRiskScore} is above the defined " + - "min value ${highRiskScoreClass.min}" - ) - } + // these are the defined risk classes. They will divide the calculated + // risk score into the low and increased risk + val riskScoreClassification = appConfig.riskScoreClasses + + // get the high risk score class + val highRiskScoreClass = + riskScoreClassification.riskClassesList.find { it.label == "HIGH" } + ?: throw RiskLevelCalculationException(IllegalStateException("no high risk score class found")) + + // if the calculated risk score is above the defined level threshold we return the high level risk score + if (riskScore >= highRiskScoreClass.min && riskScore <= highRiskScoreClass.max) { + Log.v( + TAG, "$riskScore is above the defined " + + "min value ${highRiskScoreClass.min}" + ) + return@executeState INCREASED_RISK + } else if (riskScore > highRiskScoreClass.max) { + throw RiskLevelCalculationException( + IllegalStateException("risk score is above the max threshold for score class") + ) + } Log.v(TAG, "$transactionId - INCREASED_RISK not applicable") return@executeState UNDETERMINED @@ -352,6 +398,13 @@ object RiskLevelTransaction : Transaction() { executeState(UPDATE_RISK_LEVEL) { Log.v(TAG, "$transactionId - update the risk level with $riskLevel") updateRiskLevelScore(riskLevel) + .also { + // todo remove after testing sessions + recordedTransactionValuesForTestingOnly.riskLevel = riskLevel + tempExposedTransactionValuesForTestingOnly.postValue( + recordedTransactionValuesForTestingOnly + ) + } } /** @@ -381,7 +434,7 @@ object RiskLevelTransaction : Transaction() { "$transactionId - $riskLevel was determined by the transaction. " + "UPDATE and CLOSE will be called" ) - lastCalculatedRiskLevelScoreForRollback.set(getLastCalculatedRiskLevelScore()) + lastCalculatedRiskLevelScoreForRollback.set(RiskLevelRepository.getLastCalculatedScore()) executeUpdateRiskLevelScore(riskLevel) executeClose() return true @@ -404,14 +457,15 @@ object RiskLevelTransaction : Transaction() { } /** - * Make a call to the backend to retrieve the current risk threshold values from the RKI + * Make a call to the backend to retrieve the current application configuration values * - * @return the [RiskScoreClassification] from the backend + * @return the [ApplicationConfigurationOuterClass.ApplicationConfiguration] from the backend */ - private suspend fun getRiskThreshold(): RiskScoreClassification = withContext(Dispatchers.Default) { - return@withContext RiskScoreClassificationService.asyncRetrieveRiskScoreClassification() - .also { Log.v(TAG, "risk score classes from backend: $it") } - } + private suspend fun getApplicationConfiguration(): ApplicationConfigurationOuterClass.ApplicationConfiguration = + withContext(Dispatchers.Default) { + return@withContext ApplicationConfigurationService.asyncRetrieveApplicationConfiguration() + .also { Log.v(TAG, "configuration from backend: $it") } + } /** * Returns a Boolean if the duration of the activated tracing time is above the @@ -433,15 +487,6 @@ object RiskLevelTransaction : Transaction() { } } - /** - * Retrieves the last calculated Risk Level Score from the persisted storage - * - * @return - */ - private fun getLastCalculatedRiskLevelScore(): RiskLevel { - return RiskLevelRepository.getLastCalculatedScore() - } - /** * Updates the Risk Level Score in the repository with the calculated Risk Level * @@ -468,7 +513,8 @@ object RiskLevelTransaction : Transaction() { val googleToken = LocalData.googleApiToken() ?: throw RiskLevelCalculationException(IllegalStateException("exposure summary is not persisted")) - val exposureSummary = InternalExposureNotificationClient.asyncGetExposureSummary(googleToken) + val exposureSummary = + InternalExposureNotificationClient.asyncGetExposureSummary(googleToken) ExposureSummaryRepository.getExposureSummaryRepository() .insertExposureSummaryEntity(exposureSummary) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/transaction/SubmitDiagnosisKeysTransaction.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/transaction/SubmitDiagnosisKeysTransaction.kt index f998b199a8d..d7cc02559b5 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/transaction/SubmitDiagnosisKeysTransaction.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/transaction/SubmitDiagnosisKeysTransaction.kt @@ -52,7 +52,7 @@ object SubmitDiagnosisKeysTransaction : Transaction() { * RETRIEVE TAN ****************************************************/ val authCode = executeState(RETRIEVE_TAN) { - SubmissionService.asyncRequestAuthCode() + SubmissionService.asyncRequestAuthCode(registrationToken) } /**************************************************** * RETRIEVE TEMPORARY EXPOSURE KEY HISTORY diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/transaction/Transaction.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/transaction/Transaction.kt index eccaf7ce3cd..f77781f0ad5 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/transaction/Transaction.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/transaction/Transaction.kt @@ -21,10 +21,8 @@ package de.rki.coronawarnapp.transaction import android.util.Log import de.rki.coronawarnapp.BuildConfig -import de.rki.coronawarnapp.exception.ExceptionCategory.INTERNAL import de.rki.coronawarnapp.exception.RollbackException import de.rki.coronawarnapp.exception.TransactionException -import de.rki.coronawarnapp.exception.report import de.rki.coronawarnapp.risk.TimeVariables import de.rki.coronawarnapp.transaction.Transaction.InternalTransactionStates.INIT import kotlinx.coroutines.CoroutineScope @@ -258,7 +256,6 @@ abstract class Transaction { protected open suspend fun handleTransactionError(error: Throwable?): Nothing { rollback() resetExecutedStateStack() - error?.report(INTERNAL) throw TransactionException( transactionId.get(), currentTransactionState.toString(), diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/UiConstants.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/UiConstants.kt deleted file mode 100644 index 4179af0c143..00000000000 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/UiConstants.kt +++ /dev/null @@ -1,8 +0,0 @@ -package de.rki.coronawarnapp.ui - -object UiConstants { - const val INFORMATION_URI = "https://www.bundesregierung.de/c19app-intern" - - // todo move to strings if translatable is needed? if yes include regex in CallHelper to filter non-numerical chars excluding '+' - const val TECHNICAL_HOTLINE = "tel:+49 800 7540001" -} 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 09308fcbcfc..7107692858b 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 @@ -32,7 +32,7 @@ class InformationAboutFragment : BaseFragment() { } private fun setButtonOnClickListener() { - binding.informationAboutHeader.informationHeader.headerButtonBack.buttonIcon.setOnClickListener { + binding.informationAboutHeader.headerButtonBack.buttonIcon.setOnClickListener { (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 e05286d770d..3da4c84a61b 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 @@ -4,9 +4,9 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import de.rki.coronawarnapp.R import de.rki.coronawarnapp.databinding.FragmentInformationContactBinding import de.rki.coronawarnapp.ui.BaseFragment -import de.rki.coronawarnapp.ui.UiConstants import de.rki.coronawarnapp.ui.main.MainActivity import de.rki.coronawarnapp.util.CallHelper @@ -34,11 +34,16 @@ class InformationContactFragment : BaseFragment() { } private fun setButtonOnClickListener() { - binding.informationContactHeader.informationHeader.headerButtonBack.buttonIcon.setOnClickListener { + binding.informationContactHeader.headerButtonBack.buttonIcon.setOnClickListener { (activity as MainActivity).goBack() } binding.informationContactNavigationRowPhone.navigationRow.setOnClickListener { - CallHelper.call(this, UiConstants.TECHNICAL_HOTLINE) + CallHelper.call( + this, + requireContext().getString( + R.string.information_contact_phone_call_number + ) + ) } } } 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 34a1994eb91..76866d570f1 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 @@ -4,9 +4,9 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import de.rki.coronawarnapp.R import de.rki.coronawarnapp.databinding.FragmentInformationBinding import de.rki.coronawarnapp.ui.BaseFragment -import de.rki.coronawarnapp.ui.UiConstants import de.rki.coronawarnapp.ui.main.MainActivity import de.rki.coronawarnapp.util.OpenUrlHelper @@ -55,7 +55,7 @@ class InformationFragment : BaseFragment() { ) } binding.informationHelp.mainRow.setOnClickListener { - OpenUrlHelper.navigate(this, UiConstants.INFORMATION_URI) + OpenUrlHelper.navigate(this, requireContext().getString(R.string.main_about_link)) } binding.informationLegal.mainRow.setOnClickListener { doNavigate( @@ -67,7 +67,7 @@ class InformationFragment : BaseFragment() { InformationFragmentDirections.actionInformationFragmentToInformationTechnicalFragment() ) } - binding.informationHeader.informationHeader.headerButtonBack.buttonIcon.setOnClickListener { + binding.informationHeader.headerButtonBack.buttonIcon.setOnClickListener { (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 c8e749ae390..2d32a639d14 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 @@ -32,7 +32,7 @@ class InformationLegalFragment : BaseFragment() { } private fun setButtonOnClickListener() { - binding.informationLegalHeader.informationHeader.headerButtonBack.buttonIcon.setOnClickListener { + binding.informationLegalHeader.headerButtonBack.buttonIcon.setOnClickListener { (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 79fc00d186e..7b3cc61e44c 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 @@ -32,7 +32,7 @@ class InformationPrivacyFragment : BaseFragment() { } private fun setButtonOnClickListener() { - binding.informationPrivacyHeader.informationHeader.headerButtonBack.buttonIcon.setOnClickListener { + binding.informationPrivacyHeader.headerButtonBack.buttonIcon.setOnClickListener { (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 a240ada4f56..a4e295f7e5e 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 @@ -32,7 +32,7 @@ class InformationTechnicalFragment : BaseFragment() { } private fun setButtonOnClickListener() { - binding.informationTechnicalHeader.informationHeader.headerButtonBack.buttonIcon.setOnClickListener { + binding.informationTechnicalHeader.headerButtonBack.buttonIcon.setOnClickListener { (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 8581c18eab9..486172f2a36 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 @@ -32,7 +32,7 @@ class InformationTermsFragment : BaseFragment() { } private fun setButtonOnClickListener() { - binding.informationTermsHeader.informationHeader.headerButtonBack.buttonIcon.setOnClickListener { + binding.informationTermsHeader.headerButtonBack.buttonIcon.setOnClickListener { (activity as MainActivity).goBack() } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/MainActivity.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/MainActivity.kt index 595f3580cbd..f6643d11320 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/MainActivity.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/MainActivity.kt @@ -1,12 +1,15 @@ package de.rki.coronawarnapp.ui.main import android.content.Intent +import android.content.IntentFilter import android.os.Bundle import androidx.appcompat.app.AppCompatActivity import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager import androidx.lifecycle.ViewModelProviders +import androidx.localbroadcastmanager.content.LocalBroadcastManager import de.rki.coronawarnapp.R +import de.rki.coronawarnapp.exception.ErrorReportReceiver import de.rki.coronawarnapp.ui.viewmodel.SettingsViewModel import de.rki.coronawarnapp.util.ConnectivityHelper import de.rki.coronawarnapp.worker.BackgroundWorkScheduler @@ -29,6 +32,8 @@ class MainActivity : AppCompatActivity() { private lateinit var settingsViewModel: SettingsViewModel + private val errorReceiver = ErrorReportReceiver(this) + /** * Register connection callback. */ @@ -69,6 +74,7 @@ class MainActivity : AppCompatActivity() { */ override fun onResume() { super.onResume() + LocalBroadcastManager.getInstance(this).registerReceiver(errorReceiver, IntentFilter("error-report")) ConnectivityHelper.registerNetworkStatusCallback(this, callbackNetwork) ConnectivityHelper.registerBluetoothStatusCallback(this, callbackBluetooth) } @@ -80,6 +86,8 @@ class MainActivity : AppCompatActivity() { super.onPause() ConnectivityHelper.unregisterNetworkStatusCallback(this, callbackNetwork) ConnectivityHelper.unregisterBluetoothStatusCallback(this, callbackBluetooth) + // Unregister since the activity is about to be closed. + LocalBroadcastManager.getInstance(this).unregisterReceiver(errorReceiver) } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/MainFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/MainFragment.kt index 5d2606f2096..6f9355b5220 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/MainFragment.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/MainFragment.kt @@ -14,7 +14,6 @@ import de.rki.coronawarnapp.databinding.FragmentMainBinding import de.rki.coronawarnapp.notification.NotificationHelper import de.rki.coronawarnapp.timer.TimerHelper import de.rki.coronawarnapp.ui.BaseFragment -import de.rki.coronawarnapp.ui.UiConstants import de.rki.coronawarnapp.ui.viewmodel.SettingsViewModel import de.rki.coronawarnapp.ui.viewmodel.SubmissionViewModel import de.rki.coronawarnapp.ui.viewmodel.TracingViewModel @@ -69,9 +68,7 @@ class MainFragment : BaseFragment() { tracingViewModel.refreshActiveTracingDaysInRetentionPeriod() settingsViewModel.refreshBackgroundJobEnabled() TimerHelper.checkManualKeyRetrievalTimer() - if (submissionViewModel.deviceRegistered) { - submissionViewModel.refreshTestResult() - } + submissionViewModel.refreshDeviceUIState() } private fun setButtonOnClickListener() { @@ -110,7 +107,7 @@ class MainFragment : BaseFragment() { doNavigate(MainFragmentDirections.actionMainFragmentToSettingsTracingFragment()) } binding.mainAbout.mainCard.setOnClickListener { - OpenUrlHelper.navigate(this, UiConstants.INFORMATION_URI) + OpenUrlHelper.navigate(this, requireContext().getString(R.string.main_about_link)) } binding.mainHeaderShare.buttonIcon.setOnClickListener { doNavigate(MainFragmentDirections.actionMainFragmentToMainSharingFragment()) @@ -143,6 +140,11 @@ class MainFragment : BaseFragment() { true } // todo remove only for testing + R.id.menu_test_risk_level -> { + doNavigate(MainFragmentDirections.actionMainFragmentToTestRiskLevelCalculation()) + true + } + // todo remove only for testing R.id.menu_notification_test -> { Log.i(TAG, "calling notification") Log.i( 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 5a007838f7b..03d4f392972 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 @@ -37,7 +37,7 @@ class MainOverviewFragment : BaseFragment() { } private fun setButtonOnClickListener() { - binding.mainOverviewHeader.informationHeader.headerButtonBack.buttonIcon.setOnClickListener { + binding.mainOverviewHeader.headerButtonBack.buttonIcon.setOnClickListener { (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 9bff3a7ade9..e6b42e0eab6 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 @@ -45,7 +45,7 @@ class MainShareFragment : BaseFragment() { binding.mainShareButton.setOnClickListener { ShareHelper.shareText(this, getString(R.string.main_share_message), null) } - binding.mainShareHeader.informationHeader.headerButtonBack.buttonIcon.setOnClickListener { + binding.mainShareHeader.headerButtonBack.buttonIcon.setOnClickListener { (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 0e36914cd7a..ed39f371606 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 @@ -59,7 +59,7 @@ class SettingsFragment : BaseFragment() { val tracingRow = binding.settingsTracing.settingsRow val notificationRow = binding.settingsNotifications.settingsRow val resetRow = binding.settingsReset - val goBack = binding.settingsHeader.informationHeader.headerButtonBack.buttonIcon + val goBack = binding.settingsHeader.headerButtonBack.buttonIcon 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 7e335ef7c0c..695362b30c0 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 @@ -62,7 +62,7 @@ class SettingsNotificationFragment : Fragment() { // Settings val settingsRow = binding.settingsNavigationRowSystem.navigationRow val goBack = - binding.settingsDetailsHeaderNotifications.informationHeader.headerButtonBack.buttonIcon + binding.settingsNotificationsHeader.headerButtonBack.buttonIcon // 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 33ec04776d2..0606f781908 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 @@ -6,7 +6,6 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.Toast -import androidx.fragment.app.activityViewModels import androidx.lifecycle.lifecycleScope import com.google.android.gms.common.api.ApiException import de.rki.coronawarnapp.databinding.FragmentSettingsResetBinding @@ -16,7 +15,6 @@ import de.rki.coronawarnapp.nearby.InternalExposureNotificationClient import de.rki.coronawarnapp.ui.BaseFragment import de.rki.coronawarnapp.ui.main.MainActivity import de.rki.coronawarnapp.ui.onboarding.OnboardingActivity -import de.rki.coronawarnapp.ui.viewmodel.TracingViewModel import de.rki.coronawarnapp.util.DataRetentionHelper import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -25,7 +23,6 @@ import kotlinx.coroutines.withContext /** * The user is informed what a reset means and he can perform it. * - * @see TracingViewModel */ class SettingsResetFragment : BaseFragment() { @@ -33,7 +30,6 @@ class SettingsResetFragment : BaseFragment() { private val TAG: String? = SettingsResetFragment::class.simpleName } - private val tracingViewModel: TracingViewModel by activityViewModels() private lateinit var binding: FragmentSettingsResetBinding override fun onCreateView( @@ -42,8 +38,6 @@ class SettingsResetFragment : BaseFragment() { savedInstanceState: Bundle? ): View? { binding = FragmentSettingsResetBinding.inflate(inflater) - binding.tracingViewModel = tracingViewModel - binding.lifecycleOwner = this return binding.root } @@ -55,7 +49,7 @@ class SettingsResetFragment : BaseFragment() { binding.settingsResetButtonCancel.setOnClickListener { (activity as MainActivity).goBack() } - binding.settingsDetailsHeaderReset.informationHeader.headerButtonBack.buttonIcon.setOnClickListener { + binding.settingsResetHeader.headerButtonBack.buttonIcon.setOnClickListener { (activity as MainActivity).goBack() } } @@ -90,7 +84,7 @@ class SettingsResetFragment : BaseFragment() { activity?.finish() } - private suspend fun deleteLocalAppContent() { + private fun deleteLocalAppContent() { DataRetentionHelper.clearAllLocalData(requireContext()) } } 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 89c4cdd24c7..219dfa2cd4b 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 @@ -102,7 +102,7 @@ class SettingsTracingFragment : BaseFragment(), } } } - binding.settingsTracingHeader.informationHeader.headerButtonBack.buttonIcon.setOnClickListener { + binding.settingsTracingHeader.headerButtonBack.buttonIcon.setOnClickListener { (activity as MainActivity).goBack() } binding.settingsTracingStatusBluetooth.tracingStatusCardButton.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 a3841c8f6d5..af9ab63b466 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 @@ -30,7 +30,12 @@ class SubmissionDoneFragment : BaseFragment() { } private fun setButtonOnClickListener() { - binding.submissionDoneHeader.informationHeader.headerButtonBack.buttonIcon.setOnClickListener { + binding + .submissionDoneInclude + .submissionDoneHeader + .informationHeader + .headerButtonBack.buttonIcon + .setOnClickListener { doNavigate( SubmissionDoneFragmentDirections.actionSubmissionDoneFragmentToMainFragment() ) 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 b94a5e620ba..527afa53285 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 @@ -29,7 +29,7 @@ class SubmissionResultPositiveOtherWarningFragment : BaseFragment(), private var submissionRequested = false private var submissionFailed = false private lateinit var internalExposureNotificationPermissionHelper: - InternalExposureNotificationPermissionHelper + InternalExposureNotificationPermissionHelper override fun onResume() { super.onResume() 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 22b63d2d9bb..c18d49a0153 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 @@ -10,21 +10,13 @@ class SubmissionTanViewModel : ViewModel() { companion object { private val TAG: String? = SubmissionTanViewModel::class.simpleName - - private const val TAN_LENGTH = 7 - private val EXCLUDED_TAN_CHARS = listOf('0', 'O', 'I', '1') - private val VALID_TAN_CHARS = - ('a'..'z') - .plus('A'..'Z') - .plus('0'..'9') - .minus(EXCLUDED_TAN_CHARS) } val tan = MutableLiveData(null) val isValidTanFormat = Transformations.map(tan) { - it != null && it.length == TAN_LENGTH && it.all { c -> VALID_TAN_CHARS.contains(c) } + it != null && it.length == TanConstants.MAX_LENGTH } 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 9b04678d913..cdb2f61ca13 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 @@ -45,13 +45,13 @@ class SubmissionTestResultFragment : BaseFragment() { override fun onResume() { super.onResume() - submissionViewModel.refreshTestResult() + submissionViewModel.refreshDeviceUIState() tracingViewModel.refreshIsTracingEnabled() } private fun setButtonOnClickListener() { binding.submissionTestResultButtonPendingRefresh.setOnClickListener { - submissionViewModel.refreshTestResult() + submissionViewModel.refreshDeviceUIState() } binding.submissionTestResultButtonPendingRemoveTest.setOnClickListener { 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 new file mode 100644 index 00000000000..b6be13be81d --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/TanConstants.kt @@ -0,0 +1,6 @@ +package de.rki.coronawarnapp.ui.submission + +object TanConstants { + const val MAX_LENGTH = 7 + 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 d1a25184653..70cc48b6833 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 @@ -2,6 +2,7 @@ package de.rki.coronawarnapp.ui.submission import android.content.Context import android.os.Handler +import android.text.InputFilter import android.util.AttributeSet import android.view.inputmethod.InputMethodManager import android.widget.FrameLayout @@ -22,6 +23,15 @@ class TanInput(context: Context, attrs: AttributeSet) : FrameLayout(context, att private const val KEYBOARD_TRIGGER_DELAY = 100L } + private val whitespaceFilter = + InputFilter { source, _, _, _, _, _ -> source.filter { !it.isWhitespace() } } + private val alphaNumericFilter = InputFilter { source, _, _, _, _, _ -> + source.filter { + TanConstants.ALPHA_NUMERIC_CHARS.contains(it) + } + } + private val lengthFilter = InputFilter.LengthFilter(TanConstants.MAX_LENGTH) + var listener: ((String?) -> Unit)? = null private var tan: String? = null @@ -29,6 +39,8 @@ class TanInput(context: Context, attrs: AttributeSet) : FrameLayout(context, att init { inflate(context, R.layout.view_tan_input, this) + tan_input_edittext.filters = arrayOf(whitespaceFilter, alphaNumericFilter, lengthFilter) + // register listener tan_input_edittext.doOnTextChanged { text, _, _, _ -> updateTan(text) } setOnClickListener { showKeyboard() } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/view/CircleProgress.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/view/CircleProgress.kt index dc4e6a9af5e..bef8b354ff1 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/view/CircleProgress.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/view/CircleProgress.kt @@ -89,7 +89,8 @@ class CircleProgress @JvmOverloads constructor( val progressColor = styleAttrs.getColor(R.styleable.CircleProgress_progressColor, ContextCompat.getColor(context, R.color.colorPrimary)) // attribute textColor; default = colorGrey - val textColor = styleAttrs.getColor(R.styleable.CircleProgress_textColor, + val textColor = styleAttrs.getColor( + R.styleable.CircleProgress_textColor, ContextCompat.getColor(context, R.color.textColorGrey) ) // attribute disableText; default = true 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 7baf40e1213..de2bb9f5066 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 @@ -11,28 +11,27 @@ import de.rki.coronawarnapp.storage.LocalData import de.rki.coronawarnapp.storage.SubmissionRepository import de.rki.coronawarnapp.ui.submission.ApiRequestState import de.rki.coronawarnapp.ui.submission.ScanStatus -import de.rki.coronawarnapp.util.formatter.TestResult +import de.rki.coronawarnapp.util.DeviceUIState import kotlinx.coroutines.launch import java.util.Date class SubmissionViewModel : ViewModel() { private val _scanStatus = MutableLiveData(ScanStatus.STARTED) private val _registrationState = MutableLiveData(ApiRequestState.IDLE) - private val _testResultState = MutableLiveData(ApiRequestState.IDLE) - private val _authCodeState = MutableLiveData(ApiRequestState.IDLE) + private val _uiStateState = MutableLiveData(ApiRequestState.IDLE) private val _submissionState = MutableLiveData(ApiRequestState.IDLE) val scanStatus: LiveData = _scanStatus val registrationState: LiveData = _registrationState - val testResultState: LiveData = _testResultState - val authCodeState: LiveData = _authCodeState + val uiStateState: LiveData = _uiStateState val submissionState: LiveData = _submissionState val deviceRegistered get() = LocalData.registrationToken() != null - val testResult: LiveData = - SubmissionRepository.testResult - val testResultReceivedDate: LiveData = SubmissionRepository.testResultReceivedDate + val testResultReceivedDate: LiveData = + SubmissionRepository.testResultReceivedDate + val deviceUiState: LiveData = + SubmissionRepository.deviceUIState fun submitDiagnosisKeys() = executeRequestWithState(SubmissionService::asyncSubmitExposureKeys, _submissionState) @@ -40,12 +39,12 @@ class SubmissionViewModel : ViewModel() { fun doDeviceRegistration() = executeRequestWithState(SubmissionService::asyncRegisterDevice, _registrationState) - fun refreshTestResult() = - executeRequestWithState(SubmissionRepository::refreshTestResult, _testResultState) + fun refreshDeviceUIState() = + executeRequestWithState(SubmissionRepository::refreshUIState, _uiStateState) fun validateAndStoreTestGUID(scanResult: String) { - val guid = SubmissionService.extractGUID(scanResult) - if (guid != null) { + if (SubmissionService.containsValidGUID(scanResult)) { + val guid = SubmissionService.extractGUID(scanResult) SubmissionService.storeTestGUID(guid) _scanStatus.value = ScanStatus.SUCCESS } else { 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 31257882044..c7ef6e95de1 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 @@ -35,7 +35,6 @@ import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.withContext import java.io.File -import java.util.ArrayList import java.util.Date import java.util.UUID @@ -193,27 +192,11 @@ object CachedKeyFileHolder { * Get all dates from server based as formatted dates */ private suspend fun getDatesFromServer() = - WebRequestBuilder.asyncGetArrayListFromGenericRequest( - DiagnosisKeyConstants.AVAILABLE_DATES_URL - ) { - val result = ArrayList() - for (i in 0 until it.length()) { - result.add(it.get(i).toString()) - } - result - } + WebRequestBuilder.asyncGetDateIndex() /** * Get all hours from server based as formatted dates */ private suspend fun getHoursFromServer(day: Date) = - WebRequestBuilder.asyncGetArrayListFromGenericRequest( - "${DiagnosisKeyConstants.AVAILABLE_DATES_URL}/${day.toServerFormat()}/${DiagnosisKeyConstants.HOUR}" - ) { - val result = ArrayList() - for (i in 0 until it.length()) { - result.add(it.get(i).toString()) - } - result - } + WebRequestBuilder.asyncGetHourIndex(day) } 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 6d2bbcf58a8..6118ea66109 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 @@ -136,6 +136,19 @@ object ConnectivityHelper { return bAdapter.isEnabled } + /** + * Get network enabled status. + * + * @return current network status + * + */ + fun isNetworkEnabled(context: Context): Boolean { + val manager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + val activeNetwork: Network? = manager.activeNetwork + val caps: NetworkCapabilities? = manager.getNetworkCapabilities(activeNetwork) + return caps?.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) ?: false + } + /** * Abstract bluetooth state change callback. * diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/DialogHelper.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/DialogHelper.kt index 5e8ba7e359e..5c07fbd3315 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/DialogHelper.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/DialogHelper.kt @@ -7,14 +7,34 @@ object DialogHelper { data class DialogInstance( val activity: Activity, - val title: Int, - val message: Int, - val positiveButton: Int, - val negativeButton: Int? = null, - val cancelable: Boolean = true, + val title: String, + val message: String?, + val positiveButton: String, + val negativeButton: String? = null, + val cancelable: Boolean? = true, val positiveButtonFunction: () -> Unit? = {}, val negativeButtonFunction: () -> Unit? = {} - ) + ) { + constructor( + activity: Activity, + title: Int, + message: Int, + positiveButton: Int, + negativeButton: Int? = null, + cancelable: Boolean? = true, + positiveButtonFunction: () -> Unit? = {}, + negativeButtonFunction: () -> Unit? = {} + ) : this( + activity, + activity.resources.getString(title), + activity.resources.getString(message), + activity.resources.getString(positiveButton), + negativeButton?.let { activity.resources.getString(it) }, + cancelable, + positiveButtonFunction, + negativeButtonFunction + ) + } fun showDialog( dialogInstance: DialogInstance @@ -24,7 +44,7 @@ object DialogHelper { builder.apply { setTitle(dialogInstance.title) setMessage(dialogInstance.message) - setCancelable(dialogInstance.cancelable) + setCancelable(dialogInstance.cancelable ?: true) setPositiveButton( dialogInstance.positiveButton ) { _, _ -> 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 new file mode 100644 index 00000000000..c162ac72f29 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/PropertyLoader.kt @@ -0,0 +1,28 @@ +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/ProtoFormatConverterExtensions.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/ProtoFormatConverterExtensions.kt index 80325f9529a..c81679e5d43 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/ProtoFormatConverterExtensions.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/ProtoFormatConverterExtensions.kt @@ -9,19 +9,46 @@ object ProtoFormatConverterExtensions { private const val ROLLING_PERIOD = 144 private const val DEFAULT_TRANSMISSION_RISK_LEVEL = 1 + private const val TRANSMISSION_RISK_DAY_0 = 5 + private const val TRANSMISSION_RISK_DAY_1 = 6 + private const val TRANSMISSION_RISK_DAY_2 = 7 + private const val TRANSMISSION_RISK_DAY_3 = 8 + private const val TRANSMISSION_RISK_DAY_4 = 7 + private const val TRANSMISSION_RISK_DAY_5 = 5 + private const val TRANSMISSION_RISK_DAY_6 = 3 + private const val TRANSMISSION_RISK_DAY_7 = 2 + private val DEFAULT_TRANSMISSION_RISK_VECTOR = intArrayOf( + TRANSMISSION_RISK_DAY_0, + TRANSMISSION_RISK_DAY_1, + TRANSMISSION_RISK_DAY_2, + TRANSMISSION_RISK_DAY_3, + TRANSMISSION_RISK_DAY_4, + TRANSMISSION_RISK_DAY_5, + TRANSMISSION_RISK_DAY_6, + TRANSMISSION_RISK_DAY_7 + ) private const val MAXIMUM_KEYS = 14 fun List.limitKeyCount() = - this.sortedWith(compareBy({ it.rollingStartIntervalNumber })).asReversed().take(MAXIMUM_KEYS) + this.sortedWith(compareBy { it.rollingStartIntervalNumber }).asReversed().take(MAXIMUM_KEYS) - fun List.transformKeyHistoryToExternalFormat() = this.map { - KeyExportFormat.TemporaryExposureKey.newBuilder() - .setKeyData(ByteString.readFrom(it.keyData.inputStream())) - .setRollingStartIntervalNumber(it.rollingStartIntervalNumber) - .setRollingPeriod(ROLLING_PERIOD) - .setTransmissionRiskLevel(DEFAULT_TRANSMISSION_RISK_LEVEL) - .build() - } + fun List.transformKeyHistoryToExternalFormat() = + this.sortedWith(compareBy { it.rollingStartIntervalNumber }) + .mapIndexed { index, it -> + // The earliest key we receive is from yesterday (i.e. 1 day ago), + // thus we need use index+1 + val riskValue = + if (index + 1 <= DEFAULT_TRANSMISSION_RISK_VECTOR.lastIndex) + DEFAULT_TRANSMISSION_RISK_VECTOR[index + 1] + else + DEFAULT_TRANSMISSION_RISK_LEVEL + KeyExportFormat.TemporaryExposureKey.newBuilder() + .setKeyData(ByteString.readFrom(it.keyData.inputStream())) + .setRollingStartIntervalNumber(it.rollingStartIntervalNumber) + .setRollingPeriod(ROLLING_PERIOD) + .setTransmissionRiskLevel(riskValue) + .build() + } fun AppleLegacyKeyExchange.Key.convertToGoogleKey(): KeyExportFormat.TemporaryExposureKey = KeyExportFormat.TemporaryExposureKey.newBuilder() diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/UiStateHelper.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/UiStateHelper.kt deleted file mode 100644 index 3a3a5e0c412..00000000000 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/UiStateHelper.kt +++ /dev/null @@ -1,24 +0,0 @@ -package de.rki.coronawarnapp.util - -import de.rki.coronawarnapp.storage.LocalData -import de.rki.coronawarnapp.util.DeviceUIState.PAIRED_NO_RESULT -import de.rki.coronawarnapp.util.DeviceUIState.PAIRED_POSITIVE -import de.rki.coronawarnapp.util.DeviceUIState.SUBMITTED_FINAL -import de.rki.coronawarnapp.util.DeviceUIState.UNPAIRED - -object UiStateHelper { - private fun uiState(): DeviceUIState { - var uiState = UNPAIRED - if (LocalData.registrationToken() != "") { - if (LocalData.inititalTestResultReceivedTimestamp() == null) { - uiState = PAIRED_NO_RESULT - } else if (LocalData.isAllowedToSubmitDiagnosisKeys() == true) { - uiState = PAIRED_POSITIVE - } - } else if (LocalData.numberOfSuccessfulSubmissions() == 1) { - uiState = SUBMITTED_FINAL - } - - return uiState - } -} 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 9ecbe2a7b59..29adcd8d9e2 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 @@ -113,15 +113,19 @@ fun formatRiskSavedRisk(riskLevelScore: Int?, savedRiskLevelScore: Int?): String */ fun formatRiskContact(riskLevelScore: Int?, matchedKeysCount: Int?): String { val appContext = CoronaWarnApplication.getAppContext() - val keysArg = matchedKeysCount?.toString() + val resources = appContext.resources + val contacts = matchedKeysCount ?: 0 return when (riskLevelScore) { RiskLevelConstants.INCREASED_RISK, RiskLevelConstants.LOW_LEVEL_RISK -> { - if (matchedKeysCount != null && matchedKeysCount != 0) { - appContext.getString(R.string.risk_card_body_contact_value) - .format(keysArg) - } else { + if (matchedKeysCount == 0) { appContext.getString(R.string.risk_card_body_contact) + } else { + resources.getQuantityString( + R.plurals.risk_card_body_contact_value, + contacts, + contacts + ) } } else -> "" @@ -139,16 +143,14 @@ fun formatRiskContact(riskLevelScore: Int?, matchedKeysCount: Int?): String { */ fun formatRiskContactLast(riskLevelScore: Int?, daysSinceLastExposure: Int?): String { val appContext = CoronaWarnApplication.getAppContext() - val daysArg = daysSinceLastExposure.toString() - + val resources = appContext.resources + val days = daysSinceLastExposure ?: 0 return if (riskLevelScore == RiskLevelConstants.INCREASED_RISK) { - if (daysSinceLastExposure != null && daysArg != "") { - appContext.getString(R.string.risk_card_increased_risk_body_contact_last) - .format(daysArg) - } else { - appContext.getString(R.string.risk_card_increased_risk_body_contact_last) - .format("0") - } + resources.getQuantityString( + R.plurals.risk_card_increased_risk_body_contact_last, + days, + days + ) } else { "" } @@ -297,13 +299,15 @@ fun formatRiskDetailsRiskLevelSubtitle(riskLevelScore: Int?): String { */ fun formatRiskDetailsRiskLevelBody(riskLevelScore: Int?, daysSinceLastExposure: Int?): String { val appContext = CoronaWarnApplication.getAppContext() - val daysArg = daysSinceLastExposure.toString() - + val resources = appContext.resources + val days = daysSinceLastExposure ?: 0 return when (riskLevelScore) { - RiskLevelConstants.INCREASED_RISK -> { - appContext.getString(R.string.risk_details_information_body_increased_risk) - .format(daysArg) - } + RiskLevelConstants.INCREASED_RISK -> + resources.getQuantityString( + R.plurals.risk_details_information_body_increased_risk, + days, + days + ) RiskLevelConstants.UNKNOWN_RISK_OUTDATED_RESULTS -> appContext.getString(R.string.risk_details_information_body_outdated_risk) RiskLevelConstants.LOW_LEVEL_RISK -> diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/formatter/FormatterSettingsHelper.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/formatter/FormatterSettingsHelper.kt index 41def653e3c..aff12f2ac96 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/formatter/FormatterSettingsHelper.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/formatter/FormatterSettingsHelper.kt @@ -109,16 +109,12 @@ fun formatNotificationsDescription(notifications: Boolean): String = formatText( * @param activeTracingDaysInRetentionPeriod * @return String */ -fun formatTracingStatusBody(tracing: Boolean, activeTracingDaysInRetentionPeriod: Long): String { +// TODO add generic plural formatter helper +fun formatTracingStatusBody(activeTracingDaysInRetentionPeriod: Long): String { val appContext = CoronaWarnApplication.getAppContext() - val daysArg = activeTracingDaysInRetentionPeriod.toString() - return if (tracing) { - appContext.getString(R.string.settings_tracing_status_body_active) - .format(daysArg) - } else { - appContext.getString(R.string.settings_tracing_status_body_inactive) - .format(daysArg) - } + val resources = appContext.resources + val days = activeTracingDaysInRetentionPeriod.toInt() + return resources.getQuantityString(R.plurals.settings_tracing_status_body_active, days, days) } /*Styler*/ 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 5d19352eef0..590f0cee20d 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 @@ -7,145 +7,160 @@ import android.view.View import de.rki.coronawarnapp.CoronaWarnApplication import de.rki.coronawarnapp.R import de.rki.coronawarnapp.ui.submission.ApiRequestState -import de.rki.coronawarnapp.util.formatter.TestResult.INVALID -import de.rki.coronawarnapp.util.formatter.TestResult.NEGATIVE -import de.rki.coronawarnapp.util.formatter.TestResult.PENDING -import de.rki.coronawarnapp.util.formatter.TestResult.POSITIVE +import de.rki.coronawarnapp.util.DeviceUIState import java.util.Date -fun formatTestResultSpinnerVisible(testResultStatus: ApiRequestState?): Int = - formatVisibility(testResultStatus != ApiRequestState.SUCCESS) +fun formatTestResultSpinnerVisible(uiStateState: ApiRequestState?): Int = + formatVisibility(uiStateState != ApiRequestState.SUCCESS) -fun formatTestResultVisible(testResultStatus: ApiRequestState?): Int = - formatVisibility(testResultStatus == ApiRequestState.SUCCESS) +fun formatTestResultVisible(uiStateState: ApiRequestState?): Int = + formatVisibility(uiStateState == ApiRequestState.SUCCESS) -fun formatTestResultVirusNameTextVisible(testResult: TestResult?): Int { - return when (testResult) { - POSITIVE, NEGATIVE -> View.VISIBLE +fun formatTestResultVirusNameTextVisible(uiState: DeviceUIState?): Int { + return when (uiState) { + DeviceUIState.PAIRED_POSITIVE, + DeviceUIState.PAIRED_POSITIVE_TELETAN, + DeviceUIState.PAIRED_NEGATIVE -> View.VISIBLE else -> View.GONE } } -fun formatTestResultStatusTextVisible(testResult: TestResult?): Int { - return when (testResult) { - POSITIVE, NEGATIVE -> View.VISIBLE +fun formatTestResultStatusTextVisible(uiState: DeviceUIState?): Int { + return when (uiState) { + DeviceUIState.PAIRED_POSITIVE, + DeviceUIState.PAIRED_POSITIVE_TELETAN, + DeviceUIState.PAIRED_NEGATIVE -> View.VISIBLE else -> View.GONE } } -fun formatTestResultStatusText(testResult: TestResult?): String { +fun formatTestResultStatusText(uiState: DeviceUIState?): String { val appContext = CoronaWarnApplication.getAppContext() - return when (testResult) { - NEGATIVE -> appContext.getString(R.string.test_result_card_status_negative) - POSITIVE -> appContext.getString(R.string.test_result_card_status_positive) + return when (uiState) { + DeviceUIState.PAIRED_NEGATIVE -> appContext.getString(R.string.test_result_card_status_negative) + DeviceUIState.PAIRED_POSITIVE, + DeviceUIState.PAIRED_POSITIVE_TELETAN -> appContext.getString(R.string.test_result_card_status_positive) else -> appContext.getString(R.string.test_result_card_status_invalid) } } -fun formatTestResultStatusColor(testResult: TestResult?): Int { +fun formatTestResultStatusColor(uiState: DeviceUIState?): Int { val appContext = CoronaWarnApplication.getAppContext() - return when (testResult) { - NEGATIVE -> appContext.getColor(R.color.colorGreen) - POSITIVE -> appContext.getColor(R.color.colorRed) + return when (uiState) { + DeviceUIState.PAIRED_NEGATIVE -> appContext.getColor(R.color.colorGreen) + DeviceUIState.PAIRED_POSITIVE, + DeviceUIState.PAIRED_POSITIVE_TELETAN -> appContext.getColor(R.color.colorRed) else -> appContext.getColor(R.color.colorRed) } } -fun formatTestStatusIcon(testResult: TestResult?): Drawable? { +fun formatTestStatusIcon(uiState: DeviceUIState?): Drawable? { val appContext = CoronaWarnApplication.getAppContext() // TODO Replace with real drawables when design is finished - return when (testResult) { - PENDING -> appContext.getDrawable(R.drawable.ic_test_result_illustration_pending) - POSITIVE -> appContext.getDrawable(R.drawable.ic_test_result_illustration_positive) - NEGATIVE -> appContext.getDrawable(R.drawable.ic_main_illustration_negative) - INVALID -> appContext.getDrawable(R.drawable.ic_test_result_illustration_invalid) + return when (uiState) { + DeviceUIState.PAIRED_NO_RESULT -> appContext.getDrawable(R.drawable.ic_test_result_illustration_pending) + DeviceUIState.PAIRED_POSITIVE_TELETAN, + DeviceUIState.PAIRED_POSITIVE -> appContext.getDrawable(R.drawable.ic_test_result_illustration_positive) + DeviceUIState.PAIRED_NEGATIVE -> appContext.getDrawable(R.drawable.ic_main_illustration_negative) + DeviceUIState.PAIRED_ERROR -> appContext.getDrawable(R.drawable.ic_test_result_illustration_invalid) else -> appContext.getDrawable(R.drawable.ic_test_result_illustration_invalid) } } -fun formatTestResultInvalidStatusTextVisible(testResult: TestResult?): Int = - formatVisibility(testResult == INVALID) +fun formatTestResultInvalidStatusTextVisible(uiState: DeviceUIState?): Int = + formatVisibility(uiState == DeviceUIState.PAIRED_ERROR) -fun formatTestResultPendingStatusTextVisible(testResult: TestResult?): Int = - formatVisibility(testResult == PENDING) +fun formatTestResultPendingStatusTextVisible(uiState: DeviceUIState?): Int = + formatVisibility(uiState == DeviceUIState.PAIRED_NO_RESULT) fun formatTestResultRegisteredAtText(registeredAt: Date?): String { val appContext = CoronaWarnApplication.getAppContext() return appContext.getString(R.string.test_result_card_registered_at_text).format(registeredAt) } -fun formatTestResultPendingStepsVisible(testResult: TestResult?): Int = - formatVisibility(testResult == PENDING) +fun formatTestResultPendingStepsVisible(uiState: DeviceUIState?): Int = + formatVisibility(uiState == DeviceUIState.PAIRED_NO_RESULT) -fun formatTestResultNegativeStepsVisible(testResult: TestResult?): Int = - formatVisibility(testResult == NEGATIVE) +fun formatTestResultNegativeStepsVisible(uiState: DeviceUIState?): Int = + formatVisibility(uiState == DeviceUIState.PAIRED_NEGATIVE) -fun formatTestResultPositiveStepsVisible(testResult: TestResult?): Int = - formatVisibility(testResult == POSITIVE) +fun formatTestResultPositiveStepsVisible(uiState: DeviceUIState?): Int = + formatVisibility(uiState == DeviceUIState.PAIRED_POSITIVE || uiState == DeviceUIState.PAIRED_POSITIVE_TELETAN) -fun formatTestResultInvalidStepsVisible(testResult: TestResult?): Int = - formatVisibility(testResult == INVALID) +fun formatTestResultInvalidStepsVisible(uiState: DeviceUIState?): Int = + formatVisibility(uiState == DeviceUIState.PAIRED_ERROR) -fun formatSubmissionStatusCardContentTitleText(testResult: TestResult?): String { +fun formatSubmissionStatusCardContentTitleText(uiState: DeviceUIState?): String { val appContext = CoronaWarnApplication.getAppContext() - return when (testResult) { - INVALID, NEGATIVE, POSITIVE -> appContext.getString(R.string.submission_status_card_title_available) - PENDING -> appContext.getString(R.string.submission_status_card_title_pending) + return when (uiState) { + DeviceUIState.PAIRED_ERROR, + DeviceUIState.PAIRED_NEGATIVE, + DeviceUIState.PAIRED_POSITIVE_TELETAN, + DeviceUIState.PAIRED_POSITIVE -> appContext.getString(R.string.submission_status_card_title_available) + DeviceUIState.PAIRED_NO_RESULT -> appContext.getString(R.string.submission_status_card_title_pending) else -> appContext.getString(R.string.submission_status_card_title_pending) } } -fun formatSubmissionStatusCardContentBodyText(testResult: TestResult?): String { +fun formatSubmissionStatusCardContentBodyText(uiState: DeviceUIState?): String { val appContext = CoronaWarnApplication.getAppContext() - return when (testResult) { - INVALID -> appContext.getString(R.string.submission_status_card_body_invalid) - NEGATIVE -> appContext.getString(R.string.submission_status_card_body_negative) - POSITIVE -> appContext.getString(R.string.submission_status_card_body_positive) - PENDING -> appContext.getString(R.string.submission_status_card_body_pending) + return when (uiState) { + DeviceUIState.PAIRED_ERROR -> appContext.getString(R.string.submission_status_card_body_invalid) + DeviceUIState.PAIRED_NEGATIVE -> appContext.getString(R.string.submission_status_card_body_negative) + DeviceUIState.PAIRED_POSITIVE, + DeviceUIState.PAIRED_POSITIVE_TELETAN -> appContext.getString(R.string.submission_status_card_body_positive) + DeviceUIState.PAIRED_NO_RESULT -> appContext.getString(R.string.submission_status_card_body_pending) else -> appContext.getString(R.string.submission_status_card_body_pending) } } -fun formatSubmissionStatusCardContentButtonText(testResult: TestResult?): String { +fun formatSubmissionStatusCardContentButtonText(uiState: DeviceUIState?): String { val appContext = CoronaWarnApplication.getAppContext() - return when (testResult) { - INVALID, NEGATIVE, POSITIVE -> appContext.getString(R.string.submission_status_card_button_show_results) + return when (uiState) { + DeviceUIState.PAIRED_ERROR, + DeviceUIState.PAIRED_NEGATIVE, + DeviceUIState.PAIRED_POSITIVE_TELETAN, + DeviceUIState.PAIRED_POSITIVE -> appContext.getString(R.string.submission_status_card_button_show_results) else -> appContext.getString(R.string.submission_status_card_button_show_details) } } -fun formatSubmissionStatusCardContentStatusTextVisible(testResult: TestResult?): Int { - return when (testResult) { - POSITIVE, NEGATIVE, INVALID -> View.VISIBLE +fun formatSubmissionStatusCardContentStatusTextVisible(uiState: DeviceUIState?): Int { + return when (uiState) { + DeviceUIState.PAIRED_POSITIVE, + DeviceUIState.PAIRED_POSITIVE_TELETAN, + DeviceUIState.PAIRED_NEGATIVE, + DeviceUIState.PAIRED_ERROR -> View.VISIBLE else -> View.GONE } } -fun formatSubmissionStatusCardContentIcon(testResult: TestResult?): Drawable? { +fun formatSubmissionStatusCardContentIcon(uiState: DeviceUIState?): Drawable? { val appContext = CoronaWarnApplication.getAppContext() // TODO Replace with real drawables when design is finished - return when (testResult) { - PENDING -> appContext.getDrawable(R.drawable.ic_main_illustration_pending) - POSITIVE -> appContext.getDrawable(R.drawable.ic_main_illustration_pending) - NEGATIVE -> appContext.getDrawable(R.drawable.ic_main_illustration_negative) - INVALID -> appContext.getDrawable(R.drawable.ic_main_illustration_invalid) + return when (uiState) { + DeviceUIState.PAIRED_NO_RESULT -> appContext.getDrawable(R.drawable.ic_main_illustration_pending) + DeviceUIState.PAIRED_POSITIVE, + DeviceUIState.PAIRED_POSITIVE_TELETAN -> appContext.getDrawable(R.drawable.ic_main_illustration_pending) + DeviceUIState.PAIRED_NEGATIVE -> appContext.getDrawable(R.drawable.ic_main_illustration_negative) + DeviceUIState.PAIRED_ERROR -> appContext.getDrawable(R.drawable.ic_main_illustration_invalid) else -> appContext.getDrawable(R.drawable.ic_main_illustration_invalid) } } fun formatSubmissionStatusCardFetchingVisible( deviceRegistered: Boolean?, - testResultState: ApiRequestState? + uiStateState: ApiRequestState? ): Int = formatVisibility( deviceRegistered == true && ( - testResultState == ApiRequestState.STARTED || - testResultState == ApiRequestState.FAILED) + uiStateState == ApiRequestState.STARTED || + uiStateState == ApiRequestState.FAILED) ) fun formatSubmissionStatusCardContentVisible( deviceRegistered: Boolean?, - testResultState: ApiRequestState? -): Int = formatVisibility(deviceRegistered == true && testResultState == ApiRequestState.SUCCESS) + uiStateState: ApiRequestState? +): Int = formatVisibility(deviceRegistered == true && uiStateState == ApiRequestState.SUCCESS) fun formatSubmissionTanButtonTint(isValidTanFormat: Boolean) = formatColor( isValidTanFormat, @@ -159,11 +174,25 @@ fun formatSubmissionTanButtonTextColor(isValidTanFormat: Boolean) = formatColor( R.color.colorGreyDisabled ) -fun formatShowSubmissionStatusCard(testResult: TestResult?): Int = - formatVisibility(testResult != POSITIVE) - -fun formatShowSubmissionStatusPositiveCard(testResult: TestResult?): Int = - formatVisibility(testResult == POSITIVE) - -fun formatShowRiskStatusCard(testResult: TestResult?): Int = - formatVisibility(testResult != POSITIVE) +fun formatShowSubmissionStatusCard(deviceUiState: DeviceUIState?): Int = + formatVisibility( + deviceUiState != DeviceUIState.PAIRED_POSITIVE && + deviceUiState != DeviceUIState.PAIRED_POSITIVE_TELETAN && + deviceUiState != DeviceUIState.SUBMITTED_FINAL + ) + +fun formatShowSubmissionStatusPositiveCard(deviceUiState: DeviceUIState?): Int = + formatVisibility( + deviceUiState == DeviceUIState.PAIRED_POSITIVE || + deviceUiState == DeviceUIState.PAIRED_POSITIVE_TELETAN + ) + +fun formatShowSubmissionDoneCard(deviceUiState: DeviceUIState?): Int = + formatVisibility(deviceUiState == DeviceUIState.SUBMITTED_FINAL) + +fun formatShowRiskStatusCard(deviceUiState: DeviceUIState?): Int = + formatVisibility( + deviceUiState != DeviceUIState.PAIRED_POSITIVE && + deviceUiState != DeviceUIState.PAIRED_POSITIVE_TELETAN && + deviceUiState != DeviceUIState.SUBMITTED_FINAL + ) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/security/SecurityHelper.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/security/SecurityHelper.kt index 9a8868b34f1..799a18636a0 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/security/SecurityHelper.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/security/SecurityHelper.kt @@ -23,11 +23,9 @@ import android.content.Context import android.content.SharedPreferences import androidx.security.crypto.EncryptedSharedPreferences import androidx.security.crypto.MasterKeys -import com.android.volley.RequestQueue -import com.android.volley.toolbox.Volley import de.rki.coronawarnapp.CoronaWarnApplication -import de.rki.coronawarnapp.http.OkHttp3Stack import java.security.KeyStore +import java.security.MessageDigest /** * Key Store and Password Access @@ -38,7 +36,7 @@ object SecurityHelper { private val masterKeyAlias = MasterKeys.getOrCreate(keyGenParameterSpec) private const val AndroidKeyStore = "AndroidKeyStore" - val keyStore: KeyStore by lazy { + private val keyStore: KeyStore by lazy { KeyStore.getInstance(AndroidKeyStore).also { it.load(null) } @@ -68,7 +66,8 @@ object SecurityHelper { .toString() .toCharArray() - fun getPinnedWebStack(appContext: Context): RequestQueue { - return Volley.newRequestQueue(appContext, OkHttp3Stack(appContext)) - } + fun hash256(input: String): String = MessageDigest + .getInstance("SHA-256") + .digest(input.toByteArray()) + .fold("", { str, it -> str + "%02x".format(it) }) } diff --git a/Corona-Warn-App/src/main/res/drawable/ic_main_overview_circle.xml b/Corona-Warn-App/src/main/res/drawable/ic_main_overview_circle.xml index 84d7b629759..1aa46b8b109 100644 --- a/Corona-Warn-App/src/main/res/drawable/ic_main_overview_circle.xml +++ b/Corona-Warn-App/src/main/res/drawable/ic_main_overview_circle.xml @@ -3,10 +3,10 @@ android:height="42dp" android:viewportWidth="40" android:viewportHeight="42"> - + diff --git a/Corona-Warn-App/src/main/res/drawable/ic_settings_reset_circle.xml b/Corona-Warn-App/src/main/res/drawable/ic_settings_reset_circle.xml index ded4098d212..b9f5b384bcc 100644 --- a/Corona-Warn-App/src/main/res/drawable/ic_settings_reset_circle.xml +++ b/Corona-Warn-App/src/main/res/drawable/ic_settings_reset_circle.xml @@ -3,26 +3,26 @@ android:height="24dp" android:viewportWidth="24" android:viewportHeight="24"> - - - + + + 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 42f5e3ab808..5c8d871ef92 100644 --- a/Corona-Warn-App/src/main/res/layout/fragment_information.xml +++ b/Corona-Warn-App/src/main/res/layout/fragment_information.xml @@ -8,130 +8,129 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + android:layout_height="match_parent"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Corona-Warn-App/src/main/res/layout/fragment_information_about.xml b/Corona-Warn-App/src/main/res/layout/fragment_information_about.xml index e222be48e28..155009e579b 100644 --- a/Corona-Warn-App/src/main/res/layout/fragment_information_about.xml +++ b/Corona-Warn-App/src/main/res/layout/fragment_information_about.xml @@ -2,70 +2,94 @@ - + android:layout_height="match_parent"> - - - + + - + - + - + - + + + + + + + + + + + + + + + - - - \ No newline at end of file diff --git a/Corona-Warn-App/src/main/res/layout/fragment_information_contact.xml b/Corona-Warn-App/src/main/res/layout/fragment_information_contact.xml index a8b9be9aa33..54f23216215 100644 --- a/Corona-Warn-App/src/main/res/layout/fragment_information_contact.xml +++ b/Corona-Warn-App/src/main/res/layout/fragment_information_contact.xml @@ -2,99 +2,133 @@ - - - - - - - - - - - - - - - - - - - - - - - + android:layout_height="match_parent"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Corona-Warn-App/src/main/res/layout/fragment_information_legal.xml b/Corona-Warn-App/src/main/res/layout/fragment_information_legal.xml index 3f4c91ec331..03fda1730c7 100644 --- a/Corona-Warn-App/src/main/res/layout/fragment_information_legal.xml +++ b/Corona-Warn-App/src/main/res/layout/fragment_information_legal.xml @@ -2,75 +2,99 @@ - + android:layout_height="match_parent"> - + + - + - + - + - + - + - + + + + + + + + + + + + + - - - \ No newline at end of file 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 889f75a31c3..fe958e6ffb0 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 @@ -2,76 +2,99 @@ - + android:layout_height="match_parent"> - + + - - + - + - + - + - + - + + + + + + + + + + + + + - - - \ No newline at end of file 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 f8c799f913d..0f90814a25f 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 @@ -2,75 +2,99 @@ - + android:layout_height="match_parent"> - + + - + - + - + - + - + - + + + + + + + + + + + + + - - - \ No newline at end of file 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 b80435e35f8..f01bbfcc3f6 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,75 +2,99 @@ - + android:layout_height="match_parent"> - + + - + - + - + - + - + - + + + + + + + + + + + + + - - - \ No newline at end of file diff --git a/Corona-Warn-App/src/main/res/layout/fragment_main.xml b/Corona-Warn-App/src/main/res/layout/fragment_main.xml index a9ab393c0b0..dfaba52e58a 100644 --- a/Corona-Warn-App/src/main/res/layout/fragment_main.xml +++ b/Corona-Warn-App/src/main/res/layout/fragment_main.xml @@ -126,10 +126,10 @@ + + + + + + + - - - - - - - - - - - + android:layout_height="match_parent"> + + + + + - - + + - + - - - - - - - - - + - - - + app:layout_constraintTop_toBottomOf="@+id/main_overview_risk"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - + + + + + + \ No newline at end of file diff --git a/Corona-Warn-App/src/main/res/layout/fragment_main_share.xml b/Corona-Warn-App/src/main/res/layout/fragment_main_share.xml index 45b42ae43ed..d364e5d2ae4 100644 --- a/Corona-Warn-App/src/main/res/layout/fragment_main_share.xml +++ b/Corona-Warn-App/src/main/res/layout/fragment_main_share.xml @@ -7,70 +7,87 @@ + - - - - - - -