Android Gets A Print Framework

Written by: on November 14, 2013

Have you ever tried adding the option to print a document to your Android application?

Prior to the release of Android 4.4, KitKat, there were no Android APIs specifically for printing. In order to implement printing, you had to rely on a third party solution or implement protocols such as SMB/CIFS to directly communicate with the printer via Wi-Fi, Bluetooth or USB. Since most of us won’t be attempting to directly communicate with a printer, relying on a solution which already exists would be the best choice. In fact, it is essentially what the new additions to the framework allow you to do, albeit a bit more cleanly and reliably.

Cloud Printing

Google Cloud Print service first debuted in April, 2010. GCP is a web-based printer and print job management system which enables printing from any web-connected device to any printer setup with the GCP service. A user can add just about any printer to their GCP service, even older, non-‘Cloud Ready’ printers, provided that the printer is sharable from a web-connected computer with Google Chrome installed. Using GCP, in this author’s opinion, is the easiest way to allow printing from an Android device running an OS prior to 4.4. GCP is also the primary print service that you should expect to be on any device with Android 4.4, so go ahead and check it out here.

Printing Prior to KitKat

The Google Cloud Print service provides a set of APIs for Google account authentication, submitting print jobs and receiving print jobs. You can check out the developer guide for Google Cloud Print here. I won’t go into any implementation details because this approach is a bit long winded and gains you little. You still rely on the user having a properly set up GCP account with printers attached, but you avoid relying on either of the two approaches I am about to discuss.

Google Cloud Print App

Cloud Print by Paulo Fernandes was the first Android app to leverage the GCP APIs and enable printing from an Android device with the GCP service. Two years later, in June of 2013, Google released it’s own version called Google Cloud Print. Since Google Cloud Print will likely be more popular going forward, I will use that as an example, though I suspect many cloud print apps could be leveraged in a similar fashion. First, we need to use the PackageManager to ensure the device your app is running on has Google Cloud Print installed using private boolean hasGoogleCloudPrint() {

    PackageManager pm = getPackageManager();
    try {
        pm.getPackageInfo(“com.google.android.apps.cloudprint”, 0);
        return true;
    } catch(PackageManager.NameNotFoundException e) {
        return false;
    }
}

If the device does have the Google Cloud Print app installed, we can go ahead and use an intent to start printing:

