OSDN Git Service

Updating the print dialog and its interactinos with the printing app.
authorSvetoslav Ganov <svetoslavganov@google.com>
Tue, 23 Jul 2013 20:29:31 +0000 (13:29 -0700)
committerSvetoslav Ganov <svetoslavganov@google.com>
Wed, 24 Jul 2013 01:05:53 +0000 (18:05 -0700)
1. Added support for reporting the old print attributes during layout.
   Now we keep track of the old print attributes, so the app can
   compute the delta and decide whether re-layout work is needed.

2. Fixed PrintDocumentAdapter callback interleavings. Layout callbacks
   were intermixing with write ones - a mess. Now we make an attempt
   to cancel layout and write if they respond to cancellation, otherwise
   we wait but do not interleave them.

3. Refactored the PrintJobConfigActivity for easier maintenance and
   to have a single update UI method that does the minimal amount
   of work.

Change-Id: I31ada1a0550882e6185018e6f17f923aed165d15

core/java/android/print/PrintAttributes.java
packages/PrintSpooler/res/layout/print_job_config_activity.xml
packages/PrintSpooler/res/values/constants.xml
packages/PrintSpooler/res/values/strings.xml
packages/PrintSpooler/src/com/android/printspooler/PrintJobConfigActivity.java
packages/PrintSpooler/src/com/android/printspooler/RemotePrintDocumentAdapter.java

index 65f1330..87d75c0 100644 (file)
@@ -384,6 +384,22 @@ public final class PrintAttributes implements Parcelable {
     }
 
     /**
+     * @hide
+     */
+    public void copyFrom(PrintAttributes other) {
+        mMediaSize = other.mMediaSize;
+        mResolution = other.mResolution;
+        mMargins = other.mMargins;
+        mInputTray = other.mInputTray;
+        mOutputTray = other.mOutputTray;
+        mDuplexMode = other.mDuplexMode;
+        mColorMode = other.mColorMode;
+        mFittingMode = other.mFittingMode;
+        mOrientation = other.mOrientation;
+        mCopies = other.mCopies;
+    }
+
+    /**
      * This class specifies a supported media size.
      */
     public static final class MediaSize {
index 8736bdd..32bc15a 100644 (file)
@@ -5,7 +5,7 @@
      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,
      limitations under the License.
 -->
 
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="fill_parent"
     android:layout_height="wrap_content"
-    android:orientation="vertical">
+    android:orientation="vertical"
+    android:scrollbars="vertical">
 
-    <ScrollView
-        android:layout_width="fill_parent"
+    <GridLayout
+        android:layout_width="wrap_content"
         android:layout_height="wrap_content"
-        android:background="@*android:color/bright_foreground_disabled_holo_light">
+        android:orientation="vertical"
+        android:columnCount="2">
 
-        <GridLayout
+        <!-- Destination -->
+
+        <Spinner
+            android:id="@+id/destination_spinner"
+            android:layout_width="fill_parent"
+            android:layout_height="wrap_content"
+            android:layout_gravity="fill_horizontal"
+            android:layout_marginLeft="32dip"
+            android:layout_marginRight="32dip"
+            android:layout_marginBottom="12dip"
+            android:layout_row="0"
+            android:layout_column="0"
+            android:layout_columnSpan="2"
+            android:minHeight="?android:attr/listPreferredItemHeightSmall">
+        </Spinner>
+
+        <!-- Copies -->
+
+        <view
+            class="com.android.printspooler.PrintJobConfigActivity$CustomEditText"
+            android:id="@+id/copies_edittext"
+            android:layout_width="fill_parent"
+            android:layout_height="wrap_content"
+            android:layout_marginLeft="32dip"
+            android:layout_marginRight="12dip"
+            android:layout_marginBottom="12dip"
+            android:layout_row="2"
+            android:layout_column="0"
+            android:layout_gravity="bottom"
+            android:inputType="numberDecimal"
+            android:selectAllOnFocus="true"
+            android:minWidth="150dip">
+        </view>
+
+        <TextView
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
-            android:layout_margin="32dip"
-            android:orientation="vertical"
-            android:columnCount="2">
-
-            <!-- Destination -->
-
-            <Spinner
-                android:id="@+id/destination_spinner"
-                android:layout_width="fill_parent"
-                android:layout_height="wrap_content"
-                android:layout_marginBottom="12dip"
-                android:layout_row="0"
-                android:layout_column="0"
-                android:layout_columnSpan="2"
-                android:minWidth="324dip"
-                android:minHeight="?android:attr/listPreferredItemHeightSmall">
-            </Spinner>
-
-            <!-- Copies -->
-
-            <view
-                class="com.android.printspooler.PrintJobConfigActivity$CustomEditText"
-                android:id="@+id/copies_edittext"
-                android:layout_width="fill_parent"
-                android:layout_height="wrap_content"
-                android:layout_marginRight="12dip"
-                android:layout_marginBottom="12dip"
-                android:layout_row="2"
-                android:layout_column="0"
-                android:layout_gravity="bottom"
-                android:inputType="numberDecimal"
-                android:selectAllOnFocus="true"
-                android:minWidth="150dip">
-            </view>
-
-            <TextView
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
-                android:layout_marginTop="12dip"
-                android:layout_marginRight="12dip"
-                android:layout_row="1"
-                android:layout_column="0"
-                android:layout_gravity="left|bottom"
-                android:text="@string/label_copies"
-                android:textAppearance="?android:attr/textAppearanceSmall"
-                android:textStyle="bold"
-                android:labelFor="@id/copies_edittext">
-            </TextView>
-
-            <!-- Paper size -->
-
-            <Spinner
-                android:id="@+id/paper_size_spinner"
-                android:layout_width="fill_parent"
-                android:layout_height="wrap_content"
-                android:layout_marginLeft="12dip"
-                android:layout_marginBottom="12dip"
-                android:layout_row="2"
-                android:layout_column="1"
-                android:minWidth="150dip">
-            </Spinner>
-
-            <TextView
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
-                android:layout_marginLeft="12dip"
-                android:layout_marginTop="12dip"
-                android:layout_row="1"
-                android:layout_column="1"
-                android:text="@string/label_paper_size"
-                android:textAppearance="?android:attr/textAppearanceSmall"
-                android:textStyle="bold"
-                android:labelFor="@id/paper_size_spinner">
-            </TextView>
-
-            <!-- Color -->
-
-            <Spinner
-                android:id="@+id/color_spinner"
-                android:layout_width="fill_parent"
-                android:layout_height="wrap_content"
-                android:layout_marginRight="12dip"
-                android:layout_marginBottom="12dip"
-                android:layout_row="4"
-                android:layout_column="0"
-                android:minWidth="150dip">
-            </Spinner>
-
-            <TextView
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
-                android:layout_marginTop="12dip"
-                android:layout_marginRight="12dip"
-                android:layout_row="3"
-                android:layout_column="0"
-                android:text="@string/label_color"
-                android:textAppearance="?android:attr/textAppearanceSmall"
-                android:textStyle="bold"
-                android:labelFor="@id/color_spinner">
-            </TextView>
-
-            <!-- Orientation -->
-
-            <Spinner
-                android:id="@+id/orientation_spinner"
-                android:layout_width="fill_parent"
-                android:layout_height="wrap_content"
-                android:layout_marginLeft="12dip"
-                android:layout_marginBottom="12dip"
-                android:layout_row="4"
-                android:layout_column="1"
-                android:minWidth="150dip">
-            </Spinner>
-
-            <TextView
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
-                android:layout_marginLeft="12dip"
-                android:layout_marginTop="12dip"
-                android:layout_row="3"
-                android:layout_column="1"
-                android:text="@string/label_orientation"
-                android:textAppearance="?android:attr/textAppearanceSmall"
-                android:textStyle="bold"
-                android:labelFor="@id/orientation_spinner">
-            </TextView>
-
-            <!-- Pages -->
-
-            <Spinner
-                android:id="@+id/range_options_spinner"
-                android:layout_width="fill_parent"
-                android:layout_height="wrap_content"
-                android:layout_marginRight="12dip"
-                android:layout_row="6"
-                android:layout_column="0"
-                android:minWidth="150dip">
-            </Spinner>
-
-            <EditText
-                android:id="@+id/page_range_edittext"
-                android:layout_width="fill_parent"
-                android:layout_height="wrap_content"
-                android:layout_marginLeft="12dip"
-                android:layout_row="6"
-                android:layout_column="1"
-                android:layout_gravity="bottom"
-                android:selectAllOnFocus="true"
-                android:minWidth="150dip"
-                android:hint="@string/pages_range_example"
-                android:inputType="textNoSuggestions"
-                android:visibility="gone">
-            </EditText>
-
-            <TextView
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
-                android:layout_marginTop="12dip"
-                android:layout_marginRight="12dip"
-                android:layout_row="5"
-                android:layout_column="0"
-                android:text="@string/label_pages"
-                android:textAppearance="?android:attr/textAppearanceSmall"
-                android:textStyle="bold"
-                android:labelFor="@id/range_options_spinner">
-            </TextView>
-
-        </GridLayout>
-
-    </ScrollView>
-
-    <Button
-        android:id="@+id/print_button"
-        android:layout_width="fill_parent"
-        android:layout_height="wrap_content"
-        android:padding="0dip"
-        android:text="@string/print_button"
-        android:background="?android:attr/selectableItemBackground">
-    </Button>
+            android:layout_marginLeft="32dip"
+            android:layout_marginTop="12dip"
+            android:layout_marginRight="12dip"
+            android:layout_row="1"
+            android:layout_column="0"
+            android:layout_gravity="left|bottom"
+            android:text="@string/label_copies"
+            android:textAppearance="?android:attr/textAppearanceSmall"
+            android:textStyle="bold"
+            android:labelFor="@id/copies_edittext">
+        </TextView>
+
+        <!-- Paper size -->
+
+        <Spinner
+            android:id="@+id/paper_size_spinner"
+            android:layout_width="fill_parent"
+            android:layout_height="wrap_content"
+            android:layout_marginLeft="12dip"
+            android:layout_marginRight="32dip"
+            android:layout_marginBottom="12dip"
+            android:layout_row="2"
+            android:layout_column="1"
+            android:minWidth="150dip">
+        </Spinner>
+
+        <TextView
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginLeft="12dip"
+            android:layout_marginRight="32dip"
+            android:layout_marginTop="12dip"
+            android:layout_row="1"
+            android:layout_column="1"
+            android:text="@string/label_paper_size"
+            android:textAppearance="?android:attr/textAppearanceSmall"
+            android:textStyle="bold"
+            android:labelFor="@id/paper_size_spinner">
+        </TextView>
+
+        <!-- Color -->
+
+        <Spinner
+            android:id="@+id/color_spinner"
+            android:layout_width="fill_parent"
+            android:layout_height="wrap_content"
+            android:layout_marginLeft="32dip"
+            android:layout_marginRight="12dip"
+            android:layout_marginBottom="12dip"
+            android:layout_row="4"
+            android:layout_column="0"
+            android:minWidth="150dip">
+        </Spinner>
+
+        <TextView
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginLeft="32dip"
+            android:layout_marginTop="12dip"
+            android:layout_marginRight="12dip"
+            android:layout_row="3"
+            android:layout_column="0"
+            android:text="@string/label_color"
+            android:textAppearance="?android:attr/textAppearanceSmall"
+            android:textStyle="bold"
+            android:labelFor="@id/color_spinner">
+        </TextView>
+
+        <!-- Orientation -->
+
+        <Spinner
+            android:id="@+id/orientation_spinner"
+            android:layout_width="fill_parent"
+            android:layout_height="wrap_content"
+            android:layout_marginLeft="12dip"
+            android:layout_marginRight="32dip"
+            android:layout_marginBottom="12dip"
+            android:layout_row="4"
+            android:layout_column="1"
+            android:minWidth="150dip">
+        </Spinner>
+
+        <TextView
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginLeft="12dip"
+            android:layout_marginTop="12dip"
+            android:layout_marginRight="32dip"
+            android:layout_row="3"
+            android:layout_column="1"
+            android:text="@string/label_orientation"
+            android:textAppearance="?android:attr/textAppearanceSmall"
+            android:textStyle="bold"
+            android:labelFor="@id/orientation_spinner">
+        </TextView>
+
+        <!-- Pages -->
+
+        <Spinner
+            android:id="@+id/range_options_spinner"
+            android:layout_width="fill_parent"
+            android:layout_height="wrap_content"
+            android:layout_marginLeft="32dip"
+            android:layout_marginRight="12dip"
+            android:layout_row="6"
+            android:layout_column="0"
+            android:minWidth="150dip">
+        </Spinner>
+
+        <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_marginLeft="12dip"
+            android:layout_marginRight="32dip"
+            android:layout_row="6"
+            android:layout_column="1"
+            android:layout_gravity="bottom"
+            android:selectAllOnFocus="true"
+            android:minWidth="150dip"
+            android:hint="@string/pages_range_example"
+            android:inputType="textNoSuggestions"
+            android:visibility="gone">
+        </view>
+
+        <TextView
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginLeft="32dip"
+            android:layout_marginTop="12dip"
+            android:layout_marginRight="12dip"
+            android:layout_row="5"
+            android:layout_column="0"
+            android:text="@string/label_pages"
+            android:textAppearance="?android:attr/textAppearanceSmall"
+            android:textStyle="bold"
+            android:labelFor="@id/range_options_spinner">
+        </TextView>
+
+        <!-- Print pereview  -->
+
+        <ImageView
+            android:layout_width="fill_parent"
+            android:layout_height="wrap_content"
+            android:layout_gravity="fill_horizontal"
+            android:layout_marginLeft="32dip"
+            android:layout_marginTop="32dip"
+            android:layout_marginRight="32dip"
+            android:layout_row="7"
+            android:layout_column="0"
+            android:layout_columnSpan="2"
+            android:background="?android:attr/listDivider"
+            android:contentDescription="@null">
+        </ImageView>
+
+        <Button
+            android:id="@+id/print_preview_button"
+            android:layout_width="fill_parent"
+            android:layout_height="wrap_content"
+            android:layout_gravity="fill_horizontal"
+            android:layout_marginLeft="32dip"
+            android:layout_marginRight="32dip"
+            android:layout_row="8"
+            android:layout_column="0"
+            android:layout_columnSpan="2"
+            android:text="@string/print_preview"
+            android:gravity="left|center_vertical"
+            android:background="?android:attr/selectableItemBackground">
+        </Button>
+
+        <ImageView
+            android:layout_width="fill_parent"
+            android:layout_height="wrap_content"
+            android:layout_gravity="fill_horizontal"
+            android:layout_marginLeft="32dip"
+            android:layout_marginRight="32dip"
+            android:layout_marginBottom="32dip"
+            android:layout_row="9"
+            android:layout_column="0"
+            android:layout_columnSpan="2"
+            android:background="?android:attr/listDivider"
+            android:contentDescription="@null">
+        </ImageView>
+
+        <ImageView
+            android:layout_width="fill_parent"
+            android:layout_height="wrap_content"
+            android:layout_gravity="fill_horizontal"
+            android:layout_row="10"
+            android:layout_column="0"
+            android:layout_columnSpan="2"
+            android:background="?android:attr/listDivider"
+            android:contentDescription="@null">
+        </ImageView>
+
+        <Button
+            android:id="@+id/print_button"
+            android:layout_width="fill_parent"
+            android:layout_height="wrap_content"
+            android:layout_gravity="fill_horizontal"
+            android:layout_row="11"
+            android:layout_column="0"
+            android:layout_columnSpan="2"
+            android:padding="0dip"
+            android:text="@string/print_button"
+            android:background="?android:attr/selectableItemBackground">
+        </Button>
+
+    </GridLayout>
 
-</LinearLayout>
+</ScrollView>
index 7d2cdc3..96cdeb1 100644 (file)
@@ -16,8 +16,8 @@
 
 <resources>
 
-    <integer name="page_option_value_all">1</integer>
-    <integer name="page_option_value_page_range">2</integer>
+    <integer name="page_option_value_all">0</integer>
+    <integer name="page_option_value_page_range">1</integer>
 
     <integer-array name="page_options_values" translatable="false">
         <item>@integer/page_option_value_all</item>
index de6fb60..27540d7 100644 (file)
     <!-- Page range exmple used as a hint of how to specify such. [CHAR LIMIT=15] -->
     <string name="pages_range_example">e.g. 1&#8211;5, 8</string>
 
-    <!-- Message to notify the user of entering invalid input. [CHAR LIMIT=25] -->
-    <string name="invalid_input">Invalid input</string>
+    <!-- Title for the pring preview button .[CHAR LIMIT=30] -->
+    <string name="print_preview">Print preview</string>
+
+    <!-- Title for the pring preview button if there is no PDF viewer isntalled. [CHAR LIMIT=50] -->
+    <string name="install_for_print_preview">Install PDF viewer for preview</string>
+
+    <!-- Title of the message that the printing application crashed. [CHAR LIMIT=50] -->
+    <string name="printing_app_crashed">Printing app crashed</string>
 
     <!-- Color mode labels. -->
     <string-array name="color_mode_labels">
index 19f545d..1e1cc24 100644 (file)
@@ -23,6 +23,7 @@ import android.content.pm.PackageInfo;
 import android.content.pm.PackageManager;
 import android.content.pm.PackageManager.NameNotFoundException;
 import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
 import android.net.Uri;
 import android.os.AsyncTask;
 import android.os.Binder;
@@ -50,6 +51,7 @@ import android.text.TextUtils;
 import android.text.TextWatcher;
 import android.util.AttributeSet;
 import android.util.Log;
+import android.view.Choreographer;
 import android.view.View;
 import android.view.View.OnClickListener;
 import android.view.ViewGroup;
@@ -95,12 +97,18 @@ public class PrintJobConfigActivity extends Activity {
 
     private final PrintSpooler mPrintSpooler = PrintSpooler.getInstance(this);
 
+    private Handler mHandler;
+
+    private Editor mEditor;
+
     private IPrinterDiscoveryObserver mPrinterDiscoveryObserver;
 
     private int mAppId;
     private int mPrintJobId;
 
-    private PrintAttributes mPrintAttributes;
+    private final PrintAttributes mOldPrintAttributes = new PrintAttributes.Builder().create();
+    private final PrintAttributes mCurrPrintAttributes = new PrintAttributes.Builder().create();
+    private final PrintAttributes mTempPrintAttributes = new PrintAttributes.Builder().create();
 
     private RemotePrintDocumentAdapter mRemotePrintAdapter;
 
@@ -112,152 +120,6 @@ public class PrintJobConfigActivity extends Activity {
 
     private PrintDocumentInfo mPrintDocumentInfo;
 
-    // UI elements
-
-    private EditText mCopiesEditText;
-
-    private EditText mRangeEditText;
-
-    private Spinner mDestinationSpinner;
-    public ArrayAdapter<SpinnerItem<PrinterInfo>> mDestinationSpinnerAdapter;
-
-    private Spinner mMediaSizeSpinner;
-    public ArrayAdapter<SpinnerItem<MediaSize>> mMediaSizeSpinnerAdapter;
-
-    private Spinner mColorModeSpinner;
-    public ArrayAdapter<SpinnerItem<Integer>> mColorModeSpinnerAdapter;
-
-    private Spinner mOrientationSpinner;
-    public ArrayAdapter<SpinnerItem<Integer>> mOrientationSpinnerAdapter;
-
-    private Spinner mRangeOptionsSpinner;
-    public ArrayAdapter<SpinnerItem<Integer>> mRangeOptionsSpinnerAdapter;
-
-    private Button mPrintButton;
-
-    // TODO: Implement store/restore state.
-
-    private final OnItemSelectedListener mOnItemSelectedListener =
-            new AdapterView.OnItemSelectedListener() {
-        @Override
-        public void onItemSelected(AdapterView<?> spinner, View view, int position, long id) {
-            if (spinner == mDestinationSpinner) {
-                updateUi();
-                notifyPrintableStartIfNeeded();
-            } else if (spinner == mMediaSizeSpinner) {
-                SpinnerItem<MediaSize> mediaItem = mMediaSizeSpinnerAdapter.getItem(position);
-                mPrintAttributes.setMediaSize(mediaItem.value);
-                updatePrintableContentIfNeeded();
-            } else if (spinner == mColorModeSpinner) {
-                SpinnerItem<Integer> colorModeItem =
-                        mColorModeSpinnerAdapter.getItem(position);
-                mPrintAttributes.setColorMode(colorModeItem.value);
-            } else if (spinner == mOrientationSpinner) {
-                SpinnerItem<Integer> orientationItem =
-                        mOrientationSpinnerAdapter.getItem(position);
-                mPrintAttributes.setOrientation(orientationItem.value);
-            } else if (spinner == mRangeOptionsSpinner) {
-                SpinnerItem<Integer> rangeOptionItem =
-                        mRangeOptionsSpinnerAdapter.getItem(position);
-                if (rangeOptionItem.value == getResources().getInteger(
-                        R.integer.page_option_value_all)) {
-                    mRangeEditText.setVisibility(View.INVISIBLE);
-                    mRangeEditText.setEnabled(false);
-                    mRangeEditText.setText(null);
-                    mRangeEditText.setError(null);
-                    mPrintButton.setEnabled(true);
-                } else if (rangeOptionItem.value ==  getResources().getInteger(
-                        R.integer.page_option_value_page_range)) {
-                    mRangeEditText.setVisibility(View.VISIBLE);
-                    mRangeEditText.setEnabled(true);
-                    mRangeEditText.requestFocus();
-                    mRangeEditText.setError(getString(R.string.invalid_input));
-                    InputMethodManager imm = (InputMethodManager)
-                            getSystemService(INPUT_METHOD_SERVICE);
-                    imm.showSoftInput(mRangeEditText, 0);
-                }
-            }
-        }
-
-        @Override
-        public void onNothingSelected(AdapterView<?> parent) {
-            /* do nothing*/
-        }
-    };
-
-    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 (editable.length() == 0) {
-                mCopiesEditText.setError(getString(R.string.invalid_input));
-                mPrintButton.setEnabled(false);
-                return;
-            }
-            final int copies = Integer.parseInt(editable.toString());
-            if (copies < MIN_COPIES) {
-                mCopiesEditText.setError(getString(R.string.invalid_input));
-                mPrintButton.setEnabled(false);
-                return;
-            }
-            mPrintAttributes.setCopies(copies);
-            mPrintButton.setEnabled(true);
-        }
-    };
-
-    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) {
-            String text = editable.toString();
-
-            if (TextUtils.isEmpty(text)) {
-                mRangeEditText.setError(getString(R.string.invalid_input));
-                mPrintButton.setEnabled(false);
-                return;
-            }
-
-            String escapedText = PATTERN_ESCAPE_SPECIAL_CHARS.matcher(text).replaceAll("////");
-            if (!PATTERN_PAGE_RANGE.matcher(escapedText).matches()) {
-                mRangeEditText.setError(getString(R.string.invalid_input));
-                mPrintButton.setEnabled(false);
-                return;
-            }
-
-            Matcher matcher = PATTERN_DIGITS.matcher(text);
-            while (matcher.find()) {
-                String numericString = text.substring(matcher.start(), matcher.end());
-                final int pageIndex = Integer.parseInt(numericString);
-                if (pageIndex < 1 || pageIndex > mPrintDocumentInfo.getPageCount()) {
-                    mRangeEditText.setError(getString(R.string.invalid_input));
-                    mPrintButton.setEnabled(false);
-                    return;
-                }
-            }
-
-            mRangeEditText.setError(null);
-            mPrintButton.setEnabled(true);
-        }
-    };
-
     private final DeathRecipient mDeathRecipient = new DeathRecipient() {
         @Override
         public void binderDied() {
@@ -266,207 +128,21 @@ public class PrintJobConfigActivity extends Activity {
     };
 
     @Override
-    protected void onCreate(Bundle bundle) {
-        super.onCreate(bundle);
-        setContentView(R.layout.print_job_config_activity);
-
-        getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN
-                | WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN);
-
-        Bundle extras = getIntent().getExtras();
-
-        mPrintJobId = extras.getInt(EXTRA_PRINT_JOB_ID, -1);
-        if (mPrintJobId < 0) {
-            throw new IllegalArgumentException("Invalid print job id: " + mPrintJobId);
-        }
-
-        mAppId = extras.getInt(EXTRA_APP_ID, -1);
-        if (mAppId < 0) {
-            throw new IllegalArgumentException("Invalid app id: " + mAppId);
-        }
-
-        mPrintAttributes = getIntent().getParcelableExtra(EXTRA_ATTRIBUTES);
-        if (mPrintAttributes == null) {
-            mPrintAttributes = new PrintAttributes.Builder().create();
-        }
-
-        mIPrintDocumentAdapter = extras.getBinder(EXTRA_PRINTABLE);
-        if (mIPrintDocumentAdapter == null) {
-            throw new IllegalArgumentException("Printable cannot be null");
-        }
-        mRemotePrintAdapter = new RemotePrintDocumentAdapter(
-                IPrintDocumentAdapter.Stub.asInterface(mIPrintDocumentAdapter),
-                mPrintSpooler.generateFileForPrintJob(mPrintJobId));
-
-        try {
-            mIPrintDocumentAdapter.linkToDeath(mDeathRecipient, 0);
-        } catch (RemoteException re) {
-            finish();
-        }
-
-        mPrinterDiscoveryObserver = new PrintDiscoveryObserver(getMainLooper());
-
-        bindUi();
-    }
-
-    @Override
     protected void onDestroy() {
         mIPrintDocumentAdapter.unlinkToDeath(mDeathRecipient, 0);
         super.onDestroy();
     }
 
-    private void bindUi() {
-        // Copies
-        mCopiesEditText = (EditText) findViewById(R.id.copies_edittext);
-        mCopiesEditText.setText(String.valueOf(MIN_COPIES));
-        mCopiesEditText.addTextChangedListener(mCopiesTextWatcher);
-
-        // Destination.
-        mDestinationSpinner = (Spinner) findViewById(R.id.destination_spinner);
-        mDestinationSpinnerAdapter = new ArrayAdapter<SpinnerItem<PrinterInfo>>(this,
-                R.layout.spinner_dropdown_item) {
-                    @Override
-                    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 = getLayoutInflater().inflate(
-                                    R.layout.spinner_dropdown_item, parent, false);
-                        }
-
-                        PrinterInfo printerInfo = getItem(position).value;
-                        TextView title = (TextView) convertView.findViewById(R.id.title);
-                        title.setText(printerInfo.getLabel());
-
-                        try {
-                            TextView subtitle = (TextView) convertView.findViewById(R.id.subtitle);
-                            PackageManager pm = getPackageManager();
-                            PackageInfo packageInfo = pm.getPackageInfo(
-                                    printerInfo.getId().getService().getPackageName(), 0);
-                            subtitle.setText(packageInfo.applicationInfo.loadLabel(pm));
-                            subtitle.setVisibility(View.VISIBLE);
-                        } catch (NameNotFoundException nnfe) {
-                            /* ignore */
-                        }
-
-                        return convertView;
-                    }
-        };
-        mDestinationSpinner.setAdapter(mDestinationSpinnerAdapter);
-        mDestinationSpinner.setOnItemSelectedListener(mOnItemSelectedListener);
-
-        // Media size.
-        mMediaSizeSpinner = (Spinner) findViewById(R.id.paper_size_spinner);
-        mMediaSizeSpinnerAdapter = new ArrayAdapter<SpinnerItem<MediaSize>>(this,
-                R.layout.spinner_dropdown_item, R.id.title);
-        mMediaSizeSpinner.setAdapter(mMediaSizeSpinnerAdapter);
-        mMediaSizeSpinner.setOnItemSelectedListener(mOnItemSelectedListener);
-
-        // Color mode.
-        mColorModeSpinner = (Spinner) findViewById(R.id.color_spinner);
-        mColorModeSpinnerAdapter = new ArrayAdapter<SpinnerItem<Integer>>(this,
-                R.layout.spinner_dropdown_item, R.id.title);
-        mColorModeSpinner.setAdapter(mColorModeSpinnerAdapter);
-        mColorModeSpinner.setOnItemSelectedListener(mOnItemSelectedListener);
-
-        // Orientation
-        mOrientationSpinner = (Spinner) findViewById(R.id.orientation_spinner);
-        mOrientationSpinnerAdapter = new ArrayAdapter<SpinnerItem<Integer>>(this,
-                R.layout.spinner_dropdown_item, R.id.title);
-        mOrientationSpinner.setAdapter(mOrientationSpinnerAdapter);
-        mOrientationSpinner.setOnItemSelectedListener(mOnItemSelectedListener);
-
-        // Range
-        mRangeEditText = (EditText) findViewById(R.id.page_range_edittext);
-        mRangeEditText.addTextChangedListener(mRangeTextWatcher);
-
-        // Range options
-        mRangeOptionsSpinner = (Spinner) findViewById(R.id.range_options_spinner);
-        mRangeOptionsSpinnerAdapter = new ArrayAdapter<SpinnerItem<Integer>>(this,
-                R.layout.spinner_dropdown_item, R.id.title);
-        mRangeOptionsSpinner.setAdapter(mRangeOptionsSpinnerAdapter);
-        mRangeOptionsSpinner.setOnItemSelectedListener(mOnItemSelectedListener);
-        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]));
-        }
-        mRangeOptionsSpinner.setSelection(0);
+    @Override
+    protected void onCreate(Bundle bundle) {
+        super.onCreate(bundle);
+        setContentView(R.layout.print_job_config_activity);
 
-        mPrintButton = (Button) findViewById(R.id.print_button);
-        mPrintButton.setOnClickListener(new OnClickListener() {
-            @Override
-            public void onClick(View v) {
-                mPrintConfirmed = true;
-                finish();
-            }
-        });
-    }
+        getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN
+                | WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN);
 
-    private void updateUi() {
-        final int selectedIndex = mDestinationSpinner.getSelectedItemPosition();
-        PrinterInfo printer = mDestinationSpinnerAdapter.getItem(selectedIndex).value;
-        printer.getDefaults(mPrintAttributes);
-
-        // Copies.
-        mCopiesEditText.setText(String.valueOf(
-                Math.max(mPrintAttributes.getCopies(), MIN_COPIES)));
-        mCopiesEditText.selectAll();
-
-        // Media size.
-        mMediaSizeSpinnerAdapter.clear();
-        List<MediaSize> mediaSizes = printer.getMediaSizes();
-        final int mediaSizeCount = mediaSizes.size();
-        for (int i = 0; i < mediaSizeCount; i++) {
-            MediaSize mediaSize = mediaSizes.get(i);
-            mMediaSizeSpinnerAdapter.add(new SpinnerItem<MediaSize>(
-                    mediaSize, mediaSize.getLabel()));
-        }
-        final int selectedMediaSizeIndex = mediaSizes.indexOf(
-                mPrintAttributes.getMediaSize());
-        mMediaSizeSpinner.setOnItemSelectedListener(null);
-        mMediaSizeSpinner.setSelection(selectedMediaSizeIndex);
-
-        // Color mode.
-        final int colorModes = printer.getColorModes();
-        mColorModeSpinnerAdapter.clear();
-        String[] colorModeLabels = getResources().getStringArray(
-                R.array.color_mode_labels);
-        int remainingColorModes = colorModes;
-        while (remainingColorModes != 0) {
-            final int colorBitOffset = Integer.numberOfTrailingZeros(remainingColorModes);
-            final int colorMode = 1 << colorBitOffset;
-            remainingColorModes &= ~colorMode;
-            mColorModeSpinnerAdapter.add(new SpinnerItem<Integer>(colorMode,
-                    colorModeLabels[colorBitOffset]));
-        }
-        final int selectedColorModeIndex = Integer.numberOfTrailingZeros(
-                (colorModes & mPrintAttributes.getColorMode()));
-        mColorModeSpinner.setSelection(selectedColorModeIndex);
-
-        // Orientation.
-        final int orientations = printer.getOrientations();
-        mOrientationSpinnerAdapter.clear();
-        String[] orientationLabels = getResources().getStringArray(
-                R.array.orientation_labels);
-        int remainingOrientations = orientations;
-        while (remainingOrientations != 0) {
-            final int orientationBitOffset = Integer.numberOfTrailingZeros(remainingOrientations);
-            final int orientation = 1 << orientationBitOffset;
-            remainingOrientations &= ~orientation;
-            mOrientationSpinnerAdapter.add(new SpinnerItem<Integer>(orientation,
-                    orientationLabels[orientationBitOffset]));
-        }
-        final int selectedOrientationIndex = Integer.numberOfTrailingZeros(
-                (orientations & mPrintAttributes.getOrientation()));
-        mOrientationSpinner.setSelection(selectedOrientationIndex);
+        mHandler = new MyHandler(Looper.getMainLooper());
+        mEditor = new Editor();
     }
 
     @Override
