/*
 * 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_SERVICES;
import static android.content.pm.PackageManager.MATCH_DEBUG_TRIAGED_MISSING;

import android.annotation.NonNull;
import android.app.ActivityManager;
import android.app.admin.DevicePolicyManagerInternal;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.content.pm.UserInfo;
import android.database.ContentObserver;
import android.graphics.drawable.Icon;
import android.net.Uri;
import android.os.Binder;
import android.os.Bundle;
import android.os.Looper;
import android.os.Process;
import android.os.RemoteException;
import android.os.UserHandle;
import android.os.UserManager;
import android.print.IPrintDocumentAdapter;
import android.print.IPrintJobStateChangeListener;
import android.print.IPrintManager;
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.printservice.PrintServiceInfo;
import android.printservice.recommendation.IRecommendationsChangeListener;
import android.printservice.recommendation.RecommendationInfo;
import android.provider.Settings;
import android.service.print.PrintServiceDumpProto;
import android.util.Log;
import android.util.SparseArray;
import android.util.proto.ProtoOutputStream;
import android.widget.Toast;

import com.android.internal.content.PackageMonitor;
import com.android.internal.os.BackgroundThread;
import com.android.internal.util.DumpUtils;
import com.android.internal.util.IndentingPrintWriter;
import com.android.internal.util.Preconditions;
import com.android.internal.util.dump.DualDumpOutputStream;
import com.android.server.LocalServices;
import com.android.server.SystemService;

import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

/**
 * SystemService wrapper for the PrintManager implementation. Publishes
 * Context.PRINT_SERVICE.
 * PrintManager implementation is contained within.
 */
public final class PrintManagerService extends SystemService {
    private static final String LOG_TAG = "PrintManagerService";

    private final PrintManagerImpl mPrintManagerImpl;

    public PrintManagerService(Context context) {
        super(context);
        mPrintManagerImpl = new PrintManagerImpl(context);
    }

    @Override
    public void onStart() {
        publishBinderService(Context.PRINT_SERVICE, mPrintManagerImpl);
    }

    @Override
    public void onUnlockUser(int userHandle) {
        mPrintManagerImpl.handleUserUnlocked(userHandle);
    }

    @Override
    public void onStopUser(int userHandle) {
        mPrintManagerImpl.handleUserStopped(userHandle);
    }

    class PrintManagerImpl extends IPrintManager.Stub {
        private static final int BACKGROUND_USER_ID = -10;

        private final Object mLock = new Object();

        private final Context mContext;

        private final UserManager mUserManager;

        private final SparseArray<UserState> mUserStates = new SparseArray<>();

        PrintManagerImpl(Context context) {
            mContext = context;
            mUserManager = (UserManager) context.getSystemService(Context.USER_SERVICE);
            registerContentObservers();
            registerBroadcastReceivers();
        }

        @Override
        public Bundle print(String printJobName, IPrintDocumentAdapter adapter,
                PrintAttributes attributes, String packageName, int appId, int userId) {
            adapter = Preconditions.checkNotNull(adapter);
            if (!isPrintingEnabled()) {
                CharSequence disabledMessage = null;
                DevicePolicyManagerInternal dpmi =
                        LocalServices.getService(DevicePolicyManagerInternal.class);
                final int callingUserId = UserHandle.getCallingUserId();
                final long identity = Binder.clearCallingIdentity();
                try {
                    disabledMessage = dpmi.getPrintingDisabledReasonForUser(callingUserId);
                } finally {
                    Binder.restoreCallingIdentity(identity);
                }
                if (disabledMessage != null) {
                    Toast.makeText(mContext, Looper.getMainLooper(), disabledMessage,
                            Toast.LENGTH_LONG).show();
                }
                try {
                    adapter.start();
                } catch (RemoteException re) {
                    Log.e(LOG_TAG, "Error calling IPrintDocumentAdapter.start()");
                }
                try {
                    adapter.finish();
                } catch (RemoteException re) {
                    Log.e(LOG_TAG, "Error calling IPrintDocumentAdapter.finish()");
                }
                return null;
            }
            printJobName = Preconditions.checkStringNotEmpty(printJobName);
            packageName = Preconditions.checkStringNotEmpty(packageName);

            final int resolvedUserId = resolveCallingUserEnforcingPermissions(userId);
            final int resolvedAppId;
            final UserState userState;
            final String resolvedPackageName;
            synchronized (mLock) {
                // Only the current group members can start new print jobs.
                if (resolveCallingProfileParentLocked(resolvedUserId) != getCurrentUserId()) {
                    return null;
                }
                resolvedAppId = resolveCallingAppEnforcingPermissions(appId);
                resolvedPackageName = resolveCallingPackageNameEnforcingSecurity(packageName);
                userState = getOrCreateUserStateLocked(resolvedUserId, false);
            }
            final long identity = Binder.clearCallingIdentity();
            try {
                return userState.print(printJobName, adapter, attributes,
                        resolvedPackageName, resolvedAppId);
            } finally {
                Binder.restoreCallingIdentity(identity);
            }
        }

        @Override
        public List<PrintJobInfo> getPrintJobInfos(int appId, int userId) {
            final int resolvedUserId = resolveCallingUserEnforcingPermissions(userId);
            final int resolvedAppId;
            final UserState userState;
            synchronized (mLock) {
                // Only the current group members can query for state of print jobs.
                if (resolveCallingProfileParentLocked(resolvedUserId) != getCurrentUserId()) {
                    return null;
                }
                resolvedAppId = resolveCallingAppEnforcingPermissions(appId);
                userState = getOrCreateUserStateLocked(resolvedUserId, false);
            }
            final long identity = Binder.clearCallingIdentity();
            try {
                return userState.getPrintJobInfos(resolvedAppId);
            } finally {
                Binder.restoreCallingIdentity(identity);
            }
        }

        @Override
        public PrintJobInfo getPrintJobInfo(PrintJobId printJobId, int appId, int userId) {
            if (printJobId == null) {
                return null;
            }

            final int resolvedUserId = resolveCallingUserEnforcingPermissions(userId);
            final int resolvedAppId;
            final UserState userState;
            synchronized (mLock) {
                // Only the current group members can query for state of a print job.
                if (resolveCallingProfileParentLocked(resolvedUserId) != getCurrentUserId()) {
                    return null;
                }
                resolvedAppId = resolveCallingAppEnforcingPermissions(appId);
                userState = getOrCreateUserStateLocked(resolvedUserId, false);
            }
            final long identity = Binder.clearCallingIdentity();
            try {
                return userState.getPrintJobInfo(printJobId, resolvedAppId);
            } finally {
                Binder.restoreCallingIdentity(identity);
            }
        }

        @Override
        public Icon getCustomPrinterIcon(PrinterId printerId, int userId) {
            printerId = Preconditions.checkNotNull(printerId);

            final int resolvedUserId = resolveCallingUserEnforcingPermissions(userId);
            final UserState userState;
            synchronized (mLock) {
                // Only the current group members can get the printer icons.
                if (resolveCallingProfileParentLocked(resolvedUserId) != getCurrentUserId()) {
                    return null;
                }
                userState = getOrCreateUserStateLocked(resolvedUserId, false);
            }
            final long identity = Binder.clearCallingIdentity();
            try {
                return userState.getCustomPrinterIcon(printerId);
            } finally {
                Binder.restoreCallingIdentity(identity);
            }
        }

        @Override
        public void cancelPrintJob(PrintJobId printJobId, int appId, int userId) {
            if (printJobId == null) {
                return;
            }

            final int resolvedUserId = resolveCallingUserEnforcingPermissions(userId);
            final int resolvedAppId;
            final UserState userState;
            synchronized (mLock) {
                // Only the current group members can cancel a print job.
                if (resolveCallingProfileParentLocked(resolvedUserId) != getCurrentUserId()) {
                    return;
                }
                resolvedAppId = resolveCallingAppEnforcingPermissions(appId);
                userState = getOrCreateUserStateLocked(resolvedUserId, false);
            }
            final long identity = Binder.clearCallingIdentity();
            try {
                userState.cancelPrintJob(printJobId, resolvedAppId);
            } finally {
                Binder.restoreCallingIdentity(identity);
            }
        }

        @Override
        public void restartPrintJob(PrintJobId printJobId, int appId, int userId) {
            if (printJobId == null || !isPrintingEnabled()) {
                // if printing is disabled the state just remains "failed".
                return;
            }

            final int resolvedUserId = resolveCallingUserEnforcingPermissions(userId);
            final int resolvedAppId;
            final UserState userState;
            synchronized (mLock) {
                // Only the current group members can restart a print job.
                if (resolveCallingProfileParentLocked(resolvedUserId) != getCurrentUserId()) {
                    return;
                }
                resolvedAppId = resolveCallingAppEnforcingPermissions(appId);
                userState = getOrCreateUserStateLocked(resolvedUserId, false);
            }
            final long identity = Binder.clearCallingIdentity();
            try {
                userState.restartPrintJob(printJobId, resolvedAppId);
            } finally {
                Binder.restoreCallingIdentity(identity);
            }
        }

        @Override
        public List<PrintServiceInfo> getPrintServices(int selectionFlags, int userId) {
            Preconditions.checkFlagsArgument(selectionFlags,
                    PrintManager.DISABLED_SERVICES | PrintManager.ENABLED_SERVICES);

            mContext.enforceCallingOrSelfPermission(
                    android.Manifest.permission.READ_PRINT_SERVICES, null);
            final int resolvedUserId = resolveCallingUserEnforcingPermissions(userId);
            final UserState userState;
            synchronized (mLock) {
                // Only the current group members can get print services.
                if (resolveCallingProfileParentLocked(resolvedUserId) != getCurrentUserId()) {
                    return null;
                }
                userState = getOrCreateUserStateLocked(resolvedUserId, false);
            }
            final long identity = Binder.clearCallingIdentity();
            try {
                return userState.getPrintServices(selectionFlags);
            } finally {
                Binder.restoreCallingIdentity(identity);
            }
        }

        @Override
        public void setPrintServiceEnabled(ComponentName service, boolean isEnabled, int userId) {
            final int resolvedUserId = resolveCallingUserEnforcingPermissions(userId);
            final int appId = UserHandle.getAppId(Binder.getCallingUid());

            try {
                if (appId != Process.SYSTEM_UID && appId != UserHandle.getAppId(
                        mContext.getPackageManager().getPackageUidAsUser(
                                PrintManager.PRINT_SPOOLER_PACKAGE_NAME, resolvedUserId))) {
                    throw new SecurityException("Only system and print spooler can call this");
                }
            } catch (PackageManager.NameNotFoundException e) {
                Log.e(LOG_TAG, "Could not verify caller", e);
                return;
            }

            service = Preconditions.checkNotNull(service);

            final UserState userState;
            synchronized (mLock) {
                // Only the current group members can enable / disable services.
                if (resolveCallingProfileParentLocked(resolvedUserId) != getCurrentUserId()) {
                    return;
                }
                userState = getOrCreateUserStateLocked(resolvedUserId, false);
            }
            final long identity = Binder.clearCallingIdentity();
            try {
                userState.setPrintServiceEnabled(service, isEnabled);
            } finally {
                Binder.restoreCallingIdentity(identity);
            }
        }

        @Override
        public List<RecommendationInfo> getPrintServiceRecommendations(int userId) {
            mContext.enforceCallingOrSelfPermission(
                    android.Manifest.permission.READ_PRINT_SERVICE_RECOMMENDATIONS, null);
            final int resolvedUserId = resolveCallingUserEnforcingPermissions(userId);
            final UserState userState;
            synchronized (mLock) {
                // Only the current group members can get print service recommendations.
                if (resolveCallingProfileParentLocked(resolvedUserId) != getCurrentUserId()) {
                    return null;
                }
                userState = getOrCreateUserStateLocked(resolvedUserId, false);
            }
            final long identity = Binder.clearCallingIdentity();
            try {
                return userState.getPrintServiceRecommendations();
            } finally {
                Binder.restoreCallingIdentity(identity);
            }
        }

        @Override
        public void createPrinterDiscoverySession(IPrinterDiscoveryObserver observer,
                int userId) {
            observer = Preconditions.checkNotNull(observer);

            final int resolvedUserId = resolveCallingUserEnforcingPermissions(userId);
            final UserState userState;
            synchronized (mLock) {
                // Only the current group members can create a discovery session.
                if (resolveCallingProfileParentLocked(resolvedUserId) != getCurrentUserId()) {
                    return;
                }
                userState = getOrCreateUserStateLocked(resolvedUserId, false);
            }
            final long identity = Binder.clearCallingIdentity();
            try {
                userState.createPrinterDiscoverySession(observer);
            } finally {
                Binder.restoreCallingIdentity(identity);
            }
        }

        @Override
        public void destroyPrinterDiscoverySession(IPrinterDiscoveryObserver observer,
                int userId) {
            observer = Preconditions.checkNotNull(observer);

            final int resolvedUserId = resolveCallingUserEnforcingPermissions(userId);
            final UserState userState;
            synchronized (mLock) {
                // Only the current group members can destroy a discovery session.
                if (resolveCallingProfileParentLocked(resolvedUserId) != getCurrentUserId()) {
                    return;
                }
                userState = getOrCreateUserStateLocked(resolvedUserId, false);
            }
            final long identity = Binder.clearCallingIdentity();
            try {
                userState.destroyPrinterDiscoverySession(observer);
            } finally {
                Binder.restoreCallingIdentity(identity);
            }
        }

        @Override
        public void startPrinterDiscovery(IPrinterDiscoveryObserver observer,
                List<PrinterId> priorityList, int userId) {
            observer = Preconditions.checkNotNull(observer);
            if (priorityList != null) {
                priorityList = Preconditions.checkCollectionElementsNotNull(priorityList,
                        "PrinterId");
            }

            final int resolvedUserId = resolveCallingUserEnforcingPermissions(userId);
            final UserState userState;
            synchronized (mLock) {
                // Only the current group members can start discovery.
                if (resolveCallingProfileParentLocked(resolvedUserId) != getCurrentUserId()) {
                    return;
                }
                userState = getOrCreateUserStateLocked(resolvedUserId, false);
            }
            final long identity = Binder.clearCallingIdentity();
            try {
                userState.startPrinterDiscovery(observer, priorityList);
            } finally {
                Binder.restoreCallingIdentity(identity);
            }
        }

        @Override
        public void stopPrinterDiscovery(IPrinterDiscoveryObserver observer, int userId) {
            observer = Preconditions.checkNotNull(observer);

            final int resolvedUserId = resolveCallingUserEnforcingPermissions(userId);
            final UserState userState;
            synchronized (mLock) {
                // Only the current group members can stop discovery.
                if (resolveCallingProfileParentLocked(resolvedUserId) != getCurrentUserId()) {
                    return;
                }
                userState = getOrCreateUserStateLocked(resolvedUserId, false);
            }
            final long identity = Binder.clearCallingIdentity();
            try {
                userState.stopPrinterDiscovery(observer);
            } finally {
                Binder.restoreCallingIdentity(identity);
            }
        }

        @Override
        public void validatePrinters(List<PrinterId> printerIds, int userId) {
            printerIds = Preconditions.checkCollectionElementsNotNull(printerIds, "PrinterId");

            final int resolvedUserId = resolveCallingUserEnforcingPermissions(userId);
            final UserState userState;
            synchronized (mLock) {
                // Only the current group members can validate printers.
                if (resolveCallingProfileParentLocked(resolvedUserId) != getCurrentUserId()) {
                    return;
                }
                userState = getOrCreateUserStateLocked(resolvedUserId, false);
            }
            final long identity = Binder.clearCallingIdentity();
            try {
                userState.validatePrinters(printerIds);
            } finally {
                Binder.restoreCallingIdentity(identity);
            }
        }

        @Override
        public void startPrinterStateTracking(PrinterId printerId, int userId) {
            printerId = Preconditions.checkNotNull(printerId);

            final int resolvedUserId = resolveCallingUserEnforcingPermissions(userId);
            final UserState userState;
            synchronized (mLock) {
                // Only the current group members can start printer tracking.
                if (resolveCallingProfileParentLocked(resolvedUserId) != getCurrentUserId()) {
                    return;
                }
                userState = getOrCreateUserStateLocked(resolvedUserId, false);
            }
            final long identity = Binder.clearCallingIdentity();
            try {
                userState.startPrinterStateTracking(printerId);
            } finally {
                Binder.restoreCallingIdentity(identity);
            }
        }

        @Override
        public void stopPrinterStateTracking(PrinterId printerId, int userId) {
            printerId = Preconditions.checkNotNull(printerId);

            final int resolvedUserId = resolveCallingUserEnforcingPermissions(userId);
            final UserState userState;
            synchronized (mLock) {
                // Only the current group members can stop printer tracking.
                if (resolveCallingProfileParentLocked(resolvedUserId) != getCurrentUserId()) {
                    return;
                }
                userState = getOrCreateUserStateLocked(resolvedUserId, false);
            }
            final long identity = Binder.clearCallingIdentity();
            try {
                userState.stopPrinterStateTracking(printerId);
            } finally {
                Binder.restoreCallingIdentity(identity);
            }
        }

