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