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

import android.annotation.IntDef;
import android.annotation.Nullable;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.hardware.display.DisplayManagerInternal;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.os.SystemClock;
import android.util.Slog;
import android.util.TimeUtils;
import android.view.Display;

import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.display.utils.SensorUtils;

import java.io.PrintWriter;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

/**
 * Maintains the proximity state of the display.
 * Internally listens for proximity updates and schedules a power state update when the proximity
 * state changes.
 */
public final class DisplayPowerProximityStateController {
    @VisibleForTesting
    static final int PROXIMITY_UNKNOWN = -1;
    private static final int PROXIMITY_NEGATIVE = 0;
    @VisibleForTesting
    static final int PROXIMITY_POSITIVE = 1;

    @IntDef(prefix = { "PROXIMITY_" }, value = {
            PROXIMITY_UNKNOWN,
            PROXIMITY_NEGATIVE,
            PROXIMITY_POSITIVE
    })
    @Retention(RetentionPolicy.SOURCE)
    @interface ProximityState {}

    @VisibleForTesting
    static final int MSG_PROXIMITY_SENSOR_DEBOUNCED = 1;
    @VisibleForTesting
    static final int PROXIMITY_SENSOR_POSITIVE_DEBOUNCE_DELAY = 0;

    private static final int MSG_IGNORE_PROXIMITY = 2;

    private static final boolean DEBUG_PRETEND_PROXIMITY_SENSOR_ABSENT = false;
    // Proximity sensor debounce delay in milliseconds for positive transitions.

    // Proximity sensor debounce delay in milliseconds for negative transitions.
    private static final int PROXIMITY_SENSOR_NEGATIVE_DEBOUNCE_DELAY = 250;
    // Trigger proximity if distance is less than 5 cm.
    private static final float TYPICAL_PROXIMITY_THRESHOLD = 5.0f;

    private final String mTag;
    // A lock to handle the deadlock and race conditions.
    private final Object mLock = new Object();
    // The manager which lets us access the device's ProximitySensor
    private final SensorManager mSensorManager;
    // An entity which manages the wakelocks.
    private final WakelockController mWakelockController;
    // A handler to process all the events on this thread in a synchronous manner
    private final DisplayPowerProximityStateHandler mHandler;
    // A runnable to execute the utility to update the power state.
    private final Runnable mNudgeUpdatePowerState;
    private final Clock mClock;
    // A listener which listen's to the events emitted by the proximity sensor.
    private final SensorEventListener mProximitySensorListener = new SensorEventListener() {
        @Override
        public void onSensorChanged(SensorEvent event) {
            if (mProximitySensorEnabled) {
                final long time = mClock.uptimeMillis();
                final float distance = event.values[0];
                boolean positive = distance >= 0.0f && distance < mProximityThreshold;
                handleProximitySensorEvent(time, positive);
            }
        }

        @Override
        public void onAccuracyChanged(Sensor sensor, int accuracy) {
            // Not used.
        }
    };

    // The proximity sensor, or null if not available or needed.
    private Sensor mProximitySensor;

    // The configurations for the associated display
    private DisplayDeviceConfig mDisplayDeviceConfig;

    // True if a request has been made to wait for the proximity sensor to go negative.
    @GuardedBy("mLock")
    private boolean mPendingWaitForNegativeProximityLocked;

    // True if the device should wait for negative proximity sensor before
    // waking up the screen.  This is set to false as soon as a negative
    // proximity sensor measurement is observed or when the device is forced to
    // go to sleep by the user.  While true, the screen remains off.
    private boolean mWaitingForNegativeProximity;

    // True if the device should not take into account the proximity sensor
    // until either the proximity sensor state changes, or there is no longer a
    // request to listen to proximity sensor.
    private boolean mIgnoreProximityUntilChanged;

    // Set to true if the proximity sensor listener has been registered
    // with the sensor manager.
    private boolean mProximitySensorEnabled;

    // -1 if fully debounced. Else, represents the time in ms when the debounce suspend blocker will
    // be removed. Applies for both positive and negative proximity flips.
    private long mPendingProximityDebounceTime = -1;

