/*
 * 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 static com.android.internal.print.DumpUtils.writePrinterId;
import static com.android.internal.util.dump.DumpUtils.writeComponentName;

import android.annotation.FloatRange;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.StringRes;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.pm.ParceledListSlice;
import android.graphics.drawable.Icon;
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.service.print.ActivePrintServiceProto;
import android.util.Slog;

import com.android.internal.annotations.GuardedBy;
import com.android.internal.util.dump.DualDumpOutputStream;

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 Object mLock = new Object();

    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;

    @GuardedBy("mLock")
    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);

        /**
         * 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);
    }

    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() {
        // 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() {
        if (mPrintService != null) {
            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() {
        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) {
        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) {
        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() {
        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() {
        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) {
        // 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() {
        // 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) {
        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(@NonNull PrinterId printerId) {
        mHandler.obtainMessage(MyHandler.MSG_START_PRINTER_STATE_TRACKING,
                printerId).sendToTarget();
    }

    /**
     * Queue a request for a custom printer icon for a printer.
     *
     * @param printerId the id of the printer the icon should be loaded for
     * @see android.print.PrinterInfo.Builder#setHasCustomPrinterIcon
     */
    public void requestCustomPrinterIcon(@NonNull PrinterId printerId) {
        mHandler.obtainMessage(MyHandler.MSG_REQUEST_CUSTOM_PRINTER_ICON,
                printerId).sendToTarget();
    }

    /**
     * Request a custom printer icon for a printer.
     *
     * @param printerId the id of the printer the icon should be loaded for
     * @see android.print.PrinterInfo.Builder#setHasCustomPrinterIcon
     */
    private void handleRequestCustomPrinterIcon(@NonNull PrinterId printerId) {
        if (!isBound()) {
            ensureBound();
            mPendingCommands.add(() -> handleRequestCustomPrinterIcon(printerId));
        } else {
            if (DEBUG) {
                Slog.i(LOG_TAG, "[user: " + mUserId + "] requestCustomPrinterIcon()");
            }

            try {
                mPrintService.requestCustomPrinterIcon(printerId);
            } catch (RemoteException re) {
                Slog.e(LOG_TAG, "Error requesting icon for " + printerId, re);
            }
        }
    }

    private void handleStartPrinterStateTracking(final @NonNull PrinterId printerId) {
        synchronized (mLock) {
            // 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) {
        synchronized (mLock) {
            // 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() {
        synchronized (mLock) {
            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(@NonNull DualDumpOutputStream proto) {
        writeComponentName(proto, "component_name", ActivePrintServiceProto.COMPONENT_NAME,
                mComponentName);

        proto.write("is_destroyed", ActivePrintServiceProto.IS_DESTROYED, mDestroyed);
        proto.write("is_bound", ActivePrintServiceProto.IS_BOUND, isBound());
        proto.write("has_discovery_session", ActivePrintServiceProto.HAS_DISCOVERY_SESSION,
                mHasPrinterDiscoverySession);
        proto.write("has_active_print_jobs", ActivePrintServiceProto.HAS_ACTIVE_PRINT_JOBS,
                mHasActivePrintJobs);
        proto.write("is_discovering_printers", ActivePrintServiceProto.IS_DISCOVERING_PRINTERS,
                mDiscoveryPriorityList != null);

        synchronized (mLock) {
            if (mTrackedPrinterList != null) {
                int numTrackedPrinters = mTrackedPrinterList.size();
                for (int i = 0; i < numTrackedPrinters; i++) {
                    writePrinterId(proto, "tracked_printers",
                            ActivePrintServiceProto.TRACKED_PRINTERS, mTrackedPrinterList.get(i));
                }
            }
        }
    }

    private boolean isBound() {
        return mPrintService != null;
    }

    private void ensureBound() {
        if (isBound() || mBinding) {
            return;
        }
        if (DEBUG) {
            Slog.i(LOG_TAG, "[user: " + mUserId + "] ensureBound()");
        }
        mBinding = true;

        boolean wasBound = mContext.bindServiceAsUser(mIntent, mServiceConnection,
                Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE,
                new UserHandle(mUserId));

        if (!wasBound) {
            if (DEBUG) {
                Slog.i(LOG_TAG, "[user: " + mUserId + "] could not bind to " + mIntent);
            }
            mBinding = false;

            if (!mServiceDied) {
                handleBinderDied();
            }
        }
    }

    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;

        synchronized (mLock) {
            mTrackedPrinterList = null;
        }

        if (isBound()) {
            try {
                mPrintService.setClient(null);
            } catch (RemoteException re) {
                /* ignore */
            }
            mPrintService.asBinder().unlinkToDeath(this, 0);
            mPrintService = null;
            mContext.unbindService(mServiceConnection);
        }
    }

    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);
            }
            synchronized (mLock) {
                // 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 static final int MSG_REQUEST_CUSTOM_PRINTER_ICON = 13;

        public MyHandler(Looper looper) {
            super(looper, null, false);
        }

        @Override
        @SuppressWarnings("unchecked")
        public void handleMessage(Message message) {
            if (mDestroyed) {
                Slog.w(LOG_TAG, "Not handling " + message + " as service for " + mComponentName
                        + " is already destroyed");
                return;
            }
            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;

                case MSG_REQUEST_CUSTOM_PRINTER_ICON: {
                    PrinterId printerId = (PrinterId) message.obj;
                    handleRequestCustomPrinterIcon(printerId);
                } 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
        public void setProgress(@NonNull PrintJobId printJobId,
                @FloatRange(from=0.0, to=1.0) float progress) {
            RemotePrintService service = mWeakService.get();
            if (service != null) {
                final long identity = Binder.clearCallingIdentity();
                try {
                    service.mSpooler.setProgress(printJobId, progress);
                } finally {
                    Binder.restoreCallingIdentity(identity);
                }
            }
        }

        @Override
        public void setStatus(@NonNull PrintJobId printJobId, @Nullable CharSequence status) {
            RemotePrintService service = mWeakService.get();
            if (service != null) {
                final long identity = Binder.clearCallingIdentity();
                try {
                    service.mSpooler.setStatus(printJobId, status);
                } finally {
                    Binder.restoreCallingIdentity(identity);
                }
            }
        }

        @Override
        public void setStatusRes(@NonNull PrintJobId printJobId, @StringRes int status,
                @NonNull CharSequence appPackageName) {
            RemotePrintService service = mWeakService.get();
            if (service != null) {
                final long identity = Binder.clearCallingIdentity();
                try {
                    service.mSpooler.setStatus(printJobId, status, appPackageName);
                } 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().equals(serviceName)) {
                throw new IllegalArgumentException("Invalid printer id: " + printerId);
            }
        }

        @Override
        public void onCustomPrinterIconLoaded(PrinterId printerId, Icon icon)
                throws RemoteException {
            RemotePrintService service = mWeakService.get();
            if (service != null) {
                final long identity = Binder.clearCallingIdentity();
                try {
                    service.mCallbacks.onCustomPrinterIconLoaded(printerId, icon);
                } finally {
                    Binder.restoreCallingIdentity(identity);
                }
            }
        }
    }
}
