/*
 * Copyright (C) 2012 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.dreams;

import static android.Manifest.permission.BIND_DREAM_SERVICE;

import com.android.internal.util.DumpUtils;
import com.android.server.FgThread;
import com.android.server.SystemService;

import android.Manifest;
import android.app.ActivityManager;
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.PackageManager.NameNotFoundException;
import android.content.pm.ServiceInfo;
import android.os.Binder;
import android.os.Build;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.os.PowerManager;
import android.os.PowerManagerInternal;
import android.os.SystemClock;
import android.os.SystemProperties;
import android.os.UserHandle;
import android.provider.Settings;
import android.service.dreams.DreamManagerInternal;
import android.service.dreams.DreamService;
import android.service.dreams.IDreamManager;
import android.text.TextUtils;
import android.util.Slog;
import android.view.Display;

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

import libcore.util.Objects;

/**
 * Service api for managing dreams.
 *
 * @hide
 */
public final class DreamManagerService extends SystemService {
    private static final boolean DEBUG = false;
    private static final String TAG = "DreamManagerService";

    private final Object mLock = new Object();

    private final Context mContext;
    private final DreamHandler mHandler;
    private final DreamController mController;
    private final PowerManager mPowerManager;
    private final PowerManagerInternal mPowerManagerInternal;
    private final PowerManager.WakeLock mDozeWakeLock;

    private Binder mCurrentDreamToken;
    private ComponentName mCurrentDreamName;
    private int mCurrentDreamUserId;
    private boolean mCurrentDreamIsTest;
    private boolean mCurrentDreamCanDoze;
    private boolean mCurrentDreamIsDozing;
    private boolean mCurrentDreamIsWaking;
    private int mCurrentDreamDozeScreenState = Display.STATE_UNKNOWN;
    private int mCurrentDreamDozeScreenBrightness = PowerManager.BRIGHTNESS_DEFAULT;

    public DreamManagerService(Context context) {
        super(context);
        mContext = context;
        mHandler = new DreamHandler(FgThread.get().getLooper());
        mController = new DreamController(context, mHandler, mControllerListener);

        mPowerManager = (PowerManager)context.getSystemService(Context.POWER_SERVICE);
        mPowerManagerInternal = getLocalService(PowerManagerInternal.class);
        mDozeWakeLock = mPowerManager.newWakeLock(PowerManager.DOZE_WAKE_LOCK, TAG);
    }

    @Override
    public void onStart() {
        publishBinderService(DreamService.DREAM_SERVICE, new BinderService());
        publishLocalService(DreamManagerInternal.class, new LocalService());
    }

    @Override
    public void onBootPhase(int phase) {
        if (phase == SystemService.PHASE_THIRD_PARTY_APPS_CAN_START) {
            if (Build.IS_DEBUGGABLE) {
                SystemProperties.addChangeCallback(mSystemPropertiesChanged);
            }
            mContext.registerReceiver(new BroadcastReceiver() {
                @Override
                public void onReceive(Context context, Intent intent) {
                    synchronized (mLock) {
                        stopDreamLocked(false /*immediate*/);
                    }
                }
            }, new IntentFilter(Intent.ACTION_USER_SWITCHED), null, mHandler);
        }
    }

    private void dumpInternal(PrintWriter pw) {
        pw.println("DREAM MANAGER (dumpsys dreams)");
        pw.println();
        pw.println("mCurrentDreamToken=" + mCurrentDreamToken);
        pw.println("mCurrentDreamName=" + mCurrentDreamName);
        pw.println("mCurrentDreamUserId=" + mCurrentDreamUserId);
        pw.println("mCurrentDreamIsTest=" + mCurrentDreamIsTest);
        pw.println("mCurrentDreamCanDoze=" + mCurrentDreamCanDoze);
        pw.println("mCurrentDreamIsDozing=" + mCurrentDreamIsDozing);
        pw.println("mCurrentDreamIsWaking=" + mCurrentDreamIsWaking);
        pw.println("mCurrentDreamDozeScreenState="
                + Display.stateToString(mCurrentDreamDozeScreenState));
        pw.println("mCurrentDreamDozeScreenBrightness=" + mCurrentDreamDozeScreenBrightness);
        pw.println("getDozeComponent()=" + getDozeComponent());
        pw.println();

        DumpUtils.dumpAsync(mHandler, new DumpUtils.Dump() {
            @Override
            public void dump(PrintWriter pw, String prefix) {
                mController.dump(pw);
            }
        }, pw, "", 200);
    }

