PrintService.java revision b4fda134761c9521a7e127db3806a07a18763b77
1/*
2 * Copyright (C) 2013 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.printservice;
18
19import android.R;
20import android.app.Service;
21import android.content.ComponentName;
22import android.content.Context;
23import android.content.Intent;
24import android.os.Handler;
25import android.os.IBinder;
26import android.os.Looper;
27import android.os.Message;
28import android.os.RemoteException;
29import android.print.PrintJobInfo;
30import android.print.PrinterId;
31import android.util.Log;
32
33import java.util.ArrayList;
34import java.util.Collections;
35import java.util.List;
36
37/**
38 * <p>
39 * This is the base class for implementing print services. A print service knows
40 * how to discover and interact one or more printers via one or more protocols.
41 * </p>
42 * <h3>Printer discovery</h3>
43 * <p>
44 * A print service is responsible for discovering printers, adding discovered printers,
45 * removing added printers, and updating added printers. When the system is interested
46 * in printers managed by your service it will call {@link
47 * #onCreatePrinterDiscoverySession()} from which you must return a new {@link
48 * PrinterDiscoverySession} instance. The returned session encapsulates the interaction
49 * between the system and your service during printer discovery. For description of this
50 * interaction refer to the documentation for {@link PrinterDiscoverySession}.
51 * </p>
52 * <p>
53 * For every printer discovery session all printers have to be added since system does
54 * not retain printers across sessions. Hence, each printer known to this print service
55 * should be added only once during a discovery session. Only an already added printer
56 * can be removed or updated. Removed printers can be added again.
57 * </p>
58 * <h3>Print jobs</h3>
59 * <p>
60 * When a new print job targeted to a printer managed by this print service is is queued,
61 * i.e. ready for processing by the print service, you will receive a call to {@link
62 * #onPrintJobQueued(PrintJob)}. The print service may handle the print job immediately
63 * or schedule that for an appropriate time in the future. The list of all active print
64 * jobs for this service is obtained by calling {@link #getActivePrintJobs()}. Active
65 * print jobs are ones that are queued or started.
66 * </p>
67 * <p>
68 * A print service is responsible for setting a print job's state as appropriate
69 * while processing it. Initially, a print job is queued, i.e. {@link PrintJob#isQueued()
70 * PrintJob.isQueued()} returns true, which means that the document to be printed is
71 * spooled by the system and the print service can begin processing it. You can obtain
72 * the printed document by calling {@link PrintJob#getDocument() PrintJob.getDocument()}
73 * whose data is accessed via {@link PrintDocument#getData() PrintDocument.getData()}.
74 * After the print service starts printing the data it should set the print job's
75 * state to started by calling {@link PrintJob#start()} after which
76 * {@link PrintJob#isStarted() PrintJob.isStarted()} would return true. Upon successful
77 * completion, the print job should be marked as completed by calling {@link
78 * PrintJob#complete() PrintJob.complete()} after which {@link PrintJob#isCompleted()
79 * PrintJob.isCompleted()} would return true. In case of a failure, the print job should
80 * be marked as failed by calling {@link PrintJob#fail(String) PrintJob.fail(
81 * String)} after which {@link PrintJob#isFailed() PrintJob.isFailed()} would
82 * return true.
83 * </p>
84 * <p>
85 * If a print job is queued or started and the user requests to cancel it, the print
86 * service will receive a call to {@link #onRequestCancelPrintJob(PrintJob)} which
87 * requests from the service to do best effort in canceling the job. In case the job
88 * is successfully canceled, its state has to be marked as cancelled by calling {@link
89 * PrintJob#cancel() PrintJob.cancel()} after which {@link PrintJob#isCancelled()
90 * PrintJob.isCacnelled()} would return true.
91 * </p>
92 * <h3>Lifecycle</h3>
93 * <p>
94 * The lifecycle of a print service is managed exclusively by the system and follows
95 * the established service lifecycle. Additionally, starting or stopping a print service
96 * is triggered exclusively by an explicit user action through enabling or disabling it
97 * in the device settings. After the system binds to a print service, it calls {@link
98 * #onConnected()}. This method can be overriden by clients to perform post binding setup.
99 * Also after the system unbinds from a print service, it calls {@link #onDisconnected()}.
100 * This method can be overriden by clients to perform post unbinding cleanup. Your should
101 * not do any work after the system disconnected from your print service since the
102 * service can be killed at any time to reclaim memory. The system will not disconnect
103 * from a print service if there are active print jobs for the printers managed by it.
104 * </p>
105 * <h3>Declaration</h3>
106 * <p>
107 * A print service is declared as any other service in an AndroidManifest.xml but it must
108 * also specify that it handles the {@link android.content.Intent} with action {@link
109 * #SERVICE_INTERFACE android.printservice.PrintService}. Failure to declare this intent
110 * will cause the system to ignore the print service. Additionally, a print service must
111 * request the {@link android.Manifest.permission#BIND_PRINT_SERVICE
112 * android.permission.BIND_PRINT_SERVICE} permission to ensure that only the system can
113 * bind to it. Failure to declare this intent will cause the system to ignore the print
114 * service. Following is an example declaration:
115 * </p>
116 * <pre>
117 * &lt;service android:name=".MyPrintService"
118 *         android:permission="android.permission.BIND_PRINT_SERVICE"&gt;
119 *     &lt;intent-filter&gt;
120 *         &lt;action android:name="android.printservice.PrintService" /&gt;
121 *     &lt;/intent-filter&gt;
122 *     . . .
123 * &lt;/service&gt;
124 * </pre>
125 * <h3>Configuration</h3>
126 * <p>
127 * A print service can be configured by specifying an optional settings activity which
128 * exposes service specific settings, an optional add printers activity which is used for
129 * manual addition of printers, vendor name ,etc. It is a responsibility of the system
130 * to launch the settings and add printers activities when appropriate.
131 * </p>
132 * <p>
133 * A print service is configured by providing a {@link #SERVICE_META_DATA meta-data}
134 * entry in the manifest when declaring the service. A service declaration with a meta-data
135 * tag is presented below:
136 * <pre> &lt;service android:name=".MyPrintService"
137 *         android:permission="android.permission.BIND_PRINT_SERVICE"&gt;
138 *     &lt;intent-filter&gt;
139 *         &lt;action android:name="android.printservice.PrintService" /&gt;
140 *     &lt;/intent-filter&gt;
141 *     &lt;meta-data android:name="android.printservice" android:resource="@xml/printservice" /&gt;
142 * &lt;/service&gt;</pre>
143 * </p>
144 * <p>
145 * For more details for how to configure your print service via the meta-data refer to
146 * {@link #SERVICE_META_DATA} and <code>&lt;{@link android.R.styleable#PrintService
147 * print-service}&gt;</code>.
148 * </p>
149 * <p>
150 * <strong>Note: </strong> All callbacks in this class are executed on the main
151 * application thread. You should also invoke any method of this class on the main
152 * application thread.
153 * </p>
154 */
155public abstract class PrintService extends Service {
156
157    private static final String LOG_TAG = "PrintService";
158
159    private static final boolean DEBUG = false;
160
161    /**
162     * The {@link Intent} action that must be declared as handled by a service
163     * in its manifest for the system to recognize it as a print service.
164     */
165    public static final String SERVICE_INTERFACE = "android.printservice.PrintService";
166
167    /**
168     * Name under which a {@link PrintService} component publishes additional information
169     * about itself. This meta-data must reference a XML resource containing a <code>
170     * &lt;{@link android.R.styleable#PrintService print-service}&gt;</code> tag. This is
171     * a sample XML file configuring a print service:
172     * <pre> &lt;print-service
173     *     android:vendor="SomeVendor"
174     *     android:settingsActivity="foo.bar.MySettingsActivity"
175     *     andorid:addPrintersActivity="foo.bar.MyAddPrintersActivity."
176     *     . . .
177     * /&gt;</pre>
178     * <p>
179     * For detailed configuration options that can be specified via the meta-data
180     * refer to {@link android.R.styleable#PrintService android.R.styleable.PrintService}.
181     * </p>
182     * <p>
183     * If you declare a settings or add a printers activity, they have to be exported,
184     * by setting the {@link android.R.attr#exported} activity attribute to <code>true
185     * </code>. Also in case you want only the system to be able to start any of these
186     * activities you can specify that they request the android.permission
187     * .START_PRINT_SERVICE_CONFIG_ACTIVITY permission by setting the
188     * {@link android.R.attr#permission} activity attribute.
189     * </p>
190     */
191    public static final String SERVICE_META_DATA = "android.printservice";
192
193    /**
194     * If you declared an optional activity with advanced print options via the
195     * {@link R.attr#advancedPrintOptionsActivity advancedPrintOptionsActivity}
196     * attribute, this extra is used to pass in the currently constructed {@link
197     * PrintJobInfo} to your activity allowing you to modify it. After you are
198     * done, you must return the modified {@link PrintJobInfo} via the same extra.
199     * <p>
200     * You cannot modify the passed in {@link PrintJobInfo} directly, rather you
201     * should build another one using the {@link PrintJobInfo.Builder} class. You
202     * can specify any standard properties and add advanced, printer specific,
203     * ones via {@link PrintJobInfo.Builder#putAdvancedOption(String, String)
204     * PrintJobInfo.Builder#putAdvancedOption(String, String)} and {@link
205     * PrintJobInfo.Builder#putAdvancedOption(String, int)
206     * PrintJobInfo.Builder#putAdvancedOption(String, int)}. The advanced options
207     * are not interpreted by the system, they will not be visible to applications,
208     * and can only be accessed by your print service via {@link
209     * PrintJob#getAdvancedStringOption(String) PrintJob.getAdvancedStringOption(String)}
210     * and {@link PrintJob#getAdvancedIntOption(String) PrintJob.getAdvancedIntOption(String)}.
211     * </p>
212     * <p>
213     * If the advanced print options activity offers changes to the standard print
214     * options, you can get the current {@link android.print.PrinterInfo} using the
215     * "android.intent.extra.print.EXTRA_PRINTER_INFO" extra which will allow you to
216     * present the user with UI options supported by the current printer. For example,
217     * if the current printer does not support a give media size, you should not
218     * offer it in the advanced print options dialog.
219     * </p>
220     */
221    public static final String EXTRA_PRINT_JOB_INFO = "android.intent.extra.print.PRINT_JOB_INFO";
222
223    private Handler mHandler;
224
225    private IPrintServiceClient mClient;
226
227    private int mLastSessionId = -1;
228
229    private PrinterDiscoverySession mDiscoverySession;
230
231    @Override
232    protected final void attachBaseContext(Context base) {
233        super.attachBaseContext(base);
234        mHandler = new ServiceHandler(base.getMainLooper());
235    }
236
237    /**
238     * The system has connected to this service.
239     */
240    protected void onConnected() {
241        /* do nothing */
242    }
243
244    /**
245     * The system has disconnected from this service.
246     */
247    protected void onDisconnected() {
248        /* do nothing */
249    }
250
251    /**
252     * Callback asking you to create a new {@link PrinterDiscoverySession}.
253     *
254     * @see PrinterDiscoverySession
255     */
256    protected abstract PrinterDiscoverySession onCreatePrinterDiscoverySession();
257
258    /**
259     * Called when cancellation of a print job is requested. The service
260     * should do best effort to fulfill the request. After the cancellation
261     * is performed, the print job should be marked as cancelled state by
262     * calling {@link PrintJob#cancel()}.
263     *
264     * @param printJob The print job to cancel.
265     *
266     * @see PrintJob#cancel() PrintJob.cancel()
267     * @see PrintJob#isCancelled() PrintJob.isCancelled()
268     */
269    protected abstract void onRequestCancelPrintJob(PrintJob printJob);
270
271    /**
272     * Called when there is a queued print job for one of the printers
273     * managed by this print service.
274     *
275     * @param printJob The new queued print job.
276     *
277     * @see PrintJob#isQueued() PrintJob.isQueued()
278     * @see #getActivePrintJobs()
279     */
280    protected abstract void onPrintJobQueued(PrintJob printJob);
281
282    /**
283     * Gets the active print jobs for the printers managed by this service.
284     * Active print jobs are ones that are not in a final state, i.e. whose
285     * state is queued or started.
286     *
287     * @return The active print jobs.
288     *
289     * @see PrintJob#isQueued() PrintJob.isQueued()
290     * @see PrintJob#isStarted() PrintJob.isStarted()
291     */
292    public final List<PrintJob> getActivePrintJobs() {
293        throwIfNotCalledOnMainThread();
294        if (mClient == null) {
295            return Collections.emptyList();
296        }
297        try {
298            List<PrintJob> printJobs = null;
299            List<PrintJobInfo> printJobInfos = mClient.getPrintJobInfos();
300            if (printJobInfos != null) {
301                final int printJobInfoCount = printJobInfos.size();
302                printJobs = new ArrayList<PrintJob>(printJobInfoCount);
303                for (int i = 0; i < printJobInfoCount; i++) {
304                    printJobs.add(new PrintJob(printJobInfos.get(i), mClient));
305                }
306            }
307            if (printJobs != null) {
308                return printJobs;
309            }
310        } catch (RemoteException re) {
311            Log.e(LOG_TAG, "Error calling getPrintJobs()", re);
312        }
313        return Collections.emptyList();
314    }
315
316    /**
317     * Generates a global printer id given the printer's locally unique one.
318     *
319     * @param localId A locally unique id in the context of your print service.
320     * @return Global printer id.
321     */
322    public final PrinterId generatePrinterId(String localId) {
323        throwIfNotCalledOnMainThread();
324        return new PrinterId(new ComponentName(getPackageName(),
325                getClass().getName()), localId);
326    }
327
328    static void throwIfNotCalledOnMainThread() {
329        if (!Looper.getMainLooper().isCurrentThread()) {
330            throw new IllegalAccessError("must be called from the main thread");
331        }
332    }
333
334    @Override
335    public final IBinder onBind(Intent intent) {
336        return new IPrintService.Stub() {
337            @Override
338            public void createPrinterDiscoverySession() {
339                mHandler.sendEmptyMessage(ServiceHandler.MSG_CREATE_PRINTER_DISCOVERY_SESSION);
340            }
341
342            @Override
343            public void destroyPrinterDiscoverySession() {
344                mHandler.sendEmptyMessage(ServiceHandler.MSG_DESTROY_PRINTER_DISCOVERY_SESSION);
345            }
346
347            public void startPrinterDiscovery(List<PrinterId> priorityList) {
348                mHandler.obtainMessage(ServiceHandler.MSG_START_PRINTER_DISCOVERY,
349                        priorityList).sendToTarget();
350            }
351
352            @Override
353            public void stopPrinterDiscovery() {
354                mHandler.sendEmptyMessage(ServiceHandler.MSG_STOP_PRINTER_DISCOVERY);
355            }
356
357            @Override
358            public void validatePrinters(List<PrinterId> printerIds) {
359                mHandler.obtainMessage(ServiceHandler.MSG_VALIDATE_PRINTERS,
360                        printerIds).sendToTarget();
361            }
362
363            @Override
364            public void startPrinterStateTracking(PrinterId printerId) {
365                mHandler.obtainMessage(ServiceHandler.MSG_START_PRINTER_STATE_TRACKING,
366                        printerId).sendToTarget();
367            }
368
369            @Override
370            public void stopPrinterStateTracking(PrinterId printerId) {
371                mHandler.obtainMessage(ServiceHandler.MSG_STOP_PRINTER_STATE_TRACKING,
372                        printerId).sendToTarget();
373            }
374
375            @Override
376            public void setClient(IPrintServiceClient client) {
377                mHandler.obtainMessage(ServiceHandler.MSG_SET_CLEINT, client)
378                        .sendToTarget();
379            }
380
381            @Override
382            public void requestCancelPrintJob(PrintJobInfo printJobInfo) {
383                mHandler.obtainMessage(ServiceHandler.MSG_ON_REQUEST_CANCEL_PRINTJOB,
384                        printJobInfo).sendToTarget();
385            }
386
387            @Override
388            public void onPrintJobQueued(PrintJobInfo printJobInfo) {
389                mHandler.obtainMessage(ServiceHandler.MSG_ON_PRINTJOB_QUEUED,
390                        printJobInfo).sendToTarget();
391            }
392        };
393    }
394
395    private final class ServiceHandler extends Handler {
396        public static final int MSG_CREATE_PRINTER_DISCOVERY_SESSION = 1;
397        public static final int MSG_DESTROY_PRINTER_DISCOVERY_SESSION = 2;
398        public static final int MSG_START_PRINTER_DISCOVERY = 3;
399        public static final int MSG_STOP_PRINTER_DISCOVERY = 4;
400        public static final int MSG_VALIDATE_PRINTERS = 5;
401        public static final int MSG_START_PRINTER_STATE_TRACKING = 6;
402        public static final int MSG_STOP_PRINTER_STATE_TRACKING = 7;
403        public static final int MSG_ON_PRINTJOB_QUEUED = 8;
404        public static final int MSG_ON_REQUEST_CANCEL_PRINTJOB = 9;
405        public static final int MSG_SET_CLEINT = 10;
406
407        public ServiceHandler(Looper looper) {
408            super(looper, null, true);
409        }
410
411        @Override
412        @SuppressWarnings("unchecked")
413        public void handleMessage(Message message) {
414            final int action = message.what;
415            switch (action) {
416                case MSG_CREATE_PRINTER_DISCOVERY_SESSION: {
417                    if (DEBUG) {
418                        Log.i(LOG_TAG, "MSG_CREATE_PRINTER_DISCOVERY_SESSION "
419                                + getPackageName());
420                    }
421                    PrinterDiscoverySession session = onCreatePrinterDiscoverySession();
422                    if (session == null) {
423                        throw new NullPointerException("session cannot be null");
424                    }
425                    if (session.getId() == mLastSessionId) {
426                        throw new IllegalStateException("cannot reuse session instances");
427                    }
428                    mDiscoverySession = session;
429                    mLastSessionId = session.getId();
430                    session.setObserver(mClient);
431                } break;
432
433                case MSG_DESTROY_PRINTER_DISCOVERY_SESSION: {
434                    if (DEBUG) {
435                        Log.i(LOG_TAG, "MSG_DESTROY_PRINTER_DISCOVERY_SESSION "
436                                + getPackageName());
437                    }
438                    if (mDiscoverySession != null) {
439                        mDiscoverySession.destroy();
440                        mDiscoverySession = null;
441                    }
442                } break;
443
444                case MSG_START_PRINTER_DISCOVERY: {
445                    if (DEBUG) {
446                        Log.i(LOG_TAG, "MSG_START_PRINTER_DISCOVERY "
447                                + getPackageName());
448                    }
449                    if (mDiscoverySession != null) {
450                        List<PrinterId> priorityList = (ArrayList<PrinterId>) message.obj;
451                        mDiscoverySession.startPrinterDiscovery(priorityList);
452                    }
453                } break;
454
455                case MSG_STOP_PRINTER_DISCOVERY: {
456                    if (DEBUG) {
457                        Log.i(LOG_TAG, "MSG_STOP_PRINTER_DISCOVERY "
458                                + getPackageName());
459                    }
460                    if (mDiscoverySession != null) {
461                        mDiscoverySession.stopPrinterDiscovery();
462                    }
463                } break;
464
465                case MSG_VALIDATE_PRINTERS: {
466                    if (DEBUG) {
467                        Log.i(LOG_TAG, "MSG_VALIDATE_PRINTERS "
468                                + getPackageName());
469                    }
470                    if (mDiscoverySession != null) {
471                        List<PrinterId> printerIds = (List<PrinterId>) message.obj;
472                        mDiscoverySession.validatePrinters(printerIds);
473                    }
474                } break;
475
476                case MSG_START_PRINTER_STATE_TRACKING: {
477                    if (DEBUG) {
478                        Log.i(LOG_TAG, "MSG_START_PRINTER_STATE_TRACKING "
479                                + getPackageName());
480                    }
481                    if (mDiscoverySession != null) {
482                        PrinterId printerId = (PrinterId) message.obj;
483                        mDiscoverySession.startPrinterStateTracking(printerId);
484                    }
485                } break;
486
487                case MSG_STOP_PRINTER_STATE_TRACKING: {
488                    if (DEBUG) {
489                        Log.i(LOG_TAG, "MSG_STOP_PRINTER_STATE_TRACKING "
490                                + getPackageName());
491                    }
492                    if (mDiscoverySession != null) {
493                        PrinterId printerId = (PrinterId) message.obj;
494                        mDiscoverySession.stopPrinterStateTracking(printerId);
495                    }
496                } break;
497
498                case MSG_ON_REQUEST_CANCEL_PRINTJOB: {
499                    if (DEBUG) {
500                        Log.i(LOG_TAG, "MSG_ON_REQUEST_CANCEL_PRINTJOB "
501                                + getPackageName());
502                    }
503                    PrintJobInfo printJobInfo = (PrintJobInfo) message.obj;
504                    onRequestCancelPrintJob(new PrintJob(printJobInfo, mClient));
505                } break;
506
507                case MSG_ON_PRINTJOB_QUEUED: {
508                    if (DEBUG) {
509                        Log.i(LOG_TAG, "MSG_ON_PRINTJOB_QUEUED "
510                                + getPackageName());
511                    }
512                    PrintJobInfo printJobInfo = (PrintJobInfo) message.obj;
513                    if (DEBUG) {
514                        Log.i(LOG_TAG, "Queued: " + printJobInfo);
515                    }
516                    onPrintJobQueued(new PrintJob(printJobInfo, mClient));
517                } break;
518
519                case MSG_SET_CLEINT: {
520                    if (DEBUG) {
521                        Log.i(LOG_TAG, "MSG_SET_CLEINT "
522                                + getPackageName());
523                    }
524                    mClient = (IPrintServiceClient) message.obj;
525                    if (mClient != null) {
526                        onConnected();
527                     } else {
528                        onDisconnected();
529                     }
530                } break;
531
532                default: {
533                    throw new IllegalArgumentException("Unknown message: " + action);
534                }
535            }
536        }
537    }
538}
539