From 59b0fbd1048521fed9180d592b689c4aa3d81a97 Mon Sep 17 00:00:00 2001 From: "Philip P. Moltmann" Date: Wed, 28 Sep 2016 17:38:43 -0700 Subject: Update print attributes when printer gets selected Test: Add new non-CTS print workflow tests. These tests will cover general printing workflows such as the situation fixed in this change. Change-Id: I33b6842bba164c45a6afbc09f2e0c9b0a523ef30 --- core/tests/coretests/AndroidManifest.xml | 5 + core/tests/coretests/res/xml/printservice.xml | 3 +- .../coretests/src/android/print/BasePrintTest.java | 40 ++- .../android/print/IPrintManagerParametersTest.java | 41 +-- .../coretests/src/android/print/WorkflowTest.java | 391 +++++++++++++++++++++ .../print/mockservice/AddPrintersActivity.java | 52 +++ .../com/android/printspooler/ui/PrintActivity.java | 4 + 7 files changed, 493 insertions(+), 43 deletions(-) create mode 100644 core/tests/coretests/src/android/print/WorkflowTest.java create mode 100644 core/tests/coretests/src/android/print/mockservice/AddPrintersActivity.java diff --git a/core/tests/coretests/AndroidManifest.xml b/core/tests/coretests/AndroidManifest.xml index c234b6aa314b..ee7861313428 100644 --- a/core/tests/coretests/AndroidManifest.xml +++ b/core/tests/coretests/AndroidManifest.xml @@ -1329,6 +1329,11 @@ android:exported="true"> + + + + android:settingsActivity="android.print.mockservice.SettingsActivity" + android:addPrintersActivity="android.print.mockservice.AddPrintersActivity" /> diff --git a/core/tests/coretests/src/android/print/BasePrintTest.java b/core/tests/coretests/src/android/print/BasePrintTest.java index ca7b5e19fcd3..8ef806290230 100644 --- a/core/tests/coretests/src/android/print/BasePrintTest.java +++ b/core/tests/coretests/src/android/print/BasePrintTest.java @@ -57,8 +57,7 @@ import java.util.concurrent.TimeoutException; * This is the base class for print tests. */ abstract class BasePrintTest { - - private static final long OPERATION_TIMEOUT = 30000; + protected static final long OPERATION_TIMEOUT = 30000; private static final String PM_CLEAR_SUCCESS_OUTPUT = "Success"; private static final int CURRENT_USER_ID = -2; // Mirrors UserHandle.USER_CURRENT @@ -74,6 +73,39 @@ abstract class BasePrintTest { public ActivityTestRule mActivityRule = new ActivityTestRule<>(PrintTestActivity.class, false, true); + /** + * {@link Runnable} that can throw and {@link Exception} + */ + interface Invokable { + /** + * Execute the invokable + * + * @throws Exception + */ + void run() throws Exception; + } + + /** + * Assert that the invokable throws an expectedException + * + * @param invokable The {@link Invokable} to run + * @param expectedClass The {@link Exception} that is supposed to be thrown + */ + void assertException(Invokable invokable, Class expectedClass) + throws Exception { + try { + invokable.run(); + } catch (Exception e) { + if (e.getClass().isAssignableFrom(expectedClass)) { + return; + } else { + throw e; + } + } + + throw new AssertionError("No exception thrown"); + } + /** * Return the UI device * @@ -105,14 +137,14 @@ abstract class BasePrintTest { } @Before - public void setUp() throws Exception { + public void initCounters() throws Exception { // Initialize the latches. mStartCallCounter = new CallCounter(); mStartSessionCallCounter = new CallCounter(); } @After - public void tearDown() throws Exception { + public void exitActivities() throws Exception { // Exit print spooler getUiDevice().pressBack(); getUiDevice().pressBack(); diff --git a/core/tests/coretests/src/android/print/IPrintManagerParametersTest.java b/core/tests/coretests/src/android/print/IPrintManagerParametersTest.java index 75be426e94e1..2e9c8e735fd9 100644 --- a/core/tests/coretests/src/android/print/IPrintManagerParametersTest.java +++ b/core/tests/coretests/src/android/print/IPrintManagerParametersTest.java @@ -41,6 +41,7 @@ import android.print.mockservice.StubbablePrinterDiscoverySession; import android.support.test.filters.LargeTest; import android.support.test.filters.MediumTest; import android.support.test.runner.AndroidJUnit4; +import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -219,50 +220,14 @@ public class IPrintManagerParametersTest extends BasePrintTest { return new PrinterId(getActivity().getComponentName(), "dummy printer"); } - @Override - public void setUp() throws Exception { - super.setUp(); - + @Before + public void setUpMockService() throws Exception { MockPrintService.setCallbacks(createMockCallbacks()); mIPrintManager = IPrintManager.Stub .asInterface(ServiceManager.getService(Context.PRINT_SERVICE)); } - /** - * {@link Runnable} that can throw and {@link Exception} - */ - private interface Invokable { - /** - * Execute the invokable - * - * @throws Exception - */ - void run() throws Exception; - } - - /** - * Assert that the invokable throws an expectedException - * - * @param invokable The {@link Invokable} to run - * @param expectedClass The {@link Exception} that is supposed to be thrown - */ - public void assertException(Invokable invokable, Class expectedClass) - throws Exception { - try { - invokable.run(); - } catch (Exception e) { - if (e.getClass().isAssignableFrom(expectedClass)) { - return; - } else { - throw new AssertionError("Expected: " + expectedClass.getName() + ", got: " - + e.getClass().getName()); - } - } - - throw new AssertionError("No exception thrown"); - } - /** * test IPrintManager.getPrintJobInfo */ diff --git a/core/tests/coretests/src/android/print/WorkflowTest.java b/core/tests/coretests/src/android/print/WorkflowTest.java new file mode 100644 index 000000000000..35cfe223a6bf --- /dev/null +++ b/core/tests/coretests/src/android/print/WorkflowTest.java @@ -0,0 +1,391 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.print; + +import android.graphics.pdf.PdfDocument; +import android.os.Bundle; +import android.os.CancellationSignal; +import android.os.ParcelFileDescriptor; +import android.print.mockservice.AddPrintersActivity; +import android.print.mockservice.MockPrintService; + +import android.print.mockservice.PrinterDiscoverySessionCallbacks; +import android.print.mockservice.StubbablePrinterDiscoverySession; +import android.print.pdf.PrintedPdfDocument; +import android.support.test.filters.LargeTest; +import android.support.test.uiautomator.UiObject; +import android.support.test.uiautomator.UiObjectNotFoundException; +import android.support.test.uiautomator.UiSelector; +import android.util.Log; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeoutException; +import java.util.function.Supplier; + +import static org.junit.Assert.assertEquals; + +/** + * Tests for the basic printing workflows + */ +public class WorkflowTest extends BasePrintTest { + private static final String LOG_TAG = WorkflowTest.class.getSimpleName(); + + private static float sWindowAnimationScaleBefore; + private static float sTransitionAnimationScaleBefore; + private static float sAnimatiorDurationScaleBefore; + + interface InterruptableConsumer { + void accept(T t) throws InterruptedException; + } + + /** + * Execute {@code waiter} until {@code condition} is met. + * + * @param condition Conditions to wait for + * @param waiter Code to execute while waiting + */ + private void waitWithTimeout(Supplier condition, InterruptableConsumer waiter) + throws TimeoutException, InterruptedException { + long startTime = System.currentTimeMillis(); + while (condition.get()) { + long timeLeft = OPERATION_TIMEOUT - (System.currentTimeMillis() - startTime); + if (timeLeft < 0) { + throw new TimeoutException(); + } + + waiter.accept(timeLeft); + } + } + + /** + * Executes a shell command using shell user identity, and return the standard output in + * string. + * + * @param cmd the command to run + * + * @return the standard output of the command + */ + private static String runShellCommand(String cmd) throws IOException { + try (FileInputStream is = new ParcelFileDescriptor.AutoCloseInputStream( + getInstrumentation().getUiAutomation().executeShellCommand(cmd))) { + byte[] buf = new byte[64]; + int bytesRead; + + StringBuilder stdout = new StringBuilder(); + while ((bytesRead = is.read(buf)) != -1) { + stdout.append(new String(buf, 0, bytesRead)); + } + + return stdout.toString(); + } + } + + @BeforeClass + public static void disableAnimations() throws Exception { + try { + sWindowAnimationScaleBefore = Float.parseFloat(runShellCommand( + "settings get global window_animation_scale")); + + runShellCommand("settings put global window_animation_scale 0"); + } catch (NumberFormatException e) { + sWindowAnimationScaleBefore = Float.NaN; + } + try { + sTransitionAnimationScaleBefore = Float.parseFloat(runShellCommand( + "settings get global transition_animation_scale")); + + runShellCommand("settings put global transition_animation_scale 0"); + } catch (NumberFormatException e) { + sTransitionAnimationScaleBefore = Float.NaN; + } + try { + sAnimatiorDurationScaleBefore = Float.parseFloat(runShellCommand( + "settings get global animator_duration_scale")); + + runShellCommand("settings put global animator_duration_scale 0"); + } catch (NumberFormatException e) { + sAnimatiorDurationScaleBefore = Float.NaN; + } + } + + @AfterClass + public static void enableAnimations() throws Exception { + if (sWindowAnimationScaleBefore != Float.NaN) { + runShellCommand( + "settings put global window_animation_scale " + sWindowAnimationScaleBefore); + } + if (sTransitionAnimationScaleBefore != Float.NaN) { + runShellCommand( + "settings put global transition_animation_scale " + + sTransitionAnimationScaleBefore); + } + if (sAnimatiorDurationScaleBefore != Float.NaN) { + runShellCommand( + "settings put global animator_duration_scale " + sAnimatiorDurationScaleBefore); + } + } + + /** Add a printer with a given name and supported mediasize to a session */ + private void addPrinter(StubbablePrinterDiscoverySession session, + String name, PrintAttributes.MediaSize mediaSize) { + PrinterId printerId = session.getService().generatePrinterId(name); + List printers = new ArrayList<>(1); + + PrinterCapabilitiesInfo.Builder builder = + new PrinterCapabilitiesInfo.Builder(printerId); + + builder.setMinMargins(new PrintAttributes.Margins(0, 0, 0, 0)) + .setColorModes(PrintAttributes.COLOR_MODE_COLOR, + PrintAttributes.COLOR_MODE_COLOR) + .addMediaSize(mediaSize, true) + .addResolution(new PrintAttributes.Resolution("300x300", "300x300", 300, 300), + true); + + printers.add(new PrinterInfo.Builder(printerId, name, + PrinterInfo.STATUS_IDLE).setCapabilities(builder.build()).build()); + + session.addPrinters(printers); + } + + /** Find a certain element in the UI and click on it */ + private void clickOn(UiSelector selector) throws UiObjectNotFoundException { + Log.i(LOG_TAG, "Click on " + selector); + UiObject view = getUiDevice().findObject(selector); + view.click(); + getUiDevice().waitForIdle(); + } + + /** Find a certain text in the UI and click on it */ + private void clickOnText(String text) throws UiObjectNotFoundException { + clickOn(new UiSelector().text(text)); + } + + /** Set the printer in the print activity */ + private void setPrinter(String printerName) throws UiObjectNotFoundException { + clickOn(new UiSelector().resourceId("com.android.printspooler:id/destination_spinner")); + + clickOnText(printerName); + } + + /** + * Init mock print servic that returns a single printer by default. + * + * @param sessionRef Where to store the reference to the session once started + */ + private void setMockPrintServiceCallbacks(StubbablePrinterDiscoverySession[] sessionRef) { + MockPrintService.setCallbacks(createMockPrintServiceCallbacks( + inv -> createMockPrinterDiscoverySessionCallbacks(inv2 -> { + synchronized (sessionRef) { + sessionRef[0] = ((PrinterDiscoverySessionCallbacks) inv2.getMock()) + .getSession(); + + addPrinter(sessionRef[0], "1st printer", + PrintAttributes.MediaSize.ISO_A0); + + sessionRef.notifyAll(); + } + return null; + }, + null, null, null, null, null, inv2 -> { + synchronized (sessionRef) { + sessionRef[0] = null; + sessionRef.notifyAll(); + } + return null; + } + ), null, null)); + } + + /** + * Start print operation that just prints a single empty page + * + * @param printAttributesRef Where to store the reference to the print attributes once started + */ + private void print(PrintAttributes[] printAttributesRef) { + print(new PrintDocumentAdapter() { + @Override + public void onStart() { + } + + @Override + public void onLayout(PrintAttributes oldAttributes, PrintAttributes newAttributes, + CancellationSignal cancellationSignal, LayoutResultCallback callback, + Bundle extras) { + callback.onLayoutFinished((new PrintDocumentInfo.Builder("doc")).build(), + !newAttributes.equals(printAttributesRef[0])); + + synchronized (printAttributesRef) { + printAttributesRef[0] = newAttributes; + printAttributesRef.notifyAll(); + } + } + + @Override + public void onWrite(PageRange[] pages, ParcelFileDescriptor destination, + CancellationSignal cancellationSignal, WriteResultCallback callback) { + try { + try { + PrintedPdfDocument document = new PrintedPdfDocument(getActivity(), + printAttributesRef[0]); + try { + PdfDocument.Page page = document.startPage(0); + document.finishPage(page); + try (FileOutputStream os = new FileOutputStream( + destination.getFileDescriptor())) { + document.writeTo(os); + os.flush(); + } + } finally { + document.close(); + } + } finally { + destination.close(); + } + + callback.onWriteFinished(pages); + } catch (IOException e) { + callback.onWriteFailed(e.getMessage()); + } + } + }, null); + } + + @Test + @LargeTest + public void addAndSelectPrinter() throws Exception { + final StubbablePrinterDiscoverySession session[] = new StubbablePrinterDiscoverySession[1]; + final PrintAttributes printAttributes[] = new PrintAttributes[1]; + + setMockPrintServiceCallbacks(session); + print(printAttributes); + + // We are now in the PrintActivity + Log.i(LOG_TAG, "Waiting for session"); + synchronized (session) { + waitWithTimeout(() -> session[0] == null, session::wait); + } + + setPrinter("1st printer"); + + Log.i(LOG_TAG, "Waiting for print attributes to change"); + synchronized (printAttributes) { + waitWithTimeout( + () -> printAttributes[0] == null || !printAttributes[0].getMediaSize().equals( + PrintAttributes.MediaSize.ISO_A0), printAttributes::wait); + } + + setPrinter("All printers\u2026"); + + // We are now in the SelectPrinterActivity + clickOnText("Add printer"); + + // We are now in the AddPrinterActivity + AddPrintersActivity.addObserver( + () -> addPrinter(session[0], "2nd printer", PrintAttributes.MediaSize.ISO_A1)); + + // This executes the observer registered above + clickOn(new UiSelector().text(MockPrintService.class.getCanonicalName()) + .resourceId("com.android.printspooler:id/title")); + + getUiDevice().pressBack(); + AddPrintersActivity.clearObservers(); + + // We are now in the SelectPrinterActivity + clickOnText("2nd printer"); + + // We are now in the PrintActivity + Log.i(LOG_TAG, "Waiting for print attributes to change"); + synchronized (printAttributes) { + waitWithTimeout( + () -> printAttributes[0] == null || !printAttributes[0].getMediaSize().equals( + PrintAttributes.MediaSize.ISO_A1), printAttributes::wait); + } + + getUiDevice().pressBack(); + + // We are back in the test activity + Log.i(LOG_TAG, "Waiting for session to end"); + synchronized (session) { + waitWithTimeout(() -> session[0] != null, session::wait); + } + } + + @Test + @LargeTest + public void abortSelectingPrinter() throws Exception { + final StubbablePrinterDiscoverySession session[] = new StubbablePrinterDiscoverySession[1]; + final PrintAttributes printAttributes[] = new PrintAttributes[1]; + + setMockPrintServiceCallbacks(session); + print(printAttributes); + + // We are now in the PrintActivity + Log.i(LOG_TAG, "Waiting for session"); + synchronized (session) { + waitWithTimeout(() -> session[0] == null, session::wait); + } + + setPrinter("1st printer"); + + Log.i(LOG_TAG, "Waiting for print attributes to change"); + synchronized (printAttributes) { + waitWithTimeout( + () -> printAttributes[0] == null || !printAttributes[0].getMediaSize().equals( + PrintAttributes.MediaSize.ISO_A0), printAttributes::wait); + } + + setPrinter("All printers\u2026"); + + // We are now in the SelectPrinterActivity + clickOnText("Add printer"); + + // We are now in the AddPrinterActivity + AddPrintersActivity.addObserver( + () -> addPrinter(session[0], "2nd printer", PrintAttributes.MediaSize.ISO_A1)); + + // This executes the observer registered above + clickOn(new UiSelector().text(MockPrintService.class.getCanonicalName()) + .resourceId("com.android.printspooler:id/title")); + + getUiDevice().pressBack(); + AddPrintersActivity.clearObservers(); + + // Do not select a new printer, just press back + getUiDevice().pressBack(); + + // We are now in the PrintActivity + // The media size should not change + Log.i(LOG_TAG, "Make sure print attributes did not change"); + Thread.sleep(100); + assertEquals(PrintAttributes.MediaSize.ISO_A0, printAttributes[0].getMediaSize()); + + getUiDevice().pressBack(); + + // We are back in the test activity + Log.i(LOG_TAG, "Waiting for session to end"); + synchronized (session) { + waitWithTimeout(() -> session[0] != null, session::wait); + } + } +} diff --git a/core/tests/coretests/src/android/print/mockservice/AddPrintersActivity.java b/core/tests/coretests/src/android/print/mockservice/AddPrintersActivity.java new file mode 100644 index 000000000000..8f1a9edeacba --- /dev/null +++ b/core/tests/coretests/src/android/print/mockservice/AddPrintersActivity.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.print.mockservice; + +import android.app.Activity; +import android.os.Bundle; +import android.support.annotation.NonNull; + +import java.util.ArrayList; + +public class AddPrintersActivity extends Activity { + private static final ArrayList sObservers = new ArrayList<>(); + + public static void addObserver(@NonNull Runnable observer) { + synchronized (sObservers) { + sObservers.add(observer); + } + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + synchronized (sObservers) { + for (Runnable sObserver : sObservers) { + sObserver.run(); + } + } + + finish(); + } + + public static void clearObservers() { + synchronized (sObservers) { + sObservers.clear(); + } + } +} diff --git a/packages/PrintSpooler/src/com/android/printspooler/ui/PrintActivity.java b/packages/PrintSpooler/src/com/android/printspooler/ui/PrintActivity.java index f688a8e11f6b..b3cfea520767 100644 --- a/packages/PrintSpooler/src/com/android/printspooler/ui/PrintActivity.java +++ b/packages/PrintSpooler/src/com/android/printspooler/ui/PrintActivity.java @@ -760,6 +760,10 @@ public class PrintActivity extends Activity implements RemotePrintDocument.Updat mPrintJob.setPrinterId(printerInfo.getId()); mPrintJob.setPrinterName(printerInfo.getName()); + if (printerInfo.getCapabilities() != null) { + updatePrintAttributesFromCapabilities(printerInfo.getCapabilities()); + } + mDestinationSpinnerAdapter.ensurePrinterInVisibleAdapterPosition(printerInfo); MetricsLogger.action(this, MetricsEvent.ACTION_PRINTER_SELECT_ALL, -- cgit v1.2.3-59-g8ed1b