2013年11月17日日曜日

Android 4.4 詳解 Printing Framework 後編

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク
 1週間ってアッという間ですね。「Android 4.4 KitKat 冬コミ原稿リレーを開催」の11/9担当のsys1yagiです。

 Printing Frameworkの後編という事で、PrintServiceについて解説します。印刷の出力部分を実装する話は「Android 4.4 詳解 Printing Framework 前編」を参照して下さい。

 サンプルコードとしてDropboxにPDFをアップロードするDropboxPrintServiceを作りました。ソースはコチラ→DropboxPrintService

PrintServiceとは


 PrintServiceはプリンタの検出と、印刷ジョブの処理を行うサービスです。ネットワークプリンタのドライバの様な役割をする他、PDFを任意の場所に出力するサービスとしても実装できます。

PrintServiceの追加や設定


 PrintServiceの実装を含んだアプリケーションをインストールすると、SettingsのPrinting画面にサービスが追加されます(図01)。ユーザはそれぞれのPrintServiceのOn/Offを切り替えたり、各PrintServiceが提供する設定画面を起動したりできます。


図01 [PrintServiceの詳細画面]

PrintServiceの実行フロー


 PrintServiceはシステムが管理するPrintManagerと連携しながら動作します。PrintServiceの実行フローを[図02]に示します。



[図02 PrintServiceの実行フロー]

  1. PrintManagerが印刷ダイアログを表示する
  2. システムからプリンタの検出セッションの開始を要求される(onCreatePrinterDiscoverySessionメソッドの呼び出し)
  3. PrinterDiscoverySessionクラスのインスタンスを返却する
  4. PrinterDiscoverySessionクラス内でプリンタの追加、削除、更新を行う
  5. ユーザがプリンタの選択、設定を行い、印刷を開始する
  6. onPrintJobQueuedメソッドにPrintJobが渡される
  7. PrintJobの情報を元に印刷処理を行う
 これらのフローのうち、3,4,5の処理をPrintService側で実装することになります。

PrintServiceを作る


 では早速PrintServiceを実装してみましょう。PrintServiceはServiceを継承した抽象クラスです。[リスト01]に示す通り3つの抽象メソッドを持っています。

リスト01 [PrintServiceの持つ抽象メソッド]

public class SamplePrintService extends PrintService {

  @Override
  protected PrinterDiscoverySession onCreatePrinterDiscoverySession() {
    // プリンタの検索セッション用オブジェクトを返す
    return null;
  }

  @Override
  protected void onPrintJobQueued(PrintJob paramPrintJob) {
    // 印刷処理をする
  }

  @Override
  protected void onRequestCancelPrintJob(PrintJob paramPrintJob) {
    // キャンセルリクエスト
  }

}

onCreatePrinterDiscoverySession()


 システムがプリンタの検出を開始したい時、PrintServiceのonCreatePrinterDiscoverySessionメソッドが呼び出されます。この時PrintService側はPrinterDiscoverySessionのインスタンスを返却する必要があります。

 PrinterDiscoverySessionはプリンタの検出をする間、システムとPrintService間のやりとりをカプセル化するクラスです。PrinterDiscoverySessionクラスの各コールバックメソッドが呼び出されるので、プリンタの追加や更新、削除を行います。

 PrinterDiscoverySessionクラスは沢山の抽象メソッドを持っています。[表01]にPrinterDiscoverySessionクラスの抽象メソッドの解説を示します。

表01 [PrinterDiscoverySessionの抽象メソッド]

項目 解説
void onStartPrinterDiscovery(List<PrinterId> printers) プリンタの検出を開始します。printersには検出済みのプリンタ情報が入っています。printersは補助的な情報です。このセッションで検出するプリンタとは関係ないので、printersに含まれるからといってプリンタ情報を追加しなくてよいわけではありません。プリンタの追加、更新はaddPrintersメソッドで行います。削除はremovePrintersメソッドで行います。
void onStopPrinterDiscovery() プリンタ検出を停止します
void onStartPrinterStateTracking(PrinterId printerId) プリンタの状態の追跡を開始するように求めるコールバックです。印刷ダイアログでプリンタが選択された時に呼び出されます。プリンタの状態が変化した時はすみやかにシステムに通知しなければなりません
void onStopPrinterStateTracking(PrinterId printerId) プリンタの状態の追跡が終了した事を通知するコールバックです。
void onValidatePrinters(List<PrinterId> printerIds) 渡されたPrinterIdのリストが有効であるかの検証を求めるコールバックです。有効な場合はaddPrintersメソッドでプリンタ情報を更新する必要があります
void onDestroy() システムからこのセッションが不要であると判断された時に呼び出されます


 [リスト02]にPrinterDiscoverySessionの実装例を示します。この例は実際にDropboxPrintServiceで利用しています。onStartPrinterDiscoveryメソッドでプリンタを追加している以外は何もしていません。

 プリンタ情報はPrinterInfo.Builderを使って作成します。印刷可能な用紙サイズや、色、解像度などの設定を行えます。addPrintersメソッドでPrinterInfoのリストを渡せば、印刷ダイアログ上にプリンタ情報が表示されます。[リスト02]ではonStartPrinterDiscoveryメソッド内でPrinterInfoを作成し、addPrintersメソッドを呼び出していますが、通常はスレッドを起動してプリンタの検出を行い、Handler等を使ってプリンタ情報を追加する事になるでしょう。

