diff --git a/MyTracks/src/com/google/android/apps/mytracks/MyTracksApplication.java b/MyTracks/src/com/google/android/apps/mytracks/MyTracksApplication.java index 92610c570..2788cd88d 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/MyTracksApplication.java +++ b/MyTracks/src/com/google/android/apps/mytracks/MyTracksApplication.java @@ -15,7 +15,6 @@ */ package com.google.android.apps.mytracks; -import com.google.android.apps.mytracks.content.TrackDataHub; import com.google.android.apps.mytracks.services.RemoveTempFilesService; import com.google.android.apps.mytracks.util.AnalyticsUtils; import com.google.android.apps.mytracks.util.ApiAdapterFactory; @@ -26,13 +25,11 @@ import android.content.Intent; /** * MyTracksApplication for keeping global state. - * + * * @author Jimmy Shih */ public class MyTracksApplication extends Application { - private TrackDataHub trackDataHub; - @Override public void onCreate() { super.onCreate(); @@ -43,16 +40,4 @@ public class MyTracksApplication extends Application { Intent intent = new Intent(this, RemoveTempFilesService.class); startService(intent); } - - /** - * Gets the application's TrackDataHub. - * - * Note: use synchronized to make sure only one instance is created per application. - */ - public synchronized TrackDataHub getTrackDataHub() { - if (trackDataHub == null) { - trackDataHub = TrackDataHub.newInstance(getApplicationContext()); - } - return trackDataHub; - } } diff --git a/MyTracks/src/com/google/android/apps/mytracks/TrackDetailActivity.java b/MyTracks/src/com/google/android/apps/mytracks/TrackDetailActivity.java index 058bd574c..a476ec7fd 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/TrackDetailActivity.java +++ b/MyTracks/src/com/google/android/apps/mytracks/TrackDetailActivity.java @@ -137,8 +137,7 @@ public class TrackDetailActivity extends AbstractMyTracksActivity implements Del getSharedPreferences(Constants.SETTINGS_NAME, Context.MODE_PRIVATE) .registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener); trackRecordingServiceConnection = new TrackRecordingServiceConnection(this, null); - trackDataHub = ((MyTracksApplication) getApplication()).getTrackDataHub(); - trackDataHub.loadTrack(trackId); + trackDataHub = TrackDataHub.newInstance(this); mapViewContainer = getLayoutInflater().inflate(R.layout.map, null); ApiAdapterFactory.getApiAdapter().disableHardwareAccelerated(mapViewContainer); @@ -167,7 +166,6 @@ public class TrackDetailActivity extends AbstractMyTracksActivity implements Del public void onNewIntent(Intent intent) { setIntent(intent); handleIntent(intent); - trackDataHub.loadTrack(trackId); showMarker(); } @@ -180,6 +178,7 @@ public class TrackDetailActivity extends AbstractMyTracksActivity implements Del @Override protected void onResume() { super.onResume(); + trackDataHub.loadTrack(trackId); TrackRecordingServiceConnectionUtils.resume(this, trackRecordingServiceConnection); setTitle(trackId == PreferencesUtils.getLong(this, R.string.recording_track_id_key)); } @@ -350,12 +349,19 @@ public class TrackDetailActivity extends AbstractMyTracksActivity implements Del } /** - * @return the mapViewContainer + * Gets the map view container. */ public View getMapViewContainer() { return mapViewContainer; } + /** + * Gets the {@link TrackDataHub}. + */ + public TrackDataHub getTrackDataHub() { + return trackDataHub; + } + /** * Handles the data in the intent. */ diff --git a/MyTracks/src/com/google/android/apps/mytracks/content/DataSource.java b/MyTracks/src/com/google/android/apps/mytracks/content/DataSource.java index 5cda864d7..6c82e9a0a 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/content/DataSource.java +++ b/MyTracks/src/com/google/android/apps/mytracks/content/DataSource.java @@ -149,25 +149,25 @@ public class DataSource { } /** - * Registers a compass listener. + * Registers a heading listener. * * @param listener the listener */ - public void registerCompassListener(SensorEventListener listener) { - Sensor compass = sensorManager.getDefaultSensor(Sensor.TYPE_ORIENTATION); - if (compass == null) { - Log.d(TAG, "No compass sensor."); + public void registerHeadingListener(SensorEventListener listener) { + Sensor heading = sensorManager.getDefaultSensor(Sensor.TYPE_ORIENTATION); + if (heading == null) { + Log.d(TAG, "No heading sensor."); return; } - sensorManager.registerListener(listener, compass, SensorManager.SENSOR_DELAY_UI); + sensorManager.registerListener(listener, heading, SensorManager.SENSOR_DELAY_UI); } /** - * Unregisters a compass listener. + * Unregisters a heading listener. * * @param listener the listener */ - public void unregisterCompassListener(SensorEventListener listener) { + public void unregisterHeadingListener(SensorEventListener listener) { sensorManager.unregisterListener(listener); } diff --git a/MyTracks/src/com/google/android/apps/mytracks/content/DataSourceManager.java b/MyTracks/src/com/google/android/apps/mytracks/content/DataSourceManager.java index 8da7841a7..03417a6a0 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/content/DataSourceManager.java +++ b/MyTracks/src/com/google/android/apps/mytracks/content/DataSourceManager.java @@ -133,11 +133,11 @@ public class DataSourceManager { } /** - * Listener for compass changes. + * Listener for heading changes. * * @author Jimmy Shih */ - private class CompassListener implements SensorEventListener { + private class HeadingListener implements SensorEventListener { @Override public void onAccuracyChanged(Sensor sensor, int accuracy) { @@ -174,7 +174,7 @@ public class DataSourceManager { private final WaypointsTableObserver waypointsTableObserver; private final TrackPointsTableObserver trackPointsTableObserver; private final CurrentLocationListener currentLocationListener; - private final CompassListener compassListener; + private final HeadingListener headingListener; private final PreferenceListener preferenceListener; public DataSourceManager(DataSource dataSource, DataSourceListener dataSourceListener) { @@ -186,7 +186,7 @@ public class DataSourceManager { waypointsTableObserver = new WaypointsTableObserver(); trackPointsTableObserver = new TrackPointsTableObserver(); currentLocationListener = new CurrentLocationListener(); - compassListener = new CompassListener(); + headingListener = new HeadingListener(); preferenceListener = new PreferenceListener(); } @@ -202,9 +202,9 @@ public class DataSourceManager { * Map SAMPLED_OUT_POINT_UPDATES to POINT_UPDATES since they correspond to * the same internal listener */ - if (neededListeners.contains(TrackDataType.SAMPLED_OUT_TRACK_POINTS)) { - neededListeners.remove(TrackDataType.SAMPLED_OUT_TRACK_POINTS); - neededListeners.add(TrackDataType.TRACK_POINTS_TABLE); + if (neededListeners.contains(TrackDataType.SAMPLED_OUT_TRACK_POINTS_TABLE)) { + neededListeners.remove(TrackDataType.SAMPLED_OUT_TRACK_POINTS_TABLE); + neededListeners.add(TrackDataType.SAMPLED_IN_TRACK_POINTS_TABLE); } Log.d(TAG, "Updating listeners " + neededListeners); @@ -248,18 +248,18 @@ public class DataSourceManager { case WAYPOINTS_TABLE: dataSource.registerContentObserver(WaypointsColumns.CONTENT_URI, waypointsTableObserver); break; - case TRACK_POINTS_TABLE: + case SAMPLED_IN_TRACK_POINTS_TABLE: dataSource.registerContentObserver( TrackPointsColumns.CONTENT_URI, trackPointsTableObserver); break; - case SAMPLED_OUT_TRACK_POINTS: + case SAMPLED_OUT_TRACK_POINTS_TABLE: // Do nothing. SAMPLED_OUT_POINT_UPDATES is mapped to POINT_UPDATES. break; case LOCATION: dataSource.registerLocationListener(currentLocationListener); break; - case COMPASS: - dataSource.registerCompassListener(compassListener); + case HEADING: + dataSource.registerHeadingListener(headingListener); break; case PREFERENCE: dataSource.registerOnSharedPreferenceChangeListener(preferenceListener); @@ -285,17 +285,17 @@ public class DataSourceManager { case WAYPOINTS_TABLE: dataSource.unregisterContentObserver(waypointsTableObserver); break; - case TRACK_POINTS_TABLE: + case SAMPLED_IN_TRACK_POINTS_TABLE: dataSource.unregisterContentObserver(trackPointsTableObserver); break; - case SAMPLED_OUT_TRACK_POINTS: + case SAMPLED_OUT_TRACK_POINTS_TABLE: // Do nothing. SAMPLED_OUT_POINT_UPDATES is mapped to POINT_UPDATES. break; case LOCATION: dataSource.unregisterLocationListener(currentLocationListener); break; - case COMPASS: - dataSource.unregisterCompassListener(compassListener); + case HEADING: + dataSource.unregisterHeadingListener(headingListener); break; case PREFERENCE: dataSource.unregisterOnSharedPreferenceChangeListener(preferenceListener); diff --git a/MyTracks/src/com/google/android/apps/mytracks/content/ListenerState.java b/MyTracks/src/com/google/android/apps/mytracks/content/ListenerState.java deleted file mode 100644 index e1e3598a7..000000000 --- a/MyTracks/src/com/google/android/apps/mytracks/content/ListenerState.java +++ /dev/null @@ -1,105 +0,0 @@ -/* - * Copyright 2011 Google Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.android.apps.mytracks.content; - -import java.util.EnumSet; - -/** - * State for a registered {@link TrackDataListener}. - * - * @author Jimmy Shih - */ -public class ListenerState { - - private TrackDataListener trackDataListener; - private EnumSet trackDataTypes; - - private long lastTrackId; - private long lastPointId; - private int lastSamplingFrequency; - private int numberOfLoadedPoints; - - public ListenerState( - TrackDataListener trackDataListener, EnumSet trackDataTypes) { - this.trackDataListener = trackDataListener; - this.trackDataTypes = trackDataTypes; - } - - /** - * Gets the {@link TrackDataListener}. - */ - public TrackDataListener getTrackDataListener() { - return trackDataListener; - } - - /** - * Gets the track data types. - */ - public EnumSet getTrackDataTypes() { - return trackDataTypes; - } - - /** - * Gets the last track id. - */ - public long getLastTrackId() { - return lastTrackId; - } - - /** - * Gets the last point id. - */ - public long getLastPointId() { - return lastPointId; - } - - /** - * Gets the last sampling frequency. - */ - public int getLastSamplingFrequency() { - return lastSamplingFrequency; - } - - /** - * Get the number of loaded points. - */ - public int getNumberOfLoadedPoints() { - return numberOfLoadedPoints; - } - - /** - * Sets the listener state. - * - * @param trackId track id - * @param pointId point id - * @param samplingFrequency sampling frequency - * @param loadedPoints number of loaded points - */ - public void setState(long trackId, long pointId, int samplingFrequency, int loadedPoints) { - lastTrackId = trackId; - lastPointId = pointId; - lastSamplingFrequency = samplingFrequency; - numberOfLoadedPoints = loadedPoints; - } - - /** - * Resets state. - */ - public void resetState() { - setState(0L, 0L, 0, 0); - } -} \ No newline at end of file diff --git a/MyTracks/src/com/google/android/apps/mytracks/content/TrackDataHub.java b/MyTracks/src/com/google/android/apps/mytracks/content/TrackDataHub.java index 52c8c3c4a..dcd2189fc 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/content/TrackDataHub.java +++ b/MyTracks/src/com/google/android/apps/mytracks/content/TrackDataHub.java @@ -13,20 +13,22 @@ * License for the specific language governing permissions and limitations under * the License. */ + package com.google.android.apps.mytracks.content; import static com.google.android.apps.mytracks.Constants.MAX_DISPLAYED_WAYPOINTS_POINTS; import static com.google.android.apps.mytracks.Constants.MAX_LOCATION_AGE_MS; import static com.google.android.apps.mytracks.Constants.MAX_NETWORK_AGE_MS; -import static com.google.android.apps.mytracks.Constants.TAG; import static com.google.android.apps.mytracks.Constants.TARGET_DISPLAYED_TRACK_POINTS; +import com.google.android.apps.mytracks.Constants; import com.google.android.apps.mytracks.content.MyTracksProviderUtils.DoubleBufferedLocationFactory; import com.google.android.apps.mytracks.content.MyTracksProviderUtils.LocationIterator; -import com.google.android.apps.mytracks.content.TrackDataListener.ProviderState; +import com.google.android.apps.mytracks.content.TrackDataListener.LocationState; import com.google.android.apps.mytracks.util.LocationUtils; import com.google.android.apps.mytracks.util.PreferencesUtils; import com.google.android.maps.mytracks.R; +import com.google.common.annotations.VisibleForTesting; import android.content.Context; import android.database.Cursor; @@ -42,1020 +44,734 @@ import java.util.EnumSet; import java.util.Set; /** - * Track data hub, which receives data (both live and recorded) from many - * different sources and distributes it to those interested after some standard - * processing. - * - * TODO: Simplify the threading model here, it's overly complex and it's not obvious why - * certain race conditions won't happen. - * + * Track data hub. Receives data from {@link DataSource} and distributes it to + * {@link TrackDataListener} after some processing. + * * @author Rodrigo Damazio */ -public class TrackDataHub { +public class TrackDataHub implements DataSourceListener { - // Overridable constants - private final int targetNumPoints; + private static final String TAG = TrackDataHub.class.getSimpleName(); - /** Listener which receives events from the system. */ - private class HubDataSourceListener implements DataSourceListener { - @Override - public void notifyTracksTableUpdated() { - TrackDataHub.this.notifyTrackUpdated(getListenersFor(TrackDataType.TRACKS_TABLE)); - } + // One hour in milliseconds + private static final int ONE_HOUR = 60 * 60 * 1000; - @Override - public void notifyWaypointsTableUpdated() { - TrackDataHub.this.notifyWaypointUpdated(getListenersFor(TrackDataType.WAYPOINTS_TABLE)); - } - - @Override - public void notifyTrackPointsTableUpdated() { - TrackDataHub.this.notifyPointsUpdated(true, 0, 0, - getListenersFor(TrackDataType.TRACK_POINTS_TABLE), - getListenersFor(TrackDataType.SAMPLED_OUT_TRACK_POINTS)); - } - - @Override - public void notifyPreferenceChanged(String key) { - TrackDataHub.this.notifyPreferenceChanged(key); - } - - @Override - public void notifyLocationProviderEnabled(boolean enabled) { - hasProviderEnabled = enabled; - TrackDataHub.this.notifyFixType(); - } - - @Override - public void notifyLocationProviderAvailable(boolean available) { - hasFix = available; - TrackDataHub.this.notifyFixType(); - } - - @Override - public void notifyLocationChanged(Location loc) { - TrackDataHub.this.notifyLocationChanged(loc, - getListenersFor(TrackDataType.LOCATION)); - } - - @Override - public void notifyHeadingChanged(float heading) { - lastSeenMagneticHeading = heading; - maybeUpdateDeclination(); - TrackDataHub.this.notifyHeadingChanged(getListenersFor(TrackDataType.COMPASS)); - } - } - - // Application services private final Context context; - private final MyTracksProviderUtils providerUtils; - - // Get content notifications on the main thread, send listener callbacks in another. - // This ensures listener calls are serialized. - private HandlerThread listenerHandlerThread; - private Handler listenerHandler; - - /** Manager for external listeners (those from activities). */ private final TrackDataManager trackDataManager; + private final MyTracksProviderUtils myTracksProviderUtils; + private final int targetNumPoints; + private final DoubleBufferedLocationFactory locationFactory; + private boolean started; + private HandlerThread handlerThread; + private Handler handler; private DataSource dataSource; private DataSourceManager dataSourceManager; - /** Condensed listener for system data listener events. */ - private final DataSourceListener dataSourceListener = new HubDataSourceListener(); - - // Cached preference values - private int minRequiredAccuracy; + // Preference values + private long selectedTrackId; private boolean metricUnits; private boolean reportSpeed; + private int minRequiredAccuracy; - // Cached sensor readings - private float declination; - private long lastDeclinationUpdate; - private float lastSeenMagneticHeading; + // Heading values + private float lastHeading = 0; + private float lastDeclination = 0; + private long lastDeclinationUpdate = 0; - // Cached GPS readings - private Location lastSeenLocation; + // Location values + private Location lastSeenLocation = null; private boolean hasProviderEnabled = true; - private boolean hasFix; - private boolean hasGoodFix; + private boolean hasFix = false; + private boolean hasGoodFix = false; - // Transient state about the selected track - private long selectedTrackId; + // Track points sampling state + private int numLoadedPoints; private long firstSeenLocationId; private long lastSeenLocationId; - private int numLoadedPoints; - private int lastSamplingFrequency; - private DoubleBufferedLocationFactory locationFactory; - - private boolean started = false; /** - * Builds a new {@link TrackDataHub} instance. + * Creates a new instance. */ public synchronized static TrackDataHub newInstance(Context context) { - MyTracksProviderUtils providerUtils = MyTracksProviderUtils.Factory.get(context); - return new TrackDataHub( - context, new TrackDataManager(), providerUtils, TARGET_DISPLAYED_TRACK_POINTS); + return new TrackDataHub(context, new TrackDataManager(), MyTracksProviderUtils.Factory.get( + context), TARGET_DISPLAYED_TRACK_POINTS); } /** - * Injection constructor. + * Constructor. + * + * @param context the context + * @param trackDataManager the track data manager + * @param myTracksProviderUtils the my tracks provider utils + * @param targetNumPoints the target number of points */ - // @VisibleForTesting - TrackDataHub(Context ctx, TrackDataManager trackDataManager, MyTracksProviderUtils providerUtils, - int targetNumPoints) { - this.context = ctx; + @VisibleForTesting + TrackDataHub(Context context, TrackDataManager trackDataManager, + MyTracksProviderUtils myTracksProviderUtils, int targetNumPoints) { + this.context = context; this.trackDataManager = trackDataManager; - this.providerUtils = providerUtils; + this.myTracksProviderUtils = myTracksProviderUtils; this.targetNumPoints = targetNumPoints; this.locationFactory = new DoubleBufferedLocationFactory(); - - resetState(); + resetSamplingState(); } /** - * Starts listening to data sources and reporting the data to external - * listeners. + * Starts. */ public void start() { - Log.i(TAG, "TrackDataHub.start"); - if (isStarted()) { - Log.w(TAG, "Already started, ignoring"); + if (started) { + Log.i(TAG, "TrackDataHub already started, ignoring start."); return; } started = true; - - listenerHandlerThread = new HandlerThread("trackDataContentThread"); - listenerHandlerThread.start(); - listenerHandler = new Handler(listenerHandlerThread.getLooper()); + handlerThread = new HandlerThread("TrackDataHubHandlerThread"); + handlerThread.start(); + handler = new Handler(handlerThread.getLooper()); dataSource = newDataSource(); - dataSourceManager = new DataSourceManager(dataSource, dataSourceListener); + dataSourceManager = new DataSourceManager(dataSource, this); - // This may or may not register internal listeners, depending on whether - // we already had external listeners. - dataSourceManager.updateListeners(getNeededListenerTypes()); - loadSharedPreferences(); - - // If there were listeners already registered, make sure they become up-to-date. - loadDataForAllListeners(); - } - - // @VisibleForTesting - protected DataSource newDataSource() { - return new DataSource(context); + runInHanderThread(new Runnable() { + @Override + public void run() { + dataSourceManager.updateListeners(trackDataManager.getRegisteredTrackDataTypes()); + selectedTrackId = PreferencesUtils.getLong(context, R.string.selected_track_id_key); + metricUnits = PreferencesUtils.getBoolean( + context, R.string.metric_units_key, PreferencesUtils.METRIC_UNITS_DEFAULT); + reportSpeed = PreferencesUtils.getBoolean( + context, R.string.report_speed_key, PreferencesUtils.REPORT_SPEED_DEFAULT); + minRequiredAccuracy = PreferencesUtils.getInt(context, R.string.min_required_accuracy_key, + PreferencesUtils.MIN_REQUIRED_ACCURACY_DEFAULT); + loadDataForAll(); + } + }); } /** - * Stops listening to data sources and reporting the data to external - * listeners. + * Stops. */ public void stop() { - Log.i(TAG, "TrackDataHub.stop"); - if (!isStarted()) { - Log.w(TAG, "Not started, ignoring"); + if (!started) { + Log.i(TAG, "TrackDataHub not started, ignoring stop."); return; } - - // Unregister internal listeners even if there are external listeners registered. - dataSourceManager.unregisterAllListeners(); - listenerHandlerThread.getLooper().quit(); - started = false; + dataSourceManager.unregisterAllListeners(); + handlerThread.getLooper().quit(); + + handlerThread = null; + handler = null; dataSource = null; dataSourceManager = null; - listenerHandlerThread = null; - listenerHandler = null; - } - - private boolean isStarted() { - return started; - } - - @Override - protected void finalize() throws Throwable { - if (isStarted() || - (listenerHandlerThread != null && listenerHandlerThread.isAlive())) { - Log.e(TAG, "Forgot to stop() TrackDataHub"); - } - - super.finalize(); - } - - private void loadSharedPreferences() { - selectedTrackId = PreferencesUtils.getLong(context, R.string.selected_track_id_key); - metricUnits = PreferencesUtils.getBoolean( - context, R.string.metric_units_key, PreferencesUtils.METRIC_UNITS_DEFAULT); - reportSpeed = PreferencesUtils.getBoolean( - context, R.string.report_speed_key, PreferencesUtils.REPORT_SPEED_DEFAULT); - minRequiredAccuracy = PreferencesUtils.getInt(context, R.string.min_required_accuracy_key, - PreferencesUtils.MIN_REQUIRED_ACCURACY_DEFAULT); - } - - /** Updates known magnetic declination if needed. */ - private void maybeUpdateDeclination() { - if (lastSeenLocation == null) { - // We still don't know where we are. - return; - } - - // Update the declination every hour - long now = System.currentTimeMillis(); - if (now - lastDeclinationUpdate < 60 * 60 * 1000) { - return; - } - - lastDeclinationUpdate = now; - long timestamp = lastSeenLocation.getTime(); - if (timestamp == 0) { - // Hack for Samsung phones which don't populate the time field - timestamp = now; - } - - declination = getDeclinationFor(lastSeenLocation, timestamp); - Log.i(TAG, "Updated magnetic declination to " + declination); - } - - // @VisibleForTesting - protected float getDeclinationFor(Location location, long timestamp) { - GeomagneticField field = new GeomagneticField( - (float) location.getLatitude(), - (float) location.getLongitude(), - (float) location.getAltitude(), - timestamp); - return field.getDeclination(); } /** - * Forces the current location to be updated and reported to all listeners. - * The reported location may be from the network provider if the GPS provider - * is not available or doesn't have a fix. + * Loads a track. + * + * @param trackId the track id + */ + public void loadTrack(final long trackId) { + runInHanderThread(new Runnable() { + @Override + public void run() { + if (trackId == selectedTrackId) { + Log.i(TAG, "Not reloading track " + trackId); + return; + } + selectedTrackId = trackId; + PreferencesUtils.setLong(context, R.string.selected_track_id_key, selectedTrackId); + loadDataForAll(); + } + }); + } + + /** + * Registers a {@link TrackDataListener}. + * + * @param trackDataListener the track data listener + * @param trackDataTypes the track data types + */ + public void registerTrackDataListener( + final TrackDataListener trackDataListener, final EnumSet trackDataTypes) { + runInHanderThread(new Runnable() { + @Override + public void run() { + trackDataManager.registerListener(trackDataListener, trackDataTypes); + dataSourceManager.updateListeners(trackDataManager.getRegisteredTrackDataTypes()); + loadDataForListener(trackDataListener); + } + }); + } + + /** + * Unregisters a {@link TrackDataListener}. + * + * @param trackDataListener the track data listener + */ + public void unregisterTrackDataListener(final TrackDataListener trackDataListener) { + runInHanderThread(new Runnable() { + @Override + public void run() { + trackDataManager.unregisterListener(trackDataListener); + dataSourceManager.updateListeners(trackDataManager.getRegisteredTrackDataTypes()); + } + }); + } + + /** + * Reloads data for a {@link TrackDataListener}. + */ + public void reloadDataForListener(final TrackDataListener trackDataListener) { + runInHanderThread(new Runnable() { + @Override + public void run() { + loadDataForListener(trackDataListener); + } + }); + } + + /** + * Forces update location and reports to all listeners. */ public void forceUpdateLocation() { - if (!isStarted()) { - Log.w(TAG, "Not started, not forcing location update"); - return; - } - Log.i(TAG, "Forcing location update"); - - Location loc = dataSource.getLastKnownLocation(); - if (loc != null) { - notifyLocationChanged(loc, getListenersFor(TrackDataType.LOCATION)); - } + final Location location = dataSource.getLastKnownLocation(); + runInHanderThread(new Runnable() { + @Override + public void run() { + notifyLocationChanged( + location, false, trackDataManager.getListeners(TrackDataType.LOCATION)); + } + }); } - - /** Returns the ID of the currently-selected track. */ - public long getSelectedTrackId() { - if (!isStarted()) { - loadSharedPreferences(); - } - return selectedTrackId; - } - - /** Returns whether there's a track currently selected. */ - public boolean isATrackSelected() { - return getSelectedTrackId() > 0; - } - - /** Returns whether the selected track is still being recorded. */ - public boolean isRecordingSelected() { - if (!isStarted()) { - loadSharedPreferences(); - } + /** + * Returns true if the selected track is recording. + */ + public boolean isSelectedTrackRecording() { long recordingTrackId = PreferencesUtils.getLong(context, R.string.recording_track_id_key); return recordingTrackId != PreferencesUtils.RECORDING_TRACK_ID_DEFAULT && recordingTrackId == selectedTrackId; } - /** - * Loads the given track and makes it the currently-selected one. - * It is ok to call this method before {@link #start}, and in that case - * the data will only be passed to listeners when {@link #start} is called. - * - * @param trackId the ID of the track to load - */ - public void loadTrack(long trackId) { - if (trackId == selectedTrackId) { - Log.w(TAG, "Not reloading track, id=" + trackId); - return; - } - - // Save the selection to memory and flush. - selectedTrackId = trackId; - PreferencesUtils.setLong(context, R.string.selected_track_id_key, selectedTrackId); - - // Force it to reload data from the beginning. - Log.d(TAG, "Loading track"); - resetState(); - - loadDataForAllListeners(); - } - - /** - * Resets the internal state of what data has already been loaded into listeners. - */ - private void resetState() { - firstSeenLocationId = -1; - lastSeenLocationId = -1; - numLoadedPoints = 0; - lastSamplingFrequency = -1; - } - - /** - * Unloads the currently-selected track. - */ - public void unloadCurrentTrack() { - loadTrack(-1); - } - - public void registerTrackDataListener( - TrackDataListener listener, EnumSet dataTypes) { - synchronized (trackDataManager) { - ListenerState listenerState = trackDataManager.registerListener(listener, dataTypes); - - // Don't load any data or start internal listeners if start() hasn't been - // called. When it is called, we'll do both things. - if (!isStarted()) { - return; - } - - loadNewDataForListener(listenerState); - - dataSourceManager.updateListeners(getNeededListenerTypes()); - } - } - - public void unregisterTrackDataListener(TrackDataListener listener) { - synchronized (trackDataManager) { - trackDataManager.unregisterListener(listener); - - // Don't load any data or start internal listeners if start() hasn't been - // called. When it is called, we'll do both things. - if (!isStarted()) return; - - dataSourceManager.updateListeners(getNeededListenerTypes()); - } - } - - /** - * Reloads all track data received so far into the specified listeners. - */ - public void reloadDataForListener(TrackDataListener listener) { - synchronized (trackDataManager) { - ListenerState listenerState = trackDataManager.getListenerState(listener); - listenerState.resetState(); - loadNewDataForListener(listenerState); - } - } - - /** - * Reloads all track data received so far into the specified listeners. - * - * Assumes it's called from a block that synchronizes on {@link #trackDataManager}. - */ - private void loadNewDataForListener(final ListenerState listenerState) { - if (!isStarted()) { - Log.w(TAG, "Not started, not reloading"); - return; - } - if (listenerState == null) { - Log.w(TAG, "Not reloading for null listener state"); - return; - } - - // If a listener happens to be added after this method but before the Runnable below is - // executed, it will have triggered a separate call to load data only up to the point this - // listener got to. This is ensured by being synchronized on listeners. - final boolean isOnlyListener = (trackDataManager.getNumberOfListeners() == 1); - - runInListenerThread(new Runnable() { - @SuppressWarnings("unchecked") - @Override + @Override + public void notifyTracksTableUpdated() { + runInHanderThread(new Runnable() { + @Override public void run() { - // Reload everything if either it's a different track, or the track has been resampled - // (this also covers the case of a new registration). - boolean reloadAll = listenerState.getLastTrackId() != selectedTrackId || - listenerState.getLastSamplingFrequency() != lastSamplingFrequency; - Log.d(TAG, "Doing a " + (reloadAll ? "full" : "partial") + " reload for " + listenerState); + notifyTracksTableUpdate(trackDataManager.getListeners(TrackDataType.TRACKS_TABLE)); + } + }); + } - TrackDataListener listener = listenerState.getTrackDataListener(); - Set listenerSet = Collections.singleton(listener); + @Override + public void notifyWaypointsTableUpdated() { + runInHanderThread(new Runnable() { + @Override + public void run() { + notifyWaypointsTableUpdate(trackDataManager.getListeners(TrackDataType.WAYPOINTS_TABLE)); + } + }); + } - EnumSet trackDataTypes = listenerState.getTrackDataTypes(); - - if (trackDataTypes.contains(TrackDataType.PREFERENCE)) { - reloadAll |= listener.onUnitsChanged(metricUnits); - reloadAll |= listener.onReportSpeedChanged(reportSpeed); - } + @Override + public void notifyTrackPointsTableUpdated() { + runInHanderThread(new Runnable() { + @Override + public void run() { + notifyTrackPointsTableUpdate( + true, trackDataManager.getListeners(TrackDataType.SAMPLED_IN_TRACK_POINTS_TABLE), + trackDataManager.getListeners(TrackDataType.SAMPLED_OUT_TRACK_POINTS_TABLE)); + } + }); + } - if (reloadAll && trackDataTypes.contains(TrackDataType.SELECTED_TRACK)) { - notifySelectedTrackChanged(selectedTrackId, listenerSet); - } - - if (trackDataTypes.contains(TrackDataType.TRACKS_TABLE)) { - notifyTrackUpdated(listenerSet); - } - - boolean interestedInPoints = trackDataTypes.contains(TrackDataType.TRACK_POINTS_TABLE); - boolean interestedInSampledOutPoints = trackDataTypes.contains( - TrackDataType.SAMPLED_OUT_TRACK_POINTS); - if (interestedInPoints || interestedInSampledOutPoints) { - long minPointId = 0; - int previousNumPoints = 0; - - if (reloadAll) { - // Clear existing points and send them all again - notifyPointsCleared(listenerSet); - } else { - // Send only new points - minPointId = listenerState.getLastPointId() + 1; - previousNumPoints = listenerState.getNumberOfLoadedPoints(); + @Override + public void notifyPreferenceChanged(final String key) { + runInHanderThread(new Runnable() { + @Override + public void run() { + if (PreferencesUtils.getKey(context, R.string.min_required_accuracy_key).equals(key)) { + minRequiredAccuracy = PreferencesUtils.getInt(context, R.string.min_required_accuracy_key, + PreferencesUtils.MIN_REQUIRED_ACCURACY_DEFAULT); + } else if (PreferencesUtils.getKey(context, R.string.metric_units_key).equals(key)) { + metricUnits = PreferencesUtils.getBoolean( + context, R.string.metric_units_key, PreferencesUtils.METRIC_UNITS_DEFAULT); + Set trackDataListeners = trackDataManager.getListeners( + TrackDataType.PREFERENCE); + for (TrackDataListener trackDataListener : trackDataListeners) { + if (trackDataListener.onMetricUnitsChanged(metricUnits)) { + loadDataForListener(trackDataListener); + } } - - // If this is the only listener we have registered, keep the state that we serve to it as - // a reference for other future listeners. - if (isOnlyListener && reloadAll) { - resetState(); + } else if (PreferencesUtils.getKey(context, R.string.report_speed_key).equals(key)) { + reportSpeed = PreferencesUtils.getBoolean( + context, R.string.report_speed_key, PreferencesUtils.REPORT_SPEED_DEFAULT); + Set trackDataListeners = trackDataManager.getListeners( + TrackDataType.PREFERENCE); + for (TrackDataListener trackDataListener : trackDataListeners) { + if (trackDataListener.onReportSpeedChanged(reportSpeed)) { + loadDataForListener(trackDataListener); + } } - - notifyPointsUpdated(isOnlyListener, - minPointId, - previousNumPoints, - listenerSet, - interestedInSampledOutPoints ? listenerSet : Collections.EMPTY_SET); - } - - if (trackDataTypes.contains(TrackDataType.WAYPOINTS_TABLE)) { - notifyWaypointUpdated(listenerSet); - } - - if (trackDataTypes.contains(TrackDataType.LOCATION)) { - if (lastSeenLocation != null) { - notifyLocationChanged(lastSeenLocation, true, listenerSet); - } else { - notifyFixType(); + } else if (PreferencesUtils.getKey(context, R.string.selected_track_id_key).equals(key)) { + long trackId = PreferencesUtils.getLong(context, R.string.selected_track_id_key); + if (trackId == selectedTrackId) { + Log.i(TAG, "Not reloading track " + trackId); + return; } - } - - if (trackDataTypes.contains(TrackDataType.COMPASS)) { - notifyHeadingChanged(listenerSet); + selectedTrackId = trackId; + loadDataForAll(); } } }); } - /** - * Reloads all track data received so far into the specified listeners. - */ - private void loadDataForAllListeners() { - if (!isStarted()) { - Log.w(TAG, "Not started, not reloading"); - return; - } - synchronized (trackDataManager) { - if (trackDataManager.getNumberOfListeners() == 0) { - Log.d(TAG, "No listeners, not reloading"); - return; - } - } - - runInListenerThread(new Runnable() { - @Override + @Override + public void notifyLocationProviderEnabled(final boolean enabled) { + runInHanderThread(new Runnable() { + @Override public void run() { - // Ignore the return values here, we're already sending the full data set anyway - for (TrackDataListener listener : - getListenersFor(TrackDataType.PREFERENCE)) { - listener.onUnitsChanged(metricUnits); - listener.onReportSpeedChanged(reportSpeed); - } + hasProviderEnabled = enabled; + notifyLocationStateChanged(trackDataManager.getListeners(TrackDataType.LOCATION)); + } + }); + } - notifySelectedTrackChanged(selectedTrackId, - getListenersFor(TrackDataType.SELECTED_TRACK)); + @Override + public void notifyLocationProviderAvailable(final boolean available) { + runInHanderThread(new Runnable() { + @Override + public void run() { + hasFix = available; + notifyLocationStateChanged(trackDataManager.getListeners(TrackDataType.LOCATION)); + } + }); + } - notifyTrackUpdated(getListenersFor(TrackDataType.TRACKS_TABLE)); + @Override + public void notifyLocationChanged(final Location location) { + runInHanderThread(new Runnable() { + @Override + public void run() { + notifyLocationChanged( + location, false, trackDataManager.getListeners(TrackDataType.LOCATION)); + } + }); + } - Set pointListeners = - getListenersFor(TrackDataType.TRACK_POINTS_TABLE); - Set sampledOutPointListeners = - getListenersFor(TrackDataType.SAMPLED_OUT_TRACK_POINTS); - notifyPointsCleared(pointListeners); - notifyPointsUpdated(true, 0, 0, pointListeners, sampledOutPointListeners); - - notifyWaypointUpdated(getListenersFor(TrackDataType.WAYPOINTS_TABLE)); + @Override + public void notifyHeadingChanged(final float heading) { + runInHanderThread(new Runnable() { + @Override + public void run() { + lastHeading = heading; if (lastSeenLocation != null) { - notifyLocationChanged(lastSeenLocation, true, - getListenersFor(TrackDataType.LOCATION)); - } else { - notifyFixType(); + // Update the declination at most once an hour + long now = System.currentTimeMillis(); + if (now - lastDeclinationUpdate > ONE_HOUR) { + lastDeclinationUpdate = now; + long timestamp = lastSeenLocation.getTime(); + if (timestamp == 0) { + // Hack for Samsung phones which don't populate the time field + timestamp = now; + } + lastDeclination = getDeclination(lastSeenLocation, timestamp); + } } - - notifyHeadingChanged(getListenersFor(TrackDataType.COMPASS)); + notifyHeadingChange(trackDataManager.getListeners(TrackDataType.HEADING)); } }); } /** - * Called when a preference changes. + * Loads data for all listeners. To be run in the {@link #handler} thread. + */ + private void loadDataForAll() { + resetSamplingState(); + if (trackDataManager.getNumberOfListeners() == 0) { + return; + } + + for (TrackDataListener trackDataListener : + trackDataManager.getListeners(TrackDataType.PREFERENCE)) { + trackDataListener.onMetricUnitsChanged(metricUnits); + trackDataListener.onReportSpeedChanged(reportSpeed); + } + + notifySelectedTrackChanged(trackDataManager.getListeners(TrackDataType.SELECTED_TRACK)); + notifyTracksTableUpdate(trackDataManager.getListeners(TrackDataType.TRACKS_TABLE)); + + for (TrackDataListener listener : + trackDataManager.getListeners(TrackDataType.SAMPLED_IN_TRACK_POINTS_TABLE)) { + listener.clearTrackPoints(); + } + notifyTrackPointsTableUpdate(true, + trackDataManager.getListeners(TrackDataType.SAMPLED_IN_TRACK_POINTS_TABLE), + trackDataManager.getListeners(TrackDataType.SAMPLED_OUT_TRACK_POINTS_TABLE)); + notifyWaypointsTableUpdate(trackDataManager.getListeners(TrackDataType.WAYPOINTS_TABLE)); + + if (lastSeenLocation != null) { + notifyLocationChanged( + lastSeenLocation, true, trackDataManager.getListeners(TrackDataType.LOCATION)); + } else { + notifyLocationStateChanged(trackDataManager.getListeners(TrackDataType.LOCATION)); + } + notifyHeadingChange(trackDataManager.getListeners(TrackDataType.HEADING)); + } + + /** + * Loads data for a listener. To be run in the {@link #handler} thread. * - * @param key the key to the preference that changed + * @param trackDataListener the track data listener. */ - private void notifyPreferenceChanged(String key) { - if (PreferencesUtils.getKey(context, R.string.min_required_accuracy_key).equals(key)) { - minRequiredAccuracy = PreferencesUtils.getInt(context, R.string.min_required_accuracy_key, - PreferencesUtils.MIN_REQUIRED_ACCURACY_DEFAULT); - } else if (PreferencesUtils.getKey(context, R.string.metric_units_key).equals(key)) { - metricUnits = PreferencesUtils.getBoolean( - context, R.string.metric_units_key, PreferencesUtils.METRIC_UNITS_DEFAULT); - notifyUnitsChanged(); - } else if (PreferencesUtils.getKey(context, R.string.report_speed_key).equals(key)) { - reportSpeed = PreferencesUtils.getBoolean( - context, R.string.report_speed_key, PreferencesUtils.REPORT_SPEED_DEFAULT); - notifySpeedReportingChanged(); - } else if (PreferencesUtils.getKey(context, R.string.selected_track_id_key).equals(key)) { - loadTrack(PreferencesUtils.getLong(context, R.string.selected_track_id_key)); + private void loadDataForListener(TrackDataListener trackDataListener) { + Set trackDataListeners = Collections.singleton(trackDataListener); + EnumSet trackDataTypes = trackDataManager.getTrackDataTypes(trackDataListener); + + if (trackDataTypes.contains(TrackDataType.PREFERENCE)) { + trackDataListener.onMetricUnitsChanged(metricUnits); + trackDataListener.onReportSpeedChanged(reportSpeed); + } + + if (trackDataTypes.contains(TrackDataType.SELECTED_TRACK)) { + notifySelectedTrackChanged(trackDataListeners); + } + + if (trackDataTypes.contains(TrackDataType.TRACKS_TABLE)) { + notifyTracksTableUpdate(trackDataListeners); + } + + boolean hasSampledIn = trackDataTypes.contains(TrackDataType.SAMPLED_IN_TRACK_POINTS_TABLE); + boolean hasSampledOut = trackDataTypes.contains(TrackDataType.SAMPLED_OUT_TRACK_POINTS_TABLE); + if (hasSampledIn || hasSampledOut) { + trackDataListener.clearTrackPoints(); + boolean isOnlyListener = trackDataManager.getNumberOfListeners() == 1; + if (isOnlyListener) { + resetSamplingState(); + } + Set sampledInListeners = trackDataListeners; + Set sampledOutListeners = hasSampledOut ? trackDataListeners + : Collections. emptySet(); + notifyTrackPointsTableUpdate(isOnlyListener, sampledInListeners, sampledOutListeners); + } + + if (trackDataTypes.contains(TrackDataType.WAYPOINTS_TABLE)) { + notifyWaypointsTableUpdate(trackDataListeners); + } + + if (trackDataTypes.contains(TrackDataType.LOCATION)) { + if (lastSeenLocation != null) { + notifyLocationChanged(lastSeenLocation, true, trackDataListeners); + } else { + notifyLocationStateChanged(trackDataListeners); + } + } + + if (trackDataTypes.contains(TrackDataType.HEADING)) { + notifyHeadingChange(trackDataListeners); } } - /** Called when the speed/pace reporting preference changes. */ - private void notifySpeedReportingChanged() { - if (!isStarted()) return; + /** + * Notifies selected track changed. To be run in the {@link #handler} thread. + * + * @param trackDataListeners the track data listeners to notify + */ + private void notifySelectedTrackChanged(Set trackDataListeners) { + if (trackDataListeners.isEmpty()) { + return; + } + Track track = myTracksProviderUtils.getTrack(selectedTrackId); + for (TrackDataListener trackDataListener : trackDataListeners) { + trackDataListener.onSelectedTrackChanged(track, isSelectedTrackRecording()); + } + } - runInListenerThread(new Runnable() { - @Override - public void run() { - Set displayListeners = - getListenersFor(TrackDataType.PREFERENCE); + /** + * Notifies track table update. To be run in the {@link #handler} thread. + * + * @param trackDataListeners the track data listeners to notify + */ + private void notifyTracksTableUpdate(Set trackDataListeners) { + if (trackDataListeners.isEmpty()) { + return; + } + Track track = myTracksProviderUtils.getTrack(selectedTrackId); + for (TrackDataListener trackDataListener : trackDataListeners) { + trackDataListener.onTrackUpdated(track); + } + } - for (TrackDataListener listener : displayListeners) { - // TODO: Do the reloading just once for all interested listeners - if (listener.onReportSpeedChanged(reportSpeed)) { - synchronized (trackDataManager) { - reloadDataForListener(listener); - } + /** + * Notifies waypoint table update. Currently, reloads all the waypoints up to + * {@link Constants#MAX_DISPLAYED_WAYPOINTS_POINTS}. To be run in the + * {@link #handler} thread. + * + * @param trackDataListeners the track data listeners to notify + */ + private void notifyWaypointsTableUpdate(Set trackDataListeners) { + if (trackDataListeners.isEmpty()) { + return; + } + + for (TrackDataListener trackDataListener : trackDataListeners) { + trackDataListener.clearWaypoints(); + } + + Cursor cursor = null; + try { + cursor = myTracksProviderUtils.getWaypointsCursor( + selectedTrackId, 0L, MAX_DISPLAYED_WAYPOINTS_POINTS); + if (cursor != null && cursor.moveToFirst()) { + do { + Waypoint waypoint = myTracksProviderUtils.createWaypoint(cursor); + if (!LocationUtils.isValidLocation(waypoint.getLocation())) { + continue; } - } - } - }); - } - - /** Called when the metric units setting changes. */ - private void notifyUnitsChanged() { - if (!isStarted()) return; - - runInListenerThread(new Runnable() { - @Override - public void run() { - Set displayListeners = getListenersFor(TrackDataType.PREFERENCE); - - for (TrackDataListener listener : displayListeners) { - if (listener.onUnitsChanged(metricUnits)) { - synchronized (trackDataManager) { - reloadDataForListener(listener); - } + for (TrackDataListener trackDataListener : trackDataListeners) { + trackDataListener.onNewWaypoint(waypoint); } - } + } while (cursor.moveToNext()); } - }); - } - - /** Notifies about the current GPS fix state. */ - private void notifyFixType() { - final TrackDataListener.ProviderState state; - if (!hasProviderEnabled) { - state = ProviderState.DISABLED; - } else if (!hasFix) { - state = ProviderState.NO_FIX; - } else if (!hasGoodFix) { - state = ProviderState.BAD_FIX; - } else { - state = ProviderState.GOOD_FIX; - } - - runInListenerThread(new Runnable() { - @Override - public void run() { - // Notify to everyone. - Log.d(TAG, "Notifying fix type: " + state); - for (TrackDataListener listener : - getListenersFor(TrackDataType.LOCATION)) { - listener.onProviderStateChange(state); - } - } - }); - } - - /** - * Notifies the the current location has changed, without any filtering. - * If the state of GPS fix has changed, that will also be reported. - * - * @param location the current location - * @param listeners the listeners to notify - */ - private void notifyLocationChanged(Location location, Set listeners) { - notifyLocationChanged(location, false, listeners); - } - - /** - * Notifies that the current location has changed, without any filtering. - * If the state of GPS fix has changed, that will also be reported. - * - * @param location the current location - * @param forceUpdate whether to force the notifications to happen - * @param listeners the listeners to notify - */ - private void notifyLocationChanged(Location location, boolean forceUpdate, - final Set listeners) { - if (location == null) return; - if (listeners.isEmpty()) return; - - boolean isGpsLocation = location.getProvider().equals(LocationManager.GPS_PROVIDER); - - boolean oldHasFix = hasFix; - boolean oldHasGoodFix = hasGoodFix; - - long now = System.currentTimeMillis(); - if (isGpsLocation) { - // We consider a good fix to be a recent one with reasonable accuracy. - hasFix = !isLocationOld(location, now, MAX_LOCATION_AGE_MS); - hasGoodFix = (location.getAccuracy() <= minRequiredAccuracy); - } else { - if (!isLocationOld(lastSeenLocation, now, MAX_LOCATION_AGE_MS)) { - // This is a network location, but we have a recent/valid GPS location, just ignore this. - return; - } - - // We haven't gotten a GPS location in a while (or at all), assume we have no fix anymore. - hasFix = false; - hasGoodFix = false; - - // If the network location is recent, we'll use that. - if (isLocationOld(location, now, MAX_NETWORK_AGE_MS)) { - // Alas, we have no clue where we are. - location = null; + } finally { + if (cursor != null) { + cursor.close(); } } - if (hasFix != oldHasFix || hasGoodFix != oldHasGoodFix || forceUpdate) { - notifyFixType(); + for (TrackDataListener trackDataListener : trackDataListeners) { + trackDataListener.onNewWaypointsDone(); } - - lastSeenLocation = location; - final Location finalLoc = location; - runInListenerThread(new Runnable() { - @Override - public void run() { - for (TrackDataListener listener : listeners) { - listener.onCurrentLocationChanged(finalLoc); - } - } - }); } /** - * Returns true if the given location is either invalid or too old. - * - * @param location the location to test - * @param now the current timestamp in milliseconds - * @param maxAge the maximum age in milliseconds - * @return true if it's invalid or too old, false otherwise + * Notifies track points table update. To be run in the {@link #handler} + * thread. + * + * @param updateSamplingState true to update the sampling state + * @param sampledInListeners the sampled-in listeners + * @param sampledOutListeners the sampled-out listeners */ - private static boolean isLocationOld(Location location, long now, long maxAge) { - return !LocationUtils.isValidLocation(location) || now - location.getTime() > maxAge; - } - - /** - * Notifies that the current heading has changed. - * - * @param listeners the listeners to notify - */ - private void notifyHeadingChanged(final Set listeners) { - if (listeners.isEmpty()) return; - - runInListenerThread(new Runnable() { - @Override - public void run() { - float heading = lastSeenMagneticHeading + declination; - for (TrackDataListener listener : listeners) { - listener.onCurrentHeadingChanged(heading); - } - } - }); - } - - /** - * Notifies that a new track has been selected.. - * - * @param trackId the new selected track - * @param listeners the listeners to notify - */ - private void notifySelectedTrackChanged(long trackId, - final Set listeners) { - if (listeners.isEmpty()) return; - - Log.i(TAG, "New track selected, id=" + trackId); - final Track track = providerUtils.getTrack(trackId); - - runInListenerThread(new Runnable() { - @Override - public void run() { - for (TrackDataListener listener : listeners) { - listener.onSelectedTrackChanged(track, isRecordingSelected()); - } - } - }); - } - - /** - * Notifies that the currently-selected track's data has been updated. - * - * @param listeners the listeners to notify - */ - private void notifyTrackUpdated(final Set listeners) { - if (listeners.isEmpty()) return; - - final Track track = providerUtils.getTrack(selectedTrackId); - - runInListenerThread(new Runnable() { - @Override - public void run() { - for (TrackDataListener listener : listeners) { - listener.onTrackUpdated(track); - } - } - }); - } - - /** - * Notifies that waypoints have been updated. - * We assume few waypoints, so we reload them all every time. - * - * @param listeners the listeners to notify - */ - private void notifyWaypointUpdated(final Set listeners) { - if (listeners.isEmpty()) return; - - // Always reload all the waypoints. - final Cursor cursor = providerUtils.getWaypointsCursor( - selectedTrackId, 0L, MAX_DISPLAYED_WAYPOINTS_POINTS); - - runInListenerThread(new Runnable() { - @Override - public void run() { - Log.d(TAG, "Reloading waypoints"); - for (TrackDataListener listener : listeners) { - listener.clearWaypoints(); - } - - try { - if (cursor != null && cursor.moveToFirst()) { - do { - Waypoint waypoint = providerUtils.createWaypoint(cursor); - if (!LocationUtils.isValidLocation(waypoint.getLocation())) { - continue; - } - - for (TrackDataListener listener : listeners) { - listener.onNewWaypoint(waypoint); - } - } while (cursor.moveToNext()); - } - } finally { - if (cursor != null) { - cursor.close(); - } - } - - for (TrackDataListener listener : listeners) { - listener.onNewWaypointsDone(); - } - } - }); - } - - /** - * Tells listeners to clear the current list of points. - * - * @param listeners the listeners to notify - */ - private void notifyPointsCleared(final Set listeners) { - if (listeners.isEmpty()) return; - - runInListenerThread(new Runnable() { - @Override - public void run() { - for (TrackDataListener listener : listeners) { - listener.clearTrackPoints(); - } - } - }); - } - - /** - * Notifies the given listeners about track points in the given ID range. - * - * @param keepState whether to load and save state about the already-notified points. - * If true, only new points are reported. - * If false, then the whole track will be loaded, without affecting the state. - * @param minPointId the first point ID to notify, inclusive, or 0 to determine from - * internal state - * @param previousNumPoints the number of points to assume were previously loaded for - * these listeners, or 0 to assume it's the kept state - */ - private void notifyPointsUpdated(final boolean keepState, - final long minPointId, final int previousNumPoints, - final Set sampledListeners, - final Set sampledOutListeners) { - if (sampledListeners.isEmpty() && sampledOutListeners.isEmpty()) return; - - runInListenerThread(new Runnable() { - @Override - public void run() { - notifyPointsUpdatedSync(keepState, minPointId, previousNumPoints, sampledListeners, sampledOutListeners); - } - }); - } - - /** - * Synchronous version of the above method. - */ - private void notifyPointsUpdatedSync(boolean keepState, - long minPointId, int previousNumPoints, - Set sampledListeners, - Set sampledOutListeners) { - // If we're loading state, start from after the last seen point up to the last recorded one - // (all new points) - // If we're not loading state, then notify about all the previously-seen points. - if (minPointId <= 0) { - minPointId = keepState ? lastSeenLocationId + 1 : 0; + private void notifyTrackPointsTableUpdate(boolean updateSamplingState, + Set sampledInListeners, Set sampledOutListeners) { + if (sampledInListeners.isEmpty() && sampledOutListeners.isEmpty()) { + return; } - long maxPointId = keepState ? -1 : lastSeenLocationId; - - // TODO: Move (re)sampling to a separate class. - if (numLoadedPoints >= targetNumPoints) { - // We're about to exceed the maximum desired number of points, so reload - // the whole track with fewer points (the sampling frequency will be - // lower). We do this for every listener even if we were loading just for - // a few of them (why miss the oportunity?). - - Log.i(TAG, "Resampling point set after " + numLoadedPoints + " points."); - resetState(); - synchronized (trackDataManager) { - sampledListeners = getListenersFor(TrackDataType.TRACK_POINTS_TABLE); - sampledOutListeners = getListenersFor(TrackDataType.SAMPLED_OUT_TRACK_POINTS); - } - maxPointId = -1; - minPointId = 0; - previousNumPoints = 0; - keepState = true; - - for (TrackDataListener listener : sampledListeners) { + if (updateSamplingState && numLoadedPoints >= targetNumPoints) { + // Reload and resample the track at a lower frequency. + Log.i(TAG, "Resampling track after " + numLoadedPoints + " points."); + resetSamplingState(); + for (TrackDataListener listener : sampledInListeners) { listener.clearTrackPoints(); } } - // Keep the originally selected track ID so we can stop if it changes. - long currentSelectedTrackId = selectedTrackId; + int localNumLoadedPoints = updateSamplingState ? numLoadedPoints : 0; + long localFirstSeenLocationId = updateSamplingState ? firstSeenLocationId : -1L; + long localLastSeenLocationId = updateSamplingState ? lastSeenLocationId : -1L; + long maxPointId = updateSamplingState ? -1L : lastSeenLocationId; - // If we're ignoring state, start from the beginning of the track - int localNumLoadedPoints = previousNumPoints; - if (previousNumPoints <= 0) { - localNumLoadedPoints = keepState ? numLoadedPoints : 0; - } - long localFirstSeenLocationId = keepState ? firstSeenLocationId : -1; - long localLastSeenLocationId = minPointId; - long lastStoredLocationId = providerUtils.getLastLocationId(currentSelectedTrackId); - int pointSamplingFrequency = -1; + long lastLocationId = myTracksProviderUtils.getLastLocationId(selectedTrackId); + int samplingFrequency = -1; + LocationIterator iterator = myTracksProviderUtils.getLocationIterator( + selectedTrackId, localLastSeenLocationId + 1, false, locationFactory); + boolean includeNextPoint = false; + while (iterator.hasNext()) { + Location location = iterator.next(); + long locationId = iterator.getLocationId(); - LocationIterator it = providerUtils.getLocationIterator( - currentSelectedTrackId, minPointId, false, locationFactory); - - while (it.hasNext()) { - if (currentSelectedTrackId != selectedTrackId) { - // The selected track changed beneath us, stop. - break; - } - - Location location = it.next(); - long locationId = it.getLocationId(); - - // If past the last wanted point, stop. - // This happens when adding a new listener after data has already been loaded, - // in which case we only want to bring that listener up to the point where the others - // were. In case it does happen, we should be wasting few points (only the ones not - // yet notified to other listeners). - if (maxPointId > 0 && locationId > maxPointId) { + // Stop if past the last wanted point + if (maxPointId != -1L && locationId > maxPointId) { break; } if (localFirstSeenLocationId == -1) { - // This was our first point, keep its ID localFirstSeenLocationId = locationId; } - if (pointSamplingFrequency == -1) { - // Now we already have at least one point, calculate the sampling - // frequency. - // It should be noted that a non-obvious consequence of this sampling is that - // no matter how many points we get in the newest batch, we'll never exceed - // MAX_DISPLAYED_TRACK_POINTS = 2 * TARGET_DISPLAYED_TRACK_POINTS before resampling. - long numTotalPoints = lastStoredLocationId - localFirstSeenLocationId; - numTotalPoints = Math.max(0L, numTotalPoints); - pointSamplingFrequency = (int) (1 + numTotalPoints / targetNumPoints); + if (samplingFrequency == -1) { + long numTotalPoints = Math.max(0L, lastLocationId - localFirstSeenLocationId); + samplingFrequency = 1 + (int) (numTotalPoints / targetNumPoints); } - notifyNewPoint(location, locationId, lastStoredLocationId, - localNumLoadedPoints, pointSamplingFrequency, sampledListeners, sampledOutListeners); + if (!LocationUtils.isValidLocation(location)) { + // TODO: also include the last valid point before a split + for (TrackDataListener trackDataListener : sampledInListeners) { + trackDataListener.onSegmentSplit(); + includeNextPoint = true; + } + } else { + // Also include the last point if the selected track is not recording. + if (includeNextPoint || (localNumLoadedPoints % samplingFrequency == 0) + || (locationId == lastLocationId && !isSelectedTrackRecording())) { + includeNextPoint = false; + for (TrackDataListener trackDataListener : sampledInListeners) { + trackDataListener.onSampledInTrackPoint(location); + } + } else { + for (TrackDataListener trackDataListener : sampledOutListeners) { + trackDataListener.onSampledOutTrackPoint(location); + } + } + } localNumLoadedPoints++; localLastSeenLocationId = locationId; } - it.close(); + iterator.close(); - if (keepState) { + if (updateSamplingState) { numLoadedPoints = localNumLoadedPoints; firstSeenLocationId = localFirstSeenLocationId; lastSeenLocationId = localLastSeenLocationId; } - // Always keep the sampling frequency - if it changes we'll do a full reload above anyway. - lastSamplingFrequency = pointSamplingFrequency; - - for (TrackDataListener listener : sampledListeners) { + for (TrackDataListener listener : sampledInListeners) { listener.onNewTrackPointsDone(); - - // Update the listener state - ListenerState listenerState = trackDataManager.getListenerState(listener); - if (listenerState != null) { - listenerState.setState(currentSelectedTrackId, localLastSeenLocationId, - pointSamplingFrequency, localNumLoadedPoints); - } } } - private void notifyNewPoint(Location location, - long locationId, - long lastStoredLocationId, - int loadedPoints, - int pointSamplingFrequency, - Set sampledListeners, - Set sampledOutListeners) { - boolean isValid = LocationUtils.isValidLocation(location); - if (!isValid) { - // Invalid points are segment splits - report those separately. - // TODO: Always send last valid point before and first valid point after a split - for (TrackDataListener listener : sampledListeners) { - listener.onSegmentSplit(); - } + /** + * Notifies location state changed. To be run in the {@link #handler} thread. + * + * @param trackDataListeners the track data listeners to notify + */ + private void notifyLocationStateChanged(Set trackDataListeners) { + if (trackDataListeners.isEmpty()) { return; } - - // Include a point if it fits one of the following criteria: - // - Has the mod for the sampling frequency (includes first point). - // - Is the last point and we are not recording this track. - boolean recordingSelected = isRecordingSelected(); - boolean includeInSample = - (loadedPoints % pointSamplingFrequency == 0 || - (!recordingSelected && locationId == lastStoredLocationId)); - - if (!includeInSample) { - for (TrackDataListener listener : sampledOutListeners) { - listener.onSampledOutTrackPoint(location); - } + TrackDataListener.LocationState locationState; + if (!hasProviderEnabled) { + locationState = LocationState.DISABLED; + } else if (!hasFix) { + locationState = LocationState.NO_FIX; + } else if (!hasGoodFix) { + locationState = LocationState.BAD_FIX; } else { - // Point is valid and included in sample. - for (TrackDataListener listener : sampledListeners) { - // No need to allocate a new location (we can safely reuse the existing). - listener.onNewTrackPoint(location); - } + locationState = LocationState.GOOD_FIX; + } + for (TrackDataListener trackDataListener : trackDataListeners) { + trackDataListener.onLocationStateChanged(locationState); } } - // @VisibleForTesting - protected void runInListenerThread(Runnable runnable) { - if (listenerHandler == null) { - // Use a Throwable to ensure the stack trace is logged. - Log.e(TAG, "Tried to use listener thread before start()", new Throwable()); + /** + * Notifies location changed. To be run in the {@link #handler} thread. + * + * @param location the location + * @param notifyLocationStateChange true to always notify location state + * change + * @param trackDataListeners the track data listeners to notify + */ + private void notifyLocationChanged(Location location, boolean notifyLocationStateChange, + Set trackDataListeners) { + if (location == null) { + return; + } + boolean oldHasFix = hasFix; + boolean oldHasGoodFix = hasGoodFix; + long now = System.currentTimeMillis(); + if (location.getProvider().equals(LocationManager.GPS_PROVIDER)) { + hasFix = !isLocationOld(location, now, MAX_LOCATION_AGE_MS); + hasGoodFix = location.getAccuracy() <= minRequiredAccuracy; + lastSeenLocation = location; + } else { + // A network location. + + // If has a recent GPS location, ignore the network location. + if (!isLocationOld(lastSeenLocation, now, MAX_LOCATION_AGE_MS)) { + return; + } + + hasFix = false; + hasGoodFix = false; + lastSeenLocation = isLocationOld(location, now, MAX_NETWORK_AGE_MS) ? null : location; + } + + if (trackDataListeners.isEmpty()) { return; } - listenerHandler.post(runnable); - } + if (notifyLocationStateChange || hasFix != oldHasFix || hasGoodFix != oldHasGoodFix) { + notifyLocationStateChanged(trackDataListeners); + } - private Set getListenersFor(TrackDataType type) { - synchronized (trackDataManager) { - return trackDataManager.getListeners(type); + for (TrackDataListener trackDataListener : trackDataListeners) { + trackDataListener.onLocationChanged(lastSeenLocation); } } - private EnumSet getNeededListenerTypes() { - EnumSet neededTypes = trackDataManager.getRegisteredTrackDataTypes(); - - // We always want preference updates. - neededTypes.add(TrackDataType.PREFERENCE); - - return neededTypes; + /** + * Notifies heading change. To be run in the {@link #handler} thread. + * + * @param trackDataListeners the track data listeners to notify + */ + private void notifyHeadingChange(Set trackDataListeners) { + if (trackDataListeners.isEmpty()) { + return; + } + float value = lastHeading + lastDeclination; + for (TrackDataListener trackDataListener : trackDataListeners) { + trackDataListener.onHeadingChanged(value); + } } -} + + /** + * Resets the track points sampling states. + */ + private void resetSamplingState() { + numLoadedPoints = 0; + firstSeenLocationId = -1L; + lastSeenLocationId = -1L; + } + + /** + * Returns true if a location is invalid or too old. + * + * @param location the location + * @param now the current time + * @param maxAge the maximum age + */ + private boolean isLocationOld(Location location, long now, long maxAge) { + return !LocationUtils.isValidLocation(location) || (now - location.getTime() > maxAge); + } + + /** + * Creates a {@link DataSource}. + */ + @VisibleForTesting + protected DataSource newDataSource() { + return new DataSource(context); + } + + /** + * Gets a declination. + * + * @param location the location + * @param time the time + */ + @VisibleForTesting + protected float getDeclination(Location location, long time) { + GeomagneticField field = new GeomagneticField((float) location.getLatitude(), (float) location + .getLongitude(), (float) location.getAltitude(), time); + return field.getDeclination(); + } + + /** + * Run in the handler thread. + * + * @param runnable the runnable + */ + @VisibleForTesting + protected void runInHanderThread(Runnable runnable) { + if (handler == null) { + // Use a Throwable to ensure the stack trace is logged. + Log.e(TAG, "handler is null.", new Throwable()); + return; + } + handler.post(runnable); + } +} \ No newline at end of file diff --git a/MyTracks/src/com/google/android/apps/mytracks/content/TrackDataListener.java b/MyTracks/src/com/google/android/apps/mytracks/content/TrackDataListener.java index be71e785e..f40439a09 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/content/TrackDataListener.java +++ b/MyTracks/src/com/google/android/apps/mytracks/content/TrackDataListener.java @@ -13,138 +13,131 @@ * License for the specific language governing permissions and limitations under * the License. */ -package com.google.android.apps.mytracks.content; -import com.google.android.apps.mytracks.content.Track; -import com.google.android.apps.mytracks.content.Waypoint; +package com.google.android.apps.mytracks.content; import android.location.Location; /** - * Listener for track data, for both initial and incremental loading. - * + * Listener for track data changes. + * * @author Rodrigo Damazio */ public interface TrackDataListener { - /** States for the GPS location provider. */ - public enum ProviderState { - DISABLED, - NO_FIX, - BAD_FIX, - GOOD_FIX; + /** + * Location state. + * + * @author Jimmy Shih + */ + public enum LocationState { + DISABLED, NO_FIX, BAD_FIX, GOOD_FIX; } /** - * Called when the location provider changes state. + * Called when the location state changes. */ - void onProviderStateChange(ProviderState state); + public void onLocationStateChanged(LocationState locationState); /** - * Called when the current location changes. - * This is meant for immediate location display only - track point data is - * delivered by other methods below, such as {@link #onNewTrackPoint}. - * - * @param loc the last known location - */ - void onCurrentLocationChanged(Location loc); - - /** - * Called when the current heading changes. - * - * @param heading the current heading, already accounting magnetic declination - */ - void onCurrentHeadingChanged(double heading); - - /** - * Called when the currently-selected track changes. - * This will be followed by calls to data methods such as - * {@link #onTrackUpdated}, {@link #clearTrackPoints}, - * {@link #onNewTrackPoint(Location)}, etc., even if no track is currently - * selected (in which case you'll only get calls to clear the current data). + * Called when the location changes. This is meant for location display only, + * track point data is reported with other methods like + * {@link #onSampledInTrackPoint(Location)} and + * {@link #onSampledOutTrackPoint(Location)}. * - * @param track the selected track, or null if no track is selected - * @param isRecording whether we're currently recording the selected track + * @param location the location */ - void onSelectedTrackChanged(Track track, boolean isRecording); + public void onLocationChanged(Location location); /** - * Called when the track and/or its statistics have been updated. - * - * @param track the updated version of the track + * Called when the heading changes. + * + * @param heading the heading */ - void onTrackUpdated(Track track); + public void onHeadingChanged(double heading); /** - * Called to clear any previously-sent track points. - * This can be called at any time that we decide the data needs to be - * reloaded, such as when it needs to be resampled. + * Called when the selected track changes. This will be followed by calls to + * data methods such as {@link #onTrackUpdated(Track)}, + * {@link #clearTrackPoints()}, {@link #onSampledInTrackPoint(Location)}, + * etc., even if no track is currently selected (in which case you'll only get + * calls to clear the current data). + * + * @param track the selected track or null if no track is selected + * @param isRecording true if the selected track is recording */ - void clearTrackPoints(); + public void onSelectedTrackChanged(Track track, boolean isRecording); /** - * Called when a new interesting track point is read. - * In this case, interesting means that the point has already undergone - * sampling and invalid point filtering. - * - * @param loc the new track point + * Called when the track or its statistics has been updated. + * + * @param track the track */ - void onNewTrackPoint(Location loc); + public void onTrackUpdated(Track track); /** - * Called when a uninteresting track point is read. - * Uninteresting points are all points that get sampled out of the track. - * - * @param loc the new track point + * Called to clear previously-sent track points. */ - void onSampledOutTrackPoint(Location loc); + public void clearTrackPoints(); /** - * Called when an invalid point (representing a segment split) is read. + * Called when a sampled in track point is read. + * + * @param location the location */ - void onSegmentSplit(); + public void onSampledInTrackPoint(Location location); /** - * Called when we're done (for the time being) sending new points. - * This gets called after every batch of calls to {@link #onNewTrackPoint}, - * {@link #onSampledOutTrackPoint} and {@link #onSegmentSplit}. + * Called when a sampled out track point is read. + * + * @param location the location */ - void onNewTrackPointsDone(); + public void onSampledOutTrackPoint(Location location); /** - * Called to clear any previously-sent waypoints. - * This can be called at any time that we decide the data needs to be - * reloaded. + * Called when an invalid track point representing a segment split is read. */ - void clearWaypoints(); + public void onSegmentSplit(); + + /** + * Called when finish sending new track points. This gets called after every + * batch of calls to {@link #onSampledInTrackPoint(Location)}, + * {@link #onSampledOutTrackPoint(Location)} and {@link #onSegmentSplit()}. + */ + public void onNewTrackPointsDone(); + + /** + * Called to clear previously sent waypoints. + */ + public void clearWaypoints(); /** * Called when a new waypoint is read. - * - * @param wpt the new waypoint + * + * @param waypoint the waypoint */ - void onNewWaypoint(Waypoint wpt); + public void onNewWaypoint(Waypoint waypoint); /** - * Called when we're done (for the time being) sending new waypoints. - * This gets called after every batch of calls to {@link #clearWaypoints} and - * {@link #onNewWaypoint}. + * Called when finish sending new waypoints. This gets called after every + * batch of calls to {@link #clearWaypoints()} and + * {@link #onNewWaypoint(Waypoint)}. */ - void onNewWaypointsDone(); + public void onNewWaypointsDone(); /** - * Called when the display units are changed by the user. - * - * @param metric true if the units are metric, false if imperial - * @return true to reload all the data, false otherwise + * Called when the metric units preference value is change. + * + * @param metricUnits true to use metric units, false to use imperial units + * @return true to reload all the data, false otherwise. */ - boolean onUnitsChanged(boolean metric); + public boolean onMetricUnitsChanged(boolean metricUnits); /** - * Called when the speed/pace display unit is changed by the user. - * - * @param reportSpeed true to report speed, false for pace - * @return true to reload all the data, false otherwise + * Called when the report speed preference value is changed. + * + * @param reportSpeed true to report speed, false to report pace + * @return true to reload all the data, false otherwise. */ - boolean onReportSpeedChanged(boolean reportSpeed); + public boolean onReportSpeedChanged(boolean reportSpeed); } diff --git a/MyTracks/src/com/google/android/apps/mytracks/content/TrackDataManager.java b/MyTracks/src/com/google/android/apps/mytracks/content/TrackDataManager.java index 9c3ededd7..e816d2408 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/content/TrackDataManager.java +++ b/MyTracks/src/com/google/android/apps/mytracks/content/TrackDataManager.java @@ -35,9 +35,9 @@ import java.util.Set; */ public class TrackDataManager { - // Map of listener to its state - private final Map - listenerToStateMap = new HashMap(); + // Map of listener to its track data types + private final Map> + listenerToTypesMap = new HashMap>(); // Map of track data type to listeners private final Map> @@ -55,20 +55,15 @@ public class TrackDataManager { * @param listener the listener * @param trackDataTypes the track data types the listener is interested */ - public ListenerState registerListener( + public void registerListener( TrackDataListener listener, EnumSet trackDataTypes) { - if (listenerToStateMap.containsKey(listener)) { + if (listenerToTypesMap.containsKey(listener)) { throw new IllegalStateException("Listener is already registered"); } - - ListenerState listenerState = new ListenerState(listener, trackDataTypes); - listenerToStateMap.put(listener, listenerState); - + listenerToTypesMap.put(listener, trackDataTypes); for (TrackDataType trackDataType : trackDataTypes) { typeToListenersMap.get(trackDataType).add(listener); } - - return listenerState; } /** @@ -77,14 +72,14 @@ public class TrackDataManager { * @param listener the listener */ public void unregisterListener(TrackDataListener listener) { - ListenerState removed = listenerToStateMap.remove(listener); - if (removed == null) { + EnumSet removedTypes = listenerToTypesMap.remove(listener); + if (removedTypes == null) { Log.w(TAG, "Tried to unregister a listener that is not registered."); return; } // Remove the listener from the typeToListenersMap - for (TrackDataType trackDataType : removed.getTrackDataTypes()) { + for (TrackDataType trackDataType : removedTypes) { typeToListenersMap.get(trackDataType).remove(listener); } } @@ -93,16 +88,16 @@ public class TrackDataManager { * Gets the number of {@link TrackDataListener}. */ public int getNumberOfListeners() { - return listenerToStateMap.size(); + return listenerToTypesMap.size(); } /** - * Gets the track listener state. + * Gets the track data types for a listener. * * @param listener the listener */ - public ListenerState getListenerState(TrackDataListener listener) { - return listenerToStateMap.get(listener); + public EnumSet getTrackDataTypes(TrackDataListener listener) { + return listenerToTypesMap.get(listener); } /** @@ -119,9 +114,11 @@ public class TrackDataManager { */ public EnumSet getRegisteredTrackDataTypes() { EnumSet types = EnumSet.noneOf(TrackDataType.class); - for (ListenerState registration : this.listenerToStateMap.values()) { - types.addAll(registration.getTrackDataTypes()); + for (EnumSet value : listenerToTypesMap.values()) { + types.addAll(value); } + // Always include preference + types.add(TrackDataType.PREFERENCE); return types; } } diff --git a/MyTracks/src/com/google/android/apps/mytracks/content/TrackDataType.java b/MyTracks/src/com/google/android/apps/mytracks/content/TrackDataType.java index 319b5bbc0..87a5f9549 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/content/TrackDataType.java +++ b/MyTracks/src/com/google/android/apps/mytracks/content/TrackDataType.java @@ -22,13 +22,12 @@ package com.google.android.apps.mytracks.content; * @author Jimmy Shih */ public enum TrackDataType { - SELECTED_TRACK, // selected track changes TRACKS_TABLE, // tracks table changes WAYPOINTS_TABLE, // waypoints table changes - TRACK_POINTS_TABLE, // track points table changes - SAMPLED_OUT_TRACK_POINTS, // sampled-out track points changes + SAMPLED_IN_TRACK_POINTS_TABLE, // sampled-in track points table changes + SAMPLED_OUT_TRACK_POINTS_TABLE, // sampled-out track points table changes LOCATION, // location changes - COMPASS, // compass changes - PREFERENCE; // preference changes + HEADING, // heading changes + PREFERENCE // preference changes } \ No newline at end of file diff --git a/MyTracks/src/com/google/android/apps/mytracks/fragments/ChartFragment.java b/MyTracks/src/com/google/android/apps/mytracks/fragments/ChartFragment.java index e1dab8720..a7964af05 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/fragments/ChartFragment.java +++ b/MyTracks/src/com/google/android/apps/mytracks/fragments/ChartFragment.java @@ -18,14 +18,14 @@ package com.google.android.apps.mytracks.fragments; import com.google.android.apps.mytracks.ChartView; import com.google.android.apps.mytracks.Constants; -import com.google.android.apps.mytracks.MyTracksApplication; -import com.google.android.apps.mytracks.content.TrackDataType; +import com.google.android.apps.mytracks.TrackDetailActivity; import com.google.android.apps.mytracks.content.MyTracksLocation; import com.google.android.apps.mytracks.content.Sensor; import com.google.android.apps.mytracks.content.Sensor.SensorDataSet; import com.google.android.apps.mytracks.content.Track; import com.google.android.apps.mytracks.content.TrackDataHub; import com.google.android.apps.mytracks.content.TrackDataListener; +import com.google.android.apps.mytracks.content.TrackDataType; import com.google.android.apps.mytracks.content.Waypoint; import com.google.android.apps.mytracks.stats.DoubleBuffer; import com.google.android.apps.mytracks.stats.TripStatisticsBuilder; @@ -96,7 +96,7 @@ public class ChartFragment extends Fragment implements TrackDataListener { zoomControls.setIsZoomInEnabled(chartView.canZoomIn()); zoomControls.setIsZoomOutEnabled(chartView.canZoomOut()); - chartView.setShowPointer(isRecording()); + chartView.setShowPointer(isSelectedTrackRecording()); chartView.invalidate(); } }; @@ -164,17 +164,17 @@ public class ChartFragment extends Fragment implements TrackDataListener { } @Override - public void onProviderStateChange(ProviderState state) { + public void onLocationStateChanged(LocationState state) { // We don't care. } @Override - public void onCurrentLocationChanged(Location loc) { + public void onLocationChanged(Location loc) { // We don't care. } @Override - public void onCurrentHeadingChanged(double heading) { + public void onHeadingChanged(double heading) { // We don't care. } @@ -213,7 +213,7 @@ public class ChartFragment extends Fragment implements TrackDataListener { } @Override - public void onNewTrackPoint(Location location) { + public void onSampledInTrackPoint(Location location) { if (LocationUtils.isValidLocation(location)) { double[] data = new double[ChartView.NUM_SERIES + 1]; fillDataPoint(location, data); @@ -259,7 +259,7 @@ public class ChartFragment extends Fragment implements TrackDataListener { } @Override - public boolean onUnitsChanged(boolean metric) { + public boolean onMetricUnitsChanged(boolean metric) { if (metricUnits == metric) { return false; } @@ -358,13 +358,13 @@ public class ChartFragment extends Fragment implements TrackDataListener { * accessed by multiple threads. */ private synchronized void resumeTrackDataHub() { - trackDataHub = ((MyTracksApplication) getActivity().getApplication()).getTrackDataHub(); + trackDataHub = ((TrackDetailActivity) getActivity()).getTrackDataHub(); trackDataHub.registerTrackDataListener(this, EnumSet.of( TrackDataType.SELECTED_TRACK, TrackDataType.TRACKS_TABLE, TrackDataType.WAYPOINTS_TABLE, - TrackDataType.TRACK_POINTS_TABLE, - TrackDataType.SAMPLED_OUT_TRACK_POINTS, + TrackDataType.SAMPLED_IN_TRACK_POINTS_TABLE, + TrackDataType.SAMPLED_OUT_TRACK_POINTS_TABLE, TrackDataType.PREFERENCE)); } @@ -378,11 +378,11 @@ public class ChartFragment extends Fragment implements TrackDataListener { } /** - * Returns true if recording. Needs to be synchronized because trackDataHub - * can be accessed by multiple threads. + * Returns true if the selected track is recording. Needs to be synchronized + * because trackDataHub can be accessed by multiple threads. */ - private synchronized boolean isRecording() { - return trackDataHub != null && trackDataHub.isRecordingSelected(); + private synchronized boolean isSelectedTrackRecording() { + return trackDataHub != null && trackDataHub.isSelectedTrackRecording(); } /** diff --git a/MyTracks/src/com/google/android/apps/mytracks/fragments/MapFragment.java b/MyTracks/src/com/google/android/apps/mytracks/fragments/MapFragment.java index 5a5e5c72c..02dbc55f6 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/fragments/MapFragment.java +++ b/MyTracks/src/com/google/android/apps/mytracks/fragments/MapFragment.java @@ -17,14 +17,13 @@ package com.google.android.apps.mytracks.fragments; import com.google.android.apps.mytracks.MapOverlay; -import com.google.android.apps.mytracks.MyTracksApplication; import com.google.android.apps.mytracks.TrackDetailActivity; -import com.google.android.apps.mytracks.content.TrackDataType; import com.google.android.apps.mytracks.content.MyTracksProviderUtils; import com.google.android.apps.mytracks.content.MyTracksProviderUtils.Factory; import com.google.android.apps.mytracks.content.Track; import com.google.android.apps.mytracks.content.TrackDataHub; import com.google.android.apps.mytracks.content.TrackDataListener; +import com.google.android.apps.mytracks.content.TrackDataType; import com.google.android.apps.mytracks.content.Waypoint; import com.google.android.apps.mytracks.stats.TripStatistics; import com.google.android.apps.mytracks.util.ApiAdapterFactory; @@ -271,7 +270,7 @@ public class MapFragment extends Fragment } @Override - public void onProviderStateChange(ProviderState state) { + public void onLocationStateChanged(LocationState state) { final int messageId; final boolean isGpsDisabled; switch (state) { @@ -315,13 +314,13 @@ public class MapFragment extends Fragment } @Override - public void onCurrentLocationChanged(Location location) { + public void onLocationChanged(Location location) { currentLocation = location; updateCurrentLocation(); } @Override - public void onCurrentHeadingChanged(double heading) { + public void onHeadingChanged(double heading) { if (mapOverlay.setHeading((float) heading)) { mapView.postInvalidate(); } @@ -362,7 +361,7 @@ public class MapFragment extends Fragment } @Override - public void onNewTrackPoint(Location location) { + public void onSampledInTrackPoint(Location location) { if (LocationUtils.isValidLocation(location)) { mapOverlay.addLocation(location); } @@ -402,7 +401,7 @@ public class MapFragment extends Fragment } @Override - public boolean onUnitsChanged(boolean metric) { + public boolean onMetricUnitsChanged(boolean metric) { // We don't care. return false; } @@ -418,13 +417,13 @@ public class MapFragment extends Fragment * accessed by multiple threads. */ private synchronized void resumeTrackDataHub() { - trackDataHub = ((MyTracksApplication) getActivity().getApplication()).getTrackDataHub(); + trackDataHub = ((TrackDetailActivity) getActivity()).getTrackDataHub(); trackDataHub.registerTrackDataListener(this, EnumSet.of( TrackDataType.SELECTED_TRACK, TrackDataType.WAYPOINTS_TABLE, - TrackDataType.TRACK_POINTS_TABLE, + TrackDataType.SAMPLED_IN_TRACK_POINTS_TABLE, TrackDataType.LOCATION, - TrackDataType.COMPASS)); + TrackDataType.HEADING)); } /** diff --git a/MyTracks/src/com/google/android/apps/mytracks/fragments/StatsFragment.java b/MyTracks/src/com/google/android/apps/mytracks/fragments/StatsFragment.java index 0eaf01a89..580ed784f 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/fragments/StatsFragment.java +++ b/MyTracks/src/com/google/android/apps/mytracks/fragments/StatsFragment.java @@ -16,11 +16,11 @@ package com.google.android.apps.mytracks.fragments; -import com.google.android.apps.mytracks.MyTracksApplication; -import com.google.android.apps.mytracks.content.TrackDataType; +import com.google.android.apps.mytracks.TrackDetailActivity; import com.google.android.apps.mytracks.content.Track; import com.google.android.apps.mytracks.content.TrackDataHub; import com.google.android.apps.mytracks.content.TrackDataListener; +import com.google.android.apps.mytracks.content.TrackDataType; import com.google.android.apps.mytracks.content.Waypoint; import com.google.android.apps.mytracks.stats.TripStatistics; import com.google.android.apps.mytracks.util.PreferencesUtils; @@ -61,7 +61,7 @@ public class StatsFragment extends Fragment implements TrackDataListener { // A runnable to update the total time field. private final Runnable updateTotalTime = new Runnable() { public void run() { - if (isRecording()) { + if (isSelectedTrackRecording()) { StatsUtils.setTotalTimeValue(getActivity(), System.currentTimeMillis() - startTime); } } @@ -117,8 +117,8 @@ public class StatsFragment extends Fragment implements TrackDataListener { } @Override - public void onProviderStateChange(ProviderState state) { - if (isResumed() && (state == ProviderState.DISABLED || state == ProviderState.NO_FIX)) { + public void onLocationStateChanged(LocationState state) { + if (isResumed() && (state == LocationState.DISABLED || state == LocationState.NO_FIX)) { getActivity().runOnUiThread(new Runnable() { @Override public void run() { @@ -130,8 +130,8 @@ public class StatsFragment extends Fragment implements TrackDataListener { } @Override - public void onCurrentLocationChanged(final Location location) { - if (isResumed() && isRecording()) { + public void onLocationChanged(final Location location) { + if (isResumed() && isSelectedTrackRecording()) { getActivity().runOnUiThread(new Runnable() { @Override public void run() { @@ -143,7 +143,7 @@ public class StatsFragment extends Fragment implements TrackDataListener { } @Override - public void onCurrentHeadingChanged(double heading) { + public void onHeadingChanged(double heading) { // We don't care. } @@ -175,7 +175,7 @@ public class StatsFragment extends Fragment implements TrackDataListener { lastTripStatistics = track.getTripStatistics(); startTime = track.getTripStatistics().getStartTime(); - if (!isRecording()) { + if (!isSelectedTrackRecording()) { lastLocation = null; } updateUi(); @@ -190,7 +190,7 @@ public class StatsFragment extends Fragment implements TrackDataListener { } @Override - public void onNewTrackPoint(Location loc) { + public void onSampledInTrackPoint(Location loc) { // We don't care. } @@ -225,7 +225,7 @@ public class StatsFragment extends Fragment implements TrackDataListener { } @Override - public boolean onUnitsChanged(final boolean metric) { + public boolean onMetricUnitsChanged(final boolean metric) { if (isResumed()) { getActivity().runOnUiThread(new Runnable() { @Override @@ -255,7 +255,7 @@ public class StatsFragment extends Fragment implements TrackDataListener { * be accessed by multiple threads. */ private synchronized void resumeTrackDataHub() { - trackDataHub = ((MyTracksApplication) getActivity().getApplication()).getTrackDataHub(); + trackDataHub = ((TrackDetailActivity) getActivity()).getTrackDataHub(); trackDataHub.registerTrackDataListener(this, EnumSet.of( TrackDataType.SELECTED_TRACK, TrackDataType.TRACKS_TABLE, @@ -273,11 +273,11 @@ public class StatsFragment extends Fragment implements TrackDataListener { } /** - * Returns true if recording. Needs to be synchronized because trackDataHub - * can be accessed by multiple threads. + * Returns true if the selected track is recording. Needs to be synchronized + * because trackDataHub can be accessed by multiple threads. */ - private synchronized boolean isRecording() { - return trackDataHub != null && trackDataHub.isRecordingSelected(); + private synchronized boolean isSelectedTrackRecording() { + return trackDataHub != null && trackDataHub.isSelectedTrackRecording(); } /** diff --git a/MyTracksLib/src/com/google/android/apps/mytracks/content/MyTracksProviderUtilsImpl.java b/MyTracksLib/src/com/google/android/apps/mytracks/content/MyTracksProviderUtilsImpl.java index 3c5d63be0..1d882bab6 100644 --- a/MyTracksLib/src/com/google/android/apps/mytracks/content/MyTracksProviderUtilsImpl.java +++ b/MyTracksLib/src/com/google/android/apps/mytracks/content/MyTracksProviderUtilsImpl.java @@ -824,7 +824,7 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils { throw new IllegalArgumentException("locationFactory is null"); } return new LocationIterator() { - private long lastTrackPointId = startTrackPointId; + private long lastTrackPointId = -1L; private Cursor cursor = getCursor(startTrackPointId); private final CachedTrackPointsIndexes indexes = cursor != null ? new CachedTrackPointsIndexes(cursor) @@ -843,7 +843,8 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils { * Advances the cursor to the next batch. Returns true if successful. */ private boolean advanceCursorToNextBatch() { - long trackPointId = lastTrackPointId + (descending ? -1 : 1); + long trackPointId = lastTrackPointId == -1L ? -1L : lastTrackPointId + (descending ? -1 + : 1); Log.d(TAG, "Advancing track point id: " + trackPointId); cursor.close(); cursor = getCursor(trackPointId); diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/content/TrackDataHubTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/content/TrackDataHubTest.java index e6ab88ef0..119494743 100644 --- a/MyTracksTest/src/com/google/android/apps/mytracks/content/TrackDataHubTest.java +++ b/MyTracksTest/src/com/google/android/apps/mytracks/content/TrackDataHubTest.java @@ -19,12 +19,11 @@ import static com.google.android.testing.mocking.AndroidMock.capture; import static com.google.android.testing.mocking.AndroidMock.eq; import static com.google.android.testing.mocking.AndroidMock.expect; import static com.google.android.testing.mocking.AndroidMock.isA; -import static com.google.android.testing.mocking.AndroidMock.leq; import com.google.android.apps.mytracks.Constants; import com.google.android.apps.mytracks.content.MyTracksProviderUtils.LocationFactory; import com.google.android.apps.mytracks.content.MyTracksProviderUtils.LocationIterator; -import com.google.android.apps.mytracks.content.TrackDataListener.ProviderState; +import com.google.android.apps.mytracks.content.TrackDataListener.LocationState; import com.google.android.apps.mytracks.services.TrackRecordingServiceTest.MockContext; import com.google.android.apps.mytracks.util.PreferencesUtils; import com.google.android.maps.mytracks.R; @@ -37,7 +36,6 @@ import android.content.SharedPreferences.OnSharedPreferenceChangeListener; import android.database.ContentObserver; import android.database.Cursor; import android.database.MatrixCursor; -import android.hardware.Sensor; import android.hardware.SensorEvent; import android.hardware.SensorEventListener; import android.location.Location; @@ -57,7 +55,7 @@ import org.easymock.IAnswer; /** * Tests for {@link TrackDataHub}. - * + * * @author Rodrigo Damazio */ public class TrackDataHubTest extends AndroidTestCase { @@ -65,53 +63,51 @@ public class TrackDataHubTest extends AndroidTestCase { private static final long TRACK_ID = 42L; private static final int TARGET_POINTS = 50; - private MyTracksProviderUtils providerUtils; - private TrackDataHub hub; - private TrackDataManager trackDataManager; - private DataSource dataSource; - private SharedPreferences sharedPreferences; - private TrackDataListener listener1; - private TrackDataListener listener2; - private Capture preferenceListenerCapture = - new Capture(); private MockContext context; + private SharedPreferences sharedPreferences; + private MyTracksProviderUtils myTracksProviderUtils; + private DataSource dataSource; + private TrackDataManager trackDataManager; + private TrackDataHub trackDataHub; + private TrackDataListener trackDataListener1; + private TrackDataListener trackDataListener2; + private Capture preferenceChangeListenerCapture = new Capture< + SharedPreferences.OnSharedPreferenceChangeListener>(); private float declination; - @UsesMocks({MyTracksProviderUtils.class, DataSource.class, TrackDataListener.class}) + @UsesMocks({ MyTracksProviderUtils.class, DataSource.class, TrackDataListener.class }) @Override protected void setUp() throws Exception { super.setUp(); - - MockContentResolver mockContentResolver = new MockContentResolver(); - RenamingDelegatingContext targetContext = new RenamingDelegatingContext( - getContext(), getContext(), "test."); - context = new MockContext(mockContentResolver, targetContext); - + context = new MockContext(new MockContentResolver(), new RenamingDelegatingContext( + getContext(), getContext(), "test.")); sharedPreferences = context.getSharedPreferences(Constants.SETTINGS_NAME, Context.MODE_PRIVATE); - providerUtils = AndroidMock.createMock("providerUtils", MyTracksProviderUtils.class); - dataSource = AndroidMock.createMock("dataSource", DataSource.class, context); - + myTracksProviderUtils = AndroidMock.createMock(MyTracksProviderUtils.class); + dataSource = AndroidMock.createMock(DataSource.class, context); trackDataManager = new TrackDataManager(); - hub = new TrackDataHub(context, trackDataManager, providerUtils, TARGET_POINTS) { - @Override + trackDataHub = new TrackDataHub( + context, trackDataManager, myTracksProviderUtils, TARGET_POINTS) { + @Override protected DataSource newDataSource() { return dataSource; } - @Override - protected void runInListenerThread(Runnable runnable) { - // Run everything in the same thread. + @Override + protected void runInHanderThread(Runnable runnable) { + // Run everything in the same thread runnable.run(); } - @Override - protected float getDeclinationFor(Location location, long timestamp) { + @Override + protected float getDeclination(Location location, long timestamp) { return declination; } }; - listener1 = AndroidMock.createStrictMock("listener1", TrackDataListener.class); - listener2 = AndroidMock.createStrictMock("listener2", TrackDataListener.class); + trackDataListener1 = AndroidMock.createStrictMock( + "trackDataListener1", TrackDataListener.class); + trackDataListener2 = AndroidMock.createStrictMock( + "trackDataListener2", TrackDataListener.class); PreferencesUtils.setLong(context, R.string.recording_track_id_key, TRACK_ID); PreferencesUtils.setLong(context, R.string.selected_track_id_key, TRACK_ID); } @@ -121,78 +117,563 @@ public class TrackDataHubTest extends AndroidTestCase { AndroidMock.reset(dataSource); // Expect everything to be unregistered. - if (preferenceListenerCapture.hasCaptured()) { - dataSource.unregisterOnSharedPreferenceChangeListener(preferenceListenerCapture.getValue()); - } - dataSource.unregisterLocationListener(isA(LocationListener.class)); - dataSource.unregisterCompassListener(isA(SensorEventListener.class)); dataSource.unregisterContentObserver(isA(ContentObserver.class)); AndroidMock.expectLastCall().times(3); - + dataSource.unregisterLocationListener(isA(LocationListener.class)); + dataSource.unregisterHeadingListener(isA(SensorEventListener.class)); + dataSource.unregisterOnSharedPreferenceChangeListener( + isA(OnSharedPreferenceChangeListener.class)); AndroidMock.replay(dataSource); - hub.stop(); - hub = null; - + trackDataHub.stop(); + trackDataHub = null; super.tearDown(); } - public void testTrackListen() { - Capture observerCapture = new Capture(); + /** + * Tests registering for tracks table update. + */ + public void testTracksTableUpdate() { + + // Register two listeners + Capture contentObserverCapture = new Capture(); Track track = new Track(); - expect(providerUtils.getTrack(TRACK_ID)).andStubReturn(track); - expectStart(); - dataSource.registerContentObserver(eq(TracksColumns.CONTENT_URI), capture(observerCapture)); - - // Expect the initial loading. - // Both listeners (registered before and after start) should get the same data. - listener1.onTrackUpdated(track); - listener2.onTrackUpdated(track); - + expect(myTracksProviderUtils.getTrack(TRACK_ID)).andStubReturn(track); + dataSource.registerOnSharedPreferenceChangeListener(capture(preferenceChangeListenerCapture)); + dataSource.registerContentObserver( + eq(TracksColumns.CONTENT_URI), capture(contentObserverCapture)); + trackDataListener1.onTrackUpdated(track); + trackDataListener2.onTrackUpdated(track); replay(); - hub.registerTrackDataListener(listener1, EnumSet.of(TrackDataType.TRACKS_TABLE)); - hub.start(); - hub.registerTrackDataListener(listener2, EnumSet.of(TrackDataType.TRACKS_TABLE)); - + trackDataHub.start(); + trackDataHub.registerTrackDataListener( + trackDataListener1, EnumSet.of(TrackDataType.TRACKS_TABLE)); + trackDataHub.registerTrackDataListener( + trackDataListener2, EnumSet.of(TrackDataType.TRACKS_TABLE)); verifyAndReset(); - ContentObserver observer = observerCapture.getValue(); - expect(providerUtils.getTrack(TRACK_ID)).andStubReturn(track); - - // Now expect an update. - listener1.onTrackUpdated(track); - listener2.onTrackUpdated(track); - + // Causes tracks table update + ContentObserver contentObserver = contentObserverCapture.getValue(); + expect(myTracksProviderUtils.getTrack(TRACK_ID)).andStubReturn(track); + trackDataListener1.onTrackUpdated(track); + trackDataListener2.onTrackUpdated(track); replay(); - observer.onChange(false); - + contentObserver.onChange(false); verifyAndReset(); - // Unregister one, get another update. - expect(providerUtils.getTrack(TRACK_ID)).andStubReturn(track); - listener2.onTrackUpdated(track); - + // Unregister one listener + expect(myTracksProviderUtils.getTrack(TRACK_ID)).andStubReturn(track); + trackDataListener2.onTrackUpdated(track); replay(); - hub.unregisterTrackDataListener(listener1); - - observer.onChange(false); - + trackDataHub.unregisterTrackDataListener(trackDataListener1); + contentObserver.onChange(false); verifyAndReset(); - // Unregister the other, expect internal unregistration - dataSource.unregisterContentObserver(observer); - + // Unregister the second listener + dataSource.unregisterContentObserver(contentObserver); replay(); - hub.unregisterTrackDataListener(listener2); - observer.onChange(false); - + trackDataHub.unregisterTrackDataListener(trackDataListener2); + contentObserver.onChange(false); verifyAndReset(); } + /** + * Tests registering for waypoints table update. + */ + public void testWaypointsTableUpdate() { + Waypoint waypoint1 = new Waypoint(); + Waypoint waypoint2 = new Waypoint(); + Waypoint waypoint3 = new Waypoint(); + Waypoint waypoint4 = new Waypoint(); + Location location = new Location("gps"); + location.setLatitude(10.0); + location.setLongitude(8.0); + waypoint1.setLocation(location); + waypoint2.setLocation(location); + waypoint3.setLocation(location); + waypoint4.setLocation(location); + + // Register two listeners + Capture contentObserverCapture = new Capture(); + expect(myTracksProviderUtils.getWaypointsCursor( + eq(TRACK_ID), AndroidMock.leq(0L), eq(Constants.MAX_DISPLAYED_WAYPOINTS_POINTS))) + .andStubAnswer(new FixedSizeCursorAnswer(2)); + expect(myTracksProviderUtils.createWaypoint(isA(Cursor.class))) + .andReturn(waypoint1).andReturn(waypoint2).andReturn(waypoint1).andReturn(waypoint2); + dataSource.registerOnSharedPreferenceChangeListener(capture(preferenceChangeListenerCapture)); + dataSource.registerContentObserver( + eq(WaypointsColumns.CONTENT_URI), capture(contentObserverCapture)); + trackDataListener1.clearWaypoints(); + trackDataListener2.clearWaypoints(); + trackDataListener1.onNewWaypoint(waypoint1); + trackDataListener2.onNewWaypoint(waypoint1); + trackDataListener1.onNewWaypoint(waypoint2); + trackDataListener2.onNewWaypoint(waypoint2); + trackDataListener1.onNewWaypointsDone(); + trackDataListener2.onNewWaypointsDone(); + replay(); + + trackDataHub.start(); + trackDataHub.registerTrackDataListener( + trackDataListener1, EnumSet.of(TrackDataType.WAYPOINTS_TABLE)); + trackDataHub.registerTrackDataListener( + trackDataListener2, EnumSet.of(TrackDataType.WAYPOINTS_TABLE)); + verifyAndReset(); + + // Cause waypoints table update + ContentObserver contentObserver = contentObserverCapture.getValue(); + expect(myTracksProviderUtils.getWaypointsCursor( + eq(TRACK_ID), AndroidMock.leq(0L), eq(Constants.MAX_DISPLAYED_WAYPOINTS_POINTS))) + .andStubAnswer(new FixedSizeCursorAnswer(3)); + expect(myTracksProviderUtils.createWaypoint(isA(Cursor.class))) + .andReturn(waypoint1).andReturn(waypoint2).andReturn(waypoint3); + trackDataListener1.clearWaypoints(); + trackDataListener2.clearWaypoints(); + trackDataListener1.onNewWaypoint(waypoint1); + trackDataListener2.onNewWaypoint(waypoint1); + trackDataListener1.onNewWaypoint(waypoint2); + trackDataListener2.onNewWaypoint(waypoint2); + trackDataListener1.onNewWaypoint(waypoint3); + trackDataListener2.onNewWaypoint(waypoint3); + trackDataListener1.onNewWaypointsDone(); + trackDataListener2.onNewWaypointsDone(); + replay(); + + contentObserver.onChange(false); + verifyAndReset(); + + // Unregister one listener + expect(myTracksProviderUtils.getWaypointsCursor( + eq(TRACK_ID), AndroidMock.leq(0L), eq(Constants.MAX_DISPLAYED_WAYPOINTS_POINTS))) + .andStubAnswer(new FixedSizeCursorAnswer(4)); + expect(myTracksProviderUtils.createWaypoint(isA(Cursor.class))) + .andReturn(waypoint1).andReturn(waypoint2).andReturn(waypoint3).andReturn(waypoint4); + trackDataListener2.clearWaypoints(); + trackDataListener2.onNewWaypoint(waypoint1); + trackDataListener2.onNewWaypoint(waypoint2); + trackDataListener2.onNewWaypoint(waypoint3); + trackDataListener2.onNewWaypoint(waypoint4); + trackDataListener2.onNewWaypointsDone(); + replay(); + + trackDataHub.unregisterTrackDataListener(trackDataListener1); + contentObserver.onChange(false); + verifyAndReset(); + + // Unregister the second listener + dataSource.unregisterContentObserver(contentObserver); + replay(); + + trackDataHub.unregisterTrackDataListener(trackDataListener2); + contentObserver.onChange(false); + verifyAndReset(); + } + + /** + * Tests track points table update. + */ + public void testTrackPointsTableUpdate() { + // Register one listener + Capture contentObserverCapture = new Capture(); + dataSource.registerOnSharedPreferenceChangeListener(capture(preferenceChangeListenerCapture)); + dataSource.registerContentObserver( + eq(TrackPointsColumns.CONTENT_URI), capture(contentObserverCapture)); + + FixedSizeLocationIterator locationIterator = new FixedSizeLocationIterator(1, 10, 5); + expect(myTracksProviderUtils.getLocationIterator( + eq(TRACK_ID), eq(0L), eq(false), isA(LocationFactory.class))).andReturn(locationIterator); + expect(myTracksProviderUtils.getLastLocationId(TRACK_ID)).andReturn(10L); + trackDataListener1.clearTrackPoints(); + locationIterator.expectLocationsDelivered(trackDataListener1); + trackDataListener1.onNewTrackPointsDone(); + replay(); + + trackDataHub.start(); + trackDataHub.registerTrackDataListener( + trackDataListener1, EnumSet.of(TrackDataType.SAMPLED_IN_TRACK_POINTS_TABLE)); + verifyAndReset(); + + // Register a second listener + locationIterator = new FixedSizeLocationIterator(1, 10, 5); + expect(myTracksProviderUtils.getLocationIterator( + eq(TRACK_ID), eq(0L), eq(false), isA(LocationFactory.class))).andReturn(locationIterator); + expect(myTracksProviderUtils.getLastLocationId(TRACK_ID)).andReturn(10L); + trackDataListener2.clearTrackPoints(); + locationIterator.expectLocationsDelivered(trackDataListener2); + trackDataListener2.onNewTrackPointsDone(); + replay(); + + trackDataHub.registerTrackDataListener( + trackDataListener2, EnumSet.of(TrackDataType.SAMPLED_IN_TRACK_POINTS_TABLE)); + verifyAndReset(); + + // Deliver more points - should go to both listeners without clearing + ContentObserver contentObserver = contentObserverCapture.getValue(); + locationIterator = new FixedSizeLocationIterator(11, 10, 1); + expect(myTracksProviderUtils.getLocationIterator( + eq(TRACK_ID), eq(11L), eq(false), isA(LocationFactory.class))).andReturn(locationIterator); + expect(myTracksProviderUtils.getLastLocationId(TRACK_ID)).andReturn(20L); + locationIterator.expectLocationsDelivered(trackDataListener1); + locationIterator.expectLocationsDelivered(trackDataListener2); + trackDataListener1.onNewTrackPointsDone(); + trackDataListener2.onNewTrackPointsDone(); + replay(); + + contentObserver.onChange(false); + verifyAndReset(); + + // Unregister one listener and change track + locationIterator = new FixedSizeLocationIterator(101, 10); + expect(myTracksProviderUtils.getLocationIterator( + eq(TRACK_ID + 1), eq(0L), eq(false), isA(LocationFactory.class))) + .andReturn(locationIterator); + expect(myTracksProviderUtils.getLastLocationId(TRACK_ID + 1)).andReturn(110L); + trackDataListener2.clearTrackPoints(); + locationIterator.expectLocationsDelivered(trackDataListener2); + trackDataListener2.onNewTrackPointsDone(); + replay(); + + trackDataHub.unregisterTrackDataListener(trackDataListener1); + trackDataHub.loadTrack(TRACK_ID + 1); + verifyAndReset(); + } + + /** + * Tests track points table update with registering the same listener. + */ + public void testTrackPointsTableUpdate_reRegister() { + + // Register one listener + Capture contentObserverCapture = new Capture(); + dataSource.registerOnSharedPreferenceChangeListener(capture(preferenceChangeListenerCapture)); + dataSource.registerContentObserver( + eq(TrackPointsColumns.CONTENT_URI), capture(contentObserverCapture)); + + FixedSizeLocationIterator locationIterator = new FixedSizeLocationIterator(1, 10, 5); + expect(myTracksProviderUtils.getLocationIterator( + eq(TRACK_ID), eq(0L), eq(false), isA(LocationFactory.class))).andReturn(locationIterator); + expect(myTracksProviderUtils.getLastLocationId(TRACK_ID)).andReturn(10L); + + trackDataListener1.clearTrackPoints(); + locationIterator.expectLocationsDelivered(trackDataListener1); + trackDataListener1.onNewTrackPointsDone(); + replay(); + + trackDataHub.start(); + trackDataHub.registerTrackDataListener( + trackDataListener1, EnumSet.of(TrackDataType.SAMPLED_IN_TRACK_POINTS_TABLE)); + verifyAndReset(); + + // Unregister the listener + ContentObserver observer = contentObserverCapture.getValue(); + dataSource.unregisterContentObserver(observer); + replay(); + + trackDataHub.unregisterTrackDataListener(trackDataListener1); + verifyAndReset(); + + // Register again + dataSource.registerContentObserver( + eq(TrackPointsColumns.CONTENT_URI), capture(contentObserverCapture)); + locationIterator = new FixedSizeLocationIterator(1, 10, 5); + expect(myTracksProviderUtils.getLocationIterator( + eq(TRACK_ID), eq(0L), eq(false), isA(LocationFactory.class))).andReturn(locationIterator); + expect(myTracksProviderUtils.getLastLocationId(TRACK_ID)).andReturn(10L); + trackDataListener1.clearTrackPoints(); + locationIterator.expectLocationsDelivered(trackDataListener1); + trackDataListener1.onNewTrackPointsDone(); + replay(); + + trackDataHub.registerTrackDataListener( + trackDataListener1, EnumSet.of(TrackDataType.SAMPLED_IN_TRACK_POINTS_TABLE)); + verifyAndReset(); + } + + /** + * Tests tracks point able change. Register a listener after a track change. + */ + public void testTrackPointsTableUpdate_reRegisterAfterTrackChange() { + + // Register one listener + Capture observerCapture = new Capture(); + dataSource.registerOnSharedPreferenceChangeListener(capture(preferenceChangeListenerCapture)); + dataSource.registerContentObserver( + eq(TrackPointsColumns.CONTENT_URI), capture(observerCapture)); + + FixedSizeLocationIterator locationIterator = new FixedSizeLocationIterator(1, 10, 5); + expect(myTracksProviderUtils.getLocationIterator( + eq(TRACK_ID), eq(0L), eq(false), isA(LocationFactory.class))).andReturn(locationIterator); + expect(myTracksProviderUtils.getLastLocationId(TRACK_ID)).andReturn(10L); + trackDataListener1.clearTrackPoints(); + locationIterator.expectLocationsDelivered(trackDataListener1); + trackDataListener1.onNewTrackPointsDone(); + replay(); + + trackDataHub.start(); + trackDataHub.registerTrackDataListener( + trackDataListener1, EnumSet.of(TrackDataType.SAMPLED_IN_TRACK_POINTS_TABLE)); + verifyAndReset(); + + // Unregister the listener + ContentObserver observer = observerCapture.getValue(); + dataSource.unregisterContentObserver(observer); + replay(); + + trackDataHub.unregisterTrackDataListener(trackDataListener1); + verifyAndReset(); + + // Register the listener after a new track + dataSource.registerContentObserver( + eq(TrackPointsColumns.CONTENT_URI), capture(observerCapture)); + locationIterator = new FixedSizeLocationIterator(1, 10); + expect(myTracksProviderUtils.getLocationIterator( + eq(TRACK_ID + 1), eq(0L), eq(false), isA(LocationFactory.class))) + .andReturn(locationIterator); + expect(myTracksProviderUtils.getLastLocationId(TRACK_ID + 1)).andReturn(10L); + trackDataListener1.clearTrackPoints(); + locationIterator.expectLocationsDelivered(trackDataListener1); + trackDataListener1.onNewTrackPointsDone(); + replay(); + + trackDataHub.loadTrack(TRACK_ID + 1); + trackDataHub.registerTrackDataListener( + trackDataListener1, EnumSet.of(TrackDataType.SAMPLED_IN_TRACK_POINTS_TABLE)); + verifyAndReset(); + } + + /** + * Tests track points table update with large track sampling. + */ + public void testTrackPointsTableUpdate_largeTrackSampling() { + Capture contentObserverCapture = new Capture(); + dataSource.registerOnSharedPreferenceChangeListener(capture(preferenceChangeListenerCapture)); + dataSource.registerContentObserver( + eq(TrackPointsColumns.CONTENT_URI), capture(contentObserverCapture)); + + expect(myTracksProviderUtils.getLastLocationId(TRACK_ID)).andReturn(200L); + AndroidMock.expectLastCall().anyTimes(); + FixedSizeLocationIterator locationIterator1 = new FixedSizeLocationIterator( + 1, 200, 4, 25, 71, 120); + expect(myTracksProviderUtils.getLocationIterator( + eq(TRACK_ID), eq(0L), eq(false), isA(LocationFactory.class))).andReturn(locationIterator1); + FixedSizeLocationIterator locationIterator2 = new FixedSizeLocationIterator( + 1, 200, 4, 25, 71, 120); + expect(myTracksProviderUtils.getLocationIterator( + eq(TRACK_ID), eq(0L), eq(false), isA(LocationFactory.class))).andReturn(locationIterator2); + + trackDataListener1.clearTrackPoints(); + locationIterator1.expectSampledLocationsDelivered(trackDataListener1, 4, false); + trackDataListener1.onNewTrackPointsDone(); + trackDataListener2.clearTrackPoints(); + locationIterator2.expectSampledLocationsDelivered(trackDataListener2, 4, true); + trackDataListener2.onNewTrackPointsDone(); + replay(); + + trackDataHub.start(); + trackDataHub.registerTrackDataListener( + trackDataListener1, EnumSet.of(TrackDataType.SAMPLED_IN_TRACK_POINTS_TABLE)); + trackDataHub.registerTrackDataListener(trackDataListener2, EnumSet.of( + TrackDataType.SAMPLED_IN_TRACK_POINTS_TABLE, TrackDataType.SAMPLED_OUT_TRACK_POINTS_TABLE)); + verifyAndReset(); + } + + /** + * Tests track points table update with resampling. + */ + public void testTrackPointsTableUpdate_resampling() { + Capture observerCapture = new Capture(); + dataSource.registerOnSharedPreferenceChangeListener(capture(preferenceChangeListenerCapture)); + dataSource.registerContentObserver( + eq(TrackPointsColumns.CONTENT_URI), capture(observerCapture)); + + // Deliver 30 points (no sampling happens) + FixedSizeLocationIterator locationIterator = new FixedSizeLocationIterator(1, 30, 5); + expect(myTracksProviderUtils.getLocationIterator( + eq(TRACK_ID), eq(0L), eq(false), isA(LocationFactory.class))).andReturn(locationIterator); + expect(myTracksProviderUtils.getLastLocationId(TRACK_ID)).andReturn(30L); + + trackDataListener1.clearTrackPoints(); + locationIterator.expectLocationsDelivered(trackDataListener1); + trackDataListener1.onNewTrackPointsDone(); + replay(); + + trackDataHub.start(); + trackDataHub.registerTrackDataListener( + trackDataListener1, EnumSet.of(TrackDataType.SAMPLED_IN_TRACK_POINTS_TABLE)); + verifyAndReset(); + + // Now deliver 30 more (incrementally sampled) + ContentObserver observer = observerCapture.getValue(); + locationIterator = new FixedSizeLocationIterator(31, 30); + expect(myTracksProviderUtils.getLocationIterator( + eq(TRACK_ID), eq(31L), eq(false), isA(LocationFactory.class))).andReturn(locationIterator); + expect(myTracksProviderUtils.getLastLocationId(TRACK_ID)).andReturn(60L); + locationIterator.expectSampledLocationsDelivered(trackDataListener1, 2, false); + trackDataListener1.onNewTrackPointsDone(); + replay(); + + observer.onChange(false); + verifyAndReset(); + + // Now another 30 (triggers resampling) + locationIterator = new FixedSizeLocationIterator(1, 90); + expect(myTracksProviderUtils.getLocationIterator( + eq(TRACK_ID), eq(0L), eq(false), isA(LocationFactory.class))).andReturn(locationIterator); + expect(myTracksProviderUtils.getLastLocationId(TRACK_ID)).andReturn(90L); + trackDataListener1.clearTrackPoints(); + locationIterator.expectSampledLocationsDelivered(trackDataListener1, 2, false); + trackDataListener1.onNewTrackPointsDone(); + replay(); + + observer.onChange(false); + verifyAndReset(); + } + + /** + * Tests headings change. + */ + public void testHeadingsChange() throws Exception { + dataSource.registerOnSharedPreferenceChangeListener(capture(preferenceChangeListenerCapture)); + Capture sensorEventListenerCapture = new Capture(); + dataSource.registerHeadingListener(capture(sensorEventListenerCapture)); + Capture locationListenerCapture = new Capture(); + dataSource.registerLocationListener(capture(locationListenerCapture)); + + SensorEvent event = newSensorEvent(); + + // Expect location state changed + trackDataListener1.onLocationStateChanged(isA(LocationState.class)); + AndroidMock.expectLastCall().anyTimes(); + + // First, get a dummy heading update + trackDataListener1.onHeadingChanged(0.0); + + // Second, get a heading update without known location + trackDataListener1.onHeadingChanged(42.0f); + replay(); + + // Register one listener and update heading value + trackDataHub.start(); + trackDataHub.registerTrackDataListener( + trackDataListener1, EnumSet.of(TrackDataType.HEADING, TrackDataType.LOCATION)); + SensorEventListener sensorListener = sensorEventListenerCapture.getValue(); + LocationListener locationListener = locationListenerCapture.getValue(); + event.values[0] = 42.0f; + sensorListener.onSensorChanged(event); + verifyAndReset(); + + // Expect location state changed + trackDataListener1.onLocationStateChanged(isA(LocationState.class)); + AndroidMock.expectLastCall().anyTimes(); + + // Expect location changed + trackDataListener1.onLocationChanged(isA(Location.class)); + AndroidMock.expectLastCall().anyTimes(); + + // Expect a heading update with declination + trackDataListener1.onHeadingChanged(52.0); + replay(); + + // Update location and sensor + Location location = new Location("gps"); + location.setLatitude(10.0); + location.setLongitude(20.0); + location.setAltitude(30.0); + declination = 10.0f; + locationListener.onLocationChanged(location); + sensorListener.onSensorChanged(event); + verifyAndReset(); + + trackDataListener1.onHeadingChanged(52.0); + replay(); + + /* + * Change declination. Should still get the old value since the declination + * is only updated once an hour. + */ + declination = 20.0f; + sensorListener.onSensorChanged(event); + verifyAndReset(); + } + + /** + * Tests preferences change. + */ + public void testPreferencesChange() throws Exception { + + // Register two listeners + PreferencesUtils.setBoolean(context, R.string.report_speed_key, true); + PreferencesUtils.setBoolean(context, R.string.metric_units_key, true); + + dataSource.registerOnSharedPreferenceChangeListener(capture(preferenceChangeListenerCapture)); + expect(trackDataListener1.onMetricUnitsChanged(true)).andReturn(false); + expect(trackDataListener1.onReportSpeedChanged(true)).andReturn(false); + expect(trackDataListener2.onMetricUnitsChanged(true)).andReturn(false); + expect(trackDataListener2.onReportSpeedChanged(true)).andReturn(false); + replay(); + + trackDataHub.start(); + trackDataHub.registerTrackDataListener( + trackDataListener1, EnumSet.of(TrackDataType.PREFERENCE)); + trackDataHub.registerTrackDataListener( + trackDataListener2, EnumSet.of(TrackDataType.PREFERENCE)); + verifyAndReset(); + + // Change report speed to false + expect(trackDataListener1.onReportSpeedChanged(false)).andReturn(false); + expect(trackDataListener2.onReportSpeedChanged(false)).andReturn(false); + replay(); + + PreferencesUtils.setBoolean(context, R.string.report_speed_key, false); + OnSharedPreferenceChangeListener listener = preferenceChangeListenerCapture.getValue(); + listener.onSharedPreferenceChanged( + sharedPreferences, PreferencesUtils.getKey(context, R.string.report_speed_key)); + verifyAndReset(); + + // Change metric units to false + expect(trackDataListener1.onMetricUnitsChanged(false)).andReturn(false); + expect(trackDataListener2.onMetricUnitsChanged(false)).andReturn(false); + replay(); + + PreferencesUtils.setBoolean(context, R.string.metric_units_key, false); + listener.onSharedPreferenceChanged( + sharedPreferences, PreferencesUtils.getKey(context, R.string.metric_units_key)); + verifyAndReset(); + } + + /** + * Creates a new sensor event. + */ + private SensorEvent newSensorEvent() throws Exception { + Constructor constructor = SensorEvent.class.getDeclaredConstructor(int.class); + constructor.setAccessible(true); + return constructor.newInstance(3); + } + + /** + * Replays mocks. + */ + private void replay() { + AndroidMock.replay(myTracksProviderUtils, dataSource, trackDataListener1, trackDataListener2); + } + + /** + * Verifies and resets mocks. + */ + private void verifyAndReset() { + AndroidMock.verify(myTracksProviderUtils, dataSource, trackDataListener1, trackDataListener2); + AndroidMock.reset(myTracksProviderUtils, dataSource, trackDataListener1, trackDataListener2); + } + + /** + * Fixed size cursor answer. + * + * @author Jimmy Shih + */ private static class FixedSizeCursorAnswer implements IAnswer { private final int size; @@ -203,79 +684,86 @@ public class TrackDataHubTest extends AndroidTestCase { @Override public Cursor answer() throws Throwable { MatrixCursor cursor = new MatrixCursor(new String[] { BaseColumns._ID }); - for (long i = 1; i <= size; i++) { + for (long i = 0; i < size; i++) { cursor.addRow(new Object[] { i }); } return cursor; } } + /** + * Fixed size location iterator. + * + * @author Jimmy Shih + */ private static class FixedSizeLocationIterator implements LocationIterator { private final long startId; - private final Location[] locs; + private final Location[] locations; private final Set splitIndexSet = new HashSet(); - private int currentIdx = -1; + private int currentIndex = -1; public FixedSizeLocationIterator(long startId, int size) { this(startId, size, null); } - public FixedSizeLocationIterator(long startId, int size, int... splitIndices) { + public FixedSizeLocationIterator(long startId, int size, int... splitIndexes) { this.startId = startId; - this.locs = new Location[size]; + this.locations = new Location[size]; for (int i = 0; i < size; i++) { - Location loc = new Location("gps"); - loc.setLatitude(-15.0 + i / 1000.0); - loc.setLongitude(37 + i / 1000.0); - loc.setAltitude(i); - - locs[i] = loc; + Location location = new Location("gps"); + location.setLatitude(-15.0 + i / 1000.0); + location.setLongitude(37 + i / 1000.0); + location.setAltitude(i); + locations[i] = location; } - if (splitIndices != null) { - for (int splitIdx : splitIndices) { - splitIndexSet.add(splitIdx); + if (splitIndexes != null) { + for (int splitIndex : splitIndexes) { + splitIndexSet.add(splitIndex); - Location splitLoc = locs[splitIdx]; - splitLoc.setLatitude(100.0); - splitLoc.setLongitude(200.0); + Location splitLocation = locations[splitIndex]; + splitLocation.setLatitude(100.0); + splitLocation.setLongitude(200.0); } } } public void expectLocationsDelivered(TrackDataListener listener) { - for (int i = 0; i < locs.length; i++) { + for (int i = 0; i < locations.length; i++) { if (splitIndexSet.contains(i)) { listener.onSegmentSplit(); } else { - listener.onNewTrackPoint(locs[i]); + listener.onSampledInTrackPoint(locations[i]); } } } public void expectSampledLocationsDelivered( TrackDataListener listener, int sampleFrequency, boolean includeSampledOut) { - for (int i = 0; i < locs.length; i++) { + boolean includeNext = false; + for (int i = 0; i < locations.length; i++) { if (splitIndexSet.contains(i)) { listener.onSegmentSplit(); - } else if (i % sampleFrequency == 0) { - listener.onNewTrackPoint(locs[i]); + includeNext = true; + } else if (includeNext || (i % sampleFrequency == 0)) { + listener.onSampledInTrackPoint(locations[i]); + includeNext = false; } else if (includeSampledOut) { - listener.onSampledOutTrackPoint(locs[i]); + listener.onSampledOutTrackPoint(locations[i]); } } } @Override public boolean hasNext() { - return currentIdx < (locs.length - 1); + return currentIndex < locations.length - 1; } @Override public Location next() { - currentIdx++; - return locs[currentIdx]; + currentIndex++; + return locations[currentIndex]; } @Override @@ -285,7 +773,7 @@ public class TrackDataHubTest extends AndroidTestCase { @Override public long getLocationId() { - return startId + currentIdx; + return startId + currentIndex; } @Override @@ -293,544 +781,4 @@ public class TrackDataHubTest extends AndroidTestCase { // Do nothing } } - - public void testWaypointListen() { - Capture observerCapture = new Capture(); - - Waypoint wpt1 = new Waypoint(), - wpt2 = new Waypoint(), - wpt3 = new Waypoint(), - wpt4 = new Waypoint(); - Location loc = new Location("gps"); - loc.setLatitude(10.0); - loc.setLongitude(8.0); - wpt1.setLocation(loc); - wpt2.setLocation(loc); - wpt3.setLocation(loc); - wpt4.setLocation(loc); - - expect(providerUtils.getWaypointsCursor( - eq(TRACK_ID), leq(0L), eq(Constants.MAX_DISPLAYED_WAYPOINTS_POINTS))) - .andStubAnswer(new FixedSizeCursorAnswer(2)); - expect(providerUtils.createWaypoint(isA(Cursor.class))) - .andReturn(wpt1) - .andReturn(wpt2) - .andReturn(wpt1) - .andReturn(wpt2); - - expectStart(); - dataSource.registerContentObserver(eq(WaypointsColumns.CONTENT_URI), capture(observerCapture)); - - // Expect the initial loading. - // Both listeners (registered before and after start) should get the same data. - listener1.clearWaypoints(); - listener1.onNewWaypoint(wpt1); - listener1.onNewWaypoint(wpt2); - listener1.onNewWaypointsDone(); - listener2.clearWaypoints(); - listener2.onNewWaypoint(wpt1); - listener2.onNewWaypoint(wpt2); - listener2.onNewWaypointsDone(); - - replay(); - - hub.registerTrackDataListener(listener1, EnumSet.of(TrackDataType.WAYPOINTS_TABLE)); - hub.start(); - hub.registerTrackDataListener(listener2, EnumSet.of(TrackDataType.WAYPOINTS_TABLE)); - - verifyAndReset(); - - ContentObserver observer = observerCapture.getValue(); - - expect(providerUtils.getWaypointsCursor( - eq(TRACK_ID), leq(0L), eq(Constants.MAX_DISPLAYED_WAYPOINTS_POINTS))) - .andStubAnswer(new FixedSizeCursorAnswer(3)); - expect(providerUtils.createWaypoint(isA(Cursor.class))) - .andReturn(wpt1) - .andReturn(wpt2) - .andReturn(wpt3); - - // Now expect an update. - listener1.clearWaypoints(); - listener2.clearWaypoints(); - listener1.onNewWaypoint(wpt1); - listener2.onNewWaypoint(wpt1); - listener1.onNewWaypoint(wpt2); - listener2.onNewWaypoint(wpt2); - listener1.onNewWaypoint(wpt3); - listener2.onNewWaypoint(wpt3); - listener1.onNewWaypointsDone(); - listener2.onNewWaypointsDone(); - - replay(); - - observer.onChange(false); - - verifyAndReset(); - - // Unregister one, get another update. - expect(providerUtils.getWaypointsCursor( - eq(TRACK_ID), leq(0L), eq(Constants.MAX_DISPLAYED_WAYPOINTS_POINTS))) - .andStubAnswer(new FixedSizeCursorAnswer(4)); - expect(providerUtils.createWaypoint(isA(Cursor.class))) - .andReturn(wpt1) - .andReturn(wpt2) - .andReturn(wpt3) - .andReturn(wpt4); - - // Now expect an update. - listener2.clearWaypoints(); - listener2.onNewWaypoint(wpt1); - listener2.onNewWaypoint(wpt2); - listener2.onNewWaypoint(wpt3); - listener2.onNewWaypoint(wpt4); - listener2.onNewWaypointsDone(); - - replay(); - - hub.unregisterTrackDataListener(listener1); - - observer.onChange(false); - - verifyAndReset(); - - // Unregister the other, expect internal unregistration - dataSource.unregisterContentObserver(observer); - - replay(); - - hub.unregisterTrackDataListener(listener2); - observer.onChange(false); - - verifyAndReset(); - } - - public void testPointsListen() { - Capture observerCapture = new Capture(); - - expectStart(); - dataSource.registerContentObserver( - eq(TrackPointsColumns.CONTENT_URI), capture(observerCapture)); - - FixedSizeLocationIterator locationIterator = new FixedSizeLocationIterator(1, 10, 5); - expect(providerUtils.getLocationIterator( - eq(TRACK_ID), eq(0L), eq(false), isA(LocationFactory.class))) - .andReturn(locationIterator); - expect(providerUtils.getLastLocationId(TRACK_ID)).andReturn(10L); - - listener1.clearTrackPoints(); - locationIterator.expectLocationsDelivered(listener1); - listener1.onNewTrackPointsDone(); - - replay(); - - hub.start(); - hub.registerTrackDataListener(listener1, EnumSet.of(TrackDataType.TRACK_POINTS_TABLE)); - - verifyAndReset(); - - // Register a second listener - it will get the same points as the previous one - locationIterator = new FixedSizeLocationIterator(1, 10, 5); - expect(providerUtils.getLocationIterator( - eq(TRACK_ID), eq(0L), eq(false), isA(LocationFactory.class))) - .andReturn(locationIterator); - expect(providerUtils.getLastLocationId(TRACK_ID)).andReturn(10L); - - listener2.clearTrackPoints(); - locationIterator.expectLocationsDelivered(listener2); - listener2.onNewTrackPointsDone(); - - replay(); - - hub.start(); - hub.registerTrackDataListener(listener2, EnumSet.of(TrackDataType.TRACK_POINTS_TABLE)); - - verifyAndReset(); - - // Deliver more points - should go to both listeners, without clearing. - ContentObserver observer = observerCapture.getValue(); - - locationIterator = new FixedSizeLocationIterator(11, 10, 1); - expect(providerUtils.getLocationIterator( - eq(TRACK_ID), eq(11L), eq(false), isA(LocationFactory.class))) - .andReturn(locationIterator); - expect(providerUtils.getLastLocationId(TRACK_ID)).andReturn(20L); - - locationIterator.expectLocationsDelivered(listener1); - locationIterator.expectLocationsDelivered(listener2); - listener1.onNewTrackPointsDone(); - listener2.onNewTrackPointsDone(); - - replay(); - - observer.onChange(false); - - verifyAndReset(); - - // Unregister listener1, switch tracks to ensure data is cleared/reloaded. - locationIterator = new FixedSizeLocationIterator(101, 10); - expect(providerUtils.getLocationIterator( - eq(TRACK_ID + 1), eq(0L), eq(false), isA(LocationFactory.class))) - .andReturn(locationIterator); - expect(providerUtils.getLastLocationId(TRACK_ID + 1)).andReturn(110L); - - listener2.clearTrackPoints(); - locationIterator.expectLocationsDelivered(listener2); - listener2.onNewTrackPointsDone(); - - replay(); - - hub.unregisterTrackDataListener(listener1); - hub.loadTrack(TRACK_ID + 1); - - verifyAndReset(); - } - - public void testPointsListen_beforeStart() { - - } - - public void testPointsListen_reRegister() { - Capture observerCapture = new Capture(); - - expectStart(); - dataSource.registerContentObserver( - eq(TrackPointsColumns.CONTENT_URI), capture(observerCapture)); - - FixedSizeLocationIterator locationIterator = new FixedSizeLocationIterator(1, 10, 5); - expect(providerUtils.getLocationIterator( - eq(TRACK_ID), eq(0L), eq(false), isA(LocationFactory.class))) - .andReturn(locationIterator); - expect(providerUtils.getLastLocationId(TRACK_ID)).andReturn(10L); - - listener1.clearTrackPoints(); - locationIterator.expectLocationsDelivered(listener1); - listener1.onNewTrackPointsDone(); - - replay(); - - hub.start(); - hub.registerTrackDataListener(listener1, EnumSet.of(TrackDataType.TRACK_POINTS_TABLE)); - - verifyAndReset(); - - // Unregister - ContentObserver observer = observerCapture.getValue(); - dataSource.unregisterContentObserver(observer); - - replay(); - - hub.unregisterTrackDataListener(listener1); - - verifyAndReset(); - - // Register again, except only points since unregistered. - dataSource.registerContentObserver( - eq(TrackPointsColumns.CONTENT_URI), capture(observerCapture)); - - locationIterator = new FixedSizeLocationIterator(1, 10, 5); - expect(providerUtils.getLocationIterator( - eq(TRACK_ID), eq(0L), eq(false), isA(LocationFactory.class))) - .andReturn(locationIterator); - expect(providerUtils.getLastLocationId(TRACK_ID)).andReturn(10L); - - listener1.clearTrackPoints(); - locationIterator.expectLocationsDelivered(listener1); - listener1.onNewTrackPointsDone(); - - replay(); - - hub.registerTrackDataListener(listener1, EnumSet.of(TrackDataType.TRACK_POINTS_TABLE)); - - verifyAndReset(); - } - - public void testPointsListen_reRegisterTrackChanged() { - Capture observerCapture = new Capture(); - - expectStart(); - dataSource.registerContentObserver( - eq(TrackPointsColumns.CONTENT_URI), capture(observerCapture)); - - FixedSizeLocationIterator locationIterator = new FixedSizeLocationIterator(1, 10, 5); - expect(providerUtils.getLocationIterator( - eq(TRACK_ID), eq(0L), eq(false), isA(LocationFactory.class))) - .andReturn(locationIterator); - expect(providerUtils.getLastLocationId(TRACK_ID)).andReturn(10L); - - listener1.clearTrackPoints(); - locationIterator.expectLocationsDelivered(listener1); - listener1.onNewTrackPointsDone(); - - replay(); - - hub.start(); - hub.registerTrackDataListener(listener1, EnumSet.of(TrackDataType.TRACK_POINTS_TABLE)); - - verifyAndReset(); - - // Unregister - ContentObserver observer = observerCapture.getValue(); - dataSource.unregisterContentObserver(observer); - - replay(); - - hub.unregisterTrackDataListener(listener1); - - verifyAndReset(); - - // Register again after track changed, expect all points. - dataSource.registerContentObserver( - eq(TrackPointsColumns.CONTENT_URI), capture(observerCapture)); - - locationIterator = new FixedSizeLocationIterator(1, 10); - expect(providerUtils.getLocationIterator( - eq(TRACK_ID + 1), eq(0L), eq(false), isA(LocationFactory.class))) - .andReturn(locationIterator); - expect(providerUtils.getLastLocationId(TRACK_ID + 1)).andReturn(10L); - - listener1.clearTrackPoints(); - locationIterator.expectLocationsDelivered(listener1); - listener1.onNewTrackPointsDone(); - - replay(); - - hub.loadTrack(TRACK_ID + 1); - hub.registerTrackDataListener(listener1, EnumSet.of(TrackDataType.TRACK_POINTS_TABLE)); - - verifyAndReset(); - } - - public void testPointsListen_largeTrackSampling() { - Capture observerCapture = new Capture(); - - expectStart(); - dataSource.registerContentObserver( - eq(TrackPointsColumns.CONTENT_URI), capture(observerCapture)); - - FixedSizeLocationIterator locationIterator = new FixedSizeLocationIterator(1, 200, 4, 25, 71, 120); - expect(providerUtils.getLocationIterator( - eq(TRACK_ID), eq(0L), eq(false), isA(LocationFactory.class))) - .andReturn(locationIterator); - expect(providerUtils.getLastLocationId(TRACK_ID)).andReturn(200L); - - listener1.clearTrackPoints(); - listener2.clearTrackPoints(); - locationIterator.expectSampledLocationsDelivered(listener1, 4, false); - locationIterator.expectSampledLocationsDelivered(listener2, 4, true); - listener1.onNewTrackPointsDone(); - listener2.onNewTrackPointsDone(); - - replay(); - - hub.registerTrackDataListener(listener1, - EnumSet.of(TrackDataType.TRACK_POINTS_TABLE)); - hub.registerTrackDataListener(listener2, - EnumSet.of(TrackDataType.TRACK_POINTS_TABLE, TrackDataType.SAMPLED_OUT_TRACK_POINTS)); - hub.start(); - - verifyAndReset(); - } - - public void testPointsListen_resampling() { - Capture observerCapture = new Capture(); - - expectStart(); - dataSource.registerContentObserver( - eq(TrackPointsColumns.CONTENT_URI), capture(observerCapture)); - - // Deliver 30 points (no sampling happens) - FixedSizeLocationIterator locationIterator = new FixedSizeLocationIterator(1, 30, 5); - expect(providerUtils.getLocationIterator( - eq(TRACK_ID), eq(0L), eq(false), isA(LocationFactory.class))) - .andReturn(locationIterator); - expect(providerUtils.getLastLocationId(TRACK_ID)).andReturn(30L); - - listener1.clearTrackPoints(); - locationIterator.expectLocationsDelivered(listener1); - listener1.onNewTrackPointsDone(); - - replay(); - - hub.start(); - hub.registerTrackDataListener(listener1, EnumSet.of(TrackDataType.TRACK_POINTS_TABLE)); - - verifyAndReset(); - - // Now deliver 30 more (incrementally sampled) - ContentObserver observer = observerCapture.getValue(); - locationIterator = new FixedSizeLocationIterator(31, 30); - expect(providerUtils.getLocationIterator( - eq(TRACK_ID), eq(31L), eq(false), isA(LocationFactory.class))) - .andReturn(locationIterator); - expect(providerUtils.getLastLocationId(TRACK_ID)).andReturn(60L); - - locationIterator.expectSampledLocationsDelivered(listener1, 2, false); - listener1.onNewTrackPointsDone(); - - replay(); - - observer.onChange(false); - - verifyAndReset(); - - // Now another 30 (triggers resampling) - locationIterator = new FixedSizeLocationIterator(1, 90); - expect(providerUtils.getLocationIterator( - eq(TRACK_ID), eq(0L), eq(false), isA(LocationFactory.class))) - .andReturn(locationIterator); - expect(providerUtils.getLastLocationId(TRACK_ID)).andReturn(90L); - - listener1.clearTrackPoints(); - locationIterator.expectSampledLocationsDelivered(listener1, 2, false); - listener1.onNewTrackPointsDone(); - - replay(); - - observer.onChange(false); - - verifyAndReset(); - } - - public void testLocationListen() { - // TODO - } - - public void testCompassListen() throws Exception { - AndroidMock.resetToDefault(listener1); - - expectStart(); - Capture listenerCapture = new Capture(); - dataSource.registerCompassListener(capture(listenerCapture)); - - Capture locationListenerCapture = new Capture(); - dataSource.registerLocationListener(capture(locationListenerCapture)); - - SensorEvent event = newSensorEvent(); - - // First, get a dummy heading update. - listener1.onCurrentHeadingChanged(0.0); - - // Then, get a heading update without a known location (thus can't calculate declination). - listener1.onCurrentHeadingChanged(42.0f); - - // Also expect location updates which are not relevant to us. - listener1.onProviderStateChange(isA(ProviderState.class)); - AndroidMock.expectLastCall().anyTimes(); - - replay(); - - hub.registerTrackDataListener(listener1, - EnumSet.of(TrackDataType.COMPASS, TrackDataType.LOCATION)); - hub.start(); - - SensorEventListener sensorListener = listenerCapture.getValue(); - LocationListener locationListener = locationListenerCapture.getValue(); - event.values[0] = 42.0f; - sensorListener.onSensorChanged(event); - - verifyAndReset(); - - // Expect the heading update to include declination. - listener1.onCurrentHeadingChanged(52.0); - - // Also expect location updates which are not relevant to us. - listener1.onProviderStateChange(isA(ProviderState.class)); - AndroidMock.expectLastCall().anyTimes(); - listener1.onCurrentLocationChanged(isA(Location.class)); - AndroidMock.expectLastCall().anyTimes(); - - replay(); - - // Now try injecting a location update, triggering a declination update. - Location location = new Location("gps"); - location.setLatitude(10.0); - location.setLongitude(20.0); - location.setAltitude(30.0); - declination = 10.0f; - locationListener.onLocationChanged(location); - sensorListener.onSensorChanged(event); - - verifyAndReset(); - - listener1.onCurrentHeadingChanged(52.0); - - replay(); - - // Now try changing the known declination - it should still return the old declination, since - // updates only happen sparsely. - declination = 20.0f; - sensorListener.onSensorChanged(event); - - verifyAndReset(); - } - - private Sensor newSensor() throws Exception { - Constructor constructor = Sensor.class.getDeclaredConstructor(); - constructor.setAccessible(true); - return constructor.newInstance(); - } - - private SensorEvent newSensorEvent() throws Exception { - Constructor constructor = SensorEvent.class.getDeclaredConstructor(int.class); - constructor.setAccessible(true); - return constructor.newInstance(3); - } - - public void testDisplayPreferencesListen() throws Exception { - PreferencesUtils.setBoolean(context, R.string.report_speed_key, true); - PreferencesUtils.setBoolean(context, R.string.metric_units_key, true); - - expectStart(); - - expect(listener1.onUnitsChanged(true)).andReturn(false); - expect(listener2.onUnitsChanged(true)).andReturn(false); - expect(listener1.onReportSpeedChanged(true)).andReturn(false); - expect(listener2.onReportSpeedChanged(true)).andReturn(false); - - replay(); - - hub.registerTrackDataListener(listener1, EnumSet.of(TrackDataType.PREFERENCE)); - hub.start(); - hub.registerTrackDataListener(listener2, EnumSet.of(TrackDataType.PREFERENCE)); - - verifyAndReset(); - - expect(listener1.onReportSpeedChanged(false)).andReturn(false); - expect(listener2.onReportSpeedChanged(false)).andReturn(false); - - replay(); - - PreferencesUtils.setBoolean(context, R.string.report_speed_key, false); - OnSharedPreferenceChangeListener listener = preferenceListenerCapture.getValue(); - listener.onSharedPreferenceChanged( - sharedPreferences, PreferencesUtils.getKey(context, R.string.report_speed_key)); - - AndroidMock.verify(dataSource, providerUtils, listener1, listener2); - AndroidMock.reset(dataSource, providerUtils, listener1, listener2); - - expect(listener1.onUnitsChanged(false)).andReturn(false); - expect(listener2.onUnitsChanged(false)).andReturn(false); - - replay(); - - PreferencesUtils.setBoolean(context, R.string.metric_units_key, false); - listener.onSharedPreferenceChanged( - sharedPreferences, PreferencesUtils.getKey(context, R.string.metric_units_key)); - - verifyAndReset(); - } - - private void expectStart() { - dataSource.registerOnSharedPreferenceChangeListener(capture(preferenceListenerCapture)); - } - - private void replay() { - AndroidMock.replay(dataSource, providerUtils, listener1, listener2); - } - - private void verifyAndReset() { - AndroidMock.verify(listener1, listener2, dataSource, providerUtils); - AndroidMock.reset(listener1, listener2, dataSource, providerUtils); - } }