package org.commcare.print;
import android.annotation.TargetApi;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import android.os.CancellationSignal;
import android.os.ParcelFileDescriptor;
import android.print.PageRange;
import android.print.PrintAttributes;
import android.print.PrintDocumentAdapter;
import android.print.PrintJob;
import android.print.PrintJobInfo;
import android.print.PrintManager;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import org.commcare.CommCareApplication;
import org.commcare.android.javarosa.IntentCallout;
import org.commcare.dalvik.R;
import org.commcare.preferences.CommCarePreferences;
import org.commcare.suite.model.Detail;
import org.commcare.print.TemplatePrinterTask.PopulateListener;
import org.commcare.utils.CompoundIntentList;
import org.commcare.utils.FileUtil;
import org.commcare.utils.TemplatePrinterUtils;
import org.javarosa.core.reference.InvalidReferenceException;
import org.javarosa.core.reference.ReferenceManager;
import org.javarosa.core.services.locale.Localization;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import java.io.File;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
/**
* Intermediate activity which populates an HTML template with data and then prints it
*
* @author Richard Lu
* @author amstone
*/
public class TemplatePrinterActivity extends Activity implements PopulateListener {
private static final String KEY_TEMPLATE_STYLE = "PRINT_TEMPLATE_STYLE";
private static final String TEMPLATE_STYLE_HTML = "TEMPLATE_HTML";
private static final String TEMPLATE_STYLE_ZPL = "TEMPLATE_ZPL";
public static final String PRINT_TEMPLATE_REF_STRING = "cc:print_template_reference";
private static final int CALLOUT_ZPL = 1;
/**
* The path to the temp file location that is written to in TemplatePrinterTask, and then
* read back from in doHtmlPrint()
*/
private String outputPath;
/**
* Unique name to use for the print job name
*/
private String jobName;
private PrintJob printJob;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_template_printer);
String printStyle = this.getIntent().getExtras().getString(KEY_TEMPLATE_STYLE);
if (printStyle == null) {
if(CompoundIntentList.isIntentCompound(this.getIntent())) {
//Only zebra print jobs can compound
//TODO: This still isn't a particularly great way for us to be differentiating
printStyle = TEMPLATE_STYLE_ZPL;
} else {
printStyle = TEMPLATE_STYLE_HTML;
}
}
if (TEMPLATE_STYLE_ZPL.equals(printStyle)) {
//Since this and the callout activities are raised as "dialog" activities, they will
//recreate themselves on rotation. If we detect that we need to not "re-kick-off" the
//activity, it will result in duplicate activities.
if (savedInstanceState != null) {
return;
} else {
doZebraPrint();
return;
}
}
String pathToTemplateFile = getTemplateFilePathOrThrowError(getIntent().getExtras());
// A null return code from the path retriever means that it is displaying a message;
if (pathToTemplateFile == null) {
return;
}
// Check to make sure we are targeting API 19 or above, which is where print is supported
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
showErrorDialog(Localization.get("print.not.supported"));
return;
}
this.outputPath = CommCareApplication.instance().getTempFilePath() + ".html";
preparePrintDoc(pathToTemplateFile);
}
private void doZebraPrint() {
Intent i = new Intent("com.dimagi.android.zebraprinttool.action.PrintTemplate");
if (CompoundIntentList.isIntentCompound(this.getIntent())) {
ArrayList<String> keys = this.getIntent().getStringArrayListExtra(
CompoundIntentList.EXTRA_COMPOUND_DATA_INDICES);
i.putStringArrayListExtra("zebra:bundle_list", keys);
for(String key : keys) {
Bundle b = this.getIntent().getBundleExtra(key);
prepareZebraBundleFromFile(b);
i.putExtra(key, b);
}
} else {
String key = "single_job";
Bundle intentBundle = this.getIntent().getExtras();
prepareZebraBundleFromFile(intentBundle);
i.putExtra(key, intentBundle);
ArrayList<String> extraKeys = new ArrayList<>();
extraKeys.add(key);
i.putStringArrayListExtra("zebra:bundle_list", extraKeys);
}
this.startActivityForResult(i, CALLOUT_ZPL);
}
private void prepareZebraBundleFromFile(Bundle bundle) {
String path = getTemplateFilePathOrThrowError(bundle);
File destFile = new File(path);
bundle.putString("zebra:template_file_path", destFile.getAbsolutePath());
}
/**
* Retrieve a valid path that is the template file to be used during printing, or
* display an error message to the user. If a message is displayed, the method will
* return null and the activity should not continue attempting to print
*/
private String getTemplateFilePathOrThrowError(Bundle data) {
// Check to make sure key-value data has been passed with the intent
if (data == null) {
showErrorDialog(Localization.get("no.print.data"));
return null;
}
// Check if a doc location is coming in from the Intent
// Will return a reference of format jr://... if it has been set
String path = data.getString(PRINT_TEMPLATE_REF_STRING);
if (path != null && !path.equals(Detail.PRINT_TEMPLATE_PROVIDED_VIA_GLOBAL_SETTING)) {
try {
path = ReferenceManager.instance().DeriveReference(path).getLocalURI();
return path;
} catch (InvalidReferenceException e) {
showErrorDialog(Localization.get("template.invalid"));
return null;
}
} else {
// Try to use the document location that was set in Settings menu
path = CommCarePreferences.getGlobalTemplatePath();
if (path == null) {
showErrorDialog(Localization.get("missing.template.file"));
}
return path;
}
}
private void preparePrintDoc(String inputPath) {
generateJobName(inputPath);
String extension = FileUtil.getExtension(inputPath);
File templateFile = new File(inputPath);
if (extension.equalsIgnoreCase("html") && templateFile.exists()) {
new TemplatePrinterTask(
templateFile,
outputPath,
getIntent().getExtras(),
this
).execute();
} else {
showErrorDialog(Localization.get("template.invalid"));
}
}
/**
* Generate a unique name for this print job, using the name of the template file and the date
*
* @param templateFilename the path to the given template file
*/
private void generateJobName(String templateFilename) {
String inputWithoutExtension = templateFilename.substring(0,
templateFilename.lastIndexOf('.'));
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
String dateString = sdf.format(new Date());
jobName = inputWithoutExtension + "_" + dateString;
}
/**
* Called when TemplatePrinterTask finishes, with a result code indicating what happened. If
* a .html file of the filled-out template has been created and saved successfully, proceeds
* with printing. Otherwise, displays the appropriate error message.
*/
@Override
public void onPopulationFinished(TemplatePrinterTask.PrintTaskResult result, String problemString) {
switch (result) {
case SUCCESS:
doHtmlPrint();
break;
case IO_ERROR:
showErrorDialog(Localization.get("print.io.error"));
break;
case VALIDATION_ERROR_MUSTACHE:
showErrorDialog(Localization.get("template.malformed.mustache", new String[]{problemString}));
break;
case VALIDATION_ERROR_CHEVRON:
showErrorDialog(Localization.get("template.malformed.chevron", new String[]{problemString}));
}
}
private void showErrorDialog(String message) {
TemplatePrinterUtils.showAlertDialog(this, getString(R.string.error_occured), message, true);
}
/**
* Prepares a WebView of the html document generated by this activity, which can then be
* printed by the Android print framework
*
* Source: https://developer.android.com/training/printing/html-docs.html
*/
private void doHtmlPrint() {
WebView webView = new WebView(this);
WebSettings settings = webView.getSettings();
settings.setJavaScriptEnabled(true);
webView.setWebViewClient(new WebViewClient() {
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
return false;
}
@Override
public void onPageFinished(WebView view, String url) {
createWebPrintJob(view);
}
});
try {
String finalHtml = TemplatePrinterUtils.readStringFromFile(outputPath);
webView.loadDataWithBaseURL(null, finalHtml, "text/HTML", "UTF-8", null);
} catch (IOException e) {
showErrorDialog(Localization.get("print.io.error"));
}
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if(requestCode == CALLOUT_ZPL) {
Intent response = new Intent();
if(resultCode != Activity.RESULT_CANCELED) {
response.putExtra(IntentCallout.INTENT_RESULT_VALUE, "");
}
this.setResult(resultCode, response);
this.finish();
}
}
/**
* Starts a print job for the given WebView
*
* Source: https://developer.android.com/training/printing/html-docs.html
*
* @param v the WebView to be printed
*/
@TargetApi(Build.VERSION_CODES.KITKAT)
private void createWebPrintJob(WebView v) {
// Get a PrintManager instance
PrintManager printManager = (PrintManager) getSystemService(Context.PRINT_SERVICE);
// Get a print adapter instance
PrintDocumentAdapter printAdapter = new PrintDocumentAdapterWrapper(this, v.createPrintDocumentAdapter());
// Create a print job with name and adapter instance
printJob = printManager.print(jobName, printAdapter, new PrintAttributes.Builder().build());
}
/**
* A wrapper for the default print document adapter generated for a web view, to enable
* implementation of a custom callback when onFinish() is called
*
* Source: http://stackoverflow.com/questions/30742051/android-printmanager-get-callback
*/
@TargetApi(Build.VERSION_CODES.KITKAT)
class PrintDocumentAdapterWrapper extends PrintDocumentAdapter {
private final PrintDocumentAdapter delegate;
private final Activity activity;
public PrintDocumentAdapterWrapper(Activity activity, PrintDocumentAdapter adapter) {
super();
this.activity = activity;
this.delegate = adapter;
}
@Override
public void onLayout(PrintAttributes oldAttributes, PrintAttributes newAttributes,
CancellationSignal cancellationSignal,
LayoutResultCallback callback, Bundle extras) {
delegate.onLayout(oldAttributes, newAttributes, cancellationSignal, callback, extras);
}
@Override
public void onWrite(PageRange[] pages, ParcelFileDescriptor destination,
CancellationSignal cancellationSignal,
WriteResultCallback callback) {
delegate.onWrite(pages, destination, cancellationSignal, callback);
}
@Override
public void onFinish() {
delegate.onFinish();
String printDialogTitle = Localization.get("print.dialog.title");
String msg = "";
boolean printInitiated = false;
switch (printJob.getInfo().getState()) {
case PrintJobInfo.STATE_BLOCKED:
msg = Localization.get("printjob.blocked");
break;
case PrintJobInfo.STATE_CANCELED:
msg = Localization.get("printjob.not.started");
break;
case PrintJobInfo.STATE_COMPLETED:
msg = Localization.get("printing.done");
printInitiated = true;
break;
case PrintJobInfo.STATE_FAILED:
msg = Localization.get("print.error");
break;
case PrintJobInfo.STATE_CREATED:
case PrintJobInfo.STATE_QUEUED:
case PrintJobInfo.STATE_STARTED:
msg = Localization.get("printjob.started");
printInitiated = true;
}
TemplatePrinterUtils.showPrintStatusDialog(activity, printDialogTitle, msg,
printInitiated);
}
}
}