/** AirCasting - Share your Air! Copyright (C) 2011-2012 HabitatMap, Inc. 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 (at your option) 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 <http://www.gnu.org/licenses/>. You can contact the authors by email at <info@habitatmap.org> */ package pl.llp.aircasting.model; import com.google.android.gms.maps.model.LatLng; import pl.llp.aircasting.Intents; import pl.llp.aircasting.activity.ApplicationState; import pl.llp.aircasting.activity.events.SessionChangeEvent; import pl.llp.aircasting.activity.events.SessionStartedEvent; import pl.llp.aircasting.activity.events.SessionStoppedEvent; import pl.llp.aircasting.android.Logger; import pl.llp.aircasting.helper.LocationHelper; import pl.llp.aircasting.helper.NotificationHelper; import pl.llp.aircasting.model.events.MeasurementEvent; import pl.llp.aircasting.model.events.SensorEvent; import pl.llp.aircasting.sensor.builtin.SimpleAudioReader; import pl.llp.aircasting.sensor.external.ExternalSensors; import pl.llp.aircasting.storage.DatabaseTaskQueue; import pl.llp.aircasting.storage.ProgressListener; import pl.llp.aircasting.storage.db.DBConstants; import pl.llp.aircasting.storage.db.WritableDatabaseTask; import pl.llp.aircasting.storage.repository.SessionRepository; import pl.llp.aircasting.tracking.ContinuousTracker; import android.app.Application; import android.content.ContentValues; import android.database.sqlite.SQLiteDatabase; import android.location.Location; import android.telephony.PhoneStateListener; import android.telephony.TelephonyManager; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; import com.google.common.eventbus.EventBus; import com.google.common.eventbus.Subscribe; import com.google.inject.Inject; import com.google.inject.Singleton; import org.intellij.lang.annotations.Language; import org.jetbrains.annotations.NotNull; import java.util.Collection; import java.util.Date; import java.util.List; import java.util.Map; import static com.google.common.collect.Lists.newArrayList; import static com.google.common.collect.Maps.newHashMap; @Singleton public class SessionManager { public static final double TOTALLY_FAKE_COORDINATE = 200; @Inject SimpleAudioReader audioReader; @Inject EventBus eventBus; @Inject SessionRepository sessionRepository; @Inject DatabaseTaskQueue dbQueue; @Inject LocationHelper locationHelper; @Inject NotificationHelper notificationHelper; @Inject Application applicationContext; @Inject TelephonyManager telephonyManager; @Inject SensorManager sensorManager; @NotNull Session session = new Session(); @Inject ExternalSensors externalSensors; @Inject ContinuousTracker tracker; @Inject ApplicationState state; private Map<String, Double> recentMeasurements = newHashMap(); private boolean paused; @Inject public void init() { telephonyManager.listen(new PhoneStateListener() { @Override public void onCallStateChanged(int state, String incomingNumber) { if (state == TelephonyManager.CALL_STATE_IDLE) { continueSession(); } else { pauseSession(); } } }, PhoneStateListener.LISTEN_CALL_STATE); eventBus.register(this); } @NotNull public Session getSession() { return session; } public void loadSession(long sessionId, @NotNull ProgressListener listener) { Preconditions.checkNotNull(listener); Session newSession = sessionRepository.loadFully(sessionId, listener); state.recording().startShowingOldSession(); setSession(newSession); } public void refreshUnits() { if (state.recording().isJustShowingCurrentValues()) setSession(new Session()); } void setSession(@NotNull Session session) { Preconditions.checkNotNull(session, "Cannot set null session"); this.session = session; notifyNewSession(session); } public boolean isSessionSaved() { return state.recording().isShowingOldSession(); } public void updateSession(Session from) { Preconditions.checkNotNull(from.getId(), "Unsaved session?"); setTitleTagsDescription(from.getId(), from.getTitle(), from.getTags(), from.getDescription()); } public Note makeANote(Date date, String text, String picturePath) { Note note = new Note(date, text, locationHelper.getLastLocation(), picturePath); tracker.addNote(note); return note; } public Iterable<Note> getNotes() { return getSession().getNotes(); } public void startSensors() { if (!state.sensors().started()) { locationHelper.start(); audioReader.start(); externalSensors.start(); state.sensors().start(); } } public synchronized void pauseSession() { if (state.recording().isRecording()) { paused = true; audioReader.stop(); } } public synchronized void continueSession() { if (paused) { paused = false; audioReader.start(); } } public void stopSensors() { if (state.recording().isRecording()) { return; } locationHelper.stop(); audioReader.stop(); state.sensors().stop(); } public boolean isRecording() { return state.recording().isRecording(); } public boolean canSessionHaveNotes() { return !session.isFixed(); } public void setContribute(long sessionId, boolean shouldContribute) { tracker.setContribute(sessionId, shouldContribute); } @Subscribe public synchronized void onEvent(SensorEvent event) { double value = event.getValue(); String sensorName = event.getSensorName(); Sensor sensor = sensorManager.getSensorByName(sensorName); recentMeasurements.put(sensorName, value); Location location = getLocation(); if (location != null && sensor != null && sensor.isEnabled()) { double latitude = location.getLatitude(); double longitude = location.getLongitude(); Measurement measurement = new Measurement(latitude, longitude, value, event.getMeasuredValue(), event.getDate()); if (state.recording().isRecording()) { MeasurementStream stream = prepareStream(event); tracker.addMeasurement(sensor, stream, measurement); } else { eventBus.post(new MeasurementEvent(measurement, sensor)); } } } private Location getLocation() { Location location = locationHelper.getLastLocation(); if(session.isFixed()) { location.setLatitude(session.getLatitude()); location.setLongitude(session.getLongitude()); } else if(session.isLocationless()) { location = new Location("fake"); location.setLatitude(TOTALLY_FAKE_COORDINATE); location.setLongitude(TOTALLY_FAKE_COORDINATE); } return location; } private MeasurementStream prepareStream(SensorEvent event) { String sensorName = event.getSensorName(); if (!session.hasStream(sensorName)) { MeasurementStream stream = event.stream(); tracker.addStream(stream); } MeasurementStream stream = session.getStream(sensorName); if(stream.isVisible()) { stream.markAs(MeasurementStream.Visibility.VISIBLE_RECONNECTED); } return stream; } public Note getNote(int i) { return session.getNotes().get(i); } public void deleteNote(Note note) { tracker.deleteNote(session, note); } public int getNoteCount() { return session.getNotes().size(); } public void restartSensors() { externalSensors.start(); } public Collection<MeasurementStream> getMeasurementStreams() { return newArrayList(session.getActiveMeasurementStreams()); } public MeasurementStream getMeasurementStream(String sensorName) { return session.getStream(sensorName); } @VisibleForTesting void discardSession() { Long sessionId = getSession().getId(); discardSession(sessionId); } public synchronized double getNow(Sensor sensor) { if (state.recording().isRecording()) { return tracker.getNow(sensor); } else { if (!recentMeasurements.containsKey(sensor.getSensorName())) { return 0; } return recentMeasurements.get(sensor.getSensorName()); } } private void notifyNewSession(Session session) { eventBus.post(new SessionChangeEvent(session)); } public void startMobileSession(boolean locationLess) { startSession(new Session(false), locationLess); } public void startFixedSession(String title, String tags, String description, boolean isIndoor, LatLng latlng) { Session newSession = new Session(true); newSession.setTitle(title); newSession.setTags(tags); newSession.setDescription(description); newSession.setIndoor(isIndoor); if(latlng == null) { newSession.setLatitude(TOTALLY_FAKE_COORDINATE); newSession.setLongitude(TOTALLY_FAKE_COORDINATE); } else { newSession.setLatitude(latlng.latitude); newSession.setLongitude(latlng.longitude); } startSession(newSession, true); } private void startSession(Session newSession, boolean locationLess) { eventBus.post(new SessionStartedEvent(getSession())); setSession(newSession); locationHelper.start(); startSensors(); state.recording().startRecording(); notificationHelper.showRecordingNotification(); if(!tracker.startTracking(getSession(), locationLess)) cleanup(); } public void stopSession() { tracker.stopTracking(getSession()); locationHelper.stop(); state.recording().stopRecording(); notificationHelper.hideRecordingNotification(); eventBus.post(new SessionStoppedEvent(getSession())); } public void finishSession(long sessionId) { synchronized (this) { tracker.complete(sessionId); Intents.triggerSync(applicationContext); } cleanup(); } public void discardSession(long sessionId) { tracker.discard(sessionId); cleanup(); } public void resetSession(long sessionId) { tracker.stopTracking(); cleanup(); } public void deleteSession() { Long sessionId = session.getId(); sessionRepository.markSessionForRemoval(sessionId); discardSession(sessionId); } private void cleanup() { locationHelper.stop(); state.recording().stopRecording(); setSession(new Session()); notificationHelper.hideRecordingNotification(); } public boolean isSessionStarted() { return state.recording().isRecording(); } public double getAvg(Sensor sensor) { String sensorName = sensor.getSensorName(); if (session.hasStream(sensorName)) { return session.getStream(sensorName).getAvg(); } else { return 0; } } public double getPeak(Sensor sensor) { String sensorName = sensor.getSensorName(); if (session.hasStream(sensorName)) { return session.getStream(sensorName).getPeak(); } else { return 0; } } public List<Measurement> getMeasurements(Sensor sensor) { String name = sensor.getSensorName(); if (session.hasStream(name)) { MeasurementStream stream = session.getStream(name); return stream.getMeasurements(); } else { return newArrayList(); } } public void deleteSensorStream(Sensor sensor) { String sensorName = sensor.getSensorName(); deleteSensorStream(sensorName); } void deleteSensorStream(String sensorName) { MeasurementStream stream = getMeasurementStream(sensorName); if(stream == null) { Logger.w("No stream for sensor [" + sensorName + "]"); return; } sessionRepository.deleteStream(session, stream); session.removeStream(stream); } public boolean isLocationless() { return session.isLocationless(); } public void setTitleTagsDescription(long sessionId, String title, String tags, String description) { tracker.setTitle(sessionId, title); tracker.setTags(sessionId, tags); tracker.setDescription(sessionId, description); } public void updateNote(final Note currentNote) { dbQueue.add(new WritableDatabaseTask<Void>() { @Override public Void execute(SQLiteDatabase writableDatabase) { ContentValues values = new ContentValues(); values.put(DBConstants.NOTE_TEXT, currentNote.getText()); @Language("SQLite") String whereClause = " WHERE " + DBConstants.NOTE_NUMBER + " = " + currentNote.getNumber() + " AND " + DBConstants.NOTE_SESSION_ID + " = " + session.getId(); writableDatabase.update(DBConstants.NOTE_TABLE_NAME, values, whereClause, null); return null; } }); } public void continueStreamingSession(Session session, boolean locationLess) { eventBus.post(new SessionStartedEvent(getSession())); setSession(session); locationHelper.start(); startSensors(); state.recording().startRecording(); notificationHelper.showRecordingNotification(); if(!tracker.continueTracking(getSession(), locationLess)) cleanup(); } }