diff options
Diffstat (limited to 'packages/PrintSpooler')
61 files changed, 5176 insertions, 3958 deletions
diff --git a/packages/PrintSpooler/Android.mk b/packages/PrintSpooler/Android.mk index 9e7b969..96592b4 100644 --- a/packages/PrintSpooler/Android.mk +++ b/packages/PrintSpooler/Android.mk @@ -24,4 +24,6 @@ LOCAL_PACKAGE_NAME := PrintSpooler LOCAL_JAVA_LIBRARIES := framework-base +LOCAL_STATIC_JAVA_LIBRARIES := android-support-v4 + include $(BUILD_PACKAGE) diff --git a/packages/PrintSpooler/AndroidManifest.xml b/packages/PrintSpooler/AndroidManifest.xml index e1d0aec..4c0bbb8 100644 --- a/packages/PrintSpooler/AndroidManifest.xml +++ b/packages/PrintSpooler/AndroidManifest.xml @@ -42,23 +42,23 @@ <uses-permission android:name="android.permission.START_PRINT_SERVICE_CONFIG_ACTIVITY"/> <application - android:allowClearUserData="true" - android:label="@string/app_label" - android:allowBackup= "false" - android:supportsRtl="true" - android:icon="@*android:drawable/ic_print"> + android:allowClearUserData="true" + android:label="@string/app_label" + android:allowBackup= "false" + android:supportsRtl="true" + android:icon="@*android:drawable/ic_print"> <service - android:name=".PrintSpoolerService" + android:name=".model.PrintSpoolerService" android:exported="true" android:permission="android.permission.BIND_PRINT_SPOOLER_SERVICE"> </service> <activity - android:name=".PrintJobConfigActivity" + android:name=".ui.PrintActivity" android:configChanges="orientation|screenSize" android:permission="android.permission.BIND_PRINT_SPOOLER_SERVICE" - android:theme="@style/PrintJobConfigActivityTheme"> + android:theme="@android:style/Theme.DeviceDefault.NoActionBar"> <intent-filter> <action android:name="android.print.PRINT_DIALOG" /> <category android:name="android.intent.category.DEFAULT" /> @@ -67,7 +67,7 @@ </activity> <activity - android:name=".SelectPrinterActivity" + android:name=".ui.SelectPrinterActivity" android:label="@string/all_printers_label" android:theme="@style/SelectPrinterActivityTheme" android:exported="false"> diff --git a/packages/PrintSpooler/res/drawable-hdpi/ic_expand_less_24dp.png b/packages/PrintSpooler/res/drawable-hdpi/ic_expand_less_24dp.png Binary files differnew file mode 100644 index 0000000..d2e5408 --- /dev/null +++ b/packages/PrintSpooler/res/drawable-hdpi/ic_expand_less_24dp.png diff --git a/packages/PrintSpooler/res/drawable-hdpi/ic_expand_more_24dp.png b/packages/PrintSpooler/res/drawable-hdpi/ic_expand_more_24dp.png Binary files differnew file mode 100644 index 0000000..f4c4b0c --- /dev/null +++ b/packages/PrintSpooler/res/drawable-hdpi/ic_expand_more_24dp.png diff --git a/packages/PrintSpooler/res/drawable-hdpi/ic_grayedout_printer.png b/packages/PrintSpooler/res/drawable-hdpi/ic_grayedout_printer.png Binary files differnew file mode 100644 index 0000000..5e54970 --- /dev/null +++ b/packages/PrintSpooler/res/drawable-hdpi/ic_grayedout_printer.png diff --git a/packages/PrintSpooler/res/drawable-hdpi/print_button_background.png b/packages/PrintSpooler/res/drawable-hdpi/print_button_background.png Binary files differnew file mode 100644 index 0000000..88e8495 --- /dev/null +++ b/packages/PrintSpooler/res/drawable-hdpi/print_button_background.png diff --git a/packages/PrintSpooler/res/drawable-mdpi/ic_expand_less_24dp.png b/packages/PrintSpooler/res/drawable-mdpi/ic_expand_less_24dp.png Binary files differnew file mode 100644 index 0000000..3220eea --- /dev/null +++ b/packages/PrintSpooler/res/drawable-mdpi/ic_expand_less_24dp.png diff --git a/packages/PrintSpooler/res/drawable-mdpi/ic_expand_more_24dp.png b/packages/PrintSpooler/res/drawable-mdpi/ic_expand_more_24dp.png Binary files differnew file mode 100644 index 0000000..5530f52 --- /dev/null +++ b/packages/PrintSpooler/res/drawable-mdpi/ic_expand_more_24dp.png diff --git a/packages/PrintSpooler/res/drawable-mdpi/ic_grayedout_printer.png b/packages/PrintSpooler/res/drawable-mdpi/ic_grayedout_printer.png Binary files differnew file mode 100644 index 0000000..5e54970 --- /dev/null +++ b/packages/PrintSpooler/res/drawable-mdpi/ic_grayedout_printer.png diff --git a/packages/PrintSpooler/res/drawable-mdpi/print_button_background.png b/packages/PrintSpooler/res/drawable-mdpi/print_button_background.png Binary files differnew file mode 100644 index 0000000..3a37b27 --- /dev/null +++ b/packages/PrintSpooler/res/drawable-mdpi/print_button_background.png diff --git a/packages/PrintSpooler/res/drawable-xhdpi/ic_expand_less_24dp.png b/packages/PrintSpooler/res/drawable-xhdpi/ic_expand_less_24dp.png Binary files differnew file mode 100644 index 0000000..f007427 --- /dev/null +++ b/packages/PrintSpooler/res/drawable-xhdpi/ic_expand_less_24dp.png diff --git a/packages/PrintSpooler/res/drawable-xhdpi/ic_expand_more_24dp.png b/packages/PrintSpooler/res/drawable-xhdpi/ic_expand_more_24dp.png Binary files differnew file mode 100644 index 0000000..43debb3 --- /dev/null +++ b/packages/PrintSpooler/res/drawable-xhdpi/ic_expand_more_24dp.png diff --git a/packages/PrintSpooler/res/drawable-xhdpi/ic_grayedout_printer.png b/packages/PrintSpooler/res/drawable-xhdpi/ic_grayedout_printer.png Binary files differnew file mode 100644 index 0000000..5e54970 --- /dev/null +++ b/packages/PrintSpooler/res/drawable-xhdpi/ic_grayedout_printer.png diff --git a/packages/PrintSpooler/res/drawable-xhdpi/print_button_background.png b/packages/PrintSpooler/res/drawable-xhdpi/print_button_background.png Binary files differnew file mode 100644 index 0000000..b2ed8cd --- /dev/null +++ b/packages/PrintSpooler/res/drawable-xhdpi/print_button_background.png diff --git a/packages/PrintSpooler/res/drawable-xxhdpi/ic_expand_less_24dp.png b/packages/PrintSpooler/res/drawable-xxhdpi/ic_expand_less_24dp.png Binary files differnew file mode 100644 index 0000000..39bc2ba --- /dev/null +++ b/packages/PrintSpooler/res/drawable-xxhdpi/ic_expand_less_24dp.png diff --git a/packages/PrintSpooler/res/drawable-xxhdpi/ic_expand_more_24dp.png b/packages/PrintSpooler/res/drawable-xxhdpi/ic_expand_more_24dp.png Binary files differnew file mode 100644 index 0000000..664f3f2 --- /dev/null +++ b/packages/PrintSpooler/res/drawable-xxhdpi/ic_expand_more_24dp.png diff --git a/packages/PrintSpooler/res/drawable-xxxhdpi/ic_expand_less_24dp.png b/packages/PrintSpooler/res/drawable-xxxhdpi/ic_expand_less_24dp.png Binary files differnew file mode 100644 index 0000000..fe9c539 --- /dev/null +++ b/packages/PrintSpooler/res/drawable-xxxhdpi/ic_expand_less_24dp.png diff --git a/packages/PrintSpooler/res/drawable-xxxhdpi/ic_expand_more_24dp.png b/packages/PrintSpooler/res/drawable-xxxhdpi/ic_expand_more_24dp.png Binary files differnew file mode 100644 index 0000000..18d075c --- /dev/null +++ b/packages/PrintSpooler/res/drawable-xxxhdpi/ic_expand_more_24dp.png diff --git a/packages/PrintSpooler/res/drawable/ic_expand_less.xml b/packages/PrintSpooler/res/drawable/ic_expand_less.xml new file mode 100644 index 0000000..b0c7d51 --- /dev/null +++ b/packages/PrintSpooler/res/drawable/ic_expand_less.xml @@ -0,0 +1,43 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2014 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. +--> + +<selector xmlns:android="http://schemas.android.com/apk/res/android" + android:autoMirrored="true"> + + <item + android:state_checked="true"> + <bitmap + android:src="@drawable/ic_expand_less_24dp" + android:tint="?android:attr/colorControlActivated"> + </bitmap> + </item> + + <item + android:state_pressed="true"> + <bitmap + android:src="@drawable/ic_expand_less_24dp" + android:tint="?android:attr/colorControlActivated"> + </bitmap> + </item> + + <item> + <bitmap + android:src="@drawable/ic_expand_less_24dp" + android:tint="?android:attr/colorControlNormal"> + </bitmap> + </item> + +</selector> diff --git a/packages/PrintSpooler/res/drawable/ic_expand_more.xml b/packages/PrintSpooler/res/drawable/ic_expand_more.xml new file mode 100644 index 0000000..b809c25 --- /dev/null +++ b/packages/PrintSpooler/res/drawable/ic_expand_more.xml @@ -0,0 +1,43 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2014 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. +--> + +<selector xmlns:android="http://schemas.android.com/apk/res/android" + android:autoMirrored="true"> + + <item + android:state_checked="true"> + <bitmap + android:src="@drawable/ic_expand_more_24dp" + android:tint="?android:attr/colorControlActivated"> + </bitmap> + </item> + + <item + android:state_pressed="true"> + <bitmap + android:src="@drawable/ic_expand_more_24dp" + android:tint="?android:attr/colorControlActivated"> + </bitmap> + </item> + + <item> + <bitmap + android:src="@drawable/ic_expand_more_24dp" + android:tint="?android:attr/colorControlNormal"> + </bitmap> + </item> + +</selector> diff --git a/packages/PrintSpooler/res/layout/print_job_config_activity_container.xml b/packages/PrintSpooler/res/drawable/print_button.xml index 3303ef1..d4b6a82 100644 --- a/packages/PrintSpooler/res/layout/print_job_config_activity_container.xml +++ b/packages/PrintSpooler/res/drawable/print_button.xml @@ -1,11 +1,12 @@ <?xml version="1.0" encoding="utf-8"?> -<!-- Copyright (C) 2013 The Android Open Source Project +<!-- + Copyright (C) 2014 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 + 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, @@ -14,13 +15,10 @@ limitations under the License. --> -<com.android.printspooler.PrintDialogFrame xmlns:android="http://schemas.android.com/apk/res/android" - android:layout_width="fill_parent" - android:layout_height="wrap_content"> - <FrameLayout - android:id="@+id/content_container" - android:layout_width="fill_parent" - android:layout_height="fill_parent" - android:background="@color/container_background"> - </FrameLayout> -</com.android.printspooler.PrintDialogFrame> +<ripple xmlns:android="http://schemas.android.com/apk/res/android" + android:tint="@color/print_button_tint_color" + android:pinned="true"> + <item + android:drawable="@drawable/print_button_background"> + </item> +</ripple> diff --git a/packages/PrintSpooler/res/layout/print_activity.xml b/packages/PrintSpooler/res/layout/print_activity.xml new file mode 100644 index 0000000..9715322 --- /dev/null +++ b/packages/PrintSpooler/res/layout/print_activity.xml @@ -0,0 +1,389 @@ +<?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. +--> + +<com.android.printspooler.widget.ContentView + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:printspooler="http://schemas.android.com/apk/res/com.android.printspooler" + android:id="@+id/options_content" + android:layout_width="fill_parent" + android:layout_height="fill_parent" + android:visibility="invisible" + android:background="?android:attr/colorForeground"> + + <FrameLayout + android:id="@+id/static_content" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:padding="16dip" + android:background="?android:attr/colorForegroundInverse"> + + <!-- Destination --> + + <Spinner + android:id="@+id/destination_spinner" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:dropDownWidth="wrap_content" + android:minHeight="?android:attr/listPreferredItemHeightSmall"> + </Spinner> + + </FrameLayout> + + <!-- Summary --> + + <LinearLayout + android:id="@+id/summary_content" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:paddingStart="16dip" + android:paddingEnd="16dip" + android:orientation="horizontal" + android:background="?android:attr/colorForegroundInverse"> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="8dip" + android:layout_marginStart="12dip" + android:textAppearance="?android:attr/textAppearanceSmall" + android:labelFor="@+id/copies_count_summary" + android:text="@string/label_copies_summary"> + </TextView> + + <TextView + android:id="@+id/copies_count_summary" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="8dip" + android:layout_marginStart="16dip" + android:textAppearance="?android:attr/textAppearanceMedium"> + </TextView> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="8dip" + android:layout_marginStart="32dip" + android:textAppearance="?android:attr/textAppearanceSmall" + android:labelFor="@+id/paper_size_summary" + android:text="@string/label_paper_size_summary"> + </TextView> + + <TextView + android:id="@+id/paper_size_summary" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="8dip" + android:layout_marginStart="16dip" + android:textAppearance="?android:attr/textAppearanceMedium"> + </TextView> + + </LinearLayout> + + <FrameLayout + android:id="@+id/dynamic_content" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:paddingBottom="16dip"> + + <LinearLayout + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:orientation="vertical"> + + <LinearLayout + android:id="@+id/draggable_content" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:orientation="vertical"> + + <com.android.printspooler.widget.PrintOptionsLayout + android:id="@+id/options_container" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:background="?android:attr/colorForegroundInverse" + printspooler:columnCount="@integer/print_option_column_count"> + + <LinearLayout + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginStart="16dip" + android:layout_marginEnd="16dip" + android:orientation="vertical"> + + <!-- Copies --> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="8dip" + android:layout_marginStart="12dip" + android:textAppearance="?android:attr/textAppearanceSmall" + android:labelFor="@+id/copies_edittext" + android:text="@string/label_copies"> + </TextView> + + <view + class="com.android.printspooler.widget.FirstFocusableEditText" + android:id="@+id/copies_edittext" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + style="?android:attr/editTextStyle" + android:inputType="numberDecimal"> + </view> + + </LinearLayout> + + <LinearLayout + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginStart="16dip" + android:layout_marginEnd="16dip" + android:orientation="vertical"> + + <!-- Paper size --> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="8dip" + android:layout_marginStart="12dip" + android:textAppearance="?android:attr/textAppearanceSmall" + android:labelFor="@+id/paper_size_spinner" + android:text="@string/label_paper_size"> + </TextView> + + <Spinner + android:id="@+id/paper_size_spinner" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + style="@style/PrintOptionSpinnerStyle"> + </Spinner> + + </LinearLayout> + + <LinearLayout + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginStart="16dip" + android:layout_marginEnd="16dip" + android:orientation="vertical"> + + <!-- Color --> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="8dip" + android:layout_marginStart="12dip" + android:textAppearance="?android:attr/textAppearanceSmall" + android:labelFor="@+id/color_spinner" + android:text="@string/label_color"> + </TextView> + + <Spinner + android:id="@+id/color_spinner" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + style="@style/PrintOptionSpinnerStyle"> + </Spinner> + + </LinearLayout> + + <LinearLayout + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginStart="16dip" + android:layout_marginEnd="16dip" + android:orientation="vertical"> + + <!-- Orientation --> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="8dip" + android:layout_marginStart="12dip" + android:textAppearance="?android:attr/textAppearanceSmall" + android:labelFor="@+id/orientation_spinner" + android:text="@string/label_orientation"> + </TextView> + + <Spinner + android:id="@+id/orientation_spinner" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + style="@style/PrintOptionSpinnerStyle"> + </Spinner> + + </LinearLayout> + + <LinearLayout + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginStart="16dip" + android:layout_marginEnd="16dip" + android:orientation="vertical"> + + <!-- Range options --> + + <TextView + android:id="@+id/range_options_title" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="8dip" + android:layout_marginStart="12dip" + android:textAppearance="?android:attr/textAppearanceSmall" + android:labelFor="@+id/range_options_spinner" + android:text="@string/page_count_unknown"> + </TextView> + + <Spinner + android:id="@+id/range_options_spinner" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + style="@style/PrintOptionSpinnerStyle"> + </Spinner> + + </LinearLayout> + + <LinearLayout + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginStart="16dip" + android:layout_marginEnd="16dip" + android:orientation="vertical"> + + <!-- Pages --> + + <TextView + android:id="@+id/page_range_title" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="8dip" + android:layout_marginStart="12dip" + android:textAppearance="?android:attr/textAppearanceSmall" + android:text="@string/pages_range_example" + android:labelFor="@+id/page_range_edittext" + android:textAllCaps="false" + android:visibility="visible"> + </TextView> + + <view + class="com.android.printspooler.widget.FirstFocusableEditText" + android:id="@+id/page_range_edittext" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:layout_gravity="bottom|fill_horizontal" + style="@style/PrintOptionEditTextStyle" + android:visibility="visible" + android:inputType="textNoSuggestions"> + </view> + + </LinearLayout> + + </com.android.printspooler.widget.PrintOptionsLayout> + + <!-- More options --> + + <LinearLayout + android:id="@+id/more_options_container" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:paddingStart="28dip" + android:paddingEnd="28dip" + android:orientation="vertical" + android:visibility="visible" + android:background="?android:attr/colorForegroundInverse"> + + <ImageView + android:layout_width="fill_parent" + android:layout_height="1dip" + android:layout_gravity="fill_horizontal" + android:background="?android:attr/colorControlNormal" + android:contentDescription="@null"> + </ImageView> + + <Button + android:id="@+id/more_options_button" + style="?android:attr/borderlessButtonStyle" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:layout_gravity="fill_horizontal" + android:text="@string/more_options_button" + android:gravity="start|center_vertical" + android:textAllCaps="false"> + </Button> + + <ImageView + android:layout_width="fill_parent" + android:layout_height="1dip" + android:layout_gravity="fill_horizontal" + android:background="?android:attr/colorControlNormal" + android:contentDescription="@null"> + </ImageView> + + </LinearLayout> + + </LinearLayout> + + <!-- Expand/collapse handle --> + + <FrameLayout + android:id="@+id/expand_collapse_handle" + android:layout_width="fill_parent" + android:layout_height="?android:attr/listPreferredItemHeightSmall" + android:layout_marginBottom="28dip" + android:background="?android:attr/colorForegroundInverse" + android:elevation="12dip"> + + <ImageButton + android:id="@+id/expand_collapse_icon" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center" + android:background="@drawable/ic_expand_more"> + </ImageButton> + + </FrameLayout> + + </LinearLayout> + + <!-- Print button --> + + <ImageButton + android:id="@+id/print_button" + style="?android:attr/buttonStyleSmall" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="end|bottom" + android:layout_marginEnd="16dip" + android:elevation="12dip" + android:background="@drawable/print_button" + android:src="@*android:drawable/ic_print"> + </ImageButton> + + </FrameLayout> + + + <FrameLayout + android:id="@+id/embedded_content_container" + android:layout_width="fill_parent" + android:layout_height="0dip" + android:animateLayoutChanges="true"> + </FrameLayout> + +</com.android.printspooler.widget.ContentView> diff --git a/packages/PrintSpooler/res/layout/print_error_fragment.xml b/packages/PrintSpooler/res/layout/print_error_fragment.xml new file mode 100644 index 0000000..dc44339 --- /dev/null +++ b/packages/PrintSpooler/res/layout/print_error_fragment.xml @@ -0,0 +1,47 @@ +<?xml version="1.0" encoding="utf-8"?> + +<!-- Copyright (C) 2014 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. + --> + +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="fill_parent" + android:layout_height="fill_parent" + android:gravity="center" + android:orientation="vertical"> + + <ImageView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginBottom="12dip" + android:src="@drawable/ic_grayedout_printer" + android:contentDescription="@null"> + </ImageView> + + <TextView + android:id="@+id/message" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/print_error_default_message" + android:textAppearance="?android:attr/textAppearanceLargeInverse"> + </TextView> + + <Button + android:id="@+id/action_button" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/print_error_retry"> + </Button> + +</LinearLayout> diff --git a/packages/PrintSpooler/res/layout/print_job_config_activity_content_editing.xml b/packages/PrintSpooler/res/layout/print_job_config_activity_content_editing.xml deleted file mode 100644 index e50a7af..0000000 --- a/packages/PrintSpooler/res/layout/print_job_config_activity_content_editing.xml +++ /dev/null @@ -1,282 +0,0 @@ -<?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" - android:orientation="vertical" - android:scrollbars="vertical"> - - <LinearLayout - android:layout_width="fill_parent" - android:layout_height="wrap_content" - android:orientation="vertical"> - - <!-- Destination --> - - <Spinner - android:id="@+id/destination_spinner" - android:layout_width="fill_parent" - android:layout_height="wrap_content" - android:layout_gravity="fill_horizontal" - android:layout_marginTop="24dip" - android:layout_marginStart="24dip" - android:layout_marginEnd="24dip" - android:minHeight="?android:attr/listPreferredItemHeightSmall"> - </Spinner> - - <LinearLayout - android:layout_width="fill_parent" - android:layout_height="wrap_content" - android:layout_marginBottom="24dip" - android:orientation="horizontal" - android:baselineAligned="false"> - - <LinearLayout - android:layout_width="0dip" - android:layout_height="wrap_content" - android:layout_weight="1" - android:orientation="vertical"> - - <!-- Copies --> - - <TextView - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_marginTop="12dip" - android:layout_marginStart="36dip" - android:layout_marginEnd="6dip" - android:textAppearance="@style/PrintOptionTitleTextAppearance" - android:labelFor="@+id/copies_edittext" - android:text="@string/label_copies"> - </TextView> - - <view - class="com.android.printspooler.PrintJobConfigActivity$CustomEditText" - android:id="@+id/copies_edittext" - android:layout_width="fill_parent" - android:layout_height="wrap_content" - android:layout_marginStart="24dip" - android:layout_marginEnd="6dip" - style="@style/PrintOptionEditTextStyle" - android:inputType="numberDecimal"> - </view> - - <!-- Color --> - - <TextView - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_marginTop="12dip" - android:layout_marginStart="36dip" - android:layout_marginEnd="6dip" - android:textAppearance="@style/PrintOptionTitleTextAppearance" - android:labelFor="@+id/color_spinner" - android:text="@string/label_color"> - </TextView> - - <Spinner - android:id="@+id/color_spinner" - android:layout_width="fill_parent" - android:layout_height="wrap_content" - android:layout_marginStart="24dip" - android:layout_marginEnd="6dip" - style="@style/PrintOptionSpinnerStyle"> - </Spinner> - - <!-- Range options --> - - <TextView - android:id="@+id/range_options_title" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_marginTop="12dip" - android:layout_marginStart="36dip" - android:textAppearance="@style/PrintOptionTitleTextAppearance" - android:labelFor="@+id/range_options_spinner" - android:text="@string/page_count_unknown"> - </TextView> - - <Spinner - android:id="@+id/range_options_spinner" - android:layout_width="fill_parent" - android:layout_height="wrap_content" - android:layout_marginStart="24dip" - android:layout_marginEnd="6dip" - style="@style/PrintOptionSpinnerStyle"> - </Spinner> - - </LinearLayout> - - <LinearLayout - android:layout_width="0dip" - android:layout_height="wrap_content" - android:layout_weight="1" - android:orientation="vertical"> - - <!-- Paper size --> - - <TextView - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_marginTop="12dip" - android:layout_marginStart="18dip" - android:layout_marginEnd="24dip" - android:textAppearance="@style/PrintOptionTitleTextAppearance" - android:labelFor="@+id/paper_size_spinner" - android:text="@string/label_paper_size"> - </TextView> - - <Spinner - android:id="@+id/paper_size_spinner" - android:layout_width="fill_parent" - android:layout_height="wrap_content" - android:layout_marginStart="6dip" - android:layout_marginEnd="24dip" - style="@style/PrintOptionSpinnerStyle"> - </Spinner> - - <!-- Orientation --> - - <TextView - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_marginTop="12dip" - android:layout_marginStart="18dip" - android:layout_marginEnd="24dip" - android:textAppearance="@style/PrintOptionTitleTextAppearance" - android:labelFor="@+id/orientation_spinner" - android:text="@string/label_orientation"> - </TextView> - - <Spinner - android:id="@+id/orientation_spinner" - android:layout_width="fill_parent" - android:layout_height="wrap_content" - android:layout_marginStart="6dip" - android:layout_marginEnd="24dip" - style="@style/PrintOptionSpinnerStyle"> - </Spinner> - - <!-- Pages --> - - <TextView - android:id="@+id/page_range_title" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_marginTop="12dip" - android:layout_marginStart="12dip" - android:layout_marginEnd="24dip" - android:textAppearance="@style/PrintOptionTitleTextAppearance" - android:text="@string/pages_range_example" - android:labelFor="@+id/page_range_edittext" - android:textAllCaps="false"> - </TextView> - - <view - class="com.android.printspooler.PrintJobConfigActivity$CustomEditText" - android:id="@+id/page_range_edittext" - android:layout_width="fill_parent" - android:layout_height="wrap_content" - android:layout_marginStart="6dip" - android:layout_marginEnd="24dip" - android:layout_gravity="bottom|fill_horizontal" - style="@style/PrintOptionEditTextStyle" - android:visibility="gone" - android:inputType="textNoSuggestions"> - </view> - - </LinearLayout> - - </LinearLayout> - - <!-- Advanced settings button --> - - <LinearLayout - android:id="@+id/advanced_settings_container" - android:layout_width="fill_parent" - android:layout_height="wrap_content" - android:orientation="vertical" - android:visibility="gone"> - - <ImageView - android:layout_width="fill_parent" - android:layout_height="1dip" - android:layout_marginStart="24dip" - android:layout_marginEnd="24dip" - android:layout_gravity="fill_horizontal" - android:background="@color/separator" - android:contentDescription="@null"> - </ImageView> - - <Button - android:id="@+id/advanced_settings_button" - style="?android:attr/buttonBarButtonStyle" - android:layout_width="fill_parent" - android:layout_height="wrap_content" - android:layout_marginStart="24dip" - android:layout_marginEnd="24dip" - android:layout_gravity="fill_horizontal" - android:text="@string/advanced_settings_button" - android:gravity="start|center_vertical" - android:textSize="16sp" - android:textColor="@color/item_text_color"> - </Button> - - <ImageView - android:layout_width="fill_parent" - android:layout_height="1dip" - android:layout_gravity="fill_horizontal" - android:layout_marginStart="24dip" - android:layout_marginEnd="24dip" - android:background="@color/separator" - android:contentDescription="@null"> - </ImageView> - - </LinearLayout> - - <!-- Print button --> - - <FrameLayout - android:layout_width="fill_parent" - android:layout_height="wrap_content" - android:layout_marginTop="24dip" - android:background="@color/action_button_background"> - - <ImageView - android:layout_width="fill_parent" - android:layout_height="1dip" - android:layout_gravity="fill_horizontal" - android:background="@color/separator" - android:contentDescription="@null"> - </ImageView> - - <Button - android:id="@+id/print_button" - style="?android:attr/buttonBarButtonStyle" - android:layout_width="fill_parent" - android:layout_height="wrap_content" - android:layout_gravity="fill_horizontal" - android:text="@string/print_button" - android:textSize="16sp" - android:textColor="@color/item_text_color"> - </Button> - - </FrameLayout> - - </LinearLayout> - -</ScrollView> diff --git a/packages/PrintSpooler/res/layout/print_job_config_activity_content_error.xml b/packages/PrintSpooler/res/layout/print_job_config_activity_content_error.xml deleted file mode 100644 index d9f0a9a..0000000 --- a/packages/PrintSpooler/res/layout/print_job_config_activity_content_error.xml +++ /dev/null @@ -1,70 +0,0 @@ -<?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. ---> - -<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" - android:id="@+id/content_generating" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:orientation="vertical"> - - <LinearLayout - android:layout_width="fill_parent" - android:layout_height="fill_parent" - android:orientation="vertical"> - - <TextView - android:id="@+id/message" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_marginStart="16dip" - android:layout_marginEnd="16dip" - android:layout_marginTop="32dip" - android:layout_marginBottom="32dip" - android:layout_gravity="center" - style="?android:attr/buttonBarButtonStyle" - android:ellipsize="end" - android:text="@string/print_error_default_message" - android:textColor="@color/important_text" - android:textSize="16sp"> - </TextView> - - </LinearLayout> - - <FrameLayout - android:layout_width="fill_parent" - android:layout_height="wrap_content" - android:background="@color/action_button_background"> - - <View - android:layout_width="fill_parent" - android:layout_height="1dip" - android:background="@color/separator"> - </View> - - <Button - android:id="@+id/ok_button" - android:layout_width="fill_parent" - android:layout_height="wrap_content" - android:layout_gravity="fill_horizontal" - style="?android:attr/buttonBarButtonStyle" - android:text="@android:string/ok" - android:textSize="16sp" - android:textColor="@color/important_text"> - </Button> - - </FrameLayout> - -</LinearLayout> diff --git a/packages/PrintSpooler/res/layout/print_job_config_activity_content_generating.xml b/packages/PrintSpooler/res/layout/print_job_config_activity_content_generating.xml deleted file mode 100644 index 10602ee..0000000 --- a/packages/PrintSpooler/res/layout/print_job_config_activity_content_generating.xml +++ /dev/null @@ -1,79 +0,0 @@ -<?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. ---> - -<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" - android:id="@+id/content_generating" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:orientation="vertical"> - - <LinearLayout - android:layout_width="fill_parent" - android:layout_height="fill_parent" - android:orientation="vertical"> - - <TextView - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_marginStart="16dip" - android:layout_marginEnd="16dip" - android:layout_gravity="center" - style="?android:attr/buttonBarButtonStyle" - android:singleLine="true" - android:ellipsize="end" - android:text="@string/generating_print_job" - android:textColor="@color/important_text" - android:textSize="16sp"> - </TextView> - - <ProgressBar - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_marginStart="32dip" - android:layout_marginEnd="32dip" - android:layout_marginTop="16dip" - android:layout_marginBottom="32dip" - android:layout_gravity="center_horizontal" - style="?android:attr/progressBarStyleLarge"> - </ProgressBar> - - </LinearLayout> - - <FrameLayout - android:layout_width="fill_parent" - android:layout_height="wrap_content" - android:background="@color/action_button_background"> - - <View - android:layout_width="fill_parent" - android:layout_height="1dip" - android:background="@color/separator"> - </View> - - <Button - android:id="@+id/cancel_button" - android:layout_width="fill_parent" - android:layout_height="wrap_content" - android:layout_gravity="fill_horizontal" - style="?android:attr/buttonBarButtonStyle" - android:text="@string/cancel" - android:textSize="16sp" - android:textColor="@color/important_text"> - </Button> - - </FrameLayout> - -</LinearLayout> diff --git a/packages/PrintSpooler/res/layout/print_progress_fragment.xml b/packages/PrintSpooler/res/layout/print_progress_fragment.xml new file mode 100644 index 0000000..212da9e --- /dev/null +++ b/packages/PrintSpooler/res/layout/print_progress_fragment.xml @@ -0,0 +1,65 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2014 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. +--> + +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center" + android:gravity="center" + android:orientation="vertical"> + + <ImageView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginBottom="12dip" + android:src="@drawable/ic_grayedout_printer" + android:contentDescription="@null"> + </ImageView> + + <ProgressBar + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:indeterminate="true" + style="?android:attr/progressBarStyleHorizontal"> + </ProgressBar> + + <FrameLayout + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:minHeight="?android:attr/listPreferredItemHeight" + android:gravity="center" + android:animateLayoutChanges="true"> + + <TextView + android:id="@+id/message" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textAppearance="?android:attr/textAppearanceLargeInverse" + android:text="@string/print_operation_canceling" + android:visibility="gone"> + </TextView> + + <Button + android:id="@+id/cancel_button" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@android:string/cancel"> + </Button> + + </FrameLayout> + +</LinearLayout> + diff --git a/packages/PrintSpooler/res/layout/printer_dropdown_item.xml b/packages/PrintSpooler/res/layout/printer_dropdown_item.xml index 1a61b99..43d8aaf 100644 --- a/packages/PrintSpooler/res/layout/printer_dropdown_item.xml +++ b/packages/PrintSpooler/res/layout/printer_dropdown_item.xml @@ -19,7 +19,7 @@ android:layout_height="wrap_content" android:paddingStart="16dip" android:paddingEnd="16dip" - android:minHeight="?android:attr/listPreferredItemHeightSmall" + android:minHeight="56dip" android:orientation="horizontal" android:gravity="start|center_vertical"> @@ -49,7 +49,7 @@ android:ellipsize="end" android:textIsSelectable="false" android:gravity="top|start" - android:textColor="@color/item_text_color" + android:textColor="?android:attr/textColorPrimary" android:duplicateParentState="true"> </TextView> @@ -62,7 +62,7 @@ android:ellipsize="end" android:textIsSelectable="false" android:visibility="gone" - android:textColor="@color/print_option_title" + android:textColor="?android:attr/textColorPrimary" android:duplicateParentState="true"> </TextView> diff --git a/packages/PrintSpooler/res/layout/printer_list_item.xml b/packages/PrintSpooler/res/layout/printer_list_item.xml index 47eb0b5..1f5efbc 100644 --- a/packages/PrintSpooler/res/layout/printer_list_item.xml +++ b/packages/PrintSpooler/res/layout/printer_list_item.xml @@ -15,13 +15,13 @@ --> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" - android:layout_width="fill_parent" - android:layout_height="wrap_content" - android:paddingStart="?android:attr/listPreferredItemPaddingStart" - android:paddingEnd="?android:attr/listPreferredItemPaddingEnd" - android:minHeight="?android:attr/listPreferredItemHeight" - android:orientation="horizontal" - android:gravity="start|center_vertical"> + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:paddingStart="?android:attr/listPreferredItemPaddingStart" + android:paddingEnd="?android:attr/listPreferredItemPaddingEnd" + android:minHeight="?android:attr/listPreferredItemHeight" + android:orientation="horizontal" + android:gravity="start|center_vertical"> <ImageView android:id="@+id/icon" @@ -31,7 +31,7 @@ android:layout_marginEnd="8dip" android:duplicateParentState="true" android:contentDescription="@null" - android:visibility="gone"> + android:visibility="invisible"> </ImageView> <LinearLayout @@ -49,7 +49,7 @@ android:ellipsize="end" android:textIsSelectable="false" android:gravity="top|start" - android:textColor="@color/item_text_color" + android:textColor="?android:attr/textColorSecondary" android:duplicateParentState="true"> </TextView> @@ -62,7 +62,7 @@ android:ellipsize="end" android:textIsSelectable="false" android:visibility="gone" - android:textColor="@color/print_option_title" + android:textColor="?android:attr/textColorSecondary" android:duplicateParentState="true"> </TextView> diff --git a/packages/PrintSpooler/res/layout/select_printer_activity.xml b/packages/PrintSpooler/res/layout/select_printer_activity.xml index 4488b6a..173057b 100644 --- a/packages/PrintSpooler/res/layout/select_printer_activity.xml +++ b/packages/PrintSpooler/res/layout/select_printer_activity.xml @@ -19,12 +19,16 @@ android:layout_width="fill_parent" android:layout_height="fill_parent"> - <fragment - android:name="com.android.printspooler.SelectPrinterFragment" - android:id="@+id/select_printer_fragment" + <ListView + android:id="@android:id/list" android:layout_width="fill_parent" - android:layout_height="wrap_content"> - </fragment> + android:layout_height="fill_parent" + android:paddingStart="@dimen/printer_list_view_padding_start" + android:paddingEnd="@dimen/printer_list_view_padding_end" + android:scrollbarStyle="outsideOverlay" + android:cacheColorHint="@android:color/transparent" + android:scrollbarAlwaysDrawVerticalTrack="true" > + </ListView> <FrameLayout android:id="@+id/empty_print_state" diff --git a/packages/PrintSpooler/res/layout/spinner_dropdown_item.xml b/packages/PrintSpooler/res/layout/spinner_dropdown_item.xml index c3c5021..1fb221a 100644 --- a/packages/PrintSpooler/res/layout/spinner_dropdown_item.xml +++ b/packages/PrintSpooler/res/layout/spinner_dropdown_item.xml @@ -32,7 +32,7 @@ android:ellipsize="end" android:textIsSelectable="false" android:gravity="top|left" - android:textColor="@color/item_text_color" + android:textColor="?android:attr/textColorPrimary" android:duplicateParentState="true"> </TextView> @@ -45,7 +45,7 @@ android:ellipsize="end" android:textIsSelectable="false" android:visibility="gone" - android:textColor="@color/print_option_title" + android:textColor="?android:attr/textColorPrimary" android:duplicateParentState="true"> </TextView> diff --git a/packages/PrintSpooler/res/values-land/constants.xml b/packages/PrintSpooler/res/values-land/constants.xml index d68b77e..0db7513 100644 --- a/packages/PrintSpooler/res/values-land/constants.xml +++ b/packages/PrintSpooler/res/values-land/constants.xml @@ -18,5 +18,6 @@ <dimen name="printer_list_view_padding_start">48dip</dimen> <dimen name="printer_list_view_padding_end">48dip</dimen> + <integer name="print_option_column_count">3</integer> </resources> diff --git a/packages/PrintSpooler/res/values-sw600dp-land/constants.xml b/packages/PrintSpooler/res/values-sw600dp-land/constants.xml new file mode 100644 index 0000000..cacdf98 --- /dev/null +++ b/packages/PrintSpooler/res/values-sw600dp-land/constants.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2014 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> + + <integer name="print_option_column_count">6</integer> + +</resources> diff --git a/packages/PrintSpooler/res/layout/select_printer_fragment.xml b/packages/PrintSpooler/res/values-sw600dp/constants.xml index bbd012e..14c099c 100644 --- a/packages/PrintSpooler/res/layout/select_printer_fragment.xml +++ b/packages/PrintSpooler/res/values-sw600dp/constants.xml @@ -14,13 +14,8 @@ limitations under the License. --> -<ListView xmlns:android="http://schemas.android.com/apk/res/android" - android:id="@android:id/list" - android:layout_width="fill_parent" - android:layout_height="fill_parent" - android:paddingStart="@dimen/printer_list_view_padding_start" - android:paddingEnd="@dimen/printer_list_view_padding_end" - android:scrollbarStyle="outsideOverlay" - android:cacheColorHint="@android:color/transparent" - android:scrollbarAlwaysDrawVerticalTrack="true" > -</ListView> +<resources> + + <integer name="print_option_column_count">3</integer> + +</resources> diff --git a/packages/PrintSpooler/res/values/attrs.xml b/packages/PrintSpooler/res/values/attrs.xml new file mode 100644 index 0000000..feb8be1 --- /dev/null +++ b/packages/PrintSpooler/res/values/attrs.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2014 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> + + <declare-styleable name="PrintOptionsLayout"> + + <attr name="columnCount" format="integer" /> + + </declare-styleable> + +</resources> diff --git a/packages/PrintSpooler/res/values/colors.xml b/packages/PrintSpooler/res/values/colors.xml index 4fc25b3..fd6ae19 100644 --- a/packages/PrintSpooler/res/values/colors.xml +++ b/packages/PrintSpooler/res/values/colors.xml @@ -16,10 +16,6 @@ <resources> - <color name="container_background">#F2F2F2</color> - <color name="important_text">#333333</color> - <color name="print_option_title">#888888</color> - <color name="separator">#CCCCCC</color> - <color name="action_button_background">#FFFFFF</color> + <color name="print_button_tint_color">#EEFF41</color> </resources> diff --git a/packages/PrintSpooler/res/values/constants.xml b/packages/PrintSpooler/res/values/constants.xml index e9c925c..9a2c14e 100644 --- a/packages/PrintSpooler/res/values/constants.xml +++ b/packages/PrintSpooler/res/values/constants.xml @@ -19,6 +19,8 @@ <integer name="page_option_value_all">0</integer> <integer name="page_option_value_page_range">1</integer> + <integer name="print_option_column_count">2</integer> + <integer-array name="page_options_values" translatable="false"> <item>@integer/page_option_value_all</item> <item>@integer/page_option_value_page_range</item> diff --git a/packages/PrintSpooler/res/values/strings.xml b/packages/PrintSpooler/res/values/strings.xml index d2613d0..d85529c 100644 --- a/packages/PrintSpooler/res/values/strings.xml +++ b/packages/PrintSpooler/res/values/strings.xml @@ -19,14 +19,8 @@ <!-- Title of the PrintSpooler application. [CHAR LIMIT=50] --> <string name="app_label">Print Spooler</string> - <!-- Label of the print dialog's button for advanced printer settings. [CHAR LIMIT=25] --> - <string name="advanced_settings_button">Printer settings</string> - - <!-- Label of the print dialog's print button. [CHAR LIMIT=16] --> - <string name="print_button">Print</string> - - <!-- Label of the print dialog's save button. [CHAR LIMIT=16] --> - <string name="save_button">Save</string> + <!-- Label of the print dialog's button for more print options. [CHAR LIMIT=25] --> + <string name="more_options_button">More options</string> <!-- Label of the destination widget. [CHAR LIMIT=20] --> <string name="label_destination">Destination</string> @@ -34,8 +28,14 @@ <!-- Label of the copies count widget. [CHAR LIMIT=20] --> <string name="label_copies">Copies</string> + <!-- Label of the copies count for the print options summary. [CHAR LIMIT=20] --> + <string name="label_copies_summary">Copies:</string> + <!-- Label of the paper size widget. [CHAR LIMIT=20] --> - <string name="label_paper_size">Paper Size</string> + <string name="label_paper_size">Paper size</string> + + <!-- Label of the paper size for the print options summary. [CHAR LIMIT=20] --> + <string name="label_paper_size_summary">Paper size:</string> <!-- Label of the color mode widget. [CHAR LIMIT=20] --> <string name="label_color">Color</string> @@ -118,19 +118,19 @@ <!-- Notifications --> - <!-- Template for the notificaiton label for a printing print job. [CHAR LIMIT=25] --> + <!-- Template for the notification label for a printing print job. [CHAR LIMIT=25] --> <string name="printing_notification_title_template">Printing <xliff:g id="print_job_name" example="foo.jpg">%1$s</xliff:g></string> - <!-- Template for the notificaiton label for a cancelling print job. [CHAR LIMIT=25] --> + <!-- Template for the notification label for a cancelling print job. [CHAR LIMIT=25] --> <string name="cancelling_notification_title_template">Cancelling <xliff:g id="print_job_name" example="foo.jpg">%1$s</xliff:g></string> - <!-- Template for the notificaiton label for a failed print job. [CHAR LIMIT=25] --> + <!-- Template for the notification label for a failed print job. [CHAR LIMIT=25] --> <string name="failed_notification_title_template">Printer error <xliff:g id="print_job_name" example="foo.jpg">%1$s</xliff:g></string> - <!-- Template for the notificaiton label for a blocked print job. [CHAR LIMIT=25] --> + <!-- Template for the notification label for a blocked print job. [CHAR LIMIT=25] --> <string name="blocked_notification_title_template">Printer blocked <xliff:g id="print_job_name" example="foo.jpg">%1$s</xliff:g></string> - <!-- Template for the notificaiton label for a composite (multiple items) print jobs notification. [CHAR LIMIT=25] --> + <!-- Template for the notification label for a composite (multiple items) print jobs notification. [CHAR LIMIT=25] --> <plurals name="composite_notification_title_template"> <item quantity="one"><xliff:g id="print_job_name" example="foo.jpg">%1$d</xliff:g> print job</item> <item quantity="other"><xliff:g id="print_job_name" example="foo.jpg">%1$d</xliff:g> print jobs</item> @@ -139,7 +139,7 @@ <!-- Label for the notification button for cancelling a print job. [CHAR LIMIT=25] --> <string name="cancel">Cancel</string> - <!-- Label for the notification button for restrating a filed print job. [CHAR LIMIT=25] --> + <!-- Label for the notification button for restarting a filed print job. [CHAR LIMIT=25] --> <string name="restart">Restart</string> <!-- Message that there is no connection to a printer. [CHAR LIMIT=40] --> @@ -151,9 +151,6 @@ <!-- Label for a printer that is not available. [CHAR LIMIT=25] --> <string name="printer_unavailable"><xliff:g id="print_job_name" example="Canon-123GHT">%1$s</xliff:g> – unavailable</string> - <!-- Default message of an alert dialog for app error while generating a print job. [CHAR LIMIT=50] --> - <string name="print_error_default_message">Couldn\'t generate print job</string> - <!-- Arrays --> <!-- Color mode labels. --> @@ -200,4 +197,23 @@ holder to start the configuration activities of a print service. Should never be needed for normal apps.</string> + <!-- Error messages --> + + <!-- Message for an error when trying to print to a PDF file. [CHAR LIMIT=50] --> + <string name="print_write_error_message">Couldn\'t write to file</string> + + <!-- Default message for an error while generating a print job. [CHAR LIMIT=50] --> + <string name="print_error_default_message">Couldn\'t generate print job</string> + + <!-- Label for the retry button in the error message. [CHAR LIMIT=50] --> + <string name="print_error_retry">Retry</string> + + <!-- Message for the currently selected printer becoming unavailable. [CHAR LIMIT=50] --> + <string name="print_error_printer_unavailable">Printer unavailable</string> + + <!-- Long running operations --> + + <!-- Message for the cancel print operation UI while waiting for cancel to be performed. [CHAR LIMIT=50] --> + <string name="print_operation_canceling">Cancelling\u2026</string> + </resources> diff --git a/packages/PrintSpooler/res/values/styles.xml b/packages/PrintSpooler/res/values/styles.xml index d64380a..9637847 100644 --- a/packages/PrintSpooler/res/values/styles.xml +++ b/packages/PrintSpooler/res/values/styles.xml @@ -16,13 +16,6 @@ <resources> - <style name="PrintOptionTitleTextAppearance"> - <item name="android:textStyle">normal</item> - <item name="android:textSize">14sp</item> - <item name="android:textAllCaps">true</item> - <item name="android:textColor">@color/print_option_title</item> - </style> - <style name="PrintOptionSpinnerStyle"> <item name="android:paddingTop">0dip</item> <item name="android:paddingBottom">0dip</item> @@ -30,7 +23,7 @@ </style> <style name="PrintOptionEditTextStyle"> - <item name="android:minHeight">?android:attr/listPreferredItemHeightSmall</item> + <item name="android:singleLine">true</item> <item name="android:ellipsize">end</item> </style> diff --git a/packages/PrintSpooler/res/values/themes.xml b/packages/PrintSpooler/res/values/themes.xml index 94ab895..40bf725 100644 --- a/packages/PrintSpooler/res/values/themes.xml +++ b/packages/PrintSpooler/res/values/themes.xml @@ -16,15 +16,6 @@ <resources> - <style name="PrintJobConfigActivityTheme" parent="@android:style/Theme.DeviceDefault.Light.NoActionBar"> - <item name="android:windowBackground">@android:color/transparent</item> - <item name="android:windowSoftInputMode">stateAlwaysHidden|adjustResize</item> - <item name="android:windowIsTranslucent">true</item> - <item name="android:backgroundDimEnabled">true</item> - <item name="android:colorBackgroundCacheHint">@android:color/transparent</item> - <item name="android:windowIsFloating">true</item> - </style> - <style name="SelectPrinterActivityTheme" parent="@android:style/Theme.DeviceDefault.Light"> <item name="android:actionBarStyle">@style/SelectPrinterActivityActionBarStyle</item> </style> diff --git a/packages/PrintSpooler/src/com/android/printspooler/PrintDialogFrame.java b/packages/PrintSpooler/src/com/android/printspooler/PrintDialogFrame.java deleted file mode 100644 index c1c4d21..0000000 --- a/packages/PrintSpooler/src/com/android/printspooler/PrintDialogFrame.java +++ /dev/null @@ -1,69 +0,0 @@ -/* - * 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.content.Context; -import android.util.AttributeSet; -import android.widget.FrameLayout; - -public class PrintDialogFrame extends FrameLayout { - - public final int mMaxWidth; - - public int mHeight; - - public PrintDialogFrame(Context context, AttributeSet attrs) { - super(context, attrs); - mMaxWidth = context.getResources().getDimensionPixelSize( - R.dimen.print_dialog_frame_max_width_dip); - } - - @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - super.onMeasure(widthMeasureSpec, heightMeasureSpec); - - int measuredWidth = getMeasuredWidth(); - final int widthMode = MeasureSpec.getMode(widthMeasureSpec); - switch (widthMode) { - case MeasureSpec.UNSPECIFIED: { - measuredWidth = mMaxWidth; - } break; - - case MeasureSpec.AT_MOST: { - final int receivedWidth = MeasureSpec.getSize(widthMeasureSpec); - measuredWidth = Math.min(mMaxWidth, receivedWidth); - } break; - } - - mHeight = Math.max(mHeight, getMeasuredHeight()); - - int measuredHeight = getMeasuredHeight(); - final int heightMode = MeasureSpec.getMode(heightMeasureSpec); - switch (heightMode) { - case MeasureSpec.UNSPECIFIED: { - measuredHeight = mHeight; - } break; - - case MeasureSpec.AT_MOST: { - final int receivedHeight = MeasureSpec.getSize(heightMeasureSpec); - measuredHeight = Math.min(mHeight, receivedHeight); - } break; - } - - setMeasuredDimension(measuredWidth, measuredHeight); - } -} diff --git a/packages/PrintSpooler/src/com/android/printspooler/PrintJobConfigActivity.java b/packages/PrintSpooler/src/com/android/printspooler/PrintJobConfigActivity.java deleted file mode 100644 index e3d8d05..0000000 --- a/packages/PrintSpooler/src/com/android/printspooler/PrintJobConfigActivity.java +++ /dev/null @@ -1,2966 +0,0 @@ -/* - * 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.app.Dialog; -import android.app.LoaderManager; -import android.content.ActivityNotFoundException; -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.content.Loader; -import android.content.ServiceConnection; -import android.content.pm.PackageInfo; -import android.content.pm.PackageManager.NameNotFoundException; -import android.content.pm.ResolveInfo; -import android.content.pm.ServiceInfo; -import android.database.DataSetObserver; -import android.graphics.Rect; -import android.graphics.drawable.Drawable; -import android.net.Uri; -import android.os.AsyncTask; -import android.os.Bundle; -import android.os.Handler; -import android.os.IBinder; -import android.os.IBinder.DeathRecipient; -import android.os.Looper; -import android.os.Message; -import android.os.RemoteException; -import android.print.ILayoutResultCallback; -import android.print.IPrintDocumentAdapter; -import android.print.IPrintDocumentAdapterObserver; -import android.print.IWriteResultCallback; -import android.print.PageRange; -import android.print.PrintAttributes; -import android.print.PrintAttributes.Margins; -import android.print.PrintAttributes.MediaSize; -import android.print.PrintAttributes.Resolution; -import android.print.PrintDocumentAdapter; -import android.print.PrintDocumentInfo; -import android.print.PrintJobId; -import android.print.PrintJobInfo; -import android.print.PrintManager; -import android.print.PrinterCapabilitiesInfo; -import android.print.PrinterId; -import android.print.PrinterInfo; -import android.printservice.PrintService; -import android.printservice.PrintServiceInfo; -import android.provider.DocumentsContract; -import android.text.Editable; -import android.text.TextUtils; -import android.text.TextUtils.SimpleStringSplitter; -import android.text.TextWatcher; -import android.util.ArrayMap; -import android.util.AttributeSet; -import android.util.Log; -import android.view.Gravity; -import android.view.KeyEvent; -import android.view.MotionEvent; -import android.view.View; -import android.view.View.MeasureSpec; -import android.view.View.OnAttachStateChangeListener; -import android.view.View.OnClickListener; -import android.view.View.OnFocusChangeListener; -import android.view.ViewConfiguration; -import android.view.ViewGroup; -import android.view.ViewGroup.LayoutParams; -import android.view.ViewPropertyAnimator; -import android.view.inputmethod.InputMethodManager; -import android.widget.AdapterView; -import android.widget.AdapterView.OnItemSelectedListener; -import android.widget.ArrayAdapter; -import android.widget.BaseAdapter; -import android.widget.Button; -import android.widget.EditText; -import android.widget.FrameLayout; -import android.widget.ImageView; -import android.widget.Spinner; -import android.widget.TextView; - -import com.android.printspooler.MediaSizeUtils.MediaSizeComparator; - -import libcore.io.IoUtils; - -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.lang.ref.WeakReference; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.Comparator; -import java.util.List; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -/** - * Activity for configuring a print job. - */ -public class PrintJobConfigActivity extends Activity { - - private static final String LOG_TAG = "PrintJobConfigActivity"; - - private static final boolean DEBUG = false; - - public static final String INTENT_EXTRA_PRINTER_ID = "INTENT_EXTRA_PRINTER_ID"; - - private static final int LOADER_ID_PRINTERS_LOADER = 1; - - private static final int ORIENTATION_PORTRAIT = 0; - private static final int ORIENTATION_LANDSCAPE = 1; - - private static final int DEST_ADAPTER_MAX_ITEM_COUNT = 9; - - private static final int DEST_ADAPTER_ITEM_ID_SAVE_AS_PDF = Integer.MAX_VALUE; - private static final int DEST_ADAPTER_ITEM_ID_ALL_PRINTERS = Integer.MAX_VALUE - 1; - - private static final int ACTIVITY_REQUEST_CREATE_FILE = 1; - private static final int ACTIVITY_REQUEST_SELECT_PRINTER = 2; - private static final int ACTIVITY_POPULATE_ADVANCED_PRINT_OPTIONS = 3; - - private static final int CONTROLLER_STATE_FINISHED = 1; - private static final int CONTROLLER_STATE_FAILED = 2; - private static final int CONTROLLER_STATE_CANCELLED = 3; - private static final int CONTROLLER_STATE_INITIALIZED = 4; - private static final int CONTROLLER_STATE_STARTED = 5; - private static final int CONTROLLER_STATE_LAYOUT_STARTED = 6; - private static final int CONTROLLER_STATE_LAYOUT_COMPLETED = 7; - private static final int CONTROLLER_STATE_WRITE_STARTED = 8; - private static final int CONTROLLER_STATE_WRITE_COMPLETED = 9; - - private static final int EDITOR_STATE_INITIALIZED = 1; - private static final int EDITOR_STATE_CONFIRMED_PRINT = 2; - private static final int EDITOR_STATE_CANCELLED = 3; - - private static final int MIN_COPIES = 1; - private static final String MIN_COPIES_STRING = String.valueOf(MIN_COPIES); - - private static final Pattern PATTERN_DIGITS = Pattern.compile("[\\d]+"); - - private static final Pattern PATTERN_ESCAPE_SPECIAL_CHARS = Pattern.compile( - "(?=[]\\[+&|!(){}^\"~*?:\\\\])"); - - private static final Pattern PATTERN_PAGE_RANGE = Pattern.compile( - "[\\s]*[0-9]*[\\s]*[\\-]?[\\s]*[0-9]*[\\s]*?(([,])" - + "[\\s]*[0-9]*[\\s]*[\\-]?[\\s]*[0-9]*[\\s]*|[\\s]*)+"); - - public static final PageRange[] ALL_PAGES_ARRAY = new PageRange[] {PageRange.ALL_PAGES}; - - private final PrintAttributes mOldPrintAttributes = new PrintAttributes.Builder().build(); - private final PrintAttributes mCurrPrintAttributes = new PrintAttributes.Builder().build(); - - private final DeathRecipient mDeathRecipient = new DeathRecipient() { - @Override - public void binderDied() { - finish(); - } - }; - - private Editor mEditor; - private Document mDocument; - private PrintController mController; - - private PrintJobId mPrintJobId; - - private IBinder mIPrintDocumentAdapter; - - private Dialog mGeneratingPrintJobDialog; - - private PrintSpoolerProvider mSpoolerProvider; - - private String mCallingPackageName; - - @Override - protected void onCreate(Bundle bundle) { - super.onCreate(bundle); - - setTitle(R.string.print_dialog); - - Bundle extras = getIntent().getExtras(); - - PrintJobInfo printJob = extras.getParcelable(PrintManager.EXTRA_PRINT_JOB); - if (printJob == null) { - throw new IllegalArgumentException("printJob cannot be null"); - } - - mPrintJobId = printJob.getId(); - mIPrintDocumentAdapter = extras.getBinder(PrintManager.EXTRA_PRINT_DOCUMENT_ADAPTER); - if (mIPrintDocumentAdapter == null) { - throw new IllegalArgumentException("PrintDocumentAdapter cannot be null"); - } - - try { - IPrintDocumentAdapter.Stub.asInterface(mIPrintDocumentAdapter) - .setObserver(new PrintDocumentAdapterObserver(this)); - } catch (RemoteException re) { - finish(); - return; - } - - PrintAttributes attributes = printJob.getAttributes(); - if (attributes != null) { - mCurrPrintAttributes.copyFrom(attributes); - } - - mCallingPackageName = extras.getString(DocumentsContract.EXTRA_PACKAGE_NAME); - - setContentView(R.layout.print_job_config_activity_container); - - try { - mIPrintDocumentAdapter.linkToDeath(mDeathRecipient, 0); - } catch (RemoteException re) { - finish(); - return; - } - - mDocument = new Document(); - mEditor = new Editor(); - - mSpoolerProvider = new PrintSpoolerProvider(this, - new Runnable() { - @Override - public void run() { - // We got the spooler so unleash the UI. - mController = new PrintController(new RemotePrintDocumentAdapter( - IPrintDocumentAdapter.Stub.asInterface(mIPrintDocumentAdapter), - mSpoolerProvider.getSpooler().generateFileForPrintJob(mPrintJobId))); - mController.initialize(); - - mEditor.initialize(); - mEditor.postCreate(); - } - }); - } - - @Override - public void onResume() { - super.onResume(); - if (mSpoolerProvider.getSpooler() != null) { - mEditor.refreshCurrentPrinter(); - } - } - - @Override - public void onPause() { - if (isFinishing()) { - if (mController != null && mController.hasStarted()) { - mController.finish(); - } - if (mEditor != null && mEditor.isPrintConfirmed() - && mController != null && mController.isFinished()) { - mSpoolerProvider.getSpooler().setPrintJobState(mPrintJobId, - PrintJobInfo.STATE_QUEUED, null); - } else { - mSpoolerProvider.getSpooler().setPrintJobState(mPrintJobId, - PrintJobInfo.STATE_CANCELED, null); - } - if (mGeneratingPrintJobDialog != null) { - mGeneratingPrintJobDialog.dismiss(); - mGeneratingPrintJobDialog = null; - } - mIPrintDocumentAdapter.unlinkToDeath(mDeathRecipient, 0); - mSpoolerProvider.destroy(); - } - super.onPause(); - } - - public boolean onTouchEvent(MotionEvent event) { - if (mController != null && mEditor != null && - !mEditor.isPrintConfirmed() && mEditor.shouldCloseOnTouch(event)) { - if (!mController.isWorking()) { - PrintJobConfigActivity.this.finish(); - } - mEditor.cancel(); - return true; - } - return super.onTouchEvent(event); - } - - public boolean onKeyDown(int keyCode, KeyEvent event) { - if (keyCode == KeyEvent.KEYCODE_BACK) { - event.startTracking(); - } - return super.onKeyDown(keyCode, event); - } - - public boolean onKeyUp(int keyCode, KeyEvent event) { - if (mController != null && mEditor != null) { - if (keyCode == KeyEvent.KEYCODE_BACK) { - if (mEditor.isShwoingGeneratingPrintJobUi()) { - return true; - } - if (event.isTracking() && !event.isCanceled()) { - if (!mController.isWorking()) { - PrintJobConfigActivity.this.finish(); - } - } - mEditor.cancel(); - return true; - } - } - return super.onKeyUp(keyCode, event); - } - - private boolean printAttributesChanged() { - return !mOldPrintAttributes.equals(mCurrPrintAttributes); - } - - private class PrintController { - private final AtomicInteger mRequestCounter = new AtomicInteger(); - - private final RemotePrintDocumentAdapter mRemotePrintAdapter; - - private final Bundle mMetadata; - - private final ControllerHandler mHandler; - - private final LayoutResultCallback mLayoutResultCallback; - - private final WriteResultCallback mWriteResultCallback; - - private int mControllerState = CONTROLLER_STATE_INITIALIZED; - - private boolean mHasStarted; - - private PageRange[] mRequestedPages; - - public PrintController(RemotePrintDocumentAdapter adapter) { - mRemotePrintAdapter = adapter; - mMetadata = new Bundle(); - mHandler = new ControllerHandler(getMainLooper()); - mLayoutResultCallback = new LayoutResultCallback(mHandler); - mWriteResultCallback = new WriteResultCallback(mHandler); - } - - public void initialize() { - mHasStarted = false; - mControllerState = CONTROLLER_STATE_INITIALIZED; - } - - public void cancel() { - if (isWorking()) { - mRemotePrintAdapter.cancel(); - } - mControllerState = CONTROLLER_STATE_CANCELLED; - } - - public boolean isCancelled() { - return (mControllerState == CONTROLLER_STATE_CANCELLED); - } - - public boolean isFinished() { - return (mControllerState == CONTROLLER_STATE_FINISHED); - } - - public boolean hasStarted() { - return mHasStarted; - } - - public boolean hasPerformedLayout() { - return mControllerState >= CONTROLLER_STATE_LAYOUT_COMPLETED; - } - - public boolean isPerformingLayout() { - return mControllerState == CONTROLLER_STATE_LAYOUT_STARTED; - } - - public boolean isWorking() { - return mControllerState == CONTROLLER_STATE_LAYOUT_STARTED - || mControllerState == CONTROLLER_STATE_WRITE_STARTED; - } - - public void start() { - mControllerState = CONTROLLER_STATE_STARTED; - mHasStarted = true; - mRemotePrintAdapter.start(); - } - - public void update() { - if (!mController.hasStarted()) { - mController.start(); - } - - // If the print attributes are the same and we are performing - // a layout, then we have to wait for it to completed which will - // trigger writing of the necessary pages. - final boolean printAttributesChanged = printAttributesChanged(); - if (!printAttributesChanged && isPerformingLayout()) { - return; - } - - // If print is confirmed we always do a layout since the previous - // ones were for preview and this one is for printing. - if (!printAttributesChanged && !mEditor.isPrintConfirmed()) { - if (mDocument.info == null) { - // We are waiting for the result of a layout, so do nothing. - return; - } - // If the attributes didn't change and we have done a layout, then - // we do not do a layout but may have to ask the app to write some - // pages. Hence, pretend layout completed and nothing changed, so - // we handle writing as usual. - handleOnLayoutFinished(mDocument.info, false, mRequestCounter.get()); - } else { - mSpoolerProvider.getSpooler().setPrintJobAttributesNoPersistence( - mPrintJobId, mCurrPrintAttributes); - - mMetadata.putBoolean(PrintDocumentAdapter.EXTRA_PRINT_PREVIEW, - !mEditor.isPrintConfirmed()); - - mControllerState = CONTROLLER_STATE_LAYOUT_STARTED; - - mRemotePrintAdapter.layout(mOldPrintAttributes, mCurrPrintAttributes, - mLayoutResultCallback, mMetadata, mRequestCounter.incrementAndGet()); - - mOldPrintAttributes.copyFrom(mCurrPrintAttributes); - } - } - - public void finish() { - mControllerState = CONTROLLER_STATE_FINISHED; - mRemotePrintAdapter.finish(); - } - - private void handleOnLayoutFinished(PrintDocumentInfo info, - boolean layoutChanged, int sequence) { - if (mRequestCounter.get() != sequence) { - return; - } - - if (isCancelled()) { - mEditor.updateUi(); - if (mEditor.isDone()) { - PrintJobConfigActivity.this.finish(); - } - return; - } - - final int oldControllerState = mControllerState; - - if (mControllerState == CONTROLLER_STATE_LAYOUT_STARTED) { - mControllerState = CONTROLLER_STATE_LAYOUT_COMPLETED; - } - - // For layout purposes we care only whether the type or the page - // count changed. We still do not have the size since we did not - // call write. We use "layoutChanged" set by the application to - // know whether something else changed about the document. - final boolean infoChanged = !equalsIgnoreSize(info, mDocument.info); - // If the info changed, we update the document and the print job. - if (infoChanged) { - mDocument.info = info; - // Set the info. - mSpoolerProvider.getSpooler().setPrintJobPrintDocumentInfoNoPersistence( - mPrintJobId, info); - } - - // If the document info or the layout changed, then - // drop the pages since we have to fetch them again. - if (infoChanged || layoutChanged) { - mDocument.pages = null; - mSpoolerProvider.getSpooler().setPrintJobPagesNoPersistence( - mPrintJobId, null); - } - - PageRange[] oldRequestedPages = mRequestedPages; - - // No pages means that the user selected an invalid range while we - // were doing a layout or the layout returned a document info for - // which the selected range is invalid. In such a case we do not - // write anything and wait for the user to fix the range which will - // trigger an update. - mRequestedPages = mEditor.getRequestedPages(); - if (mRequestedPages == null || mRequestedPages.length == 0) { - mEditor.updateUi(); - if (mEditor.isDone()) { - PrintJobConfigActivity.this.finish(); - } - return; - } else { - // If print is not confirmed we just ask for the first of the - // selected pages to emulate a behavior that shows preview - // increasing the chances that apps will implement the APIs - // correctly. - if (!mEditor.isPrintConfirmed()) { - if (ALL_PAGES_ARRAY.equals(mRequestedPages)) { - mRequestedPages = new PageRange[] {new PageRange(0, 0)}; - } else { - final int firstPage = mRequestedPages[0].getStart(); - mRequestedPages = new PageRange[] {new PageRange(firstPage, firstPage)}; - } - } - } - - // If the info and the layout did not change... - if (!infoChanged && !layoutChanged - // and we have the requested pages ... - && (PageRangeUtils.contains(mDocument.pages, mRequestedPages)) - // ...or the requested pages are being written... - || (oldControllerState == CONTROLLER_STATE_WRITE_STARTED - && oldRequestedPages != null - && PageRangeUtils.contains(oldRequestedPages, mRequestedPages))) { - // Nothing interesting changed and we have all requested pages. - // Then update the print jobs's pages as we will not do a write - // and we usually update the pages in the write complete callback. - if (mDocument.pages != null) { - // Update the print job's pages given we have them. - updatePrintJobPages(mDocument.pages, mRequestedPages); - } - mEditor.updateUi(); - if (mEditor.isDone()) { - requestCreatePdfFileOrFinish(); - } - return; - } - - mEditor.updateUi(); - - // Request a write of the pages of interest. - mControllerState = CONTROLLER_STATE_WRITE_STARTED; - mRemotePrintAdapter.write(mRequestedPages, mWriteResultCallback, - mRequestCounter.incrementAndGet()); - } - - private void handleOnLayoutFailed(final CharSequence error, int sequence) { - if (mRequestCounter.get() != sequence) { - return; - } - mControllerState = CONTROLLER_STATE_FAILED; - mEditor.showUi(Editor.UI_ERROR, new Runnable() { - @Override - public void run() { - if (!TextUtils.isEmpty(error)) { - TextView messageView = (TextView) findViewById(R.id.message); - messageView.setText(error); - } - } - }); - } - - private void handleOnWriteFinished(PageRange[] pages, int sequence) { - if (mRequestCounter.get() != sequence) { - return; - } - - if (isCancelled()) { - if (mEditor.isDone()) { - PrintJobConfigActivity.this.finish(); - } - return; - } - - mControllerState = CONTROLLER_STATE_WRITE_COMPLETED; - - // Update the document size. - File file = mSpoolerProvider.getSpooler() - .generateFileForPrintJob(mPrintJobId); - mDocument.info.setDataSize(file.length()); - - // Update the print job with the updated info. - mSpoolerProvider.getSpooler().setPrintJobPrintDocumentInfoNoPersistence( - mPrintJobId, mDocument.info); - - // Update which pages we have fetched. - mDocument.pages = PageRangeUtils.normalize(pages); - - if (DEBUG) { - Log.i(LOG_TAG, "Requested: " + Arrays.toString(mRequestedPages) - + " and got: " + Arrays.toString(mDocument.pages)); - } - - updatePrintJobPages(mDocument.pages, mRequestedPages); - - if (mEditor.isDone()) { - requestCreatePdfFileOrFinish(); - } - } - - private void updatePrintJobPages(PageRange[] writtenPages, PageRange[] requestedPages) { - // Adjust the print job pages based on what was requested and written. - // The cases are ordered in the most expected to the least expected. - if (Arrays.equals(writtenPages, requestedPages)) { - // We got a document with exactly the pages we wanted. Hence, - // the printer has to print all pages in the data. - mSpoolerProvider.getSpooler().setPrintJobPagesNoPersistence(mPrintJobId, - ALL_PAGES_ARRAY); - } else if (Arrays.equals(writtenPages, ALL_PAGES_ARRAY)) { - // We requested specific pages but got all of them. Hence, - // the printer has to print only the requested pages. - mSpoolerProvider.getSpooler().setPrintJobPagesNoPersistence(mPrintJobId, - requestedPages); - } else if (PageRangeUtils.contains(writtenPages, requestedPages)) { - // We requested specific pages and got more but not all pages. - // Hence, we have to offset appropriately the printed pages to - // be based off the start of the written ones instead of zero. - // The written pages are always non-null and not empty. - final int offset = -writtenPages[0].getStart(); - PageRange[] offsetPages = Arrays.copyOf(requestedPages, requestedPages.length); - PageRangeUtils.offset(offsetPages, offset); - mSpoolerProvider.getSpooler().setPrintJobPagesNoPersistence(mPrintJobId, - offsetPages); - } else if (Arrays.equals(requestedPages, ALL_PAGES_ARRAY) - && writtenPages.length == 1 && writtenPages[0].getStart() == 0 - && writtenPages[0].getEnd() == mDocument.info.getPageCount() - 1) { - // We requested all pages via the special constant and got all - // of them as an explicit enumeration. Hence, the printer has - // to print only the requested pages. - mSpoolerProvider.getSpooler().setPrintJobPagesNoPersistence(mPrintJobId, - writtenPages); - } else { - // We did not get the pages we requested, then the application - // misbehaves, so we fail quickly. - mControllerState = CONTROLLER_STATE_FAILED; - Log.e(LOG_TAG, "Received invalid pages from the app: requested=" - + Arrays.toString(requestedPages) + " written=" - + Arrays.toString(writtenPages)); - mEditor.showUi(Editor.UI_ERROR, null); - } - } - - private void requestCreatePdfFileOrFinish() { - if (mEditor.isPrintingToPdf()) { - Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT); - intent.setType("application/pdf"); - intent.putExtra(Intent.EXTRA_TITLE, mDocument.info.getName()); - intent.putExtra(DocumentsContract.EXTRA_PACKAGE_NAME, mCallingPackageName); - startActivityForResult(intent, ACTIVITY_REQUEST_CREATE_FILE); - } else { - PrintJobConfigActivity.this.finish(); - } - } - - private void handleOnWriteFailed(final CharSequence error, int sequence) { - if (mRequestCounter.get() != sequence) { - return; - } - mControllerState = CONTROLLER_STATE_FAILED; - mEditor.showUi(Editor.UI_ERROR, new Runnable() { - @Override - public void run() { - if (!TextUtils.isEmpty(error)) { - TextView messageView = (TextView) findViewById(R.id.message); - messageView.setText(error); - } - } - }); - } - - private boolean equalsIgnoreSize(PrintDocumentInfo lhs, PrintDocumentInfo rhs) { - if (lhs == rhs) { - return true; - } - if (lhs == null) { - if (rhs != null) { - return false; - } - } else { - if (rhs == null) { - return false; - } - if (lhs.getContentType() != rhs.getContentType() - || lhs.getPageCount() != rhs.getPageCount()) { - return false; - } - } - return true; - } - - private final class ControllerHandler extends Handler { - public static final int MSG_ON_LAYOUT_FINISHED = 1; - public static final int MSG_ON_LAYOUT_FAILED = 2; - public static final int MSG_ON_WRITE_FINISHED = 3; - public static final int MSG_ON_WRITE_FAILED = 4; - - public ControllerHandler(Looper looper) { - super(looper, null, false); - } - - @Override - public void handleMessage(Message message) { - switch (message.what) { - case MSG_ON_LAYOUT_FINISHED: { - PrintDocumentInfo info = (PrintDocumentInfo) message.obj; - final boolean changed = (message.arg1 == 1); - final int sequence = message.arg2; - handleOnLayoutFinished(info, changed, sequence); - } break; - - case MSG_ON_LAYOUT_FAILED: { - CharSequence error = (CharSequence) message.obj; - final int sequence = message.arg1; - handleOnLayoutFailed(error, sequence); - } break; - - case MSG_ON_WRITE_FINISHED: { - PageRange[] pages = (PageRange[]) message.obj; - final int sequence = message.arg1; - handleOnWriteFinished(pages, sequence); - } break; - - case MSG_ON_WRITE_FAILED: { - CharSequence error = (CharSequence) message.obj; - final int sequence = message.arg1; - handleOnWriteFailed(error, sequence); - } break; - } - } - } - } - - private static final class LayoutResultCallback extends ILayoutResultCallback.Stub { - private final WeakReference<PrintController.ControllerHandler> mWeakHandler; - - public LayoutResultCallback(PrintController.ControllerHandler handler) { - mWeakHandler = new WeakReference<PrintController.ControllerHandler>(handler); - } - - @Override - public void onLayoutFinished(PrintDocumentInfo info, boolean changed, int sequence) { - Handler handler = mWeakHandler.get(); - if (handler != null) { - handler.obtainMessage(PrintController.ControllerHandler.MSG_ON_LAYOUT_FINISHED, - changed ? 1 : 0, sequence, info).sendToTarget(); - } - } - - @Override - public void onLayoutFailed(CharSequence error, int sequence) { - Handler handler = mWeakHandler.get(); - if (handler != null) { - handler.obtainMessage(PrintController.ControllerHandler.MSG_ON_LAYOUT_FAILED, - sequence, 0, error).sendToTarget(); - } - } - } - - private static final class WriteResultCallback extends IWriteResultCallback.Stub { - private final WeakReference<PrintController.ControllerHandler> mWeakHandler; - - public WriteResultCallback(PrintController.ControllerHandler handler) { - mWeakHandler = new WeakReference<PrintController.ControllerHandler>(handler); - } - - @Override - public void onWriteFinished(PageRange[] pages, int sequence) { - Handler handler = mWeakHandler.get(); - if (handler != null) { - handler.obtainMessage(PrintController.ControllerHandler.MSG_ON_WRITE_FINISHED, - sequence, 0, pages).sendToTarget(); - } - } - - @Override - public void onWriteFailed(CharSequence error, int sequence) { - Handler handler = mWeakHandler.get(); - if (handler != null) { - handler.obtainMessage(PrintController.ControllerHandler.MSG_ON_WRITE_FAILED, - sequence, 0, error).sendToTarget(); - } - } - } - - @Override - protected void onActivityResult(int requestCode, int resultCode, Intent data) { - switch (requestCode) { - case ACTIVITY_REQUEST_CREATE_FILE: { - if (data != null) { - Uri uri = data.getData(); - writePrintJobDataAndFinish(uri); - } else { - mEditor.showUi(Editor.UI_EDITING_PRINT_JOB, - new Runnable() { - @Override - public void run() { - mEditor.initialize(); - mEditor.bindUi(); - mEditor.reselectCurrentPrinter(); - mEditor.updateUi(); - } - }); - } - } break; - - case ACTIVITY_REQUEST_SELECT_PRINTER: { - if (resultCode == RESULT_OK) { - PrinterId printerId = (PrinterId) data.getParcelableExtra( - INTENT_EXTRA_PRINTER_ID); - if (printerId != null) { - mEditor.ensurePrinterSelected(printerId); - break; - } - } - mEditor.ensureCurrentPrinterSelected(); - } break; - - case ACTIVITY_POPULATE_ADVANCED_PRINT_OPTIONS: { - if (resultCode == RESULT_OK) { - PrintJobInfo printJobInfo = (PrintJobInfo) data.getParcelableExtra( - PrintService.EXTRA_PRINT_JOB_INFO); - if (printJobInfo != null) { - mEditor.updateFromAdvancedOptions(printJobInfo); - break; - } - } - mEditor.cancel(); - PrintJobConfigActivity.this.finish(); - } break; - } - } - - private void writePrintJobDataAndFinish(final Uri uri) { - new AsyncTask<Void, Void, Void>() { - @Override - protected Void doInBackground(Void... params) { - InputStream in = null; - OutputStream out = null; - try { - PrintJobInfo printJob = mSpoolerProvider.getSpooler() - .getPrintJobInfo(mPrintJobId, PrintManager.APP_ID_ANY); - if (printJob == null) { - return null; - } - File file = mSpoolerProvider.getSpooler() - .generateFileForPrintJob(mPrintJobId); - in = new FileInputStream(file); - out = getContentResolver().openOutputStream(uri); - final byte[] buffer = new byte[8192]; - while (true) { - final int readByteCount = in.read(buffer); - if (readByteCount < 0) { - break; - } - 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 { - IoUtils.closeQuietly(in); - IoUtils.closeQuietly(out); - } - return null; - } - - @Override - public void onPostExecute(Void result) { - mEditor.cancel(); - PrintJobConfigActivity.this.finish(); - } - }.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR, (Void[]) null); - } - - private final class Editor { - private static final int UI_NONE = 0; - private static final int UI_EDITING_PRINT_JOB = 1; - private static final int UI_GENERATING_PRINT_JOB = 2; - private static final int UI_ERROR = 3; - - private EditText mCopiesEditText; - - private TextView mRangeOptionsTitle; - private TextView mPageRangeTitle; - private EditText mPageRangeEditText; - - private Spinner mDestinationSpinner; - private DestinationAdapter mDestinationSpinnerAdapter; - - private Spinner mMediaSizeSpinner; - private ArrayAdapter<SpinnerItem<MediaSize>> mMediaSizeSpinnerAdapter; - - private Spinner mColorModeSpinner; - private ArrayAdapter<SpinnerItem<Integer>> mColorModeSpinnerAdapter; - - private Spinner mOrientationSpinner; - private ArrayAdapter<SpinnerItem<Integer>> mOrientationSpinnerAdapter; - - private Spinner mRangeOptionsSpinner; - private ArrayAdapter<SpinnerItem<Integer>> mRangeOptionsSpinnerAdapter; - - private SimpleStringSplitter mStringCommaSplitter = - new SimpleStringSplitter(','); - - private View mContentContainer; - - private View mAdvancedPrintOptionsContainer; - - private Button mAdvancedOptionsButton; - - private Button mPrintButton; - - private PrinterId mNextPrinterId; - - private PrinterInfo mCurrentPrinter; - - private MediaSizeComparator mMediaSizeComparator; - - private final OnFocusChangeListener mFocusListener = new OnFocusChangeListener() { - @Override - public void onFocusChange(View view, boolean hasFocus) { - EditText editText = (EditText) view; - if (!TextUtils.isEmpty(editText.getText())) { - editText.setSelection(editText.getText().length()); - } - } - }; - - private final OnItemSelectedListener mOnItemSelectedListener = - new AdapterView.OnItemSelectedListener() { - @Override - public void onItemSelected(AdapterView<?> spinner, View view, int position, long id) { - if (spinner == mDestinationSpinner) { - if (mIgnoreNextDestinationChange) { - mIgnoreNextDestinationChange = false; - return; - } - - if (position == AdapterView.INVALID_POSITION) { - updateUi(); - return; - } - - if (id == DEST_ADAPTER_ITEM_ID_ALL_PRINTERS) { - startSelectPrinterActivity(); - return; - } - - mCapabilitiesTimeout.remove(); - - mCurrentPrinter = (PrinterInfo) mDestinationSpinnerAdapter - .getItem(position); - - mSpoolerProvider.getSpooler().setPrintJobPrinterNoPersistence( - mPrintJobId, mCurrentPrinter); - - if (mCurrentPrinter.getStatus() == PrinterInfo.STATUS_UNAVAILABLE) { - mCapabilitiesTimeout.post(); - updateUi(); - return; - } - - PrinterCapabilitiesInfo capabilities = mCurrentPrinter.getCapabilities(); - if (capabilities == null) { - mCapabilitiesTimeout.post(); - updateUi(); - refreshCurrentPrinter(); - } else { - updatePrintAttributes(capabilities); - updateUi(); - mController.update(); - refreshCurrentPrinter(); - } - } else if (spinner == mMediaSizeSpinner) { - if (mIgnoreNextMediaSizeChange) { - mIgnoreNextMediaSizeChange = false; - return; - } - if (mOldMediaSizeSelectionIndex - == mMediaSizeSpinner.getSelectedItemPosition()) { - mOldMediaSizeSelectionIndex = AdapterView.INVALID_POSITION; - return; - } - SpinnerItem<MediaSize> mediaItem = mMediaSizeSpinnerAdapter.getItem(position); - if (mOrientationSpinner.getSelectedItemPosition() == 0) { - mCurrPrintAttributes.setMediaSize(mediaItem.value.asPortrait()); - } else { - mCurrPrintAttributes.setMediaSize(mediaItem.value.asLandscape()); - } - if (!hasErrors()) { - mController.update(); - } - } else if (spinner == mColorModeSpinner) { - if (mIgnoreNextColorChange) { - mIgnoreNextColorChange = false; - return; - } - if (mOldColorModeSelectionIndex - == mColorModeSpinner.getSelectedItemPosition()) { - mOldColorModeSelectionIndex = AdapterView.INVALID_POSITION; - return; - } - SpinnerItem<Integer> colorModeItem = - mColorModeSpinnerAdapter.getItem(position); - mCurrPrintAttributes.setColorMode(colorModeItem.value); - if (!hasErrors()) { - mController.update(); - } - } else if (spinner == mOrientationSpinner) { - if (mIgnoreNextOrientationChange) { - mIgnoreNextOrientationChange = false; - return; - } - SpinnerItem<Integer> orientationItem = - mOrientationSpinnerAdapter.getItem(position); - setCurrentPrintAttributesOrientation(orientationItem.value); - if (!hasErrors()) { - mController.update(); - } - } else if (spinner == mRangeOptionsSpinner) { - if (mIgnoreNextRangeOptionChange) { - mIgnoreNextRangeOptionChange = false; - return; - } - updateUi(); - if (!hasErrors()) { - mController.update(); - } - } - } - - @Override - public void onNothingSelected(AdapterView<?> parent) { - /* do nothing*/ - } - }; - - private void setCurrentPrintAttributesOrientation(int orientation) { - MediaSize mediaSize = mCurrPrintAttributes.getMediaSize(); - if (orientation == ORIENTATION_PORTRAIT) { - if (!mediaSize.isPortrait()) { - // Rotate the media size. - mCurrPrintAttributes.setMediaSize(mediaSize.asPortrait()); - - // Rotate the resolution. - Resolution oldResolution = mCurrPrintAttributes.getResolution(); - Resolution newResolution = new Resolution( - oldResolution.getId(), - oldResolution.getLabel(), - oldResolution.getVerticalDpi(), - oldResolution.getHorizontalDpi()); - mCurrPrintAttributes.setResolution(newResolution); - - // Rotate the physical margins. - Margins oldMinMargins = mCurrPrintAttributes.getMinMargins(); - Margins newMinMargins = new Margins( - oldMinMargins.getBottomMils(), - oldMinMargins.getLeftMils(), - oldMinMargins.getTopMils(), - oldMinMargins.getRightMils()); - mCurrPrintAttributes.setMinMargins(newMinMargins); - } - } else { - if (mediaSize.isPortrait()) { - // Rotate the media size. - mCurrPrintAttributes.setMediaSize(mediaSize.asLandscape()); - - // Rotate the resolution. - Resolution oldResolution = mCurrPrintAttributes.getResolution(); - Resolution newResolution = new Resolution( - oldResolution.getId(), - oldResolution.getLabel(), - oldResolution.getVerticalDpi(), - oldResolution.getHorizontalDpi()); - mCurrPrintAttributes.setResolution(newResolution); - - // Rotate the physical margins. - Margins oldMinMargins = mCurrPrintAttributes.getMinMargins(); - Margins newMargins = new Margins( - oldMinMargins.getTopMils(), - oldMinMargins.getRightMils(), - oldMinMargins.getBottomMils(), - oldMinMargins.getLeftMils()); - mCurrPrintAttributes.setMinMargins(newMargins); - } - } - } - - private void updatePrintAttributes(PrinterCapabilitiesInfo capabilities) { - PrintAttributes defaults = capabilities.getDefaults(); - - // Sort the media sizes based on the current locale. - List<MediaSize> sortedMediaSizes = new ArrayList<MediaSize>( - capabilities.getMediaSizes()); - Collections.sort(sortedMediaSizes, mMediaSizeComparator); - - // Media size. - MediaSize currMediaSize = mCurrPrintAttributes.getMediaSize(); - if (currMediaSize == null) { - mCurrPrintAttributes.setMediaSize(defaults.getMediaSize()); - } else { - MediaSize currMediaSizePortrait = currMediaSize.asPortrait(); - final int mediaSizeCount = sortedMediaSizes.size(); - for (int i = 0; i < mediaSizeCount; i++) { - MediaSize mediaSize = sortedMediaSizes.get(i); - if (currMediaSizePortrait.equals(mediaSize.asPortrait())) { - mCurrPrintAttributes.setMediaSize(currMediaSize); - break; - } - } - } - - // Color mode. - final int colorMode = mCurrPrintAttributes.getColorMode(); - if ((capabilities.getColorModes() & colorMode) == 0) { - mCurrPrintAttributes.setColorMode(colorMode); - } - - // Resolution - Resolution resolution = mCurrPrintAttributes.getResolution(); - if (resolution == null || !capabilities.getResolutions().contains(resolution)) { - mCurrPrintAttributes.setResolution(defaults.getResolution()); - } - - // Margins. - Margins margins = mCurrPrintAttributes.getMinMargins(); - if (margins == null) { - mCurrPrintAttributes.setMinMargins(defaults.getMinMargins()); - } else { - Margins minMargins = capabilities.getMinMargins(); - if (margins.getLeftMils() < minMargins.getLeftMils() - || margins.getTopMils() < minMargins.getTopMils() - || margins.getRightMils() > minMargins.getRightMils() - || margins.getBottomMils() > minMargins.getBottomMils()) { - mCurrPrintAttributes.setMinMargins(defaults.getMinMargins()); - } - } - } - - private final TextWatcher mCopiesTextWatcher = new TextWatcher() { - @Override - public void onTextChanged(CharSequence s, int start, int before, int count) { - /* do nothing */ - } - - @Override - public void beforeTextChanged(CharSequence s, int start, int count, int after) { - /* do nothing */ - } - - @Override - public void afterTextChanged(Editable editable) { - if (mIgnoreNextCopiesChange) { - mIgnoreNextCopiesChange = false; - return; - } - - final boolean hadErrors = hasErrors(); - - if (editable.length() == 0) { - mCopiesEditText.setError(""); - updateUi(); - return; - } - - int copies = 0; - try { - copies = Integer.parseInt(editable.toString()); - } catch (NumberFormatException nfe) { - /* ignore */ - } - - if (copies < MIN_COPIES) { - mCopiesEditText.setError(""); - updateUi(); - return; - } - - mCopiesEditText.setError(null); - mSpoolerProvider.getSpooler().setPrintJobCopiesNoPersistence( - mPrintJobId, copies); - updateUi(); - - if (hadErrors && !hasErrors() && printAttributesChanged()) { - mController.update(); - } - } - }; - - private final TextWatcher mRangeTextWatcher = new TextWatcher() { - @Override - public void onTextChanged(CharSequence s, int start, int before, int count) { - /* do nothing */ - } - - @Override - public void beforeTextChanged(CharSequence s, int start, int count, int after) { - /* do nothing */ - } - - @Override - public void afterTextChanged(Editable editable) { - if (mIgnoreNextRangeChange) { - mIgnoreNextRangeChange = false; - return; - } - - final boolean hadErrors = hasErrors(); - - String text = editable.toString(); - - if (TextUtils.isEmpty(text)) { - mPageRangeEditText.setError(""); - updateUi(); - return; - } - - String escapedText = PATTERN_ESCAPE_SPECIAL_CHARS.matcher(text).replaceAll("////"); - if (!PATTERN_PAGE_RANGE.matcher(escapedText).matches()) { - mPageRangeEditText.setError(""); - updateUi(); - return; - } - - // The range - Matcher matcher = PATTERN_DIGITS.matcher(text); - while (matcher.find()) { - String numericString = text.substring(matcher.start(), matcher.end()).trim(); - if (TextUtils.isEmpty(numericString)) { - continue; - } - final int pageIndex = Integer.parseInt(numericString); - if (pageIndex < 1 || pageIndex > mDocument.info.getPageCount()) { - mPageRangeEditText.setError(""); - updateUi(); - return; - } - } - - // We intentionally do not catch the case of the from page being - // greater than the to page. When computing the requested pages - // we just swap them if necessary. - - // Keep the print job up to date with the selected pages if we - // know how many pages are there in the document. - PageRange[] requestedPages = getRequestedPages(); - if (requestedPages != null && requestedPages.length > 0 - && requestedPages[requestedPages.length - 1].getEnd() - < mDocument.info.getPageCount()) { - mSpoolerProvider.getSpooler().setPrintJobPagesNoPersistence( - mPrintJobId, requestedPages); - } - - mPageRangeEditText.setError(null); - mPrintButton.setEnabled(true); - updateUi(); - - if (hadErrors && !hasErrors() && printAttributesChanged()) { - updateUi(); - } - } - }; - - private final WaitForPrinterCapabilitiesTimeout mCapabilitiesTimeout = - new WaitForPrinterCapabilitiesTimeout(); - - private int mEditorState; - - private boolean mIgnoreNextDestinationChange; - private int mOldMediaSizeSelectionIndex; - private int mOldColorModeSelectionIndex; - private boolean mIgnoreNextOrientationChange; - private boolean mIgnoreNextRangeOptionChange; - private boolean mIgnoreNextCopiesChange; - private boolean mIgnoreNextRangeChange; - private boolean mIgnoreNextMediaSizeChange; - private boolean mIgnoreNextColorChange; - - private int mCurrentUi = UI_NONE; - - private boolean mFavoritePrinterSelected; - - public Editor() { - showUi(UI_EDITING_PRINT_JOB, null); - } - - public void postCreate() { - // Destination. - mMediaSizeComparator = new MediaSizeComparator(PrintJobConfigActivity.this); - mDestinationSpinnerAdapter = new DestinationAdapter(); - mDestinationSpinnerAdapter.registerDataSetObserver(new DataSetObserver() { - @Override - public void onChanged() { - // Initially, we have only safe to PDF as a printer but after some - // printers are loaded we want to select the user's favorite one - // which is the first. - if (!mFavoritePrinterSelected && mDestinationSpinnerAdapter.getCount() > 1) { - mFavoritePrinterSelected = true; - mDestinationSpinner.setSelection(0); - // Workaround again the weird spinner behavior to notify for selection - // change on the next layout pass as the current printer is used below. - mCurrentPrinter = (PrinterInfo) mDestinationSpinnerAdapter.getItem(0); - } - - // If there is a next printer to select and we succeed selecting - // it - done. Let the selection handling code make everything right. - if (mNextPrinterId != null && selectPrinter(mNextPrinterId)) { - mNextPrinterId = null; - return; - } - - // If the current printer properties changed, we update the UI. - if (mCurrentPrinter != null) { - final int printerCount = mDestinationSpinnerAdapter.getCount(); - for (int i = 0; i < printerCount; i++) { - Object item = mDestinationSpinnerAdapter.getItem(i); - // Some items are not printers - if (item instanceof PrinterInfo) { - PrinterInfo printer = (PrinterInfo) item; - if (!printer.getId().equals(mCurrentPrinter.getId())) { - continue; - } - - // If nothing changed - done. - if (mCurrentPrinter.equals(printer)) { - return; - } - - // If the current printer became available and has no - // capabilities, we refresh it. - if (mCurrentPrinter.getStatus() == PrinterInfo.STATUS_UNAVAILABLE - && printer.getStatus() != PrinterInfo.STATUS_UNAVAILABLE - && printer.getCapabilities() == null) { - if (!mCapabilitiesTimeout.isPosted()) { - mCapabilitiesTimeout.post(); - } - mCurrentPrinter.copyFrom(printer); - refreshCurrentPrinter(); - return; - } - - // If the current printer became unavailable or its - // capabilities go away, we update the UI and add a - // timeout to declare the printer as unavailable. - if ((mCurrentPrinter.getStatus() != PrinterInfo.STATUS_UNAVAILABLE - && printer.getStatus() == PrinterInfo.STATUS_UNAVAILABLE) - || (mCurrentPrinter.getCapabilities() != null - && printer.getCapabilities() == null)) { - if (!mCapabilitiesTimeout.isPosted()) { - mCapabilitiesTimeout.post(); - } - mCurrentPrinter.copyFrom(printer); - updateUi(); - return; - } - - // We just refreshed the current printer. - if (printer.getCapabilities() != null - && mCapabilitiesTimeout.isPosted()) { - mCapabilitiesTimeout.remove(); - updatePrintAttributes(printer.getCapabilities()); - updateUi(); - mController.update(); - } - - // Update the UI if capabilities changed. - boolean capabilitiesChanged = false; - - if (mCurrentPrinter.getCapabilities() == null) { - if (printer.getCapabilities() != null) { - updatePrintAttributes(printer.getCapabilities()); - capabilitiesChanged = true; - } - } else if (!mCurrentPrinter.getCapabilities().equals( - printer.getCapabilities())) { - capabilitiesChanged = true; - } - - // Update the UI if the status changed. - final boolean statusChanged = mCurrentPrinter.getStatus() - != printer.getStatus(); - - // Update the printer with the latest info. - if (!mCurrentPrinter.equals(printer)) { - mCurrentPrinter.copyFrom(printer); - } - - if (capabilitiesChanged || statusChanged) { - // If something changed during update... - if (updateUi() || !mController.hasPerformedLayout()) { - // Update the document. - mController.update(); - } - } - - break; - } - } - } - } - - @Override - public void onInvalidated() { - /* do nothing - we always have one fake PDF printer */ - } - }); - - // Media size. - mMediaSizeSpinnerAdapter = new ArrayAdapter<SpinnerItem<MediaSize>>( - PrintJobConfigActivity.this, - R.layout.spinner_dropdown_item, R.id.title); - - // Color mode. - mColorModeSpinnerAdapter = new ArrayAdapter<SpinnerItem<Integer>>( - PrintJobConfigActivity.this, - R.layout.spinner_dropdown_item, R.id.title); - - // Orientation - mOrientationSpinnerAdapter = new ArrayAdapter<SpinnerItem<Integer>>( - PrintJobConfigActivity.this, - R.layout.spinner_dropdown_item, R.id.title); - String[] orientationLabels = getResources().getStringArray( - R.array.orientation_labels); - mOrientationSpinnerAdapter.add(new SpinnerItem<Integer>( - ORIENTATION_PORTRAIT, orientationLabels[0])); - mOrientationSpinnerAdapter.add(new SpinnerItem<Integer>( - ORIENTATION_LANDSCAPE, orientationLabels[1])); - - // Range options - mRangeOptionsSpinnerAdapter = new ArrayAdapter<SpinnerItem<Integer>>( - PrintJobConfigActivity.this, - R.layout.spinner_dropdown_item, R.id.title); - final int[] rangeOptionsValues = getResources().getIntArray( - R.array.page_options_values); - String[] rangeOptionsLabels = getResources().getStringArray( - R.array.page_options_labels); - final int rangeOptionsCount = rangeOptionsLabels.length; - for (int i = 0; i < rangeOptionsCount; i++) { - mRangeOptionsSpinnerAdapter.add(new SpinnerItem<Integer>( - rangeOptionsValues[i], rangeOptionsLabels[i])); - } - - showUi(UI_EDITING_PRINT_JOB, null); - bindUi(); - updateUi(); - } - - public void reselectCurrentPrinter() { - if (mCurrentPrinter != null) { - // TODO: While the data did not change and we set the adapter - // to a newly inflated spinner, the latter does not show the - // current item unless we poke the adapter. This requires more - // investigation. Maybe an optimization in AdapterView does not - // call into the adapter if the view is not visible which is the - // case when we set the adapter. - mDestinationSpinnerAdapter.notifyDataSetChanged(); - final int position = mDestinationSpinnerAdapter.getPrinterIndex( - mCurrentPrinter.getId()); - mDestinationSpinner.setSelection(position); - } - } - - public void refreshCurrentPrinter() { - PrinterInfo printer = (PrinterInfo) mDestinationSpinner.getSelectedItem(); - if (printer != null) { - FusedPrintersProvider printersLoader = (FusedPrintersProvider) - (Loader<?>) getLoaderManager().getLoader( - LOADER_ID_PRINTERS_LOADER); - if (printersLoader != null) { - printersLoader.setTrackedPrinter(printer.getId()); - } - } - } - - public void addCurrentPrinterToHistory() { - PrinterInfo printer = (PrinterInfo) mDestinationSpinner.getSelectedItem(); - PrinterId fakePdfPritnerId = mDestinationSpinnerAdapter.mFakePdfPrinter.getId(); - if (printer != null && !printer.getId().equals(fakePdfPritnerId)) { - FusedPrintersProvider printersLoader = (FusedPrintersProvider) - (Loader<?>) getLoaderManager().getLoader( - LOADER_ID_PRINTERS_LOADER); - if (printersLoader != null) { - printersLoader.addHistoricalPrinter(printer); - } - } - } - - public void updateFromAdvancedOptions(PrintJobInfo printJobInfo) { - boolean updateContent = false; - - // Copies. - mCopiesEditText.setText(String.valueOf(printJobInfo.getCopies())); - - // Media size and orientation - PrintAttributes attributes = printJobInfo.getAttributes(); - if (!mCurrPrintAttributes.getMediaSize().equals(attributes.getMediaSize())) { - final int mediaSizeCount = mMediaSizeSpinnerAdapter.getCount(); - for (int i = 0; i < mediaSizeCount; i++) { - MediaSize mediaSize = mMediaSizeSpinnerAdapter.getItem(i).value; - if (mediaSize.asPortrait().equals(attributes.getMediaSize().asPortrait())) { - updateContent = true; - mCurrPrintAttributes.setMediaSize(attributes.getMediaSize()); - mMediaSizeSpinner.setSelection(i); - mIgnoreNextMediaSizeChange = true; - if (attributes.getMediaSize().isPortrait()) { - mOrientationSpinner.setSelection(0); - mIgnoreNextOrientationChange = true; - } else { - mOrientationSpinner.setSelection(1); - mIgnoreNextOrientationChange = true; - } - break; - } - } - } - - // Color mode. - final int colorMode = attributes.getColorMode(); - if (mCurrPrintAttributes.getColorMode() != colorMode) { - if (colorMode == PrintAttributes.COLOR_MODE_MONOCHROME) { - updateContent = true; - mColorModeSpinner.setSelection(0); - mIgnoreNextColorChange = true; - mCurrPrintAttributes.setColorMode(attributes.getColorMode()); - } else if (colorMode == PrintAttributes.COLOR_MODE_COLOR) { - updateContent = true; - mColorModeSpinner.setSelection(1); - mIgnoreNextColorChange = true; - mCurrPrintAttributes.setColorMode(attributes.getColorMode()); - } - } - - // Range. - PageRange[] pageRanges = printJobInfo.getPages(); - if (pageRanges != null && pageRanges.length > 0) { - pageRanges = PageRangeUtils.normalize(pageRanges); - final int pageRangeCount = pageRanges.length; - if (pageRangeCount == 1 && pageRanges[0] == PageRange.ALL_PAGES) { - mRangeOptionsSpinner.setSelection(0); - } else { - final int pageCount = mDocument.info.getPageCount(); - if (pageRanges[0].getStart() >= 0 - && pageRanges[pageRanges.length - 1].getEnd() < pageCount) { - mRangeOptionsSpinner.setSelection(1); - StringBuilder builder = new StringBuilder(); - for (int i = 0; i < pageRangeCount; i++) { - if (builder.length() > 0) { - builder.append(','); - } - PageRange pageRange = pageRanges[i]; - final int shownStartPage = pageRange.getStart() + 1; - final int shownEndPage = pageRange.getEnd() + 1; - builder.append(shownStartPage); - if (shownStartPage != shownEndPage) { - builder.append('-'); - builder.append(shownEndPage); - } - } - mPageRangeEditText.setText(builder.toString()); - } - } - } - - // Update the advanced options. - mSpoolerProvider.getSpooler().setPrintJobAdvancedOptionsNoPersistence( - mPrintJobId, printJobInfo.getAdvancedOptions()); - - // Update the content if needed. - if (updateContent) { - mController.update(); - } - } - - public void ensurePrinterSelected(PrinterId printerId) { - // If the printer is not present maybe the loader is not - // updated yet. In this case make a note and as soon as - // the printer appears will will select it. - if (!selectPrinter(printerId)) { - mNextPrinterId = printerId; - } - } - - public boolean selectPrinter(PrinterId printerId) { - mDestinationSpinnerAdapter.ensurePrinterInVisibleAdapterPosition(printerId); - final int position = mDestinationSpinnerAdapter.getPrinterIndex(printerId); - if (position != AdapterView.INVALID_POSITION - && position != mDestinationSpinner.getSelectedItemPosition()) { - Object item = mDestinationSpinnerAdapter.getItem(position); - mCurrentPrinter = (PrinterInfo) item; - mDestinationSpinner.setSelection(position); - return true; - } - return false; - } - - public void ensureCurrentPrinterSelected() { - if (mCurrentPrinter != null) { - selectPrinter(mCurrentPrinter.getId()); - } - } - - public boolean isPrintingToPdf() { - return mDestinationSpinner.getSelectedItem() - == mDestinationSpinnerAdapter.mFakePdfPrinter; - } - - public boolean shouldCloseOnTouch(MotionEvent event) { - if (event.getAction() != MotionEvent.ACTION_DOWN) { - return false; - } - - final int[] locationInWindow = new int[2]; - mContentContainer.getLocationInWindow(locationInWindow); - - final int windowTouchSlop = ViewConfiguration.get(PrintJobConfigActivity.this) - .getScaledWindowTouchSlop(); - final int eventX = (int) event.getX(); - final int eventY = (int) event.getY(); - final int lenientWindowLeft = locationInWindow[0] - windowTouchSlop; - final int lenientWindowRight = lenientWindowLeft + mContentContainer.getWidth() - + windowTouchSlop; - final int lenientWindowTop = locationInWindow[1] - windowTouchSlop; - final int lenientWindowBottom = lenientWindowTop + mContentContainer.getHeight() - + windowTouchSlop; - - if (eventX < lenientWindowLeft || eventX > lenientWindowRight - || eventY < lenientWindowTop || eventY > lenientWindowBottom) { - return true; - } - return false; - } - - public boolean isShwoingGeneratingPrintJobUi() { - return (mCurrentUi == UI_GENERATING_PRINT_JOB); - } - - public void showUi(int ui, final Runnable postSwitchCallback) { - if (ui == UI_NONE) { - throw new IllegalStateException("cannot remove the ui"); - } - - if (mCurrentUi == ui) { - return; - } - - final int oldUi = mCurrentUi; - mCurrentUi = ui; - - switch (oldUi) { - case UI_NONE: { - switch (ui) { - case UI_EDITING_PRINT_JOB: { - doUiSwitch(R.layout.print_job_config_activity_content_editing); - registerPrintButtonClickListener(); - if (postSwitchCallback != null) { - postSwitchCallback.run(); - } - } break; - - case UI_GENERATING_PRINT_JOB: { - doUiSwitch(R.layout.print_job_config_activity_content_generating); - registerCancelButtonClickListener(); - if (postSwitchCallback != null) { - postSwitchCallback.run(); - } - } break; - } - } break; - - case UI_EDITING_PRINT_JOB: { - switch (ui) { - case UI_GENERATING_PRINT_JOB: { - animateUiSwitch(R.layout.print_job_config_activity_content_generating, - new Runnable() { - @Override - public void run() { - registerCancelButtonClickListener(); - if (postSwitchCallback != null) { - postSwitchCallback.run(); - } - } - }, - new FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, - ViewGroup.LayoutParams.WRAP_CONTENT, Gravity.CENTER)); - } break; - - case UI_ERROR: { - animateUiSwitch(R.layout.print_job_config_activity_content_error, - new Runnable() { - @Override - public void run() { - registerOkButtonClickListener(); - if (postSwitchCallback != null) { - postSwitchCallback.run(); - } - } - }, - new FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, - ViewGroup.LayoutParams.WRAP_CONTENT, Gravity.CENTER)); - } break; - } - } break; - - case UI_GENERATING_PRINT_JOB: { - switch (ui) { - case UI_EDITING_PRINT_JOB: { - animateUiSwitch(R.layout.print_job_config_activity_content_editing, - new Runnable() { - @Override - public void run() { - registerPrintButtonClickListener(); - if (postSwitchCallback != null) { - postSwitchCallback.run(); - } - } - }, - new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.MATCH_PARENT, Gravity.CENTER)); - } break; - - case UI_ERROR: { - animateUiSwitch(R.layout.print_job_config_activity_content_error, - new Runnable() { - @Override - public void run() { - registerOkButtonClickListener(); - if (postSwitchCallback != null) { - postSwitchCallback.run(); - } - } - }, - new FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, - ViewGroup.LayoutParams.WRAP_CONTENT, Gravity.CENTER)); - } break; - } - } break; - - case UI_ERROR: { - switch (ui) { - case UI_EDITING_PRINT_JOB: { - animateUiSwitch(R.layout.print_job_config_activity_content_editing, - new Runnable() { - @Override - public void run() { - registerPrintButtonClickListener(); - if (postSwitchCallback != null) { - postSwitchCallback.run(); - } - } - }, - new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.MATCH_PARENT, Gravity.CENTER)); - } break; - } - } break; - } - } - - private void registerAdvancedPrintOptionsButtonClickListener() { - Button advancedOptionsButton = (Button) findViewById(R.id.advanced_settings_button); - advancedOptionsButton.setOnClickListener(new OnClickListener() { - @Override - public void onClick(View v) { - ComponentName serviceName = mCurrentPrinter.getId().getServiceName(); - String activityName = getAdvancedOptionsActivityName(serviceName); - if (TextUtils.isEmpty(activityName)) { - return; - } - Intent intent = new Intent(Intent.ACTION_MAIN); - intent.setComponent(new ComponentName(serviceName.getPackageName(), - activityName)); - - List<ResolveInfo> resolvedActivities = getPackageManager() - .queryIntentActivities(intent, 0); - if (resolvedActivities.isEmpty()) { - return; - } - // The activity is a component name, therefore it is one or none. - if (resolvedActivities.get(0).activityInfo.exported) { - PrintJobInfo printJobInfo = mSpoolerProvider.getSpooler().getPrintJobInfo( - mPrintJobId, PrintManager.APP_ID_ANY); - intent.putExtra(PrintService.EXTRA_PRINT_JOB_INFO, printJobInfo); - // TODO: Make this an API for the next release. - intent.putExtra("android.intent.extra.print.EXTRA_PRINTER_INFO", - mCurrentPrinter); - try { - startActivityForResult(intent, - ACTIVITY_POPULATE_ADVANCED_PRINT_OPTIONS); - } catch (ActivityNotFoundException anfe) { - Log.e(LOG_TAG, "Error starting activity for intent: " + intent, anfe); - } - } - } - }); - } - - private void registerPrintButtonClickListener() { - Button printButton = (Button) findViewById(R.id.print_button); - printButton.setOnClickListener(new OnClickListener() { - @Override - public void onClick(View v) { - PrinterInfo printer = (PrinterInfo) mDestinationSpinner.getSelectedItem(); - if (printer != null) { - mEditor.confirmPrint(); - mController.update(); - if (!printer.equals(mDestinationSpinnerAdapter.mFakePdfPrinter)) { - mEditor.refreshCurrentPrinter(); - } - } else { - mEditor.cancel(); - PrintJobConfigActivity.this.finish(); - } - } - }); - } - - private void registerCancelButtonClickListener() { - Button cancelButton = (Button) findViewById(R.id.cancel_button); - cancelButton.setOnClickListener(new OnClickListener() { - @Override - public void onClick(View v) { - if (!mController.isWorking()) { - PrintJobConfigActivity.this.finish(); - } - mEditor.cancel(); - } - }); - } - - private void registerOkButtonClickListener() { - Button okButton = (Button) findViewById(R.id.ok_button); - okButton.setOnClickListener(new OnClickListener() { - @Override - public void onClick(View v) { - mEditor.showUi(Editor.UI_EDITING_PRINT_JOB, new Runnable() { - @Override - public void run() { - // Start over with a clean slate. - mOldPrintAttributes.clear(); - mController.initialize(); - mEditor.initialize(); - mEditor.bindUi(); - mEditor.reselectCurrentPrinter(); - if (!mController.hasPerformedLayout()) { - mController.update(); - } - } - }); - } - }); - } - - private void doUiSwitch(int showLayoutId) { - ViewGroup contentContainer = (ViewGroup) findViewById(R.id.content_container); - contentContainer.removeAllViews(); - getLayoutInflater().inflate(showLayoutId, contentContainer, true); - } - - private void animateUiSwitch(int showLayoutId, final Runnable beforeShowNewUiAction, - final LayoutParams containerParams) { - // Find everything we will shuffle around. - final ViewGroup contentContainer = (ViewGroup) findViewById(R.id.content_container); - final View hidingView = contentContainer.getChildAt(0); - final View showingView = getLayoutInflater().inflate(showLayoutId, - null, false); - - // First animation - fade out the old content. - AutoCancellingAnimator.animate(hidingView).alpha(0.0f) - .withLayer().withEndAction(new Runnable() { - @Override - public void run() { - hidingView.setVisibility(View.INVISIBLE); - - // Prepare the new content with correct size and alpha. - showingView.setMinimumWidth(contentContainer.getWidth()); - showingView.setAlpha(0.0f); - - // Compute how to much shrink /stretch the content. - final int widthSpec = MeasureSpec.makeMeasureSpec( - contentContainer.getWidth(), MeasureSpec.UNSPECIFIED); - final int heightSpec = MeasureSpec.makeMeasureSpec( - contentContainer.getHeight(), MeasureSpec.UNSPECIFIED); - showingView.measure(widthSpec, heightSpec); - final float scaleY = (float) showingView.getMeasuredHeight() - / (float) contentContainer.getHeight(); - - // Second animation - resize the container. - AutoCancellingAnimator.animate(contentContainer).scaleY(scaleY) - .withEndAction(new Runnable() { - @Override - public void run() { - // Swap the old and the new content. - contentContainer.removeAllViews(); - contentContainer.setScaleY(1.0f); - contentContainer.addView(showingView); - - contentContainer.setLayoutParams(containerParams); - - beforeShowNewUiAction.run(); - - // Third animation - show the new content. - AutoCancellingAnimator.animate(showingView).alpha(1.0f); - } - }); - } - }); - } - - public void initialize() { - mEditorState = EDITOR_STATE_INITIALIZED; - } - - public boolean isCancelled() { - return mEditorState == EDITOR_STATE_CANCELLED; - } - - public void cancel() { - mEditorState = EDITOR_STATE_CANCELLED; - mController.cancel(); - updateUi(); - } - - public boolean isDone() { - return isPrintConfirmed() || isCancelled(); - } - - public boolean isPrintConfirmed() { - return mEditorState == EDITOR_STATE_CONFIRMED_PRINT; - } - - public void confirmPrint() { - addCurrentPrinterToHistory(); - mEditorState = EDITOR_STATE_CONFIRMED_PRINT; - showUi(UI_GENERATING_PRINT_JOB, null); - } - - public PageRange[] getRequestedPages() { - if (hasErrors()) { - return null; - } - if (mRangeOptionsSpinner.getSelectedItemPosition() > 0) { - List<PageRange> pageRanges = new ArrayList<PageRange>(); - mStringCommaSplitter.setString(mPageRangeEditText.getText().toString()); - - while (mStringCommaSplitter.hasNext()) { - String range = mStringCommaSplitter.next().trim(); - if (TextUtils.isEmpty(range)) { - continue; - } - final int dashIndex = range.indexOf('-'); - final int fromIndex; - final int toIndex; - - if (dashIndex > 0) { - fromIndex = Integer.parseInt(range.substring(0, dashIndex).trim()) - 1; - // It is possible that the dash is at the end since the input - // verification can has to allow the user to keep entering if - // this would lead to a valid input. So we handle this. - toIndex = (dashIndex < range.length() - 1) - ? Integer.parseInt(range.substring(dashIndex + 1, - range.length()).trim()) - 1 : fromIndex; - } else { - fromIndex = toIndex = Integer.parseInt(range) - 1; - } - - PageRange pageRange = new PageRange(Math.min(fromIndex, toIndex), - Math.max(fromIndex, toIndex)); - pageRanges.add(pageRange); - } - - PageRange[] pageRangesArray = new PageRange[pageRanges.size()]; - pageRanges.toArray(pageRangesArray); - - return PageRangeUtils.normalize(pageRangesArray); - } - - return ALL_PAGES_ARRAY; - } - - private void bindUi() { - if (mCurrentUi != UI_EDITING_PRINT_JOB) { - return; - } - - // Content container - mContentContainer = findViewById(R.id.content_container); - - // Copies - mCopiesEditText = (EditText) findViewById(R.id.copies_edittext); - mCopiesEditText.setOnFocusChangeListener(mFocusListener); - mCopiesEditText.setText(MIN_COPIES_STRING); - mCopiesEditText.setSelection(mCopiesEditText.getText().length()); - mCopiesEditText.addTextChangedListener(mCopiesTextWatcher); - if (!TextUtils.equals(mCopiesEditText.getText(), MIN_COPIES_STRING)) { - mIgnoreNextCopiesChange = true; - } - mSpoolerProvider.getSpooler().setPrintJobCopiesNoPersistence( - mPrintJobId, MIN_COPIES); - - // Destination. - mDestinationSpinner = (Spinner) findViewById(R.id.destination_spinner); - mDestinationSpinner.setDropDownWidth(ViewGroup.LayoutParams.MATCH_PARENT); - mDestinationSpinner.setAdapter(mDestinationSpinnerAdapter); - mDestinationSpinner.setOnItemSelectedListener(mOnItemSelectedListener); - if (mDestinationSpinnerAdapter.getCount() > 0) { - mIgnoreNextDestinationChange = true; - } - - // Media size. - mMediaSizeSpinner = (Spinner) findViewById(R.id.paper_size_spinner); - mMediaSizeSpinner.setAdapter(mMediaSizeSpinnerAdapter); - mMediaSizeSpinner.setOnItemSelectedListener(mOnItemSelectedListener); - if (mMediaSizeSpinnerAdapter.getCount() > 0) { - mOldMediaSizeSelectionIndex = 0; - } - - // Color mode. - mColorModeSpinner = (Spinner) findViewById(R.id.color_spinner); - mColorModeSpinner.setAdapter(mColorModeSpinnerAdapter); - mColorModeSpinner.setOnItemSelectedListener(mOnItemSelectedListener); - if (mColorModeSpinnerAdapter.getCount() > 0) { - mOldColorModeSelectionIndex = 0; - } - - // Orientation - mOrientationSpinner = (Spinner) findViewById(R.id.orientation_spinner); - mOrientationSpinner.setAdapter(mOrientationSpinnerAdapter); - mOrientationSpinner.setOnItemSelectedListener(mOnItemSelectedListener); - if (mOrientationSpinnerAdapter.getCount() > 0) { - mIgnoreNextOrientationChange = true; - } - - // Range options - mRangeOptionsTitle = (TextView) findViewById(R.id.range_options_title); - mRangeOptionsSpinner = (Spinner) findViewById(R.id.range_options_spinner); - mRangeOptionsSpinner.setAdapter(mRangeOptionsSpinnerAdapter); - mRangeOptionsSpinner.setOnItemSelectedListener(mOnItemSelectedListener); - if (mRangeOptionsSpinnerAdapter.getCount() > 0) { - mIgnoreNextRangeOptionChange = true; - } - - // Page range - mPageRangeTitle = (TextView) findViewById(R.id.page_range_title); - mPageRangeEditText = (EditText) findViewById(R.id.page_range_edittext); - mPageRangeEditText.setOnFocusChangeListener(mFocusListener); - mPageRangeEditText.addTextChangedListener(mRangeTextWatcher); - - // Advanced options button. - mAdvancedPrintOptionsContainer = findViewById(R.id.advanced_settings_container); - mAdvancedOptionsButton = (Button) findViewById(R.id.advanced_settings_button); - registerAdvancedPrintOptionsButtonClickListener(); - - // Print button - mPrintButton = (Button) findViewById(R.id.print_button); - registerPrintButtonClickListener(); - } - - public boolean updateUi() { - if (mCurrentUi != UI_EDITING_PRINT_JOB) { - return false; - } - if (isPrintConfirmed() || isCancelled()) { - mDestinationSpinner.setEnabled(false); - mCopiesEditText.setEnabled(false); - mMediaSizeSpinner.setEnabled(false); - mColorModeSpinner.setEnabled(false); - mOrientationSpinner.setEnabled(false); - mRangeOptionsSpinner.setEnabled(false); - mPageRangeEditText.setEnabled(false); - mPrintButton.setEnabled(false); - mAdvancedOptionsButton.setEnabled(false); - return false; - } - - // If a printer with capabilities is selected, then we enabled all options. - boolean allOptionsEnabled = false; - final int selectedIndex = mDestinationSpinner.getSelectedItemPosition(); - if (selectedIndex >= 0) { - Object item = mDestinationSpinnerAdapter.getItem(selectedIndex); - if (item instanceof PrinterInfo) { - PrinterInfo printer = (PrinterInfo) item; - if (printer.getCapabilities() != null - && printer.getStatus() != PrinterInfo.STATUS_UNAVAILABLE) { - allOptionsEnabled = true; - } - } - } - - if (!allOptionsEnabled) { - mCopiesEditText.setEnabled(false); - mMediaSizeSpinner.setEnabled(false); - mColorModeSpinner.setEnabled(false); - mOrientationSpinner.setEnabled(false); - mRangeOptionsSpinner.setEnabled(false); - mPageRangeEditText.setEnabled(false); - mPrintButton.setEnabled(false); - mAdvancedOptionsButton.setEnabled(false); - return false; - } else { - boolean someAttributeSelectionChanged = false; - - PrinterInfo printer = (PrinterInfo) mDestinationSpinner.getSelectedItem(); - PrinterCapabilitiesInfo capabilities = printer.getCapabilities(); - PrintAttributes defaultAttributes = printer.getCapabilities().getDefaults(); - - // Media size. - // Sort the media sizes based on the current locale. - List<MediaSize> mediaSizes = new ArrayList<MediaSize>(capabilities.getMediaSizes()); - Collections.sort(mediaSizes, mMediaSizeComparator); - - // If the media sizes changed, we update the adapter and the spinner. - boolean mediaSizesChanged = false; - final int mediaSizeCount = mediaSizes.size(); - if (mediaSizeCount != mMediaSizeSpinnerAdapter.getCount()) { - mediaSizesChanged = true; - } else { - for (int i = 0; i < mediaSizeCount; i++) { - if (!mediaSizes.get(i).equals(mMediaSizeSpinnerAdapter.getItem(i).value)) { - mediaSizesChanged = true; - break; - } - } - } - if (mediaSizesChanged) { - // Remember the old media size to try selecting it again. - int oldMediaSizeNewIndex = AdapterView.INVALID_POSITION; - MediaSize oldMediaSize = mCurrPrintAttributes.getMediaSize(); - - // Rebuild the adapter data. - mMediaSizeSpinnerAdapter.clear(); - for (int i = 0; i < mediaSizeCount; i++) { - MediaSize mediaSize = mediaSizes.get(i); - if (mediaSize.asPortrait().equals(oldMediaSize.asPortrait())) { - // Update the index of the old selection. - oldMediaSizeNewIndex = i; - } - mMediaSizeSpinnerAdapter.add(new SpinnerItem<MediaSize>( - mediaSize, mediaSize.getLabel(getPackageManager()))); - } - - mMediaSizeSpinner.setEnabled(true); - - if (oldMediaSizeNewIndex != AdapterView.INVALID_POSITION) { - // Select the old media size - nothing really changed. - setMediaSizeSpinnerSelectionNoCallback(oldMediaSizeNewIndex); - } else { - // Select the first or the default and mark if selection changed. - final int mediaSizeIndex = Math.max(mediaSizes.indexOf( - defaultAttributes.getMediaSize()), 0); - setMediaSizeSpinnerSelectionNoCallback(mediaSizeIndex); - if (oldMediaSize.isPortrait()) { - mCurrPrintAttributes.setMediaSize(mMediaSizeSpinnerAdapter - .getItem(mediaSizeIndex).value.asPortrait()); - } else { - mCurrPrintAttributes.setMediaSize(mMediaSizeSpinnerAdapter - .getItem(mediaSizeIndex).value.asLandscape()); - } - someAttributeSelectionChanged = true; - } - } - mMediaSizeSpinner.setEnabled(true); - - // Color mode. - final int colorModes = capabilities.getColorModes(); - - // If the color modes changed, we update the adapter and the spinner. - boolean colorModesChanged = false; - if (Integer.bitCount(colorModes) != mColorModeSpinnerAdapter.getCount()) { - colorModesChanged = true; - } else { - int remainingColorModes = colorModes; - int adapterIndex = 0; - while (remainingColorModes != 0) { - final int colorBitOffset = Integer.numberOfTrailingZeros( - remainingColorModes); - final int colorMode = 1 << colorBitOffset; - remainingColorModes &= ~colorMode; - if (colorMode != mColorModeSpinnerAdapter.getItem(adapterIndex).value) { - colorModesChanged = true; - break; - } - adapterIndex++; - } - } - if (colorModesChanged) { - // Remember the old color mode to try selecting it again. - int oldColorModeNewIndex = AdapterView.INVALID_POSITION; - final int oldColorMode = mCurrPrintAttributes.getColorMode(); - - // Rebuild the adapter data. - 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; - if (colorMode == oldColorMode) { - // Update the index of the old selection. - oldColorModeNewIndex = colorBitOffset; - } - remainingColorModes &= ~colorMode; - mColorModeSpinnerAdapter.add(new SpinnerItem<Integer>(colorMode, - colorModeLabels[colorBitOffset])); - } - mColorModeSpinner.setEnabled(true); - if (oldColorModeNewIndex != AdapterView.INVALID_POSITION) { - // Select the old color mode - nothing really changed. - setColorModeSpinnerSelectionNoCallback(oldColorModeNewIndex); - } else { - final int selectedColorMode = colorModes & defaultAttributes.getColorMode(); - final int itemCount = mColorModeSpinnerAdapter.getCount(); - for (int i = 0; i < itemCount; i++) { - SpinnerItem<Integer> item = mColorModeSpinnerAdapter.getItem(i); - if (selectedColorMode == item.value) { - setColorModeSpinnerSelectionNoCallback(i); - mCurrPrintAttributes.setColorMode(selectedColorMode); - someAttributeSelectionChanged = true; - } - } - } - } - mColorModeSpinner.setEnabled(true); - - // Orientation - MediaSize mediaSize = mCurrPrintAttributes.getMediaSize(); - if (mediaSize.isPortrait() - && mOrientationSpinner.getSelectedItemPosition() != 0) { - mIgnoreNextOrientationChange = true; - mOrientationSpinner.setSelection(0); - } else if (!mediaSize.isPortrait() - && mOrientationSpinner.getSelectedItemPosition() != 1) { - mIgnoreNextOrientationChange = true; - mOrientationSpinner.setSelection(1); - } - mOrientationSpinner.setEnabled(true); - - // Range options - PrintDocumentInfo info = mDocument.info; - if (info != null && info.getPageCount() > 0) { - if (info.getPageCount() == 1) { - mRangeOptionsSpinner.setEnabled(false); - } else { - mRangeOptionsSpinner.setEnabled(true); - if (mRangeOptionsSpinner.getSelectedItemPosition() > 0) { - if (!mPageRangeEditText.isEnabled()) { - mPageRangeEditText.setEnabled(true); - mPageRangeEditText.setVisibility(View.VISIBLE); - mPageRangeTitle.setVisibility(View.VISIBLE); - mPageRangeEditText.requestFocus(); - InputMethodManager imm = (InputMethodManager) - getSystemService(INPUT_METHOD_SERVICE); - imm.showSoftInput(mPageRangeEditText, 0); - } - } else { - mPageRangeEditText.setEnabled(false); - mPageRangeEditText.setVisibility(View.INVISIBLE); - mPageRangeTitle.setVisibility(View.INVISIBLE); - } - } - final int pageCount = mDocument.info.getPageCount(); - String title = (pageCount != PrintDocumentInfo.PAGE_COUNT_UNKNOWN) - ? getString(R.string.label_pages, String.valueOf(pageCount)) - : getString(R.string.page_count_unknown); - mRangeOptionsTitle.setText(title); - } else { - if (mRangeOptionsSpinner.getSelectedItemPosition() != 0) { - mIgnoreNextRangeOptionChange = true; - mRangeOptionsSpinner.setSelection(0); - } - mRangeOptionsSpinner.setEnabled(false); - mRangeOptionsTitle.setText(getString(R.string.page_count_unknown)); - mPageRangeEditText.setEnabled(false); - mPageRangeEditText.setVisibility(View.INVISIBLE); - mPageRangeTitle.setVisibility(View.INVISIBLE); - } - - // Advanced print options - ComponentName serviceName = mCurrentPrinter.getId().getServiceName(); - if (!TextUtils.isEmpty(getAdvancedOptionsActivityName(serviceName))) { - mAdvancedPrintOptionsContainer.setVisibility(View.VISIBLE); - mAdvancedOptionsButton.setEnabled(true); - } else { - mAdvancedPrintOptionsContainer.setVisibility(View.GONE); - mAdvancedOptionsButton.setEnabled(false); - } - - // Print - if (mDestinationSpinner.getSelectedItemId() - != DEST_ADAPTER_ITEM_ID_SAVE_AS_PDF) { - String newText = getString(R.string.print_button); - if (!TextUtils.equals(newText, mPrintButton.getText())) { - mPrintButton.setText(R.string.print_button); - } - } else { - String newText = getString(R.string.save_button); - if (!TextUtils.equals(newText, mPrintButton.getText())) { - mPrintButton.setText(R.string.save_button); - } - } - if ((mRangeOptionsSpinner.getSelectedItemPosition() == 1 - && (TextUtils.isEmpty(mPageRangeEditText.getText()) || hasErrors())) - || (mRangeOptionsSpinner.getSelectedItemPosition() == 0 - && (!mController.hasPerformedLayout() || hasErrors()))) { - mPrintButton.setEnabled(false); - } else { - mPrintButton.setEnabled(true); - } - - // Copies - if (mDestinationSpinner.getSelectedItemId() - != DEST_ADAPTER_ITEM_ID_SAVE_AS_PDF) { - mCopiesEditText.setEnabled(true); - } else { - mCopiesEditText.setEnabled(false); - } - if (mCopiesEditText.getError() == null - && TextUtils.isEmpty(mCopiesEditText.getText())) { - mIgnoreNextCopiesChange = true; - mCopiesEditText.setText(String.valueOf(MIN_COPIES)); - mCopiesEditText.requestFocus(); - } - - return someAttributeSelectionChanged; - } - } - - private String getAdvancedOptionsActivityName(ComponentName serviceName) { - PrintManager printManager = (PrintManager) getSystemService(Context.PRINT_SERVICE); - List<PrintServiceInfo> printServices = printManager.getEnabledPrintServices(); - final int printServiceCount = printServices.size(); - for (int i = 0; i < printServiceCount; i ++) { - PrintServiceInfo printServiceInfo = printServices.get(i); - ServiceInfo serviceInfo = printServiceInfo.getResolveInfo().serviceInfo; - if (serviceInfo.name.equals(serviceName.getClassName()) - && serviceInfo.packageName.equals(serviceName.getPackageName())) { - return printServiceInfo.getAdvancedOptionsActivityName(); - } - } - return null; - } - - private void setMediaSizeSpinnerSelectionNoCallback(int position) { - if (mMediaSizeSpinner.getSelectedItemPosition() != position) { - mOldMediaSizeSelectionIndex = position; - mMediaSizeSpinner.setSelection(position); - } - } - - private void setColorModeSpinnerSelectionNoCallback(int position) { - if (mColorModeSpinner.getSelectedItemPosition() != position) { - mOldColorModeSelectionIndex = position; - mColorModeSpinner.setSelection(position); - } - } - - private void startSelectPrinterActivity() { - Intent intent = new Intent(PrintJobConfigActivity.this, - SelectPrinterActivity.class); - startActivityForResult(intent, ACTIVITY_REQUEST_SELECT_PRINTER); - } - - private boolean hasErrors() { - if (mCopiesEditText.getError() != null) { - return true; - } - return mPageRangeEditText.getVisibility() == View.VISIBLE - && mPageRangeEditText.getError() != null; - } - - 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(); - } - } - - private final class WaitForPrinterCapabilitiesTimeout implements Runnable { - private static final long GET_CAPABILITIES_TIMEOUT_MILLIS = 10000; // 10sec - - private boolean mIsPosted; - - public void post() { - if (!mIsPosted) { - mDestinationSpinner.postDelayed(this, - GET_CAPABILITIES_TIMEOUT_MILLIS); - mIsPosted = true; - } - } - - public void remove() { - if (mIsPosted) { - mIsPosted = false; - mDestinationSpinner.removeCallbacks(this); - } - } - - public boolean isPosted() { - return mIsPosted; - } - - @Override - public void run() { - mIsPosted = false; - if (mDestinationSpinner.getSelectedItemPosition() >= 0) { - View itemView = mDestinationSpinner.getSelectedView(); - TextView titleView = (TextView) itemView.findViewById(R.id.subtitle); - try { - PackageInfo packageInfo = getPackageManager().getPackageInfo( - mCurrentPrinter.getId().getServiceName().getPackageName(), 0); - CharSequence service = packageInfo.applicationInfo.loadLabel( - getPackageManager()); - String subtitle = getString(R.string.printer_unavailable, service.toString()); - titleView.setText(subtitle); - } catch (NameNotFoundException nnfe) { - /* ignore */ - } - } - } - } - - private final class DestinationAdapter extends BaseAdapter - implements LoaderManager.LoaderCallbacks<List<PrinterInfo>>{ - private final List<PrinterInfo> mPrinters = new ArrayList<PrinterInfo>(); - - private PrinterInfo mFakePdfPrinter; - - public DestinationAdapter() { - getLoaderManager().initLoader(LOADER_ID_PRINTERS_LOADER, null, this); - } - - public int getPrinterIndex(PrinterId printerId) { - for (int i = 0; i < getCount(); i++) { - PrinterInfo printer = (PrinterInfo) getItem(i); - if (printer != null && printer.getId().equals(printerId)) { - return i; - } - } - return AdapterView.INVALID_POSITION; - } - - public void ensurePrinterInVisibleAdapterPosition(PrinterId printerId) { - final int printerCount = mPrinters.size(); - for (int i = 0; i < printerCount; i++) { - PrinterInfo printer = (PrinterInfo) mPrinters.get(i); - if (printer.getId().equals(printerId)) { - // If already in the list - do nothing. - if (i < getCount() - 2) { - return; - } - // Else replace the last one (two items are not printers). - final int lastPrinterIndex = getCount() - 3; - mPrinters.set(i, mPrinters.get(lastPrinterIndex)); - mPrinters.set(lastPrinterIndex, printer); - notifyDataSetChanged(); - return; - } - } - } - - @Override - public int getCount() { - if (mFakePdfPrinter == null) { - return 0; - } - return Math.min(mPrinters.size() + 2, DEST_ADAPTER_MAX_ITEM_COUNT); - } - - @Override - public boolean isEnabled(int position) { - Object item = getItem(position); - if (item instanceof PrinterInfo) { - PrinterInfo printer = (PrinterInfo) item; - return printer.getStatus() != PrinterInfo.STATUS_UNAVAILABLE; - } - return true; - } - - @Override - public Object getItem(int position) { - if (mPrinters.isEmpty()) { - if (position == 0 && mFakePdfPrinter != null) { - return mFakePdfPrinter; - } - } else { - if (position < 1) { - return mPrinters.get(position); - } - if (position == 1 && mFakePdfPrinter != null) { - return mFakePdfPrinter; - } - if (position < getCount() - 1) { - return mPrinters.get(position - 1); - } - } - return null; - } - - @Override - public long getItemId(int position) { - if (mPrinters.isEmpty()) { - if (mFakePdfPrinter != null) { - if (position == 0) { - return DEST_ADAPTER_ITEM_ID_SAVE_AS_PDF; - } else if (position == 1) { - return DEST_ADAPTER_ITEM_ID_ALL_PRINTERS; - } - } - } else { - if (position == 1 && mFakePdfPrinter != null) { - return DEST_ADAPTER_ITEM_ID_SAVE_AS_PDF; - } - if (position == getCount() - 1) { - return DEST_ADAPTER_ITEM_ID_ALL_PRINTERS; - } - } - return position; - } - - @Override - public View getDropDownView(int position, View convertView, - ViewGroup parent) { - View view = getView(position, convertView, parent); - view.setEnabled(isEnabled(position)); - return view; - } - - @Override - public View getView(int position, View convertView, ViewGroup parent) { - if (convertView == null) { - convertView = getLayoutInflater().inflate( - R.layout.printer_dropdown_item, parent, false); - } - - CharSequence title = null; - CharSequence subtitle = null; - Drawable icon = null; - - if (mPrinters.isEmpty()) { - if (position == 0 && mFakePdfPrinter != null) { - PrinterInfo printer = (PrinterInfo) getItem(position); - title = printer.getName(); - } else if (position == 1) { - title = getString(R.string.all_printers); - } - } else { - if (position == 1 && mFakePdfPrinter != null) { - PrinterInfo printer = (PrinterInfo) getItem(position); - title = printer.getName(); - } else if (position == getCount() - 1) { - title = getString(R.string.all_printers); - } else { - PrinterInfo printer = (PrinterInfo) getItem(position); - title = printer.getName(); - try { - PackageInfo packageInfo = getPackageManager().getPackageInfo( - printer.getId().getServiceName().getPackageName(), 0); - subtitle = packageInfo.applicationInfo.loadLabel(getPackageManager()); - icon = packageInfo.applicationInfo.loadIcon(getPackageManager()); - } catch (NameNotFoundException nnfe) { - /* ignore */ - } - } - } - - TextView titleView = (TextView) convertView.findViewById(R.id.title); - titleView.setText(title); - - TextView subtitleView = (TextView) convertView.findViewById(R.id.subtitle); - if (!TextUtils.isEmpty(subtitle)) { - subtitleView.setText(subtitle); - subtitleView.setVisibility(View.VISIBLE); - } else { - subtitleView.setText(null); - subtitleView.setVisibility(View.GONE); - } - - ImageView iconView = (ImageView) convertView.findViewById(R.id.icon); - if (icon != null) { - iconView.setImageDrawable(icon); - iconView.setVisibility(View.VISIBLE); - } else { - iconView.setVisibility(View.INVISIBLE); - } - - return convertView; - } - - @Override - public Loader<List<PrinterInfo>> onCreateLoader(int id, Bundle args) { - if (id == LOADER_ID_PRINTERS_LOADER) { - return new FusedPrintersProvider(PrintJobConfigActivity.this); - } - return null; - } - - @Override - public void onLoadFinished(Loader<List<PrinterInfo>> loader, - List<PrinterInfo> printers) { - // If this is the first load, create the fake PDF printer. - // We do this to avoid flicker where the PDF printer is the - // only one and as soon as the loader loads the favorites - // it gets switched. Not a great user experience. - if (mFakePdfPrinter == null) { - mCurrentPrinter = mFakePdfPrinter = createFakePdfPrinter(); - updatePrintAttributes(mCurrentPrinter.getCapabilities()); - updateUi(); - } - - // We rearrange the printers if the user selects a printer - // not shown in the initial short list. Therefore, we have - // to keep the printer order. - - // No old printers - do not bother keeping their position. - if (mPrinters.isEmpty()) { - mPrinters.addAll(printers); - mEditor.ensureCurrentPrinterSelected(); - notifyDataSetChanged(); - return; - } - - // Add the new printers to a map. - ArrayMap<PrinterId, PrinterInfo> newPrintersMap = - new ArrayMap<PrinterId, PrinterInfo>(); - final int printerCount = printers.size(); - for (int i = 0; i < printerCount; i++) { - PrinterInfo printer = printers.get(i); - newPrintersMap.put(printer.getId(), printer); - } - - List<PrinterInfo> newPrinters = new ArrayList<PrinterInfo>(); - - // Update printers we already have. - final int oldPrinterCount = mPrinters.size(); - for (int i = 0; i < oldPrinterCount; i++) { - PrinterId oldPrinterId = mPrinters.get(i).getId(); - PrinterInfo updatedPrinter = newPrintersMap.remove(oldPrinterId); - if (updatedPrinter != null) { - newPrinters.add(updatedPrinter); - } - } - - // Add the rest of the new printers, i.e. what is left. - newPrinters.addAll(newPrintersMap.values()); - - mPrinters.clear(); - mPrinters.addAll(newPrinters); - - mEditor.ensureCurrentPrinterSelected(); - notifyDataSetChanged(); - } - - @Override - public void onLoaderReset(Loader<List<PrinterInfo>> loader) { - mPrinters.clear(); - notifyDataSetInvalidated(); - } - - - private PrinterInfo createFakePdfPrinter() { - MediaSize defaultMediaSize = MediaSizeUtils.getDefault(PrintJobConfigActivity.this); - - PrinterId printerId = new PrinterId(getComponentName(), "PDF printer"); - - PrinterCapabilitiesInfo.Builder builder = - new PrinterCapabilitiesInfo.Builder(printerId); - - String[] mediaSizeIds = getResources().getStringArray( - R.array.pdf_printer_media_sizes); - final int mediaSizeIdCount = mediaSizeIds.length; - for (int i = 0; i < mediaSizeIdCount; i++) { - String id = mediaSizeIds[i]; - MediaSize mediaSize = MediaSize.getStandardMediaSizeById(id); - builder.addMediaSize(mediaSize, mediaSize.equals(defaultMediaSize)); - } - - builder.addResolution(new Resolution("PDF resolution", "PDF resolution", - 300, 300), true); - builder.setColorModes(PrintAttributes.COLOR_MODE_COLOR - | PrintAttributes.COLOR_MODE_MONOCHROME, - PrintAttributes.COLOR_MODE_COLOR); - - return new PrinterInfo.Builder(printerId, getString(R.string.save_as_pdf), - PrinterInfo.STATUS_IDLE) - .setCapabilities(builder.build()) - .build(); - } - } - } - - /** - * An instance of this class class is intended to be the first focusable - * in a layout to which the system automatically gives focus. It performs - * some voodoo to avoid the first tap on it to start an edit mode, rather - * to bring up the IME, i.e. to get the behavior as if the view was not - * focused. - */ - public static final class CustomEditText extends EditText { - private boolean mClickedBeforeFocus; - private CharSequence mError; - - public CustomEditText(Context context, AttributeSet attrs) { - super(context, attrs); - } - - @Override - public boolean performClick() { - super.performClick(); - if (isFocused() && !mClickedBeforeFocus) { - clearFocus(); - requestFocus(); - } - mClickedBeforeFocus = true; - return true; - } - - @Override - public CharSequence getError() { - return mError; - } - - @Override - public void setError(CharSequence error, Drawable icon) { - setCompoundDrawables(null, null, icon, null); - mError = error; - } - - protected void onFocusChanged(boolean gainFocus, int direction, - Rect previouslyFocusedRect) { - if (!gainFocus) { - mClickedBeforeFocus = false; - } - super.onFocusChanged(gainFocus, direction, previouslyFocusedRect); - } - } - - private static final class Document { - public PrintDocumentInfo info; - public PageRange[] pages; - } - - private static final class PageRangeUtils { - - private static final Comparator<PageRange> sComparator = new Comparator<PageRange>() { - @Override - public int compare(PageRange lhs, PageRange rhs) { - return lhs.getStart() - rhs.getStart(); - } - }; - - private PageRangeUtils() { - throw new UnsupportedOperationException(); - } - - public static boolean contains(PageRange[] ourRanges, PageRange[] otherRanges) { - if (ourRanges == null || otherRanges == null) { - return false; - } - - if (ourRanges.length == 1 - && PageRange.ALL_PAGES.equals(ourRanges[0])) { - return true; - } - - ourRanges = normalize(ourRanges); - otherRanges = normalize(otherRanges); - - // Note that the code below relies on the ranges being normalized - // which is they contain monotonically increasing non-intersecting - // subranges whose start is less that or equal to the end. - int otherRangeIdx = 0; - final int ourRangeCount = ourRanges.length; - final int otherRangeCount = otherRanges.length; - for (int ourRangeIdx = 0; ourRangeIdx < ourRangeCount; ourRangeIdx++) { - PageRange ourRange = ourRanges[ourRangeIdx]; - for (; otherRangeIdx < otherRangeCount; otherRangeIdx++) { - PageRange otherRange = otherRanges[otherRangeIdx]; - if (otherRange.getStart() > ourRange.getEnd()) { - break; - } - if (otherRange.getStart() < ourRange.getStart() - || otherRange.getEnd() > ourRange.getEnd()) { - return false; - } - } - } - if (otherRangeIdx < otherRangeCount) { - return false; - } - return true; - } - - public static PageRange[] normalize(PageRange[] pageRanges) { - if (pageRanges == null) { - return null; - } - final int oldRangeCount = pageRanges.length; - if (oldRangeCount <= 1) { - return pageRanges; - } - Arrays.sort(pageRanges, sComparator); - int newRangeCount = 1; - for (int i = 0; i < oldRangeCount - 1; i++) { - newRangeCount++; - PageRange currentRange = pageRanges[i]; - PageRange nextRange = pageRanges[i + 1]; - if (currentRange.getEnd() + 1 >= nextRange.getStart()) { - newRangeCount--; - pageRanges[i] = null; - pageRanges[i + 1] = new PageRange(currentRange.getStart(), - Math.max(currentRange.getEnd(), nextRange.getEnd())); - } - } - if (newRangeCount == oldRangeCount) { - return pageRanges; - } - return Arrays.copyOfRange(pageRanges, oldRangeCount - newRangeCount, - oldRangeCount); - } - - public static void offset(PageRange[] pageRanges, int offset) { - if (offset == 0) { - return; - } - final int pageRangeCount = pageRanges.length; - for (int i = 0; i < pageRangeCount; i++) { - final int start = pageRanges[i].getStart() + offset; - final int end = pageRanges[i].getEnd() + offset; - pageRanges[i] = new PageRange(start, end); - } - } - } - - private static final class AutoCancellingAnimator - implements OnAttachStateChangeListener, Runnable { - - private ViewPropertyAnimator mAnimator; - - private boolean mCancelled; - private Runnable mEndCallback; - - public static AutoCancellingAnimator animate(View view) { - ViewPropertyAnimator animator = view.animate(); - AutoCancellingAnimator cancellingWrapper = - new AutoCancellingAnimator(animator); - view.addOnAttachStateChangeListener(cancellingWrapper); - return cancellingWrapper; - } - - private AutoCancellingAnimator(ViewPropertyAnimator animator) { - mAnimator = animator; - } - - public AutoCancellingAnimator alpha(float alpha) { - mAnimator = mAnimator.alpha(alpha); - return this; - } - - public void cancel() { - mAnimator.cancel(); - } - - public AutoCancellingAnimator withLayer() { - mAnimator = mAnimator.withLayer(); - return this; - } - - public AutoCancellingAnimator withEndAction(Runnable callback) { - mEndCallback = callback; - mAnimator = mAnimator.withEndAction(this); - return this; - } - - public AutoCancellingAnimator scaleY(float scale) { - mAnimator = mAnimator.scaleY(scale); - return this; - } - - @Override - public void onViewAttachedToWindow(View v) { - /* do nothing */ - } - - @Override - public void onViewDetachedFromWindow(View v) { - cancel(); - } - - @Override - public void run() { - if (!mCancelled) { - mEndCallback.run(); - } - } - } - - private static final class PrintSpoolerProvider implements ServiceConnection { - private final Context mContext; - private final Runnable mCallback; - - private PrintSpoolerService mSpooler; - - public PrintSpoolerProvider(Context context, Runnable callback) { - mContext = context; - mCallback = callback; - Intent intent = new Intent(mContext, PrintSpoolerService.class); - mContext.bindService(intent, this, 0); - } - - public PrintSpoolerService getSpooler() { - return mSpooler; - } - - public void destroy() { - if (mSpooler != null) { - mContext.unbindService(this); - } - } - - @Override - public void onServiceConnected(ComponentName name, IBinder service) { - mSpooler = ((PrintSpoolerService.PrintSpooler) service).getService(); - if (mSpooler != null) { - mCallback.run(); - } - } - - @Override - public void onServiceDisconnected(ComponentName name) { - /* do noting - we are in the same process */ - } - } - - private static final class PrintDocumentAdapterObserver - extends IPrintDocumentAdapterObserver.Stub { - private final WeakReference<PrintJobConfigActivity> mWeakActvity; - - public PrintDocumentAdapterObserver(PrintJobConfigActivity activity) { - mWeakActvity = new WeakReference<PrintJobConfigActivity>(activity); - } - - @Override - public void onDestroy() { - final PrintJobConfigActivity activity = mWeakActvity.get(); - if (activity != null) { - activity.mController.mHandler.post(new Runnable() { - @Override - public void run() { - if (activity.mController != null) { - activity.mController.cancel(); - } - if (activity.mEditor != null) { - activity.mEditor.cancel(); - } - activity.finish(); - } - }); - } - } - } -} diff --git a/packages/PrintSpooler/src/com/android/printspooler/RemotePrintDocumentAdapter.java b/packages/PrintSpooler/src/com/android/printspooler/RemotePrintDocumentAdapter.java deleted file mode 100644 index d9ccb5d..0000000 --- a/packages/PrintSpooler/src/com/android/printspooler/RemotePrintDocumentAdapter.java +++ /dev/null @@ -1,151 +0,0 @@ -/* - * 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.AsyncTask; -import android.os.Bundle; -import android.os.ParcelFileDescriptor; -import android.os.RemoteException; -import android.print.ILayoutResultCallback; -import android.print.IPrintDocumentAdapter; -import android.print.IWriteResultCallback; -import android.print.PageRange; -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; - -/** - * This class represents a remote print document adapter instance. - */ -final class RemotePrintDocumentAdapter { - private static final String LOG_TAG = "RemotePrintDocumentAdapter"; - - private static final boolean DEBUG = false; - - private final IPrintDocumentAdapter mRemoteInterface; - - private final File mFile; - - public RemotePrintDocumentAdapter(IPrintDocumentAdapter printAdatper, File file) { - mRemoteInterface = printAdatper; - mFile = file; - } - - public void start() { - if (DEBUG) { - Log.i(LOG_TAG, "start()"); - } - try { - mRemoteInterface.start(); - } catch (RemoteException re) { - Log.e(LOG_TAG, "Error calling start()", re); - } - } - - public void layout(PrintAttributes oldAttributes, PrintAttributes newAttributes, - ILayoutResultCallback callback, Bundle metadata, int sequence) { - if (DEBUG) { - Log.i(LOG_TAG, "layout()"); - } - try { - mRemoteInterface.layout(oldAttributes, newAttributes, callback, metadata, sequence); - } catch (RemoteException re) { - Log.e(LOG_TAG, "Error calling layout()", re); - } - } - - public void write(final PageRange[] pages, final IWriteResultCallback callback, - final int sequence) { - if (DEBUG) { - Log.i(LOG_TAG, "write()"); - } - new AsyncTask<Void, Void, Void>() { - @Override - protected Void doInBackground(Void... params) { - InputStream in = null; - OutputStream out = null; - ParcelFileDescriptor source = null; - ParcelFileDescriptor sink = null; - 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.write(pages, sink, callback, sequence); - - // Close the source. It is now held by the client. - sink.close(); - sink = null; - - // Read the data. - final byte[] buffer = new byte[8192]; - while (true) { - final int readByteCount = in.read(buffer); - if (readByteCount < 0) { - break; - } - out.write(buffer, 0, readByteCount); - } - } catch (RemoteException re) { - Log.e(LOG_TAG, "Error calling write()", re); - } catch (IOException ioe) { - Log.e(LOG_TAG, "Error calling write()", ioe); - } finally { - IoUtils.closeQuietly(in); - IoUtils.closeQuietly(out); - IoUtils.closeQuietly(sink); - IoUtils.closeQuietly(source); - } - return null; - } - }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void[]) null); - } - - public void finish() { - if (DEBUG) { - Log.i(LOG_TAG, "finish()"); - } - try { - mRemoteInterface.finish(); - } catch (RemoteException re) { - Log.e(LOG_TAG, "Error calling finish()", re); - } - } - - public void cancel() { - if (DEBUG) { - Log.i(LOG_TAG, "cancel()"); - } - try { - mRemoteInterface.cancel(); - } catch (RemoteException re) { - Log.e(LOG_TAG, "Error calling cancel()", re); - } - } -} diff --git a/packages/PrintSpooler/src/com/android/printspooler/SelectPrinterActivity.java b/packages/PrintSpooler/src/com/android/printspooler/SelectPrinterActivity.java deleted file mode 100644 index 141dbd1..0000000 --- a/packages/PrintSpooler/src/com/android/printspooler/SelectPrinterActivity.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * 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.os.Bundle; -import android.print.PrinterId; - -import com.android.printspooler.SelectPrinterFragment.OnPrinterSelectedListener; - -public class SelectPrinterActivity extends Activity implements OnPrinterSelectedListener { - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.select_printer_activity); - } - - @Override - public void onPrinterSelected(PrinterId printer) { - Intent intent = new Intent(); - intent.putExtra(PrintJobConfigActivity.INTENT_EXTRA_PRINTER_ID, printer); - setResult(RESULT_OK, intent); - finish(); - } -} diff --git a/packages/PrintSpooler/src/com/android/printspooler/NotificationController.java b/packages/PrintSpooler/src/com/android/printspooler/model/NotificationController.java index 968a8bf..929f0fc 100644 --- a/packages/PrintSpooler/src/com/android/printspooler/NotificationController.java +++ b/packages/PrintSpooler/src/com/android/printspooler/model/NotificationController.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.printspooler; +package com.android.printspooler.model; import android.app.Notification; import android.app.Notification.InboxStyle; @@ -38,6 +38,8 @@ import android.print.PrintManager; import android.provider.Settings; import android.util.Log; +import com.android.printspooler.R; + import java.util.ArrayList; import java.util.List; @@ -45,7 +47,7 @@ import java.util.List; * This class is responsible for updating the print notifications * based on print job state transitions. */ -public class NotificationController { +final class NotificationController { public static final boolean DEBUG = false; public static final String LOG_TAG = "NotificationController"; diff --git a/packages/PrintSpooler/src/com/android/printspooler/model/PrintSpoolerProvider.java b/packages/PrintSpooler/src/com/android/printspooler/model/PrintSpoolerProvider.java new file mode 100644 index 0000000..06723c3 --- /dev/null +++ b/packages/PrintSpooler/src/com/android/printspooler/model/PrintSpoolerProvider.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2014 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.model; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.os.IBinder; + +public class PrintSpoolerProvider implements ServiceConnection { + private final Context mContext; + private final Runnable mCallback; + + private PrintSpoolerService mSpooler; + + public PrintSpoolerProvider(Context context, Runnable callback) { + mContext = context; + mCallback = callback; + Intent intent = new Intent(mContext, PrintSpoolerService.class); + mContext.bindService(intent, this, 0); + } + + public PrintSpoolerService getSpooler() { + return mSpooler; + } + + public void destroy() { + if (mSpooler != null) { + mContext.unbindService(this); + } + } + + @Override + public void onServiceConnected(ComponentName name, IBinder service) { + mSpooler = ((PrintSpoolerService.PrintSpooler) service).getService(); + if (mSpooler != null) { + mCallback.run(); + } + } + + @Override + public void onServiceDisconnected(ComponentName name) { + /* do nothing - we are in the same process */ + } +} diff --git a/packages/PrintSpooler/src/com/android/printspooler/PrintSpoolerService.java b/packages/PrintSpooler/src/com/android/printspooler/model/PrintSpoolerService.java index 615d667..045a2f9 100644 --- a/packages/PrintSpooler/src/com/android/printspooler/PrintSpoolerService.java +++ b/packages/PrintSpooler/src/com/android/printspooler/model/PrintSpoolerService.java @@ -14,10 +14,11 @@ * limitations under the License. */ -package com.android.printspooler; +package com.android.printspooler.model; import android.app.Service; import android.content.ComponentName; +import android.content.Context; import android.content.Intent; import android.os.AsyncTask; import android.os.Bundle; @@ -38,7 +39,6 @@ import android.print.PrintJobId; import android.print.PrintJobInfo; import android.print.PrintManager; import android.print.PrinterId; -import android.print.PrinterInfo; import android.text.TextUtils; import android.util.ArrayMap; import android.util.AtomicFile; @@ -48,6 +48,7 @@ import android.util.Xml; import com.android.internal.os.HandlerCaller; import com.android.internal.util.FastXmlSerializer; +import com.android.printspooler.R; import libcore.io.IoUtils; @@ -89,7 +90,7 @@ public final class PrintSpoolerService extends Service { private final Object mLock = new Object(); - private final List<PrintJobInfo> mPrintJobs = new ArrayList<PrintJobInfo>(); + private final List<PrintJobInfo> mPrintJobs = new ArrayList<>(); private static PrintSpoolerService sInstance; @@ -274,7 +275,7 @@ public final class PrintSpoolerService extends Service { && isScheduledState(printJob.getState())); if (sameComponent && sameAppId && sameState) { if (foundPrintJobs == null) { - foundPrintJobs = new ArrayList<PrintJobInfo>(); + foundPrintJobs = new ArrayList<>(); } foundPrintJobs.add(printJob); } @@ -400,7 +401,7 @@ public final class PrintSpoolerService extends Service { FileOutputStream out = null; try { if (printJob != null) { - File file = generateFileForPrintJob(printJobId); + File file = generateFileForPrintJob(PrintSpoolerService.this, printJobId); in = new FileInputStream(file); out = new FileOutputStream(fd.getFileDescriptor()); } @@ -427,8 +428,8 @@ public final class PrintSpoolerService extends Service { }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void[]) null); } - public File generateFileForPrintJob(PrintJobId printJobId) { - return new File(getFilesDir(), PRINT_JOB_FILE_PREFIX + public static File generateFileForPrintJob(Context context, PrintJobId printJobId) { + return new File(context.getFilesDir(), PRINT_JOB_FILE_PREFIX + printJobId.flattenToString() + "." + PRINT_FILE_EXTENSION); } @@ -461,7 +462,7 @@ public final class PrintSpoolerService extends Service { } private void removePrintJobFileLocked(PrintJobId printJobId) { - File file = generateFileForPrintJob(printJobId); + File file = generateFileForPrintJob(PrintSpoolerService.this, printJobId); if (file.exists()) { file.delete(); if (DEBUG_PRINT_JOB_LIFECYCLE) { @@ -557,7 +558,7 @@ public final class PrintSpoolerService extends Service { } private boolean isObsoleteState(int printJobState) { - return (isTeminalState(printJobState) + return (isTerminalState(printJobState) || printJobState == PrintJobInfo.STATE_QUEUED); } @@ -574,7 +575,7 @@ public final class PrintSpoolerService extends Service { || printJobState == PrintJobInfo.STATE_BLOCKED; } - private boolean isTeminalState(int printJobState) { + private boolean isTerminalState(int printJobState) { return printJobState == PrintJobInfo.STATE_COMPLETED || printJobState == PrintJobInfo.STATE_CANCELED; } @@ -619,61 +620,23 @@ public final class PrintSpoolerService extends Service { } } - public void setPrintJobCopiesNoPersistence(PrintJobId printJobId, int copies) { + public void updatePrintJobUserConfigurableOptionsNoPersistence(PrintJobInfo printJob) { synchronized (mLock) { - PrintJobInfo printJob = getPrintJobInfo(printJobId, PrintManager.APP_ID_ANY); - if (printJob != null) { - printJob.setCopies(copies); - } - } - } - - public void setPrintJobAdvancedOptionsNoPersistence(PrintJobId printJobId, - Bundle advancedOptions) { - synchronized (mLock) { - PrintJobInfo printJob = getPrintJobInfo(printJobId, PrintManager.APP_ID_ANY); - if (printJob != null) { - printJob.setAdvancedOptions(advancedOptions); - } - } - } - - public void setPrintJobPrintDocumentInfoNoPersistence(PrintJobId printJobId, - PrintDocumentInfo info) { - synchronized (mLock) { - PrintJobInfo printJob = getPrintJobInfo(printJobId, PrintManager.APP_ID_ANY); - if (printJob != null) { - printJob.setDocumentInfo(info); - } - } - } - - public void setPrintJobAttributesNoPersistence(PrintJobId printJobId, - PrintAttributes attributes) { - synchronized (mLock) { - PrintJobInfo printJob = getPrintJobInfo(printJobId, PrintManager.APP_ID_ANY); - if (printJob != null) { - printJob.setAttributes(attributes); - } - } - } - - public void setPrintJobPrinterNoPersistence(PrintJobId printJobId, PrinterInfo printer) { - synchronized (mLock) { - PrintJobInfo printJob = getPrintJobInfo(printJobId, PrintManager.APP_ID_ANY); - if (printJob != null) { - printJob.setPrinterId(printer.getId()); - printJob.setPrinterName(printer.getName()); - } - } - } - - public void setPrintJobPagesNoPersistence(PrintJobId printJobId, PageRange[] pages) { - synchronized (mLock) { - PrintJobInfo printJob = getPrintJobInfo(printJobId, PrintManager.APP_ID_ANY); - if (printJob != null) { - printJob.setPages(pages); + final int printJobCount = mPrintJobs.size(); + for (int i = 0; i < printJobCount; i++) { + PrintJobInfo cachedPrintJob = mPrintJobs.get(i); + if (cachedPrintJob.getId().equals(printJob.getId())) { + cachedPrintJob.setPrinterId(printJob.getPrinterId()); + cachedPrintJob.setPrinterName(printJob.getPrinterName()); + cachedPrintJob.setCopies(printJob.getCopies()); + cachedPrintJob.setDocumentInfo(printJob.getDocumentInfo()); + cachedPrintJob.setPages(printJob.getPages()); + cachedPrintJob.setAttributes(printJob.getAttributes()); + cachedPrintJob.setAdvancedOptions(printJob.getAdvancedOptions()); + return; + } } + throw new IllegalArgumentException("No print job with id:" + printJob.getId()); } } @@ -1250,7 +1213,7 @@ public final class PrintSpoolerService extends Service { } } - final class PrintSpooler extends IPrintSpooler.Stub { + public final class PrintSpooler extends IPrintSpooler.Stub { @Override public void getPrintJobInfos(IPrintSpoolerCallbacks callback, ComponentName componentName, int state, int appId, int sequence) diff --git a/packages/PrintSpooler/src/com/android/printspooler/model/RemotePrintDocument.java b/packages/PrintSpooler/src/com/android/printspooler/model/RemotePrintDocument.java new file mode 100644 index 0000000..e70c361 --- /dev/null +++ b/packages/PrintSpooler/src/com/android/printspooler/model/RemotePrintDocument.java @@ -0,0 +1,1148 @@ +/* + * Copyright (C) 2014 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.model; + +import android.content.ContentResolver; +import android.content.Context; +import android.net.Uri; +import android.os.AsyncTask; +import android.os.Bundle; +import android.os.Handler; +import android.os.IBinder.DeathRecipient; +import android.os.ICancellationSignal; +import android.os.Looper; +import android.os.Message; +import android.os.ParcelFileDescriptor; +import android.os.RemoteException; +import android.print.ILayoutResultCallback; +import android.print.IPrintDocumentAdapter; +import android.print.IPrintDocumentAdapterObserver; +import android.print.IWriteResultCallback; +import android.print.PageRange; +import android.print.PrintAttributes; +import android.print.PrintDocumentAdapter; +import android.print.PrintDocumentInfo; +import android.util.Log; + +import com.android.printspooler.R; +import com.android.printspooler.util.PageRangeUtils; + +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.lang.ref.WeakReference; +import java.util.Arrays; + +public final class RemotePrintDocument { + private static final String LOG_TAG = "RemotePrintDocument"; + + private static final boolean DEBUG = false; + + private static final int STATE_INITIAL = 0; + private static final int STATE_STARTED = 1; + private static final int STATE_UPDATING = 2; + private static final int STATE_UPDATED = 3; + private static final int STATE_FAILED = 4; + private static final int STATE_FINISHED = 5; + private static final int STATE_CANCELING = 6; + private static final int STATE_CANCELED = 7; + private static final int STATE_DESTROYED = 8; + + private static final PageRange[] ALL_PAGES_ARRAY = new PageRange[] { + PageRange.ALL_PAGES + }; + + private final Context mContext; + + private final RemotePrintDocumentInfo mDocumentInfo; + private final UpdateSpec mUpdateSpec = new UpdateSpec(); + + private final Looper mLooper; + private final IPrintDocumentAdapter mPrintDocumentAdapter; + private final DocumentObserver mDocumentObserver; + + private final UpdateResultCallbacks mUpdateCallbacks; + + private final CommandDoneCallback mCommandResultCallback = + new CommandDoneCallback() { + @Override + public void onDone() { + if (mCurrentCommand.isCompleted()) { + if (mCurrentCommand instanceof LayoutCommand) { + // If there is a next command after a layout is done, then another + // update was issued and the next command is another layout, so we + // do nothing. However, if there is no next command we may need to + // ask for some pages given we do not already have them or we do + // but the content has changed. + LayoutCommand layoutCommand = (LayoutCommand) mCurrentCommand; + if (mNextCommand == null) { + if (layoutCommand.isDocumentChanged() || !PageRangeUtils.contains( + mDocumentInfo.writtenPages, mUpdateSpec.pages)) { + mNextCommand = new WriteCommand(mContext, mLooper, + mPrintDocumentAdapter, mDocumentInfo, + mDocumentInfo.info.getPageCount(), mUpdateSpec.pages, + mDocumentInfo.file, mCommandResultCallback); + } else { + // If we have the requested pages just update that ones to be printed. + mDocumentInfo.printedPages = computePrintedPages(mUpdateSpec.pages, + mDocumentInfo.writtenPages, mDocumentInfo.info.getPageCount()); + // Notify we are done. + notifyUpdateCompleted(); + } + } + } else { + // We always notify after a write. + notifyUpdateCompleted(); + } + runPendingCommand(); + } else if (mCurrentCommand.isFailed()) { + mState = STATE_FAILED; + CharSequence error = mCurrentCommand.getError(); + mCurrentCommand = null; + mNextCommand = null; + mUpdateSpec.reset(); + notifyUpdateFailed(error); + } else if (mCurrentCommand.isCanceled()) { + if (mState == STATE_CANCELING) { + mState = STATE_CANCELED; + notifyUpdateCanceled(); + } + runPendingCommand(); + } + } + }; + + private final DeathRecipient mDeathRecipient = new DeathRecipient() { + @Override + public void binderDied() { + finish(); + } + }; + + private int mState = STATE_INITIAL; + + private AsyncCommand mCurrentCommand; + private AsyncCommand mNextCommand; + + public interface DocumentObserver { + public void onDestroy(); + } + + public interface UpdateResultCallbacks { + public void onUpdateCompleted(RemotePrintDocumentInfo document); + public void onUpdateCanceled(); + public void onUpdateFailed(CharSequence error); + } + + public RemotePrintDocument(Context context, IPrintDocumentAdapter adapter, + File file, DocumentObserver destroyListener, UpdateResultCallbacks callbacks) { + mPrintDocumentAdapter = adapter; + mLooper = context.getMainLooper(); + mContext = context; + mDocumentObserver = destroyListener; + mDocumentInfo = new RemotePrintDocumentInfo(); + mDocumentInfo.file = file; + mUpdateCallbacks = callbacks; + connectToRemoteDocument(); + } + + public void start() { + if (DEBUG) { + Log.i(LOG_TAG, "[CALLED] start()"); + } + if (mState != STATE_INITIAL) { + throw new IllegalStateException("Cannot start in state:" + stateToString(mState)); + } + try { + mPrintDocumentAdapter.start(); + mState = STATE_STARTED; + } catch (RemoteException re) { + Log.e(LOG_TAG, "Error calling start()", re); + mState = STATE_FAILED; + mDocumentObserver.onDestroy(); + } + } + + public boolean update(PrintAttributes attributes, PageRange[] pages, boolean preview) { + boolean willUpdate; + + if (DEBUG) { + Log.i(LOG_TAG, "[CALLED] update()"); + } + + if (hasUpdateError()) { + throw new IllegalStateException("Cannot update without a clearing the failure"); + } + + if (mState == STATE_INITIAL || mState == STATE_FINISHED || mState == STATE_DESTROYED) { + throw new IllegalStateException("Cannot update in state:" + stateToString(mState)); + } + + // We schedule a layout if the constraints changed. + if (!mUpdateSpec.hasSameConstraints(attributes, preview)) { + willUpdate = true; + + // If there is a current command that is running we ask for a + // cancellation and start over. + if (mCurrentCommand != null && (mCurrentCommand.isRunning() + || mCurrentCommand.isPending())) { + mCurrentCommand.cancel(); + } + + // Schedule a layout command. + PrintAttributes oldAttributes = mDocumentInfo.attributes != null + ? mDocumentInfo.attributes : new PrintAttributes.Builder().build(); + AsyncCommand command = new LayoutCommand(mLooper, mPrintDocumentAdapter, + mDocumentInfo, oldAttributes, attributes, preview, mCommandResultCallback); + scheduleCommand(command); + + mState = STATE_UPDATING; + // If no layout in progress and we don't have all pages - schedule a write. + } else if ((!(mCurrentCommand instanceof LayoutCommand) + || (!mCurrentCommand.isPending() && !mCurrentCommand.isRunning())) + && !PageRangeUtils.contains(mUpdateSpec.pages, pages)) { + willUpdate = true; + + // Cancel the current write as a new one is to be scheduled. + if (mCurrentCommand instanceof WriteCommand + && (mCurrentCommand.isPending() || mCurrentCommand.isRunning())) { + mCurrentCommand.cancel(); + } + + // Schedule a write command. + AsyncCommand command = new WriteCommand(mContext, mLooper, mPrintDocumentAdapter, + mDocumentInfo, mDocumentInfo.info.getPageCount(), pages, + mDocumentInfo.file, mCommandResultCallback); + scheduleCommand(command); + + mState = STATE_UPDATING; + } else { + willUpdate = false; + if (DEBUG) { + Log.i(LOG_TAG, "[SKIPPING] No update needed"); + } + } + + // Keep track of what is requested. + mUpdateSpec.update(attributes, preview, pages); + + runPendingCommand(); + + return willUpdate; + } + + public void finish() { + if (DEBUG) { + Log.i(LOG_TAG, "[CALLED] finish()"); + } + if (mState != STATE_STARTED && mState != STATE_UPDATED + && mState != STATE_FAILED && mState != STATE_CANCELING) { + throw new IllegalStateException("Cannot finish in state:" + + stateToString(mState)); + } + try { + mPrintDocumentAdapter.finish(); + mState = STATE_FINISHED; + } catch (RemoteException re) { + Log.e(LOG_TAG, "Error calling finish()", re); + mState = STATE_FAILED; + mDocumentObserver.onDestroy(); + } + } + + public void cancel() { + if (DEBUG) { + Log.i(LOG_TAG, "[CALLED] cancel()"); + } + + if (mState == STATE_CANCELING) { + return; + } + + if (mState != STATE_UPDATING) { + throw new IllegalStateException("Cannot cancel in state:" + stateToString(mState)); + } + + mState = STATE_CANCELING; + + mCurrentCommand.cancel(); + } + + public void destroy() { + if (DEBUG) { + Log.i(LOG_TAG, "[CALLED] destroy()"); + } + if (mState == STATE_DESTROYED) { + throw new IllegalStateException("Cannot destroy in state:" + stateToString(mState)); + } + + mState = STATE_DESTROYED; + + disconnectFromRemoteDocument(); + mDocumentObserver.onDestroy(); + } + + public boolean isUpdating() { + return mState == STATE_UPDATING || mState == STATE_CANCELING; + } + + public boolean isDestroyed() { + return mState == STATE_DESTROYED; + } + + public boolean hasUpdateError() { + return mState == STATE_FAILED; + } + + public void clearUpdateError() { + if (!hasUpdateError()) { + throw new IllegalStateException("No update error to clear"); + } + mState = STATE_STARTED; + } + + public RemotePrintDocumentInfo getDocumentInfo() { + return mDocumentInfo; + } + + public void writeContent(ContentResolver contentResolver, Uri uri) { + InputStream in = null; + OutputStream out = null; + try { + in = new FileInputStream(mDocumentInfo.file); + out = contentResolver.openOutputStream(uri); + final byte[] buffer = new byte[8192]; + while (true) { + final int readByteCount = in.read(buffer); + if (readByteCount < 0) { + break; + } + out.write(buffer, 0, readByteCount); + } + } catch (IOException e) { + Log.e(LOG_TAG, "Error writing document content.", e); + } finally { + IoUtils.closeQuietly(in); + IoUtils.closeQuietly(out); + } + } + + private void notifyUpdateCanceled() { + if (DEBUG) { + Log.i(LOG_TAG, "[CALLING] onUpdateCanceled()"); + } + mUpdateCallbacks.onUpdateCanceled(); + } + + private void notifyUpdateCompleted() { + if (DEBUG) { + Log.i(LOG_TAG, "[CALLING] onUpdateCompleted()"); + } + mUpdateCallbacks.onUpdateCompleted(mDocumentInfo); + } + + private void notifyUpdateFailed(CharSequence error) { + if (DEBUG) { + Log.i(LOG_TAG, "[CALLING] onUpdateCompleted()"); + } + mUpdateCallbacks.onUpdateFailed(error); + } + + private void connectToRemoteDocument() { + try { + mPrintDocumentAdapter.asBinder().linkToDeath(mDeathRecipient, 0); + } catch (RemoteException re) { + Log.w(LOG_TAG, "The printing process is dead."); + destroy(); + return; + } + + try { + mPrintDocumentAdapter.setObserver(new PrintDocumentAdapterObserver(this)); + } catch (RemoteException re) { + Log.w(LOG_TAG, "Error setting observer to the print adapter."); + destroy(); + } + } + + private void disconnectFromRemoteDocument() { + try { + mPrintDocumentAdapter.setObserver(null); + } catch (RemoteException re) { + Log.w(LOG_TAG, "Error setting observer to the print adapter."); + // Keep going - best effort... + } + + mPrintDocumentAdapter.asBinder().unlinkToDeath(mDeathRecipient, 0); + } + + private void scheduleCommand(AsyncCommand command) { + if (mCurrentCommand == null) { + mCurrentCommand = command; + } else { + mNextCommand = command; + } + } + + private void runPendingCommand() { + if (mCurrentCommand != null + && (mCurrentCommand.isCompleted() + || mCurrentCommand.isCanceled())) { + mCurrentCommand = mNextCommand; + mNextCommand = null; + } + + if (mCurrentCommand != null) { + if (mCurrentCommand.isPending()) { + mCurrentCommand.run(); + } + mState = STATE_UPDATING; + } else { + mState = STATE_UPDATED; + } + } + + private static String stateToString(int state) { + switch (state) { + case STATE_FINISHED: { + return "STATE_FINISHED"; + } + case STATE_FAILED: { + return "STATE_FAILED"; + } + case STATE_STARTED: { + return "STATE_STARTED"; + } + case STATE_UPDATING: { + return "STATE_UPDATING"; + } + case STATE_UPDATED: { + return "STATE_UPDATED"; + } + case STATE_CANCELING: { + return "STATE_CANCELING"; + } + case STATE_CANCELED: { + return "STATE_CANCELED"; + } + case STATE_DESTROYED: { + return "STATE_DESTROYED"; + } + default: { + return "STATE_UNKNOWN"; + } + } + } + + private static PageRange[] computePrintedPages(PageRange[] requestedPages, + PageRange[] writtenPages, int pageCount) { + // Adjust the print job pages based on what was requested and written. + // The cases are ordered in the most expected to the least expected. + if (Arrays.equals(writtenPages, requestedPages)) { + // We got a document with exactly the pages we wanted. Hence, + // the printer has to print all pages in the data. + return ALL_PAGES_ARRAY; + } else if (Arrays.equals(writtenPages, ALL_PAGES_ARRAY)) { + // We requested specific pages but got all of them. Hence, + // the printer has to print only the requested pages. + return requestedPages; + } else if (PageRangeUtils.contains(writtenPages, requestedPages)) { + // We requested specific pages and got more but not all pages. + // Hence, we have to offset appropriately the printed pages to + // be based off the start of the written ones instead of zero. + // The written pages are always non-null and not empty. + final int offset = -writtenPages[0].getStart(); + PageRangeUtils.offset(requestedPages, offset); + return requestedPages; + } else if (Arrays.equals(requestedPages, ALL_PAGES_ARRAY) + && isAllPages(writtenPages, pageCount)) { + // We requested all pages via the special constant and got all + // of them as an explicit enumeration. Hence, the printer has + // to print only the requested pages. + return ALL_PAGES_ARRAY; + } + + return null; + } + + private static boolean isAllPages(PageRange[] pageRanges, int pageCount) { + return pageRanges.length > 0 && pageRanges[0].getStart() == 0 + && pageRanges[pageRanges.length - 1].getEnd() == pageCount - 1; + } + + static final class UpdateSpec { + final PrintAttributes attributes = new PrintAttributes.Builder().build(); + boolean preview; + PageRange[] pages; + + public void update(PrintAttributes attributes, boolean preview, + PageRange[] pages) { + this.attributes.copyFrom(attributes); + this.preview = preview; + this.pages = Arrays.copyOf(pages, pages.length); + } + + public void reset() { + attributes.clear(); + preview = false; + pages = null; + } + + public boolean hasSameConstraints(PrintAttributes attributes, boolean preview) { + return this.attributes.equals(attributes) && this.preview == preview; + } + } + + public static final class RemotePrintDocumentInfo { + public PrintAttributes attributes; + public Bundle metadata; + public PrintDocumentInfo info; + public PageRange[] printedPages; + public PageRange[] writtenPages; + public File file; + } + + private interface CommandDoneCallback { + public void onDone(); + } + + private static abstract class AsyncCommand implements Runnable { + private static final int STATE_PENDING = 0; + private static final int STATE_RUNNING = 1; + private static final int STATE_COMPLETED = 2; + private static final int STATE_CANCELED = 3; + private static final int STATE_CANCELING = 4; + private static final int STATE_FAILED = 5; + + private static int sSequenceCounter; + + protected final int mSequence = sSequenceCounter++; + protected final IPrintDocumentAdapter mAdapter; + protected final RemotePrintDocumentInfo mDocument; + + protected final CommandDoneCallback mDoneCallback; + + protected ICancellationSignal mCancellation; + + private CharSequence mError; + + private int mState = STATE_PENDING; + + public AsyncCommand(IPrintDocumentAdapter adapter, RemotePrintDocumentInfo document, + CommandDoneCallback doneCallback) { + mAdapter = adapter; + mDocument = document; + mDoneCallback = doneCallback; + } + + protected final boolean isCanceling() { + return mState == STATE_CANCELING; + } + + public final boolean isCanceled() { + return mState == STATE_CANCELED; + } + + public final void cancel() { + if (isRunning()) { + canceling(); + if (mCancellation != null) { + try { + mCancellation.cancel(); + } catch (RemoteException re) { + Log.w(LOG_TAG, "Error while canceling", re); + } + } + } else { + canceled(); + + // Done. + mDoneCallback.onDone(); + } + } + + protected final void canceling() { + if (mState != STATE_PENDING && mState != STATE_RUNNING) { + throw new IllegalStateException("Command not pending or running."); + } + mState = STATE_CANCELING; + } + + protected final void canceled() { + if (mState != STATE_CANCELING) { + throw new IllegalStateException("Not canceling."); + } + mState = STATE_CANCELED; + } + + public final boolean isPending() { + return mState == STATE_PENDING; + } + + protected final void running() { + if (mState != STATE_PENDING) { + throw new IllegalStateException("Not pending."); + } + mState = STATE_RUNNING; + } + + public final boolean isRunning() { + return mState == STATE_RUNNING; + } + + protected final void completed() { + if (mState != STATE_RUNNING && mState != STATE_CANCELING) { + throw new IllegalStateException("Not running."); + } + mState = STATE_COMPLETED; + } + + public final boolean isCompleted() { + return mState == STATE_COMPLETED; + } + + protected final void failed(CharSequence error) { + if (mState != STATE_RUNNING) { + throw new IllegalStateException("Not running."); + } + mState = STATE_FAILED; + + mError = error; + } + + public final boolean isFailed() { + return mState == STATE_FAILED; + } + + public CharSequence getError() { + return mError; + } + } + + private static final class LayoutCommand extends AsyncCommand { + private final PrintAttributes mOldAttributes = new PrintAttributes.Builder().build(); + private final PrintAttributes mNewAttributes = new PrintAttributes.Builder().build(); + private final Bundle mMetadata = new Bundle(); + + private final ILayoutResultCallback mRemoteResultCallback; + + private final Handler mHandler; + + private boolean mDocumentChanged; + + public LayoutCommand(Looper looper, IPrintDocumentAdapter adapter, + RemotePrintDocumentInfo document, PrintAttributes oldAttributes, + PrintAttributes newAttributes, boolean preview, CommandDoneCallback callback) { + super(adapter, document, callback); + mHandler = new LayoutHandler(looper); + mRemoteResultCallback = new LayoutResultCallback(mHandler); + mOldAttributes.copyFrom(oldAttributes); + mNewAttributes.copyFrom(newAttributes); + mMetadata.putBoolean(PrintDocumentAdapter.EXTRA_PRINT_PREVIEW, preview); + } + + public boolean isDocumentChanged() { + return mDocumentChanged; + } + + @Override + public void run() { + running(); + + try { + if (DEBUG) { + Log.i(LOG_TAG, "[PERFORMING] layout"); + } + mAdapter.layout(mOldAttributes, mNewAttributes, mRemoteResultCallback, + mMetadata, mSequence); + } catch (RemoteException re) { + Log.e(LOG_TAG, "Error calling layout", re); + handleOnLayoutFailed(null, mSequence); + } + } + + private void handleOnLayoutStarted(ICancellationSignal cancellation, int sequence) { + if (sequence != mSequence) { + return; + } + + if (DEBUG) { + Log.i(LOG_TAG, "[CALLBACK] onLayoutStarted"); + } + + if (isCanceling()) { + try { + cancellation.cancel(); + } catch (RemoteException re) { + Log.e(LOG_TAG, "Error cancelling", re); + handleOnLayoutFailed(null, mSequence); + } + } else { + mCancellation = cancellation; + } + } + + private void handleOnLayoutFinished(PrintDocumentInfo info, + boolean changed, int sequence) { + if (sequence != mSequence) { + return; + } + + if (DEBUG) { + Log.i(LOG_TAG, "[CALLBACK] onLayoutFinished"); + } + + completed(); + + // If the document description changed or the content in the + // document changed, the we need to invalidate the pages. + if (changed || !equalsIgnoreSize(mDocument.info, info)) { + // If the content changed we throw away all pages as + // we will request them again with the new content. + mDocument.writtenPages = null; + mDocument.printedPages = null; + mDocumentChanged = true; + } + + // Update the document with data from the layout pass. + mDocument.attributes = mNewAttributes; + mDocument.metadata = mMetadata; + mDocument.info = info; + + // Release the remote cancellation interface. + mCancellation = null; + + // Done. + mDoneCallback.onDone(); + } + + private void handleOnLayoutFailed(CharSequence error, int sequence) { + if (sequence != mSequence) { + return; + } + + if (DEBUG) { + Log.i(LOG_TAG, "[CALLBACK] onLayoutFailed"); + } + + failed(error); + + // Release the remote cancellation interface. + mCancellation = null; + + // Failed. + mDoneCallback.onDone(); + } + + private void handleOnLayoutCanceled(int sequence) { + if (sequence != mSequence) { + return; + } + + if (DEBUG) { + Log.i(LOG_TAG, "[CALLBACK] onLayoutCanceled"); + } + + canceled(); + + // Release the remote cancellation interface. + mCancellation = null; + + // Done. + mDoneCallback.onDone(); + } + + private boolean equalsIgnoreSize(PrintDocumentInfo lhs, PrintDocumentInfo rhs) { + if (lhs == rhs) { + return true; + } + if (lhs == null) { + return false; + } else { + if (rhs == null) { + return false; + } + if (lhs.getContentType() != rhs.getContentType() + || lhs.getPageCount() != rhs.getPageCount()) { + return false; + } + } + return true; + } + + private final class LayoutHandler extends Handler { + public static final int MSG_ON_LAYOUT_STARTED = 1; + public static final int MSG_ON_LAYOUT_FINISHED = 2; + public static final int MSG_ON_LAYOUT_FAILED = 3; + public static final int MSG_ON_LAYOUT_CANCELED = 4; + + public LayoutHandler(Looper looper) { + super(looper, null, false); + } + + @Override + public void handleMessage(Message message) { + switch (message.what) { + case MSG_ON_LAYOUT_STARTED: { + ICancellationSignal cancellation = (ICancellationSignal) message.obj; + final int sequence = message.arg1; + handleOnLayoutStarted(cancellation, sequence); + } break; + + case MSG_ON_LAYOUT_FINISHED: { + PrintDocumentInfo info = (PrintDocumentInfo) message.obj; + final boolean changed = (message.arg1 == 1); + final int sequence = message.arg2; + handleOnLayoutFinished(info, changed, sequence); + } break; + + case MSG_ON_LAYOUT_FAILED: { + CharSequence error = (CharSequence) message.obj; + final int sequence = message.arg1; + handleOnLayoutFailed(error, sequence); + } break; + + case MSG_ON_LAYOUT_CANCELED: { + final int sequence = message.arg1; + handleOnLayoutCanceled(sequence); + } break; + } + } + } + + private static final class LayoutResultCallback extends ILayoutResultCallback.Stub { + private final WeakReference<Handler> mWeakHandler; + + public LayoutResultCallback(Handler handler) { + mWeakHandler = new WeakReference<>(handler); + } + + @Override + public void onLayoutStarted(ICancellationSignal cancellation, int sequence) { + Handler handler = mWeakHandler.get(); + if (handler != null) { + handler.obtainMessage(LayoutHandler.MSG_ON_LAYOUT_STARTED, + sequence, 0, cancellation).sendToTarget(); + } + } + + @Override + public void onLayoutFinished(PrintDocumentInfo info, boolean changed, int sequence) { + Handler handler = mWeakHandler.get(); + if (handler != null) { + handler.obtainMessage(LayoutHandler.MSG_ON_LAYOUT_FINISHED, + changed ? 1 : 0, sequence, info).sendToTarget(); + } + } + + @Override + public void onLayoutFailed(CharSequence error, int sequence) { + Handler handler = mWeakHandler.get(); + if (handler != null) { + handler.obtainMessage(LayoutHandler.MSG_ON_LAYOUT_FAILED, + sequence, 0, error).sendToTarget(); + } + } + + @Override + public void onLayoutCanceled(int sequence) { + Handler handler = mWeakHandler.get(); + if (handler != null) { + handler.obtainMessage(LayoutHandler.MSG_ON_LAYOUT_CANCELED, + sequence, 0).sendToTarget(); + } + } + } + } + + private static final class WriteCommand extends AsyncCommand { + private final int mPageCount; + private final PageRange[] mPages; + private final File mContentFile; + + private final IWriteResultCallback mRemoteResultCallback; + private final CommandDoneCallback mDoneCallback; + + private final Context mContext; + private final Handler mHandler; + + public WriteCommand(Context context, Looper looper, IPrintDocumentAdapter adapter, + RemotePrintDocumentInfo document, int pageCount, PageRange[] pages, + File contentFile, CommandDoneCallback callback) { + super(adapter, document, callback); + mContext = context; + mHandler = new WriteHandler(looper); + mRemoteResultCallback = new WriteResultCallback(mHandler); + mPageCount = pageCount; + mPages = Arrays.copyOf(pages, pages.length); + mContentFile = contentFile; + mDoneCallback = callback; + } + + @Override + public void run() { + running(); + + // This is a long running operation as we will be reading fully + // the written data. In case of a cancellation, we ask the client + // to stop writing data and close the file descriptor after + // which we will reach the end of the stream, thus stop reading. + new AsyncTask<Void, Void, Void>() { + @Override + protected Void doInBackground(Void... params) { + InputStream in = null; + OutputStream out = null; + ParcelFileDescriptor source = null; + ParcelFileDescriptor sink = null; + try { + ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createPipe(); + source = pipe[0]; + sink = pipe[1]; + + in = new FileInputStream(source.getFileDescriptor()); + out = new FileOutputStream(mContentFile); + + // Async call to initiate the other process writing the data. + if (DEBUG) { + Log.i(LOG_TAG, "[PERFORMING] write"); + } + mAdapter.write(mPages, sink, mRemoteResultCallback, mSequence); + + // Close the source. It is now held by the client. + sink.close(); + sink = null; + + // Read the data. + final byte[] buffer = new byte[8192]; + while (true) { + final int readByteCount = in.read(buffer); + if (readByteCount < 0) { + break; + } + out.write(buffer, 0, readByteCount); + } + } catch (RemoteException | IOException e) { + Log.e(LOG_TAG, "Error calling write()", e); + } finally { + IoUtils.closeQuietly(in); + IoUtils.closeQuietly(out); + IoUtils.closeQuietly(sink); + IoUtils.closeQuietly(source); + } + return null; + } + }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void[]) null); + } + + private void handleOnWriteStarted(ICancellationSignal cancellation, int sequence) { + if (sequence != mSequence) { + return; + } + + if (DEBUG) { + Log.i(LOG_TAG, "[CALLBACK] onWriteStarted"); + } + + if (isCanceling()) { + try { + cancellation.cancel(); + } catch (RemoteException re) { + Log.e(LOG_TAG, "Error cancelling", re); + handleOnWriteFailed(null, sequence); + } + } else { + mCancellation = cancellation; + } + } + + private void handleOnWriteFinished(PageRange[] pages, int sequence) { + if (sequence != mSequence) { + return; + } + + if (DEBUG) { + Log.i(LOG_TAG, "[CALLBACK] onWriteFinished"); + } + + PageRange[] writtenPages = PageRangeUtils.normalize(pages); + PageRange[] printedPages = computePrintedPages(mPages, writtenPages, mPageCount); + + // Handle if we got invalid pages + if (printedPages != null) { + mDocument.writtenPages = writtenPages; + mDocument.printedPages = printedPages; + completed(); + } else { + mDocument.writtenPages = null; + mDocument.printedPages = null; + failed(mContext.getString(R.string.print_error_default_message)); + } + + // Release the remote cancellation interface. + mCancellation = null; + + // Done. + mDoneCallback.onDone(); + } + + private void handleOnWriteFailed(CharSequence error, int sequence) { + if (sequence != mSequence) { + return; + } + + if (DEBUG) { + Log.i(LOG_TAG, "[CALLBACK] onWriteFailed"); + } + + failed(error); + + // Release the remote cancellation interface. + mCancellation = null; + + // Done. + mDoneCallback.onDone(); + } + + private void handleOnWriteCanceled(int sequence) { + if (sequence != mSequence) { + return; + } + + if (DEBUG) { + Log.i(LOG_TAG, "[CALLBACK] onWriteCanceled"); + } + + canceled(); + + // Release the remote cancellation interface. + mCancellation = null; + + // Done. + mDoneCallback.onDone(); + } + + private final class WriteHandler extends Handler { + public static final int MSG_ON_WRITE_STARTED = 1; + public static final int MSG_ON_WRITE_FINISHED = 2; + public static final int MSG_ON_WRITE_FAILED = 3; + public static final int MSG_ON_WRITE_CANCELED = 4; + + public WriteHandler(Looper looper) { + super(looper, null, false); + } + + @Override + public void handleMessage(Message message) { + switch (message.what) { + case MSG_ON_WRITE_STARTED: { + ICancellationSignal cancellation = (ICancellationSignal) message.obj; + final int sequence = message.arg1; + handleOnWriteStarted(cancellation, sequence); + } break; + + case MSG_ON_WRITE_FINISHED: { + PageRange[] pages = (PageRange[]) message.obj; + final int sequence = message.arg1; + handleOnWriteFinished(pages, sequence); + } break; + + case MSG_ON_WRITE_FAILED: { + CharSequence error = (CharSequence) message.obj; + final int sequence = message.arg1; + handleOnWriteFailed(error, sequence); + } break; + + case MSG_ON_WRITE_CANCELED: { + final int sequence = message.arg1; + handleOnWriteCanceled(sequence); + } break; + } + } + } + + private static final class WriteResultCallback extends IWriteResultCallback.Stub { + private final WeakReference<Handler> mWeakHandler; + + public WriteResultCallback(Handler handler) { + mWeakHandler = new WeakReference<>(handler); + } + + @Override + public void onWriteStarted(ICancellationSignal cancellation, int sequence) { + Handler handler = mWeakHandler.get(); + if (handler != null) { + handler.obtainMessage(WriteHandler.MSG_ON_WRITE_STARTED, + sequence, 0, cancellation).sendToTarget(); + } + } + + @Override + public void onWriteFinished(PageRange[] pages, int sequence) { + Handler handler = mWeakHandler.get(); + if (handler != null) { + handler.obtainMessage(WriteHandler.MSG_ON_WRITE_FINISHED, + sequence, 0, pages).sendToTarget(); + } + } + + @Override + public void onWriteFailed(CharSequence error, int sequence) { + Handler handler = mWeakHandler.get(); + if (handler != null) { + handler.obtainMessage(WriteHandler.MSG_ON_WRITE_FAILED, + sequence, 0, error).sendToTarget(); + } + } + + @Override + public void onWriteCanceled(int sequence) { + Handler handler = mWeakHandler.get(); + if (handler != null) { + handler.obtainMessage(WriteHandler.MSG_ON_WRITE_CANCELED, + sequence, 0).sendToTarget(); + } + } + } + } + + private static final class PrintDocumentAdapterObserver + extends IPrintDocumentAdapterObserver.Stub { + private final WeakReference<RemotePrintDocument> mWeakDocument; + + public PrintDocumentAdapterObserver(RemotePrintDocument document) { + mWeakDocument = new WeakReference<>(document); + } + + @Override + public void onDestroy() { + final RemotePrintDocument document = mWeakDocument.get(); + if (document != null) { + new Handler(document.mLooper).post(new Runnable() { + @Override + public void run() { + document.mDocumentObserver.onDestroy(); + } + }); + } + } + } +} diff --git a/packages/PrintSpooler/src/com/android/printspooler/FusedPrintersProvider.java b/packages/PrintSpooler/src/com/android/printspooler/ui/FusedPrintersProvider.java index 9831839..d802cd8 100644 --- a/packages/PrintSpooler/src/com/android/printspooler/FusedPrintersProvider.java +++ b/packages/PrintSpooler/src/com/android/printspooler/ui/FusedPrintersProvider.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.printspooler; +package com.android.printspooler.ui; import android.content.ComponentName; import android.content.Context; @@ -57,7 +57,7 @@ import libcore.io.IoUtils; * This class is responsible for loading printers by doing discovery * and merging the discovered printers with the previously used ones. */ -public class FusedPrintersProvider extends Loader<List<PrinterInfo>> { +public final class FusedPrintersProvider extends Loader<List<PrinterInfo>> { private static final String LOG_TAG = "FusedPrintersProvider"; private static final boolean DEBUG = false; @@ -192,7 +192,7 @@ public class FusedPrintersProvider extends Loader<List<PrinterInfo>> { for (int i = 0; i < favoriteCount; i++) { printerIds.add(mFavoritePrinters.get(i).getId()); } - mDiscoverySession.startPrinterDisovery(printerIds); + mDiscoverySession.startPrinterDiscovery(printerIds); List<PrinterInfo> printers = mDiscoverySession.getPrinters(); if (!printers.isEmpty()) { updatePrinters(printers, mFavoritePrinters); @@ -281,7 +281,9 @@ public class FusedPrintersProvider extends Loader<List<PrinterInfo>> { mDiscoverySession.stopPrinterStateTracking(mTrackedPrinter); } mTrackedPrinter = printerId; - mDiscoverySession.startPrinterStateTracking(printerId); + if (printerId != null) { + mDiscoverySession.startPrinterStateTracking(printerId); + } } } @@ -514,7 +516,7 @@ public class FusedPrintersProvider extends Loader<List<PrinterInfo>> { } private List<PrinterInfo> doReadPrinterHistory() { - FileInputStream in = null; + final FileInputStream in; try { in = mStatePersistFile.openRead(); } catch (FileNotFoundException fnfe) { @@ -641,7 +643,7 @@ public class FusedPrintersProvider extends Loader<List<PrinterInfo>> { } return true; } - }; + } private final class WriteTask extends AsyncTask<List<PrinterInfo>, Void, Void> { @Override @@ -703,6 +705,6 @@ public class FusedPrintersProvider extends Loader<List<PrinterInfo>> { IoUtils.closeQuietly(out); } } - }; + } } } diff --git a/packages/PrintSpooler/src/com/android/printspooler/ui/PrintActivity.java b/packages/PrintSpooler/src/com/android/printspooler/ui/PrintActivity.java new file mode 100644 index 0000000..f71cafe --- /dev/null +++ b/packages/PrintSpooler/src/com/android/printspooler/ui/PrintActivity.java @@ -0,0 +1,1953 @@ +/* + * Copyright (C) 2014 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.ui; + +import android.app.Activity; +import android.app.Fragment; +import android.app.FragmentTransaction; +import android.content.ActivityNotFoundException; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager.NameNotFoundException; +import android.content.pm.ResolveInfo; +import android.database.DataSetObserver; +import android.graphics.drawable.Drawable; +import android.net.Uri; +import android.os.Bundle; +import android.os.Handler; +import android.os.IBinder; +import android.print.IPrintDocumentAdapter; +import android.print.PageRange; +import android.print.PrintAttributes; +import android.print.PrintAttributes.MediaSize; +import android.print.PrintAttributes.Resolution; +import android.print.PrintDocumentInfo; +import android.print.PrintJobInfo; +import android.print.PrintManager; +import android.print.PrinterCapabilitiesInfo; +import android.print.PrinterId; +import android.print.PrinterInfo; +import android.printservice.PrintService; +import android.provider.DocumentsContract; +import android.text.Editable; +import android.text.TextUtils; +import android.text.TextUtils.SimpleStringSplitter; +import android.text.TextWatcher; +import android.util.ArrayMap; +import android.util.Log; +import android.view.KeyEvent; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.View.OnFocusChangeListener; +import android.view.ViewGroup; +import android.view.inputmethod.InputMethodManager; +import android.widget.AdapterView; +import android.widget.AdapterView.OnItemSelectedListener; +import android.widget.ArrayAdapter; +import android.widget.BaseAdapter; +import android.widget.Button; +import android.widget.EditText; +import android.widget.ImageView; +import android.widget.Spinner; +import android.widget.TextView; + +import com.android.printspooler.R; +import com.android.printspooler.model.PrintSpoolerProvider; +import com.android.printspooler.model.PrintSpoolerService; +import com.android.printspooler.model.RemotePrintDocument; +import com.android.printspooler.util.MediaSizeUtils; +import com.android.printspooler.util.MediaSizeUtils.MediaSizeComparator; +import com.android.printspooler.util.PageRangeUtils; +import com.android.printspooler.util.PrintOptionUtils; +import com.android.printspooler.widget.ContentView; +import com.android.printspooler.widget.ContentView.OptionsStateChangeListener; + +import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class PrintActivity extends Activity implements RemotePrintDocument.UpdateResultCallbacks, + PrintErrorFragment.OnActionListener, PrintProgressFragment.OnCancelRequestListener { + private static final String LOG_TAG = "PrintActivity"; + + public static final String INTENT_EXTRA_PRINTER_ID = "INTENT_EXTRA_PRINTER_ID"; + + private static final int ORIENTATION_PORTRAIT = 0; + private static final int ORIENTATION_LANDSCAPE = 1; + + private static final int ACTIVITY_REQUEST_CREATE_FILE = 1; + private static final int ACTIVITY_REQUEST_SELECT_PRINTER = 2; + private static final int ACTIVITY_REQUEST_POPULATE_ADVANCED_PRINT_OPTIONS = 3; + + private static final int DEST_ADAPTER_MAX_ITEM_COUNT = 9; + + private static final int DEST_ADAPTER_ITEM_ID_SAVE_AS_PDF = Integer.MAX_VALUE; + private static final int DEST_ADAPTER_ITEM_ID_ALL_PRINTERS = Integer.MAX_VALUE - 1; + + private static final int STATE_CONFIGURING = 0; + private static final int STATE_PRINT_CONFIRMED = 1; + private static final int STATE_PRINT_CANCELED = 2; + private static final int STATE_UPDATE_FAILED = 3; + private static final int STATE_CREATE_FILE_FAILED = 4; + private static final int STATE_PRINTER_UNAVAILABLE = 5; + + private static final int UI_STATE_PREVIEW = 0; + private static final int UI_STATE_ERROR = 1; + private static final int UI_STATE_PROGRESS = 2; + + private static final int MIN_COPIES = 1; + private static final String MIN_COPIES_STRING = String.valueOf(MIN_COPIES); + + private static final Pattern PATTERN_DIGITS = Pattern.compile("[\\d]+"); + + private static final Pattern PATTERN_ESCAPE_SPECIAL_CHARS = Pattern.compile( + "(?=[]\\[+&|!(){}^\"~*?:\\\\])"); + + private static final Pattern PATTERN_PAGE_RANGE = Pattern.compile( + "[\\s]*[0-9]*[\\s]*[\\-]?[\\s]*[0-9]*[\\s]*?(([,])" + + "[\\s]*[0-9]*[\\s]*[\\-]?[\\s]*[0-9]*[\\s]*|[\\s]*)+"); + + public static final PageRange[] ALL_PAGES_ARRAY = new PageRange[] {PageRange.ALL_PAGES}; + + private final PrinterAvailabilityDetector mPrinterAvailabilityDetector = + new PrinterAvailabilityDetector(); + + private final SimpleStringSplitter mStringCommaSplitter = new SimpleStringSplitter(','); + + private final OnFocusChangeListener mSelectAllOnFocusListener = new SelectAllOnFocusListener(); + + private PrintSpoolerProvider mSpoolerProvider; + + private PrintJobInfo mPrintJob; + private RemotePrintDocument mPrintedDocument; + private PrinterRegistry mPrinterRegistry; + + private EditText mCopiesEditText; + + private TextView mPageRangeOptionsTitle; + private TextView mPageRangeTitle; + private EditText mPageRangeEditText; + + private Spinner mDestinationSpinner; + private DestinationAdapter mDestinationSpinnerAdapter; + + private Spinner mMediaSizeSpinner; + private ArrayAdapter<SpinnerItem<MediaSize>> mMediaSizeSpinnerAdapter; + + private Spinner mColorModeSpinner; + private ArrayAdapter<SpinnerItem<Integer>> mColorModeSpinnerAdapter; + + private Spinner mOrientationSpinner; + private ArrayAdapter<SpinnerItem<Integer>> mOrientationSpinnerAdapter; + + private Spinner mRangeOptionsSpinner; + + private ContentView mOptionsContent; + + private TextView mSummaryCopies; + private TextView mSummaryPaperSize; + + private View mAdvancedPrintOptionsContainer; + + private Button mMoreOptionsButton; + + private ImageView mPrintButton; + + private ProgressMessageController mProgressMessageController; + + private MediaSizeComparator mMediaSizeComparator; + + private PageRange[] mRequestedPages; + + private PrinterInfo mOldCurrentPrinter; + + private String mCallingPackageName; + + private int mState = STATE_CONFIGURING; + + private int mUiState = UI_STATE_PREVIEW; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + Bundle extras = getIntent().getExtras(); + + mPrintJob = extras.getParcelable(PrintManager.EXTRA_PRINT_JOB); + if (mPrintJob == null) { + throw new IllegalArgumentException(PrintManager.EXTRA_PRINT_JOB + + " cannot be null"); + } + mPrintJob.setAttributes(new PrintAttributes.Builder().build()); + + final IBinder adapter = extras.getBinder(PrintManager.EXTRA_PRINT_DOCUMENT_ADAPTER); + if (adapter == null) { + throw new IllegalArgumentException(PrintManager.EXTRA_PRINT_DOCUMENT_ADAPTER + + " cannot be null"); + } + + mCallingPackageName = extras.getString(DocumentsContract.EXTRA_PACKAGE_NAME); + + // This will take just a few milliseconds, so just wait to + // bind to the local service before showing the UI. + mSpoolerProvider = new PrintSpoolerProvider(this, + new Runnable() { + @Override + public void run() { + // Now that we are bound to the print spooler service, + // create the printer registry and wait for it to get + // the first batch of results which will be delivered + // after reading historical data. This should be pretty + // fast, so just wait before showing the UI. + mPrinterRegistry = new PrinterRegistry(PrintActivity.this, + new Runnable() { + @Override + public void run() { + setTitle(R.string.print_dialog); + setContentView(R.layout.print_activity); + + mPrintedDocument = new RemotePrintDocument(PrintActivity.this, + IPrintDocumentAdapter.Stub.asInterface(adapter), + PrintSpoolerService.generateFileForPrintJob(PrintActivity.this, + mPrintJob.getId()), + new RemotePrintDocument.DocumentObserver() { + @Override + public void onDestroy() { + finish(); + } + }, PrintActivity.this); + + mProgressMessageController = new ProgressMessageController(PrintActivity.this); + + mMediaSizeComparator = new MediaSizeComparator(PrintActivity.this); + + mDestinationSpinnerAdapter = new DestinationAdapter(); + + bindUi(); + + updateOptionsUi(); + + // Now show the updated UI to avoid flicker. + mOptionsContent.setVisibility(View.VISIBLE); + + mRequestedPages = computeRequestedPages(); + + mPrintedDocument.start(); + + ensurePreviewUiShown(); + } + }); + } + }); + } + + @Override + public void onPause() { + if (isFinishing()) { + PrintSpoolerService spooler = mSpoolerProvider.getSpooler(); + if (mState == STATE_PRINT_CONFIRMED) { + spooler.updatePrintJobUserConfigurableOptionsNoPersistence(mPrintJob); + spooler.setPrintJobState(mPrintJob.getId(), PrintJobInfo.STATE_QUEUED, null); + } else { + spooler.setPrintJobState(mPrintJob.getId(), PrintJobInfo.STATE_CANCELED, null); + } + mProgressMessageController.cancel(); + mPrinterRegistry.setTrackedPrinter(null); + mSpoolerProvider.destroy(); + mPrintedDocument.finish(); + mPrintedDocument.destroy(); + } + + mPrinterAvailabilityDetector.cancel(); + + super.onPause(); + } + + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + if (keyCode == KeyEvent.KEYCODE_BACK) { + event.startTracking(); + return true; + } + return super.onKeyDown(keyCode, event); + } + + @Override + public boolean onKeyUp(int keyCode, KeyEvent event) { + if (keyCode == KeyEvent.KEYCODE_BACK + && event.isTracking() && !event.isCanceled()) { + cancelPrint(); + return true; + } + return super.onKeyUp(keyCode, event); + } + + @Override + public void onActionPerformed() { + switch (mState) { + case STATE_UPDATE_FAILED: { + if (canUpdateDocument()) { + updateDocument(true, true); + ensurePreviewUiShown(); + mState = STATE_CONFIGURING; + updateOptionsUi(); + } + } break; + + case STATE_CREATE_FILE_FAILED: { + mState = STATE_CONFIGURING; + ensurePreviewUiShown(); + updateOptionsUi(); + } break; + } + } + + @Override + public void onCancelRequest() { + if (mPrintedDocument.isUpdating()) { + mPrintedDocument.cancel(); + } + } + + public void onUpdateCanceled() { + mProgressMessageController.cancel(); + ensurePreviewUiShown(); + finishIfConfirmedOrCanceled(); + updateOptionsUi(); + } + + @Override + public void onUpdateCompleted(RemotePrintDocument.RemotePrintDocumentInfo document) { + mProgressMessageController.cancel(); + ensurePreviewUiShown(); + + // Update the print job with the info for the written document. The page + // count we get from the remote document is the pages in the document from + // the app perspective but the print job should contain the page count from + // print service perspective which is the pages in the written PDF not the + // pages in the printed document. + PrintDocumentInfo info = document.info; + if (info == null) { + return; + } + final int pageCount = PageRangeUtils.getNormalizedPageCount(document.writtenPages, + info.getPageCount()); + PrintDocumentInfo adjustedInfo = new PrintDocumentInfo.Builder(info.getName()) + .setContentType(info.getContentType()) + .setPageCount(pageCount) + .build(); + mPrintJob.setDocumentInfo(adjustedInfo); + mPrintJob.setPages(document.printedPages); + finishIfConfirmedOrCanceled(); + updateOptionsUi(); + } + + @Override + public void onUpdateFailed(CharSequence error) { + mState = STATE_UPDATE_FAILED; + ensureErrorUiShown(error, PrintErrorFragment.ACTION_RETRY); + updateOptionsUi(); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + switch (requestCode) { + case ACTIVITY_REQUEST_CREATE_FILE: { + onStartCreateDocumentActivityResult(resultCode, data); + } break; + + case ACTIVITY_REQUEST_SELECT_PRINTER: { + onSelectPrinterActivityResult(resultCode, data); + } break; + + case ACTIVITY_REQUEST_POPULATE_ADVANCED_PRINT_OPTIONS: { + onAdvancedPrintOptionsActivityResult(resultCode, data); + } break; + } + } + + private void startCreateDocumentActivity() { + PrintDocumentInfo info = mPrintedDocument.getDocumentInfo().info; + if (info == null) { + return; + } + Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT); + intent.setType("application/pdf"); + intent.putExtra(Intent.EXTRA_TITLE, info.getName()); + intent.putExtra(DocumentsContract.EXTRA_PACKAGE_NAME, mCallingPackageName); + startActivityForResult(intent, ACTIVITY_REQUEST_CREATE_FILE); + } + + private void onStartCreateDocumentActivityResult(int resultCode, Intent data) { + if (resultCode == RESULT_OK && data != null) { + Uri uri = data.getData(); + mPrintedDocument.writeContent(getContentResolver(), uri); + finish(); + } else if (resultCode == RESULT_CANCELED) { + mState = STATE_CONFIGURING; + updateOptionsUi(); + } else { + ensureErrorUiShown(getString(R.string.print_write_error_message), + PrintErrorFragment.ACTION_CONFIRM); + mState = STATE_CREATE_FILE_FAILED; + updateOptionsUi(); + } + } + + private void startSelectPrinterActivity() { + Intent intent = new Intent(this, SelectPrinterActivity.class); + startActivityForResult(intent, ACTIVITY_REQUEST_SELECT_PRINTER); + } + + private void onSelectPrinterActivityResult(int resultCode, Intent data) { + if (resultCode == RESULT_OK && data != null) { + PrinterId printerId = data.getParcelableExtra(INTENT_EXTRA_PRINTER_ID); + if (printerId != null) { + mDestinationSpinnerAdapter.ensurePrinterInVisibleAdapterPosition(printerId); + final int index = mDestinationSpinnerAdapter.getPrinterIndex(printerId); + if (index != AdapterView.INVALID_POSITION) { + mDestinationSpinner.setSelection(index); + return; + } + } + } + + PrinterId printerId = mOldCurrentPrinter.getId(); + final int index = mDestinationSpinnerAdapter.getPrinterIndex(printerId); + mDestinationSpinner.setSelection(index); + } + + private void startAdvancedPrintOptionsActivity(PrinterInfo printer) { + ComponentName serviceName = printer.getId().getServiceName(); + + String activityName = PrintOptionUtils.getAdvancedOptionsActivityName(this, serviceName); + if (TextUtils.isEmpty(activityName)) { + return; + } + + Intent intent = new Intent(Intent.ACTION_MAIN); + intent.setComponent(new ComponentName(serviceName.getPackageName(), activityName)); + + List<ResolveInfo> resolvedActivities = getPackageManager() + .queryIntentActivities(intent, 0); + if (resolvedActivities.isEmpty()) { + return; + } + + // The activity is a component name, therefore it is one or none. + if (resolvedActivities.get(0).activityInfo.exported) { + intent.putExtra(PrintService.EXTRA_PRINT_JOB_INFO, mPrintJob); + intent.putExtra(PrintService.EXTRA_PRINTER_INFO, printer); + + // This is external activity and may not be there. + try { + startActivityForResult(intent, ACTIVITY_REQUEST_POPULATE_ADVANCED_PRINT_OPTIONS); + } catch (ActivityNotFoundException anfe) { + Log.e(LOG_TAG, "Error starting activity for intent: " + intent, anfe); + } + } + } + + private void onAdvancedPrintOptionsActivityResult(int resultCode, Intent data) { + if (resultCode != RESULT_OK || data == null) { + return; + } + + PrintJobInfo printJobInfo = data.getParcelableExtra(PrintService.EXTRA_PRINT_JOB_INFO); + + if (printJobInfo == null) { + return; + } + + // Take the advanced options without interpretation. + mPrintJob.setAdvancedOptions(printJobInfo.getAdvancedOptions()); + + // Take copies without interpretation as the advanced print dialog + // cannot create a print job info with invalid copies. + mCopiesEditText.setText(String.valueOf(printJobInfo.getCopies())); + mPrintJob.setCopies(printJobInfo.getCopies()); + + PrintAttributes currAttributes = mPrintJob.getAttributes(); + PrintAttributes newAttributes = printJobInfo.getAttributes(); + + // Take the media size only if the current printer supports is. + MediaSize oldMediaSize = currAttributes.getMediaSize(); + MediaSize newMediaSize = newAttributes.getMediaSize(); + if (!oldMediaSize.equals(newMediaSize)) { + final int mediaSizeCount = mMediaSizeSpinnerAdapter.getCount(); + MediaSize newMediaSizePortrait = newAttributes.getMediaSize().asPortrait(); + for (int i = 0; i < mediaSizeCount; i++) { + MediaSize supportedSizePortrait = mMediaSizeSpinnerAdapter.getItem(i).value.asPortrait(); + if (supportedSizePortrait.equals(newMediaSizePortrait)) { + currAttributes.setMediaSize(newMediaSize); + mMediaSizeSpinner.setSelection(i); + if (currAttributes.getMediaSize().isPortrait()) { + if (mOrientationSpinner.getSelectedItemPosition() != 0) { + mOrientationSpinner.setSelection(0); + } + } else { + if (mOrientationSpinner.getSelectedItemPosition() != 1) { + mOrientationSpinner.setSelection(1); + } + } + break; + } + } + } + + // Take the color mode only if the current printer supports it. + final int currColorMode = currAttributes.getColorMode(); + final int newColorMode = newAttributes.getColorMode(); + if (currColorMode != newColorMode) { + final int colorModeCount = mColorModeSpinner.getCount(); + for (int i = 0; i < colorModeCount; i++) { + final int supportedColorMode = mColorModeSpinnerAdapter.getItem(i).value; + if (supportedColorMode == newColorMode) { + currAttributes.setColorMode(newColorMode); + mColorModeSpinner.setSelection(i); + break; + } + } + } + + // Take the page range only if it is valid. + PageRange[] pageRanges = printJobInfo.getPages(); + if (pageRanges != null && pageRanges.length > 0) { + pageRanges = PageRangeUtils.normalize(pageRanges); + + PrintDocumentInfo info = mPrintedDocument.getDocumentInfo().info; + final int pageCount = (info != null) ? info.getPageCount() : 0; + + // Handle the case where all pages are specified explicitly + // instead of the *all pages* constant. + if (pageRanges.length == 1) { + if (pageRanges[0].getStart() == 0 && pageRanges[0].getEnd() == pageCount - 1) { + pageRanges[0] = PageRange.ALL_PAGES; + } + } + + if (Arrays.equals(pageRanges, ALL_PAGES_ARRAY)) { + mPrintJob.setPages(pageRanges); + + if (mRangeOptionsSpinner.getSelectedItemPosition() != 0) { + mRangeOptionsSpinner.setSelection(0); + } + } else if (pageRanges[0].getStart() >= 0 + && pageRanges[pageRanges.length - 1].getEnd() < pageCount) { + mPrintJob.setPages(pageRanges); + + if (mRangeOptionsSpinner.getSelectedItemPosition() != 1) { + mRangeOptionsSpinner.setSelection(1); + } + + StringBuilder builder = new StringBuilder(); + final int pageRangeCount = pageRanges.length; + for (int i = 0; i < pageRangeCount; i++) { + if (builder.length() > 0) { + builder.append(','); + } + + final int shownStartPage; + final int shownEndPage; + PageRange pageRange = pageRanges[i]; + if (pageRange.equals(PageRange.ALL_PAGES)) { + shownStartPage = 1; + shownEndPage = pageCount; + } else { + shownStartPage = pageRange.getStart() + 1; + shownEndPage = pageRange.getEnd() + 1; + } + + builder.append(shownStartPage); + + if (shownStartPage != shownEndPage) { + builder.append('-'); + builder.append(shownEndPage); + } + } + mPageRangeEditText.setText(builder.toString()); + } + } + + // Update the content if needed. + if (canUpdateDocument()) { + updateDocument(true, false); + } + } + + private void ensureProgressUiShown() { + if (mUiState != UI_STATE_PROGRESS) { + mUiState = UI_STATE_PROGRESS; + Fragment fragment = PrintProgressFragment.newInstance(); + showFragment(fragment); + } + } + + private void ensurePreviewUiShown() { + if (mUiState != UI_STATE_PREVIEW) { + mUiState = UI_STATE_PREVIEW; + Fragment fragment = PrintPreviewFragment.newInstance(); + showFragment(fragment); + } + } + + private void ensureErrorUiShown(CharSequence message, int action) { + if (mUiState != UI_STATE_ERROR) { + mUiState = UI_STATE_ERROR; + Fragment fragment = PrintErrorFragment.newInstance(message, action); + showFragment(fragment); + } + } + + private void showFragment(Fragment fragment) { + FragmentTransaction transaction = getFragmentManager().beginTransaction(); + Fragment oldFragment = getFragmentManager().findFragmentById( + R.id.embedded_content_container); + if (oldFragment != null) { + transaction.remove(oldFragment); + } + transaction.add(R.id.embedded_content_container, fragment); + transaction.commit(); + } + + private void requestCreatePdfFileOrFinish() { + if (getCurrentPrinter() == mDestinationSpinnerAdapter.getPdfPrinter()) { + startCreateDocumentActivity(); + } else { + finish(); + } + } + + private void finishIfConfirmedOrCanceled() { + if (mState == STATE_PRINT_CONFIRMED) { + requestCreatePdfFileOrFinish(); + } else if (mState == STATE_PRINT_CANCELED) { + finish(); + } + } + + private void updatePrintAttributesFromCapabilities(PrinterCapabilitiesInfo capabilities) { + PrintAttributes defaults = capabilities.getDefaults(); + + // Sort the media sizes based on the current locale. + List<MediaSize> sortedMediaSizes = new ArrayList<>(capabilities.getMediaSizes()); + Collections.sort(sortedMediaSizes, mMediaSizeComparator); + + PrintAttributes attributes = mPrintJob.getAttributes(); + + // Media size. + MediaSize currMediaSize = attributes.getMediaSize(); + if (currMediaSize == null) { + attributes.setMediaSize(defaults.getMediaSize()); + } else { + boolean foundCurrentMediaSize = false; + // Try to find the current media size in the capabilities as + // it may be in a different orientation. + MediaSize currMediaSizePortrait = currMediaSize.asPortrait(); + final int mediaSizeCount = sortedMediaSizes.size(); + for (int i = 0; i < mediaSizeCount; i++) { + MediaSize mediaSize = sortedMediaSizes.get(i); + if (currMediaSizePortrait.equals(mediaSize.asPortrait())) { + attributes.setMediaSize(currMediaSize); + foundCurrentMediaSize = true; + break; + } + } + // If we did not find the current media size fall back to default. + if (!foundCurrentMediaSize) { + attributes.setMediaSize(defaults.getMediaSize()); + } + } + + // Color mode. + final int colorMode = attributes.getColorMode(); + if ((capabilities.getColorModes() & colorMode) == 0) { + attributes.setColorMode(defaults.getColorMode()); + } + + // Resolution + Resolution resolution = attributes.getResolution(); + if (resolution == null || !capabilities.getResolutions().contains(resolution)) { + attributes.setResolution(defaults.getResolution()); + } + + // Margins. + attributes.setMinMargins(defaults.getMinMargins()); + } + + private boolean updateDocument(boolean preview, boolean clearLastError) { + if (!clearLastError && mPrintedDocument.hasUpdateError()) { + return false; + } + + if (clearLastError && mPrintedDocument.hasUpdateError()) { + mPrintedDocument.clearUpdateError(); + } + + if (mRequestedPages != null && mRequestedPages.length > 0) { + final PageRange[] pages; + if (preview) { + final int firstPage = mRequestedPages[0].getStart(); + pages = new PageRange[]{new PageRange(firstPage, firstPage)}; + } else { + pages = mRequestedPages; + } + final boolean willUpdate = mPrintedDocument.update(mPrintJob.getAttributes(), + pages, preview); + + if (willUpdate) { + mProgressMessageController.post(); + return true; + } + } + + return false; + } + + private void addCurrentPrinterToHistory() { + PrinterInfo currentPrinter = getCurrentPrinter(); + if (currentPrinter != null) { + PrinterId fakePdfPrinterId = mDestinationSpinnerAdapter.getPdfPrinter().getId(); + if (!currentPrinter.getId().equals(fakePdfPrinterId)) { + mPrinterRegistry.addHistoricalPrinter(currentPrinter); + } + } + } + + private PrinterInfo getCurrentPrinter() { + return ((PrinterHolder) mDestinationSpinner.getSelectedItem()).printer; + } + + private void cancelPrint() { + mState = STATE_PRINT_CANCELED; + updateOptionsUi(); + if (mPrintedDocument.isUpdating()) { + mPrintedDocument.cancel(); + } + finish(); + } + + private void confirmPrint() { + mState = STATE_PRINT_CONFIRMED; + updateOptionsUi(); + if (canUpdateDocument()) { + updateDocument(false, false); + } + addCurrentPrinterToHistory(); + if (!mPrintedDocument.isUpdating()) { + requestCreatePdfFileOrFinish(); + } + } + + private void bindUi() { + // Summary + mSummaryCopies = (TextView) findViewById(R.id.copies_count_summary); + mSummaryPaperSize = (TextView) findViewById(R.id.paper_size_summary); + + // Options container + mOptionsContent = (ContentView) findViewById(R.id.options_content); + mOptionsContent.setOptionsStateChangeListener(new OptionsStateChangeListener() { + @Override + public void onOptionsOpened() { + // TODO: Update preview. + } + + @Override + public void onOptionsClosed() { + // TODO: Update preview. + } + }); + + OnItemSelectedListener itemSelectedListener = new MyOnItemSelectedListener(); + OnClickListener clickListener = new MyClickListener(); + + // Copies + mCopiesEditText = (EditText) findViewById(R.id.copies_edittext); + mCopiesEditText.setOnFocusChangeListener(mSelectAllOnFocusListener); + mCopiesEditText.setText(MIN_COPIES_STRING); + mCopiesEditText.setSelection(mCopiesEditText.getText().length()); + mCopiesEditText.addTextChangedListener(new EditTextWatcher()); + + // Destination. + mDestinationSpinnerAdapter.registerDataSetObserver(new PrintersObserver()); + mDestinationSpinner = (Spinner) findViewById(R.id.destination_spinner); + mDestinationSpinner.setAdapter(mDestinationSpinnerAdapter); + mDestinationSpinner.setOnItemSelectedListener(itemSelectedListener); + mDestinationSpinner.setSelection(0); + + // Media size. + mMediaSizeSpinnerAdapter = new ArrayAdapter<>( + this, R.layout.spinner_dropdown_item, R.id.title); + mMediaSizeSpinner = (Spinner) findViewById(R.id.paper_size_spinner); + mMediaSizeSpinner.setAdapter(mMediaSizeSpinnerAdapter); + mMediaSizeSpinner.setOnItemSelectedListener(itemSelectedListener); + + // Color mode. + mColorModeSpinnerAdapter = new ArrayAdapter<>( + this, R.layout.spinner_dropdown_item, R.id.title); + mColorModeSpinner = (Spinner) findViewById(R.id.color_spinner); + mColorModeSpinner.setAdapter(mColorModeSpinnerAdapter); + mColorModeSpinner.setOnItemSelectedListener(itemSelectedListener); + + // Orientation + mOrientationSpinnerAdapter = new ArrayAdapter<>( + this, R.layout.spinner_dropdown_item, R.id.title); + String[] orientationLabels = getResources().getStringArray( + R.array.orientation_labels); + mOrientationSpinnerAdapter.add(new SpinnerItem<>( + ORIENTATION_PORTRAIT, orientationLabels[0])); + mOrientationSpinnerAdapter.add(new SpinnerItem<>( + ORIENTATION_LANDSCAPE, orientationLabels[1])); + mOrientationSpinner = (Spinner) findViewById(R.id.orientation_spinner); + mOrientationSpinner.setAdapter(mOrientationSpinnerAdapter); + mOrientationSpinner.setOnItemSelectedListener(itemSelectedListener); + + // Range options + ArrayAdapter<SpinnerItem<Integer>> rangeOptionsSpinnerAdapter = + new ArrayAdapter<>(this, R.layout.spinner_dropdown_item, R.id.title); + final int[] rangeOptionsValues = getResources().getIntArray( + R.array.page_options_values); + String[] rangeOptionsLabels = getResources().getStringArray( + R.array.page_options_labels); + final int rangeOptionsCount = rangeOptionsLabels.length; + for (int i = 0; i < rangeOptionsCount; i++) { + rangeOptionsSpinnerAdapter.add(new SpinnerItem<>( + rangeOptionsValues[i], rangeOptionsLabels[i])); + } + mPageRangeOptionsTitle = (TextView) findViewById(R.id.range_options_title); + mRangeOptionsSpinner = (Spinner) findViewById(R.id.range_options_spinner); + mRangeOptionsSpinner.setAdapter(rangeOptionsSpinnerAdapter); + mRangeOptionsSpinner.setOnItemSelectedListener(itemSelectedListener); + + // Page range + mPageRangeTitle = (TextView) findViewById(R.id.page_range_title); + mPageRangeEditText = (EditText) findViewById(R.id.page_range_edittext); + mPageRangeEditText.setOnFocusChangeListener(mSelectAllOnFocusListener); + mPageRangeEditText.addTextChangedListener(new RangeTextWatcher()); + + // Advanced options button. + mAdvancedPrintOptionsContainer = findViewById(R.id.more_options_container); + mMoreOptionsButton = (Button) findViewById(R.id.more_options_button); + mMoreOptionsButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + PrinterInfo currentPrinter = getCurrentPrinter(); + if (currentPrinter != null) { + startAdvancedPrintOptionsActivity(currentPrinter); + } + } + }); + + // Print button + mPrintButton = (ImageView) findViewById(R.id.print_button); + mPrintButton.setOnClickListener(clickListener); + } + + private final class MyClickListener implements OnClickListener { + @Override + public void onClick(View view) { + if (view == mPrintButton) { + PrinterInfo currentPrinter = getCurrentPrinter(); + if (currentPrinter != null) { + confirmPrint(); + } else { + cancelPrint(); + } + } else if (view == mMoreOptionsButton) { + PrinterInfo currentPrinter = getCurrentPrinter(); + if (currentPrinter != null) { + startAdvancedPrintOptionsActivity(currentPrinter); + } + } + } + } + + private static boolean canPrint(PrinterInfo printer) { + return printer.getCapabilities() != null + && printer.getStatus() != PrinterInfo.STATUS_UNAVAILABLE; + } + + private void updateOptionsUi() { + // Always update the summary. + if (!TextUtils.isEmpty(mCopiesEditText.getText())) { + mSummaryCopies.setText(mCopiesEditText.getText()); + } + + final int selectedMediaIndex = mMediaSizeSpinner.getSelectedItemPosition(); + if (selectedMediaIndex >= 0) { + SpinnerItem<MediaSize> mediaItem = mMediaSizeSpinnerAdapter.getItem(selectedMediaIndex); + mSummaryPaperSize.setText(mediaItem.label); + } + + if (mState == STATE_PRINT_CONFIRMED + || mState == STATE_PRINT_CANCELED + || mState == STATE_UPDATE_FAILED + || mState == STATE_CREATE_FILE_FAILED + || mState == STATE_PRINTER_UNAVAILABLE) { + if (mState != STATE_PRINTER_UNAVAILABLE) { + mDestinationSpinner.setEnabled(false); + } + mCopiesEditText.setEnabled(false); + mMediaSizeSpinner.setEnabled(false); + mColorModeSpinner.setEnabled(false); + mOrientationSpinner.setEnabled(false); + mRangeOptionsSpinner.setEnabled(false); + mPageRangeEditText.setEnabled(false); + mPrintButton.setEnabled(false); + mMoreOptionsButton.setEnabled(false); + return; + } + + // If no current printer, or it has no capabilities, or it is not + // available, we disable all print options except the destination. + PrinterInfo currentPrinter = getCurrentPrinter(); + if (currentPrinter == null || !canPrint(currentPrinter)) { + mCopiesEditText.setEnabled(false); + mMediaSizeSpinner.setEnabled(false); + mColorModeSpinner.setEnabled(false); + mOrientationSpinner.setEnabled(false); + mRangeOptionsSpinner.setEnabled(false); + mPageRangeEditText.setEnabled(false); + mPrintButton.setEnabled(false); + mMoreOptionsButton.setEnabled(false); + return; + } + + PrinterCapabilitiesInfo capabilities = currentPrinter.getCapabilities(); + PrintAttributes defaultAttributes = capabilities.getDefaults(); + + // Media size. + mMediaSizeSpinner.setEnabled(true); + + List<MediaSize> mediaSizes = new ArrayList<>(capabilities.getMediaSizes()); + // Sort the media sizes based on the current locale. + Collections.sort(mediaSizes, mMediaSizeComparator); + + PrintAttributes attributes = mPrintJob.getAttributes(); + + // If the media sizes changed, we update the adapter and the spinner. + boolean mediaSizesChanged = false; + final int mediaSizeCount = mediaSizes.size(); + if (mediaSizeCount != mMediaSizeSpinnerAdapter.getCount()) { + mediaSizesChanged = true; + } else { + for (int i = 0; i < mediaSizeCount; i++) { + if (!mediaSizes.get(i).equals(mMediaSizeSpinnerAdapter.getItem(i).value)) { + mediaSizesChanged = true; + break; + } + } + } + if (mediaSizesChanged) { + // Remember the old media size to try selecting it again. + int oldMediaSizeNewIndex = AdapterView.INVALID_POSITION; + MediaSize oldMediaSize = attributes.getMediaSize(); + + // Rebuild the adapter data. + mMediaSizeSpinnerAdapter.clear(); + for (int i = 0; i < mediaSizeCount; i++) { + MediaSize mediaSize = mediaSizes.get(i); + if (oldMediaSize != null + && mediaSize.asPortrait().equals(oldMediaSize.asPortrait())) { + // Update the index of the old selection. + oldMediaSizeNewIndex = i; + } + mMediaSizeSpinnerAdapter.add(new SpinnerItem<>( + mediaSize, mediaSize.getLabel(getPackageManager()))); + } + + if (oldMediaSizeNewIndex != AdapterView.INVALID_POSITION) { + // Select the old media size - nothing really changed. + if (mMediaSizeSpinner.getSelectedItemPosition() != oldMediaSizeNewIndex) { + mMediaSizeSpinner.setSelection(oldMediaSizeNewIndex); + } + } else { + // Select the first or the default. + final int mediaSizeIndex = Math.max(mediaSizes.indexOf( + defaultAttributes.getMediaSize()), 0); + if (mMediaSizeSpinner.getSelectedItemPosition() != mediaSizeIndex) { + mMediaSizeSpinner.setSelection(mediaSizeIndex); + } + // Respect the orientation of the old selection. + if (oldMediaSize != null) { + if (oldMediaSize.isPortrait()) { + attributes.setMediaSize(mMediaSizeSpinnerAdapter + .getItem(mediaSizeIndex).value.asPortrait()); + } else { + attributes.setMediaSize(mMediaSizeSpinnerAdapter + .getItem(mediaSizeIndex).value.asLandscape()); + } + } + } + } + + // Color mode. + mColorModeSpinner.setEnabled(true); + final int colorModes = capabilities.getColorModes(); + + // If the color modes changed, we update the adapter and the spinner. + boolean colorModesChanged = false; + if (Integer.bitCount(colorModes) != mColorModeSpinnerAdapter.getCount()) { + colorModesChanged = true; + } else { + int remainingColorModes = colorModes; + int adapterIndex = 0; + while (remainingColorModes != 0) { + final int colorBitOffset = Integer.numberOfTrailingZeros(remainingColorModes); + final int colorMode = 1 << colorBitOffset; + remainingColorModes &= ~colorMode; + if (colorMode != mColorModeSpinnerAdapter.getItem(adapterIndex).value) { + colorModesChanged = true; + break; + } + adapterIndex++; + } + } + if (colorModesChanged) { + // Remember the old color mode to try selecting it again. + int oldColorModeNewIndex = AdapterView.INVALID_POSITION; + final int oldColorMode = attributes.getColorMode(); + + // Rebuild the adapter data. + 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; + if (colorMode == oldColorMode) { + // Update the index of the old selection. + oldColorModeNewIndex = colorBitOffset; + } + remainingColorModes &= ~colorMode; + mColorModeSpinnerAdapter.add(new SpinnerItem<>(colorMode, + colorModeLabels[colorBitOffset])); + } + if (oldColorModeNewIndex != AdapterView.INVALID_POSITION) { + // Select the old color mode - nothing really changed. + if (mColorModeSpinner.getSelectedItemPosition() != oldColorModeNewIndex) { + mColorModeSpinner.setSelection(oldColorModeNewIndex); + } + } else { + // Select the default. + final int selectedColorMode = colorModes & defaultAttributes.getColorMode(); + final int itemCount = mColorModeSpinnerAdapter.getCount(); + for (int i = 0; i < itemCount; i++) { + SpinnerItem<Integer> item = mColorModeSpinnerAdapter.getItem(i); + if (selectedColorMode == item.value) { + if (mColorModeSpinner.getSelectedItemPosition() != i) { + mColorModeSpinner.setSelection(i); + } + attributes.setColorMode(selectedColorMode); + } + } + } + } + + // Orientation + mOrientationSpinner.setEnabled(true); + MediaSize mediaSize = attributes.getMediaSize(); + if (mediaSize != null) { + if (mediaSize.isPortrait() + && mOrientationSpinner.getSelectedItemPosition() != 0) { + mOrientationSpinner.setSelection(0); + } else if (!mediaSize.isPortrait() + && mOrientationSpinner.getSelectedItemPosition() != 1) { + mOrientationSpinner.setSelection(1); + } + } + + // Range options + PrintDocumentInfo info = mPrintedDocument.getDocumentInfo().info; + if (info != null && info.getPageCount() > 0) { + if (info.getPageCount() == 1) { + mRangeOptionsSpinner.setEnabled(false); + } else { + mRangeOptionsSpinner.setEnabled(true); + if (mRangeOptionsSpinner.getSelectedItemPosition() > 0) { + if (!mPageRangeEditText.isEnabled()) { + mPageRangeEditText.setEnabled(true); + mPageRangeEditText.setVisibility(View.VISIBLE); + mPageRangeTitle.setVisibility(View.VISIBLE); + mPageRangeEditText.requestFocus(); + InputMethodManager imm = (InputMethodManager) + getSystemService(Context.INPUT_METHOD_SERVICE); + imm.showSoftInput(mPageRangeEditText, 0); + } + } else { + mPageRangeEditText.setEnabled(false); + mPageRangeEditText.setVisibility(View.INVISIBLE); + mPageRangeTitle.setVisibility(View.INVISIBLE); + } + } + String title = (info.getPageCount() != PrintDocumentInfo.PAGE_COUNT_UNKNOWN) + ? getString(R.string.label_pages, String.valueOf(info.getPageCount())) + : getString(R.string.page_count_unknown); + mPageRangeOptionsTitle.setText(title); + } else { + if (mRangeOptionsSpinner.getSelectedItemPosition() != 0) { + mRangeOptionsSpinner.setSelection(0); + } + mRangeOptionsSpinner.setEnabled(false); + mPageRangeOptionsTitle.setText(getString(R.string.page_count_unknown)); + mPageRangeEditText.setEnabled(false); + mPageRangeEditText.setVisibility(View.INVISIBLE); + mPageRangeTitle.setVisibility(View.INVISIBLE); + } + + // Advanced print options + ComponentName serviceName = currentPrinter.getId().getServiceName(); + if (!TextUtils.isEmpty(PrintOptionUtils.getAdvancedOptionsActivityName( + this, serviceName))) { + mAdvancedPrintOptionsContainer.setVisibility(View.VISIBLE); + mMoreOptionsButton.setEnabled(true); + } else { + mAdvancedPrintOptionsContainer.setVisibility(View.GONE); + mMoreOptionsButton.setEnabled(false); + } + + // Print + if (mDestinationSpinnerAdapter.getPdfPrinter() != currentPrinter) { + mPrintButton.setImageResource(com.android.internal.R.drawable.ic_print); + } else { + mPrintButton.setImageResource(com.android.internal.R.drawable.ic_menu_save); + } + if ((mRangeOptionsSpinner.getSelectedItemPosition() == 1 + && (TextUtils.isEmpty(mPageRangeEditText.getText()) || hasErrors())) + || (mRangeOptionsSpinner.getSelectedItemPosition() == 0 + && (mPrintedDocument.getDocumentInfo() == null || hasErrors()))) { + mPrintButton.setEnabled(false); + } else { + mPrintButton.setEnabled(true); + } + + // Copies + if (mDestinationSpinnerAdapter.getPdfPrinter() != currentPrinter) { + mCopiesEditText.setEnabled(true); + } else { + mCopiesEditText.setEnabled(false); + } + if (mCopiesEditText.getError() == null + && TextUtils.isEmpty(mCopiesEditText.getText())) { + mCopiesEditText.setText(String.valueOf(MIN_COPIES)); + mCopiesEditText.requestFocus(); + } + } + + private PageRange[] computeRequestedPages() { + if (hasErrors()) { + return null; + } + + if (mRangeOptionsSpinner.getSelectedItemPosition() > 0) { + List<PageRange> pageRanges = new ArrayList<>(); + mStringCommaSplitter.setString(mPageRangeEditText.getText().toString()); + + while (mStringCommaSplitter.hasNext()) { + String range = mStringCommaSplitter.next().trim(); + if (TextUtils.isEmpty(range)) { + continue; + } + final int dashIndex = range.indexOf('-'); + final int fromIndex; + final int toIndex; + + if (dashIndex > 0) { + fromIndex = Integer.parseInt(range.substring(0, dashIndex).trim()) - 1; + // It is possible that the dash is at the end since the input + // verification can has to allow the user to keep entering if + // this would lead to a valid input. So we handle this. + if (dashIndex < range.length() - 1) { + String fromString = range.substring(dashIndex + 1, range.length()).trim(); + toIndex = Integer.parseInt(fromString) - 1; + } else { + toIndex = fromIndex; + } + } else { + fromIndex = toIndex = Integer.parseInt(range) - 1; + } + + PageRange pageRange = new PageRange(Math.min(fromIndex, toIndex), + Math.max(fromIndex, toIndex)); + pageRanges.add(pageRange); + } + + PageRange[] pageRangesArray = new PageRange[pageRanges.size()]; + pageRanges.toArray(pageRangesArray); + + return PageRangeUtils.normalize(pageRangesArray); + } + + return ALL_PAGES_ARRAY; + } + + private boolean hasErrors() { + return (mCopiesEditText.getError() != null) + || (mPageRangeEditText.getVisibility() == View.VISIBLE + && mPageRangeEditText.getError() != null); + } + + public void onPrinterAvailable(PrinterInfo printer) { + PrinterInfo currentPrinter = getCurrentPrinter(); + if (currentPrinter.equals(printer)) { + mState = STATE_CONFIGURING; + if (canUpdateDocument()) { + updateDocument(true, false); + } + ensurePreviewUiShown(); + updateOptionsUi(); + } + } + + public void onPrinterUnavailable(PrinterInfo printer) { + if (getCurrentPrinter().getId().equals(printer.getId())) { + mState = STATE_PRINTER_UNAVAILABLE; + if (mPrintedDocument.isUpdating()) { + mPrintedDocument.cancel(); + } + ensureErrorUiShown(getString(R.string.print_error_printer_unavailable), + PrintErrorFragment.ACTION_NONE); + updateOptionsUi(); + } + } + + private final class SpinnerItem<T> { + final T value; + final CharSequence label; + + public SpinnerItem(T value, CharSequence label) { + this.value = value; + this.label = label; + } + + public String toString() { + return label.toString(); + } + } + + private final class PrinterAvailabilityDetector implements Runnable { + private static final long UNAVAILABLE_TIMEOUT_MILLIS = 10000; // 10sec + + private boolean mPosted; + + private boolean mPrinterUnavailable; + + private PrinterInfo mPrinter; + + public void updatePrinter(PrinterInfo printer) { + if (printer.equals(mDestinationSpinnerAdapter.getPdfPrinter())) { + return; + } + + final boolean available = printer.getStatus() != PrinterInfo.STATUS_UNAVAILABLE + && printer.getCapabilities() != null; + final boolean notifyIfAvailable; + + if (mPrinter == null || !mPrinter.getId().equals(printer.getId())) { + notifyIfAvailable = true; + unpostIfNeeded(); + mPrinterUnavailable = false; + mPrinter = new PrinterInfo.Builder(printer).build(); + } else { + notifyIfAvailable = + (mPrinter.getStatus() == PrinterInfo.STATUS_UNAVAILABLE + && printer.getStatus() != PrinterInfo.STATUS_UNAVAILABLE) + || (mPrinter.getCapabilities() == null + && printer.getCapabilities() != null); + mPrinter.copyFrom(printer); + } + + if (available) { + unpostIfNeeded(); + mPrinterUnavailable = false; + if (notifyIfAvailable) { + onPrinterAvailable(mPrinter); + } + } else { + if (!mPrinterUnavailable) { + postIfNeeded(); + } + } + } + + public void cancel() { + unpostIfNeeded(); + mPrinterUnavailable = false; + } + + private void postIfNeeded() { + if (!mPosted) { + mPosted = true; + mDestinationSpinner.postDelayed(this, UNAVAILABLE_TIMEOUT_MILLIS); + } + } + + private void unpostIfNeeded() { + if (mPosted) { + mPosted = false; + mDestinationSpinner.removeCallbacks(this); + } + } + + @Override + public void run() { + mPosted = false; + mPrinterUnavailable = true; + onPrinterUnavailable(mPrinter); + } + } + + private static final class PrinterHolder { + PrinterInfo printer; + boolean removed; + + public PrinterHolder(PrinterInfo printer) { + this.printer = printer; + } + } + + private final class DestinationAdapter extends BaseAdapter + implements PrinterRegistry.OnPrintersChangeListener { + private final List<PrinterHolder> mPrinterHolders = new ArrayList<>(); + + private final PrinterHolder mFakePdfPrinterHolder; + + public DestinationAdapter() { + addPrinters(mPrinterHolders, mPrinterRegistry.getPrinters()); + mPrinterRegistry.setOnPrintersChangeListener(this); + mFakePdfPrinterHolder = new PrinterHolder(createFakePdfPrinter()); + } + + public PrinterInfo getPdfPrinter() { + return mFakePdfPrinterHolder.printer; + } + + public int getPrinterIndex(PrinterId printerId) { + for (int i = 0; i < getCount(); i++) { + PrinterHolder printerHolder = (PrinterHolder) getItem(i); + if (printerHolder != null && !printerHolder.removed + && printerHolder.printer.getId().equals(printerId)) { + return i; + } + } + return AdapterView.INVALID_POSITION; + } + + public void ensurePrinterInVisibleAdapterPosition(PrinterId printerId) { + final int printerCount = mPrinterHolders.size(); + for (int i = 0; i < printerCount; i++) { + PrinterHolder printerHolder = mPrinterHolders.get(i); + if (printerHolder.printer.getId().equals(printerId)) { + // If already in the list - do nothing. + if (i < getCount() - 2) { + return; + } + // Else replace the last one (two items are not printers). + final int lastPrinterIndex = getCount() - 3; + mPrinterHolders.set(i, mPrinterHolders.get(lastPrinterIndex)); + mPrinterHolders.set(lastPrinterIndex, printerHolder); + notifyDataSetChanged(); + return; + } + } + } + + @Override + public int getCount() { + return Math.min(mPrinterHolders.size() + 2, DEST_ADAPTER_MAX_ITEM_COUNT); + } + + @Override + public boolean isEnabled(int position) { + Object item = getItem(position); + if (item instanceof PrinterHolder) { + PrinterHolder printerHolder = (PrinterHolder) item; + return !printerHolder.removed + && printerHolder.printer.getStatus() != PrinterInfo.STATUS_UNAVAILABLE; + } + return true; + } + + @Override + public Object getItem(int position) { + if (mPrinterHolders.isEmpty()) { + if (position == 0) { + return mFakePdfPrinterHolder; + } + } else { + if (position < 1) { + return mPrinterHolders.get(position); + } + if (position == 1) { + return mFakePdfPrinterHolder; + } + if (position < getCount() - 1) { + return mPrinterHolders.get(position - 1); + } + } + return null; + } + + @Override + public long getItemId(int position) { + if (mPrinterHolders.isEmpty()) { + if (position == 0) { + return DEST_ADAPTER_ITEM_ID_SAVE_AS_PDF; + } else if (position == 1) { + return DEST_ADAPTER_ITEM_ID_ALL_PRINTERS; + } + } else { + if (position == 1) { + return DEST_ADAPTER_ITEM_ID_SAVE_AS_PDF; + } + if (position == getCount() - 1) { + return DEST_ADAPTER_ITEM_ID_ALL_PRINTERS; + } + } + return position; + } + + @Override + public View getDropDownView(int position, View convertView, ViewGroup parent) { + View view = getView(position, convertView, parent); + view.setEnabled(isEnabled(position)); + return view; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + if (convertView == null) { + convertView = getLayoutInflater().inflate( + R.layout.printer_dropdown_item, parent, false); + } + + CharSequence title = null; + CharSequence subtitle = null; + Drawable icon = null; + + if (mPrinterHolders.isEmpty()) { + if (position == 0 && getPdfPrinter() != null) { + PrinterHolder printerHolder = (PrinterHolder) getItem(position); + title = printerHolder.printer.getName(); + icon = getResources().getDrawable(com.android.internal.R.drawable.ic_menu_save); + } else if (position == 1) { + title = getString(R.string.all_printers); + } + } else { + if (position == 1 && getPdfPrinter() != null) { + PrinterHolder printerHolder = (PrinterHolder) getItem(position); + title = printerHolder.printer.getName(); + icon = getResources().getDrawable(com.android.internal.R.drawable.ic_menu_save); + } else if (position == getCount() - 1) { + title = getString(R.string.all_printers); + } else { + PrinterHolder printerHolder = (PrinterHolder) getItem(position); + title = printerHolder.printer.getName(); + try { + PackageInfo packageInfo = getPackageManager().getPackageInfo( + printerHolder.printer.getId().getServiceName().getPackageName(), 0); + subtitle = packageInfo.applicationInfo.loadLabel(getPackageManager()); + icon = packageInfo.applicationInfo.loadIcon(getPackageManager()); + } catch (NameNotFoundException nnfe) { + /* ignore */ + } + } + } + + TextView titleView = (TextView) convertView.findViewById(R.id.title); + titleView.setText(title); + + TextView subtitleView = (TextView) convertView.findViewById(R.id.subtitle); + if (!TextUtils.isEmpty(subtitle)) { + subtitleView.setText(subtitle); + subtitleView.setVisibility(View.VISIBLE); + } else { + subtitleView.setText(null); + subtitleView.setVisibility(View.GONE); + } + + ImageView iconView = (ImageView) convertView.findViewById(R.id.icon); + if (icon != null) { + iconView.setImageDrawable(icon); + iconView.setVisibility(View.VISIBLE); + } else { + iconView.setVisibility(View.INVISIBLE); + } + + return convertView; + } + + @Override + public void onPrintersChanged(List<PrinterInfo> printers) { + // We rearrange the printers if the user selects a printer + // not shown in the initial short list. Therefore, we have + // to keep the printer order. + + // No old printers - do not bother keeping their position. + if (mPrinterHolders.isEmpty()) { + addPrinters(mPrinterHolders, printers); + notifyDataSetChanged(); + return; + } + + // Add the new printers to a map. + ArrayMap<PrinterId, PrinterInfo> newPrintersMap = new ArrayMap<>(); + final int printerCount = printers.size(); + for (int i = 0; i < printerCount; i++) { + PrinterInfo printer = printers.get(i); + newPrintersMap.put(printer.getId(), printer); + } + + List<PrinterHolder> newPrinterHolders = new ArrayList<>(); + + // Update printers we already have which are either updated or removed. + // We do not remove printers if the currently selected printer is removed + // to prevent the user printing to a wrong printer. + final int oldPrinterCount = mPrinterHolders.size(); + for (int i = 0; i < oldPrinterCount; i++) { + PrinterHolder printerHolder = mPrinterHolders.get(i); + PrinterId oldPrinterId = printerHolder.printer.getId(); + PrinterInfo updatedPrinter = newPrintersMap.remove(oldPrinterId); + if (updatedPrinter != null) { + printerHolder.printer = updatedPrinter; + } else { + printerHolder.removed = true; + } + newPrinterHolders.add(printerHolder); + } + + // Add the rest of the new printers, i.e. what is left. + addPrinters(newPrinterHolders, newPrintersMap.values()); + + mPrinterHolders.clear(); + mPrinterHolders.addAll(newPrinterHolders); + + notifyDataSetChanged(); + } + + @Override + public void onPrintersInvalid() { + mPrinterHolders.clear(); + notifyDataSetInvalidated(); + } + + public PrinterHolder getPrinterHolder(PrinterId printerId) { + final int itemCount = getCount(); + for (int i = 0; i < itemCount; i++) { + Object item = getItem(i); + if (item instanceof PrinterHolder) { + PrinterHolder printerHolder = (PrinterHolder) item; + if (printerId.equals(printerHolder.printer.getId())) { + return printerHolder; + } + } + } + return null; + } + + public void pruneRemovedPrinters() { + final int holderCounts = mPrinterHolders.size(); + for (int i = holderCounts - 1; i >= 0; i--) { + PrinterHolder printerHolder = mPrinterHolders.get(i); + if (printerHolder.removed) { + mPrinterHolders.remove(i); + } + } + } + + private void addPrinters(List<PrinterHolder> list, Collection<PrinterInfo> printers) { + for (PrinterInfo printer : printers) { + PrinterHolder printerHolder = new PrinterHolder(printer); + list.add(printerHolder); + } + } + + private PrinterInfo createFakePdfPrinter() { + MediaSize defaultMediaSize = MediaSizeUtils.getDefault(PrintActivity.this); + + PrinterId printerId = new PrinterId(getComponentName(), "PDF printer"); + + PrinterCapabilitiesInfo.Builder builder = + new PrinterCapabilitiesInfo.Builder(printerId); + + String[] mediaSizeIds = getResources().getStringArray(R.array.pdf_printer_media_sizes); + final int mediaSizeIdCount = mediaSizeIds.length; + for (int i = 0; i < mediaSizeIdCount; i++) { + String id = mediaSizeIds[i]; + MediaSize mediaSize = MediaSize.getStandardMediaSizeById(id); + builder.addMediaSize(mediaSize, mediaSize.equals(defaultMediaSize)); + } + + builder.addResolution(new Resolution("PDF resolution", "PDF resolution", 300, 300), + true); + builder.setColorModes(PrintAttributes.COLOR_MODE_COLOR + | PrintAttributes.COLOR_MODE_MONOCHROME, PrintAttributes.COLOR_MODE_COLOR); + + return new PrinterInfo.Builder(printerId, getString(R.string.save_as_pdf), + PrinterInfo.STATUS_IDLE).setCapabilities(builder.build()).build(); + } + } + + private final class PrintersObserver extends DataSetObserver { + @Override + public void onChanged() { + PrinterInfo oldPrinterState = mOldCurrentPrinter; + if (oldPrinterState == null) { + return; + } + + PrinterHolder printerHolder = mDestinationSpinnerAdapter.getPrinterHolder( + oldPrinterState.getId()); + if (printerHolder == null) { + return; + } + PrinterInfo newPrinterState = printerHolder.printer; + + if (!printerHolder.removed) { + mDestinationSpinnerAdapter.pruneRemovedPrinters(); + } else { + onPrinterUnavailable(newPrinterState); + } + + if (oldPrinterState.equals(newPrinterState)) { + return; + } + + PrinterCapabilitiesInfo oldCapab = oldPrinterState.getCapabilities(); + PrinterCapabilitiesInfo newCapab = newPrinterState.getCapabilities(); + + final boolean hasCapab = newCapab != null; + final boolean gotCapab = oldCapab == null && newCapab != null; + final boolean lostCapab = oldCapab != null && newCapab == null; + final boolean capabChanged = capabilitiesChanged(oldCapab, newCapab); + + final int oldStatus = oldPrinterState.getStatus(); + final int newStatus = newPrinterState.getStatus(); + + final boolean isActive = newStatus != PrinterInfo.STATUS_UNAVAILABLE; + final boolean becameActive = (oldStatus == PrinterInfo.STATUS_UNAVAILABLE + && oldStatus != newStatus); + final boolean becameInactive = (newStatus == PrinterInfo.STATUS_UNAVAILABLE + && oldStatus != newStatus); + + mPrinterAvailabilityDetector.updatePrinter(newPrinterState); + + oldPrinterState.copyFrom(newPrinterState); + + if ((isActive && gotCapab) || (becameActive && hasCapab)) { + onPrinterAvailable(newPrinterState); + } else if ((becameInactive && hasCapab)|| (isActive && lostCapab)) { + onPrinterUnavailable(newPrinterState); + } + + if (hasCapab && capabChanged) { + updatePrintAttributesFromCapabilities(newCapab); + } + + final boolean updateNeeded = ((capabChanged && hasCapab && isActive) + || (becameActive && hasCapab) || (isActive && gotCapab)); + + if (updateNeeded && canUpdateDocument()) { + updateDocument(true, false); + } + + updateOptionsUi(); + } + + private boolean capabilitiesChanged(PrinterCapabilitiesInfo oldCapabilities, + PrinterCapabilitiesInfo newCapabilities) { + if (oldCapabilities == null) { + if (newCapabilities != null) { + return true; + } + } else if (!oldCapabilities.equals(newCapabilities)) { + return true; + } + return false; + } + } + + private final class MyOnItemSelectedListener implements AdapterView.OnItemSelectedListener { + @Override + public void onItemSelected(AdapterView<?> spinner, View view, int position, long id) { + if (spinner == mDestinationSpinner) { + if (position == AdapterView.INVALID_POSITION) { + return; + } + + if (id == DEST_ADAPTER_ITEM_ID_ALL_PRINTERS) { + startSelectPrinterActivity(); + return; + } + + PrinterInfo currentPrinter = getCurrentPrinter(); + + // Why on earth item selected is called if no selection changed. + if (mOldCurrentPrinter == currentPrinter) { + return; + } + mOldCurrentPrinter = currentPrinter; + + PrinterHolder printerHolder = mDestinationSpinnerAdapter.getPrinterHolder( + currentPrinter.getId()); + if (!printerHolder.removed) { + mDestinationSpinnerAdapter.pruneRemovedPrinters(); + ensurePreviewUiShown(); + } + + mPrintJob.setPrinterId(currentPrinter.getId()); + mPrintJob.setPrinterName(currentPrinter.getName()); + + mPrinterRegistry.setTrackedPrinter(currentPrinter.getId()); + + PrinterCapabilitiesInfo capabilities = currentPrinter.getCapabilities(); + if (capabilities != null) { + updatePrintAttributesFromCapabilities(capabilities); + } + + mPrinterAvailabilityDetector.updatePrinter(currentPrinter); + } else if (spinner == mMediaSizeSpinner) { + SpinnerItem<MediaSize> mediaItem = mMediaSizeSpinnerAdapter.getItem(position); + if (mOrientationSpinner.getSelectedItemPosition() == 0) { + mPrintJob.getAttributes().setMediaSize(mediaItem.value.asPortrait()); + } else { + mPrintJob.getAttributes().setMediaSize(mediaItem.value.asLandscape()); + } + } else if (spinner == mColorModeSpinner) { + SpinnerItem<Integer> colorModeItem = mColorModeSpinnerAdapter.getItem(position); + mPrintJob.getAttributes().setColorMode(colorModeItem.value); + } else if (spinner == mOrientationSpinner) { + SpinnerItem<Integer> orientationItem = mOrientationSpinnerAdapter.getItem(position); + PrintAttributes attributes = mPrintJob.getAttributes(); + if (orientationItem.value == ORIENTATION_PORTRAIT) { + attributes.copyFrom(attributes.asPortrait()); + } else { + attributes.copyFrom(attributes.asLandscape()); + } + } + + if (canUpdateDocument()) { + updateDocument(true, false); + } + + updateOptionsUi(); + } + + @Override + public void onNothingSelected(AdapterView<?> parent) { + /* do nothing*/ + } + } + + private boolean canUpdateDocument() { + if (mPrintedDocument.isDestroyed()) { + return false; + } + + if (hasErrors()) { + return false; + } + + PrintAttributes attributes = mPrintJob.getAttributes(); + + final int colorMode = attributes.getColorMode(); + if (colorMode != PrintAttributes.COLOR_MODE_COLOR + && colorMode != PrintAttributes.COLOR_MODE_MONOCHROME) { + return false; + } + if (attributes.getMediaSize() == null) { + return false; + } + if (attributes.getMinMargins() == null) { + return false; + } + if (attributes.getResolution() == null) { + return false; + } + + PrinterInfo currentPrinter = getCurrentPrinter(); + if (currentPrinter == null) { + return false; + } + PrinterCapabilitiesInfo capabilities = currentPrinter.getCapabilities(); + if (capabilities == null) { + return false; + } + if (currentPrinter.getStatus() == PrinterInfo.STATUS_UNAVAILABLE) { + return false; + } + + return true; + } + + private final class SelectAllOnFocusListener implements OnFocusChangeListener { + @Override + public void onFocusChange(View view, boolean hasFocus) { + EditText editText = (EditText) view; + if (!TextUtils.isEmpty(editText.getText())) { + editText.setSelection(editText.getText().length()); + } + } + } + + private final class RangeTextWatcher implements TextWatcher { + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + /* do nothing */ + } + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + /* do nothing */ + } + + @Override + public void afterTextChanged(Editable editable) { + final boolean hadErrors = hasErrors(); + + String text = editable.toString(); + + if (TextUtils.isEmpty(text)) { + mPageRangeEditText.setError(""); + updateOptionsUi(); + return; + } + + String escapedText = PATTERN_ESCAPE_SPECIAL_CHARS.matcher(text).replaceAll("////"); + if (!PATTERN_PAGE_RANGE.matcher(escapedText).matches()) { + mPageRangeEditText.setError(""); + updateOptionsUi(); + return; + } + + PrintDocumentInfo info = mPrintedDocument.getDocumentInfo().info; + final int pageCount = (info != null) ? info.getPageCount() : 0; + + // The range + Matcher matcher = PATTERN_DIGITS.matcher(text); + while (matcher.find()) { + String numericString = text.substring(matcher.start(), matcher.end()).trim(); + if (TextUtils.isEmpty(numericString)) { + continue; + } + final int pageIndex = Integer.parseInt(numericString); + if (pageIndex < 1 || pageIndex > pageCount) { + mPageRangeEditText.setError(""); + updateOptionsUi(); + return; + } + } + + // We intentionally do not catch the case of the from page being + // greater than the to page. When computing the requested pages + // we just swap them if necessary. + + // Keep the print job up to date with the selected pages if we + // know how many pages are there in the document. + mRequestedPages = computeRequestedPages(); + + mPageRangeEditText.setError(null); + mPrintButton.setEnabled(true); + updateOptionsUi(); + + if (hadErrors && !hasErrors()) { + updateOptionsUi(); + } + } + } + + private final class EditTextWatcher implements TextWatcher { + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + /* do nothing */ + } + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + /* do nothing */ + } + + @Override + public void afterTextChanged(Editable editable) { + final boolean hadErrors = hasErrors(); + + if (editable.length() == 0) { + mCopiesEditText.setError(""); + updateOptionsUi(); + return; + } + + int copies = 0; + try { + copies = Integer.parseInt(editable.toString()); + } catch (NumberFormatException nfe) { + /* ignore */ + } + + if (copies < MIN_COPIES) { + mCopiesEditText.setError(""); + updateOptionsUi(); + return; + } + + mPrintJob.setCopies(copies); + + mCopiesEditText.setError(null); + + updateOptionsUi(); + + if (hadErrors && canUpdateDocument()) { + updateDocument(true, false); + } + } + } + + private final class ProgressMessageController implements Runnable { + private static final long PROGRESS_TIMEOUT_MILLIS = 1000; + + private final Handler mHandler; + + private boolean mPosted; + + public ProgressMessageController(Context context) { + mHandler = new Handler(context.getMainLooper(), null, false); + } + + public void post() { + if (mPosted) { + return; + } + mPosted = true; + mHandler.postDelayed(this, PROGRESS_TIMEOUT_MILLIS); + } + + public void cancel() { + if (!mPosted) { + return; + } + mPosted = false; + mHandler.removeCallbacks(this); + } + + @Override + public void run() { + ensureProgressUiShown(); + } + } +} diff --git a/packages/PrintSpooler/src/com/android/printspooler/ui/PrintErrorFragment.java b/packages/PrintSpooler/src/com/android/printspooler/ui/PrintErrorFragment.java new file mode 100644 index 0000000..b708356 --- /dev/null +++ b/packages/PrintSpooler/src/com/android/printspooler/ui/PrintErrorFragment.java @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2014 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.ui; + +import android.app.Activity; +import android.app.Fragment; +import android.os.Bundle; +import android.text.TextUtils; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.View.OnClickListener; +import android.widget.Button; +import android.widget.TextView; + +import com.android.printspooler.R; + +/** + * Fragment for showing an error UI. + */ +public final class PrintErrorFragment extends Fragment { + public static final int ACTION_NONE = 0; + public static final int ACTION_RETRY = 1; + public static final int ACTION_CONFIRM = 2; + + public interface OnActionListener { + public void onActionPerformed(); + } + + private static final String EXTRA_ERROR_MESSAGE = "EXTRA_ERROR_MESSAGE"; + private static final String EXTRA_ACTION = "EXTRA_ACTION"; + + public static PrintErrorFragment newInstance(CharSequence errorMessage, int action) { + PrintErrorFragment instance = new PrintErrorFragment(); + Bundle arguments = new Bundle(); + arguments.putCharSequence(EXTRA_ERROR_MESSAGE, errorMessage); + arguments.putInt(EXTRA_ACTION, action); + instance.setArguments(arguments); + return instance; + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup root, + Bundle savedInstanceState) { + return inflater.inflate(R.layout.print_error_fragment, root, false); + } + + @Override + public void onViewCreated(View view, Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + Bundle arguments = getArguments(); + + CharSequence error = arguments.getString(EXTRA_ERROR_MESSAGE); + if (!TextUtils.isEmpty(error)) { + TextView message = (TextView) view.findViewById(R.id.message); + message.setText(error); + } + + Button actionButton = (Button) view.findViewById(R.id.action_button); + + final int action = getArguments().getInt(EXTRA_ACTION); + switch (action) { + case ACTION_RETRY: { + actionButton.setVisibility(View.VISIBLE); + actionButton.setText(R.string.print_error_retry); + } break; + + case ACTION_CONFIRM: { + actionButton.setVisibility(View.VISIBLE); + actionButton.setText(android.R.string.ok); + } break; + + case ACTION_NONE: { + actionButton.setVisibility(View.GONE); + } break; + } + + actionButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View view) { + Activity activity = getActivity(); + if (activity instanceof OnActionListener) { + ((OnActionListener) getActivity()).onActionPerformed(); + } + } + }); + } +} diff --git a/packages/PrintSpooler/src/com/android/printspooler/ui/PrintPreviewFragment.java b/packages/PrintSpooler/src/com/android/printspooler/ui/PrintPreviewFragment.java new file mode 100644 index 0000000..d68a6aa --- /dev/null +++ b/packages/PrintSpooler/src/com/android/printspooler/ui/PrintPreviewFragment.java @@ -0,0 +1,12 @@ +package com.android.printspooler.ui; + +import android.app.Fragment; + +public class PrintPreviewFragment extends Fragment { + + public static PrintPreviewFragment newInstance() { + return new PrintPreviewFragment(); + } + + // TODO: Implement +} diff --git a/packages/PrintSpooler/src/com/android/printspooler/ui/PrintProgressFragment.java b/packages/PrintSpooler/src/com/android/printspooler/ui/PrintProgressFragment.java new file mode 100644 index 0000000..96aa153 --- /dev/null +++ b/packages/PrintSpooler/src/com/android/printspooler/ui/PrintProgressFragment.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2014 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.ui; + +import android.app.Activity; +import android.app.Fragment; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.View.OnClickListener; +import android.widget.Button; + +import android.widget.TextView; +import com.android.printspooler.R; + +/** + * Fragment for showing a work in progress UI. + */ +public final class PrintProgressFragment extends Fragment { + + public interface OnCancelRequestListener { + public void onCancelRequest(); + } + + public static PrintProgressFragment newInstance() { + return new PrintProgressFragment(); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup root, + Bundle savedInstanceState) { + return inflater.inflate(R.layout.print_progress_fragment, root, false); + } + + @Override + public void onViewCreated(View view, Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + final Button cancelButton = (Button) view.findViewById(R.id.cancel_button); + final TextView message = (TextView) view.findViewById(R.id.message); + + cancelButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View view) { + Activity activity = getActivity(); + if (activity instanceof OnCancelRequestListener) { + ((OnCancelRequestListener) getActivity()).onCancelRequest(); + } + cancelButton.setVisibility(View.GONE); + message.setVisibility(View.VISIBLE); + } + }); + } +} diff --git a/packages/PrintSpooler/src/com/android/printspooler/ui/PrinterRegistry.java b/packages/PrintSpooler/src/com/android/printspooler/ui/PrinterRegistry.java new file mode 100644 index 0000000..7816d66 --- /dev/null +++ b/packages/PrintSpooler/src/com/android/printspooler/ui/PrinterRegistry.java @@ -0,0 +1,168 @@ +/* + * Copyright (C) 2014 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.ui; + +import android.app.Activity; +import android.app.LoaderManager.LoaderCallbacks; +import android.content.Loader; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.print.PrinterId; +import android.print.PrinterInfo; +import com.android.internal.os.SomeArgs; + +import java.util.ArrayList; +import java.util.List; + +public class PrinterRegistry { + + private static final int LOADER_ID_PRINTERS_LOADER = 1; + + private final Activity mActivity; + + private final List<PrinterInfo> mPrinters = new ArrayList<>(); + + private final Runnable mReadyCallback; + + private final Handler mHandler; + + private boolean mReady; + + private OnPrintersChangeListener mOnPrintersChangeListener; + + public interface OnPrintersChangeListener { + public void onPrintersChanged(List<PrinterInfo> printers); + public void onPrintersInvalid(); + } + + public PrinterRegistry(Activity activity, Runnable readyCallback) { + mActivity = activity; + mReadyCallback = readyCallback; + mHandler = new MyHandler(activity.getMainLooper()); + activity.getLoaderManager().initLoader(LOADER_ID_PRINTERS_LOADER, + null, mLoaderCallbacks); + } + + public void setOnPrintersChangeListener(OnPrintersChangeListener listener) { + mOnPrintersChangeListener = listener; + } + + public List<PrinterInfo> getPrinters() { + return mPrinters; + } + + public void addHistoricalPrinter(PrinterInfo printer) { + getPrinterProvider().addHistoricalPrinter(printer); + } + + public void forgetFavoritePrinter(PrinterId printerId) { + getPrinterProvider().forgetFavoritePrinter(printerId); + } + + public boolean isFavoritePrinter(PrinterId printerId) { + return getPrinterProvider().isFavoritePrinter(printerId); + } + + public void setTrackedPrinter(PrinterId printerId) { + getPrinterProvider().setTrackedPrinter(printerId); + } + + private FusedPrintersProvider getPrinterProvider() { + Loader<?> loader = mActivity.getLoaderManager().getLoader(LOADER_ID_PRINTERS_LOADER); + return (FusedPrintersProvider) loader; + } + + private final LoaderCallbacks<List<PrinterInfo>> mLoaderCallbacks = + new LoaderCallbacks<List<PrinterInfo>>() { + @Override + public void onLoaderReset(Loader<List<PrinterInfo>> loader) { + if (loader.getId() == LOADER_ID_PRINTERS_LOADER) { + mPrinters.clear(); + if (mOnPrintersChangeListener != null) { + // Post a message as we are in onLoadFinished and certain operations + // are not allowed in this callback, such as fragment transactions. + // Clients should not handle this explicitly. + mHandler.obtainMessage(MyHandler.MSG_PRINTERS_INVALID, + mOnPrintersChangeListener).sendToTarget(); + } + } + } + + // LoaderCallbacks#onLoadFinished + @Override + public void onLoadFinished(Loader<List<PrinterInfo>> loader, List<PrinterInfo> printers) { + if (loader.getId() == LOADER_ID_PRINTERS_LOADER) { + mPrinters.clear(); + mPrinters.addAll(printers); + if (mOnPrintersChangeListener != null) { + // Post a message as we are in onLoadFinished and certain operations + // are not allowed in this callback, such as fragment transactions. + // Clients should not handle this explicitly. + SomeArgs args = SomeArgs.obtain(); + args.arg1 = mOnPrintersChangeListener; + args.arg2 = printers; + mHandler.obtainMessage(MyHandler.MSG_PRINTERS_CHANGED, args).sendToTarget(); + } + if (!mReady) { + mReady = true; + if (mReadyCallback != null) { + mReadyCallback.run(); + } + } + } + } + + // LoaderCallbacks#onCreateLoader + @Override + public Loader<List<PrinterInfo>> onCreateLoader(int id, Bundle args) { + if (id == LOADER_ID_PRINTERS_LOADER) { + return new FusedPrintersProvider(mActivity); + } + return null; + } + }; + + private static final class MyHandler extends Handler { + public static final int MSG_PRINTERS_CHANGED = 0; + public static final int MSG_PRINTERS_INVALID = 1; + + public MyHandler(Looper looper) { + super(looper, null , false); + } + + @Override + @SuppressWarnings("unchecked") + public void handleMessage(Message message) { + switch (message.what) { + case MSG_PRINTERS_CHANGED: { + SomeArgs args = (SomeArgs) message.obj; + OnPrintersChangeListener callback = (OnPrintersChangeListener) args.arg1; + List<PrinterInfo> printers = (List<PrinterInfo>) args.arg2; + args.recycle(); + callback.onPrintersChanged(printers); + } break; + + case MSG_PRINTERS_INVALID: { + OnPrintersChangeListener callback = (OnPrintersChangeListener) message.obj; + callback.onPrintersInvalid(); + } break; + } + } + } +} diff --git a/packages/PrintSpooler/src/com/android/printspooler/SelectPrinterFragment.java b/packages/PrintSpooler/src/com/android/printspooler/ui/SelectPrinterActivity.java index fe5920c..7715579 100644 --- a/packages/PrintSpooler/src/com/android/printspooler/SelectPrinterFragment.java +++ b/packages/PrintSpooler/src/com/android/printspooler/ui/SelectPrinterActivity.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.printspooler; +package com.android.printspooler.ui; import android.app.Activity; import android.app.AlertDialog; @@ -22,13 +22,11 @@ import android.app.Dialog; import android.app.DialogFragment; import android.app.Fragment; import android.app.FragmentTransaction; -import android.app.LoaderManager; import android.content.ActivityNotFoundException; import android.content.ComponentName; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; -import android.content.Loader; import android.content.pm.ActivityInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; @@ -48,9 +46,7 @@ import android.text.TextUtils; import android.util.Log; import android.view.ContextMenu; import android.view.ContextMenu.ContextMenuInfo; -import android.view.LayoutInflater; import android.view.Menu; -import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; @@ -66,63 +62,60 @@ import android.widget.ListView; import android.widget.SearchView; import android.widget.TextView; +import com.android.printspooler.R; + import java.util.ArrayList; import java.util.List; /** - * This is a fragment for selecting a printer. + * This is an activity for selecting a printer. */ -public final class SelectPrinterFragment extends Fragment { +public final class SelectPrinterActivity extends Activity { private static final String LOG_TAG = "SelectPrinterFragment"; - private static final int LOADER_ID_PRINTERS_LOADER = 1; + public static final String INTENT_EXTRA_PRINTER_ID = "INTENT_EXTRA_PRINTER_ID"; - private static final String FRAGMRNT_TAG_ADD_PRINTER_DIALOG = - "FRAGMRNT_TAG_ADD_PRINTER_DIALOG"; + private static final String FRAGMENT_TAG_ADD_PRINTER_DIALOG = + "FRAGMENT_TAG_ADD_PRINTER_DIALOG"; - private static final String FRAGMRNT_ARGUMENT_PRINT_SERVICE_INFOS = - "FRAGMRNT_ARGUMENT_PRINT_SERVICE_INFOS"; + private static final String FRAGMENT_ARGUMENT_PRINT_SERVICE_INFOS = + "FRAGMENT_ARGUMENT_PRINT_SERVICE_INFOS"; private static final String EXTRA_PRINTER_ID = "EXTRA_PRINTER_ID"; private final ArrayList<PrintServiceInfo> mAddPrinterServices = - new ArrayList<PrintServiceInfo>(); + new ArrayList<>(); + + private PrinterRegistry mPrinterRegistry; private ListView mListView; private AnnounceFilterResult mAnnounceFilterResult; - public static interface OnPrinterSelectedListener { - public void onPrinterSelected(PrinterId printerId); - } - @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - setHasOptionsMenu(true); - getActivity().getActionBar().setIcon(R.drawable.ic_menu_print); - } + getActionBar().setIcon(R.drawable.ic_menu_print); - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { - View content = inflater.inflate(R.layout.select_printer_fragment, container, false); + setContentView(R.layout.select_printer_activity); + + mPrinterRegistry = new PrinterRegistry(this, null); // Hook up the list view. - mListView = (ListView) content.findViewById(android.R.id.list); + mListView = (ListView) findViewById(android.R.id.list); final DestinationAdapter adapter = new DestinationAdapter(); adapter.registerDataSetObserver(new DataSetObserver() { @Override public void onChanged() { - if (!getActivity().isFinishing() && adapter.getCount() <= 0) { + if (!isFinishing() && adapter.getCount() <= 0) { updateEmptyView(adapter); } } @Override public void onInvalidated() { - if (!getActivity().isFinishing()) { + if (!isFinishing()) { updateEmptyView(adapter); } } @@ -135,26 +128,20 @@ public final class SelectPrinterFragment extends Fragment { if (!((DestinationAdapter) mListView.getAdapter()).isActionable(position)) { return; } + PrinterInfo printer = (PrinterInfo) mListView.getAdapter().getItem(position); - Activity activity = getActivity(); - if (activity instanceof OnPrinterSelectedListener) { - ((OnPrinterSelectedListener) activity).onPrinterSelected(printer.getId()); - } else { - throw new IllegalStateException("the host activity must implement" - + " OnPrinterSelectedListener"); - } + onPrinterSelected(printer.getId()); } }); registerForContextMenu(mListView); - - return content; } @Override - public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { - super.onCreateOptionsMenu(menu, inflater); - inflater.inflate(R.menu.select_printer_activity, menu); + public boolean onCreateOptionsMenu(Menu menu) { + super.onCreateOptionsMenu(menu); + + getMenuInflater().inflate(R.menu.select_printer_activity, menu); MenuItem searchItem = menu.findItem(R.id.action_search); SearchView searchView = (SearchView) searchItem.getActionView(); @@ -173,16 +160,15 @@ public final class SelectPrinterFragment extends Fragment { searchView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() { @Override public void onViewAttachedToWindow(View view) { - if (AccessibilityManager.getInstance(getActivity()).isEnabled()) { + if (AccessibilityManager.getInstance(SelectPrinterActivity.this).isEnabled()) { view.announceForAccessibility(getString( R.string.print_search_box_shown_utterance)); } } @Override public void onViewDetachedFromWindow(View view) { - Activity activity = getActivity(); - if (activity != null && !activity.isFinishing() - && AccessibilityManager.getInstance(activity).isEnabled()) { + if (!isFinishing() && AccessibilityManager.getInstance( + SelectPrinterActivity.this).isEnabled()) { view.announceForAccessibility(getString( R.string.print_search_box_hidden_utterance)); } @@ -192,6 +178,8 @@ public final class SelectPrinterFragment extends Fragment { if (mAddPrinterServices.isEmpty()) { menu.removeItem(R.id.action_add_printer); } + + return true; } @Override @@ -212,9 +200,7 @@ public final class SelectPrinterFragment extends Fragment { } // Add the forget menu item if applicable. - FusedPrintersProvider provider = (FusedPrintersProvider) (Loader<?>) - getLoaderManager().getLoader(LOADER_ID_PRINTERS_LOADER); - if (provider.isFavoritePrinter(printer.getId())) { + if (mPrinterRegistry.isFavoritePrinter(printer.getId())) { MenuItem forgetItem = menu.add(Menu.NONE, R.string.print_forget_printer, Menu.NONE, R.string.print_forget_printer); Intent intent = new Intent(); @@ -228,23 +214,13 @@ public final class SelectPrinterFragment extends Fragment { public boolean onContextItemSelected(MenuItem item) { switch (item.getItemId()) { case R.string.print_select_printer: { - PrinterId printerId = (PrinterId) item.getIntent().getParcelableExtra( - EXTRA_PRINTER_ID); - Activity activity = getActivity(); - if (activity instanceof OnPrinterSelectedListener) { - ((OnPrinterSelectedListener) activity).onPrinterSelected(printerId); - } else { - throw new IllegalStateException("the host activity must implement" - + " OnPrinterSelectedListener"); - } + PrinterId printerId = item.getIntent().getParcelableExtra(EXTRA_PRINTER_ID); + onPrinterSelected(printerId); } return true; case R.string.print_forget_printer: { - PrinterId printerId = (PrinterId) item.getIntent().getParcelableExtra( - EXTRA_PRINTER_ID); - FusedPrintersProvider provider = (FusedPrintersProvider) (Loader<?>) - getLoaderManager().getLoader(LOADER_ID_PRINTERS_LOADER); - provider.forgetFavoritePrinter(printerId); + PrinterId printerId = item.getIntent().getParcelableExtra(EXTRA_PRINTER_ID); + mPrinterRegistry.forgetFavoritePrinter(printerId); } return true; } return false; @@ -252,9 +228,9 @@ public final class SelectPrinterFragment extends Fragment { @Override public void onResume() { - updateAddPrintersAdapter(); - getActivity().invalidateOptionsMenu(); super.onResume(); + updateServicesWithAddPrinterActivity(); + invalidateOptionsMenu(); } @Override @@ -274,12 +250,18 @@ public final class SelectPrinterFragment extends Fragment { return super.onOptionsItemSelected(item); } - private void updateAddPrintersAdapter() { + private void onPrinterSelected(PrinterId printerId) { + Intent intent = new Intent(); + intent.putExtra(INTENT_EXTRA_PRINTER_ID, printerId); + setResult(RESULT_OK, intent); + finish(); + } + + private void updateServicesWithAddPrinterActivity() { mAddPrinterServices.clear(); // Get all enabled print services. - PrintManager printManager = (PrintManager) getActivity() - .getSystemService(Context.PRINT_SERVICE); + PrintManager printManager = (PrintManager) getSystemService(Context.PRINT_SERVICE); List<PrintServiceInfo> enabledServices = printManager.getEnabledPrintServices(); // No enabled print services - done. @@ -292,7 +274,7 @@ public final class SelectPrinterFragment extends Fragment { for (int i = 0; i < enabledServiceCount; i++) { PrintServiceInfo enabledService = enabledServices.get(i); - // No add printers activity declared - done. + // No add printers activity declared - next. if (TextUtils.isEmpty(enabledService.getAddPrintersActivityName())) { continue; } @@ -304,15 +286,14 @@ public final class SelectPrinterFragment extends Fragment { .setComponent(addPrintersComponentName); // The add printers activity is valid - add it. - PackageManager pm = getActivity().getPackageManager(); + PackageManager pm = getPackageManager(); List<ResolveInfo> resolvedActivities = pm.queryIntentActivities(addPritnersIntent, 0); if (!resolvedActivities.isEmpty()) { // The activity is a component name, therefore it is one or none. ActivityInfo activityInfo = resolvedActivities.get(0).activityInfo; if (activityInfo.exported && (activityInfo.permission == null - || pm.checkPermission(activityInfo.permission, - getActivity().getPackageName()) + || pm.checkPermission(activityInfo.permission, getPackageName()) == PackageManager.PERMISSION_GRANTED)) { mAddPrinterServices.add(enabledService); } @@ -323,26 +304,26 @@ public final class SelectPrinterFragment extends Fragment { private void showAddPrinterSelectionDialog() { FragmentTransaction transaction = getFragmentManager().beginTransaction(); Fragment oldFragment = getFragmentManager().findFragmentByTag( - FRAGMRNT_TAG_ADD_PRINTER_DIALOG); + FRAGMENT_TAG_ADD_PRINTER_DIALOG); if (oldFragment != null) { transaction.remove(oldFragment); } AddPrinterAlertDialogFragment newFragment = new AddPrinterAlertDialogFragment(); Bundle arguments = new Bundle(); - arguments.putParcelableArrayList(FRAGMRNT_ARGUMENT_PRINT_SERVICE_INFOS, + arguments.putParcelableArrayList(FRAGMENT_ARGUMENT_PRINT_SERVICE_INFOS, mAddPrinterServices); newFragment.setArguments(arguments); - transaction.add(newFragment, FRAGMRNT_TAG_ADD_PRINTER_DIALOG); + transaction.add(newFragment, FRAGMENT_TAG_ADD_PRINTER_DIALOG); transaction.commit(); } public void updateEmptyView(DestinationAdapter adapter) { if (mListView.getEmptyView() == null) { - View emptyView = getActivity().findViewById(R.id.empty_print_state); + View emptyView = findViewById(R.id.empty_print_state); mListView.setEmptyView(emptyView); } - TextView titleView = (TextView) getActivity().findViewById(R.id.title); - View progressBar = getActivity().findViewById(R.id.progress_bar); + TextView titleView = (TextView) findViewById(R.id.title); + View progressBar = findViewById(R.id.progress_bar); if (adapter.getUnfilteredCount() <= 0) { titleView.setText(R.string.print_searching_for_printers); progressBar.setVisibility(View.VISIBLE); @@ -353,7 +334,7 @@ public final class SelectPrinterFragment extends Fragment { } private void announceSearchResultIfNeeded() { - if (AccessibilityManager.getInstance(getActivity()).isEnabled()) { + if (AccessibilityManager.getInstance(this).isEnabled()) { if (mAnnounceFilterResult == null) { mAnnounceFilterResult = new AnnounceFilterResult(); } @@ -372,9 +353,9 @@ public final class SelectPrinterFragment extends Fragment { .setTitle(R.string.choose_print_service); final List<PrintServiceInfo> printServices = (List<PrintServiceInfo>) (List<?>) - getArguments().getParcelableArrayList(FRAGMRNT_ARGUMENT_PRINT_SERVICE_INFOS); + getArguments().getParcelableArrayList(FRAGMENT_ARGUMENT_PRINT_SERVICE_INFOS); - final ArrayAdapter<String> adapter = new ArrayAdapter<String>( + final ArrayAdapter<String> adapter = new ArrayAdapter<>( getActivity(), android.R.layout.simple_list_item_1); final int printServiceCount = printServices.size(); for (int i = 0; i < printServiceCount; i++) { @@ -382,32 +363,33 @@ public final class SelectPrinterFragment extends Fragment { adapter.add(printService.getResolveInfo().loadLabel( getActivity().getPackageManager()).toString()); } + final String searchUri = Settings.Secure.getString(getActivity().getContentResolver(), Settings.Secure.PRINT_SERVICE_SEARCH_URI); - final Intent marketIntent; + final Intent viewIntent; if (!TextUtils.isEmpty(searchUri)) { Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(searchUri)); if (getActivity().getPackageManager().resolveActivity(intent, 0) != null) { - marketIntent = intent; + viewIntent = intent; mAddPrintServiceItem = getString(R.string.add_print_service_label); adapter.add(mAddPrintServiceItem); } else { - marketIntent = null; + viewIntent = null; } } else { - marketIntent = null; + viewIntent = null; } builder.setAdapter(adapter, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { String item = adapter.getItem(which); - if (item == mAddPrintServiceItem) { + if (item.equals(mAddPrintServiceItem)) { try { - startActivity(marketIntent); - } catch (ActivityNotFoundException anfe) { - Log.w(LOG_TAG, "Couldn't start add printer activity", anfe); - } + startActivity(viewIntent); + } catch (ActivityNotFoundException anfe) { + Log.w(LOG_TAG, "Couldn't start add printer activity", anfe); + } } else { PrintServiceInfo printService = printServices.get(which); ComponentName componentName = new ComponentName( @@ -418,7 +400,7 @@ public final class SelectPrinterFragment extends Fragment { try { startActivity(intent); } catch (ActivityNotFoundException anfe) { - Log.w(LOG_TAG, "Couldn't start settings activity", anfe); + Log.w(LOG_TAG, "Couldn't start add printer activity", anfe); } } } @@ -428,19 +410,41 @@ public final class SelectPrinterFragment extends Fragment { } } - private final class DestinationAdapter extends BaseAdapter - implements LoaderManager.LoaderCallbacks<List<PrinterInfo>>, Filterable { + private final class DestinationAdapter extends BaseAdapter implements Filterable { private final Object mLock = new Object(); - private final List<PrinterInfo> mPrinters = new ArrayList<PrinterInfo>(); + private final List<PrinterInfo> mPrinters = new ArrayList<>(); - private final List<PrinterInfo> mFilteredPrinters = new ArrayList<PrinterInfo>(); + private final List<PrinterInfo> mFilteredPrinters = new ArrayList<>(); private CharSequence mLastSearchString; public DestinationAdapter() { - getLoaderManager().initLoader(LOADER_ID_PRINTERS_LOADER, null, this); + mPrinterRegistry.setOnPrintersChangeListener(new PrinterRegistry.OnPrintersChangeListener() { + @Override + public void onPrintersChanged(List<PrinterInfo> printers) { + synchronized (mLock) { + mPrinters.clear(); + mPrinters.addAll(printers); + mFilteredPrinters.clear(); + mFilteredPrinters.addAll(printers); + if (!TextUtils.isEmpty(mLastSearchString)) { + getFilter().filter(mLastSearchString); + } + } + notifyDataSetChanged(); + } + + @Override + public void onPrintersInvalid() { + synchronized (mLock) { + mPrinters.clear(); + mFilteredPrinters.clear(); + } + notifyDataSetInvalidated(); + } + }); } @Override @@ -453,7 +457,7 @@ public final class SelectPrinterFragment extends Fragment { return null; } FilterResults results = new FilterResults(); - List<PrinterInfo> filteredPrinters = new ArrayList<PrinterInfo>(); + List<PrinterInfo> filteredPrinters = new ArrayList<>(); String constraintLowerCase = constraint.toString().toLowerCase(); final int printerCount = mPrinters.size(); for (int i = 0; i < printerCount; i++) { @@ -518,28 +522,27 @@ public final class SelectPrinterFragment extends Fragment { } @Override - public View getDropDownView(int position, View convertView, - ViewGroup parent) { + public View getDropDownView(int position, View convertView, ViewGroup parent) { return getView(position, convertView, parent); } @Override public View getView(int position, View convertView, ViewGroup parent) { if (convertView == null) { - convertView = getActivity().getLayoutInflater().inflate( + convertView = getLayoutInflater().inflate( R.layout.printer_list_item, parent, false); } convertView.setEnabled(isActionable(position)); - CharSequence title = null; + PrinterInfo printer = (PrinterInfo) getItem(position); + + CharSequence title = printer.getName(); CharSequence subtitle = null; Drawable icon = null; - PrinterInfo printer = (PrinterInfo) getItem(position); - title = printer.getName(); try { - PackageManager pm = getActivity().getPackageManager(); + PackageManager pm = getPackageManager(); PackageInfo packageInfo = pm.getPackageInfo(printer.getId() .getServiceName().getPackageName(), 0); subtitle = packageInfo.applicationInfo.loadLabel(pm); @@ -576,38 +579,6 @@ public final class SelectPrinterFragment extends Fragment { PrinterInfo printer = (PrinterInfo) getItem(position); return printer.getStatus() != PrinterInfo.STATUS_UNAVAILABLE; } - - @Override - public Loader<List<PrinterInfo>> onCreateLoader(int id, Bundle args) { - if (id == LOADER_ID_PRINTERS_LOADER) { - return new FusedPrintersProvider(getActivity()); - } - return null; - } - - @Override - public void onLoadFinished(Loader<List<PrinterInfo>> loader, - List<PrinterInfo> printers) { - synchronized (mLock) { - mPrinters.clear(); - mPrinters.addAll(printers); - mFilteredPrinters.clear(); - mFilteredPrinters.addAll(printers); - if (!TextUtils.isEmpty(mLastSearchString)) { - getFilter().filter(mLastSearchString); - } - } - notifyDataSetChanged(); - } - - @Override - public void onLoaderReset(Loader<List<PrinterInfo>> loader) { - synchronized (mLock) { - mPrinters.clear(); - mFilteredPrinters.clear(); - } - notifyDataSetInvalidated(); - } } private final class AnnounceFilterResult implements Runnable { @@ -629,7 +600,7 @@ public final class SelectPrinterFragment extends Fragment { if (count <= 0) { text = getString(R.string.print_no_printers); } else { - text = getActivity().getResources().getQuantityString( + text = getResources().getQuantityString( R.plurals.print_search_result_count_utterance, count, count); } mListView.announceForAccessibility(text); diff --git a/packages/PrintSpooler/src/com/android/printspooler/MediaSizeUtils.java b/packages/PrintSpooler/src/com/android/printspooler/util/MediaSizeUtils.java index ac27562..912ee1d 100644 --- a/packages/PrintSpooler/src/com/android/printspooler/MediaSizeUtils.java +++ b/packages/PrintSpooler/src/com/android/printspooler/util/MediaSizeUtils.java @@ -14,22 +14,28 @@ * limitations under the License. */ -package com.android.printspooler; +package com.android.printspooler.util; import android.content.Context; import android.print.PrintAttributes.MediaSize; import android.util.ArrayMap; +import com.android.printspooler.R; + import java.util.Comparator; import java.util.Map; /** * Utility functions and classes for dealing with media sizes. */ -public class MediaSizeUtils { +public final class MediaSizeUtils { private static Map<MediaSize, String> sMediaSizeToStandardMap; + private MediaSizeUtils() { + /* do nothing - hide constructor */ + } + /** * Gets the default media size for the current locale. * diff --git a/packages/PrintSpooler/src/com/android/printspooler/util/PageRangeUtils.java b/packages/PrintSpooler/src/com/android/printspooler/util/PageRangeUtils.java new file mode 100644 index 0000000..33b294f --- /dev/null +++ b/packages/PrintSpooler/src/com/android/printspooler/util/PageRangeUtils.java @@ -0,0 +1,159 @@ +/* + * Copyright (C) 2014 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.util; + +import android.print.PageRange; + +import java.util.Arrays; +import java.util.Comparator; + +/** + * This class contains utility functions for working with page ranges. + */ +public final class PageRangeUtils { + + private static final PageRange[] ALL_PAGES_RANGE = new PageRange[] {PageRange.ALL_PAGES}; + + private static final Comparator<PageRange> sComparator = new Comparator<PageRange>() { + @Override + public int compare(PageRange lhs, PageRange rhs) { + return lhs.getStart() - rhs.getStart(); + } + }; + + private PageRangeUtils() { + /* do nothing - hide constructor */ + } + + /** + * Checks whether one page range array contains another one. + * + * @param ourRanges The container page ranges. + * @param otherRanges The contained page ranges. + * @return Whether the container page ranges contains the contained ones. + */ + public static boolean contains(PageRange[] ourRanges, PageRange[] otherRanges) { + if (ourRanges == null || otherRanges == null) { + return false; + } + + if (Arrays.equals(ourRanges, ALL_PAGES_RANGE)) { + return true; + } + + ourRanges = normalize(ourRanges); + otherRanges = normalize(otherRanges); + + // Note that the code below relies on the ranges being normalized + // which is they contain monotonically increasing non-intersecting + // sub-ranges whose start is less that or equal to the end. + int otherRangeIdx = 0; + final int ourRangeCount = ourRanges.length; + final int otherRangeCount = otherRanges.length; + for (int ourRangeIdx = 0; ourRangeIdx < ourRangeCount; ourRangeIdx++) { + PageRange ourRange = ourRanges[ourRangeIdx]; + for (; otherRangeIdx < otherRangeCount; otherRangeIdx++) { + PageRange otherRange = otherRanges[otherRangeIdx]; + if (otherRange.getStart() > ourRange.getEnd()) { + break; + } + if (otherRange.getStart() < ourRange.getStart() + || otherRange.getEnd() > ourRange.getEnd()) { + return false; + } + } + } + if (otherRangeIdx < otherRangeCount) { + return false; + } + return true; + } + + /** + * Normalizes a page range, which is the resulting page ranges are + * non-overlapping with the start lesser than or equal to the end + * and ordered in an ascending order. + * + * @param pageRanges The page ranges to normalize. + * @return The normalized page ranges. + */ + public static PageRange[] normalize(PageRange[] pageRanges) { + if (pageRanges == null) { + return null; + } + final int oldRangeCount = pageRanges.length; + if (oldRangeCount <= 1) { + return pageRanges; + } + Arrays.sort(pageRanges, sComparator); + int newRangeCount = 1; + for (int i = 0; i < oldRangeCount - 1; i++) { + newRangeCount++; + PageRange currentRange = pageRanges[i]; + PageRange nextRange = pageRanges[i + 1]; + if (currentRange.getEnd() + 1 >= nextRange.getStart()) { + newRangeCount--; + pageRanges[i] = null; + pageRanges[i + 1] = new PageRange(currentRange.getStart(), + Math.max(currentRange.getEnd(), nextRange.getEnd())); + } + } + if (newRangeCount == oldRangeCount) { + return pageRanges; + } + return Arrays.copyOfRange(pageRanges, oldRangeCount - newRangeCount, + oldRangeCount); + } + + /** + * Offsets a the start and end of page ranges with the given value. + * + * @param pageRanges The page ranges to offset. + * @param offset The offset value. + */ + public static void offset(PageRange[] pageRanges, int offset) { + if (offset == 0) { + return; + } + final int pageRangeCount = pageRanges.length; + for (int i = 0; i < pageRangeCount; i++) { + final int start = pageRanges[i].getStart() + offset; + final int end = pageRanges[i].getEnd() + offset; + pageRanges[i] = new PageRange(start, end); + } + } + + /** + * Gets the number of pages in a normalized range array. + * + * @param pageRanges Normalized page ranges. + * @param layoutPageCount Page count after reported after layout pass. + * @return The page count in the ranges. + */ + public static int getNormalizedPageCount(PageRange[] pageRanges, int layoutPageCount) { + int pageCount = 0; + final int pageRangeCount = pageRanges.length; + for (int i = 0; i < pageRangeCount; i++) { + PageRange pageRange = pageRanges[i]; + if (PageRange.ALL_PAGES.equals(pageRange)) { + return layoutPageCount; + } + pageCount += pageRange.getEnd() - pageRange.getStart() + 1; + } + return pageCount; + } +} diff --git a/packages/PrintSpooler/src/com/android/printspooler/util/PrintOptionUtils.java b/packages/PrintSpooler/src/com/android/printspooler/util/PrintOptionUtils.java new file mode 100644 index 0000000..446952d --- /dev/null +++ b/packages/PrintSpooler/src/com/android/printspooler/util/PrintOptionUtils.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2014 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.util; + +import android.content.ComponentName; +import android.content.Context; +import android.content.pm.ServiceInfo; +import android.print.PrintManager; +import android.printservice.PrintServiceInfo; + +import java.util.List; + +public class PrintOptionUtils { + + private PrintOptionUtils() { + /* ignore - hide constructor */ + } + + /** + * Gets the advanced options activity name for a print service. + * + * @param context Context for accessing system resources. + * @param serviceName The print service name. + * @return The advanced options activity name or null. + */ + public static String getAdvancedOptionsActivityName(Context context, + ComponentName serviceName) { + PrintManager printManager = (PrintManager) context.getSystemService( + Context.PRINT_SERVICE); + List<PrintServiceInfo> printServices = printManager.getEnabledPrintServices(); + final int printServiceCount = printServices.size(); + for (int i = 0; i < printServiceCount; i ++) { + PrintServiceInfo printServiceInfo = printServices.get(i); + ServiceInfo serviceInfo = printServiceInfo.getResolveInfo().serviceInfo; + if (serviceInfo.name.equals(serviceName.getClassName()) + && serviceInfo.packageName.equals(serviceName.getPackageName())) { + return printServiceInfo.getAdvancedOptionsActivityName(); + } + } + return null; + } +} diff --git a/packages/PrintSpooler/src/com/android/printspooler/widget/ContentView.java b/packages/PrintSpooler/src/com/android/printspooler/widget/ContentView.java new file mode 100644 index 0000000..77ca541 --- /dev/null +++ b/packages/PrintSpooler/src/com/android/printspooler/widget/ContentView.java @@ -0,0 +1,338 @@ +/* + * Copyright (C) 2014 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.widget; + +import android.content.Context; +import android.support.v4.widget.ViewDragHelper; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import com.android.printspooler.R; + +/** + * This class is a layout manager for the print screen. It has a sliding + * area that contains the print options. If the sliding area is open the + * print options are visible and if it is closed a summary of the print + * job is shown. Under the sliding area there is a place for putting + * arbitrary content such as preview, error message, progress indicator, + * etc. The sliding area is covering the content holder under it when + * the former is opened. + */ +@SuppressWarnings("unused") +public final class ContentView extends ViewGroup implements View.OnClickListener { + private static final int FIRST_POINTER_ID = 0; + + private final ViewDragHelper mDragger; + + private View mStaticContent; + private ViewGroup mSummaryContent; + private View mDynamicContent; + + private View mDraggableContent; + private ViewGroup mMoreOptionsContainer; + private ViewGroup mOptionsContainer; + + private View mEmbeddedContentContainer; + + private View mExpandCollapseHandle; + private View mExpandCollapseIcon; + + private int mClosedOptionsOffsetY; + private int mCurrentOptionsOffsetY; + + private OptionsStateChangeListener mOptionsStateChangeListener; + + private int mOldDraggableHeight; + + public interface OptionsStateChangeListener { + public void onOptionsOpened(); + public void onOptionsClosed(); + } + + public ContentView(Context context, AttributeSet attrs) { + super(context, attrs); + mDragger = ViewDragHelper.create(this, new DragCallbacks()); + + // The options view is sliding under the static header but appears + // after it in the layout, so we will draw in opposite order. + setChildrenDrawingOrderEnabled(true); + } + + public void setOptionsStateChangeListener(OptionsStateChangeListener listener) { + mOptionsStateChangeListener = listener; + } + + private boolean isOptionsOpened() { + return mCurrentOptionsOffsetY == 0; + } + + private boolean isOptionsClosed() { + return mCurrentOptionsOffsetY == mClosedOptionsOffsetY; + } + + private void openOptions() { + if (isOptionsOpened()) { + return; + } + mDragger.smoothSlideViewTo(mDynamicContent, mDynamicContent.getLeft(), + getOpenedOptionsY()); + invalidate(); + } + + private void closeOptions() { + if (isOptionsClosed()) { + return; + } + mDragger.smoothSlideViewTo(mDynamicContent, mDynamicContent.getLeft(), + getClosedOptionsY()); + invalidate(); + } + + @Override + protected int getChildDrawingOrder(int childCount, int i) { + return childCount - i - 1; + } + + @Override + protected void onFinishInflate() { + mStaticContent = findViewById(R.id.static_content); + mSummaryContent = (ViewGroup) findViewById(R.id.summary_content); + mDynamicContent = findViewById(R.id.dynamic_content); + mDraggableContent = findViewById(R.id.draggable_content); + mMoreOptionsContainer = (ViewGroup) findViewById(R.id.more_options_container); + mOptionsContainer = (ViewGroup) findViewById(R.id.options_container); + mEmbeddedContentContainer = findViewById(R.id.embedded_content_container); + mExpandCollapseIcon = findViewById(R.id.expand_collapse_icon); + mExpandCollapseHandle = findViewById(R.id.expand_collapse_handle); + + mExpandCollapseIcon.setOnClickListener(this); + mExpandCollapseHandle.setOnClickListener(this); + + // Make sure we start in a closed options state. + onDragProgress(1.0f); + } + + @Override + public void onClick(View view) { + if (view == mExpandCollapseHandle || view == mExpandCollapseIcon) { + if (isOptionsClosed()) { + openOptions(); + } else if (isOptionsOpened()) { + closeOptions(); + } // else in open/close progress do nothing. + } + } + + @Override + public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) { + /* do nothing */ + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + mDragger.processTouchEvent(event); + return true; + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent event) { + return mDragger.shouldInterceptTouchEvent(event) + || super.onInterceptTouchEvent(event); + } + + @Override + public void computeScroll() { + if (mDragger.continueSettling(true)) { + postInvalidateOnAnimation(); + } + } + + private int getOpenedOptionsY() { + return mStaticContent.getBottom(); + } + + private int getClosedOptionsY() { + return getOpenedOptionsY() + mClosedOptionsOffsetY; + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + measureChild(mStaticContent, widthMeasureSpec, heightMeasureSpec); + + if (mSummaryContent.getVisibility() != View.GONE) { + measureChild(mSummaryContent, widthMeasureSpec, heightMeasureSpec); + } + + measureChild(mDynamicContent, widthMeasureSpec, heightMeasureSpec); + + final int heightSize = MeasureSpec.getSize(heightMeasureSpec); + +// // The height of the draggable content may change and if that happens +// // we have to adjust the current offset to ensure the sliding area is +// // at the same position. +// mCurrentOptionsOffsetY -= mDraggableContent.getMeasuredHeight() +// - oldDraggableHeight; + + if (mOldDraggableHeight != mDraggableContent.getMeasuredHeight()) { + mCurrentOptionsOffsetY -= mDraggableContent.getMeasuredHeight() + - mOldDraggableHeight; + mOldDraggableHeight = mDraggableContent.getMeasuredHeight(); + } + + // The height of the draggable content may change and if that happens + // we have to adjust the sliding area closed state offset. + mClosedOptionsOffsetY = mSummaryContent.getMeasuredHeight() + - mDraggableContent.getMeasuredHeight(); + + // The content host must be maximally large size that fits entirely + // on the screen when the options are collapsed. + ViewGroup.LayoutParams params = mEmbeddedContentContainer.getLayoutParams(); + if (params.height == 0) { + params.height = heightSize - mStaticContent.getMeasuredHeight() + - mSummaryContent.getMeasuredHeight() - mDynamicContent.getMeasuredHeight() + + mDraggableContent.getMeasuredHeight(); + + mCurrentOptionsOffsetY = mClosedOptionsOffsetY; + } + + // The content host can grow vertically as much as needed - we will be covering it. + final int hostHeightMeasureSpec = MeasureSpec.makeMeasureSpec(MeasureSpec.UNSPECIFIED, 0); + measureChild(mEmbeddedContentContainer, widthMeasureSpec, hostHeightMeasureSpec); + + setMeasuredDimension(resolveSize(MeasureSpec.getSize(widthMeasureSpec), widthMeasureSpec), + resolveSize(heightSize, heightMeasureSpec)); + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + mStaticContent.layout(left, top, right, mStaticContent.getMeasuredHeight()); + + if (mSummaryContent.getVisibility() != View.GONE) { + mSummaryContent.layout(left, mStaticContent.getMeasuredHeight(), right, + mStaticContent.getMeasuredHeight() + mSummaryContent.getMeasuredHeight()); + } + + final int dynContentTop = mStaticContent.getMeasuredHeight() + mCurrentOptionsOffsetY; + final int dynContentBottom = dynContentTop + mDynamicContent.getMeasuredHeight(); + + mDynamicContent.layout(left, dynContentTop, right, dynContentBottom); + + final int embContentTop = mStaticContent.getMeasuredHeight() + mClosedOptionsOffsetY + + mDynamicContent.getMeasuredHeight(); + final int embContentBottom = embContentTop + mEmbeddedContentContainer.getMeasuredHeight(); + + mEmbeddedContentContainer.layout(left, embContentTop, right, embContentBottom); + } + + private void onDragProgress(float progress) { + final int summaryCount = mSummaryContent.getChildCount(); + for (int i = 0; i < summaryCount; i++) { + View child = mSummaryContent.getChildAt(i); + child.setAlpha(progress); + } + + if (progress == 0) { + if (mOptionsStateChangeListener != null) { + mOptionsStateChangeListener.onOptionsOpened(); + } + mSummaryContent.setVisibility(View.GONE); + mExpandCollapseIcon.setBackgroundResource(R.drawable.ic_expand_less); + } else { + mSummaryContent.setVisibility(View.VISIBLE); + } + + final float inverseAlpha = 1.0f - progress; + + final int optionCount = mOptionsContainer.getChildCount(); + for (int i = 0; i < optionCount; i++) { + View child = mOptionsContainer.getChildAt(i); + child.setAlpha(inverseAlpha); + } + + if (mMoreOptionsContainer.getVisibility() != View.GONE) { + final int moreOptionCount = mMoreOptionsContainer.getChildCount(); + for (int i = 0; i < moreOptionCount; i++) { + View child = mMoreOptionsContainer.getChildAt(i); + child.setAlpha(inverseAlpha); + } + } + + if (inverseAlpha == 0) { + if (mOptionsStateChangeListener != null) { + mOptionsStateChangeListener.onOptionsClosed(); + } + if (mMoreOptionsContainer.getVisibility() != View.GONE) { + mMoreOptionsContainer.setVisibility(View.INVISIBLE); + } + mDraggableContent.setVisibility(View.INVISIBLE); + mExpandCollapseIcon.setBackgroundResource(R.drawable.ic_expand_more); + } else { + if (mMoreOptionsContainer.getVisibility() != View.GONE) { + mMoreOptionsContainer.setVisibility(View.VISIBLE); + } + mDraggableContent.setVisibility(View.VISIBLE); + } + } + + private final class DragCallbacks extends ViewDragHelper.Callback { + @Override + public boolean tryCaptureView(View child, int pointerId) { + return child == mDynamicContent && pointerId == FIRST_POINTER_ID; + } + + @Override + public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) { + mCurrentOptionsOffsetY += dy; + final float progress = ((float) top - getOpenedOptionsY()) + / (getClosedOptionsY() - getOpenedOptionsY()); + + mDraggableContent.notifySubtreeAccessibilityStateChangedIfNeeded(); + + onDragProgress(progress); + } + + public void onViewReleased(View child, float velocityX, float velocityY) { + final int childTop = child.getTop(); + + final int openedOptionsY = getOpenedOptionsY(); + final int closedOptionsY = getClosedOptionsY(); + + if (childTop == openedOptionsY || childTop == closedOptionsY) { + return; + } + + final int halfRange = closedOptionsY + (openedOptionsY - closedOptionsY) / 2; + if (childTop < halfRange) { + mDragger.smoothSlideViewTo(child, child.getLeft(), closedOptionsY); + } else { + mDragger.smoothSlideViewTo(child, child.getLeft(), openedOptionsY); + } + + invalidate(); + } + + public int getViewVerticalDragRange(View child) { + return mDraggableContent.getHeight(); + } + + public int clampViewPositionVertical(View child, int top, int dy) { + final int staticOptionBottom = mStaticContent.getBottom(); + return Math.max(Math.min(top, getOpenedOptionsY()), getClosedOptionsY()); + } + } +} diff --git a/packages/PrintSpooler/src/com/android/printspooler/widget/FirstFocusableEditText.java b/packages/PrintSpooler/src/com/android/printspooler/widget/FirstFocusableEditText.java new file mode 100644 index 0000000..d6bb7c8 --- /dev/null +++ b/packages/PrintSpooler/src/com/android/printspooler/widget/FirstFocusableEditText.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2014 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.widget; + +import android.content.Context; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.util.AttributeSet; +import android.widget.EditText; + +/** + * An instance of this class class is intended to be the first focusable + * in a layout to which the system automatically gives focus. It performs + * some voodoo to avoid the first tap on it to start an edit mode, rather + * to bring up the IME, i.e. to get the behavior as if the view was not + * focused. + */ +public final class FirstFocusableEditText extends EditText { + private boolean mClickedBeforeFocus; + private CharSequence mError; + + public FirstFocusableEditText(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + public boolean performClick() { + super.performClick(); + if (isFocused() && !mClickedBeforeFocus) { + clearFocus(); + requestFocus(); + } + mClickedBeforeFocus = true; + return true; + } + + @Override + public CharSequence getError() { + return mError; + } + + @Override + public void setError(CharSequence error, Drawable icon) { + setCompoundDrawables(null, null, icon, null); + mError = error; + } + + protected void onFocusChanged(boolean gainFocus, int direction, + Rect previouslyFocusedRect) { + if (!gainFocus) { + mClickedBeforeFocus = false; + } + super.onFocusChanged(gainFocus, direction, previouslyFocusedRect); + } +}
\ No newline at end of file diff --git a/packages/PrintSpooler/src/com/android/printspooler/widget/PrintOptionsLayout.java b/packages/PrintSpooler/src/com/android/printspooler/widget/PrintOptionsLayout.java new file mode 100644 index 0000000..23c8d08 --- /dev/null +++ b/packages/PrintSpooler/src/com/android/printspooler/widget/PrintOptionsLayout.java @@ -0,0 +1,166 @@ +/* + * Copyright (C) 2014 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.widget; + +import android.content.Context; +import android.content.res.TypedArray; +import android.util.AttributeSet; +import android.view.View; +import android.view.ViewGroup; +import com.android.printspooler.R; + +/** + * This class is a layout manager for the print options. The options are + * arranged in a configurable number of columns and enough rows to fit all + * the options given the column count. + */ +@SuppressWarnings("unused") +public final class PrintOptionsLayout extends ViewGroup { + + private final int mColumnCount; + + public PrintOptionsLayout(Context context, AttributeSet attrs) { + super(context, attrs); + + TypedArray typedArray = context.obtainStyledAttributes(attrs, + R.styleable.PrintOptionsLayout); + mColumnCount = typedArray.getInteger(R.styleable.PrintOptionsLayout_columnCount, 0); + typedArray.recycle(); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + final int widthMode = MeasureSpec.getMode(widthMeasureSpec); + final int widthSize = MeasureSpec.getSize(widthMeasureSpec); + + final int columnWidth = (widthSize != 0) + ? (widthSize - mPaddingLeft - mPaddingRight) / mColumnCount : 0; + + int width = 0; + int height = 0; + int childState = 0; + + final int childCount = getChildCount(); + final int rowCount = childCount / mColumnCount + childCount % mColumnCount; + + for (int row = 0; row < rowCount; row++) { + int rowWidth = 0; + int rowHeight = 0; + + for (int col = 0; col < mColumnCount; col++) { + final int childIndex = row * mColumnCount + col; + + if (childIndex >= childCount) { + break; + } + + View child = getChildAt(childIndex); + + if (child.getVisibility() == GONE) { + continue; + } + + MarginLayoutParams childParams = (MarginLayoutParams) child.getLayoutParams(); + + final int childWidthMeasureSpec; + if (columnWidth > 0) { + childWidthMeasureSpec = MeasureSpec.makeMeasureSpec( + columnWidth - childParams.getMarginStart() - childParams.getMarginEnd(), + MeasureSpec.EXACTLY); + } else { + childWidthMeasureSpec = getChildMeasureSpec(heightMeasureSpec, + getPaddingStart() + getPaddingEnd() + width, childParams.width); + } + + final int childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec, + getPaddingTop() + getPaddingBottom() + height, childParams.height); + + child.measure(childWidthMeasureSpec, childHeightMeasureSpec); + + childState = combineMeasuredStates(childState, child.getMeasuredState()); + + rowWidth += child.getMeasuredWidth() + childParams.getMarginStart() + + childParams.getMarginEnd(); + + rowHeight = Math.max(rowHeight, child.getMeasuredHeight() + childParams.topMargin + + childParams.bottomMargin); + } + + width = Math.max(width, rowWidth); + height += rowHeight; + } + + width += getPaddingStart() + getPaddingEnd(); + width = Math.max(width, getMinimumWidth()); + + height += getPaddingTop() + getPaddingBottom(); + height = Math.max(height, getMinimumHeight()); + + setMeasuredDimension(resolveSizeAndState(width, widthMeasureSpec, childState), + resolveSizeAndState(height, heightMeasureSpec, + childState << MEASURED_HEIGHT_STATE_SHIFT)); + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + final int childCount = getChildCount(); + final int rowCount = childCount / mColumnCount + childCount % mColumnCount; + + int cellStart = getPaddingStart(); + int cellTop = getPaddingTop(); + + for (int row = 0; row < rowCount; row++) { + int rowHeight = 0; + + for (int col = 0; col < mColumnCount; col++) { + final int childIndex = row * mColumnCount + col; + + if (childIndex >= childCount) { + break; + } + + View child = getChildAt(childIndex); + + if (child.getVisibility() == GONE) { + continue; + } + + MarginLayoutParams childParams = (MarginLayoutParams) child.getLayoutParams(); + + final int childLeft = cellStart + childParams.getMarginStart(); + final int childTop = cellTop + childParams.topMargin; + final int childRight = childLeft + child.getMeasuredWidth(); + final int childBottom = childTop + child.getMeasuredHeight(); + + child.layout(childLeft, childTop, childRight, childBottom); + + cellStart = childRight + childParams.getMarginEnd(); + + rowHeight = Math.max(rowHeight, child.getMeasuredHeight() + + childParams.topMargin + childParams.bottomMargin); + } + + cellStart = getPaddingStart(); + cellTop += cellTop + rowHeight; + } + } + + @Override + public LayoutParams generateLayoutParams(AttributeSet attrs) { + return new ViewGroup.MarginLayoutParams(getContext(), attrs); + } +} |