    private boolean isDreamingInternal() {
        synchronized (mLock) {
            return mCurrentDreamToken != null && !mCurrentDreamIsTest
                    && !mCurrentDreamIsWaking;
        }
    }

    private void requestDreamInternal() {
        // Ask the power manager to nap.  It will eventually call back into
        // startDream() if/when it is appropriate to start dreaming.
        // Because napping could cause the screen to turn off immediately if the dream
        // cannot be started, we keep one eye open and gently poke user activity.
        long time = SystemClock.uptimeMillis();
        mPowerManager.userActivity(time, true /*noChangeLights*/);
        mPowerManager.nap(time);
    }

    private void requestAwakenInternal() {
        // Treat an explicit request to awaken as user activity so that the
        // device doesn't immediately go to sleep if the timeout expired,
        // for example when being undocked.
        long time = SystemClock.uptimeMillis();
        mPowerManager.userActivity(time, false /*noChangeLights*/);
        stopDreamInternal(false /*immediate*/);
    }

    private void finishSelfInternal(IBinder token, boolean immediate) {
        if (DEBUG) {
            Slog.d(TAG, "Dream finished: " + token + ", immediate=" + immediate);
        }

        // Note that a dream finishing and self-terminating is not
        // itself considered user activity.  If the dream is ending because
        // the user interacted with the device then user activity will already
        // have been poked so the device will stay awake a bit longer.
        // If the dream is ending on its own for other reasons and no wake
        // locks are held and the user activity timeout has expired then the
        // device may simply go to sleep.
        synchronized (mLock) {
            if (mCurrentDreamToken == token) {
                stopDreamLocked(immediate);
            }
        }
    }

    private void testDreamInternal(ComponentName dream, int userId) {
        synchronized (mLock) {
            startDreamLocked(dream, true /*isTest*/, false /*canDoze*/, userId);
        }
    }

    private void startDreamInternal(boolean doze) {
        final int userId = ActivityManager.getCurrentUser();
        final ComponentName dream = chooseDreamForUser(doze, userId);
        if (dream != null) {
            synchronized (mLock) {
                startDreamLocked(dream, false /*isTest*/, doze, userId);
            }
        }
    }

    private void stopDreamInternal(boolean immediate) {
        synchronized (mLock) {
            stopDreamLocked(immediate);
        }
    }

    private void startDozingInternal(IBinder token, int screenState,
            int screenBrightness) {
        if (DEBUG) {
            Slog.d(TAG, "Dream requested to start dozing: " + token
                    + ", screenState=" + screenState
                    + ", screenBrightness=" + screenBrightness);
        }

        synchronized (mLock) {
            if (mCurrentDreamToken == token && mCurrentDreamCanDoze) {
                mCurrentDreamDozeScreenState = screenState;
                mCurrentDreamDozeScreenBrightness = screenBrightness;
                mPowerManagerInternal.setDozeOverrideFromDreamManager(
                        screenState, screenBrightness);
                if (!mCurrentDreamIsDozing) {
                    mCurrentDreamIsDozing = true;
                    mDozeWakeLock.acquire();
                }
            }
        }
    }

    private void stopDozingInternal(IBinder token) {
        if (DEBUG) {
            Slog.d(TAG, "Dream requested to stop dozing: " + token);
        }

        synchronized (mLock) {
            if (mCurrentDreamToken == token && mCurrentDreamIsDozing) {
                mCurrentDreamIsDozing = false;
                mDozeWakeLock.release();
                mPowerManagerInternal.setDozeOverrideFromDreamManager(
                        Display.STATE_UNKNOWN, PowerManager.BRIGHTNESS_DEFAULT);
            }
        }
    }

    private ComponentName chooseDreamForUser(boolean doze, int userId) {
        if (doze) {
            ComponentName dozeComponent = getDozeComponent(userId);
            return validateDream(dozeComponent) ? dozeComponent : null;
        }
        ComponentName[] dreams = getDreamComponentsForUser(userId);
        return dreams != null && dreams.length != 0 ? dreams[0] : null;
    }

