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.app.Service; 20import android.content.ComponentName; 21import android.content.Context; 22import android.content.Intent; 23import android.os.Handler; 24import android.os.IBinder; 25import android.os.Looper; 26import android.os.Message; 27import android.os.RemoteException; 28import android.print.PrintJobInfo; 29import android.print.PrinterId; 30import android.util.Log; 31 32import java.util.ArrayList; 33import java.util.Collections; 34import 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 * <service android:name=".MyPrintService" 117 * android:permission="android.permission.BIND_PRINT_SERVICE"> 118 * <intent-filter> 119 * <action android:name="android.printservice.PrintService" /> 120 * </intent-filter> 121 * . . . 122 * </service> 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> <service android:name=".MyPrintService" 136 * android:permission="android.permission.BIND_PRINT_SERVICE"> 137 * <intent-filter> 138 * <action android:name="android.printservice.PrintService" /> 139 * </intent-filter> 140 * <meta-data android:name="android.printservice" android:resource="@xml/printservice" /> 141 * </service></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><{@link android.R.styleable#PrintService 146 * print-service}></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 */ 154public 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 * <{@link android.R.styleable#PrintService print-service}></code> tag. This is 170 * a sample XML file configuring a print service: 171 * <pre> <print-service 172 * android:vendor="SomeVendor" 173 * android:settingsActivity="foo.bar.MySettingsActivity" 174 * andorid:addPrintersActivity="foo.bar.MyAddPrintersActivity." 175 * . . . 176 * /></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 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 */ 264 protected void onConnected() { 265 /* do nothing */ 266 } 267 268 /** 269 * The system has disconnected from this service. 270 */ 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 */ 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 */ 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 */ 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 */ 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 */ 346 public final PrinterId generatePrinterId(String localId) { 347 throwIfNotCalledOnMainThread(); 348 return new PrinterId(new ComponentName(getPackageName(), 349 getClass().getName()), localId); 350 } 351 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 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