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