chromium/services/device/generic_sensor/android/java/src/org/chromium/device/sensors/PlatformSensor.java

// Copyright 2016 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.device.sensors;

import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;

import androidx.annotation.GuardedBy;

import org.jni_zero.CalledByNative;
import org.jni_zero.CalledByNativeForTesting;
import org.jni_zero.JNINamespace;
import org.jni_zero.NativeMethods;

import org.chromium.base.Log;
import org.chromium.device.mojom.ReportingMode;
import org.chromium.device.mojom.SensorType;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.List;

/**
 * Implementation of PlatformSensor that uses Android Sensor Framework. Lifetime is controlled by
 * the device::PlatformSensorAndroid.
 */
@JNINamespace("device")
public class PlatformSensor implements SensorEventListener {
    private static final double MICROSECONDS_IN_SECOND = 1000000;
    private static final double SECONDS_IN_MICROSECOND = 0.000001d;
    private static final double SECONDS_IN_NANOSECOND = 0.000000001d;
    private static final String TAG = "PlatformSensor";

    /**
     * The SENSOR_FREQUENCY_NORMAL is defined as 5Hz which corresponds to a polling delay
     * @see android.hardware.SensorManager.SENSOR_DELAY_NORMAL value that is defined as 200000
     * microseconds.
     */
    private static final double SENSOR_FREQUENCY_NORMAL = 5.0d;

    /** Lock protecting access to mNativePlatformSensorAndroid. */
    private final Object mLock = new Object();

    /** Identifier of device::PlatformSensorAndroid instance. */
    @GuardedBy("mLock")
    private long mNativePlatformSensorAndroid;

    /**
     * Used for fetching sensor reading values and obtaining information about the sensor.
     * @see android.hardware.Sensor
     */
    private final Sensor mSensor;

    /** The minimum delay between two readings in microseconds that is supported by the sensor. */
    private final int mMinDelayUsec;

    /** The number of sensor reading values required from the sensor. */
    private final int mReadingCount;

    /** Frequency that is currently used by the sensor for polling. */
    private double mCurrentPollingFrequency;

    /**
     * Provides shared SensorManager and event processing thread Handler to PlatformSensor objects.
     */
    private final PlatformSensorProvider mProvider;

    /**
     * Creates new PlatformSensor.
     *
     * @param provider object that shares SensorManager and polling thread Handler with sensors.
     * @param type type of the sensor to be constructed. @see android.hardware.Sensor.TYPE_*
     * @param nativePlatformSensorAndroid identifier of device::PlatformSensorAndroid instance.
     */
    @CalledByNative
    public static PlatformSensor create(
            PlatformSensorProvider provider, int type, long nativePlatformSensorAndroid) {
        SensorManager sensorManager = provider.getSensorManager();
        if (sensorManager == null) return null;

        int sensorType;
        int readingCount;
        switch (type) {
            case SensorType.AMBIENT_LIGHT:
                sensorType = Sensor.TYPE_LIGHT;
                readingCount = 1;
                break;
            case SensorType.ACCELEROMETER:
                sensorType = Sensor.TYPE_ACCELEROMETER;
                readingCount = 3;
                break;
            case SensorType.LINEAR_ACCELERATION:
                sensorType = Sensor.TYPE_LINEAR_ACCELERATION;
                readingCount = 3;
                break;
            case SensorType.GRAVITY:
                sensorType = Sensor.TYPE_GRAVITY;
                readingCount = 3;
                break;
            case SensorType.GYROSCOPE:
                sensorType = Sensor.TYPE_GYROSCOPE;
                readingCount = 3;
                break;
            case SensorType.MAGNETOMETER:
                sensorType = Sensor.TYPE_MAGNETIC_FIELD;
                readingCount = 3;
                break;
            case SensorType.ABSOLUTE_ORIENTATION_QUATERNION:
                sensorType = Sensor.TYPE_ROTATION_VECTOR;
                readingCount = 4;
                break;
            case SensorType.RELATIVE_ORIENTATION_QUATERNION:
                sensorType = Sensor.TYPE_GAME_ROTATION_VECTOR;
                readingCount = 4;
                break;
            default:
                return null;
        }

        List<Sensor> sensors = sensorManager.getSensorList(sensorType);
        if (sensors.isEmpty()) return null;
        return new PlatformSensor(
                sensors.get(0), readingCount, provider, nativePlatformSensorAndroid);
    }

    /** Constructor. */
    protected PlatformSensor(
            Sensor sensor,
            int readingCount,
            PlatformSensorProvider provider,
            long nativePlatformSensorAndroid) {
        mReadingCount = readingCount;
        mProvider = provider;
        mSensor = sensor;
        mNativePlatformSensorAndroid = nativePlatformSensorAndroid;
        mMinDelayUsec = mSensor.getMinDelay();
    }

    /**
     * Returns reporting mode supported by the sensor.
     *
     * @return ReportingMode reporting mode.
     */
    @CalledByNative
    protected int getReportingMode() {
        return mSensor.getReportingMode() == Sensor.REPORTING_MODE_CONTINUOUS
                ? ReportingMode.CONTINUOUS
                : ReportingMode.ON_CHANGE;
    }

    /**
     * Returns default configuration supported by the sensor. Currently only frequency is supported.
     *
     * @return double frequency.
     */
    @CalledByNative
    protected double getDefaultConfiguration() {
        return SENSOR_FREQUENCY_NORMAL;
    }

    /**
     * Returns maximum sampling frequency supported by the sensor.
     *
     * @return double frequency in Hz.
     */
    @CalledByNative
    protected double getMaximumSupportedFrequency() {
        if (mMinDelayUsec == 0) return getDefaultConfiguration();
        return 1 / (mMinDelayUsec * SECONDS_IN_MICROSECOND);
    }

