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