1/*
2 * Copyright (C) 2016 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
17package android.print;
18
19import static org.junit.Assert.assertTrue;
20import static org.junit.Assert.fail;
21import static org.junit.Assume.assumeTrue;
22import static org.mockito.Matchers.any;
23import static org.mockito.Mockito.doAnswer;
24import static org.mockito.Mockito.doCallRealMethod;
25import static org.mockito.Mockito.mock;
26import static org.mockito.Mockito.when;
27
28import android.annotation.NonNull;
29import android.app.Instrumentation;
30import android.content.Context;
31import android.content.pm.PackageManager;
32import android.os.CancellationSignal;
33import android.os.ParcelFileDescriptor;
34import android.os.SystemClock;
35import android.print.mockservice.PrintServiceCallbacks;
36import android.print.mockservice.PrinterDiscoverySessionCallbacks;
37import android.print.mockservice.StubbablePrinterDiscoverySession;
38import android.printservice.CustomPrinterIconCallback;
39import android.printservice.PrintJob;
40import android.printservice.PrintService;
41import android.support.test.InstrumentationRegistry;
42import android.support.test.uiautomator.UiDevice;
43import android.support.test.rule.ActivityTestRule;
44
45import org.junit.After;
46import org.junit.Before;
47import org.junit.BeforeClass;
48import org.junit.Rule;
49import org.mockito.stubbing.Answer;
50
51import java.io.FileInputStream;
52import java.io.IOException;
53import java.util.List;
54import java.util.concurrent.TimeoutException;
55
56/**
57 * This is the base class for print tests.
58 */
59abstract class BasePrintTest {
60    protected static final long OPERATION_TIMEOUT = 30000;
61    private static final String PM_CLEAR_SUCCESS_OUTPUT = "Success";
62    private static final int CURRENT_USER_ID = -2; // Mirrors UserHandle.USER_CURRENT
63
64    private android.print.PrintJob mPrintJob;
65
66    private CallCounter mStartCallCounter;
67    private CallCounter mStartSessionCallCounter;
68
69    private static Instrumentation sInstrumentation;
70    private static UiDevice sUiDevice;
71
72    @Rule
73    public ActivityTestRule<PrintTestActivity> mActivityRule =
74            new ActivityTestRule<>(PrintTestActivity.class, false, true);
75
76    /**
77     * {@link Runnable} that can throw and {@link Exception}
78     */
79    interface Invokable {
80        /**
81         * Execute the invokable
82         *
83         * @throws Exception
84         */
85        void run() throws Exception;
86    }
87
88    /**
89     * Assert that the invokable throws an expectedException
90     *
91     * @param invokable The {@link Invokable} to run
92     * @param expectedClass The {@link Exception} that is supposed to be thrown
93     */
94    void assertException(Invokable invokable, Class<? extends Exception> expectedClass)
95            throws Exception {
96        try {
97            invokable.run();
98        } catch (Exception e) {
99            if (e.getClass().isAssignableFrom(expectedClass)) {
100                return;
101            } else {
102                throw e;
103            }
104        }
105
106        throw new AssertionError("No exception thrown");
107    }
108
109    /**
110     * Return the UI device
111     *
112     * @return the UI device
113     */
114    public UiDevice getUiDevice() {
115        return sUiDevice;
116    }
117
118    protected static Instrumentation getInstrumentation() {
119        return sInstrumentation;
120    }
121
122    @BeforeClass
123    public static void setUpClass() throws Exception {
124        sInstrumentation = InstrumentationRegistry.getInstrumentation();
125        assumeTrue(sInstrumentation.getContext().getPackageManager().hasSystemFeature(
126                PackageManager.FEATURE_PRINTING));
127
128        sUiDevice = UiDevice.getInstance(sInstrumentation);
129
130        // Make sure we start with a clean slate.
131        clearPrintSpoolerData();
132
133        // Workaround for dexmaker bug: https://code.google.com/p/dexmaker/issues/detail?id=2
134        // Dexmaker is used by mockito.
135        System.setProperty("dexmaker.dexcache", getInstrumentation()
136                .getTargetContext().getCacheDir().getPath());
137    }
138
139    @Before
140    public void initCounters() throws Exception {
141        // Initialize the latches.
142        mStartCallCounter = new CallCounter();
143        mStartSessionCallCounter = new CallCounter();
144    }
145
146    @After
147    public void exitActivities() throws Exception {
148        // Exit print spooler
149        getUiDevice().pressBack();
150        getUiDevice().pressBack();
151    }
152
153    protected android.print.PrintJob print(@NonNull final PrintDocumentAdapter adapter,
154            final PrintAttributes attributes) {
155        // Initiate printing as if coming from the app.
156        getInstrumentation().runOnMainSync(() -> {
157            PrintManager printManager = (PrintManager) getActivity()
158                    .getSystemService(Context.PRINT_SERVICE);
159            mPrintJob = printManager.print("Print job", adapter, attributes);
160        });
161
162        return mPrintJob;
163    }
164
165    protected void onStartCalled() {
166        mStartCallCounter.call();
167    }
168
169    protected void onPrinterDiscoverySessionStartCalled() {
170        mStartSessionCallCounter.call();
171    }
172
173    protected void waitForPrinterDiscoverySessionStartCallbackCalled() {
174        waitForCallbackCallCount(mStartSessionCallCounter, 1,
175                "Did not get expected call to onStartPrinterDiscoverySession.");
176    }
177
178    protected void waitForStartAdapterCallbackCalled() {
179        waitForCallbackCallCount(mStartCallCounter, 1, "Did not get expected call to start.");
180    }
181
182    private static void waitForCallbackCallCount(CallCounter counter, int count, String message) {
183        try {
184            counter.waitForCount(count, OPERATION_TIMEOUT);
185        } catch (TimeoutException te) {
186            fail(message);
187        }
188    }
189
190    protected PrintTestActivity getActivity() {
191        return mActivityRule.getActivity();
192    }
193
194    public static String runShellCommand(Instrumentation instrumentation, String cmd)
195            throws IOException {
196        ParcelFileDescriptor pfd = instrumentation.getUiAutomation().executeShellCommand(cmd);
197        byte[] buf = new byte[512];
198        int bytesRead;
199        FileInputStream fis = new ParcelFileDescriptor.AutoCloseInputStream(pfd);
200        StringBuilder stdout = new StringBuilder();
201        while ((bytesRead = fis.read(buf)) != -1) {
202            stdout.append(new String(buf, 0, bytesRead));
203        }
204        fis.close();
205        return stdout.toString();
206    }
207
208    protected static void clearPrintSpoolerData() throws Exception {
209        assertTrue("failed to clear print spooler data",
210                runShellCommand(getInstrumentation(), String.format(
211                        "pm clear --user %d %s", CURRENT_USER_ID,
212                        PrintManager.PRINT_SPOOLER_PACKAGE_NAME))
213                        .contains(PM_CLEAR_SUCCESS_OUTPUT));
214    }
215
216    @SuppressWarnings("unchecked")
217    protected PrinterDiscoverySessionCallbacks createMockPrinterDiscoverySessionCallbacks(
218            Answer<Void> onStartPrinterDiscovery, Answer<Void> onStopPrinterDiscovery,
219            Answer<Void> onValidatePrinters, Answer<Void> onStartPrinterStateTracking,
220            Answer<Void> onRequestCustomPrinterIcon, Answer<Void> onStopPrinterStateTracking,
221            Answer<Void> onDestroy) {
222        PrinterDiscoverySessionCallbacks callbacks = mock(PrinterDiscoverySessionCallbacks.class);
223
224        doCallRealMethod().when(callbacks).setSession(any(StubbablePrinterDiscoverySession.class));
225        when(callbacks.getSession()).thenCallRealMethod();
226
227        if (onStartPrinterDiscovery != null) {
228            doAnswer(onStartPrinterDiscovery).when(callbacks).onStartPrinterDiscovery(
229                    any(List.class));
230        }
231        if (onStopPrinterDiscovery != null) {
232            doAnswer(onStopPrinterDiscovery).when(callbacks).onStopPrinterDiscovery();
233        }
234        if (onValidatePrinters != null) {
235            doAnswer(onValidatePrinters).when(callbacks).onValidatePrinters(
236                    any(List.class));
237        }
238        if (onStartPrinterStateTracking != null) {
239            doAnswer(onStartPrinterStateTracking).when(callbacks).onStartPrinterStateTracking(
240                    any(PrinterId.class));
241        }
242        if (onRequestCustomPrinterIcon != null) {
243            doAnswer(onRequestCustomPrinterIcon).when(callbacks).onRequestCustomPrinterIcon(
244                    any(PrinterId.class), any(CancellationSignal.class),
245                    any(CustomPrinterIconCallback.class));
246        }
247        if (onStopPrinterStateTracking != null) {
248            doAnswer(onStopPrinterStateTracking).when(callbacks).onStopPrinterStateTracking(
249                    any(PrinterId.class));
250        }
251        if (onDestroy != null) {
252            doAnswer(onDestroy).when(callbacks).onDestroy();
253        }
254
255        return callbacks;
256    }
257
258    protected PrintServiceCallbacks createMockPrintServiceCallbacks(
259            Answer<PrinterDiscoverySessionCallbacks> onCreatePrinterDiscoverySessionCallbacks,
260            Answer<Void> onPrintJobQueued, Answer<Void> onRequestCancelPrintJob) {
261        final PrintServiceCallbacks service = mock(PrintServiceCallbacks.class);
262
263        doCallRealMethod().when(service).setService(any(PrintService.class));
264        when(service.getService()).thenCallRealMethod();
265
266        if (onCreatePrinterDiscoverySessionCallbacks != null) {
267            doAnswer(onCreatePrinterDiscoverySessionCallbacks).when(service)
268                    .onCreatePrinterDiscoverySessionCallbacks();
269        }
270        if (onPrintJobQueued != null) {
271            doAnswer(onPrintJobQueued).when(service).onPrintJobQueued(any(PrintJob.class));
272        }
273        if (onRequestCancelPrintJob != null) {
274            doAnswer(onRequestCancelPrintJob).when(service).onRequestCancelPrintJob(
275                    any(PrintJob.class));
276        }
277
278        return service;
279    }
280
281    private static final class CallCounter {
282        private final Object mLock = new Object();
283
284        private int mCallCount;
285
286        public void call() {
287            synchronized (mLock) {
288                mCallCount++;
289                mLock.notifyAll();
290            }
291        }
292
293        int getCallCount() {
294            synchronized (mLock) {
295                return mCallCount;
296            }
297        }
298
299        public void waitForCount(int count, long timeoutMillis) throws TimeoutException {
300            synchronized (mLock) {
301                final long startTimeMillis = SystemClock.uptimeMillis();
302                while (mCallCount < count) {
303                    try {
304                        final long elapsedTimeMillis = SystemClock.uptimeMillis() - startTimeMillis;
305                        final long remainingTimeMillis = timeoutMillis - elapsedTimeMillis;
306                        if (remainingTimeMillis <= 0) {
307                            throw new TimeoutException();
308                        }
309                        mLock.wait(timeoutMillis);
310                    } catch (InterruptedException ie) {
311                        /* ignore */
312                    }
313                }
314            }
315        }
316    }
317}
318