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;
}
}
六 總結
學習了該部分知識之後,已經可以初步從系統列印服務接入印表機,並取得要列印的檔案。之後根據使用情況,適時地跟進細節即可。