Print - platform  APIs

Related changes:
    Skia (inlcude PDF APIs): https://googleplex-android-review.googlesource.com/#/c/305814/
    Canvas to PDF: https://googleplex-android-review.googlesource.com/#/c/319367/
    Settings (initial version): https://googleplex-android-review.googlesource.com/#/c/306077/
    Build: https://googleplex-android-review.googlesource.com/#/c/292437/
    Sample print services: https://googleplex-android-review.googlesource.com/#/c/281785/

Change-Id: I104d12efd12577f05c7b9b2a5e5e49125c0f09da
diff --git a/packages/PrintSpooler/Android.mk b/packages/PrintSpooler/Android.mk
new file mode 100644
index 0000000..a68fcdf
--- /dev/null
+++ b/packages/PrintSpooler/Android.mk
@@ -0,0 +1,32 @@
+# Copyright (C) 2013 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+LOCAL_PATH:= $(call my-dir)
+
+include $(CLEAR_VARS)
+
+LOCAL_MODULE_TAGS := optional
+
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+LOCAL_PACKAGE_NAME := PrintSpooler
+
+LOCAL_JAVA_LIBRARIES := framework
+
+LOCAL_CERTIFICATE := platform
+
+LOCAL_PROGUARD_ENABLED := disabled
+
+include $(BUILD_PACKAGE)
+
diff --git a/packages/PrintSpooler/AndroidManifest.xml b/packages/PrintSpooler/AndroidManifest.xml
new file mode 100644
index 0000000..fbb0060
--- /dev/null
+++ b/packages/PrintSpooler/AndroidManifest.xml
@@ -0,0 +1,54 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+ * Copyright (c) 2013 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+        package="com.android.printspooler"
+        android:sharedUserId="android.uid.printspooler"
+        android:versionName="1"
+        android:versionCode="1"
+        coreApp="true">
+
+    <uses-sdk android:minSdkVersion="17" android:targetSdkVersion="17"/>
+
+    <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
+    <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS_FULL"/>
+
+    <permission android:name="android.permission.BIND_PRINT_SPOOLER_SERVICE"
+        android:label="@string/permlab_bindPrintSpoolerService"
+        android:description="@string/permdesc_bindPrintSpoolerService"
+        android:protectionLevel="signature" />
+
+    <application
+            android:allowClearUserData="false"
+            android:label="@string/app_label"
+            android:allowBackup= "false">
+
+        <service
+            android:name=".PrintSpoolerService"
+            android:exported="true"
+            android:permission="android.permission.BIND_PRINT_SPOOLER_SERVICE">
+        </service>
+
+        <activity
+            android:name=".PrintJobConfigActivity"
+            android:exported="true">
+        </activity>
+
+    </application>
+
+</manifest>
diff --git a/packages/PrintSpooler/MODULE_LICENSE_APACHE2 b/packages/PrintSpooler/MODULE_LICENSE_APACHE2
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/packages/PrintSpooler/MODULE_LICENSE_APACHE2
diff --git a/packages/PrintSpooler/NOTICE b/packages/PrintSpooler/NOTICE
new file mode 100644
index 0000000..c5b1efa
--- /dev/null
+++ b/packages/PrintSpooler/NOTICE
@@ -0,0 +1,190 @@
+
+   Copyright (c) 2005-2008, The Android Open Source Project
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
diff --git a/packages/PrintSpooler/res/layout/print_job_config_activity.xml b/packages/PrintSpooler/res/layout/print_job_config_activity.xml
new file mode 100644
index 0000000..51e425d
--- /dev/null
+++ b/packages/PrintSpooler/res/layout/print_job_config_activity.xml
@@ -0,0 +1,261 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="fill_parent"
+    android:layout_height="wrap_content">
+
+    <GridLayout
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center"
+        android:orientation="vertical"
+        android:columnCount="2">
+
+        <EditText
+            android:id="@+id/copies_edittext"
+            android:layout_width="fill_parent"
+            android:layout_height="wrap_content"
+            android:layout_marginLeft="12dip"
+            android:layout_marginRight="12dip"
+            android:layout_row="0"
+            android:layout_column="1"
+            android:minWidth="150dip"
+            android:inputType="number"
+            android:selectAllOnFocus="true">
+        </EditText>
+
+        <TextView
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginLeft="12dip"
+            android:layout_marginRight="12dip"
+            android:layout_row="0"
+            android:layout_column="0"
+            android:text="@string/label_copies"
+            android:textAppearance="?android:attr/textAppearanceMedium"
+            android:labelFor="@id/copies_edittext">
+        </TextView>
+
+        <TextView
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginLeft="12dip"
+            android:layout_marginRight="12dip"
+            android:layout_row="1"
+            android:layout_column="0"
+            android:text="@string/label_destination"
+            android:textAppearance="?android:attr/textAppearanceMedium">
+        </TextView>
+
+        <Spinner
+            android:id="@+id/destination_spinner"
+            android:layout_width="fill_parent"
+            android:layout_height="wrap_content"
+            android:layout_marginLeft="12dip"
+            android:layout_marginRight="12dip"
+            android:layout_row="1"
+            android:layout_column="1"
+            android:minWidth="150dip">
+        </Spinner>
+
+
+        <TextView
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginLeft="12dip"
+            android:layout_marginRight="12dip"
+            android:layout_row="2"
+            android:layout_column="0"
+            android:text="@string/label_media_size"
+            android:textAppearance="?android:attr/textAppearanceMedium">
+        </TextView>
+
+        <Spinner
+            android:id="@+id/media_size_spinner"
+            android:layout_width="fill_parent"
+            android:layout_height="wrap_content"
+            android:layout_marginLeft="12dip"
+            android:layout_marginRight="12dip"
+            android:layout_row="2"
+            android:layout_column="1"
+            android:minWidth="150dip">
+        </Spinner>
+
+
+        <TextView
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginLeft="12dip"
+            android:layout_marginRight="12dip"
+            android:layout_row="3"
+            android:layout_column="0"
+            android:text="@string/label_resolution"
+            android:textAppearance="?android:attr/textAppearanceMedium">
+        </TextView>
+
+        <Spinner
+            android:id="@+id/resolution_spinner"
+            android:layout_width="fill_parent"
+            android:layout_height="wrap_content"
+            android:layout_marginLeft="12dip"
+            android:layout_marginRight="12dip"
+            android:layout_row="3"
+            android:layout_column="1"
+            android:minWidth="150dip">
+        </Spinner>
+
+
+        <TextView
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginLeft="12dip"
+            android:layout_marginRight="12dip"
+            android:layout_row="4"
+            android:layout_column="0"
+            android:text="@string/label_input_tray"
+            android:textAppearance="?android:attr/textAppearanceMedium">
+        </TextView>
+
+        <Spinner
+            android:id="@+id/input_tray_spinner"
+            android:layout_width="fill_parent"
+            android:layout_height="wrap_content"
+            android:layout_marginLeft="12dip"
+            android:layout_marginRight="12dip"
+            android:layout_row="4"
+            android:layout_column="1"
+            android:minWidth="150dip">
+        </Spinner>
+
+
+        <TextView
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginLeft="12dip"
+            android:layout_marginRight="12dip"
+            android:layout_row="5"
+            android:layout_column="0"
+            android:text="@string/label_output_tray"
+            android:textAppearance="?android:attr/textAppearanceMedium">
+        </TextView>
+
+        <Spinner
+            android:id="@+id/output_tray_spinner"
+            android:layout_width="fill_parent"
+            android:layout_height="wrap_content"
+            android:layout_marginLeft="12dip"
+            android:layout_marginRight="12dip"
+            android:layout_row="5"
+            android:layout_column="1"
+            android:minWidth="150dip">
+        </Spinner>
+
+
+        <TextView
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginLeft="12dip"
+            android:layout_marginRight="12dip"
+            android:layout_row="6"
+            android:layout_column="0"
+            android:text="@string/label_duplex_mode"
+            android:textAppearance="?android:attr/textAppearanceMedium">
+        </TextView>
+
+        <Spinner
+            android:id="@+id/duplex_mode_spinner"
+            android:layout_width="fill_parent"
+            android:layout_height="wrap_content"
+            android:layout_marginLeft="12dip"
+            android:layout_marginRight="12dip"
+            android:layout_row="6"
+            android:layout_column="1"
+            android:minWidth="150dip">
+        </Spinner>
+
+
+        <TextView
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginLeft="12dip"
+            android:layout_marginRight="12dip"
+            android:layout_row="7"
+            android:layout_column="0"
+            android:text="@string/label_color_mode"
+            android:textAppearance="?android:attr/textAppearanceMedium">
+        </TextView>
+
+        <Spinner
+            android:id="@+id/color_mode_spinner"
+            android:layout_width="fill_parent"
+            android:layout_height="wrap_content"
+            android:layout_marginLeft="12dip"
+            android:layout_marginRight="12dip"
+            android:layout_row="7"
+            android:layout_column="1"
+            android:minWidth="150dip">
+        </Spinner>
+
+
+        <TextView
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginLeft="12dip"
+            android:layout_marginRight="12dip"
+            android:layout_row="8"
+            android:layout_column="0"
+            android:text="@string/label_fitting_mode"
+            android:textAppearance="?android:attr/textAppearanceMedium">
+        </TextView>
+
+        <Spinner
+            android:id="@+id/fitting_mode_spinner"
+            android:layout_width="fill_parent"
+            android:layout_height="wrap_content"
+            android:layout_marginLeft="12dip"
+            android:layout_marginRight="12dip"
+            android:layout_row="8"
+            android:layout_column="1"
+            android:minWidth="150dip">
+        </Spinner>
+
+
+        <TextView
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginLeft="12dip"
+            android:layout_marginRight="12dip"
+            android:layout_row="9"
+            android:layout_column="0"
+            android:text="@string/label_orientation"
+            android:textAppearance="?android:attr/textAppearanceMedium">
+        </TextView>
+
+        <Spinner
+            android:id="@+id/orientation_spinner"
+            android:layout_width="fill_parent"
+            android:layout_height="wrap_content"
+            android:layout_marginLeft="12dip"
+            android:layout_marginRight="12dip"
+            android:layout_row="9"
+            android:layout_column="1"
+            android:minWidth="150dip">
+        </Spinner>
+
+    </GridLayout>
+
+</ScrollView>
diff --git a/packages/PrintSpooler/res/menu/print_job_config_activity.xml b/packages/PrintSpooler/res/menu/print_job_config_activity.xml
new file mode 100644
index 0000000..149c274
--- /dev/null
+++ b/packages/PrintSpooler/res/menu/print_job_config_activity.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:id="@+id/print_button"
+          android:title="@string/print_button"
+          android:showAsAction="ifRoom">
+    </item>
+</menu>
diff --git a/packages/PrintSpooler/res/values/strings.xml b/packages/PrintSpooler/res/values/strings.xml
new file mode 100644
index 0000000..8b4b40a
--- /dev/null
+++ b/packages/PrintSpooler/res/values/strings.xml
@@ -0,0 +1,103 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<resources>
+
+    <!-- Title of the PrintSpooler application. [CHAR LIMIT=16] -->
+    <string name="app_label">Print Spooler</string>
+
+    <!-- Title of the print dialog. [CHAR LIMIT=10] -->
+    <string name="print_job_config_dialog_title">Print</string>
+
+    <!-- Label of the print dialog's print button. [CHAR LIMIT=16] -->
+    <string name="print_button">Print</string>
+
+    <!-- Label of the print dialog's cancel button. [CHAR LIMIT=16] -->
+    <string name="cancel_button">Cancel</string>
+
+    <!-- Label of the destination spinner. [CHAR LIMIT=16] -->
+    <string name="label_destination">Destination</string>
+
+    <!-- Label of the copies count edit text. [CHAR LIMIT=16] -->
+    <string name="label_copies">Copies</string>
+
+    <!-- Label of the media size spinner. [CHAR LIMIT=16] -->
+    <string name="label_media_size">Media size</string>
+
+    <!-- Label of the resolution spinner. [CHAR LIMIT=16] -->
+    <string name="label_resolution">Resolution</string>
+
+    <!-- Label of the input tray spinner. [CHAR LIMIT=16] -->
+    <string name="label_input_tray">Input tray</string>
+
+    <!-- Label of the output tray spinner. [CHAR LIMIT=16] -->
+    <string name="label_output_tray">Output tray</string>
+
+    <!-- Label of the duplex mode spinner. [CHAR LIMIT=16] -->
+    <string name="label_duplex_mode">Duplex mode</string>
+
+    <!-- Label of the color mode spinner. [CHAR LIMIT=16] -->
+    <string name="label_color_mode">Color mode</string>
+
+    <!-- Label of the fitting mode spinner. [CHAR LIMIT=16] -->
+    <string name="label_fitting_mode">Fitting mode</string>
+
+    <!-- Label of the orientation spinner. [CHAR LIMIT=16] -->
+    <string name="label_orientation">Orientation</string>
+
+    <!-- Duplex mode labels. -->
+    <string-array name="duplex_mode_labels">
+        <!-- Duplex mode label: No duplexing. [CHAR LIMIT=20] -->
+        <item>None</item>
+        <!-- Duplex mode label: Turn a page along its long edge, e.g. like a book. [CHAR LIMIT=20] -->
+        <item>Long edge</item>
+        <!-- Duplex mode label: Turn a page along its short edge, e.g. like a notepad. [CHAR LIMIT=20] -->
+        <item>Short edge</item>
+    </string-array>
+
+    <!-- Color mode labels. -->
+    <string-array name="color_mode_labels">
+        <!-- Color modelabel: Monochrome color scheme, e.g. one color is used. [CHAR LIMIT=20] -->
+        <item>Monochrome</item>
+        <!-- Color mode label: Color color scheme, e.g. many colors are used. [CHAR LIMIT=20] -->
+        <item>Color</item>
+    </string-array>
+
+    <!-- Fitting mode labels. -->
+    <string-array name="fitting_mode_labels">
+        <!-- Fitting mode label: No fitting. [CHAR LIMIT=30] -->
+        <item>None</item>
+        <!-- Fitting mode label: Fit the content to the page. [CHAR LIMIT=30] -->
+        <item>Fit to page</item>
+    </string-array>
+
+    <!-- Orientation labels. -->
+    <string-array name="orientation_labels">
+        <!-- Orientation label: Portrait page orientation. [CHAR LIMIT=30] -->
+        <item>Portrait</item>
+        <!-- Orientation label: Landscape page orientation [CHAR LIMIT=30] -->
+        <item>Landscape</item>
+    </string-array>
+
+    <!-- Title of an application permission, listed so the user can choose
+         whether they want to allow the application to do this. -->
+    <string name="permlab_bindPrintSpoolerService">bind to a print spooler service</string>
+    <!-- Description of an application permission, listed so the user can
+         choose whether they want to allow the application to do this. -->
+    <string name="permdesc_bindPrintSpoolerService">Allows the holder to bind to the top-level
+        interface of a print spooler service. Should never be needed for normal apps.</string>
+
+</resources>
diff --git a/packages/PrintSpooler/src/com/android/printspooler/PrintJobConfigActivity.java b/packages/PrintSpooler/src/com/android/printspooler/PrintJobConfigActivity.java
new file mode 100644
index 0000000..ae2fe5c
--- /dev/null
+++ b/packages/PrintSpooler/src/com/android/printspooler/PrintJobConfigActivity.java
@@ -0,0 +1,794 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.printspooler;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Binder;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.Message;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.os.UserHandle;
+import android.os.IBinder.DeathRecipient;
+import android.print.IPrintAdapter;
+import android.print.IPrintManager;
+import android.print.IPrinterDiscoveryObserver;
+import android.print.PageRange;
+import android.print.PrintAttributes;
+import android.print.PrintAttributes.MediaSize;
+import android.print.PrintAttributes.Resolution;
+import android.print.PrintAttributes.Tray;
+import android.print.PrintJobInfo;
+import android.print.PrinterId;
+import android.print.PrinterInfo;
+import android.text.Editable;
+import android.text.InputFilter;
+import android.text.Spanned;
+import android.text.TextUtils;
+import android.text.TextWatcher;
+import android.util.Log;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.WindowManager;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemSelectedListener;
+import android.widget.ArrayAdapter;
+import android.widget.EditText;
+import android.widget.Spinner;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Activity for configuring a print job.
+ */
+public class PrintJobConfigActivity extends Activity {
+
+    private static final boolean DEBUG = false;
+
+    private static final String LOG_TAG = PrintJobConfigActivity.class.getSimpleName();
+
+    public static final String EXTRA_PRINTABLE = "printable";
+    public static final String EXTRA_APP_ID = "appId";
+    public static final String EXTRA_ATTRIBUTES = "attributes";
+    public static final String EXTRA_PRINT_JOB_ID = "printJobId";
+
+    private static final int MIN_COPIES = 1;
+
+    private final List<QueuedAsyncTask<?>> mTaskQueue = new ArrayList<QueuedAsyncTask<?>>();
+
+    private IPrintManager mPrintManager;
+
+    private IPrinterDiscoveryObserver mPrinterDiscoveryObserver;
+
+    private int mAppId;
+    private int mPrintJobId;
+
+    private PrintAttributes mPrintAttributes;
+
+    private final PrintSpooler mPrintSpooler = PrintSpooler.getInstance(this);
+
+    private RemotePrintAdapter mRemotePrintAdapter;
+
+    // UI elements
+
+    private EditText mCopiesEditText;
+
+    private Spinner mDestinationSpinner;
+    public ArrayAdapter<SpinnerItem<PrinterInfo>> mDestinationSpinnerAdapter;
+
+    private Spinner mMediaSizeSpinner;
+    public ArrayAdapter<SpinnerItem<MediaSize>> mMediaSizeSpinnerAdapter;
+
+    private Spinner mResolutionSpinner;
+    public ArrayAdapter<SpinnerItem<Resolution>> mResolutionSpinnerAdapter;
+
+    private Spinner mInputTraySpinner;
+    public ArrayAdapter<SpinnerItem<Tray>> mInputTraySpinnerAdapter;
+
+    private Spinner mOutputTraySpinner;
+    public ArrayAdapter<SpinnerItem<Tray>> mOutputTraySpinnerAdapter;
+
+    private Spinner mDuplexModeSpinner;
+    public ArrayAdapter<SpinnerItem<Integer>> mDuplexModeSpinnerAdapter;
+
+    private Spinner mColorModeSpinner;
+    public ArrayAdapter<SpinnerItem<Integer>> mColorModeSpinnerAdapter;
+
+    private Spinner mFittingModeSpinner;
+    public ArrayAdapter<SpinnerItem<Integer>> mFittingModeSpinnerAdapter;
+
+    private Spinner mOrientationSpinner;
+    public ArrayAdapter<SpinnerItem<Integer>> mOrientationSpinnerAdapter;
+
+    private boolean mPrintStarted;
+
+    private boolean mPrintConfirmed;
+
+    private IBinder mPrinable;
+
+    // TODO: Implement store/restore state.
+
+    private final OnItemSelectedListener mOnItemSelectedListener =
+            new AdapterView.OnItemSelectedListener() {
+        @Override
+        public void onItemSelected(AdapterView<?> spinner, View view, int position, long id) {
+            if (spinner == mDestinationSpinner) {
+                updateUi();
+                notifyPrintableStartIfNeeded();
+            } else if (spinner == mMediaSizeSpinner) {
+                SpinnerItem<MediaSize> mediaItem = mMediaSizeSpinnerAdapter.getItem(position);
+                mPrintAttributes.setMediaSize(mediaItem.value);
+                updatePrintableContentIfNeeded();
+            } else if (spinner == mResolutionSpinner) {
+                SpinnerItem<Resolution> resolutionItem =
+                        mResolutionSpinnerAdapter.getItem(position);
+                mPrintAttributes.setResolution(resolutionItem.value);
+                updatePrintableContentIfNeeded();
+            } else if (spinner == mInputTraySpinner) {
+                SpinnerItem<Tray> inputTrayItem =
+                        mInputTraySpinnerAdapter.getItem(position);
+                mPrintAttributes.setInputTray(inputTrayItem.value);
+            } else if (spinner == mOutputTraySpinner) {
+                SpinnerItem<Tray> outputTrayItem =
+                        mOutputTraySpinnerAdapter.getItem(position);
+                mPrintAttributes.setOutputTray(outputTrayItem.value);
+            } else if (spinner == mDuplexModeSpinner) {
+                SpinnerItem<Integer> duplexModeItem =
+                        mDuplexModeSpinnerAdapter.getItem(position);
+                mPrintAttributes.setDuplexMode(duplexModeItem.value);
+            } else if (spinner == mColorModeSpinner) {
+                SpinnerItem<Integer> colorModeItem =
+                        mColorModeSpinnerAdapter.getItem(position);
+                mPrintAttributes.setColorMode(colorModeItem.value);
+            } else if (spinner == mFittingModeSpinner) {
+                SpinnerItem<Integer> fittingModeItem =
+                        mFittingModeSpinnerAdapter.getItem(position);
+                mPrintAttributes.setFittingMode(fittingModeItem.value);
+            } else if (spinner == mOrientationSpinner) {
+                SpinnerItem<Integer> orientationItem =
+                        mOrientationSpinnerAdapter.getItem(position);
+                mPrintAttributes.setOrientation(orientationItem.value);
+            }
+        }
+
+        @Override
+        public void onNothingSelected(AdapterView<?> parent) {
+            /* do nothing*/
+        }
+    };
+
+    private final TextWatcher mTextWatcher = new TextWatcher() {
+        @Override
+        public void onTextChanged(CharSequence s, int start, int before, int count) {
+            final int copies = Integer.parseInt(mCopiesEditText.getText().toString());
+            mPrintAttributes.setCopies(copies);
+        }
+
+        @Override
+        public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+            /* do nothing */
+        }
+
+        @Override
+        public void afterTextChanged(Editable s) {
+            /* do nothing */
+        }
+    };
+
+    private final InputFilter mInputFilter = new InputFilter() {
+        @Override
+        public CharSequence filter(CharSequence source, int start, int end,
+                Spanned dest, int dstart, int dend) {
+            StringBuffer text = new StringBuffer(dest.toString());
+            text.replace(dstart, dend, source.subSequence(start, end).toString());
+            if (TextUtils.isEmpty(text)) {
+                return dest;
+            }
+            final int copies = Integer.parseInt(text.toString());
+            if (copies < MIN_COPIES) {
+                return dest;
+            }
+            return null;
+        }
+    };
+
+    private final DeathRecipient mDeathRecipient = new DeathRecipient() {
+        @Override
+        public void binderDied() {
+            finish();
+        }
+    };
+
+    @Override
+    protected void onCreate(Bundle bundle) {
+        super.onCreate(bundle);
+        setContentView(R.layout.print_job_config_activity);
+
+        getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN
+                | WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN);
+
+        mPrintManager = (IPrintManager) IPrintManager.Stub.asInterface(
+                ServiceManager.getService(PRINT_SERVICE));
+
+        Bundle extras = getIntent().getExtras();
+
+        mPrintJobId = extras.getInt(EXTRA_PRINT_JOB_ID, -1);
+        if (mPrintJobId < 0) {
+            throw new IllegalArgumentException("Invalid print job id: " + mPrintJobId);
+        }
+
+        mAppId = extras.getInt(EXTRA_APP_ID, -1);
+        if (mAppId < 0) {
+            throw new IllegalArgumentException("Invalid app id: " + mAppId);
+        }
+
+        mPrintAttributes = getIntent().getParcelableExtra(EXTRA_ATTRIBUTES);
+        if (mPrintAttributes == null) {
+            mPrintAttributes = new PrintAttributes.Builder().create();
+        }
+
+        mPrinable = extras.getBinder(EXTRA_PRINTABLE);
+        if (mPrinable == null) {
+            throw new IllegalArgumentException("Printable cannot be null");
+        }
+        mRemotePrintAdapter = new RemotePrintAdapter(IPrintAdapter.Stub.asInterface(mPrinable),
+                mPrintSpooler.generateFileForPrintJob(mPrintJobId));
+
+        try {
+            mPrinable.linkToDeath(mDeathRecipient, 0);
+        } catch (RemoteException re) {
+            finish();
+        }
+
+        mPrinterDiscoveryObserver = new PrintDiscoveryObserver(getMainLooper());
+
+        bindUi();
+    }
+
+    @Override
+    protected void onDestroy() {
+        mPrinable.unlinkToDeath(mDeathRecipient, 0);
+        super.onDestroy();
+    }
+
+    private void bindUi() {
+        // Copies
+        mCopiesEditText = (EditText) findViewById(R.id.copies_edittext);
+        mCopiesEditText.setText(String.valueOf(MIN_COPIES));
+        mCopiesEditText.addTextChangedListener(mTextWatcher);
+        mCopiesEditText.setFilters(new InputFilter[] {mInputFilter});
+
+        // Destination.
+        mDestinationSpinner = (Spinner) findViewById(R.id.destination_spinner);
+        mDestinationSpinnerAdapter = new ArrayAdapter<SpinnerItem<PrinterInfo>>(this,
+                android.R.layout.simple_spinner_dropdown_item);
+        mDestinationSpinner.setAdapter(mDestinationSpinnerAdapter);
+        mDestinationSpinner.setOnItemSelectedListener(mOnItemSelectedListener);
+
+        // Media size.
+        mMediaSizeSpinner = (Spinner) findViewById(R.id.media_size_spinner);
+        mMediaSizeSpinnerAdapter = new ArrayAdapter<SpinnerItem<MediaSize>>(this,
+                android.R.layout.simple_spinner_dropdown_item);
+        mMediaSizeSpinner.setAdapter(mMediaSizeSpinnerAdapter);
+        mMediaSizeSpinner.setOnItemSelectedListener(mOnItemSelectedListener);
+
+        // Resolution.
+        mResolutionSpinner = (Spinner) findViewById(R.id.resolution_spinner);
+        mResolutionSpinnerAdapter = new ArrayAdapter<SpinnerItem<Resolution>>(this,
+                android.R.layout.simple_spinner_dropdown_item);
+        mResolutionSpinner.setAdapter(mResolutionSpinnerAdapter);
+        mResolutionSpinner.setOnItemSelectedListener(mOnItemSelectedListener);
+
+        // Input tray.
+        mInputTraySpinner = (Spinner) findViewById(R.id.input_tray_spinner);
+        mInputTraySpinnerAdapter = new ArrayAdapter<SpinnerItem<Tray>>(this,
+                android.R.layout.simple_spinner_dropdown_item);
+        mInputTraySpinner.setAdapter(mInputTraySpinnerAdapter);
+
+        // Output tray.
+        mOutputTraySpinner = (Spinner) findViewById(R.id.output_tray_spinner);
+        mOutputTraySpinnerAdapter = new ArrayAdapter<SpinnerItem<Tray>>(this,
+                android.R.layout.simple_spinner_dropdown_item);
+        mOutputTraySpinner.setAdapter(mOutputTraySpinnerAdapter);
+        mOutputTraySpinner.setOnItemSelectedListener(mOnItemSelectedListener);
+
+        // Duplex mode.
+        mDuplexModeSpinner = (Spinner) findViewById(R.id.duplex_mode_spinner);
+        mDuplexModeSpinnerAdapter = new ArrayAdapter<SpinnerItem<Integer>>(this,
+                android.R.layout.simple_spinner_dropdown_item);
+        mDuplexModeSpinner.setAdapter(mDuplexModeSpinnerAdapter);
+        mDuplexModeSpinner.setOnItemSelectedListener(mOnItemSelectedListener);
+
+        // Color mode.
+        mColorModeSpinner = (Spinner) findViewById(R.id.color_mode_spinner);
+        mColorModeSpinnerAdapter = new ArrayAdapter<SpinnerItem<Integer>>(this,
+                android.R.layout.simple_spinner_dropdown_item);
+        mColorModeSpinner.setAdapter(mColorModeSpinnerAdapter);
+        mColorModeSpinner.setOnItemSelectedListener(mOnItemSelectedListener);
+
+        // Color mode.
+        mFittingModeSpinner = (Spinner) findViewById(R.id.fitting_mode_spinner);
+        mFittingModeSpinnerAdapter = new ArrayAdapter<SpinnerItem<Integer>>(this,
+                android.R.layout.simple_spinner_dropdown_item);
+        mFittingModeSpinner.setAdapter(mFittingModeSpinnerAdapter);
+        mFittingModeSpinner.setOnItemSelectedListener(mOnItemSelectedListener);
+
+        // Orientation
+        mOrientationSpinner = (Spinner) findViewById(R.id.orientation_spinner);
+        mOrientationSpinnerAdapter = new ArrayAdapter<SpinnerItem<Integer>>(this,
+                android.R.layout.simple_spinner_dropdown_item);
+        mOrientationSpinner.setAdapter(mOrientationSpinnerAdapter);
+        mOrientationSpinner.setOnItemSelectedListener(mOnItemSelectedListener);
+    }
+
+    private void updateUi() {
+        final int selectedIndex = mDestinationSpinner.getSelectedItemPosition();
+        PrinterInfo printer = mDestinationSpinnerAdapter.getItem(selectedIndex).value;
+        printer.getDefaults(mPrintAttributes);
+
+        // Copies.
+        mCopiesEditText.setText(String.valueOf(
+                Math.max(mPrintAttributes.getCopies(), MIN_COPIES)));
+
+        // Media size.
+        mMediaSizeSpinnerAdapter.clear();
+        List<MediaSize> mediaSizes = printer.getMediaSizes();
+        final int mediaSizeCount = mediaSizes.size();
+        for (int i = 0; i < mediaSizeCount; i++) {
+            MediaSize mediaSize = mediaSizes.get(i);
+            mMediaSizeSpinnerAdapter.add(new SpinnerItem<MediaSize>(
+                    mediaSize, mediaSize.getLabel(getPackageManager())));
+        }
+        final int selectedMediaSizeIndex = mediaSizes.indexOf(
+                mPrintAttributes.getMediaSize());
+        mMediaSizeSpinner.setOnItemSelectedListener(null);
+        mMediaSizeSpinner.setSelection(selectedMediaSizeIndex);
+        mMediaSizeSpinner.setOnItemSelectedListener(mOnItemSelectedListener);
+
+        // Resolution.
+        mResolutionSpinnerAdapter.clear();
+        List<Resolution> resolutions = printer.getResolutions();
+        final int resolutionCount = resolutions.size();
+        for (int i = 0; i < resolutionCount; i++) {
+            Resolution resolution = resolutions.get(i);
+            mResolutionSpinnerAdapter.add(new SpinnerItem<Resolution>(
+                    resolution, resolution.getLabel(getPackageManager())));
+        }
+        final int selectedResolutionIndex = resolutions.indexOf(
+                mPrintAttributes.getResolution());
+        mResolutionSpinner.setOnItemSelectedListener(null);
+        mResolutionSpinner.setSelection(selectedResolutionIndex);
+        mResolutionSpinner.setOnItemSelectedListener(mOnItemSelectedListener);
+
+        // Input tray.
+        mInputTraySpinnerAdapter.clear();
+        List<Tray> inputTrays = printer.getInputTrays();
+        final int inputTrayCount = inputTrays.size();
+        for (int i = 0; i < inputTrayCount; i++) {
+            Tray inputTray = inputTrays.get(i);
+            mInputTraySpinnerAdapter.add(new SpinnerItem<Tray>(
+                    inputTray, inputTray.getLabel(getPackageManager())));
+        }
+        final int selectedInputTrayIndex = inputTrays.indexOf(
+                mPrintAttributes.getInputTray());
+        mInputTraySpinner.setSelection(selectedInputTrayIndex);
+
+        // Output tray.
+        mOutputTraySpinnerAdapter.clear();
+        List<Tray> outputTrays = printer.getOutputTrays();
+        final int outputTrayCount = outputTrays.size();
+        for (int i = 0; i < outputTrayCount; i++) {
+            Tray outputTray = outputTrays.get(i);
+            mOutputTraySpinnerAdapter.add(new SpinnerItem<Tray>(
+                    outputTray, outputTray.getLabel(getPackageManager())));
+        }
+        final int selectedOutputTrayIndex = outputTrays.indexOf(
+                mPrintAttributes.getOutputTray());
+        mOutputTraySpinner.setSelection(selectedOutputTrayIndex);
+
+        // Duplex mode.
+        final int duplexModes = printer.getDuplexModes();
+        mDuplexModeSpinnerAdapter.clear();
+        String[] duplexModeLabels = getResources().getStringArray(
+                R.array.duplex_mode_labels);
+        int remainingDuplexModes = duplexModes;
+        while (remainingDuplexModes != 0) {
+            final int duplexBitOffset = Integer.numberOfTrailingZeros(remainingDuplexModes);
+            final int duplexMode = 1 << duplexBitOffset;
+            remainingDuplexModes &= ~duplexMode;
+            mDuplexModeSpinnerAdapter.add(new SpinnerItem<Integer>(duplexMode,
+                    duplexModeLabels[duplexBitOffset]));
+        }
+        final int selectedDuplexModeIndex = Integer.numberOfTrailingZeros(
+                (duplexModes & mPrintAttributes.getDuplexMode()));
+        mDuplexModeSpinner.setSelection(selectedDuplexModeIndex);
+
+        // Color mode.
+        final int colorModes = printer.getColorModes();
+        mColorModeSpinnerAdapter.clear();
+        String[] colorModeLabels = getResources().getStringArray(
+                R.array.color_mode_labels);
+        int remainingColorModes = colorModes;
+        while (remainingColorModes != 0) {
+            final int colorBitOffset = Integer.numberOfTrailingZeros(remainingColorModes);
+            final int colorMode = 1 << colorBitOffset;
+            remainingColorModes &= ~colorMode;
+            mColorModeSpinnerAdapter.add(new SpinnerItem<Integer>(colorMode,
+                    colorModeLabels[colorBitOffset]));
+        }
+        final int selectedColorModeIndex = Integer.numberOfTrailingZeros(
+                (colorModes & mPrintAttributes.getColorMode()));
+        mColorModeSpinner.setSelection(selectedColorModeIndex);
+
+        // Fitting mode.
+        final int fittingModes = printer.getFittingModes();
+        mFittingModeSpinnerAdapter.clear();
+        String[] fittingModeLabels = getResources().getStringArray(
+                R.array.fitting_mode_labels);
+        int remainingFittingModes = fittingModes;
+        while (remainingFittingModes != 0) {
+            final int fittingBitOffset = Integer.numberOfTrailingZeros(remainingFittingModes);
+            final int fittingMode = 1 << fittingBitOffset;
+            remainingFittingModes &= ~fittingMode;
+            mFittingModeSpinnerAdapter.add(new SpinnerItem<Integer>(fittingMode,
+                    fittingModeLabels[fittingBitOffset]));
+        }
+        final int selectedFittingModeIndex = Integer.numberOfTrailingZeros(
+                (fittingModes & mPrintAttributes.getFittingMode()));
+        mFittingModeSpinner.setSelection(selectedFittingModeIndex);
+
+        // Orientation.
+        final int orientations = printer.getOrientations();
+        mOrientationSpinnerAdapter.clear();
+        String[] orientationLabels = getResources().getStringArray(
+                R.array.orientation_labels);
+        int remainingOrientations = orientations;
+        while (remainingOrientations != 0) {
+            final int orientationBitOffset = Integer.numberOfTrailingZeros(remainingOrientations);
+            final int orientation = 1 << orientationBitOffset;
+            remainingOrientations &= ~orientation;
+            mOrientationSpinnerAdapter.add(new SpinnerItem<Integer>(orientation,
+                    orientationLabels[orientationBitOffset]));
+        }
+        final int selectedOrientationIndex = Integer.numberOfTrailingZeros(
+                (orientations & mPrintAttributes.getOrientation()));
+        mOrientationSpinner.setSelection(selectedOrientationIndex);
+    }
+
+    @Override
+    protected void onResume() {
+        super.onResume();
+        try {
+            mPrintManager.startDiscoverPrinters(mPrinterDiscoveryObserver);
+        } catch (RemoteException re) {
+            Log.e(LOG_TAG, "Error starting printer discovery!", re);
+        }
+        notifyPrintableStartIfNeeded();
+    }
+
+    @Override
+    protected void onPause() {
+        super.onPause();
+        try {
+            mPrintManager.stopDiscoverPrinters();
+        } catch (RemoteException re) {
+            Log.e(LOG_TAG, "Error starting printer discovery!", re);
+        }
+        notifyPrintableFinishIfNeeded();
+    }
+
+    @Override
+    public boolean onCreateOptionsMenu(Menu menu) {
+        getMenuInflater().inflate(R.menu.print_job_config_activity, menu);
+        return true;
+    }
+
+    @Override
+    public boolean onOptionsItemSelected(MenuItem item) {
+        if (item.getItemId() == R.id.print_button) {
+            mPrintConfirmed = true;
+            finish();
+        }
+        return super.onOptionsItemSelected(item);
+    }
+
+    private void notifyPrintableStartIfNeeded() {
+        if (mDestinationSpinner.getSelectedItemPosition() < 0
+                || mPrintStarted) {
+            return;
+        }
+        mPrintStarted = true;
+        new QueuedAsyncTask<Void>(mTaskQueue) {
+            @Override
+            protected Void doInBackground(Void... params) {
+                try {
+                    mRemotePrintAdapter.start();
+                } catch (IOException ioe) {
+                    Log.e(LOG_TAG, "Error reading printed data!", ioe);
+                }
+                return null;
+            }
+
+            @Override
+            protected void onPostExecute(Void result) {
+                super.onPostExecute(result);
+                updatePrintableContentIfNeeded();
+            }
+        }.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR, (Void[]) null);
+    }
+
+    private void updatePrintableContentIfNeeded() {
+        if (!mPrintStarted) {
+            return;
+        }
+
+        mPrintSpooler.setPrintJobAttributes(mPrintJobId, mPrintAttributes);
+
+        // TODO: Implement page selector.
+        final List<PageRange> pages = new ArrayList<PageRange>();
+        pages.add(PageRange.ALL_PAGES);
+
+        new QueuedAsyncTask<File>(mTaskQueue) {
+            @Override
+            protected File doInBackground(Void... params) {
+                try {
+                    mRemotePrintAdapter.printAttributesChanged(mPrintAttributes);
+                    mRemotePrintAdapter.cancelPrint();
+                    mRemotePrintAdapter.print(pages);
+                    return mRemotePrintAdapter.getFile();
+                } catch (IOException ioe) {
+                    Log.e(LOG_TAG, "Error reading printed data!", ioe);
+                }
+                return null;
+            }
+
+            @Override
+            protected void onPostExecute(File file) {
+                super.onPostExecute(file);
+                updatePrintPreview(file);
+            }
+        }.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR, (Void[]) null);
+    }
+
+    private void notifyPrintableFinishIfNeeded() {
+        if (!mPrintStarted) {
+            return;
+        }
+        mPrintStarted = false;
+
+        // Cancel all pending async tasks if the activity was canceled.
+        if (!mPrintConfirmed) {
+            final int taskCount = mTaskQueue.size();
+            for (int i = taskCount - 1; i >= 0; i--) {
+                mTaskQueue.remove(i).cancel();
+            }
+        }
+
+        new AsyncTask<Void, Void, Void>() {
+            @Override
+            protected Void doInBackground(Void... params) {
+                // Notify the app that printing completed.
+                try {
+                    mRemotePrintAdapter.finish();
+                } catch (IOException ioe) {
+                    Log.e(LOG_TAG, "Error reading printed data!", ioe);
+                }
+
+                // If canceled, nothing to do.
+                if (!mPrintConfirmed) {
+                    mPrintSpooler.setPrintJobState(mPrintJobId,
+                            PrintJobInfo.STATE_CANCELED);
+                    return null;
+                }
+
+                // No printer, nothing to do.
+                final int selectedIndex = mDestinationSpinner.getSelectedItemPosition();
+                if (selectedIndex < 0) {
+                    // Update the print job's status.
+                    mPrintSpooler.setPrintJobState(mPrintJobId,
+                            PrintJobInfo.STATE_CANCELED);
+                    return null;
+                }
+
+                // Update the print job's printer.
+                SpinnerItem<PrinterInfo> printerItem =
+                        mDestinationSpinnerAdapter.getItem(selectedIndex);
+                PrinterId printerId =  printerItem.value.getId();
+                mPrintSpooler.setPrintJobPrinterId(mPrintJobId, printerId);
+
+                // Update the print job's status.
+                mPrintSpooler.setPrintJobState(mPrintJobId,
+                        PrintJobInfo.STATE_QUEUED);
+                return null;
+            }
+
+            // Important: If we are canceling, then we do not wait for the write
+            // to complete since the result will be discarded anyway, we simply
+            // execute the finish immediately which will interrupt the write.
+        }.executeOnExecutor(mPrintConfirmed ? AsyncTask.SERIAL_EXECUTOR
+                : AsyncTask.THREAD_POOL_EXECUTOR, (Void[]) null);
+
+        if (DEBUG) {
+            if (mPrintConfirmed) {
+                File file = mRemotePrintAdapter.getFile();
+                if (file.exists()) {
+                    new ViewSpooledFileAsyncTask(file).executeOnExecutor(
+                          AsyncTask.SERIAL_EXECUTOR, (Void[]) null);
+                }
+            }
+        }
+    }
+
+    private void updatePrintPreview(File file) {
+        // TODO: Implement
+    }
+
+    private void addPrinters(List<PrinterInfo> addedPrinters) {
+        final int addedPrinterCount = addedPrinters.size();
+        for (int i = 0; i < addedPrinterCount; i++) {
+            PrinterInfo addedPrinter = addedPrinters.get(i);
+            boolean duplicate = false;
+            final int existingPrinterCount = mDestinationSpinnerAdapter.getCount();
+            for (int j = 0; j < existingPrinterCount; j++) {
+                PrinterInfo existingPrinter = mDestinationSpinnerAdapter.getItem(j).value;
+                if (addedPrinter.getId().equals(existingPrinter.getId())) {
+                    duplicate = true;
+                    break;
+                }
+            }
+            if (!duplicate) {
+                mDestinationSpinnerAdapter.add(new SpinnerItem<PrinterInfo>(
+                        addedPrinter, addedPrinter.getLabel()));
+            } else {
+                Log.w(LOG_TAG, "Skipping a duplicate printer: " + addedPrinter);
+            }
+        }
+    }
+
+    private void removePrinters(List<PrinterId> pritnerIds) {
+        final int printerIdCount = pritnerIds.size();
+        for (int i = 0; i < printerIdCount; i++) {
+            PrinterId removedPrinterId = pritnerIds.get(i);
+            boolean removed = false;
+            final int existingPrinterCount = mDestinationSpinnerAdapter.getCount();
+            for (int j = 0; j < existingPrinterCount; j++) {
+                PrinterInfo existingPrinter = mDestinationSpinnerAdapter.getItem(j).value;
+                if (removedPrinterId.equals(existingPrinter.getId())) {
+                    mDestinationSpinnerAdapter.remove(mDestinationSpinnerAdapter.getItem(j));
+                    removed = true;
+                    break;
+                }
+            }
+            if (!removed) {
+                Log.w(LOG_TAG, "Ignoring not added printer with id: " + removedPrinterId);
+            }
+        }
+    }
+
+    private abstract class QueuedAsyncTask<T> extends AsyncTask<Void, Void, T> {
+
+        private final List<QueuedAsyncTask<?>> mPendingOrRunningTasks;
+
+        public QueuedAsyncTask(List<QueuedAsyncTask<?>> pendingOrRunningTasks) {
+            mPendingOrRunningTasks = pendingOrRunningTasks;
+        }
+
+        @Override
+        protected void onPreExecute() {
+            mPendingOrRunningTasks.add(this);
+        }
+
+        @Override
+        protected void onPostExecute(T result) {
+            mPendingOrRunningTasks.remove(this);
+        }
+
+        public void cancel() {
+            super.cancel(true);
+            mPendingOrRunningTasks.remove(this);
+        }
+    }
+
+    private final class ViewSpooledFileAsyncTask extends AsyncTask<Void, Void, Void> {
+
+        private final File mFile;
+
+        public ViewSpooledFileAsyncTask(File file) {
+            mFile = file;
+        }
+
+        @Override
+        protected Void doInBackground(Void... params) {
+            mFile.setExecutable(true, false);
+            mFile.setWritable(true, false);
+            mFile.setReadable(true, false);
+
+            final long identity = Binder.clearCallingIdentity();
+            Intent intent = new Intent(Intent.ACTION_VIEW);
+            intent.setDataAndType(Uri.fromFile(mFile), "application/pdf");
+            intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+            startActivityAsUser(intent, null, UserHandle.CURRENT);
+            Binder.restoreCallingIdentity(identity);
+            return null;
+        }
+    }
+
+    private final class PrintDiscoveryObserver extends IPrinterDiscoveryObserver.Stub {
+        private static final int MESSAGE_ADD_DICOVERED_PRINTERS = 1;
+        private static final int MESSAGE_REMOVE_DICOVERED_PRINTERS = 2;
+
+        private final Handler mHandler;
+
+        @SuppressWarnings("unchecked")
+        public PrintDiscoveryObserver(Looper looper) {
+            mHandler = new Handler(looper, null, true) {
+                @Override
+                public void handleMessage(Message message) {
+                    switch (message.what) {
+                        case MESSAGE_ADD_DICOVERED_PRINTERS: {
+                            List<PrinterInfo> printers = (List<PrinterInfo>) message.obj;
+                            addPrinters(printers);
+                            // Just added the first printer, so select it and start printing.
+                            if (mDestinationSpinnerAdapter.getCount() == 1) {
+                                mDestinationSpinner.setSelection(0);
+                            }
+                        } break;
+                        case MESSAGE_REMOVE_DICOVERED_PRINTERS: {
+                            List<PrinterId> printerIds = (List<PrinterId>) message.obj;
+                            removePrinters(printerIds);
+                            // TODO: Handle removing the last printer.
+                        } break;
+                    }
+                }
+            };
+        }
+
+        @Override
+        public void addDiscoveredPrinters(List<PrinterInfo> printers) {
+            mHandler.obtainMessage(MESSAGE_ADD_DICOVERED_PRINTERS, printers).sendToTarget();
+        }
+
+        @Override
+        public void removeDiscoveredPrinters(List<PrinterId> printers) {
+            mHandler.obtainMessage(MESSAGE_REMOVE_DICOVERED_PRINTERS, printers).sendToTarget();
+        }
+    }
+
+    private final class SpinnerItem<T> {
+        final T value;
+        CharSequence label;
+
+        public SpinnerItem(T value, CharSequence label) {
+            this.value = value;
+            this.label = label;
+        }
+
+        public String toString() {
+            return label.toString();
+        }
+    }
+}
diff --git a/packages/PrintSpooler/src/com/android/printspooler/PrintSpooler.java b/packages/PrintSpooler/src/com/android/printspooler/PrintSpooler.java
new file mode 100644
index 0000000..2b27b69
--- /dev/null
+++ b/packages/PrintSpooler/src/com/android/printspooler/PrintSpooler.java
@@ -0,0 +1,734 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.printspooler;
+
+import java.io.Closeable;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlSerializer;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.os.AsyncTask;
+import android.os.ParcelFileDescriptor;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.print.IPrintClient;
+import android.print.IPrintManager;
+import android.print.PrintAttributes;
+import android.print.PrintJobInfo;
+import android.print.PrintManager;
+import android.print.PrinterId;
+import android.util.AtomicFile;
+import android.util.Log;
+import android.util.Slog;
+import android.util.Xml;
+
+import com.android.internal.util.FastXmlSerializer;
+
+public class PrintSpooler {
+
+    private static final String LOG_TAG = PrintSpooler.class.getSimpleName();
+
+    private static final boolean DEBUG_PRINT_JOB_LIFECYCLE = false;
+
+    private static final boolean DEBUG_PERSISTENCE = false;
+
+    private static final boolean PERSISTNECE_MANAGER_ENABLED = false;
+
+    private static final String PRINT_FILE_EXTENSION = "pdf";
+
+    private static int sPrintJobIdCounter;
+
+    private static final Object sLock = new Object();
+
+    private final Object mLock = new Object();
+
+    private static PrintSpooler sInstance;
+
+    private final List<PrintJobInfo> mPrintJobs = new ArrayList<PrintJobInfo>();
+
+    private final PersistenceManager mPersistanceManager;
+
+    private final Context mContext;
+
+    private final IPrintManager mPrintManager;
+
+    public static PrintSpooler getInstance(Context context) {
+        synchronized (sLock) {
+            if (sInstance == null) {
+                sInstance = new PrintSpooler(context);
+            }
+            return sInstance;
+        }
+    }
+
+    private PrintSpooler(Context context) {
+        mContext = context;
+        mPersistanceManager = new PersistenceManager();
+        mPersistanceManager.readStateLocked();
+        mPrintManager = IPrintManager.Stub.asInterface(
+                ServiceManager.getService("print"));
+    }
+
+    public List<PrintJobInfo> getPrintJobs(ComponentName componentName, int state, int appId) {
+        synchronized (mLock) {
+            List<PrintJobInfo> foundPrintJobs = null;
+            final int printJobCount = mPrintJobs.size();
+            for (int i = 0; i < printJobCount; i++) {
+                PrintJobInfo printJob = mPrintJobs.get(i);
+                PrinterId printerId = printJob.getPrinterId();
+                final boolean sameComponent = (componentName == null
+                        || (printerId != null
+                        && componentName.equals(printerId.getServiceComponentName())));
+                final boolean sameAppId = appId == PrintManager.APP_ID_ANY
+                        || printJob.getAppId() == appId;
+                final boolean sameState = state == PrintJobInfo.STATE_ANY
+                        || state == printJob.getState();
+                if (sameComponent && sameAppId && sameState) {
+                    if (foundPrintJobs == null) {
+                        foundPrintJobs = new ArrayList<PrintJobInfo>();
+                    }
+                    foundPrintJobs.add(printJob);
+                }
+            }
+            return foundPrintJobs;
+        }
+    }
+
+    public PrintJobInfo getPrintJob(int printJobId, int appId) {
+        synchronized (mLock) {
+            final int printJobCount = mPrintJobs.size();
+            for (int i = 0; i < printJobCount; i++) {
+                PrintJobInfo printJob = mPrintJobs.get(i);
+                if (printJob.getId() == printJobId
+                        && (appId == PrintManager.APP_ID_ANY || appId == printJob.getAppId())) {
+                    return printJob;
+                }
+             }
+            return null;
+        }
+    }
+
+    public boolean cancelPrintJob(int printJobId, int appId) {
+        synchronized (mLock) {
+            PrintJobInfo printJob = getPrintJob(printJobId, appId);
+            if (printJob != null) {
+                switch (printJob.getState()) {
+                    case PrintJobInfo.STATE_CREATED: {
+                        removePrintJobLocked(printJob);
+                    } return true;
+                    case PrintJobInfo.STATE_QUEUED: {
+                        removePrintJobLocked(printJob);
+                    } return true;
+                    default: {
+                        return false;
+                    }
+                }
+            }
+            return false;
+        }
+    }
+
+    public PrintJobInfo createPrintJob(CharSequence label, IPrintClient client,
+            PrintAttributes attributes, int appId) {
+        synchronized (mLock) {
+            final int printJobId = generatePrintJobIdLocked();
+            PrintJobInfo printJob = new PrintJobInfo();
+            printJob.setId(printJobId);
+            printJob.setAppId(appId);
+            printJob.setLabel(label);
+            printJob.setAttributes(attributes);
+
+            addPrintJobLocked(printJob);
+            setPrintJobState(printJobId, PrintJobInfo.STATE_CREATED);
+
+            return printJob;
+        }
+    }
+
+    private int generatePrintJobIdLocked() {
+        int printJobId = sPrintJobIdCounter++;
+        while (isDuplicatePrintJobId(printJobId)) {
+            printJobId = sPrintJobIdCounter++;
+        }
+        return printJobId;
+    }
+
+    private boolean isDuplicatePrintJobId(int printJobId) {
+        final int printJobCount = mPrintJobs.size();
+        for (int j = 0; j < printJobCount; j++) {
+            PrintJobInfo printJob = mPrintJobs.get(j);
+            if (printJob.getId() == printJobId) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    @SuppressWarnings("resource")
+    public boolean writePrintJobData(ParcelFileDescriptor fd, int printJobId) {
+        synchronized (mLock) {
+            FileInputStream in = null;
+            FileOutputStream out = null;
+            try {
+                PrintJobInfo printJob = getPrintJob(printJobId, PrintManager.APP_ID_ANY);
+                if (printJob != null) {
+                    File file = generateFileForPrintJob(printJobId);
+                    in = new FileInputStream(file);
+                    out = new FileOutputStream(fd.getFileDescriptor());
+                    final byte[] buffer = new byte[8192];
+                    while (true) {
+                        final int readByteCount = in.read(buffer);
+                        if (readByteCount < 0) {
+                            return true;
+                        }
+                        out.write(buffer, 0, readByteCount);
+                    }
+                }
+            } catch (FileNotFoundException fnfe) {
+                Log.e(LOG_TAG, "Error writing print job data!", fnfe);
+            } catch (IOException ioe) {
+                Log.e(LOG_TAG, "Error writing print job data!", ioe);
+            } finally {
+                closeIfNotNullNoException(in);
+                closeIfNotNullNoException(out);
+                closeIfNotNullNoException(fd);
+            }
+        }
+        return false;
+    }
+
+    private void closeIfNotNullNoException(Closeable closeable) {
+        if (closeable != null) {
+            try {
+                closeable.close();
+            } catch (IOException ioe) {
+                /* ignore */;
+            }
+        }
+    }
+
+    public File generateFileForPrintJob(int printJobId) {
+        return new File(mContext.getFilesDir(), "print_job_"
+                + printJobId + "." + PRINT_FILE_EXTENSION);
+    }
+
+    private void addPrintJobLocked(PrintJobInfo printJob) {
+        mPrintJobs.add(printJob);
+        if (DEBUG_PRINT_JOB_LIFECYCLE) {
+            Slog.i(LOG_TAG, "[ADD] " + printJob);
+        }
+    }
+
+    private void removePrintJobLocked(PrintJobInfo printJob) {
+        if (mPrintJobs.remove(printJob)) {
+            generateFileForPrintJob(printJob.getId()).delete();
+            if (DEBUG_PRINT_JOB_LIFECYCLE) {
+                Slog.i(LOG_TAG, "[REMOVE] " + printJob);
+            }
+        }
+    }
+
+    public boolean setPrintJobState(int printJobId, int state) {
+        boolean success = false;
+        PrintJobInfo queuedPrintJob = null;
+
+        synchronized (mLock) {
+            PrintJobInfo printJob = getPrintJob(printJobId, PrintManager.APP_ID_ANY);
+            if (printJob != null && printJob.getState() < state) {
+                success = true;
+                printJob.setState(state);
+                // TODO: Update notifications.
+                switch (state) {
+                    case PrintJobInfo.STATE_COMPLETED:
+                    case PrintJobInfo.STATE_CANCELED: {
+                        removePrintJobLocked(printJob);
+                    } break;
+                    case PrintJobInfo.STATE_QUEUED: {
+                        queuedPrintJob = new PrintJobInfo(printJob);
+                    } break;
+                }
+                if (DEBUG_PRINT_JOB_LIFECYCLE) {
+                    Slog.i(LOG_TAG, "[STATUS CHANGED] " + printJob);
+                }
+                mPersistanceManager.writeStateLocked();
+            }
+        }
+
+        if (queuedPrintJob != null) {
+            try {
+                mPrintManager.onPrintJobQueued(queuedPrintJob.getPrinterId(),
+                        queuedPrintJob);
+            } catch (RemoteException re) {
+                /* ignore */
+            }
+        }
+
+        return success;
+    }
+
+    public boolean setPrintJobTag(int printJobId, String tag) {
+        synchronized (mLock) {
+            PrintJobInfo printJob = getPrintJob(printJobId, PrintManager.APP_ID_ANY);
+            if (printJob != null) {
+                printJob.setTag(tag);
+                mPersistanceManager.writeStateLocked();
+                return true;
+            }
+        }
+        return false;
+    }
+
+    public void setPrintJobAttributes(int printJobId, PrintAttributes attributes) {
+        synchronized (mLock) {
+            PrintJobInfo printJob = getPrintJob(printJobId, PrintManager.APP_ID_ANY);
+            if (printJob != null) {
+                printJob.setAttributes(attributes);
+                mPersistanceManager.writeStateLocked();
+            }
+        }
+    }
+
+    public void setPrintJobPrinterId(int printJobId, PrinterId printerId) {
+        synchronized (mLock) {
+            PrintJobInfo printJob = getPrintJob(printJobId, PrintManager.APP_ID_ANY);
+            if (printJob != null) {
+                printJob.setPrinterId(printerId);
+                mPersistanceManager.writeStateLocked();
+            }
+        }
+    }
+
+    private final class PersistenceManager {
+        private static final String PERSIST_FILE_NAME = "print_spooler_state.xml";
+
+        private static final String TAG_SPOOLER = "spooler";
+        private static final String TAG_JOB = "job";
+        private static final String TAG_ID = "id";
+        private static final String TAG_TAG = "tag";
+        private static final String TAG_APP_ID = "app-id";
+        private static final String TAG_STATE = "state";
+        private static final String TAG_ATTRIBUTES = "attributes";
+        private static final String TAG_LABEL = "label";
+        private static final String TAG_PRINTER = "printer";
+
+        private static final String ATTRIBUTE_MEDIA_SIZE = "mediaSize";
+        private static final String ATTRIBUTE_RESOLUTION = "resolution";
+        private static final String ATTRIBUTE_MARGINS = "margins";
+        private static final String ATTRIBUTE_INPUT_TRAY = "inputTray";
+        private static final String ATTRIBUTE_OUTPUT_TRAY = "outputTray";
+        private static final String ATTRIBUTE_DUPLEX_MODE = "duplexMode";
+        private static final String ATTRIBUTE_COLOR_MODE = "colorMode";
+        private static final String ATTRIBUTE_FITTING_MODE = "fittingMode";
+        private static final String ATTRIBUTE_ORIENTATION = "orientation";
+
+        private final AtomicFile mStatePersistFile;
+
+        private boolean mWriteStateScheduled;
+
+        private PersistenceManager() {
+            mStatePersistFile = new AtomicFile(new File(mContext.getFilesDir(),
+                    PERSIST_FILE_NAME));
+        }
+
+        public void writeStateLocked() {
+            // TODO: Implement persistence of PrintableInfo
+            if (!PERSISTNECE_MANAGER_ENABLED) {
+                return;
+            }
+            if (mWriteStateScheduled) {
+                return;
+            }
+            mWriteStateScheduled = true;
+            new AsyncTask<Void, Void, Void>() {
+                @Override
+                protected Void doInBackground(Void... params) {
+                    synchronized (mLock) {
+                        mWriteStateScheduled = false;
+                        doWriteStateLocked();
+                    }
+                    return null;
+                }
+            }.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR, (Void[]) null);
+        }
+
+        private void doWriteStateLocked() {
+            FileOutputStream out = null;
+            try {
+                out = mStatePersistFile.startWrite();
+
+                XmlSerializer serializer = new FastXmlSerializer();
+                serializer.setOutput(out, "utf-8");
+                serializer.startDocument(null, true);
+                serializer.startTag(null, TAG_SPOOLER);
+
+                List<PrintJobInfo> printJobs = mPrintJobs;
+
+                final int printJobCount = printJobs.size();
+                for (int j = 0; j < printJobCount; j++) {
+                    PrintJobInfo printJob = printJobs.get(j);
+
+                    final int state = printJob.getState();
+                    if (state < PrintJobInfo.STATE_QUEUED
+                            || state > PrintJobInfo.STATE_FAILED) {
+                        continue;
+                    }
+
+                    serializer.startTag(null, TAG_JOB);
+
+                    serializer.startTag(null, TAG_ID);
+                    serializer.text(String.valueOf(printJob.getId()));
+                    serializer.endTag(null, TAG_ID);
+
+                    serializer.startTag(null, TAG_TAG);
+                    serializer.text(printJob.getTag());
+                    serializer.endTag(null, TAG_TAG);
+
+                    serializer.startTag(null, TAG_APP_ID);
+                    serializer.text(String.valueOf(printJob.getAppId()));
+                    serializer.endTag(null, TAG_APP_ID);
+
+                    serializer.startTag(null, TAG_LABEL);
+                    serializer.text(printJob.getLabel().toString());
+                    serializer.endTag(null, TAG_LABEL);
+
+                    serializer.startTag(null, TAG_STATE);
+                    serializer.text(String.valueOf(printJob.getState()));
+                    serializer.endTag(null, TAG_STATE);
+
+                    serializer.startTag(null, TAG_PRINTER);
+                    serializer.text(printJob.getPrinterId().flattenToString());
+                    serializer.endTag(null, TAG_PRINTER);
+
+                    PrintAttributes attributes = printJob.getAttributes();
+                    if (attributes != null) {
+                        serializer.startTag(null, TAG_ATTRIBUTES);
+
+                            //TODO: Implement persistence of the attributes below.
+
+//                            MediaSize mediaSize = attributes.getMediaSize();
+//                            if (mediaSize != null) {
+//                                serializer.attribute(null, ATTRIBUTE_MEDIA_SIZE,
+//                                        mediaSize.flattenToString());
+//                            }
+//
+//                            Resolution resolution = attributes.getResolution();
+//                            if (resolution != null) {
+//                                serializer.attribute(null, ATTRIBUTE_RESOLUTION,
+//                                        resolution.flattenToString());
+//                            }
+//
+//                            Margins margins = attributes.getMargins();
+//                            if (margins != null) {
+//                                serializer.attribute(null, ATTRIBUTE_MARGINS,
+//                                        margins.flattenToString());
+//                            }
+//
+//                            Tray inputTray = attributes.getInputTray();
+//                            if (inputTray != null) {
+//                                serializer.attribute(null, ATTRIBUTE_INPUT_TRAY,
+//                                        inputTray.flattenToString());
+//                            }
+//
+//                            Tray outputTray = attributes.getOutputTray();
+//                            if (outputTray != null) {
+//                                serializer.attribute(null, ATTRIBUTE_OUTPUT_TRAY,
+//                                        outputTray.flattenToString());
+//                            }
+
+                        final int duplexMode = attributes.getDuplexMode();
+                        if (duplexMode > 0) {
+                            serializer.attribute(null, ATTRIBUTE_DUPLEX_MODE,
+                                    String.valueOf(duplexMode));
+                        }
+
+                        final int colorMode = attributes.getColorMode();
+                        if (colorMode > 0) {
+                            serializer.attribute(null, ATTRIBUTE_COLOR_MODE,
+                                    String.valueOf(colorMode));
+                        }
+
+                        final int fittingMode = attributes.getFittingMode();
+                        if (fittingMode > 0) {
+                            serializer.attribute(null, ATTRIBUTE_FITTING_MODE,
+                                    String.valueOf(fittingMode));
+                        }
+
+                        final int orientation = attributes.getOrientation();
+                        if (orientation > 0) {
+                            serializer.attribute(null, ATTRIBUTE_ORIENTATION,
+                                    String.valueOf(orientation));
+                        }
+
+                        serializer.endTag(null, TAG_ATTRIBUTES);
+                    }
+
+                    serializer.endTag(null, TAG_JOB);
+
+                    if (DEBUG_PERSISTENCE) {
+                        Log.i(LOG_TAG, "[PERSISTED] " + printJob);
+                    }
+                }
+
+                serializer.endTag(null, TAG_SPOOLER);
+                serializer.endDocument();
+                mStatePersistFile.finishWrite(out);
+            } catch (IOException e) {
+                Slog.w(LOG_TAG, "Failed to write state, restoring backup.", e);
+                mStatePersistFile.failWrite(out);
+            } finally {
+                if (out != null) {
+                    try {
+                        out.close();
+                    } catch (IOException ioe) {
+                        /* ignore */
+                    }
+                }
+            }
+        }
+
+        public void readStateLocked() {
+            if (!PERSISTNECE_MANAGER_ENABLED) {
+                return;
+            }
+            FileInputStream in = null;
+            try {
+                in = mStatePersistFile.openRead();
+            } catch (FileNotFoundException e) {
+                Log.i(LOG_TAG, "No existing print spooler state.");
+                return;
+            }
+            try {
+                XmlPullParser parser = Xml.newPullParser();
+                parser.setInput(in, null);
+                parseState(parser);
+            } catch (IllegalStateException ise) {
+                Slog.w(LOG_TAG, "Failed parsing " + ise);
+            } catch (NullPointerException npe) {
+                Slog.w(LOG_TAG, "Failed parsing " + npe);
+            } catch (NumberFormatException nfe) {
+                Slog.w(LOG_TAG, "Failed parsing " + nfe);
+            } catch (XmlPullParserException xppe) {
+                Slog.w(LOG_TAG, "Failed parsing " + xppe);
+            } catch (IOException ioe) {
+                Slog.w(LOG_TAG, "Failed parsing " + ioe);
+            } catch (IndexOutOfBoundsException iobe) {
+                Slog.w(LOG_TAG, "Failed parsing " + iobe);
+            } finally {
+                try {
+                    in.close();
+                } catch (IOException ioe) {
+                    /* ignore */
+                }
+            }
+        }
+
+        private void parseState(XmlPullParser parser)
+                throws IOException, XmlPullParserException {
+            parser.next();
+            skipEmptyTextTags(parser);
+            expect(parser, XmlPullParser.START_TAG, TAG_SPOOLER);
+            parser.next();
+
+            while (parsePrintJob(parser)) {
+                parser.next();
+            }
+
+            skipEmptyTextTags(parser);
+            expect(parser, XmlPullParser.END_TAG, TAG_SPOOLER);
+        }
+
+        private boolean parsePrintJob(XmlPullParser parser)
+                throws IOException, XmlPullParserException {
+            skipEmptyTextTags(parser);
+            if (!accept(parser, XmlPullParser.START_TAG, TAG_JOB)) {
+                return false;
+            }
+            parser.next();
+
+            skipEmptyTextTags(parser);
+            expect(parser, XmlPullParser.START_TAG, TAG_ID);
+            parser.next();
+            final int printJobId = Integer.parseInt(parser.getText());
+            parser.next();
+            skipEmptyTextTags(parser);
+            expect(parser, XmlPullParser.END_TAG, TAG_ID);
+            parser.next();
+
+            skipEmptyTextTags(parser);
+            expect(parser, XmlPullParser.START_TAG, TAG_TAG);
+            parser.next();
+            String tag = parser.getText();
+            parser.next();
+            skipEmptyTextTags(parser);
+            expect(parser, XmlPullParser.END_TAG, TAG_TAG);
+            parser.next();
+
+            skipEmptyTextTags(parser);
+            expect(parser, XmlPullParser.START_TAG, TAG_APP_ID);
+            parser.next();
+            final int appId = Integer.parseInt(parser.getText());
+            parser.next();
+            skipEmptyTextTags(parser);
+            expect(parser, XmlPullParser.END_TAG, TAG_APP_ID);
+            parser.next();
+
+            skipEmptyTextTags(parser);
+            expect(parser, XmlPullParser.START_TAG, TAG_LABEL);
+            parser.next();
+            String label = parser.getText();
+            parser.next();
+            skipEmptyTextTags(parser);
+            expect(parser, XmlPullParser.END_TAG, TAG_LABEL);
+            parser.next();
+
+            skipEmptyTextTags(parser);
+            expect(parser, XmlPullParser.START_TAG, TAG_STATE);
+            parser.next();
+            final int state = Integer.parseInt(parser.getText());
+            parser.next();
+            skipEmptyTextTags(parser);
+            expect(parser, XmlPullParser.END_TAG, TAG_STATE);
+            parser.next();
+
+            skipEmptyTextTags(parser);
+            expect(parser, XmlPullParser.START_TAG, TAG_PRINTER);
+            parser.next();
+            PrinterId printerId = PrinterId.unflattenFromString(parser.getText());
+            parser.next();
+            skipEmptyTextTags(parser);
+            expect(parser, XmlPullParser.END_TAG, TAG_PRINTER);
+            parser.next();
+
+            skipEmptyTextTags(parser);
+            expect(parser, XmlPullParser.START_TAG, TAG_ATTRIBUTES);
+
+            final int attributeCount = parser.getAttributeCount();
+            PrintAttributes attributes = null;
+            if (attributeCount > 0) {
+                PrintAttributes.Builder builder = new PrintAttributes.Builder();
+
+                // TODO: Implement reading of the attributes below.
+
+//                String mediaSize = parser.getAttributeValue(null, ATTRIBUTE_MEDIA_SIZE);
+//                if (mediaSize != null) {
+//                    builder.setMediaSize(MediaSize.unflattenFromString(mediaSize));
+//                }
+//
+//                String resolution = parser.getAttributeValue(null, ATTRIBUTE_RESOLUTION);
+//                if (resolution != null) {
+//                    builder.setMediaSize(Resolution.unflattenFromString(resolution));
+//                }
+//
+//                String margins = parser.getAttributeValue(null, ATTRIBUTE_MARGINS);
+//                if (margins != null) {
+//                    builder.setMediaSize(Margins.unflattenFromString(margins));
+//                }
+//
+//                String inputTray = parser.getAttributeValue(null, ATTRIBUTE_INPUT_TRAY);
+//                if (inputTray != null) {
+//                    builder.setMediaSize(Tray.unflattenFromString(inputTray));
+//                }
+//
+//                String outputTray = parser.getAttributeValue(null, ATTRIBUTE_OUTPUT_TRAY);
+//                if (outputTray != null) {
+//                    builder.setMediaSize(Tray.unflattenFromString(outputTray));
+//                }
+//
+//                String duplexMode = parser.getAttributeValue(null, ATTRIBUTE_DUPLEX_MODE);
+//                if (duplexMode != null) {
+//                    builder.setDuplexMode(Integer.parseInt(duplexMode));
+//                }
+
+                String colorMode = parser.getAttributeValue(null, ATTRIBUTE_COLOR_MODE);
+                if (colorMode != null) {
+                    builder.setColorMode(Integer.parseInt(colorMode));
+                }
+
+                String fittingMode = parser.getAttributeValue(null, ATTRIBUTE_COLOR_MODE);
+                if (fittingMode != null) {
+                    builder.setFittingMode(Integer.parseInt(fittingMode));
+                }
+            }
+            parser.next();
+            skipEmptyTextTags(parser);
+            expect(parser, XmlPullParser.END_TAG, TAG_ATTRIBUTES);
+            parser.next();
+
+            PrintJobInfo printJob = new PrintJobInfo();
+            printJob.setId(printJobId);
+            printJob.setTag(tag);
+            printJob.setAppId(appId);
+            printJob.setLabel(label);
+            printJob.setState(state);
+            printJob.setAttributes(attributes);
+            printJob.setPrinterId(printerId);
+
+            mPrintJobs.add(printJob);
+
+            if (DEBUG_PERSISTENCE) {
+                Log.i(LOG_TAG, "[RESTORED] " + printJob);
+            }
+
+            skipEmptyTextTags(parser);
+            expect(parser, XmlPullParser.END_TAG, TAG_JOB);
+
+            return true;
+        }
+
+        private void expect(XmlPullParser parser, int type, String tag)
+                throws IOException, XmlPullParserException {
+            if (!accept(parser, type, tag)) {
+                throw new XmlPullParserException("Exepected event: " + type
+                        + " and tag: " + tag + " but got event: " + parser.getEventType()
+                        + " and tag:" + parser.getName());
+            }
+        }
+
+        private void skipEmptyTextTags(XmlPullParser parser)
+                throws IOException, XmlPullParserException {
+            while (accept(parser, XmlPullParser.TEXT, null)
+                    && "\n".equals(parser.getText())) {
+                parser.next();
+            }
+        }
+
+        private boolean accept(XmlPullParser parser, int type, String tag)
+                throws IOException, XmlPullParserException {
+            if (parser.getEventType() != type) {
+                return false;
+            }
+            if (tag != null) {
+                if (!tag.equals(parser.getName())) {
+                    return false;
+                }
+            } else if (parser.getName() != null) {
+                return false;
+            }
+            return true;
+        }
+    }
+}
diff --git a/packages/PrintSpooler/src/com/android/printspooler/PrintSpoolerService.java b/packages/PrintSpooler/src/com/android/printspooler/PrintSpoolerService.java
new file mode 100644
index 0000000..57c4557
--- /dev/null
+++ b/packages/PrintSpooler/src/com/android/printspooler/PrintSpoolerService.java
@@ -0,0 +1,187 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.printspooler;
+
+import java.util.List;
+
+import android.app.PendingIntent;
+import android.app.Service;
+import android.content.ComponentName;
+import android.content.Intent;
+import android.content.IntentSender;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.Message;
+import android.os.ParcelFileDescriptor;
+import android.os.RemoteException;
+import android.print.IPrintAdapter;
+import android.print.IPrintClient;
+import android.print.IPrintSpoolerService;
+import android.print.IPrintSpoolerServiceCallbacks;
+import android.print.PrintAttributes;
+import android.print.PrintJobInfo;
+import android.util.Slog;
+
+import com.android.internal.os.SomeArgs;
+
+/**
+ * Service for exposing some of the {@link PrintSpooler} functionality to
+ * another process.
+ */
+public final class PrintSpoolerService extends Service {
+
+    private static final String LOG_TAG = "PrintSpoolerService";
+
+    private Intent mStartPrintJobConfigActivityIntent;
+
+    private PrintSpooler mSpooler;
+
+    private Handler mHanlder;
+
+    @Override
+    public void onCreate() {
+        super.onCreate();
+        mStartPrintJobConfigActivityIntent = new Intent(PrintSpoolerService.this,
+                PrintJobConfigActivity.class);
+        mSpooler = PrintSpooler.getInstance(this);
+        mHanlder = new MyHandler(getMainLooper());
+    }
+
+    @Override
+    public IBinder onBind(Intent intent) {
+        return new IPrintSpoolerService.Stub() {
+            @Override
+            public void getPrintJobs(IPrintSpoolerServiceCallbacks callback,
+                    ComponentName componentName, int state, int appId, int sequence)
+                            throws RemoteException {
+                List<PrintJobInfo> printJobs = null;
+                try {
+                    printJobs = mSpooler.getPrintJobs(componentName, state, appId);
+                } finally {
+                    callback.onGetPrintJobsResult(printJobs, sequence);
+                }
+            }
+
+            @Override
+            public void getPrintJob(int printJobId, IPrintSpoolerServiceCallbacks callback,
+                    int appId, int sequence) throws RemoteException {
+                PrintJobInfo printJob = null;
+                try {
+                    printJob = mSpooler.getPrintJob(printJobId, appId);
+                } finally {
+                    callback.onGetPrintJobInfoResult(printJob, sequence);
+                }
+            }
+
+            @Override
+            public void cancelPrintJob(int printJobId, IPrintSpoolerServiceCallbacks callback,
+                    int appId, int sequence) throws RemoteException {
+                boolean success = false;
+                try {
+                    success = mSpooler.cancelPrintJob(printJobId, appId);
+                } finally {
+                    callback.onCancelPrintJobResult(success, sequence);
+                }
+            }
+
+            @SuppressWarnings("deprecation")
+            @Override
+            public void createPrintJob(String printJobName, IPrintClient client,
+                    IPrintAdapter printAdapter, PrintAttributes attributes,
+                    IPrintSpoolerServiceCallbacks callback, int appId, int sequence)
+                            throws RemoteException {
+                PrintJobInfo printJob = null;
+                try {
+                    printJob = mSpooler.createPrintJob(printJobName, client,
+                            attributes, appId);
+                    if (printJob != null) {
+                        Intent intent = mStartPrintJobConfigActivityIntent;
+                        intent.putExtra(PrintJobConfigActivity.EXTRA_PRINTABLE,
+                                printAdapter.asBinder());
+                        intent.putExtra(PrintJobConfigActivity.EXTRA_APP_ID, appId);
+                        intent.putExtra(PrintJobConfigActivity.EXTRA_PRINT_JOB_ID,
+                                printJob.getId());
+                        intent.putExtra(PrintJobConfigActivity.EXTRA_ATTRIBUTES, attributes);
+
+                        IntentSender sender = PendingIntent.getActivity(
+                                PrintSpoolerService.this, 0, intent, PendingIntent.FLAG_ONE_SHOT
+                                | PendingIntent.FLAG_CANCEL_CURRENT).getIntentSender();
+
+                        SomeArgs args = SomeArgs.obtain();
+                        args.arg1 = client;
+                        args.arg2 = sender;
+                        mHanlder.obtainMessage(0, args).sendToTarget();
+                    }
+                } finally {
+                    callback.onCreatePrintJobResult(printJob, sequence);
+                }
+            }
+
+            @Override
+            public void setPrintJobState(int printJobId, int state,
+                    IPrintSpoolerServiceCallbacks callback, int sequece)
+                            throws RemoteException {
+                boolean success = false;
+                try {
+                    // TODO: Make sure the clients (print services) can set the state
+                    //       only to acceptable ones, e.g. not settings STATE_CREATED.
+                    success = mSpooler.setPrintJobState(printJobId, state);
+                } finally {
+                    callback.onSetPrintJobStateResult(success, sequece);
+                }
+            }
+
+            @Override
+            public void setPrintJobTag(int printJobId, String tag,
+                    IPrintSpoolerServiceCallbacks callback, int sequece)
+                            throws RemoteException {
+                boolean success = false;
+                try {
+                    success = mSpooler.setPrintJobTag(printJobId, tag);
+                } finally {
+                    callback.onSetPrintJobTagResult(success, sequece);
+                }
+            }
+
+            @Override
+            public void writePrintJobData(ParcelFileDescriptor fd, int printJobId) {
+                mSpooler.writePrintJobData(fd, printJobId);
+            }
+        };
+    }
+
+    private static final class MyHandler extends Handler {
+
+        public MyHandler(Looper looper) {
+            super(looper, null, true);
+        }
+
+        @Override
+        public void handleMessage(Message message) {
+            SomeArgs args = (SomeArgs) message.obj;
+            IPrintClient client = (IPrintClient) args.arg1;
+            IntentSender sender = (IntentSender) args.arg2;
+            args.recycle();
+            try {
+                client.startPrintJobConfigActivity(sender);
+            } catch (RemoteException re) {
+                Slog.i(LOG_TAG, "Error starting print job config activity!", re);
+            }
+        }
+    }
+}
diff --git a/packages/PrintSpooler/src/com/android/printspooler/RemotePrintAdapter.java b/packages/PrintSpooler/src/com/android/printspooler/RemotePrintAdapter.java
new file mode 100644
index 0000000..7537218
--- /dev/null
+++ b/packages/PrintSpooler/src/com/android/printspooler/RemotePrintAdapter.java
@@ -0,0 +1,219 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.printspooler;
+
+import android.os.ICancellationSignal;
+import android.os.ParcelFileDescriptor;
+import android.os.RemoteException;
+import android.print.IPrintAdapter;
+import android.print.IPrintProgressListener;
+import android.print.PageRange;
+import android.print.PrintAdapterInfo;
+import android.print.PrintAttributes;
+import android.util.Log;
+
+import libcore.io.IoUtils;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.List;
+
+/**
+ * This class represents a remote print adapter instance.
+ */
+final class RemotePrintAdapter {
+    private static final String LOG_TAG = "RemotePrintAdapter";
+
+    private static final boolean DEBUG = true;
+
+    private final Object mLock = new Object();
+
+    private final IPrintAdapter mRemoteInterface;
+
+    private final File mFile;
+
+    private final IPrintProgressListener mIPrintProgressListener;
+
+    private PrintAdapterInfo mInfo;
+
+    private ICancellationSignal mCancellationSignal;
+
+    private Thread mWriteThread;
+
+    public RemotePrintAdapter(IPrintAdapter printAdatper, File file) {
+        mRemoteInterface = printAdatper;
+        mFile = file;
+        mIPrintProgressListener = new IPrintProgressListener.Stub() {
+            @Override
+            public void onWriteStarted(PrintAdapterInfo info,
+                    ICancellationSignal cancellationSignal) {
+                if (DEBUG) {
+                    Log.i(LOG_TAG, "IPrintProgressListener#onWriteStarted()");
+                }
+                synchronized (mLock) {
+                    mInfo = info;
+                    mCancellationSignal = cancellationSignal;
+                }
+            }
+
+            @Override
+            public void onWriteFinished(List<PageRange> pages) {
+                if (DEBUG) {
+                    Log.i(LOG_TAG, "IPrintProgressListener#onWriteFinished(" + pages + ")");
+                }
+                synchronized (mLock) {
+                    if (isPrintingLocked()) {
+                        mWriteThread.interrupt();
+                        mCancellationSignal = null;
+                    }
+                }
+            }
+
+            @Override
+            public void onWriteFailed(CharSequence error) {
+                if (DEBUG) {
+                    Log.i(LOG_TAG, "IPrintProgressListener#onWriteFailed(" + error + ")");
+                }
+                synchronized (mLock) {
+                    if (isPrintingLocked()) {
+                        mWriteThread.interrupt();
+                        mCancellationSignal = null;
+                    }
+                }
+            }
+        };
+    }
+
+    public File getFile() {
+        if (DEBUG) {
+            Log.i(LOG_TAG, "getFile()");
+        }
+        return mFile;
+    }
+
+    public void start() throws IOException {
+        if (DEBUG) {
+            Log.i(LOG_TAG, "start()");
+        }
+        try {
+            mRemoteInterface.start();
+        } catch (RemoteException re) {
+            throw new IOException("Error reading file", re);
+        }
+    }
+
+    public void printAttributesChanged(PrintAttributes attributes) throws IOException {
+        if (DEBUG) {
+            Log.i(LOG_TAG, "printAttributesChanged(" + attributes +")");
+        }
+        try {
+            mRemoteInterface.printAttributesChanged(attributes);
+        } catch (RemoteException re) {
+            throw new IOException("Error reading file", re);
+        }
+    }
+
+    public void print(List<PageRange> pages) throws IOException {
+        if (DEBUG) {
+            Log.i(LOG_TAG, "print(" + pages +")");
+        }
+        InputStream in = null;
+        OutputStream out = null;
+        ParcelFileDescriptor source = null;
+        ParcelFileDescriptor sink = null;
+        synchronized (mLock) {
+            mWriteThread = Thread.currentThread();
+        }
+        try {
+            ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createPipe();
+            source = pipe[0];
+            sink = pipe[1];
+
+            in = new FileInputStream(source.getFileDescriptor());
+            out = new FileOutputStream(mFile);
+
+            // Async call to initiate the other process writing the data.
+            mRemoteInterface.print(pages, sink, mIPrintProgressListener);
+
+            // Close the source. It is now held by the client.
+            sink.close();
+            sink = null;
+
+            final byte[] buffer = new byte[8192];
+            while (true) {
+                if (Thread.currentThread().isInterrupted()) {
+                    Thread.currentThread().interrupt();
+                    break;
+                }
+                final int readByteCount = in.read(buffer);
+                if (readByteCount < 0) {
+                    break;
+                }
+                out.write(buffer, 0, readByteCount);
+            }
+        } catch (RemoteException re) {
+            throw new IOException("Error reading file", re);
+        } catch (IOException ioe) {
+            throw new IOException("Error reading file", ioe);
+        } finally {
+            IoUtils.closeQuietly(in);
+            IoUtils.closeQuietly(out);
+            IoUtils.closeQuietly(sink);
+            IoUtils.closeQuietly(source);
+        }
+    }
+
+    public void cancelPrint() throws IOException {
+        if (DEBUG) {
+            Log.i(LOG_TAG, "cancelPrint()");
+        }
+        synchronized (mLock) {
+            if (isPrintingLocked()) {
+                try {
+                    mCancellationSignal.cancel();
+                } catch (RemoteException re) {
+                    throw new IOException("Error cancelling print", re);
+                }
+            }
+        }
+    }
+
+    public void finish() throws IOException {
+        if (DEBUG) {
+            Log.i(LOG_TAG, "finish()");
+        }
+        try {
+            mRemoteInterface.finish();
+        } catch (RemoteException re) {
+            throw new IOException("Error reading file", re);
+        }
+    }
+
+    public PrintAdapterInfo getInfo() {
+        synchronized (mLock) {
+            return mInfo;
+        }
+    }
+
+    private boolean isPrintingLocked() {
+        return mCancellationSignal != null;
+    }
+}