Home | History | Annotate | Download | only in test
      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      */
    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 
    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 
    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 
    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 
    192     protected static Instrumentation getInstrumentation() {
    193         return InstrumentationRegistry.getInstrumentation();
    194     }
    195 
    196     @BeforeClass
    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      */
    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      */
    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
    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
    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
    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 
    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 
    346     protected void print(PrintDocumentAdapter adapter) {
    347         print(adapter, (PrintAttributes) null);
    348     }
    349 
    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      */
    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 
    368     protected void onCancelOperationCalled() {
    369         mCancelOperationCounter.call();
    370     }
    371 
    372     public void onStartCalled() {
    373         mStartCallCounter.call();
    374     }
    375 
    376     protected void onLayoutCalled() {
    377         mLayoutCallCounter.call();
    378     }
    379 
    380     protected int getWriteCallCount() {
    381         return mWriteCallCounter.getCallCount();
    382     }
    383 
    384     protected void onWriteCalled() {
    385         mWriteCallCounter.call();
    386     }
    387 
    388     protected void onWriteCancelCalled() {
    389         mWriteCancelCallCounter.call();
    390     }
    391 
    392     protected void onFinishCalled() {
    393         mFinishCallCounter.call();
    394     }
    395 
    396     protected void onPrintJobQueuedCalled() {
    397         mPrintJobQueuedCallCounter.call();
    398     }
    399 
    400     protected void onPrinterDiscoverySessionCreateCalled() {
    401         mCreateSessionCallCounter.call();
    402     }
    403 
    404     protected void onPrinterDiscoverySessionDestroyCalled() {
    405         mDestroySessionCallCounter.call();
    406     }
    407 
    408     protected void waitForCancelOperationCallbackCalled() {
    409         waitForCallbackCallCount(mCancelOperationCounter, 1,
    410                 "Did not get expected call to onCancel for the current operation.");
    411     }
    412 
    413     protected void waitForPrinterDiscoverySessionCreateCallbackCalled() {
    414         waitForCallbackCallCount(mCreateSessionCallCounter, 1,
    415                 "Did not get expected call to onCreatePrinterDiscoverySession.");
    416     }
    417 
    418     public void waitForPrinterDiscoverySessionDestroyCallbackCalled(int count) {
    419         waitForCallbackCallCount(mDestroySessionCallCounter, count,
    420                 "Did not get expected call to onDestroyPrinterDiscoverySession.");
    421     }
    422 
    423     protected void waitForServiceOnPrintJobQueuedCallbackCalled(int count) {
    424         waitForCallbackCallCount(mPrintJobQueuedCallCounter, count,
    425                 "Did not get expected call to onPrintJobQueued.");
    426     }
    427 
    428     protected void waitForAdapterStartCallbackCalled() {
    429         waitForCallbackCallCount(mStartCallCounter, 1,
    430                 "Did not get expected call to start.");
    431     }
    432 
    433     protected void waitForAdapterFinishCallbackCalled() {
    434         waitForCallbackCallCount(mFinishCallCounter, 1,
    435                 "Did not get expected call to finish.");
    436     }
    437 
    438     protected void waitForLayoutAdapterCallbackCount(int count) {
    439         waitForCallbackCallCount(mLayoutCallCounter, count,
    440                 "Did not get expected call to layout.");
    441     }
    442 
    443     public void waitForWriteAdapterCallback(int count) {
    444         waitForCallbackCallCount(mWriteCallCounter, count, "Did not get expected call to write.");
    445     }
    446 
    447     protected void waitForWriteCancelCallback(int count) {
    448         waitForCallbackCallCount(mWriteCancelCallCounter, count,
    449                 "Did not get expected cancel of write.");
    450     }
    451 
    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      */
    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      */
    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 
    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      */
    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      */
    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      */
    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      */
    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      */
    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 
    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 
    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 
    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 
    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 
    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 
    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 
    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 
    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 
    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 
    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 
    726     protected void assertNoPrintButton() throws UiObjectNotFoundException, IOException {
    727         assertFalse(getUiDevice().hasObject(By.res("com.android.printspooler:id/print_button")));
    728     }
    729 
    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 
    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 
    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 
    762     protected PrintDocumentActivity getActivity() {
    763         return mActivity;
    764     }
    765 
    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 
    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 
    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 
    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 
    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 
    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      */
    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")
    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 
    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 
    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 
    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 
    993         public void call() {
    994             synchronized (mLock) {
    995                 mCallCount++;
    996                 mLock.notifyAll();
    997             }
    998         }
    999 
   1000         int getCallCount() {
   1001             synchronized (mLock) {
   1002                 return mCallCount;
   1003             }
   1004         }
   1005 
   1006         public void reset() {
   1007             synchronized (mLock) {
   1008                 mCallCount = 0;
   1009             }
   1010         }
   1011 
   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      */
   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
   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