/* * Copyright (C) 2013 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.printspooler.model; import android.annotation.FloatRange; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.StringRes; import android.app.Service; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.graphics.drawable.Icon; import android.os.AsyncTask; import android.os.Bundle; import android.os.IBinder; import android.os.Message; import android.os.ParcelFileDescriptor; import android.os.RemoteException; import android.print.IPrintSpooler; import android.print.IPrintSpoolerCallbacks; import android.print.IPrintSpoolerClient; import android.print.PageRange; import android.print.PrintAttributes; import android.print.PrintAttributes.Margins; import android.print.PrintAttributes.MediaSize; import android.print.PrintAttributes.Resolution; import android.print.PrintDocumentInfo; import android.print.PrintJobId; import android.print.PrintJobInfo; import android.print.PrintManager; import android.print.PrinterId; import android.text.TextUtils; import android.util.ArrayMap; import android.util.AtomicFile; import android.util.Log; import android.util.Slog; import android.util.Xml; import com.android.internal.logging.MetricsLogger; import com.android.internal.os.HandlerCaller; import com.android.internal.util.FastXmlSerializer; import com.android.printspooler.R; import com.android.printspooler.util.ApprovedPrintServices; import libcore.io.IoUtils; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import org.xmlpull.v1.XmlSerializer; import java.io.File; import java.io.FileDescriptor; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.PrintWriter; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; import java.util.Set; /** * Service for exposing some of the {@link PrintSpooler} functionality to * another process. */ public final class PrintSpoolerService extends Service { private static final String LOG_TAG = "PrintSpoolerService"; private static final boolean DEBUG_PRINT_JOB_LIFECYCLE = false; private static final boolean DEBUG_PERSISTENCE = false; private static final boolean PERSISTENCE_MANAGER_ENABLED = true; private static final String PRINT_JOB_STATE_HISTO = "print_job_state"; private static final long CHECK_ALL_PRINTJOBS_HANDLED_DELAY = 5000; private static final String PRINT_JOB_FILE_PREFIX = "print_job_"; private static final String PRINT_FILE_EXTENSION = "pdf"; private static final Object sLock = new Object(); private final Object mLock = new Object(); private final List mPrintJobs = new ArrayList<>(); private static PrintSpoolerService sInstance; private IPrintSpoolerClient mClient; private HandlerCaller mHandlerCaller; private PersistenceManager mPersistanceManager; private NotificationController mNotificationController; /** Cache for custom printer icons loaded from the print service */ private CustomPrinterIconCache mCustomIconCache; public static PrintSpoolerService peekInstance() { synchronized (sLock) { return sInstance; } } @Override public void onCreate() { super.onCreate(); mHandlerCaller = new HandlerCaller(this, getMainLooper(), new HandlerCallerCallback(), false); mPersistanceManager = new PersistenceManager(); mNotificationController = new NotificationController(PrintSpoolerService.this); mCustomIconCache = new CustomPrinterIconCache(getCacheDir()); synchronized (mLock) { mPersistanceManager.readStateLocked(); handleReadPrintJobsLocked(); } synchronized (sLock) { sInstance = this; } } @Override public void onDestroy() { super.onDestroy(); } @Override public IBinder onBind(Intent intent) { return new PrintSpooler(); } @Override protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) { String prefix = (args.length > 0) ? args[0] : ""; String tab = " "; synchronized (mLock) { pw.append(prefix).append("print jobs:").println(); final int printJobCount = mPrintJobs.size(); for (int i = 0; i < printJobCount; i++) { PrintJobInfo printJob = mPrintJobs.get(i); pw.append(prefix).append(tab).append(printJob.toString()); pw.println(); } pw.append(prefix).append("print job files:").println(); File[] files = getFilesDir().listFiles(); if (files != null) { final int fileCount = files.length; for (int i = 0; i < fileCount; i++) { File file = files[i]; if (file.isFile() && file.getName().startsWith(PRINT_JOB_FILE_PREFIX)) { pw.append(prefix).append(tab).append(file.getName()).println(); } } } } pw.append(prefix).append("approved print services:").println(); Set approvedPrintServices = (new ApprovedPrintServices(this)).getApprovedServices(); if (approvedPrintServices != null) { for (String approvedService : approvedPrintServices) { pw.append(prefix).append(tab).append(approvedService).println(); } } } private void sendOnPrintJobQueued(PrintJobInfo printJob) { Message message = mHandlerCaller.obtainMessageO( HandlerCallerCallback.MSG_ON_PRINT_JOB_QUEUED, printJob); mHandlerCaller.executeOrSendMessage(message); } private void sendOnAllPrintJobsForServiceHandled(ComponentName service) { Message message = mHandlerCaller.obtainMessageO( HandlerCallerCallback.MSG_ON_ALL_PRINT_JOBS_FOR_SERIVICE_HANDLED, service); mHandlerCaller.executeOrSendMessage(message); } private void sendOnAllPrintJobsHandled() { Message message = mHandlerCaller.obtainMessage( HandlerCallerCallback.MSG_ON_ALL_PRINT_JOBS_HANDLED); mHandlerCaller.executeOrSendMessage(message); } private final class HandlerCallerCallback implements HandlerCaller.Callback { public static final int MSG_SET_CLIENT = 1; public static final int MSG_ON_PRINT_JOB_QUEUED = 2; public static final int MSG_ON_ALL_PRINT_JOBS_FOR_SERIVICE_HANDLED = 3; public static final int MSG_ON_ALL_PRINT_JOBS_HANDLED = 4; public static final int MSG_CHECK_ALL_PRINTJOBS_HANDLED = 5; public static final int MSG_ON_PRINT_JOB_STATE_CHANGED = 6; @Override public void executeMessage(Message message) { switch (message.what) { case MSG_SET_CLIENT: { synchronized (mLock) { mClient = (IPrintSpoolerClient) message.obj; if (mClient != null) { Message msg = mHandlerCaller.obtainMessage( HandlerCallerCallback.MSG_CHECK_ALL_PRINTJOBS_HANDLED); mHandlerCaller.sendMessageDelayed(msg, CHECK_ALL_PRINTJOBS_HANDLED_DELAY); } } } break; case MSG_ON_PRINT_JOB_QUEUED: { PrintJobInfo printJob = (PrintJobInfo) message.obj; if (mClient != null) { try { mClient.onPrintJobQueued(printJob); } catch (RemoteException re) { Slog.e(LOG_TAG, "Error notify for a queued print job.", re); } } } break; case MSG_ON_ALL_PRINT_JOBS_FOR_SERIVICE_HANDLED: { ComponentName service = (ComponentName) message.obj; if (mClient != null) { try { mClient.onAllPrintJobsForServiceHandled(service); } catch (RemoteException re) { Slog.e(LOG_TAG, "Error notify for all print jobs per service" + " handled.", re); } } } break; case MSG_ON_ALL_PRINT_JOBS_HANDLED: { if (mClient != null) { try { mClient.onAllPrintJobsHandled(); } catch (RemoteException re) { Slog.e(LOG_TAG, "Error notify for all print job handled.", re); } } } break; case MSG_CHECK_ALL_PRINTJOBS_HANDLED: { checkAllPrintJobsHandled(); } break; case MSG_ON_PRINT_JOB_STATE_CHANGED: { if (mClient != null) { PrintJobInfo printJob = (PrintJobInfo) message.obj; try { mClient.onPrintJobStateChanged(printJob); } catch (RemoteException re) { Slog.e(LOG_TAG, "Error notify for print job state change.", re); } } } break; } } } public List getPrintJobInfos(ComponentName componentName, int state, int appId) { List foundPrintJobs = null; synchronized (mLock) { final int printJobCount = mPrintJobs.size(); for (int i = 0; i < printJobCount; i++) { PrintJobInfo printJob = mPrintJobs.get(i); PrinterId printerId = printJob.getPrinterId(); final boolean sameComponent = (componentName == null || (printerId != null && componentName.equals(printerId.getServiceName()))); final boolean sameAppId = appId == PrintManager.APP_ID_ANY || printJob.getAppId() == appId; final boolean sameState = (state == printJob.getState()) || (state == PrintJobInfo.STATE_ANY) || (state == PrintJobInfo.STATE_ANY_VISIBLE_TO_CLIENTS && isStateVisibleToUser(printJob.getState())) || (state == PrintJobInfo.STATE_ANY_ACTIVE && isActiveState(printJob.getState())) || (state == PrintJobInfo.STATE_ANY_SCHEDULED && isScheduledState(printJob.getState())); if (sameComponent && sameAppId && sameState) { if (foundPrintJobs == null) { foundPrintJobs = new ArrayList<>(); } foundPrintJobs.add(printJob); } } } return foundPrintJobs; } private boolean isStateVisibleToUser(int state) { return (isActiveState(state) && (state == PrintJobInfo.STATE_FAILED || state == PrintJobInfo.STATE_COMPLETED || state == PrintJobInfo.STATE_CANCELED || state == PrintJobInfo.STATE_BLOCKED)); } public PrintJobInfo getPrintJobInfo(PrintJobId printJobId, int appId) { synchronized (mLock) { final int printJobCount = mPrintJobs.size(); for (int i = 0; i < printJobCount; i++) { PrintJobInfo printJob = mPrintJobs.get(i); if (printJob.getId().equals(printJobId) && (appId == PrintManager.APP_ID_ANY || appId == printJob.getAppId())) { return printJob; } } return null; } } public void createPrintJob(PrintJobInfo printJob) { synchronized (mLock) { addPrintJobLocked(printJob); setPrintJobState(printJob.getId(), PrintJobInfo.STATE_CREATED, null); Message message = mHandlerCaller.obtainMessageO( HandlerCallerCallback.MSG_ON_PRINT_JOB_STATE_CHANGED, printJob); mHandlerCaller.executeOrSendMessage(message); } } private void handleReadPrintJobsLocked() { // Make a map with the files for a print job since we may have // to delete some. One example of getting orphan files if the // spooler crashes while constructing a print job. We do not // persist partially populated print jobs under construction to // avoid special handling for various attributes missing. ArrayMap fileForJobMap = null; File[] files = getFilesDir().listFiles(); if (files != null) { final int fileCount = files.length; for (int i = 0; i < fileCount; i++) { File file = files[i]; if (file.isFile() && file.getName().startsWith(PRINT_JOB_FILE_PREFIX)) { if (fileForJobMap == null) { fileForJobMap = new ArrayMap(); } String printJobIdString = file.getName().substring( PRINT_JOB_FILE_PREFIX.length(), file.getName().indexOf('.')); PrintJobId printJobId = PrintJobId.unflattenFromString( printJobIdString); fileForJobMap.put(printJobId, file); } } } final int printJobCount = mPrintJobs.size(); for (int i = 0; i < printJobCount; i++) { PrintJobInfo printJob = mPrintJobs.get(i); // We want to have only the orphan files at the end. if (fileForJobMap != null) { fileForJobMap.remove(printJob.getId()); } switch (printJob.getState()) { case PrintJobInfo.STATE_QUEUED: case PrintJobInfo.STATE_STARTED: case PrintJobInfo.STATE_BLOCKED: { // We have a print job that was queued or started or blocked in // the past but the device battery died or a crash occurred. In // this case we assume the print job failed and let the user // decide whether to restart the job or just cancel it. setPrintJobState(printJob.getId(), PrintJobInfo.STATE_FAILED, getString(R.string.no_connection_to_printer)); } break; } } if (!mPrintJobs.isEmpty()) { // Update the notification. mNotificationController.onUpdateNotifications(mPrintJobs); } // Delete the orphan files. if (fileForJobMap != null) { final int orphanFileCount = fileForJobMap.size(); for (int i = 0; i < orphanFileCount; i++) { File file = fileForJobMap.valueAt(i); file.delete(); } } } public void checkAllPrintJobsHandled() { synchronized (mLock) { if (!hasActivePrintJobsLocked()) { notifyOnAllPrintJobsHandled(); } } } public void writePrintJobData(final ParcelFileDescriptor fd, final PrintJobId printJobId) { final PrintJobInfo printJob; synchronized (mLock) { printJob = getPrintJobInfo(printJobId, PrintManager.APP_ID_ANY); } new AsyncTask() { @Override protected Void doInBackground(Void... params) { FileInputStream in = null; FileOutputStream out = null; try { if (printJob != null) { File file = generateFileForPrintJob(PrintSpoolerService.this, printJobId); in = new FileInputStream(file); out = new FileOutputStream(fd.getFileDescriptor()); } final byte[] buffer = new byte[8192]; while (true) { final int readByteCount = in.read(buffer); if (readByteCount < 0) { return null; } out.write(buffer, 0, readByteCount); } } catch (FileNotFoundException fnfe) { Log.e(LOG_TAG, "Error writing print job data!", fnfe); } catch (IOException ioe) { Log.e(LOG_TAG, "Error writing print job data!", ioe); } finally { IoUtils.closeQuietly(in); IoUtils.closeQuietly(out); IoUtils.closeQuietly(fd); } Log.i(LOG_TAG, "[END WRITE]"); return null; } }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void[]) null); } public static File generateFileForPrintJob(Context context, PrintJobId printJobId) { return new File(context.getFilesDir(), PRINT_JOB_FILE_PREFIX + printJobId.flattenToString() + "." + PRINT_FILE_EXTENSION); } private void addPrintJobLocked(PrintJobInfo printJob) { mPrintJobs.add(printJob); if (DEBUG_PRINT_JOB_LIFECYCLE) { Slog.i(LOG_TAG, "[ADD] " + printJob); } } private void removeObsoletePrintJobs() { synchronized (mLock) { boolean persistState = false; final int printJobCount = mPrintJobs.size(); for (int i = printJobCount - 1; i >= 0; i--) { PrintJobInfo printJob = mPrintJobs.get(i); if (isObsoleteState(printJob.getState())) { mPrintJobs.remove(i); if (DEBUG_PRINT_JOB_LIFECYCLE) { Slog.i(LOG_TAG, "[REMOVE] " + printJob.getId().flattenToString()); } removePrintJobFileLocked(printJob.getId()); persistState = true; } } if (persistState) { mPersistanceManager.writeStateLocked(); } } } private void removePrintJobFileLocked(PrintJobId printJobId) { File file = generateFileForPrintJob(PrintSpoolerService.this, printJobId); if (file.exists()) { file.delete(); if (DEBUG_PRINT_JOB_LIFECYCLE) { Slog.i(LOG_TAG, "[REMOVE FILE FOR] " + printJobId); } } } /** * Notify all interested parties that a print job has been updated. * * @param printJob The updated print job. */ private void notifyPrintJobUpdated(PrintJobInfo printJob) { Message message = mHandlerCaller.obtainMessageO( HandlerCallerCallback.MSG_ON_PRINT_JOB_STATE_CHANGED, printJob); mHandlerCaller.executeOrSendMessage(message); mNotificationController.onUpdateNotifications(mPrintJobs); } public boolean setPrintJobState(PrintJobId printJobId, int state, String error) { boolean success = false; synchronized (mLock) { PrintJobInfo printJob = getPrintJobInfo(printJobId, PrintManager.APP_ID_ANY); if (printJob != null) { final int oldState = printJob.getState(); if (oldState == state) { return false; } success = true; printJob.setState(state); printJob.setStatus(error); printJob.setCancelling(false); if (DEBUG_PRINT_JOB_LIFECYCLE) { Slog.i(LOG_TAG, "[STATE CHANGED] " + printJob); } MetricsLogger.histogram(this, PRINT_JOB_STATE_HISTO, state); switch (state) { case PrintJobInfo.STATE_COMPLETED: case PrintJobInfo.STATE_CANCELED: mPrintJobs.remove(printJob); removePrintJobFileLocked(printJob.getId()); // $fall-through$ case PrintJobInfo.STATE_FAILED: { PrinterId printerId = printJob.getPrinterId(); if (printerId != null) { ComponentName service = printerId.getServiceName(); if (!hasActivePrintJobsForServiceLocked(service)) { sendOnAllPrintJobsForServiceHandled(service); } } } break; case PrintJobInfo.STATE_QUEUED: { sendOnPrintJobQueued(new PrintJobInfo(printJob)); } break; } if (shouldPersistPrintJob(printJob)) { mPersistanceManager.writeStateLocked(); } if (!hasActivePrintJobsLocked()) { notifyOnAllPrintJobsHandled(); } notifyPrintJobUpdated(printJob); } } return success; } /** * Set the progress for a print job. * * @param printJobId ID of the print job to update * @param progress the new progress */ public void setProgress(@NonNull PrintJobId printJobId, @FloatRange(from=0.0, to=1.0) float progress) { synchronized (mLock) { getPrintJobInfo(printJobId, PrintManager.APP_ID_ANY).setProgress(progress); mNotificationController.onUpdateNotifications(mPrintJobs); } } /** * Set the status for a print job. * * @param printJobId ID of the print job to update * @param status the new status */ public void setStatus(@NonNull PrintJobId printJobId, @Nullable CharSequence status) { synchronized (mLock) { PrintJobInfo printJob = getPrintJobInfo(printJobId, PrintManager.APP_ID_ANY); if (printJob != null) { printJob.setStatus(status); notifyPrintJobUpdated(printJob); } } } /** * Set the status for a print job. * * @param printJobId ID of the print job to update * @param status the new status as a string resource * @param appPackageName app package the resource belongs to */ public void setStatus(@NonNull PrintJobId printJobId, @StringRes int status, @Nullable CharSequence appPackageName) { synchronized (mLock) { PrintJobInfo printJob = getPrintJobInfo(printJobId, PrintManager.APP_ID_ANY); if (printJob != null) { printJob.setStatus(status, appPackageName); notifyPrintJobUpdated(printJob); } } } public boolean hasActivePrintJobsLocked() { final int printJobCount = mPrintJobs.size(); for (int i = 0; i < printJobCount; i++) { PrintJobInfo printJob = mPrintJobs.get(i); if (isActiveState(printJob.getState())) { return true; } } return false; } public boolean hasActivePrintJobsForServiceLocked(ComponentName service) { final int printJobCount = mPrintJobs.size(); for (int i = 0; i < printJobCount; i++) { PrintJobInfo printJob = mPrintJobs.get(i); if (isActiveState(printJob.getState()) && printJob.getPrinterId() != null && printJob.getPrinterId().getServiceName().equals(service)) { return true; } } return false; } private boolean isObsoleteState(int printJobState) { return (isTerminalState(printJobState) || printJobState == PrintJobInfo.STATE_QUEUED); } private boolean isScheduledState(int printJobState) { return printJobState == PrintJobInfo.STATE_QUEUED || printJobState == PrintJobInfo.STATE_STARTED || printJobState == PrintJobInfo.STATE_BLOCKED; } private boolean isActiveState(int printJobState) { return printJobState == PrintJobInfo.STATE_CREATED || printJobState == PrintJobInfo.STATE_QUEUED || printJobState == PrintJobInfo.STATE_STARTED || printJobState == PrintJobInfo.STATE_BLOCKED; } private boolean isTerminalState(int printJobState) { return printJobState == PrintJobInfo.STATE_COMPLETED || printJobState == PrintJobInfo.STATE_CANCELED; } public boolean setPrintJobTag(PrintJobId printJobId, String tag) { synchronized (mLock) { PrintJobInfo printJob = getPrintJobInfo(printJobId, PrintManager.APP_ID_ANY); if (printJob != null) { String printJobTag = printJob.getTag(); if (printJobTag == null) { if (tag == null) { return false; } } else if (printJobTag.equals(tag)) { return false; } printJob.setTag(tag); if (shouldPersistPrintJob(printJob)) { mPersistanceManager.writeStateLocked(); } return true; } } return false; } public void setPrintJobCancelling(PrintJobId printJobId, boolean cancelling) { synchronized (mLock) { PrintJobInfo printJob = getPrintJobInfo(printJobId, PrintManager.APP_ID_ANY); if (printJob != null) { printJob.setCancelling(cancelling); if (shouldPersistPrintJob(printJob)) { mPersistanceManager.writeStateLocked(); } mNotificationController.onUpdateNotifications(mPrintJobs); Message message = mHandlerCaller.obtainMessageO( HandlerCallerCallback.MSG_ON_PRINT_JOB_STATE_CHANGED, printJob); mHandlerCaller.executeOrSendMessage(message); } } } public void updatePrintJobUserConfigurableOptionsNoPersistence(PrintJobInfo printJob) { synchronized (mLock) { final int printJobCount = mPrintJobs.size(); for (int i = 0; i < printJobCount; i++) { PrintJobInfo cachedPrintJob = mPrintJobs.get(i); if (cachedPrintJob.getId().equals(printJob.getId())) { cachedPrintJob.setPrinterId(printJob.getPrinterId()); cachedPrintJob.setPrinterName(printJob.getPrinterName()); cachedPrintJob.setCopies(printJob.getCopies()); cachedPrintJob.setDocumentInfo(printJob.getDocumentInfo()); cachedPrintJob.setPages(printJob.getPages()); cachedPrintJob.setAttributes(printJob.getAttributes()); cachedPrintJob.setAdvancedOptions(printJob.getAdvancedOptions()); return; } } throw new IllegalArgumentException("No print job with id:" + printJob.getId()); } } private boolean shouldPersistPrintJob(PrintJobInfo printJob) { return printJob.getState() >= PrintJobInfo.STATE_QUEUED; } private void notifyOnAllPrintJobsHandled() { // This has to run on the tread that is persisting the current state // since this call may result in the system unbinding from the spooler // and as a result the spooler process may get killed before the write // completes. new AsyncTask() { @Override protected Void doInBackground(Void... params) { sendOnAllPrintJobsHandled(); return null; } }.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR, (Void[]) null); } /** * Handle that a custom icon for a printer was loaded. * * @param printerId the id of the printer the icon belongs to * @param icon the icon that was loaded * @see android.print.PrinterInfo.Builder#setHasCustomPrinterIcon() */ public void onCustomPrinterIconLoaded(PrinterId printerId, Icon icon) { mCustomIconCache.onCustomPrinterIconLoaded(printerId, icon); } /** * Get the custom icon for a printer. If the icon is not cached, the icon is * requested asynchronously. Once it is available the printer is updated. * * @param printerId the id of the printer the icon should be loaded for * @return the custom icon to be used for the printer or null if the icon is * not yet available * @see android.print.PrinterInfo.Builder#setHasCustomPrinterIcon() */ public Icon getCustomPrinterIcon(PrinterId printerId) { return mCustomIconCache.getIcon(printerId); } /** * Clear the custom printer icon cache. */ public void clearCustomPrinterIconCache() { mCustomIconCache.clear(); } private final class PersistenceManager { private static final String PERSIST_FILE_NAME = "print_spooler_state.xml"; private static final String TAG_SPOOLER = "spooler"; private static final String TAG_JOB = "job"; private static final String TAG_PRINTER_ID = "printerId"; private static final String TAG_PAGE_RANGE = "pageRange"; private static final String TAG_ATTRIBUTES = "attributes"; private static final String TAG_DOCUMENT_INFO = "documentInfo"; private static final String ATTR_ID = "id"; private static final String ATTR_LABEL = "label"; private static final String ATTR_LABEL_RES_ID = "labelResId"; private static final String ATTR_PACKAGE_NAME = "packageName"; private static final String ATTR_STATE = "state"; private static final String ATTR_APP_ID = "appId"; private static final String ATTR_TAG = "tag"; private static final String ATTR_CREATION_TIME = "creationTime"; private static final String ATTR_COPIES = "copies"; private static final String ATTR_PRINTER_NAME = "printerName"; private static final String ATTR_STATE_REASON = "stateReason"; private static final String ATTR_STATUS = "status"; private static final String ATTR_PROGRESS = "progress"; private static final String ATTR_CANCELLING = "cancelling"; private static final String TAG_ADVANCED_OPTIONS = "advancedOptions"; private static final String TAG_ADVANCED_OPTION = "advancedOption"; private static final String ATTR_KEY = "key"; private static final String ATTR_TYPE = "type"; private static final String ATTR_VALUE = "value"; private static final String TYPE_STRING = "string"; private static final String TYPE_INT = "int"; private static final String TAG_MEDIA_SIZE = "mediaSize"; private static final String TAG_RESOLUTION = "resolution"; private static final String TAG_MARGINS = "margins"; private static final String ATTR_COLOR_MODE = "colorMode"; private static final String ATTR_DUPLEX_MODE = "duplexMode"; private static final String ATTR_LOCAL_ID = "localId"; private static final String ATTR_SERVICE_NAME = "serviceName"; private static final String ATTR_WIDTH_MILS = "widthMils"; private static final String ATTR_HEIGHT_MILS = "heightMils"; private static final String ATTR_HORIZONTAL_DPI = "horizontalDip"; private static final String ATTR_VERTICAL_DPI = "verticalDpi"; private static final String ATTR_LEFT_MILS = "leftMils"; private static final String ATTR_TOP_MILS = "topMils"; private static final String ATTR_RIGHT_MILS = "rightMils"; private static final String ATTR_BOTTOM_MILS = "bottomMils"; private static final String ATTR_START = "start"; private static final String ATTR_END = "end"; private static final String ATTR_NAME = "name"; private static final String ATTR_PAGE_COUNT = "pageCount"; private static final String ATTR_CONTENT_TYPE = "contentType"; private static final String ATTR_DATA_SIZE = "dataSize"; private final AtomicFile mStatePersistFile; private boolean mWriteStateScheduled; private PersistenceManager() { mStatePersistFile = new AtomicFile(new File(getFilesDir(), PERSIST_FILE_NAME)); } public void writeStateLocked() { if (!PERSISTENCE_MANAGER_ENABLED) { return; } if (mWriteStateScheduled) { return; } mWriteStateScheduled = true; new AsyncTask() { @Override protected Void doInBackground(Void... params) { synchronized (mLock) { mWriteStateScheduled = false; doWriteStateLocked(); } return null; } }.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR, (Void[]) null); } private void doWriteStateLocked() { if (DEBUG_PERSISTENCE) { Log.i(LOG_TAG, "[PERSIST START]"); } FileOutputStream out = null; try { out = mStatePersistFile.startWrite(); XmlSerializer serializer = new FastXmlSerializer(); serializer.setOutput(out, StandardCharsets.UTF_8.name()); serializer.startDocument(null, true); serializer.startTag(null, TAG_SPOOLER); List printJobs = mPrintJobs; final int printJobCount = printJobs.size(); for (int j = 0; j < printJobCount; j++) { PrintJobInfo printJob = printJobs.get(j); if (!shouldPersistPrintJob(printJob)) { continue; } serializer.startTag(null, TAG_JOB); serializer.attribute(null, ATTR_ID, printJob.getId().flattenToString()); serializer.attribute(null, ATTR_LABEL, printJob.getLabel().toString()); serializer.attribute(null, ATTR_STATE, String.valueOf(printJob.getState())); serializer.attribute(null, ATTR_APP_ID, String.valueOf(printJob.getAppId())); String tag = printJob.getTag(); if (tag != null) { serializer.attribute(null, ATTR_TAG, tag); } serializer.attribute(null, ATTR_CREATION_TIME, String.valueOf( printJob.getCreationTime())); serializer.attribute(null, ATTR_COPIES, String.valueOf(printJob.getCopies())); String printerName = printJob.getPrinterName(); if (!TextUtils.isEmpty(printerName)) { serializer.attribute(null, ATTR_PRINTER_NAME, printerName); } serializer.attribute(null, ATTR_CANCELLING, String.valueOf( printJob.isCancelling())); float progress = printJob.getProgress(); if (!Float.isNaN(progress)) { serializer.attribute(null, ATTR_PROGRESS, String.valueOf(progress)); } CharSequence status = printJob.getStatus(getPackageManager()); if (!TextUtils.isEmpty(status)) { serializer.attribute(null, ATTR_STATUS, status.toString()); } PrinterId printerId = printJob.getPrinterId(); if (printerId != null) { serializer.startTag(null, TAG_PRINTER_ID); serializer.attribute(null, ATTR_LOCAL_ID, printerId.getLocalId()); serializer.attribute(null, ATTR_SERVICE_NAME, printerId.getServiceName() .flattenToString()); serializer.endTag(null, TAG_PRINTER_ID); } PageRange[] pages = printJob.getPages(); if (pages != null) { for (int i = 0; i < pages.length; i++) { serializer.startTag(null, TAG_PAGE_RANGE); serializer.attribute(null, ATTR_START, String.valueOf( pages[i].getStart())); serializer.attribute(null, ATTR_END, String.valueOf( pages[i].getEnd())); serializer.endTag(null, TAG_PAGE_RANGE); } } PrintAttributes attributes = printJob.getAttributes(); if (attributes != null) { serializer.startTag(null, TAG_ATTRIBUTES); final int colorMode = attributes.getColorMode(); serializer.attribute(null, ATTR_COLOR_MODE, String.valueOf(colorMode)); final int duplexMode = attributes.getDuplexMode(); serializer.attribute(null, ATTR_DUPLEX_MODE, String.valueOf(duplexMode)); MediaSize mediaSize = attributes.getMediaSize(); if (mediaSize != null) { serializer.startTag(null, TAG_MEDIA_SIZE); serializer.attribute(null, ATTR_ID, mediaSize.getId()); serializer.attribute(null, ATTR_WIDTH_MILS, String.valueOf( mediaSize.getWidthMils())); serializer.attribute(null, ATTR_HEIGHT_MILS, String.valueOf( mediaSize.getHeightMils())); // We prefer to store only the package name and // resource id and fallback to the label. if (!TextUtils.isEmpty(mediaSize.mPackageName) && mediaSize.mLabelResId > 0) { serializer.attribute(null, ATTR_PACKAGE_NAME, mediaSize.mPackageName); serializer.attribute(null, ATTR_LABEL_RES_ID, String.valueOf(mediaSize.mLabelResId)); } else { serializer.attribute(null, ATTR_LABEL, mediaSize.getLabel(getPackageManager())); } serializer.endTag(null, TAG_MEDIA_SIZE); } Resolution resolution = attributes.getResolution(); if (resolution != null) { serializer.startTag(null, TAG_RESOLUTION); serializer.attribute(null, ATTR_ID, resolution.getId()); serializer.attribute(null, ATTR_HORIZONTAL_DPI, String.valueOf( resolution.getHorizontalDpi())); serializer.attribute(null, ATTR_VERTICAL_DPI, String.valueOf( resolution.getVerticalDpi())); serializer.attribute(null, ATTR_LABEL, resolution.getLabel()); serializer.endTag(null, TAG_RESOLUTION); } Margins margins = attributes.getMinMargins(); if (margins != null) { serializer.startTag(null, TAG_MARGINS); serializer.attribute(null, ATTR_LEFT_MILS, String.valueOf( margins.getLeftMils())); serializer.attribute(null, ATTR_TOP_MILS, String.valueOf( margins.getTopMils())); serializer.attribute(null, ATTR_RIGHT_MILS, String.valueOf( margins.getRightMils())); serializer.attribute(null, ATTR_BOTTOM_MILS, String.valueOf( margins.getBottomMils())); serializer.endTag(null, TAG_MARGINS); } serializer.endTag(null, TAG_ATTRIBUTES); } PrintDocumentInfo documentInfo = printJob.getDocumentInfo(); if (documentInfo != null) { serializer.startTag(null, TAG_DOCUMENT_INFO); serializer.attribute(null, ATTR_NAME, documentInfo.getName()); serializer.attribute(null, ATTR_CONTENT_TYPE, String.valueOf( documentInfo.getContentType())); serializer.attribute(null, ATTR_PAGE_COUNT, String.valueOf( documentInfo.getPageCount())); serializer.attribute(null, ATTR_DATA_SIZE, String.valueOf( documentInfo.getDataSize())); serializer.endTag(null, TAG_DOCUMENT_INFO); } Bundle advancedOptions = printJob.getAdvancedOptions(); if (advancedOptions != null) { serializer.startTag(null, TAG_ADVANCED_OPTIONS); for (String key : advancedOptions.keySet()) { Object value = advancedOptions.get(key); if (value instanceof String) { String stringValue = (String) value; serializer.startTag(null, TAG_ADVANCED_OPTION); serializer.attribute(null, ATTR_KEY, key); serializer.attribute(null, ATTR_TYPE, TYPE_STRING); serializer.attribute(null, ATTR_VALUE, stringValue); serializer.endTag(null, TAG_ADVANCED_OPTION); } else if (value instanceof Integer) { String intValue = Integer.toString((Integer) value); serializer.startTag(null, TAG_ADVANCED_OPTION); serializer.attribute(null, ATTR_KEY, key); serializer.attribute(null, ATTR_TYPE, TYPE_INT); serializer.attribute(null, ATTR_VALUE, intValue); serializer.endTag(null, TAG_ADVANCED_OPTION); } } serializer.endTag(null, TAG_ADVANCED_OPTIONS); } serializer.endTag(null, TAG_JOB); if (DEBUG_PERSISTENCE) { Log.i(LOG_TAG, "[PERSISTED] " + printJob); } } serializer.endTag(null, TAG_SPOOLER); serializer.endDocument(); mStatePersistFile.finishWrite(out); if (DEBUG_PERSISTENCE) { Log.i(LOG_TAG, "[PERSIST END]"); } } catch (IOException e) { Slog.w(LOG_TAG, "Failed to write state, restoring backup.", e); mStatePersistFile.failWrite(out); } finally { IoUtils.closeQuietly(out); } } public void readStateLocked() { if (!PERSISTENCE_MANAGER_ENABLED) { return; } FileInputStream in = null; try { in = mStatePersistFile.openRead(); } catch (FileNotFoundException e) { if (DEBUG_PERSISTENCE) { Log.d(LOG_TAG, "No existing print spooler state."); } return; } try { XmlPullParser parser = Xml.newPullParser(); parser.setInput(in, StandardCharsets.UTF_8.name()); parseState(parser); } catch (IllegalStateException ise) { Slog.w(LOG_TAG, "Failed parsing ", ise); } catch (NullPointerException npe) { Slog.w(LOG_TAG, "Failed parsing ", npe); } catch (NumberFormatException nfe) { Slog.w(LOG_TAG, "Failed parsing ", nfe); } catch (XmlPullParserException xppe) { Slog.w(LOG_TAG, "Failed parsing ", xppe); } catch (IOException ioe) { Slog.w(LOG_TAG, "Failed parsing ", ioe); } catch (IndexOutOfBoundsException iobe) { Slog.w(LOG_TAG, "Failed parsing ", iobe); } finally { IoUtils.closeQuietly(in); } } private void parseState(XmlPullParser parser) throws IOException, XmlPullParserException { parser.next(); skipEmptyTextTags(parser); expect(parser, XmlPullParser.START_TAG, TAG_SPOOLER); parser.next(); while (parsePrintJob(parser)) { parser.next(); } skipEmptyTextTags(parser); expect(parser, XmlPullParser.END_TAG, TAG_SPOOLER); } private boolean parsePrintJob(XmlPullParser parser) throws IOException, XmlPullParserException { skipEmptyTextTags(parser); if (!accept(parser, XmlPullParser.START_TAG, TAG_JOB)) { return false; } PrintJobInfo printJob = new PrintJobInfo(); PrintJobId printJobId = PrintJobId.unflattenFromString( parser.getAttributeValue(null, ATTR_ID)); printJob.setId(printJobId); String label = parser.getAttributeValue(null, ATTR_LABEL); printJob.setLabel(label); final int state = Integer.parseInt(parser.getAttributeValue(null, ATTR_STATE)); printJob.setState(state); final int appId = Integer.parseInt(parser.getAttributeValue(null, ATTR_APP_ID)); printJob.setAppId(appId); String tag = parser.getAttributeValue(null, ATTR_TAG); printJob.setTag(tag); String creationTime = parser.getAttributeValue(null, ATTR_CREATION_TIME); printJob.setCreationTime(Long.parseLong(creationTime)); String copies = parser.getAttributeValue(null, ATTR_COPIES); printJob.setCopies(Integer.parseInt(copies)); String printerName = parser.getAttributeValue(null, ATTR_PRINTER_NAME); printJob.setPrinterName(printerName); String progressString = parser.getAttributeValue(null, ATTR_PROGRESS); if (progressString != null) { float progress = Float.parseFloat(progressString); if (progress != -1) { printJob.setProgress(progress); } } CharSequence status = parser.getAttributeValue(null, ATTR_STATUS); printJob.setStatus(status); // stateReason is deprecated, but might be used by old print jobs String stateReason = parser.getAttributeValue(null, ATTR_STATE_REASON); if (stateReason != null) { printJob.setStatus(stateReason); } String cancelling = parser.getAttributeValue(null, ATTR_CANCELLING); printJob.setCancelling(!TextUtils.isEmpty(cancelling) ? Boolean.parseBoolean(cancelling) : false); parser.next(); skipEmptyTextTags(parser); if (accept(parser, XmlPullParser.START_TAG, TAG_PRINTER_ID)) { String localId = parser.getAttributeValue(null, ATTR_LOCAL_ID); ComponentName service = ComponentName.unflattenFromString(parser.getAttributeValue( null, ATTR_SERVICE_NAME)); printJob.setPrinterId(new PrinterId(service, localId)); parser.next(); skipEmptyTextTags(parser); expect(parser, XmlPullParser.END_TAG, TAG_PRINTER_ID); parser.next(); } skipEmptyTextTags(parser); List pageRanges = null; while (accept(parser, XmlPullParser.START_TAG, TAG_PAGE_RANGE)) { final int start = Integer.parseInt(parser.getAttributeValue(null, ATTR_START)); final int end = Integer.parseInt(parser.getAttributeValue(null, ATTR_END)); PageRange pageRange = new PageRange(start, end); if (pageRanges == null) { pageRanges = new ArrayList(); } pageRanges.add(pageRange); parser.next(); skipEmptyTextTags(parser); expect(parser, XmlPullParser.END_TAG, TAG_PAGE_RANGE); parser.next(); skipEmptyTextTags(parser); } if (pageRanges != null) { PageRange[] pageRangesArray = new PageRange[pageRanges.size()]; pageRanges.toArray(pageRangesArray); printJob.setPages(pageRangesArray); } skipEmptyTextTags(parser); if (accept(parser, XmlPullParser.START_TAG, TAG_ATTRIBUTES)) { PrintAttributes.Builder builder = new PrintAttributes.Builder(); String colorMode = parser.getAttributeValue(null, ATTR_COLOR_MODE); builder.setColorMode(Integer.parseInt(colorMode)); String duplexMode = parser.getAttributeValue(null, ATTR_DUPLEX_MODE); // Duplex mode was added later, so null check is needed. if (duplexMode != null) { builder.setDuplexMode(Integer.parseInt(duplexMode)); } parser.next(); skipEmptyTextTags(parser); if (accept(parser, XmlPullParser.START_TAG, TAG_MEDIA_SIZE)) { String id = parser.getAttributeValue(null, ATTR_ID); label = parser.getAttributeValue(null, ATTR_LABEL); final int widthMils = Integer.parseInt(parser.getAttributeValue(null, ATTR_WIDTH_MILS)); final int heightMils = Integer.parseInt(parser.getAttributeValue(null, ATTR_HEIGHT_MILS)); String packageName = parser.getAttributeValue(null, ATTR_PACKAGE_NAME); String labelResIdString = parser.getAttributeValue(null, ATTR_LABEL_RES_ID); final int labelResId = (labelResIdString != null) ? Integer.parseInt(labelResIdString) : 0; label = parser.getAttributeValue(null, ATTR_LABEL); MediaSize mediaSize = new MediaSize(id, label, packageName, widthMils, heightMils, labelResId); builder.setMediaSize(mediaSize); parser.next(); skipEmptyTextTags(parser); expect(parser, XmlPullParser.END_TAG, TAG_MEDIA_SIZE); parser.next(); } skipEmptyTextTags(parser); if (accept(parser, XmlPullParser.START_TAG, TAG_RESOLUTION)) { String id = parser.getAttributeValue(null, ATTR_ID); label = parser.getAttributeValue(null, ATTR_LABEL); final int horizontalDpi = Integer.parseInt(parser.getAttributeValue(null, ATTR_HORIZONTAL_DPI)); final int verticalDpi = Integer.parseInt(parser.getAttributeValue(null, ATTR_VERTICAL_DPI)); Resolution resolution = new Resolution(id, label, horizontalDpi, verticalDpi); builder.setResolution(resolution); parser.next(); skipEmptyTextTags(parser); expect(parser, XmlPullParser.END_TAG, TAG_RESOLUTION); parser.next(); } skipEmptyTextTags(parser); if (accept(parser, XmlPullParser.START_TAG, TAG_MARGINS)) { final int leftMils = Integer.parseInt(parser.getAttributeValue(null, ATTR_LEFT_MILS)); final int topMils = Integer.parseInt(parser.getAttributeValue(null, ATTR_TOP_MILS)); final int rightMils = Integer.parseInt(parser.getAttributeValue(null, ATTR_RIGHT_MILS)); final int bottomMils = Integer.parseInt(parser.getAttributeValue(null, ATTR_BOTTOM_MILS)); Margins margins = new Margins(leftMils, topMils, rightMils, bottomMils); builder.setMinMargins(margins); parser.next(); skipEmptyTextTags(parser); expect(parser, XmlPullParser.END_TAG, TAG_MARGINS); parser.next(); } printJob.setAttributes(builder.build()); skipEmptyTextTags(parser); expect(parser, XmlPullParser.END_TAG, TAG_ATTRIBUTES); parser.next(); } skipEmptyTextTags(parser); if (accept(parser, XmlPullParser.START_TAG, TAG_DOCUMENT_INFO)) { String name = parser.getAttributeValue(null, ATTR_NAME); final int pageCount = Integer.parseInt(parser.getAttributeValue(null, ATTR_PAGE_COUNT)); final int contentType = Integer.parseInt(parser.getAttributeValue(null, ATTR_CONTENT_TYPE)); final int dataSize = Integer.parseInt(parser.getAttributeValue(null, ATTR_DATA_SIZE)); PrintDocumentInfo info = new PrintDocumentInfo.Builder(name) .setPageCount(pageCount) .setContentType(contentType).build(); printJob.setDocumentInfo(info); info.setDataSize(dataSize); parser.next(); skipEmptyTextTags(parser); expect(parser, XmlPullParser.END_TAG, TAG_DOCUMENT_INFO); parser.next(); } skipEmptyTextTags(parser); if (accept(parser, XmlPullParser.START_TAG, TAG_ADVANCED_OPTIONS)) { parser.next(); skipEmptyTextTags(parser); Bundle advancedOptions = new Bundle(); while (accept(parser, XmlPullParser.START_TAG, TAG_ADVANCED_OPTION)) { String key = parser.getAttributeValue(null, ATTR_KEY); String value = parser.getAttributeValue(null, ATTR_VALUE); String type = parser.getAttributeValue(null, ATTR_TYPE); if (TYPE_STRING.equals(type)) { advancedOptions.putString(key, value); } else if (TYPE_INT.equals(type)) { advancedOptions.putInt(key, Integer.parseInt(value)); } parser.next(); skipEmptyTextTags(parser); expect(parser, XmlPullParser.END_TAG, TAG_ADVANCED_OPTION); parser.next(); skipEmptyTextTags(parser); } printJob.setAdvancedOptions(advancedOptions); skipEmptyTextTags(parser); expect(parser, XmlPullParser.END_TAG, TAG_ADVANCED_OPTIONS); parser.next(); } mPrintJobs.add(printJob); if (DEBUG_PERSISTENCE) { Log.i(LOG_TAG, "[RESTORED] " + printJob); } skipEmptyTextTags(parser); expect(parser, XmlPullParser.END_TAG, TAG_JOB); return true; } private void expect(XmlPullParser parser, int type, String tag) throws XmlPullParserException { if (!accept(parser, type, tag)) { throw new XmlPullParserException("Exepected event: " + type + " and tag: " + tag + " but got event: " + parser.getEventType() + " and tag:" + parser.getName()); } } private void skipEmptyTextTags(XmlPullParser parser) throws IOException, XmlPullParserException { while (accept(parser, XmlPullParser.TEXT, null) && "\n".equals(parser.getText())) { parser.next(); } } private boolean accept(XmlPullParser parser, int type, String tag) throws XmlPullParserException { if (parser.getEventType() != type) { return false; } if (tag != null) { if (!tag.equals(parser.getName())) { return false; } } else if (parser.getName() != null) { return false; } return true; } } public final class PrintSpooler extends IPrintSpooler.Stub { @Override public void getPrintJobInfos(IPrintSpoolerCallbacks callback, ComponentName componentName, int state, int appId, int sequence) throws RemoteException { List printJobs = null; try { printJobs = PrintSpoolerService.this.getPrintJobInfos( componentName, state, appId); } finally { callback.onGetPrintJobInfosResult(printJobs, sequence); } } @Override public void getPrintJobInfo(PrintJobId printJobId, IPrintSpoolerCallbacks callback, int appId, int sequence) throws RemoteException { PrintJobInfo printJob = null; try { printJob = PrintSpoolerService.this.getPrintJobInfo(printJobId, appId); } finally { callback.onGetPrintJobInfoResult(printJob, sequence); } } @Override public void createPrintJob(PrintJobInfo printJob) { PrintSpoolerService.this.createPrintJob(printJob); } @Override public void setPrintJobState(PrintJobId printJobId, int state, String error, IPrintSpoolerCallbacks callback, int sequece) throws RemoteException { boolean success = false; try { success = PrintSpoolerService.this.setPrintJobState( printJobId, state, error); } finally { callback.onSetPrintJobStateResult(success, sequece); } } @Override public void setPrintJobTag(PrintJobId printJobId, String tag, IPrintSpoolerCallbacks callback, int sequece) throws RemoteException { boolean success = false; try { success = PrintSpoolerService.this.setPrintJobTag(printJobId, tag); } finally { callback.onSetPrintJobTagResult(success, sequece); } } @Override public void writePrintJobData(ParcelFileDescriptor fd, PrintJobId printJobId) { PrintSpoolerService.this.writePrintJobData(fd, printJobId); } @Override public void setClient(IPrintSpoolerClient client) { Message message = mHandlerCaller.obtainMessageO( HandlerCallerCallback.MSG_SET_CLIENT, client); mHandlerCaller.executeOrSendMessage(message); } @Override public void removeObsoletePrintJobs() { PrintSpoolerService.this.removeObsoletePrintJobs(); } @Override protected void dump(FileDescriptor fd, PrintWriter writer, String[] args) { PrintSpoolerService.this.dump(fd, writer, args); } @Override public void setPrintJobCancelling(PrintJobId printJobId, boolean cancelling) { PrintSpoolerService.this.setPrintJobCancelling(printJobId, cancelling); } @Override public void pruneApprovedPrintServices(List servicesToKeep) { (new ApprovedPrintServices(PrintSpoolerService.this)) .pruneApprovedServices(servicesToKeep); } @Override public void setProgress(@NonNull PrintJobId printJobId, @FloatRange(from=0.0, to=1.0) float progress) throws RemoteException { PrintSpoolerService.this.setProgress(printJobId, progress); } @Override public void setStatus(@NonNull PrintJobId printJobId, @Nullable CharSequence status) throws RemoteException { PrintSpoolerService.this.setStatus(printJobId, status); } @Override public void setStatusRes(@NonNull PrintJobId printJobId, @StringRes int status, @NonNull CharSequence appPackageName) throws RemoteException { PrintSpoolerService.this.setStatus(printJobId, status, appPackageName); } public PrintSpoolerService getService() { return PrintSpoolerService.this; } @Override public void onCustomPrinterIconLoaded(PrinterId printerId, Icon icon, IPrintSpoolerCallbacks callbacks, int sequence) throws RemoteException { try { PrintSpoolerService.this.onCustomPrinterIconLoaded(printerId, icon); } finally { callbacks.onCustomPrinterIconCached(sequence); } } @Override public void getCustomPrinterIcon(PrinterId printerId, IPrintSpoolerCallbacks callbacks, int sequence) throws RemoteException { Icon icon = null; try { icon = PrintSpoolerService.this.getCustomPrinterIcon(printerId); } finally { callbacks.onGetCustomPrinterIconResult(icon, sequence); } } @Override public void clearCustomPrinterIconCache(IPrintSpoolerCallbacks callbacks, int sequence) throws RemoteException { try { PrintSpoolerService.this.clearCustomPrinterIconCache(); } finally { callbacks.customPrinterIconCacheCleared(sequence); } } } }