/*
 * Copyright (C) 2020 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 android.hardware.input;

import android.annotation.NonNull;
import android.hardware.HardwareBuffer;
import android.hardware.Sensor;
import android.hardware.SensorAdditionalInfo;
import android.hardware.SensorDirectChannel;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.hardware.TriggerEventListener;
import android.os.Build;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.os.MemoryFile;
import android.os.Message;
import android.os.RemoteException;
import android.util.Slog;
import android.util.SparseArray;
import android.view.InputDevice;

import com.android.internal.annotations.GuardedBy;
import com.android.internal.os.SomeArgs;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * Sensor manager implementation that communicates with the input device
 * sensors.
 * @hide
 */
public class InputDeviceSensorManager implements InputManager.InputDeviceListener {
    private static final String TAG = "InputDeviceSensorManager";
    private static final boolean DEBUG = false;

    private static final int MSG_SENSOR_ACCURACY_CHANGED = 1;
    private static final int MSG_SENSOR_CHANGED = 2;

    private InputManager mInputManager;

    // sensor map from device id to sensor list
    @GuardedBy("mInputSensorLock")
    private final Map<Integer, List<Sensor>> mSensors = new HashMap<>();

    private final Object mInputSensorLock = new Object();
    private InputSensorEventListener mInputServiceSensorListener;
    @GuardedBy("mInputSensorLock")
    private final ArrayList<InputSensorEventListenerDelegate> mInputSensorEventListeners =
            new ArrayList<InputSensorEventListenerDelegate>();
    private final HandlerThread mSensorThread;
    private final Handler mSensorHandler;

    public InputDeviceSensorManager(InputManager inputManager) {
        mInputManager = inputManager;

        mSensorThread = new HandlerThread("SensorThread");
        mSensorThread.start();
        mSensorHandler = new Handler(mSensorThread.getLooper());

        // Register the input device listener
        mInputManager.registerInputDeviceListener(this, mSensorHandler);
        // Initialize the sensor list
        initializeSensors();
    }

    /*
     * Get SensorManager object for specific input device
     *
     * @param deviceId Input device ID
     * @return SensorManager object for input device
     */
    SensorManager getSensorManager(int deviceId) {
        return new InputSensorManager(deviceId);
    }

    /*
     * Update input device sensor info for specified input device ID.
     */
    private void updateInputDeviceSensorInfoLocked(int deviceId) {
        final InputDevice inputDevice = InputDevice.getDevice(deviceId);
        if (inputDevice != null && inputDevice.hasSensor()) {
            final InputSensorInfo[] sensorInfos =
                    mInputManager.getSensorList(deviceId);
            populateSensorsForInputDeviceLocked(deviceId, sensorInfos);
        }
    }

    @Override
    public void onInputDeviceAdded(int deviceId) {
        synchronized (mInputSensorLock) {
            if (!mSensors.containsKey(deviceId)) {
                updateInputDeviceSensorInfoLocked(deviceId);
            } else {
                Slog.e(TAG, "Received 'device added' notification for device " + deviceId
                        + ", but it is already in the list");
            }
        }
    }

    @Override
    public void onInputDeviceRemoved(int deviceId) {
        synchronized (mInputSensorLock) {
            mSensors.remove(deviceId);
        }
    }

    @Override
    public void onInputDeviceChanged(int deviceId) {
        synchronized (mInputSensorLock) {
            mSensors.remove(deviceId);
            updateInputDeviceSensorInfoLocked(deviceId);
        }
    }

    private static boolean sensorEquals(@NonNull Sensor lhs, @NonNull Sensor rhs) {
        if (lhs.getType() == rhs.getType() && lhs.getId() == rhs.getId()) {
            return true;
        }
        return false;
    }

    private void populateSensorsForInputDeviceLocked(int deviceId, InputSensorInfo[] sensorInfos) {
        List<Sensor> sensors = new ArrayList<Sensor>();
        for (int i = 0; i < sensorInfos.length; i++) {
            Sensor sensor = new Sensor(sensorInfos[i]);
            if (DEBUG) {
                Slog.d(TAG, "Device " + deviceId + " sensor " + sensor.getStringType() + " added");
            }
            sensors.add(sensor);
        }
        mSensors.put(deviceId, sensors);
    }