    // True if the screen was turned off because of the proximity sensor.
    // When the screen turns on again, we report user activity to the power manager.
    private boolean mScreenOffBecauseOfProximity;

    // The raw non-debounced proximity sensor state.
    private @ProximityState int mPendingProximity = PROXIMITY_UNKNOWN;

    // The debounced proximity sensor state.
    private @ProximityState int mProximity = PROXIMITY_UNKNOWN;

    // The actual proximity sensor threshold value.
    private float mProximityThreshold;

    // A flag representing if the ramp is to be skipped when the proximity changes from positive
    // to negative
    private boolean mSkipRampBecauseOfProximityChangeToNegative = false;

    // The DisplayId of the associated Logical Display.
    private final int mDisplayId;

    /**
     * Create a new instance of DisplayPowerProximityStateController.
     *
     * @param wakeLockController    WakelockController used to acquire/release wakelocks
     * @param displayDeviceConfig   DisplayDeviceConfig instance from which the configs(Proximity
     *                              Sensor) are to be loaded
     * @param looper                A looper onto which the handler is to be associated.
     * @param nudgeUpdatePowerState A runnable to execute the utility to update the power state
     * @param displayId             The DisplayId of the associated Logical Display.
     * @param sensorManager         The manager which lets us access the display's ProximitySensor
     */
    public DisplayPowerProximityStateController(WakelockController wakeLockController,
            DisplayDeviceConfig displayDeviceConfig, Looper looper,
            Runnable nudgeUpdatePowerState, int displayId, SensorManager sensorManager) {
        this(wakeLockController, displayDeviceConfig, looper, nudgeUpdatePowerState, displayId,
                sensorManager, new Injector());
    }

    @VisibleForTesting
    DisplayPowerProximityStateController(WakelockController wakeLockController,
            DisplayDeviceConfig displayDeviceConfig, Looper looper,
            Runnable nudgeUpdatePowerState, int displayId, SensorManager sensorManager,
            @Nullable Injector injector) {
        if (injector == null) {
            injector = new Injector();
        }
        mClock = injector.createClock();
        mWakelockController = wakeLockController;
        mHandler = new DisplayPowerProximityStateHandler(looper);
        mNudgeUpdatePowerState = nudgeUpdatePowerState;
        mDisplayDeviceConfig = displayDeviceConfig;
        mDisplayId = displayId;
        mTag = "DisplayPowerProximityStateController[" + mDisplayId + "]";
        mSensorManager = sensorManager;
        loadProximitySensor();
    }

    /**
     * Manages the pending state of the proximity.
     */
    public void updatePendingProximityRequestsLocked() {
        synchronized (mLock) {
            mWaitingForNegativeProximity |= mPendingWaitForNegativeProximityLocked;
            mPendingWaitForNegativeProximityLocked = false;

            if (mIgnoreProximityUntilChanged) {
                // Also, lets stop waiting for negative proximity if we're ignoring it.
                mWaitingForNegativeProximity = false;
            }
        }
    }

    /**
     * Clean up all resources that are accessed via the {@link #mHandler} thread.
     */
    public void cleanup() {
        setProximitySensorEnabled(false);
    }

    /**
     * Returns true if the proximity sensor screen-off function is available.
     */
    public boolean isProximitySensorAvailable() {
        return mProximitySensor != null;
    }

    /**
     * Sets the flag to indicate that the system is waiting for the negative proximity event
     */
    public boolean setPendingWaitForNegativeProximityLocked(
            boolean requestWaitForNegativeProximity) {
        synchronized (mLock) {
            if (requestWaitForNegativeProximity
                    && !mPendingWaitForNegativeProximityLocked) {
                mPendingWaitForNegativeProximityLocked = true;
                return true;
            }
            return false;
        }
    }

    /**
     * Updates the proximity state of the display, based on the newly received DisplayPowerRequest
     * and the target display state
     */
    public void updateProximityState(
            DisplayManagerInternal.DisplayPowerRequest displayPowerRequest,
            int displayState) {
        mSkipRampBecauseOfProximityChangeToNegative = false;
        if (mProximitySensor != null) {
            if (displayPowerRequest.useProximitySensor && displayState != Display.STATE_OFF) {
                // At this point the policy says that the screen should be on, but we've been
                // asked to listen to the prox sensor to adjust the display state, so lets make
                // sure the sensor is on.
                setProximitySensorEnabled(true);
                if (!mScreenOffBecauseOfProximity
                        && mProximity == PROXIMITY_POSITIVE
                        && !mIgnoreProximityUntilChanged) {
                    // Prox sensor already reporting "near" so we should turn off the screen.
                    // Also checked that we aren't currently set to ignore the proximity sensor
                    // temporarily.
                    mScreenOffBecauseOfProximity = true;
                    sendOnProximityPositiveWithWakelock();
                }
            } else if (mWaitingForNegativeProximity
                    && mScreenOffBecauseOfProximity
                    && mProximity == PROXIMITY_POSITIVE
                    && displayState != Display.STATE_OFF) {
                // The policy says that we should have the screen on, but it's off due to the prox
                // and we've been asked to wait until the screen is far from the user to turn it
                // back on. Let keep the prox sensor on so we can tell when it's far again.
                setProximitySensorEnabled(true);
            } else {
                // We haven't been asked to use the prox sensor and we're not waiting on the screen
                // to turn back on...so let's shut down the prox sensor.
                setProximitySensorEnabled(false);
                mWaitingForNegativeProximity = false;
            }
            if (mScreenOffBecauseOfProximity
                    && (mProximity != PROXIMITY_POSITIVE || mIgnoreProximityUntilChanged)) {
                // The screen *was* off due to prox being near, but now it's "far" so lets turn
                // the screen back on.  Also turn it back on if we've been asked to ignore the
                // prox sensor temporarily.
                mScreenOffBecauseOfProximity = false;
                mSkipRampBecauseOfProximityChangeToNegative = true;
                sendOnProximityNegativeWithWakelock();
            }
        } else {
            setProximitySensorEnabled(false);
            mWaitingForNegativeProximity = false;
            mIgnoreProximityUntilChanged = false;

            if (mScreenOffBecauseOfProximity) {
                // The screen *was* off due to prox being near, but now there's no prox sensor, so
                // let's turn the screen back on.
                mScreenOffBecauseOfProximity = false;
                mSkipRampBecauseOfProximityChangeToNegative = true;
                sendOnProximityNegativeWithWakelock();
            }
        }
    }

    /**
     * A utility to check if the brightness change ramp is to be skipped because the proximity was
     * changed from positive to negative.
     */
    public boolean shouldSkipRampBecauseOfProximityChangeToNegative() {
        return mSkipRampBecauseOfProximityChangeToNegative;
    }

    /**
     * Represents of the screen is currently turned off because of the proximity state.
     */
    public boolean isScreenOffBecauseOfProximity() {
        return mScreenOffBecauseOfProximity;
    }

    /**
     * Ignores the proximity sensor until the sensor state changes, but only if the sensor is
     * currently enabled and forcing the screen to be dark.
     */
    public void ignoreProximitySensorUntilChanged() {
        mHandler.sendEmptyMessage(MSG_IGNORE_PROXIMITY);
    }

    /**
     * This adjusts the state of this class when a change in the DisplayDevice is detected.
     */
    public void notifyDisplayDeviceChanged(DisplayDeviceConfig displayDeviceConfig) {
        this.mDisplayDeviceConfig = displayDeviceConfig;
        loadProximitySensor();
    }

    /**
     * Used to dump the state.
     *
     * @param pw The PrintWriter used to dump the state.
     */
    public void dumpLocal(PrintWriter pw) {
        pw.println();
        pw.println("DisplayPowerProximityStateController:");
        pw.println("-------------------------------------");
        synchronized (mLock) {
            pw.println("  mPendingWaitForNegativeProximityLocked="
                    + mPendingWaitForNegativeProximityLocked);
        }
        pw.println("  mDisplayId=" + mDisplayId);
        pw.println("  mWaitingForNegativeProximity=" + mWaitingForNegativeProximity);
        pw.println("  mIgnoreProximityUntilChanged=" + mIgnoreProximityUntilChanged);
        pw.println("  mProximitySensor=" + mProximitySensor);
        pw.println("  mProximitySensorEnabled=" + mProximitySensorEnabled);
        pw.println("  mProximityThreshold=" + mProximityThreshold);
        pw.println("  mProximity=" + proximityToString(mProximity));
        pw.println("  mPendingProximity=" + proximityToString(mPendingProximity));
        pw.println("  mPendingProximityDebounceTime="
                + TimeUtils.formatUptime(mPendingProximityDebounceTime));
        pw.println("  mScreenOffBecauseOfProximity=" + mScreenOffBecauseOfProximity);
        pw.println("  mSkipRampBecauseOfProximityChangeToNegative="
                + mSkipRampBecauseOfProximityChangeToNegative);
    }

