| /* |
| * Copyright (C) 2015 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package android.print.cts; |
| |
| import static android.print.test.Utils.assertException; |
| import static android.print.test.Utils.eventually; |
| import static android.print.test.Utils.getPrintJob; |
| import static android.print.test.Utils.runOnMainThread; |
| |
| import static org.junit.Assert.assertEquals; |
| import static org.junit.Assert.assertFalse; |
| import static org.junit.Assert.assertNotNull; |
| import static org.junit.Assert.assertTrue; |
| |
| import android.app.Activity; |
| import android.app.PendingIntent; |
| import android.content.ComponentName; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.pm.ApplicationInfo; |
| import android.content.pm.PackageInfo; |
| import android.content.pm.PackageManager; |
| import android.graphics.Bitmap; |
| import android.graphics.BitmapFactory; |
| import android.graphics.Canvas; |
| import android.graphics.drawable.Drawable; |
| import android.graphics.drawable.Icon; |
| import android.print.PrintAttributes; |
| import android.print.PrintAttributes.Margins; |
| import android.print.PrintAttributes.MediaSize; |
| import android.print.PrintAttributes.Resolution; |
| import android.print.PrintDocumentAdapter; |
| import android.print.PrintManager; |
| import android.print.PrinterCapabilitiesInfo; |
| import android.print.PrinterId; |
| import android.print.PrinterInfo; |
| import android.print.test.BasePrintTest; |
| import android.print.test.services.FirstPrintService; |
| import android.print.test.services.InfoActivity; |
| import android.print.test.services.PrintServiceCallbacks; |
| import android.print.test.services.PrinterDiscoverySessionCallbacks; |
| import android.print.test.services.SecondPrintService; |
| import android.print.test.services.StubbablePrintService; |
| import android.print.test.services.StubbablePrinterDiscoverySession; |
| import android.printservice.CustomPrinterIconCallback; |
| import android.printservice.PrintJob; |
| import android.printservice.PrintService; |
| import android.support.test.uiautomator.UiDevice; |
| import android.support.test.uiautomator.UiObject; |
| import android.support.test.uiautomator.UiSelector; |
| |
| import androidx.test.runner.AndroidJUnit4; |
| |
| import org.junit.Test; |
| import org.junit.runner.RunWith; |
| |
| import java.util.ArrayList; |
| import java.util.List; |
| |
| /** |
| * Test the interface from a print service to the print manager |
| */ |
| @RunWith(AndroidJUnit4.class) |
| public class PrintServicesTest extends BasePrintTest { |
| private static final String PRINTER_NAME = "Test printer"; |
| |
| /** The print job processed in the test */ |
| private static PrintJob sPrintJob; |
| |
| /** Printer under test */ |
| private static PrinterInfo sPrinter; |
| |
| /** The custom printer icon to use */ |
| private Icon mIcon; |
| |
| /** |
| * Create a mock {@link PrinterDiscoverySessionCallbacks} that discovers a single printer with |
| * minimal capabilities. |
| * |
| * @return The mock session callbacks |
| */ |
| private PrinterDiscoverySessionCallbacks createMockPrinterDiscoverySessionCallbacks( |
| String printerName, ArrayList<String> trackedPrinters) { |
| return createMockPrinterDiscoverySessionCallbacks(invocation -> { |
| // Get the session. |
| StubbablePrinterDiscoverySession session = |
| ((PrinterDiscoverySessionCallbacks) invocation.getMock()).getSession(); |
| |
| if (session.getPrinters().isEmpty()) { |
| List<PrinterInfo> printers = new ArrayList<>(); |
| |
| // Add the printer. |
| PrinterId printerId = session.getService() |
| .generatePrinterId(printerName); |
| |
| PrinterCapabilitiesInfo capabilities = new PrinterCapabilitiesInfo.Builder( |
| printerId) |
| .setMinMargins(new Margins(200, 200, 200, 200)) |
| .addMediaSize(MediaSize.ISO_A4, true) |
| .addResolution(new Resolution("300x300", "300x300", 300, 300), |
| true) |
| .setColorModes(PrintAttributes.COLOR_MODE_COLOR, |
| PrintAttributes.COLOR_MODE_COLOR) |
| .build(); |
| |
| Intent infoIntent = new Intent(getActivity(), InfoActivity.class); |
| infoIntent.putExtra("PRINTER_NAME", PRINTER_NAME); |
| |
| PendingIntent infoPendingIntent = PendingIntent.getActivity(getActivity(), 0, |
| infoIntent, PendingIntent.FLAG_UPDATE_CURRENT); |
| |
| sPrinter = new PrinterInfo.Builder(printerId, printerName, |
| PrinterInfo.STATUS_IDLE) |
| .setCapabilities(capabilities) |
| .setDescription("Minimal capabilities") |
| .setInfoIntent(infoPendingIntent) |
| .build(); |
| printers.add(sPrinter); |
| |
| session.addPrinters(printers); |
| } |
| |
| onPrinterDiscoverySessionCreateCalled(); |
| |
| return null; |
| }, null, null, invocation -> { |
| if (trackedPrinters != null) { |
| synchronized (trackedPrinters) { |
| trackedPrinters |
| .add(((PrinterId) invocation.getArguments()[0]).getLocalId()); |
| trackedPrinters.notifyAll(); |
| } |
| } |
| return null; |
| }, invocation -> { |
| CustomPrinterIconCallback callback = (CustomPrinterIconCallback) invocation |
| .getArguments()[2]; |
| |
| if (mIcon != null) { |
| callback.onCustomPrinterIconLoaded(mIcon); |
| } |
| return null; |
| }, invocation -> { |
| if (trackedPrinters != null) { |
| synchronized (trackedPrinters) { |
| trackedPrinters.remove(((PrinterId) invocation.getArguments()[0]).getLocalId()); |
| trackedPrinters.notifyAll(); |
| } |
| } |
| |
| return null; |
| }, invocation -> { |
| // Take a note onDestroy was called. |
| onPrinterDiscoverySessionDestroyCalled(); |
| return null; |
| }); |
| } |
| |
| /** |
| * Get the current progress of #sPrintJob |
| * |
| * @return The current progress |
| * |
| * @throws InterruptedException If the thread was interrupted while setting the progress |
| * @throws Throwable If anything is unexpected. |
| */ |
| private float getProgress() throws Throwable { |
| float[] printProgress = new float[1]; |
| runOnMainThread(() -> printProgress[0] = sPrintJob.getInfo().getProgress()); |
| |
| return printProgress[0]; |
| } |
| |
| /** |
| * Get the current status of #sPrintJob |
| * |
| * @return The current status |
| * |
| * @throws InterruptedException If the thread was interrupted while getting the status |
| * @throws Throwable If anything is unexpected. |
| */ |
| private CharSequence getStatus() throws Throwable { |
| CharSequence[] printStatus = new CharSequence[1]; |
| runOnMainThread(() -> printStatus[0] = sPrintJob.getInfo().getStatus(getActivity() |
| .getPackageManager())); |
| |
| return printStatus[0]; |
| } |
| |
| /** |
| * Check if a print progress is correct. |
| * |
| * @param desiredProgress The expected @{link PrintProgresses} |
| * |
| * @throws Throwable If anything goes wrong or this takes more than 5 seconds |
| */ |
| private void checkNotification(float desiredProgress, CharSequence desiredStatus) |
| throws Throwable { |
| eventually(() -> assertEquals(desiredProgress, getProgress(), 0.1)); |
| eventually(() -> assertEquals(desiredStatus.toString(), getStatus().toString())); |
| } |
| |
| /** |
| * Set a new progress and status for #sPrintJob |
| * |
| * @param progress The new progress to set |
| * @param status The new status to set |
| * |
| * @throws InterruptedException If the thread was interrupted while setting |
| * @throws Throwable If anything is unexpected. |
| */ |
| private void setProgressAndStatus(final float progress, final CharSequence status) |
| throws Throwable { |
| runOnMainThread(() -> { |
| sPrintJob.setProgress(progress); |
| sPrintJob.setStatus(status); |
| }); |
| } |
| |
| /** |
| * Progress print job and check the print job state. |
| * |
| * @param progress How much to progress |
| * @param status The status to set |
| * |
| * @throws Throwable If anything goes wrong. |
| */ |
| private void progress(float progress, CharSequence status) throws Throwable { |
| setProgressAndStatus(progress, status); |
| |
| // Check that progress of job is correct |
| checkNotification(progress, status); |
| } |
| |
| /** |
| * Create mock service callback for a session. |
| * |
| * @param sessionCallbacks The callbacks of the sessopm |
| */ |
| private PrintServiceCallbacks createMockPrinterServiceCallbacks( |
| final PrinterDiscoverySessionCallbacks sessionCallbacks) { |
| return createMockPrintServiceCallbacks( |
| invocation -> sessionCallbacks, |
| invocation -> { |
| sPrintJob = (PrintJob) invocation.getArguments()[0]; |
| sPrintJob.start(); |
| onPrintJobQueuedCalled(); |
| |
| return null; |
| }, invocation -> { |
| sPrintJob = (PrintJob) invocation.getArguments()[0]; |
| sPrintJob.cancel(); |
| |
| return null; |
| }); |
| } |
| |
| /** |
| * Test that the progress and status is propagated correctly. |
| * |
| * @throws Throwable If anything is unexpected. |
| */ |
| @Test |
| public void progress() throws Throwable { |
| // Create the session callbacks that we will be checking. |
| PrinterDiscoverySessionCallbacks sessionCallbacks |
| = createMockPrinterDiscoverySessionCallbacks(PRINTER_NAME, null); |
| |
| // Create the service callbacks for the first print service. |
| PrintServiceCallbacks serviceCallbacks = createMockPrinterServiceCallbacks( |
| sessionCallbacks); |
| |
| // Configure the print services. |
| FirstPrintService.setCallbacks(serviceCallbacks); |
| |
| // We don't use the second service, but we have to still configure it |
| SecondPrintService.setCallbacks(createMockPrintServiceCallbacks(null, null, null)); |
| |
| // Create a print adapter that respects the print contract. |
| PrintDocumentAdapter adapter = createDefaultPrintDocumentAdapter(1); |
| |
| // Start printing. |
| print(adapter); |
| |
| // Wait for write of the first page. |
| waitForWriteAdapterCallback(1); |
| |
| // Select the printer. |
| selectPrinter(PRINTER_NAME); |
| |
| // Click the print button. |
| clickPrintButton(); |
| |
| // Answer the dialog for the print service cloud warning |
| answerPrintServicesWarning(true); |
| |
| // Wait until the print job is queued and #sPrintJob is set |
| waitForServiceOnPrintJobQueuedCallbackCalled(1); |
| |
| // Progress print job and check for appropriate notifications |
| progress(0, "printed 0"); |
| progress(0.5f, "printed 50"); |
| progress(1, "printed 100"); |
| |
| // Call complete from the main thread |
| runOnMainThread(sPrintJob::complete); |
| |
| // Wait for all print jobs to be handled after which the session destroyed. |
| waitForPrinterDiscoverySessionDestroyCallbackCalled(1); |
| } |
| |
| /** |
| * Render a {@link Drawable} into a {@link Bitmap}. |
| * |
| * @param d the drawable to be rendered |
| * |
| * @return the rendered bitmap |
| */ |
| private static Bitmap renderDrawable(Drawable d) { |
| Bitmap bitmap = Bitmap.createBitmap(d.getIntrinsicWidth(), d.getIntrinsicHeight(), |
| Bitmap.Config.ARGB_8888); |
| |
| Canvas canvas = new Canvas(bitmap); |
| d.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); |
| d.draw(canvas); |
| |
| return bitmap; |
| } |
| |
| /** |
| * Update the printer |
| * |
| * @param sessionCallbacks The callbacks for the service the printer belongs to |
| * @param printer the new printer to use |
| * |
| * @throws InterruptedException If we were interrupted while the printer was updated. |
| * @throws Throwable If anything is unexpected. |
| */ |
| private void updatePrinter(PrinterDiscoverySessionCallbacks sessionCallbacks, |
| final PrinterInfo printer) throws Throwable { |
| runOnMainThread(() -> { |
| ArrayList<PrinterInfo> printers = new ArrayList<>(1); |
| printers.add(printer); |
| sessionCallbacks.getSession().addPrinters(printers); |
| }); |
| |
| // Update local copy of printer |
| sPrinter = printer; |
| } |
| |
| /** |
| * Assert is the printer icon does not match the bitmap. As the icon update might take some time |
| * we try up to 5 seconds. |
| * |
| * @param bitmap The bitmap to match |
| * |
| * @throws Throwable If anything is unexpected. |
| */ |
| private void assertThatIconIs(Bitmap bitmap) throws Throwable { |
| eventually( |
| () -> assertTrue(bitmap.sameAs(renderDrawable(sPrinter.loadIcon(getActivity()))))); |
| } |
| |
| /** |
| * Test that the icon get be updated. |
| * |
| * @throws Throwable If anything is unexpected. |
| */ |
| @Test |
| public void updateIcon() throws Throwable { |
| // Create the session callbacks that we will be checking. |
| final PrinterDiscoverySessionCallbacks sessionCallbacks |
| = createMockPrinterDiscoverySessionCallbacks(PRINTER_NAME, null); |
| |
| // Create the service callbacks for the first print service. |
| PrintServiceCallbacks serviceCallbacks = createMockPrinterServiceCallbacks( |
| sessionCallbacks); |
| |
| // Configure the print services. |
| FirstPrintService.setCallbacks(serviceCallbacks); |
| |
| // We don't use the second service, but we have to still configure it |
| SecondPrintService.setCallbacks(createMockPrintServiceCallbacks(null, null, null)); |
| |
| // Create a print adapter that respects the print contract. |
| PrintDocumentAdapter adapter = createDefaultPrintDocumentAdapter(1); |
| |
| // Start printing. |
| print(adapter); |
| |
| // Open printer selection dropdown list to display icon on screen |
| UiObject destinationSpinner = UiDevice.getInstance(getInstrumentation()) |
| .findObject(new UiSelector().resourceId( |
| "com.android.printspooler:id/destination_spinner")); |
| destinationSpinner.click(); |
| |
| // Get the print service's icon |
| PackageManager packageManager = getActivity().getPackageManager(); |
| PackageInfo packageInfo = packageManager.getPackageInfo( |
| new ComponentName(getActivity(), FirstPrintService.class).getPackageName(), 0); |
| ApplicationInfo appInfo = packageInfo.applicationInfo; |
| Drawable printServiceIcon = appInfo.loadIcon(packageManager); |
| |
| assertThatIconIs(renderDrawable(printServiceIcon)); |
| |
| // Update icon to resource |
| updatePrinter(sessionCallbacks, |
| (new PrinterInfo.Builder(sPrinter)).setIconResourceId(R.drawable.red_printer) |
| .build()); |
| |
| assertThatIconIs(renderDrawable(getActivity().getDrawable(R.drawable.red_printer))); |
| |
| // Update icon to bitmap |
| Bitmap bm = BitmapFactory.decodeResource(getActivity().getResources(), |
| R.raw.yellow); |
| // Icon will be picked up from the discovery session once setHasCustomPrinterIcon is set |
| mIcon = Icon.createWithBitmap(bm); |
| updatePrinter(sessionCallbacks, |
| (new PrinterInfo.Builder(sPrinter)).setHasCustomPrinterIcon(true).build()); |
| |
| assertThatIconIs(renderDrawable(mIcon.loadDrawable(getActivity()))); |
| |
| getUiDevice().pressBack(); |
| getUiDevice().pressBack(); |
| waitForPrinterDiscoverySessionDestroyCallbackCalled(1); |
| } |
| |
| /** |
| * Test that we cannot call attachBaseContext |
| * |
| * @throws Throwable If anything is unexpected. |
| */ |
| @Test |
| public void cannotUseAttachBaseContext() throws Throwable { |
| // Create the session callbacks that we will be checking. |
| final PrinterDiscoverySessionCallbacks sessionCallbacks |
| = createMockPrinterDiscoverySessionCallbacks(PRINTER_NAME, null); |
| |
| // Create the service callbacks for the first print service. |
| PrintServiceCallbacks serviceCallbacks = createMockPrinterServiceCallbacks( |
| sessionCallbacks); |
| |
| // Configure the print services. |
| FirstPrintService.setCallbacks(serviceCallbacks); |
| |
| // Create a print adapter that respects the print contract. |
| PrintDocumentAdapter adapter = createDefaultPrintDocumentAdapter(1); |
| |
| // We don't use the second service, but we have to still configure it |
| SecondPrintService.setCallbacks(createMockPrintServiceCallbacks(null, null, null)); |
| |
| // Start printing to set serviceCallbacks.getService() |
| print(adapter); |
| eventually(() -> assertNotNull(serviceCallbacks.getService())); |
| |
| // attachBaseContext should always throw an exception no matter what input value |
| assertException(() -> serviceCallbacks.getService().callAttachBaseContext(null), |
| IllegalStateException.class); |
| assertException(() -> serviceCallbacks.getService().callAttachBaseContext(getActivity()), |
| IllegalStateException.class); |
| |
| getUiDevice().pressBack(); |
| getUiDevice().pressBack(); |
| waitForPrinterDiscoverySessionDestroyCallbackCalled(1); |
| } |
| |
| /** |
| * Test that the active print jobs can be read |
| * |
| * @throws Throwable If anything is unexpected. |
| */ |
| @Test |
| public void getActivePrintJobs() throws Throwable { |
| clearPrintSpoolerData(); |
| |
| try { |
| PrintManager pm = (PrintManager) getActivity().getSystemService(Context.PRINT_SERVICE); |
| |
| // Configure first print service |
| PrinterDiscoverySessionCallbacks sessionCallbacks1 |
| = createMockPrinterDiscoverySessionCallbacks("Printer1", null); |
| PrintServiceCallbacks serviceCallbacks1 = createMockPrinterServiceCallbacks( |
| sessionCallbacks1); |
| FirstPrintService.setCallbacks(serviceCallbacks1); |
| |
| // Configure second print service |
| PrinterDiscoverySessionCallbacks sessionCallbacks2 |
| = createMockPrinterDiscoverySessionCallbacks("Printer2", null); |
| PrintServiceCallbacks serviceCallbacks2 = createMockPrinterServiceCallbacks( |
| sessionCallbacks2); |
| SecondPrintService.setCallbacks(serviceCallbacks2); |
| |
| // Create a print adapter that respects the print contract. |
| PrintDocumentAdapter adapter = createDefaultPrintDocumentAdapter(1); |
| |
| runOnMainThread(() -> pm.print("job1", adapter, null)); |
| |
| // Init services |
| waitForPrinterDiscoverySessionCreateCallbackCalled(); |
| StubbablePrintService firstService = serviceCallbacks1.getService(); |
| |
| waitForWriteAdapterCallback(1); |
| selectPrinter("Printer1"); |
| |
| // Job is not yet confirmed, hence it is not yet "active" |
| runOnMainThread(() -> assertEquals(0, firstService.callGetActivePrintJobs().size())); |
| |
| clickPrintButton(); |
| answerPrintServicesWarning(true); |
| onPrintJobQueuedCalled(); |
| |
| eventually(() -> runOnMainThread( |
| () -> assertEquals(1, firstService.callGetActivePrintJobs().size()))); |
| |
| // Add another print job to first service |
| resetCounters(); |
| runOnMainThread(() -> pm.print("job2", adapter, null)); |
| waitForWriteAdapterCallback(1); |
| clickPrintButton(); |
| onPrintJobQueuedCalled(); |
| |
| eventually(() -> runOnMainThread( |
| () -> assertEquals(2, firstService.callGetActivePrintJobs().size()))); |
| |
| // Create print job in second service |
| resetCounters(); |
| runOnMainThread(() -> pm.print("job3", adapter, null)); |
| |
| waitForPrinterDiscoverySessionCreateCallbackCalled(); |
| |
| StubbablePrintService secondService = serviceCallbacks2.getService(); |
| runOnMainThread(() -> assertEquals(0, secondService.callGetActivePrintJobs().size())); |
| |
| waitForWriteAdapterCallback(1); |
| selectPrinter("Printer2"); |
| clickPrintButton(); |
| answerPrintServicesWarning(true); |
| onPrintJobQueuedCalled(); |
| |
| eventually(() -> runOnMainThread( |
| () -> assertEquals(1, secondService.callGetActivePrintJobs().size()))); |
| runOnMainThread(() -> assertEquals(2, firstService.callGetActivePrintJobs().size())); |
| |
| // Block last print job. Blocked jobs are still considered active |
| runOnMainThread(() -> sPrintJob.block(null)); |
| eventually(() -> runOnMainThread(() -> assertTrue(sPrintJob.isBlocked()))); |
| runOnMainThread(() -> assertEquals(1, secondService.callGetActivePrintJobs().size())); |
| |
| // Fail last print job. Failed job are not active |
| runOnMainThread(() -> sPrintJob.fail(null)); |
| eventually(() -> runOnMainThread(() -> assertTrue(sPrintJob.isFailed()))); |
| runOnMainThread(() -> assertEquals(0, secondService.callGetActivePrintJobs().size())); |
| |
| // Cancel job. Canceled jobs are not active |
| runOnMainThread(() -> assertEquals(2, firstService.callGetActivePrintJobs().size())); |
| android.print.PrintJob job2 = getPrintJob(pm, "job2"); |
| runOnMainThread(job2::cancel); |
| eventually(() -> runOnMainThread(() -> assertTrue(job2.isCancelled()))); |
| runOnMainThread(() -> assertEquals(1, firstService.callGetActivePrintJobs().size())); |
| |
| waitForPrinterDiscoverySessionDestroyCallbackCalled(1); |
| } finally { |
| clearPrintSpoolerData(); |
| } |
| } |
| |
| /** |
| * Test that the icon get be updated. |
| * |
| * @throws Throwable If anything is unexpected. |
| */ |
| @Test |
| public void selectViaInfoIntent() throws Throwable { |
| ArrayList<String> trackedPrinters = new ArrayList<>(); |
| |
| // Create the session callbacks that we will be checking. |
| final PrinterDiscoverySessionCallbacks sessionCallbacks |
| = createMockPrinterDiscoverySessionCallbacks(PRINTER_NAME, trackedPrinters); |
| |
| // Create the service callbacks for the first print service. |
| PrintServiceCallbacks serviceCallbacks = createMockPrinterServiceCallbacks( |
| sessionCallbacks); |
| |
| // Configure the print services. |
| FirstPrintService.setCallbacks(serviceCallbacks); |
| |
| // We don't use the second service, but we have to still configure it |
| SecondPrintService.setCallbacks(createMockPrintServiceCallbacks(null, null, null)); |
| |
| // Create a print adapter that respects the print contract. |
| PrintDocumentAdapter adapter = createDefaultPrintDocumentAdapter(1); |
| |
| // Start printing. |
| print(adapter); |
| |
| // Enter select printer activity |
| selectPrinter("All printers…"); |
| |
| assertFalse(trackedPrinters.contains(PRINTER_NAME)); |
| |
| InfoActivity.addObserver(activity -> { |
| Intent intent = activity.getIntent(); |
| |
| assertEquals(PRINTER_NAME, intent.getStringExtra("PRINTER_NAME")); |
| assertTrue(intent.getBooleanExtra(PrintService.EXTRA_CAN_SELECT_PRINTER, |
| false)); |
| |
| activity.setResult(Activity.RESULT_OK, |
| (new Intent()).putExtra(PrintService.EXTRA_SELECT_PRINTER, true)); |
| activity.finish(); |
| }); |
| |
| // Open info activity which executed the code above |
| UiObject moreInfoButton = getUiDevice().findObject(new UiSelector().resourceId( |
| "com.android.printspooler:id/more_info")); |
| moreInfoButton.click(); |
| |
| // Wait until printer is selected and thereby tracked |
| eventually(() -> assertTrue(trackedPrinters.contains(PRINTER_NAME))); |
| |
| InfoActivity.clearObservers(); |
| |
| getUiDevice().pressBack(); |
| getUiDevice().pressBack(); |
| waitForPrinterDiscoverySessionDestroyCallbackCalled(1); |
| } |
| } |