/* * 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.server.print; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.ServiceConnection; import android.content.pm.ParceledListSlice; import android.os.Binder; import android.os.Handler; import android.os.IBinder; import android.os.IBinder.DeathRecipient; import android.os.Looper; import android.os.Message; import android.os.ParcelFileDescriptor; import android.os.RemoteException; import android.os.UserHandle; import android.print.PrintJobId; import android.print.PrintJobInfo; import android.print.PrintManager; import android.print.PrinterId; import android.print.PrinterInfo; import android.printservice.IPrintService; import android.printservice.IPrintServiceClient; import android.util.Slog; import java.io.PrintWriter; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.List; /** * This class represents a remote print service. It abstracts away the binding * and unbinding from the remote implementation. Clients can call methods of * this class without worrying about when and how to bind/unbind. */ final class RemotePrintService implements DeathRecipient { private static final String LOG_TAG = "RemotePrintService"; private static final boolean DEBUG = false; private final Context mContext; private final ComponentName mComponentName; private final Intent mIntent; private final RemotePrintSpooler mSpooler; private final PrintServiceCallbacks mCallbacks; private final int mUserId; private final List mPendingCommands = new ArrayList(); private final ServiceConnection mServiceConnection = new RemoteServiceConneciton(); private final RemotePrintServiceClient mPrintServiceClient; private final Handler mHandler; private IPrintService mPrintService; private boolean mBinding; private boolean mDestroyed; private boolean mHasActivePrintJobs; private boolean mHasPrinterDiscoverySession; private boolean mServiceDied; private List mDiscoveryPriorityList; private List mTrackedPrinterList; public static interface PrintServiceCallbacks { public void onPrintersAdded(List printers); public void onPrintersRemoved(List printerIds); public void onServiceDied(RemotePrintService service); } public RemotePrintService(Context context, ComponentName componentName, int userId, RemotePrintSpooler spooler, PrintServiceCallbacks callbacks) { mContext = context; mCallbacks = callbacks; mComponentName = componentName; mIntent = new Intent().setComponent(mComponentName); mUserId = userId; mSpooler = spooler; mHandler = new MyHandler(context.getMainLooper()); mPrintServiceClient = new RemotePrintServiceClient(this); } public ComponentName getComponentName() { return mComponentName; } public void destroy() { mHandler.sendEmptyMessage(MyHandler.MSG_DESTROY); } private void handleDestroy() { throwIfDestroyed(); // Stop tracking printers. stopTrackingAllPrinters(); // Stop printer discovery. if (mDiscoveryPriorityList != null) { handleStopPrinterDiscovery(); } // Destroy the discovery session. if (mHasPrinterDiscoverySession) { handleDestroyPrinterDiscoverySession(); } // Unbind. ensureUnbound(); // Done mDestroyed = true; } @Override public void binderDied() { mHandler.sendEmptyMessage(MyHandler.MSG_BINDER_DIED); } private void handleBinderDied() { mPrintService.asBinder().unlinkToDeath(this, 0); mPrintService = null; mServiceDied = true; mCallbacks.onServiceDied(this); } public void onAllPrintJobsHandled() { mHandler.sendEmptyMessage(MyHandler.MSG_ON_ALL_PRINT_JOBS_HANDLED); } private void handleOnAllPrintJobsHandled() { throwIfDestroyed(); mHasActivePrintJobs = false; if (!isBound()) { // The service is dead and neither has active jobs nor discovery // session, so ensure we are unbound since the service has no work. if (mServiceDied && !mHasPrinterDiscoverySession) { ensureUnbound(); return; } ensureBound(); mPendingCommands.add(new Runnable() { @Override public void run() { handleOnAllPrintJobsHandled(); } }); } else { if (DEBUG) { Slog.i(LOG_TAG, "[user: " + mUserId + "] onAllPrintJobsHandled()"); } // If the service has a printer discovery session // created we should not disconnect from it just yet. if (!mHasPrinterDiscoverySession) { ensureUnbound(); } } } public void onRequestCancelPrintJob(PrintJobInfo printJob) { mHandler.obtainMessage(MyHandler.MSG_ON_REQUEST_CANCEL_PRINT_JOB, printJob).sendToTarget(); } private void handleRequestCancelPrintJob(final PrintJobInfo printJob) { throwIfDestroyed(); if (!isBound()) { ensureBound(); mPendingCommands.add(new Runnable() { @Override public void run() { handleRequestCancelPrintJob(printJob); } }); } else { if (DEBUG) { Slog.i(LOG_TAG, "[user: " + mUserId + "] requestCancelPrintJob()"); } try { mPrintService.requestCancelPrintJob(printJob); } catch (RemoteException re) { Slog.e(LOG_TAG, "Error canceling a pring job.", re); } } } public void onPrintJobQueued(PrintJobInfo printJob) { mHandler.obtainMessage(MyHandler.MSG_ON_PRINT_JOB_QUEUED, printJob).sendToTarget(); } private void handleOnPrintJobQueued(final PrintJobInfo printJob) { throwIfDestroyed(); mHasActivePrintJobs = true; if (!isBound()) { ensureBound(); mPendingCommands.add(new Runnable() { @Override public void run() { handleOnPrintJobQueued(printJob); } }); } else { if (DEBUG) { Slog.i(LOG_TAG, "[user: " + mUserId + "] onPrintJobQueued()"); } try { mPrintService.onPrintJobQueued(printJob); } catch (RemoteException re) { Slog.e(LOG_TAG, "Error announcing queued pring job.", re); } } } public void createPrinterDiscoverySession() { mHandler.sendEmptyMessage(MyHandler.MSG_CREATE_PRINTER_DISCOVERY_SESSION); } private void handleCreatePrinterDiscoverySession() { throwIfDestroyed(); mHasPrinterDiscoverySession = true; if (!isBound()) { ensureBound(); mPendingCommands.add(new Runnable() { @Override public void run() { handleCreatePrinterDiscoverySession(); } }); } else { if (DEBUG) { Slog.i(LOG_TAG, "[user: " + mUserId + "] createPrinterDiscoverySession()"); } try { mPrintService.createPrinterDiscoverySession(); } catch (RemoteException re) { Slog.e(LOG_TAG, "Error creating printer discovery session.", re); } } } public void destroyPrinterDiscoverySession() { mHandler.sendEmptyMessage(MyHandler.MSG_DESTROY_PRINTER_DISCOVERY_SESSION); } private void handleDestroyPrinterDiscoverySession() { throwIfDestroyed(); mHasPrinterDiscoverySession = false; if (!isBound()) { // The service is dead and neither has active jobs nor discovery // session, so ensure we are unbound since the service has no work. if (mServiceDied && !mHasActivePrintJobs) { ensureUnbound(); return; } ensureBound(); mPendingCommands.add(new Runnable() { @Override public void run() { handleDestroyPrinterDiscoverySession(); } }); } else { if (DEBUG) { Slog.i(LOG_TAG, "[user: " + mUserId + "] destroyPrinterDiscoverySession()"); } try { mPrintService.destroyPrinterDiscoverySession(); } catch (RemoteException re) { Slog.e(LOG_TAG, "Error destroying printer dicovery session.", re); } // If the service has no print jobs and no active discovery // session anymore we should disconnect from it. if (!mHasActivePrintJobs) { ensureUnbound(); } } } public void startPrinterDiscovery(List priorityList) { mHandler.obtainMessage(MyHandler.MSG_START_PRINTER_DISCOVERY, priorityList).sendToTarget(); } private void handleStartPrinterDiscovery(final List priorityList) { throwIfDestroyed(); // Take a note that we are doing discovery. mDiscoveryPriorityList = new ArrayList(); if (priorityList != null) { mDiscoveryPriorityList.addAll(priorityList); } if (!isBound()) { ensureBound(); mPendingCommands.add(new Runnable() { @Override public void run() { handleStartPrinterDiscovery(priorityList); } }); } else { if (DEBUG) { Slog.i(LOG_TAG, "[user: " + mUserId + "] startPrinterDiscovery()"); } try { mPrintService.startPrinterDiscovery(priorityList); } catch (RemoteException re) { Slog.e(LOG_TAG, "Error starting printer dicovery.", re); } } } public void stopPrinterDiscovery() { mHandler.sendEmptyMessage(MyHandler.MSG_STOP_PRINTER_DISCOVERY); } private void handleStopPrinterDiscovery() { throwIfDestroyed(); // We are not doing discovery anymore. mDiscoveryPriorityList = null; if (!isBound()) { ensureBound(); mPendingCommands.add(new Runnable() { @Override public void run() { handleStopPrinterDiscovery(); } }); } else { if (DEBUG) { Slog.i(LOG_TAG, "[user: " + mUserId + "] stopPrinterDiscovery()"); } // Stop tracking printers. stopTrackingAllPrinters(); try { mPrintService.stopPrinterDiscovery(); } catch (RemoteException re) { Slog.e(LOG_TAG, "Error stopping printer discovery.", re); } } } public void validatePrinters(List printerIds) { mHandler.obtainMessage(MyHandler.MSG_VALIDATE_PRINTERS, printerIds).sendToTarget(); } private void handleValidatePrinters(final List printerIds) { throwIfDestroyed(); if (!isBound()) { ensureBound(); mPendingCommands.add(new Runnable() { @Override public void run() { handleValidatePrinters(printerIds); } }); } else { if (DEBUG) { Slog.i(LOG_TAG, "[user: " + mUserId + "] validatePrinters()"); } try { mPrintService.validatePrinters(printerIds); } catch (RemoteException re) { Slog.e(LOG_TAG, "Error requesting printers validation.", re); } } } public void startPrinterStateTracking(PrinterId printerId) { mHandler.obtainMessage(MyHandler.MSG_START_PRINTER_STATE_TRACKING, printerId).sendToTarget(); } private void handleStartPrinterStateTracking(final PrinterId printerId) { throwIfDestroyed(); // Take a note we are tracking the printer. if (mTrackedPrinterList == null) { mTrackedPrinterList = new ArrayList(); } mTrackedPrinterList.add(printerId); if (!isBound()) { ensureBound(); mPendingCommands.add(new Runnable() { @Override public void run() { handleStartPrinterStateTracking(printerId); } }); } else { if (DEBUG) { Slog.i(LOG_TAG, "[user: " + mUserId + "] startPrinterTracking()"); } try { mPrintService.startPrinterStateTracking(printerId); } catch (RemoteException re) { Slog.e(LOG_TAG, "Error requesting start printer tracking.", re); } } } public void stopPrinterStateTracking(PrinterId printerId) { mHandler.obtainMessage(MyHandler.MSG_STOP_PRINTER_STATE_TRACKING, printerId).sendToTarget(); } private void handleStopPrinterStateTracking(final PrinterId printerId) { throwIfDestroyed(); // We are no longer tracking the printer. if (mTrackedPrinterList == null || !mTrackedPrinterList.remove(printerId)) { return; } if (mTrackedPrinterList.isEmpty()) { mTrackedPrinterList = null; } if (!isBound()) { ensureBound(); mPendingCommands.add(new Runnable() { @Override public void run() { handleStopPrinterStateTracking(printerId); } }); } else { if (DEBUG) { Slog.i(LOG_TAG, "[user: " + mUserId + "] stopPrinterTracking()"); } try { mPrintService.stopPrinterStateTracking(printerId); } catch (RemoteException re) { Slog.e(LOG_TAG, "Error requesting stop printer tracking.", re); } } } private void stopTrackingAllPrinters() { if (mTrackedPrinterList == null) { return; } final int trackedPrinterCount = mTrackedPrinterList.size(); for (int i = trackedPrinterCount - 1; i >= 0; i--) { PrinterId printerId = mTrackedPrinterList.get(i); if (printerId.getServiceName().equals(mComponentName)) { handleStopPrinterStateTracking(printerId); } } } public void dump(PrintWriter pw, String prefix) { String tab = " "; pw.append(prefix).append("service:").println(); pw.append(prefix).append(tab).append("componentName=") .append(mComponentName.flattenToString()).println(); pw.append(prefix).append(tab).append("destroyed=") .append(String.valueOf(mDestroyed)).println(); pw.append(prefix).append(tab).append("bound=") .append(String.valueOf(isBound())).println(); pw.append(prefix).append(tab).append("hasDicoverySession=") .append(String.valueOf(mHasPrinterDiscoverySession)).println(); pw.append(prefix).append(tab).append("hasActivePrintJobs=") .append(String.valueOf(mHasActivePrintJobs)).println(); pw.append(prefix).append(tab).append("isDiscoveringPrinters=") .append(String.valueOf(mDiscoveryPriorityList != null)).println(); pw.append(prefix).append(tab).append("trackedPrinters=") .append((mTrackedPrinterList != null) ? mTrackedPrinterList.toString() : "null"); } private boolean isBound() { return mPrintService != null; } private void ensureBound() { if (isBound() || mBinding) { return; } if (DEBUG) { Slog.i(LOG_TAG, "[user: " + mUserId + "] ensureBound()"); } mBinding = true; mContext.bindServiceAsUser(mIntent, mServiceConnection, Context.BIND_AUTO_CREATE, new UserHandle(mUserId)); } private void ensureUnbound() { if (!isBound() && !mBinding) { return; } if (DEBUG) { Slog.i(LOG_TAG, "[user: " + mUserId + "] ensureUnbound()"); } mBinding = false; mPendingCommands.clear(); mHasActivePrintJobs = false; mHasPrinterDiscoverySession = false; mDiscoveryPriorityList = null; mTrackedPrinterList = null; if (isBound()) { try { mPrintService.setClient(null); } catch (RemoteException re) { /* ignore */ } mPrintService.asBinder().unlinkToDeath(this, 0); mPrintService = null; mContext.unbindService(mServiceConnection); } } private void throwIfDestroyed() { if (mDestroyed) { throw new IllegalStateException("Cannot interact with a destroyed service"); } } private class RemoteServiceConneciton implements ServiceConnection { @Override public void onServiceConnected(ComponentName name, IBinder service) { if (mDestroyed || !mBinding) { mContext.unbindService(mServiceConnection); return; } mBinding = false; mPrintService = IPrintService.Stub.asInterface(service); try { service.linkToDeath(RemotePrintService.this, 0); } catch (RemoteException re) { handleBinderDied(); return; } try { mPrintService.setClient(mPrintServiceClient); } catch (RemoteException re) { Slog.e(LOG_TAG, "Error setting client for: " + service, re); handleBinderDied(); return; } // If the service died and there is a discovery session, recreate it. if (mServiceDied && mHasPrinterDiscoverySession) { handleCreatePrinterDiscoverySession(); } // If the service died and there is discovery started, restart it. if (mServiceDied && mDiscoveryPriorityList != null) { handleStartPrinterDiscovery(mDiscoveryPriorityList); } // If the service died and printers were tracked, start tracking. if (mServiceDied && mTrackedPrinterList != null) { final int trackedPrinterCount = mTrackedPrinterList.size(); for (int i = 0; i < trackedPrinterCount; i++) { handleStartPrinterStateTracking(mTrackedPrinterList.get(i)); } } // Finally, do all the pending work. while (!mPendingCommands.isEmpty()) { Runnable pendingCommand = mPendingCommands.remove(0); pendingCommand.run(); } // We did a best effort to get to the last state if we crashed. // If we do not have print jobs and no discovery is in progress, // then no need to be bound. if (!mHasPrinterDiscoverySession && !mHasActivePrintJobs) { ensureUnbound(); } mServiceDied = false; } @Override public void onServiceDisconnected(ComponentName name) { mBinding = true; } } private final class MyHandler extends Handler { public static final int MSG_CREATE_PRINTER_DISCOVERY_SESSION = 1; public static final int MSG_DESTROY_PRINTER_DISCOVERY_SESSION = 2; public static final int MSG_START_PRINTER_DISCOVERY = 3; public static final int MSG_STOP_PRINTER_DISCOVERY = 4; public static final int MSG_VALIDATE_PRINTERS = 5; public static final int MSG_START_PRINTER_STATE_TRACKING = 6; public static final int MSG_STOP_PRINTER_STATE_TRACKING = 7; public static final int MSG_ON_ALL_PRINT_JOBS_HANDLED = 8; public static final int MSG_ON_REQUEST_CANCEL_PRINT_JOB = 9; public static final int MSG_ON_PRINT_JOB_QUEUED = 10; public static final int MSG_DESTROY = 11; public static final int MSG_BINDER_DIED = 12; public MyHandler(Looper looper) { super(looper, null, false); } @Override @SuppressWarnings("unchecked") public void handleMessage(Message message) { switch (message.what) { case MSG_CREATE_PRINTER_DISCOVERY_SESSION: { handleCreatePrinterDiscoverySession(); } break; case MSG_DESTROY_PRINTER_DISCOVERY_SESSION: { handleDestroyPrinterDiscoverySession(); } break; case MSG_START_PRINTER_DISCOVERY: { List priorityList = (ArrayList) message.obj; handleStartPrinterDiscovery(priorityList); } break; case MSG_STOP_PRINTER_DISCOVERY: { handleStopPrinterDiscovery(); } break; case MSG_VALIDATE_PRINTERS: { List printerIds = (List) message.obj; handleValidatePrinters(printerIds); } break; case MSG_START_PRINTER_STATE_TRACKING: { PrinterId printerId = (PrinterId) message.obj; handleStartPrinterStateTracking(printerId); } break; case MSG_STOP_PRINTER_STATE_TRACKING: { PrinterId printerId = (PrinterId) message.obj; handleStopPrinterStateTracking(printerId); } break; case MSG_ON_ALL_PRINT_JOBS_HANDLED: { handleOnAllPrintJobsHandled(); } break; case MSG_ON_REQUEST_CANCEL_PRINT_JOB: { PrintJobInfo printJob = (PrintJobInfo) message.obj; handleRequestCancelPrintJob(printJob); } break; case MSG_ON_PRINT_JOB_QUEUED: { PrintJobInfo printJob = (PrintJobInfo) message.obj; handleOnPrintJobQueued(printJob); } break; case MSG_DESTROY: { handleDestroy(); } break; case MSG_BINDER_DIED: { handleBinderDied(); } break; } } } private static final class RemotePrintServiceClient extends IPrintServiceClient.Stub { private final WeakReference mWeakService; public RemotePrintServiceClient(RemotePrintService service) { mWeakService = new WeakReference(service); } @Override public List getPrintJobInfos() { RemotePrintService service = mWeakService.get(); if (service != null) { final long identity = Binder.clearCallingIdentity(); try { return service.mSpooler.getPrintJobInfos(service.mComponentName, PrintJobInfo.STATE_ANY_SCHEDULED, PrintManager.APP_ID_ANY); } finally { Binder.restoreCallingIdentity(identity); } } return null; } @Override public PrintJobInfo getPrintJobInfo(PrintJobId printJobId) { RemotePrintService service = mWeakService.get(); if (service != null) { final long identity = Binder.clearCallingIdentity(); try { return service.mSpooler.getPrintJobInfo(printJobId, PrintManager.APP_ID_ANY); } finally { Binder.restoreCallingIdentity(identity); } } return null; } @Override public boolean setPrintJobState(PrintJobId printJobId, int state, String error) { RemotePrintService service = mWeakService.get(); if (service != null) { final long identity = Binder.clearCallingIdentity(); try { return service.mSpooler.setPrintJobState(printJobId, state, error); } finally { Binder.restoreCallingIdentity(identity); } } return false; } @Override public boolean setPrintJobTag(PrintJobId printJobId, String tag) { RemotePrintService service = mWeakService.get(); if (service != null) { final long identity = Binder.clearCallingIdentity(); try { return service.mSpooler.setPrintJobTag(printJobId, tag); } finally { Binder.restoreCallingIdentity(identity); } } return false; } @Override public void writePrintJobData(ParcelFileDescriptor fd, PrintJobId printJobId) { RemotePrintService service = mWeakService.get(); if (service != null) { final long identity = Binder.clearCallingIdentity(); try { service.mSpooler.writePrintJobData(fd, printJobId); } finally { Binder.restoreCallingIdentity(identity); } } } @Override @SuppressWarnings({"rawtypes", "unchecked"}) public void onPrintersAdded(ParceledListSlice printers) { RemotePrintService service = mWeakService.get(); if (service != null) { List addedPrinters = (List) printers.getList(); throwIfPrinterIdsForPrinterInfoTampered(service.mComponentName, addedPrinters); final long identity = Binder.clearCallingIdentity(); try { service.mCallbacks.onPrintersAdded(addedPrinters); } finally { Binder.restoreCallingIdentity(identity); } } } @Override @SuppressWarnings({"rawtypes", "unchecked"}) public void onPrintersRemoved(ParceledListSlice printerIds) { RemotePrintService service = mWeakService.get(); if (service != null) { List removedPrinterIds = (List) printerIds.getList(); throwIfPrinterIdsTampered(service.mComponentName, removedPrinterIds); final long identity = Binder.clearCallingIdentity(); try { service.mCallbacks.onPrintersRemoved(removedPrinterIds); } finally { Binder.restoreCallingIdentity(identity); } } } private void throwIfPrinterIdsForPrinterInfoTampered(ComponentName serviceName, List printerInfos) { final int printerInfoCount = printerInfos.size(); for (int i = 0; i < printerInfoCount; i++) { PrinterId printerId = printerInfos.get(i).getId(); throwIfPrinterIdTampered(serviceName, printerId); } } private void throwIfPrinterIdsTampered(ComponentName serviceName, List printerIds) { final int printerIdCount = printerIds.size(); for (int i = 0; i < printerIdCount; i++) { PrinterId printerId = printerIds.get(i); throwIfPrinterIdTampered(serviceName, printerId); } } private void throwIfPrinterIdTampered(ComponentName serviceName, PrinterId printerId) { if (printerId == null || printerId.getServiceName() == null || !printerId.getServiceName().equals(serviceName)) { throw new IllegalArgumentException("Invalid printer id: " + printerId); } } } }