chromium/chrome/android/javatests/src/org/chromium/chrome/browser/printing/PrintingControllerTest.java

// Copyright 2013 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.chrome.browser.printing;

import android.os.CancellationSignal;
import android.os.ParcelFileDescriptor;
import android.print.PageRange;
import android.print.PrintAttributes;
import android.print.PrintDocumentAdapter;
import android.print.PrintDocumentInfo;

import androidx.test.filters.LargeTest;
import androidx.test.filters.MediumTest;
import androidx.test.filters.SmallTest;

import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;

import org.chromium.base.ThreadUtils;
import org.chromium.base.test.util.CallbackHelper;
import org.chromium.base.test.util.CommandLineFlags;
import org.chromium.base.test.util.Feature;
import org.chromium.base.test.util.TestFileUtil;
import org.chromium.base.test.util.UrlUtils;
import org.chromium.chrome.browser.flags.ChromeSwitches;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.tabmodel.TabModelUtils;
import org.chromium.chrome.test.ChromeJUnit4ClassRunner;
import org.chromium.chrome.test.ChromeTabbedActivityTestRule;
import org.chromium.printing.PrintDocumentAdapterWrapper.LayoutResultCallbackWrapper;
import org.chromium.printing.PrintDocumentAdapterWrapper.WriteResultCallbackWrapper;
import org.chromium.printing.PrintManagerDelegate;
import org.chromium.printing.PrintingControllerImpl;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

/**
 * Tests Android printing. TODO(cimamoglu): Add a test with cancellation. TODO(cimamoglu): Add a
 * test with multiple, stacked onLayout/onWrite calls. TODO(cimamoglu): Add a test which emulates
 * Chromium failing to generate a PDF.
 */
@RunWith(ChromeJUnit4ClassRunner.class)
@CommandLineFlags.Add({ChromeSwitches.DISABLE_FIRST_RUN_EXPERIENCE})
public class PrintingControllerTest {
    @Rule
    public final ChromeTabbedActivityTestRule mActivityTestRule =
            new ChromeTabbedActivityTestRule();

    private static final String TEMP_FILE_NAME = "temp_print";
    private static final String TEMP_FILE_EXTENSION = ".pdf";
    private static final String URL =
            UrlUtils.encodeHtmlDataUri("<html><head></head><body>foo</body></html>");
    private static final String PDF_PREAMBLE = "%PDF-1";
    private static final long TEST_TIMEOUT = 20000L;

    @Before
    public void setUp() {
        // Do nothing.
    }

    private static class LayoutResultCallbackWrapperMock implements LayoutResultCallbackWrapper {
        @Override
        public void onLayoutFinished(PrintDocumentInfo info, boolean changed) {}

        @Override
        public void onLayoutFailed(CharSequence error) {}

        @Override
        public void onLayoutCancelled() {}
    }

    private static class WriteResultCallbackWrapperMock implements WriteResultCallbackWrapper {
        @Override
        public void onWriteFinished(PageRange[] pages) {}

        @Override
        public void onWriteFailed(CharSequence error) {}

        @Override
        public void onWriteCancelled() {}
    }

    private static class WaitForOnWriteHelper extends CallbackHelper {
        public void waitForCallback(String msg) throws TimeoutException {
            waitForOnly(msg, TEST_TIMEOUT, TimeUnit.MILLISECONDS);
        }
    }

    private static class TemporaryFileHandler implements AutoCloseable {
        private File mTempFile;
        private ParcelFileDescriptor mFileDescriptor;

        public TemporaryFileHandler() throws IOException {
            mTempFile = File.createTempFile(TEMP_FILE_NAME, TEMP_FILE_EXTENSION);
            try {
                mFileDescriptor =
                        ParcelFileDescriptor.open(mTempFile, ParcelFileDescriptor.MODE_READ_WRITE);
            } catch (FileNotFoundException e) {
                // Exception happened, can't continue, cleanup the file.
                TestFileUtil.deleteFile(mTempFile.getAbsolutePath());
                throw new FileNotFoundException();
            }
        }

        ParcelFileDescriptor getFileDescriptor() {
            return mFileDescriptor;
        }

        @Override
        public void close() throws IOException {
            try {
                mFileDescriptor.close();
            } finally {
                TestFileUtil.deleteFile(mTempFile.getAbsolutePath());
            }
        }
    }

    private static class PrintingControllerImplPdfWritingDone extends PrintingControllerImpl {
        private WaitForOnWriteHelper mWaitForOnWrite;

        public PrintingControllerImplPdfWritingDone(WaitForOnWriteHelper waitForOnWrite) {
            mWaitForOnWrite = waitForOnWrite;
            sInstance = this;
        }

        @Override
        public void pdfWritingDone(int pageCount) {
            mWaitForOnWrite.notifyCalled();
        }
    }

    /**
     * Test a basic printing flow by emulating the corresponding system calls to the printing
     * controller: onStart, onLayout, onWrite, onFinish. Each one is called once, and in this order,
     * in the UI thread.
     */
    @Test
    @LargeTest
    @Feature({"Printing"})
    public void testNormalPrintingFlow() throws Throwable {
        mActivityTestRule.startMainActivityWithURL(URL);
        final Tab currentTab = mActivityTestRule.getActivity().getActivityTab();

        final PrintingControllerImpl printingController = createControllerOnUiThread();

        startControllerOnUiThread(printingController, currentTab);
        // {@link PrintDocumentAdapter#onStart} is always called first.
        callStartOnUiThread(printingController);

        // Create a temporary file to save the PDF.
        final File tempFile = File.createTempFile(TEMP_FILE_NAME, TEMP_FILE_EXTENSION);
        final ParcelFileDescriptor fileDescriptor =
                ParcelFileDescriptor.open(tempFile, ParcelFileDescriptor.MODE_READ_WRITE);

        // Use this to wait for PDF generation to complete, as it will happen asynchronously.
        final WaitForOnWriteHelper onWriteFinishedCompleted = new WaitForOnWriteHelper();

        final WriteResultCallbackWrapper writeResultCallback =
                new WriteResultCallbackWrapperMock() {
                    @Override
                    public void onWriteFinished(PageRange[] pages) {
                        onWriteFinishedCompleted.notifyCalled();
                    }
                };

        final LayoutResultCallbackWrapper layoutResultCallback =
                new LayoutResultCallbackWrapperMock() {
                    // Called on UI thread.
                    @Override
                    public void onLayoutFinished(PrintDocumentInfo info, boolean changed) {
                        printingController.onWrite(
                                new PageRange[] {PageRange.ALL_PAGES},
                                fileDescriptor,
                                new CancellationSignal(),
                                writeResultCallback);
                    }
                };

        callLayoutOnUiThread(
                printingController, null, createDummyPrintAttributes(), layoutResultCallback);

        FileInputStream in = null;
        try {
            onWriteFinishedCompleted.waitForCallback("onWriteFinished callback never completed.");
            Assert.assertTrue(tempFile.length() > 0);
            in = new FileInputStream(tempFile);
            byte[] b = new byte[PDF_PREAMBLE.length()];
            in.read(b);
            String preamble = new String(b);
            Assert.assertEquals(PDF_PREAMBLE, preamble);
        } finally {
            if (in != null) in.close();
            callFinishOnUiThread(printingController);
            // Close the descriptor, if not closed already.
            fileDescriptor.close();
            TestFileUtil.deleteFile(tempFile.getAbsolutePath());
        }
    }

    /**
     * Test for http://crbug.com/528909 Simulating while a printing job is triggered and about to
     * call Android framework to show UI, the corresponding tab is closed, this behaviour is mostly
     * from JavaScript code. Make sure we don't crash and won't call into framework.
     */
    @Test
    @MediumTest
    @Feature({"Printing"})
    public void testPrintCloseWindowBeforeStart() {
        mActivityTestRule.startMainActivityWithURL(URL);
        final Tab currentTab = mActivityTestRule.getActivity().getActivityTab();
        final PrintingControllerImpl printingController = createControllerOnUiThread();
        final PrintManagerDelegate mockPrintManagerDelegate =
                mockPrintManagerDelegate(() -> Assert.fail("Shouldn't start a printing job."));

        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    printingController.setPendingPrint(
                            new TabPrinter(currentTab), mockPrintManagerDelegate, -1, -1);
                    TabModelUtils.closeCurrentTab(
                            mActivityTestRule.getActivity().getCurrentTabModel());
                    Assert.assertFalse(
                            "currentTab should be closed already.", currentTab.isInitialized());
                    printingController.startPendingPrint();
                });
    }

    /**
     * Test for http://crbug.com/528909 Simulating while a printing job is triggered and printing UI
     * is showing, the corresponding tab is closed, this behaviour is mostly from JavaScript code.
     * Make sure we don't crash and let framework notify user that we can't perform printing job.
     */
    @Test
    @LargeTest
    @Feature({"Printing"})
    public void testPrintCloseWindowBeforeOnWrite() throws Throwable {
        mActivityTestRule.startMainActivityWithURL(URL);
        final Tab currentTab = mActivityTestRule.getActivity().getActivityTab();
        final PrintingControllerImpl printingController = createControllerOnUiThread();

        startControllerOnUiThread(printingController, currentTab);
        callStartOnUiThread(printingController);

        final WaitForOnWriteHelper onWriteFinishedCompleted = new WaitForOnWriteHelper();
        final LayoutResultCallbackWrapper layoutResultCallback =
                new LayoutResultCallbackWrapperMock() {
                    @Override
                    public void onLayoutFinished(PrintDocumentInfo info, boolean changed) {
                        onWriteFinishedCompleted.notifyCalled();
                    }
                };
        callLayoutOnUiThread(
                printingController, null, createDummyPrintAttributes(), layoutResultCallback);

        onWriteFinishedCompleted.waitForCallback("onWriteFinished callback never completed.");

        final WaitForOnWriteHelper onWriteFailedCompleted = new WaitForOnWriteHelper();
        // Create a temporary file to save the PDF.
        final File tempFile = File.createTempFile(TEMP_FILE_NAME, TEMP_FILE_EXTENSION);
        final ParcelFileDescriptor fileDescriptor =
                ParcelFileDescriptor.open(tempFile, ParcelFileDescriptor.MODE_READ_WRITE);
        try {
            ThreadUtils.runOnUiThreadBlocking(
                    () -> {
                        // Close tab.
                        TabModelUtils.closeCurrentTab(
                                mActivityTestRule.getActivity().getCurrentTabModel());
                        Assert.assertFalse(
                                "currentTab should be closed already.", currentTab.isInitialized());

                        final WriteResultCallbackWrapper writeResultCallback =
                                new WriteResultCallbackWrapperMock() {
                                    @Override
                                    public void onWriteFailed(CharSequence error) {
                                        onWriteFailedCompleted.notifyCalled();
                                    }
                                };
                        // Call onWrite.
                        printingController.onWrite(
                                new PageRange[] {PageRange.ALL_PAGES},
                                fileDescriptor,
                                new CancellationSignal(),
                                writeResultCallback);
                    });

            onWriteFailedCompleted.waitForCallback("onWriteFailed callback never completed.");
        } finally {
            // Proper cleanup.
            callFinishOnUiThread(printingController);
            // Close the descriptor, if not closed already.
            fileDescriptor.close();
            TestFileUtil.deleteFile(tempFile.getAbsolutePath());
        }
    }

    /**
     * Test for http://crbug.com/863297 This bug shows Android printing framework could call
     * |PrintDocumentAdapter.onFinish()| before one of |WriteResultCallback.onWrite{Cancelled,
     * Failed, Finished}()| get called. Crash test, pass if there is no crash.
     */
    @Test
    @MediumTest
    @Feature({"Printing"})
    public void testCancelPrintBeforeWriteResultCallbacks() throws Throwable {
        mActivityTestRule.startMainActivityWithURL(URL);

        final WaitForOnWriteHelper onWriteHelper = new WaitForOnWriteHelper();
        final Tab currentTab = mActivityTestRule.getActivity().getActivityTab();
        final PrintingControllerImpl printingController =
                ThreadUtils.runOnUiThreadBlocking(
                        () -> new PrintingControllerImplPdfWritingDone(onWriteHelper));

        startControllerOnUiThread(printingController, currentTab);
        callStartOnUiThread(printingController);

        final WriteResultCallbackWrapper writeResultCallback =
                new WriteResultCallbackWrapperMock() {
                    @Override
                    public void onWriteFinished(PageRange[] pages) {
                        Assert.fail("onWriteFinished shouldn't be called");
                    }

                    @Override
                    public void onWriteFailed(CharSequence error) {
                        Assert.fail("onWriteFailed shouldn't be called");
                    }

                    @Override
                    public void onWriteCancelled() {
                        Assert.fail("onWriteCancelled shouldn't be called");
                    }
                };

        try (TemporaryFileHandler handler = new TemporaryFileHandler()) {
            final LayoutResultCallbackWrapper layoutResultCallback =
                    new LayoutResultCallbackWrapperMock() {
                        @Override
                        public void onLayoutFinished(PrintDocumentInfo info, boolean changed) {
                            printingController.onWrite(
                                    new PageRange[] {PageRange.ALL_PAGES},
                                    handler.getFileDescriptor(),
                                    new CancellationSignal(),
                                    writeResultCallback);
                        }
                    };
            callLayoutOnUiThread(
                    printingController, null, createDummyPrintAttributes(), layoutResultCallback);
            onWriteHelper.waitForCallback("pdfWritingDone never called");
            callFinishOnUiThread(printingController);
        }
    }

    /**
     * Regresstion test for crbug.com/974581. In some cases, native printing code will fail without
     * starting a printing task in Java side. pdfWritingDone() will be called with |pageCount| = 0
     * in this case. We don't need to do anything for this in Java side for now.
     */
    @Test
    @SmallTest
    @Feature({"Printing"})
    public void testPdfWritingDoneCalledWithoutInitailizePrintingTask() {
        mActivityTestRule.startMainActivityWithURL(URL);
        final PrintingControllerImpl controller = createControllerOnUiThread();

        // Calling pdfWritingDone() with |pageCount| = 0 before onWrite() was called. It shouldn't
        // crash.
        ThreadUtils.runOnUiThreadBlocking(() -> controller.pdfWritingDone(0));
    }

    private PrintingControllerImpl createControllerOnUiThread() {
        return ThreadUtils.runOnUiThreadBlocking(
                () -> (PrintingControllerImpl) PrintingControllerImpl.getInstance());
    }

    private PrintAttributes createDummyPrintAttributes() {
        return new PrintAttributes.Builder()
                .setMediaSize(PrintAttributes.MediaSize.ISO_A4)
                .setResolution(new PrintAttributes.Resolution("foo", "bar", 300, 300))
                .setMinMargins(PrintAttributes.Margins.NO_MARGINS)
                .build();
    }

    private PrintManagerDelegate mockPrintManagerDelegate(final Runnable r) {
        return new PrintManagerDelegate() {
            @Override
            public void print(
                    String printJobName,
                    PrintDocumentAdapter documentAdapter,
                    PrintAttributes attributes) {
                if (r != null) r.run();
            }
        };
    }

    private void startControllerOnUiThread(final PrintingControllerImpl controller, final Tab tab) {
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    controller.startPrint(
                            new TabPrinter(tab),
                            /* non-op PrintManagerDelegate */ mockPrintManagerDelegate(null));
                });
    }

    private void callStartOnUiThread(final PrintingControllerImpl controller) {
        ThreadUtils.runOnUiThreadBlocking(() -> controller.onStart());
    }

    private void callLayoutOnUiThread(
            final PrintingControllerImpl controller,
            final PrintAttributes oldAttributes,
            final PrintAttributes newAttributes,
            final LayoutResultCallbackWrapper layoutResultCallback) {
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    controller.onLayout(
                            oldAttributes,
                            newAttributes,
                            new CancellationSignal(),
                            layoutResultCallback,
                            null);
                });
    }

    private void callFinishOnUiThread(final PrintingControllerImpl controller) {
        ThreadUtils.runOnUiThreadBlocking(() -> controller.onFinish());
    }
}