blob: a8c739c841f2b035d1f8f3e34efecd0a875fee50 [file] [log] [blame]
/*
* 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<Runnable> mPendingCommands = new ArrayList<Runnable>();
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<PrinterId> mDiscoveryPriorityList;
private List<PrinterId> mTrackedPrinterList;
public static interface PrintServiceCallbacks {
public void onPrintersAdded(List<PrinterInfo> printers);
public void onPrintersRemoved(List<PrinterId> 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<PrinterId> priorityList) {
mHandler.obtainMessage(MyHandler.MSG_START_PRINTER_DISCOVERY,
priorityList).sendToTarget();
}
private void handleStartPrinterDiscovery(final List<PrinterId> priorityList) {
throwIfDestroyed();
// Take a note that we are doing discovery.
mDiscoveryPriorityList = new ArrayList<PrinterId>();
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<PrinterId> printerIds) {
mHandler.obtainMessage(MyHandler.MSG_VALIDATE_PRINTERS,
printerIds).sendToTarget();
}
private void handleValidatePrinters(final List<PrinterId> 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<PrinterId>();
}
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<PrinterId> priorityList = (ArrayList<PrinterId>) message.obj;
handleStartPrinterDiscovery(priorityList);
} break;
case MSG_STOP_PRINTER_DISCOVERY: {
handleStopPrinterDiscovery();
} break;
case MSG_VALIDATE_PRINTERS: {
List<PrinterId> printerIds = (List<PrinterId>) 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<RemotePrintService> mWeakService;
public RemotePrintServiceClient(RemotePrintService service) {
mWeakService = new WeakReference<RemotePrintService>(service);
}
@Override
public List<PrintJobInfo> 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<PrinterInfo> addedPrinters = (List<PrinterInfo>) 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<PrinterId> removedPrinterIds = (List<PrinterId>) 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<PrinterInfo> 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<PrinterId> 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);
}
}
}
}