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 static android.content.pm.PackageManager.GET_META_DATA;
  import static android.content.pm.PackageManager.GET_SERVICES;
  import static android.content.pm.PackageManager.MATCH_DEBUG_TRIAGED_MISSING;
  import static android.content.pm.PackageManager.MATCH_INSTANT;
  
  import static com.android.internal.print.DumpUtils.writePrintJobInfo;
  import static com.android.internal.print.DumpUtils.writePrinterId;
  import static com.android.internal.print.DumpUtils.writePrinterInfo;
  import static com.android.internal.util.dump.DumpUtils.writeComponentName;
  import static com.android.internal.util.dump.DumpUtils.writeStringIfNotNull;
  import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage;
  
  import android.annotation.NonNull;
  import android.annotation.Nullable;
  import android.app.PendingIntent;
  import android.content.ComponentName;
  import android.content.Context;
  import android.content.Intent;
  import android.content.IntentSender;
  import android.content.pm.ParceledListSlice;
  import android.content.pm.ResolveInfo;
  import android.graphics.drawable.Icon;
  import android.net.Uri;
  import android.os.Binder;
  import android.os.Bundle;
  import android.os.Handler;
  import android.os.IBinder;
  import android.os.IBinder.DeathRecipient;
  import android.os.IInterface;
  import android.os.Looper;
  import android.os.RemoteCallbackList;
  import android.os.RemoteException;
  import android.os.UserHandle;
  import android.print.IPrintDocumentAdapter;
  import android.print.IPrintJobStateChangeListener;
  import android.print.IPrintServicesChangeListener;
  import android.print.IPrinterDiscoveryObserver;
  import android.print.PrintAttributes;
  import android.print.PrintJobId;
  import android.print.PrintJobInfo;
  import android.print.PrintManager;
  import android.print.PrinterId;
  import android.print.PrinterInfo;
  import android.printservice.PrintServiceInfo;
  import android.printservice.recommendation.IRecommendationsChangeListener;
  import android.printservice.recommendation.RecommendationInfo;
  import android.provider.DocumentsContract;
  import android.provider.Settings;
  import android.service.print.CachedPrintJobProto;
  import android.service.print.InstalledPrintServiceProto;
  import android.service.print.PrintUserStateProto;
  import android.service.print.PrinterDiscoverySessionProto;
  import android.text.TextUtils;
  import android.text.TextUtils.SimpleStringSplitter;
  import android.util.ArrayMap;
  import android.util.ArraySet;
  import android.util.Log;
  import android.util.Slog;
  import android.util.SparseArray;
  
  import com.android.internal.R;
  import com.android.internal.logging.MetricsLogger;
  import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
  import com.android.internal.os.BackgroundThread;
  import com.android.internal.util.dump.DualDumpOutputStream;
  import com.android.internal.util.function.pooled.PooledLambda;
  import com.android.server.print.RemotePrintService.PrintServiceCallbacks;
  import com.android.server.print.RemotePrintServiceRecommendationService
          .RemotePrintServiceRecommendationServiceCallbacks;
  import com.android.server.print.RemotePrintSpooler.PrintSpoolerCallbacks;
  
  import java.util.ArrayList;
  import java.util.Collections;
  import java.util.HashSet;
  import java.util.Iterator;
  import java.util.List;
  import java.util.Map;
  import java.util.Set;
  import java.util.function.IntSupplier;
  
  /**
   * Represents the print state for a user.
   */
  final class UserState implements PrintSpoolerCallbacks, PrintServiceCallbacks,
          RemotePrintServiceRecommendationServiceCallbacks {
  
      private static final String LOG_TAG = "UserState";
  
      private static final boolean DEBUG = false;
  
      private static final char COMPONENT_NAME_SEPARATOR = ':';
  
      private static final int SERVICE_RESTART_DELAY_MILLIS = 500;
  
      private final SimpleStringSplitter mStringColonSplitter =
              new SimpleStringSplitter(COMPONENT_NAME_SEPARATOR);
  
      private final Intent mQueryIntent =
              new Intent(android.printservice.PrintService.SERVICE_INTERFACE);
  
      private final ArrayMap<ComponentName, RemotePrintService> mActiveServices =
              new ArrayMap<ComponentName, RemotePrintService>();
  
      private final List<PrintServiceInfo> mInstalledServices =
              new ArrayList<PrintServiceInfo>();
  
      private final Set<ComponentName> mDisabledServices =
              new ArraySet<ComponentName>();
  
      private final PrintJobForAppCache mPrintJobForAppCache =
              new PrintJobForAppCache();
  
      private final Object mLock;
  
      private final Context mContext;
  
      private final int mUserId;
  
      private final RemotePrintSpooler mSpooler;
  
      private PrinterDiscoverySessionMediator mPrinterDiscoverySession;
  
      private List<PrintJobStateChangeListenerRecord> mPrintJobStateChangeListenerRecords;
  
      private List<ListenerRecord<IPrintServicesChangeListener>> mPrintServicesChangeListenerRecords;
  
      private List<ListenerRecord<IRecommendationsChangeListener>>
              mPrintServiceRecommendationsChangeListenerRecords;
  
      private boolean mDestroyed;
  
      /** Currently known list of print service recommendations */
      private List<RecommendationInfo> mPrintServiceRecommendations;
  
      /**
       * Connection to the service updating the {@link #mPrintServiceRecommendations print service
       * recommendations}.
       */
      private RemotePrintServiceRecommendationService mPrintServiceRecommendationsService;
  
      /**
       * Can services from instant apps be bound? (usually disabled, only used by testing)
       */
      private boolean mIsInstantServiceAllowed;
  
      public UserState(Context context, int userId, Object lock, boolean lowPriority) {
          mContext = context;
          mUserId = userId;
          mLock = lock;
          mSpooler = new RemotePrintSpooler(context, userId, lowPriority, this);
  
          synchronized (mLock) {
              readInstalledPrintServicesLocked();
              upgradePersistentStateIfNeeded();
              readDisabledPrintServicesLocked();
          }
  
          // Some print services might have gotten installed before the User State came up
          prunePrintServices();
  
          onConfigurationChanged();
      }
  
      public void increasePriority() {
          mSpooler.increasePriority();
      }
  
      @Override
      public void onPrintJobQueued(PrintJobInfo printJob) {
          final RemotePrintService service;
          synchronized (mLock) {
              throwIfDestroyedLocked();
              ComponentName printServiceName = printJob.getPrinterId().getServiceName();
              service = mActiveServices.get(printServiceName);
          }
          if (service != null) {
              service.onPrintJobQueued(printJob);
          } else {
              // The service for the job is no longer enabled, so just
              // fail the job with the appropriate message.
              mSpooler.setPrintJobState(printJob.getId(), PrintJobInfo.STATE_FAILED,
                      mContext.getString(R.string.reason_service_unavailable));
          }
      }
  
      @Override
      public void onAllPrintJobsForServiceHandled(ComponentName printService) {
          final RemotePrintService service;
          synchronized (mLock) {
              throwIfDestroyedLocked();
              service = mActiveServices.get(printService);
          }
          if (service != null) {
              service.onAllPrintJobsHandled();
          }
      }
  
      public void removeObsoletePrintJobs() {
          mSpooler.removeObsoletePrintJobs();
      }
  
      @SuppressWarnings("deprecation")
      public Bundle print(@NonNull String printJobName, @NonNull IPrintDocumentAdapter adapter,
              @Nullable PrintAttributes attributes, @NonNull String packageName, int appId) {
          // Create print job place holder.
          final PrintJobInfo printJob = new PrintJobInfo();
          printJob.setId(new PrintJobId());
          printJob.setAppId(appId);
          printJob.setLabel(printJobName);
          printJob.setAttributes(attributes);
          printJob.setState(PrintJobInfo.STATE_CREATED);
          printJob.setCopies(1);
          printJob.setCreationTime(System.currentTimeMillis());
  
          // Track this job so we can forget it when the creator dies.
          if (!mPrintJobForAppCache.onPrintJobCreated(adapter.asBinder(), appId,
                  printJob)) {
              // Not adding a print job means the client is dead - done.
              return null;
          }
  
          final long identity = Binder.clearCallingIdentity();
          try {
              Intent intent = new Intent(PrintManager.ACTION_PRINT_DIALOG);
              intent.setData(Uri.fromParts("printjob", printJob.getId().flattenToString(), null));
              intent.putExtra(PrintManager.EXTRA_PRINT_DOCUMENT_ADAPTER, adapter.asBinder());
              intent.putExtra(PrintManager.EXTRA_PRINT_JOB, printJob);
              intent.putExtra(Intent.EXTRA_PACKAGE_NAME, packageName);
  
              IntentSender intentSender = PendingIntent.getActivityAsUser(
                      mContext, 0, intent, PendingIntent.FLAG_ONE_SHOT
                      | PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE,
                      null, new UserHandle(mUserId)) .getIntentSender();
  
              Bundle result = new Bundle();
              result.putParcelable(PrintManager.EXTRA_PRINT_JOB, printJob);
              result.putParcelable(PrintManager.EXTRA_PRINT_DIALOG_INTENT, intentSender);
  
              return result;
          } finally {
              Binder.restoreCallingIdentity(identity);
          }
      }
  
      public List<PrintJobInfo> getPrintJobInfos(int appId) {
          List<PrintJobInfo> cachedPrintJobs = mPrintJobForAppCache.getPrintJobs(appId);
          // Note that the print spooler is not storing print jobs that
          // are in a terminal state as it is non-trivial to properly update
          // the spooler state for when to forget print jobs in terminal state.
          // Therefore, we fuse the cached print jobs for running apps (some
          // jobs are in a terminal state) with the ones that the print
          // spooler knows about (some jobs are being processed).
          ArrayMap<PrintJobId, PrintJobInfo> result =
                  new ArrayMap<PrintJobId, PrintJobInfo>();
  
          // Add the cached print jobs for running apps.
          final int cachedPrintJobCount = cachedPrintJobs.size();
          for (int i = 0; i < cachedPrintJobCount; i++) {
              PrintJobInfo cachedPrintJob = cachedPrintJobs.get(i);
              result.put(cachedPrintJob.getId(), cachedPrintJob);
              // Strip out the tag and the advanced print options.
              // They are visible only to print services.
              cachedPrintJob.setTag(null);
              cachedPrintJob.setAdvancedOptions(null);
          }
  
          // Add everything else the spooler knows about.
          List<PrintJobInfo> printJobs = mSpooler.getPrintJobInfos(null,
                  PrintJobInfo.STATE_ANY, appId);
          if (printJobs != null) {
              final int printJobCount = printJobs.size();
              for (int i = 0; i < printJobCount; i++) {
                  PrintJobInfo printJob = printJobs.get(i);
                  result.put(printJob.getId(), printJob);
                  // Strip out the tag and the advanced print options.
                  // They are visible only to print services.
                  printJob.setTag(null);
                  printJob.setAdvancedOptions(null);
              }
          }
  
          return new ArrayList<PrintJobInfo>(result.values());
      }
  
      public PrintJobInfo getPrintJobInfo(@NonNull PrintJobId printJobId, int appId) {
          PrintJobInfo printJob = mPrintJobForAppCache.getPrintJob(printJobId, appId);
          if (printJob == null) {
              printJob = mSpooler.getPrintJobInfo(printJobId, appId);
          }
          if (printJob != null) {
              // Strip out the tag and the advanced print options.
              // They are visible only to print services.
              printJob.setTag(null);
              printJob.setAdvancedOptions(null);
          }
          return printJob;
      }
  
      /**
       * 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 @Nullable Icon getCustomPrinterIcon(@NonNull PrinterId printerId) {
          Icon icon = mSpooler.getCustomPrinterIcon(printerId);
  
          if (icon == null) {
              RemotePrintService service = mActiveServices.get(printerId.getServiceName());
              if (service != null) {
                  service.requestCustomPrinterIcon(printerId);
              }
          }
  
          return icon;
      }
  
      public void cancelPrintJob(@NonNull PrintJobId printJobId, int appId) {
          PrintJobInfo printJobInfo = mSpooler.getPrintJobInfo(printJobId, appId);
          if (printJobInfo == null) {
              return;
          }
  
          // Take a note that we are trying to cancel the job.
          mSpooler.setPrintJobCancelling(printJobId, true);
  
          if (printJobInfo.getState() != PrintJobInfo.STATE_FAILED) {
              PrinterId printerId = printJobInfo.getPrinterId();
  
              if (printerId != null) {
                  ComponentName printServiceName = printerId.getServiceName();
                  RemotePrintService printService = null;
                  synchronized (mLock) {
                      printService = mActiveServices.get(printServiceName);
                  }
                  if (printService == null) {
                      return;
                  }
                  printService.onRequestCancelPrintJob(printJobInfo);
              }
          } else {
              // If the print job is failed we do not need cooperation
              // from the print service.
              mSpooler.setPrintJobState(printJobId, PrintJobInfo.STATE_CANCELED, null);
          }
      }
  
      public void restartPrintJob(@NonNull PrintJobId printJobId, int appId) {
          PrintJobInfo printJobInfo = getPrintJobInfo(printJobId, appId);
          if (printJobInfo == null || printJobInfo.getState() != PrintJobInfo.STATE_FAILED) {
              return;
          }
          mSpooler.setPrintJobState(printJobId, PrintJobInfo.STATE_QUEUED, null);
      }
  
      public @Nullable List<PrintServiceInfo> getPrintServices(int selectionFlags) {
          synchronized (mLock) {
              List<PrintServiceInfo> selectedServices = null;
              final int installedServiceCount = mInstalledServices.size();
              for (int i = 0; i < installedServiceCount; i++) {
                  PrintServiceInfo installedService = mInstalledServices.get(i);
  
                  ComponentName componentName = new ComponentName(
                          installedService.getResolveInfo().serviceInfo.packageName,
                          installedService.getResolveInfo().serviceInfo.name);
  
                  // Update isEnabled under the same lock the final returned list is created
                  installedService.setIsEnabled(mActiveServices.containsKey(componentName));
  
                  if (installedService.isEnabled()) {
                      if ((selectionFlags & PrintManager.ENABLED_SERVICES) == 0) {
                          continue;
                      }
                  } else {
                      if ((selectionFlags & PrintManager.DISABLED_SERVICES) == 0) {
                          continue;
                      }
                  }
  
                  if (selectedServices == null) {
                      selectedServices = new ArrayList<>();
                  }
                  selectedServices.add(installedService);
              }
              return selectedServices;
          }
      }
  
      public void setPrintServiceEnabled(@NonNull ComponentName serviceName, boolean isEnabled) {
          synchronized (mLock) {
              boolean isChanged = false;
              if (isEnabled) {
                  isChanged = mDisabledServices.remove(serviceName);
              } else {
                  // Make sure to only disable services that are currently installed
                  final int numServices = mInstalledServices.size();
                  for (int i = 0; i < numServices; i++) {
                      PrintServiceInfo service = mInstalledServices.get(i);
  
                      if (service.getComponentName().equals(serviceName)) {
                          mDisabledServices.add(serviceName);
                          isChanged = true;
                          break;
                      }
                  }
              }
  
              if (isChanged) {
                  writeDisabledPrintServicesLocked(mDisabledServices);
  
                  MetricsLogger.action(mContext, MetricsEvent.ACTION_PRINT_SERVICE_TOGGLE,
                          isEnabled ? 0 : 1);
  
                  onConfigurationChangedLocked();
              }
          }
      }
  
      /**
       * @return The currently known print service recommendations
       */
      public @Nullable List<RecommendationInfo> getPrintServiceRecommendations() {
          return mPrintServiceRecommendations;
      }
  
      public void createPrinterDiscoverySession(@NonNull IPrinterDiscoveryObserver observer) {
          mSpooler.clearCustomPrinterIconCache();
  
          synchronized (mLock) {
              throwIfDestroyedLocked();
  
              if (mPrinterDiscoverySession == null) {
                  // If we do not have a session, tell all service to create one.
                  mPrinterDiscoverySession = new PrinterDiscoverySessionMediator() {
                      @Override
                      public void onDestroyed() {
                          mPrinterDiscoverySession = null;
                      }
                  };
                  // Add the observer to the brand new session.
                  mPrinterDiscoverySession.addObserverLocked(observer);
              } else {
                  // If services have created session, just add the observer.
                  mPrinterDiscoverySession.addObserverLocked(observer);
              }
          }
      }
  
      public void destroyPrinterDiscoverySession(@NonNull IPrinterDiscoveryObserver observer) {
          synchronized (mLock) {
              // Already destroyed - nothing to do.
              if (mPrinterDiscoverySession == null) {
                  return;
              }
              // Remove this observer.
              mPrinterDiscoverySession.removeObserverLocked(observer);
          }
      }
  
      public void startPrinterDiscovery(@NonNull IPrinterDiscoveryObserver observer,
              @Nullable List<PrinterId> printerIds) {
          synchronized (mLock) {
              throwIfDestroyedLocked();
  
              // No session - nothing to do.
              if (mPrinterDiscoverySession == null) {
                  return;
              }
              // Kick of discovery.
              mPrinterDiscoverySession.startPrinterDiscoveryLocked(observer,
                      printerIds);
          }
      }
  
      public void stopPrinterDiscovery(@NonNull IPrinterDiscoveryObserver observer) {
          synchronized (mLock) {
              throwIfDestroyedLocked();
  
              // No session - nothing to do.
              if (mPrinterDiscoverySession == null) {
                  return;
              }
              // Kick of discovery.
              mPrinterDiscoverySession.stopPrinterDiscoveryLocked(observer);
          }
      }
  
      public void validatePrinters(@NonNull List<PrinterId> printerIds) {
          synchronized (mLock) {
              throwIfDestroyedLocked();
              // No services - nothing to do.
              if (mActiveServices.isEmpty()) {
                  return;
              }
              // No session - nothing to do.
              if (mPrinterDiscoverySession == null) {
                  return;
              }
              // Request an updated.
              mPrinterDiscoverySession.validatePrintersLocked(printerIds);
          }
      }
  
      public void startPrinterStateTracking(@NonNull PrinterId printerId) {
          synchronized (mLock) {
              throwIfDestroyedLocked();
              // No services - nothing to do.
              if (mActiveServices.isEmpty()) {
                  return;
              }
              // No session - nothing to do.
              if (mPrinterDiscoverySession == null) {
                  return;
              }
              // Request start tracking the printer.
              mPrinterDiscoverySession.startPrinterStateTrackingLocked(printerId);
          }
      }
  
      public void stopPrinterStateTracking(PrinterId printerId) {
          synchronized (mLock) {
              throwIfDestroyedLocked();
              // No services - nothing to do.
              if (mActiveServices.isEmpty()) {
                  return;
              }
              // No session - nothing to do.
              if (mPrinterDiscoverySession == null) {
                  return;
              }
              // Request stop tracking the printer.
              mPrinterDiscoverySession.stopPrinterStateTrackingLocked(printerId);
          }
      }
  
      public void addPrintJobStateChangeListener(@NonNull IPrintJobStateChangeListener listener,
              int appId) throws RemoteException {
          synchronized (mLock) {
              throwIfDestroyedLocked();
              if (mPrintJobStateChangeListenerRecords == null) {
                  mPrintJobStateChangeListenerRecords =
                          new ArrayList<PrintJobStateChangeListenerRecord>();
              }
              mPrintJobStateChangeListenerRecords.add(
                      new PrintJobStateChangeListenerRecord(listener, appId) {
                  @Override
                  public void onBinderDied() {
                      synchronized (mLock) {
                          if (mPrintJobStateChangeListenerRecords != null) {
                              mPrintJobStateChangeListenerRecords.remove(this);
                          }
                      }
                  }
              });
          }
      }
  
      public void removePrintJobStateChangeListener(@NonNull IPrintJobStateChangeListener listener) {
          synchronized (mLock) {
              throwIfDestroyedLocked();
              if (mPrintJobStateChangeListenerRecords == null) {
                  return;
              }
              final int recordCount = mPrintJobStateChangeListenerRecords.size();
              for (int i = 0; i < recordCount; i++) {
                  PrintJobStateChangeListenerRecord record =
                          mPrintJobStateChangeListenerRecords.get(i);
                  if (record.listener.asBinder().equals(listener.asBinder())) {
                      record.destroy();
                      mPrintJobStateChangeListenerRecords.remove(i);
                      break;
                  }
              }
              if (mPrintJobStateChangeListenerRecords.isEmpty()) {
                  mPrintJobStateChangeListenerRecords = null;
              }
          }
      }
  
      public void addPrintServicesChangeListener(@NonNull IPrintServicesChangeListener listener)
              throws RemoteException {
          synchronized (mLock) {
              throwIfDestroyedLocked();
              if (mPrintServicesChangeListenerRecords == null) {
                  mPrintServicesChangeListenerRecords = new ArrayList<>();
              }
              mPrintServicesChangeListenerRecords.add(
                      new ListenerRecord<IPrintServicesChangeListener>(listener) {
                          @Override
                          public void onBinderDied() {
                              synchronized (mLock) {
                                  if (mPrintServicesChangeListenerRecords != null) {
                                      mPrintServicesChangeListenerRecords.remove(this);
                                  }
                              }
                          }
                      });
          }
      }
  
      public void removePrintServicesChangeListener(@NonNull IPrintServicesChangeListener listener) {
          synchronized (mLock) {
              throwIfDestroyedLocked();
              if (mPrintServicesChangeListenerRecords == null) {
                  return;
              }
              final int recordCount = mPrintServicesChangeListenerRecords.size();
              for (int i = 0; i < recordCount; i++) {
                  ListenerRecord<IPrintServicesChangeListener> record =
                          mPrintServicesChangeListenerRecords.get(i);
                  if (record.listener.asBinder().equals(listener.asBinder())) {
                      record.destroy();
                      mPrintServicesChangeListenerRecords.remove(i);
                      break;
                  }
              }
              if (mPrintServicesChangeListenerRecords.isEmpty()) {
                  mPrintServicesChangeListenerRecords = null;
              }
          }
      }
  
      public void addPrintServiceRecommendationsChangeListener(
              @NonNull IRecommendationsChangeListener listener) throws RemoteException {
          synchronized (mLock) {
              throwIfDestroyedLocked();
              if (mPrintServiceRecommendationsChangeListenerRecords == null) {
                  mPrintServiceRecommendationsChangeListenerRecords = new ArrayList<>();
  
                  mPrintServiceRecommendationsService =
                          new RemotePrintServiceRecommendationService(mContext,
                                  UserHandle.getUserHandleForUid(mUserId), this);
              }
              mPrintServiceRecommendationsChangeListenerRecords.add(
                      new ListenerRecord<IRecommendationsChangeListener>(listener) {
                          @Override
                          public void onBinderDied() {
                              synchronized (mLock) {
                                  if (mPrintServiceRecommendationsChangeListenerRecords != null) {
                                      mPrintServiceRecommendationsChangeListenerRecords.remove(this);
                                  }
                              }
                          }
                      });
          }
      }
  
      public void removePrintServiceRecommendationsChangeListener(
              @NonNull IRecommendationsChangeListener listener) {
          synchronized (mLock) {
              throwIfDestroyedLocked();
              if (mPrintServiceRecommendationsChangeListenerRecords == null) {
                  return;
              }
              final int recordCount = mPrintServiceRecommendationsChangeListenerRecords.size();
              for (int i = 0; i < recordCount; i++) {
                  ListenerRecord<IRecommendationsChangeListener> record =
                          mPrintServiceRecommendationsChangeListenerRecords.get(i);
                  if (record.listener.asBinder().equals(listener.asBinder())) {
                      record.destroy();
                      mPrintServiceRecommendationsChangeListenerRecords.remove(i);
                      break;
                  }
              }
              if (mPrintServiceRecommendationsChangeListenerRecords.isEmpty()) {
                  mPrintServiceRecommendationsChangeListenerRecords = null;
  
                  mPrintServiceRecommendations = null;
  
                  mPrintServiceRecommendationsService.close();
                  mPrintServiceRecommendationsService = null;
              }
          }
      }
  
      @Override
      public void onPrintJobStateChanged(PrintJobInfo printJob) {
          mPrintJobForAppCache.onPrintJobStateChanged(printJob);
          Handler.getMain().sendMessage(obtainMessage(
                  UserState::handleDispatchPrintJobStateChanged,
                  this, printJob.getId(),
                  PooledLambda.obtainSupplier(printJob.getAppId()).recycleOnUse()));
      }
  
      public void onPrintServicesChanged() {
          Handler.getMain().sendMessage(obtainMessage(
                  UserState::handleDispatchPrintServicesChanged, this));
      }
  
      @Override
      public void onPrintServiceRecommendationsUpdated(List<RecommendationInfo> recommendations) {
          Handler.getMain().sendMessage(obtainMessage(
                  UserState::handleDispatchPrintServiceRecommendationsUpdated,
                  this, recommendations));
      }
  
      @Override
      public void onPrintersAdded(List<PrinterInfo> printers) {
          synchronized (mLock) {
              throwIfDestroyedLocked();
              // No services - nothing to do.
              if (mActiveServices.isEmpty()) {
                  return;
              }
              // No session - nothing to do.
              if (mPrinterDiscoverySession == null) {
                  return;
              }
              mPrinterDiscoverySession.onPrintersAddedLocked(printers);
          }
      }
  
      @Override
      public void onPrintersRemoved(List<PrinterId> printerIds) {
          synchronized (mLock) {
              throwIfDestroyedLocked();
              // No services - nothing to do.
              if (mActiveServices.isEmpty()) {
                  return;
              }
              // No session - nothing to do.
              if (mPrinterDiscoverySession == null) {
                  return;
              }
              mPrinterDiscoverySession.onPrintersRemovedLocked(printerIds);
          }
      }
  
      @Override
      public void onCustomPrinterIconLoaded(PrinterId printerId, Icon icon) {
          mSpooler.onCustomPrinterIconLoaded(printerId, icon);
  
          synchronized (mLock) {
              throwIfDestroyedLocked();
  
              // No session - nothing to do.
              if (mPrinterDiscoverySession == null) {
                  return;
              }
              mPrinterDiscoverySession.onCustomPrinterIconLoadedLocked(printerId);
          }
      }
  
      @Override
      public void onServiceDied(RemotePrintService service) {
          synchronized (mLock) {
              throwIfDestroyedLocked();
              // No services - nothing to do.
              if (mActiveServices.isEmpty()) {
                  return;
              }
              // Fail all print jobs.
              failActivePrintJobsForService(service.getComponentName());
              service.onAllPrintJobsHandled();
  
              mActiveServices.remove(service.getComponentName());
  
              // The service might need to be restarted if it died because of an update
              Handler.getMain().sendMessageDelayed(obtainMessage(
                      UserState::onConfigurationChanged, this),
                      SERVICE_RESTART_DELAY_MILLIS);
  
              // No session - nothing to do.
              if (mPrinterDiscoverySession == null) {
                  return;
              }
              mPrinterDiscoverySession.onServiceDiedLocked(service);
          }
      }
  
      public void updateIfNeededLocked() {
          throwIfDestroyedLocked();
          readConfigurationLocked();
          onConfigurationChangedLocked();
      }
  
      public void destroyLocked() {
          throwIfDestroyedLocked();
          mSpooler.destroy();
          for (RemotePrintService service : mActiveServices.values()) {
              service.destroy();
          }
          mActiveServices.clear();
          mInstalledServices.clear();
          mDisabledServices.clear();
          if (mPrinterDiscoverySession != null) {
              mPrinterDiscoverySession.destroyLocked();
              mPrinterDiscoverySession = null;
          }
          mDestroyed = true;
      }
  
      public void dump(@NonNull DualDumpOutputStream dumpStream) {
          synchronized (mLock) {
              dumpStream.write("user_id", PrintUserStateProto.USER_ID, mUserId);
  
              final int installedServiceCount = mInstalledServices.size();
              for (int i = 0; i < installedServiceCount; i++) {
                  long token = dumpStream.start("installed_services",
                          PrintUserStateProto.INSTALLED_SERVICES);
                  PrintServiceInfo installedService = mInstalledServices.get(i);
  
                  ResolveInfo resolveInfo = installedService.getResolveInfo();
                  writeComponentName(dumpStream, "component_name",
                          InstalledPrintServiceProto.COMPONENT_NAME,
                          new ComponentName(resolveInfo.serviceInfo.packageName,
                                  resolveInfo.serviceInfo.name));
  
                  writeStringIfNotNull(dumpStream, "settings_activity",
                          InstalledPrintServiceProto.SETTINGS_ACTIVITY,
                          installedService.getSettingsActivityName());
                  writeStringIfNotNull(dumpStream, "add_printers_activity",
                          InstalledPrintServiceProto.ADD_PRINTERS_ACTIVITY,
                          installedService.getAddPrintersActivityName());
                  writeStringIfNotNull(dumpStream, "advanced_options_activity",
                          InstalledPrintServiceProto.ADVANCED_OPTIONS_ACTIVITY,
                          installedService.getAdvancedOptionsActivityName());
  
                  dumpStream.end(token);
              }
  
              for (ComponentName disabledService : mDisabledServices) {
                  writeComponentName(dumpStream, "disabled_services",
                          PrintUserStateProto.DISABLED_SERVICES, disabledService);
              }
  
              final int activeServiceCount = mActiveServices.size();
              for (int i = 0; i < activeServiceCount; i++) {
                  long token = dumpStream.start("actives_services",
                          PrintUserStateProto.ACTIVE_SERVICES);
                  mActiveServices.valueAt(i).dump(dumpStream);
                  dumpStream.end(token);
              }
  
              mPrintJobForAppCache.dumpLocked(dumpStream);
  
              if (mPrinterDiscoverySession != null) {
                  long token = dumpStream.start("discovery_service",
                          PrintUserStateProto.DISCOVERY_SESSIONS);
                  mPrinterDiscoverySession.dumpLocked(dumpStream);
                  dumpStream.end(token);
              }
  
          }
  
          long token = dumpStream.start("print_spooler_state",
                  PrintUserStateProto.PRINT_SPOOLER_STATE);
          mSpooler.dump(dumpStream);
          dumpStream.end(token);
      }
  
      private void readConfigurationLocked() {
          readInstalledPrintServicesLocked();
          readDisabledPrintServicesLocked();
      }
  
      private void readInstalledPrintServicesLocked() {
          Set<PrintServiceInfo> tempPrintServices = new HashSet<PrintServiceInfo>();
  
          int queryIntentFlags = GET_SERVICES | GET_META_DATA | MATCH_DEBUG_TRIAGED_MISSING;
  
          if (mIsInstantServiceAllowed) {
              queryIntentFlags |= MATCH_INSTANT;
          }
  
          List<ResolveInfo> installedServices = mContext.getPackageManager()
                  .queryIntentServicesAsUser(mQueryIntent, queryIntentFlags, mUserId);
  
          final int installedCount = installedServices.size();
          for (int i = 0, count = installedCount; i < count; i++) {
              ResolveInfo installedService = installedServices.get(i);
              if (!android.Manifest.permission.BIND_PRINT_SERVICE.equals(
                      installedService.serviceInfo.permission)) {
                  ComponentName serviceName = new ComponentName(
                          installedService.serviceInfo.packageName,
                          installedService.serviceInfo.name);
                  Slog.w(LOG_TAG, "Skipping print service "
                          + serviceName.flattenToShortString()
                          + " since it does not require permission "
                          + android.Manifest.permission.BIND_PRINT_SERVICE);
                  continue;
              }
              tempPrintServices.add(PrintServiceInfo.create(mContext, installedService));
          }
  
          mInstalledServices.clear();
          mInstalledServices.addAll(tempPrintServices);
      }
  
      /**
       * Update persistent state from a previous version of Android.
       */
      private void upgradePersistentStateIfNeeded() {
          String enabledSettingValue = Settings.Secure.getStringForUser(mContext.getContentResolver(),
                  Settings.Secure.ENABLED_PRINT_SERVICES, mUserId);
  
          // Pre N we store the enabled services, in N and later we store the disabled services.
          // Hence if enabledSettingValue is still set, we need to upgrade.
          if (enabledSettingValue != null) {
              Set<ComponentName> enabledServiceNameSet = new HashSet<ComponentName>();
              readPrintServicesFromSettingLocked(Settings.Secure.ENABLED_PRINT_SERVICES,
                      enabledServiceNameSet);
  
              ArraySet<ComponentName> disabledServices = new ArraySet<>();
              final int numInstalledServices = mInstalledServices.size();
              for (int i = 0; i < numInstalledServices; i++) {
                  ComponentName serviceName = mInstalledServices.get(i).getComponentName();
                  if (!enabledServiceNameSet.contains(serviceName)) {
                      disabledServices.add(serviceName);
                  }
              }
  
              writeDisabledPrintServicesLocked(disabledServices);
  
              // We won't needed ENABLED_PRINT_SERVICES anymore, set to null to prevent upgrade to run
              // again.
              Settings.Secure.putStringForUser(mContext.getContentResolver(),
                      Settings.Secure.ENABLED_PRINT_SERVICES, null, mUserId);
          }
      }
  
      /**
       * Read the set of disabled print services from the secure settings.
       *
       * @return true if the state changed.
       */
      private void readDisabledPrintServicesLocked() {
          Set<ComponentName> tempDisabledServiceNameSet = new HashSet<ComponentName>();
          readPrintServicesFromSettingLocked(Settings.Secure.DISABLED_PRINT_SERVICES,
                  tempDisabledServiceNameSet);
          if (!tempDisabledServiceNameSet.equals(mDisabledServices)) {
              mDisabledServices.clear();
              mDisabledServices.addAll(tempDisabledServiceNameSet);
          }
      }
  
      private void readPrintServicesFromSettingLocked(String setting,
              Set<ComponentName> outServiceNames) {
          String settingValue = Settings.Secure.getStringForUser(mContext.getContentResolver(),
                  setting, mUserId);
          if (!TextUtils.isEmpty(settingValue)) {
              TextUtils.SimpleStringSplitter splitter = mStringColonSplitter;
              splitter.setString(settingValue);
              while (splitter.hasNext()) {
                  String string = splitter.next();
                  if (TextUtils.isEmpty(string)) {
                      continue;
                  }
                  ComponentName componentName = ComponentName.unflattenFromString(string);
                  if (componentName != null) {
                      outServiceNames.add(componentName);
                  }
              }
          }
      }
  
      /**
       * Persist the disabled print services to the secure settings.
       */
      private void writeDisabledPrintServicesLocked(Set<ComponentName> disabledServices) {
          StringBuilder builder = new StringBuilder();
          for (ComponentName componentName : disabledServices) {
              if (builder.length() > 0) {
                  builder.append(COMPONENT_NAME_SEPARATOR);
              }
              builder.append(componentName.flattenToShortString());
          }
          Settings.Secure.putStringForUser(mContext.getContentResolver(),
                  Settings.Secure.DISABLED_PRINT_SERVICES, builder.toString(), mUserId);
      }
  
      /**
       * Get the {@link ComponentName names} of the installed print services
       *
       * @return The names of the installed print services
       */
      private ArrayList<ComponentName> getInstalledComponents() {
          ArrayList<ComponentName> installedComponents = new ArrayList<ComponentName>();
  
          final int installedCount = mInstalledServices.size();
          for (int i = 0; i < installedCount; i++) {
              ResolveInfo resolveInfo = mInstalledServices.get(i).getResolveInfo();
              ComponentName serviceName = new ComponentName(resolveInfo.serviceInfo.packageName,
                      resolveInfo.serviceInfo.name);
  
              installedComponents.add(serviceName);
          }
  
          return installedComponents;
      }
  
      /**
       * Prune persistent state if a print service was uninstalled
       */
      public void prunePrintServices() {
          ArrayList<ComponentName> installedComponents;
  
          synchronized (mLock) {
              installedComponents = getInstalledComponents();
  
              // Remove unnecessary entries from persistent state "disabled services"
              boolean disabledServicesUninstalled = mDisabledServices.retainAll(installedComponents);
              if (disabledServicesUninstalled) {
                  writeDisabledPrintServicesLocked(mDisabledServices);
              }
          }
  
          // Remove unnecessary entries from persistent state "approved services"
          mSpooler.pruneApprovedPrintServices(installedComponents);
  
      }
  
      private void onConfigurationChangedLocked() {
          ArrayList<ComponentName> installedComponents = getInstalledComponents();
  
          final int installedCount = installedComponents.size();
          for (int i = 0; i < installedCount; i++) {
              ComponentName serviceName = installedComponents.get(i);
  
              if (!mDisabledServices.contains(serviceName)) {
                  if (!mActiveServices.containsKey(serviceName)) {
                      RemotePrintService service = new RemotePrintService(
                              mContext, serviceName, mUserId, mSpooler, this);
                      addServiceLocked(service);
                  }
              } else {
                  RemotePrintService service = mActiveServices.remove(serviceName);
                  if (service != null) {
                      removeServiceLocked(service);
                  }
              }
          }
  
          Iterator<Map.Entry<ComponentName, RemotePrintService>> iterator =
                  mActiveServices.entrySet().iterator();
          while (iterator.hasNext()) {
              Map.Entry<ComponentName, RemotePrintService> entry = iterator.next();
              ComponentName serviceName = entry.getKey();
              RemotePrintService service = entry.getValue();
              if (!installedComponents.contains(serviceName)) {
                  removeServiceLocked(service);
                  iterator.remove();
              }
          }
  
          onPrintServicesChanged();
      }
  
      private void addServiceLocked(RemotePrintService service) {
          mActiveServices.put(service.getComponentName(), service);
          if (mPrinterDiscoverySession != null) {
              mPrinterDiscoverySession.onServiceAddedLocked(service);
          }
      }
  
      private void removeServiceLocked(RemotePrintService service) {
          // Fail all print jobs.
          failActivePrintJobsForService(service.getComponentName());
          // If discovery is in progress, tear down the service.
          if (mPrinterDiscoverySession != null) {
              mPrinterDiscoverySession.onServiceRemovedLocked(service);
          } else {
              // Otherwise, just destroy it.
              service.destroy();
          }
      }
  
      private void failActivePrintJobsForService(final ComponentName serviceName) {
          // Makes sure all active print jobs are failed since the service
          // just died. Do this off the main thread since we do to allow
          // calls into the spooler on the main thread.
          if (Looper.getMainLooper().isCurrentThread()) {
              BackgroundThread.getHandler().sendMessage(obtainMessage(
                      UserState::failScheduledPrintJobsForServiceInternal, this, serviceName));
          } else {
              failScheduledPrintJobsForServiceInternal(serviceName);
          }
      }
  
      private void failScheduledPrintJobsForServiceInternal(ComponentName serviceName) {
          List<PrintJobInfo> printJobs = mSpooler.getPrintJobInfos(serviceName,
                  PrintJobInfo.STATE_ANY_SCHEDULED, PrintManager.APP_ID_ANY);
          if (printJobs == null) {
              return;
          }
          final long identity = Binder.clearCallingIdentity();
          try {
              final int printJobCount = printJobs.size();
              for (int i = 0; i < printJobCount; i++) {
                  PrintJobInfo printJob = printJobs.get(i);
                  mSpooler.setPrintJobState(printJob.getId(), PrintJobInfo.STATE_FAILED,
                          mContext.getString(R.string.reason_service_unavailable));
              }
          } finally {
              Binder.restoreCallingIdentity(identity);
          }
      }
  
      private void throwIfDestroyedLocked() {
          if (mDestroyed) {
              throw new IllegalStateException("Cannot interact with a destroyed instance.");
          }
      }
  
      private void handleDispatchPrintJobStateChanged(
              PrintJobId printJobId, IntSupplier appIdSupplier) {
          int appId = appIdSupplier.getAsInt();
          final List<PrintJobStateChangeListenerRecord> records;
          synchronized (mLock) {
              if (mPrintJobStateChangeListenerRecords == null) {
                  return;
              }
              records = new ArrayList<>(mPrintJobStateChangeListenerRecords);
          }
          final int recordCount = records.size();
          for (int i = 0; i < recordCount; i++) {
              PrintJobStateChangeListenerRecord record = records.get(i);
              if (record.appId == PrintManager.APP_ID_ANY
                      || record.appId == appId) {
                  try {
                      record.listener.onPrintJobStateChanged(printJobId);
                  } catch (RemoteException re) {
                      Log.e(LOG_TAG, "Error notifying for print job state change", re);
                  }
              }
          }
      }
  
      private void handleDispatchPrintServicesChanged() {
          final List<ListenerRecord<IPrintServicesChangeListener>> records;
          synchronized (mLock) {
              if (mPrintServicesChangeListenerRecords == null) {
                  return;
              }
              records = new ArrayList<>(mPrintServicesChangeListenerRecords);
          }
          final int recordCount = records.size();
          for (int i = 0; i < recordCount; i++) {
              ListenerRecord<IPrintServicesChangeListener> record = records.get(i);
  
              try {
                  record.listener.onPrintServicesChanged();;
              } catch (RemoteException re) {
                  Log.e(LOG_TAG, "Error notifying for print services change", re);
              }
          }
      }
  
      private void handleDispatchPrintServiceRecommendationsUpdated(
              @Nullable List<RecommendationInfo> recommendations) {
          final List<ListenerRecord<IRecommendationsChangeListener>> records;
          synchronized (mLock) {
              if (mPrintServiceRecommendationsChangeListenerRecords == null) {
                  return;
              }
              records = new ArrayList<>(mPrintServiceRecommendationsChangeListenerRecords);
  
              mPrintServiceRecommendations = recommendations;
          }
          final int recordCount = records.size();
          for (int i = 0; i < recordCount; i++) {
              ListenerRecord<IRecommendationsChangeListener> record = records.get(i);
  
              try {
                  record.listener.onRecommendationsChanged();
              } catch (RemoteException re) {
                  Log.e(LOG_TAG, "Error notifying for print service recommendations change", re);
              }
          }
      }
  
      private void onConfigurationChanged() {
          synchronized (mLock) {
              onConfigurationChangedLocked();
          }
      }
  
      public boolean getBindInstantServiceAllowed() {
          return mIsInstantServiceAllowed;
      }
  
      public void setBindInstantServiceAllowed(boolean allowed) {
          synchronized (mLock) {
              mIsInstantServiceAllowed = allowed;
  
              updateIfNeededLocked();
          }
      }
  
      private abstract class PrintJobStateChangeListenerRecord implements DeathRecipient {
          @NonNull final IPrintJobStateChangeListener listener;
          final int appId;
  
          public PrintJobStateChangeListenerRecord(@NonNull IPrintJobStateChangeListener listener,
                  int appId) throws RemoteException {
              this.listener = listener;
              this.appId = appId;
              listener.asBinder().linkToDeath(this, 0);
          }
  
          public void destroy() {
              listener.asBinder().unlinkToDeath(this, 0);
          }
  
          @Override
          public void binderDied() {
              listener.asBinder().unlinkToDeath(this, 0);
              onBinderDied();
          }
  
          public abstract void onBinderDied();
      }
  
      private abstract class ListenerRecord<T extends IInterface> implements DeathRecipient {
          @NonNull final T listener;
  
          public ListenerRecord(@NonNull T listener) throws RemoteException {
              this.listener = listener;
              listener.asBinder().linkToDeath(this, 0);
          }
  
          public void destroy() {
              listener.asBinder().unlinkToDeath(this, 0);
          }
  
          @Override
          public void binderDied() {
              listener.asBinder().unlinkToDeath(this, 0);
              onBinderDied();
          }
  
          public abstract void onBinderDied();
      }
  
      private class PrinterDiscoverySessionMediator {
          private final ArrayMap<PrinterId, PrinterInfo> mPrinters =
                  new ArrayMap<PrinterId, PrinterInfo>();
  
          private final RemoteCallbackList<IPrinterDiscoveryObserver> mDiscoveryObservers =
                  new RemoteCallbackList<IPrinterDiscoveryObserver>() {
              @Override
              public void onCallbackDied(IPrinterDiscoveryObserver observer) {
                  synchronized (mLock) {
                      stopPrinterDiscoveryLocked(observer);
                      removeObserverLocked(observer);
                  }
              }
          };
  
          private final List<IBinder> mStartedPrinterDiscoveryTokens = new ArrayList<IBinder>();
  
          private final List<PrinterId> mStateTrackedPrinters = new ArrayList<PrinterId>();
  
          private boolean mIsDestroyed;
  
          PrinterDiscoverySessionMediator() {
              // Kick off the session creation.
              Handler.getMain().sendMessage(obtainMessage(UserState.PrinterDiscoverySessionMediator::
                      handleDispatchCreatePrinterDiscoverySession,
                      this, new ArrayList<>(mActiveServices.values())));
          }
  
          public void addObserverLocked(@NonNull IPrinterDiscoveryObserver observer) {
              // Add the observer.
              mDiscoveryObservers.register(observer);
  
              // Bring the added observer up to speed with the printers.
              if (!mPrinters.isEmpty()) {
                  Handler.getMain().sendMessage(obtainMessage(
                          UserState.PrinterDiscoverySessionMediator::handlePrintersAdded,
                          this, observer, new ArrayList<>(mPrinters.values())));
              }
          }
  
          public void removeObserverLocked(@NonNull IPrinterDiscoveryObserver observer) {
              // Remove the observer.
              mDiscoveryObservers.unregister(observer);
              // No one else observing - then kill it.
              if (mDiscoveryObservers.getRegisteredCallbackCount() == 0) {
                  destroyLocked();
              }
          }
  
          public final void startPrinterDiscoveryLocked(@NonNull IPrinterDiscoveryObserver observer,
                  @Nullable List<PrinterId> priorityList) {
              if (mIsDestroyed) {
                  Log.w(LOG_TAG, "Not starting dicovery - session destroyed");
                  return;
              }
  
              final boolean discoveryStarted = !mStartedPrinterDiscoveryTokens.isEmpty();
  
              // Remember we got a start request to match with an end.
              mStartedPrinterDiscoveryTokens.add(observer.asBinder());
  
              // If printer discovery is ongoing and the start request has a list
              // of printer to be checked, then we just request validating them.
              if (discoveryStarted && priorityList != null && !priorityList.isEmpty()) {
                  validatePrinters(priorityList);
                  return;
              }
  
              // The service are already performing discovery - nothing to do.
              if (mStartedPrinterDiscoveryTokens.size() > 1) {
                  return;
              }
  
              Handler.getMain().sendMessage(obtainMessage(UserState.PrinterDiscoverySessionMediator::
                      handleDispatchStartPrinterDiscovery, this,
                      new ArrayList<>(mActiveServices.values()), priorityList));
          }
  
          public final void stopPrinterDiscoveryLocked(@NonNull IPrinterDiscoveryObserver observer) {
              if (mIsDestroyed) {
                  Log.w(LOG_TAG, "Not stopping dicovery - session destroyed");
                  return;
              }
              // This one did not make an active discovery request - nothing to do.
              if (!mStartedPrinterDiscoveryTokens.remove(observer.asBinder())) {
                  return;
              }
              // There are other interested observers - do not stop discovery.
              if (!mStartedPrinterDiscoveryTokens.isEmpty()) {
                  return;
              }
              Handler.getMain().sendMessage(obtainMessage(UserState.PrinterDiscoverySessionMediator::
                      handleDispatchStopPrinterDiscovery,
                      this, new ArrayList<>(mActiveServices.values())));
          }
  
          public void validatePrintersLocked(@NonNull List<PrinterId> printerIds) {
              if (mIsDestroyed) {
                  Log.w(LOG_TAG, "Not validating pritners - session destroyed");
                  return;
              }
  
              List<PrinterId> remainingList = new ArrayList<PrinterId>(printerIds);
              while (!remainingList.isEmpty()) {
                  Iterator<PrinterId> iterator = remainingList.iterator();
                  // Gather the printers per service and request a validation.
                  List<PrinterId> updateList = new ArrayList<PrinterId>();
                  ComponentName serviceName = null;
                  while (iterator.hasNext()) {
                      PrinterId printerId = iterator.next();
                      if (printerId != null) {
                          if (updateList.isEmpty()) {
                              updateList.add(printerId);
                              serviceName = printerId.getServiceName();
                              iterator.remove();
                          } else if (printerId.getServiceName().equals(serviceName)) {
                              updateList.add(printerId);
                              iterator.remove();
                          }
                      }
                  }
                  // Schedule a notification of the service.
                  RemotePrintService service = mActiveServices.get(serviceName);
                  if (service != null) {
                      Handler.getMain().sendMessage(obtainMessage(
                              UserState.PrinterDiscoverySessionMediator::handleValidatePrinters,
                              this, service, updateList));
                  }
              }
          }
  
          public final void startPrinterStateTrackingLocked(@NonNull PrinterId printerId) {
              if (mIsDestroyed) {
                  Log.w(LOG_TAG, "Not starting printer state tracking - session destroyed");
                  return;
              }
              // If printer discovery is not started - nothing to do.
              if (mStartedPrinterDiscoveryTokens.isEmpty()) {
                  return;
              }
              final boolean containedPrinterId = mStateTrackedPrinters.contains(printerId);
              // Keep track of the number of requests to track this one.
              mStateTrackedPrinters.add(printerId);
              // If we were tracking this printer - nothing to do.
              if (containedPrinterId) {
                  return;
              }
              // No service - nothing to do.
              RemotePrintService service = mActiveServices.get(printerId.getServiceName());
              if (service == null) {
                  return;
              }
              // Ask the service to start tracking.
              Handler.getMain().sendMessage(obtainMessage(UserState.PrinterDiscoverySessionMediator::
                      handleStartPrinterStateTracking, this, service, printerId));
          }
  
          public final void stopPrinterStateTrackingLocked(PrinterId printerId) {
              if (mIsDestroyed) {
                  Log.w(LOG_TAG, "Not stopping printer state tracking - session destroyed");
                  return;
              }
              // If printer discovery is not started - nothing to do.
              if (mStartedPrinterDiscoveryTokens.isEmpty()) {
                  return;
              }
              // If we did not track this printer - nothing to do.
              if (!mStateTrackedPrinters.remove(printerId)) {
                  return;
              }
              // No service - nothing to do.
              RemotePrintService service = mActiveServices.get(printerId.getServiceName());
              if (service == null) {
                  return;
              }
              // Ask the service to start tracking.
              Handler.getMain().sendMessage(obtainMessage(UserState.PrinterDiscoverySessionMediator::
                      handleStopPrinterStateTracking, this, service, printerId));
          }
  
          public void onDestroyed() {
              /* do nothing */
          }
  
          public void destroyLocked() {
              if (mIsDestroyed) {
                  Log.w(LOG_TAG, "Not destroying - session destroyed");
                  return;
              }
              mIsDestroyed = true;
              // Make sure printer tracking is stopped.
              final int printerCount = mStateTrackedPrinters.size();
              for (int i = 0; i < printerCount; i++) {
                  PrinterId printerId = mStateTrackedPrinters.get(i);
                  stopPrinterStateTracking(printerId);
              }
              // Make sure discovery is stopped.
              final int observerCount = mStartedPrinterDiscoveryTokens.size();
              for (int i = 0; i < observerCount; i++) {
                  IBinder token = mStartedPrinterDiscoveryTokens.get(i);
                  stopPrinterDiscoveryLocked(IPrinterDiscoveryObserver.Stub.asInterface(token));
              }
              // Tell the services we are done.
              Handler.getMain().sendMessage(obtainMessage(UserState.PrinterDiscoverySessionMediator::
                      handleDispatchDestroyPrinterDiscoverySession,
                      this, new ArrayList<>(mActiveServices.values())));
          }
  
          public void onPrintersAddedLocked(List<PrinterInfo> printers) {
              if (DEBUG) {
                  Log.i(LOG_TAG, "onPrintersAddedLocked()");
              }
              if (mIsDestroyed) {
                  Log.w(LOG_TAG, "Not adding printers - session destroyed");
                  return;
              }
              List<PrinterInfo> addedPrinters = null;
              final int addedPrinterCount = printers.size();
              for (int i = 0; i < addedPrinterCount; i++) {
                  PrinterInfo printer = printers.get(i);
                  PrinterInfo oldPrinter = mPrinters.put(printer.getId(), printer);
                  if (oldPrinter == null || !oldPrinter.equals(printer)) {
                      if (addedPrinters == null) {
                          addedPrinters = new ArrayList<PrinterInfo>();
                      }
                      addedPrinters.add(printer);
                  }
              }
              if (addedPrinters != null) {
                  Handler.getMain().sendMessage(obtainMessage(
                          UserState.PrinterDiscoverySessionMediator::handleDispatchPrintersAdded,
                          this, addedPrinters));
              }
          }
  
          public void onPrintersRemovedLocked(List<PrinterId> printerIds) {
              if (DEBUG) {
                  Log.i(LOG_TAG, "onPrintersRemovedLocked()");
              }
              if (mIsDestroyed) {
                  Log.w(LOG_TAG, "Not removing printers - session destroyed");
                  return;
              }
              List<PrinterId> removedPrinterIds = null;
              final int removedPrinterCount = printerIds.size();
              for (int i = 0; i < removedPrinterCount; i++) {
                  PrinterId removedPrinterId = printerIds.get(i);
                  if (mPrinters.remove(removedPrinterId) != null) {
                      if (removedPrinterIds == null) {
                          removedPrinterIds = new ArrayList<PrinterId>();
                      }
                      removedPrinterIds.add(removedPrinterId);
                  }
              }
              if (removedPrinterIds != null) {
                  Handler.getMain().sendMessage(obtainMessage(
                          UserState.PrinterDiscoverySessionMediator::handleDispatchPrintersRemoved,
                          this, removedPrinterIds));
              }
          }
  
          public void onServiceRemovedLocked(RemotePrintService service) {
              if (mIsDestroyed) {
                  Log.w(LOG_TAG, "Not updating removed service - session destroyed");
                  return;
              }
              // Remove the reported and tracked printers for that service.
              ComponentName serviceName = service.getComponentName();
              removePrintersForServiceLocked(serviceName);
              service.destroy();
          }
  
          /**
           * Handle that a custom icon for a printer was loaded.
           *
           * This increments the icon generation and adds the printer again which triggers an update
           * in all users of the currently known printers.
           *
           * @param printerId the id of the printer the icon belongs to
           * @see android.print.PrinterInfo.Builder#setHasCustomPrinterIcon
           */
          public void onCustomPrinterIconLoadedLocked(PrinterId printerId) {
              if (DEBUG) {
                  Log.i(LOG_TAG, "onCustomPrinterIconLoadedLocked()");
              }
              if (mIsDestroyed) {
                  Log.w(LOG_TAG, "Not updating printer - session destroyed");
                  return;
              }
  
              PrinterInfo printer = mPrinters.get(printerId);
              if (printer != null) {
                  PrinterInfo newPrinter = (new PrinterInfo.Builder(printer))
                          .incCustomPrinterIconGen().build();
                  mPrinters.put(printerId, newPrinter);
  
                  ArrayList<PrinterInfo> addedPrinters = new ArrayList<>(1);
                  addedPrinters.add(newPrinter);
                  Handler.getMain().sendMessage(obtainMessage(
                          UserState.PrinterDiscoverySessionMediator::handleDispatchPrintersAdded,
                          this, addedPrinters));
              }
          }
  
          public void onServiceDiedLocked(RemotePrintService service) {
              removeServiceLocked(service);
          }
  
          public void onServiceAddedLocked(RemotePrintService service) {
              if (mIsDestroyed) {
                  Log.w(LOG_TAG, "Not updating added service - session destroyed");
                  return;
              }
              // Tell the service to create a session.
              Handler.getMain().sendMessage(obtainMessage(
                      RemotePrintService::createPrinterDiscoverySession, service));
              // Start printer discovery if necessary.
              if (!mStartedPrinterDiscoveryTokens.isEmpty()) {
                  Handler.getMain().sendMessage(obtainMessage(
                          RemotePrintService::startPrinterDiscovery, service, null));
              }
              // Start tracking printers if necessary
              final int trackedPrinterCount = mStateTrackedPrinters.size();
              for (int i = 0; i < trackedPrinterCount; i++) {
                  PrinterId printerId = mStateTrackedPrinters.get(i);
                  if (printerId.getServiceName().equals(service.getComponentName())) {
                      Handler.getMain().sendMessage(obtainMessage(
                              RemotePrintService::startPrinterStateTracking, service, printerId));
                  }
              }
          }
  
          public void dumpLocked(@NonNull DualDumpOutputStream dumpStream) {
              dumpStream.write("is_destroyed", PrinterDiscoverySessionProto.IS_DESTROYED, mDestroyed);
              dumpStream.write("is_printer_discovery_in_progress",
                      PrinterDiscoverySessionProto.IS_PRINTER_DISCOVERY_IN_PROGRESS,
                      !mStartedPrinterDiscoveryTokens.isEmpty());
  
              final int observerCount = mDiscoveryObservers.beginBroadcast();
              for (int i = 0; i < observerCount; i++) {
                  IPrinterDiscoveryObserver observer = mDiscoveryObservers.getBroadcastItem(i);
                  dumpStream.write("printer_discovery_observers",
                          PrinterDiscoverySessionProto.PRINTER_DISCOVERY_OBSERVERS,
                          observer.toString());
              }
              mDiscoveryObservers.finishBroadcast();
  
              final int tokenCount = this.mStartedPrinterDiscoveryTokens.size();
              for (int i = 0; i < tokenCount; i++) {
                  IBinder token = mStartedPrinterDiscoveryTokens.get(i);
                  dumpStream.write("discovery_requests",
                          PrinterDiscoverySessionProto.DISCOVERY_REQUESTS, token.toString());
              }
  
              final int trackedPrinters = mStateTrackedPrinters.size();
              for (int i = 0; i < trackedPrinters; i++) {
                  PrinterId printer = mStateTrackedPrinters.get(i);
                  writePrinterId(dumpStream, "tracked_printer_requests",
                          PrinterDiscoverySessionProto.TRACKED_PRINTER_REQUESTS, printer);
              }
  
              final int printerCount = mPrinters.size();
              for (int i = 0; i < printerCount; i++) {
                  PrinterInfo printer = mPrinters.valueAt(i);
                  writePrinterInfo(mContext, dumpStream, "printer",
                          PrinterDiscoverySessionProto.PRINTER, printer);
              }
          }
  
          private void removePrintersForServiceLocked(ComponentName serviceName) {
              // No printers - nothing to do.
              if (mPrinters.isEmpty()) {
                  return;
              }
              // Remove the printers for that service.
              List<PrinterId> removedPrinterIds = null;
              final int printerCount = mPrinters.size();
              for (int i = 0; i < printerCount; i++) {
                  PrinterId printerId = mPrinters.keyAt(i);
                  if (printerId.getServiceName().equals(serviceName)) {
                      if (removedPrinterIds == null) {
                          removedPrinterIds = new ArrayList<PrinterId>();
                      }
                      removedPrinterIds.add(printerId);
                  }
              }
              if (removedPrinterIds != null) {
                  final int removedPrinterCount = removedPrinterIds.size();
                  for (int i = 0; i < removedPrinterCount; i++) {
                      mPrinters.remove(removedPrinterIds.get(i));
                  }
                  Handler.getMain().sendMessage(obtainMessage(
                          UserState.PrinterDiscoverySessionMediator::handleDispatchPrintersRemoved,
                          this, removedPrinterIds));
              }
          }
  
          private void handleDispatchPrintersAdded(List<PrinterInfo> addedPrinters) {
              final int observerCount = mDiscoveryObservers.beginBroadcast();
              for (int i = 0; i < observerCount; i++) {
                  IPrinterDiscoveryObserver observer = mDiscoveryObservers.getBroadcastItem(i);
                  handlePrintersAdded(observer, addedPrinters);
              }
              mDiscoveryObservers.finishBroadcast();
          }
  
          private void handleDispatchPrintersRemoved(List<PrinterId> removedPrinterIds) {
              final int observerCount = mDiscoveryObservers.beginBroadcast();
              for (int i = 0; i < observerCount; i++) {
                  IPrinterDiscoveryObserver observer = mDiscoveryObservers.getBroadcastItem(i);
                  handlePrintersRemoved(observer, removedPrinterIds);
              }
              mDiscoveryObservers.finishBroadcast();
          }
  
          private void handleDispatchCreatePrinterDiscoverySession(
                  List<RemotePrintService> services) {
              final int serviceCount = services.size();
              for (int i = 0; i < serviceCount; i++) {
                  RemotePrintService service = services.get(i);
                  service.createPrinterDiscoverySession();
              }
          }
  
          private void handleDispatchDestroyPrinterDiscoverySession(
                  List<RemotePrintService> services) {
              final int serviceCount = services.size();
              for (int i = 0; i < serviceCount; i++) {
                  RemotePrintService service = services.get(i);
                  service.destroyPrinterDiscoverySession();
              }
              onDestroyed();
          }
  
          private void handleDispatchStartPrinterDiscovery(
                  List<RemotePrintService> services, List<PrinterId> printerIds) {
              final int serviceCount = services.size();
              for (int i = 0; i < serviceCount; i++) {
                  RemotePrintService service = services.get(i);
                  service.startPrinterDiscovery(printerIds);
              }
          }
  
          private void handleDispatchStopPrinterDiscovery(List<RemotePrintService> services) {
              final int serviceCount = services.size();
              for (int i = 0; i < serviceCount; i++) {
                  RemotePrintService service = services.get(i);
                  service.stopPrinterDiscovery();
              }
          }
  
          private void handleValidatePrinters(RemotePrintService service,
                  List<PrinterId> printerIds) {
              service.validatePrinters(printerIds);
          }
  
          private void handleStartPrinterStateTracking(@NonNull RemotePrintService service,
                  @NonNull PrinterId printerId) {
              service.startPrinterStateTracking(printerId);
          }
  
          private void handleStopPrinterStateTracking(RemotePrintService service,
                  PrinterId printerId) {
              service.stopPrinterStateTracking(printerId);
          }
  
          private void handlePrintersAdded(IPrinterDiscoveryObserver observer,
              List<PrinterInfo> printers) {
              try {
                  observer.onPrintersAdded(new ParceledListSlice<PrinterInfo>(printers));
              } catch (RemoteException re) {
                  Log.e(LOG_TAG, "Error sending added printers", re);
              }
          }
  
          private void handlePrintersRemoved(IPrinterDiscoveryObserver observer,
              List<PrinterId> printerIds) {
              try {
                  observer.onPrintersRemoved(new ParceledListSlice<PrinterId>(printerIds));
              } catch (RemoteException re) {
                  Log.e(LOG_TAG, "Error sending removed printers", re);
              }
          }
      }
  
      private final class PrintJobForAppCache {
          private final SparseArray<List<PrintJobInfo>> mPrintJobsForRunningApp =
                  new SparseArray<List<PrintJobInfo>>();
  
          public boolean onPrintJobCreated(final IBinder creator, final int appId,
                  PrintJobInfo printJob) {
              try {
                  creator.linkToDeath(new DeathRecipient() {
                      @Override
                      public void binderDied() {
                          creator.unlinkToDeath(this, 0);
                          synchronized (mLock) {
                              mPrintJobsForRunningApp.remove(appId);
                          }
                      }
                  }, 0);
              } catch (RemoteException re) {
                  /* The process is already dead - we just failed. */
                  return false;
              }
              synchronized (mLock) {
                  List<PrintJobInfo> printJobsForApp = mPrintJobsForRunningApp.get(appId);
                  if (printJobsForApp == null) {
                      printJobsForApp = new ArrayList<PrintJobInfo>();
                      mPrintJobsForRunningApp.put(appId, printJobsForApp);
                  }
                  printJobsForApp.add(printJob);
              }
              return true;
          }
  
          public void onPrintJobStateChanged(PrintJobInfo printJob) {
              synchronized (mLock) {
                  List<PrintJobInfo> printJobsForApp = mPrintJobsForRunningApp.get(
                          printJob.getAppId());
                  if (printJobsForApp == null) {
                      return;
                  }
                  final int printJobCount = printJobsForApp.size();
                  for (int i = 0; i < printJobCount; i++) {
                      PrintJobInfo oldPrintJob = printJobsForApp.get(i);
                      if (oldPrintJob.getId().equals(printJob.getId())) {
                          printJobsForApp.set(i, printJob);
                      }
                  }
              }
          }
  
          public PrintJobInfo getPrintJob(PrintJobId printJobId, int appId) {
              synchronized (mLock) {
                  List<PrintJobInfo> printJobsForApp = mPrintJobsForRunningApp.get(appId);
                  if (printJobsForApp == null) {
                      return null;
                  }
                  final int printJobCount = printJobsForApp.size();
                  for (int i = 0; i < printJobCount; i++) {
                      PrintJobInfo printJob = printJobsForApp.get(i);
                      if (printJob.getId().equals(printJobId)) {
                          return printJob;
                      }
                  }
              }
              return null;
          }
  
          public List<PrintJobInfo> getPrintJobs(int appId) {
              synchronized (mLock) {
                  List<PrintJobInfo> printJobs = null;
                  if (appId == PrintManager.APP_ID_ANY) {
                      final int bucketCount = mPrintJobsForRunningApp.size();
                      for (int i = 0; i < bucketCount; i++) {
                          List<PrintJobInfo> bucket = mPrintJobsForRunningApp.valueAt(i);
                          if (printJobs == null) {
                              printJobs = new ArrayList<PrintJobInfo>();
                          }
                          printJobs.addAll(bucket);
                      }
                  } else {
                      List<PrintJobInfo> bucket = mPrintJobsForRunningApp.get(appId);
                      if (bucket != null) {
                          if (printJobs == null) {
                              printJobs = new ArrayList<PrintJobInfo>();
                          }
                          printJobs.addAll(bucket);
                      }
                  }
                  if (printJobs != null) {
                      return printJobs;
                  }
                  return Collections.emptyList();
              }
          }
  
          public void dumpLocked(@NonNull DualDumpOutputStream dumpStream) {
              final int bucketCount = mPrintJobsForRunningApp.size();
              for (int i = 0; i < bucketCount; i++) {
                  final int appId = mPrintJobsForRunningApp.keyAt(i);
                  List<PrintJobInfo> bucket = mPrintJobsForRunningApp.valueAt(i);
                  final int printJobCount = bucket.size();
                  for (int j = 0; j < printJobCount; j++) {
                      long token = dumpStream.start("cached_print_jobs",
                              PrintUserStateProto.CACHED_PRINT_JOBS);
  
                      dumpStream.write("app_id", CachedPrintJobProto.APP_ID, appId);
  
                      writePrintJobInfo(mContext, dumpStream, "print_job",
                              CachedPrintJobProto.PRINT_JOB, bucket.get(j));
  
                      dumpStream.end(token);
                  }
              }
          }
      }
  }
  