Search code examples
javaandroidxmlprintingandroid-4.4-kitkat

How do I specify and add a custom printer in an Android app?


I'm creating an app for Android. Part of the desired app functionality is that the user can select a special printer (let's just call it Transfer Printer) which will pass on the document-to-be-printed to a process running on an external server.

What steps do I need to take to add a custom printer to the list of printers in the Android print panel, accessible from the Print option of the Overflow menu?

It is desirable to use the existing Android print panel functionality rather than, for example, an additional Share option in the App Selector because of user experience considerations; it won't be intuitive to the user to click Share rather than Print for the desired functionality.

Prior research

There is an existing similar question which has gathered little interest since it was posted some time ago. The asker has identified the PrintManager class as a lead but I believe that the PrintService class is likely to be more fruitful:

A print service is responsible for discovering printers, adding discovered printers, removing added printers, and updating added printers.

The same page details Declaration and Configuration of the print service. I've done so as below.

Attempted Execution

Declaration and Configuration

In AndroidManifest.xml:

...
<application
    ... >
    ...
    <service
        android:name=".TransferPrintService"
        android:permission="android.permission.BIND_PRINT_SERVICE"
        android:enabled="true"
        android:exported="false">
        <intent-filter>
            <action android:name="android.printservice.PrintService" />
        </intent-filter>
        <meta-data
            android:name="android.printservice"
            android:resource="@xml/transfer_print_service" />
    </service>
</application>

Meta-data

It's unclear to me exactly where the meta-data is supposed to be specified. From SERVICE_META_DATA section of the PrintService page:

This meta-data must reference a XML resource containing a print-service tag.

In res/xml/transfer_print_service.xml:

<print-service
    android:label="TransferPrintService"
    android:vendor="Company Ltd." />

TransferPrintService Class

This creates a custom PrinterDiscoverySession. My goal at this stage is to just get a printer appearing in the print panel and work from there.

public class TransferPrintService extends PrintService {

    public TransferPrintService() {
    }

    @Override
    public void onPrintJobQueued(PrintJob printJob) {
        printJob.start();
        printJob.complete();
    }

    @Override
    public PrinterDiscoverySession onCreatePrinterDiscoverySession() {
        return new TransferPrinterDiscoverySession(this);
    }

    @Override
    public void onRequestCancelPrintJob(PrintJob printJob) {
    }
}

The service is started in a BroadcastReceiver on an ACTION_BOOT_COMPLETED intent.

TransferPrinterDiscoverySession Class

This actually creates the custom printer.

public class TransferPrinterDiscoverySession extends PrinterDiscoverySession {
    private transferPrintService printService;
    private static final String PRINTER = "Transfer Printer";

    public transferPrinterDiscoverySession(TransferPrintService printService) {
        this.printService = printService;
    }

    @Override
    public void onStartPrinterDiscovery(List<PrinterId> printerList) {
        PrinterId id = printService.generatePrinterId(PRINTER);
        PrinterInfo.Builder builder =
                new PrinterInfo.Builder(id, PRINTER, PrinterInfo.STATUS_IDLE);
        PrinterInfo info = builder.build();
        List<PrinterInfo> infos = new ArrayList<>();
        infos.add(info);
        addPrinters(infos);
    }

    @Override
    public void onStopPrinterDiscovery() {
    }

    @Override
    public void onValidatePrinters(List<PrinterId> printerIds) {
    }

    @Override
    public void onStartPrinterStateTracking(PrinterId printerId) {
        PrinterInfo.Builder builder = new PrinterInfo.Builder(printerId,
                PRINTER, PrinterInfo.STATUS_IDLE);
        PrinterCapabilitiesInfo.Builder capBuilder =
                new PrinterCapabilitiesInfo.Builder(printerId);

        capBuilder.addMediaSize(PrintAttributes.MediaSize.ISO_A4, true);
        capBuilder.addResolution(new PrintAttributes.Resolution(
                "Default", "Default", 360, 360), true);
        capBuilder.setColorModes(PrintAttributes.COLOR_MODE_COLOR
                + PrintAttributes.COLOR_MODE_MONOCHROME,
                PrintAttributes.COLOR_MODE_COLOR);
        capBuilder.setMinMargins(PrintAttributes.Margins.NO_MARGINS);

        PrinterCapabilitiesInfo caps = capBuilder.build();
        builder.setCapabilities(caps);
        PrinterInfo info = builder.build();
        List<PrinterInfo> infos = new ArrayList<PrinterInfo>();
        infos.add(info);
        addPrinters(infos);
    }

    @Override
    public void onStopPrinterStateTracking(PrinterId printerId) {
    }

    @Override
    public void onDestroy() {
    }
}

Major Concerns

  • This doesn't produce an additional printer option.
  • Is the arrangement of documents correct? Specifically, having the <print-service> tag in a separate XML document under res? Trying to place the tag anywhere in the AndroidManfiest.xml document produces IDE errors.
  • How do I call into the TransferPrintService? As an example, suppose I'm in Chrome, I open the Overflow menu, and select Print... Which PrintService is invoked? How do I make sure it's mine?
  • Am I on completely the wrong track here?

Solution

  • The trick I was missing was actually enabling the Print Service via the Android Settings menu. Actually doing this wasn't as straightforward as I would have hoped as the device manufacturer had removed the setting from the menu. It should be right under Accessibility in the System section of the menu.

    I ended up installing the Cloud Print app by Google, which gave me access to the Print Service settings temporarily (to enable the Cloud Print service). Once in here I noticed that my own service was, in fact, present.

    For posterity: To avoid un-installing and re-installing Cloud Print every time you want to change the Print Service settings, use the following SQLite3 commands, either with adb shell or from Terminal Emulator (or similar):

    sqlite3 data/data/com.android.providers.settings/databases/settings.db
    

    You should now have access to the Settings database and be using the SQLite3 command line shell. The settings of interest are located in the secure table and are enabled_print_services and enabled_on_first_boot_system_print_services. You can check if these settings already exist by using:

    .dump secure
    

    If they don't, then use the following commands:

    INSERT INTO secure VALUES(<id>, 'enabled_on_first_boot_system_print_services', 'com.companyname.appservice/com.companyname.appservice.TransferPrintService');
    INSERT INTO secure VALUES(<id>, 'enabled_print_services', 'com.companyname.appservice/com.companyname.appservice.TransferPrintService');
    

    You should, of course, replace 'com.companyname.appservice' with your own package and 'TransferPrintService' with your own print service. If these setting names do already exist, and your print service isn't listed, then you'll need to UPDATE instead of INSERT INTO:

    UPDATE secure SET value = '<existing print services>:<new print service>' WHERE name = 'enabled_on_first_boot_system_print_services';
    UPDATE secure SET value = '<existing print services>:<new print service>' WHERE name = 'enabled_print_services';
    

    You'll need to make sure to include any existing print services as part of the UPDATE command; listed print services are separated by a colon ":".

    Reboot the device to apply the updates to the settings database.