Skip to content
This repository has been archived by the owner on Jun 20, 2023. It is now read-only.

Enable export of all certificates of a CWA user (EXPOSUREAPP-13018) #5250

Merged
merged 12 commits into from
Jun 8, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -125,8 +125,8 @@ class TestCertificateDetailsFragmentTest : BaseUITest() {
get() = null
override val containerId: TestCertificateContainerId
get() = TestCertificateContainerId("identifier")
override val targetName: String
get() = "Schneider, Andrea"
override val targetDisease: String
get() = "Covid 19"
override val testType: String
get() = "SARS-CoV-2-Test"
override val testResult: String
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import com.google.gson.GsonBuilder
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import de.rki.coronawarnapp.appconfig.AppConfigProvider
import de.rki.coronawarnapp.diagnosiskeys.download.DownloadDiagnosisKeysSettings
import de.rki.coronawarnapp.diagnosiskeys.download.DownloadDiagnosisKeysTask
import de.rki.coronawarnapp.diagnosiskeys.storage.KeyCacheRepository
Expand Down Expand Up @@ -38,13 +37,12 @@ import timber.log.Timber
import java.io.File

class TestRiskLevelCalculationFragmentCWAViewModel @AssistedInject constructor(
dispatcherProvider: DispatcherProvider,
@Assisted private val handle: SavedStateHandle,
@Assisted private val exampleArg: String?,
@AppContext private val context: Context, // App context
private val dispatcherProvider: DispatcherProvider,
private val taskController: TaskController,
private val keyCacheRepository: KeyCacheRepository,
appConfigProvider: AppConfigProvider,
private val riskLevelStorage: RiskLevelStorage,
private val testSettings: TestSettings,
private val timeStamper: TimeStamper,
Expand Down
2,926 changes: 2,926 additions & 0 deletions Corona-Warn-App/src/main/assets/template/de_rc_v4.1.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1,579 changes: 1,579 additions & 0 deletions Corona-Warn-App/src/main/assets/template/de_tc_v4.1.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
484 changes: 484 additions & 0 deletions Corona-Warn-App/src/main/assets/template/de_vc_v4.1.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2,949 changes: 2,949 additions & 0 deletions Corona-Warn-App/src/main/assets/template/rc_v4.1.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1,527 changes: 1,527 additions & 0 deletions Corona-Warn-App/src/main/assets/template/tc_v4.1.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
497 changes: 497 additions & 0 deletions Corona-Warn-App/src/main/assets/template/vc_v4.1.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
48 changes: 48 additions & 0 deletions Corona-Warn-App/src/main/java/android/print/FilePrinter.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package android.print

import android.os.CancellationSignal
import android.os.ParcelFileDescriptor
import android.print.PrintDocumentAdapter.LayoutResultCallback
import android.print.PrintDocumentAdapter.WriteResultCallback
import kotlinx.coroutines.suspendCancellableCoroutine
import java.io.File
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException

/**
* Print [PrintDocumentAdapter] content to a file
* Note: this file has to be in `android.print` as workaround to be able to create
* [LayoutResultCallback] and [WriteResultCallback]
*/
class FilePrinter(private val attributes: PrintAttributes) {
suspend fun print(printAdapter: PrintDocumentAdapter, path: File, fileName: String) =
suspendCancellableCoroutine<Unit> { cont ->
printAdapter.onLayout(
null,
attributes,
null,
object : LayoutResultCallback() {
override fun onLayoutFinished(info: PrintDocumentInfo, changed: Boolean) {
printAdapter.onWrite(
arrayOf(PageRange.ALL_PAGES),
getOutputFileDescriptor(path, fileName),
CancellationSignal(),
object : WriteResultCallback() {
override fun onWriteFinished(pages: Array<PageRange>) = cont.resume(Unit)

override fun onWriteFailed(error: CharSequence?) =
cont.resumeWithException(Exception(error.toString()))
}
)
}
},
null
)
}

private fun getOutputFileDescriptor(path: File, fileName: String): ParcelFileDescriptor {
if (!path.exists()) path.mkdir()
val file = File(path, fileName).also { it.createNewFile() }
return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_WRITE)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import dagger.Module
import dagger.android.ContributesAndroidInjector
import de.rki.coronawarnapp.covidcertificate.boosterinfodetails.BoosterInfoDetailsFragment
import de.rki.coronawarnapp.covidcertificate.boosterinfodetails.BoosterInfoDetailsFragmentModule
import de.rki.coronawarnapp.covidcertificate.pdf.ui.exportAll.DccExportAllOverviewFragment
import de.rki.coronawarnapp.covidcertificate.pdf.ui.exportAll.DccExportAllOverviewModule
import de.rki.coronawarnapp.covidcertificate.pdf.ui.poster.CertificatePosterFragment
import de.rki.coronawarnapp.covidcertificate.pdf.ui.poster.CertificatePosterModule
import de.rki.coronawarnapp.covidcertificate.person.ui.admission.AdmissionScenarioFragmentModule
Expand Down Expand Up @@ -62,4 +64,7 @@ abstract class DigitalCovidCertificateUIModule {

@ContributesAndroidInjector(modules = [AdmissionScenarioFragmentModule::class])
abstract fun admissionScenariosFragment(): AdmissionScenariosFragment

@ContributesAndroidInjector(modules = [DccExportAllOverviewModule::class])
abstract fun dccExportAllOverviewFragment(): DccExportAllOverviewFragment
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import de.rki.coronawarnapp.covidcertificate.vaccination.core.VaccinationCertifi
import de.rki.coronawarnapp.covidcertificate.vaccination.core.repository.VaccinationCertificateRepository
import de.rki.coronawarnapp.covidcertificate.vaccination.core.repository.VaccinationCertificateWrapper
import de.rki.coronawarnapp.util.coroutine.AppScope
import de.rki.coronawarnapp.util.coroutine.DispatcherProvider
import de.rki.coronawarnapp.util.flow.shareLatest
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
Expand All @@ -26,8 +25,7 @@ class CertificateProvider @Inject constructor(
vcRepo: VaccinationCertificateRepository,
tcRepo: TestCertificateRepository,
rcRepo: RecoveryCertificateRepository,
@AppScope private val appScope: CoroutineScope,
dispatcherProvider: DispatcherProvider
@AppScope private val appScope: CoroutineScope
) {

/**
Expand All @@ -47,7 +45,7 @@ class CertificateProvider @Inject constructor(
vcRepo.certificates
) { recoveries, tests, vaccinations ->
CertificateContainer(recoveries, tests, vaccinations)
}.shareLatest(scope = appScope + dispatcherProvider.IO)
}.shareLatest(scope = appScope)

/**
* Finds a [CwaCovidCertificate] by [CertificateContainerId]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ interface CwaCovidCertificate : Recyclable {
val certificateIssuer: String
val certificateCountry: String
val qrCodeHash: String
val targetDisease: String

/**
* `ci` field
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package de.rki.coronawarnapp.covidcertificate.pdf.core

import de.rki.coronawarnapp.covidcertificate.common.certificate.CwaCovidCertificate
import de.rki.coronawarnapp.covidcertificate.recovery.core.RecoveryCertificate
import de.rki.coronawarnapp.covidcertificate.test.core.TestCertificate
import de.rki.coronawarnapp.covidcertificate.vaccination.core.VaccinationCertificate
import de.rki.coronawarnapp.util.TimeAndDateExtensions.toLocalDateUserTz
import de.rki.coronawarnapp.util.toJavaInstant
import de.rki.coronawarnapp.util.toJavaTime
import java.time.Duration
import java.time.Instant

internal fun Collection<CwaCovidCertificate>.filterAndSortForExport(
nowUtc: Instant
): List<CwaCovidCertificate> {
return filter {
it.isIncludedInExport(nowUtc)
}.sort()
}

internal fun CwaCovidCertificate.isIncludedInExport(nowUtc: Instant): Boolean {
return state.isIncludedInExport && when (this) {
is TestCertificate -> this.isRecent(nowUtc)
else -> true
}
}

internal fun List<CwaCovidCertificate>.sort(): List<CwaCovidCertificate> = sortedWith(
compareBy(
{ it.fullName },
{
when (it) {
is TestCertificate -> it.sampleCollectedAt?.toLocalDateUserTz()?.toJavaTime()
is VaccinationCertificate -> it.vaccinatedOn?.toJavaTime()
is RecoveryCertificate -> it.testedPositiveOn?.toJavaTime()
else -> null
}
}
)
)

internal fun TestCertificate.isRecent(nowUtc: Instant): Boolean {
return this.sampleCollectedAt?.let {
Duration.between(it.toJavaInstant(), nowUtc) <= Duration.ofHours(72)
} ?: false
}

internal val CwaCovidCertificate.State.isIncludedInExport: Boolean
get() = this is CwaCovidCertificate.State.Valid ||
this is CwaCovidCertificate.State.Expired ||
this is CwaCovidCertificate.State.ExpiringSoon ||
this is CwaCovidCertificate.State.Invalid
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ class TestCertificateDrawHelper @Inject constructor(@OpenSansTypeFace font: Type
with(canvas) {
save()
rotate(180f, PdfGenerator.A4_WIDTH / 2f, PdfGenerator.A4_HEIGHT / 2f)
drawTextIntoRectangle(certificate.targetName, paint, TextArea(476.20f, 489.40f, 112.75f))
drawTextIntoRectangle(certificate.targetDisease, paint, TextArea(476.20f, 489.40f, 112.75f))
drawTextIntoRectangle(certificate.testType, paint, TextArea(476.20f, 515.79f, 112.75f))
drawTextIntoRectangle(certificate.testName ?: "", paint, TextArea(314.27f, 581.76f, 236.89f))
drawTextIntoRectangle(certificate.testNameAndManufacturer ?: "", paint, TextArea(314.27f, 628.54f, 236.89f))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package de.rki.coronawarnapp.covidcertificate.pdf.ui

import android.os.Bundle
import android.view.View
import androidx.fragment.app.Fragment
import com.google.android.material.transition.MaterialSharedAxis
import de.rki.coronawarnapp.R
import de.rki.coronawarnapp.databinding.ExportAllCertsPdfInfoFragmentBinding
import de.rki.coronawarnapp.ui.view.onOffsetChange
import de.rki.coronawarnapp.util.ui.doNavigate
import de.rki.coronawarnapp.util.ui.popBackStack
import de.rki.coronawarnapp.util.ui.viewBinding

class ExportAllCertsPdfInfoFragment : Fragment(R.layout.export_all_certs_pdf_info_fragment) {

private val binding: ExportAllCertsPdfInfoFragmentBinding by viewBinding()

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

enterTransition = MaterialSharedAxis(MaterialSharedAxis.Z, true)
returnTransition = MaterialSharedAxis(MaterialSharedAxis.Z, false)
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.apply {
toolbar.setNavigationOnClickListener { popBackStack() }
nextButton.setOnClickListener {
doNavigate(
ExportAllCertsPdfInfoFragmentDirections
.actionExportAllCertsPdfInfoFragmentToDccExportAllOverviewFragment()
)
}
appBarLayout.onOffsetChange { _, subtitleAlpha ->
headerImage.alpha = subtitleAlpha
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
package de.rki.coronawarnapp.covidcertificate.pdf.ui.exportAll

import android.os.Bundle
import androidx.fragment.app.Fragment
import android.view.View
import android.webkit.WebSettings
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.core.view.isEmpty
import androidx.core.view.isVisible
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import de.rki.coronawarnapp.R
import de.rki.coronawarnapp.covidcertificate.pdf.ui.exportAll.DccExportAllOverviewViewModel.ExportResult
import de.rki.coronawarnapp.covidcertificate.pdf.ui.exportAll.DccExportAllOverviewViewModel.PDFResult
import de.rki.coronawarnapp.covidcertificate.pdf.ui.exportAll.DccExportAllOverviewViewModel.PrintResult
import de.rki.coronawarnapp.covidcertificate.pdf.ui.exportAll.DccExportAllOverviewViewModel.ShareResult
import de.rki.coronawarnapp.databinding.FragmentDccExportAllOverviewBinding
import de.rki.coronawarnapp.util.ExternalActionHelper.openUrl
import de.rki.coronawarnapp.util.di.AutoInject
import de.rki.coronawarnapp.util.ui.popBackStack
import de.rki.coronawarnapp.util.ui.viewBinding
import de.rki.coronawarnapp.util.viewmodel.CWAViewModelFactoryProvider
import de.rki.coronawarnapp.util.viewmodel.cwaViewModels
import java.time.Instant
import javax.inject.Inject

class DccExportAllOverviewFragment : Fragment(R.layout.fragment_dcc_export_all_overview), AutoInject {
private val binding by viewBinding<FragmentDccExportAllOverviewBinding>()
private val jobName get() = "CoronaWarn-" + Instant.now().toString()

@Inject
lateinit var viewModelFactory: CWAViewModelFactoryProvider.Factory
private val viewModel by cwaViewModels<DccExportAllOverviewViewModel> { viewModelFactory }

override fun onViewCreated(view: View, savedInstanceState: Bundle?) = with(binding) {
setupToolbar()
setupWebView()
cancelButton.setOnClickListener { popBackStack() }
with(viewModel) {
error.observe(viewLifecycleOwner) { showErrorDialog() }
exportResult.observe(viewLifecycleOwner) { handleExportResult(it) }
pdfString.observe(viewLifecycleOwner) { loadData(it) }
}
}

private fun FragmentDccExportAllOverviewBinding.loadData(
data: String
) {
webView.loadDataWithBaseURL(
null,
data,
"text/HTML",
Charsets.UTF_8.name(),
null
)
}

private fun FragmentDccExportAllOverviewBinding.setupToolbar() {
toolbar.setNavigationOnClickListener { popBackStack() }
toolbar.setOnMenuItemClickListener { item ->
when (item.itemId) {
R.id.action_print -> viewModel.print(webView.createPrintDocumentAdapter(jobName))
R.id.action_share -> viewModel.sharePDF()
}
true
}
}

private fun FragmentDccExportAllOverviewBinding.handleExportResult(
exportResult: ExportResult
) {
when (exportResult) {
is ShareResult -> exportResult.provider.intent(requireActivity()).also { startActivity(it) }
is PrintResult -> exportResult.print(requireActivity())
is PDFResult -> {
progressLayout.isVisible = false
if (toolbar.menu.isEmpty()) {
toolbar.inflateMenu(R.menu.menu_certificate_poster)
}
}
}
}

private fun showErrorDialog() {
MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.export_all_error_title)
.setMessage(R.string.export_all_error_message)
.setNeutralButton(R.string.export_all_error_faq) { _, _ ->
openUrl(R.string.certificate_export_error_dialog_faq_link)
}.setPositiveButton(android.R.string.ok) { _, _ -> }
.setOnDismissListener { popBackStack() }
.show()
}

private fun FragmentDccExportAllOverviewBinding.setupWebView() {
webView.apply {
with(settings) {
loadWithOverviewMode = true
useWideViewPort = true
builtInZoomControls = true
layoutAlgorithm = WebSettings.LayoutAlgorithm.NORMAL
displayZoomControls = false
}

webViewClient = object : WebViewClient() {
override fun onPageFinished(view: WebView?, url: String?) {
view?.let {
viewModel.createPDF(view.createPrintDocumentAdapter(jobName))
}
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package de.rki.coronawarnapp.covidcertificate.pdf.ui.exportAll

import dagger.Binds
import dagger.Module
import dagger.multibindings.IntoMap
import de.rki.coronawarnapp.util.viewmodel.CWAViewModel
import de.rki.coronawarnapp.util.viewmodel.CWAViewModelFactory
import de.rki.coronawarnapp.util.viewmodel.CWAViewModelKey

@Module
abstract class DccExportAllOverviewModule {
@Binds
@IntoMap
@CWAViewModelKey(DccExportAllOverviewViewModel::class)
abstract fun certificatePosterFragment(
factory: DccExportAllOverviewViewModel.Factory
): CWAViewModelFactory<out CWAViewModel>
}
Loading