diff options
78 files changed, 11700 insertions, 171 deletions
diff --git a/Android.mk b/Android.mk index f0828b7552b4..618f40433b18 100644 --- a/Android.mk +++ b/Android.mk @@ -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 7281d011bf12..4debd4daf64d 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"; @@ -16895,7 +16899,7 @@ package android.opengl { } public class Matrix { - ctor public Matrix(); + ctor public deprecated Matrix(); method public static void frustumM(float[], int, float, float, float, float, float, float); method public static boolean invertM(float[], int, float[], int); method public static float length(float, float, float); @@ -18385,6 +18389,233 @@ 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[] getPages(); + 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 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 +18649,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 c6f3fb8fdee3..992d8b72b174 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 35e51a69bf59..7c9117c0263b 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 4b022d96646b..c5d2c7760377 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/Binder.java b/core/java/android/os/Binder.java index 7ffd30b0c3b1..7b9141840da2 100644 --- a/core/java/android/os/Binder.java +++ b/core/java/android/os/Binder.java @@ -17,6 +17,7 @@ package android.os; import android.util.Log; +import com.android.internal.util.FastPrintWriter; import java.io.FileDescriptor; import java.io.FileOutputStream; @@ -288,7 +289,7 @@ public class Binder implements IBinder { */ public void dump(FileDescriptor fd, String[] args) { FileOutputStream fout = new FileOutputStream(fd); - PrintWriter pw = new PrintWriter(fout); + PrintWriter pw = new FastPrintWriter(fout); try { final String disabled; synchronized (Binder.class) { @@ -310,7 +311,7 @@ public class Binder implements IBinder { */ public void dumpAsync(final FileDescriptor fd, final String[] args) { final FileOutputStream fout = new FileOutputStream(fd); - final PrintWriter pw = new PrintWriter(fout); + final PrintWriter pw = new FastPrintWriter(fout); Thread thr = new Thread("Binder.dumpAsync") { public void run() { try { diff --git a/core/java/android/os/CancellationSignal.java b/core/java/android/os/CancellationSignal.java index dcba9b7758dd..e8053d5d275d 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 000000000000..a9b4fb7c3734 --- /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 000000000000..3f39d0887954 --- /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 000000000000..ff9877e04980 --- /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 000000000000..2c0d607bd8ee --- /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 000000000000..e84d5927fe0c --- /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 000000000000..0c519136bb83 --- /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 000000000000..39aeb8c73ec9 --- /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 000000000000..b1ae14f73c2c --- /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 000000000000..044a71568cfd --- /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 000000000000..a7f809bd801d --- /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 000000000000..27bf717f6d20 --- /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 000000000000..06e6b10f3b48 --- /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 000000000000..0b765a20ffa3 --- /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 000000000000..8511d0b7ed28 --- /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 000000000000..bbfc394a08a0 --- /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 000000000000..f7cca875a2c9 --- /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 000000000000..fbca99c60ce1 --- /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 000000000000..72d6057e5323 --- /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 pages. + * + * @return The included pages or <code>null</code> if not set. + */ + public PageRange[] getPages() { + return mPageRanges; + } + + /** + * Sets the included pages. + * + * @return The included pages. + * + * @hide + */ + public void setPages(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 000000000000..32a0f5a0a360 --- /dev/null +++ b/core/java/android/print/PrintManager.java @@ -0,0 +1,392 @@ +/* + * 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; + + /** + * 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 000000000000..84f422f15722 --- /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 000000000000..b853eb0f8e8a --- /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 000000000000..6ec5a58714e2 --- /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 000000000000..9283472ee5fc --- /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 000000000000..eabd96dc0118 --- /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 000000000000..cff8c028b93b --- /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 000000000000..9688761e1d1a --- /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 000000000000..d5cadc025244 --- /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 000000000000..95b67f2c18cd --- /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 000000000000..0370a252531b --- /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 1d2fbd8e5649..5dae599ae485 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 000000000000..abb2b6401d71 --- /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/java/android/view/HardwareRenderer.java b/core/java/android/view/HardwareRenderer.java index 03428c732c2d..bcefdbaf5d90 100644 --- a/core/java/android/view/HardwareRenderer.java +++ b/core/java/android/view/HardwareRenderer.java @@ -1301,7 +1301,10 @@ public abstract class HardwareRenderer { void destroySurface() { if (mEglSurface != null && mEglSurface != EGL_NO_SURFACE) { - sEgl.eglMakeCurrent(sEglDisplay, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); + if (mEglSurface.equals(sEgl.eglGetCurrentSurface(EGL_DRAW))) { + sEgl.eglMakeCurrent(sEglDisplay, + EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); + } sEgl.eglDestroySurface(sEglDisplay, mEglSurface); mEglSurface = null; } diff --git a/core/java/com/android/internal/util/FastPrintWriter.java b/core/java/com/android/internal/util/FastPrintWriter.java new file mode 100644 index 000000000000..6ad8e66eb72d --- /dev/null +++ b/core/java/com/android/internal/util/FastPrintWriter.java @@ -0,0 +1,478 @@ +package com.android.internal.util; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.OutputStream; +import java.io.PrintWriter; +import java.io.UnsupportedEncodingException; +import java.io.Writer; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.Charset; +import java.nio.charset.CharsetEncoder; +import java.nio.charset.CoderResult; +import java.nio.charset.CodingErrorAction; + +public class FastPrintWriter extends PrintWriter { + private static final int BUFFER_LEN = 8192; + + private final char[] mText = new char[BUFFER_LEN]; + private int mPos; + + final private OutputStream mOutputStream; + private CharsetEncoder mCharset; + final private ByteBuffer mBytes = ByteBuffer.allocate(BUFFER_LEN); + + /** + * Constructs a new {@code PrintWriter} with {@code out} as its target + * stream. By default, the new print writer does not automatically flush its + * contents to the target stream when a newline is encountered. + * + * @param out + * the target output stream. + * @throws NullPointerException + * if {@code out} is {@code null}. + */ + public FastPrintWriter(OutputStream out) { + super(out); + mOutputStream = out; + initDefaultEncoder(); + } + + /** + * Constructs a new {@code PrintWriter} with {@code out} as its target + * stream. The parameter {@code autoFlush} determines if the print writer + * automatically flushes its contents to the target stream when a newline is + * encountered. + * + * @param out + * the target output stream. + * @param autoFlush + * indicates whether contents are flushed upon encountering a + * newline sequence. + * @throws NullPointerException + * if {@code out} is {@code null}. + */ + public FastPrintWriter(OutputStream out, boolean autoFlush) { + super(out, autoFlush); + mOutputStream = out; + initDefaultEncoder(); + } + + /** + * Constructs a new {@code PrintWriter} with {@code wr} as its target + * writer. By default, the new print writer does not automatically flush its + * contents to the target writer when a newline is encountered. + * + * @param wr + * the target writer. + * @throws NullPointerException + * if {@code wr} is {@code null}. + */ + public FastPrintWriter(Writer wr) { + super(wr); + mOutputStream = null; + initDefaultEncoder(); + } + + /** + * Constructs a new {@code PrintWriter} with {@code out} as its target + * writer. The parameter {@code autoFlush} determines if the print writer + * automatically flushes its contents to the target writer when a newline is + * encountered. + * + * @param wr + * the target writer. + * @param autoFlush + * indicates whether to flush contents upon encountering a + * newline sequence. + * @throws NullPointerException + * if {@code out} is {@code null}. + */ + public FastPrintWriter(Writer wr, boolean autoFlush) { + super(wr, autoFlush); + mOutputStream = null; + initDefaultEncoder(); + } + + /** + * Constructs a new {@code PrintWriter} with {@code file} as its target. The + * VM's default character set is used for character encoding. + * The print writer does not automatically flush its contents to the target + * file when a newline is encountered. The output to the file is buffered. + * + * @param file + * the target file. If the file already exists, its contents are + * removed, otherwise a new file is created. + * @throws java.io.FileNotFoundException + * if an error occurs while opening or creating the target file. + */ + public FastPrintWriter(File file) throws FileNotFoundException { + super(file); + mOutputStream = null; + initDefaultEncoder(); + } + + /** + * Constructs a new {@code PrintWriter} with {@code file} as its target. The + * character set named {@code csn} is used for character encoding. + * The print writer does not automatically flush its contents to the target + * file when a newline is encountered. The output to the file is buffered. + * + * @param file + * the target file. If the file already exists, its contents are + * removed, otherwise a new file is created. + * @param csn + * the name of the character set used for character encoding. + * @throws FileNotFoundException + * if an error occurs while opening or creating the target file. + * @throws NullPointerException + * if {@code csn} is {@code null}. + * @throws java.io.UnsupportedEncodingException + * if the encoding specified by {@code csn} is not supported. + */ + public FastPrintWriter(File file, String csn) throws FileNotFoundException, + UnsupportedEncodingException { + super(file, csn); + mOutputStream = null; + initEncoder(csn); + } + + /** + * Constructs a new {@code PrintWriter} with the file identified by {@code + * fileName} as its target. The VM's default character set is + * used for character encoding. The print writer does not automatically + * flush its contents to the target file when a newline is encountered. The + * output to the file is buffered. + * + * @param fileName + * the target file's name. If the file already exists, its + * contents are removed, otherwise a new file is created. + * @throws FileNotFoundException + * if an error occurs while opening or creating the target file. + */ + public FastPrintWriter(String fileName) throws FileNotFoundException { + super(fileName); + mOutputStream = null; + initDefaultEncoder(); + } + + /** + * Constructs a new {@code PrintWriter} with the file identified by {@code + * fileName} as its target. The character set named {@code csn} is used for + * character encoding. The print writer does not automatically flush its + * contents to the target file when a newline is encountered. The output to + * the file is buffered. + * + * @param fileName + * the target file's name. If the file already exists, its + * contents are removed, otherwise a new file is created. + * @param csn + * the name of the character set used for character encoding. + * @throws FileNotFoundException + * if an error occurs while opening or creating the target file. + * @throws NullPointerException + * if {@code csn} is {@code null}. + * @throws UnsupportedEncodingException + * if the encoding specified by {@code csn} is not supported. + */ + public FastPrintWriter(String fileName, String csn) + throws FileNotFoundException, UnsupportedEncodingException { + super(fileName, csn); + mOutputStream = null; + initEncoder(csn); + } + + private final void initEncoder(String csn) throws UnsupportedEncodingException { + try { + mCharset = Charset.forName(csn).newEncoder(); + } catch (Exception e) { + throw new UnsupportedEncodingException(csn); + } + mCharset.onMalformedInput(CodingErrorAction.REPLACE); + mCharset.onUnmappableCharacter(CodingErrorAction.REPLACE); + } + + private final void initDefaultEncoder() { + mCharset = Charset.defaultCharset().newEncoder(); + mCharset.onMalformedInput(CodingErrorAction.REPLACE); + mCharset.onUnmappableCharacter(CodingErrorAction.REPLACE); + } + + private void appendInner(char c) throws IOException { + int pos = mPos; + if (pos >= (BUFFER_LEN-1)) { + flush(); + pos = mPos; + } + mText[pos] = c; + mPos = pos+1; + } + + private void appendInner(String str, int i, final int length) throws IOException { + if (length > BUFFER_LEN) { + final int end = i + length; + while (i < end) { + int next = i + BUFFER_LEN; + appendInner(str, i, next<end ? BUFFER_LEN : (end-i)); + i = next; + } + return; + } + int pos = mPos; + if ((pos+length) > BUFFER_LEN) { + flush(); + pos = mPos; + } + str.getChars(i, i + length, mText, pos); + mPos = pos + length; + } + + private void appendInner(char[] buf, int i, final int length) throws IOException { + if (length > BUFFER_LEN) { + final int end = i + length; + while (i < end) { + int next = i + BUFFER_LEN; + appendInner(buf, i, next < end ? BUFFER_LEN : (end - i)); + i = next; + } + return; + } + int pos = mPos; + if ((pos+length) > BUFFER_LEN) { + flush(); + pos = mPos; + } + System.arraycopy(buf, i, mText, pos, length); + mPos = pos + length; + } + + private void flushBytesInner() throws IOException { + int position; + if ((position = mBytes.position()) > 0) { + mBytes.flip(); + mOutputStream.write(mBytes.array(), 0, position); + mBytes.clear(); + } + } + + private void flushInner() throws IOException { + //Log.i("PackageManager", "flush mPos=" + mPos); + if (mPos > 0) { + if (mOutputStream != null) { + CharBuffer charBuffer = CharBuffer.wrap(mText, 0, mPos); + CoderResult result = mCharset.encode(charBuffer, mBytes, true); + while (true) { + if (result.isError()) { + throw new IOException(result.toString()); + } else if (result.isOverflow()) { + flushBytesInner(); + result = mCharset.encode(charBuffer, mBytes, true); + continue; + } + break; + } + flushBytesInner(); + mOutputStream.flush(); + } else { + out.write(mText, 0, mPos); + out.flush(); + } + mPos = 0; + } + } + + /** + * Ensures that all pending data is sent out to the target. It also + * flushes the target. If an I/O error occurs, this writer's error + * state is set to {@code true}. + */ + @Override + public void flush() { + try { + flushInner(); + } catch (IOException e) { + } + super.flush(); + } + + /** + * Prints the string representation of the specified character array + * to the target. + * + * @param charArray + * the character array to print to the target. + * @see #print(String) + */ + public void print(char[] charArray) { + try { + appendInner(charArray, 0, charArray.length); + } catch (IOException e) { + } + } + + /** + * Prints the string representation of the specified character to the + * target. + * + * @param ch + * the character to print to the target. + * @see #print(String) + */ + public void print(char ch) { + try { + appendInner(ch); + } catch (IOException e) { + } + } + + /** + * Prints a string to the target. The string is converted to an array of + * bytes using the encoding chosen during the construction of this writer. + * The bytes are then written to the target with {@code write(int)}. + * <p> + * If an I/O error occurs, this writer's error flag is set to {@code true}. + * + * @param str + * the string to print to the target. + * @see #write(int) + */ + public void print(String str) { + if (str == null) { + str = String.valueOf((Object) null); + } + try { + appendInner(str, 0, str.length()); + } catch (IOException e) { + } + } + + /** + * Prints the string representation of the character array {@code chars} followed by a newline. + * Flushes this writer if the autoFlush flag is set to {@code true}. + */ + public void println(char[] chars) { + print(chars); + println(); + } + + /** + * Prints the string representation of the char {@code c} followed by a newline. + * Flushes this writer if the autoFlush flag is set to {@code true}. + */ + public void println(char c) { + print(c); + println(); + } + + /** + * Writes {@code count} characters from {@code buffer} starting at {@code + * offset} to the target. + * <p> + * This writer's error flag is set to {@code true} if this writer is closed + * or an I/O error occurs. + * + * @param buf + * the buffer to write to the target. + * @param offset + * the index of the first character in {@code buffer} to write. + * @param count + * the number of characters in {@code buffer} to write. + * @throws IndexOutOfBoundsException + * if {@code offset < 0} or {@code count < 0}, or if {@code + * offset + count} is greater than the length of {@code buf}. + */ + @Override + public void write(char[] buf, int offset, int count) { + try { + appendInner(buf, offset, count); + } catch (IOException e) { + } + } + + /** + * Writes one character to the target. Only the two least significant bytes + * of the integer {@code oneChar} are written. + * <p> + * This writer's error flag is set to {@code true} if this writer is closed + * or an I/O error occurs. + * + * @param oneChar + * the character to write to the target. + */ + @Override + public void write(int oneChar) { + try { + appendInner((char) oneChar); + } catch (IOException e) { + } + } + + /** + * Writes the characters from the specified string to the target. + * + * @param str + * the non-null string containing the characters to write. + */ + @Override + public void write(String str) { + try { + appendInner(str, 0, str.length()); + } catch (IOException e) { + } + } + + /** + * Writes {@code count} characters from {@code str} starting at {@code + * offset} to the target. + * + * @param str + * the non-null string containing the characters to write. + * @param offset + * the index of the first character in {@code str} to write. + * @param count + * the number of characters from {@code str} to write. + * @throws IndexOutOfBoundsException + * if {@code offset < 0} or {@code count < 0}, or if {@code + * offset + count} is greater than the length of {@code str}. + */ + @Override + public void write(String str, int offset, int count) { + try { + appendInner(str, offset, count); + } catch (IOException e) { + } + } + + /** + * Appends a subsequence of the character sequence {@code csq} to the + * target. This method works the same way as {@code + * PrintWriter.print(csq.subsequence(start, end).toString())}. If {@code + * csq} is {@code null}, then the specified subsequence of the string "null" + * will be written to the target. + * + * @param csq + * the character sequence appended to the target. + * @param start + * the index of the first char in the character sequence appended + * to the target. + * @param end + * the index of the character following the last character of the + * subsequence appended to the target. + * @return this writer. + * @throws StringIndexOutOfBoundsException + * if {@code start > end}, {@code start < 0}, {@code end < 0} or + * either {@code start} or {@code end} are greater or equal than + * the length of {@code csq}. + */ + @Override + public PrintWriter append(CharSequence csq, int start, int end) { + if (csq == null) { + csq = "null"; + } + String output = csq.subSequence(start, end).toString(); + write(output, 0, output.length()); + return this; + } +} diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index df2aea85fb99..ca274e3decb8 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 459a63414dd3..99f7e7a327d5 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 3858dcf283e7..3ab88257e688 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 3e2d8d377fe6..1938b888ab9a 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 e1d1e3372261..cb8d1442956f 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/docs/html/about/about_toc.cs b/docs/html/about/about_toc.cs index 64cdadf2f081..b752e20fa283 100644 --- a/docs/html/about/about_toc.cs +++ b/docs/html/about/about_toc.cs @@ -10,6 +10,7 @@ <div class="nav-section-header"><a href="<?cs var:toroot ?>about/versions/jelly-bean.html"> <span class="en">Jelly Bean</span></a></div> <ul> + <li><a href="<?cs var:toroot ?>about/versions/android-4.3.html">Android 4.3 APIs</a></li> <li><a href="<?cs var:toroot ?>about/versions/android-4.2.html">Android 4.2 APIs</a></li> <li><a href="<?cs var:toroot ?>about/versions/android-4.1.html">Android 4.1 APIs</a></li> </ul> diff --git a/docs/html/about/versions/android-4.0.jd b/docs/html/about/versions/android-4.0.jd index 1d81bc2d93cf..1ce005d7ae31 100644 --- a/docs/html/about/versions/android-4.0.jd +++ b/docs/html/about/versions/android-4.0.jd @@ -54,8 +54,8 @@ class="toggle-content-img" alt="" /> <strong>Table of Contents</strong> </a></p> - <div class="toggle-content-toggleme" style="padding-left:2em;"> - <ol class="toc" style="margin-left:-1em"> + <div class="toggle-content-toggleme" style="padding: 5px 18px"> + <ol> <li><a href="#Contacts">Social APIs in Contacts Provider</a></li> <li><a href="#Calendar">Calendar Provider</a></li> <li><a href="#Voicemail">Voicemail Provider</a></li> diff --git a/docs/html/about/versions/android-4.3.jd b/docs/html/about/versions/android-4.3.jd new file mode 100644 index 000000000000..0ca3bc658814 --- /dev/null +++ b/docs/html/about/versions/android-4.3.jd @@ -0,0 +1,1163 @@ +page.title=Android 4.3 APIs +excludeFromSuggestions=true +sdk.platform.version=4.3 +sdk.platform.apiLevel=18 +@jd:body + + +<div id="qv-wrapper"> +<div id="qv"> + +<h2>In this document + <a href="#" onclick="hideNestedItems('#toc43',this);return false;" class="header-toggle"> + <span class="more">show more</span> + <span class="less" style="display:none">show less</span></a></h2> + +<ol id="toc43" class="hide-nested"> + <li><a href="#ApiLevel">Update your target API level</a></li> + <li><a href="#Behaviors">Important Behavior Changes</a> + <ol> + <li><a href="#BehaviorsIntents">If your app uses implicit intents...</a></li> + <li><a href="#BehaviorsAccounts">If your app depends on accounts...</a></li> + </ol> + </li> + <li><a href="#RestrictedProfiles">Restricted Profiles</a> + <ol> + <li><a href="#AccountsInProfile">Supporting accounts in a restricted profile</a></li> + </ol> + </li> + <li><a href="#Wireless">Wireless and Connectivity</a> + <ol> + <li><a href="#BTLE">Bluetooth Low Energy (Smart Ready)</a></li> + <li><a href="#WiFiScan">Wi-Fi scan-only mode</a></li> + <li><a href="#WiFiConfig">Wi-Fi configuration</a></li> + <li><a href="#QuickResponse">Quick response for incoming calls</a></li> + </ol> + </li> + <li><a href="#Multimedia">Multimedia</a> + <ol> + <li><a href="#DASH">MPEG DASH support</a></li> + <li><a href="#DRM">Media DRM</a></li> + <li><a href="#EncodingSurface">Video encoding from a Surface</a></li> + <li><a href="#MediaMuxing">Media muxing</a></li> + <li><a href="#ProgressAndScrubbing">Playback progress and scrubbing for RemoteControlClient</a></li> + </ol> + </li> + <li><a href="#Graphics">Graphics</a> + <ol> + <li><a href="#OpenGL">Support for OpenGL ES 3.0</a></li> + <li><a href="#MipMap">Mipmapping for drawables</a></li> + </ol> + </li> + <li><a href="#UI">User Interface</a> + <ol> + <li><a href="#ViewOverlay">View overlays</a></li> + <li><a href="#OpticalBounds">Optical bounds layout</a></li> + <li><a href="#AnimationRect">Animation for Rect values</a></li> + <li><a href="#AttachFocus">Window attach and focus listener</a></li> + <li><a href="#Overscan">TV overscan support</a></li> + <li><a href="#Orientation">Screen orientation</a></li> + <li><a href="#RotationAnimation">Rotation animations</a></li> + </ol> + </li> + <li><a href="#UserInput">User Input</a> + <ol> + <li><a href="#SignificantMotion">Detect significant motion</a></li> + <li><a href="#Sensors">New sensor types</a></li> + </ol> + </li> + <li><a href="#NotificationListener">Notification Listener</a></li> + <li><a href="#Contacts">Contacts Provider</a> + <ol> + <li><a href="#Contactables">Query for "contactables"</a></li> + <li><a href="#ContactsDelta">Query for contacts deltas</a></li> + </ol> + </li> + <li><a href="#Localization">Localization</a> + <ol> + <li><a href="#BiDi">Improved support for bi-directional text</a></li> + </ol> + </li> + <li><a href="#A11yService">Accessibility Services</a> + <ol> + <li><a href="#A11yKeyEvents">Handle key events</a></li> + <li><a href="#A11yText">Select text and copy/paste</a></li> + <li><a href="#A11yFeatures">Declare accessibility features</a></li> + </ol> + </li> + <li><a href="#Testing">Testing and Debugging</a> + <ol> + <li><a href="#UiAutomation">Automated UI testing</a></li> + <li><a href="#Systrace">Systrace events for apps</a></li> + </ol> + </li> + <li><a href="#Security">Security</a> + <ol> + <li><a href="#KeyStore">Android key store for app-private keys</a></li> + <li><a href="#HardwareKeyChain">Hardware credential storage</a></li> + </ol> + </li> + <li><a href="#Manifest">Manifest Declarations</a> + <ol> + <li><a href="#ManifestFeatures">Declarable required features</a></li> + <li><a href="#ManifestPermissions">User permissions</a></li> + </ol> + </li> +</ol> + +<h2>See also</h2> +<ol> +<li><a href="{@docRoot}sdk/api_diff/18/changes.html">API +Differences Report »</a> </li> +<li><a +href="{@docRoot}tools/extras/support-library.html">Support Library</a></li> +</ol> + +</div> +</div> + + + +<p>API Level: {@sdkPlatformApiLevel}</p> + +<p>Android {@sdkPlatformVersion} ({@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR2}) +is an update to the Jelly Bean release that offers new features for users and app +developers. This document provides an introduction to the most notable +new APIs.</p> + +<p>As an app developer, you should download the Android {@sdkPlatformVersion} system image +and SDK platform from the <a href="{@docRoot}tools/help/sdk-manager.html">SDK Manager</a> as +soon as possible. If you don't have a device running Android {@sdkPlatformVersion} on which to +test your app, use the Android {@sdkPlatformVersion} system +image to test your app on the <a href="{@docRoot}tools/devices/emulator.html">Android emulator</a>. +Then build your apps against the Android {@sdkPlatformVersion} platform to begin using the +latest APIs.</p> + + +<h3 id="ApiLevel">Update your target API level</h3> + +<p>To better optimize your app for devices running Android {@sdkPlatformVersion}, + you should set your <a +href="{@docRoot}guide/topics/manifest/uses-sdk-element.html#target">{@code targetSdkVersion}</a> to +<code>"{@sdkPlatformApiLevel}"</code>, install it on an Android {@sdkPlatformVersion} system image, +test it, then publish an update with this change.</p> + +<p>You can use APIs in Android {@sdkPlatformVersion} while also supporting older versions by adding +conditions to your code that check for the system API level before executing +APIs not supported by your <a +href="{@docRoot}guide/topics/manifest/uses-sdk-element.html#min">{@code minSdkVersion}</a>. +To learn more about maintaining backward compatibility, read <a +href="{@docRoot}training/basics/supporting-devices/platforms.html">Supporting Different +Platform Versions</a>.</p> + +<p>Various APIs are also available in the Android <a +href="{@docRoot}tools/extras/support-library.html">Support Library</a> that allow you to implement +new features on older versions of the platform.</p> + +<p>For more information about how API levels work, read <a +href="{@docRoot}guide/topics/manifest/uses-sdk-element.html#ApiLevels">What is API +Level?</a></p> + + + + + +<h2 id="Behaviors">Important Behavior Changes</h2> + +<p>If you have previously published an app for Android, be aware that your app might +be affected by changes in Android {@sdkPlatformVersion}.</p> + + +<h3 id="BehaviorsIntents">If your app uses implicit intents...</h3> + +<p>Your app might misbehave in a restricted profile environment.</p> + +<p>Users in a <a href="#RestrictedProfiles">restricted profile</a> environment might not +have all the standard Android apps available. For example, a restricted profile might have the +web browser and camera app disabled. So your app should not make assumptions about which apps are +available, because if you call {@link android.app.Activity#startActivity startActivity()} without +verifying whether an app is available to handle the {@link android.content.Intent}, +your app might crash in a restricted profile.</p> + +<p>When using an implicit intent, you should always verify that an app is available to handle the intent by calling {@link android.content.Intent#resolveActivity resolveActivity()} or {@link android.content.pm.PackageManager#queryIntentActivities queryIntentActivities()}. For example:</p> + +<pre> +Intent intent = new Intent(Intent.ACTION_SEND); +... +if (intent.resolveActivity(getPackageManager()) != null) { + startActivity(intent); +} else { + Toast.makeText(context, R.string.app_not_available, Toast.LENGTH_LONG).show(); +} +</pre> + + +<h3 id="BehaviorsAccounts">If your app depends on accounts...</h3> + +<p>Your app might misbehave in a restricted profile environment.</p> + +<p>Users within a restricted profile environment do not have access to user accounts by default. +If your app depends on an {@link android.accounts.Account}, then your app might crash or behave +unexpectedly when used in a restricted profile.</p> + +<p>If you'd like to prevent restricted profiles from using your app entirely because your +app depends on account information that's sensitive, specify the <a +href="{@docRoot}guide/topics/manifest/application-element.html#requiredAccountType">{@code +android:requiredAccountType}</a> attribute in your manifest's <a +href="{@docRoot}guide/topics/manifest/application-element.html">{@code <application>}</a> +element.</p> + +<p>If you’d like to allow restricted profiles to continue using your app even though they can’t +create their own accounts, then you can either disable your app features that require an account +or allow restricted profiles to access the accounts created by the primary user. For more +information, see the section +below about <a href="#AccountsInProfile">Supporting accounts in a restricted profile</a>.</p> + + + + +<h2 id="RestrictedProfiles">Restricted Profiles</h2> + +<p>On Android tablets, users can now create restricted profiles based on the primary user. +When users create a restricted profile, they can enable restrictions such as which apps are +available to the profile. A new set of APIs in Android 4.3 also allow you to build fine-grain +restriction settings for the apps you develop. For example, by using the new APIs, you can +allow users to control what type of content is available within your app when running in a +restricted profile environment.</p> + +<p>The UI for users to control the restrictions you've built is managed by the system's +Settings application. To make your app's restriction settings appear to the user, +you must declare the restrictions your app provides by creating a {@link +android.content.BroadcastReceiver} that receives the {@link android.content.Intent#ACTION_GET_RESTRICTION_ENTRIES} intent. The system invokes this intent to query +all apps for available restrictions, then builds the UI to allow the primary user to +manage restrictions for each restricted profile. </p> + +<p>In the {@link android.content.BroadcastReceiver#onReceive onReceive()} method of +your {@link android.content.BroadcastReceiver}, you must create a {@link +android.content.RestrictionEntry} for each restriction your app provides. Each {@link +android.content.RestrictionEntry} defines a restriction title, description, and one of the +following data types:</p> + +<ul> + <li>{@link android.content.RestrictionEntry#TYPE_BOOLEAN} for a restriction that is + either true or false. + <li>{@link android.content.RestrictionEntry#TYPE_CHOICE} for a restriction that has + multiple choices that are mutually exclusive (radio button choices). + <li>{@link android.content.RestrictionEntry#TYPE_MULTI_SELECT} for a restriction that + has multiple choices that are <em>not</em> mutually exclusive (checkbox choices). +</ul> + +<p>You then put all the {@link android.content.RestrictionEntry} objects into an {@link +java.util.ArrayList} and put it into the broadcast receiver's result as the value for the +{@link android.content.Intent#EXTRA_RESTRICTIONS_LIST} extra.</p> + +<p>The system creates the UI for your app's restrictions in the Settings app and saves each +restriction with the unique key you provided for each {@link android.content.RestrictionEntry} +object. When the user opens your app, you can query for any current restrictions by +calling {@link android.os.UserManager#getApplicationRestrictions getApplicationRestrictions()}. +This returns a {@link android.os.Bundle} containing the key-value pairs for each restriction +you defined with the {@link android.content.RestrictionEntry} objects.</p> + +<p>If you want to provide more specific restrictions that can't be handled by boolean, single +choice, and multi-choice values, then you can create an activity where the user can specify the +restrictions and allow users to open that activity from the restriction settings. In your +broadcast receiver, include the {@link android.content.Intent#EXTRA_RESTRICTIONS_INTENT} extra +in the result {@link android.os.Bundle}. This extra must specify an {@link android.content.Intent} +indicating the {@link android.app.Activity} class to launch (use the +{@link android.os.Bundle#putParcelable putParcelable()} method to pass {@link +android.content.Intent#EXTRA_RESTRICTIONS_INTENT} with the intent). +When the primary user enters your activity to set custom restrictions, your +activity must then return a result containing the restriction values in an extra using either +the {@link android.content.Intent#EXTRA_RESTRICTIONS_LIST} or {@link +android.content.Intent#EXTRA_RESTRICTIONS_BUNDLE} key, depending on whether you specify +{@link android.content.RestrictionEntry} objects or key-value pairs, respectively.</p> + + +<h3 id="AccountsInProfile">Supporting accounts in a restricted profile</h3> + +<p>Any accounts added to the primary user are available to a restricted profile, but the +accounts are not accessible from the {@link android.accounts.AccountManager} APIs by default. +If you attempt to add an account with {@link android.accounts.AccountManager} while in a restricted +profile, you will get a failure result. Due to these restrictions, you have the following +three options:</p> + +<li><strong>Allow access to the owner’s accounts from a restricted profile.</strong> +<p>To get access to an account from a restricted profile, you must add the <a href="{@docRoot}guide/topics/manifest/application-element.html#restrictedAccountType">{@code android:restrictedAccountType}</a> attribute to the <a +href="{@docRoot}guide/topics/manifest/application-element.html"><application></a> tag:</p> +<pre> +<application ... + android:restrictedAccountType="com.example.account.type" > +</pre> + +<p class="caution"><strong>Caution:</strong> Enabling this attribute provides your +app access to the primary user's accounts from restricted profiles. So you should allow this +only if the information displayed by your app does not reveal personally identifiable +information (PII) that’s considered sensitive.</p> +</li> + + +<li><strong>Disable certain functionality when unable to modify accounts.</strong> +<p>If you want to use accounts, but don’t actually require them for your app’s primary +functionality, you can check for account availability and disable features when not available. +You should first check if there is an existing account available. If not, then query whether +it’s possible to create a new account by calling {@link +android.os.UserManager#getUserRestrictions()} and check the {@link +android.os.UserManager#DISALLOW_MODIFY_ACCOUNTS} extra in the result. If it is {@code true}, +then you should disable whatever functionality of your app requires access to accounts. +For example:</p> +<pre> +UserManager um = (UserManager) context.getSystemService(Context.USER_SERVICE); +Bundle restrictions = um.getUserRestrictions(); +if (restrictions.getBoolean(UserManager.DISALLOW_MODIFY_ACCOUNTS, false)) { + // cannot add accounts, disable some functionality +} +</pre> +<p class="note"><strong>Note:</strong> In this scenario, you should <em>not</em> declare +any new attributes in your manifest file.</p> +</li> + +<li><strong>Disable your app when unable to access private accounts.</strong> +<p>If it’s instead important that your app not be available to restricted profiles because +your app depends on sensitive personal information in an account (and because restricted profiles +currently cannot add new accounts), add +the <a href="{@docRoot}guide/topics/manifest/application-element.html#requiredAccountType">{@code +android:requiredAccountType}</a> attribute to the <a +href="{@docRoot}guide/topics/manifest/application-element.html"><application></a> tag:</p> +<pre> +<application ... + android:requiredAccountType="com.example.account.type" > +</pre> +<p>For example, the Gmail app uses this attribute to disable itself for restricted profiles, +because the owner's personal email should not be available to restricted profiles.</p> +</li> + + + +<h2 id="Wireless">Wireless and Connectivity</h2> + + +<h3 id="BTLE">Bluetooth Low Energy (Smart Ready)</h3> + +<p>Android now supports Bluetooth Low Energy (LE) with new APIs in {@link android.bluetooth}. +With the new APIs, you can build Android apps that communicate with Bluetooth Low Energy +peripherals such as heart rate monitors and pedometers.</p> + +<p>Because Bluetooth LE is a hardware feature that is not available on all +Android-powered devices, you must declare in your manifest file a <a +href="{@docRoot}guide/topics/manifest/uses-feature-element.html">{@code <uses-feature>}</a> +element for {@code "android.hardware.bluetooth_le"}:</p> +<pre> +<uses-feature android:name="android.hardware.bluetooth_le" android:required="true" /> +</pre> + +<p>If you're already familiar with Android's Classic Bluetooth APIs, notice that using the +Bluetooth LE APIs has some differences. Most importantly is that there's now a {@link +android.bluetooth.BluetoothManager} class that you should use for some high level operations +such as acquiring a {@link android.bluetooth.BluetoothAdapter}, getting a list of connected +devices, and checking the state of a device. For example, here's how you should now get the +{@link android.bluetooth.BluetoothAdapter}:</p> +<pre> +final BluetoothManager bluetoothManager = + (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE); +mBluetoothAdapter = bluetoothManager.getAdapter(); +</pre> + +<p>To discover Bluetooth LE peripherals, call {@link android.bluetooth.BluetoothAdapter#startLeScan +startLeScan()} on the {@link android.bluetooth.BluetoothAdapter}, passing it an implementation +of the {@link android.bluetooth.BluetoothAdapter.LeScanCallback} interface. When the Bluetooth +adapter detects a Bluetooth LE peripheral, your {@link +android.bluetooth.BluetoothAdapter.LeScanCallback} implementation receives a call to the +{@link android.bluetooth.BluetoothAdapter.LeScanCallback#onLeScan onLeScan()} method. This +method provides you with a {@link android.bluetooth.BluetoothDevice} object representing the +detected device, the RSSI value for the device, and a byte array containing the device's +advertisement record.</p> + +<p>If you want to scan for only specific types of peripherals, you can instead call {@link +android.bluetooth.BluetoothAdapter#startLeScan startLeScan()} and include an array of {@link +java.util.UUID} objects that specify the GATT services your app supports.</p> + +<p class="note"><strong>Note:</strong> You can only scan for Bluetooth LE devices <em>or</em> +scan for Classic Bluetooth devices using previous APIs. You cannot scan for both LE and Classic +Bluetooth devices at once.</p> + +<p>To then connect to a Bluetooth LE peripheral, call {@link +android.bluetooth.BluetoothDevice#connectGatt connectGatt()} on the corresponding +{@link android.bluetooth.BluetoothDevice} object, passing it an implementation of +{@link android.bluetooth.BluetoothGattCallback}. Your implementation of {@link +android.bluetooth.BluetoothGattCallback} receives callbacks regarding the connectivity +state with the device and other events. It's during the {@link +android.bluetooth.BluetoothGattCallback#onConnectionStateChange onConnectionStateChange()} +callback that you can begin communicating with the device if the method passes {@link +android.bluetooth.BluetoothProfile#STATE_CONNECTED} as the new state.</p> + +<p>Accessing Bluetooth features on a device also requires that your app request certain +Bluetooth user permissions. For more information, see the <a +href="{@docRoot}guide/topics/connectivity/bluetooth-le.html">Bluetooth Low Energy</a> API guide.</p> + + +<h3 id="WiFiScan">Wi-Fi scan-only mode</h3> + +<p>When attempting to identify the user's location, Android may use Wi-Fi to help determine +the location by scanning nearby access points. However, users often keep Wi-Fi turned off to +conserve battery, resulting in location data that's less accurate. Android now includes a +scan-only mode that allows the device Wi-Fi to scan access points to help obtain the location +without connecting to an access point, thus greatly reducing battery usage.</p> + +<p>If you want to acquire the user's location but Wi-Fi is currently off, you can request the +user to enable Wi-Fi scan-only mode by calling {@link android.content.Context#startActivity +startActivity()} with the action {@link +android.net.wifi.WifiManager#ACTION_REQUEST_SCAN_ALWAYS_AVAILABLE}.</p> + + +<h3 id="WiFiConfig">Wi-Fi configuration</h3> + +<p>New {@link android.net.wifi.WifiEnterpriseConfig} APIs allow enterprise-oriented services to +automate Wi-Fi configuration for managed devices.</p> + + +<h3 id="QuickResponse">Quick response for incoming calls</h3> + +<p>Since Android 4.0, a feature called "Quick response" allows users to respond to incoming +calls with an immediate text message without needing to pick up the call or unlock the device. +Until now, these quick messages were always handled by the default Messaging app. Now any app +can declare its capability to handle these messages by creating a {@link android.app.Service} +with an intent filter for {@link android.telephony.TelephonyManager#ACTION_RESPOND_VIA_MESSAGE}.</p> + +<p>When the user responds to an incoming call with a quick response, the Phone app sends +the {@link android.telephony.TelephonyManager#ACTION_RESPOND_VIA_MESSAGE} intent with a URI +describing the recipient (the caller) and the {@link android.content.Intent#EXTRA_TEXT} extra +with the message the user wants to send. When your service receives the intent, it should deliver +the message and immediately stop itself (your app should not show an activity).</p> + +<p>In order to receive this intent, you must declare the {@link +android.Manifest.permission#SEND_RESPOND_VIA_MESSAGE} permission.</p> + + + +<h2 id="Multimedia">Multimedia</h2> + +<h3 id="DASH">MPEG DASH support</h3> + +<p>Android now supports Dynamic Adaptive Streaming over HTTP (DASH) in accordance with the +ISO/IEC 23009-1 standard, using existing APIs in {@link android.media.MediaCodec} and {@link +android.media.MediaExtractor}. The framework underlying these APIs has been updated to support +parsing of fragmented MP4 files, but your app is still responsible for parsing the MPD metadata +and passing the individual streams to {@link android.media.MediaExtractor}.</p> + +<p>If you want to use DASH with encrypted content, notice that the {@link android.media.MediaExtractor#getSampleCryptoInfo getSampleCryptoInfo()} method returns the {@link +android.media.MediaCodec.CryptoInfo} metadata describing the structure of each encrypted media +sample. Also, the {@link android.media.MediaExtractor#getPsshInfo()} method has been added to +{@link android.media.MediaExtractor} so you can access the PSSH metadata for your DASH media. +This method returns a map of {@link java.util.UUID} objects to bytes, with the +{@link java.util.UUID} specifying the crypto scheme, and the bytes being the data specific +to that scheme.</p> + + +<h3 id="DRM">Media DRM</h3> + +<p>The new {@link android.media.MediaDrm} class provides a modular solution for digital rights +management (DRM) with your media content by separating DRM concerns from media playback. For +instance, this API separation allows you to play back Widevine-encrypted content without having +to use the Widevine media format. This DRM solution also supports DASH Common Encryption so you +can use a variety of DRM schemes with your streaming content.</p> + +<p>You can use {@link android.media.MediaDrm} to obtain opaque key-request messages and process +key-response messages from the server for license acquisition and provisioning. Your app is +responsible for handling the network communication with the servers; the {@link +android.media.MediaDrm} class provides only the ability to generate and process the messages.</p> + +<p>The {@link android.media.MediaDrm} APIs are intended to be used in conjunction with the +{@link android.media.MediaCodec} APIs that were introduced in Android 4.1 (API level 16), +including {@link android.media.MediaCodec} for encoding and decoding your content, {@link +android.media.MediaCrypto} for handling encrypted content, and {@link android.media.MediaExtractor} +for extracting and demuxing your content.</p> + +<p>You must first construct {@link android.media.MediaExtractor} and +{@link android.media.MediaCodec} objects. You can then access the DRM-scheme-identifying +{@link java.util.UUID}, typically from metadata in the content, and use it to construct an +instance of a {@link android.media.MediaDrm} object with its constructor.</p> + + +<h3 id="EncodingSurface">Video encoding from a Surface</h3> + +<p>Android 4.1 (API level 16) added the {@link android.media.MediaCodec} class for low-level +encoding and decoding of media content. When encoding video, Android 4.1 required that you provide +the media with a {@link java.nio.ByteBuffer} array, but Android 4.3 now allows you to use a {@link +android.view.Surface} as the input to an encoder. For instance, this allows you to encode input +from an existing video file or using frames generated from OpenGL ES.</p> + +<p>To use a {@link android.view.Surface} as the input to your encoder, first call {@link +android.media.MediaCodec#configure configure()} for your {@link android.media.MediaCodec}. +Then call {@link android.media.MediaCodec#createInputSurface()} to receive the {@link +android.view.Surface} upon which you can stream your media.</p> + +<p>For example, you can use the given {@link android.view.Surface} as the window for an OpenGL +context by passing it to {@link android.opengl.EGL14#eglCreateWindowSurface +eglCreateWindowSurface()}. Then while rendering the surface, call {@link +android.opengl.EGL14#eglSwapBuffers eglSwapBuffers()} to pass the frame to the {@link +android.media.MediaCodec}.</p> + +<p>To begin encoding, call {@link android.media.MediaCodec#start()} on the {@link +android.media.MediaCodec}. When done, call {@link android.media.MediaCodec#signalEndOfInputStream} +to terminate encoding, and call {@link android.view.Surface#release()} on the +{@link android.view.Surface}.</p> + + +<h3 id="MediaMuxing">Media muxing</h3> + +<p>The new {@link android.media.MediaMuxer} class enables multiplexing between one audio stream +and one video stream. These APIs serve as a counterpart to the {@link android.media.MediaExtractor} +class added in Android 4.2 for de-multiplexing (demuxing) media.</p> + +<p>Supported output formats are defined in {@link android.media.MediaMuxer.OutputFormat}. Currently, +MP4 is the only supported output format and {@link android.media.MediaMuxer} currently supports +only one audio stream and/or one video stream at a time.</p> + +<p>{@link android.media.MediaMuxer} is mostly designed to work with {@link android.media.MediaCodec} +so you can perform video processing through {@link android.media.MediaCodec} then save the +output to an MP4 file through {@link android.media.MediaMuxer}. You can also use {@link +android.media.MediaMuxer} in combination with {@link android.media.MediaExtractor} to perform +media editing without the need to encode or decode.</p> + + +<h3 id="ProgressAndScrubbing">Playback progress and scrubbing for RemoteControlClient</h3> + +<p>In Android 4.0 (API level 14), the {@link android.media.RemoteControlClient} was added to +enable media playback controls from remote control clients such as the controls available on the +lock screen. Android 4.3 now provides the ability for such controllers to display the playback +position and controls for scrubbing the playback. If you've enabled remote control for your +media app with the {@link android.media.RemoteControlClient} APIs, then you can allow playback +scrubbing by implementing two new interfaces.</p> + +<p>First, you must enable the {@link +android.media.RemoteControlClient#FLAG_KEY_MEDIA_POSITION_UPDATE} flag by passing it to +{@link android.media.RemoteControlClient#setTransportControlFlags setTransportControlsFlags()}.</p> + +<p>Then implement the following two new interfaces:</p> +<dl> + <dt>{@link android.media.RemoteControlClient.OnGetPlaybackPositionListener}</dt> + <dd>This includes the callback {@link android.media.RemoteControlClient.OnGetPlaybackPositionListener#onGetPlaybackPosition}, which requests the current position + of your media when the remote control needs to update the progress in its UI.</dd> + + <dt>{@link android.media.RemoteControlClient.OnPlaybackPositionUpdateListener}</dt> + <dd>This includes the callback {@link android.media.RemoteControlClient.OnPlaybackPositionUpdateListener#onPlaybackPositionUpdate onPlaybackPositionUpdate()}, which + tells your app the new time code for your media when the user scrubs the playback with the + remote control UI. + <p>Once you update your playback with the new position, call {@link + android.media.RemoteControlClient#setPlaybackState setPlaybackState()} to indicate the + new playback state, position, and speed.</p> + </dd> +</dl> + +<p>With these interfaces defined, you can set them for your {@link +android.media.RemoteControlClient} by calling {@link android.media.RemoteControlClient#setOnGetPlaybackPositionListener setOnGetPlaybackPositionListener()} and +{@link android.media.RemoteControlClient#setPlaybackPositionUpdateListener +setPlaybackPositionUpdateListener()}, respectively.</p> + + + +<h2 id="Graphics">Graphics</h2> + +<h3 id="OpenGL">Support for OpenGL ES 3.0</h3> + +<p>Android 4.3 adds Java interfaces and native support for OpenGL ES 3.0. Key new functionality +provided in OpenGL ES 3.0 includes:</p> +<ul> + <li>Acceleration of advanced visual effects</li> + <li>High quality ETC2/EAC texture compression as a standard feature</li> + <li>A new version of the GLSL ES shading language with integer and 32-bit floating point support</li> + <li>Advanced texture rendering</li> + <li>Broader standardization of texture size and render-buffer formats</li> +</ul> + +<p>The Java interface for OpenGL ES 3.0 on Android is provided with {@link android.opengl.GLES30}. +When using OpenGL ES 3.0, be sure that you declare it in your manifest file with the +<a href="{@docRoot}guide/topics/manifest/uses-feature-element.html"><uses-feature></a> +tag and the {@code android:glEsVersion} attribute. For example:</p> +<pre> +<manifest> + <uses-feature android:glEsVersion="0x00030000" /> + ... +</manifest> +</pre> + +<p>And remember to specify the OpenGL ES context by calling {@link android.opengl.GLSurfaceView#setEGLContextClientVersion setEGLContextClientVersion()}, passing {@code 3} as the version.</p> + + +<h3 id="MipMap">Mipmapping for drawables</h3> + +<p>Using a mipmap as the source for your bitmap or drawable is a simple way to provide a +quality image and various image scales, which can be particularly useful if you expect your +image to be scaled during an animation.</p> + +<p>Android 4.2 (API level 17) added support for mipmaps in the {@link android.graphics.Bitmap} +class—Android swaps the mip images in your {@link android.graphics.Bitmap} when you've +supplied a mipmap source and have enabled {@link android.graphics.Bitmap#setHasMipMap +setHasMipMap()}. Now in Android 4.3, you can enable mipmaps for a {@link +android.graphics.drawable.BitmapDrawable} object as well, by providing a mipmap asset and +setting the {@code android:mipMap} attribute in a bitmap resource file or by calling {@link +android.graphics.drawable.BitmapDrawable#hasMipMap hasMipMap()}. +</p> + + + +<h2 id="UI">User Interface</h2> + +<h3 id="ViewOverlay">View overlays</h3> + +<p>The new {@link android.view.ViewOverlay} class provides a transparent layer on top of +a {@link android.view.View} on which you can add visual content and which does not affect +the layout hierarchy. You can get a {@link android.view.ViewOverlay} for any {@link +android.view.View} by calling {@link android.view.View#getOverlay}. The overlay +always has the same size and position as its host view (the view from which it was created), +allowing you to add content that appears in front of the host view, but which cannot extend +the bounds of that host view. +</p> + +<p>Using a {@link android.view.ViewOverlay} is particularly useful when you want to create +animations such as sliding a view outside of its container or moving items around the screen +without affecting the view hierarchy. However, because the usable area of an overlay is +restricted to the same area as its host view, if you want to animate a view moving outside +its position in the layout, you must use an overlay from a parent view that has the desired +layout bounds.</p> + +<p>When you create an overlay for a widget view such as a {@link android.widget.Button}, you +can add {@link android.graphics.drawable.Drawable} objects to the overlay by calling +{@link android.view.ViewOverlay#add(Drawable)}. If you call {@link +android.view.ViewGroup#getOverlay} for a layout view, such as {@link android.widget.RelativeLayout}, +the object returned is a {@link android.view.ViewGroupOverlay}. The +{@link android.view.ViewGroupOverlay} class is a subclass +of {@link android.view.ViewOverlay} that also allows you to add {@link android.view.View} +objects by calling {@link android.view.ViewGroupOverlay#add(View)}. +</p> + +<p class="note"><strong>Note:</strong> All drawables and views that you add to an overlay +are visual only. They cannot receive focus or input events.</p> + +<p>For example, the following code animates a view sliding to the right by placing the view +in the parent view's overlay, then performing a translation animation on that view:</p> +<pre> +View view = findViewById(R.id.view_to_remove); +ViewGroup container = (ViewGroup) view.getParent(); +container.getOverlay().add(view); +ObjectAnimator anim = ObjectAnimator.ofFloat(view, "translationX", container.getRight()); +anim.start(); +</pre> + + +<h3 id="OpticalBounds">Optical bounds layout</h3> + +<p>For views that contain nine-patch background images, you can now specify that they should +be aligned with neighboring views based on the "optical" bounds of the background image rather +than the "clip" bounds of the view.</p> + +<p>For example, figures 1 and 2 each show the same layout, but the version in figure 1 is +using clip bounds (the default behavior), while figure 2 is using optical bounds. Because the +nine-patch images used for the button and the photo frame include padding around the edges, +they don’t appear to align with each other or the text when using clip bounds.</p> + +<p class="note"><strong>Note:</strong> The screenshot in figures 1 and 2 have the "Show +layout bounds" developer setting enabled. For each view, red lines indicate the optical +bounds, blue lines indicate the clip bounds, and pink indicates margins.</p> + +<script type="text/javascript"> +function toggleOpticalImages(mouseover) { + + $("img.optical-img").each(function() { + $img = $(this); + var index = $img.attr('src').lastIndexOf("/") + 1; + var path = $img.attr('src').substr(0,index); + var name = $img.attr('src').substr(index); + var splitname; + var highres = false; + if (name.indexOf("@2x") != -1) { + splitname = name.split("@2x."); + highres = true; + } else { + splitname = name.split("."); + } + + var newname; + if (mouseover) { + if (highres) { + newname = splitname[0] + "-normal@2x.png"; + } else { + newname = splitname[0] + "-normal.png"; + } + } else { + if (highres) { + newname = splitname[0].split("-normal")[0] + "@2x.png"; + } else { + newname = splitname[0].split("-normal")[0] + ".png"; + } + } + + $img.attr('src', path + newname); + + }); +} +</script> + +<p class="table-caption"><em>Mouse over to hide the layout bounds.</em></p> +<div style="float:left;width:296px"> +<img src="{@docRoot}images/tools/clipbounds@2x.png" width="296" alt="" class="optical-img" + onmouseover="toggleOpticalImages(true)" onmouseout="toggleOpticalImages(false)" /> +<p class="img-caption"><strong>Figure 1.</strong> Layout using clip bounds (default).</p> +</div> +<div style="float:left;width:296px;margin-left:60px"> +<img src="{@docRoot}images/tools/opticalbounds@2x.png" width="296" alt="" class="optical-img" + onmouseover="toggleOpticalImages(true)" onmouseout="toggleOpticalImages(false)" /> +<p class="img-caption"><strong>Figure 2.</strong> Layout using optical bounds.</p> +</div> + + +<p style="clear:left">To align the views based on their optical bounds, set the {@code android:layoutMode} attribute to {@code "opticalBounds"} in one of the parent layouts. For example:</p> + +<pre> +<LinearLayout android:layoutMode="opticalBounds" ... > +</pre> + + +<div class="figure" style="width:155px"> +<img src="{@docRoot}images/tools/ninepatch_opticalbounds@2x.png" width="121" alt="" /> +<p class="img-caption"><strong>Figure 3.</strong> Zoomed view of the Holo button nine-patch with +optical bounds. +</p> +</div> + +<p>For this to work, the nine-patch images applied to the background of your views must specify +the optical bounds using red lines along the bottom and right-side of the nine-patch file (as +shown in figure 3). The red lines indicate the region that should be subtracted from +the clip bounds, leaving the optical bounds of the image.</p> + +<p>When you enable optical bounds for a {@link android.view.ViewGroup} in your layout, all +descendant views inherit the optical bounds layout mode unless you override it for a group by +setting {@code android:layoutMode} to {@code "clipBounds"}. All layout elements also honor the +optical bounds of their child views, adapting their own bounds based on the optical bounds of +the views within them. However, layout elements (subclasses of {@link android.view.ViewGroup}) +currently do not support optical bounds for nine-patch images applied to their own background.</p> + +<p>If you create a custom view by subclassing {@link android.view.View}, {@link android.view.ViewGroup}, or any subclasses thereof, your view will inherit these optical bound behaviors.</p> + +<p class="note"><strong>Note:</strong> All widgets supported by the Holo theme have been updated +with optical bounds, including {@link android.widget.Button}, {@link android.widget.Spinner}, +{@link android.widget.EditText}, and others. So you can immediately benefit by setting the +{@code android:layoutMode} attribute to {@code "opticalBounds"} if your app applies a Holo theme +({@link android.R.style#Theme_Holo Theme.Holo}, {@link android.R.style#Theme_Holo_Light +Theme.Holo.Light}, etc.). +</p> + +<p>To specify optical bounds for your own nine-patch images with the <a +href="{@docRoot}tools/help/draw9patch.html">Draw 9-patch</a> tool, hold CTRL when clicking on +the border pixels.</p> + + + + +<h3 id="AnimationRect">Animation for Rect values</h3> + +<p>You can now animate between two {@link android.graphics.Rect} values with the new {@link +android.animation.RectEvaluator}. This new class is an implementation of {@link +android.animation.TypeEvaluator} that you can pass to {@link +android.animation.ValueAnimator#setEvaluator ValueAnimator.setEvaluator()}. +</p> + +<h3 id="AttachFocus">Window attach and focus listener</h3> + +<p>Previously, if you wanted to listen for when your view attached/detached to the window or +when its focus changed, you needed to override the {@link android.view.View} class to +implement {@link android.view.View#onAttachedToWindow onAttachedToWindow()} and {@link +android.view.View#onDetachedFromWindow onDetachedFromWindow()}, or {@link +android.view.View#onWindowFocusChanged onWindowFocusChanged()}, respectively. +</p> + +<p>Now, to receive attach and detach events you can instead implement {@link +android.view.ViewTreeObserver.OnWindowAttachListener} and set it on a view with +{@link android.view.ViewTreeObserver#addOnWindowAttachListener addOnWindowAttachListener()}. +And to receive focus events, you can implement {@link +android.view.ViewTreeObserver.OnWindowFocusChangeListener} and set it on a view with +{@link android.view.ViewTreeObserver#addOnWindowFocusChangeListener +addOnWindowFocusChangeListener()}. +</p> + + +<h3 id="Overscan">TV overscan support</h3> + +<p>To be sure your app fills the entire screen on every television, you can now enable overscan +for you app layout. Overscan mode is determined by the {@link android.view.WindowManager.LayoutParams#FLAG_LAYOUT_IN_OVERSCAN} flag, which you can enable with platform themes such as +{@link android.R.style#Theme_DeviceDefault_NoActionBar_Overscan} or by enabling the +{@link android.R.attr#windowOverscan} style in a custom theme.</p> + + +<h3 id="Orientation">Screen orientation</h3> + +<p>The <a +href="{@docRoot}guide/topics/manifest/activity-element.html">{@code <activity>}</a> +tag's <a +href="{@docRoot}guide/topics/manifest/activity-element.html#screen">{@code screenOrientation}</a> +attribute now supports additional values to honor the user's preference for auto-rotation:</p> +<dl> +<dt>{@code "userLandscape"}</dt> +<dd>Behaves the same as {@code "sensorLandscape"}, except if the user disables auto-rotate +then it locks in the normal landscape orientation and will not flip. +</dd> + +<dt>{@code "userPortrait"}</dt> +<dd>Behaves the same as {@code "sensorPortrait"}, except if the user disables auto-rotate then +it locks in the normal portrait orientation and will not flip. +</dd> + +<dt>{@code "fullUser"}</dt> +<dd>Behaves the same as {@code "fullSensor"} and allows rotation in all four directions, except +if the user disables auto-rotate then it locks in the user's preferred orientation. +</dd></dl> + +<p>Additionally, you can now also declare {@code "locked"} to lock your app's orientation into +the screen's current orientation.</p> + + +<h3 id="RotationAnimation">Rotation animations</h3> + +<p>The new {@link android.view.WindowManager.LayoutParams#rotationAnimation} field in +{@link android.view.WindowManager} allows you to select between one of three animations you +want to use when the system switches screen orientations. The three animations are:</p> +<ul> + <li>{@link android.view.WindowManager.LayoutParams#ROTATION_ANIMATION_CROSSFADE}</li> + <li>{@link android.view.WindowManager.LayoutParams#ROTATION_ANIMATION_JUMPCUT}</li> + <li>{@link android.view.WindowManager.LayoutParams#ROTATION_ANIMATION_ROTATE}</li> +</ul> + +<p class="note"><strong>Note:</strong> These animations are available only if you've set your activity to use "fullscreen" mode, which you can enable with themes such as {@link android.R.style#Theme_Holo_NoActionBar_Fullscreen Theme.Holo.NoActionBar.Fullscreen}.</p> + +<p>For example, here's how you can enable the "crossfade" animation:</p> +<pre> +protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + WindowManager.LayoutParams params = getWindow().getAttributes(); + params.rotationAnimation = WindowManager.LayoutParams.ROTATION_ANIMATION_CROSSFADE; + getWindow().setAttributes(params); + ... +} +</pre> + + +<h2 id="UserInput">User Input</h2> + +<h3 id="SignificantMotion">Detect significant motion</h3> + +<p>The {@link android.hardware.SensorManager} APIs now allow you to request a callback when the +device sensors detect "significant motion." For instance, this event may be triggered by new +motion such as when the user starts to walk.</p> + +<p>To register a listener for significant motion, extend the {@link android.hardware.TriggerEventListener} class and implement the {@link android.hardware.TriggerEventListener#onTrigger onTrigger()} callback method. Then register your event listener with the {@link android.hardware.SensorManager} by passing it to {@link android.hardware.SensorManager#requestTriggerSensor requestTriggerSensor()}, passing it your {@link android.hardware.TriggerEventListener} and {@link android.hardware.Sensor#TYPE_SIGNIFICANT_MOTION}.</p> + +<h3 id="Sensors">New sensor types</h3> +<p>The new {@link android.hardware.Sensor#TYPE_GAME_ROTATION_VECTOR} sensor allows you to detect the device's rotations without worrying about magnetic interferences. Unlike the {@link android.hardware.Sensor#TYPE_ROTATION_VECTOR} sensor, the {@link android.hardware.Sensor#TYPE_GAME_ROTATION_VECTOR} is not based on magnetic north.</p> + +<p>The new {@link android.hardware.Sensor#TYPE_GYROSCOPE_UNCALIBRATED} and {@link +android.hardware.Sensor#TYPE_MAGNETIC_FIELD_UNCALIBRATED} sensors provide raw sensor data without +consideration for bias estimations. That is, the existing {@link +android.hardware.Sensor#TYPE_GYROSCOPE} and {@link android.hardware.Sensor#TYPE_MAGNETIC_FIELD} +sensors provide sensor data that takes into account estimated bias from gyro-drift and hard iron +in the device, respectively. Whereas the new "uncalibrated" versions of these sensors instead provide +the raw sensor data and offer the estimated bias values separately. These sensors allow you to +provide your own custom calibration for the sensor data by enhancing the estimated bias with +external data.</p> + + + +<h2 id="NotificationListener">Notification Listener</h2> + +<p>Android 4.3 adds a new service class, {@link android.service.notification.NotificationListenerService}, that allows your app to receive information about new notifications as they are posted by the system. </p> + +<p>If your app currently uses the accessibility service APIs to access system notifications, you should update your app to use these APIs instead.</p> + + + + +<h2 id="Contacts">Contacts Provider</h2> + +<h3 id="Contactables">Query for "contactables"</h3> + +<p>The new Contacts Provider query, {@link android.provider.ContactsContract.CommonDataKinds.Contactables#CONTENT_URI Contactables.CONTENT_URI}, provides an efficient way to get one {@link android.database.Cursor} that contains all email addresses and phone numbers belonging to all contacts matching the specified query.</p> + + +<h3 id="ContactsDelta">Query for contacts deltas</h3> + +<p>New APIs have been added to Contacts Provider that allow you to efficiently query recent changes to the contacts data. Previously, your app could be notified when something in the contacts data changed, but you would not know exactly what changed and would need to retrieve all contacts then iterate through them to discover the change.</p> + +<p>To track changes to inserts and updates, you can now include the {@link android.provider.ContactsContract.ContactsColumns#CONTACT_LAST_UPDATED_TIMESTAMP} parameter with your selection to query only the contacts that have changed since the last time you queried the provider.</p> + +<p>To track which contacts have been deleted, the new table {@link android.provider.ContactsContract.DeletedContacts} provides a log of contacts that have been deleted (but each contact deleted is held in this table for a limited time). Similar to {@link android.provider.ContactsContract.ContactsColumns#CONTACT_LAST_UPDATED_TIMESTAMP}, you can use the new selection parameter, {@link android.provider.ContactsContract.DeletedContacts#CONTACT_DELETED_TIMESTAMP} to check which contacts have been deleted since the last time you queried the provider. The table also contains the constant {@link android.provider.ContactsContract.DeletedContacts#DAYS_KEPT_MILLISECONDS} containing the number of days (in milliseconds) that the log will be kept.</p> + +<p>Additionally, the Contacts Provider now broadcasts the {@link +android.provider.ContactsContract.Intents#CONTACTS_DATABASE_CREATED} action when the user +clears the contacts storage through the system settings menu, effectively recreating the +Contacts Provider database. It’s intended to signal apps that they need to drop all the contact +information they’ve stored and reload it with a new query.</p> + +<p>For sample code using these APIs to check for changes to the contacts, look in the ApiDemos +sample available in the <a href="{@docRoot}tools/samples/index.html">SDK Samples</a> download.</p> + + +<h2 id="Localization">Localization</h2> + +<h3 id="BiDi">Improved support for bi-directional text</h3> + +<p>Previous versions of Android support right-to-left (RTL) languages and layout, +but sometimes don't properly handle mixed-direction text. So Android 4.3 adds the {@link +android.text.BidiFormatter} APIs that help you properly format text with opposite-direction +content without garbling any parts of it.</p> + +<p>For example, when you want to create a sentence with a string variable, such as "Did you mean +15 Bay Street, Laurel, CA?", you normally pass a localized string resource and the variable to +{@link java.lang.String#format String.format()}:</p> +<pre> +Resources res = getResources(); +String suggestion = String.format(res.getString(R.string.did_you_mean), address); +</pre> + +<p>However, if the locale is Hebrew, then the formatted string comes out like this:</p> + +<p dir="rtl">האם התכוונת ל 15 Bay Street, Laurel, CA?</p> + +<p>That's wrong because the "15" should be left of "Bay Street." The solution is to use {@link +android.text.BidiFormatter} and its {@link android.text.BidiFormatter#unicodeWrap(String) +unicodeWrap()} method. For example, the code above becomes:</p> +<pre> +Resources res = getResources(); +BidiFormatter bidiFormatter = BidiFormatter.getInstance(); +String suggestion = String.format(res.getString(R.string.did_you_mean), + bidiFormatter.unicodeWrap(address)); +</pre> + +<p> +By default, {@link android.text.BidiFormatter#unicodeWrap(String) unicodeWrap()} uses the +first-strong directionality estimation heuristic, which can get things wrong if the first +signal for text direction does not represent the appropriate direction for the content as a whole. +If necessary, you can specify a different heuristic by passing one of the {@link +android.text.TextDirectionHeuristic} constants from {@link android.text.TextDirectionHeuristics} +to {@link android.text.BidiFormatter#unicodeWrap(String,TextDirectionHeuristic) unicodeWrap()}.</p> + +<p class="note"><strong>Note:</strong> These new APIs are also available for previous versions +of Android through the Android <a href="{@docRoot}tools/extras/support-library.html">Support +Library</a>, with the {@link android.support.v4.text.BidiFormatter} class and related APIs.</p> + + + +<h2 id="A11yService">Accessibility Services</h2> + +<h3 id="A11yKeyEvents">Handle key events</h3> + +<p>An {@link android.accessibilityservice.AccessibilityService} can now receive a callback for +key input events with the {@link android.accessibilityservice.AccessibilityService#onKeyEvent +onKeyEvent()} callback method. This allows your accessibility service to handle input for +key-based input devices such as a keyboard and translate those events to special actions that +previously may have been possible only with touch input or the device's directional pad.</p> + + +<h3 id="A11yText">Select text and copy/paste</h3> + +<p>The {@link android.view.accessibility.AccessibilityNodeInfo} now provides APIs that allow +an {@link android.accessibilityservice.AccessibilityService} to select, cut, copy, and paste +text in a node.</p> + +<p>To specify the selection of text to cut or copy, your accessibility service can use the new +action, {@link android.view.accessibility.AccessibilityNodeInfo#ACTION_SET_SELECTION}, passing +with it the selection start and end position with {@link +android.view.accessibility.AccessibilityNodeInfo#ACTION_ARGUMENT_SELECTION_START_INT} and {@link +android.view.accessibility.AccessibilityNodeInfo#ACTION_ARGUMENT_SELECTION_END_INT}. +Alternatively you can select text by manipulating the cursor position using the existing +action, {@link android.view.accessibility.AccessibilityNodeInfo#ACTION_NEXT_AT_MOVEMENT_GRANULARITY} +(previously only for moving the cursor position), and adding the argument {@link +android.view.accessibility.AccessibilityNodeInfo#ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN}.</p> + +<p>You can then cut or copy with {@link android.view.accessibility.AccessibilityNodeInfo#ACTION_CUT}, +{@link android.view.accessibility.AccessibilityNodeInfo#ACTION_COPY}, then later paste with +{@link android.view.accessibility.AccessibilityNodeInfo#ACTION_PASTE}.</p> + + +<p class="note"><strong>Note:</strong> These new APIs are also available for previous versions +of Android through the Android <a href="{@docRoot}tools/extras/support-library.html">Support +Library</a>, with the {@link android.support.v4.view.accessibility.AccessibilityNodeInfoCompat} +class.</p> + + + +<h3 id="A11yFeatures">Declare accessibility features</h3> + +<p>Beginning with Android 4.3, an accessibility service must declare accessibility capabilities +in its metadata file in order to use certain accessibility features. If the capability is not +requested in the metadata file, then the feature will be a no-op. To declare your service's +accessibility capabilities, you must use XML attributes that correspond to the various +"capability" constants in the {@link android.accessibilityservice.AccessibilityServiceInfo} +class.</p> + +<p>For example, if a service does not request the {@link android.R.styleable#AccessibilityService_canRequestFilterKeyEvents flagRequestFilterKeyEvents} capability, +then it will not receive key events.</p> + + +<h2 id="Testing">Testing and Debugging</h2> + +<h3 id="UiAutomation">Automated UI testing</h3> + +<p>The new {@link android.app.UiAutomation} class provides APIs that allow you to simulate user +actions for test automation. By using the platform's {@link +android.accessibilityservice.AccessibilityService} APIs, the {@link android.app.UiAutomation} +APIs allow you to inspect the screen content and inject arbitrary keyboard and touch events.</p> + +<p>To get an instance of {@link android.app.UiAutomation}, call {@link +android.app.Instrumentation#getUiAutomation Instrumentation.getUiAutomation()}. In order +for this to work, you must supply the {@code -w} option with the {@code instrument} command +when running your {@link android.test.InstrumentationTestCase} from <a +href="{@docRoot}tools/help/adb.html#am">{@code adb shell}</a>.</p> + +<p>With the {@link android.app.UiAutomation} instance, you can execute arbitrary events to test +your app by calling {@link android.app.UiAutomation#executeAndWaitForEvent +executeAndWaitForEvent()}, passing it a {@link java.lang.Runnable} to perform, a timeout +period for the operation, and an implementation of the {@link +android.app.UiAutomation.AccessibilityEventFilter} interface. It's within your {@link +android.app.UiAutomation.AccessibilityEventFilter} implementation that you'll receive a call +that allows you to filter the events that you're interested in and determine the success or +failure of a given test case.</p> + +<p>To observe all the events during a test, create an implementation of {@link +android.app.UiAutomation.OnAccessibilityEventListener} and pass it to {@link +android.app.UiAutomation#setOnAccessibilityEventListener setOnAccessibilityEventListener()}. +Your listener interface then receives a call to {@link +android.app.UiAutomation.OnAccessibilityEventListener#onAccessibilityEvent onAccessibilityEvent()} +each time an event occurs, receiving an {@link android.view.accessibility.AccessibilityEvent} object +that describes the event.</p> + +<p>There is a variety of other operations that the {@link android.app.UiAutomation} APIs expose +at a very low level to encourage the development of UI test tools such as <a href="{@docRoot}tools/help/uiautomator/index.html">uiautomator</a>. For instance, +{@link android.app.UiAutomation} can also:</p> +<ul> + <li>Inject input events + <li>Change the orientation of the screen + <li>Take screenshots +</ul> + +<p>And most importantly for UI test tools, the {@link android.app.UiAutomation} APIs work +across application boundaries, unlike those in {@link android.app.Instrumentation}.</p> + + +<h3 id="Systrace">Systrace events for apps</h3> + +<p>Android 4.3 adds the {@link android.os.Trace} class with two static methods, +{@link android.os.Trace#beginSection beginSection()} and {@link android.os.Trace#endSection()}, +which allow you to define blocks of code to include with the systrace report. By creating +sections of traceable code in your app, the systrace logs provide you a much more detailed +analysis of where slowdown occurs within your app.</p> + +<p>For information about using the Systrace tool, read <a href="{@docRoot}tools/debugging/systrace.html">Analyzing Display and Performance with Systrace</a>.</p> + + +<h2 id="Security">Security</h2> + +<h3 id="KeyStore">Android key store for app-private keys</h3> + +<p>Android now offers a custom Java Security Provider in the {@link java.security.KeyStore} +facility, called Android Key Store, which allows you to generate and save private keys that +may be seen and used by only your app. To load the Android Key Store, pass +{@code "AndroidKeyStore"} to {@link java.security.KeyStore#getInstance(String) +KeyStore.getInstance()}.</p> + +<p>To manage your app's private credentials in the Android Key Store, generate a new key with +{@link java.security.KeyPairGenerator} with {@link android.security.KeyPairGeneratorSpec}. First +get an instance of {@link java.security.KeyPairGenerator} by calling {@link +java.security.KeyPairGenerator#getInstance getInstance()}. Then call +{@link java.security.KeyPairGenerator#initialize initialize()}, passing it an instance of +{@link android.security.KeyPairGeneratorSpec}, which you can get using +{@link android.security.KeyPairGeneratorSpec.Builder KeyPairGeneratorSpec.Builder}. +Finally, get your {@link java.security.KeyPair} by calling {@link +java.security.KeyPairGenerator#generateKeyPair generateKeyPair()}.</p> + + +<h3 id="HardwareKeyChain">Hardware credential storage</h3> + +<p>Android also now supports hardware-backed storage for your {@link android.security.KeyChain} +credentials, providing more security by making the keys unavailable for extraction. That is, once +keys are in a hardware-backed key store (Secure Element, TPM, or TrustZone), they can be used for +cryptographic operations but the private key material cannot be exported. Even the OS kernel +cannot access this key material. While not all Android-powered devices support storage on +hardware, you can check at runtime if hardware-backed storage is available by calling +{@link android.security.KeyChain#isBoundKeyAlgorithm KeyChain.IsBoundKeyAlgorithm()}.</p> + + + +<h2 id="Manifest">Manifest Declarations</h2> + +<h3 id="ManifestFeatures">Declarable required features</h3> + +<p>The following values are now supported in the <a +href="{@docRoot}guide/topics/manifest/uses-feature-element.html">{@code <uses-feature>}</a> +element so you can ensure that your app is installed only on devices that provide the features +your app needs.</p> + +<dl> +<dt>{@link android.content.pm.PackageManager#FEATURE_APP_WIDGETS}</dt> +<dd>Declares that your app provides an app widget and should be installed only on devices that +include a Home screen or similar location where users can embed app widgets. +Example: +<pre> +<uses-feature android:name="android.software.app_widgets" android:required="true" /> +</pre> +</dd> + +<dt>{@link android.content.pm.PackageManager#FEATURE_HOME_SCREEN}</dt> +<dd>Declares that your app behaves as a Home screen replacement and should be installed only on +devices that support third-party Home screen apps. +Example: +<pre> +<uses-feature android:name="android.software.home_screen" android:required="true" /> +</pre> +</dd> + +<dt>{@link android.content.pm.PackageManager#FEATURE_INPUT_METHODS}</dt> +<dd>Declares that your app provides a custom input method (a keyboard built with {@link +android.inputmethodservice.InputMethodService}) and should be installed only on devices that +support third-party input methods. +Example: +<pre> +<uses-feature android:name="android.software.input_methods" android:required="true" /> +</pre> +</dd> + +<dt>{@link android.content.pm.PackageManager#FEATURE_BLUETOOTH_LE}</dt> +<dd>Declares that your app uses Bluetooth Low Energy APIs and should be installed only on devices +that are capable of communicating with other devices via Bluetooth Low Energy. +Example: +<pre> +<uses-feature android:name="android.software.bluetooth_le" android:required="true" /> +</pre> +</dd> +</dl> + + +<h3 id="ManifestPermissions">User permissions</h3> +<p>The following values are now supported in the <a +href="{@docRoot}guide/topics/manifest/uses-permission-element.html">{@code <uses-permission>}</a> +to declare the +permissions your app requires in order to access certain APIs.</p> + +<dl> +<dt>{@link android.Manifest.permission#BIND_NOTIFICATION_LISTENER_SERVICE} +</dt> +<dd>Required to use the new {@link android.service.notification.NotificationListenerService} APIs. +</dd> + +<dt>{@link android.Manifest.permission#SEND_RESPOND_VIA_MESSAGE}</dt> +<dd>Required to receive the {@link android.telephony.TelephonyManager#ACTION_RESPOND_VIA_MESSAGE} +intent.</dd> +</dl> + + + + +<p class="note">For a detailed view of all API changes in Android 4.3, see the +<a href="{@docRoot}sdk/api_diff/18/changes.html">API Differences Report</a>.</p> + + + diff --git a/docs/html/distribute/distribute_toc.cs b/docs/html/distribute/distribute_toc.cs index 907d2670a0e8..75cf9f93d13e 100644 --- a/docs/html/distribute/distribute_toc.cs +++ b/docs/html/distribute/distribute_toc.cs @@ -88,6 +88,7 @@ <ul> <li><a href="<?cs var:toroot ?>distribute/googleplay/edu/about.html">About</a></li> <li><a href="<?cs var:toroot ?>distribute/googleplay/edu/start.html">Get Started</a></li> + <li><a href="<?cs var:toroot ?>distribute/googleplay/edu/guidelines.html">Guidelines</a></li> <li><a href="<?cs var:toroot ?>distribute/googleplay/edu/contact.html">Sign Up</a></li> </ul> </li> diff --git a/docs/html/distribute/googleplay/edu/contact.jd b/docs/html/distribute/googleplay/edu/contact.jd index a302bc93cf02..804d925864cb 100644 --- a/docs/html/distribute/googleplay/edu/contact.jd +++ b/docs/html/distribute/googleplay/edu/contact.jd @@ -24,7 +24,7 @@ teachers and administrators buy, deploy, and use apps. </p> Whether you have an existing educational app or are developing a fresh idea that will unlock learning in the classroom — sign up to receive information about the upcoming launch of Google Play for Education. To get your apps ready, read our -<a href="{@docRoot}distribute/googleplay/edu/start.html">guidelines</a> for building +<a href="{@docRoot}distribute/googleplay/edu/guidelines.html">guidelines</a> for building educational apps.</p> </p><a href="http://developer.android.com/edu/signup">Developer Sign Up »</a> </div> diff --git a/docs/html/distribute/googleplay/edu/guidelines.jd b/docs/html/distribute/googleplay/edu/guidelines.jd new file mode 100644 index 000000000000..c1d30651db01 --- /dev/null +++ b/docs/html/distribute/googleplay/edu/guidelines.jd @@ -0,0 +1,252 @@ +page.title=Guidelines for Apps +page.metaDescription=Get your apps ready for Google Play for Education. +excludeFromSuggestions=true +@jd:body + +<div style="position:absolute;margin-left: 636px; + margin-top:-76px;color:#777;">If you're interested<br> + <a href="{@docRoot}distribute/googleplay/edu/contact.html" + class="go-link" + style="display: block;text-align: right;">SIGN UP</a></div> + +<div +style="background-color:#fffdeb;width:100%;margin-bottom:1em;padding:.5em;">You +can now include your apps in the Google Play for Education <a +href="{@docRoot}distribute/googleplay/edu/start.html#program">pilot program</a>, +getting it into the hands of participating schools and key influencers in the +education technology community. See <a href="start.html">Get Started</a> to +learn how to participate. </div> + +<p>The sections below list the guidelines and requirements for apps +participating in Google Play for Education. + +<p>Before you include your app in Google Play for Education, set up a <a +href="#test-environment">test environment</a> and make sure your app meets all +of the safety, usability, and quality guidelines given here. You can use the +linked resources to help +you develop a great app for students that offers compelling content and an +intuitive user experience on Android tablets.</p> + +<p>In addition, ensure that your app complies with the terms of a <a +href="https://play.google.com/about/developer-distribution-agreement-addendum. +html" target="_policies">Google Play for Education Addendum</a>, as well as +the standard <a +href="http://play.google.com/about/developer-content-policy.html" +target="_policies">Google Play Developer Program Policies</a> and <a +href="http://play.google.com/about/developer-distribution-agreement.html" +target="_policies">Developer Distribution Agreement</a>.</p> + + +<h2 id="requirements">Safety First</h2> + +<p>To participate, your apps must be designed to be appropriate for +the K-12 market. The basic requirements that your apps must meet are:</p> + +<ol> + <li>Apps and the ads they contain must not collect personally identifiable +information other than user credentials or data required to operate and improve +the app.</li> + <li>Apps must not use student data for purposes unrelated to its educational +function.</li> + <li>Apps must have a content rating of "Everyone" or "Low Maturity" (apps with +a "Medium Maturity" rating are allowed, if they have that rating solely because +they allow communication between students).</li> + <li>App content, including ads displayed by the app, must be consistent with +the app's maturity rating. The app must not display any “offensive” content, as +described in the <a +href="http://play.google.com/about/developer-content-policy.html">Google Play +Developer Program Policies</a> and <a +href="https://support.google.com/googleplay/android-developer/answer/188189"> +content-rating guidelines</a>.</p></li> +<li>Apps must comply with the Children’s Online Privacy Protection Act +and all other applicable laws and regulations.</li> +</ol> + + +<h2 id="inapp">Monetizing and Ads</h2> + +<p>Google Play for Education provides a simple and secure environment for students +and teachers. To support that environment, priced or free apps that do not use in-app +purchases are preferred, as are apps that do not display ads. Apps that use in-app +payments or ads are acceptable, but you must declare those behaviors when opting-in +to Google Play for Education. Your app's use of in-app purchases or ads will be +disclosed to educators when they are browsing for content.</p> + +<p>Follow the guidelines below to help your app receive the + highest ratings and offer the best possible user-experience.</p> + +<p>If your app is priced or sells in-app products, you must:</p> + +<ul> + <li>Sell all content and services through Google Play for Education</li> + <li>Allow Google Play to offer teachers limited free trials before purchase +(through business terms only, no development work is needed)</li> +<li>Disable in-app purchases if possible, or ensure that: + +<ul> +<li>Users can access your app's core functionality for a classroom setting without +an in-app purchase.</li> +<li>In-app purchases are clearly identifiable in your UI.</li> +<li>You declare the use of in-app purchases at <a href="{@docRoot}distribute/googleplay/edu/start.html#opt-in">opt-in</a>.</li> +</ul> +</li> +</ul> + +<p class="note"><strong>Note</strong>: In-app +purchases are blocked on Google Play for Education tablets at this time.</p> + +<p>If your app displays ads, you should: + <ul> + <li>Disable the display of ads if possible, or ensure that: + <ul> + <li>Ads are not distracting for students or teachers</li> + <li>Ads do not occupy a significant portion of the screen</li> + <li>Ads content does not exceed the maturity rating of the app.</li> + <li>You declare the use of ads at <a href="{@docRoot}distribute/googleplay/edu/start.html#opt-in">opt-in</a>.</li> + </ul> + </li> +</ul> + + +<h2 id="approved">Educational Value</h2> + +<p>Apps submitted to Google Play for Education will be evaluated by a +third-party educator network, which will review them based on alignment with <a +href="http://www.corestandards.org/" class="external-link" +target="_android">Common Core Standards</a> and other factors. This will help +make your content more discoverable for teachers and administrators as they +browse by grade level, subject, core curriculum, and other parameters. </p> + +<p>Apps with highest educational value will have these characteristics:</p> +<ul> + <li>Designed for use in K-12 classrooms.</li> + <li>Aligned with a common core standard or support common-core learning.</li> + <li>Simple, easy to use, and intuitive for the grade levels the app is targeting. + App is relatively easy to navigate without teacher guidance. Not distracting + or overwhelming to students.</li> + <li>Enjoyable and interactive. App is engaging to students and lets them control + their experience.</li> + <li>Versatile. App has features make the it useful for more than one classroom + function or lesson throughout the school year.</li> + <li>Supports the "4Cs": + <ul> + <li><em>Creativity</em> — Allows students to create in order to express + understanding of the learning objectives, and try new approaches, innovation + and invention to get things done.</li> + <li><em>Critical thinking</em> — Allows students to look at problems in + a new way, linking learning across subjects and disciplines.</li> + <li><em>Collaboration</em> — Allows students and (if appropriate) educators + to work together to reach a goal.</li> + <li><em>Communication</em> — Allows students to comprehend, critique and + share thoughts, questions, ideas and solutions.</li> + </ul> + </li> +</ul> + +<p>As you design and develop your app, make sure it offers high educational value +by addressing as many of those characteristics as possible.</p> + + +<h2 id="quality">App Quality</h2> + +<p>Google Play for Education brings educational content to students and teachers +on Android tablets. Your apps should be designed to perform well and look great +on Android tablets, and they should offer the best user experience possible. +</p> + +<p>High quality apps are engaging, intuitive, and offer compelling content. +Google Play for Education will highlight high-quality apps for easy discovery in +the store. Here are some recommendations for making your app easy for students +and teachers to enjoy.</p> + +<ul> + <li>Meet Core app quality guidelines + <ul> + <li>Follow <a + href="{@docRoot}design/index.html">Android Design Guidelines</a>. Pay special + attention to the sections on <a href="{@docRoot}design/patterns/actionbar.html">Action + Bar</a>, <a href="{@docRoot}design/patterns/navigation.html">Navigation</a> and <a + href="{@docRoot}design/patterns/pure-android.html">Pure Android</a>.</li> + <li>Test your apps against the <a href="{@docRoot}distribute/googleplay/quality/core.html">Core + App Quality Guidelines</a>.</li> + </ul> + </li> +<li>Meet tablet app quality guidelines + <ul> + <li>Follow our best practices for tablet app development</li> + <li>Review the <a href="{@docRoot}distribute/googleplay/quality/tablet.html">Tablet App + Quality Checklist</a> and <a + href="http://android-developers.blogspot.com/2012/11/designing-for-tablets-were-here-to-help.html" + target="_android">blog post on designing for tablets</a></li> + <li>Check your Optimization Tips in the Google Play Developer Console (if you've + already uploaded your app)</li> + </ul> +<li>Strive for simplicity and highest usability for students + <ul> + <li>Design your app so that teachers and students can use all capabilities of + your app without having to sign-in to multiple accounts and remember + multiple passwords.</li> + <li>Every student or teacher using a Google Play for Education tablet will already be + signed in with a Google account on the device. You can take advantage of that to provide a + simple, seamless sign-in experience in your app. A recommended approach is to use + <a href="{@docRoot}google/play-services/auth.html">Google OAuth 2 authorization</a> + through Google Play Services.</li> + </ul> +</li> +</ul> + + +<h2 id="test-environment">Test Environment</h2> + +<p>To test your app and assess it against the guidelines in this document, it's +recommended that you set up a test environment that replicates the actual +environment in which students and teachers will run your app.</p> + +<p>In general, you should use the test environment described in <a +href="{@docRoot}distribute/googleplay/quality/tablet.html#test-environment"> +Setting Up a Test Environment for Tablets</a>, including a small number of +actual hardware devices that replicate the tablet form factors used in the +Google Play for Education.</p> + +<h3 id="devices">Android tablets</h3> + +<p>Google Play for Education uses primarily Nexus 7 devices, so +your testing can focus on that specific hardware device. You can purchase the +device from <a href="https://play.google.com/store/devices/details?id=nexus_7_16gb" +target="_android">Google Play</a> and other stores. Although testing on Nexus +devices is preferred, you can test on other 7-inch (or 10-inch) tablets or virtual +devices if you don't have access to Nexus devices.</p> + +<h3 id="conditions">Test conditions</h3> + +<p>Once you've set up a suitable hardware environment, make sure to test your +apps under conditions that simulate those of schools. For example, Google Play +for Education lets administrators control or disable certain capabilities for +students, so it's good to test your app with those capabilities disabled. Below +are some conditions to test your app in, to ensure best results in the Google +Play for Education environment:</p> + +<ul> +<li><em>Android version</em> — Test the app on devices running Android +4.2. Google Play for Education devices will be running Android 4.2 or higher +(API level 17).</li> +<li><em>Proxy server</em> — Test the app in network environment that uses +proxies. Many schools use proxies.</li> +<li><em>Secondary user account</em> — Test the app using a secondary user +account. Most Google Play for Education users will not be using the primary <a +href="{@docRoot}about/versions/jelly-bean.html#42-multiuser">multiuser</a> +account on their devices. For testing, create a secondary multiuser account on +your tablet.</li> +<li><em>No location services</em> — Test the app to make sure it works +properly with location services disabled. Many schools will disable location +services for student devices.</li> +<li><em>No In-app Billing</em> — Test the app to make sure it works +properly without access to In-app Billing. In-app purchases are blocked on +Google Play for Education devices at this time.</li> +<li><em>No Bluetooth</em> — Test the app to make sure it works properly +when Bluetooth is disabled. Many schools will disable Bluetooth on student +devices.</li> +<li><em>No access to network</em> — Test the app to make sure it works +properly when the device cannot connect to the internet. </li> +</ul> + diff --git a/docs/html/distribute/googleplay/edu/start.jd b/docs/html/distribute/googleplay/edu/start.jd index e740cce9b4d4..419d5ea39697 100644 --- a/docs/html/distribute/googleplay/edu/start.jd +++ b/docs/html/distribute/googleplay/edu/start.jd @@ -1,5 +1,5 @@ page.title=Get Started -page.metaDescription=Get your apps ready for Google Play for Education. +page.metaDescription=Get Started with Google Play for Education excludeFromSuggestions=true @jd:body @@ -9,126 +9,256 @@ excludeFromSuggestions=true class="go-link" style="display: block;text-align: right;">SIGN UP</a></div> +<div style="background-color:#fffdeb;width:100%;margin-bottom:1em;padding:.5em;">You +can now include your apps in the Google Play for Education <a href="#program">pilot program</a>, +getting it into the hands of participating schools and key influencers in the education technology +community. See the sections below to learn more.</div> + <p>If you've got a great app for education or just an idea for one, plan to be a part of Google Play for Education to reach even more teachers and students. It's -easy to participate, and you will be able to offer new or existing Android apps -using the familiar tools in Google Play.</p> +easy to participate, and you'll be able to offer new or existing Android apps +using familiar tools and processes in Google Play.</p> + +<p>To get started, review the sections in this document and learn how to make +your apps available through Google Play for Education. Also make sure to read <a +href="{@docRoot}distribute/googleplay/edu/guidelines.html">Guidelines for +Apps</a> for information on the safety, usability, and quality standards that +your apps should meet. When your app is ready, you can opt-in to Google Play for +Education from the Developer Console.</p> + +<p>Note that the initial launch of Google Play for Education is planned for Fall +2013 and will include schools in the United States only, with support for other +countries to follow. At this time, please include your app in Google Play for +Education only if it is targeting the <strong>US K-12 market</strong>. </p> + + +<h2 id="participate">How to Participate</h2> + +<div style="float:right; padding-top:2em;"><img +src="{@docRoot}images/gp-edu-process.png"></div> + +<p>Google Play for Education lets you put your educational apps in front of a +new audience of teachers and students. You can develop and publish using +familiar tools and processes, such as your existing Developer Console account +and your current distribution and pricing settings. It's easy to participate +— the sections below outline the process.</p> + +<h3 id="basic-info">1. Understand guidelines and policies</h3> + +<p>To prepare for a successful launch on Google Play for Education, start by +reviewing the guidelines for educational apps in Google Play and the policies +that apply to your apps. See <a +href="{@docRoot}distribute/googleplay/edu/guidelines.html">Guidelines for +Apps</a> for details.</p> + +<p>Also, make sure that your are familiar with the policies that your app must +comply with, including +<a href="http://play.google.com/about/developer-content-policy.html" target="_policies">content +policies</a>, the <a +href="http://play.google.com/about/developer-distribution-agreement.html" +target="_policies">developer agreement</a>, and <a +href="https://play.google.com/about/developer-distribution-agreement-addendum. +html" target="_policies">Google Play for Education Addendum</a>.</p> + +<h3 id="developing">2. Design and develop a great app for education</h3> + +<p>A great app for educators and students is designed for classroom use, looks +great on tablets, and delivers a compelling feature set for teachers and +students. If you are developing an app for education, make sure that it is +appropriate for K-12 classrooms, offers educational value, and is refined to +offer a polished, high-quality tablet experience.</p> + +<p>Assess your app against the criteria listed in <a +href="{@docRoot}distribute/googleplay/edu/guidelines.html">Guidelines for +Apps</a> and plan on supporting them to the greatest extent possible. In some +cases you might need to modify your features or UI to support the requirements +of the classroom use-case. It's a good idea to identify those areas early in +development so that you are able address them properly. </p> + +<p>With Google Play for Education, optimizing your app for tablets is a crucial +part of getting your app ready for distribution to educators. A variety of +resources are available to help you understand what you need to optimize for +tablets — a good starting point is the <a +href="{@docRoot}distribute/googleplay/quality/tablet.html">Tablet App Quality +Guidelines</a>. </p> + +<p>Throughout design and development, it's important to have a suitable device +on which to prototype and test your user experience. It's highly recommended +that you acquire one or more tablet devices and set up your testing environment +as early as possible. The recommended hardware device that replicates the Google +Play for Education environment is the Nexus 7, which is available from <a href="https://play.google.com/store/devices/details?id=nexus_7_16gb" target="_android">Google Play</a> and other stores.</p> + +<p>Proper testing and quality assurance are key aspects of delivering a great +app for teachers and students. Make sure you set up a <a +href="{@docRoot}distribute/googleplay/edu/guidelines.html#test-environment"> +proper test environment</a> to ensure that your app meets guidelines under +realistic conditions.</p> + +<h3 id="opt-in">3. Opt-in to Google Play for Education and publish</h3> + +<div class="sidebox-wrapper"> +<div class="sidebox"> +<h2>Before you opt-in</h2> +<p>To participate in Google Play for Education, you must agree to a <a +href="https://play.google.com/about/developer-distribution-agreement-addendum.html" +target="_policies">Google Play for Education Addendum</a> +to the standard Developer Distribution Agreement.</p> -<p>To get started, review the sections in this document and learn about the -safety, usability, and quality guidelines that apps should meet. Assess your -apps against these guidelines and make any adjustments needed. You can use the -linked resources to help you develop a great app for students that offers -compelling content and an intuitive user experience on Android tablets.</p> +<p>Before you opt-in, review the Addendum completely and make any necessary +modifications to your app.</p> +</div> +</div> -<h2 id="requirements">Safety First</h2> +<p>When you've built your release-ready APK and tested to ensure that it meets +the <a href="{@docRoot}distribute/googleplay/edu/guidelines.html">app guidelines</a>, +upload it to the Developer Console, create your store listing, and set +distribution options. If you aren't familiar with how to prepare for launch on +Google Play, see the <a +href="{@docRoot}distribute/googleplay/publish/preparing.html">Launch Checklist</a>. </p> -<p>To participate, your apps must be designed to be usable and appropriate for -the K-12 market. The basic requirements that your apps must meet are:</p> +<p>When your app is ready to publish, you can <em>opt-in</em> to Google Play for +Education from the Developer Console. Opt-in means that you want your app to be +made available to educators through Google Play for Education, including review, +classification, and approval by our third-party educator network. Note that +opt-in does not affect the availability of your app in Google Play Store.</p> + +<p>Opt-in also confirms that your app complies with <a +href="http://play.google.com/about/developer-content-policy.html" +target="_policies">Google Play Developer Program +Policies</a> and the <a +href="http://play.google.com/about/developer-distribution-agreement.html" +target="_policies">Developer Distribution Agreement</a>, +including a <a +href="https://play.google.com/about/developer-distribution-agreement-addendum. +html" target="_policies">Google Play for Education +Addendum</a>. If you are not familiar with these policy documents or the +Addendum, make sure to read them before opting-in. </p> + +<p>Here's how to opt-in to Google Play for Education for a specific app:</p> <ol> - <li>Apps and the ads they contain must not collect personally identifiable -information other than user credentials or data required to operate and improve -the app.</li> - <li>Apps must not use student data for purposes unrelated to its educational -function.</li> - <li>Apps must have a content rating of "Everyone" or "Low Maturity" (apps with -a "Medium Maturity" rating are allowed, if they have that rating solely because -they allow communication between students).</li> - <li>App content, including ads displayed by the app, must be consistent with -the app's maturity rating. The app must not display any “offensive” content, as -described in the <a -href="http://play.google.com/about/developer-content-policy.html">Google Play -Developer Program Policies</a> and <a -href="https://support.google.com/googleplay/android-developer/answer/188189"> -content-rating guidelines</a>.</p></li> + <li>In the Developer Console All Applications page, click the app you want to +opt-in. </li> + <li>Under Pricing and Distribution, scroll down to find "Google Play for +Education" and the opt-in checkbox. </li> + <li>Click the checkbox next to "Include my app in Google Play for +Education..."</li> + <li>After you've opted-in, find the "Ads" and "In-app purchases" checkboxes below. +Check each checkbox that applies. Your app's use of ads or in-app purchases will +be shown to educators when they are browsing your app. </li> + <li>Click "Save" to save your Pricing and Distribution changes.</li> </ol> +<div style="clear:both;margin-top:1.5em;margin-bottom:1.5em;width:660px;"> +<img src="{@docRoot}images/gp-edu-optin.png" style="border:2px solid #ddd;width:660px;"> +<p class="image-caption"><span style="font-weight:500;">Opt-in for apps</span>: +Include your app in Google Play for Education by opting-in from the Developer Console.</p> +</div> -<h2 id="approved">With the Help of Educators</h2> +<p>Once you save changes and publish your app, the app will be submitted to our +third-party educator network for review and approval. If the app is already +published, it will be submitted for review as soon as you opt-in and save your +changes. </p> -<p>App content submitted to Google Play for Education will be reviewed by -educators who will categorize the apps and align them with <a -href="http://www.corestandards.org/" class="external-link" -target="_android">Common Core Standards</a>. This will help make your content -discoverable in a way that is easy for teachers and administrators. </p> +<p class="note"><strong>Note</strong>: Google Play for Education is part of +Google Play. When you publish an app that's opted-in to Google Play for +Education, the app becomes available to users in Google Play right away. After +the app is reviewed and approved, it then becomes available to educators in +Google Play for Education.</p> +<h3 id="review">4. Track your review and approval</h3> -<h2 id="secure">Sold Simply</h2> +<p>Google Play for Education provides content to educators in a way that's +properly organized by subject, grade level, and common core standards (where +applicable). To ensure high educational value and proper classification, we work +with a third-party educator network to review and approve apps before making +them discoverable through the Google Play for Education browsing tools. </p> -<p>Google Play for Education provides a simple and secure environment in which -educators can buy apps in a way that's easy for schools — through purchase -orders. Your apps must support this environment by ensuring that they:</p> +<p>Our third-party educator network will evaluate apps according to educational +value and alignment with K-12 core standards, then assign the metadata for +subject, grade level, and core curriculum that makes them easily browseable for +educators. To understand how your apps will be evaluated, please see the <a +href="{@docRoot}distribute/googleplay/edu/guidelines.html">Guidelines for +Apps</a> document.</p> -<ul> - <li>Sell all content and services through Google Play for Education</li> - <li>Permit Google Play to offer teachers limited free trials before purchase -(through business terms only, no development work is needed)</li> -</ul> +<p>As soon as you opt-in to Google Play for Education and publish, your app is +queued for review by our third-party educator network. The review and approval +process can take <strong>3-4 weeks or more</strong>. You'll receive notification +by email (to your developer account address) when the review is complete, with a +summary of the review results. </p> + +<p class="note"><strong>Note</strong>: Until the full product launch in Fall +2013, please expect the initial review of your app to take longer than usual. +</p> + +<p>At any time, you can check the review and approval status of your app in the +Developer Console, under "Google Play for Education" in the app's Pricing and +Distribution page. There are three approval states:</p> -<p>In addition, it's highly recommended that your apps:</p> <ul> - <li>Disable in-app purchase in any UI accessible to students.</li> +<li><em>Pending</em> — Your app was sent for review and the review +is not yet complete.</li> +<li><em>Approved</em> — Your app was reviewed and approved. The app +will be made available directly to educators through Google Play for Education. +Until the full product launch later this year, your app will be available to a +limited number of educators through the <a +href="{@docRoot}distribute/googleplay/edu/start.html#program">pilot program</a>. +Once your app is approved, you can update it at your convenience without needing +another full review. </li> +<li><em>Not approved</em> — Your app was reviewed and not approved. +Check the notification email for information about why the app was not approved. +You can address any issues and opt-in again for another review. </li> </ul> -<h2 id="quality">High Quality and Usability</h2> +<p>If you have questions about the review status of your app, follow the process +discussed in the next section. </p> -<p>Google Play for Education brings educational content to students and teachers -on Android tablets. Your apps should be designed to perform well and look great -on Android tablets, and they should offer the best user experience possible. -</p> +<h3 id="appeal">5. Get support or appeal your review results</h3> -<p>High quality apps are engaging, intuitive, and offer compelling content. -Google Play for Education will highlight high-quality apps for easy discovery in -the store. Here are some recommendations for making your app easy for students -and teachers to enjoy.</p> +<p>After your app is reviewed you'll receive an email giving you the review +results, including whether the app was approved, how the app was classified, and +what issues may need to be addressed. You'll receive the email at the address +you specified for your developer account. </p> -<ul> - <li>Meet Core app quality guidelines - <ul> - <li>Follow <a href="{@docRoot}design/index.html">Android Design Guidelines</a>. - Pay special attention to the sections on <a href="{@docRoot}design/patterns/actionbar.html">Action - Bar</a>, <a href="{@docRoot}design/patterns/navigation.html">Navigation</a> and <a - href="{@docRoot}design/patterns/pure-android.html">Pure Android</a>.</li> - <li>Test your apps against the <a href="{@docRoot}distribute/googleplay/quality/core.html">Core - App Quality Guidelines</a>.</li> - </ul> - </li> -<li>Meet tablet app quality guidelines - <ul> - <li>Follow our best practices for tablet app development</li> - <li>Review the <a href="{@docRoot}distribute/googleplay/quality/tablet.html">Tablet App - Quality Checklist</a> and <a href="http://android-developers.blogspot.com/2012/11/designing-for-tablets-were-here-to-help.html" - class="external-link;" target="_android">blog post on designing for tablets</a></li> - <li>Check your Optimization Tips in the Google Play Developer Console (if you've already uploaded your app)</li> - </ul> -<li>Strive for simplicity and highest usability for students - <ul> - <li>Design your app so that teachers and students can use all capabilities of your app without - having to sign-in to multiple accounts and remember multiple passwords. </li> - <li>For best experience, use Google sign-in in your apps, which provides seamless authentication - across apps. <!--Google Account login service and integrate with Google Drive where appropriate. --></li> - </ul> -</li> -</ul> -<p>In addition, it's highly recommended that your apps:</p> -<ul> - <li>Disable advertisements in any UI accessible to students.</li> -</ul> +<p>If you believe your app was reviewed or classified incorrectly, you will be +able to appeal and request reconsideration. Watch for more information on the +appeal process and links in the weeks to come.</p> + +<p class="note"><strong>Note</strong>: Support and appeal forms are not yet +available, but will be available soon.</p> + + +<h2 id="program">Including Your Apps in the Pilot Program</h2> + +<p>Leading up to the Fall 2013 launch, the Google Play for Education team is +conducting an extensive series of pilots that include schools and students across +the United States. Educators in participating schools can browse for apps and +purchase them in bulk, then deploy them instantly to teacher and student +devices. </p> + +<h3 id="pilot">Early opt-in and publishing</h3> +<p>As an app developer, you can take part in the pilot program, getting your app +into the hands of schools and key influencers in the education technology +community. It's a great way to get early feedback on your educational app. </p> +<p>To offer your app in the pilot program, prepare the app and ensure that it meets +the <a href="{@docRoot}distribute/googleplay/edu/guidelines.html">Guidelines +for Apps</a>. Then opt-in to Google Play for Education and publish as soon +as you are ready. Once your app is approved during review by our third-party +educator network, it will be made available to educators in the pilot program +right away. Note that during the pilot program, the review and approval process +may take longer than usual.</p> -<h2 id="strategies">Strategies for Development</h2> +<h3 id="launch">Full launch to US schools</h3> +<p>The initial launch of Google Play for Education is planned for Fall 2013. The +pilot program and full launch will include schools in the United States only, +with support for schools in other countries to follow. </p> - <p>If you have an existing educational app in Google Play, the classroom -environment offered by Google Play for Education presents a slightly different -set of needs, requirements, and also opportunities.</p> +<p>At this time, you should include your app in Google Play for Education only +if it is targeting the US K-12 market. </p> - <p>We're working to give you the tools you need to build for the classroom -environment from a single APK, delivered as a single product to all of your -users in Google Play. Our goal is to let you customize your app's UI and -features as minimally or deeply as you need, to provide a simple, intuitive, and -beautiful learning experience. </p> +<h3 id="more">More information</h3> - <p>Watch for more information on developer tools coming in the weeks ahead. -We'll update this page as we roll out tools for you to use. As a starting point, -we recommend planning your app's design and ensuring its optimization on Nexus -tablets.</p> +<p>If you'd like to be notified by email of the latest information about Google Play +for Education, visit the <a href="{@docRoot}distribute/googleplay/edu/contact.html"> +Sign Up</a> page and fill out the form. </p>
\ No newline at end of file diff --git a/docs/html/distribute/googleplay/publish/preparing.jd b/docs/html/distribute/googleplay/publish/preparing.jd index 0cbc27037f2d..5593f4f8efc9 100644 --- a/docs/html/distribute/googleplay/publish/preparing.jd +++ b/docs/html/distribute/googleplay/publish/preparing.jd @@ -81,6 +81,10 @@ violations, termination of your developer account. </p> <tr> <td><p>Related resources:</p> <ul style="margin-top:-.5em;"> + +<li><strong><a href="{@docRoot}distribute/googleplay/policies/index.html">Google Play Policies and Guidelines</a></strong> — An overview of Google Play policies for spam, intellectual property, and ads, with examples of common problems. </li> +</a></strong> — Help Center document describing various content policies and processes.</li> + <li><strong><a href="http://support.google.com/googleplay/android-developer/bin/topic.py?hl=en&topic=2364761&parent=2365624&ctx=topic">Policy and Best Practices </a></strong> — Help Center document describing various content policies and processes.</li> diff --git a/docs/html/images/gp-edu-optin.png b/docs/html/images/gp-edu-optin.png Binary files differnew file mode 100644 index 000000000000..91e4e0d1e041 --- /dev/null +++ b/docs/html/images/gp-edu-optin.png diff --git a/docs/html/images/gp-edu-process.png b/docs/html/images/gp-edu-process.png Binary files differnew file mode 100644 index 000000000000..febf0078a5f4 --- /dev/null +++ b/docs/html/images/gp-edu-process.png diff --git a/docs/html/images/tools/clipbounds-normal@2x.png b/docs/html/images/tools/clipbounds-normal@2x.png Binary files differnew file mode 100644 index 000000000000..99e0e149daf4 --- /dev/null +++ b/docs/html/images/tools/clipbounds-normal@2x.png diff --git a/docs/html/images/tools/clipbounds@2x.png b/docs/html/images/tools/clipbounds@2x.png Binary files differnew file mode 100644 index 000000000000..d5261e6eafde --- /dev/null +++ b/docs/html/images/tools/clipbounds@2x.png diff --git a/docs/html/images/tools/ninepatch_opticalbounds@2x.png b/docs/html/images/tools/ninepatch_opticalbounds@2x.png Binary files differnew file mode 100644 index 000000000000..cb7eb0840f5c --- /dev/null +++ b/docs/html/images/tools/ninepatch_opticalbounds@2x.png diff --git a/docs/html/images/tools/opticalbounds-normal@2x.png b/docs/html/images/tools/opticalbounds-normal@2x.png Binary files differnew file mode 100644 index 000000000000..66622643d350 --- /dev/null +++ b/docs/html/images/tools/opticalbounds-normal@2x.png diff --git a/docs/html/images/tools/opticalbounds@2x.png b/docs/html/images/tools/opticalbounds@2x.png Binary files differnew file mode 100644 index 000000000000..bd09f9b29512 --- /dev/null +++ b/docs/html/images/tools/opticalbounds@2x.png diff --git a/docs/html/index.jd b/docs/html/index.jd index af271ee79349..d82deec814ea 100644 --- a/docs/html/index.jd +++ b/docs/html/index.jd @@ -13,6 +13,20 @@ page.metaDescription=The official site for Android developers. Provides the Andr <div class="frame"> <ul> <li class="item carousel-home"> + <div class="content-left col-9"> + <a href="{@docRoot}about/versions/jelly-bean.html"><img src="{@docRoot}images/home/android-jellybean.png" ></a> + </div> + <div class="content-right col-6"> + <h1>More Jelly Beans!</h1> + <p>Android 4.3 is now available with a variety of performance improvements + and new developer features. </p> + <p>With this release, Android now supports Bluetooth Low Energy for battery + savings with wireless peripherals, OpenGL ES 3.0 for the most advanced mobile 3D + graphics, MPEG DASH support for high quality media streaming, and much more.</p> + <p><a href="{@docRoot}about/versions/jelly-bean.html" class="button">Learn More</a></p> + </div> + </li> + <li class="item carousel-home"> <div class="content-left col-11" style="padding-top:10px;"> <a href="{@docRoot}channels/io2013.html"> <img src="{@docRoot}images/home/io-videos-2013.png" style="margin:60px 0 0; @@ -76,17 +90,6 @@ page.metaDescription=The official site for Android developers. Provides the Andr </div> </li> <li class="item carousel-home"> - <div class="content-left col-9"> - <a href="{@docRoot}about/versions/jelly-bean.html"><img src="{@docRoot}images/home/android-jellybean.png" ></a> - </div> - <div class="content-right col-6"> - <h1>Android 4.2 Jelly Bean!</h1> - <p>The latest version of Jelly Bean is here, with performance optimizations, a refreshed UI, and great new features for developers. </p> - <p>Android 4.2 includes APIs for developing lock sceen widgets and Daydream screensavers, using external displays, creating RTL layouts, building flexible UI with nested Fragments, and much more.</p> - <p><a href="{@docRoot}about/versions/jelly-bean.html" class="button">Learn More</a></p> - </div> - </li> - <li class="item carousel-home"> <div class="content-left col-10"> <img src="{@docRoot}images/home/design.png" style="margin-top:30px"> </div> diff --git a/opengl/java/android/opengl/Matrix.java b/opengl/java/android/opengl/Matrix.java index 72128acbcb61..ce3f57ebfea1 100644 --- a/opengl/java/android/opengl/Matrix.java +++ b/opengl/java/android/opengl/Matrix.java @@ -19,24 +19,21 @@ package android.opengl; /** * Matrix math utilities. These methods operate on OpenGL ES format * matrices and vectors stored in float arrays. - * + * <p> * Matrices are 4 x 4 column-vector matrices stored in column-major * order: * <pre> * m[offset + 0] m[offset + 4] m[offset + 8] m[offset + 12] * m[offset + 1] m[offset + 5] m[offset + 9] m[offset + 13] * m[offset + 2] m[offset + 6] m[offset + 10] m[offset + 14] - * m[offset + 3] m[offset + 7] m[offset + 11] m[offset + 15] - * </pre> + * m[offset + 3] m[offset + 7] m[offset + 11] m[offset + 15]</pre> * - * Vectors are 4 row x 1 column column-vectors stored in order: + * Vectors are 4 x 1 column vectors stored in order: * <pre> * v[offset + 0] * v[offset + 1] * v[offset + 2] - * v[offset + 3] - * </pre> - * + * v[offset + 3]</pre> */ public class Matrix { @@ -44,12 +41,18 @@ public class Matrix { private final static float[] sTemp = new float[32]; /** - * Multiply two 4x4 matrices together and store the result in a third 4x4 + * @deprecated All methods are static, do not instantiate this class. + */ + @Deprecated + public Matrix() {} + + /** + * Multiplies two 4x4 matrices together and stores the result in a third 4x4 * matrix. In matrix notation: result = lhs x rhs. Due to the way * matrix multiplication works, the result matrix will have the same * effect as first multiplying by the rhs matrix, then multiplying by * the lhs matrix. This is the opposite of what you might expect. - * + * <p> * The same float array may be passed for result, lhs, and/or rhs. However, * the result element values are undefined if the result elements overlap * either the lhs or rhs elements. @@ -70,9 +73,9 @@ public class Matrix { float[] lhs, int lhsOffset, float[] rhs, int rhsOffset); /** - * Multiply a 4 element vector by a 4x4 matrix and store the result in a 4 - * element column vector. In matrix notation: result = lhs x rhs - * + * Multiplies a 4 element vector by a 4x4 matrix and stores the result in a + * 4-element column vector. In matrix notation: result = lhs x rhs + * <p> * The same float array may be passed for resultVec, lhsMat, and/or rhsVec. * However, the resultVec element values are undefined if the resultVec * elements overlap either the lhsMat or rhsVec elements. @@ -97,12 +100,14 @@ public class Matrix { /** * Transposes a 4 x 4 matrix. + * <p> + * mTrans and m must not overlap. * - * @param mTrans the array that holds the output inverted matrix - * @param mTransOffset an offset into mInv where the inverted matrix is + * @param mTrans the array that holds the output transposed matrix + * @param mTransOffset an offset into mTrans where the transposed matrix is * stored. * @param m the input array - * @param mOffset an offset into m where the matrix is stored. + * @param mOffset an offset into m where the input matrix is stored. */ public static void transposeM(float[] mTrans, int mTransOffset, float[] m, int mOffset) { @@ -117,12 +122,14 @@ public class Matrix { /** * Inverts a 4 x 4 matrix. + * <p> + * mInv and m must not overlap. * * @param mInv the array that holds the output inverted matrix * @param mInvOffset an offset into mInv where the inverted matrix is * stored. * @param m the input array - * @param mOffset an offset into m where the matrix is stored. + * @param mOffset an offset into m where the input matrix is stored. * @return true if the matrix could be inverted, false if it could not. */ public static boolean invertM(float[] mInv, int mInvOffset, float[] m, @@ -301,10 +308,11 @@ public class Matrix { /** - * Define a projection matrix in terms of six clip planes - * @param m the float array that holds the perspective matrix + * Defines a projection matrix in terms of six clip planes. + * + * @param m the float array that holds the output perspective matrix * @param offset the offset into float array m where the perspective - * matrix data is written + * matrix data is written * @param left * @param right * @param bottom @@ -358,11 +366,12 @@ public class Matrix { } /** - * Define a projection matrix in terms of a field of view angle, an - * aspect ratio, and z clip planes + * Defines a projection matrix in terms of a field of view angle, an + * aspect ratio, and z clip planes. + * * @param m the float array that holds the perspective matrix * @param offset the offset into float array m where the perspective - * matrix data is written + * matrix data is written * @param fovy field of view in y direction, in degrees * @param aspect width to height aspect ratio of the viewport * @param zNear @@ -395,7 +404,7 @@ public class Matrix { } /** - * Computes the length of a vector + * Computes the length of a vector. * * @param x x coordinate of a vector * @param y y coordinate of a vector @@ -408,6 +417,7 @@ public class Matrix { /** * Sets matrix m to the identity matrix. + * * @param sm returns the result * @param smOffset index into sm where the result matrix starts */ @@ -421,7 +431,10 @@ public class Matrix { } /** - * Scales matrix m by x, y, and z, putting the result in sm + * Scales matrix m by x, y, and z, putting the result in sm. + * <p> + * m and sm must not overlap. + * * @param sm returns the result * @param smOffset index into sm where the result matrix starts * @param m source matrix @@ -444,7 +457,8 @@ public class Matrix { } /** - * Scales matrix m in place by sx, sy, and sz + * Scales matrix m in place by sx, sy, and sz. + * * @param m matrix to scale * @param mOffset index into m where the matrix starts * @param x scale factor x @@ -462,7 +476,10 @@ public class Matrix { } /** - * Translates matrix m by x, y, and z, putting the result in tm + * Translates matrix m by x, y, and z, putting the result in tm. + * <p> + * m and tm must not overlap. + * * @param tm returns the result * @param tmOffset index into sm where the result matrix starts * @param m source matrix @@ -487,6 +504,7 @@ public class Matrix { /** * Translates matrix m by x, y, and z in place. + * * @param m matrix * @param mOffset index into m where the matrix starts * @param x translation factor x @@ -503,15 +521,18 @@ public class Matrix { } /** - * Rotates matrix m by angle a (in degrees) around the axis (x, y, z) + * Rotates matrix m by angle a (in degrees) around the axis (x, y, z). + * <p> + * m and rm must not overlap. + * * @param rm returns the result * @param rmOffset index into rm where the result matrix starts * @param m source matrix * @param mOffset index into m where the source matrix starts * @param a angle to rotate in degrees - * @param x scale factor x - * @param y scale factor y - * @param z scale factor z + * @param x X axis component + * @param y Y axis component + * @param z Z axis component */ public static void rotateM(float[] rm, int rmOffset, float[] m, int mOffset, @@ -524,13 +545,14 @@ public class Matrix { /** * Rotates matrix m in place by angle a (in degrees) - * around the axis (x, y, z) + * around the axis (x, y, z). + * * @param m source matrix * @param mOffset index into m where the matrix starts * @param a angle to rotate in degrees - * @param x scale factor x - * @param y scale factor y - * @param z scale factor z + * @param x X axis component + * @param y Y axis component + * @param z Z axis component */ public static void rotateM(float[] m, int mOffset, float a, float x, float y, float z) { @@ -542,13 +564,18 @@ public class Matrix { } /** - * Rotates matrix m by angle a (in degrees) around the axis (x, y, z) + * Creates a matrix for rotation by angle a (in degrees) + * around the axis (x, y, z). + * <p> + * An optimized path will be used for rotation about a major axis + * (e.g. x=1.0f y=0.0f z=0.0f). + * * @param rm returns the result * @param rmOffset index into rm where the result matrix starts * @param a angle to rotate in degrees - * @param x scale factor x - * @param y scale factor y - * @param z scale factor z + * @param x X axis component + * @param y Y axis component + * @param z Z axis component */ public static void setRotateM(float[] rm, int rmOffset, float a, float x, float y, float z) { @@ -608,7 +635,8 @@ public class Matrix { } /** - * Converts Euler angles to a rotation matrix + * Converts Euler angles to a rotation matrix. + * * @param rm returns the result * @param rmOffset index into rm where the result matrix starts * @param x angle of rotation, in degrees @@ -651,7 +679,7 @@ public class Matrix { } /** - * Define a viewing transformation in terms of an eye point, a center of + * Defines a viewing transformation in terms of an eye point, a center of * view, and an up vector. * * @param rm returns the result diff --git a/packages/PrintSpooler/Android.mk b/packages/PrintSpooler/Android.mk new file mode 100644 index 000000000000..a68fcdf85ea8 --- /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 000000000000..fbb0060fa4a1 --- /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 000000000000..e69de29bb2d1 --- /dev/null +++ b/packages/PrintSpooler/MODULE_LICENSE_APACHE2 diff --git a/packages/PrintSpooler/NOTICE b/packages/PrintSpooler/NOTICE new file mode 100644 index 000000000000..c5b1efa7aac7 --- /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 000000000000..51e425d8c75b --- /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 000000000000..149c274dd1b9 --- /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 000000000000..8b4b40aa2ec7 --- /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 000000000000..ae2fe5c43c52 --- /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 000000000000..2b27b694f1ac --- /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 000000000000..57c45577f8b9 --- /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 000000000000..753721854a06 --- /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 55f2a32fa78b..acfc0962c212 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/am/ActivityStack.java b/services/java/com/android/server/am/ActivityStack.java index 2d7a171cddda..5e6bdb1ac7f1 100644 --- a/services/java/com/android/server/am/ActivityStack.java +++ b/services/java/com/android/server/am/ActivityStack.java @@ -2857,6 +2857,8 @@ final class ActivityStack { return; } + mStackSupervisor.moveHomeStack(isHomeStack()); + // Shift all activities with this task up to the top // of the stack, keeping them in the same internal order. mTaskHistory.remove(tr); diff --git a/services/java/com/android/server/pm/KeySetManager.java b/services/java/com/android/server/pm/KeySetManager.java index 2acc77930364..ee8cc71bdefc 100644 --- a/services/java/com/android/server/pm/KeySetManager.java +++ b/services/java/com/android/server/pm/KeySetManager.java @@ -505,9 +505,6 @@ public class KeySetManager { readKeysLPw(parser); } else if (tagName.equals("keysets")) { readKeySetListLPw(parser); - } else { - PackageManagerService.reportSettingsProblem(Log.WARN, - "Could not read KeySets for KeySetManager!"); } } } @@ -528,9 +525,6 @@ public class KeySetManager { lastIssuedKeyId = Long.parseLong(parser.getAttributeValue(null, "value")); } else if (tagName.equals("lastIssuedKeySetId")) { lastIssuedKeySetId = Long.parseLong(parser.getAttributeValue(null, "value")); - } else { - PackageManagerService.reportSettingsProblem(Log.WARN, - "Could not read keys for KeySetManager!"); } } } @@ -553,9 +547,6 @@ public class KeySetManager { } else if (tagName.equals("key-id")) { long id = readIdentifierLPw(parser); mKeySetMapping.get(currentKeySetId).add(id); - } else { - PackageManagerService.reportSettingsProblem(Log.WARN, - "Could not read KeySets for KeySetManager!"); } } } @@ -571,10 +562,7 @@ public class KeySetManager { long identifier = Long.parseLong(encodedID); String encodedPublicKey = parser.getAttributeValue(null, "value"); PublicKey pub = PackageParser.parsePublicKey(encodedPublicKey); - if (pub == null) { - PackageManagerService.reportSettingsProblem(Log.WARN, - "Could not read public key for KeySetManager!"); - } else { + if (pub != null) { mPublicKeys.put(identifier, pub); } } 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 000000000000..51739984c613 --- /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 000000000000..fef581818183 --- /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 |