@@ -484,13 +160,12 @@ public class PrintJobConfigActivity extends Activity {
     }
 
     private void notifyPrintableStartIfNeeded() {
-        if (mDestinationSpinner.getSelectedItemPosition() < 0
+        if (mEditor.getCurrentPrinter() == null
                 || mStarted) {
             return;
         }
         mStarted = true;
         mRemotePrintAdapter.start();
-        updatePrintableContentIfNeeded();
     }
 
     private void updatePrintableContentIfNeeded() {
@@ -498,43 +173,66 @@ public class PrintJobConfigActivity extends Activity {
             return;
         }
 
-        // TODO: Implement old attributes tracking
-        mPrintSpooler.setPrintJobAttributes(mPrintJobId, mPrintAttributes);
+        mPrintSpooler.setPrintJobAttributes(mPrintJobId, mCurrPrintAttributes);
+
+        mRemotePrintAdapter.cancel();
+        mHandler.removeMessages(MyHandler.MSG_ON_LAYOUT_FINISHED);
+        mHandler.removeMessages(MyHandler.MSG_ON_LAYOUT_FAILED);
 
         // TODO: Implement setting the print preview attribute
-        mRemotePrintAdapter.layout(new PrintAttributes.Builder().create(),
-                mPrintAttributes, new LayoutResultCallback() {
+        mRemotePrintAdapter.layout(mOldPrintAttributes,
+                mCurrPrintAttributes, new LayoutResultCallback() {
             @Override
             public void onLayoutFinished(PrintDocumentInfo info, boolean changed) {
-                mPrintDocumentInfo = info;
+                mHandler.obtainMessage(MyHandler.MSG_ON_LAYOUT_FINISHED, changed ? 1 : 0,
+                        0, info).sendToTarget();
+            }
 
-                // TODO: Handle the case of unchanged content
-                mPrintSpooler.setPrintJobPrintDocumentInfo(mPrintJobId, info);
+            @Override
+            public void onLayoutFailed(CharSequence error) {
+                mHandler.obtainMessage(MyHandler.MSG_ON_LAYOUT_FAILED, error).sendToTarget();
+            }
+        }, new Bundle());
+    }
 
-                // TODO: Implement page selector.
-                final List<PageRange> pages = new ArrayList<PageRange>();
-                pages.add(PageRange.ALL_PAGES);
+    private void handleOnLayoutFinished(PrintDocumentInfo info, boolean changed) {
+        mPrintDocumentInfo = info;
 
-                mRemotePrintAdapter.write(pages, new WriteResultCallback() {
-                    @Override
-                    public void onWriteFinished(List<PageRange> pages) {
-                        updatePrintPreview(mRemotePrintAdapter.getFile());
-                    }
+        mEditor.updateUiIfNeeded();
 
-                    @Override
-                    public void onWriteFailed(CharSequence error) {
-                        Log.e(LOG_TAG, "Error write layout: " + error);
-                        finishActivity(Activity.RESULT_CANCELED);
-                    }
-                });
+        // TODO: Handle the case of unchanged content
+        mPrintSpooler.setPrintJobPrintDocumentInfo(mPrintJobId, info);
+
+        // TODO: Implement page selector.
+        final List<PageRange> pages = new ArrayList<PageRange>();
+        pages.add(PageRange.ALL_PAGES);
+
+        mRemotePrintAdapter.write(pages, new WriteResultCallback() {
+            @Override
+            public void onWriteFinished(List<PageRange> pages) {
+                mHandler.obtainMessage(MyHandler.MSG_ON_WRITE_FINISHED, pages).sendToTarget();
             }
 
             @Override
-            public void onLayoutFailed(CharSequence error) {
-                Log.e(LOG_TAG, "Error during layout: " + error);
-                finishActivity(Activity.RESULT_CANCELED);
+            public void onWriteFailed(CharSequence error) {
+                mHandler.obtainMessage(MyHandler.MSG_ON_WRITE_FAILED, error).sendToTarget();
             }
-        }, new Bundle());
+        });
+    }
+
+    private void handleOnLayoutFailed(CharSequence error) {
+        Log.e(LOG_TAG, "Error during layout: " + error);
+        finishActivity(Activity.RESULT_CANCELED);
+    }
+
+    private void handleOnWriteFinished(List<PageRange> pages) {
+        // TODO: Now we have to allow the preview button
+        mEditor.updatePrintPreview(mRemotePrintAdapter.getFile());
+    }
+
+    private void handleOnWriteFailed(CharSequence error) {
+        Log.e(LOG_TAG, "Error write layout: " + error);
+        finishActivity(Activity.RESULT_CANCELED);
     }
 
     private void notifyPrintableFinishIfNeeded() {
@@ -542,11 +240,14 @@ public class PrintJobConfigActivity extends Activity {
             return;
         }
 
-        mRemotePrintAdapter.finish(!mPrintConfirmed);
+        if (!mPrintConfirmed) {
+            mRemotePrintAdapter.cancel();
+        }
+        mRemotePrintAdapter.finish();
 
+        PrinterInfo printer = mEditor.getCurrentPrinter();
         // If canceled or no printer, nothing to do.
-        final int selectedIndex = mDestinationSpinner.getSelectedItemPosition();
-        if (!mPrintConfirmed || selectedIndex < 0) {
+        if (!mPrintConfirmed || printer == null) {
             // Update the print job's status.
             mPrintSpooler.setPrintJobState(mPrintJobId,
                     PrintJobInfo.STATE_CANCELED);
@@ -554,10 +255,7 @@ public class PrintJobConfigActivity extends Activity {
         }
 
         // Update the print job's printer.
-        SpinnerItem<PrinterInfo> printerItem =
-                mDestinationSpinnerAdapter.getItem(selectedIndex);
-        PrinterId printerId =  printerItem.value.getId();
-        mPrintSpooler.setPrintJobPrinterId(mPrintJobId, printerId);
+        mPrintSpooler.setPrintJobPrinterId(mPrintJobId, printer.getId());
 
         // Update the print job's status.
         mPrintSpooler.setPrintJobState(mPrintJobId,
@@ -574,50 +272,11 @@ public class PrintJobConfigActivity extends Activity {
         }
     }
 
-    private void updatePrintPreview(File file) {
-        // TODO: Implement
-    }
-
-    private void addPrinters(List<PrinterInfo> addedPrinters) {
-        final int addedPrinterCount = addedPrinters.size();
-        for (int i = 0; i < addedPrinterCount; i++) {
-            PrinterInfo addedPrinter = addedPrinters.get(i);
-            boolean duplicate = false;
-            final int existingPrinterCount = mDestinationSpinnerAdapter.getCount();
-            for (int j = 0; j < existingPrinterCount; j++) {
-                PrinterInfo existingPrinter = mDestinationSpinnerAdapter.getItem(j).value;
-                if (addedPrinter.getId().equals(existingPrinter.getId())) {
-                    duplicate = true;
-                    break;
-                }
-            }
-            if (!duplicate) {
-                mDestinationSpinnerAdapter.add(new SpinnerItem<PrinterInfo>(
-                        addedPrinter, addedPrinter.getLabel()));
-            } else {
-                Log.w(LOG_TAG, "Skipping a duplicate printer: " + addedPrinter);
-            }
-        }
-    }
-
-    private void removePrinters(List<PrinterId> pritnerIds) {
-        final int printerIdCount = pritnerIds.size();
-        for (int i = 0; i < printerIdCount; i++) {
-            PrinterId removedPrinterId = pritnerIds.get(i);
-            boolean removed = false;
-            final int existingPrinterCount = mDestinationSpinnerAdapter.getCount();
-            for (int j = 0; j < existingPrinterCount; j++) {
-                PrinterInfo existingPrinter = mDestinationSpinnerAdapter.getItem(j).value;
-                if (removedPrinterId.equals(existingPrinter.getId())) {
-                    mDestinationSpinnerAdapter.remove(mDestinationSpinnerAdapter.getItem(j));
-                    removed = true;
-                    break;
-                }
-            }
-            if (!removed) {
-                Log.w(LOG_TAG, "Ignoring not added printer with id: " + removedPrinterId);
-            }
-        }
+    private boolean hasPdfViewer() {
+        Intent intent = new Intent(Intent.ACTION_VIEW);
+        intent.setType("application/pdf");
+        return !getPackageManager().queryIntentActivities(intent,
+                PackageManager.MATCH_DEFAULT_ONLY).isEmpty();
     }
 
     // Caution: Use this only for debugging
@@ -659,16 +318,11 @@ public class PrintJobConfigActivity extends Activity {
                     switch (message.what) {
                         case MESSAGE_ADD_DICOVERED_PRINTERS: {
                             List<PrinterInfo> printers = (List<PrinterInfo>) message.obj;
-                            addPrinters(printers);
-                            // Just added the first printer, so select it and start printing.
-                            if (mDestinationSpinnerAdapter.getCount() == 1) {
-                                mDestinationSpinner.setSelection(0);
-                            }
+                            mEditor.addPrinters(printers);
                         } break;
                         case MESSAGE_REMOVE_DICOVERED_PRINTERS: {
                             List<PrinterId> printerIds = (List<PrinterId>) message.obj;
-                            removePrinters(printerIds);
-                            // TODO: Handle removing the last printer.
+                            mEditor.removePrinters(printerIds);
                         } break;
                     }
                 }
@@ -725,6 +379,11 @@ public class PrintJobConfigActivity extends Activity {
             return true;
         }
 
+        @Override
+        public void setError(CharSequence error, Drawable icon) {
+            setCompoundDrawables(null, null, icon, null);
+        }
+
         protected void onFocusChanged(boolean gainFocus, int direction,
                 Rect previouslyFocusedRect) {
             if (!gainFocus) {
@@ -733,4 +392,611 @@ public class PrintJobConfigActivity extends Activity {
             super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
         }
     }
+
+    private final class MyHandler 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 MyHandler(Looper looper) {
+            super(looper, null, false);
+        }
+
+        @Override
+        @SuppressWarnings("unchecked")
+        public void handleMessage(Message message) {
+            switch (message.what) {
+                case MSG_ON_LAYOUT_FINISHED: {
+                    PrintDocumentInfo info = (PrintDocumentInfo) message.obj;
+                    final boolean changed = (message.arg1 == 1);
+                    handleOnLayoutFinished(info, changed);
+                } break;
+
+                case MSG_ON_LAYOUT_FAILED: {
+                    CharSequence error = (CharSequence) message.obj;
+                    handleOnLayoutFailed(error);
+                } break;
+
+                case MSG_ON_WRITE_FINISHED: {
+                    List<PageRange> pages = (List<PageRange>) message.obj;
+                    handleOnWriteFinished(pages);
+                } break;
+
+                case MSG_ON_WRITE_FAILED: {
+                    CharSequence error = (CharSequence) message.obj;
+                    handleOnWriteFailed(error);
+                } break;
+            }
+        }
+    }
+
+    private class Editor {
+        private EditText mCopiesEditText;
+
+        private EditText mRangeEditText;
+
+        private Spinner mDestinationSpinner;
+        public ArrayAdapter<SpinnerItem<PrinterInfo>> mDestinationSpinnerAdapter;
+
+        private Spinner mMediaSizeSpinner;
+        public ArrayAdapter<SpinnerItem<MediaSize>> mMediaSizeSpinnerAdapter;
+
+        private Spinner mColorModeSpinner;
+        public ArrayAdapter<SpinnerItem<Integer>> mColorModeSpinnerAdapter;
+
+        private Spinner mOrientationSpinner;
+        public ArrayAdapter<SpinnerItem<Integer>> mOrientationSpinnerAdapter;
+
+        private Spinner mRangeOptionsSpinner;
+        public ArrayAdapter<SpinnerItem<Integer>> mRangeOptionsSpinnerAdapter;
+
+        private Button mPrintPreviewButton;
+
+        private Button mPrintButton;
+
+        private final OnItemSelectedListener mOnItemSelectedListener =
+                new AdapterView.OnItemSelectedListener() {
+            @Override
+            public void onItemSelected(AdapterView<?> spinner, View view, int position, long id) {
+                if (spinner == mDestinationSpinner) {
+                    mOldPrintAttributes.copyFrom(mCurrPrintAttributes);
+                    mCurrPrintAttributes.clear();
+                    final int selectedIndex = mDestinationSpinner.getSelectedItemPosition();
+                    if (selectedIndex >= 0) {
+                        mDestinationSpinnerAdapter.getItem(selectedIndex).value.getDefaults(
+                                mCurrPrintAttributes);
+                    }
+                    updateUiIfNeeded();
+                    notifyPrintableStartIfNeeded();
+                    updatePrintableContentIfNeeded();
+                } else if (spinner == mMediaSizeSpinner) {
+                    SpinnerItem<MediaSize> mediaItem = mMediaSizeSpinnerAdapter.getItem(position);
+                    mOldPrintAttributes.copyFrom(mCurrPrintAttributes);
+                    mCurrPrintAttributes.setMediaSize(mediaItem.value);
+                    updatePrintableContentIfNeeded();
+                } else if (spinner == mColorModeSpinner) {
+                    SpinnerItem<Integer> colorModeItem =
+                            mColorModeSpinnerAdapter.getItem(position);
+                    mOldPrintAttributes.copyFrom(mCurrPrintAttributes);
+                    mCurrPrintAttributes.setColorMode(colorModeItem.value);
+                    updatePrintableContentIfNeeded();
+                } else if (spinner == mOrientationSpinner) {
+                    SpinnerItem<Integer> orientationItem =
+                            mOrientationSpinnerAdapter.getItem(position);
+                    mOldPrintAttributes.copyFrom(mCurrPrintAttributes);
+                    mCurrPrintAttributes.setOrientation(orientationItem.value);
+                    updatePrintableContentIfNeeded();
+                } else if (spinner == mRangeOptionsSpinner) {
+                    updateUiIfNeeded();
+                    updatePrintableContentIfNeeded();
+                }
+            }
+
+            @Override
+            public void onNothingSelected(AdapterView<?> parent) {
+                /* do nothing*/
+            }
+        };
+
+        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 (editable.length() == 0) {
+                    mCopiesEditText.setError("");
+                    mPrintButton.setEnabled(false);
+                    return;
+                }
+                final int copies = Integer.parseInt(editable.toString());
+                if (copies < MIN_COPIES) {
+                    mCopiesEditText.setError("");
+                    mPrintButton.setEnabled(false);
+                    return;
+                }
+                mOldPrintAttributes.copyFrom(mCurrPrintAttributes);
+                mCurrPrintAttributes.setCopies(copies);
+            }
+        };
+
+        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) {
+                String text = editable.toString();
+
+                if (TextUtils.isEmpty(text)) {
+                    mRangeEditText.setError("");
+                    mPrintButton.setEnabled(false);
+                    return;
+                }
+
+                String escapedText = PATTERN_ESCAPE_SPECIAL_CHARS.matcher(text).replaceAll("////");
+                if (!PATTERN_PAGE_RANGE.matcher(escapedText).matches()) {
+                    mRangeEditText.setError("");
+                    mPrintButton.setEnabled(false);
+                    return;
+                }
+
+                Matcher matcher = PATTERN_DIGITS.matcher(text);
+                while (matcher.find()) {
+                    String numericString = text.substring(matcher.start(), matcher.end());
+                    final int pageIndex = Integer.parseInt(numericString);
+                    if (pageIndex < 1 || pageIndex > mPrintDocumentInfo.getPageCount()) {
+                        mRangeEditText.setError("");
+                        mPrintButton.setEnabled(false);
+                        return;
+                    }
+                }
+
+                mRangeEditText.setError(null);
+                mPrintButton.setEnabled(true);
+            }
+        };
+
+        public Editor() {
+            Bundle extras = getIntent().getExtras();
+
+            mPrintJobId = extras.getInt(EXTRA_PRINT_JOB_ID, -1);
+            if (mPrintJobId < 0) {
+                throw new IllegalArgumentException("Invalid print job id: " + mPrintJobId);
+            }
+
+            mAppId = extras.getInt(EXTRA_APP_ID, -1);
+            if (mAppId < 0) {
+                throw new IllegalArgumentException("Invalid app id: " + mAppId);
+            }
+
+            PrintAttributes attributes = getIntent().getParcelableExtra(EXTRA_ATTRIBUTES);
+            if (attributes == null) {
+                mCurrPrintAttributes.copyFrom(attributes);
+            }
+
+            mIPrintDocumentAdapter = extras.getBinder(EXTRA_PRINTABLE);
+            if (mIPrintDocumentAdapter == null) {
+                throw new IllegalArgumentException("Printable cannot be null");
+            }
+            mRemotePrintAdapter = new RemotePrintDocumentAdapter(
+                    IPrintDocumentAdapter.Stub.asInterface(mIPrintDocumentAdapter),
+                    mPrintSpooler.generateFileForPrintJob(mPrintJobId));
+
+            try {
+                mIPrintDocumentAdapter.linkToDeath(mDeathRecipient, 0);
+            } catch (RemoteException re) {
+                finish();
+            }
+
+            mPrinterDiscoveryObserver = new PrintDiscoveryObserver(getMainLooper());
+
+            bindUi();
+        }
+
+        private void bindUi() {
+            // Copies
+            mCopiesEditText = (EditText) findViewById(R.id.copies_edittext);
+            mCopiesEditText.setText(String.valueOf(MIN_COPIES));
+            mCopiesEditText.addTextChangedListener(mCopiesTextWatcher);
+            mCopiesEditText.setText(String.valueOf(
+                    Math.max(mCurrPrintAttributes.getCopies(), MIN_COPIES)));
+            mCopiesEditText.selectAll();
+
+            // Destination.
+            mDestinationSpinner = (Spinner) findViewById(R.id.destination_spinner);
+            mDestinationSpinnerAdapter = new ArrayAdapter<SpinnerItem<PrinterInfo>>(
+                    PrintJobConfigActivity.this, R.layout.spinner_dropdown_item) {
+                        @Override
+                        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 = getLayoutInflater().inflate(
+                                        R.layout.spinner_dropdown_item, parent, false);
+                            }
+
+                            PrinterInfo printerInfo = getItem(position).value;
+                            TextView title = (TextView) convertView.findViewById(R.id.title);
+                            title.setText(printerInfo.getLabel());
+
+                            try {
+                                TextView subtitle = (TextView)
+                                        convertView.findViewById(R.id.subtitle);
+                                PackageManager pm = getPackageManager();
+                                PackageInfo packageInfo = pm.getPackageInfo(
+                                        printerInfo.getId().getService().getPackageName(), 0);
+                                subtitle.setText(packageInfo.applicationInfo.loadLabel(pm));
+                                subtitle.setVisibility(View.VISIBLE);
+                            } catch (NameNotFoundException nnfe) {
+                                /* ignore */
+                            }
+
+                            return convertView;
+                        }
+            };
+            mDestinationSpinner.setAdapter(mDestinationSpinnerAdapter);
+            mDestinationSpinner.setOnItemSelectedListener(mOnItemSelectedListener);
+
+            // Media size.
+            mMediaSizeSpinner = (Spinner) findViewById(R.id.paper_size_spinner);
+            mMediaSizeSpinnerAdapter = new ArrayAdapter<SpinnerItem<MediaSize>>(
+                    PrintJobConfigActivity.this,
+                    R.layout.spinner_dropdown_item, R.id.title);
+            mMediaSizeSpinner.setAdapter(mMediaSizeSpinnerAdapter);
+            mMediaSizeSpinner.setOnItemSelectedListener(mOnItemSelectedListener);
+
+            // Color mode.
+            mColorModeSpinner = (Spinner) findViewById(R.id.color_spinner);
+            mColorModeSpinnerAdapter = new ArrayAdapter<SpinnerItem<Integer>>(
+                    PrintJobConfigActivity.this,
+                    R.layout.spinner_dropdown_item, R.id.title);
+            mColorModeSpinner.setAdapter(mColorModeSpinnerAdapter);
+            mColorModeSpinner.setOnItemSelectedListener(mOnItemSelectedListener);
+
+            // Orientation
+            mOrientationSpinner = (Spinner) findViewById(R.id.orientation_spinner);
+            mOrientationSpinnerAdapter = new ArrayAdapter<SpinnerItem<Integer>>(
+                    PrintJobConfigActivity.this,
+                    R.layout.spinner_dropdown_item, R.id.title);
+            mOrientationSpinner.setAdapter(mOrientationSpinnerAdapter);
+            mOrientationSpinner.setOnItemSelectedListener(mOnItemSelectedListener);
+
+            // Range
+            mRangeEditText = (EditText) findViewById(R.id.page_range_edittext);
+            mRangeEditText.addTextChangedListener(mRangeTextWatcher);
+
+            // Range options
+            mRangeOptionsSpinner = (Spinner) findViewById(R.id.range_options_spinner);
+            mRangeOptionsSpinnerAdapter = new ArrayAdapter<SpinnerItem<Integer>>(
+                    PrintJobConfigActivity.this,
+                    R.layout.spinner_dropdown_item, R.id.title);
+            mRangeOptionsSpinner.setAdapter(mRangeOptionsSpinnerAdapter);
+            mRangeOptionsSpinner.setOnItemSelectedListener(mOnItemSelectedListener);
+            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]));
+            }
+            mRangeOptionsSpinner.setSelection(0);
+
+            mPrintPreviewButton = (Button) findViewById(R.id.print_preview_button);
+            mPrintPreviewButton.setOnClickListener(new OnClickListener() {
+                @Override
+                public void onClick(View v) {
+                    // TODO: Implement
+                }
+            });
+
+            mPrintButton = (Button) findViewById(R.id.print_button);
+            mPrintButton.setOnClickListener(new OnClickListener() {
+                @Override
+                public void onClick(View v) {
+                    mPrintConfirmed = true;
+                    finish();
+                }
+            });
+        }
+
+        private void updateUiIfNeeded() {
+            final int selectedIndex = mDestinationSpinner.getSelectedItemPosition();
+
+            if (selectedIndex < 0) {
+                // Destination
+                mDestinationSpinner.setEnabled(false);
+
+                // Copies
+                mCopiesEditText.setText("1");
+                mCopiesEditText.setEnabled(false);
+
+                // Media size
+                mMediaSizeSpinner.setOnItemSelectedListener(null);
+                mMediaSizeSpinner.setSelection(AdapterView.INVALID_POSITION);
+                mMediaSizeSpinner.setEnabled(false);
+
+                // Color mode
+                mColorModeSpinner.setOnItemSelectedListener(null);
+                mColorModeSpinner.setSelection(AdapterView.INVALID_POSITION);
+                mColorModeSpinner.setEnabled(false);
+
+                // Orientation
+                mOrientationSpinner.setOnItemSelectedListener(null);
+                mOrientationSpinner.setSelection(AdapterView.INVALID_POSITION);
+                mOrientationSpinner.setEnabled(false);
+
+                // Range
+                mRangeOptionsSpinner.setOnItemSelectedListener(null);
+                mRangeOptionsSpinner.setSelection(0);
+                mRangeOptionsSpinner.setEnabled(false);
+                mRangeEditText.setText("");
+                mRangeEditText.setEnabled(false);
+                mRangeEditText.setVisibility(View.INVISIBLE);
+
+                // Print preview
+                mPrintPreviewButton.setEnabled(false);
+                mPrintPreviewButton.setText(getString(R.string.print_preview));
+
+                // Print
+                mPrintButton.setEnabled(false);
+            } else {
+                PrintAttributes defaultAttributes = mTempPrintAttributes;
+                PrinterInfo printer = mDestinationSpinnerAdapter.getItem(selectedIndex).value;
+                printer.getDefaults(defaultAttributes);
+
+                // Destination
+                mDestinationSpinner.setEnabled(true);
+
+                // Copies
+                mCopiesEditText.setEnabled(true);
+
+                // Media size.
+                List<MediaSize> mediaSizes = printer.getMediaSizes();
+                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) {
+                    mMediaSizeSpinnerAdapter.clear();
+                    for (int i = 0; i < mediaSizeCount; i++) {
+                        MediaSize mediaSize = mediaSizes.get(i);
+                        mMediaSizeSpinnerAdapter.add(new SpinnerItem<MediaSize>(
+                                mediaSize, mediaSize.getLabel()));
+                    }
+                    if (mediaSizeCount > 0) {
+                        mMediaSizeSpinner.setEnabled(true);
+                        final int selectedMediaSizeIndex = Math.max(mediaSizes.indexOf(
+                                defaultAttributes.getMediaSize()), 0);
+                        mMediaSizeSpinner.setOnItemSelectedListener(null);
+                        mMediaSizeSpinner.setSelection(selectedMediaSizeIndex);
+                    }
+                }
+
+                // Color mode.
+                final int colorModes = printer.getColorModes();
+                boolean colorModesChanged = false;
+                if (Integer.bitCount(colorModes) != mColorModeSpinnerAdapter.getCount()) {
+                    colorModesChanged = true;
+                } else {
+                    int remainingColorModes = colorModes;
+                    while (remainingColorModes != 0) {
+                        final int colorBitOffset = Integer.numberOfTrailingZeros(
+                                remainingColorModes);
+                        final int colorMode = 1 << colorBitOffset;
+                        remainingColorModes &= ~colorMode;
+                        if (colorMode != mColorModeSpinnerAdapter.getItem(colorBitOffset).value) {
+                            colorModesChanged = true;
+                            break;
+                        }
+                    }
+                }
+                if (colorModesChanged) {
+                    mColorModeSpinnerAdapter.clear();
+                    String[] colorModeLabels = getResources().getStringArray(
+                            R.array.color_mode_labels);
+                    int remainingColorModes = colorModes;
+                    while (remainingColorModes != 0) {
+                        final int colorBitOffset = Integer.numberOfTrailingZeros(
+                                remainingColorModes);
+                        final int colorMode = 1 << colorBitOffset;
+                        remainingColorModes &= ~colorMode;
+                        mColorModeSpinnerAdapter.add(new SpinnerItem<Integer>(colorMode,
+                                colorModeLabels[colorBitOffset]));
+                    }
+                    if (colorModes > 0) {
+                        mColorModeSpinner.setEnabled(true);
+                        final int selectedColorModeIndex = Integer.numberOfTrailingZeros(
+                                (colorModes & defaultAttributes.getColorMode()));
+                        mColorModeSpinner.setOnItemSelectedListener(null);
+                        mColorModeSpinner.setSelection(selectedColorModeIndex);
+                    }
+                }
+
+                // Orientation.
+                final int orientations = printer.getOrientations();
+                boolean orientationsChanged = false;
+                if (Integer.bitCount(orientations) != mOrientationSpinnerAdapter.getCount()) {
+                    orientationsChanged = true;
+                } else {
+                    int remainingOrientations = orientations;
+                    while (remainingOrientations != 0) {
+                        final int orientationBitOffset = Integer.numberOfTrailingZeros(
+                                remainingOrientations);
+                        final int orientation = 1 << orientationBitOffset;
+                        remainingOrientations &= ~orientation;
+                        if (orientation != mOrientationSpinnerAdapter.getItem(
+                                orientationBitOffset).value) {
+                            orientationsChanged = true;
+                            break;
+                        }
+                    }
+                }
+                if (orientationsChanged) {
+                    mOrientationSpinnerAdapter.clear();
+                    String[] orientationLabels = getResources().getStringArray(
+                            R.array.orientation_labels);
+                    int remainingOrientations = orientations;
+                    while (remainingOrientations != 0) {
+                        final int orientationBitOffset = Integer.numberOfTrailingZeros(
+                                remainingOrientations);
+                        final int orientation = 1 << orientationBitOffset;
+                        remainingOrientations &= ~orientation;
+                        mOrientationSpinnerAdapter.add(new SpinnerItem<Integer>(orientation,
+                                orientationLabels[orientationBitOffset]));
+                    }
+                    if (orientations > 0) {
+                        mOrientationSpinner.setEnabled(true);
+                        final int selectedOrientationIndex = Integer.numberOfTrailingZeros(
+                                (orientations & defaultAttributes.getOrientation()));
+                        mOrientationSpinner.setOnItemSelectedListener(null);
+                        mOrientationSpinner.setSelection(selectedOrientationIndex);
+                    }
+                }
+
+                // Range options
+                if (mPrintDocumentInfo != null && (mPrintDocumentInfo.getPageCount() > 1
+                        || mPrintDocumentInfo.getPageCount()
+                            == PrintDocumentInfo.PAGE_COUNT_UNKNOWN)) {
+                    mRangeOptionsSpinner.setEnabled(true);
+                    if (mRangeOptionsSpinner.getSelectedItemPosition() > 0
+                            && !mRangeEditText.isEnabled()) {
+                        mRangeEditText.setEnabled(true);
+                        mRangeEditText.setError("");
+                        mRangeEditText.setVisibility(View.VISIBLE);
+                        mRangeEditText.requestFocus();
+                        InputMethodManager imm = (InputMethodManager)
+                                getSystemService(INPUT_METHOD_SERVICE);
+                        imm.showSoftInput(mRangeEditText, 0);
+                    }
+                } else {
+                    mRangeOptionsSpinner.setOnItemSelectedListener(null);
+                    mRangeOptionsSpinner.setSelection(0);
+                    mRangeOptionsSpinner.setEnabled(false);
+                    mRangeEditText.setEnabled(false);
+                    mRangeEditText.setText("");
+                    mRangeEditText.setVisibility(View.INVISIBLE);
+                }
+
+                // Print preview
+                mPrintPreviewButton.setEnabled(true);
+                if (hasPdfViewer()) {
+                    mPrintPreviewButton.setText(getString(R.string.print_preview));
+                } else {
+                    mPrintPreviewButton.setText(getString(R.string.install_for_print_preview));
+                }
+
+                // Print
+                mPrintButton.setEnabled(true);
+            }
+
+            // Here is some voodoo to circumvent the weird behavior of AdapterView
+            // in which a selection listener may get a callback for an event that
+            // happened before the listener was registered. The reason for that is
+            // that the selection change is handled on the next layout pass.
+            Choreographer.getInstance().postCallback(Choreographer.CALLBACK_TRAVERSAL,
+                    new Runnable() {
+                @Override
+                public void run() {
+                    mMediaSizeSpinner.setOnItemSelectedListener(mOnItemSelectedListener);
+                    mColorModeSpinner.setOnItemSelectedListener(mOnItemSelectedListener);
+                    mOrientationSpinner.setOnItemSelectedListener(mOnItemSelectedListener);
+                    mRangeOptionsSpinner.setOnItemSelectedListener(mOnItemSelectedListener);
+                }
+            }, null);
+        }
+
+        public PrinterInfo getCurrentPrinter() {
+            final int selectedIndex = mDestinationSpinner.getSelectedItemPosition();
+            if (selectedIndex >= 0) {
+                return mDestinationSpinnerAdapter.getItem(selectedIndex).value;
+            }
+            return null;
+        }
+
+        public void addPrinters(List<PrinterInfo> addedPrinters) {
+            final int addedPrinterCount = addedPrinters.size();
+            for (int i = 0; i < addedPrinterCount; i++) {
+                PrinterInfo addedPrinter = addedPrinters.get(i);
+                boolean duplicate = false;
+                final int existingPrinterCount = mDestinationSpinnerAdapter.getCount();
+                for (int j = 0; j < existingPrinterCount; j++) {
+                    PrinterInfo existingPrinter = mDestinationSpinnerAdapter.getItem(j).value;
+                    if (addedPrinter.getId().equals(existingPrinter.getId())) {
+                        duplicate = true;
+                        break;
+                    }
+                }
+                if (!duplicate) {
+                    mDestinationSpinnerAdapter.add(new SpinnerItem<PrinterInfo>(
+                            addedPrinter, addedPrinter.getLabel()));
+                } else {
+                    Log.w(LOG_TAG, "Skipping a duplicate printer: " + addedPrinter);
+                }
+            }
+
+            if (mDestinationSpinner.getSelectedItemPosition() == AdapterView.INVALID_POSITION
+                    && mDestinationSpinnerAdapter.getCount() > 0) {
+                mDestinationSpinner.setSelection(0);
+            }
+        }
+
+        public void removePrinters(List<PrinterId> pritnerIds) {
+            final int printerIdCount = pritnerIds.size();
+            for (int i = 0; i < printerIdCount; i++) {
+                PrinterId removedPrinterId = pritnerIds.get(i);
+                boolean removed = false;
+                final int existingPrinterCount = mDestinationSpinnerAdapter.getCount();
+                for (int j = 0; j < existingPrinterCount; j++) {
+                    PrinterInfo existingPrinter = mDestinationSpinnerAdapter.getItem(j).value;
+                    if (removedPrinterId.equals(existingPrinter.getId())) {
+                        mDestinationSpinnerAdapter.remove(mDestinationSpinnerAdapter.getItem(j));
+                        removed = true;
+                        break;
+                    }
+                }
+                if (!removed) {
+                    Log.w(LOG_TAG, "Ignoring not added printer with id: " + removedPrinterId);
+                }
+            }
+
+            if (mDestinationSpinner.getSelectedItemPosition() != AdapterView.INVALID_POSITION
+                    && mDestinationSpinnerAdapter.getCount() == 0) {
+                mDestinationSpinner.setSelection(AdapterView.INVALID_POSITION);
+            }
+        }
+
+        private void updatePrintPreview(File file) {
+            // TODO: Implement
+        }
+    }
 }
