/** * Copyright (C) 2013 - 2015 the enviroCar community * <p> * This file is part of the enviroCar app. * <p> * The enviroCar app 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 * (at your option) any later version. * <p> * The enviroCar app 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. * <p> * You should have received a copy of the GNU General Public License along * with the enviroCar app. If not, see http://www.gnu.org/licenses/. */ package org.envirocar.app.services; import android.app.Notification; import android.app.NotificationManager; import android.bluetooth.BluetoothDevice; import android.content.Context; import android.content.Intent; import android.os.PowerManager; import android.speech.tts.TextToSpeech; import android.support.v4.app.NotificationCompat; import com.squareup.otto.Subscribe; import org.envirocar.algorithm.MeasurementProvider; import org.envirocar.app.R; import org.envirocar.app.events.TrackDetailsProvider; import org.envirocar.app.handler.BluetoothHandler; import org.envirocar.app.handler.CarPreferenceHandler; import org.envirocar.app.handler.LocationHandler; import org.envirocar.app.handler.PreferencesHandler; import org.envirocar.app.handler.TrackRecordingHandler; import org.envirocar.core.entity.Car; import org.envirocar.core.entity.Measurement; import org.envirocar.core.events.NewMeasurementEvent; import org.envirocar.core.events.gps.GpsLocationChangedEvent; import org.envirocar.core.events.gps.GpsSatelliteFix; import org.envirocar.core.events.gps.GpsSatelliteFixEvent; import org.envirocar.core.exception.FuelConsumptionException; import org.envirocar.core.exception.NoMeasurementsException; import org.envirocar.core.exception.UnsupportedFuelTypeException; import org.envirocar.core.injection.BaseInjectorService; import org.envirocar.core.logging.Logger; import org.envirocar.core.trackprocessing.AbstractCalculatedMAFAlgorithm; import org.envirocar.core.trackprocessing.CalculatedMAFWithStaticVolumetricEfficiency; import org.envirocar.core.trackprocessing.ConsumptionAlgorithm; import org.envirocar.core.utils.CarUtils; import org.envirocar.core.utils.ServiceUtils; import org.envirocar.obd.ConnectionListener; import org.envirocar.obd.OBDController; import org.envirocar.obd.bluetooth.BluetoothSocketWrapper; import org.envirocar.obd.events.BluetoothServiceStateChangedEvent; import org.envirocar.obd.events.SpeedUpdateEvent; import org.envirocar.obd.service.BluetoothServiceState; import org.envirocar.storage.EnviroCarDB; import java.io.IOException; import java.util.Arrays; import java.util.List; import java.util.Locale; import java.util.concurrent.TimeUnit; import javax.inject.Inject; import rx.Scheduler; import rx.Subscriber; import rx.Subscription; import rx.functions.Action0; import rx.schedulers.Schedulers; import rx.subjects.PublishSubject; import static org.envirocar.app.services.obd.OBDServiceHandler.context; /** * @author dewall */ public class OBDConnectionService extends BaseInjectorService { private static final Logger LOG = Logger.getLogger(OBDConnectionService.class); public static void startService(Context context){ ServiceUtils.startService(context, OBDConnectionService.class); } public static void stopService(Context context){ ServiceUtils.stopService(context, OBDConnectionService.class); } protected static final int MAX_RECONNECT_COUNT = 2; public static final int BG_NOTIFICATION_ID = 42; public static BluetoothServiceState CURRENT_SERVICE_STATE = BluetoothServiceState .SERVICE_STOPPED; // Injected fields. @Inject protected BluetoothHandler mBluetoothHandler; @Inject protected LocationHandler mLocationHandler; @Inject protected TrackDetailsProvider mTrackDetailsProvider; @Inject protected PowerManager.WakeLock mWakeLock; @Inject protected MeasurementProvider measurementProvider; @Inject protected CarPreferenceHandler carHandler; @Inject protected EnviroCarDB enviroCarDB; @Inject protected TrackRecordingHandler trackRecordingHandler; @Inject protected OBDConnectionHandler obdConnectionHandler; private AbstractCalculatedMAFAlgorithm mafAlgorithm; // Text to speech variables. private TextToSpeech mTTS; private boolean mIsTTSAvailable; private boolean mIsTTSPrefChecked; // Member fields required for the connection to the OBD device. private OBDController mOBDController; // Different subscriptions private Subscription mTTSPrefSubscription; private Subscription mConnectingSubscription; private Subscription mMeasurementSubscription; private BluetoothSocketWrapper bluetoothSocketWrapper; // This satellite fix indicates that there is no satellite connection yet. private GpsSatelliteFix mCurrentGpsSatelliteFix = new GpsSatelliteFix(0, false); private OBDConnectionRecognizer connectionRecognizer = new OBDConnectionRecognizer(); private ConsumptionAlgorithm consumptionAlgorithm; private final Scheduler.Worker backgroundWorker = Schedulers.io().createWorker(); @Override public void onCreate() { LOG.info("OBDConnectionService.onCreate()"); super.onCreate(); // register on the event bus this.bus.register(this); this.bus.register(mTrackDetailsProvider); this.bus.register(connectionRecognizer); this.bus.register(measurementProvider); mTTS = new TextToSpeech(getApplicationContext(), new TextToSpeech.OnInitListener() { @Override public void onInit(int status) { try { if (status == TextToSpeech.SUCCESS) { mTTS.setLanguage(Locale.ENGLISH); mIsTTSAvailable = true; } else { LOG.warn("TextToSpeech is not available."); } } catch(IllegalArgumentException e){ LOG.warn("TextToSpeech is not available"); } } }); mTTSPrefSubscription = PreferencesHandler.getTextToSpeechObservable(getApplicationContext()) .subscribe(aBoolean -> { mIsTTSPrefChecked = aBoolean; }); /** * create the consumption and MAF algorithm, final for this connection */ Car car = carHandler.getCar(); this.consumptionAlgorithm = CarUtils.resolveConsumptionAlgorithm(car.getFuelType()); this.mafAlgorithm = new CalculatedMAFWithStaticVolumetricEfficiency(car); } @Override public int onStartCommand(Intent intent, int flags, int startId) { LOG.info("OBDConnectionService.onStartCommand()"); doTextToSpeech("Establishing connection"); // Acquire the wake lock for keeping the CPU active. mWakeLock.acquire(); // Start the location mLocationHandler.startLocating(); // Get the default device BluetoothDevice device = mBluetoothHandler.getSelectedBluetoothDevice(); if (device != null) { LOG.info("The BluetoothHandler has a valid device. Start the OBD connection"); // Start the OBD Connection. mConnectingSubscription = startOBDConnection(device); } else { LOG.severe("No default Bluetooth device selected"); } return START_STICKY; } /** * Sets the current remoteService state and fire an event on the bus. * * @param state the state of the remoteService. */ private void setBluetoothServiceState(BluetoothServiceState state) { // Set the new remoteService state CURRENT_SERVICE_STATE = state; // TODO FIX // and fire an event on the event bus. this.bus.post(produceBluetoothServiceStateChangedEvent()); } // @Produce public BluetoothServiceStateChangedEvent produceBluetoothServiceStateChangedEvent() { LOG.info(String.format("produceBluetoothServiceStateChangedEvent(): %s", CURRENT_SERVICE_STATE.toString())); return new BluetoothServiceStateChangedEvent(CURRENT_SERVICE_STATE); } @Override public void onDestroy() { LOG.info("OBDConnectionService.onDestroy()"); super.onDestroy(); // Stop this remoteService and emove this remoteService from foreground state. stopOBDConnection(); // Unregister from the event bus. bus.unregister(this); bus.unregister(mTrackDetailsProvider); bus.unregister(connectionRecognizer); bus.unregister(measurementProvider); LOG.info("OBDConnectionService successfully destroyed"); } @Override public List<Object> getInjectionModules() { return Arrays.<Object>asList(new OBDServiceModule()); } @Subscribe public void onReceiveGpsSatelliteFixEvent(GpsSatelliteFixEvent event) { boolean isFix = event.mGpsSatelliteFix.isFix(); if (isFix != mCurrentGpsSatelliteFix.isFix()) { if (isFix) { doTextToSpeech("GPS positioning established"); } else { doTextToSpeech("GPS positioning lost. Try to move the phone"); } this.mCurrentGpsSatelliteFix = event.mGpsSatelliteFix; } } private void doTextToSpeech(String string) { if (mIsTTSAvailable && mIsTTSPrefChecked) { mTTS.speak(string, TextToSpeech.QUEUE_ADD, null); } } /** * @param device the device to start a connection to. */ private Subscription startOBDConnection(final BluetoothDevice device) { return obdConnectionHandler.getOBDConnectionObservable(device) .subscribeOn(Schedulers.io()) .observeOn(Schedulers.io()) .subscribe(new Subscriber<BluetoothSocketWrapper>() { @Override public void onStart() { LOG.info("onStart() connection"); // Set remoteService state to STARTING and fire an event on the bus. setBluetoothServiceState(BluetoothServiceState.SERVICE_STARTING); } @Override public void onCompleted() { LOG.info("onCompleted(): BluetoothSocketWrapper connection completed"); } @Override public void onError(Throwable e) { LOG.error(e.getMessage(), e); unsubscribe(); } @Override public void onNext(BluetoothSocketWrapper socketWrapper) { LOG.info("startOBDConnection.onNext() socket successfully connected."); bluetoothSocketWrapper = socketWrapper; onDeviceConnected(bluetoothSocketWrapper); onCompleted(); } }); } private void onDeviceConnected(BluetoothSocketWrapper bluetoothSocket) { LOG.info(String.format("OBDConnectionService.onDeviceConntected(%s)", bluetoothSocket.getRemoteDeviceName())); try { this.mOBDController = new OBDController(bluetoothSocket, new ConnectionListener() { private int mReconnectCount = 0; @Override public void onConnectionVerified() { setBluetoothServiceState(BluetoothServiceState.SERVICE_STARTED); subscribeForMeasurements(); } @Override public void onAllAdaptersFailed() { LOG.info("all adapters failed!"); stopOBDConnection(); doTextToSpeech("failed to connect to the OBD adapter"); } @Override public void onStatusUpdate(String message) { } @Override public void requestConnectionRetry(IOException e) { if (mReconnectCount++ >= MAX_RECONNECT_COUNT) { LOG.warn("Max count of reconnecctes reaced", e); } else { LOG.info("Restarting Device Connection..."); doTextToSpeech("Connection lost. Trying to reconnect."); } } }, bus); } catch (IOException e) { LOG.warn(e.getMessage(), e); stopSelf(); return; } doTextToSpeech("Connection established"); } /** * Method that stops the remoteService, removes everything from the waiting list */ private void stopOBDConnection() { LOG.info("stopOBDConnection called"); backgroundWorker.schedule(() -> { stopForeground(true); // If there is an active UUID subscription. if (mConnectingSubscription != null && !mConnectingSubscription.isUnsubscribed()) mConnectingSubscription.unsubscribe(); if (mTTSPrefSubscription != null && !mTTSPrefSubscription.isUnsubscribed()) mTTSPrefSubscription.unsubscribe(); if (mMeasurementSubscription != null && !mMeasurementSubscription.isUnsubscribed()) mMeasurementSubscription.unsubscribe(); if (mOBDController != null) mOBDController.shutdown(); if (bluetoothSocketWrapper != null) bluetoothSocketWrapper.shutdown(); if (connectionRecognizer != null) connectionRecognizer.shutDown(); if (mTrackDetailsProvider != null) mTrackDetailsProvider.clear(); if (mWakeLock != null && mWakeLock.isHeld()) { mWakeLock.release(); } mLocationHandler.stopLocating(); // showServiceStateStoppedNotification(); doTextToSpeech("Device disconnected"); // Set state of the remoteService to stopped. setBluetoothServiceState(BluetoothServiceState.SERVICE_STOPPED); }); } private void subscribeForMeasurements() { // this is the first access to the measurement objects push it further Long samplingRate = PreferencesHandler.getSamplingRate(getApplicationContext()) * 1000; mMeasurementSubscription = measurementProvider.measurements(samplingRate) .subscribeOn(Schedulers.io()) .observeOn(Schedulers.io()) .subscribe(getMeasurementSubscriber()); } private Subscriber<Measurement> getMeasurementSubscriber() { return new Subscriber<Measurement>() { PublishSubject<Measurement> measurementPublisher = PublishSubject.create(); @Override public void onStart() { LOG.info("onStart(): MeasuremnetProvider Subscription"); add(trackRecordingHandler.startNewTrack(measurementPublisher)); } @Override public void onCompleted() { LOG.info("onCompleted(): MeasurementProvider"); measurementPublisher.onCompleted(); measurementPublisher = null; } @Override public void onError(Throwable e) { LOG.error(e.getMessage(), e); measurementPublisher.onError(e); measurementPublisher = null; } @Override public void onNext(Measurement measurement) { LOG.info("onNNNNENEEXT()"); try { if (!measurement.hasProperty(Measurement.PropertyKey.MAF)) { try { measurement.setProperty(Measurement.PropertyKey .CALCULATED_MAF, mafAlgorithm.calculateMAF(measurement)); } catch (NoMeasurementsException e) { LOG.warn(e.getMessage()); } } if (consumptionAlgorithm != null) { double consumption = consumptionAlgorithm.calculateConsumption(measurement); double co2 = consumptionAlgorithm.calculateCO2FromConsumption(consumption); measurement.setProperty(Measurement.PropertyKey.CONSUMPTION, consumption); measurement.setProperty(Measurement.PropertyKey.CO2, co2); } } catch (FuelConsumptionException e) { LOG.warn(e.getMessage()); } catch (UnsupportedFuelTypeException e) { LOG.warn(e.getMessage()); } measurementPublisher.onNext(measurement); bus.post(new NewMeasurementEvent(measurement)); } }; } // private void showServiceStateStoppedNotification() { // NotificationManager manager = (NotificationManager) getSystemService(Context // .NOTIFICATION_SERVICE); // Notification noti = new NotificationCompat.Builder(getApplicationContext()) // .setContentTitle("enviroCar") // .setContentText(getResources() // .getText(R.string.service_state_stopped)) // .setSmallIcon(R.drawable.dashboard) // .setAutoCancel(true) // .build(); // manager.notify(BG_NOTIFICATION_ID, noti); // } private final class OBDConnectionRecognizer { private static final long OBD_INTERVAL = 1000 * 10; // 10 seconds; private static final long GPS_INTERVAL = 1000 * 60 * 2; // 2 minutes; private long timeLastSpeedMeasurement; private long timeLastGpsMeasurement; private final Scheduler.Worker mBackgroundWorker = Schedulers.io().createWorker(); private Subscription mOBDCheckerSubscription; private Subscription mGPSCheckerSubscription; private final Action0 gpsConnectionCloser = () -> { LOG.warn("CONNECTION CLOSED due to no GPS values"); stopSelf(); }; private final Action0 obdConnectionCloser = () -> { LOG.warn("CONNECTION CLOSED due to no OBD values"); stopSelf(); }; @Subscribe public void onReceiveGpsLocationChangedEvent(GpsLocationChangedEvent event) { if (mGPSCheckerSubscription != null) { mGPSCheckerSubscription.unsubscribe(); mGPSCheckerSubscription = null; } timeLastGpsMeasurement = System.currentTimeMillis(); mGPSCheckerSubscription = mBackgroundWorker.schedule( gpsConnectionCloser, GPS_INTERVAL, TimeUnit.MILLISECONDS); } @Subscribe public void onReceiveSpeedUpdateEvent(SpeedUpdateEvent event) { LOG.info("Received speed update, no stop required via mOBDCheckerSubscription!"); if (mOBDCheckerSubscription != null) { mOBDCheckerSubscription.unsubscribe(); mOBDCheckerSubscription = null; } timeLastSpeedMeasurement = System.currentTimeMillis(); mOBDCheckerSubscription = mBackgroundWorker.schedule( obdConnectionCloser, OBD_INTERVAL, TimeUnit.MILLISECONDS); } public void shutDown() { LOG.info("shutDown() OBDConnectionRecognizer"); if (mOBDCheckerSubscription != null) mOBDCheckerSubscription.unsubscribe(); if (mGPSCheckerSubscription != null) mGPSCheckerSubscription.unsubscribe(); } } }