OSDN Git Service

Implemented advanced printer selection and API refactoring.
authorSvetoslav <svetoslavganov@google.com>
Thu, 15 Aug 2013 00:31:04 +0000 (17:31 -0700)
committerSvetoslav Ganov <svetoslavganov@google.com>
Mon, 19 Aug 2013 20:24:11 +0000 (13:24 -0700)
1. Added past printer history tracking and merging favorite printers
   with discovered printers.

2. Added save as PDF support.

3. Added all printers activity with search capability and optional
   add printers chooser (if any print service provides add printers
   activity)

4. Refactored the printer discovery session APIs. Now one session
   can have multiple window discovery windows and the session stores
   the printers found during past discovery periods.

5. Merged the print spooler and the print spooler service - much
   simpler and easier to maintain.

Change-Id: I4830b0eb6367e1c748b768a5ea9ea11baf36cfad

39 files changed:
Android.mk
api/current.txt
core/java/android/print/IPrintSpooler.aidl
core/java/android/print/IPrintSpoolerClient.aidl
core/java/android/print/IPrinterDiscoverySessionController.aidl [deleted file]
core/java/android/print/IPrinterDiscoverySessionObserver.aidl [deleted file]
core/java/android/print/PrintJobInfo.java
core/java/android/print/PrintManager.java
core/java/android/print/PrinterInfo.java
core/java/android/print/pdf/PdfDocument.java
core/java/android/printservice/IPrintService.aidl
core/java/android/printservice/IPrintServiceClient.aidl
core/java/android/printservice/PrintJob.java
core/java/android/printservice/PrintService.java
core/java/android/printservice/PrinterDiscoverySession.java
core/java/com/android/internal/os/HandlerCaller.java
packages/PrintSpooler/AndroidManifest.xml
packages/PrintSpooler/res/drawable-hdpi/ic_menu_add.png [new file with mode: 0644]
packages/PrintSpooler/res/drawable-mdpi/ic_menu_add.png [new file with mode: 0644]
packages/PrintSpooler/res/drawable-xhdpi/ic_menu_add.png [new file with mode: 0644]
packages/PrintSpooler/res/layout/print_job_config_activity_container.xml
packages/PrintSpooler/res/layout/select_printer_activity.xml [moved from packages/PrintSpooler/res/layout/choose_printer_activity.xml with 58% similarity]
packages/PrintSpooler/res/layout/spinner_dropdown_item.xml
packages/PrintSpooler/res/menu/select_printer_activity.xml [moved from packages/PrintSpooler/res/menu/choose_printer_activity.xml with 79% similarity]
packages/PrintSpooler/res/values/strings.xml
packages/PrintSpooler/res/values/themes.xml
packages/PrintSpooler/src/com/android/printspooler/AvailablePrinterProvider.java [deleted file]
packages/PrintSpooler/src/com/android/printspooler/DataLoader.java [deleted file]
packages/PrintSpooler/src/com/android/printspooler/DataProvider.java [deleted file]
packages/PrintSpooler/src/com/android/printspooler/FavoritePrinterProvider.java [deleted file]
packages/PrintSpooler/src/com/android/printspooler/FusedPrintersProvider.java [new file with mode: 0644]
packages/PrintSpooler/src/com/android/printspooler/PrintJobConfigActivity.java
packages/PrintSpooler/src/com/android/printspooler/PrintSpooler.java [deleted file]
packages/PrintSpooler/src/com/android/printspooler/PrintSpoolerService.java
packages/PrintSpooler/src/com/android/printspooler/SelectPrinterActivity.java [moved from packages/PrintSpooler/src/com/android/printspooler/ChoosePrinterActivity.java with 53% similarity]
packages/PrintSpooler/src/com/android/printspooler/SelectPrinterFragment.java [new file with mode: 0644]
services/java/com/android/server/print/RemotePrintService.java
services/java/com/android/server/print/RemotePrintSpooler.java
services/java/com/android/server/print/UserState.java

