How to distinguish day from night if you are Android

    Hi, Habr.


    Today we’ll talk about how cool it is to read in the dark. In childhood, our mothers forbade us all to do this, but now there are tablets! Unlike paper books, you don’t need to shine a flashlight on them, they will do everything for you. And it is we who teach them this. However, first things first.




    In one of the mobile applications for Android, which we have developed, there is a screen for reading news. For the convenience of users, we have provided two display modes in it - day and night. It's simple: if the device “knows” that it is day (or just light), a regular screen works, with black font on white. If it understands that the user is in the dark, he offers him to switch to night mode - a white font and a black screen.



    The most important thing is to prompt the user to make a switch in time. To do this, you need to determine whether it is day or night using the device’s sensor.

    Working with any sensor in Android comes down to the following steps:
    1. Get access to the SensorManager .
    2. Get access to the desired sensor.
    3. Register the listener using the interface common to all sensors.

    Example of working with SensorManager:

    public class SensorActivity extends Activity implements SensorEventListener {
         private final SensorManager sensorManager;
         private final Sensor lightSensor;
         public SensorActivity() {
             sensorManager = (SensorManager)getSystemService(SENSOR_SERVICE);
             lightSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_LIGHT); //Датчик освещённости
         }
         protected void onResume() {
             super.onResume();
             sensorManager.registerListener(this, lightSensor, SensorManager.SENSOR_DELAY_NORMAL); //Подключаемся к сенсору
         }
         protected void onPause() {
             super.onPause();
             sensorManager.unregisterListener(this); //Отключаемся от сенсора
         }
         public void onAccuracyChanged(Sensor sensor, int accuracy) {
         }
         public void onSensorChanged(SensorEvent event) {
            //Получаем данные из SensorEvent
         }
     }
    


    All data from the sensor comes in the SensorEvent # values array .
    According to the documentation, this is what the light sensor sends us:

    Sensor.TYPE_LIGHT:
    values ​​[0]: Ambient light level in SI lux units


    There is only one value - the number of suites.

    Minute education


    What is a suite? Well, everything is simple: lux is a unit of illumination of a surface of 1 m² with a luminous flux of incident radiation equal to 1 lm (lumen). A lumen is a unit of measurement of the light flux equal to the light flux emitted by a point isotropic source, with a light force equal to one candela, into a solid angle of one steradian. And the steradian is ... However, let's just look at the picture:



    (source blog.tredz.co.uk/wp-content/uploads/2012/09/light-dia1.jpg )
    If everything is together, then lux is such illumination of the surface in 1 m², which occurs when such a light shines on it with a light intensity of 1 cd (candela) with such a beam of light with a size of 1 steradian.
    OK, the number of suites we know, what next? Next, we will try to find out what level of illumination is typical for daylight hours and for dark.
    By the way, do not try to search the search engines for the keywords “luxury”, “day”, “night”, if you do not want to keep abreast of the best prices for comfortable hotel rooms :).

    In the Russian Wiki, you can find a plate with examples of illumination , in which you can find such useful examples as:
    • up to 20 - In the sea at a depth of ~ 50 m.
    • 350 ± 150 - Sunrise or sunset on Venus



    Due to the fact that we do not use the application for the inhabitants of Venus, let us dwell on the value of 50 suites, which corresponds to the illumination in the living room.

    Matter of technology


    We will write the LightSensorManager class, which can be “turned on” and “turned off” and which will report to us if it has become dark or light.

    LightSensorManager
    public class LightSensorManager implements SensorEventListener {
        private enum Environment {DAY, NIGHT}
        public interface EnvironmentChangedListener {
            void onDayDetected();
            void onNightDetected();
        }
        private static final int THRESHOLD_LUX = 50;
        private static final String TAG = "LightSensorManager";
        private final SensorManager sensorManager;
        private final Sensor lightSensor;
        private EnvironmentChangedListener environmentChangedListener;
        private Environment currentEnvironment;
        public LightSensorManager(Context context) {
            sensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE);
            lightSensor = sensorManager.getDefaultSensor(Sensor.TYPE_LIGHT); // Сенсор освещённости
        }
        public void enable() {
            if (lightSensor != null){
                sensorManager.registerListener(this, lightSensor, SensorManager.SENSOR_DELAY_NORMAL);
            } else {
                Log.w(TAG, "Light sensor in not supported");
            }
        }
        public void disable() {
            sensorManager.unregisterListener(this);
        }
        public EnvironmentChangedListener getEnvironmentChangedListener() {
            return environmentChangedListener;
        }
        public void setEnvironmentChangedListener(EnvironmentChangedListener environmentChangedListener) {
            this.environmentChangedListener = environmentChangedListener;
        }
        @Override
        public void onSensorChanged(SensorEvent event) {
            float luxLevel = event.values[0];
            Environment oldEnvironment = currentEnvironment;
            currentEnvironment = luxLevel < THRESHOLD_LUX ? Environment.NIGHT : Environment.DAY;
            if (hasChanged(oldEnvironment, currentEnvironment)){
                callListener(currentEnvironment);
            }
        }
        @Override
        public void onAccuracyChanged(Sensor sensor, int accuracy) {}
        private boolean hasChanged(Environment oldEnvironment, Environment newEnvironment) {
            return oldEnvironment != newEnvironment;
        }
        private void callListener(Environment environment) {
            if (environmentChangedListener == null || environment == null){
                return;
            }
            switch (environment) {
                case DAY:
                    environmentChangedListener.onDayDetected();
                    break;
                case NIGHT:
                    environmentChangedListener.onNightDetected();
                    break;
            }
        }
    }
    



    Now we can add this manager to our Activity, including it in onResume and turning it off onPause.
    You can watch how the level of illumination changes without leaving the room. Just find the sensor on the device and close it with your finger.
    It may happen that the device turns out to be in a room with an illumination level approximately equal to our chosen threshold value of 50 lux and, fluctuating, will often cross the threshold value. This will lead to the fact that our manager will begin to inform us very often about the change of day and night. We will get rid of this by introducing 2 threshold values: upper and lower. We will consider higher than the upper one during the day, lower than the lower - at night, and we will ignore changes between the thresholds.

    LightSensorManager with two thresholds
    public class LightSensorManager implements SensorEventListener {
        private enum Environment {DAY, NIGHT}
        public interface EnvironmentChangedListener {
            void onDayDetected();
            void onNightDetected();
        }
        private static final int THRESHOLD_DAY_LUX = 50;
        private static final int THRESHOLD_NIGHT_LUX = 40;
        private static final String TAG = "LightSensorManager";
        private final SensorManager sensorManager;
        private final Sensor lightSensor;
        private EnvironmentChangedListener environmentChangedListener;
        private Environment currentEnvironment;
        public LightSensorManager(Context context) {
            sensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE);
            lightSensor = sensorManager.getDefaultSensor(Sensor.TYPE_LIGHT); // Сенсор освещённости
        }
        public void enable() {
            if (lightSensor != null){
                sensorManager.registerListener(this, lightSensor, SensorManager.SENSOR_DELAY_NORMAL);
            } else {
                Log.w(TAG, "Light sensor in not supported");
            }
        }
        public void disable() {
            sensorManager.unregisterListener(this);
        }
        public EnvironmentChangedListener getEnvironmentChangedListener() {
            return environmentChangedListener;
        }
        public void setEnvironmentChangedListener(EnvironmentChangedListener environmentChangedListener) {
            this.environmentChangedListener = environmentChangedListener;
        }
        @Override
        public void onSensorChanged(SensorEvent event) {
            float luxLevel = event.values[0];
            Environment oldEnvironment = currentEnvironment;
            if (luxLevel < THRESHOLD_NIGHT_LUX){
                currentEnvironment = Environment.NIGHT;
            } else if (luxLevel > THRESHOLD_DAY_LUX){
                currentEnvironment = Environment.DAY;
            }
            if (hasChanged(oldEnvironment, currentEnvironment)){
                callListener(currentEnvironment);
            }
        }
        @Override
        public void onAccuracyChanged(Sensor sensor, int accuracy) {}
        private boolean hasChanged(Environment oldEnvironment, Environment newEnvironment) {
            return oldEnvironment != newEnvironment;
        }
        private void callListener(Environment environment) {
            if (environmentChangedListener == null || environment == null){
                return;
            }
            switch (environment) {
                case DAY:
                    environmentChangedListener.onDayDetected();
                    break;
                case NIGHT:
                    environmentChangedListener.onNightDetected();
                    break;
            }
        }
    }
    



    And one more nuance: we can get a false positive with a short-term strong change in the level of illumination. For example, if the light “blinks” due to an overvoltage or the user passes at night under a lamppost.



    We can get rid of this problem if we program a low-pass filter (aka low pass filter). It will smooth out all sharp and short-term changes in the data from the sensor.

    LightSensorManager with low pass filter
    public class LightSensorManager implements SensorEventListener {
        private enum Environment {DAY, NIGHT}
        public interface EnvironmentChangedListener {
            void onDayDetected();
            void onNightDetected();
        }
        private static final float SMOOTHING = 10;
        private static final int THRESHOLD_DAY_LUX = 50;
        private static final int THRESHOLD_NIGHT_LUX = 40;
        private static final String TAG = "LightSensorManager";
        private final SensorManager sensorManager;
        private final Sensor lightSensor;
        private EnvironmentChangedListener environmentChangedListener;
        private Environment currentEnvironment;
        private final LowPassFilter lowPassFilter;
        public LightSensorManager(Context context) {
            sensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE);
            lightSensor = sensorManager.getDefaultSensor(Sensor.TYPE_LIGHT);
            lowPassFilter = new LowPassFilter(SMOOTHING);
        }
        public void enable() {
            if (lightSensor != null){
                sensorManager.registerListener(this, lightSensor, SensorManager.SENSOR_DELAY_NORMAL);
            } else {
                Log.w(TAG, "Light sensor in not supported");
            }
        }
        public void disable() {
            sensorManager.unregisterListener(this);
        }
        public EnvironmentChangedListener getEnvironmentChangedListener() {
            return environmentChangedListener;
        }
        public void setEnvironmentChangedListener(EnvironmentChangedListener environmentChangedListener) {
            this.environmentChangedListener = environmentChangedListener;
        }
        @Override
        public void onSensorChanged(SensorEvent event) {
            float luxLevel = event.values[0];
            luxLevel = lowPassFilter.submit(luxLevel);
            Environment oldEnvironment = currentEnvironment;
            if (luxLevel < THRESHOLD_NIGHT_LUX){
                currentEnvironment = Environment.NIGHT;
            } else if (luxLevel > THRESHOLD_DAY_LUX){
                currentEnvironment = Environment.DAY;
            }
            if (hasChanged(oldEnvironment, currentEnvironment)){
                callListener(currentEnvironment);
            }
        }
        @Override
        public void onAccuracyChanged(Sensor sensor, int accuracy) {}
        private boolean hasChanged(Environment oldEnvironment, Environment newEnvironment) {
            return oldEnvironment != newEnvironment;
        }
        private void callListener(Environment environment) {
            if (environmentChangedListener == null || environment == null){
                return;
            }
            switch (environment) {
                case DAY:
                    environmentChangedListener.onDayDetected();
                    break;
                case NIGHT:
                    environmentChangedListener.onNightDetected();
                    break;
            }
        }
    }
    public class LowPassFilter {
        private float filteredValue;
        private final float smoothing;
        private boolean firstTime = true;
        public LowPassFilter(float smoothing) {
            this.smoothing = smoothing;
        }
        public float submit(float newValue){
            if (firstTime){
                filteredValue = newValue;
                firstTime = false;
                return filteredValue;
            }
            filteredValue += (newValue - filteredValue) / smoothing;
            return filteredValue;
        }
    }
    


    By the way, Android developers have kindly added several constants to the SensorManager class related to varying degrees of illumination, for example, SensorManager.LIGHT_CLOUDY or SensorManager.LIGHT_FULLMOON .

    Well, that's it, the implementation is quite simple. It's great that under the soulless code lies a connection with physics. Using the sensors that the device is equipped with, we can make the application more user-friendly and somewhat interactive. Now you can continue to read without hesitation, regardless of the occurrence of the day or night, entering the tunnel or going to the beach.
    Moreover, summer is on the way - all run to the beach to read.


    Also popular now: