/*
 * 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.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
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.content.pm.ResolveInfo;
import android.content.pm.ServiceInfo;
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.PrintJobId;
import android.print.PrintJobInfo;
import android.print.PrinterId;
import android.printservice.PrintServiceInfo;
import android.provider.Settings;
import android.util.SparseArray;

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

import java.io.FileDescriptor;
import java.io.PrintWriter;
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 static final String EXTRA_PRINT_SERVICE_COMPONENT_NAME =
            "EXTRA_PRINT_SERVICE_COMPONENT_NAME";

    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() {
                final UserState userState;
                synchronized (mLock) {
                    userState = getCurrentUserStateLocked();
                    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();
            }
        });
    }

    @Override
    public PrintJobInfo print(String printJobName, final IPrintClient client,
            final IPrintDocumentAdapter documentAdapter, PrintAttributes attributes,
            int appId, int userId) {
        final int resolvedAppId = resolveCallingAppEnforcingPermissions(appId);
        final int resolvedUserId = resolveCallingUserEnforcingPermissions(userId);
        final UserState userState;
        synchronized (mLock) {
            userState = getOrCreateUserStateLocked(resolvedUserId);
        }
        final long identity = Binder.clearCallingIdentity();
        try {
            return userState.print(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;
        synchronized (mLock) {
            userState = getOrCreateUserStateLocked(resolvedUserId);
        }
        final long identity = Binder.clearCallingIdentity();
        try {
            return userState.getPrintJobInfos(resolvedAppId);
        } finally {
            Binder.restoreCallingIdentity(identity);
        }
    }

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

    @Override
    public void cancelPrintJob(PrintJobId printJobId, int appId, int userId) {
        final int resolvedAppId = resolveCallingAppEnforcingPermissions(appId);
        final int resolvedUserId = resolveCallingUserEnforcingPermissions(userId);
        final UserState userState;
        synchronized (mLock) {
            userState = getOrCreateUserStateLocked(resolvedUserId);
        }
        final long identity = Binder.clearCallingIdentity();
        try {
            userState.cancelPrintJob(printJobId, resolvedAppId);
        } finally {
            Binder.restoreCallingIdentity(identity);
        }
    }

    @Override
    public void restartPrintJob(PrintJobId printJobId, int appId, int userId) {
        final int resolvedAppId = resolveCallingAppEnforcingPermissions(appId);
        final int resolvedUserId = resolveCallingUserEnforcingPermissions(userId);
        final UserState userState;
        synchronized (mLock) {
            userState = getOrCreateUserStateLocked(resolvedUserId);
        }
        final long identity = Binder.clearCallingIdentity();
        try {
            userState.restartPrintJob(printJobId, resolvedAppId);
        } finally {
            Binder.restoreCallingIdentity(identity);
        }
    }

    @Override
    public List<PrintServiceInfo> getEnabledPrintServices(int userId) {
        final int resolvedUserId = resolveCallingUserEnforcingPermissions(userId);
        final UserState userState;
        synchronized (mLock) {
            userState = getOrCreateUserStateLocked(resolvedUserId);
        }
        final long identity = Binder.clearCallingIdentity();
        try {
            return userState.getEnabledPrintServices();
        } 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 validatePrinters(List<PrinterId> printerIds, int userId) {
        final int resolvedUserId = resolveCallingUserEnforcingPermissions(userId);
        final UserState userState;
        synchronized (mLock) {
            userState = getOrCreateUserStateLocked(resolvedUserId);
        }
        final long identity = Binder.clearCallingIdentity();
        try {
            userState.validatePrinters(printerIds);
        } finally {
            Binder.restoreCallingIdentity(identity);
        }
    }

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

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

    @Override
    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
        if (mContext.checkCallingOrSelfPermission(Manifest.permission.DUMP)
                != PackageManager.PERMISSION_GRANTED) {
            pw.println("Permission Denial: can't dump PrintManager from from pid="
                    + Binder.getCallingPid()
                    + ", uid=" + Binder.getCallingUid());
            return;
        }

        synchronized (mLock) {
            pw.println("PRINT MANAGER STATE (dumpsys print)");
            final int userStateCount = mUserStates.size();
            for (int i = 0; i < userStateCount; i++) {
                UserState userState = mUserStates.get(i);
                userState.dump(fd, pw, "");
                pw.println();
            }
        }
    }

    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;
                }
            }

            @Override
            public void onPackageAdded(String packageName, int uid) {
                Intent intent = new Intent(android.printservice.PrintService.SERVICE_INTERFACE);
                intent.setPackage(packageName);

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

                final int installedServiceCount = installedServices.size();
                for (int i = 0; i < installedServiceCount; i++) {
                    ServiceInfo serviceInfo = installedServices.get(i).serviceInfo;
                    ComponentName component = new ComponentName(serviceInfo.packageName,
                            serviceInfo.name);
                    String label = serviceInfo.loadLabel(mContext.getPackageManager()).toString();
                    showEnableInstalledPrintServiceNotification(component, label);
                }
            }

            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);
        intentFilter.addAction(Intent.ACTION_USER_REMOVED);

        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) {
        UserState userState;
        synchronized (mLock) {
            if (newUserId == mCurrentUserId) {
                return;
            }
            mCurrentUserId = newUserId;
            userState = mUserStates.get(mCurrentUserId);
            if (userState == null) {
                userState = getCurrentUserStateLocked();
                userState.updateIfNeededLocked();
            } else {
                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 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(
                "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) {
        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.");
    }

    private void showEnableInstalledPrintServiceNotification(ComponentName component,
            String label) {
        Intent intent = new Intent(Settings.ACTION_PRINT_SETTINGS);
        intent.putExtra(EXTRA_PRINT_SERVICE_COMPONENT_NAME, component.flattenToString());

        PendingIntent pendingIntent = PendingIntent.getActivity(mContext, 0, intent,
                PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_CANCEL_CURRENT, null);

        Notification.Builder builder = new Notification.Builder(mContext)
                .setSmallIcon(R.drawable.ic_print)
                .setContentTitle(mContext.getString(R.string.print_service_installed_title, label))
                .setContentText(mContext.getString(R.string.print_service_installed_message))
                .setContentIntent(pendingIntent)
                .setWhen(System.currentTimeMillis())
                .setAutoCancel(true)
                .setShowWhen(true);

        NotificationManager notificationManager = (NotificationManager) mContext
                .getSystemService(Context.NOTIFICATION_SERVICE);

        String notificationTag = getClass().getName() + ":" + component.flattenToString();
        notificationManager.notify(notificationTag, 0, builder.build());
    }
}
