/*
 * Copyright (C) 2013 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.server.print;

import android.Manifest;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.database.ContentObserver;
import android.net.Uri;
import android.os.Binder;
import android.os.Process;
import android.os.UserHandle;
import android.print.IPrintClient;
import android.print.IPrintDocumentAdapter;
import android.print.IPrintManager;
import android.print.IPrinterDiscoveryObserver;
import android.print.PrintAttributes;
import android.print.PrintJobInfo;
import android.print.PrinterId;
import android.provider.Settings;
import android.util.SparseArray;

import com.android.internal.content.PackageMonitor;
import com.android.internal.os.BackgroundThread;

import java.util.Iterator;
import java.util.List;
import java.util.Set;

public final class PrintManagerService extends IPrintManager.Stub {

    private static final char COMPONENT_NAME_SEPARATOR = ':';

    private final Object mLock = new Object();

    private final Context mContext;

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

    private int mCurrentUserId = UserHandle.USER_OWNER;

    public PrintManagerService(Context context) {
        mContext = context;
        registerContentObservers();
        registerBoradcastReceivers();
    }

    public void systemRuning() {
        BackgroundThread.getHandler().post(new Runnable() {
            @Override
            public void run() {
                synchronized (mLock) {
                    UserState userState = getCurrentUserStateLocked();
                    userState.updateIfNeededLocked();
                    userState.getSpoolerLocked().start();
                }
            }
        });
    }

    @Override
    public PrintJobInfo print(String printJobName, IPrintClient client,
            IPrintDocumentAdapter documentAdapter, PrintAttributes attributes, int appId,
            int userId) {
        final int resolvedAppId = resolveCallingAppEnforcingPermissions(appId);
        final int resolvedUserId = resolveCallingUserEnforcingPermissions(userId);
        final UserState userState;
        final RemotePrintSpooler spooler;
        synchronized (mLock) {
            userState = getOrCreateUserStateLocked(resolvedUserId);
            spooler = userState.getSpoolerLocked();
        }
        final long identity = Binder.clearCallingIdentity();
        try {
            return spooler.createPrintJob(printJobName, client, documentAdapter,
                    attributes, resolvedAppId);
        } finally {
            Binder.restoreCallingIdentity(identity);
        }
    }

    @Override
    public List<PrintJobInfo> getPrintJobInfos(int appId, int userId) {
        final int resolvedAppId = resolveCallingAppEnforcingPermissions(appId);
        final int resolvedUserId = resolveCallingUserEnforcingPermissions(userId);
        final UserState userState;
        final RemotePrintSpooler spooler;
        synchronized (mLock) {
            userState = getOrCreateUserStateLocked(resolvedUserId);
            spooler = userState.getSpoolerLocked();
        }
        final long identity = Binder.clearCallingIdentity();
        try {
            return spooler.getPrintJobInfos(null, PrintJobInfo.STATE_ANY,
                    resolvedAppId);
        } finally {
            Binder.restoreCallingIdentity(identity);
        }
    }

    @Override
    public PrintJobInfo getPrintJobInfo(int printJobId, int appId, int userId) {
        final int resolvedAppId = resolveCallingAppEnforcingPermissions(appId);
        final int resolvedUserId = resolveCallingUserEnforcingPermissions(userId);
        final UserState userState;
        final RemotePrintSpooler spooler;
        synchronized (mLock) {
            userState = getOrCreateUserStateLocked(resolvedUserId);
            spooler = userState.getSpoolerLocked();
        }
        final long identity = Binder.clearCallingIdentity();
        try {
            return spooler.getPrintJobInfo(printJobId, resolvedAppId);
        } finally {
            Binder.restoreCallingIdentity(identity);
        }
    }

    @Override
    public void cancelPrintJob(int printJobId, int appId, int userId) {
        final int resolvedAppId = resolveCallingAppEnforcingPermissions(appId);
        final int resolvedUserId = resolveCallingUserEnforcingPermissions(userId);
        final UserState userState;
        final RemotePrintSpooler spooler;
        synchronized (mLock) {
            userState = getOrCreateUserStateLocked(resolvedUserId);
            spooler = userState.getSpoolerLocked();
        }
        final long identity = Binder.clearCallingIdentity();
        try {
            PrintJobInfo printJobInfo = spooler.getPrintJobInfo(printJobId, resolvedAppId);
            if (printJobInfo == null) {
                return;
            }
            if (printJobInfo.getState() != PrintJobInfo.STATE_FAILED) {
                ComponentName printServiceName = printJobInfo.getPrinterId().getServiceName();
                RemotePrintService printService = null;
                synchronized (mLock) {
                    printService = userState.getActiveServicesLocked().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.
                spooler.setPrintJobState(printJobId, PrintJobInfo.STATE_CANCELED, null);
            }
        } finally {
            Binder.restoreCallingIdentity(identity);
        }
    }

    @Override
    public void restartPrintJob(int printJobId, int appId, int userId) {
        final int resolvedAppId = resolveCallingAppEnforcingPermissions(appId);
        final int resolvedUserId = resolveCallingUserEnforcingPermissions(userId);
        final RemotePrintSpooler spooler;
        synchronized (mLock) {
            spooler = getOrCreateUserStateLocked(resolvedUserId).getSpoolerLocked();
        }
        final long identity = Binder.clearCallingIdentity();
        try {
            PrintJobInfo printJobInfo = getPrintJobInfo(printJobId, resolvedAppId, resolvedUserId);
            if (printJobInfo == null || printJobInfo.getState() != PrintJobInfo.STATE_FAILED) {
                return;
            }
            spooler.setPrintJobState(printJobId, PrintJobInfo.STATE_QUEUED, null);
        } finally {
            Binder.restoreCallingIdentity(identity);
        }
    }

    @Override
    public void createPrinterDiscoverySession(IPrinterDiscoveryObserver observer,
            int userId) {
        final int resolvedUserId = resolveCallingUserEnforcingPermissions(userId);
        final UserState userState;
        synchronized (mLock) {
            userState = getOrCreateUserStateLocked(resolvedUserId);
        }
        final long identity = Binder.clearCallingIdentity();
        try {
            userState.createPrinterDiscoverySession(observer);
        } finally {
            Binder.restoreCallingIdentity(identity);
        }
    }

    @Override
    public void destroyPrinterDiscoverySession(IPrinterDiscoveryObserver observer,
            int userId) {
        final int resolvedUserId = resolveCallingUserEnforcingPermissions(userId);
        final UserState userState;
        synchronized (mLock) {
            userState = getOrCreateUserStateLocked(resolvedUserId);
        }
        final long identity = Binder.clearCallingIdentity();
        try {
            userState.destroyPrinterDiscoverySession(observer);
        } finally {
            Binder.restoreCallingIdentity(identity);
        }
    }

    @Override
    public void startPrinterDiscovery(IPrinterDiscoveryObserver observer,
            List<PrinterId> priorityList, int userId) {
        final int resolvedUserId = resolveCallingUserEnforcingPermissions(userId);
        final UserState userState;
        synchronized (mLock) {
            userState = getOrCreateUserStateLocked(resolvedUserId);
        }
        final long identity = Binder.clearCallingIdentity();
        try {
            userState.startPrinterDiscovery(observer, priorityList);
        } finally {
            Binder.restoreCallingIdentity(identity);
        }
    }

    @Override
    public void stopPrinterDiscovery(IPrinterDiscoveryObserver observer, int userId) {
        final int resolvedUserId = resolveCallingUserEnforcingPermissions(userId);
        final UserState userState;
        synchronized (mLock) {
            userState = getOrCreateUserStateLocked(resolvedUserId);
        }
        final long identity = Binder.clearCallingIdentity();
        try {
            userState.stopPrinterDiscovery(observer);
        } finally {
            Binder.restoreCallingIdentity(identity);
        }
    }

    @Override
    public void requestPrinterUpdate(PrinterId printerId, int userId) {
        final int resolvedUserId = resolveCallingUserEnforcingPermissions(userId);
        final UserState userState;
        synchronized (mLock) {
            userState = getOrCreateUserStateLocked(resolvedUserId);
        }
        final long identity = Binder.clearCallingIdentity();
        try {
            userState.requestPrinterUpdate(printerId);
        } finally {
            Binder.restoreCallingIdentity(identity);
        }
    }

    private void registerContentObservers() {
        final Uri enabledPrintServicesUri = Settings.Secure.getUriFor(
                Settings.Secure.ENABLED_PRINT_SERVICES);

        ContentObserver observer = new ContentObserver(BackgroundThread.getHandler()) {
            @Override
            public void onChange(boolean selfChange, Uri uri) {
                if (enabledPrintServicesUri.equals(uri)) {
                    synchronized (mLock) {
                        UserState userState = getCurrentUserStateLocked();
                        userState.updateIfNeededLocked();
                    }
                }
            }
        };

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

    private void registerBoradcastReceivers() {
        PackageMonitor monitor = new PackageMonitor() {
            @Override
            public boolean onPackageChanged(String packageName, int uid, String[] components) {
                synchronized (mLock) {
                    UserState userState = getOrCreateUserStateLocked(getChangingUserId());
                    Iterator<ComponentName> iterator = userState.getEnabledServices().iterator();
                    while (iterator.hasNext()) {
                        ComponentName componentName = iterator.next();
                        if (packageName.equals(componentName.getPackageName())) {
                            userState.updateIfNeededLocked();
                            return true;
                        }
                    }
                }
                return false;
            }

            @Override
            public void onPackageRemoved(String packageName, int uid) {
                synchronized (mLock) {
                    UserState userState = getOrCreateUserStateLocked(getChangingUserId());
                    Iterator<ComponentName> iterator = userState.getEnabledServices().iterator();
                    while (iterator.hasNext()) {
                        ComponentName componentName = iterator.next();
                        if (packageName.equals(componentName.getPackageName())) {
                            iterator.remove();
                            persistComponentNamesToSettingLocked(
                                    Settings.Secure.ENABLED_PRINT_SERVICES,
                                    userState.getEnabledServices(), getChangingUserId());
                            userState.updateIfNeededLocked();
                            return;
                        }
                    }
                }
            }

            @Override
            public boolean onHandleForceStop(Intent intent, String[] stoppedPackages,
                    int uid, boolean doit) {
                synchronized (mLock) {
                    UserState userState = getOrCreateUserStateLocked(getChangingUserId());
                    boolean stoppedSomePackages = false;
                    Iterator<ComponentName> iterator = userState.getEnabledServices().iterator();
                    while (iterator.hasNext()) {
                        ComponentName componentName = iterator.next();
                        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;
                }
            }

            private void persistComponentNamesToSettingLocked(String settingName,
                    Set<ComponentName> componentNames, int userId) {
                StringBuilder builder = new StringBuilder();
                for (ComponentName componentName : componentNames) {
                    if (builder.length() > 0) {
                        builder.append(COMPONENT_NAME_SEPARATOR);
                    }
                    builder.append(componentName.flattenToShortString());
                }
                Settings.Secure.putStringForUser(mContext.getContentResolver(),
                        settingName, builder.toString(), userId);
            }
        };

        // package changes
        monitor.register(mContext, BackgroundThread.getHandler().getLooper(),
                UserHandle.ALL, true);

        // user changes
        IntentFilter intentFilter = new IntentFilter();
        intentFilter.addAction(Intent.ACTION_USER_SWITCHED);

        mContext.registerReceiverAsUser(new BroadcastReceiver() {
            @Override
            public void onReceive(Context context, Intent intent) {
                String action = intent.getAction();
                if (Intent.ACTION_USER_SWITCHED.equals(action)) {
                    switchUser(intent.getIntExtra(Intent.EXTRA_USER_HANDLE, 0));
                } else if (Intent.ACTION_USER_REMOVED.equals(action)) {
                    removeUser(intent.getIntExtra(Intent.EXTRA_USER_HANDLE, 0));
                }
            }
        }, UserHandle.ALL, intentFilter, null, BackgroundThread.getHandler());
    }

    private UserState getCurrentUserStateLocked() {
        return getOrCreateUserStateLocked(mCurrentUserId);
    }

    private UserState getOrCreateUserStateLocked(int userId) {
        UserState userState = mUserStates.get(userId);
        if (userState == null) {
            userState = new UserState(mContext, userId, mLock);
            mUserStates.put(userId, userState);
        }
        return userState;
    }

    private void switchUser(int newUserId) {
        synchronized (mLock) {
            if (newUserId == mCurrentUserId) {
                return;
            }
            mCurrentUserId = newUserId;
            UserState userState = getCurrentUserStateLocked();
            userState.updateIfNeededLocked();
            userState.getSpoolerLocked().start();
        }
    }

    private void removeUser(int removedUserId) {
        synchronized (mLock) {
            UserState userState = mUserStates.get(removedUserId);
            if (userState != null) {
                userState.destroyLocked();
                mUserStates.remove(removedUserId);
            }
        }
    }

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

    private int resolveCallingUserEnforcingPermissions(int userId) {
        final int callingUid = Binder.getCallingUid();
        if (callingUid == 0 || callingUid == Process.SYSTEM_UID
                || callingUid == Process.SHELL_UID) {
            return userId;
        }
        final int callingUserId = UserHandle.getUserId(callingUid);
        if (callingUserId == userId) {
            return userId;
        }
        if (mContext.checkCallingPermission(Manifest.permission.INTERACT_ACROSS_USERS_FULL)
                != PackageManager.PERMISSION_GRANTED
            ||  mContext.checkCallingPermission(Manifest.permission.INTERACT_ACROSS_USERS)
                != PackageManager.PERMISSION_GRANTED) {
            if (userId == UserHandle.USER_CURRENT_OR_SELF) {
                return callingUserId;
            }
            throw new SecurityException("Call from user " + callingUserId + " as user "
                + userId + " without permission INTERACT_ACROSS_USERS or "
                + "INTERACT_ACROSS_USERS_FULL not allowed.");
        }
        if (userId == UserHandle.USER_CURRENT || userId == UserHandle.USER_CURRENT_OR_SELF) {
            return mCurrentUserId;
        }
        throw new IllegalArgumentException("Calling user can be changed to only "
                + "UserHandle.USER_CURRENT or UserHandle.USER_CURRENT_OR_SELF.");
    }
}
