Skip to content
Snippets Groups Projects
Unverified Commit 4e200f2b authored by chris-cwa's avatar chris-cwa Committed by GitHub
Browse files

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: default avatarMohamed Metwalli <mohamed.metwalli@sap.com>
parent fb84dd6b
No related branches found
No related tags found
No related merge requests found
package de.rki.coronawarnapp.covidcertificate.recovery.core
class DuplicateRecoveryCertificateException(
message: String
) : IllegalArgumentException(message)
package de.rki.coronawarnapp.covidcertificate.recovery.core 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.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.common.repository.RecoveryCertificateContainerId
import de.rki.coronawarnapp.covidcertificate.recovery.core.qrcode.RecoveryCertificateQRCode import de.rki.coronawarnapp.covidcertificate.recovery.core.qrcode.RecoveryCertificateQRCode
import de.rki.coronawarnapp.covidcertificate.recovery.core.storage.RecoveryCertificateContainer 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.AppScope
import de.rki.coronawarnapp.util.coroutine.DispatcherProvider import de.rki.coronawarnapp.util.coroutine.DispatcherProvider
import de.rki.coronawarnapp.util.flow.HotDataFlow
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow 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 timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
...@@ -17,26 +28,82 @@ import javax.inject.Singleton ...@@ -17,26 +28,82 @@ import javax.inject.Singleton
@Singleton @Singleton
class RecoveryCertificateRepository @Inject constructor( class RecoveryCertificateRepository @Inject constructor(
@AppScope private val appScope: CoroutineScope, @AppScope private val appScope: CoroutineScope,
private val dispatcherProvider: DispatcherProvider, dispatcherProvider: DispatcherProvider,
private val qrCodeExtractor: DccQrCodeExtractor, 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 { suspend fun registerCertificate(qrCode: RecoveryCertificateQRCode): RecoveryCertificateContainer {
Timber.tag(TAG).d("registerCertificate(qrCode=%s)", qrCode) 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) Timber.tag(TAG).d("deleteCertificate(containerId=%s)", containerId)
throw NotImplementedError() internalData.updateBlocking {
mapNotNull { if (it.containerId == containerId) null else it }.toSet()
}
} }
suspend fun clear() { suspend fun clear() {
Timber.tag(TAG).i("clear()") Timber.tag(TAG).w("Clearing recovery certificate data.")
throw NotImplementedError() internalData.updateBlocking {
Timber.tag(TAG).v("Deleting: %s", this)
emptySet()
}
} }
companion object { companion object {
......
...@@ -14,7 +14,6 @@ data class RecoveryCertificateWrapper( ...@@ -14,7 +14,6 @@ data class RecoveryCertificateWrapper(
val isUpdatingData = container.isUpdatingData val isUpdatingData = container.isUpdatingData
val testCertificate: RecoveryCertificate? by lazy { val testCertificate: RecoveryCertificate? by lazy {
// TODO container.toRecoveryCertificate()
container.toRecoveryCertificate(null)
} }
} }
...@@ -10,7 +10,6 @@ import de.rki.coronawarnapp.covidcertificate.common.repository.CertificateRepoCo ...@@ -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.common.repository.RecoveryCertificateContainerId
import de.rki.coronawarnapp.covidcertificate.recovery.core.RecoveryCertificate import de.rki.coronawarnapp.covidcertificate.recovery.core.RecoveryCertificate
import de.rki.coronawarnapp.covidcertificate.recovery.core.qrcode.RecoveryCertificateQRCode 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.Instant
import org.joda.time.LocalDate import org.joda.time.LocalDate
import java.util.Locale import java.util.Locale
...@@ -34,13 +33,12 @@ data class RecoveryCertificateContainer( ...@@ -34,13 +33,12 @@ data class RecoveryCertificateContainer(
} }
override val containerId: RecoveryCertificateContainerId override val containerId: RecoveryCertificateContainerId
get() = RecoveryCertificateContainerId(data.identifier) get() = RecoveryCertificateContainerId(certificateData.certificate.recovery.uniqueCertificateIdentifier)
val certificateId: String val certificateId: String
get() = certificateData.certificate.recovery.uniqueCertificateIdentifier get() = certificateData.certificate.recovery.uniqueCertificateIdentifier
fun toRecoveryCertificate( fun toRecoveryCertificate(
valueSet: TestCertificateValueSets?,
userLocale: Locale = Locale.getDefault(), userLocale: Locale = Locale.getDefault(),
): RecoveryCertificate { ): RecoveryCertificate {
val header = certificateData.header val header = certificateData.header
......
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
}
}
package de.rki.coronawarnapp.covidcertificate.recovery.core.storage package de.rki.coronawarnapp.covidcertificate.recovery.core.storage
import com.google.gson.annotations.SerializedName import com.google.gson.annotations.SerializedName
import org.joda.time.Instant
data class StoredRecoveryCertificateData( data class StoredRecoveryCertificateData(
@SerializedName("identifier") override val identifier: String,
@SerializedName("registeredAt") override val registeredAt: Instant,
@SerializedName("recoveryCertificateQrCode") override val recoveryCertificateQrCode: String?, @SerializedName("recoveryCertificateQrCode") override val recoveryCertificateQrCode: String?,
) : StoredRecoveryCertificate ) : StoredRecoveryCertificate
interface StoredRecoveryCertificate { interface StoredRecoveryCertificate {
val identifier: String
val registeredAt: Instant
val recoveryCertificateQrCode: String? val recoveryCertificateQrCode: String?
} }
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment