davidmigloz/go-bees

View on GitHub
app/src/main/java/com/davidmiguel/gobees/monitoring/MonitoringService.java

Summary

Maintainability
B
6 hrs
Test Coverage
/*
 * GoBees
 * Copyright (c) 2016 - 2017 David Miguel Lozano
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
 */

package com.davidmiguel.gobees.monitoring;

import android.app.Notification;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Intent;
import android.hardware.Camera;
import android.os.Binder;
import android.os.IBinder;
import android.os.SystemClock;
import android.support.annotation.Nullable;
import android.support.v4.app.NotificationCompat;

import com.davidmiguel.gobees.Injection;
import com.davidmiguel.gobees.R;
import com.davidmiguel.gobees.data.model.Apiary;
import com.davidmiguel.gobees.data.model.Record;
import com.davidmiguel.gobees.data.source.GoBeesDataSource;
import com.davidmiguel.gobees.data.source.GoBeesDataSource.SaveRecordingCallback;
import com.davidmiguel.gobees.data.source.repository.GoBeesRepository;
import com.davidmiguel.gobees.monitoring.algorithm.AreaBeesCounter;
import com.davidmiguel.gobees.monitoring.algorithm.BeesCounter;
import com.davidmiguel.gobees.monitoring.camera.AndroidCamera;
import com.davidmiguel.gobees.monitoring.camera.AndroidCameraImpl;
import com.davidmiguel.gobees.monitoring.camera.AndroidCameraListener;
import com.davidmiguel.gobees.monitoring.camera.CameraFrame;
import com.davidmiguel.gobees.utils.DateTimeUtils;
import com.davidmiguel.gobees.utils.NotificationsHelper;

import org.opencv.android.BaseLoaderCallback;
import org.opencv.android.LoaderCallbackInterface;
import org.opencv.android.OpenCVLoader;

import java.util.Date;
import java.util.LinkedList;
import java.util.Timer;
import java.util.TimerTask;

/**
 * Monitoring service.
 * It reads the camera feed and run the bee counter algorithm in background.
 * It also gets weather information periodically.
 * Notes:
 * - Intent with START action: starts monitoring.
 * - Intent with STOP action: stops monitoring.
 * - There is a delay of INITIAL_DELAY before the camera is opened (to avoid trepidations when the
 * user manipulates the phone).
 * - The first INITIAL_NUM_FRAMES frames are used to create a background model (they are not used
 * to count bees). During this time, the frame rate is INITIAL_FRAME_RATE.
 * - After the background model is created, the frame rate is set to the one configured by the user.
 * - The recording must have more than 5 records, if not, it is ignored.
 * - The first and last record of a recording always have numBees = -1 (this is used to know
 * when the recording starts and ends).
 * - The service gets and saves apiary weather data every WEATHER_REFRESH_RATE.
 * - When the service ends, it calls the appropriate callback:
 */
@SuppressWarnings("deprecation")
public class MonitoringService extends Service implements AndroidCameraListener {

    // Intent argument
    public static final String ARGUMENT_MON_SETTINGS = "MONITORING_SETTINGS";
    // Intent actions
    public static final String START_ACTION = "start_action";
    public static final String STOP_ACTION = "stop_action";
    // Notification id
    private static final int NOTIFICATION_ID = 101;

    // Delay before start recording
    private static final long INITIAL_DELAY = DateTimeUtils.T_5_SECONDS;
    // Frame rate while creating background model
    private static final long INITIAL_FRAME_RATE = 300;
    // Number of frames to create background model
    private static final long INITIAL_NUM_FRAMES = 10;
    // Number of last recording seconds to delete (they usually contains noise)
    private static final long NUM_LAST_SEC_TO_DELETE = DateTimeUtils.T_5_SECONDS;
    // Weather refresh rate
    private static final long WEATHER_REFRESH_RATE = DateTimeUtils.T_15_MINUTES;

    // Notifications
    private NotificationsHelper notificationsHelper;

    // Service stuff
    private static MonitoringService instance = null;
    private final IBinder binder = new MonitoringBinder();

    // Persistence
    private GoBeesRepository goBeesRepository;
    private SaveRecordingCallback callback;
    private LinkedList<Record> records;

    // Camera and algorithm
    private AndroidCamera androidCamera;
    private boolean openCvLoaded = false;
    private BeesCounter bc;
    private int initialNumFrames;
    private long startTime;

    // Weather
    private Timer timer;
    private FetchWeatherTask fetchWeatherTask;

    // Model info
    private Apiary apiary;
    private MonitoringSettings monitoringSettings;

    /**
     * Checks whether the service is running.
     *
     * @return status.
     */
    public static boolean isRunning() {
        return instance != null;
    }

    @Override
    public void onCreate() {
        super.onCreate();
        instance = this;
        // Init record list
        records = new LinkedList<>();
        // Notifications
        notificationsHelper = new NotificationsHelper(this);
        // Init db
        goBeesRepository = Injection.provideApiariesRepository();
        goBeesRepository.openDb();
        // Create fetch weather task
        fetchWeatherTask = new FetchWeatherTask();
        timer = new Timer();
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        super.onStartCommand(intent, flags, startId);

        // START action
        if (intent.getAction().equals(START_ACTION)) {
            // Calculate start time (to be use in chronometer)
            Date now = new Date();
            long elapsedRealTimeOffset = System.currentTimeMillis() - SystemClock.elapsedRealtime();
            startTime = now.getTime() - elapsedRealTimeOffset;
            // Get monitoring config
            monitoringSettings =
                    (MonitoringSettings) intent.getSerializableExtra(ARGUMENT_MON_SETTINGS);
            // Get apiary
            apiary = goBeesRepository.getApiaryBlocking(monitoringSettings.getApiaryId());
            // Configurations
            configBeeCounter();
            configCamera();
            Notification not = notificationsHelper.getMonitoringNotification(
                    monitoringSettings.getApiaryId(), monitoringSettings.getHiveId());
            configOpenCv();
            // Start service in foreground
            startForeground(NOTIFICATION_ID, not);

            // STOP action
        } else if (intent.getAction().equals(STOP_ACTION)) {
            // Release camera
            androidCamera.release();
            // Save records
            if (!records.isEmpty()) {
                // Clean records
                cleanRecords();
                // Save records on db
                goBeesRepository.saveRecords(monitoringSettings.getHiveId(), records,
                        new SaveRecordingCallback() {
                            @Override
                            public void onRecordingTooShort() {
                                stopService();
                                callback.onRecordingTooShort();
                            }

                            @Override
                            public void onSuccess() {
                                stopService();
                                callback.onSuccess();
                            }

                            @Override
                            public void onFailure() {
                                stopService();
                                callback.onFailure();
                            }
                        });
            } else {
                stopService();
                callback.onRecordingTooShort();
            }
        }
        return START_STICKY;
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        instance = null;
        // Release camera
        if (androidCamera != null && androidCamera.isConnected()) {
            androidCamera.release();
            androidCamera = null;
        }
        // Stop fetching weather data
        if (timer != null) {
            timer.cancel();
            timer = null;
            fetchWeatherTask = null;
        }
        // Close database
        goBeesRepository.closeDb();
    }

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return binder;
    }

    @Override
    public boolean isOpenCvLoaded() {
        return openCvLoaded;
    }

    @Override
    public void onCameraStarted(int width, int height) {
        // Counter for creating background model with the first frames
        initialNumFrames = 0;
    }

    @Override
    public void onPreviewFrame(CameraFrame cameraFrame) {
        if (initialNumFrames < INITIAL_NUM_FRAMES) {
            // To create background model
            bc.countBees(cameraFrame.gray());
            bc.getProcessedFrame().release();
            initialNumFrames++;
            return;
        } else if (initialNumFrames == INITIAL_NUM_FRAMES) {
            // After creating background model, set real configuration
            androidCamera.updateFrameRate(0, monitoringSettings.getFrameRate());
            initialNumFrames++;
        }
        // Process frame
        int numBees = bc.countBees(cameraFrame.gray());
        bc.getProcessedFrame().release();
        // Save record
        records.add(new Record(new Date(), numBees));
    }

    /**
     * Get monitoring start time.
     *
     * @return start time.
     */
    public long getStartTime() {
        return startTime + INITIAL_DELAY;
    }

    /**
     * Config bee counter with settings.
     */
    private void configBeeCounter() {
        bc = AreaBeesCounter.getInstance();
        bc.updateBlobSize(monitoringSettings.getBlobSize());
        bc.updateMinArea(monitoringSettings.getMinArea());
        bc.updateMaxArea(monitoringSettings.getMaxArea());
    }

    /**
     * Config camera with settings.
     */
    private void configCamera() {
        androidCamera = new AndroidCameraImpl(MonitoringService.this,
                Camera.CameraInfo.CAMERA_FACING_BACK,
                monitoringSettings.getMaxFrameWidth(),
                monitoringSettings.getMaxFrameHeight(),
                monitoringSettings.getZoomRatio(),
                INITIAL_DELAY,
                INITIAL_FRAME_RATE);
    }

    /**
     * Config notification.
     */
    private Notification configNotification() {
        // Intent to the monitoring activity (when the notification is clicked)
        Intent monitoringIntent = new Intent(this, MonitoringActivity.class);
        monitoringIntent.putExtra(MonitoringFragment.ARGUMENT_APIARY_ID,
                monitoringSettings.getApiaryId());
        monitoringIntent.putExtra(MonitoringFragment.ARGUMENT_HIVE_ID,
                monitoringSettings.getHiveId());
        PendingIntent pMonitoringIntent = PendingIntent.getActivity(this, 0,
                monitoringIntent, PendingIntent.FLAG_UPDATE_CURRENT);
        // Create notification
        return new NotificationCompat.Builder(this)
                .setContentTitle(getString(R.string.app_name))
                .setTicker(getString(R.string.app_name))
                .setContentText(getString(R.string.monitoring_notification_text))
                .setSmallIcon(R.mipmap.ic_launcher)
                .setContentIntent(pMonitoringIntent)
                .setOngoing(true).build();
    }


    /**
     * Config OpenCV (config callback and init OpenCV).
     * When OpenCV is ready, it starts monitoring.
     */
    private void configOpenCv() {
        // OpenCV callback
        BaseLoaderCallback loaderCallback = new BaseLoaderCallback(this) {
            @Override
            public void onManagerConnected(final int status) {
                if (status == LoaderCallbackInterface.SUCCESS) {
                    openCvLoaded = true;
                    startMonitoring();
                } else {
                    super.onManagerConnected(status);
                }
            }
        };
        // Init openCV
        OpenCVLoader.initAsync(OpenCVLoader.OPENCV_VERSION_3_2_0, this, loaderCallback);
    }

    /**
     * Start monitoring (frames will be received via onPreviewFrame()).
     */
    private void startMonitoring() {
        // If apiary has location -> Start fetching weather data (each WEATHER_REFRESH_RATE)
        if (apiary.hasLocation()) {
            timer.scheduleAtFixedRate(fetchWeatherTask,
                    getTotalInitialDelay(), WEATHER_REFRESH_RATE);
        }
        // Start camera
        if (!androidCamera.isConnected()) {
            androidCamera.connect();
        }
    }

    /**
     * Stop service.
     */
    private void stopService() {
        stopForeground(true);
        stopSelf();
    }

    /**
     * Delete last records that usually contain noise and add two special records
     * at the beginning and ending to know the limits of the recording.
     */
    private void cleanRecords() {
        // Delete last seconds
        long endTime = records.getLast().getTimestamp().getTime();
        while (!records.isEmpty()
                && endTime - records.getLast().getTimestamp().getTime() < NUM_LAST_SEC_TO_DELETE) {
            records.removeLast();
        }
        // Save initial and last record (to know the beginning and ending of the recording)
        if (!records.isEmpty()) {
            records.getFirst().setNumBees(-1);
            records.getLast().setNumBees(-1);
        }
    }

    /**
     * Get total initial delay: camera delay + creation of background model + 5 sec (of margin).
     *
     * @return total initial delay
     */
    private long getTotalInitialDelay() {
        return INITIAL_DELAY + INITIAL_FRAME_RATE * INITIAL_NUM_FRAMES + DateTimeUtils.T_5_SECONDS;
    }

    /**
     * Class used for the client Binder.  Because we know this service always
     * runs in the same process as its clients, we don't need to deal with IPC.
     */
    public class MonitoringBinder extends Binder {
        MonitoringService getService(SaveRecordingCallback c) {
            callback = c;
            // Return this instance of MonitoringService so clients can call public methods
            return MonitoringService.this;
        }
    }

    /**
     * Task that makes a request to weather server and stores the received weather data.
     */
    private class FetchWeatherTask extends TimerTask {
        @Override
        public void run() {
            goBeesRepository.getAndSaveMeteoRecord(apiary, new GoBeesDataSource.TaskCallback() {
                @Override
                public void onSuccess() {
                    // Don't do anything
                }

                @Override
                public void onFailure() {
                    // Don't do anything
                }
            });
        }
    }
}