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