    private boolean validateDream(ComponentName component) {
        if (component == null) return false;
        final ServiceInfo serviceInfo = getServiceInfo(component);
        if (serviceInfo == null) {
            Slog.w(TAG, "Dream " + component + " does not exist");
            return false;
        } else if (serviceInfo.applicationInfo.targetSdkVersion >= Build.VERSION_CODES.LOLLIPOP
                && !BIND_DREAM_SERVICE.equals(serviceInfo.permission)) {
            Slog.w(TAG, "Dream " + component
                    + " is not available because its manifest is missing the " + BIND_DREAM_SERVICE
                    + " permission on the dream service declaration.");
            return false;
        }
        return true;
    }

    private ComponentName[] getDreamComponentsForUser(int userId) {
        String names = Settings.Secure.getStringForUser(mContext.getContentResolver(),
                Settings.Secure.SCREENSAVER_COMPONENTS,
                userId);
        ComponentName[] components = componentsFromString(names);

        // first, ensure components point to valid services
        List<ComponentName> validComponents = new ArrayList<ComponentName>();
        if (components != null) {
            for (ComponentName component : components) {
                if (validateDream(component)) {
                    validComponents.add(component);
                }
            }
        }

        // fallback to the default dream component if necessary
        if (validComponents.isEmpty()) {
            ComponentName defaultDream = getDefaultDreamComponentForUser(userId);
            if (defaultDream != null) {
                Slog.w(TAG, "Falling back to default dream " + defaultDream);
                validComponents.add(defaultDream);
            }
        }
        return validComponents.toArray(new ComponentName[validComponents.size()]);
    }

    private void setDreamComponentsForUser(int userId, ComponentName[] componentNames) {
        Settings.Secure.putStringForUser(mContext.getContentResolver(),
                Settings.Secure.SCREENSAVER_COMPONENTS,
                componentsToString(componentNames),
                userId);
    }

    private ComponentName getDefaultDreamComponentForUser(int userId) {
        String name = Settings.Secure.getStringForUser(mContext.getContentResolver(),
                Settings.Secure.SCREENSAVER_DEFAULT_COMPONENT,
                userId);
        return name == null ? null : ComponentName.unflattenFromString(name);
    }

    private ComponentName getDozeComponent() {
        return getDozeComponent(ActivityManager.getCurrentUser());
    }

    private ComponentName getDozeComponent(int userId) {
        // Read the component from a system property to facilitate debugging.
        // Note that for production devices, the dream should actually be declared in
        // a config.xml resource.
        String name = Build.IS_DEBUGGABLE ? SystemProperties.get("debug.doze.component") : null;
        if (TextUtils.isEmpty(name)) {
            // Read the component from a config.xml resource.
            // The value should be specified in a resource overlay for the product.
            name = mContext.getResources().getString(
                    com.android.internal.R.string.config_dozeComponent);
        }
        boolean enabled = Settings.Secure.getIntForUser(mContext.getContentResolver(),
                Settings.Secure.DOZE_ENABLED, 1, userId) != 0;
        return TextUtils.isEmpty(name) || !enabled ? null : ComponentName.unflattenFromString(name);
    }

    private ServiceInfo getServiceInfo(ComponentName name) {
        try {
            return name != null ? mContext.getPackageManager().getServiceInfo(name,
                    PackageManager.MATCH_DEBUG_TRIAGED_MISSING) : null;
        } catch (NameNotFoundException e) {
            return null;
        }
    }

    private void startDreamLocked(final ComponentName name,
            final boolean isTest, final boolean canDoze, final int userId) {
        if (Objects.equal(mCurrentDreamName, name)
                && mCurrentDreamIsTest == isTest
                && mCurrentDreamCanDoze == canDoze
                && mCurrentDreamUserId == userId) {
            return;
        }

        stopDreamLocked(true /*immediate*/);

        Slog.i(TAG, "Entering dreamland.");

        final Binder newToken = new Binder();
        mCurrentDreamToken = newToken;
        mCurrentDreamName = name;
        mCurrentDreamIsTest = isTest;
        mCurrentDreamCanDoze = canDoze;
        mCurrentDreamUserId = userId;

        mHandler.post(new Runnable() {
            @Override
            public void run() {
                mController.startDream(newToken, name, isTest, canDoze, userId);
            }
        });
    }

    private void stopDreamLocked(final boolean immediate) {
        if (mCurrentDreamToken != null) {
            if (immediate) {
                Slog.i(TAG, "Leaving dreamland.");
                cleanupDreamLocked();
            } else if (mCurrentDreamIsWaking) {
                return; // already waking
            } else {
                Slog.i(TAG, "Gently waking up from dream.");
                mCurrentDreamIsWaking = true;
            }

            mHandler.post(new Runnable() {
                @Override
                public void run() {
                    mController.stopDream(immediate);
                }
            });
        }
    }

    private void cleanupDreamLocked() {
        mCurrentDreamToken = null;
        mCurrentDreamName = null;
        mCurrentDreamIsTest = false;
        mCurrentDreamCanDoze = false;
        mCurrentDreamUserId = 0;
        mCurrentDreamIsWaking = false;
        if (mCurrentDreamIsDozing) {
            mCurrentDreamIsDozing = false;
            mDozeWakeLock.release();
        }
        mCurrentDreamDozeScreenState = Display.STATE_UNKNOWN;
        mCurrentDreamDozeScreenBrightness = PowerManager.BRIGHTNESS_DEFAULT;
    }

    private void checkPermission(String permission) {
        if (mContext.checkCallingOrSelfPermission(permission)
                != PackageManager.PERMISSION_GRANTED) {
            throw new SecurityException("Access denied to process: " + Binder.getCallingPid()
                    + ", must have permission " + permission);
        }
    }

    private static String componentsToString(ComponentName[] componentNames) {
        StringBuilder names = new StringBuilder();
        if (componentNames != null) {
            for (ComponentName componentName : componentNames) {
                if (names.length() > 0) {
                    names.append(',');
                }
                names.append(componentName.flattenToString());
            }
        }
        return names.toString();
    }

    private static ComponentName[] componentsFromString(String names) {
        if (names == null) {
            return null;
        }
        String[] namesArray = names.split(",");
        ComponentName[] componentNames = new ComponentName[namesArray.length];
        for (int i = 0; i < namesArray.length; i++) {
            componentNames[i] = ComponentName.unflattenFromString(namesArray[i]);
        }
        return componentNames;
    }

    private final DreamController.Listener mControllerListener = new DreamController.Listener() {
        @Override
        public void onDreamStopped(Binder token) {
            synchronized (mLock) {
                if (mCurrentDreamToken == token) {
                    cleanupDreamLocked();
                }
            }
        }
    };

    /**
     * Handler for asynchronous operations performed by the dream manager.
     * Ensures operations to {@link DreamController} are single-threaded.
     */
    private final class DreamHandler extends Handler {
        public DreamHandler(Looper looper) {
            super(looper, null, true /*async*/);
        }
    }

    private final class BinderService extends IDreamManager.Stub {
        @Override // Binder call
        protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
            if (mContext.checkCallingOrSelfPermission(Manifest.permission.DUMP)
                    != PackageManager.PERMISSION_GRANTED) {
                pw.println("Permission Denial: can't dump DreamManager from from pid="
                        + Binder.getCallingPid()
                        + ", uid=" + Binder.getCallingUid());
                return;
            }

            final long ident = Binder.clearCallingIdentity();
            try {
                dumpInternal(pw);
            } finally {
                Binder.restoreCallingIdentity(ident);
            }
        }

        @Override // Binder call
        public ComponentName[] getDreamComponents() {
            checkPermission(android.Manifest.permission.READ_DREAM_STATE);

            final int userId = UserHandle.getCallingUserId();
            final long ident = Binder.clearCallingIdentity();
            try {
                return getDreamComponentsForUser(userId);
            } finally {
                Binder.restoreCallingIdentity(ident);
            }
        }

        @Override // Binder call
        public void setDreamComponents(ComponentName[] componentNames) {
            checkPermission(android.Manifest.permission.WRITE_DREAM_STATE);

            final int userId = UserHandle.getCallingUserId();
            final long ident = Binder.clearCallingIdentity();
            try {
                setDreamComponentsForUser(userId, componentNames);
            } finally {
                Binder.restoreCallingIdentity(ident);
            }
        }