        @Override
        public void addPrintJobStateChangeListener(IPrintJobStateChangeListener listener,
                int appId, int userId) throws RemoteException {
            listener = Preconditions.checkNotNull(listener);

            final int resolvedUserId = resolveCallingUserEnforcingPermissions(userId);
            final int resolvedAppId;
            final UserState userState;
            synchronized (mLock) {
                // Only the current group members can add a print job listener.
                if (resolveCallingProfileParentLocked(resolvedUserId) != getCurrentUserId()) {
                    return;
                }
                resolvedAppId = resolveCallingAppEnforcingPermissions(appId);
                userState = getOrCreateUserStateLocked(resolvedUserId, false);
            }
            final long identity = Binder.clearCallingIdentity();
            try {
                userState.addPrintJobStateChangeListener(listener, resolvedAppId);
            } finally {
                Binder.restoreCallingIdentity(identity);
            }
        }

        @Override
        public void removePrintJobStateChangeListener(IPrintJobStateChangeListener listener,
                int userId) {
            listener = Preconditions.checkNotNull(listener);

            final int resolvedUserId = resolveCallingUserEnforcingPermissions(userId);
            final UserState userState;
            synchronized (mLock) {
                // Only the current group members can remove a print job listener.
                if (resolveCallingProfileParentLocked(resolvedUserId) != getCurrentUserId()) {
                    return;
                }
                userState = getOrCreateUserStateLocked(resolvedUserId, false);
            }
            final long identity = Binder.clearCallingIdentity();
            try {
                userState.removePrintJobStateChangeListener(listener);
            } finally {
                Binder.restoreCallingIdentity(identity);
            }
        }

        @Override
        public void addPrintServicesChangeListener(IPrintServicesChangeListener listener,
                int userId) throws RemoteException {
            listener = Preconditions.checkNotNull(listener);

            mContext.enforceCallingOrSelfPermission(android.Manifest.permission.READ_PRINT_SERVICES,
                    null);
            final int resolvedUserId = resolveCallingUserEnforcingPermissions(userId);
            final UserState userState;
            synchronized (mLock) {
                // Only the current group members can add a print services listener.
                if (resolveCallingProfileParentLocked(resolvedUserId) != getCurrentUserId()) {
                    return;
                }
                userState = getOrCreateUserStateLocked(resolvedUserId, false);
            }
            final long identity = Binder.clearCallingIdentity();
            try {
                userState.addPrintServicesChangeListener(listener);
            } finally {
                Binder.restoreCallingIdentity(identity);
            }
        }

        @Override
        public void removePrintServicesChangeListener(IPrintServicesChangeListener listener,
                int userId) {
            listener = Preconditions.checkNotNull(listener);

            mContext.enforceCallingOrSelfPermission(android.Manifest.permission.READ_PRINT_SERVICES,
                    null);
            final int resolvedUserId = resolveCallingUserEnforcingPermissions(userId);
            final UserState userState;
            synchronized (mLock) {
                // Only the current group members can remove a print services change listener.
                if (resolveCallingProfileParentLocked(resolvedUserId) != getCurrentUserId()) {
                    return;
                }
                userState = getOrCreateUserStateLocked(resolvedUserId, false);
            }
            final long identity = Binder.clearCallingIdentity();
            try {
                userState.removePrintServicesChangeListener(listener);
            } finally {
                Binder.restoreCallingIdentity(identity);
            }
        }

        @Override
        public void addPrintServiceRecommendationsChangeListener(
                IRecommendationsChangeListener listener, int userId)
                throws RemoteException {
            listener = Preconditions.checkNotNull(listener);

            mContext.enforceCallingOrSelfPermission(
                    android.Manifest.permission.READ_PRINT_SERVICE_RECOMMENDATIONS, null);
            final int resolvedUserId = resolveCallingUserEnforcingPermissions(userId);
            final UserState userState;
            synchronized (mLock) {
                // Only the current group members can add a print service recommendations listener.
                if (resolveCallingProfileParentLocked(resolvedUserId) != getCurrentUserId()) {
                    return;
                }
                userState = getOrCreateUserStateLocked(resolvedUserId, false);
            }
            final long identity = Binder.clearCallingIdentity();
            try {
                userState.addPrintServiceRecommendationsChangeListener(listener);
            } finally {
                Binder.restoreCallingIdentity(identity);
            }
        }

