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

Commit

Permalink
Enable export of all certificates of a CWA user (EXPOSUREAPP-13018) (#…
Browse files Browse the repository at this point in the history
…5250)

* Add ToDo

* Implement new Toolbar in person overview (EXPOSUREAPP-13260) (#5253)

* Toolbar icons

* Connect icons

* Tooltip

* Update Corona-Warn-App/src/main/res/values-de/covid_certificate_strings.xml

Co-authored-by: KathrinLuecke <91470863+KathrinLuecke@users.noreply.github.com>

Co-authored-by: Alex Paulescu <alex.paulescu@gmail.com>
Co-authored-by: KathrinLuecke <91470863+KathrinLuecke@users.noreply.github.com>

* Adapt release not strings. (#5262)

* All certificates PDF export info screen (EXPOSUREAPP-13261) (#5258)

* Add pdf export info screen.

* Update Corona-Warn-App/src/main/res/values-de/covid_certificate_strings.xml

Co-authored-by: KathrinLuecke <91470863+KathrinLuecke@users.noreply.github.com>

* Use new texts.

Co-authored-by: KathrinLuecke <91470863+KathrinLuecke@users.noreply.github.com>

* DCC export filter and sorting (EXPOSUREAPP-13311) (#5266)

* filter

* filter

* test

* Certs preview screen (EXPOSUREAPP-13262) (#5260)

* Toolbar icons

* Connect icons

* Tooltip

* Web Approach

* Inject data

* Lint

* Lint

* Common fields

* `use`

* rename

* Zoom

* unique template classes

* Adjust style

* Page

* Toolbar

* Remove `EU` flag

* toolbar actions

* Update CertificateTemplate.kt

* Print

* px

* Progress

* Size

* Enhancing

* Fix tests

* Avoid multi inflate

* Name

* `-`

* catch error

* Share PDF

* Update DccExportAllOverviewFragment.kt

* move to vm

* Refactoring

* Docs

* Log

* Progress / error messages

* Catch errors

* lint

* Alignment

* Update Corona-Warn-App/src/main/res/values-de/covid_certificate_strings.xml

Co-authored-by: KathrinLuecke <91470863+KathrinLuecke@users.noreply.github.com>

* Update Corona-Warn-App/src/main/res/values-de/covid_certificate_strings.xml

Co-authored-by: KathrinLuecke <91470863+KathrinLuecke@users.noreply.github.com>

* Format

* Main dispatcher

* create pdf

* Delete file

* Formatting

* Clean up

* lint

* Cleaning

* bind filteriing

* lint

* Avoid crash

Co-authored-by: Nikolaus Schauersberger <nikolaus.schauersberger@gmail.com>
Co-authored-by: KathrinLuecke <91470863+KathrinLuecke@users.noreply.github.com>

* Unify flag SVG paths (EXPOSUREAPP-13310) (#5267)

* Toolbar icons

* Connect icons

* Tooltip

* Web Approach

* Inject data

* Lint

* Lint

* Common fields

* `use`

* rename

* Zoom

* unique template classes

* Adjust style

* Page

* Toolbar

* Remove `EU` flag

* toolbar actions

* Update CertificateTemplate.kt

* Print

* px

* Progress

* Size

* Enhancing

* Fix tests

* Avoid multi inflate

* Name

* `-`

* catch error

* Share PDF

* Update DccExportAllOverviewFragment.kt

* move to vm

* Refactoring

* Docs

* Log

* Progress / error messages

* Catch errors

* lint

* Alignment

* Update Corona-Warn-App/src/main/res/values-de/covid_certificate_strings.xml

Co-authored-by: KathrinLuecke <91470863+KathrinLuecke@users.noreply.github.com>

* Update Corona-Warn-App/src/main/res/values-de/covid_certificate_strings.xml

Co-authored-by: KathrinLuecke <91470863+KathrinLuecke@users.noreply.github.com>

* Format

* Main dispatcher

* create pdf

* Delete file

* Formatting

* Clean up

* lint

* Cleaning

* bind filteriing

* lint

* Avoid crash

* unify flag svg paths

Co-authored-by: Mohamed Metwalli <mohamed.metwalli@sap.com>
Co-authored-by: KathrinLuecke <91470863+KathrinLuecke@users.noreply.github.com>

Co-authored-by: Alex Paulescu <alex.paulescu@gmail.com>
Co-authored-by: KathrinLuecke <91470863+KathrinLuecke@users.noreply.github.com>
Co-authored-by: Chilja Gossow <49635654+chiljamgossow@users.noreply.github.com>
Co-authored-by: Nikolaus Schauersberger <nikolaus.schauersberger@gmail.com>
  • Loading branch information
5 people authored Jun 8, 2022
1 parent 84241fe commit 84d5d4d
Show file tree
Hide file tree
Showing 48 changed files with 11,083 additions and 40 deletions.
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

0 comments on commit 84d5d4d

Please sign in to comment.