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 * <service android:name=".MyPrintService" 121 * android:permission="android.permission.BIND_PRINT_SERVICE"> 122 * <intent-filter> 123 * <action android:name="android.printservice.PrintService" /> 124 * </intent-filter> 125 * . . . 126 * </service> 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> <service android:name=".MyPrintService" 140 * android:permission="android.permission.BIND_PRINT_SERVICE"> 141 * <intent-filter> 142 * <action android:name="android.printservice.PrintService" /> 143 * </intent-filter> 144 * <meta-data android:name="android.printservice" android:resource="@xml/printservice" /> 145 * </service></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><{@link android.R.styleable#PrintService 150 * print-service}></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 * <{@link android.R.styleable#PrintService print-service}></code> tag. This is 174 * a sample XML file configuring a print service: 175 * <pre> <print-service 176 * android:vendor="SomeVendor" 177 * android:settingsActivity="foo.bar.MySettingsActivity" 178 * andorid:addPrintersActivity="foo.bar.MyAddPrintersActivity." 179 * . . . 180 * /></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