        @Override
        public void removePrintServiceRecommendationsChangeListener(
                IRecommendationsChangeListener listener, int userId) {
            listener = Preconditions.checkNotNull(listener);

            mContext.enforceCallingOrSelfPermission(
                    android.Manifest.permission.READ_PRINT_SERVICE_RECOMMENDATIONS, null);
            final int resolvedUserId = resolveCallingUserEnforcingPermissions(userId);
            final UserState userState;
            synchronized (mLock) {
                // Only the current group members can remove a print service recommendations
                // listener.
                if (resolveCallingProfileParentLocked(resolvedUserId) != getCurrentUserId()) {
                    return;
                }
                userState = getOrCreateUserStateLocked(resolvedUserId, false);
            }
            final long identity = Binder.clearCallingIdentity();
            try {
                userState.removePrintServiceRecommendationsChangeListener(listener);
            } finally {
                Binder.restoreCallingIdentity(identity);
            }
        }

        @Override
        public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
            fd = Preconditions.checkNotNull(fd);

            if (!DumpUtils.checkDumpPermission(mContext, LOG_TAG, pw)) return;

            int opti = 0;
            boolean dumpAsProto = false;
            while (opti < args.length) {
                String opt = args[opti];
                if (opt == null || opt.length() <= 0 || opt.charAt(0) != '-') {
                    break;
                }
                opti++;
                if ("--proto".equals(opt)) {
                    dumpAsProto = true;
                } else {
                    pw.println("Unknown argument: " + opt + "; use -h for help");
                }
            }

            ArrayList<UserState> userStatesToDump = new ArrayList<>();
            synchronized (mLock) {
                int numUserStates = mUserStates.size();
                for (int i = 0; i < numUserStates; i++) {
                    userStatesToDump.add(mUserStates.valueAt(i));
                }
            }

            final long identity = Binder.clearCallingIdentity();
            try {
                if (dumpAsProto) {
                    dump(new DualDumpOutputStream(new ProtoOutputStream(fd)),
                            userStatesToDump);
                } else {
                    pw.println("PRINT MANAGER STATE (dumpsys print)");

                    dump(new DualDumpOutputStream(new IndentingPrintWriter(pw, "  ")),
                            userStatesToDump);
                }
            } finally {
                Binder.restoreCallingIdentity(identity);
            }
        }

        private boolean isPrintingEnabled() {
            return !mUserManager.hasUserRestriction(UserManager.DISALLOW_PRINTING);
        }

        private void dump(@NonNull DualDumpOutputStream dumpStream,
                @NonNull ArrayList<UserState> userStatesToDump) {
            final int userStateCount = userStatesToDump.size();
            for (int i = 0; i < userStateCount; i++) {
                long token = dumpStream.start("user_states", PrintServiceDumpProto.USER_STATES);
                userStatesToDump.get(i).dump(dumpStream);
                dumpStream.end(token);
            }

            dumpStream.flush();
        }

        private void registerContentObservers() {
            final Uri enabledPrintServicesUri = Settings.Secure.getUriFor(
                    Settings.Secure.DISABLED_PRINT_SERVICES);
            ContentObserver observer = new ContentObserver(BackgroundThread.getHandler()) {
                @Override
                public void onChange(boolean selfChange, Uri uri, int userId) {
                    if (enabledPrintServicesUri.equals(uri)) {
                        synchronized (mLock) {
                            final int userCount = mUserStates.size();
                            for (int i = 0; i < userCount; i++) {
                                if (userId == UserHandle.USER_ALL
                                        || userId == mUserStates.keyAt(i)) {
                                    mUserStates.valueAt(i).updateIfNeededLocked();
                                }
                            }
                        }
                    }
                }
            };

            mContext.getContentResolver().registerContentObserver(enabledPrintServicesUri,
                    false, observer, UserHandle.USER_ALL);
        }

        private void registerBroadcastReceivers() {
            PackageMonitor monitor = new PackageMonitor() {
                /**
                 * Checks if the package contains a print service.
                 *
                 * @param packageName The name of the package
                 *
                 * @return true iff the package contains a print service
                 */
                private boolean hasPrintService(String packageName) {
                    Intent intent = new Intent(android.printservice.PrintService.SERVICE_INTERFACE);
                    intent.setPackage(packageName);

                    List<ResolveInfo> installedServices = mContext.getPackageManager()
                            .queryIntentServicesAsUser(intent,
                                    GET_SERVICES | MATCH_DEBUG_TRIAGED_MISSING,
                                    getChangingUserId());

                    return installedServices != null && !installedServices.isEmpty();
                }

                /**
                 * Checks if there is a print service currently registered for this package.
                 *
                 * @param userState The userstate for the current user
                 * @param packageName The name of the package
                 *
                 * @return true iff the package contained (and might still contain) a print service
                 */
                private boolean hadPrintService(@NonNull UserState userState, String packageName) {
                    List<PrintServiceInfo> installedServices = userState
                            .getPrintServices(PrintManager.ALL_SERVICES);

                    if (installedServices == null) {
                        return false;
                    }

                    final int numInstalledServices = installedServices.size();
                    for (int i = 0; i < numInstalledServices; i++) {
                        if (installedServices.get(i).getResolveInfo().serviceInfo.packageName
                                .equals(packageName)) {
                            return true;
                        }
                    }

                    return false;
                }

                @Override
                public void onPackageModified(String packageName) {
                    if (!mUserManager.isUserUnlockingOrUnlocked(getChangingUserId())) return;
                    UserState userState = getOrCreateUserStateLocked(getChangingUserId(), false,
                            false /* enforceUserUnlockingOrUnlocked */);

                    boolean prunePrintServices = false;
                    synchronized (mLock) {
                        if (hadPrintService(userState, packageName)
                                || hasPrintService(packageName)) {
                            userState.updateIfNeededLocked();
                            prunePrintServices = true;
                        }
                    }

                    if (prunePrintServices) {
                        userState.prunePrintServices();
                    }
                }

                @Override
                public void onPackageRemoved(String packageName, int uid) {
                    if (!mUserManager.isUserUnlockingOrUnlocked(getChangingUserId())) return;
                    UserState userState = getOrCreateUserStateLocked(getChangingUserId(), false,
                            false /* enforceUserUnlockingOrUnlocked */);

                    boolean prunePrintServices = false;
                    synchronized (mLock) {
                        if (hadPrintService(userState, packageName)) {
                            userState.updateIfNeededLocked();
                            prunePrintServices = true;
                        }
                    }

                    if (prunePrintServices) {
                        userState.prunePrintServices();
                    }
                }

                @Override
                public boolean onHandleForceStop(Intent intent, String[] stoppedPackages,
                        int uid, boolean doit) {
                    if (!mUserManager.isUserUnlockingOrUnlocked(getChangingUserId())) return false;
                    synchronized (mLock) {
                        // A background user/profile's print jobs are running but there is
                        // no UI shown. Hence, if the packages of such a user change we need
                        // to handle it as the change may affect ongoing print jobs.
                        UserState userState = getOrCreateUserStateLocked(getChangingUserId(), false,
                                false /* enforceUserUnlockingOrUnlocked */);
                        boolean stoppedSomePackages = false;

                        List<PrintServiceInfo> enabledServices = userState
                                .getPrintServices(PrintManager.ENABLED_SERVICES);
                        if (enabledServices == null) {
                            return false;
                        }

                        Iterator<PrintServiceInfo> iterator = enabledServices.iterator();
                        while (iterator.hasNext()) {
                            ComponentName componentName = iterator.next().getComponentName();
                            String componentPackage = componentName.getPackageName();
                            for (String stoppedPackage : stoppedPackages) {
                                if (componentPackage.equals(stoppedPackage)) {
                                    if (!doit) {
                                        return true;
                                    }
                                    stoppedSomePackages = true;
                                    break;
                                }
                            }
                        }
                        if (stoppedSomePackages) {
                            userState.updateIfNeededLocked();
                        }
                        return false;
                    }
                }

                @Override
                public void onPackageAdded(String packageName, int uid) {
                    if (!mUserManager.isUserUnlockingOrUnlocked(getChangingUserId())) return;
                    synchronized (mLock) {
                        if (hasPrintService(packageName)) {
                            UserState userState = getOrCreateUserStateLocked(getChangingUserId(),
                                    false, false /* enforceUserUnlockingOrUnlocked */);
                            userState.updateIfNeededLocked();
                        }
                    }
                }
            };

            // package changes
            monitor.register(mContext, BackgroundThread.getHandler().getLooper(),
                    UserHandle.ALL, true);
        }
        private UserState getOrCreateUserStateLocked(int userId, boolean lowPriority) {
            return getOrCreateUserStateLocked(userId, lowPriority,
                    true /* enforceUserUnlockingOrUnlocked */);
        }

