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