    void ignoreProximitySensorUntilChangedInternal() {
        if (!mIgnoreProximityUntilChanged
                && mProximity == PROXIMITY_POSITIVE) {
            // Only ignore if it is still reporting positive (near)
            mIgnoreProximityUntilChanged = true;
            Slog.i(mTag, "Ignoring proximity");
            mNudgeUpdatePowerState.run();
        }
    }

    private void sendOnProximityPositiveWithWakelock() {
        mWakelockController.acquireWakelock(WakelockController.WAKE_LOCK_PROXIMITY_POSITIVE);
        mHandler.post(mWakelockController.getOnProximityPositiveRunnable());
    }

    private void sendOnProximityNegativeWithWakelock() {
        mWakelockController.acquireWakelock(WakelockController.WAKE_LOCK_PROXIMITY_NEGATIVE);
        mHandler.post(mWakelockController.getOnProximityNegativeRunnable());
    }

    private void loadProximitySensor() {
        if (DEBUG_PRETEND_PROXIMITY_SENSOR_ABSENT || mDisplayId != Display.DEFAULT_DISPLAY) {
            return;
        }
        mProximitySensor = SensorUtils.findSensor(mSensorManager,
                mDisplayDeviceConfig.getProximitySensor(), Sensor.TYPE_PROXIMITY);
        if (mProximitySensor != null) {
            mProximityThreshold = Math.min(mProximitySensor.getMaximumRange(),
                    TYPICAL_PROXIMITY_THRESHOLD);
        }
    }

    private void setProximitySensorEnabled(boolean enable) {
        if (enable) {
            if (!mProximitySensorEnabled) {
                // Register the listener.
                // Proximity sensor state already cleared initially.
                mProximitySensorEnabled = true;
                mIgnoreProximityUntilChanged = false;
                mSensorManager.registerListener(mProximitySensorListener, mProximitySensor,
                        SensorManager.SENSOR_DELAY_NORMAL, mHandler);
            }
        } else {
            if (mProximitySensorEnabled) {
                // Unregister the listener.
                // Clear the proximity sensor state for next time.
                mProximitySensorEnabled = false;
                mProximity = PROXIMITY_UNKNOWN;
                mIgnoreProximityUntilChanged = false;
                mPendingProximity = PROXIMITY_UNKNOWN;
                mHandler.removeMessages(MSG_PROXIMITY_SENSOR_DEBOUNCED);
                mSensorManager.unregisterListener(mProximitySensorListener);
                // release wake lock(must be last)
                boolean proxDebounceSuspendBlockerReleased =
                        mWakelockController.releaseWakelock(
                                WakelockController.WAKE_LOCK_PROXIMITY_DEBOUNCE);
                if (proxDebounceSuspendBlockerReleased) {
                    mPendingProximityDebounceTime = -1;
                }
            }
        }
    }

    private void handleProximitySensorEvent(long time, boolean positive) {
        if (mProximitySensorEnabled) {
            if (mPendingProximity == PROXIMITY_NEGATIVE && !positive) {
                return; // no change
            }
            if (mPendingProximity == PROXIMITY_POSITIVE && positive) {
                return; // no change
            }

            // Only accept a proximity sensor reading if it remains
            // stable for the entire debounce delay.  We hold a wake lock while
            // debouncing the sensor.
            mHandler.removeMessages(MSG_PROXIMITY_SENSOR_DEBOUNCED);
            if (positive) {
                mPendingProximity = PROXIMITY_POSITIVE;
                mPendingProximityDebounceTime = time + PROXIMITY_SENSOR_POSITIVE_DEBOUNCE_DELAY;
                mWakelockController.acquireWakelock(
                        WakelockController.WAKE_LOCK_PROXIMITY_DEBOUNCE); // acquire wake lock
            } else {
                mPendingProximity = PROXIMITY_NEGATIVE;
                mPendingProximityDebounceTime = time + PROXIMITY_SENSOR_NEGATIVE_DEBOUNCE_DELAY;
                mWakelockController.acquireWakelock(
                        WakelockController.WAKE_LOCK_PROXIMITY_DEBOUNCE); // acquire wake lock
            }

            // Debounce the new sensor reading.
            debounceProximitySensor();
        }
    }

    private void debounceProximitySensor() {
        if (mProximitySensorEnabled
                && mPendingProximity != PROXIMITY_UNKNOWN
                && mPendingProximityDebounceTime >= 0) {
            final long now = mClock.uptimeMillis();
            if (mPendingProximityDebounceTime <= now) {
                if (mProximity != mPendingProximity) {
                    // if the status of the sensor changed, stop ignoring.
                    mIgnoreProximityUntilChanged = false;
                    Slog.i(mTag, "Applying proximity: " + proximityToString(mPendingProximity));
                }
                // Sensor reading accepted.  Apply the change then release the wake lock.
                mProximity = mPendingProximity;
                mNudgeUpdatePowerState.run();
                // (must be last)
                boolean proxDebounceSuspendBlockerReleased =
                        mWakelockController.releaseWakelock(
                                WakelockController.WAKE_LOCK_PROXIMITY_DEBOUNCE);
                if (proxDebounceSuspendBlockerReleased) {
                    mPendingProximityDebounceTime = -1;
                }

            } else {
                // Need to wait a little longer.
                // Debounce again later.  We continue holding a wake lock while waiting.
                Message msg = mHandler.obtainMessage(MSG_PROXIMITY_SENSOR_DEBOUNCED);
                mHandler.sendMessageAtTime(msg, mPendingProximityDebounceTime);
            }
        }
    }

    private class DisplayPowerProximityStateHandler extends Handler {
        DisplayPowerProximityStateHandler(Looper looper) {
            super(looper, null, true /*async*/);
        }

        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case MSG_PROXIMITY_SENSOR_DEBOUNCED:
                    debounceProximitySensor();
                    break;

                case MSG_IGNORE_PROXIMITY:
                    ignoreProximitySensorUntilChangedInternal();
                    break;
            }
        }
    }

    private String proximityToString(@ProximityState int state) {
        switch (state) {
            case PROXIMITY_UNKNOWN:
                return "Unknown";
            case PROXIMITY_NEGATIVE:
                return "Negative";
            case PROXIMITY_POSITIVE:
                return "Positive";
            default:
                return Integer.toString(state);
        }
    }

    @VisibleForTesting
    boolean getPendingWaitForNegativeProximityLocked() {
        synchronized (mLock) {
            return mPendingWaitForNegativeProximityLocked;
        }
    }

    @VisibleForTesting
    boolean getWaitingForNegativeProximity() {
        return mWaitingForNegativeProximity;
    }

    @VisibleForTesting
    boolean shouldIgnoreProximityUntilChanged() {
        return mIgnoreProximityUntilChanged;
    }

    boolean isProximitySensorEnabled() {
        return mProximitySensorEnabled;
    }

    @VisibleForTesting
    Handler getHandler() {
        return mHandler;
    }

    @VisibleForTesting
    @ProximityState int getPendingProximity() {
        return mPendingProximity;
    }

    @VisibleForTesting
    @ProximityState int getProximity() {
        return mProximity;
    }


    @VisibleForTesting
    long getPendingProximityDebounceTime() {
        return mPendingProximityDebounceTime;
    }

    @VisibleForTesting
    SensorEventListener getProximitySensorListener() {
        return mProximitySensorListener;
    }

    /** Functional interface for providing time. */
    @VisibleForTesting
    interface Clock {
        /**
         * Returns current time in milliseconds since boot, not counting time spent in deep sleep.
         */
        long uptimeMillis();
    }

    @VisibleForTesting
    static class Injector {
        Clock createClock() {
            return SystemClock::uptimeMillis;
        }
    }
}