    /** Requests sensor to start polling for data. */
    @CalledByNative
    protected void startSensor(double frequency) {
        // If we already polling hw with same frequency, do not restart the sensor.
        if (mCurrentPollingFrequency == frequency) return;

        // Unregister old listener if polling frequency has changed.
        unregisterListener();

        mProvider.sensorStarted(this);
        boolean sensorStarted;
        try {
            sensorStarted =
                    mProvider
                            .getSensorManager()
                            .registerListener(
                                    this,
                                    mSensor,
                                    getSamplingPeriod(frequency),
                                    mProvider.getHandler());
        } catch (RuntimeException e) {
            // This can fail due to internal framework errors. https://crbug.com/884190
            Log.w(TAG, "Failed to register sensor listener.", e);
            sensorStarted = false;
        }

        if (!sensorStarted) {
            stopSensor();
            synchronized (mLock) {
                sensorError();
            }
        } else {
            mCurrentPollingFrequency = frequency;
        }
    }

    private void unregisterListener() {
        // Do not unregister if current polling frequency is 0, not polling for data.
        if (mCurrentPollingFrequency == 0) return;
        mProvider.getSensorManager().unregisterListener(this, mSensor);
    }

    /** Requests sensor to stop polling for data. */
    @CalledByNative
    protected void stopSensor() {
        unregisterListener();
        mProvider.sensorStopped(this);
        mCurrentPollingFrequency = 0;
    }

    /**
     * Checks whether configuration is supported by the sensor. Currently only frequency is
     * supported.
     *
     * @return boolean true if configuration is supported, false otherwise.
     */
    @CalledByNative
    protected boolean checkSensorConfiguration(double frequency) {
        return mMinDelayUsec <= getSamplingPeriod(frequency);
    }

    /**
     * Called from device::PlatformSensorAndroid destructor, so that this instance would be
     * notified not to deliver any updates about new sensor readings or errors.
     */
    @CalledByNative
    protected void sensorDestroyed() {
        synchronized (mLock) {
            mNativePlatformSensorAndroid = 0;
        }
    }

    /** Converts frequency to sampling period in microseconds. */
    private int getSamplingPeriod(double frequency) {
        return (int) ((1 / frequency) * MICROSECONDS_IN_SECOND);
    }

    /** Notifies native device::PlatformSensorAndroid when there is an error. */
    @GuardedBy("mLock")
    protected void sensorError() {
        if (mNativePlatformSensorAndroid != 0) {
            PlatformSensorJni.get()
                    .notifyPlatformSensorError(mNativePlatformSensorAndroid, PlatformSensor.this);
        }
    }

    /** Updates reading at native device::PlatformSensorAndroid. */
    @GuardedBy("mLock")
    protected void updateSensorReading(
            double timestamp, double value1, double value2, double value3, double value4) {
        PlatformSensorJni.get()
                .updatePlatformSensorReading(
                        mNativePlatformSensorAndroid,
                        PlatformSensor.this,
                        timestamp,
                        value1,
                        value2,
                        value3,
                        value4);
    }

    @Override
    public void onAccuracyChanged(Sensor sensor, int accuracy) {}

    @Override
    public void onSensorChanged(SensorEvent event) {
        // Acquire mLock to ensure that mNativePlatformSensorAndroid is not reset between this check
        // and when it is used.
        synchronized (mLock) {
            if (mNativePlatformSensorAndroid == 0) {
                Log.w(
                        TAG,
                        "Should not get sensor events after PlatformSensorAndroid is destroyed.");
                return;
            }

            if (event.values.length < mReadingCount) {
                sensorError();
                stopSensor();
                return;
            }

            double timestamp = event.timestamp * SECONDS_IN_NANOSECOND;
            switch (event.values.length) {
                case 1:
                    updateSensorReading(timestamp, event.values[0], 0.0, 0.0, 0.0);
                    break;
                case 2:
                    updateSensorReading(timestamp, event.values[0], event.values[1], 0.0, 0.0);
                    break;
                case 3:
                    updateSensorReading(
                            timestamp, event.values[0], event.values[1], event.values[2], 0.0);
                    break;
                default:
                    updateSensorReading(
                            timestamp,
                            event.values[0],
                            event.values[1],
                            event.values[2],
                            event.values[3]);
            }
        }
    }

    /**
     * A testing method to let device::PlatformSensorAndroid simulates a OnSensorChanged call. The
     * event with length |readingValuesLength| is created and filled with readings as (reading_index
     * + 0.1).
     */
    @CalledByNativeForTesting
    public void simulateSensorEventForTesting(int readingValuesLength) {
        try {
            Constructor<SensorEvent> sensorEventConstructor =
                    SensorEvent.class.getDeclaredConstructor(Integer.TYPE);
            sensorEventConstructor.setAccessible(true);
            SensorEvent event = sensorEventConstructor.newInstance(readingValuesLength);
            event.timestamp = 123L;
            for (int i = 0; i < readingValuesLength; ++i) {
                event.values[i] = (float) (i + 0.1);
            }
            onSensorChanged(event);
        } catch (InvocationTargetException
                | NoSuchMethodException
                | InstantiationException
                | IllegalAccessException e) {
            Log.e(TAG, "Failed to create simulated SensorEvent.", e);
            return;
        }
    }

    @NativeMethods
    interface Natives {
        void notifyPlatformSensorError(long nativePlatformSensorAndroid, PlatformSensor caller);

        void updatePlatformSensorReading(
                long nativePlatformSensorAndroid,
                PlatformSensor caller,
                double timestamp,
                double value1,
                double value2,
                double value3,
                double value4);
    }
}