private void printViaGoogleCloudPrintApp(Uri content) {
    Intent printIntent = new Intent(Intent.ACTION_SEND);
    printIntent.setPackage(“com.google.android.apps.cloudprint”);
    printIntent.setType(“image/*”);
    printIntent.putExtra(Intent.EXTRA_TITLE, “Print Test Title”);
    printIntent.putExtra(Intent.EXTRA_STREAM, uri);

    startActivity(printIntent);
}

If the device does not have Google Cloud Print installed, you can either send the user to the Play Store to download it:

private void startPlayStore() {
    Uri uri = Uri.parse(“market://details?id=com.google.android.apps.cloudprint”);
    Intent marketIntent = new Intent(Intent.ACTION_VIEW, uri);

    startActivity(marketIntent);
}

or you can open up the Google Cloud Print web interface in a WebView. It is omitted for brevity, but you can find a complete example here.

Printing With KitKat

With the addition of the new print APIs, printing has become a bit easier and more reliable. Here is a breakdown of the API updates organized by package:

android.support.v4.print

  • PrintHelper – Useful for creating print jobs to print bitmaps

android.print

  • PrintDocumentAdapter – base class for providing custom print documents
  • PrintManager – allows access to print jobs for this application and facilitates printing with a PrintDocumentAdapter

android.print.pdf

  • PrintedPdfDocument – helper for creating PDFs based on the specified PrintAttributes

android.webkit

  • WebView.createPrintDocumentAdapter – creates a PrintDocumentAdapter for printing the contents of the WebView

android.printservice

  • Contains classes for implementing your own PrintService

Let’s start with android.printservice. This package contains classes relevant to implementing your own print service. A print service abstracts communication with the actual printer (or another intermediary such as GCP). The Google Cloud Print app installed on KitKat devices implements this service for creating print jobs on GCP. The actual implementation of a print service is beyond the scope of this article, but it is useful to be aware of PrintService, as at least one is required to do any actual printing with the other classes mentioned later on. (Note: Most KitKat devices come with Google Cloud Print, Chrome, Drive, Gallery and Quickoffice pre-installed, all of which provide print services)

Next, let’s look at PrintHelper which is located in android.support.v4.print. You might think that, since this class is in the support package, you can leverage it to print on older devices; unfortunately this does not seem to be the case. The PrintHelper class has a static method called systemSupportsPrint(). From my tests, I found that this method returns true only on KitKat devices. Also, trying to call the printBitmap() method on any device where systemSupportsPrint() returns false does nothing. So, for now we are left to wonder why the PrintHelper class was included in the support package, given that it seems to have no use, at least not currently.

Anyway, the PrintHelper is useful for printing on KitKat devices and provides a simple and straightforward method for printing Bitmaps.

  • You can specify a scale mode, FILL or FIT, which behave as expected. FILL will fill the print area with the given image, maintaining aspect ratio but potentially cropping. FIT will fill either the horizontal or vertical area depending on your printed document size and the size of your image, once again maintaining aspect ratio but with no cropping, so there will be some whitespace unless your image is the same ratio as your printed document.
  • You can also specify the color mode as either COLOR or MONOCHROME.

Another useful addition is the method createPrintDocumentAdapter() for WebView. This method, used in conjunction with PrintManager, allows for easy printing of the contents of your WebView.

PrintDocumentAdapter printDocumentAdapter = mWebView.createPrintDocumentAdapter();
PrintManager printManger = (PrintManager) getSystemService(Context.PRINT_SERVICE);
printManager.print(“Test Print Job”, printDocumentAdapter, null);

There are a few caveats to take note of when printing the contents of a WebView:

  • The user will not be able to specify page ranges, so the entire contents of the WebView will be printed.
  • The PrintDocumentAdapter created by the WebView will provide the contents of the WebView as they currently are. If the page isn’t finished loading, or if you start loading another page, you may not get the expected result, or the PrintDocumentAdapter could fail altogether.
  • An instance of a WebView can only process one print job at a time.

The PrintManager only has two public methods: getPrintJobs() and print(). getPrintJobs() will return a List of PrintJob objects which were started by your application. A PrintJob object is useful for checking the status and potentially canceling or restarting the job. The print method will create a PrintJob with the specified title, PrintDocumentAdapter and PrintAttributes. PrintAttributes allow you to specify a color mode, MediaSize, Margins and Resolution, although it is important to note that how these attributes are handled is entirely up to the PrintService the user selects to handle the job. It is quite possible that some of the attributes are either irrelevant to the specific service or will simply be ignored. One example of this is that the ‘Save as PDF’ print service ignores color mode.

The last class we are going to take a look at is the PrintDocumentAdapter. The PrintDocumentAdapter is the base class to extend for creating custom layouts and content. Although you can easily print anything you can draw to a canvas using the PrintHelper and Bitmap classes, the PrintDocumentAdapter gives you much more flexibility in terms of layout and attributes. When implementing a PrintDocumentAdapter, there are two optional methods and two required methods. The optional methods, onStart() and onFinish(), are basically opportunities for your implementation to allocate and release any additional resources it may require. The first required method we will take a look at is onLayout(), which is called anytime the PrintAttributes change. The main goal of this method is to create a PrintDocumentInfo object describing your content and to call one of the three methods on the LayoutResultCallback object. It is also a good place to create your PrintedPdfDocument object, so you can pass in the PrintAttributes.

@Override
public void onLayout(PrintAttributes oldAttributes, PrintAttributes newAttributes,
        CancellationSignal cancellationSignal, LayoutResultCallback callback, Bundle extras) {
    // If the CancellationSignal indicates the print job has been cancelled then call
    // onLayoutCancelled and return as there is nothing else to do
    if (cancellationSignal.isCanceled()) {
        callback.onLayoutCanceled();
        return;
    }

    // If you are using PrintedPdfDocument helper class this is a good place to instantiate it
    mPdfDocument = new PrintedPdfDocument(getContext(), newAttributes);

    // Next you will want to determine the number of pages your document will require based
    // on the specified attributes and your content, whatever that may be
    int pageCount = determinePageCount(newAttributes);

    // Finally you will need to create a PrintDocumentInfo object which specifies the content
    // type and page count. The content type value can either by DOCUMENT or PHOTO
    // and will potentially be used by the print service but could also be ignored depending on
    // what print service is being used. The Android developer docs mention that the print
    // service may use the document type to determine the paper quality and/or other quality
    // settings though again, it is entirely up to the print service how this value is used.
    PrintDocumentInfo info = new PrintDocumentInfo.Builder(“Document Title”)
            .setContentType(PrintDocumentInfo.CONTENT_TYPE_DOCUMENT)
            .setPageCount(pageCount)
            .build();

    // If anything else went wrong that prevented us from making our determinations etc
    if (somethingWentWrong) {
        callback.onLayoutFailed(“Something went wrong”);
    } else {
        // Once everything is complete we make a call to onLayoutFinished with the
        // PrintDocumentInfo we created and we specify whether the layout changed.
        // In this example we always specify true, that the layout has changed, but in
        // general this may not be the case. This method, onLayout, may be called
        // multiple times and in some cases the changes to the PrintAttributes may not
        // affect your overall layout - this might be a case where you would want to return
        // false, it really depends on your content and layout
        callback.onLayoutFinished(info, true);
    }
}

The other required method is onWrite(). This method is called when there are changes which will need to be written to the PDF file specified by the file descriptor. It is important to note that this method (as well as all of the PrintDocumentAdapter methods) is called on the main thread; it is mentioned here because this method in particular may be a good candidate for backgrounding, especially given that we will be doing some file IO. The main goal of this method will be draw content to the PDF, write the PDF to file and call one of the callback methods.

@Override
public void onWrite(PageRange[] pageRanges, ParcelFileDescriptor destination,
        CancellationSignal cancellationSignal, WriteResultCallback callback) {

    // First we need to loop over all of our pages and draw their content
    for(int index = 0; index < mTotalPages; index ++) {
        // If at any point while drawing the pages we see that the cancellation signal has
        // been triggered we make the onWriteCancelled method call, clean-up the
        // PrintedPdfDocument and return
        if (cancellationSignal.isCanceled()) {
            callback.onWriteCancelled();
            mPdfDocument.close();
            mPdfDocument = null;
            return;
        }

        // It is possible that the requested pageRanges do not include every page, in this
        // case we need to potentially skip some pages
        if (!pageRangesContainPage(pageRanges, index)) {
            continue;
        }

        // Use the PrintedPdfDocument object to start a page
        PdfDocument.Page page = mPdfDocument.startPage(index);
        Canvas canvas = page.getCanvas();

        // … draw some stuff to your page’s canvas…

        // … you may also want to write some PageRange information here so you can
        // determine all of the ranges you supplied pages for when you go to call
        // onWriteFinished

        mPdfDocument.finishPage(page);
    }

    // Next we need to write out the updated PrintedPdfDocument to the specified
    // destination
    try {
        mPdfDocument.writeTo(new FileOutputStream(destination.getFileDescriptor()));
    } catch(IOException e) {
        callback.onWriteFailed(e.toString());
        return;
    } finally {
        // Whether or not the write is successful we need to clean-up the
        // PrintedPdfDocument object
        mPdfDocument.close();
        mPdfDocument = null;
    }

    // It is possible that we did not supply pages for all of the requested PageRanges so we
    // need to pass onWriteFinished an accurate array of the PageRanges. In most cases it
    // would be sufficient to pass back the same PageRanges array object that was passed
    // in to us, pageRanges.
    PageRange[] completedPages = getCompletedPageRanges();
    callback.onWriteFinished(completedPages);
}

Overall, the addition of the print framework to Android does not add much new functionality, but it does allow significantly easier and more reliable printing through apps which provide a print service, several of which come pre-installed on KitKat devices.

This piece is the fourth of eight in our KitKat Developer’s Guide. Check back later this week for new updates or follow us on twitter.

Michael Wally

Michael Wally

Michael Wally is a Mobile Software Engineer at Double Encore. He specializes in Android Development and being really, really tall.
Article


Add your voice to the discussion: