/* * 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.annotation.UserIdInt; import android.app.ActivityOptions; 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.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 mActiveServices = new ArrayMap(); private final List mInstalledServices = new ArrayList(); private final Set mDisabledServices = new ArraySet(); private final PrintJobForAppCache mPrintJobForAppCache = new PrintJobForAppCache(); private final Object mLock; private final Context mContext; private final @UserIdInt int mUserId; private final RemotePrintSpooler mSpooler; private PrinterDiscoverySessionMediator mPrinterDiscoverySession; private List mPrintJobStateChangeListenerRecords; private List> mPrintServicesChangeListenerRecords; private List> mPrintServiceRecommendationsChangeListenerRecords; private boolean mDestroyed; /** Currently known list of print service recommendations */ private List 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); ActivityOptions activityOptions = ActivityOptions.makeBasic() .setPendingIntentCreatorBackgroundActivityStartMode( ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_DENIED); IntentSender intentSender = PendingIntent.getActivityAsUser( mContext, 0, intent, PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE, activityOptions.toBundle(), 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 getPrintJobInfos(int appId) { List 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 result = new ArrayMap(); // 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 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(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 getPrintServices(int selectionFlags) { synchronized (mLock) { List 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(); } } } public boolean isPrintServiceEnabled(@NonNull ComponentName serviceName) { synchronized (mLock) { if (mDisabledServices.contains(serviceName)) { return false; } return true; } } /** * @return The currently known print service recommendations */ public @Nullable List 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 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 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(); } 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(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 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.of(mUserId), this); } mPrintServiceRecommendationsChangeListenerRecords.add( new ListenerRecord(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 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 recommendations) { Handler.getMain().sendMessage(obtainMessage( UserState::handleDispatchPrintServiceRecommendationsUpdated, this, recommendations)); } @Override public void onPrintersAdded(List 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 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 tempPrintServices = new HashSet(); int queryIntentFlags = GET_SERVICES | GET_META_DATA | MATCH_DEBUG_TRIAGED_MISSING; if (mIsInstantServiceAllowed) { queryIntentFlags |= MATCH_INSTANT; } List 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 enabledServiceNameSet = new HashSet(); readPrintServicesFromSettingLocked(Settings.Secure.ENABLED_PRINT_SERVICES, enabledServiceNameSet); ArraySet 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 tempDisabledServiceNameSet = new HashSet(); readPrintServicesFromSettingLocked(Settings.Secure.DISABLED_PRINT_SERVICES, tempDisabledServiceNameSet); if (!tempDisabledServiceNameSet.equals(mDisabledServices)) { mDisabledServices.clear(); mDisabledServices.addAll(tempDisabledServiceNameSet); } } private void readPrintServicesFromSettingLocked(String setting, Set 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 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 getInstalledComponents() { ArrayList installedComponents = new ArrayList(); 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 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 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> iterator = mActiveServices.entrySet().iterator(); while (iterator.hasNext()) { Map.Entry 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 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 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> records; synchronized (mLock) { if (mPrintServicesChangeListenerRecords == null) { return; } records = new ArrayList<>(mPrintServicesChangeListenerRecords); } final int recordCount = records.size(); for (int i = 0; i < recordCount; i++) { ListenerRecord 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 recommendations) { final List> 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 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 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 mPrinters = new ArrayMap(); private final RemoteCallbackList mDiscoveryObservers = new RemoteCallbackList() { @Override public void onCallbackDied(IPrinterDiscoveryObserver observer) { synchronized (mLock) { stopPrinterDiscoveryLocked(observer); removeObserverLocked(observer); } } }; private final List mStartedPrinterDiscoveryTokens = new ArrayList(); private final List mStateTrackedPrinters = new ArrayList(); 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 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 printerIds) { if (mIsDestroyed) { Log.w(LOG_TAG, "Not validating pritners - session destroyed"); return; } List remainingList = new ArrayList(printerIds); while (!remainingList.isEmpty()) { Iterator iterator = remainingList.iterator(); // Gather the printers per service and request a validation. List updateList = new ArrayList(); 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 printers) { if (DEBUG) { Log.i(LOG_TAG, "onPrintersAddedLocked()"); } if (mIsDestroyed) { Log.w(LOG_TAG, "Not adding printers - session destroyed"); return; } List 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(); } addedPrinters.add(printer); } } if (addedPrinters != null) { Handler.getMain().sendMessage(obtainMessage( UserState.PrinterDiscoverySessionMediator::handleDispatchPrintersAdded, this, addedPrinters)); } } public void onPrintersRemovedLocked(List printerIds) { if (DEBUG) { Log.i(LOG_TAG, "onPrintersRemovedLocked()"); } if (mIsDestroyed) { Log.w(LOG_TAG, "Not removing printers - session destroyed"); return; } List 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(); } 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 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 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(); } 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 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 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 services) { final int serviceCount = services.size(); for (int i = 0; i < serviceCount; i++) { RemotePrintService service = services.get(i); service.createPrinterDiscoverySession(); } } private void handleDispatchDestroyPrinterDiscoverySession( List 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 services, List 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 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 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 printers) { try { observer.onPrintersAdded(new ParceledListSlice(printers)); } catch (RemoteException re) { Log.e(LOG_TAG, "Error sending added printers", re); } } private void handlePrintersRemoved(IPrinterDiscoveryObserver observer, List printerIds) { try { observer.onPrintersRemoved(new ParceledListSlice(printerIds)); } catch (RemoteException re) { Log.e(LOG_TAG, "Error sending removed printers", re); } } } private final class PrintJobForAppCache { private final SparseArray> mPrintJobsForRunningApp = new SparseArray>(); 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 printJobsForApp = mPrintJobsForRunningApp.get(appId); if (printJobsForApp == null) { printJobsForApp = new ArrayList(); mPrintJobsForRunningApp.put(appId, printJobsForApp); } printJobsForApp.add(printJob); } return true; } public void onPrintJobStateChanged(PrintJobInfo printJob) { synchronized (mLock) { List 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 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 getPrintJobs(int appId) { synchronized (mLock) { List printJobs = null; if (appId == PrintManager.APP_ID_ANY) { final int bucketCount = mPrintJobsForRunningApp.size(); for (int i = 0; i < bucketCount; i++) { List bucket = mPrintJobsForRunningApp.valueAt(i); if (printJobs == null) { printJobs = new ArrayList(); } printJobs.addAll(bucket); } } else { List bucket = mPrintJobsForRunningApp.get(appId); if (bucket != null) { if (printJobs == null) { printJobs = new ArrayList(); } 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 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); } } } } }