    private void initializeSensors() {
        synchronized (mInputSensorLock) {
            mSensors.clear();
            int[] deviceIds = mInputManager.getInputDeviceIds();
            for (int i = 0; i < deviceIds.length; i++) {
                final int deviceId = deviceIds[i];
                updateInputDeviceSensorInfoLocked(deviceId);
            }
        }
    }

    /**
     * Get a sensor object for input device, with specific sensor type.
     * @param deviceId The input devicd ID
     * @param sensorType The sensor type
     * @return The sensor object if exists or null
     */
    @GuardedBy("mInputSensorLock")
    private Sensor getInputDeviceSensorLocked(int deviceId, int sensorType) {
        List<Sensor> sensors = mSensors.get(deviceId);
        for (Sensor sensor : sensors) {
            if (sensor.getType() == sensorType) {
                return sensor;
            }
        }
        return null;
    }

    @GuardedBy("mInputSensorLock")
    private int findSensorEventListenerLocked(SensorEventListener listener) {
        for (int i = 0; i < mInputSensorEventListeners.size(); i++) {
            if (mInputSensorEventListeners.get(i).getListener() == listener) {
                return i;
            }
        }
        return Integer.MIN_VALUE;
    }

    private void onInputSensorChanged(int deviceId, int sensorType, int accuracy, long timestamp,
            float[] values) {
        if (DEBUG) {
            Slog.d(TAG, "Sensor changed: deviceId =" + deviceId
                    + " timestamp=" + timestamp + " sensorType=" + sensorType);
        }
        synchronized (mInputSensorLock) {
            Sensor sensor = getInputDeviceSensorLocked(deviceId, sensorType);
            for (int i = 0; i < mInputSensorEventListeners.size(); i++) {
                InputSensorEventListenerDelegate listener =
                        mInputSensorEventListeners.get(i);
                if (listener.hasSensorRegistered(deviceId, sensorType)) {
                    SensorEvent event = listener.getSensorEvent(sensor);
                    if (event == null) {
                        Slog.wtf(TAG, "Failed to get SensorEvent.");
                        return;
                    }
                    event.sensor = sensor;
                    event.accuracy = accuracy;
                    event.timestamp = timestamp;
                    System.arraycopy(values, 0, event.values, 0, event.values.length);
                    // Call listener for sensor changed
                    listener.sendSensorChanged(event);
                }
            }
        }
    }

    private void onInputSensorAccuracyChanged(int deviceId, int sensorType, int accuracy) {
        if (DEBUG) {
            Slog.d(TAG, "Sensor accuracy changed: "
                    + "accuracy=" + accuracy + ", sensorType=" + sensorType);
        }
        synchronized (mInputSensorLock) {
            for (int i = 0; i < mInputSensorEventListeners.size(); i++) {
                InputSensorEventListenerDelegate listener =
                        mInputSensorEventListeners.get(i);
                if (listener.hasSensorRegistered(deviceId, sensorType)) {
                    listener.sendSensorAccuracyChanged(deviceId, sensorType, accuracy);
                }
            }
        }
    }

    private final class InputSensorEventListener extends IInputSensorEventListener.Stub {
        @Override
        public void onInputSensorChanged(int deviceId, int sensorType, int accuracy, long timestamp,
                float[] values) throws RemoteException {
            InputDeviceSensorManager.this.onInputSensorChanged(
                    deviceId, sensorType, accuracy, timestamp, values);
        }

        @Override
        public void onInputSensorAccuracyChanged(int deviceId, int sensorType, int accuracy)
                throws RemoteException {
            InputDeviceSensorManager.this.onInputSensorAccuracyChanged(deviceId, sensorType,
                    accuracy);
        }

    }

    private static final class InputSensorEventListenerDelegate extends Handler {
        private final SensorEventListener mListener;
        private final int mDelayUs;
        private final int mMaxBatchReportLatencyUs;
        // List of sensors being listened to
        private List<Sensor> mSensors = new ArrayList<Sensor>();
        // Sensor event array by sensor type, preallocate sensor events for each sensor of listener
        // to avoid allocation and garbage collection for each listener callback.
        private final SparseArray<SensorEvent> mSensorEvents = new SparseArray<SensorEvent>();

