/** * 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.handler; import android.content.Context; import com.squareup.otto.Bus; import org.envirocar.app.R; import org.envirocar.core.entity.Car; import org.envirocar.core.entity.Measurement; import org.envirocar.core.entity.Track; import org.envirocar.core.entity.TrackImpl; import org.envirocar.core.events.TrackFinishedEvent; import org.envirocar.core.exception.MeasurementSerializationException; import org.envirocar.core.exception.NoMeasurementsException; import org.envirocar.core.injection.InjectApplicationScope; import org.envirocar.core.injection.Injector; import org.envirocar.core.logging.Logger; import org.envirocar.obd.events.BluetoothServiceStateChangedEvent; import org.envirocar.obd.service.BluetoothServiceState; import org.envirocar.remote.DAOProvider; import org.envirocar.storage.EnviroCarDB; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.Date; import javax.inject.Inject; import rx.Observable; import rx.Subscriber; import rx.Subscription; import rx.functions.Action0; import rx.functions.Func1; import rx.schedulers.Schedulers; import rx.subjects.PublishSubject; /** * TODO JavaDoc * * @author dewall */ public class TrackRecordingHandler { private static final Logger LOGGER = Logger.getLogger(TrackRecordingHandler.class); private static final DateFormat format = SimpleDateFormat.getDateTimeInstance(); private static final long DEFAULT_MAX_TIME_BETWEEN_MEASUREMENTS = 1000 * 60 * 15; private static final double DEFAULT_MAX_DISTANCE_BETWEEN_MEASUREMENTS = 3.0; @Inject @InjectApplicationScope protected Context mContext; @Inject protected Bus mBus; @Inject protected EnviroCarDB mEnvirocarDB; @Inject protected BluetoothHandler mBluetoothHandler; @Inject protected DAOProvider mDAOProvider; @Inject protected TrackDAOHandler trackDAOHandler; @Inject protected UserHandler mUserManager; @Inject protected TermsOfUseManager mTermsOfUseManager; @Inject protected CarPreferenceHandler carHander; private Track currentTrack; /** * Constructor. * * @param context the context of the activity's scope. */ public TrackRecordingHandler(Context context) { // Inject all annotated fields. ((Injector) context).injectObjects(this); } public Subscription startNewTrack(PublishSubject<Measurement> publishSubject) { return getActiveTrackReference(true) .subscribeOn(Schedulers.io()) .observeOn(Schedulers.io()) .subscribe(new Subscriber<Track>() { @Override public void onCompleted() { LOGGER.info("onCompleted()"); } @Override public void onError(Throwable e) { LOGGER.error(e.getMessage(), e); } @Override public void onNext(Track track) { add(publishSubject.doOnUnsubscribe(new Action0() { @Override public void call() { LOGGER.info("doOnUnsubscribe(): finish current track."); finishCurrentTrack(); } }).subscribe(new Subscriber <Measurement>() { @Override public void onStart() { super.onStart(); LOGGER.info("Subscribed on Measurement publisher"); } @Override public void onCompleted() { LOGGER.info("NewMeasurementSubject onCompleted()"); currentTrack = track; finishCurrentTrack(); } @Override public void onError(Throwable e) { LOGGER.error(e.getMessage(), e); currentTrack = track; finishCurrentTrack(); } @Override public void onNext(Measurement measurement) { LOGGER.info("onNextMeasurement()"); if (isUnsubscribed()) return; LOGGER.info("Insert new measurement "); // set the track database ID of the current active track measurement.setTrackId(track.getTrackID()); track.getMeasurements().add(measurement); try { mEnvirocarDB.insertMeasurement(measurement); } catch (MeasurementSerializationException e) { LOGGER.error(e.getMessage(), e); currentTrack = track; finishCurrentTrack(); } } })); } }); } /** * Returns the most recent track, which is not finished yet. It only returns the track when * it has not been finished yet, i.e. its last measurement'S position meets the requirements * for continuing a track. Otherwise, it sets the track to finished and creates a new database * entry when required. * * @param createNew indicates whether it should create a new track reference when no active * track is available. * @return an observable returning the active track reference. */ private Observable<Track> getActiveTrackReference(boolean createNew) { return Observable.just(currentTrack) // Is there a current reference? if not, then try to find an instance in the // enviroCar database. .flatMap(track -> track == null ? mEnvirocarDB.getActiveTrackObservable(false) : Observable.just(track)) .flatMap(validateTrackRef(createNew)) // Optimize it.... .map(track -> { currentTrack = track; return track; }); } /** * This function checks whether the last unfinished track reference is a valid track * reference, i.e. if its last measurement's spatial position is no to far away from the * current position and the time difference between now and the last measurement is not to * large. * * @param createNew should create a new measurement when it is not matching the requirements. * @return a function that validates the requirements. */ private Func1<Track, Observable<Track>> validateTrackRef(boolean createNew) { return new Func1<Track, Observable<Track>>() { @Override public Observable<Track> call(Track track) { if (track != null && track.getTrackStatus() == Track.TrackStatus.FINISHED) { try { // Check whether the last unfinished track reference is too old to be // considered. if ((System.currentTimeMillis() - track.getEndTime() < DEFAULT_MAX_TIME_BETWEEN_MEASUREMENTS / 10)) return Observable.just(track); // TODO: Spatial Filtering... // trackreference is too old. Set it to finished. track.setTrackStatus(Track.TrackStatus.FINISHED); mEnvirocarDB.updateTrack(track); track = null; } catch (NoMeasurementsException e) { LOGGER.info("Last unfinished track ref does not contain any measurements." + " Delete the track"); // No Measurements in the last track and it cannot be considered as // active anymore. Therefore, delete the database entry. trackDAOHandler.deleteLocalTrack(track); } } if (track != null) { return Observable.just(track); } else { // if there is no current reference cached or in the database, then create a new // one and persist it. return createNew ? createNewDatabaseTrackObservable() : Observable.just(null); } } }; } private Observable<Track> createNewDatabaseTrackObservable() { return Observable.create(new Observable.OnSubscribe<Track>() { @Override public void call(Subscriber<? super Track> subscriber) { String date = format.format(new Date()); Car car = carHander.getCar(); Track track = new TrackImpl(); track.setCar(car); track.setName("Track " + date); track.setDescription(String.format( mContext.getString(R.string.default_track_description), car != null ? car.getModel() : "null")); subscriber.onNext(track); } }).flatMap(track -> mEnvirocarDB.insertTrackObservable(track)); } /** * Finishes the current track. On the one hand, the background service that handles the * connection to the Bluetooth device gets closed and the track in the database gets finished. */ public void finishCurrentTrack() { LOGGER.info("finishCurrentTrack()"); finishCurrentTrackObservable() .doOnError(throwable -> LOGGER.warn(throwable.getMessage(), throwable)) .toBlocking() .first(); } /** * Finishes the current track. On the one hand, the background service that handles the * connection to the Bluetooth device gets closed and the track in the database gets finished. */ public Observable<Track> finishCurrentTrackObservable() { LOGGER.info("finishCurrentTrackObservable()"); // Set the current remoteService state to SERVICE_STOPPING. mBus.post(new BluetoothServiceStateChangedEvent(BluetoothServiceState.SERVICE_STOPPING)); return getActiveTrackReference(false) .flatMap(track -> { // Stop the background service. mBluetoothHandler.stopOBDConnectionService(); if (track == null) return Observable.just(track); // Fire a new TrackFinishedEvent on the event bus. mBus.post(new TrackFinishedEvent(currentTrack)); track.setTrackStatus(Track.TrackStatus.FINISHED); LOGGER.info(String.format("Track with local id [%s] successful " + "finished.", track.getTrackID())); currentTrack = null; // Depending on the number of measurements inside the track either update the // database and return the updated reference or delete the database entry. return (track.getMeasurements().size() <= 1) ? mEnvirocarDB.deleteTrackObservable(track) : mEnvirocarDB.updateTrackObservable(track); }); } }