Printing a Form from ODK Collect using XSLT

Hi there, I'm attempting to modify ODK Collect so that I can print out Form Instances directly from a mobile device to a Wi-Fi printer.

I'm new to programming and so it's been a bit of a jump in the deep end but I have got somewhere at last. However I need some pointers!

I've managed to write an app that will take a hardcoded XML file in res/raw and use XSLT to transform it into HTML which Google's WebView then prints out successfully.

What I want to do now is replace the hardcoded XML file with the desired XML of the Form Instance that the user wants to print out.

But I'm stuck!

I know there are XML files for each Form Instance but I can't work out how they are linked back to the Form Instance that you see when using the UI.

When looking at a particular Form Instance in the UI would it be possible to retrieve a URI for its XML file?

If so then I could simply add this print button to my Form UI and have my print code fetch the relevant XML file and print it upon pressing the 'Print' button.

See screenshot:

52b75a741bccdac5d6f8d890ed58bad4a7a02204_2_281x500

Any help much appreciated!

Cheers

The hierarchy activity has access to the global FormController through Collect.getInstance().getFormController(). You can call getInstanceFile to get a reference to the XML document that represents the filled instance.

2 Likes

Fantastic, thanks @LN!

Can't wait to have a go at it tomorrow! :grinning:

I'll be so chuffed if I can get this to work...

1 Like

Ok, so I'm having a bit of trouble here, I keep getting an 'Unhandled exception: java.io.IOException´ with whichever method I attempt to convert the File to a String.

At the moment I'm trying:

    File InstanceXmlFile = Collect.getInstance().getFormController().getInstanceFile();

    String strXML = FileUtils.readFileToString(InstanceXmlFile, "utf-8");

@LN What am I doing wrong?

Ah ok, I think I had to add a catch statement?

Seem to be sorted now, although I can't quite get the end result to work. Hmm, back to it...

Wow, I have success! :grin:

1 Like

Just some proof :grin:

Screenshot_20200214-164902 (1)

Not a very elegant solution as we still need to update the XSLT for each new Form Template that we want to print out, but it serves our needs at the minute!

If anyone is interested here is the main PrintFormInstanceActivity code:

package org.odk.collect.android.activities;

import android.app.Activity;
import android.content.Context;
import android.os.Bundle;
import android.print.PrintAttributes;
import android.print.PrintDocumentAdapter;
import android.print.PrintJob;
import android.print.PrintManager;
import android.util.Log;
import android.view.Window;
import android.webkit.WebView;
import android.webkit.WebViewClient;


import org.apache.commons.io.FileUtils;
import org.odk.collect.android.R;
import org.odk.collect.android.application.Collect;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringWriter;
import java.io.UnsupportedEncodingException;

import javax.xml.transform.Result;
import javax.xml.transform.Source;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.TransformerFactoryConfigurationError;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;

public class PrintFormInstanceActivity extends Activity {
    public static final String TAG = "YOUR-TAG-NAME";
    private WebView mWebView;
    /**
     * Called when the activity is first created.
     */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        getWindow().requestFeature(Window.FEATURE_PROGRESS);

        WebView webview = new WebView(this);
        setContentView(webview);

        //Reading XSLT
        String strXSLT = GetStyleSheet(R.raw.xsltfile);

        //Reading XML

        File InstanceXmlFile = Collect.getInstance().getFormController().getInstanceFile();

        String strXML = null;
        try {
            strXML = FileUtils.readFileToString(InstanceXmlFile, "utf-8");
        } catch (IOException e) {
            e.printStackTrace();
        }


        //Transform ...
        String html = StaticTransform(strXSLT, strXML);


        //Loading the above transformed XSLT in to Webview...
        webview.loadData(html, "text/html", null);


        WebView webView = new WebView(this);
        webView.setWebViewClient(new WebViewClient() {

            public boolean shouldOverrideUrlLoading(WebView view, String url) {
                return false;
            }

            @Override
            public void onPageFinished(WebView view, String url) {
                Log.i(TAG, "page finished loading " + url);
                createWebPrintJob(view);
                mWebView = null;
            }
        });

        webView.loadDataWithBaseURL(null, html, "text/HTML", "UTF-8", null);

        mWebView = webView;


    }

    /** Google's WebView print code **/

    private void createWebPrintJob(WebView webView) {

        // Get a PrintManager instance
        PrintManager printManager = (PrintManager) this
                .getSystemService(Context.PRINT_SERVICE);

        String jobName = getString(R.string.app_name) + " Document";

        // Get a print adapter instance
        PrintDocumentAdapter printAdapter = webView.createPrintDocumentAdapter(jobName);

        // Set default page size to A4
        PrintAttributes.Builder builder = new PrintAttributes.Builder();
        builder.setMediaSize( PrintAttributes.MediaSize.ISO_A4);

        // Create a print job with name and adapter instance
        PrintJob printJob = printManager.print(jobName, printAdapter, builder.build());


        // Save the job object for later status checking
        // printJobs.add(printJob);
    }


    /**
     * Transform XML and XSLT to HTML string
     **/
    public static String StaticTransform(String strXsl, String strXml) {
        String html = "";

        try {

            InputStream ds = null;
            ds = new ByteArrayInputStream(strXml.getBytes("UTF-8"));

            Source xmlSource = new StreamSource(ds);

            InputStream xs = new ByteArrayInputStream(strXsl.getBytes("UTF-8"));
            Source xsltSource = new StreamSource(xs);

            StringWriter writer = new StringWriter();
            Result result = new StreamResult(writer);
            TransformerFactory tFactory = TransformerFactory.newInstance();
            Transformer transformer = tFactory.newTransformer(xsltSource);
            transformer.transform(xmlSource, result);

            html = writer.toString();

            ds.close();
            xs.close();

            xmlSource = null;
            xsltSource = null;

        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        } catch (TransformerConfigurationException e) {
            e.printStackTrace();
        } catch (TransformerFactoryConfigurationError e) {
            e.printStackTrace();
        } catch (TransformerException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (Exception e) {
            e.printStackTrace();
        }

        return html;
    }

    /**
     * Read XSLT file from res/raw...
     **/
    private String GetStyleSheet(int fileId) {
        String strXsl = null;

        InputStream raw = getResources().openRawResource(fileId);
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        int size = 0;
        // Read the entire resource into a local byte buffer.
        byte[] buffer = new byte[1024];
        try {
            while ((size = raw.read(buffer, 0, 1024)) >= 0) {
                outputStream.write(buffer, 0, size);
            }
            raw.close();

            strXsl = outputStream.toString();

            Log.v("Log", "xsl ==> " + strXsl);
        } catch (IOException e) {
            e.printStackTrace();
        }

        return strXsl;

    }



}

Was a bit hesitant about sharing the code as I know it's pretty horrendous, but it's my first shot at coding so I guess I don't even know how bad it really is :sweat_smile:

P.s. there maaay be some memory leak issues. I tried to narrow it down with the profiler but all I could see was something related to the GNSS stuff, so not sure if I did something or it's always like that?

2 Likes

Well done!

Yes, creating a generic XSLT script that'll handle an arbitrary XForm is a bit trickier, since you have then to extract the question info from the XML form def (or pull it out of Collects's internal data structures). But this is still a great accomplishment, and may inspire others to also take up the challenge... :wink:

1 Like

Thanks!

Yeah I thought I'd leave the catch-all scenario for another time haha.

It will definitely be harder to design something that makes all those possible nested groups look half-decent in all cases.

Would be an interesting challenge though!