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