1 /* 2 * Copyright (C) 2014 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.test; 18 19 import static android.content.pm.PackageManager.GET_META_DATA; 20 import static android.content.pm.PackageManager.GET_SERVICES; 21 import static android.print.test.Utils.getPrintManager; 22 23 import static org.junit.Assert.assertFalse; 24 import static org.junit.Assert.assertTrue; 25 import static org.junit.Assert.fail; 26 import static org.junit.Assume.assumeTrue; 27 import static org.mockito.Matchers.any; 28 import static org.mockito.Matchers.eq; 29 import static org.mockito.Mockito.doAnswer; 30 import static org.mockito.Mockito.doCallRealMethod; 31 import static org.mockito.Mockito.mock; 32 import static org.mockito.Mockito.when; 33 import static org.mockito.hamcrest.MockitoHamcrest.argThat; 34 35 import android.app.Activity; 36 import android.app.Instrumentation; 37 import android.content.ComponentName; 38 import android.content.Context; 39 import android.content.Intent; 40 import android.content.pm.PackageManager; 41 import android.content.pm.ResolveInfo; 42 import android.graphics.pdf.PdfDocument; 43 import android.os.Bundle; 44 import android.os.CancellationSignal; 45 import android.os.ParcelFileDescriptor; 46 import android.os.SystemClock; 47 import android.print.PageRange; 48 import android.print.PrintAttributes; 49 import android.print.PrintDocumentAdapter; 50 import android.print.PrintDocumentAdapter.LayoutResultCallback; 51 import android.print.PrintDocumentAdapter.WriteResultCallback; 52 import android.print.PrintDocumentInfo; 53 import android.print.PrintManager; 54 import android.print.PrinterId; 55 import android.print.pdf.PrintedPdfDocument; 56 import android.print.test.services.PrintServiceCallbacks; 57 import android.print.test.services.PrinterDiscoverySessionCallbacks; 58 import android.print.test.services.StubbablePrintService; 59 import android.print.test.services.StubbablePrinterDiscoverySession; 60 import android.printservice.CustomPrinterIconCallback; 61 import android.printservice.PrintJob; 62 import android.provider.Settings; 63 import android.support.test.InstrumentationRegistry; 64 import android.support.test.uiautomator.By; 65 import android.support.test.uiautomator.UiDevice; 66 import android.support.test.uiautomator.UiObject; 67 import android.support.test.uiautomator.UiObjectNotFoundException; 68 import android.support.test.uiautomator.UiSelector; 69 import android.util.Log; 70 import android.util.SparseArray; 71 72 import androidx.annotation.NonNull; 73 import androidx.annotation.Nullable; 74 75 import com.android.compatibility.common.util.SystemUtil; 76 77 import org.hamcrest.BaseMatcher; 78 import org.hamcrest.Description; 79 import org.junit.After; 80 import org.junit.AfterClass; 81 import org.junit.Before; 82 import org.junit.BeforeClass; 83 import org.junit.Rule; 84 import org.junit.rules.TestRule; 85 import org.junit.runners.model.Statement; 86 import org.mockito.InOrder; 87 import org.mockito.stubbing.Answer; 88 89 import java.io.BufferedReader; 90 import java.io.ByteArrayOutputStream; 91 import java.io.FileInputStream; 92 import java.io.FileOutputStream; 93 import java.io.IOException; 94 import java.io.InputStreamReader; 95 import java.lang.annotation.Annotation; 96 import java.lang.annotation.ElementType; 97 import java.lang.annotation.Retention; 98 import java.lang.annotation.RetentionPolicy; 99 import java.lang.annotation.Target; 100 import java.util.ArrayList; 101 import java.util.List; 102 import java.util.concurrent.TimeoutException; 103 import java.util.concurrent.atomic.AtomicInteger; 104 105 /** 106 * This is the base class for print tests. 107 */ 108 public abstract class BasePrintTest { 109 private static final String LOG_TAG = "BasePrintTest"; 110 111 protected static final long OPERATION_TIMEOUT_MILLIS = 60000; 112 protected static final String PRINT_JOB_NAME = "Test"; 113 static final String TEST_ID = "BasePrintTest.EXTRA_TEST_ID"; 114 115 private static final String PRINT_SPOOLER_PACKAGE_NAME = "com.android.printspooler"; 116 private static final String PM_CLEAR_SUCCESS_OUTPUT = "Success"; 117 private static final String COMMAND_LIST_ENABLED_IME_COMPONENTS = "ime list -s"; 118 private static final String COMMAND_PREFIX_ENABLE_IME = "ime enable "; 119 private static final String COMMAND_PREFIX_DISABLE_IME = "ime disable "; 120 private static final int CURRENT_USER_ID = -2; // Mirrors UserHandle.USER_CURRENT 121 private static final String PRINTSPOOLER_PACKAGE = "com.android.printspooler"; 122 123 private static final AtomicInteger sLastTestID = new AtomicInteger(); 124 private int mTestId; 125 private PrintDocumentActivity mActivity; 126 127 private static String sDisabledPrintServicesBefore; 128 129 private static final SparseArray<BasePrintTest> sIdToTest = new SparseArray<>(); 130 131 public final @Rule ShouldStartActivity mShouldStartActivityRule = new ShouldStartActivity(); 132 133 /** 134 * Return the UI device 135 * 136 * @return the UI device 137 */ getUiDevice()138 public static UiDevice getUiDevice() { 139 return UiDevice.getInstance(getInstrumentation()); 140 } 141 142 private CallCounter mCancelOperationCounter; 143 private CallCounter mLayoutCallCounter; 144 private CallCounter mWriteCallCounter; 145 private CallCounter mWriteCancelCallCounter; 146 private CallCounter mStartCallCounter; 147 private CallCounter mFinishCallCounter; 148 private CallCounter mPrintJobQueuedCallCounter; 149 private CallCounter mCreateSessionCallCounter; 150 private CallCounter mDestroySessionCallCounter; 151 private CallCounter mDestroyActivityCallCounter = new CallCounter(); 152 private CallCounter mCreateActivityCallCounter = new CallCounter(); 153 154 private static String[] sEnabledImes; 155 getEnabledImes()156 private static String[] getEnabledImes() throws IOException { 157 List<String> imeList = new ArrayList<>(); 158 159 ParcelFileDescriptor pfd = getInstrumentation().getUiAutomation() 160 .executeShellCommand(COMMAND_LIST_ENABLED_IME_COMPONENTS); 161 try (BufferedReader reader = new BufferedReader( 162 new InputStreamReader(new FileInputStream(pfd.getFileDescriptor())))) { 163 164 String line; 165 while ((line = reader.readLine()) != null) { 166 imeList.add(line); 167 } 168 } 169 170 String[] imeArray = new String[imeList.size()]; 171 imeList.toArray(imeArray); 172 173 return imeArray; 174 } 175 disableImes()176 private static void disableImes() throws Exception { 177 sEnabledImes = getEnabledImes(); 178 for (String ime : sEnabledImes) { 179 String disableImeCommand = COMMAND_PREFIX_DISABLE_IME + ime; 180 SystemUtil.runShellCommand(getInstrumentation(), disableImeCommand); 181 } 182 } 183 enableImes()184 private static void enableImes() throws Exception { 185 for (String ime : sEnabledImes) { 186 String enableImeCommand = COMMAND_PREFIX_ENABLE_IME + ime; 187 SystemUtil.runShellCommand(getInstrumentation(), enableImeCommand); 188 } 189 sEnabledImes = null; 190 } 191 getInstrumentation()192 protected static Instrumentation getInstrumentation() { 193 return InstrumentationRegistry.getInstrumentation(); 194 } 195 196 @BeforeClass setUpClass()197 public static void setUpClass() throws Exception { 198 Log.d(LOG_TAG, "setUpClass()"); 199 200 Instrumentation instrumentation = getInstrumentation(); 201 202 // Make sure we start with a clean slate. 203 Log.d(LOG_TAG, "clearPrintSpoolerData()"); 204 clearPrintSpoolerData(); 205 Log.d(LOG_TAG, "disableImes()"); 206 disableImes(); 207 Log.d(LOG_TAG, "disablePrintServices()"); 208 disablePrintServices(instrumentation.getTargetContext().getPackageName()); 209 210 // Workaround for dexmaker bug: https://code.google.com/p/dexmaker/issues/detail?id=2 211 // Dexmaker is used by mockito. 212 System.setProperty("dexmaker.dexcache", instrumentation 213 .getTargetContext().getCacheDir().getPath()); 214 215 Log.d(LOG_TAG, "setUpClass() done"); 216 } 217 218 /** 219 * Disable all print services beside the ones we want to leave enabled. 220 * 221 * @param packageToLeaveEnabled The package of the services to leave enabled. 222 */ disablePrintServices(@onNull String packageToLeaveEnabled)223 private static void disablePrintServices(@NonNull String packageToLeaveEnabled) 224 throws IOException { 225 Instrumentation instrumentation = getInstrumentation(); 226 227 sDisabledPrintServicesBefore = SystemUtil.runShellCommand(instrumentation, 228 "settings get secure " + Settings.Secure.DISABLED_PRINT_SERVICES); 229 230 Intent printServiceIntent = new Intent(android.printservice.PrintService.SERVICE_INTERFACE); 231 List<ResolveInfo> installedServices = instrumentation.getContext().getPackageManager() 232 .queryIntentServices(printServiceIntent, GET_SERVICES | GET_META_DATA); 233 234 StringBuilder builder = new StringBuilder(); 235 for (ResolveInfo service : installedServices) { 236 if (packageToLeaveEnabled.equals(service.serviceInfo.packageName)) { 237 continue; 238 } 239 if (builder.length() > 0) { 240 builder.append(":"); 241 } 242 builder.append(new ComponentName(service.serviceInfo.packageName, 243 service.serviceInfo.name).flattenToString()); 244 } 245 246 SystemUtil.runShellCommand(instrumentation, "settings put secure " 247 + Settings.Secure.DISABLED_PRINT_SERVICES + " " + builder); 248 } 249 250 /** 251 * Revert {@link #disablePrintServices(String)} 252 */ enablePrintServices()253 private static void enablePrintServices() throws IOException { 254 SystemUtil.runShellCommand(getInstrumentation(), 255 "settings put secure " + Settings.Secure.DISABLED_PRINT_SERVICES + " " 256 + sDisabledPrintServicesBefore); 257 } 258 259 @Before setUp()260 public void setUp() throws Exception { 261 Log.d(LOG_TAG, "setUp()"); 262 263 assumeTrue(getInstrumentation().getContext().getPackageManager().hasSystemFeature( 264 PackageManager.FEATURE_PRINTING)); 265 266 // Prevent rotation 267 getUiDevice().freezeRotation(); 268 while (!getUiDevice().isNaturalOrientation()) { 269 getUiDevice().setOrientationNatural(); 270 getUiDevice().waitForIdle(); 271 } 272 273 // Initialize the latches. 274 Log.d(LOG_TAG, "init counters"); 275 mCancelOperationCounter = new CallCounter(); 276 mLayoutCallCounter = new CallCounter(); 277 mStartCallCounter = new CallCounter(); 278 mFinishCallCounter = new CallCounter(); 279 mWriteCallCounter = new CallCounter(); 280 mWriteCancelCallCounter = new CallCounter(); 281 mFinishCallCounter = new CallCounter(); 282 mPrintJobQueuedCallCounter = new CallCounter(); 283 mCreateSessionCallCounter = new CallCounter(); 284 mDestroySessionCallCounter = new CallCounter(); 285 286 mTestId = sLastTestID.incrementAndGet(); 287 sIdToTest.put(mTestId, this); 288 289 // Create the activity if needed 290 if (!mShouldStartActivityRule.mNoActivity) { 291 createActivity(); 292 } 293 294 Log.d(LOG_TAG, "setUp() done"); 295 } 296 297 @After tearDown()298 public void tearDown() throws Exception { 299 Log.d(LOG_TAG, "tearDown()"); 300 301 finishActivity(); 302 303 sIdToTest.remove(mTestId); 304 305 // Allow rotation 306 getUiDevice().unfreezeRotation(); 307 308 Log.d(LOG_TAG, "tearDown() done"); 309 } 310 311 @AfterClass tearDownClass()312 public static void tearDownClass() throws Exception { 313 Log.d(LOG_TAG, "tearDownClass()"); 314 315 Instrumentation instrumentation = getInstrumentation(); 316 317 Log.d(LOG_TAG, "enablePrintServices()"); 318 enablePrintServices(); 319 320 Log.d(LOG_TAG, "enableImes()"); 321 enableImes(); 322 323 // Make sure the spooler is cleaned, this also un-approves all services 324 Log.d(LOG_TAG, "clearPrintSpoolerData()"); 325 clearPrintSpoolerData(); 326 327 SystemUtil.runShellCommand(instrumentation, "settings put secure " 328 + Settings.Secure.DISABLED_PRINT_SERVICES + " null"); 329 330 Log.d(LOG_TAG, "tearDownClass() done"); 331 } 332 print(@onNull final PrintDocumentAdapter adapter, final PrintAttributes attributes)333 protected android.print.PrintJob print(@NonNull final PrintDocumentAdapter adapter, 334 final PrintAttributes attributes) { 335 android.print.PrintJob[] printJob = new android.print.PrintJob[1]; 336 // Initiate printing as if coming from the app. 337 getInstrumentation().runOnMainSync(() -> { 338 PrintManager printManager = (PrintManager) getActivity() 339 .getSystemService(Context.PRINT_SERVICE); 340 printJob[0] = printManager.print("Print job", adapter, attributes); 341 }); 342 343 return printJob[0]; 344 } 345 print(PrintDocumentAdapter adapter)346 protected void print(PrintDocumentAdapter adapter) { 347 print(adapter, (PrintAttributes) null); 348 } 349 print(PrintDocumentAdapter adapter, String printJobName)350 protected void print(PrintDocumentAdapter adapter, String printJobName) { 351 print(adapter, printJobName, null); 352 } 353 354 /** 355 * Start printing 356 * 357 * @param adapter Adapter supplying data to print 358 * @param printJobName The name of the print job 359 */ print(@onNull PrintDocumentAdapter adapter, @NonNull String printJobName, @Nullable PrintAttributes attributes)360 protected void print(@NonNull PrintDocumentAdapter adapter, @NonNull String printJobName, 361 @Nullable PrintAttributes attributes) { 362 // Initiate printing as if coming from the app. 363 getInstrumentation() 364 .runOnMainSync(() -> getPrintManager(getActivity()).print(printJobName, adapter, 365 attributes)); 366 } 367 onCancelOperationCalled()368 protected void onCancelOperationCalled() { 369 mCancelOperationCounter.call(); 370 } 371 onStartCalled()372 public void onStartCalled() { 373 mStartCallCounter.call(); 374 } 375 onLayoutCalled()376 protected void onLayoutCalled() { 377 mLayoutCallCounter.call(); 378 } 379 getWriteCallCount()380 protected int getWriteCallCount() { 381 return mWriteCallCounter.getCallCount(); 382 } 383 onWriteCalled()384 protected void onWriteCalled() { 385 mWriteCallCounter.call(); 386 } 387 onWriteCancelCalled()388 protected void onWriteCancelCalled() { 389 mWriteCancelCallCounter.call(); 390 } 391 onFinishCalled()392 protected void onFinishCalled() { 393 mFinishCallCounter.call(); 394 } 395 onPrintJobQueuedCalled()396 protected void onPrintJobQueuedCalled() { 397 mPrintJobQueuedCallCounter.call(); 398 } 399 onPrinterDiscoverySessionCreateCalled()400 protected void onPrinterDiscoverySessionCreateCalled() { 401 mCreateSessionCallCounter.call(); 402 } 403 onPrinterDiscoverySessionDestroyCalled()404 protected void onPrinterDiscoverySessionDestroyCalled() { 405 mDestroySessionCallCounter.call(); 406 } 407 waitForCancelOperationCallbackCalled()408 protected void waitForCancelOperationCallbackCalled() { 409 waitForCallbackCallCount(mCancelOperationCounter, 1, 410 "Did not get expected call to onCancel for the current operation."); 411 } 412 waitForPrinterDiscoverySessionCreateCallbackCalled()413 protected void waitForPrinterDiscoverySessionCreateCallbackCalled() { 414 waitForCallbackCallCount(mCreateSessionCallCounter, 1, 415 "Did not get expected call to onCreatePrinterDiscoverySession."); 416 } 417 waitForPrinterDiscoverySessionDestroyCallbackCalled(int count)418 public void waitForPrinterDiscoverySessionDestroyCallbackCalled(int count) { 419 waitForCallbackCallCount(mDestroySessionCallCounter, count, 420 "Did not get expected call to onDestroyPrinterDiscoverySession."); 421 } 422 waitForServiceOnPrintJobQueuedCallbackCalled(int count)423 protected void waitForServiceOnPrintJobQueuedCallbackCalled(int count) { 424 waitForCallbackCallCount(mPrintJobQueuedCallCounter, count, 425 "Did not get expected call to onPrintJobQueued."); 426 } 427 waitForAdapterStartCallbackCalled()428 protected void waitForAdapterStartCallbackCalled() { 429 waitForCallbackCallCount(mStartCallCounter, 1, 430 "Did not get expected call to start."); 431 } 432 waitForAdapterFinishCallbackCalled()433 protected void waitForAdapterFinishCallbackCalled() { 434 waitForCallbackCallCount(mFinishCallCounter, 1, 435 "Did not get expected call to finish."); 436 } 437 waitForLayoutAdapterCallbackCount(int count)438 protected void waitForLayoutAdapterCallbackCount(int count) { 439 waitForCallbackCallCount(mLayoutCallCounter, count, 440 "Did not get expected call to layout."); 441 } 442 waitForWriteAdapterCallback(int count)443 public void waitForWriteAdapterCallback(int count) { 444 waitForCallbackCallCount(mWriteCallCounter, count, "Did not get expected call to write."); 445 } 446 waitForWriteCancelCallback(int count)447 protected void waitForWriteCancelCallback(int count) { 448 waitForCallbackCallCount(mWriteCancelCallCounter, count, 449 "Did not get expected cancel of write."); 450 } 451 waitForCallbackCallCount(CallCounter counter, int count, String message)452 private static void waitForCallbackCallCount(CallCounter counter, int count, String message) { 453 try { 454 counter.waitForCount(count, OPERATION_TIMEOUT_MILLIS); 455 } catch (TimeoutException te) { 456 fail(message); 457 } 458 } 459 460 /** 461 * Indicate the print activity was created. 462 */ onActivityCreateCalled(int testId, PrintDocumentActivity activity)463 static void onActivityCreateCalled(int testId, PrintDocumentActivity activity) { 464 synchronized (sIdToTest) { 465 BasePrintTest test = sIdToTest.get(testId); 466 if (test != null) { 467 test.mActivity = activity; 468 test.mCreateActivityCallCounter.call(); 469 } 470 } 471 } 472 473 /** 474 * Indicate the print activity was destroyed. 475 */ onActivityDestroyCalled(int testId)476 static void onActivityDestroyCalled(int testId) { 477 synchronized (sIdToTest) { 478 BasePrintTest test = sIdToTest.get(testId); 479 if (test != null) { 480 test.mDestroyActivityCallCounter.call(); 481 } 482 } 483 } 484 finishActivity()485 private void finishActivity() { 486 Activity activity = mActivity; 487 488 if (activity != null) { 489 if (!activity.isFinishing()) { 490 activity.finish(); 491 } 492 493 while (!activity.isDestroyed()) { 494 int creates = mCreateActivityCallCounter.getCallCount(); 495 waitForCallbackCallCount(mDestroyActivityCallCounter, creates, 496 "Activity was not destroyed"); 497 } 498 } 499 } 500 501 /** 502 * Get the number of ties the print activity was destroyed. 503 * 504 * @return The number of onDestroy calls on the print activity. 505 */ getActivityDestroyCallbackCallCount()506 protected int getActivityDestroyCallbackCallCount() { 507 return mDestroyActivityCallCounter.getCallCount(); 508 } 509 510 /** 511 * Get the number of ties the print activity was created. 512 * 513 * @return The number of onCreate calls on the print activity. 514 */ getActivityCreateCallbackCallCount()515 private int getActivityCreateCallbackCallCount() { 516 return mCreateActivityCallCounter.getCallCount(); 517 } 518 519 /** 520 * Wait until create was called {@code count} times. 521 * 522 * @param count The number of create calls to expect. 523 */ waitForActivityCreateCallbackCalled(int count)524 private void waitForActivityCreateCallbackCalled(int count) { 525 waitForCallbackCallCount(mCreateActivityCallCounter, count, 526 "Did not get expected call to create."); 527 } 528 529 /** 530 * Reset all counters. 531 */ resetCounters()532 public void resetCounters() { 533 mCancelOperationCounter.reset(); 534 mLayoutCallCounter.reset(); 535 mWriteCallCounter.reset(); 536 mWriteCancelCallCounter.reset(); 537 mStartCallCounter.reset(); 538 mFinishCallCounter.reset(); 539 mPrintJobQueuedCallCounter.reset(); 540 mCreateSessionCallCounter.reset(); 541 mDestroySessionCallCounter.reset(); 542 mDestroyActivityCallCounter.reset(); 543 mCreateActivityCallCounter.reset(); 544 } 545 546 /** 547 * Wait until the message is shown that indicates that a printer is unavailable. 548 * 549 * @throws Exception If anything was unexpected. 550 */ waitForPrinterUnavailable()551 protected void waitForPrinterUnavailable() throws Exception { 552 final String printerUnavailableMessage = "This printer isn\'t available right now."; 553 554 UiObject message = getUiDevice().findObject(new UiSelector().resourceId( 555 "com.android.printspooler:id/message")); 556 if (!message.getText().equals(printerUnavailableMessage)) { 557 throw new Exception("Wrong message: " + message.getText() + " instead of " 558 + printerUnavailableMessage); 559 } 560 } 561 selectPrinter(String printerName)562 protected void selectPrinter(String printerName) throws UiObjectNotFoundException, IOException { 563 try { 564 long delay = 1; 565 while (true) { 566 try { 567 UiDevice uiDevice = getUiDevice(); 568 UiObject destinationSpinner = uiDevice.findObject(new UiSelector() 569 .resourceId("com.android.printspooler:id/destination_spinner")); 570 571 destinationSpinner.click(); 572 getUiDevice().waitForIdle(); 573 574 // Give spinner some time to expand 575 try { 576 Thread.sleep(delay); 577 } catch (InterruptedException e) { 578 // ignore 579 } 580 581 // try to select printer 582 UiObject printerOption = uiDevice.findObject( 583 new UiSelector().text(printerName)); 584 printerOption.click(); 585 } catch (UiObjectNotFoundException e) { 586 Log.e(LOG_TAG, "Could not select printer " + printerName, e); 587 } 588 589 getUiDevice().waitForIdle(); 590 591 if (!printerName.equals("All printers…")) { 592 // Make sure printer is selected 593 if (getUiDevice().hasObject(By.text(printerName))) { 594 break; 595 } else { 596 if (delay <= OPERATION_TIMEOUT_MILLIS) { 597 Log.w(LOG_TAG, "Cannot find printer " + printerName + ", retrying."); 598 delay *= 2; 599 } else { 600 throw new UiObjectNotFoundException( 601 "Could find printer " + printerName 602 + " even though we retried"); 603 } 604 } 605 } else { 606 break; 607 } 608 } 609 } catch (UiObjectNotFoundException e) { 610 dumpWindowHierarchy(); 611 throw e; 612 } 613 } 614 answerPrintServicesWarning(boolean confirm)615 protected void answerPrintServicesWarning(boolean confirm) throws UiObjectNotFoundException { 616 UiObject button; 617 if (confirm) { 618 button = getUiDevice().findObject(new UiSelector().resourceId("android:id/button1")); 619 } else { 620 button = getUiDevice().findObject(new UiSelector().resourceId("android:id/button2")); 621 } 622 button.click(); 623 } 624 changeOrientation(String orientation)625 protected void changeOrientation(String orientation) throws UiObjectNotFoundException, 626 IOException { 627 try { 628 UiDevice uiDevice = getUiDevice(); 629 UiObject orientationSpinner = uiDevice.findObject(new UiSelector().resourceId( 630 "com.android.printspooler:id/orientation_spinner")); 631 orientationSpinner.click(); 632 UiObject orientationOption = uiDevice.findObject(new UiSelector().text(orientation)); 633 orientationOption.click(); 634 } catch (UiObjectNotFoundException e) { 635 dumpWindowHierarchy(); 636 throw e; 637 } 638 } 639 getOrientation()640 public String getOrientation() throws UiObjectNotFoundException, IOException { 641 try { 642 UiObject orientationSpinner = getUiDevice().findObject(new UiSelector().resourceId( 643 "com.android.printspooler:id/orientation_spinner")); 644 return orientationSpinner.getText(); 645 } catch (UiObjectNotFoundException e) { 646 dumpWindowHierarchy(); 647 throw e; 648 } 649 } 650 changeMediaSize(String mediaSize)651 protected void changeMediaSize(String mediaSize) throws UiObjectNotFoundException, IOException { 652 try { 653 UiDevice uiDevice = getUiDevice(); 654 UiObject mediaSizeSpinner = uiDevice.findObject(new UiSelector().resourceId( 655 "com.android.printspooler:id/paper_size_spinner")); 656 mediaSizeSpinner.click(); 657 UiObject mediaSizeOption = uiDevice.findObject(new UiSelector().text(mediaSize)); 658 mediaSizeOption.click(); 659 } catch (UiObjectNotFoundException e) { 660 dumpWindowHierarchy(); 661 throw e; 662 } 663 } 664 changeColor(String color)665 protected void changeColor(String color) throws UiObjectNotFoundException, IOException { 666 try { 667 UiDevice uiDevice = getUiDevice(); 668 UiObject colorSpinner = uiDevice.findObject(new UiSelector().resourceId( 669 "com.android.printspooler:id/color_spinner")); 670 colorSpinner.click(); 671 UiObject colorOption = uiDevice.findObject(new UiSelector().text(color)); 672 colorOption.click(); 673 } catch (UiObjectNotFoundException e) { 674 dumpWindowHierarchy(); 675 throw e; 676 } 677 } 678 getColor()679 public String getColor() throws UiObjectNotFoundException, IOException { 680 try { 681 UiObject colorSpinner = getUiDevice().findObject(new UiSelector().resourceId( 682 "com.android.printspooler:id/color_spinner")); 683 return colorSpinner.getText(); 684 } catch (UiObjectNotFoundException e) { 685 dumpWindowHierarchy(); 686 throw e; 687 } 688 } 689 changeDuplex(String duplex)690 protected void changeDuplex(String duplex) throws UiObjectNotFoundException, IOException { 691 try { 692 UiDevice uiDevice = getUiDevice(); 693 UiObject duplexSpinner = uiDevice.findObject(new UiSelector().resourceId( 694 "com.android.printspooler:id/duplex_spinner")); 695 duplexSpinner.click(); 696 UiObject duplexOption = uiDevice.findObject(new UiSelector().text(duplex)); 697 duplexOption.click(); 698 } catch (UiObjectNotFoundException e) { 699 dumpWindowHierarchy(); 700 throw e; 701 } 702 } 703 changeCopies(int newCopies)704 protected void changeCopies(int newCopies) throws UiObjectNotFoundException, IOException { 705 try { 706 UiObject copies = getUiDevice().findObject(new UiSelector().resourceId( 707 "com.android.printspooler:id/copies_edittext")); 708 copies.setText(Integer.valueOf(newCopies).toString()); 709 } catch (UiObjectNotFoundException e) { 710 dumpWindowHierarchy(); 711 throw e; 712 } 713 } 714 getCopies()715 protected String getCopies() throws UiObjectNotFoundException, IOException { 716 try { 717 UiObject copies = getUiDevice().findObject(new UiSelector().resourceId( 718 "com.android.printspooler:id/copies_edittext")); 719 return copies.getText(); 720 } catch (UiObjectNotFoundException e) { 721 dumpWindowHierarchy(); 722 throw e; 723 } 724 } 725 assertNoPrintButton()726 protected void assertNoPrintButton() throws UiObjectNotFoundException, IOException { 727 assertFalse(getUiDevice().hasObject(By.res("com.android.printspooler:id/print_button"))); 728 } 729 clickPrintButton()730 public void clickPrintButton() throws UiObjectNotFoundException, IOException { 731 try { 732 UiObject printButton = getUiDevice().findObject(new UiSelector().resourceId( 733 "com.android.printspooler:id/print_button")); 734 printButton.click(); 735 } catch (UiObjectNotFoundException e) { 736 dumpWindowHierarchy(); 737 throw e; 738 } 739 } 740 clickRetryButton()741 protected void clickRetryButton() throws UiObjectNotFoundException, IOException { 742 try { 743 UiObject retryButton = getUiDevice().findObject(new UiSelector().resourceId( 744 "com.android.printspooler:id/action_button")); 745 retryButton.click(); 746 } catch (UiObjectNotFoundException e) { 747 dumpWindowHierarchy(); 748 throw e; 749 } 750 } 751 dumpWindowHierarchy()752 public void dumpWindowHierarchy() throws IOException { 753 ByteArrayOutputStream os = new ByteArrayOutputStream(); 754 getUiDevice().dumpWindowHierarchy(os); 755 756 Log.w(LOG_TAG, "Window hierarchy:"); 757 for (String line : os.toString("UTF-8").split("\n")) { 758 Log.w(LOG_TAG, line); 759 } 760 } 761 getActivity()762 protected PrintDocumentActivity getActivity() { 763 return mActivity; 764 } 765 createActivity()766 protected void createActivity() throws IOException { 767 Log.d(LOG_TAG, "createActivity()"); 768 769 int createBefore = getActivityCreateCallbackCallCount(); 770 771 Intent intent = new Intent(Intent.ACTION_MAIN); 772 intent.putExtra(TEST_ID, mTestId); 773 774 Instrumentation instrumentation = getInstrumentation(); 775 776 // Unlock screen. 777 SystemUtil.runShellCommand(instrumentation, "input keyevent KEYCODE_WAKEUP"); 778 779 intent.setClassName(instrumentation.getTargetContext().getPackageName(), 780 PrintDocumentActivity.class.getName()); 781 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 782 instrumentation.startActivitySync(intent); 783 784 waitForActivityCreateCallbackCalled(createBefore + 1); 785 } 786 openPrintOptions()787 protected void openPrintOptions() throws UiObjectNotFoundException { 788 UiObject expandHandle = getUiDevice().findObject(new UiSelector().resourceId( 789 "com.android.printspooler:id/expand_collapse_handle")); 790 expandHandle.click(); 791 } 792 openCustomPrintOptions()793 protected void openCustomPrintOptions() throws UiObjectNotFoundException { 794 UiObject expandHandle = getUiDevice().findObject(new UiSelector().resourceId( 795 "com.android.printspooler:id/more_options_button")); 796 expandHandle.click(); 797 } 798 clearPrintSpoolerData()799 protected static void clearPrintSpoolerData() throws Exception { 800 if (getInstrumentation().getContext().getPackageManager().hasSystemFeature( 801 PackageManager.FEATURE_PRINTING)) { 802 assertTrue("failed to clear print spooler data", 803 SystemUtil.runShellCommand(getInstrumentation(), String.format( 804 "pm clear --user %d %s", CURRENT_USER_ID, PRINT_SPOOLER_PACKAGE_NAME)) 805 .contains(PM_CLEAR_SUCCESS_OUTPUT)); 806 } 807 } 808 verifyLayoutCall(InOrder inOrder, PrintDocumentAdapter mock, PrintAttributes oldAttributes, PrintAttributes newAttributes, final boolean forPreview)809 protected void verifyLayoutCall(InOrder inOrder, PrintDocumentAdapter mock, 810 PrintAttributes oldAttributes, PrintAttributes newAttributes, 811 final boolean forPreview) { 812 inOrder.verify(mock).onLayout(eq(oldAttributes), eq(newAttributes), 813 any(CancellationSignal.class), any(LayoutResultCallback.class), argThat( 814 new BaseMatcher<Bundle>() { 815 @Override 816 public boolean matches(Object item) { 817 Bundle bundle = (Bundle) item; 818 return forPreview == bundle.getBoolean( 819 PrintDocumentAdapter.EXTRA_PRINT_PREVIEW); 820 } 821 822 @Override 823 public void describeTo(Description description) { 824 /* do nothing */ 825 } 826 })); 827 } 828 createMockPrintDocumentAdapter(Answer<Void> layoutAnswer, Answer<Void> writeAnswer, Answer<Void> finishAnswer)829 protected PrintDocumentAdapter createMockPrintDocumentAdapter(Answer<Void> layoutAnswer, 830 Answer<Void> writeAnswer, Answer<Void> finishAnswer) { 831 // Create a mock print adapter. 832 PrintDocumentAdapter adapter = mock(PrintDocumentAdapter.class); 833 if (layoutAnswer != null) { 834 doAnswer(layoutAnswer).when(adapter).onLayout(any(PrintAttributes.class), 835 any(PrintAttributes.class), any(CancellationSignal.class), 836 any(LayoutResultCallback.class), any(Bundle.class)); 837 } 838 if (writeAnswer != null) { 839 doAnswer(writeAnswer).when(adapter).onWrite(any(PageRange[].class), 840 any(ParcelFileDescriptor.class), any(CancellationSignal.class), 841 any(WriteResultCallback.class)); 842 } 843 if (finishAnswer != null) { 844 doAnswer(finishAnswer).when(adapter).onFinish(); 845 } 846 return adapter; 847 } 848 849 /** 850 * Create a mock {@link PrintDocumentAdapter} that provides one empty page. 851 * 852 * @return The mock adapter 853 */ createDefaultPrintDocumentAdapter(int numPages)854 public @NonNull PrintDocumentAdapter createDefaultPrintDocumentAdapter(int numPages) { 855 final PrintAttributes[] printAttributes = new PrintAttributes[1]; 856 857 return createMockPrintDocumentAdapter( 858 invocation -> { 859 PrintAttributes oldAttributes = (PrintAttributes) invocation.getArguments()[0]; 860 printAttributes[0] = (PrintAttributes) invocation.getArguments()[1]; 861 LayoutResultCallback callback = 862 (LayoutResultCallback) invocation 863 .getArguments()[3]; 864 865 callback.onLayoutFinished(new PrintDocumentInfo.Builder(PRINT_JOB_NAME) 866 .setPageCount(numPages).build(), 867 !oldAttributes.equals(printAttributes[0])); 868 869 oldAttributes = printAttributes[0]; 870 871 onLayoutCalled(); 872 return null; 873 }, invocation -> { 874 Object[] args = invocation.getArguments(); 875 PageRange[] pages = (PageRange[]) args[0]; 876 ParcelFileDescriptor fd = (ParcelFileDescriptor) args[1]; 877 WriteResultCallback callback = 878 (WriteResultCallback) args[3]; 879 880 writeBlankPages(printAttributes[0], fd, pages[0].getStart(), pages[0].getEnd()); 881 fd.close(); 882 callback.onWriteFinished(pages); 883 884 onWriteCalled(); 885 return null; 886 }, invocation -> { 887 onFinishCalled(); 888 return null; 889 }); 890 } 891 892 @SuppressWarnings("unchecked") createMockPrinterDiscoverySessionCallbacks( Answer<Void> onStartPrinterDiscovery, Answer<Void> onStopPrinterDiscovery, Answer<Void> onValidatePrinters, Answer<Void> onStartPrinterStateTracking, Answer<Void> onRequestCustomPrinterIcon, Answer<Void> onStopPrinterStateTracking, Answer<Void> onDestroy)893 protected static PrinterDiscoverySessionCallbacks createMockPrinterDiscoverySessionCallbacks( 894 Answer<Void> onStartPrinterDiscovery, Answer<Void> onStopPrinterDiscovery, 895 Answer<Void> onValidatePrinters, Answer<Void> onStartPrinterStateTracking, 896 Answer<Void> onRequestCustomPrinterIcon, Answer<Void> onStopPrinterStateTracking, 897 Answer<Void> onDestroy) { 898 PrinterDiscoverySessionCallbacks callbacks = mock(PrinterDiscoverySessionCallbacks.class); 899 900 doCallRealMethod().when(callbacks).setSession(any(StubbablePrinterDiscoverySession.class)); 901 when(callbacks.getSession()).thenCallRealMethod(); 902 903 if (onStartPrinterDiscovery != null) { 904 doAnswer(onStartPrinterDiscovery).when(callbacks).onStartPrinterDiscovery( 905 any(List.class)); 906 } 907 if (onStopPrinterDiscovery != null) { 908 doAnswer(onStopPrinterDiscovery).when(callbacks).onStopPrinterDiscovery(); 909 } 910 if (onValidatePrinters != null) { 911 doAnswer(onValidatePrinters).when(callbacks).onValidatePrinters( 912 any(List.class)); 913 } 914 if (onStartPrinterStateTracking != null) { 915 doAnswer(onStartPrinterStateTracking).when(callbacks).onStartPrinterStateTracking( 916 any(PrinterId.class)); 917 } 918 if (onRequestCustomPrinterIcon != null) { 919 doAnswer(onRequestCustomPrinterIcon).when(callbacks).onRequestCustomPrinterIcon( 920 any(PrinterId.class), any(CancellationSignal.class), 921 any(CustomPrinterIconCallback.class)); 922 } 923 if (onStopPrinterStateTracking != null) { 924 doAnswer(onStopPrinterStateTracking).when(callbacks).onStopPrinterStateTracking( 925 any(PrinterId.class)); 926 } 927 if (onDestroy != null) { 928 doAnswer(onDestroy).when(callbacks).onDestroy(); 929 } 930 931 return callbacks; 932 } 933 createMockPrintServiceCallbacks( Answer<PrinterDiscoverySessionCallbacks> onCreatePrinterDiscoverySessionCallbacks, Answer<Void> onPrintJobQueued, Answer<Void> onRequestCancelPrintJob)934 protected PrintServiceCallbacks createMockPrintServiceCallbacks( 935 Answer<PrinterDiscoverySessionCallbacks> onCreatePrinterDiscoverySessionCallbacks, 936 Answer<Void> onPrintJobQueued, Answer<Void> onRequestCancelPrintJob) { 937 final PrintServiceCallbacks service = mock(PrintServiceCallbacks.class); 938 939 doCallRealMethod().when(service).setService(any(StubbablePrintService.class)); 940 when(service.getService()).thenCallRealMethod(); 941 942 if (onCreatePrinterDiscoverySessionCallbacks != null) { 943 doAnswer(onCreatePrinterDiscoverySessionCallbacks).when(service) 944 .onCreatePrinterDiscoverySessionCallbacks(); 945 } 946 if (onPrintJobQueued != null) { 947 doAnswer(onPrintJobQueued).when(service).onPrintJobQueued(any(PrintJob.class)); 948 } 949 if (onRequestCancelPrintJob != null) { 950 doAnswer(onRequestCancelPrintJob).when(service).onRequestCancelPrintJob( 951 any(PrintJob.class)); 952 } 953 954 return service; 955 } 956 writeBlankPages(PrintAttributes constraints, ParcelFileDescriptor output, int fromIndex, int toIndex)957 protected void writeBlankPages(PrintAttributes constraints, ParcelFileDescriptor output, 958 int fromIndex, int toIndex) throws IOException { 959 PrintedPdfDocument document = new PrintedPdfDocument(getActivity(), constraints); 960 final int pageCount = toIndex - fromIndex + 1; 961 for (int i = 0; i < pageCount; i++) { 962 PdfDocument.Page page = document.startPage(i); 963 document.finishPage(page); 964 } 965 FileOutputStream fos = new FileOutputStream(output.getFileDescriptor()); 966 document.writeTo(fos); 967 fos.flush(); 968 document.close(); 969 } 970 selectPages(String pages, int totalPages)971 protected void selectPages(String pages, int totalPages) throws Exception { 972 UiObject pagesSpinner = getUiDevice().findObject(new UiSelector().resourceId( 973 "com.android.printspooler:id/range_options_spinner")); 974 pagesSpinner.click(); 975 976 UiObject rangeOption = getUiDevice().findObject(new UiSelector().textContains("Range of " 977 + totalPages)); 978 rangeOption.click(); 979 980 UiObject pagesEditText = getUiDevice().findObject(new UiSelector().resourceId( 981 "com.android.printspooler:id/page_range_edittext")); 982 pagesEditText.setText(pages); 983 984 // Hide the keyboard. 985 getUiDevice().pressBack(); 986 } 987 988 private static final class CallCounter { 989 private final Object mLock = new Object(); 990 991 private int mCallCount; 992 call()993 public void call() { 994 synchronized (mLock) { 995 mCallCount++; 996 mLock.notifyAll(); 997 } 998 } 999 getCallCount()1000 int getCallCount() { 1001 synchronized (mLock) { 1002 return mCallCount; 1003 } 1004 } 1005 reset()1006 public void reset() { 1007 synchronized (mLock) { 1008 mCallCount = 0; 1009 } 1010 } 1011 waitForCount(int count, long timeoutMillis)1012 public void waitForCount(int count, long timeoutMillis) throws TimeoutException { 1013 synchronized (mLock) { 1014 final long startTimeMillis = SystemClock.uptimeMillis(); 1015 while (mCallCount < count) { 1016 try { 1017 final long elapsedTimeMillis = SystemClock.uptimeMillis() - startTimeMillis; 1018 final long remainingTimeMillis = timeoutMillis - elapsedTimeMillis; 1019 if (remainingTimeMillis <= 0) { 1020 throw new TimeoutException(); 1021 } 1022 mLock.wait(timeoutMillis); 1023 } catch (InterruptedException ie) { 1024 /* ignore */ 1025 } 1026 } 1027 } 1028 } 1029 } 1030 1031 1032 /** 1033 * Make {@code printerName} the default printer by adding it to the history of printers by 1034 * printing once. 1035 * 1036 * @param adapter The {@link PrintDocumentAdapter} used 1037 * @throws Exception If the printer could not be made default 1038 */ makeDefaultPrinter(PrintDocumentAdapter adapter, String printerName)1039 public void makeDefaultPrinter(PrintDocumentAdapter adapter, String printerName) 1040 throws Exception { 1041 // Perform a full print operation on the printer 1042 Log.d(LOG_TAG, "print"); 1043 print(adapter); 1044 Log.d(LOG_TAG, "waitForWriteAdapterCallback"); 1045 waitForWriteAdapterCallback(1); 1046 Log.d(LOG_TAG, "selectPrinter"); 1047 selectPrinter(printerName); 1048 Log.d(LOG_TAG, "clickPrintButton"); 1049 clickPrintButton(); 1050 Log.d(LOG_TAG, "answerPrintServicesWarning"); 1051 answerPrintServicesWarning(true); 1052 Log.d(LOG_TAG, "waitForPrinterDiscoverySessionDestroyCallbackCalled"); 1053 waitForPrinterDiscoverySessionDestroyCallbackCalled(1); 1054 1055 // Switch to new activity, which should now use the default printer 1056 Log.d(LOG_TAG, "getActivity().finish()"); 1057 getActivity().finish(); 1058 1059 createActivity(); 1060 } 1061 1062 /** 1063 * Annotation used to signal that a test does not need an activity. 1064 */ 1065 @Retention(RetentionPolicy.RUNTIME) 1066 @Target(ElementType.METHOD) 1067 protected @interface NoActivity { } 1068 1069 /** 1070 * Rule that handles the {@link NoActivity} annotation. 1071 */ 1072 private static class ShouldStartActivity implements TestRule { 1073 boolean mNoActivity; 1074 1075 @Override apply(Statement base, org.junit.runner.Description description)1076 public Statement apply(Statement base, org.junit.runner.Description description) { 1077 for (Annotation annotation : description.getAnnotations()) { 1078 if (annotation instanceof NoActivity) { 1079 mNoActivity = true; 1080 break; 1081 } 1082 } 1083 1084 return base; 1085 } 1086 } 1087 } 1088