diff options
56 files changed, 9489 insertions, 3 deletions
@@ -157,6 +157,15 @@ LOCAL_SRC_FILES += \ core/java/android/os/IUserManager.aidl \ core/java/android/os/IVibratorService.aidl \ core/java/android/service/notification/INotificationListener.aidl \ + core/java/android/print/IPrinterDiscoveryObserver.aidl \ + core/java/android/print/IPrintAdapter.aidl \ + core/java/android/print/IPrintClient.aidl \ + core/java/android/print/IPrintProgressListener.aidl \ + core/java/android/print/IPrintManager.aidl \ + core/java/android/print/IPrintSpoolerService.aidl \ + core/java/android/print/IPrintSpoolerServiceCallbacks.aidl \ + core/java/android/printservice/IPrintService.aidl \ + core/java/android/printservice/IPrintServiceClient.aidl \ core/java/android/service/dreams/IDreamManager.aidl \ core/java/android/service/dreams/IDreamService.aidl \ core/java/android/service/wallpaper/IWallpaperConnection.aidl \ diff --git a/api/current.txt b/api/current.txt index 7281d01..7c7ebb2 100644 --- a/api/current.txt +++ b/api/current.txt @@ -23,6 +23,7 @@ package android { field public static final java.lang.String BIND_DEVICE_ADMIN = "android.permission.BIND_DEVICE_ADMIN"; field public static final java.lang.String BIND_INPUT_METHOD = "android.permission.BIND_INPUT_METHOD"; field public static final java.lang.String BIND_NOTIFICATION_LISTENER_SERVICE = "android.permission.BIND_NOTIFICATION_LISTENER_SERVICE"; + field public static final java.lang.String BIND_PRINT_SERVICE = "android.permission.BIND_PRINT_SERVICE"; field public static final java.lang.String BIND_REMOTEVIEWS = "android.permission.BIND_REMOTEVIEWS"; field public static final java.lang.String BIND_TEXT_SERVICE = "android.permission.BIND_TEXT_SERVICE"; field public static final java.lang.String BIND_VPN_SERVICE = "android.permission.BIND_VPN_SERVICE"; @@ -254,6 +255,7 @@ package android { field public static final int activityCloseExitAnimation = 16842939; // 0x10100bb field public static final int activityOpenEnterAnimation = 16842936; // 0x10100b8 field public static final int activityOpenExitAnimation = 16842937; // 0x10100b9 + field public static final int addPrintersActivity = 16843747; // 0x10103e3 field public static final int addStatesFromChildren = 16842992; // 0x10100f0 field public static final int adjustViewBounds = 16843038; // 0x101011e field public static final int alertDialogIcon = 16843605; // 0x1010355 @@ -1142,6 +1144,7 @@ package android { field public static final int valueTo = 16843487; // 0x10102df field public static final int valueType = 16843488; // 0x10102e0 field public static final int variablePadding = 16843157; // 0x1010195 + field public static final int vendor = 16843748; // 0x10103e4 field public static final int versionCode = 16843291; // 0x101021b field public static final int versionName = 16843292; // 0x101021c field public static final int verticalCorrection = 16843322; // 0x101023a @@ -5766,6 +5769,7 @@ package android.content { field public static final java.lang.String NOTIFICATION_SERVICE = "notification"; field public static final java.lang.String NSD_SERVICE = "servicediscovery"; field public static final java.lang.String POWER_SERVICE = "power"; + field public static final java.lang.String PRINT_SERVICE = "print"; field public static final java.lang.String SEARCH_SERVICE = "search"; field public static final java.lang.String SENSOR_SERVICE = "sensor"; field public static final java.lang.String STORAGE_SERVICE = "storage"; @@ -18385,6 +18389,237 @@ package android.preference { } +package android.print { + + public final class PageRange implements android.os.Parcelable { + method public int describeContents(); + method public int getEnd(); + method public int getStart(); + method public void writeToParcel(android.os.Parcel, int); + field public static final android.print.PageRange ALL_PAGES; + field public static final android.os.Parcelable.Creator CREATOR; + } + + public abstract class PrintAdapter { + ctor public PrintAdapter(); + method public abstract android.print.PrintAdapterInfo getInfo(); + method public void onFinish(); + method public abstract void onPrint(java.util.List<android.print.PageRange>, java.io.FileDescriptor, android.os.CancellationSignal, android.print.PrintAdapter.PrintProgressCallback); + method public boolean onPrintAttributesChanged(android.print.PrintAttributes); + method public void onStart(); + } + + public static abstract class PrintAdapter.PrintProgressCallback { + method public void onPrintFailed(java.lang.CharSequence); + method public void onPrintFinished(java.util.List<android.print.PageRange>); + } + + public final class PrintAdapterInfo implements android.os.Parcelable { + method public int describeContents(); + method public int getFlags(); + method public int getPageCount(); + method public void writeToParcel(android.os.Parcel, int); + field public static final android.os.Parcelable.Creator CREATOR; + field public static final int PAGE_COUNT_UNKNOWN = -1; // 0xffffffff + } + + public static final class PrintAdapterInfo.Builder { + ctor public PrintAdapterInfo.Builder(); + method public android.print.PrintAdapterInfo create(); + method public android.print.PrintAdapterInfo.Builder setFlags(int); + method public android.print.PrintAdapterInfo.Builder setPageCount(int); + } + + public final class PrintAttributes implements android.os.Parcelable { + method public void clear(); + method public int describeContents(); + method public int getColorMode(); + method public int getCopies(); + method public int getDuplexMode(); + method public int getFittingMode(); + method public android.print.PrintAttributes.Tray getInputTray(); + method public android.print.PrintAttributes.Margins getMargins(); + method public android.print.PrintAttributes.MediaSize getMediaSize(); + method public int getOrientation(); + method public android.print.PrintAttributes.Tray getOutputTray(); + method public android.print.PrintAttributes.Resolution getResolution(); + method public void writeToParcel(android.os.Parcel, int); + field public static final int COLOR_MODE_COLOR = 2; // 0x2 + field public static final int COLOR_MODE_MONOCHROME = 1; // 0x1 + field public static final android.os.Parcelable.Creator CREATOR; + field public static final int DUPLEX_MODE_LONG_EDGE = 2; // 0x2 + field public static final int DUPLEX_MODE_NONE = 1; // 0x1 + field public static final int DUPLEX_MODE_SHORT_EDGE = 4; // 0x4 + field public static final int FITTING_MODE_FIT_TO_PAGE = 2; // 0x2 + field public static final int FITTING_MODE_NONE = 1; // 0x1 + field public static final int ORIENTATION_LANDSCAPE = 2; // 0x2 + field public static final int ORIENTATION_PORTRAIT = 1; // 0x1 + } + + public static final class PrintAttributes.Builder { + ctor public PrintAttributes.Builder(); + method public android.print.PrintAttributes create(); + method public android.print.PrintAttributes.Builder setColorMode(int); + method public android.print.PrintAttributes.Builder setCopyCount(int); + method public android.print.PrintAttributes.Builder setDuplexMode(int); + method public android.print.PrintAttributes.Builder setFittingMode(int); + method public android.print.PrintAttributes.Builder setInputTray(android.print.PrintAttributes.Tray); + method public android.print.PrintAttributes.Builder setMargins(android.print.PrintAttributes.Margins); + method public android.print.PrintAttributes.Builder setMediaSize(android.print.PrintAttributes.MediaSize); + method public android.print.PrintAttributes.Builder setOrientation(int); + method public android.print.PrintAttributes.Builder setOutputTray(android.print.PrintAttributes.Tray); + method public android.print.PrintAttributes.Builder setResolution(android.print.PrintAttributes.Resolution); + } + + public static final class PrintAttributes.Margins { + ctor public PrintAttributes.Margins(int, int, int, int); + method public int getBottomMils(); + method public int getLeftMils(); + method public int getRightMils(); + method public int getTopMils(); + } + + public static final class PrintAttributes.MediaSize { + ctor public PrintAttributes.MediaSize(java.lang.String, java.lang.String, int, int, int); + method public int getHeightMils(); + method public java.lang.String getId(); + method public java.lang.CharSequence getLabel(android.content.pm.PackageManager); + method public int getWidthMils(); + field public static final android.print.PrintAttributes.MediaSize ISO_A0; + field public static final android.print.PrintAttributes.MediaSize ISO_A1; + field public static final android.print.PrintAttributes.MediaSize ISO_A10; + field public static final android.print.PrintAttributes.MediaSize ISO_A2; + field public static final android.print.PrintAttributes.MediaSize ISO_A3; + field public static final android.print.PrintAttributes.MediaSize ISO_A4; + field public static final android.print.PrintAttributes.MediaSize ISO_A5; + field public static final android.print.PrintAttributes.MediaSize ISO_A6; + field public static final android.print.PrintAttributes.MediaSize ISO_A7; + field public static final android.print.PrintAttributes.MediaSize ISO_A8; + field public static final android.print.PrintAttributes.MediaSize ISO_A9; + field public static final android.print.PrintAttributes.MediaSize ISO_B0; + field public static final android.print.PrintAttributes.MediaSize ISO_B1; + field public static final android.print.PrintAttributes.MediaSize ISO_B10; + field public static final android.print.PrintAttributes.MediaSize ISO_B2; + field public static final android.print.PrintAttributes.MediaSize ISO_B3; + field public static final android.print.PrintAttributes.MediaSize ISO_B4; + field public static final android.print.PrintAttributes.MediaSize ISO_B5; + field public static final android.print.PrintAttributes.MediaSize ISO_B6; + field public static final android.print.PrintAttributes.MediaSize ISO_B7; + field public static final android.print.PrintAttributes.MediaSize ISO_B8; + field public static final android.print.PrintAttributes.MediaSize ISO_B9; + field public static final android.print.PrintAttributes.MediaSize ISO_C0; + field public static final android.print.PrintAttributes.MediaSize ISO_C1; + field public static final android.print.PrintAttributes.MediaSize ISO_C10; + field public static final android.print.PrintAttributes.MediaSize ISO_C2; + field public static final android.print.PrintAttributes.MediaSize ISO_C3; + field public static final android.print.PrintAttributes.MediaSize ISO_C4; + field public static final android.print.PrintAttributes.MediaSize ISO_C5; + field public static final android.print.PrintAttributes.MediaSize ISO_C6; + field public static final android.print.PrintAttributes.MediaSize ISO_C7; + field public static final android.print.PrintAttributes.MediaSize ISO_C8; + field public static final android.print.PrintAttributes.MediaSize ISO_C9; + field public static final android.print.PrintAttributes.MediaSize NA_GOVT_LETTER; + field public static final android.print.PrintAttributes.MediaSize NA_JUNIOR_LEGAL; + field public static final android.print.PrintAttributes.MediaSize NA_LEDGER; + field public static final android.print.PrintAttributes.MediaSize NA_LEGAL; + field public static final android.print.PrintAttributes.MediaSize NA_LETTER; + field public static final android.print.PrintAttributes.MediaSize NA_TBLOID; + } + + public static final class PrintAttributes.Resolution { + ctor public PrintAttributes.Resolution(java.lang.String, java.lang.String, int, int, int); + method public int getHorizontalDpi(); + method public java.lang.String getId(); + method public java.lang.CharSequence getLabel(android.content.pm.PackageManager); + method public int getVerticalDpi(); + } + + public static final class PrintAttributes.Tray { + ctor public PrintAttributes.Tray(java.lang.String, java.lang.String, int); + method public java.lang.String getId(); + method public java.lang.CharSequence getLabel(android.content.pm.PackageManager); + } + + public final class PrintJob { + method public void cancel(); + method public int getId(); + method public android.print.PrintJobInfo getInfo(); + } + + public final class PrintJobInfo implements android.os.Parcelable { + method public int describeContents(); + method public android.print.PrintAttributes getAttributes(); + method public int getId(); + method public java.lang.CharSequence getLabel(); + method public android.print.PageRange[] getPageRanges(); + method public android.print.PrinterId getPrinterId(); + method public int getState(); + method public java.lang.String getTag(); + method public void writeToParcel(android.os.Parcel, int); + field public static final android.os.Parcelable.Creator CREATOR; + field public static final int PRINT_JOB_ID_UNDEFINED = -1; // 0xffffffff + field public static final int STATE_CANCELED = 6; // 0x6 + field public static final int STATE_COMPLETED = 4; // 0x4 + field public static final int STATE_CREATED = 1; // 0x1 + field public static final int STATE_FAILED = 5; // 0x5 + field public static final int STATE_QUEUED = 2; // 0x2 + field public static final int STATE_STARTED = 3; // 0x3 + } + + public final class PrintManager { + method public java.util.List<android.print.PrintJob> getPrintJobs(); + method public android.print.PrintJob print(java.lang.String, java.io.File, android.print.PrintAttributes); + method public android.print.PrintJob print(java.lang.String, android.print.PrintAdapter, android.print.PrintAttributes); + } + + public static abstract interface PrintManager.PrintJobStateListener { + method public abstract void onStateChanged(int); + } + + public final class PrinterId implements android.os.Parcelable { + method public int describeContents(); + method public java.lang.String getLocalId(); + method public void writeToParcel(android.os.Parcel, int); + field public static final android.os.Parcelable.Creator CREATOR; + } + + public final class PrinterInfo implements android.os.Parcelable { + method public int describeContents(); + method public int getColorModes(); + method public void getDefaults(android.print.PrintAttributes); + method public int getDuplexModes(); + method public int getFittingModes(); + method public android.print.PrinterId getId(); + method public java.util.List<android.print.PrintAttributes.Tray> getInputTrays(); + method public java.lang.CharSequence getLabel(); + method public java.util.List<android.print.PrintAttributes.MediaSize> getMediaSizes(); + method public android.print.PrintAttributes.Margins getMinMargins(); + method public int getOrientations(); + method public java.util.List<android.print.PrintAttributes.Tray> getOutputTrays(); + method public java.util.List<android.print.PrintAttributes.Resolution> getResolutions(); + method public int getStatus(); + method public void writeToParcel(android.os.Parcel, int); + field public static final android.os.Parcelable.Creator CREATOR; + field public static final int STATUS_READY = 1; // 0x1 + } + + public static final class PrinterInfo.Builder { + ctor public PrinterInfo.Builder(android.print.PrinterId, java.lang.CharSequence); + method public android.print.PrinterInfo.Builder addInputTray(android.print.PrintAttributes.Tray, boolean); + method public android.print.PrinterInfo.Builder addMediaSize(android.print.PrintAttributes.MediaSize, boolean); + method public android.print.PrinterInfo.Builder addOutputTray(android.print.PrintAttributes.Tray, boolean); + method public android.print.PrinterInfo.Builder addResolution(android.print.PrintAttributes.Resolution, boolean); + method public android.print.PrinterInfo create(); + method public android.print.PrinterInfo.Builder setColorModes(int, int); + method public android.print.PrinterInfo.Builder setDuplexModes(int, int); + method public android.print.PrinterInfo.Builder setFittingModes(int, int); + method public android.print.PrinterInfo.Builder setMinMargins(android.print.PrintAttributes.Margins, android.print.PrintAttributes.Margins); + method public android.print.PrinterInfo.Builder setOrientations(int, int); + method public android.print.PrinterInfo.Builder setStatus(int); + } + +} + package android.print.pdf { public final class PdfDocument { @@ -18418,6 +18653,40 @@ package android.print.pdf { } +package android.printservice { + + public final class PrintJob { + method public boolean cancel(); + method public boolean complete(); + method public boolean fail(java.lang.CharSequence); + method public final java.io.FileDescriptor getData(); + method public int getId(); + method public android.print.PrintJobInfo getInfo(); + method public boolean isQueued(); + method public boolean isStarted(); + method public boolean setTag(java.lang.String); + method public boolean start(); + } + + public abstract class PrintService extends android.app.Service { + ctor public PrintService(); + method public final void addDiscoveredPrinters(java.util.List<android.print.PrinterInfo>); + method public final android.print.PrinterId generatePrinterId(java.lang.String); + method public final java.util.List<android.printservice.PrintJob> getPrintJobs(); + method public final android.os.IBinder onBind(android.content.Intent); + method protected void onConnected(); + method protected void onDisconnected(); + method protected abstract void onPrintJobQueued(android.printservice.PrintJob); + method protected void onRequestCancelPrintJob(android.printservice.PrintJob); + method protected abstract void onStartPrinterDiscovery(); + method protected abstract void onStopPrinterDiscovery(); + method public final void removeDiscoveredPrinters(java.util.List<android.print.PrinterId>); + field public static final java.lang.String SERVICE_INTERFACE = "android.printservice.PrintService"; + field public static final java.lang.String SERVICE_META_DATA = "android.printservice"; + } + +} + package android.provider { public final class AlarmClock { diff --git a/core/java/android/app/ContextImpl.java b/core/java/android/app/ContextImpl.java index c6f3fb8..992d8b7 100644 --- a/core/java/android/app/ContextImpl.java +++ b/core/java/android/app/ContextImpl.java @@ -91,6 +91,8 @@ import android.os.UserHandle; import android.os.SystemVibrator; import android.os.UserManager; import android.os.storage.StorageManager; +import android.print.IPrintManager; +import android.print.PrintManager; import android.telephony.TelephonyManager; import android.content.ClipboardManager; import android.util.AndroidRuntimeException; @@ -548,6 +550,15 @@ class ContextImpl extends Context { registerService(CAMERA_SERVICE, new StaticServiceFetcher() { public Object createStaticService() { return new CameraManager(); + } + }); + + registerService(PRINT_SERVICE, new ServiceFetcher() { + public Object createService(ContextImpl ctx) { + IBinder iBinder = ServiceManager.getService(Context.PRINT_SERVICE); + IPrintManager service = IPrintManager.Stub.asInterface(iBinder); + return new PrintManager(ctx.getOuterContext(), service, UserHandle.myUserId(), + UserHandle.getAppId(Process.myUid())); }}); } diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java index 35e51a6..7c9117c 100644 --- a/core/java/android/content/Context.java +++ b/core/java/android/content/Context.java @@ -45,7 +45,6 @@ import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; -import java.util.List; /** * Interface to global information about an application environment. This is @@ -2294,6 +2293,15 @@ public abstract class Context { public static final String CAMERA_SERVICE = "camera"; /** + * {@link android.print.PrintManager} for printing and managing + * printers and print taks. + * + * @see #getSystemService + * @see android.print.PrintManager + */ + public static final String PRINT_SERVICE = "print"; + + /** * Determine whether the given permission is allowed for a particular * process and user ID running in the system. * diff --git a/core/java/android/net/Uri.java b/core/java/android/net/Uri.java index 4b022d9..c5d2c77 100644 --- a/core/java/android/net/Uri.java +++ b/core/java/android/net/Uri.java @@ -19,7 +19,6 @@ package android.net; import android.os.Environment; import android.os.Parcel; import android.os.Parcelable; -import android.os.Environment.UserEnvironment; import android.os.StrictMode; import android.util.Log; import java.io.File; diff --git a/core/java/android/os/CancellationSignal.java b/core/java/android/os/CancellationSignal.java index dcba9b7..e8053d5 100644 --- a/core/java/android/os/CancellationSignal.java +++ b/core/java/android/os/CancellationSignal.java @@ -17,7 +17,6 @@ package android.os; import android.os.ICancellationSignal; -import android.os.ICancellationSignal.Stub; /** * Provides the ability to cancel an operation in progress. diff --git a/core/java/android/print/IPrintAdapter.aidl b/core/java/android/print/IPrintAdapter.aidl new file mode 100644 index 0000000..a9b4fb7 --- /dev/null +++ b/core/java/android/print/IPrintAdapter.aidl @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2013 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; + +import android.os.ParcelFileDescriptor; +import android.print.IPrintProgressListener; +import android.print.PageRange; +import android.print.PrintAttributes; + +/** + * Interface for communication with the print adapter object. + * + * @hide + */ +oneway interface IPrintAdapter { + void start(); + void printAttributesChanged(in PrintAttributes attributes); + void print(in List<PageRange> pages, in ParcelFileDescriptor fd, + IPrintProgressListener progressListener); + void finish(); +} diff --git a/core/java/android/print/IPrintClient.aidl b/core/java/android/print/IPrintClient.aidl new file mode 100644 index 0000000..3f39d08 --- /dev/null +++ b/core/java/android/print/IPrintClient.aidl @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2013 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; + +import android.content.IntentSender; + +/** + * Interface for communication with a printing app. + * + * @see android.print.IPrintClientCallback + * + * @hide + */ +oneway interface IPrintClient { + void startPrintJobConfigActivity(in IntentSender intent); +} diff --git a/core/java/android/print/IPrintManager.aidl b/core/java/android/print/IPrintManager.aidl new file mode 100644 index 0000000..ff9877e --- /dev/null +++ b/core/java/android/print/IPrintManager.aidl @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2013 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; + +import android.os.ICancellationSignal; +import android.print.IPrintAdapter; +import android.print.IPrintClient; +import android.print.IPrinterDiscoveryObserver; +import android.print.PrinterId; +import android.print.PrintJobInfo; +import android.print.PrintAttributes; + +/** + * Interface for communication with the core print manager service. + * + * @hide + */ +interface IPrintManager { + List<PrintJobInfo> getPrintJobs(int appId, int userId); + PrintJobInfo getPrintJob(int printJobId, int appId, int userId); + PrintJobInfo print(String printJobName, in IPrintClient client, in IPrintAdapter printAdapter, + in PrintAttributes attributes, int appId, int userId); + void cancelPrintJob(int printJobId, int appId, int userId); + void onPrintJobQueued(in PrinterId printerId, in PrintJobInfo printJob); + void startDiscoverPrinters(IPrinterDiscoveryObserver observer); + void stopDiscoverPrinters(); +} diff --git a/core/java/android/print/IPrintProgressListener.aidl b/core/java/android/print/IPrintProgressListener.aidl new file mode 100644 index 0000000..2c0d607 --- /dev/null +++ b/core/java/android/print/IPrintProgressListener.aidl @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2013 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; + +import android.os.ICancellationSignal; +import android.print.PageRange; +import android.print.PrintAdapterInfo; + +/** + * Callbacks for observing the print progress (writing of printed content) + * of a PrintAdapter. + * + * @hide + */ +oneway interface IPrintProgressListener { + void onWriteStarted(in PrintAdapterInfo info, ICancellationSignal cancellationSignal); + void onWriteFinished(in List<PageRange> pages); + void onWriteFailed(CharSequence error); +} diff --git a/core/java/android/print/IPrintSpoolerService.aidl b/core/java/android/print/IPrintSpoolerService.aidl new file mode 100644 index 0000000..e84d592 --- /dev/null +++ b/core/java/android/print/IPrintSpoolerService.aidl @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2013 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; + +import android.content.ComponentName; +import android.os.ParcelFileDescriptor; +import android.print.IPrintAdapter; +import android.print.IPrintClient; +import android.print.IPrintSpoolerServiceCallbacks; +import android.print.PrinterInfo; +import android.print.PrintAttributes; + +/** + * Interface for communication with the print spooler service. + * + * @see android.print.IPrintSpoolerServiceCallbacks + * + * @hide + */ +oneway interface IPrintSpoolerService { + void getPrintJobs(IPrintSpoolerServiceCallbacks callback, in ComponentName componentName, + int state, int appId, int sequence); + void getPrintJob(int printJobId, IPrintSpoolerServiceCallbacks callback, + int appId, int sequence); + void createPrintJob(String printJobName, in IPrintClient client, in IPrintAdapter printAdapter, + in PrintAttributes attributes, IPrintSpoolerServiceCallbacks callback, int appId, + int sequence); + void cancelPrintJob(int printJobId, IPrintSpoolerServiceCallbacks callback, + int appId, int sequence); + void setPrintJobState(int printJobId, int status, IPrintSpoolerServiceCallbacks callback, + int sequence); + void setPrintJobTag(int printJobId, String tag, IPrintSpoolerServiceCallbacks callback, + int sequence); + void writePrintJobData(in ParcelFileDescriptor fd, int printJobId); +}
\ No newline at end of file diff --git a/core/java/android/print/IPrintSpoolerServiceCallbacks.aidl b/core/java/android/print/IPrintSpoolerServiceCallbacks.aidl new file mode 100644 index 0000000..0c51913 --- /dev/null +++ b/core/java/android/print/IPrintSpoolerServiceCallbacks.aidl @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2013 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; + +import android.print.PrintJobInfo; +import java.util.List; + +/** + * Callbacks for communication with the print spooler service. + * + * @see android.print.IPrintSpoolerService + * + * @hide + */ +oneway interface IPrintSpoolerServiceCallbacks { + void onGetPrintJobsResult(in List<PrintJobInfo> printJob, int sequence); + void onGetPrintJobInfoResult(in PrintJobInfo printJob, int sequence); + void onCreatePrintJobResult(in PrintJobInfo printJob, int sequence); + void onCancelPrintJobResult(boolean canceled, int sequence); + void onSetPrintJobStateResult(boolean success, int sequence); + void onSetPrintJobTagResult(boolean success, int sequence); +} diff --git a/core/java/android/print/IPrinterDiscoveryObserver.aidl b/core/java/android/print/IPrinterDiscoveryObserver.aidl new file mode 100644 index 0000000..39aeb8c --- /dev/null +++ b/core/java/android/print/IPrinterDiscoveryObserver.aidl @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2013 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; + +import android.print.PrinterId; +import android.print.PrinterInfo; + +/** + * Interface for observing printer discovery. + * + * @hide + */ +oneway interface IPrinterDiscoveryObserver { + void addDiscoveredPrinters(in List<PrinterInfo> printers); + void removeDiscoveredPrinters(in List<PrinterId> printers); +} diff --git a/core/java/android/print/PageRange.aidl b/core/java/android/print/PageRange.aidl new file mode 100644 index 0000000..b1ae14f --- /dev/null +++ b/core/java/android/print/PageRange.aidl @@ -0,0 +1,19 @@ +/** + * Copyright (c) 2013, 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; + +parcelable PageRange; diff --git a/core/java/android/print/PageRange.java b/core/java/android/print/PageRange.java new file mode 100644 index 0000000..044a715 --- /dev/null +++ b/core/java/android/print/PageRange.java @@ -0,0 +1,119 @@ +/* + * Copyright (C) 2013 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; + +import android.os.Parcel; +import android.os.Parcelable; + +/** + * Represents a range of pages. The start and end page indices of + * the range are zero based and are inclusive. + */ +public final class PageRange implements Parcelable { + + /** + * Constant for specifying all pages. + */ + public static final PageRange ALL_PAGES = new PageRange(0, Integer.MAX_VALUE); + + private final int mStart; + private final int mEnd; + + /** + * Creates a new instance. + * + * @param start The start page index (zero based and inclusive). + * @param end The end page index (zero based and inclusive). + * + * @throws IllegalArgumentException If start is less than zero. + * @throws IllegalArgumentException If end is less than zero. + * @throws IllegalArgumentException If start greater than end. + */ + PageRange(int start, int end) { + if (start < 0) { + throw new IllegalArgumentException("start cannot be less than zero."); + } + if (end < 0) { + throw new IllegalArgumentException("end cannot be less than zero."); + } + if (start > end) { + throw new IllegalArgumentException("start must be lesser than end."); + } + mStart = start; + mEnd = end; + } + + private PageRange (Parcel parcel) { + this(parcel.readInt(), parcel.readInt()); + } + + /** + * Gets the start page index (zero based and inclusive). + * + * @return The start page index. + */ + public int getStart() { + return mStart; + } + + /** + * Gets the end page index (zero based and inclusive). + * + * @return The end page index. + */ + public int getEnd() { + return mEnd; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel parcel, int flags) { + parcel.writeInt(mStart); + parcel.writeInt(mEnd); + } + + @Override + public String toString() { + if (this == ALL_PAGES) { + return "PageRange[<all pages>]"; + } + StringBuilder builder = new StringBuilder(); + builder.append("PageRange[") + .append(mStart) + .append(" - ") + .append(mEnd) + .append("]"); + return builder.toString(); + } + + public static final Parcelable.Creator<PageRange> CREATOR = + new Creator<PageRange>() { + @Override + public PageRange createFromParcel(Parcel parcel) { + return new PageRange(parcel); + } + + @Override + public PageRange[] newArray(int size) { + return new PageRange[size]; + } + }; +} diff --git a/core/java/android/print/PrintAdapter.java b/core/java/android/print/PrintAdapter.java new file mode 100644 index 0000000..a7f809b --- /dev/null +++ b/core/java/android/print/PrintAdapter.java @@ -0,0 +1,162 @@ +/* + * Copyright (C) 2013 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; + +import java.io.FileDescriptor; +import java.io.IOException; +import java.util.List; + +import android.os.CancellationSignal; + +/** + * Base class that provides data to be printed. + * + * <h3>Lifecycle</h3> + * <p> + * <ul> + * <li> + * You will receive a call on {@link #onStart()} when printing starts. + * This callback can be used to allocate resources. + * </li> + * <li> + * Next you will get one or more calls to the pair + * {@link #onPrintAttributesChanged(PrintAttributes)} and {@link #onPrint(List, + * FileDescriptor, CancellationSignal, PrintProgressCallback)}. The first callback + * informs you that the print attributes (page size, density, etc) changed giving + * you an opportunity to re-layout the content. The second method asks you to write + * a PDF file with the content for specific pages. + * </li> + * <li> + * Finally, you will receive a call on {@link #onFinish()} right after printing. + * You can use this callback to release resources. + * </li> + * <li> + * You can receive calls to {@link #getInfo()} at any point which should return + * a {@link PrintAdapterInfo} describing your {@link PrintAdapter}. + * </li> + * </ul> + * </p> + * <p> + */ +public abstract class PrintAdapter { + + /** + * Called when printing started. You can use this callback to + * allocate resources. + * <p> + * <strong>Note:</strong> Invoked on the main thread. + * </p> + */ + public void onStart() { + /* do nothing - stub */ + } + + /** + * Called when the print job attributes (page size, density, etc) + * changed giving you a chance to re-layout the content such that + * it matches the new constraints. + * <p> + * <strong>Note:</strong> Invoked on the main thread. + * </p> + * + * @param attributes The print job attributes. + * @return Whether the content changed based on the provided attributes. + */ + public boolean onPrintAttributesChanged(PrintAttributes attributes) { + return false; + } + + /** + * Called when specific pages of the content have to be printed in the from of + * a PDF file to the given file descriptor. You should <strong>not</strong> + * close the file descriptor instead you have to invoke {@link PrintProgressCallback + * #onWriteFinished()} or {@link PrintProgressCallback#onPrintFailed(CharSequence)}. + * <p> + * <strong>Note:</strong> If the printed content is large, it is a good + * practice to schedule writing it on a dedicated thread and register a + * callback in the provided {@link CancellationSignal} upon which to stop + * writing data. The cancellation callback will not be made on the main + * thread. + * </p> + * <p> + * <strong>Note:</strong> Invoked on the main thread. + * </p> + * <p> + * + * @param pages The pages whose content to write. + * @param destination The destination file descriptor to which to start writing. + * @param cancellationSignal Signal for observing cancel write requests. + * @param progressListener Callback to inform the system with the write progress. + * + * @see CancellationSignal + */ + public abstract void onPrint(List<PageRange> pages, FileDescriptor destination, + CancellationSignal cancellationSignal, PrintProgressCallback progressListener); + + /** + * Called when printing finished. You can use this callback to release + * resources. + * <p> + * <strong>Note:</strong> Invoked on the main thread. + * </p> + */ + public void onFinish() { + /* do nothing - stub */ + } + + /** + * Gets a {@link PrinterInfo} object that contains metadata about the + * printed content. + * <p> + * <strong>Note:</strong> Invoked on the main thread. + * </p> + * + * @return The info object for this {@link PrintAdapter}. + * + * @see PrintAdapterInfo + */ + public abstract PrintAdapterInfo getInfo(); + + /** + * Base class for implementing a listener for the printing progress + * of a {@link PrintAdapter}. + */ + public static abstract class PrintProgressCallback { + + PrintProgressCallback() { + /* do nothing - hide constructor */ + } + + /** + * Notifies that all the data was printed. + * + * @param pages The pages that were printed. + */ + public void onPrintFinished(List<PageRange> pages) { + /* do nothing - stub */ + } + + /** + * Notifies that an error occurred while printing the data. + * + * @param error Error message. May be null if error is unknown. + */ + public void onPrintFailed(CharSequence error) { + /* do nothing - stub */ + } + } +} diff --git a/core/java/android/print/PrintAdapterInfo.aidl b/core/java/android/print/PrintAdapterInfo.aidl new file mode 100644 index 0000000..27bf717 --- /dev/null +++ b/core/java/android/print/PrintAdapterInfo.aidl @@ -0,0 +1,19 @@ +/** + * Copyright (c) 2013, 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; + +parcelable PrintAdapterInfo; diff --git a/core/java/android/print/PrintAdapterInfo.java b/core/java/android/print/PrintAdapterInfo.java new file mode 100644 index 0000000..06e6b10 --- /dev/null +++ b/core/java/android/print/PrintAdapterInfo.java @@ -0,0 +1,136 @@ +/* + * Copyright (C) 2013 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; + +import android.os.Parcel; +import android.os.Parcelable; + +/** + * This class encapsulates information about a {@link PrintAdapter} object. + */ +public final class PrintAdapterInfo implements Parcelable { + + /** + * Constant for unknown page count. + */ + public static final int PAGE_COUNT_UNKNOWN = -1; + + private int mPageCount; + private int mFlags; + + /** + * Creates a new instance. + */ + private PrintAdapterInfo() { + /* do nothing */ + } + + /** + * Creates a new instance. + * + * @param parcel Data from which to initialize. + */ + private PrintAdapterInfo(Parcel parcel) { + mPageCount = parcel.readInt(); + mFlags = parcel.readInt(); + } + + /** + * Gets the total number of pages. + * + * @return The number of pages. + */ + public int getPageCount() { + return mPageCount; + } + + /** + * @return The flags of this printable info. + * + * @see #FLAG_NOTIFY_FOR_ATTRIBUTES_CHANGE + */ + public int getFlags() { + return mFlags; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel parcel, int flags) { + parcel.writeInt(mPageCount); + parcel.writeInt(mFlags); + } + + /** + * Builder for creating an {@link PrintAdapterInfo}. + */ + public static final class Builder { + private final PrintAdapterInfo mPrintableInfo = new PrintAdapterInfo(); + + /** + * Sets the total number of pages. + * + * @param pageCount The number of pages. Must be + * greater than zero. + */ + public Builder setPageCount(int pageCount) { + if (pageCount < 0) { + throw new IllegalArgumentException("pageCount" + + " must be greater than or euqal to zero!"); + } + mPrintableInfo.mPageCount = pageCount; + return this; + } + + /** + * Sets the flags of this printable info. + * + * @param flags The flags. + * + * @see #FLAG_NOTIFY_FOR_ATTRIBUTES_CHANGE + */ + public Builder setFlags(int flags) { + mPrintableInfo.mFlags = flags; + return this; + } + + /** + * Creates a new {@link PrintAdapterInfo} instance. + * + * @return The new instance. + */ + public PrintAdapterInfo create() { + return mPrintableInfo; + } + } + + public static final Parcelable.Creator<PrintAdapterInfo> CREATOR = + new Creator<PrintAdapterInfo>() { + @Override + public PrintAdapterInfo createFromParcel(Parcel parcel) { + return new PrintAdapterInfo(parcel); + } + + @Override + public PrintAdapterInfo[] newArray(int size) { + return new PrintAdapterInfo[size]; + } + }; +} diff --git a/core/java/android/print/PrintAttributes.aidl b/core/java/android/print/PrintAttributes.aidl new file mode 100644 index 0000000..0b765a2 --- /dev/null +++ b/core/java/android/print/PrintAttributes.aidl @@ -0,0 +1,19 @@ +/** + * Copyright (c) 2013, 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; + +parcelable PrintAttributes; diff --git a/core/java/android/print/PrintAttributes.java b/core/java/android/print/PrintAttributes.java new file mode 100644 index 0000000..8511d0b --- /dev/null +++ b/core/java/android/print/PrintAttributes.java @@ -0,0 +1,1226 @@ +/* + * Copyright (C) 2013 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; + +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.content.res.Resources.NotFoundException; +import android.os.Parcel; +import android.os.Parcelable; +import android.text.TextUtils; +import android.util.Log; + +import com.android.internal.R; + +/** + * This class represents the attributes of a print job. + */ +public final class PrintAttributes implements Parcelable { + /** Duplex mode: No duplexing */ + public static final int DUPLEX_MODE_NONE = 1 << 0; + /** Duplex mode: Turn a page along its long edge, e.g. like a book */ + public static final int DUPLEX_MODE_LONG_EDGE = 1 << 1; + /** Duplex mode: Turn a page along its short edge, e.g. like a notepad */ + public static final int DUPLEX_MODE_SHORT_EDGE = 1 << 2; + + + /** Orientation: Portrait page orientation. */ + public static final int ORIENTATION_PORTRAIT = 1 << 0; + /** Orientation: Landscape page orientation. */ + public static final int ORIENTATION_LANDSCAPE = 1 << 1; + + + /** Color mode: Monochrome color scheme, e.g. one color is used. */ + public static final int COLOR_MODE_MONOCHROME = 1 << 0; + /** Color mode: Color color scheme, e.g. many colors are used. */ + public static final int COLOR_MODE_COLOR = 1 << 1; + + + /** Fitting mode: No fitting. */ + public static final int FITTING_MODE_NONE = 0x00000001; + /** Fitting mode: Fit the content to the page. */ + public static final int FITTING_MODE_FIT_TO_PAGE = 0x00000002; + + + private static final int VALID_DUPLEX_MODES = + DUPLEX_MODE_NONE | DUPLEX_MODE_LONG_EDGE | DUPLEX_MODE_SHORT_EDGE; + + private static final int VALID_COLOR_MODES = + COLOR_MODE_MONOCHROME | COLOR_MODE_COLOR; + + private static final int VALID_FITTING_MODES = + FITTING_MODE_NONE | FITTING_MODE_FIT_TO_PAGE; + + private static final int VALID_ORIENTATIONS = + ORIENTATION_PORTRAIT | ORIENTATION_LANDSCAPE; + + private MediaSize mMediaSize; + private Resolution mResolution; + private Margins mMargins; + private Tray mInputTray; + private Tray mOutputTray; + + private int mDuplexMode; + private int mColorMode; + private int mFittingMode; + private int mOrientation; + private int mCopies; + + PrintAttributes() { + /* hide constructor */ + } + + private PrintAttributes(Parcel parcel) { + mMediaSize = (parcel.readInt() == 1) ? MediaSize.createFromParcel(parcel) : null; + mResolution = (parcel.readInt() == 1) ? Resolution.createFromParcel(parcel) : null; + mMargins = (parcel.readInt() == 1) ? Margins.createFromParcel(parcel) : null; + mInputTray = (parcel.readInt() == 1) ? Tray.createFromParcel(parcel) : null; + mOutputTray = (parcel.readInt() == 1) ? Tray.createFromParcel(parcel) : null; + mDuplexMode = parcel.readInt(); + mColorMode = parcel.readInt(); + mFittingMode = parcel.readInt(); + mOrientation = parcel.readInt(); + mCopies = parcel.readInt(); + } + + /** + * Gets the media size. + * + * @return The media size or <code>null</code> if not set. + */ + public MediaSize getMediaSize() { + return mMediaSize; + } + + /** + * Sets the media size. + * + * @param The media size. + * + * @hide + */ + public void setMediaSize(MediaSize mediaSize) { + mMediaSize = mediaSize; + } + + /** + * Gets the resolution. + * + * @return The resolution or <code>null</code> if not set. + */ + public Resolution getResolution() { + return mResolution; + } + + /** + * Sets the resolution. + * + * @param The resolution. + * + * @hide + */ + public void setResolution(Resolution resolution) { + mResolution = resolution; + } + + /** + * Gets the margins. + * + * @return The margins or <code>null</code> if not set. + */ + public Margins getMargins() { + return mMargins; + } + + /** + * Sets the margins. + * + * @param The margins. + * + * @hide + */ + public void setMargins(Margins margins) { + mMargins = margins; + } + + /** + * Sets the input tray. + * + * @return The input tray or <code>null</code> if not set. + */ + public Tray getInputTray() { + return mInputTray; + } + + /** + * Gets the input tray. + * + * @param The input tray. + * + * @hide + */ + public void setInputTray(Tray inputTray) { + mInputTray = inputTray; + } + + /** + * Gets the output tray. + * + * @return The output tray or <code>null</code> if not set. + */ + public Tray getOutputTray() { + return mOutputTray; + } + + /** + * Sets the output tray. + * + * @param The output tray. + * + * @hide + */ + public void setOutputTray(Tray outputTray) { + mOutputTray = outputTray; + } + + /** + * Gets the duplex mode. + * + * @return The duplex mode or zero if not set. + * + * @see #DUPLEX_MODE_NONE + * @see #DUPLEX_MODE_SHORT_EDGE + * @see #DUPLEX_MODE_LONG_EDGE + */ + public int getDuplexMode() { + return mDuplexMode; + } + + /** + * Sets the duplex mode. + * + * @param The duplex mode. + * + * @hide + */ + public void setDuplexMode(int duplexMode) { + enforceValidDuplexMode(duplexMode); + mDuplexMode = duplexMode; + } + + /** + * Gets the color mode. + * + * @return The color mode or zero if not set. + * + * @see #COLOR_MODE_COLOR + * @see #COLOR_MODE_MONOCHROME + */ + public int getColorMode() { + return mColorMode; + } + + /** + * Sets the color mode. + * + * @param The color mode. + * + * @see #COLOR_MODE_MONOCHROME + * @see #COLOR_MODE_COLOR + * + * @hide + */ + public void setColorMode(int colorMode) { + enforceValidColorMode(colorMode); + mColorMode = colorMode; + } + + /** + * Gets the fitting mode. + * + * @return The fitting mode or zero if not set. + * + * @see #FITTING_MODE_NONE + * @see #FITTING_MODE_FIT_TO_PAGE + */ + public int getFittingMode() { + return mFittingMode; + } + + /** + * Sets the fitting mode. + * + * @param The fitting mode. + * + * @see #FITTING_MODE_NONE + * @see #FITTING_MODE_FIT_TO_PAGE + * + * @hide + */ + public void setFittingMode(int fittingMode) { + enfoceValidFittingMode(fittingMode); + mFittingMode = fittingMode; + } + + /** + * Gets the orientation. + * + * @return The orientation or zero if not set. + * + * @see #ORIENTATION_PORTRAIT + * @see #ORIENTATION_LANDSCAPE + */ + public int getOrientation() { + return mOrientation; + } + + /** + * Sets the orientation. + * + * @param The orientation. + * + * @see #ORIENTATION_PORTRAIT + * @see #ORIENTATION_LANDSCAPE + * + * @hide + */ + public void setOrientation(int orientation) { + enforceValidOrientation(orientation); + mOrientation = orientation; + } + + /** + * Gets the number of copies. + * + * @return The number of copies or zero if not set. + */ + public int getCopies() { + return mCopies; + } + + /** + * Sets the number of copies. + * + * @param copyCount The number of copies. + * + * @hide + */ + public void setCopies(int copyCount) { + if (copyCount < 1) { + throw new IllegalArgumentException("Copies must be more than one."); + } + mCopies = copyCount; + } + + @Override + public void writeToParcel(Parcel parcel, int flags) { + if (mMediaSize != null) { + parcel.writeInt(1); + mMediaSize.writeToParcel(parcel); + } else { + parcel.writeInt(0); + } + if (mResolution != null) { + parcel.writeInt(1); + mResolution.writeToParcel(parcel); + } else { + parcel.writeInt(0); + } + if (mMargins != null) { + parcel.writeInt(1); + mMargins.writeToParcel(parcel); + } else { + parcel.writeInt(0); + } + if (mInputTray != null) { + parcel.writeInt(1); + mInputTray.writeToParcel(parcel); + } else { + parcel.writeInt(0); + } + if (mOutputTray != null) { + parcel.writeInt(1); + mOutputTray.writeToParcel(parcel); + } else { + parcel.writeInt(0); + } + parcel.writeInt(mDuplexMode); + parcel.writeInt(mColorMode); + parcel.writeInt(mFittingMode); + parcel.writeInt(mOrientation); + parcel.writeInt(mCopies); + } + + @Override + public int describeContents() { + return 0; + } + + /** hide */ + public void clear() { + mMediaSize = null; + mResolution = null; + mMargins = null; + mInputTray = null; + mOutputTray = null; + mDuplexMode = 0; + mColorMode = 0; + mFittingMode = 0; + mOrientation = 0; + mCopies = 0; + } + + /** + * This class specifies a supported media size. + */ + public static final class MediaSize { + private static final String LOG_TAG = "MediaSize"; + + // TODO: Verify media sizes and add more standard ones. + + // ISO sizes + + /** ISO A0 media size: 841mm x 1189mm (33.11" x 46.81") */ + public static final MediaSize ISO_A0 = + new MediaSize("ISO_A0", "android", R.string.mediaSize_iso_a0, 33110, 46810); + /** ISO A1 media size: 594mm x 841mm (23.39" x 33.11") */ + public static final MediaSize ISO_A1 = + new MediaSize("ISO_A1", "android", R.string.mediaSize_iso_a1, 23390, 33110); + /** ISO A2 media size: 420mm x 594mm (16.54" x 23.39") */ + public static final MediaSize ISO_A2 = + new MediaSize("ISO_A2", "android", R.string.mediaSize_iso_a2, 16540, 23390); + /** ISO A3 media size: 297mm x 420mm (11.69" x 16.54") */ + public static final MediaSize ISO_A3 = + new MediaSize("ISO_A3", "android", R.string.mediaSize_iso_a3, 11690, 16540); + /** ISO A4 media size: 210mm x 297mm (8.27" x 11.69") */ + public static final MediaSize ISO_A4 = + new MediaSize("ISO_A4", "android", R.string.mediaSize_iso_a4, 8270, 11690); + /** ISO A5 media size: 148mm x 210mm (5.83" x 8.27") */ + public static final MediaSize ISO_A5 = + new MediaSize("ISO_A5", "android", R.string.mediaSize_iso_a5, 5830, 8270); + /** ISO A6 media size: 105mm x 148mm (4.13" x 5.83") */ + public static final MediaSize ISO_A6 = + new MediaSize("ISO_A6", "android", R.string.mediaSize_iso_a6, 4130, 5830); + /** ISO A7 media size: 74mm x 105mm (2.91" x 4.13") */ + public static final MediaSize ISO_A7 = + new MediaSize("ISO_A7", "android", R.string.mediaSize_iso_a7, 2910, 4130); + /** ISO A8 media size: 52mm x 74mm (2.05" x 2.91") */ + public static final MediaSize ISO_A8 = + new MediaSize("ISO_A8", "android", R.string.mediaSize_iso_a8, 2050, 2910); + /** ISO A9 media size: 37mm x 52mm (1.46" x 2.05") */ + public static final MediaSize ISO_A9 = + new MediaSize("ISO_A9", "android", R.string.mediaSize_iso_a9, 1460, 2050); + /** ISO A10 media size: 26mm x 37mm (1.02" x 1.46") */ + public static final MediaSize ISO_A10 = + new MediaSize("ISO_A10", "android", R.string.mediaSize_iso_a10, 1020, 1460); + + /** ISO B0 media size: 1000mm x 1414mm (39.37" x 55.67") */ + public static final MediaSize ISO_B0 = + new MediaSize("ISO_B0", "android", R.string.mediaSize_iso_b0, 39370, 55670); + /** ISO B1 media size: 707mm x 1000mm (27.83" x 39.37") */ + public static final MediaSize ISO_B1 = + new MediaSize("ISO_B1", "android", R.string.mediaSize_iso_b1, 27830, 39370); + /** ISO B2 media size: 500mm x 707mm (19.69" x 27.83") */ + public static final MediaSize ISO_B2 = + new MediaSize("ISO_B2", "android", R.string.mediaSize_iso_b2, 19690, 27830); + /** ISO B3 media size: 353mm x 500mm (13.90" x 19.69") */ + public static final MediaSize ISO_B3 = + new MediaSize("ISO_B3", "android", R.string.mediaSize_iso_b3, 13900, 19690); + /** ISO B4 media size: 250mm x 353mm (9.84" x 13.90") */ + public static final MediaSize ISO_B4 = + new MediaSize("ISO_B4", "android", R.string.mediaSize_iso_b4, 9840, 13900); + /** ISO B5 media size: 176mm x 250mm (6.93" x 9.84") */ + public static final MediaSize ISO_B5 = + new MediaSize("ISO_B5", "android", R.string.mediaSize_iso_b5, 6930, 9840); + /** ISO B6 media size: 125mm x 176mm (4.92" x 6.93") */ + public static final MediaSize ISO_B6 = + new MediaSize("ISO_B6", "android", R.string.mediaSize_iso_b6, 4920, 6930); + /** ISO B7 media size: 88mm x 125mm (3.46" x 4.92") */ + public static final MediaSize ISO_B7 = + new MediaSize("ISO_B7", "android", R.string.mediaSize_iso_b7, 3460, 4920); + /** ISO B8 media size: 62mm x 88mm (2.44" x 3.46") */ + public static final MediaSize ISO_B8 = + new MediaSize("ISO_B8", "android", R.string.mediaSize_iso_b8, 2440, 3460); + /** ISO B9 media size: 44mm x 62mm (1.73" x 2.44") */ + public static final MediaSize ISO_B9 = + new MediaSize("ISO_B9", "android", R.string.mediaSize_iso_b9, 1730, 2440); + /** ISO B10 media size: 31mm x 44mm (1.22" x 1.73") */ + public static final MediaSize ISO_B10 = + new MediaSize("ISO_B10", "android", R.string.mediaSize_iso_b10, 1220, 1730); + + /** ISO C0 media size: 917mm x 1297mm (36.10" x 51.06") */ + public static final MediaSize ISO_C0 = + new MediaSize("ISO_C0", "android", R.string.mediaSize_iso_c0, 36100, 51060); + /** ISO C1 media size: 648mm x 917mm (25.51" x 36.10") */ + public static final MediaSize ISO_C1 = + new MediaSize("ISO_C1", "android", R.string.mediaSize_iso_c1, 25510, 36100); + /** ISO C2 media size: 458mm x 648mm (18.03" x 25.51") */ + public static final MediaSize ISO_C2 = + new MediaSize("ISO_C2", "android", R.string.mediaSize_iso_c2, 18030, 25510); + /** ISO C3 media size: 324mm x 458mm (12.76" x 18.03") */ + public static final MediaSize ISO_C3 = + new MediaSize("ISO_C3", "android", R.string.mediaSize_iso_c3, 12760, 18030); + /** ISO C4 media size: 229mm x 324mm (9.02" x 12.76") */ + public static final MediaSize ISO_C4 = + new MediaSize("ISO_C4", "android", R.string.mediaSize_iso_c4, 9020, 12760); + /** ISO C5 media size: 162mm x 229mm (6.38" x 9.02") */ + public static final MediaSize ISO_C5 = + new MediaSize("ISO_C5", "android", R.string.mediaSize_iso_c5, 6380, 9020); + /** ISO C6 media size: 114mm x 162mm (4.49" x 6.38") */ + public static final MediaSize ISO_C6 = + new MediaSize("ISO_C6", "android", R.string.mediaSize_iso_c6, 4490, 6380); + /** ISO C7 media size: 81mm x 114mm (3.19" x 4.49") */ + public static final MediaSize ISO_C7 = + new MediaSize("ISO_C7", "android", R.string.mediaSize_iso_c7, 3190, 4490); + /** ISO C8 media size: 57mm x 81mm (2.24" x 3.19") */ + public static final MediaSize ISO_C8 = + new MediaSize("ISO_C8", "android", R.string.mediaSize_iso_c8, 2240, 3190); + /** ISO C9 media size: 40mm x 57mm (1.57" x 2.24") */ + public static final MediaSize ISO_C9 = + new MediaSize("ISO_C9", "android", R.string.mediaSize_iso_c9, 1570, 2240); + /** ISO C10 media size: 28mm x 40mm (1.10" x 1.57") */ + public static final MediaSize ISO_C10 = + new MediaSize("ISO_C10", "android", R.string.mediaSize_iso_c10, 1100, 1570); + + // North America + + /** North America Letter media size: 8.5" x 11" */ + public static final MediaSize NA_LETTER = + new MediaSize("NA_LETTER", "android", R.string.mediaSize_na_letter, 8500, 11000); + /** North America Government-Letter media size: 8.0" x 10.5" */ + public static final MediaSize NA_GOVT_LETTER = + new MediaSize("NA_GOVT_LETTER", "android", + R.string.mediaSize_na_gvrnmt_letter, 8000, 10500); + /** North America Legal media size: 8.5" x 14" */ + public static final MediaSize NA_LEGAL = + new MediaSize("NA_LEGAL", "android", R.string.mediaSize_na_legal, 8500, 14000); + /** North America Junior Legal media size: 8.0" x 5.0" */ + public static final MediaSize NA_JUNIOR_LEGAL = + new MediaSize("NA_JUNIOR_LEGAL", "android", + R.string.mediaSize_na_junior_legal, 8000, 5000); + /** North America Ledger media size: 17" x 11" */ + public static final MediaSize NA_LEDGER = + new MediaSize("NA_LEDGER", "android", R.string.mediaSize_na_ledger, 17000, 11000); + /** North America Tabloid media size: 11" x 17" */ + public static final MediaSize NA_TBLOID = + new MediaSize("NA_TABLOID", "android", + R.string.mediaSize_na_tabloid, 11000, 17000); + + private final String mId; + private final String mPackageName; + private final int mLabelResId; + private final int mWidthMils; + private final int mHeightMils; + + /** + * Gets the unique media size id. + * + * @return The unique media size id. + */ + public String getId() { + return mId; + } + + /** + * Gets the human readable media size label. + * + * @return The human readable label. + */ + public CharSequence getLabel(PackageManager packageManager) { + try { + return packageManager.getResourcesForApplication( + mPackageName).getString(mLabelResId); + } catch (NotFoundException nfe) { + Log.w(LOG_TAG, "Could not load resouce" + mLabelResId + + " from package " + mPackageName); + } catch (NameNotFoundException nnfee) { + Log.w(LOG_TAG, "Could not load resouce" + mLabelResId + + " from package " + mPackageName); + } + return null; + } + + /** + * Gets the media width in mils (thousands of an inch). + * + * @return The media width. + */ + public int getWidthMils() { + return mWidthMils; + } + + /** + * Gets the media height in mils (thousands of an inch). + * + * @return The media height. + */ + public int getHeightMils() { + return mHeightMils; + } + + /** + * Creates a new instance. + * + * @param id The unique media size id. + * @param packageName The name of the creating package. + * @param labelResId The resource if of a human readable label. + * @param widthMils The width in mils (thousands of an inch). + * @param heightMils The height in mils (thousands of an inch). + * + * @throws IllegalArgumentException If the id is empty. + * @throws IllegalArgumentException If the label is empty. + * @throws IllegalArgumentException If the widthMils is less than or equal to zero. + * @throws IllegalArgumentException If the heightMils is less than or equal to zero. + */ + public MediaSize(String id, String packageName, int labelResId, + int widthMils, int heightMils) { + if (TextUtils.isEmpty(id)) { + throw new IllegalArgumentException("id cannot be empty."); + } + if (TextUtils.isEmpty(packageName)) { + throw new IllegalArgumentException("packageName cannot be empty."); + } + if (labelResId <= 0) { + throw new IllegalArgumentException("labelResId must be greater than zero."); + } + if (widthMils <= 0) { + throw new IllegalArgumentException("widthMils " + + "cannot be less than or equal to zero."); + } + if (heightMils <= 0) { + throw new IllegalArgumentException("heightMils " + + "cannot be less than or euqual to zero."); + } + mPackageName = packageName; + mId = id; + mLabelResId = labelResId; + mWidthMils = widthMils; + mHeightMils = heightMils; + } + + void writeToParcel(Parcel parcel) { + parcel.writeString(mId); + parcel.writeString(mPackageName); + parcel.writeInt(mLabelResId); + parcel.writeInt(mWidthMils); + parcel.writeInt(mHeightMils); + } + + static MediaSize createFromParcel(Parcel parcel) { + return new MediaSize( + parcel.readString(), + parcel.readString(), + parcel.readInt(), + parcel.readInt(), + parcel.readInt()); + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("MediaSize{"); + builder.append("id: ").append(mId); + builder.append(", packageName: ").append(mPackageName); + builder.append(", labelResId: ").append(mLabelResId); + builder.append(", heightMils: ").append(mHeightMils); + builder.append(", widthMils: ").append(mWidthMils); + builder.append("}"); + return builder.toString(); + } + } + + /** + * This class specifies a supported resolution in dpi (dots per inch). + */ + public static final class Resolution { + private static final String LOG_TAG = "Resolution"; + + private final String mId; + private final String mPackageName; + private final int mLabelResId; + private final int mHorizontalDpi; + private final int mVerticalDpi; + + /** + * Gets the unique resolution id. + * + * @return The unique resolution id. + */ + public String getId() { + return mId; + } + + /** + * Gets the resolution human readable label. + * + * @return The human readable label. + */ + public CharSequence getLabel(PackageManager packageManager) { + try { + return packageManager.getResourcesForApplication( + mPackageName).getString(mLabelResId); + } catch (NotFoundException nfe) { + Log.w(LOG_TAG, "Could not load resouce" + mLabelResId + + " from package " + mPackageName); + } catch (NameNotFoundException nnfee) { + Log.w(LOG_TAG, "Could not load resouce" + mLabelResId + + " from package " + mPackageName); + } + return null; + } + + /** + * Gets the horizontal resolution in dpi. + * + * @return The horizontal resolution. + */ + public int getHorizontalDpi() { + return mHorizontalDpi; + } + + /** + * Gets the vertical resolution in dpi. + * + * @return The vertical resolution. + */ + public int getVerticalDpi() { + return mVerticalDpi; + } + + /** + * Creates a new instance. + * + * @param id The unique resolution id. + * @param packageName The name of the creating package. + * @param labelResId The resource id of a human readable label. + * @param horizontalDpi The horizontal resolution in dpi. + * @param verticalDpi The vertical resolution in dpi. + * + * @throws IllegalArgumentException If the id is empty. + * @throws IllegalArgumentException If the label is empty. + * @throws IllegalArgumentException If the horizontalDpi is less than or equal to zero. + * @throws IllegalArgumentException If the verticalDpi is less than or equal to zero. + */ + public Resolution(String id, String packageName, int labelResId, + int horizontalDpi, int verticalDpi) { + if (TextUtils.isEmpty(id)) { + throw new IllegalArgumentException("id cannot be empty."); + } + if (TextUtils.isEmpty(packageName)) { + throw new IllegalArgumentException("packageName cannot be empty."); + } + if (labelResId <= 0) { + throw new IllegalArgumentException("labelResId must be greater than zero."); + } + if (horizontalDpi <= 0) { + throw new IllegalArgumentException("horizontalDpi " + + "cannot be less than or equal to zero."); + } + if (verticalDpi <= 0) { + throw new IllegalArgumentException("verticalDpi" + + " cannot be less than or equal to zero."); + } + mId = id; + mPackageName = packageName; + mLabelResId = labelResId; + mHorizontalDpi = horizontalDpi; + mVerticalDpi = verticalDpi; + } + + void writeToParcel(Parcel parcel) { + parcel.writeString(mId); + parcel.writeString(mPackageName); + parcel.writeInt(mLabelResId); + parcel.writeInt(mHorizontalDpi); + parcel.writeInt(mVerticalDpi); + } + + static Resolution createFromParcel(Parcel parcel) { + return new Resolution( + parcel.readString(), + parcel.readString(), + parcel.readInt(), + parcel.readInt(), + parcel.readInt()); + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("Resolution{"); + builder.append("id: ").append(mId); + builder.append(", packageName: ").append(mPackageName); + builder.append(", labelResId: ").append(mLabelResId); + builder.append(", horizontalDpi: ").append(mHorizontalDpi); + builder.append(", verticalDpi: ").append(mVerticalDpi); + builder.append("}"); + return builder.toString(); + } + } + + /** + * This class specifies content margins. + */ + public static final class Margins { + private final int mLeftMils; + private final int mTopMils; + private final int mRightMils; + private final int mBottomMils; + + /** + * Gets the left margin in mils (thousands of an inch). + * + * @return The left margin. + */ + public int getLeftMils() { + return mLeftMils; + } + + /** + * Gets the top margin in mils (thousands of an inch). + * + * @return The top margin. + */ + public int getTopMils() { + return mTopMils; + } + + /** + * Gets the right margin in mils (thousands of an inch). + * + * @return The right margin. + */ + public int getRightMils() { + return mRightMils; + } + + /** + * Gets the bottom margin in mils (thousands of an inch). + * + * @return The bottom margin. + */ + public int getBottomMils() { + return mBottomMils; + } + + /** + * Creates a new instance. + * + * @param leftMils The left margin in mils (thousands of an inch). + * @param topMils The top margin in mils (thousands of an inch). + * @param rightMils The right margin in mils (thousands of an inch). + * @param bottomMils The bottom margin in mils (thousands of an inch). + * + * @throws IllegalArgumentException If the leftMils is less than zero. + * @throws IllegalArgumentException If the topMils is less than zero. + * @throws IllegalArgumentException If the rightMils is less than zero. + * @throws IllegalArgumentException If the bottomMils is less than zero. + */ + public Margins(int leftMils, int topMils, int rightMils, int bottomMils) { + if (leftMils < 0) { + throw new IllegalArgumentException("leftMils cannot be less than zero."); + } + if (topMils < 0) { + throw new IllegalArgumentException("topMils cannot be less than zero."); + } + if (rightMils < 0) { + throw new IllegalArgumentException("rightMils cannot be less than zero."); + } + if (bottomMils < 0) { + throw new IllegalArgumentException("bottomMils cannot be less than zero."); + } + mTopMils = topMils; + mLeftMils = leftMils; + mRightMils = rightMils; + mBottomMils = bottomMils; + } + + void writeToParcel(Parcel parcel) { + parcel.writeInt(mLeftMils); + parcel.writeInt(mTopMils); + parcel.writeInt(mRightMils); + parcel.writeInt(mBottomMils); + } + + static Margins createFromParcel(Parcel parcel) { + return new Margins( + parcel.readInt(), + parcel.readInt(), + parcel.readInt(), + parcel.readInt()); + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("Margins{"); + builder.append("leftMils: ").append(mLeftMils); + builder.append(", topMils: ").append(mTopMils); + builder.append(", rightMils: ").append(mRightMils); + builder.append(", bottomMils: ").append(mBottomMils); + builder.append("}"); + return builder.toString(); + } + } + + /** + * Represents a printer tray. + */ + public static final class Tray { + private static final String LOG_TAG = "Tray"; + + private final String mId; + private final String mPackageName; + private final int mLabelResId; + + /** + * Gets the unique tray id. + * + * @return The unique tray id. + */ + public String getId() { + return mId; + } + + /** + * Gets the tray human readable label. + * + * @return The human readable label. + */ + public CharSequence getLabel(PackageManager packageManager) { + try { + return packageManager.getResourcesForApplication( + mPackageName).getString(mLabelResId); + } catch (NotFoundException nfe) { + Log.w(LOG_TAG, "Could not load resouce" + mLabelResId + + " from package " + mPackageName); + } catch (NameNotFoundException nnfee) { + Log.w(LOG_TAG, "Could not load resouce" + mLabelResId + + " from package " + mPackageName); + } + return null; + } + + /** + * Creates a new instance. + * + * @param id The unique tray id. + * @param packageName The name of the creating package. + * @param labelResId The resource id of a human readable label. + * + * @throws IllegalArgumentException If the id is empty. + * @throws IllegalArgumentException If the label is empty. + */ + public Tray(String id, String packageName, int labelResId) { + if (TextUtils.isEmpty(id)) { + throw new IllegalArgumentException("id cannot be empty."); + } + if (TextUtils.isEmpty(packageName)) { + throw new IllegalArgumentException("packageName cannot be empty."); + } + if (labelResId <= 0) { + throw new IllegalArgumentException("label must be greater than zero."); + } + mId = id; + mPackageName = packageName; + mLabelResId = labelResId; + } + + void writeToParcel(Parcel parcel) { + parcel.writeString(mId); + parcel.writeString(mPackageName); + parcel.writeInt(mLabelResId); + } + + static Tray createFromParcel(Parcel parcel) { + return new Tray( + parcel.readString(), + parcel.readString(), + parcel.readInt()); + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("Tray{"); + builder.append("id: ").append(mId); + builder.append("id: ").append(mId); + builder.append(", packageName: ").append(mPackageName); + builder.append(", labelResId: ").append(mLabelResId); + builder.append("}"); + return builder.toString(); + } + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("PrintAttributes{"); + builder.append("mediaSize: ").append(mMediaSize); + builder.append(", resolution: ").append(mResolution); + builder.append(", margins: ").append(mMargins); + builder.append(", duplexMode: ").append(duplexModeToString(mDuplexMode)); + builder.append(", colorMode: ").append(colorModeToString(mColorMode)); + builder.append(", fittingMode: ").append(fittingModeToString(mFittingMode)); + builder.append(", orientation: ").append(orientationToString(mOrientation)); + builder.append(", copies: ").append(mCopies); + return builder.toString(); + } + + private static String duplexModeToString(int duplexMode) { + switch (duplexMode) { + case DUPLEX_MODE_NONE: { + return "DUPLEX_MODE_NONE"; + } + case DUPLEX_MODE_LONG_EDGE: { + return "DUPLEX_MODE_LONG_EDGE"; + } + case DUPLEX_MODE_SHORT_EDGE: { + return "DUPLEX_MODE_SHORT_EDGE"; + } + default: + return "DUPLEX_MODE_UNKNOWN"; + } + } + + private static String colorModeToString(int colorMode) { + switch (colorMode) { + case COLOR_MODE_MONOCHROME: { + return "COLOR_MODE_MONOCHROME"; + } + case COLOR_MODE_COLOR: { + return "COLOR_MODE_COLOR"; + } + default: + return "COLOR_MODE_UNKNOWN"; + } + } + + private static String orientationToString(int orientation) { + switch (orientation) { + case ORIENTATION_PORTRAIT: { + return "ORIENTATION_PORTRAIT"; + } + case ORIENTATION_LANDSCAPE: { + return "ORIENTATION_LANDSCAPE"; + } + default: + return "ORIENTATION_UNKNOWN"; + } + } + + private static String fittingModeToString(int fittingMode) { + switch (fittingMode) { + case FITTING_MODE_NONE: { + return "FITTING_MODE_NONE"; + } + case FITTING_MODE_FIT_TO_PAGE: { + return "FITTING_MODE_FIT_TO_PAGE"; + } + default: + return "FITTING_MODE_UNKNOWN"; + } + } + + static void enforceValidDuplexMode(int duplexMode) { + if ((duplexMode & VALID_DUPLEX_MODES) == 0) { + throw new IllegalArgumentException("invalid duplex mode: " + duplexMode); + } + } + + static void enforceValidColorMode(int colorMode) { + if ((colorMode & VALID_COLOR_MODES) == 0) { + throw new IllegalArgumentException("invalid color mode: " + colorMode); + } + } + + static void enfoceValidFittingMode(int fittingMode) { + if ((fittingMode & VALID_FITTING_MODES) == 0) { + throw new IllegalArgumentException("invalid fitting mode: " + fittingMode); + } + } + + static void enforceValidOrientation(int orientation) { + if ((orientation & VALID_ORIENTATIONS) == 0) { + throw new IllegalArgumentException("invalid orientation: " + orientation); + } + } + + /** + * Builder for creating {@link PrintAttributes}. + */ + public static final class Builder { + private final PrintAttributes mAttributes = new PrintAttributes(); + + /** + * Sets the media size. + * + * @param mediaSize The media size. + * @return This builder. + */ + public Builder setMediaSize(MediaSize mediaSize) { + mAttributes.setMediaSize(mediaSize); + return this; + } + + /** + * Sets the resolution. + * + * @param resolution The resolution. + * @return This builder. + */ + public Builder setResolution(Resolution resolution) { + mAttributes.setResolution(resolution); + return this; + } + + /** + * Sets the margins. + * + * @param margins The margins. + * @return This builder. + */ + public Builder setMargins(Margins margins) { + mAttributes.setMargins(margins); + return this; + } + + /** + * Sets the input tray. + * + * @param inputTray The tray. + * @return This builder. + */ + public Builder setInputTray(Tray inputTray) { + mAttributes.setInputTray(inputTray); + return this; + } + + /** + * Sets the output tray. + * + * @param outputTray The tray. + * @return This builder. + */ + public Builder setOutputTray(Tray outputTray) { + mAttributes.setOutputTray(outputTray); + return this; + } + + /** + * Sets the duplex mode. + * + * @param duplexMode A valid duplex mode or zero. + * @return This builder. + * + * @see PrintAttributes#DUPLEX_MODE_NONE + * @see PrintAttributes#DUPLEX_MODE_SHORT_EDGE + * @see PrintAttributes#DUPLEX_MODE_LONG_EDGE + */ + public Builder setDuplexMode(int duplexMode) { + if (Integer.bitCount(duplexMode) != 1) { + throw new IllegalArgumentException("can specify at most one duplexMode bit."); + } + mAttributes.setDuplexMode(duplexMode); + return this; + } + + /** + * Sets the color mode. + * + * @param colorMode A valid color mode or zero. + * @return This builder. + * + * @see PrintAttributes#COLOR_MODE_MONOCHROME + * @see PrintAttributes#COLOR_MODE_COLOR + */ + public Builder setColorMode(int colorMode) { + if (Integer.bitCount(colorMode) > 1) { + throw new IllegalArgumentException("can specify at most one colorMode bit."); + } + mAttributes.setColorMode(colorMode); + return this; + } + + /** + * Sets the fitting mode. + * + * @param fittingMode A valid fitting mode or zero. + * @return This builder. + * + * @see PrintAttributes#FITTING_MODE_NONE + * @see PrintAttributes#FITTING_MODE_FIT_TO_PAGE + */ + public Builder setFittingMode(int fittingMode) { + if (Integer.bitCount(fittingMode) > 1) { + throw new IllegalArgumentException("can specify at most one fittingMode bit."); + } + mAttributes.setFittingMode(fittingMode); + return this; + } + + /** + * Sets the orientation. + * + * @param orientation A valid orientation or zero. + * @return This builder. + * + * @see PrintAttributes#ORIENTATION_PORTRAIT + * @see PrintAttributes#ORIENTATION_LANDSCAPE + */ + public Builder setOrientation(int orientation) { + if (Integer.bitCount(orientation) > 1) { + throw new IllegalArgumentException("can specify at most one orientation bit."); + } + mAttributes.setOrientation(orientation); + return this; + } + + /** + * Sets the number of copies. + * + * @param copyCount A greater or equal to zero copy count. + * @return This builder. + */ + public Builder setCopyCount(int copyCount) { + mAttributes.setCopies(copyCount); + return this; + } + + /** + * Creates a new {@link PrintAttributes} instance. + * + * @return The new instance. + */ + public PrintAttributes create() { + return mAttributes; + } + } + + public static final Parcelable.Creator<PrintAttributes> CREATOR = + new Creator<PrintAttributes>() { + @Override + public PrintAttributes createFromParcel(Parcel parcel) { + return new PrintAttributes(parcel); + } + + @Override + public PrintAttributes[] newArray(int size) { + return new PrintAttributes[size]; + } + }; +} diff --git a/core/java/android/print/PrintFileAdapter.java b/core/java/android/print/PrintFileAdapter.java new file mode 100644 index 0000000..bbfc394 --- /dev/null +++ b/core/java/android/print/PrintFileAdapter.java @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2013 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; + +import android.os.AsyncTask; +import android.os.CancellationSignal; +import android.os.CancellationSignal.OnCancelListener; +import android.util.Log; + +import libcore.io.IoUtils; + +import java.io.File; +import java.io.FileDescriptor; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.List; + +/** + * Adapter for printing files. + */ +class PrintFileAdapter extends PrintAdapter { + + private static final String LOG_TAG = "PrintFileAdapter"; + + private final File mFile; + + private WriteFileAsyncTask mWriteFileAsyncTask; + + public PrintFileAdapter(File file) { + if (file == null) { + throw new IllegalArgumentException("File cannot be null!"); + } + mFile = file; + } + + @Override + public void onPrint(List<PageRange> pages, FileDescriptor destination, + CancellationSignal cancellationSignal, PrintProgressCallback progressListener) { + mWriteFileAsyncTask = new WriteFileAsyncTask(mFile, destination, cancellationSignal, + progressListener); + mWriteFileAsyncTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, + (Void[]) null); + + } + + @Override + public PrintAdapterInfo getInfo() { + // TODO: When we have PDF render library we should query the page count. + return new PrintAdapterInfo.Builder().create(); + } + + private static final class WriteFileAsyncTask extends AsyncTask<Void, Void, Void> { + + private final File mSource; + + private final FileDescriptor mDestination; + + private final PrintProgressCallback mProgressListener; + + private final CancellationSignal mCancellationSignal; + + public WriteFileAsyncTask(File source, FileDescriptor destination, + CancellationSignal cancellationSignal, PrintProgressCallback progressListener) { + mSource = source; + mDestination = destination; + mProgressListener = progressListener; + mCancellationSignal = cancellationSignal; + mCancellationSignal.setOnCancelListener(new OnCancelListener() { + @Override + public void onCancel() { + cancel(true); + } + }); + } + + @Override + protected Void doInBackground(Void... params) { + InputStream in = null; + OutputStream out = new FileOutputStream(mDestination); + final byte[] buffer = new byte[8192]; + try { + in = new FileInputStream(mSource); + while (true) { + final int readByteCount = in.read(buffer); + if (readByteCount < 0) { + break; + } + out.write(buffer, 0, readByteCount); + } + } catch (IOException ioe) { + Log.e(LOG_TAG, "Error writing data!", ioe); + } finally { + IoUtils.closeQuietly(in); + IoUtils.closeQuietly(out); + if (!isCancelled()) { + List<PageRange> pages = new ArrayList<PageRange>(); + pages.add(PageRange.ALL_PAGES); + mProgressListener.onPrintFinished(pages); + } else { + mProgressListener.onPrintFailed("Cancelled"); + } + } + return null; + } + } +} + diff --git a/core/java/android/print/PrintJob.java b/core/java/android/print/PrintJob.java new file mode 100644 index 0000000..f7cca87 --- /dev/null +++ b/core/java/android/print/PrintJob.java @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2013 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; + + +/** + * This class represents a print job from the perspective of + * an application. + */ +public final class PrintJob { + + private final int mId; + + private final PrintManager mPrintManager; + + private PrintJobInfo mCachedInfo; + + PrintJob(PrintJobInfo info, PrintManager printManager) { + mCachedInfo = info; + mPrintManager = printManager; + mId = info.getId(); + } + + /** + * Gets the unique print job id. + * + * @return The id. + */ + public int getId() { + return mId; + } + + /** + * Gets the {@link PrintJobInfo} that describes this job. + * <p> + * <strong>Node:</strong>The returned info object is a snapshot of the + * current print job state. Every call to this method returns a fresh + * info object that reflects the current print job state. + * </p> + * + * @return The print job info. + */ + public PrintJobInfo getInfo() { + PrintJobInfo info = mPrintManager.getPrintJob(mId); + if (info != null) { + mCachedInfo = info; + } + return mCachedInfo; + } + + /** + * Cancels this print job. + */ + public void cancel() { + mPrintManager.cancelPrintJob(mId); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + PrintJob other = (PrintJob) obj; + return mId == other.mId; + } + + @Override + public int hashCode() { + return mId; + } +} diff --git a/core/java/android/print/PrintJobInfo.aidl b/core/java/android/print/PrintJobInfo.aidl new file mode 100644 index 0000000..fbca99c --- /dev/null +++ b/core/java/android/print/PrintJobInfo.aidl @@ -0,0 +1,19 @@ +/** + * Copyright (c) 2013, 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; + +parcelable PrintJobInfo; diff --git a/core/java/android/print/PrintJobInfo.java b/core/java/android/print/PrintJobInfo.java new file mode 100644 index 0000000..1a5d671 --- /dev/null +++ b/core/java/android/print/PrintJobInfo.java @@ -0,0 +1,414 @@ +/* + * Copyright (C) 2013 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; + +import android.os.Parcel; +import android.os.Parcelable; + +/** + * This class represents the description of a print job. + */ +public final class PrintJobInfo implements Parcelable { + + /** Undefined print job id. */ + public static final int PRINT_JOB_ID_UNDEFINED = -1; + + /** + * Constant for matching any print job state. + * + * @hide + */ + public static final int STATE_ANY = -1; + + /** + * Print job state: The print job is being created but not yet + * ready to be printed. + * <p> + * Next valid states: {@link #STATE_QUEUED} + * </p> + */ + public static final int STATE_CREATED = 1; + + /** + * Print job status: The print jobs is created, it is ready + * to be printed and should be processed. + * <p> + * Next valid states: {@link #STATE_STARTED}, {@link #STATE_FAILED}, + * {@link #STATE_CANCELED} + * </p> + */ + public static final int STATE_QUEUED = 2; + + /** + * Print job status: The print job is being printed. + * <p> + * Next valid states: {@link #STATE_COMPLETED}, {@link #STATE_FAILED}, + * {@link #STATE_CANCELED} + * </p> + */ + public static final int STATE_STARTED = 3; + + /** + * Print job status: The print job was successfully printed. + * This is a terminal state. + * <p> + * Next valid states: None + * </p> + */ + public static final int STATE_COMPLETED = 4; + + /** + * Print job status: The print job was printing but printing failed. + * This is a terminal state. + * <p> + * Next valid states: None + * </p> + */ + public static final int STATE_FAILED = 5; + + /** + * Print job status: The print job was canceled. + * This is a terminal state. + * <p> + * Next valid states: None + * </p> + */ + public static final int STATE_CANCELED = 6; + + /** The unique print job id. */ + private int mId; + + /** The human readable print job label. */ + private CharSequence mLabel; + + /** The unique id of the printer. */ + private PrinterId mPrinterId; + + /** The status of the print job. */ + private int mState; + + /** The id of the app that created the job. */ + private int mAppId; + + /** The id of the user that created the job. */ + private int mUserId; + + /** Optional tag assigned by a print service.*/ + private String mTag; + + /** The pages to print */ + private PageRange[] mPageRanges; + + /** The print job attributes size. */ + private PrintAttributes mAttributes; + + /** + * Gets the unique print job id. + * + * @return The id. + */ + public int getId() { + return mId; + } + + /** + * Sets the unique print job id. + * + * @param The job id. + * + * @hide + */ + public void setId(int id) { + this.mId = id; + } + + /** + * Gets the human readable job label. + * + * @return The label. + */ + public CharSequence getLabel() { + return mLabel; + } + + /** + * Sets the human readable job label. + * + * @param label The label. + * + * @hide + */ + public void setLabel(CharSequence label) { + mLabel = label; + } + + /** + * Gets the unique target printer id. + * + * @return The target printer id. + */ + public PrinterId getPrinterId() { + return mPrinterId; + } + + /** + * Sets the unique target pritner id. + * + * @param printerId The target printer id. + * + * @hide + */ + public void setPrinterId(PrinterId printerId) { + mPrinterId = printerId; + } + + /** + * Gets the current job state. + * + * @return The job state. + */ + public int getState() { + return mState; + } + + /** + * Sets the current job state. + * + * @param state The job state. + * + * @hide + */ + public void setState(int state) { + mState = state; + } + + /** + * Sets the owning application id. + * + * @return The owning app id. + * + * @hide + */ + public int getAppId() { + return mAppId; + } + + /** + * Sets the owning application id. + * + * @param appId The owning app id. + * + * @hide + */ + public void setAppId(int appId) { + mAppId = appId; + } + + /** + * Gets the owning user id. + * + * @return The user id. + * + * @hide + */ + public int getUserId() { + return mUserId; + } + + /** + * Sets the owning user id. + * + * @param userId The user id. + * + * @hide + */ + public void setUserId(int userId) { + mUserId = userId; + } + + /** + * Gets the optional tag assigned by a print service. + * + * @return The tag. + */ + public String getTag() { + return mTag; + } + + /** + * Sets the optional tag assigned by a print service. + * + * @param tag The tag. + * + * @hide + */ + public void setTag(String tag) { + mTag = tag; + } + + /** + * Gets the included page ranges. + * + * @return The included page ranges or <code>null</code> if not set. + */ + public PageRange[] getPageRanges() { + return mPageRanges; + } + + /** + * Sets the included page ranges. + * + * @return The included page ranges. + * + * @hide + */ + public void setPageRanges(PageRange[] pageRanges) { + mPageRanges = pageRanges; + } + + /** + * Gets the print job attributes. + * + * @return The attributes. + */ + public PrintAttributes getAttributes() { + return mAttributes; + } + + /** + * Sets the print job attributes. + * + * @param attributes The attributes. + * + * @hide + */ + public void setAttributes(PrintAttributes attributes) { + mAttributes = attributes; + } + + /** @hide*/ + public PrintJobInfo() { + /* do nothing */ + } + + /** @hide */ + public PrintJobInfo(PrintJobInfo other) { + mId = other.mId; + mLabel = other.mLabel; + mPrinterId = other.mPrinterId; + mState = other.mState; + mAppId = other.mAppId; + mUserId = other.mUserId; + mAttributes = other.mAttributes; + } + + private PrintJobInfo(Parcel parcel) { + mId = parcel.readInt(); + mLabel = parcel.readCharSequence(); + mPrinterId = parcel.readParcelable(null); + mState = parcel.readInt(); + mAppId = parcel.readInt(); + mUserId = parcel.readInt(); + if (parcel.readInt() == 1) { + mPageRanges = (PageRange[]) parcel.readParcelableArray(null); + } + if (parcel.readInt() == 1) { + mAttributes = PrintAttributes.CREATOR.createFromParcel(parcel); + } + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel parcel, int flags) { + parcel.writeInt(mId); + parcel.writeCharSequence(mLabel); + parcel.writeParcelable(mPrinterId, flags); + parcel.writeInt(mState); + parcel.writeInt(mAppId); + parcel.writeInt(mUserId); + if (mPageRanges != null) { + parcel.writeInt(1); + parcel.writeParcelableArray(mPageRanges, flags); + } else { + parcel.writeInt(0); + } + if (mAttributes != null) { + parcel.writeInt(1); + mAttributes.writeToParcel(parcel, flags); + } else { + parcel.writeInt(0); + } + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("PrintJobInfo{"); + builder.append("label: ").append(mLabel); + builder.append(", id: ").append(mId); + builder.append(", status: ").append(stateToString(mState)); + builder.append(", printer: " + mPrinterId); + builder.append(", attributes: " + (mAttributes != null ? mAttributes.toString() : null)); + builder.append("}"); + return builder.toString(); + } + + /** @hide */ + public static String stateToString(int state) { + switch (state) { + case STATE_CREATED: { + return "STATUS_CREATED"; + } + case STATE_QUEUED: { + return "STATE_QUEUED"; + } + case STATE_STARTED: { + return "STATE_STARTED"; + } + case STATE_FAILED: { + return "STATUS_FAILED"; + } + case STATE_COMPLETED: { + return "STATUS_COMPLETED"; + } + case STATE_CANCELED: { + return "STATUS_CANCELED"; + } + default: { + return "STATUS_UNKNOWN"; + } + } + } + + + public static final Parcelable.Creator<PrintJobInfo> CREATOR = + new Creator<PrintJobInfo>() { + @Override + public PrintJobInfo createFromParcel(Parcel parcel) { + return new PrintJobInfo(parcel); + } + + @Override + public PrintJobInfo[] newArray(int size) { + return new PrintJobInfo[size]; + } + }; +} diff --git a/core/java/android/print/PrintManager.java b/core/java/android/print/PrintManager.java new file mode 100644 index 0000000..df14a5c --- /dev/null +++ b/core/java/android/print/PrintManager.java @@ -0,0 +1,399 @@ +/* + * Copyright (C) 2013 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; + +import android.content.Context; +import android.content.IntentSender; +import android.content.IntentSender.SendIntentException; +import android.os.CancellationSignal; +import android.os.Handler; +import android.os.ICancellationSignal; +import android.os.Looper; +import android.os.Message; +import android.os.ParcelFileDescriptor; +import android.os.RemoteException; +import android.print.PrintAdapter.PrintProgressCallback; +import android.util.Log; + +import com.android.internal.os.SomeArgs; + +import libcore.io.IoUtils; + +import java.io.File; +import java.io.FileDescriptor; +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * System level service for accessing the printing capabilities of the platform. + * <p> + * To obtain a handle to the print manager do the following: + * </p> + * <pre> + * PrintManager printManager = + * (PrintManager) context.getSystemService(Context.PRINT_SERVICE); + * </pre> + */ +public final class PrintManager { + + private static final String LOG_TAG = "PrintManager"; + + /** @hide */ + public static final int APP_ID_ANY = -2; + + private final Context mContext; + + private final IPrintManager mService; + + private final int mUserId; + + private final int mAppId; + + private final PrintClient mPrintClient; + + private final Handler mHandler; + + /** + * Listener for the state of a print job. + */ + public static interface PrintJobStateListener { + public void onStateChanged(int state); + } + + /** + * Creates a new instance. + * + * @param context The current context in which to operate. + * @param service The backing system service. + * + * @hide + */ + public PrintManager(Context context, IPrintManager service, int userId, int appId) { + mContext = context; + mService = service; + mUserId = userId; + mAppId = appId; + mPrintClient = new PrintClient(this); + mHandler = new Handler(context.getMainLooper(), null, false) { + @Override + public void handleMessage(Message message) { + SomeArgs args = (SomeArgs) message.obj; + Context context = (Context) args.arg1; + IntentSender intent = (IntentSender) args.arg2; + args.recycle(); + try { + context.startIntentSender(intent, null, 0, 0, 0); + } catch (SendIntentException sie) { + Log.e(LOG_TAG, "Couldn't start print job config activity.", sie); + } + } + }; + } + + /** + * Creates an instance that can access all print jobs. + * + * @param userId The user id for which to get all print jobs. + * @return An instance of the caller has the permission to access + * all print jobs, null otherwise. + * + * @hide + */ + public PrintManager getGlobalPrintManagerForUser(int userId) { + return new PrintManager(mContext, mService, userId, APP_ID_ANY); + } + + PrintJobInfo getPrintJob(int printJobId) { + try { + return mService.getPrintJob(printJobId, mAppId, mUserId); + } catch (RemoteException re) { + Log.e(LOG_TAG, "Error getting print job:" + printJobId, re); + } + return null; + } + + /** + * Gets the print jobs for this application. + * + * @return The print job list. + * + * @see PrintJob + */ + public List<PrintJob> getPrintJobs() { + try { + List<PrintJobInfo> printJobInfos = mService.getPrintJobs(mAppId, mUserId); + if (printJobInfos == null) { + return Collections.emptyList(); + } + final int printJobCount = printJobInfos.size(); + List<PrintJob> printJobs = new ArrayList<PrintJob>(printJobCount); + for (int i = 0; i < printJobCount; i++) { + printJobs.add(new PrintJob(printJobInfos.get(i), this)); + } + return printJobs; + } catch (RemoteException re) { + Log.e(LOG_TAG, "Error getting print jobs!", re); + } + return Collections.emptyList(); + } + + ICancellationSignal cancelPrintJob(int printJobId) { + try { + mService.cancelPrintJob(printJobId, mAppId, mUserId); + } catch (RemoteException re) { + Log.e(LOG_TAG, "Error cancleing a print job:" + printJobId, re); + } + return null; + } + + /** + * Creates a print job for printing a file with default print attributes. + * + * @param printJobName A name for the new print job. + * @param pdfFile The PDF file to print. + * @param attributes The default print job attributes. + * @return The created print job. + */ + public PrintJob print(String printJobName, File pdfFile, PrintAttributes attributes) { + PrintFileAdapter printable = new PrintFileAdapter(pdfFile); + return print(printJobName, printable, attributes); + } + + /** + * Creates a print job for printing a {@link PrintAdapter} with default print + * attributes. + * + * @param printJobName A name for the new print job. + * @param printAdapter The printable adapter to print. + * @param attributes The default print job attributes. + * @return The created print job. + */ + public PrintJob print(String printJobName, PrintAdapter printAdapter, + PrintAttributes attributes) { + PrintAdapterDelegate delegate = new PrintAdapterDelegate(printAdapter, + mContext.getMainLooper()); + try { + PrintJobInfo printJob = mService.print(printJobName, mPrintClient, delegate, + attributes, mAppId, mUserId); + if (printJob != null) { + return new PrintJob(printJob, this); + } + } catch (RemoteException re) { + Log.e(LOG_TAG, "Error creating a print job", re); + } + return null; + } + + private static final class PrintClient extends IPrintClient.Stub { + + private final WeakReference<PrintManager> mWeakPrintManager; + + public PrintClient(PrintManager manager) { + mWeakPrintManager = new WeakReference<PrintManager>(manager); + } + + @Override + public void startPrintJobConfigActivity(IntentSender intent) { + PrintManager manager = mWeakPrintManager.get(); + if (manager != null) { + SomeArgs args = SomeArgs.obtain(); + args.arg1 = manager.mContext; + args.arg2 = intent; + manager.mHandler.obtainMessage(0, args).sendToTarget(); + } + } + } + + private static final class PrintAdapterDelegate extends IPrintAdapter.Stub { + private final Object mLock = new Object(); + + private PrintAdapter mPrintAdapter; + + private Handler mHandler; + + public PrintAdapterDelegate(PrintAdapter printAdapter, Looper looper) { + mPrintAdapter = printAdapter; + mHandler = new MyHandler(looper); + } + + @Override + public void start() { + synchronized (mLock) { + if (isFinishedLocked()) { + return; + } + mHandler.obtainMessage(MyHandler.MESSAGE_START, + mPrintAdapter).sendToTarget(); + } + } + + @Override + public void printAttributesChanged(PrintAttributes attributes) { + synchronized (mLock) { + if (isFinishedLocked()) { + return; + } + SomeArgs args = SomeArgs.obtain(); + args.arg1 = mPrintAdapter; + args.arg2 = attributes; + mHandler.obtainMessage(MyHandler.MESSAGE_PRINT_ATTRIBUTES_CHANGED, + args).sendToTarget(); + } + } + + @Override + public void print(List<PageRange> pages, ParcelFileDescriptor fd, + IPrintProgressListener progressListener) { + synchronized (mLock) { + if (isFinishedLocked()) { + return; + } + SomeArgs args = SomeArgs.obtain(); + args.arg1 = mPrintAdapter; + args.arg2 = pages; + args.arg3 = fd.getFileDescriptor(); + args.arg4 = progressListener; + mHandler.obtainMessage(MyHandler.MESSAGE_PRINT, args).sendToTarget(); + } + } + + @Override + public void finish() { + synchronized (mLock) { + if (isFinishedLocked()) { + return; + } + mHandler.obtainMessage(MyHandler.MESSAGE_FINIS, + mPrintAdapter).sendToTarget(); + } + } + + private boolean isFinishedLocked() { + return mPrintAdapter == null; + } + + private void finishLocked() { + mPrintAdapter = null; + mHandler = null; + } + + private final class MyHandler extends Handler { + public static final int MESSAGE_START = 1; + public static final int MESSAGE_PRINT_ATTRIBUTES_CHANGED = 2; + public static final int MESSAGE_PRINT = 3; + public static final int MESSAGE_FINIS = 4; + + public MyHandler(Looper looper) { + super(looper, null, true); + } + + @Override + public void handleMessage(Message message) { + switch (message.what) { + case MESSAGE_START: { + PrintAdapter adapter = (PrintAdapter) message.obj; + adapter.onStart(); + } break; + + case MESSAGE_PRINT_ATTRIBUTES_CHANGED: { + SomeArgs args = (SomeArgs) message.obj; + PrintAdapter adapter = (PrintAdapter) args.arg1; + PrintAttributes attributes = (PrintAttributes) args.arg2; + args.recycle(); + adapter.onPrintAttributesChanged(attributes); + } break; + + case MESSAGE_PRINT: { + SomeArgs args = (SomeArgs) message.obj; + PrintAdapter adapter = (PrintAdapter) args.arg1; + @SuppressWarnings("unchecked") + List<PageRange> pages = (List<PageRange>) args.arg2; + final FileDescriptor fd = (FileDescriptor) args.arg3; + IPrintProgressListener listener = (IPrintProgressListener) args.arg4; + args.recycle(); + try { + ICancellationSignal remoteSignal = CancellationSignal.createTransport(); + listener.onWriteStarted(adapter.getInfo(), remoteSignal); + + CancellationSignal localSignal = CancellationSignal.fromTransport( + remoteSignal); + adapter.onPrint(pages, fd, localSignal, + new PrintProgressListenerWrapper(listener) { + @Override + public void onPrintFinished(List<PageRange> pages) { + IoUtils.closeQuietly(fd); + super.onPrintFinished(pages); + } + + @Override + public void onPrintFailed(CharSequence error) { + IoUtils.closeQuietly(fd); + super.onPrintFailed(error); + } + }); + } catch (RemoteException re) { + Log.e(LOG_TAG, "Error printing", re); + IoUtils.closeQuietly(fd); + } + } break; + + case MESSAGE_FINIS: { + PrintAdapter adapter = (PrintAdapter) message.obj; + adapter.onFinish(); + synchronized (mLock) { + finishLocked(); + } + } break; + + default: { + throw new IllegalArgumentException("Unknown message: " + + message.what); + } + } + } + } + } + + private static abstract class PrintProgressListenerWrapper extends PrintProgressCallback { + + private final IPrintProgressListener mWrappedListener; + + public PrintProgressListenerWrapper(IPrintProgressListener listener) { + mWrappedListener = listener; + } + + @Override + public void onPrintFinished(List<PageRange> pages) { + try { + mWrappedListener.onWriteFinished(pages); + } catch (RemoteException re) { + Log.e(LOG_TAG, "Error calling onWriteFinished", re); + } + } + + @Override + public void onPrintFailed(CharSequence error) { + try { + mWrappedListener.onWriteFailed(error); + } catch (RemoteException re) { + Log.e(LOG_TAG, "Error calling onWriteFailed", re); + } + } + } +} diff --git a/core/java/android/print/PrinterId.aidl b/core/java/android/print/PrinterId.aidl new file mode 100644 index 0000000..84f422f --- /dev/null +++ b/core/java/android/print/PrinterId.aidl @@ -0,0 +1,19 @@ +/** + * Copyright (c) 2013, 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; + +parcelable PrinterId; diff --git a/core/java/android/print/PrinterId.java b/core/java/android/print/PrinterId.java new file mode 100644 index 0000000..b853eb0 --- /dev/null +++ b/core/java/android/print/PrinterId.java @@ -0,0 +1,160 @@ +/* + * Copyright (C) 2013 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; + +import android.content.ComponentName; +import android.os.Parcel; +import android.os.Parcelable; + +/** + * This class represents the unique id of a printer. + */ +public final class PrinterId implements Parcelable { + + private final ComponentName mServiceComponentName; + + private final String mLocalId; + + /** + * Creates a new instance. + * + * @param serviceComponentName The managing print service. + * @param localId The unique id within the managing service. + * + * @hide + */ + public PrinterId(ComponentName serviceComponentName, String localId) { + mServiceComponentName = serviceComponentName; + mLocalId = localId; + } + + private PrinterId(Parcel parcel) { + mServiceComponentName = parcel.readParcelable(null); + mLocalId = parcel.readString(); + } + + /** + * The id of the print service this printer is managed by. + * + * @return The print service component name. + * + * @hide + */ + public ComponentName getServiceComponentName() { + return mServiceComponentName; + } + + /** + * Gets the local id of this printer in the context + * of the print service that manages it. + * + * @return The local id. + */ + public String getLocalId() { + return mLocalId; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel parcel, int flags) { + parcel.writeParcelable(mServiceComponentName, flags); + parcel.writeString(mLocalId); + } + + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } + if (object == null) { + return false; + } + if (getClass() != object.getClass()) { + return false; + } + PrinterId other = (PrinterId) object; + if (mServiceComponentName == null) { + if (other.mServiceComponentName != null) { + return false; + } + } else if (!mServiceComponentName.equals(other.mServiceComponentName)) { + return false; + } + if (mLocalId != other.mLocalId) { + return false; + } + return true; + } + + @Override + public int hashCode() { + final int prime = 31; + int hashCode = 1; + hashCode = prime * hashCode + ((mServiceComponentName != null) + ? mServiceComponentName.hashCode() : 1); + hashCode = prime * hashCode + mLocalId.hashCode(); + return hashCode; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("PrinterId{"); + builder.append(mServiceComponentName.flattenToString()); + builder.append(":"); + builder.append(mLocalId); + builder.append('}'); + return builder.toString(); + } + + /** + * @hide + */ + public String flattenToString() { + return mServiceComponentName.flattenToString() + ":" + mLocalId; + } + + /** + * @hide + */ + public static PrinterId unflattenFromString(String string) { + String[] split = string.split(":"); + if (split.length != 2) { + throw new IllegalArgumentException("Not well-formed printer id:" + string); + } + ComponentName component = ComponentName.unflattenFromString(split[0]); + String localId = String.valueOf(split[1]); + return new PrinterId(component, localId); + } + + public static final Parcelable.Creator<PrinterId> CREATOR = + new Creator<PrinterId>() { + @Override + public PrinterId createFromParcel(Parcel parcel) { + return new PrinterId(parcel); + } + + @Override + public PrinterId[] newArray(int size) { + return new PrinterId[size]; + } + }; +} diff --git a/core/java/android/print/PrinterInfo.aidl b/core/java/android/print/PrinterInfo.aidl new file mode 100644 index 0000000..6ec5a58 --- /dev/null +++ b/core/java/android/print/PrinterInfo.aidl @@ -0,0 +1,19 @@ +/** + * Copyright (c) 2013, 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; + +parcelable PrinterInfo; diff --git a/core/java/android/print/PrinterInfo.java b/core/java/android/print/PrinterInfo.java new file mode 100644 index 0000000..9283472 --- /dev/null +++ b/core/java/android/print/PrinterInfo.java @@ -0,0 +1,798 @@ +/* + * Copyright (C) 2013 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; + +import android.os.Parcel; +import android.os.Parcelable; +import android.print.PrintAttributes.Margins; +import android.print.PrintAttributes.MediaSize; +import android.print.PrintAttributes.Resolution; +import android.print.PrintAttributes.Tray; +import android.text.TextUtils; +import android.util.SparseIntArray; + +import java.util.ArrayList; +import java.util.List; + +/** + * This class represents the description of a printer. A description + * contains the printer id, human readable name, status, and available + * options for various printer capabilities, such as media size, etc. + */ +public final class PrinterInfo implements Parcelable { + /** + * Undefined default value. + * + * @hide + */ + public static final int DEFAULT_UNDEFINED = -1; + + private static final int MIN_COPIES = 1; + + private static final int PROPERTY_MEDIA_SIZE = 0; + private static final int PROPERTY_RESOLUTION = 1; + private static final int PROPERTY_INPUT_TRAY = 2; + private static final int PROPERTY_OUTPUT_TRAY = 3; + private static final int PROPERTY_DUPLEX_MODE = 4; + private static final int PROPERTY_COLOR_MODE = 5; + private static final int PROPERTY_FITTING_MODE = 6; + private static final int PROPERTY_ORIENTATION = 7; + + /** Printer status: the printer is ready to print. */ + public static final int STATUS_READY = 1; + + // TODO: Add printer status constants. + + private PrinterId mId; + private CharSequence mLabel; + private int mStatus; + + private Margins mMinMargins; + private final List<MediaSize> mMediaSizes = new ArrayList<MediaSize>(); // required + private final List<Resolution> mResolutions = new ArrayList<Resolution>(); // required + private List<Tray> mInputTrays; + private List<Tray> mOutputTrays; + + private int mDuplexModes; + private int mColorModes; + private int mFittingModes; + private int mOrientations; + + private final SparseIntArray mDefaults = new SparseIntArray(); + private Margins mDefaultMargins; + + private PrinterInfo() { + mDefaults.put(PROPERTY_MEDIA_SIZE, DEFAULT_UNDEFINED); + mDefaults.put(PROPERTY_RESOLUTION, DEFAULT_UNDEFINED); + mDefaults.put(PROPERTY_INPUT_TRAY, DEFAULT_UNDEFINED); + mDefaults.put(PROPERTY_OUTPUT_TRAY, DEFAULT_UNDEFINED); + mDefaults.put(PROPERTY_DUPLEX_MODE, DEFAULT_UNDEFINED); + mDefaults.put(PROPERTY_COLOR_MODE, DEFAULT_UNDEFINED); + mDefaults.put(PROPERTY_FITTING_MODE, DEFAULT_UNDEFINED); + mDefaults.put(PROPERTY_ORIENTATION, DEFAULT_UNDEFINED); + } + + /** + * Get the globally unique printer id. + * + * @return The printer id. + */ + public PrinterId getId() { + return mId; + } + + /** + * Gets the human readable printer label. + * + * @return The human readable label. + */ + public CharSequence getLabel() { + return mLabel; + } + + /** + * Gets the status of the printer. + * + * @return The status. + */ + public int getStatus() { + return mStatus; + } + + /** + * Gets the supported media sizes. + * + * @return The supported media sizes. + */ + public List<MediaSize> getMediaSizes() { + return mMediaSizes; + } + + /** + * Gets the supported resolutions. + * + * @return The supported resolutions. + */ + public List<Resolution> getResolutions() { + return mResolutions; + } + + /** + * Gets the minimal supported margins. + * + * @return The minimal margins. + */ + public Margins getMinMargins() { + return mMinMargins; + } + + /** + * Gets the available input trays. + * + * @return The input trays. + */ + public List<Tray> getInputTrays() { + return mInputTrays; + } + + /** + * Gets the available output trays. + * + * @return The output trays. + */ + public List<Tray> getOutputTrays() { + return mOutputTrays; + } + + /** + * Gets the supported duplex modes. + * + * @return The duplex modes. + * + * @see PrintAttributes#DUPLEX_MODE_NONE + * @see PrintAttributes#DUPLEX_MODE_LONG_EDGE + * @see PrintAttributes#DUPLEX_MODE_SHORT_EDGE + */ + public int getDuplexModes() { + return mDuplexModes; + } + + /** + * Gets the supported color modes. + * + * @return The color modes. + * + * @see PrintAttributes#COLOR_MODE_COLOR + * @see PrintAttributes#COLOR_MODE_MONOCHROME + */ + public int getColorModes() { + return mColorModes; + } + + /** + * Gets the supported fitting modes. + * + * @return The fitting modes. + * + * @see PrintAttributes#FITTING_MODE_NONE + * @see PrintAttributes#FITTING_MODE_FIT_TO_PAGE + */ + public int getFittingModes() { + return mFittingModes; + } + + /** + * Gets the supported orientations. + * + * @return The orientations. + * + * @see PrintAttributes#ORIENTATION_PORTRAIT + * @see PrintAttributes#ORIENTATION_LANDSCAPE + */ + public int getOrientations() { + return mOrientations; + } + + /** + * Gets the default print attributes. + * + * @param outAttributes The attributes to populated. + */ + public void getDefaults(PrintAttributes outAttributes) { + outAttributes.clear(); + + // TODO: Do we want a printer to specify default copies? + outAttributes.setCopies(MIN_COPIES); + + outAttributes.setMargins(mDefaultMargins); + + final int mediaSizeIndex = mDefaults.get(PROPERTY_MEDIA_SIZE); + if (mediaSizeIndex >= 0) { + outAttributes.setMediaSize(mMediaSizes.get(mediaSizeIndex)); + } + + final int resolutionIndex = mDefaults.get(PROPERTY_RESOLUTION); + if (resolutionIndex >= 0) { + outAttributes.setResolution(mResolutions.get(resolutionIndex)); + } + + final int inputTrayIndex = mDefaults.get(PROPERTY_INPUT_TRAY); + if (inputTrayIndex >= 0) { + outAttributes.setInputTray(mInputTrays.get(inputTrayIndex)); + } + + final int outputTrayIndex = mDefaults.get(PROPERTY_OUTPUT_TRAY); + if (outputTrayIndex >= 0) { + outAttributes.setOutputTray(mOutputTrays.get(outputTrayIndex)); + } + + final int duplexMode = mDefaults.get(PROPERTY_DUPLEX_MODE); + if (duplexMode > 0) { + outAttributes.setDuplexMode(duplexMode); + } + + final int colorMode = mDefaults.get(PROPERTY_COLOR_MODE); + if (colorMode > 0) { + outAttributes.setColorMode(mColorModes & colorMode); + } + + final int fittingMode = mDefaults.get(PROPERTY_FITTING_MODE); + if (fittingMode > 0) { + outAttributes.setFittingMode(fittingMode); + } + + final int orientation = mDefaults.get(PROPERTY_ORIENTATION); + if (orientation > 0) { + outAttributes.setOrientation(orientation); + } + } + + private PrinterInfo(Parcel parcel) { + mId = parcel.readParcelable(null); + mLabel = parcel.readCharSequence(); + mStatus = parcel.readInt(); + + mMinMargins = readMargins(parcel); + readMediaSizes(parcel); + readResolutions(parcel); + mInputTrays = readInputTrays(parcel); + mOutputTrays = readOutputTrays(parcel); + + mColorModes = parcel.readInt(); + mDuplexModes = parcel.readInt(); + mFittingModes = parcel.readInt(); + mOrientations = parcel.readInt(); + + readDefaults(parcel); + mDefaultMargins = readMargins(parcel); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel parcel, int flags) { + parcel.writeParcelable(mId, flags); + parcel.writeCharSequence(mLabel); + parcel.writeInt(mStatus); + + writeMargins(mMinMargins, parcel); + writeMediaSizes(parcel); + writeResolutions(parcel); + writeInputTrays(parcel); + writeOutputTrays(parcel); + + parcel.writeInt(mColorModes); + parcel.writeInt(mDuplexModes); + parcel.writeInt(mFittingModes); + parcel.writeInt(mOrientations); + + writeDefaults(parcel); + writeMargins(mDefaultMargins, parcel); + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("PrinterInfo{"); + builder.append(mId).append(", \""); + builder.append(mLabel); + builder.append("\"}"); + return builder.toString(); + } + + private void writeMediaSizes(Parcel parcel) { + if (mMediaSizes == null) { + parcel.writeInt(0); + return; + } + final int mediaSizeCount = mMediaSizes.size(); + parcel.writeInt(mediaSizeCount); + for (int i = 0; i < mediaSizeCount; i++) { + mMediaSizes.get(i).writeToParcel(parcel); + } + } + + private void readMediaSizes(Parcel parcel) { + final int mediaSizeCount = parcel.readInt(); + for (int i = 0; i < mediaSizeCount; i++) { + mMediaSizes.add(MediaSize.createFromParcel(parcel)); + } + } + + private void writeResolutions(Parcel parcel) { + final int resolutionCount = mResolutions.size(); + parcel.writeInt(resolutionCount); + for (int i = 0; i < resolutionCount; i++) { + mResolutions.get(i).writeToParcel(parcel); + } + } + + private void readResolutions(Parcel parcel) { + final int resolutionCount = parcel.readInt(); + for (int i = 0; i < resolutionCount; i++) { + mResolutions.add(Resolution.createFromParcel(parcel)); + } + } + + private void writeMargins(Margins margins, Parcel parcel) { + if (margins == null) { + parcel.writeInt(0); + } else { + parcel.writeInt(1); + margins.writeToParcel(parcel); + } + } + + private Margins readMargins(Parcel parcel) { + return (parcel.readInt() == 1) ? Margins.createFromParcel(parcel) : null; + } + + private void writeInputTrays(Parcel parcel) { + if (mInputTrays == null) { + parcel.writeInt(0); + return; + } + final int inputTrayCount = mInputTrays.size(); + parcel.writeInt(inputTrayCount); + for (int i = 0; i < inputTrayCount; i++) { + mInputTrays.get(i).writeToParcel(parcel); + } + } + + private List<Tray> readInputTrays(Parcel parcel) { + final int inputTrayCount = parcel.readInt(); + if (inputTrayCount <= 0) { + return null; + } + List<Tray> inputTrays = new ArrayList<Tray>(inputTrayCount); + for (int i = 0; i < inputTrayCount; i++) { + inputTrays.add(Tray.createFromParcel(parcel)); + } + return inputTrays; + } + + private void writeOutputTrays(Parcel parcel) { + if (mOutputTrays == null) { + parcel.writeInt(0); + return; + } + final int outputTrayCount = mOutputTrays.size(); + parcel.writeInt(outputTrayCount); + for (int i = 0; i < outputTrayCount; i++) { + mOutputTrays.get(i).writeToParcel(parcel); + } + } + + private List<Tray> readOutputTrays(Parcel parcel) { + final int outputTrayCount = parcel.readInt(); + if (outputTrayCount <= 0) { + return null; + } + List<Tray> outputTrays = new ArrayList<Tray>(outputTrayCount); + for (int i = 0; i < outputTrayCount; i++) { + outputTrays.add(Tray.createFromParcel(parcel)); + } + return outputTrays; + } + + private void readDefaults(Parcel parcel) { + final int defaultCount = parcel.readInt(); + for (int i = 0; i < defaultCount; i++) { + mDefaults.append(mDefaults.size(), parcel.readInt()); + } + } + + private void writeDefaults(Parcel parcel) { + final int defaultCount = mDefaults.size(); + parcel.writeInt(defaultCount); + for (int i = 0; i < defaultCount; i++) { + parcel.writeInt(mDefaults.valueAt(i)); + } + } + + /** + * Builder for creating of a {@link PrinterInfo}. This class is responsible + * to enforce that all required attributes have at least one default value. + * In other words, this class creates only well-formed {@link PrinterInfo}s. + * <p> + * Look at the individual methods for a reference whether a property is + * required or if it is optional. + * </p> + */ + public static final class Builder { + private final PrinterInfo mPrinterInfo; + + /** + * Creates a new instance. + * + * @param printerId The printer id. + * @param label The human readable printer label. + * + * @throws IllegalArgumentException IF the printer id is null. + * @throws IllegalArgumentException IF the label is empty. + */ + public Builder(PrinterId printerId, CharSequence label) { + if (printerId == null) { + throw new IllegalArgumentException("printerId cannot be null."); + } + if (TextUtils.isEmpty(label)) { + throw new IllegalArgumentException("label cannot be empty."); + } + mPrinterInfo = new PrinterInfo(); + mPrinterInfo.mLabel = label; + mPrinterInfo.mId = printerId; + } + + /** + * Sets the printer status. + * <p> + * <strong>Required:</strong> Yes + * </p> + * + * @param status The status. + * @return This builder. + */ + public Builder setStatus(int status) { + mPrinterInfo.mStatus = status; + return this; + } + + /** + * Adds a supported media size. + * <p> + * <strong>Required:</strong> Yes + * </p> + * + * @param mediaSize A media size. + * @param isDefault Whether this is the default. + * @return This builder. + * @throws IllegalArgumentException If set as default and there + * is already a default. + * + * @see PrintAttributes.MediaSize + */ + public Builder addMediaSize(MediaSize mediaSize, boolean isDefault) { + final int insertionIndex = mPrinterInfo.mMediaSizes.size(); + mPrinterInfo.mMediaSizes.add(mediaSize); + if (isDefault) { + throwIfDefaultAlreadySpecified(PROPERTY_MEDIA_SIZE); + mPrinterInfo.mDefaults.put(PROPERTY_MEDIA_SIZE, insertionIndex); + } + return this; + } + + /** + * Adds a supported resolution. + * <p> + * <strong>Required:</strong> Yes + * </p> + * + * @param resolution A resolution. + * @param isDefault Whether this is the default. + * @return This builder. + * + * @throws IllegalArgumentException If set as default and there + * is already a default. + * + * @see PrintAttributes.Resolution + */ + public Builder addResolution(Resolution resolution, boolean isDefault) { + final int insertionIndex = mPrinterInfo.mResolutions.size(); + mPrinterInfo.mResolutions.add(resolution); + if (isDefault) { + throwIfDefaultAlreadySpecified(PROPERTY_RESOLUTION); + mPrinterInfo.mDefaults.put(PROPERTY_RESOLUTION, insertionIndex); + } + return this; + } + + /** + * Sets the minimal margins. + * <p> + * <strong>Required:</strong> No + * </p> + * + * @param margins The margins. + * @param defaultMargins The default margins. + * @return This builder. + * + * @see PrintAttributes.Margins + */ + public Builder setMinMargins(Margins margins, Margins defaultMargins) { + if (margins.getLeftMils() > defaultMargins.getLeftMils() + || margins.getTopMils() > defaultMargins.getTopMils() + || margins.getRightMils() < defaultMargins.getRightMils() + || margins.getBottomMils() < defaultMargins.getBottomMils()) { + throw new IllegalArgumentException("Default margins" + + " cannot be outside of the min margins."); + } + mPrinterInfo.mMinMargins = margins; + mPrinterInfo.mDefaultMargins = defaultMargins; + return this; + } + + /** + * Adds an input tray. + * <p> + * <strong>Required:</strong> No + * </p> + * + * @param inputTray A tray. + * @param isDefault Whether this is the default. + * @return This builder. + * + * @throws IllegalArgumentException If set as default and there + * is already a default. + * + * @see PrintAttributes.Tray + */ + public Builder addInputTray(Tray inputTray, boolean isDefault) { + if (mPrinterInfo.mInputTrays == null) { + mPrinterInfo.mInputTrays = new ArrayList<Tray>(); + } + final int insertionIndex = mPrinterInfo.mInputTrays.size(); + mPrinterInfo.mInputTrays.add(inputTray); + if (isDefault) { + throwIfDefaultAlreadySpecified(PROPERTY_INPUT_TRAY); + mPrinterInfo.mDefaults.put(PROPERTY_INPUT_TRAY, insertionIndex); + } + return this; + } + + /** + * Adds an output tray. + * <p> + * <strong>Required:</strong> No + * </p> + * + * @param outputTray A tray. + * @param isDefault Whether this is the default. + * @return This builder. + * + * @throws IllegalArgumentException If set as default and there + * is already a default. + * + * @see PrintAttributes.Tray + */ + public Builder addOutputTray(Tray outputTray, boolean isDefault) { + if (mPrinterInfo.mOutputTrays == null) { + mPrinterInfo.mOutputTrays = new ArrayList<Tray>(); + } + final int insertionIndex = mPrinterInfo.mOutputTrays.size(); + mPrinterInfo.mOutputTrays.add(outputTray); + if (isDefault) { + throwIfDefaultAlreadySpecified(PROPERTY_OUTPUT_TRAY); + mPrinterInfo.mDefaults.put(PROPERTY_OUTPUT_TRAY, insertionIndex); + } + return this; + } + + /** + * Sets the color modes. + * <p> + * <strong>Required:</strong> Yes + * </p> + * + * @param colorModes The color mode bit mask. + * @param defaultColorMode The default color mode. + * @return This builder. + * + * @throws IllegalArgumentException If color modes contains an invalid + * mode bit or if the default color mode is invalid. + * + * @see PrintAttributes#COLOR_MODE_COLOR + * @see PrintAttributes#COLOR_MODE_MONOCHROME + */ + public Builder setColorModes(int colorModes, int defaultColorMode) { + int currentModes = colorModes; + while (currentModes > 0) { + final int currentMode = (1 << Integer.numberOfTrailingZeros(currentModes)); + currentModes &= ~currentMode; + PrintAttributes.enforceValidColorMode(currentMode); + } + if ((colorModes & defaultColorMode) == 0) { + throw new IllegalArgumentException("Default color mode not in color modes."); + } + PrintAttributes.enforceValidColorMode(colorModes); + mPrinterInfo.mColorModes = colorModes; + mPrinterInfo.mDefaults.put(PROPERTY_COLOR_MODE, defaultColorMode); + return this; + } + + /** + * Set the duplex modes. + * <p> + * <strong>Required:</strong> No + * </p> + * + * @param duplexModes The duplex mode bit mask. + * @param defaultDuplexMode The default duplex mode. + * @return This builder. + * + * @throws IllegalArgumentException If duplex modes contains an invalid + * mode bit or if the default duplex mode is invalid. + * + * @see PrintAttributes#DUPLEX_MODE_NONE + * @see PrintAttributes#DUPLEX_MODE_LONG_EDGE + * @see PrintAttributes#DUPLEX_MODE_SHORT_EDGE + */ + public Builder setDuplexModes(int duplexModes, int defaultDuplexMode) { + int currentModes = duplexModes; + while (currentModes > 0) { + final int currentMode = (1 << Integer.numberOfTrailingZeros(currentModes)); + currentModes &= ~currentMode; + PrintAttributes.enforceValidDuplexMode(currentMode); + } + if ((duplexModes & defaultDuplexMode) == 0) { + throw new IllegalArgumentException("Default duplex mode not in duplex modes."); + } + PrintAttributes.enforceValidDuplexMode(defaultDuplexMode); + mPrinterInfo.mDuplexModes = duplexModes; + mPrinterInfo.mDefaults.put(PROPERTY_DUPLEX_MODE, defaultDuplexMode); + return this; + } + + /** + * Sets the fitting modes. + * <p> + * <strong>Required:</strong> No + * </p> + * + * @param fittingModes The fitting mode bit mask. + * @param defaultFittingMode The default fitting mode. + * @return This builder. + * + * @throws IllegalArgumentException If fitting modes contains an invalid + * mode bit or if the default fitting mode is invalid. + * + * @see PrintAttributes#FITTING_MODE_NONE + * @see PrintAttributes#FITTING_MODE_FIT_TO_PAGE + */ + public Builder setFittingModes(int fittingModes, int defaultFittingMode) { + int currentModes = fittingModes; + while (currentModes > 0) { + final int currentMode = (1 << Integer.numberOfTrailingZeros(currentModes)); + currentModes &= ~currentMode; + PrintAttributes.enfoceValidFittingMode(currentMode); + } + if ((fittingModes & defaultFittingMode) == 0) { + throw new IllegalArgumentException("Default fitting mode not in fiting modes."); + } + PrintAttributes.enfoceValidFittingMode(defaultFittingMode); + mPrinterInfo.mFittingModes = fittingModes; + mPrinterInfo.mDefaults.put(PROPERTY_FITTING_MODE, defaultFittingMode); + return this; + } + + /** + * Sets the orientations. + * <p> + * <strong>Required:</strong> Yes + * </p> + * + * @param orientations The orientation bit mask. + * @param defaultOrientation The default orientation. + * @return This builder. + * + * @throws IllegalArgumentException If orientations contains an invalid + * mode bit or if the default orientation is invalid. + * + * @see PrintAttributes#ORIENTATION_PORTRAIT + * @see PrintAttributes#ORIENTATION_LANDSCAPE + */ + public Builder setOrientations(int orientations, int defaultOrientation) { + int currentOrientaions = orientations; + while (currentOrientaions > 0) { + final int currentOrnt = (1 << Integer.numberOfTrailingZeros(currentOrientaions)); + currentOrientaions &= ~currentOrnt; + PrintAttributes.enforceValidOrientation(currentOrnt); + } + if ((orientations & defaultOrientation) == 0) { + throw new IllegalArgumentException("Default orientation not in orientations."); + } + PrintAttributes.enforceValidOrientation(defaultOrientation); + mPrinterInfo.mOrientations = orientations; + mPrinterInfo.mDefaults.put(PROPERTY_ORIENTATION, defaultOrientation); + return this; + } + + /** + * Crates a new {@link PrinterInfo} enforcing that all required properties + * have need specified. See individual methods in this class for reference + * about required attributes. + * + * @return A new {@link PrinterInfo}. + * + * @throws IllegalStateException If a required attribute was not specified. + */ + public PrinterInfo create() { + if (mPrinterInfo.mMediaSizes == null || mPrinterInfo.mMediaSizes.isEmpty()) { + throw new IllegalStateException("No media size specified."); + } + if (mPrinterInfo.mDefaults.valueAt(PROPERTY_MEDIA_SIZE) == DEFAULT_UNDEFINED) { + throw new IllegalStateException("No default media size specified."); + } + if (mPrinterInfo.mResolutions == null || mPrinterInfo.mResolutions.isEmpty()) { + throw new IllegalStateException("No resolution specified."); + } + if (mPrinterInfo.mDefaults.valueAt(PROPERTY_RESOLUTION) == DEFAULT_UNDEFINED) { + throw new IllegalStateException("No default resolution specified."); + } + if (mPrinterInfo.mColorModes == 0) { + throw new IllegalStateException("No color mode specified."); + } + if (mPrinterInfo.mDefaults.valueAt(PROPERTY_COLOR_MODE) == DEFAULT_UNDEFINED) { + throw new IllegalStateException("No default color mode specified."); + } + if (mPrinterInfo.mOrientations == 0) { + throw new IllegalStateException("No oprientation specified."); + } + if (mPrinterInfo.mDefaults.valueAt(PROPERTY_ORIENTATION) == DEFAULT_UNDEFINED) { + throw new IllegalStateException("No default orientation specified."); + } + if (mPrinterInfo.mMinMargins == null) { + mPrinterInfo.mMinMargins = new Margins(0, 0, 0, 0); + } + if (mPrinterInfo.mDefaultMargins == null) { + mPrinterInfo.mDefaultMargins = mPrinterInfo.mMinMargins; + } + return mPrinterInfo; + } + + private void throwIfDefaultAlreadySpecified(int propertyIndex) { + if (mPrinterInfo.mDefaults.get(propertyIndex) != DEFAULT_UNDEFINED) { + throw new IllegalArgumentException("Default already specified."); + } + } + } + + public static final Parcelable.Creator<PrinterInfo> CREATOR = + new Parcelable.Creator<PrinterInfo>() { + @Override + public PrinterInfo createFromParcel(Parcel parcel) { + return new PrinterInfo(parcel); + } + + @Override + public PrinterInfo[] newArray(int size) { + return new PrinterInfo[size]; + } + }; +} diff --git a/core/java/android/printservice/IPrintService.aidl b/core/java/android/printservice/IPrintService.aidl new file mode 100644 index 0000000..eabd96d --- /dev/null +++ b/core/java/android/printservice/IPrintService.aidl @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2013 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.printservice; + +import android.os.ICancellationSignal; +import android.print.PrintJobInfo; +import android.print.PrinterId; +import android.printservice.IPrintServiceClient; + +/** + * Top-level interface to a print service component. + * + * @hide + */ +oneway interface IPrintService { + void setClient(IPrintServiceClient client); + void requestCancelPrintJob(in PrintJobInfo printJob); + void onPrintJobQueued(in PrintJobInfo printJob); + void startPrinterDiscovery(); + void stopPrinterDiscovery(); +} diff --git a/core/java/android/printservice/IPrintServiceClient.aidl b/core/java/android/printservice/IPrintServiceClient.aidl new file mode 100644 index 0000000..cff8c02 --- /dev/null +++ b/core/java/android/printservice/IPrintServiceClient.aidl @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2013 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.printservice; + +import android.os.ParcelFileDescriptor; +import android.print.PrintJobInfo; +import android.print.PrinterId; +import android.print.PrinterInfo; + +/** + * The top-level interface from a print service to the system. + * + * @hide + */ +interface IPrintServiceClient { + List<PrintJobInfo> getPrintJobs(); + PrintJobInfo getPrintJob(int printJobId); + boolean setPrintJobState(int printJobId, int status); + boolean setPrintJobTag(int printJobId, String tag); + oneway void writePrintJobData(in ParcelFileDescriptor fd, int printJobId); + oneway void addDiscoveredPrinters(in List<PrinterInfo> printers); + oneway void removeDiscoveredPrinters(in List<PrinterId> printers); +} diff --git a/core/java/android/printservice/PrintJob.java b/core/java/android/printservice/PrintJob.java new file mode 100644 index 0000000..9688761 --- /dev/null +++ b/core/java/android/printservice/PrintJob.java @@ -0,0 +1,254 @@ +/* + * Copyright (C) 2013 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.printservice; + +import java.io.FileDescriptor; +import java.io.IOException; + +import android.os.ParcelFileDescriptor; +import android.os.RemoteException; +import android.print.PrintJobInfo; +import android.util.Log; + +/** + * This class represents a print job from the perspective of a + * print service. It provides APIs for observing the print job + * state and performing operations on the print job. + */ +public final class PrintJob { + + private static final String LOG_TAG = "PrintJob"; + + private final int mId; + + private final IPrintServiceClient mPrintServiceClient; + + private PrintJobInfo mCachedInfo; + + PrintJob(PrintJobInfo info, IPrintServiceClient client) { + if (client == null) { + throw new IllegalStateException("Print serivice not connected!"); + } + mCachedInfo = info; + mId = info.getId(); + mPrintServiceClient = client; + } + + /** + * Gets the unique print job id. + * + * @return The id. + */ + public int getId() { + return mId; + } + + /** + * Gets the {@link PrintJobInfo} that describes this job. + * <p> + * <strong>Node:</strong>The returned info object is a snapshot of the + * current print job state. Every call to this method returns a fresh + * info object that reflects the current print jobs state. + * </p> + * + * @return The print job info. + */ + public PrintJobInfo getInfo() { + PrintJobInfo info = null; + try { + info = mPrintServiceClient.getPrintJob(mId); + } catch (RemoteException re) { + Log.e(LOG_TAG, "Couldn't get info for job: " + mId, re); + } + if (info != null) { + mCachedInfo = info; + } + return mCachedInfo; + } + + /** + * Gets whether this print job is queued. Such a print job is + * ready to be printed and can be started. + * + * @return Whether the print job is queued. + * + * @see #start() + */ + public boolean isQueued() { + return getInfo().getState() == PrintJobInfo.STATE_QUEUED; + } + + /** + * Gets whether this print job is started. Such a print job is + * being printed and can be completed or canceled or failed. + * + * @return Whether the print job is started. + * + * @see #complete() + * @see #cancel() + * @see #fail() + */ + public boolean isStarted() { + return getInfo().getState() == PrintJobInfo.STATE_STARTED; + } + + /** + * Starts the print job. You should call this method if {@link + * #isQueued()} returns true and you started printing. + * + * @return Whether the job as started. + * + * @see #isQueued() + */ + public boolean start() { + if (isQueued()) { + return setState(PrintJobInfo.STATE_STARTED); + } + return false; + } + + /** + * Completes the print job. You should call this method if {@link + * #isStarted()} returns true and you are done printing. + * + * @return Whether the job as completed. + * + * @see #isStarted() + */ + public boolean complete() { + if (isStarted()) { + return setState(PrintJobInfo.STATE_COMPLETED); + } + return false; + } + + /** + * Fails the print job. You should call this method if {@link + * #isStarted()} returns true you filed while printing. + * + * @return Whether the job as failed. + * + * @see #isStarted() + */ + public boolean fail(CharSequence error) { + // TODO: Propagate the error message to the UI. + if (isStarted()) { + return setState(PrintJobInfo.STATE_FAILED); + } + return false; + } + + /** + * Cancels the print job. You should call this method if {@link + * #isStarted()} returns true and you canceled the print job as a + * response to a call to {@link PrintService#onRequestCancelPrintJob( + * PrintJob)}. + * + * @return Whether the job as canceled. + * + * @see #isStarted() + */ + public boolean cancel() { + if (isStarted()) { + return setState(PrintJobInfo.STATE_CANCELED); + } + return false; + } + + /** + * Sets a tag that is valid in the context of a {@link PrintService} + * and is not interpreted by the system. For example, a print service + * may set as a tag the key of the print job returned by a remote + * print server, if the printing is off handed to a cloud based service. + * + * @param tag The tag. + * @return True if the tag was set, false otherwise. + */ + public boolean setTag(String tag) { + try { + return mPrintServiceClient.setPrintJobTag(mId, tag); + } catch (RemoteException re) { + Log.e(LOG_TAG, "Error setting tag for job:" + mId, re); + } + return false; + } + + /** + * Gets the data associated with this print job. It is a responsibility of + * the print service to open a stream to the returned file descriptor + * and fully read the content. + * + * @return A file descriptor for reading the data or <code>null</code>. + */ + public final FileDescriptor getData() { + ParcelFileDescriptor source = null; + ParcelFileDescriptor sink = null; + try { + ParcelFileDescriptor[] fds = ParcelFileDescriptor.createPipe(); + source = fds[0]; + sink = fds[1]; + mPrintServiceClient.writePrintJobData(sink, mId); + return source.getFileDescriptor(); + } catch (IOException ioe) { + Log.e(LOG_TAG, "Error calling getting print job data!", ioe); + } catch (RemoteException re) { + Log.e(LOG_TAG, "Error calling getting print job data!", re); + } finally { + if (sink != null) { + try { + sink.close(); + } catch (IOException ioe) { + /* ignore */ + } + } + } + return null; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + PrintJob other = (PrintJob) obj; + return (mId == other.mId); + } + + @Override + public int hashCode() { + return mId; + } + + private boolean setState(int state) { + // Best effort - update the state of the cached info since + // we may not be able to re-fetch it later if the job gets + // removed from the spooler. + mCachedInfo.setState(state); + try { + return mPrintServiceClient.setPrintJobState(mId, state); + } catch (RemoteException re) { + Log.e(LOG_TAG, "Error setting the state of job:" + mId, re); + } + return false; + } +} diff --git a/core/java/android/printservice/PrintService.java b/core/java/android/printservice/PrintService.java new file mode 100644 index 0000000..d5cadc0 --- /dev/null +++ b/core/java/android/printservice/PrintService.java @@ -0,0 +1,462 @@ +/* + * Copyright (C) 2013 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.printservice; + +import android.app.Service; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.os.Handler; +import android.os.IBinder; +import android.os.Looper; +import android.os.Message; +import android.os.RemoteException; +import android.print.PrintJobInfo; +import android.print.PrinterId; +import android.print.PrinterInfo; +import android.util.Log; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * <p> + * This is the base class for implementing print services. A print service + * knows how to discover and interact one or more printers via one or more + * protocols. + * </p> + * <h3>Printer discovery</h3> + * <p> + * A print service is responsible for discovering and reporting printers. + * A printer discovery period starts with a call to + * {@link #onStartPrinterDiscovery()} and ends with a call to + * {@link #onStopPrinterDiscovery()}. During a printer discovery + * period the print service reports newly discovered printers by + * calling {@link #addDiscoveredPrinters(List)} and added printers + * that disappeared by calling {@link #removeDiscoveredPrinters(List)}. + * Calls to {@link #addDiscoveredPrinters(List)} and + * {@link #removeDiscoveredPrinters(List)} before a call to + * {@link #onStartPrinterDiscovery()} and after a call to + * {@link #onStopPrinterDiscovery()} is a no-op. + * </p> + * <p> + * For every printer discovery period all printers have to be added. Each + * printer known to this print service should be added only once during a + * discovery period, unless it was added and then removed before that. + * Only an already added printer can be removed. + * </p> + * <h3>Print jobs</h3> + * <p> + * When a new print job targeted to the printers managed by this print + * service is queued, i.e. ready for processing by the print service, + * a call to {@link #onPrintJobQueued(PrintJob)} is made and the print + * service may handle it immediately or schedule that for an appropriate + * time in the future. The list of all print jobs for this service + * are be available by calling {@link #getPrintJobs()}. A queued print + * job is in a {@link PrintJobInfo#STATE_QUEUED} state. + * </p> + * <p> + * A print service is responsible for setting the print job state as + * appropriate while processing it. Initially, a print job is in a + * {@link PrintJobInfo#STATE_QUEUED} state which means that the data to + * be printed is spooled by the system and the print service can obtain + * that data by calling {@link PrintJob#getData()}. After the print + * service starts printing the data it should set the print job state + * to {@link PrintJobInfo#STATE_STARTED}. Upon successful completion, the + * print job state has to be set to {@link PrintJobInfo#STATE_COMPLETED}. + * In a case of a failure, the print job state should be set to + * {@link PrintJobInfo#STATE_FAILED}. If a print job is in a + * {@link PrintJobInfo#STATE_STARTED} state and the user requests to + * cancel it, the print service will receive a call to + * {@link #onRequestCancelPrintJob(PrintJob)} which requests from the + * service to do a best effort in canceling the job. In case the job + * is successfully canceled, its state has to be set to + * {@link PrintJobInfo#STATE_CANCELED}. + * </p> + * <h3>Lifecycle</h3> + * <p> + * The lifecycle of a print service is managed exclusively by the system + * and follows the established service lifecycle. Additionally, starting + * or stopping a print service is triggered exclusively by an explicit + * user action through enabling or disabling it in the device settings. + * After the system binds to a print service, it calls {@link #onConnected()}. + * This method can be overriden by clients to perform post binding setup. + * Also after the system unbinds from a print service, it calls + * {@link #onDisconnected()}. This method can be overriden by clients to + * perform post unbinding cleanup. + * </p> + * <h3>Declaration</h3> + * <p> + * A print service is declared as any other service in an AndroidManifest.xml + * but it must also specify that it handles the {@link android.content.Intent} + * with action {@link #SERVICE_INTERFACE}. Failure to declare this intent + * will cause the system to ignore the print service. Additionally, a print + * service must request the {@link android.Manifest.permission#BIND_PRINT_SERVICE} + * permission to ensure that only the system can bind to it. Failure to + * declare this intent will cause the system to ignore the print service. + * Following is an example declaration: + * </p> + * <pre> + * <service android:name=".MyPrintService" + * android:permission="android.permission.BIND_PRINT_SERVICE"> + * <intent-filter> + * <action android:name="android.printservice.PrintService" /> + * </intent-filter> + * . . . + * </service> + * </pre> + * <h3>Configuration</h3> + * <p> + * A print service can be configured by specifying an optional settings + * activity which exposes service specific options, an optional add + * prints activity which is used for manual addition of printers, etc. + * It is a responsibility of the system to launch the settings and add + * printers activities when appropriate. + * </p> + * <p> + * A print service is configured by providing a + * {@link #SERVICE_META_DATA meta-data} entry in the manifest when declaring + * the service. A service declaration with a meta-data tag is presented + * below: + * <pre> <service android:name=".MyPrintService" + * android:permission="android.permission.BIND_PRINT_SERVICE"> + * <intent-filter> + * <action android:name="android.printservice.PrintService" /> + * </intent-filter> + * <meta-data android:name="android.printservice" android:resource="@xml/printservice" /> + * </service></pre> + * </p> + * <p> + * For more details refer to {@link #SERVICE_META_DATA} and + * <code><{@link android.R.styleable#PrintService print-service}></code>. + * </p> + */ +public abstract class PrintService extends Service { + + private static final String LOG_TAG = PrintService.class.getSimpleName(); + + /** + * The {@link Intent} action that must be declared as handled by a service + * in its manifest to allow the system to recognize it as a print service. + */ + public static final String SERVICE_INTERFACE = "android.printservice.PrintService"; + + /** + * Name under which a PrintService component publishes additional information + * about itself. This meta-data must reference an XML resource containing a + * <code><{@link android.R.styleable#PrintService print-service}></code> + * tag. This is a a sample XML file configuring a print service: + * <pre> <print-service + * android:settingsActivity="foo.bar.MySettingsActivity" + * andorid:addPrintersActivity="foo.bar.MyAddPrintersActivity." + * . . . + * /></pre> + */ + public static final String SERVICE_META_DATA = "android.printservice"; + + private final Object mLock = new Object(); + + private Handler mHandler; + + private IPrintServiceClient mClient; + + private boolean mDiscoveringPrinters; + + @Override + protected void attachBaseContext(Context base) { + super.attachBaseContext(base); + mHandler = new MyHandler(base.getMainLooper()); + } + + /** + * The system has connected to this service. + */ + protected void onConnected() { + /* do nothing */ + } + + /** + * The system has disconnected from this service. + */ + protected void onDisconnected() { + /* do nothing */ + } + + /** + * Callback requesting from this service to start printer discovery. + * At the end of the printer discovery period the system will call + * {@link #onStopPrinterDiscovery(). Discovered printers should be + * reported by calling #addDiscoveredPrinters(List) and reported ones + * that disappear should be reported by calling + * {@link #removeDiscoveredPrinters(List)}. + * + * @see #onStopPrinterDiscovery() + * @see #addDiscoveredPrinters(List) + * @see #removeDiscoveredPrinters(List) + */ + protected abstract void onStartPrinterDiscovery(); + + /** + * Callback requesting from this service to stop printer discovery. + * + * @see #onStartPrinterDiscovery() + * @see #addDiscoveredPrinters(List) + * @see #removeDiscoveredPrinters(List) + */ + protected abstract void onStopPrinterDiscovery(); + + /** + * Adds discovered printers. This method should be called during a + * printer discovery period, i.e. after a call to + * {@link #onStartPrinterDiscovery()} and before the corresponding + * call to {@link #onStopPrinterDiscovery()}, otherwise it does nothing. + * <p> + * <strong>Note:</strong> For every printer discovery period all + * printers have to be added. You can call this method as many times as + * necessary during the discovery period but should not pass in already + * added printers. If a printer is already added in the same printer + * discovery period, it will be ignored. + * </p> + * + * @param printers A list with discovered printers. + * + * @throws IllegalStateException If this service is not connected. + * + * @see #removeDiscoveredPrinters(List) + * @see #onStartPrinterDiscovery() + * @see #onStopPrinterDiscovery() + */ + public final void addDiscoveredPrinters(List<PrinterInfo> printers) { + synchronized (mLock) { + if (mClient == null) { + throw new IllegalStateException("Print serivice not connected!"); + } + if (mDiscoveringPrinters) { + try { + // Calling with a lock into the system is fine. + mClient.addDiscoveredPrinters(printers); + } catch (RemoteException re) { + Log.e(LOG_TAG, "Error adding discovered printers!", re); + } + } + } + } + + /** + * Removes discovered printers given their ids. This method should be called + * during a printer discovery period, i.e. after a call to + * {@link #onStartPrinterDiscovery()} and before the corresponding + * call to {@link #onStopPrinterDiscovery()}, otherwise it does nothing. + * <p> + * For every printer discovery period all printers have to be added. You + * should remove only printers that were added in this printer discovery + * period by a call to {@link #addDiscoveredPrinters(List)}. You can call + * this method as many times as necessary during the discovery period + * but should not pass in already removed printer ids. If a printer with + * a given id is already removed in the same discovery period, it will + * be ignored. + * </p> + * + * @param printerIds A list with disappeared printer ids. + * + * @throws IllegalStateException If this service is not connected. + * + * @see #addDiscoveredPrinters(List) + * @see #onStartPrinterDiscovery() + * @see #onStopPrinterDiscovery() + */ + public final void removeDiscoveredPrinters(List<PrinterId> printerIds) { + synchronized (mLock) { + if (mClient == null) { + throw new IllegalStateException("Print serivice not connected!"); + } + if (mDiscoveringPrinters) { + try { + // Calling with a lock into the system is fine. + mClient.removeDiscoveredPrinters(printerIds); + } catch (RemoteException re) { + Log.e(LOG_TAG, "Error removing discovered printers!", re); + } + } + } + } + + /** + * Called when canceling of a print job is requested. The service + * should do best effort to fulfill the request. After the print + * job is canceled it state has to be set to + * {@link PrintJobInfo#STATE_CANCELED}. + * + * @param printJob The print job to be canceled. + */ + protected void onRequestCancelPrintJob(PrintJob printJob) { + } + + /** + * Called when there is a queued print job for one of the printers + * managed by this print service. A queued print job is ready for + * processing by a print service which can get the data to be printed + * by calling {@link PrintJob#getData()}. This service may start + * processing the passed in print job or schedule handling of queued + * print jobs at a convenient time. The service can get the print + * jobs by a call to {@link #getPrintJobs()} and examine their state + * to find the ones with state {@link PrintJobInfo#STATE_QUEUED}. + * + * @param printJob The new queued print job. + * + * @see #getPrintJobs() + */ + protected abstract void onPrintJobQueued(PrintJob printJob); + + /** + * Gets the print jobs for the printers managed by this service. + * + * @return The print jobs. + * + * @throws IllegalStateException If this service is not connected. + */ + public final List<PrintJob> getPrintJobs() { + synchronized (mLock) { + if (mClient == null) { + throw new IllegalStateException("Print serivice not connected!"); + } + try { + List<PrintJob> printJobs = null; + List<PrintJobInfo> printJobInfos = mClient.getPrintJobs(); + if (printJobInfos != null) { + final int printJobInfoCount = printJobInfos.size(); + printJobs = new ArrayList<PrintJob>(printJobInfoCount); + for (int i = 0; i < printJobInfoCount; i++) { + printJobs.add(new PrintJob(printJobInfos.get(i), mClient)); + } + } + if (printJobs != null) { + return printJobs; + } + } catch (RemoteException re) { + Log.e(LOG_TAG, "Error calling getPrintJobs()", re); + } + return Collections.emptyList(); + } + } + + /** + * Generates a global printer id from a local id. The local id is unique + * only within this print service. + * + * @param localId The local id. + * @return Global printer id. + */ + public final PrinterId generatePrinterId(String localId) { + return new PrinterId(new ComponentName(getPackageName(), + getClass().getName()), localId); + } + + @Override + public final IBinder onBind(Intent intent) { + return new IPrintService.Stub() { + @Override + public void setClient(IPrintServiceClient client) { + mHandler.obtainMessage(MyHandler.MESSAGE_SET_CLEINT, client).sendToTarget(); + } + + @Override + public void startPrinterDiscovery() { + mHandler.sendEmptyMessage(MyHandler.MESSAGE_START_PRINTER_DISCOVERY); + } + + @Override + public void stopPrinterDiscovery() { + mHandler.sendEmptyMessage(MyHandler.MESSAGE_STOP_PRINTER_DISCOVERY); + } + + @Override + public void requestCancelPrintJob(PrintJobInfo printJob) { + mHandler.obtainMessage(MyHandler.MESSAGE_CANCEL_PRINTJOB, printJob).sendToTarget(); + } + + @Override + public void onPrintJobQueued(PrintJobInfo printJob) { + mHandler.obtainMessage(MyHandler.MESSAGE_ON_PRINTJOB_QUEUED, + printJob).sendToTarget(); + } + }; + } + + private final class MyHandler extends Handler { + public static final int MESSAGE_START_PRINTER_DISCOVERY = 1; + public static final int MESSAGE_STOP_PRINTER_DISCOVERY = 2; + public static final int MESSAGE_CANCEL_PRINTJOB = 3; + public static final int MESSAGE_ON_PRINTJOB_QUEUED = 4; + public static final int MESSAGE_SET_CLEINT = 5; + + public MyHandler(Looper looper) { + super(looper, null, true); + } + + @Override + public void handleMessage(Message message) { + final int action = message.what; + switch (action) { + case MESSAGE_START_PRINTER_DISCOVERY: { + synchronized (mLock) { + mDiscoveringPrinters = true; + } + onStartPrinterDiscovery(); + } break; + + case MESSAGE_STOP_PRINTER_DISCOVERY: { + synchronized (mLock) { + mDiscoveringPrinters = false; + } + onStopPrinterDiscovery(); + } break; + + case MESSAGE_CANCEL_PRINTJOB: { + PrintJobInfo printJob = (PrintJobInfo) message.obj; + onRequestCancelPrintJob(new PrintJob(printJob, mClient)); + } break; + + case MESSAGE_ON_PRINTJOB_QUEUED: { + PrintJobInfo printJob = (PrintJobInfo) message.obj; + onPrintJobQueued(new PrintJob(printJob, mClient)); + } break; + + case MESSAGE_SET_CLEINT: { + IPrintServiceClient client = (IPrintServiceClient) message.obj; + synchronized (mLock) { + mClient = client; + if (client == null) { + mDiscoveringPrinters = false; + } + } + if (client != null) { + onConnected(); + } else { + onStopPrinterDiscovery(); + onDisconnected(); + } + } break; + + default: { + throw new IllegalArgumentException("Unknown message: " + action); + } + } + } + } +} diff --git a/core/java/android/printservice/PrintServiceInfo.aidl b/core/java/android/printservice/PrintServiceInfo.aidl new file mode 100644 index 0000000..95b67f2 --- /dev/null +++ b/core/java/android/printservice/PrintServiceInfo.aidl @@ -0,0 +1,19 @@ +/** + * Copyright (c) 2013, 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.printservice; + +parcelable PrintServiceInfo; diff --git a/core/java/android/printservice/PrintServiceInfo.java b/core/java/android/printservice/PrintServiceInfo.java new file mode 100644 index 0000000..0370a25 --- /dev/null +++ b/core/java/android/printservice/PrintServiceInfo.java @@ -0,0 +1,267 @@ +/* + * Copyright (C) 2013 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.printservice; + +import android.content.ComponentName; +import android.content.Context; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.content.pm.ResolveInfo; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.content.res.XmlResourceParser; +import android.os.Parcel; +import android.os.Parcelable; +import android.util.AttributeSet; +import android.util.Log; +import android.util.Xml; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.IOException; + +/** + * This class describes a {@link PrintService}. A print service knows + * how to communicate with one or more printers over one or more protocols + * and exposes printers for use by the applications via the platform print + * APIs. + * + * @see PrintService + * @see android.print.PrintManager + * + * @hide + */ +public final class PrintServiceInfo implements Parcelable { + + private static final boolean DEBUG = false; + + private static final String LOG_TAG = PrintServiceInfo.class.getSimpleName(); + + private static final String TAG_PRINT_SERVICE = "print-service"; + + private final String mId; + + private final ResolveInfo mResolveInfo; + + private final String mSettingsActivityName; + + private final String mAddPrintersActivityName; + + /** + * Creates a new instance. + * + * @hide + */ + public PrintServiceInfo(Parcel parcel) { + mId = parcel.readString(); + mResolveInfo = parcel.readParcelable(null); + mSettingsActivityName = parcel.readString(); + mAddPrintersActivityName = parcel.readString(); + } + + /** + * Creates a new instance. + * + * @param resolveInfo The service resolve info. + * @param settingsActivityName Optional settings activity name. + * @param addPrintersActivityName Optional add printers activity name. + */ + public PrintServiceInfo(ResolveInfo resolveInfo, String settingsActivityName, + String addPrintersActivityName) { + mId = new ComponentName(resolveInfo.serviceInfo.packageName, + resolveInfo.serviceInfo.name).flattenToString(); + mResolveInfo = resolveInfo; + mSettingsActivityName = settingsActivityName; + mAddPrintersActivityName = addPrintersActivityName; + } + + /** + * Creates a new instance. + * + * @param resolveInfo The service resolve info. + * @param context Context for accessing resources. + * @throws XmlPullParserException If a XML parsing error occurs. + * @throws IOException If a I/O error occurs. + * @hide + */ + public static PrintServiceInfo create(ResolveInfo resolveInfo, Context context) { + String settingsActivityName = null; + String addPrintersActivityName = null; + + XmlResourceParser parser = null; + PackageManager packageManager = context.getPackageManager(); + parser = resolveInfo.serviceInfo.loadXmlMetaData(packageManager, + PrintService.SERVICE_META_DATA); + if (parser != null) { + try { + int type = 0; + while (type != XmlPullParser.END_DOCUMENT && type != XmlPullParser.START_TAG) { + type = parser.next(); + } + + String nodeName = parser.getName(); + if (!TAG_PRINT_SERVICE.equals(nodeName)) { + throw new XmlPullParserException( + "Meta-data does not start with" + TAG_PRINT_SERVICE + " tag"); + } + + Resources resources = packageManager.getResourcesForApplication( + resolveInfo.serviceInfo.applicationInfo); + AttributeSet allAttributes = Xml.asAttributeSet(parser); + TypedArray attributes = resources.obtainAttributes(allAttributes, + com.android.internal.R.styleable.PrintService); + + settingsActivityName = attributes.getString( + com.android.internal.R.styleable.PrintService_settingsActivity); + + addPrintersActivityName = attributes.getString( + com.android.internal.R.styleable.PrintService_addPrintersActivity); + + attributes.recycle(); + } catch (IOException ioe) { + Log.w(LOG_TAG, "Error reading meta-data:" + ioe); + } catch (XmlPullParserException xppe) { + Log.w(LOG_TAG, "Error reading meta-data:" + xppe); + } catch (NameNotFoundException e) { + Log.e(LOG_TAG, "Unable to load resources for: " + + resolveInfo.serviceInfo.packageName); + } finally { + if (parser != null) { + parser.close(); + } + } + } + + return new PrintServiceInfo(resolveInfo, settingsActivityName, addPrintersActivityName); + } + + /** + * The accessibility service id. + * <p> + * <strong>Generated by the system.</strong> + * </p> + * + * @return The id. + */ + public String getId() { + return mId; + } + + /** + * The service {@link ResolveInfo}. + * + * @return The info. + */ + public ResolveInfo getResolveInfo() { + return mResolveInfo; + } + + /** + * The settings activity name. + * <p> + * <strong>Statically set from + * {@link PrintService#SERVICE_META_DATA meta-data}.</strong> + * </p> + * + * @return The settings activity name. + */ + public String getSettingsActivityName() { + return mSettingsActivityName; + } + + /** + * The add printers activity name. + * <p> + * <strong>Statically set from + * {@link PrintService#SERVICE_META_DATA meta-data}.</strong> + * </p> + * + * @return The add printers activity name. + */ + public String getAddPrintersActivityName() { + return mAddPrintersActivityName; + } + + /** + * {@inheritDoc} + */ + public int describeContents() { + return 0; + } + + public void writeToParcel(Parcel parcel, int flagz) { + parcel.writeString(mId); + parcel.writeParcelable(mResolveInfo, 0); + parcel.writeString(mSettingsActivityName); + parcel.writeString(mAddPrintersActivityName); + } + + @Override + public int hashCode() { + return 31 * 1 + ((mId == null) ? 0 : mId.hashCode()); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + PrintServiceInfo other = (PrintServiceInfo) obj; + if (mId == null) { + if (other.mId != null) { + return false; + } + } else if (!mId.equals(other.mId)) { + return false; + } + return true; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("PrintServiceInfo{"); + builder.append("id:").append(mId).append(", "); + builder.append("resolveInfo:").append(mResolveInfo).append(", "); + if (DEBUG) { + builder.append("settingsActivityName:").append(mSettingsActivityName); + builder.append("addPrintersActivityName:").append(mAddPrintersActivityName); + } else if (mSettingsActivityName != null || mAddPrintersActivityName != null) { + builder.append("<has meta-data>"); + } + builder.append("}"); + return builder.toString(); + } + + public static final Parcelable.Creator<PrintServiceInfo> CREATOR = + new Parcelable.Creator<PrintServiceInfo>() { + public PrintServiceInfo createFromParcel(Parcel parcel) { + return new PrintServiceInfo(parcel); + } + + public PrintServiceInfo[] newArray(int size) { + return new PrintServiceInfo[size]; + } + }; +} diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 1d2fbd8..5dae599 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -3508,6 +3508,13 @@ public final class Settings { public static final String LONG_PRESS_TIMEOUT = "long_press_timeout"; /** + * List of the enabled print providers. + * @hide + */ + public static final String ENABLED_PRINT_SERVICES = + "enabled_print_services"; + + /** * Setting to always use the default text-to-speech settings regardless * of the application settings. * 1 = override application settings, diff --git a/core/java/android/util/TimedRemoteCaller.java b/core/java/android/util/TimedRemoteCaller.java new file mode 100644 index 0000000..abb2b64 --- /dev/null +++ b/core/java/android/util/TimedRemoteCaller.java @@ -0,0 +1,134 @@ +/* + * Copyright (C) 2013 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.util; + +import android.os.SystemClock; + +import java.util.concurrent.TimeoutException; + +/** + * This is a helper class for making an async one way call and + * its async one way response response in a sync fashion within + * a timeout. The key idea is to call the remote method with a + * sequence number and a callback and then starting to wait for + * the response. The remote method calls back with the result and + * the sequence number. If the response comes within the timeout + * and its sequence number is the one sent in the method invocation, + * then the call succeeded. If the response does not come within + * the timeout then the call failed. Older result received when + * waiting for the result are ignored. + * <p> + * Typical usage is: + * </p> + * <p><pre><code> + * public class MyMethodCaller extends TimeoutRemoteCallHelper<Object> { + * // The one way remote method to call. + * private final IRemoteInterface mTarget; + * + * // One way callback invoked when the remote method is done. + * private final IRemoteCallback mCallback = new IRemoteCallback.Stub() { + * public void onCompleted(Object result, int sequence) { + * onRemoteMethodResult(result, sequence); + * } + * }; + * + * public MyMethodCaller(IRemoteInterface target) { + * mTarget = target; + * } + * + * public Object onCallMyMethod(Object arg) throws RemoteException { + * final int sequence = onBeforeRemoteCall(); + * mTarget.myMethod(arg, sequence); + * return getResultTimed(sequence); + * } + * } + * </code></pre></p> + * + * @param <T> The type of the expected result. + * + * @hide + */ +public abstract class TimedRemoteCaller<T> { + + public static final long DEFAULT_CALL_TIMEOUT_MILLIS = 5000; + + private static final int UNDEFINED_SEQUENCE = -1; + + private final Object mLock = new Object(); + + private final long mCallTimeoutMillis; + + private int mSequenceCounter; + + private int mReceivedSequence = UNDEFINED_SEQUENCE; + + private int mAwaitedSequence = UNDEFINED_SEQUENCE; + + private T mResult; + + public TimedRemoteCaller(long callTimeoutMillis) { + mCallTimeoutMillis = callTimeoutMillis; + } + + public final int onBeforeRemoteCall() { + synchronized (mLock) { + mAwaitedSequence = mSequenceCounter++; + return mAwaitedSequence; + } + } + + public final T getResultTimed(int sequence) throws TimeoutException { + synchronized (mLock) { + final boolean success = waitForResultTimedLocked(sequence); + if (!success) { + throw new TimeoutException("No reponse for sequence: " + sequence); + } + T result = mResult; + mResult = null; + return result; + } + } + + public final void onRemoteMethodResult(T result, int sequence) { + synchronized (mLock) { + if (sequence == mAwaitedSequence) { + mReceivedSequence = sequence; + mResult = result; + mLock.notifyAll(); + } + } + } + + private boolean waitForResultTimedLocked(int sequence) { + final long startMillis = SystemClock.uptimeMillis(); + while (true) { + try { + if (mReceivedSequence == sequence) { + return true; + } + final long elapsedMillis = SystemClock.uptimeMillis() - startMillis; + final long waitMillis = mCallTimeoutMillis - elapsedMillis; + if (waitMillis <= 0) { + return false; + } + mLock.wait(waitMillis); + } catch (InterruptedException ie) { + /* ignore */ + } + } + } +} diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index df2aea8..ca274e3 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -1849,6 +1849,22 @@ android:description="@string/permdesc_bindAccessibilityService" android:protectionLevel="signature" /> + <!-- Must be required by an {@link android.printservice.PrintService}, + to ensure that only the system can bind to it. --> + <permission android:name="android.permission.BIND_PRINT_SERVICE" + android:label="@string/permlab_bindPrintService" + android:description="@string/permdesc_bindPrintService" + android:protectionLevel="signature" /> + + <!-- Allows an application to call APIs that give it access to all print jobs + on the device. Usually an app can access only the print jobts it created. + This permission is not available to third party applications. + @hide --> + <permission android:name="android.permission.ACCESS_ALL_PRINT_JOBS" + android:label="@string/permlab_accessAllPrintJobs" + android:description="@string/permdesc_accessAllPrintJobs" + android:protectionLevel="signature" /> + <!-- Must be required by a TextService (e.g. SpellCheckerService) to ensure that only the system can bind to it. --> <permission android:name="android.permission.BIND_TEXT_SERVICE" diff --git a/core/res/res/values/attrs.xml b/core/res/res/values/attrs.xml index 459a634..99f7e7a 100644 --- a/core/res/res/values/attrs.xml +++ b/core/res/res/values/attrs.xml @@ -2562,6 +2562,21 @@ <attr name="description" /> </declare-styleable> + <!-- Use <code>print-service</code> as the root tag of the XML resource that + describes an {@link android.printservice.PrintService} service, which is + referenced from its {@link android.printservice.PrintService#SERVICE_META_DATA} + meta-data entry. --> + <declare-styleable name="PrintService"> + <!-- Fully qualified class name of an activity that allows the user to modify + the settings for this service. --> + <attr name="settingsActivity" /> + <!-- Fully qualified class name of an activity that allows the user to manually + add printers to this print service. --> + <attr name="addPrintersActivity" format="string"/> + <!-- The vendor name if this print service is vendor specific. --> + <attr name="vendor" format="string"/> + </declare-styleable> + <declare-styleable name="ActionMenuItemView"> <attr name="minWidth" /> </declare-styleable> diff --git a/core/res/res/values/public.xml b/core/res/res/values/public.xml index 3858dcf..3ab8825 100644 --- a/core/res/res/values/public.xml +++ b/core/res/res/values/public.xml @@ -2066,5 +2066,7 @@ <public type="attr" name="ssp" /> <public type="attr" name="sspPrefix" /> <public type="attr" name="sspPattern" /> + <public type="attr" name="addPrintersActivity" /> + <public type="attr" name="vendor" /> </resources> diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml index 3e2d8d3..1938b88 100644 --- a/core/res/res/values/strings.xml +++ b/core/res/res/values/strings.xml @@ -970,6 +970,18 @@ interface of an accessibility service. Should never be needed for normal apps.</string> <!-- Title of an application permission, listed so the user can choose whether they want to allow the application to do this. --> + <string name="permlab_bindPrintService">bind to a print service</string> + <!-- Description of an application permission, listed so the user can choose whether they want to allow the application to do this. --> + <string name="permdesc_bindPrintService">Allows the holder to bind to the top-level + interface of a print service. Should never be needed for normal apps.</string> + + <!-- Title of an application permission, listed so the user can choose whether they want to allow the application to do this. --> + <string name="permlab_accessAllPrintJobs">access all print jobs</string> + <!-- Description of an application permission, listed so the user can choose whether they want to allow the application to do this. --> + <string name="permdesc_accessAllPrintJobs">Allows the holder to access print jobs + created by another app. Should never be needed for normal apps.</string> + + <!-- Title of an application permission, listed so the user can choose whether they want to allow the application to do this. --> <string name="permlab_bindTextService">bind to a text service</string> <!-- Description of an application permission, listed so the user can choose whether they want to allow the application to do this. --> <string name="permdesc_bindTextService">Allows the holder to bind to the top-level @@ -4130,4 +4142,89 @@ <!-- Message informing user that the requested activity could not be found [CHAR LIMIT=none] --> <string name="app_not_found">No application found to handle this action</string> <string name="revoke">Revoke</string> + + <!-- Printing --> + + <!-- ISO A0 media size: 33.11" × 46.81" --> + <string name="mediaSize_iso_a0">ISO A0</string> + <!-- ISO A1 media size: 23.39" × 33.11" --> + <string name="mediaSize_iso_a1">ISO A1</string> + <!-- ISO A2 media size: 16.54" x 23.39" --> + <string name="mediaSize_iso_a2">ISO A2</string> + <!-- ISO A3 media size: 11.69" x 16.54" --> + <string name="mediaSize_iso_a3">ISO A3</string> + <!-- ISO A4 media size: 8.27" x 11.69" --> + <string name="mediaSize_iso_a4">ISO A4</string> + <!-- ISO A5 media size: 5.83" x 8.27" --> + <string name="mediaSize_iso_a5">ISO A5</string> + <!-- ISO A6 media size: 4.13" x 5.83" --> + <string name="mediaSize_iso_a6">ISO A6</string> + <!-- ISO A7 media size: 2.91" x 4.13" --> + <string name="mediaSize_iso_a7">ISO A7</string> + <!-- ISO A8 media size: 2.05" x 2.91" --> + <string name="mediaSize_iso_a8">ISO A8</string> + <!-- ISO A9 media size: 1.46" x 2.05" --> + <string name="mediaSize_iso_a9">ISO A9</string> + <!-- ISO A10 media size: 1.02" x 1.46" --> + <string name="mediaSize_iso_a10">ISO A10</string> + + <!-- ISO B0 media size: 39.37" x 55.67" --> + <string name="mediaSize_iso_b0">ISO B0</string> + <!-- ISO B1 media size: 27.83" x 39.37" --> + <string name="mediaSize_iso_b1">ISO B1</string> + <!-- ISO B2 media size - 19.69" x 27.83" --> + <string name="mediaSize_iso_b2">ISO B2</string> + <!-- ISO B3 media size: 13.90" x 19.69" --> + <string name="mediaSize_iso_b3">ISO B3</string> + <!-- ISO B4 media size: 9.84" x 13.90" --> + <string name="mediaSize_iso_b4">ISO B4</string> + <!-- ISO B5 media size: 6.93" x 9.84" --> + <string name="mediaSize_iso_b5">ISO B5</string> + <!-- ISO B6 media size: 4.92" x 6.93" --> + <string name="mediaSize_iso_b6">ISO B6</string> + <!-- ISO B7 media size: 3.46" x 4.92" --> + <string name="mediaSize_iso_b7">ISO B7</string> + <!-- ISO B8 media size: 2.44" x 3.46" --> + <string name="mediaSize_iso_b8">ISO B8</string> + <!-- ISO B9 media size: 1.73" x 2.44" --> + <string name="mediaSize_iso_b9">ISO B9</string> + <!-- ISO B10 media size: 1.22" x 1.73" --> + <string name="mediaSize_iso_b10">ISO B10</string> + + <!-- ISO C0 media size: 36.10" x 51.06" --> + <string name="mediaSize_iso_c0">ISO C0</string> + <!-- ISO C1 media size: 25.51" x 36.10" --> + <string name="mediaSize_iso_c1">ISO C1</string> + <!-- ISO C2 media size: 18.03" x 25.51" --> + <string name="mediaSize_iso_c2">ISO C2</string> + <!-- ISO C3 media size: 12.76" x 18.03" --> + <string name="mediaSize_iso_c3">ISO C3</string> + <!-- ISO C4 media size: 9.02" x 12.76" --> + <string name="mediaSize_iso_c4">ISO C4</string> + <!-- ISO C5 media size: 6.38" x 9.02" --> + <string name="mediaSize_iso_c5">ISO C5</string> + <!-- ISO C6 media size: 4.49" x 6.38" --> + <string name="mediaSize_iso_c6">ISO C6</string> + <!-- ISO C7 media size: 3.19" x 4.49" --> + <string name="mediaSize_iso_c7">ISO C7</string> + <!-- ISO ISO C8 media size: 2.24" x 3.19" --> + <string name="mediaSize_iso_c8">ISO C8</string> + <!-- ISO ISO C9 media size: 1.57" x 2.24" --> + <string name="mediaSize_iso_c9">ISO C9</string> + <!-- ISO C10 media size: 1.10" x 1.57" --> + <string name="mediaSize_iso_c10">ISO C10</string> + + <!-- North America Letter media size: 8.5" × 11" --> + <string name="mediaSize_na_letter">Letter</string> + <!-- North America Government Letter media size: 8.0" × 10.5" --> + <string name="mediaSize_na_gvrnmt_letter">Government Letter</string> + <!-- North America Legal media size: 8.5" × 14" --> + <string name="mediaSize_na_legal">Legal</string> + <!-- North America Junior Legal media size: 8.0" × 5.0" --> + <string name="mediaSize_na_junior_legal">Junior Legal</string> + <!-- North America Ledger media size: 17" × 11" --> + <string name="mediaSize_na_ledger">Ledger</string> + <!-- North America Tabloid media size: 11" × 17" --> + <string name="mediaSize_na_tabloid">Tabloid</string> + </resources> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index e1d1e33..cb8d144 100755 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -858,6 +858,45 @@ <java-symbol type="string" name="action_bar_home_description_format" /> <java-symbol type="string" name="action_bar_home_subtitle_description_format" /> <java-symbol type="string" name="wireless_display_route_description" /> + <java-symbol type="string" name="mediaSize_iso_a0" /> + <java-symbol type="string" name="mediaSize_iso_a1" /> + <java-symbol type="string" name="mediaSize_iso_a2" /> + <java-symbol type="string" name="mediaSize_iso_a3" /> + <java-symbol type="string" name="mediaSize_iso_a4" /> + <java-symbol type="string" name="mediaSize_iso_a5" /> + <java-symbol type="string" name="mediaSize_iso_a6" /> + <java-symbol type="string" name="mediaSize_iso_a7" /> + <java-symbol type="string" name="mediaSize_iso_a8" /> + <java-symbol type="string" name="mediaSize_iso_a9" /> + <java-symbol type="string" name="mediaSize_iso_a10" /> + <java-symbol type="string" name="mediaSize_iso_b0" /> + <java-symbol type="string" name="mediaSize_iso_b1" /> + <java-symbol type="string" name="mediaSize_iso_b2" /> + <java-symbol type="string" name="mediaSize_iso_b3" /> + <java-symbol type="string" name="mediaSize_iso_b4" /> + <java-symbol type="string" name="mediaSize_iso_b5" /> + <java-symbol type="string" name="mediaSize_iso_b6" /> + <java-symbol type="string" name="mediaSize_iso_b7" /> + <java-symbol type="string" name="mediaSize_iso_b8" /> + <java-symbol type="string" name="mediaSize_iso_b9" /> + <java-symbol type="string" name="mediaSize_iso_b10" /> + <java-symbol type="string" name="mediaSize_iso_c0" /> + <java-symbol type="string" name="mediaSize_iso_c1" /> + <java-symbol type="string" name="mediaSize_iso_c2" /> + <java-symbol type="string" name="mediaSize_iso_c3" /> + <java-symbol type="string" name="mediaSize_iso_c4" /> + <java-symbol type="string" name="mediaSize_iso_c5" /> + <java-symbol type="string" name="mediaSize_iso_c6" /> + <java-symbol type="string" name="mediaSize_iso_c7" /> + <java-symbol type="string" name="mediaSize_iso_c8" /> + <java-symbol type="string" name="mediaSize_iso_c9" /> + <java-symbol type="string" name="mediaSize_iso_c10" /> + <java-symbol type="string" name="mediaSize_na_letter" /> + <java-symbol type="string" name="mediaSize_na_gvrnmt_letter" /> + <java-symbol type="string" name="mediaSize_na_legal" /> + <java-symbol type="string" name="mediaSize_na_junior_legal" /> + <java-symbol type="string" name="mediaSize_na_ledger" /> + <java-symbol type="string" name="mediaSize_na_tabloid" /> <java-symbol type="plurals" name="abbrev_in_num_days" /> <java-symbol type="plurals" name="abbrev_in_num_hours" /> diff --git a/packages/PrintSpooler/Android.mk b/packages/PrintSpooler/Android.mk new file mode 100644 index 0000000..a68fcdf --- /dev/null +++ b/packages/PrintSpooler/Android.mk @@ -0,0 +1,32 @@ +# Copyright (C) 2013 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. + +LOCAL_PATH:= $(call my-dir) + +include $(CLEAR_VARS) + +LOCAL_MODULE_TAGS := optional + +LOCAL_SRC_FILES := $(call all-java-files-under, src) + +LOCAL_PACKAGE_NAME := PrintSpooler + +LOCAL_JAVA_LIBRARIES := framework + +LOCAL_CERTIFICATE := platform + +LOCAL_PROGUARD_ENABLED := disabled + +include $(BUILD_PACKAGE) + diff --git a/packages/PrintSpooler/AndroidManifest.xml b/packages/PrintSpooler/AndroidManifest.xml new file mode 100644 index 0000000..fbb0060 --- /dev/null +++ b/packages/PrintSpooler/AndroidManifest.xml @@ -0,0 +1,54 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +/* + * Copyright (c) 2013 Google Inc. + * + * 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. + */ +--> +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.printspooler" + android:sharedUserId="android.uid.printspooler" + android:versionName="1" + android:versionCode="1" + coreApp="true"> + + <uses-sdk android:minSdkVersion="17" android:targetSdkVersion="17"/> + + <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/> + <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS_FULL"/> + + <permission android:name="android.permission.BIND_PRINT_SPOOLER_SERVICE" + android:label="@string/permlab_bindPrintSpoolerService" + android:description="@string/permdesc_bindPrintSpoolerService" + android:protectionLevel="signature" /> + + <application + android:allowClearUserData="false" + android:label="@string/app_label" + android:allowBackup= "false"> + + <service + android:name=".PrintSpoolerService" + android:exported="true" + android:permission="android.permission.BIND_PRINT_SPOOLER_SERVICE"> + </service> + + <activity + android:name=".PrintJobConfigActivity" + android:exported="true"> + </activity> + + </application> + +</manifest> diff --git a/packages/PrintSpooler/MODULE_LICENSE_APACHE2 b/packages/PrintSpooler/MODULE_LICENSE_APACHE2 new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/packages/PrintSpooler/MODULE_LICENSE_APACHE2 diff --git a/packages/PrintSpooler/NOTICE b/packages/PrintSpooler/NOTICE new file mode 100644 index 0000000..c5b1efa --- /dev/null +++ b/packages/PrintSpooler/NOTICE @@ -0,0 +1,190 @@ + + Copyright (c) 2005-2008, 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. + + 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. + + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + diff --git a/packages/PrintSpooler/res/layout/print_job_config_activity.xml b/packages/PrintSpooler/res/layout/print_job_config_activity.xml new file mode 100644 index 0000000..51e425d --- /dev/null +++ b/packages/PrintSpooler/res/layout/print_job_config_activity.xml @@ -0,0 +1,261 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2013 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. +--> + +<ScrollView xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="fill_parent" + android:layout_height="wrap_content"> + + <GridLayout + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center" + android:orientation="vertical" + android:columnCount="2"> + + <EditText + android:id="@+id/copies_edittext" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:layout_marginLeft="12dip" + android:layout_marginRight="12dip" + android:layout_row="0" + android:layout_column="1" + android:minWidth="150dip" + android:inputType="number" + android:selectAllOnFocus="true"> + </EditText> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginLeft="12dip" + android:layout_marginRight="12dip" + android:layout_row="0" + android:layout_column="0" + android:text="@string/label_copies" + android:textAppearance="?android:attr/textAppearanceMedium" + android:labelFor="@id/copies_edittext"> + </TextView> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginLeft="12dip" + android:layout_marginRight="12dip" + android:layout_row="1" + android:layout_column="0" + android:text="@string/label_destination" + android:textAppearance="?android:attr/textAppearanceMedium"> + </TextView> + + <Spinner + android:id="@+id/destination_spinner" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:layout_marginLeft="12dip" + android:layout_marginRight="12dip" + android:layout_row="1" + android:layout_column="1" + android:minWidth="150dip"> + </Spinner> + + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginLeft="12dip" + android:layout_marginRight="12dip" + android:layout_row="2" + android:layout_column="0" + android:text="@string/label_media_size" + android:textAppearance="?android:attr/textAppearanceMedium"> + </TextView> + + <Spinner + android:id="@+id/media_size_spinner" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:layout_marginLeft="12dip" + android:layout_marginRight="12dip" + android:layout_row="2" + android:layout_column="1" + android:minWidth="150dip"> + </Spinner> + + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginLeft="12dip" + android:layout_marginRight="12dip" + android:layout_row="3" + android:layout_column="0" + android:text="@string/label_resolution" + android:textAppearance="?android:attr/textAppearanceMedium"> + </TextView> + + <Spinner + android:id="@+id/resolution_spinner" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:layout_marginLeft="12dip" + android:layout_marginRight="12dip" + android:layout_row="3" + android:layout_column="1" + android:minWidth="150dip"> + </Spinner> + + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginLeft="12dip" + android:layout_marginRight="12dip" + android:layout_row="4" + android:layout_column="0" + android:text="@string/label_input_tray" + android:textAppearance="?android:attr/textAppearanceMedium"> + </TextView> + + <Spinner + android:id="@+id/input_tray_spinner" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:layout_marginLeft="12dip" + android:layout_marginRight="12dip" + android:layout_row="4" + android:layout_column="1" + android:minWidth="150dip"> + </Spinner> + + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginLeft="12dip" + android:layout_marginRight="12dip" + android:layout_row="5" + android:layout_column="0" + android:text="@string/label_output_tray" + android:textAppearance="?android:attr/textAppearanceMedium"> + </TextView> + + <Spinner + android:id="@+id/output_tray_spinner" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:layout_marginLeft="12dip" + android:layout_marginRight="12dip" + android:layout_row="5" + android:layout_column="1" + android:minWidth="150dip"> + </Spinner> + + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginLeft="12dip" + android:layout_marginRight="12dip" + android:layout_row="6" + android:layout_column="0" + android:text="@string/label_duplex_mode" + android:textAppearance="?android:attr/textAppearanceMedium"> + </TextView> + + <Spinner + android:id="@+id/duplex_mode_spinner" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:layout_marginLeft="12dip" + android:layout_marginRight="12dip" + android:layout_row="6" + android:layout_column="1" + android:minWidth="150dip"> + </Spinner> + + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginLeft="12dip" + android:layout_marginRight="12dip" + android:layout_row="7" + android:layout_column="0" + android:text="@string/label_color_mode" + android:textAppearance="?android:attr/textAppearanceMedium"> + </TextView> + + <Spinner + android:id="@+id/color_mode_spinner" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:layout_marginLeft="12dip" + android:layout_marginRight="12dip" + android:layout_row="7" + android:layout_column="1" + android:minWidth="150dip"> + </Spinner> + + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginLeft="12dip" + android:layout_marginRight="12dip" + android:layout_row="8" + android:layout_column="0" + android:text="@string/label_fitting_mode" + android:textAppearance="?android:attr/textAppearanceMedium"> + </TextView> + + <Spinner + android:id="@+id/fitting_mode_spinner" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:layout_marginLeft="12dip" + android:layout_marginRight="12dip" + android:layout_row="8" + android:layout_column="1" + android:minWidth="150dip"> + </Spinner> + + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginLeft="12dip" + android:layout_marginRight="12dip" + android:layout_row="9" + android:layout_column="0" + android:text="@string/label_orientation" + android:textAppearance="?android:attr/textAppearanceMedium"> + </TextView> + + <Spinner + android:id="@+id/orientation_spinner" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:layout_marginLeft="12dip" + android:layout_marginRight="12dip" + android:layout_row="9" + android:layout_column="1" + android:minWidth="150dip"> + </Spinner> + + </GridLayout> + +</ScrollView> diff --git a/packages/PrintSpooler/res/menu/print_job_config_activity.xml b/packages/PrintSpooler/res/menu/print_job_config_activity.xml new file mode 100644 index 0000000..149c274 --- /dev/null +++ b/packages/PrintSpooler/res/menu/print_job_config_activity.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2013 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. +--> +<menu xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:id="@+id/print_button" + android:title="@string/print_button" + android:showAsAction="ifRoom"> + </item> +</menu> diff --git a/packages/PrintSpooler/res/values/strings.xml b/packages/PrintSpooler/res/values/strings.xml new file mode 100644 index 0000000..8b4b40a --- /dev/null +++ b/packages/PrintSpooler/res/values/strings.xml @@ -0,0 +1,103 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2013 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. +--> + +<resources> + + <!-- Title of the PrintSpooler application. [CHAR LIMIT=16] --> + <string name="app_label">Print Spooler</string> + + <!-- Title of the print dialog. [CHAR LIMIT=10] --> + <string name="print_job_config_dialog_title">Print</string> + + <!-- Label of the print dialog's print button. [CHAR LIMIT=16] --> + <string name="print_button">Print</string> + + <!-- Label of the print dialog's cancel button. [CHAR LIMIT=16] --> + <string name="cancel_button">Cancel</string> + + <!-- Label of the destination spinner. [CHAR LIMIT=16] --> + <string name="label_destination">Destination</string> + + <!-- Label of the copies count edit text. [CHAR LIMIT=16] --> + <string name="label_copies">Copies</string> + + <!-- Label of the media size spinner. [CHAR LIMIT=16] --> + <string name="label_media_size">Media size</string> + + <!-- Label of the resolution spinner. [CHAR LIMIT=16] --> + <string name="label_resolution">Resolution</string> + + <!-- Label of the input tray spinner. [CHAR LIMIT=16] --> + <string name="label_input_tray">Input tray</string> + + <!-- Label of the output tray spinner. [CHAR LIMIT=16] --> + <string name="label_output_tray">Output tray</string> + + <!-- Label of the duplex mode spinner. [CHAR LIMIT=16] --> + <string name="label_duplex_mode">Duplex mode</string> + + <!-- Label of the color mode spinner. [CHAR LIMIT=16] --> + <string name="label_color_mode">Color mode</string> + + <!-- Label of the fitting mode spinner. [CHAR LIMIT=16] --> + <string name="label_fitting_mode">Fitting mode</string> + + <!-- Label of the orientation spinner. [CHAR LIMIT=16] --> + <string name="label_orientation">Orientation</string> + + <!-- Duplex mode labels. --> + <string-array name="duplex_mode_labels"> + <!-- Duplex mode label: No duplexing. [CHAR LIMIT=20] --> + <item>None</item> + <!-- Duplex mode label: Turn a page along its long edge, e.g. like a book. [CHAR LIMIT=20] --> + <item>Long edge</item> + <!-- Duplex mode label: Turn a page along its short edge, e.g. like a notepad. [CHAR LIMIT=20] --> + <item>Short edge</item> + </string-array> + + <!-- Color mode labels. --> + <string-array name="color_mode_labels"> + <!-- Color modelabel: Monochrome color scheme, e.g. one color is used. [CHAR LIMIT=20] --> + <item>Monochrome</item> + <!-- Color mode label: Color color scheme, e.g. many colors are used. [CHAR LIMIT=20] --> + <item>Color</item> + </string-array> + + <!-- Fitting mode labels. --> + <string-array name="fitting_mode_labels"> + <!-- Fitting mode label: No fitting. [CHAR LIMIT=30] --> + <item>None</item> + <!-- Fitting mode label: Fit the content to the page. [CHAR LIMIT=30] --> + <item>Fit to page</item> + </string-array> + + <!-- Orientation labels. --> + <string-array name="orientation_labels"> + <!-- Orientation label: Portrait page orientation. [CHAR LIMIT=30] --> + <item>Portrait</item> + <!-- Orientation label: Landscape page orientation [CHAR LIMIT=30] --> + <item>Landscape</item> + </string-array> + + <!-- Title of an application permission, listed so the user can choose + whether they want to allow the application to do this. --> + <string name="permlab_bindPrintSpoolerService">bind to a print spooler service</string> + <!-- Description of an application permission, listed so the user can + choose whether they want to allow the application to do this. --> + <string name="permdesc_bindPrintSpoolerService">Allows the holder to bind to the top-level + interface of a print spooler service. Should never be needed for normal apps.</string> + +</resources> diff --git a/packages/PrintSpooler/src/com/android/printspooler/PrintJobConfigActivity.java b/packages/PrintSpooler/src/com/android/printspooler/PrintJobConfigActivity.java new file mode 100644 index 0000000..ae2fe5c --- /dev/null +++ b/packages/PrintSpooler/src/com/android/printspooler/PrintJobConfigActivity.java @@ -0,0 +1,794 @@ +/* + * Copyright (C) 2013 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 com.android.printspooler; + +import android.app.Activity; +import android.content.Intent; +import android.net.Uri; +import android.os.AsyncTask; +import android.os.Binder; +import android.os.Bundle; +import android.os.Handler; +import android.os.IBinder; +import android.os.Looper; +import android.os.Message; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.os.UserHandle; +import android.os.IBinder.DeathRecipient; +import android.print.IPrintAdapter; +import android.print.IPrintManager; +import android.print.IPrinterDiscoveryObserver; +import android.print.PageRange; +import android.print.PrintAttributes; +import android.print.PrintAttributes.MediaSize; +import android.print.PrintAttributes.Resolution; +import android.print.PrintAttributes.Tray; +import android.print.PrintJobInfo; +import android.print.PrinterId; +import android.print.PrinterInfo; +import android.text.Editable; +import android.text.InputFilter; +import android.text.Spanned; +import android.text.TextUtils; +import android.text.TextWatcher; +import android.util.Log; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.WindowManager; +import android.widget.AdapterView; +import android.widget.AdapterView.OnItemSelectedListener; +import android.widget.ArrayAdapter; +import android.widget.EditText; +import android.widget.Spinner; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +/** + * Activity for configuring a print job. + */ +public class PrintJobConfigActivity extends Activity { + + private static final boolean DEBUG = false; + + private static final String LOG_TAG = PrintJobConfigActivity.class.getSimpleName(); + + public static final String EXTRA_PRINTABLE = "printable"; + public static final String EXTRA_APP_ID = "appId"; + public static final String EXTRA_ATTRIBUTES = "attributes"; + public static final String EXTRA_PRINT_JOB_ID = "printJobId"; + + private static final int MIN_COPIES = 1; + + private final List<QueuedAsyncTask<?>> mTaskQueue = new ArrayList<QueuedAsyncTask<?>>(); + + private IPrintManager mPrintManager; + + private IPrinterDiscoveryObserver mPrinterDiscoveryObserver; + + private int mAppId; + private int mPrintJobId; + + private PrintAttributes mPrintAttributes; + + private final PrintSpooler mPrintSpooler = PrintSpooler.getInstance(this); + + private RemotePrintAdapter mRemotePrintAdapter; + + // UI elements + + private EditText mCopiesEditText; + + private Spinner mDestinationSpinner; + public ArrayAdapter<SpinnerItem<PrinterInfo>> mDestinationSpinnerAdapter; + + private Spinner mMediaSizeSpinner; + public ArrayAdapter<SpinnerItem<MediaSize>> mMediaSizeSpinnerAdapter; + + private Spinner mResolutionSpinner; + public ArrayAdapter<SpinnerItem<Resolution>> mResolutionSpinnerAdapter; + + private Spinner mInputTraySpinner; + public ArrayAdapter<SpinnerItem<Tray>> mInputTraySpinnerAdapter; + + private Spinner mOutputTraySpinner; + public ArrayAdapter<SpinnerItem<Tray>> mOutputTraySpinnerAdapter; + + private Spinner mDuplexModeSpinner; + public ArrayAdapter<SpinnerItem<Integer>> mDuplexModeSpinnerAdapter; + + private Spinner mColorModeSpinner; + public ArrayAdapter<SpinnerItem<Integer>> mColorModeSpinnerAdapter; + + private Spinner mFittingModeSpinner; + public ArrayAdapter<SpinnerItem<Integer>> mFittingModeSpinnerAdapter; + + private Spinner mOrientationSpinner; + public ArrayAdapter<SpinnerItem<Integer>> mOrientationSpinnerAdapter; + + private boolean mPrintStarted; + + private boolean mPrintConfirmed; + + private IBinder mPrinable; + + // TODO: Implement store/restore state. + + private final OnItemSelectedListener mOnItemSelectedListener = + new AdapterView.OnItemSelectedListener() { + @Override + public void onItemSelected(AdapterView<?> spinner, View view, int position, long id) { + if (spinner == mDestinationSpinner) { + updateUi(); + notifyPrintableStartIfNeeded(); + } else if (spinner == mMediaSizeSpinner) { + SpinnerItem<MediaSize> mediaItem = mMediaSizeSpinnerAdapter.getItem(position); + mPrintAttributes.setMediaSize(mediaItem.value); + updatePrintableContentIfNeeded(); + } else if (spinner == mResolutionSpinner) { + SpinnerItem<Resolution> resolutionItem = + mResolutionSpinnerAdapter.getItem(position); + mPrintAttributes.setResolution(resolutionItem.value); + updatePrintableContentIfNeeded(); + } else if (spinner == mInputTraySpinner) { + SpinnerItem<Tray> inputTrayItem = + mInputTraySpinnerAdapter.getItem(position); + mPrintAttributes.setInputTray(inputTrayItem.value); + } else if (spinner == mOutputTraySpinner) { + SpinnerItem<Tray> outputTrayItem = + mOutputTraySpinnerAdapter.getItem(position); + mPrintAttributes.setOutputTray(outputTrayItem.value); + } else if (spinner == mDuplexModeSpinner) { + SpinnerItem<Integer> duplexModeItem = + mDuplexModeSpinnerAdapter.getItem(position); + mPrintAttributes.setDuplexMode(duplexModeItem.value); + } else if (spinner == mColorModeSpinner) { + SpinnerItem<Integer> colorModeItem = + mColorModeSpinnerAdapter.getItem(position); + mPrintAttributes.setColorMode(colorModeItem.value); + } else if (spinner == mFittingModeSpinner) { + SpinnerItem<Integer> fittingModeItem = + mFittingModeSpinnerAdapter.getItem(position); + mPrintAttributes.setFittingMode(fittingModeItem.value); + } else if (spinner == mOrientationSpinner) { + SpinnerItem<Integer> orientationItem = + mOrientationSpinnerAdapter.getItem(position); + mPrintAttributes.setOrientation(orientationItem.value); + } + } + + @Override + public void onNothingSelected(AdapterView<?> parent) { + /* do nothing*/ + } + }; + + private final TextWatcher mTextWatcher = new TextWatcher() { + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + final int copies = Integer.parseInt(mCopiesEditText.getText().toString()); + mPrintAttributes.setCopies(copies); + } + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + /* do nothing */ + } + + @Override + public void afterTextChanged(Editable s) { + /* do nothing */ + } + }; + + private final InputFilter mInputFilter = new InputFilter() { + @Override + public CharSequence filter(CharSequence source, int start, int end, + Spanned dest, int dstart, int dend) { + StringBuffer text = new StringBuffer(dest.toString()); + text.replace(dstart, dend, source.subSequence(start, end).toString()); + if (TextUtils.isEmpty(text)) { + return dest; + } + final int copies = Integer.parseInt(text.toString()); + if (copies < MIN_COPIES) { + return dest; + } + return null; + } + }; + + private final DeathRecipient mDeathRecipient = new DeathRecipient() { + @Override + public void binderDied() { + finish(); + } + }; + + @Override + protected void onCreate(Bundle bundle) { + super.onCreate(bundle); + setContentView(R.layout.print_job_config_activity); + + getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN + | WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN); + + mPrintManager = (IPrintManager) IPrintManager.Stub.asInterface( + ServiceManager.getService(PRINT_SERVICE)); + + Bundle extras = getIntent().getExtras(); + + mPrintJobId = extras.getInt(EXTRA_PRINT_JOB_ID, -1); + if (mPrintJobId < 0) { + throw new IllegalArgumentException("Invalid print job id: " + mPrintJobId); + } + + mAppId = extras.getInt(EXTRA_APP_ID, -1); + if (mAppId < 0) { + throw new IllegalArgumentException("Invalid app id: " + mAppId); + } + + mPrintAttributes = getIntent().getParcelableExtra(EXTRA_ATTRIBUTES); + if (mPrintAttributes == null) { + mPrintAttributes = new PrintAttributes.Builder().create(); + } + + mPrinable = extras.getBinder(EXTRA_PRINTABLE); + if (mPrinable == null) { + throw new IllegalArgumentException("Printable cannot be null"); + } + mRemotePrintAdapter = new RemotePrintAdapter(IPrintAdapter.Stub.asInterface(mPrinable), + mPrintSpooler.generateFileForPrintJob(mPrintJobId)); + + try { + mPrinable.linkToDeath(mDeathRecipient, 0); + } catch (RemoteException re) { + finish(); + } + + mPrinterDiscoveryObserver = new PrintDiscoveryObserver(getMainLooper()); + + bindUi(); + } + + @Override + protected void onDestroy() { + mPrinable.unlinkToDeath(mDeathRecipient, 0); + super.onDestroy(); + } + + private void bindUi() { + // Copies + mCopiesEditText = (EditText) findViewById(R.id.copies_edittext); + mCopiesEditText.setText(String.valueOf(MIN_COPIES)); + mCopiesEditText.addTextChangedListener(mTextWatcher); + mCopiesEditText.setFilters(new InputFilter[] {mInputFilter}); + + // Destination. + mDestinationSpinner = (Spinner) findViewById(R.id.destination_spinner); + mDestinationSpinnerAdapter = new ArrayAdapter<SpinnerItem<PrinterInfo>>(this, + android.R.layout.simple_spinner_dropdown_item); + mDestinationSpinner.setAdapter(mDestinationSpinnerAdapter); + mDestinationSpinner.setOnItemSelectedListener(mOnItemSelectedListener); + + // Media size. + mMediaSizeSpinner = (Spinner) findViewById(R.id.media_size_spinner); + mMediaSizeSpinnerAdapter = new ArrayAdapter<SpinnerItem<MediaSize>>(this, + android.R.layout.simple_spinner_dropdown_item); + mMediaSizeSpinner.setAdapter(mMediaSizeSpinnerAdapter); + mMediaSizeSpinner.setOnItemSelectedListener(mOnItemSelectedListener); + + // Resolution. + mResolutionSpinner = (Spinner) findViewById(R.id.resolution_spinner); + mResolutionSpinnerAdapter = new ArrayAdapter<SpinnerItem<Resolution>>(this, + android.R.layout.simple_spinner_dropdown_item); + mResolutionSpinner.setAdapter(mResolutionSpinnerAdapter); + mResolutionSpinner.setOnItemSelectedListener(mOnItemSelectedListener); + + // Input tray. + mInputTraySpinner = (Spinner) findViewById(R.id.input_tray_spinner); + mInputTraySpinnerAdapter = new ArrayAdapter<SpinnerItem<Tray>>(this, + android.R.layout.simple_spinner_dropdown_item); + mInputTraySpinner.setAdapter(mInputTraySpinnerAdapter); + + // Output tray. + mOutputTraySpinner = (Spinner) findViewById(R.id.output_tray_spinner); + mOutputTraySpinnerAdapter = new ArrayAdapter<SpinnerItem<Tray>>(this, + android.R.layout.simple_spinner_dropdown_item); + mOutputTraySpinner.setAdapter(mOutputTraySpinnerAdapter); + mOutputTraySpinner.setOnItemSelectedListener(mOnItemSelectedListener); + + // Duplex mode. + mDuplexModeSpinner = (Spinner) findViewById(R.id.duplex_mode_spinner); + mDuplexModeSpinnerAdapter = new ArrayAdapter<SpinnerItem<Integer>>(this, + android.R.layout.simple_spinner_dropdown_item); + mDuplexModeSpinner.setAdapter(mDuplexModeSpinnerAdapter); + mDuplexModeSpinner.setOnItemSelectedListener(mOnItemSelectedListener); + + // Color mode. + mColorModeSpinner = (Spinner) findViewById(R.id.color_mode_spinner); + mColorModeSpinnerAdapter = new ArrayAdapter<SpinnerItem<Integer>>(this, + android.R.layout.simple_spinner_dropdown_item); + mColorModeSpinner.setAdapter(mColorModeSpinnerAdapter); + mColorModeSpinner.setOnItemSelectedListener(mOnItemSelectedListener); + + // Color mode. + mFittingModeSpinner = (Spinner) findViewById(R.id.fitting_mode_spinner); + mFittingModeSpinnerAdapter = new ArrayAdapter<SpinnerItem<Integer>>(this, + android.R.layout.simple_spinner_dropdown_item); + mFittingModeSpinner.setAdapter(mFittingModeSpinnerAdapter); + mFittingModeSpinner.setOnItemSelectedListener(mOnItemSelectedListener); + + // Orientation + mOrientationSpinner = (Spinner) findViewById(R.id.orientation_spinner); + mOrientationSpinnerAdapter = new ArrayAdapter<SpinnerItem<Integer>>(this, + android.R.layout.simple_spinner_dropdown_item); + mOrientationSpinner.setAdapter(mOrientationSpinnerAdapter); + mOrientationSpinner.setOnItemSelectedListener(mOnItemSelectedListener); + } + + private void updateUi() { + final int selectedIndex = mDestinationSpinner.getSelectedItemPosition(); + PrinterInfo printer = mDestinationSpinnerAdapter.getItem(selectedIndex).value; + printer.getDefaults(mPrintAttributes); + + // Copies. + mCopiesEditText.setText(String.valueOf( + Math.max(mPrintAttributes.getCopies(), MIN_COPIES))); + + // Media size. + mMediaSizeSpinnerAdapter.clear(); + List<MediaSize> mediaSizes = printer.getMediaSizes(); + final int mediaSizeCount = mediaSizes.size(); + for (int i = 0; i < mediaSizeCount; i++) { + MediaSize mediaSize = mediaSizes.get(i); + mMediaSizeSpinnerAdapter.add(new SpinnerItem<MediaSize>( + mediaSize, mediaSize.getLabel(getPackageManager()))); + } + final int selectedMediaSizeIndex = mediaSizes.indexOf( + mPrintAttributes.getMediaSize()); + mMediaSizeSpinner.setOnItemSelectedListener(null); + mMediaSizeSpinner.setSelection(selectedMediaSizeIndex); + mMediaSizeSpinner.setOnItemSelectedListener(mOnItemSelectedListener); + + // Resolution. + mResolutionSpinnerAdapter.clear(); + List<Resolution> resolutions = printer.getResolutions(); + final int resolutionCount = resolutions.size(); + for (int i = 0; i < resolutionCount; i++) { + Resolution resolution = resolutions.get(i); + mResolutionSpinnerAdapter.add(new SpinnerItem<Resolution>( + resolution, resolution.getLabel(getPackageManager()))); + } + final int selectedResolutionIndex = resolutions.indexOf( + mPrintAttributes.getResolution()); + mResolutionSpinner.setOnItemSelectedListener(null); + mResolutionSpinner.setSelection(selectedResolutionIndex); + mResolutionSpinner.setOnItemSelectedListener(mOnItemSelectedListener); + + // Input tray. + mInputTraySpinnerAdapter.clear(); + List<Tray> inputTrays = printer.getInputTrays(); + final int inputTrayCount = inputTrays.size(); + for (int i = 0; i < inputTrayCount; i++) { + Tray inputTray = inputTrays.get(i); + mInputTraySpinnerAdapter.add(new SpinnerItem<Tray>( + inputTray, inputTray.getLabel(getPackageManager()))); + } + final int selectedInputTrayIndex = inputTrays.indexOf( + mPrintAttributes.getInputTray()); + mInputTraySpinner.setSelection(selectedInputTrayIndex); + + // Output tray. + mOutputTraySpinnerAdapter.clear(); + List<Tray> outputTrays = printer.getOutputTrays(); + final int outputTrayCount = outputTrays.size(); + for (int i = 0; i < outputTrayCount; i++) { + Tray outputTray = outputTrays.get(i); + mOutputTraySpinnerAdapter.add(new SpinnerItem<Tray>( + outputTray, outputTray.getLabel(getPackageManager()))); + } + final int selectedOutputTrayIndex = outputTrays.indexOf( + mPrintAttributes.getOutputTray()); + mOutputTraySpinner.setSelection(selectedOutputTrayIndex); + + // Duplex mode. + final int duplexModes = printer.getDuplexModes(); + mDuplexModeSpinnerAdapter.clear(); + String[] duplexModeLabels = getResources().getStringArray( + R.array.duplex_mode_labels); + int remainingDuplexModes = duplexModes; + while (remainingDuplexModes != 0) { + final int duplexBitOffset = Integer.numberOfTrailingZeros(remainingDuplexModes); + final int duplexMode = 1 << duplexBitOffset; + remainingDuplexModes &= ~duplexMode; + mDuplexModeSpinnerAdapter.add(new SpinnerItem<Integer>(duplexMode, + duplexModeLabels[duplexBitOffset])); + } + final int selectedDuplexModeIndex = Integer.numberOfTrailingZeros( + (duplexModes & mPrintAttributes.getDuplexMode())); + mDuplexModeSpinner.setSelection(selectedDuplexModeIndex); + + // Color mode. + final int colorModes = printer.getColorModes(); + mColorModeSpinnerAdapter.clear(); + String[] colorModeLabels = getResources().getStringArray( + R.array.color_mode_labels); + int remainingColorModes = colorModes; + while (remainingColorModes != 0) { + final int colorBitOffset = Integer.numberOfTrailingZeros(remainingColorModes); + final int colorMode = 1 << colorBitOffset; + remainingColorModes &= ~colorMode; + mColorModeSpinnerAdapter.add(new SpinnerItem<Integer>(colorMode, + colorModeLabels[colorBitOffset])); + } + final int selectedColorModeIndex = Integer.numberOfTrailingZeros( + (colorModes & mPrintAttributes.getColorMode())); + mColorModeSpinner.setSelection(selectedColorModeIndex); + + // Fitting mode. + final int fittingModes = printer.getFittingModes(); + mFittingModeSpinnerAdapter.clear(); + String[] fittingModeLabels = getResources().getStringArray( + R.array.fitting_mode_labels); + int remainingFittingModes = fittingModes; + while (remainingFittingModes != 0) { + final int fittingBitOffset = Integer.numberOfTrailingZeros(remainingFittingModes); + final int fittingMode = 1 << fittingBitOffset; + remainingFittingModes &= ~fittingMode; + mFittingModeSpinnerAdapter.add(new SpinnerItem<Integer>(fittingMode, + fittingModeLabels[fittingBitOffset])); + } + final int selectedFittingModeIndex = Integer.numberOfTrailingZeros( + (fittingModes & mPrintAttributes.getFittingMode())); + mFittingModeSpinner.setSelection(selectedFittingModeIndex); + + // Orientation. + final int orientations = printer.getOrientations(); + mOrientationSpinnerAdapter.clear(); + String[] orientationLabels = getResources().getStringArray( + R.array.orientation_labels); + int remainingOrientations = orientations; + while (remainingOrientations != 0) { + final int orientationBitOffset = Integer.numberOfTrailingZeros(remainingOrientations); + final int orientation = 1 << orientationBitOffset; + remainingOrientations &= ~orientation; + mOrientationSpinnerAdapter.add(new SpinnerItem<Integer>(orientation, + orientationLabels[orientationBitOffset])); + } + final int selectedOrientationIndex = Integer.numberOfTrailingZeros( + (orientations & mPrintAttributes.getOrientation())); + mOrientationSpinner.setSelection(selectedOrientationIndex); + } + + @Override + protected void onResume() { + super.onResume(); + try { + mPrintManager.startDiscoverPrinters(mPrinterDiscoveryObserver); + } catch (RemoteException re) { + Log.e(LOG_TAG, "Error starting printer discovery!", re); + } + notifyPrintableStartIfNeeded(); + } + + @Override + protected void onPause() { + super.onPause(); + try { + mPrintManager.stopDiscoverPrinters(); + } catch (RemoteException re) { + Log.e(LOG_TAG, "Error starting printer discovery!", re); + } + notifyPrintableFinishIfNeeded(); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.print_job_config_activity, menu); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if (item.getItemId() == R.id.print_button) { + mPrintConfirmed = true; + finish(); + } + return super.onOptionsItemSelected(item); + } + + private void notifyPrintableStartIfNeeded() { + if (mDestinationSpinner.getSelectedItemPosition() < 0 + || mPrintStarted) { + return; + } + mPrintStarted = true; + new QueuedAsyncTask<Void>(mTaskQueue) { + @Override + protected Void doInBackground(Void... params) { + try { + mRemotePrintAdapter.start(); + } catch (IOException ioe) { + Log.e(LOG_TAG, "Error reading printed data!", ioe); + } + return null; + } + + @Override + protected void onPostExecute(Void result) { + super.onPostExecute(result); + updatePrintableContentIfNeeded(); + } + }.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR, (Void[]) null); + } + + private void updatePrintableContentIfNeeded() { + if (!mPrintStarted) { + return; + } + + mPrintSpooler.setPrintJobAttributes(mPrintJobId, mPrintAttributes); + + // TODO: Implement page selector. + final List<PageRange> pages = new ArrayList<PageRange>(); + pages.add(PageRange.ALL_PAGES); + + new QueuedAsyncTask<File>(mTaskQueue) { + @Override + protected File doInBackground(Void... params) { + try { + mRemotePrintAdapter.printAttributesChanged(mPrintAttributes); + mRemotePrintAdapter.cancelPrint(); + mRemotePrintAdapter.print(pages); + return mRemotePrintAdapter.getFile(); + } catch (IOException ioe) { + Log.e(LOG_TAG, "Error reading printed data!", ioe); + } + return null; + } + + @Override + protected void onPostExecute(File file) { + super.onPostExecute(file); + updatePrintPreview(file); + } + }.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR, (Void[]) null); + } + + private void notifyPrintableFinishIfNeeded() { + if (!mPrintStarted) { + return; + } + mPrintStarted = false; + + // Cancel all pending async tasks if the activity was canceled. + if (!mPrintConfirmed) { + final int taskCount = mTaskQueue.size(); + for (int i = taskCount - 1; i >= 0; i--) { + mTaskQueue.remove(i).cancel(); + } + } + + new AsyncTask<Void, Void, Void>() { + @Override + protected Void doInBackground(Void... params) { + // Notify the app that printing completed. + try { + mRemotePrintAdapter.finish(); + } catch (IOException ioe) { + Log.e(LOG_TAG, "Error reading printed data!", ioe); + } + + // If canceled, nothing to do. + if (!mPrintConfirmed) { + mPrintSpooler.setPrintJobState(mPrintJobId, + PrintJobInfo.STATE_CANCELED); + return null; + } + + // No printer, nothing to do. + final int selectedIndex = mDestinationSpinner.getSelectedItemPosition(); + if (selectedIndex < 0) { + // Update the print job's status. + mPrintSpooler.setPrintJobState(mPrintJobId, + PrintJobInfo.STATE_CANCELED); + return null; + } + + // Update the print job's printer. + SpinnerItem<PrinterInfo> printerItem = + mDestinationSpinnerAdapter.getItem(selectedIndex); + PrinterId printerId = printerItem.value.getId(); + mPrintSpooler.setPrintJobPrinterId(mPrintJobId, printerId); + + // Update the print job's status. + mPrintSpooler.setPrintJobState(mPrintJobId, + PrintJobInfo.STATE_QUEUED); + return null; + } + + // Important: If we are canceling, then we do not wait for the write + // to complete since the result will be discarded anyway, we simply + // execute the finish immediately which will interrupt the write. + }.executeOnExecutor(mPrintConfirmed ? AsyncTask.SERIAL_EXECUTOR + : AsyncTask.THREAD_POOL_EXECUTOR, (Void[]) null); + + if (DEBUG) { + if (mPrintConfirmed) { + File file = mRemotePrintAdapter.getFile(); + if (file.exists()) { + new ViewSpooledFileAsyncTask(file).executeOnExecutor( + AsyncTask.SERIAL_EXECUTOR, (Void[]) null); + } + } + } + } + + private void updatePrintPreview(File file) { + // TODO: Implement + } + + private void addPrinters(List<PrinterInfo> addedPrinters) { + final int addedPrinterCount = addedPrinters.size(); + for (int i = 0; i < addedPrinterCount; i++) { + PrinterInfo addedPrinter = addedPrinters.get(i); + boolean duplicate = false; + final int existingPrinterCount = mDestinationSpinnerAdapter.getCount(); + for (int j = 0; j < existingPrinterCount; j++) { + PrinterInfo existingPrinter = mDestinationSpinnerAdapter.getItem(j).value; + if (addedPrinter.getId().equals(existingPrinter.getId())) { + duplicate = true; + break; + } + } + if (!duplicate) { + mDestinationSpinnerAdapter.add(new SpinnerItem<PrinterInfo>( + addedPrinter, addedPrinter.getLabel())); + } else { + Log.w(LOG_TAG, "Skipping a duplicate printer: " + addedPrinter); + } + } + } + + private void removePrinters(List<PrinterId> pritnerIds) { + final int printerIdCount = pritnerIds.size(); + for (int i = 0; i < printerIdCount; i++) { + PrinterId removedPrinterId = pritnerIds.get(i); + boolean removed = false; + final int existingPrinterCount = mDestinationSpinnerAdapter.getCount(); + for (int j = 0; j < existingPrinterCount; j++) { + PrinterInfo existingPrinter = mDestinationSpinnerAdapter.getItem(j).value; + if (removedPrinterId.equals(existingPrinter.getId())) { + mDestinationSpinnerAdapter.remove(mDestinationSpinnerAdapter.getItem(j)); + removed = true; + break; + } + } + if (!removed) { + Log.w(LOG_TAG, "Ignoring not added printer with id: " + removedPrinterId); + } + } + } + + private abstract class QueuedAsyncTask<T> extends AsyncTask<Void, Void, T> { + + private final List<QueuedAsyncTask<?>> mPendingOrRunningTasks; + + public QueuedAsyncTask(List<QueuedAsyncTask<?>> pendingOrRunningTasks) { + mPendingOrRunningTasks = pendingOrRunningTasks; + } + + @Override + protected void onPreExecute() { + mPendingOrRunningTasks.add(this); + } + + @Override + protected void onPostExecute(T result) { + mPendingOrRunningTasks.remove(this); + } + + public void cancel() { + super.cancel(true); + mPendingOrRunningTasks.remove(this); + } + } + + private final class ViewSpooledFileAsyncTask extends AsyncTask<Void, Void, Void> { + + private final File mFile; + + public ViewSpooledFileAsyncTask(File file) { + mFile = file; + } + + @Override + protected Void doInBackground(Void... params) { + mFile.setExecutable(true, false); + mFile.setWritable(true, false); + mFile.setReadable(true, false); + + final long identity = Binder.clearCallingIdentity(); + Intent intent = new Intent(Intent.ACTION_VIEW); + intent.setDataAndType(Uri.fromFile(mFile), "application/pdf"); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + startActivityAsUser(intent, null, UserHandle.CURRENT); + Binder.restoreCallingIdentity(identity); + return null; + } + } + + private final class PrintDiscoveryObserver extends IPrinterDiscoveryObserver.Stub { + private static final int MESSAGE_ADD_DICOVERED_PRINTERS = 1; + private static final int MESSAGE_REMOVE_DICOVERED_PRINTERS = 2; + + private final Handler mHandler; + + @SuppressWarnings("unchecked") + public PrintDiscoveryObserver(Looper looper) { + mHandler = new Handler(looper, null, true) { + @Override + public void handleMessage(Message message) { + switch (message.what) { + case MESSAGE_ADD_DICOVERED_PRINTERS: { + List<PrinterInfo> printers = (List<PrinterInfo>) message.obj; + addPrinters(printers); + // Just added the first printer, so select it and start printing. + if (mDestinationSpinnerAdapter.getCount() == 1) { + mDestinationSpinner.setSelection(0); + } + } break; + case MESSAGE_REMOVE_DICOVERED_PRINTERS: { + List<PrinterId> printerIds = (List<PrinterId>) message.obj; + removePrinters(printerIds); + // TODO: Handle removing the last printer. + } break; + } + } + }; + } + + @Override + public void addDiscoveredPrinters(List<PrinterInfo> printers) { + mHandler.obtainMessage(MESSAGE_ADD_DICOVERED_PRINTERS, printers).sendToTarget(); + } + + @Override + public void removeDiscoveredPrinters(List<PrinterId> printers) { + mHandler.obtainMessage(MESSAGE_REMOVE_DICOVERED_PRINTERS, printers).sendToTarget(); + } + } + + private final class SpinnerItem<T> { + final T value; + CharSequence label; + + public SpinnerItem(T value, CharSequence label) { + this.value = value; + this.label = label; + } + + public String toString() { + return label.toString(); + } + } +} diff --git a/packages/PrintSpooler/src/com/android/printspooler/PrintSpooler.java b/packages/PrintSpooler/src/com/android/printspooler/PrintSpooler.java new file mode 100644 index 0000000..2b27b69 --- /dev/null +++ b/packages/PrintSpooler/src/com/android/printspooler/PrintSpooler.java @@ -0,0 +1,734 @@ +/* + * Copyright (C) 2013 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 com.android.printspooler; + +import java.io.Closeable; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; +import org.xmlpull.v1.XmlSerializer; + +import android.content.ComponentName; +import android.content.Context; +import android.os.AsyncTask; +import android.os.ParcelFileDescriptor; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.print.IPrintClient; +import android.print.IPrintManager; +import android.print.PrintAttributes; +import android.print.PrintJobInfo; +import android.print.PrintManager; +import android.print.PrinterId; +import android.util.AtomicFile; +import android.util.Log; +import android.util.Slog; +import android.util.Xml; + +import com.android.internal.util.FastXmlSerializer; + +public class PrintSpooler { + + private static final String LOG_TAG = PrintSpooler.class.getSimpleName(); + + private static final boolean DEBUG_PRINT_JOB_LIFECYCLE = false; + + private static final boolean DEBUG_PERSISTENCE = false; + + private static final boolean PERSISTNECE_MANAGER_ENABLED = false; + + private static final String PRINT_FILE_EXTENSION = "pdf"; + + private static int sPrintJobIdCounter; + + private static final Object sLock = new Object(); + + private final Object mLock = new Object(); + + private static PrintSpooler sInstance; + + private final List<PrintJobInfo> mPrintJobs = new ArrayList<PrintJobInfo>(); + + private final PersistenceManager mPersistanceManager; + + private final Context mContext; + + private final IPrintManager mPrintManager; + + public static PrintSpooler getInstance(Context context) { + synchronized (sLock) { + if (sInstance == null) { + sInstance = new PrintSpooler(context); + } + return sInstance; + } + } + + private PrintSpooler(Context context) { + mContext = context; + mPersistanceManager = new PersistenceManager(); + mPersistanceManager.readStateLocked(); + mPrintManager = IPrintManager.Stub.asInterface( + ServiceManager.getService("print")); + } + + public List<PrintJobInfo> getPrintJobs(ComponentName componentName, int state, int appId) { + synchronized (mLock) { + List<PrintJobInfo> foundPrintJobs = null; + final int printJobCount = mPrintJobs.size(); + for (int i = 0; i < printJobCount; i++) { + PrintJobInfo printJob = mPrintJobs.get(i); + PrinterId printerId = printJob.getPrinterId(); + final boolean sameComponent = (componentName == null + || (printerId != null + && componentName.equals(printerId.getServiceComponentName()))); + final boolean sameAppId = appId == PrintManager.APP_ID_ANY + || printJob.getAppId() == appId; + final boolean sameState = state == PrintJobInfo.STATE_ANY + || state == printJob.getState(); + if (sameComponent && sameAppId && sameState) { + if (foundPrintJobs == null) { + foundPrintJobs = new ArrayList<PrintJobInfo>(); + } + foundPrintJobs.add(printJob); + } + } + return foundPrintJobs; + } + } + + public PrintJobInfo getPrintJob(int printJobId, int appId) { + synchronized (mLock) { + final int printJobCount = mPrintJobs.size(); + for (int i = 0; i < printJobCount; i++) { + PrintJobInfo printJob = mPrintJobs.get(i); + if (printJob.getId() == printJobId + && (appId == PrintManager.APP_ID_ANY || appId == printJob.getAppId())) { + return printJob; + } + } + return null; + } + } + + public boolean cancelPrintJob(int printJobId, int appId) { + synchronized (mLock) { + PrintJobInfo printJob = getPrintJob(printJobId, appId); + if (printJob != null) { + switch (printJob.getState()) { + case PrintJobInfo.STATE_CREATED: { + removePrintJobLocked(printJob); + } return true; + case PrintJobInfo.STATE_QUEUED: { + removePrintJobLocked(printJob); + } return true; + default: { + return false; + } + } + } + return false; + } + } + + public PrintJobInfo createPrintJob(CharSequence label, IPrintClient client, + PrintAttributes attributes, int appId) { + synchronized (mLock) { + final int printJobId = generatePrintJobIdLocked(); + PrintJobInfo printJob = new PrintJobInfo(); + printJob.setId(printJobId); + printJob.setAppId(appId); + printJob.setLabel(label); + printJob.setAttributes(attributes); + + addPrintJobLocked(printJob); + setPrintJobState(printJobId, PrintJobInfo.STATE_CREATED); + + return printJob; + } + } + + private int generatePrintJobIdLocked() { + int printJobId = sPrintJobIdCounter++; + while (isDuplicatePrintJobId(printJobId)) { + printJobId = sPrintJobIdCounter++; + } + return printJobId; + } + + private boolean isDuplicatePrintJobId(int printJobId) { + final int printJobCount = mPrintJobs.size(); + for (int j = 0; j < printJobCount; j++) { + PrintJobInfo printJob = mPrintJobs.get(j); + if (printJob.getId() == printJobId) { + return true; + } + } + return false; + } + + @SuppressWarnings("resource") + public boolean writePrintJobData(ParcelFileDescriptor fd, int printJobId) { + synchronized (mLock) { + FileInputStream in = null; + FileOutputStream out = null; + try { + PrintJobInfo printJob = getPrintJob(printJobId, PrintManager.APP_ID_ANY); + if (printJob != null) { + File file = generateFileForPrintJob(printJobId); + in = new FileInputStream(file); + out = new FileOutputStream(fd.getFileDescriptor()); + final byte[] buffer = new byte[8192]; + while (true) { + final int readByteCount = in.read(buffer); + if (readByteCount < 0) { + return true; + } + out.write(buffer, 0, readByteCount); + } + } + } catch (FileNotFoundException fnfe) { + Log.e(LOG_TAG, "Error writing print job data!", fnfe); + } catch (IOException ioe) { + Log.e(LOG_TAG, "Error writing print job data!", ioe); + } finally { + closeIfNotNullNoException(in); + closeIfNotNullNoException(out); + closeIfNotNullNoException(fd); + } + } + return false; + } + + private void closeIfNotNullNoException(Closeable closeable) { + if (closeable != null) { + try { + closeable.close(); + } catch (IOException ioe) { + /* ignore */; + } + } + } + + public File generateFileForPrintJob(int printJobId) { + return new File(mContext.getFilesDir(), "print_job_" + + printJobId + "." + PRINT_FILE_EXTENSION); + } + + private void addPrintJobLocked(PrintJobInfo printJob) { + mPrintJobs.add(printJob); + if (DEBUG_PRINT_JOB_LIFECYCLE) { + Slog.i(LOG_TAG, "[ADD] " + printJob); + } + } + + private void removePrintJobLocked(PrintJobInfo printJob) { + if (mPrintJobs.remove(printJob)) { + generateFileForPrintJob(printJob.getId()).delete(); + if (DEBUG_PRINT_JOB_LIFECYCLE) { + Slog.i(LOG_TAG, "[REMOVE] " + printJob); + } + } + } + + public boolean setPrintJobState(int printJobId, int state) { + boolean success = false; + PrintJobInfo queuedPrintJob = null; + + synchronized (mLock) { + PrintJobInfo printJob = getPrintJob(printJobId, PrintManager.APP_ID_ANY); + if (printJob != null && printJob.getState() < state) { + success = true; + printJob.setState(state); + // TODO: Update notifications. + switch (state) { + case PrintJobInfo.STATE_COMPLETED: + case PrintJobInfo.STATE_CANCELED: { + removePrintJobLocked(printJob); + } break; + case PrintJobInfo.STATE_QUEUED: { + queuedPrintJob = new PrintJobInfo(printJob); + } break; + } + if (DEBUG_PRINT_JOB_LIFECYCLE) { + Slog.i(LOG_TAG, "[STATUS CHANGED] " + printJob); + } + mPersistanceManager.writeStateLocked(); + } + } + + if (queuedPrintJob != null) { + try { + mPrintManager.onPrintJobQueued(queuedPrintJob.getPrinterId(), + queuedPrintJob); + } catch (RemoteException re) { + /* ignore */ + } + } + + return success; + } + + public boolean setPrintJobTag(int printJobId, String tag) { + synchronized (mLock) { + PrintJobInfo printJob = getPrintJob(printJobId, PrintManager.APP_ID_ANY); + if (printJob != null) { + printJob.setTag(tag); + mPersistanceManager.writeStateLocked(); + return true; + } + } + return false; + } + + public void setPrintJobAttributes(int printJobId, PrintAttributes attributes) { + synchronized (mLock) { + PrintJobInfo printJob = getPrintJob(printJobId, PrintManager.APP_ID_ANY); + if (printJob != null) { + printJob.setAttributes(attributes); + mPersistanceManager.writeStateLocked(); + } + } + } + + public void setPrintJobPrinterId(int printJobId, PrinterId printerId) { + synchronized (mLock) { + PrintJobInfo printJob = getPrintJob(printJobId, PrintManager.APP_ID_ANY); + if (printJob != null) { + printJob.setPrinterId(printerId); + mPersistanceManager.writeStateLocked(); + } + } + } + + private final class PersistenceManager { + private static final String PERSIST_FILE_NAME = "print_spooler_state.xml"; + + private static final String TAG_SPOOLER = "spooler"; + private static final String TAG_JOB = "job"; + private static final String TAG_ID = "id"; + private static final String TAG_TAG = "tag"; + private static final String TAG_APP_ID = "app-id"; + private static final String TAG_STATE = "state"; + private static final String TAG_ATTRIBUTES = "attributes"; + private static final String TAG_LABEL = "label"; + private static final String TAG_PRINTER = "printer"; + + private static final String ATTRIBUTE_MEDIA_SIZE = "mediaSize"; + private static final String ATTRIBUTE_RESOLUTION = "resolution"; + private static final String ATTRIBUTE_MARGINS = "margins"; + private static final String ATTRIBUTE_INPUT_TRAY = "inputTray"; + private static final String ATTRIBUTE_OUTPUT_TRAY = "outputTray"; + private static final String ATTRIBUTE_DUPLEX_MODE = "duplexMode"; + private static final String ATTRIBUTE_COLOR_MODE = "colorMode"; + private static final String ATTRIBUTE_FITTING_MODE = "fittingMode"; + private static final String ATTRIBUTE_ORIENTATION = "orientation"; + + private final AtomicFile mStatePersistFile; + + private boolean mWriteStateScheduled; + + private PersistenceManager() { + mStatePersistFile = new AtomicFile(new File(mContext.getFilesDir(), + PERSIST_FILE_NAME)); + } + + public void writeStateLocked() { + // TODO: Implement persistence of PrintableInfo + if (!PERSISTNECE_MANAGER_ENABLED) { + return; + } + if (mWriteStateScheduled) { + return; + } + mWriteStateScheduled = true; + new AsyncTask<Void, Void, Void>() { + @Override + protected Void doInBackground(Void... params) { + synchronized (mLock) { + mWriteStateScheduled = false; + doWriteStateLocked(); + } + return null; + } + }.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR, (Void[]) null); + } + + private void doWriteStateLocked() { + FileOutputStream out = null; + try { + out = mStatePersistFile.startWrite(); + + XmlSerializer serializer = new FastXmlSerializer(); + serializer.setOutput(out, "utf-8"); + serializer.startDocument(null, true); + serializer.startTag(null, TAG_SPOOLER); + + List<PrintJobInfo> printJobs = mPrintJobs; + + final int printJobCount = printJobs.size(); + for (int j = 0; j < printJobCount; j++) { + PrintJobInfo printJob = printJobs.get(j); + + final int state = printJob.getState(); + if (state < PrintJobInfo.STATE_QUEUED + || state > PrintJobInfo.STATE_FAILED) { + continue; + } + + serializer.startTag(null, TAG_JOB); + + serializer.startTag(null, TAG_ID); + serializer.text(String.valueOf(printJob.getId())); + serializer.endTag(null, TAG_ID); + + serializer.startTag(null, TAG_TAG); + serializer.text(printJob.getTag()); + serializer.endTag(null, TAG_TAG); + + serializer.startTag(null, TAG_APP_ID); + serializer.text(String.valueOf(printJob.getAppId())); + serializer.endTag(null, TAG_APP_ID); + + serializer.startTag(null, TAG_LABEL); + serializer.text(printJob.getLabel().toString()); + serializer.endTag(null, TAG_LABEL); + + serializer.startTag(null, TAG_STATE); + serializer.text(String.valueOf(printJob.getState())); + serializer.endTag(null, TAG_STATE); + + serializer.startTag(null, TAG_PRINTER); + serializer.text(printJob.getPrinterId().flattenToString()); + serializer.endTag(null, TAG_PRINTER); + + PrintAttributes attributes = printJob.getAttributes(); + if (attributes != null) { + serializer.startTag(null, TAG_ATTRIBUTES); + + //TODO: Implement persistence of the attributes below. + +// MediaSize mediaSize = attributes.getMediaSize(); +// if (mediaSize != null) { +// serializer.attribute(null, ATTRIBUTE_MEDIA_SIZE, +// mediaSize.flattenToString()); +// } +// +// Resolution resolution = attributes.getResolution(); +// if (resolution != null) { +// serializer.attribute(null, ATTRIBUTE_RESOLUTION, +// resolution.flattenToString()); +// } +// +// Margins margins = attributes.getMargins(); +// if (margins != null) { +// serializer.attribute(null, ATTRIBUTE_MARGINS, +// margins.flattenToString()); +// } +// +// Tray inputTray = attributes.getInputTray(); +// if (inputTray != null) { +// serializer.attribute(null, ATTRIBUTE_INPUT_TRAY, +// inputTray.flattenToString()); +// } +// +// Tray outputTray = attributes.getOutputTray(); +// if (outputTray != null) { +// serializer.attribute(null, ATTRIBUTE_OUTPUT_TRAY, +// outputTray.flattenToString()); +// } + + final int duplexMode = attributes.getDuplexMode(); + if (duplexMode > 0) { + serializer.attribute(null, ATTRIBUTE_DUPLEX_MODE, + String.valueOf(duplexMode)); + } + + final int colorMode = attributes.getColorMode(); + if (colorMode > 0) { + serializer.attribute(null, ATTRIBUTE_COLOR_MODE, + String.valueOf(colorMode)); + } + + final int fittingMode = attributes.getFittingMode(); + if (fittingMode > 0) { + serializer.attribute(null, ATTRIBUTE_FITTING_MODE, + String.valueOf(fittingMode)); + } + + final int orientation = attributes.getOrientation(); + if (orientation > 0) { + serializer.attribute(null, ATTRIBUTE_ORIENTATION, + String.valueOf(orientation)); + } + + serializer.endTag(null, TAG_ATTRIBUTES); + } + + serializer.endTag(null, TAG_JOB); + + if (DEBUG_PERSISTENCE) { + Log.i(LOG_TAG, "[PERSISTED] " + printJob); + } + } + + serializer.endTag(null, TAG_SPOOLER); + serializer.endDocument(); + mStatePersistFile.finishWrite(out); + } catch (IOException e) { + Slog.w(LOG_TAG, "Failed to write state, restoring backup.", e); + mStatePersistFile.failWrite(out); + } finally { + if (out != null) { + try { + out.close(); + } catch (IOException ioe) { + /* ignore */ + } + } + } + } + + public void readStateLocked() { + if (!PERSISTNECE_MANAGER_ENABLED) { + return; + } + FileInputStream in = null; + try { + in = mStatePersistFile.openRead(); + } catch (FileNotFoundException e) { + Log.i(LOG_TAG, "No existing print spooler state."); + return; + } + try { + XmlPullParser parser = Xml.newPullParser(); + parser.setInput(in, null); + parseState(parser); + } catch (IllegalStateException ise) { + Slog.w(LOG_TAG, "Failed parsing " + ise); + } catch (NullPointerException npe) { + Slog.w(LOG_TAG, "Failed parsing " + npe); + } catch (NumberFormatException nfe) { + Slog.w(LOG_TAG, "Failed parsing " + nfe); + } catch (XmlPullParserException xppe) { + Slog.w(LOG_TAG, "Failed parsing " + xppe); + } catch (IOException ioe) { + Slog.w(LOG_TAG, "Failed parsing " + ioe); + } catch (IndexOutOfBoundsException iobe) { + Slog.w(LOG_TAG, "Failed parsing " + iobe); + } finally { + try { + in.close(); + } catch (IOException ioe) { + /* ignore */ + } + } + } + + private void parseState(XmlPullParser parser) + throws IOException, XmlPullParserException { + parser.next(); + skipEmptyTextTags(parser); + expect(parser, XmlPullParser.START_TAG, TAG_SPOOLER); + parser.next(); + + while (parsePrintJob(parser)) { + parser.next(); + } + + skipEmptyTextTags(parser); + expect(parser, XmlPullParser.END_TAG, TAG_SPOOLER); + } + + private boolean parsePrintJob(XmlPullParser parser) + throws IOException, XmlPullParserException { + skipEmptyTextTags(parser); + if (!accept(parser, XmlPullParser.START_TAG, TAG_JOB)) { + return false; + } + parser.next(); + + skipEmptyTextTags(parser); + expect(parser, XmlPullParser.START_TAG, TAG_ID); + parser.next(); + final int printJobId = Integer.parseInt(parser.getText()); + parser.next(); + skipEmptyTextTags(parser); + expect(parser, XmlPullParser.END_TAG, TAG_ID); + parser.next(); + + skipEmptyTextTags(parser); + expect(parser, XmlPullParser.START_TAG, TAG_TAG); + parser.next(); + String tag = parser.getText(); + parser.next(); + skipEmptyTextTags(parser); + expect(parser, XmlPullParser.END_TAG, TAG_TAG); + parser.next(); + + skipEmptyTextTags(parser); + expect(parser, XmlPullParser.START_TAG, TAG_APP_ID); + parser.next(); + final int appId = Integer.parseInt(parser.getText()); + parser.next(); + skipEmptyTextTags(parser); + expect(parser, XmlPullParser.END_TAG, TAG_APP_ID); + parser.next(); + + skipEmptyTextTags(parser); + expect(parser, XmlPullParser.START_TAG, TAG_LABEL); + parser.next(); + String label = parser.getText(); + parser.next(); + skipEmptyTextTags(parser); + expect(parser, XmlPullParser.END_TAG, TAG_LABEL); + parser.next(); + + skipEmptyTextTags(parser); + expect(parser, XmlPullParser.START_TAG, TAG_STATE); + parser.next(); + final int state = Integer.parseInt(parser.getText()); + parser.next(); + skipEmptyTextTags(parser); + expect(parser, XmlPullParser.END_TAG, TAG_STATE); + parser.next(); + + skipEmptyTextTags(parser); + expect(parser, XmlPullParser.START_TAG, TAG_PRINTER); + parser.next(); + PrinterId printerId = PrinterId.unflattenFromString(parser.getText()); + parser.next(); + skipEmptyTextTags(parser); + expect(parser, XmlPullParser.END_TAG, TAG_PRINTER); + parser.next(); + + skipEmptyTextTags(parser); + expect(parser, XmlPullParser.START_TAG, TAG_ATTRIBUTES); + + final int attributeCount = parser.getAttributeCount(); + PrintAttributes attributes = null; + if (attributeCount > 0) { + PrintAttributes.Builder builder = new PrintAttributes.Builder(); + + // TODO: Implement reading of the attributes below. + +// String mediaSize = parser.getAttributeValue(null, ATTRIBUTE_MEDIA_SIZE); +// if (mediaSize != null) { +// builder.setMediaSize(MediaSize.unflattenFromString(mediaSize)); +// } +// +// String resolution = parser.getAttributeValue(null, ATTRIBUTE_RESOLUTION); +// if (resolution != null) { +// builder.setMediaSize(Resolution.unflattenFromString(resolution)); +// } +// +// String margins = parser.getAttributeValue(null, ATTRIBUTE_MARGINS); +// if (margins != null) { +// builder.setMediaSize(Margins.unflattenFromString(margins)); +// } +// +// String inputTray = parser.getAttributeValue(null, ATTRIBUTE_INPUT_TRAY); +// if (inputTray != null) { +// builder.setMediaSize(Tray.unflattenFromString(inputTray)); +// } +// +// String outputTray = parser.getAttributeValue(null, ATTRIBUTE_OUTPUT_TRAY); +// if (outputTray != null) { +// builder.setMediaSize(Tray.unflattenFromString(outputTray)); +// } +// +// String duplexMode = parser.getAttributeValue(null, ATTRIBUTE_DUPLEX_MODE); +// if (duplexMode != null) { +// builder.setDuplexMode(Integer.parseInt(duplexMode)); +// } + + String colorMode = parser.getAttributeValue(null, ATTRIBUTE_COLOR_MODE); + if (colorMode != null) { + builder.setColorMode(Integer.parseInt(colorMode)); + } + + String fittingMode = parser.getAttributeValue(null, ATTRIBUTE_COLOR_MODE); + if (fittingMode != null) { + builder.setFittingMode(Integer.parseInt(fittingMode)); + } + } + parser.next(); + skipEmptyTextTags(parser); + expect(parser, XmlPullParser.END_TAG, TAG_ATTRIBUTES); + parser.next(); + + PrintJobInfo printJob = new PrintJobInfo(); + printJob.setId(printJobId); + printJob.setTag(tag); + printJob.setAppId(appId); + printJob.setLabel(label); + printJob.setState(state); + printJob.setAttributes(attributes); + printJob.setPrinterId(printerId); + + mPrintJobs.add(printJob); + + if (DEBUG_PERSISTENCE) { + Log.i(LOG_TAG, "[RESTORED] " + printJob); + } + + skipEmptyTextTags(parser); + expect(parser, XmlPullParser.END_TAG, TAG_JOB); + + return true; + } + + private void expect(XmlPullParser parser, int type, String tag) + throws IOException, XmlPullParserException { + if (!accept(parser, type, tag)) { + throw new XmlPullParserException("Exepected event: " + type + + " and tag: " + tag + " but got event: " + parser.getEventType() + + " and tag:" + parser.getName()); + } + } + + private void skipEmptyTextTags(XmlPullParser parser) + throws IOException, XmlPullParserException { + while (accept(parser, XmlPullParser.TEXT, null) + && "\n".equals(parser.getText())) { + parser.next(); + } + } + + private boolean accept(XmlPullParser parser, int type, String tag) + throws IOException, XmlPullParserException { + if (parser.getEventType() != type) { + return false; + } + if (tag != null) { + if (!tag.equals(parser.getName())) { + return false; + } + } else if (parser.getName() != null) { + return false; + } + return true; + } + } +} diff --git a/packages/PrintSpooler/src/com/android/printspooler/PrintSpoolerService.java b/packages/PrintSpooler/src/com/android/printspooler/PrintSpoolerService.java new file mode 100644 index 0000000..57c4557 --- /dev/null +++ b/packages/PrintSpooler/src/com/android/printspooler/PrintSpoolerService.java @@ -0,0 +1,187 @@ +/* + * Copyright (C) 2013 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 com.android.printspooler; + +import java.util.List; + +import android.app.PendingIntent; +import android.app.Service; +import android.content.ComponentName; +import android.content.Intent; +import android.content.IntentSender; +import android.os.Handler; +import android.os.IBinder; +import android.os.Looper; +import android.os.Message; +import android.os.ParcelFileDescriptor; +import android.os.RemoteException; +import android.print.IPrintAdapter; +import android.print.IPrintClient; +import android.print.IPrintSpoolerService; +import android.print.IPrintSpoolerServiceCallbacks; +import android.print.PrintAttributes; +import android.print.PrintJobInfo; +import android.util.Slog; + +import com.android.internal.os.SomeArgs; + +/** + * Service for exposing some of the {@link PrintSpooler} functionality to + * another process. + */ +public final class PrintSpoolerService extends Service { + + private static final String LOG_TAG = "PrintSpoolerService"; + + private Intent mStartPrintJobConfigActivityIntent; + + private PrintSpooler mSpooler; + + private Handler mHanlder; + + @Override + public void onCreate() { + super.onCreate(); + mStartPrintJobConfigActivityIntent = new Intent(PrintSpoolerService.this, + PrintJobConfigActivity.class); + mSpooler = PrintSpooler.getInstance(this); + mHanlder = new MyHandler(getMainLooper()); + } + + @Override + public IBinder onBind(Intent intent) { + return new IPrintSpoolerService.Stub() { + @Override + public void getPrintJobs(IPrintSpoolerServiceCallbacks callback, + ComponentName componentName, int state, int appId, int sequence) + throws RemoteException { + List<PrintJobInfo> printJobs = null; + try { + printJobs = mSpooler.getPrintJobs(componentName, state, appId); + } finally { + callback.onGetPrintJobsResult(printJobs, sequence); + } + } + + @Override + public void getPrintJob(int printJobId, IPrintSpoolerServiceCallbacks callback, + int appId, int sequence) throws RemoteException { + PrintJobInfo printJob = null; + try { + printJob = mSpooler.getPrintJob(printJobId, appId); + } finally { + callback.onGetPrintJobInfoResult(printJob, sequence); + } + } + + @Override + public void cancelPrintJob(int printJobId, IPrintSpoolerServiceCallbacks callback, + int appId, int sequence) throws RemoteException { + boolean success = false; + try { + success = mSpooler.cancelPrintJob(printJobId, appId); + } finally { + callback.onCancelPrintJobResult(success, sequence); + } + } + + @SuppressWarnings("deprecation") + @Override + public void createPrintJob(String printJobName, IPrintClient client, + IPrintAdapter printAdapter, PrintAttributes attributes, + IPrintSpoolerServiceCallbacks callback, int appId, int sequence) + throws RemoteException { + PrintJobInfo printJob = null; + try { + printJob = mSpooler.createPrintJob(printJobName, client, + attributes, appId); + if (printJob != null) { + Intent intent = mStartPrintJobConfigActivityIntent; + intent.putExtra(PrintJobConfigActivity.EXTRA_PRINTABLE, + printAdapter.asBinder()); + intent.putExtra(PrintJobConfigActivity.EXTRA_APP_ID, appId); + intent.putExtra(PrintJobConfigActivity.EXTRA_PRINT_JOB_ID, + printJob.getId()); + intent.putExtra(PrintJobConfigActivity.EXTRA_ATTRIBUTES, attributes); + + IntentSender sender = PendingIntent.getActivity( + PrintSpoolerService.this, 0, intent, PendingIntent.FLAG_ONE_SHOT + | PendingIntent.FLAG_CANCEL_CURRENT).getIntentSender(); + + SomeArgs args = SomeArgs.obtain(); + args.arg1 = client; + args.arg2 = sender; + mHanlder.obtainMessage(0, args).sendToTarget(); + } + } finally { + callback.onCreatePrintJobResult(printJob, sequence); + } + } + + @Override + public void setPrintJobState(int printJobId, int state, + IPrintSpoolerServiceCallbacks callback, int sequece) + throws RemoteException { + boolean success = false; + try { + // TODO: Make sure the clients (print services) can set the state + // only to acceptable ones, e.g. not settings STATE_CREATED. + success = mSpooler.setPrintJobState(printJobId, state); + } finally { + callback.onSetPrintJobStateResult(success, sequece); + } + } + + @Override + public void setPrintJobTag(int printJobId, String tag, + IPrintSpoolerServiceCallbacks callback, int sequece) + throws RemoteException { + boolean success = false; + try { + success = mSpooler.setPrintJobTag(printJobId, tag); + } finally { + callback.onSetPrintJobTagResult(success, sequece); + } + } + + @Override + public void writePrintJobData(ParcelFileDescriptor fd, int printJobId) { + mSpooler.writePrintJobData(fd, printJobId); + } + }; + } + + private static final class MyHandler extends Handler { + + public MyHandler(Looper looper) { + super(looper, null, true); + } + + @Override + public void handleMessage(Message message) { + SomeArgs args = (SomeArgs) message.obj; + IPrintClient client = (IPrintClient) args.arg1; + IntentSender sender = (IntentSender) args.arg2; + args.recycle(); + try { + client.startPrintJobConfigActivity(sender); + } catch (RemoteException re) { + Slog.i(LOG_TAG, "Error starting print job config activity!", re); + } + } + } +} diff --git a/packages/PrintSpooler/src/com/android/printspooler/RemotePrintAdapter.java b/packages/PrintSpooler/src/com/android/printspooler/RemotePrintAdapter.java new file mode 100644 index 0000000..7537218 --- /dev/null +++ b/packages/PrintSpooler/src/com/android/printspooler/RemotePrintAdapter.java @@ -0,0 +1,219 @@ +/* + * Copyright (C) 2013 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 com.android.printspooler; + +import android.os.ICancellationSignal; +import android.os.ParcelFileDescriptor; +import android.os.RemoteException; +import android.print.IPrintAdapter; +import android.print.IPrintProgressListener; +import android.print.PageRange; +import android.print.PrintAdapterInfo; +import android.print.PrintAttributes; +import android.util.Log; + +import libcore.io.IoUtils; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.List; + +/** + * This class represents a remote print adapter instance. + */ +final class RemotePrintAdapter { + private static final String LOG_TAG = "RemotePrintAdapter"; + + private static final boolean DEBUG = true; + + private final Object mLock = new Object(); + + private final IPrintAdapter mRemoteInterface; + + private final File mFile; + + private final IPrintProgressListener mIPrintProgressListener; + + private PrintAdapterInfo mInfo; + + private ICancellationSignal mCancellationSignal; + + private Thread mWriteThread; + + public RemotePrintAdapter(IPrintAdapter printAdatper, File file) { + mRemoteInterface = printAdatper; + mFile = file; + mIPrintProgressListener = new IPrintProgressListener.Stub() { + @Override + public void onWriteStarted(PrintAdapterInfo info, + ICancellationSignal cancellationSignal) { + if (DEBUG) { + Log.i(LOG_TAG, "IPrintProgressListener#onWriteStarted()"); + } + synchronized (mLock) { + mInfo = info; + mCancellationSignal = cancellationSignal; + } + } + + @Override + public void onWriteFinished(List<PageRange> pages) { + if (DEBUG) { + Log.i(LOG_TAG, "IPrintProgressListener#onWriteFinished(" + pages + ")"); + } + synchronized (mLock) { + if (isPrintingLocked()) { + mWriteThread.interrupt(); + mCancellationSignal = null; + } + } + } + + @Override + public void onWriteFailed(CharSequence error) { + if (DEBUG) { + Log.i(LOG_TAG, "IPrintProgressListener#onWriteFailed(" + error + ")"); + } + synchronized (mLock) { + if (isPrintingLocked()) { + mWriteThread.interrupt(); + mCancellationSignal = null; + } + } + } + }; + } + + public File getFile() { + if (DEBUG) { + Log.i(LOG_TAG, "getFile()"); + } + return mFile; + } + + public void start() throws IOException { + if (DEBUG) { + Log.i(LOG_TAG, "start()"); + } + try { + mRemoteInterface.start(); + } catch (RemoteException re) { + throw new IOException("Error reading file", re); + } + } + + public void printAttributesChanged(PrintAttributes attributes) throws IOException { + if (DEBUG) { + Log.i(LOG_TAG, "printAttributesChanged(" + attributes +")"); + } + try { + mRemoteInterface.printAttributesChanged(attributes); + } catch (RemoteException re) { + throw new IOException("Error reading file", re); + } + } + + public void print(List<PageRange> pages) throws IOException { + if (DEBUG) { + Log.i(LOG_TAG, "print(" + pages +")"); + } + InputStream in = null; + OutputStream out = null; + ParcelFileDescriptor source = null; + ParcelFileDescriptor sink = null; + synchronized (mLock) { + mWriteThread = Thread.currentThread(); + } + try { + ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createPipe(); + source = pipe[0]; + sink = pipe[1]; + + in = new FileInputStream(source.getFileDescriptor()); + out = new FileOutputStream(mFile); + + // Async call to initiate the other process writing the data. + mRemoteInterface.print(pages, sink, mIPrintProgressListener); + + // Close the source. It is now held by the client. + sink.close(); + sink = null; + + final byte[] buffer = new byte[8192]; + while (true) { + if (Thread.currentThread().isInterrupted()) { + Thread.currentThread().interrupt(); + break; + } + final int readByteCount = in.read(buffer); + if (readByteCount < 0) { + break; + } + out.write(buffer, 0, readByteCount); + } + } catch (RemoteException re) { + throw new IOException("Error reading file", re); + } catch (IOException ioe) { + throw new IOException("Error reading file", ioe); + } finally { + IoUtils.closeQuietly(in); + IoUtils.closeQuietly(out); + IoUtils.closeQuietly(sink); + IoUtils.closeQuietly(source); + } + } + + public void cancelPrint() throws IOException { + if (DEBUG) { + Log.i(LOG_TAG, "cancelPrint()"); + } + synchronized (mLock) { + if (isPrintingLocked()) { + try { + mCancellationSignal.cancel(); + } catch (RemoteException re) { + throw new IOException("Error cancelling print", re); + } + } + } + } + + public void finish() throws IOException { + if (DEBUG) { + Log.i(LOG_TAG, "finish()"); + } + try { + mRemoteInterface.finish(); + } catch (RemoteException re) { + throw new IOException("Error reading file", re); + } + } + + public PrintAdapterInfo getInfo() { + synchronized (mLock) { + return mInfo; + } + } + + private boolean isPrintingLocked() { + return mCancellationSignal != null; + } +} diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java index 55f2a32..acfc096 100644 --- a/services/java/com/android/server/SystemServer.java +++ b/services/java/com/android/server/SystemServer.java @@ -63,6 +63,7 @@ import com.android.server.pm.PackageManagerService; import com.android.server.pm.UserManagerService; import com.android.server.power.PowerManagerService; import com.android.server.power.ShutdownThread; +import com.android.server.print.PrintManagerService; import com.android.server.search.SearchManagerService; import com.android.server.usb.UsbService; import com.android.server.wifi.WifiService; @@ -789,6 +790,14 @@ class ServerThread { } catch (Throwable e) { reportWtf("starting IdleMaintenanceService", e); } + + try { + Slog.i(TAG, "Print Service"); + ServiceManager.addService(Context.PRINT_SERVICE, + new PrintManagerService(context)); + } catch (Throwable e) { + reportWtf("starting Print Service", e); + } } // Before things start rolling, be sure we have decided whether diff --git a/services/java/com/android/server/print/PrintManagerService.java b/services/java/com/android/server/print/PrintManagerService.java new file mode 100644 index 0000000..5173998 --- /dev/null +++ b/services/java/com/android/server/print/PrintManagerService.java @@ -0,0 +1,789 @@ +/* + * Copyright (C) 2013 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 com.android.server.print; + +import android.Manifest; +import android.content.BroadcastReceiver; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.ServiceConnection; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.database.ContentObserver; +import android.net.Uri; +import android.os.Binder; +import android.os.Handler; +import android.os.IBinder; +import android.os.ParcelFileDescriptor; +import android.os.Process; +import android.os.RemoteException; +import android.os.UserHandle; +import android.print.IPrintAdapter; +import android.print.IPrintClient; +import android.print.IPrintManager; +import android.print.IPrinterDiscoveryObserver; +import android.print.PrintAttributes; +import android.print.PrintJobInfo; +import android.print.PrintManager; +import android.print.PrinterId; +import android.print.PrinterInfo; +import android.printservice.IPrintService; +import android.printservice.IPrintServiceClient; +import android.printservice.PrintServiceInfo; +import android.provider.Settings; +import android.text.TextUtils; +import android.text.TextUtils.SimpleStringSplitter; +import android.util.Slog; + +import com.android.internal.content.PackageMonitor; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Set; + +public final class PrintManagerService extends IPrintManager.Stub { + + private static final String LOG_TAG = PrintManagerService.class.getSimpleName(); + + private static final char COMPONENT_NAME_SEPARATOR = ':'; + + private final Object mLock = new Object(); + + private final SimpleStringSplitter mStringColonSplitter = + new SimpleStringSplitter(COMPONENT_NAME_SEPARATOR); + + private final Map<ComponentName, PrintServiceClient> mServices = + new HashMap<ComponentName, PrintServiceClient>(); + + private final List<PrintServiceInfo> mInstalledServices = new ArrayList<PrintServiceInfo>(); + + private final Set<ComponentName> mEnabledServiceNames = new HashSet<ComponentName>(); + + private final Context mContext; + + private final RemoteSpooler mSpooler; + + private final int mMyUid; + + private int mCurrentUserId = UserHandle.USER_OWNER; + + private IPrinterDiscoveryObserver mPrinterDiscoveryObserver; + + public PrintManagerService(Context context) { + mContext = context; + mSpooler = new RemoteSpooler(context); + mMyUid = android.os.Process.myUid(); + registerContentObservers(); + registerBoradcastreceivers(); + } + + @Override + public PrintJobInfo print(String printJobName, IPrintClient client, IPrintAdapter printAdapter, + PrintAttributes attributes, int appId, int userId) { + final int resolvedAppId = resolveCallingAppEnforcingPermissionsLocked(appId); + final int resolvedUserId = resolveCallingUserEnforcingPermissionsIdLocked(userId); + final long identity = Binder.clearCallingIdentity(); + try { + return mSpooler.createPrintJob(printJobName, client, printAdapter, + attributes, resolvedAppId, resolvedUserId); + } finally { + Binder.restoreCallingIdentity(identity); + } + } + + @Override + public List<PrintJobInfo> getPrintJobs(int appId, int userId) { + final int resolvedAppId = resolveCallingAppEnforcingPermissionsLocked(appId); + final int resolvedUserId = resolveCallingUserEnforcingPermissionsIdLocked(userId); + // TODO: Do we want to return jobs in STATE_CREATED? We should probably + // have additional argument for the types to get + final long identity = Binder.clearCallingIdentity(); + try { + return mSpooler.getPrintJobs(null, PrintJobInfo.STATE_ANY, + resolvedAppId, resolvedUserId); + } finally { + Binder.restoreCallingIdentity(identity); + } + } + + @Override + public PrintJobInfo getPrintJob(int printJobId, int appId, int userId) { + final int resolvedAppId = resolveCallingAppEnforcingPermissionsLocked(appId); + final int resolvedUserId = resolveCallingUserEnforcingPermissionsIdLocked(userId); + final long identity = Binder.clearCallingIdentity(); + try { + return mSpooler.getPrintJobInfo(printJobId, resolvedAppId, resolvedUserId); + } finally { + Binder.restoreCallingIdentity(identity); + } + } + + @Override + public void cancelPrintJob(int printJobId, int appId, int userId) { + final int resolvedAppId = resolveCallingAppEnforcingPermissionsLocked(appId); + final int resolvedUserId = resolveCallingUserEnforcingPermissionsIdLocked(userId); + final long identity = Binder.clearCallingIdentity(); + try { + if (mSpooler.cancelPrintJob(printJobId, resolvedAppId, resolvedUserId)) { + return; + } + PrintJobInfo printJob = getPrintJob(printJobId, resolvedAppId, resolvedUserId); + if (printJob == null) { + return; + } + ComponentName printServiceName = printJob.getPrinterId().getServiceComponentName(); + PrintServiceClient printService = null; + synchronized (mLock) { + printService = mServices.get(printServiceName); + } + if (printService == null) { + return; + } + printService.requestCancelPrintJob(printJob); + } finally { + Binder.restoreCallingIdentity(identity); + } + } + + // Called only from the spooler. + @Override + public void onPrintJobQueued(PrinterId printerId, PrintJobInfo printJob) { + throwIfCallerNotSignedWithSystemKey(); + PrintServiceClient printService = null; + synchronized (mLock) { + ComponentName printServiceName = printerId.getServiceComponentName(); + printService = mServices.get(printServiceName); + } + if (printService != null) { + final long identity = Binder.clearCallingIdentity(); + try { + printService.notifyPrintJobQueued(printJob); + } finally { + Binder.restoreCallingIdentity(identity); + } + } + } + + // Called only from the spooler. + @Override + public void startDiscoverPrinters(IPrinterDiscoveryObserver observer) { + throwIfCallerNotSignedWithSystemKey(); + List<PrintServiceClient> services = new ArrayList<PrintServiceClient>(); + synchronized (mLock) { + mPrinterDiscoveryObserver = observer; + services.addAll(mServices.values()); + } + final int serviceCount = services.size(); + if (serviceCount <= 0) { + return; + } + final long identity = Binder.clearCallingIdentity(); + try { + for (int i = 0; i < serviceCount; i++) { + PrintServiceClient service = services.get(i); + service.startPrinterDiscovery(); + } + } finally { + Binder.restoreCallingIdentity(identity); + } + } + + // Called only from the spooler. + @Override + public void stopDiscoverPrinters() { + throwIfCallerNotSignedWithSystemKey(); + List<PrintServiceClient> services = new ArrayList<PrintServiceClient>(); + synchronized (mLock) { + mPrinterDiscoveryObserver = null; + services.addAll(mServices.values()); + } + final int serviceCount = services.size(); + if (serviceCount <= 0) { + return; + } + final long identity = Binder.clearCallingIdentity(); + try { + for (int i = 0; i < serviceCount; i++) { + PrintServiceClient service = services.get(i); + service.stopPrintersDiscovery(); + } + } finally { + Binder.restoreCallingIdentity(identity); + } + } + + private void registerContentObservers() { + final Uri enabledPrintServicesUri = Settings.Secure.getUriFor( + Settings.Secure.ENABLED_PRINT_SERVICES); + + ContentObserver observer = new ContentObserver(new Handler(mContext.getMainLooper())) { + @Override + public void onChange(boolean selfChange, Uri uri) { + if (enabledPrintServicesUri.equals(uri)) { + synchronized (mLock) { + if (readEnabledPrintServicesChangedLocked()) { + onUserStateChangedLocked(); + } + } + } + } + }; + + mContext.getContentResolver().registerContentObserver(enabledPrintServicesUri, + false, observer, UserHandle.USER_ALL); + } + + private void registerBoradcastreceivers() { + PackageMonitor monitor = new PackageMonitor() { + @Override + public void onSomePackagesChanged() { + synchronized (mLock) { + if (getChangingUserId() != mCurrentUserId) { + return; + } + if (readConfigurationForUserStateLocked()) { + onUserStateChangedLocked(); + } + } + } + + @Override + public void onPackageRemoved(String packageName, int uid) { + synchronized (mLock) { + if (getChangingUserId() != mCurrentUserId) { + return; + } + Iterator<ComponentName> iterator = mEnabledServiceNames.iterator(); + while (iterator.hasNext()) { + ComponentName componentName = iterator.next(); + if (packageName.equals(componentName.getPackageName())) { + iterator.remove(); + onEnabledServiceNamesChangedLocked(); + return; + } + } + } + } + + @Override + public boolean onHandleForceStop(Intent intent, String[] stoppedPackages, + int uid, boolean doit) { + synchronized (mLock) { + if (getChangingUserId() != mCurrentUserId) { + return false; + } + Iterator<ComponentName> iterator = mEnabledServiceNames.iterator(); + while (iterator.hasNext()) { + ComponentName componentName = iterator.next(); + String componentPackage = componentName.getPackageName(); + for (String stoppedPackage : stoppedPackages) { + if (componentPackage.equals(stoppedPackage)) { + if (!doit) { + return true; + } + iterator.remove(); + onEnabledServiceNamesChangedLocked(); + } + } + } + return false; + } + } + + private void onEnabledServiceNamesChangedLocked() { + // Update the enabled services setting. + persistComponentNamesToSettingLocked( + Settings.Secure.ENABLED_PRINT_SERVICES, + mEnabledServiceNames, mCurrentUserId); + // Update the current user state. + onUserStateChangedLocked(); + } + }; + + // package changes + monitor.register(mContext, null, UserHandle.ALL, true); + + // user changes + IntentFilter intentFilter = new IntentFilter(); + intentFilter.addAction(Intent.ACTION_USER_SWITCHED); + + mContext.registerReceiverAsUser(new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + if (Intent.ACTION_USER_SWITCHED.equals(action)) { + switchUser(intent.getIntExtra(Intent.EXTRA_USER_HANDLE, 0)); + } + } + }, UserHandle.ALL, intentFilter, null, null); + } + + private void throwIfCallerNotSignedWithSystemKey() { + if (mContext.getPackageManager().checkSignatures( + mMyUid, Binder.getCallingUid()) != PackageManager.SIGNATURE_MATCH) { + throw new SecurityException("Caller must be signed with the system key!"); + } + } + + private void onUserStateChangedLocked() { + manageServicesLocked(); + } + + private void manageServicesLocked() { + final int installedCount = mInstalledServices.size(); + for (int i = 0; i < installedCount; i++) { + ResolveInfo resolveInfo = mInstalledServices.get(i).getResolveInfo(); + ComponentName serviceName = new ComponentName(resolveInfo.serviceInfo.packageName, + resolveInfo.serviceInfo.name); + if (mEnabledServiceNames.contains(serviceName)) { + if (!mServices.containsKey(serviceName)) { + new PrintServiceClient(serviceName, mCurrentUserId).ensureBoundLocked(); + } + } else { + PrintServiceClient service = mServices.get(serviceName); + if (service != null) { + service.ensureUnboundLocked(); + } + } + } + } + + private boolean readConfigurationForUserStateLocked() { + boolean somethingChanged = false; + somethingChanged |= readInstalledPrintServiceLocked(); + somethingChanged |= readEnabledPrintServicesChangedLocked(); + return somethingChanged; + } + + private boolean readEnabledPrintServicesChangedLocked() { + Set<ComponentName> tempEnabledServiceNameSet = new HashSet<ComponentName>(); + readComponentNamesFromSettingLocked(Settings.Secure.ENABLED_PRINT_SERVICES, + mCurrentUserId, tempEnabledServiceNameSet); + if (!tempEnabledServiceNameSet.equals(mEnabledServiceNames)) { + mEnabledServiceNames.clear(); + mEnabledServiceNames.addAll(tempEnabledServiceNameSet); + return true; + } + return false; + } + + private boolean readInstalledPrintServiceLocked() { + Set<PrintServiceInfo> tempPrintServices = new HashSet<PrintServiceInfo>(); + + List<ResolveInfo> installedServices = mContext.getPackageManager() + .queryIntentServicesAsUser( + new Intent(android.printservice.PrintService.SERVICE_INTERFACE), + PackageManager.GET_SERVICES | PackageManager.GET_META_DATA, + mCurrentUserId); + + final int installedCount = installedServices.size(); + for (int i = 0, count = installedCount; i < count; i++) { + ResolveInfo installedService = installedServices.get(i); + if (!android.Manifest.permission.BIND_PRINT_SERVICE.equals( + installedService.serviceInfo.permission)) { + ComponentName serviceName = new ComponentName( + installedService.serviceInfo.packageName, + installedService.serviceInfo.name); + Slog.w(LOG_TAG, "Skipping print service " + + serviceName.flattenToShortString() + + " since it does not require permission " + + android.Manifest.permission.BIND_PRINT_SERVICE); + continue; + } + tempPrintServices.add(PrintServiceInfo.create(installedService, mContext)); + } + + if (!tempPrintServices.equals(mInstalledServices)) { + mInstalledServices.clear(); + mInstalledServices.addAll(tempPrintServices); + return true; + } + return false; + } + + private void readComponentNamesFromSettingLocked(String settingName, int userId, + Set<ComponentName> outComponentNames) { + String settingValue = Settings.Secure.getStringForUser(mContext.getContentResolver(), + settingName, userId); + outComponentNames.clear(); + if (!TextUtils.isEmpty(settingValue)) { + TextUtils.SimpleStringSplitter splitter = mStringColonSplitter; + splitter.setString(settingValue); + while (splitter.hasNext()) { + String string = splitter.next(); + if (TextUtils.isEmpty(string)) { + continue; + } + ComponentName componentName = ComponentName.unflattenFromString(string); + if (componentName != null) { + outComponentNames.add(componentName); + } + } + } + } + + private void persistComponentNamesToSettingLocked(String settingName, + Set<ComponentName> componentNames, int userId) { + StringBuilder builder = new StringBuilder(); + for (ComponentName componentName : componentNames) { + if (builder.length() > 0) { + builder.append(COMPONENT_NAME_SEPARATOR); + } + builder.append(componentName.flattenToShortString()); + } + Settings.Secure.putStringForUser(mContext.getContentResolver(), + settingName, builder.toString(), userId); + } + + private void switchUser(int newUserId) { + synchronized (mLock) { + // Disconnect services for the old user. + mEnabledServiceNames.clear(); + onUserStateChangedLocked(); + + // The user changed. + mCurrentUserId = newUserId; + + // Update the user state based on current settings. + readConfigurationForUserStateLocked(); + onUserStateChangedLocked(); + } + + // Unbind the spooler for the old user). + mSpooler.unbind(); + + // If we have queued jobs, advertise it, or we do + // not need the spooler for now. + if (notifyQueuedPrintJobs()) { + mSpooler.unbind(); + } + } + + private boolean notifyQueuedPrintJobs() { + Map<PrintServiceClient, List<PrintJobInfo>> notifications = + new HashMap<PrintServiceClient, List<PrintJobInfo>>(); + synchronized (mLock) { + for (PrintServiceClient service : mServices.values()) { + List<PrintJobInfo> printJobs = mSpooler.getPrintJobs( + service.mComponentName, PrintJobInfo.STATE_QUEUED, + PrintManager.APP_ID_ANY, service.mUserId); + notifications.put(service, printJobs); + } + } + if (notifications.isEmpty()) { + return false; + } + for (Map.Entry<PrintServiceClient, List<PrintJobInfo>> notification + : notifications.entrySet()) { + PrintServiceClient service = notification.getKey(); + List<PrintJobInfo> printJobs = notification.getValue(); + final int printJobIdCount = printJobs.size(); + for (int i = 0; i < printJobIdCount; i++) { + service.notifyPrintJobQueued(printJobs.get(i)); + } + } + return true; + } + + private int resolveCallingUserEnforcingPermissionsIdLocked(int userId) { + final int callingUid = Binder.getCallingUid(); + if (callingUid == 0 || callingUid == Process.SYSTEM_UID + || callingUid == Process.SHELL_UID) { + return userId; + } + final int callingUserId = UserHandle.getUserId(callingUid); + if (callingUserId == userId) { + return userId; + } + if (mContext.checkCallingPermission(Manifest.permission.INTERACT_ACROSS_USERS_FULL) + != PackageManager.PERMISSION_GRANTED + || mContext.checkCallingPermission(Manifest.permission.INTERACT_ACROSS_USERS) + != PackageManager.PERMISSION_GRANTED) { + if (userId == UserHandle.USER_CURRENT_OR_SELF) { + return callingUserId; + } + throw new SecurityException("Call from user " + callingUserId + " as user " + + userId + " without permission INTERACT_ACROSS_USERS or " + + "INTERACT_ACROSS_USERS_FULL not allowed."); + } + if (userId == UserHandle.USER_CURRENT || userId == UserHandle.USER_CURRENT_OR_SELF) { + return mCurrentUserId; + } + throw new IllegalArgumentException("Calling user can be changed to only " + + "UserHandle.USER_CURRENT or UserHandle.USER_CURRENT_OR_SELF."); + } + + private int resolveCallingAppEnforcingPermissionsLocked(int appId) { + final int callingUid = Binder.getCallingUid(); + if (callingUid == 0 || callingUid == Process.SYSTEM_UID + || callingUid == Process.SHELL_UID) { + return appId; + } + final int callingAppId = UserHandle.getAppId(callingUid); + if (appId == callingAppId) { + return appId; + } + if (mContext.checkCallingPermission(Manifest.permission.ACCESS_ALL_PRINT_JOBS) + != PackageManager.PERMISSION_GRANTED) { + throw new SecurityException("Call from app " + callingAppId + " as app " + + appId + " without permission INTERACT_ACROSS_APPS"); + } + return appId; + } + + private final class PrintServiceClient extends IPrintServiceClient.Stub + implements ServiceConnection, DeathRecipient { + + private final ComponentName mComponentName; + + private final Intent mIntent; + + private final int mUserId; + + private IPrintService mInterface; + + private boolean mBinding; + + private boolean mWasConnectedAndDied; + + public PrintServiceClient(ComponentName componentName, int userId) { + mComponentName = componentName; + mIntent = new Intent().setComponent(mComponentName); + mUserId = userId; + } + + @Override + public List<PrintJobInfo> getPrintJobs() { + return mSpooler.getPrintJobs(mComponentName, PrintJobInfo.STATE_ANY, + PrintManager.APP_ID_ANY, mUserId); + } + + @Override + public PrintJobInfo getPrintJob(int printJobId) { + return mSpooler.getPrintJobInfo(printJobId, + PrintManager.APP_ID_ANY, mUserId); + } + + @Override + public boolean setPrintJobState(int printJobId, int state) { + return mSpooler.setPrintJobState(printJobId, state, mUserId); + } + + @Override + public boolean setPrintJobTag(int printJobId, String tag) { + return mSpooler.setPrintJobTag(printJobId, tag, mUserId); + } + + @Override + public void writePrintJobData(ParcelFileDescriptor fd, int printJobId) { + mSpooler.writePrintJobData(fd, printJobId, mUserId); + } + + @Override + public void addDiscoveredPrinters(List<PrinterInfo> printers) { + throwIfPrinterIdsForPrinterInfoTampered(printers); + synchronized (mLock) { + if (mPrinterDiscoveryObserver != null) { + try { + mPrinterDiscoveryObserver.addDiscoveredPrinters(printers); + } catch (RemoteException re) { + /* ignore */ + } + } + } + } + + @Override + public void removeDiscoveredPrinters(List<PrinterId> printerIds) { + throwIfPrinterIdsTampered(printerIds); + synchronized (mLock) { + if (mPrinterDiscoveryObserver != null) { + try { + mPrinterDiscoveryObserver.removeDiscoveredPrinters(printerIds); + } catch (RemoteException re) { + /* ignore */ + } + } + } + } + + public void requestCancelPrintJob(PrintJobInfo printJob) { + synchronized (mLock) { + try { + mInterface.requestCancelPrintJob(printJob); + } catch (RemoteException re) { + Slog.e(LOG_TAG, "Error canceling pring job!", re); + } + } + } + + public void notifyPrintJobQueued(PrintJobInfo printJob) { + IPrintService service = mInterface; + if (service != null) { + try { + service.onPrintJobQueued(printJob); + } catch (RemoteException re) { + /* ignore */ + } + } + } + + public void startPrinterDiscovery() { + IPrintService service = mInterface; + if (service != null) { + try { + service.startPrinterDiscovery(); + } catch (RemoteException re) { + /* ignore */ + } + } + } + + public void stopPrintersDiscovery() { + IPrintService service = mInterface; + if (service != null) { + try { + service.stopPrinterDiscovery(); + } catch (RemoteException re) { + /* ignore */ + } + } + } + + public void ensureBoundLocked() { + if (mBinding) { + return; + } + if (mInterface == null) { + mBinding = true; + mContext.bindServiceAsUser(mIntent, this, + Context.BIND_AUTO_CREATE, new UserHandle(mUserId)); + } + } + + public void ensureUnboundLocked() { + if (mBinding) { + mBinding = false; + return; + } + if (mInterface != null) { + mContext.unbindService(this); + destroyLocked(); + } + } + + @Override + public void onServiceConnected(ComponentName name, IBinder service) { + synchronized (mLock) { + mInterface = IPrintService.Stub.asInterface(service); + mServices.put(mComponentName, this); + try { + mInterface.asBinder().linkToDeath(this, 0); + } catch (RemoteException re) { + destroyLocked(); + return; + } + if (mUserId != mCurrentUserId) { + destroyLocked(); + return; + } + if (mBinding || mWasConnectedAndDied) { + mBinding = false; + mWasConnectedAndDied = false; + onUserStateChangedLocked(); + try { + mInterface.setClient(this); + } catch (RemoteException re) { + Slog.w(LOG_TAG, "Error while setting client for service: " + + service, re); + } + } else { + destroyLocked(); + } + } + } + + @Override + public void onServiceDisconnected(ComponentName name) { + /* do nothing - #binderDied takes care */ + } + + @Override + public void binderDied() { + synchronized (mLock) { + if (isConnectedLocked()) { + mWasConnectedAndDied = true; + } + destroyLocked(); + } + } + + private void destroyLocked() { + if (mServices.remove(mComponentName) == null) { + return; + } + if (isConnectedLocked()) { + try { + mInterface.asBinder().unlinkToDeath(this, 0); + } catch (NoSuchElementException nse) { + /* ignore */ + } + try { + mInterface.setClient(null); + } catch (RemoteException re) { + /* ignore */ + } + mInterface = null; + } + mBinding = false; + } + + private boolean isConnectedLocked() { + return (mInterface != null); + } + + private void throwIfPrinterIdsForPrinterInfoTampered(List<PrinterInfo> printerInfos) { + final int printerInfoCount = printerInfos.size(); + for (int i = 0; i < printerInfoCount; i++) { + PrinterId printerId = printerInfos.get(i).getId(); + throwIfPrinterIdTampered(printerId); + } + } + + private void throwIfPrinterIdsTampered(List<PrinterId> printerIds) { + final int printerIdCount = printerIds.size(); + for (int i = 0; i < printerIdCount; i++) { + PrinterId printerId = printerIds.get(i); + throwIfPrinterIdTampered(printerId); + } + } + + private void throwIfPrinterIdTampered(PrinterId printerId) { + if (printerId == null || printerId.getServiceComponentName() == null + || !printerId.getServiceComponentName().equals(mComponentName)) { + throw new IllegalArgumentException("Invalid printer id: " + printerId); + } + } + } +} diff --git a/services/java/com/android/server/print/RemoteSpooler.java b/services/java/com/android/server/print/RemoteSpooler.java new file mode 100644 index 0000000..fef5818 --- /dev/null +++ b/services/java/com/android/server/print/RemoteSpooler.java @@ -0,0 +1,416 @@ +/* + * Copyright (C) 2013 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 com.android.server.print; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.os.Binder; +import android.os.IBinder; +import android.os.ParcelFileDescriptor; +import android.os.RemoteException; +import android.os.SystemClock; +import android.os.UserHandle; +import android.os.IBinder.DeathRecipient; +import android.print.IPrintAdapter; +import android.print.IPrintClient; +import android.print.IPrintSpoolerService; +import android.print.IPrintSpoolerServiceCallbacks; +import android.print.PrintAttributes; +import android.print.PrintJobInfo; +import android.util.Slog; +import android.util.TimedRemoteCaller; + +import java.io.IOException; +import java.util.List; +import java.util.concurrent.TimeoutException; + +/** + * This represents the remote print spooler as a local object to the + * PrintManagerSerivce. It is responsible to connecting to the remove + * spooler if needed, to make the timed out remote calls, and to handle + * remove exceptions. + */ +final class RemoteSpooler implements ServiceConnection, DeathRecipient { + + private static final String LOG_TAG = "Spooler"; + + private static final long BIND_SPOOLER_SERVICE_TIMEOUT = 10000; + + private final Object mLock = new Object(); + + private final Context mContext; + + private final Intent mIntent; + + private final GetPrintJobsCaller mGetPrintJobsCaller = new GetPrintJobsCaller(); + + private final CreatePrintJobCaller mCreatePrintJobCaller = new CreatePrintJobCaller(); + + private final CancelPrintJobCaller mCancelPrintJobCaller = new CancelPrintJobCaller(); + + private final GetPrintJobCaller mGetPrintJobCaller = new GetPrintJobCaller(); + + private final SetPrintJobStateCaller mSetPrintJobStatusCaller = new SetPrintJobStateCaller(); + + private final SetPrintJobTagCaller mSetPrintJobTagCaller = new SetPrintJobTagCaller(); + + private IPrintSpoolerService mRemoteInterface; + + private int mUserId = UserHandle.USER_NULL; + + public RemoteSpooler(Context context) { + mContext = context; + mIntent = new Intent(); + mIntent.setComponent(new ComponentName("com.android.printspooler", + "com.android.printspooler.PrintSpoolerService")); + } + + public List<PrintJobInfo> getPrintJobs(ComponentName componentName, int state, int appId, + int userId) { + try { + return mGetPrintJobsCaller.getPrintJobs(getRemoteInstance(userId), + componentName, state, appId); + } catch (RemoteException re) { + Slog.e(LOG_TAG, "Error getting print jobs!", re); + } catch (TimeoutException te) { + Slog.e(LOG_TAG, "Error getting print jobs!", te); + } + return null; + } + + public PrintJobInfo createPrintJob(String printJobName, IPrintClient client, + IPrintAdapter printAdapter, PrintAttributes attributes, int appId, int userId) { + try { + return mCreatePrintJobCaller.createPrintJob(getRemoteInstance(userId), + printJobName, client, printAdapter, attributes, appId); + } catch (RemoteException re) { + Slog.e(LOG_TAG, "Error creating print job!", re); + } catch (TimeoutException te) { + Slog.e(LOG_TAG, "Error creating print job!", te); + } + return null; + } + + public boolean cancelPrintJob(int printJobId, int appId, int userId) { + try { + return mCancelPrintJobCaller.cancelPrintJob(getRemoteInstance(userId), + printJobId, appId); + } catch (RemoteException re) { + Slog.e(LOG_TAG, "Error canceling print job!", re); + } catch (TimeoutException te) { + Slog.e(LOG_TAG, "Error canceling print job!", te); + } + return false; + } + + public void writePrintJobData(ParcelFileDescriptor fd, int printJobId, int userId) { + try { + getRemoteInstance(userId).writePrintJobData(fd, printJobId); + } catch (RemoteException re) { + Slog.e(LOG_TAG, "Error writing print job data!", re); + } catch (TimeoutException te) { + Slog.e(LOG_TAG, "Error writing print job data!", te); + } finally { + // We passed the file descriptor across and now the other + // side is responsible to close it, so close the local copy. + try { + fd.close(); + } catch (IOException ioe) { + /* ignore */ + } + } + } + + public PrintJobInfo getPrintJobInfo(int printJobId, int appId, int userId) { + try { + return mGetPrintJobCaller.getPrintJobInfo(getRemoteInstance(userId), + printJobId, appId); + } catch (RemoteException re) { + Slog.e(LOG_TAG, "Error getting print job!", re); + } catch (TimeoutException te) { + Slog.e(LOG_TAG, "Error getting print job!", te); + } + return null; + } + + public boolean setPrintJobState(int printJobId, int state, int userId) { + try { + return mSetPrintJobStatusCaller.setPrintJobState(getRemoteInstance(userId), + printJobId, state); + } catch (RemoteException re) { + Slog.e(LOG_TAG, "Error setting print job status!", re); + } catch (TimeoutException te) { + Slog.e(LOG_TAG, "Error setting print job status!", te); + } + return false; + } + + public boolean setPrintJobTag(int printJobId, String tag, int userId) { + try { + return mSetPrintJobTagCaller.setPrintJobTag(getRemoteInstance(userId), + printJobId, tag); + } catch (RemoteException re) { + Slog.e(LOG_TAG, "Error setting print job tag!", re); + } catch (TimeoutException te) { + Slog.e(LOG_TAG, "Error setting print job tag!", te); + } + return false; + } + + @Override + public void onServiceDisconnected(ComponentName name) { + binderDied(); + } + + @Override + public void onServiceConnected(ComponentName name, IBinder service) { + synchronized (mLock) { + try { + service.linkToDeath(this, 0); + mRemoteInterface = IPrintSpoolerService.Stub.asInterface(service); + } catch (RemoteException re) { + /* ignore */ + } + } + } + + private IPrintSpoolerService getRemoteInstance(int userId) throws TimeoutException { + synchronized (mLock) { + if (mRemoteInterface != null && mUserId == userId) { + return mRemoteInterface; + } + + final long identity = Binder.clearCallingIdentity(); + try { + if (mUserId != UserHandle.USER_NULL && mUserId != userId) { + unbind(); + } + + mContext.bindServiceAsUser(mIntent, this, + Context.BIND_AUTO_CREATE | Context.BIND_ALLOW_OOM_MANAGEMENT, + UserHandle.CURRENT); + } finally { + Binder.restoreCallingIdentity(identity); + } + + final long startMillis = SystemClock.uptimeMillis(); + while (true) { + if (mRemoteInterface != null) { + break; + } + final long elapsedMillis = SystemClock.uptimeMillis() - startMillis; + final long remainingMillis = BIND_SPOOLER_SERVICE_TIMEOUT - elapsedMillis; + if (remainingMillis <= 0) { + throw new TimeoutException("Cannot get spooler!"); + } + try { + mLock.wait(remainingMillis); + } catch (InterruptedException ie) { + /* ignore */ + } + } + + mUserId = userId; + + return mRemoteInterface; + } + } + + public void unbind() { + synchronized (mLock) { + if (mRemoteInterface != null) { + mContext.unbindService(this); + mRemoteInterface = null; + mUserId = UserHandle.USER_NULL; + } + } + } + + @Override + public void binderDied() { + synchronized (mLock) { + if (mRemoteInterface != null) { + mRemoteInterface.asBinder().unlinkToDeath(this, 0); + mRemoteInterface = null; + } + } + } + + private final class GetPrintJobsCaller extends TimedRemoteCaller<List<PrintJobInfo>> { + private final IPrintSpoolerServiceCallbacks mCallback; + + public GetPrintJobsCaller() { + super(TimedRemoteCaller.DEFAULT_CALL_TIMEOUT_MILLIS); + mCallback = new BasePrintSpoolerServiceCallbacks() { + @Override + public void onGetPrintJobsResult(List<PrintJobInfo> printJobs, int sequence) { + onRemoteMethodResult(printJobs, sequence); + } + }; + } + + public List<PrintJobInfo> getPrintJobs(IPrintSpoolerService target, + ComponentName componentName, int state, int appId) + throws RemoteException, TimeoutException { + final int sequence = onBeforeRemoteCall(); + target.getPrintJobs(mCallback, componentName, state, appId, sequence); + return getResultTimed(sequence); + } + } + + private final class CreatePrintJobCaller extends TimedRemoteCaller<PrintJobInfo> { + private final IPrintSpoolerServiceCallbacks mCallback; + + public CreatePrintJobCaller() { + super(TimedRemoteCaller.DEFAULT_CALL_TIMEOUT_MILLIS); + mCallback = new BasePrintSpoolerServiceCallbacks() { + @Override + public void onCreatePrintJobResult(PrintJobInfo printJob, int sequence) { + onRemoteMethodResult(printJob, sequence); + } + }; + } + + public PrintJobInfo createPrintJob(IPrintSpoolerService target, String printJobName, + IPrintClient client, IPrintAdapter printAdapter, PrintAttributes attributes, + int appId) throws RemoteException, TimeoutException { + final int sequence = onBeforeRemoteCall(); + target.createPrintJob(printJobName, client, printAdapter, attributes, + mCallback, appId, sequence); + return getResultTimed(sequence); + } + } + + private final class CancelPrintJobCaller extends TimedRemoteCaller<Boolean> { + private final IPrintSpoolerServiceCallbacks mCallback; + + public CancelPrintJobCaller() { + super(TimedRemoteCaller.DEFAULT_CALL_TIMEOUT_MILLIS); + mCallback = new BasePrintSpoolerServiceCallbacks() { + @Override + public void onCancelPrintJobResult(boolean canceled, int sequence) { + onRemoteMethodResult(canceled, sequence); + } + }; + } + + public boolean cancelPrintJob(IPrintSpoolerService target, int printJobId, + int appId) throws RemoteException, TimeoutException { + final int sequence = onBeforeRemoteCall(); + target.cancelPrintJob(printJobId, mCallback, appId, sequence); + return getResultTimed(sequence); + } + } + + private final class GetPrintJobCaller extends TimedRemoteCaller<PrintJobInfo> { + private final IPrintSpoolerServiceCallbacks mCallback; + + public GetPrintJobCaller() { + super(TimedRemoteCaller.DEFAULT_CALL_TIMEOUT_MILLIS); + mCallback = new BasePrintSpoolerServiceCallbacks() { + @Override + public void onGetPrintJobInfoResult(PrintJobInfo printJob, int sequence) { + onRemoteMethodResult(printJob, sequence); + } + }; + } + + public PrintJobInfo getPrintJobInfo(IPrintSpoolerService target, int printJobId, + int appId) throws RemoteException, TimeoutException { + final int sequence = onBeforeRemoteCall(); + target.getPrintJob(printJobId, mCallback, appId, sequence); + return getResultTimed(sequence); + } + } + + private final class SetPrintJobStateCaller extends TimedRemoteCaller<Boolean> { + private final IPrintSpoolerServiceCallbacks mCallback; + + public SetPrintJobStateCaller() { + super(TimedRemoteCaller.DEFAULT_CALL_TIMEOUT_MILLIS); + mCallback = new BasePrintSpoolerServiceCallbacks() { + @Override + public void onSetPrintJobStateResult(boolean success, int sequence) { + onRemoteMethodResult(success, sequence); + } + }; + } + + public boolean setPrintJobState(IPrintSpoolerService target, int printJobId, + int status) throws RemoteException, TimeoutException { + final int sequence = onBeforeRemoteCall(); + target.setPrintJobState(printJobId, status, mCallback, sequence); + return getResultTimed(sequence); + } + } + + private final class SetPrintJobTagCaller extends TimedRemoteCaller<Boolean> { + private final IPrintSpoolerServiceCallbacks mCallback; + + public SetPrintJobTagCaller() { + super(TimedRemoteCaller.DEFAULT_CALL_TIMEOUT_MILLIS); + mCallback = new BasePrintSpoolerServiceCallbacks() { + @Override + public void onSetPrintJobTagResult(boolean success, int sequence) { + onRemoteMethodResult(success, sequence); + } + }; + } + + public boolean setPrintJobTag(IPrintSpoolerService target, int printJobId, + String tag) throws RemoteException, TimeoutException { + final int sequence = onBeforeRemoteCall(); + target.setPrintJobTag(printJobId, tag, mCallback, sequence); + return getResultTimed(sequence); + } + } + + private abstract class BasePrintSpoolerServiceCallbacks + extends IPrintSpoolerServiceCallbacks.Stub { + @Override + public void onGetPrintJobsResult(List<PrintJobInfo> printJobIds, int sequence) { + /** do nothing */ + } + + @Override + public void onGetPrintJobInfoResult(PrintJobInfo printJob, int sequence) { + /** do nothing */ + } + + @Override + public void onCreatePrintJobResult(PrintJobInfo printJob, int sequence) { + /** do nothing */ + } + + @Override + public void onCancelPrintJobResult(boolean canceled, int sequence) { + /** do nothing */ + } + + @Override + public void onSetPrintJobStateResult(boolean success, int sequece) { + /** do nothing */ + } + + @Override + public void onSetPrintJobTagResult(boolean success, int sequence) { + /** do nothing */ + } + } +}
\ No newline at end of file |