diff options
Diffstat (limited to 'packages/PrintSpooler')
11 files changed, 2595 insertions, 0 deletions
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; + } +} |