forked from upstream-mirrors/OpenTracks
Standardizing the creator string across exported file formats.
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Copyright 2010 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;
|
||||
|
||||
/**
|
||||
* Utilities for serializing primitive types.
|
||||
*
|
||||
* @author Rodrigo Damazio
|
||||
*/
|
||||
public class ContentTypeIds {
|
||||
public static final byte BOOLEAN_TYPE_ID = 0;
|
||||
public static final byte LONG_TYPE_ID = 1;
|
||||
public static final byte INT_TYPE_ID = 2;
|
||||
public static final byte FLOAT_TYPE_ID = 3;
|
||||
public static final byte DOUBLE_TYPE_ID = 4;
|
||||
public static final byte STRING_TYPE_ID = 5;
|
||||
public static final byte BLOB_TYPE_ID = 6;
|
||||
|
||||
private ContentTypeIds() { /* Not instantiable */ }
|
||||
}
|
||||
@@ -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<ListenerDataType> 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<ListenerDataType> externallyNeededListeners) {
|
||||
EnumSet<ListenerDataType> 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<ListenerDataType> unnecessaryListeners = EnumSet.copyOf(registeredListeners);
|
||||
unnecessaryListeners.removeAll(neededListeners);
|
||||
|
||||
// Missing = needed - registered
|
||||
Set<ListenerDataType> 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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* 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 android.content.SharedPreferences.OnSharedPreferenceChangeListener;
|
||||
import android.database.ContentObserver;
|
||||
import android.hardware.Sensor;
|
||||
import android.hardware.SensorEventListener;
|
||||
import android.location.Location;
|
||||
import android.location.LocationListener;
|
||||
import android.net.Uri;
|
||||
|
||||
/**
|
||||
* Interface for abstracting registration of external data source listeners.
|
||||
*
|
||||
* @author Rodrigo Damazio
|
||||
*/
|
||||
interface DataSourcesWrapper {
|
||||
// Preferences
|
||||
void registerOnSharedPreferenceChangeListener(
|
||||
OnSharedPreferenceChangeListener listener);
|
||||
void unregisterOnSharedPreferenceChangeListener(
|
||||
OnSharedPreferenceChangeListener listener);
|
||||
|
||||
// Content provider
|
||||
void registerContentObserver(Uri contentUri, boolean descendents,
|
||||
ContentObserver observer);
|
||||
void unregisterContentObserver(ContentObserver observer);
|
||||
|
||||
// Sensors
|
||||
Sensor getSensor(int type);
|
||||
void registerSensorListener(SensorEventListener listener,
|
||||
Sensor sensor, int sensorDelay);
|
||||
void unregisterSensorListener(SensorEventListener listener);
|
||||
|
||||
// Location
|
||||
boolean isLocationProviderEnabled(String provider);
|
||||
void requestLocationUpdates(LocationListener listener);
|
||||
void removeLocationUpdates(LocationListener listener);
|
||||
Location getLastKnownLocation();
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
/*
|
||||
* 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.MAX_LOCATION_AGE_MS;
|
||||
import static com.google.android.apps.mytracks.Constants.MAX_NETWORK_AGE_MS;
|
||||
|
||||
import com.google.android.apps.mytracks.Constants;
|
||||
import com.google.android.maps.mytracks.R;
|
||||
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
|
||||
import android.database.ContentObserver;
|
||||
import android.hardware.Sensor;
|
||||
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.net.Uri;
|
||||
import android.util.Log;
|
||||
import android.widget.Toast;
|
||||
|
||||
/**
|
||||
* Real implementation of the data sources, which talks to system services.
|
||||
*
|
||||
* @author Rodrigo Damazio
|
||||
*/
|
||||
class DataSourcesWrapperImpl implements DataSourcesWrapper {
|
||||
// System services
|
||||
private final SensorManager sensorManager;
|
||||
private final LocationManager locationManager;
|
||||
private final ContentResolver contentResolver;
|
||||
private final SharedPreferences sharedPreferences;
|
||||
private final Context context;
|
||||
|
||||
DataSourcesWrapperImpl(Context context, SharedPreferences sharedPreferences) {
|
||||
this.context = context;
|
||||
this.sensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE);
|
||||
this.locationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE);
|
||||
this.contentResolver = context.getContentResolver();
|
||||
this.sharedPreferences = sharedPreferences;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerOnSharedPreferenceChangeListener(
|
||||
OnSharedPreferenceChangeListener listener) {
|
||||
sharedPreferences.registerOnSharedPreferenceChangeListener(listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unregisterOnSharedPreferenceChangeListener(
|
||||
OnSharedPreferenceChangeListener listener) {
|
||||
sharedPreferences.unregisterOnSharedPreferenceChangeListener(listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerContentObserver(Uri contentUri, boolean descendents,
|
||||
ContentObserver observer) {
|
||||
contentResolver.registerContentObserver(contentUri, descendents, observer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unregisterContentObserver(ContentObserver observer) {
|
||||
contentResolver.unregisterContentObserver(observer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Sensor getSensor(int type) {
|
||||
return sensorManager.getDefaultSensor(type);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerSensorListener(SensorEventListener listener,
|
||||
Sensor sensor, int sensorDelay) {
|
||||
sensorManager.registerListener(listener, sensor, sensorDelay);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unregisterSensorListener(SensorEventListener listener) {
|
||||
sensorManager.unregisterListener(listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isLocationProviderEnabled(String provider) {
|
||||
return locationManager.isProviderEnabled(provider);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void requestLocationUpdates(LocationListener listener) {
|
||||
// Check if the provider exists.
|
||||
LocationProvider gpsProvider = locationManager.getProvider(LocationManager.GPS_PROVIDER);
|
||||
if (gpsProvider == null) {
|
||||
listener.onProviderDisabled(LocationManager.GPS_PROVIDER);
|
||||
locationManager.removeUpdates(listener);
|
||||
return;
|
||||
}
|
||||
|
||||
// Listen to GPS location.
|
||||
String providerName = gpsProvider.getName();
|
||||
Log.d(Constants.TAG, "TrackDataHub: Using location provider " + providerName);
|
||||
locationManager.requestLocationUpdates(providerName,
|
||||
0 /*minTime*/, 0 /*minDist*/, listener);
|
||||
|
||||
// Give an initial update on provider state.
|
||||
if (locationManager.isProviderEnabled(providerName)) {
|
||||
listener.onProviderEnabled(providerName);
|
||||
} else {
|
||||
listener.onProviderDisabled(providerName);
|
||||
}
|
||||
|
||||
// Listen to network location
|
||||
try {
|
||||
locationManager.requestLocationUpdates(LocationManager.NETWORK_PROVIDER,
|
||||
1000 * 60 * 5 /*minTime*/, 0 /*minDist*/, listener);
|
||||
} 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.", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Location getLastKnownLocation() {
|
||||
// TODO: Let's look at more advanced algorithms to determine the best
|
||||
// current location.
|
||||
|
||||
Location loc = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER);
|
||||
final long now = System.currentTimeMillis();
|
||||
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);
|
||||
|
||||
int toastResId = R.string.status_approximate_location;
|
||||
if (loc == null || loc.getTime() < now - MAX_NETWORK_AGE_MS) {
|
||||
// We don't have a recent cell tower location, let the user know:
|
||||
toastResId = R.string.status_no_location;
|
||||
}
|
||||
|
||||
// Let the user know we have only an approximate location:
|
||||
Toast.makeText(context, context.getString(toastResId), Toast.LENGTH_LONG).show();
|
||||
}
|
||||
return loc;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeLocationUpdates(LocationListener listener) {
|
||||
locationManager.removeUpdates(listener);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,434 @@
|
||||
/*
|
||||
* Copyright 2008 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 com.google.android.apps.mytracks.Constants;
|
||||
import com.google.android.apps.mytracks.util.ApiFeatures;
|
||||
|
||||
import android.content.ContentProvider;
|
||||
import android.content.ContentUris;
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.content.UriMatcher;
|
||||
import android.database.Cursor;
|
||||
import android.database.SQLException;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.database.sqlite.SQLiteException;
|
||||
import android.database.sqlite.SQLiteOpenHelper;
|
||||
import android.database.sqlite.SQLiteQueryBuilder;
|
||||
import android.net.Uri;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
/**
|
||||
* A provider that handles recorded (GPS) tracks and their track points.
|
||||
*
|
||||
* @author Leif Hendrik Wilden
|
||||
*/
|
||||
public class MyTracksProvider extends ContentProvider {
|
||||
|
||||
private static final String DATABASE_NAME = "mytracks.db";
|
||||
private static final int DATABASE_VERSION = 19;
|
||||
private static final int TRACKPOINTS = 1;
|
||||
private static final int TRACKPOINTS_ID = 2;
|
||||
private static final int TRACKS = 3;
|
||||
private static final int TRACKS_ID = 4;
|
||||
private static final int WAYPOINTS = 5;
|
||||
private static final int WAYPOINTS_ID = 6;
|
||||
private static final String TRACKPOINTS_TABLE = "trackpoints";
|
||||
private static final String TRACKS_TABLE = "tracks";
|
||||
private static final String WAYPOINTS_TABLE = "waypoints";
|
||||
public static final String TAG = "MyTracksProvider";
|
||||
|
||||
/**
|
||||
* Helper which creates or upgrades the database if necessary.
|
||||
*/
|
||||
private static class DatabaseHelper extends SQLiteOpenHelper {
|
||||
|
||||
public DatabaseHelper(Context context) {
|
||||
super(context, DATABASE_NAME, null, DATABASE_VERSION);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(SQLiteDatabase db) {
|
||||
db.execSQL("CREATE TABLE " + TRACKPOINTS_TABLE + " ("
|
||||
+ TrackPointsColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT, "
|
||||
+ TrackPointsColumns.TRACKID + " INTEGER, "
|
||||
+ TrackPointsColumns.LONGITUDE + " INTEGER, "
|
||||
+ TrackPointsColumns.LATITUDE + " INTEGER, "
|
||||
+ TrackPointsColumns.TIME + " INTEGER, "
|
||||
+ TrackPointsColumns.ALTITUDE + " FLOAT, "
|
||||
+ TrackPointsColumns.ACCURACY + " FLOAT, "
|
||||
+ TrackPointsColumns.SPEED + " FLOAT, "
|
||||
+ TrackPointsColumns.BEARING + " FLOAT, "
|
||||
+ TrackPointsColumns.SENSOR + " BLOB);");
|
||||
db.execSQL("CREATE TABLE " + TRACKS_TABLE + " ("
|
||||
+ TracksColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT, "
|
||||
+ TracksColumns.NAME + " STRING, "
|
||||
+ TracksColumns.DESCRIPTION + " STRING, "
|
||||
+ TracksColumns.CATEGORY + " STRING, "
|
||||
+ TracksColumns.STARTID + " INTEGER, "
|
||||
+ TracksColumns.STOPID + " INTEGER, "
|
||||
+ TracksColumns.STARTTIME + " INTEGER, "
|
||||
+ TracksColumns.STOPTIME + " INTEGER, "
|
||||
+ TracksColumns.NUMPOINTS + " INTEGER, "
|
||||
+ TracksColumns.TOTALDISTANCE + " FLOAT, "
|
||||
+ TracksColumns.TOTALTIME + " INTEGER, "
|
||||
+ TracksColumns.MOVINGTIME + " INTEGER, "
|
||||
+ TracksColumns.MINLAT + " INTEGER, "
|
||||
+ TracksColumns.MAXLAT + " INTEGER, "
|
||||
+ TracksColumns.MINLON + " INTEGER, "
|
||||
+ TracksColumns.MAXLON + " INTEGER, "
|
||||
+ TracksColumns.AVGSPEED + " FLOAT, "
|
||||
+ TracksColumns.AVGMOVINGSPEED + " FLOAT, "
|
||||
+ TracksColumns.MAXSPEED + " FLOAT, "
|
||||
+ TracksColumns.MINELEVATION + " FLOAT, "
|
||||
+ TracksColumns.MAXELEVATION + " FLOAT, "
|
||||
+ TracksColumns.ELEVATIONGAIN + " FLOAT, "
|
||||
+ TracksColumns.MINGRADE + " FLOAT, "
|
||||
+ TracksColumns.MAXGRADE + " FLOAT, "
|
||||
+ TracksColumns.MAPID + " STRING, "
|
||||
+ TracksColumns.TABLEID + " STRING);");
|
||||
db.execSQL("CREATE TABLE " + WAYPOINTS_TABLE + " ("
|
||||
+ WaypointsColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT, "
|
||||
+ WaypointsColumns.NAME + " STRING, "
|
||||
+ WaypointsColumns.DESCRIPTION + " STRING, "
|
||||
+ WaypointsColumns.CATEGORY + " STRING, "
|
||||
+ WaypointsColumns.ICON + " STRING, "
|
||||
+ WaypointsColumns.TRACKID + " INTEGER, "
|
||||
+ WaypointsColumns.TYPE + " INTEGER, "
|
||||
+ WaypointsColumns.LENGTH + " FLOAT, "
|
||||
+ WaypointsColumns.DURATION + " INTEGER, "
|
||||
+ WaypointsColumns.STARTTIME + " INTEGER, "
|
||||
+ WaypointsColumns.STARTID + " INTEGER, "
|
||||
+ WaypointsColumns.STOPID + " INTEGER, "
|
||||
+ WaypointsColumns.LONGITUDE + " INTEGER, "
|
||||
+ WaypointsColumns.LATITUDE + " INTEGER, "
|
||||
+ WaypointsColumns.TIME + " INTEGER, "
|
||||
+ WaypointsColumns.ALTITUDE + " FLOAT, "
|
||||
+ WaypointsColumns.ACCURACY + " FLOAT, "
|
||||
+ WaypointsColumns.SPEED + " FLOAT, "
|
||||
+ WaypointsColumns.BEARING + " FLOAT, "
|
||||
+ WaypointsColumns.TOTALDISTANCE + " FLOAT, "
|
||||
+ WaypointsColumns.TOTALTIME + " INTEGER, "
|
||||
+ WaypointsColumns.MOVINGTIME + " INTEGER, "
|
||||
+ WaypointsColumns.AVGSPEED + " FLOAT, "
|
||||
+ WaypointsColumns.AVGMOVINGSPEED + " FLOAT, "
|
||||
+ WaypointsColumns.MAXSPEED + " FLOAT, "
|
||||
+ WaypointsColumns.MINELEVATION + " FLOAT, "
|
||||
+ WaypointsColumns.MAXELEVATION + " FLOAT, "
|
||||
+ WaypointsColumns.ELEVATIONGAIN + " FLOAT, "
|
||||
+ WaypointsColumns.MINGRADE + " FLOAT, "
|
||||
+ WaypointsColumns.MAXGRADE + " FLOAT);");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
|
||||
if (oldVersion < 17) {
|
||||
// Wipe the old data.
|
||||
Log.w(TAG, "Upgrading database from version " + oldVersion + " to "
|
||||
+ newVersion + ", which will destroy all old data");
|
||||
db.execSQL("DROP TABLE IF EXISTS " + TRACKPOINTS_TABLE);
|
||||
db.execSQL("DROP TABLE IF EXISTS " + TRACKS_TABLE);
|
||||
db.execSQL("DROP TABLE IF EXISTS " + WAYPOINTS_TABLE);
|
||||
onCreate(db);
|
||||
} else {
|
||||
// Incremental updates go here.
|
||||
// Each time you increase the DB version, add a corresponding if clause.
|
||||
Log.w(TAG, "Upgrading database from version " + oldVersion + " to "
|
||||
+ newVersion);
|
||||
|
||||
// Sensor data.
|
||||
if (oldVersion <= 17) {
|
||||
Log.w(TAG, "Upgrade DB: Adding sensor column.");
|
||||
db.execSQL("ALTER TABLE " + TRACKPOINTS_TABLE
|
||||
+ " ADD " + TrackPointsColumns.SENSOR + " BLOB");
|
||||
}
|
||||
if (oldVersion <= 18) {
|
||||
Log.w(TAG, "Upgrade DB: Adding tableid column.");
|
||||
db.execSQL("ALTER TABLE " + TRACKS_TABLE
|
||||
+ " ADD " + TracksColumns.TABLEID + " STRING");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final UriMatcher urlMatcher;
|
||||
|
||||
private SQLiteDatabase db;
|
||||
|
||||
public MyTracksProvider() {
|
||||
urlMatcher = new UriMatcher(UriMatcher.NO_MATCH);
|
||||
urlMatcher.addURI(MyTracksProviderUtils.AUTHORITY,
|
||||
"trackpoints", TRACKPOINTS);
|
||||
urlMatcher.addURI(MyTracksProviderUtils.AUTHORITY,
|
||||
"trackpoints/#", TRACKPOINTS_ID);
|
||||
urlMatcher.addURI(MyTracksProviderUtils.AUTHORITY, "tracks", TRACKS);
|
||||
urlMatcher.addURI(MyTracksProviderUtils.AUTHORITY, "tracks/#", TRACKS_ID);
|
||||
urlMatcher.addURI(MyTracksProviderUtils.AUTHORITY, "waypoints", WAYPOINTS);
|
||||
urlMatcher.addURI(MyTracksProviderUtils.AUTHORITY,
|
||||
"waypoints/#", WAYPOINTS_ID);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreate() {
|
||||
DatabaseHelper dbHelper = new DatabaseHelper(getContext());
|
||||
db = dbHelper.getWritableDatabase();
|
||||
return db != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int delete(Uri url, String where, String[] selectionArgs) {
|
||||
String table;
|
||||
boolean shouldVacuum = false;
|
||||
switch (urlMatcher.match(url)) {
|
||||
case TRACKPOINTS:
|
||||
table = TRACKPOINTS_TABLE;
|
||||
break;
|
||||
case TRACKS:
|
||||
table = TRACKS_TABLE;
|
||||
shouldVacuum = true;
|
||||
break;
|
||||
case WAYPOINTS:
|
||||
table = WAYPOINTS_TABLE;
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("Unknown URL " + url);
|
||||
}
|
||||
|
||||
Log.w(MyTracksProvider.TAG, "provider delete in " + table + "!");
|
||||
int count = db.delete(table, where, selectionArgs);
|
||||
getContext().getContentResolver().notifyChange(url, null, true);
|
||||
|
||||
if (shouldVacuum) {
|
||||
// If a potentially large amount of data was deleted, we want to reclaim its space.
|
||||
Log.i(TAG, "Vacuuming the database");
|
||||
db.execSQL("VACUUM");
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getType(Uri url) {
|
||||
switch (urlMatcher.match(url)) {
|
||||
case TRACKPOINTS:
|
||||
return TrackPointsColumns.CONTENT_TYPE;
|
||||
case TRACKPOINTS_ID:
|
||||
return TrackPointsColumns.CONTENT_ITEMTYPE;
|
||||
case TRACKS:
|
||||
return TracksColumns.CONTENT_TYPE;
|
||||
case TRACKS_ID:
|
||||
return TracksColumns.CONTENT_ITEMTYPE;
|
||||
case WAYPOINTS:
|
||||
return WaypointsColumns.CONTENT_TYPE;
|
||||
case WAYPOINTS_ID:
|
||||
return WaypointsColumns.CONTENT_ITEMTYPE;
|
||||
default:
|
||||
throw new IllegalArgumentException("Unknown URL " + url);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Uri insert(Uri url, ContentValues initialValues) {
|
||||
Log.d(MyTracksProvider.TAG, "MyTracksProvider.insert");
|
||||
ContentValues values;
|
||||
if (initialValues != null) {
|
||||
values = initialValues;
|
||||
} else {
|
||||
values = new ContentValues();
|
||||
}
|
||||
|
||||
int urlMatchType = urlMatcher.match(url);
|
||||
return insertType(url, urlMatchType, values);
|
||||
}
|
||||
|
||||
private Uri insertType(Uri url, int urlMatchType, ContentValues values) {
|
||||
switch (urlMatchType) {
|
||||
case TRACKPOINTS:
|
||||
return insertTrackPoint(url, values);
|
||||
case TRACKS:
|
||||
return insertTrack(url, values);
|
||||
case WAYPOINTS:
|
||||
return insertWaypoint(url, values);
|
||||
default:
|
||||
throw new IllegalArgumentException("Unknown URL " + url);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public int bulkInsert(Uri url, ContentValues[] valuesBulk) {
|
||||
Log.d(MyTracksProvider.TAG, "MyTracksProvider.bulkInsert");
|
||||
int numInserted = 0;
|
||||
try {
|
||||
// Use a transaction in order to make the insertions run as a single batch
|
||||
db.beginTransaction();
|
||||
|
||||
int urlMatch = urlMatcher.match(url);
|
||||
for (numInserted = 0; numInserted < valuesBulk.length; numInserted++) {
|
||||
ContentValues values = valuesBulk[numInserted];
|
||||
if (values == null) { values = new ContentValues(); }
|
||||
|
||||
insertType(url, urlMatch, values);
|
||||
}
|
||||
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
|
||||
return numInserted;
|
||||
}
|
||||
|
||||
private Uri insertTrackPoint(Uri url, ContentValues values) {
|
||||
boolean hasLat = values.containsKey(TrackPointsColumns.LATITUDE);
|
||||
boolean hasLong = values.containsKey(TrackPointsColumns.LONGITUDE);
|
||||
boolean hasTime = values.containsKey(TrackPointsColumns.TIME);
|
||||
if (!hasLat || !hasLong || !hasTime) {
|
||||
throw new IllegalArgumentException(
|
||||
"Latitude, longitude, and time values are required.");
|
||||
}
|
||||
long rowId = db.insert(TRACKPOINTS_TABLE, TrackPointsColumns._ID, values);
|
||||
if (rowId >= 0) {
|
||||
Uri uri = ContentUris.appendId(
|
||||
TrackPointsColumns.CONTENT_URI.buildUpon(), rowId).build();
|
||||
getContext().getContentResolver().notifyChange(url, null, true);
|
||||
return uri;
|
||||
}
|
||||
throw new SQLiteException("Failed to insert row into " + url);
|
||||
}
|
||||
|
||||
private Uri insertTrack(Uri url, ContentValues values) {
|
||||
boolean hasStartTime = values.containsKey(TracksColumns.STARTTIME);
|
||||
boolean hasStartId = values.containsKey(TracksColumns.STARTID);
|
||||
if (!hasStartTime || !hasStartId) {
|
||||
throw new IllegalArgumentException(
|
||||
"Both start time and start id values are required.");
|
||||
}
|
||||
long rowId = db.insert(TRACKS_TABLE, TracksColumns._ID, values);
|
||||
if (rowId > 0) {
|
||||
Uri uri = ContentUris.appendId(
|
||||
TracksColumns.CONTENT_URI.buildUpon(), rowId).build();
|
||||
getContext().getContentResolver().notifyChange(url, null, true);
|
||||
return uri;
|
||||
}
|
||||
throw new SQLException("Failed to insert row into " + url);
|
||||
}
|
||||
|
||||
private Uri insertWaypoint(Uri url, ContentValues values) {
|
||||
long rowId = db.insert(WAYPOINTS_TABLE, WaypointsColumns._ID, values);
|
||||
if (rowId > 0) {
|
||||
Uri uri = ContentUris.appendId(
|
||||
WaypointsColumns.CONTENT_URI.buildUpon(), rowId).build();
|
||||
getContext().getContentResolver().notifyChange(url, null, true);
|
||||
return uri;
|
||||
}
|
||||
throw new SQLException("Failed to insert row into " + url);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Cursor query(
|
||||
Uri url, String[] projection, String selection, String[] selectionArgs,
|
||||
String sort) {
|
||||
SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
|
||||
int match = urlMatcher.match(url);
|
||||
String sortOrder = null;
|
||||
if (match == TRACKPOINTS) {
|
||||
qb.setTables(TRACKPOINTS_TABLE);
|
||||
if (sort != null) {
|
||||
sortOrder = sort;
|
||||
} else {
|
||||
sortOrder = TrackPointsColumns.DEFAULT_SORT_ORDER;
|
||||
}
|
||||
} else if (match == TRACKPOINTS_ID) {
|
||||
qb.setTables(TRACKPOINTS_TABLE);
|
||||
qb.appendWhere("_id=" + url.getPathSegments().get(1));
|
||||
} else if (match == TRACKS) {
|
||||
qb.setTables(TRACKS_TABLE);
|
||||
if (sort != null) {
|
||||
sortOrder = sort;
|
||||
} else {
|
||||
sortOrder = TracksColumns.DEFAULT_SORT_ORDER;
|
||||
}
|
||||
} else if (match == TRACKS_ID) {
|
||||
qb.setTables(TRACKS_TABLE);
|
||||
qb.appendWhere("_id=" + url.getPathSegments().get(1));
|
||||
} else if (match == WAYPOINTS) {
|
||||
qb.setTables(WAYPOINTS_TABLE);
|
||||
if (sort != null) {
|
||||
sortOrder = sort;
|
||||
} else {
|
||||
sortOrder = WaypointsColumns.DEFAULT_SORT_ORDER;
|
||||
}
|
||||
} else if (match == WAYPOINTS_ID) {
|
||||
qb.setTables(WAYPOINTS_TABLE);
|
||||
qb.appendWhere("_id=" + url.getPathSegments().get(1));
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unknown URL " + url);
|
||||
}
|
||||
|
||||
if (ApiFeatures.getInstance().canReuseSQLiteQueryBuilder()) {
|
||||
Log.i(Constants.TAG,
|
||||
"Build query: " + qb.buildQuery(projection, selection, selectionArgs,
|
||||
null, null, sortOrder, null));
|
||||
}
|
||||
Cursor c = qb.query(db, projection, selection, selectionArgs, null, null,
|
||||
sortOrder);
|
||||
c.setNotificationUri(getContext().getContentResolver(), url);
|
||||
return c;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int update(Uri url, ContentValues values, String where,
|
||||
String[] selectionArgs) {
|
||||
int count;
|
||||
int match = urlMatcher.match(url);
|
||||
if (match == TRACKPOINTS) {
|
||||
count = db.update(TRACKPOINTS_TABLE, values, where, selectionArgs);
|
||||
} else if (match == TRACKPOINTS_ID) {
|
||||
String segment = url.getPathSegments().get(1);
|
||||
count = db.update(TRACKPOINTS_TABLE, values, "_id=" + segment
|
||||
+ (!TextUtils.isEmpty(where)
|
||||
? " AND (" + where + ')'
|
||||
: ""),
|
||||
selectionArgs);
|
||||
} else if (match == TRACKS) {
|
||||
count = db.update(TRACKS_TABLE, values, where, selectionArgs);
|
||||
} else if (match == TRACKS_ID) {
|
||||
String segment = url.getPathSegments().get(1);
|
||||
count = db.update(TRACKS_TABLE, values, "_id=" + segment
|
||||
+ (!TextUtils.isEmpty(where)
|
||||
? " AND (" + where + ')'
|
||||
: ""),
|
||||
selectionArgs);
|
||||
} else if (match == WAYPOINTS) {
|
||||
count = db.update(WAYPOINTS_TABLE, values, where, selectionArgs);
|
||||
} else if (match == WAYPOINTS_ID) {
|
||||
String segment = url.getPathSegments().get(1);
|
||||
count = db.update(WAYPOINTS_TABLE, values, "_id=" + segment
|
||||
+ (!TextUtils.isEmpty(where)
|
||||
? " AND (" + where + ')'
|
||||
: ""),
|
||||
selectionArgs);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unknown URL " + url);
|
||||
}
|
||||
getContext().getContentResolver().notifyChange(url, null, true);
|
||||
return count;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,979 @@
|
||||
/*
|
||||
* 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.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.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.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.database.Cursor;
|
||||
import android.hardware.GeomagneticField;
|
||||
import android.location.Location;
|
||||
import android.location.LocationManager;
|
||||
import android.os.Handler;
|
||||
import android.os.HandlerThread;
|
||||
import android.util.Log;
|
||||
import android.widget.Toast;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.EnumSet;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Track data hub, which receives data (both live and recorded) from many
|
||||
* different sources and distributes it to those interested after some standard
|
||||
* processing.
|
||||
*
|
||||
* @author Rodrigo Damazio
|
||||
*/
|
||||
public class TrackDataHub {
|
||||
|
||||
// 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;
|
||||
|
||||
/** Types of data that we can expose. */
|
||||
public static enum ListenerDataType {
|
||||
/** Listen to when the selected track changes. */
|
||||
SELECTED_TRACK_CHANGED,
|
||||
|
||||
/** Listen to when the tracks change. */
|
||||
TRACK_UPDATES,
|
||||
|
||||
/** Listen to when the waypoints change. */
|
||||
WAYPOINT_UPDATES,
|
||||
|
||||
/** Listen to when the current track points change. */
|
||||
POINT_UPDATES,
|
||||
|
||||
/**
|
||||
* Listen to sampled-out points.
|
||||
* Listening to this without listening to {@link #SAMPLED_POINT_UPDATES}
|
||||
* makes no sense and may yield unexpected results.
|
||||
*/
|
||||
SAMPLED_OUT_POINT_UPDATES,
|
||||
|
||||
/** Listen to updates to the current location. */
|
||||
LOCATION_UPDATES,
|
||||
|
||||
/** Listen to updates to the current heading. */
|
||||
COMPASS_UPDATES,
|
||||
|
||||
/** Listens to changes in display preferences. */
|
||||
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 MyTracksProviderUtils providerUtils;
|
||||
private final SharedPreferences preferences;
|
||||
|
||||
// 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;
|
||||
|
||||
// Cached preference values
|
||||
private int minRequiredAccuracy;
|
||||
private boolean useMetricUnits;
|
||||
private boolean reportSpeed;
|
||||
|
||||
// Cached sensor readings
|
||||
private float declination;
|
||||
private long lastDeclinationUpdate;
|
||||
private float lastSeenMagneticHeading;
|
||||
|
||||
// Cached GPS readings
|
||||
private Location lastSeenLocation;
|
||||
private boolean hasProviderEnabled;
|
||||
private boolean hasFix;
|
||||
private boolean hasGoodFix;
|
||||
|
||||
// Transient state about the selected track
|
||||
private long selectedTrackId;
|
||||
private long recordingTrackId;
|
||||
private long firstSeenLocationId;
|
||||
private long lastSeenLocationId;
|
||||
private int numLoadedPoints;
|
||||
|
||||
/**
|
||||
* Default constructor.
|
||||
*/
|
||||
public TrackDataHub(Context ctx, SharedPreferences preferences,
|
||||
MyTracksProviderUtils providerUtils) {
|
||||
this(ctx, new DataSourcesWrapperImpl(ctx, preferences), new TrackDataListeners(),
|
||||
preferences, providerUtils);
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection constructor.
|
||||
*/
|
||||
// @VisibleForTesting
|
||||
TrackDataHub(Context ctx, DataSourcesWrapper dataSources, TrackDataListeners listeners,
|
||||
SharedPreferences preferences, MyTracksProviderUtils providerUtils) {
|
||||
this.context = ctx;
|
||||
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);
|
||||
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());
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
// This may or may not register internal listeners, depending on whether
|
||||
// we already had external listeners.
|
||||
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).
|
||||
loadDataForAllListeners();
|
||||
}
|
||||
|
||||
private void loadSharedPreferences() {
|
||||
selectedTrackId = preferences.getLong(SELECTED_TRACK_KEY, -1);
|
||||
recordingTrackId = preferences.getLong(RECORDING_TRACK_KEY, -1);
|
||||
useMetricUnits = preferences.getBoolean(METRIC_UNITS_KEY, true);
|
||||
reportSpeed = preferences.getBoolean(SPEED_REPORTING_KEY, true);
|
||||
minRequiredAccuracy = preferences.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;
|
||||
}
|
||||
|
||||
// Unregister internal listeners even if there are external listeners registered.
|
||||
dataSourceManager.unregisterAllListeners();
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
ApiFeatures.getInstance().getApiPlatformAdapter()
|
||||
.stopHandlerThread(listenerHandlerThread);
|
||||
}
|
||||
|
||||
/** Updates known magnetic declination if needed. */
|
||||
private void maybeUpdateDeclination() {
|
||||
if (lastSeenLocation == null) {
|
||||
// We still don't know where we are.
|
||||
return;
|
||||
}
|
||||
|
||||
// Update the declination every hour
|
||||
long now = System.currentTimeMillis();
|
||||
if (now - lastDeclinationUpdate < 60 * 60 * 1000) {
|
||||
return;
|
||||
}
|
||||
|
||||
lastDeclinationUpdate = now;
|
||||
long timestamp = lastSeenLocation.getTime();
|
||||
if (timestamp == 0) {
|
||||
// Hack for Samsung phones which don't populate the time field
|
||||
timestamp = now;
|
||||
}
|
||||
|
||||
declination = getDeclinationFor(lastSeenLocation, timestamp);
|
||||
Log.i(TAG, "Updated magnetic declination to " + declination);
|
||||
}
|
||||
|
||||
// @VisibleForTesting
|
||||
protected float getDeclinationFor(Location location, long timestamp) {
|
||||
GeomagneticField field = new GeomagneticField(
|
||||
(float) location.getLatitude(),
|
||||
(float) location.getLongitude(),
|
||||
(float) location.getAltitude(),
|
||||
timestamp);
|
||||
return field.getDeclination();
|
||||
}
|
||||
|
||||
/**
|
||||
* Forces the current location to be updated and reported to all listeners.
|
||||
* The reported location may be from the network provider if the GPS provider
|
||||
* is not available or doesn't have a fix.
|
||||
*/
|
||||
public void forceUpdateLocation() {
|
||||
Log.i(TAG, "Forcing location update");
|
||||
|
||||
Location loc = dataSources.getLastKnownLocation();
|
||||
if (loc != null) {
|
||||
notifyLocationChanged(loc, getListenersFor(ListenerDataType.LOCATION_UPDATES));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/** 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(
|
||||
preferences.edit().putLong(SELECTED_TRACK_KEY, trackId));
|
||||
selectedTrackId = trackId;
|
||||
|
||||
// Force it to reload data from the beginning.
|
||||
firstSeenLocationId = -1;
|
||||
lastSeenLocationId = -1;
|
||||
numLoadedPoints = 0;
|
||||
|
||||
loadDataForAllListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
* Unloads the currently-selected track.
|
||||
*/
|
||||
public void unloadCurrentTrack() {
|
||||
loadTrack(-1);
|
||||
}
|
||||
|
||||
public void registerTrackDataListener(
|
||||
TrackDataListener listener, EnumSet<ListenerDataType> 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);
|
||||
|
||||
dataSourceManager.updateAllListeners(getNeededListenerTypes());
|
||||
}
|
||||
}
|
||||
|
||||
public void unregisterTrackDataListener(TrackDataListener 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());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reloads all track data received so far into the specified listeners.
|
||||
*/
|
||||
public void reloadDataForListener(TrackDataListener listener) {
|
||||
reloadDataForListener(listeners.getRegistration(listener));
|
||||
}
|
||||
|
||||
/**
|
||||
* Reloads all track data received so far into the specified listeners.
|
||||
*/
|
||||
private void reloadDataForListener(final ListenerRegistration registration) {
|
||||
if (!started) {
|
||||
Log.w(TAG, "Not started, not reloading");
|
||||
return;
|
||||
}
|
||||
|
||||
runInListenerThread(new Runnable() {
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public void run() {
|
||||
TrackDataListener listener = registration.listener;
|
||||
Set<TrackDataListener> listenerSet = Collections.singleton(listener);
|
||||
|
||||
if (registration.isInterestedIn(ListenerDataType.DISPLAY_PREFERENCES)) {
|
||||
// Ignore the return values here, we're already sending the full data set anyway
|
||||
listener.onUnitsChanged(useMetricUnits);
|
||||
listener.onReportSpeedChanged(reportSpeed);
|
||||
}
|
||||
|
||||
if (registration.isInterestedIn(ListenerDataType.SELECTED_TRACK_CHANGED)) {
|
||||
notifySelectedTrackChanged(selectedTrackId, listenerSet);
|
||||
}
|
||||
|
||||
if (registration.isInterestedIn(ListenerDataType.TRACK_UPDATES)) {
|
||||
notifyTrackUpdated(listenerSet);
|
||||
}
|
||||
|
||||
boolean interestedInPoints =
|
||||
registration.isInterestedIn(ListenerDataType.POINT_UPDATES);
|
||||
boolean interestedInSampledOutPoints =
|
||||
registration.isInterestedIn(ListenerDataType.SAMPLED_OUT_POINT_UPDATES);
|
||||
if (interestedInPoints || interestedInSampledOutPoints) {
|
||||
notifyPointsCleared(listenerSet);
|
||||
notifyPointsUpdated(false,
|
||||
listenerSet,
|
||||
interestedInSampledOutPoints ? listenerSet : Collections.EMPTY_SET);
|
||||
}
|
||||
|
||||
if (registration.isInterestedIn(ListenerDataType.WAYPOINT_UPDATES)) {
|
||||
notifyWaypointUpdated(listenerSet);
|
||||
}
|
||||
|
||||
if (registration.isInterestedIn(ListenerDataType.LOCATION_UPDATES)) {
|
||||
if (lastSeenLocation != null) {
|
||||
notifyLocationChanged(lastSeenLocation, true, listenerSet);
|
||||
} else {
|
||||
notifyFixType();
|
||||
}
|
||||
}
|
||||
|
||||
if (registration.isInterestedIn(ListenerDataType.COMPASS_UPDATES)) {
|
||||
notifyHeadingChanged(listenerSet);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reloads all track data received so far into the specified listeners.
|
||||
*/
|
||||
private void loadDataForAllListeners() {
|
||||
if (!listeners.hasListeners()) {
|
||||
Log.d(TAG, "No listeners, not reloading");
|
||||
return;
|
||||
}
|
||||
if (!started) {
|
||||
Log.w(TAG, "Not started, not reloading");
|
||||
return;
|
||||
}
|
||||
|
||||
runInListenerThread(new Runnable() {
|
||||
@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)) {
|
||||
listener.onUnitsChanged(useMetricUnits);
|
||||
listener.onReportSpeedChanged(reportSpeed);
|
||||
}
|
||||
|
||||
notifySelectedTrackChanged(selectedTrackId,
|
||||
getListenersFor(ListenerDataType.SELECTED_TRACK_CHANGED));
|
||||
|
||||
notifyTrackUpdated(getListenersFor(ListenerDataType.TRACK_UPDATES));
|
||||
|
||||
Set<TrackDataListener> pointListeners =
|
||||
getListenersFor(ListenerDataType.POINT_UPDATES);
|
||||
Set<TrackDataListener> sampledOutPointListeners =
|
||||
getListenersFor(ListenerDataType.SAMPLED_OUT_POINT_UPDATES);
|
||||
notifyPointsCleared(pointListeners);
|
||||
notifyPointsUpdated(true, pointListeners, sampledOutPointListeners);
|
||||
|
||||
notifyWaypointUpdated(getListenersFor(ListenerDataType.WAYPOINT_UPDATES));
|
||||
|
||||
if (lastSeenLocation != null) {
|
||||
notifyLocationChanged(lastSeenLocation, true,
|
||||
getListenersFor(ListenerDataType.LOCATION_UPDATES));
|
||||
} else {
|
||||
notifyFixType();
|
||||
}
|
||||
|
||||
notifyHeadingChanged(getListenersFor(ListenerDataType.COMPASS_UPDATES));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a preference changes.
|
||||
*
|
||||
* @param key the key to the preference that changed
|
||||
*/
|
||||
private void notifyPreferenceChanged(String key) {
|
||||
if (RECORDING_TRACK_KEY.equals(key)) {
|
||||
recordingTrackId = preferences.getLong(RECORDING_TRACK_KEY, -1);
|
||||
} else if (MIN_REQUIRED_ACCURACY_KEY.equals(key)) {
|
||||
minRequiredAccuracy = preferences.getInt(MIN_REQUIRED_ACCURACY_KEY,
|
||||
Constants.DEFAULT_MIN_REQUIRED_ACCURACY);
|
||||
} else if (METRIC_UNITS_KEY.equals(key)) {
|
||||
useMetricUnits = preferences.getBoolean(METRIC_UNITS_KEY, true);
|
||||
notifyUnitsChanged();
|
||||
} else if (SPEED_REPORTING_KEY.equals(key)) {
|
||||
reportSpeed = preferences.getBoolean(SPEED_REPORTING_KEY, true);
|
||||
notifySpeedReportingChanged();
|
||||
}
|
||||
}
|
||||
|
||||
/** Called when the speed/pace reporting preference changes. */
|
||||
private void notifySpeedReportingChanged() {
|
||||
if (!started) return;
|
||||
|
||||
runInListenerThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
Set<TrackDataListener> displayListeners =
|
||||
getListenersFor(ListenerDataType.DISPLAY_PREFERENCES);
|
||||
|
||||
for (TrackDataListener listener : displayListeners) {
|
||||
// TODO: Do the reloading just once for all interested listeners
|
||||
if (listener.onReportSpeedChanged(reportSpeed)) {
|
||||
reloadDataForListener(listeners.getRegistration(listener));
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** Called when the metric units setting changes. */
|
||||
private void notifyUnitsChanged() {
|
||||
if (!started) return;
|
||||
|
||||
runInListenerThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
Set<TrackDataListener> displayListeners = getListenersFor(ListenerDataType.DISPLAY_PREFERENCES);
|
||||
|
||||
for (TrackDataListener listener : displayListeners) {
|
||||
if (listener.onUnitsChanged(useMetricUnits)) {
|
||||
reloadDataForListener(listeners.getRegistration(listener));
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** Notifies about the current GPS fix state. */
|
||||
private void notifyFixType() {
|
||||
final TrackDataListener.ProviderState state;
|
||||
if (!hasProviderEnabled) {
|
||||
state = ProviderState.DISABLED;
|
||||
|
||||
// Give a global warning about this state.
|
||||
Toast.makeText(context, R.string.error_no_gps_location_provider, Toast.LENGTH_LONG).show();
|
||||
} else if (!hasFix) {
|
||||
state = ProviderState.NO_FIX;
|
||||
} else if (!hasGoodFix) {
|
||||
state = ProviderState.BAD_FIX;
|
||||
} else {
|
||||
state = ProviderState.GOOD_FIX;
|
||||
}
|
||||
|
||||
runInListenerThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
// Notify to everyone.
|
||||
Log.d(TAG, "Notifying fix type: " + state);
|
||||
for (TrackDataListener listener :
|
||||
getListenersFor(ListenerDataType.LOCATION_UPDATES)) {
|
||||
listener.onProviderStateChange(state);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies the the current location has changed, without any filtering.
|
||||
* If the state of GPS fix has changed, that will also be reported.
|
||||
*
|
||||
* @param location the current location
|
||||
* @param listeners the listeners to notify
|
||||
*/
|
||||
private void notifyLocationChanged(Location location, Set<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 Set<TrackDataListener> listeners) {
|
||||
if (location == null) return;
|
||||
if (listeners.isEmpty()) 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();
|
||||
}
|
||||
}
|
||||
|
||||
runInListenerThread(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 Set<TrackDataListener> listeners) {
|
||||
if (listeners.isEmpty()) return;
|
||||
|
||||
runInListenerThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
float heading = lastSeenMagneticHeading + declination;
|
||||
for (TrackDataListener listener : listeners) {
|
||||
listener.onCurrentHeadingChanged(heading);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies that a new track has been selected..
|
||||
*
|
||||
* @param track the new selected track
|
||||
* @param listeners the listeners to notify
|
||||
*/
|
||||
private void notifySelectedTrackChanged(long trackId,
|
||||
final Set<TrackDataListener> listeners) {
|
||||
if (listeners.isEmpty()) return;
|
||||
|
||||
Log.i(TAG, "New track selected, id=" + trackId);
|
||||
final Track track = providerUtils.getTrack(trackId);
|
||||
|
||||
runInListenerThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
for (TrackDataListener listener : listeners) {
|
||||
listener.onSelectedTrackChanged(track, isRecordingSelected());
|
||||
|
||||
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 Set<TrackDataListener> listeners) {
|
||||
if (listeners.isEmpty()) return;
|
||||
|
||||
final Track track = providerUtils.getTrack(selectedTrackId);
|
||||
|
||||
runInListenerThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
for (TrackDataListener listener : listeners) {
|
||||
listener.onTrackUpdated(track);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies that waypoints have been updated.
|
||||
* We assume few waypoints, so we reload them all every time.
|
||||
*
|
||||
* @param listeners the listeners to notify
|
||||
*/
|
||||
private void notifyWaypointUpdated(final Set<TrackDataListener> listeners) {
|
||||
if (listeners.isEmpty()) return;
|
||||
|
||||
// Always reload all the waypoints.
|
||||
final Cursor cursor = providerUtils.getWaypointsCursor(
|
||||
selectedTrackId, 0L, Constants.MAX_DISPLAYED_WAYPOINTS_POINTS);
|
||||
|
||||
runInListenerThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
Log.d(TAG, "Reloading waypoints");
|
||||
for (TrackDataListener listener : listeners) {
|
||||
listener.clearWaypoints();
|
||||
}
|
||||
|
||||
try {
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
do {
|
||||
Waypoint waypoint = providerUtils.createWaypoint(cursor);
|
||||
if (!LocationUtils.isValidLocation(waypoint.getLocation())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (TrackDataListener listener : listeners) {
|
||||
listener.onNewWaypoint(waypoint);
|
||||
}
|
||||
} while (cursor.moveToNext());
|
||||
}
|
||||
} finally {
|
||||
if (cursor != null) {
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
|
||||
for (TrackDataListener listener : listeners) {
|
||||
listener.onNewWaypointsDone();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells listeners to clear the current list of points.
|
||||
*
|
||||
* @param listeners the listeners to notify
|
||||
*/
|
||||
private void notifyPointsCleared(final Set<TrackDataListener> listeners) {
|
||||
if (listeners.isEmpty()) return;
|
||||
|
||||
runInListenerThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
for (TrackDataListener listener : listeners) {
|
||||
listener.clearTrackPoints();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies the given listeners about track points in the given ID range.
|
||||
*
|
||||
* @param 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
|
||||
* @param trackDataListeners
|
||||
*/
|
||||
private void notifyPointsUpdated(final boolean keepState,
|
||||
final Set<TrackDataListener> sampledListeners,
|
||||
final Set<TrackDataListener> sampledOutListeners) {
|
||||
if (sampledListeners.isEmpty() && sampledOutListeners.isEmpty()) return;
|
||||
|
||||
runInListenerThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
notifyPointsUpdatedSync(keepState, sampledListeners, sampledOutListeners);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Asynchronous version of the above method.
|
||||
*/
|
||||
private void notifyPointsUpdatedSync(boolean keepState,
|
||||
Set<TrackDataListener> sampledListeners,
|
||||
Set<TrackDataListener> sampledOutListeners) {
|
||||
// If we're loading state, start from after the last seen point up to the last recorded one
|
||||
// (all new points)
|
||||
// If we're not loading state, then notify about all the previously-seen points.
|
||||
long minPointId = keepState ? lastSeenLocationId + 1 : 0;
|
||||
long maxPointId = keepState ? -1 : lastSeenLocationId;
|
||||
|
||||
// TODO: Move (re)sampling to a separate class.
|
||||
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?).
|
||||
|
||||
Log.i(TAG, "Resampling point set after " + numLoadedPoints + " points.");
|
||||
firstSeenLocationId = -1;
|
||||
lastSeenLocationId = -1;
|
||||
numLoadedPoints = 0;
|
||||
synchronized (listeners) {
|
||||
sampledListeners = getListenersFor(ListenerDataType.POINT_UPDATES);
|
||||
sampledOutListeners = getListenersFor(ListenerDataType.SAMPLED_OUT_POINT_UPDATES);
|
||||
}
|
||||
maxPointId = -1;
|
||||
minPointId = 0;
|
||||
keepState = true;
|
||||
|
||||
for (TrackDataListener listener : sampledListeners) {
|
||||
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, sampledListeners, sampledOutListeners);
|
||||
|
||||
localNumLoadedPoints++;
|
||||
localLastSeenLocationId = locationId;
|
||||
}
|
||||
it.close();
|
||||
|
||||
if (keepState) {
|
||||
numLoadedPoints = localNumLoadedPoints;
|
||||
firstSeenLocationId = localFirstSeenLocationId;
|
||||
lastSeenLocationId = localLastSeenLocationId;
|
||||
}
|
||||
|
||||
for (TrackDataListener listener : sampledListeners) {
|
||||
listener.onNewTrackPointsDone();
|
||||
}
|
||||
}
|
||||
|
||||
private void notifyNewPoint(Location location,
|
||||
long locationId,
|
||||
long lastStoredLocationId,
|
||||
int numLoadedPoints,
|
||||
int pointSamplingFrequency,
|
||||
Set<TrackDataListener> sampledListeners,
|
||||
Set<TrackDataListener> sampledOutListeners) {
|
||||
boolean isValid = LocationUtils.isValidLocation(location);
|
||||
if (!isValid) {
|
||||
// Invalid points are segment splits - report those separately.
|
||||
// TODO: Always send last valid point before and first valid point after a split
|
||||
for (TrackDataListener listener : sampledListeners) {
|
||||
listener.onSegmentSplit();
|
||||
}
|
||||
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 : sampledOutListeners) {
|
||||
listener.onSampledOutTrackPoint(location);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Point is valid and included in sample.
|
||||
for (TrackDataListener listener : sampledListeners) {
|
||||
// No need to allocate a new location (we can safely reuse the existing).
|
||||
listener.onNewTrackPoint(location);
|
||||
}
|
||||
}
|
||||
|
||||
// @VisibleForTesting
|
||||
protected void runInListenerThread(Runnable runnable) {
|
||||
listenerHandler.post(runnable);
|
||||
}
|
||||
|
||||
private Set<TrackDataListener> getListenersFor(ListenerDataType type) {
|
||||
synchronized (listeners) {
|
||||
return listeners.getListenersFor(type);
|
||||
}
|
||||
}
|
||||
|
||||
private EnumSet<ListenerDataType> getNeededListenerTypes() {
|
||||
EnumSet<ListenerDataType> neededTypes = listeners.getAllRegisteredTypes();
|
||||
|
||||
// We always want preference updates.
|
||||
neededTypes.add(ListenerDataType.DISPLAY_PREFERENCES);
|
||||
|
||||
return neededTypes;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
/*
|
||||
* 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 com.google.android.apps.mytracks.content.Track;
|
||||
import com.google.android.apps.mytracks.content.Waypoint;
|
||||
|
||||
import android.location.Location;
|
||||
|
||||
/**
|
||||
* Listener for track data, for both initial and incremental loading.
|
||||
*
|
||||
* @author Rodrigo Damazio
|
||||
*/
|
||||
public interface TrackDataListener {
|
||||
|
||||
/** States for the GPS location provider. */
|
||||
public enum ProviderState {
|
||||
DISABLED,
|
||||
NO_FIX,
|
||||
BAD_FIX,
|
||||
GOOD_FIX;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the location provider changes state.
|
||||
*/
|
||||
void onProviderStateChange(ProviderState state);
|
||||
|
||||
/**
|
||||
* Called when the current location changes.
|
||||
* This is meant for immediate location display only - track point data is
|
||||
* delivered by other methods below, such as {@link #onNewTrackPoint}.
|
||||
*
|
||||
* @param loc the last known location
|
||||
*/
|
||||
void onCurrentLocationChanged(Location loc);
|
||||
|
||||
/**
|
||||
* Called when the current heading changes.
|
||||
*
|
||||
* @param heading the current heading, already accounting magnetic declination
|
||||
*/
|
||||
void onCurrentHeadingChanged(double heading);
|
||||
|
||||
/**
|
||||
* Called when the currently-selected track changes.
|
||||
* This will be followed by calls to data methods such as
|
||||
* {@link #onTrackUpdated}, {@link #clearTrackPoints},
|
||||
* {@link #onNewTrackPoint(Location)}, etc., even if no track is currently
|
||||
* selected (in which case you'll only get calls to clear the current data).
|
||||
*
|
||||
* @param track the selected track, or null if no track is selected
|
||||
* @param isRecording whether we're currently recording the selected track
|
||||
*/
|
||||
void onSelectedTrackChanged(Track track, boolean isRecording);
|
||||
|
||||
/**
|
||||
* Called when the track and/or its statistics have been updated.
|
||||
*
|
||||
* @param track the updated version of the track
|
||||
*/
|
||||
void onTrackUpdated(Track track);
|
||||
|
||||
/**
|
||||
* Called to clear any previously-sent track points.
|
||||
* This can be called at any time that we decide the data needs to be
|
||||
* reloaded, such as when it needs to be resampled.
|
||||
*/
|
||||
void clearTrackPoints();
|
||||
|
||||
/**
|
||||
* Called when a new interesting track point is read.
|
||||
* In this case, interesting means that the point has already undergone
|
||||
* sampling and invalid point filtering.
|
||||
*
|
||||
* @param loc the new track point
|
||||
*/
|
||||
void onNewTrackPoint(Location loc);
|
||||
|
||||
/**
|
||||
* Called when a uninteresting track point is read.
|
||||
* Uninteresting points are all points that get sampled out of the track.
|
||||
*
|
||||
* @param loc the new track point
|
||||
*/
|
||||
void onSampledOutTrackPoint(Location loc);
|
||||
|
||||
/**
|
||||
* Called when an invalid point (representing a segment split) is read.
|
||||
*/
|
||||
void onSegmentSplit();
|
||||
|
||||
/**
|
||||
* Called when we're done (for the time being) sending new points.
|
||||
* This gets called after every batch of calls to {@link #onNewTrackPoint},
|
||||
* {@link #onSampledOutTrackPoint} and {@link #onSegmentSplit}.
|
||||
*/
|
||||
void onNewTrackPointsDone();
|
||||
|
||||
/**
|
||||
* Called to clear any previously-sent waypoints.
|
||||
* This can be called at any time that we decide the data needs to be
|
||||
* reloaded.
|
||||
*/
|
||||
void clearWaypoints();
|
||||
|
||||
/**
|
||||
* Called when a new waypoint is read.
|
||||
*
|
||||
* @param wpt the new waypoint
|
||||
*/
|
||||
void onNewWaypoint(Waypoint wpt);
|
||||
|
||||
/**
|
||||
* Called when we're done (for the time being) sending new waypoints.
|
||||
* This gets called after every batch of calls to {@link #clearWaypoints} and
|
||||
* {@link #onNewWaypoint}.
|
||||
*/
|
||||
void onNewWaypointsDone();
|
||||
|
||||
/**
|
||||
* Called when the display units are changed by the user.
|
||||
*
|
||||
* @param metric true if the units are metric, false if imperial
|
||||
* @return true to reload all the data, false otherwise
|
||||
*/
|
||||
boolean onUnitsChanged(boolean metric);
|
||||
|
||||
/**
|
||||
* Called when the speed/pace display unit is changed by the user.
|
||||
*
|
||||
* @param reportSpeed true to report speed, false for pace
|
||||
* @return true to reload all the data, false otherwise
|
||||
*/
|
||||
boolean onReportSpeedChanged(boolean reportSpeed);
|
||||
}
|
||||
@@ -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<ListenerDataType> types;
|
||||
// TODO: Add the last-notified point ID here, to allow pausing/resuming.
|
||||
|
||||
public ListenerRegistration(TrackDataListener listener,
|
||||
EnumSet<ListenerDataType> 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<TrackDataListener, ListenerRegistration> registeredListeners =
|
||||
new HashMap<TrackDataListener, ListenerRegistration>();
|
||||
|
||||
/** Map of data type to external listeners interested in it. */
|
||||
private final Map<ListenerDataType, Set<TrackDataListener>> listenerSetsPerType =
|
||||
new EnumMap<ListenerDataType, Set<TrackDataListener>>(ListenerDataType.class);
|
||||
|
||||
public TrackDataListeners() {
|
||||
// Create sets for all data types at startup.
|
||||
for (ListenerDataType type : ListenerDataType.values()) {
|
||||
listenerSetsPerType.put(type, new LinkedHashSet<TrackDataListener>());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<ListenerDataType> 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<TrackDataListener> 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<TrackDataListener> getListenersFor(ListenerDataType type) {
|
||||
return listenerSetsPerType.get(type);
|
||||
}
|
||||
|
||||
public EnumSet<ListenerDataType> getAllRegisteredTypes() {
|
||||
EnumSet<ListenerDataType> listeners = EnumSet.noneOf(ListenerDataType.class);
|
||||
for (ListenerRegistration registration : this.registeredListeners.values()) {
|
||||
listeners.addAll(registration.types);
|
||||
}
|
||||
return listeners;
|
||||
}
|
||||
|
||||
public boolean hasListeners() {
|
||||
return !registeredListeners.isEmpty();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user