Merge "Added the callback signal." into klp-dev
diff --git a/Android.mk b/Android.mk
index 8e59ffa..3c9cc2c 100644
--- a/Android.mk
+++ b/Android.mk
@@ -162,8 +162,6 @@
 	core/java/android/os/IVibratorService.aidl \
 	core/java/android/service/notification/INotificationListener.aidl \
 	core/java/android/print/ILayoutResultCallback.aidl \
-	core/java/android/print/IPrinterDiscoverySessionController.aidl \
-	core/java/android/print/IPrinterDiscoverySessionObserver.aidl \
 	core/java/android/print/IPrintDocumentAdapter.aidl \
 	core/java/android/print/IPrintClient.aidl \
 	core/java/android/print/IPrintManager.aidl \
diff --git a/api/current.txt b/api/current.txt
index 42ba6b8..394acba 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -5014,7 +5014,8 @@
   }
 
   public final class BluetoothGatt implements android.bluetooth.BluetoothProfile {
-    method public void abortReliableWrite(android.bluetooth.BluetoothDevice);
+    method public void abortReliableWrite();
+    method public deprecated void abortReliableWrite(android.bluetooth.BluetoothDevice);
     method public boolean beginReliableWrite();
     method public void close();
     method public boolean connect();
@@ -12796,6 +12797,7 @@
 
   public static class MediaPlayer.TrackInfo implements android.os.Parcelable {
     method public int describeContents();
+    method public android.media.MediaFormat getFormat();
     method public java.lang.String getLanguage();
     method public int getTrackType();
     method public void writeToParcel(android.os.Parcel, int);
@@ -19036,7 +19038,7 @@
     method public android.print.PrintAttributes getAttributes();
     method public int getCopies();
     method public int getId();
-    method public java.lang.CharSequence getLabel();
+    method public java.lang.String getLabel();
     method public android.print.PageRange[] getPages();
     method public android.print.PrinterId getPrinterId();
     method public int getState();
@@ -19160,7 +19162,7 @@
   public final class PrintJob {
     method public boolean cancel();
     method public boolean complete();
-    method public boolean fail(java.lang.CharSequence);
+    method public boolean fail(java.lang.String);
     method public android.printservice.PrintDocument getDocument();
     method public int getId();
     method public android.print.PrintJobInfo getInfo();
@@ -19189,11 +19191,15 @@
   }
 
   public abstract class PrinterDiscoverySession {
-    ctor public PrinterDiscoverySession(android.content.Context);
+    ctor public PrinterDiscoverySession();
     method public final void addPrinters(java.util.List<android.print.PrinterInfo>);
-    method public abstract void onClose();
-    method public abstract void onOpen(java.util.List<android.print.PrinterId>);
+    method public final java.util.List<android.print.PrinterInfo> getPrinters();
+    method public final boolean isDestroyed();
+    method public final boolean isPrinterDiscoveryStarted();
+    method public abstract void onDestroy();
     method public abstract void onRequestPrinterUpdate(android.print.PrinterId);
+    method public abstract void onStartPrinterDiscovery(java.util.List<android.print.PrinterId>);
+    method public abstract void onStopPrinterDiscovery();
     method public final void removePrinters(java.util.List<android.print.PrinterId>);
     method public final void updatePrinters(java.util.List<android.print.PrinterInfo>);
   }
@@ -21139,16 +21145,20 @@
     method public static float getFloat(android.content.ContentResolver, java.lang.String) throws android.provider.Settings.SettingNotFoundException;
     method public static int getInt(android.content.ContentResolver, java.lang.String, int);
     method public static int getInt(android.content.ContentResolver, java.lang.String) throws android.provider.Settings.SettingNotFoundException;
+    method public static final int getLocationMode(android.content.ContentResolver);
+    method public static final int getLocationModeForUser(android.content.ContentResolver, int);
     method public static long getLong(android.content.ContentResolver, java.lang.String, long);
     method public static long getLong(android.content.ContentResolver, java.lang.String) throws android.provider.Settings.SettingNotFoundException;
     method public static java.lang.String getString(android.content.ContentResolver, java.lang.String);
     method public static android.net.Uri getUriFor(java.lang.String);
-    method public static final boolean isLocationProviderEnabled(android.content.ContentResolver, java.lang.String);
+    method public static final deprecated boolean isLocationProviderEnabled(android.content.ContentResolver, java.lang.String);
     method public static boolean putFloat(android.content.ContentResolver, java.lang.String, float);
     method public static boolean putInt(android.content.ContentResolver, java.lang.String, int);
     method public static boolean putLong(android.content.ContentResolver, java.lang.String, long);
     method public static boolean putString(android.content.ContentResolver, java.lang.String, java.lang.String);
-    method public static final void setLocationProviderEnabled(android.content.ContentResolver, java.lang.String, boolean);
+    method public static final void setLocationMode(android.content.ContentResolver, int);
+    method public static final void setLocationModeForUser(android.content.ContentResolver, int, int);
+    method public static final deprecated void setLocationProviderEnabled(android.content.ContentResolver, java.lang.String, boolean);
     field public static final java.lang.String ACCESSIBILITY_ENABLED = "accessibility_enabled";
     field public static final java.lang.String ACCESSIBILITY_SPEAK_PASSWORD = "speak_password";
     field public static final deprecated java.lang.String ADB_ENABLED = "adb_enabled";
@@ -21167,6 +21177,10 @@
     field public static final deprecated java.lang.String HTTP_PROXY = "http_proxy";
     field public static final java.lang.String INPUT_METHOD_SELECTOR_VISIBILITY = "input_method_selector_visibility";
     field public static final deprecated java.lang.String INSTALL_NON_MARKET_APPS = "install_non_market_apps";
+    field public static final int LOCATION_MODE_BATTERY_SAVING = 2; // 0x2
+    field public static final int LOCATION_MODE_HIGH_ACCURACY = 3; // 0x3
+    field public static final int LOCATION_MODE_OFF = 0; // 0x0
+    field public static final int LOCATION_MODE_SENSORS_ONLY = 1; // 0x1
     field public static final java.lang.String LOCATION_PROVIDERS_ALLOWED = "location_providers_allowed";
     field public static final java.lang.String LOCK_PATTERN_ENABLED = "lock_pattern_autolock";
     field public static final deprecated java.lang.String LOCK_PATTERN_TACTILE_FEEDBACK_ENABLED = "lock_pattern_tactile_feedback_enabled";
diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java
index be831d7..e0b1c00 100644
--- a/core/java/android/app/admin/DevicePolicyManager.java
+++ b/core/java/android/app/admin/DevicePolicyManager.java
@@ -32,10 +32,17 @@
 import android.os.UserHandle;
 import android.util.Log;
 
+import com.android.org.conscrypt.TrustedCertificateStore;
+
+import java.io.ByteArrayInputStream;
 import java.io.IOException;
 import java.net.InetSocketAddress;
 import java.net.Proxy;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
 import java.util.List;
+import java.util.Set;
 
 /**
  * Public interface for managing policies enforced on a device.  Most clients
@@ -1328,6 +1335,70 @@
     }
 
     /**
+     * Installs the given certificate as a User CA.
+     *
+     * @return false if the certBuffer cannot be parsed or installation is
+     *         interrupted, otherwise true
+     * @hide
+     */
+    public boolean installCaCert(byte[] certBuffer) {
+        if (mService != null) {
+            try {
+                return mService.installCaCert(certBuffer);
+            } catch (RemoteException e) {
+                Log.w(TAG, "Failed talking with device policy service", e);
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Uninstalls the given certificate from the list of User CAs, if present.
+     *
+     * @hide
+     */
+    public void uninstallCaCert(byte[] certBuffer) {
+        if (mService != null) {
+            try {
+                mService.uninstallCaCert(certBuffer);
+            } catch (RemoteException e) {
+                Log.w(TAG, "Failed talking with device policy service", e);
+            }
+        }
+    }
+
+    /**
+     * Returns whether there are any user-installed CA certificates.
+     *
+     * @hide
+     */
+    public boolean hasAnyCaCertsInstalled() {
+        TrustedCertificateStore certStore = new TrustedCertificateStore();
+        Set<String> aliases = certStore.userAliases();
+        return aliases != null && !aliases.isEmpty();
+    }
+
+    /**
+     * Returns whether this certificate has been installed as a User CA.
+     *
+     * @hide
+     */
+    public boolean hasCaCertInstalled(byte[] certBuffer) {
+        TrustedCertificateStore certStore = new TrustedCertificateStore();
+        String alias;
+        byte[] pemCert;
+        try {
+            CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
+            X509Certificate cert = (X509Certificate) certFactory.generateCertificate(
+                            new ByteArrayInputStream(certBuffer));
+            return certStore.getCertificateAlias(cert) != null;
+        } catch (CertificateException ce) {
+            Log.w(TAG, "Could not parse certificate", ce);
+        }
+        return false;
+    }
+
+    /**
      * Called by an application that is administering the device to disable all cameras
      * on the device.  After setting this, no applications will be able to access any cameras
      * on the device.
diff --git a/core/java/android/app/admin/IDevicePolicyManager.aidl b/core/java/android/app/admin/IDevicePolicyManager.aidl
index 9659a91..172c47c 100644
--- a/core/java/android/app/admin/IDevicePolicyManager.aidl
+++ b/core/java/android/app/admin/IDevicePolicyManager.aidl
@@ -102,4 +102,7 @@
     boolean isDeviceOwner(String packageName);
     String getDeviceOwner();
     String getDeviceOwnerName();
+
+    boolean installCaCert(in byte[] certBuffer);
+    void uninstallCaCert(in byte[] certBuffer);
 }
diff --git a/core/java/android/bluetooth/BluetoothGatt.java b/core/java/android/bluetooth/BluetoothGatt.java
index a8c310b..b390aa1 100644
--- a/core/java/android/bluetooth/BluetoothGatt.java
+++ b/core/java/android/bluetooth/BluetoothGatt.java
@@ -1102,7 +1102,7 @@
      *
      * <p>Requires {@link android.Manifest.permission#BLUETOOTH} permission.
      */
-    public void abortReliableWrite(BluetoothDevice mDevice) {
+    public void abortReliableWrite() {
         if (DBG) Log.d(TAG, "abortReliableWrite() - device: " + mDevice.getAddress());
         if (mService == null || mClientIf == 0) return;
 
@@ -1114,6 +1114,13 @@
     }
 
     /**
+     * @deprecated Use {@link #abortReliableWrite()}
+     */
+    public void abortReliableWrite(BluetoothDevice mDevice) {
+        abortReliableWrite();
+    }
+
+    /**
      * Enable or disable notifications/indications for a given characteristic.
      *
      * <p>Once notifications are enabled for a characteristic, a
diff --git a/core/java/android/print/IPrintSpooler.aidl b/core/java/android/print/IPrintSpooler.aidl
index 8178180..5c8a22a 100644
--- a/core/java/android/print/IPrintSpooler.aidl
+++ b/core/java/android/print/IPrintSpooler.aidl
@@ -18,6 +18,7 @@
 
 import android.content.ComponentName;
 import android.os.ParcelFileDescriptor;
+import android.print.PrinterId;
 import android.print.IPrintDocumentAdapter;
 import android.print.IPrintClient;
 import android.print.IPrintSpoolerClient;
@@ -40,10 +41,15 @@
     void createPrintJob(String printJobName, in IPrintClient client,
             in IPrintDocumentAdapter printAdapter, in PrintAttributes attributes,
             IPrintSpoolerCallbacks callback, int appId, int sequence);
-    void setPrintJobState(int printJobId, int status, CharSequence error,
+    void setPrintJobState(int printJobId, int status, String error,
             IPrintSpoolerCallbacks callback, int sequence);
     void setPrintJobTag(int printJobId, String tag, IPrintSpoolerCallbacks callback,
             int sequence);
     void writePrintJobData(in ParcelFileDescriptor fd, int printJobId);
     void setClient(IPrintSpoolerClient client);
+
+    // Printer discovery APIs
+    void onPrintersAdded(in List<PrinterInfo> printers);
+    void onPrintersRemoved(in List<PrinterId> printerIds);
+    void onPrintersUpdated(in List<PrinterInfo> printerIds);
 }
diff --git a/core/java/android/print/IPrintSpoolerClient.aidl b/core/java/android/print/IPrintSpoolerClient.aidl
index 8db2169..da60120 100644
--- a/core/java/android/print/IPrintSpoolerClient.aidl
+++ b/core/java/android/print/IPrintSpoolerClient.aidl
@@ -17,7 +17,6 @@
 package android.print;
 
 import android.content.ComponentName;
-import android.print.IPrinterDiscoverySessionObserver;
 import android.print.PrinterId;
 import android.print.PrintJobInfo;
 
@@ -28,8 +27,14 @@
  * @hide
  */
 oneway interface IPrintSpoolerClient {
-    void createPrinterDiscoverySession(IPrinterDiscoverySessionObserver observer);
     void onPrintJobQueued(in PrintJobInfo printJob);
     void onAllPrintJobsForServiceHandled(in ComponentName printService);
     void onAllPrintJobsHandled();
+
+    // Printer discovery APIs
+    void createPrinterDiscoverySession();
+    void startPrinterDiscovery(in List<PrinterId> priorityList);
+    void stopPrinterDiscovery();
+    void requestPrinterUpdate(in PrinterId printerId);
+    void destroyPrinterDiscoverySession();
 }
diff --git a/core/java/android/print/IPrinterDiscoverySessionController.aidl b/core/java/android/print/IPrinterDiscoverySessionController.aidl
deleted file mode 100644
index 13116ef..0000000
--- a/core/java/android/print/IPrinterDiscoverySessionController.aidl
+++ /dev/null
@@ -1,30 +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 android.print;
-
-import android.print.PrinterId;
-
-/**
-* Interface for the controlling part of a printer discovery session.
- *
- * @hide
- */
-oneway interface IPrinterDiscoverySessionController {
-    void open(in List<PrinterId> priorityList);
-    void requestPrinterUpdate(in PrinterId printerId);
-    void close();
-}
diff --git a/core/java/android/print/IPrinterDiscoverySessionObserver.aidl b/core/java/android/print/IPrinterDiscoverySessionObserver.aidl
deleted file mode 100644
index a78924c..0000000
--- a/core/java/android/print/IPrinterDiscoverySessionObserver.aidl
+++ /dev/null
@@ -1,33 +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 android.print;
-
-import android.print.IPrinterDiscoverySessionController;
-import android.print.PrinterId;
-import android.print.PrinterInfo;
-
-/**
- * Interface for the observing part of a printer discovery session.
- *
- * @hide
- */
-oneway interface IPrinterDiscoverySessionObserver {
-    void setController(IPrinterDiscoverySessionController controller);
-    void onPrintersAdded(in List<PrinterInfo> printers);
-    void onPrintersRemoved(in List<PrinterId> printerIds);
-    void onPrintersUpdated(in List<PrinterInfo> printerIds);
-}
diff --git a/core/java/android/print/PrintJobInfo.java b/core/java/android/print/PrintJobInfo.java
index 2fb4751..602f3c1 100644
--- a/core/java/android/print/PrintJobInfo.java
+++ b/core/java/android/print/PrintJobInfo.java
@@ -104,7 +104,7 @@
     private int mId;
 
     /** The human readable print job label. */
-    private CharSequence mLabel;
+    private String mLabel;
 
     /** The unique id of the printer. */
     private PrinterId mPrinterId;
@@ -128,7 +128,7 @@
     private int mCopies;
 
     /** Failure reason if this job failed. */
-    private CharSequence mFailureReason;
+    private String mFailureReason;
 
     /** The pages to print */
     private PageRange[] mPageRanges;
@@ -163,7 +163,7 @@
 
     private PrintJobInfo(Parcel parcel) {
         mId = parcel.readInt();
-        mLabel = parcel.readCharSequence();
+        mLabel = parcel.readString();
         mPrinterId = parcel.readParcelable(null);
         mPrinterName = parcel.readString();
         mState = parcel.readInt();
@@ -171,9 +171,7 @@
         mUserId = parcel.readInt();
         mTag = parcel.readString();
         mCopies = parcel.readInt();
-        if (parcel.readInt() == 1) {
-            mFailureReason = parcel.readCharSequence();
-        }
+        mFailureReason = parcel.readString();
         if (parcel.readInt() == 1) {
             Parcelable[] parcelables = parcel.readParcelableArray(null);
             mPageRanges = new PageRange[parcelables.length];
@@ -214,7 +212,7 @@
      *
      * @return The label.
      */
-    public CharSequence getLabel() {
+    public String getLabel() {
         return mLabel;
     }
 
@@ -225,7 +223,7 @@
      *
      * @hide
      */
-    public void setLabel(CharSequence label) {
+    public void setLabel(String label) {
         mLabel = label;
     }
 
@@ -385,7 +383,7 @@
      *
      * @hide
      */
-    public CharSequence getFailureReason() {
+    public String getFailureReason() {
         return mFailureReason;
     }
 
@@ -396,7 +394,7 @@
      *
      * @hide
      */
-    public void setFailureReason(CharSequence failureReason) {
+    public void setFailureReason(String failureReason) {
         mFailureReason = failureReason;
     }
 
@@ -470,7 +468,7 @@
     @Override
     public void writeToParcel(Parcel parcel, int flags) {
         parcel.writeInt(mId);
-        parcel.writeCharSequence(mLabel);
+        parcel.writeString(mLabel);
         parcel.writeParcelable(mPrinterId, flags);
         parcel.writeString(mPrinterName);
         parcel.writeInt(mState);
@@ -478,12 +476,7 @@
         parcel.writeInt(mUserId);
         parcel.writeString(mTag);
         parcel.writeInt(mCopies);
-        if (mFailureReason != null) {
-            parcel.writeInt(1);
-            parcel.writeCharSequence(mFailureReason);
-        } else {
-            parcel.writeInt(0);
-        }
+        parcel.writeString(mFailureReason);
         if (mPageRanges != null) {
             parcel.writeInt(1);
             parcel.writeParcelableArray(mPageRanges, flags);
diff --git a/core/java/android/print/PrintManager.java b/core/java/android/print/PrintManager.java
index 636b9d4..531dcb2 100644
--- a/core/java/android/print/PrintManager.java
+++ b/core/java/android/print/PrintManager.java
@@ -374,14 +374,14 @@
 
             @Override
             public void onLayoutFinished(PrintDocumentInfo info, boolean changed) {
+                if (info == null) {
+                    throw new NullPointerException("document info cannot be null");
+                }
                 final ILayoutResultCallback callback;
                 synchronized (mLock) {
                     callback = mCallback;
                     clearLocked();
                 }
-                if (info == null) {
-                    throw new IllegalArgumentException("info cannot be null");
-                }
                 if (callback != null) {
                     try {
                         callback.onLayoutFinished(info, changed, mSequence);
diff --git a/core/java/android/print/PrinterInfo.java b/core/java/android/print/PrinterInfo.java
index ac782a8..6f567a6 100644
--- a/core/java/android/print/PrinterInfo.java
+++ b/core/java/android/print/PrinterInfo.java
@@ -229,10 +229,11 @@
         /**
          * Constructor.
          *
-         * @param prototype Prototype from which to start building.
+         * @param other Other info from which to start building.
          */
-        public Builder(PrinterInfo prototype) {
-            mPrototype = prototype;
+        public Builder(PrinterInfo other) {
+            mPrototype = new PrinterInfo();
+            mPrototype.copyFrom(other);
         }
 
         /**
diff --git a/core/java/android/print/pdf/PdfDocument.java b/core/java/android/print/pdf/PdfDocument.java
index cfeb975..dbd7dd1 100644
--- a/core/java/android/print/pdf/PdfDocument.java
+++ b/core/java/android/print/pdf/PdfDocument.java
@@ -324,7 +324,7 @@
             /**
              * Creates a new builder with the mandatory page info attributes.
              *
-             * @param pageSize The page size in pixels.
+             * @param pageSize The page size in points, <strong>not</strong> dips.
              * @param pageNumber The page number.
              * @param density The page density in DPI.
              */
diff --git a/core/java/android/printservice/IPrintService.aidl b/core/java/android/printservice/IPrintService.aidl
index 16b7a26..2cee1d8 100644
--- a/core/java/android/printservice/IPrintService.aidl
+++ b/core/java/android/printservice/IPrintService.aidl
@@ -16,7 +16,7 @@
 
 package android.printservice;
 
-import android.print.IPrinterDiscoverySessionObserver;
+import android.print.PrinterId;
 import android.print.PrintJobInfo;
 import android.printservice.IPrintServiceClient;
 
@@ -29,5 +29,10 @@
     void setClient(IPrintServiceClient client);
     void requestCancelPrintJob(in PrintJobInfo printJobInfo);
     void onPrintJobQueued(in PrintJobInfo printJobInfo);
-    void createPrinterDiscoverySession(IPrinterDiscoverySessionObserver observer);
+
+    void createPrinterDiscoverySession();
+    void startPrinterDiscovery(in List<PrinterId> priorityList);
+    void stopPrinterDiscovery();
+    void requestPrinterUpdate(in PrinterId printerId);
+    void destroyPrinterDiscoverySession();
 }
diff --git a/core/java/android/printservice/IPrintServiceClient.aidl b/core/java/android/printservice/IPrintServiceClient.aidl
index f00b37c4..1e33fc0 100644
--- a/core/java/android/printservice/IPrintServiceClient.aidl
+++ b/core/java/android/printservice/IPrintServiceClient.aidl
@@ -29,7 +29,11 @@
 interface IPrintServiceClient {
     List<PrintJobInfo> getPrintJobInfos();
     PrintJobInfo getPrintJobInfo(int printJobId);
-    boolean setPrintJobState(int printJobId, int state, CharSequence error);
+    boolean setPrintJobState(int printJobId, int state, String error);
     boolean setPrintJobTag(int printJobId, String tag);
     oneway void writePrintJobData(in ParcelFileDescriptor fd, int printJobId);
+
+    void onPrintersAdded(in List<PrinterInfo> printers);
+    void onPrintersRemoved(in List<PrinterId> printerIds);
+    void onPrintersUpdated(in List<PrinterInfo> printers);
 }
diff --git a/core/java/android/printservice/PrintJob.java b/core/java/android/printservice/PrintJob.java
index 5f9a730..d2fbef2 100644
--- a/core/java/android/printservice/PrintJob.java
+++ b/core/java/android/printservice/PrintJob.java
@@ -24,6 +24,10 @@
  * This class represents a print job from the perspective of a print
  * service. It provides APIs for observing the print job state and
  * performing operations on the print job.
+ * <p>
+ * <strong>Note: </strong> All methods of this class must be executed on the main
+ * application thread.
+ * </p>
  */
 public final class PrintJob {
 
@@ -48,6 +52,7 @@
      * @return The id.
      */
     public int getId() {
+        PrintService.throwIfNotCalledOnMainThread();
         return mCachedInfo.getId();
     }
 
@@ -62,6 +67,7 @@
      * @return The print job info.
      */
     public PrintJobInfo getInfo() {
+        PrintService.throwIfNotCalledOnMainThread();
         if (isInImmutableState()) {
             return mCachedInfo;
         }
@@ -83,6 +89,7 @@
      * @return The document.
      */
     public PrintDocument getDocument() {
+        PrintService.throwIfNotCalledOnMainThread();
         return mDocument;
     }
 
@@ -96,6 +103,7 @@
      * @see #cancel()
      */
     public boolean isQueued() {
+        PrintService.throwIfNotCalledOnMainThread();
         return getInfo().getState() == PrintJobInfo.STATE_QUEUED;
     }
 
@@ -110,6 +118,7 @@
      * @see #fail(CharSequence)
      */
     public boolean isStarted() {
+        PrintService.throwIfNotCalledOnMainThread();
         return getInfo().getState() == PrintJobInfo.STATE_STARTED;
     }
 
@@ -122,6 +131,7 @@
      * @see #complete()
      */
     public boolean isCompleted() {
+        PrintService.throwIfNotCalledOnMainThread();
         return getInfo().getState() == PrintJobInfo.STATE_COMPLETED;
     }
 
@@ -134,6 +144,7 @@
      * @see #fail(CharSequence)
      */
     public boolean isFailed() {
+        PrintService.throwIfNotCalledOnMainThread();
         return getInfo().getState() == PrintJobInfo.STATE_FAILED;
     }
 
@@ -146,6 +157,7 @@
      * @see #cancel()
      */
     public boolean isCancelled() {
+        PrintService.throwIfNotCalledOnMainThread();
         return getInfo().getState() == PrintJobInfo.STATE_FAILED;
     }
 
@@ -158,6 +170,7 @@
      * @see #isQueued()
      */
     public boolean start() {
+        PrintService.throwIfNotCalledOnMainThread();
         if (isQueued()) {
             return setState(PrintJobInfo.STATE_STARTED, null);
         }
@@ -173,6 +186,7 @@
      * @see #isStarted()
      */
     public boolean complete() {
+        PrintService.throwIfNotCalledOnMainThread();
         if (isStarted()) {
             return setState(PrintJobInfo.STATE_COMPLETED, null);
         }
@@ -191,7 +205,8 @@
      * @see #isQueued()
      * @see #isStarted()
      */
-    public boolean fail(CharSequence error) {
+    public boolean fail(String error) {
+        PrintService.throwIfNotCalledOnMainThread();
         if (isQueued() || isStarted()) {
             return setState(PrintJobInfo.STATE_FAILED, error);
         }
@@ -210,6 +225,7 @@
      * @see #isQueued()
      */
     public boolean cancel() {
+        PrintService.throwIfNotCalledOnMainThread();
         if (isQueued() || isStarted()) {
             return setState(PrintJobInfo.STATE_CANCELED, null);
         }
@@ -226,6 +242,7 @@
      * @return True if the tag was set, false otherwise.
      */
     public boolean setTag(String tag) {
+        PrintService.throwIfNotCalledOnMainThread();
         if (isInImmutableState()) {
             return false;
         }
@@ -263,7 +280,7 @@
                 || state == PrintJobInfo.STATE_CANCELED;
     }
 
-    private boolean setState(int state, CharSequence error) {
+    private boolean setState(int state, String error) {
         try {
             if (mPrintServiceClient.setPrintJobState(mCachedInfo.getId(), state, error)) {
                 // Best effort - update the state of the cached info since
diff --git a/core/java/android/printservice/PrintService.java b/core/java/android/printservice/PrintService.java
index 92bccd4..8fe770c 100644
--- a/core/java/android/printservice/PrintService.java
+++ b/core/java/android/printservice/PrintService.java
@@ -25,7 +25,6 @@
 import android.os.Looper;
 import android.os.Message;
 import android.os.RemoteException;
-import android.print.IPrinterDiscoverySessionObserver;
 import android.print.PrintJobInfo;
 import android.print.PrinterId;
 import android.util.Log;
@@ -146,6 +145,11 @@
  * {@link #SERVICE_META_DATA} and <code>&lt;{@link android.R.styleable#PrintService
  * print-service}&gt;</code>.
  * </p>
+ * <p>
+ * <strong>Note: </strong> All callbacks in this class are executed on the main
+ * application thread. You should also invoke any method of this class on the main
+ * application thread.
+ * </p>
  */
 public abstract class PrintService extends Service {
 
@@ -175,14 +179,14 @@
      */
     public static final String SERVICE_META_DATA = "android.printservice";
 
-    private final Object mLock = new Object();
-
     private Handler mHandler;
 
     private IPrintServiceClient mClient;
 
     private int mLastSessionId = -1;
 
+    private PrinterDiscoverySession mDiscoverySession;
+
     @Override
     protected final void attachBaseContext(Context base) {
         super.attachBaseContext(base);
@@ -245,21 +249,18 @@
      * @see PrintJob#isStarted() PrintJob.isStarted()
      */
     public final List<PrintJob> getActivePrintJobs() {
-        final IPrintServiceClient client;
-        synchronized (mLock) {
-            client = mClient;
-        }
-        if (client == null) {
+        throwIfNotCalledOnMainThread();
+        if (mClient == null) {
             return Collections.emptyList();
         }
         try {
             List<PrintJob> printJobs = null;
-            List<PrintJobInfo> printJobInfos = client.getPrintJobInfos();
+            List<PrintJobInfo> printJobInfos = mClient.getPrintJobInfos();
             if (printJobInfos != null) {
                 final int printJobInfoCount = printJobInfos.size();
                 printJobs = new ArrayList<PrintJob>(printJobInfoCount);
                 for (int i = 0; i < printJobInfoCount; i++) {
-                    printJobs.add(new PrintJob(printJobInfos.get(i), client));
+                    printJobs.add(new PrintJob(printJobInfos.get(i), mClient));
                 }
             }
             if (printJobs != null) {
@@ -278,23 +279,50 @@
      * @return Global printer id.
      */
     public final PrinterId generatePrinterId(String localId) {
+        throwIfNotCalledOnMainThread();
         return new PrinterId(new ComponentName(getPackageName(),
                 getClass().getName()), localId);
     }
 
+    static void throwIfNotCalledOnMainThread() {
+        if (!Looper.getMainLooper().isCurrentThread()) {
+            throw new IllegalAccessError("must be called from the main thread");
+        }
+    }
+
     @Override
     public final IBinder onBind(Intent intent) {
         return new IPrintService.Stub() {
             @Override
-            public void setClient(IPrintServiceClient client) {
-                mHandler.obtainMessage(ServiceHandler.MSG_SET_CLEINT, client)
-                        .sendToTarget();
+            public void createPrinterDiscoverySession() {
+                mHandler.sendEmptyMessage(ServiceHandler.MSG_CREATE_PRINTER_DISCOVERY_SESSION);
             }
 
             @Override
-            public void createPrinterDiscoverySession(IPrinterDiscoverySessionObserver observer) {
-                mHandler.obtainMessage(ServiceHandler.MSG_ON_CREATE_PRINTER_DISCOVERY_SESSION,
-                        observer).sendToTarget();
+            public void destroyPrinterDiscoverySession() {
+                mHandler.sendEmptyMessage(ServiceHandler.MSG_DESTROY_PRINTER_DISCOVERY_SESSION);
+            }
+
+            public void startPrinterDiscovery(List<PrinterId> priorityList) {
+                mHandler.obtainMessage(ServiceHandler.MSG_START_PRINTER_DISCOVERY,
+                        priorityList).sendToTarget();
+            }
+
+            @Override
+            public void stopPrinterDiscovery() {
+                mHandler.sendEmptyMessage(ServiceHandler.MSG_STOP_PRINTER_DISCOVERY);
+            }
+
+            @Override
+            public void requestPrinterUpdate(PrinterId printerId) {
+                mHandler.obtainMessage(ServiceHandler.MSG_REQUEST_PRINTER_UPDATE,
+                        printerId).sendToTarget();
+            }
+
+            @Override
+            public void setClient(IPrintServiceClient client) {
+                mHandler.obtainMessage(ServiceHandler.MSG_SET_CLEINT, client)
+                        .sendToTarget();
             }
 
             @Override
@@ -312,33 +340,62 @@
     }
 
     private final class ServiceHandler extends Handler {
-        public static final int MSG_ON_CREATE_PRINTER_DISCOVERY_SESSION = 1;
-        public static final int MSG_ON_PRINTJOB_QUEUED = 2;
-        public static final int MSG_ON_REQUEST_CANCEL_PRINTJOB = 3;
-        public static final int MSG_SET_CLEINT = 4;
+        public static final int MSG_CREATE_PRINTER_DISCOVERY_SESSION = 1;
+        public static final int MSG_DESTROY_PRINTER_DISCOVERY_SESSION = 2;
+        public static final int MSG_START_PRINTER_DISCOVERY = 3;
+        public static final int MSG_STOP_PRINTER_DISCOVERY = 4;
+        public static final int MSG_REQUEST_PRINTER_UPDATE = 5;
+        public static final int MSG_ON_PRINTJOB_QUEUED = 6;
+        public static final int MSG_ON_REQUEST_CANCEL_PRINTJOB = 7;
+        public static final int MSG_SET_CLEINT = 8;
 
         public ServiceHandler(Looper looper) {
             super(looper, null, true);
         }
 
         @Override
+        @SuppressWarnings("unchecked")
         public void handleMessage(Message message) {
             final int action = message.what;
             switch (action) {
-                case MSG_ON_CREATE_PRINTER_DISCOVERY_SESSION: {
-                    IPrinterDiscoverySessionObserver observer =
-                            (IPrinterDiscoverySessionObserver) message.obj;
+                case MSG_CREATE_PRINTER_DISCOVERY_SESSION: {
                     PrinterDiscoverySession session = onCreatePrinterDiscoverySession();
                     if (session == null) {
                         throw new NullPointerException("session cannot be null");
                     }
-                    synchronized (mLock) {
-                        if (session.getId() == mLastSessionId) {
-                            throw new IllegalStateException("cannot reuse sessions");
-                        }
-                        mLastSessionId = session.getId();
+                    if (session.getId() == mLastSessionId) {
+                        throw new IllegalStateException("cannot reuse session instances");
                     }
-                    session.setObserver(observer);
+                    mDiscoverySession = session;
+                    mLastSessionId = session.getId();
+                    session.setObserver(mClient);
+                } break;
+
+                case MSG_DESTROY_PRINTER_DISCOVERY_SESSION: {
+                    if (mDiscoverySession != null) {
+                        mDiscoverySession.destroy();
+                        mDiscoverySession = null;
+                    }
+                } break;
+
+                case MSG_START_PRINTER_DISCOVERY: {
+                    if (mDiscoverySession != null) {
+                        List<PrinterId> priorityList = (ArrayList<PrinterId>) message.obj;
+                        mDiscoverySession.startPrinterDiscovery(priorityList);
+                    }
+                } break;
+
+                case MSG_STOP_PRINTER_DISCOVERY: {
+                    if (mDiscoverySession != null) {
+                        mDiscoverySession.stopPrinterDiscovery();
+                    }
+                } break;
+
+                case MSG_REQUEST_PRINTER_UPDATE: {
+                    if (mDiscoverySession != null) {
+                        PrinterId printerId = (PrinterId) message.obj;
+                        mDiscoverySession.requestPrinterUpdate(printerId);
+                    }
                 } break;
 
                 case MSG_ON_REQUEST_CANCEL_PRINTJOB: {
@@ -352,15 +409,12 @@
                 } break;
 
                 case MSG_SET_CLEINT: {
-                    IPrintServiceClient client = (IPrintServiceClient) message.obj;
-                    synchronized (mLock) {
-                        mClient = client;
-                    }
-                    if (client != null) {
+                    mClient = (IPrintServiceClient) message.obj;
+                    if (mClient != null) {
                         onConnected();
                      } else {
                         onDisconnected();
-                    }
+                     }
                 } break;
 
                 default: {
diff --git a/core/java/android/printservice/PrinterDiscoverySession.java b/core/java/android/printservice/PrinterDiscoverySession.java
index 92dc0dd..8b959a6 100644
--- a/core/java/android/printservice/PrinterDiscoverySession.java
+++ b/core/java/android/printservice/PrinterDiscoverySession.java
@@ -16,18 +16,15 @@
 
 package android.printservice;
 
-import android.content.Context;
-import android.os.Handler;
-import android.os.Looper;
-import android.os.Message;
 import android.os.RemoteException;
-import android.print.IPrinterDiscoverySessionController;
-import android.print.IPrinterDiscoverySessionObserver;
+import android.print.PrinterCapabilitiesInfo;
 import android.print.PrinterId;
 import android.print.PrinterInfo;
+import android.util.ArrayMap;
 import android.util.Log;
 
-import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.Collections;
 import java.util.List;
 
 /**
@@ -36,67 +33,75 @@
  * for adding discovered printers, removing already added printers that
  * disappeared, and updating already added printers.
  * <p>
- * The opening of the session is announced by a call to {@link
- * PrinterDiscoverySession#onOpen(List)} at which point you should start printer
- * discovery. The closing of the session is announced by a call to {@link
- * PrinterDiscoverySession#onClose()} at which point you should stop printer
- * discovery. Discovered printers are added by invoking {@link
- * PrinterDiscoverySession#addPrinters(List)}. Added printers that disappeared
- * are removed by invoking {@link PrinterDiscoverySession#removePrinters(List)}.
- * Added printers whose properties or capabilities changed are updated through
- * a call to {@link PrinterDiscoverySession#updatePrinters(List)}.
+ * During the lifetime of this session you may be asked to start and stop
+ * performing printer discovery multiple times. You will receive a call to {@link
+ * PrinterDiscoverySession#onStartPrinterDiscovery(List)} to start printer
+ * discovery and a call to {@link PrinterDiscoverySession#onStopPrinterDiscovery()}
+ * to stop printer discovery. When the system is no longer interested in printers
+ * discovered by this session you will receive a call to {@link #onDestroy()} at
+ * which point the system will no longer call into the session and all the session
+ * methods will do nothing.
+ * </p>
+ * <p>
+ * Discovered printers are added by invoking {@link
+ * PrinterDiscoverySession#addPrinters(List)}. Added printers that disappeared are
+ * removed by invoking {@link PrinterDiscoverySession#removePrinters(List)}. Added
+ * printers whose properties or capabilities changed are updated through a call to
+ * {@link PrinterDiscoverySession#updatePrinters(List)}. The printers added in this
+ * session can be acquired via {@link #getPrinters()} where the returned printers
+ * will be an up-to-date snapshot of the printers that you reported during the
+ * session. Printers are <strong>not</strong> persisted across sessions.
  * </p>
  * <p>
  * The system will make a call to
  * {@link PrinterDiscoverySession#onRequestPrinterUpdate(PrinterId)} if you
  * need to update a given printer. It is possible that you add a printer without
- * specifying its capabilities. This enables you to avoid querying all
- * discovered printers for their capabilities, rather querying the capabilities
- * of a printer only if necessary. For example, the system will require that you
- * update a printer if it gets selected by the user. If you did not report the
- * printer capabilities when adding it, you must do so after the system requests
- * a printer update. Otherwise, the printer will be ignored.
+ * specifying its capabilities. This enables you to avoid querying all discovered
+ * printers for their capabilities, rather querying the capabilities of a printer
+ * only if necessary. For example, the system will request that you update a printer
+ * if it gets selected by the user. If you did not report the printer capabilities
+ * when adding it, you must do so after the system requests a printer update.
+ * Otherwise, the printer will be ignored.
  * </p>
  * <p>
- * During printer discovery all printers that are known to your print service
- * have to be added. The system does not retain any printers from previous
- * sessions.
+ * <strong>Note: </strong> All callbacks in this class are executed on the main
+ * application thread. You also have to invoke any method of this class on the main
+ * application thread.
  * </p>
  */
 public abstract class PrinterDiscoverySession {
     private static final String LOG_TAG = "PrinterDiscoverySession";
 
+    private static final int MAX_ITEMS_PER_CALLBACK = 100;
+
     private static int sIdCounter = 0;
 
-    private final Object mLock = new Object();
-
-    private final Handler mHandler;
-
     private final int mId;
 
-    private IPrinterDiscoverySessionController mController;
+    private final ArrayMap<PrinterId, PrinterInfo> mPrinters =
+            new ArrayMap<PrinterId, PrinterInfo>();
 
-    private IPrinterDiscoverySessionObserver mObserver;
+    private ArrayMap<PrinterId, PrinterInfo> mLastSentPrinters;
+
+    private IPrintServiceClient mObserver;
+
+    private boolean mIsDestroyed;
+
+    private boolean mIsDiscoveryStarted;
 
     /**
      * Constructor.
-     *
-     * @param context A context instance.
      */
-    public PrinterDiscoverySession(Context context) {
+    public PrinterDiscoverySession() {
         mId = sIdCounter++;
-        mHandler = new SessionHandler(context.getMainLooper());
-        mController = new PrinterDiscoverySessionController(this);
     }
 
-    void setObserver(IPrinterDiscoverySessionObserver observer) {
-        synchronized (mLock) {
-            mObserver = observer;
-            try {
-                mObserver.setController(mController);
-            } catch (RemoteException re) {
-                Log.e(LOG_TAG, "Error setting session controller", re);
-            }
+    void setObserver(IPrintServiceClient observer) {
+        mObserver = observer;
+        // If some printers were added in the method that
+        // created the session, send them over.
+        if (!mPrinters.isEmpty()) {
+            sendAddedPrinters(mObserver, getPrinters());
         }
     }
 
@@ -105,131 +110,357 @@
     }
 
     /**
+     * Gets the printers reported in this session. For example, if you add two
+     * printers and remove one of them, the returned list will contain only
+     * the printer that was added but not removed.
+     * <p>
+     * <strong>Note: </strong> Calls to this method after the session is
+     * destroyed, i.e. after the {@link #onDestroy()} callback, will be ignored.
+     * </p>
+     *
+     * @return The printers.
+     *
+     * @see #addPrinters(List)
+     * @see #removePrinters(List)
+     * @see #updatePrinters(List)
+     * @see #isDestroyed()
+     */
+    public final List<PrinterInfo> getPrinters() {
+        PrintService.throwIfNotCalledOnMainThread();
+        if (mIsDestroyed) {
+            return Collections.emptyList();
+        }
+        return new ArrayList<PrinterInfo>(mPrinters.values());
+    }
+
+    /**
      * Adds discovered printers. Adding an already added printer has no effect.
      * Removed printers can be added again. You can call this method multiple
-     * times during printer discovery.
+     * times during the life of this session. Duplicates will be ignored.
      * <p>
-     * <strong>Note: </strong> Calls to this method before the session is opened,
-     * i.e. before the {@link #onOpen(List)} call, and after the session is closed,
-     * i.e. after the call to {@link #onClose()}, will be ignored.
+     * <strong>Note: </strong> Calls to this method after the session is
+     * destroyed, i.e. after the {@link #onDestroy()} callback, will be ignored.
      * </p>
      *
      * @param printers The printers to add.
      *
      * @see #removePrinters(List)
      * @see #updatePrinters(List)
+     * @see #getPrinters()
+     * @see #isDestroyed()
      */
     public final void addPrinters(List<PrinterInfo> printers) {
-        final IPrinterDiscoverySessionObserver observer;
-        synchronized (mLock) {
-            observer = mObserver;
+        PrintService.throwIfNotCalledOnMainThread();
+
+        // If the session is destroyed - nothing do to.
+        if (mIsDestroyed) {
+            Log.w(LOG_TAG, "Not adding printers - session destroyed.");
+            return;
         }
-        if (observer != null) {
-            try {
-                observer.onPrintersAdded(printers);
-            } catch (RemoteException re) {
-                Log.e(LOG_TAG, "Error adding printers", re);
+
+        if (mIsDiscoveryStarted) {
+            // If during discovery, add the new printers and send them.
+            List<PrinterInfo> addedPrinters = new ArrayList<PrinterInfo>();
+            final int addedPrinterCount = printers.size();
+            for (int i = 0; i < addedPrinterCount; i++) {
+                PrinterInfo addedPrinter = printers.get(i);
+                if (mPrinters.get(addedPrinter.getId()) == null) {
+                    mPrinters.put(addedPrinter.getId(), addedPrinter);
+                    addedPrinters.add(addedPrinter);
+                }
+            }
+
+            // Send the added printers, if such.
+            if (!addedPrinters.isEmpty()) {
+                sendAddedPrinters(mObserver, addedPrinters);
             }
         } else {
-            Log.w(LOG_TAG, "Printer discovery session not open not adding printers.");
+            // Remember the last sent printers if needed.
+            if (mLastSentPrinters == null) {
+                mLastSentPrinters = new ArrayMap<PrinterId, PrinterInfo>(mPrinters);
+            }
+
+            // Update the printers.
+            final int addedPrinterCount = printers.size();
+            for (int i = 0; i < addedPrinterCount; i++) {
+                PrinterInfo addedPrinter = printers.get(i);
+                if (mPrinters.get(addedPrinter.getId()) == null) {
+                    mPrinters.put(addedPrinter.getId(), addedPrinter);
+                }
+            }
+        }
+    }
+
+    private static void sendAddedPrinters(IPrintServiceClient observer,
+        List<PrinterInfo> printers) {
+        try {
+            final int printerCount = printers.size();
+            if (printerCount <= MAX_ITEMS_PER_CALLBACK) {
+                observer.onPrintersAdded(printers);
+            } else {
+                // Send the added printers in chunks avoiding the binder transaction limit.
+                final int transactionCount = (printerCount / MAX_ITEMS_PER_CALLBACK) + 1;
+                for (int i = 0; i < transactionCount; i++) {
+                    final int start = i * MAX_ITEMS_PER_CALLBACK;
+                    final int end = Math.min(start + MAX_ITEMS_PER_CALLBACK, printerCount);
+                    List<PrinterInfo> subPrinters = printers.subList(start, end);
+                    observer.onPrintersAdded(subPrinters);
+                }
+            }
+        } catch (RemoteException re) {
+            Log.e(LOG_TAG, "Error sending added printers", re);
         }
     }
 
     /**
      * Removes added printers. Removing an already removed or never added
-     * printer has no effect. Removed printers can be added again. You
-     * can call this method multiple times during printer discovery.
+     * printer has no effect. Removed printers can be added again. You can
+     * call this method multiple times during the lifetime of this session.
      * <p>
-     * <strong>Note: </strong> Calls to this method before the session is opened,
-     * i.e. before the {@link #onOpen(List)} call, and after the session is closed,
-     * i.e. after the call to {@link #onClose()}, will be ignored.
+     * <strong>Note: </strong> Calls to this method after the session is
+     * destroyed, i.e. after the {@link #onDestroy()} callback, will be ignored.
      * </p>
      *
      * @param printerIds The ids of the removed printers.
      *
      * @see #addPrinters(List)
      * @see #updatePrinters(List)
+     * @see #getPrinters()
+     * @see #isDestroyed()
      */
     public final void removePrinters(List<PrinterId> printerIds) {
-        final IPrinterDiscoverySessionObserver observer;
-        synchronized (mLock) {
-            observer = mObserver;
+        PrintService.throwIfNotCalledOnMainThread();
+
+        // If the session is destroyed - nothing do to.
+        if (mIsDestroyed) {
+            Log.w(LOG_TAG, "Not removing printers - session destroyed.");
+            return;
         }
-        if (observer != null) {
-            try {
-                observer.onPrintersRemoved(printerIds);
-            } catch (RemoteException re) {
-                Log.e(LOG_TAG, "Error removing printers", re);
+
+        if (mIsDiscoveryStarted) {
+            // If during discovery, remove existing printers and send them.
+            List<PrinterId> removedPrinterIds = new ArrayList<PrinterId>();
+            final int removedPrinterIdCount = printerIds.size();
+            for (int i = 0; i < removedPrinterIdCount; i++) {
+                PrinterId removedPrinterId = printerIds.get(i);
+                if (mPrinters.remove(removedPrinterId) != null) {
+                    removedPrinterIds.add(removedPrinterId);
+                }
+            }
+
+            // Send the removed printers, if such.
+            if (!removedPrinterIds.isEmpty()) {
+                sendRemovedPrinters(mObserver, removedPrinterIds);
             }
         } else {
-            Log.w(LOG_TAG, "Printer discovery session not open not removing printers.");
+            // Remember the last sent printers if needed.
+            if (mLastSentPrinters == null) {
+                mLastSentPrinters = new ArrayMap<PrinterId, PrinterInfo>(mPrinters);
+            }
+
+            // Update the printers.
+            final int removedPrinterIdCount = printerIds.size();
+            for (int i = 0; i < removedPrinterIdCount; i++) {
+                PrinterId removedPrinterId = printerIds.get(i);
+                mPrinters.remove(removedPrinterId);
+            }
+        }
+    }
+
+    private static void sendRemovedPrinters(IPrintServiceClient observer,
+            List<PrinterId> printerIds) {
+        try {
+            final int printerIdCount = printerIds.size();
+            if (printerIdCount <= MAX_ITEMS_PER_CALLBACK) {
+                observer.onPrintersRemoved(printerIds);
+            } else {
+                final int transactionCount = (printerIdCount / MAX_ITEMS_PER_CALLBACK) + 1;
+                for (int i = 0; i < transactionCount; i++) {
+                    final int start = i * MAX_ITEMS_PER_CALLBACK;
+                    final int end = Math.min(start + MAX_ITEMS_PER_CALLBACK, printerIdCount);
+                    List<PrinterId> subPrinterIds = printerIds.subList(start, end);
+                    observer.onPrintersRemoved(subPrinterIds);
+                }
+            }
+        } catch (RemoteException re) {
+            Log.e(LOG_TAG, "Error sending removed printers", re);
         }
     }
 
     /**
      * Updates added printers. Updating a printer that was not added or that
      * was removed has no effect. You can call this method multiple times
-     * during printer discovery.
+     * during the lifetime of this session.
      * <p>
-     * <strong>Note: </strong> Calls to this method before the session is opened,
-     * i.e. before the {@link #onOpen(List)} call, and after the session is closed,
-     * i.e. after the call to {@link #onClose()}, will be ignored.
+     * <strong>Note: </strong> Calls to this method after the session is
+     * destroyed, i.e. after the {@link #onDestroy()} callback, will be ignored.
      * </p>
      *
      * @param printers The printers to update.
      *
      * @see #addPrinters(List)
      * @see #removePrinters(List)
+     * @see #getPrinters()
+     * @see #isDestroyed()
      */
     public final void updatePrinters(List<PrinterInfo> printers) {
-        final IPrinterDiscoverySessionObserver observer;
-        synchronized (mLock) {
-            observer = mObserver;
+        PrintService.throwIfNotCalledOnMainThread();
+
+        // If the session is destroyed - nothing do to.
+        if (mIsDestroyed) {
+            Log.w(LOG_TAG, "Not updating printers - session destroyed.");
+            return;
         }
-        if (observer != null) {
-            try {
-                observer.onPrintersUpdated(printers);
-            } catch (RemoteException re) {
-                Log.e(LOG_TAG, "Error updating printers", re);
+
+        if (mIsDiscoveryStarted) {
+            // If during discovery, update existing printers and send them.
+            List<PrinterInfo> updatedPrinters = new ArrayList<PrinterInfo>();
+            final int updatedPrinterCount = printers.size();
+            for (int i = 0; i < updatedPrinterCount; i++) {
+                PrinterInfo updatedPrinter = printers.get(i);
+                PrinterInfo oldPrinter = mPrinters.get(updatedPrinter.getId());
+                if (oldPrinter != null && !oldPrinter.equals(updatedPrinter)) {
+                    mPrinters.put(updatedPrinter.getId(), updatedPrinter);
+                    updatedPrinters.add(updatedPrinter);
+                }
+            }
+
+            // Send the updated printers, if such.
+            if (!updatedPrinters.isEmpty()) {
+                sendUpdatedPrinters(mObserver, updatedPrinters);
             }
         } else {
-            Log.w(LOG_TAG, "Printer discovery session not open not updating printers.");
+            // Remember the last sent printers if needed.
+            if (mLastSentPrinters == null) {
+                mLastSentPrinters = new ArrayMap<PrinterId, PrinterInfo>(mPrinters);
+            }
+
+            // Update the printers.
+            final int updatedPrinterCount = printers.size();
+            for (int i = 0; i < updatedPrinterCount; i++) {
+                PrinterInfo updatedPrinter = printers.get(i);
+                PrinterInfo oldPrinter = mPrinters.get(updatedPrinter.getId());
+                if (oldPrinter != null && !oldPrinter.equals(updatedPrinter)) {
+                    mPrinters.put(updatedPrinter.getId(), updatedPrinter);
+                }
+            }
         }
     }
 
+    private static void sendUpdatedPrinters(IPrintServiceClient observer,
+            List<PrinterInfo> printers) {
+        try {
+            final int printerCount = printers.size();
+            if (printerCount <= MAX_ITEMS_PER_CALLBACK) {
+                observer.onPrintersUpdated(printers);
+            } else {
+                final int transactionCount = (printerCount / MAX_ITEMS_PER_CALLBACK) + 1;
+                for (int i = 0; i < transactionCount; i++) {
+                    final int start = i * MAX_ITEMS_PER_CALLBACK;
+                    final int end = Math.min(start + MAX_ITEMS_PER_CALLBACK, printerCount);
+                    List<PrinterInfo> subPrinters = printers.subList(start, end);
+                    observer.onPrintersUpdated(subPrinters);
+                }
+            }
+        } catch (RemoteException re) {
+            Log.e(LOG_TAG, "Error sending updated printers", re);
+        }
+    }
+
+    private void sendOutOfDiscoveryPeriodPrinterChanges() {
+        // Noting changed since the last discovery period - nothing to do.
+        if (mLastSentPrinters == null || mLastSentPrinters.isEmpty()) {
+            mLastSentPrinters = null;
+            return;
+        }
+
+        List<PrinterInfo> addedPrinters = null;
+        List<PrinterInfo> updatedPrinters = null;
+        List<PrinterId> removedPrinterIds = null;
+
+        // Determine the added and updated printers.
+        for (PrinterInfo printer : mPrinters.values()) {
+            PrinterInfo sentPrinter = mLastSentPrinters.get(printer.getId());
+            if (sentPrinter != null) {
+                if (!sentPrinter.equals(printer)) {
+                    if (updatedPrinters == null) {
+                        updatedPrinters = new ArrayList<PrinterInfo>();
+                    }
+                    updatedPrinters.add(printer);
+                }
+            } else {
+                if (addedPrinters == null) {
+                    addedPrinters = new ArrayList<PrinterInfo>();
+                }
+                addedPrinters.add(printer);
+            }
+        }
+
+        // Send the added printers, if such.
+        if (addedPrinters != null) {
+            sendAddedPrinters(mObserver, addedPrinters);
+        }
+
+        // Send the updated printers, if such.
+        if (updatedPrinters != null) {
+            sendUpdatedPrinters(mObserver, updatedPrinters);
+        }
+
+        // Determine the removed printers.
+        for (PrinterInfo sentPrinter : mLastSentPrinters.values()) {
+            if (!mPrinters.containsKey(sentPrinter.getId())) {
+                if (removedPrinterIds == null) {
+                    removedPrinterIds = new ArrayList<PrinterId>();
+                }
+                removedPrinterIds.add(sentPrinter.getId());
+            }
+        }
+
+        // Send the removed printers, if such.
+        if (removedPrinterIds != null) {
+            sendRemovedPrinters(mObserver, removedPrinterIds);
+        }
+
+        mLastSentPrinters = null;
+    }
+
     /**
-     * Callback notifying you that the session is open and you should start
-     * printer discovery. Discovered printers should be added via calling
-     * {@link #addPrinters(List)}. Added printers that disappeared should be
-     * removed via calling {@link #removePrinters(List)}. Added printers whose
-     * properties or capabilities changes should be updated via calling {@link
-     * #updatePrinters(List)}. When the session is closed you will receive a
-     * call to {@link #onClose()}.
+     * Callback asking you to start printer discovery. Discovered printers should be
+     * added via calling {@link #addPrinters(List)}. Added printers that disappeared
+     * should be removed via calling {@link #removePrinters(List)}. Added printers
+     * whose properties or capabilities changed should be updated via calling {@link
+     * #updatePrinters(List)}. You will receive a call to call to {@link
+     * #onStopPrinterDiscovery()} when you should stop printer discovery.
      * <p>
-     * During printer discovery all printers that are known to your print
-     * service have to be added. The system does not retain any printers from
-     * previous sessions.
+     * During the lifetime of this session all printers that are known to your print
+     * service have to be added. The system does not retain any printers across sessions.
+     * However, if you were asked to start and then stop performing printer discovery
+     * in this session, then a subsequent discovering should not re-discover already
+     * discovered printers.
      * </p>
      * <p>
-     * <strong>Note: </strong>You are also given a list of printers whose
-     * availability has to be checked first. For example, these printers could
-     * be the user's favorite ones, therefore they have to be verified first.
+     * <strong>Note: </strong>You are also given a list of printers whose availability
+     * has to be checked first. For example, these printers could be the user's favorite
+     * ones, therefore they have to be verified first.
      * </p>
      *
-     * @see #onClose()
+     * @param priorityList The list of printers to validate first. Never null.
+     *
+     * @see #onStopPrinterDiscovery()
      * @see #addPrinters(List)
      * @see #removePrinters(List)
      * @see #updatePrinters(List)
+     * @see #isPrinterDiscoveryStarted()
      */
-    public abstract void onOpen(List<PrinterId> priorityList);
+    public abstract void onStartPrinterDiscovery(List<PrinterId> priorityList);
 
     /**
-     * Callback notifying you that the session is closed and you should stop
-     * printer discovery. After the session is closed any call to the methods
-     * of this instance will be ignored. Once the session is closed
-     * it will never be opened again.
+     * Callback notifying you that you should stop printer discovery.
+     *
+     * @see #onStartPrinterDiscovery(List)
+     * @see #isPrinterDiscoveryStarted()
      */
-    public abstract void onClose();
+    public abstract void onStopPrinterDiscovery();
 
     /**
      * Requests that you update a printer. You are responsible for updating
@@ -255,77 +486,72 @@
      */
     public abstract void onRequestPrinterUpdate(PrinterId printerId);
 
-    void close() {
-        synchronized (mLock) {
-            mController = null;
+    /**
+     * Notifies you that the session is destroyed. After this callback is invoked
+     * any calls to the methods of this class will be ignored, {@link #isDestroyed()}
+     * will return true and you will also no longer receive callbacks.
+     *
+     * @see #isDestroyed()
+     */
+    public abstract void onDestroy();
+
+    /**
+     * Gets whether the session is destroyed.
+     *
+     * @return Whether the session is destroyed.
+     *
+     * @see #onDestroy()
+     */
+    public final boolean isDestroyed() {
+        PrintService.throwIfNotCalledOnMainThread();
+        return mIsDestroyed;
+    }
+
+    /**
+     * Gets whether printer discovery is started.
+     *
+     * @return Whether printer discovery is destroyed.
+     *
+     * @see #onStartPrinterDiscovery(List)
+     * @see #onStopPrinterDiscovery()
+     */
+    public final boolean isPrinterDiscoveryStarted() {
+        PrintService.throwIfNotCalledOnMainThread();
+        return mIsDiscoveryStarted;
+    }
+
+    void startPrinterDiscovery(List<PrinterId> priorityList) {
+        if (!mIsDestroyed) {
+            mIsDiscoveryStarted = true;
+            sendOutOfDiscoveryPeriodPrinterChanges();
+            if (priorityList == null) {
+                priorityList = Collections.emptyList();
+            }
+            onStartPrinterDiscovery(priorityList);
+        }
+    }
+
+    void stopPrinterDiscovery() {
+        if (!mIsDestroyed) {
+            mIsDiscoveryStarted = false;
+            onStopPrinterDiscovery();
+        }
+    }
+
+    void requestPrinterUpdate(PrinterId printerId) {
+        if (!mIsDestroyed) {
+            onRequestPrinterUpdate(printerId);
+        }
+    }
+
+    void destroy() {
+        if (!mIsDestroyed) {
+            mIsDestroyed = true;
+            mIsDiscoveryStarted = false;
+            mPrinters.clear();
+            mLastSentPrinters = null;
             mObserver = null;
+            onDestroy();
         }
     }
-
-    private final class SessionHandler extends Handler {
-        public static final int MSG_OPEN = 1;
-        public static final int MSG_CLOSE = 2;
-        public static final int MSG_REQUEST_PRINTER_UPDATE = 3;
-
-        public SessionHandler(Looper looper) {
-            super(looper, null, true);
-        }
-
-        @Override
-        @SuppressWarnings("unchecked")
-        public void handleMessage(Message message) {
-            switch (message.what) {
-                case MSG_OPEN: {
-                    List<PrinterId> priorityList = (List<PrinterId>) message.obj;
-                    onOpen(priorityList);
-                } break;
-
-                case MSG_CLOSE: {
-                    onClose();
-                    close();
-                } break;
-
-                case MSG_REQUEST_PRINTER_UPDATE: {
-                    PrinterId printerId = (PrinterId) message.obj;
-                    onRequestPrinterUpdate(printerId);
-                } break;
-            }
-        }
-    }
-
-    private static final class PrinterDiscoverySessionController extends
-            IPrinterDiscoverySessionController.Stub {
-        private final WeakReference<PrinterDiscoverySession> mWeakSession;
-
-        public PrinterDiscoverySessionController(PrinterDiscoverySession session) {
-            mWeakSession = new WeakReference<PrinterDiscoverySession>(session);
-        }
-
-        @Override
-        public void open(List<PrinterId> priorityList) {
-            PrinterDiscoverySession session = mWeakSession.get();
-            if (session != null) {
-                session.mHandler.obtainMessage(SessionHandler.MSG_OPEN,
-                        priorityList).sendToTarget();
-            }
-        }
-
-        @Override
-        public void close() {
-            PrinterDiscoverySession session = mWeakSession.get();
-            if (session != null) {
-                session.mHandler.sendEmptyMessage(SessionHandler.MSG_CLOSE);
-            }
-        }
-
-        @Override
-        public void requestPrinterUpdate(PrinterId printerId) {
-            PrinterDiscoverySession session = mWeakSession.get();
-            if (session != null) {
-                session.mHandler.obtainMessage(
-                        SessionHandler.MSG_REQUEST_PRINTER_UPDATE,
-                        printerId).sendToTarget();
-            }
-        }
-    };
 }
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index 585115a..fad6c73 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -3269,6 +3269,23 @@
         public static final String LOCATION_PROVIDERS_ALLOWED = "location_providers_allowed";
 
         /**
+         * Location access disabled
+         */
+        public static final int LOCATION_MODE_OFF = 0;
+        /**
+         * Network Location Provider disabled, but GPS and other sensors enabled.
+         */
+        public static final int LOCATION_MODE_SENSORS_ONLY = 1;
+        /**
+         * Reduced power usage, such as limiting the number of GPS updates per hour.
+         */
+        public static final int LOCATION_MODE_BATTERY_SAVING = 2;
+        /**
+         * Best-effort location computation allowed.
+         */
+        public static final int LOCATION_MODE_HIGH_ACCURACY = 3;
+
+        /**
          * A flag containing settings used for biometric weak
          * @hide
          */
@@ -4319,23 +4336,27 @@
          * @param cr the content resolver to use
          * @param provider the location provider to query
          * @return true if the provider is enabled
+         * @deprecated use {@link #getLocationMode(ContentResolver)}
          */
+        @Deprecated
         public static final boolean isLocationProviderEnabled(ContentResolver cr, String provider) {
             return isLocationProviderEnabledForUser(cr, provider, UserHandle.myUserId());
         }
 
         /**
          * Helper method for determining if the location master switch is enabled.
+         *
+         * TODO: worth retaining this method?
+         *
          * @param cr the content resolver to use
          * @return true if the master switch is enabled
+         * @deprecated use {@link #getLocationMode(ContentResolver)} != {@link #LOCATION_MODE_OFF}
          * @hide
          */
+        @Deprecated
         public static final boolean isLocationMasterSwitchEnabled(ContentResolver cr) {
-            int uid = UserHandle.myUserId();
-            synchronized (mLocationSettingsLock) {
-                return isLocationProviderEnabledForUser(cr, LocationManager.NETWORK_PROVIDER, uid)
-                        || isLocationProviderEnabledForUser(cr, LocationManager.GPS_PROVIDER, uid);
-            }
+            int mode = getLocationMode(cr);
+            return mode != LOCATION_MODE_OFF;
         }
 
         /**
@@ -4344,8 +4365,10 @@
          * @param provider the location provider to query
          * @param userId the userId to query
          * @return true if the provider is enabled
+         * @deprecated use {@link #getLocationModeForUser(ContentResolver, int)}
          * @hide
          */
+        @Deprecated
         public static final boolean isLocationProviderEnabledForUser(ContentResolver cr, String provider, int userId) {
             String allowedProviders = Settings.Secure.getStringForUser(cr,
                     LOCATION_PROVIDERS_ALLOWED, userId);
@@ -4357,7 +4380,9 @@
          * @param cr the content resolver to use
          * @param provider the location provider to enable or disable
          * @param enabled true if the provider should be enabled
+         * @deprecated use {@link #setLocationMode(ContentResolver, int)}
          */
+        @Deprecated
         public static final void setLocationProviderEnabled(ContentResolver cr,
                 String provider, boolean enabled) {
             setLocationProviderEnabledForUser(cr, provider, enabled, UserHandle.myUserId());
@@ -4368,8 +4393,11 @@
          *
          * @param cr the content resolver to use
          * @param enabled true if master switch should be enabled
+         * @deprecated use {@link #setLocationMode(ContentResolver, int)} with
+         * {@link #LOCATION_MODE_HIGH_ACCURACY}
          * @hide
          */
+        @Deprecated
         public static final void setLocationMasterSwitchEnabled(ContentResolver cr,
                 boolean enabled) {
             int uid = UserHandle.myUserId();
@@ -4386,8 +4414,10 @@
          * @param provider the location provider to enable or disable
          * @param enabled true if the provider should be enabled
          * @param userId the userId for which to enable/disable providers
+         * @deprecated use {@link #setLocationModeForUser(ContentResolver, int, int)}
          * @hide
          */
+        @Deprecated
         public static final void setLocationProviderEnabledForUser(ContentResolver cr,
                 String provider, boolean enabled, int userId) {
             synchronized (mLocationSettingsLock) {
@@ -4403,6 +4433,97 @@
                         userId);
             }
         }
+
+        /**
+         * Thread-safe method for setting the location mode to one of
+         * {@link #LOCATION_MODE_HIGH_ACCURACY}, {@link #LOCATION_MODE_SENSORS_ONLY},
+         * {@link #LOCATION_MODE_BATTERY_SAVING}, or {@link #LOCATION_MODE_OFF}.
+         *
+         * @param cr the content resolver to use
+         * @param mode such as {@link #LOCATION_MODE_HIGH_ACCURACY}
+         * @param userId the userId for which to change mode
+         *
+         * @throws IllegalArgumentException if mode is not one of the supported values
+         */
+        public static final void setLocationModeForUser(ContentResolver cr, int mode, int userId) {
+            synchronized (mLocationSettingsLock) {
+                boolean gps = false;
+                boolean network = false;
+                switch (mode) {
+                    case LOCATION_MODE_OFF:
+                        break;
+                    case LOCATION_MODE_SENSORS_ONLY:
+                        gps = true;
+                        break;
+                    case LOCATION_MODE_BATTERY_SAVING:
+                        network = true;
+                        break;
+                    case LOCATION_MODE_HIGH_ACCURACY:
+                        gps = true;
+                        network = true;
+                        break;
+                    default:
+                        throw new IllegalArgumentException("Invalid location mode: " + mode);
+                }
+                Settings.Secure.setLocationProviderEnabledForUser(
+                        cr, LocationManager.GPS_PROVIDER, gps, userId);
+                Settings.Secure.setLocationProviderEnabledForUser(
+                        cr, LocationManager.NETWORK_PROVIDER, network, userId);
+            }
+        }
+
+        /**
+         * Thread-safe method for setting the location mode to one of
+         * {@link #LOCATION_MODE_HIGH_ACCURACY}, {@link #LOCATION_MODE_SENSORS_ONLY},
+         * {@link #LOCATION_MODE_BATTERY_SAVING}, or {@link #LOCATION_MODE_OFF}.
+         *
+         * @param cr the content resolver to use
+         * @param mode such as {@link #LOCATION_MODE_HIGH_ACCURACY}
+         *
+         * @throws IllegalArgumentException if mode is not one of the supported values
+         */
+        public static final void setLocationMode(ContentResolver cr, int mode) {
+            setLocationModeForUser(cr, mode, UserHandle.myUserId());
+        }
+
+        /**
+         * Thread-safe method for reading the location mode, returns one of
+         * {@link #LOCATION_MODE_HIGH_ACCURACY}, {@link #LOCATION_MODE_SENSORS_ONLY},
+         * {@link #LOCATION_MODE_BATTERY_SAVING}, or {@link #LOCATION_MODE_OFF}.
+         *
+         * @param cr the content resolver to use
+         * @param userId the userId for which to read the mode
+         * @return the location mode
+         */
+        public static final int getLocationModeForUser(ContentResolver cr, int userId) {
+            synchronized (mLocationSettingsLock) {
+                boolean gpsEnabled = Settings.Secure.isLocationProviderEnabledForUser(
+                        cr, LocationManager.GPS_PROVIDER, userId);
+                boolean networkEnabled = Settings.Secure.isLocationProviderEnabledForUser(
+                        cr, LocationManager.NETWORK_PROVIDER, userId);
+                if (gpsEnabled && networkEnabled) {
+                    return LOCATION_MODE_HIGH_ACCURACY;
+                } else if (gpsEnabled) {
+                    return LOCATION_MODE_SENSORS_ONLY;
+                } else if (networkEnabled) {
+                    return LOCATION_MODE_BATTERY_SAVING;
+                } else {
+                    return LOCATION_MODE_OFF;
+                }
+            }
+        }
+
+        /**
+         * Thread-safe method for reading the location mode, returns one of
+         * {@link #LOCATION_MODE_HIGH_ACCURACY}, {@link #LOCATION_MODE_SENSORS_ONLY},
+         * {@link #LOCATION_MODE_BATTERY_SAVING}, or {@link #LOCATION_MODE_OFF}.
+         *
+         * @param cr the content resolver to use
+         * @return the location mode
+         */
+        public static final int getLocationMode(ContentResolver cr) {
+            return getLocationModeForUser(cr, UserHandle.myUserId());
+        }
     }
 
     /**
diff --git a/core/java/android/view/Surface.java b/core/java/android/view/Surface.java
index e0786f7..409db84 100644
--- a/core/java/android/view/Surface.java
+++ b/core/java/android/view/Surface.java
@@ -36,7 +36,7 @@
             throws OutOfResourcesException;
     private static native int nativeCreateFromSurfaceControl(int surfaceControlNativeObject);
 
-    private static native void nativeLockCanvas(int nativeObject, Canvas canvas, Rect dirty)
+    private static native int nativeLockCanvas(int nativeObject, Canvas canvas, Rect dirty)
             throws OutOfResourcesException;
     private static native void nativeUnlockCanvasAndPost(int nativeObject, Canvas canvas);
 
@@ -72,6 +72,7 @@
     final Object mLock = new Object(); // protects the native state
     private String mName;
     int mNativeObject; // package scope only for SurfaceControl access
+    private int mLockedObject;
     private int mGenerationId; // incremented each time mNativeObject changes
     private final Canvas mCanvas = new CompatibleCanvas();
 
@@ -233,7 +234,14 @@
             throws OutOfResourcesException, IllegalArgumentException {
         synchronized (mLock) {
             checkNotReleasedLocked();
-            nativeLockCanvas(mNativeObject, mCanvas, inOutDirty);
+            if (mLockedObject != 0) {
+                // Ideally, nativeLockCanvas() would throw in this situation and prevent the
+                // double-lock, but that won't happen if mNativeObject was updated.  We can't
+                // abandon the old mLockedObject because it might still be in use, so instead
+                // we just refuse to re-lock the Surface.
+                throw new RuntimeException("Surface was already locked");
+            }
+            mLockedObject = nativeLockCanvas(mNativeObject, mCanvas, inOutDirty);
             return mCanvas;
         }
     }
@@ -252,11 +260,21 @@
 
         synchronized (mLock) {
             checkNotReleasedLocked();
-            nativeUnlockCanvasAndPost(mNativeObject, canvas);
+            if (mNativeObject != mLockedObject) {
+                Log.w(TAG, "WARNING: Surface's mNativeObject (0x" +
+                        Integer.toHexString(mNativeObject) + ") != mLockedObject (0x" +
+                        Integer.toHexString(mLockedObject) +")");
+            }
+            if (mLockedObject == 0) {
+                throw new RuntimeException("Surface was not locked");
+            }
+            nativeUnlockCanvasAndPost(mLockedObject, canvas);
+            nativeRelease(mLockedObject);
+            mLockedObject = 0;
         }
     }
 
-    /** 
+    /**
      * @deprecated This API has been removed and is not supported.  Do not use.
      */
     @Deprecated
@@ -343,6 +361,10 @@
         }
 
         synchronized (mLock) {
+            // nativeReadFromParcel() will either return mNativeObject, or
+            // create a new native Surface and return it after reducing
+            // the reference count on mNativeObject.  Either way, it is
+            // not necessary to call nativeRelease() here.
             mName = source.readString();
             setNativeObjectLocked(nativeReadFromParcel(mNativeObject, source));
         }
@@ -365,7 +387,8 @@
     @Override
     public String toString() {
         synchronized (mLock) {
-            return "Surface(name=" + mName + ")";
+            return "Surface(name=" + mName + ")/@0x" +
+                    Integer.toHexString(System.identityHashCode(this));
         }
     }
 
@@ -463,7 +486,7 @@
         public void getMatrix(Matrix m) {
             super.getMatrix(m);
             if (mOrigMatrix == null) {
-                mOrigMatrix = new Matrix(); 
+                mOrigMatrix = new Matrix();
             }
             mOrigMatrix.set(m);
         }
diff --git a/core/java/com/android/internal/os/HandlerCaller.java b/core/java/com/android/internal/os/HandlerCaller.java
index b442ff5..d9e3ef6 100644
--- a/core/java/com/android/internal/os/HandlerCaller.java
+++ b/core/java/com/android/internal/os/HandlerCaller.java
@@ -65,7 +65,11 @@
         
         mH.sendMessage(msg);
     }
-    
+
+    public void sendMessageDelayed(Message msg, long delayMillis) {
+        mH.sendMessageDelayed(msg, delayMillis);
+    }
+
     public boolean hasMessages(int what) {
         return mH.hasMessages(what);
     }
diff --git a/core/jni/android_view_Surface.cpp b/core/jni/android_view_Surface.cpp
index 304514b..3f54fd7 100644
--- a/core/jni/android_view_Surface.cpp
+++ b/core/jni/android_view_Surface.cpp
@@ -196,13 +196,13 @@
   SkSafeUnref(previousCanvas);
 }
 
-static void nativeLockCanvas(JNIEnv* env, jclass clazz,
+static jint nativeLockCanvas(JNIEnv* env, jclass clazz,
         jint nativeObject, jobject canvasObj, jobject dirtyRectObj) {
     sp<Surface> surface(reinterpret_cast<Surface *>(nativeObject));
 
     if (!isSurfaceValid(surface)) {
         doThrowIAE(env);
-        return;
+        return 0;
     }
 
     Rect dirtyRect;
@@ -223,7 +223,7 @@
                 OutOfResourcesException :
                 "java/lang/IllegalArgumentException";
         jniThrowException(env, exception, NULL);
-        return;
+        return 0;
     }
 
     // Associate a SkCanvas object to this surface
@@ -255,6 +255,13 @@
         env->SetIntField(dirtyRectObj, gRectClassInfo.right,  dirtyRect.right);
         env->SetIntField(dirtyRectObj, gRectClassInfo.bottom, dirtyRect.bottom);
     }
+
+    // Create another reference to the surface and return it.  This reference
+    // should be passed to nativeUnlockCanvasAndPost in place of mNativeObject,
+    // because the latter could be replaced while the surface is locked.
+    sp<Surface> lockedSurface(surface);
+    lockedSurface->incStrong(&sRefBaseOwner);
+    return (int) lockedSurface.get();
 }
 
 static void nativeUnlockCanvasAndPost(JNIEnv* env, jclass clazz,
@@ -351,7 +358,7 @@
             (void*)nativeIsValid },
     {"nativeIsConsumerRunningBehind", "(I)Z",
             (void*)nativeIsConsumerRunningBehind },
-    {"nativeLockCanvas", "(ILandroid/graphics/Canvas;Landroid/graphics/Rect;)V",
+    {"nativeLockCanvas", "(ILandroid/graphics/Canvas;Landroid/graphics/Rect;)I",
             (void*)nativeLockCanvas },
     {"nativeUnlockCanvasAndPost", "(ILandroid/graphics/Canvas;)V",
             (void*)nativeUnlockCanvasAndPost },
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index 1363e3c..12e33d5 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -1617,6 +1617,14 @@
         android:label="@string/permlab_anyCodecForPlayback"
         android:description="@string/permdesc_anyCodecForPlayback" />
 
+    <!-- Allows an application to install and/or uninstall CA certificates on
+         behalf of the user.
+         @hide -->
+    <permission android:name="android.permission.MANAGE_CA_CERTIFICATES"
+        android:protectionLevel="signature|system"
+        android:label="@string/permlab_manageCaCertificates"
+        android:description="@string/permdesc_manageCaCertificates" />
+
     <!-- ========================================= -->
     <!-- Permissions for special development tools -->
     <!-- ========================================= -->
diff --git a/core/res/res/drawable/spinner_ab_holo_dark.xml b/core/res/res/drawable/spinner_ab_holo_dark.xml
index 0932eff..9b8967c 100644
--- a/core/res/res/drawable/spinner_ab_holo_dark.xml
+++ b/core/res/res/drawable/spinner_ab_holo_dark.xml
@@ -14,7 +14,8 @@
      limitations under the License.
 -->
 
-<selector xmlns:android="http://schemas.android.com/apk/res/android">
+<selector xmlns:android="http://schemas.android.com/apk/res/android"
+          android:autoMirrored="true">
     <item android:state_enabled="false"
           android:drawable="@drawable/spinner_ab_disabled_holo_dark" />
     <item android:state_pressed="true"
diff --git a/core/res/res/drawable/spinner_ab_holo_light.xml b/core/res/res/drawable/spinner_ab_holo_light.xml
index e785cf4..a324c08 100644
--- a/core/res/res/drawable/spinner_ab_holo_light.xml
+++ b/core/res/res/drawable/spinner_ab_holo_light.xml
@@ -14,7 +14,8 @@
      limitations under the License.
 -->
 
-<selector xmlns:android="http://schemas.android.com/apk/res/android">
+<selector xmlns:android="http://schemas.android.com/apk/res/android"
+          android:autoMirrored="true">
     <item android:state_enabled="false"
           android:drawable="@drawable/spinner_ab_disabled_holo_light" />
     <item android:state_pressed="true"
diff --git a/core/res/res/drawable/spinner_background_holo_dark.xml b/core/res/res/drawable/spinner_background_holo_dark.xml
index eb6b18b..a57f720 100644
--- a/core/res/res/drawable/spinner_background_holo_dark.xml
+++ b/core/res/res/drawable/spinner_background_holo_dark.xml
@@ -14,7 +14,8 @@
      limitations under the License.
 -->
 
-<selector xmlns:android="http://schemas.android.com/apk/res/android">
+<selector xmlns:android="http://schemas.android.com/apk/res/android"
+          android:autoMirrored="true">
     <item android:state_enabled="false"
           android:drawable="@drawable/spinner_disabled_holo_dark" />
     <item android:state_pressed="true"
diff --git a/core/res/res/drawable/spinner_background_holo_light.xml b/core/res/res/drawable/spinner_background_holo_light.xml
index 9d17ed0c..899633c 100644
--- a/core/res/res/drawable/spinner_background_holo_light.xml
+++ b/core/res/res/drawable/spinner_background_holo_light.xml
@@ -14,7 +14,8 @@
      limitations under the License.
 -->
 
-<selector xmlns:android="http://schemas.android.com/apk/res/android">
+<selector xmlns:android="http://schemas.android.com/apk/res/android"
+          android:autoMirrored="true">
     <item android:state_enabled="false"
           android:drawable="@drawable/spinner_disabled_holo_light" />
     <item android:state_pressed="true"
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index e497c85..5f5564c 100644
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -1128,6 +1128,11 @@
     <string name="permdesc_anyCodecForPlayback">Allows the app to use any installed
         media decoder to decode for playback.</string>
 
+    <!-- Title of an application permission, listed so the user can choose whether they want to allow the application to do this. This permission allows the app to install or uninstall trusted credentials, a.k.a. CA certificates. [CHAR LIMIT=NONE] -->
+    <string name="permlab_manageCaCertificates">manage trusted credentials</string>
+    <!-- Description of an application permission, listed so the user can choose whether they want to allow the application to do this. [CHAR LIMIT=NONE]-->
+    <string name="permdesc_manageCaCertificates">Allows the app to install and uninstall CA certificates as trusted credentials.</string>
+
     <!-- Title of an application permission, listed so the user can choose whether they want to allow the application to do this. -->
     <string name="permlab_diagnostic">read/write to resources owned by diag</string>
     <!-- Description of an application permission, listed so the user can choose whether they want to allow the application to do this. -->
diff --git a/keystore/java/android/security/KeyChain.java b/keystore/java/android/security/KeyChain.java
index 328ac5d..9ea325a 100644
--- a/keystore/java/android/security/KeyChain.java
+++ b/keystore/java/android/security/KeyChain.java
@@ -443,7 +443,10 @@
             }
             @Override public void onServiceDisconnected(ComponentName name) {}
         };
-        boolean isBound = context.bindService(new Intent(IKeyChainService.class.getName()),
+        Intent intent = new Intent(IKeyChainService.class.getName());
+        ComponentName comp = intent.resolveSystemService(context.getPackageManager(), 0);
+        intent.setComponent(comp);
+        boolean isBound = context.bindService(intent,
                                               keyChainServiceConnection,
                                               Context.BIND_AUTO_CREATE);
         if (!isBound) {
diff --git a/libs/hwui/FontRenderer.cpp b/libs/hwui/FontRenderer.cpp
index cb6bb2e..22b1485 100644
--- a/libs/hwui/FontRenderer.cpp
+++ b/libs/hwui/FontRenderer.cpp
@@ -737,7 +737,7 @@
             }
 
             mRsElement = RSC::Element::A_8(mRs);
-            mRsScript = new RSC::ScriptIntrinsicBlur(mRs, mRsElement);
+            mRsScript = RSC::ScriptIntrinsicBlur::create(mRs, mRsElement);
         }
 
         RSC::sp<const RSC::Type> t = RSC::Type::create(mRs, mRsElement, width, height, 0);
@@ -749,7 +749,8 @@
                 outImage);
 
         mRsScript->setRadius(radius);
-        mRsScript->blur(ain, aout);
+        mRsScript->setInput(ain);
+        mRsScript->forEach(aout);
 
         // replace the original image's pointer, avoiding a copy back to the original buffer
         free(*image);
diff --git a/media/java/android/media/MediaPlayer.java b/media/java/android/media/MediaPlayer.java
index 241c7fa..946dd71 100644
--- a/media/java/android/media/MediaPlayer.java
+++ b/media/java/android/media/MediaPlayer.java
@@ -38,6 +38,8 @@
 import android.graphics.Bitmap;
 import android.graphics.SurfaceTexture;
 import android.media.AudioManager;
+import android.media.MediaFormat;
+import android.media.SubtitleData;
 
 import java.io.File;
 import java.io.FileDescriptor;
@@ -1335,6 +1337,7 @@
         mOnInfoListener = null;
         mOnVideoSizeChangedListener = null;
         mOnTimedTextListener = null;
+        mOnSubtitleDataListener = null;
         _release();
     }
 
@@ -1526,20 +1529,43 @@
          * ISO-639-2 language code, "und", is returned.
          */
         public String getLanguage() {
-            return mLanguage;
+            String language = mFormat.getString(MediaFormat.KEY_LANGUAGE);
+            return language == null ? "und" : language;
+        }
+
+        /**
+         * Gets the {@link MediaFormat} of the track.  If the format is
+         * unknown or could not be determined, null is returned.
+         */
+        public MediaFormat getFormat() {
+            if (mTrackType == MEDIA_TRACK_TYPE_TIMEDTEXT) {
+                return mFormat;
+            }
+            return null;
         }
 
         public static final int MEDIA_TRACK_TYPE_UNKNOWN = 0;
         public static final int MEDIA_TRACK_TYPE_VIDEO = 1;
         public static final int MEDIA_TRACK_TYPE_AUDIO = 2;
         public static final int MEDIA_TRACK_TYPE_TIMEDTEXT = 3;
+        /** @hide */
+        public static final int MEDIA_TRACK_TYPE_SUBTITLE = 4;
 
         final int mTrackType;
-        final String mLanguage;
+        final MediaFormat mFormat;
 
         TrackInfo(Parcel in) {
             mTrackType = in.readInt();
-            mLanguage = in.readString();
+            // TODO: parcel in the full MediaFormat
+            String language = in.readString();
+
+            if (mTrackType == MEDIA_TRACK_TYPE_TIMEDTEXT) {
+                mFormat = MediaFormat.createSubtitleFormat(
+                    MEDIA_MIMETYPE_TEXT_SUBRIP, language);
+            } else {
+                mFormat = new MediaFormat();
+                mFormat.setString(MediaFormat.KEY_LANGUAGE, language);
+            }
         }
 
         /**
@@ -1556,7 +1582,7 @@
         @Override
         public void writeToParcel(Parcel dest, int flags) {
             dest.writeInt(mTrackType);
-            dest.writeString(mLanguage);
+            dest.writeString(getLanguage());
         }
 
         /**
@@ -1891,6 +1917,7 @@
     private static final int MEDIA_TIMED_TEXT = 99;
     private static final int MEDIA_ERROR = 100;
     private static final int MEDIA_INFO = 200;
+    private static final int MEDIA_SUBTITLE_DATA = 201;
 
     private class EventHandler extends Handler
     {
@@ -1970,6 +1997,18 @@
                 }
                 return;
 
+            case MEDIA_SUBTITLE_DATA:
+                if (mOnSubtitleDataListener == null) {
+                    return;
+                }
+                if (msg.obj instanceof Parcel) {
+                    Parcel parcel = (Parcel) msg.obj;
+                    SubtitleData data = new SubtitleData(parcel);
+                    parcel.recycle();
+                    mOnSubtitleDataListener.onSubtitleData(mMediaPlayer, data);
+                }
+                return;
+
             case MEDIA_NOP: // interface test message - ignore
                 break;
 
@@ -2181,6 +2220,30 @@
 
     private OnTimedTextListener mOnTimedTextListener;
 
+    /**
+     * Interface definition of a callback to be invoked when a
+     * track has data available.
+     *
+     * @hide
+     */
+    public interface OnSubtitleDataListener
+    {
+        public void onSubtitleData(MediaPlayer mp, SubtitleData data);
+    }
+
+    /**
+     * Register a callback to be invoked when a track has data available.
+     *
+     * @param listener the callback that will be run
+     *
+     * @hide
+     */
+    public void setOnSubtitleDataListener(OnSubtitleDataListener listener)
+    {
+        mOnSubtitleDataListener = listener;
+    }
+
+    private OnSubtitleDataListener mOnSubtitleDataListener;
 
     /* Do not change these values without updating their counterparts
      * in include/media/mediaplayer.h!
diff --git a/media/java/android/media/SubtitleData.java b/media/java/android/media/SubtitleData.java
new file mode 100644
index 0000000..f552e82
--- /dev/null
+++ b/media/java/android/media/SubtitleData.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.media;
+
+import android.os.Parcel;
+import android.util.Log;
+
+/**
+ * @hide
+ *
+ * Class to hold the subtitle track's data, including:
+ * <ul>
+ * <li> Track index</li>
+ * <li> Start time (in microseconds) of the data</li>
+ * <li> Duration (in microseconds) of the data</li>
+ * <li> A byte-array of the data</li>
+ * </ul>
+ *
+ * <p> To receive the subtitle data, applications need to do the following:
+ *
+ * <ul>
+ * <li> Select a track of type MEDIA_TRACK_TYPE_SUBTITLE with {@link MediaPlayer.selectTrack(int)</li>
+ * <li> Implement the {@link MediaPlayer.OnSubtitleDataListener} interface</li>
+ * <li> Register the {@link MediaPlayer.OnSubtitleDataListener} callback on a MediaPlayer object</li>
+ * </ul>
+ *
+ * @see android.media.MediaPlayer
+ */
+public final class SubtitleData
+{
+    private static final String TAG = "SubtitleData";
+
+    private int mTrackIndex;
+    private long mStartTimeUs;
+    private long mDurationUs;
+    private byte[] mData;
+
+    public SubtitleData(Parcel parcel) {
+        if (!parseParcel(parcel)) {
+            throw new IllegalArgumentException("parseParcel() fails");
+        }
+    }
+
+    public int getTrackIndex() {
+        return mTrackIndex;
+    }
+
+    public long getStartTimeUs() {
+        return mStartTimeUs;
+    }
+
+    public long getDurationUs() {
+        return mDurationUs;
+    }
+
+    public byte[] getData() {
+        return mData;
+    }
+
+    private boolean parseParcel(Parcel parcel) {
+        parcel.setDataPosition(0);
+        if (parcel.dataAvail() == 0) {
+            return false;
+        }
+
+        mTrackIndex = parcel.readInt();
+        mStartTimeUs = parcel.readLong();
+        mDurationUs = parcel.readLong();
+        mData = new byte[parcel.readInt()];
+        parcel.readByteArray(mData);
+
+        return true;
+    }
+}
diff --git a/packages/Keyguard/AndroidManifest.xml b/packages/Keyguard/AndroidManifest.xml
index 7d77c48..f3106da 100644
--- a/packages/Keyguard/AndroidManifest.xml
+++ b/packages/Keyguard/AndroidManifest.xml
@@ -38,6 +38,9 @@
     <uses-permission android:name="android.permission.BIND_DEVICE_ADMIN" />
     <uses-permission android:name="android.permission.CHANGE_COMPONENT_ENABLED_STATE" />
 
+    <!-- Permission for the Hotword detector service -->
+    <uses-permission android:name="com.google.android.googlequicksearchbox.SEARCH_API" />
+
     <application android:label="@string/app_name"
         android:process="com.android.systemui"
         android:persistent="true" >
diff --git a/packages/Keyguard/src/com/android/keyguard/HotwordServiceClient.java b/packages/Keyguard/src/com/android/keyguard/HotwordServiceClient.java
new file mode 100644
index 0000000..94733d4
--- /dev/null
+++ b/packages/Keyguard/src/com/android/keyguard/HotwordServiceClient.java
@@ -0,0 +1,208 @@
+/*
+ * 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.keyguard;
+
+import android.app.SearchManager;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Message;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.util.Log;
+
+import com.google.android.search.service.IHotwordService;
+import com.google.android.search.service.IHotwordServiceCallback;
+
+/**
+ * Utility class with its callbacks to simplify usage of {@link IHotwordService}.
+ *
+ * The client is meant to be used for a single hotword detection in a session.
+ * start() -> stop(); client is asked to stop & disconnect from the service.
+ * start() -> onHotwordDetected(); client disconnects from the service automatically.
+ */
+public class HotwordServiceClient implements Handler.Callback {
+    private static final String TAG = "HotwordServiceClient";
+    private static final boolean DBG = true;
+    private static final String ACTION_HOTWORD =
+            "com.google.android.search.service.IHotwordService";
+
+    private static final int MSG_SERVICE_CONNECTED = 0;
+    private static final int MSG_SERVICE_DISCONNECTED = 1;
+    private static final int MSG_HOTWORD_STARTED = 2;
+    private static final int MSG_HOTWORD_STOPPED = 3;
+    private static final int MSG_HOTWORD_DETECTED = 4;
+
+    private final Context mContext;
+    private final Callback mClientCallback;
+    private final Handler mHandler;
+
+    private IHotwordService mService;
+
+    public HotwordServiceClient(Context context, Callback callback) {
+        mContext = context;
+        mClientCallback = callback;
+        mHandler = new Handler(this);
+    }
+
+    public interface Callback {
+        void onServiceConnected();
+        void onServiceDisconnected();
+        void onHotwordDetectionStarted();
+        void onHotwordDetectionStopped();
+        void onHotwordDetected(String action);
+    }
+
+    /**
+     * Binds to the {@link IHotwordService} and starts hotword detection
+     * when the service is connected.
+     *
+     * @return false if the service can't be bound to.
+     */
+    public synchronized boolean start() {
+        if (mService != null) {
+            if (DBG) Log.d(TAG, "Multiple call to start(), service was already bound");
+            return true;
+        } else {
+            // TODO: The hotword service is currently hosted within the search app
+            // so the component handling the assist intent should handle hotwording
+            // as well.
+            final Intent intent =
+                    ((SearchManager) mContext.getSystemService(Context.SEARCH_SERVICE))
+                            .getAssistIntent(mContext, true, UserHandle.USER_CURRENT);
+            if (intent == null) {
+                return false;
+            }
+
+            Intent hotwordIntent = new Intent(ACTION_HOTWORD);
+            hotwordIntent.fillIn(intent, Intent.FILL_IN_PACKAGE);
+            return mContext.bindService(
+                    hotwordIntent,
+                   mConnection,
+                   Context.BIND_AUTO_CREATE);
+        }
+    }
+
+    /**
+     * Unbinds from the the {@link IHotwordService}.
+     */
+    public synchronized void stop() {
+        if (mService != null) {
+            mContext.unbindService(mConnection);
+            mService = null;
+        }
+    }
+
+    @Override
+    public boolean handleMessage(Message msg) {
+        switch (msg.what) {
+            case MSG_SERVICE_CONNECTED:
+                handleServiceConnected();
+                break;
+            case MSG_SERVICE_DISCONNECTED:
+                handleServiceDisconnected();
+                break;
+            case MSG_HOTWORD_STARTED:
+                handleHotwordDetectionStarted();
+                break;
+            case MSG_HOTWORD_STOPPED:
+                handleHotwordDetectionStopped();
+                break;
+            case MSG_HOTWORD_DETECTED:
+                handleHotwordDetected((String) msg.obj);
+                break;
+            default:
+                if (DBG) Log.e(TAG, "Unhandled message");
+                return false;
+        }
+        return true;
+    }
+
+    private void handleServiceConnected() {
+        if (DBG) Log.d(TAG, "handleServiceConnected()");
+        if (mClientCallback != null) mClientCallback.onServiceConnected();
+        try {
+            mService.requestHotwordDetection(mServiceCallback);
+        } catch (RemoteException e) {
+            Log.e(TAG, "Exception while registering callback", e);
+            mHandler.sendEmptyMessage(MSG_SERVICE_DISCONNECTED);
+        }
+    }
+
+    private void handleServiceDisconnected() {
+        if (DBG) Log.d(TAG, "handleServiceDisconnected()");
+        mService = null;
+        if (mClientCallback != null) mClientCallback.onServiceDisconnected();
+    }
+
+    private void handleHotwordDetectionStarted() {
+        if (DBG) Log.d(TAG, "handleHotwordDetectionStarted()");
+        if (mClientCallback != null) mClientCallback.onHotwordDetectionStarted();
+    }
+
+    private void handleHotwordDetectionStopped() {
+        if (DBG) Log.d(TAG, "handleHotwordDetectionStopped()");
+        if (mClientCallback != null) mClientCallback.onHotwordDetectionStopped();
+    }
+
+    void handleHotwordDetected(final String action) {
+        if (DBG) Log.d(TAG, "handleHotwordDetected()");
+        if (mClientCallback != null) mClientCallback.onHotwordDetected(action);
+        stop();
+    }
+
+    /**
+     * Implements service connection methods.
+     */
+    private ServiceConnection mConnection = new ServiceConnection() {
+        /**
+         * Called when the service connects after calling bind().
+         */
+        public void onServiceConnected(ComponentName className, IBinder iservice) {
+            mService = IHotwordService.Stub.asInterface(iservice);
+            mHandler.sendEmptyMessage(MSG_SERVICE_CONNECTED);
+        }
+
+        /**
+         * Called if the service unexpectedly disconnects. This indicates an error.
+         */
+        public void onServiceDisconnected(ComponentName className) {
+            mService = null;
+            mHandler.sendEmptyMessage(MSG_SERVICE_DISCONNECTED);
+        }
+    };
+
+    /**
+     * Implements the AIDL IHotwordServiceCallback interface.
+     */
+    private final IHotwordServiceCallback mServiceCallback = new IHotwordServiceCallback.Stub() {
+
+        public void onHotwordDetectionStarted() {
+            mHandler.sendEmptyMessage(MSG_HOTWORD_STARTED);
+        }
+
+        public void onHotwordDetectionStopped() {
+            mHandler.sendEmptyMessage(MSG_HOTWORD_STOPPED);
+        }
+
+        public void onHotwordDetected(String action) {
+            mHandler.obtainMessage(MSG_HOTWORD_DETECTED, action).sendToTarget();
+        }
+    };
+}
diff --git a/packages/Keyguard/src/com/android/keyguard/KeyguardSelectorView.java b/packages/Keyguard/src/com/android/keyguard/KeyguardSelectorView.java
index 4d891be..1c658e3 100644
--- a/packages/Keyguard/src/com/android/keyguard/KeyguardSelectorView.java
+++ b/packages/Keyguard/src/com/android/keyguard/KeyguardSelectorView.java
@@ -22,8 +22,11 @@
 import android.content.Context;
 import android.content.Intent;
 import android.graphics.drawable.Drawable;
+import android.os.Build;
+import android.os.PowerManager;
 import android.os.UserHandle;
 import android.provider.Settings;
+import android.telephony.TelephonyManager;
 import android.util.AttributeSet;
 import android.util.Log;
 import android.util.Slog;
@@ -34,12 +37,15 @@
 import com.android.internal.widget.LockPatternUtils;
 import com.android.internal.widget.multiwaveview.GlowPadView;
 import com.android.internal.widget.multiwaveview.GlowPadView.OnTriggerListener;
+import com.android.keyguard.KeyguardHostView.OnDismissAction;
 
 public class KeyguardSelectorView extends LinearLayout implements KeyguardSecurityView {
     private static final boolean DEBUG = KeyguardHostView.DEBUG;
     private static final String TAG = "SecuritySelectorView";
     private static final String ASSIST_ICON_METADATA_NAME =
         "com.android.systemui.action_assist_icon";
+    // Flag to enable/disable hotword detection on lock screen.
+    private static final boolean FLAG_HOTWORD = true;
 
     private KeyguardSecurityCallback mCallback;
     private GlowPadView mGlowPadView;
@@ -51,11 +57,15 @@
     private LockPatternUtils mLockPatternUtils;
     private SecurityMessageDisplay mSecurityMessageDisplay;
     private Drawable mBouncerFrame;
+    private HotwordServiceClient mHotwordClient;
 
     OnTriggerListener mOnTriggerListener = new OnTriggerListener() {
 
         public void onTrigger(View v, int target) {
             final int resId = mGlowPadView.getResourceIdForTarget(target);
+            if (FLAG_HOTWORD) {
+                maybeStopHotwordDetector();
+            }
             switch (resId) {
                 case R.drawable.ic_action_assist_generic:
                     Intent assistIntent =
@@ -103,7 +113,7 @@
 
     };
 
-    KeyguardUpdateMonitorCallback mInfoCallback = new KeyguardUpdateMonitorCallback() {
+    KeyguardUpdateMonitorCallback mUpdateCallback = new KeyguardUpdateMonitorCallback() {
 
         @Override
         public void onDevicePolicyManagerStateChanged() {
@@ -114,6 +124,24 @@
         public void onSimStateChanged(State simState) {
             updateTargets();
         }
+
+        @Override
+        public void onPhoneStateChanged(int phoneState) {
+            if (FLAG_HOTWORD) {
+                // We need to stop the hotwording when a phone call comes in
+                // TODO(sansid): This is not really needed if onPause triggers
+                // when we navigate away from the keyguard
+                if (phoneState == TelephonyManager.CALL_STATE_RINGING) {
+                    if (DEBUG) Log.d(TAG, "Stopping due to CALL_STATE_RINGING");
+                    maybeStopHotwordDetector();
+                }
+            }
+        }
+
+        @Override
+        public void onUserSwitching(int userId) {
+            maybeStopHotwordDetector();
+        }
     };
 
     private final KeyguardActivityLauncher mActivityLauncher = new KeyguardActivityLauncher() {
@@ -152,6 +180,9 @@
         mSecurityMessageDisplay = new KeyguardMessageArea.Helper(this);
         View bouncerFrameView = findViewById(R.id.keyguard_selector_view_frame);
         mBouncerFrame = bouncerFrameView.getBackground();
+        if (FLAG_HOTWORD) {
+            mHotwordClient = new HotwordServiceClient(getContext(), mHotwordCallback);
+        }
     }
 
     public void setCarrierArea(View carrierArea) {
@@ -254,12 +285,22 @@
 
     @Override
     public void onPause() {
-        KeyguardUpdateMonitor.getInstance(getContext()).removeCallback(mInfoCallback);
+        KeyguardUpdateMonitor.getInstance(getContext()).removeCallback(mUpdateCallback);
     }
 
     @Override
     public void onResume(int reason) {
-        KeyguardUpdateMonitor.getInstance(getContext()).registerCallback(mInfoCallback);
+        KeyguardUpdateMonitor.getInstance(getContext()).registerCallback(mUpdateCallback);
+        // TODO: Figure out if there's a better way to do it.
+        // Right now we don't get onPause at all, and onResume gets called
+        // multiple times (even when the screen is turned off with VIEW_REVEALED)
+        if (reason == SCREEN_ON) {
+            if (!KeyguardUpdateMonitor.getInstance(getContext()).isSwitchingUser()) {
+                maybeStartHotwordDetector();
+            }
+        } else {
+            maybeStopHotwordDetector();
+        }
     }
 
     @Override
@@ -280,4 +321,83 @@
         KeyguardSecurityViewHelper.
                 hideBouncer(mSecurityMessageDisplay, mFadeView, mBouncerFrame, duration);
     }
+
+    /**
+     * Start the hotword detector if:
+     * <li> HOTWORDING_ENABLED is true and
+     * <li> HotwordUnlock is initialized and
+     * <li> TelephonyManager is in CALL_STATE_IDLE
+     *
+     * If this method is called when the screen is off,
+     * it attempts to stop hotwording if it's running.
+     */
+    private void maybeStartHotwordDetector() {
+        if (FLAG_HOTWORD) {
+            if (DEBUG) Log.d(TAG, "maybeStartHotwordDetector()");
+            // Don't start it if the screen is off or not showing
+            PowerManager powerManager = (PowerManager) getContext().getSystemService(
+                    Context.POWER_SERVICE);
+            if (!powerManager.isScreenOn()) {
+                if (DEBUG) Log.d(TAG, "screen was off, not starting");
+                return;
+            }
+
+            KeyguardUpdateMonitor monitor = KeyguardUpdateMonitor.getInstance(getContext());
+            if (monitor.getPhoneState() != TelephonyManager.CALL_STATE_IDLE) {
+                if (DEBUG) Log.d(TAG, "Call underway, not starting");
+                return;
+            }
+            if (!mHotwordClient.start()) {
+                Log.w(TAG, "Failed to start the hotword detector");
+            }
+        }
+    }
+
+    /**
+     * Stop hotword detector if HOTWORDING_ENABLED is true.
+     */
+    private void maybeStopHotwordDetector() {
+        if (FLAG_HOTWORD) {
+            if (DEBUG) Log.d(TAG, "maybeStopHotwordDetector()");
+            mHotwordClient.stop();
+        }
+    }
+
+    private final HotwordServiceClient.Callback mHotwordCallback =
+            new HotwordServiceClient.Callback() {
+        private static final String TAG = "HotwordServiceClient.Callback";
+
+        @Override
+        public void onServiceConnected() {
+            if (DEBUG) Log.d(TAG, "onServiceConnected()");
+        }
+
+        @Override
+        public void onServiceDisconnected() {
+            if (DEBUG) Log.d(TAG, "onServiceDisconnected()");
+        }
+
+        @Override
+        public void onHotwordDetectionStarted() {
+            if (DEBUG) Log.d(TAG, "onHotwordDetectionStarted()");
+            // TODO: Change the usage of SecurityMessageDisplay to a better visual indication.
+            mSecurityMessageDisplay.setMessage("\"Ok Google...\"", true);
+        }
+
+        @Override
+        public void onHotwordDetectionStopped() {
+            if (DEBUG) Log.d(TAG, "onHotwordDetectionStopped()");
+            // TODO: Change the usage of SecurityMessageDisplay to a better visual indication.
+        }
+
+        @Override
+        public void onHotwordDetected(String action) {
+            if (DEBUG) Log.d(TAG, "onHotwordDetected(" + action + ")");
+            if (action != null) {
+                Intent intent = new Intent(action);
+                mActivityLauncher.launchActivity(intent, true, true, null, null);
+            }
+            mCallback.userActivity(0);
+        }
+    };
 }
diff --git a/packages/Keyguard/src/com/google/android/search/service/IHotwordService.aidl b/packages/Keyguard/src/com/google/android/search/service/IHotwordService.aidl
new file mode 100644
index 0000000..e053d7d
--- /dev/null
+++ b/packages/Keyguard/src/com/google/android/search/service/IHotwordService.aidl
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.search.service;
+
+import com.google.android.search.service.IHotwordServiceCallback;
+
+/**
+ * Interface exposing hotword detector as a service.
+ */
+oneway interface IHotwordService {
+
+    /**
+     * Indicates a desire to start hotword detection.
+     * It's best-effort and the client should rely on
+     * the callbacks to figure out if hotwording was actually
+     * started or not.
+     *
+     * @param a callback to notify of hotword events.
+     */
+    void requestHotwordDetection(in IHotwordServiceCallback callback);
+}
diff --git a/packages/Keyguard/src/com/google/android/search/service/IHotwordServiceCallback.aidl b/packages/Keyguard/src/com/google/android/search/service/IHotwordServiceCallback.aidl
new file mode 100644
index 0000000..7b3765f
--- /dev/null
+++ b/packages/Keyguard/src/com/google/android/search/service/IHotwordServiceCallback.aidl
@@ -0,0 +1,34 @@
+/*
+ * 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.google.android.search.service;
+
+/**
+ * Interface implemented by users of Hotword service to get callbacks
+ * for hotword events.
+ */
+oneway interface IHotwordServiceCallback {
+
+    /** Hotword detection start/stop callbacks */
+    void onHotwordDetectionStarted();
+    void onHotwordDetectionStopped();
+
+    /**
+     * Called back when hotword is detected.
+     * The action tells the client what action to take, post hotword-detection.
+     */
+    void onHotwordDetected(in String action);
+}
diff --git a/packages/PrintSpooler/AndroidManifest.xml b/packages/PrintSpooler/AndroidManifest.xml
index c00639d..1f10af8 100644
--- a/packages/PrintSpooler/AndroidManifest.xml
+++ b/packages/PrintSpooler/AndroidManifest.xml
@@ -18,7 +18,7 @@
 -->
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
         package="com.android.printspooler"
-        android:sharedUserId="android.uid.printspooler"
+        android:sharedUserId="android.uid.system"
         android:versionName="1"
         android:versionCode="1"
         coreApp="true">
@@ -51,9 +51,10 @@
         </activity>
 
         <activity
-            android:name=".ChoosePrinterActivity"
-            android:exported="false"
-            android:theme="@android:style/Theme.Holo.Light">
+            android:name=".SelectPrinterActivity"
+            android:label="@string/all_printers_label"
+            android:theme="@style/SelectPrinterActivityTheme"
+            android:exported="false">
         </activity>
 
         <receiver
diff --git a/packages/PrintSpooler/res/drawable-hdpi/ic_menu_add.png b/packages/PrintSpooler/res/drawable-hdpi/ic_menu_add.png
new file mode 100644
index 0000000..4b68f52
--- /dev/null
+++ b/packages/PrintSpooler/res/drawable-hdpi/ic_menu_add.png
Binary files differ
diff --git a/packages/PrintSpooler/res/drawable-mdpi/ic_menu_add.png b/packages/PrintSpooler/res/drawable-mdpi/ic_menu_add.png
new file mode 100644
index 0000000..15ffadd
--- /dev/null
+++ b/packages/PrintSpooler/res/drawable-mdpi/ic_menu_add.png
Binary files differ
diff --git a/packages/PrintSpooler/res/drawable-xhdpi/ic_menu_add.png b/packages/PrintSpooler/res/drawable-xhdpi/ic_menu_add.png
new file mode 100644
index 0000000..420510e
--- /dev/null
+++ b/packages/PrintSpooler/res/drawable-xhdpi/ic_menu_add.png
Binary files differ
diff --git a/packages/PrintSpooler/res/layout/choose_printer_activity.xml b/packages/PrintSpooler/res/layout/choose_printer_activity.xml
deleted file mode 100644
index c34a108..0000000
--- a/packages/PrintSpooler/res/layout/choose_printer_activity.xml
+++ /dev/null
@@ -1,23 +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.
--->
-
-<ListView xmlns:android="http://schemas.android.com/apk/res/android"
-    android:id="@+id/list_view"
-    android:layout_width="fill_parent"
-    android:layout_height="fill_parent"
-    android:orientation="vertical">
-</ListView>
-
diff --git a/packages/PrintSpooler/res/layout/print_job_config_activity_container.xml b/packages/PrintSpooler/res/layout/print_job_config_activity_container.xml
index a0c111b..7817094 100644
--- a/packages/PrintSpooler/res/layout/print_job_config_activity_container.xml
+++ b/packages/PrintSpooler/res/layout/print_job_config_activity_container.xml
@@ -20,9 +20,4 @@
     android:layout_height="wrap_content"
     android:layout_gravity="center"
     android:background="@color/container_background">
-
-    <include
-        layout="@layout/print_job_config_activity_content_editing">
-    </include>
-
 </FrameLayout>
diff --git a/packages/PrintSpooler/res/layout/select_printer_activity.xml b/packages/PrintSpooler/res/layout/select_printer_activity.xml
new file mode 100644
index 0000000..f4e1853
--- /dev/null
+++ b/packages/PrintSpooler/res/layout/select_printer_activity.xml
@@ -0,0 +1,29 @@
+<?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.
+-->
+
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:orientation="horizontal"
+    android:layout_width="fill_parent"
+    android:layout_height="wrap_content">
+
+    <fragment
+        android:name="com.android.printspooler.SelectPrinterFragment"
+        android:id="@+id/select_printer_fragment"
+        android:layout_width="fill_parent"
+        android:layout_height="wrap_content">
+    </fragment>
+
+</FrameLayout>
\ No newline at end of file
diff --git a/packages/PrintSpooler/res/layout/spinner_dropdown_item.xml b/packages/PrintSpooler/res/layout/spinner_dropdown_item.xml
index 002cc14..d14c064 100644
--- a/packages/PrintSpooler/res/layout/spinner_dropdown_item.xml
+++ b/packages/PrintSpooler/res/layout/spinner_dropdown_item.xml
@@ -15,7 +15,7 @@
 -->
 
 <LinearLayout  xmlns:android="http://schemas.android.com/apk/res/android"
-    android:layout_width="match_parent"
+    android:layout_width="fill_parent"
     android:layout_height="wrap_content"
     android:paddingStart="8dip"
     android:paddingEnd="8dip"
diff --git a/packages/PrintSpooler/res/menu/choose_printer_activity.xml b/packages/PrintSpooler/res/menu/select_printer_activity.xml
similarity index 79%
rename from packages/PrintSpooler/res/menu/choose_printer_activity.xml
rename to packages/PrintSpooler/res/menu/select_printer_activity.xml
index 3774279..28fbd35 100644
--- a/packages/PrintSpooler/res/menu/choose_printer_activity.xml
+++ b/packages/PrintSpooler/res/menu/select_printer_activity.xml
@@ -23,7 +23,15 @@
         android:actionViewClass="android.widget.SearchView"
         android:showAsAction="ifRoom"
         android:alphabeticShortcut="f"
-         android:imeOptions="actionSearch">
+        android:imeOptions="actionSearch">
+    </item>
+
+    <item
+        android:id="@+id/action_add_printer"
+        android:title="@null"
+        android:icon="@drawable/ic_menu_add"
+        android:showAsAction="ifRoom"
+        android:alphabeticShortcut="a">
     </item>
 
 </menu>
diff --git a/packages/PrintSpooler/res/values/strings.xml b/packages/PrintSpooler/res/values/strings.xml
index 1cd611f..41fc516 100644
--- a/packages/PrintSpooler/res/values/strings.xml
+++ b/packages/PrintSpooler/res/values/strings.xml
@@ -58,11 +58,32 @@
     <!-- Title for the temporary dialog show while an app is generating a print job. [CHAR LIMIT=30] -->
     <string name="generating_print_job">Generating print job</string>
 
-    <!-- Choose printer activity -->
+    <!-- Title for the save as PDF option in the printer list. [CHAR LIMIT=30] -->
+    <string name="save_as_pdf">Save as PDF</string>
+
+    <!-- Title for the open all printers UI option in the printer list. [CHAR LIMIT=30] -->
+    <string name="all_printers">All printers\.\.\.</string>
+
+    <!-- Title for the searching for printers option in the printer list
+         (only option if not printers are available). [CHAR LIMIT=40] -->
+    <string name="searching_for_printers">Searching for printers\.\.\.</string>
+
+    <!-- Select printer activity -->
 
     <!-- Title for the share action bar menu item. [CHAR LIMIT=20] -->
     <string name="search">Search</string>
 
+    <!-- Title for the select printer activity. [CHAR LIMIT=30] -->
+    <string name="all_printers_label">All printers</string>
+
+    <!-- Add printer dialog  -->
+
+    <!-- Title for the alert dialog for selecting a print service. [CHAR LIMIT=50] -->
+    <string name="choose_print_service">Choose print service</string>
+
+    <!-- Title for the button to search the play store for print services. [CHAR LIMIT=50] -->
+    <string name="search_play_store">Search in play store</string>
+
     <!-- Notifications -->
 
     <!-- Template for the notificaiton label for a printing print job. [CHAR LIMIT=25] -->
diff --git a/packages/PrintSpooler/res/values/themes.xml b/packages/PrintSpooler/res/values/themes.xml
index ab16c65..831b0ec 100644
--- a/packages/PrintSpooler/res/values/themes.xml
+++ b/packages/PrintSpooler/res/values/themes.xml
@@ -24,4 +24,12 @@
         <item name="android:colorBackgroundCacheHint">@android:color/transparent</item>
     </style>
 
+    <style name="SelectPrinterActivityTheme" parent="@android:style/Theme.Holo.Light">
+        <item name="android:actionBarStyle">@style/SelectPrinterActivityActionBarStyle</item>
+    </style>
+
+    <style name="SelectPrinterActivityActionBarStyle" parent="@android:style/Widget.Holo.ActionBar">
+        <item name="android:displayOptions">showTitle</item>
+    </style>
+
 </resources>
diff --git a/packages/PrintSpooler/src/com/android/printspooler/AvailablePrinterProvider.java b/packages/PrintSpooler/src/com/android/printspooler/AvailablePrinterProvider.java
deleted file mode 100644
index 658a224..0000000
--- a/packages/PrintSpooler/src/com/android/printspooler/AvailablePrinterProvider.java
+++ /dev/null
@@ -1,285 +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.os.Handler;
-import android.os.Looper;
-import android.os.Message;
-import android.os.RemoteException;
-import android.print.IPrinterDiscoverySessionController;
-import android.print.IPrinterDiscoverySessionObserver;
-import android.print.PrinterId;
-import android.print.PrinterInfo;
-import android.util.ArraySet;
-import android.util.Log;
-
-import java.lang.ref.WeakReference;
-import java.util.ArrayList;
-import java.util.Iterator;
-import java.util.List;
-import java.util.Set;
-
-/**
- * This class is responsible to provide the available printers.
- * It starts and stops printer discovery and manages the returned
- * printers.
- */
-public class AvailablePrinterProvider extends DataProvider<PrinterInfo>
-        implements DataLoader {
-    private static final String LOG_TAG = "AvailablePrinterProvider";
-
-    private final Set<PrinterId> mPrinteIdsSet = new ArraySet<PrinterId>();
-
-    private final List<PrinterInfo> mPrinters = new ArrayList<PrinterInfo>();
-
-    private final List<PrinterId> mPriorityList;
-
-    private PrinterDiscoverySession mDiscoverySession;
-
-    public AvailablePrinterProvider(Context context, List<PrinterId> priorityList) {
-        mDiscoverySession = new PrinterDiscoverySession(context.getMainLooper());
-        mPriorityList = priorityList;
-    }
-
-    @Override
-    public void startLoadData() {
-        mDiscoverySession.open();
-    }
-
-    @Override
-    public void stopLoadData() {
-        mDiscoverySession.close();
-    }
-
-    @Override
-    public int getItemCount() {
-        return mPrinters.size();
-    }
-
-    @Override
-    public int getItemIndex(PrinterInfo printer) {
-        return mPrinters.indexOf(printer);
-    }
-
-    @Override
-    public PrinterInfo getItemAt(int index) {
-        return mPrinters.get(index);
-    }
-
-    public void refreshItem(int index) {
-        PrinterInfo printer = getItemAt(index);
-        mDiscoverySession.requestPrinterUpdate(printer.getId());
-    }
-
-    private void addPrinters(List<PrinterInfo> printers) {
-        boolean addedPrinters = false;
-
-        final int addedPrinterCount = printers.size();
-        for (int i = 0; i < addedPrinterCount; i++) {
-           PrinterInfo addedPrinter = printers.get(i);
-           if (mPrinteIdsSet.add(addedPrinter.getId())) {
-               mPrinters.add(addedPrinter);
-               addedPrinters = true;
-           }
-        }
-
-        if (addedPrinters) {
-            notifyChanged();
-        }
-    }
-
-    private void updatePrinters(List<PrinterInfo> printers) {
-        boolean updatedPrinters = false;
-
-        final int updatedPrinterCount = printers.size();
-        for (int i = 0; i < updatedPrinterCount; i++) {
-            PrinterInfo updatedPrinter = printers.get(i);
-            if (mPrinteIdsSet.contains(updatedPrinter.getId())) {
-                final int oldPrinterCount = mPrinters.size();
-                for (int j = 0; j < oldPrinterCount; j++) {
-                    PrinterInfo oldPrinter = mPrinters.get(j);
-                    if (updatedPrinter.getId().equals(oldPrinter.getId())) {
-                        mPrinters.set(j, updatedPrinter);
-                        updatedPrinters = true;
-                        break;
-                    }
-                }
-            }
-        }
-
-        if (updatedPrinters) {
-            notifyChanged();
-        }
-    }
-
-    private void removePrinters(List<PrinterId> printers) {
-        boolean removedPrinters = false;
-
-        final int removedPrinterCount = printers.size();
-        for (int i = 0; i < removedPrinterCount; i++) {
-            PrinterId removedPrinter = printers.get(i);
-            if (mPrinteIdsSet.contains(removedPrinter)) {
-                mPrinteIdsSet.remove(removedPrinter);
-                Iterator<PrinterInfo> iterator = mPrinters.iterator();
-                while (iterator.hasNext()) {
-                    PrinterInfo oldPrinter = iterator.next();
-                    if (removedPrinter.equals(oldPrinter.getId())) {
-                        iterator.remove();
-                        break;
-                    }
-                }
-            }
-        }
-
-        if (removedPrinters) {
-            notifyChanged();
-        }
-    }
-
-    private final class PrinterDiscoverySession {
-
-        private final Handler mHandler;
-
-        private final IPrinterDiscoverySessionObserver mObserver;
-
-        private IPrinterDiscoverySessionController mController;
-
-        public PrinterDiscoverySession(Looper looper) {
-            mHandler = new SessionHandler(looper);
-            mObserver = new PrinterDiscoverySessionObserver(this);
-        }
-
-        public void open() {
-            PrintSpooler.peekInstance().createPrinterDiscoverySession(
-                    mObserver);
-        }
-
-        public void close() {
-            if (mController != null) {
-                try {
-                    mController.close();
-                } catch (RemoteException re) {
-                    Log.e(LOG_TAG, "Error closing printer discovery session", re);
-                } finally {
-                    mController = null;
-                }
-            }
-        }
-
-        public void requestPrinterUpdate(PrinterId printerId) {
-            if (mController != null) {
-                try {
-                    mController.requestPrinterUpdate(printerId);
-                } catch (RemoteException re) {
-                    Log.e(LOG_TAG, "Error requesting printer udpdate", re);
-                }
-            }
-        }
-
-        private final class SessionHandler extends Handler {
-            public static final int MSG_SET_CONTROLLER = 1;
-            public static final int MSG_ON_PRINTERS_ADDED = 2;
-            public static final int MSG_ON_PRINTERS_REMOVED = 3;
-            public static final int MSG_ON_PRINTERS_UPDATED = 4;
-
-            public SessionHandler(Looper looper) {
-                super(looper, null, false);
-            }
-
-            @Override
-            @SuppressWarnings("unchecked")
-            public void handleMessage(Message message) {
-                switch (message.what) {
-                    case MSG_SET_CONTROLLER: {
-                        mController = (IPrinterDiscoverySessionController) message.obj;
-                        try {
-                            mController.open(mPriorityList);
-                        } catch (RemoteException e) {
-                            Log.e(LOG_TAG, "Error starting printer discovery");
-                        }
-                    } break;
-
-                    case MSG_ON_PRINTERS_ADDED: {
-                        List<PrinterInfo> printers = (List<PrinterInfo>) message.obj;
-                        addPrinters(printers);
-                    } break;
-
-                    case MSG_ON_PRINTERS_REMOVED: {
-                        List<PrinterId> printers = (List<PrinterId>) message.obj;
-                        removePrinters(printers);
-                    } break;
-
-                    case MSG_ON_PRINTERS_UPDATED: {
-                        List<PrinterInfo> printers = (List<PrinterInfo>) message.obj;
-                        updatePrinters(printers);
-                    } break;
-                };
-            }
-        }
-    }
-
-    private static final class PrinterDiscoverySessionObserver
-            extends IPrinterDiscoverySessionObserver.Stub {
-
-        private final WeakReference<PrinterDiscoverySession> mWeakSession;
-
-        public PrinterDiscoverySessionObserver(PrinterDiscoverySession session) {
-            mWeakSession = new WeakReference<PrinterDiscoverySession>(session);
-        }
-
-        @Override
-        public void setController(IPrinterDiscoverySessionController controller) {
-            PrinterDiscoverySession sesison = mWeakSession.get();
-            if (sesison != null) {
-                sesison.mHandler.obtainMessage(
-                        PrinterDiscoverySession.SessionHandler.MSG_SET_CONTROLLER,
-                        controller).sendToTarget();
-            }
-        }
-
-        @Override
-        public void onPrintersAdded(List<PrinterInfo> printers) {
-            PrinterDiscoverySession sesison = mWeakSession.get();
-            if (sesison != null) {
-                sesison.mHandler.obtainMessage(
-                        PrinterDiscoverySession.SessionHandler.MSG_ON_PRINTERS_ADDED,
-                        printers).sendToTarget();
-            }
-        }
-
-        @Override
-        public void onPrintersRemoved(List<PrinterId> printers) {
-            PrinterDiscoverySession session = mWeakSession.get();
-            if (session != null) {
-                session.mHandler.obtainMessage(
-                        PrinterDiscoverySession.SessionHandler.MSG_ON_PRINTERS_REMOVED,
-                        printers).sendToTarget();
-            }
-        }
-
-        @Override
-        public void onPrintersUpdated(List<PrinterInfo> printers) {
-            PrinterDiscoverySession session = mWeakSession.get();
-            if (session != null) {
-                session.mHandler.obtainMessage(
-                        PrinterDiscoverySession.SessionHandler.MSG_ON_PRINTERS_UPDATED,
-                        printers).sendToTarget();
-            }
-        }
-    };
-}
diff --git a/packages/PrintSpooler/src/com/android/printspooler/ChoosePrinterActivity.java b/packages/PrintSpooler/src/com/android/printspooler/ChoosePrinterActivity.java
deleted file mode 100644
index 8b0dd66a..0000000
--- a/packages/PrintSpooler/src/com/android/printspooler/ChoosePrinterActivity.java
+++ /dev/null
@@ -1,28 +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.os.Bundle;
-
-public class ChoosePrinterActivity extends Activity {
-
-    @Override
-    public void onCreate(Bundle bundle) {
-        setContentView(R.layout.choose_printer_activity);
-    }
-}
diff --git a/packages/PrintSpooler/src/com/android/printspooler/DataLoader.java b/packages/PrintSpooler/src/com/android/printspooler/DataLoader.java
deleted file mode 100644
index 82cc2e1..0000000
--- a/packages/PrintSpooler/src/com/android/printspooler/DataLoader.java
+++ /dev/null
@@ -1,33 +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;
-
-/**
- * This is the contract for a class that know how to load data.
- */
-public interface DataLoader {
-
-    /**
-     * Requests to start loading data.
-     */
-    public void startLoadData();
-
-    /**
-     * Requests to stop loading data.
-     */
-    public void stopLoadData();
-}
diff --git a/packages/PrintSpooler/src/com/android/printspooler/DataProvider.java b/packages/PrintSpooler/src/com/android/printspooler/DataProvider.java
deleted file mode 100644
index 7b10903..0000000
--- a/packages/PrintSpooler/src/com/android/printspooler/DataProvider.java
+++ /dev/null
@@ -1,50 +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.database.DataSetObservable;
-
-/**
- * This is the simple contract for data providers.
- *
- * @param <T> The type of the providers data.
- */
-public abstract class DataProvider<T> extends DataSetObservable {
-
-    /**
-     * Gets the number of items.
-     *
-     * @return The item count.
-     */
-    public abstract int getItemCount();
-
-    /**
-     * Gets the index of an item.
-     *
-     * @param item The item.
-     * @return The item index.
-     */
-    public abstract int getItemIndex(T item);
-
-    /**
-     * Gets an item at a given position.
-     *
-     * @param index The position.
-     * @return The item.
-     */
-    public abstract T getItemAt(int index);
-}
diff --git a/packages/PrintSpooler/src/com/android/printspooler/FavoritePrinterProvider.java b/packages/PrintSpooler/src/com/android/printspooler/FavoritePrinterProvider.java
deleted file mode 100644
index 2c539d1..0000000
--- a/packages/PrintSpooler/src/com/android/printspooler/FavoritePrinterProvider.java
+++ /dev/null
@@ -1,364 +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.ComponentName;
-import android.content.Context;
-import android.os.AsyncTask;
-import android.os.Build;
-import android.print.PrinterId;
-import android.print.PrinterInfo;
-import android.util.ArrayMap;
-import android.util.AtomicFile;
-import android.util.Log;
-import android.util.Slog;
-import android.util.Xml;
-
-import com.android.internal.util.FastXmlSerializer;
-
-import libcore.io.IoUtils;
-
-import org.xmlpull.v1.XmlPullParser;
-import org.xmlpull.v1.XmlPullParserException;
-import org.xmlpull.v1.XmlSerializer;
-
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileNotFoundException;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-import java.util.Map;
-
-/**
- * This class provides the favorite printers based on past usage.
- */
-final class FavoritePrinterProvider extends DataProvider<PrinterInfo> implements DataLoader {
-
-    private static final String LOG_TAG = "FavoritePrinterProvider";
-
-    private static final boolean DEBUG = true && Build.IS_DEBUGGABLE;
-
-    private static final int MAX_HISTORY_LENGTH = 50;
-
-    private static final double WEIGHT_DECAY_COEFFICIENT = 0.95f;
-
-    private final List<PrinterRecord> mHistoricalPrinters = new ArrayList<PrinterRecord>();
-
-    private final List<PrinterRecord> mFavoritePrinters = new ArrayList<PrinterRecord>();
-
-    private final PersistenceManager mPersistenceManager;
-
-    public FavoritePrinterProvider(Context context) {
-        mPersistenceManager = new PersistenceManager(context);
-    }
-
-    public void addPrinter(PrinterInfo printer) {
-        addPrinterInternal(printer);
-        computeFavoritePrinters();
-        mPersistenceManager.writeState();
-    }
-
-    @Override
-    public int getItemCount() {
-        return mFavoritePrinters.size();
-    }
-
-    @Override
-    public PrinterInfo getItemAt(int index) {
-        return mFavoritePrinters.get(index).printer;
-    }
-
-    @Override
-    public int getItemIndex(PrinterInfo printer) {
-        return mFavoritePrinters.indexOf(printer);
-    }
-
-    @Override
-    public void startLoadData() {
-        mPersistenceManager.readStateLocked();
-        computeFavoritePrinters();
-    }
-
-    @Override
-    public void stopLoadData() {
-        /* do nothing */
-    }
-
-    private void addPrinterInternal(PrinterInfo printer) {
-        if (mHistoricalPrinters.size() >= MAX_HISTORY_LENGTH) {
-            mHistoricalPrinters.remove(0);
-        }
-        mHistoricalPrinters.add(new PrinterRecord(printer));
-    }
-
-    private void computeFavoritePrinters() {
-        Map<PrinterId, PrinterRecord> recordMap =
-                new ArrayMap<PrinterId, PrinterRecord>();
-
-        // Recompute the weights.
-        float currentWeight = 1.0f;
-        final int printerCount = mHistoricalPrinters.size();
-        for (int i = printerCount - 1; i >= 0; i--) {
-            PrinterRecord record = mHistoricalPrinters.get(i);
-            record.weight = currentWeight;
-            // Aggregate weight for the same printer
-            PrinterRecord oldRecord = recordMap.put(record.printer.getId(), record);
-            if (oldRecord != null) {
-                record.weight += oldRecord.weight;
-            }
-            currentWeight *= WEIGHT_DECAY_COEFFICIENT;
-        }
-
-        // Copy the unique printer records with computed weights.
-        mFavoritePrinters.addAll(recordMap.values());
-
-        // Soft the favorite printers.
-        Collections.sort(mFavoritePrinters);
-    }
-
-    private final class PrinterRecord implements Comparable<PrinterRecord> {
-        public final PrinterInfo printer;
-        public float weight;
-
-        public PrinterRecord(PrinterInfo printer) {
-            this.printer = printer;
-        }
-
-        @Override
-        public int compareTo(PrinterRecord another) {
-            return Float.floatToIntBits(another.weight) - Float.floatToIntBits(weight);
-        }
-    }
-
-    private final class PersistenceManager {
-        private static final String PERSIST_FILE_NAME = "printer_history.xml";
-
-        private static final String TAG_PRINTERS = "printers";
-
-        private static final String TAG_PRINTER = "printer";
-        private static final String TAG_PRINTER_ID = "printerId";
-
-        private static final String ATTR_LOCAL_ID = "localId";
-        private static final String ATTR_SERVICE_NAME = "serviceName";
-
-        private static final String ATTR_NAME = "name";
-        private static final String ATTR_DESCRIPTION = "description";
-        private static final String ATTR_STATUS = "status";
-
-        private final AtomicFile mStatePersistFile;
-
-        private PersistenceManager(Context context) {
-            mStatePersistFile = new AtomicFile(new File(context.getFilesDir(),
-                    PERSIST_FILE_NAME));
-        }
-
-        @SuppressWarnings("unchecked")
-        public void writeState() {
-
-            new AsyncTask<List<PrinterRecord>, Void, Void>() {
-                @Override
-                protected Void doInBackground(List<PrinterRecord>... printers) {
-                    doWriteState(printers[0]);
-                    return null;
-                }
-
-                @Override
-                protected void onPostExecute(Void result) {
-                    notifyChanged();
-                }
-
-            }.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR,
-                    new ArrayList<PrinterRecord>(mHistoricalPrinters));
-        }
-
-        private void doWriteState(List<PrinterRecord> printers) {
-            FileOutputStream out = null;
-            try {
-                out = mStatePersistFile.startWrite();
-
-                XmlSerializer serializer = new FastXmlSerializer();
-                serializer.setOutput(out, "utf-8");
-                serializer.startDocument(null, true);
-                serializer.startTag(null, TAG_PRINTERS);
-
-                final int printerCount = printers.size();
-                for (int i = printerCount - 1; i >= 0; i--) {
-                    PrinterInfo printer = printers.get(i).printer;
-
-                    serializer.startTag(null, TAG_PRINTER);
-
-                    serializer.attribute(null, ATTR_NAME, printer.getName());
-                    serializer.attribute(null, ATTR_STATUS, String.valueOf(printer.getStatus()));
-                    String description = printer.getDescription();
-                    if (description != null) {
-                        serializer.attribute(null, ATTR_DESCRIPTION, description);
-                    }
-
-                    PrinterId printerId = printer.getId();
-                    serializer.startTag(null, TAG_PRINTER_ID);
-                    serializer.attribute(null, ATTR_LOCAL_ID, printerId.getLocalId());
-                    serializer.attribute(null, ATTR_SERVICE_NAME, printerId.getServiceName()
-                            .flattenToString());
-                    serializer.endTag(null, TAG_PRINTER_ID);
-
-                    serializer.endTag(null, TAG_PRINTER);
-
-                    if (DEBUG) {
-                        Log.i(LOG_TAG, "[PERSISTED] " + printer);
-                    }
-                }
-
-                serializer.endTag(null, TAG_PRINTERS);
-                serializer.endDocument();
-                mStatePersistFile.finishWrite(out);
-
-                if (DEBUG) {
-                    Log.i(LOG_TAG, "[PERSIST END]");
-                }
-            } catch (IOException ioe) {
-                Slog.w(LOG_TAG, "Failed to write printer history, restoring backup.", ioe);
-                mStatePersistFile.failWrite(out);
-            } finally {
-                IoUtils.closeQuietly(out);
-            }
-        }
-
-        public void readStateLocked() {
-            FileInputStream in = null;
-            try {
-                in = mStatePersistFile.openRead();
-            } catch (FileNotFoundException e) {
-                Log.i(LOG_TAG, "No existing printer history.");
-                return;
-            }
-            try {
-                XmlPullParser parser = Xml.newPullParser();
-                parser.setInput(in, null);
-                parseState(parser);
-            } catch (IllegalStateException ise) {
-                Slog.w(LOG_TAG, "Failed parsing ", ise);
-            } catch (NullPointerException npe) {
-                Slog.w(LOG_TAG, "Failed parsing ", npe);
-            } catch (NumberFormatException nfe) {
-                Slog.w(LOG_TAG, "Failed parsing ", nfe);
-            } catch (XmlPullParserException xppe) {
-                Slog.w(LOG_TAG, "Failed parsing ", xppe);
-            } catch (IOException ioe) {
-                Slog.w(LOG_TAG, "Failed parsing ", ioe);
-            } catch (IndexOutOfBoundsException iobe) {
-                Slog.w(LOG_TAG, "Failed parsing ", iobe);
-            } finally {
-                IoUtils.closeQuietly(in);
-            }
-            notifyChanged();
-        }
-
-        private void parseState(XmlPullParser parser)
-                throws IOException, XmlPullParserException {
-            parser.next();
-            skipEmptyTextTags(parser);
-            expect(parser, XmlPullParser.START_TAG, TAG_PRINTERS);
-            parser.next();
-
-            while (parsePrinter(parser)) {
-                parser.next();
-            }
-
-            skipEmptyTextTags(parser);
-            expect(parser, XmlPullParser.END_TAG, TAG_PRINTERS);
-
-            // We were reading the new records first and appended them first,
-            // hence the historical list is in a reversed order, so fix that.
-            Collections.reverse(mHistoricalPrinters);
-        }
-
-        private boolean parsePrinter(XmlPullParser parser)
-                throws IOException, XmlPullParserException {
-            skipEmptyTextTags(parser);
-            if (!accept(parser, XmlPullParser.START_TAG, TAG_PRINTER)) {
-                return false;
-            }
-
-            String name = parser.getAttributeValue(null, ATTR_NAME);
-            String description = parser.getAttributeValue(null, ATTR_DESCRIPTION);
-            final int status = Integer.parseInt(parser.getAttributeValue(null, ATTR_STATUS));
-
-            parser.next();
-
-            skipEmptyTextTags(parser);
-            expect(parser, XmlPullParser.START_TAG, TAG_PRINTER_ID);
-            String localId = parser.getAttributeValue(null, ATTR_LOCAL_ID);
-            ComponentName service = ComponentName.unflattenFromString(parser.getAttributeValue(
-                    null, ATTR_SERVICE_NAME));
-            PrinterId printerId =  new PrinterId(service, localId);
-            parser.next();
-            skipEmptyTextTags(parser);
-            expect(parser, XmlPullParser.END_TAG, TAG_PRINTER_ID);
-            parser.next();
-
-            PrinterInfo.Builder builder = new PrinterInfo.Builder(printerId, name, status);
-            builder.setDescription(description);
-            PrinterInfo printer = builder.create();
-
-            addPrinterInternal(printer);
-
-            if (DEBUG) {
-                Log.i(LOG_TAG, "[RESTORED] " + printer);
-            }
-
-            skipEmptyTextTags(parser);
-            expect(parser, XmlPullParser.END_TAG, TAG_PRINTER);
-
-            return true;
-        }
-
-        private void expect(XmlPullParser parser, int type, String tag)
-                throws IOException, XmlPullParserException {
-            if (!accept(parser, type, tag)) {
-                throw new XmlPullParserException("Exepected event: " + type
-                        + " and tag: " + tag + " but got event: " + parser.getEventType()
-                        + " and tag:" + parser.getName());
-            }
-        }
-
-        private void skipEmptyTextTags(XmlPullParser parser)
-                throws IOException, XmlPullParserException {
-            while (accept(parser, XmlPullParser.TEXT, null)
-                    && "\n".equals(parser.getText())) {
-                parser.next();
-            }
-        }
-
-        private boolean accept(XmlPullParser parser, int type, String tag)
-                throws IOException, XmlPullParserException {
-            if (parser.getEventType() != type) {
-                return false;
-            }
-            if (tag != null) {
-                if (!tag.equals(parser.getName())) {
-                    return false;
-                }
-            } else if (parser.getName() != null) {
-                return false;
-            }
-            return true;
-        }
-    }
-}
diff --git a/packages/PrintSpooler/src/com/android/printspooler/FusedPrintersProvider.java b/packages/PrintSpooler/src/com/android/printspooler/FusedPrintersProvider.java
new file mode 100644
index 0000000..6bad5b3
--- /dev/null
+++ b/packages/PrintSpooler/src/com/android/printspooler/FusedPrintersProvider.java
@@ -0,0 +1,575 @@
+/*
+ * 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.ComponentName;
+import android.content.Context;
+import android.content.Loader;
+import android.os.AsyncTask;
+import android.os.Build;
+import android.print.PrinterId;
+import android.print.PrinterInfo;
+import android.util.ArrayMap;
+import android.util.AtomicFile;
+import android.util.Log;
+import android.util.Slog;
+import android.util.Xml;
+
+import com.android.internal.util.FastXmlSerializer;
+import com.android.printspooler.PrintSpoolerService.PrinterDiscoverySession;
+
+import libcore.io.IoUtils;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlSerializer;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 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>> {
+    private static final String LOG_TAG = "FusedPrintersProvider";
+
+    private static final boolean DEBUG = true && Build.IS_DEBUGGABLE;
+
+    private static final double WEIGHT_DECAY_COEFFICIENT = 0.95f;
+
+    private static final int MAX_HISTORY_LENGTH = 50;
+
+    private static final int MAX_HISTORICAL_PRINTER_COUNT = 4;
+
+    private final Map<PrinterId, PrinterInfo> mPrinters =
+            new LinkedHashMap<PrinterId, PrinterInfo>();
+
+    private final PersistenceManager mPersistenceManager;
+
+    private PrinterDiscoverySession mDiscoverySession;
+
+    private List<PrinterInfo> mFavoritePrinters;
+
+    public FusedPrintersProvider(Context context) {
+        super(context);
+        mPersistenceManager = new PersistenceManager(context);
+    }
+
+    public void addHistoricalPrinter(PrinterInfo printer) {
+        mPersistenceManager.addPrinterAndWritePrinterHistory(printer);
+    }
+
+    public List<PrinterInfo> getPrinters() {
+        return new ArrayList<PrinterInfo>(mPrinters.values());
+    }
+
+    @Override
+    public void deliverResult(List<PrinterInfo> printers) {
+        if (isStarted()) {
+            super.deliverResult(printers);
+        }
+    }
+
+    @Override
+    protected void onStartLoading() {
+        if (DEBUG) {
+            Log.i(LOG_TAG, "onStartLoading()");
+        }
+        // The contract is that if we already have a valid,
+        // result the we have to deliver it immediately.
+        if (!mPrinters.isEmpty()) {
+            deliverResult(new ArrayList<PrinterInfo>(mPrinters.values()));
+        }
+        // If the data has changed since the last load
+        // or is not available, start a load.
+        if (takeContentChanged() || mPrinters.isEmpty()) {
+            onForceLoad();
+        }
+    }
+
+    @Override
+    protected void onStopLoading() {
+        if (DEBUG) {
+            Log.i(LOG_TAG, "onStopLoading()");
+        }
+        onCancelLoad();
+    }
+
+    @Override
+    protected void onForceLoad() {
+        if (DEBUG) {
+            Log.i(LOG_TAG, "onForceLoad()");
+        }
+        onCancelLoad();
+        loadInternal();
+    }
+
+    private void loadInternal() {
+        if (mDiscoverySession == null) {
+            mDiscoverySession = new MyPrinterDiscoverySession();
+            mPersistenceManager.readPrinterHistory();
+        }
+        if (mPersistenceManager.isReadHistoryCompleted()
+                && !mDiscoverySession.isStarted()) {
+            final int favoriteCount = Math.min(MAX_HISTORICAL_PRINTER_COUNT,
+                    mFavoritePrinters.size());
+            List<PrinterId> printerIds = new ArrayList<PrinterId>(favoriteCount);
+            for (int i = 0; i < favoriteCount; i++) {
+                printerIds.add(mFavoritePrinters.get(i).getId());
+            }
+            mDiscoverySession.startPrinterDisovery(printerIds);
+        }
+    }
+
+    @Override
+    protected boolean onCancelLoad() {
+        if (DEBUG) {
+            Log.i(LOG_TAG, "onCancelLoad()");
+        }
+        return cancelInternal();
+    }
+
+    private boolean cancelInternal() {
+        if (mDiscoverySession != null && mDiscoverySession.isStarted()) {
+            mDiscoverySession.stopPrinterDiscovery();
+            return true;
+        } else if (mPersistenceManager.isReadHistoryInProgress()) {
+            return mPersistenceManager.stopReadPrinterHistory();
+        }
+        return false;
+    }
+
+    @Override
+    protected void onReset() {
+        if (DEBUG) {
+            Log.i(LOG_TAG, "onReset()");
+        }
+        onStopLoading();
+        mPrinters.clear();
+        if (mDiscoverySession != null) {
+            mDiscoverySession.destroy();
+            mDiscoverySession = null;
+        }
+    }
+
+    @Override
+    protected void onAbandon() {
+        if (DEBUG) {
+            Log.i(LOG_TAG, "onAbandon()");
+        }
+        onStopLoading();
+    }
+
+    public void refreshPrinter(PrinterId printerId) {
+        if (isStarted() && mDiscoverySession != null && mDiscoverySession.isStarted()) {
+            mDiscoverySession.requestPrinterUpdated(printerId);
+        }
+    }
+
+    private final class MyPrinterDiscoverySession extends PrinterDiscoverySession {
+
+        @Override
+        public void onPrintersAdded(List<PrinterInfo> printers) {
+            if (DEBUG) {
+                Log.i(LOG_TAG, "MyPrinterDiscoverySession#onPrintersAdded()");
+            }
+            boolean printersAdded = false;
+            final int addedPrinterCount = printers.size();
+            for (int i = 0; i < addedPrinterCount; i++) {
+                PrinterInfo printer = printers.get(i);
+                if (!mPrinters.containsKey(printer.getId())) {
+                    mPrinters.put(printer.getId(), printer);
+                    printersAdded = true;
+                }
+            }
+            if (printersAdded) {
+                deliverResult(new ArrayList<PrinterInfo>(mPrinters.values()));
+            }
+        }
+
+        @Override
+        public void onPrintersRemoved(List<PrinterId> printerIds) {
+            if (DEBUG) {
+                Log.i(LOG_TAG, "MyPrinterDiscoverySession#onPrintersRemoved()");
+            }
+            boolean removedPrinters = false;
+            final int removedPrinterCount = printerIds.size();
+            for (int i = 0; i < removedPrinterCount; i++) {
+                PrinterId removedPrinterId = printerIds.get(i);
+                if (mPrinters.remove(removedPrinterId) != null) {
+                    removedPrinters = true;
+                }
+            }
+            if (removedPrinters) {
+                deliverResult(new ArrayList<PrinterInfo>(mPrinters.values()));
+            }
+        }
+
+        @Override
+        public void onPrintersUpdated(List<PrinterInfo> printers) {
+            if (DEBUG) {
+                Log.i(LOG_TAG, "MyPrinterDiscoverySession#onPrintersUpdated()");
+            }
+            boolean updatedPrinters = false;
+            final int updatedPrinterCount = printers.size();
+            for (int i = 0; i < updatedPrinterCount; i++) {
+                PrinterInfo updatedPrinter = printers.get(i);
+                if (mPrinters.containsKey(updatedPrinter.getId())) {
+                    mPrinters.put(updatedPrinter.getId(), updatedPrinter);
+                    updatedPrinters = true;
+                }
+            }
+            if (updatedPrinters) {
+                deliverResult(new ArrayList<PrinterInfo>(mPrinters.values()));
+            }
+        }
+    }
+
+    private final class PersistenceManager {
+        private static final String PERSIST_FILE_NAME = "printer_history.xml";
+
+        private static final String TAG_PRINTERS = "printers";
+
+        private static final String TAG_PRINTER = "printer";
+        private static final String TAG_PRINTER_ID = "printerId";
+
+        private static final String ATTR_LOCAL_ID = "localId";
+        private static final String ATTR_SERVICE_NAME = "serviceName";
+
+        private static final String ATTR_NAME = "name";
+        private static final String ATTR_DESCRIPTION = "description";
+        private static final String ATTR_STATUS = "status";
+
+        private final AtomicFile mStatePersistFile;
+
+        private List<PrinterInfo> mHistoricalPrinters;
+
+        private boolean mReadHistoryCompleted;
+        private boolean mReadHistoryInProgress;
+
+        private final AsyncTask<Void, Void, List<PrinterInfo>> mReadTask =
+                new AsyncTask<Void, Void, List<PrinterInfo>>() {
+            @Override
+            protected List<PrinterInfo> doInBackground(Void... args) {
+               return doReadPrinterHistory();
+            }
+
+            @Override
+            protected void onPostExecute(List<PrinterInfo> printers) {
+                if (DEBUG) {
+                    Log.i(LOG_TAG, "read history completed");
+                }
+
+                mHistoricalPrinters = printers;
+
+                // Compute the favorite printers.
+                mFavoritePrinters = computeFavoritePrinters(printers);
+
+                // We want the first few favorite printers on top of the list.
+                final int favoriteCount = Math.min(mFavoritePrinters.size(),
+                        MAX_HISTORICAL_PRINTER_COUNT);
+                for (int i = 0; i < favoriteCount; i++) {
+                    PrinterInfo favoritePrinter = mFavoritePrinters.get(i);
+                    mPrinters.put(favoritePrinter.getId(), favoritePrinter);
+                }
+
+                mReadHistoryInProgress = false;
+                mReadHistoryCompleted = true;
+
+                loadInternal();
+            }
+
+            private List<PrinterInfo> doReadPrinterHistory() {
+                FileInputStream in = null;
+                try {
+                    in = mStatePersistFile.openRead();
+                } catch (FileNotFoundException fnfe) {
+                    Log.i(LOG_TAG, "No existing printer history.");
+                    return new ArrayList<PrinterInfo>();
+                }
+                try {
+                    List<PrinterInfo> printers = new ArrayList<PrinterInfo>();
+                    XmlPullParser parser = Xml.newPullParser();
+                    parser.setInput(in, null);
+                    parseState(parser, printers);
+                    return printers;
+                } catch (IllegalStateException ise) {
+                    Slog.w(LOG_TAG, "Failed parsing ", ise);
+                } catch (NullPointerException npe) {
+                    Slog.w(LOG_TAG, "Failed parsing ", npe);
+                } catch (NumberFormatException nfe) {
+                    Slog.w(LOG_TAG, "Failed parsing ", nfe);
+                } catch (XmlPullParserException xppe) {
+                    Slog.w(LOG_TAG, "Failed parsing ", xppe);
+                } catch (IOException ioe) {
+                    Slog.w(LOG_TAG, "Failed parsing ", ioe);
+                } catch (IndexOutOfBoundsException iobe) {
+                    Slog.w(LOG_TAG, "Failed parsing ", iobe);
+                } finally {
+                    IoUtils.closeQuietly(in);
+                }
+
+                return Collections.emptyList();
+            }
+
+            private void parseState(XmlPullParser parser, List<PrinterInfo> outPrinters)
+                    throws IOException, XmlPullParserException {
+                parser.next();
+                skipEmptyTextTags(parser);
+                expect(parser, XmlPullParser.START_TAG, TAG_PRINTERS);
+                parser.next();
+
+                while (parsePrinter(parser, outPrinters)) {
+                    // Be nice and respond to cancellation
+                    if (isCancelled()) {
+                        return;
+                    }
+                    parser.next();
+                }
+
+                skipEmptyTextTags(parser);
+                expect(parser, XmlPullParser.END_TAG, TAG_PRINTERS);
+            }
+
+            private boolean parsePrinter(XmlPullParser parser, List<PrinterInfo> outPrinters)
+                    throws IOException, XmlPullParserException {
+                skipEmptyTextTags(parser);
+                if (!accept(parser, XmlPullParser.START_TAG, TAG_PRINTER)) {
+                    return false;
+                }
+
+                String name = parser.getAttributeValue(null, ATTR_NAME);
+                String description = parser.getAttributeValue(null, ATTR_DESCRIPTION);
+                final int status = Integer.parseInt(parser.getAttributeValue(null, ATTR_STATUS));
+
+                parser.next();
+
+                skipEmptyTextTags(parser);
+                expect(parser, XmlPullParser.START_TAG, TAG_PRINTER_ID);
+                String localId = parser.getAttributeValue(null, ATTR_LOCAL_ID);
+                ComponentName service = ComponentName.unflattenFromString(parser.getAttributeValue(
+                        null, ATTR_SERVICE_NAME));
+                PrinterId printerId =  new PrinterId(service, localId);
+                parser.next();
+                skipEmptyTextTags(parser);
+                expect(parser, XmlPullParser.END_TAG, TAG_PRINTER_ID);
+                parser.next();
+
+                PrinterInfo.Builder builder = new PrinterInfo.Builder(printerId, name, status);
+                builder.setDescription(description);
+                PrinterInfo printer = builder.create();
+
+                outPrinters.add(printer);
+
+                if (DEBUG) {
+                    Log.i(LOG_TAG, "[RESTORED] " + printer);
+                }
+
+                skipEmptyTextTags(parser);
+                expect(parser, XmlPullParser.END_TAG, TAG_PRINTER);
+
+                return true;
+            }
+
+            private void expect(XmlPullParser parser, int type, String tag)
+                    throws IOException, XmlPullParserException {
+                if (!accept(parser, type, tag)) {
+                    throw new XmlPullParserException("Exepected event: " + type
+                            + " and tag: " + tag + " but got event: " + parser.getEventType()
+                            + " and tag:" + parser.getName());
+                }
+            }
+
+            private void skipEmptyTextTags(XmlPullParser parser)
+                    throws IOException, XmlPullParserException {
+                while (accept(parser, XmlPullParser.TEXT, null)
+                        && "\n".equals(parser.getText())) {
+                    parser.next();
+                }
+            }
+
+            private boolean accept(XmlPullParser parser, int type, String tag)
+                    throws IOException, XmlPullParserException {
+                if (parser.getEventType() != type) {
+                    return false;
+                }
+                if (tag != null) {
+                    if (!tag.equals(parser.getName())) {
+                        return false;
+                    }
+                } else if (parser.getName() != null) {
+                    return false;
+                }
+                return true;
+            }
+        };
+
+        private final AsyncTask<List<PrinterInfo>, Void, Void> mWriteTask =
+                new AsyncTask<List<PrinterInfo>, Void, Void>() {
+            @Override
+            protected Void doInBackground(List<PrinterInfo>... printers) {
+                doWritePrinterHistory(printers[0]);
+                return null;
+            }
+
+            private void doWritePrinterHistory(List<PrinterInfo> printers) {
+                FileOutputStream out = null;
+                try {
+                    out = mStatePersistFile.startWrite();
+
+                    XmlSerializer serializer = new FastXmlSerializer();
+                    serializer.setOutput(out, "utf-8");
+                    serializer.startDocument(null, true);
+                    serializer.startTag(null, TAG_PRINTERS);
+
+                    final int printerCount = printers.size();
+                    for (int i = 0; i < printerCount; i++) {
+                        PrinterInfo printer = printers.get(i);
+
+                        serializer.startTag(null, TAG_PRINTER);
+
+                        serializer.attribute(null, ATTR_NAME, printer.getName());
+                        serializer.attribute(null, ATTR_STATUS, String.valueOf(
+                                printer.getStatus()));
+                        String description = printer.getDescription();
+                        if (description != null) {
+                            serializer.attribute(null, ATTR_DESCRIPTION, description);
+                        }
+
+                        PrinterId printerId = printer.getId();
+                        serializer.startTag(null, TAG_PRINTER_ID);
+                        serializer.attribute(null, ATTR_LOCAL_ID, printerId.getLocalId());
+                        serializer.attribute(null, ATTR_SERVICE_NAME, printerId.getServiceName()
+                                .flattenToString());
+                        serializer.endTag(null, TAG_PRINTER_ID);
+
+                        serializer.endTag(null, TAG_PRINTER);
+
+                        if (DEBUG) {
+                            Log.i(LOG_TAG, "[PERSISTED] " + printer);
+                        }
+                    }
+
+                    serializer.endTag(null, TAG_PRINTERS);
+                    serializer.endDocument();
+                    mStatePersistFile.finishWrite(out);
+
+                    if (DEBUG) {
+                        Log.i(LOG_TAG, "[PERSIST END]");
+                    }
+                } catch (IOException ioe) {
+                    Slog.w(LOG_TAG, "Failed to write printer history, restoring backup.", ioe);
+                    mStatePersistFile.failWrite(out);
+                } finally {
+                    IoUtils.closeQuietly(out);
+                }
+            }
+        };
+
+        private PersistenceManager(Context context) {
+            mStatePersistFile = new AtomicFile(new File(context.getFilesDir(),
+                    PERSIST_FILE_NAME));
+        }
+
+        public boolean isReadHistoryInProgress() {
+            return mReadHistoryInProgress;
+        }
+
+        public boolean isReadHistoryCompleted() {
+            return mReadHistoryCompleted;
+        }
+
+        public boolean stopReadPrinterHistory() {
+            return mReadTask.cancel(true);
+        }
+
+        public void readPrinterHistory() {
+            if (DEBUG) {
+                Log.i(LOG_TAG, "read history started");
+            }
+            mReadHistoryInProgress = true;
+            mReadTask.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR, (Void[]) null);
+        }
+
+        @SuppressWarnings("unchecked")
+        public void addPrinterAndWritePrinterHistory(PrinterInfo printer) {
+            if (mHistoricalPrinters.size() >= MAX_HISTORY_LENGTH) {
+                mHistoricalPrinters.remove(0);
+            }
+            mHistoricalPrinters.add(printer);
+            mWriteTask.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR, mHistoricalPrinters);
+        }
+
+        private List<PrinterInfo> computeFavoritePrinters(List<PrinterInfo> printers) {
+            Map<PrinterId, PrinterRecord> recordMap =
+                    new ArrayMap<PrinterId, PrinterRecord>();
+
+            // Recompute the weights.
+            float currentWeight = 1.0f;
+            final int printerCount = printers.size();
+            for (int i = printerCount - 1; i >= 0; i--) {
+                PrinterInfo printer = printers.get(i);
+                // Aggregate weight for the same printer
+                PrinterRecord record = recordMap.get(printer.getId());
+                if (record == null) {
+                    record = new PrinterRecord(printer);
+                    recordMap.put(printer.getId(), record);
+                }
+                record.weight += currentWeight;
+                currentWeight *= WEIGHT_DECAY_COEFFICIENT;
+            }
+
+            // Soft the favorite printers.
+            List<PrinterRecord> favoriteRecords = new ArrayList<PrinterRecord>(
+                    recordMap.values());
+            Collections.sort(favoriteRecords);
+
+            // Write the favorites to the output.
+            final int favoriteCount = favoriteRecords.size();
+            List<PrinterInfo> favoritePrinters = new ArrayList<PrinterInfo>(favoriteCount);
+            for (int i = 0; i < favoriteCount; i++) {
+                PrinterInfo printer = favoriteRecords.get(i).printer;
+                favoritePrinters.add(printer);
+            }
+
+            return favoritePrinters;
+        }
+
+        private final class PrinterRecord implements Comparable<PrinterRecord> {
+            public final PrinterInfo printer;
+            public float weight;
+
+            public PrinterRecord(PrinterInfo printer) {
+                this.printer = printer;
+            }
+
+            @Override
+            public int compareTo(PrinterRecord another) {
+                return Float.floatToIntBits(another.weight) - Float.floatToIntBits(weight);
+            }
+        }
+    }
+}
diff --git a/packages/PrintSpooler/src/com/android/printspooler/PrintJobConfigActivity.java b/packages/PrintSpooler/src/com/android/printspooler/PrintJobConfigActivity.java
index f8e9f43..d3dd8c9 100644
--- a/packages/PrintSpooler/src/com/android/printspooler/PrintJobConfigActivity.java
+++ b/packages/PrintSpooler/src/com/android/printspooler/PrintJobConfigActivity.java
@@ -18,13 +18,17 @@
 
 import android.app.Activity;
 import android.app.Dialog;
+import android.app.LoaderManager;
 import android.content.Context;
+import android.content.Intent;
+import android.content.Loader;
 import android.content.pm.PackageInfo;
-import android.content.pm.PackageManager;
 import android.content.pm.PackageManager.NameNotFoundException;
 import android.database.DataSetObserver;
 import android.graphics.Rect;
 import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.os.AsyncTask;
 import android.os.Build;
 import android.os.Bundle;
 import android.os.Handler;
@@ -39,9 +43,11 @@
 import android.print.PageRange;
 import android.print.PrintAttributes;
 import android.print.PrintAttributes.MediaSize;
+import android.print.PrintAttributes.Resolution;
 import android.print.PrintDocumentAdapter;
 import android.print.PrintDocumentInfo;
 import android.print.PrintJobInfo;
+import android.print.PrintManager;
 import android.print.PrinterCapabilitiesInfo;
 import android.print.PrinterId;
 import android.print.PrinterInfo;
@@ -69,6 +75,14 @@
 import android.widget.Spinner;
 import android.widget.TextView;
 
+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;
@@ -93,15 +107,32 @@
     public static final String EXTRA_PRINT_ATTRIBUTES = "printAttributes";
     public static final String EXTRA_PRINT_JOB_ID = "printJobId";
 
-    private static final int CONTROLLER_STATE_INITIALIZED = 1;
-    private static final int CONTROLLER_STATE_STARTED = 2;
-    private static final int CONTROLLER_STATE_LAYOUT_STARTED = 3;
-    private static final int CONTROLLER_STATE_LAYOUT_COMPLETED = 4;
-    private static final int CONTROLLER_STATE_WRITE_STARTED = 5;
-    private static final int CONTROLLER_STATE_WRITE_COMPLETED = 6;
-    private static final int CONTROLLER_STATE_FINISHED = 7;
-    private static final int CONTROLLER_STATE_FAILED = 8;
-    private static final int CONTROLLER_STATE_CANCELLED = 9;
+    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 DEST_ADAPTER_MIN_ITEM_COUNT = 2;
+    private static final int DEST_ADAPTER_MAX_ITEM_COUNT = 9;
+
+    private static final int DEST_ADAPTER_POSITION_SEARCHING_FOR_PRINTERS = 0;
+    private static final int DEST_ADAPTER_POSITION_SAVE_AS_PDF = 1;
+
+    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 DEST_ADAPTER_ITEM_ID_SEARCHING_FOR_PRINTERS = Integer.MAX_VALUE - 2;
+
+    private static final int ACTIVITY_REQUEST_CREATE_FILE = 1;
+    private static final int ACTIVITY_REQUEST_SELECT_PRINTER = 2;
+
+    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;
@@ -109,6 +140,7 @@
     private static final int EDITOR_STATE_CANCELLED = 4;
 
     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");
 
@@ -135,10 +167,6 @@
     private Document mDocument;
     private PrintController mController;
 
-    private AvailablePrinterProvider mAvailablePrinters;
-
-    private FavoritePrinterProvider mFavoritePrinters;
-
     private int mPrintJobId;
 
     private IBinder mIPrintDocumentAdapter;
@@ -148,9 +176,6 @@
     @Override
     protected void onCreate(Bundle bundle) {
         super.onCreate(bundle);
-        setContentView(R.layout.print_job_config_activity_container);
-
-        getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN);
 
         Bundle extras = getIntent().getExtras();
 
@@ -169,15 +194,17 @@
             mCurrPrintAttributes.copyFrom(attributes);
         }
 
-        // TODO: Use history
-        mAvailablePrinters = new AvailablePrinterProvider(this, null);
-        mFavoritePrinters = new FavoritePrinterProvider(this);
+        setContentView(R.layout.print_job_config_activity_container);
+
+        // TODO: This should be on the style
+        getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN);
+
 
         mEditor = new Editor();
         mDocument = new Document();
         mController = new PrintController(new RemotePrintDocumentAdapter(
                 IPrintDocumentAdapter.Stub.asInterface(mIPrintDocumentAdapter),
-                PrintSpooler.peekInstance().generateFileForPrintJob(mPrintJobId)));
+                PrintSpoolerService.peekInstance().generateFileForPrintJob(mPrintJobId)));
 
         try {
             mIPrintDocumentAdapter.linkToDeath(mDeathRecipient, 0);
@@ -191,26 +218,6 @@
     }
 
     @Override
-    protected void onResume() {
-        super.onResume();
-        // TODO: Polish this
-        if (!mEditor.isPrintConfirmed()) {
-            mAvailablePrinters.startLoadData();
-            mFavoritePrinters.startLoadData();
-        }
-    }
-
-    @Override
-    protected void onPause() {
-        // TODO: Polish this
-        if (!mEditor.isPrintConfirmed()) {
-            mAvailablePrinters.stopLoadData();
-            mFavoritePrinters.stopLoadData();
-        }
-        super.onPause();
-    }
-
-    @Override
     protected void onDestroy() {
         // We can safely do the work in here since at this point
         // the system is bound to our (spooler) process which
@@ -219,10 +226,10 @@
             mController.finish();
         }
         if (mEditor.isPrintConfirmed() && mController.isFinished()) {
-            PrintSpooler.peekInstance().setPrintJobState(mPrintJobId,
+            PrintSpoolerService.peekInstance().setPrintJobState(mPrintJobId,
                     PrintJobInfo.STATE_QUEUED, null);
         } else {
-            PrintSpooler.peekInstance().setPrintJobState(mPrintJobId,
+            PrintSpoolerService.peekInstance().setPrintJobState(mPrintJobId,
                     PrintJobInfo.STATE_CANCELED, null);
         }
         mIPrintDocumentAdapter.unlinkToDeath(mDeathRecipient, 0);
@@ -333,13 +340,13 @@
 
         public void update() {
             if (!printAttributesChanged()) {
-                // If the attributes changes, then we do not do a layout but may
+                // If the attributes changed, 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 {
-                PrintSpooler.peekInstance().setPrintJobAttributesNoPersistence(mPrintJobId,
-                        mCurrPrintAttributes);
+                PrintSpoolerService.peekInstance().setPrintJobAttributesNoPersistence(
+                        mPrintJobId, mCurrPrintAttributes);
 
                 mMetadata.putBoolean(PrintDocumentAdapter.METADATA_KEY_PRINT_PREVIEW,
                         !mEditor.isPrintConfirmed());
@@ -378,7 +385,7 @@
             final boolean infoChanged = !info.equals(mDocument.info);
             if (infoChanged) {
                 mDocument.info = info;
-                PrintSpooler.peekInstance().setPrintJobPrintDocumentInfoNoPersistence(
+                PrintSpoolerService.peekInstance().setPrintJobPrintDocumentInfoNoPersistence(
                         mPrintJobId, info);
             }
 
@@ -465,12 +472,12 @@
             if (Arrays.equals(mDocument.pages, mRequestedPages)) {
                 // We got a document with exactly the pages we wanted. Hence,
                 // the printer has to print all pages in the data.
-                PrintSpooler.peekInstance().setPrintJobPagesNoPersistence(mPrintJobId,
+                PrintSpoolerService.peekInstance().setPrintJobPagesNoPersistence(mPrintJobId,
                         ALL_PAGES_ARRAY);
             } else if (Arrays.equals(mDocument.pages, ALL_PAGES_ARRAY)) {
                 // We requested specific pages but got all of them. Hence,
                 // the printer has to print only the requested pages.
-                PrintSpooler.peekInstance().setPrintJobPagesNoPersistence(mPrintJobId,
+                PrintSpoolerService.peekInstance().setPrintJobPagesNoPersistence(mPrintJobId,
                         mRequestedPages);
             } else if (PageRangeUtils.contains(mDocument.pages, mRequestedPages)) {
                 // We requested specific pages and got more but not all pages.
@@ -480,7 +487,7 @@
                 final int offset = mDocument.pages[0].getStart() - pages[0].getStart();
                 PageRange[] offsetPages = Arrays.copyOf(mDocument.pages, mDocument.pages.length);
                 PageRangeUtils.offsetStart(offsetPages, offset);
-                PrintSpooler.peekInstance().setPrintJobPagesNoPersistence(mPrintJobId,
+                PrintSpoolerService.peekInstance().setPrintJobPagesNoPersistence(mPrintJobId,
                         offsetPages);
             } else if (Arrays.equals(mRequestedPages, ALL_PAGES_ARRAY)
                     && mDocument.pages.length == 1 && mDocument.pages[0].getStart() == 0
@@ -488,7 +495,7 @@
                 // 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.
-                PrintSpooler.peekInstance().setPrintJobPagesNoPersistence(mPrintJobId,
+                PrintSpoolerService.peekInstance().setPrintJobPagesNoPersistence(mPrintJobId,
                         mDocument.pages);
             } else {
                 // We did not get the pages we requested, then the application
@@ -500,7 +507,16 @@
             }
 
             if (mEditor.isDone()) {
-                PrintJobConfigActivity.this.finish();
+                if (mEditor.isPrintingToPdf()) {
+                    PrintJobInfo printJob = PrintSpoolerService.peekInstance()
+                            .getPrintJobInfo(mPrintJobId, PrintManager.APP_ID_ANY);
+                    Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
+                    intent.setType("application/pdf");
+                    intent.putExtra(Intent.EXTRA_TITLE, printJob.getLabel());
+                    startActivityForResult(intent, ACTIVITY_REQUEST_CREATE_FILE);
+                } else {
+                    PrintJobConfigActivity.this.finish();
+                }
             }
         }
 
@@ -607,33 +623,111 @@
         }
     }
 
+    @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.updateUi();
+                        }
+                    });
+                }
+            } break;
+
+            case ACTIVITY_REQUEST_SELECT_PRINTER: {
+                if (resultCode == RESULT_OK) {
+                    PrinterId printerId = (PrinterId) data.getParcelableExtra(
+                            INTENT_EXTRA_PRINTER_ID);
+                    // TODO: Make sure the selected printer is in the shown list.
+                    mEditor.selectPrinter(printerId);
+                }
+            } 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 = PrintSpoolerService.peekInstance()
+                            .getPrintJobInfo(mPrintJobId, PrintManager.APP_ID_ANY);
+                    if (printJob == null) {
+                        return null;
+                    }
+                    File file = PrintSpoolerService.peekInstance()
+                            .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 final EditText mCopiesEditText;
+        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 final TextView mRangeTitle;
-        private final EditText mRangeEditText;
+        private EditText mCopiesEditText;
 
-        private final Spinner mDestinationSpinner;
+        private TextView mRangeTitle;
+        private EditText mRangeEditText;
+
+        private Spinner mDestinationSpinner;
         private final DestinationAdapter mDestinationSpinnerAdapter;
 
-        private final Spinner mMediaSizeSpinner;
+        private Spinner mMediaSizeSpinner;
         private final ArrayAdapter<SpinnerItem<MediaSize>> mMediaSizeSpinnerAdapter;
 
-        private final Spinner mColorModeSpinner;
+        private Spinner mColorModeSpinner;
         private final ArrayAdapter<SpinnerItem<Integer>> mColorModeSpinnerAdapter;
 
-        private final Spinner mOrientationSpinner;
+        private Spinner mOrientationSpinner;
         private final  ArrayAdapter<SpinnerItem<Integer>> mOrientationSpinnerAdapter;
 
-        private final Spinner mRangeOptionsSpinner;
+        private Spinner mRangeOptionsSpinner;
         private final ArrayAdapter<SpinnerItem<Integer>> mRangeOptionsSpinnerAdapter;
 
         private final SimpleStringSplitter mStringCommaSplitter =
                 new SimpleStringSplitter(',');
 
-        private final View mContentContainer;
+        private View mContentContainer;
 
-        private final Button mPrintButton;
+        private Button mPrintButton;
 
         private final OnItemSelectedListener mOnItemSelectedListener =
                 new AdapterView.OnItemSelectedListener() {
@@ -644,18 +738,31 @@
                         mIgnoreNextDestinationChange = false;
                         return;
                     }
+                    if (id == DEST_ADAPTER_ITEM_ID_ALL_PRINTERS) {
+                        mIgnoreNextDestinationChange = true;
+                        mDestinationSpinner.setSelection(0);
+                        Intent intent = new Intent(PrintJobConfigActivity.this,
+                                SelectPrinterActivity.class);
+                        startActivityForResult(intent, ACTIVITY_REQUEST_SELECT_PRINTER);
+                        return;
+                    }
+                    mWaitingForPrinterCapabilities = false;
                     mCurrPrintAttributes.clear();
                     PrinterInfo printer = (PrinterInfo) mDestinationSpinnerAdapter
                             .getItem(position);
                     if (printer != null) {
-                        PrintSpooler.peekInstance().setPrintJobPrinterNoPersistence(
+                        PrintSpoolerService.peekInstance().setPrintJobPrinterNoPersistence(
                                 mPrintJobId, printer);
                         PrinterCapabilitiesInfo capabilities = printer.getCapabilities();
                         if (capabilities == null) {
                             List<PrinterId> printerIds = new ArrayList<PrinterId>();
                             printerIds.add(printer.getId());
-                            final int index = mAvailablePrinters.getItemIndex(printer);
-                            mAvailablePrinters.refreshItem(index);
+                            FusedPrintersProvider printersLoader = (FusedPrintersProvider)
+                                    (Loader<?>) getLoaderManager().getLoader(
+                                            LOADER_ID_PRINTERS_LOADER);
+                            if (printersLoader != null) {
+                                printersLoader.refreshPrinter(printer.getId());
+                            }
                             mWaitingForPrinterCapabilities = true;
                             //TODO: We need a timeout for the update.
                         } else {
@@ -668,6 +775,31 @@
                             }
                         }
                     }
+
+                    // The printer changed so we want to start with a clean slate
+                    // for the print options and let them be populated from the
+                    // printer capabilities and use the printer defaults.
+                    if (!mMediaSizeSpinnerAdapter.isEmpty()) {
+                        mIgnoreNextMediaSizeChange = true;
+                        mMediaSizeSpinnerAdapter.clear();
+                    }
+                    if (!mColorModeSpinnerAdapter.isEmpty()) {
+                        mIgnoreNextColorModeChange = true;
+                        mColorModeSpinnerAdapter.clear();
+                    }
+                    if (!mOrientationSpinnerAdapter.isEmpty()) {
+                        mIgnoreNextOrientationChange = true;
+                        mOrientationSpinnerAdapter.clear();
+                    }
+                    if (mRangeOptionsSpinner.getSelectedItemPosition() != 0) {
+                        mIgnoreNextRangeOptionChange = true;
+                        mRangeOptionsSpinner.setSelection(0);
+                    }
+                    if (!TextUtils.isEmpty(mCopiesEditText.getText())) {
+                        mIgnoreNextCopiesChange = true;
+                        mCopiesEditText.setText(MIN_COPIES_STRING);
+                    }
+
                     updateUi();
                 } else if (spinner == mMediaSizeSpinner) {
                     if (mIgnoreNextMediaSizeChange) {
@@ -753,7 +885,8 @@
                 }
 
                 mCopiesEditText.setError(null);
-                PrintSpooler.peekInstance().setPrintJobCopiesNoPersistence(mPrintJobId, copies);
+                PrintSpoolerService.peekInstance().setPrintJobCopiesNoPersistence(
+                        mPrintJobId, copies);
                 updateUi();
 
                 if (hadErrors && !hasErrors() && printAttributesChanged()) {
@@ -832,22 +965,25 @@
 
         private boolean mWaitingForPrinterCapabilities;
 
+        private int mCurrentUi = UI_NONE;
+
         public Editor() {
-            // Content container
-            mContentContainer = findViewById(R.id.content_container);
-
-            // Copies
-            mCopiesEditText = (EditText) findViewById(R.id.copies_edittext);
-            mCopiesEditText.setText(String.valueOf(MIN_COPIES));
-            PrintSpooler.peekInstance().setPrintJobCopiesNoPersistence(mPrintJobId, MIN_COPIES);
-            mCopiesEditText.addTextChangedListener(mCopiesTextWatcher);
-            mCopiesEditText.selectAll();
-
             // Destination.
-            mDestinationSpinnerAdapter = new DestinationAdapter(mAvailablePrinters);
+            mDestinationSpinnerAdapter = new DestinationAdapter();
             mDestinationSpinnerAdapter.registerDataSetObserver(new DataSetObserver() {
                 @Override
                 public void onChanged() {
+                    final int selectedPosition = mDestinationSpinner.getSelectedItemPosition();
+                    if (mDestinationSpinnerAdapter.getCount() > 0) {
+                        // Make sure we select the first printer if we have data.
+                        if (selectedPosition == AdapterView.INVALID_POSITION) {
+                            mDestinationSpinner.setSelection(0);
+                        }
+                    } else {
+                        // Make sure we select no printer if we have no data.
+                        mDestinationSpinner.setSelection(AdapterView.INVALID_POSITION);
+                    }
+
                     // Maybe we did not have capabilities when the current printer was
                     // selected, but now the selected printer has capabilities. Generate
                     // a fake selection so the code in the selection change handling takes
@@ -855,13 +991,9 @@
                     if (mWaitingForPrinterCapabilities) {
                         mWaitingForPrinterCapabilities = false;
                         PrinterInfo printer = (PrinterInfo) mDestinationSpinner.getSelectedItem();
-                        if (printer != null) {
-                            if (printer.getCapabilities() != null) {
-                                final int selectedPosition =
-                                        mDestinationSpinner.getSelectedItemPosition();
-                                mOnItemSelectedListener.onItemSelected(mDestinationSpinner, null,
-                                        selectedPosition, selectedPosition);
-                            }
+                        if (printer != null && printer.getCapabilities() != null) {
+                            mOnItemSelectedListener.onItemSelected(mDestinationSpinner, null,
+                                    selectedPosition, selectedPosition);
                         }
                     }
                     updateUi();
@@ -872,41 +1004,23 @@
                     updateUi();
                 }
             });
-            mDestinationSpinner = (Spinner) findViewById(R.id.destination_spinner);
-            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
-            mRangeTitle = (TextView) findViewById(R.id.page_range_title);
-            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);
@@ -919,26 +1033,28 @@
                 mRangeOptionsSpinnerAdapter.add(new SpinnerItem<Integer>(
                         rangeOptionsValues[i], rangeOptionsLabels[i]));
             }
-            mRangeOptionsSpinner.setAdapter(mRangeOptionsSpinnerAdapter);
-            if (mRangeOptionsSpinner.getSelectedItemPosition() != 0) {
-                mIgnoreNextRangeOptionChange = true;
-                mRangeOptionsSpinner.setSelection(0);
-            }
 
-            // Print button
-            mPrintButton = (Button) findViewById(R.id.print_button);
-            mPrintButton.setOnClickListener(new OnClickListener() {
-                @Override
-                public void onClick(View v) {
-                    mEditor.confirmPrint();
-                    mController.update();
-                    showGeneratingPrintJobUi();
-                }
-            });
-
+            showUi(UI_EDITING_PRINT_JOB, null);
+            bindUi();
             updateUi();
         }
 
+        public void selectPrinter(PrinterId printerId) {
+            final int printerCount = mDestinationSpinnerAdapter.getCount();
+            for (int i = 0; i < printerCount; i++) {
+                PrinterInfo printer = (PrinterInfo) mDestinationSpinnerAdapter.getItem(i);
+                if (printer.getId().equals(printerId)) {
+                    mDestinationSpinner.setSelection(i);
+                    return;
+                }
+            }
+        }
+
+        public boolean isPrintingToPdf() {
+            return mDestinationSpinner.getSelectedItem()
+                    == mDestinationSpinnerAdapter.mFakePdfPrinter;
+        }
+
         public boolean shouldCloseOnTouch(MotionEvent event) {
             if (event.getAction() != MotionEvent.ACTION_DOWN) {
                 return false;
@@ -966,19 +1082,104 @@
         }
 
         public boolean isShwoingGeneratingPrintJobUi() {
-            return (findViewById(R.id.content_generating) != null);
+            return (mCurrentUi == UI_GENERATING_PRINT_JOB);
         }
 
-        private void showGeneratingPrintJobUi() {
-            // Find everything we will shuffle around.
-            final ViewGroup contentContainer = (ViewGroup) findViewById(R.id.content_container);
-            final View contentEditing = contentContainer.findViewById(R.id.content_editing);
-            final View contentGenerating = getLayoutInflater().inflate(
-                    R.layout.print_job_config_activity_content_generating,
-                    contentContainer, false);
+        public void showUi(int ui, final Runnable postSwitchCallback) {
+            if (ui == UI_NONE) {
+                throw new IllegalStateException("cannot remove the ui");
+            }
 
-            // Wire the cancel action.
-            Button cancelButton = (Button) contentGenerating.findViewById(R.id.cancel_button);
+            if (mCurrentUi == ui) {
+                return;
+            }
+
+            switch (mCurrentUi) {
+                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();
+                                    }
+                                }
+                            });
+                        } 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();
+                                    }
+                                }
+                            });
+                        } break;
+                    }
+                } break;
+            }
+
+            mCurrentUi = ui;
+        }
+
+        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)) {
+                            FusedPrintersProvider printersLoader = (FusedPrintersProvider)
+                                    (Loader<?>) getLoaderManager().getLoader(
+                                            LOADER_ID_PRINTERS_LOADER);
+                            if (printersLoader != null) {
+                                printersLoader.addHistoricalPrinter(printer);
+                            }
+                        }
+                    } 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) {
@@ -988,24 +1189,38 @@
                     mEditor.cancel();
                 }
             });
+        }
+
+        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 postAnimateCommand) {
+            // 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.
-            contentEditing.animate().alpha(0.0f).withLayer().withEndAction(new Runnable() {
+            hidingView.animate().alpha(0.0f).withLayer().withEndAction(new Runnable() {
                 @Override
                 public void run() {
-                    contentEditing.setVisibility(View.INVISIBLE);
+                    hidingView.setVisibility(View.INVISIBLE);
 
                     // Prepare the new content with correct size and alpha.
-                    contentGenerating.setMinimumWidth(contentContainer.getWidth());
-                    contentGenerating.setAlpha(0.0f);
+                    showingView.setMinimumWidth(contentContainer.getWidth());
+                    showingView.setAlpha(0.0f);
 
-                    // Compute how to much shrink the container to fit around the new content.
+                    // Compute how to much shrink /stretch the content.
                     final int widthSpec = MeasureSpec.makeMeasureSpec(
-                            contentContainer.getWidth(), MeasureSpec.AT_MOST);
+                            contentContainer.getWidth(), MeasureSpec.UNSPECIFIED);
                     final int heightSpec = MeasureSpec.makeMeasureSpec(
-                            contentContainer.getHeight(), MeasureSpec.AT_MOST);
-                    contentGenerating.measure(widthSpec, heightSpec);
-                    final float scaleY = (float) contentGenerating.getMeasuredHeight()
+                            contentContainer.getHeight(), MeasureSpec.UNSPECIFIED);
+                    showingView.measure(widthSpec, heightSpec);
+                    final float scaleY = (float) showingView.getMeasuredHeight()
                             / (float) contentContainer.getHeight();
 
                     // Second animation - resize the container.
@@ -1016,10 +1231,16 @@
                             // Swap the old and the new content.
                             contentContainer.removeAllViews();
                             contentContainer.setScaleY(1.0f);
-                            contentContainer.addView(contentGenerating);
+                            contentContainer.addView(showingView);
 
                             // Third animation - show the new content.
-                            contentGenerating.animate().withLayer().alpha(1.0f);
+                            showingView.animate().withLayer().alpha(1.0f).withEndAction(
+                                    new Runnable() {
+                                @Override
+                                public void run() {
+                                    postAnimateCommand.run();
+                                }
+                            });
                         }
                     });
                 }
@@ -1028,10 +1249,6 @@
 
         public void initialize() {
             mEditorState = EDITOR_STATE_INITIALIZED;
-            if (mDestinationSpinner.getSelectedItemPosition() != AdapterView.INVALID_POSITION) {
-                mIgnoreNextDestinationChange = true;
-                mDestinationSpinner.setSelection(AdapterView.INVALID_POSITION);
-            }
         }
 
         public boolean isCancelled() {
@@ -1054,11 +1271,7 @@
 
         public void confirmPrint() {
             mEditorState = EDITOR_STATE_CONFIRMED_PRINT;
-            PrinterInfo printer = (PrinterInfo) mDestinationSpinner.getSelectedItem();
-            if (printer != null) {
-                mFavoritePrinters.addPrinter(printer);
-            }
-            updateUi();
+            showUi(UI_GENERATING_PRINT_JOB, null);
         }
 
         public boolean isPreviewConfirmed() {
@@ -1104,7 +1317,79 @@
             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.setText(MIN_COPIES_STRING);
+            mCopiesEditText.addTextChangedListener(mCopiesTextWatcher);
+            mCopiesEditText.selectAll();
+            if (!TextUtils.equals(mCopiesEditText.getText(), MIN_COPIES_STRING)) {
+                mIgnoreNextCopiesChange = true;
+            }
+            PrintSpoolerService.peekInstance().setPrintJobCopiesNoPersistence(
+                    mPrintJobId, MIN_COPIES);
+
+            // Destination.
+            mDestinationSpinner = (Spinner) findViewById(R.id.destination_spinner);
+            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) {
+                mIgnoreNextMediaSizeChange = true;
+            }
+
+            // Color mode.
+            mColorModeSpinner = (Spinner) findViewById(R.id.color_spinner);
+            mColorModeSpinner.setAdapter(mColorModeSpinnerAdapter);
+            mColorModeSpinner.setOnItemSelectedListener(mOnItemSelectedListener);
+            if (mColorModeSpinnerAdapter.getCount() > 0) {
+                mIgnoreNextColorModeChange = true;
+            }
+
+            // Orientation
+            mOrientationSpinner = (Spinner) findViewById(R.id.orientation_spinner);
+            mOrientationSpinner.setAdapter(mOrientationSpinnerAdapter);
+            mOrientationSpinner.setOnItemSelectedListener(mOnItemSelectedListener);
+            if (mOrientationSpinnerAdapter.getCount() > 0) {
+                mIgnoreNextOrientationChange = true;
+            }
+
+            // Range
+            mRangeTitle = (TextView) findViewById(R.id.page_range_title);
+            mRangeEditText = (EditText) findViewById(R.id.page_range_edittext);
+            mRangeEditText.addTextChangedListener(mRangeTextWatcher);
+
+            // Range options
+            mRangeOptionsSpinner = (Spinner) findViewById(R.id.range_options_spinner);
+            mRangeOptionsSpinner.setAdapter(mRangeOptionsSpinnerAdapter);
+            mRangeOptionsSpinner.setOnItemSelectedListener(mOnItemSelectedListener);
+            if (mRangeOptionsSpinnerAdapter.getCount() > 0) {
+                mIgnoreNextRangeOptionChange = true;
+            }
+
+            // Print button
+            mPrintButton = (Button) findViewById(R.id.print_button);
+            registerPrintButtonClickListener();
+        }
+
         public void updateUi() {
+            if (mCurrentUi != UI_EDITING_PRINT_JOB) {
+                return;
+            }
             if (isPrintConfirmed() || isPreviewConfirmed() || isCancelled()) {
                 mDestinationSpinner.setEnabled(false);
                 mCopiesEditText.setEnabled(false);
@@ -1119,14 +1404,20 @@
                 return;
             }
 
+            // 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) {
+                        allOptionsEnabled = true;
+                    }
+                }
+            }
 
-            if (selectedIndex < 0 || ((PrinterInfo) mDestinationSpinnerAdapter.getItem(
-                    selectedIndex)).getCapabilities() == null) {
-
-                // Destination
-                mDestinationSpinner.setEnabled(false);
-
+            if (!allOptionsEnabled) {
                 String minCopiesString = String.valueOf(MIN_COPIES);
                 if (!TextUtils.equals(mCopiesEditText.getText(), minCopiesString)) {
                     mIgnoreNextCopiesChange = true;
@@ -1183,9 +1474,6 @@
                 PrinterCapabilitiesInfo capabilities = printer.getCapabilities();
                 printer.getCapabilities().getDefaults(defaultAttributes);
 
-                // Destination
-                mDestinationSpinner.setEnabled(true);
-
                 // Copies
                 mCopiesEditText.setEnabled(true);
 
@@ -1223,6 +1511,7 @@
                         }
                     }
                 }
+                mMediaSizeSpinner.setEnabled(true);
 
                 // Color mode.
                 final int colorModes = capabilities.getColorModes();
@@ -1271,6 +1560,7 @@
                         }
                     }
                 }
+                mColorModeSpinner.setEnabled(true);
 
                 // Orientation.
                 final int orientations = capabilities.getOrientations();
@@ -1321,20 +1611,25 @@
                         }
                     }
                 }
+                mOrientationSpinner.setEnabled(true);
 
                 // Range options
                 PrintDocumentInfo info = mDocument.info;
                 if (info != null && (info.getPageCount() > 1
                         || info.getPageCount() == PrintDocumentInfo.PAGE_COUNT_UNKNOWN)) {
                     mRangeOptionsSpinner.setEnabled(true);
-                    if (mRangeOptionsSpinner.getSelectedItemPosition() > 0
-                            && !mRangeEditText.isEnabled()) {
-                        mRangeEditText.setEnabled(true);
-                        mRangeEditText.setVisibility(View.VISIBLE);
-                        mRangeEditText.requestFocus();
-                        InputMethodManager imm = (InputMethodManager)
-                                getSystemService(INPUT_METHOD_SERVICE);
-                        imm.showSoftInput(mRangeEditText, 0);
+                    if (mRangeOptionsSpinner.getSelectedItemPosition() > 0) {
+                        if (!mRangeEditText.isEnabled()) {
+                            mRangeEditText.setEnabled(true);
+                            mRangeEditText.setVisibility(View.VISIBLE);
+                            mRangeEditText.requestFocus();
+                            InputMethodManager imm = (InputMethodManager)
+                                    getSystemService(INPUT_METHOD_SERVICE);
+                            imm.showSoftInput(mRangeEditText, 0);
+                        }
+                    } else {
+                        mRangeEditText.setEnabled(false);
+                        mRangeEditText.setVisibility(View.INVISIBLE);
                     }
                     final int pageCount = mDocument.info.getPageCount();
                     mRangeTitle.setText(getString(R.string.label_pages,
@@ -1352,6 +1647,7 @@
                     mRangeEditText.setEnabled(false);
                     mRangeEditText.setVisibility(View.INVISIBLE);
                 }
+                mRangeOptionsSpinner.setEnabled(true);
 
                 // Print/Print preview
                 if ((mRangeOptionsSpinner.getSelectedItemPosition() == 1
@@ -1378,6 +1674,7 @@
                     mCopiesEditText.selectAll();
                     mCopiesEditText.requestFocus();
                 }
+                mCopiesEditText.setEnabled(true);
             }
         }
 
@@ -1407,38 +1704,51 @@
             }
         }
 
-        private final class DestinationAdapter extends BaseAdapter {
-            private final AvailablePrinterProvider mProvider;
+        private final class DestinationAdapter extends BaseAdapter
+                implements LoaderManager.LoaderCallbacks<List<PrinterInfo>>{
+            private final List<PrinterInfo> mPrinters = new ArrayList<PrinterInfo>();
 
-            private final DataSetObserver mObserver = new DataSetObserver() {
-                @Override
-                public void onChanged() {
-                    notifyDataSetChanged();
-                }
+            public final PrinterInfo mFakePdfPrinter;
 
-                @Override
-                public void onInvalidated() {
-                    notifyDataSetInvalidated();
-                }
-            };
-
-            public DestinationAdapter(AvailablePrinterProvider provider) {
-                mProvider = provider;
-                mProvider.registerObserver(mObserver);
+            public DestinationAdapter() {
+                getLoaderManager().initLoader(LOADER_ID_PRINTERS_LOADER, null, this);
+                mFakePdfPrinter = createFakePdfPrinter();
             }
 
             @Override
             public int getCount() {
-                return mProvider.getItemCount();
+                return Math.max(Math.min(mPrinters.size(), DEST_ADAPTER_MAX_ITEM_COUNT),
+                        DEST_ADAPTER_MIN_ITEM_COUNT);
             }
 
             @Override
             public Object getItem(int position) {
-                return mProvider.getItemAt(position);
+                if (position == DEST_ADAPTER_POSITION_SAVE_AS_PDF) {
+                    return mFakePdfPrinter;
+                }
+                if (!mPrinters.isEmpty()) {
+                    if (position < DEST_ADAPTER_POSITION_SAVE_AS_PDF) {
+                        return mPrinters.get(position);
+                    } else if (position > DEST_ADAPTER_POSITION_SAVE_AS_PDF
+                            && position < getCount() - 1) {
+                        return mPrinters.get(position - 1);
+                    }
+                }
+                return null;
             }
 
             @Override
             public long getItemId(int position) {
+                if (position == DEST_ADAPTER_POSITION_SAVE_AS_PDF) {
+                    return DEST_ADAPTER_ITEM_ID_SAVE_AS_PDF;
+                }
+                if (mPrinters.isEmpty()) {
+                    if (position == DEST_ADAPTER_POSITION_SEARCHING_FOR_PRINTERS) {
+                        return DEST_ADAPTER_ITEM_ID_SEARCHING_FOR_PRINTERS;
+                    }
+                } else if (position == getCount() - 1) {
+                    return DEST_ADAPTER_ITEM_ID_ALL_PRINTERS;
+                }
                 return position;
             }
 
@@ -1455,24 +1765,92 @@
                             R.layout.spinner_dropdown_item, parent, false);
                 }
 
-                PrinterInfo printerInfo = mProvider.getItemAt(position);
-                TextView title = (TextView) convertView.findViewById(R.id.title);
-                title.setText(printerInfo.getName());
+                CharSequence title = null;
+                CharSequence subtitle = null;
 
-                try {
-                    TextView subtitle = (TextView)
-                            convertView.findViewById(R.id.subtitle);
-                    PackageManager pm = getPackageManager();
-                    PackageInfo packageInfo = pm.getPackageInfo(
-                            printerInfo.getId().getServiceName().getPackageName(), 0);
-                    subtitle.setText(packageInfo.applicationInfo.loadLabel(pm));
-                    subtitle.setVisibility(View.VISIBLE);
-                } catch (NameNotFoundException nnfe) {
-                    /* ignore */
+                if (mPrinters.isEmpty()
+                        && position == DEST_ADAPTER_POSITION_SEARCHING_FOR_PRINTERS) {
+                        title = getString(R.string.searching_for_printers);
+                } else {
+                    if (position == DEST_ADAPTER_POSITION_SAVE_AS_PDF) {
+                        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());
+                        } 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);
                 }
 
                 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) {
+                mPrinters.clear();
+                mPrinters.addAll(printers);
+                notifyDataSetChanged();
+            }
+
+            @Override
+            public void onLoaderReset(Loader<List<PrinterInfo>> loader) {
+                mPrinters.clear();
+                notifyDataSetInvalidated();
+            }
+
+            private PrinterInfo createFakePdfPrinter() {
+                PrinterId printerId = new PrinterId(getComponentName(), "PDF printer");
+
+                PrinterCapabilitiesInfo capabilities =
+                        new PrinterCapabilitiesInfo.Builder(printerId)
+                    .addMediaSize(MediaSize.createMediaSize(getPackageManager(),
+                            MediaSize.ISO_A4), true)
+                    .addMediaSize(MediaSize.createMediaSize(getPackageManager(),
+                            MediaSize.NA_LETTER), false)
+                    .addResolution(new Resolution("PDF resolution", "PDF resolution",
+                            300, 300), true)
+                    .setColorModes(PrintAttributes.COLOR_MODE_COLOR
+                            | PrintAttributes.COLOR_MODE_MONOCHROME,
+                            PrintAttributes.COLOR_MODE_COLOR)
+                    .setOrientations(PrintAttributes.ORIENTATION_PORTRAIT
+                            | PrintAttributes.ORIENTATION_LANDSCAPE,
+                            PrintAttributes.ORIENTATION_PORTRAIT)
+                    .create();
+
+                return new PrinterInfo.Builder(printerId, getString(R.string.save_as_pdf),
+                        PrinterInfo.STATUS_READY)
+                    .setCapabilities(capabilities)
+                    .create();
+            }
         }
     }
 
diff --git a/packages/PrintSpooler/src/com/android/printspooler/PrintSpooler.java b/packages/PrintSpooler/src/com/android/printspooler/PrintSpooler.java
deleted file mode 100644
index c2cf65e..0000000
--- a/packages/PrintSpooler/src/com/android/printspooler/PrintSpooler.java
+++ /dev/null
@@ -1,969 +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.ComponentName;
-import android.content.Context;
-import android.os.AsyncTask;
-import android.os.ParcelFileDescriptor;
-import android.print.IPrintClient;
-import android.print.IPrinterDiscoverySessionObserver;
-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.PrintAttributes.Tray;
-import android.print.PrintDocumentInfo;
-import android.print.PrintJobInfo;
-import android.print.PrintManager;
-import android.print.PrinterId;
-import android.print.PrinterInfo;
-import android.util.AtomicFile;
-import android.util.Log;
-import android.util.Slog;
-import android.util.Xml;
-
-import com.android.internal.util.FastXmlSerializer;
-
-import libcore.io.IoUtils;
-
-import org.xmlpull.v1.XmlPullParser;
-import org.xmlpull.v1.XmlPullParserException;
-import org.xmlpull.v1.XmlSerializer;
-
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileNotFoundException;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.List;
-
-public class PrintSpooler {
-
-    private static final String LOG_TAG = "PrintSpooler";
-
-    private static final boolean DEBUG_PRINT_JOB_LIFECYCLE = true;
-
-    private static final boolean DEBUG_PERSISTENCE = true;
-
-    private static final boolean PERSISTNECE_MANAGER_ENABLED = true;
-
-    private static final String PRINT_FILE_EXTENSION = "pdf";
-
-    private static int sPrintJobIdCounter;
-
-    private static final Object sLock = new Object();
-
-    private static PrintSpooler sInstance;
-
-    private final Object mLock = new Object();
-
-    private final List<PrintJobInfo> mPrintJobs = new ArrayList<PrintJobInfo>();
-
-    private final PersistenceManager mPersistanceManager;
-
-    private final NotificationController mNotificationController;
-
-    private final PrintSpoolerService mService;
-
-    public static void destroyInstance() {
-        synchronized (sLock) {
-            sInstance = null;
-        }
-    }
-
-    public static void createInstance(PrintSpoolerService service) {
-        synchronized (sLock) {
-            sInstance = new PrintSpooler(service);
-        }
-    }
-
-    public static PrintSpooler peekInstance() {
-        synchronized (sLock) {
-            return sInstance;
-        }
-    }
-
-    private PrintSpooler(PrintSpoolerService service) {
-        mService = service;
-        mPersistanceManager = new PersistenceManager(service);
-        mNotificationController = new NotificationController(service);
-        synchronized (mLock) {
-            mPersistanceManager.readStateLocked();
-            handleReadPrintJobsLocked();
-        }
-    }
-
-    public List<PrintJobInfo> getPrintJobInfos(ComponentName componentName,
-            int state, int appId) {
-        List<PrintJobInfo> foundPrintJobs = null;
-        synchronized (mLock) {
-            final int printJobCount = mPrintJobs.size();
-            for (int i = 0; i < printJobCount; i++) {
-                PrintJobInfo printJob = mPrintJobs.get(i);
-                PrinterId printerId = printJob.getPrinterId();
-                final boolean sameComponent = (componentName == null
-                        || (printerId != null
-                        && componentName.equals(printerId.getServiceName())));
-                final boolean sameAppId = appId == PrintManager.APP_ID_ANY
-                        || printJob.getAppId() == appId;
-                final boolean sameState = (state == printJob.getState())
-                        || (state == PrintJobInfo.STATE_ANY)
-                        || (state == PrintJobInfo.STATE_ANY_VISIBLE_TO_CLIENTS
-                                && printJob.getState() > PrintJobInfo.STATE_CREATED);
-                if (sameComponent && sameAppId && sameState) {
-                    if (foundPrintJobs == null) {
-                        foundPrintJobs = new ArrayList<PrintJobInfo>();
-                    }
-                    foundPrintJobs.add(printJob);
-                }
-            }
-        }
-        return foundPrintJobs;
-    }
-
-    public PrintJobInfo getPrintJobInfo(int printJobId, int appId) {
-        synchronized (mLock) {
-            final int printJobCount = mPrintJobs.size();
-            for (int i = 0; i < printJobCount; i++) {
-                PrintJobInfo printJob = mPrintJobs.get(i);
-                if (printJob.getId() == printJobId
-                        && (appId == PrintManager.APP_ID_ANY
-                                || appId == printJob.getAppId())) {
-                    return printJob;
-                }
-             }
-             return null;
-        }
-    }
-
-    public PrintJobInfo createPrintJob(CharSequence label, IPrintClient client,
-            PrintAttributes attributes, int appId) {
-        synchronized (mLock) {
-            final int printJobId = generatePrintJobIdLocked();
-            PrintJobInfo printJob = new PrintJobInfo();
-            printJob.setId(printJobId);
-            printJob.setAppId(appId);
-            printJob.setLabel(label);
-            printJob.setAttributes(attributes);
-            printJob.setState(PrintJobInfo.STATE_CREATED);
-
-            addPrintJobLocked(printJob);
-
-            return printJob;
-        }
-    }
-
-    private void handleReadPrintJobsLocked() {
-        final int printJobCount = mPrintJobs.size();
-        for (int i = 0; i < printJobCount; i++) {
-            PrintJobInfo printJob = mPrintJobs.get(i);
-
-            // Update the notification.
-            mNotificationController.onPrintJobStateChanged(printJob);
-
-            switch (printJob.getState()) {
-                case PrintJobInfo.STATE_QUEUED:
-                case PrintJobInfo.STATE_STARTED: {
-                    // We have a print job that was queued or started in the past
-                    // but the device battery died or a crash occurred. In this case
-                    // we assume the print job failed and let the user decide whether
-                    // to restart the job or just
-                    setPrintJobState(printJob.getId(), PrintJobInfo.STATE_FAILED,
-                            mService.getString(R.string.no_connection_to_printer));
-                } break;
-            }
-        }
-    }
-
-    public void checkAllPrintJobsHandled() {
-        synchronized (mLock) {
-            if (!hasActivePrintJobsLocked()) {
-                notifyOnAllPrintJobsHandled();
-            }
-        }
-    }
-
-    public void createPrinterDiscoverySession(IPrinterDiscoverySessionObserver observer) {
-        mService.createPrinterDiscoverySession(observer);
-    }
-
-    private int generatePrintJobIdLocked() {
-        int printJobId = sPrintJobIdCounter++;
-        while (isDuplicatePrintJobId(printJobId)) {
-            printJobId = sPrintJobIdCounter++;
-        }
-        return printJobId;
-    }
-
-    private boolean isDuplicatePrintJobId(int printJobId) {
-        final int printJobCount = mPrintJobs.size();
-        for (int j = 0; j < printJobCount; j++) {
-            PrintJobInfo printJob = mPrintJobs.get(j);
-            if (printJob.getId() == printJobId) {
-                return true;
-            }
-        }
-        return false;
-    }
-
-    public void writePrintJobData(final ParcelFileDescriptor fd, final int printJobId) {
-        final PrintJobInfo printJob;
-        synchronized (mLock) {
-            printJob = getPrintJobInfo(printJobId, PrintManager.APP_ID_ANY);
-        }
-        new AsyncTask<Void, Void, Void>() {
-            @Override
-            protected Void doInBackground(Void... params) {
-                FileInputStream in = null;
-                FileOutputStream out = null;
-                try {
-                    if (printJob != null) {
-                        File file = generateFileForPrintJob(printJobId);
-                        in = new FileInputStream(file);
-                        out = new FileOutputStream(fd.getFileDescriptor());
-                    }
-                    final byte[] buffer = new byte[8192];
-                    while (true) {
-                        final int readByteCount = in.read(buffer);
-                        if (readByteCount < 0) {
-                            return null;
-                        }
-                        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);
-                    IoUtils.closeQuietly(fd);
-                }
-                Log.i(LOG_TAG, "[END WRITE]");
-                return null;
-            }
-        }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void[]) null);
-    }
-
-    public File generateFileForPrintJob(int printJobId) {
-        return new File(mService.getFilesDir(), "print_job_"
-                + printJobId + "." + PRINT_FILE_EXTENSION);
-    }
-
-    private void addPrintJobLocked(PrintJobInfo printJob) {
-        mPrintJobs.add(printJob);
-        if (DEBUG_PRINT_JOB_LIFECYCLE) {
-            Slog.i(LOG_TAG, "[ADD] " + printJob);
-        }
-    }
-
-    private void removePrintJobLocked(PrintJobInfo printJob) {
-        if (mPrintJobs.remove(printJob)) {
-            generateFileForPrintJob(printJob.getId()).delete();
-            if (DEBUG_PRINT_JOB_LIFECYCLE) {
-                Slog.i(LOG_TAG, "[REMOVE] " + printJob);
-            }
-        }
-    }
-
-    public boolean setPrintJobState(int printJobId, int state, CharSequence error) {
-        boolean success = false;
-
-        synchronized (mLock) {
-            PrintJobInfo printJob = getPrintJobInfo(printJobId, PrintManager.APP_ID_ANY);
-            if (printJob != null) {
-                success = true;
-
-                printJob.setState(state);
-                printJob.setFailureReason(error);
-                mNotificationController.onPrintJobStateChanged(printJob);
-
-                if (DEBUG_PRINT_JOB_LIFECYCLE) {
-                    Slog.i(LOG_TAG, "[STATE CHANGED] " + printJob);
-                }
-
-                switch (state) {
-                    case PrintJobInfo.STATE_COMPLETED:
-                    case PrintJobInfo.STATE_CANCELED:
-                        removePrintJobLocked(printJob);
-                        // $fall-through$
-                    case PrintJobInfo.STATE_FAILED: {
-                        PrinterId printerId = printJob.getPrinterId();
-                        if (printerId != null) {
-                            ComponentName service = printerId.getServiceName();
-                            if (!hasActivePrintJobsForServiceLocked(service)) {
-                                mService.onAllPrintJobsForServiceHandled(service);
-                            }
-                        }
-                    } break;
-
-                    case PrintJobInfo.STATE_QUEUED: {
-                        mService.onPrintJobQueued(new PrintJobInfo(printJob));
-                    } break;
-                }
-
-                if (shouldPersistPrintJob(printJob)) {
-                    mPersistanceManager.writeStateLocked();
-                }
-
-                if (!hasActivePrintJobsLocked()) {
-                    notifyOnAllPrintJobsHandled();
-                }
-            }
-        }
-
-        return success;
-    }
-
-    public boolean hasActivePrintJobsLocked() {
-        final int printJobCount = mPrintJobs.size();
-        for (int i = 0; i < printJobCount; i++) {
-            PrintJobInfo printJob = mPrintJobs.get(i);
-            if (isActiveState(printJob.getState())) {
-                return true;
-            }
-        }
-        return false;
-    }
-
-    public boolean hasActivePrintJobsForServiceLocked(ComponentName service) {
-        final int printJobCount = mPrintJobs.size();
-        for (int i = 0; i < printJobCount; i++) {
-            PrintJobInfo printJob = mPrintJobs.get(i);
-            if (isActiveState(printJob.getState())
-                    && printJob.getPrinterId().getServiceName().equals(service)) {
-                return true;
-            }
-        }
-        return false;
-    }
-
-    private static boolean isActiveState(int printJobState) {
-        return printJobState == PrintJobInfo.STATE_CREATED
-                || printJobState == PrintJobInfo.STATE_QUEUED
-                || printJobState == PrintJobInfo.STATE_STARTED;
-    }
-
-    public boolean setPrintJobTag(int printJobId, String tag) {
-        synchronized (mLock) {
-            PrintJobInfo printJob = getPrintJobInfo(printJobId, PrintManager.APP_ID_ANY);
-            if (printJob != null) {
-                String printJobTag = printJob.getTag();
-                if (printJobTag == null) {
-                    if (tag == null) {
-                        return false;
-                    }
-                } else if (printJobTag.equals(tag)) {
-                    return false;
-                }
-                printJob.setTag(tag);
-                if (shouldPersistPrintJob(printJob)) {
-                    mPersistanceManager.writeStateLocked();
-                }
-                return true;
-            }
-        }
-        return false;
-    }
-
-    public void setPrintJobCopiesNoPersistence(int printJobId, int copies) {
-        synchronized (mLock) {
-            PrintJobInfo printJob = getPrintJobInfo(printJobId, PrintManager.APP_ID_ANY);
-            if (printJob != null) {
-                printJob.setCopies(copies);
-            }
-        }
-    }
-
-    public void setPrintJobPrintDocumentInfoNoPersistence(int printJobId, PrintDocumentInfo info) {
-        synchronized (mLock) {
-            PrintJobInfo printJob = getPrintJobInfo(printJobId, PrintManager.APP_ID_ANY);
-            if (printJob != null) {
-                printJob.setDocumentInfo(info);
-            }
-        }
-    }
-
-    public void setPrintJobAttributesNoPersistence(int printJobId, PrintAttributes attributes) {
-        synchronized (mLock) {
-            PrintJobInfo printJob = getPrintJobInfo(printJobId, PrintManager.APP_ID_ANY);
-            if (printJob != null) {
-                printJob.setAttributes(attributes);
-            }
-        }
-    }
-
-    public void setPrintJobPrinterNoPersistence(int 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(int printJobId, PageRange[] pages) {
-        synchronized (mLock) {
-            PrintJobInfo printJob = getPrintJobInfo(printJobId, PrintManager.APP_ID_ANY);
-            if (printJob != null) {
-                printJob.setPages(pages);
-            }
-        }
-    }
-
-    private boolean shouldPersistPrintJob(PrintJobInfo printJob) {
-        return printJob.getState() >= PrintJobInfo.STATE_QUEUED;
-    }
-
-    private void notifyOnAllPrintJobsHandled() {
-        // This has to run on the tread that is persisting the current state
-        // since this call may result in the system unbinding from the spooler
-        // and as a result the spooler process may get killed before the write
-        // completes.
-        new AsyncTask<Void, Void, Void>() {
-            @Override
-            protected Void doInBackground(Void... params) {
-                mService.onAllPrintJobsHandled();
-                return null;
-            }
-        }.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR, (Void[]) null);
-    }
-
-    private final class PersistenceManager {
-        private static final String PERSIST_FILE_NAME = "print_spooler_state.xml";
-
-        private static final String TAG_SPOOLER = "spooler";
-        private static final String TAG_JOB = "job";
-
-        private static final String TAG_PRINTER_ID = "printerId";
-        private static final String TAG_PAGE_RANGE = "pageRange";
-        private static final String TAG_ATTRIBUTES = "attributes";
-        private static final String TAG_DOCUMENT_INFO = "documentInfo";
-
-        private static final String ATTR_ID = "id";
-        private static final String ATTR_LABEL = "label";
-        private static final String ATTR_STATE = "state";
-        private static final String ATTR_APP_ID = "appId";
-        private static final String ATTR_USER_ID = "userId";
-        private static final String ATTR_TAG = "tag";
-        private static final String ATTR_COPIES = "copies";
-
-        private static final String TAG_MEDIA_SIZE = "mediaSize";
-        private static final String TAG_RESOLUTION = "resolution";
-        private static final String TAG_MARGINS = "margins";
-        private static final String TAG_INPUT_TRAY = "inputTray";
-        private static final String TAG_OUTPUT_TRAY = "outputTray";
-
-        private static final String ATTR_DUPLEX_MODE = "duplexMode";
-        private static final String ATTR_COLOR_MODE = "colorMode";
-        private static final String ATTR_FITTING_MODE = "fittingMode";
-        private static final String ATTR_ORIENTATION = "orientation";
-
-        private static final String ATTR_LOCAL_ID = "printerName";
-        private static final String ATTR_SERVICE_NAME = "serviceName";
-
-        private static final String ATTR_WIDTH_MILS = "widthMils";
-        private static final String ATTR_HEIGHT_MILS = "heightMils";
-
-        private static final String ATTR_HORIZONTAL_DPI = "horizontalDip";
-        private static final String ATTR_VERTICAL_DPI = "verticalDpi";
-
-        private static final String ATTR_LEFT_MILS = "leftMils";
-        private static final String ATTR_TOP_MILS = "topMils";
-        private static final String ATTR_RIGHT_MILS = "rightMils";
-        private static final String ATTR_BOTTOM_MILS = "bottomMils";
-
-        private static final String ATTR_START = "start";
-        private static final String ATTR_END = "end";
-
-        private static final String ATTR_NAME = "name";
-        private static final String ATTR_PAGE_COUNT = "pageCount";
-        private static final String ATTR_CONTENT_TYPE = "contentType";
-
-        private final AtomicFile mStatePersistFile;
-
-        private boolean mWriteStateScheduled;
-
-        private PersistenceManager(Context context) {
-            mStatePersistFile = new AtomicFile(new File(context.getFilesDir(),
-                    PERSIST_FILE_NAME));
-        }
-
-        public void writeStateLocked() {
-            if (!PERSISTNECE_MANAGER_ENABLED) {
-                return;
-            }
-            if (mWriteStateScheduled) {
-                return;
-            }
-            mWriteStateScheduled = true;
-            new AsyncTask<Void, Void, Void>() {
-                @Override
-                protected Void doInBackground(Void... params) {
-                    synchronized (mLock) {
-                        mWriteStateScheduled = false;
-                        doWriteStateLocked();
-                    }
-                    return null;
-                }
-            }.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR, (Void[]) null);
-        }
-
-        private void doWriteStateLocked() {
-            if (DEBUG_PERSISTENCE) {
-                Log.i(LOG_TAG, "[PERSIST START]");
-            }
-            FileOutputStream out = null;
-            try {
-                out = mStatePersistFile.startWrite();
-
-                XmlSerializer serializer = new FastXmlSerializer();
-                serializer.setOutput(out, "utf-8");
-                serializer.startDocument(null, true);
-                serializer.startTag(null, TAG_SPOOLER);
-
-                List<PrintJobInfo> printJobs = mPrintJobs;
-
-                final int printJobCount = printJobs.size();
-                for (int j = 0; j < printJobCount; j++) {
-                    PrintJobInfo printJob = printJobs.get(j);
-
-                    final int state = printJob.getState();
-                    if (state < PrintJobInfo.STATE_QUEUED
-                            || state > PrintJobInfo.STATE_CANCELED) {
-                        continue;
-                    }
-
-                    serializer.startTag(null, TAG_JOB);
-
-                    serializer.attribute(null, ATTR_ID, String.valueOf(printJob.getId()));
-                    serializer.attribute(null, ATTR_LABEL, printJob.getLabel().toString());
-                    serializer.attribute(null, ATTR_STATE, String.valueOf(printJob.getState()));
-                    serializer.attribute(null, ATTR_APP_ID, String.valueOf(printJob.getAppId()));
-                    serializer.attribute(null, ATTR_USER_ID, String.valueOf(printJob.getUserId()));
-                    String tag = printJob.getTag();
-                    if (tag != null) {
-                        serializer.attribute(null, ATTR_TAG, tag);
-                    }
-                    serializer.attribute(null, ATTR_COPIES, String.valueOf(printJob.getCopies()));
-
-                    PrinterId printerId = printJob.getPrinterId();
-                    if (printerId != null) {
-                        serializer.startTag(null, TAG_PRINTER_ID);
-                        serializer.attribute(null, ATTR_LOCAL_ID, printerId.getLocalId());
-                        serializer.attribute(null, ATTR_SERVICE_NAME, printerId.getServiceName()
-                                .flattenToString());
-                        serializer.endTag(null, TAG_PRINTER_ID);
-                    }
-
-                    PageRange[] pages = printJob.getPages();
-                    if (pages != null) {
-                        for (int i = 0; i < pages.length; i++) {
-                            serializer.startTag(null, TAG_PAGE_RANGE);
-                            serializer.attribute(null, ATTR_START, String.valueOf(
-                                    pages[i].getStart()));
-                            serializer.attribute(null, ATTR_END, String.valueOf(
-                                    pages[i].getEnd()));
-                            serializer.endTag(null, TAG_PAGE_RANGE);
-                        }
-                    }
-
-                    PrintAttributes attributes = printJob.getAttributes();
-                    if (attributes != null) {
-                        serializer.startTag(null, TAG_ATTRIBUTES);
-
-                        final int duplexMode = attributes.getDuplexMode();
-                        serializer.attribute(null, ATTR_DUPLEX_MODE,
-                                String.valueOf(duplexMode));
-
-                        final int colorMode = attributes.getColorMode();
-                        serializer.attribute(null, ATTR_COLOR_MODE,
-                                String.valueOf(colorMode));
-
-                        final int fittingMode = attributes.getFittingMode();
-                        serializer.attribute(null, ATTR_FITTING_MODE,
-                                String.valueOf(fittingMode));
-
-                        final int orientation = attributes.getOrientation();
-                        serializer.attribute(null, ATTR_ORIENTATION,
-                                String.valueOf(orientation));
-
-                        MediaSize mediaSize = attributes.getMediaSize();
-                        if (mediaSize != null) {
-                            serializer.startTag(null, TAG_MEDIA_SIZE);
-                            serializer.attribute(null, ATTR_ID, mediaSize.getId());
-                            serializer.attribute(null, ATTR_LABEL, mediaSize.getLabel()
-                                    .toString());
-                            serializer.attribute(null, ATTR_WIDTH_MILS, String.valueOf(
-                                    mediaSize.getWidthMils()));
-                            serializer.attribute(null, ATTR_HEIGHT_MILS,String.valueOf(
-                                    mediaSize.getHeightMils()));
-                            serializer.endTag(null, TAG_MEDIA_SIZE);
-                        }
-
-                        Resolution resolution = attributes.getResolution();
-                        if (resolution != null) {
-                            serializer.startTag(null, TAG_RESOLUTION);
-                            serializer.attribute(null, ATTR_ID, resolution.getId());
-                            serializer.attribute(null, ATTR_LABEL, resolution.getLabel()
-                                    .toString());
-                            serializer.attribute(null, ATTR_HORIZONTAL_DPI, String.valueOf(
-                                     resolution.getHorizontalDpi()));
-                            serializer.attribute(null, ATTR_VERTICAL_DPI, String.valueOf(
-                                    resolution.getVerticalDpi()));
-                            serializer.endTag(null, TAG_RESOLUTION);
-                        }
-
-                        Margins margins = attributes.getMargins();
-                        if (margins != null) {
-                            serializer.startTag(null, TAG_MARGINS);
-                            serializer.attribute(null, ATTR_LEFT_MILS, String.valueOf(
-                                    margins.getLeftMils()));
-                            serializer.attribute(null, ATTR_TOP_MILS, String.valueOf(
-                                    margins.getTopMils()));
-                            serializer.attribute(null, ATTR_RIGHT_MILS, String.valueOf(
-                                    margins.getRightMils()));
-                            serializer.attribute(null, ATTR_BOTTOM_MILS, String.valueOf(
-                                    margins.getBottomMils()));
-                            serializer.endTag(null, TAG_MARGINS);
-                        }
-
-                        Tray inputTray = attributes.getInputTray();
-                        if (inputTray != null) {
-                            serializer.startTag(null, TAG_INPUT_TRAY);
-                            serializer.attribute(null, ATTR_ID, inputTray.getId());
-                            serializer.attribute(null, ATTR_LABEL, inputTray.getLabel()
-                                    .toString());
-                            serializer.endTag(null, TAG_INPUT_TRAY);
-                        }
-
-                        Tray outputTray = attributes.getOutputTray();
-                        if (outputTray != null) {
-                            serializer.startTag(null, TAG_OUTPUT_TRAY);
-                            serializer.attribute(null, ATTR_ID, outputTray.getId());
-                            serializer.attribute(null, ATTR_LABEL, outputTray.getLabel()
-                                    .toString());
-                            serializer.endTag(null, TAG_OUTPUT_TRAY);
-                        }
-
-                        serializer.endTag(null, TAG_ATTRIBUTES);
-                    }
-
-                    PrintDocumentInfo documentInfo = printJob.getDocumentInfo();
-                    if (documentInfo != null) {
-                        serializer.startTag(null, TAG_DOCUMENT_INFO);
-                        serializer.attribute(null, ATTR_NAME, documentInfo.getName());
-                        serializer.attribute(null, ATTR_CONTENT_TYPE, String.valueOf(
-                                documentInfo.getContentType()));
-                        serializer.attribute(null, ATTR_PAGE_COUNT, String.valueOf(
-                                documentInfo.getPageCount()));
-                        serializer.endTag(null, TAG_DOCUMENT_INFO);
-                    }
-
-                    serializer.endTag(null, TAG_JOB);
-
-                    if (DEBUG_PERSISTENCE) {
-                        Log.i(LOG_TAG, "[PERSISTED] " + printJob);
-                    }
-                }
-
-                serializer.endTag(null, TAG_SPOOLER);
-                serializer.endDocument();
-                mStatePersistFile.finishWrite(out);
-                if (DEBUG_PERSISTENCE) {
-                    Log.i(LOG_TAG, "[PERSIST END]");
-                }
-            } catch (IOException e) {
-                Slog.w(LOG_TAG, "Failed to write state, restoring backup.", e);
-                mStatePersistFile.failWrite(out);
-            } finally {
-                IoUtils.closeQuietly(out);
-            }
-        }
-
-        public void readStateLocked() {
-            if (!PERSISTNECE_MANAGER_ENABLED) {
-                return;
-            }
-            FileInputStream in = null;
-            try {
-                in = mStatePersistFile.openRead();
-            } catch (FileNotFoundException e) {
-                Log.i(LOG_TAG, "No existing print spooler state.");
-                return;
-            }
-            try {
-                XmlPullParser parser = Xml.newPullParser();
-                parser.setInput(in, null);
-                parseState(parser);
-            } catch (IllegalStateException ise) {
-                Slog.w(LOG_TAG, "Failed parsing ", ise);
-            } catch (NullPointerException npe) {
-                Slog.w(LOG_TAG, "Failed parsing ", npe);
-            } catch (NumberFormatException nfe) {
-                Slog.w(LOG_TAG, "Failed parsing ", nfe);
-            } catch (XmlPullParserException xppe) {
-                Slog.w(LOG_TAG, "Failed parsing ", xppe);
-            } catch (IOException ioe) {
-                Slog.w(LOG_TAG, "Failed parsing ", ioe);
-            } catch (IndexOutOfBoundsException iobe) {
-                Slog.w(LOG_TAG, "Failed parsing ", iobe);
-            } finally {
-                IoUtils.closeQuietly(in);
-            }
-        }
-
-        private void parseState(XmlPullParser parser)
-                throws IOException, XmlPullParserException {
-            parser.next();
-            skipEmptyTextTags(parser);
-            expect(parser, XmlPullParser.START_TAG, TAG_SPOOLER);
-            parser.next();
-
-            while (parsePrintJob(parser)) {
-                parser.next();
-            }
-
-            skipEmptyTextTags(parser);
-            expect(parser, XmlPullParser.END_TAG, TAG_SPOOLER);
-        }
-
-        private boolean parsePrintJob(XmlPullParser parser)
-                throws IOException, XmlPullParserException {
-            skipEmptyTextTags(parser);
-            if (!accept(parser, XmlPullParser.START_TAG, TAG_JOB)) {
-                return false;
-            }
-
-            PrintJobInfo printJob = new PrintJobInfo();
-
-            final int printJobId = Integer.parseInt(parser.getAttributeValue(null, ATTR_ID));
-            printJob.setId(printJobId);
-            String label = parser.getAttributeValue(null, ATTR_LABEL);
-            printJob.setLabel(label);
-            final int state = Integer.parseInt(parser.getAttributeValue(null, ATTR_STATE));
-            printJob.setState(state);
-            final int appId = Integer.parseInt(parser.getAttributeValue(null, ATTR_APP_ID));
-            printJob.setAppId(appId);
-            final int userId = Integer.parseInt(parser.getAttributeValue(null, ATTR_USER_ID));
-            printJob.setUserId(userId);
-            String tag = parser.getAttributeValue(null, ATTR_TAG);
-            printJob.setTag(tag);
-            String copies = parser.getAttributeValue(null, ATTR_COPIES);
-            printJob.setCopies(Integer.parseInt(copies));
-
-            parser.next();
-
-            skipEmptyTextTags(parser);
-            if (accept(parser, XmlPullParser.START_TAG, TAG_PRINTER_ID)) {
-                String localId = parser.getAttributeValue(null, ATTR_LOCAL_ID);
-                ComponentName service = ComponentName.unflattenFromString(parser.getAttributeValue(
-                        null, ATTR_SERVICE_NAME));
-                printJob.setPrinterId(new PrinterId(service, localId));
-                parser.next();
-                skipEmptyTextTags(parser);
-                expect(parser, XmlPullParser.END_TAG, TAG_PRINTER_ID);
-                parser.next();
-            }
-
-            skipEmptyTextTags(parser);
-            List<PageRange> pageRanges = null;
-            while (accept(parser, XmlPullParser.START_TAG, TAG_PAGE_RANGE)) {
-                final int start = Integer.parseInt(parser.getAttributeValue(null, ATTR_START));
-                final int end = Integer.parseInt(parser.getAttributeValue(null, ATTR_END));
-                PageRange pageRange = new PageRange(start, end);
-                if (pageRanges == null) {
-                    pageRanges = new ArrayList<PageRange>();
-                }
-                pageRanges.add(pageRange);
-                parser.next();
-                skipEmptyTextTags(parser);
-                expect(parser, XmlPullParser.END_TAG, TAG_PAGE_RANGE);
-                parser.next();
-            }
-            if (pageRanges != null) {
-                PageRange[] pageRangesArray = new PageRange[pageRanges.size()];
-                pageRanges.toArray(pageRangesArray);
-                printJob.setPages(pageRangesArray);
-            }
-
-            skipEmptyTextTags(parser);
-            if (accept(parser, XmlPullParser.START_TAG, TAG_ATTRIBUTES)) {
-
-                PrintAttributes.Builder builder = new PrintAttributes.Builder();
-
-                String duplexMode = parser.getAttributeValue(null, ATTR_DUPLEX_MODE);
-                builder.setDuplexMode(Integer.parseInt(duplexMode));
-
-                String colorMode = parser.getAttributeValue(null, ATTR_COLOR_MODE);
-                builder.setColorMode(Integer.parseInt(colorMode));
-
-                String fittingMode = parser.getAttributeValue(null, ATTR_FITTING_MODE);
-                builder.setFittingMode(Integer.parseInt(fittingMode));
-
-                String orientation = parser.getAttributeValue(null, ATTR_ORIENTATION);
-                builder.setOrientation(Integer.parseInt(orientation));
-
-                parser.next();
-
-                skipEmptyTextTags(parser);
-                if (accept(parser, XmlPullParser.START_TAG, TAG_MEDIA_SIZE)) {
-                    String id = parser.getAttributeValue(null, ATTR_ID);
-                    label = parser.getAttributeValue(null, ATTR_LABEL);
-                    final int widthMils = Integer.parseInt(parser.getAttributeValue(null,
-                            ATTR_WIDTH_MILS));
-                    final int heightMils = Integer.parseInt(parser.getAttributeValue(null,
-                            ATTR_HEIGHT_MILS));
-                    MediaSize mediaSize = new MediaSize(id, label, widthMils, heightMils);
-                    builder.setMediaSize(mediaSize);
-                    parser.next();
-                    skipEmptyTextTags(parser);
-                    expect(parser, XmlPullParser.END_TAG, TAG_MEDIA_SIZE);
-                    parser.next();
-                }
-
-                skipEmptyTextTags(parser);
-                if (accept(parser, XmlPullParser.START_TAG, TAG_RESOLUTION)) {
-                    String id = parser.getAttributeValue(null, ATTR_ID);
-                    label = parser.getAttributeValue(null, ATTR_LABEL);
-                    final int horizontalDpi = Integer.parseInt(parser.getAttributeValue(null,
-                            ATTR_HORIZONTAL_DPI));
-                    final int verticalDpi = Integer.parseInt(parser.getAttributeValue(null,
-                            ATTR_VERTICAL_DPI));
-                    Resolution resolution = new Resolution(id, label, horizontalDpi, verticalDpi);
-                    builder.setResolution(resolution);
-                    parser.next();
-                    skipEmptyTextTags(parser);
-                    expect(parser, XmlPullParser.END_TAG, TAG_RESOLUTION);
-                    parser.next();
-                }
-
-                skipEmptyTextTags(parser);
-                if (accept(parser, XmlPullParser.START_TAG, TAG_MARGINS)) {
-                    final int leftMils = Integer.parseInt(parser.getAttributeValue(null,
-                            ATTR_LEFT_MILS));
-                    final int topMils = Integer.parseInt(parser.getAttributeValue(null,
-                            ATTR_TOP_MILS));
-                    final int rightMils = Integer.parseInt(parser.getAttributeValue(null,
-                            ATTR_RIGHT_MILS));
-                    final int bottomMils = Integer.parseInt(parser.getAttributeValue(null,
-                            ATTR_BOTTOM_MILS));
-                    Margins margins = new Margins(leftMils, topMils, rightMils, bottomMils);
-                    builder.setMargins(margins);
-                    parser.next();
-                    skipEmptyTextTags(parser);
-                    expect(parser, XmlPullParser.END_TAG, TAG_MARGINS);
-                    parser.next();
-                }
-
-                skipEmptyTextTags(parser);
-                if (accept(parser, XmlPullParser.START_TAG, TAG_INPUT_TRAY)) {
-                    String id = parser.getAttributeValue(null, ATTR_ID);
-                    label = parser.getAttributeValue(null, ATTR_LABEL);
-                    Tray tray = new Tray(id, label);
-                    builder.setInputTray(tray);
-                    parser.next();
-                    skipEmptyTextTags(parser);
-                    expect(parser, XmlPullParser.END_TAG, TAG_INPUT_TRAY);
-                    parser.next();
-                }
-
-                skipEmptyTextTags(parser);
-                if (accept(parser, XmlPullParser.START_TAG, TAG_OUTPUT_TRAY)) {
-                    String id = parser.getAttributeValue(null, ATTR_ID);
-                    label = parser.getAttributeValue(null, ATTR_LABEL);
-                    Tray tray = new Tray(id, label);
-                    builder.setOutputTray(tray);
-                    parser.next();
-                    skipEmptyTextTags(parser);
-                    expect(parser, XmlPullParser.END_TAG, TAG_OUTPUT_TRAY);
-                    parser.next();
-                }
-
-                printJob.setAttributes(builder.create());
-
-                skipEmptyTextTags(parser);
-                expect(parser, XmlPullParser.END_TAG, TAG_ATTRIBUTES);
-                parser.next();
-            }
-
-            skipEmptyTextTags(parser);
-            if (accept(parser, XmlPullParser.START_TAG, TAG_DOCUMENT_INFO)) {
-                String name = parser.getAttributeValue(null, ATTR_NAME);
-                final int pageCount = Integer.parseInt(parser.getAttributeValue(null,
-                        ATTR_PAGE_COUNT));
-                final int contentType = Integer.parseInt(parser.getAttributeValue(null,
-                        ATTR_CONTENT_TYPE));
-                PrintDocumentInfo info = new PrintDocumentInfo.Builder(name)
-                        .setPageCount(pageCount)
-                        .setContentType(contentType).create();
-                printJob.setDocumentInfo(info);
-                parser.next();
-                skipEmptyTextTags(parser);
-                expect(parser, XmlPullParser.END_TAG, TAG_DOCUMENT_INFO);
-                parser.next();
-            }
-
-            mPrintJobs.add(printJob);
-
-            if (DEBUG_PERSISTENCE) {
-                Log.i(LOG_TAG, "[RESTORED] " + printJob);
-            }
-
-            skipEmptyTextTags(parser);
-            expect(parser, XmlPullParser.END_TAG, TAG_JOB);
-
-            return true;
-        }
-
-        private void expect(XmlPullParser parser, int type, String tag)
-                throws IOException, XmlPullParserException {
-            if (!accept(parser, type, tag)) {
-                throw new XmlPullParserException("Exepected event: " + type
-                        + " and tag: " + tag + " but got event: " + parser.getEventType()
-                        + " and tag:" + parser.getName());
-            }
-        }
-
-        private void skipEmptyTextTags(XmlPullParser parser)
-                throws IOException, XmlPullParserException {
-            while (accept(parser, XmlPullParser.TEXT, null)
-                    && "\n".equals(parser.getText())) {
-                parser.next();
-            }
-        }
-
-        private boolean accept(XmlPullParser parser, int type, String tag)
-                throws IOException, XmlPullParserException {
-            if (parser.getEventType() != type) {
-                return false;
-            }
-            if (tag != null) {
-                if (!tag.equals(parser.getName())) {
-                    return false;
-                }
-            } else if (parser.getName() != null) {
-                return false;
-            }
-            return true;
-        }
-    }
-}
diff --git a/packages/PrintSpooler/src/com/android/printspooler/PrintSpoolerService.java b/packages/PrintSpooler/src/com/android/printspooler/PrintSpoolerService.java
index 4fab4f8..fda64c9 100644
--- a/packages/PrintSpooler/src/com/android/printspooler/PrintSpoolerService.java
+++ b/packages/PrintSpooler/src/com/android/printspooler/PrintSpoolerService.java
@@ -21,9 +21,8 @@
 import android.content.ComponentName;
 import android.content.Intent;
 import android.content.IntentSender;
-import android.os.Handler;
+import android.os.AsyncTask;
 import android.os.IBinder;
-import android.os.Looper;
 import android.os.Message;
 import android.os.ParcelFileDescriptor;
 import android.os.RemoteException;
@@ -32,14 +31,38 @@
 import android.print.IPrintSpooler;
 import android.print.IPrintSpoolerCallbacks;
 import android.print.IPrintSpoolerClient;
-import android.print.IPrinterDiscoverySessionObserver;
+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.PrintAttributes.Tray;
+import android.print.PrintDocumentInfo;
 import android.print.PrintJobInfo;
+import android.print.PrintManager;
+import android.print.PrinterId;
+import android.print.PrinterInfo;
+import android.util.AtomicFile;
 import android.util.Log;
 import android.util.Slog;
+import android.util.Xml;
 
+import com.android.internal.os.HandlerCaller;
 import com.android.internal.os.SomeArgs;
+import com.android.internal.util.FastXmlSerializer;
 
+import libcore.io.IoUtils;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlSerializer;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.ArrayList;
 import java.util.List;
 
 /**
@@ -48,22 +71,65 @@
  */
 public final class PrintSpoolerService extends Service {
 
+    private static final String LOG_TAG = "PrintSpoolerService";
+
+    private static final boolean DEBUG_PRINT_JOB_LIFECYCLE = true;
+
+    private static final boolean DEBUG_PERSISTENCE = true;
+
+    private static final boolean PERSISTNECE_MANAGER_ENABLED = true;
+
     private static final long CHECK_ALL_PRINTJOBS_HANDLED_DELAY = 5000;
 
-    private static final String LOG_TAG = "PrintSpoolerService";
+    private static final String PRINT_FILE_EXTENSION = "pdf";
+
+    private static final Object sLock = new Object();
+
+    private final Object mLock = new Object();
+
+    private final List<PrintJobInfo> mPrintJobs = new ArrayList<PrintJobInfo>();
+
+    private static PrintSpoolerService sInstance;
+
+    private static int sPrintJobIdCounter;
 
     private Intent mStartPrintJobConfigActivityIntent;
 
     private IPrintSpoolerClient mClient;
 
-    private Handler mHandler;
+    private HandlerCaller mHandlerCaller;
+
+    private PersistenceManager mPersistanceManager;
+
+    private NotificationController mNotificationController;
+
+    private PrinterDiscoverySession mDiscoverySession;
+
+    public static PrintSpoolerService peekInstance() {
+        synchronized (sLock) {
+            return sInstance;
+        }
+    }
 
     @Override
     public void onCreate() {
         super.onCreate();
         mStartPrintJobConfigActivityIntent = new Intent(PrintSpoolerService.this,
                 PrintJobConfigActivity.class);
-        mHandler = new MyHandler(getMainLooper());
+        mHandlerCaller = new HandlerCaller(this, getMainLooper(),
+                new HandlerCallerCallback(), false);
+
+        mPersistanceManager = new PersistenceManager();
+        mNotificationController = new NotificationController(PrintSpoolerService.this);
+
+        synchronized (mLock) {
+            mPersistanceManager.readStateLocked();
+            handleReadPrintJobsLocked();
+        }
+
+        synchronized (sLock) {
+            sInstance = this;
+        }
     }
 
     @Override
@@ -72,10 +138,10 @@
             @Override
             public void getPrintJobInfos(IPrintSpoolerCallbacks callback,
                     ComponentName componentName, int state, int appId, int sequence)
-                            throws RemoteException {
+                    throws RemoteException {
                 List<PrintJobInfo> printJobs = null;
                 try {
-                    printJobs = PrintSpooler.peekInstance().getPrintJobInfos(
+                    printJobs = PrintSpoolerService.this.getPrintJobInfos(
                             componentName, state, appId);
                 } finally {
                     callback.onGetPrintJobInfosResult(printJobs, sequence);
@@ -87,7 +153,7 @@
                     int appId, int sequence) throws RemoteException {
                 PrintJobInfo printJob = null;
                 try {
-                    printJob = PrintSpooler.peekInstance().getPrintJobInfo(printJobId, appId);
+                    printJob = PrintSpoolerService.this.getPrintJobInfo(printJobId, appId);
                 } finally {
                     callback.onGetPrintJobInfoResult(printJob, sequence);
                 }
@@ -98,11 +164,11 @@
             public void createPrintJob(String printJobName, IPrintClient client,
                     IPrintDocumentAdapter printAdapter, PrintAttributes attributes,
                     IPrintSpoolerCallbacks callback, int appId, int sequence)
-                            throws RemoteException {
+                    throws RemoteException {
                 PrintJobInfo printJob = null;
                 try {
-                    printJob = PrintSpooler.peekInstance().createPrintJob(printJobName, client,
-                            attributes, appId);
+                    printJob = PrintSpoolerService.this.createPrintJob(
+                            printJobName, client, attributes, appId);
                     if (printJob != null) {
                         Intent intent = mStartPrintJobConfigActivityIntent;
                         intent.putExtra(PrintJobConfigActivity.EXTRA_PRINT_DOCUMENT_ADAPTER,
@@ -113,13 +179,12 @@
 
                         IntentSender sender = PendingIntent.getActivity(
                                 PrintSpoolerService.this, 0, intent, PendingIntent.FLAG_ONE_SHOT
-                                | PendingIntent.FLAG_CANCEL_CURRENT).getIntentSender();
+                                        | PendingIntent.FLAG_CANCEL_CURRENT).getIntentSender();
 
-                        SomeArgs args = SomeArgs.obtain();
-                        args.arg1 = client;
-                        args.arg2 = sender;
-                        mHandler.obtainMessage(MyHandler.MSG_START_PRINT_JOB_CONFIG_ACTIVITY,
-                                args).sendToTarget();
+                        Message message = mHandlerCaller.obtainMessageOO(
+                                HandlerCallerCallback.MSG_START_PRINT_JOB_CONFIG_ACTIVITY,
+                                client, sender);
+                        mHandlerCaller.executeOrSendMessage(message);
                     }
                 } finally {
                     callback.onCreatePrintJobResult(printJob, sequence);
@@ -127,11 +192,11 @@
             }
 
             @Override
-            public void setPrintJobState(int printJobId, int state, CharSequence error,
+            public void setPrintJobState(int printJobId, int state, String error,
                     IPrintSpoolerCallbacks callback, int sequece) throws RemoteException {
                 boolean success = false;
                 try {
-                    success = PrintSpooler.peekInstance().setPrintJobState(
+                    success = PrintSpoolerService.this.setPrintJobState(
                             printJobId, state, error);
                 } finally {
                     callback.onSetPrintJobStateResult(success, sequece);
@@ -143,7 +208,7 @@
                     IPrintSpoolerCallbacks callback, int sequece) throws RemoteException {
                 boolean success = false;
                 try {
-                    success = PrintSpooler.peekInstance().setPrintJobTag(printJobId, tag);
+                    success = PrintSpoolerService.this.setPrintJobTag(printJobId, tag);
                 } finally {
                     callback.onSetPrintJobTagResult(success, sequece);
                 }
@@ -151,60 +216,191 @@
 
             @Override
             public void writePrintJobData(ParcelFileDescriptor fd, int printJobId) {
-                PrintSpooler.peekInstance().writePrintJobData(fd, printJobId);
+                PrintSpoolerService.this.writePrintJobData(fd, printJobId);
             }
 
             @Override
             public void setClient(IPrintSpoolerClient client) {
-                mHandler.obtainMessage(MyHandler.MSG_SET_CLIENT, client).sendToTarget();
+                Message message = mHandlerCaller.obtainMessageO(
+                        HandlerCallerCallback.MSG_SET_CLIENT, client);
+                mHandlerCaller.executeOrSendMessage(message);
+            }
+
+            @Override
+            public void onPrintersAdded(List<PrinterInfo> printers) {
+                Message message = mHandlerCaller.obtainMessageO(
+                        HandlerCallerCallback.MSG_ON_PRINTERS_ADDED, printers);
+                mHandlerCaller.executeOrSendMessage(message);
+            }
+
+            @Override
+            public void onPrintersRemoved(List<PrinterId> printerIds) {
+                Message message = mHandlerCaller.obtainMessageO(
+                        HandlerCallerCallback.MSG_ON_PRINTERS_REMOVED, printerIds);
+                mHandlerCaller.executeOrSendMessage(message);
+            }
+
+            @Override
+            public void onPrintersUpdated(List<PrinterInfo> printers) {
+                Message message = mHandlerCaller.obtainMessageO(
+                        HandlerCallerCallback.MSG_ON_PRINTERS_UPDATED, printers);
+                mHandlerCaller.executeOrSendMessage(message);
             }
         };
     }
 
-    public void onPrintJobQueued(PrintJobInfo printJob) {
-        mHandler.obtainMessage(MyHandler.MSG_ON_PRINT_JOB_QUEUED,
-                printJob).sendToTarget();
+    public void createPrinterDiscoverySession() {
+        Message message = mHandlerCaller.obtainMessage(
+                HandlerCallerCallback.MSG_CREATE_PRINTER_DISCOVERY_SESSION);
+        mHandlerCaller.executeOrSendMessage(message);
     }
 
-    public void createPrinterDiscoverySession(IPrinterDiscoverySessionObserver observer) {
-        mHandler.obtainMessage(MyHandler.MSG_CREATE_PRINTER_DISCOVERY_SESSION,
-                observer).sendToTarget();
+    public void destroyPrinterDiscoverySession() {
+        Message message = mHandlerCaller.obtainMessage(
+                HandlerCallerCallback.MSG_DESTROY_PRINTER_DISCOVERY_SESSION);
+        mHandlerCaller.executeOrSendMessage(message);
     }
 
-    public void onAllPrintJobsForServiceHandled(ComponentName service) {
-        mHandler.obtainMessage(MyHandler.MSG_ON_ALL_PRINT_JOBS_FOR_SERIVICE_HANDLED,
-                service).sendToTarget();
+    public void startPrinterDiscovery(List<PrinterId> priorityList) {
+        Message message = mHandlerCaller.obtainMessageO(
+                HandlerCallerCallback.MSG_START_PRINTER_DISCOVERY, priorityList);
+        mHandlerCaller.executeOrSendMessage(message);
     }
 
-    public void onAllPrintJobsHandled() {
-        mHandler.sendEmptyMessage(MyHandler.MSG_ON_ALL_PRINT_JOBS_HANDLED);
+    public void stopPrinterDiscovery() {
+        Message message = mHandlerCaller.obtainMessage(
+                HandlerCallerCallback.MSG_STOP_PRINTER_DISCOVERY);
+        mHandlerCaller.executeOrSendMessage(message);
     }
 
-    private final class MyHandler extends Handler {
-        public static final int MSG_SET_CLIENT = 1;
-        public static final int MSG_START_PRINT_JOB_CONFIG_ACTIVITY = 2;
-        public static final int MSG_CREATE_PRINTER_DISCOVERY_SESSION = 3;
-        public static final int MSG_ON_PRINT_JOB_QUEUED = 5;
-        public static final int MSG_ON_ALL_PRINT_JOBS_FOR_SERIVICE_HANDLED = 6;
-        public static final int MSG_ON_ALL_PRINT_JOBS_HANDLED = 7;
-        public static final int MSG_CHECK_ALL_PRINTJOBS_HANDLED = 9;
+    public void requestPrinterUpdate(PrinterId pritnerId) {
+        Message message = mHandlerCaller.obtainMessageO(
+                HandlerCallerCallback.MSG_REQUEST_PRINTER_UPDATE, pritnerId);
+        mHandlerCaller.executeOrSendMessage(message);
+    }
 
-        public MyHandler(Looper looper) {
-            super(looper, null, false);
-        }
+
+    private void sendOnPrintJobQueued(PrintJobInfo printJob) {
+        Message message = mHandlerCaller.obtainMessageO(
+                HandlerCallerCallback.MSG_ON_PRINT_JOB_QUEUED, printJob);
+        mHandlerCaller.executeOrSendMessage(message);
+    }
+
+    private void sendOnAllPrintJobsForServiceHandled(ComponentName service) {
+        Message message = mHandlerCaller.obtainMessageO(
+                HandlerCallerCallback.MSG_ON_ALL_PRINT_JOBS_FOR_SERIVICE_HANDLED, service);
+        mHandlerCaller.executeOrSendMessage(message);
+    }
+
+    private void sendOnAllPrintJobsHandled() {
+        Message message = mHandlerCaller.obtainMessage(
+                HandlerCallerCallback.MSG_ON_ALL_PRINT_JOBS_HANDLED);
+        mHandlerCaller.executeOrSendMessage(message);
+    }
+
+    private final class HandlerCallerCallback implements HandlerCaller.Callback {
+        public static final int MSG_CREATE_PRINTER_DISCOVERY_SESSION = 1;
+        public static final int MSG_DESTROY_PRINTER_DISCOVERY_SESSION = 2;
+        public static final int MSG_START_PRINTER_DISCOVERY = 3;
+        public static final int MSG_STOP_PRINTER_DISCOVERY = 4;
+        public static final int MSG_REQUEST_PRINTER_UPDATE = 5;
+
+        public static final int MSG_ON_PRINTERS_ADDED = 6;
+        public static final int MSG_ON_PRINTERS_REMOVED = 7;
+        public static final int MSG_ON_PRINTERS_UPDATED = 8;
+
+        public static final int MSG_SET_CLIENT = 9;
+        public static final int MSG_START_PRINT_JOB_CONFIG_ACTIVITY = 10;
+        public static final int MSG_ON_PRINT_JOB_QUEUED = 11;
+        public static final int MSG_ON_ALL_PRINT_JOBS_FOR_SERIVICE_HANDLED = 12;
+        public static final int MSG_ON_ALL_PRINT_JOBS_HANDLED = 13;
+        public static final int MSG_CHECK_ALL_PRINTJOBS_HANDLED = 14;
 
         @Override
-        public void handleMessage(Message message) {
+        @SuppressWarnings("unchecked")
+        public void executeMessage(Message message) {
             switch (message.what) {
+                case MSG_CREATE_PRINTER_DISCOVERY_SESSION: {
+                    final IPrintSpoolerClient client;
+                    synchronized (mLock) {
+                        client = mClient;
+                    }
+                    if (client != null) {
+                        try {
+                            client.createPrinterDiscoverySession();
+                        } catch (RemoteException re) {
+                            Log.e(LOG_TAG, "Error creating printer discovery session.", re);
+                        }
+                    }
+                } break;
+
+                case MSG_DESTROY_PRINTER_DISCOVERY_SESSION: {
+                    final IPrintSpoolerClient client;
+                    synchronized (mLock) {
+                        client = mClient;
+                    }
+                    if (client != null) {
+                        try {
+                            client.destroyPrinterDiscoverySession();
+                        } catch (RemoteException re) {
+                            Log.e(LOG_TAG, "Error destroying printer discovery session.", re);
+                        }
+                    }
+                } break;
+
+                case MSG_START_PRINTER_DISCOVERY: {
+                    final IPrintSpoolerClient client;
+                    synchronized (mLock) {
+                        client = mClient;
+                    }
+                    if (client != null) {
+                        List<PrinterId> priorityList = (ArrayList<PrinterId>) message.obj;
+                        try {
+                            client.startPrinterDiscovery(priorityList);
+                        } catch (RemoteException re) {
+                            Log.e(LOG_TAG, "Error starting printer discovery.", re);
+                        }
+                    }
+                } break;
+
+                case MSG_STOP_PRINTER_DISCOVERY: {
+                    final IPrintSpoolerClient client;
+                    synchronized (mLock) {
+                        client = mClient;
+                    }
+                    if (client != null) {
+                        try {
+                            client.stopPrinterDiscovery();
+                        } catch (RemoteException re) {
+                            Log.e(LOG_TAG, "Error stopping printer discovery.", re);
+                        }
+                    }
+                } break;
+
+                case MSG_REQUEST_PRINTER_UPDATE: {
+                    final IPrintSpoolerClient client;
+                    synchronized (mLock) {
+                        client = mClient;
+                    }
+                    if (client != null) {
+                        PrinterId printerId = (PrinterId) message.obj;
+                        try {
+                            client.requestPrinterUpdate(printerId);
+                        } catch (RemoteException re) {
+                            Log.e(LOG_TAG, "Error requesing printer update.", re);
+                        }
+                    }
+                } break;
+
                 case MSG_SET_CLIENT: {
-                    mClient = (IPrintSpoolerClient) message.obj;
-                    if (mClient != null) {
-                        PrintSpooler.createInstance(PrintSpoolerService.this);
-                        mHandler.sendEmptyMessageDelayed(
-                                MyHandler.MSG_CHECK_ALL_PRINTJOBS_HANDLED,
-                                CHECK_ALL_PRINTJOBS_HANDLED_DELAY);
-                    } else {
-                        PrintSpooler.destroyInstance();
+                    synchronized (mLock) {
+                        mClient = (IPrintSpoolerClient) message.obj;
+                        if (mClient != null) {
+                            Message msg = mHandlerCaller.obtainMessage(
+                                    HandlerCallerCallback.MSG_CHECK_ALL_PRINTJOBS_HANDLED);
+                            mHandlerCaller.sendMessageDelayed(msg,
+                                    CHECK_ALL_PRINTJOBS_HANDLED_DELAY);
+                        }
                     }
                 } break;
 
@@ -220,18 +416,6 @@
                     }
                 } break;
 
-                case MSG_CREATE_PRINTER_DISCOVERY_SESSION: {
-                    IPrinterDiscoverySessionObserver observer =
-                            (IPrinterDiscoverySessionObserver) message.obj;
-                    if (mClient != null) {
-                        try {
-                            mClient.createPrinterDiscoverySession(observer);
-                        } catch (RemoteException re) {
-                            Log.e(LOG_TAG, "Error creating printer discovery session.", re);
-                        }
-                    }
-                } break;
-
                 case MSG_ON_PRINT_JOB_QUEUED: {
                     PrintJobInfo printJob = (PrintJobInfo) message.obj;
                     if (mClient != null) {
@@ -266,12 +450,948 @@
                 } break;
 
                 case MSG_CHECK_ALL_PRINTJOBS_HANDLED: {
-                    PrintSpooler spooler = PrintSpooler.peekInstance();
-                    if (spooler != null) {
-                        spooler.checkAllPrintJobsHandled();
+                    checkAllPrintJobsHandled();
+                } break;
+
+                case MSG_ON_PRINTERS_ADDED: {
+                    final PrinterDiscoverySession session;
+                    synchronized (mLock) {
+                        session = mDiscoverySession;
+                    }
+                    if (session != null) {
+                        List<PrinterInfo> printers = (ArrayList<PrinterInfo>) message.obj;
+                        session.onPrintersAdded(printers);
+                    }
+                } break;
+
+                case MSG_ON_PRINTERS_REMOVED: {
+                    final PrinterDiscoverySession session;
+                    synchronized (mLock) {
+                        session = mDiscoverySession;
+                    }
+                    if (session != null) {
+                        List<PrinterId> printerIds = (ArrayList<PrinterId>) message.obj;
+                        session.onPrintersRemoved(printerIds);
+                    }
+                } break;
+
+                case MSG_ON_PRINTERS_UPDATED: {
+                    final PrinterDiscoverySession session;
+                    synchronized (mLock) {
+                        session = mDiscoverySession;
+                    }
+                    if (session != null) {
+                        List<PrinterInfo> printers = (ArrayList<PrinterInfo>) message.obj;
+                        session.onPrintersUpdated(printers);
                     }
                 } break;
             }
         }
     }
+
+    public List<PrintJobInfo> getPrintJobInfos(ComponentName componentName,
+            int state, int appId) {
+        List<PrintJobInfo> foundPrintJobs = null;
+        synchronized (mLock) {
+            final int printJobCount = mPrintJobs.size();
+            for (int i = 0; i < printJobCount; i++) {
+                PrintJobInfo printJob = mPrintJobs.get(i);
+                PrinterId printerId = printJob.getPrinterId();
+                final boolean sameComponent = (componentName == null
+                        || (printerId != null
+                        && componentName.equals(printerId.getServiceName())));
+                final boolean sameAppId = appId == PrintManager.APP_ID_ANY
+                        || printJob.getAppId() == appId;
+                final boolean sameState = (state == printJob.getState())
+                        || (state == PrintJobInfo.STATE_ANY)
+                        || (state == PrintJobInfo.STATE_ANY_VISIBLE_TO_CLIENTS
+                        && printJob.getState() > PrintJobInfo.STATE_CREATED);
+                if (sameComponent && sameAppId && sameState) {
+                    if (foundPrintJobs == null) {
+                        foundPrintJobs = new ArrayList<PrintJobInfo>();
+                    }
+                    foundPrintJobs.add(printJob);
+                }
+            }
+        }
+        return foundPrintJobs;
+    }
+
+    public PrintJobInfo getPrintJobInfo(int printJobId, int appId) {
+        synchronized (mLock) {
+            final int printJobCount = mPrintJobs.size();
+            for (int i = 0; i < printJobCount; i++) {
+                PrintJobInfo printJob = mPrintJobs.get(i);
+                if (printJob.getId() == printJobId
+                        && (appId == PrintManager.APP_ID_ANY
+                        || appId == printJob.getAppId())) {
+                    return printJob;
+                }
+            }
+            return null;
+        }
+    }
+
+    public PrintJobInfo createPrintJob(String label, IPrintClient client,
+            PrintAttributes attributes, int appId) {
+        synchronized (mLock) {
+            final int printJobId = generatePrintJobIdLocked();
+            PrintJobInfo printJob = new PrintJobInfo();
+            printJob.setId(printJobId);
+            printJob.setAppId(appId);
+            printJob.setLabel(label);
+            printJob.setAttributes(attributes);
+            printJob.setState(PrintJobInfo.STATE_CREATED);
+
+            addPrintJobLocked(printJob);
+
+            return printJob;
+        }
+    }
+
+    private void handleReadPrintJobsLocked() {
+        final int printJobCount = mPrintJobs.size();
+        for (int i = 0; i < printJobCount; i++) {
+            PrintJobInfo printJob = mPrintJobs.get(i);
+
+            // Update the notification.
+            mNotificationController.onPrintJobStateChanged(printJob);
+
+            switch (printJob.getState()) {
+                case PrintJobInfo.STATE_QUEUED:
+                case PrintJobInfo.STATE_STARTED: {
+                    // We have a print job that was queued or started in the
+                    // past
+                    // but the device battery died or a crash occurred. In this
+                    // case
+                    // we assume the print job failed and let the user decide
+                    // whether
+                    // to restart the job or just
+                    setPrintJobState(printJob.getId(), PrintJobInfo.STATE_FAILED,
+                            getString(R.string.no_connection_to_printer));
+                }
+                    break;
+            }
+        }
+    }
+
+    public void checkAllPrintJobsHandled() {
+        synchronized (mLock) {
+            if (!hasActivePrintJobsLocked()) {
+                notifyOnAllPrintJobsHandled();
+            }
+        }
+    }
+
+    private void setPrinterDiscoverySessionClient(PrinterDiscoverySession session) {
+        synchronized (mLock) {
+            mDiscoverySession = session;
+        }
+    }
+
+    private int generatePrintJobIdLocked() {
+        int printJobId = sPrintJobIdCounter++;
+        while (isDuplicatePrintJobId(printJobId)) {
+            printJobId = sPrintJobIdCounter++;
+        }
+        return printJobId;
+    }
+
+    private boolean isDuplicatePrintJobId(int printJobId) {
+        final int printJobCount = mPrintJobs.size();
+        for (int j = 0; j < printJobCount; j++) {
+            PrintJobInfo printJob = mPrintJobs.get(j);
+            if (printJob.getId() == printJobId) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    public void writePrintJobData(final ParcelFileDescriptor fd, final int printJobId) {
+        final PrintJobInfo printJob;
+        synchronized (mLock) {
+            printJob = getPrintJobInfo(printJobId, PrintManager.APP_ID_ANY);
+        }
+        new AsyncTask<Void, Void, Void>() {
+            @Override
+            protected Void doInBackground(Void... params) {
+                FileInputStream in = null;
+                FileOutputStream out = null;
+                try {
+                    if (printJob != null) {
+                        File file = generateFileForPrintJob(printJobId);
+                        in = new FileInputStream(file);
+                        out = new FileOutputStream(fd.getFileDescriptor());
+                    }
+                    final byte[] buffer = new byte[8192];
+                    while (true) {
+                        final int readByteCount = in.read(buffer);
+                        if (readByteCount < 0) {
+                            return null;
+                        }
+                        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);
+                    IoUtils.closeQuietly(fd);
+                }
+                Log.i(LOG_TAG, "[END WRITE]");
+                return null;
+            }
+        }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void[]) null);
+    }
+
+    public File generateFileForPrintJob(int printJobId) {
+        return new File(getFilesDir(), "print_job_"
+                + printJobId + "." + PRINT_FILE_EXTENSION);
+    }
+
+    private void addPrintJobLocked(PrintJobInfo printJob) {
+        mPrintJobs.add(printJob);
+        if (DEBUG_PRINT_JOB_LIFECYCLE) {
+            Slog.i(LOG_TAG, "[ADD] " + printJob);
+        }
+    }
+
+    private void removePrintJobLocked(PrintJobInfo printJob) {
+        if (mPrintJobs.remove(printJob)) {
+            generateFileForPrintJob(printJob.getId()).delete();
+            if (DEBUG_PRINT_JOB_LIFECYCLE) {
+                Slog.i(LOG_TAG, "[REMOVE] " + printJob);
+            }
+        }
+    }
+
+    public boolean setPrintJobState(int printJobId, int state, String error) {
+        boolean success = false;
+
+        synchronized (mLock) {
+            PrintJobInfo printJob = getPrintJobInfo(printJobId, PrintManager.APP_ID_ANY);
+            if (printJob != null) {
+                success = true;
+
+                printJob.setState(state);
+                printJob.setFailureReason(error);
+                mNotificationController.onPrintJobStateChanged(printJob);
+
+                if (DEBUG_PRINT_JOB_LIFECYCLE) {
+                    Slog.i(LOG_TAG, "[STATE CHANGED] " + printJob);
+                }
+
+                switch (state) {
+                    case PrintJobInfo.STATE_COMPLETED:
+                    case PrintJobInfo.STATE_CANCELED:
+                        removePrintJobLocked(printJob);
+                        // $fall-through$
+
+                    case PrintJobInfo.STATE_FAILED: {
+                        PrinterId printerId = printJob.getPrinterId();
+                        if (printerId != null) {
+                            ComponentName service = printerId.getServiceName();
+                            if (!hasActivePrintJobsForServiceLocked(service)) {
+                                sendOnAllPrintJobsForServiceHandled(service);
+                            }
+                        }
+                    } break;
+
+                    case PrintJobInfo.STATE_QUEUED: {
+                        sendOnPrintJobQueued(new PrintJobInfo(printJob));
+                    }  break;
+                }
+
+                if (shouldPersistPrintJob(printJob)) {
+                    mPersistanceManager.writeStateLocked();
+                }
+
+                if (!hasActivePrintJobsLocked()) {
+                    notifyOnAllPrintJobsHandled();
+                }
+            }
+        }
+
+        return success;
+    }
+
+    public boolean hasActivePrintJobsLocked() {
+        final int printJobCount = mPrintJobs.size();
+        for (int i = 0; i < printJobCount; i++) {
+            PrintJobInfo printJob = mPrintJobs.get(i);
+            if (isActiveState(printJob.getState())) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    public boolean hasActivePrintJobsForServiceLocked(ComponentName service) {
+        final int printJobCount = mPrintJobs.size();
+        for (int i = 0; i < printJobCount; i++) {
+            PrintJobInfo printJob = mPrintJobs.get(i);
+            if (isActiveState(printJob.getState())
+                    && printJob.getPrinterId().getServiceName().equals(service)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private boolean isActiveState(int printJobState) {
+        return printJobState == PrintJobInfo.STATE_CREATED
+                || printJobState == PrintJobInfo.STATE_QUEUED
+                || printJobState == PrintJobInfo.STATE_STARTED;
+    }
+
+    public boolean setPrintJobTag(int printJobId, String tag) {
+        synchronized (mLock) {
+            PrintJobInfo printJob = getPrintJobInfo(printJobId, PrintManager.APP_ID_ANY);
+            if (printJob != null) {
+                String printJobTag = printJob.getTag();
+                if (printJobTag == null) {
+                    if (tag == null) {
+                        return false;
+                    }
+                } else if (printJobTag.equals(tag)) {
+                    return false;
+                }
+                printJob.setTag(tag);
+                if (shouldPersistPrintJob(printJob)) {
+                    mPersistanceManager.writeStateLocked();
+                }
+                return true;
+            }
+        }
+        return false;
+    }
+
+    public void setPrintJobCopiesNoPersistence(int printJobId, int copies) {
+        synchronized (mLock) {
+            PrintJobInfo printJob = getPrintJobInfo(printJobId, PrintManager.APP_ID_ANY);
+            if (printJob != null) {
+                printJob.setCopies(copies);
+            }
+        }
+    }
+
+    public void setPrintJobPrintDocumentInfoNoPersistence(int printJobId, PrintDocumentInfo info) {
+        synchronized (mLock) {
+            PrintJobInfo printJob = getPrintJobInfo(printJobId, PrintManager.APP_ID_ANY);
+            if (printJob != null) {
+                printJob.setDocumentInfo(info);
+            }
+        }
+    }
+
+    public void setPrintJobAttributesNoPersistence(int printJobId, PrintAttributes attributes) {
+        synchronized (mLock) {
+            PrintJobInfo printJob = getPrintJobInfo(printJobId, PrintManager.APP_ID_ANY);
+            if (printJob != null) {
+                printJob.setAttributes(attributes);
+            }
+        }
+    }
+
+    public void setPrintJobPrinterNoPersistence(int 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(int printJobId, PageRange[] pages) {
+        synchronized (mLock) {
+            PrintJobInfo printJob = getPrintJobInfo(printJobId, PrintManager.APP_ID_ANY);
+            if (printJob != null) {
+                printJob.setPages(pages);
+            }
+        }
+    }
+
+    private boolean shouldPersistPrintJob(PrintJobInfo printJob) {
+        return printJob.getState() >= PrintJobInfo.STATE_QUEUED;
+    }
+
+    private void notifyOnAllPrintJobsHandled() {
+        // This has to run on the tread that is persisting the current state
+        // since this call may result in the system unbinding from the spooler
+        // and as a result the spooler process may get killed before the write
+        // completes.
+        new AsyncTask<Void, Void, Void>() {
+            @Override
+            protected Void doInBackground(Void... params) {
+                sendOnAllPrintJobsHandled();
+                return null;
+            }
+        }.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR, (Void[]) null);
+    }
+
+    private final class PersistenceManager {
+        private static final String PERSIST_FILE_NAME = "print_spooler_state.xml";
+
+        private static final String TAG_SPOOLER = "spooler";
+        private static final String TAG_JOB = "job";
+
+        private static final String TAG_PRINTER_ID = "printerId";
+        private static final String TAG_PAGE_RANGE = "pageRange";
+        private static final String TAG_ATTRIBUTES = "attributes";
+        private static final String TAG_DOCUMENT_INFO = "documentInfo";
+
+        private static final String ATTR_ID = "id";
+        private static final String ATTR_LABEL = "label";
+        private static final String ATTR_STATE = "state";
+        private static final String ATTR_APP_ID = "appId";
+        private static final String ATTR_USER_ID = "userId";
+        private static final String ATTR_TAG = "tag";
+        private static final String ATTR_COPIES = "copies";
+
+        private static final String TAG_MEDIA_SIZE = "mediaSize";
+        private static final String TAG_RESOLUTION = "resolution";
+        private static final String TAG_MARGINS = "margins";
+        private static final String TAG_INPUT_TRAY = "inputTray";
+        private static final String TAG_OUTPUT_TRAY = "outputTray";
+
+        private static final String ATTR_DUPLEX_MODE = "duplexMode";
+        private static final String ATTR_COLOR_MODE = "colorMode";
+        private static final String ATTR_FITTING_MODE = "fittingMode";
+        private static final String ATTR_ORIENTATION = "orientation";
+
+        private static final String ATTR_LOCAL_ID = "printerName";
+        private static final String ATTR_SERVICE_NAME = "serviceName";
+
+        private static final String ATTR_WIDTH_MILS = "widthMils";
+        private static final String ATTR_HEIGHT_MILS = "heightMils";
+
+        private static final String ATTR_HORIZONTAL_DPI = "horizontalDip";
+        private static final String ATTR_VERTICAL_DPI = "verticalDpi";
+
+        private static final String ATTR_LEFT_MILS = "leftMils";
+        private static final String ATTR_TOP_MILS = "topMils";
+        private static final String ATTR_RIGHT_MILS = "rightMils";
+        private static final String ATTR_BOTTOM_MILS = "bottomMils";
+
+        private static final String ATTR_START = "start";
+        private static final String ATTR_END = "end";
+
+        private static final String ATTR_NAME = "name";
+        private static final String ATTR_PAGE_COUNT = "pageCount";
+        private static final String ATTR_CONTENT_TYPE = "contentType";
+
+        private final AtomicFile mStatePersistFile;
+
+        private boolean mWriteStateScheduled;
+
+        private PersistenceManager() {
+            mStatePersistFile = new AtomicFile(new File(getFilesDir(),
+                    PERSIST_FILE_NAME));
+        }
+
+        public void writeStateLocked() {
+            if (!PERSISTNECE_MANAGER_ENABLED) {
+                return;
+            }
+            if (mWriteStateScheduled) {
+                return;
+            }
+            mWriteStateScheduled = true;
+            new AsyncTask<Void, Void, Void>() {
+                @Override
+                protected Void doInBackground(Void... params) {
+                    synchronized (mLock) {
+                        mWriteStateScheduled = false;
+                        doWriteStateLocked();
+                    }
+                    return null;
+                }
+            }.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR, (Void[]) null);
+        }
+
+        private void doWriteStateLocked() {
+            if (DEBUG_PERSISTENCE) {
+                Log.i(LOG_TAG, "[PERSIST START]");
+            }
+            FileOutputStream out = null;
+            try {
+                out = mStatePersistFile.startWrite();
+
+                XmlSerializer serializer = new FastXmlSerializer();
+                serializer.setOutput(out, "utf-8");
+                serializer.startDocument(null, true);
+                serializer.startTag(null, TAG_SPOOLER);
+
+                List<PrintJobInfo> printJobs = mPrintJobs;
+
+                final int printJobCount = printJobs.size();
+                for (int j = 0; j < printJobCount; j++) {
+                    PrintJobInfo printJob = printJobs.get(j);
+
+                    final int state = printJob.getState();
+                    if (state < PrintJobInfo.STATE_QUEUED
+                            || state > PrintJobInfo.STATE_CANCELED) {
+                        continue;
+                    }
+
+                    serializer.startTag(null, TAG_JOB);
+
+                    serializer.attribute(null, ATTR_ID, String.valueOf(printJob.getId()));
+                    serializer.attribute(null, ATTR_LABEL, printJob.getLabel().toString());
+                    serializer.attribute(null, ATTR_STATE, String.valueOf(printJob.getState()));
+                    serializer.attribute(null, ATTR_APP_ID, String.valueOf(printJob.getAppId()));
+                    serializer.attribute(null, ATTR_USER_ID, String.valueOf(printJob.getUserId()));
+                    String tag = printJob.getTag();
+                    if (tag != null) {
+                        serializer.attribute(null, ATTR_TAG, tag);
+                    }
+                    serializer.attribute(null, ATTR_COPIES, String.valueOf(printJob.getCopies()));
+
+                    PrinterId printerId = printJob.getPrinterId();
+                    if (printerId != null) {
+                        serializer.startTag(null, TAG_PRINTER_ID);
+                        serializer.attribute(null, ATTR_LOCAL_ID, printerId.getLocalId());
+                        serializer.attribute(null, ATTR_SERVICE_NAME, printerId.getServiceName()
+                                .flattenToString());
+                        serializer.endTag(null, TAG_PRINTER_ID);
+                    }
+
+                    PageRange[] pages = printJob.getPages();
+                    if (pages != null) {
+                        for (int i = 0; i < pages.length; i++) {
+                            serializer.startTag(null, TAG_PAGE_RANGE);
+                            serializer.attribute(null, ATTR_START, String.valueOf(
+                                    pages[i].getStart()));
+                            serializer.attribute(null, ATTR_END, String.valueOf(
+                                    pages[i].getEnd()));
+                            serializer.endTag(null, TAG_PAGE_RANGE);
+                        }
+                    }
+
+                    PrintAttributes attributes = printJob.getAttributes();
+                    if (attributes != null) {
+                        serializer.startTag(null, TAG_ATTRIBUTES);
+
+                        final int duplexMode = attributes.getDuplexMode();
+                        serializer.attribute(null, ATTR_DUPLEX_MODE,
+                                String.valueOf(duplexMode));
+
+                        final int colorMode = attributes.getColorMode();
+                        serializer.attribute(null, ATTR_COLOR_MODE,
+                                String.valueOf(colorMode));
+
+                        final int fittingMode = attributes.getFittingMode();
+                        serializer.attribute(null, ATTR_FITTING_MODE,
+                                String.valueOf(fittingMode));
+
+                        final int orientation = attributes.getOrientation();
+                        serializer.attribute(null, ATTR_ORIENTATION,
+                                String.valueOf(orientation));
+
+                        MediaSize mediaSize = attributes.getMediaSize();
+                        if (mediaSize != null) {
+                            serializer.startTag(null, TAG_MEDIA_SIZE);
+                            serializer.attribute(null, ATTR_ID, mediaSize.getId());
+                            serializer.attribute(null, ATTR_LABEL, mediaSize.getLabel()
+                                    .toString());
+                            serializer.attribute(null, ATTR_WIDTH_MILS, String.valueOf(
+                                    mediaSize.getWidthMils()));
+                            serializer.attribute(null, ATTR_HEIGHT_MILS, String.valueOf(
+                                    mediaSize.getHeightMils()));
+                            serializer.endTag(null, TAG_MEDIA_SIZE);
+                        }
+
+                        Resolution resolution = attributes.getResolution();
+                        if (resolution != null) {
+                            serializer.startTag(null, TAG_RESOLUTION);
+                            serializer.attribute(null, ATTR_ID, resolution.getId());
+                            serializer.attribute(null, ATTR_LABEL, resolution.getLabel()
+                                    .toString());
+                            serializer.attribute(null, ATTR_HORIZONTAL_DPI, String.valueOf(
+                                    resolution.getHorizontalDpi()));
+                            serializer.attribute(null, ATTR_VERTICAL_DPI, String.valueOf(
+                                    resolution.getVerticalDpi()));
+                            serializer.endTag(null, TAG_RESOLUTION);
+                        }
+
+                        Margins margins = attributes.getMargins();
+                        if (margins != null) {
+                            serializer.startTag(null, TAG_MARGINS);
+                            serializer.attribute(null, ATTR_LEFT_MILS, String.valueOf(
+                                    margins.getLeftMils()));
+                            serializer.attribute(null, ATTR_TOP_MILS, String.valueOf(
+                                    margins.getTopMils()));
+                            serializer.attribute(null, ATTR_RIGHT_MILS, String.valueOf(
+                                    margins.getRightMils()));
+                            serializer.attribute(null, ATTR_BOTTOM_MILS, String.valueOf(
+                                    margins.getBottomMils()));
+                            serializer.endTag(null, TAG_MARGINS);
+                        }
+
+                        Tray inputTray = attributes.getInputTray();
+                        if (inputTray != null) {
+                            serializer.startTag(null, TAG_INPUT_TRAY);
+                            serializer.attribute(null, ATTR_ID, inputTray.getId());
+                            serializer.attribute(null, ATTR_LABEL, inputTray.getLabel()
+                                    .toString());
+                            serializer.endTag(null, TAG_INPUT_TRAY);
+                        }
+
+                        Tray outputTray = attributes.getOutputTray();
+                        if (outputTray != null) {
+                            serializer.startTag(null, TAG_OUTPUT_TRAY);
+                            serializer.attribute(null, ATTR_ID, outputTray.getId());
+                            serializer.attribute(null, ATTR_LABEL, outputTray.getLabel()
+                                    .toString());
+                            serializer.endTag(null, TAG_OUTPUT_TRAY);
+                        }
+
+                        serializer.endTag(null, TAG_ATTRIBUTES);
+                    }
+
+                    PrintDocumentInfo documentInfo = printJob.getDocumentInfo();
+                    if (documentInfo != null) {
+                        serializer.startTag(null, TAG_DOCUMENT_INFO);
+                        serializer.attribute(null, ATTR_NAME, documentInfo.getName());
+                        serializer.attribute(null, ATTR_CONTENT_TYPE, String.valueOf(
+                                documentInfo.getContentType()));
+                        serializer.attribute(null, ATTR_PAGE_COUNT, String.valueOf(
+                                documentInfo.getPageCount()));
+                        serializer.endTag(null, TAG_DOCUMENT_INFO);
+                    }
+
+                    serializer.endTag(null, TAG_JOB);
+
+                    if (DEBUG_PERSISTENCE) {
+                        Log.i(LOG_TAG, "[PERSISTED] " + printJob);
+                    }
+                }
+
+                serializer.endTag(null, TAG_SPOOLER);
+                serializer.endDocument();
+                mStatePersistFile.finishWrite(out);
+                if (DEBUG_PERSISTENCE) {
+                    Log.i(LOG_TAG, "[PERSIST END]");
+                }
+            } catch (IOException e) {
+                Slog.w(LOG_TAG, "Failed to write state, restoring backup.", e);
+                mStatePersistFile.failWrite(out);
+            } finally {
+                IoUtils.closeQuietly(out);
+            }
+        }
+
+        public void readStateLocked() {
+            if (!PERSISTNECE_MANAGER_ENABLED) {
+                return;
+            }
+            FileInputStream in = null;
+            try {
+                in = mStatePersistFile.openRead();
+            } catch (FileNotFoundException e) {
+                Log.i(LOG_TAG, "No existing print spooler state.");
+                return;
+            }
+            try {
+                XmlPullParser parser = Xml.newPullParser();
+                parser.setInput(in, null);
+                parseState(parser);
+            } catch (IllegalStateException ise) {
+                Slog.w(LOG_TAG, "Failed parsing ", ise);
+            } catch (NullPointerException npe) {
+                Slog.w(LOG_TAG, "Failed parsing ", npe);
+            } catch (NumberFormatException nfe) {
+                Slog.w(LOG_TAG, "Failed parsing ", nfe);
+            } catch (XmlPullParserException xppe) {
+                Slog.w(LOG_TAG, "Failed parsing ", xppe);
+            } catch (IOException ioe) {
+                Slog.w(LOG_TAG, "Failed parsing ", ioe);
+            } catch (IndexOutOfBoundsException iobe) {
+                Slog.w(LOG_TAG, "Failed parsing ", iobe);
+            } finally {
+                IoUtils.closeQuietly(in);
+            }
+        }
+
+        private void parseState(XmlPullParser parser)
+                throws IOException, XmlPullParserException {
+            parser.next();
+            skipEmptyTextTags(parser);
+            expect(parser, XmlPullParser.START_TAG, TAG_SPOOLER);
+            parser.next();
+
+            while (parsePrintJob(parser)) {
+                parser.next();
+            }
+
+            skipEmptyTextTags(parser);
+            expect(parser, XmlPullParser.END_TAG, TAG_SPOOLER);
+        }
+
+        private boolean parsePrintJob(XmlPullParser parser)
+                throws IOException, XmlPullParserException {
+            skipEmptyTextTags(parser);
+            if (!accept(parser, XmlPullParser.START_TAG, TAG_JOB)) {
+                return false;
+            }
+
+            PrintJobInfo printJob = new PrintJobInfo();
+
+            final int printJobId = Integer.parseInt(parser.getAttributeValue(null, ATTR_ID));
+            printJob.setId(printJobId);
+            String label = parser.getAttributeValue(null, ATTR_LABEL);
+            printJob.setLabel(label);
+            final int state = Integer.parseInt(parser.getAttributeValue(null, ATTR_STATE));
+            printJob.setState(state);
+            final int appId = Integer.parseInt(parser.getAttributeValue(null, ATTR_APP_ID));
+            printJob.setAppId(appId);
+            final int userId = Integer.parseInt(parser.getAttributeValue(null, ATTR_USER_ID));
+            printJob.setUserId(userId);
+            String tag = parser.getAttributeValue(null, ATTR_TAG);
+            printJob.setTag(tag);
+            String copies = parser.getAttributeValue(null, ATTR_COPIES);
+            printJob.setCopies(Integer.parseInt(copies));
+
+            parser.next();
+
+            skipEmptyTextTags(parser);
+            if (accept(parser, XmlPullParser.START_TAG, TAG_PRINTER_ID)) {
+                String localId = parser.getAttributeValue(null, ATTR_LOCAL_ID);
+                ComponentName service = ComponentName.unflattenFromString(parser.getAttributeValue(
+                        null, ATTR_SERVICE_NAME));
+                printJob.setPrinterId(new PrinterId(service, localId));
+                parser.next();
+                skipEmptyTextTags(parser);
+                expect(parser, XmlPullParser.END_TAG, TAG_PRINTER_ID);
+                parser.next();
+            }
+
+            skipEmptyTextTags(parser);
+            List<PageRange> pageRanges = null;
+            while (accept(parser, XmlPullParser.START_TAG, TAG_PAGE_RANGE)) {
+                final int start = Integer.parseInt(parser.getAttributeValue(null, ATTR_START));
+                final int end = Integer.parseInt(parser.getAttributeValue(null, ATTR_END));
+                PageRange pageRange = new PageRange(start, end);
+                if (pageRanges == null) {
+                    pageRanges = new ArrayList<PageRange>();
+                }
+                pageRanges.add(pageRange);
+                parser.next();
+                skipEmptyTextTags(parser);
+                expect(parser, XmlPullParser.END_TAG, TAG_PAGE_RANGE);
+                parser.next();
+            }
+            if (pageRanges != null) {
+                PageRange[] pageRangesArray = new PageRange[pageRanges.size()];
+                pageRanges.toArray(pageRangesArray);
+                printJob.setPages(pageRangesArray);
+            }
+
+            skipEmptyTextTags(parser);
+            if (accept(parser, XmlPullParser.START_TAG, TAG_ATTRIBUTES)) {
+
+                PrintAttributes.Builder builder = new PrintAttributes.Builder();
+
+                String duplexMode = parser.getAttributeValue(null, ATTR_DUPLEX_MODE);
+                builder.setDuplexMode(Integer.parseInt(duplexMode));
+
+                String colorMode = parser.getAttributeValue(null, ATTR_COLOR_MODE);
+                builder.setColorMode(Integer.parseInt(colorMode));
+
+                String fittingMode = parser.getAttributeValue(null, ATTR_FITTING_MODE);
+                builder.setFittingMode(Integer.parseInt(fittingMode));
+
+                String orientation = parser.getAttributeValue(null, ATTR_ORIENTATION);
+                builder.setOrientation(Integer.parseInt(orientation));
+
+                parser.next();
+
+                skipEmptyTextTags(parser);
+                if (accept(parser, XmlPullParser.START_TAG, TAG_MEDIA_SIZE)) {
+                    String id = parser.getAttributeValue(null, ATTR_ID);
+                    label = parser.getAttributeValue(null, ATTR_LABEL);
+                    final int widthMils = Integer.parseInt(parser.getAttributeValue(null,
+                            ATTR_WIDTH_MILS));
+                    final int heightMils = Integer.parseInt(parser.getAttributeValue(null,
+                            ATTR_HEIGHT_MILS));
+                    MediaSize mediaSize = new MediaSize(id, label, widthMils, heightMils);
+                    builder.setMediaSize(mediaSize);
+                    parser.next();
+                    skipEmptyTextTags(parser);
+                    expect(parser, XmlPullParser.END_TAG, TAG_MEDIA_SIZE);
+                    parser.next();
+                }
+
+                skipEmptyTextTags(parser);
+                if (accept(parser, XmlPullParser.START_TAG, TAG_RESOLUTION)) {
+                    String id = parser.getAttributeValue(null, ATTR_ID);
+                    label = parser.getAttributeValue(null, ATTR_LABEL);
+                    final int horizontalDpi = Integer.parseInt(parser.getAttributeValue(null,
+                            ATTR_HORIZONTAL_DPI));
+                    final int verticalDpi = Integer.parseInt(parser.getAttributeValue(null,
+                            ATTR_VERTICAL_DPI));
+                    Resolution resolution = new Resolution(id, label, horizontalDpi, verticalDpi);
+                    builder.setResolution(resolution);
+                    parser.next();
+                    skipEmptyTextTags(parser);
+                    expect(parser, XmlPullParser.END_TAG, TAG_RESOLUTION);
+                    parser.next();
+                }
+
+                skipEmptyTextTags(parser);
+                if (accept(parser, XmlPullParser.START_TAG, TAG_MARGINS)) {
+                    final int leftMils = Integer.parseInt(parser.getAttributeValue(null,
+                            ATTR_LEFT_MILS));
+                    final int topMils = Integer.parseInt(parser.getAttributeValue(null,
+                            ATTR_TOP_MILS));
+                    final int rightMils = Integer.parseInt(parser.getAttributeValue(null,
+                            ATTR_RIGHT_MILS));
+                    final int bottomMils = Integer.parseInt(parser.getAttributeValue(null,
+                            ATTR_BOTTOM_MILS));
+                    Margins margins = new Margins(leftMils, topMils, rightMils, bottomMils);
+                    builder.setMargins(margins);
+                    parser.next();
+                    skipEmptyTextTags(parser);
+                    expect(parser, XmlPullParser.END_TAG, TAG_MARGINS);
+                    parser.next();
+                }
+
+                skipEmptyTextTags(parser);
+                if (accept(parser, XmlPullParser.START_TAG, TAG_INPUT_TRAY)) {
+                    String id = parser.getAttributeValue(null, ATTR_ID);
+                    label = parser.getAttributeValue(null, ATTR_LABEL);
+                    Tray tray = new Tray(id, label);
+                    builder.setInputTray(tray);
+                    parser.next();
+                    skipEmptyTextTags(parser);
+                    expect(parser, XmlPullParser.END_TAG, TAG_INPUT_TRAY);
+                    parser.next();
+                }
+
+                skipEmptyTextTags(parser);
+                if (accept(parser, XmlPullParser.START_TAG, TAG_OUTPUT_TRAY)) {
+                    String id = parser.getAttributeValue(null, ATTR_ID);
+                    label = parser.getAttributeValue(null, ATTR_LABEL);
+                    Tray tray = new Tray(id, label);
+                    builder.setOutputTray(tray);
+                    parser.next();
+                    skipEmptyTextTags(parser);
+                    expect(parser, XmlPullParser.END_TAG, TAG_OUTPUT_TRAY);
+                    parser.next();
+                }
+
+                printJob.setAttributes(builder.create());
+
+                skipEmptyTextTags(parser);
+                expect(parser, XmlPullParser.END_TAG, TAG_ATTRIBUTES);
+                parser.next();
+            }
+
+            skipEmptyTextTags(parser);
+            if (accept(parser, XmlPullParser.START_TAG, TAG_DOCUMENT_INFO)) {
+                String name = parser.getAttributeValue(null, ATTR_NAME);
+                final int pageCount = Integer.parseInt(parser.getAttributeValue(null,
+                        ATTR_PAGE_COUNT));
+                final int contentType = Integer.parseInt(parser.getAttributeValue(null,
+                        ATTR_CONTENT_TYPE));
+                PrintDocumentInfo info = new PrintDocumentInfo.Builder(name)
+                        .setPageCount(pageCount)
+                        .setContentType(contentType).create();
+                printJob.setDocumentInfo(info);
+                parser.next();
+                skipEmptyTextTags(parser);
+                expect(parser, XmlPullParser.END_TAG, TAG_DOCUMENT_INFO);
+                parser.next();
+            }
+
+            mPrintJobs.add(printJob);
+
+            if (DEBUG_PERSISTENCE) {
+                Log.i(LOG_TAG, "[RESTORED] " + printJob);
+            }
+
+            skipEmptyTextTags(parser);
+            expect(parser, XmlPullParser.END_TAG, TAG_JOB);
+
+            return true;
+        }
+
+        private void expect(XmlPullParser parser, int type, String tag)
+                throws IOException, XmlPullParserException {
+            if (!accept(parser, type, tag)) {
+                throw new XmlPullParserException("Exepected event: " + type
+                        + " and tag: " + tag + " but got event: " + parser.getEventType()
+                        + " and tag:" + parser.getName());
+            }
+        }
+
+        private void skipEmptyTextTags(XmlPullParser parser)
+                throws IOException, XmlPullParserException {
+            while (accept(parser, XmlPullParser.TEXT, null)
+                    && "\n".equals(parser.getText())) {
+                parser.next();
+            }
+        }
+
+        private boolean accept(XmlPullParser parser, int type, String tag)
+                throws IOException, XmlPullParserException {
+            if (parser.getEventType() != type) {
+                return false;
+            }
+            if (tag != null) {
+                if (!tag.equals(parser.getName())) {
+                    return false;
+                }
+            } else if (parser.getName() != null) {
+                return false;
+            }
+            return true;
+        }
+    }
+
+    public static abstract class PrinterDiscoverySession {
+
+        private PrintSpoolerService mService;
+
+        private boolean mIsStarted;
+
+        public PrinterDiscoverySession() {
+            mService = PrintSpoolerService.peekInstance();
+            mService.createPrinterDiscoverySession();
+            mService.setPrinterDiscoverySessionClient(this);
+        }
+
+        public final void startPrinterDisovery(List<PrinterId> priorityList) {
+            mIsStarted = true;
+            mService.startPrinterDiscovery(priorityList);
+        }
+
+        public final void stopPrinterDiscovery() {
+            mIsStarted = false;
+            mService.stopPrinterDiscovery();
+        }
+
+        public void requestPrinterUpdated(PrinterId printerId) {
+            mService.requestPrinterUpdate(printerId);
+        }
+
+        public final void destroy() {
+            mService.setPrinterDiscoverySessionClient(null);
+            mService.destroyPrinterDiscoverySession();
+        }
+
+        public final boolean isStarted() {
+            return mIsStarted;
+        }
+
+        public abstract void onPrintersAdded(List<PrinterInfo> printers);
+
+        public abstract void onPrintersRemoved(List<PrinterId> printerIds);
+
+        public abstract void onPrintersUpdated(List<PrinterInfo> printers);
+    }
 }
diff --git a/packages/PrintSpooler/src/com/android/printspooler/SelectPrinterActivity.java b/packages/PrintSpooler/src/com/android/printspooler/SelectPrinterActivity.java
new file mode 100644
index 0000000..141dbd1
--- /dev/null
+++ b/packages/PrintSpooler/src/com/android/printspooler/SelectPrinterActivity.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package 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/SelectPrinterFragment.java b/packages/PrintSpooler/src/com/android/printspooler/SelectPrinterFragment.java
new file mode 100644
index 0000000..9ca3a86
--- /dev/null
+++ b/packages/PrintSpooler/src/com/android/printspooler/SelectPrinterFragment.java
@@ -0,0 +1,401 @@
+/*
+ * 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.AlertDialog;
+import android.app.Dialog;
+import android.app.DialogFragment;
+import android.app.Fragment;
+import android.app.FragmentTransaction;
+import android.app.ListFragment;
+import android.app.LoaderManager;
+import android.content.ComponentName;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.Loader;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.pm.ResolveInfo;
+import android.net.Uri;
+import android.os.Bundle;
+import android.print.PrinterId;
+import android.print.PrinterInfo;
+import android.printservice.PrintServiceInfo;
+import android.text.TextUtils;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.BaseAdapter;
+import android.widget.Filter;
+import android.widget.Filterable;
+import android.widget.ListView;
+import android.widget.SearchView;
+import android.widget.TextView;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * This is a fragment for selecting a printer.
+ */
+public final class SelectPrinterFragment extends ListFragment {
+
+    private static final int LOADER_ID_PRINTERS_LOADER = 1;
+
+    private static final String FRAGMRNT_TAG_ADD_PRINTER_DIALOG =
+            "FRAGMRNT_TAG_ADD_PRINTER_DIALOG";
+
+    private static final String FRAGMRNT_ARGUMENT_PRINT_SERVICE_INFOS =
+            "FRAGMRNT_ARGUMENT_PRINT_SERVICE_INFOS";
+
+    private final ArrayList<PrintServiceInfo> mAddPrinterServices =
+            new ArrayList<PrintServiceInfo>();
+
+    public static interface OnPrinterSelectedListener {
+        public void onPrinterSelected(PrinterId printerId);
+    }
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setHasOptionsMenu(true);
+    }
+
+    @Override
+    public void onActivityCreated(Bundle savedInstanceState) {
+        super.onActivityCreated(savedInstanceState);
+        setListAdapter(new DestinationAdapter());
+    }
+
+    @Override
+    public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
+        super.onCreateOptionsMenu(menu, inflater);
+        inflater.inflate(R.menu.select_printer_activity, menu);
+
+        MenuItem searchItem = menu.findItem(R.id.action_search);
+        SearchView searchView = (SearchView) searchItem.getActionView();
+        searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
+            @Override
+            public boolean onQueryTextSubmit(String query) {
+                return true;
+            }
+
+            @Override
+            public boolean onQueryTextChange(String searchString) {
+                ((DestinationAdapter) getListAdapter()).getFilter().filter(searchString);
+                return true;
+            }
+        });
+
+        if (mAddPrinterServices.isEmpty()) {
+            menu.removeItem(R.id.action_add_printer);
+        }
+    }
+
+    @Override
+    public void onResume() {
+        updateAddPrintersAdapter();
+        getActivity().invalidateOptionsMenu();
+        super.onResume();
+    }
+
+    @Override
+    public void onListItemClick(ListView list, View view, int position, long id) {
+        PrinterInfo printer = (PrinterInfo) list.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");
+        }
+    }
+
+    @Override
+    public boolean onOptionsItemSelected(MenuItem item) {
+        if (item.getItemId() == R.id.action_add_printer) {
+            showAddPrinterSelectionDialog();
+            return true;
+        }
+        return super.onOptionsItemSelected(item);
+    }
+
+    private void updateAddPrintersAdapter() {
+        mAddPrinterServices.clear();
+
+        // Get all print services.
+        List<ResolveInfo> resolveInfos = getActivity().getPackageManager().queryIntentServices(
+                new Intent(android.printservice.PrintService.SERVICE_INTERFACE),
+                PackageManager.GET_SERVICES | PackageManager.GET_META_DATA);
+
+        // No print services - done.
+        if (resolveInfos.isEmpty()) {
+            return;
+        }
+
+        // Find the services with valid add printers activities.
+        final int resolveInfoCount = resolveInfos.size();
+        for (int i = 0; i < resolveInfoCount; i++) {
+            ResolveInfo resolveInfo = resolveInfos.get(i);
+
+            PrintServiceInfo printServiceInfo = PrintServiceInfo.create(
+                    resolveInfo, getActivity());
+            String addPrintersActivity = printServiceInfo.getAddPrintersActivityName();
+
+            // No add printers activity declared - done.
+            if (TextUtils.isEmpty(addPrintersActivity)) {
+                continue;
+            }
+
+            ComponentName addPrintersComponentName = new ComponentName(
+                    resolveInfo.serviceInfo.packageName,
+                    addPrintersActivity);
+            Intent addPritnersIntent = new Intent(Intent.ACTION_MAIN)
+                .setComponent(addPrintersComponentName);
+
+            // The add printers activity is valid - add it.
+            if (!getActivity().getPackageManager().queryIntentActivities(
+                    addPritnersIntent, 0).isEmpty()) {
+                mAddPrinterServices.add(printServiceInfo);
+            }
+        }
+    }
+
+    private void showAddPrinterSelectionDialog() {
+        FragmentTransaction transaction = getFragmentManager().beginTransaction();
+        Fragment oldFragment = getFragmentManager().findFragmentByTag(
+                FRAGMRNT_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,
+                mAddPrinterServices);
+        newFragment.setArguments(arguments);
+        transaction.add(newFragment, FRAGMRNT_TAG_ADD_PRINTER_DIALOG);
+        transaction.commit();
+    }
+
+    public static class AddPrinterAlertDialogFragment extends DialogFragment {
+
+        private static final String DEFAULT_MARKET_QUERY_STRING =
+                "market://search?q=print";
+
+        @Override
+        @SuppressWarnings("unchecked")
+        public Dialog onCreateDialog(Bundle savedInstanceState) {
+            AlertDialog.Builder builder = new AlertDialog.Builder(getActivity())
+                    .setTitle(R.string.choose_print_service);
+
+            final List<PrintServiceInfo> printServices = (List<PrintServiceInfo>) (List<?>)
+                    getArguments().getParcelableArrayList(FRAGMRNT_ARGUMENT_PRINT_SERVICE_INFOS);
+
+            ArrayAdapter<String> adapter = new ArrayAdapter<String>(getActivity(),
+                    android.R.layout.simple_list_item_1);
+            final int printServiceCount = printServices.size();
+            for (int i = 0; i < printServiceCount; i++) {
+                PrintServiceInfo printService = printServices.get(i);
+                adapter.add(printService.getResolveInfo().loadLabel(
+                        getActivity().getPackageManager()).toString());
+            }
+
+            builder.setAdapter(adapter, new DialogInterface.OnClickListener() {
+                @Override
+                public void onClick(DialogInterface dialog, int which) {
+                    PrintServiceInfo printService = printServices.get(which);
+                    ComponentName componentName = new ComponentName(
+                            printService.getResolveInfo().serviceInfo.packageName,
+                            printService.getAddPrintersActivityName());
+                    Intent intent = new Intent(Intent.ACTION_MAIN);
+                    intent.setComponent(componentName);
+                    startActivity(intent);
+                }
+            });
+
+            Uri marketUri = Uri.parse(DEFAULT_MARKET_QUERY_STRING);
+            final Intent marketIntent = new Intent(Intent.ACTION_VIEW, marketUri);
+            if (getActivity().getPackageManager().resolveActivity(marketIntent, 0) != null) {
+                builder.setPositiveButton(R.string.search_play_store,
+                    new DialogInterface.OnClickListener() {
+                        public void onClick(DialogInterface dialog, int whichButton) {
+                            startActivity(marketIntent);
+                        }
+                    });
+            }
+
+            return builder.create();
+        }
+    }
+
+    private final class DestinationAdapter extends BaseAdapter
+            implements LoaderManager.LoaderCallbacks<List<PrinterInfo>>, Filterable {
+
+        private final Object mLock = new Object();
+
+        private final List<PrinterInfo> mPrinters = new ArrayList<PrinterInfo>();
+
+        private final List<PrinterInfo> mFilteredPrinters = new ArrayList<PrinterInfo>();
+
+        private CharSequence mLastSearchString;
+
+        public DestinationAdapter() {
+            getLoaderManager().initLoader(LOADER_ID_PRINTERS_LOADER, null, this);
+        }
+
+        @Override
+        public Filter getFilter() {
+            return new Filter() {
+                @Override
+                protected FilterResults performFiltering(CharSequence constraint) {
+                    synchronized (mLock) {
+                        if (TextUtils.isEmpty(constraint)) {
+                            return null;
+                        }
+                        FilterResults results = new FilterResults();
+                        List<PrinterInfo> filteredPrinters = new ArrayList<PrinterInfo>();
+                        String constraintLowerCase = constraint.toString().toLowerCase();
+                        final int printerCount = mPrinters.size();
+                        for (int i = 0; i < printerCount; i++) {
+                            PrinterInfo printer = mPrinters.get(i);
+                            if (printer.getName().toLowerCase().contains(constraintLowerCase)) {
+                                filteredPrinters.add(printer);
+                            }
+                        }
+                        results.values = filteredPrinters;
+                        results.count = filteredPrinters.size();
+                        return results;
+                    }
+                }
+
+                @Override
+                @SuppressWarnings("unchecked")
+                protected void publishResults(CharSequence constraint, FilterResults results) {
+                    synchronized (mLock) {
+                        mLastSearchString = constraint;
+                        mFilteredPrinters.clear();
+                        if (results == null) {
+                            mFilteredPrinters.addAll(mPrinters);
+                        } else {
+                            List<PrinterInfo> printers = (List<PrinterInfo>) results.values;
+                            mFilteredPrinters.addAll(printers);
+                        }
+                    }
+                    notifyDataSetChanged();
+                }
+            };
+        }
+
+        @Override
+        public int getCount() {
+            synchronized (mLock) {
+                return mFilteredPrinters.size();
+            }
+        }
+
+        @Override
+        public Object getItem(int position) {
+            synchronized (mLock) {
+                return mFilteredPrinters.get(position);
+            }
+        }
+
+        @Override
+        public long getItemId(int position) {
+            return position;
+        }
+
+        @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 = getActivity().getLayoutInflater().inflate(
+                        R.layout.spinner_dropdown_item, parent, false);
+            }
+
+            CharSequence title = null;
+            CharSequence subtitle = null;
+
+            PrinterInfo printer = (PrinterInfo) getItem(position);
+            title = printer.getName();
+            try {
+                PackageManager pm = getActivity().getPackageManager();
+                PackageInfo packageInfo = pm.getPackageInfo(printer.getId()
+                        .getServiceName().getPackageName(), 0);
+                subtitle = packageInfo.applicationInfo.loadLabel(pm);
+            } 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);
+            }
+
+            return convertView;
+        }
+
+        @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();
+        }
+    }
+}
diff --git a/packages/services/Proxy/AndroidManifest.xml b/packages/services/Proxy/AndroidManifest.xml
index 02475c0..09b8327 100644
--- a/packages/services/Proxy/AndroidManifest.xml
+++ b/packages/services/Proxy/AndroidManifest.xml
@@ -6,7 +6,6 @@
     <uses-permission android:name="android.permission.INTERNET" />
 
     <application
-        android:persistent="true"
         android:label="@string/app_label"
         android:process="com.android.proxyhandler">
 
diff --git a/packages/services/Proxy/src/com/android/proxyhandler/ProxyService.java b/packages/services/Proxy/src/com/android/proxyhandler/ProxyService.java
index 0aea5ee1..18ed645 100644
--- a/packages/services/Proxy/src/com/android/proxyhandler/ProxyService.java
+++ b/packages/services/Proxy/src/com/android/proxyhandler/ProxyService.java
@@ -24,24 +24,28 @@
     @Override
     public int onStartCommand(Intent intent, int flags, int startId) {
         if (intent != null) {
-            handleCommand(intent);
+            if (handleCommand(intent)) {
+                return START_REDELIVER_INTENT;
+            }
         }
-        return START_STICKY;
+        return START_NOT_STICKY;
     }
 
-    private void handleCommand(Intent intent) {
+    private boolean handleCommand(Intent intent) {
         Bundle bundle = intent.getExtras();
         ProxyProperties proxy = null;
         if ((bundle != null) && bundle.containsKey(Proxy.EXTRA_PROXY_INFO)) {
             proxy = bundle.getParcelable(Proxy.EXTRA_PROXY_INFO);
             if ((proxy != null) && !TextUtils.isEmpty(proxy.getPacFileUrl())) {
                 startProxy(proxy);
+                return true;
             } else {
                 stopSelf();
             }
         } else {
             stopSelf();
         }
+        return false;
     }
 
 
diff --git a/packages/services/Proxy/src/com/android/proxyhandler/ProxyServiceReceiver.java b/packages/services/Proxy/src/com/android/proxyhandler/ProxyServiceReceiver.java
index f5c2ca5..4638def 100644
--- a/packages/services/Proxy/src/com/android/proxyhandler/ProxyServiceReceiver.java
+++ b/packages/services/Proxy/src/com/android/proxyhandler/ProxyServiceReceiver.java
@@ -4,7 +4,9 @@
 import android.content.Context;
 import android.content.Intent;
 import android.net.Proxy;
+import android.net.ProxyProperties;
 import android.os.Bundle;
+import android.text.TextUtils;
 
 public class ProxyServiceReceiver extends BroadcastReceiver {
 
@@ -12,11 +14,16 @@
     public void onReceive(Context context, Intent intent) {
         Intent service = new Intent(context, ProxyService.class);
         Bundle bundle = intent.getExtras();
+        ProxyProperties proxy = null;
         if (bundle != null) {
-            service.putExtra(Proxy.EXTRA_PROXY_INFO,
-                    bundle.getParcelable(Proxy.EXTRA_PROXY_INFO));
+            proxy = bundle.getParcelable(Proxy.EXTRA_PROXY_INFO);
+            service.putExtra(Proxy.EXTRA_PROXY_INFO, proxy);
         }
-        context.startService(service);
+        if ((proxy != null) && (!TextUtils.isEmpty(proxy.getPacFileUrl()))) {
+            context.startService(service);
+        } else {
+            context.stopService(service);
+        }
     }
 
 }
diff --git a/services/java/Android.mk b/services/java/Android.mk
index 95b28d9..8c3d0f0 100644
--- a/services/java/Android.mk
+++ b/services/java/Android.mk
@@ -11,7 +11,7 @@
 
 LOCAL_MODULE:= services
 
-LOCAL_JAVA_LIBRARIES := android.policy telephony-common
+LOCAL_JAVA_LIBRARIES := android.policy conscrypt telephony-common
 
 include $(BUILD_JAVA_LIBRARY)
 
diff --git a/services/java/com/android/server/DevicePolicyManagerService.java b/services/java/com/android/server/DevicePolicyManagerService.java
index 43f95c3..7e83396 100644
--- a/services/java/com/android/server/DevicePolicyManagerService.java
+++ b/services/java/com/android/server/DevicePolicyManagerService.java
@@ -16,11 +16,14 @@
 
 package com.android.server;
 
+import static android.Manifest.permission.MANAGE_CA_CERTIFICATES;
+
 import com.android.internal.os.storage.ExternalStorageFormatter;
 import com.android.internal.util.FastXmlSerializer;
 import com.android.internal.util.JournaledFile;
 import com.android.internal.util.XmlUtils;
 import com.android.internal.widget.LockPatternUtils;
+import com.android.org.conscrypt.TrustedCertificateStore;
 
 import org.xmlpull.v1.XmlPullParser;
 import org.xmlpull.v1.XmlPullParserException;
@@ -49,6 +52,7 @@
 import android.content.pm.PackageManager.NameNotFoundException;
 import android.content.pm.ResolveInfo;
 import android.net.Uri;
+import android.os.AsyncTask;
 import android.os.Binder;
 import android.os.Bundle;
 import android.os.Environment;
@@ -66,7 +70,12 @@
 import android.os.UserHandle;
 import android.os.UserManager;
 import android.provider.Settings;
+import android.security.Credentials;
+import android.security.IKeyChainService;
+import android.security.KeyChain;
+import android.security.KeyChain.KeyChainConnection;
 import android.util.AtomicFile;
+import android.util.Log;
 import android.util.PrintWriterPrinter;
 import android.util.Printer;
 import android.util.Slog;
@@ -75,6 +84,7 @@
 import android.view.IWindowManager;
 import android.view.WindowManagerPolicy;
 
+import java.io.ByteArrayInputStream;
 import java.io.File;
 import java.io.FileDescriptor;
 import java.io.FileInputStream;
@@ -82,8 +92,14 @@
 import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.PrintWriter;
+import java.security.KeyStore.TrustedCertificateEntry;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
 import java.text.DateFormat;
 import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
 import java.util.Date;
 import java.util.HashMap;
 import java.util.List;
@@ -1870,6 +1886,76 @@
         return !"".equals(state);
     }
 
+    public boolean installCaCert(byte[] certBuffer) throws RemoteException {
+        mContext.enforceCallingOrSelfPermission(MANAGE_CA_CERTIFICATES, null);
+        KeyChainConnection keyChainConnection = null;
+        byte[] pemCert;
+        try {
+            X509Certificate cert = parseCert(certBuffer);
+            pemCert =  Credentials.convertToPem(cert);
+        } catch (CertificateException ce) {
+            Log.e(TAG, "Problem converting cert", ce);
+            return false;
+        } catch (IOException ioe) {
+            Log.e(TAG, "Problem reading cert", ioe);
+            return false;
+        }
+        try {
+            keyChainConnection = KeyChain.bind(mContext);
+            try {
+                keyChainConnection.getService().installCaCertificate(pemCert);
+                return true;
+            } finally {
+                if (keyChainConnection != null) {
+                    keyChainConnection.close();
+                    keyChainConnection = null;
+                }
+            }
+        } catch (InterruptedException e1) {
+            Log.w(TAG, "installCaCertsToKeyChain(): ", e1);
+            Thread.currentThread().interrupt();
+        }
+        return false;
+    }
+
+    private static X509Certificate parseCert(byte[] certBuffer)
+            throws CertificateException, IOException {
+        CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
+        return (X509Certificate) certFactory.generateCertificate(new ByteArrayInputStream(
+                certBuffer));
+    }
+
+    public void uninstallCaCert(final byte[] certBuffer) {
+        mContext.enforceCallingOrSelfPermission(MANAGE_CA_CERTIFICATES, null);
+        TrustedCertificateStore certStore = new TrustedCertificateStore();
+        String alias = null;
+        try {
+            X509Certificate cert = parseCert(certBuffer);
+            alias = certStore.getCertificateAlias(cert);
+        } catch (CertificateException ce) {
+            Log.e(TAG, "Problem creating X509Certificate", ce);
+            return;
+        } catch (IOException ioe) {
+            Log.e(TAG, "Problem reading certificate", ioe);
+            return;
+        }
+        try {
+            KeyChainConnection keyChainConnection = KeyChain.bind(mContext);
+            IKeyChainService service = keyChainConnection.getService();
+            try {
+                service.deleteCaCertificate(alias);
+            } catch (RemoteException e) {
+                Log.e(TAG, "from CaCertUninstaller: ", e);
+            } finally {
+                keyChainConnection.close();
+                keyChainConnection = null;
+            }
+        } catch (InterruptedException ie) {
+            Log.w(TAG, "CaCertUninstaller: ", ie);
+            Thread.currentThread().interrupt();
+        }
+    }
+
     void wipeDataLocked(int flags) {
         // If the SD card is encrypted and non-removable, we have to force a wipe.
         boolean forceExtWipe = !Environment.isExternalStorageRemovable() && isExtStorageEncrypted();
diff --git a/services/java/com/android/server/print/RemotePrintService.java b/services/java/com/android/server/print/RemotePrintService.java
index 491dddc..5c68460 100644
--- a/services/java/com/android/server/print/RemotePrintService.java
+++ b/services/java/com/android/server/print/RemotePrintService.java
@@ -30,8 +30,6 @@
 import android.os.ParcelFileDescriptor;
 import android.os.RemoteException;
 import android.os.UserHandle;
-import android.print.IPrinterDiscoverySessionController;
-import android.print.IPrinterDiscoverySessionObserver;
 import android.print.PrintJobInfo;
 import android.print.PrintManager;
 import android.print.PrinterId;
@@ -79,6 +77,10 @@
 
     private boolean mDestroyed;
 
+    private boolean mAllPrintJobsHandled;
+
+    private boolean mHasPrinterDiscoverySession;
+
     public RemotePrintService(Context context, ComponentName componentName, int userId,
             RemotePrintSpooler spooler) {
         mContext = context;
@@ -97,6 +99,8 @@
     private void handleDestroy() {
         throwIfDestroyed();
         ensureUnbound();
+        mAllPrintJobsHandled = false;
+        mHasPrinterDiscoverySession = false;
         mDestroyed = true;
     }
 
@@ -110,18 +114,27 @@
     }
 
     private void handleBinderDied() {
+        mAllPrintJobsHandled = false;
+        mHasPrinterDiscoverySession = false;
         mPendingCommands.clear();
         ensureUnbound();
     }
 
     private void handleOnAllPrintJobsHandled() {
         throwIfDestroyed();
+
+        mAllPrintJobsHandled = true;
+
         if (isBound()) {
             if (DEBUG) {
                 Slog.i(LOG_TAG, "[user: " + mUserId + "] handleOnAllPrintJobsHandled()");
             }
-            // If bound and all the work is completed, then unbind.
-            ensureUnbound();
+
+            // If the service has a printer discovery session
+            // created we should not disconnect from it just yet.
+            if (!mHasPrinterDiscoverySession) {
+                ensureUnbound();
+            }
         }
     }
 
@@ -153,6 +166,9 @@
 
     private void handleOnPrintJobQueued(final PrintJobInfo printJob) {
         throwIfDestroyed();
+
+        mAllPrintJobsHandled = false;
+
         if (!isBound()) {
             ensureBound();
             mPendingCommands.add(new Runnable() {
@@ -173,20 +189,18 @@
         }
     }
 
-    public void createPrinterDiscoverySession(IPrinterDiscoverySessionObserver observer) {
-        mHandler.obtainMessage(MyHandler.MSG_CREATE_PRINTER_DISCOVERY_SESSION,
-                observer).sendToTarget();
+    public void createPrinterDiscoverySession() {
+        mHandler.sendEmptyMessage(MyHandler.MSG_CREATE_PRINTER_DISCOVERY_SESSION);
     }
 
-    private void handleCreatePrinterDiscoverySession(
-            final IPrinterDiscoverySessionObserver observer) {
+    private void handleCreatePrinterDiscoverySession() {
         throwIfDestroyed();
         if (!isBound()) {
             ensureBound();
             mPendingCommands.add(new Runnable() {
                 @Override
                 public void run() {
-                    handleCreatePrinterDiscoverySession(observer);
+                    handleCreatePrinterDiscoverySession();
                 }
             });
         } else {
@@ -194,9 +208,126 @@
                 Slog.i(LOG_TAG, "[user: " + mUserId + "] createPrinterDiscoverySession()");
             }
             try {
-                mPrintService.createPrinterDiscoverySession(observer);
+                mPrintService.createPrinterDiscoverySession();
             } catch (RemoteException re) {
-                Slog.e(LOG_TAG, "Error announcing start printer dicovery.", re);
+                Slog.e(LOG_TAG, "Error creating printer dicovery session.", re);
+            }
+
+            mHasPrinterDiscoverySession = true;
+        }
+    }
+
+    public void destroyPrinterDiscoverySession() {
+        mHandler.sendEmptyMessage(MyHandler.MSG_DESTROY_PRINTER_DISCOVERY_SESSION);
+    }
+
+    private void handleDestroyPrinterDiscoverySession() {
+        throwIfDestroyed();
+        if (!isBound()) {
+            ensureBound();
+            mPendingCommands.add(new Runnable() {
+                @Override
+                public void run() {
+                    handleDestroyPrinterDiscoverySession();
+                }
+            });
+        } else {
+            if (DEBUG) {
+                Slog.i(LOG_TAG, "[user: " + mUserId + "] destroyPrinterDiscoverySession()");
+            }
+
+            mHasPrinterDiscoverySession = false;
+
+            try {
+                mPrintService.destroyPrinterDiscoverySession();
+            } catch (RemoteException re) {
+                Slog.e(LOG_TAG, "Error destroying printer dicovery session.", re);
+            }
+
+            // If the service has no print jobs and no active discovery
+            // session anymore we should disconnect from it.
+            if (mAllPrintJobsHandled) {
+                ensureUnbound();
+            }
+        }
+    }
+
+    public void startPrinterDiscovery(List<PrinterId> priorityList) {
+        mHandler.obtainMessage(MyHandler.MSG_START_PRINTER_DISCOVERY,
+                priorityList).sendToTarget();
+    }
+
+    private void handleStartPrinterDiscovery(final List<PrinterId> priorityList) {
+        throwIfDestroyed();
+        if (!isBound()) {
+            ensureBound();
+            mPendingCommands.add(new Runnable() {
+                @Override
+                public void run() {
+                    handleStartPrinterDiscovery(priorityList);
+                }
+            });
+        } else {
+            if (DEBUG) {
+                Slog.i(LOG_TAG, "[user: " + mUserId + "] startPrinterDiscovery()");
+            }
+            try {
+                mPrintService.startPrinterDiscovery(priorityList);
+            } catch (RemoteException re) {
+                Slog.e(LOG_TAG, "Error starting printer dicovery.", re);
+            }
+        }
+    }
+
+    public void stopPrinterDiscovery() {
+        mHandler.sendEmptyMessage(MyHandler.MSG_STOP_PRINTER_DISCOVERY);
+    }
+
+    private void handleStopPrinterDiscovery() {
+        throwIfDestroyed();
+        if (!isBound()) {
+            ensureBound();
+            mPendingCommands.add(new Runnable() {
+                @Override
+                public void run() {
+                    handleStopPrinterDiscovery();
+                }
+            });
+        } else {
+            if (DEBUG) {
+                Slog.i(LOG_TAG, "[user: " + mUserId + "] stopPrinterDiscovery()");
+            }
+            try {
+                mPrintService.stopPrinterDiscovery();
+            } catch (RemoteException re) {
+                Slog.e(LOG_TAG, "Error stopping printer dicovery.", re);
+            }
+        }
+    }
+
+    public void requestPrinterUpdate(PrinterId printerId) {
+        mHandler.obtainMessage(MyHandler.MSG_REQUEST_PRINTER_UPDATE,
+                printerId).sendToTarget();
+    }
+
+    private void handleRequestPrinterUpdate(final PrinterId printerId) {
+        throwIfDestroyed();
+        if (!isBound()) {
+            ensureBound();
+            mPendingCommands.add(new Runnable() {
+                @Override
+                public void run() {
+                    handleRequestPrinterUpdate(printerId);
+                }
+            });
+        } else {
+            if (DEBUG) {
+                Slog.i(LOG_TAG, "[user: " + mUserId + "] requestPrinterUpdate()");
+            }
+            try {
+                mPrintService.requestPrinterUpdate(printerId);
+            } catch (RemoteException re) {
+                Slog.e(LOG_TAG, "Error requesting a printer update.", re);
             }
         }
     }
@@ -279,20 +410,47 @@
     }
 
     private final class MyHandler extends Handler {
-        public static final int MSG_ON_ALL_PRINT_JOBS_HANDLED = 1;
-        public static final int MSG_ON_REQUEST_CANCEL_PRINT_JOB = 2;
-        public static final int MSG_ON_PRINT_JOB_QUEUED = 3;
-        public static final int MSG_CREATE_PRINTER_DISCOVERY_SESSION = 4;
-        public static final int MSG_DESTROY = 6;
-        public static final int MSG_BINDER_DIED = 7;
+        public static final int MSG_CREATE_PRINTER_DISCOVERY_SESSION = 1;
+        public static final int MSG_DESTROY_PRINTER_DISCOVERY_SESSION = 2;
+        public static final int MSG_START_PRINTER_DISCOVERY = 3;
+        public static final int MSG_STOP_PRINTER_DISCOVERY = 4;
+        public static final int MSG_REQUEST_PRINTER_UPDATE = 5;
+        public static final int MSG_ON_ALL_PRINT_JOBS_HANDLED = 6;
+        public static final int MSG_ON_REQUEST_CANCEL_PRINT_JOB = 7;
+        public static final int MSG_ON_PRINT_JOB_QUEUED = 8;
+        public static final int MSG_DESTROY = 9;
+        public static final int MSG_BINDER_DIED = 10;
 
         public MyHandler(Looper looper) {
             super(looper, null, false);
         }
 
         @Override
+        @SuppressWarnings("unchecked")
         public void handleMessage(Message message) {
             switch (message.what) {
+                case MSG_CREATE_PRINTER_DISCOVERY_SESSION: {
+                    handleCreatePrinterDiscoverySession();
+                } break;
+
+                case MSG_DESTROY_PRINTER_DISCOVERY_SESSION: {
+                    handleDestroyPrinterDiscoverySession();
+                } break;
+
+                case MSG_START_PRINTER_DISCOVERY: {
+                    List<PrinterId> priorityList = (ArrayList<PrinterId>) message.obj;
+                    handleStartPrinterDiscovery(priorityList);
+                } break;
+
+                case MSG_STOP_PRINTER_DISCOVERY: {
+                    handleStopPrinterDiscovery();
+                } break;
+
+                case MSG_REQUEST_PRINTER_UPDATE: {
+                    PrinterId printerId = (PrinterId) message.obj;
+                    handleRequestPrinterUpdate(printerId);
+                } break;
+
                 case MSG_ON_ALL_PRINT_JOBS_HANDLED: {
                     handleOnAllPrintJobsHandled();
                 } break;
@@ -307,13 +465,6 @@
                     handleOnPrintJobQueued(printJob);
                 } break;
 
-                case MSG_CREATE_PRINTER_DISCOVERY_SESSION: {
-                    IPrinterDiscoverySessionObserver observer =
-                            (IPrinterDiscoverySessionObserver) message.obj;
-                    handleCreatePrinterDiscoverySession(new SecurePrinterDiscoverySessionObserver(
-                            mComponentName, observer));
-                } break;
-
                 case MSG_DESTROY: {
                     handleDestroy();
                 } break;
@@ -363,7 +514,7 @@
         }
 
         @Override
-        public boolean setPrintJobState(int printJobId, int state, CharSequence error) {
+        public boolean setPrintJobState(int printJobId, int state, String error) {
             RemotePrintService service = mWeakService.get();
             if (service != null) {
                 final long identity = Binder.clearCallingIdentity();
@@ -402,79 +553,70 @@
                 }
             }
         }
-    }
-
-    private static final class SecurePrinterDiscoverySessionObserver
-            extends IPrinterDiscoverySessionObserver.Stub {
-        private final ComponentName mComponentName;
-
-        private final IPrinterDiscoverySessionObserver mDecoratedObsever;
-
-        public SecurePrinterDiscoverySessionObserver(ComponentName componentName,
-                IPrinterDiscoverySessionObserver observer) {
-            mComponentName = componentName;
-            mDecoratedObsever = observer;
-        }
 
         @Override
         public void onPrintersAdded(List<PrinterInfo> printers) {
-            throwIfPrinterIdsForPrinterInfoTampered(printers);
-            try {
-                mDecoratedObsever.onPrintersAdded(printers);
-            } catch (RemoteException re) {
-                Slog.e(LOG_TAG, "Error delegating to onPrintersAdded", re);
-            }
-        }
-
-        @Override
-        public void onPrintersUpdated(List<PrinterInfo> printers) {
-            throwIfPrinterIdsForPrinterInfoTampered(printers);
-            try {
-                mDecoratedObsever.onPrintersUpdated(printers);
-            } catch (RemoteException re) {
-                Slog.e(LOG_TAG, "Error delegating to onPrintersUpdated.", re);
+            RemotePrintService service = mWeakService.get();
+            if (service != null) {
+                throwIfPrinterIdsForPrinterInfoTampered(service.mComponentName, printers);
+                final long identity = Binder.clearCallingIdentity();
+                try {
+                    service.mSpooler.onPrintersAdded(printers);
+                } finally {
+                    Binder.restoreCallingIdentity(identity);
+                }
             }
         }
 
         @Override
         public void onPrintersRemoved(List<PrinterId> printerIds) {
-            throwIfPrinterIdsTampered(printerIds);
-            try {
-                mDecoratedObsever.onPrintersRemoved(printerIds);
-            } catch (RemoteException re) {
-                Slog.e(LOG_TAG, "Error delegating to onPrintersRemoved", re);
+            RemotePrintService service = mWeakService.get();
+            if (service != null) {
+                throwIfPrinterIdsTampered(service.mComponentName, printerIds);
+                final long identity = Binder.clearCallingIdentity();
+                try {
+                    service.mSpooler.onPrintersRemoved(printerIds);
+                } finally {
+                    Binder.restoreCallingIdentity(identity);
+                }
             }
         }
 
         @Override
-        public void setController(IPrinterDiscoverySessionController controller) {
-            try {
-                mDecoratedObsever.setController(controller);
-            } catch (RemoteException re) {
-                Slog.e(LOG_TAG, "Error setting controller", re);
+        public void onPrintersUpdated(List<PrinterInfo> printers) {
+            RemotePrintService service = mWeakService.get();
+            if (service != null) {
+                throwIfPrinterIdsForPrinterInfoTampered(service.mComponentName, printers);
+                final long identity = Binder.clearCallingIdentity();
+                try {
+                    service.mSpooler.onPrintersUpdated(printers);
+                } finally {
+                    Binder.restoreCallingIdentity(identity);
+                }
             }
         }
 
-        private void throwIfPrinterIdsForPrinterInfoTampered(
+        private void throwIfPrinterIdsForPrinterInfoTampered(ComponentName serviceName,
                 List<PrinterInfo> printerInfos) {
             final int printerInfoCount = printerInfos.size();
             for (int i = 0; i < printerInfoCount; i++) {
                 PrinterId printerId = printerInfos.get(i).getId();
-                throwIfPrinterIdTampered(printerId);
+                throwIfPrinterIdTampered(serviceName, printerId);
             }
         }
 
-        private void throwIfPrinterIdsTampered(List<PrinterId> printerIds) {
+        private void throwIfPrinterIdsTampered(ComponentName serviceName,
+                List<PrinterId> printerIds) {
             final int printerIdCount = printerIds.size();
             for (int i = 0; i < printerIdCount; i++) {
                 PrinterId printerId = printerIds.get(i);
-                throwIfPrinterIdTampered(printerId);
+                throwIfPrinterIdTampered(serviceName, printerId);
             }
         }
 
-        private void throwIfPrinterIdTampered(PrinterId printerId) {
+        private void throwIfPrinterIdTampered(ComponentName serviceName, PrinterId printerId) {
             if (printerId == null || printerId.getServiceName() == null
-                    || !printerId.getServiceName().equals(mComponentName)) {
+                    || !printerId.getServiceName().equals(serviceName)) {
                 throw new IllegalArgumentException("Invalid printer id: " + printerId);
             }
         }
diff --git a/services/java/com/android/server/print/RemotePrintSpooler.java b/services/java/com/android/server/print/RemotePrintSpooler.java
index c932e9b..d261288 100644
--- a/services/java/com/android/server/print/RemotePrintSpooler.java
+++ b/services/java/com/android/server/print/RemotePrintSpooler.java
@@ -32,9 +32,10 @@
 import android.print.IPrintSpooler;
 import android.print.IPrintSpoolerCallbacks;
 import android.print.IPrintSpoolerClient;
-import android.print.IPrinterDiscoverySessionObserver;
 import android.print.PrintAttributes;
 import android.print.PrintJobInfo;
+import android.print.PrinterId;
+import android.print.PrinterInfo;
 import android.util.Slog;
 import android.util.TimedRemoteCaller;
 
@@ -92,7 +93,11 @@
     public static interface PrintSpoolerCallbacks {
         public void onPrintJobQueued(PrintJobInfo printJob);
         public void onAllPrintJobsForServiceHandled(ComponentName printService);
-        public void createPrinterDiscoverySession(IPrinterDiscoverySessionObserver observer);
+        public void createPrinterDiscoverySession();
+        public void destroyPrinterDiscoverySession();
+        public void startPrinterDiscovery(List<PrinterId> priorityList);
+        public void stopPrinterDiscovery();
+        public void requestPrinterUpdate(PrinterId printerId);
     }
 
     public RemotePrintSpooler(Context context, int userId,
@@ -209,7 +214,7 @@
         return null;
     }
 
-    public final boolean setPrintJobState(int printJobId, int state, CharSequence error) {
+    public final boolean setPrintJobState(int printJobId, int state, String error) {
         throwIfCalledOnMainThread();
         synchronized (mLock) {
             throwIfDestroyedLocked();
@@ -300,6 +305,78 @@
         }
     }
 
+    public final void onPrintersAdded(List<PrinterInfo> printers) {
+        throwIfCalledOnMainThread();
+        synchronized (mLock) {
+            throwIfDestroyedLocked();
+            mCanUnbind = false;
+        }
+        try {
+            getRemoteInstanceLazy().onPrintersAdded(printers);
+        } catch (RemoteException re) {
+            Slog.e(LOG_TAG, "Error adding printers.", re);
+        } catch (TimeoutException te) {
+            Slog.e(LOG_TAG, "Error adding printers.", te);
+        } finally {
+            if (DEBUG) {
+                Slog.i(LOG_TAG, "[user: " + mUserHandle.getIdentifier()
+                        + "] onPrintersAdded()");
+            }
+            synchronized (mLock) {
+                mCanUnbind = true;
+                mLock.notifyAll();
+            }
+        }
+    }
+
+    public final void onPrintersRemoved(List<PrinterId> printerIds) {
+        throwIfCalledOnMainThread();
+        synchronized (mLock) {
+            throwIfDestroyedLocked();
+            mCanUnbind = false;
+        }
+        try {
+            getRemoteInstanceLazy().onPrintersRemoved(printerIds);
+        } catch (RemoteException re) {
+            Slog.e(LOG_TAG, "Error removing printers.", re);
+        } catch (TimeoutException te) {
+            Slog.e(LOG_TAG, "Error removing printers.", te);
+        } finally {
+            if (DEBUG) {
+                Slog.i(LOG_TAG, "[user: " + mUserHandle.getIdentifier()
+                        + "] onPrintersRemoved()");
+            }
+            synchronized (mLock) {
+                mCanUnbind = true;
+                mLock.notifyAll();
+            }
+        }
+    }
+
+    public final void onPrintersUpdated(List<PrinterInfo> printers) {
+        throwIfCalledOnMainThread();
+        synchronized (mLock) {
+            throwIfDestroyedLocked();
+            mCanUnbind = false;
+        }
+        try {
+            getRemoteInstanceLazy().onPrintersUpdated(printers);
+        } catch (RemoteException re) {
+            Slog.e(LOG_TAG, "Error updating printers.", re);
+        } catch (TimeoutException te) {
+            Slog.e(LOG_TAG, "Error updating printers.", te);
+        } finally {
+            if (DEBUG) {
+                Slog.i(LOG_TAG, "[user: " + mUserHandle.getIdentifier()
+                        + "] onPrintersUpdted()");
+            }
+            synchronized (mLock) {
+                mCanUnbind = true;
+                mLock.notifyAll();
+            }
+        }
+    }
+
     private IPrintSpooler getRemoteInstanceLazy() throws TimeoutException {
         synchronized (mLock) {
             if (mRemoteInstance != null) {
@@ -488,7 +565,7 @@
         }
 
         public boolean setPrintJobState(IPrintSpooler target, int printJobId,
-                int status, CharSequence error) throws RemoteException, TimeoutException {
+                int status, String error) throws RemoteException, TimeoutException {
             final int sequence = onBeforeRemoteCall();
             target.setPrintJobState(printJobId, status, error, mCallback, sequence);
             return getResultTimed(sequence);
@@ -597,12 +674,64 @@
         }
 
         @Override
-        public void createPrinterDiscoverySession(IPrinterDiscoverySessionObserver observer) {
+        public void createPrinterDiscoverySession() {
             RemotePrintSpooler spooler = mWeakSpooler.get();
             if (spooler != null) {
                 final long identity = Binder.clearCallingIdentity();
                 try {
-                    spooler.mCallbacks.createPrinterDiscoverySession(observer);
+                    spooler.mCallbacks.createPrinterDiscoverySession();
+                } finally {
+                    Binder.restoreCallingIdentity(identity);
+                }
+            }
+        }
+
+        @Override
+        public void destroyPrinterDiscoverySession() {
+            RemotePrintSpooler spooler = mWeakSpooler.get();
+            if (spooler != null) {
+                final long identity = Binder.clearCallingIdentity();
+                try {
+                    spooler.mCallbacks.destroyPrinterDiscoverySession();
+                } finally {
+                    Binder.restoreCallingIdentity(identity);
+                }
+            }
+        }
+
+        @Override
+        public void startPrinterDiscovery(List<PrinterId> priorityList) {
+            RemotePrintSpooler spooler = mWeakSpooler.get();
+            if (spooler != null) {
+                final long identity = Binder.clearCallingIdentity();
+                try {
+                    spooler.mCallbacks.startPrinterDiscovery(priorityList);
+                } finally {
+                    Binder.restoreCallingIdentity(identity);
+                }
+            }
+        }
+
+        @Override
+        public void stopPrinterDiscovery() {
+            RemotePrintSpooler spooler = mWeakSpooler.get();
+            if (spooler != null) {
+                final long identity = Binder.clearCallingIdentity();
+                try {
+                    spooler.mCallbacks.stopPrinterDiscovery();
+                } finally {
+                    Binder.restoreCallingIdentity(identity);
+                }
+            }
+        }
+
+        @Override
+        public void requestPrinterUpdate(PrinterId printerId) {
+            RemotePrintSpooler spooler = mWeakSpooler.get();
+            if (spooler != null) {
+                final long identity = Binder.clearCallingIdentity();
+                try {
+                    spooler.mCallbacks.requestPrinterUpdate(printerId);
                 } finally {
                     Binder.restoreCallingIdentity(identity);
                 }
diff --git a/services/java/com/android/server/print/UserState.java b/services/java/com/android/server/print/UserState.java
index ffcc9c3..9d7cfdd 100644
--- a/services/java/com/android/server/print/UserState.java
+++ b/services/java/com/android/server/print/UserState.java
@@ -21,8 +21,8 @@
 import android.content.Intent;
 import android.content.pm.PackageManager;
 import android.content.pm.ResolveInfo;
-import android.print.IPrinterDiscoverySessionObserver;
 import android.print.PrintJobInfo;
+import android.print.PrinterId;
 import android.printservice.PrintServiceInfo;
 import android.provider.Settings;
 import android.text.TextUtils;
@@ -105,7 +105,7 @@
     }
 
     @Override
-    public void createPrinterDiscoverySession(IPrinterDiscoverySessionObserver observer) {
+    public void createPrinterDiscoverySession() {
         final List<RemotePrintService> services;
         synchronized (mLock) {
             throwIfDestroyedLocked();
@@ -117,7 +117,73 @@
         final int serviceCount = services.size();
         for (int i = 0; i < serviceCount; i++) {
             RemotePrintService service = services.get(i);
-            service.createPrinterDiscoverySession(observer);
+            service.createPrinterDiscoverySession();
+        }
+    }
+
+    @Override
+    public void destroyPrinterDiscoverySession() {
+        final List<RemotePrintService> services;
+        synchronized (mLock) {
+            throwIfDestroyedLocked();
+            if (mActiveServices.isEmpty()) {
+                return;
+            }
+            services = new ArrayList<RemotePrintService>(mActiveServices.values());
+        }
+        final int serviceCount = services.size();
+        for (int i = 0; i < serviceCount; i++) {
+            RemotePrintService service = services.get(i);
+            service.destroyPrinterDiscoverySession();
+        }
+    }
+
+    @Override
+    public void startPrinterDiscovery(List<PrinterId> printerIds) {
+        final List<RemotePrintService> services;
+        synchronized (mLock) {
+            throwIfDestroyedLocked();
+            if (mActiveServices.isEmpty()) {
+                return;
+            }
+            services = new ArrayList<RemotePrintService>(mActiveServices.values());
+        }
+        final int serviceCount = services.size();
+        for (int i = 0; i < serviceCount; i++) {
+            RemotePrintService service = services.get(i);
+            service.startPrinterDiscovery(printerIds);
+        }
+    }
+
+    @Override
+    public void stopPrinterDiscovery() {
+        final List<RemotePrintService> services;
+        synchronized (mLock) {
+            throwIfDestroyedLocked();
+            if (mActiveServices.isEmpty()) {
+                return;
+            }
+            services = new ArrayList<RemotePrintService>(mActiveServices.values());
+        }
+        final int serviceCount = services.size();
+        for (int i = 0; i < serviceCount; i++) {
+            RemotePrintService service = services.get(i);
+            service.stopPrinterDiscovery();
+        }
+    }
+
+    @Override
+    public void requestPrinterUpdate(PrinterId printerId) {
+        final RemotePrintService service;
+        synchronized (mLock) {
+            throwIfDestroyedLocked();
+            if (mActiveServices.isEmpty()) {
+                return;
+            }
+            service = mActiveServices.get(printerId.getServiceName());
+        }
+        if (service != null) {
+            service.requestPrinterUpdate(printerId);
         }
     }