index 656e40c..ff6d4a0 100644 (file)
@@ -162,8 +162,6 @@ LOCAL_SRC_FILES += \
        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 \
index 4fb2fb9..82ffaac 100644 (file)
@@ -19031,7 +19031,7 @@ package android.print {
     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();
@@ -19155,7 +19155,7 @@ package android.printservice {
   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();
@@ -19184,11 +19184,15 @@ package android.printservice {
   }
 
   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>);
   }
index 8178180..5c8a22a 100644 (file)
@@ -18,6 +18,7 @@ package android.print;
 
 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 @@ oneway interface IPrintSpooler {
     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);
 }
index 8db2169..da60120 100644 (file)
@@ -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 @@ import android.print.PrintJobInfo;
  * @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 (file)
index 13116ef..0000000
+++ /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 (file)
index a78924c..0000000
+++ /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);
-}
index 2fb4751..602f3c1 100644 (file)
@@ -104,7 +104,7 @@ public final class PrintJobInfo implements Parcelable {
     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 @@ public final class PrintJobInfo implements Parcelable {
     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 @@ public final class PrintJobInfo implements Parcelable {
 
     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 @@ public final class PrintJobInfo implements Parcelable {
         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 @@ public final class PrintJobInfo implements Parcelable {
      *
      * @return The label.
      */
-    public CharSequence getLabel() {
+    public String getLabel() {
         return mLabel;
     }
 
@@ -225,7 +223,7 @@ public final class PrintJobInfo implements Parcelable {
      *
      * @hide
      */
-    public void setLabel(CharSequence label) {
+    public void setLabel(String label) {
         mLabel = label;
     }
 
@@ -385,7 +383,7 @@ public final class PrintJobInfo implements Parcelable {
      *
      * @hide
      */
-    public CharSequence getFailureReason() {
+    public String getFailureReason() {
         return mFailureReason;
     }
 
@@ -396,7 +394,7 @@ public final class PrintJobInfo implements Parcelable {
      *
      * @hide
      */
-    public void setFailureReason(CharSequence failureReason) {
+    public void setFailureReason(String failureReason) {
         mFailureReason = failureReason;
     }
 
@@ -470,7 +468,7 @@ public final class PrintJobInfo implements Parcelable {
     @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 @@ public final class PrintJobInfo implements Parcelable {
         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);
index 636b9d4..531dcb2 100644 (file)
@@ -374,14 +374,14 @@ public final class PrintManager {
 
             @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);
index ac782a8..6f567a6 100644 (file)
@@ -229,10 +229,11 @@ public final class PrinterInfo implements Parcelable {
         /**
          * 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);
         }
 
         /**
index cfeb975..dbd7dd1 100644 (file)
@@ -324,7 +324,7 @@ public final class PdfDocument {
             /**
              * 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.
              */
index 16b7a26..2cee1d8 100644 (file)
@@ -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 @@ oneway interface IPrintService {
     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();
 }
index f00b37c..1e33fc0 100644 (file)
@@ -29,7 +29,11 @@ import android.print.PrinterInfo;
 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);
 }
index 5f9a730..d2fbef2 100644 (file)
@@ -24,6 +24,10 @@ import android.util.Log;
  * 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 @@ public final class PrintJob {
      * @return The id.
      */
     public int getId() {
+        PrintService.throwIfNotCalledOnMainThread();
         return mCachedInfo.getId();
     }
 
@@ -62,6 +67,7 @@ public final class PrintJob {
      * @return The print job info.
      */
     public PrintJobInfo getInfo() {
+        PrintService.throwIfNotCalledOnMainThread();
         if (isInImmutableState()) {
             return mCachedInfo;
         }
@@ -83,6 +89,7 @@ public final class PrintJob {
      * @return The document.
      */
     public PrintDocument getDocument() {
+        PrintService.throwIfNotCalledOnMainThread();
         return mDocument;
     }
 
@@ -96,6 +103,7 @@ public final class PrintJob {
      * @see #cancel()
      */
     public boolean isQueued() {
+        PrintService.throwIfNotCalledOnMainThread();
         return getInfo().getState() == PrintJobInfo.STATE_QUEUED;
     }
 
@@ -110,6 +118,7 @@ public final class PrintJob {
      * @see #fail(CharSequence)
      */
     public boolean isStarted() {
+        PrintService.throwIfNotCalledOnMainThread();
         return getInfo().getState() == PrintJobInfo.STATE_STARTED;
     }
 
@@ -122,6 +131,7 @@ public final class PrintJob {
      * @see #complete()
      */
     public boolean isCompleted() {
+        PrintService.throwIfNotCalledOnMainThread();
         return getInfo().getState() == PrintJobInfo.STATE_COMPLETED;
     }
 
@@ -134,6 +144,7 @@ public final class PrintJob {
      * @see #fail(CharSequence)
      */
     public boolean isFailed() {
+        PrintService.throwIfNotCalledOnMainThread();
         return getInfo().getState() == PrintJobInfo.STATE_FAILED;
     }
 
@@ -146,6 +157,7 @@ public final class PrintJob {
      * @see #cancel()
      */
     public boolean isCancelled() {
+        PrintService.throwIfNotCalledOnMainThread();
         return getInfo().getState() == PrintJobInfo.STATE_FAILED;
     }
 
@@ -158,6 +170,7 @@ public final class PrintJob {
      * @see #isQueued()
      */
     public boolean start() {
+        PrintService.throwIfNotCalledOnMainThread();
         if (isQueued()) {
             return setState(PrintJobInfo.STATE_STARTED, null);
         }
@@ -173,6 +186,7 @@ public final class PrintJob {
      * @see #isStarted()
      */
     public boolean complete() {
+        PrintService.throwIfNotCalledOnMainThread();
         if (isStarted()) {
             return setState(PrintJobInfo.STATE_COMPLETED, null);
         }
@@ -191,7 +205,8 @@ public final class PrintJob {
      * @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 @@ public final class PrintJob {
      * @see #isQueued()
      */
     public boolean cancel() {
+        PrintService.throwIfNotCalledOnMainThread();
         if (isQueued() || isStarted()) {
             return setState(PrintJobInfo.STATE_CANCELED, null);
         }
@@ -226,6 +242,7 @@ public final class PrintJob {
      * @return True if the tag was set, false otherwise.
      */
     public boolean setTag(String tag) {
+        PrintService.throwIfNotCalledOnMainThread();
         if (isInImmutableState()) {
             return false;
         }
@@ -263,7 +280,7 @@ public final class PrintJob {
                 || 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
index 92bccd4..8fe770c 100644 (file)
@@ -25,7 +25,6 @@ import android.os.IBinder;
 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 @@ import java.util.List;
  * {@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 abstract class PrintService extends Service {
      */
     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 @@ public abstract class PrintService extends Service {
      * @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 @@ public abstract class PrintService extends Service {
      * @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 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 createPrinterDiscoverySession(IPrinterDiscoverySessionObserver observer) {
-                mHandler.obtainMessage(ServiceHandler.MSG_ON_CREATE_PRINTER_DISCOVERY_SESSION,
-                        observer).sendToTarget();
+            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 @@ public abstract class PrintService extends Service {
     }
 
     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");
+                    }
+                    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);
                     }
-                    session.setObserver(observer);
                 } break;
 
                 case MSG_ON_REQUEST_CANCEL_PRINTJOB: {
@@ -352,15 +409,12 @@ public abstract class PrintService extends Service {
                 } 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: {
index 92dc0dd..8b959a6 100644 (file)
 
 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 @@ import java.util.List;
  * 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 int mId;
 
-    private final Handler mHandler;
+    private final ArrayMap<PrinterId, PrinterInfo> mPrinters =
+            new ArrayMap<PrinterId, PrinterInfo>();
 
-    private final int mId;
+    private ArrayMap<PrinterId, PrinterInfo> mLastSentPrinters;
+
+    private IPrintServiceClient mObserver;
 
-    private IPrinterDiscoverySessionController mController;
+    private boolean mIsDestroyed;
 
-    private IPrinterDiscoverySessionObserver mObserver;
+    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 @@ public abstract class PrinterDiscoverySession {
     }
 
     /**
+     * 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 class PrinterDiscoverySession {
      */
     public abstract void onRequestPrinterUpdate(PrinterId printerId);
 
-    void close() {
-        synchronized (mLock) {
-            mController = null;
-            mObserver = 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();
 
-    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;
+    /**
+     * Gets whether the session is destroyed.
+     *
+     * @return Whether the session is destroyed.
+     *
+     * @see #onDestroy()
+     */
+    public final boolean isDestroyed() {
+        PrintService.throwIfNotCalledOnMainThread();
+        return mIsDestroyed;
+    }
 
-        public SessionHandler(Looper looper) {
-            super(looper, null, true);
-        }
+    /**
+     * 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;
+    }
 
-        @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;
+    void startPrinterDiscovery(List<PrinterId> priorityList) {
+        if (!mIsDestroyed) {
+            mIsDiscoveryStarted = true;
+            sendOutOfDiscoveryPeriodPrinterChanges();
+            if (priorityList == null) {
+                priorityList = Collections.emptyList();
             }
+            onStartPrinterDiscovery(priorityList);
         }
     }
 
-    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();
-            }
+    void stopPrinterDiscovery() {
+        if (!mIsDestroyed) {
+            mIsDiscoveryStarted = false;
+            onStopPrinterDiscovery();
         }
+    }
 
-        @Override
-        public void close() {
-            PrinterDiscoverySession session = mWeakSession.get();
-            if (session != null) {
-                session.mHandler.sendEmptyMessage(SessionHandler.MSG_CLOSE);
-            }
+    void requestPrinterUpdate(PrinterId printerId) {
+        if (!mIsDestroyed) {
+            onRequestPrinterUpdate(printerId);
         }
+    }
 
-        @Override
-        public void requestPrinterUpdate(PrinterId printerId) {
-            PrinterDiscoverySession session = mWeakSession.get();
-            if (session != null) {
-                session.mHandler.obtainMessage(
-                        SessionHandler.MSG_REQUEST_PRINTER_UPDATE,
-                        printerId).sendToTarget();
-            }
+    void destroy() {
+        if (!mIsDestroyed) {
+            mIsDestroyed = true;
+            mIsDiscoveryStarted = false;
+            mPrinters.clear();
+            mLastSentPrinters = null;
+            mObserver = null;
+            onDestroy();
         }
-    };
+    }
 }
index b442ff5..d9e3ef6 100644 (file)
@@ -65,7 +65,11 @@ public class HandlerCaller {
         
         mH.sendMessage(msg);
     }
-    
+
+    public void sendMessageDelayed(Message msg, long delayMillis) {
+        mH.sendMessageDelayed(msg, delayMillis);
+    }
+
     public boolean hasMessages(int what) {
         return mH.hasMessages(what);
     }
index c00639d..1f10af8 100644 (file)
@@ -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">
         </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 (file)
index 0000000..4b68f52
Binary files /dev/null and b/packages/PrintSpooler/res/drawable-hdpi/ic_menu_add.png 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 (file)
index 0000000..15ffadd
Binary files /dev/null and b/packages/PrintSpooler/res/drawable-mdpi/ic_menu_add.png 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 (file)
index 0000000..420510e
Binary files /dev/null and b/packages/PrintSpooler/res/drawable-xhdpi/ic_menu_add.png differ
index a0c111b..7817094 100644 (file)
@@ -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>
@@ -5,7 +5,7 @@
      you may not use this file except in compliance with the License.
      You may obtain a copy of the License at
 
-      http://www.apache.org/licenses/LICENSE-2.0
+          http://www.apache.org/licenses/LICENSE-2.0
 
      Unless required by applicable law or agreed to in writing, software
      distributed under the License is distributed on an "AS IS" BASIS,
      limitations under the License.
 -->
 
-<ListView xmlns:android="http://schemas.android.com/apk/res/android"
-    android:id="@+id/list_view"
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:orientation="horizontal"
     android:layout_width="fill_parent"
-    android:layout_height="fill_parent"
-    android:orientation="vertical">
-</ListView>
+    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
index 002cc14..d14c064 100644 (file)
@@ -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"
         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>
index 1cd611f..41fc516 100644 (file)
     <!-- 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] -->
index ab16c65..831b0ec 100644 (file)
         <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 (file)
index 658a224..0000000
+++ /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/DataLoader.java b/packages/PrintSpooler/src/com/android/printspooler/DataLoader.java
deleted file mode 100644 (file)
index 82cc2e1..0000000
+++ /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 (file)
index 7b10903..0000000
+++ /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 (file)
index 2c539d1..0000000
+++ /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 (file)
index 0000000..6bad5b3
--- /dev/null
@@ -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);
+            }
+        }
+    }
+}
index f8e9f43..d3dd8c9 100644 (file)
@@ -18,13 +18,17 @@ package com.android.printspooler;
 
 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.IWriteResultCallback;
 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.EditText;
 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 class PrintJobConfigActivity extends Activity {
     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 @@ public class PrintJobConfigActivity extends Activity {
     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 @@ public class PrintJobConfigActivity extends Activity {
     private Document mDocument;
     private PrintController mController;
 
-    private AvailablePrinterProvider mAvailablePrinters;
-
-    private FavoritePrinterProvider mFavoritePrinters;
-
     private int mPrintJobId;
 
     private IBinder mIPrintDocumentAdapter;
@@ -148,9 +176,6 @@ public class PrintJobConfigActivity extends Activity {
     @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 @@ public class PrintJobConfigActivity extends Activity {
             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 @@ public class PrintJobConfigActivity extends Activity {
     }
 
     @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 @@ public class PrintJobConfigActivity extends Activity {
             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 class PrintJobConfigActivity extends Activity {
 
         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 @@ public class PrintJobConfigActivity extends Activity {
             final boolean infoChanged = !info.equals(mDocument.info);
             if (infoChanged) {
                 mDocument.info = info;
-                PrintSpooler.peekInstance().setPrintJobPrintDocumentInfoNoPersistence(
+                PrintSpoolerService.peekInstance().setPrintJobPrintDocumentInfoNoPersistence(
                         mPrintJobId, info);
             }
 
@@ -465,12 +472,12 @@ public class PrintJobConfigActivity extends Activity {
             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 @@ public class PrintJobConfigActivity extends Activity {
                 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 @@ public class PrintJobConfigActivity extends Activity {
                 // 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 @@ public class PrintJobConfigActivity extends Activity {
             }
 
             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 @@ public class PrintJobConfigActivity extends Activity {
         }
     }
 
+    @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 EditText mCopiesEditText;
 
-        private final TextView mRangeTitle;
-        private final EditText mRangeEditText;
+        private TextView mRangeTitle;
+        private EditText mRangeEditText;
 
-        private final Spinner mDestinationSpinner;
+        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 @@ public class PrintJobConfigActivity extends Activity {
                         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 @@ public class PrintJobConfigActivity extends Activity {
                             }
                         }
                     }
+
+                    // 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 @@ public class PrintJobConfigActivity extends Activity {
                 }
 
                 mCopiesEditText.setError(null);
-                PrintSpooler.peekInstance().setPrintJobCopiesNoPersistence(mPrintJobId, copies);
+                PrintSpoolerService.peekInstance().setPrintJobCopiesNoPersistence(
+                        mPrintJobId, copies);
                 updateUi();
 
                 if (hadErrors && !hasErrors() && printAttributesChanged()) {
@@ -832,22 +965,25 @@ public class PrintJobConfigActivity extends Activity {
 
         private boolean mWaitingForPrinterCapabilities;
 
-        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();
+        private int mCurrentUi = UI_NONE;
 
+        public Editor() {
             // 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 @@ public class PrintJobConfigActivity extends Activity {
                     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 @@ public class PrintJobConfigActivity extends Activity {
                     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,24 +1033,26 @@ public class PrintJobConfigActivity extends Activity {
                 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;
                 }
-            });
+            }
+        }
 
-            updateUi();
+        public boolean isPrintingToPdf() {
+            return mDestinationSpinner.getSelectedItem()
+                    == mDestinationSpinnerAdapter.mFakePdfPrinter;
         }
 
         public boolean shouldCloseOnTouch(MotionEvent event) {
@@ -966,19 +1082,104 @@ public class PrintJobConfigActivity extends Activity {
         }
 
         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");
+            }
+
+            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();
+                    }
+                }
+            });
+        }
 
-            // Wire the cancel action.
-            Button cancelButton = (Button) contentGenerating.findViewById(R.id.cancel_button);
+        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 @@ public class PrintJobConfigActivity extends Activity {
                     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 @@ public class PrintJobConfigActivity extends Activity {
                             // 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 class PrintJobConfigActivity extends Activity {
 
         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 class PrintJobConfigActivity extends Activity {
 
         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 @@ public class PrintJobConfigActivity extends Activity {
             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 @@ public class PrintJobConfigActivity extends Activity {
                 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 @@ public class PrintJobConfigActivity extends Activity {
                 PrinterCapabilitiesInfo capabilities = printer.getCapabilities();
                 printer.getCapabilities().getDefaults(defaultAttributes);
 
-                // Destination
-                mDestinationSpinner.setEnabled(true);
-
                 // Copies
                 mCopiesEditText.setEnabled(true);
 
@@ -1223,6 +1511,7 @@ public class PrintJobConfigActivity extends Activity {
                         }
                     }
                 }
+                mMediaSizeSpinner.setEnabled(true);
 
                 // Color mode.
                 final int colorModes = capabilities.getColorModes();
@@ -1271,6 +1560,7 @@ public class PrintJobConfigActivity extends Activity {
                         }
                     }
                 }
+                mColorModeSpinner.setEnabled(true);
 
                 // Orientation.
                 final int orientations = capabilities.getOrientations();
@@ -1321,20 +1611,25 @@ public class PrintJobConfigActivity extends Activity {
                         }
                     }
                 }
+                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 @@ public class PrintJobConfigActivity extends Activity {
                     mRangeEditText.setEnabled(false);
                     mRangeEditText.setVisibility(View.INVISIBLE);
                 }
+                mRangeOptionsSpinner.setEnabled(true);
 
                 // Print/Print preview
                 if ((mRangeOptionsSpinner.getSelectedItemPosition() == 1
@@ -1378,6 +1674,7 @@ public class PrintJobConfigActivity extends Activity {
                     mCopiesEditText.selectAll();
                     mCopiesEditText.requestFocus();
                 }
+                mCopiesEditText.setEnabled(true);
             }
         }
 
@@ -1407,38 +1704,51 @@ public class PrintJobConfigActivity extends Activity {
             }
         }
 
-        private final class DestinationAdapter extends BaseAdapter {
-            private final AvailablePrinterProvider mProvider;
-
-            private final DataSetObserver mObserver = new DataSetObserver() {
-                @Override
-                public void onChanged() {
-                    notifyDataSetChanged();
-                }
+        private final class DestinationAdapter extends BaseAdapter
+                implements LoaderManager.LoaderCallbacks<List<PrinterInfo>>{
+            private final List<PrinterInfo> mPrinters = new ArrayList<PrinterInfo>();
 
-                @Override
-                public void onInvalidated() {
-                    notifyDataSetInvalidated();
-                }
-            };
+            public final PrinterInfo mFakePdfPrinter;
 
-            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 @@ public class PrintJobConfigActivity extends Activity {
                             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 (file)
index c2cf65e..0000000
+++ /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;
-        }
-    }
-}
index 4fab4f8..fda64c9 100644 (file)
@@ -21,9 +21,8 @@ import android.app.Service;
 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.IPrintDocumentAdapter;
 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 @@ import java.util.List;
  */
 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 @@ public final class PrintSpoolerService extends Service {
             @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 @@ public final class PrintSpoolerService extends Service {
                     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 final class PrintSpoolerService extends Service {
             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 @@ public final class PrintSpoolerService extends Service {
 
                         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 @@ public final class PrintSpoolerService extends Service {
             }
 
             @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 @@ public final class PrintSpoolerService extends Service {
                     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 @@ public final class PrintSpoolerService extends Service {
 
             @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 @@ public final class PrintSpoolerService extends Service {
                     }
                 } 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 @@ public final class PrintSpoolerService extends Service {
                 } 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);
+    }
 }
 package com.android.printspooler;
 
 import android.app.Activity;
+import android.content.Intent;
 import android.os.Bundle;
+import android.print.PrinterId;
 
-public class ChoosePrinterActivity extends Activity {
+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 onCreate(Bundle bundle) {
-        setContentView(R.layout.choose_printer_activity);
+    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 (file)
index 0000000..9ca3a86
--- /dev/null
@@ -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();
+        }
+    }
+}
index 491dddc..5c68460 100644 (file)
@@ -30,8 +30,6 @@ import android.os.Message;
 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 @@ final class RemotePrintService implements DeathRecipient {
 
     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 @@ final class RemotePrintService implements DeathRecipient {
     private void handleDestroy() {
         throwIfDestroyed();
         ensureUnbound();
+        mAllPrintJobsHandled = false;
+        mHasPrinterDiscoverySession = false;
         mDestroyed = true;
     }
 
@@ -110,18 +114,27 @@ final class RemotePrintService implements DeathRecipient {
     }
 
     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 @@ final class RemotePrintService implements DeathRecipient {
 
     private void handleOnPrintJobQueued(final PrintJobInfo printJob) {
         throwIfDestroyed();
+
+        mAllPrintJobsHandled = false;
+
         if (!isBound()) {
             ensureBound();
             mPendingCommands.add(new Runnable() {
@@ -173,20 +189,18 @@ final class RemotePrintService implements DeathRecipient {
         }
     }
 
-    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 @@ final class RemotePrintService implements DeathRecipient {
                 Slog.i(LOG_TAG, "[user: " + mUserId + "] createPrinterDiscoverySession()");
             }
             try {
-                mPrintService.createPrinterDiscoverySession(observer);
+                mPrintService.createPrinterDiscoverySession();
+            } catch (RemoteException 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 announcing start printer dicovery.", 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 @@ final class RemotePrintService implements DeathRecipient {
     }
 
     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 @@ final class RemotePrintService implements DeathRecipient {
                     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 @@ final class RemotePrintService implements DeathRecipient {
         }
 
         @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 @@ final class RemotePrintService implements DeathRecipient {
                 }
             }
         }
-    }
-
-    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);
             }
         }
index c932e9b..d261288 100644 (file)
@@ -32,9 +32,10 @@ import android.print.IPrintDocumentAdapter;
 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 @@ final class RemotePrintSpooler {
     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 @@ final class RemotePrintSpooler {
         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 @@ final class RemotePrintSpooler {
         }
     }
 
+    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 @@ final class RemotePrintSpooler {
         }
 
         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 @@ final class RemotePrintSpooler {
         }
 
         @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();
+                } 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.createPrinterDiscoverySession(observer);
+                    spooler.mCallbacks.requestPrinterUpdate(printerId);
                 } finally {
                     Binder.restoreCallingIdentity(identity);
                 }
index ffcc9c3..9d7cfdd 100644 (file)
@@ -21,8 +21,8 @@ import android.content.Context;
 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 @@ final class UserState implements PrintSpoolerCallbacks {
     }
 
     @Override
-    public void createPrinterDiscoverySession(IPrinterDiscoverySessionObserver observer) {
+    public void createPrinterDiscoverySession() {
         final List<RemotePrintService> services;
         synchronized (mLock) {
             throwIfDestroyedLocked();
@@ -117,7 +117,73 @@ final class UserState implements PrintSpoolerCallbacks {
         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);
         }
     }