1. 程式人生 > >Android系統列印服務外掛printservice開發

Android系統列印服務外掛printservice開發

一 簡介

從Android4.4開始,系統加入了列印相關的API,可以通過系統列印服務實現列印。對於需要使用列印功能的APP可參考官方教程接入列印服務。

這不是本文的內容,本文介紹印表機廠商如何使自己的印表機接入android,即編寫自己的列印外掛接入android列印服務。且僅介紹接入部分,與印表機如何連線不在本文範圍之內。

系統列印服務框架程式碼位於android.printservice包中。系統並沒有實現具體列印功能,需要印表機廠商製作外掛接入系統列印服務之後,自行實現。本文基於Android API Reference和以下兩個github上的開源專案研究而來。兩個參考專案如下:

通過實驗,初步實現了系統列印服務的接入(新增印表機)和模擬列印(將要列印的檔案輸出)。

二 主要類介紹

  • PrintDocument 表示待列印檔案,裡面存放有檔案的大小等資訊和檔案內容。

PrinterDiscoverySession 由 PrintService 建立,通過 onCreatePrinterDiscoverySession() 函式返回給系統。
PrintJob 由需要列印的APP建立,傳送給 PrintService 。
PrintDocument 存放在 PrintJob 裡面,被一同發過來。

三 列印服務外掛的工作流程

1 列印機發現過程

當用戶在設定裡開啟你的列印服務外掛和進入系統列印服務介面時,系統會呼叫 PrinterDiscoverySession 裡的 onStartPrinterDiscovery(List priorityList) 函式,通知你的外掛查詢印表機。具體查詢方式需要自己實現,可能是查詢USB介面,可能是搜尋網路。系統只管結果,你通過呼叫其父類的 addPrinters() 方法將印表機新增進去。印表機是放在List數組裡傳入。

當用戶離開上述列印外掛的介面時,系統會呼叫 onStopPrinterDiscovery() 函式,表示外掛可以停止尋找印表機了。

另外,在自定義的 addPrintersActivity 中,系統不會自動觸發印表機尋找過程,需要自行處理。

2 印表機選擇過程

當用戶通過一些有列印功能的APP呼叫系統列印服務時,如果選擇了你的外掛的印表機,那麼系統會呼叫 PrinterDiscoverySession 裡的 onStartPrinterStateTracking(PrinterId printerId) 方法。這裡系統主要希望得到印表機的 PrinterCapabilitiesInfo 和狀態,裡面包括印表機支援的紙張大小,以及色彩等詳細功能引數。

比如:如果沒有addMediaSize(PrintAttributes.MediaSize.ISO_A4, false),那麼使用者就不能選擇A4大小進行列印。後面的false表示是否設為預設值。

印表機有STATUS_BUSY、STATUS_IDLE、STATUS_UNAVAILABLE三種狀態,只有印表機處於STATUS_IDLE時,系統才允許使用該印表機。

印表機引數直接體現在系統列印服務介面,只可以選擇支援的引數,比如選擇紙張的大小為A4。

同樣,當用戶離開該介面或者選擇其他印表機時,系統會呼叫 onStopPrinterStateTracking(PrinterId printerId) 函式,來告訴外掛不用再提供印表機的資訊了。

3 列印過程

當用戶在剛剛的系統列印服務介面點選右上角的列印按鈕時,系統會呼叫印表機所屬的 PrintService 裡的 onPrintJobQueued(PrintJob printJob) 方法,外掛需要處理該 PrintJob 。首先需要通過 PrintJob.isQueued() 判斷,該PrintJob是否準備好列印,返回true代表可以列印。然後可以通過 PrintJob.getDocument() 獲得要列印的文件,這裡面的資料可以通過 PrintDocument.getData() 讀取。開始列印的時候,呼叫PrintJob.start()標記開始狀態。當列印成功時,呼叫 PrintJob.complete() 標記列印成功。或者列印失敗時,呼叫 PrintJob.fail( String) 標記失敗。

注意:一定要對PrintJob進行狀態標記,包括開始或者成功失敗。如果什麼都不標記,系統會一直在工作列提示該任務列印中,並且該印表機不可列印其他任務,處於準備中。如果任務結束不標記成功或者失敗,一段時間之後,系統會自動將該任務標記為失敗,並且印表機狀態自動變為不可用。

四 系統列印服務輸出的資料

通過編寫DEMO測試,發現android系統列印服務輸出的資料是pdf 1.4的格式,無論檔案內容是照片還是文件,都會統一轉換為pdf 1.4。

