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