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