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 
17 package com.android.printspooler.model;
18 
19 import android.annotation.FloatRange;
20 import android.annotation.NonNull;
21 import android.annotation.Nullable;
22 import android.annotation.StringRes;
23 import android.app.Service;
24 import android.content.ComponentName;
25 import android.content.Context;
26 import android.content.Intent;
27 import android.graphics.drawable.Icon;
28 import android.os.AsyncTask;
29 import android.os.Bundle;
30 import android.os.IBinder;
31 import android.os.Message;
32 import android.os.ParcelFileDescriptor;
33 import android.os.RemoteException;
34 import android.print.IPrintSpooler;
35 import android.print.IPrintSpoolerCallbacks;
36 import android.print.IPrintSpoolerClient;
37 import android.print.PageRange;
38 import android.print.PrintAttributes;
39 import android.print.PrintAttributes.Margins;
40 import android.print.PrintAttributes.MediaSize;
41 import android.print.PrintAttributes.Resolution;
42 import android.print.PrintDocumentInfo;
43 import android.print.PrintJobId;
44 import android.print.PrintJobInfo;
45 import android.print.PrintManager;
46 import android.print.PrinterId;
47 import android.text.TextUtils;
48 import android.util.ArrayMap;
49 import android.util.AtomicFile;
50 import android.util.Log;
51 import android.util.Slog;
52 import android.util.Xml;
53 
54 import com.android.internal.logging.MetricsLogger;
55 import com.android.internal.os.HandlerCaller;
56 import com.android.internal.util.FastXmlSerializer;
57 import com.android.printspooler.R;
58 import com.android.printspooler.util.ApprovedPrintServices;
59 
60 import libcore.io.IoUtils;
61 
62 import org.xmlpull.v1.XmlPullParser;
63 import org.xmlpull.v1.XmlPullParserException;
64 import org.xmlpull.v1.XmlSerializer;
65 
66 import java.io.File;
67 import java.io.FileDescriptor;
68 import java.io.FileInputStream;
69 import java.io.FileNotFoundException;
70 import java.io.FileOutputStream;
71 import java.io.IOException;
72 import java.io.PrintWriter;
73 import java.nio.charset.StandardCharsets;
74 import java.util.ArrayList;
75 import java.util.List;
76 import java.util.Set;
77 
78 /**
79  * Service for exposing some of the {@link PrintSpooler} functionality to
80  * another process.
81  */
82 public 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 
peekInstance()117     public static PrintSpoolerService peekInstance() {
118         synchronized (sLock) {
119             return sInstance;
120         }
121     }
122 
123     @Override
onCreate()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
onDestroy()144     public void onDestroy() {
145         super.onDestroy();
146     }
147 
148     @Override
onBind(Intent intent)149     public IBinder onBind(Intent intent) {
150         return new PrintSpooler();
151     }
152 
153     @Override
dump(FileDescriptor fd, PrintWriter pw, String[] args)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 
sendOnPrintJobQueued(PrintJobInfo printJob)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 
sendOnAllPrintJobsForServiceHandled(ComponentName service)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 
sendOnAllPrintJobsHandled()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
executeMessage(Message message)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 
getPrintJobInfos(ComponentName componentName, int state, int appId)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 
isStateVisibleToUser(int state)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 
getPrintJobInfo(PrintJobId printJobId, int appId)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 
createPrintJob(PrintJobInfo printJob)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 
handleReadPrintJobsLocked()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 
checkAllPrintJobsHandled()410     public void checkAllPrintJobsHandled() {
411         synchronized (mLock) {
412             if (!hasActivePrintJobsLocked()) {
413                 notifyOnAllPrintJobsHandled();
414             }
415         }
416     }
417 
writePrintJobData(final ParcelFileDescriptor fd, final PrintJobId printJobId)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 
generateFileForPrintJob(Context context, PrintJobId printJobId)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 
addPrintJobLocked(PrintJobInfo printJob)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 
removeObsoletePrintJobs()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 
removePrintJobFileLocked(PrintJobId printJobId)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      */
notifyPrintJobUpdated(PrintJobInfo printJob)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 
setPrintJobState(PrintJobId printJobId, int state, String error)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      */
setProgress(@onNull PrintJobId printJobId, @FloatRange(from=0.0, to=1.0) float progress)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      */
setStatus(@onNull PrintJobId printJobId, @Nullable CharSequence status)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      */
setStatus(@onNull PrintJobId printJobId, @StringRes int status, @Nullable CharSequence appPackageName)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 
hasActivePrintJobsLocked()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 
hasActivePrintJobsForServiceLocked(ComponentName service)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 
isObsoleteState(int printJobState)647     private boolean isObsoleteState(int printJobState) {
648         return (isTerminalState(printJobState)
649                 || printJobState == PrintJobInfo.STATE_QUEUED);
650     }
651 
isScheduledState(int printJobState)652     private boolean isScheduledState(int printJobState) {
653         return printJobState == PrintJobInfo.STATE_QUEUED
654                 || printJobState == PrintJobInfo.STATE_STARTED
655                 || printJobState == PrintJobInfo.STATE_BLOCKED;
656     }
657 
isActiveState(int printJobState)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 
isTerminalState(int printJobState)665     private boolean isTerminalState(int printJobState) {
666         return printJobState == PrintJobInfo.STATE_COMPLETED
667                 || printJobState == PrintJobInfo.STATE_CANCELED;
668     }
669 
setPrintJobTag(PrintJobId printJobId, String tag)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 
setPrintJobCancelling(PrintJobId printJobId, boolean cancelling)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 
updatePrintJobUserConfigurableOptionsNoPersistence(PrintJobInfo printJob)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 
shouldPersistPrintJob(PrintJobInfo printJob)730     private boolean shouldPersistPrintJob(PrintJobInfo printJob) {
731         return printJob.getState() >= PrintJobInfo.STATE_QUEUED;
732     }
733 
notifyOnAllPrintJobsHandled()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      */
onCustomPrinterIconLoaded(PrinterId printerId, Icon icon)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      */
getCustomPrinterIcon(PrinterId printerId)768     public Icon getCustomPrinterIcon(PrinterId printerId) {
769         return mCustomIconCache.getIcon(printerId);
770     }
771 
772     /**
773      * Clear the custom printer icon cache.
774      */
clearCustomPrinterIconCache()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 
PersistenceManager()846         private PersistenceManager() {
847             mStatePersistFile = new AtomicFile(new File(getFilesDir(),
848                     PERSIST_FILE_NAME));
849         }
850 
writeStateLocked()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 
doWriteStateLocked()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 
readStateLocked()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 
parseState(XmlPullParser parser)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 
parsePrintJob(XmlPullParser parser)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 
expect(XmlPullParser parser, int type, String tag)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 
skipEmptyTextTags(XmlPullParser parser)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 
accept(XmlPullParser parser, int type, String tag)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
getPrintJobInfos(IPrintSpoolerCallbacks callback, ComponentName componentName, int state, int appId, int sequence)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
getPrintJobInfo(PrintJobId printJobId, IPrintSpoolerCallbacks callback, int appId, int sequence)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
createPrintJob(PrintJobInfo printJob)1398         public void createPrintJob(PrintJobInfo printJob) {
1399             PrintSpoolerService.this.createPrintJob(printJob);
1400         }
1401 
1402         @Override
setPrintJobState(PrintJobId printJobId, int state, String error, IPrintSpoolerCallbacks callback, int sequece)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
setPrintJobTag(PrintJobId printJobId, String tag, IPrintSpoolerCallbacks callback, int sequece)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
writePrintJobData(ParcelFileDescriptor fd, PrintJobId printJobId)1426         public void writePrintJobData(ParcelFileDescriptor fd, PrintJobId printJobId) {
1427             PrintSpoolerService.this.writePrintJobData(fd, printJobId);
1428         }
1429 
1430         @Override
setClient(IPrintSpoolerClient client)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
removeObsoletePrintJobs()1438         public void removeObsoletePrintJobs() {
1439             PrintSpoolerService.this.removeObsoletePrintJobs();
1440         }
1441 
1442         @Override
dump(FileDescriptor fd, PrintWriter writer, String[] args)1443         protected void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
1444             PrintSpoolerService.this.dump(fd, writer, args);
1445         }
1446 
1447         @Override
setPrintJobCancelling(PrintJobId printJobId, boolean cancelling)1448         public void setPrintJobCancelling(PrintJobId printJobId, boolean cancelling) {
1449             PrintSpoolerService.this.setPrintJobCancelling(printJobId, cancelling);
1450         }
1451 
1452         @Override
pruneApprovedPrintServices(List<ComponentName> servicesToKeep)1453         public void pruneApprovedPrintServices(List<ComponentName> servicesToKeep) {
1454             (new ApprovedPrintServices(PrintSpoolerService.this))
1455                     .pruneApprovedServices(servicesToKeep);
1456         }
1457 
1458         @Override
setProgress(@onNull PrintJobId printJobId, @FloatRange(from=0.0, to=1.0) float progress)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
setStatus(@onNull PrintJobId printJobId, @Nullable CharSequence status)1465         public void setStatus(@NonNull PrintJobId printJobId,
1466                 @Nullable CharSequence status) throws RemoteException {
1467             PrintSpoolerService.this.setStatus(printJobId, status);
1468         }
1469 
1470         @Override
setStatusRes(@onNull PrintJobId printJobId, @StringRes int status, @NonNull CharSequence appPackageName)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 
getService()1477         public PrintSpoolerService getService() {
1478             return PrintSpoolerService.this;
1479         }
1480 
1481         @Override
onCustomPrinterIconLoaded(PrinterId printerId, Icon icon, IPrintSpoolerCallbacks callbacks, int sequence)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
getCustomPrinterIcon(PrinterId printerId, IPrintSpoolerCallbacks callbacks, int sequence)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
clearCustomPrinterIconCache(IPrintSpoolerCallbacks callbacks, int sequence)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