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 com.android.printspooler.outofprocess.tests;
18
19import android.graphics.pdf.PdfDocument;
20import android.os.Bundle;
21import android.os.CancellationSignal;
22import android.os.ParcelFileDescriptor;
23import android.print.PageRange;
24import android.print.PrintAttributes;
25import android.print.PrintDocumentAdapter;
26import android.print.PrintDocumentInfo;
27import android.print.PrinterCapabilitiesInfo;
28import android.print.PrinterId;
29import android.print.PrinterInfo;
30import com.android.printspooler.outofprocess.tests.mockservice.AddPrintersActivity;
31import com.android.printspooler.outofprocess.tests.mockservice.MockPrintService;
32import com.android.printspooler.outofprocess.tests.mockservice.PrinterDiscoverySessionCallbacks;
33import com.android.printspooler.outofprocess.tests.mockservice.StubbablePrinterDiscoverySession;
34import android.print.pdf.PrintedPdfDocument;
35import android.support.test.filters.LargeTest;
36import android.support.test.uiautomator.By;
37import android.support.test.uiautomator.UiObject;
38import android.support.test.uiautomator.UiObjectNotFoundException;
39import android.support.test.uiautomator.UiSelector;
40import android.support.test.uiautomator.Until;
41import android.util.Log;
42import org.junit.AfterClass;
43import org.junit.BeforeClass;
44import org.junit.Test;
45import org.junit.runner.RunWith;
46import org.junit.runners.Parameterized;
47
48import java.io.FileInputStream;
49import java.io.FileOutputStream;
50import java.io.IOException;
51import java.util.ArrayList;
52import java.util.Collection;
53import java.util.List;
54import java.util.concurrent.TimeoutException;
55import java.util.function.Supplier;
56
57import static org.junit.Assert.assertNotNull;
58
59/**
60 * Tests for the basic printing workflows
61 */
62@RunWith(Parameterized.class)
63public class WorkflowTest extends BasePrintTest {
64    private static final String LOG_TAG = WorkflowTest.class.getSimpleName();
65
66    private static float sWindowAnimationScaleBefore;
67    private static float sTransitionAnimationScaleBefore;
68    private static float sAnimatiorDurationScaleBefore;
69
70    private PrintAttributes.MediaSize mFirst;
71    private boolean mSelectPrinter;
72    private PrintAttributes.MediaSize mSecond;
73
74    public WorkflowTest(PrintAttributes.MediaSize first, boolean selectPrinter,
75            PrintAttributes.MediaSize second) {
76        mFirst = first;
77        mSelectPrinter = selectPrinter;
78        mSecond = second;
79    }
80
81    interface InterruptableConsumer<T> {
82        void accept(T t) throws InterruptedException;
83    }
84
85    /**
86     * Execute {@code waiter} until {@code condition} is met.
87     *
88     * @param condition Conditions to wait for
89     * @param waiter    Code to execute while waiting
90     */
91    private void waitWithTimeout(Supplier<Boolean> condition, InterruptableConsumer<Long> waiter)
92            throws TimeoutException, InterruptedException {
93        long startTime = System.currentTimeMillis();
94        while (condition.get()) {
95            long timeLeft = OPERATION_TIMEOUT - (System.currentTimeMillis() - startTime);
96            if (timeLeft < 0) {
97                throw new TimeoutException();
98            }
99
100            waiter.accept(timeLeft);
101        }
102    }
103
104    /**
105     * Executes a shell command using shell user identity, and return the standard output in
106     * string.
107     *
108     * @param cmd the command to run
109     *
110     * @return the standard output of the command
111     */
112    private static String runShellCommand(String cmd) throws IOException {
113        try (FileInputStream is = new ParcelFileDescriptor.AutoCloseInputStream(
114                getInstrumentation().getUiAutomation().executeShellCommand(cmd))) {
115            byte[] buf = new byte[64];
116            int bytesRead;
117
118            StringBuilder stdout = new StringBuilder();
119            while ((bytesRead = is.read(buf)) != -1) {
120                stdout.append(new String(buf, 0, bytesRead));
121            }
122
123            return stdout.toString();
124        }
125    }
126
127    @BeforeClass
128    public static void disableAnimations() throws Exception {
129        try {
130            sWindowAnimationScaleBefore = Float.parseFloat(runShellCommand(
131                    "settings get global window_animation_scale"));
132
133            runShellCommand("settings put global window_animation_scale 0");
134        } catch (NumberFormatException e) {
135            sWindowAnimationScaleBefore = Float.NaN;
136        }
137        try {
138            sTransitionAnimationScaleBefore = Float.parseFloat(runShellCommand(
139                    "settings get global transition_animation_scale"));
140
141            runShellCommand("settings put global transition_animation_scale 0");
142        } catch (NumberFormatException e) {
143            sTransitionAnimationScaleBefore = Float.NaN;
144        }
145        try {
146            sAnimatiorDurationScaleBefore = Float.parseFloat(runShellCommand(
147                    "settings get global animator_duration_scale"));
148
149            runShellCommand("settings put global animator_duration_scale 0");
150        } catch (NumberFormatException e) {
151            sAnimatiorDurationScaleBefore = Float.NaN;
152        }
153    }
154
155    @AfterClass
156    public static void enableAnimations() throws Exception {
157        if (sWindowAnimationScaleBefore != Float.NaN) {
158            runShellCommand(
159                    "settings put global window_animation_scale " + sWindowAnimationScaleBefore);
160        }
161        if (sTransitionAnimationScaleBefore != Float.NaN) {
162            runShellCommand(
163                    "settings put global transition_animation_scale " +
164                            sTransitionAnimationScaleBefore);
165        }
166        if (sAnimatiorDurationScaleBefore != Float.NaN) {
167            runShellCommand(
168                    "settings put global animator_duration_scale " + sAnimatiorDurationScaleBefore);
169        }
170    }
171
172    /** Add a printer with a given name and supported mediasize to a session */
173    private void addPrinter(StubbablePrinterDiscoverySession session,
174            String name, PrintAttributes.MediaSize mediaSize) {
175        PrinterId printerId = session.getService().generatePrinterId(name);
176        List<PrinterInfo> printers = new ArrayList<>(1);
177
178        PrinterCapabilitiesInfo.Builder builder =
179                new PrinterCapabilitiesInfo.Builder(printerId);
180
181        PrinterInfo printerInfo;
182        if (mediaSize != null) {
183            builder.setMinMargins(new PrintAttributes.Margins(0, 0, 0, 0))
184                    .setColorModes(PrintAttributes.COLOR_MODE_COLOR,
185                            PrintAttributes.COLOR_MODE_COLOR)
186                    .addMediaSize(mediaSize, true)
187                    .addResolution(new PrintAttributes.Resolution("300x300", "300x300", 300, 300),
188                            true);
189
190            printerInfo = new PrinterInfo.Builder(printerId, name,
191                    PrinterInfo.STATUS_IDLE).setCapabilities(builder.build()).build();
192        } else {
193            printerInfo = (new PrinterInfo.Builder(printerId, name,
194                    PrinterInfo.STATUS_IDLE)).build();
195        }
196
197        printers.add(printerInfo);
198        session.addPrinters(printers);
199    }
200
201    /** Find a certain element in the UI and click on it */
202    private void clickOn(UiSelector selector) throws UiObjectNotFoundException {
203        Log.i(LOG_TAG, "Click on " + selector);
204        UiObject view = getUiDevice().findObject(selector);
205        view.click();
206        getUiDevice().waitForIdle();
207    }
208
209    /** Find a certain text in the UI and click on it */
210    private void clickOnText(String text) throws UiObjectNotFoundException {
211        clickOn(new UiSelector().text(text));
212    }
213
214    /** Set the printer in the print activity */
215    private void setPrinter(String printerName) throws UiObjectNotFoundException {
216        clickOn(new UiSelector().resourceId("com.android.printspooler:id/destination_spinner"));
217
218        clickOnText(printerName);
219    }
220
221    /**
222     * Init mock print servic that returns a single printer by default.
223     *
224     * @param sessionRef Where to store the reference to the session once started
225     */
226    private void setMockPrintServiceCallbacks(StubbablePrinterDiscoverySession[] sessionRef,
227            ArrayList<String> trackedPrinters, PrintAttributes.MediaSize mediaSize) {
228        MockPrintService.setCallbacks(createMockPrintServiceCallbacks(
229                inv -> createMockPrinterDiscoverySessionCallbacks(inv2 -> {
230                            synchronized (sessionRef) {
231                                sessionRef[0] = ((PrinterDiscoverySessionCallbacks) inv2.getMock())
232                                        .getSession();
233
234                                addPrinter(sessionRef[0], "1st printer", mediaSize);
235
236                                sessionRef.notifyAll();
237                            }
238                            return null;
239                        },
240                        null, null, inv2 -> {
241                            synchronized (trackedPrinters) {
242                                trackedPrinters
243                                        .add(((PrinterId) inv2.getArguments()[0]).getLocalId());
244                                trackedPrinters.notifyAll();
245                            }
246                            return null;
247                        }, null, inv2 -> {
248                            synchronized (trackedPrinters) {
249                                trackedPrinters
250                                        .remove(((PrinterId) inv2.getArguments()[0]).getLocalId());
251                                trackedPrinters.notifyAll();
252                            }
253                            return null;
254                        }, inv2 -> {
255                            synchronized (sessionRef) {
256                                sessionRef[0] = null;
257                                sessionRef.notifyAll();
258                            }
259                            return null;
260                        }
261                ), null, null));
262    }
263
264    /**
265     * Start print operation that just prints a single empty page
266     *
267     * @param printAttributesRef Where to store the reference to the print attributes once started
268     */
269    private void print(PrintAttributes[] printAttributesRef) {
270        print(new PrintDocumentAdapter() {
271            @Override
272            public void onStart() {
273            }
274
275            @Override
276            public void onLayout(PrintAttributes oldAttributes, PrintAttributes newAttributes,
277                    CancellationSignal cancellationSignal, LayoutResultCallback callback,
278                    Bundle extras) {
279                callback.onLayoutFinished((new PrintDocumentInfo.Builder("doc")).build(),
280                        !newAttributes.equals(printAttributesRef[0]));
281
282                synchronized (printAttributesRef) {
283                    printAttributesRef[0] = newAttributes;
284                    printAttributesRef.notifyAll();
285                }
286            }
287
288            @Override
289            public void onWrite(PageRange[] pages, ParcelFileDescriptor destination,
290                    CancellationSignal cancellationSignal, WriteResultCallback callback) {
291                try {
292                    try {
293                        PrintedPdfDocument document = new PrintedPdfDocument(getActivity(),
294                                printAttributesRef[0]);
295                        try {
296                            PdfDocument.Page page = document.startPage(0);
297                            document.finishPage(page);
298                            try (FileOutputStream os = new FileOutputStream(
299                                    destination.getFileDescriptor())) {
300                                document.writeTo(os);
301                                os.flush();
302                            }
303                        } finally {
304                            document.close();
305                        }
306                    } finally {
307                        destination.close();
308                    }
309
310                    callback.onWriteFinished(pages);
311                } catch (IOException e) {
312                    callback.onWriteFailed(e.getMessage());
313                }
314            }
315        }, null);
316    }
317
318    @Parameterized.Parameters
319    public static Collection<Object[]> getParameters() {
320        ArrayList<Object[]> tests = new ArrayList<>();
321
322        for (PrintAttributes.MediaSize first : new PrintAttributes.MediaSize[]{
323                PrintAttributes.MediaSize.ISO_A0, null}) {
324            for (Boolean selectPrinter : new Boolean[]{true, false}) {
325                for (PrintAttributes.MediaSize second : new PrintAttributes.MediaSize[]{
326                        PrintAttributes.MediaSize.ISO_A1, null}) {
327                    // If we do not use the second printer, no need to try various options
328                    if (!selectPrinter && second == null) {
329                        continue;
330                    }
331                    tests.add(new Object[]{first, selectPrinter, second});
332                }
333            }
334        }
335
336        return tests;
337    }
338
339    @Test
340    @LargeTest
341    public void addAndSelectPrinter() throws Exception {
342        final StubbablePrinterDiscoverySession session[] = new StubbablePrinterDiscoverySession[1];
343        final PrintAttributes printAttributes[] = new PrintAttributes[1];
344        ArrayList<String> trackedPrinters = new ArrayList<>();
345
346        Log.i(LOG_TAG, "Running " + mFirst + " " + mSelectPrinter + " " + mSecond);
347
348        setMockPrintServiceCallbacks(session, trackedPrinters, mFirst);
349        print(printAttributes);
350
351        // We are now in the PrintActivity
352        Log.i(LOG_TAG, "Waiting for session");
353        synchronized (session) {
354            waitWithTimeout(() -> session[0] == null, session::wait);
355        }
356
357        setPrinter("1st printer");
358
359        Log.i(LOG_TAG, "Waiting for 1st printer to be tracked");
360        synchronized (trackedPrinters) {
361            waitWithTimeout(() -> !trackedPrinters.contains("1st printer"), trackedPrinters::wait);
362        }
363
364        if (mFirst != null) {
365            Log.i(LOG_TAG, "Waiting for print attributes to change");
366            synchronized (printAttributes) {
367                waitWithTimeout(
368                        () -> printAttributes[0] == null ||
369                                !printAttributes[0].getMediaSize().equals(
370                                        mFirst), printAttributes::wait);
371            }
372        } else {
373            Log.i(LOG_TAG, "Waiting for error message");
374            assertNotNull(getUiDevice().wait(Until.findObject(
375                    By.text("This printer isn't available right now.")), OPERATION_TIMEOUT));
376        }
377
378        setPrinter("All printers\u2026");
379
380        // We are now in the SelectPrinterActivity
381        clickOnText("Add printer");
382
383        // We are now in the AddPrinterActivity
384        AddPrintersActivity.addObserver(
385                () -> addPrinter(session[0], "2nd printer", mSecond));
386
387        // This executes the observer registered above
388        clickOn(new UiSelector().text(MockPrintService.class.getCanonicalName())
389                .resourceId("com.android.printspooler:id/title"));
390
391        getUiDevice().pressBack();
392        AddPrintersActivity.clearObservers();
393
394        if (mSelectPrinter) {
395            // We are now in the SelectPrinterActivity
396            clickOnText("2nd printer");
397        } else {
398            getUiDevice().pressBack();
399        }
400
401        // We are now in the PrintActivity
402        if (mSelectPrinter) {
403            if (mSecond != null) {
404                Log.i(LOG_TAG, "Waiting for print attributes to change");
405                synchronized (printAttributes) {
406                    waitWithTimeout(
407                            () -> printAttributes[0] == null ||
408                                    !printAttributes[0].getMediaSize().equals(
409                                            mSecond), printAttributes::wait);
410                }
411            } else {
412                Log.i(LOG_TAG, "Waiting for error message");
413                assertNotNull(getUiDevice().wait(Until.findObject(
414                        By.text("This printer isn't available right now.")), OPERATION_TIMEOUT));
415            }
416
417            Log.i(LOG_TAG, "Waiting for 1st printer to be not tracked");
418            synchronized (trackedPrinters) {
419                waitWithTimeout(() -> trackedPrinters.contains("1st printer"),
420                        trackedPrinters::wait);
421            }
422
423            Log.i(LOG_TAG, "Waiting for 2nd printer to be tracked");
424            synchronized (trackedPrinters) {
425                waitWithTimeout(() -> !trackedPrinters.contains("2nd printer"),
426                        trackedPrinters::wait);
427            }
428        } else {
429            Thread.sleep(100);
430
431            if (mFirst != null) {
432                Log.i(LOG_TAG, "Waiting for print attributes to change");
433                synchronized (printAttributes) {
434                    waitWithTimeout(
435                            () -> printAttributes[0] == null ||
436                                    !printAttributes[0].getMediaSize().equals(
437                                            mFirst), printAttributes::wait);
438                }
439            } else {
440                Log.i(LOG_TAG, "Waiting for error message");
441                assertNotNull(getUiDevice().wait(Until.findObject(
442                        By.text("This printer isn't available right now.")), OPERATION_TIMEOUT));
443            }
444
445            Log.i(LOG_TAG, "Waiting for 1st printer to be tracked");
446            synchronized (trackedPrinters) {
447                waitWithTimeout(() -> !trackedPrinters.contains("1st printer"),
448                        trackedPrinters::wait);
449            }
450        }
451
452        getUiDevice().pressBack();
453
454        // We are back in the test activity
455        Log.i(LOG_TAG, "Waiting for session to end");
456        synchronized (session) {
457            waitWithTimeout(() -> session[0] != null, session::wait);
458        }
459    }
460}
461