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