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