• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2015 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 android.print.cts;
18 
19 import static android.print.test.Utils.assertException;
20 import static android.print.test.Utils.eventually;
21 import static android.print.test.Utils.getPrintJob;
22 import static android.print.test.Utils.runOnMainThread;
23 
24 import static org.junit.Assert.assertEquals;
25 import static org.junit.Assert.assertFalse;
26 import static org.junit.Assert.assertNotNull;
27 import static org.junit.Assert.assertTrue;
28 
29 import android.app.Activity;
30 import android.app.PendingIntent;
31 import android.content.ComponentName;
32 import android.content.Context;
33 import android.content.Intent;
34 import android.content.pm.ApplicationInfo;
35 import android.content.pm.PackageInfo;
36 import android.content.pm.PackageManager;
37 import android.graphics.Bitmap;
38 import android.graphics.BitmapFactory;
39 import android.graphics.Canvas;
40 import android.graphics.drawable.Drawable;
41 import android.graphics.drawable.Icon;
42 import android.print.PrintAttributes;
43 import android.print.PrintAttributes.Margins;
44 import android.print.PrintAttributes.MediaSize;
45 import android.print.PrintAttributes.Resolution;
46 import android.print.PrintDocumentAdapter;
47 import android.print.PrintManager;
48 import android.print.PrinterCapabilitiesInfo;
49 import android.print.PrinterId;
50 import android.print.PrinterInfo;
51 import android.print.test.BasePrintTest;
52 import android.print.test.services.FirstPrintService;
53 import android.print.test.services.InfoActivity;
54 import android.print.test.services.PrintServiceCallbacks;
55 import android.print.test.services.PrinterDiscoverySessionCallbacks;
56 import android.print.test.services.SecondPrintService;
57 import android.print.test.services.StubbablePrintService;
58 import android.print.test.services.StubbablePrinterDiscoverySession;
59 import android.printservice.CustomPrinterIconCallback;
60 import android.printservice.PrintJob;
61 import android.printservice.PrintService;
62 import android.support.test.uiautomator.By;
63 import android.support.test.uiautomator.UiDevice;
64 import android.support.test.uiautomator.UiObject;
65 import android.support.test.uiautomator.UiSelector;
66 import android.support.test.uiautomator.Until;
67 
68 import androidx.annotation.NonNull;
69 import androidx.test.runner.AndroidJUnit4;
70 
71 import org.junit.Test;
72 import org.junit.runner.RunWith;
73 
74 import java.util.ArrayList;
75 import java.util.List;
76 
77 /**
78  * Test the interface from a print service to the print manager
79  */
80 @RunWith(AndroidJUnit4.class)
81 public class PrintServicesTest extends BasePrintTest {
82     private static final String PRINTER_NAME = "Test printer";
83 
84     /** The print job processed in the test */
85     private static PrintJob sPrintJob;
86 
87     /** Printer under test */
88     private static PrinterInfo sPrinter;
89 
90     /** The custom printer icon to use */
91     private Icon mIcon;
92 
getDefaultOptionPrinterCapabilites( @onNull PrinterId printerId)93     private @NonNull PrinterCapabilitiesInfo getDefaultOptionPrinterCapabilites(
94             @NonNull PrinterId printerId) {
95         return new PrinterCapabilitiesInfo.Builder(printerId)
96                 .setMinMargins(new Margins(200, 200, 200, 200))
97                 .addMediaSize(MediaSize.ISO_A4, true)
98                 .addResolution(new Resolution("300x300", "300x300", 300, 300), true)
99                 .setColorModes(PrintAttributes.COLOR_MODE_COLOR,
100                         PrintAttributes.COLOR_MODE_COLOR).build();
101     }
102 
103     /**
104      * Create a mock {@link PrinterDiscoverySessionCallbacks} that discovers a single printer with
105      * minimal capabilities.
106      *
107      * @return The mock session callbacks
108      */
createMockPrinterDiscoverySessionCallbacks( String printerName)109     private PrinterDiscoverySessionCallbacks createMockPrinterDiscoverySessionCallbacks(
110             String printerName) {
111         return createMockPrinterDiscoverySessionCallbacks(invocation -> {
112             // Get the session.
113             StubbablePrinterDiscoverySession session =
114                     ((PrinterDiscoverySessionCallbacks) invocation.getMock()).getSession();
115 
116             if (session.getPrinters().isEmpty()) {
117                 List<PrinterInfo> printers = new ArrayList<>();
118 
119                 // Add the printer.
120                 PrinterId printerId = session.getService()
121                         .generatePrinterId(printerName);
122 
123                 Intent infoIntent = new Intent(getActivity(), InfoActivity.class);
124                 infoIntent.putExtra("PRINTER_NAME", PRINTER_NAME);
125 
126                 PendingIntent infoPendingIntent = PendingIntent.getActivity(getActivity(),
127                         0,
128                         infoIntent, PendingIntent.FLAG_UPDATE_CURRENT);
129 
130                 sPrinter = new PrinterInfo.Builder(printerId, printerName,
131                         PrinterInfo.STATUS_IDLE)
132                         .setCapabilities(getDefaultOptionPrinterCapabilites(printerId))
133                         .setDescription("Minimal capabilities")
134                         .setInfoIntent(infoPendingIntent)
135                         .build();
136                 printers.add(sPrinter);
137 
138                 session.addPrinters(printers);
139             }
140 
141             onPrinterDiscoverySessionCreateCalled();
142 
143             return null;
144         }, null, null, null, invocation -> {
145             CustomPrinterIconCallback callback = (CustomPrinterIconCallback) invocation
146                     .getArguments()[2];
147 
148             if (mIcon != null) {
149                 callback.onCustomPrinterIconLoaded(mIcon);
150             }
151             return null;
152         }, null, invocation -> {
153             // Take a note onDestroy was called.
154             onPrinterDiscoverySessionDestroyCalled();
155             return null;
156         });
157     }
158 
159     /**
160      * Get the current progress of #sPrintJob
161      *
162      * @return The current progress
163      *
164      * @throws InterruptedException If the thread was interrupted while setting the progress
165      * @throws Throwable            If anything is unexpected.
166      */
getProgress()167     private float getProgress() throws Throwable {
168         float[] printProgress = new float[1];
169         runOnMainThread(() -> printProgress[0] = sPrintJob.getInfo().getProgress());
170 
171         return printProgress[0];
172     }
173 
174     /**
175      * Get the current status of #sPrintJob
176      *
177      * @return The current status
178      *
179      * @throws InterruptedException If the thread was interrupted while getting the status
180      * @throws Throwable            If anything is unexpected.
181      */
getStatus()182     private CharSequence getStatus() throws Throwable {
183         CharSequence[] printStatus = new CharSequence[1];
184         runOnMainThread(() -> printStatus[0] = sPrintJob.getInfo().getStatus(getActivity()
185                 .getPackageManager()));
186 
187         return printStatus[0];
188     }
189 
190     /**
191      * Check if a print progress is correct.
192      *
193      * @param desiredProgress The expected @{link PrintProgresses}
194      *
195      * @throws Throwable If anything goes wrong or this takes more than 5 seconds
196      */
checkNotification(float desiredProgress, CharSequence desiredStatus)197     private void checkNotification(float desiredProgress, CharSequence desiredStatus)
198             throws Throwable {
199         eventually(() -> assertEquals(desiredProgress, getProgress(), 0.1));
200         eventually(() -> assertEquals(desiredStatus.toString(), getStatus().toString()));
201     }
202 
203     /**
204      * Set a new progress and status for #sPrintJob
205      *
206      * @param progress The new progress to set
207      * @param status   The new status to set
208      *
209      * @throws InterruptedException If the thread was interrupted while setting
210      * @throws Throwable            If anything is unexpected.
211      */
setProgressAndStatus(final float progress, final CharSequence status)212     private void setProgressAndStatus(final float progress, final CharSequence status)
213             throws Throwable {
214         runOnMainThread(() -> {
215             sPrintJob.setProgress(progress);
216             sPrintJob.setStatus(status);
217         });
218     }
219 
220     /**
221      * Progress print job and check the print job state.
222      *
223      * @param progress How much to progress
224      * @param status   The status to set
225      *
226      * @throws Throwable If anything goes wrong.
227      */
progress(float progress, CharSequence status)228     private void progress(float progress, CharSequence status) throws Throwable {
229         setProgressAndStatus(progress, status);
230 
231         // Check that progress of job is correct
232         checkNotification(progress, status);
233     }
234 
235     /**
236      * Create mock service callback for a session.
237      *
238      * @param sessionCallbacks The callbacks of the sessopm
239      */
createMockPrinterServiceCallbacks( final PrinterDiscoverySessionCallbacks sessionCallbacks)240     private PrintServiceCallbacks createMockPrinterServiceCallbacks(
241             final PrinterDiscoverySessionCallbacks sessionCallbacks) {
242         return createMockPrintServiceCallbacks(
243                 invocation -> sessionCallbacks,
244                 invocation -> {
245                     sPrintJob = (PrintJob) invocation.getArguments()[0];
246                     sPrintJob.start();
247                     onPrintJobQueuedCalled();
248 
249                     return null;
250                 }, invocation -> {
251                     sPrintJob = (PrintJob) invocation.getArguments()[0];
252                     sPrintJob.cancel();
253 
254                     return null;
255                 });
256     }
257 
258     /**
259      * Test that the progress and status is propagated correctly.
260      *
261      * @throws Throwable If anything is unexpected.
262      */
263     @Test
264     public void progress() throws Throwable {
265         // Create the session callbacks that we will be checking.
266         PrinterDiscoverySessionCallbacks sessionCallbacks =
267                 createMockPrinterDiscoverySessionCallbacks(PRINTER_NAME);
268 
269         // Create the service callbacks for the first print service.
270         PrintServiceCallbacks serviceCallbacks = createMockPrinterServiceCallbacks(
271                 sessionCallbacks);
272 
273         // Configure the print services.
274         FirstPrintService.setCallbacks(serviceCallbacks);
275 
276         // We don't use the second service, but we have to still configure it
277         SecondPrintService.setCallbacks(createMockPrintServiceCallbacks(null, null, null));
278 
279         // Create a print adapter that respects the print contract.
280         PrintDocumentAdapter adapter = createDefaultPrintDocumentAdapter(1);
281 
282         // Start printing.
283         print(adapter);
284 
285         // Wait for write of the first page.
286         waitForWriteAdapterCallback(1);
287 
288         // Select the printer.
289         selectPrinter(PRINTER_NAME);
290 
291         // Click the print button.
292         clickPrintButton();
293 
294         eventually(() -> {
295             // Answer the dialog for the print service cloud warning
296             answerPrintServicesWarning(true);
297 
298             // Wait until the print job is queued and #sPrintJob is set
299             waitForServiceOnPrintJobQueuedCallbackCalled(1);
300         }, OPERATION_TIMEOUT_MILLIS * 2);
301 
302         // Progress print job and check for appropriate notifications
303         progress(0, "printed 0");
304         progress(0.5f, "printed 50");
305         progress(1, "printed 100");
306 
307         // Call complete from the main thread
308         runOnMainThread(sPrintJob::complete);
309 
310         // Wait for all print jobs to be handled after which the session destroyed.
311         waitForPrinterDiscoverySessionDestroyCallbackCalled(1);
312     }
313 
314     /**
315      * Render a {@link Drawable} into a {@link Bitmap}.
316      *
317      * @param d the drawable to be rendered
318      *
319      * @return the rendered bitmap
320      */
321     private static Bitmap renderDrawable(Drawable d) {
322         Bitmap bitmap = Bitmap.createBitmap(d.getIntrinsicWidth(), d.getIntrinsicHeight(),
323                 Bitmap.Config.ARGB_8888);
324 
325         Canvas canvas = new Canvas(bitmap);
326         d.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
327         d.draw(canvas);
328 
329         return bitmap;
330     }
331 
332     /**
333      * Update the printer
334      *
335      * @param sessionCallbacks The callbacks for the service the printer belongs to
336      * @param printer the new printer to use
337      *
338      * @throws InterruptedException If we were interrupted while the printer was updated.
339      * @throws Throwable            If anything is unexpected.
340      */
341     private void updatePrinter(PrinterDiscoverySessionCallbacks sessionCallbacks,
342             final PrinterInfo printer) throws Throwable {
343         runOnMainThread(() -> {
344             ArrayList<PrinterInfo> printers = new ArrayList<>(1);
345             printers.add(printer);
346             sessionCallbacks.getSession().addPrinters(printers);
347         });
348 
349         // Update local copy of printer
350         sPrinter = printer;
351     }
352 
353     /**
354      * Assert is the printer icon does not match the bitmap. As the icon update might take some time
355      * we try up to 5 seconds.
356      *
357      * @param bitmap The bitmap to match
358      *
359      * @throws Throwable If anything is unexpected.
360      */
361     private void assertThatIconIs(Bitmap bitmap) throws Throwable {
362         eventually(
363                 () -> assertTrue(bitmap.sameAs(renderDrawable(sPrinter.loadIcon(getActivity())))));
364     }
365 
366     /**
367      * Test that the icon get be updated.
368      *
369      * @throws Throwable If anything is unexpected.
370      */
371     @Test
372     public void updateIcon() throws Throwable {
373         // Create the session callbacks that we will be checking.
374         final PrinterDiscoverySessionCallbacks sessionCallbacks =
375                 createMockPrinterDiscoverySessionCallbacks(PRINTER_NAME);
376 
377         // Create the service callbacks for the first print service.
378         PrintServiceCallbacks serviceCallbacks = createMockPrinterServiceCallbacks(
379                 sessionCallbacks);
380 
381         // Configure the print services.
382         FirstPrintService.setCallbacks(serviceCallbacks);
383 
384         // We don't use the second service, but we have to still configure it
385         SecondPrintService.setCallbacks(createMockPrintServiceCallbacks(null, null, null));
386 
387         // Create a print adapter that respects the print contract.
388         PrintDocumentAdapter adapter = createDefaultPrintDocumentAdapter(1);
389 
390         // Start printing.
391         print(adapter);
392 
393         // Open printer selection dropdown list to display icon on screen
394         UiObject destinationSpinner = UiDevice.getInstance(getInstrumentation())
395                 .findObject(new UiSelector().resourceId(
396                         "com.android.printspooler:id/destination_spinner"));
397         destinationSpinner.click();
398 
399         // Get the print service's icon
400         PackageManager packageManager = getActivity().getPackageManager();
401         PackageInfo packageInfo = packageManager.getPackageInfo(
402                 new ComponentName(getActivity(), FirstPrintService.class).getPackageName(), 0);
403         ApplicationInfo appInfo = packageInfo.applicationInfo;
404         Drawable printServiceIcon = appInfo.loadIcon(packageManager);
405 
406         assertThatIconIs(renderDrawable(printServiceIcon));
407 
408         // Update icon to resource
409         updatePrinter(sessionCallbacks,
410                 (new PrinterInfo.Builder(sPrinter)).setIconResourceId(R.drawable.red_printer)
411                 .build());
412 
413         assertThatIconIs(renderDrawable(getActivity().getDrawable(R.drawable.red_printer)));
414 
415         // Update icon to bitmap
416         Bitmap bm = BitmapFactory.decodeResource(getActivity().getResources(),
417                 R.raw.yellow);
418         // Icon will be picked up from the discovery session once setHasCustomPrinterIcon is set
419         mIcon = Icon.createWithBitmap(bm);
420         updatePrinter(sessionCallbacks,
421                 (new PrinterInfo.Builder(sPrinter)).setHasCustomPrinterIcon(true).build());
422 
423         assertThatIconIs(renderDrawable(mIcon.loadDrawable(getActivity())));
424 
425         getUiDevice().pressBack();
426         getUiDevice().pressBack();
427         waitForPrinterDiscoverySessionDestroyCallbackCalled(1);
428     }
429 
430     /**
431      * Test that we cannot call attachBaseContext
432      *
433      * @throws Throwable If anything is unexpected.
434      */
435     @Test
436     public void cannotUseAttachBaseContext() throws Throwable {
437         // Create the session callbacks that we will be checking.
438         final PrinterDiscoverySessionCallbacks sessionCallbacks =
439                 createMockPrinterDiscoverySessionCallbacks(PRINTER_NAME);
440 
441         // Create the service callbacks for the first print service.
442         PrintServiceCallbacks serviceCallbacks = createMockPrinterServiceCallbacks(
443                 sessionCallbacks);
444 
445         // Configure the print services.
446         FirstPrintService.setCallbacks(serviceCallbacks);
447 
448         // Create a print adapter that respects the print contract.
449         PrintDocumentAdapter adapter = createDefaultPrintDocumentAdapter(1);
450 
451         // We don't use the second service, but we have to still configure it
452         SecondPrintService.setCallbacks(createMockPrintServiceCallbacks(null, null, null));
453 
454         // Start printing to set serviceCallbacks.getService()
455         print(adapter);
456         eventually(() -> assertNotNull(serviceCallbacks.getService()));
457 
458         // attachBaseContext should always throw an exception no matter what input value
459         assertException(() -> serviceCallbacks.getService().callAttachBaseContext(null),
460                 IllegalStateException.class);
461         assertException(() -> serviceCallbacks.getService().callAttachBaseContext(getActivity()),
462                 IllegalStateException.class);
463 
464         getUiDevice().pressBack();
465         getUiDevice().pressBack();
466         waitForPrinterDiscoverySessionDestroyCallbackCalled(1);
467     }
468 
469     /**
470      * Test that the active print jobs can be read
471      *
472      * @throws Throwable If anything is unexpected.
473      */
474     @Test
475     public void getActivePrintJobs() throws Throwable {
476         clearPrintSpoolerData();
477 
478         try {
479             PrintManager pm = (PrintManager) getActivity().getSystemService(Context.PRINT_SERVICE);
480 
481             // Configure first print service
482             PrinterDiscoverySessionCallbacks sessionCallbacks1 =
483                     createMockPrinterDiscoverySessionCallbacks("Printer1");
484             PrintServiceCallbacks serviceCallbacks1 = createMockPrinterServiceCallbacks(
485                     sessionCallbacks1);
486             FirstPrintService.setCallbacks(serviceCallbacks1);
487 
488             // Configure second print service
489             PrinterDiscoverySessionCallbacks sessionCallbacks2 =
490                     createMockPrinterDiscoverySessionCallbacks("Printer2");
491             PrintServiceCallbacks serviceCallbacks2 = createMockPrinterServiceCallbacks(
492                     sessionCallbacks2);
493             SecondPrintService.setCallbacks(serviceCallbacks2);
494 
495             // Create a print adapter that respects the print contract.
496             PrintDocumentAdapter adapter = createDefaultPrintDocumentAdapter(1);
497 
498             runOnMainThread(() -> pm.print("job1", adapter, null));
499 
500             // Init services
501             waitForPrinterDiscoverySessionCreateCallbackCalled();
502 
503             waitForWriteAdapterCallback(1);
504             selectPrinter("Printer1");
505 
506             StubbablePrintService firstService = serviceCallbacks1.getService();
507             // Job is not yet confirmed, hence it is not yet "active"
508             runOnMainThread(() -> assertEquals(0, firstService.callGetActivePrintJobs().size()));
509 
510             clickPrintButton();
511 
512             eventually(() -> {
513                 answerPrintServicesWarning(true);
514                 waitForServiceOnPrintJobQueuedCallbackCalled(1);
515             }, OPERATION_TIMEOUT_MILLIS * 2);
516 
517             eventually(() -> runOnMainThread(
518                     () -> assertEquals(1, firstService.callGetActivePrintJobs().size())));
519 
520             // Add another print job to first service
521             resetCounters();
522             runOnMainThread(() -> pm.print("job2", adapter, null));
523             waitForWriteAdapterCallback(1);
524             clickPrintButton();
525             waitForServiceOnPrintJobQueuedCallbackCalled(1);
526 
527             eventually(() -> runOnMainThread(
528                     () -> assertEquals(2, firstService.callGetActivePrintJobs().size())));
529 
530             // Create print job in second service
531             resetCounters();
532             runOnMainThread(() -> pm.print("job3", adapter, null));
533 
534             waitForPrinterDiscoverySessionCreateCallbackCalled();
535 
536             waitForWriteAdapterCallback(1);
537             selectPrinter("Printer2");
538             clickPrintButton();
539 
540             StubbablePrintService secondService = serviceCallbacks2.getService();
541             runOnMainThread(() -> assertEquals(0, secondService.callGetActivePrintJobs().size()));
542 
543             eventually(() -> {
544                 answerPrintServicesWarning(true);
545                 waitForServiceOnPrintJobQueuedCallbackCalled(1);
546             }, OPERATION_TIMEOUT_MILLIS * 2);
547 
548             eventually(() -> runOnMainThread(
549                     () -> assertEquals(1, secondService.callGetActivePrintJobs().size())));
550             runOnMainThread(() -> assertEquals(2, firstService.callGetActivePrintJobs().size()));
551 
552             // Block last print job. Blocked jobs are still considered active
553             runOnMainThread(() -> sPrintJob.block(null));
554             eventually(() -> runOnMainThread(() -> assertTrue(sPrintJob.isBlocked())));
555             runOnMainThread(() -> assertEquals(1, secondService.callGetActivePrintJobs().size()));
556 
557             // Fail last print job. Failed job are not active
558             runOnMainThread(() -> sPrintJob.fail(null));
559             eventually(() -> runOnMainThread(() -> assertTrue(sPrintJob.isFailed())));
560             runOnMainThread(() -> assertEquals(0, secondService.callGetActivePrintJobs().size()));
561 
562             // Cancel job. Canceled jobs are not active
563             runOnMainThread(() -> assertEquals(2, firstService.callGetActivePrintJobs().size()));
564             android.print.PrintJob job2 = getPrintJob(pm, "job2");
565             runOnMainThread(job2::cancel);
566             eventually(() -> runOnMainThread(() -> assertTrue(job2.isCancelled())));
567             runOnMainThread(() -> assertEquals(1, firstService.callGetActivePrintJobs().size()));
568 
569             waitForPrinterDiscoverySessionDestroyCallbackCalled(1);
570         } finally {
571             clearPrintSpoolerData();
572         }
573     }
574 
575     /**
576      * Test that the icon get be updated.
577      *
578      * @throws Throwable If anything is unexpected.
579      */
580     @Test
581     public void selectViaInfoIntent() throws Throwable {
582         ArrayList<String> trackedPrinters = new ArrayList<>();
583 
584         // Create the session callbacks that we will be checking.
585         final PrinterDiscoverySessionCallbacks sessionCallbacks =
586                 createMockPrinterDiscoverySessionCallbacks(invocation -> {
587                     // Get the session.
588                     StubbablePrinterDiscoverySession session =
589                             ((PrinterDiscoverySessionCallbacks) invocation.getMock()).getSession();
590 
591                     PrinterId printer1Id = session.getService().generatePrinterId("Printer1");
592 
593                     PrinterInfo printer1 = new PrinterInfo.Builder(printer1Id, "Printer1",
594                             PrinterInfo.STATUS_IDLE).setCapabilities(
595                             getDefaultOptionPrinterCapabilites(printer1Id)).build();
596 
597                     PrinterId printer2Id = session.getService().generatePrinterId("Printer2");
598 
599                     Intent infoIntent = new Intent(getActivity(), InfoActivity.class);
600                     infoIntent.putExtra("PRINTER_NAME", "Printer2");
601 
602                     PendingIntent infoPendingIntent = PendingIntent.getActivity(getActivity(), 0,
603                             infoIntent, PendingIntent.FLAG_UPDATE_CURRENT);
604 
605                     PrinterInfo printer2 = new PrinterInfo.Builder(printer2Id, "Printer2",
606                             PrinterInfo.STATUS_IDLE)
607                             .setInfoIntent(infoPendingIntent)
608                             .setCapabilities(getDefaultOptionPrinterCapabilites(printer2Id))
609                             .build();
610 
611                     List<PrinterInfo> printers = new ArrayList<>();
612                     printers.add(printer1);
613                     printers.add(printer2);
614                     session.addPrinters(printers);
615 
616                     onPrinterDiscoverySessionCreateCalled();
617 
618                     return null;
619                 }, null, null, invocation -> {
620                     synchronized (trackedPrinters) {
621                         trackedPrinters.add(
622                                 ((PrinterId) invocation.getArguments()[0]).getLocalId());
623                         trackedPrinters.notifyAll();
624                     }
625 
626                     return null;
627                 }, null, invocation -> {
628                     synchronized (trackedPrinters) {
629                         trackedPrinters.remove(
630                                 ((PrinterId) invocation.getArguments()[0]).getLocalId());
631                         trackedPrinters.notifyAll();
632                     }
633 
634                     return null;
635                 }, invocation -> {
636                     onPrinterDiscoverySessionDestroyCalled();
637                     return null;
638                 });
639 
640         // Create the service callbacks for the first print service.
641         PrintServiceCallbacks serviceCallbacks = createMockPrinterServiceCallbacks(
642                 sessionCallbacks);
643 
644         // Configure the print services.
645         FirstPrintService.setCallbacks(serviceCallbacks);
646 
647         // We don't use the second service, but we have to still configure it
648         SecondPrintService.setCallbacks(createMockPrintServiceCallbacks(null, null, null));
649 
650         // Create a print adapter that respects the print contract.
651         PrintDocumentAdapter adapter = createDefaultPrintDocumentAdapter(1);
652 
653         // Start printing.
654         print(adapter);
655 
656         selectPrinter("Printer1");
657 
658         eventually(() -> {
659             synchronized (trackedPrinters) {
660                 assertFalse(trackedPrinters.contains("Printer2"));
661             }
662         });
663 
664         // Enter select printer activity
665         selectPrinter("All printers…");
666 
667         try {
668             InfoActivity.addObserver(activity -> {
669                 Intent intent = activity.getIntent();
670 
671                 assertEquals("Printer2", intent.getStringExtra("PRINTER_NAME"));
672                 assertTrue(intent.getBooleanExtra(PrintService.EXTRA_CAN_SELECT_PRINTER,
673                         false));
674 
675                 activity.setResult(Activity.RESULT_OK,
676                         (new Intent()).putExtra(PrintService.EXTRA_SELECT_PRINTER, true));
677                 activity.finish();
678             });
679 
680             try {
681                 // Wait until printer is selected and thereby tracked
682                 eventually(() -> {
683                     getUiDevice().waitForIdle();
684                     // Open info activity which executes the code above
685                     getUiDevice().wait(
686                             Until.findObject(By.res("com.android.printspooler:id/more_info")),
687                             OPERATION_TIMEOUT_MILLIS).click();
688 
689                     eventually(() -> {
690                         synchronized (trackedPrinters) {
691                             assertTrue(trackedPrinters.contains("Printer2"));
692                         }
693                     }, OPERATION_TIMEOUT_MILLIS  / 2);
694                 }, OPERATION_TIMEOUT_MILLIS * 2);
695             } finally {
696                 InfoActivity.clearObservers();
697             }
698         } finally {
699             getUiDevice().pressBack();
700         }
701 
702         getUiDevice().pressBack();
703         waitForPrinterDiscoverySessionDestroyCallbackCalled(1);
704     }
705 }
706