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 0000000000000000000000000000000000000000..23e47d2dccb9fea350592ce75b235fc0fc1b38ad --- /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 94f5fdb6343397a55e848fa36eb0e326e1193179..bcc3498e7164db6b860efbdb2d762bb7023f3dc9 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 214930872941688332f7e2eb87dfa20ef613fde1..ffa672e226be6c5d8b27b4e04f02660bf5db71e2 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 580d5a54827c16efbc47ec3218879df9b767a0ff..6bec0fa62b56c9250b7d0385ea8bba687e237931 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 0000000000000000000000000000000000000000..d12aee6d4770601f938a929f4cfa14a05dae9665 --- /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 b9408f2774ff56971b671c3f61bc267b00d213dc..f4f3dddf2ac4d988ea7c5a6d2ca855c23d517874 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? }