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