index 2bf62ee..25bb37c 100644 (file)
@@ -32,6 +32,8 @@ import android.print.PrintDocumentInfo;
 import android.util.Log;
 import android.util.Slog;
 
+import libcore.io.IoUtils;
+
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.FileOutputStream;
@@ -41,8 +43,6 @@ import java.io.OutputStream;
 import java.util.ArrayList;
 import java.util.List;
 
-import libcore.io.IoUtils;
-
 /**
  * This class represents a remote print document adapter instance.
  */
@@ -53,10 +53,12 @@ final class RemotePrintDocumentAdapter {
 
     public static final int STATE_INITIALIZED = 0;
     public static final int STATE_START_COMPLETED = 1;
-    public static final int STATE_LAYOUT_COMPLETED = 2;
-    public static final int STATE_WRITE_COMPLETED = 3;
-    public static final int STATE_FINISH_COMPLETED = 4;
-    public static final int STATE_FAILED = 6;
+    public static final int STATE_LAYOUT_STARTED = 2;
+    public static final int STATE_LAYOUT_COMPLETED = 3;
+    public static final int STATE_WRITE_STARTED = 4;
+    public static final int STATE_WRITE_COMPLETED = 5;
+    public static final int STATE_FINISH_COMPLETED = 6;
+    public static final int STATE_FAILED = 7;
 
     private final Object mLock = new Object();
 
@@ -94,7 +96,6 @@ final class RemotePrintDocumentAdapter {
                     Log.i(LOG_TAG, "start()");
                 }
                 synchronized (mLock) {
-                    mTaskQueue.add(this);
                     if (mState != STATE_INITIALIZED) {
                         throw new IllegalStateException("Invalid state: " + mState);
                     }
@@ -109,7 +110,15 @@ final class RemotePrintDocumentAdapter {
                 }
                 return null;
             }
+
+            @Override
+            public void cancel() {
+                /* cannot be cancelled */
+            }
         };
