From 8fb0b69a791668d8070bda1e4dce8c6f1bd15f32 Mon Sep 17 00:00:00 2001 From: Rodrigo Damazio Date: Sat, 30 Apr 2011 02:39:56 -0300 Subject: [PATCH] Further splitting the data hub. --- .../mytracks/content/DataSourceManager.java | 264 ++++++++++ ...taSources.java => DataSourcesWrapper.java} | 4 +- ...sImpl.java => DataSourcesWrapperImpl.java} | 4 +- .../apps/mytracks/content/TrackDataHub.java | 463 +++++------------- .../mytracks/content/TrackDataListener.java | 2 - .../mytracks/content/TrackDataListeners.java | 135 +++++ .../mytracks/content/TrackDataHubTest.java | 10 +- 7 files changed, 519 insertions(+), 363 deletions(-) create mode 100644 MyTracks/src/com/google/android/apps/mytracks/content/DataSourceManager.java rename MyTracks/src/com/google/android/apps/mytracks/content/{TrackDataSources.java => DataSourcesWrapper.java} (94%) rename MyTracks/src/com/google/android/apps/mytracks/content/{TrackDataSourcesImpl.java => DataSourcesWrapperImpl.java} (97%) create mode 100644 MyTracks/src/com/google/android/apps/mytracks/content/TrackDataListeners.java diff --git a/MyTracks/src/com/google/android/apps/mytracks/content/DataSourceManager.java b/MyTracks/src/com/google/android/apps/mytracks/content/DataSourceManager.java new file mode 100644 index 000000000..2b532bfa4 --- /dev/null +++ b/MyTracks/src/com/google/android/apps/mytracks/content/DataSourceManager.java @@ -0,0 +1,264 @@ +package com.google.android.apps.mytracks.content; + +import static com.google.android.apps.mytracks.Constants.TAG; + +import com.google.android.apps.mytracks.content.TrackDataHub.ListenerDataType; + +import android.content.SharedPreferences; +import android.content.SharedPreferences.OnSharedPreferenceChangeListener; +import android.database.ContentObserver; +import android.hardware.Sensor; +import android.hardware.SensorEvent; +import android.hardware.SensorEventListener; +import android.hardware.SensorManager; +import android.location.Location; +import android.location.LocationListener; +import android.location.LocationManager; +import android.location.LocationProvider; +import android.os.Bundle; +import android.os.Handler; +import android.util.Log; + +import java.util.EnumSet; +import java.util.Set; + +/** + * External data source manager, which converts system-level events into My Tracks data events. + * + * @author Rodrigo Damazio + */ +class DataSourceManager { + + /** Single interface for receiving system events that were registered for. */ + interface DataSourceListener { + void notifyTrackUpdated(); + void notifyWaypointUpdated(); + void notifyPointsUpdated(); + void notifyPreferenceChanged(String key); + void notifyLocationProviderEnabled(boolean enabled); + void notifyLocationChanged(Location loc); + void notifyHeadingChanged(float heading); + } + + private final DataSourceListener listener; + + /** Observer for when the tracks table is updated. */ + private class TrackObserver extends ContentObserver { + public TrackObserver() { + super(contentHandler); + } + + @Override + public void onChange(boolean selfChange) { + listener.notifyTrackUpdated(); + } + } + + /** Observer for when the waypoints table is updated. */ + private class WaypointObserver extends ContentObserver { + public WaypointObserver() { + super(contentHandler); + } + + @Override + public void onChange(boolean selfChange) { + listener.notifyWaypointUpdated(); + } + } + + /** Observer for when the points table is updated. */ + private class PointObserver extends ContentObserver { + public PointObserver() { + super(contentHandler); + } + + @Override + public void onChange(boolean selfChange) { + listener.notifyPointsUpdated(); + } + } + + /** Listener for when preferences change. */ + private class HubSharedPreferenceListener implements OnSharedPreferenceChangeListener { + @Override + public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { + listener.notifyPreferenceChanged(key); + } + } + + /** Listener for the current location (independent from track data). */ + private class CurrentLocationListener implements + LocationListener { + @Override + public void onStatusChanged(String provider, int status, Bundle extras) { + if (!LocationManager.GPS_PROVIDER.equals(provider)) return; + + listener.notifyLocationProviderEnabled(status == LocationProvider.AVAILABLE); + } + + @Override + public void onProviderEnabled(String provider) { + if (!LocationManager.GPS_PROVIDER.equals(provider)) return; + + listener.notifyLocationProviderEnabled(true); + } + + @Override + public void onProviderDisabled(String provider) { + if (!LocationManager.GPS_PROVIDER.equals(provider)) return; + + listener.notifyLocationProviderEnabled(false); + } + + @Override + public void onLocationChanged(Location location) { + listener.notifyLocationChanged(location); + } + } + + /** Listener for compass readings. */ + private class CompassListener implements + SensorEventListener { + @Override + public void onSensorChanged(SensorEvent event) { + listener.notifyHeadingChanged(event.values[0]); + } + + @Override + public void onAccuracyChanged(Sensor sensor, int accuracy) { + // Do nothing + } + } + + /** Wrapper for registering internal listeners. */ + private final DataSourcesWrapper dataSources; + + // Internal listeners (to receive data from the system) + private final Set registeredListeners = + EnumSet.noneOf(ListenerDataType.class); + private final Handler contentHandler; + private final ContentObserver pointObserver; + private final ContentObserver waypointObserver; + private final ContentObserver trackObserver; + private final LocationListener locationListener; + private final OnSharedPreferenceChangeListener preferenceListener; + private final SensorEventListener compassListener; + + DataSourceManager(DataSourceListener listener, DataSourcesWrapper dataSources) { + this.listener = listener; + this.dataSources = dataSources; + + contentHandler = new Handler(); + pointObserver = new PointObserver(); + waypointObserver = new WaypointObserver(); + trackObserver = new TrackObserver(); + + compassListener = new CompassListener(); + locationListener = new CurrentLocationListener(); + preferenceListener = new HubSharedPreferenceListener(); + } + + /** Updates the internal (sensor, position, etc) listeners. */ + void updateAllListeners(EnumSet externallyNeededListeners) { + EnumSet neededListeners = EnumSet.copyOf(externallyNeededListeners); + + // Special case - map sampled-out points type to points type since they + // correspond to the same internal listener. + if (neededListeners.contains(ListenerDataType.SAMPLED_OUT_POINT_UPDATES)) { + neededListeners.remove(ListenerDataType.SAMPLED_OUT_POINT_UPDATES); + neededListeners.add(ListenerDataType.POINT_UPDATES); + } + + Log.d(TAG, "Updating internal listeners to types " + neededListeners); + + // Unnecessary = registered - needed + Set unnecessaryListeners = EnumSet.copyOf(registeredListeners); + unnecessaryListeners.removeAll(neededListeners); + + // Missing = needed - registered + Set missingListeners = EnumSet.copyOf(neededListeners); + missingListeners.removeAll(registeredListeners); + + // Remove all unnecessary listeners. + for (ListenerDataType type : unnecessaryListeners) { + unregisterListener(type); + } + + // Add all missing listeners. + for (ListenerDataType type : missingListeners) { + registerListener(type); + } + + // Now all needed types are registered. + registeredListeners.clear(); + registeredListeners.addAll(neededListeners); + } + + private void registerListener(ListenerDataType type) { + switch (type) { + case COMPASS_UPDATES: { + // Listen to compass + Sensor compass = dataSources.getSensor(Sensor.TYPE_ORIENTATION); + if (compass != null) { + Log.d(TAG, "TrackDataHub: Now registering sensor listener."); + dataSources.registerSensorListener(compassListener, compass, SensorManager.SENSOR_DELAY_UI); + } + break; + } + case LOCATION_UPDATES: + dataSources.requestLocationUpdates(locationListener); + break; + case POINT_UPDATES: + dataSources.registerContentObserver( + TrackPointsColumns.CONTENT_URI, false, pointObserver); + break; + case TRACK_UPDATES: + dataSources.registerContentObserver(TracksColumns.CONTENT_URI, false, trackObserver); + break; + case WAYPOINT_UPDATES: + dataSources.registerContentObserver( + WaypointsColumns.CONTENT_URI, false, waypointObserver); + break; + case DISPLAY_PREFERENCES: + dataSources.registerOnSharedPreferenceChangeListener(preferenceListener); + break; + case SAMPLED_OUT_POINT_UPDATES: + throw new IllegalArgumentException("Should have been mapped to point updates"); + } + } + + private void unregisterListener(ListenerDataType type) { + switch (type) { + case COMPASS_UPDATES: + dataSources.unregisterSensorListener(compassListener); + break; + case LOCATION_UPDATES: + dataSources.removeLocationUpdates(locationListener); + break; + case POINT_UPDATES: + dataSources.unregisterContentObserver(pointObserver); + break; + case TRACK_UPDATES: + dataSources.unregisterContentObserver(trackObserver); + break; + case WAYPOINT_UPDATES: + dataSources.unregisterContentObserver(waypointObserver); + break; + case DISPLAY_PREFERENCES: + dataSources.unregisterOnSharedPreferenceChangeListener(preferenceListener); + break; + case SAMPLED_OUT_POINT_UPDATES: + throw new IllegalArgumentException("Should have been mapped to point updates"); + } + } + + /** Unregisters all internal (sensor, position, etc.) listeners. */ + void unregisterAllListeners() { + dataSources.removeLocationUpdates(locationListener); + dataSources.unregisterSensorListener(compassListener); + dataSources.unregisterContentObserver(trackObserver); + dataSources.unregisterContentObserver(waypointObserver); + dataSources.unregisterContentObserver(pointObserver); + dataSources.unregisterOnSharedPreferenceChangeListener(preferenceListener); + } +} diff --git a/MyTracks/src/com/google/android/apps/mytracks/content/TrackDataSources.java b/MyTracks/src/com/google/android/apps/mytracks/content/DataSourcesWrapper.java similarity index 94% rename from MyTracks/src/com/google/android/apps/mytracks/content/TrackDataSources.java rename to MyTracks/src/com/google/android/apps/mytracks/content/DataSourcesWrapper.java index 994240f9f..a4dbf3544 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/content/TrackDataSources.java +++ b/MyTracks/src/com/google/android/apps/mytracks/content/DataSourcesWrapper.java @@ -24,11 +24,11 @@ import android.location.LocationListener; import android.net.Uri; /** - * Interface for abstracting external data sources. + * Interface for abstracting registration of external data source listeners. * * @author Rodrigo Damazio */ -interface TrackDataSources { +interface DataSourcesWrapper { // Preferences void registerOnSharedPreferenceChangeListener( OnSharedPreferenceChangeListener listener); diff --git a/MyTracks/src/com/google/android/apps/mytracks/content/TrackDataSourcesImpl.java b/MyTracks/src/com/google/android/apps/mytracks/content/DataSourcesWrapperImpl.java similarity index 97% rename from MyTracks/src/com/google/android/apps/mytracks/content/TrackDataSourcesImpl.java rename to MyTracks/src/com/google/android/apps/mytracks/content/DataSourcesWrapperImpl.java index c5ab73af4..c4a94c3a0 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/content/TrackDataSourcesImpl.java +++ b/MyTracks/src/com/google/android/apps/mytracks/content/DataSourcesWrapperImpl.java @@ -42,7 +42,7 @@ import android.widget.Toast; * * @author Rodrigo Damazio */ -class TrackDataSourcesImpl implements TrackDataSources { +class DataSourcesWrapperImpl implements DataSourcesWrapper { // System services private final SensorManager sensorManager; private final LocationManager locationManager; @@ -50,7 +50,7 @@ class TrackDataSourcesImpl implements TrackDataSources { private final SharedPreferences sharedPreferences; private final Context context; - TrackDataSourcesImpl(Context context, SharedPreferences sharedPreferences) { + DataSourcesWrapperImpl(Context context, SharedPreferences sharedPreferences) { this.context = context; this.sensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE); this.locationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE); 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 5d46e3ff8..4ed5bcde6 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/content/TrackDataHub.java +++ b/MyTracks/src/com/google/android/apps/mytracks/content/TrackDataHub.java @@ -19,44 +19,27 @@ import static com.google.android.apps.mytracks.Constants.MAX_LOCATION_AGE_MS; import static com.google.android.apps.mytracks.Constants.TAG; import com.google.android.apps.mytracks.Constants; -import com.google.android.apps.mytracks.content.MyTracksProviderUtils; +import com.google.android.apps.mytracks.content.DataSourceManager.DataSourceListener; 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.Track; -import com.google.android.apps.mytracks.content.TrackPointsColumns; -import com.google.android.apps.mytracks.content.TracksColumns; -import com.google.android.apps.mytracks.content.Waypoint; -import com.google.android.apps.mytracks.content.WaypointsColumns; +import com.google.android.apps.mytracks.content.TrackDataListeners.ListenerRegistration; import com.google.android.apps.mytracks.util.ApiFeatures; import com.google.android.apps.mytracks.util.LocationUtils; import com.google.android.maps.mytracks.R; import android.content.Context; import android.content.SharedPreferences; -import android.content.SharedPreferences.OnSharedPreferenceChangeListener; -import android.database.ContentObserver; import android.database.Cursor; import android.hardware.GeomagneticField; -import android.hardware.Sensor; -import android.hardware.SensorEvent; -import android.hardware.SensorEventListener; -import android.hardware.SensorManager; import android.location.Location; -import android.location.LocationListener; import android.location.LocationManager; -import android.location.LocationProvider; -import android.os.Bundle; import android.os.Handler; import android.os.HandlerThread; import android.util.Log; import android.widget.Toast; import java.util.Collections; -import java.util.EnumMap; import java.util.EnumSet; -import java.util.HashMap; -import java.util.LinkedHashSet; -import java.util.Map; import java.util.Set; /** @@ -106,37 +89,72 @@ public class TrackDataHub { DISPLAY_PREFERENCES; } + /** Listener which receives events from the system. */ + private class HubDataSourceListener implements DataSourceListener { + @Override + public void notifyTrackUpdated() { + TrackDataHub.this.notifyTrackUpdated(getListenersFor(ListenerDataType.TRACK_UPDATES)); + } + + @Override + public void notifyWaypointUpdated() { + TrackDataHub.this.notifyWaypointUpdated(getListenersFor(ListenerDataType.WAYPOINT_UPDATES)); + } + + @Override + public void notifyPointsUpdated() { + TrackDataHub.this.notifyPointsUpdated(true, + getListenersFor(ListenerDataType.POINT_UPDATES), + getListenersFor(ListenerDataType.SAMPLED_OUT_POINT_UPDATES)); + } + + @Override + public void notifyPreferenceChanged(String key) { + TrackDataHub.this.notifyPreferenceChanged(key); + } + + @Override + public void notifyLocationProviderEnabled(boolean enabled) { + hasProviderEnabled = enabled; + TrackDataHub.this.notifyFixType(); + } + + @Override + public void notifyLocationChanged(Location loc) { + TrackDataHub.this.notifyLocationChanged(loc, + getListenersFor(ListenerDataType.LOCATION_UPDATES)); + } + + @Override + public void notifyHeadingChanged(float heading) { + lastSeenMagneticHeading = heading; + maybeUpdateDeclination(); + TrackDataHub.this.notifyHeadingChanged(getListenersFor(ListenerDataType.COMPASS_UPDATES)); + } + } + // Application services private final Context context; - private final TrackDataSources dataSources; private final MyTracksProviderUtils providerUtils; private final SharedPreferences preferences; - // Internal listeners (to receive data from the system) - private final ContentObserver pointObserver; - private final ContentObserver waypointObserver; - private final ContentObserver trackObserver; - private final LocationListener locationListener; - private final OnSharedPreferenceChangeListener preferenceListener; - private final SensorEventListener compassListener; - - /** Set of internal listeners which are already registered. */ - private final Set registeredInternalListeners = - EnumSet.noneOf(ListenerDataType.class); - - /** Map of external listener to its registration details. */ - private final Map registeredListeners = - new HashMap(); - - /** Map of data type to external listeners interested in it. */ - private final Map> listenerSetsPerType = - new EnumMap>(ListenerDataType.class); - // Get content notifications on the main thread, send listener callbacks in another. // This ensures listener calls are serialized. private final HandlerThread listenerHandlerThread; private final Handler listenerHandler; + /** Manager for external listeners (those from activities). */ + private final TrackDataListeners listeners; + + /** Wrapper for interacting with system data managers. */ + private final DataSourcesWrapper dataSources; + + /** Manager for system data listener registrations. */ + private final DataSourceManager dataSourceManager; + + /** Condensed listener for system data listener events. */ + private final DataSourceListener dataSourceListener = new HubDataSourceListener(); + /** Whether we've been started. */ private boolean started; @@ -163,158 +181,27 @@ public class TrackDataHub { private long lastSeenLocationId; private int numLoadedPoints; - /** Internal representation of a listener's registration. */ - private static class ListenerRegistration { - final TrackDataListener listener; - final EnumSet types; - // TODO: Add the last-notified point ID here, to allow pausing/resuming. - - public ListenerRegistration(TrackDataListener listener, - EnumSet types) { - this.listener = listener; - this.types = types; - } - - public boolean isInterestedIn(ListenerDataType type) { - return types.contains(type); - } - } - - /** Callback for when the tracks table is updated. */ - private class TrackObserverCallback implements Runnable { - @Override - public void run() { - notifyTrackUpdated(getListenersFor(ListenerDataType.TRACK_UPDATES)); - } - } - - /** Callback for when the waypoints table is updated. */ - private class WaypointObserverCallback implements Runnable { - @Override - public void run() { - notifyWaypointUpdated(getListenersFor(ListenerDataType.WAYPOINT_UPDATES)); - } - } - - /** Callback for when the points table is updated. */ - private class PointObserverCallback implements Runnable { - @Override - public void run() { - notifyPointsUpdated(true, - getListenersFor(ListenerDataType.POINT_UPDATES), - getListenersFor(ListenerDataType.SAMPLED_OUT_POINT_UPDATES)); - } - } - - /** Listener for when preferences change. */ - private class HubSharedPreferenceListener implements OnSharedPreferenceChangeListener { - @Override - public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { - notifyPreferenceChanged(key); - } - } - - /** - * Generic content observer which will call a given {@link Runnable} in the - * given handler if the content has changed and we're recording the selected - * track. - */ - private class TrackContentObserver extends ContentObserver { - private final Runnable callback; - - public TrackContentObserver(Handler contentHandler, Runnable callback) { - super(contentHandler); - - this.callback = callback; - } - - @Override - public void onChange(boolean selfChange) { - Log.v(TAG, "TrackContentObserver.onChange"); - - // We want to filter only updates from the selected track, but since - // we can't see what the update is, we'll let two cases pass: - // 1 - The point(s) was(ere) changed because it's a recording track - // (and thus we care about it if the recording is the selected one) - // 2 - The point(s) was(ere) changed because it's syncing a track - // (and thus there will be no new points for the selected one) - if (!isRecordingSelected()) { - return; - } - - // Update can potentially be lengthy, put it in its own thread: - runInListenerThread(callback); - } - } - - /** Listener for the current location (independent from track data). */ - private class CurrentLocationListener implements - LocationListener { - @Override - public void onStatusChanged(String provider, int status, Bundle extras) { - if (!LocationManager.GPS_PROVIDER.equals(provider)) return; - - hasProviderEnabled = (status == LocationProvider.AVAILABLE); - notifyFixType(); - } - - @Override - public void onProviderEnabled(String provider) { - if (!LocationManager.GPS_PROVIDER.equals(provider)) return; - - hasProviderEnabled = true; - notifyFixType(); - } - - @Override - public void onProviderDisabled(String provider) { - if (!LocationManager.GPS_PROVIDER.equals(provider)) return; - - hasProviderEnabled = false; - notifyFixType(); - } - - @Override - public void onLocationChanged(Location location) { - notifyLocationChanged(location, - getListenersFor(ListenerDataType.LOCATION_UPDATES)); - } - } - - /** Listener for compass readings. */ - private class CompassListener implements - SensorEventListener { - @Override - public void onSensorChanged(SensorEvent event) { - lastSeenMagneticHeading = event.values[0]; - maybeUpdateDeclination(); - notifyHeadingChanged(getListenersFor(ListenerDataType.COMPASS_UPDATES)); - } - - @Override - public void onAccuracyChanged(Sensor sensor, int accuracy) { - // Do nothing - } - } - /** * Default constructor. */ public TrackDataHub(Context ctx, SharedPreferences preferences, MyTracksProviderUtils providerUtils) { - this(ctx, new TrackDataSourcesImpl(ctx, preferences), preferences, providerUtils); + this(ctx, new DataSourcesWrapperImpl(ctx, preferences), new TrackDataListeners(), + preferences, providerUtils); } /** * Injection constructor. */ // @VisibleForTesting - TrackDataHub(Context ctx, TrackDataSources dataSources, SharedPreferences preferences, - MyTracksProviderUtils providerUtils) { + TrackDataHub(Context ctx, DataSourcesWrapper dataSources, TrackDataListeners listeners, + SharedPreferences preferences, MyTracksProviderUtils providerUtils) { this.context = ctx; - this.dataSources = dataSources; + this.listeners = listeners; this.preferences = preferences; this.providerUtils = providerUtils; + this.dataSources = dataSources; + this.dataSourceManager = new DataSourceManager(dataSourceListener, dataSources); SELECTED_TRACK_KEY = context.getString(R.string.selected_track_key); RECORDING_TRACK_KEY = context.getString(R.string.recording_track_key); @@ -322,24 +209,9 @@ public class TrackDataHub { METRIC_UNITS_KEY = context.getString(R.string.metric_units_key); SPEED_REPORTING_KEY = context.getString(R.string.report_speed_key); - // Create sets for all data type at startup. - for (ListenerDataType type : ListenerDataType.values()) { - listenerSetsPerType.put(type, new LinkedHashSet()); - } - listenerHandlerThread = new HandlerThread("trackDataContentThread"); listenerHandlerThread.start(); listenerHandler = new Handler(listenerHandlerThread.getLooper()); - - preferenceListener = new HubSharedPreferenceListener(); - - Handler contentHandler = new Handler(); - pointObserver = new TrackContentObserver(contentHandler, new PointObserverCallback()); - waypointObserver = new TrackContentObserver(contentHandler, new WaypointObserverCallback()); - trackObserver = new TrackContentObserver(contentHandler, new TrackObserverCallback()); - - compassListener = new CompassListener(); - locationListener = new CurrentLocationListener(); } /** @@ -354,12 +226,10 @@ public class TrackDataHub { } started = true; - dataSources.registerOnSharedPreferenceChangeListener(preferenceListener); - loadSharedPreferences(); - // This may or may not register internal listeners, depending on whether // we already had external listeners. - updateInternalListeners(); + dataSourceManager.updateAllListeners(getNeededListenerTypes()); + loadSharedPreferences(); // If there were listeners already registered, make sure they become up-to-date. // TODO: This should really only send new data (in a start-stop-start cycle). @@ -386,10 +256,8 @@ public class TrackDataHub { return; } - dataSources.unregisterOnSharedPreferenceChangeListener(preferenceListener); - // Unregister internal listeners even if there are external listeners registered. - unregisterInternalListeners(); + dataSourceManager.unregisterAllListeners(); started = false; } @@ -515,163 +383,38 @@ public class TrackDataHub { loadTrack(-1); } - /** - * Registers a listener to send data to. - * 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 listener the listener to register - * @param dataTypes the type of data that the listener is interested in - */ - public void registerTrackDataListener(final TrackDataListener listener, EnumSet dataTypes) { - Log.d(TAG, "Registered track data listener: " + listener); - ListenerRegistration registration = new ListenerRegistration(listener, dataTypes); - synchronized (registeredListeners) { - if (registeredListeners.get(listener) != null) { - throw new IllegalStateException("Listener already registered"); - } - registeredListeners.put(listener, registration); - - for (ListenerDataType type : dataTypes) { - // This is guaranteed not to be null. - Set typeSet = listenerSetsPerType.get(type); - typeSet.add(listener); - } + public void registerTrackDataListener( + TrackDataListener listener, EnumSet dataTypes) { + synchronized (listeners) { + ListenerRegistration registration = listeners.registerTrackDataListener(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 (!started) return; reloadDataForListener(registration); - } - updateInternalListeners(); + dataSourceManager.updateAllListeners(getNeededListenerTypes()); + } } - /** - * Unregisters a listener to send data to. - * - * @param listener the listener to unregister - */ public void unregisterTrackDataListener(TrackDataListener listener) { - Log.d(TAG, "Unregistered track data listener: " + listener); - synchronized (registeredListeners) { - // Remove and keep the corresponding registration. - ListenerRegistration match = registeredListeners.remove(listener); - if (match == null) { - Log.w(TAG, "Tried to unregister listener which is not registered."); - return; - } - - // Remove it from the per-type sets - for (ListenerDataType type : match.types) { - listenerSetsPerType.get(type).remove(listener); - } + synchronized (listeners) { + listeners.unregisterTrackDataListener(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 (!started) return; + + dataSourceManager.updateAllListeners(getNeededListenerTypes()); } - updateInternalListeners(); - } - - /** Updates the internal (sensor, position, etc) listeners. */ - private void updateInternalListeners() { - synchronized (registeredListeners) { - Set registeredListeners = registeredInternalListeners; - Set neededListeners = EnumSet.noneOf(ListenerDataType.class); - for (ListenerRegistration registration : this.registeredListeners.values()) { - neededListeners.addAll(registration.types); - } - - // Unnecessary = registered - needed - Set unnecessaryListeners = EnumSet.copyOf(registeredListeners); - unnecessaryListeners.removeAll(neededListeners); - - // Missing = needed - registered - Set missingListeners = EnumSet.copyOf(neededListeners); - missingListeners.removeAll(registeredListeners); - - // Remove all unnecessary listeners. - for (ListenerDataType type : unnecessaryListeners) { - switch (type) { - case COMPASS_UPDATES: - dataSources.unregisterSensorListener(compassListener); - break; - case LOCATION_UPDATES: - dataSources.removeLocationUpdates(locationListener); - break; - case POINT_UPDATES: - case SAMPLED_OUT_POINT_UPDATES: - // Special case - don't unregister if the other type is needed. - if (!neededListeners.contains(ListenerDataType.POINT_UPDATES) && - !neededListeners.contains(ListenerDataType.SAMPLED_OUT_POINT_UPDATES)) { - dataSources.unregisterContentObserver(pointObserver); - } - break; - case TRACK_UPDATES: - dataSources.unregisterContentObserver(trackObserver); - break; - case WAYPOINT_UPDATES: - dataSources.unregisterContentObserver(waypointObserver); - break; - } - } - - // Add all missing listeners. - for (ListenerDataType type : missingListeners) { - switch (type) { - case COMPASS_UPDATES: { - // Listen to compass - Sensor compass = dataSources.getSensor(Sensor.TYPE_ORIENTATION); - if (compass != null) { - Log.d(Constants.TAG, "TrackDataHub: Now registering sensor listener."); - dataSources.registerSensorListener(compassListener, compass, SensorManager.SENSOR_DELAY_UI); - } - break; - } - case LOCATION_UPDATES: - dataSources.requestLocationUpdates(locationListener); - break; - case POINT_UPDATES: - case SAMPLED_OUT_POINT_UPDATES: - // Special case - don't register if the other type was already registered. - if (!registeredListeners.contains(ListenerDataType.POINT_UPDATES) && - !registeredListeners.contains(ListenerDataType.SAMPLED_OUT_POINT_UPDATES)) { - dataSources.registerContentObserver( - TrackPointsColumns.CONTENT_URI, false, pointObserver); - } - break; - case TRACK_UPDATES: - dataSources.registerContentObserver(TracksColumns.CONTENT_URI, false, trackObserver); - break; - case WAYPOINT_UPDATES: - dataSources.registerContentObserver( - WaypointsColumns.CONTENT_URI, false, waypointObserver); - break; - } - } - - // Now all needed types are registered. - registeredInternalListeners.clear(); - registeredInternalListeners.addAll(neededListeners); - } // synchronized - } - - /** Unregisters all internal (sensor, position, etc.) listeners. */ - private void unregisterInternalListeners() { - dataSources.removeLocationUpdates(locationListener); - dataSources.unregisterSensorListener(compassListener); - dataSources.unregisterContentObserver(trackObserver); - dataSources.unregisterContentObserver(waypointObserver); - dataSources.unregisterContentObserver(pointObserver); } /** * Reloads all track data received so far into the specified listeners. */ public void reloadDataForListener(TrackDataListener listener) { - reloadDataForListener(registeredListeners.get(listener)); + reloadDataForListener(listeners.getRegistration(listener)); } /** @@ -738,7 +481,7 @@ public class TrackDataHub { * Reloads all track data received so far into the specified listeners. */ private void loadDataForAllListeners() { - if (registeredListeners.isEmpty()) { + if (!listeners.hasListeners()) { Log.d(TAG, "No listeners, not reloading"); return; } @@ -751,7 +494,8 @@ public class TrackDataHub { @Override public void run() { // Ignore the return values here, we're already sending the full data set anyway - for (TrackDataListener listener : getListenersFor(ListenerDataType.DISPLAY_PREFERENCES)) { + for (TrackDataListener listener : + getListenersFor(ListenerDataType.DISPLAY_PREFERENCES)) { listener.onUnitsChanged(useMetricUnits); listener.onReportSpeedChanged(reportSpeed); } @@ -809,12 +553,13 @@ public class TrackDataHub { runInListenerThread(new Runnable() { @Override public void run() { - Set listeners = getListenersFor(ListenerDataType.DISPLAY_PREFERENCES); + Set displayListeners = + getListenersFor(ListenerDataType.DISPLAY_PREFERENCES); - for (TrackDataListener listener : listeners) { + for (TrackDataListener listener : displayListeners) { // TODO: Do the reloading just once for all interested listeners if (listener.onReportSpeedChanged(reportSpeed)) { - reloadDataForListener(registeredListeners.get(listener)); + reloadDataForListener(listeners.getRegistration(listener)); } } } @@ -828,11 +573,11 @@ public class TrackDataHub { runInListenerThread(new Runnable() { @Override public void run() { - Set listeners = getListenersFor(ListenerDataType.DISPLAY_PREFERENCES); + Set displayListeners = getListenersFor(ListenerDataType.DISPLAY_PREFERENCES); - for (TrackDataListener listener : listeners) { + for (TrackDataListener listener : displayListeners) { if (listener.onUnitsChanged(useMetricUnits)) { - reloadDataForListener(registeredListeners.get(listener)); + reloadDataForListener(listeners.getRegistration(listener)); } } } @@ -860,7 +605,8 @@ public class TrackDataHub { public void run() { // Notify to everyone. Log.d(TAG, "Notifying fix type: " + state); - for (TrackDataListener listener : getListenersFor(ListenerDataType.LOCATION_UPDATES)) { + for (TrackDataListener listener : + getListenersFor(ListenerDataType.LOCATION_UPDATES)) { listener.onProviderStateChange(state); } } @@ -1094,8 +840,10 @@ public class TrackDataHub { firstSeenLocationId = -1; lastSeenLocationId = -1; numLoadedPoints = 0; - sampledListeners = getListenersFor(ListenerDataType.POINT_UPDATES); - sampledOutListeners = getListenersFor(ListenerDataType.SAMPLED_OUT_POINT_UPDATES); + synchronized (listeners) { + sampledListeners = getListenersFor(ListenerDataType.POINT_UPDATES); + sampledOutListeners = getListenersFor(ListenerDataType.SAMPLED_OUT_POINT_UPDATES); + } maxPointId = -1; minPointId = 0; keepState = true; @@ -1209,14 +957,23 @@ public class TrackDataHub { } } - private Set getListenersFor(ListenerDataType type) { - synchronized (registeredListeners) { - return listenerSetsPerType.get(type); - } - } - // @VisibleForTesting protected void runInListenerThread(Runnable runnable) { listenerHandler.post(runnable); } + + private Set getListenersFor(ListenerDataType type) { + synchronized (listeners) { + return listeners.getListenersFor(type); + } + } + + private EnumSet getNeededListenerTypes() { + EnumSet neededTypes = listeners.getAllRegisteredTypes(); + + // We always want preference updates. + neededTypes.add(ListenerDataType.DISPLAY_PREFERENCES); + + return neededTypes; + } } 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 62f63f8cd..be71e785e 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/content/TrackDataListener.java +++ b/MyTracks/src/com/google/android/apps/mytracks/content/TrackDataListener.java @@ -23,8 +23,6 @@ import android.location.Location; /** * Listener for track data, for both initial and incremental loading. * - * TODO: Allow listening to just part of the data received here. - * * @author Rodrigo Damazio */ public interface TrackDataListener { diff --git a/MyTracks/src/com/google/android/apps/mytracks/content/TrackDataListeners.java b/MyTracks/src/com/google/android/apps/mytracks/content/TrackDataListeners.java new file mode 100644 index 000000000..9fb6c0c1a --- /dev/null +++ b/MyTracks/src/com/google/android/apps/mytracks/content/TrackDataListeners.java @@ -0,0 +1,135 @@ +/* + * 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 static com.google.android.apps.mytracks.Constants.TAG; + +import com.google.android.apps.mytracks.content.TrackDataHub.ListenerDataType; + +import android.util.Log; + +import java.util.EnumMap; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; + +/** + * Manager for the external data listeners and their listening types. + * + * @author Rodrigo Damazio + */ +class TrackDataListeners { + + /** Internal representation of a listener's registration. */ + static class ListenerRegistration { + final TrackDataListener listener; + final EnumSet types; + // TODO: Add the last-notified point ID here, to allow pausing/resuming. + + public ListenerRegistration(TrackDataListener listener, + EnumSet types) { + this.listener = listener; + this.types = types; + } + + public boolean isInterestedIn(ListenerDataType type) { + return types.contains(type); + } + } + + /** Map of external listener to its registration details. */ + private final Map registeredListeners = + new HashMap(); + + /** Map of data type to external listeners interested in it. */ + private final Map> listenerSetsPerType = + new EnumMap>(ListenerDataType.class); + + public TrackDataListeners() { + // Create sets for all data types at startup. + for (ListenerDataType type : ListenerDataType.values()) { + listenerSetsPerType.put(type, new LinkedHashSet()); + } + } + + /** + * Registers a listener to send data to. + * 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 listener the listener to register + * @param dataTypes the type of data that the listener is interested in + */ + public ListenerRegistration registerTrackDataListener(final TrackDataListener listener, EnumSet dataTypes) { + Log.d(TAG, "Registered track data listener: " + listener); + ListenerRegistration registration = new ListenerRegistration(listener, dataTypes); + + if (registeredListeners.get(listener) != null) { + throw new IllegalStateException("Listener already registered"); + } + registeredListeners.put(listener, registration); + + for (ListenerDataType type : dataTypes) { + // This is guaranteed not to be null. + Set typeSet = listenerSetsPerType.get(type); + typeSet.add(listener); + } + + return registration; + } + + /** + * Unregisters a listener to send data to. + * + * @param listener the listener to unregister + */ + public void unregisterTrackDataListener(TrackDataListener listener) { + Log.d(TAG, "Unregistered track data listener: " + listener); + // Remove and keep the corresponding registration. + ListenerRegistration match = registeredListeners.remove(listener); + if (match == null) { + Log.w(TAG, "Tried to unregister listener which is not registered."); + return; + } + + // Remove it from the per-type sets + for (ListenerDataType type : match.types) { + listenerSetsPerType.get(type).remove(listener); + } + } + + public ListenerRegistration getRegistration(TrackDataListener listener) { + return registeredListeners.get(listener); + } + + public Set getListenersFor(ListenerDataType type) { + return listenerSetsPerType.get(type); + } + + public EnumSet getAllRegisteredTypes() { + EnumSet listeners = EnumSet.noneOf(ListenerDataType.class); + for (ListenerRegistration registration : this.registeredListeners.values()) { + listeners.addAll(registration.types); + } + return listeners; + } + + public boolean hasListeners() { + return !registeredListeners.isEmpty(); + } +} 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 3806f291f..991caf5ec 100644 --- a/MyTracksTest/src/com/google/android/apps/mytracks/content/TrackDataHubTest.java +++ b/MyTracksTest/src/com/google/android/apps/mytracks/content/TrackDataHubTest.java @@ -28,7 +28,7 @@ import com.google.android.apps.mytracks.content.MyTracksProviderUtils; 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.TrackDataSources; +import com.google.android.apps.mytracks.content.DataSourcesWrapper; import com.google.android.apps.mytracks.content.TracksColumns; import com.google.android.apps.mytracks.content.Waypoint; import com.google.android.apps.mytracks.content.WaypointsColumns; @@ -69,7 +69,8 @@ public class TrackDataHubTest extends AndroidTestCase { private static final long TRACK_ID = 42; private MyTracksProviderUtils providerUtils; private TrackDataHub hub; - private TrackDataSources dataSources; + private TrackDataListeners listeners; + private DataSourcesWrapper dataSources; private SharedPreferences prefs; private TrackDataListener listener1; private TrackDataListener listener2; @@ -89,9 +90,10 @@ public class TrackDataHubTest extends AndroidTestCase { prefs = context.getSharedPreferences(Constants.SETTINGS_NAME, 0); providerUtils = AndroidMock.createMock("providerUtils", MyTracksProviderUtils.class); - dataSources = AndroidMock.createNiceMock("dataSources", TrackDataSources.class); + dataSources = AndroidMock.createNiceMock("dataSources", DataSourcesWrapper.class); - hub = new TrackDataHub(context, dataSources, prefs, providerUtils) { + listeners = new TrackDataListeners(); + hub = new TrackDataHub(context, dataSources, listeners, prefs, providerUtils) { @Override protected void runInListenerThread(Runnable runnable) { // Run everything in the same thread.