        @Override // Binder call
        public ComponentName getDefaultDreamComponent() {
            checkPermission(android.Manifest.permission.READ_DREAM_STATE);

            final int userId = UserHandle.getCallingUserId();
            final long ident = Binder.clearCallingIdentity();
            try {
                return getDefaultDreamComponentForUser(userId);
            } finally {
                Binder.restoreCallingIdentity(ident);
            }
        }

        @Override // Binder call
        public boolean isDreaming() {
            checkPermission(android.Manifest.permission.READ_DREAM_STATE);

            final long ident = Binder.clearCallingIdentity();
            try {
                return isDreamingInternal();
            } finally {
                Binder.restoreCallingIdentity(ident);
            }
        }

        @Override // Binder call
        public void dream() {
            checkPermission(android.Manifest.permission.WRITE_DREAM_STATE);

            final long ident = Binder.clearCallingIdentity();
            try {
                requestDreamInternal();
            } finally {
                Binder.restoreCallingIdentity(ident);
            }
        }

        @Override // Binder call
        public void testDream(ComponentName dream) {
            if (dream == null) {
                throw new IllegalArgumentException("dream must not be null");
            }
            checkPermission(android.Manifest.permission.WRITE_DREAM_STATE);

            final int callingUserId = UserHandle.getCallingUserId();
            final int currentUserId = ActivityManager.getCurrentUser();
            if (callingUserId != currentUserId) {
                // This check is inherently prone to races but at least it's something.
                Slog.w(TAG, "Aborted attempt to start a test dream while a different "
                        + " user is active: callingUserId=" + callingUserId
                        + ", currentUserId=" + currentUserId);
                return;
            }
            final long ident = Binder.clearCallingIdentity();
            try {
                testDreamInternal(dream, callingUserId);
            } finally {
                Binder.restoreCallingIdentity(ident);
            }
        }

        @Override // Binder call
        public void awaken() {
            checkPermission(android.Manifest.permission.WRITE_DREAM_STATE);

            final long ident = Binder.clearCallingIdentity();
            try {
                requestAwakenInternal();
            } finally {
                Binder.restoreCallingIdentity(ident);
            }
        }

        @Override // Binder call
        public void finishSelf(IBinder token, boolean immediate) {
            // Requires no permission, called by Dream from an arbitrary process.
            if (token == null) {
                throw new IllegalArgumentException("token must not be null");
            }

            final long ident = Binder.clearCallingIdentity();
            try {
                finishSelfInternal(token, immediate);
            } finally {
                Binder.restoreCallingIdentity(ident);
            }
        }

        @Override // Binder call
        public void startDozing(IBinder token, int screenState, int screenBrightness) {
            // Requires no permission, called by Dream from an arbitrary process.
            if (token == null) {
                throw new IllegalArgumentException("token must not be null");
            }

            final long ident = Binder.clearCallingIdentity();
            try {
                startDozingInternal(token, screenState, screenBrightness);
            } finally {
                Binder.restoreCallingIdentity(ident);
            }
        }

        @Override // Binder call
        public void stopDozing(IBinder token) {
            // Requires no permission, called by Dream from an arbitrary process.
            if (token == null) {
                throw new IllegalArgumentException("token must not be null");
            }

            final long ident = Binder.clearCallingIdentity();
            try {
                stopDozingInternal(token);
            } finally {
                Binder.restoreCallingIdentity(ident);
            }
        }
    }

    private final class LocalService extends DreamManagerInternal {
        @Override
        public void startDream(boolean doze) {
            startDreamInternal(doze);
        }

        @Override
        public void stopDream(boolean immediate) {
            stopDreamInternal(immediate);
        }

        @Override
        public boolean isDreaming() {
            return isDreamingInternal();
        }
    }

    private final Runnable mSystemPropertiesChanged = new Runnable() {
        @Override
        public void run() {
            if (DEBUG) Slog.d(TAG, "System properties changed");
            synchronized (mLock) {
                if (mCurrentDreamName != null && mCurrentDreamCanDoze
                        && !mCurrentDreamName.equals(getDozeComponent())) {
                    // May have updated the doze component, wake up
                    mPowerManager.wakeUp(SystemClock.uptimeMillis(),
                            "android.server.dreams:SYSPROP");
                }
            }
        }
    };
}