五 列印服務外掛初步編寫

1 列印服務外掛的宣告

一個列印服務和其他任何服務一樣,需要在AndroidManifest.xml裡宣告。但是它還必須處理action為android.printservice.PrintService的Intent。這個intent宣告失敗會導致系統忽略該列印服務。另外,一個列印服務必須請求android.permission.BIND_PRINT_SERVICE許可權,來保證只有系統能繫結(bind)它。宣告這個失敗會導致系統忽略這個列印服務。

一個列印服務可通過自定義設定頁面(setting activity)進行配置,該activity提供自定義設定功能。還有一個新增印表機的activity可以手動新增印表機,供應商名稱等等。系統負責在適當的時候啟動設定和新增印表機的activities。

一個列印服務在宣告的時候,要在mainfest裡提供一條 android:name=”android.printservice” 的 meta-data,這是指定上述activities的方式。

AndroidManifest.xml檔案如下:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.testprintservice">

    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.START_PRINT_SERVICE_CONFIG_ACTIVITY" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <service
            android:name=".MyPrintService"
            android:permission="android.permission.BIND_PRINT_SERVICE">
            <intent-filter>
                <action android:name="android.printservice.PrintService" />
            </intent-filter>

            <meta-data
                android:name="android.printservice"
                android:resource="@xml/printservice" />
        </service>

        <activity
            android:name=".SettingsActivity"
            android:exported="true"
            android:label="@string/settings_activity_label" />
        <activity
            android:name=".AddPrintersActivity"
            android:exported="true"
            android:label="@string/add_pritners_activity_label">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <activity
            android:name=".AdvancedPrintOptionsActivity"
            android:label="@string/advanced_print_options_activity_label"
            android:exported="true"></activity>
    </application>

</manifest>

android:resource="@xml/printservice"對應的檔案為printservice.xml。

這裡面指定的settingsActivity在列印外掛開啟介面右上角的選單裡,用於配置外掛。

addPrintersActivity除了在列印外掛開啟介面的選單裡,在列印檔案時新增印表機裡也會被觸發,這個activity用來自定義新增印表機。

advancedPrintOptionsActivity則是在列印檔案的介面上點選更多箭頭裡出現的MORE OPTIONS選項觸發,這個activity用配置印表機的跟多資訊。當然這是可選的操作,也可以沒有這個activity。

printservice.xml檔案內容如下所示:

<?xml version="1.0" encoding="utf-8"?>
<print-service xmlns:android="http://schemas.android.com/apk/res/android"
    android:vendor="example"
    android:settingsActivity="com.example.testprintservice.SettingsActivity"
    android:addPrintersActivity="com.example.testprintservice.AddPrintersActivity"
     android:advancedPrintOptionsActivity="com.example.testprintservice.AdvancedPrintOptionsActivity"
    >
</print-service>

2 PrintService實現類編寫

在這裡的 onPrintJobQueued 方法中,直接將需要列印的資料輸出為檔案。存放在APP根目錄裡的files資料夾。

package com.example.testprintservice;

import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
import android.os.ParcelFileDescriptor;
import android.print.PrintJobInfo;
import android.printservice.PrintDocument;
import android.printservice.PrintJob;
import android.printservice.PrintService;
import android.printservice.PrinterDiscoverySession;
import android.util.Log;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.channels.FileChannel;

public class MyPrintService extends PrintService {

    private static final String TAG = "MyPrintService";

    @Override
    protected PrinterDiscoverySession onCreatePrinterDiscoverySession() {
        Log.d(TAG, "onCreatePrinterDiscoverySession()");
        return new MyPrintDiscoverySession(this);
    }

    @Override
    protected void onRequestCancelPrintJob(PrintJob printJob) {
        Log.d(TAG, "onRequestCancelPrintJob()");
        printJob.cancel();
    }