        InputSensorEventListenerDelegate(SensorEventListener listener, Sensor sensor,
                int delayUs, int maxBatchReportLatencyUs, Handler handler) {
            super(handler != null ? handler.getLooper() : Looper.myLooper());
            mListener = listener;
            mDelayUs = delayUs;
            mMaxBatchReportLatencyUs = maxBatchReportLatencyUs;
            addSensor(sensor);
        }

        public List<Sensor> getSensors() {
            return mSensors;
        }

        public boolean isEmpty() {
            return mSensors.isEmpty();
        }

        /**
         * Remove sensor from sensor list for listener
         */
        public void removeSensor(Sensor sensor) {
            // If sensor is not specified the listener will be unregistered for all sensors
            // and the sensor list is cleared.
            if (sensor == null) {
                mSensors.clear();
                mSensorEvents.clear();
            }
            for (Sensor s : mSensors) {
                if (sensorEquals(s, sensor)) {
                    mSensors.remove(sensor);
                    mSensorEvents.remove(sensor.getType());
                }
            }
        }

        /**
         * Add a sensor to listener's sensor list
         */
        public void addSensor(@NonNull Sensor sensor) {
            for (Sensor s : mSensors) {
                if (sensorEquals(s, sensor)) {
                    Slog.w(TAG, "Adding sensor " + sensor + " already exist!");
                    return;
                }
            }
            mSensors.add(sensor);
            final int vecLength = sensor.getMaxLengthValuesArray(sensor, Build.VERSION.SDK_INT);
            SensorEvent event = new SensorEvent(sensor, SensorManager.SENSOR_STATUS_NO_CONTACT,
                    0 /* timestamp */, new float[vecLength]);
            mSensorEvents.put(sensor.getType(), event);
        }

        /**
         * Check if the listener has been registered to the sensor
         * @param deviceId The input device ID of the sensor
         * @param sensorType The sensor type of the sensor
         * @return true if specified sensor is registered for the listener.
         */
        public boolean hasSensorRegistered(int deviceId, int sensorType) {
            for (Sensor sensor : mSensors) {
                if (sensor.getType() == sensorType && sensor.getId() == deviceId) {
                    return true;
                }
            }
            return false;
        }

        /**
         * Get listener handle for the delegate
         */
        public SensorEventListener getListener() {
            return mListener;
        }

        /**
         * Get SensorEvent object for input device, with specified sensor.
         */
        private SensorEvent getSensorEvent(@NonNull Sensor sensor) {
            return mSensorEvents.get(sensor.getType());
        }

        /**
         * Send sensor changed message
         */
        public void sendSensorChanged(SensorEvent event) {
            SomeArgs args = SomeArgs.obtain();
            obtainMessage(MSG_SENSOR_CHANGED, event).sendToTarget();
        }

