PrintSpoolerService.java revision 4bd8fac48ed28494ca24e456e20b3058f78e8fe9
1/*
2 * Copyright (C) 2013 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.printspooler.model;
18
19import android.annotation.FloatRange;
20import android.annotation.NonNull;
21import android.annotation.Nullable;
22import android.annotation.StringRes;
23import android.app.Service;
24import android.content.ComponentName;
25import android.content.Context;
26import android.content.Intent;
27import android.graphics.drawable.Icon;
28import android.os.AsyncTask;
29import android.os.Bundle;
30import android.os.IBinder;
31import android.os.Message;
32import android.os.ParcelFileDescriptor;
33import android.os.RemoteException;
34import android.print.IPrintSpooler;
35import android.print.IPrintSpoolerCallbacks;
36import android.print.IPrintSpoolerClient;
37import android.print.PageRange;
38import android.print.PrintAttributes;
39import android.print.PrintAttributes.Margins;
40import android.print.PrintAttributes.MediaSize;
41import android.print.PrintAttributes.Resolution;
42import android.print.PrintDocumentInfo;
43import android.print.PrintJobId;
44import android.print.PrintJobInfo;
45import android.print.PrintManager;
46import android.print.PrinterId;
47import android.text.TextUtils;
48import android.util.ArrayMap;
49import android.util.AtomicFile;
50import android.util.Log;
51import android.util.Slog;
52import android.util.Xml;
53
54import com.android.internal.logging.MetricsLogger;
55import com.android.internal.os.HandlerCaller;
56import com.android.internal.util.FastXmlSerializer;
57import com.android.printspooler.R;
58import com.android.printspooler.util.ApprovedPrintServices;
59
60import libcore.io.IoUtils;
61
62import org.xmlpull.v1.XmlPullParser;
63import org.xmlpull.v1.XmlPullParserException;
64import org.xmlpull.v1.XmlSerializer;
65
66import java.io.File;
67import java.io.FileDescriptor;
68import java.io.FileInputStream;
69import java.io.FileNotFoundException;
70import java.io.FileOutputStream;
71import java.io.IOException;
72import java.io.PrintWriter;
73import java.nio.charset.StandardCharsets;
74import java.util.ArrayList;
75import java.util.List;
76import java.util.Set;
77
78/**
79 * Service for exposing some of the {@link PrintSpooler} functionality to
80 * another process.
81 */
82public final class PrintSpoolerService extends Service {
83
84    private static final String LOG_TAG = "PrintSpoolerService";
85
86    private static final boolean DEBUG_PRINT_JOB_LIFECYCLE = false;
87
88    private static final boolean DEBUG_PERSISTENCE = false;
89
90    private static final boolean PERSISTENCE_MANAGER_ENABLED = true;
91
92    private static final long CHECK_ALL_PRINTJOBS_HANDLED_DELAY = 5000;
93
94    private static final String PRINT_JOB_FILE_PREFIX = "print_job_";
95
96    private static final String PRINT_FILE_EXTENSION = "pdf";
97
98    private static final Object sLock = new Object();
99
100    private final Object mLock = new Object();
101
102    private final List<PrintJobInfo> mPrintJobs = new ArrayList<>();
103
104    private static PrintSpoolerService sInstance;
105
106    private IPrintSpoolerClient mClient;
107
108    private HandlerCaller mHandlerCaller;
109
110    private PersistenceManager mPersistanceManager;
111
112    private NotificationController mNotificationController;
113
114    /** Cache for custom printer icons loaded from the print service */
115    private CustomPrinterIconCache mCustomIconCache;
116
117    public static PrintSpoolerService peekInstance() {
118        synchronized (sLock) {
119            return sInstance;
120        }
121    }
122
123    @Override
124    public void onCreate() {
125        super.onCreate();
126        mHandlerCaller = new HandlerCaller(this, getMainLooper(),
127                new HandlerCallerCallback(), false);
128
129        mPersistanceManager = new PersistenceManager();
130        mNotificationController = new NotificationController(PrintSpoolerService.this);
131        mCustomIconCache = new CustomPrinterIconCache(getCacheDir());
132
133        synchronized (mLock) {
134            mPersistanceManager.readStateLocked();
135            handleReadPrintJobsLocked();
136        }
137
138        synchronized (sLock) {
139            sInstance = this;
140        }
141    }
142
143    @Override
144    public void onDestroy() {
145        super.onDestroy();
146    }
147
148    @Override
149    public IBinder onBind(Intent intent) {
150        return new PrintSpooler();
151    }
152
153    @Override
154    protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
155        String prefix = (args.length > 0) ? args[0] : "";
156        String tab = "  ";
157
158        synchronized (mLock) {
159            pw.append(prefix).append("print jobs:").println();
160            final int printJobCount = mPrintJobs.size();
161            for (int i = 0; i < printJobCount; i++) {
162                PrintJobInfo printJob = mPrintJobs.get(i);
163                pw.append(prefix).append(tab).append(printJob.toString());
164                pw.println();
165            }
166
167            pw.append(prefix).append("print job files:").println();
168            File[] files = getFilesDir().listFiles();
169            if (files != null) {
170                final int fileCount = files.length;
171                for (int i = 0; i < fileCount; i++) {
172                    File file = files[i];
173                    if (file.isFile() && file.getName().startsWith(PRINT_JOB_FILE_PREFIX)) {
174                        pw.append(prefix).append(tab).append(file.getName()).println();
175                    }
176                }
177            }
178        }
179
180        pw.append(prefix).append("approved print services:").println();
181        Set<String> approvedPrintServices = (new ApprovedPrintServices(this)).getApprovedServices();
182        if (approvedPrintServices != null) {
183            for (String approvedService : approvedPrintServices) {
184                pw.append(prefix).append(tab).append(approvedService).println();
185            }
186        }
187    }
188
189    private void sendOnPrintJobQueued(PrintJobInfo printJob) {
190        Message message = mHandlerCaller.obtainMessageO(
191                HandlerCallerCallback.MSG_ON_PRINT_JOB_QUEUED, printJob);
192        mHandlerCaller.executeOrSendMessage(message);
193    }
194
195    private void sendOnAllPrintJobsForServiceHandled(ComponentName service) {
196        Message message = mHandlerCaller.obtainMessageO(
197                HandlerCallerCallback.MSG_ON_ALL_PRINT_JOBS_FOR_SERIVICE_HANDLED, service);
198        mHandlerCaller.executeOrSendMessage(message);
199    }
200
201    private void sendOnAllPrintJobsHandled() {
202        Message message = mHandlerCaller.obtainMessage(
203                HandlerCallerCallback.MSG_ON_ALL_PRINT_JOBS_HANDLED);
204        mHandlerCaller.executeOrSendMessage(message);
205    }
206
207    private final class HandlerCallerCallback implements HandlerCaller.Callback {
208        public static final int MSG_SET_CLIENT = 1;
209        public static final int MSG_ON_PRINT_JOB_QUEUED = 2;
210        public static final int MSG_ON_ALL_PRINT_JOBS_FOR_SERIVICE_HANDLED = 3;
211        public static final int MSG_ON_ALL_PRINT_JOBS_HANDLED = 4;
212        public static final int MSG_CHECK_ALL_PRINTJOBS_HANDLED = 5;
213        public static final int MSG_ON_PRINT_JOB_STATE_CHANGED = 6;
214
215        @Override
216        public void executeMessage(Message message) {
217            switch (message.what) {
218                case MSG_SET_CLIENT: {
219                    synchronized (mLock) {
220                        mClient = (IPrintSpoolerClient) message.obj;
221                        if (mClient != null) {
222                            Message msg = mHandlerCaller.obtainMessage(
223                                    HandlerCallerCallback.MSG_CHECK_ALL_PRINTJOBS_HANDLED);
224                            mHandlerCaller.sendMessageDelayed(msg,
225                                    CHECK_ALL_PRINTJOBS_HANDLED_DELAY);
226                        }
227                    }
228                } break;
229
230                case MSG_ON_PRINT_JOB_QUEUED: {
231                    PrintJobInfo printJob = (PrintJobInfo) message.obj;
232                    if (mClient != null) {
233                        try {
234                            mClient.onPrintJobQueued(printJob);
235                        } catch (RemoteException re) {
236                            Slog.e(LOG_TAG, "Error notify for a queued print job.", re);
237                        }
238                    }
239                } break;
240
241                case MSG_ON_ALL_PRINT_JOBS_FOR_SERIVICE_HANDLED: {
242                    ComponentName service = (ComponentName) message.obj;
243                    if (mClient != null) {
244                        try {
245                            mClient.onAllPrintJobsForServiceHandled(service);
246                        } catch (RemoteException re) {
247                            Slog.e(LOG_TAG, "Error notify for all print jobs per service"
248                                    + " handled.", re);
249                        }
250                    }
251                } break;
252
253                case MSG_ON_ALL_PRINT_JOBS_HANDLED: {
254                    if (mClient != null) {
255                        try {
256                            mClient.onAllPrintJobsHandled();
257                        } catch (RemoteException re) {
258                            Slog.e(LOG_TAG, "Error notify for all print job handled.", re);
259                        }
260                    }
261                } break;
262
263                case MSG_CHECK_ALL_PRINTJOBS_HANDLED: {
264                    checkAllPrintJobsHandled();
265                } break;
266
267                case MSG_ON_PRINT_JOB_STATE_CHANGED: {
268                    if (mClient != null) {
269                        PrintJobInfo printJob = (PrintJobInfo) message.obj;
270                        try {
271                            mClient.onPrintJobStateChanged(printJob);
272                        } catch (RemoteException re) {
273                            Slog.e(LOG_TAG, "Error notify for print job state change.", re);
274                        }
275                    }
276                } break;
277            }
278        }
279    }
280
281    public List<PrintJobInfo> getPrintJobInfos(ComponentName componentName,
282            int state, int appId) {
283        List<PrintJobInfo> foundPrintJobs = null;
284        synchronized (mLock) {
285            final int printJobCount = mPrintJobs.size();
286            for (int i = 0; i < printJobCount; i++) {
287                PrintJobInfo printJob = mPrintJobs.get(i);
288                PrinterId printerId = printJob.getPrinterId();
289                final boolean sameComponent = (componentName == null
290                        || (printerId != null
291                        && componentName.equals(printerId.getServiceName())));
292                final boolean sameAppId = appId == PrintManager.APP_ID_ANY
293                        || printJob.getAppId() == appId;
294                final boolean sameState = (state == printJob.getState())
295                        || (state == PrintJobInfo.STATE_ANY)
296                        || (state == PrintJobInfo.STATE_ANY_VISIBLE_TO_CLIENTS
297                            && isStateVisibleToUser(printJob.getState()))
298                        || (state == PrintJobInfo.STATE_ANY_ACTIVE
299                            && isActiveState(printJob.getState()))
300                        || (state == PrintJobInfo.STATE_ANY_SCHEDULED
301                            && isScheduledState(printJob.getState()));
302                if (sameComponent && sameAppId && sameState) {
303                    if (foundPrintJobs == null) {
304                        foundPrintJobs = new ArrayList<>();
305                    }
306                    foundPrintJobs.add(printJob);
307                }
308            }
309        }
310        return foundPrintJobs;
311    }
312
313    private boolean isStateVisibleToUser(int state) {
314        return (isActiveState(state) && (state == PrintJobInfo.STATE_FAILED
315                || state == PrintJobInfo.STATE_COMPLETED || state == PrintJobInfo.STATE_CANCELED
316                || state == PrintJobInfo.STATE_BLOCKED));
317    }
318
319    public PrintJobInfo getPrintJobInfo(PrintJobId printJobId, int appId) {
320        synchronized (mLock) {
321            final int printJobCount = mPrintJobs.size();
322            for (int i = 0; i < printJobCount; i++) {
323                PrintJobInfo printJob = mPrintJobs.get(i);
324                if (printJob.getId().equals(printJobId)
325                        && (appId == PrintManager.APP_ID_ANY
326                        || appId == printJob.getAppId())) {
327                    return printJob;
328                }
329            }
330            return null;
331        }
332    }
333
334    public void createPrintJob(PrintJobInfo printJob) {
335        synchronized (mLock) {
336            addPrintJobLocked(printJob);
337            setPrintJobState(printJob.getId(), PrintJobInfo.STATE_CREATED, null);
338
339            Message message = mHandlerCaller.obtainMessageO(
340                    HandlerCallerCallback.MSG_ON_PRINT_JOB_STATE_CHANGED,
341                    printJob);
342            mHandlerCaller.executeOrSendMessage(message);
343        }
344    }
345
346    private void handleReadPrintJobsLocked() {
347        // Make a map with the files for a print job since we may have
348        // to delete some. One example of getting orphan files if the
349        // spooler crashes while constructing a print job. We do not
350        // persist partially populated print jobs under construction to
351        // avoid special handling for various attributes missing.
352        ArrayMap<PrintJobId, File> fileForJobMap = null;
353        File[] files = getFilesDir().listFiles();
354        if (files != null) {
355            final int fileCount = files.length;
356            for (int i = 0; i < fileCount; i++) {
357                File file = files[i];
358                if (file.isFile() && file.getName().startsWith(PRINT_JOB_FILE_PREFIX)) {
359                    if (fileForJobMap == null) {
360                        fileForJobMap = new ArrayMap<PrintJobId, File>();
361                    }
362                    String printJobIdString = file.getName().substring(
363                            PRINT_JOB_FILE_PREFIX.length(),
364                            file.getName().indexOf('.'));
365                    PrintJobId printJobId = PrintJobId.unflattenFromString(
366                            printJobIdString);
367                    fileForJobMap.put(printJobId, file);
368                }
369            }
370        }
371
372        final int printJobCount = mPrintJobs.size();
373        for (int i = 0; i < printJobCount; i++) {
374            PrintJobInfo printJob = mPrintJobs.get(i);
375
376            // We want to have only the orphan files at the end.
377            if (fileForJobMap != null) {
378                fileForJobMap.remove(printJob.getId());
379            }
380
381            switch (printJob.getState()) {
382                case PrintJobInfo.STATE_QUEUED:
383                case PrintJobInfo.STATE_STARTED:
384                case PrintJobInfo.STATE_BLOCKED: {
385                    // We have a print job that was queued or started or blocked in
386                    // the past but the device battery died or a crash occurred. In
387                    // this case we assume the print job failed and let the user
388                    // decide whether to restart the job or just cancel it.
389                    setPrintJobState(printJob.getId(), PrintJobInfo.STATE_FAILED,
390                            getString(R.string.no_connection_to_printer));
391                } break;
392            }
393        }
394
395        if (!mPrintJobs.isEmpty()) {
396            // Update the notification.
397            mNotificationController.onUpdateNotifications(mPrintJobs);
398        }
399
400        // Delete the orphan files.
401        if (fileForJobMap != null) {
402            final int orphanFileCount = fileForJobMap.size();
403            for (int i = 0; i < orphanFileCount; i++) {
404                File file = fileForJobMap.valueAt(i);
405                file.delete();
406            }
407        }
408    }
409
410    public void checkAllPrintJobsHandled() {
411        synchronized (mLock) {
412            if (!hasActivePrintJobsLocked()) {
413                notifyOnAllPrintJobsHandled();
414            }
415        }
416    }
417
418    public void writePrintJobData(final ParcelFileDescriptor fd, final PrintJobId printJobId) {
419        final PrintJobInfo printJob;
420        synchronized (mLock) {
421            printJob = getPrintJobInfo(printJobId, PrintManager.APP_ID_ANY);
422        }
423        new AsyncTask<Void, Void, Void>() {
424            @Override
425            protected Void doInBackground(Void... params) {
426                FileInputStream in = null;
427                FileOutputStream out = null;
428                try {
429                    if (printJob != null) {
430                        File file = generateFileForPrintJob(PrintSpoolerService.this, printJobId);
431                        in = new FileInputStream(file);
432                        out = new FileOutputStream(fd.getFileDescriptor());
433                    }
434                    final byte[] buffer = new byte[8192];
435                    while (true) {
436                        final int readByteCount = in.read(buffer);
437                        if (readByteCount < 0) {
438                            return null;
439                        }
440                        out.write(buffer, 0, readByteCount);
441                    }
442                } catch (FileNotFoundException fnfe) {
443                    Log.e(LOG_TAG, "Error writing print job data!", fnfe);
444                } catch (IOException ioe) {
445                    Log.e(LOG_TAG, "Error writing print job data!", ioe);
446                } finally {
447                    IoUtils.closeQuietly(in);
448                    IoUtils.closeQuietly(out);
449                    IoUtils.closeQuietly(fd);
450                }
451                Log.i(LOG_TAG, "[END WRITE]");
452                return null;
453            }
454        }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void[]) null);
455    }
456
457    public static File generateFileForPrintJob(Context context, PrintJobId printJobId) {
458        return new File(context.getFilesDir(), PRINT_JOB_FILE_PREFIX
459                + printJobId.flattenToString() + "." + PRINT_FILE_EXTENSION);
460    }
461
462    private void addPrintJobLocked(PrintJobInfo printJob) {
463        mPrintJobs.add(printJob);
464        if (DEBUG_PRINT_JOB_LIFECYCLE) {
465            Slog.i(LOG_TAG, "[ADD] " + printJob);
466        }
467    }
468
469    private void removeObsoletePrintJobs() {
470        synchronized (mLock) {
471            boolean persistState = false;
472            final int printJobCount = mPrintJobs.size();
473            for (int i = printJobCount - 1; i >= 0; i--) {
474                PrintJobInfo printJob = mPrintJobs.get(i);
475                if (isObsoleteState(printJob.getState())) {
476                    mPrintJobs.remove(i);
477                    if (DEBUG_PRINT_JOB_LIFECYCLE) {
478                        Slog.i(LOG_TAG, "[REMOVE] " + printJob.getId().flattenToString());
479                    }
480                    removePrintJobFileLocked(printJob.getId());
481                    persistState = true;
482                }
483            }
484            if (persistState) {
485                mPersistanceManager.writeStateLocked();
486            }
487        }
488    }
489
490    private void removePrintJobFileLocked(PrintJobId printJobId) {
491        File file = generateFileForPrintJob(PrintSpoolerService.this, printJobId);
492        if (file.exists()) {
493            file.delete();
494            if (DEBUG_PRINT_JOB_LIFECYCLE) {
495                Slog.i(LOG_TAG, "[REMOVE FILE FOR] " + printJobId);
496            }
497        }
498    }
499
500    /**
501     * Notify all interested parties that a print job has been updated.
502     *
503     * @param printJob The updated print job.
504     */
505    private void notifyPrintJobUpdated(PrintJobInfo printJob) {
506        Message message = mHandlerCaller.obtainMessageO(
507                HandlerCallerCallback.MSG_ON_PRINT_JOB_STATE_CHANGED,
508                printJob);
509        mHandlerCaller.executeOrSendMessage(message);
510
511        mNotificationController.onUpdateNotifications(mPrintJobs);
512    }
513
514    public boolean setPrintJobState(PrintJobId printJobId, int state, String error) {
515        boolean success = false;
516
517        synchronized (mLock) {
518            PrintJobInfo printJob = getPrintJobInfo(printJobId, PrintManager.APP_ID_ANY);
519            if (printJob != null) {
520                final int oldState = printJob.getState();
521                if (oldState == state) {
522                    return false;
523                }
524
525                success = true;
526
527                printJob.setState(state);
528                printJob.setStatus(error);
529                printJob.setCancelling(false);
530
531                if (DEBUG_PRINT_JOB_LIFECYCLE) {
532                    Slog.i(LOG_TAG, "[STATE CHANGED] " + printJob);
533                }
534
535                MetricsLogger.histogram(this, "print_job_state", state);
536                switch (state) {
537                    case PrintJobInfo.STATE_COMPLETED:
538                    case PrintJobInfo.STATE_CANCELED:
539                        mPrintJobs.remove(printJob);
540                        removePrintJobFileLocked(printJob.getId());
541                        // $fall-through$
542
543                    case PrintJobInfo.STATE_FAILED: {
544                        PrinterId printerId = printJob.getPrinterId();
545                        if (printerId != null) {
546                            ComponentName service = printerId.getServiceName();
547                            if (!hasActivePrintJobsForServiceLocked(service)) {
548                                sendOnAllPrintJobsForServiceHandled(service);
549                            }
550                        }
551                    } break;
552
553                    case PrintJobInfo.STATE_QUEUED: {
554                        sendOnPrintJobQueued(new PrintJobInfo(printJob));
555                    }  break;
556                }
557
558                if (shouldPersistPrintJob(printJob)) {
559                    mPersistanceManager.writeStateLocked();
560                }
561
562                if (!hasActivePrintJobsLocked()) {
563                    notifyOnAllPrintJobsHandled();
564                }
565
566                notifyPrintJobUpdated(printJob);
567            }
568        }
569
570        return success;
571    }
572
573    /**
574     * Set the progress for a print job.
575     *
576     * @param printJobId ID of the print job to update
577     * @param progress the new progress
578     */
579    public void setProgress(@NonNull PrintJobId printJobId,
580            @FloatRange(from=0.0, to=1.0) float progress) {
581        synchronized (mLock) {
582            getPrintJobInfo(printJobId, PrintManager.APP_ID_ANY).setProgress(progress);
583
584            mNotificationController.onUpdateNotifications(mPrintJobs);
585        }
586    }
587
588    /**
589     * Set the status for a print job.
590     *
591     * @param printJobId ID of the print job to update
592     * @param status the new status
593     */
594    public void setStatus(@NonNull PrintJobId printJobId, @Nullable CharSequence status) {
595        synchronized (mLock) {
596            PrintJobInfo printJob = getPrintJobInfo(printJobId, PrintManager.APP_ID_ANY);
597
598            if (printJob != null) {
599                printJob.setStatus(status);
600                notifyPrintJobUpdated(printJob);
601            }
602        }
603    }
604
605    /**
606     * Set the status for a print job.
607     *
608     * @param printJobId ID of the print job to update
609     * @param status the new status as a string resource
610     * @param appPackageName app package the resource belongs to
611     */
612    public void setStatus(@NonNull PrintJobId printJobId, @StringRes int status,
613            @Nullable CharSequence appPackageName) {
614        synchronized (mLock) {
615            PrintJobInfo printJob = getPrintJobInfo(printJobId, PrintManager.APP_ID_ANY);
616
617            if (printJob != null) {
618                printJob.setStatus(status, appPackageName);
619                notifyPrintJobUpdated(printJob);
620            }
621        }
622    }
623
624    public boolean hasActivePrintJobsLocked() {
625        final int printJobCount = mPrintJobs.size();
626        for (int i = 0; i < printJobCount; i++) {
627            PrintJobInfo printJob = mPrintJobs.get(i);
628            if (isActiveState(printJob.getState())) {
629                return true;
630            }
631        }
632        return false;
633    }
634
635    public boolean hasActivePrintJobsForServiceLocked(ComponentName service) {
636        final int printJobCount = mPrintJobs.size();
637        for (int i = 0; i < printJobCount; i++) {
638            PrintJobInfo printJob = mPrintJobs.get(i);
639            if (isActiveState(printJob.getState()) && printJob.getPrinterId() != null
640                    && printJob.getPrinterId().getServiceName().equals(service)) {
641                return true;
642            }
643        }
644        return false;
645    }
646
647    private boolean isObsoleteState(int printJobState) {
648        return (isTerminalState(printJobState)
649                || printJobState == PrintJobInfo.STATE_QUEUED);
650    }
651
652    private boolean isScheduledState(int printJobState) {
653        return printJobState == PrintJobInfo.STATE_QUEUED
654                || printJobState == PrintJobInfo.STATE_STARTED
655                || printJobState == PrintJobInfo.STATE_BLOCKED;
656    }
657
658    private boolean isActiveState(int printJobState) {
659        return printJobState == PrintJobInfo.STATE_CREATED
660                || printJobState == PrintJobInfo.STATE_QUEUED
661                || printJobState == PrintJobInfo.STATE_STARTED
662                || printJobState == PrintJobInfo.STATE_BLOCKED;
663    }
664
665    private boolean isTerminalState(int printJobState) {
666        return printJobState == PrintJobInfo.STATE_COMPLETED
667                || printJobState == PrintJobInfo.STATE_CANCELED;
668    }
669
670    public boolean setPrintJobTag(PrintJobId printJobId, String tag) {
671        synchronized (mLock) {
672            PrintJobInfo printJob = getPrintJobInfo(printJobId, PrintManager.APP_ID_ANY);
673            if (printJob != null) {
674                String printJobTag = printJob.getTag();
675                if (printJobTag == null) {
676                    if (tag == null) {
677                        return false;
678                    }
679                } else if (printJobTag.equals(tag)) {
680                    return false;
681                }
682                printJob.setTag(tag);
683                if (shouldPersistPrintJob(printJob)) {
684                    mPersistanceManager.writeStateLocked();
685                }
686                return true;
687            }
688        }
689        return false;
690    }
691
692    public void setPrintJobCancelling(PrintJobId printJobId, boolean cancelling) {
693        synchronized (mLock) {
694            PrintJobInfo printJob = getPrintJobInfo(printJobId, PrintManager.APP_ID_ANY);
695            if (printJob != null) {
696                printJob.setCancelling(cancelling);
697                if (shouldPersistPrintJob(printJob)) {
698                    mPersistanceManager.writeStateLocked();
699                }
700                mNotificationController.onUpdateNotifications(mPrintJobs);
701
702                Message message = mHandlerCaller.obtainMessageO(
703                        HandlerCallerCallback.MSG_ON_PRINT_JOB_STATE_CHANGED,
704                        printJob);
705                mHandlerCaller.executeOrSendMessage(message);
706            }
707        }
708    }
709
710    public void updatePrintJobUserConfigurableOptionsNoPersistence(PrintJobInfo printJob) {
711        synchronized (mLock) {
712            final int printJobCount = mPrintJobs.size();
713            for (int i = 0; i < printJobCount; i++) {
714                PrintJobInfo cachedPrintJob = mPrintJobs.get(i);
715                if (cachedPrintJob.getId().equals(printJob.getId())) {
716                    cachedPrintJob.setPrinterId(printJob.getPrinterId());
717                    cachedPrintJob.setPrinterName(printJob.getPrinterName());
718                    cachedPrintJob.setCopies(printJob.getCopies());
719                    cachedPrintJob.setDocumentInfo(printJob.getDocumentInfo());
720                    cachedPrintJob.setPages(printJob.getPages());
721                    cachedPrintJob.setAttributes(printJob.getAttributes());
722                    cachedPrintJob.setAdvancedOptions(printJob.getAdvancedOptions());
723                    return;
724                }
725            }
726            throw new IllegalArgumentException("No print job with id:" + printJob.getId());
727        }
728    }
729
730    private boolean shouldPersistPrintJob(PrintJobInfo printJob) {
731        return printJob.getState() >= PrintJobInfo.STATE_QUEUED;
732    }
733
734    private void notifyOnAllPrintJobsHandled() {
735        // This has to run on the tread that is persisting the current state
736        // since this call may result in the system unbinding from the spooler
737        // and as a result the spooler process may get killed before the write
738        // completes.
739        new AsyncTask<Void, Void, Void>() {
740            @Override
741            protected Void doInBackground(Void... params) {
742                sendOnAllPrintJobsHandled();
743                return null;
744            }
745        }.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR, (Void[]) null);
746    }
747
748    /**
749     * Handle that a custom icon for a printer was loaded.
750     *
751     * @param printerId the id of the printer the icon belongs to
752     * @param icon the icon that was loaded
753     * @see android.print.PrinterInfo.Builder#setHasCustomPrinterIcon()
754     */
755    public void onCustomPrinterIconLoaded(PrinterId printerId, Icon icon) {
756        mCustomIconCache.onCustomPrinterIconLoaded(printerId, icon);
757    }
758
759    /**
760     * Get the custom icon for a printer. If the icon is not cached, the icon is
761     * requested asynchronously. Once it is available the printer is updated.
762     *
763     * @param printerId the id of the printer the icon should be loaded for
764     * @return the custom icon to be used for the printer or null if the icon is
765     *         not yet available
766     * @see android.print.PrinterInfo.Builder#setHasCustomPrinterIcon()
767     */
768    public Icon getCustomPrinterIcon(PrinterId printerId) {
769        return mCustomIconCache.getIcon(printerId);
770    }
771
772    /**
773     * Clear the custom printer icon cache.
774     */
775    public void clearCustomPrinterIconCache() {
776        mCustomIconCache.clear();
777    }
778
779    private final class PersistenceManager {
780        private static final String PERSIST_FILE_NAME = "print_spooler_state.xml";
781
782        private static final String TAG_SPOOLER = "spooler";
783        private static final String TAG_JOB = "job";
784
785        private static final String TAG_PRINTER_ID = "printerId";
786        private static final String TAG_PAGE_RANGE = "pageRange";
787        private static final String TAG_ATTRIBUTES = "attributes";
788        private static final String TAG_DOCUMENT_INFO = "documentInfo";
789
790        private static final String ATTR_ID = "id";
791        private static final String ATTR_LABEL = "label";
792        private static final String ATTR_LABEL_RES_ID = "labelResId";
793        private static final String ATTR_PACKAGE_NAME = "packageName";
794        private static final String ATTR_STATE = "state";
795        private static final String ATTR_APP_ID = "appId";
796        private static final String ATTR_TAG = "tag";
797        private static final String ATTR_CREATION_TIME = "creationTime";
798        private static final String ATTR_COPIES = "copies";
799        private static final String ATTR_PRINTER_NAME = "printerName";
800        private static final String ATTR_STATE_REASON = "stateReason";
801        private static final String ATTR_STATUS = "status";
802        private static final String ATTR_PROGRESS = "progress";
803        private static final String ATTR_CANCELLING = "cancelling";
804
805        private static final String TAG_ADVANCED_OPTIONS = "advancedOptions";
806        private static final String TAG_ADVANCED_OPTION = "advancedOption";
807        private static final String ATTR_KEY = "key";
808        private static final String ATTR_TYPE = "type";
809        private static final String ATTR_VALUE = "value";
810        private static final String TYPE_STRING = "string";
811        private static final String TYPE_INT = "int";
812
813        private static final String TAG_MEDIA_SIZE = "mediaSize";
814        private static final String TAG_RESOLUTION = "resolution";
815        private static final String TAG_MARGINS = "margins";
816
817        private static final String ATTR_COLOR_MODE = "colorMode";
818        private static final String ATTR_DUPLEX_MODE = "duplexMode";
819
820        private static final String ATTR_LOCAL_ID = "localId";
821        private static final String ATTR_SERVICE_NAME = "serviceName";
822
823        private static final String ATTR_WIDTH_MILS = "widthMils";
824        private static final String ATTR_HEIGHT_MILS = "heightMils";
825
826        private static final String ATTR_HORIZONTAL_DPI = "horizontalDip";
827        private static final String ATTR_VERTICAL_DPI = "verticalDpi";
828
829        private static final String ATTR_LEFT_MILS = "leftMils";
830        private static final String ATTR_TOP_MILS = "topMils";
831        private static final String ATTR_RIGHT_MILS = "rightMils";
832        private static final String ATTR_BOTTOM_MILS = "bottomMils";
833
834        private static final String ATTR_START = "start";
835        private static final String ATTR_END = "end";
836
837        private static final String ATTR_NAME = "name";
838        private static final String ATTR_PAGE_COUNT = "pageCount";
839        private static final String ATTR_CONTENT_TYPE = "contentType";
840        private static final String ATTR_DATA_SIZE = "dataSize";
841
842        private final AtomicFile mStatePersistFile;
843
844        private boolean mWriteStateScheduled;
845
846        private PersistenceManager() {
847            mStatePersistFile = new AtomicFile(new File(getFilesDir(),
848                    PERSIST_FILE_NAME));
849        }
850
851        public void writeStateLocked() {
852            if (!PERSISTENCE_MANAGER_ENABLED) {
853                return;
854            }
855            if (mWriteStateScheduled) {
856                return;
857            }
858            mWriteStateScheduled = true;
859            new AsyncTask<Void, Void, Void>() {
860                @Override
861                protected Void doInBackground(Void... params) {
862                    synchronized (mLock) {
863                        mWriteStateScheduled = false;
864                        doWriteStateLocked();
865                    }
866                    return null;
867                }
868            }.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR, (Void[]) null);
869        }
870
871        private void doWriteStateLocked() {
872            if (DEBUG_PERSISTENCE) {
873                Log.i(LOG_TAG, "[PERSIST START]");
874            }
875            FileOutputStream out = null;
876            try {
877                out = mStatePersistFile.startWrite();
878
879                XmlSerializer serializer = new FastXmlSerializer();
880                serializer.setOutput(out, StandardCharsets.UTF_8.name());
881                serializer.startDocument(null, true);
882                serializer.startTag(null, TAG_SPOOLER);
883
884                List<PrintJobInfo> printJobs = mPrintJobs;
885
886                final int printJobCount = printJobs.size();
887                for (int j = 0; j < printJobCount; j++) {
888                    PrintJobInfo printJob = printJobs.get(j);
889
890                    if (!shouldPersistPrintJob(printJob)) {
891                        continue;
892                    }
893
894                    serializer.startTag(null, TAG_JOB);
895
896                    serializer.attribute(null, ATTR_ID, printJob.getId().flattenToString());
897                    serializer.attribute(null, ATTR_LABEL, printJob.getLabel().toString());
898                    serializer.attribute(null, ATTR_STATE, String.valueOf(printJob.getState()));
899                    serializer.attribute(null, ATTR_APP_ID, String.valueOf(printJob.getAppId()));
900                    String tag = printJob.getTag();
901                    if (tag != null) {
902                        serializer.attribute(null, ATTR_TAG, tag);
903                    }
904                    serializer.attribute(null, ATTR_CREATION_TIME, String.valueOf(
905                            printJob.getCreationTime()));
906                    serializer.attribute(null, ATTR_COPIES, String.valueOf(printJob.getCopies()));
907                    String printerName = printJob.getPrinterName();
908                    if (!TextUtils.isEmpty(printerName)) {
909                        serializer.attribute(null, ATTR_PRINTER_NAME, printerName);
910                    }
911                    serializer.attribute(null, ATTR_CANCELLING, String.valueOf(
912                            printJob.isCancelling()));
913
914                    float progress = printJob.getProgress();
915                    if (progress != Float.NaN) {
916                        serializer.attribute(null, ATTR_PROGRESS, String.valueOf(progress));
917                    }
918
919                    CharSequence status = printJob.getStatus(getPackageManager());
920                    if (!TextUtils.isEmpty(status)) {
921                        serializer.attribute(null, ATTR_STATUS, status.toString());
922                    }
923
924                    PrinterId printerId = printJob.getPrinterId();
925                    if (printerId != null) {
926                        serializer.startTag(null, TAG_PRINTER_ID);
927                        serializer.attribute(null, ATTR_LOCAL_ID, printerId.getLocalId());
928                        serializer.attribute(null, ATTR_SERVICE_NAME, printerId.getServiceName()
929                                .flattenToString());
930                        serializer.endTag(null, TAG_PRINTER_ID);
931                    }
932
933                    PageRange[] pages = printJob.getPages();
934                    if (pages != null) {
935                        for (int i = 0; i < pages.length; i++) {
936                            serializer.startTag(null, TAG_PAGE_RANGE);
937                            serializer.attribute(null, ATTR_START, String.valueOf(
938                                    pages[i].getStart()));
939                            serializer.attribute(null, ATTR_END, String.valueOf(
940                                    pages[i].getEnd()));
941                            serializer.endTag(null, TAG_PAGE_RANGE);
942                        }
943                    }
944
945                    PrintAttributes attributes = printJob.getAttributes();
946                    if (attributes != null) {
947                        serializer.startTag(null, TAG_ATTRIBUTES);
948
949                        final int colorMode = attributes.getColorMode();
950                        serializer.attribute(null, ATTR_COLOR_MODE,
951                                String.valueOf(colorMode));
952
953                        final int duplexMode = attributes.getDuplexMode();
954                        serializer.attribute(null, ATTR_DUPLEX_MODE,
955                                String.valueOf(duplexMode));
956
957                        MediaSize mediaSize = attributes.getMediaSize();
958                        if (mediaSize != null) {
959                            serializer.startTag(null, TAG_MEDIA_SIZE);
960                            serializer.attribute(null, ATTR_ID, mediaSize.getId());
961                            serializer.attribute(null, ATTR_WIDTH_MILS, String.valueOf(
962                                    mediaSize.getWidthMils()));
963                            serializer.attribute(null, ATTR_HEIGHT_MILS, String.valueOf(
964                                    mediaSize.getHeightMils()));
965                            // We prefer to store only the package name and
966                            // resource id and fallback to the label.
967                            if (!TextUtils.isEmpty(mediaSize.mPackageName)
968                                    && mediaSize.mLabelResId > 0) {
969                                serializer.attribute(null, ATTR_PACKAGE_NAME,
970                                        mediaSize.mPackageName);
971                                serializer.attribute(null, ATTR_LABEL_RES_ID,
972                                        String.valueOf(mediaSize.mLabelResId));
973                            } else {
974                                serializer.attribute(null, ATTR_LABEL,
975                                        mediaSize.getLabel(getPackageManager()));
976                            }
977                            serializer.endTag(null, TAG_MEDIA_SIZE);
978                        }
979
980                        Resolution resolution = attributes.getResolution();
981                        if (resolution != null) {
982                            serializer.startTag(null, TAG_RESOLUTION);
983                            serializer.attribute(null, ATTR_ID, resolution.getId());
984                            serializer.attribute(null, ATTR_HORIZONTAL_DPI, String.valueOf(
985                                    resolution.getHorizontalDpi()));
986                            serializer.attribute(null, ATTR_VERTICAL_DPI, String.valueOf(
987                                    resolution.getVerticalDpi()));
988                            serializer.attribute(null, ATTR_LABEL,
989                                    resolution.getLabel());
990                            serializer.endTag(null, TAG_RESOLUTION);
991                        }
992
993                        Margins margins = attributes.getMinMargins();
994                        if (margins != null) {
995                            serializer.startTag(null, TAG_MARGINS);
996                            serializer.attribute(null, ATTR_LEFT_MILS, String.valueOf(
997                                    margins.getLeftMils()));
998                            serializer.attribute(null, ATTR_TOP_MILS, String.valueOf(
999                                    margins.getTopMils()));
1000                            serializer.attribute(null, ATTR_RIGHT_MILS, String.valueOf(
1001                                    margins.getRightMils()));
1002                            serializer.attribute(null, ATTR_BOTTOM_MILS, String.valueOf(
1003                                    margins.getBottomMils()));
1004                            serializer.endTag(null, TAG_MARGINS);
1005                        }
1006
1007                        serializer.endTag(null, TAG_ATTRIBUTES);
1008                    }
1009
1010                    PrintDocumentInfo documentInfo = printJob.getDocumentInfo();
1011                    if (documentInfo != null) {
1012                        serializer.startTag(null, TAG_DOCUMENT_INFO);
1013                        serializer.attribute(null, ATTR_NAME, documentInfo.getName());
1014                        serializer.attribute(null, ATTR_CONTENT_TYPE, String.valueOf(
1015                                documentInfo.getContentType()));
1016                        serializer.attribute(null, ATTR_PAGE_COUNT, String.valueOf(
1017                                documentInfo.getPageCount()));
1018                        serializer.attribute(null, ATTR_DATA_SIZE, String.valueOf(
1019                                documentInfo.getDataSize()));
1020                        serializer.endTag(null, TAG_DOCUMENT_INFO);
1021                    }
1022
1023                    Bundle advancedOptions = printJob.getAdvancedOptions();
1024                    if (advancedOptions != null) {
1025                        serializer.startTag(null, TAG_ADVANCED_OPTIONS);
1026                        for (String key : advancedOptions.keySet()) {
1027                            Object value = advancedOptions.get(key);
1028                            if (value instanceof String) {
1029                                String stringValue = (String) value;
1030                                serializer.startTag(null, TAG_ADVANCED_OPTION);
1031                                serializer.attribute(null, ATTR_KEY, key);
1032                                serializer.attribute(null, ATTR_TYPE, TYPE_STRING);
1033                                serializer.attribute(null, ATTR_VALUE, stringValue);
1034                                serializer.endTag(null, TAG_ADVANCED_OPTION);
1035                            } else if (value instanceof Integer) {
1036                                String intValue = Integer.toString((Integer) value);
1037                                serializer.startTag(null, TAG_ADVANCED_OPTION);
1038                                serializer.attribute(null, ATTR_KEY, key);
1039                                serializer.attribute(null, ATTR_TYPE, TYPE_INT);
1040                                serializer.attribute(null, ATTR_VALUE, intValue);
1041                                serializer.endTag(null, TAG_ADVANCED_OPTION);
1042                            }
1043                        }
1044                        serializer.endTag(null, TAG_ADVANCED_OPTIONS);
1045                    }
1046
1047                    serializer.endTag(null, TAG_JOB);
1048
1049                    if (DEBUG_PERSISTENCE) {
1050                        Log.i(LOG_TAG, "[PERSISTED] " + printJob);
1051                    }
1052                }
1053
1054                serializer.endTag(null, TAG_SPOOLER);
1055                serializer.endDocument();
1056                mStatePersistFile.finishWrite(out);
1057                if (DEBUG_PERSISTENCE) {
1058                    Log.i(LOG_TAG, "[PERSIST END]");
1059                }
1060            } catch (IOException e) {
1061                Slog.w(LOG_TAG, "Failed to write state, restoring backup.", e);
1062                mStatePersistFile.failWrite(out);
1063            } finally {
1064                IoUtils.closeQuietly(out);
1065            }
1066        }
1067
1068        public void readStateLocked() {
1069            if (!PERSISTENCE_MANAGER_ENABLED) {
1070                return;
1071            }
1072            FileInputStream in = null;
1073            try {
1074                in = mStatePersistFile.openRead();
1075            } catch (FileNotFoundException e) {
1076                if (DEBUG_PERSISTENCE) {
1077                    Log.d(LOG_TAG, "No existing print spooler state.");
1078                }
1079                return;
1080            }
1081            try {
1082                XmlPullParser parser = Xml.newPullParser();
1083                parser.setInput(in, StandardCharsets.UTF_8.name());
1084                parseState(parser);
1085            } catch (IllegalStateException ise) {
1086                Slog.w(LOG_TAG, "Failed parsing ", ise);
1087            } catch (NullPointerException npe) {
1088                Slog.w(LOG_TAG, "Failed parsing ", npe);
1089            } catch (NumberFormatException nfe) {
1090                Slog.w(LOG_TAG, "Failed parsing ", nfe);
1091            } catch (XmlPullParserException xppe) {
1092                Slog.w(LOG_TAG, "Failed parsing ", xppe);
1093            } catch (IOException ioe) {
1094                Slog.w(LOG_TAG, "Failed parsing ", ioe);
1095            } catch (IndexOutOfBoundsException iobe) {
1096                Slog.w(LOG_TAG, "Failed parsing ", iobe);
1097            } finally {
1098                IoUtils.closeQuietly(in);
1099            }
1100        }
1101
1102        private void parseState(XmlPullParser parser)
1103                throws IOException, XmlPullParserException {
1104            parser.next();
1105            skipEmptyTextTags(parser);
1106            expect(parser, XmlPullParser.START_TAG, TAG_SPOOLER);
1107            parser.next();
1108
1109            while (parsePrintJob(parser)) {
1110                parser.next();
1111            }
1112
1113            skipEmptyTextTags(parser);
1114            expect(parser, XmlPullParser.END_TAG, TAG_SPOOLER);
1115        }
1116
1117        private boolean parsePrintJob(XmlPullParser parser)
1118                throws IOException, XmlPullParserException {
1119            skipEmptyTextTags(parser);
1120            if (!accept(parser, XmlPullParser.START_TAG, TAG_JOB)) {
1121                return false;
1122            }
1123
1124            PrintJobInfo printJob = new PrintJobInfo();
1125
1126            PrintJobId printJobId = PrintJobId.unflattenFromString(
1127                    parser.getAttributeValue(null, ATTR_ID));
1128            printJob.setId(printJobId);
1129            String label = parser.getAttributeValue(null, ATTR_LABEL);
1130            printJob.setLabel(label);
1131            final int state = Integer.parseInt(parser.getAttributeValue(null, ATTR_STATE));
1132            printJob.setState(state);
1133            final int appId = Integer.parseInt(parser.getAttributeValue(null, ATTR_APP_ID));
1134            printJob.setAppId(appId);
1135            String tag = parser.getAttributeValue(null, ATTR_TAG);
1136            printJob.setTag(tag);
1137            String creationTime = parser.getAttributeValue(null, ATTR_CREATION_TIME);
1138            printJob.setCreationTime(Long.parseLong(creationTime));
1139            String copies = parser.getAttributeValue(null, ATTR_COPIES);
1140            printJob.setCopies(Integer.parseInt(copies));
1141            String printerName = parser.getAttributeValue(null, ATTR_PRINTER_NAME);
1142            printJob.setPrinterName(printerName);
1143
1144            String progressString = parser.getAttributeValue(null, ATTR_PROGRESS);
1145            if (progressString != null) {
1146                float progress = Float.parseFloat(progressString);
1147
1148                if (progress != -1) {
1149                    printJob.setProgress(progress);
1150                }
1151            }
1152
1153            CharSequence status = parser.getAttributeValue(null, ATTR_STATUS);
1154            printJob.setStatus(status);
1155
1156            // stateReason is deprecated, but might be used by old print jobs
1157            String stateReason = parser.getAttributeValue(null, ATTR_STATE_REASON);
1158            if (stateReason != null) {
1159                printJob.setStatus(stateReason);
1160            }
1161
1162            String cancelling = parser.getAttributeValue(null, ATTR_CANCELLING);
1163            printJob.setCancelling(!TextUtils.isEmpty(cancelling)
1164                    ? Boolean.parseBoolean(cancelling) : false);
1165
1166            parser.next();
1167
1168            skipEmptyTextTags(parser);
1169            if (accept(parser, XmlPullParser.START_TAG, TAG_PRINTER_ID)) {
1170                String localId = parser.getAttributeValue(null, ATTR_LOCAL_ID);
1171                ComponentName service = ComponentName.unflattenFromString(parser.getAttributeValue(
1172                        null, ATTR_SERVICE_NAME));
1173                printJob.setPrinterId(new PrinterId(service, localId));
1174                parser.next();
1175                skipEmptyTextTags(parser);
1176                expect(parser, XmlPullParser.END_TAG, TAG_PRINTER_ID);
1177                parser.next();
1178            }
1179
1180            skipEmptyTextTags(parser);
1181            List<PageRange> pageRanges = null;
1182            while (accept(parser, XmlPullParser.START_TAG, TAG_PAGE_RANGE)) {
1183                final int start = Integer.parseInt(parser.getAttributeValue(null, ATTR_START));
1184                final int end = Integer.parseInt(parser.getAttributeValue(null, ATTR_END));
1185                PageRange pageRange = new PageRange(start, end);
1186                if (pageRanges == null) {
1187                    pageRanges = new ArrayList<PageRange>();
1188                }
1189                pageRanges.add(pageRange);
1190                parser.next();
1191                skipEmptyTextTags(parser);
1192                expect(parser, XmlPullParser.END_TAG, TAG_PAGE_RANGE);
1193                parser.next();
1194                skipEmptyTextTags(parser);
1195            }
1196            if (pageRanges != null) {
1197                PageRange[] pageRangesArray = new PageRange[pageRanges.size()];
1198                pageRanges.toArray(pageRangesArray);
1199                printJob.setPages(pageRangesArray);
1200            }
1201
1202            skipEmptyTextTags(parser);
1203            if (accept(parser, XmlPullParser.START_TAG, TAG_ATTRIBUTES)) {
1204
1205                PrintAttributes.Builder builder = new PrintAttributes.Builder();
1206
1207                String colorMode = parser.getAttributeValue(null, ATTR_COLOR_MODE);
1208                builder.setColorMode(Integer.parseInt(colorMode));
1209
1210                String duplexMode = parser.getAttributeValue(null, ATTR_DUPLEX_MODE);
1211                // Duplex mode was added later, so null check is needed.
1212                if (duplexMode != null) {
1213                    builder.setDuplexMode(Integer.parseInt(duplexMode));
1214                }
1215
1216                parser.next();
1217
1218                skipEmptyTextTags(parser);
1219                if (accept(parser, XmlPullParser.START_TAG, TAG_MEDIA_SIZE)) {
1220                    String id = parser.getAttributeValue(null, ATTR_ID);
1221                    label = parser.getAttributeValue(null, ATTR_LABEL);
1222                    final int widthMils = Integer.parseInt(parser.getAttributeValue(null,
1223                            ATTR_WIDTH_MILS));
1224                    final int heightMils = Integer.parseInt(parser.getAttributeValue(null,
1225                            ATTR_HEIGHT_MILS));
1226                    String packageName = parser.getAttributeValue(null, ATTR_PACKAGE_NAME);
1227                    String labelResIdString = parser.getAttributeValue(null, ATTR_LABEL_RES_ID);
1228                    final int labelResId = (labelResIdString != null)
1229                            ? Integer.parseInt(labelResIdString) : 0;
1230                    label = parser.getAttributeValue(null, ATTR_LABEL);
1231                    MediaSize mediaSize = new MediaSize(id, label, packageName,
1232                                widthMils, heightMils, labelResId);
1233                    builder.setMediaSize(mediaSize);
1234                    parser.next();
1235                    skipEmptyTextTags(parser);
1236                    expect(parser, XmlPullParser.END_TAG, TAG_MEDIA_SIZE);
1237                    parser.next();
1238                }
1239
1240                skipEmptyTextTags(parser);
1241                if (accept(parser, XmlPullParser.START_TAG, TAG_RESOLUTION)) {
1242                    String id = parser.getAttributeValue(null, ATTR_ID);
1243                    label = parser.getAttributeValue(null, ATTR_LABEL);
1244                    final int horizontalDpi = Integer.parseInt(parser.getAttributeValue(null,
1245                            ATTR_HORIZONTAL_DPI));
1246                    final int verticalDpi = Integer.parseInt(parser.getAttributeValue(null,
1247                            ATTR_VERTICAL_DPI));
1248                    Resolution resolution = new Resolution(id, label, horizontalDpi, verticalDpi);
1249                    builder.setResolution(resolution);
1250                    parser.next();
1251                    skipEmptyTextTags(parser);
1252                    expect(parser, XmlPullParser.END_TAG, TAG_RESOLUTION);
1253                    parser.next();
1254                }
1255
1256                skipEmptyTextTags(parser);
1257                if (accept(parser, XmlPullParser.START_TAG, TAG_MARGINS)) {
1258                    final int leftMils = Integer.parseInt(parser.getAttributeValue(null,
1259                            ATTR_LEFT_MILS));
1260                    final int topMils = Integer.parseInt(parser.getAttributeValue(null,
1261                            ATTR_TOP_MILS));
1262                    final int rightMils = Integer.parseInt(parser.getAttributeValue(null,
1263                            ATTR_RIGHT_MILS));
1264                    final int bottomMils = Integer.parseInt(parser.getAttributeValue(null,
1265                            ATTR_BOTTOM_MILS));
1266                    Margins margins = new Margins(leftMils, topMils, rightMils, bottomMils);
1267                    builder.setMinMargins(margins);
1268                    parser.next();
1269                    skipEmptyTextTags(parser);
1270                    expect(parser, XmlPullParser.END_TAG, TAG_MARGINS);
1271                    parser.next();
1272                }
1273
1274                printJob.setAttributes(builder.build());
1275
1276                skipEmptyTextTags(parser);
1277                expect(parser, XmlPullParser.END_TAG, TAG_ATTRIBUTES);
1278                parser.next();
1279            }
1280
1281            skipEmptyTextTags(parser);
1282            if (accept(parser, XmlPullParser.START_TAG, TAG_DOCUMENT_INFO)) {
1283                String name = parser.getAttributeValue(null, ATTR_NAME);
1284                final int pageCount = Integer.parseInt(parser.getAttributeValue(null,
1285                        ATTR_PAGE_COUNT));
1286                final int contentType = Integer.parseInt(parser.getAttributeValue(null,
1287                        ATTR_CONTENT_TYPE));
1288                final int dataSize = Integer.parseInt(parser.getAttributeValue(null,
1289                        ATTR_DATA_SIZE));
1290                PrintDocumentInfo info = new PrintDocumentInfo.Builder(name)
1291                        .setPageCount(pageCount)
1292                        .setContentType(contentType).build();
1293                printJob.setDocumentInfo(info);
1294                info.setDataSize(dataSize);
1295                parser.next();
1296                skipEmptyTextTags(parser);
1297                expect(parser, XmlPullParser.END_TAG, TAG_DOCUMENT_INFO);
1298                parser.next();
1299            }
1300
1301            skipEmptyTextTags(parser);
1302            if (accept(parser, XmlPullParser.START_TAG, TAG_ADVANCED_OPTIONS)) {
1303                parser.next();
1304                skipEmptyTextTags(parser);
1305                Bundle advancedOptions = new Bundle();
1306                while (accept(parser, XmlPullParser.START_TAG, TAG_ADVANCED_OPTION)) {
1307                    String key = parser.getAttributeValue(null, ATTR_KEY);
1308                    String value = parser.getAttributeValue(null, ATTR_VALUE);
1309                    String type = parser.getAttributeValue(null, ATTR_TYPE);
1310                    if (TYPE_STRING.equals(type)) {
1311                        advancedOptions.putString(key, value);
1312                    } else if (TYPE_INT.equals(type)) {
1313                        advancedOptions.putInt(key, Integer.valueOf(value));
1314                    }
1315                    parser.next();
1316                    skipEmptyTextTags(parser);
1317                    expect(parser, XmlPullParser.END_TAG, TAG_ADVANCED_OPTION);
1318                    parser.next();
1319                    skipEmptyTextTags(parser);
1320                }
1321                printJob.setAdvancedOptions(advancedOptions);
1322                skipEmptyTextTags(parser);
1323                expect(parser, XmlPullParser.END_TAG, TAG_ADVANCED_OPTIONS);
1324                parser.next();
1325            }
1326
1327            mPrintJobs.add(printJob);
1328
1329            if (DEBUG_PERSISTENCE) {
1330                Log.i(LOG_TAG, "[RESTORED] " + printJob);
1331            }
1332
1333            skipEmptyTextTags(parser);
1334            expect(parser, XmlPullParser.END_TAG, TAG_JOB);
1335
1336            return true;
1337        }
1338
1339        private void expect(XmlPullParser parser, int type, String tag)
1340                throws XmlPullParserException {
1341            if (!accept(parser, type, tag)) {
1342                throw new XmlPullParserException("Exepected event: " + type
1343                        + " and tag: " + tag + " but got event: " + parser.getEventType()
1344                        + " and tag:" + parser.getName());
1345            }
1346        }
1347
1348        private void skipEmptyTextTags(XmlPullParser parser)
1349                throws IOException, XmlPullParserException {
1350            while (accept(parser, XmlPullParser.TEXT, null)
1351                    && "\n".equals(parser.getText())) {
1352                parser.next();
1353            }
1354        }
1355
1356        private boolean accept(XmlPullParser parser, int type, String tag)
1357                throws XmlPullParserException {
1358            if (parser.getEventType() != type) {
1359                return false;
1360            }
1361            if (tag != null) {
1362                if (!tag.equals(parser.getName())) {
1363                    return false;
1364                }
1365            } else if (parser.getName() != null) {
1366                return false;
1367            }
1368            return true;
1369        }
1370    }
1371
1372    public final class PrintSpooler extends IPrintSpooler.Stub {
1373        @Override
1374        public void getPrintJobInfos(IPrintSpoolerCallbacks callback,
1375                ComponentName componentName, int state, int appId, int sequence)
1376                throws RemoteException {
1377            List<PrintJobInfo> printJobs = null;
1378            try {
1379                printJobs = PrintSpoolerService.this.getPrintJobInfos(
1380                        componentName, state, appId);
1381            } finally {
1382                callback.onGetPrintJobInfosResult(printJobs, sequence);
1383            }
1384        }
1385
1386        @Override
1387        public void getPrintJobInfo(PrintJobId printJobId, IPrintSpoolerCallbacks callback,
1388                int appId, int sequence) throws RemoteException {
1389            PrintJobInfo printJob = null;
1390            try {
1391                printJob = PrintSpoolerService.this.getPrintJobInfo(printJobId, appId);
1392            } finally {
1393                callback.onGetPrintJobInfoResult(printJob, sequence);
1394            }
1395        }
1396
1397        @Override
1398        public void createPrintJob(PrintJobInfo printJob) {
1399            PrintSpoolerService.this.createPrintJob(printJob);
1400        }
1401
1402        @Override
1403        public void setPrintJobState(PrintJobId printJobId, int state, String error,
1404                IPrintSpoolerCallbacks callback, int sequece) throws RemoteException {
1405            boolean success = false;
1406            try {
1407                success = PrintSpoolerService.this.setPrintJobState(
1408                        printJobId, state, error);
1409            } finally {
1410                callback.onSetPrintJobStateResult(success, sequece);
1411            }
1412        }
1413
1414        @Override
1415        public void setPrintJobTag(PrintJobId printJobId, String tag,
1416                IPrintSpoolerCallbacks callback, int sequece) throws RemoteException {
1417            boolean success = false;
1418            try {
1419                success = PrintSpoolerService.this.setPrintJobTag(printJobId, tag);
1420            } finally {
1421                callback.onSetPrintJobTagResult(success, sequece);
1422            }
1423        }
1424
1425        @Override
1426        public void writePrintJobData(ParcelFileDescriptor fd, PrintJobId printJobId) {
1427            PrintSpoolerService.this.writePrintJobData(fd, printJobId);
1428        }
1429
1430        @Override
1431        public void setClient(IPrintSpoolerClient client) {
1432            Message message = mHandlerCaller.obtainMessageO(
1433                    HandlerCallerCallback.MSG_SET_CLIENT, client);
1434            mHandlerCaller.executeOrSendMessage(message);
1435        }
1436
1437        @Override
1438        public void removeObsoletePrintJobs() {
1439            PrintSpoolerService.this.removeObsoletePrintJobs();
1440        }
1441
1442        @Override
1443        protected void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
1444            PrintSpoolerService.this.dump(fd, writer, args);
1445        }
1446
1447        @Override
1448        public void setPrintJobCancelling(PrintJobId printJobId, boolean cancelling) {
1449            PrintSpoolerService.this.setPrintJobCancelling(printJobId, cancelling);
1450        }
1451
1452        @Override
1453        public void pruneApprovedPrintServices(List<ComponentName> servicesToKeep) {
1454            (new ApprovedPrintServices(PrintSpoolerService.this))
1455                    .pruneApprovedServices(servicesToKeep);
1456        }
1457
1458        @Override
1459        public void setProgress(@NonNull PrintJobId printJobId,
1460                @FloatRange(from=0.0, to=1.0) float progress) throws RemoteException {
1461            PrintSpoolerService.this.setProgress(printJobId, progress);
1462        }
1463
1464        @Override
1465        public void setStatus(@NonNull PrintJobId printJobId,
1466                @Nullable CharSequence status) throws RemoteException {
1467            PrintSpoolerService.this.setStatus(printJobId, status);
1468        }
1469
1470        @Override
1471        public void setStatusRes(@NonNull PrintJobId printJobId, @StringRes int status,
1472                @NonNull CharSequence appPackageName) throws RemoteException {
1473            PrintSpoolerService.this.setStatus(printJobId, status, appPackageName);
1474        }
1475
1476
1477        public PrintSpoolerService getService() {
1478            return PrintSpoolerService.this;
1479        }
1480
1481        @Override
1482        public void onCustomPrinterIconLoaded(PrinterId printerId, Icon icon,
1483                IPrintSpoolerCallbacks callbacks, int sequence)
1484                throws RemoteException {
1485            try {
1486                PrintSpoolerService.this.onCustomPrinterIconLoaded(printerId, icon);
1487            } finally {
1488                callbacks.onCustomPrinterIconCached(sequence);
1489            }
1490        }
1491
1492        @Override
1493        public void getCustomPrinterIcon(PrinterId printerId, IPrintSpoolerCallbacks callbacks,
1494                int sequence) throws RemoteException {
1495            Icon icon = null;
1496            try {
1497                icon = PrintSpoolerService.this.getCustomPrinterIcon(printerId);
1498            } finally {
1499                callbacks.onGetCustomPrinterIconResult(icon, sequence);
1500            }
1501        }
1502
1503        @Override
1504        public void clearCustomPrinterIconCache(IPrintSpoolerCallbacks callbacks,
1505                int sequence) throws RemoteException {
1506            try {
1507                PrintSpoolerService.this.clearCustomPrinterIconCache();
1508            } finally {
1509                callbacks.customPrinterIconCacheCleared(sequence);
1510            }
1511        }
1512
1513    }
1514}
1515