1 /* 2 * Copyright (C) 2015 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.print.cts; 18 19 import static android.print.test.Utils.assertException; 20 import static android.print.test.Utils.eventually; 21 import static android.print.test.Utils.getPrintJob; 22 import static android.print.test.Utils.runOnMainThread; 23 24 import static org.junit.Assert.assertEquals; 25 import static org.junit.Assert.assertFalse; 26 import static org.junit.Assert.assertNotNull; 27 import static org.junit.Assert.assertTrue; 28 29 import android.app.Activity; 30 import android.app.PendingIntent; 31 import android.content.ComponentName; 32 import android.content.Context; 33 import android.content.Intent; 34 import android.content.pm.ApplicationInfo; 35 import android.content.pm.PackageInfo; 36 import android.content.pm.PackageManager; 37 import android.graphics.Bitmap; 38 import android.graphics.BitmapFactory; 39 import android.graphics.Canvas; 40 import android.graphics.drawable.Drawable; 41 import android.graphics.drawable.Icon; 42 import android.print.PrintAttributes; 43 import android.print.PrintAttributes.Margins; 44 import android.print.PrintAttributes.MediaSize; 45 import android.print.PrintAttributes.Resolution; 46 import android.print.PrintDocumentAdapter; 47 import android.print.PrintManager; 48 import android.print.PrinterCapabilitiesInfo; 49 import android.print.PrinterId; 50 import android.print.PrinterInfo; 51 import android.print.test.BasePrintTest; 52 import android.print.test.services.FirstPrintService; 53 import android.print.test.services.InfoActivity; 54 import android.print.test.services.PrintServiceCallbacks; 55 import android.print.test.services.PrinterDiscoverySessionCallbacks; 56 import android.print.test.services.SecondPrintService; 57 import android.print.test.services.StubbablePrintService; 58 import android.print.test.services.StubbablePrinterDiscoverySession; 59 import android.printservice.CustomPrinterIconCallback; 60 import android.printservice.PrintJob; 61 import android.printservice.PrintService; 62 import android.support.test.runner.AndroidJUnit4; 63 import android.support.test.uiautomator.UiDevice; 64 import android.support.test.uiautomator.UiObject; 65 import android.support.test.uiautomator.UiSelector; 66 67 import org.junit.Test; 68 import org.junit.runner.RunWith; 69 70 import java.util.ArrayList; 71 import java.util.List; 72 73 /** 74 * Test the interface from a print service to the print manager 75 */ 76 @RunWith(AndroidJUnit4.class) 77 public class PrintServicesTest extends BasePrintTest { 78 private static final String PRINTER_NAME = "Test printer"; 79 80 /** The print job processed in the test */ 81 private static PrintJob sPrintJob; 82 83 /** Printer under test */ 84 private static PrinterInfo sPrinter; 85 86 /** The custom printer icon to use */ 87 private Icon mIcon; 88 89 /** 90 * Create a mock {@link PrinterDiscoverySessionCallbacks} that discovers a single printer with 91 * minimal capabilities. 92 * 93 * @return The mock session callbacks 94 */ createMockPrinterDiscoverySessionCallbacks( String printerName, ArrayList<String> trackedPrinters)95 private PrinterDiscoverySessionCallbacks createMockPrinterDiscoverySessionCallbacks( 96 String printerName, ArrayList<String> trackedPrinters) { 97 return createMockPrinterDiscoverySessionCallbacks(invocation -> { 98 // Get the session. 99 StubbablePrinterDiscoverySession session = 100 ((PrinterDiscoverySessionCallbacks) invocation.getMock()).getSession(); 101 102 if (session.getPrinters().isEmpty()) { 103 List<PrinterInfo> printers = new ArrayList<>(); 104 105 // Add the printer. 106 PrinterId printerId = session.getService() 107 .generatePrinterId(printerName); 108 109 PrinterCapabilitiesInfo capabilities = new PrinterCapabilitiesInfo.Builder( 110 printerId) 111 .setMinMargins(new Margins(200, 200, 200, 200)) 112 .addMediaSize(MediaSize.ISO_A4, true) 113 .addResolution(new Resolution("300x300", "300x300", 300, 300), 114 true) 115 .setColorModes(PrintAttributes.COLOR_MODE_COLOR, 116 PrintAttributes.COLOR_MODE_COLOR) 117 .build(); 118 119 Intent infoIntent = new Intent(getActivity(), InfoActivity.class); 120 infoIntent.putExtra("PRINTER_NAME", PRINTER_NAME); 121 122 PendingIntent infoPendingIntent = PendingIntent.getActivity(getActivity(), 0, 123 infoIntent, PendingIntent.FLAG_UPDATE_CURRENT); 124 125 sPrinter = new PrinterInfo.Builder(printerId, printerName, 126 PrinterInfo.STATUS_IDLE) 127 .setCapabilities(capabilities) 128 .setDescription("Minimal capabilities") 129 .setInfoIntent(infoPendingIntent) 130 .build(); 131 printers.add(sPrinter); 132 133 session.addPrinters(printers); 134 } 135 136 onPrinterDiscoverySessionCreateCalled(); 137 138 return null; 139 }, null, null, invocation -> { 140 if (trackedPrinters != null) { 141 synchronized (trackedPrinters) { 142 trackedPrinters 143 .add(((PrinterId) invocation.getArguments()[0]).getLocalId()); 144 trackedPrinters.notifyAll(); 145 } 146 } 147 return null; 148 }, invocation -> { 149 CustomPrinterIconCallback callback = (CustomPrinterIconCallback) invocation 150 .getArguments()[2]; 151 152 if (mIcon != null) { 153 callback.onCustomPrinterIconLoaded(mIcon); 154 } 155 return null; 156 }, invocation -> { 157 if (trackedPrinters != null) { 158 synchronized (trackedPrinters) { 159 trackedPrinters.remove(((PrinterId) invocation.getArguments()[0]).getLocalId()); 160 trackedPrinters.notifyAll(); 161 } 162 } 163 164 return null; 165 }, invocation -> { 166 // Take a note onDestroy was called. 167 onPrinterDiscoverySessionDestroyCalled(); 168 return null; 169 }); 170 } 171 172 /** 173 * Get the current progress of #sPrintJob 174 * 175 * @return The current progress 176 * 177 * @throws InterruptedException If the thread was interrupted while setting the progress 178 * @throws Throwable If anything is unexpected. 179 */ getProgress()180 private float getProgress() throws Throwable { 181 float[] printProgress = new float[1]; 182 runOnMainThread(() -> printProgress[0] = sPrintJob.getInfo().getProgress()); 183 184 return printProgress[0]; 185 } 186 187 /** 188 * Get the current status of #sPrintJob 189 * 190 * @return The current status 191 * 192 * @throws InterruptedException If the thread was interrupted while getting the status 193 * @throws Throwable If anything is unexpected. 194 */ getStatus()195 private CharSequence getStatus() throws Throwable { 196 CharSequence[] printStatus = new CharSequence[1]; 197 runOnMainThread(() -> printStatus[0] = sPrintJob.getInfo().getStatus(getActivity() 198 .getPackageManager())); 199 200 return printStatus[0]; 201 } 202 203 /** 204 * Check if a print progress is correct. 205 * 206 * @param desiredProgress The expected @{link PrintProgresses} 207 * 208 * @throws Throwable If anything goes wrong or this takes more than 5 seconds 209 */ checkNotification(float desiredProgress, CharSequence desiredStatus)210 private void checkNotification(float desiredProgress, CharSequence desiredStatus) 211 throws Throwable { 212 eventually(() -> assertEquals(desiredProgress, getProgress(), 0.1)); 213 eventually(() -> assertEquals(desiredStatus.toString(), getStatus().toString())); 214 } 215 216 /** 217 * Set a new progress and status for #sPrintJob 218 * 219 * @param progress The new progress to set 220 * @param status The new status to set 221 * 222 * @throws InterruptedException If the thread was interrupted while setting 223 * @throws Throwable If anything is unexpected. 224 */ setProgressAndStatus(final float progress, final CharSequence status)225 private void setProgressAndStatus(final float progress, final CharSequence status) 226 throws Throwable { 227 runOnMainThread(() -> { 228 sPrintJob.setProgress(progress); 229 sPrintJob.setStatus(status); 230 }); 231 } 232 233 /** 234 * Progress print job and check the print job state. 235 * 236 * @param progress How much to progress 237 * @param status The status to set 238 * 239 * @throws Throwable If anything goes wrong. 240 */ progress(float progress, CharSequence status)241 private void progress(float progress, CharSequence status) throws Throwable { 242 setProgressAndStatus(progress, status); 243 244 // Check that progress of job is correct 245 checkNotification(progress, status); 246 } 247 248 /** 249 * Create mock service callback for a session. 250 * 251 * @param sessionCallbacks The callbacks of the sessopm 252 */ createMockPrinterServiceCallbacks( final PrinterDiscoverySessionCallbacks sessionCallbacks)253 private PrintServiceCallbacks createMockPrinterServiceCallbacks( 254 final PrinterDiscoverySessionCallbacks sessionCallbacks) { 255 return createMockPrintServiceCallbacks( 256 invocation -> sessionCallbacks, 257 invocation -> { 258 sPrintJob = (PrintJob) invocation.getArguments()[0]; 259 sPrintJob.start(); 260 onPrintJobQueuedCalled(); 261 262 return null; 263 }, invocation -> { 264 sPrintJob = (PrintJob) invocation.getArguments()[0]; 265 sPrintJob.cancel(); 266 267 return null; 268 }); 269 } 270 271 /** 272 * Test that the progress and status is propagated correctly. 273 * 274 * @throws Throwable If anything is unexpected. 275 */ 276 @Test 277 public void progress() throws Throwable { 278 // Create the session callbacks that we will be checking. 279 PrinterDiscoverySessionCallbacks sessionCallbacks 280 = createMockPrinterDiscoverySessionCallbacks(PRINTER_NAME, null); 281 282 // Create the service callbacks for the first print service. 283 PrintServiceCallbacks serviceCallbacks = createMockPrinterServiceCallbacks( 284 sessionCallbacks); 285 286 // Configure the print services. 287 FirstPrintService.setCallbacks(serviceCallbacks); 288 289 // We don't use the second service, but we have to still configure it 290 SecondPrintService.setCallbacks(createMockPrintServiceCallbacks(null, null, null)); 291 292 // Create a print adapter that respects the print contract. 293 PrintDocumentAdapter adapter = createDefaultPrintDocumentAdapter(1); 294 295 // Start printing. 296 print(adapter); 297 298 // Wait for write of the first page. 299 waitForWriteAdapterCallback(1); 300 301 // Select the printer. 302 selectPrinter(PRINTER_NAME); 303 304 // Click the print button. 305 clickPrintButton(); 306 307 // Answer the dialog for the print service cloud warning 308 answerPrintServicesWarning(true); 309 310 // Wait until the print job is queued and #sPrintJob is set 311 waitForServiceOnPrintJobQueuedCallbackCalled(1); 312 313 // Progress print job and check for appropriate notifications 314 progress(0, "printed 0"); 315 progress(0.5f, "printed 50"); 316 progress(1, "printed 100"); 317 318 // Call complete from the main thread 319 runOnMainThread(sPrintJob::complete); 320 321 // Wait for all print jobs to be handled after which the session destroyed. 322 waitForPrinterDiscoverySessionDestroyCallbackCalled(1); 323 } 324 325 /** 326 * Render a {@link Drawable} into a {@link Bitmap}. 327 * 328 * @param d the drawable to be rendered 329 * 330 * @return the rendered bitmap 331 */ 332 private static Bitmap renderDrawable(Drawable d) { 333 Bitmap bitmap = Bitmap.createBitmap(d.getIntrinsicWidth(), d.getIntrinsicHeight(), 334 Bitmap.Config.ARGB_8888); 335 336 Canvas canvas = new Canvas(bitmap); 337 d.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); 338 d.draw(canvas); 339 340 return bitmap; 341 } 342 343 /** 344 * Update the printer 345 * 346 * @param sessionCallbacks The callbacks for the service the printer belongs to 347 * @param printer the new printer to use 348 * 349 * @throws InterruptedException If we were interrupted while the printer was updated. 350 * @throws Throwable If anything is unexpected. 351 */ 352 private void updatePrinter(PrinterDiscoverySessionCallbacks sessionCallbacks, 353 final PrinterInfo printer) throws Throwable { 354 runOnMainThread(() -> { 355 ArrayList<PrinterInfo> printers = new ArrayList<>(1); 356 printers.add(printer); 357 sessionCallbacks.getSession().addPrinters(printers); 358 }); 359 360 // Update local copy of printer 361 sPrinter = printer; 362 } 363 364 /** 365 * Assert is the printer icon does not match the bitmap. As the icon update might take some time 366 * we try up to 5 seconds. 367 * 368 * @param bitmap The bitmap to match 369 * 370 * @throws Throwable If anything is unexpected. 371 */ 372 private void assertThatIconIs(Bitmap bitmap) throws Throwable { 373 eventually( 374 () -> assertTrue(bitmap.sameAs(renderDrawable(sPrinter.loadIcon(getActivity()))))); 375 } 376 377 /** 378 * Test that the icon get be updated. 379 * 380 * @throws Throwable If anything is unexpected. 381 */ 382 @Test 383 public void updateIcon() throws Throwable { 384 // Create the session callbacks that we will be checking. 385 final PrinterDiscoverySessionCallbacks sessionCallbacks 386 = createMockPrinterDiscoverySessionCallbacks(PRINTER_NAME, null); 387 388 // Create the service callbacks for the first print service. 389 PrintServiceCallbacks serviceCallbacks = createMockPrinterServiceCallbacks( 390 sessionCallbacks); 391 392 // Configure the print services. 393 FirstPrintService.setCallbacks(serviceCallbacks); 394 395 // We don't use the second service, but we have to still configure it 396 SecondPrintService.setCallbacks(createMockPrintServiceCallbacks(null, null, null)); 397 398 // Create a print adapter that respects the print contract. 399 PrintDocumentAdapter adapter = createDefaultPrintDocumentAdapter(1); 400 401 // Start printing. 402 print(adapter); 403 404 // Open printer selection dropdown list to display icon on screen 405 UiObject destinationSpinner = UiDevice.getInstance(getInstrumentation()) 406 .findObject(new UiSelector().resourceId( 407 "com.android.printspooler:id/destination_spinner")); 408 destinationSpinner.click(); 409 410 // Get the print service's icon 411 PackageManager packageManager = getActivity().getPackageManager(); 412 PackageInfo packageInfo = packageManager.getPackageInfo( 413 new ComponentName(getActivity(), FirstPrintService.class).getPackageName(), 0); 414 ApplicationInfo appInfo = packageInfo.applicationInfo; 415 Drawable printServiceIcon = appInfo.loadIcon(packageManager); 416 417 assertThatIconIs(renderDrawable(printServiceIcon)); 418 419 // Update icon to resource 420 updatePrinter(sessionCallbacks, 421 (new PrinterInfo.Builder(sPrinter)).setIconResourceId(R.drawable.red_printer) 422 .build()); 423 424 assertThatIconIs(renderDrawable(getActivity().getDrawable(R.drawable.red_printer))); 425 426 // Update icon to bitmap 427 Bitmap bm = BitmapFactory.decodeResource(getActivity().getResources(), 428 R.raw.yellow); 429 // Icon will be picked up from the discovery session once setHasCustomPrinterIcon is set 430 mIcon = Icon.createWithBitmap(bm); 431 updatePrinter(sessionCallbacks, 432 (new PrinterInfo.Builder(sPrinter)).setHasCustomPrinterIcon(true).build()); 433 434 assertThatIconIs(renderDrawable(mIcon.loadDrawable(getActivity()))); 435 436 getUiDevice().pressBack(); 437 getUiDevice().pressBack(); 438 waitForPrinterDiscoverySessionDestroyCallbackCalled(1); 439 } 440 441 /** 442 * Test that we cannot call attachBaseContext 443 * 444 * @throws Throwable If anything is unexpected. 445 */ 446 @Test 447 public void cannotUseAttachBaseContext() throws Throwable { 448 // Create the session callbacks that we will be checking. 449 final PrinterDiscoverySessionCallbacks sessionCallbacks 450 = createMockPrinterDiscoverySessionCallbacks(PRINTER_NAME, null); 451 452 // Create the service callbacks for the first print service. 453 PrintServiceCallbacks serviceCallbacks = createMockPrinterServiceCallbacks( 454 sessionCallbacks); 455 456 // Configure the print services. 457 FirstPrintService.setCallbacks(serviceCallbacks); 458 459 // Create a print adapter that respects the print contract. 460 PrintDocumentAdapter adapter = createDefaultPrintDocumentAdapter(1); 461 462 // We don't use the second service, but we have to still configure it 463 SecondPrintService.setCallbacks(createMockPrintServiceCallbacks(null, null, null)); 464 465 // Start printing to set serviceCallbacks.getService() 466 print(adapter); 467 eventually(() -> assertNotNull(serviceCallbacks.getService())); 468 469 // attachBaseContext should always throw an exception no matter what input value 470 assertException(() -> serviceCallbacks.getService().callAttachBaseContext(null), 471 IllegalStateException.class); 472 assertException(() -> serviceCallbacks.getService().callAttachBaseContext(getActivity()), 473 IllegalStateException.class); 474 475 getUiDevice().pressBack(); 476 getUiDevice().pressBack(); 477 waitForPrinterDiscoverySessionDestroyCallbackCalled(1); 478 } 479 480 /** 481 * Test that the active print jobs can be read 482 * 483 * @throws Throwable If anything is unexpected. 484 */ 485 @Test 486 public void getActivePrintJobs() throws Throwable { 487 clearPrintSpoolerData(); 488 489 try { 490 PrintManager pm = (PrintManager) getActivity().getSystemService(Context.PRINT_SERVICE); 491 492 // Configure first print service 493 PrinterDiscoverySessionCallbacks sessionCallbacks1 494 = createMockPrinterDiscoverySessionCallbacks("Printer1", null); 495 PrintServiceCallbacks serviceCallbacks1 = createMockPrinterServiceCallbacks( 496 sessionCallbacks1); 497 FirstPrintService.setCallbacks(serviceCallbacks1); 498 499 // Configure second print service 500 PrinterDiscoverySessionCallbacks sessionCallbacks2 501 = createMockPrinterDiscoverySessionCallbacks("Printer2", null); 502 PrintServiceCallbacks serviceCallbacks2 = createMockPrinterServiceCallbacks( 503 sessionCallbacks2); 504 SecondPrintService.setCallbacks(serviceCallbacks2); 505 506 // Create a print adapter that respects the print contract. 507 PrintDocumentAdapter adapter = createDefaultPrintDocumentAdapter(1); 508 509 runOnMainThread(() -> pm.print("job1", adapter, null)); 510 511 // Init services 512 waitForPrinterDiscoverySessionCreateCallbackCalled(); 513 StubbablePrintService firstService = serviceCallbacks1.getService(); 514 515 waitForWriteAdapterCallback(1); 516 selectPrinter("Printer1"); 517 518 // Job is not yet confirmed, hence it is not yet "active" 519 runOnMainThread(() -> assertEquals(0, firstService.callGetActivePrintJobs().size())); 520 521 clickPrintButton(); 522 answerPrintServicesWarning(true); 523 onPrintJobQueuedCalled(); 524 525 eventually(() -> runOnMainThread( 526 () -> assertEquals(1, firstService.callGetActivePrintJobs().size()))); 527 528 // Add another print job to first service 529 resetCounters(); 530 runOnMainThread(() -> pm.print("job2", adapter, null)); 531 waitForWriteAdapterCallback(1); 532 clickPrintButton(); 533 onPrintJobQueuedCalled(); 534 535 eventually(() -> runOnMainThread( 536 () -> assertEquals(2, firstService.callGetActivePrintJobs().size()))); 537 538 // Create print job in second service 539 resetCounters(); 540 runOnMainThread(() -> pm.print("job3", adapter, null)); 541 542 waitForPrinterDiscoverySessionCreateCallbackCalled(); 543 544 StubbablePrintService secondService = serviceCallbacks2.getService(); 545 runOnMainThread(() -> assertEquals(0, secondService.callGetActivePrintJobs().size())); 546 547 waitForWriteAdapterCallback(1); 548 selectPrinter("Printer2"); 549 clickPrintButton(); 550 answerPrintServicesWarning(true); 551 onPrintJobQueuedCalled(); 552 553 eventually(() -> runOnMainThread( 554 () -> assertEquals(1, secondService.callGetActivePrintJobs().size()))); 555 runOnMainThread(() -> assertEquals(2, firstService.callGetActivePrintJobs().size())); 556 557 // Block last print job. Blocked jobs are still considered active 558 runOnMainThread(() -> sPrintJob.block(null)); 559 eventually(() -> runOnMainThread(() -> assertTrue(sPrintJob.isBlocked()))); 560 runOnMainThread(() -> assertEquals(1, secondService.callGetActivePrintJobs().size())); 561 562 // Fail last print job. Failed job are not active 563 runOnMainThread(() -> sPrintJob.fail(null)); 564 eventually(() -> runOnMainThread(() -> assertTrue(sPrintJob.isFailed()))); 565 runOnMainThread(() -> assertEquals(0, secondService.callGetActivePrintJobs().size())); 566 567 // Cancel job. Canceled jobs are not active 568 runOnMainThread(() -> assertEquals(2, firstService.callGetActivePrintJobs().size())); 569 android.print.PrintJob job2 = getPrintJob(pm, "job2"); 570 runOnMainThread(job2::cancel); 571 eventually(() -> runOnMainThread(() -> assertTrue(job2.isCancelled()))); 572 runOnMainThread(() -> assertEquals(1, firstService.callGetActivePrintJobs().size())); 573 574 waitForPrinterDiscoverySessionDestroyCallbackCalled(1); 575 } finally { 576 clearPrintSpoolerData(); 577 } 578 } 579 580 /** 581 * Test that the icon get be updated. 582 * 583 * @throws Throwable If anything is unexpected. 584 */ 585 @Test 586 public void selectViaInfoIntent() throws Throwable { 587 ArrayList<String> trackedPrinters = new ArrayList<>(); 588 589 // Create the session callbacks that we will be checking. 590 final PrinterDiscoverySessionCallbacks sessionCallbacks 591 = createMockPrinterDiscoverySessionCallbacks(PRINTER_NAME, trackedPrinters); 592 593 // Create the service callbacks for the first print service. 594 PrintServiceCallbacks serviceCallbacks = createMockPrinterServiceCallbacks( 595 sessionCallbacks); 596 597 // Configure the print services. 598 FirstPrintService.setCallbacks(serviceCallbacks); 599 600 // We don't use the second service, but we have to still configure it 601 SecondPrintService.setCallbacks(createMockPrintServiceCallbacks(null, null, null)); 602 603 // Create a print adapter that respects the print contract. 604 PrintDocumentAdapter adapter = createDefaultPrintDocumentAdapter(1); 605 606 // Start printing. 607 print(adapter); 608 609 // Enter select printer activity 610 selectPrinter("All printers…"); 611 612 assertFalse(trackedPrinters.contains(PRINTER_NAME)); 613 614 InfoActivity.addObserver(activity -> { 615 Intent intent = activity.getIntent(); 616 617 assertEquals(PRINTER_NAME, intent.getStringExtra("PRINTER_NAME")); 618 assertTrue(intent.getBooleanExtra(PrintService.EXTRA_CAN_SELECT_PRINTER, 619 false)); 620 621 activity.setResult(Activity.RESULT_OK, 622 (new Intent()).putExtra(PrintService.EXTRA_SELECT_PRINTER, true)); 623 activity.finish(); 624 }); 625 626 // Open info activity which executed the code above 627 UiObject moreInfoButton = getUiDevice().findObject(new UiSelector().resourceId( 628 "com.android.printspooler:id/more_info")); 629 moreInfoButton.click(); 630 631 // Wait until printer is selected and thereby tracked 632 eventually(() -> assertTrue(trackedPrinters.contains(PRINTER_NAME))); 633 634 InfoActivity.clearObservers(); 635 636 getUiDevice().pressBack(); 637 getUiDevice().pressBack(); 638 waitForPrinterDiscoverySessionDestroyCallbackCalled(1); 639 } 640 } 641