forked from upstream-mirrors/OpenTracks
1015 lines
34 KiB
Java
1015 lines
34 KiB
Java
/*
|
|
* 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.Constants.TAG;
|
|
|
|
import com.google.android.apps.mytracks.TrackDataListener.ProviderState;
|
|
import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
|
|
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.util.ApiFeatures;
|
|
import com.google.android.apps.mytracks.util.LocationUtils;
|
|
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 final 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 final ContentObserver pointObserver;
|
|
private final ContentObserver waypointObserver;
|
|
private final ContentObserver trackObserver;
|
|
private final LocationListener locationListener;
|
|
private final OnSharedPreferenceChangeListener preferenceListener;
|
|
private final 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 final HandlerThread listenerHandlerThread;
|
|
private final 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;
|
|
|
|
/** Callback for when the tracks table is updated. */
|
|
private class TrackObserverCallback implements Runnable {
|
|
@Override
|
|
public void run() {
|
|
notifyTrackUpdated(getRegisteredListenerArray());
|
|
}
|
|
}
|
|
|
|
/** Callback for when the waypoints table is updated. */
|
|
private class WaypointObserverCallback implements Runnable {
|
|
@Override
|
|
public void run() {
|
|
notifyWaypointUpdated(getRegisteredListenerArray());
|
|
}
|
|
}
|
|
|
|
/** Callback for when the points table is updated. */
|
|
private class PointObserverCallback implements Runnable {
|
|
@Override
|
|
public void run() {
|
|
notifyPointsUpdated(true, getRegisteredListenerArray());
|
|
}
|
|
}
|
|
|
|
/** 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:
|
|
listenerHandler.post(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);
|
|
}
|
|
}
|
|
|
|
/** Listener for compass readings. */
|
|
private class CompassListener implements
|
|
SensorEventListener {
|
|
@Override
|
|
public void onSensorChanged(SensorEvent event) {
|
|
lastSeenMagneticHeading = event.values[0];
|
|
maybeUpdateDeclination();
|
|
notifyHeadingChanged(getRegisteredListenerArray());
|
|
}
|
|
|
|
@Override
|
|
public void onAccuracyChanged(Sensor sensor, int accuracy) {
|
|
// Do nothing
|
|
}
|
|
}
|
|
|
|
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(Constants.SETTINGS_NAME, 0);
|
|
preferenceListener = new HubSharedPreferenceListener();
|
|
|
|
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 PointObserverCallback());
|
|
waypointObserver = new TrackContentObserver(contentHandler, new WaypointObserverCallback());
|
|
trackObserver = new TrackContentObserver(contentHandler, new TrackObserverCallback());
|
|
|
|
compassListener = new CompassListener();
|
|
locationListener = new CurrentLocationListener();
|
|
}
|
|
|
|
/**
|
|
* 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);
|
|
loadSharedPreferences();
|
|
|
|
// 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());
|
|
}
|
|
|
|
private void loadSharedPreferences() {
|
|
selectedTrackId = sharedPreferences.getLong(SELECTED_TRACK_KEY, -1);
|
|
recordingTrackId = sharedPreferences.getLong(RECORDING_TRACK_KEY, -1);
|
|
useMetricUnits = sharedPreferences.getBoolean(METRIC_UNITS_KEY, true);
|
|
reportSpeed = sharedPreferences.getBoolean(SPEED_REPORTING_KEY, true);
|
|
minRequiredAccuracy = sharedPreferences.getInt(MIN_REQUIRED_ACCURACY_KEY,
|
|
Constants.DEFAULT_MIN_REQUIRED_ACCURACY);
|
|
}
|
|
|
|
/**
|
|
* 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(
|
|
Constants.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() {
|
|
if (!started) {
|
|
loadSharedPreferences();
|
|
}
|
|
return selectedTrackId;
|
|
}
|
|
|
|
/** Returns whether there's a track currently selected. */
|
|
public boolean isATrackSelected() {
|
|
return getSelectedTrackId() > 0;
|
|
}
|
|
|
|
/** Returns whether we're currently recording a track. */
|
|
public boolean isRecording() {
|
|
if (!started) {
|
|
loadSharedPreferences();
|
|
}
|
|
return recordingTrackId > 0;
|
|
}
|
|
|
|
/** Returns whether the selected track is still being recorded. */
|
|
public boolean isRecordingSelected() {
|
|
if (!started) {
|
|
loadSharedPreferences();
|
|
}
|
|
return recordingTrackId > 0 && 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(Constants.TAG,
|
|
"TrackDataHub: Now registering sensor listeners.");
|
|
sensorManager.registerListener(compassListener, compass, SensorManager.SENSOR_DELAY_UI);
|
|
}
|
|
|
|
// Listen to GPS
|
|
LocationProvider gpsProvider =
|
|
locationManager.getProvider(Constants.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(Constants.TAG, "TrackDataHub: Using location provider "
|
|
+ gpsProvider.getName());
|
|
}
|
|
locationManager.requestLocationUpdates(gpsProvider.getName(),
|
|
0 /*minTime*/, 0 /*minDist*/, locationListener);
|
|
hasProviderEnabled = locationManager.isProviderEnabled(Constants.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(Constants.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(MIN_REQUIRED_ACCURACY_KEY,
|
|
Constants.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, Constants.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 (!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 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 >= Constants.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.
|
|
MyTracksProviderUtils.DoubleBufferedLocationFactory locationFactory =
|
|
new MyTracksProviderUtils.DoubleBufferedLocationFactory();
|
|
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) {
|
|
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 / Constants.TARGET_DISPLAYED_TRACK_POINTS);
|
|
}
|
|
|
|
notifyNewPoint(location, locationId, lastStoredLocationId,
|
|
localNumLoadedPoints, pointSamplingFrequency, listeners);
|
|
|
|
localNumLoadedPoints++;
|
|
localLastSeenLocationId = locationId;
|
|
}
|
|
it.close();
|
|
|
|
if (keepState) {
|
|
numLoadedPoints = localNumLoadedPoints;
|
|
firstSeenLocationId = localFirstSeenLocationId;
|
|
lastSeenLocationId = localLastSeenLocationId;
|
|
}
|
|
|
|
for (TrackDataListener listener : listeners) {
|
|
listener.onNewTrackPointsDone();
|
|
}
|
|
}
|
|
|
|
private void notifyNewPoint(Location location,
|
|
long locationId,
|
|
long lastStoredLocationId,
|
|
int numLoadedPoints,
|
|
int pointSamplingFrequency,
|
|
TrackDataListener[] listeners) {
|
|
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 : listeners) {
|
|
listener.onSegmentSplit();
|
|
}
|
|
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 includeInSample =
|
|
(numLoadedPoints % pointSamplingFrequency == 0 ||
|
|
(!isRecordingSelected() && locationId == lastStoredLocationId));
|
|
|
|
if (!includeInSample) {
|
|
for (TrackDataListener listener : listeners) {
|
|
listener.onSampledOutTrackPoint(location);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Point is valid and included in sample.
|
|
for (TrackDataListener listener : listeners) {
|
|
// No need to allocate a new location (we can safely reuse the existing).
|
|
listener.onNewTrackPoint(location);
|
|
}
|
|
}
|
|
|
|
/** 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.");
|
|
}
|
|
}
|
|
}
|