From aaeb4996d16f8b94a4030d6ea57dbe34df715058 Mon Sep 17 00:00:00 2001
From: BMItter <Berndus@gmx.de>
Date: Mon, 17 May 2021 15:52:19 +0200
Subject: [PATCH] Vaccination server - ValueSet, Responses,..
 (EXPOSUREAPP-6855) (#3141)

* Created vaccination server

* Separation of concern

* Lazy initialization

* better err messages

* Local caching

* detekt & ktlint clean

* missing import

* Update DataResetTest.kt

* whatever

* Fixed tests

* Serialize as we want

* - Load value sets, Use download cdn http client

* 304 cleanup

* Created stored value set version

* fix conflicted VaccinationListFragment

* clean detekt & sourceCheck

* Update VaccinationListViewModel.kt

* user errors wont crash the app

* Tests for repo

* clean

* Ignore server cache headers

* detekt

Co-authored-by: Matthias Urhahn <matthias.urhahn@sap.com>
---
 .../de/rki/coronawarnapp/util/DataReset.kt    |   4 +-
 .../core/repository/ValueSetsRepository.kt    |  74 +++++-
 .../storage/VaccinationContainer.kt           |   1 +
 .../repository/storage/ValueSetsStorage.kt    |  98 ++++++++
 .../valueset/DefaultVaccinationValueSet.kt    |  21 ++
 .../core/server/valueset/VaccinationServer.kt |  62 ++++-
 .../server/valueset/VaccinationValueSet.kt    |  20 +-
 .../valueset/VaccinationValueSetModule.kt     |  63 ++++--
 ...ationValueSetHttpClient.kt => ValueSet.kt} |   2 +-
 .../internal/VaccinationValueSetMapper.kt     |  28 +++
 .../ValueSetInvalidSignatureException.kt      |   9 +
 .../ui/list/VaccinationListViewModel.kt       |  10 +
 .../rki/coronawarnapp/util/DataResetTest.kt   |   3 +
 .../repository/ValueSetsRepositoryTest.kt     | 213 ++++++++++++++++++
 .../storage/VaccinationContainerTest.kt       |  26 ++-
 15 files changed, 596 insertions(+), 38 deletions(-)
 create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/repository/storage/ValueSetsStorage.kt
 create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/server/valueset/DefaultVaccinationValueSet.kt
 rename Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/server/valueset/{VaccinationValueSetHttpClient.kt => ValueSet.kt} (77%)
 create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/server/valueset/internal/VaccinationValueSetMapper.kt
 create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/server/valueset/internal/ValueSetInvalidSignatureException.kt
 create mode 100644 Corona-Warn-App/src/test/java/de/rki/coronawarnapp/vaccination/core/repository/ValueSetsRepositoryTest.kt

diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/DataReset.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/DataReset.kt
index 55b3107f6..df7f430bb 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/DataReset.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/DataReset.kt
@@ -25,6 +25,7 @@ import de.rki.coronawarnapp.storage.TracingSettings
 import de.rki.coronawarnapp.submission.SubmissionRepository
 import de.rki.coronawarnapp.submission.SubmissionSettings
 import de.rki.coronawarnapp.ui.presencetracing.TraceLocationPreferences
+import de.rki.coronawarnapp.vaccination.core.repository.ValueSetsRepository
 import de.rki.coronawarnapp.vaccination.core.VaccinationPreferences
 import de.rki.coronawarnapp.vaccination.core.repository.VaccinationRepository
 import kotlinx.coroutines.sync.Mutex
@@ -63,6 +64,7 @@ class DataReset @Inject constructor(
     private val traceWarningRepository: TraceWarningRepository,
     private val coronaTestRepository: CoronaTestRepository,
     private val ratProfileSettings: RATProfileSettings,
+    private val valueSetsRepository: ValueSetsRepository,
     private val vaccinationPreferences: VaccinationPreferences,
     private val vaccinationRepository: VaccinationRepository,
 ) {
@@ -88,7 +90,6 @@ class DataReset @Inject constructor(
         riskLevelStorage.clear()
         contactDiaryPreferences.clear()
         traceLocationPreferences.clear()
-
         cwaSettings.clear()
         surveySettings.clear()
         analyticsSettings.clear()
@@ -110,6 +111,7 @@ class DataReset @Inject constructor(
         coronaTestRepository.clear()
         ratProfileSettings.deleteProfile()
 
+        valueSetsRepository.clear()
         vaccinationRepository.clear()
         vaccinationPreferences.clear()
 
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/repository/ValueSetsRepository.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/repository/ValueSetsRepository.kt
index e3497c090..ca5f244a1 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/repository/ValueSetsRepository.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/repository/ValueSetsRepository.kt
@@ -1,17 +1,77 @@
 package de.rki.coronawarnapp.vaccination.core.repository
 
-import de.rki.coronawarnapp.submission.server.SubmissionServer
+import dagger.Reusable
+import de.rki.coronawarnapp.util.coroutine.AppScope
+import de.rki.coronawarnapp.vaccination.core.repository.storage.ValueSetsStorage
+import de.rki.coronawarnapp.vaccination.core.server.valueset.DefaultVaccinationValueSet
+import de.rki.coronawarnapp.vaccination.core.server.valueset.VaccinationServer
 import de.rki.coronawarnapp.vaccination.core.server.valueset.VaccinationValueSet
-
+import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.stateIn
+import timber.log.Timber
+import java.util.Locale
 import javax.inject.Inject
-import javax.inject.Singleton
 
-@Singleton
+@Reusable
 class ValueSetsRepository @Inject constructor(
-    private val submissionServer: SubmissionServer
+    private val vaccinationServer: VaccinationServer,
+    private val valueSetsStorage: ValueSetsStorage,
+    @AppScope appScope: CoroutineScope
 ) {
 
-    val latestValueSet: Flow<VaccinationValueSet?> = flowOf(null)
+    private val internalFlow = MutableStateFlow<Locale?>(null)
+
+    val latestValueSet: Flow<VaccinationValueSet?> = internalFlow
+        .filterNotNull()
+        .map {
+            fromServer(it) ?: fromLocalStorage() ?: fromServer(Locale.ENGLISH) ?: createEmptyValueSet(it)
+            // Return empty value set as last resort
+        }
+        .stateIn(
+            scope = appScope,
+            started = SharingStarted.Lazily,
+            initialValue = createEmptyValueSet(Locale.ENGLISH)
+        )
+
+    fun reloadValueSet(languageCode: Locale) {
+        Timber.d("reloadValueSet(languageCode=%s)", languageCode)
+        internalFlow.value = languageCode
+    }
+
+    private suspend fun fromServer(languageCode: Locale): VaccinationValueSet? = try {
+        Timber.d("fromServer(languageCode=%s)", languageCode)
+        vaccinationServer.getVaccinationValueSets(languageCode = languageCode)?.also {
+            Timber.d("Saving new value sets %s", it)
+            valueSetsStorage.vaccinationValueSet = it
+        }
+    } catch (e: Exception) {
+        Timber.w(e, "fromServer(): %s", e.message)
+        null
+    }
+
+    private fun fromLocalStorage(): VaccinationValueSet? = try {
+        Timber.d("fromLocalStorage()")
+        valueSetsStorage.vaccinationValueSet
+    } catch (e: Exception) {
+        Timber.w(e, "fromLocalStorage(): %s", e.message)
+        null
+    }
+
+    private fun createEmptyValueSet(languageCode: Locale) = DefaultVaccinationValueSet(
+        languageCode = languageCode,
+        vp = DefaultVaccinationValueSet.DefaultValueSet(items = emptyList()),
+        mp = DefaultVaccinationValueSet.DefaultValueSet(items = emptyList()),
+        ma = DefaultVaccinationValueSet.DefaultValueSet(items = emptyList())
+    )
+
+    fun clear() {
+        Timber.d("Clearing value sets")
+        vaccinationServer.clear()
+        valueSetsStorage.clear()
+    }
 }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/repository/storage/VaccinationContainer.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/repository/storage/VaccinationContainer.kt
index 76ca4e4b5..d9320fb31 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/repository/storage/VaccinationContainer.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/repository/storage/VaccinationContainer.kt
@@ -13,6 +13,7 @@ import de.rki.coronawarnapp.vaccination.core.qrcode.VaccinationCertificateData
 import de.rki.coronawarnapp.vaccination.core.qrcode.VaccinationCertificateQRCode
 import de.rki.coronawarnapp.vaccination.core.qrcode.VaccinationQRCodeExtractor
 import de.rki.coronawarnapp.vaccination.core.server.valueset.VaccinationValueSet
+import de.rki.coronawarnapp.vaccination.core.server.valueset.getDisplayText
 import org.joda.time.Instant
 import org.joda.time.LocalDate
 
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/repository/storage/ValueSetsStorage.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/repository/storage/ValueSetsStorage.kt
new file mode 100644
index 000000000..e9d91b475
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/repository/storage/ValueSetsStorage.kt
@@ -0,0 +1,98 @@
+package de.rki.coronawarnapp.vaccination.core.repository.storage
+
+import android.content.Context
+import android.content.SharedPreferences
+import androidx.annotation.Keep
+import androidx.core.content.edit
+import com.google.gson.Gson
+import com.google.gson.annotations.SerializedName
+import dagger.Reusable
+import de.rki.coronawarnapp.util.di.AppContext
+import de.rki.coronawarnapp.util.preferences.clearAndNotify
+import de.rki.coronawarnapp.util.serialization.BaseGson
+import de.rki.coronawarnapp.vaccination.core.server.valueset.VaccinationValueSet
+import timber.log.Timber
+import java.util.Locale
+import javax.inject.Inject
+
+@Reusable
+class ValueSetsStorage @Inject constructor(
+    @AppContext private val context: Context,
+    @BaseGson private val gson: Gson
+) {
+
+    private val prefs: SharedPreferences by lazy {
+        context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE)
+    }
+
+    var vaccinationValueSet: VaccinationValueSet?
+        get() = getValueSet()
+        set(value) = setValueSet(value)
+
+    private fun getValueSet(): VaccinationValueSet? {
+        Timber.d("Loading value set")
+        return prefs.getString(PKEY_VALUE_SETS_PREFIX, null)?.let {
+            gson.fromJson(it, StoredVaccinationValueSet::class.java).also { loaded -> Timber.d("Loaded %s", loaded) }
+        }.also { Timber.d("Returning %s", it) }
+    }
+
+    private fun setValueSet(value: VaccinationValueSet?) {
+        Timber.d("Saving %s", value)
+        value?.let {
+            prefs.edit {
+                val storeValue = it.toStoredVaccinationValueSet()
+                val json = gson.toJson(storeValue, StoredVaccinationValueSet::class.java)
+                Timber.d("String %s", json)
+                putString(PKEY_VALUE_SETS_PREFIX, json)
+            }
+        }
+    }
+
+    fun clear() {
+        Timber.d("Clearing local storage")
+        prefs.clearAndNotify()
+    }
+
+    @Keep
+    private data class StoredVaccinationValueSet(
+        @SerializedName("languageCode") override val languageCode: Locale,
+        @SerializedName("vp") override val vp: StoredValueSet,
+        @SerializedName("mp") override val mp: StoredValueSet,
+        @SerializedName("ma") override val ma: StoredValueSet
+    ) : VaccinationValueSet {
+
+        @Keep
+        data class StoredValueSet(
+            @SerializedName("items") override val items: List<StoredItem>
+        ) : VaccinationValueSet.ValueSet {
+
+            @Keep
+            data class StoredItem(
+                @SerializedName("key") override val key: String,
+                @SerializedName("displayText") override val displayText: String
+            ) : VaccinationValueSet.ValueSet.Item
+        }
+    }
+
+    private fun VaccinationValueSet.toStoredVaccinationValueSet(): StoredVaccinationValueSet =
+        StoredVaccinationValueSet(
+            languageCode = languageCode,
+            vp = vp.toStoredValueSet(),
+            mp = mp.toStoredValueSet(),
+            ma = ma.toStoredValueSet()
+        )
+
+    private fun VaccinationValueSet.ValueSet.toStoredValueSet(): StoredVaccinationValueSet.StoredValueSet =
+        StoredVaccinationValueSet.StoredValueSet(
+            items = items.map { it.toStoredItem() }
+        )
+
+    private fun VaccinationValueSet.ValueSet.Item.toStoredItem(): StoredVaccinationValueSet.StoredValueSet.StoredItem =
+        StoredVaccinationValueSet.StoredValueSet.StoredItem(
+            key = key,
+            displayText = displayText
+        )
+}
+
+private const val PREF_NAME = "valuesets_local"
+private const val PKEY_VALUE_SETS_PREFIX = "valuesets"
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/server/valueset/DefaultVaccinationValueSet.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/server/valueset/DefaultVaccinationValueSet.kt
new file mode 100644
index 000000000..1962cf330
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/server/valueset/DefaultVaccinationValueSet.kt
@@ -0,0 +1,21 @@
+package de.rki.coronawarnapp.vaccination.core.server.valueset
+
+import java.util.Locale
+
+data class DefaultVaccinationValueSet(
+    override val languageCode: Locale,
+    override val vp: VaccinationValueSet.ValueSet,
+    override val mp: VaccinationValueSet.ValueSet,
+    override val ma: VaccinationValueSet.ValueSet
+) : VaccinationValueSet {
+
+    data class DefaultValueSet(
+        override val items: List<VaccinationValueSet.ValueSet.Item>
+    ) : VaccinationValueSet.ValueSet {
+
+        data class DefaultItem(
+            override val key: String,
+            override val displayText: String
+        ) : VaccinationValueSet.ValueSet.Item
+    }
+}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/server/valueset/VaccinationServer.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/server/valueset/VaccinationServer.kt
index cb64715a6..0d108cd2a 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/server/valueset/VaccinationServer.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/server/valueset/VaccinationServer.kt
@@ -1,7 +1,19 @@
 package de.rki.coronawarnapp.vaccination.core.server.valueset
 
+import dagger.Lazy
 import dagger.Reusable
+import de.rki.coronawarnapp.server.protocols.internal.dgc.ValueSetsOuterClass
+import de.rki.coronawarnapp.util.ZipHelper.readIntoMap
+import de.rki.coronawarnapp.util.ZipHelper.unzip
+import de.rki.coronawarnapp.util.coroutine.DispatcherProvider
+import de.rki.coronawarnapp.util.security.SignatureValidation
+import de.rki.coronawarnapp.vaccination.core.server.valueset.internal.ValueSetInvalidSignatureException
+import de.rki.coronawarnapp.vaccination.core.server.valueset.internal.toVaccinationValueSet
+import kotlinx.coroutines.withContext
 import okhttp3.Cache
+import okhttp3.ResponseBody
+import retrofit2.HttpException
+import retrofit2.Response
 import timber.log.Timber
 import java.util.Locale
 import javax.inject.Inject
@@ -11,11 +23,52 @@ import javax.inject.Inject
  */
 @Reusable
 class VaccinationServer @Inject constructor(
-    @VaccinationValueSetHttpClient private val cache: Cache
+    @ValueSet private val cache: Cache,
+    private val apiV1: Lazy<VaccinationValueSetApiV1>,
+    private val dispatcherProvider: DispatcherProvider,
+    private val signatureValidation: SignatureValidation
 ) {
 
-    suspend fun getVaccinationValueSets(languageCode: Locale): VaccinationValueSet {
-        throw NotImplementedError()
+    suspend fun getVaccinationValueSets(languageCode: Locale): VaccinationValueSet? =
+        withContext(dispatcherProvider.Default) {
+            return@withContext try {
+                val response = requestValueSets(languageCode.language)
+                if (!response.isSuccessful) throw HttpException(response)
+
+                val body = requireNotNull(response.body()) { "Body of response was null" }
+                val valueSetsProtobuf = body.parseBody()
+                valueSetsProtobuf.toVaccinationValueSet(languageCode = languageCode)
+            } catch (e: Exception) {
+                Timber.e(e, "Getting vaccination value sets from server failed cause: ${e.message}")
+                null
+            }
+        }
+
+    private suspend fun requestValueSets(languageCode: String): Response<ResponseBody> =
+        withContext(dispatcherProvider.IO) {
+            Timber.d("Requesting value sets for language $languageCode from server")
+            apiV1.get().getValueSets(languageCode = languageCode)
+        }
+
+    private fun ResponseBody.parseBody(): ValueSetsOuterClass.ValueSets {
+        val fileMap = this.byteStream().unzip().readIntoMap()
+
+        val exportBinary = fileMap[EXPORT_BINARY_FILE_NAME]
+        val exportSignature = fileMap[EXPORT_SIGNATURE_FILE_NAME]
+
+        if (exportBinary == null || exportSignature == null)
+            throw ValueSetInvalidSignatureException(msg = "Unknown files ${fileMap.entries}")
+
+        val hasValidSignature = signatureValidation.hasValidSignature(
+            toVerify = exportBinary,
+            signatureList = SignatureValidation.parseTEKStyleSignature(exportSignature)
+        )
+
+        if (!hasValidSignature) {
+            throw ValueSetInvalidSignatureException(msg = "Signature of value sets did not match")
+        }
+
+        return ValueSetsOuterClass.ValueSets.parseFrom(exportBinary)
     }
 
     fun clear() {
@@ -24,3 +77,6 @@ class VaccinationServer @Inject constructor(
         cache.evictAll()
     }
 }
+
+private const val EXPORT_BINARY_FILE_NAME = "export.bin"
+private const val EXPORT_SIGNATURE_FILE_NAME = "export.sig"
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/server/valueset/VaccinationValueSet.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/server/valueset/VaccinationValueSet.kt
index 6bace45c4..2ebc6e08a 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/server/valueset/VaccinationValueSet.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/server/valueset/VaccinationValueSet.kt
@@ -1,9 +1,27 @@
 package de.rki.coronawarnapp.vaccination.core.server.valueset
 
+import androidx.annotation.Keep
 import java.util.Locale
 
+@Keep
 interface VaccinationValueSet {
     val languageCode: Locale
+    val vp: ValueSet
+    val mp: ValueSet
+    val ma: ValueSet
 
-    fun getDisplayText(key: String): String?
+    interface ValueSet {
+        val items: List<Item>
+
+        // Use custom item instead of map to allow for future extensions
+        interface Item {
+            val key: String
+            val displayText: String
+        }
+    }
 }
+
+fun VaccinationValueSet.getDisplayText(key: String): String? =
+    vp.getDisplayText(key) ?: mp.getDisplayText(key) ?: ma.getDisplayText(key)
+
+fun VaccinationValueSet.ValueSet.getDisplayText(key: String): String? = items.find { key == it.key }?.displayText
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/server/valueset/VaccinationValueSetModule.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/server/valueset/VaccinationValueSetModule.kt
index 1a4d73746..0fd80cda3 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/server/valueset/VaccinationValueSetModule.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/server/valueset/VaccinationValueSetModule.kt
@@ -4,48 +4,67 @@ import android.content.Context
 import dagger.Module
 import dagger.Provides
 import dagger.Reusable
-import de.rki.coronawarnapp.environment.vaccination.VaccinationCertificateCDNUrl
-import de.rki.coronawarnapp.http.HttpClientDefault
+import de.rki.coronawarnapp.environment.download.DownloadCDNHttpClient
+import de.rki.coronawarnapp.environment.download.DownloadCDNServerUrl
 import de.rki.coronawarnapp.util.di.AppContext
 import okhttp3.Cache
+import okhttp3.CacheControl
+import okhttp3.Interceptor
 import okhttp3.OkHttpClient
+import okhttp3.Response
 import retrofit2.Retrofit
 import java.io.File
+import java.util.concurrent.TimeUnit
 
 @Module
 class VaccinationValueSetModule {
 
     @Reusable
-    @VaccinationValueSetHttpClient
+    @ValueSet
     @Provides
     fun cache(
         @AppContext context: Context
     ): Cache {
         val cacheDir = File(context.cacheDir, "vaccination_value")
-        val cacheFile = File(cacheDir, "http_cache")
-        return Cache(cacheFile, CACHE_SIZE_5MB)
+        return Cache(cacheDir, CACHE_SIZE_5MB)
     }
 
-    @Reusable
-    @VaccinationValueSetHttpClient
-    @Provides
-    fun httpClient(
-        @HttpClientDefault defaultHttpClient: OkHttpClient,
-        @VaccinationValueSetHttpClient cache: Cache
-    ): OkHttpClient = defaultHttpClient.newBuilder()
-        .cache(cache)
-        .build()
-
     @Reusable
     @Provides
     fun api(
-        @VaccinationValueSetHttpClient httpClient: OkHttpClient,
-        @VaccinationCertificateCDNUrl url: String
-    ): VaccinationValueSetApiV1 = Retrofit.Builder()
-        .client(httpClient)
-        .baseUrl(url)
-        .build()
-        .create(VaccinationValueSetApiV1::class.java)
+        @DownloadCDNHttpClient httpClient: OkHttpClient,
+        @DownloadCDNServerUrl url: String,
+        @ValueSet cache: Cache
+    ): VaccinationValueSetApiV1 {
+        val client = httpClient.newBuilder()
+            .addNetworkInterceptor(CacheInterceptor())
+            .cache(cache)
+            .build()
+
+        return Retrofit.Builder()
+            .client(client)
+            .baseUrl(url)
+            .build()
+            .create(VaccinationValueSetApiV1::class.java)
+    }
+
+    private class CacheInterceptor : Interceptor {
+        override fun intercept(chain: Interceptor.Chain): Response {
+            val response = chain.proceed(chain.request())
+
+            val cacheControl = CacheControl.Builder()
+                .maxAge(300, TimeUnit.SECONDS)
+                .build()
+
+            // We cache as we please
+            val cacheHeader = "Cache-Control"
+            return response.newBuilder()
+                .removeHeader("Pragma")
+                .removeHeader(cacheHeader)
+                .addHeader(cacheHeader, cacheControl.toString())
+                .build()
+        }
+    }
 
     companion object {
         private const val CACHE_SIZE_5MB = 5 * 1024 * 1024L // 5MB
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/server/valueset/VaccinationValueSetHttpClient.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/server/valueset/ValueSet.kt
similarity index 77%
rename from Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/server/valueset/VaccinationValueSetHttpClient.kt
rename to Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/server/valueset/ValueSet.kt
index 1650bcb5f..1300c045c 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/server/valueset/VaccinationValueSetHttpClient.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/server/valueset/ValueSet.kt
@@ -5,4 +5,4 @@ import javax.inject.Qualifier
 @Qualifier
 @MustBeDocumented
 @Retention(AnnotationRetention.RUNTIME)
-annotation class VaccinationValueSetHttpClient
+annotation class ValueSet
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/server/valueset/internal/VaccinationValueSetMapper.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/server/valueset/internal/VaccinationValueSetMapper.kt
new file mode 100644
index 000000000..ad13f40aa
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/server/valueset/internal/VaccinationValueSetMapper.kt
@@ -0,0 +1,28 @@
+package de.rki.coronawarnapp.vaccination.core.server.valueset.internal
+
+import de.rki.coronawarnapp.server.protocols.internal.dgc.ValueSetsOuterClass
+import de.rki.coronawarnapp.vaccination.core.server.valueset.DefaultVaccinationValueSet
+import de.rki.coronawarnapp.vaccination.core.server.valueset.VaccinationValueSet
+import timber.log.Timber
+import java.util.Locale
+
+internal fun ValueSetsOuterClass.ValueSets.toVaccinationValueSet(languageCode: Locale): VaccinationValueSet {
+    Timber.d("toVaccinationValueSet(valueSets=%s, languageCode=%s)", this, languageCode)
+    return DefaultVaccinationValueSet(
+        languageCode = languageCode,
+        vp = vp.toValueSet(),
+        mp = mp.toValueSet(),
+        ma = ma.toValueSet()
+    ).also { Timber.tag(TAG).d("Created %s", it) }
+}
+
+internal fun ValueSetsOuterClass.ValueSet.toValueSet(): VaccinationValueSet.ValueSet =
+    DefaultVaccinationValueSet.DefaultValueSet(items = itemsList.map { it.toItem() })
+
+internal fun ValueSetsOuterClass.ValueSetItem.toItem(): VaccinationValueSet.ValueSet.Item =
+    DefaultVaccinationValueSet.DefaultValueSet.DefaultItem(
+        key = key,
+        displayText = displayText
+    )
+
+private const val TAG: String = "ValueSetMapper"
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/server/valueset/internal/ValueSetInvalidSignatureException.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/server/valueset/internal/ValueSetInvalidSignatureException.kt
new file mode 100644
index 000000000..6de24adaf
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/server/valueset/internal/ValueSetInvalidSignatureException.kt
@@ -0,0 +1,9 @@
+package de.rki.coronawarnapp.vaccination.core.server.valueset.internal
+
+import de.rki.coronawarnapp.exception.reporting.ErrorCodes
+import de.rki.coronawarnapp.util.security.InvalidSignatureException
+
+class ValueSetInvalidSignatureException(msg: String) : InvalidSignatureException(
+    code = ErrorCodes.APPLICATION_CONFIGURATION_INVALID.code,
+    message = msg
+)
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/ui/list/VaccinationListViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/ui/list/VaccinationListViewModel.kt
index 0ed0550a9..a8dbcba0f 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/ui/list/VaccinationListViewModel.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/ui/list/VaccinationListViewModel.kt
@@ -1,17 +1,21 @@
 package de.rki.coronawarnapp.vaccination.ui.list
 
+import android.content.Context
 import androidx.lifecycle.LiveData
 import androidx.lifecycle.asLiveData
 import dagger.assisted.Assisted
 import dagger.assisted.AssistedFactory
 import dagger.assisted.AssistedInject
+import de.rki.coronawarnapp.contactdiary.util.getLocale
 import de.rki.coronawarnapp.util.TimeAndDateExtensions.toDayFormat
+import de.rki.coronawarnapp.util.di.AppContext
 import de.rki.coronawarnapp.util.ui.SingleLiveEvent
 import de.rki.coronawarnapp.util.viewmodel.CWAViewModel
 import de.rki.coronawarnapp.util.viewmodel.CWAViewModelFactory
 import de.rki.coronawarnapp.vaccination.core.VaccinatedPerson
 import de.rki.coronawarnapp.vaccination.core.VaccinatedPerson.Status.COMPLETE
 import de.rki.coronawarnapp.vaccination.core.repository.VaccinationRepository
+import de.rki.coronawarnapp.vaccination.core.repository.ValueSetsRepository
 import de.rki.coronawarnapp.vaccination.ui.list.adapter.VaccinationListItem
 import de.rki.coronawarnapp.vaccination.ui.list.adapter.viewholder.VaccinationListIncompleteTopCardItemVH.VaccinationListIncompleteTopCardItem
 import de.rki.coronawarnapp.vaccination.ui.list.adapter.viewholder.VaccinationListNameCardItemVH.VaccinationListNameCardItem
@@ -21,9 +25,15 @@ import kotlinx.coroutines.flow.map
 
 class VaccinationListViewModel @AssistedInject constructor(
     vaccinationRepository: VaccinationRepository,
+    valueSetsRepository: ValueSetsRepository,
+    @AppContext context: Context,
     @Assisted private val personIdentifierCodeSha256: String
 ) : CWAViewModel() {
 
+    init {
+        valueSetsRepository.reloadValueSet(languageCode = context.getLocale())
+    }
+
     val events = SingleLiveEvent<Event>()
 
     private val vaccinatedPersonFlow = vaccinationRepository.vaccinationInfos.map { vaccinatedPersonSet ->
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/DataResetTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/DataResetTest.kt
index 1dfedac57..5577560dc 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/DataResetTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/DataResetTest.kt
@@ -26,6 +26,7 @@ import de.rki.coronawarnapp.submission.SubmissionSettings
 import de.rki.coronawarnapp.ui.presencetracing.TraceLocationPreferences
 import de.rki.coronawarnapp.vaccination.core.repository.VaccinationRepository
 import de.rki.coronawarnapp.vaccination.core.VaccinationPreferences
+import de.rki.coronawarnapp.vaccination.core.repository.ValueSetsRepository
 import io.mockk.MockKAnnotations
 import io.mockk.coVerify
 import io.mockk.impl.annotations.MockK
@@ -62,6 +63,7 @@ internal class DataResetTest : BaseTest() {
     @MockK lateinit var ratProfileSettings: RATProfileSettings
     @MockK lateinit var vaccinationRepository: VaccinationRepository
     @MockK lateinit var vaccinationPreferences: VaccinationPreferences
+    @MockK lateinit var valueSetsRepository: ValueSetsRepository
 
     @BeforeEach
     fun setUp() {
@@ -95,6 +97,7 @@ internal class DataResetTest : BaseTest() {
         ratProfileSettings = ratProfileSettings,
         vaccinationPreferences = vaccinationPreferences,
         vaccinationRepository = vaccinationRepository,
+        valueSetsRepository = valueSetsRepository
     )
 
     @Test
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/vaccination/core/repository/ValueSetsRepositoryTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/vaccination/core/repository/ValueSetsRepositoryTest.kt
new file mode 100644
index 000000000..3edc08266
--- /dev/null
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/vaccination/core/repository/ValueSetsRepositoryTest.kt
@@ -0,0 +1,213 @@
+package de.rki.coronawarnapp.vaccination.core.repository
+
+import de.rki.coronawarnapp.vaccination.core.repository.storage.ValueSetsStorage
+import de.rki.coronawarnapp.vaccination.core.server.valueset.DefaultVaccinationValueSet
+import de.rki.coronawarnapp.vaccination.core.server.valueset.VaccinationServer
+import io.kotest.matchers.shouldBe
+import io.mockk.MockKAnnotations
+import io.mockk.coEvery
+import io.mockk.coVerify
+import io.mockk.every
+import io.mockk.impl.annotations.MockK
+import io.mockk.just
+import io.mockk.runs
+import io.mockk.verify
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.test.TestCoroutineScope
+import kotlinx.coroutines.test.runBlockingTest
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+import testhelpers.BaseTest
+import java.util.Locale
+
+class ValueSetsRepositoryTest : BaseTest() {
+
+    @MockK lateinit var vaccinationServer: VaccinationServer
+    @MockK lateinit var valueSetsStorage: ValueSetsStorage
+
+    private val testScope = TestCoroutineScope()
+
+    private val emptyValueSetEN = createValueSet(languageCode = Locale.ENGLISH)
+    private val emptyValueSetDE = createValueSet(languageCode = Locale.GERMAN)
+
+    private val valueSetEN = createValueSet(
+        languageCode = Locale.ENGLISH,
+        vpItems = listOf(
+            DefaultVaccinationValueSet.DefaultValueSet.DefaultItem(
+                key = "1119305005",
+                displayText = "Vaccine-Name"
+            )
+        ),
+        mpItems = listOf(
+            DefaultVaccinationValueSet.DefaultValueSet.DefaultItem(
+                key = "EU/1/21/1529",
+                displayText = "MedicalProduct-Name"
+            )
+        ),
+        maItems = listOf(
+            DefaultVaccinationValueSet.DefaultValueSet.DefaultItem(
+                key = "ORG-100001699",
+                displayText = "Manufactorer-Name"
+            )
+        )
+    )
+
+    private val valueSetDE = createValueSet(
+        languageCode = Locale.GERMAN,
+        vpItems = listOf(
+            DefaultVaccinationValueSet.DefaultValueSet.DefaultItem(
+                key = "1119305005",
+                displayText = "Impfstoff-Name"
+            )
+        ),
+        mpItems = listOf(
+            DefaultVaccinationValueSet.DefaultValueSet.DefaultItem(
+                key = "EU/1/21/1529",
+                displayText = "Arzneimittel-Name"
+            )
+        ),
+        maItems = listOf(
+            DefaultVaccinationValueSet.DefaultValueSet.DefaultItem(
+                key = "ORG-100001699",
+                displayText = "Hersteller-Name"
+            )
+        )
+    )
+
+    private fun createValueSet(
+        languageCode: Locale,
+        vpItems: List<DefaultVaccinationValueSet.DefaultValueSet.DefaultItem> = emptyList(),
+        mpItems: List<DefaultVaccinationValueSet.DefaultValueSet.DefaultItem> = emptyList(),
+        maItems: List<DefaultVaccinationValueSet.DefaultValueSet.DefaultItem> = emptyList()
+    ) = DefaultVaccinationValueSet(
+        languageCode = languageCode,
+        vp = DefaultVaccinationValueSet.DefaultValueSet(items = vpItems),
+        mp = DefaultVaccinationValueSet.DefaultValueSet(items = mpItems),
+        ma = DefaultVaccinationValueSet.DefaultValueSet(items = maItems)
+    )
+
+    private fun createInstance() = ValueSetsRepository(
+        vaccinationServer = vaccinationServer,
+        valueSetsStorage = valueSetsStorage,
+        appScope = testScope
+    )
+
+    @BeforeEach
+    fun setUp() {
+        MockKAnnotations.init(this)
+        coEvery { vaccinationServer.getVaccinationValueSets(any()) } returns null
+        every { vaccinationServer.clear() } just runs
+        every { valueSetsStorage.vaccinationValueSet } returns null
+        every { valueSetsStorage.vaccinationValueSet = any() } just runs
+        every { valueSetsStorage.clear() } just runs
+    }
+
+    @Test
+    fun `default value is an empty value set EN`() = runBlockingTest {
+        createInstance().run {
+            latestValueSet.first() shouldBe emptyValueSetEN
+
+            coVerify(exactly = 0) {
+                vaccinationServer.getVaccinationValueSets(any())
+                valueSetsStorage.vaccinationValueSet
+            }
+        }
+    }
+
+    @Test
+    fun `falls back to empty value set with specified language code`() = runBlockingTest {
+        createInstance().run {
+            reloadValueSet(languageCode = Locale.GERMAN)
+            latestValueSet.first() shouldBe emptyValueSetDE
+
+            coVerify(exactly = 1) {
+                valueSetsStorage.vaccinationValueSet
+                vaccinationServer.getVaccinationValueSets(Locale.GERMAN)
+                vaccinationServer.getVaccinationValueSets(Locale.ENGLISH)
+            }
+        }
+    }
+
+    @Test
+    fun `returns value set for specified language from server`() = runBlockingTest {
+        coEvery { vaccinationServer.getVaccinationValueSets(Locale.GERMAN) } returns valueSetDE
+        coEvery { vaccinationServer.getVaccinationValueSets(Locale.ENGLISH) } returns valueSetEN
+
+        createInstance().run {
+            reloadValueSet(Locale.GERMAN)
+            latestValueSet.first() shouldBe valueSetDE
+
+            reloadValueSet(Locale.ENGLISH)
+            latestValueSet.first() shouldBe valueSetEN
+        }
+
+        verify(exactly = 0) {
+            valueSetsStorage.vaccinationValueSet
+        }
+
+        coVerify(exactly = 1) {
+            vaccinationServer.getVaccinationValueSets(Locale.GERMAN)
+            vaccinationServer.getVaccinationValueSets(Locale.ENGLISH)
+        }
+    }
+
+    @Test
+    fun `if no value set is available for specified language fall back to EN`() = runBlockingTest {
+        coEvery { vaccinationServer.getVaccinationValueSets(Locale.ENGLISH) } returns valueSetEN
+
+        createInstance().run {
+            reloadValueSet(Locale.GERMAN)
+            latestValueSet.first() shouldBe valueSetEN
+
+            coVerify(exactly = 1) {
+                vaccinationServer.getVaccinationValueSets(Locale.GERMAN)
+                vaccinationServer.getVaccinationValueSets(Locale.ENGLISH)
+            }
+        }
+    }
+
+    @Test
+    fun `use local storage if server returns nothing`() = runBlockingTest {
+        every { valueSetsStorage.vaccinationValueSet } returns valueSetDE
+
+        createInstance().run {
+            reloadValueSet(Locale.GERMAN)
+            latestValueSet.first() shouldBe valueSetDE
+
+            coVerify(exactly = 1) {
+                valueSetsStorage.vaccinationValueSet
+                vaccinationServer.getVaccinationValueSets(any())
+            }
+        }
+    }
+
+    @Test
+    fun `user errors will not crash the app`() = runBlockingTest {
+        val userError = Exception("User error")
+        coEvery { vaccinationServer.getVaccinationValueSets(any()) } throws userError
+        every { valueSetsStorage.vaccinationValueSet } throws userError
+
+        createInstance().run {
+            reloadValueSet(Locale.GERMAN)
+            latestValueSet.first() shouldBe emptyValueSetDE
+
+            coVerify(exactly = 1) {
+                valueSetsStorage.vaccinationValueSet
+                vaccinationServer.getVaccinationValueSets(Locale.GERMAN)
+                vaccinationServer.getVaccinationValueSets(Locale.ENGLISH)
+            }
+        }
+    }
+
+    @Test
+    fun `clear() clears server and local storage`() {
+        createInstance().run {
+            clear()
+
+            coVerify(exactly = 1) {
+                vaccinationServer.clear()
+                valueSetsStorage.clear()
+            }
+        }
+    }
+}
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/vaccination/core/repository/storage/VaccinationContainerTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/vaccination/core/repository/storage/VaccinationContainerTest.kt
index ec211990b..6aaf73ff2 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/vaccination/core/repository/storage/VaccinationContainerTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/vaccination/core/repository/storage/VaccinationContainerTest.kt
@@ -83,11 +83,31 @@ class VaccinationContainerTest : BaseTest() {
 
     @Test
     fun `mapping to user facing data - with valueset`() {
+        val vpItem = mockk<VaccinationValueSet.ValueSet.Item> {
+            every { key } returns "1119305005"
+            every { displayText } returns "Vaccine-Name"
+        }
+
+        val mpItem = mockk<VaccinationValueSet.ValueSet.Item> {
+            every { key } returns "EU/1/21/1529"
+            every { displayText } returns "MedicalProduct-Name"
+        }
+
+        val maItem = mockk<VaccinationValueSet.ValueSet.Item> {
+            every { key } returns "ORG-100001699"
+            every { displayText } returns "Manufactorer-Name"
+        }
+
+        val vpMockk = mockk<VaccinationValueSet.ValueSet> {
+            every { items } returns listOf(vpItem, mpItem, maItem)
+        }
+
         val valueSet = mockk<VaccinationValueSet> {
-            every { getDisplayText("ORG-100001699") } returns "Manufactorer-Name"
-            every { getDisplayText("EU/1/21/1529") } returns "MedicalProduct-Name"
-            every { getDisplayText("1119305005") } returns "Vaccine-Name"
+            every { vp } returns vpMockk
+            every { mp } returns vpMockk
+            every { ma } returns vpMockk
         }
+
         testData.personAVac1Container.toVaccinationCertificate(valueSet).apply {
             firstName shouldBe "Andreas"
             lastName shouldBe "Astrá Eins"
-- 
GitLab