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