Standardizing the creator string across exported file formats.

This commit is contained in:
Rodrigo Damazio
2011-05-04 18:21:49 -03:00
commit 8fcc809299
436 changed files with 55418 additions and 0 deletions
@@ -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();
}
}