リスト02 [PrinterDiscoverySessionの実装例]

public class DropboxPrinterDiscoverySession
  extends PrinterDiscoverySession {

  private final static String TAG = 
    DropboxPrinterDiscoverySession.class.getSimpleName();
  private final static String PRINTER_ID = "dropbox.print.service";

  private PrintService mPrintService;

  public DropboxPrinterDiscoverySession(PrintService printServie){
    mPrintService = printServie;
  }

  @Override
  public void onStartPrinterDiscovery(List<PrinterId> printers) {
    Log.d(TAG, "onStartPrinterDiscovery()");
    for (PrinterId id : printers) {
      Log.d(TAG, "printerId:" + id.getLocalId());
    }
    List<PrinterInfo> addPrinters = new ArrayList<PrinterInfo>();
    PrinterId printerId = mPrintService.generatePrinterId(PRINTER_ID);

    PrinterInfo.Builder builder = new PrinterInfo.Builder(printerId,
        "Dropbox Printer", PrinterInfo.STATUS_IDLE);

    PrinterCapabilitiesInfo.Builder capBuilder =
      new PrinterCapabilitiesInfo.Builder(printerId);
    capBuilder.addMediaSize(PrintAttributes.MediaSize.ISO_A4, true);
    capBuilder.addMediaSize(PrintAttributes.MediaSize.ISO_B5, false);
    capBuilder.addResolution(new PrintAttributes.Resolution(
        "Default", "default resolution", 600, 600), true);
    capBuilder.setColorModes(PrintAttributes.COLOR_MODE_COLOR
        | PrintAttributes.COLOR_MODE_MONOCHROME,
        PrintAttributes.COLOR_MODE_COLOR);

    builder.setCapabilities(capBuilder.build());
    addPrinters.add(builder.build());
    addPrinters(addPrinters);
  }

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

  @Override
  public void onValidatePrinters(List<PrinterId> printerIds) {
    Log.d(TAG, "onValidatePrinters()");
    for (PrinterId id : printerIds) {
      Log.d(TAG, "printerId:" + id.getLocalId());
    }
  }

  @Override
  public void onStartPrinterStateTracking(PrinterId printerId) {
    Log.d(TAG,
        "onStartPrinterStateTracking(printerId: "
            + printerId.getLocalId() + ")");
  }

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

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

onPrintJobQueued(PrintJob paramPrintJob)


 検出したプリンタに対して、ユーザが印刷を開始すると、onPrintJobQueuedメソッドにPrintJobが渡されます。PrintJobにはプリンタIDの他、印刷サイズや色情報、PDFファイルへのFileDescriptorなどが含まれています。印刷データの取り出しはFileDescriptorを使って行います。

 PrintJobは開始、完了、失敗、キャンセルなどのステータスを持っています。印刷の状態に応じてセットする必要があります。

 [リスト03]はPrintJobからPDFを取り出して保存する例です。onPrintJobQueuedメソッド上で全ての処理を行なっていますが、通常はAsyncTask等を使う事になるでしょう。ただし、PrintJobの操作はUIスレッド上で行わなければなりません。PDFデータのFileDescriptorも同様です。PDFデータをネットワーク上に送信する場合は、一時ファイル等を作成した上でAsyncTaskを利用する事になります。

リスト03 [PDFを保存する]

@Override
protected void onPrintJobQueued(final PrintJob printJob) {

  printJob.start();

  File tmp = new File(getExternalCacheDir(), printJob.getDocument()
      .getInfo().getName());

  FileOutputStream fos = null;
  FileInputStream fin = null;
  try {
    fos = new FileOutputStream(tmp);
    fin = new FileInputStream(printJob.getDocument()
        .getData().getFileDescriptor());
    int c;
    byte[] bytes = new byte[1024];

    while ((c = fin.read(bytes)) != -1) {
      fos.write(bytes, 0, c);
    }
    fos.close();
    fin.close();
  } catch (Exception e) {
    e.printStackTrace();
    printJob.fail(e.getMessage());
    return;
  } finally {
    if(fos != null){try{fos.close();}catch(Exception e){}}
    if(fin != null){try{fin.close();}catch(Exception e){}}
  }

  printJob.complete();

}

 PrintJobはPrintService内で自動的にキューイングされているので、改めて自分でキューのハンドリングをする必要はありません。アクティブなPrintJobはgetActivePrintJobsメソッドで取り出せます。

onRequestCancelPrintJob(PrintJob paramPrintJob)


 onRequestCancelPrintJobメソッドはユーザが印刷をキャンセルした時に呼び出されます。PrintJobが渡されるので、対応する処理を停止する必要があります。PrintJobクラスのisCancelledメソッドで確認する事もできます(リスト04)。

リスト04 [isCancelledメソッドを使う例]

protected void onPostExecute(PrintStatus result) {
    if(printJob.isCancelled()) {
        toast("キャンセルされました");
    } else if (result.isError()) {
        printJob.fail(result.getMessage());
    } else {
        printJob.complete();
    }
    toast(result.getMessage());
};

AndroidManifest.xmlに追加する


 作成したPrintServiceをAndroidManifest.xmlに定義する事で、 アプリケーションのインストール時にPrintServiceをシステムに追加できます(リスト05)。

リスト05 [AndroidManifest.xmlにPrintServiceを追加する]

<service
  android:name="app.package.name..YourPrintService"
  android:permission="android.permission.BIND_PRINT_SERVICE" >
  <intent-filter>
    <action android:name="andraoid.printservice.PrintService" />
  </intent-filter>
</service>

設定画面を作る


 多くの場合、ネットワーク越しのプリンタに接続する為の設定がPrintServiceに必要となるでしょう。AndroidManifest.xmlでPrintServiceを定義する際にメタデータの設定を行えます。

 [リスト06]はPrintServiceで使う設定画面の定義例です。設定用のActivityを定義しています。XMLファイルは"res/xml"配下に作
成してください。

リスト06 [res/xml/your_printservice.xml]

<print-service
  xmlns:android="http://schemas.android.com/apk/res/android"
  android:description="@string/description"
  android:settingsActivity="app.package.name.YourSettingActivity" />

 作成したXMLファイルをAndroidManifest.xmlのメタデータで指定します(リスト07)。これでPrintServiceの詳細画面から設定画面へ遷移できます。設定画面のActivityも忘れずAndroidManifest.xmlに定義しましょう。

リスト07 [AndroidManifest.xmlの設定]

<service
  android:name="app.package.name.YourPrintService"
  android:permission="android.permission.BIND_PRINT_SERVICE" >
  <intent-filter>
    <action android:name="andraoid.printservice.PrintService" />
  </intent-filter>

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

<activity
  android:name="app.package.name.YourSettingActivity"
  android:exported="true"
  android:label="@string/title_activity_setting"
  android:permission="android.permission.BIND_PRINT_SERVICE" >
</activity>

DropboxPrintServiceを作りました


 本エントリを書くに当たり、調査、習作としてDropboxにPDFを送信するPrintServiceを作成しました。使い方としては以下の通りです。

  • ソースはコチラ→DropboxPrintService
    • ソースの方にはAPIキーは含めていませんので、ソースからビルドして使う場合はDropboxでAPIキーを取得して下さい。
  • apkはコチラ→DropboxPrintService.apk


Dropboxにログインする


 DropboxPrintServiceをインストールしたら、まずはDropboxにログインする必要があります。SettingsのPrinting画面でDropboxPrintServiceを選択し、詳細画面を開いてDropboxPrintServiceをONにして下さい。下部のメニューから設定画面を開けるのでDropboxにログインして下さい。



[図03 Dropboxにログインする]

印刷する


 あとは普通に印刷するだけです。



[図04 Dropbox Printerを使う]

 PDFがDropboxにアップロードされます。



[図05 DropboxにアップロードされたPDF]

おわりに


 本エントリではPrintServiceの役割とざっくりした実装を解説しました。プリンタの追加、削除や、プリントジョブのキャンセル等をしっかり作りこむには色々と考慮点が多そうですが、基本的な事は本エントリの情報で実現できると思います。

 「印刷先がプリンタである必要はない」という点はとてもおもしろく、アイデア次第で便利なサービスを作れる気がします。Kitkatが普及するのはまだ先ですが、今後面白いPrintServiceが登場する事を期待しています。

1 件のコメント:

  1. I have followed your blog. The issue I am facing is that, the print job are not reaching the printer. What is printerId and LocalIds?. Are they ip addresses or Mac addresses

    返信削除