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