+        synchronized (mLock) {
+            mTaskQueue.add(task);
+        }
         task.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR, (Void[]) null);
     }
 
@@ -117,29 +126,37 @@ final class RemotePrintDocumentAdapter {
             LayoutResultCallback callback, Bundle metadata) {
         LayoutAsyncTask task = new LayoutAsyncTask(oldAttributes, newAttributes, callback,
                 metadata);
+        synchronized (mLock) {
+            mTaskQueue.add(task);
+        }
         task.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR, (Void[]) null);
     }
 
     public void write(List<PageRange> pages, WriteResultCallback callback) {
         WriteAsyncTask task = new WriteAsyncTask(pages, callback);
+        synchronized (mLock) {
+            mTaskQueue.add(task);
+        }
         task.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR, (Void[]) null);
     }
 
-    public void finish(final boolean abortPendingWork) {
+    public void cancel() {
+        synchronized (mLock) {
+            final int taskCount = mTaskQueue.size();
+            for (int i = taskCount - 1; i >= 0; i--) {
+                mTaskQueue.remove(i).cancel();
+            }
+        }
+    }
+
+    public void finish() {
         QueuedAsyncTask task = new QueuedAsyncTask() {
             @Override
             protected Void doInBackground(Void... params) {
                 if (DEBUG) {
-                    Log.i(LOG_TAG, "finish");
+                    Log.i(LOG_TAG, "finish()");
                 }
                 synchronized (mLock) {
-                    if (abortPendingWork) {
-                        final int taskCount = mTaskQueue.size();
-                        for (int i = taskCount - 1; i >= 0; i--) {
-                            mTaskQueue.remove(i).cancel();
-                        }
-                    }
-                    mTaskQueue.add(this);
                     if (mState < STATE_START_COMPLETED) {
                         return null;
                     }
@@ -155,7 +172,15 @@ final class RemotePrintDocumentAdapter {
                 }
                 return null;
             }
+
+            @Override
+            public void cancel() {
+                /* cannot be cancelled */
+            }
         };
+        synchronized (mLock) {
+            mTaskQueue.add(task);
+        }
         task.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR, (Void[]) null);
     }
 
@@ -179,6 +204,9 @@ final class RemotePrintDocumentAdapter {
                 new ILayoutResultCallback.Stub() {
             @Override
             public void onLayoutStarted(ICancellationSignal cancellationSignal) {
+                if (DEBUG) {
+                    Log.i(LOG_TAG, "onLayoutStarted()");
+                }
                 synchronized (mLock) {
                     mCancellationSignal = cancellationSignal;
                     if (isCancelled()) {
@@ -189,30 +217,43 @@ final class RemotePrintDocumentAdapter {
 
             @Override
             public void onLayoutFinished(PrintDocumentInfo info, boolean changed) {
+                if (DEBUG) {
+                    Log.i(LOG_TAG, "onLayoutFinished()");
+                }
+                final boolean cancelled;
                 synchronized (mLock) {
+                    cancelled = isCancelled();
                     mCancellationSignal = null;
-                    mCompleted = true;
+                    mState = STATE_LAYOUT_COMPLETED;
+                    mTaskQueue.remove(this);
                     mLock.notifyAll();
                 }
-                mCallback.onLayoutFinished(info, changed);
+                if (!cancelled) {
+                    mCallback.onLayoutFinished(info, changed);
+                }
             }
 
             @Override
             public void onLayoutFailed(CharSequence error) {
+                if (DEBUG) {
+                    Log.i(LOG_TAG, "onLayoutFailed()");
+                }
+                final boolean cancelled;
                 synchronized (mLock) {
+                    cancelled = isCancelled();
                     mCancellationSignal = null;
-                    mCompleted = true;
+                    mState = STATE_LAYOUT_COMPLETED;
+                    mTaskQueue.remove(this);
                     mLock.notifyAll();
                 }
-                Slog.e(LOG_TAG, "Error laying out print document: " + error);
-                mCallback.onLayoutFailed(error);
+                if (!cancelled) {
+                    mCallback.onLayoutFailed(error);
+                }
             }
         };
 
         private ICancellationSignal mCancellationSignal;
 
-        private boolean mCompleted;
-
         public LayoutAsyncTask(PrintAttributes oldAttributes, PrintAttributes newAttributes,
                 LayoutResultCallback callback, Bundle metadata) {
             mOldAttributes = oldAttributes;
@@ -233,25 +274,22 @@ final class RemotePrintDocumentAdapter {
         @Override
         protected Void doInBackground(Void... params) {
             synchronized (mLock) {
-                mTaskQueue.add(this);
+                if (DEBUG) {
+                    Log.i(LOG_TAG, "layout()");
+                }
                 if (mState != STATE_START_COMPLETED
                         && mState != STATE_LAYOUT_COMPLETED
                         && mState != STATE_WRITE_COMPLETED) {
                     throw new IllegalStateException("Invalid state: " + mState);
                 }
+                mState = STATE_LAYOUT_STARTED;
             }
             try {
                 mRemoteInterface.layout(mOldAttributes, mNewAttributes,
                         mILayoutResultCallback, mMetadata);
                 synchronized (mLock) {
                     while (true) {
-                        if (isCancelled()) {
-                            mTaskQueue.remove(this);
-                            break;
-                        }
-                        if (mCompleted) {
-                            mState = STATE_LAYOUT_COMPLETED;
-                            mTaskQueue.remove(this);
+                        if (mState == STATE_LAYOUT_COMPLETED) {
                             break;
                         }
                         try {
@@ -264,6 +302,8 @@ final class RemotePrintDocumentAdapter {
             } catch (RemoteException re) {
                 Slog.e(LOG_TAG, "Error calling layout", re);
                 mState = STATE_FAILED;
+                mTaskQueue.remove(this);
+                notifyLayoutFailedQuietly();
             }
             return null;
         }
@@ -274,10 +314,19 @@ final class RemotePrintDocumentAdapter {
                     mCancellationSignal.cancel();
                 } catch (RemoteException re) {
                     Slog.e(LOG_TAG, "Error cancelling layout", re);
+                    notifyLayoutFailedQuietly();
                 }
             }
         }
 
+        public void notifyLayoutFailedQuietly() {
+            try {
+                mILayoutResultCallback.onLayoutFailed(null);
+            } catch (RemoteException re) {
+                /* ignore */
+            }
+        }
+
         private void throwIfCancelledLocked() {
             if (isCancelled()) {
                 throw new IllegalStateException("Already cancelled");
@@ -295,6 +344,9 @@ final class RemotePrintDocumentAdapter {
                 new IWriteResultCallback.Stub() {
             @Override
             public void onWriteStarted(ICancellationSignal cancellationSignal) {
+                if (DEBUG) {
+                    Log.i(LOG_TAG, "onWriteStarted()");
+                }
                 synchronized (mLock) {
                     mCancellationSignal = cancellationSignal;
                     if (isCancelled()) {
@@ -305,9 +357,13 @@ final class RemotePrintDocumentAdapter {
 
             @Override
             public void onWriteFinished(List<PageRange> pages) {
+                if (DEBUG) {
+                    Log.i(LOG_TAG, "onWriteFinished()");
+                }
                 synchronized (mLock) {
                     mCancellationSignal = null;
-                    mCompleted = true;
+                    mState = STATE_WRITE_COMPLETED;
+                    mTaskQueue.remove(this);
                     mLock.notifyAll();
                 }
                 mCallback.onWriteFinished(pages);
@@ -315,9 +371,13 @@ final class RemotePrintDocumentAdapter {
 
             @Override
             public void onWriteFailed(CharSequence error) {
+                if (DEBUG) {
+                    Log.i(LOG_TAG, "onWriteFailed()");
+                }
                 synchronized (mLock) {
                     mCancellationSignal = null;
-                    mCompleted = true;
+                    mState = STATE_WRITE_COMPLETED;
+                    mTaskQueue.remove(this);
                     mLock.notifyAll();
                 }
                 Slog.e(LOG_TAG, "Error writing print document: " + error);
@@ -327,8 +387,6 @@ final class RemotePrintDocumentAdapter {
 
         private ICancellationSignal mCancellationSignal;
 
-        private boolean mCompleted;
-
         private Thread mWriteThread;
 
         public WriteAsyncTask(List<PageRange> pages, WriteResultCallback callback) {
@@ -349,14 +407,14 @@ final class RemotePrintDocumentAdapter {
         @Override
         protected Void doInBackground(Void... params) {
             if (DEBUG) {
-                Log.i(LOG_TAG, "print()");
+                Log.i(LOG_TAG, "write()");
             }
             synchronized (mLock) {
-                mTaskQueue.add(this);
                 if (mState != STATE_LAYOUT_COMPLETED
                         && mState != STATE_WRITE_COMPLETED) {
                     throw new IllegalStateException("Invalid state: " + mState);
                 }
+                mState = STATE_WRITE_STARTED;
             }
             InputStream in = null;
             OutputStream out = null;
@@ -394,13 +452,7 @@ final class RemotePrintDocumentAdapter {
                 }
                 synchronized (mLock) {
                     while (true) {
-                        if (isCancelled()) {
-                            mTaskQueue.remove(this);
-                            break;
-                        }
-                        if (mCompleted) {
-                            mState = STATE_WRITE_COMPLETED;
-                            mTaskQueue.remove(this);
+                        if (mState == STATE_WRITE_COMPLETED) {
                             break;
                         }
                         try {
@@ -413,9 +465,13 @@ final class RemotePrintDocumentAdapter {
             } catch (RemoteException re) {
                 Slog.e(LOG_TAG, "Error writing print document", re);
                 mState = STATE_FAILED;
+                mTaskQueue.remove(this);
+                notifyWriteFailedQuietly();
             } catch (IOException ioe) {
                 Slog.e(LOG_TAG, "Error writing print document", ioe);
                 mState = STATE_FAILED;
+                mTaskQueue.remove(this);
+                notifyWriteFailedQuietly();
             } finally {
                 IoUtils.closeQuietly(in);
                 IoUtils.closeQuietly(out);
@@ -431,10 +487,19 @@ final class RemotePrintDocumentAdapter {
                     mCancellationSignal.cancel();
                 } catch (RemoteException re) {
                     Slog.e(LOG_TAG, "Error cancelling layout", re);
+                    notifyWriteFailedQuietly();
                 }
             }
         }
 
+        private void notifyWriteFailedQuietly() {
+            try {
+                mIWriteResultCallback.onWriteFailed(null);
+            } catch (RemoteException re) {
+                /* ignore */
+            }
+        }
+
         private void throwIfCancelledLocked() {
             if (isCancelled()) {
                 throw new IllegalStateException("Already cancelled");