From 4e200f2ba062958a86b96d00a5f15f5d64d95cb0 Mon Sep 17 00:00:00 2001
From: chris-cwa <69595386+chris-cwa@users.noreply.github.com>
Date: Wed, 23 Jun 2021 12:16:59 +0200
Subject: [PATCH] Recovery certificate repository (EXPOSUERAPP-7617) (#3463)

* + recovery certificate storage

* changed storage approach

* not using valuesets

* + uuid for identification

* requestCertificate

* qr code -> recovery data

* use container id

* wrong exception

* prevented race condition

* remove key if set is empty

* store json, not string set

* use type token instead of dto

* fixed "this" confusion

* no need for extraction

* do not use container id

* removed unused "registeredAt"

* removed redundant identifier

* fixed compile errors

* Fix flow emission

Co-authored-by: Mohamed Metwalli <mohamed.metwalli@sap.com>
---
 .../DuplicateRecoveryCertificateException.kt  |  5 ++
 .../core/RecoveryCertificateRepository.kt     | 87 ++++++++++++++++---
 .../core/RecoveryCertificateWrapper.kt        |  3 +-
 .../storage/RecoveryCertificateContainer.kt   |  4 +-
 .../storage/RecoveryCertificateStorage.kt     | 56 ++++++++++++
 .../storage/StoredRecoveryCertificateData.kt  |  5 --
 6 files changed, 140 insertions(+), 20 deletions(-)
 create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/recovery/core/DuplicateRecoveryCertificateException.kt
 create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/recovery/core/storage/RecoveryCertificateStorage.kt

diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/recovery/core/DuplicateRecoveryCertificateException.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/recovery/core/DuplicateRecoveryCertificateException.kt
new file mode 100644
index 000000000..23e47d2dc
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/recovery/core/DuplicateRecoveryCertificateException.kt
@@ -0,0 +1,5 @@
+package de.rki.coronawarnapp.covidcertificate.recovery.core
+
+class DuplicateRecoveryCertificateException(
+    message: String
+) : IllegalArgumentException(message)
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/recovery/core/RecoveryCertificateRepository.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/recovery/core/RecoveryCertificateRepository.kt
index 94f5fdb63..bcc3498e7 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/recovery/core/RecoveryCertificateRepository.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/recovery/core/RecoveryCertificateRepository.kt
@@ -1,15 +1,26 @@
 package de.rki.coronawarnapp.covidcertificate.recovery.core
 
+import de.rki.coronawarnapp.bugreporting.reportProblem
 import de.rki.coronawarnapp.covidcertificate.common.certificate.DccQrCodeExtractor
+import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidHealthCertificateException
+import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidRecoveryCertificateException
 import de.rki.coronawarnapp.covidcertificate.common.repository.RecoveryCertificateContainerId
 import de.rki.coronawarnapp.covidcertificate.recovery.core.qrcode.RecoveryCertificateQRCode
 import de.rki.coronawarnapp.covidcertificate.recovery.core.storage.RecoveryCertificateContainer
-import de.rki.coronawarnapp.covidcertificate.valueset.ValueSetsRepository
+import de.rki.coronawarnapp.covidcertificate.recovery.core.storage.RecoveryCertificateStorage
+import de.rki.coronawarnapp.covidcertificate.recovery.core.storage.StoredRecoveryCertificateData
 import de.rki.coronawarnapp.util.coroutine.AppScope
 import de.rki.coronawarnapp.util.coroutine.DispatcherProvider
+import de.rki.coronawarnapp.util.flow.HotDataFlow
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.catch
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.flow.onStart
+import kotlinx.coroutines.plus
 import timber.log.Timber
 import javax.inject.Inject
 import javax.inject.Singleton
@@ -17,26 +28,82 @@ import javax.inject.Singleton
 @Singleton
 class RecoveryCertificateRepository @Inject constructor(
     @AppScope private val appScope: CoroutineScope,
-    private val dispatcherProvider: DispatcherProvider,
+    dispatcherProvider: DispatcherProvider,
     private val qrCodeExtractor: DccQrCodeExtractor,
-    valueSetsRepository: ValueSetsRepository,
+    private val storage: RecoveryCertificateStorage,
 ) {
 
-    val certificates: Flow<Set<RecoveryCertificateWrapper>> = flowOf(emptySet())
+    private val internalData: HotDataFlow<Set<RecoveryCertificateContainer>> = HotDataFlow(
+        loggingTag = TAG,
+        scope = appScope + dispatcherProvider.IO,
+        sharingBehavior = SharingStarted.Lazily,
+    ) {
+        storage.recoveryCertificates
+            .map { recoveryCertificate ->
+                RecoveryCertificateContainer(
+                    data = recoveryCertificate,
+                    qrCodeExtractor = qrCodeExtractor
+                )
+            }
+            .toSet()
+            .also { Timber.tag(TAG).v("Restored recovery certificate data: %s", it) }
+    }
+
+    init {
+        internalData.data
+            .onStart { Timber.tag(TAG).d("Observing data.") }
+            .onEach { recoveryCertificates ->
+                Timber.tag(TAG).v("Recovery Certificate data changed: %s", recoveryCertificates)
+                storage.recoveryCertificates = recoveryCertificates.map { it.data }.toSet()
+            }
+            .catch {
+                it.reportProblem(TAG, "Failed to snapshot recovery certificate data to storage.")
+                throw it
+            }
+            .launchIn(appScope + dispatcherProvider.IO)
+    }
 
+    val certificates: Flow<Set<RecoveryCertificateWrapper>> =
+        internalData.data.map { set ->
+            set.map { RecoveryCertificateWrapper(null, it) }.toSet()
+        }
+
+    @Throws(InvalidRecoveryCertificateException::class)
     suspend fun registerCertificate(qrCode: RecoveryCertificateQRCode): RecoveryCertificateContainer {
         Timber.tag(TAG).d("registerCertificate(qrCode=%s)", qrCode)
-        throw NotImplementedError()
+        val newContainer = qrCode.toContainer()
+        internalData.updateBlocking {
+            if (any { it.certificateId == newContainer.certificateId }) {
+                throw InvalidRecoveryCertificateException(
+                    InvalidHealthCertificateException.ErrorCode.ALREADY_REGISTERED
+                )
+            }
+            plus(newContainer)
+        }
+        return newContainer
     }
 
-    suspend fun deleteCertificate(containerId: RecoveryCertificateContainerId): RecoveryCertificateContainer? {
+    private fun RecoveryCertificateQRCode.toContainer() = RecoveryCertificateContainer(
+        data = StoredRecoveryCertificateData(
+            recoveryCertificateQrCode = qrCode
+        ),
+        qrCodeExtractor = qrCodeExtractor,
+        isUpdatingData = false
+    )
+
+    suspend fun deleteCertificate(containerId: RecoveryCertificateContainerId) {
         Timber.tag(TAG).d("deleteCertificate(containerId=%s)", containerId)
-        throw NotImplementedError()
+        internalData.updateBlocking {
+            mapNotNull { if (it.containerId == containerId) null else it }.toSet()
+        }
     }
 
     suspend fun clear() {
-        Timber.tag(TAG).i("clear()")
-        throw NotImplementedError()
+        Timber.tag(TAG).w("Clearing recovery certificate data.")
+        internalData.updateBlocking {
+            Timber.tag(TAG).v("Deleting: %s", this)
+            emptySet()
+        }
     }
 
     companion object {
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/recovery/core/RecoveryCertificateWrapper.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/recovery/core/RecoveryCertificateWrapper.kt
index 214930872..ffa672e22 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/recovery/core/RecoveryCertificateWrapper.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/recovery/core/RecoveryCertificateWrapper.kt
@@ -14,7 +14,6 @@ data class RecoveryCertificateWrapper(
     val isUpdatingData = container.isUpdatingData
 
     val testCertificate: RecoveryCertificate? by lazy {
-        // TODO
-        container.toRecoveryCertificate(null)
+        container.toRecoveryCertificate()
     }
 }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/recovery/core/storage/RecoveryCertificateContainer.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/recovery/core/storage/RecoveryCertificateContainer.kt
index 580d5a548..6bec0fa62 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/recovery/core/storage/RecoveryCertificateContainer.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/recovery/core/storage/RecoveryCertificateContainer.kt
@@ -10,7 +10,6 @@ import de.rki.coronawarnapp.covidcertificate.common.repository.CertificateRepoCo
 import de.rki.coronawarnapp.covidcertificate.common.repository.RecoveryCertificateContainerId
 import de.rki.coronawarnapp.covidcertificate.recovery.core.RecoveryCertificate
 import de.rki.coronawarnapp.covidcertificate.recovery.core.qrcode.RecoveryCertificateQRCode
-import de.rki.coronawarnapp.covidcertificate.valueset.valuesets.TestCertificateValueSets
 import org.joda.time.Instant
 import org.joda.time.LocalDate
 import java.util.Locale
@@ -34,13 +33,12 @@ data class RecoveryCertificateContainer(
     }
 
     override val containerId: RecoveryCertificateContainerId
-        get() = RecoveryCertificateContainerId(data.identifier)
+        get() = RecoveryCertificateContainerId(certificateData.certificate.recovery.uniqueCertificateIdentifier)
 
     val certificateId: String
         get() = certificateData.certificate.recovery.uniqueCertificateIdentifier
 
     fun toRecoveryCertificate(
-        valueSet: TestCertificateValueSets?,
         userLocale: Locale = Locale.getDefault(),
     ): RecoveryCertificate {
         val header = certificateData.header
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/recovery/core/storage/RecoveryCertificateStorage.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/recovery/core/storage/RecoveryCertificateStorage.kt
new file mode 100644
index 000000000..d12aee6d4
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/recovery/core/storage/RecoveryCertificateStorage.kt
@@ -0,0 +1,56 @@
+package de.rki.coronawarnapp.covidcertificate.recovery.core.storage
+
+import android.content.Context
+import androidx.core.content.edit
+import com.google.gson.Gson
+import com.google.gson.reflect.TypeToken
+import de.rki.coronawarnapp.util.di.AppContext
+import de.rki.coronawarnapp.util.serialization.BaseGson
+import timber.log.Timber
+import javax.inject.Inject
+import javax.inject.Singleton
+
+@Singleton
+class RecoveryCertificateStorage @Inject constructor(
+    @AppContext val context: Context,
+    @BaseGson val gson: Gson,
+) {
+
+    private val prefs by lazy {
+        context.getSharedPreferences("recovery_localdata", Context.MODE_PRIVATE)
+    }
+
+    var recoveryCertificates: Set<StoredRecoveryCertificateData>
+        get() {
+            Timber.tag(TAG).d("recoveryCertificates - load()")
+            return gson.fromJson<Set<StoredRecoveryCertificateData>>(
+                prefs.getString(PKEY_RECOVERY_CERT, null) ?: return emptySet(), TYPE_TOKEN
+            ).onEach {
+                Timber.tag(TAG).v("recovery certificate loaded: %s", it)
+            }
+        }
+        set(value) {
+            Timber.tag(TAG).d("recoveryCertificates - save(%s)", value)
+            prefs.edit {
+                if (value.isEmpty()) {
+                    remove(PKEY_RECOVERY_CERT)
+                } else {
+                    putString(
+                        PKEY_RECOVERY_CERT,
+                        gson.toJson(
+                            value.onEach { data ->
+                                Timber.tag(TAG).v("Storing recovery certificate %s", data.recoveryCertificateQrCode)
+                            },
+                            TYPE_TOKEN
+                        )
+                    )
+                }
+            }
+        }
+
+    companion object {
+        private const val TAG = "RecoveryCertStorage"
+        private const val PKEY_RECOVERY_CERT = "recovery.certificate"
+        private val TYPE_TOKEN = object : TypeToken<Set<StoredRecoveryCertificateData>>() {}.type
+    }
+}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/recovery/core/storage/StoredRecoveryCertificateData.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/recovery/core/storage/StoredRecoveryCertificateData.kt
index b9408f277..f4f3dddf2 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/recovery/core/storage/StoredRecoveryCertificateData.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/recovery/core/storage/StoredRecoveryCertificateData.kt
@@ -1,16 +1,11 @@
 package de.rki.coronawarnapp.covidcertificate.recovery.core.storage
 
 import com.google.gson.annotations.SerializedName
-import org.joda.time.Instant
 
 data class StoredRecoveryCertificateData(
-    @SerializedName("identifier") override val identifier: String,
-    @SerializedName("registeredAt") override val registeredAt: Instant,
     @SerializedName("recoveryCertificateQrCode") override val recoveryCertificateQrCode: String?,
 ) : StoredRecoveryCertificate
 
 interface StoredRecoveryCertificate {
-    val identifier: String
-    val registeredAt: Instant
     val recoveryCertificateQrCode: String?
 }
-- 
GitLab