blob: ccb1f0756a459b91a0fe540db21437dd95adac69 [file] [log] [blame]
/*
* 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 android.app.Activity;
import android.app.PendingIntent;
import android.content.ComponentName;
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.os.Handler;
import android.os.Looper;
import android.os.ParcelFileDescriptor;
import android.os.SystemClock;
import android.print.PageRange;
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.PrintDocumentAdapter.LayoutResultCallback;
import android.print.PrintDocumentAdapter.WriteResultCallback;
import android.print.PrintDocumentInfo;
import android.print.PrinterCapabilitiesInfo;
import android.print.PrinterId;
import android.print.PrinterInfo;
import android.print.cts.services.FirstPrintService;
import android.print.cts.services.PrintServiceCallbacks;
import android.print.cts.services.PrinterDiscoverySessionCallbacks;
import android.print.cts.services.SecondPrintService;
import android.print.cts.services.StubbablePrinterDiscoverySession;
import android.printservice.CustomPrinterIconCallback;
import android.printservice.PrintJob;
import android.support.test.uiautomator.UiDevice;
import android.support.test.uiautomator.UiObject;
import android.support.test.uiautomator.UiSelector;
import junit.framework.AssertionFailedError;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.concurrent.TimeoutException;
/**
* Test the interface from a print service to the print manager
*/
public class PrintServicesTest extends BasePrintTest {
private static final String PRINTER_NAME = "Test printer";
private static final int NUM_PAGES = 2;
/** The print job processed in the test */
private static PrintJob mPrintJob;
/** The current progress of #mPrintJob once read from the system */
private static float mPrintProgress;
/** Printer under test */
private static PrinterInfo mPrinter;
/** The printer discovery session used in this test */
private static StubbablePrinterDiscoverySession mDiscoverySession;
/** The current status of #mPrintJob once read from the system */
private static CharSequence mPrintStatus;
/** The custom printer icon to use */
private Icon mIcon;
/**
* Create a mock {@link PrintDocumentAdapter} that provides {@link #NUM_PAGES} empty pages.
*
* @return The mock adapter
*/
private PrintDocumentAdapter createMockPrintDocumentAdapter() {
final PrintAttributes[] printAttributes = new PrintAttributes[1];
return createMockPrintDocumentAdapter(
new Answer<Void>() {
@Override
public Void answer(InvocationOnMock invocation) throws Throwable {
printAttributes[0] = (PrintAttributes) invocation.getArguments()[1];
LayoutResultCallback callback = (LayoutResultCallback) invocation
.getArguments()[3];
PrintDocumentInfo info = new PrintDocumentInfo.Builder(PRINT_JOB_NAME)
.setContentType(PrintDocumentInfo.CONTENT_TYPE_DOCUMENT)
.setPageCount(NUM_PAGES)
.build();
callback.onLayoutFinished(info, false);
// Mark layout was called.
onLayoutCalled();
return null;
}
}, new Answer<Void>() {
@Override
public Void answer(InvocationOnMock invocation) throws Throwable {
Object[] args = invocation.getArguments();
PageRange[] pages = (PageRange[]) args[0];
ParcelFileDescriptor fd = (ParcelFileDescriptor) args[1];
WriteResultCallback callback = (WriteResultCallback) args[3];
writeBlankPages(printAttributes[0], fd, pages[0].getStart(),
pages[0].getEnd());
fd.close();
callback.onWriteFinished(pages);
// Mark write was called.
onWriteCalled();
return null;
}
}, new Answer<Void>() {
@Override
public Void answer(InvocationOnMock invocation) throws Throwable {
// Mark finish was called.
onFinishCalled();
return null;
}
});
}
/**
* Create a mock {@link PrinterDiscoverySessionCallbacks} that discovers a single printer with
* minimal capabilities.
*
* @return The mock session callbacks
*/
private PrinterDiscoverySessionCallbacks createFirstMockPrinterDiscoverySessionCallbacks() {
return createMockPrinterDiscoverySessionCallbacks(new Answer<Void>() {
@Override
public Void answer(InvocationOnMock invocation) {
// Get the session.
mDiscoverySession = ((PrinterDiscoverySessionCallbacks) invocation.getMock())
.getSession();
if (mDiscoverySession.getPrinters().isEmpty()) {
List<PrinterInfo> printers = new ArrayList<PrinterInfo>();
// Add the printer.
PrinterId printerId = mDiscoverySession.getService()
.generatePrinterId(PRINTER_NAME);
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(), Activity.class);
PendingIntent infoPendingIntent = PendingIntent.getActivity(getActivity(), 0,
infoIntent, PendingIntent.FLAG_IMMUTABLE);
mPrinter = new PrinterInfo.Builder(printerId, PRINTER_NAME,
PrinterInfo.STATUS_IDLE)
.setCapabilities(capabilities)
.setDescription("Minimal capabilities")
.setInfoIntent(infoPendingIntent)
.build();
printers.add(mPrinter);
mDiscoverySession.addPrinters(printers);
}
return null;
}
}, null, null, new Answer<Void>() {
@Override
public Void answer(InvocationOnMock invocation) throws Throwable {
return null;
}
}, new Answer<Void>() {
@Override
public Void answer(InvocationOnMock invocation) throws Throwable {
CustomPrinterIconCallback callback = (CustomPrinterIconCallback) invocation
.getArguments()[2];
if (mIcon != null) {
callback.onCustomPrinterIconLoaded(mIcon);
}
return null;
}
}, null, new Answer<Void>() {
@Override
public Void answer(InvocationOnMock invocation) throws Throwable {
// Take a note onDestroy was called.
onPrinterDiscoverySessionDestroyCalled();
return null;
}
});
}
/**
* Get the current progress of #mPrintJob
*
* @return The current progress
* @throws InterruptedException If the thread was interrupted while setting the progress
*/
private float getProgress() throws InterruptedException {
final PrintServicesTest synchronizer = PrintServicesTest.this;
synchronized (synchronizer) {
Runnable getter = new Runnable() {
@Override
public void run() {
synchronized (synchronizer) {
mPrintProgress = mPrintJob.getInfo().getProgress();
synchronizer.notify();
}
}
};
(new Handler(Looper.getMainLooper())).post(getter);
synchronizer.wait();
}
return mPrintProgress;
}
/**
* Get the current status of #mPrintJob
*
* @return The current status
* @throws InterruptedException If the thread was interrupted while getting the status
*/
private CharSequence getStatus() throws InterruptedException {
final PrintServicesTest synchronizer = PrintServicesTest.this;
synchronized (synchronizer) {
Runnable getter = new Runnable() {
@Override
public void run() {
synchronized (synchronizer) {
mPrintStatus = mPrintJob.getInfo()
.getStatus(getActivity().getPackageManager());
synchronizer.notify();
}
}
};
(new Handler(Looper.getMainLooper())).post(getter);
synchronizer.wait();
}
return mPrintStatus;
}
/**
* Check if a print progress is correct.
*
* @param desiredProgress The expected @{link PrintProgresses}
* @throws Exception If anything goes wrong or this takes more than 5 seconds
*/
private void checkNotification(float desiredProgress,
CharSequence desiredStatus) throws Exception {
final long TIMEOUT = 5000;
final Date start = new Date();
while ((new Date()).getTime() - start.getTime() < TIMEOUT) {
if (desiredProgress == getProgress()
&& desiredStatus.toString().equals(getStatus().toString())) {
return;
}
Thread.sleep(200);
}
throw new TimeoutException("Progress or status not updated in " + TIMEOUT + " ms");
}
/**
* Set a new progress and status for #mPrintJob
*
* @param progress The new progress to set
* @param status The new status to set
* @throws InterruptedException If the thread was interrupted while setting
*/
private void setProgressAndStatus(final float progress, final CharSequence status)
throws InterruptedException {
final PrintServicesTest synchronizer = PrintServicesTest.this;
synchronized (synchronizer) {
Runnable completer = new Runnable() {
@Override
public void run() {
synchronized (synchronizer) {
mPrintJob.setProgress(progress);
mPrintJob.setStatus(status);
synchronizer.notify();
}
}
};
(new Handler(Looper.getMainLooper())).post(completer);
synchronizer.wait();
}
}
/**
* Progress print job and check the print job state.
*
* @param progress How much to progress
* @param status The status to set
* @throws Exception If anything goes wrong.
*/
private void progress(float progress, CharSequence status) throws Exception {
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 createFirstMockPrinterServiceCallbacks(
final PrinterDiscoverySessionCallbacks sessionCallbacks) {
return createMockPrintServiceCallbacks(
new Answer<PrinterDiscoverySessionCallbacks>() {
@Override
public PrinterDiscoverySessionCallbacks answer(InvocationOnMock invocation) {
return sessionCallbacks;
}
},
new Answer<Void>() {
@Override
public Void answer(InvocationOnMock invocation) {
mPrintJob = (PrintJob) invocation.getArguments()[0];
mPrintJob.start();
onPrintJobQueuedCalled();
return null;
}
}, null);
}
/**
* Test that the progress and status is propagated correctly.
*
* @throws Exception If anything is unexpected.
*/
public void testProgress()
throws Exception {
if (!supportsPrinting()) {
return;
}
// Create the session callbacks that we will be checking.
PrinterDiscoverySessionCallbacks sessionCallbacks
= createFirstMockPrinterDiscoverySessionCallbacks();
// Create the service callbacks for the first print service.
PrintServiceCallbacks serviceCallbacks = createFirstMockPrinterServiceCallbacks(
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 = createMockPrintDocumentAdapter();
// 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 #mPrintJob 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
Handler handler = new Handler(Looper.getMainLooper());
Runnable completer = new Runnable() {
@Override
public void run() {
mPrintJob.complete();
}
};
handler.post(completer);
// 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 printer the new printer to use
* @throws InterruptedException If we were interrupted while the printer was updated.
*/
private void updatePrinter(final PrinterInfo printer)
throws InterruptedException {
final PrintServicesTest synchronizer = PrintServicesTest.this;
synchronized (synchronizer) {
Runnable updated = new Runnable() {
@Override
public void run() {
synchronized (synchronizer) {
ArrayList<PrinterInfo> printers = new ArrayList<>(1);
printers.add(printer);
mDiscoverySession.addPrinters(printers);
synchronizer.notifyAll();
}
}
};
(new Handler(Looper.getMainLooper())).post(updated);
synchronizer.wait();
}
// Update local copy of printer
mPrinter = 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
*/
private void assertThatIconIs(Bitmap bitmap) {
final long TIMEOUT = 5000;
final long startMillis = SystemClock.uptimeMillis();
while (true) {
try {
if (bitmap.sameAs(renderDrawable(mPrinter.loadIcon(getActivity())))) {
return;
}
final long elapsedMillis = SystemClock.uptimeMillis() - startMillis;
final long waitMillis = TIMEOUT - elapsedMillis;
if (waitMillis <= 0) {
throw new AssertionFailedError("Icon does not match bitmap");
}
// We do not get notified about the icon update, hence wait and try again.
Thread.sleep(1000);
} catch (InterruptedException ie) {
/* ignore */
}
}
}
/**
* Test that the icon get be updated.
*
* @throws Exception If anything is unexpected.
*/
public void testUpdateIcon()
throws Exception {
if (!supportsPrinting()) {
return;
}
// Create the session callbacks that we will be checking.
final PrinterDiscoverySessionCallbacks sessionCallbacks
= createFirstMockPrinterDiscoverySessionCallbacks();
// Create the service callbacks for the first print service.
PrintServiceCallbacks serviceCallbacks = createFirstMockPrinterServiceCallbacks(
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 = createMockPrintDocumentAdapter();
// 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((new PrinterInfo.Builder(mPrinter)).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_printer);
// Icon will be picked up from the discovery session once setHasCustomPrinterIcon is set
mIcon = Icon.createWithBitmap(bm);
updatePrinter((new PrinterInfo.Builder(mPrinter)).setHasCustomPrinterIcon(true).build());
assertThatIconIs(renderDrawable(mIcon.loadDrawable(getActivity())));
}
}