content: /*
   * 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.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.graphics.drawable.Icon;
  import android.os.Binder;
  import android.os.Build;
  import android.os.IBinder;
  import android.os.ParcelFileDescriptor;
  import android.os.RemoteException;
  import android.os.SystemClock;
  import android.os.UserHandle;
  import android.print.IPrintSpooler;
  import android.print.IPrintSpoolerCallbacks;
  import android.print.IPrintSpoolerClient;
  import android.print.PrintJobId;
  import android.print.PrintJobInfo;
  import android.print.PrintManager;
  import android.print.PrinterId;
  import android.printservice.PrintService;
  import android.service.print.PrintSpoolerStateProto;
  import android.util.Slog;
  import android.util.TimedRemoteCaller;
  
  import com.android.internal.annotations.GuardedBy;
  import com.android.internal.os.TransferPipe;
  import com.android.internal.util.dump.DualDumpOutputStream;
  
  import libcore.io.IoUtils;
  
  import java.io.IOException;
  import java.lang.ref.WeakReference;
  import java.util.List;
  import java.util.concurrent.TimeoutException;
  
  /**
   * This represents the remote print spooler as a local object to the
   * PrintManagerService. It is responsible to connecting to the remote
   * spooler if needed, to make the timed remote calls, to handle
   * remote exceptions, and to bind/unbind to the remote instance as
   * needed.
   *
   * The calls might be blocking and need the main thread of to be unblocked to finish. Hence do not
   * call this while holding any monitors that might need to be acquired the main thread.
   */
  final class RemotePrintSpooler {
  
      private static final String LOG_TAG = "RemotePrintSpooler";
  
      private static final boolean DEBUG = false;
  
      private static final long BIND_SPOOLER_SERVICE_TIMEOUT =
              (Build.IS_ENG) ? 120000 : 10000;
  
      private final Object mLock = new Object();
  
      private final GetPrintJobInfosCaller mGetPrintJobInfosCaller = new GetPrintJobInfosCaller();
  
      private final GetPrintJobInfoCaller mGetPrintJobInfoCaller = new GetPrintJobInfoCaller();
  
      private final SetPrintJobStateCaller mSetPrintJobStatusCaller = new SetPrintJobStateCaller();
  
      private final SetPrintJobTagCaller mSetPrintJobTagCaller = new SetPrintJobTagCaller();
  
      private final OnCustomPrinterIconLoadedCaller mCustomPrinterIconLoadedCaller =
              new OnCustomPrinterIconLoadedCaller();
  
      private final ClearCustomPrinterIconCacheCaller mClearCustomPrinterIconCache =
              new ClearCustomPrinterIconCacheCaller();
  
      private final GetCustomPrinterIconCaller mGetCustomPrinterIconCaller =
              new GetCustomPrinterIconCaller();
  
      private final ServiceConnection mServiceConnection = new MyServiceConnection();
  
      private final Context mContext;
  
      private final UserHandle mUserHandle;
  
      private final PrintSpoolerClient mClient;
  
      private final Intent mIntent;
  
      private final PrintSpoolerCallbacks mCallbacks;
  
      private boolean mIsLowPriority;
  
      private IPrintSpooler mRemoteInstance;
  
      private boolean mDestroyed;
  
      private boolean mCanUnbind;
  
      /** Whether a thread is currently trying to {@link #bindLocked() bind to the print service} */
      @GuardedBy("mLock")
      private boolean mIsBinding;
  
      public static interface PrintSpoolerCallbacks {
          public void onPrintJobQueued(PrintJobInfo printJob);
          public void onAllPrintJobsForServiceHandled(ComponentName printService);
          public void onPrintJobStateChanged(PrintJobInfo printJob);
      }
  
      public RemotePrintSpooler(Context context, int userId, boolean lowPriority,
              PrintSpoolerCallbacks callbacks) {
          mContext = context;
          mUserHandle = new UserHandle(userId);
          mCallbacks = callbacks;
          mIsLowPriority = lowPriority;
          mClient = new PrintSpoolerClient(this);
          mIntent = new Intent();
          mIntent.setComponent(new ComponentName(PrintManager.PRINT_SPOOLER_PACKAGE_NAME,
                  PrintManager.PRINT_SPOOLER_PACKAGE_NAME + ".model.PrintSpoolerService"));
      }
  
      public void increasePriority() {
          if (mIsLowPriority) {
              mIsLowPriority = false;
  
              synchronized (mLock) {
                  throwIfDestroyedLocked();
  
                  while (!mCanUnbind) {
                      try {
                          mLock.wait();
                      } catch (InterruptedException e) {
                          Slog.e(LOG_TAG, "Interrupted while waiting for operation to complete");
                      }
                  }
  
                  if (DEBUG) {
                      Slog.i(LOG_TAG, "Unbinding as previous binding was low priority");
                  }
  
                  unbindLocked();
              }
          }
      }
  
      public final List<PrintJobInfo> getPrintJobInfos(ComponentName componentName, int state,
              int appId) {
          throwIfCalledOnMainThread();
          synchronized (mLock) {
              throwIfDestroyedLocked();
              mCanUnbind = false;
          }
          try {
              return mGetPrintJobInfosCaller.getPrintJobInfos(getRemoteInstanceLazy(),
                      componentName, state, appId);
          } catch (RemoteException | TimeoutException | InterruptedException e) {
              Slog.e(LOG_TAG, "Error getting print jobs.", e);
          } finally {
              if (DEBUG) {
                  Slog.i(LOG_TAG, "[user: " + mUserHandle.getIdentifier() + "] getPrintJobInfos()");
              }
              synchronized (mLock) {
                  mCanUnbind = true;
                  mLock.notifyAll();
              }
          }
          return null;
      }
  
      public final void createPrintJob(PrintJobInfo printJob) {
          throwIfCalledOnMainThread();
          synchronized (mLock) {
              throwIfDestroyedLocked();
              mCanUnbind = false;
          }
          try {
              getRemoteInstanceLazy().createPrintJob(printJob);
          } catch (RemoteException | TimeoutException | InterruptedException e) {
              Slog.e(LOG_TAG, "Error creating print job.", e);
          } finally {
              if (DEBUG) {
                  Slog.i(LOG_TAG, "[user: " + mUserHandle.getIdentifier() + "] createPrintJob()");
              }
              synchronized (mLock) {
                  mCanUnbind = true;
                  mLock.notifyAll();
              }
          }
      }
  
      public final void writePrintJobData(ParcelFileDescriptor fd, PrintJobId printJobId) {
          throwIfCalledOnMainThread();
          synchronized (mLock) {
              throwIfDestroyedLocked();
              mCanUnbind = false;
          }
          try {
              getRemoteInstanceLazy().writePrintJobData(fd, printJobId);
          } catch (RemoteException | TimeoutException | InterruptedException e) {
              Slog.e(LOG_TAG, "Error writing print job data.", e);
          } finally {
              if (DEBUG) {
                  Slog.i(LOG_TAG, "[user: " + mUserHandle.getIdentifier() + "] writePrintJobData()");
              }
              // We passed the file descriptor across and now the other
              // side is responsible to close it, so close the local copy.
              IoUtils.closeQuietly(fd);
              synchronized (mLock) {
                  mCanUnbind = true;
                  mLock.notifyAll();
              }
          }
      }
  
      public final PrintJobInfo getPrintJobInfo(PrintJobId printJobId, int appId) {
          throwIfCalledOnMainThread();
          synchronized (mLock) {
              throwIfDestroyedLocked();
              mCanUnbind = false;
          }
          try {
              return mGetPrintJobInfoCaller.getPrintJobInfo(getRemoteInstanceLazy(),
                      printJobId, appId);
          } catch (RemoteException | TimeoutException | InterruptedException e) {
              Slog.e(LOG_TAG, "Error getting print job info.", e);
          } finally {
              if (DEBUG) {
                  Slog.i(LOG_TAG, "[user: " + mUserHandle.getIdentifier() + "] getPrintJobInfo()");
              }
              synchronized (mLock) {
                  mCanUnbind = true;
                  mLock.notifyAll();
              }
          }
          return null;
      }
  
      public final boolean setPrintJobState(PrintJobId printJobId, int state, String error) {
          throwIfCalledOnMainThread();
          synchronized (mLock) {
              throwIfDestroyedLocked();
              mCanUnbind = false;
          }
          try {
              return mSetPrintJobStatusCaller.setPrintJobState(getRemoteInstanceLazy(),
                      printJobId, state, error);
          } catch (RemoteException | TimeoutException | InterruptedException e) {
              Slog.e(LOG_TAG, "Error setting print job state.", e);
          } finally {
              if (DEBUG) {
                  Slog.i(LOG_TAG, "[user: " + mUserHandle.getIdentifier() + "] setPrintJobState()");
              }
              synchronized (mLock) {
                  mCanUnbind = true;
                  mLock.notifyAll();
              }
          }
          return false;
      }
  
      /**
       * Set progress of a print job.
       *
       * @param printJobId The print job to update
       * @param progress The new progress
       */
      public final void setProgress(@NonNull PrintJobId printJobId,
              @FloatRange(from=0.0, to=1.0) float progress) {
          throwIfCalledOnMainThread();
          synchronized (mLock) {
              throwIfDestroyedLocked();
              mCanUnbind = false;
          }
          try {
              getRemoteInstanceLazy().setProgress(printJobId, progress);
          } catch (RemoteException | TimeoutException | InterruptedException re) {
              Slog.e(LOG_TAG, "Error setting progress.", re);
          } finally {
              if (DEBUG) {
                  Slog.i(LOG_TAG, "[user: " + mUserHandle.getIdentifier() + "] setProgress()");
              }
              synchronized (mLock) {
                  mCanUnbind = true;
                  mLock.notifyAll();
              }
          }
      }
  
      /**
       * Set status of a print job.
       *
       * @param printJobId The print job to update
       * @param status The new status
       */
      public final void setStatus(@NonNull PrintJobId printJobId, @Nullable CharSequence status) {
          throwIfCalledOnMainThread();
          synchronized (mLock) {
              throwIfDestroyedLocked();
              mCanUnbind = false;
          }
          try {
              getRemoteInstanceLazy().setStatus(printJobId, status);
          } catch (RemoteException | TimeoutException | InterruptedException e) {
              Slog.e(LOG_TAG, "Error setting status.", e);
          } finally {
              if (DEBUG) {
                  Slog.i(LOG_TAG, "[user: " + mUserHandle.getIdentifier() + "] setStatus()");
              }
              synchronized (mLock) {
                  mCanUnbind = true;
                  mLock.notifyAll();
              }
          }
      }
  
      /**
       * Set status of a print job.
       *
       * @param printJobId The print job to update
       * @param status The new status as a string resource
       * @param appPackageName The app package name the string res belongs to
       */
      public final void setStatus(@NonNull PrintJobId printJobId, @StringRes int status,
              @NonNull CharSequence appPackageName) {
          throwIfCalledOnMainThread();
          synchronized (mLock) {
              throwIfDestroyedLocked();
              mCanUnbind = false;
          }
          try {
              getRemoteInstanceLazy().setStatusRes(printJobId, status, appPackageName);
          } catch (RemoteException | TimeoutException | InterruptedException e) {
              Slog.e(LOG_TAG, "Error setting status.", e);
          } finally {
              if (DEBUG) {
                  Slog.i(LOG_TAG, "[user: " + mUserHandle.getIdentifier() + "] setStatus()");
              }
              synchronized (mLock) {
                  mCanUnbind = true;
                  mLock.notifyAll();
              }
          }
      }
  
      /**
       * 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 final void onCustomPrinterIconLoaded(@NonNull PrinterId printerId,
              @Nullable Icon icon) {
          throwIfCalledOnMainThread();
          synchronized (mLock) {
              throwIfDestroyedLocked();
              mCanUnbind = false;
          }
          try {
              mCustomPrinterIconLoadedCaller.onCustomPrinterIconLoaded(getRemoteInstanceLazy(),
                      printerId, icon);
          } catch (RemoteException | TimeoutException | InterruptedException re) {
              Slog.e(LOG_TAG, "Error loading new custom printer icon.", re);
          } finally {
              if (DEBUG) {
                  Slog.i(LOG_TAG,
                          "[user: " + mUserHandle.getIdentifier() + "] onCustomPrinterIconLoaded()");
              }
              synchronized (mLock) {
                  mCanUnbind = true;
                  mLock.notifyAll();
              }
          }
      }
  
      /**
       * 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 final @Nullable Icon getCustomPrinterIcon(@NonNull PrinterId printerId) {
          throwIfCalledOnMainThread();
          synchronized (mLock) {
              throwIfDestroyedLocked();
              mCanUnbind = false;
          }
          try {
              return mGetCustomPrinterIconCaller.getCustomPrinterIcon(getRemoteInstanceLazy(),
                      printerId);
          } catch (RemoteException | TimeoutException | InterruptedException e) {
              Slog.e(LOG_TAG, "Error getting custom printer icon.", e);
              return null;
          } finally {
              if (DEBUG) {
                  Slog.i(LOG_TAG,
                          "[user: " + mUserHandle.getIdentifier() + "] getCustomPrinterIcon()");
              }
              synchronized (mLock) {
                  mCanUnbind = true;
                  mLock.notifyAll();
              }
          }
      }
  
      /**
       * Clear the custom printer icon cache
       */
      public void clearCustomPrinterIconCache() {
          throwIfCalledOnMainThread();
          synchronized (mLock) {
              throwIfDestroyedLocked();
              mCanUnbind = false;
          }
          try {
              mClearCustomPrinterIconCache.clearCustomPrinterIconCache(getRemoteInstanceLazy());
          } catch (RemoteException | TimeoutException | InterruptedException e) {
              Slog.e(LOG_TAG, "Error clearing custom printer icon cache.", e);
          } finally {
              if (DEBUG) {
                  Slog.i(LOG_TAG,
                          "[user: " + mUserHandle.getIdentifier()
                                  + "] clearCustomPrinterIconCache()");
              }
              synchronized (mLock) {
                  mCanUnbind = true;
                  mLock.notifyAll();
              }
          }
      }
  
      public final boolean setPrintJobTag(PrintJobId printJobId, String tag) {
          throwIfCalledOnMainThread();
          synchronized (mLock) {
              throwIfDestroyedLocked();
              mCanUnbind = false;
          }
          try {
              return mSetPrintJobTagCaller.setPrintJobTag(getRemoteInstanceLazy(),
                      printJobId, tag);
          } catch (RemoteException | TimeoutException | InterruptedException e) {
              Slog.e(LOG_TAG, "Error setting print job tag.", e);
          } finally {
              if (DEBUG) {
                  Slog.i(LOG_TAG, "[user: " + mUserHandle.getIdentifier() + "] setPrintJobTag()");
              }
              synchronized (mLock) {
                  mCanUnbind = true;
                  mLock.notifyAll();
              }
          }
          return false;
      }
  
      public final void setPrintJobCancelling(PrintJobId printJobId, boolean cancelling) {
          throwIfCalledOnMainThread();
          synchronized (mLock) {
              throwIfDestroyedLocked();
              mCanUnbind = false;
          }
          try {
              getRemoteInstanceLazy().setPrintJobCancelling(printJobId,
                      cancelling);
          } catch (RemoteException | TimeoutException | InterruptedException e) {
              Slog.e(LOG_TAG, "Error setting print job cancelling.", e);
          } finally {
              if (DEBUG) {
                  Slog.i(LOG_TAG, "[user: " + mUserHandle.getIdentifier()
                          + "] setPrintJobCancelling()");
              }
              synchronized (mLock) {
                  mCanUnbind = true;
                  mLock.notifyAll();
              }
          }
      }
  
      /**
       * Remove all approved {@link PrintService print services} that are not in the given set.
       *
       * @param servicesToKeep The {@link ComponentName names } of the services to keep
       */
      public final void pruneApprovedPrintServices(List<ComponentName> servicesToKeep) {
          throwIfCalledOnMainThread();
          synchronized (mLock) {
              throwIfDestroyedLocked();
              mCanUnbind = false;
          }
          try {
              getRemoteInstanceLazy().pruneApprovedPrintServices(servicesToKeep);
          } catch (RemoteException | TimeoutException | InterruptedException e) {
              Slog.e(LOG_TAG, "Error pruning approved print services.", e);
          } finally {
              if (DEBUG) {
                  Slog.i(LOG_TAG, "[user: " + mUserHandle.getIdentifier()
                          + "] pruneApprovedPrintServices()");
              }
              synchronized (mLock) {
                  mCanUnbind = true;
                  mLock.notifyAll();
              }
          }
      }
  
      public final void removeObsoletePrintJobs() {
          throwIfCalledOnMainThread();
          synchronized (mLock) {
              throwIfDestroyedLocked();
              mCanUnbind = false;
          }
          try {
              getRemoteInstanceLazy().removeObsoletePrintJobs();
          } catch (RemoteException | TimeoutException | InterruptedException te) {
              Slog.e(LOG_TAG, "Error removing obsolete print jobs .", te);
          } finally {
              if (DEBUG) {
                  Slog.i(LOG_TAG, "[user: " + mUserHandle.getIdentifier()
                          + "] removeObsoletePrintJobs()");
              }
              synchronized (mLock) {
                  mCanUnbind = true;
                  mLock.notifyAll();
              }
          }
      }
  
      public final void destroy() {
          throwIfCalledOnMainThread();
          if (DEBUG) {
              Slog.i(LOG_TAG, "[user: " + mUserHandle.getIdentifier() + "] destroy()");
          }
          synchronized (mLock) {
              throwIfDestroyedLocked();
              unbindLocked();
              mDestroyed = true;
              mCanUnbind = false;
          }
      }
  
      public void dump(@NonNull DualDumpOutputStream dumpStream) {
          synchronized (mLock) {
              dumpStream.write("is_destroyed", PrintSpoolerStateProto.IS_DESTROYED, mDestroyed);
              dumpStream.write("is_bound", PrintSpoolerStateProto.IS_BOUND, mRemoteInstance != null);
          }
  
          try {
              if (dumpStream.isProto()) {
                  dumpStream.write(null, PrintSpoolerStateProto.INTERNAL_STATE,
                          TransferPipe.dumpAsync(getRemoteInstanceLazy().asBinder(), "--proto"));
              } else {
                  dumpStream.writeNested("internal_state", TransferPipe.dumpAsync(
                          getRemoteInstanceLazy().asBinder()));
              }
          } catch (IOException | TimeoutException | RemoteException | InterruptedException e) {
              Slog.e(LOG_TAG, "Failed to dump remote instance", e);
          }
      }
  
      private void onAllPrintJobsHandled() {
          synchronized (mLock) {
              throwIfDestroyedLocked();
              unbindLocked();
          }
      }
  
      private void onPrintJobStateChanged(PrintJobInfo printJob) {
          mCallbacks.onPrintJobStateChanged(printJob);
      }
  
      private IPrintSpooler getRemoteInstanceLazy() throws TimeoutException, InterruptedException {
          synchronized (mLock) {
              if (mRemoteInstance != null) {
                  return mRemoteInstance;
              }
              bindLocked();
              return mRemoteInstance;
          }
      }
  
      @GuardedBy("mLock")
      private void bindLocked() throws TimeoutException, InterruptedException {
          while (mIsBinding) {
              mLock.wait();
          }
  
          if (mRemoteInstance != null) {
              return;
          }
  
          mIsBinding = true;
  
          if (DEBUG) {
              Slog.i(LOG_TAG, "[user: " + mUserHandle.getIdentifier() + "] bindLocked() " +
                      (mIsLowPriority ? "low priority" : ""));
          }
  
          try {
              int flags;
              if (mIsLowPriority) {
                  flags = Context.BIND_AUTO_CREATE;
              } else {
                  flags = Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE;
              }
  
              mContext.bindServiceAsUser(mIntent, mServiceConnection, flags, mUserHandle);
  
              final long startMillis = SystemClock.uptimeMillis();
              while (true) {
                  if (mRemoteInstance != null) {
                      break;
                  }
                  final long elapsedMillis = SystemClock.uptimeMillis() - startMillis;
                  final long remainingMillis = BIND_SPOOLER_SERVICE_TIMEOUT - elapsedMillis;
                  if (remainingMillis <= 0) {
                      throw new TimeoutException("Cannot get spooler!");
                  }
                  mLock.wait(remainingMillis);
              }
  
              mCanUnbind = true;
          } finally {
              mIsBinding = false;
              mLock.notifyAll();
          }
      }
  
      private void unbindLocked() {
          if (mRemoteInstance == null) {
              return;
          }
          while (true) {
              if (mCanUnbind) {
                  if (DEBUG) {
                      Slog.i(LOG_TAG, "[user: " + mUserHandle.getIdentifier() + "] unbindLocked()");
                  }
                  clearClientLocked();
                  mRemoteInstance = null;
                  mContext.unbindService(mServiceConnection);
                  return;
              }
              try {
                  mLock.wait();
              } catch (InterruptedException ie) {
                  /* ignore */
              }
          }
  
      }
  
      private void setClientLocked() {
          try {
              mRemoteInstance.setClient(mClient);
          } catch (RemoteException re) {
              Slog.d(LOG_TAG, "Error setting print spooler client", re);
          }
      }
  
      private void clearClientLocked() {
          try {
              mRemoteInstance.setClient(null);
          } catch (RemoteException re) {
              Slog.d(LOG_TAG, "Error clearing print spooler client", re);
          }
  
      }
  
      private void throwIfDestroyedLocked() {
          if (mDestroyed) {
              throw new IllegalStateException("Cannot interact with a destroyed instance.");
          }
      }
  
      private void throwIfCalledOnMainThread() {
          if (Thread.currentThread() == mContext.getMainLooper().getThread()) {
              throw new RuntimeException("Cannot invoke on the main thread");
          }
      }
  
      private final class MyServiceConnection implements ServiceConnection {
          @Override
          public void onServiceConnected(ComponentName name, IBinder service) {
              synchronized (mLock) {
                  mRemoteInstance = IPrintSpooler.Stub.asInterface(service);
                  setClientLocked();
                  mLock.notifyAll();
              }
          }
  
          @Override
          public void onServiceDisconnected(ComponentName name) {
              synchronized (mLock) {
                  if (mRemoteInstance != null) {
                      clearClientLocked();
                      mRemoteInstance = null;
                  }
              }
          }
      }
  
      private static final class GetPrintJobInfosCaller
              extends TimedRemoteCaller<List<PrintJobInfo>> {
          private final IPrintSpoolerCallbacks mCallback;
  
          public GetPrintJobInfosCaller() {
              super(TimedRemoteCaller.DEFAULT_CALL_TIMEOUT_MILLIS);
              mCallback = new BasePrintSpoolerServiceCallbacks() {
                  @Override
                  public void onGetPrintJobInfosResult(List<PrintJobInfo> printJobs, int sequence) {
                      onRemoteMethodResult(printJobs, sequence);
                  }
              };
          }
  
          public List<PrintJobInfo> getPrintJobInfos(IPrintSpooler target,
                  ComponentName componentName, int state, int appId)
                          throws RemoteException, TimeoutException {
              final int sequence = onBeforeRemoteCall();
              target.getPrintJobInfos(mCallback, componentName, state, appId, sequence);
              return getResultTimed(sequence);
          }
      }
  
      private static final class GetPrintJobInfoCaller extends TimedRemoteCaller<PrintJobInfo> {
          private final IPrintSpoolerCallbacks mCallback;
  
          public GetPrintJobInfoCaller() {
              super(TimedRemoteCaller.DEFAULT_CALL_TIMEOUT_MILLIS);
              mCallback = new BasePrintSpoolerServiceCallbacks() {
                  @Override
                  public void onGetPrintJobInfoResult(PrintJobInfo printJob, int sequence) {
                      onRemoteMethodResult(printJob, sequence);
                  }
              };
          }
  
          public PrintJobInfo getPrintJobInfo(IPrintSpooler target, PrintJobId printJobId,
                  int appId) throws RemoteException, TimeoutException {
              final int sequence = onBeforeRemoteCall();
              target.getPrintJobInfo(printJobId, mCallback, appId, sequence);
              return getResultTimed(sequence);
          }
      }
  
      private static final class SetPrintJobStateCaller extends TimedRemoteCaller<Boolean> {
          private final IPrintSpoolerCallbacks mCallback;
  
          public SetPrintJobStateCaller() {
              super(TimedRemoteCaller.DEFAULT_CALL_TIMEOUT_MILLIS);
              mCallback = new BasePrintSpoolerServiceCallbacks() {
                  @Override
                  public void onSetPrintJobStateResult(boolean success, int sequence) {
                      onRemoteMethodResult(success, sequence);
                  }
              };
          }
  
          public boolean setPrintJobState(IPrintSpooler target, PrintJobId printJobId,
                  int status, String error) throws RemoteException, TimeoutException {
              final int sequence = onBeforeRemoteCall();
              target.setPrintJobState(printJobId, status, error, mCallback, sequence);
              return getResultTimed(sequence);
          }
      }
  
      private static final class SetPrintJobTagCaller extends TimedRemoteCaller<Boolean> {
          private final IPrintSpoolerCallbacks mCallback;
  
          public SetPrintJobTagCaller() {
              super(TimedRemoteCaller.DEFAULT_CALL_TIMEOUT_MILLIS);
              mCallback = new BasePrintSpoolerServiceCallbacks() {
                  @Override
                  public void onSetPrintJobTagResult(boolean success, int sequence) {
                      onRemoteMethodResult(success, sequence);
                  }
              };
          }
  
          public boolean setPrintJobTag(IPrintSpooler target, PrintJobId printJobId,
                  String tag) throws RemoteException, TimeoutException {
              final int sequence = onBeforeRemoteCall();
              target.setPrintJobTag(printJobId, tag, mCallback, sequence);
              return getResultTimed(sequence);
          }
      }
  
      private static final class OnCustomPrinterIconLoadedCaller extends TimedRemoteCaller<Void> {
          private final IPrintSpoolerCallbacks mCallback;
  
          public OnCustomPrinterIconLoadedCaller() {
              super(TimedRemoteCaller.DEFAULT_CALL_TIMEOUT_MILLIS);
              mCallback = new BasePrintSpoolerServiceCallbacks() {
                  @Override
                  public void onCustomPrinterIconCached(int sequence) {
                      onRemoteMethodResult(null, sequence);
                  }
              };
          }
  
          public Void onCustomPrinterIconLoaded(IPrintSpooler target, PrinterId printerId,
                  Icon icon) throws RemoteException, TimeoutException {
              final int sequence = onBeforeRemoteCall();
              target.onCustomPrinterIconLoaded(printerId, icon, mCallback, sequence);
              return getResultTimed(sequence);
          }
      }
  
      private static final class ClearCustomPrinterIconCacheCaller extends TimedRemoteCaller<Void> {
          private final IPrintSpoolerCallbacks mCallback;
  
          public ClearCustomPrinterIconCacheCaller() {
              super(TimedRemoteCaller.DEFAULT_CALL_TIMEOUT_MILLIS);
              mCallback = new BasePrintSpoolerServiceCallbacks() {
                  @Override
                  public void customPrinterIconCacheCleared(int sequence) {
                      onRemoteMethodResult(null, sequence);
                  }
              };
          }
  
          public Void clearCustomPrinterIconCache(IPrintSpooler target)
                  throws RemoteException, TimeoutException {
              final int sequence = onBeforeRemoteCall();
              target.clearCustomPrinterIconCache(mCallback, sequence);
              return getResultTimed(sequence);
          }
      }
  
      private static final class GetCustomPrinterIconCaller extends TimedRemoteCaller<Icon> {
          private final IPrintSpoolerCallbacks mCallback;
  
          public GetCustomPrinterIconCaller() {
              super(TimedRemoteCaller.DEFAULT_CALL_TIMEOUT_MILLIS);
              mCallback = new BasePrintSpoolerServiceCallbacks() {
                  @Override
                  public void onGetCustomPrinterIconResult(Icon icon, int sequence) {
                      onRemoteMethodResult(icon, sequence);
                  }
              };
          }
  
          public Icon getCustomPrinterIcon(IPrintSpooler target, PrinterId printerId)
                  throws RemoteException, TimeoutException {
              final int sequence = onBeforeRemoteCall();
              target.getCustomPrinterIcon(printerId, mCallback, sequence);
              return getResultTimed(sequence);
          }
      }
  
      private static abstract class BasePrintSpoolerServiceCallbacks
              extends IPrintSpoolerCallbacks.Stub {
          @Override
          public void onGetPrintJobInfosResult(List<PrintJobInfo> printJobIds, int sequence) {
              /* do nothing */
          }
  
          @Override
          public void onGetPrintJobInfoResult(PrintJobInfo printJob, int sequence) {
              /* do nothing */
          }
  
          @Override
          public void onCancelPrintJobResult(boolean canceled, int sequence) {
              /* do nothing */
          }
  
          @Override
          public void onSetPrintJobStateResult(boolean success, int sequece) {
              /* do nothing */
          }
  
          @Override
          public void onSetPrintJobTagResult(boolean success, int sequence) {
              /* do nothing */
          }
  
          @Override
          public void onCustomPrinterIconCached(int sequence) {
              /* do nothing */
          }
  
          @Override
          public void onGetCustomPrinterIconResult(@Nullable Icon icon, int sequence) {
              /* do nothing */
          }
  
          @Override
          public void customPrinterIconCacheCleared(int sequence) {
              /* do nothing */
          }
      }
  
      private static final class PrintSpoolerClient extends IPrintSpoolerClient.Stub {
  
          private final WeakReference<RemotePrintSpooler> mWeakSpooler;
  
          public PrintSpoolerClient(RemotePrintSpooler spooler) {
              mWeakSpooler = new WeakReference<RemotePrintSpooler>(spooler);
          }
  
          @Override
          public void onPrintJobQueued(PrintJobInfo printJob) {
              RemotePrintSpooler spooler = mWeakSpooler.get();
              if (spooler != null) {
                  final long identity = Binder.clearCallingIdentity();
                  try {
                      spooler.mCallbacks.onPrintJobQueued(printJob);
                  } finally {
                      Binder.restoreCallingIdentity(identity);
                  }
              }
          }
  
          @Override
          public void onAllPrintJobsForServiceHandled(ComponentName printService) {
              RemotePrintSpooler spooler = mWeakSpooler.get();
              if (spooler != null) {
                  final long identity = Binder.clearCallingIdentity();
                  try {
                      spooler.mCallbacks.onAllPrintJobsForServiceHandled(printService);
                  } finally {
                      Binder.restoreCallingIdentity(identity);
                  }
              }
          }
  
          @Override
          public void onAllPrintJobsHandled() {
              RemotePrintSpooler spooler = mWeakSpooler.get();
              if (spooler != null) {
                  final long identity = Binder.clearCallingIdentity();
                  try {
                      spooler.onAllPrintJobsHandled();
                  } finally {
                      Binder.restoreCallingIdentity(identity);
                  }
              }
          }
  
          @Override
          public void onPrintJobStateChanged(PrintJobInfo printJob) {
              RemotePrintSpooler spooler = mWeakSpooler.get();
              if (spooler != null) {
                  final long identity = Binder.clearCallingIdentity();
                  try {
                      spooler.onPrintJobStateChanged(printJob);
                  } finally {
                      Binder.restoreCallingIdentity(identity);
                  }
              }
          }
      }
  }
  