        private UserState getOrCreateUserStateLocked(int userId, boolean lowPriority,
                boolean enforceUserUnlockingOrUnlocked) {
            if (enforceUserUnlockingOrUnlocked && !mUserManager.isUserUnlockingOrUnlocked(userId)) {
                throw new IllegalStateException(
                        "User " + userId + " must be unlocked for printing to be available");
            }

            UserState userState = mUserStates.get(userId);
            if (userState == null) {
                userState = new UserState(mContext, userId, mLock, lowPriority);
                mUserStates.put(userId, userState);
            }

            if (!lowPriority) {
                userState.increasePriority();
            }

            return userState;
        }

        private void handleUserUnlocked(final int userId) {
            // This code will touch the remote print spooler which
            // must be called off the main thread, so post the work.
            BackgroundThread.getHandler().post(new Runnable() {
                @Override
                public void run() {
                    if (!mUserManager.isUserUnlockingOrUnlocked(userId)) return;

                    UserState userState;
                    synchronized (mLock) {
                        userState = getOrCreateUserStateLocked(userId, true,
                                false /*enforceUserUnlockingOrUnlocked */);
                        userState.updateIfNeededLocked();
                    }
                    // This is the first time we switch to this user after boot, so
                    // now is the time to remove obsolete print jobs since they
                    // are from the last boot and no application would query them.
                    userState.removeObsoletePrintJobs();
                }
            });
        }

        private void handleUserStopped(final int userId) {
            // This code will touch the remote print spooler which
            // must be called off the main thread, so post the work.
            BackgroundThread.getHandler().post(new Runnable() {
                @Override
                public void run() {
                    synchronized (mLock) {
                        UserState userState = mUserStates.get(userId);
                        if (userState != null) {
                            userState.destroyLocked();
                            mUserStates.remove(userId);
                        }
                    }
                }
            });
        }

        private int resolveCallingProfileParentLocked(int userId) {
            if (userId != getCurrentUserId()) {
                final long identity = Binder.clearCallingIdentity();
                try {
                    UserInfo parent = mUserManager.getProfileParent(userId);
                    if (parent != null) {
                        return parent.getUserHandle().getIdentifier();
                    } else {
                        return BACKGROUND_USER_ID;
                    }
                } finally {
                    Binder.restoreCallingIdentity(identity);
                }
            }
            return userId;
        }

        private int resolveCallingAppEnforcingPermissions(int appId) {
            final int callingUid = Binder.getCallingUid();
            if (callingUid == 0) {
                return appId;
            }
            final int callingAppId = UserHandle.getAppId(callingUid);
            if (appId == callingAppId || callingAppId == Process.SHELL_UID
                    || callingAppId == Process.SYSTEM_UID) {
                return appId;
            }
            if (mContext.checkCallingPermission(
                    "com.android.printspooler.permission.ACCESS_ALL_PRINT_JOBS")
                    != PackageManager.PERMISSION_GRANTED) {
                throw new SecurityException("Call from app " + callingAppId + " as app "
                        + appId + " without com.android.printspooler.permission"
                        + ".ACCESS_ALL_PRINT_JOBS");
            }
            return appId;
        }

        private int resolveCallingUserEnforcingPermissions(int userId) {
            try {
                return ActivityManager.getService().handleIncomingUser(Binder.getCallingPid(),
                        Binder.getCallingUid(), userId, true, true, "", null);
            } catch (RemoteException re) {
                // Shouldn't happen, local.
            }
            return userId;
        }

        private @NonNull String resolveCallingPackageNameEnforcingSecurity(
                @NonNull String packageName) {
            String[] packages = mContext.getPackageManager().getPackagesForUid(
                    Binder.getCallingUid());
            final int packageCount = packages.length;
            for (int i = 0; i < packageCount; i++) {
                if (packageName.equals(packages[i])) {
                    return packageName;
                }
            }
            throw new IllegalArgumentException("packageName has to belong to the caller");
        }

        private int getCurrentUserId () {
            final long identity = Binder.clearCallingIdentity();
            try {
                return ActivityManager.getCurrentUser();
            } finally {
                Binder.restoreCallingIdentity(identity);
            }
        }
    }
}
