mirror of
https://codeberg.org/OpenTracksApp/OpenTracks.git
synced 2026-10-04 10:23:06 +02:00
merge heads with Google Location settings change from My Tracks 2.0.1
This commit is contained in:
@@ -81,9 +81,6 @@ public class ChartView extends View {
|
||||
private static final int SPACER = 4;
|
||||
private static final int Y_AXIS_OFFSET = 16;
|
||||
|
||||
// Marker pin x position / marker icon width
|
||||
private static final double MARKER_PIN_X_POSITION_PERCENTAGE = 13 / 48.0;
|
||||
|
||||
private final ChartValueSeries[] series = new ChartValueSeries[NUM_SERIES];
|
||||
private final ArrayList<double[]> chartData = new ArrayList<double[]>();
|
||||
private final ArrayList<Waypoint> waypoints = new ArrayList<Waypoint>();
|
||||
@@ -559,8 +556,8 @@ public class ChartView extends View {
|
||||
float x = getX(getWaypointXValue(waypoint));
|
||||
canvas.drawLine(
|
||||
x, topBorder + spacer + markerHeight / 2, x, topBorder + effectiveHeight, markerPaint);
|
||||
canvas.translate(
|
||||
x - (float) (markerWidth * MARKER_PIN_X_POSITION_PERCENTAGE), topBorder + spacer);
|
||||
canvas.translate(x - (float) (markerWidth * Constants.WAYPOINT_X_OFFSET_PERCENTAGE),
|
||||
topBorder + spacer);
|
||||
if (waypoints.get(i).getType() == Waypoint.TYPE_STATISTICS) {
|
||||
statisticsMarker.draw(canvas);
|
||||
} else {
|
||||
|
||||
@@ -126,6 +126,10 @@ public abstract class Constants {
|
||||
|
||||
public static final String SETTINGS_NAME = "SettingsActivity";
|
||||
|
||||
public static final double WAYPOINT_X_OFFSET_PERCENTAGE = 13 / 48.0;
|
||||
|
||||
public static final double MARKER_Y_OFFSET_PERCENTAGE = 91 / 96.0;
|
||||
|
||||
/**
|
||||
* This is an abstract utility class.
|
||||
*/
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
|
||||
package com.google.android.apps.mytracks;
|
||||
|
||||
import static com.google.android.apps.mytracks.Constants.TAG;
|
||||
@@ -20,7 +21,7 @@ import static com.google.android.apps.mytracks.Constants.TAG;
|
||||
import com.google.android.apps.mytracks.content.Waypoint;
|
||||
import com.google.android.apps.mytracks.maps.TrackPathPainter;
|
||||
import com.google.android.apps.mytracks.maps.TrackPathPainterFactory;
|
||||
import com.google.android.apps.mytracks.maps.TrackPathUtilities;
|
||||
import com.google.android.apps.mytracks.maps.TrackPathUtils;
|
||||
import com.google.android.apps.mytracks.util.IntentUtils;
|
||||
import com.google.android.apps.mytracks.util.LocationUtils;
|
||||
import com.google.android.apps.mytracks.util.PreferencesUtils;
|
||||
@@ -30,6 +31,7 @@ import com.google.android.maps.MapView;
|
||||
import com.google.android.maps.Overlay;
|
||||
import com.google.android.maps.Projection;
|
||||
import com.google.android.maps.mytracks.R;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
@@ -50,47 +52,47 @@ import java.util.concurrent.ArrayBlockingQueue;
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
|
||||
/**
|
||||
* A map overlay that displays a "MyLocation" arrow, an error circle, the
|
||||
* currently recording track and optionally a selected track.
|
||||
*
|
||||
* A map overlay that displays my location arrow, error circle, and track info.
|
||||
*
|
||||
* @author Leif Hendrik Wilden
|
||||
*/
|
||||
public class MapOverlay extends Overlay implements OnSharedPreferenceChangeListener {
|
||||
|
||||
private final Drawable[] arrows;
|
||||
private final int arrowWidth, arrowHeight;
|
||||
private final Drawable statsMarker;
|
||||
private final Drawable waypointMarker;
|
||||
private final Drawable startMarker;
|
||||
private final Drawable endMarker;
|
||||
private final int markerWidth, markerHeight;
|
||||
private final Paint errorCirclePaint;
|
||||
private final Context context;
|
||||
private final List<Waypoint> waypoints;
|
||||
private final List<CachedLocation> points;
|
||||
private final BlockingQueue<CachedLocation> pendingPoints;
|
||||
private final Drawable[] arrows;
|
||||
private final int arrowWidth;
|
||||
private final int arrowHeight;
|
||||
private final Drawable statsMarker;
|
||||
private final Drawable waypointMarker;
|
||||
private final Drawable startMarker;
|
||||
private final Drawable endMarker;
|
||||
private final int markerWidth;
|
||||
private final int markerHeight;
|
||||
private final Paint errorCirclePaint;
|
||||
private TrackPathPainter trackPathPainter;
|
||||
|
||||
private boolean trackDrawingEnabled;
|
||||
private int lastHeading = 0;
|
||||
private Location myLocation;
|
||||
private boolean showEndMarker = true;
|
||||
// TODO: Remove it completely after completing performance tests.
|
||||
private boolean alwaysVisible = true;
|
||||
private int headingIndex = 0;
|
||||
private Location myLocation;
|
||||
|
||||
private GeoPoint lastReferencePoint;
|
||||
private Rect lastViewRect;
|
||||
private boolean lastPathExists;
|
||||
private TrackPathPainter trackPathPainter;
|
||||
|
||||
/**
|
||||
* Represents a pre-processed {@code Location} to speed up drawing.
|
||||
* This class is more like a data object and doesn't provide accessors.
|
||||
* A pre-processed {@link Location} to speed up drawing.
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
public static class CachedLocation {
|
||||
public final boolean valid;
|
||||
public final GeoPoint geoPoint;
|
||||
public final int speed;
|
||||
|
||||
|
||||
private final boolean valid;
|
||||
private final GeoPoint geoPoint;
|
||||
private final int speed;
|
||||
|
||||
/**
|
||||
* Constructor for an invalid cached location.
|
||||
*/
|
||||
@@ -108,41 +110,53 @@ public class MapOverlay extends Overlay implements OnSharedPreferenceChangeListe
|
||||
this.geoPoint = valid ? LocationUtils.getGeoPoint(location) : null;
|
||||
this.speed = (int) Math.floor(location.getSpeed() * UnitConversions.MS_TO_KMH);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the location is valid.
|
||||
*/
|
||||
public boolean isValid() {
|
||||
return valid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the {@link GeoPoint}.
|
||||
*/
|
||||
public GeoPoint getGeoPoint() {
|
||||
return geoPoint;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the speed in kilometers per hour.
|
||||
*/
|
||||
public int getSpeed() {
|
||||
return speed;
|
||||
}
|
||||
};
|
||||
|
||||
public MapOverlay(Context context) {
|
||||
this.context = context;
|
||||
|
||||
this.waypoints = new ArrayList<Waypoint>();
|
||||
this.points = new ArrayList<CachedLocation>(1024);
|
||||
this.pendingPoints = new ArrayBlockingQueue<CachedLocation>(
|
||||
Constants.MAX_DISPLAYED_TRACK_POINTS, true);
|
||||
|
||||
// TODO: Can we use a FrameAnimation or similar here rather
|
||||
// than individual resources for each arrow direction?
|
||||
/*
|
||||
* TODO: Use animation rather than individual resources for each arrow
|
||||
* direction.
|
||||
*/
|
||||
final Resources resources = context.getResources();
|
||||
arrows = new Drawable[] {
|
||||
resources.getDrawable(R.drawable.arrow_0),
|
||||
resources.getDrawable(R.drawable.arrow_20),
|
||||
resources.getDrawable(R.drawable.arrow_40),
|
||||
resources.getDrawable(R.drawable.arrow_60),
|
||||
resources.getDrawable(R.drawable.arrow_80),
|
||||
resources.getDrawable(R.drawable.arrow_100),
|
||||
resources.getDrawable(R.drawable.arrow_120),
|
||||
resources.getDrawable(R.drawable.arrow_140),
|
||||
resources.getDrawable(R.drawable.arrow_160),
|
||||
resources.getDrawable(R.drawable.arrow_180),
|
||||
resources.getDrawable(R.drawable.arrow_200),
|
||||
resources.getDrawable(R.drawable.arrow_220),
|
||||
resources.getDrawable(R.drawable.arrow_240),
|
||||
resources.getDrawable(R.drawable.arrow_260),
|
||||
resources.getDrawable(R.drawable.arrow_280),
|
||||
resources.getDrawable(R.drawable.arrow_300),
|
||||
resources.getDrawable(R.drawable.arrow_320),
|
||||
resources.getDrawable(R.drawable.arrow_340)
|
||||
};
|
||||
arrowWidth = arrows[lastHeading].getIntrinsicWidth();
|
||||
arrowHeight = arrows[lastHeading].getIntrinsicHeight();
|
||||
arrows = new Drawable[] { resources.getDrawable(R.drawable.arrow_0),
|
||||
resources.getDrawable(R.drawable.arrow_20), resources.getDrawable(R.drawable.arrow_40),
|
||||
resources.getDrawable(R.drawable.arrow_60), resources.getDrawable(R.drawable.arrow_80),
|
||||
resources.getDrawable(R.drawable.arrow_100), resources.getDrawable(R.drawable.arrow_120),
|
||||
resources.getDrawable(R.drawable.arrow_140), resources.getDrawable(R.drawable.arrow_160),
|
||||
resources.getDrawable(R.drawable.arrow_180), resources.getDrawable(R.drawable.arrow_200),
|
||||
resources.getDrawable(R.drawable.arrow_220), resources.getDrawable(R.drawable.arrow_240),
|
||||
resources.getDrawable(R.drawable.arrow_260), resources.getDrawable(R.drawable.arrow_280),
|
||||
resources.getDrawable(R.drawable.arrow_300), resources.getDrawable(R.drawable.arrow_320),
|
||||
resources.getDrawable(R.drawable.arrow_340) };
|
||||
arrowWidth = arrows[headingIndex].getIntrinsicWidth();
|
||||
arrowHeight = arrows[headingIndex].getIntrinsicHeight();
|
||||
for (Drawable arrow : arrows) {
|
||||
arrow.setBounds(0, 0, arrowWidth, arrowHeight);
|
||||
}
|
||||
@@ -152,36 +166,36 @@ public class MapOverlay extends Overlay implements OnSharedPreferenceChangeListe
|
||||
markerHeight = statsMarker.getIntrinsicHeight();
|
||||
statsMarker.setBounds(0, 0, markerWidth, markerHeight);
|
||||
|
||||
waypointMarker = resources.getDrawable(R.drawable.blue_pushpin);
|
||||
waypointMarker.setBounds(0, 0, markerWidth, markerHeight);
|
||||
|
||||
startMarker = resources.getDrawable(R.drawable.green_dot);
|
||||
startMarker.setBounds(0, 0, markerWidth, markerHeight);
|
||||
|
||||
endMarker = resources.getDrawable(R.drawable.red_dot);
|
||||
endMarker.setBounds(0, 0, markerWidth, markerHeight);
|
||||
|
||||
waypointMarker = resources.getDrawable(R.drawable.blue_pushpin);
|
||||
waypointMarker.setBounds(0, 0, markerWidth, markerHeight);
|
||||
|
||||
errorCirclePaint = TrackPathUtilities.getPaint(R.color.blue, context);
|
||||
errorCirclePaint = TrackPathUtils.getPaint(context, R.color.blue);
|
||||
errorCirclePaint.setAlpha(127);
|
||||
|
||||
|
||||
trackPathPainter = TrackPathPainterFactory.getTrackPathPainter(context);
|
||||
|
||||
|
||||
context.getSharedPreferences(Constants.SETTINGS_NAME, Context.MODE_PRIVATE)
|
||||
.registerOnSharedPreferenceChangeListener(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a location to the map overlay.
|
||||
*
|
||||
* <p>
|
||||
* NOTE: This method doesn't take ownership of the given location, so it is
|
||||
* safe to reuse the same location while calling this method.
|
||||
*
|
||||
* @param l the location to add.
|
||||
*
|
||||
* @param location the location
|
||||
*/
|
||||
public void addLocation(Location l) {
|
||||
// Queue up in the pending queue until it's merged with {@code #points}.
|
||||
if (!pendingPoints.offer(new CachedLocation(l))) {
|
||||
Log.e(TAG, "Unable to add pending points");
|
||||
public void addLocation(Location location) {
|
||||
// Queue up in the pendingPoints until it's merged with points.
|
||||
if (!pendingPoints.offer(new CachedLocation(location))) {
|
||||
Log.e(TAG, "Unable to add to pendingPoints.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -189,81 +203,125 @@ public class MapOverlay extends Overlay implements OnSharedPreferenceChangeListe
|
||||
* Adds a segment split to the map overlay.
|
||||
*/
|
||||
public void addSegmentSplit() {
|
||||
// Queue up in the pendingPoints until it's merged with points.
|
||||
if (!pendingPoints.offer(new CachedLocation())) {
|
||||
Log.e(TAG, "Unable to add pending points");
|
||||
Log.e(TAG, "Unable to add to pendingPoints");
|
||||
}
|
||||
}
|
||||
|
||||
public void addWaypoint(Waypoint wpt) {
|
||||
// Note: We don't cache waypoints, because it's not worth the effort.
|
||||
if (wpt != null && wpt.getLocation() != null) {
|
||||
/**
|
||||
* Clears the locations.
|
||||
*/
|
||||
public void clearPoints() {
|
||||
synchronized (points) {
|
||||
points.clear();
|
||||
pendingPoints.clear();
|
||||
trackPathPainter.clearPath();
|
||||
lastReferencePoint = null;
|
||||
lastViewRect = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a waypoint to the map overlay.
|
||||
*
|
||||
* @param waypoint the waypoint
|
||||
*/
|
||||
public void addWaypoint(Waypoint waypoint) {
|
||||
// Note: We don't cache waypoints because it's not worth the effort.
|
||||
if (waypoint != null && waypoint.getLocation() != null) {
|
||||
synchronized (waypoints) {
|
||||
waypoints.add(wpt);
|
||||
waypoints.add(waypoint);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int getNumLocations() {
|
||||
synchronized (points) {
|
||||
return points.size() + pendingPoints.size();
|
||||
}
|
||||
}
|
||||
|
||||
// Visible for testing.
|
||||
public int getNumWaypoints() {
|
||||
synchronized (waypoints) {
|
||||
return waypoints.size();
|
||||
}
|
||||
}
|
||||
|
||||
public void clearPoints() {
|
||||
synchronized (getPoints()) {
|
||||
getPoints().clear();
|
||||
pendingPoints.clear();
|
||||
lastPathExists = false;
|
||||
lastViewRect = null;
|
||||
trackPathPainter.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the waypoints.
|
||||
*/
|
||||
public void clearWaypoints() {
|
||||
synchronized (waypoints) {
|
||||
waypoints.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether to draw the track or not.
|
||||
*
|
||||
* @param trackDrawingEnabled true to draw track
|
||||
*/
|
||||
public void setTrackDrawingEnabled(boolean trackDrawingEnabled) {
|
||||
this.trackDrawingEnabled = trackDrawingEnabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether to draw the end maker or not.
|
||||
*
|
||||
* @param showEndMarker true to draw end marker
|
||||
*/
|
||||
public void setShowEndMarker(boolean showEndMarker) {
|
||||
this.showEndMarker = showEndMarker;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets my location.
|
||||
*
|
||||
* @param myLocation my location
|
||||
*/
|
||||
public void setMyLocation(Location myLocation) {
|
||||
this.myLocation = myLocation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the heading.
|
||||
*
|
||||
* @param heading the heading
|
||||
* @return true if the visible heading has changed.
|
||||
*/
|
||||
public boolean setHeading(float heading) {
|
||||
/*
|
||||
* Use -heading because the arrow images are counter-clockwise rather than
|
||||
* clockwise.
|
||||
*/
|
||||
int index = Math.round(-heading / 360 * 18);
|
||||
while (index < 0) {
|
||||
index += 18;
|
||||
}
|
||||
while (index > 17) {
|
||||
index -= 18;
|
||||
}
|
||||
if (index != headingIndex) {
|
||||
headingIndex = index;
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(Canvas canvas, MapView mapView, boolean shadow) {
|
||||
if (shadow) {
|
||||
return;
|
||||
}
|
||||
|
||||
// It's safe to keep projection within a single draw operation.
|
||||
final Projection projection = getMapProjection(mapView);
|
||||
// It's safe to keep projection within a single draw operation
|
||||
Projection projection = getMapProjection(mapView);
|
||||
if (projection == null) {
|
||||
Log.w(TAG, "No projection, unable to draw");
|
||||
Log.w(TAG, "No projection, unable to draw.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the current viewing window.
|
||||
if (trackDrawingEnabled) {
|
||||
// Get the current viewing Rect
|
||||
Rect viewRect = getMapViewRect(mapView);
|
||||
|
||||
// Draw the selected track:
|
||||
// Draw the selected track
|
||||
drawTrack(canvas, projection, viewRect);
|
||||
|
||||
// Draw the "Start" and "End" markers:
|
||||
// Draw the start and end markers
|
||||
drawMarkers(canvas, projection);
|
||||
|
||||
// Draw the waypoints:
|
||||
|
||||
// Draw the waypoints
|
||||
drawWaypoints(canvas, projection);
|
||||
}
|
||||
|
||||
@@ -271,164 +329,19 @@ public class MapOverlay extends Overlay implements OnSharedPreferenceChangeListe
|
||||
drawMyLocation(canvas, projection);
|
||||
}
|
||||
|
||||
private void drawMarkers(Canvas canvas, Projection projection) {
|
||||
// Draw the "End" marker.
|
||||
if (showEndMarker) {
|
||||
for (int i = getPoints().size() - 1; i >= 0; --i) {
|
||||
if (getPoints().get(i).valid) {
|
||||
drawElement(canvas, projection, getPoints().get(i).geoPoint, endMarker,
|
||||
-markerWidth / 2, -markerHeight);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Draw the "Start" marker.
|
||||
for (int i = 0; i < getPoints().size(); ++i) {
|
||||
if (getPoints().get(i).valid) {
|
||||
drawElement(canvas, projection, getPoints().get(i).geoPoint, startMarker,
|
||||
-markerWidth / 2, -markerHeight);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Visible for testing.
|
||||
Projection getMapProjection(MapView mapView) {
|
||||
return mapView.getProjection();
|
||||
}
|
||||
|
||||
// Visible for testing.
|
||||
Rect getMapViewRect(MapView mapView) {
|
||||
int w = mapView.getLongitudeSpan();
|
||||
int h = mapView.getLatitudeSpan();
|
||||
int cx = mapView.getMapCenter().getLongitudeE6();
|
||||
int cy = mapView.getMapCenter().getLatitudeE6();
|
||||
return new Rect(cx - w / 2, cy - h / 2, cx + w / 2, cy + h / 2);
|
||||
}
|
||||
|
||||
// For use in testing only.
|
||||
public TrackPathPainter getTrackPathPainter() {
|
||||
return trackPathPainter;
|
||||
}
|
||||
|
||||
// For use in testing only.
|
||||
public void setTrackPathPainter(TrackPathPainter trackPathPainter) {
|
||||
this.trackPathPainter = trackPathPainter;
|
||||
}
|
||||
|
||||
private void drawWaypoints(Canvas canvas, Projection projection) {
|
||||
synchronized (waypoints) {;
|
||||
for (Waypoint wpt : waypoints) {
|
||||
Location loc = wpt.getLocation();
|
||||
drawElement(canvas, projection, LocationUtils.getGeoPoint(loc),
|
||||
wpt.getType() == Waypoint.TYPE_STATISTICS ? statsMarker
|
||||
: waypointMarker, -(markerWidth / 2) + 3, -markerHeight);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void drawMyLocation(Canvas canvas, Projection projection) {
|
||||
// Draw the arrow icon.
|
||||
if (myLocation == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Point pt = drawElement(canvas, projection,
|
||||
LocationUtils.getGeoPoint(myLocation), arrows[lastHeading],
|
||||
-(arrowWidth / 2) + 3, -(arrowHeight / 2));
|
||||
// Draw the error circle.
|
||||
float radius = projection.metersToEquatorPixels(myLocation.getAccuracy());
|
||||
canvas.drawCircle(pt.x, pt.y, radius, errorCirclePaint);
|
||||
}
|
||||
|
||||
private void drawTrack(Canvas canvas, Projection projection, Rect viewRect)
|
||||
{
|
||||
boolean draw;
|
||||
|
||||
synchronized (points) {
|
||||
// Merge the pending points with the list of cached locations.
|
||||
final GeoPoint referencePoint = projection.fromPixels(0, 0);
|
||||
int newPoints = pendingPoints.drainTo(points);
|
||||
boolean newProjection = !viewRect.equals(lastViewRect) ||
|
||||
!referencePoint.equals(lastReferencePoint);
|
||||
if (newPoints == 0 && lastPathExists && !newProjection) {
|
||||
// No need to recreate path (same points and viewing area).
|
||||
draw = true;
|
||||
} else {
|
||||
int numPoints = points.size();
|
||||
if (numPoints < 2) {
|
||||
// Not enough points to draw a path.
|
||||
draw = false;
|
||||
} else if (!trackPathPainter.needsRedraw() && lastPathExists && !newProjection) {
|
||||
// Incremental update of the path, without repositioning the view.
|
||||
draw = true;
|
||||
trackPathPainter.updatePath(projection, viewRect, numPoints - newPoints, alwaysVisible, points);
|
||||
} else {
|
||||
// The view has changed so we have to start from scratch.
|
||||
draw = true;
|
||||
trackPathPainter.updatePath(projection, viewRect, 0, alwaysVisible, points);
|
||||
}
|
||||
}
|
||||
lastReferencePoint = referencePoint;
|
||||
lastViewRect = viewRect;
|
||||
}
|
||||
if (draw) {
|
||||
trackPathPainter.drawTrack(canvas);
|
||||
}
|
||||
}
|
||||
|
||||
// Visible for testing.
|
||||
Point drawElement(Canvas canvas, Projection projection, GeoPoint geoPoint,
|
||||
Drawable element, int offsetX, int offsetY) {
|
||||
Point pt = new Point();
|
||||
projection.toPixels(geoPoint, pt);
|
||||
canvas.save();
|
||||
canvas.translate(pt.x + offsetX, pt.y + offsetY);
|
||||
element.draw(canvas);
|
||||
canvas.restore();
|
||||
return pt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the pointer location (will be drawn on next invalidate).
|
||||
*/
|
||||
public void setMyLocation(Location myLocation) {
|
||||
this.myLocation = myLocation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the pointer heading in degrees (will be drawn on next invalidate).
|
||||
*
|
||||
* @return true if the visible heading changed (i.e. a redraw of pointer is
|
||||
* potentially necessary)
|
||||
*/
|
||||
public boolean setHeading(float heading) {
|
||||
int newhdg = Math.round(-heading / 360 * 18 + 180);
|
||||
while (newhdg < 0)
|
||||
newhdg += 18;
|
||||
while (newhdg > 17)
|
||||
newhdg -= 18;
|
||||
if (newhdg != lastHeading) {
|
||||
lastHeading = newhdg;
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onTap(GeoPoint p, MapView mapView) {
|
||||
if (p.equals(mapView.getMapCenter())) {
|
||||
// There is (unfortunately) no good way to determine whether the tap was
|
||||
// caused by an actual tap on the screen or the track ball. If the
|
||||
// location is equal to the map center,then it was a track ball press with
|
||||
// very high likelihood.
|
||||
public boolean onTap(GeoPoint geoPoint, MapView mapView) {
|
||||
if (geoPoint.equals(mapView.getMapCenter())) {
|
||||
/*
|
||||
* There is (unfortunately) no good way to determine whether the tap was
|
||||
* caused by an actual tap on the screen or the track ball. If the
|
||||
* location is equal to the map center,then it was a track ball press with
|
||||
* very high likelihood.
|
||||
*/
|
||||
return false;
|
||||
}
|
||||
|
||||
final Location tapLocation = LocationUtils.getLocation(p);
|
||||
double dmin = Double.MAX_VALUE;
|
||||
final Location tapLocation = LocationUtils.getLocation(geoPoint);
|
||||
double minDistance = Double.MAX_VALUE;
|
||||
Waypoint waypoint = null;
|
||||
synchronized (waypoints) {
|
||||
for (int i = 0; i < waypoints.size(); i++) {
|
||||
@@ -436,35 +349,231 @@ public class MapOverlay extends Overlay implements OnSharedPreferenceChangeListe
|
||||
if (waypointLocation == null) {
|
||||
continue;
|
||||
}
|
||||
final double d = waypointLocation.distanceTo(tapLocation);
|
||||
if (d < dmin) {
|
||||
dmin = d;
|
||||
final double distance = waypointLocation.distanceTo(tapLocation);
|
||||
if (distance < minDistance) {
|
||||
minDistance = distance;
|
||||
waypoint = waypoints.get(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (waypoint != null && dmin < 15000000 / Math.pow(2, mapView.getZoomLevel())) {
|
||||
if (waypoint != null && minDistance < 15000000 / Math.pow(2, mapView.getZoomLevel())) {
|
||||
Intent intent = IntentUtils.newIntent(context, MarkerDetailActivity.class)
|
||||
.putExtra(MarkerDetailActivity.EXTRA_MARKER_ID, waypoint.getId());
|
||||
context.startActivity(intent);
|
||||
return true;
|
||||
}
|
||||
return super.onTap(p, mapView);
|
||||
return super.onTap(geoPoint, mapView);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the points
|
||||
*/
|
||||
public List<CachedLocation> getPoints() {
|
||||
return points;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
|
||||
Log.d(TAG, "MapOverlay: onSharedPreferences changed " + key);
|
||||
if (PreferencesUtils.getKey(context, R.string.track_color_mode_key).equals(key)) {
|
||||
trackPathPainter = TrackPathPainterFactory.getTrackPathPainter(context);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the points.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
public List<CachedLocation> getPoints() {
|
||||
return points;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the track path painter.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
public TrackPathPainter getTrackPathPainter() {
|
||||
return trackPathPainter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the track path painter.
|
||||
*
|
||||
* @param trackPathPainter the track path painter
|
||||
*/
|
||||
@VisibleForTesting
|
||||
public void setTrackPathPainter(TrackPathPainter trackPathPainter) {
|
||||
this.trackPathPainter = trackPathPainter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the map view projection.
|
||||
*
|
||||
* @param mapView the map view
|
||||
*/
|
||||
@VisibleForTesting
|
||||
protected Projection getMapProjection(MapView mapView) {
|
||||
return mapView.getProjection();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the map view Rect.
|
||||
*
|
||||
* @param mapView the map view
|
||||
*/
|
||||
@VisibleForTesting
|
||||
protected Rect getMapViewRect(MapView mapView) {
|
||||
int width = mapView.getLongitudeSpan();
|
||||
int height = mapView.getLatitudeSpan();
|
||||
int centerX = mapView.getMapCenter().getLongitudeE6();
|
||||
int centerY = mapView.getMapCenter().getLatitudeE6();
|
||||
return new Rect(centerX - width / 2, centerY - height / 2, centerX + width / 2, centerY + height
|
||||
/ 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets number of locations.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
int getNumLocations() {
|
||||
synchronized (points) {
|
||||
return points.size() + pendingPoints.size();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets number of waypoints.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
int getNumWaypoints() {
|
||||
synchronized (waypoints) {
|
||||
return waypoints.size();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws the track.
|
||||
*
|
||||
* @param canvas the canvas
|
||||
* @param projection the projection
|
||||
* @param viewRect the view rect
|
||||
*/
|
||||
private void drawTrack(Canvas canvas, Projection projection, Rect viewRect) {
|
||||
boolean draw;
|
||||
|
||||
synchronized (points) {
|
||||
// Merge the pending points with the list of cached locations.
|
||||
GeoPoint referencePoint = projection.fromPixels(0, 0);
|
||||
int newPoints = pendingPoints.drainTo(points);
|
||||
boolean newProjection = !viewRect.equals(lastViewRect)
|
||||
|| !referencePoint.equals(lastReferencePoint);
|
||||
// Call updateState first to trigger its side effects.
|
||||
boolean currentPathValid = !trackPathPainter.updateState() && !newProjection
|
||||
&& trackPathPainter.hasPath();
|
||||
if (newPoints == 0 && currentPathValid) {
|
||||
// No need to update
|
||||
draw = true;
|
||||
} else {
|
||||
int numPoints = points.size();
|
||||
if (numPoints < 2) {
|
||||
// Not enough points to draw a path
|
||||
draw = false;
|
||||
} else if (currentPathValid) {
|
||||
// Incremental update of the path
|
||||
draw = true;
|
||||
trackPathPainter.updatePath(projection, viewRect, numPoints - newPoints, points);
|
||||
} else {
|
||||
// Reload the path
|
||||
draw = true;
|
||||
trackPathPainter.clearPath();
|
||||
trackPathPainter.updatePath(projection, viewRect, 0, points);
|
||||
}
|
||||
}
|
||||
lastReferencePoint = referencePoint;
|
||||
lastViewRect = viewRect;
|
||||
}
|
||||
if (draw) {
|
||||
trackPathPainter.drawPath(canvas);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws the start and end markers.
|
||||
*
|
||||
* @param canvas the canvas
|
||||
* @param projection the projection
|
||||
*/
|
||||
private void drawMarkers(Canvas canvas, Projection projection) {
|
||||
int offsetY = (int) (markerHeight * Constants.MARKER_Y_OFFSET_PERCENTAGE);
|
||||
// Draw the end marker
|
||||
if (showEndMarker) {
|
||||
for (int i = points.size() - 1; i >= 0; i--) {
|
||||
if (points.get(i).valid) {
|
||||
drawElement(
|
||||
canvas, projection, points.get(i).geoPoint, endMarker, -markerWidth / 2, -offsetY);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Draw the start marker
|
||||
for (int i = 0; i < points.size(); i++) {
|
||||
if (points.get(i).valid) {
|
||||
drawElement(
|
||||
canvas, projection, points.get(i).geoPoint, startMarker, -markerWidth / 2, -offsetY);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws the waypoints.
|
||||
*
|
||||
* @param canvas the canvas
|
||||
* @param projection the projection
|
||||
*/
|
||||
private void drawWaypoints(Canvas canvas, Projection projection) {
|
||||
synchronized (waypoints) {
|
||||
int offsetX = (int) (markerWidth * Constants.WAYPOINT_X_OFFSET_PERCENTAGE);
|
||||
for (Waypoint waypoint : waypoints) {
|
||||
Location location = waypoint.getLocation();
|
||||
Drawable drawable = waypoint.getType() == Waypoint.TYPE_STATISTICS ? statsMarker
|
||||
: waypointMarker;
|
||||
drawElement(canvas, projection, LocationUtils.getGeoPoint(location), drawable, -offsetX,
|
||||
-markerHeight);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws my location.
|
||||
*
|
||||
* @param canvas the canvas
|
||||
* @param projection the projection
|
||||
*/
|
||||
private void drawMyLocation(Canvas canvas, Projection projection) {
|
||||
if (myLocation == null) {
|
||||
return;
|
||||
}
|
||||
Point point = drawElement(canvas, projection, LocationUtils.getGeoPoint(myLocation),
|
||||
arrows[headingIndex], -(arrowWidth / 2), -(arrowHeight / 2));
|
||||
// Draw the error circle
|
||||
float radius = projection.metersToEquatorPixels(myLocation.getAccuracy());
|
||||
canvas.drawCircle(point.x, point.y, radius, errorCirclePaint);
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws an element.
|
||||
*
|
||||
* @param canvas the canvas
|
||||
* @param projection the projection
|
||||
* @param geoPoint the geo point
|
||||
* @param drawable the drawable
|
||||
* @param offsetX the x offset
|
||||
* @param offsetY the y offset
|
||||
* @return the point of the drawing.
|
||||
*/
|
||||
private Point drawElement(Canvas canvas, Projection projection, GeoPoint geoPoint,
|
||||
Drawable drawable, int offsetX, int offsetY) {
|
||||
Point point = new Point();
|
||||
projection.toPixels(geoPoint, point);
|
||||
canvas.save();
|
||||
canvas.translate(point.x + offsetX, point.y + offsetY);
|
||||
drawable.draw(canvas);
|
||||
canvas.restore();
|
||||
return point;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,7 +165,7 @@ public class MarkerEditActivity extends AbstractMyTracksActivity {
|
||||
waypointName.getText().toString(),
|
||||
waypointMarkerType.getText().toString(),
|
||||
waypointDescription.getText().toString(),
|
||||
getString(R.string.marker_waypoint_icon_url));
|
||||
null);
|
||||
TrackRecordingServiceConnectionUtils.addMarker(
|
||||
this, trackRecordingServiceConnection, waypointCreationRequest);
|
||||
}
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
*/
|
||||
package com.google.android.apps.mytracks;
|
||||
|
||||
import com.google.android.apps.mytracks.content.TrackDataHub;
|
||||
import com.google.android.apps.mytracks.services.RemoveTempFilesService;
|
||||
import com.google.android.apps.mytracks.util.AnalyticsUtils;
|
||||
import com.google.android.apps.mytracks.util.ApiAdapterFactory;
|
||||
@@ -26,13 +25,11 @@ import android.content.Intent;
|
||||
|
||||
/**
|
||||
* MyTracksApplication for keeping global state.
|
||||
*
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
public class MyTracksApplication extends Application {
|
||||
|
||||
private TrackDataHub trackDataHub;
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
@@ -43,16 +40,4 @@ public class MyTracksApplication extends Application {
|
||||
Intent intent = new Intent(this, RemoveTempFilesService.class);
|
||||
startService(intent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the application's TrackDataHub.
|
||||
*
|
||||
* Note: use synchronized to make sure only one instance is created per application.
|
||||
*/
|
||||
public synchronized TrackDataHub getTrackDataHub() {
|
||||
if (trackDataHub == null) {
|
||||
trackDataHub = TrackDataHub.newInstance(getApplicationContext());
|
||||
}
|
||||
return trackDataHub;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -323,9 +323,10 @@ public class SearchListActivity extends AbstractMyTracksActivity implements Dele
|
||||
String textQuery = intent.getStringExtra(SearchManager.QUERY);
|
||||
setTitle(textQuery);
|
||||
|
||||
Location currentLocation = myTracksLocationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER);
|
||||
final SearchQuery query = new SearchQuery(textQuery, currentLocation, -1L, System
|
||||
.currentTimeMillis());
|
||||
Location currentLocation = myTracksLocationManager.getLastKnownLocation(
|
||||
LocationManager.GPS_PROVIDER);
|
||||
final SearchQuery query = new SearchQuery(
|
||||
textQuery, currentLocation, -1L, System.currentTimeMillis());
|
||||
new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
|
||||
@@ -137,8 +137,7 @@ public class TrackDetailActivity extends AbstractMyTracksActivity implements Del
|
||||
getSharedPreferences(Constants.SETTINGS_NAME, Context.MODE_PRIVATE)
|
||||
.registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
|
||||
trackRecordingServiceConnection = new TrackRecordingServiceConnection(this, null);
|
||||
trackDataHub = ((MyTracksApplication) getApplication()).getTrackDataHub();
|
||||
trackDataHub.loadTrack(trackId);
|
||||
trackDataHub = TrackDataHub.newInstance(this);
|
||||
|
||||
mapViewContainer = getLayoutInflater().inflate(R.layout.map, null);
|
||||
ApiAdapterFactory.getApiAdapter().disableHardwareAccelerated(mapViewContainer);
|
||||
@@ -167,7 +166,6 @@ public class TrackDetailActivity extends AbstractMyTracksActivity implements Del
|
||||
public void onNewIntent(Intent intent) {
|
||||
setIntent(intent);
|
||||
handleIntent(intent);
|
||||
trackDataHub.loadTrack(trackId);
|
||||
showMarker();
|
||||
}
|
||||
|
||||
@@ -180,6 +178,7 @@ public class TrackDetailActivity extends AbstractMyTracksActivity implements Del
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
trackDataHub.loadTrack(trackId);
|
||||
TrackRecordingServiceConnectionUtils.resume(this, trackRecordingServiceConnection);
|
||||
setTitle(trackId == PreferencesUtils.getLong(this, R.string.recording_track_id_key));
|
||||
}
|
||||
@@ -350,12 +349,19 @@ public class TrackDetailActivity extends AbstractMyTracksActivity implements Del
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the mapViewContainer
|
||||
* Gets the map view container.
|
||||
*/
|
||||
public View getMapViewContainer() {
|
||||
return mapViewContainer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the {@link TrackDataHub}.
|
||||
*/
|
||||
public TrackDataHub getTrackDataHub() {
|
||||
return trackDataHub;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the data in the intent.
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,209 @@
|
||||
/*
|
||||
* 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 com.google.android.apps.mytracks.Constants;
|
||||
import com.google.android.apps.mytracks.services.MyTracksLocationManager;
|
||||
import com.google.android.apps.mytracks.util.GoogleLocationUtils;
|
||||
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.net.Uri;
|
||||
import android.util.Log;
|
||||
import android.widget.Toast;
|
||||
|
||||
/**
|
||||
* Data source on the phone.
|
||||
*
|
||||
* @author Rodrigo Damazio
|
||||
*/
|
||||
public class DataSource {
|
||||
|
||||
private static final int NETWORK_PROVIDER_MIN_TIME = 5 * 60 * 1000; // 5 minutes
|
||||
private static final String TAG = DataSource.class.getSimpleName();
|
||||
|
||||
private final Context context;
|
||||
private final ContentResolver contentResolver;
|
||||
private final MyTracksLocationManager myTracksLocationManager;
|
||||
private final SensorManager sensorManager;
|
||||
private final SharedPreferences sharedPreferences;
|
||||
|
||||
public DataSource(Context context) {
|
||||
this.context = context;
|
||||
contentResolver = context.getContentResolver();
|
||||
myTracksLocationManager = new MyTracksLocationManager(context);
|
||||
sensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE);
|
||||
sharedPreferences = context.getSharedPreferences(Constants.SETTINGS_NAME, Context.MODE_PRIVATE);
|
||||
}
|
||||
|
||||
public void close() {
|
||||
myTracksLocationManager.close();
|
||||
}
|
||||
|
||||
public boolean isAllowed() {
|
||||
return myTracksLocationManager.isAllowed();
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a content observer.
|
||||
*
|
||||
* @param uri the uri
|
||||
* @param observer the observer
|
||||
*/
|
||||
public void registerContentObserver(Uri uri, ContentObserver observer) {
|
||||
contentResolver.registerContentObserver(uri, false, observer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregisters a content observer.
|
||||
*
|
||||
* @param observer the observer
|
||||
*/
|
||||
public void unregisterContentObserver(ContentObserver observer) {
|
||||
contentResolver.unregisterContentObserver(observer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a location listener.
|
||||
*
|
||||
* @param listener the listener
|
||||
*/
|
||||
public void registerLocationListener(LocationListener listener) {
|
||||
// Check if the GPS provider exists
|
||||
if (myTracksLocationManager.getProvider(LocationManager.GPS_PROVIDER) == null) {
|
||||
listener.onProviderDisabled(LocationManager.GPS_PROVIDER);
|
||||
unregisterLocationListener(listener);
|
||||
return;
|
||||
}
|
||||
|
||||
// Listen for GPS location
|
||||
myTracksLocationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 0, 0, listener);
|
||||
|
||||
// Update the listener with the current provider state
|
||||
if (myTracksLocationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)) {
|
||||
listener.onProviderEnabled(LocationManager.GPS_PROVIDER);
|
||||
} else {
|
||||
listener.onProviderDisabled(LocationManager.GPS_PROVIDER);
|
||||
}
|
||||
|
||||
// Listen for network location
|
||||
try {
|
||||
myTracksLocationManager.requestLocationUpdates(
|
||||
LocationManager.NETWORK_PROVIDER, NETWORK_PROVIDER_MIN_TIME, 0, listener);
|
||||
} catch (RuntimeException e) {
|
||||
// Network location is optional, so just log the exception
|
||||
Log.w(TAG, "Could not register for network location.", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregisters a location listener.
|
||||
*
|
||||
* @param listener the listener
|
||||
*/
|
||||
public void unregisterLocationListener(LocationListener listener) {
|
||||
myTracksLocationManager.removeUpdates(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the last known location.
|
||||
*/
|
||||
public Location getLastKnownLocation() {
|
||||
Location location = myTracksLocationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER);
|
||||
if (!isLocationRecent(location)) {
|
||||
// Try network location
|
||||
location = myTracksLocationManager.getLastKnownLocation(LocationManager.NETWORK_PROVIDER);
|
||||
String toast;
|
||||
if (isLocationRecent(location)) {
|
||||
toast = context.getString(R.string.my_location_approximate_location);
|
||||
} else {
|
||||
String setting = context.getString(
|
||||
GoogleLocationUtils.isAvailable(context) ? R.string.gps_google_location_settings
|
||||
: R.string.gps_location_access);
|
||||
toast = context.getString(R.string.my_location_no_gps, setting);
|
||||
}
|
||||
Toast.makeText(context, toast, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
return location;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the location is recent.
|
||||
*
|
||||
* @param location the location
|
||||
*/
|
||||
private boolean isLocationRecent(Location location) {
|
||||
if (location == null) {
|
||||
return false;
|
||||
}
|
||||
return location.getTime() > System.currentTimeMillis() - MAX_LOCATION_AGE_MS;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a heading listener.
|
||||
*
|
||||
* @param listener the listener
|
||||
*/
|
||||
public void registerHeadingListener(SensorEventListener listener) {
|
||||
Sensor heading = sensorManager.getDefaultSensor(Sensor.TYPE_ORIENTATION);
|
||||
if (heading == null) {
|
||||
Log.d(TAG, "No heading sensor.");
|
||||
return;
|
||||
}
|
||||
sensorManager.registerListener(listener, heading, SensorManager.SENSOR_DELAY_UI);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregisters a heading listener.
|
||||
*
|
||||
* @param listener the listener
|
||||
*/
|
||||
public void unregisterHeadingListener(SensorEventListener listener) {
|
||||
sensorManager.unregisterListener(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a shared preference change listener.
|
||||
*
|
||||
* @param listener the listener
|
||||
*/
|
||||
public void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {
|
||||
sharedPreferences.registerOnSharedPreferenceChangeListener(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregisters a shared preference change listener.
|
||||
*
|
||||
* @param listener the listener
|
||||
*/
|
||||
public void unregisterOnSharedPreferenceChangeListener(
|
||||
OnSharedPreferenceChangeListener listener) {
|
||||
sharedPreferences.unregisterOnSharedPreferenceChangeListener(listener);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* 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.location.Location;
|
||||
|
||||
/**
|
||||
* Listener to be invoked when {@link DataSource} changes.
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
public interface DataSourceListener {
|
||||
|
||||
/**
|
||||
* Notifies when the tracks table is updated.
|
||||
*/
|
||||
public void notifyTracksTableUpdated();
|
||||
|
||||
/**
|
||||
* Notifies when the waypoints table is updated.
|
||||
*/
|
||||
public void notifyWaypointsTableUpdated();
|
||||
|
||||
/**
|
||||
* Notifies when the track points table is updated.
|
||||
*/
|
||||
public void notifyTrackPointsTableUpdated();
|
||||
|
||||
/**
|
||||
* Notifies when the current location changes.
|
||||
*
|
||||
* @param location the location
|
||||
*/
|
||||
public void notifyLocationChanged(Location location);
|
||||
|
||||
|
||||
/**
|
||||
* Notifies when the location provider enabled status changes.
|
||||
*
|
||||
* @param enabled true if enabled, false if not
|
||||
*/
|
||||
public void notifyLocationProviderEnabled(boolean enabled);
|
||||
|
||||
/**
|
||||
* Notifies when the location provider availability status changes.
|
||||
*
|
||||
* @param available true if available, false if not
|
||||
*/
|
||||
public void notifyLocationProviderAvailable(boolean available);
|
||||
|
||||
/**
|
||||
* Notifies when the compass heading changes.
|
||||
*
|
||||
* @param heading the new heading
|
||||
*/
|
||||
public void notifyHeadingChanged(float heading);
|
||||
|
||||
/**
|
||||
* Notifies when a preference changes.
|
||||
*
|
||||
* @param key the preference key
|
||||
*/
|
||||
public void notifyPreferenceChanged(String key);
|
||||
}
|
||||
@@ -1,8 +1,24 @@
|
||||
/*
|
||||
* 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 com.google.common.annotations.VisibleForTesting;
|
||||
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
|
||||
@@ -10,7 +26,6 @@ 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;
|
||||
@@ -23,87 +38,79 @@ import java.util.EnumSet;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* External data source manager, which converts system-level events into My Tracks data events.
|
||||
*
|
||||
* Data source manager. Creates observers/listeners and manages their
|
||||
* registration with {@link DataSource}. The observers/listeners calls
|
||||
* {@link DataSourceListener} when data changes.
|
||||
*
|
||||
* @author Rodrigo Damazio
|
||||
*/
|
||||
class DataSourceManager {
|
||||
public 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);
|
||||
}
|
||||
/**
|
||||
* Observer when the tracks table is updated.
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
private class TracksTableObserver extends ContentObserver {
|
||||
|
||||
private final DataSourceListener listener;
|
||||
|
||||
/** Observer for when the tracks table is updated. */
|
||||
private class TrackObserver extends ContentObserver {
|
||||
public TrackObserver() {
|
||||
super(contentHandler);
|
||||
public TracksTableObserver() {
|
||||
super(handler);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChange(boolean selfChange) {
|
||||
listener.notifyTrackUpdated();
|
||||
dataSourceListener.notifyTracksTableUpdated();
|
||||
}
|
||||
}
|
||||
|
||||
/** Observer for when the waypoints table is updated. */
|
||||
private class WaypointObserver extends ContentObserver {
|
||||
public WaypointObserver() {
|
||||
super(contentHandler);
|
||||
/**
|
||||
* Observer when the waypoints table is updated.
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
private class WaypointsTableObserver extends ContentObserver {
|
||||
|
||||
public WaypointsTableObserver() {
|
||||
super(handler);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChange(boolean selfChange) {
|
||||
listener.notifyWaypointUpdated();
|
||||
dataSourceListener.notifyWaypointsTableUpdated();
|
||||
}
|
||||
}
|
||||
|
||||
/** Observer for when the points table is updated. */
|
||||
private class PointObserver extends ContentObserver {
|
||||
public PointObserver() {
|
||||
super(contentHandler);
|
||||
/**
|
||||
* Observer when the track points table is updated.
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
private class TrackPointsTableObserver extends ContentObserver {
|
||||
|
||||
public TrackPointsTableObserver() {
|
||||
super(handler);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChange(boolean selfChange) {
|
||||
listener.notifyPointsUpdated();
|
||||
dataSourceListener.notifyTrackPointsTableUpdated();
|
||||
}
|
||||
}
|
||||
|
||||
/** Listener for when preferences change. */
|
||||
private class HubSharedPreferenceListener implements OnSharedPreferenceChangeListener {
|
||||
@Override
|
||||
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
|
||||
listener.notifyPreferenceChanged(key);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Listener for location changes.
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
@VisibleForTesting
|
||||
class CurrentLocationListener implements LocationListener {
|
||||
|
||||
/** 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 (!dataSources.isAllowed() || !LocationManager.GPS_PROVIDER.equals(provider)) {
|
||||
public void onLocationChanged(Location location) {
|
||||
if (!dataSource.isAllowed()) {
|
||||
return;
|
||||
}
|
||||
listener.notifyLocationProviderAvailable(status == LocationProvider.AVAILABLE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onProviderEnabled(String provider) {
|
||||
if (!dataSources.isAllowed() || !LocationManager.GPS_PROVIDER.equals(provider)) {
|
||||
return;
|
||||
}
|
||||
listener.notifyLocationProviderEnabled(true);
|
||||
dataSourceListener.notifyLocationChanged(location);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -111,161 +118,206 @@ class DataSourceManager {
|
||||
if (!LocationManager.GPS_PROVIDER.equals(provider)) {
|
||||
return;
|
||||
}
|
||||
listener.notifyLocationProviderEnabled(false);
|
||||
dataSourceListener.notifyLocationProviderEnabled(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLocationChanged(Location location) {
|
||||
if (!dataSources.isAllowed()) {
|
||||
public void onProviderEnabled(String provider) {
|
||||
if (!dataSource.isAllowed() || !LocationManager.GPS_PROVIDER.equals(provider)) {
|
||||
return;
|
||||
}
|
||||
listener.notifyLocationChanged(location);
|
||||
dataSourceListener.notifyLocationProviderEnabled(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStatusChanged(String provider, int status, Bundle extras) {
|
||||
if (!dataSource.isAllowed() || !LocationManager.GPS_PROVIDER.equals(provider)) {
|
||||
return;
|
||||
}
|
||||
dataSourceListener.notifyLocationProviderAvailable(status == LocationProvider.AVAILABLE);
|
||||
}
|
||||
}
|
||||
|
||||
/** Listener for compass readings. */
|
||||
private class CompassListener implements
|
||||
SensorEventListener {
|
||||
@Override
|
||||
public void onSensorChanged(SensorEvent event) {
|
||||
listener.notifyHeadingChanged(event.values[0]);
|
||||
}
|
||||
/**
|
||||
* Listener for heading changes.
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
@VisibleForTesting
|
||||
class HeadingListener implements SensorEventListener {
|
||||
|
||||
@Override
|
||||
public void onAccuracyChanged(Sensor sensor, int accuracy) {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSensorChanged(SensorEvent event) {
|
||||
dataSourceListener.notifyHeadingChanged(event.values[0]);
|
||||
}
|
||||
}
|
||||
|
||||
/** Wrapper for registering internal listeners. */
|
||||
private final DataSourcesWrapper dataSources;
|
||||
/**
|
||||
* Listener for preference changes.
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
private class PreferenceListener implements OnSharedPreferenceChangeListener {
|
||||
|
||||
// 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();
|
||||
@Override
|
||||
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
|
||||
dataSourceListener.notifyPreferenceChanged(key);
|
||||
}
|
||||
}
|
||||
|
||||
/** Updates the internal (sensor, position, etc) listeners. */
|
||||
void updateAllListeners(EnumSet<ListenerDataType> externallyNeededListeners) {
|
||||
EnumSet<ListenerDataType> neededListeners = EnumSet.copyOf(externallyNeededListeners);
|
||||
private final DataSource dataSource;
|
||||
private final DataSourceListener dataSourceListener;
|
||||
|
||||
// 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);
|
||||
// Registered listeners
|
||||
private final Set<TrackDataType> registeredListeners = EnumSet.noneOf(TrackDataType.class);
|
||||
|
||||
private final Handler handler;
|
||||
private final TracksTableObserver tracksTableObserver;
|
||||
private final WaypointsTableObserver waypointsTableObserver;
|
||||
private final TrackPointsTableObserver trackPointsTableObserver;
|
||||
private final CurrentLocationListener currentLocationListener;
|
||||
private final HeadingListener headingListener;
|
||||
private final PreferenceListener preferenceListener;
|
||||
|
||||
public DataSourceManager(DataSource dataSource, DataSourceListener dataSourceListener) {
|
||||
this.dataSource = dataSource;
|
||||
this.dataSourceListener = dataSourceListener;
|
||||
|
||||
handler = new Handler();
|
||||
tracksTableObserver = new TracksTableObserver();
|
||||
waypointsTableObserver = new WaypointsTableObserver();
|
||||
trackPointsTableObserver = new TrackPointsTableObserver();
|
||||
currentLocationListener = new CurrentLocationListener();
|
||||
headingListener = new HeadingListener();
|
||||
preferenceListener = new PreferenceListener();
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates listeners with data source.
|
||||
*
|
||||
* @param listeners the listeners
|
||||
*/
|
||||
public void updateListeners(EnumSet<TrackDataType> listeners) {
|
||||
EnumSet<TrackDataType> neededListeners = EnumSet.copyOf(listeners);
|
||||
|
||||
/*
|
||||
* Map SAMPLED_OUT_POINT_UPDATES to POINT_UPDATES since they correspond to
|
||||
* the same internal listener
|
||||
*/
|
||||
if (neededListeners.contains(TrackDataType.SAMPLED_OUT_TRACK_POINTS_TABLE)) {
|
||||
neededListeners.remove(TrackDataType.SAMPLED_OUT_TRACK_POINTS_TABLE);
|
||||
neededListeners.add(TrackDataType.SAMPLED_IN_TRACK_POINTS_TABLE);
|
||||
}
|
||||
|
||||
Log.d(TAG, "Updating internal listeners to types " + neededListeners);
|
||||
Log.d(TAG, "Updating listeners " + neededListeners);
|
||||
|
||||
// Unnecessary = registered - needed
|
||||
Set<ListenerDataType> unnecessaryListeners = EnumSet.copyOf(registeredListeners);
|
||||
Set<TrackDataType> unnecessaryListeners = EnumSet.copyOf(registeredListeners);
|
||||
unnecessaryListeners.removeAll(neededListeners);
|
||||
|
||||
// Missing = needed - registered
|
||||
Set<ListenerDataType> missingListeners = EnumSet.copyOf(neededListeners);
|
||||
Set<TrackDataType> missingListeners = EnumSet.copyOf(neededListeners);
|
||||
missingListeners.removeAll(registeredListeners);
|
||||
|
||||
// Remove all unnecessary listeners.
|
||||
for (ListenerDataType type : unnecessaryListeners) {
|
||||
unregisterListener(type);
|
||||
// Remove unnecessary listeners
|
||||
for (TrackDataType trackDataType : unnecessaryListeners) {
|
||||
unregisterListener(trackDataType);
|
||||
}
|
||||
|
||||
// Add all missing listeners.
|
||||
for (ListenerDataType type : missingListeners) {
|
||||
registerListener(type);
|
||||
// Add missing listeners
|
||||
for (TrackDataType trackDataType : missingListeners) {
|
||||
registerListener(trackDataType);
|
||||
}
|
||||
|
||||
// Now all needed types are registered.
|
||||
// Update registered listeners
|
||||
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);
|
||||
}
|
||||
/**
|
||||
* Registers a listener with data source.
|
||||
*
|
||||
* @param trackDataType the listener data type
|
||||
*/
|
||||
private void registerListener(TrackDataType trackDataType) {
|
||||
switch (trackDataType) {
|
||||
case SELECTED_TRACK:
|
||||
// Do nothing
|
||||
break;
|
||||
}
|
||||
case LOCATION_UPDATES:
|
||||
dataSources.requestLocationUpdates(locationListener);
|
||||
case TRACKS_TABLE:
|
||||
dataSource.registerContentObserver(TracksColumns.CONTENT_URI, tracksTableObserver);
|
||||
break;
|
||||
case POINT_UPDATES:
|
||||
dataSources.registerContentObserver(
|
||||
TrackPointsColumns.CONTENT_URI, false, pointObserver);
|
||||
case WAYPOINTS_TABLE:
|
||||
dataSource.registerContentObserver(WaypointsColumns.CONTENT_URI, waypointsTableObserver);
|
||||
break;
|
||||
case TRACK_UPDATES:
|
||||
dataSources.registerContentObserver(TracksColumns.CONTENT_URI, false, trackObserver);
|
||||
case SAMPLED_IN_TRACK_POINTS_TABLE:
|
||||
dataSource.registerContentObserver(
|
||||
TrackPointsColumns.CONTENT_URI, trackPointsTableObserver);
|
||||
break;
|
||||
case WAYPOINT_UPDATES:
|
||||
dataSources.registerContentObserver(
|
||||
WaypointsColumns.CONTENT_URI, false, waypointObserver);
|
||||
case SAMPLED_OUT_TRACK_POINTS_TABLE:
|
||||
// Do nothing. SAMPLED_OUT_POINT_UPDATES is mapped to POINT_UPDATES.
|
||||
break;
|
||||
case DISPLAY_PREFERENCES:
|
||||
dataSources.registerOnSharedPreferenceChangeListener(preferenceListener);
|
||||
case LOCATION:
|
||||
dataSource.registerLocationListener(currentLocationListener);
|
||||
break;
|
||||
case HEADING:
|
||||
dataSource.registerHeadingListener(headingListener);
|
||||
break;
|
||||
case PREFERENCE:
|
||||
dataSource.registerOnSharedPreferenceChangeListener(preferenceListener);
|
||||
break;
|
||||
default:
|
||||
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);
|
||||
/**
|
||||
* Unregisters a listener with data source.
|
||||
*
|
||||
* @param trackDataType listener data type
|
||||
*/
|
||||
private void unregisterListener(TrackDataType trackDataType) {
|
||||
switch (trackDataType) {
|
||||
case SELECTED_TRACK:
|
||||
// Do nothing
|
||||
break;
|
||||
case LOCATION_UPDATES:
|
||||
dataSources.removeLocationUpdates(locationListener);
|
||||
case TRACKS_TABLE:
|
||||
dataSource.unregisterContentObserver(tracksTableObserver);
|
||||
break;
|
||||
case POINT_UPDATES:
|
||||
dataSources.unregisterContentObserver(pointObserver);
|
||||
case WAYPOINTS_TABLE:
|
||||
dataSource.unregisterContentObserver(waypointsTableObserver);
|
||||
break;
|
||||
case TRACK_UPDATES:
|
||||
dataSources.unregisterContentObserver(trackObserver);
|
||||
case SAMPLED_IN_TRACK_POINTS_TABLE:
|
||||
dataSource.unregisterContentObserver(trackPointsTableObserver);
|
||||
break;
|
||||
case WAYPOINT_UPDATES:
|
||||
dataSources.unregisterContentObserver(waypointObserver);
|
||||
case SAMPLED_OUT_TRACK_POINTS_TABLE:
|
||||
// Do nothing. SAMPLED_OUT_POINT_UPDATES is mapped to POINT_UPDATES.
|
||||
break;
|
||||
case DISPLAY_PREFERENCES:
|
||||
dataSources.unregisterOnSharedPreferenceChangeListener(preferenceListener);
|
||||
case LOCATION:
|
||||
dataSource.unregisterLocationListener(currentLocationListener);
|
||||
break;
|
||||
case HEADING:
|
||||
dataSource.unregisterHeadingListener(headingListener);
|
||||
break;
|
||||
case PREFERENCE:
|
||||
dataSource.unregisterOnSharedPreferenceChangeListener(preferenceListener);
|
||||
break;
|
||||
default:
|
||||
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);
|
||||
/**
|
||||
* Unregisters all listeners with data source.
|
||||
*/
|
||||
public void unregisterAllListeners() {
|
||||
for (TrackDataType trackDataType : TrackDataType.values()) {
|
||||
unregisterListener(trackDataType);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
/*
|
||||
* 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();
|
||||
void close();
|
||||
boolean isAllowed();
|
||||
}
|
||||
@@ -1,181 +0,0 @@
|
||||
/*
|
||||
* 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.apps.mytracks.services.MyTracksLocationManager;
|
||||
import com.google.android.apps.mytracks.util.GoogleLocationUtils;
|
||||
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 MyTracksLocationManager myTracksLocationManager;
|
||||
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.myTracksLocationManager = new MyTracksLocationManager(context);
|
||||
this.contentResolver = context.getContentResolver();
|
||||
this.sharedPreferences = sharedPreferences;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
myTracksLocationManager.close();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAllowed() {
|
||||
return myTracksLocationManager.isAllowed();
|
||||
}
|
||||
|
||||
@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 myTracksLocationManager.isProviderEnabled(provider);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void requestLocationUpdates(LocationListener listener) {
|
||||
// Check if the provider exists.
|
||||
LocationProvider gpsProvider = myTracksLocationManager.getProvider(LocationManager.GPS_PROVIDER);
|
||||
if (gpsProvider == null) {
|
||||
listener.onProviderDisabled(LocationManager.GPS_PROVIDER);
|
||||
myTracksLocationManager.removeUpdates(listener);
|
||||
return;
|
||||
}
|
||||
|
||||
// Listen to GPS location.
|
||||
String providerName = gpsProvider.getName();
|
||||
Log.d(Constants.TAG, "TrackDataHub: Using location provider " + providerName);
|
||||
myTracksLocationManager.requestLocationUpdates(providerName,
|
||||
0 /*minTime*/, 0 /*minDist*/, listener);
|
||||
|
||||
// Give an initial update on provider state.
|
||||
if (myTracksLocationManager.isProviderEnabled(providerName)) {
|
||||
listener.onProviderEnabled(providerName);
|
||||
} else {
|
||||
listener.onProviderDisabled(providerName);
|
||||
}
|
||||
|
||||
// Listen to network location
|
||||
try {
|
||||
myTracksLocationManager.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 = myTracksLocationManager.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 = myTracksLocationManager.getLastKnownLocation(LocationManager.NETWORK_PROVIDER);
|
||||
|
||||
String toast = context.getString(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:
|
||||
String setting = context.getString(
|
||||
GoogleLocationUtils.isAvailable(context) ? R.string.gps_google_location_settings
|
||||
: R.string.gps_location_access);
|
||||
toast = context.getString(R.string.my_location_no_gps, setting);
|
||||
}
|
||||
|
||||
// Let the user know we have only an approximate location:
|
||||
Toast.makeText(context, toast, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
return loc;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeLocationUpdates(LocationListener listener) {
|
||||
myTracksLocationManager.removeUpdates(listener);
|
||||
}
|
||||
}
|
||||
@@ -76,8 +76,8 @@ public class DescriptionGeneratorImpl implements DescriptionGenerator {
|
||||
}
|
||||
|
||||
@Override
|
||||
public String generateWaypointDescription(Waypoint waypoint) {
|
||||
return generateTripStatisticsDescription(waypoint.getTripStatistics(), false);
|
||||
public String generateWaypointDescription(TripStatistics tripStatistics) {
|
||||
return generateTripStatisticsDescription(tripStatistics, false);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -16,9 +16,9 @@
|
||||
|
||||
package com.google.android.apps.mytracks.content;
|
||||
|
||||
import com.google.android.apps.mytracks.Constants;
|
||||
import com.google.android.apps.mytracks.util.PreferencesUtils;
|
||||
import com.google.android.maps.mytracks.R;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
|
||||
import android.content.ContentProvider;
|
||||
import android.content.ContentUris;
|
||||
@@ -38,64 +38,65 @@ import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
/**
|
||||
* A provider that handles recorded (GPS) tracks and their track points.
|
||||
*
|
||||
* A {@link ContentProvider} that handles access to track points, tracks, and
|
||||
* waypoints tables.
|
||||
*
|
||||
* @author Leif Hendrik Wilden
|
||||
*/
|
||||
public class MyTracksProvider extends ContentProvider {
|
||||
|
||||
private static final String DATABASE_NAME = "mytracks.db";
|
||||
private static final int DATABASE_VERSION = 20;
|
||||
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 TAG = MyTracksProvider.class.getSimpleName();
|
||||
@VisibleForTesting
|
||||
static final String DATABASE_NAME = "mytracks.db";
|
||||
private static final int DATABASE_VERSION = 20;
|
||||
|
||||
/**
|
||||
* Helper which creates or upgrades the database if necessary.
|
||||
* Database helper for creating and upgrading the database.
|
||||
*/
|
||||
private static class DatabaseHelper extends SQLiteOpenHelper {
|
||||
|
||||
@VisibleForTesting
|
||||
static class DatabaseHelper extends SQLiteOpenHelper {
|
||||
|
||||
public DatabaseHelper(Context context) {
|
||||
super(context, DATABASE_NAME, null, DATABASE_VERSION);
|
||||
this(context, DATABASE_NAME);
|
||||
}
|
||||
|
||||
|
||||
@VisibleForTesting
|
||||
public DatabaseHelper(Context context, String databaseName) {
|
||||
super(context, databaseName, null, DATABASE_VERSION);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(SQLiteDatabase db) {
|
||||
db.execSQL(TrackPointsColumns.CREATE_TABLE);
|
||||
db.execSQL(TracksColumns.CREATE_TABLE);
|
||||
db.execSQL(WaypointsColumns.CREATE_TABLE);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
|
||||
Log.w(TAG, "Upgrading database from version " + oldVersion + " to " + newVersion);
|
||||
if (oldVersion < 17) {
|
||||
// Delete the old data
|
||||
Log.w(TAG, "Delete all old data");
|
||||
Log.w(TAG, "Deleting all old data.");
|
||||
db.execSQL("DROP TABLE IF EXISTS " + TrackPointsColumns.TABLE_NAME);
|
||||
db.execSQL("DROP TABLE IF EXISTS " + TracksColumns.TABLE_NAME);
|
||||
db.execSQL("DROP TABLE IF EXISTS " + WaypointsColumns.TABLE_NAME);
|
||||
onCreate(db);
|
||||
} else {
|
||||
// Incremental updates go here. For each DB version, add a corresponding if clause.
|
||||
|
||||
// Track points sensor column
|
||||
// Incremental upgrades. One if statement per DB version.
|
||||
|
||||
// Add track points SENSOR column
|
||||
if (oldVersion <= 17) {
|
||||
Log.w(TAG, "Upgrade DB: Adding sensor column.");
|
||||
db.execSQL("ALTER TABLE " + TrackPointsColumns.TABLE_NAME + " ADD "
|
||||
+ TrackPointsColumns.SENSOR + " BLOB");
|
||||
}
|
||||
// Tracks table id column
|
||||
// Add tracks TABLEID column
|
||||
if (oldVersion <= 18) {
|
||||
Log.w(TAG, "Upgrade DB: Adding tableid column.");
|
||||
db.execSQL("ALTER TABLE " + TracksColumns.TABLE_NAME + " ADD " + TracksColumns.TABLEID
|
||||
+ " STRING");
|
||||
}
|
||||
// Tracks table icon column
|
||||
// Add tracks ICON column
|
||||
if (oldVersion <= 19) {
|
||||
Log.w(TAG, "Upgrade DB: Adding icon column.");
|
||||
db.execSQL(
|
||||
@@ -105,42 +106,55 @@ public class MyTracksProvider extends ContentProvider {
|
||||
}
|
||||
}
|
||||
|
||||
private final UriMatcher urlMatcher;
|
||||
/**
|
||||
* Types of url.
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
@VisibleForTesting
|
||||
enum UrlType {
|
||||
TRACKPOINTS, TRACKPOINTS_ID, TRACKS, TRACKS_ID, WAYPOINTS, WAYPOINTS_ID
|
||||
}
|
||||
|
||||
private final UriMatcher uriMatcher;
|
||||
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 {
|
||||
return PreferencesUtils.getBoolean(
|
||||
getContext(), R.string.allow_access_key, PreferencesUtils.ALLOW_ACCESS_DEFAULT);
|
||||
}
|
||||
uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
|
||||
uriMatcher.addURI(MyTracksProviderUtils.AUTHORITY, TrackPointsColumns.TABLE_NAME,
|
||||
UrlType.TRACKPOINTS.ordinal());
|
||||
uriMatcher.addURI(MyTracksProviderUtils.AUTHORITY, TrackPointsColumns.TABLE_NAME + "/#",
|
||||
UrlType.TRACKPOINTS_ID.ordinal());
|
||||
uriMatcher.addURI(
|
||||
MyTracksProviderUtils.AUTHORITY, TracksColumns.TABLE_NAME, UrlType.TRACKS.ordinal());
|
||||
uriMatcher.addURI(MyTracksProviderUtils.AUTHORITY, TracksColumns.TABLE_NAME + "/#",
|
||||
UrlType.TRACKS_ID.ordinal());
|
||||
uriMatcher.addURI(
|
||||
MyTracksProviderUtils.AUTHORITY, WaypointsColumns.TABLE_NAME, UrlType.WAYPOINTS.ordinal());
|
||||
uriMatcher.addURI(MyTracksProviderUtils.AUTHORITY, WaypointsColumns.TABLE_NAME + "/#",
|
||||
UrlType.WAYPOINTS_ID.ordinal());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreate() {
|
||||
return onCreate(getContext());
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to make onCreate is testable.
|
||||
* @param context context to creates database
|
||||
* @return true means run successfully
|
||||
*/
|
||||
@VisibleForTesting
|
||||
boolean onCreate(Context context) {
|
||||
if (!canAccess()) {
|
||||
return false;
|
||||
}
|
||||
DatabaseHelper dbHelper = new DatabaseHelper(getContext());
|
||||
DatabaseHelper databaseHelper = new DatabaseHelper(context);
|
||||
try {
|
||||
db = dbHelper.getWritableDatabase();
|
||||
db = databaseHelper.getWritableDatabase();
|
||||
} catch (SQLiteException e) {
|
||||
Log.e(TAG, "Unable to open database for writing", e);
|
||||
Log.e(TAG, "Unable to open database for writing.", e);
|
||||
}
|
||||
return db != null;
|
||||
}
|
||||
@@ -152,7 +166,7 @@ public class MyTracksProvider extends ContentProvider {
|
||||
}
|
||||
String table;
|
||||
boolean shouldVacuum = false;
|
||||
switch (urlMatcher.match(url)) {
|
||||
switch (getUrlType(url)) {
|
||||
case TRACKPOINTS:
|
||||
table = TrackPointsColumns.TABLE_NAME;
|
||||
break;
|
||||
@@ -166,17 +180,23 @@ public class MyTracksProvider extends ContentProvider {
|
||||
default:
|
||||
throw new IllegalArgumentException("Unknown URL " + url);
|
||||
}
|
||||
|
||||
Log.w(MyTracksProvider.TAG, "provider delete in " + table + "!");
|
||||
int count = db.delete(table, where, selectionArgs);
|
||||
|
||||
Log.w(MyTracksProvider.TAG, "Deleting table " + table);
|
||||
int count;
|
||||
try {
|
||||
db.beginTransaction();
|
||||
count = db.delete(table, where, selectionArgs);
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
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");
|
||||
// If a potentially large amount of data was deleted, reclaim its space.
|
||||
Log.i(TAG, "Vacuuming the database.");
|
||||
db.execSQL("VACUUM");
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
@@ -185,7 +205,7 @@ public class MyTracksProvider extends ContentProvider {
|
||||
if (!canAccess()) {
|
||||
return null;
|
||||
}
|
||||
switch (urlMatcher.match(url)) {
|
||||
switch (getUrlType(url)) {
|
||||
case TRACKPOINTS:
|
||||
return TrackPointsColumns.CONTENT_TYPE;
|
||||
case TRACKPOINTS_ID:
|
||||
@@ -208,197 +228,240 @@ public class MyTracksProvider extends ContentProvider {
|
||||
if (!canAccess()) {
|
||||
return null;
|
||||
}
|
||||
Log.d(MyTracksProvider.TAG, "MyTracksProvider.insert");
|
||||
ContentValues values;
|
||||
if (initialValues != null) {
|
||||
values = initialValues;
|
||||
} else {
|
||||
values = new ContentValues();
|
||||
if (initialValues == null) {
|
||||
initialValues = 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);
|
||||
Uri result = null;
|
||||
try {
|
||||
db.beginTransaction();
|
||||
result = insertContentValues(url, getUrlType(url), initialValues);
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
@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);
|
||||
|
||||
UrlType urlType = getUrlType(url);
|
||||
for (numInserted = 0; numInserted < valuesBulk.length; numInserted++) {
|
||||
ContentValues values = valuesBulk[numInserted];
|
||||
if (values == null) { values = new ContentValues(); }
|
||||
|
||||
insertType(url, urlMatch, values);
|
||||
ContentValues contentValues = valuesBulk[numInserted];
|
||||
if (contentValues == null) {
|
||||
contentValues = new ContentValues();
|
||||
}
|
||||
insertContentValues(url, urlType, contentValues);
|
||||
}
|
||||
|
||||
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(TrackPointsColumns.TABLE_NAME, 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(TracksColumns.TABLE_NAME, 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(WaypointsColumns.TABLE_NAME, 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) {
|
||||
Uri url, String[] projection, String selection, String[] selectionArgs, String sort) {
|
||||
if (!canAccess()) {
|
||||
return null;
|
||||
}
|
||||
SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
|
||||
int match = urlMatcher.match(url);
|
||||
SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
|
||||
String sortOrder = null;
|
||||
if (match == TRACKPOINTS) {
|
||||
qb.setTables(TrackPointsColumns.TABLE_NAME);
|
||||
if (sort != null) {
|
||||
sortOrder = sort;
|
||||
} else {
|
||||
sortOrder = TrackPointsColumns.DEFAULT_SORT_ORDER;
|
||||
}
|
||||
} else if (match == TRACKPOINTS_ID) {
|
||||
qb.setTables(TrackPointsColumns.TABLE_NAME);
|
||||
qb.appendWhere("_id=" + url.getPathSegments().get(1));
|
||||
} else if (match == TRACKS) {
|
||||
qb.setTables(TracksColumns.TABLE_NAME);
|
||||
if (sort != null) {
|
||||
sortOrder = sort;
|
||||
} else {
|
||||
sortOrder = TracksColumns.DEFAULT_SORT_ORDER;
|
||||
}
|
||||
} else if (match == TRACKS_ID) {
|
||||
qb.setTables(TracksColumns.TABLE_NAME);
|
||||
qb.appendWhere("_id=" + url.getPathSegments().get(1));
|
||||
} else if (match == WAYPOINTS) {
|
||||
qb.setTables(WaypointsColumns.TABLE_NAME);
|
||||
if (sort != null) {
|
||||
sortOrder = sort;
|
||||
} else {
|
||||
sortOrder = WaypointsColumns.DEFAULT_SORT_ORDER;
|
||||
}
|
||||
} else if (match == WAYPOINTS_ID) {
|
||||
qb.setTables(WaypointsColumns.TABLE_NAME);
|
||||
qb.appendWhere("_id=" + url.getPathSegments().get(1));
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unknown URL " + url);
|
||||
switch (getUrlType(url)) {
|
||||
case TRACKPOINTS:
|
||||
queryBuilder.setTables(TrackPointsColumns.TABLE_NAME);
|
||||
sortOrder = sort != null ? sort : TrackPointsColumns.DEFAULT_SORT_ORDER;
|
||||
break;
|
||||
case TRACKPOINTS_ID:
|
||||
queryBuilder.setTables(TrackPointsColumns.TABLE_NAME);
|
||||
queryBuilder.appendWhere("_id=" + url.getPathSegments().get(1));
|
||||
break;
|
||||
case TRACKS:
|
||||
queryBuilder.setTables(TracksColumns.TABLE_NAME);
|
||||
sortOrder = sort != null ? sort : TracksColumns.DEFAULT_SORT_ORDER;
|
||||
break;
|
||||
case TRACKS_ID:
|
||||
queryBuilder.setTables(TracksColumns.TABLE_NAME);
|
||||
queryBuilder.appendWhere("_id=" + url.getPathSegments().get(1));
|
||||
break;
|
||||
case WAYPOINTS:
|
||||
queryBuilder.setTables(WaypointsColumns.TABLE_NAME);
|
||||
sortOrder = sort != null ? sort : WaypointsColumns.DEFAULT_SORT_ORDER;
|
||||
break;
|
||||
case WAYPOINTS_ID:
|
||||
queryBuilder.setTables(WaypointsColumns.TABLE_NAME);
|
||||
queryBuilder.appendWhere("_id=" + url.getPathSegments().get(1));
|
||||
break;
|
||||
default:
|
||||
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;
|
||||
Cursor cursor = queryBuilder.query(
|
||||
db, projection, selection, selectionArgs, null, null, sortOrder);
|
||||
cursor.setNotificationUri(getContext().getContentResolver(), url);
|
||||
return cursor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int update(Uri url, ContentValues values, String where,
|
||||
String[] selectionArgs) {
|
||||
public int update(Uri url, ContentValues values, String where, String[] selectionArgs) {
|
||||
if (!canAccess()) {
|
||||
return 0;
|
||||
}
|
||||
String table;
|
||||
String whereClause;
|
||||
switch (getUrlType(url)) {
|
||||
case TRACKPOINTS:
|
||||
table = TrackPointsColumns.TABLE_NAME;
|
||||
whereClause = where;
|
||||
break;
|
||||
case TRACKPOINTS_ID:
|
||||
table = TrackPointsColumns.TABLE_NAME;
|
||||
whereClause = TrackPointsColumns._ID + "=" + url.getPathSegments().get(1);
|
||||
if (!TextUtils.isEmpty(where)) {
|
||||
whereClause += " AND (" + where + ")";
|
||||
}
|
||||
break;
|
||||
case TRACKS:
|
||||
table = TracksColumns.TABLE_NAME;
|
||||
whereClause = where;
|
||||
break;
|
||||
case TRACKS_ID:
|
||||
table = TracksColumns.TABLE_NAME;
|
||||
whereClause = TracksColumns._ID + "=" + url.getPathSegments().get(1);
|
||||
if (!TextUtils.isEmpty(where)) {
|
||||
whereClause += " AND (" + where + ")";
|
||||
}
|
||||
break;
|
||||
case WAYPOINTS:
|
||||
table = WaypointsColumns.TABLE_NAME;
|
||||
whereClause = where;
|
||||
break;
|
||||
case WAYPOINTS_ID:
|
||||
table = WaypointsColumns.TABLE_NAME;
|
||||
whereClause = WaypointsColumns._ID + "=" + url.getPathSegments().get(1);
|
||||
if (!TextUtils.isEmpty(where)) {
|
||||
whereClause += " AND (" + where + ")";
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("Unknown url " + url);
|
||||
}
|
||||
int count;
|
||||
int match = urlMatcher.match(url);
|
||||
if (match == TRACKPOINTS) {
|
||||
count = db.update(TrackPointsColumns.TABLE_NAME, values, where, selectionArgs);
|
||||
} else if (match == TRACKPOINTS_ID) {
|
||||
String segment = url.getPathSegments().get(1);
|
||||
count = db.update(TrackPointsColumns.TABLE_NAME, values, "_id=" + segment
|
||||
+ (!TextUtils.isEmpty(where)
|
||||
? " AND (" + where + ')'
|
||||
: ""),
|
||||
selectionArgs);
|
||||
} else if (match == TRACKS) {
|
||||
count = db.update(TracksColumns.TABLE_NAME, values, where, selectionArgs);
|
||||
} else if (match == TRACKS_ID) {
|
||||
String segment = url.getPathSegments().get(1);
|
||||
count = db.update(TracksColumns.TABLE_NAME, values, "_id=" + segment
|
||||
+ (!TextUtils.isEmpty(where)
|
||||
? " AND (" + where + ')'
|
||||
: ""),
|
||||
selectionArgs);
|
||||
} else if (match == WAYPOINTS) {
|
||||
count = db.update(WaypointsColumns.TABLE_NAME, values, where, selectionArgs);
|
||||
} else if (match == WAYPOINTS_ID) {
|
||||
String segment = url.getPathSegments().get(1);
|
||||
count = db.update(WaypointsColumns.TABLE_NAME, values, "_id=" + segment
|
||||
+ (!TextUtils.isEmpty(where)
|
||||
? " AND (" + where + ')'
|
||||
: ""),
|
||||
selectionArgs);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unknown URL " + url);
|
||||
try {
|
||||
db.beginTransaction();
|
||||
count = db.update(table, values, whereClause, selectionArgs);
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
getContext().getContentResolver().notifyChange(url, null, true);
|
||||
return count;
|
||||
}
|
||||
|
||||
}
|
||||
/**
|
||||
* Returns true if the caller can access the content provider.
|
||||
*/
|
||||
private boolean canAccess() {
|
||||
if (Binder.getCallingPid() == Process.myPid()) {
|
||||
return true;
|
||||
} else {
|
||||
return PreferencesUtils.getBoolean(
|
||||
getContext(), R.string.allow_access_key, PreferencesUtils.ALLOW_ACCESS_DEFAULT);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the {@link UrlType} for a url.
|
||||
*
|
||||
* @param url the url
|
||||
*/
|
||||
private UrlType getUrlType(Uri url) {
|
||||
return UrlType.values()[uriMatcher.match(url)];
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts a content based on the url type.
|
||||
*
|
||||
* @param url the content url
|
||||
* @param urlType the url type
|
||||
* @param contentValues the content values
|
||||
*/
|
||||
private Uri insertContentValues(Uri url, UrlType urlType, ContentValues contentValues) {
|
||||
switch (urlType) {
|
||||
case TRACKPOINTS:
|
||||
return insertTrackPoint(url, contentValues);
|
||||
case TRACKS:
|
||||
return insertTrack(url, contentValues);
|
||||
case WAYPOINTS:
|
||||
return insertWaypoint(url, contentValues);
|
||||
default:
|
||||
throw new IllegalArgumentException("Unknown url " + url);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts a track point.
|
||||
*
|
||||
* @param url the content url
|
||||
* @param values the content values
|
||||
*/
|
||||
private Uri insertTrackPoint(Uri url, ContentValues values) {
|
||||
boolean hasLatitude = values.containsKey(TrackPointsColumns.LATITUDE);
|
||||
boolean hasLongitude = values.containsKey(TrackPointsColumns.LONGITUDE);
|
||||
boolean hasTime = values.containsKey(TrackPointsColumns.TIME);
|
||||
if (!hasLatitude || !hasLongitude || !hasTime) {
|
||||
throw new IllegalArgumentException("Latitude, longitude, and time values are required.");
|
||||
}
|
||||
long rowId = db.insert(TrackPointsColumns.TABLE_NAME, 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 a track point " + url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts a track.
|
||||
*
|
||||
* @param url the content url
|
||||
* @param contentValues the content values
|
||||
*/
|
||||
private Uri insertTrack(Uri url, ContentValues contentValues) {
|
||||
boolean hasStartTime = contentValues.containsKey(TracksColumns.STARTTIME);
|
||||
boolean hasStartId = contentValues.containsKey(TracksColumns.STARTID);
|
||||
if (!hasStartTime || !hasStartId) {
|
||||
throw new IllegalArgumentException("Both start time and start id values are required.");
|
||||
}
|
||||
long rowId = db.insert(TracksColumns.TABLE_NAME, TracksColumns._ID, contentValues);
|
||||
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 a track " + url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts a waypoint.
|
||||
*
|
||||
* @param url the content url
|
||||
* @param contentValues the content values
|
||||
*/
|
||||
private Uri insertWaypoint(Uri url, ContentValues contentValues) {
|
||||
long rowId = db.insert(WaypointsColumns.TABLE_NAME, WaypointsColumns._ID, contentValues);
|
||||
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 a waypoint " + url);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -13,138 +13,131 @@
|
||||
* 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;
|
||||
package com.google.android.apps.mytracks.content;
|
||||
|
||||
import android.location.Location;
|
||||
|
||||
/**
|
||||
* Listener for track data, for both initial and incremental loading.
|
||||
*
|
||||
* Listener for track data changes.
|
||||
*
|
||||
* @author Rodrigo Damazio
|
||||
*/
|
||||
public interface TrackDataListener {
|
||||
|
||||
/** States for the GPS location provider. */
|
||||
public enum ProviderState {
|
||||
DISABLED,
|
||||
NO_FIX,
|
||||
BAD_FIX,
|
||||
GOOD_FIX;
|
||||
/**
|
||||
* Location state.
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
public enum LocationState {
|
||||
DISABLED, NO_FIX, BAD_FIX, GOOD_FIX;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the location provider changes state.
|
||||
* Called when the location state changes.
|
||||
*/
|
||||
void onProviderStateChange(ProviderState state);
|
||||
public void onLocationStateChanged(LocationState locationState);
|
||||
|
||||
/**
|
||||
* 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).
|
||||
* Called when the location changes. This is meant for location display only,
|
||||
* track point data is reported with other methods like
|
||||
* {@link #onSampledInTrackPoint(Location)} and
|
||||
* {@link #onSampledOutTrackPoint(Location)}.
|
||||
*
|
||||
* @param track the selected track, or null if no track is selected
|
||||
* @param isRecording whether we're currently recording the selected track
|
||||
* @param location the location
|
||||
*/
|
||||
void onSelectedTrackChanged(Track track, boolean isRecording);
|
||||
public void onLocationChanged(Location location);
|
||||
|
||||
/**
|
||||
* Called when the track and/or its statistics have been updated.
|
||||
*
|
||||
* @param track the updated version of the track
|
||||
* Called when the heading changes.
|
||||
*
|
||||
* @param heading the heading
|
||||
*/
|
||||
void onTrackUpdated(Track track);
|
||||
public void onHeadingChanged(double heading);
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* Called when the selected track changes. This will be followed by calls to
|
||||
* data methods such as {@link #onTrackUpdated(Track)},
|
||||
* {@link #clearTrackPoints()}, {@link #onSampledInTrackPoint(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 true if the selected track is recording
|
||||
*/
|
||||
void clearTrackPoints();
|
||||
public void onSelectedTrackChanged(Track track, boolean isRecording);
|
||||
|
||||
/**
|
||||
* 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
|
||||
* Called when the track or its statistics has been updated.
|
||||
*
|
||||
* @param track the track
|
||||
*/
|
||||
void onNewTrackPoint(Location loc);
|
||||
public void onTrackUpdated(Track track);
|
||||
|
||||
/**
|
||||
* 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
|
||||
* Called to clear previously-sent track points.
|
||||
*/
|
||||
void onSampledOutTrackPoint(Location loc);
|
||||
public void clearTrackPoints();
|
||||
|
||||
/**
|
||||
* Called when an invalid point (representing a segment split) is read.
|
||||
* Called when a sampled in track point is read.
|
||||
*
|
||||
* @param location the location
|
||||
*/
|
||||
void onSegmentSplit();
|
||||
public void onSampledInTrackPoint(Location location);
|
||||
|
||||
/**
|
||||
* 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}.
|
||||
* Called when a sampled out track point is read.
|
||||
*
|
||||
* @param location the location
|
||||
*/
|
||||
void onNewTrackPointsDone();
|
||||
public void onSampledOutTrackPoint(Location location);
|
||||
|
||||
/**
|
||||
* Called to clear any previously-sent waypoints.
|
||||
* This can be called at any time that we decide the data needs to be
|
||||
* reloaded.
|
||||
* Called when an invalid track point representing a segment split is read.
|
||||
*/
|
||||
void clearWaypoints();
|
||||
public void onSegmentSplit();
|
||||
|
||||
/**
|
||||
* Called when finish sending new track points. This gets called after every
|
||||
* batch of calls to {@link #onSampledInTrackPoint(Location)},
|
||||
* {@link #onSampledOutTrackPoint(Location)} and {@link #onSegmentSplit()}.
|
||||
*/
|
||||
public void onNewTrackPointsDone();
|
||||
|
||||
/**
|
||||
* Called to clear previously sent waypoints.
|
||||
*/
|
||||
public void clearWaypoints();
|
||||
|
||||
/**
|
||||
* Called when a new waypoint is read.
|
||||
*
|
||||
* @param wpt the new waypoint
|
||||
*
|
||||
* @param waypoint the waypoint
|
||||
*/
|
||||
void onNewWaypoint(Waypoint wpt);
|
||||
public void onNewWaypoint(Waypoint waypoint);
|
||||
|
||||
/**
|
||||
* 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}.
|
||||
* Called when finish sending new waypoints. This gets called after every
|
||||
* batch of calls to {@link #clearWaypoints()} and
|
||||
* {@link #onNewWaypoint(Waypoint)}.
|
||||
*/
|
||||
void onNewWaypointsDone();
|
||||
public 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
|
||||
* Called when the metric units preference value is change.
|
||||
*
|
||||
* @param metricUnits true to use metric units, false to use imperial units
|
||||
* @return true to reload all the data, false otherwise.
|
||||
*/
|
||||
boolean onUnitsChanged(boolean metric);
|
||||
public boolean onMetricUnitsChanged(boolean metricUnits);
|
||||
|
||||
/**
|
||||
* 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
|
||||
* Called when the report speed preference value is changed.
|
||||
*
|
||||
* @param reportSpeed true to report speed, false to report pace
|
||||
* @return true to reload all the data, false otherwise.
|
||||
*/
|
||||
boolean onReportSpeedChanged(boolean reportSpeed);
|
||||
public boolean onReportSpeedChanged(boolean reportSpeed);
|
||||
}
|
||||
|
||||
@@ -1,159 +0,0 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
// 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 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 = 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);
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
public int getNumListeners() {
|
||||
return registeredListeners.size();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
/*
|
||||
* 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 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;
|
||||
|
||||
/**
|
||||
* Manages register/unregister {@link TrackDataListener} and keeping the state
|
||||
* for each registered listener.
|
||||
*
|
||||
* @author Rodrigo Damazio
|
||||
*/
|
||||
public class TrackDataManager {
|
||||
|
||||
// Map of listener to its track data types
|
||||
private final Map<TrackDataListener, EnumSet<TrackDataType>>
|
||||
listenerToTypesMap = new HashMap<TrackDataListener, EnumSet<TrackDataType>>();
|
||||
|
||||
// Map of track data type to listeners
|
||||
private final Map<TrackDataType, Set<TrackDataListener>>
|
||||
typeToListenersMap = new EnumMap<TrackDataType, Set<TrackDataListener>>(TrackDataType.class);
|
||||
|
||||
public TrackDataManager() {
|
||||
for (TrackDataType trackDataType : TrackDataType.values()) {
|
||||
typeToListenersMap.put(trackDataType, new LinkedHashSet<TrackDataListener>());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a listener.
|
||||
*
|
||||
* @param listener the listener
|
||||
* @param trackDataTypes the track data types the listener is interested
|
||||
*/
|
||||
public void registerListener(
|
||||
TrackDataListener listener, EnumSet<TrackDataType> trackDataTypes) {
|
||||
if (listenerToTypesMap.containsKey(listener)) {
|
||||
throw new IllegalStateException("Listener is already registered");
|
||||
}
|
||||
listenerToTypesMap.put(listener, trackDataTypes);
|
||||
for (TrackDataType trackDataType : trackDataTypes) {
|
||||
typeToListenersMap.get(trackDataType).add(listener);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregisters a listener.
|
||||
*
|
||||
* @param listener the listener
|
||||
*/
|
||||
public void unregisterListener(TrackDataListener listener) {
|
||||
EnumSet<TrackDataType> removedTypes = listenerToTypesMap.remove(listener);
|
||||
if (removedTypes == null) {
|
||||
Log.w(TAG, "Tried to unregister a listener that is not registered.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove the listener from the typeToListenersMap
|
||||
for (TrackDataType trackDataType : removedTypes) {
|
||||
typeToListenersMap.get(trackDataType).remove(listener);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the number of {@link TrackDataListener}.
|
||||
*/
|
||||
public int getNumberOfListeners() {
|
||||
return listenerToTypesMap.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the track data types for a listener.
|
||||
*
|
||||
* @param listener the listener
|
||||
*/
|
||||
public EnumSet<TrackDataType> getTrackDataTypes(TrackDataListener listener) {
|
||||
return listenerToTypesMap.get(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the listeners for a {@link TrackDataType}.
|
||||
*
|
||||
* @param type the type
|
||||
*/
|
||||
public Set<TrackDataListener> getListeners(TrackDataType type) {
|
||||
return typeToListenersMap.get(type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all the registered {@link TrackDataType}.
|
||||
*/
|
||||
public EnumSet<TrackDataType> getRegisteredTrackDataTypes() {
|
||||
EnumSet<TrackDataType> types = EnumSet.noneOf(TrackDataType.class);
|
||||
for (EnumSet<TrackDataType> value : listenerToTypesMap.values()) {
|
||||
types.addAll(value);
|
||||
}
|
||||
// Always include preference
|
||||
types.add(TrackDataType.PREFERENCE);
|
||||
return types;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* Types of track data.
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
public enum TrackDataType {
|
||||
SELECTED_TRACK, // selected track changes
|
||||
TRACKS_TABLE, // tracks table changes
|
||||
WAYPOINTS_TABLE, // waypoints table changes
|
||||
SAMPLED_IN_TRACK_POINTS_TABLE, // sampled-in track points table changes
|
||||
SAMPLED_OUT_TRACK_POINTS_TABLE, // sampled-out track points table changes
|
||||
LOCATION, // location changes
|
||||
HEADING, // heading changes
|
||||
PREFERENCE // preference changes
|
||||
}
|
||||
@@ -18,14 +18,14 @@ package com.google.android.apps.mytracks.fragments;
|
||||
|
||||
import com.google.android.apps.mytracks.ChartView;
|
||||
import com.google.android.apps.mytracks.Constants;
|
||||
import com.google.android.apps.mytracks.MyTracksApplication;
|
||||
import com.google.android.apps.mytracks.TrackDetailActivity;
|
||||
import com.google.android.apps.mytracks.content.MyTracksLocation;
|
||||
import com.google.android.apps.mytracks.content.Sensor;
|
||||
import com.google.android.apps.mytracks.content.Sensor.SensorDataSet;
|
||||
import com.google.android.apps.mytracks.content.Track;
|
||||
import com.google.android.apps.mytracks.content.TrackDataHub;
|
||||
import com.google.android.apps.mytracks.content.TrackDataHub.ListenerDataType;
|
||||
import com.google.android.apps.mytracks.content.TrackDataListener;
|
||||
import com.google.android.apps.mytracks.content.TrackDataType;
|
||||
import com.google.android.apps.mytracks.content.Waypoint;
|
||||
import com.google.android.apps.mytracks.stats.DoubleBuffer;
|
||||
import com.google.android.apps.mytracks.stats.TripStatisticsBuilder;
|
||||
@@ -96,7 +96,7 @@ public class ChartFragment extends Fragment implements TrackDataListener {
|
||||
|
||||
zoomControls.setIsZoomInEnabled(chartView.canZoomIn());
|
||||
zoomControls.setIsZoomOutEnabled(chartView.canZoomOut());
|
||||
chartView.setShowPointer(isRecording());
|
||||
chartView.setShowPointer(isSelectedTrackRecording());
|
||||
chartView.invalidate();
|
||||
}
|
||||
};
|
||||
@@ -164,17 +164,17 @@ public class ChartFragment extends Fragment implements TrackDataListener {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onProviderStateChange(ProviderState state) {
|
||||
public void onLocationStateChanged(LocationState state) {
|
||||
// We don't care.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCurrentLocationChanged(Location loc) {
|
||||
public void onLocationChanged(Location loc) {
|
||||
// We don't care.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCurrentHeadingChanged(double heading) {
|
||||
public void onHeadingChanged(double heading) {
|
||||
// We don't care.
|
||||
}
|
||||
|
||||
@@ -213,7 +213,7 @@ public class ChartFragment extends Fragment implements TrackDataListener {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNewTrackPoint(Location location) {
|
||||
public void onSampledInTrackPoint(Location location) {
|
||||
if (LocationUtils.isValidLocation(location)) {
|
||||
double[] data = new double[ChartView.NUM_SERIES + 1];
|
||||
fillDataPoint(location, data);
|
||||
@@ -259,7 +259,7 @@ public class ChartFragment extends Fragment implements TrackDataListener {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onUnitsChanged(boolean metric) {
|
||||
public boolean onMetricUnitsChanged(boolean metric) {
|
||||
if (metricUnits == metric) {
|
||||
return false;
|
||||
}
|
||||
@@ -358,14 +358,14 @@ public class ChartFragment extends Fragment implements TrackDataListener {
|
||||
* accessed by multiple threads.
|
||||
*/
|
||||
private synchronized void resumeTrackDataHub() {
|
||||
trackDataHub = ((MyTracksApplication) getActivity().getApplication()).getTrackDataHub();
|
||||
trackDataHub = ((TrackDetailActivity) getActivity()).getTrackDataHub();
|
||||
trackDataHub.registerTrackDataListener(this, EnumSet.of(
|
||||
ListenerDataType.SELECTED_TRACK_CHANGED,
|
||||
ListenerDataType.TRACK_UPDATES,
|
||||
ListenerDataType.WAYPOINT_UPDATES,
|
||||
ListenerDataType.POINT_UPDATES,
|
||||
ListenerDataType.SAMPLED_OUT_POINT_UPDATES,
|
||||
ListenerDataType.DISPLAY_PREFERENCES));
|
||||
TrackDataType.SELECTED_TRACK,
|
||||
TrackDataType.TRACKS_TABLE,
|
||||
TrackDataType.WAYPOINTS_TABLE,
|
||||
TrackDataType.SAMPLED_IN_TRACK_POINTS_TABLE,
|
||||
TrackDataType.SAMPLED_OUT_TRACK_POINTS_TABLE,
|
||||
TrackDataType.PREFERENCE));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -378,11 +378,11 @@ public class ChartFragment extends Fragment implements TrackDataListener {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if recording. Needs to be synchronized because trackDataHub
|
||||
* can be accessed by multiple threads.
|
||||
* Returns true if the selected track is recording. Needs to be synchronized
|
||||
* because trackDataHub can be accessed by multiple threads.
|
||||
*/
|
||||
private synchronized boolean isRecording() {
|
||||
return trackDataHub != null && trackDataHub.isRecordingSelected();
|
||||
private synchronized boolean isSelectedTrackRecording() {
|
||||
return trackDataHub != null && trackDataHub.isSelectedTrackRecording();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -17,14 +17,13 @@
|
||||
package com.google.android.apps.mytracks.fragments;
|
||||
|
||||
import com.google.android.apps.mytracks.MapOverlay;
|
||||
import com.google.android.apps.mytracks.MyTracksApplication;
|
||||
import com.google.android.apps.mytracks.TrackDetailActivity;
|
||||
import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
|
||||
import com.google.android.apps.mytracks.content.MyTracksProviderUtils.Factory;
|
||||
import com.google.android.apps.mytracks.content.Track;
|
||||
import com.google.android.apps.mytracks.content.TrackDataHub;
|
||||
import com.google.android.apps.mytracks.content.TrackDataHub.ListenerDataType;
|
||||
import com.google.android.apps.mytracks.content.TrackDataListener;
|
||||
import com.google.android.apps.mytracks.content.TrackDataType;
|
||||
import com.google.android.apps.mytracks.content.Waypoint;
|
||||
import com.google.android.apps.mytracks.stats.TripStatistics;
|
||||
import com.google.android.apps.mytracks.util.ApiAdapterFactory;
|
||||
@@ -275,7 +274,7 @@ public class MapFragment extends Fragment
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onProviderStateChange(ProviderState state) {
|
||||
public void onLocationStateChanged(LocationState state) {
|
||||
final String message;
|
||||
final boolean isGpsDisabled;
|
||||
switch (state) {
|
||||
@@ -322,13 +321,13 @@ public class MapFragment extends Fragment
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCurrentLocationChanged(Location location) {
|
||||
public void onLocationChanged(Location location) {
|
||||
currentLocation = location;
|
||||
updateCurrentLocation();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCurrentHeadingChanged(double heading) {
|
||||
public void onHeadingChanged(double heading) {
|
||||
if (mapOverlay.setHeading((float) heading)) {
|
||||
mapView.postInvalidate();
|
||||
}
|
||||
@@ -369,7 +368,7 @@ public class MapFragment extends Fragment
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNewTrackPoint(Location location) {
|
||||
public void onSampledInTrackPoint(Location location) {
|
||||
if (LocationUtils.isValidLocation(location)) {
|
||||
mapOverlay.addLocation(location);
|
||||
}
|
||||
@@ -409,7 +408,7 @@ public class MapFragment extends Fragment
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onUnitsChanged(boolean metric) {
|
||||
public boolean onMetricUnitsChanged(boolean metric) {
|
||||
// We don't care.
|
||||
return false;
|
||||
}
|
||||
@@ -425,13 +424,13 @@ public class MapFragment extends Fragment
|
||||
* accessed by multiple threads.
|
||||
*/
|
||||
private synchronized void resumeTrackDataHub() {
|
||||
trackDataHub = ((MyTracksApplication) getActivity().getApplication()).getTrackDataHub();
|
||||
trackDataHub = ((TrackDetailActivity) getActivity()).getTrackDataHub();
|
||||
trackDataHub.registerTrackDataListener(this, EnumSet.of(
|
||||
ListenerDataType.SELECTED_TRACK_CHANGED,
|
||||
ListenerDataType.WAYPOINT_UPDATES,
|
||||
ListenerDataType.POINT_UPDATES,
|
||||
ListenerDataType.LOCATION_UPDATES,
|
||||
ListenerDataType.COMPASS_UPDATES));
|
||||
TrackDataType.SELECTED_TRACK,
|
||||
TrackDataType.WAYPOINTS_TABLE,
|
||||
TrackDataType.SAMPLED_IN_TRACK_POINTS_TABLE,
|
||||
TrackDataType.LOCATION,
|
||||
TrackDataType.HEADING));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -16,11 +16,11 @@
|
||||
|
||||
package com.google.android.apps.mytracks.fragments;
|
||||
|
||||
import com.google.android.apps.mytracks.MyTracksApplication;
|
||||
import com.google.android.apps.mytracks.TrackDetailActivity;
|
||||
import com.google.android.apps.mytracks.content.Track;
|
||||
import com.google.android.apps.mytracks.content.TrackDataHub;
|
||||
import com.google.android.apps.mytracks.content.TrackDataHub.ListenerDataType;
|
||||
import com.google.android.apps.mytracks.content.TrackDataListener;
|
||||
import com.google.android.apps.mytracks.content.TrackDataType;
|
||||
import com.google.android.apps.mytracks.content.Waypoint;
|
||||
import com.google.android.apps.mytracks.stats.TripStatistics;
|
||||
import com.google.android.apps.mytracks.util.PreferencesUtils;
|
||||
@@ -61,7 +61,7 @@ public class StatsFragment extends Fragment implements TrackDataListener {
|
||||
// A runnable to update the total time field.
|
||||
private final Runnable updateTotalTime = new Runnable() {
|
||||
public void run() {
|
||||
if (isRecording()) {
|
||||
if (isSelectedTrackRecording()) {
|
||||
StatsUtils.setTotalTimeValue(getActivity(), System.currentTimeMillis() - startTime);
|
||||
}
|
||||
}
|
||||
@@ -117,8 +117,8 @@ public class StatsFragment extends Fragment implements TrackDataListener {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onProviderStateChange(ProviderState state) {
|
||||
if (isResumed() && (state == ProviderState.DISABLED || state == ProviderState.NO_FIX)) {
|
||||
public void onLocationStateChanged(LocationState state) {
|
||||
if (isResumed() && (state == LocationState.DISABLED || state == LocationState.NO_FIX)) {
|
||||
getActivity().runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
@@ -130,8 +130,8 @@ public class StatsFragment extends Fragment implements TrackDataListener {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCurrentLocationChanged(final Location location) {
|
||||
if (isResumed() && isRecording()) {
|
||||
public void onLocationChanged(final Location location) {
|
||||
if (isResumed() && isSelectedTrackRecording()) {
|
||||
getActivity().runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
@@ -143,7 +143,7 @@ public class StatsFragment extends Fragment implements TrackDataListener {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCurrentHeadingChanged(double heading) {
|
||||
public void onHeadingChanged(double heading) {
|
||||
// We don't care.
|
||||
}
|
||||
|
||||
@@ -175,7 +175,7 @@ public class StatsFragment extends Fragment implements TrackDataListener {
|
||||
lastTripStatistics = track.getTripStatistics();
|
||||
|
||||
startTime = track.getTripStatistics().getStartTime();
|
||||
if (!isRecording()) {
|
||||
if (!isSelectedTrackRecording()) {
|
||||
lastLocation = null;
|
||||
}
|
||||
updateUi();
|
||||
@@ -190,7 +190,7 @@ public class StatsFragment extends Fragment implements TrackDataListener {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNewTrackPoint(Location loc) {
|
||||
public void onSampledInTrackPoint(Location loc) {
|
||||
// We don't care.
|
||||
}
|
||||
|
||||
@@ -225,7 +225,7 @@ public class StatsFragment extends Fragment implements TrackDataListener {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onUnitsChanged(final boolean metric) {
|
||||
public boolean onMetricUnitsChanged(final boolean metric) {
|
||||
if (isResumed()) {
|
||||
getActivity().runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
@@ -255,12 +255,12 @@ public class StatsFragment extends Fragment implements TrackDataListener {
|
||||
* be accessed by multiple threads.
|
||||
*/
|
||||
private synchronized void resumeTrackDataHub() {
|
||||
trackDataHub = ((MyTracksApplication) getActivity().getApplication()).getTrackDataHub();
|
||||
trackDataHub = ((TrackDetailActivity) getActivity()).getTrackDataHub();
|
||||
trackDataHub.registerTrackDataListener(this, EnumSet.of(
|
||||
ListenerDataType.SELECTED_TRACK_CHANGED,
|
||||
ListenerDataType.TRACK_UPDATES,
|
||||
ListenerDataType.LOCATION_UPDATES,
|
||||
ListenerDataType.DISPLAY_PREFERENCES));
|
||||
TrackDataType.SELECTED_TRACK,
|
||||
TrackDataType.TRACKS_TABLE,
|
||||
TrackDataType.LOCATION,
|
||||
TrackDataType.PREFERENCE));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -273,11 +273,11 @@ public class StatsFragment extends Fragment implements TrackDataListener {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if recording. Needs to be synchronized because trackDataHub
|
||||
* can be accessed by multiple threads.
|
||||
* Returns true if the selected track is recording. Needs to be synchronized
|
||||
* because trackDataHub can be accessed by multiple threads.
|
||||
*/
|
||||
private synchronized boolean isRecording() {
|
||||
return trackDataHub != null && trackDataHub.isRecordingSelected();
|
||||
private synchronized boolean isSelectedTrackRecording() {
|
||||
return trackDataHub != null && trackDataHub.isSelectedTrackRecording();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -56,26 +56,31 @@ import java.util.Locale;
|
||||
* @author Matthew Simmons
|
||||
*/
|
||||
public class SendDocsUtils {
|
||||
private static final String GET_SPREADSHEET_BY_TITLE_URI =
|
||||
@VisibleForTesting
|
||||
public static final String GET_SPREADSHEET_BY_TITLE_URI =
|
||||
"https://docs.google.com/feeds/documents/private/full?"
|
||||
+ "category=mine,spreadsheet&title=%s&title-exact=true";
|
||||
private static final String CREATE_SPREADSHEET_URI =
|
||||
"https://docs.google.com/feeds/documents/private/full";
|
||||
private static final String GET_WORKSHEETS_URI =
|
||||
"https://spreadsheets.google.com/feeds/worksheets/%s/private/full";
|
||||
private static final String GET_WORKSHEET_URI =
|
||||
@VisibleForTesting
|
||||
public static final String GET_WORKSHEET_URI =
|
||||
"https://spreadsheets.google.com/feeds/list/%s/%s/private/full";
|
||||
|
||||
private static final String SPREADSHEET_ID_PREFIX =
|
||||
@VisibleForTesting
|
||||
public static final String SPREADSHEET_ID_PREFIX =
|
||||
"https://docs.google.com/feeds/documents/private/full/spreadsheet%3A";
|
||||
|
||||
private static final String CONTENT_TYPE = "Content-Type";
|
||||
private static final String ATOM_FEED_MIME_TYPE = "application/atom+xml";
|
||||
@VisibleForTesting
|
||||
public static final String CONTENT_TYPE = "Content-Type";
|
||||
@VisibleForTesting
|
||||
public static final String ATOM_FEED_MIME_TYPE = "application/atom+xml";
|
||||
private static final String OPENDOCUMENT_SPREADSHEET_MIME_TYPE =
|
||||
"application/x-vnd.oasis.opendocument.spreadsheet";
|
||||
|
||||
private static final String AUTHORIZATION = "Authorization";
|
||||
private static final String AUTHORIZATION_PREFIX = "GoogleLogin auth=";
|
||||
@VisibleForTesting
|
||||
public static final String AUTHORIZATION = "Authorization";
|
||||
@VisibleForTesting
|
||||
public static final String AUTHORIZATION_PREFIX = "GoogleLogin auth=";
|
||||
|
||||
private static final String SLUG = "Slug";
|
||||
|
||||
|
||||
@@ -306,7 +306,7 @@ public class GpxImporter extends DefaultHandler {
|
||||
tripStatisticsBuilder = new TripStatisticsBuilder(0);
|
||||
tripStatisticsBuilder.pauseAt(0);
|
||||
}
|
||||
track.setTripStatistics(tripStatisticsBuilder.getStatistics());
|
||||
track.setTripStatistics(tripStatisticsBuilder.getTripStatistics());
|
||||
track.setNumberOfPoints(numberOfLocations);
|
||||
myTracksProviderUtils.updateTrack(track);
|
||||
tracksIds.add(track.getId());
|
||||
@@ -527,7 +527,7 @@ public class GpxImporter extends DefaultHandler {
|
||||
*/
|
||||
private long getLastPointId() {
|
||||
flushPoints();
|
||||
return myTracksProviderUtils.getLastLocationId(track.getId());
|
||||
return myTracksProviderUtils.getLastTrackLocationId(track.getId());
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -169,6 +169,7 @@ public class TcxTrackWriter implements TrackFormatWriter {
|
||||
public void writeEndTrack(Location lastPoint) {
|
||||
if (printWriter != null) {
|
||||
printWriter.println("</Lap>");
|
||||
printWriter.println("<Notes>" + StringUtils.formatCData(track.getDescription()) + "</Notes>");
|
||||
printWriter.println("<Creator xsi:type=\"Device_t\">");
|
||||
printWriter.println("<Name>"
|
||||
+ StringUtils.formatCData(context.getString(R.string.send_google_by_my_tracks, "", ""))
|
||||
|
||||
+11
-6
@@ -27,6 +27,7 @@ import com.google.api.client.http.HttpResponse;
|
||||
import com.google.api.client.http.HttpTransport;
|
||||
import com.google.api.client.http.InputStreamContent;
|
||||
import com.google.api.client.util.Strings;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
|
||||
import android.accounts.Account;
|
||||
import android.accounts.AccountManager;
|
||||
@@ -52,13 +53,17 @@ import java.util.Vector;
|
||||
*/
|
||||
public class SendFusionTablesAsyncTask extends AbstractSendAsyncTask {
|
||||
|
||||
private static final String APP_NAME_PREFIX = "Google-MyTracks-";
|
||||
@VisibleForTesting
|
||||
public static final String APP_NAME_PREFIX = "Google-MyTracks-";
|
||||
private static final String SQL_KEY = "sql=";
|
||||
private static final String CONTENT_TYPE = "application/x-www-form-urlencoded";
|
||||
private static final String FUSION_TABLES_BASE_URL =
|
||||
@VisibleForTesting
|
||||
public static final String CONTENT_TYPE = "application/x-www-form-urlencoded";
|
||||
@VisibleForTesting
|
||||
public static final String FUSION_TABLES_BASE_URL =
|
||||
"https://www.google.com/fusiontables/api/query";
|
||||
private static final int MAX_POINTS_PER_UPLOAD = 2048;
|
||||
private static final String GDATA_VERSION = "2";
|
||||
@VisibleForTesting
|
||||
public static final String GDATA_VERSION = "2";
|
||||
|
||||
private static final int PROGRESS_CREATE_TABLE = 0;
|
||||
private static final int PROGRESS_UNLIST_TABLE = 5;
|
||||
@@ -248,7 +253,7 @@ public class SendFusionTablesAsyncTask extends AbstractSendAsyncTask {
|
||||
elevationBuffer.setNext(metricUnits ? location.getAltitude()
|
||||
: location.getAltitude() * UnitConversions.M_TO_FT);
|
||||
if (i % elevationSamplingFrequency == 0) {
|
||||
distances.add(tripStatisticsBuilder.getStatistics().getTotalDistance());
|
||||
distances.add(tripStatisticsBuilder.getTripStatistics().getTotalDistance());
|
||||
elevations.add(elevationBuffer.getAverage());
|
||||
}
|
||||
lastLocation = location;
|
||||
@@ -274,7 +279,7 @@ public class SendFusionTablesAsyncTask extends AbstractSendAsyncTask {
|
||||
|
||||
// Create an end marker
|
||||
if (lastLocation != null) {
|
||||
distances.add(tripStatisticsBuilder.getStatistics().getTotalDistance());
|
||||
distances.add(tripStatisticsBuilder.getTripStatistics().getTotalDistance());
|
||||
elevations.add(elevationBuffer.getAverage());
|
||||
DescriptionGenerator descriptionGenerator = new DescriptionGeneratorImpl(context);
|
||||
track.setDescription(
|
||||
|
||||
@@ -297,7 +297,7 @@ public class SendMapsAsyncTask extends AbstractSendAsyncTask {
|
||||
elevationBuffer.setNext(metricUnits ? location.getAltitude()
|
||||
: location.getAltitude() * UnitConversions.M_TO_FT);
|
||||
if (i % elevationSamplingFrequency == 0) {
|
||||
distances.add(tripStatisticsBuilder.getStatistics().getTotalDistance());
|
||||
distances.add(tripStatisticsBuilder.getTripStatistics().getTotalDistance());
|
||||
elevations.add(elevationBuffer.getAverage());
|
||||
}
|
||||
lastLocation = location;
|
||||
@@ -323,7 +323,7 @@ public class SendMapsAsyncTask extends AbstractSendAsyncTask {
|
||||
|
||||
// Create an end marker
|
||||
if (lastLocation != null) {
|
||||
distances.add(tripStatisticsBuilder.getStatistics().getTotalDistance());
|
||||
distances.add(tripStatisticsBuilder.getTripStatistics().getTotalDistance());
|
||||
elevations.add(elevationBuffer.getAverage());
|
||||
|
||||
track.setDescription(getTrackDescription(track, distances, elevations));
|
||||
|
||||
+59
-68
@@ -13,6 +13,7 @@
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
|
||||
package com.google.android.apps.mytracks.maps;
|
||||
|
||||
import static com.google.android.apps.mytracks.Constants.TAG;
|
||||
@@ -36,60 +37,43 @@ import android.util.Log;
|
||||
*
|
||||
* @author Vangelis S.
|
||||
*/
|
||||
public class DynamicSpeedTrackPathDescriptor implements TrackPathDescriptor,
|
||||
OnSharedPreferenceChangeListener {
|
||||
public class DynamicSpeedTrackPathDescriptor
|
||||
implements TrackPathDescriptor, OnSharedPreferenceChangeListener {
|
||||
|
||||
private final Context context;
|
||||
private int speedMargin;
|
||||
private int slowSpeed;
|
||||
private int normalSpeed;
|
||||
private int speedMargin;
|
||||
private double averageMovingSpeed;
|
||||
private final Context context;
|
||||
|
||||
@VisibleForTesting
|
||||
static final int CRITICAL_DIFFERENCE_PERCENTAGE = 20;
|
||||
|
||||
public DynamicSpeedTrackPathDescriptor(Context context) {
|
||||
this.context = context;
|
||||
context.getSharedPreferences(Constants.SETTINGS_NAME, Context.MODE_PRIVATE)
|
||||
.registerOnSharedPreferenceChangeListener(this);
|
||||
|
||||
speedMargin = PreferencesUtils.getInt(context, R.string.track_color_mode_percentage_key,
|
||||
PreferencesUtils.TRACK_COLOR_MODE_PERCENTAGE_DEFAULT);
|
||||
context.getSharedPreferences(Constants.SETTINGS_NAME, Context.MODE_PRIVATE)
|
||||
.registerOnSharedPreferenceChangeListener(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the slow speed calculated based on the % below the average speed.
|
||||
*
|
||||
* @return The speed limit considered as slow.
|
||||
*/
|
||||
@Override
|
||||
public int getSlowSpeed() {
|
||||
slowSpeed = (int) (averageMovingSpeed - (averageMovingSpeed * speedMargin / 100));
|
||||
slowSpeed = (int) (averageMovingSpeed - (averageMovingSpeed * speedMargin / 100.0));
|
||||
return slowSpeed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the medium speed calculated based on the % above the average speed.
|
||||
*
|
||||
* @return The speed limit considered as normal.
|
||||
*/
|
||||
@Override
|
||||
public int getNormalSpeed() {
|
||||
normalSpeed = (int) (averageMovingSpeed + (averageMovingSpeed * speedMargin / 100));
|
||||
normalSpeed = (int) (averageMovingSpeed + (averageMovingSpeed * speedMargin / 100.0));
|
||||
return normalSpeed;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
|
||||
Log.d(TAG, "DynamicSpeedTrackPathDescriptor: onSharedPreferences changed " + key);
|
||||
if (PreferencesUtils.getKey(context, R.string.track_color_mode_percentage_key).equals(key)) {
|
||||
speedMargin = PreferencesUtils.getInt(context, R.string.track_color_mode_percentage_key,
|
||||
PreferencesUtils.TRACK_COLOR_MODE_PERCENTAGE_DEFAULT);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean needsRedraw() {
|
||||
public boolean updateState() {
|
||||
long selectedTrackId = PreferencesUtils.getLong(context, R.string.selected_track_id_key);
|
||||
if (selectedTrackId == PreferencesUtils.SELECTED_TRACK_ID_DEFAULT) {
|
||||
// Could not find track.
|
||||
Log.d(TAG, "No selected track id.");
|
||||
return false;
|
||||
}
|
||||
Track track = MyTracksProviderUtils.Factory.get(context).getTrack(selectedTrackId);
|
||||
@@ -97,40 +81,48 @@ public class DynamicSpeedTrackPathDescriptor implements TrackPathDescriptor,
|
||||
Log.d(TAG, "No track for " + selectedTrackId);
|
||||
return false;
|
||||
}
|
||||
TripStatistics stats = track.getTripStatistics();
|
||||
TripStatistics tripStatistics = track.getTripStatistics();
|
||||
double newAverageMovingSpeed = (int) Math.floor(
|
||||
stats.getAverageMovingSpeed() * UnitConversions.MS_TO_KMH);
|
||||
tripStatistics.getAverageMovingSpeed() * UnitConversions.MS_TO_KMH);
|
||||
|
||||
return isDifferenceSignificant(averageMovingSpeed, newAverageMovingSpeed);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the old speed and the new speed differ significantly or not.
|
||||
*/
|
||||
public boolean isDifferenceSignificant(double oldAverageMovingSpeed, double newAverageMovingSpeed) {
|
||||
if (oldAverageMovingSpeed == 0) {
|
||||
if (newAverageMovingSpeed == 0) {
|
||||
return false;
|
||||
} else {
|
||||
averageMovingSpeed = newAverageMovingSpeed;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Here, both oldAverageMovingSpeed and newAverageMovingSpeed are not zero.
|
||||
double maxValue = Math.max(oldAverageMovingSpeed, newAverageMovingSpeed);
|
||||
double differencePercentage = Math.abs(oldAverageMovingSpeed - newAverageMovingSpeed)
|
||||
/ maxValue * 100;
|
||||
if (differencePercentage >= CRITICAL_DIFFERENCE_PERCENTAGE) {
|
||||
if (isDifferenceSignificant(averageMovingSpeed, newAverageMovingSpeed)) {
|
||||
averageMovingSpeed = newAverageMovingSpeed;
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
|
||||
if (PreferencesUtils.getKey(context, R.string.track_color_mode_percentage_key).equals(key)) {
|
||||
speedMargin = PreferencesUtils.getInt(context, R.string.track_color_mode_percentage_key,
|
||||
PreferencesUtils.TRACK_COLOR_MODE_PERCENTAGE_DEFAULT);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the value of variable speedMargin to check the result of test.
|
||||
* @return the value of speedMargin.
|
||||
* Returns true if the average moving speed and the new average moving speed
|
||||
* are significantly different.
|
||||
*
|
||||
* @param oldAverageMovingSpeed
|
||||
* @param newAverageMovingSpeed
|
||||
*/
|
||||
@VisibleForTesting
|
||||
boolean isDifferenceSignificant(double oldAverageMovingSpeed, double newAverageMovingSpeed) {
|
||||
if (oldAverageMovingSpeed == 0) {
|
||||
return newAverageMovingSpeed != 0;
|
||||
}
|
||||
|
||||
// Here, both oldAverageMovingSpeed and newAverageMovingSpeed are not zero.
|
||||
double maxValue = Math.max(oldAverageMovingSpeed, newAverageMovingSpeed);
|
||||
double differencePercentage = Math.abs(oldAverageMovingSpeed - newAverageMovingSpeed) / maxValue
|
||||
* 100.0;
|
||||
return differencePercentage >= CRITICAL_DIFFERENCE_PERCENTAGE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the speed margin.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
int getSpeedMargin() {
|
||||
@@ -138,21 +130,20 @@ public class DynamicSpeedTrackPathDescriptor implements TrackPathDescriptor,
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the value of newAverageMovingSpeed to test the method isDifferenceSignificant.
|
||||
* @param newAverageMovingSpeed the value to set.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
void setAverageMovingSpeed(double newAverageMovingSpeed) {
|
||||
averageMovingSpeed = newAverageMovingSpeed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the value of averageMovingSpeed to check the result of test.
|
||||
*
|
||||
* @return the value of averageMovingSpeed
|
||||
* Gets the average moving speed.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
double getAverageMovingSpeed() {
|
||||
return averageMovingSpeed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the average moving speed.
|
||||
*
|
||||
* @param value the value
|
||||
*/
|
||||
@VisibleForTesting
|
||||
void setAverageMovingSpeed(double value) {
|
||||
averageMovingSpeed = value;
|
||||
}
|
||||
}
|
||||
+68
-81
@@ -25,7 +25,6 @@ import com.google.common.annotations.VisibleForTesting;
|
||||
import android.content.Context;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Path;
|
||||
import android.graphics.Point;
|
||||
import android.graphics.Rect;
|
||||
|
||||
@@ -33,117 +32,105 @@ import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A path painter that varies the path colors based on fixed speeds or average speed margin
|
||||
* depending of the TrackPathDescriptor passed to its constructor.
|
||||
* A path painter that varies the path colors based on fixed speeds or average
|
||||
* speed margin depending of the TrackPathDescriptor passed to its constructor.
|
||||
*
|
||||
* @author Vangelis S.
|
||||
* @author Vangelis S.
|
||||
*/
|
||||
public class DynamicSpeedTrackPathPainter implements TrackPathPainter {
|
||||
private final Paint selectedTrackPaintSlow;
|
||||
private final Paint selectedTrackPaintMedium;
|
||||
private final Paint selectedTrackPaintFast;
|
||||
private final List<ColoredPath> coloredPaths;
|
||||
public class DynamicSpeedTrackPathPainter implements TrackPathPainter {
|
||||
private final TrackPathDescriptor trackPathDescriptor;
|
||||
private int slowSpeed;
|
||||
private int normalSpeed;
|
||||
|
||||
public DynamicSpeedTrackPathPainter (Context context, TrackPathDescriptor trackPathDescriptor) {
|
||||
private final Paint slowPaint;
|
||||
private final Paint mediumPaint;
|
||||
private final Paint fastPaint;
|
||||
private final List<ColoredPath> coloredPaths;
|
||||
|
||||
public DynamicSpeedTrackPathPainter(Context context, TrackPathDescriptor trackPathDescriptor) {
|
||||
this.trackPathDescriptor = trackPathDescriptor;
|
||||
|
||||
selectedTrackPaintSlow = TrackPathUtilities.getPaint(R.color.slow_path, context);
|
||||
selectedTrackPaintMedium = TrackPathUtilities.getPaint(R.color.normal_path, context);
|
||||
selectedTrackPaintFast = TrackPathUtilities.getPaint(R.color.fast_path, context);
|
||||
|
||||
this.coloredPaths = new ArrayList<ColoredPath>();
|
||||
slowPaint = TrackPathUtils.getPaint(context, R.color.slow_path);
|
||||
mediumPaint = TrackPathUtils.getPaint(context, R.color.normal_path);
|
||||
fastPaint = TrackPathUtils.getPaint(context, R.color.fast_path);
|
||||
coloredPaths = new ArrayList<ColoredPath>();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void drawTrack(Canvas canvas) {
|
||||
for(int i = 0; i < coloredPaths.size(); ++i) {
|
||||
ColoredPath coloredPath = coloredPaths.get(i);
|
||||
canvas.drawPath(coloredPath.getPath(), coloredPath.getPathPaint());
|
||||
public boolean hasPath() {
|
||||
return !coloredPaths.isEmpty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean updateState() {
|
||||
return trackPathDescriptor.updateState();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updatePath(
|
||||
Projection projection, Rect viewRect, int startIndex, List<CachedLocation> points) {
|
||||
boolean hasLastPoint = startIndex != 0 && points.get(startIndex -1).isValid();
|
||||
Point point = new Point();
|
||||
if (hasLastPoint) {
|
||||
GeoPoint geoPoint = points.get(startIndex -1).getGeoPoint();
|
||||
projection.toPixels(geoPoint, point);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updatePath(Projection projection, Rect viewRect, int startLocationIdx,
|
||||
Boolean alwaysVisible, List<CachedLocation> points) {
|
||||
// Whether to start a new segment on new valid and visible point.
|
||||
boolean newSegment = startLocationIdx <= 0 || !points.get(startLocationIdx - 1).valid;
|
||||
boolean lastVisible = !newSegment;
|
||||
final Point pt = new Point();
|
||||
|
||||
clear();
|
||||
|
||||
slowSpeed = trackPathDescriptor.getSlowSpeed();
|
||||
normalSpeed = trackPathDescriptor.getNormalSpeed();
|
||||
|
||||
// Loop over track points.
|
||||
for (int i = startLocationIdx; i < points.size(); ++i) {
|
||||
CachedLocation loc = points.get(i);
|
||||
|
||||
// Check if valid, if not then indicate a new segment.
|
||||
if (!loc.valid) {
|
||||
boolean newSegment = !hasLastPoint;
|
||||
// Assume if last point exists, it is visible
|
||||
boolean lastPointVisible = hasLastPoint;
|
||||
int slowSpeed = trackPathDescriptor.getSlowSpeed();
|
||||
int normalSpeed = trackPathDescriptor.getNormalSpeed();
|
||||
|
||||
for (int i = startIndex; i < points.size(); ++i) {
|
||||
CachedLocation cachedLocation = points.get(i);
|
||||
|
||||
// If not valid, start a new segment
|
||||
if (!cachedLocation.isValid()) {
|
||||
newSegment = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
final GeoPoint geoPoint = loc.geoPoint;
|
||||
|
||||
GeoPoint geoPoint = cachedLocation.getGeoPoint();
|
||||
// Check if this breaks the existing segment.
|
||||
boolean visible = alwaysVisible || viewRect.contains(
|
||||
geoPoint.getLongitudeE6(), geoPoint.getLatitudeE6());
|
||||
if (!visible && !lastVisible) {
|
||||
// This is a point outside view not connected to a visible one.
|
||||
boolean pointVisible = viewRect.contains(geoPoint.getLongitudeE6(), geoPoint.getLatitudeE6());
|
||||
if (!pointVisible && !lastPointVisible) {
|
||||
// This point and the last point are both outside visible area.
|
||||
newSegment = true;
|
||||
}
|
||||
lastVisible = visible;
|
||||
|
||||
// Either move to beginning of a new segment or continue the old one.
|
||||
lastPointVisible = pointVisible;
|
||||
|
||||
// Either update point or draw a line from the last point
|
||||
if (newSegment) {
|
||||
projection.toPixels(geoPoint, pt);
|
||||
projection.toPixels(geoPoint, point);
|
||||
newSegment = false;
|
||||
} else {
|
||||
ColoredPath coloredPath;
|
||||
if(loc.speed <= slowSpeed) {
|
||||
coloredPath = new ColoredPath(selectedTrackPaintSlow);
|
||||
}
|
||||
else if(loc.speed <= normalSpeed) {
|
||||
coloredPath = new ColoredPath(selectedTrackPaintMedium);
|
||||
if (cachedLocation.getSpeed() <= slowSpeed) {
|
||||
coloredPath = new ColoredPath(slowPaint);
|
||||
} else if (cachedLocation.getSpeed() <= normalSpeed) {
|
||||
coloredPath = new ColoredPath(mediumPaint);
|
||||
} else {
|
||||
coloredPath = new ColoredPath(selectedTrackPaintFast);
|
||||
coloredPath = new ColoredPath(fastPaint);
|
||||
}
|
||||
coloredPath.getPath().moveTo(pt.x, pt.y);
|
||||
projection.toPixels(geoPoint, pt);
|
||||
coloredPath.getPath().lineTo(pt.x, pt.y);
|
||||
coloredPath.getPath().moveTo(point.x, point.y);
|
||||
projection.toPixels(geoPoint, point);
|
||||
coloredPath.getPath().lineTo(point.x, point.y);
|
||||
coloredPaths.add(coloredPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void clear()
|
||||
{
|
||||
public void clearPath() {
|
||||
coloredPaths.clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean needsRedraw() {
|
||||
return trackPathDescriptor.needsRedraw();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Path getLastPath() {
|
||||
Path path = new Path();
|
||||
for(int i = 0; i < coloredPaths.size(); ++i) {
|
||||
path.addPath(coloredPaths.get(i).getPath());
|
||||
public void drawPath(Canvas canvas) {
|
||||
for (int i = 0; i < coloredPaths.size(); i++) {
|
||||
ColoredPath coloredPath = coloredPaths.get(i);
|
||||
canvas.drawPath(coloredPath.getPath(), coloredPath.getPathPaint());
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns coloredPaths.
|
||||
*
|
||||
* @return coloredPaths
|
||||
* Gets the colored paths.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
List<ColoredPath> getColoredPaths() {
|
||||
|
||||
+10
-19
@@ -31,40 +31,36 @@ import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
|
||||
*/
|
||||
public class FixedSpeedTrackPathDescriptor
|
||||
implements TrackPathDescriptor, OnSharedPreferenceChangeListener {
|
||||
private final Context context;
|
||||
private int slowSpeed;
|
||||
private int normalSpeed;
|
||||
private final Context context;
|
||||
|
||||
public FixedSpeedTrackPathDescriptor(Context context) {
|
||||
this.context = context;
|
||||
|
||||
context.getSharedPreferences(Constants.SETTINGS_NAME, Context.MODE_PRIVATE)
|
||||
.registerOnSharedPreferenceChangeListener(this);
|
||||
|
||||
slowSpeed = PreferencesUtils.getInt(context, R.string.track_color_mode_slow_key,
|
||||
PreferencesUtils.TRACK_COLOR_MODE_SLOW_DEFAULT);
|
||||
normalSpeed = PreferencesUtils.getInt(context, R.string.track_color_mode_medium_key,
|
||||
PreferencesUtils.TRACK_COLOR_MODE_MEDIUM_DEFAULT);
|
||||
context.getSharedPreferences(Constants.SETTINGS_NAME, Context.MODE_PRIVATE)
|
||||
.registerOnSharedPreferenceChangeListener(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the slow speed for reference.
|
||||
*
|
||||
* @return The speed limit considered as slow.
|
||||
*/
|
||||
@Override
|
||||
public int getSlowSpeed() {
|
||||
return slowSpeed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the normal speed for reference.
|
||||
*
|
||||
* @return The speed limit considered as normal.
|
||||
*/
|
||||
@Override
|
||||
public int getNormalSpeed() {
|
||||
return normalSpeed;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean updateState() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
|
||||
if (PreferencesUtils.getKey(context, R.string.track_color_mode_slow_key).equals(key)) {
|
||||
@@ -75,9 +71,4 @@ public class FixedSpeedTrackPathDescriptor
|
||||
PreferencesUtils.TRACK_COLOR_MODE_MEDIUM_DEFAULT);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean needsRedraw() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
+64
-49
@@ -40,88 +40,103 @@ public class SingleColorTrackPathPainter implements TrackPathPainter {
|
||||
private Path path;
|
||||
|
||||
public SingleColorTrackPathPainter(Context context) {
|
||||
selectedTrackPaint = TrackPathUtilities.getPaint(R.color.red, context);
|
||||
selectedTrackPaint = TrackPathUtils.getPaint(context, R.color.red);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void drawTrack(Canvas canvas) {
|
||||
canvas.drawPath(path, selectedTrackPaint);
|
||||
public boolean hasPath() {
|
||||
return path != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updatePath(Projection projection, Rect viewRect, int startLocationIdx,
|
||||
Boolean alwaysVisible, List<CachedLocation> points) {
|
||||
path = newPath();
|
||||
updatePath(projection, viewRect, startLocationIdx, alwaysVisible, points, path);
|
||||
public boolean updateState() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updatePath(
|
||||
Projection projection, Rect viewRect, int startIndex, List<CachedLocation> points) {
|
||||
if (!hasPath()) {
|
||||
path = newPath();
|
||||
}
|
||||
updatePath(projection, viewRect, startIndex, points, path);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearPath() {
|
||||
path = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void drawPath(Canvas canvas) {
|
||||
if (path != null) {
|
||||
canvas.drawPath(path, selectedTrackPaint);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the path.
|
||||
*
|
||||
* @param projection The Canvas to draw upon.
|
||||
* @param viewRect The Path to be drawn.
|
||||
* @param startLocationIdx The start point from where update the path.
|
||||
* @param alwaysVisible Flag for always visible.
|
||||
* @param points The list of points used to update the path.
|
||||
* @param pathToUpdate The path to be created.
|
||||
* @param projection the projection
|
||||
* @param viewRect the view rectangle
|
||||
* @param startIndex the start index
|
||||
* @param points the points
|
||||
* @param pathToUpdate the path to update
|
||||
*/
|
||||
@VisibleForTesting
|
||||
void updatePath(Projection projection, Rect viewRect, int startLocationIdx,
|
||||
Boolean alwaysVisible, List<CachedLocation> points, Path pathToUpdate) {
|
||||
pathToUpdate.incReserve(points.size());
|
||||
// Whether to start a new segment on new valid and visible point.
|
||||
boolean newSegment = startLocationIdx <= 0 || !points.get(startLocationIdx - 1).valid;
|
||||
boolean lastVisible = !newSegment;
|
||||
final Point pt = new Point();
|
||||
// Loop over track points.
|
||||
for (int i = startLocationIdx; i < points.size(); ++i) {
|
||||
CachedLocation loc = points.get(i);
|
||||
void updatePath(Projection projection, Rect viewRect, int startIndex, List<CachedLocation> points,
|
||||
Path pathToUpdate) {
|
||||
pathToUpdate.incReserve(points.size() - startIndex);
|
||||
|
||||
// Check if valid, if not then indicate a new segment.
|
||||
if (!loc.valid) {
|
||||
boolean hasLastPoint = startIndex != 0 && points.get(startIndex - 1).isValid();
|
||||
boolean newSegment = !hasLastPoint;
|
||||
// Assume if last point exists, it is visible
|
||||
boolean lastPointVisible = hasLastPoint;
|
||||
Point point = new Point();
|
||||
|
||||
for (int i = startIndex; i < points.size(); i++) {
|
||||
CachedLocation cachedLocation = points.get(i);
|
||||
|
||||
// If not valid, start a new segment
|
||||
if (!cachedLocation.isValid()) {
|
||||
newSegment = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
final GeoPoint geoPoint = loc.geoPoint;
|
||||
GeoPoint geoPoint = cachedLocation.getGeoPoint();
|
||||
// Check if this breaks the existing segment.
|
||||
boolean visible = alwaysVisible
|
||||
|| viewRect.contains(geoPoint.getLongitudeE6(), geoPoint.getLatitudeE6());
|
||||
if (!visible && !lastVisible) {
|
||||
// This is a point outside view not connected to a visible one.
|
||||
boolean pointVisible = viewRect.contains(geoPoint.getLongitudeE6(), geoPoint.getLatitudeE6());
|
||||
if (!pointVisible && !lastPointVisible) {
|
||||
// This point and the last point are both outside visible area.
|
||||
newSegment = true;
|
||||
}
|
||||
lastVisible = visible;
|
||||
lastPointVisible = pointVisible;
|
||||
|
||||
// Either move to beginning of a new segment or continue the old one.
|
||||
projection.toPixels(geoPoint, pt);
|
||||
// Either update point or draw a line from the last point
|
||||
projection.toPixels(geoPoint, point);
|
||||
if (newSegment) {
|
||||
pathToUpdate.moveTo(pt.x, pt.y);
|
||||
pathToUpdate.moveTo(point.x, point.y);
|
||||
newSegment = false;
|
||||
} else {
|
||||
pathToUpdate.lineTo(pt.x, pt.y);
|
||||
pathToUpdate.lineTo(point.x, point.y);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clear() {
|
||||
path = null;
|
||||
/**
|
||||
* Creates a new path.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
protected Path newPath() {
|
||||
return new Path();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean needsRedraw() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Path getLastPath() {
|
||||
/**
|
||||
* Gets the path.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
public Path getPath() {
|
||||
return path;
|
||||
}
|
||||
|
||||
// Visible for testing
|
||||
public Path newPath() {
|
||||
return new Path();
|
||||
}
|
||||
}
|
||||
@@ -13,26 +13,28 @@
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
|
||||
package com.google.android.apps.mytracks.maps;
|
||||
|
||||
/**
|
||||
* An interface for classes which describe how to draw a track path.
|
||||
*
|
||||
* An interface for classes which describe how to draw a track path.
|
||||
*
|
||||
* @author Vangelis S.
|
||||
*/
|
||||
public interface TrackPathDescriptor {
|
||||
|
||||
/**
|
||||
* @return The maximum speed which is considered slow.
|
||||
* Gets the maximum speed which is considered slow.
|
||||
*/
|
||||
int getSlowSpeed();
|
||||
|
||||
public int getSlowSpeed();
|
||||
|
||||
/**
|
||||
* @return The maximum speed which is considered normal.
|
||||
* Gets the maximum speed which is considered normal.
|
||||
*/
|
||||
int getNormalSpeed();
|
||||
|
||||
public int getNormalSpeed();
|
||||
|
||||
/**
|
||||
* @return True if the path needs to be updated.
|
||||
* Updates state. Returns true if the state is updated.
|
||||
*/
|
||||
boolean needsRedraw();
|
||||
public boolean updateState();
|
||||
}
|
||||
@@ -13,56 +13,54 @@
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
|
||||
package com.google.android.apps.mytracks.maps;
|
||||
|
||||
import com.google.android.apps.mytracks.MapOverlay.CachedLocation;
|
||||
import com.google.android.maps.Projection;
|
||||
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Path;
|
||||
import android.graphics.Rect;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* An interface for classes which paint the track path.
|
||||
*
|
||||
* An interface for classes which paint the track path.
|
||||
*
|
||||
* @author Vangelis S.
|
||||
*/
|
||||
public interface TrackPathPainter {
|
||||
|
||||
|
||||
/**
|
||||
* Clears the related data.
|
||||
* Returns true if has path.
|
||||
*/
|
||||
void clear();
|
||||
|
||||
public boolean hasPath();
|
||||
|
||||
/**
|
||||
* Draws the path to the canvas.
|
||||
* @param canvas The Canvas to draw upon
|
||||
* Updates state. Returns true if the state is updated.
|
||||
*/
|
||||
void drawTrack(Canvas canvas);
|
||||
|
||||
public boolean updateState();
|
||||
|
||||
/**
|
||||
* Updates the path.
|
||||
* @param projection The Canvas to draw upon.
|
||||
* @param viewRect The Path to be drawn.
|
||||
* @param startLocationIdx The start point from where update the path.
|
||||
* @param alwaysVisible Flag for alwaysvisible.
|
||||
* @param points The list of points used to update the path.
|
||||
* Updates the path. Creates a new path if necessary
|
||||
*
|
||||
* @param projection the projection
|
||||
* @param viewRect the view rectangle
|
||||
* @param startIndex the start index
|
||||
* @param points the points
|
||||
*/
|
||||
void updatePath(Projection projection, Rect viewRect, int startLocationIdx,
|
||||
Boolean alwaysVisible, List<CachedLocation> points);
|
||||
|
||||
public void updatePath(
|
||||
Projection projection, Rect viewRect, int startIndex, List<CachedLocation> points);
|
||||
|
||||
/**
|
||||
* @return True if the path needs to be updated.
|
||||
* Clears the path.
|
||||
*/
|
||||
boolean needsRedraw();
|
||||
|
||||
public void clearPath();
|
||||
|
||||
/**
|
||||
* @return The path being used currently.
|
||||
* Draws the path.
|
||||
*
|
||||
* @param canvas the canvas
|
||||
*/
|
||||
Path getLastPath();
|
||||
public void drawPath(Canvas canvas);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
|
||||
package com.google.android.apps.mytracks.maps;
|
||||
|
||||
import static com.google.android.apps.mytracks.Constants.TAG;
|
||||
@@ -24,32 +25,31 @@ import android.content.Context;
|
||||
import android.util.Log;
|
||||
|
||||
/**
|
||||
* A factory for TrackPathPainters.
|
||||
*
|
||||
* A factory for {@link TrackPathPainter}.
|
||||
*
|
||||
* @author Vangelis S.
|
||||
*/
|
||||
public class TrackPathPainterFactory {
|
||||
|
||||
private TrackPathPainterFactory() {
|
||||
}
|
||||
private TrackPathPainterFactory() {}
|
||||
|
||||
/**
|
||||
* Get a new TrackPathPainter.
|
||||
* @param context Context to fetch system preferences.
|
||||
* @return The TrackPathPainter that corresponds to the track color mode setting.
|
||||
* Get a new {@link TrackPathPainter}.
|
||||
*
|
||||
* @param context the context
|
||||
*/
|
||||
public static TrackPathPainter getTrackPathPainter(Context context) {
|
||||
String trackColorMode = PreferencesUtils.getString(context, R.string.track_color_mode_key,
|
||||
context.getString(R.string.settings_map_track_color_mode_single_value));
|
||||
Log.i(TAG, "Creating track path painter of type: " + trackColorMode);
|
||||
|
||||
if (context.getString(R.string.settings_map_track_color_mode_fixed_value)
|
||||
.equals(trackColorMode)) {
|
||||
return new DynamicSpeedTrackPathPainter(context, new FixedSpeedTrackPathDescriptor(context));
|
||||
} else if (context.getString(R.string.settings_map_track_color_mode_dynamic_value)
|
||||
if (context.getString(R.string.settings_map_track_color_mode_dynamic_value)
|
||||
.equals(trackColorMode)) {
|
||||
return new DynamicSpeedTrackPathPainter(context, new DynamicSpeedTrackPathDescriptor(
|
||||
context));
|
||||
} else if (context.getString(R.string.settings_map_track_color_mode_fixed_value)
|
||||
.equals(trackColorMode)) {
|
||||
return new DynamicSpeedTrackPathPainter(context, new FixedSpeedTrackPathDescriptor(context));
|
||||
} else {
|
||||
return new SingleColorTrackPathPainter(context);
|
||||
}
|
||||
|
||||
+15
-6
@@ -13,21 +13,30 @@
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
|
||||
package com.google.android.apps.mytracks.maps;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Paint;
|
||||
|
||||
/**
|
||||
* Various utility functions for TrackPath painting.
|
||||
*
|
||||
* Various utility functions for track path painting.
|
||||
*
|
||||
* @author Vangelis S.
|
||||
*/
|
||||
public class TrackPathUtilities {
|
||||
|
||||
public static Paint getPaint(int id, Context context) {
|
||||
public class TrackPathUtils {
|
||||
|
||||
private TrackPathUtils() {}
|
||||
|
||||
/**
|
||||
* Gets a paint.
|
||||
*
|
||||
* @param context the context
|
||||
* @param colorId the color id
|
||||
*/
|
||||
public static Paint getPaint(Context context, int colorId) {
|
||||
Paint paint = new Paint();
|
||||
paint.setColor(context.getResources().getColor(id));
|
||||
paint.setColor(context.getResources().getColor(colorId));
|
||||
paint.setStrokeWidth(3);
|
||||
paint.setStyle(Paint.Style.STROKE);
|
||||
paint.setAntiAlias(true);
|
||||
+12
-19
@@ -19,41 +19,34 @@ package com.google.android.apps.mytracks.services;
|
||||
/**
|
||||
* This is a simple location listener policy that will always dictate the same
|
||||
* polling interval.
|
||||
*
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
*/
|
||||
public class AbsoluteLocationListenerPolicy implements LocationListenerPolicy {
|
||||
|
||||
/**
|
||||
* The interval to request for gps signal.
|
||||
*/
|
||||
private final long interval;
|
||||
|
||||
/**
|
||||
* @param interval The interval to request for gps signal
|
||||
* Constructor.
|
||||
*
|
||||
* @param interval the interval to request for gps signal
|
||||
*/
|
||||
public AbsoluteLocationListenerPolicy(final long interval) {
|
||||
public AbsoluteLocationListenerPolicy(long interval) {
|
||||
this.interval = interval;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The interval given in the constructor
|
||||
*/
|
||||
@Override
|
||||
public long getDesiredPollingInterval() {
|
||||
return interval;
|
||||
}
|
||||
|
||||
/**
|
||||
* Discards the idle time.
|
||||
*/
|
||||
public void updateIdleTime(long idleTime) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the minimum distance between updates.
|
||||
* Get all updates to properly measure moving time.
|
||||
*/
|
||||
@Override
|
||||
public int getMinDistance() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateIdleTime(long idleTime) {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
|
||||
+30
-39
@@ -16,67 +16,58 @@
|
||||
|
||||
package com.google.android.apps.mytracks.services;
|
||||
|
||||
|
||||
/**
|
||||
* A LocationListenerPolicy that will change based on how long the user has been
|
||||
* stationary.
|
||||
*
|
||||
* This policy will dictate a policy based on a min, max and idle time.
|
||||
* The policy will dictate an interval bounded by min and max whic is half of
|
||||
* the idle time.
|
||||
*
|
||||
* A {@link LocationListenerPolicy} that will change based on how long the user
|
||||
* has been stationary. This policy will dictate a policy based on a min, max
|
||||
* and idle time. The policy will dictate an interval bounded by min and max,
|
||||
* and is half of the idle time.
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
*/
|
||||
public class AdaptiveLocationListenerPolicy implements LocationListenerPolicy {
|
||||
|
||||
/**
|
||||
* Smallest interval this policy will dictate, in milliseconds.
|
||||
*/
|
||||
private final long minInterval;
|
||||
|
||||
/**
|
||||
* Largest interval this policy will dictate, in milliseconds.
|
||||
*/
|
||||
private final long maxInterval;
|
||||
|
||||
private final int minDistance;
|
||||
|
||||
/**
|
||||
* The time the user has been at the current location, in milliseconds.
|
||||
*/
|
||||
|
||||
// The time the user has been idle at the current location, in milliseconds.
|
||||
private long idleTime;
|
||||
|
||||
/**
|
||||
* Creates a policy that will be bounded by the given min and max.
|
||||
*
|
||||
* @param min Smallest interval this policy will dictate, in milliseconds
|
||||
* @param max Largest interval this policy will dictate, in milliseconds
|
||||
* Creates a policy that will be bounded by the given minInterval and
|
||||
* maxInterval.
|
||||
*
|
||||
* @param minInterval the smallest interval this policy will dictate, in
|
||||
* milliseconds
|
||||
* @param maxInterval the largest interval this policy will dictate, in
|
||||
* milliseconds
|
||||
* @param minDistance the minimum distance in meters
|
||||
*/
|
||||
public AdaptiveLocationListenerPolicy(long min, long max, int minDistance) {
|
||||
this.minInterval = min;
|
||||
this.maxInterval = max;
|
||||
public AdaptiveLocationListenerPolicy(long minInterval, long maxInterval, int minDistance) {
|
||||
this.minInterval = minInterval;
|
||||
this.maxInterval = maxInterval;
|
||||
this.minDistance = minDistance;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return An interval bounded by min and max which is half of the idle time
|
||||
/*
|
||||
* Returns an interval half of the idle time, but bounded by minInteval and
|
||||
* maxInterval.
|
||||
*/
|
||||
@Override
|
||||
public long getDesiredPollingInterval() {
|
||||
long desiredInterval = idleTime / 2;
|
||||
// Round to avoid setting the interval too often.
|
||||
// Round to second to avoid setting the interval too often
|
||||
desiredInterval = (desiredInterval / 1000) * 1000;
|
||||
return Math.max(Math.min(maxInterval, desiredInterval),
|
||||
minInterval);
|
||||
return Math.max(Math.min(maxInterval, desiredInterval), minInterval);
|
||||
}
|
||||
|
||||
public void updateIdleTime(long newIdleTime) {
|
||||
this.idleTime = newIdleTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the minimum distance between updates.
|
||||
*/
|
||||
@Override
|
||||
public int getMinDistance() {
|
||||
return minDistance;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateIdleTime(long newIdleTime) {
|
||||
idleTime = newIdleTime;
|
||||
}
|
||||
}
|
||||
|
||||
+11
-12
@@ -17,19 +17,18 @@
|
||||
package com.google.android.apps.mytracks.services;
|
||||
|
||||
/**
|
||||
* This is an interface for classes that will manage the location listener policy.
|
||||
* Different policy options are:
|
||||
* Absolute
|
||||
* Addaptive
|
||||
*
|
||||
* This is an interface for classes that will manage the location listener
|
||||
* policy. Different policy options are: {@link AbsoluteLocationListenerPolicy}
|
||||
* and {@link AdaptiveLocationListenerPolicy}.
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
*/
|
||||
public interface LocationListenerPolicy {
|
||||
|
||||
/**
|
||||
* Returns the polling time this policy would like at this time.
|
||||
*
|
||||
* @return The polling that this policy dictates
|
||||
* Returns the polling interval this policy would like at this moment.
|
||||
*
|
||||
* @return the polling interval
|
||||
*/
|
||||
public long getDesiredPollingInterval();
|
||||
|
||||
@@ -39,10 +38,10 @@ public interface LocationListenerPolicy {
|
||||
public int getMinDistance();
|
||||
|
||||
/**
|
||||
* Notifies the amount of time the user has been idle at their current
|
||||
* location.
|
||||
*
|
||||
* @param idleTime The time that the user has been idle at this spot
|
||||
* Notifies the amount of time the user has been idle at his current location.
|
||||
*
|
||||
* @param idleTime the time that the user has been idle at his current
|
||||
* location
|
||||
*/
|
||||
public void updateIdleTime(long idleTime);
|
||||
}
|
||||
|
||||
@@ -1,139 +0,0 @@
|
||||
/*
|
||||
* 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.services;
|
||||
|
||||
import com.google.android.apps.mytracks.Constants;
|
||||
import com.google.android.apps.mytracks.util.PreferencesUtils;
|
||||
import com.google.android.maps.mytracks.R;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
|
||||
import android.util.Log;
|
||||
|
||||
/**
|
||||
* A class that manages reading the shared preferences for the service.
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
*/
|
||||
public class PreferenceManager implements OnSharedPreferenceChangeListener {
|
||||
private TrackRecordingService service;
|
||||
private SharedPreferences sharedPreferences;
|
||||
|
||||
public PreferenceManager(TrackRecordingService service) {
|
||||
this.service = service;
|
||||
this.sharedPreferences = service.getSharedPreferences(
|
||||
Constants.SETTINGS_NAME, Context.MODE_PRIVATE);
|
||||
sharedPreferences.registerOnSharedPreferenceChangeListener(this);
|
||||
|
||||
// Refresh all properties.
|
||||
onSharedPreferenceChanged(sharedPreferences, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies that preferences have changed. Call this with key == null to
|
||||
* update all preferences in one call.
|
||||
*
|
||||
* @param key the key that changed (may be null to update all preferences)
|
||||
*/
|
||||
@Override
|
||||
public void onSharedPreferenceChanged(SharedPreferences preferences, String key) {
|
||||
if (service == null) {
|
||||
Log.w(Constants.TAG, "onSharedPreferenceChanged: a preference change (key = " + key
|
||||
+ ") after a call to shutdown()");
|
||||
return;
|
||||
}
|
||||
if (key == null
|
||||
|| PreferencesUtils.getKey(service, R.string.recording_track_id_key).equals(key)) {
|
||||
long recordingTrackId = PreferencesUtils.getLong(service, R.string.recording_track_id_key);
|
||||
/*
|
||||
* Only set the id if it is valid. Setting it to -1L should only happen
|
||||
* in TrackRecordingService.endCurrentTrack()
|
||||
*/
|
||||
if (recordingTrackId != PreferencesUtils.RECORDING_TRACK_ID_DEFAULT) {
|
||||
service.setRecordingTrackId(recordingTrackId);
|
||||
}
|
||||
}
|
||||
if (key == null || PreferencesUtils.getKey(service, R.string.metric_units_key).equals(key)) {
|
||||
service.setMetricUnits(PreferencesUtils.getBoolean(
|
||||
service, R.string.metric_units_key, PreferencesUtils.METRIC_UNITS_DEFAULT));
|
||||
}
|
||||
if (key == null
|
||||
|| PreferencesUtils.getKey(service, R.string.announcement_frequency_key).equals(key)) {
|
||||
service.setAnnouncementFrequency(PreferencesUtils.getInt(
|
||||
service, R.string.announcement_frequency_key,
|
||||
PreferencesUtils.ANNOUNCEMENT_FREQUENCY_DEFAULT));
|
||||
}
|
||||
if (key == null || PreferencesUtils.getKey(service, R.string.split_frequency_key).equals(key)) {
|
||||
service.setSplitFrequency(PreferencesUtils.getInt(
|
||||
service, R.string.split_frequency_key, PreferencesUtils.SPLIT_FREQUENCY_DEFAULT));
|
||||
}
|
||||
if (key == null
|
||||
|| PreferencesUtils.getKey(service, R.string.min_recording_interval_key).equals(key)) {
|
||||
int minRecordingInterval = PreferencesUtils.getInt(service,
|
||||
R.string.min_recording_interval_key, PreferencesUtils.MIN_RECORDING_INTERVAL_DEFAULT);
|
||||
switch (minRecordingInterval) {
|
||||
case PreferencesUtils.MIN_RECORDING_INTERVAL_ADAPT_BATTERY_LIFE:
|
||||
/*
|
||||
* Choose battery life over moving time accuracy. min: 30 seconds,
|
||||
* max: 5 minutes, inDist: 5 meters.
|
||||
*/
|
||||
service.setLocationListenerPolicy(new AdaptiveLocationListenerPolicy(30000, 300000, 5));
|
||||
break;
|
||||
case PreferencesUtils.MIN_RECORDING_INTERVAL_ADAPT_ACCURACY:
|
||||
/*
|
||||
* Get all the updates. min: 1 second, max: 30 seconds, minDist: 0
|
||||
* meter.
|
||||
*/
|
||||
service.setLocationListenerPolicy(new AdaptiveLocationListenerPolicy(1000, 30000, 0));
|
||||
break;
|
||||
default:
|
||||
service.setLocationListenerPolicy(
|
||||
new AbsoluteLocationListenerPolicy(minRecordingInterval * 1000));
|
||||
}
|
||||
}
|
||||
if (key == null
|
||||
|| PreferencesUtils.getKey(service, R.string.min_recording_distance_key).equals(key)) {
|
||||
service.setMinRecordingDistance(PreferencesUtils.getInt(
|
||||
service, R.string.min_recording_distance_key,
|
||||
PreferencesUtils.MIN_RECORDING_DISTANCE_DEFAULT));
|
||||
}
|
||||
if (key == null
|
||||
|| PreferencesUtils.getKey(service, R.string.max_recording_distance_key).equals(key)) {
|
||||
service.setMaxRecordingDistance(PreferencesUtils.getInt(
|
||||
service, R.string.max_recording_distance_key,
|
||||
PreferencesUtils.MAX_RECORDING_DISTANCE_DEFAULT));
|
||||
}
|
||||
|
||||
if (key == null
|
||||
|| PreferencesUtils.getKey(service, R.string.min_required_accuracy_key).equals(key)) {
|
||||
service.setMinRequiredAccuracy(PreferencesUtils.getInt(
|
||||
service, R.string.min_required_accuracy_key,
|
||||
PreferencesUtils.MIN_REQUIRED_ACCURACY_DEFAULT));
|
||||
}
|
||||
if (key == null
|
||||
|| PreferencesUtils.getKey(service, R.string.auto_resume_track_timeout_key).equals(key)) {
|
||||
service.setAutoResumeTrackTimeout(PreferencesUtils.getInt(
|
||||
service, R.string.auto_resume_track_timeout_key,
|
||||
PreferencesUtils.AUTO_RESUME_TRACK_TIMEOUT_DEFAULT));
|
||||
}
|
||||
}
|
||||
|
||||
public void shutdown() {
|
||||
sharedPreferences.unregisterOnSharedPreferenceChangeListener(this);
|
||||
service = null;
|
||||
}
|
||||
}
|
||||
+807
-988
File diff suppressed because it is too large
Load Diff
+298
@@ -0,0 +1,298 @@
|
||||
/*
|
||||
* Copyright 2009 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.services.tasks;
|
||||
|
||||
import com.google.android.apps.mytracks.services.TrackRecordingService;
|
||||
import com.google.android.apps.mytracks.stats.TripStatistics;
|
||||
import com.google.android.apps.mytracks.util.PreferencesUtils;
|
||||
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.speech.tts.TextToSpeech;
|
||||
import android.speech.tts.TextToSpeech.OnInitListener;
|
||||
import android.telephony.PhoneStateListener;
|
||||
import android.telephony.TelephonyManager;
|
||||
import android.util.Log;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* This class will periodically announce the user's trip statistics.
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
*/
|
||||
public class AnnouncementPeriodicTask implements PeriodicTask {
|
||||
|
||||
/**
|
||||
* The rate at which announcements are spoken.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
static final float TTS_SPEECH_RATE = 0.9f;
|
||||
|
||||
private static final String TAG = AnnouncementPeriodicTask.class.getSimpleName();
|
||||
private static final long HOUR_TO_MILLISECOND = 60 * 60 * 1000;
|
||||
|
||||
private final Context context;
|
||||
protected TextToSpeech tts;
|
||||
|
||||
// Response from TTS after its initialization
|
||||
private int initStatus = TextToSpeech.ERROR;
|
||||
|
||||
// True if TTS engine is ready
|
||||
private boolean ready = false;
|
||||
|
||||
// True if speech is allowed
|
||||
private boolean speechAllowed;
|
||||
|
||||
/**
|
||||
* Listener which updates {@link #speechAllowed} when the phone state changes.
|
||||
*/
|
||||
private final PhoneStateListener phoneStateListener = new PhoneStateListener() {
|
||||
@Override
|
||||
public void onCallStateChanged(int state, String incomingNumber) {
|
||||
speechAllowed = state == TelephonyManager.CALL_STATE_IDLE;
|
||||
if (!speechAllowed && tts != null && tts.isSpeaking()) {
|
||||
// If we're already speaking, stop it.
|
||||
tts.stop();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
public AnnouncementPeriodicTask(Context context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
if (tts == null) {
|
||||
tts = newTextToSpeech(context, new OnInitListener() {
|
||||
@Override
|
||||
public void onInit(int status) {
|
||||
initStatus = status;
|
||||
}
|
||||
});
|
||||
}
|
||||
speechAllowed = true;
|
||||
listenToPhoneState(phoneStateListener, PhoneStateListener.LISTEN_CALL_STATE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run(TrackRecordingService trackRecordingService) {
|
||||
if (trackRecordingService == null) {
|
||||
Log.e(TAG, "TrackRecordingService is null.");
|
||||
return;
|
||||
}
|
||||
announce(trackRecordingService.getTripStatistics());
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs this task.
|
||||
*
|
||||
* @param tripStatistics the trip statistics
|
||||
*/
|
||||
@VisibleForTesting
|
||||
void announce(TripStatistics tripStatistics) {
|
||||
if (tripStatistics == null) {
|
||||
Log.e(TAG, "TripStatistics is null.");
|
||||
return;
|
||||
}
|
||||
|
||||
synchronized (this) {
|
||||
if (!ready) {
|
||||
ready = initStatus == TextToSpeech.SUCCESS;
|
||||
if (ready) {
|
||||
onTtsReady();
|
||||
}
|
||||
}
|
||||
if (!ready) {
|
||||
Log.i(TAG, "TTS not ready.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!speechAllowed) {
|
||||
Log.i(TAG, "Speech is not allowed at this time.");
|
||||
return;
|
||||
}
|
||||
speakAnnouncement(getAnnouncement(tripStatistics));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void shutdown() {
|
||||
listenToPhoneState(phoneStateListener, PhoneStateListener.LISTEN_NONE);
|
||||
if (tts != null) {
|
||||
tts.shutdown();
|
||||
tts = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when TTS is ready.
|
||||
*/
|
||||
protected void onTtsReady() {
|
||||
Locale locale = Locale.getDefault();
|
||||
int languageAvailability = tts.isLanguageAvailable(locale);
|
||||
if (languageAvailability == TextToSpeech.LANG_MISSING_DATA
|
||||
|| languageAvailability == TextToSpeech.LANG_NOT_SUPPORTED) {
|
||||
Log.w(TAG, "Default locale not available, use English.");
|
||||
locale = Locale.ENGLISH;
|
||||
/*
|
||||
* TODO: instead of using english, load the language if missing and show a
|
||||
* toast if not supported. Not able to change the resource strings to
|
||||
* English.
|
||||
*/
|
||||
}
|
||||
tts.setLanguage(locale);
|
||||
|
||||
// Slow down the speed just a bit as it is hard to hear when exercising.
|
||||
tts.setSpeechRate(TTS_SPEECH_RATE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Speaks the announcement.
|
||||
*
|
||||
* @param announcement the announcement
|
||||
*/
|
||||
protected void speakAnnouncement(String announcement) {
|
||||
tts.speak(announcement, TextToSpeech.QUEUE_FLUSH, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link TextToSpeech}.
|
||||
*
|
||||
* @param aContext a context
|
||||
* @param onInitListener an on init listener
|
||||
*/
|
||||
@VisibleForTesting
|
||||
protected TextToSpeech newTextToSpeech(Context aContext, OnInitListener onInitListener) {
|
||||
return new TextToSpeech(aContext, onInitListener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the announcement.
|
||||
*
|
||||
* @param tripStatistics the trip statistics
|
||||
*/
|
||||
@VisibleForTesting
|
||||
protected String getAnnouncement(TripStatistics tripStatistics) {
|
||||
boolean metricUnits = PreferencesUtils.getBoolean(
|
||||
context, R.string.metric_units_key, PreferencesUtils.METRIC_UNITS_DEFAULT);
|
||||
boolean reportSpeed = PreferencesUtils.getBoolean(
|
||||
context, R.string.report_speed_key, PreferencesUtils.REPORT_SPEED_DEFAULT);
|
||||
double distance = tripStatistics.getTotalDistance() * UnitConversions.M_TO_KM;
|
||||
double speed = tripStatistics.getAverageMovingSpeed() * UnitConversions.MS_TO_KMH;
|
||||
|
||||
if (distance == 0) {
|
||||
return context.getString(R.string.voice_total_distance_zero);
|
||||
}
|
||||
|
||||
if (!metricUnits) {
|
||||
distance *= UnitConversions.KM_TO_MI;
|
||||
speed *= UnitConversions.KM_TO_MI;
|
||||
}
|
||||
|
||||
String rate;
|
||||
if (reportSpeed) {
|
||||
int speedId = metricUnits ? R.plurals.voiceSpeedKilometersPerHour
|
||||
: R.plurals.voiceSpeedMilesPerHour;
|
||||
rate = context.getResources().getQuantityString(speedId, getQuantityCount(speed), speed);
|
||||
} else {
|
||||
speed = speed == 0 ? 0.0 : 1 / speed;
|
||||
int paceId = metricUnits ? R.string.voice_pace_per_kilometer : R.string.voice_pace_per_mile;
|
||||
rate = context.getString(paceId, getAnnounceTime((long) (speed * HOUR_TO_MILLISECOND)));
|
||||
}
|
||||
|
||||
int totalDistanceId = metricUnits ? R.plurals.voiceTotalDistanceKilometers
|
||||
: R.plurals.voiceTotalDistanceMiles;
|
||||
String totalDistance = context.getResources()
|
||||
.getQuantityString(totalDistanceId, getQuantityCount(distance), distance);
|
||||
|
||||
return context.getString(R.string.voice_template, totalDistance, getAnnounceTime(
|
||||
tripStatistics.getMovingTime()), rate);
|
||||
}
|
||||
|
||||
/**
|
||||
* Listens to phone state.
|
||||
*
|
||||
* @param listener the listener
|
||||
* @param events the interested events
|
||||
*/
|
||||
@VisibleForTesting
|
||||
protected void listenToPhoneState(PhoneStateListener listener, int events) {
|
||||
TelephonyManager telephony = (TelephonyManager) context.getSystemService(
|
||||
Context.TELEPHONY_SERVICE);
|
||||
if (telephony != null) {
|
||||
telephony.listen(listener, events);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the announce time.
|
||||
*
|
||||
* @param time the time
|
||||
*/
|
||||
@VisibleForTesting
|
||||
String getAnnounceTime(long time) {
|
||||
int[] parts = StringUtils.getTimeParts(time);
|
||||
String seconds = context.getResources()
|
||||
.getQuantityString(R.plurals.voiceSeconds, parts[0], parts[0]);
|
||||
String minutes = context.getResources()
|
||||
.getQuantityString(R.plurals.voiceMinutes, parts[1], parts[1]);
|
||||
String hours = context.getResources()
|
||||
.getQuantityString(R.plurals.voiceHours, parts[2], parts[2]);
|
||||
StringBuilder sb = new StringBuilder();
|
||||
if (parts[2] != 0) {
|
||||
sb.append(hours);
|
||||
sb.append(" ");
|
||||
sb.append(minutes);
|
||||
} else {
|
||||
sb.append(minutes);
|
||||
sb.append(" ");
|
||||
sb.append(seconds);
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the plural count to be used by getQuantityString. getQuantityString
|
||||
* only supports integer quantities, not a double quantity like "2.2".
|
||||
* <p>
|
||||
* As a temporary workaround, we convert a double quantity to an integer
|
||||
* quantity. If the double quantity is exactly 0, 1, or 2, then we can return
|
||||
* these integer quantities. Otherwise, we cast the double quantity to an
|
||||
* integer quantity. However, we need to make sure that if the casted value is
|
||||
* 0, 1, or 2, we don't return those, instead, return the next biggest integer
|
||||
* 3.
|
||||
*
|
||||
* @param d the double value
|
||||
*/
|
||||
private int getQuantityCount(double d) {
|
||||
if (d == 0) {
|
||||
return 0;
|
||||
} else if (d == 1) {
|
||||
return 1;
|
||||
} else if (d == 2) {
|
||||
return 2;
|
||||
} else {
|
||||
int count = (int) d;
|
||||
return count < 3 ? 3 : count;
|
||||
}
|
||||
}
|
||||
}
|
||||
+4
-8
@@ -20,18 +20,14 @@ import com.google.android.apps.mytracks.util.ApiAdapterFactory;
|
||||
import android.content.Context;
|
||||
|
||||
/**
|
||||
* Factory which wraps construction and setup of text-to-speech announcements in
|
||||
* an API-level-safe way.
|
||||
*
|
||||
* A {@link PeriodicTaskFactory} for text-to-speech announcement periodic task.
|
||||
*
|
||||
* @author Rodrigo Damazio
|
||||
*/
|
||||
public class StatusAnnouncerFactory implements PeriodicTaskFactory {
|
||||
|
||||
public StatusAnnouncerFactory() {
|
||||
}
|
||||
public class AnnouncementPeriodicTaskFactory implements PeriodicTaskFactory {
|
||||
|
||||
@Override
|
||||
public PeriodicTask create(Context context) {
|
||||
return ApiAdapterFactory.getApiAdapter().getStatusAnnouncerTask(context);
|
||||
return ApiAdapterFactory.getApiAdapter().getAnnouncementPeriodicTask(context);
|
||||
}
|
||||
}
|
||||
+20
-22
@@ -16,8 +16,6 @@
|
||||
|
||||
package com.google.android.apps.mytracks.services.tasks;
|
||||
|
||||
import static com.google.android.apps.mytracks.Constants.TAG;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.content.Context;
|
||||
import android.media.AudioManager;
|
||||
@@ -27,57 +25,57 @@ import android.util.Log;
|
||||
|
||||
import java.util.HashMap;
|
||||
|
||||
|
||||
/**
|
||||
* This class will periodically announce the user's trip statistics. This class
|
||||
* will request and release audio focus. <br>
|
||||
* For API Level 8 or higher.
|
||||
*
|
||||
* will request and release audio focus.
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
*/
|
||||
@TargetApi(8)
|
||||
public class Api8StatusAnnouncerTask extends StatusAnnouncerTask {
|
||||
private final static HashMap<String, String> SPEECH_PARAMS = new HashMap<String, String>();
|
||||
public class Api8AnnouncementPeriodicTask extends AnnouncementPeriodicTask {
|
||||
|
||||
private static final String TAG = Api8AnnouncementPeriodicTask.class.getSimpleName();
|
||||
private static final HashMap<String, String> SPEECH_PARAMS = new HashMap<String, String>();
|
||||
static {
|
||||
SPEECH_PARAMS.put(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID, "not_used");
|
||||
}
|
||||
|
||||
private final AudioManager audioManager;
|
||||
private final OnUtteranceCompletedListener utteranceListener =
|
||||
new OnUtteranceCompletedListener() {
|
||||
@Override
|
||||
private final OnUtteranceCompletedListener
|
||||
utteranceListener = new OnUtteranceCompletedListener() {
|
||||
@Override
|
||||
public void onUtteranceCompleted(String utteranceId) {
|
||||
int result = audioManager.abandonAudioFocus(null);
|
||||
if (result == AudioManager.AUDIOFOCUS_REQUEST_FAILED) {
|
||||
Log.w(TAG, "FroyoStatusAnnouncerTask: Failed to relinquish audio focus");
|
||||
Log.w(TAG, "Failed to relinquish audio focus.");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
public Api8StatusAnnouncerTask(Context context) {
|
||||
super(context);
|
||||
private final AudioManager audioManager;
|
||||
|
||||
public Api8AnnouncementPeriodicTask(Context context) {
|
||||
super(context);
|
||||
audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onTtsReady() {
|
||||
super.onTtsReady();
|
||||
|
||||
tts.setOnUtteranceCompletedListener(utteranceListener);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected synchronized void speakAnnouncement(String announcement) {
|
||||
int result = audioManager.requestAudioFocus(null,
|
||||
TextToSpeech.Engine.DEFAULT_STREAM,
|
||||
AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK);
|
||||
int result = audioManager.requestAudioFocus(
|
||||
null, TextToSpeech.Engine.DEFAULT_STREAM, AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK);
|
||||
if (result == AudioManager.AUDIOFOCUS_REQUEST_FAILED) {
|
||||
Log.w(TAG, "FroyoStatusAnnouncerTask: Request for audio focus failed.");
|
||||
Log.w(TAG, "Failed to request audio focus.");
|
||||
}
|
||||
|
||||
// We don't care about the utterance id.
|
||||
// It is supplied here to force onUtteranceCompleted to be called.
|
||||
/*
|
||||
* We don't care about the utterance id. It is supplied here to force
|
||||
* onUtteranceCompleted to be called.
|
||||
*/
|
||||
tts.speak(announcement, TextToSpeech.QUEUE_FLUSH, SPEECH_PARAMS);
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,7 @@ import com.google.android.apps.mytracks.services.TrackRecordingService;
|
||||
|
||||
/**
|
||||
* This is interface for a task that will be executed on some schedule.
|
||||
*
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
*/
|
||||
public interface PeriodicTask {
|
||||
@@ -28,11 +28,13 @@ public interface PeriodicTask {
|
||||
* Sets up this task for subsequent calls to the run method.
|
||||
*/
|
||||
public void start();
|
||||
|
||||
|
||||
/**
|
||||
* This method will be called periodically.
|
||||
*
|
||||
* @param trackRecordingService the track recording service
|
||||
*/
|
||||
public void run(TrackRecordingService service);
|
||||
public void run(TrackRecordingService trackRecordingService);
|
||||
|
||||
/**
|
||||
* Shuts down this task and clean up resources.
|
||||
|
||||
+102
-91
@@ -15,119 +15,167 @@
|
||||
*/
|
||||
package com.google.android.apps.mytracks.services.tasks;
|
||||
|
||||
import static com.google.android.apps.mytracks.Constants.TAG;
|
||||
|
||||
import com.google.android.apps.mytracks.services.TrackRecordingService;
|
||||
import com.google.android.apps.mytracks.stats.TripStatistics;
|
||||
import com.google.android.apps.mytracks.util.PreferencesUtils;
|
||||
import com.google.android.apps.mytracks.util.UnitConversions;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
/**
|
||||
* Execute a task on a time or distance schedule.
|
||||
*
|
||||
* Execute a periodic task on a time or distance schedule.
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
*/
|
||||
public class PeriodicTaskExecutor {
|
||||
|
||||
private static final String TAG = PeriodicTaskExecutor.class.getSimpleName();
|
||||
private static final long MINUTE_TO_MILLISECONDS = 60000L;
|
||||
|
||||
private final TrackRecordingService trackRecordingService;
|
||||
private final PeriodicTaskFactory periodicTaskFactory;
|
||||
|
||||
/**
|
||||
* The frequency of the task.
|
||||
* A value greater than zero is a frequency in time.
|
||||
* A value less than zero is considered a frequency in distance.
|
||||
* The task frequency. A positive value is a time frequency (minutes). A
|
||||
* negative value is a distance frequency (km or mi). A zero value is to turn
|
||||
* off periodic task.
|
||||
*/
|
||||
private int taskFrequency = PreferencesUtils.FREQUENCY_OFF;
|
||||
|
||||
/**
|
||||
* The next distance when the task should execute.
|
||||
*/
|
||||
private double nextTaskDistance = 0;
|
||||
private PeriodicTask periodicTask;
|
||||
|
||||
/**
|
||||
* Time based executor.
|
||||
*/
|
||||
private TimerTaskExecutor timerExecutor = null;
|
||||
// Time periodic task executor
|
||||
private TimerTaskExecutor timerTaskExecutor = null;
|
||||
|
||||
private boolean metricUnits;
|
||||
|
||||
private final TrackRecordingService service;
|
||||
// The next distance for the distance periodic task
|
||||
private double nextTaskDistance = Double.MAX_VALUE;
|
||||
|
||||
private final PeriodicTaskFactory factory;
|
||||
|
||||
private PeriodicTask task;
|
||||
|
||||
public PeriodicTaskExecutor(TrackRecordingService service, PeriodicTaskFactory factory) {
|
||||
this.service = service;
|
||||
this.factory = factory;
|
||||
public PeriodicTaskExecutor(
|
||||
TrackRecordingService trackRecordingService, PeriodicTaskFactory periodicTaskFactory) {
|
||||
this.trackRecordingService = trackRecordingService;
|
||||
this.periodicTaskFactory = periodicTaskFactory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Restores the manager.
|
||||
* Restores the executor.
|
||||
*/
|
||||
public void restore() {
|
||||
// TODO: Decouple service from this class once and forever.
|
||||
if (!service.isRecording()) {
|
||||
if (!trackRecordingService.isRecording()) {
|
||||
Log.d(TAG, "Not recording.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isTimeFrequency()) {
|
||||
if (timerExecutor != null) {
|
||||
timerExecutor.shutdown();
|
||||
timerExecutor = null;
|
||||
if (timerTaskExecutor != null) {
|
||||
timerTaskExecutor.shutdown();
|
||||
timerTaskExecutor = null;
|
||||
}
|
||||
}
|
||||
if (taskFrequency == PreferencesUtils.FREQUENCY_OFF) {
|
||||
Log.d(TAG, "Task frequency is off.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Try to make the task.
|
||||
task = factory.create(service);
|
||||
// Returning null is ok.
|
||||
if (task == null) {
|
||||
periodicTask = periodicTaskFactory.create(trackRecordingService);
|
||||
|
||||
// Returning null is ok
|
||||
if (periodicTask == null) {
|
||||
Log.d(TAG, "Peridoic task is null.");
|
||||
return;
|
||||
}
|
||||
task.start();
|
||||
periodicTask.start();
|
||||
|
||||
if (isTimeFrequency()) {
|
||||
if (timerExecutor == null) {
|
||||
timerExecutor = new TimerTaskExecutor(task, service);
|
||||
if (timerTaskExecutor == null) {
|
||||
timerTaskExecutor = new TimerTaskExecutor(periodicTask, trackRecordingService);
|
||||
}
|
||||
timerExecutor.scheduleTask(taskFrequency * 60000L);
|
||||
timerTaskExecutor.scheduleTask(taskFrequency * MINUTE_TO_MILLISECONDS);
|
||||
} else {
|
||||
// For distance based splits.
|
||||
// For distance periodic task
|
||||
calculateNextTaskDistance();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shuts down the manager.
|
||||
* Shuts down the executor.
|
||||
*/
|
||||
public void shutdown() {
|
||||
if (task != null) {
|
||||
task.shutdown();
|
||||
task = null;
|
||||
if (periodicTask != null) {
|
||||
periodicTask.shutdown();
|
||||
periodicTask = null;
|
||||
}
|
||||
if (timerExecutor != null) {
|
||||
timerExecutor.shutdown();
|
||||
timerExecutor = null;
|
||||
if (timerTaskExecutor != null) {
|
||||
timerTaskExecutor.shutdown();
|
||||
timerTaskExecutor = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the next distance when the task should execute.
|
||||
* Updates the executor.
|
||||
*/
|
||||
void calculateNextTaskDistance() {
|
||||
// TODO: Decouple service from this class once and forever.
|
||||
if (!service.isRecording() || task == null) {
|
||||
public void update() {
|
||||
if (!isDistanceFrequency() || periodicTask == null) {
|
||||
return;
|
||||
}
|
||||
TripStatistics tripStatistics = trackRecordingService.getTripStatistics();
|
||||
if (tripStatistics == null) {
|
||||
return;
|
||||
}
|
||||
double distance = tripStatistics.getTotalDistance()
|
||||
* UnitConversions.M_TO_KM;
|
||||
if (!metricUnits) {
|
||||
distance *= UnitConversions.KM_TO_MI;
|
||||
}
|
||||
|
||||
if (distance > nextTaskDistance) {
|
||||
periodicTask.run(trackRecordingService);
|
||||
calculateNextTaskDistance();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets task frequency.
|
||||
*
|
||||
* @param taskFrequency the task frequency
|
||||
*/
|
||||
public void setTaskFrequency(int taskFrequency) {
|
||||
this.taskFrequency = taskFrequency;
|
||||
restore();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets metricUnits.
|
||||
*
|
||||
* @param metricUnits true to use metric units
|
||||
*/
|
||||
public void setMetricUnits(boolean metricUnits) {
|
||||
this.metricUnits = metricUnits;
|
||||
calculateNextTaskDistance();
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the next distance for the distance periodic task.
|
||||
*/
|
||||
private void calculateNextTaskDistance() {
|
||||
if (!trackRecordingService.isRecording() || periodicTask == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
TripStatistics tripStatistics = trackRecordingService.getTripStatistics();
|
||||
if (tripStatistics == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isDistanceFrequency()) {
|
||||
nextTaskDistance = Double.MAX_VALUE;
|
||||
Log.d(TAG, "SplitManager: Distance splits disabled.");
|
||||
return;
|
||||
}
|
||||
|
||||
double distance = service.getTripStatistics().getTotalDistance() * UnitConversions.M_TO_KM;
|
||||
double distance = tripStatistics.getTotalDistance()
|
||||
* UnitConversions.M_TO_KM;
|
||||
if (!metricUnits) {
|
||||
distance *= UnitConversions.KM_TO_MI;
|
||||
}
|
||||
@@ -135,56 +183,19 @@ public class PeriodicTaskExecutor {
|
||||
int index = (int) (distance / taskFrequency);
|
||||
index -= 1;
|
||||
nextTaskDistance = taskFrequency * index;
|
||||
Log.d(TAG, "SplitManager: Next split distance: " + nextTaskDistance);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates executer with new trip statistics.
|
||||
* True if time frequency.
|
||||
*/
|
||||
public void update() {
|
||||
if (!isDistanceFrequency() || task == null) {
|
||||
return;
|
||||
}
|
||||
// Convert the distance in meters to km or mi.
|
||||
double distance = service.getTripStatistics().getTotalDistance() * UnitConversions.M_TO_KM;
|
||||
if (!metricUnits) {
|
||||
distance *= UnitConversions.KM_TO_MI;
|
||||
}
|
||||
|
||||
if (distance > nextTaskDistance) {
|
||||
task.run(service);
|
||||
calculateNextTaskDistance();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isTimeFrequency() {
|
||||
return taskFrequency > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* True if distance frequency.
|
||||
*/
|
||||
private boolean isDistanceFrequency() {
|
||||
return taskFrequency < 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the task frequency.
|
||||
* < 0 Use the absolute value as a distance in the current measurement km
|
||||
* or mi
|
||||
* 0 Turn off the task
|
||||
* > 0 Use the value as a time in minutes
|
||||
* @param taskFrequency The frequency in time or distance
|
||||
*/
|
||||
public void setTaskFrequency(int taskFrequency) {
|
||||
Log.d(TAG, "setTaskFrequency: taskFrequency = " + taskFrequency);
|
||||
this.taskFrequency = taskFrequency;
|
||||
restore();
|
||||
}
|
||||
|
||||
public void setMetricUnits(boolean metricUnits) {
|
||||
this.metricUnits = metricUnits;
|
||||
calculateNextTaskDistance();
|
||||
}
|
||||
|
||||
double getNextTaskDistance() {
|
||||
return nextTaskDistance;
|
||||
}
|
||||
}
|
||||
|
||||
+7
-6
@@ -13,21 +13,22 @@
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
|
||||
package com.google.android.apps.mytracks.services.tasks;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
/**
|
||||
* An interface for classes that can create periodic tasks.
|
||||
*
|
||||
* An interface for classes that can create {@link PeriodicTask}.
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
*/
|
||||
public interface PeriodicTaskFactory {
|
||||
|
||||
/**
|
||||
* Creates a periodic task which does voice announcements.
|
||||
*
|
||||
* @return the task, or null if task is not supported
|
||||
* Creates a {@link PeriodicTask}.
|
||||
*
|
||||
* @return the task, or null if the task is not supported
|
||||
*/
|
||||
PeriodicTask create(Context context);
|
||||
public PeriodicTask create(Context context);
|
||||
}
|
||||
+9
-27
@@ -19,39 +19,21 @@ package com.google.android.apps.mytracks.services.tasks;
|
||||
import com.google.android.apps.mytracks.content.WaypointCreationRequest;
|
||||
import com.google.android.apps.mytracks.services.TrackRecordingService;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
|
||||
/**
|
||||
* A simple task to insert statistics markers periodically.
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
*/
|
||||
public class SplitTask implements PeriodicTask {
|
||||
public class SplitPeriodicTask implements PeriodicTask {
|
||||
|
||||
private SplitTask() {
|
||||
@Override
|
||||
public void start() {}
|
||||
|
||||
@Override
|
||||
public void run(TrackRecordingService trackRecordingService) {
|
||||
trackRecordingService.insertWaypoint(WaypointCreationRequest.DEFAULT_STATISTICS);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run(TrackRecordingService service) {
|
||||
service.insertWaypoint(WaypointCreationRequest.DEFAULT_STATISTICS);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void shutdown() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Create new SplitTasks.
|
||||
*/
|
||||
public static class Factory implements PeriodicTaskFactory {
|
||||
|
||||
@Override
|
||||
public PeriodicTask create(Context context) {
|
||||
return new SplitTask();
|
||||
}
|
||||
}
|
||||
public void shutdown() {}
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* 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.services.tasks;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
/**
|
||||
* A {@link PeriodicTaskFactory} for {@link SplitPeriodicTask}.
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
public class SplitPeriodicTaskFactory implements PeriodicTaskFactory {
|
||||
|
||||
@Override
|
||||
public PeriodicTask create(Context context) {
|
||||
return new SplitPeriodicTask();
|
||||
}
|
||||
}
|
||||
-356
@@ -1,356 +0,0 @@
|
||||
/*
|
||||
* Copyright 2009 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.services.tasks;
|
||||
|
||||
import static com.google.android.apps.mytracks.Constants.TAG;
|
||||
|
||||
import com.google.android.apps.mytracks.Constants;
|
||||
import com.google.android.apps.mytracks.services.TrackRecordingService;
|
||||
import com.google.android.apps.mytracks.stats.TripStatistics;
|
||||
import com.google.android.apps.mytracks.util.PreferencesUtils;
|
||||
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.speech.tts.TextToSpeech;
|
||||
import android.speech.tts.TextToSpeech.OnInitListener;
|
||||
import android.telephony.PhoneStateListener;
|
||||
import android.telephony.TelephonyManager;
|
||||
import android.util.Log;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* This class will periodically announce the user's trip statistics.
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
*/
|
||||
public class StatusAnnouncerTask implements PeriodicTask {
|
||||
|
||||
/**
|
||||
* The rate at which announcements are spoken.
|
||||
*/
|
||||
// @VisibleForTesting
|
||||
static final float TTS_SPEECH_RATE = 0.9f;
|
||||
|
||||
/**
|
||||
* A pointer to the service context.
|
||||
*/
|
||||
private final Context context;
|
||||
|
||||
/**
|
||||
* The interface to the text to speech engine.
|
||||
*/
|
||||
protected TextToSpeech tts;
|
||||
|
||||
/**
|
||||
* The response received from the TTS engine after initialization.
|
||||
*/
|
||||
private int initStatus = TextToSpeech.ERROR;
|
||||
|
||||
/**
|
||||
* Whether the TTS engine is ready.
|
||||
*/
|
||||
private boolean ready = false;
|
||||
|
||||
/**
|
||||
* Whether we're allowed to speak right now.
|
||||
*/
|
||||
private boolean speechAllowed;
|
||||
|
||||
/**
|
||||
* Listener which updates {@link #speechAllowed} when the phone state changes.
|
||||
*/
|
||||
private final PhoneStateListener phoneListener = new PhoneStateListener() {
|
||||
@Override
|
||||
public void onCallStateChanged(int state, String incomingNumber) {
|
||||
speechAllowed = state == TelephonyManager.CALL_STATE_IDLE;
|
||||
|
||||
if (!speechAllowed && tts != null && tts.isSpeaking()) {
|
||||
// If we're already speaking, stop it.
|
||||
tts.stop();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
public StatusAnnouncerTask(Context context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* Announces the trip status.
|
||||
*/
|
||||
@Override
|
||||
public void run(TrackRecordingService service) {
|
||||
if (service == null) {
|
||||
Log.e(TAG, "StatusAnnouncer TrackRecordingService not initialized");
|
||||
return;
|
||||
}
|
||||
|
||||
runWithStatistics(service.getTripStatistics());
|
||||
}
|
||||
|
||||
/**
|
||||
* This method exists as a convenience for testing code, allowing said code
|
||||
* to avoid needing to instantiate an entire {@link TrackRecordingService}
|
||||
* just to test the announcer.
|
||||
*/
|
||||
// @VisibleForTesting
|
||||
void runWithStatistics(TripStatistics statistics) {
|
||||
if (statistics == null) {
|
||||
Log.e(TAG, "StatusAnnouncer stats not initialized.");
|
||||
return;
|
||||
}
|
||||
|
||||
synchronized (this) {
|
||||
checkReady();
|
||||
if (!ready) {
|
||||
Log.e(TAG, "StatusAnnouncer Tts not ready.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!speechAllowed) {
|
||||
Log.i(Constants.TAG,
|
||||
"Not making announcement - not allowed at this time");
|
||||
return;
|
||||
}
|
||||
|
||||
String announcement = getAnnouncement(statistics);
|
||||
Log.d(Constants.TAG, "Announcement: " + announcement);
|
||||
speakAnnouncement(announcement);
|
||||
}
|
||||
|
||||
protected void speakAnnouncement(String announcement) {
|
||||
tts.speak(announcement, TextToSpeech.QUEUE_FLUSH, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the announcement string.
|
||||
*
|
||||
* @return The string that will be read to the user
|
||||
*/
|
||||
// @VisibleForTesting
|
||||
protected String getAnnouncement(TripStatistics stats) {
|
||||
boolean metricUnits = PreferencesUtils.getBoolean(
|
||||
context, R.string.metric_units_key, PreferencesUtils.METRIC_UNITS_DEFAULT);
|
||||
boolean reportSpeed = PreferencesUtils.getBoolean(
|
||||
context, R.string.report_speed_key, PreferencesUtils.REPORT_SPEED_DEFAULT);
|
||||
double d = stats.getTotalDistance() * UnitConversions.M_TO_KM;
|
||||
double s = stats.getAverageMovingSpeed() * UnitConversions.MS_TO_KMH;
|
||||
|
||||
if (d == 0) {
|
||||
return context.getString(R.string.voice_total_distance_zero);
|
||||
}
|
||||
|
||||
if (!metricUnits) {
|
||||
d *= UnitConversions.KM_TO_MI;
|
||||
s *= UnitConversions.KM_TO_MI;
|
||||
}
|
||||
|
||||
if (!reportSpeed) {
|
||||
s = 3600000.0 / s; // converts from speed to pace
|
||||
}
|
||||
|
||||
// Makes sure s is not NaN.
|
||||
if (Double.isNaN(s)) {
|
||||
s = 0;
|
||||
}
|
||||
|
||||
String speed;
|
||||
if (reportSpeed) {
|
||||
int speedId = metricUnits ? R.plurals.voiceSpeedKilometersPerHour
|
||||
: R.plurals.voiceSpeedMilesPerHour;
|
||||
speed = context.getResources().getQuantityString(speedId, getQuantityCount(s), s);
|
||||
} else {
|
||||
int paceId = metricUnits ? R.string.voice_pace_per_kilometer : R.string.voice_pace_per_mile;
|
||||
speed = context.getString(paceId, getAnnounceTime((long) s));
|
||||
}
|
||||
|
||||
int totalDistanceId = metricUnits ? R.plurals.voiceTotalDistanceKilometers
|
||||
: R.plurals.voiceTotalDistanceMiles;
|
||||
String totalDistance = context.getResources().getQuantityString(
|
||||
totalDistanceId, getQuantityCount(d), d);
|
||||
|
||||
return context.getString(
|
||||
R.string.voice_template, totalDistance, getAnnounceTime(stats.getMovingTime()), speed);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the plural count to be used by getQuantityString. getQuantityString
|
||||
* only supports integer quantities, not a double quantity like "2.2".
|
||||
* <p>
|
||||
* As a temporary workaround, we convert a double quantity to an integer
|
||||
* quantity. If the double quantity is exactly 0, 1, or 2, then we can return
|
||||
* these integer quantities. Otherwise, we cast the double quantity to an
|
||||
* integer quantity. However, we need to make sure that if the casted value is
|
||||
* 0, 1, or 2, we don't return those, instead, return the next biggest integer
|
||||
* 3.
|
||||
*
|
||||
* @param d the double value
|
||||
*/
|
||||
private int getQuantityCount(double d) {
|
||||
if (d == 0) {
|
||||
return 0;
|
||||
} else if (d == 1) {
|
||||
return 1;
|
||||
} else if (d == 2) {
|
||||
return 2;
|
||||
} else {
|
||||
int count = (int) d;
|
||||
return count < 3 ? 3 : count;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
Log.i(Constants.TAG, "Starting TTS");
|
||||
if (tts == null) {
|
||||
// We can't have this class also be the listener, otherwise it's unsafe to
|
||||
// reference it in Cupcake (even if we don't instantiate it).
|
||||
tts = newTextToSpeech(context, new OnInitListener() {
|
||||
@Override
|
||||
public void onInit(int status) {
|
||||
onTtsInit(status);
|
||||
}
|
||||
});
|
||||
}
|
||||
speechAllowed = true;
|
||||
|
||||
// Register ourselves as a listener so we won't speak during a call.
|
||||
listenToPhoneState(phoneListener, PhoneStateListener.LISTEN_CALL_STATE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the TTS engine is initialized.
|
||||
*/
|
||||
private void onTtsInit(int status) {
|
||||
Log.i(TAG, "TrackRecordingService.TTS init: " + status);
|
||||
synchronized (this) {
|
||||
// TTS should be valid here but NPE exceptions were reported to the market.
|
||||
initStatus = status;
|
||||
checkReady();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures that the TTS is ready (finishing its initialization if needed).
|
||||
*/
|
||||
private void checkReady() {
|
||||
synchronized (this) {
|
||||
if (ready) {
|
||||
// Already done;
|
||||
return;
|
||||
}
|
||||
|
||||
ready = initStatus == TextToSpeech.SUCCESS && tts != null;
|
||||
Log.d(TAG, "Status announcer ready: " + ready);
|
||||
|
||||
if (ready) {
|
||||
onTtsReady();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finishes the TTS engine initialization.
|
||||
* Called once (and only once) when the TTS engine is ready.
|
||||
*/
|
||||
protected void onTtsReady() {
|
||||
// Force the language to be the same as the string we will be speaking,
|
||||
// if that's available.
|
||||
Locale speechLanguage = Locale.getDefault();
|
||||
int languageAvailability = tts.isLanguageAvailable(speechLanguage);
|
||||
if (languageAvailability == TextToSpeech.LANG_MISSING_DATA ||
|
||||
languageAvailability == TextToSpeech.LANG_NOT_SUPPORTED) {
|
||||
// English is probably supported.
|
||||
// TODO: Somehow use announcement strings from English too.
|
||||
Log.w(TAG, "Default language not available, using English.");
|
||||
speechLanguage = Locale.ENGLISH;
|
||||
}
|
||||
tts.setLanguage(speechLanguage);
|
||||
|
||||
// Slow down the speed just a bit as it is hard to hear when exercising.
|
||||
tts.setSpeechRate(TTS_SPEECH_RATE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void shutdown() {
|
||||
// Stop listening to phone state.
|
||||
listenToPhoneState(phoneListener, PhoneStateListener.LISTEN_NONE);
|
||||
|
||||
if (tts != null) {
|
||||
tts.shutdown();
|
||||
tts = null;
|
||||
}
|
||||
|
||||
Log.i(Constants.TAG, "TTS shut down");
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper for instantiating a {@link TextToSpeech} object, which causes
|
||||
* several issues during testing.
|
||||
*/
|
||||
// @VisibleForTesting
|
||||
protected TextToSpeech newTextToSpeech(Context ctx, OnInitListener onInitListener) {
|
||||
return new TextToSpeech(ctx, onInitListener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper for calls to the 100%-unmockable {@link TelephonyManager#listen}.
|
||||
*/
|
||||
// @VisibleForTesting
|
||||
protected void listenToPhoneState(PhoneStateListener listener, int events) {
|
||||
TelephonyManager telephony =
|
||||
(TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
|
||||
if (telephony != null) {
|
||||
telephony.listen(listener, events);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a string to announce the time.
|
||||
*
|
||||
* @param time the time
|
||||
*/
|
||||
@VisibleForTesting
|
||||
String getAnnounceTime(long time) {
|
||||
int[] parts = StringUtils.getTimeParts(time);
|
||||
String seconds = context.getResources().getQuantityString(
|
||||
R.plurals.voiceSeconds, parts[0], parts[0]);
|
||||
String minutes = context.getResources().getQuantityString(
|
||||
R.plurals.voiceMinutes, parts[1], parts[1]);
|
||||
String hours = context.getResources().getQuantityString(
|
||||
R.plurals.voiceHours, parts[2], parts[2]);
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
if (parts[2] != 0) {
|
||||
sb.append(hours);
|
||||
sb.append(" ");
|
||||
sb.append(minutes);
|
||||
} else {
|
||||
sb.append(minutes);
|
||||
sb.append(" ");
|
||||
sb.append(seconds);
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
+26
-30
@@ -16,46 +16,46 @@
|
||||
|
||||
package com.google.android.apps.mytracks.services.tasks;
|
||||
|
||||
import static com.google.android.apps.mytracks.Constants.TAG;
|
||||
|
||||
import android.util.Log;
|
||||
import com.google.android.apps.mytracks.services.TrackRecordingService;
|
||||
import com.google.android.apps.mytracks.stats.TripStatistics;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.Timer;
|
||||
import java.util.TimerTask;
|
||||
|
||||
import com.google.android.apps.mytracks.services.TrackRecordingService;
|
||||
|
||||
/**
|
||||
* This class will periodically perform a task.
|
||||
*
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
*/
|
||||
public class TimerTaskExecutor {
|
||||
|
||||
private final PeriodicTask task;
|
||||
private final TrackRecordingService service;
|
||||
private final PeriodicTask periodicTask;
|
||||
private final TrackRecordingService trackRecordingService;
|
||||
|
||||
/**
|
||||
* A timer to schedule the announcements.
|
||||
* This is non-null if the task is in started (scheduled) state.
|
||||
* A timer to schedule the announcements. This is non-null if the task is in
|
||||
* started (scheduled) state.
|
||||
*/
|
||||
private Timer timer;
|
||||
|
||||
public TimerTaskExecutor(PeriodicTask task,
|
||||
TrackRecordingService service) {
|
||||
this.task = task;
|
||||
this.service = service;
|
||||
public TimerTaskExecutor(PeriodicTask periodicTask, TrackRecordingService trackRecordingService) {
|
||||
this.periodicTask = periodicTask;
|
||||
this.trackRecordingService = trackRecordingService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedules the task at the given interval.
|
||||
*
|
||||
* @param interval The interval in milliseconds
|
||||
* Schedules the periodic task at an interval.
|
||||
*
|
||||
* @param interval the interval in milliseconds
|
||||
*/
|
||||
public void scheduleTask(long interval) {
|
||||
// TODO: Decouple service from this class once and forever.
|
||||
if (!service.isRecording()) {
|
||||
if (!trackRecordingService.isRecording()) {
|
||||
return;
|
||||
}
|
||||
|
||||
TripStatistics tripStatistics = trackRecordingService.getTripStatistics();
|
||||
if (tripStatistics == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ public class TimerTaskExecutor {
|
||||
timer.purge();
|
||||
} else {
|
||||
// First start, or we were previously shut down.
|
||||
task.start();
|
||||
periodicTask.start();
|
||||
}
|
||||
|
||||
timer = new Timer();
|
||||
@@ -73,27 +73,22 @@ public class TimerTaskExecutor {
|
||||
}
|
||||
|
||||
long now = System.currentTimeMillis();
|
||||
long next = service.getTripStatistics().getStartTime();
|
||||
long next = tripStatistics.getStartTime();
|
||||
if (next < now) {
|
||||
next = now + interval - ((now - next) % interval);
|
||||
}
|
||||
|
||||
Date start = new Date(next);
|
||||
Log.i(TAG, task.getClass().getSimpleName() + " scheduled to start at " + start
|
||||
+ " every " + interval + " milliseconds.");
|
||||
timer.scheduleAtFixedRate(new PeriodicTimerTask(), start, interval);
|
||||
timer.scheduleAtFixedRate(new PeriodicTimerTask(), new Date(next), interval);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleans up this object.
|
||||
* Shuts down.
|
||||
*/
|
||||
public void shutdown() {
|
||||
Log.i(TAG, task.getClass().getSimpleName() + " shutting down.");
|
||||
if (timer != null) {
|
||||
timer.cancel();
|
||||
timer.purge();
|
||||
timer = null;
|
||||
task.shutdown();
|
||||
periodicTask.shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,9 +96,10 @@ public class TimerTaskExecutor {
|
||||
* The timer task to announce the trip status.
|
||||
*/
|
||||
private class PeriodicTimerTask extends TimerTask {
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
task.run(service);
|
||||
periodicTask.run(trackRecordingService);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,66 +20,35 @@ package com.google.android.apps.mytracks.stats;
|
||||
* This class maintains a buffer of doubles. This buffer is a convenient class
|
||||
* for storing a series of doubles and calculating information about them. This
|
||||
* is a FIFO buffer.
|
||||
*
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
*/
|
||||
public class DoubleBuffer {
|
||||
|
||||
/**
|
||||
* The location that the next write will occur at.
|
||||
*/
|
||||
// The location that the next write will occur at.
|
||||
private int index;
|
||||
|
||||
/**
|
||||
* The sliding buffer of doubles.
|
||||
*/
|
||||
// The sliding buffer of doubles.
|
||||
private final double[] buffer;
|
||||
|
||||
/**
|
||||
* Have all of the slots in the buffer been filled?
|
||||
*/
|
||||
// True if the buffer is full
|
||||
private boolean isFull;
|
||||
|
||||
/**
|
||||
* Creates a buffer with size elements.
|
||||
*
|
||||
* @param size the number of elements in the buffer
|
||||
* @throws IllegalArgumentException if the size is not a positive value
|
||||
* Creates a buffer with a certain size.
|
||||
*
|
||||
* @param size the size
|
||||
*/
|
||||
public DoubleBuffer(int size) {
|
||||
if (size < 1) {
|
||||
throw new IllegalArgumentException("The buffer size must be positive.");
|
||||
throw new IllegalArgumentException("The buffer size must be greater than 1.");
|
||||
}
|
||||
buffer = new double[size];
|
||||
reset();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a double to the buffer. If the buffer is full the oldest element is
|
||||
* overwritten.
|
||||
*
|
||||
* @param d the double to add
|
||||
*/
|
||||
public void setNext(double d) {
|
||||
if (index == buffer.length) {
|
||||
index = 0;
|
||||
}
|
||||
buffer[index] = d;
|
||||
index++;
|
||||
if (index == buffer.length) {
|
||||
isFull = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Are all of the entries in the buffer used?
|
||||
*/
|
||||
public boolean isFull() {
|
||||
return isFull;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the buffer to the initial state.
|
||||
* Resets the buffer.
|
||||
*/
|
||||
public void reset() {
|
||||
index = 0;
|
||||
@@ -87,16 +56,20 @@ public class DoubleBuffer {
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the average of values from the buffer.
|
||||
*
|
||||
* @return The average of the buffer
|
||||
* Returns true if the buffer is full.
|
||||
*/
|
||||
public boolean isFull() {
|
||||
return isFull;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the average of the buffer.
|
||||
*/
|
||||
public double getAverage() {
|
||||
int numberOfEntries = isFull ? buffer.length : index;
|
||||
if (numberOfEntries == 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
double sum = 0;
|
||||
for (int i = 0; i < numberOfEntries; i++) {
|
||||
sum += buffer[i];
|
||||
@@ -105,15 +78,15 @@ public class DoubleBuffer {
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the average and standard deviation of the buffer.
|
||||
*
|
||||
* @return An array of two elements - the first is the average, and the second
|
||||
* is the variance
|
||||
* Gets the average and the variance of the buffer.
|
||||
*
|
||||
* @return an array of two elements - the first is the average, the second is
|
||||
* the variance
|
||||
*/
|
||||
public double[] getAverageAndVariance() {
|
||||
int numberOfEntries = isFull ? buffer.length : index;
|
||||
if (numberOfEntries == 0) {
|
||||
return new double[]{0, 0};
|
||||
return new double[] { 0, 0 };
|
||||
}
|
||||
|
||||
double sum = 0;
|
||||
@@ -124,8 +97,25 @@ public class DoubleBuffer {
|
||||
}
|
||||
|
||||
double average = sum / numberOfEntries;
|
||||
return new double[]{average,
|
||||
sumSquares / numberOfEntries - Math.pow(average, 2)};
|
||||
double variance = sumSquares / numberOfEntries - Math.pow(average, 2);
|
||||
return new double[] { average, variance };
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a double to the buffer. If the buffer is full the oldest element is
|
||||
* overwritten.
|
||||
*
|
||||
* @param value the double to add
|
||||
*/
|
||||
public void setNext(double value) {
|
||||
if (index == buffer.length) {
|
||||
index = 0;
|
||||
}
|
||||
buffer[index] = value;
|
||||
index++;
|
||||
if (index == buffer.length) {
|
||||
isFull = true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -20,352 +20,307 @@ import static com.google.android.apps.mytracks.Constants.TAG;
|
||||
|
||||
import com.google.android.apps.mytracks.Constants;
|
||||
import com.google.android.apps.mytracks.util.PreferencesUtils;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
|
||||
import android.location.Location;
|
||||
import android.util.Log;
|
||||
|
||||
/**
|
||||
* Statistics keeper for a trip.
|
||||
* Builder for {@link TripStatistics}. For keeping statistics as a track is
|
||||
* paused/resumed and new locations are added.
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
* @author Rodrigo Damazio
|
||||
*/
|
||||
public class TripStatisticsBuilder {
|
||||
/**
|
||||
* Statistical data about the trip, which can be displayed to the user.
|
||||
*/
|
||||
private final TripStatistics data;
|
||||
|
||||
/**
|
||||
* The last location that the gps reported.
|
||||
*/
|
||||
private Location lastLocation;
|
||||
|
||||
/**
|
||||
* The last location that contributed to the stats. It is also the last
|
||||
* location the user was found to be moving.
|
||||
*/
|
||||
private Location lastMovingLocation;
|
||||
|
||||
/**
|
||||
* The current speed in meters/second as reported by the gps.
|
||||
*/
|
||||
private double currentSpeed;
|
||||
|
||||
/**
|
||||
* The current grade. This value is very noisy and not reported to the user.
|
||||
*/
|
||||
private double currentGrade;
|
||||
|
||||
/**
|
||||
* Is the trip currently paused?
|
||||
* All trips start paused.
|
||||
*/
|
||||
private boolean paused = true;
|
||||
|
||||
/**
|
||||
* A buffer of the last speed readings in meters/second.
|
||||
*/
|
||||
private final DoubleBuffer speedBuffer =
|
||||
new DoubleBuffer(Constants.SPEED_SMOOTHING_FACTOR);
|
||||
|
||||
/**
|
||||
* A buffer of the recent elevation readings in meters.
|
||||
*/
|
||||
private final DoubleBuffer elevationBuffer =
|
||||
new DoubleBuffer(Constants.ELEVATION_SMOOTHING_FACTOR);
|
||||
|
||||
/**
|
||||
* A buffer of the distance between recent gps readings in meters.
|
||||
*/
|
||||
private final DoubleBuffer distanceBuffer =
|
||||
new DoubleBuffer(Constants.DISTANCE_SMOOTHING_FACTOR);
|
||||
|
||||
/**
|
||||
* A buffer of the recent grade calculations.
|
||||
*/
|
||||
private final DoubleBuffer gradeBuffer =
|
||||
new DoubleBuffer(Constants.GRADE_SMOOTHING_FACTOR);
|
||||
|
||||
/**
|
||||
* The total number of locations in this trip.
|
||||
*/
|
||||
private long totalLocations = 0;
|
||||
// The trip statistics.
|
||||
private final TripStatistics tripStatistics;
|
||||
|
||||
// The minimum recording distance.
|
||||
private int minRecordingDistance = PreferencesUtils.MIN_RECORDING_DISTANCE_DEFAULT;
|
||||
|
||||
// True if the trip is paused. All trips start as paused.
|
||||
private boolean paused = true;
|
||||
|
||||
// The last location as reported by GPS.
|
||||
private Location lastLocation;
|
||||
|
||||
// The last moving location that contributed to the moving statistics.
|
||||
private Location lastMovingLocation;
|
||||
|
||||
// A buffer of the recent speed readings (m/s) for calculating max speed.
|
||||
private final DoubleBuffer speedBuffer = new DoubleBuffer(Constants.SPEED_SMOOTHING_FACTOR);
|
||||
|
||||
// A buffer of the recent elevation readings (m).
|
||||
private final DoubleBuffer elevationBuffer = new DoubleBuffer(
|
||||
Constants.ELEVATION_SMOOTHING_FACTOR);
|
||||
|
||||
// A buffer of the recent distance readings for calculating grade.
|
||||
private final DoubleBuffer distanceBuffer = new DoubleBuffer(Constants.DISTANCE_SMOOTHING_FACTOR);
|
||||
|
||||
// A buffer of the recent grade calculations
|
||||
private final DoubleBuffer gradeBuffer = new DoubleBuffer(Constants.GRADE_SMOOTHING_FACTOR);
|
||||
|
||||
/**
|
||||
* Creates a new trip starting at the given time.
|
||||
* Creates a new trip starting at a start time.
|
||||
*
|
||||
* @param startTime the start time.
|
||||
* @param startTime the start time
|
||||
*/
|
||||
public TripStatisticsBuilder(long startTime) {
|
||||
data = new TripStatistics();
|
||||
tripStatistics = new TripStatistics();
|
||||
resumeAt(startTime);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new trip, starting with existing statistics data.
|
||||
*
|
||||
* @param statsData the statistics data to copy and start from
|
||||
*/
|
||||
public TripStatisticsBuilder(TripStatistics statsData) {
|
||||
data = new TripStatistics(statsData);
|
||||
if (data.getStartTime() > 0) {
|
||||
resumeAt(data.getStartTime());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a location to the current trip. This will update all of the internal
|
||||
* variables with this new location.
|
||||
* Creates a new trip, starting with an existing {@link TripStatistics}.
|
||||
*
|
||||
* @param currentLocation the current gps location
|
||||
* @param systemTime the time used for calculation of totalTime. This should
|
||||
* be the phone's time (not GPS time)
|
||||
* @return true if the person is moving
|
||||
* @param other the existing {@link TripStatistics}
|
||||
*/
|
||||
public boolean addLocation(Location currentLocation, long systemTime) {
|
||||
if (paused) {
|
||||
Log.w(TAG,
|
||||
"Tried to account for location while track is paused");
|
||||
return false;
|
||||
public TripStatisticsBuilder(TripStatistics other) {
|
||||
tripStatistics = new TripStatistics(other);
|
||||
if (tripStatistics.getStartTime() > 0) {
|
||||
resumeAt(tripStatistics.getStartTime());
|
||||
}
|
||||
|
||||
totalLocations++;
|
||||
|
||||
double elevationDifference = updateElevation(currentLocation.getAltitude());
|
||||
|
||||
// Update the "instant" values:
|
||||
data.setTotalTime(systemTime - data.getStartTime());
|
||||
currentSpeed = currentLocation.getSpeed();
|
||||
|
||||
// This was the 1st location added, remember it and do nothing else:
|
||||
if (lastLocation == null) {
|
||||
lastLocation = currentLocation;
|
||||
lastMovingLocation = currentLocation;
|
||||
return false;
|
||||
}
|
||||
|
||||
updateBounds(currentLocation);
|
||||
|
||||
// Don't do anything if we didn't move since last fix:
|
||||
double distance = lastLocation.distanceTo(currentLocation);
|
||||
if (distance < minRecordingDistance &&
|
||||
currentSpeed < Constants.MAX_NO_MOVEMENT_SPEED) {
|
||||
lastLocation = currentLocation;
|
||||
return false;
|
||||
}
|
||||
|
||||
data.addTotalDistance(lastMovingLocation.distanceTo(currentLocation));
|
||||
updateSpeed(currentLocation.getTime(), currentSpeed,
|
||||
lastLocation.getTime(), lastLocation.getSpeed());
|
||||
|
||||
updateGrade(distance, elevationDifference);
|
||||
lastLocation = currentLocation;
|
||||
lastMovingLocation = currentLocation;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the track's bounding box to include the given location.
|
||||
*/
|
||||
private void updateBounds(Location location) {
|
||||
data.updateLatitudeExtremities(location.getLatitude());
|
||||
data.updateLongitudeExtremities(location.getLongitude());
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the elevation measurements.
|
||||
* Sets the min recording distance.
|
||||
*
|
||||
* @param elevation the current elevation
|
||||
* @param minRecordingDistance the min recording distance
|
||||
*/
|
||||
// @VisibleForTesting
|
||||
double updateElevation(double elevation) {
|
||||
double oldSmoothedElevation = getSmoothedElevation();
|
||||
elevationBuffer.setNext(elevation);
|
||||
double smoothedElevation = getSmoothedElevation();
|
||||
data.updateElevationExtremities(smoothedElevation);
|
||||
double elevationDifference = elevationBuffer.isFull()
|
||||
? smoothedElevation - oldSmoothedElevation
|
||||
: 0.0;
|
||||
if (elevationDifference > 0) {
|
||||
data.addTotalElevationGain(elevationDifference);
|
||||
}
|
||||
return elevationDifference;
|
||||
public void setMinRecordingDistance(int minRecordingDistance) {
|
||||
this.minRecordingDistance = minRecordingDistance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the speed measurements.
|
||||
* Resumes the current track at a given time.
|
||||
*
|
||||
* @param updateTime the time of the speed update
|
||||
* @param speed the current speed
|
||||
* @param lastLocationTime the time of the last speed update
|
||||
* @param lastLocationSpeed the speed of the last update
|
||||
* @param time the time
|
||||
*/
|
||||
// @VisibleForTesting
|
||||
void updateSpeed(long updateTime, double speed, long lastLocationTime,
|
||||
double lastLocationSpeed) {
|
||||
// We are now sure the user is moving.
|
||||
long timeDifference = updateTime - lastLocationTime;
|
||||
if (timeDifference < 0) {
|
||||
Log.e(TAG,
|
||||
"Found negative time change: " + timeDifference);
|
||||
}
|
||||
data.addMovingTime(timeDifference);
|
||||
|
||||
if (isValidSpeed(updateTime, speed, lastLocationTime, lastLocationSpeed,
|
||||
speedBuffer)) {
|
||||
speedBuffer.setNext(speed);
|
||||
if (speed > data.getMaxSpeed()) {
|
||||
data.setMaxSpeed(speed);
|
||||
}
|
||||
double movingSpeed = data.getAverageMovingSpeed();
|
||||
if (speedBuffer.isFull() && (movingSpeed > data.getMaxSpeed())) {
|
||||
data.setMaxSpeed(movingSpeed);
|
||||
}
|
||||
} else {
|
||||
Log.d(TAG,
|
||||
"TripStatistics ignoring big change: Raw Speed: " + speed
|
||||
+ " old: " + lastLocationSpeed + " [" + toString() + "]");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks to see if this is a valid speed.
|
||||
*
|
||||
* @param updateTime The time at the current reading
|
||||
* @param speed The current speed
|
||||
* @param lastLocationTime The time at the last location
|
||||
* @param lastLocationSpeed Speed at the last location
|
||||
* @param speedBuffer A buffer of recent readings
|
||||
* @return True if this is likely a valid speed
|
||||
*/
|
||||
public static boolean isValidSpeed(long updateTime, double speed,
|
||||
long lastLocationTime, double lastLocationSpeed,
|
||||
DoubleBuffer speedBuffer) {
|
||||
|
||||
// We don't want to count 0 towards the speed.
|
||||
if (speed == 0) {
|
||||
return false;
|
||||
}
|
||||
// We are now sure the user is moving.
|
||||
long timeDifference = updateTime - lastLocationTime;
|
||||
|
||||
// There are a lot of noisy speed readings.
|
||||
// Do the cheapest checks first, most expensive last.
|
||||
// The following code will ignore unlikely to be real readings.
|
||||
// - 128 m/s seems to be an internal android error code.
|
||||
if (Math.abs(speed - 128) < 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Another check for a spurious reading. See if the path seems physically
|
||||
// likely. Ignore any speeds that imply accelaration greater than 2g's
|
||||
// Really who can accelerate faster?
|
||||
double speedDifference = Math.abs(lastLocationSpeed - speed);
|
||||
if (speedDifference > Constants.MAX_ACCELERATION * timeDifference) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// There are three additional checks if the reading gets this far:
|
||||
// - Only use the speed if the buffer is full
|
||||
// - Check that the current speed is less than 10x the recent smoothed speed
|
||||
// - Double check that the current speed does not imply crazy acceleration
|
||||
double smoothedSpeed = speedBuffer.getAverage();
|
||||
double smoothedDiff = Math.abs(smoothedSpeed - speed);
|
||||
return !speedBuffer.isFull() ||
|
||||
(speed < smoothedSpeed * 10
|
||||
&& smoothedDiff < Constants.MAX_ACCELERATION * timeDifference);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the grade measurements.
|
||||
*
|
||||
* @param distance the distance the user just traveled
|
||||
* @param elevationDifference the elevation difference between the current
|
||||
* reading and the previous reading
|
||||
*/
|
||||
// @VisibleForTesting
|
||||
void updateGrade(double distance, double elevationDifference) {
|
||||
distanceBuffer.setNext(distance);
|
||||
double smoothedDistance = distanceBuffer.getAverage();
|
||||
|
||||
// With the error in the altitude measurement it is dangerous to divide
|
||||
// by anything less than 5.
|
||||
if (!elevationBuffer.isFull() || !distanceBuffer.isFull()
|
||||
|| smoothedDistance < 5.0) {
|
||||
public void resumeAt(long time) {
|
||||
if (!paused) {
|
||||
return;
|
||||
}
|
||||
currentGrade = elevationDifference / smoothedDistance;
|
||||
gradeBuffer.setNext(currentGrade);
|
||||
data.updateGradeExtremities(gradeBuffer.getAverage());
|
||||
|
||||
tripStatistics.setStartTime(time);
|
||||
tripStatistics.setStopTime(-1L);
|
||||
paused = false;
|
||||
lastLocation = null;
|
||||
lastMovingLocation = null;
|
||||
speedBuffer.reset();
|
||||
elevationBuffer.reset();
|
||||
distanceBuffer.reset();
|
||||
gradeBuffer.reset();
|
||||
}
|
||||
|
||||
/**
|
||||
* Pauses the track at the given time.
|
||||
* Pauses the track at a given time.
|
||||
*
|
||||
* @param time the time to pause at
|
||||
*/
|
||||
public void pauseAt(long time) {
|
||||
if (paused) { return; }
|
||||
|
||||
data.setStopTime(time);
|
||||
data.setTotalTime(time - data.getStartTime());
|
||||
lastLocation = null; // Make sure the counter restarts.
|
||||
if (paused) {
|
||||
return;
|
||||
}
|
||||
tripStatistics.setStopTime(time);
|
||||
// TODO: total time needs to take into account pauses
|
||||
tripStatistics.setTotalTime(time - tripStatistics.getStartTime());
|
||||
paused = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resumes the current track at the given time.
|
||||
*
|
||||
* @param time the time to resume at
|
||||
* Gets the trip statistics.
|
||||
*/
|
||||
public void resumeAt(long time) {
|
||||
if (!paused) { return; }
|
||||
|
||||
// TODO: The times are bogus if the track is paused then resumed again
|
||||
data.setStartTime(time);
|
||||
data.setStopTime(-1);
|
||||
paused = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "TripStatistics { Data: " + data.toString()
|
||||
+ "; Total Locations: " + totalLocations
|
||||
+ "; Paused: " + paused
|
||||
+ "; Current speed: " + currentSpeed
|
||||
+ "; Current grade: " + currentGrade
|
||||
+ "}";
|
||||
public TripStatistics getTripStatistics() {
|
||||
// Take a snapshot - we don't want anyone messing with our internals
|
||||
return new TripStatistics(tripStatistics);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the amount of time the user has been idle or 0 if they are moving.
|
||||
* Returns the amount of time the user has been idle or 0 if he is moving.
|
||||
*/
|
||||
public long getIdleTime() {
|
||||
if (lastLocation == null || lastMovingLocation == null)
|
||||
if (lastLocation == null || lastMovingLocation == null) {
|
||||
return 0;
|
||||
}
|
||||
return lastLocation.getTime() - lastMovingLocation.getTime();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current elevation smoothed over several readings. The elevation
|
||||
* data is very noisy so it is better to use the smoothed elevation than the
|
||||
* raw elevation for many tasks.
|
||||
*
|
||||
* @return The elevation smoothed over several readings
|
||||
* Gets the smoothed elevation over several readings. The elevation readings
|
||||
* is noisy so the smoothed elevation is better than the raw elevation for
|
||||
* many tasks.
|
||||
*/
|
||||
public double getSmoothedElevation() {
|
||||
return elevationBuffer.getAverage();
|
||||
}
|
||||
|
||||
public TripStatistics getStatistics() {
|
||||
// Take a snapshot - we don't want anyone messing with our internals
|
||||
return new TripStatistics(data);
|
||||
/**
|
||||
* Adds a location. This will update all of the internal variables with this
|
||||
* new location.
|
||||
*
|
||||
* @param location the location
|
||||
* @param systemTime the system time for calculating totalTime. This should be
|
||||
* the phone's system time (not GPS time)
|
||||
* @return true if the person is moving
|
||||
*/
|
||||
public boolean addLocation(Location location, long systemTime) {
|
||||
if (paused) {
|
||||
Log.w(TAG, "Track is paused. Ignore addLocation.");
|
||||
return false;
|
||||
}
|
||||
|
||||
tripStatistics.setTotalTime(systemTime - tripStatistics.getStartTime());
|
||||
|
||||
double elevationDifference = updateElevation(location.getAltitude());
|
||||
tripStatistics.updateLatitudeExtremities(location.getLatitude());
|
||||
tripStatistics.updateLongitudeExtremities(location.getLongitude());
|
||||
|
||||
// If this is the first location, remember it and return.
|
||||
if (lastLocation == null || lastMovingLocation == null) {
|
||||
lastLocation = location;
|
||||
lastMovingLocation = location;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Don't do anything more if we didn't move since the last location.
|
||||
double distance = lastLocation.distanceTo(location);
|
||||
if (distance < minRecordingDistance && location.getSpeed() < Constants.MAX_NO_MOVEMENT_SPEED) {
|
||||
lastLocation = location;
|
||||
return false;
|
||||
}
|
||||
|
||||
long timeDifference = location.getTime() - lastLocation.getTime();
|
||||
if (timeDifference < 0) {
|
||||
Log.e(TAG, "Negative time difference: " + timeDifference);
|
||||
lastLocation = location;
|
||||
return false;
|
||||
}
|
||||
|
||||
tripStatistics.addTotalDistance(lastMovingLocation.distanceTo(location));
|
||||
tripStatistics.addMovingTime(timeDifference);
|
||||
updateSpeed(
|
||||
location.getTime(), location.getSpeed(), lastLocation.getTime(), lastLocation.getSpeed());
|
||||
updateGrade(distance, elevationDifference);
|
||||
lastLocation = location;
|
||||
lastMovingLocation = location;
|
||||
return true;
|
||||
}
|
||||
|
||||
public void setMinRecordingDistance(int minRecordingDistance) {
|
||||
this.minRecordingDistance = minRecordingDistance;
|
||||
/**
|
||||
* Updates a speed reading. Assumes the user is moving.
|
||||
*
|
||||
* @param time the time
|
||||
* @param speed the speed
|
||||
* @param lastLocationTime the last location time
|
||||
* @param lastLocationSpeed the last location speed
|
||||
*/
|
||||
@VisibleForTesting
|
||||
void updateSpeed(long time, double speed, long lastLocationTime, double lastLocationSpeed) {
|
||||
if (!isValidSpeed(time, speed, lastLocationTime, lastLocationSpeed, speedBuffer)) {
|
||||
Log.d(TAG, "Invalid speed. speed: " + speed + " lastLocationSpeed: " + lastLocationSpeed);
|
||||
return;
|
||||
}
|
||||
speedBuffer.setNext(speed);
|
||||
if (speed > tripStatistics.getMaxSpeed()) {
|
||||
tripStatistics.setMaxSpeed(speed);
|
||||
}
|
||||
double movingSpeed = tripStatistics.getAverageMovingSpeed();
|
||||
if (speedBuffer.isFull() && movingSpeed > tripStatistics.getMaxSpeed()) {
|
||||
tripStatistics.setMaxSpeed(movingSpeed);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates an elevation reading.
|
||||
*
|
||||
* @param elevation the elevation
|
||||
*/
|
||||
@VisibleForTesting
|
||||
double updateElevation(double elevation) {
|
||||
double oldAverage = elevationBuffer.getAverage();
|
||||
elevationBuffer.setNext(elevation);
|
||||
double newAverage = elevationBuffer.getAverage();
|
||||
tripStatistics.updateElevationExtremities(newAverage);
|
||||
double elevationDifference = elevationBuffer.isFull() ? newAverage - oldAverage : 0.0;
|
||||
if (elevationDifference > 0) {
|
||||
tripStatistics.addTotalElevationGain(elevationDifference);
|
||||
}
|
||||
return elevationDifference;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a grade reading.
|
||||
*
|
||||
* @param distance the distance the user just traveled
|
||||
* @param elevationDifference the elevation difference between the current
|
||||
* reading and the previous reading
|
||||
*/
|
||||
@VisibleForTesting
|
||||
void updateGrade(double distance, double elevationDifference) {
|
||||
distanceBuffer.setNext(distance);
|
||||
double smoothedDistance = distanceBuffer.getAverage();
|
||||
|
||||
/*
|
||||
* With the error in the altitude measurement it is dangerous to divide by
|
||||
* anything less than 5.
|
||||
*/
|
||||
if (!elevationBuffer.isFull() || !distanceBuffer.isFull() || smoothedDistance < 5.0) {
|
||||
return;
|
||||
}
|
||||
gradeBuffer.setNext(elevationDifference / smoothedDistance);
|
||||
tripStatistics.updateGradeExtremities(gradeBuffer.getAverage());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the speed is valid.
|
||||
*
|
||||
* @param time the time
|
||||
* @param speed the speed
|
||||
* @param lastLocationTime the last location time
|
||||
* @param lastLocationSpeed the last location speed
|
||||
* @param speedBuffer a buffer of speed readings
|
||||
*/
|
||||
public static boolean isValidSpeed(long time, double speed, long lastLocationTime,
|
||||
double lastLocationSpeed, DoubleBuffer speedBuffer) {
|
||||
|
||||
/*
|
||||
* There are a lot of noisy speed readings. Do the cheapest checks first,
|
||||
* most expensive last.
|
||||
*/
|
||||
if (speed == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/*
|
||||
* The following code will ignore unlikely readings. 128 m/s seems to be an
|
||||
* internal android error code.
|
||||
*/
|
||||
if (Math.abs(speed - 128) < 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/*
|
||||
* See if the speed seems physically likely. Ignore any speeds that imply
|
||||
* acceleration greater than 2g.
|
||||
*/
|
||||
long timeDifference = time - lastLocationTime;
|
||||
double speedDifference = Math.abs(lastLocationSpeed - speed);
|
||||
if (speedDifference > Constants.MAX_ACCELERATION * timeDifference) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/*
|
||||
* Only check if the speed buffer is full. Check that the speed is less than
|
||||
* 10X the smoothed average and the speed difference doesn't imply 2g
|
||||
* acceleration.
|
||||
*/
|
||||
if (!speedBuffer.isFull()) {
|
||||
return true;
|
||||
}
|
||||
double average = speedBuffer.getAverage();
|
||||
double diff = Math.abs(average - speed);
|
||||
return (speed < average * 10) && (diff < Constants.MAX_ACCELERATION * timeDifference);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,8 +19,8 @@ import com.google.android.apps.mytracks.Constants;
|
||||
import com.google.android.apps.mytracks.ContextualActionModeCallback;
|
||||
import com.google.android.apps.mytracks.io.backup.BackupPreferencesListener;
|
||||
import com.google.android.apps.mytracks.services.sensors.BluetoothConnectionManager;
|
||||
import com.google.android.apps.mytracks.services.tasks.AnnouncementPeriodicTask;
|
||||
import com.google.android.apps.mytracks.services.tasks.PeriodicTask;
|
||||
import com.google.android.apps.mytracks.services.tasks.StatusAnnouncerTask;
|
||||
import com.google.api.client.http.HttpTransport;
|
||||
import com.google.api.client.http.apache.ApacheHttpTransport;
|
||||
|
||||
@@ -50,8 +50,8 @@ import java.util.List;
|
||||
public class Api7Adapter implements ApiAdapter {
|
||||
|
||||
@Override
|
||||
public PeriodicTask getStatusAnnouncerTask(Context context) {
|
||||
return new StatusAnnouncerTask(context);
|
||||
public PeriodicTask getAnnouncementPeriodicTask(Context context) {
|
||||
return new AnnouncementPeriodicTask(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -15,9 +15,9 @@
|
||||
*/
|
||||
package com.google.android.apps.mytracks.util;
|
||||
|
||||
import com.google.android.apps.mytracks.io.backup.BackupPreferencesListener;
|
||||
import com.google.android.apps.mytracks.io.backup.Api8BackupPreferencesListener;
|
||||
import com.google.android.apps.mytracks.services.tasks.Api8StatusAnnouncerTask;
|
||||
import com.google.android.apps.mytracks.io.backup.BackupPreferencesListener;
|
||||
import com.google.android.apps.mytracks.services.tasks.Api8AnnouncementPeriodicTask;
|
||||
import com.google.android.apps.mytracks.services.tasks.PeriodicTask;
|
||||
|
||||
import android.content.Context;
|
||||
@@ -30,8 +30,8 @@ import android.content.Context;
|
||||
public class Api8Adapter extends Api7Adapter {
|
||||
|
||||
@Override
|
||||
public PeriodicTask getStatusAnnouncerTask(Context context) {
|
||||
return new Api8StatusAnnouncerTask(context);
|
||||
public PeriodicTask getAnnouncementPeriodicTask(Context context) {
|
||||
return new Api8AnnouncementPeriodicTask(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -42,13 +42,13 @@ import java.util.List;
|
||||
public interface ApiAdapter {
|
||||
|
||||
/**
|
||||
* Gets a status announcer task.
|
||||
* Gets an announcement periodic task.
|
||||
* <p>
|
||||
* Due to changes in API level 8.
|
||||
*
|
||||
* @param context the context
|
||||
*/
|
||||
public PeriodicTask getStatusAnnouncerTask(Context context);
|
||||
public PeriodicTask getAnnouncementPeriodicTask(Context context);
|
||||
|
||||
/**
|
||||
* Gets a {@link BackupPreferencesListener}.
|
||||
|
||||
Reference in New Issue
Block a user