An end-to-end test case of MyTracks.

This commit is contained in:
youtaol
2012-04-24 19:38:32 +08:00
commit b9a9cc6cd9
591 changed files with 63728 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,265 @@
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 notifyLocationProviderAvailable(boolean available);
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.notifyLocationProviderAvailable(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.my_location_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.my_location_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,257 @@
/*
* Copyright 2012 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.stats.TripStatistics;
import com.google.android.apps.mytracks.util.ChartURLGenerator;
import com.google.android.apps.mytracks.util.StringUtils;
import com.google.android.apps.mytracks.util.UnitConversions;
import com.google.android.maps.mytracks.R;
import com.google.common.annotations.VisibleForTesting;
import android.content.Context;
import android.util.Pair;
import java.util.Vector;
/**
* An implementation of {@link DescriptionGenerator} for My Tracks.
*
* @author Jimmy Shih
*/
public class DescriptionGeneratorImpl implements DescriptionGenerator {
private static final String HTML_LINE_BREAK = "<br>";
private static final String TEXT_LINE_BREAK = "\n";
private Context context;
public DescriptionGeneratorImpl(Context context) {
this.context = context;
}
@Override
public String generateTrackDescription(
Track track, Vector<Double> distances, Vector<Double> elevations) {
StringBuilder builder = new StringBuilder();
// Created by
String url = context.getString(R.string.my_tracks_web_url);
builder.append(context.getString(
R.string.send_google_by_my_tracks, "<a href='http://" + url + "'>", "</a>"));
builder.append("<p>");
builder.append(generateTripStatisticsDescription(track.getStatistics(), true));
// Activity type
String trackCategory = track.getCategory();
String category = trackCategory != null && trackCategory.length() > 0 ? trackCategory
: context.getString(R.string.value_unknown);
builder.append(context.getString(R.string.description_activity_type, category));
builder.append(HTML_LINE_BREAK);
// Elevation chart
if (distances != null && elevations != null) {
builder.append("<img border=\"0\" src=\""
+ ChartURLGenerator.getChartUrl(distances, elevations, track, context) + "\"/>");
builder.append(HTML_LINE_BREAK);
}
return builder.toString();
}
@Override
public String generateWaypointDescription(Waypoint waypoint) {
return generateTripStatisticsDescription(waypoint.getStatistics(), false);
}
/**
* Generates a description for a {@link TripStatistics}.
*
* @param stats the trip statistics
* @param html true to use "<br>" for line break instead of "\n"
*/
private String generateTripStatisticsDescription(TripStatistics stats, boolean html) {
String lineBreak = html ? HTML_LINE_BREAK : TEXT_LINE_BREAK;
StringBuilder builder = new StringBuilder();
// Total distance
writeDistance(
stats.getTotalDistance(), builder, R.string.description_total_distance, lineBreak);
// Total time
writeTime(stats.getTotalTime(), builder, R.string.description_total_time, lineBreak);
// Moving time
writeTime(stats.getMovingTime(), builder, R.string.description_moving_time, lineBreak);
// Average speed
Pair<Double, Double> averageSpeed = writeSpeed(
stats.getAverageSpeed(), builder, R.string.description_average_speed, lineBreak);
// Average moving speed
Pair<Double, Double> averageMovingSpeed = writeSpeed(stats.getAverageMovingSpeed(), builder,
R.string.description_average_moving_speed, lineBreak);
// Max speed
Pair<Double, Double> maxSpeed = writeSpeed(
stats.getMaxSpeed(), builder, R.string.description_max_speed, lineBreak);
// Average pace
writePace(averageSpeed, builder, R.string.description_average_pace, lineBreak);
// Average moving pace
writePace(averageMovingSpeed, builder, R.string.description_average_moving_pace, lineBreak);
// Fastest pace
writePace(maxSpeed, builder, R.string.description_fastest_pace, lineBreak);
// Max elevation
writeElevation(stats.getMaxElevation(), builder, R.string.description_max_elevation, lineBreak);
// Min elevation
writeElevation(stats.getMinElevation(), builder, R.string.description_min_elevation, lineBreak);
// Elevation gain
writeElevation(
stats.getTotalElevationGain(), builder, R.string.description_elevation_gain, lineBreak);
// Max grade
writeGrade(stats.getMaxGrade(), builder, R.string.description_max_grade, lineBreak);
// Min grade
writeGrade(stats.getMinGrade(), builder, R.string.description_min_grade, lineBreak);
// Recorded time
builder.append(
context.getString(R.string.description_recorded_time,
StringUtils.formatDateTime(context, stats.getStartTime())));
builder.append(lineBreak);
return builder.toString();
}
/**
* Writes distance.
*
* @param distance distance in meters
* @param builder StringBuilder to append distance
* @param resId resource id of distance string
* @param lineBreak line break string
*/
@VisibleForTesting
void writeDistance(double distance, StringBuilder builder, int resId, String lineBreak) {
double distanceInKm = distance * UnitConversions.M_TO_KM;
double distanceInMi = distanceInKm * UnitConversions.KM_TO_MI;
builder.append(context.getString(resId, distanceInKm, distanceInMi));
builder.append(lineBreak);
}
/**
* Writes time.
*
* @param time time in milliseconds.
* @param builder StringBuilder to append time
* @param resId resource id of time string
* @param lineBreak line break string
*/
@VisibleForTesting
void writeTime(long time, StringBuilder builder, int resId, String lineBreak) {
builder.append(context.getString(resId, StringUtils.formatElapsedTime(time)));
builder.append(lineBreak);
}
/**
* Writes speed.
*
* @param speed speed in meters per second
* @param builder StringBuilder to append speed
* @param resId resource id of speed string
* @param lineBreak line break string
* @return a pair of speed, first in kilometers per hour, second in miles per
* hour.
*/
@VisibleForTesting
Pair<Double, Double> writeSpeed(
double speed, StringBuilder builder, int resId, String lineBreak) {
double speedInKmHr = speed * UnitConversions.MS_TO_KMH;
double speedInMiHr = speedInKmHr * UnitConversions.KM_TO_MI;
builder.append(context.getString(resId, speedInKmHr, speedInMiHr));
builder.append(lineBreak);
return Pair.create(speedInKmHr, speedInMiHr);
}
/**
* Writes pace.
*
* @param speed a pair of speed, first in kilometers per hour, second in miles
* per hour
* @param builder StringBuilder to append pace
* @param resId resource id of pace string
* @param lineBreak line break string
*/
@VisibleForTesting
void writePace(
Pair<Double, Double> speed, StringBuilder builder, int resId, String lineBreak) {
double paceInMinKm = getPace(speed.first);
double paceInMinMi = getPace(speed.second);
builder.append(context.getString(resId, paceInMinKm, paceInMinMi));
builder.append(lineBreak);
}
/**
* Writes elevation.
*
* @param elevation elevation in meters
* @param builder StringBuilder to append elevation
* @param resId resource id of elevation string
* @param lineBreak line break string
*/
@VisibleForTesting
void writeElevation(
double elevation, StringBuilder builder, int resId, String lineBreak) {
long elevationInM = Math.round(elevation);
long elevationInFt = Math.round(elevation * UnitConversions.M_TO_FT);
builder.append(context.getString(resId, elevationInM, elevationInFt));
builder.append(lineBreak);
}
/**
* Writes grade.
*
* @param grade grade in fraction
* @param builder StringBuilder to append grade
* @param resId resource id grade string
* @param lineBreak line break string
*/
@VisibleForTesting
void writeGrade(double grade, StringBuilder builder, int resId, String lineBreak) {
long gradeInPercent = Double.isNaN(grade) || Double.isInfinite(grade) ? 0L
: Math.round(grade * 100);
builder.append(context.getString(resId, gradeInPercent));
builder.append(lineBreak);
}
/**
* Gets pace (in minutes) from speed.
*
* @param speed speed in hours
*/
@VisibleForTesting
double getPace(double speed) {
return speed == 0 ? 0.0 : 60.0 / speed; // convert from hours to minutes
}
}
@@ -0,0 +1,470 @@
/*
* 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.maps.mytracks.R;
import android.content.ContentProvider;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.SharedPreferences;
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.os.Binder;
import android.os.Process;
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);
}
private boolean canAccess() {
if (Binder.getCallingPid() == Process.myPid()) {
return true;
} else {
Context context = getContext();
SharedPreferences sharedPreferences = context.getSharedPreferences(
Constants.SETTINGS_NAME, Context.MODE_PRIVATE);
return sharedPreferences.getBoolean(context.getString(R.string.allow_access_key), false);
}
}
@Override
public boolean onCreate() {
if (!canAccess()) {
return false;
}
DatabaseHelper dbHelper = new DatabaseHelper(getContext());
try {
db = dbHelper.getWritableDatabase();
} catch (SQLiteException e) {
Log.e(TAG, "Unable to open database for writing", e);
}
return db != null;
}
@Override
public int delete(Uri url, String where, String[] selectionArgs) {
if (!canAccess()) {
return 0;
}
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) {
if (!canAccess()) {
return null;
}
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) {
if (!canAccess()) {
return null;
}
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) {
if (!canAccess()) {
return 0;
}
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) {
if (!canAccess()) {
return null;
}
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);
}
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) {
if (!canAccess()) {
return 0;
}
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,405 @@
/*
* Copyright 2012 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.stats.TripStatistics;
import com.google.android.apps.mytracks.util.LocationUtils;
import com.google.android.apps.mytracks.util.UnitConversions;
import android.database.Cursor;
import android.location.Location;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.SortedSet;
import java.util.TreeSet;
/**
* Engine for searching for tracks and waypoints by text.
*
* @author Rodrigo Damazio
*/
public class SearchEngine {
/** WHERE query to get tracks by name. */
private static final String TRACK_SELECTION_QUERY =
TracksColumns.NAME + " LIKE ? OR " +
TracksColumns.DESCRIPTION + " LIKE ? OR " +
TracksColumns.CATEGORY + " LIKE ?";
/** WHERE query to get waypoints by name. */
private static final String WAYPOINT_SELECTION_QUERY =
WaypointsColumns.NAME + " LIKE ? OR " +
WaypointsColumns.DESCRIPTION + " LIKE ? OR " +
WaypointsColumns.CATEGORY + " LIKE ?";
/** Order of track results. */
private static final String TRACK_SELECTION_ORDER = TracksColumns._ID + " DESC LIMIT 1000";
/** Order of waypoint results. */
private static final String WAYPOINT_SELECTION_ORDER = WaypointsColumns._ID + " DESC";
/** How much we promote a match in the track category. */
private static final double TRACK_CATEGORY_PROMOTION = 2.0;
/** How much we promote a match in the track description. */
private static final double TRACK_DESCRIPTION_PROMOTION = 8.0;
/** How much we promote a match in the track name. */
private static final double TRACK_NAME_PROMOTION = 16.0;
/** How much we promote a waypoint result if it's in the currently-selected track. */
private static final double CURRENT_TRACK_WAYPOINT_PROMOTION = 2.0;
/** How much we promote a track result if it's the currently-selected track. */
private static final double CURRENT_TRACK_DEMOTION = 0.5;
/** Maximum number of waypoints which will be retrieved and scored. */
private static final int MAX_SCORED_WAYPOINTS = 100;
/** Oldest timestamp for which we rank based on time (2000-01-01 00:00:00.000) */
private static final long OLDEST_ALLOWED_TIMESTAMP = 946692000000L;
/**
* Description of a search query, along with all contextual data needed to execute it.
*/
public static class SearchQuery {
public SearchQuery(String textQuery, Location currentLocation, long currentTrackId,
long currentTimestamp) {
this.textQuery = textQuery.toLowerCase();
this.currentLocation = currentLocation;
this.currentTrackId = currentTrackId;
this.currentTimestamp = currentTimestamp;
}
public final String textQuery;
public final Location currentLocation;
public final long currentTrackId;
public final long currentTimestamp;
}
/**
* Description of a search result which has been retrieved and scored.
*/
public static class ScoredResult {
ScoredResult(Track track, double score) {
this.track = track;
this.waypoint = null;
this.score = score;
}
ScoredResult(Waypoint waypoint, double score) {
this.track = null;
this.waypoint = waypoint;
this.score = score;
}
public final Track track;
public final Waypoint waypoint;
public final double score;
@Override
public String toString() {
return "ScoredResult ["
+ (track != null ? ("trackId=" + track.getId() + ", ") : "")
+ (waypoint != null ? ("wptId=" + waypoint.getId() + ", ") : "")
+ "score=" + score + "]";
}
}
/** Comparador for scored results. */
private static final Comparator<ScoredResult> SCORED_RESULT_COMPARATOR =
new Comparator<ScoredResult>() {
@Override
public int compare(ScoredResult r1, ScoredResult r2) {
// Score ordering.
int scoreDiff = Double.compare(r2.score, r1.score);
if (scoreDiff != 0) {
return scoreDiff;
}
// Make tracks come before waypoints.
if (r1.waypoint != null && r2.track != null) {
return 1;
} else if (r1.track != null && r2.waypoint != null) {
return -1;
}
// Finally, use arbitrary ordering, by ID.
long id1 = r1.track != null ? r1.track.getId() : r1.waypoint.getId();
long id2 = r2.track != null ? r2.track.getId() : r2.waypoint.getId();
long idDiff = id2 - id1;
return Long.signum(idDiff);
}
};
private final MyTracksProviderUtils providerUtils;
public SearchEngine(MyTracksProviderUtils providerUtils) {
this.providerUtils = providerUtils;
}
/**
* Executes a search query and returns a set of sorted results.
*
* @param query the query to execute
* @return a set of results, sorted according to their score
*/
public SortedSet<ScoredResult> search(SearchQuery query) {
ArrayList<Track> tracks = new ArrayList<Track>();
ArrayList<Waypoint> waypoints = new ArrayList<Waypoint>();
TreeSet<ScoredResult> scoredResults = new TreeSet<ScoredResult>(SCORED_RESULT_COMPARATOR);
retrieveTracks(query, tracks);
retrieveWaypoints(query, waypoints);
scoreTrackResults(tracks, query, scoredResults);
scoreWaypointResults(waypoints, query, scoredResults);
return scoredResults;
}
/**
* Retrieves tracks matching the given query from the database.
*
* @param query the query to retrieve for
* @param tracks list to fill with the resulting tracks
*/
private void retrieveTracks(SearchQuery query, ArrayList<Track> tracks) {
String queryLikeSelection = "%" + query.textQuery + "%";
String[] trackSelectionArgs = new String[] {
queryLikeSelection,
queryLikeSelection,
queryLikeSelection };
Cursor tracksCursor = providerUtils.getTracksCursor(
TRACK_SELECTION_QUERY, trackSelectionArgs, TRACK_SELECTION_ORDER);
if (tracksCursor != null) {
try {
tracks.ensureCapacity(tracksCursor.getCount());
while (tracksCursor.moveToNext()) {
tracks.add(providerUtils.createTrack(tracksCursor));
}
} finally {
tracksCursor.close();
}
}
}
/**
* Retrieves waypoints matching the given query from the database.
*
* @param query the query to retrieve for
* @param waypoints list to fill with the resulting waypoints
*/
private void retrieveWaypoints(SearchQuery query, ArrayList<Waypoint> waypoints) {
String queryLikeSelection2 = "%" + query.textQuery + "%";
String[] waypointSelectionArgs = new String[] {
queryLikeSelection2,
queryLikeSelection2,
queryLikeSelection2 };
Cursor waypointsCursor = providerUtils.getWaypointsCursor(
WAYPOINT_SELECTION_QUERY, waypointSelectionArgs, WAYPOINT_SELECTION_ORDER,
MAX_SCORED_WAYPOINTS);
if (waypointsCursor != null) {
try {
waypoints.ensureCapacity(waypointsCursor.getCount());
while (waypointsCursor.moveToNext()) {
Waypoint waypoint = providerUtils.createWaypoint(waypointsCursor);
if (LocationUtils.isValidLocation(waypoint.getLocation())) {
waypoints.add(waypoint);
}
}
} finally {
waypointsCursor.close();
}
}
}
/**
* Scores a collection of track results.
*
* @param tracks the results to score
* @param query the query to score for
* @param output the collection to fill with scored results
*/
private void scoreTrackResults(Collection<Track> tracks, SearchQuery query, Collection<ScoredResult> output) {
for (Track track : tracks) {
// Calculate the score.
double score = scoreTrackResult(query, track);
// Add to the output.
output.add(new ScoredResult(track, score));
}
}
/**
* Scores a single track result.
*
* @param query the query to score for
* @param track the results to score
* @return the score for the track
*/
private double scoreTrackResult(SearchQuery query, Track track) {
double score = 1.0;
score *= getTitleBoost(query, track.getName(), track.getDescription(), track.getCategory());
TripStatistics statistics = track.getStatistics();
// TODO: Also boost for proximity to the currently-centered position on the map.
score *= getDistanceBoost(query, statistics.getMeanLatitude(), statistics.getMeanLongitude());
long meanTimestamp = (statistics.getStartTime() + statistics.getStopTime()) / 2L;
score *= getTimeBoost(query, meanTimestamp);
// Score the currently-selected track lower (user is already there, wouldn't be searching for it).
if (track.getId() == query.currentTrackId) {
score *= CURRENT_TRACK_DEMOTION;
}
return score;
}
/**
* Scores a collection of waypoint results.
*
* @param waypoints the results to score
* @param query the query to score for
* @param output the collection to fill with scored results
*/
private void scoreWaypointResults(Collection<Waypoint> waypoints, SearchQuery query, Collection<ScoredResult> output) {
for (Waypoint waypoint : waypoints) {
// Calculate the score.
double score = scoreWaypointResult(query, waypoint);
// Add to the output.
output.add(new ScoredResult(waypoint, score));
}
}
/**
* Scores a single waypoint result.
*
* @param query the query to score for
* @param waypoint the results to score
* @return the score for the waypoint
*/
private double scoreWaypointResult(SearchQuery query, Waypoint waypoint) {
double score = 1.0;
Location location = waypoint.getLocation();
score *= getTitleBoost(query, waypoint.getName(), waypoint.getDescription(), waypoint.getCategory());
// TODO: Also boost for proximity to the currently-centered position on the map.
score *= getDistanceBoost(query, location.getLatitude(), location.getLongitude());
score *= getTimeBoost(query, location.getTime());
// Score waypoints in the currently-selected track higher (searching inside the current track).
if (query.currentTrackId != -1 && waypoint.getTrackId() == query.currentTrackId) {
score *= CURRENT_TRACK_WAYPOINT_PROMOTION;
}
return score;
}
/**
* Calculates the boosting of the score due to the field(s) in which the match occured.
*
* @param query the query to boost for
* @param name the name of the track or waypoint
* @param description the description of the track or waypoint
* @param category the category of the track or waypoint
* @return the total boost to be applied to the result
*/
private double getTitleBoost(SearchQuery query,
String name, String description, String category) {
// Title boost: track name > description > category.
double boost = 1.0;
if (name.toLowerCase().contains(query.textQuery)) {
boost *= TRACK_NAME_PROMOTION;
}
if (description.toLowerCase().contains(query.textQuery)) {
boost *= TRACK_DESCRIPTION_PROMOTION;
}
if (category.toLowerCase().contains(query.textQuery)) {
boost *= TRACK_CATEGORY_PROMOTION;
}
return boost;
}
/**
* Calculates the boosting of the score due to the recency of the matched entity.
*
* @param query the query to boost for
* @param timestamp the timestamp to calculate the boost for
* @return the total boost to be applied to the result
*/
private double getTimeBoost(SearchQuery query, long timestamp) {
if (timestamp < OLDEST_ALLOWED_TIMESTAMP) {
// Safety: if timestamp is too old or invalid, don't rank based on time.
return 1.0;
}
// Score recent tracks higher.
long timeAgoHours = (query.currentTimestamp - timestamp) / (60L * 60L * 1000L);
if (timeAgoHours > 0L) {
return squash(timeAgoHours);
} else {
// Should rarely happen (track recorded in the last hour).
return Double.POSITIVE_INFINITY;
}
}
/**
* Calculates the boosting of the score due to proximity to a location.
*
* @param query the query to boost for
* @param latitude the latitude to calculate the boost for
* @param longitude the longitude to calculate the boost for
* @return the total boost to be applied to the result
*/
private double getDistanceBoost(SearchQuery query, double latitude, double longitude) {
if (query.currentLocation == null) {
return 1.0;
}
float[] distanceResults = new float[1];
Location.distanceBetween(
latitude, longitude,
query.currentLocation.getLatitude(), query.currentLocation.getLongitude(),
distanceResults);
// Score tracks close to the current location higher.
double distanceKm = distanceResults[0] * UnitConversions.M_TO_KM;
if (distanceKm > 0.0) {
// Use the inverse of the amortized distance.
return squash(distanceKm);
} else {
// Should rarely happen (distance is exactly 0).
return Double.POSITIVE_INFINITY;
}
}
/**
* Squashes a number by calculating 1 / log (1 + x).
*/
private static double squash(double x) {
return 1.0 / Math.log1p(x);
}
}
@@ -0,0 +1,44 @@
/*
* Copyright 2012 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.Context;
import android.content.SearchRecentSuggestionsProvider;
import android.provider.SearchRecentSuggestions;
/**
* Content provider for search suggestions.
*
* @author Rodrigo Damazio
*/
public class SearchEngineProvider extends SearchRecentSuggestionsProvider {
private static final String AUTHORITY = "com.google.android.maps.mytracks.search";
private static final int MODE = DATABASE_MODE_QUERIES;
public SearchEngineProvider() {
setupSuggestions(AUTHORITY, MODE);
}
// TODO: Also add suggestions from the database.
/**
* Creates and returns a helper for adding recent queries or clearing the recent query history.
*/
public static SearchRecentSuggestions newHelper(Context context) {
return new SearchRecentSuggestions(context, AUTHORITY, MODE);
}
}
File diff suppressed because it is too large Load Diff
@@ -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,177 @@
/*
* 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;
import java.util.WeakHashMap;
/**
* 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;
// State that was last notified to the listener, for resuming after a pause.
long lastTrackId;
long lastPointId;
int lastSamplingFrequency;
int numLoadedPoints;
public ListenerRegistration(TrackDataListener listener,
EnumSet<ListenerDataType> types) {
this.listener = listener;
this.types = types;
}
public boolean isInterestedIn(ListenerDataType type) {
return types.contains(type);
}
public void resetState() {
lastTrackId = 0L;
lastPointId = 0L;
lastSamplingFrequency = 0;
numLoadedPoints = 0;
}
@Override
public String toString() {
return "ListenerRegistration [listener=" + listener + ", types=" + types
+ ", lastTrackId=" + lastTrackId + ", lastPointId=" + lastPointId
+ ", lastSamplingFrequency=" + lastSamplingFrequency
+ ", numLoadedPoints=" + numLoadedPoints + "]";
}
}
/** Map of external listener to its registration details. */
private final Map<TrackDataListener, ListenerRegistration> registeredListeners =
new HashMap<TrackDataListener, ListenerRegistration>();
/**
* Map of external paused listener to its registration details.
* This will automatically discard listeners which are GCed.
*/
private final WeakHashMap<TrackDataListener, ListenerRegistration> oldListeners =
new WeakHashMap<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 TrackDataHub#start}, and in that case
* the data will only be passed to listeners when {@link TrackDataHub#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);
if (registeredListeners.containsKey(listener)) {
throw new IllegalStateException("Listener already registered");
}
ListenerRegistration registration = oldListeners.remove(listener);
if (registration == null) {
registration = new ListenerRegistration(listener, dataTypes);
}
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);
}
// Keep it around in case it's re-registered soon
oldListeners.put(listener, match);
}
public ListenerRegistration getRegistration(TrackDataListener listener) {
ListenerRegistration registration = registeredListeners.get(listener);
if (registration == null) {
registration = oldListeners.get(listener);
}
return registration;
}
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();
}
public int getNumListeners() {
return registeredListeners.size();
}
}