1// Copyright 2013 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5package org.chromium.printing;
6
7import android.annotation.TargetApi;
8import android.os.Build;
9import android.os.CancellationSignal;
10import android.os.ParcelFileDescriptor;
11import android.print.PageRange;
12import android.print.PrintAttributes;
13import android.print.PrintDocumentAdapter;
14import android.print.PrintDocumentInfo;
15import android.test.suitebuilder.annotation.LargeTest;
16
17import org.chromium.base.ApiCompatibilityUtils;
18import org.chromium.base.test.util.Feature;
19import org.chromium.base.test.util.TestFileUtil;
20import org.chromium.base.test.util.UrlUtils;
21import org.chromium.chrome.browser.printing.TabPrinter;
22import org.chromium.chrome.shell.ChromeShellTab;
23import org.chromium.chrome.shell.ChromeShellTestBase;
24
25import java.io.File;
26import java.io.FileInputStream;
27import java.io.IOException;
28import java.util.concurrent.Callable;
29import java.util.concurrent.FutureTask;
30import java.util.concurrent.TimeUnit;
31
32/**
33 * Tests Android printing.
34 * TODO(cimamoglu): Add a test with cancellation.
35 * TODO(cimamoglu): Add a test with multiple, stacked onLayout/onWrite calls.
36 * TODO(cimamoglu): Add a test which emulates Chromium failing to generate a PDF.
37 */
38public class PrintingControllerTest extends ChromeShellTestBase {
39
40    private static final String TEMP_FILE_NAME = "temp_print";
41    private static final String TEMP_FILE_EXTENSION = ".pdf";
42    private static final String PRINT_JOB_NAME = "foo";
43    private static final String URL = UrlUtils.encodeHtmlDataUri(
44            "<html><head></head><body>foo</body></html>");
45    private static final String PDF_PREAMBLE = "%PDF-1";
46    private static final long TEST_TIMEOUT = 20000L;
47
48    private static class LayoutResultCallbackWrapperMock implements
49            PrintDocumentAdapterWrapper.LayoutResultCallbackWrapper {
50        @Override
51        public void onLayoutFinished(PrintDocumentInfo info, boolean changed) {}
52
53        @Override
54        public void onLayoutFailed(CharSequence error) {}
55
56        @Override
57        public void onLayoutCancelled() {}
58    }
59
60    private static class WriteResultCallbackWrapperMock implements
61            PrintDocumentAdapterWrapper.WriteResultCallbackWrapper {
62        @Override
63        public void onWriteFinished(PageRange[] pages) {}
64
65        @Override
66        public void onWriteFailed(CharSequence error) {}
67
68        @Override
69        public void onWriteCancelled() {}
70    }
71
72    /**
73     * Test a basic printing flow by emulating the corresponding system calls to the printing
74     * controller: onStart, onLayout, onWrite, onFinish.  Each one is called once, and in this
75     * order, in the UI thread.
76     */
77    @TargetApi(Build.VERSION_CODES.KITKAT)
78    @LargeTest
79    @Feature({"Printing"})
80    public void testNormalPrintingFlow() throws Throwable {
81        if (!ApiCompatibilityUtils.isPrintingSupported()) return;
82
83        final ChromeShellTab currentTab = launchChromeShellWithUrl(URL).getActiveTab();
84        assertTrue(waitForActiveShellToBeDoneLoading());
85
86        final PrintingControllerImpl printingController = createControllerOnUiThread();
87
88        startControllerOnUiThread(printingController, currentTab);
89        // {@link PrintDocumentAdapter#onStart} is always called first.
90        callStartOnUiThread(printingController);
91
92        // Create a temporary file to save the PDF.
93        final File cacheDir = getInstrumentation().getTargetContext().getCacheDir();
94        final File tempFile = File.createTempFile(TEMP_FILE_NAME, TEMP_FILE_EXTENSION, cacheDir);
95        final ParcelFileDescriptor fileDescriptor =
96                ParcelFileDescriptor.open(tempFile, (ParcelFileDescriptor.MODE_CREATE |
97                                                     ParcelFileDescriptor.MODE_READ_WRITE));
98
99        PrintAttributes attributes = new PrintAttributes.Builder()
100                .setMediaSize(PrintAttributes.MediaSize.ISO_A4)
101                .setResolution(new PrintAttributes.Resolution("foo", "bar", 300, 300))
102                .setMinMargins(PrintAttributes.Margins.NO_MARGINS)
103                .build();
104
105        // Use this to wait for PDF generation to complete, as it will happen asynchronously.
106        final FutureTask<Boolean> result =
107                new FutureTask<Boolean>(new Callable<Boolean>() {
108                            @Override
109                            public Boolean call() {
110                                return true;
111                            }
112                        });
113
114        callLayoutOnUiThread(
115                printingController,
116                null,
117                attributes,
118                new LayoutResultCallbackWrapperMock() {
119            // Called on UI thread
120            @Override
121            public void onLayoutFinished(PrintDocumentInfo info, boolean changed) {
122                callWriteOnUiThread(printingController, fileDescriptor, result);
123            }
124        });
125
126        FileInputStream in = null;
127        try {
128            // This blocks until the PDF is generated.
129            result.get(TEST_TIMEOUT, TimeUnit.MILLISECONDS);
130            assertTrue(tempFile.length() > 0);
131            in = new FileInputStream(tempFile);
132            byte[] b = new byte[PDF_PREAMBLE.length()];
133            in.read(b);
134            String preamble = new String(b);
135            assertEquals(PDF_PREAMBLE, preamble);
136        } finally {
137            callFinishOnUiThread(printingController);
138            if (in != null) in.close();
139            // Close the descriptor, if not closed already.
140            fileDescriptor.close();
141            TestFileUtil.deleteFile(tempFile.getAbsolutePath());
142        }
143
144    }
145
146    private PrintingControllerImpl createControllerOnUiThread() {
147        try {
148            final FutureTask<PrintingControllerImpl> task =
149                    new FutureTask<PrintingControllerImpl>(new Callable<PrintingControllerImpl>() {
150                @Override
151                public PrintingControllerImpl call() throws Exception {
152                    return (PrintingControllerImpl) PrintingControllerImpl.create(
153                            new PrintDocumentAdapterWrapper(),
154                            PRINT_JOB_NAME);
155                }
156            });
157
158            runTestOnUiThread(task);
159            PrintingControllerImpl result = task.get(TEST_TIMEOUT, TimeUnit.MILLISECONDS);
160            return result;
161        } catch (Throwable e) {
162            fail("Error on creating PrintingControllerImpl on the UI thread: " + e);
163        }
164        return null;
165    }
166
167    private void startControllerOnUiThread(final PrintingControllerImpl controller,
168            final ChromeShellTab tab) {
169        try {
170            final PrintManagerDelegate mockPrintManagerDelegate = new PrintManagerDelegate() {
171                @Override
172                public void print(String printJobName,
173                        PrintDocumentAdapter documentAdapter,
174                        PrintAttributes attributes) {
175                    // Do nothing, as we will emulate the framework call sequence within the test.
176                }
177            };
178
179            runTestOnUiThread(new Runnable() {
180                @Override
181                public void run() {
182                    controller.startPrint(new TabPrinter(tab), mockPrintManagerDelegate);
183                }
184            });
185        } catch (Throwable e) {
186            fail("Error on calling startPrint of PrintingControllerImpl " + e);
187        }
188    }
189
190    private void callStartOnUiThread(final PrintingControllerImpl controller) {
191        try {
192            runTestOnUiThread(new Runnable() {
193                @Override
194                public void run() {
195                    controller.onStart();
196                }
197            });
198        } catch (Throwable e) {
199            fail("Error on calling onStart of PrintingControllerImpl " + e);
200        }
201    }
202
203    @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
204    private void callLayoutOnUiThread(
205            final PrintingControllerImpl controller,
206            final PrintAttributes oldAttributes,
207            final PrintAttributes newAttributes,
208            final PrintDocumentAdapterWrapper.LayoutResultCallbackWrapper layoutResultCallback) {
209        try {
210            runTestOnUiThread(new Runnable() {
211                @Override
212                public void run() {
213                    controller.onLayout(
214                            oldAttributes,
215                            newAttributes,
216                            new CancellationSignal(),
217                            layoutResultCallback,
218                            null);
219                }
220            });
221        } catch (Throwable e) {
222            fail("Error on calling onLayout of PrintingControllerImpl " + e);
223        }
224    }
225
226    @TargetApi(Build.VERSION_CODES.KITKAT)
227    private void callWriteOnUiThread(
228            final PrintingControllerImpl controller,
229            final ParcelFileDescriptor descriptor,
230            final FutureTask<Boolean> result) {
231        try {
232            controller.onWrite(
233                    new PageRange[] {PageRange.ALL_PAGES},
234                    descriptor,
235                    new CancellationSignal(),
236                    new WriteResultCallbackWrapperMock() {
237                        @Override
238                        public void onWriteFinished(PageRange[] pages) {
239                            try {
240                                descriptor.close();
241                                // Result is ready, signal to continue.
242                                result.run();
243                            } catch (IOException ex) {
244                                fail("Failed file operation: " + ex.toString());
245                            }
246                        }
247                    }
248            );
249        } catch (Throwable e) {
250            fail("Error on calling onWriteInternal of PrintingControllerImpl " + e);
251        }
252    }
253
254    private void callFinishOnUiThread(final PrintingControllerImpl controller) {
255        try {
256            runTestOnUiThread(new Runnable() {
257                @Override
258                public void run() {
259                    controller.onFinish();
260                }
261            });
262        } catch (Throwable e) {
263            fail("Error on calling onFinish of PrintingControllerImpl " + e);
264        }
265    }
266}
267