forked from upstream-mirrors/OpenTracks
First step of data flow refactoring.
This moves all data source connections into the TrackDataHub. There are still many things to (re)do here: - Optimizations about when the listeners are registered - Optimizations about what data each TrackDataListener is interested in - When activities are restarting, send new data since they were stopped (instead of doing a full reload) - Proper double-buffering of Location objects (some listeners are interested in double-buffering all points, while others are only interested in double-buffering sampled points) - etc.
This commit is contained in:
@@ -0,0 +1,988 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
import static com.google.android.apps.mytracks.MyTracksConstants.TAG;
|
||||
|
||||
import com.google.android.apps.mytracks.TrackDataListener.ProviderState;
|
||||
import com.google.android.apps.mytracks.content.MyTracksLocation;
|
||||
import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
|
||||
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.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.services.TrackRecordingService;
|
||||
import com.google.android.apps.mytracks.util.ApiFeatures;
|
||||
import com.google.android.apps.mytracks.util.MyTracksUtils;
|
||||
import com.google.android.maps.mytracks.R;
|
||||
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
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.LinkedHashSet;
|
||||
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.
|
||||
*
|
||||
* @author Rodrigo Damazio
|
||||
*/
|
||||
public class TrackDataHub {
|
||||
|
||||
private static final long MAX_LOCATION_AGE_MS = 60 * 1000; // 1 minute
|
||||
private static final long MAX_NETWORK_AGE_MS = 1000 * 60 * 10; // 10 minutes
|
||||
|
||||
// Preference keys
|
||||
private final String SELECTED_TRACK_KEY;
|
||||
private final String RECORDING_TRACK_KEY;
|
||||
private final String MIN_REQUIRED_ACCURACY_KEY;
|
||||
private final String METRIC_UNITS_KEY;
|
||||
private final String SPEED_REPORTING_KEY;
|
||||
|
||||
// Application services
|
||||
private final Context context;
|
||||
private MyTracksProviderUtils providerUtils;
|
||||
|
||||
// System services
|
||||
private final SensorManager sensorManager;
|
||||
private final LocationManager locationManager;
|
||||
private final SharedPreferences sharedPreferences;
|
||||
private final ContentResolver contentResolver;
|
||||
|
||||
// Internal listeners (to receive data from the system)
|
||||
private ContentObserver pointObserver;
|
||||
private ContentObserver waypointObserver;
|
||||
private ContentObserver trackObserver;
|
||||
private LocationListener locationListener;
|
||||
private OnSharedPreferenceChangeListener preferenceListener;
|
||||
private SensorEventListener compassListener;
|
||||
|
||||
// External listeners (to pass data to activities)
|
||||
private final Set<TrackDataListener> registeredListeners =
|
||||
new LinkedHashSet<TrackDataListener>();
|
||||
|
||||
// Get content notifications on the main thread, send listener callbacks in another.
|
||||
// This ensures listener calls are serialized.
|
||||
private HandlerThread listenerHandlerThread;
|
||||
private Handler listenerHandler;
|
||||
private boolean started;
|
||||
|
||||
// Cached preference values
|
||||
private int minRequiredAccuracy;
|
||||
private boolean useMetricUnits;
|
||||
private boolean reportSpeed;
|
||||
|
||||
// Cached sensor readings
|
||||
private float declination;
|
||||
private long lastDeclinationUpdate;
|
||||
private float lastSeenMagneticHeading;
|
||||
private Location lastSeenLocation;
|
||||
private boolean hasFix;
|
||||
private boolean hasGoodFix;
|
||||
|
||||
// Transient state about the selected track
|
||||
private long firstSeenLocationId;
|
||||
private long lastSeenLocationId;
|
||||
private long selectedTrackId;
|
||||
private long recordingTrackId;
|
||||
private int numLoadedPoints;
|
||||
private boolean hasProviderEnabled;
|
||||
|
||||
/**
|
||||
* 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:
|
||||
listenerHandler.post(callback);
|
||||
}
|
||||
}
|
||||
|
||||
public TrackDataHub(Context ctx, MyTracksProviderUtils providerUtils) {
|
||||
this.context = ctx;
|
||||
this.providerUtils = providerUtils;
|
||||
|
||||
SELECTED_TRACK_KEY = context.getString(R.string.selected_track_key);
|
||||
RECORDING_TRACK_KEY = context.getString(R.string.recording_track_key);
|
||||
MIN_REQUIRED_ACCURACY_KEY = context.getString(R.string.min_required_accuracy_key);
|
||||
METRIC_UNITS_KEY = context.getString(R.string.metric_units_key);
|
||||
SPEED_REPORTING_KEY = context.getString(R.string.report_speed_key);
|
||||
|
||||
listenerHandlerThread = new HandlerThread("trackDataContentThread");
|
||||
listenerHandlerThread.start();
|
||||
listenerHandler = new Handler(listenerHandlerThread.getLooper());
|
||||
|
||||
sharedPreferences = ctx.getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0);
|
||||
preferenceListener = new OnSharedPreferenceChangeListener() {
|
||||
@Override
|
||||
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences,
|
||||
String key) {
|
||||
notifyPreferenceChanged(key);
|
||||
}
|
||||
};
|
||||
|
||||
sensorManager = (SensorManager) ctx.getSystemService(Context.SENSOR_SERVICE);
|
||||
locationManager =
|
||||
(LocationManager) ctx.getSystemService(Context.LOCATION_SERVICE);
|
||||
|
||||
contentResolver = ctx.getContentResolver();
|
||||
Handler contentHandler = new Handler();
|
||||
pointObserver = new TrackContentObserver(contentHandler,
|
||||
new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
notifyPointsUpdated(true, getRegisteredListenerArray());
|
||||
}
|
||||
});
|
||||
waypointObserver = new TrackContentObserver(contentHandler,
|
||||
new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
notifyWaypointUpdated(getRegisteredListenerArray());
|
||||
}
|
||||
});
|
||||
trackObserver = new TrackContentObserver(contentHandler,
|
||||
new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
notifyTrackUpdated(getRegisteredListenerArray());
|
||||
}
|
||||
});
|
||||
compassListener = new SensorEventListener() {
|
||||
@Override
|
||||
public void onSensorChanged(SensorEvent event) {
|
||||
lastSeenMagneticHeading = event.values[0];
|
||||
maybeUpdateDeclination();
|
||||
notifyHeadingChanged(getRegisteredListenerArray());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAccuracyChanged(Sensor sensor, int accuracy) {
|
||||
// Do nothing
|
||||
}
|
||||
};
|
||||
locationListener = new 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);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts listening to data sources and reporting the data to external
|
||||
* listeners.
|
||||
*/
|
||||
public void start() {
|
||||
Log.i(TAG, "TrackDataHub.start");
|
||||
if (started) {
|
||||
Log.w(TAG, "Already started, ignoring");
|
||||
return;
|
||||
}
|
||||
started = true;
|
||||
|
||||
sharedPreferences.registerOnSharedPreferenceChangeListener(preferenceListener);
|
||||
selectedTrackId = sharedPreferences.getLong(context.getString(R.string.selected_track_key), -1);
|
||||
recordingTrackId = sharedPreferences.getLong(context.getString(R.string.recording_track_key), -1);
|
||||
useMetricUnits = sharedPreferences.getBoolean(METRIC_UNITS_KEY, true);
|
||||
reportSpeed = sharedPreferences.getBoolean(SPEED_REPORTING_KEY, true);
|
||||
minRequiredAccuracy = sharedPreferences.getInt(RECORDING_TRACK_KEY,
|
||||
MyTracksSettings.DEFAULT_MIN_REQUIRED_ACCURACY);
|
||||
|
||||
if (recordingTrackId > 0) {
|
||||
Intent startIntent = new Intent(context, TrackRecordingService.class);
|
||||
context.startService(startIntent);
|
||||
}
|
||||
|
||||
// This may or may not register internal listeners, depending on whether
|
||||
// we already had external listeners.
|
||||
updateInternalListeners();
|
||||
|
||||
// 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).
|
||||
reloadDataFor(getRegisteredListenerArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops listening to data sources and reporting the data to external
|
||||
* listeners.
|
||||
*/
|
||||
public void stop() {
|
||||
Log.i(TAG, "TrackDataHub.stop");
|
||||
if (!started) {
|
||||
Log.w(TAG, "Not started, ignoring");
|
||||
return;
|
||||
}
|
||||
|
||||
sharedPreferences.unregisterOnSharedPreferenceChangeListener(preferenceListener);
|
||||
|
||||
// Unregister internal listeners even if there are external listeners registered.
|
||||
unregisterInternalListeners();
|
||||
|
||||
started = false;
|
||||
}
|
||||
|
||||
/** Permanently invalidates and throws away all resources used by this class. */
|
||||
public void destroy() {
|
||||
if (started) {
|
||||
throw new IllegalStateException("Can only destroy the data hub after it's been stopped");
|
||||
}
|
||||
|
||||
listenerHandlerThread.quit();
|
||||
}
|
||||
|
||||
/** Updates known magnetic declination if needed. */
|
||||
private void maybeUpdateDeclination() {
|
||||
if (lastSeenLocation == null) {
|
||||
// We still don't know where we are.
|
||||
return;
|
||||
}
|
||||
|
||||
// Update the variation 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;
|
||||
}
|
||||
|
||||
GeomagneticField field = new GeomagneticField(
|
||||
(float) lastSeenLocation.getLatitude(),
|
||||
(float) lastSeenLocation.getLongitude(),
|
||||
(float) lastSeenLocation.getAltitude(),
|
||||
timestamp);
|
||||
declination = 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.
|
||||
*/
|
||||
public void forceUpdateLocation() {
|
||||
checkStarted();
|
||||
|
||||
// TODO: Let's look at more advanced algorithms to determine the best
|
||||
// current location.
|
||||
if (locationManager == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final long now = System.currentTimeMillis();
|
||||
Location loc = locationManager.getLastKnownLocation(
|
||||
MyTracksConstants.GPS_PROVIDER);
|
||||
if (loc == null || loc.getTime() < now - MAX_LOCATION_AGE_MS) {
|
||||
// We don't have a recent GPS fix, just use cell towers if available
|
||||
loc = locationManager.getLastKnownLocation(
|
||||
LocationManager.NETWORK_PROVIDER);
|
||||
if (loc == null || loc.getTime() < now - MAX_NETWORK_AGE_MS) {
|
||||
// We don't have a recent cell tower location, let the user know:
|
||||
Toast.makeText(context, context.getString(R.string.status_no_location),
|
||||
Toast.LENGTH_LONG).show();
|
||||
return;
|
||||
} else {
|
||||
// Let the user know we have only an approximate location:
|
||||
Toast.makeText(context, context.getString(R.string.status_approximate_location),
|
||||
Toast.LENGTH_LONG).show();
|
||||
}
|
||||
}
|
||||
|
||||
notifyLocationChanged(loc, getRegisteredListenerArray());
|
||||
}
|
||||
|
||||
/** Returns the ID of the currently-selected track. */
|
||||
public long getSelectedTrackId() {
|
||||
checkStarted();
|
||||
return selectedTrackId;
|
||||
}
|
||||
|
||||
/** Returns whether there's a track currently selected. */
|
||||
public boolean isATrackSelected() {
|
||||
checkStarted();
|
||||
return selectedTrackId > 0;
|
||||
}
|
||||
|
||||
/** Returns whether we're currently recording a track. */
|
||||
public boolean isRecording() {
|
||||
checkStarted();
|
||||
return recordingTrackId > 0;
|
||||
}
|
||||
|
||||
/** Returns whether the selected track is still being recorded. */
|
||||
public boolean isRecordingSelected() {
|
||||
checkStarted();
|
||||
return isRecording() && 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 flash.
|
||||
ApiFeatures.getInstance().getApiPlatformAdapter().applyPreferenceChanges(
|
||||
sharedPreferences.edit().putLong(SELECTED_TRACK_KEY, trackId));
|
||||
selectedTrackId = trackId;
|
||||
|
||||
// Force it to reload data from the beginning.
|
||||
firstSeenLocationId = -1;
|
||||
lastSeenLocationId = -1;
|
||||
numLoadedPoints = 0;
|
||||
|
||||
reloadDataFor(getRegisteredListenerArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* Unloads the currently-selected track.
|
||||
*/
|
||||
public void unloadCurrentTrack() {
|
||||
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
|
||||
*/
|
||||
public void registerTrackDataListener(final TrackDataListener listener) {
|
||||
Log.d(TAG, "Registered track data listener: " + listener);
|
||||
synchronized (registeredListeners) {
|
||||
registeredListeners.add(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;
|
||||
|
||||
reloadDataFor(listener);
|
||||
}
|
||||
|
||||
updateInternalListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
registeredListeners.remove(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;
|
||||
}
|
||||
updateInternalListeners();
|
||||
}
|
||||
|
||||
/** Updates the internal (sensor, position, etc) listeners. */
|
||||
private void updateInternalListeners() {
|
||||
boolean hasListeners;
|
||||
synchronized (registeredListeners) {
|
||||
hasListeners = registeredListeners.isEmpty();
|
||||
}
|
||||
|
||||
if (hasListeners) {
|
||||
unregisterInternalListeners();
|
||||
} else {
|
||||
registerInternalListeners();
|
||||
}
|
||||
}
|
||||
|
||||
/** Registers all internal (sensor, position, etc.) listeners. */
|
||||
private void registerInternalListeners() {
|
||||
// Listen to data provider
|
||||
contentResolver.registerContentObserver(
|
||||
TracksColumns.CONTENT_URI, false, trackObserver);
|
||||
contentResolver.registerContentObserver(
|
||||
WaypointsColumns.CONTENT_URI, false, waypointObserver);
|
||||
contentResolver.registerContentObserver(
|
||||
TrackPointsColumns.CONTENT_URI, false, pointObserver);
|
||||
|
||||
// Listen to compass
|
||||
Sensor compass = sensorManager.getDefaultSensor(Sensor.TYPE_ORIENTATION);
|
||||
if (compass != null) {
|
||||
Log.d(MyTracksConstants.TAG,
|
||||
"TrackDataHub: Now registering sensor listeners.");
|
||||
sensorManager.registerListener(compassListener, compass, SensorManager.SENSOR_DELAY_UI);
|
||||
}
|
||||
|
||||
// Listen to GPS
|
||||
LocationProvider gpsProvider =
|
||||
locationManager.getProvider(MyTracksConstants.GPS_PROVIDER);
|
||||
if (gpsProvider == null) {
|
||||
Toast.makeText(context, R.string.error_no_gps_location_provider, Toast.LENGTH_LONG).show();
|
||||
hasProviderEnabled = false;
|
||||
return;
|
||||
} else {
|
||||
Log.d(MyTracksConstants.TAG, "TrackDataHub: Using location provider "
|
||||
+ gpsProvider.getName());
|
||||
}
|
||||
locationManager.requestLocationUpdates(gpsProvider.getName(),
|
||||
0 /*minTime*/, 0 /*minDist*/, locationListener);
|
||||
hasProviderEnabled = locationManager.isProviderEnabled(MyTracksConstants.GPS_PROVIDER);
|
||||
|
||||
// Listen to network location
|
||||
try {
|
||||
locationManager.requestLocationUpdates(LocationManager.NETWORK_PROVIDER,
|
||||
1000 * 60 * 5 /*minTime*/, 0 /*minDist*/, locationListener);
|
||||
} catch (RuntimeException e) {
|
||||
// If anything at all goes wrong with getting a cell location do not
|
||||
// abort. Cell location is not essential to this app.
|
||||
Log.w(MyTracksConstants.TAG,
|
||||
"Could not register network location listener.");
|
||||
}
|
||||
}
|
||||
|
||||
/** Unregisters all internal (sensor, position, etc.) listeners. */
|
||||
private void unregisterInternalListeners() {
|
||||
locationManager.removeUpdates(locationListener);
|
||||
sensorManager.unregisterListener(compassListener);
|
||||
contentResolver.unregisterContentObserver(trackObserver);
|
||||
contentResolver.unregisterContentObserver(waypointObserver);
|
||||
contentResolver.unregisterContentObserver(pointObserver);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reloads all track data received so far into the specified listeners.
|
||||
*/
|
||||
public void reloadDataFor(final TrackDataListener... listeners) {
|
||||
if (listeners.length == 0) {
|
||||
Log.d(TAG, "No listeners, not reloading");
|
||||
return;
|
||||
}
|
||||
if (!started) {
|
||||
Log.w(TAG, "Not started, not reloading");
|
||||
return;
|
||||
}
|
||||
|
||||
listenerHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
Track track = providerUtils.getTrack(selectedTrackId);
|
||||
|
||||
// Ignore the return values here, we're already sending the full data set anyway
|
||||
for (TrackDataListener listener : listeners) {
|
||||
listener.onUnitsChanged(useMetricUnits);
|
||||
listener.onReportSpeedChanged(reportSpeed);
|
||||
}
|
||||
|
||||
notifySelectedTrackChanged(track, listeners);
|
||||
notifyPointsCleared(listeners);
|
||||
notifyPointsUpdated(false, listeners);
|
||||
notifyWaypointUpdated(listeners);
|
||||
|
||||
if (lastSeenLocation != null) {
|
||||
notifyLocationChanged(lastSeenLocation, true, listeners);
|
||||
} else {
|
||||
notifyFixType();
|
||||
}
|
||||
|
||||
notifyHeadingChanged(listeners);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a preference changes.
|
||||
*
|
||||
* @param key the key to the preference that changed
|
||||
*/
|
||||
private void notifyPreferenceChanged(String key) {
|
||||
if (key.equals(RECORDING_TRACK_KEY)) {
|
||||
recordingTrackId = sharedPreferences.getLong(RECORDING_TRACK_KEY, -1);
|
||||
} else if (key.equals(MIN_REQUIRED_ACCURACY_KEY)) {
|
||||
minRequiredAccuracy = sharedPreferences.getInt(RECORDING_TRACK_KEY,
|
||||
MyTracksSettings.DEFAULT_MIN_REQUIRED_ACCURACY);
|
||||
} else if (key.equals(METRIC_UNITS_KEY)) {
|
||||
useMetricUnits = sharedPreferences.getBoolean(METRIC_UNITS_KEY, true);
|
||||
notifyUnitsChanged();
|
||||
} else if (key.equals(SPEED_REPORTING_KEY)) {
|
||||
reportSpeed = sharedPreferences.getBoolean(SPEED_REPORTING_KEY, true);
|
||||
notifySpeedReportingChanged();
|
||||
}
|
||||
}
|
||||
|
||||
/** Called when the speed/pace reporting preference changes. */
|
||||
private void notifySpeedReportingChanged() {
|
||||
listenerHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
TrackDataListener[] listeners = getRegisteredListenerArray();
|
||||
|
||||
boolean reloadData = false;
|
||||
for (TrackDataListener listener : listeners) {
|
||||
reloadData |= listener.onReportSpeedChanged(reportSpeed);
|
||||
}
|
||||
|
||||
if (reloadData) {
|
||||
reloadDataFor(listeners);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** Called when the metric units setting changes. */
|
||||
private void notifyUnitsChanged() {
|
||||
listenerHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
TrackDataListener[] listeners = getRegisteredListenerArray();
|
||||
|
||||
boolean reloadData = false;
|
||||
for (TrackDataListener listener : listeners) {
|
||||
reloadData |= listener.onUnitsChanged(useMetricUnits);
|
||||
}
|
||||
|
||||
if (reloadData) {
|
||||
reloadDataFor(listeners);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** 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;
|
||||
}
|
||||
|
||||
listenerHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
for (TrackDataListener listener : registeredListeners) {
|
||||
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, TrackDataListener... 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(final Location location, boolean forceUpdate, final TrackDataListener... listeners) {
|
||||
if (location == null) return;
|
||||
|
||||
boolean isGpsLocation = location.getProvider().equals(LocationManager.GPS_PROVIDER);
|
||||
|
||||
boolean oldHasFix = hasFix;
|
||||
boolean oldHasGoodFix = hasGoodFix;
|
||||
|
||||
// We consider a good fix to be a recent one with reasonable accuracy.
|
||||
if (isGpsLocation) {
|
||||
lastSeenLocation = location;
|
||||
hasFix = (location != null && System.currentTimeMillis() - location.getTime() <= MAX_LOCATION_AGE_MS);
|
||||
hasGoodFix = (location != null && location.getAccuracy() <= minRequiredAccuracy);
|
||||
if (hasFix != oldHasFix || hasGoodFix != oldHasGoodFix || forceUpdate) {
|
||||
notifyFixType();
|
||||
}
|
||||
}
|
||||
|
||||
listenerHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
for (TrackDataListener listener : listeners) {
|
||||
listener.onCurrentLocationChanged(location);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies that the current heading has changed.
|
||||
*
|
||||
* @param listeners the listeners to notify
|
||||
*/
|
||||
private void notifyHeadingChanged(final TrackDataListener... listeners) {
|
||||
listenerHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
for (TrackDataListener listener : listeners) {
|
||||
listener.onCurrentHeadingChanged(lastSeenMagneticHeading + declination);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies that a new track has been selected..
|
||||
*
|
||||
* @param track the new selected track
|
||||
* @param listeners the listeners to notify
|
||||
*/
|
||||
private void notifySelectedTrackChanged(final Track track, final TrackDataListener... listeners) {
|
||||
Log.d(TAG, "New track selected, id=" + (track != null ? track.getId() : "none"));
|
||||
|
||||
listenerHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
for (TrackDataListener listener : listeners) {
|
||||
listener.onSelectedTrackChanged(track, isRecordingSelected());
|
||||
|
||||
if (track != null) {
|
||||
listener.onTrackUpdated(track);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies that the currently-selected track's data has been updated.
|
||||
*
|
||||
* @param listeners the listeners to notify
|
||||
*/
|
||||
private void notifyTrackUpdated(final TrackDataListener... listeners) {
|
||||
final Track track = providerUtils.getTrack(selectedTrackId);
|
||||
|
||||
listenerHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
for (TrackDataListener listener : registeredListeners) {
|
||||
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 TrackDataListener... listeners) {
|
||||
// Always reload all the waypoints.
|
||||
final Cursor cursor = providerUtils.getWaypointsCursor(
|
||||
selectedTrackId, 0, MyTracksConstants.MAX_DISPLAYED_WAYPOINTS_POINTS);
|
||||
|
||||
listenerHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
for (TrackDataListener listener : registeredListeners) {
|
||||
listener.clearWaypoints();
|
||||
}
|
||||
|
||||
try {
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
do {
|
||||
Waypoint waypoint = providerUtils.createWaypoint(cursor);
|
||||
if (!MyTracksUtils.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 TrackDataListener... listeners) {
|
||||
listenerHandler.post(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 minPointId the first point ID to notify, inclusive
|
||||
* @param maxPointId the last poind ID to notify, inclusive
|
||||
* @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 store.
|
||||
* @param listeners the listeners to notify
|
||||
*/
|
||||
private void notifyPointsUpdated(final boolean keepState, final TrackDataListener... listeners) {
|
||||
listenerHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
notifyPointsUpdatedSync(keepState, listeners);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Asynchronous version of the above method.
|
||||
*/
|
||||
private void notifyPointsUpdatedSync(boolean keepState, TrackDataListener[] listeners) {
|
||||
// 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.
|
||||
long minPointId = keepState ? lastSeenLocationId + 1 : 0;
|
||||
long maxPointId = keepState ? -1 : lastSeenLocationId;
|
||||
|
||||
if (numLoadedPoints >= MyTracksConstants.MAX_DISPLAYED_TRACK_POINTS) {
|
||||
// We're about to exceed the maximum allowed 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?).
|
||||
|
||||
firstSeenLocationId = -1;
|
||||
lastSeenLocationId = -1;
|
||||
numLoadedPoints = 0;
|
||||
listeners = getRegisteredListenerArray();
|
||||
maxPointId = -1;
|
||||
minPointId = 0;
|
||||
keepState = true;
|
||||
|
||||
for (TrackDataListener listener : listeners) {
|
||||
listener.clearTrackPoints();
|
||||
}
|
||||
}
|
||||
|
||||
// Keep the originally selected track ID so we can stop if it changes.
|
||||
long currentSelectedTrackId = selectedTrackId;
|
||||
|
||||
// If we're ignoring state, start from the beginning of the track
|
||||
int localNumLoadedPoints = keepState ? numLoadedPoints : 0;
|
||||
long localFirstSeenLocationId = keepState ? firstSeenLocationId : 0;
|
||||
long localLastSeenLocationId = minPointId;
|
||||
long lastStoredLocationId = providerUtils.getLastLocationId(currentSelectedTrackId);
|
||||
int pointSamplingFrequency = -1;
|
||||
|
||||
// Create a double-buffering location provider.
|
||||
LocationIterator it = providerUtils.getLocationIterator(
|
||||
currentSelectedTrackId, minPointId, false, new LocationFactory() {
|
||||
private final Location locs[] = new MyTracksLocation[] {
|
||||
new MyTracksLocation("gps"),
|
||||
new MyTracksLocation("gps")
|
||||
};
|
||||
|
||||
private int lastLoc = 0;
|
||||
|
||||
@Override
|
||||
public Location createLocation() {
|
||||
lastLoc = (lastLoc + 1) % locs.length;
|
||||
return locs[lastLoc];
|
||||
}
|
||||
});
|
||||
|
||||
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) {
|
||||
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.
|
||||
long numTotalPoints = lastStoredLocationId - localFirstSeenLocationId;
|
||||
pointSamplingFrequency =
|
||||
(int) (1 + numTotalPoints / MyTracksConstants.TARGET_DISPLAYED_TRACK_POINTS);
|
||||
}
|
||||
|
||||
// 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 isValid = MyTracksUtils.isValidLocation(location);
|
||||
if (isValid &&
|
||||
(localNumLoadedPoints % pointSamplingFrequency == 0 ||
|
||||
(!isRecordingSelected() && locationId == lastStoredLocationId))) {
|
||||
// No need to allocate a new location (we can safely reuse the existing).
|
||||
for (TrackDataListener listener : listeners) {
|
||||
listener.onNewTrackPoint(location);
|
||||
}
|
||||
}
|
||||
|
||||
// Report segment splits separately.
|
||||
if (!isValid) {
|
||||
// TODO: Always send last valid point before and first valid point after a split
|
||||
for (TrackDataListener listener : listeners) {
|
||||
listener.onSegmentSplit();
|
||||
}
|
||||
}
|
||||
|
||||
localNumLoadedPoints++;
|
||||
localLastSeenLocationId = locationId;
|
||||
}
|
||||
it.close();
|
||||
|
||||
if (keepState) {
|
||||
numLoadedPoints = localNumLoadedPoints;
|
||||
firstSeenLocationId = localFirstSeenLocationId;
|
||||
lastSeenLocationId = localLastSeenLocationId;
|
||||
}
|
||||
|
||||
for (TrackDataListener listener : listeners) {
|
||||
listener.onNewTrackPointsDone();
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns an array with all the currently-registered listeners. */
|
||||
private TrackDataListener[] getRegisteredListenerArray() {
|
||||
synchronized (registeredListeners) {
|
||||
TrackDataListener listenerArray[] = new TrackDataListener[registeredListeners.size()];
|
||||
return registeredListeners.toArray(listenerArray);
|
||||
}
|
||||
}
|
||||
|
||||
/** Verifies that {@link #start} has been called. */
|
||||
private void checkStarted() {
|
||||
if (!started) {
|
||||
throw new IllegalStateException("TrackDataHub has not been started.");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user