        /**
         * Send sensor accuracy changed message
         */
        public void sendSensorAccuracyChanged(int deviceId, int sensorType, int accuracy) {
            SomeArgs args = SomeArgs.obtain();
            obtainMessage(MSG_SENSOR_ACCURACY_CHANGED, deviceId, sensorType, accuracy)
                    .sendToTarget();
        }

        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case MSG_SENSOR_ACCURACY_CHANGED: {
                    final int deviceId = msg.arg1;
                    final int sensorType = msg.arg2;
                    final int accuracy = (int) msg.obj;
                    for (Sensor sensor : mSensors) {
                        if (sensor.getId() == deviceId && sensor.getType() == sensorType) {
                            mListener.onAccuracyChanged(sensor, accuracy);
                        }
                    }
                    break;
                }
                case MSG_SENSOR_CHANGED: {
                    SensorEvent event = (SensorEvent) msg.obj;
                    mListener.onSensorChanged(event);
                    break;
                }
            }
        }
    }

    /**
     * Return the default sensor object for input device, for specific sensor type.
     */
    private Sensor getSensorForInputDevice(int deviceId, int type) {
        synchronized (mInputSensorLock) {
            for (Map.Entry<Integer, List<Sensor>> entry : mSensors.entrySet()) {
                for (Sensor sensor : entry.getValue()) {
                    if (sensor.getId() == deviceId && sensor.getType() == type) {
                        if (DEBUG) {
                            Slog.d(TAG, "Device " + deviceId + " sensor " + sensor.getStringType());
                        }
                        return sensor;
                    }
                }
            }
        }
        return null;
    }

    /**
     * Return list of sensors that belong to an input device, specified by input device ID.
     */
    private List<Sensor> getFullSensorListForDevice(int deviceId) {
        List<Sensor> sensors = new ArrayList<Sensor>();
        synchronized (mInputSensorLock) {
            for (Map.Entry<Integer, List<Sensor>> entry : mSensors.entrySet()) {
                for (Sensor sensor : entry.getValue()) {
                    if (sensor.getId() == deviceId) {
                        if (DEBUG) {
                            Slog.d(TAG, "Device " + deviceId + " sensor " + sensor.getStringType());
                        }
                        sensors.add(sensor);
                    }
                }
            }
        }
        return sensors;
    }

    private boolean registerListenerInternal(SensorEventListener listener, Sensor sensor,
            int delayUs, int maxBatchReportLatencyUs, Handler handler) {
        if (DEBUG) {
            Slog.d(TAG, "registerListenerImpl listener=" + listener + " sensor=" + sensor
                    + " delayUs=" + delayUs
                    + " maxBatchReportLatencyUs=" + maxBatchReportLatencyUs);
        }
        if (listener == null) {
            Slog.e(TAG, "listener is null");
            return false;
        }

        if (sensor == null) {
            Slog.e(TAG, "sensor is null");
            return false;
        }

        // Trigger Sensors should use the requestTriggerSensor call.
        if (sensor.getReportingMode() == Sensor.REPORTING_MODE_ONE_SHOT) {
            Slog.e(TAG, "Trigger Sensors should use the requestTriggerSensor.");
            return false;
        }
        if (maxBatchReportLatencyUs < 0 || delayUs < 0) {
            Slog.e(TAG, "maxBatchReportLatencyUs and delayUs should be non-negative");
            return false;
        }

        if (getSensorForInputDevice(sensor.getId(), sensor.getType()) != null) {
            synchronized (mInputSensorLock) {
                final int deviceId = sensor.getId();
                InputDevice inputDevice = InputDevice.getDevice(deviceId);
                if (!inputDevice.hasSensor()) {
                    Slog.e(TAG, "The device doesn't have the sensor:" + sensor);
                    return false;
                }
                if (!mInputManager.enableSensor(deviceId, sensor.getType(), delayUs,
                        maxBatchReportLatencyUs)) {
                    Slog.e(TAG, "Can't enable the sensor:" + sensor);
                    return false;
                }
            }
        }

        synchronized (mInputSensorLock) {
            // Register the InputManagerService sensor listener if not yet.
            if (mInputServiceSensorListener == null) {
                mInputServiceSensorListener = new InputSensorEventListener();
                if (!mInputManager.registerSensorListener(mInputServiceSensorListener)) {
                    Slog.e(TAG, "Failed registering the sensor listener");
                    return false;
                }
            }

            int idx = findSensorEventListenerLocked(listener);
            if (idx < 0) {
                InputSensorEventListenerDelegate d =
                        new InputSensorEventListenerDelegate(listener, sensor, delayUs,
                                maxBatchReportLatencyUs,
                                handler == null ? mSensorHandler : handler);
                mInputSensorEventListeners.add(d);
            } else {
                // The listener is already registered, see if it wants to listen to more sensors.
                mInputSensorEventListeners.get(idx).addSensor(sensor);
            }
        }

        return true;
    }

    private void unregisterListenerInternal(SensorEventListener listener, Sensor sensor) {
        if (DEBUG) {
            Slog.d(TAG, "unregisterListenerImpl listener=" + listener + " sensor=" + sensor);
        }
        if (listener == null) {  // it's OK for the sensor to be null
            throw new IllegalArgumentException("listener must not be null");
        }
        synchronized (mInputSensorLock) {
            int idx = findSensorEventListenerLocked(listener);
            // Track the sensor types and the device Id the listener has registered.
            final List<Sensor> sensorsRegistered;
            if (idx >= 0) {
                InputSensorEventListenerDelegate delegate =
                        mInputSensorEventListeners.get(idx);
                sensorsRegistered = new ArrayList<Sensor>(delegate.getSensors());
                // Get the sensor types the listener is listening to
                delegate.removeSensor(sensor);
                if (delegate.isEmpty()) {
                    // If no sensors to listen, remove the listener delegate
                    mInputSensorEventListeners.remove(idx);
                }
            } else {
                Slog.e(TAG, "Listener is not registered");
                return;
            }
            // If no delegation remains, unregister the listener to input service
            if (mInputServiceSensorListener != null && mInputSensorEventListeners.size() == 0) {
                mInputManager.unregisterSensorListener(mInputServiceSensorListener);
                mInputServiceSensorListener = null;
            }
            // For each sensor type check if it is still in use by other listeners.
            for (Sensor s : sensorsRegistered) {
                final int deviceId = s.getId();
                final int sensorType = s.getType();
                // See if we can disable the sensor
                boolean enableSensor = false;
                for (int i = 0; i < mInputSensorEventListeners.size(); i++) {
                    InputSensorEventListenerDelegate delegate =
                            mInputSensorEventListeners.get(i);
                    if (delegate.hasSensorRegistered(deviceId, sensorType)) {
                        enableSensor = true;
                        Slog.w(TAG, "device " + deviceId + " still uses sensor " + sensorType);
                        break;
                    }
                }
                // Sensor is not listened, disable it.
                if (!enableSensor) {
                    if (DEBUG) {
                        Slog.d(TAG, "device " + deviceId + " sensor " + sensorType + " disabled");
                    }
                    mInputManager.disableSensor(deviceId, sensorType);
                }
            }
        }
    }

    private boolean flush(SensorEventListener listener) {
        synchronized (mInputSensorLock) {
            int idx = findSensorEventListenerLocked(listener);
            if (idx < 0) {
                return false;
            }
            for (Sensor sensor : mInputSensorEventListeners.get(idx).getSensors()) {
                final int deviceId = sensor.getId();
                if (!mInputManager.flushSensor(deviceId, sensor.getType())) {
                    return false;
                }
            }
            return true;
        }
    }

    /**
     * Sensor Manager class associated with specific input device
     */
    public class InputSensorManager extends SensorManager {
        // Input device ID that the sensors belong to
        final int mId;

        InputSensorManager(int deviceId) {
            mId = deviceId;
        }

        @Override
        public Sensor getDefaultSensor(int type) {
            return getSensorForInputDevice(mId, type);
        }

        @Override
        protected List<Sensor> getFullSensorList() {
            return getFullSensorListForDevice(mId);
        }

        @Override
        protected List<Sensor> getFullDynamicSensorList() {
            return new ArrayList<>();
        }

        @Override
        protected boolean registerListenerImpl(SensorEventListener listener, Sensor sensor,
                int delayUs, Handler handler, int maxBatchReportLatencyUs, int reservedFlags) {
            return registerListenerInternal(listener, sensor, delayUs,
                    maxBatchReportLatencyUs, handler);
        }

        @Override
        protected void unregisterListenerImpl(SensorEventListener listener, Sensor sensor) {
            unregisterListenerInternal(listener, sensor);
        }

        @Override
        protected boolean flushImpl(SensorEventListener listener) {
            return flush(listener);
        }

        @Override
        protected SensorDirectChannel createDirectChannelImpl(MemoryFile memoryFile,
                HardwareBuffer hardwareBuffer) {
            return null;
        }

        @Override
        protected void destroyDirectChannelImpl(SensorDirectChannel channel) {

        }

        @Override
        protected int configureDirectChannelImpl(SensorDirectChannel channel, Sensor s, int rate) {
            return 0;
        }

        @Override
        protected void registerDynamicSensorCallbackImpl(DynamicSensorCallback callback,
                Handler handler) {

        }

        @Override
        protected void unregisterDynamicSensorCallbackImpl(
                DynamicSensorCallback callback) {

        }

        @Override
        protected boolean requestTriggerSensorImpl(TriggerEventListener listener, Sensor sensor) {
            return true;
        }

        @Override
        protected boolean cancelTriggerSensorImpl(TriggerEventListener listener, Sensor sensor,
                boolean disable) {
            return true;
        }

        @Override
        protected boolean initDataInjectionImpl(boolean enable) {
            return false;
        }

        @Override
        protected boolean injectSensorDataImpl(Sensor sensor, float[] values, int accuracy,
                long timestamp) {
            return false;
        }

        @Override
        protected boolean setOperationParameterImpl(SensorAdditionalInfo parameter) {
            return false;
        }
    }

}