    @Override
    protected void onPrintJobQueued(PrintJob printJob) {
        Log.d(TAG, "onPrintJobQueued()");
        PrintJobInfo printjobinfo = printJob.getInfo();
        PrintDocument printdocument = printJob.getDocument();
        if (printJob.isQueued()) {
            return;
        }
        printJob.start();

        String filename = "docu.pdf";
        File outfile = new File(this.getFilesDir(), filename);
        outfile.delete();
        FileInputStream file = new ParcelFileDescriptor.AutoCloseInputStream(printdocument.getData());
        //建立一個長度為1024的記憶體空間
        byte[] bbuf = new byte[1024];
        //用於儲存實際讀取的位元組數
        int hasRead = 0;
        //使用迴圈來重複讀取資料
        try {
            FileOutputStream outStream = new FileOutputStream(outfile);
            while ((hasRead = file.read(bbuf)) > 0) {
                //將位元組陣列轉換為字串輸出
                //System.out.print(new String(bbuf, 0, hasRead));
                outStream.write(bbuf);
            }
            outStream.close();
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            //關閉檔案輸出流,放在finally塊裡更安全
            try {
                file.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        printJob.complete();
    }
}

3 PrinterDiscoverySession實現類編寫

package com.example.testprintservice;

import android.print.PrintAttributes;
import android.print.PrinterCapabilitiesInfo;
import android.print.PrinterId;
import android.print.PrinterInfo;
import android.printservice.PrinterDiscoverySession;
import android.util.Log;

import java.util.ArrayList;
import java.util.List;

/**
 * Created by bboxh on 2016/3/14.
 */
public class MyPrintDiscoverySession extends PrinterDiscoverySession {
    private static final String TAG = "MyPrintDiscoverySession";
    private final MyPrintService myPrintService;

    public MyPrintDiscoverySession(MyPrintService myPrintService) {
        Log.d(TAG, "MyPrintDiscoverySession()");
        this.myPrintService = myPrintService;
    }

    @Override
    public void onStartPrinterDiscovery(List<PrinterId> priorityList) {
        Log.d(TAG, "onStartPrinterDiscovery()");
        List<PrinterInfo> printers = this.getPrinters();
        String name = "printer1";
        PrinterInfo myprinter = new PrinterInfo
                .Builder(myPrintService.generatePrinterId(name), name, PrinterInfo.STATUS_IDLE)
                .build();
        printers.add(myprinter);
        addPrinters(printers);
    }

    @Override
    public void onStopPrinterDiscovery() {
        Log.d(TAG, "onStopPrinterDiscovery()");
    }

    /**
     * 確定這些印表機存在
     * @param printerIds
     */
    @Override
    public void onValidatePrinters(List<PrinterId> printerIds) {
        Log.d(TAG, "onValidatePrinters()");
    }

    /**
     * 選擇印表機時呼叫該方法更新印表機的狀態,能力
     * @param printerId
     */
    @Override
    public void onStartPrinterStateTracking(PrinterId printerId) {
        Log.d(TAG, "onStartPrinterStateTracking()");
        PrinterInfo printer = findPrinterInfo(printerId);
        if (printer != null) {
            PrinterCapabilitiesInfo capabilities =
                    new PrinterCapabilitiesInfo.Builder(printerId)
                            .setMinMargins(new PrintAttributes.Margins(200, 200, 200, 200))
                            .addMediaSize(PrintAttributes.MediaSize.ISO_A4, true)
                            //.addMediaSize(PrintAttributes.MediaSize.ISO_A5, false)
                            .addResolution(new PrintAttributes.Resolution("R1", "200x200", 200, 200), false)
                            .addResolution(new PrintAttributes.Resolution("R2", "300x300", 300, 300), true)
                            .setColorModes(PrintAttributes.COLOR_MODE_COLOR
                                            | PrintAttributes.COLOR_MODE_MONOCHROME,
                                    PrintAttributes.COLOR_MODE_MONOCHROME)
                            .build();

            printer = new PrinterInfo.Builder(printer)
                    .setCapabilities(capabilities)
                    .setStatus(PrinterInfo.STATUS_IDLE)
            //        .setDescription("fake print 1!")
                    .build();
            List<PrinterInfo> printers = new ArrayList<PrinterInfo>();

            printers.add(printer);
            addPrinters(printers);
        }
    }

    @Override
    public void onStopPrinterStateTracking(PrinterId printerId) {
        Log.d(TAG, "onStopPrinterStateTracking()");
    }

    @Override
    public void onDestroy() {
        Log.d(TAG, "onDestroy()");
    }

    private PrinterInfo findPrinterInfo(PrinterId printerId) {
        List<PrinterInfo> printers = getPrinters();
        final int printerCount = getPrinters().size();
        for (int i = 0; i < printerCount; i++) {
            PrinterInfo printer = printers.get(i);
            if (printer.getId().equals(printerId)) {
                return printer;
            }
        }
        return null;
    }

}

六 總結

學習了該部分知識之後,已經可以初步從系統列印服務接入印表機,並取得要列印的檔案。之後根據使用情況,適時地跟進細節即可。