Skip to content

Commit

Permalink
Configure PDF Generation (#3278)
Browse files Browse the repository at this point in the history
* Create PdfLauncherFragment

* Create PdfLauncherViewModel

* Add htmlBinaryId in QuestionnaireConfig

* Add PDF Generation as ApplicationWorkflow

* Add notes to CHANGELOG.md

* Add default html title to strings.xml

* Add specific pdf file naming

* Fix PDF Generation to close launcher after PDF is generated

* Move context to constructor

* Have pdfGenerator as class-level property

* Move Pdf classes to quest

* Add unit tests

* Create documentation for PDF Generation

* Moved tests to quest

* Only define pdfGenerator from outside for test
  • Loading branch information
FikriMilano authored Jul 29, 2024
1 parent 7e7cffa commit 5fcc4a0
Show file tree
Hide file tree
Showing 13 changed files with 682 additions and 74 deletions.
6 changes: 4 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [1.1.1] - 2024-05-20

### Added
- Added a new class (PdfGenerator) for generating PDF documents from HTML content using Android's WebView and PrintManager
- Introduced a new class (HtmlPopulator) to populate HTML templates with data from a Questionnaire Response
- Added the in-app PDF Generation feature
1. Added a new class (PdfGenerator) for generating PDF documents from HTML content using Android's WebView and PrintManager
2. Introduced a new class (HtmlPopulator) to populate HTML templates with data from a Questionnaire Response
3. Implemented functionality to launch PDF generation using a configuration setup

## [1.1.0] - 2024-02-15

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ data class QuestionnaireConfig(
val managingEntityRelationshipCode: String? = null,
val uniqueIdAssignment: UniqueIdAssignmentConfig? = null,
val linkIds: List<LinkIdConfig>? = null,
val htmlBinaryId: String? = null,
val htmlTitle: String? = null,
) : java.io.Serializable, Parcelable {

fun interpolate(computedValuesMap: Map<String, Any>) =
Expand Down Expand Up @@ -98,6 +100,8 @@ data class QuestionnaireConfig(
uniqueIdAssignment?.copy(linkId = uniqueIdAssignment.linkId.interpolate(computedValuesMap)),
linkIds = linkIds?.onEach { it.linkId.interpolate(computedValuesMap) },
saveButtonText = saveButtonText?.interpolate(computedValuesMap),
htmlBinaryId = htmlBinaryId?.interpolate(computedValuesMap),
htmlTitle = htmlTitle?.interpolate(computedValuesMap),
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,4 +59,7 @@ enum class ApplicationWorkflow {

/** A workflow that launches location selector widget * */
LAUNCH_LOCATION_SELECTOR,

/** A workflow to launch pdf generation */
LAUNCH_PDF_GENERATION,
}
1 change: 1 addition & 0 deletions android/engine/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -186,4 +186,5 @@
<string name="data_migration_started">Started data migration from version %1$d</string>
<string name="data_migration_completed">Application data migrated to version %1$d</string>
<string name="no_data">No data set</string>
<string name="default_html_title">file</string>
</resources>

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -14,23 +14,31 @@
* limitations under the License.
*/

package org.smartregister.fhircore.engine.pdf
package org.smartregister.fhircore.quest.ui.pdf

import android.content.Context
import android.print.PrintAttributes
import android.print.PrintManager
import android.webkit.WebResourceRequest
import android.webkit.WebView
import android.webkit.WebViewClient
import org.jetbrains.annotations.VisibleForTesting

/**
* PdfGenerator creates PDF files from HTML content using Android's WebView and PrintManager. Must
* be initialized on the Main thread.
*
* @param context Application context for initializing WebView and PrintManager.
* @param webView Optional WebView for testing purposes.
* @param webView WebView instance for loading HTML content (Visible for testing).
*/
class PdfGenerator(context: Context, private val webView: WebView = WebView(context)) {
class PdfGenerator(
private val context: Context,
@VisibleForTesting private val webView: WebView = WebView(context),
) {

private val printManager = context.getSystemService(Context.PRINT_SERVICE) as PrintManager
private var mWebView: WebView? = null
private val printManager: PrintManager =
context.getSystemService(Context.PRINT_SERVICE) as PrintManager

/**
* Generates a PDF file from the provided HTML content.
Expand All @@ -47,10 +55,26 @@ class PdfGenerator(context: Context, private val webView: WebView = WebView(cont
*
* @param html The HTML content to be converted into a PDF.
* @param pdfTitle The title of the PDF document.
* @param onPdfPrinted Callback to be invoked when the PDF is printed.
*/
fun generatePdfWithHtml(html: String, pdfTitle: String) {
fun generatePdfWithHtml(html: String, pdfTitle: String, onPdfPrinted: () -> Unit) {
webView.webViewClient =
object : WebViewClient() {

override fun shouldOverrideUrlLoading(view: WebView, request: WebResourceRequest) = false

override fun onPageFinished(view: WebView, url: String) {
printPdf(view, pdfTitle)
mWebView = null
onPdfPrinted.invoke()
}
}
webView.loadDataWithBaseURL(null, html, "text/HTML", "UTF-8", null)
val printAdapter = webView.createPrintDocumentAdapter(pdfTitle)
mWebView = webView
}

private fun printPdf(view: WebView, pdfTitle: String) {
val printAdapter = view.createPrintDocumentAdapter(pdfTitle)
printManager.print(
pdfTitle,
printAdapter,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
/*
* Copyright 2021-2024 Ona Systems, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.smartregister.fhircore.quest.ui.pdf

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.core.os.bundleOf
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.hl7.fhir.r4.model.Binary
import org.hl7.fhir.r4.model.QuestionnaireResponse
import org.jetbrains.annotations.VisibleForTesting
import org.smartregister.fhircore.engine.R
import org.smartregister.fhircore.engine.configuration.QuestionnaireConfig
import org.smartregister.fhircore.engine.pdf.HtmlPopulator
import org.smartregister.fhircore.engine.util.extension.decodeJson
import org.smartregister.fhircore.engine.util.extension.extractLogicalIdUuid

/**
* A fragment for generating and displaying a PDF based on a questionnaire response.
*
* This fragment uses the provided [QuestionnaireConfig] to retrieve a questionnaire response,
* populate an HTML template with the response data, and generate a PDF.
*/
@AndroidEntryPoint
class PdfLauncherFragment : DialogFragment() {

private val pdfLauncherViewModel by viewModels<PdfLauncherViewModel>()

@VisibleForTesting lateinit var pdfGenerator: PdfGenerator

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (!this::pdfGenerator.isInitialized) pdfGenerator = PdfGenerator(requireContext())

val questionnaireConfig = getQuestionnaireConfig()

val questionnaireId = questionnaireConfig.id.extractLogicalIdUuid()
val subjectId = questionnaireConfig.resourceIdentifier!!.extractLogicalIdUuid()
val subjectType = questionnaireConfig.resourceType!!
val htmlBinaryId = questionnaireConfig.htmlBinaryId!!.extractLogicalIdUuid()
val htmlTitle = questionnaireConfig.htmlTitle ?: getString(R.string.default_html_title)

lifecycleScope.launch(Dispatchers.IO) {
val questionnaireResponse =
pdfLauncherViewModel.retrieveQuestionnaireResponse(
questionnaireId,
subjectId,
subjectType,
)
val htmlBinary = pdfLauncherViewModel.retrieveBinary(htmlBinaryId)
generatePdf(questionnaireResponse, htmlBinary, htmlTitle)
}
}

/**
* Retrieves and decodes the questionnaire configuration from the fragment arguments.
*
* @return the decoded [QuestionnaireConfig] object.
* @throws IllegalArgumentException if the questionnaire config is not found in arguments.
*/
private fun getQuestionnaireConfig(): QuestionnaireConfig {
val jsonConfig =
requireArguments().getString(EXTRA_QUESTIONNAIRE_CONFIG_KEY)
?: throw IllegalArgumentException("Questionnaire config not found in arguments")
return jsonConfig.decodeJson()
}

/**
* Generates a PDF using the provided questionnaire response and HTML template.
*
* @param questionnaireResponse the [QuestionnaireResponse] object containing user responses.
* @param htmlBinary the [Binary] object containing the HTML template.
* @param htmlTitle the title to be used for the generated PDF.
*/
private suspend fun generatePdf(
questionnaireResponse: QuestionnaireResponse?,
htmlBinary: Binary?,
htmlTitle: String,
) {
if (questionnaireResponse == null || htmlBinary == null) {
dismiss()
return
}

val htmlContent = htmlBinary.content.decodeToString()
val populatedHtml = HtmlPopulator(questionnaireResponse).populateHtml(htmlContent)

withContext(Dispatchers.Main) {
pdfGenerator.generatePdfWithHtml(populatedHtml, htmlTitle) { dismiss() }
}
}

companion object {

/**
* Launches the PdfLauncherFragment.
*
* This method creates a new instance of PdfLauncherFragment, sets the provided questionnaire
* configuration JSON as an argument, and displays the fragment.
*
* @param appCompatActivity The activity from which the fragment is launched.
* @param questionnaireConfigJson The JSON string representing the questionnaire configuration.
*/
fun launch(appCompatActivity: AppCompatActivity, questionnaireConfigJson: String) {
PdfLauncherFragment()
.apply { arguments = bundleOf(EXTRA_QUESTIONNAIRE_CONFIG_KEY to questionnaireConfigJson) }
.show(appCompatActivity.supportFragmentManager, PdfLauncherFragment::class.java.simpleName)
}

@VisibleForTesting const val EXTRA_QUESTIONNAIRE_CONFIG_KEY = "questionnaire_config"
}
}
Loading

0 comments on commit 5fcc4a0

Please sign in to comment.