mirror of
https://codeberg.org/OpenTracksApp/OpenTracks.git
synced 2026-09-30 16:52:21 +02:00
Importing all the source code :)
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* 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.mymaps;
|
||||
|
||||
import com.google.android.maps.GeoPoint;
|
||||
|
||||
/**
|
||||
* A rectangle in geographical space.
|
||||
*/
|
||||
public class GeoRect {
|
||||
public int top;
|
||||
public int left;
|
||||
public int bottom;
|
||||
public int right;
|
||||
|
||||
public GeoRect() {
|
||||
top = 0;
|
||||
left = 0;
|
||||
bottom = 0;
|
||||
right = 0;
|
||||
}
|
||||
|
||||
public GeoRect(GeoPoint center, int latSpan, int longSpan) {
|
||||
top = center.getLatitudeE6() - latSpan / 2;
|
||||
left = center.getLongitudeE6() - longSpan / 2;
|
||||
bottom = center.getLatitudeE6() + latSpan / 2;
|
||||
right = center.getLongitudeE6() + longSpan / 2;
|
||||
}
|
||||
|
||||
public GeoPoint getCenter() {
|
||||
return new GeoPoint(top / 2 + bottom / 2, left / 2 + right / 2);
|
||||
}
|
||||
|
||||
public int getLatSpan() {
|
||||
return bottom - top;
|
||||
}
|
||||
|
||||
public int getLongSpan() {
|
||||
return right - left;
|
||||
}
|
||||
|
||||
public boolean contains(GeoPoint geoPoint) {
|
||||
if (geoPoint.getLatitudeE6() >= top
|
||||
&& geoPoint.getLatitudeE6() <= bottom
|
||||
&& geoPoint.getLongitudeE6() >= left
|
||||
&& geoPoint.getLongitudeE6() <= right) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* 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.mymaps;
|
||||
|
||||
import com.google.wireless.gdata.maps.MapsClient;
|
||||
|
||||
/**
|
||||
* Constants for My Maps.
|
||||
*/
|
||||
public class MyMapsConstants {
|
||||
public static final String MAPSHOP_BASE_URL =
|
||||
"http://maps.google.com/maps/ms";
|
||||
public static final String MAPSHOP_SERVICE = MapsClient.SERVICE;
|
||||
|
||||
private MyMapsConstants() { }
|
||||
}
|
||||
@@ -0,0 +1,247 @@
|
||||
/*
|
||||
* 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.mymaps;
|
||||
|
||||
import com.google.android.maps.GeoPoint;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Vector;
|
||||
|
||||
/**
|
||||
* MyMapsFeature contains all of the data associated with a feature in My Maps,
|
||||
* where a feature is a marker, line, or shape. Some of the data is stored in a
|
||||
* {@link MyMapsFeatureMetadata} object so that it can be more efficiently
|
||||
* transmitted to other activities.
|
||||
*/
|
||||
public class MyMapsFeature implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 8439035544430497236L;
|
||||
|
||||
/** A marker feature displays an icon at a single point on the map. */
|
||||
public static final int MARKER = 0;
|
||||
|
||||
/**
|
||||
* A line feature displays a line connecting a set of points on the map.
|
||||
*/
|
||||
public static final int LINE = 1;
|
||||
|
||||
/**
|
||||
* A shape feature displays a border defined by connecting a set of points,
|
||||
* including connecting the last to the first, and displays the area
|
||||
* confined by this border.
|
||||
*/
|
||||
public static final int SHAPE = 2;
|
||||
|
||||
/** The local feature id for this feature, if needed. */
|
||||
private String androidId;
|
||||
|
||||
/**
|
||||
* The latitudes of the points of this feature in order, specified in
|
||||
* millionths of a degree north.
|
||||
*/
|
||||
private final Vector<Integer> latitudeE6 = new Vector<Integer>();
|
||||
|
||||
/**
|
||||
* The longitudes of the points of this feature in order, specified in
|
||||
* millionths of a degree east.
|
||||
*/
|
||||
private final Vector<Integer> longitudeE6 = new Vector<Integer>();
|
||||
|
||||
/** The metadata of this feature in a format efficient for transmission. */
|
||||
private MyMapsFeatureMetadata featureInfo = new MyMapsFeatureMetadata();
|
||||
|
||||
/**
|
||||
* Initializes a valid but empty feature. It will default to a
|
||||
* {@link #MARKER} with a blue placemark with a dot as an icon at the
|
||||
* location (0, 0).
|
||||
*/
|
||||
public MyMapsFeature() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new point to the end of this feature.
|
||||
*
|
||||
* @param point The new point to add
|
||||
*/
|
||||
public synchronized void addPoint(GeoPoint point) {
|
||||
latitudeE6.add(point.getLatitudeE6());
|
||||
longitudeE6.add(point.getLongitudeE6());
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a new local id for this feature based on the current time and
|
||||
* a random number.
|
||||
*/
|
||||
public synchronized void generateAndroidId() {
|
||||
Long time = System.currentTimeMillis();
|
||||
Integer random = (int) (Math.random() * 10000);
|
||||
androidId = time.toString() + "." + random.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the current local id for this feature if one is available.
|
||||
*
|
||||
* @return The local id for this feature
|
||||
*/
|
||||
public String getAndroidId() {
|
||||
return androidId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the current (html) description of this feature. The description
|
||||
* is stored in the feature metadata.
|
||||
*
|
||||
* @return The description of this feature
|
||||
*/
|
||||
public String getDescription() {
|
||||
return featureInfo.getDescription();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the description of this feature. That description is stored in the
|
||||
* feature metadata.
|
||||
*
|
||||
* @param description The new description of this feature
|
||||
*/
|
||||
public synchronized void setDescription(String description) {
|
||||
featureInfo.setDescription(description);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the point at the given index for this feature.
|
||||
*
|
||||
* @param index The index of the point desired
|
||||
* @return A {@link GeoPoint} representing the point or null if that point
|
||||
* doesn't exist
|
||||
*/
|
||||
public GeoPoint getPoint(int index) {
|
||||
if (latitudeE6.size() <= index) {
|
||||
return null;
|
||||
}
|
||||
return new GeoPoint(latitudeE6.get(index), longitudeE6.get(index));
|
||||
}
|
||||
|
||||
/**
|
||||
* Counts the number of points in this feature and return that count.
|
||||
*
|
||||
* @return The number of points in this feature
|
||||
*/
|
||||
public int getPointCount() {
|
||||
return latitudeE6.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the title of this feature. That title is stored in the feature
|
||||
* metadata.
|
||||
*
|
||||
* @return the current title of this feature
|
||||
*/
|
||||
public String getTitle() {
|
||||
return featureInfo.getTitle();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the type of this feature. That type is stored in the feature
|
||||
* metadata.
|
||||
*
|
||||
* @return One of {@link #MARKER}, {@link #LINE}, or {@link #SHAPE}
|
||||
* identifying the type of this feature
|
||||
*/
|
||||
public int getType() {
|
||||
return featureInfo.getType();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the current color of this feature as an ARGB color integer.
|
||||
* That color is stored in the feature metadata.
|
||||
*
|
||||
* @return The ARGB color of this feature
|
||||
*/
|
||||
public int getColor() {
|
||||
return featureInfo.getColor();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the current line width of this feature. That line width is
|
||||
* stored in the feature metadata.
|
||||
*
|
||||
* @return The line width of this feature
|
||||
*/
|
||||
public int getLineWidth() {
|
||||
return featureInfo.getLineWidth();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the current fill color of this feature as an ARGB color
|
||||
* integer. That color is stored in the feature metadata.
|
||||
*
|
||||
* @return The ARGB fill color of this feature
|
||||
*/
|
||||
public int getFillColor() {
|
||||
return featureInfo.getFillColor();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the current icon url of this feature. That icon url is stored
|
||||
* in the feature metadata.
|
||||
*
|
||||
* @return The icon url for this feature
|
||||
*/
|
||||
public String getIconUrl() {
|
||||
return featureInfo.getIconUrl();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the title of this feature. That title is stored in the feature
|
||||
* metadata.
|
||||
*
|
||||
* @param title The new title of this feature
|
||||
*/
|
||||
public synchronized void setTitle(String title) {
|
||||
featureInfo.setTitle(title);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the type of this feature. That type is stored in the feature
|
||||
* metadata.
|
||||
*
|
||||
* @param type The new type of the feature. That type must be one of
|
||||
* {@link #MARKER}, {@link #LINE}, or {@link #SHAPE}
|
||||
*/
|
||||
public synchronized void setType(int type) {
|
||||
featureInfo.setType(type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the ARGB color of this feature. That color is stored in the feature
|
||||
* metadata.
|
||||
*
|
||||
* @param color The new ARGB color of this feature
|
||||
*/
|
||||
public synchronized void setColor(int color) {
|
||||
featureInfo.setColor(color);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the icon url of this feature. That icon url is stored in the feature
|
||||
* metadata.
|
||||
*
|
||||
* @param url The new icon url of the feature
|
||||
*/
|
||||
public synchronized void setIconUrl(String url) {
|
||||
featureInfo.setIconUrl(url);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
* 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.mymaps;
|
||||
|
||||
/**
|
||||
* Metadata about a maps feature.
|
||||
*/
|
||||
public class MyMapsFeatureMetadata {
|
||||
|
||||
private static final String BLUE_DOT_URL =
|
||||
"http://maps.google.com/mapfiles/ms/micons/blue-dot.png";
|
||||
private static final int DEFAULT_COLOR = 0x800000FF;
|
||||
private static final int DEFAULT_FILL_COLOR = 0xC00000FF;
|
||||
|
||||
private String title;
|
||||
private String description;
|
||||
private int type;
|
||||
private int color;
|
||||
private int lineWidth;
|
||||
private int fillColor;
|
||||
private String iconUrl;
|
||||
|
||||
public MyMapsFeatureMetadata() {
|
||||
title = "";
|
||||
description = "";
|
||||
type = MyMapsFeature.MARKER;
|
||||
color = DEFAULT_COLOR;
|
||||
lineWidth = 5;
|
||||
fillColor = DEFAULT_FILL_COLOR;
|
||||
iconUrl = BLUE_DOT_URL;
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public void setTitle(String title) {
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public void setDescription(String description) {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public int getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public void setType(int type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public int getColor() {
|
||||
return color;
|
||||
}
|
||||
|
||||
public void setColor(int color) {
|
||||
this.color = color;
|
||||
}
|
||||
|
||||
public int getLineWidth() {
|
||||
return lineWidth;
|
||||
}
|
||||
|
||||
public void setLineWidth(int width) {
|
||||
lineWidth = width;
|
||||
}
|
||||
|
||||
public int getFillColor() {
|
||||
return fillColor;
|
||||
}
|
||||
|
||||
public void setFillColor(int color) {
|
||||
fillColor = color;
|
||||
}
|
||||
|
||||
public String getIconUrl() {
|
||||
return iconUrl;
|
||||
}
|
||||
|
||||
public void setIconUrl(String url) {
|
||||
iconUrl = new String(url);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
/*
|
||||
* 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.mymaps;
|
||||
|
||||
import com.google.wireless.gdata.data.Entry;
|
||||
import com.google.wireless.gdata.data.StringUtils;
|
||||
import com.google.wireless.gdata.maps.MapFeatureEntry;
|
||||
import com.google.wireless.gdata.maps.MapsClient;
|
||||
|
||||
import android.graphics.Color;
|
||||
import android.util.Log;
|
||||
|
||||
import org.xmlpull.v1.XmlPullParserException;
|
||||
import org.xmlpull.v1.XmlPullParserFactory;
|
||||
import org.xmlpull.v1.XmlSerializer;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.StringWriter;
|
||||
|
||||
/**
|
||||
* Converter from GData objects to MyMaps objects.
|
||||
*/
|
||||
public class MyMapsGDataConverter {
|
||||
|
||||
private final XmlSerializer xmlSerializer;
|
||||
|
||||
public MyMapsGDataConverter() throws XmlPullParserException {
|
||||
xmlSerializer = XmlPullParserFactory.newInstance().newSerializer();
|
||||
}
|
||||
|
||||
public static MyMapsMapMetadata getMapMetadataForEntry(
|
||||
MapFeatureEntry entry) {
|
||||
MyMapsMapMetadata metadata = new MyMapsMapMetadata();
|
||||
if ("public".equals(entry.getPrivacy())) {
|
||||
metadata.setSearchable(true);
|
||||
} else {
|
||||
metadata.setSearchable(false);
|
||||
}
|
||||
metadata.setTitle(entry.getTitle());
|
||||
|
||||
String desc = entry.getContent();
|
||||
if (desc.length() > 12) {
|
||||
metadata.setDescription(desc.substring(9, desc.length() - 3));
|
||||
}
|
||||
metadata.setGDataEditUri(entry.getEditUri());
|
||||
|
||||
return metadata;
|
||||
}
|
||||
|
||||
public static String getMapidForEntry(Entry entry) {
|
||||
return MapsClient.getMapIdFromMapEntryId(entry.getId());
|
||||
}
|
||||
|
||||
public static Entry getMapEntryForMetadata(MyMapsMapMetadata metadata) {
|
||||
MapFeatureEntry entry = new MapFeatureEntry();
|
||||
entry.setEditUri(metadata.getGDataEditUri());
|
||||
entry.setTitle(metadata.getTitle());
|
||||
entry.setContent(metadata.getDescription());
|
||||
entry.setPrivacy(metadata.getSearchable() ? "public" : "unlisted");
|
||||
entry.setAuthor("android");
|
||||
entry.setEmail("nobody@google.com");
|
||||
return entry;
|
||||
}
|
||||
|
||||
public MapFeatureEntry getEntryForFeature(MyMapsFeature feature) {
|
||||
MapFeatureEntry entry = new MapFeatureEntry();
|
||||
entry.setTitle(feature.getTitle());
|
||||
entry.setAuthor("android");
|
||||
entry.setEmail("nobody@google.com");
|
||||
entry.setCategoryScheme("http://schemas.google.com/g/2005#kind");
|
||||
entry.setCategory("http://schemas.google.com/g/2008#mapfeature");
|
||||
entry.setEditUri("");
|
||||
if (!StringUtils.isEmpty(feature.getAndroidId())) {
|
||||
entry.setAttribute("_androidId", feature.getAndroidId());
|
||||
}
|
||||
try {
|
||||
StringWriter writer = new StringWriter();
|
||||
xmlSerializer.setOutput(writer);
|
||||
xmlSerializer.startTag(null, "Placemark");
|
||||
xmlSerializer.attribute(null, "xmlns", "http://earth.google.com/kml/2.2");
|
||||
xmlSerializer.startTag(null, "Style");
|
||||
if (feature.getType() == MyMapsFeature.MARKER) {
|
||||
xmlSerializer.startTag(null, "IconStyle");
|
||||
xmlSerializer.startTag(null, "Icon");
|
||||
xmlSerializer.startTag(null, "href");
|
||||
xmlSerializer.text(feature.getIconUrl());
|
||||
xmlSerializer.endTag(null, "href");
|
||||
xmlSerializer.endTag(null, "Icon");
|
||||
xmlSerializer.endTag(null, "IconStyle");
|
||||
} else {
|
||||
xmlSerializer.startTag(null, "LineStyle");
|
||||
xmlSerializer.startTag(null, "color");
|
||||
int color = feature.getColor();
|
||||
// Reverse the color because KML is ABGR and Android is ARGB
|
||||
xmlSerializer.text(Integer.toHexString(
|
||||
Color.argb(Color.alpha(color), Color.blue(color),
|
||||
Color.green(color), Color.red(color))));
|
||||
xmlSerializer.endTag(null, "color");
|
||||
xmlSerializer.startTag(null, "width");
|
||||
xmlSerializer.text(Integer.toString(feature.getLineWidth()));
|
||||
xmlSerializer.endTag(null, "width");
|
||||
xmlSerializer.endTag(null, "LineStyle");
|
||||
|
||||
if (feature.getType() == MyMapsFeature.SHAPE) {
|
||||
xmlSerializer.startTag(null, "PolyStyle");
|
||||
xmlSerializer.startTag(null, "color");
|
||||
int fcolor = feature.getFillColor();
|
||||
// Reverse the color because KML is ABGR and Android is ARGB
|
||||
xmlSerializer.text(Integer.toHexString(Color.argb(Color.alpha(fcolor),
|
||||
Color.blue(fcolor), Color.green(fcolor), Color.red(fcolor))));
|
||||
xmlSerializer.endTag(null, "color");
|
||||
xmlSerializer.startTag(null, "fill");
|
||||
xmlSerializer.text("1");
|
||||
xmlSerializer.endTag(null, "fill");
|
||||
xmlSerializer.startTag(null, "outline");
|
||||
xmlSerializer.text("1");
|
||||
xmlSerializer.endTag(null, "outline");
|
||||
xmlSerializer.endTag(null, "PolyStyle");
|
||||
}
|
||||
}
|
||||
xmlSerializer.endTag(null, "Style");
|
||||
xmlSerializer.startTag(null, "name");
|
||||
xmlSerializer.text(feature.getTitle());
|
||||
xmlSerializer.endTag(null, "name");
|
||||
xmlSerializer.startTag(null, "description");
|
||||
xmlSerializer.cdsect(feature.getDescription());
|
||||
xmlSerializer.endTag(null, "description");
|
||||
StringBuilder pointBuilder = new StringBuilder();
|
||||
for (int i = 0; i < feature.getPointCount(); ++i) {
|
||||
if (i > 0) {
|
||||
pointBuilder.append('\n');
|
||||
}
|
||||
pointBuilder.append(feature.getPoint(i).getLongitudeE6() / 1e6);
|
||||
pointBuilder.append(',');
|
||||
pointBuilder.append(feature.getPoint(i).getLatitudeE6() / 1e6);
|
||||
pointBuilder.append(",0.000000");
|
||||
}
|
||||
String pointString = pointBuilder.toString();
|
||||
if (feature.getType() == MyMapsFeature.MARKER) {
|
||||
xmlSerializer.startTag(null, "Point");
|
||||
xmlSerializer.startTag(null, "coordinates");
|
||||
xmlSerializer.text(pointString);
|
||||
xmlSerializer.endTag(null, "coordinates");
|
||||
xmlSerializer.endTag(null, "Point");
|
||||
} else if (feature.getType() == MyMapsFeature.LINE) {
|
||||
xmlSerializer.startTag(null, "LineString");
|
||||
xmlSerializer.startTag(null, "tessellate");
|
||||
xmlSerializer.text("1");
|
||||
xmlSerializer.endTag(null, "tessellate");
|
||||
xmlSerializer.startTag(null, "coordinates");
|
||||
xmlSerializer.text(pointString);
|
||||
xmlSerializer.endTag(null, "coordinates");
|
||||
xmlSerializer.endTag(null, "LineString");
|
||||
} else {
|
||||
xmlSerializer.startTag(null, "Polygon");
|
||||
xmlSerializer.startTag(null, "outerBoundaryIs");
|
||||
xmlSerializer.startTag(null, "LinearRing");
|
||||
xmlSerializer.startTag(null, "tessellate");
|
||||
xmlSerializer.text("1");
|
||||
xmlSerializer.endTag(null, "tessellate");
|
||||
xmlSerializer.startTag(null, "coordinates");
|
||||
xmlSerializer.text(pointString + "\n"
|
||||
+ Double.toString(feature.getPoint(0).getLongitudeE6() / 1e6)
|
||||
+ ","
|
||||
+ Double.toString(feature.getPoint(0).getLatitudeE6() / 1e6)
|
||||
+ ",0.000000");
|
||||
xmlSerializer.endTag(null, "coordinates");
|
||||
xmlSerializer.endTag(null, "LinearRing");
|
||||
xmlSerializer.endTag(null, "outerBoundaryIs");
|
||||
xmlSerializer.endTag(null, "Polygon");
|
||||
}
|
||||
xmlSerializer.endTag(null, "Placemark");
|
||||
xmlSerializer.flush();
|
||||
entry.setContent(writer.toString());
|
||||
Log.d("My Google Maps", "Generated kml:\n" + entry.getContent());
|
||||
Log.d("My Google Maps", "Edit URI: " + entry.getEditUri());
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
/*
|
||||
* 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.mymaps;
|
||||
|
||||
import com.google.android.apps.mytracks.MyTracksConstants;
|
||||
import com.google.android.apps.mytracks.io.AuthManager;
|
||||
import com.google.android.apps.mytracks.io.gdata.GDataClientFactory;
|
||||
import com.google.android.common.gdata.AndroidXmlParserFactory;
|
||||
import com.google.wireless.gdata.client.GDataClient;
|
||||
import com.google.wireless.gdata.client.HttpException;
|
||||
import com.google.wireless.gdata.maps.MapsClient;
|
||||
import com.google.wireless.gdata.maps.xml.XmlMapsGDataParserFactory;
|
||||
import com.google.wireless.gdata.parser.xml.SimplePullParser.ParseException;
|
||||
import com.google.wireless.gdata2.ConflictDetectedException;
|
||||
import com.google.wireless.gdata2.client.AuthenticationException;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* MyMapsGDataWrapper provides a wrapper around GData operations that maintains
|
||||
* the GData client, and provides a method to run gdata queries with proper
|
||||
* error handling. After a query is run, the wrapper can be queried about the
|
||||
* error that occurred.
|
||||
*/
|
||||
public class MyMapsGDataWrapper {
|
||||
/**
|
||||
* A QueryFunction is passed in when executing a query. The query function
|
||||
* of the class is called with the GData client as a parameter. The
|
||||
* function should execute whatever operations it desires on the client
|
||||
* without concern for whether the client will throw an error.
|
||||
*/
|
||||
public interface QueryFunction {
|
||||
public abstract void query(MapsClient client)
|
||||
throws AuthenticationException, IOException, ParseException,
|
||||
ConflictDetectedException, Exception;
|
||||
}
|
||||
|
||||
// The types of error that may be encountered
|
||||
/** No error occurred. */
|
||||
public static final int ERROR_NO_ERROR = 0;
|
||||
/** There was an authentication error, the auth token may be invalid. */
|
||||
public static final int ERROR_AUTH = 1;
|
||||
/** There was an internal error on the server side. */
|
||||
public static final int ERROR_INTERNAL = 2;
|
||||
/** There was an error connecting to the server. */
|
||||
public static final int ERROR_CONNECTION = 3;
|
||||
/** The item queried did not exit. */
|
||||
public static final int ERROR_NOT_FOUND = 4;
|
||||
/** There was an error parsing or serializing locally. */
|
||||
public static final int ERROR_LOCAL = 5;
|
||||
/** There was a conflict, update the entry and try again. */
|
||||
public static final int ERROR_CONFLICT = 6;
|
||||
/**
|
||||
* A query was run after cleaning up the wrapper, so the client was invalid.
|
||||
*/
|
||||
public static final int ERROR_CLEANED_UP = 7;
|
||||
/** An unknown error occurred. */
|
||||
public static final int ERROR_UNKNOWN = 100;
|
||||
|
||||
private final GDataClient androidGdataClient;
|
||||
private final MapsClient client;
|
||||
|
||||
private String errorMessage;
|
||||
private int errorType;
|
||||
private AuthManager auth;
|
||||
private boolean retryOnAuthFailure;
|
||||
private int retriesPending;
|
||||
private boolean cleanupCalled;
|
||||
|
||||
public MyMapsGDataWrapper(Context context) {
|
||||
androidGdataClient = GDataClientFactory.getGDataClient(context);
|
||||
client =
|
||||
new MapsClient(androidGdataClient, new XmlMapsGDataParserFactory(
|
||||
new AndroidXmlParserFactory()));
|
||||
errorType = ERROR_NO_ERROR;
|
||||
errorMessage = null;
|
||||
auth = null;
|
||||
retryOnAuthFailure = false;
|
||||
retriesPending = 0;
|
||||
cleanupCalled = false;
|
||||
}
|
||||
|
||||
public boolean runQuery(final QueryFunction query) {
|
||||
if (client == null) {
|
||||
errorType = ERROR_CLEANED_UP;
|
||||
errorMessage = "GData Wrapper has already been cleaned up!";
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
query.query(client);
|
||||
errorType = ERROR_NO_ERROR;
|
||||
errorMessage = null;
|
||||
return true;
|
||||
} catch (AuthenticationException e) {
|
||||
Log.e(MyTracksConstants.TAG, "Exception", e);
|
||||
errorType = ERROR_AUTH;
|
||||
errorMessage = e.getMessage();
|
||||
} catch (HttpException e) {
|
||||
Log.e(MyTracksConstants.TAG, "HttpException", e);
|
||||
errorMessage = e.getMessage();
|
||||
if (errorMessage.contains("401")) {
|
||||
errorType = ERROR_AUTH;
|
||||
} else {
|
||||
errorType = ERROR_CONNECTION;
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Log.e(MyTracksConstants.TAG, "Exception", e);
|
||||
errorMessage = e.getMessage();
|
||||
if (errorMessage.contains("503")) {
|
||||
errorType = ERROR_INTERNAL;
|
||||
} else {
|
||||
errorType = ERROR_CONNECTION;
|
||||
}
|
||||
} catch (ParseException e) {
|
||||
Log.e(MyTracksConstants.TAG, "Exception", e);
|
||||
errorType = ERROR_LOCAL;
|
||||
errorMessage = e.getMessage();
|
||||
} catch (ConflictDetectedException e) {
|
||||
Log.e(MyTracksConstants.TAG, "Exception", e);
|
||||
errorType = ERROR_CONFLICT;
|
||||
errorMessage = e.getMessage();
|
||||
} catch (Exception e) {
|
||||
Log.e(MyTracksConstants.TAG, "Exception", e);
|
||||
errorType = ERROR_UNKNOWN;
|
||||
errorMessage = e.getMessage();
|
||||
e.printStackTrace();
|
||||
}
|
||||
Log.d(MyTracksConstants.TAG, "GData error encountered: " + errorMessage);
|
||||
if (errorType == ERROR_AUTH && auth != null) {
|
||||
Runnable whenFinished = null;
|
||||
if (retryOnAuthFailure) {
|
||||
retriesPending++;
|
||||
whenFinished = new Runnable() {
|
||||
public void run() {
|
||||
retriesPending--;
|
||||
retryOnAuthFailure = false;
|
||||
runQuery(query);
|
||||
if (cleanupCalled && retriesPending == 0) {
|
||||
cleanUp();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
auth.invalidateAndRefresh(whenFinished);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public int getErrorType() {
|
||||
return errorType;
|
||||
}
|
||||
|
||||
public String getErrorMessage() {
|
||||
return errorMessage;
|
||||
}
|
||||
|
||||
// cleanUp must be called when done using this wrapper to close the client.
|
||||
// Note that the cleanup will be delayed if auth failure retries were
|
||||
// requested and there is a pending retry.
|
||||
public void cleanUp() {
|
||||
if (retriesPending == 0 && !cleanupCalled) {
|
||||
androidGdataClient.close();
|
||||
}
|
||||
cleanupCalled = true;
|
||||
}
|
||||
|
||||
public void setAuthManager(AuthManager newAuth) {
|
||||
this.auth = newAuth;
|
||||
}
|
||||
|
||||
public void setRetryOnAuthFailure(boolean retry) {
|
||||
retryOnAuthFailure = retry;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,350 @@
|
||||
/*
|
||||
* 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.mymaps;
|
||||
|
||||
import com.google.android.accounts.Account;
|
||||
import com.google.android.apps.mymaps.MyMapsGDataWrapper.QueryFunction;
|
||||
import com.google.android.apps.mytracks.AccountChooser;
|
||||
import com.google.android.apps.mytracks.MyTracks;
|
||||
import com.google.android.apps.mytracks.io.AuthManager;
|
||||
import com.google.android.apps.mytracks.io.AuthManagerFactory;
|
||||
import com.google.android.maps.mytracks.R;
|
||||
import com.google.wireless.gdata.maps.MapFeatureEntry;
|
||||
import com.google.wireless.gdata.maps.MapsClient;
|
||||
import com.google.wireless.gdata.parser.GDataParser;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.database.DataSetObserver;
|
||||
import android.graphics.Color;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.view.ContextMenu;
|
||||
import android.view.ContextMenu.ContextMenuInfo;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.View.OnCreateContextMenuListener;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.AdapterView.OnItemClickListener;
|
||||
import android.widget.ListAdapter;
|
||||
import android.widget.ListView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.Set;
|
||||
import java.util.Vector;
|
||||
|
||||
/**
|
||||
* Activity which displays the list of "my maps" maps.
|
||||
*/
|
||||
public class MyMapsList extends Activity implements ListAdapter {
|
||||
|
||||
private static final int MENU_OPEN = 0;
|
||||
private static final int MENU_SHARE = 2;
|
||||
private static final int GET_LOGIN = 1;
|
||||
|
||||
private LookupThread listLookup;
|
||||
private Vector<String[]> mapsList;
|
||||
private Vector<Boolean> publicList;
|
||||
private Set<DataSetObserver> observerSet;
|
||||
private Handler loadHandler;
|
||||
private int contextPosition;
|
||||
private AuthManager auth;
|
||||
|
||||
/**
|
||||
* Thread which looks up the maps list.
|
||||
*/
|
||||
private class LookupThread extends Thread {
|
||||
private boolean finished = false;
|
||||
private boolean success = true;
|
||||
|
||||
private GDataParser listParser;
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
finished = false;
|
||||
listParser = null;
|
||||
MyMapsGDataWrapper wrapper = new MyMapsGDataWrapper(MyMapsList.this);
|
||||
wrapper.setAuthManager(auth);
|
||||
wrapper.setRetryOnAuthFailure(true);
|
||||
|
||||
success = wrapper.runQuery(new QueryFunction() {
|
||||
public void query(MapsClient client) throws IOException, Exception {
|
||||
listParser = client.getParserForFeed(
|
||||
MapFeatureEntry.class, MapsClient.getMapsFeed(),
|
||||
auth.getAuthToken());
|
||||
listParser.init();
|
||||
while (listParser.hasMoreData()) {
|
||||
MapFeatureEntry entry =
|
||||
(MapFeatureEntry) listParser.readNextEntry(null);
|
||||
MyMapsMapMetadata metadata =
|
||||
MyMapsGDataConverter.getMapMetadataForEntry(entry);
|
||||
String mapid = MyMapsGDataConverter.getMapidForEntry(entry);
|
||||
synchronized (mapsList) {
|
||||
// Search through the maps list to see if it has the mapid and
|
||||
// remove it if so, so that we can replace it with updated info
|
||||
for (int i = 0; i < mapsList.size(); ++i) {
|
||||
if (mapid.equals(mapsList.get(i)[0])) {
|
||||
mapsList.remove(i);
|
||||
publicList.remove(i);
|
||||
--i;
|
||||
}
|
||||
}
|
||||
mapsList.add(new String[] {
|
||||
mapid, metadata.getTitle(), metadata.getDescription() });
|
||||
publicList.add(metadata.getSearchable());
|
||||
}
|
||||
}
|
||||
listParser.close();
|
||||
listParser = null;
|
||||
success = true;
|
||||
finished = true;
|
||||
}
|
||||
});
|
||||
|
||||
if (listParser != null) {
|
||||
listParser.close();
|
||||
}
|
||||
wrapper.cleanUp();
|
||||
if (success || wrapper.getErrorType() != MyMapsGDataWrapper.ERROR_AUTH) {
|
||||
finished = true;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isFinished() {
|
||||
return finished;
|
||||
}
|
||||
|
||||
public boolean succeeded() {
|
||||
return success;
|
||||
}
|
||||
}
|
||||
|
||||
private final Runnable checkLoaded = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (listLookup.isFinished()) {
|
||||
findViewById(R.id.loading).setVisibility(View.GONE);
|
||||
if (!listLookup.succeeded()) {
|
||||
findViewById(R.id.failed).setVisibility(View.VISIBLE);
|
||||
}
|
||||
TextView emptyView = (TextView) findViewById(R.id.mapslist_empty);
|
||||
ListView list = (ListView) findViewById(R.id.maplist);
|
||||
list.setEmptyView(emptyView);
|
||||
} else {
|
||||
loadHandler.postDelayed(checkLoaded, 500);
|
||||
}
|
||||
Iterator<DataSetObserver> iter = observerSet.iterator();
|
||||
while (iter.hasNext()) {
|
||||
iter.next().onChanged();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private final OnItemClickListener clickListener = new OnItemClickListener() {
|
||||
@Override
|
||||
public void onItemClick(AdapterView<?> parent, View v, int position,
|
||||
long id) {
|
||||
Intent result = new Intent();
|
||||
result.putExtra("mapid", (String) getItem(position));
|
||||
MyMapsList.this.setResult(RESULT_OK, result);
|
||||
finish();
|
||||
}
|
||||
};
|
||||
|
||||
private final OnCreateContextMenuListener contextMenuListener =
|
||||
new OnCreateContextMenuListener() {
|
||||
@Override
|
||||
public void onCreateContextMenu(ContextMenu menu, View v,
|
||||
ContextMenuInfo menuInfo) {
|
||||
AdapterView.AdapterContextMenuInfo info =
|
||||
(AdapterView.AdapterContextMenuInfo) menuInfo;
|
||||
contextPosition = info.position;
|
||||
menu.add(0, MENU_OPEN, 0, R.string.open_map);
|
||||
menu.add(0, MENU_SHARE, 0, R.string.share_map);
|
||||
}
|
||||
};
|
||||
|
||||
/** Called when the activity is first created. */
|
||||
@Override
|
||||
public void onCreate(Bundle icicle) {
|
||||
super.onCreate(icicle);
|
||||
|
||||
auth = AuthManagerFactory.getAuthManager(this, GET_LOGIN, null, true,
|
||||
MapsClient.SERVICE);
|
||||
|
||||
setContentView(R.layout.list);
|
||||
|
||||
ListView list = (ListView) findViewById(R.id.maplist);
|
||||
list.setOnItemClickListener(clickListener);
|
||||
list.setOnCreateContextMenuListener(contextMenuListener);
|
||||
|
||||
mapsList = new Vector<String[]>();
|
||||
publicList = new Vector<Boolean>();
|
||||
|
||||
listLookup = new LookupThread();
|
||||
// TODO fix this for non-froyo devices.
|
||||
if (AuthManagerFactory.useModernAuthManager()) {
|
||||
runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
MyTracks.getInstance().getAccountChooser().chooseAccount(
|
||||
MyMapsList.this,
|
||||
new AccountChooser.AccountHandler() {
|
||||
@Override
|
||||
public void handleAccountSelected(Account account) {
|
||||
if (account != null) {
|
||||
// The user did not quit and there was a valid google
|
||||
// account.
|
||||
doLogin(account);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
doLogin(null);
|
||||
}
|
||||
|
||||
observerSet = new HashSet<DataSetObserver>();
|
||||
|
||||
list.setAdapter(this);
|
||||
|
||||
loadHandler = new Handler();
|
||||
loadHandler.postDelayed(checkLoaded, 500);
|
||||
}
|
||||
|
||||
private void doLogin(final Account account) {
|
||||
auth.doLogin(new Runnable() {
|
||||
public void run() {
|
||||
listLookup.start();
|
||||
}
|
||||
}, account);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onActivityResult(int requestCode, int resultCode,
|
||||
Intent data) {
|
||||
if (requestCode == GET_LOGIN) {
|
||||
auth.authResult(resultCode, data);
|
||||
}
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onMenuItemSelected(int featureId, MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case MENU_OPEN:
|
||||
clickListener.onItemClick(null, null, contextPosition, 0);
|
||||
return true;
|
||||
case MENU_SHARE:
|
||||
Intent shareIntent = new Intent(Intent.ACTION_SEND);
|
||||
shareIntent.setType("text/plain");
|
||||
shareIntent.putExtra(Intent.EXTRA_SUBJECT,
|
||||
getText(R.string.share_map_subject));
|
||||
shareIntent.putExtra(Intent.EXTRA_TEXT, String.format(
|
||||
getText(R.string.share_map_body_format).toString(),
|
||||
mapsList.get(contextPosition)[1],
|
||||
MyMapsConstants.MAPSHOP_BASE_URL + "?msa=0&msid="
|
||||
+ mapsList.get(contextPosition)[0]));
|
||||
startActivity(Intent.createChooser(shareIntent,
|
||||
getText(R.string.share_map).toString()));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean areAllItemsEnabled() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEnabled(int position) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCount() {
|
||||
return mapsList.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getItem(int position) {
|
||||
return mapsList.get(position)[0];
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getItemId(int position) {
|
||||
return position;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemViewType(int position) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public View getView(int position, View convertView, ViewGroup parent) {
|
||||
if (convertView == null) {
|
||||
convertView =
|
||||
getLayoutInflater().inflate(R.layout.listitem, parent, false);
|
||||
}
|
||||
|
||||
String[] map = mapsList.get(position);
|
||||
((TextView) convertView.findViewById(R.id.maplistitem)).setText(map[1]);
|
||||
((TextView) convertView.findViewById(R.id.maplistdesc)).setText(map[2]);
|
||||
TextView publicUnlisted =
|
||||
(TextView) convertView.findViewById(R.id.maplistpublic);
|
||||
if (publicList.get(position)) {
|
||||
publicUnlisted.setTextColor(Color.RED);
|
||||
publicUnlisted.setText(R.string.public_map);
|
||||
} else {
|
||||
publicUnlisted.setTextColor(Color.GREEN);
|
||||
publicUnlisted.setText(R.string.unlisted_map);
|
||||
}
|
||||
|
||||
return convertView;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getViewTypeCount() {
|
||||
return 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasStableIds() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEmpty() {
|
||||
return mapsList.isEmpty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerDataSetObserver(DataSetObserver observer) {
|
||||
observerSet.add(observer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unregisterDataSetObserver(DataSetObserver observer) {
|
||||
observerSet.remove(observer);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* 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.mymaps;
|
||||
|
||||
/**
|
||||
* Metadata about a "my maps" map.
|
||||
*/
|
||||
public class MyMapsMapMetadata {
|
||||
|
||||
private String title;
|
||||
private String description;
|
||||
private String gdataEditUri;
|
||||
private boolean searchable;
|
||||
|
||||
public MyMapsMapMetadata() {
|
||||
title = "";
|
||||
description = "";
|
||||
gdataEditUri = "";
|
||||
searchable = false;
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public void setTitle(String title) {
|
||||
this.title = new String(title);
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public void setDescription(String description) {
|
||||
this.description = new String(description);
|
||||
}
|
||||
|
||||
public boolean getSearchable() {
|
||||
return searchable;
|
||||
}
|
||||
|
||||
public void setSearchable(boolean searchable) {
|
||||
this.searchable = searchable;
|
||||
}
|
||||
|
||||
public String getGDataEditUri() {
|
||||
return gdataEditUri;
|
||||
}
|
||||
|
||||
public void setGDataEditUri(String editUri) {
|
||||
this.gdataEditUri = new String(editUri);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
/*
|
||||
* 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.mymaps;
|
||||
|
||||
import com.google.android.apps.mymaps.MyMapsGDataWrapper.QueryFunction;
|
||||
import com.google.android.apps.mytracks.MyTracksConstants;
|
||||
import com.google.android.maps.mytracks.R;
|
||||
import com.google.wireless.gdata.GDataException;
|
||||
import com.google.wireless.gdata.client.HttpException;
|
||||
import com.google.wireless.gdata.data.Entry;
|
||||
import com.google.wireless.gdata.maps.MapsClient;
|
||||
import com.google.wireless.gdata.parser.GDataParser;
|
||||
|
||||
import android.app.AlertDialog;
|
||||
import android.app.Dialog;
|
||||
import android.app.TabActivity;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.DialogInterface.OnClickListener;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class VersionChecker {
|
||||
/** The User id for the account storing MyTracks versions. */
|
||||
private static final String VERSION_USER_ID = "116927647071651051525";
|
||||
private static final String VERSION_CLIENT = "AndroidMyTracks";
|
||||
|
||||
private static final String VERSION_STATUS_LATEST = "latest";
|
||||
private static final String VERSION_STATUS_OKAY = "okay";
|
||||
private static final String VERSION_STATUS_OLD = "old";
|
||||
private static final String VERSION_STATUS_BAD = "bad";
|
||||
|
||||
private final TabActivity parentActivity;
|
||||
private Dialog dialog;
|
||||
private boolean canceledAvailable;
|
||||
private boolean canceledRecommended;
|
||||
|
||||
public VersionChecker(TabActivity parent) {
|
||||
parentActivity = parent;
|
||||
canceledAvailable = false;
|
||||
canceledRecommended = false;
|
||||
|
||||
startMapsQueryThread();
|
||||
}
|
||||
|
||||
private void startMapsQueryThread() {
|
||||
new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
MyMapsGDataWrapper mapsGDataWrapper =
|
||||
new MyMapsGDataWrapper(parentActivity);
|
||||
mapsGDataWrapper.runQuery(new QueryFunction() {
|
||||
@Override
|
||||
public void query(MapsClient client) throws IOException, Exception {
|
||||
doMapsQuery(client);
|
||||
}
|
||||
});
|
||||
mapsGDataWrapper.cleanUp();
|
||||
}
|
||||
}.start();
|
||||
}
|
||||
|
||||
private void doMapsQuery(MapsClient client)
|
||||
throws GDataException, IOException, HttpException {
|
||||
final boolean newVersion; // Returned new version available
|
||||
final boolean newVersionRecommended; // Returned new version recommended
|
||||
final boolean newVersionRequired; // Returned new version required
|
||||
String currentVersion = parentActivity.getString(R.string.version);
|
||||
|
||||
GDataParser parser = client.getParserForFeed(
|
||||
Entry.class,
|
||||
MapsClient.getVersionFeed(
|
||||
VERSION_USER_ID,
|
||||
VERSION_CLIENT,
|
||||
currentVersion),
|
||||
"");
|
||||
Entry en = parser.parseStandaloneEntry();
|
||||
if (currentVersion.equals(en.getTitle())) {
|
||||
if (VERSION_STATUS_LATEST.equals(en.getContent())) {
|
||||
newVersion = false;
|
||||
newVersionRecommended = false;
|
||||
newVersionRequired = false;
|
||||
} else if (VERSION_STATUS_OKAY.equals(en.getContent())) {
|
||||
newVersion = true;
|
||||
newVersionRecommended = false;
|
||||
newVersionRequired = false;
|
||||
} else if (VERSION_STATUS_OLD.equals(en.getContent())) {
|
||||
newVersion = true;
|
||||
newVersionRecommended = true;
|
||||
newVersionRequired = false;
|
||||
} else if (VERSION_STATUS_BAD.equals(en.getContent())) {
|
||||
newVersion = true;
|
||||
newVersionRecommended = true;
|
||||
newVersionRequired = true;
|
||||
} else {
|
||||
Log.e(MyTracksConstants.TAG,
|
||||
"Got unknown version status: " + en.getContent());
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
Log.e(MyTracksConstants.TAG,
|
||||
"Current version '" + currentVersion + "' doesn't match entry: "
|
||||
+ en.getTitle());
|
||||
return;
|
||||
}
|
||||
|
||||
if (!newVersion) {
|
||||
Log.d(MyTracksConstants.TAG, "No new version available.");
|
||||
return;
|
||||
}
|
||||
|
||||
final AlertDialog.Builder dialogBuilder =
|
||||
new AlertDialog.Builder(parentActivity);
|
||||
dialogBuilder.setTitle(R.string.version_check_title);
|
||||
dialogBuilder.setPositiveButton(R.string.version_check_upgrade_button,
|
||||
new OnClickListener() {
|
||||
public void onClick(DialogInterface di, int number) {
|
||||
Intent upgradeIntent = new Intent();
|
||||
upgradeIntent.setAction(Intent.ACTION_VIEW);
|
||||
upgradeIntent.setData(Uri.parse(String.format("market://search?q=%s",
|
||||
parentActivity.getResources().getString(
|
||||
R.string.app_name).toString())));
|
||||
parentActivity.startActivity(upgradeIntent);
|
||||
parentActivity.finish();
|
||||
}
|
||||
});
|
||||
|
||||
if (newVersionRequired) {
|
||||
dialogBuilder.setCancelable(false);
|
||||
} else {
|
||||
final boolean recommended = newVersionRecommended;
|
||||
dialogBuilder.setNegativeButton(R.string.version_check_continue_button,
|
||||
new OnClickListener() {
|
||||
public void onClick(DialogInterface di, int number) {
|
||||
dialog.dismiss();
|
||||
canceledAvailable = true;
|
||||
if (recommended) {
|
||||
canceledRecommended = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
int messageResource;
|
||||
if (newVersionRequired) {
|
||||
messageResource = R.string.upgrade_required;
|
||||
} else if (newVersionRecommended) {
|
||||
messageResource = R.string.upgrade_recommended;
|
||||
if (canceledRecommended) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
messageResource = R.string.upgrade_available;
|
||||
if (canceledAvailable) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
String message = parentActivity.getResources().getString(messageResource);
|
||||
dialogBuilder.setMessage(String.format(message,
|
||||
parentActivity.getResources().getString(R.string.app_name)));
|
||||
parentActivity.getTabHost().post(new Runnable() {
|
||||
public void run() {
|
||||
dialog = dialogBuilder.create();
|
||||
dialog.show();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
import com.google.android.accounts.Account;
|
||||
import com.google.android.accounts.AccountManager;
|
||||
import com.google.android.maps.mytracks.R;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.content.DialogInterface;
|
||||
import android.util.Log;
|
||||
|
||||
/**
|
||||
* Choose which account to upload track information to.
|
||||
* @author Sandor Dornbush
|
||||
*/
|
||||
public class AccountChooser {
|
||||
|
||||
/**
|
||||
* The last selected account.
|
||||
*/
|
||||
private int selectedAccountIndex = -1;
|
||||
private Account selectedAccount = null;
|
||||
|
||||
/**
|
||||
* An interface for receiving updates once the user has selected the account.
|
||||
*/
|
||||
public interface AccountHandler {
|
||||
/**
|
||||
* Handle the account being selected.
|
||||
* @param account The selected account or null if none could be found
|
||||
*/
|
||||
public void handleAccountSelected(Account account);
|
||||
}
|
||||
|
||||
/**
|
||||
* Chooses the best account to upload to.
|
||||
* If no account is found the user will be alerted.
|
||||
* If only one account is found that will be used.
|
||||
* If multiple accounts are found the user will be allowed to choose.
|
||||
*
|
||||
* @param activity The parent activity
|
||||
* @param handler The handler to be notified when an account has been selected
|
||||
*/
|
||||
public void chooseAccount(final Activity activity,
|
||||
final AccountHandler handler) {
|
||||
final Account[] accounts = AccountManager.get(activity)
|
||||
.getAccountsByType(MyTracksConstants.ACCOUNT_TYPE);
|
||||
if (accounts.length < 1) {
|
||||
alertNoAccounts(activity, handler);
|
||||
return;
|
||||
}
|
||||
if (accounts.length == 1) {
|
||||
handler.handleAccountSelected(accounts[0]);
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO This should be read out of a preference.
|
||||
if (selectedAccount != null) {
|
||||
handler.handleAccountSelected(selectedAccount);
|
||||
return;
|
||||
}
|
||||
|
||||
// Let the user choose.
|
||||
Log.e(MyTracksConstants.TAG, "Multiple matching accounts found.");
|
||||
final AlertDialog.Builder builder = new AlertDialog.Builder(activity);
|
||||
builder.setTitle(R.string.choose_account_title);
|
||||
builder.setCancelable(false);
|
||||
builder.setPositiveButton(R.string.ok,
|
||||
new DialogInterface.OnClickListener() {
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
selectedAccount = accounts[selectedAccountIndex];
|
||||
handler.handleAccountSelected(selectedAccount);
|
||||
}
|
||||
});
|
||||
builder.setNegativeButton(R.string.cancel,
|
||||
new DialogInterface.OnClickListener() {
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
handler.handleAccountSelected(null);
|
||||
}
|
||||
});
|
||||
String[] choices = new String[accounts.length];
|
||||
for (int i = 0; i < accounts.length; i++) {
|
||||
choices[i] = accounts[i].name;
|
||||
}
|
||||
builder.setSingleChoiceItems(choices, selectedAccountIndex,
|
||||
new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
selectedAccountIndex = which;
|
||||
}
|
||||
});
|
||||
builder.show();
|
||||
}
|
||||
|
||||
/**
|
||||
* Puts up a dialog alerting the user that no suitable account was found.
|
||||
*/
|
||||
private void alertNoAccounts(final Activity activity,
|
||||
final AccountHandler handler) {
|
||||
Log.e(MyTracksConstants.TAG, "No matching accounts found.");
|
||||
final AlertDialog.Builder builder = new AlertDialog.Builder(activity);
|
||||
builder.setTitle(R.string.no_account_found_title);
|
||||
builder.setMessage(R.string.no_account_found);
|
||||
builder.setCancelable(true);
|
||||
builder.setNegativeButton(R.string.ok,
|
||||
new DialogInterface.OnClickListener() {
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
handler.handleAccountSelected(null);
|
||||
}
|
||||
});
|
||||
builder.show();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,592 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
import com.google.android.apps.mytracks.ChartView.Mode;
|
||||
import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
|
||||
import com.google.android.apps.mytracks.content.Track;
|
||||
import com.google.android.apps.mytracks.content.TrackPointsColumns;
|
||||
import com.google.android.apps.mytracks.content.Waypoint;
|
||||
import com.google.android.apps.mytracks.content.WaypointsColumns;
|
||||
import com.google.android.apps.mytracks.stats.DoubleBuffer;
|
||||
import com.google.android.apps.mytracks.stats.TripStatistics;
|
||||
import com.google.android.apps.mytracks.util.MyTracksUtils;
|
||||
import com.google.android.apps.mytracks.util.UnitConversions;
|
||||
import com.google.android.maps.mytracks.R;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.SharedPreferences;
|
||||
import android.database.ContentObserver;
|
||||
import android.database.Cursor;
|
||||
import android.location.Location;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.HandlerThread;
|
||||
import android.util.Log;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.ViewGroup.LayoutParams;
|
||||
import android.view.Window;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.ZoomControls;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
/**
|
||||
* An activity that displays a chart from the track point provider.
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
*/
|
||||
public class ChartActivity extends Activity implements
|
||||
SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
|
||||
private double profileLength = 0;
|
||||
|
||||
private boolean metricUnits = true;
|
||||
private boolean reportSpeed = true;
|
||||
|
||||
/**
|
||||
* The track id that is displayed.
|
||||
*/
|
||||
private long selectedTrackId = -1;
|
||||
|
||||
/**
|
||||
* Id of the last location that was seen when reading tracks from the
|
||||
* provider. This is used to determine which locations are new compared to the
|
||||
* last time the chart was updated.
|
||||
*/
|
||||
private long lastSeenLocationId = -1;
|
||||
|
||||
private long startTime = -1;
|
||||
|
||||
private Location lastLocation;
|
||||
|
||||
/**
|
||||
* The id of the track currently being recorded.
|
||||
*/
|
||||
private long recordingTrackId = -1;
|
||||
|
||||
private final DoubleBuffer elevationBuffer =
|
||||
new DoubleBuffer(MyTracksConstants.ELEVATION_SMOOTHING_FACTOR);
|
||||
private final DoubleBuffer speedBuffer =
|
||||
new DoubleBuffer(MyTracksConstants.SPEED_SMOOTHING_FACTOR);
|
||||
|
||||
private Mode mode = Mode.BY_DISTANCE;
|
||||
|
||||
/**
|
||||
* Utilities to deal with the database.
|
||||
*/
|
||||
private MyTracksProviderUtils providerUtils;
|
||||
|
||||
/*
|
||||
* UI elements:
|
||||
*/
|
||||
private ChartView cv;
|
||||
private MenuItem chartSettingsMenuItem;
|
||||
private LinearLayout busyPane;
|
||||
private ZoomControls zoomControls;
|
||||
|
||||
/** Handler for callbacks to the UI thread */
|
||||
private final Handler uiHandler = new Handler();
|
||||
|
||||
/**
|
||||
* A runnable that can be posted to the UI thread. It will remove the spinner
|
||||
* (if any), enable/disable zoom controls and orange pointer as appropriate
|
||||
* and redraw.
|
||||
*/
|
||||
private final Runnable updateChart = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
busyPane.setVisibility(View.GONE);
|
||||
zoomControls.setIsZoomInEnabled(cv.canZoomIn());
|
||||
zoomControls.setIsZoomOutEnabled(cv.canZoomOut());
|
||||
cv.setShowPointer(selectedTrackIsRecording());
|
||||
cv.invalidate();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* A runnable that can be posted to the UI thread. It will show the spinner.
|
||||
*/
|
||||
private final Runnable showSpinner = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
busyPane.setVisibility(View.VISIBLE);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* An observer for the tracks provider. Will listen to new track points being
|
||||
* added and update the chart if necessary.
|
||||
*/
|
||||
private ContentObserver observer;
|
||||
|
||||
/**
|
||||
* An observer for the waypoints provider. Will listen to new way points being
|
||||
* added and update the chart if necessary.
|
||||
*/
|
||||
private ContentObserver waypointObserver;
|
||||
|
||||
/**
|
||||
* A thread with a looper. Post to updateTrackHandler to execute Runnables on
|
||||
* this thread.
|
||||
*/
|
||||
private final HandlerThread updateTrackThread =
|
||||
new HandlerThread("updateTrackThread");
|
||||
|
||||
/** Handler for updateTrackThread */
|
||||
private Handler updateTrackHandler;
|
||||
|
||||
/**
|
||||
* A runnable that update the profile from the provider.
|
||||
*/
|
||||
private final Runnable updateTrackRunnable = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
Log.d(MyTracksConstants.TAG, "MyTracks: Updating chart.");
|
||||
Track track = providerUtils.getTrack(recordingTrackId);
|
||||
if (track == null) {
|
||||
Log.w(MyTracksConstants.TAG, "MyTracks: track not found");
|
||||
return;
|
||||
}
|
||||
Cursor cursor = null;
|
||||
try {
|
||||
cursor = providerUtils.getLocationsCursor(recordingTrackId,
|
||||
lastSeenLocationId + 1,
|
||||
MyTracksConstants.MAX_DISPLAYED_TRACK_POINTS - cv.getData().size(),
|
||||
true);
|
||||
if (cursor != null) {
|
||||
if (cursor.moveToLast()) {
|
||||
final int idColumnIdx =
|
||||
cursor.getColumnIndexOrThrow(TrackPointsColumns._ID);
|
||||
ArrayList<double[]> data = new ArrayList<double[]>();
|
||||
do {
|
||||
lastSeenLocationId = cursor.getLong(idColumnIdx);
|
||||
Location location = providerUtils.createLocation(cursor);
|
||||
if (location != null && MyTracksUtils.isValidLocation(location)) {
|
||||
data.add(getDataPoint(location, track));
|
||||
}
|
||||
} while (cursor.moveToPrevious());
|
||||
cv.addDataPoints(data);
|
||||
}
|
||||
}
|
||||
} catch (RuntimeException e) {
|
||||
Log.w(MyTracksConstants.TAG, "Caught an unexpected exception.", e);
|
||||
} finally {
|
||||
if (cursor != null) {
|
||||
cursor.close();
|
||||
}
|
||||
uiHandler.post(new Runnable() {
|
||||
public void run() {
|
||||
cv.invalidate();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences,
|
||||
String key) {
|
||||
if (key != null) {
|
||||
if (key.equals(MyTracksSettings.SELECTED_TRACK)) {
|
||||
selectedTrackId =
|
||||
sharedPreferences.getLong(MyTracksSettings.SELECTED_TRACK, -1);
|
||||
readProfileAsync();
|
||||
} else if (key.equals(MyTracksSettings.METRIC_UNITS)) {
|
||||
metricUnits =
|
||||
sharedPreferences.getBoolean(MyTracksSettings.METRIC_UNITS, true);
|
||||
cv.setMetricUnits(metricUnits);
|
||||
readProfileAsync();
|
||||
} else if (key.equals(MyTracksSettings.REPORT_SPEED)) {
|
||||
reportSpeed =
|
||||
sharedPreferences.getBoolean(MyTracksSettings.REPORT_SPEED, true);
|
||||
cv.setReportSpeed(reportSpeed, this);
|
||||
readProfileAsync();
|
||||
} else if (key.equals(MyTracksSettings.RECORDING_TRACK)) {
|
||||
recordingTrackId =
|
||||
sharedPreferences.getLong(MyTracksSettings.RECORDING_TRACK, -1);
|
||||
runOnUiThread(updateChart);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
Log.w(MyTracksConstants.TAG, "ChartActivity.onCreate");
|
||||
super.onCreate(savedInstanceState);
|
||||
MyTracks.getInstance().setChartActivity(this);
|
||||
providerUtils = MyTracksProviderUtils.Factory.get(this);
|
||||
|
||||
requestWindowFeature(Window.FEATURE_NO_TITLE);
|
||||
setContentView(R.layout.mytracks_elevation);
|
||||
ViewGroup layout = (ViewGroup) findViewById(R.id.elevation_chart);
|
||||
cv = new ChartView(this);
|
||||
cv.setMode(this.mode);
|
||||
LayoutParams params =
|
||||
new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT);
|
||||
layout.addView(cv, params);
|
||||
|
||||
SharedPreferences preferences =
|
||||
getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0);
|
||||
if (preferences != null) {
|
||||
selectedTrackId =
|
||||
preferences.getLong(MyTracksSettings.SELECTED_TRACK, -1);
|
||||
recordingTrackId =
|
||||
preferences.getLong(MyTracksSettings.RECORDING_TRACK, -1);
|
||||
metricUnits = preferences.getBoolean(MyTracksSettings.METRIC_UNITS, true);
|
||||
cv.setMetricUnits(metricUnits);
|
||||
reportSpeed = preferences.getBoolean(MyTracksSettings.REPORT_SPEED, true);
|
||||
cv.setReportSpeed(reportSpeed, this);
|
||||
preferences.registerOnSharedPreferenceChangeListener(this);
|
||||
}
|
||||
|
||||
busyPane = (LinearLayout) findViewById(R.id.elevation_busypane);
|
||||
zoomControls = (ZoomControls) findViewById(R.id.elevation_zoom);
|
||||
zoomControls.setOnZoomInClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
zoomIn();
|
||||
}
|
||||
});
|
||||
zoomControls.setOnZoomOutClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
zoomOut();
|
||||
}
|
||||
});
|
||||
|
||||
updateTrackThread.start();
|
||||
updateTrackHandler = new Handler(updateTrackThread.getLooper());
|
||||
|
||||
// Register observer for the track point provider:
|
||||
Handler contentHandler = new Handler();
|
||||
observer = new ContentObserver(contentHandler) {
|
||||
@Override
|
||||
public void onChange(boolean selfChange) {
|
||||
Log.d(MyTracksConstants.TAG, "ChartActivity: ContentObserver.onChange");
|
||||
// Check for any new locations and append them to the currently
|
||||
// recording track:
|
||||
if (!MyTracks.getInstance().isRecording()) {
|
||||
// No track is being recorded. We should not be here.
|
||||
return;
|
||||
}
|
||||
if (selectedTrackId != recordingTrackId) {
|
||||
// no track, or one other than the recording track is selected, don't
|
||||
// bother.
|
||||
return;
|
||||
}
|
||||
// Update can potentially be lengthy, put it in its own thread:
|
||||
updateTrackHandler.post(updateTrackRunnable);
|
||||
super.onChange(selfChange);
|
||||
}
|
||||
};
|
||||
|
||||
waypointObserver = new ContentObserver(contentHandler) {
|
||||
@Override
|
||||
public void onChange(boolean selfChange) {
|
||||
Log.d(MyTracksConstants.TAG,
|
||||
"MyTracksMap: ContentObserver.onChange waypoints");
|
||||
if (selectedTrackId < 0) {
|
||||
return;
|
||||
}
|
||||
Thread t = new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
readWaypoints();
|
||||
ChartActivity.this.runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
cv.invalidate();
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
t.start();
|
||||
super.onChange(selfChange);
|
||||
}
|
||||
};
|
||||
|
||||
readProfileAsync();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPause() {
|
||||
super.onPause();
|
||||
unregisterContentObservers();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
// Make sure any updates that might have happened are propagated to this
|
||||
// activity:
|
||||
observer.onChange(false);
|
||||
waypointObserver.onChange(false);
|
||||
registerContentObservers();
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the content observer for the map overlay.
|
||||
*/
|
||||
private void registerContentObservers() {
|
||||
getContentResolver().registerContentObserver(TrackPointsColumns.CONTENT_URI,
|
||||
false/* notifyForDescendents */, observer);
|
||||
getContentResolver().registerContentObserver(WaypointsColumns.CONTENT_URI,
|
||||
false/* notifyForDescendents */, waypointObserver);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregister the content observer for the map overlay.
|
||||
*/
|
||||
private void unregisterContentObservers() {
|
||||
getContentResolver().unregisterContentObserver(observer);
|
||||
getContentResolver().unregisterContentObserver(waypointObserver);
|
||||
}
|
||||
|
||||
private boolean selectedTrackIsRecording() {
|
||||
return selectedTrackId == recordingTrackId;
|
||||
}
|
||||
|
||||
private void zoomIn() {
|
||||
cv.zoomIn();
|
||||
zoomControls.setIsZoomInEnabled(cv.canZoomIn());
|
||||
zoomControls.setIsZoomOutEnabled(cv.canZoomOut());
|
||||
}
|
||||
|
||||
private void zoomOut() {
|
||||
cv.zoomOut();
|
||||
zoomControls.setIsZoomInEnabled(cv.canZoomIn());
|
||||
zoomControls.setIsZoomOutEnabled(cv.canZoomOut());
|
||||
}
|
||||
|
||||
public void setMode(Mode newMode) {
|
||||
if (this.mode != newMode) {
|
||||
this.mode = newMode;
|
||||
cv.setMode(this.mode);
|
||||
readProfileAsync();
|
||||
}
|
||||
}
|
||||
|
||||
public Mode getMode() {
|
||||
return mode;
|
||||
}
|
||||
|
||||
public void setSeriesEnabled(int index, boolean enabled) {
|
||||
cv.getChartValueSeries(index).setEnabled(enabled);
|
||||
runOnUiThread(updateChart);
|
||||
}
|
||||
|
||||
public boolean isSeriesEnabled(int index) {
|
||||
return cv.getChartValueSeries(index).isEnabled();
|
||||
}
|
||||
|
||||
private void readWaypoints() {
|
||||
if (selectedTrackId < 0) {
|
||||
return;
|
||||
}
|
||||
Cursor cursor = null;
|
||||
cv.clearWaypoints();
|
||||
try {
|
||||
// We will silently drop extra waypoints to make the app responsive.
|
||||
cursor =
|
||||
providerUtils.getWaypointsCursor(selectedTrackId, 0,
|
||||
MyTracksConstants.MAX_DISPLAYED_TRACK_POINTS);
|
||||
if (cursor != null) {
|
||||
if (cursor.moveToFirst()) {
|
||||
do {
|
||||
Waypoint wpt = providerUtils.createWaypoint(cursor);
|
||||
cv.addWaypoint(wpt);
|
||||
} while (cursor.moveToNext());
|
||||
}
|
||||
}
|
||||
} catch (RuntimeException e) {
|
||||
Log.w(MyTracksConstants.TAG, "Caught an unexpected exception.", e);
|
||||
} finally {
|
||||
if (cursor != null) {
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
super.onCreateOptionsMenu(menu);
|
||||
chartSettingsMenuItem =
|
||||
menu.add(0, MyTracksConstants.MENU_CHART_SETTINGS, 0,
|
||||
R.string.chart_settings);
|
||||
chartSettingsMenuItem.setIcon(R.drawable.chart_settings);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case MyTracksConstants.MENU_CHART_SETTINGS:
|
||||
MyTracks.getInstance().showDialogSafely(MyTracks.DIALOG_CHART_SETTINGS);
|
||||
return true;
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a location, creates a new data point for the chart. A data point is
|
||||
* an array double[2], where:
|
||||
* data[0] = the time or distance
|
||||
* data[1] = the elevation
|
||||
* data[2] = the speed
|
||||
*
|
||||
* @param location a location
|
||||
* @return the data point
|
||||
*/
|
||||
public double[] getDataPoint(Location location, Track track) {
|
||||
double[] result = new double[3];
|
||||
switch (mode) {
|
||||
case BY_DISTANCE:
|
||||
result[0] = profileLength;
|
||||
if (lastLocation != null) {
|
||||
double d = lastLocation.distanceTo(location);
|
||||
if (metricUnits) {
|
||||
profileLength += d / 1000.0;
|
||||
} else {
|
||||
profileLength += d * UnitConversions.KM_TO_MI / 1000.0;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case BY_TIME:
|
||||
if (startTime == -1) {
|
||||
// Base case
|
||||
startTime = location.getTime();
|
||||
}
|
||||
result[0] = (location.getTime() - startTime);
|
||||
break;
|
||||
default:
|
||||
Log.w(MyTracksConstants.TAG, "ChartActivity unknown mode: " + mode);
|
||||
}
|
||||
|
||||
elevationBuffer.setNext(metricUnits
|
||||
? location.getAltitude()
|
||||
: location.getAltitude() * UnitConversions.M_TO_FT);
|
||||
result[1] = elevationBuffer.getAverage();
|
||||
|
||||
if (lastLocation == null) {
|
||||
if (Math.abs(location.getSpeed() - 128) > 1) {
|
||||
speedBuffer.setNext(location.getSpeed());
|
||||
}
|
||||
} else if (TripStatistics.isValidSpeed(
|
||||
location.getTime(), location.getSpeed(), lastLocation.getTime(),
|
||||
lastLocation.getSpeed(), speedBuffer)
|
||||
&& (location.getSpeed() <= track.getMaxSpeed())) {
|
||||
speedBuffer.setNext(location.getSpeed());
|
||||
}
|
||||
result[2] = speedBuffer.getAverage() * 3.6;
|
||||
if (!metricUnits) {
|
||||
result[2] *= UnitConversions.KM_TO_MI;
|
||||
}
|
||||
if (!reportSpeed && (result[2] != 0)) {
|
||||
// Format as hours per unit
|
||||
result[2] = (60.0 / result[2]);
|
||||
}
|
||||
lastLocation = location;
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the chart data points reading from the provider. This is non-blocking.
|
||||
*/
|
||||
private void readProfileAsync() {
|
||||
cv.reset();
|
||||
updateTrackHandler.post(new Runnable() {
|
||||
public void run() {
|
||||
runOnUiThread(showSpinner);
|
||||
readProfile();
|
||||
readWaypoints();
|
||||
runOnUiThread(updateChart);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the track profile from the provider. This is a blocking function and
|
||||
* should not be run from the UI thread.
|
||||
*/
|
||||
private void readProfile() {
|
||||
profileLength = 0;
|
||||
lastLocation = null;
|
||||
startTime = -1;
|
||||
Cursor cursor = null;
|
||||
if (selectedTrackId < 0) {
|
||||
return;
|
||||
}
|
||||
Track track = providerUtils.getTrack(selectedTrackId);
|
||||
if (track == null) {
|
||||
return;
|
||||
}
|
||||
long lastLocationRead = track.getStartId();
|
||||
long totalLocations = track.getStopId() - track.getStartId();
|
||||
|
||||
// Limit the number of chart readings. Ideally we would want around 1024.
|
||||
int chartSamplingFrequency = Math.max(1, (int) (totalLocations / 1024.0));
|
||||
int bufferSize = 1024;
|
||||
try {
|
||||
final ArrayList<double[]> theData = new ArrayList<double[]>();
|
||||
int points = 0;
|
||||
while (lastLocationRead < track.getStopId()) {
|
||||
cursor = providerUtils.getLocationsCursor(
|
||||
selectedTrackId, lastLocationRead, bufferSize, false);
|
||||
if (cursor != null) {
|
||||
elevationBuffer.reset();
|
||||
speedBuffer.reset();
|
||||
if (cursor.moveToFirst()) {
|
||||
final int idColumnIdx =
|
||||
cursor.getColumnIndexOrThrow(TrackPointsColumns._ID);
|
||||
while (cursor.moveToNext()) {
|
||||
points++;
|
||||
Location location = providerUtils.createLocation(cursor);
|
||||
if (MyTracksUtils.isValidLocation(location)) {
|
||||
lastLocationRead = lastSeenLocationId =
|
||||
cursor.getLong(idColumnIdx);
|
||||
double[] point = getDataPoint(location, track);
|
||||
if (points % chartSamplingFrequency == 0) {
|
||||
theData.add(point);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
lastLocationRead += bufferSize;
|
||||
}
|
||||
} else {
|
||||
lastLocationRead += bufferSize;
|
||||
}
|
||||
cursor.close();
|
||||
cursor = null;
|
||||
}
|
||||
runOnUiThread(new Runnable() {
|
||||
public void run() {
|
||||
cv.setDataPoints(theData);
|
||||
}
|
||||
});
|
||||
} catch (RuntimeException e) {
|
||||
Log.w(MyTracksConstants.TAG, "Caught unexpected exception.", e);
|
||||
} finally {
|
||||
if (cursor != null) {
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
import com.google.android.apps.mytracks.ChartView.Mode;
|
||||
import com.google.android.maps.mytracks.R;
|
||||
|
||||
import android.app.Dialog;
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import android.view.Window;
|
||||
import android.widget.Button;
|
||||
import android.widget.CheckBox;
|
||||
import android.widget.RadioButton;
|
||||
import android.widget.RadioGroup;
|
||||
|
||||
/**
|
||||
* An activity that allows the user to set the chart settings.
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
*/
|
||||
public class ChartSettingsDialog extends Dialog {
|
||||
private RadioButton distance;
|
||||
private CheckBox elevation;
|
||||
private CheckBox speed;
|
||||
|
||||
public ChartSettingsDialog(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
requestWindowFeature(Window.FEATURE_NO_TITLE);
|
||||
setContentView(R.layout.chart_settings);
|
||||
Button cancel = (Button) findViewById(R.id.chart_settings_cancel);
|
||||
cancel.setOnClickListener(new View.OnClickListener() {
|
||||
public void onClick(View v) {
|
||||
dismiss();
|
||||
}
|
||||
});
|
||||
Button ok = (Button) findViewById(R.id.chart_settings_ok);
|
||||
ok.setOnClickListener(new View.OnClickListener() {
|
||||
public void onClick(View v) {
|
||||
handleOk();
|
||||
}
|
||||
});
|
||||
|
||||
distance = (RadioButton) findViewById(R.id.chart_settings_by_distance);
|
||||
elevation = (CheckBox) findViewById(R.id.chart_settings_elevation);
|
||||
speed = (CheckBox) findViewById(R.id.chart_settings_speed);
|
||||
}
|
||||
|
||||
public void setup(ChartActivity chart) {
|
||||
RadioGroup rd = (RadioGroup) findViewById(R.id.chart_settings_x);
|
||||
rd.check(chart.getMode() == Mode.BY_DISTANCE
|
||||
? R.id.chart_settings_by_distance
|
||||
: R.id.chart_settings_by_time);
|
||||
elevation.setChecked(chart.isSeriesEnabled(ChartView.ELEVATION_SERIES));
|
||||
speed.setChecked(chart.isSeriesEnabled(ChartView.SPEED_SERIES));
|
||||
}
|
||||
|
||||
private void handleOk() {
|
||||
ChartActivity chart = MyTracks.getInstance().getChartActivity();
|
||||
chart.setMode(distance.isChecked() ? Mode.BY_DISTANCE : Mode.BY_TIME);
|
||||
|
||||
// TODO: check that something is visible.
|
||||
chart.setSeriesEnabled(ChartView.ELEVATION_SERIES, elevation.isChecked());
|
||||
chart.setSeriesEnabled(ChartView.SPEED_SERIES, speed.isChecked());
|
||||
|
||||
dismiss();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
import com.google.android.apps.mytracks.stats.ExtremityMonitor;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Path;
|
||||
|
||||
import java.text.DecimalFormat;
|
||||
|
||||
/**
|
||||
* This class encasulates meta data about one series of values for a chart.
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
*/
|
||||
public class ChartValueSeries {
|
||||
|
||||
private final ExtremityMonitor monitor = new ExtremityMonitor();
|
||||
private final DecimalFormat format;
|
||||
private final Path path = new Path();
|
||||
private final Paint fillPaint;
|
||||
private final Paint strokePaint;
|
||||
private final int rounding;
|
||||
|
||||
private String title;
|
||||
private double min = 0;
|
||||
private double max = 1;
|
||||
private int effectiveMax = 0;
|
||||
private int effectiveMin = 0;
|
||||
private double spread = 0;
|
||||
private boolean enabled = true;
|
||||
|
||||
/**
|
||||
* Constructs a new chart value series.
|
||||
*
|
||||
* @param context The context for the chart
|
||||
* @param formatString The format of the decimal format for this series
|
||||
* @param fill The paint for filling the chart
|
||||
* @param stroke The paint for stroking the outside the chart, optional
|
||||
* @param rounding The factor to round the values by
|
||||
*/
|
||||
public ChartValueSeries(Context context,
|
||||
String formatString,
|
||||
Paint fill,
|
||||
Paint stroke,
|
||||
int rounding,
|
||||
int titleId) {
|
||||
this.format = new DecimalFormat(formatString);
|
||||
this.fillPaint = fill;
|
||||
this.strokePaint = stroke;
|
||||
this.rounding = rounding;
|
||||
this.title = context.getString(titleId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws the path of the chart
|
||||
*/
|
||||
public void drawPath(Canvas c) {
|
||||
c.drawPath(path, fillPaint);
|
||||
if (strokePaint != null) {
|
||||
c.drawPath(path, strokePaint);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets this series
|
||||
*/
|
||||
public void reset() {
|
||||
monitor.reset();
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates this series with a new value
|
||||
*/
|
||||
public void update(double d) {
|
||||
monitor.update(d);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The interval between markers
|
||||
*/
|
||||
public int getInterval() {
|
||||
// Try to find 5 even looking intervals.
|
||||
int interval = (int) (spread / 5);
|
||||
int minInterval = rounding / 4;
|
||||
if (interval < minInterval) {
|
||||
// We won't be able to find 5 even intervals.
|
||||
return minInterval;
|
||||
} else if (interval < rounding) {
|
||||
// The desired interval is less than the rounding value.
|
||||
// We will have less than 5 intervals.
|
||||
return rounding;
|
||||
} else {
|
||||
// Round the interval.
|
||||
return (interval / rounding) * rounding;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines what the min and max of the chart will be.
|
||||
* This will round down and up the min and max respectively.
|
||||
*/
|
||||
public void updateDimension() {
|
||||
if (monitor.getMax() == Double.NEGATIVE_INFINITY) {
|
||||
min = 0;
|
||||
max = 1;
|
||||
} else {
|
||||
min = monitor.getMin();
|
||||
max = monitor.getMax();
|
||||
}
|
||||
// Round it up.
|
||||
effectiveMax = ((int) (max / rounding)) * rounding + rounding;
|
||||
// Round it down.
|
||||
effectiveMin = ((int) (min / rounding)) * rounding;
|
||||
if (min < 0) {
|
||||
effectiveMin -= rounding;
|
||||
}
|
||||
spread = effectiveMax - effectiveMin;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The length of the longest string from the series
|
||||
*/
|
||||
public int getMaxLabelLength() {
|
||||
String minS = format.format(effectiveMin);
|
||||
String maxS = format.format(effectiveMax);
|
||||
return Math.max(minS.length(), maxS.length());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The rounded down minimum value
|
||||
*/
|
||||
public int getMin() {
|
||||
return effectiveMin;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The rounded up maximum value
|
||||
*/
|
||||
public int getMax() {
|
||||
return effectiveMax;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The difference between the min and max values in the series
|
||||
*/
|
||||
public double getSpread() {
|
||||
return spread;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The format for the decimal format for this series
|
||||
*/
|
||||
DecimalFormat getFormat() {
|
||||
return format;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The path for this series
|
||||
*/
|
||||
Path getPath() {
|
||||
return path;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The paint for this series
|
||||
*/
|
||||
Paint getPaint() {
|
||||
return (strokePaint == null)
|
||||
? fillPaint
|
||||
: strokePaint;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The title of the series
|
||||
*/
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public void setTitle(String title) {
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return is this series enabled
|
||||
*/
|
||||
public boolean isEnabled() {
|
||||
return enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the series enabled flag.
|
||||
*/
|
||||
public void setEnabled(boolean enabled) {
|
||||
this.enabled = enabled;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,798 @@
|
||||
/*
|
||||
* Copyright 2008 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
package com.google.android.apps.mytracks;
|
||||
|
||||
import com.google.android.apps.mytracks.content.Waypoint;
|
||||
import com.google.android.apps.mytracks.stats.ExtremityMonitor;
|
||||
import com.google.android.apps.mytracks.util.StringUtils;
|
||||
import com.google.android.apps.mytracks.util.UnitConversions;
|
||||
import com.google.android.maps.mytracks.R;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Paint.Style;
|
||||
import android.graphics.Path;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.VelocityTracker;
|
||||
import android.view.View;
|
||||
import android.view.ViewConfiguration;
|
||||
import android.widget.Scroller;
|
||||
|
||||
import java.text.DecimalFormat;
|
||||
import java.text.NumberFormat;
|
||||
import java.util.ArrayList;
|
||||
|
||||
/**
|
||||
* Visualization of the chart.
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
* @author Leif Hendrik Wilden
|
||||
*/
|
||||
public class ChartView extends View {
|
||||
|
||||
/*
|
||||
* Scrolling logic:
|
||||
*/
|
||||
private final Scroller scroller;
|
||||
private VelocityTracker velocityTracker = null;
|
||||
/** Position of the last motion event */
|
||||
private float lastMotionX;
|
||||
|
||||
/*
|
||||
* Zoom logic:
|
||||
*/
|
||||
private int zoomLevel = 1;
|
||||
private final int minZoomLevel = 1;
|
||||
private int maxZoomLevel = 5;
|
||||
|
||||
/*
|
||||
* Borders, margins, dimensions (in pixels):
|
||||
*/
|
||||
private int leftBorder = -1;
|
||||
private static final int TOP_BORDER = 15;
|
||||
private static final int BOTTOM_BORDER = 135;
|
||||
private static final int RIGHT_BORDER = 17;
|
||||
/** Space to leave for drawing the unit labels */
|
||||
private static final int UNIT_BORDER = 15;
|
||||
private static final int FONT_HEIGHT = 10;
|
||||
private int w = 0;
|
||||
private int h = 0;
|
||||
private int effectiveWidth = 0;
|
||||
private int effectiveHeight = 0;
|
||||
|
||||
/*
|
||||
* Ranges (in data units):
|
||||
*/
|
||||
private double maxX = 1;
|
||||
|
||||
/**
|
||||
* The various series.
|
||||
*/
|
||||
public static final int ELEVATION_SERIES = 0;
|
||||
public static final int SPEED_SERIES = 1;
|
||||
public static final int NUM_SERIES = 2;
|
||||
private ChartValueSeries[] series;
|
||||
|
||||
private final ExtremityMonitor xMonitor = new ExtremityMonitor();
|
||||
private final NumberFormat xFormat = new DecimalFormat("###,###");
|
||||
private final NumberFormat xShortFormat = new DecimalFormat("#.0");
|
||||
|
||||
/*
|
||||
* Paints etc. used when drawing the histogram:
|
||||
*/
|
||||
private final Paint borderPaint = new Paint();
|
||||
private final Paint labelPaint = new Paint();
|
||||
private final Paint gridPaint = new Paint();
|
||||
private final Paint clearPaint = new Paint();
|
||||
private final Drawable pointer;
|
||||
private final Drawable statsMarker;
|
||||
private final Drawable waypointMarker;
|
||||
private final int markerWidth, markerHeight;
|
||||
|
||||
/**
|
||||
* The chart data stored as an array of double arrays. Each one dimensional
|
||||
* array is composed of [x, y].
|
||||
*/
|
||||
private final ArrayList<double[]> data = new ArrayList<double[]>();
|
||||
|
||||
/**
|
||||
* List of way points to be displayed.
|
||||
*/
|
||||
private final ArrayList<Waypoint> waypoints = new ArrayList<Waypoint>();
|
||||
|
||||
private boolean metricUnits = true;
|
||||
private boolean showPointer = false;
|
||||
|
||||
private boolean yLabelMask[] = null;
|
||||
|
||||
/** Display chart versus distance or time */
|
||||
public enum Mode {
|
||||
BY_DISTANCE, BY_TIME
|
||||
}
|
||||
|
||||
private Mode mode = Mode.BY_DISTANCE;
|
||||
|
||||
public ChartView(Context context) {
|
||||
super(context);
|
||||
|
||||
setUpChartValueSeries(context);
|
||||
|
||||
labelPaint.setStyle(Style.STROKE);
|
||||
labelPaint.setColor(context.getResources().getColor(R.color.black));
|
||||
labelPaint.setAntiAlias(true);
|
||||
|
||||
borderPaint.setStyle(Style.STROKE);
|
||||
borderPaint.setColor(context.getResources().getColor(R.color.black));
|
||||
borderPaint.setAntiAlias(true);
|
||||
|
||||
gridPaint.setStyle(Style.STROKE);
|
||||
gridPaint.setColor(context.getResources().getColor(R.color.gray));
|
||||
gridPaint.setAntiAlias(false);
|
||||
|
||||
clearPaint.setStyle(Style.FILL);
|
||||
clearPaint.setColor(context.getResources().getColor(R.color.white));
|
||||
clearPaint.setAntiAlias(false);
|
||||
|
||||
pointer = context.getResources().getDrawable(R.drawable.arrow_180);
|
||||
pointer.setBounds(0, 0,
|
||||
pointer.getIntrinsicWidth(), pointer.getIntrinsicHeight());
|
||||
|
||||
statsMarker = getResources().getDrawable(R.drawable.ylw_pushpin);
|
||||
markerWidth = statsMarker.getIntrinsicWidth();
|
||||
markerHeight = statsMarker.getIntrinsicHeight();
|
||||
statsMarker.setBounds(0, 0, markerWidth, markerHeight);
|
||||
|
||||
waypointMarker = getResources().getDrawable(R.drawable.blue_pushpin);
|
||||
waypointMarker.setBounds(0, 0, markerWidth, markerHeight);
|
||||
|
||||
scroller = new Scroller(context);
|
||||
setFocusable(true);
|
||||
setClickable(true);
|
||||
updateDimensions();
|
||||
}
|
||||
|
||||
public void setUpChartValueSeries(Context context) {
|
||||
series = new ChartValueSeries[NUM_SERIES];
|
||||
|
||||
// Create the paints
|
||||
Paint fillPaint1 = new Paint();
|
||||
fillPaint1.setStyle(Style.FILL);
|
||||
fillPaint1.setColor(context.getResources().getColor(R.color.green));
|
||||
fillPaint1.setAntiAlias(true);
|
||||
|
||||
Paint fillPaint2 = new Paint();
|
||||
fillPaint2.setStyle(Style.FILL);
|
||||
fillPaint2.setColor(
|
||||
context.getResources().getColor(R.color.blue_transparent));
|
||||
fillPaint2.setAntiAlias(true);
|
||||
|
||||
Paint strokePaint = new Paint();
|
||||
strokePaint.setStyle(Style.STROKE);
|
||||
strokePaint.setColor(context.getResources().getColor(R.color.blue));
|
||||
strokePaint.setAntiAlias(true);
|
||||
|
||||
// Create the value series
|
||||
series[ELEVATION_SERIES] =
|
||||
new ChartValueSeries(context,
|
||||
"###,###",
|
||||
fillPaint1,
|
||||
null,
|
||||
100,
|
||||
R.string.elevation);
|
||||
|
||||
series[SPEED_SERIES] =
|
||||
new ChartValueSeries(context,
|
||||
"###,###.0",
|
||||
fillPaint2,
|
||||
strokePaint,
|
||||
5,
|
||||
R.string.speed);
|
||||
}
|
||||
|
||||
public void clearWaypoints() {
|
||||
waypoints.clear();
|
||||
}
|
||||
|
||||
public void addWaypoint(Waypoint waypoint) {
|
||||
waypoints.add(waypoint);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether the pointer icon is shown on the last data point.
|
||||
*/
|
||||
public void setShowPointer(boolean showPointer) {
|
||||
this.showPointer = showPointer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether metric units are used or not.
|
||||
*/
|
||||
public void setMetricUnits(boolean metricUnits) {
|
||||
this.metricUnits = metricUnits;
|
||||
}
|
||||
|
||||
public void setReportSpeed(boolean reportSpeed, Context c) {
|
||||
series[SPEED_SERIES].setTitle(c.getString(reportSpeed
|
||||
? R.string.speed
|
||||
: R.string.pace_label));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the data that is displayed by the chart.
|
||||
*
|
||||
* @return an array list with data points
|
||||
*/
|
||||
public ArrayList<double[]> getData() {
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the data that is to be displayed by the chart.
|
||||
*
|
||||
* @param theData an array list of data points
|
||||
*/
|
||||
public synchronized void setDataPoints(ArrayList<double[]> theData) {
|
||||
scrollTo(0, 0);
|
||||
zoomLevel = 1;
|
||||
data.clear();
|
||||
xMonitor.reset();
|
||||
for (ChartValueSeries cvs : series) {
|
||||
cvs.reset();
|
||||
}
|
||||
addDataPoints(theData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new data point to the chart.
|
||||
*
|
||||
* @param theData a data point
|
||||
*/
|
||||
public synchronized void addDataPoint(double[] theData) {
|
||||
data.add(theData);
|
||||
xMonitor.update(theData[0]);
|
||||
int min = Math.min(series.length, theData.length - 1);
|
||||
for (int i = 0; i < min; i++) {
|
||||
series[i].update(theData[i + 1]);
|
||||
}
|
||||
updateDimensions();
|
||||
setupPath();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds multiple data points to the chart.
|
||||
*
|
||||
* @param theData an array list of data points to be added
|
||||
*/
|
||||
public synchronized void addDataPoints(ArrayList<double[]> theData) {
|
||||
data.addAll(theData);
|
||||
for (int i = 0; i < theData.size(); i++) {
|
||||
double d[] = theData.get(i);
|
||||
xMonitor.update(d[0]);
|
||||
int min = Math.min(series.length, d.length - 1);
|
||||
for (int j = 0; j < min; j++) {
|
||||
series[j].update(d[j + 1]);
|
||||
}
|
||||
}
|
||||
updateDimensions();
|
||||
setupPath();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all data.
|
||||
* Call this only from the UI thread!
|
||||
*/
|
||||
public synchronized void reset() {
|
||||
data.clear();
|
||||
zoomLevel = 1;
|
||||
scrollTo(0, 0);
|
||||
updateDimensions();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if the chart can be zoomed into.
|
||||
*/
|
||||
public boolean canZoomIn() {
|
||||
return zoomLevel < maxZoomLevel;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if the chart can be zoomed out
|
||||
*/
|
||||
public boolean canZoomOut() {
|
||||
return zoomLevel > minZoomLevel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Zooms in one level (factor 2).
|
||||
*/
|
||||
public void zoomIn() {
|
||||
if (canZoomIn()) {
|
||||
zoomLevel++;
|
||||
setupPath();
|
||||
invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Zooms out one level (factor 2).
|
||||
*/
|
||||
public void zoomOut() {
|
||||
if (canZoomOut()) {
|
||||
zoomLevel--;
|
||||
scroller.abortAnimation();
|
||||
int scrollX = getScrollX();
|
||||
if (scrollX > effectiveWidth * (zoomLevel - 1)) {
|
||||
scrollX = effectiveWidth * (zoomLevel - 1);
|
||||
scrollTo(scrollX, 0);
|
||||
}
|
||||
setupPath();
|
||||
invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the current zoom level (1 equals to showing all data points)
|
||||
*/
|
||||
public int getZoomLevel() {
|
||||
return zoomLevel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initiates flinging.
|
||||
*
|
||||
* @param velocityX start velocity (pixels per second)
|
||||
*/
|
||||
public void fling(int velocityX) {
|
||||
scroller.fling(getScrollX(), 0, velocityX, 0, 0,
|
||||
effectiveWidth * (zoomLevel - 1), 0, 0);
|
||||
invalidate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Scrolls the view horizontally by the given amount.
|
||||
*
|
||||
* @param deltaX number of pixels to scroll
|
||||
*/
|
||||
public void scrollBy(int deltaX) {
|
||||
int scrollX = getScrollX() + deltaX;
|
||||
if (scrollX < 0) {
|
||||
scrollX = 0;
|
||||
}
|
||||
int available = effectiveWidth * (zoomLevel - 1);
|
||||
if (scrollX > available) {
|
||||
scrollX = available;
|
||||
}
|
||||
scrollTo(scrollX, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the scroll position of the chart. This will trigger a redraw.
|
||||
*/
|
||||
@Override
|
||||
public void scrollTo(int x, int y) {
|
||||
super.scrollTo(x, y);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the current display mode (by distance, by time)
|
||||
*/
|
||||
public Mode getMode() {
|
||||
return mode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the display mode (by distance, by time).
|
||||
*/
|
||||
public void setMode(Mode mode) {
|
||||
this.mode = mode;
|
||||
}
|
||||
|
||||
private int getWaypointX(Waypoint waypoint) {
|
||||
return (mode == Mode.BY_DISTANCE)
|
||||
? getX(metricUnits
|
||||
? waypoint.getLength() / 1000.0
|
||||
: waypoint.getLength() * UnitConversions.KM_TO_MI / 1000.0)
|
||||
: getX(waypoint.getDuration());
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by the parent to indicate that the mScrollX/Y values need to be
|
||||
* updated. Triggers a redraw during flinging.
|
||||
*/
|
||||
@Override
|
||||
public void computeScroll() {
|
||||
if (scroller.computeScrollOffset()) {
|
||||
int oldX = getScrollX();
|
||||
int x = scroller.getCurrX();
|
||||
scrollTo(x, 0);
|
||||
if (oldX != x) {
|
||||
onScrollChanged(x, 0, oldX, 0);
|
||||
postInvalidate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onTouchEvent(MotionEvent event) {
|
||||
if (velocityTracker == null) {
|
||||
velocityTracker = VelocityTracker.obtain();
|
||||
}
|
||||
velocityTracker.addMovement(event);
|
||||
final int action = event.getAction();
|
||||
final float x = event.getX();
|
||||
switch (action) {
|
||||
case MotionEvent.ACTION_DOWN:
|
||||
/*
|
||||
* If being flinged and user touches, stop the fling. isFinished will be
|
||||
* false if being flinged.
|
||||
*/
|
||||
if (!scroller.isFinished()) {
|
||||
scroller.abortAnimation();
|
||||
}
|
||||
// Remember where the motion event started
|
||||
lastMotionX = x;
|
||||
break;
|
||||
case MotionEvent.ACTION_MOVE:
|
||||
// Scroll to follow the motion event
|
||||
final int deltaX = (int) (lastMotionX - x);
|
||||
lastMotionX = x;
|
||||
if (deltaX < 0) {
|
||||
if (getScrollX() > 0) {
|
||||
scrollBy(deltaX);
|
||||
}
|
||||
} else if (deltaX > 0) {
|
||||
final int availableToScroll =
|
||||
effectiveWidth * (zoomLevel - 1) - getScrollX();
|
||||
if (availableToScroll > 0) {
|
||||
scrollBy(Math.min(availableToScroll, deltaX));
|
||||
}
|
||||
}
|
||||
break;
|
||||
case MotionEvent.ACTION_UP:
|
||||
// Check if top area with waypoint markers was touched and find the
|
||||
// touched marker if any:
|
||||
if (event.getY() < 100) {
|
||||
int dmin = Integer.MAX_VALUE;
|
||||
Waypoint nearestWaypoint = null;
|
||||
for (int i = 0; i < waypoints.size(); i++) {
|
||||
final Waypoint waypoint = waypoints.get(i);
|
||||
final int d = Math.abs(getWaypointX(waypoint) - (int) event.getX()
|
||||
- getScrollX());
|
||||
if (d < dmin) {
|
||||
dmin = d;
|
||||
nearestWaypoint = waypoint;
|
||||
}
|
||||
}
|
||||
if (nearestWaypoint != null && dmin < 100) {
|
||||
Intent intent =
|
||||
new Intent(getContext(), MyTracksWaypointDetails.class);
|
||||
intent.putExtra("waypointid", nearestWaypoint.getId());
|
||||
getContext().startActivity(intent);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
VelocityTracker myVelocityTracker = velocityTracker;
|
||||
myVelocityTracker.computeCurrentVelocity(1000);
|
||||
int initialVelocity = (int) myVelocityTracker.getXVelocity();
|
||||
if (Math.abs(initialVelocity) >
|
||||
ViewConfiguration.getMinimumFlingVelocity()) {
|
||||
fling(-initialVelocity);
|
||||
}
|
||||
if (velocityTracker != null) {
|
||||
velocityTracker.recycle();
|
||||
velocityTracker = null;
|
||||
}
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected synchronized void onDraw(Canvas c) {
|
||||
if (w != c.getWidth() || h != c.getHeight()) {
|
||||
// Dimensions have changed (for example due to orientation change):
|
||||
w = c.getWidth();
|
||||
h = c.getHeight();
|
||||
effectiveWidth = w - leftBorder - RIGHT_BORDER;
|
||||
effectiveHeight = h - TOP_BORDER - BOTTOM_BORDER;
|
||||
setupPath();
|
||||
}
|
||||
c.save();
|
||||
c.drawColor(Color.WHITE);
|
||||
if (data.size() < 1) {
|
||||
drawXAxis(c);
|
||||
drawYAxis(c);
|
||||
c.restore();
|
||||
return;
|
||||
}
|
||||
|
||||
// Draw the data series
|
||||
for (ChartValueSeries cvs : series) {
|
||||
if (cvs.isEnabled()) {
|
||||
cvs.drawPath(c);
|
||||
}
|
||||
}
|
||||
drawGrid(c);
|
||||
|
||||
// Draw the waypoints:
|
||||
for (int i = 1; i < waypoints.size(); i++) {
|
||||
final Waypoint waypoint = waypoints.get(i);
|
||||
if (waypoint.getLocation() == null) {
|
||||
continue;
|
||||
}
|
||||
c.save();
|
||||
|
||||
final float x = getWaypointX(waypoint);
|
||||
c.drawLine(x, h - BOTTOM_BORDER, x, TOP_BORDER, gridPaint);
|
||||
c.translate(x - (markerWidth / 2), markerHeight);
|
||||
if (waypoints.get(i).getType() == Waypoint.TYPE_STATISTICS) {
|
||||
statsMarker.draw(c);
|
||||
} else {
|
||||
waypointMarker.draw(c);
|
||||
}
|
||||
c.restore();
|
||||
}
|
||||
|
||||
// Draw the axis and labels:
|
||||
drawXLabels(c);
|
||||
drawXAxis(c);
|
||||
drawSeriesTitles(c);
|
||||
|
||||
yLabelMask = new boolean[effectiveHeight / FONT_HEIGHT + 1];
|
||||
c.translate(getScrollX(), 0);
|
||||
drawYAxis(c);
|
||||
for (ChartValueSeries cvs : series) {
|
||||
if (cvs.isEnabled()) {
|
||||
drawYLabels(cvs, c);
|
||||
}
|
||||
}
|
||||
c.restore();
|
||||
if (showPointer && data.size() > 0) {
|
||||
c.translate(getX(maxX) - pointer.getIntrinsicWidth() / 2,
|
||||
(getY(series[0], data.get(data.size() - 1)[1])
|
||||
- pointer.getIntrinsicHeight() / 2 - 12));
|
||||
pointer.draw(c);
|
||||
}
|
||||
}
|
||||
|
||||
private void drawSeriesTitles(Canvas c) {
|
||||
int sections = 1;
|
||||
for (ChartValueSeries cvs : series) {
|
||||
if (cvs.isEnabled()) {
|
||||
sections++;
|
||||
}
|
||||
}
|
||||
int j = 0;
|
||||
for (ChartValueSeries cvs : series) {
|
||||
if (cvs.isEnabled()) {
|
||||
int y = (int) (w * zoomLevel * ((double) (++j) / sections));
|
||||
c.drawText(cvs.getTitle(), y, TOP_BORDER, cvs.getPaint());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the path that is used to draw the histogram in onDraw(). The path
|
||||
* needs to be updated any time after the data or histogram dimensions change.
|
||||
*/
|
||||
private synchronized void setupPath() {
|
||||
for (ChartValueSeries cvs : series) {
|
||||
cvs.getPath().reset();
|
||||
}
|
||||
if (data.size() < 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
int min = Math.min(series.length, data.get(0).length - 1);
|
||||
|
||||
// All of the data points to the respective series.
|
||||
for (int i = 0; i < data.size(); i++) {
|
||||
double[] d = data.get(i);
|
||||
for (int j = 0; j < min; j++) {
|
||||
ChartValueSeries cvs = series[j];
|
||||
Path path = cvs.getPath();
|
||||
path.lineTo(getX(d[0]), getY(cvs, d[j + 1]));
|
||||
}
|
||||
}
|
||||
|
||||
// Close the path.
|
||||
int yCorner = TOP_BORDER + effectiveHeight;
|
||||
int xCorner = getX(data.get(0)[0]);
|
||||
for (int j = 0; j < min; j++) {
|
||||
ChartValueSeries cvs = series[j];
|
||||
Path path = cvs.getPath();
|
||||
// Bottom right corner
|
||||
path.lineTo(getX(data.get(data.size() - 1)[0]), yCorner);
|
||||
// Bottom left corner
|
||||
path.lineTo(xCorner, yCorner);
|
||||
// Top right corner
|
||||
path.lineTo(xCorner, getY(cvs, data.get(0)[j + 1]));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the histogram dimensions.
|
||||
*/
|
||||
private void updateDimensions() {
|
||||
maxX = xMonitor.getMax();
|
||||
if (data.size() <= 1) {
|
||||
maxX = 1;
|
||||
}
|
||||
for (ChartValueSeries cvs : series) {
|
||||
cvs.updateDimension();
|
||||
}
|
||||
int maxLength = 0;
|
||||
for (ChartValueSeries cvs : series) {
|
||||
maxLength = Math.max(maxLength,
|
||||
cvs.getMaxLabelLength());
|
||||
}
|
||||
leftBorder = 4 + 7 * maxLength;
|
||||
effectiveWidth = w - leftBorder - RIGHT_BORDER;
|
||||
effectiveHeight = h - TOP_BORDER - BOTTOM_BORDER;
|
||||
}
|
||||
|
||||
private int getX(double distance) {
|
||||
return leftBorder + (int) ((distance * effectiveWidth / maxX) * zoomLevel);
|
||||
}
|
||||
|
||||
private int getY(ChartValueSeries cvs, double y) {
|
||||
return TOP_BORDER + effectiveHeight
|
||||
- (int) ((y - cvs.getMin()) * effectiveHeight / cvs.getSpread());
|
||||
}
|
||||
|
||||
private void drawXLabels(Canvas c) {
|
||||
int numLabels = 0;
|
||||
double interval = (int) (maxX / zoomLevel / 4);
|
||||
boolean shortFormat = false;
|
||||
if (interval < 1) {
|
||||
interval = .5;
|
||||
shortFormat = true;
|
||||
} else {
|
||||
if (interval < 5) {
|
||||
interval = 2;
|
||||
} else if (interval < 10) {
|
||||
interval = 5;
|
||||
} else {
|
||||
interval = (interval / 10) * 10;
|
||||
}
|
||||
}
|
||||
drawXLabel(c, 0, shortFormat);
|
||||
numLabels++;
|
||||
for (int i = 1; i * interval < maxX; i++) {
|
||||
drawXLabel(c, i * interval, shortFormat);
|
||||
numLabels++;
|
||||
}
|
||||
if (numLabels < 2) {
|
||||
drawXLabel(c, (int) maxX, shortFormat);
|
||||
}
|
||||
}
|
||||
|
||||
private void drawYLabels(ChartValueSeries cvs, Canvas c) {
|
||||
int numLabels = 0;
|
||||
drawYLabel(cvs, c, 0);
|
||||
final int y0 = getY(cvs, 0);
|
||||
numLabels++;
|
||||
int interval = cvs.getInterval();
|
||||
for (int i = 0; i * interval < cvs.getSpread(); i++) {
|
||||
final int y = i * interval + cvs.getMin();
|
||||
final int yi = getY(cvs, y);
|
||||
if (Math.abs(y0 - yi) > 15) {
|
||||
drawYLabel(cvs, c, y);
|
||||
numLabels++;
|
||||
}
|
||||
}
|
||||
if (numLabels < 3) {
|
||||
drawYLabel(cvs, c, cvs.getMax());
|
||||
}
|
||||
}
|
||||
|
||||
private void drawXLabel(Canvas c, double x, boolean shortFormat) {
|
||||
String s =
|
||||
(mode == Mode.BY_DISTANCE)
|
||||
? (shortFormat ? xShortFormat.format(x) : xFormat.format(x))
|
||||
: StringUtils.formatTime((long) x);
|
||||
c.drawText(s,
|
||||
getX(x),
|
||||
effectiveHeight + UNIT_BORDER + TOP_BORDER,
|
||||
labelPaint);
|
||||
}
|
||||
|
||||
private void drawYLabel(ChartValueSeries cvs, Canvas c, int y) {
|
||||
int desiredY =
|
||||
(int) ((y - cvs.getMin()) * effectiveHeight / cvs.getSpread());
|
||||
|
||||
// Make sure we don't write one label on top of another.
|
||||
int slot = desiredY / FONT_HEIGHT;
|
||||
if (slot < 0) {
|
||||
return;
|
||||
}
|
||||
if (yLabelMask[slot]) {
|
||||
desiredY = ++slot * FONT_HEIGHT;
|
||||
}
|
||||
desiredY = TOP_BORDER + effectiveHeight + 5 - desiredY;
|
||||
if (slot < yLabelMask.length) {
|
||||
yLabelMask[slot] = true;
|
||||
}
|
||||
c.drawText(cvs.getFormat().format(y),
|
||||
2, desiredY,
|
||||
cvs.getPaint());
|
||||
}
|
||||
|
||||
private void drawXAxis(Canvas canvas) {
|
||||
float rightEdge = getX(maxX);
|
||||
canvas.drawLine(leftBorder, effectiveHeight + TOP_BORDER, rightEdge,
|
||||
effectiveHeight + TOP_BORDER, borderPaint);
|
||||
Context c = getContext();
|
||||
String s =
|
||||
(mode == Mode.BY_DISTANCE)
|
||||
? (metricUnits ? c.getString(R.string.kilometer)
|
||||
: c.getString(R.string.mile)) : (c.getString(R.string.min));
|
||||
canvas.drawText(s,
|
||||
rightEdge,
|
||||
effectiveHeight + .2f * UNIT_BORDER + TOP_BORDER,
|
||||
labelPaint);
|
||||
}
|
||||
|
||||
private void drawYAxis(Canvas canvas) {
|
||||
canvas.drawLine(leftBorder, UNIT_BORDER + TOP_BORDER,
|
||||
leftBorder, effectiveHeight + TOP_BORDER,
|
||||
borderPaint);
|
||||
canvas.drawRect(0, 0,
|
||||
leftBorder - 1, effectiveHeight + TOP_BORDER + 1,
|
||||
clearPaint);
|
||||
Context c = getContext();
|
||||
// TODO: This should really show units for all series.
|
||||
String s = metricUnits
|
||||
? c.getString(R.string.meter)
|
||||
: c.getString(R.string.feet);
|
||||
canvas.drawText(s, leftBorder - UNIT_BORDER * .2f,
|
||||
UNIT_BORDER * .8f + TOP_BORDER, labelPaint);
|
||||
}
|
||||
|
||||
private synchronized void drawGrid(Canvas c) {
|
||||
if (data.size() < 1) {
|
||||
return;
|
||||
}
|
||||
for (ChartValueSeries cvs : series) {
|
||||
if (cvs.isEnabled()) {
|
||||
int interval = cvs.getInterval();
|
||||
for (int i = 1; i * interval < cvs.getSpread(); i++) {
|
||||
int y = getY(cvs, i * interval + cvs.getMin());
|
||||
c.drawLine(getX(data.get(0)[0]), y,
|
||||
getX(data.get(data.size() - 1)[0]), y,
|
||||
cvs.getPaint());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets one of the chart value series.
|
||||
* The index should be one of the following values:
|
||||
* ELEVATION_SERIES or SPEED_SERIES
|
||||
*
|
||||
* @param index of the value series
|
||||
* @return The chart series at the index
|
||||
*/
|
||||
public ChartValueSeries getChartValueSeries(int index) {
|
||||
return series[index];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
import com.google.android.maps.mytracks.R;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* Check to see if the units are the default for the locale.
|
||||
* This comes down to warning Americans that the default is metric.
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
*/
|
||||
class CheckUnits {
|
||||
private static final String PREFERENCE_UNITS_CHECKED = "checkunits.checked";
|
||||
private static final String PREFERENCES_CHECK_UNITS = "checkunits";
|
||||
|
||||
static void check(final Context context) {
|
||||
final SharedPreferences preferences =
|
||||
context.getSharedPreferences(PREFERENCES_CHECK_UNITS, Activity.MODE_PRIVATE);
|
||||
|
||||
// Has the user already warned about the default units?
|
||||
if (preferences.getBoolean(PREFERENCE_UNITS_CHECKED, false)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Is the user in the US?
|
||||
Locale current = Locale.getDefault();
|
||||
Locale enUs = new Locale(Locale.ENGLISH.getLanguage(),
|
||||
Locale.US.getCountry());
|
||||
if (!current.equals(enUs)) {
|
||||
return;
|
||||
}
|
||||
|
||||
final AlertDialog.Builder builder = new AlertDialog.Builder(context);
|
||||
builder.setTitle(R.string.check_units_title);
|
||||
builder.setCancelable(true);
|
||||
builder.setPositiveButton(R.string.yes, new DialogInterface.OnClickListener() {
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
accept(context, preferences);
|
||||
}
|
||||
});
|
||||
builder.setNegativeButton(R.string.no, new DialogInterface.OnClickListener() {
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
recordCheckPerformed(preferences);
|
||||
}
|
||||
});
|
||||
builder.setOnCancelListener(new DialogInterface.OnCancelListener() {
|
||||
public void onCancel(DialogInterface dialog) {
|
||||
recordCheckPerformed(preferences);
|
||||
}
|
||||
});
|
||||
builder.setMessage(R.string.check_units_message);
|
||||
builder.show();
|
||||
}
|
||||
|
||||
private static void accept(Context context, SharedPreferences preferences) {
|
||||
recordCheckPerformed(preferences);
|
||||
Intent startIntent = new Intent(context, MyTracksSettings.class);
|
||||
context.startActivity(startIntent);
|
||||
}
|
||||
|
||||
private static void recordCheckPerformed(SharedPreferences preferences) {
|
||||
preferences.edit().putBoolean(PREFERENCE_UNITS_CHECKED, true).commit();
|
||||
}
|
||||
|
||||
private CheckUnits() {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
* Copyright 2008 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
package com.google.android.apps.mytracks;
|
||||
|
||||
import com.google.android.apps.mytracks.util.ResourceUtils;
|
||||
import com.google.android.maps.mytracks.R;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
|
||||
/**
|
||||
* Displays an EULA ("End User License Agreement") that the user has to accept
|
||||
* before using the application. Your application should call
|
||||
* {@link Eula#showEula(android.app.Activity)} in the onCreate() method of the
|
||||
* first activity. If the user accepts the EULA, it will never be shown again.
|
||||
* If the user refuses, {@link android.app.Activity#finish()} is invoked on your
|
||||
* activity.
|
||||
*/
|
||||
class Eula {
|
||||
private static final String PREFERENCE_EULA_ACCEPTED = "eula.accepted";
|
||||
private static final String PREFERENCES_EULA = "eula";
|
||||
|
||||
/**
|
||||
* Displays the EULA if necessary. This method should be called from the
|
||||
* onCreate() method of your main Activity.
|
||||
*
|
||||
* @param activity The Activity to finish if the user rejects the EULA
|
||||
*/
|
||||
static void showEula(final Activity activity) {
|
||||
final SharedPreferences preferences =
|
||||
activity.getSharedPreferences(PREFERENCES_EULA, Activity.MODE_PRIVATE);
|
||||
if (preferences.getBoolean(PREFERENCE_EULA_ACCEPTED, false)) {
|
||||
return;
|
||||
}
|
||||
|
||||
final AlertDialog.Builder builder = new AlertDialog.Builder(activity);
|
||||
builder.setTitle(R.string.eula_title);
|
||||
builder.setCancelable(true);
|
||||
builder.setPositiveButton(R.string.accept,
|
||||
new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
accept(activity, preferences);
|
||||
}
|
||||
});
|
||||
builder.setNegativeButton(R.string.decline,
|
||||
new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
refuse(activity);
|
||||
}
|
||||
});
|
||||
builder.setOnCancelListener(new DialogInterface.OnCancelListener() {
|
||||
@Override
|
||||
public void onCancel(DialogInterface dialog) {
|
||||
refuse(activity);
|
||||
}
|
||||
});
|
||||
builder.setMessage(ResourceUtils.readFile(activity, R.raw.eula));
|
||||
builder.show();
|
||||
}
|
||||
|
||||
private static void accept(Activity activity, SharedPreferences preferences) {
|
||||
preferences.edit().putBoolean(PREFERENCE_EULA_ACCEPTED, true).commit();
|
||||
Intent startIntent = new Intent(activity, WelcomeActivity.class);
|
||||
activity.startActivityForResult(startIntent, MyTracksConstants.WELCOME);
|
||||
}
|
||||
|
||||
private static void refuse(Activity activity) {
|
||||
activity.finish();
|
||||
}
|
||||
|
||||
private Eula() {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,227 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
|
||||
import com.google.android.apps.mytracks.content.TracksColumns;
|
||||
import com.google.android.apps.mytracks.io.TrackWriterFactory;
|
||||
import com.google.android.apps.mytracks.io.TrackWriterFactory.TrackFileFormat;
|
||||
import com.google.android.maps.mytracks.R;
|
||||
|
||||
import android.app.AlertDialog;
|
||||
import android.app.ProgressDialog;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.SharedPreferences;
|
||||
import android.database.Cursor;
|
||||
import android.os.Handler;
|
||||
import android.os.HandlerThread;
|
||||
import android.os.PowerManager;
|
||||
import android.os.PowerManager.WakeLock;
|
||||
import android.util.Log;
|
||||
import android.widget.Toast;
|
||||
|
||||
/**
|
||||
* A class that will export all tracks to the sd card.
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
*/
|
||||
public class ExportAllTracks {
|
||||
// These must line up with the index in the array.
|
||||
public static final int GPX_OPTION_INDEX = 0;
|
||||
public static final int KML_OPTION_INDEX = 1;
|
||||
public static final int CSV_OPTION_INDEX = 2;
|
||||
|
||||
private final Context context;
|
||||
private WakeLock wakeLock;
|
||||
private ProgressDialog progress;
|
||||
|
||||
private TrackFileFormat format = TrackFileFormat.GPX;
|
||||
|
||||
private final DialogInterface.OnClickListener itemClick =
|
||||
new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
switch (which) {
|
||||
case GPX_OPTION_INDEX:
|
||||
format = TrackFileFormat.GPX;
|
||||
break;
|
||||
case KML_OPTION_INDEX:
|
||||
format = TrackFileFormat.KML;
|
||||
break;
|
||||
case CSV_OPTION_INDEX:
|
||||
format = TrackFileFormat.CSV;
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
public ExportAllTracks(Context c) {
|
||||
this.context = c;
|
||||
Log.i(MyTracksConstants.TAG, "ExportAllTracks: Starting");
|
||||
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(c);
|
||||
builder.setSingleChoiceItems(R.array.export_formats, 0, itemClick);
|
||||
builder.setPositiveButton(R.string.ok, positiveClick);
|
||||
builder.setNegativeButton(R.string.cancel, null);
|
||||
builder.show();
|
||||
}
|
||||
|
||||
private final DialogInterface.OnClickListener positiveClick =
|
||||
new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
HandlerThread handlerThread;
|
||||
Handler handler;
|
||||
handlerThread = new HandlerThread("SendToMyMaps");
|
||||
handlerThread.start();
|
||||
handler = new Handler(handlerThread.getLooper());
|
||||
handler.post(runner);
|
||||
}
|
||||
};
|
||||
|
||||
private final Runnable runner = new Runnable() {
|
||||
public void run() {
|
||||
aquireLocksAndExport();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Makes sure that we keep the phone from sleeping.
|
||||
* See if there is a current track. Aquire a wake lock if there is no
|
||||
* current track.
|
||||
*/
|
||||
private void aquireLocksAndExport() {
|
||||
SharedPreferences prefs =
|
||||
context.getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0);
|
||||
long recordingTrackId = -1;
|
||||
if (prefs != null) {
|
||||
recordingTrackId = prefs.getLong(MyTracksSettings.RECORDING_TRACK, -1);
|
||||
}
|
||||
if (recordingTrackId != -1) {
|
||||
acquireWakeLock();
|
||||
}
|
||||
|
||||
// Now we can safely export everything.
|
||||
exportAll();
|
||||
|
||||
// Release the wake lock if we recorded one.
|
||||
// TODO check what happens if we started recording after getting this lock.
|
||||
if (wakeLock != null && wakeLock.isHeld()) {
|
||||
wakeLock.release();
|
||||
Log.i(MyTracksConstants.TAG, "ExportAllTracks: Releasing wake lock.");
|
||||
}
|
||||
Log.i(MyTracksConstants.TAG, "ExportAllTracks: Done");
|
||||
Toast.makeText(context, R.string.export_done, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
|
||||
private void makeProgressDialog(final int trackCount) {
|
||||
String exportMsg = context.getString(R.string.tracklist_btn_export_all);
|
||||
progress = new ProgressDialog(context);
|
||||
progress.setIcon(android.R.drawable.ic_dialog_info);
|
||||
progress.setTitle(exportMsg);
|
||||
progress.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
|
||||
progress.setMax(trackCount);
|
||||
progress.setProgress(0);
|
||||
progress.show();
|
||||
}
|
||||
|
||||
/**
|
||||
* Actually export the tracks.
|
||||
* This should be called after the wake locks have been aquired.
|
||||
*/
|
||||
private void exportAll() {
|
||||
// Get a cursor over all tracks.
|
||||
Cursor cursor = null;
|
||||
try {
|
||||
MyTracksProviderUtils providerUtils =
|
||||
MyTracksProviderUtils.Factory.get(context);
|
||||
cursor = providerUtils.getTracksCursor("");
|
||||
if (cursor == null) {
|
||||
return;
|
||||
}
|
||||
final int trackCount = cursor.getCount();
|
||||
Log.i(MyTracksConstants.TAG,
|
||||
"ExportAllTracks: Exporting: " + cursor.getCount() + " tracks.");
|
||||
int idxTrackId = cursor.getColumnIndexOrThrow(TracksColumns._ID);
|
||||
MyTracks.getInstance().runOnUiThread(new Runnable() {
|
||||
public void run() {
|
||||
makeProgressDialog(trackCount);
|
||||
}});
|
||||
for (int i = 0; cursor.moveToNext(); i++) {
|
||||
final int status = i;
|
||||
MyTracks.getInstance().runOnUiThread(new Runnable() {
|
||||
public void run() {
|
||||
synchronized (this) {
|
||||
if (progress == null) {
|
||||
return;
|
||||
}
|
||||
progress.setProgress(status);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
long id = cursor.getLong(idxTrackId);
|
||||
Log.i(MyTracksConstants.TAG, "ExportAllTracks: exporting: " + id);
|
||||
TrackWriterFactory.newWriter(context, providerUtils, id, format)
|
||||
.writeTrack();
|
||||
}
|
||||
} finally {
|
||||
if (cursor != null) {
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
if (progress != null) {
|
||||
progress.dismiss();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to acquire a partial wake lock if not already acquired. Logs errors
|
||||
* and gives up trying in case the wake lock cannot be acquired.
|
||||
*/
|
||||
private void acquireWakeLock() {
|
||||
Log.i(MyTracksConstants.TAG, "ExportAllTracks: Aquiring wake lock.");
|
||||
try {
|
||||
PowerManager pm =
|
||||
(PowerManager) context.getSystemService(Context.POWER_SERVICE);
|
||||
if (pm == null) {
|
||||
Log.e(MyTracksConstants.TAG,
|
||||
"ExportAllTracks: Power manager not found!");
|
||||
return;
|
||||
}
|
||||
if (wakeLock == null) {
|
||||
wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,
|
||||
MyTracksConstants.TAG);
|
||||
if (wakeLock == null) {
|
||||
Log.e(MyTracksConstants.TAG,
|
||||
"ExportAllTracks: Could not create wake lock (null).");
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (!wakeLock.isHeld()) {
|
||||
wakeLock.acquire();
|
||||
if (!wakeLock.isHeld()) {
|
||||
Log.e(MyTracksConstants.TAG,
|
||||
"ExportAllTracks: Could not acquire wake lock.");
|
||||
}
|
||||
}
|
||||
} catch (RuntimeException e) {
|
||||
Log.e(MyTracksConstants.TAG,
|
||||
"ExportAllTracks: Caught unexpected exception: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
import android.content.Context;
|
||||
import android.preference.ListPreference;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
/**
|
||||
* A list preference which persists its values as integers instead of strings.
|
||||
* Code reading the values should use
|
||||
* {@link android.content.SharedPreferences#getInt}.
|
||||
* When using XML-declared arrays for entry values, the arrays should be regular
|
||||
* string arrays containing valid integer values.
|
||||
*
|
||||
* @author Rodrigo Damazio
|
||||
*/
|
||||
public class IntegerListPreference extends ListPreference {
|
||||
|
||||
public IntegerListPreference(Context context) {
|
||||
super(context);
|
||||
|
||||
verifyEntryValues(null);
|
||||
}
|
||||
|
||||
public IntegerListPreference(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
|
||||
verifyEntryValues(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setEntryValues(CharSequence[] entryValues) {
|
||||
CharSequence[] oldValues = getEntryValues();
|
||||
super.setEntryValues(entryValues);
|
||||
verifyEntryValues(oldValues);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setEntryValues(int entryValuesResId) {
|
||||
CharSequence[] oldValues = getEntryValues();
|
||||
super.setEntryValues(entryValuesResId);
|
||||
verifyEntryValues(oldValues);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getPersistedString(String defaultReturnValue) {
|
||||
// During initial load, there's no known default value
|
||||
int defaultIntegerValue = Integer.MIN_VALUE;
|
||||
if (defaultReturnValue != null) {
|
||||
defaultIntegerValue = Integer.parseInt(defaultReturnValue);
|
||||
}
|
||||
|
||||
// When the list preference asks us to read a string, instead read an
|
||||
// integer.
|
||||
int value = getPersistedInt(defaultIntegerValue);
|
||||
return Integer.toString(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean persistString(String value) {
|
||||
// When asked to save a string, instead save an integer
|
||||
return persistInt(Integer.parseInt(value));
|
||||
}
|
||||
|
||||
private void verifyEntryValues(CharSequence[] oldValues) {
|
||||
CharSequence[] entryValues = getEntryValues();
|
||||
if (entryValues == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (CharSequence entryValue : entryValues) {
|
||||
try {
|
||||
Integer.parseInt(entryValue.toString());
|
||||
} catch (NumberFormatException nfe) {
|
||||
super.setEntryValues(oldValues);
|
||||
throw nfe;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,12 +1,12 @@
|
||||
/*
|
||||
* Copyright 2008 Google Inc.
|
||||
*
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
@@ -27,8 +27,173 @@ public abstract class MyTracksConstants {
|
||||
*/
|
||||
public static final String TAG = "MyTracks";
|
||||
|
||||
/**
|
||||
* Name of the gps location provider:
|
||||
*/
|
||||
public static final String GPS_PROVIDER = "gps";
|
||||
|
||||
/*
|
||||
* onActivityResult request codes:
|
||||
*/
|
||||
|
||||
public static final int GET_LOGIN = 0;
|
||||
public static final int GET_MAP = 1;
|
||||
public static final int CREATE_MAP = 2;
|
||||
public static final int SHOW_TRACK = 3;
|
||||
public static final int ADD_LIST = 4;
|
||||
public static final int FEATURE_DETAILS = 5;
|
||||
public static final int START_RECORDING = 6;
|
||||
public static final int STOP_RECORDING = 7;
|
||||
public static final int AUTHENTICATE_TO_DOCS = 8;
|
||||
public static final int AUTHENTICATE_TO_TRIX = 9;
|
||||
public static final int SEND_TO_DOCS = 10;
|
||||
public static final int DELETE_TRACK = 11;
|
||||
public static final int SEND_TO_GOOGLE = 12;
|
||||
public static final int SEND_TO_GOOGLE_DIALOG = 13;
|
||||
public static final int SHARE_LINK = 14;
|
||||
public static final int SHARE_GPX_FILE = 15;
|
||||
public static final int SHARE_KML_FILE = 16;
|
||||
public static final int SHARE_CSV_FILE = 17;
|
||||
public static final int EDIT_DETAILS = 18;
|
||||
public static final int SAVE_GPX_FILE = 19;
|
||||
public static final int SAVE_KML_FILE = 20;
|
||||
public static final int SAVE_CSV_FILE = 21;
|
||||
public static final int CLEAR_MAP = 22;
|
||||
public static final int SHOW_WAYPOINT = 23;
|
||||
public static final int EDIT_WAYPOINT = 24;
|
||||
public static final int WELCOME = 25;
|
||||
|
||||
/*
|
||||
* Menu ids:
|
||||
*/
|
||||
|
||||
public static final int MENU_START_RECORDING = 1;
|
||||
public static final int MENU_STOP_RECORDING = 2;
|
||||
public static final int MENU_LIST_TRACKS = 3;
|
||||
public static final int MENU_LIST_MARKERS = 4;
|
||||
public static final int MENU_SETTINGS = 5;
|
||||
public static final int MENU_MY_LOCATION = 6;
|
||||
public static final int MENU_TOGGLE_LAYERS = 7;
|
||||
public static final int MENU_CHART_SETTINGS = 8;
|
||||
public static final int MENU_HELP = 9;
|
||||
public static final int MENU_CURRENT_SEGMENT = 10;
|
||||
|
||||
/*
|
||||
* Context menu ids:
|
||||
*/
|
||||
|
||||
public static final int MENU_EDIT = 100;
|
||||
public static final int MENU_DELETE = 101;
|
||||
public static final int MENU_SEND_TO_GOOGLE = 102;
|
||||
public static final int MENU_SHARE = 103;
|
||||
public static final int MENU_SHOW = 104;
|
||||
public static final int MENU_SHARE_LINK = 200;
|
||||
public static final int MENU_SHARE_GPX_FILE = 201;
|
||||
public static final int MENU_SHARE_KML_FILE = 202;
|
||||
public static final int MENU_SHARE_CSV_FILE = 203;
|
||||
public static final int MENU_WRITE_TO_SD_CARD = 204;
|
||||
public static final int MENU_SAVE_GPX_FILE = 205;
|
||||
public static final int MENU_SAVE_KML_FILE = 206;
|
||||
public static final int MENU_SAVE_CSV_FILE = 207;
|
||||
public static final int MENU_CLEAR_MAP = 208;
|
||||
|
||||
/**
|
||||
* The number of distance readings to smooth to get a stable signal.
|
||||
*/
|
||||
public static final int DISTANCE_SMOOTHING_FACTOR = 25;
|
||||
|
||||
/**
|
||||
* The number of elevation readings to smooth to get a somewhat accurate
|
||||
* signal.
|
||||
*/
|
||||
public static final int ELEVATION_SMOOTHING_FACTOR = 25;
|
||||
|
||||
/**
|
||||
* The number of grade readings to smooth to get a somewhat accurate signal.
|
||||
*/
|
||||
public static final int GRADE_SMOOTHING_FACTOR = 5;
|
||||
|
||||
/**
|
||||
* The number of speed reading to smooth to get a somewhat accurate signal.
|
||||
*/
|
||||
public static final int SPEED_SMOOTHING_FACTOR = 25;
|
||||
|
||||
/**
|
||||
* Maximum number of track points displayed by the map overlay.
|
||||
*/
|
||||
public static final int MAX_DISPLAYED_TRACK_POINTS = 10000;
|
||||
|
||||
/**
|
||||
* Maximum number of track points ever loaded at once from the provider into
|
||||
* memory.
|
||||
* With a recording frequency of 2 seconds, 15000 corresponds to 8.3 hours.
|
||||
*/
|
||||
public static final int MAX_LOADED_TRACK_POINTS = 20000;
|
||||
|
||||
/**
|
||||
* Maximum number of way points displayed by the map overlay.
|
||||
*/
|
||||
public static final int MAX_DISPLAYED_WAYPOINTS_POINTS = 128;
|
||||
|
||||
/**
|
||||
* Maximum number of way points that will be loaded at one time.
|
||||
*/
|
||||
public static final int MAX_LOADED_WAYPOINTS_POINTS = 10000;
|
||||
|
||||
/**
|
||||
* Any time segment where the distance traveled is less than this value will
|
||||
* not be considered moving.
|
||||
*/
|
||||
public static final double MAX_NO_MOVEMENT_DISTANCE = 2;
|
||||
|
||||
/**
|
||||
* Anything faster than that (in meters per second) will be considered moving.
|
||||
*/
|
||||
public static final double MAX_NO_MOVEMENT_SPEED = 0.224;
|
||||
|
||||
/**
|
||||
* Ignore any acceleration faster than this.
|
||||
* Will ignore any speeds that imply accelaration greater than 2g's
|
||||
* 2g = 19.6 m/s^2 = 0.0002 m/ms^2 = 0.02 m/(m*ms)
|
||||
*/
|
||||
public static final double MAX_ACCELERATION = 0.02;
|
||||
|
||||
/**
|
||||
* The type of account that we can use for gdata uploads.
|
||||
*/
|
||||
public static final String ACCOUNT_TYPE = "com.google";
|
||||
|
||||
public static int getActionFromMenuId(int menuId) {
|
||||
switch (menuId) {
|
||||
case MyTracksConstants.MENU_SEND_TO_GOOGLE:
|
||||
return MyTracksConstants.SEND_TO_GOOGLE_DIALOG;
|
||||
case MyTracksConstants.MENU_EDIT:
|
||||
return MyTracksConstants.EDIT_DETAILS;
|
||||
case MyTracksConstants.MENU_DELETE:
|
||||
return MyTracksConstants.DELETE_TRACK;
|
||||
case MyTracksConstants.MENU_SHARE_LINK:
|
||||
return MyTracksConstants.SHARE_LINK;
|
||||
case MyTracksConstants.MENU_SHARE_KML_FILE:
|
||||
return MyTracksConstants.SHARE_KML_FILE;
|
||||
case MyTracksConstants.MENU_SHARE_GPX_FILE:
|
||||
return MyTracksConstants.SHARE_GPX_FILE;
|
||||
case MyTracksConstants.MENU_SHARE_CSV_FILE:
|
||||
return MyTracksConstants.SHARE_CSV_FILE;
|
||||
case MyTracksConstants.MENU_SAVE_GPX_FILE:
|
||||
return MyTracksConstants.SAVE_GPX_FILE;
|
||||
case MyTracksConstants.MENU_SAVE_KML_FILE:
|
||||
return MyTracksConstants.SAVE_KML_FILE;
|
||||
case MyTracksConstants.MENU_SAVE_CSV_FILE:
|
||||
return MyTracksConstants.SAVE_CSV_FILE;
|
||||
case MyTracksConstants.MENU_CLEAR_MAP:
|
||||
return MyTracksConstants.CLEAR_MAP;
|
||||
default:
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This is an abstract utility class.
|
||||
*/
|
||||
private MyTracksConstants() { }
|
||||
protected MyTracksConstants() { }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* Copyright 2008 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
package com.google.android.apps.mytracks;
|
||||
|
||||
import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
|
||||
import com.google.android.maps.mytracks.R;
|
||||
|
||||
import android.app.AlertDialog;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Handler;
|
||||
import android.os.Message;
|
||||
import android.util.Log;
|
||||
|
||||
/**
|
||||
* A utility class that can be used to delete all tracks and track points
|
||||
* from the provider, including asking for confirmation from the user via
|
||||
* a dialog.
|
||||
*
|
||||
* @author Leif Hendrik Wilden
|
||||
*/
|
||||
public class MyTracksDeleteAllTracks extends Handler {
|
||||
|
||||
private final Context context;
|
||||
private final Runnable done;
|
||||
|
||||
public MyTracksDeleteAllTracks(Context context, Runnable done) {
|
||||
this.context = context;
|
||||
this.done = done;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleMessage(Message msg) {
|
||||
super.handleMessage(msg);
|
||||
|
||||
AlertDialog dialog = null;
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(context);
|
||||
builder.setMessage(
|
||||
context.getString(R.string.all_data_will_be_permanently_deleted));
|
||||
builder.setTitle(context.getString(R.string.are_you_sure_question));
|
||||
builder.setIcon(android.R.drawable.ic_dialog_alert);
|
||||
builder.setPositiveButton(context.getString(R.string.yes),
|
||||
new DialogInterface.OnClickListener() {
|
||||
public void onClick(DialogInterface dialog, int i) {
|
||||
dialog.dismiss();
|
||||
Log.w(MyTracksConstants.TAG, "deleting all!");
|
||||
MyTracksProviderUtils.Factory.get(context).deleteAllTracks();
|
||||
SharedPreferences prefs =
|
||||
context.getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0);
|
||||
SharedPreferences.Editor editor = prefs.edit();
|
||||
editor.putLong(MyTracksSettings.SELECTED_TRACK, -1);
|
||||
editor.commit();
|
||||
if (done != null) {
|
||||
Handler h = new Handler();
|
||||
h.post(done);
|
||||
}
|
||||
}
|
||||
});
|
||||
builder.setNegativeButton(context.getString(R.string.no),
|
||||
new DialogInterface.OnClickListener() {
|
||||
public void onClick(DialogInterface dialog, int i) {
|
||||
dialog.dismiss();
|
||||
}
|
||||
});
|
||||
dialog = builder.create();
|
||||
dialog.show();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
/*
|
||||
* Copyright 2008 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
package com.google.android.apps.mytracks;
|
||||
|
||||
import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
|
||||
import com.google.android.apps.mytracks.content.Track;
|
||||
import com.google.android.apps.mytracks.content.TracksColumns;
|
||||
import com.google.android.maps.mytracks.R;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.ContentValues;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.view.View.OnClickListener;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.AutoCompleteTextView;
|
||||
import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
|
||||
/**
|
||||
* An activity that let's the user see and edit the user editable track meta
|
||||
* data such as name, activity type and description.
|
||||
*
|
||||
* @author Leif Hendrik Wilden
|
||||
*/
|
||||
public class MyTracksDetails extends Activity implements OnClickListener {
|
||||
|
||||
/**
|
||||
* The id of the track being edited (taken from bundle, "trackid")
|
||||
*/
|
||||
private Long trackId;
|
||||
|
||||
private EditText name;
|
||||
private EditText description;
|
||||
private AutoCompleteTextView category;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle bundle) {
|
||||
super.onCreate(bundle);
|
||||
setContentView(R.layout.mytracks_detail);
|
||||
|
||||
// Required extra when launching this intent:
|
||||
trackId = getIntent().getLongExtra("trackid", -1);
|
||||
if (trackId < 0) {
|
||||
Log.d(MyTracksConstants.TAG,
|
||||
"MyTracksDetails intent was launched w/o track id.");
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
// Optional extra that can be used to suppress the cancel button:
|
||||
boolean hasCancelButton =
|
||||
getIntent().getBooleanExtra("hasCancelButton", true);
|
||||
|
||||
name = (EditText) findViewById(R.id.trackdetails_name);
|
||||
description = (EditText) findViewById(R.id.trackdetails_description);
|
||||
category = (AutoCompleteTextView) findViewById(R.id.trackdetails_category);
|
||||
ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(
|
||||
this,
|
||||
R.array.activity_types,
|
||||
android.R.layout.simple_dropdown_item_1line);
|
||||
category.setAdapter(adapter);
|
||||
|
||||
Button cancel = (Button) findViewById(R.id.trackdetails_cancel);
|
||||
if (hasCancelButton) {
|
||||
cancel.setOnClickListener(this);
|
||||
cancel.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
cancel.setVisibility(View.GONE);
|
||||
}
|
||||
Button save = (Button) findViewById(R.id.trackdetails_save);
|
||||
save.setOnClickListener(this);
|
||||
|
||||
fillDialog();
|
||||
}
|
||||
|
||||
private void fillDialog() {
|
||||
Track track = MyTracksProviderUtils.Factory.get(this).getTrack(trackId);
|
||||
if (track != null) {
|
||||
name.setText(track.getName());
|
||||
description.setText(track.getDescription());
|
||||
category.setText(track.getCategory());
|
||||
}
|
||||
}
|
||||
|
||||
private void saveDialog() {
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(TracksColumns.NAME, name.getText().toString());
|
||||
values.put(TracksColumns.DESCRIPTION, description.getText().toString());
|
||||
values.put(TracksColumns.CATEGORY, category.getText().toString());
|
||||
getContentResolver().update(
|
||||
TracksColumns.CONTENT_URI,
|
||||
values,
|
||||
"_id = " + trackId,
|
||||
null/*selectionArgs*/);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
switch (v.getId()) {
|
||||
case R.id.trackdetails_cancel:
|
||||
finish();
|
||||
break;
|
||||
case R.id.trackdetails_save:
|
||||
saveDialog();
|
||||
finish();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,269 @@
|
||||
/*
|
||||
* Copyright 2008 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
package com.google.android.apps.mytracks;
|
||||
|
||||
import com.google.android.apps.mytracks.content.TracksColumns;
|
||||
import com.google.android.apps.mytracks.util.StringUtils;
|
||||
import com.google.android.apps.mytracks.util.UnitConversions;
|
||||
import com.google.android.maps.mytracks.R;
|
||||
|
||||
import android.app.ListActivity;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.database.Cursor;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.view.ContextMenu;
|
||||
import android.view.ContextMenu.ContextMenuInfo;
|
||||
import android.view.MenuItem;
|
||||
import android.view.SubMenu;
|
||||
import android.view.View;
|
||||
import android.view.View.OnCreateContextMenuListener;
|
||||
import android.view.Window;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.ListView;
|
||||
import android.widget.SimpleCursorAdapter;
|
||||
import android.widget.TextView;
|
||||
|
||||
/**
|
||||
* A list activity displaying all the recorded tracks. There's a context
|
||||
* menu (via long press) displaying various options such as showing, editing,
|
||||
* deleting, sending to MyMaps, or writing to SD card.
|
||||
*
|
||||
* @author Leif Hendrik Wilden
|
||||
*/
|
||||
public class MyTracksList extends ListActivity
|
||||
implements SharedPreferences.OnSharedPreferenceChangeListener,
|
||||
View.OnClickListener {
|
||||
|
||||
private int contextPosition = -1;
|
||||
private long trackId = -1;
|
||||
private ListView listView = null;
|
||||
private boolean metricUnits = true;
|
||||
|
||||
private Cursor tracksCursor = null;
|
||||
|
||||
/**
|
||||
* The id of the currently recording track.
|
||||
*/
|
||||
private long recordingTrackId = -1;
|
||||
|
||||
private final OnCreateContextMenuListener contextMenuListener =
|
||||
new OnCreateContextMenuListener() {
|
||||
@Override
|
||||
public void onCreateContextMenu(ContextMenu menu, View v,
|
||||
ContextMenuInfo menuInfo) {
|
||||
menu.setHeaderTitle(R.string.tracklist_this_track);
|
||||
AdapterView.AdapterContextMenuInfo info =
|
||||
(AdapterView.AdapterContextMenuInfo) menuInfo;
|
||||
contextPosition = info.position;
|
||||
trackId = MyTracksList.this.listView.getAdapter().getItemId(
|
||||
contextPosition);
|
||||
menu.add(0, MyTracksConstants.MENU_SHOW, 0,
|
||||
R.string.tracklist_show_track);
|
||||
menu.add(0, MyTracksConstants.MENU_EDIT, 0,
|
||||
R.string.tracklist_edit_track);
|
||||
if (!MyTracks.getInstance().isRecording()
|
||||
|| trackId != recordingTrackId) {
|
||||
menu.add(0, MyTracksConstants.MENU_SEND_TO_GOOGLE, 0,
|
||||
R.string.tracklist_send_to_google);
|
||||
SubMenu share = menu.addSubMenu(0, MyTracksConstants.MENU_SHARE, 0,
|
||||
R.string.tracklist_share_track);
|
||||
share.add(0, MyTracksConstants.MENU_SHARE_LINK, 0,
|
||||
R.string.tracklist_share_link);
|
||||
share.add(0, MyTracksConstants.MENU_SHARE_GPX_FILE, 0,
|
||||
R.string.tracklist_share_gpx_file);
|
||||
share.add(0, MyTracksConstants.MENU_SHARE_KML_FILE, 0,
|
||||
R.string.tracklist_share_kml_file);
|
||||
share.add(0, MyTracksConstants.MENU_SHARE_CSV_FILE, 0,
|
||||
R.string.tracklist_share_csv_file);
|
||||
SubMenu save = menu.addSubMenu(0,
|
||||
MyTracksConstants.MENU_WRITE_TO_SD_CARD, 0,
|
||||
R.string.tracklist_write_to_sd);
|
||||
save.add(0, MyTracksConstants.MENU_SAVE_GPX_FILE, 0,
|
||||
R.string.tracklist_save_as_gpx);
|
||||
save.add(0, MyTracksConstants.MENU_SAVE_KML_FILE, 0,
|
||||
R.string.tracklist_save_as_kml);
|
||||
save.add(0, MyTracksConstants.MENU_SAVE_CSV_FILE, 0,
|
||||
R.string.tracklist_save_as_csv);
|
||||
menu.add(0, MyTracksConstants.MENU_DELETE, 0,
|
||||
R.string.tracklist_delete_track);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public void onSharedPreferenceChanged(
|
||||
SharedPreferences sharedPreferences, String key) {
|
||||
if (key == null) {
|
||||
return;
|
||||
}
|
||||
if (key.equals(MyTracksSettings.METRIC_UNITS)) {
|
||||
metricUnits = sharedPreferences.getBoolean(MyTracksSettings.METRIC_UNITS, true);
|
||||
if (tracksCursor != null && !tracksCursor.isClosed()) {
|
||||
tracksCursor.requery();
|
||||
}
|
||||
}
|
||||
if (key.equals(MyTracksSettings.RECORDING_TRACK)) {
|
||||
recordingTrackId = sharedPreferences.getLong(MyTracksSettings.RECORDING_TRACK, -1);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onListItemClick(ListView l, View v, int position, long id) {
|
||||
Intent result = new Intent();
|
||||
result.putExtra("trackid", id);
|
||||
setResult(MyTracksConstants.SHOW_TRACK, result);
|
||||
finish();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onMenuItemSelected(int featureId, MenuItem item) {
|
||||
if (!super.onMenuItemSelected(featureId, item)) {
|
||||
switch (item.getItemId()) {
|
||||
case MyTracksConstants.MENU_SHOW: {
|
||||
onListItemClick(null, null, 0, trackId);
|
||||
return true;
|
||||
}
|
||||
case MyTracksConstants.MENU_EDIT: {
|
||||
Intent intent = new Intent(this, MyTracksDetails.class);
|
||||
intent.putExtra("trackid", trackId);
|
||||
startActivity(intent);
|
||||
return true;
|
||||
}
|
||||
case MyTracksConstants.MENU_SHARE:
|
||||
case MyTracksConstants.MENU_WRITE_TO_SD_CARD:
|
||||
return false;
|
||||
default: {
|
||||
Intent result = new Intent();
|
||||
result.putExtra("trackid", trackId);
|
||||
setResult(
|
||||
MyTracksConstants.getActionFromMenuId(item.getItemId()), result);
|
||||
finish();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
switch (v.getId()) {
|
||||
case R.id.tracklist_btn_delete_all: {
|
||||
Handler h = new MyTracksDeleteAllTracks(this, null);
|
||||
h.handleMessage(null);
|
||||
break;
|
||||
}
|
||||
case R.id.tracklist_btn_export_all: {
|
||||
new ExportAllTracks(this);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
// We don't need a window title bar:
|
||||
requestWindowFeature(Window.FEATURE_NO_TITLE);
|
||||
|
||||
setContentView(R.layout.mytracks_list);
|
||||
|
||||
listView = getListView();
|
||||
listView.setOnCreateContextMenuListener(contextMenuListener);
|
||||
|
||||
findViewById(R.id.tracklist_btn_delete_all).setOnClickListener(this);
|
||||
findViewById(R.id.tracklist_btn_export_all).setOnClickListener(this);
|
||||
|
||||
SharedPreferences preferences =
|
||||
getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0);
|
||||
preferences.registerOnSharedPreferenceChangeListener(this);
|
||||
metricUnits = preferences.getBoolean(MyTracksSettings.METRIC_UNITS, true);
|
||||
recordingTrackId =
|
||||
preferences.getLong(MyTracksSettings.RECORDING_TRACK, -1);
|
||||
|
||||
tracksCursor = getContentResolver().query(
|
||||
TracksColumns.CONTENT_URI, null, null, null, "_id DESC");
|
||||
startManagingCursor(tracksCursor);
|
||||
setListAdapter();
|
||||
}
|
||||
|
||||
private void setListAdapter() {
|
||||
// Get a cursor with all tracks
|
||||
SimpleCursorAdapter adapter = new SimpleCursorAdapter(
|
||||
this,
|
||||
R.layout.mytracks_list_item,
|
||||
tracksCursor,
|
||||
new String[] { TracksColumns.NAME, TracksColumns.STARTTIME,
|
||||
TracksColumns.TOTALDISTANCE, TracksColumns.DESCRIPTION,
|
||||
TracksColumns.CATEGORY },
|
||||
new int[] { R.id.trackdetails_item_name, R.id.trackdetails_item_time,
|
||||
R.id.trackdetails_item_stats, R.id.trackdetails_item_description,
|
||||
R.id.trackdetails_item_category });
|
||||
|
||||
final int startTimeIdx =
|
||||
tracksCursor.getColumnIndexOrThrow(TracksColumns.STARTTIME);
|
||||
final int totalTimeIdx =
|
||||
tracksCursor.getColumnIndexOrThrow(TracksColumns.TOTALTIME);
|
||||
final int totalDistanceIdx =
|
||||
tracksCursor.getColumnIndexOrThrow(TracksColumns.TOTALDISTANCE);
|
||||
|
||||
adapter.setViewBinder(new SimpleCursorAdapter.ViewBinder() {
|
||||
@Override
|
||||
public boolean setViewValue(View view, Cursor cursor, int columnIndex) {
|
||||
TextView textView = (TextView) view;
|
||||
if (columnIndex == startTimeIdx) {
|
||||
long time = cursor.getLong(startTimeIdx);
|
||||
textView.setText(String.format("%tc", time));
|
||||
} else if (columnIndex == totalDistanceIdx) {
|
||||
double length = cursor.getDouble(totalDistanceIdx);
|
||||
String lengthUnit = null;
|
||||
if (metricUnits) {
|
||||
if (length > 1000) {
|
||||
length /= 1000;
|
||||
lengthUnit = getString(R.string.kilometer);
|
||||
} else {
|
||||
lengthUnit = getString(R.string.meter);
|
||||
}
|
||||
} else {
|
||||
if (length > UnitConversions.MI_TO_M) {
|
||||
length /= UnitConversions.MI_TO_M;
|
||||
lengthUnit = getString(R.string.mile);
|
||||
} else {
|
||||
length *= UnitConversions.M_TO_FT;
|
||||
lengthUnit = getString(R.string.feet);
|
||||
}
|
||||
}
|
||||
textView.setText(String.format("%s %.2f %s",
|
||||
StringUtils.formatTime(cursor.getLong(totalTimeIdx)),
|
||||
length,
|
||||
lengthUnit));
|
||||
} else {
|
||||
textView.setText(cursor.getString(columnIndex));
|
||||
if (textView.getText().length() < 1) {
|
||||
textView.setVisibility(View.GONE);
|
||||
} else {
|
||||
textView.setVisibility(View.VISIBLE);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
});
|
||||
setListAdapter(adapter);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,978 @@
|
||||
/*
|
||||
* Copyright 2008 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
package com.google.android.apps.mytracks;
|
||||
|
||||
import com.google.android.apps.mymaps.GeoRect;
|
||||
import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
|
||||
import com.google.android.apps.mytracks.content.Track;
|
||||
import com.google.android.apps.mytracks.content.TrackPointsColumns;
|
||||
import com.google.android.apps.mytracks.content.Waypoint;
|
||||
import com.google.android.apps.mytracks.content.WaypointsColumns;
|
||||
import com.google.android.apps.mytracks.util.MyTracksUtils;
|
||||
import com.google.android.maps.GeoPoint;
|
||||
import com.google.android.maps.MapActivity;
|
||||
import com.google.android.maps.MapController;
|
||||
import com.google.android.maps.MapView;
|
||||
import com.google.android.maps.mytracks.R;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.database.ContentObserver;
|
||||
import android.database.Cursor;
|
||||
import android.hardware.GeomagneticField;
|
||||
import android.hardware.Sensor;
|
||||
import android.hardware.SensorEvent;
|
||||
import android.hardware.SensorEventListener;
|
||||
import android.hardware.SensorManager;
|
||||
import android.location.Location;
|
||||
import android.location.LocationListener;
|
||||
import android.location.LocationManager;
|
||||
import android.location.LocationProvider;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.HandlerThread;
|
||||
import android.provider.Settings;
|
||||
import android.util.Log;
|
||||
import android.view.ContextMenu;
|
||||
import android.view.ContextMenu.ContextMenuInfo;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.SubMenu;
|
||||
import android.view.View;
|
||||
import android.view.View.OnCreateContextMenuListener;
|
||||
import android.view.Window;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.RelativeLayout;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
/**
|
||||
* The map view activity of the MyTracks application.
|
||||
*
|
||||
* @author Leif Hendrik Wilden
|
||||
*/
|
||||
public class MyTracksMap extends MapActivity
|
||||
implements LocationListener, SensorEventListener, View.OnTouchListener,
|
||||
View.OnClickListener, SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
|
||||
// Saved instance state keys:
|
||||
// ---------------------------
|
||||
|
||||
public static final String KEY_CURRENT_LOCATION = "currentLocation";
|
||||
public static final String KEY_KEEP_MY_LOCATION_VISIBLE =
|
||||
"keepMyLocationVisible";
|
||||
public static final String KEY_HAVE_GOOD_FIX = "haveGoodFix";
|
||||
|
||||
/**
|
||||
* The currently selected track (or null if nothing selected).
|
||||
*/
|
||||
private Track selectedTrack;
|
||||
|
||||
/**
|
||||
* The id of the currently recording track.
|
||||
*/
|
||||
private long recordingTrackId = -1;
|
||||
|
||||
/**
|
||||
* True if the map should be scrolled so that the pointer is always in the
|
||||
* visible area.
|
||||
*/
|
||||
private boolean keepMyLocationVisible;
|
||||
|
||||
/**
|
||||
* Id of the last location that was seen when reading tracks from the
|
||||
* provider. This is used to determine which locations are new compared to the
|
||||
* last time the mapOverlay was updated.
|
||||
*/
|
||||
private long lastSeenLocationId = -1;
|
||||
|
||||
/**
|
||||
* Magnetic variation.
|
||||
*/
|
||||
private double variation;
|
||||
|
||||
/**
|
||||
* From the shared preferences:
|
||||
*/
|
||||
private int minRequiredAccuracy =
|
||||
MyTracksSettings.DEFAULT_MIN_REQUIRED_ACCURACY;
|
||||
|
||||
/**
|
||||
* True, if the application thinks it has a good fix, i.e. accuracy is better
|
||||
* than the required accuracy.
|
||||
*/
|
||||
private boolean haveGoodFix;
|
||||
|
||||
/**
|
||||
* The current pointer location.
|
||||
*/
|
||||
private Location currentLocation;
|
||||
|
||||
/**
|
||||
* A thread with a looper. Post to updateTrackHandler to execute Runnables on
|
||||
* this thread.
|
||||
*/
|
||||
private final HandlerThread updateTrackThread =
|
||||
new HandlerThread("updateTrackThread");
|
||||
|
||||
/** Handler for updateTrackThread */
|
||||
private Handler updateTrackHandler;
|
||||
|
||||
private MyTracksProviderUtils providerUtils;
|
||||
|
||||
/**
|
||||
* A runnable that updates the track from the provider (looking for points
|
||||
* added after "lastSeenLocationId".
|
||||
*/
|
||||
private final Runnable updateTrackRunnable = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (selectedTrack == null) {
|
||||
return;
|
||||
}
|
||||
Cursor cursor = null;
|
||||
try {
|
||||
cursor = providerUtils.getLocationsCursor(
|
||||
recordingTrackId,
|
||||
lastSeenLocationId + 1,
|
||||
MyTracksConstants.MAX_DISPLAYED_TRACK_POINTS
|
||||
- selectedTrack.getLocations().size(),
|
||||
true);
|
||||
if (cursor != null) {
|
||||
if (cursor.moveToLast()) {
|
||||
final int idColumnIdx =
|
||||
cursor.getColumnIndexOrThrow(TrackPointsColumns._ID);
|
||||
do {
|
||||
lastSeenLocationId = cursor.getLong(idColumnIdx);
|
||||
Location location = providerUtils.createLocation(cursor);
|
||||
if (location != null) {
|
||||
selectedTrack.addLocation(location);
|
||||
}
|
||||
} while (cursor.moveToPrevious());
|
||||
}
|
||||
}
|
||||
} catch (RuntimeException e) {
|
||||
Log.w(MyTracksConstants.TAG, "Caught an unexpected exception: ", e);
|
||||
} finally {
|
||||
if (cursor != null) {
|
||||
cursor.close();
|
||||
}
|
||||
mapView.postInvalidate();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* A runnable that restores all track points from the provider.
|
||||
*/
|
||||
private Runnable restoreTrackRunnable = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (selectedTrack == null) {
|
||||
return;
|
||||
}
|
||||
lastSeenLocationId = providerUtils.getTrackPoints(selectedTrack,
|
||||
MyTracksConstants.MAX_DISPLAYED_TRACK_POINTS);
|
||||
mapView.postInvalidate();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* A runnable that restores all waypoints from the provider.
|
||||
*/
|
||||
private final Runnable restoreWaypointsRunnable = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (selectedTrack == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Cursor cursor = null;
|
||||
mapOverlay.clearWaypoints();
|
||||
try {
|
||||
// We will silently drop extra waypoints to make the app responsive.
|
||||
// TODO: Try to only load the waypoints in the view port.
|
||||
cursor = providerUtils.getWaypointsCursor(
|
||||
selectedTrack.getId(), 0,
|
||||
MyTracksConstants.MAX_DISPLAYED_WAYPOINTS_POINTS);
|
||||
if (cursor != null) {
|
||||
if (cursor.moveToFirst()) {
|
||||
do {
|
||||
Waypoint wpt = providerUtils.createWaypoint(cursor);
|
||||
mapOverlay.addWaypoint(wpt);
|
||||
} while (cursor.moveToNext());
|
||||
}
|
||||
}
|
||||
} catch (RuntimeException e) {
|
||||
Log.w(MyTracksConstants.TAG, "Caught an unexpected exception.", e);
|
||||
} finally {
|
||||
if (cursor != null) {
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
mapView.postInvalidate();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* A runnable intended to be posted to the updateTrackThread after the
|
||||
* selected track changes. It will post to the ui thread to update the screen
|
||||
* elements and move the map to show the selected track.
|
||||
*/
|
||||
private final Runnable setSelectedTrackRunnable = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
uiHandler.post(new Runnable() {
|
||||
public void run() {
|
||||
showTrack(selectedTrack);
|
||||
mapOverlay.setSelectedTrack(selectedTrack);
|
||||
mapOverlay.setShowEndMarker(selectedTrack == null ||
|
||||
selectedTrack.getId() != recordingTrackId);
|
||||
mapView.invalidate();
|
||||
busyPane.setVisibility(View.GONE);
|
||||
updateOptionsButton();
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// UI elements:
|
||||
// -------------
|
||||
|
||||
private RelativeLayout screen;
|
||||
private MapView mapView;
|
||||
private MyTracksOverlay mapOverlay;
|
||||
private LinearLayout messagePane;
|
||||
private TextView messageText;
|
||||
private LinearLayout busyPane;
|
||||
private ImageButton optionsBtn;
|
||||
|
||||
private MenuItem myLocation;
|
||||
private MenuItem toggleLayers;
|
||||
|
||||
private SensorManager sensorManager;
|
||||
private LocationManager locationManager;
|
||||
private ContentObserver observer;
|
||||
private ContentObserver waypointObserver;
|
||||
|
||||
/** Handler for callbacks to the UI thread */
|
||||
private final Handler uiHandler = new Handler();
|
||||
|
||||
/**
|
||||
* We are not displaying driving directions. Just an arbitrary track that is
|
||||
* not associated to any licensed mapping data. Therefore it should be okay to
|
||||
* return false here and still comply with the terms of service.
|
||||
*/
|
||||
@Override
|
||||
protected boolean isRouteDisplayed() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* We are displaying a location. This needs to return true in order to comply
|
||||
* with the terms of service.
|
||||
*/
|
||||
@Override
|
||||
protected boolean isLocationDisplayed() {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Application life cycle:
|
||||
// ------------------------
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle bundle) {
|
||||
Log.d(MyTracksConstants.TAG, "MyTracksMap.onCreate");
|
||||
super.onCreate(bundle);
|
||||
|
||||
providerUtils = MyTracksProviderUtils.Factory.get(this);
|
||||
|
||||
// We don't need a window title bar:
|
||||
requestWindowFeature(Window.FEATURE_NO_TITLE);
|
||||
|
||||
// Inflate the layout:
|
||||
setContentView(R.layout.mytracks_layout);
|
||||
|
||||
// Remove the window's background because the MapView will obscure it
|
||||
getWindow().setBackgroundDrawable(null);
|
||||
|
||||
// Set up a map overlay:
|
||||
screen = (RelativeLayout) findViewById(R.id.screen);
|
||||
mapView = (MapView) findViewById(R.id.map);
|
||||
mapView.requestFocus();
|
||||
mapOverlay = new MyTracksOverlay(this);
|
||||
mapView.getOverlays().add(mapOverlay);
|
||||
mapView.setOnTouchListener(this);
|
||||
messagePane = (LinearLayout) findViewById(R.id.messagepane);
|
||||
messageText = (TextView) findViewById(R.id.messagetext);
|
||||
busyPane = (LinearLayout) findViewById(R.id.busypane);
|
||||
optionsBtn = (ImageButton) findViewById(R.id.showOptions);
|
||||
|
||||
optionsBtn.setOnCreateContextMenuListener(contextMenuListener);
|
||||
optionsBtn.setOnClickListener(this);
|
||||
|
||||
setupZoomControls();
|
||||
|
||||
// Get the sensor and location managers:
|
||||
sensorManager = (SensorManager) getSystemService(SENSOR_SERVICE);
|
||||
locationManager =
|
||||
(LocationManager) getSystemService(Context.LOCATION_SERVICE);
|
||||
|
||||
updateTrackThread.start();
|
||||
updateTrackHandler = new Handler(updateTrackThread.getLooper());
|
||||
|
||||
// Register observer for the track point provider:
|
||||
Handler contentHandler = new Handler();
|
||||
observer = new ContentObserver(contentHandler) {
|
||||
@Override
|
||||
public void onChange(boolean selfChange) {
|
||||
Log.d(MyTracksConstants.TAG, "MyTracksMap: ContentObserver.onChange");
|
||||
// Check for any new locations and append them to the currently
|
||||
// recording track:
|
||||
if (!MyTracks.getInstance().isRecording()) {
|
||||
// No track is being recorded. We should not be here.
|
||||
return;
|
||||
}
|
||||
if (selectedTrack == null
|
||||
|| selectedTrack.getId() != recordingTrackId) {
|
||||
// No track, or one other than the recording track is selected,
|
||||
// don't bother.
|
||||
return;
|
||||
}
|
||||
// Update can potentially be lengthy, put it in its own thread:
|
||||
updateTrackHandler.post(updateTrackRunnable);
|
||||
super.onChange(selfChange);
|
||||
}
|
||||
};
|
||||
|
||||
waypointObserver = new ContentObserver(contentHandler) {
|
||||
@Override
|
||||
public void onChange(boolean selfChange) {
|
||||
Log.d(MyTracksConstants.TAG,
|
||||
"MyTracksMap: ContentObserver.onChange waypoints");
|
||||
if (selectedTrack == null) {
|
||||
return;
|
||||
}
|
||||
updateTrackHandler.post(restoreWaypointsRunnable);
|
||||
super.onChange(selfChange);
|
||||
}
|
||||
};
|
||||
|
||||
// Read shared preferences and register change listener:
|
||||
SharedPreferences preferences =
|
||||
getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0);
|
||||
if (preferences != null) {
|
||||
minRequiredAccuracy = preferences.getInt(
|
||||
MyTracksSettings.MIN_REQUIRED_ACCURACY,
|
||||
MyTracksSettings.DEFAULT_MIN_REQUIRED_ACCURACY);
|
||||
recordingTrackId =
|
||||
preferences.getLong(MyTracksSettings.RECORDING_TRACK, -1);
|
||||
long selectedTrackId =
|
||||
preferences.getLong(MyTracksSettings.SELECTED_TRACK, -1);
|
||||
if (selectedTrackId >= 0) {
|
||||
setSelectedTrack(selectedTrackId);
|
||||
}
|
||||
updateOptionsButton();
|
||||
preferences.registerOnSharedPreferenceChangeListener(this);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
protected void setupZoomControls() {
|
||||
mapView.setBuiltInZoomControls(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
// Final cleanup before activity is destroyed.
|
||||
// May not be called at all in some situations.
|
||||
Log.d(MyTracksConstants.TAG, "MyTracksMap.onDestroy");
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStart() {
|
||||
// Called after onCreate or onStop.
|
||||
// Will be followed by onRestart.
|
||||
Log.d(MyTracksConstants.TAG, "MyTracksMap.onStart");
|
||||
super.onStart();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStop() {
|
||||
// Called when activity is no longer visible to user.
|
||||
// Next either onStart, onDestroy or nothing will be called.
|
||||
// This method may never be called in low memory situations.
|
||||
Log.d(MyTracksConstants.TAG, "MyTracksMap.onStop");
|
||||
super.onStop();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onRestart() {
|
||||
// Called when the current activity is being re-displayed.
|
||||
// Will be followed by onResume.
|
||||
Log.d(MyTracksConstants.TAG, "MyTracksMap.onRestart");
|
||||
super.onRestart();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPause() {
|
||||
// Called when activity is going into the background, but has not (yet) been
|
||||
// killed. Shouldn't block longer than approx. 2 seconds.
|
||||
Log.d(MyTracksConstants.TAG, "MyTracksMap.onPause");
|
||||
unregisterLocationAndSensorListeners();
|
||||
unregisterContentObservers();
|
||||
super.onPause();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
// Called when the current activity is being displayed or re-displayed
|
||||
// to the user.
|
||||
Log.d(MyTracksConstants.TAG, "MyTracksMap.onResume");
|
||||
super.onResume();
|
||||
|
||||
// Make sure any updates that might have happened are propagated to the
|
||||
// Map overlay:
|
||||
observer.onChange(false);
|
||||
waypointObserver.onChange(false);
|
||||
|
||||
registerContentObservers();
|
||||
registerLocationAndSensorListeners();
|
||||
|
||||
if (locationManager.isProviderEnabled(MyTracksConstants.GPS_PROVIDER)) {
|
||||
messageText.setText(R.string.wait_for_fix);
|
||||
messagePane.setOnClickListener(null);
|
||||
} else {
|
||||
messageText.setText(R.string.status_enable_gps);
|
||||
messagePane.setVisibility(View.VISIBLE);
|
||||
messagePane.setOnClickListener(this);
|
||||
screen.requestLayout();
|
||||
}
|
||||
|
||||
// While this activity was paused the user may have deleted the selected
|
||||
// track. In that case the map overlay needs to be cleared:
|
||||
Track track = mapOverlay.getSelectedTrack();
|
||||
if (track != null && !providerUtils.trackExists(track.getId())) {
|
||||
// The recording track must have been deleted meanwhile.
|
||||
mapOverlay.setSelectedTrack(null);
|
||||
mapView.invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSaveInstanceState(Bundle outState) {
|
||||
Log.d(MyTracksConstants.TAG, "MyTracksMap.onSaveInstanceState");
|
||||
outState.putBoolean(KEY_HAVE_GOOD_FIX, haveGoodFix);
|
||||
outState.putBoolean(KEY_KEEP_MY_LOCATION_VISIBLE, keepMyLocationVisible);
|
||||
if (currentLocation != null) {
|
||||
outState.putParcelable(KEY_CURRENT_LOCATION, currentLocation);
|
||||
}
|
||||
super.onSaveInstanceState(outState);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onRestoreInstanceState(Bundle bundle) {
|
||||
Log.d(MyTracksConstants.TAG, "MyTracksMap.onRestoreInstanceState");
|
||||
if (bundle != null) {
|
||||
super.onRestoreInstanceState(bundle);
|
||||
haveGoodFix = bundle.getBoolean(KEY_HAVE_GOOD_FIX, false);
|
||||
keepMyLocationVisible =
|
||||
bundle.getBoolean(KEY_KEEP_MY_LOCATION_VISIBLE, false);
|
||||
if (bundle.containsKey(KEY_CURRENT_LOCATION)) {
|
||||
currentLocation = (Location) bundle.getParcelable(KEY_CURRENT_LOCATION);
|
||||
if (currentLocation != null) {
|
||||
setVariation(currentLocation);
|
||||
showCurrentLocation();
|
||||
}
|
||||
} else {
|
||||
currentLocation = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Utility functions:
|
||||
// -------------------
|
||||
|
||||
/**
|
||||
* Toggles between satellite and map view.
|
||||
*/
|
||||
public void toggleLayer() {
|
||||
mapView.setSatellite(!mapView.isSatellite());
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers to receive location updates from the GPS location provider and
|
||||
* sensor updated from the compass.
|
||||
*/
|
||||
void registerLocationAndSensorListeners() {
|
||||
if (locationManager != null) {
|
||||
LocationProvider gpsProvider =
|
||||
locationManager.getProvider(MyTracksConstants.GPS_PROVIDER);
|
||||
if (gpsProvider == null) {
|
||||
alert(getString(R.string.error_no_gps_location_provider));
|
||||
return;
|
||||
} else {
|
||||
Log.d(MyTracksConstants.TAG, "MyTracksMap: Using location provider "
|
||||
+ gpsProvider.getName());
|
||||
}
|
||||
locationManager.requestLocationUpdates(gpsProvider.getName(),
|
||||
0 /*minTime*/, 0 /*minDist*/, this);
|
||||
try {
|
||||
locationManager.requestLocationUpdates(LocationManager.NETWORK_PROVIDER,
|
||||
1000 * 60 * 5 /*minTime*/, 0 /*minDist*/, this);
|
||||
} 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(MyTracksConstants.TAG,
|
||||
"Could not register network location listener.");
|
||||
}
|
||||
}
|
||||
if (sensorManager == null) {
|
||||
return;
|
||||
}
|
||||
Sensor compass = sensorManager.getDefaultSensor(Sensor.TYPE_ORIENTATION);
|
||||
if (compass == null) {
|
||||
return;
|
||||
}
|
||||
Log.d(MyTracksConstants.TAG,
|
||||
"MyTracksMap: Now registering sensor listeners.");
|
||||
sensorManager.registerListener(
|
||||
this, compass, SensorManager.SENSOR_DELAY_UI);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregisters all location and sensor listeners
|
||||
*/
|
||||
void unregisterLocationAndSensorListeners() {
|
||||
if (locationManager != null) {
|
||||
Log.d(MyTracksConstants.TAG,
|
||||
"MyTracksMap: Now unregistering location listeners.");
|
||||
locationManager.removeUpdates(this);
|
||||
}
|
||||
if (sensorManager != null) {
|
||||
Log.d(MyTracksConstants.TAG,
|
||||
"MyTracksMap: Now unregistering sensor listeners.");
|
||||
sensorManager.unregisterListener(this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the content observer for the map overlay.
|
||||
*/
|
||||
private void registerContentObservers() {
|
||||
getContentResolver().registerContentObserver(
|
||||
TrackPointsColumns.CONTENT_URI, false /* notifyForDescendents */,
|
||||
observer);
|
||||
getContentResolver().registerContentObserver(
|
||||
WaypointsColumns.CONTENT_URI, false /* notifyForDescendents */,
|
||||
waypointObserver);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregisters the content observer for the map overlay.
|
||||
*/
|
||||
private void unregisterContentObservers() {
|
||||
getContentResolver().unregisterContentObserver(observer);
|
||||
getContentResolver().unregisterContentObserver(waypointObserver);
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows the options button if a track is selected, or hide it if not.
|
||||
*/
|
||||
private void updateOptionsButton() {
|
||||
optionsBtn.setVisibility(
|
||||
selectedTrack != null ? View.VISIBLE : View.INVISIBLE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests if a location is visible.
|
||||
*
|
||||
* @param location a given location
|
||||
* @return true if the given location is within the visible map area
|
||||
*/
|
||||
private boolean locationIsVisible(Location location) {
|
||||
if (location == null || mapView == null) {
|
||||
return false;
|
||||
}
|
||||
GeoPoint center = mapView.getMapCenter();
|
||||
int latSpan = mapView.getLatitudeSpan();
|
||||
int lonSpan = mapView.getLongitudeSpan();
|
||||
|
||||
// Bottom of map view is obscured by zoom controls/buttons.
|
||||
// Subtract a margin from the visible area:
|
||||
GeoPoint marginBottom = mapView.getProjection().fromPixels(
|
||||
0, mapView.getHeight());
|
||||
GeoPoint marginTop = mapView.getProjection().fromPixels(0,
|
||||
mapView.getHeight()
|
||||
- mapView.getZoomButtonsController().getZoomControls().getHeight());
|
||||
int margin =
|
||||
Math.abs(marginTop.getLatitudeE6() - marginBottom.getLatitudeE6());
|
||||
GeoRect r = new GeoRect(center, latSpan, lonSpan);
|
||||
r.top += margin;
|
||||
|
||||
GeoPoint geoPoint = MyTracksUtils.getGeoPoint(location);
|
||||
return r.contains(geoPoint);
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves the location pointer to the current location and center the map if
|
||||
* the current location is outside the visible area.
|
||||
*/
|
||||
private void showCurrentLocation() {
|
||||
if (currentLocation == null || mapOverlay == null || mapView == null) {
|
||||
return;
|
||||
}
|
||||
mapOverlay.setMyLocation(currentLocation);
|
||||
mapView.invalidate();
|
||||
if (keepMyLocationVisible && !locationIsVisible(currentLocation)) {
|
||||
MapController controller = mapView.getController();
|
||||
GeoPoint geoPoint = MyTracksUtils.getGeoPoint(currentLocation);
|
||||
controller.animateTo(geoPoint);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Zooms and pans the map so that the given track is visible.
|
||||
*
|
||||
* @param track a given track
|
||||
*/
|
||||
public void showTrack(Track track) {
|
||||
if (track == null || mapView == null || track.getNumberOfPoints() < 2) {
|
||||
return;
|
||||
}
|
||||
int latSpanE6 = track.getTop() - track.getBottom();
|
||||
int lonSpanE6 = track.getRight() - track.getLeft();
|
||||
if (latSpanE6 > 0 && latSpanE6 < 180E6 && lonSpanE6 > 0
|
||||
&& lonSpanE6 < 180E6) {
|
||||
keepMyLocationVisible = false;
|
||||
GeoPoint center = new GeoPoint(
|
||||
track.getBottom() + latSpanE6 / 2,
|
||||
track.getLeft() + lonSpanE6 / 2);
|
||||
if (MyTracksUtils.isValidGeoPoint(center)) {
|
||||
mapView.getController().setCenter(center);
|
||||
mapView.getController().zoomToSpan(latSpanE6, lonSpanE6);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Zooms and pans the map so that the given waypoint is visible.
|
||||
*/
|
||||
public void showWaypoint(long waypointId) {
|
||||
Waypoint wpt = providerUtils.getWaypoint(waypointId);
|
||||
if (wpt != null && wpt.getLocation() != null) {
|
||||
keepMyLocationVisible = false;
|
||||
GeoPoint center = new GeoPoint(
|
||||
(int) (wpt.getLocation().getLatitude() * 1E6),
|
||||
(int) (wpt.getLocation().getLongitude() * 1E6));
|
||||
mapView.getController().setCenter(center);
|
||||
mapView.getController().setZoom(20);
|
||||
mapView.invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the selected track and zoom and pan the map so that it is visible.
|
||||
*
|
||||
* @param trackId a given track id
|
||||
*/
|
||||
public void setSelectedTrack(final long trackId) {
|
||||
if (selectedTrack != null && selectedTrack.getId() == trackId) {
|
||||
// Selected track did not change, nothing to do.
|
||||
mapOverlay.setSelectedTrack(selectedTrack);
|
||||
mapView.invalidate();
|
||||
updateOptionsButton();
|
||||
return;
|
||||
}
|
||||
if (trackId < 0) {
|
||||
// Remove selection.
|
||||
selectedTrack = null;
|
||||
mapOverlay.setSelectedTrack(null);
|
||||
mapOverlay.clearWaypoints();
|
||||
updateOptionsButton();
|
||||
mapView.invalidate();
|
||||
return;
|
||||
}
|
||||
busyPane.setVisibility(View.VISIBLE);
|
||||
selectedTrack = providerUtils.getTrack(trackId);
|
||||
updateTrackHandler.post(restoreTrackRunnable);
|
||||
updateTrackHandler.post(restoreWaypointsRunnable);
|
||||
updateTrackHandler.post(setSelectedTrackRunnable);
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays an alert for a few seconds.
|
||||
*
|
||||
* @param txt The text to be displayed
|
||||
*/
|
||||
public void alert(String txt) {
|
||||
Toast.makeText(this, txt, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
|
||||
public void launchMyLocationSettings() {
|
||||
startActivity(new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS));
|
||||
}
|
||||
|
||||
public void setVariation(Location location) {
|
||||
long timestamp = location.getTime();
|
||||
if (timestamp == 0) {
|
||||
// Hack for Samsung phones which don't populate the time field
|
||||
timestamp = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
GeomagneticField field = new GeomagneticField(
|
||||
(float) location.getLatitude(),
|
||||
(float) location.getLongitude(),
|
||||
(float) location.getAltitude(),
|
||||
timestamp);
|
||||
variation = field.getDeclination();
|
||||
|
||||
Log.d(MyTracksConstants.TAG,
|
||||
"MyTracksMap: Variation reset to " + variation + " degrees.");
|
||||
}
|
||||
|
||||
public MyTracksOverlay getMapOverlay() {
|
||||
return mapOverlay;
|
||||
}
|
||||
|
||||
public MapView getMapView() {
|
||||
return mapView;
|
||||
}
|
||||
|
||||
// Event listeners:
|
||||
// -----------------
|
||||
|
||||
private final OnCreateContextMenuListener contextMenuListener =
|
||||
new OnCreateContextMenuListener() {
|
||||
@Override
|
||||
public void onCreateContextMenu(ContextMenu menu, View v,
|
||||
ContextMenuInfo menuInfo) {
|
||||
menu.setHeaderTitle(R.string.tracklist_this_track);
|
||||
menu.add(0, MyTracksConstants.MENU_EDIT, 0,
|
||||
R.string.tracklist_edit_track);
|
||||
if (!MyTracks.getInstance().isRecording() || (selectedTrack != null
|
||||
&& selectedTrack.getId() != recordingTrackId)) {
|
||||
menu.add(0, MyTracksConstants.MENU_SEND_TO_GOOGLE, 0,
|
||||
R.string.tracklist_send_to_google);
|
||||
SubMenu share = menu.addSubMenu(0, MyTracksConstants.MENU_SHARE, 0,
|
||||
R.string.tracklist_share_track);
|
||||
share.add(0, MyTracksConstants.MENU_SHARE_LINK, 0,
|
||||
R.string.tracklist_share_link);
|
||||
share.add(0, MyTracksConstants.MENU_SHARE_GPX_FILE, 0,
|
||||
R.string.tracklist_share_gpx_file);
|
||||
share.add(0, MyTracksConstants.MENU_SHARE_KML_FILE, 0,
|
||||
R.string.tracklist_share_kml_file);
|
||||
share.add(0, MyTracksConstants.MENU_SHARE_CSV_FILE, 0,
|
||||
R.string.tracklist_share_csv_file);
|
||||
SubMenu save = menu.addSubMenu(0,
|
||||
MyTracksConstants.MENU_WRITE_TO_SD_CARD, 0,
|
||||
R.string.tracklist_write_to_sd);
|
||||
save.add(0, MyTracksConstants.MENU_SAVE_GPX_FILE, 0,
|
||||
R.string.tracklist_save_as_gpx);
|
||||
save.add(0, MyTracksConstants.MENU_SAVE_KML_FILE, 0,
|
||||
R.string.tracklist_save_as_kml);
|
||||
save.add(0, MyTracksConstants.MENU_SAVE_CSV_FILE, 0,
|
||||
R.string.tracklist_save_as_csv);
|
||||
menu.add(0, MyTracksConstants.MENU_CLEAR_MAP, 0,
|
||||
R.string.tracklist_clear_map);
|
||||
menu.add(0, MyTracksConstants.MENU_DELETE, 0,
|
||||
R.string.tracklist_delete_track);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public boolean onMenuItemSelected(int featureId, MenuItem item) {
|
||||
if (!super.onMenuItemSelected(featureId, item)) {
|
||||
if (selectedTrack != null) {
|
||||
MyTracks.getInstance().onActivityResult(
|
||||
MyTracksConstants.getActionFromMenuId(item.getItemId()), RESULT_OK,
|
||||
new Intent());
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
super.onCreateOptionsMenu(menu);
|
||||
myLocation = menu.add(0, MyTracksConstants.MENU_MY_LOCATION, 0,
|
||||
R.string.mylocation);
|
||||
myLocation.setIcon(android.R.drawable.ic_menu_mylocation);
|
||||
toggleLayers = menu.add(0, MyTracksConstants.MENU_TOGGLE_LAYERS, 0,
|
||||
R.string.switch_to_sat);
|
||||
toggleLayers.setIcon(android.R.drawable.ic_menu_mapmode);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPrepareOptionsMenu(Menu menu) {
|
||||
toggleLayers.setTitle(mapView.isSatellite() ?
|
||||
R.string.switch_to_map : R.string.switch_to_sat);
|
||||
return super.onPrepareOptionsMenu(menu);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case MyTracksConstants.MENU_MY_LOCATION: {
|
||||
Location loc = MyTracks.getInstance().getCurrentLocation();
|
||||
if (loc != null) {
|
||||
currentLocation = loc;
|
||||
setVariation(currentLocation);
|
||||
mapOverlay.setMyLocation(loc);
|
||||
mapView.invalidate();
|
||||
GeoPoint geoPoint = MyTracksUtils.getGeoPoint(loc);
|
||||
MapController controller = mapView.getController();
|
||||
controller.animateTo(geoPoint);
|
||||
if (mapView.getZoomLevel() < 18) {
|
||||
controller.setZoom(18);
|
||||
}
|
||||
keepMyLocationVisible = true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
case MyTracksConstants.MENU_TOGGLE_LAYERS: {
|
||||
toggleLayer();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
if (v == messagePane) {
|
||||
launchMyLocationSettings();
|
||||
}
|
||||
if (v == optionsBtn) {
|
||||
optionsBtn.performLongClick();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* We want the pointer to become visible again in case of the next location
|
||||
* update:
|
||||
*/
|
||||
@Override
|
||||
public boolean onTouch(View view, MotionEvent event) {
|
||||
if (keepMyLocationVisible && event.getAction() == MotionEvent.ACTION_MOVE) {
|
||||
if (!locationIsVisible(currentLocation)) {
|
||||
keepMyLocationVisible = false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences,
|
||||
String key) {
|
||||
if (key != null) {
|
||||
if (key.equals(MyTracksSettings.MIN_REQUIRED_ACCURACY)) {
|
||||
minRequiredAccuracy = sharedPreferences.getInt(
|
||||
MyTracksSettings.MIN_REQUIRED_ACCURACY,
|
||||
MyTracksSettings.DEFAULT_MIN_REQUIRED_ACCURACY);
|
||||
} else if (key.equals(MyTracksSettings.SELECTED_TRACK)) {
|
||||
long selectedTrackId =
|
||||
sharedPreferences.getLong(MyTracksSettings.SELECTED_TRACK, -1);
|
||||
setSelectedTrack(selectedTrackId);
|
||||
} else if (key.equals(MyTracksSettings.RECORDING_TRACK)) {
|
||||
recordingTrackId =
|
||||
sharedPreferences.getLong(MyTracksSettings.RECORDING_TRACK, -1);
|
||||
if (selectedTrack != null) {
|
||||
mapOverlay.setShowEndMarker(
|
||||
selectedTrack.getId() != recordingTrackId);
|
||||
mapView.postInvalidate();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onProviderEnabled(String provider) {
|
||||
if (provider.equals(MyTracksConstants.GPS_PROVIDER)) {
|
||||
messageText.setText(R.string.wait_for_fix);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onProviderDisabled(String provider) {
|
||||
if (provider.equals(MyTracksConstants.GPS_PROVIDER)) {
|
||||
messageText.setText(R.string.status_enable_gps);
|
||||
messagePane.setVisibility(View.VISIBLE);
|
||||
messagePane.setOnClickListener(this);
|
||||
screen.requestLayout();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLocationChanged(Location location) {
|
||||
if (location.getProvider().equals(MyTracksConstants.GPS_PROVIDER)) {
|
||||
// Recalculate the variation if there was a jump in location > 1km:
|
||||
if (currentLocation == null
|
||||
|| location.distanceTo(currentLocation) > 1000) {
|
||||
setVariation(location);
|
||||
}
|
||||
currentLocation = location;
|
||||
boolean haveGoodFixNow =
|
||||
currentLocation.getAccuracy() < minRequiredAccuracy;
|
||||
if (haveGoodFixNow != haveGoodFix) {
|
||||
haveGoodFix = haveGoodFixNow;
|
||||
messagePane.setVisibility(haveGoodFix ? View.GONE : View.VISIBLE);
|
||||
screen.requestLayout();
|
||||
}
|
||||
showCurrentLocation();
|
||||
} else {
|
||||
Log.d(MyTracksConstants.TAG,
|
||||
"MyTracksMap: Network location update received.");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStatusChanged(String provider, int status, Bundle extras) {
|
||||
if (provider.equals(MyTracksConstants.GPS_PROVIDER)) {
|
||||
switch (status) {
|
||||
case LocationProvider.OUT_OF_SERVICE:
|
||||
case LocationProvider.TEMPORARILY_UNAVAILABLE:
|
||||
haveGoodFix = false;
|
||||
messagePane.setVisibility(View.VISIBLE);
|
||||
screen.requestLayout();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSensorChanged(SensorEvent se) {
|
||||
synchronized (this) {
|
||||
float magneticHeading = se.values[0];
|
||||
double heading = magneticHeading + variation;
|
||||
if (mapOverlay.setHeading((float) heading)) {
|
||||
mapView.invalidate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAccuracyChanged(Sensor s, int accuracy) {
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,383 @@
|
||||
/*
|
||||
* Copyright 2008 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
package com.google.android.apps.mytracks;
|
||||
|
||||
import com.google.android.apps.mytracks.content.Track;
|
||||
import com.google.android.apps.mytracks.content.Waypoint;
|
||||
import com.google.android.apps.mytracks.util.MyTracksUtils;
|
||||
|
||||
import com.google.android.maps.GeoPoint;
|
||||
import com.google.android.maps.MapView;
|
||||
import com.google.android.maps.Overlay;
|
||||
import com.google.android.maps.mytracks.R;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Path;
|
||||
import android.graphics.Point;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.location.Location;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
/**
|
||||
* A map overlay that displays a "MyLocation" arrow, an error circle, the
|
||||
* currently recording track and optionally a selected track.
|
||||
*
|
||||
* @author Leif Hendrik Wilden
|
||||
*/
|
||||
public class MyTracksOverlay extends Overlay {
|
||||
|
||||
private final Drawable arrow[] = new Drawable[18];
|
||||
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 selectedTrackPaint;
|
||||
private final Paint errorCirclePaint;
|
||||
private final Context context;
|
||||
private final ArrayList<Waypoint> waypoints = new ArrayList<Waypoint>();
|
||||
|
||||
private Track selectedTrack;
|
||||
private int lastHeading = 0;
|
||||
private Location myLocation;
|
||||
private boolean showEndMarker = true;
|
||||
|
||||
public MyTracksOverlay(Context context) {
|
||||
this.context = context;
|
||||
|
||||
// TODO: Can we use a FrameAnimation or similar here rather
|
||||
// than individual resources for each arrow direction?
|
||||
|
||||
arrow[0] = context.getResources().getDrawable(R.drawable.arrow_0);
|
||||
arrow[1] = context.getResources().getDrawable(R.drawable.arrow_20);
|
||||
arrow[2] = context.getResources().getDrawable(R.drawable.arrow_40);
|
||||
arrow[3] = context.getResources().getDrawable(R.drawable.arrow_60);
|
||||
arrow[4] = context.getResources().getDrawable(R.drawable.arrow_80);
|
||||
arrow[5] = context.getResources().getDrawable(R.drawable.arrow_100);
|
||||
arrow[6] = context.getResources().getDrawable(R.drawable.arrow_120);
|
||||
arrow[7] = context.getResources().getDrawable(R.drawable.arrow_140);
|
||||
arrow[8] = context.getResources().getDrawable(R.drawable.arrow_160);
|
||||
arrow[9] = context.getResources().getDrawable(R.drawable.arrow_180);
|
||||
arrow[10] = context.getResources().getDrawable(R.drawable.arrow_200);
|
||||
arrow[11] = context.getResources().getDrawable(R.drawable.arrow_220);
|
||||
arrow[12] = context.getResources().getDrawable(R.drawable.arrow_240);
|
||||
arrow[13] = context.getResources().getDrawable(R.drawable.arrow_260);
|
||||
arrow[14] = context.getResources().getDrawable(R.drawable.arrow_280);
|
||||
arrow[15] = context.getResources().getDrawable(R.drawable.arrow_300);
|
||||
arrow[16] = context.getResources().getDrawable(R.drawable.arrow_320);
|
||||
arrow[17] = context.getResources().getDrawable(R.drawable.arrow_340);
|
||||
arrowWidth = arrow[lastHeading].getIntrinsicWidth();
|
||||
arrowHeight = arrow[lastHeading].getIntrinsicHeight();
|
||||
for (int i = 0; i <= 17; i++) {
|
||||
arrow[i].setBounds(0, 0, arrowWidth, arrowHeight);
|
||||
}
|
||||
|
||||
statsMarker = context.getResources().getDrawable(R.drawable.ylw_pushpin);
|
||||
markerWidth = statsMarker.getIntrinsicWidth();
|
||||
markerHeight = statsMarker.getIntrinsicHeight();
|
||||
statsMarker.setBounds(0, 0, markerWidth, markerHeight);
|
||||
|
||||
startMarker = context.getResources().getDrawable(R.drawable.green_dot);
|
||||
startMarker.setBounds(0, 0, markerWidth, markerHeight);
|
||||
|
||||
endMarker = context.getResources().getDrawable(R.drawable.red_dot);
|
||||
endMarker.setBounds(0, 0, markerWidth, markerHeight);
|
||||
|
||||
waypointMarker =
|
||||
context.getResources().getDrawable(R.drawable.blue_pushpin);
|
||||
waypointMarker.setBounds(0, 0, markerWidth, markerHeight);
|
||||
|
||||
selectedTrackPaint = new Paint();
|
||||
selectedTrackPaint.setColor(context.getResources().getColor(R.color.red));
|
||||
selectedTrackPaint.setStrokeWidth(3);
|
||||
selectedTrackPaint.setStyle(Paint.Style.STROKE);
|
||||
selectedTrackPaint.setAntiAlias(true);
|
||||
|
||||
errorCirclePaint = new Paint();
|
||||
errorCirclePaint.setColor(context.getResources().getColor(R.color.blue));
|
||||
errorCirclePaint.setStyle(Paint.Style.STROKE);
|
||||
errorCirclePaint.setStrokeWidth(3);
|
||||
errorCirclePaint.setAlpha(127);
|
||||
errorCirclePaint.setAntiAlias(true);
|
||||
}
|
||||
|
||||
public void setSelectedTrack(Track track) {
|
||||
selectedTrack = track;
|
||||
}
|
||||
|
||||
public Track getSelectedTrack() {
|
||||
return selectedTrack;
|
||||
}
|
||||
|
||||
public void addWaypoint(Waypoint wpt) {
|
||||
waypoints.add(wpt);
|
||||
}
|
||||
|
||||
public void clearWaypoints() {
|
||||
waypoints.clear();
|
||||
}
|
||||
|
||||
public void setShowEndMarker(boolean showEndMarker) {
|
||||
this.showEndMarker = showEndMarker;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(Canvas canvas, MapView mapView, boolean shadow) {
|
||||
|
||||
if (shadow) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Draw the selected track:
|
||||
drawTrack(canvas, mapView, selectedTrack, true);
|
||||
|
||||
// Draw the waypoints:
|
||||
ArrayList<Waypoint> currentWaypoints = waypoints;
|
||||
for (int i = 1; i < currentWaypoints.size(); i++) {
|
||||
Location loc = currentWaypoints.get(i).getLocation();
|
||||
if (loc == null) {
|
||||
continue;
|
||||
}
|
||||
GeoPoint geoPoint = MyTracksUtils.getGeoPoint(loc);
|
||||
Point pt = new Point();
|
||||
mapView.getProjection().toPixels(geoPoint, pt);
|
||||
canvas.save();
|
||||
canvas.translate(pt.x - (markerWidth / 2) + 3, pt.y - (markerHeight));
|
||||
if (currentWaypoints.get(i).getType() == Waypoint.TYPE_STATISTICS) {
|
||||
statsMarker.draw(canvas);
|
||||
} else {
|
||||
waypointMarker.draw(canvas);
|
||||
}
|
||||
canvas.restore();
|
||||
}
|
||||
|
||||
// Draw the arrow icon:
|
||||
if (myLocation == null) {
|
||||
return;
|
||||
}
|
||||
GeoPoint geoPoint = new GeoPoint(
|
||||
(int) (myLocation.getLatitude() * 1E6),
|
||||
(int) (myLocation.getLongitude() * 1E6));
|
||||
Point pt = new Point();
|
||||
mapView.getProjection().toPixels(geoPoint, pt);
|
||||
canvas.save();
|
||||
canvas.translate(pt.x - (arrowWidth / 2), pt.y - (arrowHeight / 2));
|
||||
arrow[lastHeading].draw(canvas);
|
||||
canvas.restore();
|
||||
|
||||
// Draw the error circle:
|
||||
float radius =
|
||||
mapView.getProjection().metersToEquatorPixels(myLocation.getAccuracy());
|
||||
canvas.drawCircle(pt.x, pt.y, radius, errorCirclePaint);
|
||||
}
|
||||
|
||||
public void drawTrack(Canvas canvas, MapView mapView, Track track,
|
||||
boolean selected) {
|
||||
|
||||
if (track == null) {
|
||||
return;
|
||||
}
|
||||
ArrayList<Location> points = track.getLocations();
|
||||
if (points.size() < 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the current viewing window:
|
||||
int w = mapView.getLongitudeSpan();
|
||||
int h = mapView.getLatitudeSpan();
|
||||
int cx = mapView.getMapCenter().getLongitudeE6();
|
||||
int cy = mapView.getMapCenter().getLatitudeE6();
|
||||
Rect rect = new Rect(cx - w / 2, cy - h / 2, cx + w / 2, cy + h / 2);
|
||||
|
||||
Point pt = new Point();
|
||||
GeoPoint geoPoint;
|
||||
Location loc;
|
||||
Location lastValidLocation = null;
|
||||
Path path;
|
||||
boolean wasInside;
|
||||
|
||||
// Do as much allocation and preparation outside the loop over track
|
||||
// points:
|
||||
wasInside = false;
|
||||
int locLon = 0, locLat = 0, minLon, maxLon, minLat, maxLat;
|
||||
int lastLocLon = (int) (points.get(0).getLongitude() * 1E6);
|
||||
int lastLocLat = (int) (points.get(0).getLatitude() * 1E6);
|
||||
|
||||
// Line decimation for dummies:
|
||||
// Skip di additional points, where di depends on zoom level:
|
||||
int di = 0;
|
||||
int dl = 17 - mapView.getZoomLevel();
|
||||
if (dl > 0) {
|
||||
di += (dl * dl);
|
||||
}
|
||||
|
||||
// Loop over track points:
|
||||
path = new Path();
|
||||
for (int i = 1; i < points.size(); i++) {
|
||||
|
||||
loc = points.get(i);
|
||||
if (loc == null) {
|
||||
continue;
|
||||
}
|
||||
locLon = (int) (loc.getLongitude() * 1E6);
|
||||
locLat = (int) (loc.getLatitude() * 1E6);
|
||||
|
||||
// Skip to every n-th point (depends on zoom level, see above):
|
||||
for (int j = 0; j < di && i < points.size() - 1; j++) {
|
||||
// TODO Check the thread synchronization.
|
||||
// There is no reason that points.get(i + 1) should be null but users
|
||||
// have reported it causing null pointer exceptions.
|
||||
if ((locLat > 90E6) ||
|
||||
(lastLocLat > 90E6) ||
|
||||
(points.get(i + 1) == null) ||
|
||||
(points.get(i + 1).getLatitude() > 90)) {
|
||||
break;
|
||||
}
|
||||
i++;
|
||||
loc = points.get(i);
|
||||
locLon = (int) (loc.getLongitude() * 1E6);
|
||||
locLat = (int) (loc.getLatitude() * 1E6);
|
||||
}
|
||||
|
||||
// Draw a line segment if it's inside the viewing window:
|
||||
if (locLat < 90E6 && lastLocLat < 90E6) {
|
||||
lastValidLocation = loc;
|
||||
minLon = Math.min(locLon, lastLocLon);
|
||||
maxLon = Math.max(locLon, lastLocLon);
|
||||
minLat = Math.min(locLat, lastLocLat);
|
||||
maxLat = Math.max(locLat, lastLocLat);
|
||||
if (rect.intersects(minLon, minLat, maxLon, maxLat)) {
|
||||
if (!wasInside) {
|
||||
geoPoint = new GeoPoint(lastLocLat, lastLocLon);
|
||||
mapView.getProjection().toPixels(geoPoint, pt);
|
||||
path.moveTo(pt.x, pt.y);
|
||||
}
|
||||
geoPoint = new GeoPoint(locLat, locLon);
|
||||
mapView.getProjection().toPixels(geoPoint, pt);
|
||||
path.lineTo(pt.x, pt.y);
|
||||
wasInside = rect.contains(locLon, locLat);
|
||||
}
|
||||
} else {
|
||||
wasInside = false;
|
||||
}
|
||||
lastLocLon = locLon;
|
||||
lastLocLat = locLat;
|
||||
}
|
||||
canvas.drawPath(path, selectedTrackPaint);
|
||||
|
||||
// Draw the "End" marker:
|
||||
Location currentLastValidLocation = lastValidLocation;
|
||||
if (showEndMarker && currentLastValidLocation != null) {
|
||||
canvas.save();
|
||||
geoPoint = MyTracksUtils.getGeoPoint(currentLastValidLocation);
|
||||
mapView.getProjection().toPixels(geoPoint, pt);
|
||||
canvas.translate(pt.x - (markerWidth / 2), pt.y - markerHeight);
|
||||
endMarker.draw(canvas);
|
||||
canvas.restore();
|
||||
}
|
||||
|
||||
// Draw the "Start" marker:
|
||||
for (int i = 0; i < points.size(); i++) {
|
||||
loc = points.get(i);
|
||||
if (loc.getLatitude() < 90) {
|
||||
locLon = (int) (loc.getLongitude() * 1E6);
|
||||
locLat = (int) (loc.getLatitude() * 1E6);
|
||||
geoPoint = new GeoPoint(locLat, locLon);
|
||||
mapView.getProjection().toPixels(geoPoint, pt);
|
||||
canvas.save();
|
||||
canvas.translate(pt.x - (markerWidth / 2), pt.y - markerHeight);
|
||||
startMarker.draw(canvas);
|
||||
canvas.restore();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean draw(Canvas canvas, MapView mapView, boolean shadow,
|
||||
long when) {
|
||||
draw(canvas, mapView, shadow);
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
return false;
|
||||
}
|
||||
|
||||
final Location tapLocation = MyTracksUtils.getLocation(p);
|
||||
double dmin = Double.MAX_VALUE;
|
||||
Waypoint waypoint = null;
|
||||
for (int i = 0; i < waypoints.size(); i++) {
|
||||
final Location waypointLocation = waypoints.get(i).getLocation();
|
||||
if (waypointLocation == null) {
|
||||
continue;
|
||||
}
|
||||
final double d = waypointLocation.distanceTo(tapLocation);
|
||||
if (d < dmin) {
|
||||
dmin = d;
|
||||
waypoint = waypoints.get(i);
|
||||
}
|
||||
}
|
||||
|
||||
if (waypoint != null
|
||||
&& dmin < 15000000 / Math.pow(2, mapView.getZoomLevel())) {
|
||||
Intent intent = new Intent(context, MyTracksWaypointDetails.class);
|
||||
intent.putExtra("waypointid", waypoint.getId());
|
||||
context.startActivity(intent);
|
||||
return true;
|
||||
}
|
||||
return super.onTap(p, mapView);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
/*
|
||||
* Copyright 2008 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
package com.google.android.apps.mytracks;
|
||||
|
||||
import com.google.android.apps.mytracks.services.SafeStatusAnnouncerTask;
|
||||
import com.google.android.maps.mytracks.R;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.preference.CheckBoxPreference;
|
||||
import android.preference.ListPreference;
|
||||
import android.preference.Preference;
|
||||
import android.preference.Preference.OnPreferenceChangeListener;
|
||||
import android.preference.PreferenceActivity;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.util.Log;
|
||||
|
||||
/**
|
||||
* An activity that let's the user see and edit the settings.
|
||||
*
|
||||
* @author Leif Hendrik Wilden
|
||||
* @author Rodrigo Damazio
|
||||
*/
|
||||
public class MyTracksSettings extends PreferenceActivity {
|
||||
|
||||
public static final String SETTINGS_NAME = "MyTracksSettings";
|
||||
|
||||
public static final String ANNOUNCEMENT_FREQUENCY = "announcementFrequency";
|
||||
public static final String DEFAULT_MAP_PUBLIC = "defaultMapPublic";
|
||||
public static final String MAX_RECORDING_DISTANCE = "maxRecordingDistance";
|
||||
public static final String METRIC_UNITS = "metricUnits";
|
||||
public static final String MIN_RECORDING_DISTANCE = "minRecordingDistance";
|
||||
public static final String MIN_RECORDING_INTERVAL = "minRecordingInterval";
|
||||
public static final String MIN_REQUIRED_ACCURACY = "minRequiredAccuracy";
|
||||
public static final String PICK_EXISTING_MAP = "pickExistingMap";
|
||||
public static final String RECORDING_TRACK = "recordingTrack";
|
||||
public static final String REPORT_SPEED = "reportSpeed";
|
||||
public static final String SELECTED_TRACK = "selectedTrack";
|
||||
public static final String SEND_STATS_AND_POINTS = "sendStatsAndPoints";
|
||||
public static final String SEND_TO_DOCS = "sendToDocs";
|
||||
public static final String SEND_TO_MYMAPS = "sendToMyMaps";
|
||||
public static final String SHARE_URL_ONLY = "shareUrlOnly";
|
||||
public static final String SIGNAL_SAMPLING_FREQUENCY =
|
||||
"signalSamplingFrequency";
|
||||
public static final String SPLIT_FREQUENCY = "splitFrequency";
|
||||
public static final String WRITE_TO_SD_CARD = "writeToSdCard";
|
||||
|
||||
/*
|
||||
* Default values - keep in sync with those in preferences.xml.
|
||||
*/
|
||||
public static final int DEFAULT_ANNOUNCEMENT_FREQUENCY = -1;
|
||||
public static final int DEFAULT_MAX_RECORDING_DISTANCE = 200;
|
||||
public static final int DEFAULT_MIN_RECORDING_DISTANCE = 5;
|
||||
public static final int DEFAULT_MIN_RECORDING_INTERVAL = 0;
|
||||
public static final int DEFAULT_MIN_REQUIRED_ACCURACY = 200;
|
||||
public static final int DEFAULT_SPLIT_FREQUENCY = 0;
|
||||
|
||||
private static boolean mTTSAvailable;
|
||||
|
||||
/* establish whether the tts class is available to us */
|
||||
static {
|
||||
try {
|
||||
SafeStatusAnnouncerTask.checkAvailable();
|
||||
mTTSAvailable = true;
|
||||
} catch (Throwable t) {
|
||||
Log.d(MyTracksConstants.TAG, "TTS not available.", t);
|
||||
mTTSAvailable = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** Called when the activity is first created. */
|
||||
@Override
|
||||
public void onCreate(Bundle icicle) {
|
||||
super.onCreate(icicle);
|
||||
|
||||
// Tell it where to read/write preferences
|
||||
PreferenceManager preferenceManager = getPreferenceManager();
|
||||
preferenceManager.setSharedPreferencesName(SETTINGS_NAME);
|
||||
preferenceManager.setSharedPreferencesMode(0);
|
||||
|
||||
// Load the preferences to be displayed
|
||||
addPreferencesFromResource(R.xml.preferences);
|
||||
|
||||
// Hook up switching of displayed list entries between metric and imperial
|
||||
// units
|
||||
CheckBoxPreference metricUnitsPreference =
|
||||
(CheckBoxPreference) findPreference(METRIC_UNITS);
|
||||
metricUnitsPreference.setOnPreferenceChangeListener(
|
||||
new OnPreferenceChangeListener() {
|
||||
@Override
|
||||
public boolean onPreferenceChange(Preference preference,
|
||||
Object newValue) {
|
||||
boolean isMetric = (Boolean) newValue;
|
||||
updatePreferenceUnits(isMetric);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
updatePreferenceUnits(metricUnitsPreference.isChecked());
|
||||
|
||||
// Disable TTS announcement preference if not available
|
||||
if (!mTTSAvailable) {
|
||||
IntegerListPreference announcementFrequency =
|
||||
(IntegerListPreference) findPreference(ANNOUNCEMENT_FREQUENCY);
|
||||
announcementFrequency.setEnabled(false);
|
||||
announcementFrequency.setValue("-1");
|
||||
announcementFrequency.setSummary(
|
||||
R.string.settings_announcement_not_available_summary);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates all the preferences which give options with distance units to use
|
||||
* the proper unit the user has selected.
|
||||
*
|
||||
* @param isMetric true to use metric units, false to use imperial
|
||||
*/
|
||||
private void updatePreferenceUnits(boolean isMetric) {
|
||||
final ListPreference minRecordingDistance =
|
||||
(ListPreference) findPreference(MIN_RECORDING_DISTANCE);
|
||||
final ListPreference maxRecordingDistance =
|
||||
(ListPreference) findPreference(MAX_RECORDING_DISTANCE);
|
||||
final ListPreference minRequiredAccuracy =
|
||||
(ListPreference) findPreference(MIN_REQUIRED_ACCURACY);
|
||||
final ListPreference splitFrequency =
|
||||
(ListPreference) findPreference(SPLIT_FREQUENCY);
|
||||
|
||||
minRecordingDistance.setEntries(isMetric
|
||||
? R.array.min_recording_distance_options
|
||||
: R.array.min_recording_distance_options_ft);
|
||||
maxRecordingDistance.setEntries(isMetric
|
||||
? R.array.max_recording_distance_options
|
||||
: R.array.max_recording_distance_options_ft);
|
||||
minRequiredAccuracy.setEntries(isMetric
|
||||
? R.array.min_required_accuracy_options
|
||||
: R.array.min_required_accuracy_options_ft);
|
||||
splitFrequency.setEntries(isMetric
|
||||
? R.array.split_frequency_options
|
||||
: R.array.split_frequency_options_ft);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
|
||||
import com.google.android.apps.mytracks.content.Waypoint;
|
||||
import com.google.android.apps.mytracks.content.WaypointsColumns;
|
||||
import com.google.android.maps.mytracks.R;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.ContentValues;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.view.View.OnClickListener;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.AutoCompleteTextView;
|
||||
import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
import android.widget.ImageView;
|
||||
|
||||
/**
|
||||
* Screen in which the user enters details about a waypoint.
|
||||
*
|
||||
* @author Leif Hendrik Wilden
|
||||
*/
|
||||
public class MyTracksWaypointDetails extends Activity
|
||||
implements OnClickListener {
|
||||
|
||||
/**
|
||||
* The id of the way point being edited (taken from bundle, "waypointid")
|
||||
*/
|
||||
private Long waypointId;
|
||||
|
||||
private EditText name;
|
||||
private EditText description;
|
||||
private AutoCompleteTextView category;
|
||||
private View detailsView;
|
||||
private View statsView;
|
||||
|
||||
private StatsUtilities utils;
|
||||
private Waypoint waypoint;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle bundle) {
|
||||
super.onCreate(bundle);
|
||||
setContentView(R.layout.mytracks_waypoint_details);
|
||||
|
||||
utils = new StatsUtilities(this);
|
||||
SharedPreferences preferences =
|
||||
getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0);
|
||||
if (preferences != null) {
|
||||
boolean useMetric =
|
||||
preferences.getBoolean(MyTracksSettings.METRIC_UNITS, true);
|
||||
utils.setMetricUnits(useMetric);
|
||||
|
||||
boolean displaySpeed =
|
||||
preferences.getBoolean(MyTracksSettings.REPORT_SPEED, true);
|
||||
utils.setReportSpeed(displaySpeed);
|
||||
|
||||
utils.updateWaypointUnits();
|
||||
utils.setSpeedLabels();
|
||||
}
|
||||
|
||||
// Required extra when launching this intent:
|
||||
waypointId = getIntent().getLongExtra("waypointid", -1);
|
||||
if (waypointId < 0) {
|
||||
Log.d(MyTracksConstants.TAG,
|
||||
"MyTracksWaypointsDetails intent was launched w/o waypoint id.");
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
// Optional extra that can be used to suppress the cancel button:
|
||||
boolean hasCancelButton =
|
||||
getIntent().getBooleanExtra("hasCancelButton", true);
|
||||
|
||||
name = (EditText) findViewById(R.id.waypointdetails_name);
|
||||
description = (EditText) findViewById(R.id.waypointdetails_description);
|
||||
category =
|
||||
(AutoCompleteTextView) findViewById(R.id.waypointdetails_category);
|
||||
statsView = findViewById(R.id.waypoint_stats);
|
||||
ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(
|
||||
this,
|
||||
R.array.waypoint_types,
|
||||
android.R.layout.simple_dropdown_item_1line);
|
||||
category.setAdapter(adapter);
|
||||
detailsView = findViewById(R.id.waypointdetails_description_layout);
|
||||
|
||||
Button cancel = (Button) findViewById(R.id.waypointdetails_cancel);
|
||||
if (hasCancelButton) {
|
||||
cancel.setOnClickListener(this);
|
||||
cancel.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
cancel.setVisibility(View.GONE);
|
||||
}
|
||||
Button save = (Button) findViewById(R.id.waypointdetails_save);
|
||||
save.setOnClickListener(this);
|
||||
|
||||
fillDialog();
|
||||
}
|
||||
|
||||
private void fillDialog() {
|
||||
waypoint = MyTracksProviderUtils.Factory.get(this).getWaypoint(waypointId);
|
||||
if (waypoint != null) {
|
||||
name.setText(waypoint.getName());
|
||||
ImageView icon = (ImageView) findViewById(R.id.waypointdetails_icon);
|
||||
int iconId = -1;
|
||||
switch(waypoint.getType()) {
|
||||
case Waypoint.TYPE_WAYPOINT:
|
||||
description.setText(waypoint.getDescription());
|
||||
detailsView.setVisibility(View.VISIBLE);
|
||||
category.setText(waypoint.getCategory());
|
||||
statsView.setVisibility(View.GONE);
|
||||
iconId = R.drawable.blue_pushpin;
|
||||
break;
|
||||
case Waypoint.TYPE_STATISTICS:
|
||||
detailsView.setVisibility(View.GONE);
|
||||
statsView.setVisibility(View.VISIBLE);
|
||||
iconId = R.drawable.ylw_pushpin;
|
||||
utils.setAllStats(waypoint);
|
||||
utils.setTime(R.id.total_time_register, waypoint.getTotalTime());
|
||||
utils.setAltitude(
|
||||
R.id.elevation_register, waypoint.getLocation().getAltitude());
|
||||
break;
|
||||
}
|
||||
icon.setImageDrawable(getResources().getDrawable(iconId));
|
||||
}
|
||||
}
|
||||
|
||||
private void saveDialog() {
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(WaypointsColumns.NAME, name.getText().toString());
|
||||
if (waypoint != null && waypoint.getType() == Waypoint.TYPE_WAYPOINT) {
|
||||
values.put(WaypointsColumns.DESCRIPTION,
|
||||
description.getText().toString());
|
||||
values.put(WaypointsColumns.CATEGORY, category.getText().toString());
|
||||
}
|
||||
getContentResolver().update(
|
||||
WaypointsColumns.CONTENT_URI,
|
||||
values,
|
||||
"_id = " + waypointId,
|
||||
null /*selectionArgs*/);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
switch (v.getId()) {
|
||||
case R.id.waypointdetails_cancel:
|
||||
finish();
|
||||
break;
|
||||
case R.id.waypointdetails_save:
|
||||
saveDialog();
|
||||
finish();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,261 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
|
||||
import com.google.android.apps.mytracks.content.Waypoint;
|
||||
import com.google.android.apps.mytracks.content.WaypointsColumns;
|
||||
import com.google.android.apps.mytracks.util.StringUtils;
|
||||
import com.google.android.maps.mytracks.R;
|
||||
|
||||
import android.app.AlertDialog;
|
||||
import android.app.ListActivity;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.database.Cursor;
|
||||
import android.os.Bundle;
|
||||
import android.view.ContextMenu;
|
||||
import android.view.ContextMenu.ContextMenuInfo;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.View.OnCreateContextMenuListener;
|
||||
import android.view.Window;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.Button;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.ListView;
|
||||
import android.widget.SimpleCursorAdapter;
|
||||
import android.widget.TextView;
|
||||
|
||||
/**
|
||||
* Activity which shows the list of waypoints in a track.
|
||||
*
|
||||
* @author Leif Hendrik Wilden
|
||||
*/
|
||||
public class MyTracksWaypointsList extends ListActivity
|
||||
implements View.OnClickListener {
|
||||
|
||||
private int contextPosition = -1;
|
||||
private long trackId = -1;
|
||||
private long waypointId = -1;
|
||||
private ListView listView = null;
|
||||
private Button insertWaypointButton = null;
|
||||
private Button insertStatisticsButton = null;
|
||||
private long recordingTrackId = -1;
|
||||
private long selectedTrackId = -1;
|
||||
private MyTracksProviderUtils providerUtils;
|
||||
|
||||
private Cursor waypointsCursor = null;
|
||||
|
||||
private final OnCreateContextMenuListener contextMenuListener =
|
||||
new OnCreateContextMenuListener() {
|
||||
public void onCreateContextMenu(ContextMenu menu, View v,
|
||||
ContextMenuInfo menuInfo) {
|
||||
menu.setHeaderTitle(R.string.waypointslist_this_waypoint);
|
||||
AdapterView.AdapterContextMenuInfo info =
|
||||
(AdapterView.AdapterContextMenuInfo) menuInfo;
|
||||
contextPosition = info.position;
|
||||
waypointId = MyTracksWaypointsList.this.listView.getAdapter()
|
||||
.getItemId(contextPosition);
|
||||
int type = providerUtils.getWaypoint(info.id).getType();
|
||||
menu.add(0, MyTracksConstants.MENU_SHOW, 0,
|
||||
R.string.waypointslist_show_waypoint);
|
||||
menu.add(0, MyTracksConstants.MENU_EDIT, 0,
|
||||
R.string.waypointslist_edit_waypoint);
|
||||
menu.add(0, MyTracksConstants.MENU_DELETE, 0,
|
||||
R.string.waypointslist_delete_waypoint).setEnabled(
|
||||
recordingTrackId < 0 || type == Waypoint.TYPE_WAYPOINT ||
|
||||
info.id != providerUtils.getLastWaypointId(recordingTrackId));
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
protected void onListItemClick(ListView l, View v, int position, long id) {
|
||||
Intent result = new Intent();
|
||||
result.putExtra("trackid", trackId);
|
||||
result.putExtra("waypointid", id);
|
||||
setResult(MyTracksConstants.EDIT_WAYPOINT, result);
|
||||
finish();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onMenuItemSelected(int featureId, MenuItem item) {
|
||||
if (!super.onMenuItemSelected(featureId, item)) {
|
||||
switch (item.getItemId()) {
|
||||
case MyTracksConstants.MENU_SHOW: {
|
||||
onListItemClick(null, null, 0, waypointId);
|
||||
return true;
|
||||
}
|
||||
case MyTracksConstants.MENU_EDIT: {
|
||||
Intent intent = new Intent(this, MyTracksWaypointDetails.class);
|
||||
intent.putExtra("trackid", trackId);
|
||||
intent.putExtra("waypointid", waypointId);
|
||||
startActivity(intent);
|
||||
return true;
|
||||
}
|
||||
case MyTracksConstants.MENU_DELETE: {
|
||||
deleteWaypoint(waypointId);
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
providerUtils = MyTracksProviderUtils.Factory.get(this);
|
||||
|
||||
// We don't need a window title bar:
|
||||
requestWindowFeature(Window.FEATURE_NO_TITLE);
|
||||
|
||||
setContentView(R.layout.mytracks_waypoints_list);
|
||||
|
||||
listView = getListView();
|
||||
listView.setOnCreateContextMenuListener(contextMenuListener);
|
||||
|
||||
insertWaypointButton =
|
||||
(Button) findViewById(R.id.waypointslist_btn_insert_waypoint);
|
||||
insertWaypointButton.setOnClickListener(this);
|
||||
insertStatisticsButton =
|
||||
(Button) findViewById(R.id.waypointslist_btn_insert_statistics);
|
||||
insertStatisticsButton.setOnClickListener(this);
|
||||
SharedPreferences preferences =
|
||||
getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0);
|
||||
if (preferences != null) {
|
||||
recordingTrackId =
|
||||
preferences.getLong(MyTracksSettings.RECORDING_TRACK, -1);
|
||||
selectedTrackId =
|
||||
preferences.getLong(MyTracksSettings.SELECTED_TRACK, -1);
|
||||
}
|
||||
boolean selectedRecording = selectedTrackId > 0
|
||||
&& selectedTrackId == recordingTrackId;
|
||||
insertWaypointButton.setEnabled(selectedRecording);
|
||||
insertStatisticsButton.setEnabled(selectedRecording);
|
||||
|
||||
if (getIntent() != null && getIntent().getExtras() != null) {
|
||||
trackId = getIntent().getExtras().getLong("trackid", -1);
|
||||
} else {
|
||||
trackId = -1;
|
||||
}
|
||||
|
||||
final long firstWaypointId = providerUtils.getFirstWaypointId(trackId);
|
||||
waypointsCursor = getContentResolver().query(
|
||||
WaypointsColumns.CONTENT_URI, null,
|
||||
WaypointsColumns.TRACKID + "=" + trackId + " AND "
|
||||
+ WaypointsColumns._ID + "!=" + firstWaypointId, null, null);
|
||||
startManagingCursor(waypointsCursor);
|
||||
setListAdapter();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
switch (v.getId()) {
|
||||
case R.id.waypointslist_btn_insert_waypoint: {
|
||||
long id = MyTracks.getInstance().insertWaypointMarker();
|
||||
Intent intent = new Intent(this, MyTracksWaypointDetails.class);
|
||||
intent.putExtra("waypointid", id);
|
||||
startActivity(intent);
|
||||
break;
|
||||
}
|
||||
case R.id.waypointslist_btn_insert_statistics: {
|
||||
long id = MyTracks.getInstance().insertStatisticsMarker();
|
||||
Intent intent = new Intent(this, MyTracksWaypointDetails.class);
|
||||
intent.putExtra("waypointid", id);
|
||||
startActivity(intent);
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private void setListAdapter() {
|
||||
// Get a cursor with all tracks
|
||||
SimpleCursorAdapter adapter = new SimpleCursorAdapter(
|
||||
this,
|
||||
R.layout.mytracks_marker_item,
|
||||
waypointsCursor,
|
||||
new String[] { WaypointsColumns.NAME, WaypointsColumns.TIME,
|
||||
WaypointsColumns.CATEGORY, WaypointsColumns.TYPE },
|
||||
new int[] { R.id.waypointslist_item_name,
|
||||
R.id.waypointslist_item_time,
|
||||
R.id.waypointslist_item_category,
|
||||
R.id.waypointslist_item_icon });
|
||||
|
||||
final int timeIdx =
|
||||
waypointsCursor.getColumnIndexOrThrow(WaypointsColumns.TIME);
|
||||
final int typeIdx =
|
||||
waypointsCursor.getColumnIndexOrThrow(WaypointsColumns.TYPE);
|
||||
adapter.setViewBinder(new SimpleCursorAdapter.ViewBinder() {
|
||||
@Override
|
||||
public boolean setViewValue(View view, Cursor cursor, int columnIndex) {
|
||||
if (columnIndex == timeIdx) {
|
||||
long time = cursor.getLong(timeIdx);
|
||||
TextView textView = (TextView) view;
|
||||
textView.setText(String.format("%tc", time));
|
||||
textView.setVisibility(
|
||||
textView.getText().length() < 1 ? View.GONE : View.VISIBLE);
|
||||
} else if (columnIndex == typeIdx) {
|
||||
int type = cursor.getInt(typeIdx);
|
||||
ImageView imageView = (ImageView) view;
|
||||
imageView.setImageDrawable(getResources().getDrawable(
|
||||
type == Waypoint.TYPE_STATISTICS
|
||||
? R.drawable.ylw_pushpin
|
||||
: R.drawable.blue_pushpin));
|
||||
} else {
|
||||
TextView textView = (TextView) view;
|
||||
textView.setText(cursor.getString(columnIndex));
|
||||
textView.setVisibility(
|
||||
textView.getText().length() < 1 ? View.GONE : View.VISIBLE);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
});
|
||||
setListAdapter(adapter);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the way point with the given id.
|
||||
* Prompts the user if he want to really delete it.
|
||||
*/
|
||||
public void deleteWaypoint(final long waypointId) {
|
||||
AlertDialog dialog = null;
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(this);
|
||||
builder.setMessage(getString(R.string.marker_will_be_permanently_deleted));
|
||||
builder.setTitle(getString(R.string.are_you_sure_question));
|
||||
builder.setIcon(android.R.drawable.ic_dialog_alert);
|
||||
builder.setPositiveButton(getString(R.string.yes),
|
||||
new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int i) {
|
||||
dialog.dismiss();
|
||||
providerUtils.deleteWaypoint(waypointId,
|
||||
new StringUtils(MyTracksWaypointsList.this));
|
||||
}
|
||||
});
|
||||
builder.setNegativeButton(getString(R.string.no),
|
||||
new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int i) {
|
||||
dialog.dismiss();
|
||||
}
|
||||
});
|
||||
dialog = builder.create();
|
||||
dialog.show();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,220 @@
|
||||
/*
|
||||
* Copyright 2008 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
package com.google.android.apps.mytracks;
|
||||
|
||||
import com.google.android.maps.mytracks.R;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Handler;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.animation.AlphaAnimation;
|
||||
import android.view.animation.Animation;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.RelativeLayout;
|
||||
import android.widget.RelativeLayout.LayoutParams;
|
||||
|
||||
/**
|
||||
* Creates previous and next arrows for a given activity.
|
||||
*
|
||||
* @author Leif Hendrik Wilden
|
||||
*/
|
||||
public class NavControls {
|
||||
|
||||
private static final int KEEP_VISIBLE_MILLIS = 4000;
|
||||
private static final boolean FADE_CONTROLS = true;
|
||||
|
||||
private static final Animation SHOW_NEXT_ANIMATION =
|
||||
new AlphaAnimation(0F, 1F);
|
||||
private static final Animation HIDE_NEXT_ANIMATION =
|
||||
new AlphaAnimation(1F, 0F);
|
||||
private static final Animation SHOW_PREV_ANIMATION =
|
||||
new AlphaAnimation(0F, 1F);
|
||||
private static final Animation HIDE_PREV_ANIMATION =
|
||||
new AlphaAnimation(1F, 0F);
|
||||
|
||||
/**
|
||||
* A touchable image view.
|
||||
*/
|
||||
public class TouchLayout extends RelativeLayout {
|
||||
private final ImageView arrow;
|
||||
private final ImageView icon;
|
||||
|
||||
public TouchLayout(Context context, boolean isLeft) {
|
||||
super(context);
|
||||
arrow = new ImageView(context);
|
||||
arrow.setImageDrawable(context.getResources().getDrawable(
|
||||
isLeft ? R.drawable.btn_arrow_left : R.drawable.btn_arrow_right));
|
||||
icon = new ImageView(context);
|
||||
icon.setVisibility(View.GONE);
|
||||
addView(arrow);
|
||||
addView(icon);
|
||||
icon.setPadding((isLeft ? 15 : 10), 27, 15, 0);
|
||||
}
|
||||
|
||||
public void setIcon(Drawable drawable) {
|
||||
icon.setImageDrawable(drawable);
|
||||
icon.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onTouchEvent(MotionEvent event) {
|
||||
switch (event.getAction()) {
|
||||
case MotionEvent.ACTION_DOWN:
|
||||
setPressed(true);
|
||||
hide();
|
||||
if (this == prevImage) {
|
||||
if (prevRunnable != null) {
|
||||
handler.post(prevRunnable);
|
||||
return true;
|
||||
}
|
||||
} else if (this == nextImage) {
|
||||
if (nextRunnable != null) {
|
||||
handler.post(nextRunnable);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case MotionEvent.ACTION_UP:
|
||||
setPressed(false);
|
||||
break;
|
||||
}
|
||||
return super.onTouchEvent(event);
|
||||
}
|
||||
}
|
||||
|
||||
private final Context context;
|
||||
private final Runnable prevRunnable;
|
||||
private final Runnable nextRunnable;
|
||||
private final Handler handler = new Handler();
|
||||
private final Runnable dismissControls = new Runnable() {
|
||||
public void run() {
|
||||
hide();
|
||||
}
|
||||
};
|
||||
private final TouchLayout prevImage;
|
||||
private final TouchLayout nextImage;
|
||||
|
||||
private boolean isVisible = false;
|
||||
private boolean hasNext = true;
|
||||
private boolean hasPrev = true;
|
||||
|
||||
public NavControls(Context context, ViewGroup container,
|
||||
Runnable prevRunnable, Runnable nextRunnable) {
|
||||
this.context = context;
|
||||
this.prevRunnable = prevRunnable;
|
||||
this.nextRunnable = nextRunnable;
|
||||
LayoutParams prevParams = new LayoutParams(
|
||||
LayoutParams.WRAP_CONTENT,
|
||||
LayoutParams.WRAP_CONTENT);
|
||||
prevParams.addRule(RelativeLayout.ALIGN_PARENT_LEFT);
|
||||
prevParams.addRule(RelativeLayout.CENTER_VERTICAL);
|
||||
prevImage = new TouchLayout(context, true);
|
||||
prevImage.setLayoutParams(prevParams);
|
||||
prevImage.setVisibility(View.INVISIBLE);
|
||||
container.addView(prevImage);
|
||||
LayoutParams nextParams = new LayoutParams(
|
||||
LayoutParams.WRAP_CONTENT,
|
||||
LayoutParams.WRAP_CONTENT);
|
||||
nextParams.addRule(RelativeLayout.ALIGN_PARENT_RIGHT);
|
||||
nextParams.addRule(RelativeLayout.CENTER_VERTICAL);
|
||||
nextImage = new TouchLayout(context, false);
|
||||
nextImage.setLayoutParams(nextParams);
|
||||
nextImage.setVisibility(View.INVISIBLE);
|
||||
container.addView(nextImage);
|
||||
}
|
||||
|
||||
public void setHasNext(boolean hasNext) {
|
||||
this.hasNext = hasNext;
|
||||
}
|
||||
|
||||
public void setHasPrev(boolean hasPrev) {
|
||||
this.hasPrev = hasPrev;
|
||||
}
|
||||
|
||||
private void keepVisible() {
|
||||
if (isVisible && FADE_CONTROLS) {
|
||||
handler.removeCallbacks(dismissControls);
|
||||
handler.postDelayed(dismissControls, KEEP_VISIBLE_MILLIS);
|
||||
}
|
||||
}
|
||||
|
||||
public void show() {
|
||||
if (!isVisible) {
|
||||
if (prevRunnable != null && hasPrev) {
|
||||
SHOW_PREV_ANIMATION.setDuration(500);
|
||||
SHOW_PREV_ANIMATION.startNow();
|
||||
prevImage.setPressed(false);
|
||||
prevImage.setAnimation(SHOW_PREV_ANIMATION);
|
||||
prevImage.setVisibility(View.VISIBLE);
|
||||
}
|
||||
if (nextRunnable != null && hasNext) {
|
||||
SHOW_NEXT_ANIMATION.setDuration(500);
|
||||
SHOW_NEXT_ANIMATION.startNow();
|
||||
nextImage.setPressed(false);
|
||||
nextImage.setAnimation(SHOW_NEXT_ANIMATION);
|
||||
nextImage.setVisibility(View.VISIBLE);
|
||||
}
|
||||
isVisible = true;
|
||||
keepVisible();
|
||||
} else {
|
||||
keepVisible();
|
||||
}
|
||||
}
|
||||
|
||||
public void hideNow() {
|
||||
handler.removeCallbacks(dismissControls);
|
||||
isVisible = false;
|
||||
prevImage.clearAnimation();
|
||||
prevImage.setVisibility(View.INVISIBLE);
|
||||
nextImage.clearAnimation();
|
||||
nextImage.setVisibility(View.INVISIBLE);
|
||||
}
|
||||
|
||||
public void hide() {
|
||||
isVisible = false;
|
||||
if (prevRunnable != null) {
|
||||
if (hasPrev) {
|
||||
prevImage.setAnimation(HIDE_PREV_ANIMATION);
|
||||
HIDE_PREV_ANIMATION.setDuration(500);
|
||||
HIDE_PREV_ANIMATION.startNow();
|
||||
} else {
|
||||
prevImage.clearAnimation();
|
||||
}
|
||||
prevImage.setVisibility(View.INVISIBLE);
|
||||
}
|
||||
if (nextRunnable != null) {
|
||||
if (hasNext) {
|
||||
nextImage.setAnimation(HIDE_NEXT_ANIMATION);
|
||||
HIDE_NEXT_ANIMATION.setDuration(500);
|
||||
HIDE_NEXT_ANIMATION.startNow();
|
||||
} else {
|
||||
nextImage.clearAnimation();
|
||||
}
|
||||
nextImage.setVisibility(View.INVISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
public void setLeftIcon(int resourceId) {
|
||||
prevImage.setIcon(context.getResources().getDrawable(resourceId));
|
||||
}
|
||||
|
||||
public void setRightIcon(int resourceId) {
|
||||
nextImage.setIcon(context.getResources().getDrawable(resourceId));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* Copyright 2008 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
package com.google.android.apps.mytracks;
|
||||
|
||||
/**
|
||||
* Interface that allows to set a progress dialog message and progress value
|
||||
* in percent.
|
||||
*
|
||||
* @author Leif Hendrik Wilden
|
||||
*/
|
||||
public interface ProgressIndicator {
|
||||
|
||||
public void setProgressMessage(String text);
|
||||
|
||||
public void setProgressValue(int percent);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
/*
|
||||
* Copyright 2008 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
package com.google.android.apps.mytracks;
|
||||
|
||||
import com.google.android.maps.mytracks.R;
|
||||
|
||||
import android.app.Dialog;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.SharedPreferences.Editor;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import android.view.Window;
|
||||
import android.widget.Button;
|
||||
import android.widget.CheckBox;
|
||||
import android.widget.CompoundButton;
|
||||
import android.widget.CompoundButton.OnCheckedChangeListener;
|
||||
import android.widget.RadioButton;
|
||||
import android.widget.RadioGroup;
|
||||
|
||||
/**
|
||||
* A dialog where the user can choose where to send the tracks to, i.e.
|
||||
* to Google My Maps, Google Docs, etc.
|
||||
*
|
||||
* @author Leif Hendrik Wilden
|
||||
*/
|
||||
public class SendToGoogleDialog extends Dialog {
|
||||
|
||||
private RadioGroup groupMyMaps;
|
||||
private RadioButton createNewMapRadioButton;
|
||||
private RadioButton pickMapRadioButton;
|
||||
private CheckBox sendToMyMapsCheckBox;
|
||||
private CheckBox sendToDocsCheckBox;
|
||||
private RadioButton sendStatsRadioButton;
|
||||
private RadioButton sendStatsAndPointsRadioButton;
|
||||
private Button sendButton;
|
||||
|
||||
public SendToGoogleDialog(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
requestWindowFeature(Window.FEATURE_NO_TITLE);
|
||||
setContentView(R.layout.mytracks_send_to_google);
|
||||
|
||||
Button cancel = (Button) findViewById(R.id.sendtogoogle_cancel);
|
||||
cancel.setOnClickListener(new View.OnClickListener() {
|
||||
public void onClick(View v) {
|
||||
MyTracks.getInstance().dismissDialog(MyTracks.DIALOG_SEND_TO_GOOGLE);
|
||||
}
|
||||
});
|
||||
|
||||
Button send = (Button) findViewById(R.id.sendtogoogle_send_now);
|
||||
send.setOnClickListener(new View.OnClickListener() {
|
||||
public void onClick(View v) {
|
||||
dismiss();
|
||||
MyTracks.getInstance().sendToGoogle();
|
||||
}
|
||||
});
|
||||
|
||||
OnCheckedChangeListener checkBoxListener = new OnCheckedChangeListener() {
|
||||
public void onCheckedChanged(CompoundButton button, boolean checked) {
|
||||
sendButton.setEnabled(
|
||||
sendToMyMapsCheckBox.isChecked() ||
|
||||
sendToDocsCheckBox.isChecked());
|
||||
groupMyMaps.setVisibility(
|
||||
sendToMyMapsCheckBox.isChecked() ? View.VISIBLE : View.INVISIBLE);
|
||||
//groupDocs.setVisibility(
|
||||
// sendToDocsCheckBox.isChecked() ? View.VISIBLE : View.INVISIBLE);
|
||||
}
|
||||
};
|
||||
|
||||
sendButton = (Button) findViewById(R.id.sendtogoogle_send_now);
|
||||
groupMyMaps = (RadioGroup) findViewById(R.id.sendtogoogle_group_mymaps);
|
||||
sendToMyMapsCheckBox =
|
||||
(CheckBox) findViewById(R.id.sendtogoogle_google_mymaps);
|
||||
sendToMyMapsCheckBox.setOnCheckedChangeListener(checkBoxListener);
|
||||
sendToDocsCheckBox = (CheckBox) findViewById(R.id.sendtogoogle_google_docs);
|
||||
sendToDocsCheckBox.setOnCheckedChangeListener(checkBoxListener);
|
||||
createNewMapRadioButton =
|
||||
(RadioButton) findViewById(R.id.sendtogoogle_create_new_map);
|
||||
pickMapRadioButton =
|
||||
(RadioButton) findViewById(R.id.sendtogoogle_pick_existing_map);
|
||||
sendStatsRadioButton =
|
||||
(RadioButton) findViewById(R.id.sendtogoogle_send_stats);
|
||||
sendStatsAndPointsRadioButton = (RadioButton) findViewById(
|
||||
R.id.sendtogoogle_send_stats_and_points);
|
||||
|
||||
SharedPreferences prefs =
|
||||
getContext().getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0);
|
||||
if (prefs != null) {
|
||||
sendToMyMapsCheckBox.setChecked(
|
||||
prefs.getBoolean(MyTracksSettings.SEND_TO_MYMAPS, true));
|
||||
sendToDocsCheckBox.setChecked(
|
||||
prefs.getBoolean(MyTracksSettings.SEND_TO_DOCS, true));
|
||||
pickMapRadioButton.setChecked(
|
||||
prefs.getBoolean(MyTracksSettings.PICK_EXISTING_MAP, false));
|
||||
createNewMapRadioButton.setChecked(
|
||||
!prefs.getBoolean(MyTracksSettings.PICK_EXISTING_MAP, false));
|
||||
sendStatsAndPointsRadioButton.setChecked(
|
||||
prefs.getBoolean(MyTracksSettings.SEND_STATS_AND_POINTS, false));
|
||||
sendStatsRadioButton.setChecked(
|
||||
!prefs.getBoolean(MyTracksSettings.SEND_STATS_AND_POINTS, false));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStop() {
|
||||
SharedPreferences prefs =
|
||||
getContext().getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0);
|
||||
if (prefs != null) {
|
||||
Editor editor = prefs.edit();
|
||||
if (editor != null) {
|
||||
editor.putBoolean(
|
||||
MyTracksSettings.SEND_TO_MYMAPS, sendToMyMapsCheckBox.isChecked());
|
||||
editor.putBoolean(
|
||||
MyTracksSettings.SEND_TO_DOCS, sendToDocsCheckBox.isChecked());
|
||||
editor.putBoolean(
|
||||
MyTracksSettings.PICK_EXISTING_MAP, pickMapRadioButton.isChecked());
|
||||
editor.putBoolean(
|
||||
MyTracksSettings.SEND_STATS_AND_POINTS,
|
||||
sendStatsAndPointsRadioButton.isChecked());
|
||||
editor.commit();
|
||||
}
|
||||
}
|
||||
super.onStop();
|
||||
}
|
||||
|
||||
public boolean getSendToMyMaps() {
|
||||
return sendToMyMapsCheckBox.isChecked();
|
||||
}
|
||||
|
||||
public boolean getSendToDocs() {
|
||||
return sendToDocsCheckBox.isChecked();
|
||||
}
|
||||
|
||||
public boolean getCreateNewMap() {
|
||||
return createNewMapRadioButton.isChecked();
|
||||
}
|
||||
|
||||
public boolean getSendStatsAndPoints() {
|
||||
return sendStatsAndPointsRadioButton.isChecked();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,474 @@
|
||||
/*
|
||||
* Copyright 2008 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
package com.google.android.apps.mytracks;
|
||||
|
||||
import com.google.android.apps.mytracks.content.TracksColumns;
|
||||
import com.google.android.apps.mytracks.content.WaypointsColumns;
|
||||
import com.google.android.maps.mytracks.R;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
|
||||
import android.database.ContentObserver;
|
||||
import android.database.Cursor;
|
||||
import android.location.Location;
|
||||
import android.location.LocationListener;
|
||||
import android.location.LocationManager;
|
||||
import android.location.LocationProvider;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.util.Log;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.Window;
|
||||
import android.widget.ScrollView;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
/**
|
||||
* An activity that displays track statistics to the user.
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
*/
|
||||
public class StatsActivity extends Activity
|
||||
implements OnSharedPreferenceChangeListener, LocationListener {
|
||||
|
||||
/**
|
||||
* Columns we need from the provider
|
||||
*/
|
||||
private static final String[] PROJECTION_COLUMNS = {
|
||||
TracksColumns._ID,
|
||||
TracksColumns.STARTTIME,
|
||||
TracksColumns.TOTALTIME,
|
||||
TracksColumns.MOVINGTIME,
|
||||
TracksColumns.TOTALDISTANCE,
|
||||
TracksColumns.AVGSPEED,
|
||||
TracksColumns.AVGMOVINGSPEED,
|
||||
TracksColumns.MAXSPEED,
|
||||
TracksColumns.MINELEVATION,
|
||||
TracksColumns.MAXELEVATION,
|
||||
TracksColumns.ELEVATIONGAIN,
|
||||
TracksColumns.MINGRADE,
|
||||
TracksColumns.MAXGRADE };
|
||||
|
||||
private final StatsUtilities utils;
|
||||
private UIUpdateThread thread;
|
||||
|
||||
private ContentObserver observer;
|
||||
private LocationManager locationManager;
|
||||
|
||||
/**
|
||||
* The id of the currently selected track.
|
||||
*/
|
||||
private long selectedTrackId = -1;
|
||||
|
||||
/**
|
||||
* The id of the currently recording track.
|
||||
*/
|
||||
private long recordingTrackId = -1;
|
||||
|
||||
/**
|
||||
* The start time of the selected track.
|
||||
*/
|
||||
private long startTime = -1;
|
||||
|
||||
/**
|
||||
* True if distances should be displayed in metric units (from shared
|
||||
* preferences).
|
||||
*/
|
||||
private boolean metricUnits = true;
|
||||
|
||||
/**
|
||||
* True if pace should be displayed as dist/time (from shared preferences).
|
||||
*/
|
||||
private boolean displaySpeed = true;
|
||||
|
||||
/**
|
||||
* true if activity has resumed and is on top
|
||||
*/
|
||||
private boolean activityOnTop = false;
|
||||
|
||||
/**
|
||||
* If true, the statistics for the current segment are shown, otherwise
|
||||
* for the full track.
|
||||
*/
|
||||
private boolean showCurrentSegment = false;
|
||||
|
||||
/**
|
||||
* A runnable for posting to the UI thread. Will update the total time field.
|
||||
*/
|
||||
private final Runnable updateResults = new Runnable() {
|
||||
public void run() {
|
||||
updateTotalTime();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* A thread that updates the total time field every second.
|
||||
*/
|
||||
private class UIUpdateThread extends Thread {
|
||||
|
||||
public UIUpdateThread() {
|
||||
super();
|
||||
Log.i(MyTracksConstants.TAG, "Created UI update thread");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
Log.i(MyTracksConstants.TAG, "Started UI update thread");
|
||||
while (MyTracks.getInstance().isRecording()) {
|
||||
long sleeptime = 1000;
|
||||
runOnUiThread(updateResults);
|
||||
try {
|
||||
Thread.sleep(sleeptime);
|
||||
} catch (InterruptedException e) {
|
||||
Log.w(MyTracksConstants.TAG,
|
||||
"StatsActivity: Caught exception on sleep.", e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
Log.w(MyTracksConstants.TAG, "UIUpdateThread finished.");
|
||||
}
|
||||
}
|
||||
|
||||
public StatsActivity() {
|
||||
this.utils = new StatsUtilities(this);
|
||||
}
|
||||
|
||||
/** Called when the activity is first created. */
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
// We don't need a window title bar:
|
||||
requestWindowFeature(Window.FEATURE_NO_TITLE);
|
||||
|
||||
setContentView(R.layout.stats);
|
||||
|
||||
Handler contentHandler = new Handler();
|
||||
observer = new ContentObserver(contentHandler) {
|
||||
@Override
|
||||
public void onChange(boolean selfChange) {
|
||||
Log.d(MyTracksConstants.TAG, "StatsActivity: ContentObserver.onChange");
|
||||
restoreStats();
|
||||
super.onChange(selfChange);
|
||||
}
|
||||
};
|
||||
|
||||
ScrollView sv = ((ScrollView) findViewById(R.id.scrolly));
|
||||
sv.setScrollBarStyle(ScrollView.SCROLLBARS_OUTSIDE_INSET);
|
||||
|
||||
SharedPreferences preferences =
|
||||
getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0);
|
||||
if (preferences != null) {
|
||||
selectedTrackId = preferences.getLong(MyTracksSettings.SELECTED_TRACK,
|
||||
-1);
|
||||
recordingTrackId = preferences.getLong(MyTracksSettings.RECORDING_TRACK,
|
||||
-1);
|
||||
metricUnits = preferences.getBoolean(MyTracksSettings.METRIC_UNITS,
|
||||
true);
|
||||
displaySpeed =
|
||||
preferences.getBoolean(MyTracksSettings.REPORT_SPEED, true);
|
||||
checkLiveTrack();
|
||||
restoreStats();
|
||||
updateLocation(null);
|
||||
preferences.registerOnSharedPreferenceChangeListener(this);
|
||||
}
|
||||
locationManager =
|
||||
(LocationManager) getSystemService(Context.LOCATION_SERVICE);
|
||||
utils.setMetricUnits(metricUnits);
|
||||
utils.setReportSpeed(displaySpeed);
|
||||
utils.updateUnits();
|
||||
utils.setSpeedLabel(R.id.speed_label, R.string.speed, R.string.pace_label);
|
||||
utils.setSpeedLabels();
|
||||
|
||||
DisplayMetrics metrics = new DisplayMetrics();
|
||||
getWindowManager().getDefaultDisplay().getMetrics(metrics);
|
||||
if (metrics.heightPixels > 600) {
|
||||
((TextView) findViewById(R.id.speed_register)).setTextSize(80.0f);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPause() {
|
||||
unregisterLocationListener();
|
||||
if (thread != null) {
|
||||
thread.interrupt();
|
||||
thread = null;
|
||||
}
|
||||
getContentResolver().unregisterContentObserver(observer);
|
||||
activityOnTop = false;
|
||||
super.onPause();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
activityOnTop = true;
|
||||
checkLiveTrack();
|
||||
restoreStats();
|
||||
updateLocation(null);
|
||||
super.onResume();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSharedPreferenceChanged(
|
||||
SharedPreferences sharedPreferences, String key) {
|
||||
Log.d(MyTracksConstants.TAG,
|
||||
"StatsActivity: onSharedPreferences changed " + key);
|
||||
if (key != null) {
|
||||
if (key.equals(MyTracksSettings.SELECTED_TRACK)) {
|
||||
selectedTrackId =
|
||||
sharedPreferences.getLong(MyTracksSettings.SELECTED_TRACK, -1);
|
||||
checkLiveTrack();
|
||||
restoreStats();
|
||||
updateLocation(null);
|
||||
} else if (key.equals(MyTracksSettings.RECORDING_TRACK)) {
|
||||
recordingTrackId =
|
||||
sharedPreferences.getLong(MyTracksSettings.RECORDING_TRACK, -1);
|
||||
checkLiveTrack();
|
||||
restoreStats();
|
||||
updateLocation(null);
|
||||
} else if (key.equals(MyTracksSettings.METRIC_UNITS)) {
|
||||
metricUnits =
|
||||
sharedPreferences.getBoolean(MyTracksSettings.METRIC_UNITS, true);
|
||||
utils.setMetricUnits(metricUnits);
|
||||
utils.updateUnits();
|
||||
restoreStats();
|
||||
} else if (key.equals(MyTracksSettings.REPORT_SPEED)) {
|
||||
displaySpeed =
|
||||
sharedPreferences.getBoolean(MyTracksSettings.REPORT_SPEED, true);
|
||||
utils.setReportSpeed(displaySpeed);
|
||||
utils.updateUnits();
|
||||
utils.setSpeedLabel(
|
||||
R.id.speed_label, R.string.speed, R.string.pace_label);
|
||||
Log.w(MyTracksConstants.TAG, "Setting speed labels");
|
||||
utils.setSpeedLabels();
|
||||
restoreStats();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
super.onCreateOptionsMenu(menu);
|
||||
MenuItem currentSegment = menu.add(0,
|
||||
MyTracksConstants.MENU_CURRENT_SEGMENT, 0, R.string.current_segment);
|
||||
currentSegment.setIcon(R.drawable.menu_by_distance);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPrepareOptionsMenu(Menu menu) {
|
||||
MenuItem item = menu.findItem(MyTracksConstants.MENU_CURRENT_SEGMENT);
|
||||
if (item != null) {
|
||||
item.setTitle(showCurrentSegment
|
||||
? getString(R.string.current_track)
|
||||
: getString(R.string.current_segment));
|
||||
}
|
||||
return super.onPrepareOptionsMenu(menu);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case MyTracksConstants.MENU_CURRENT_SEGMENT:
|
||||
showCurrentSegment = !showCurrentSegment;
|
||||
restoreStats();
|
||||
return true;
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLocationChanged(Location l) {
|
||||
if (selectedTrackIsRecording()) {
|
||||
updateLocation(l);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onProviderDisabled(String provider) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onProviderEnabled(String provider) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStatusChanged(String provider, int status, Bundle extras) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers to receive location updates from the GPS location provider.
|
||||
*/
|
||||
private void registerLocationListener() {
|
||||
if (locationManager != null) {
|
||||
LocationProvider gpsProvider =
|
||||
locationManager.getProvider(MyTracksConstants.GPS_PROVIDER);
|
||||
if (gpsProvider == null) {
|
||||
Toast.makeText(this, getString(R.string.error_no_gps_location_provider),
|
||||
Toast.LENGTH_LONG).show();
|
||||
return;
|
||||
} else {
|
||||
Log.d(MyTracksConstants.TAG, "StatsActivity: Using location provider "
|
||||
+ gpsProvider.getName());
|
||||
}
|
||||
locationManager.requestLocationUpdates(gpsProvider.getName(),
|
||||
0/*minTime*/, 0/*minDist*/, this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregisters all location listener.
|
||||
*/
|
||||
private void unregisterLocationListener() {
|
||||
if (locationManager != null) {
|
||||
locationManager.removeUpdates(this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if the selected track is the currently recording track
|
||||
*/
|
||||
private boolean selectedTrackIsRecording() {
|
||||
return MyTracks.getInstance().isRecording()
|
||||
&& selectedTrackId == recordingTrackId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads values for selected tracks from provider and update the UI.
|
||||
*/
|
||||
private void restoreStats() {
|
||||
if (selectedTrackId < 0) {
|
||||
utils.setAllToUnknown();
|
||||
return;
|
||||
}
|
||||
|
||||
Cursor cursor = null;
|
||||
try {
|
||||
cursor = getContentResolver().query(
|
||||
showCurrentSegment
|
||||
? WaypointsColumns.CONTENT_URI
|
||||
: TracksColumns.CONTENT_URI,
|
||||
PROJECTION_COLUMNS,
|
||||
showCurrentSegment
|
||||
? WaypointsColumns.TRACKID + "=" + selectedTrackId
|
||||
: "_id=" + selectedTrackId,
|
||||
null,
|
||||
"_id LIMIT 1");
|
||||
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
final int idxStartTime =
|
||||
cursor.getColumnIndexOrThrow(TracksColumns.STARTTIME);
|
||||
final int idxTotalTime =
|
||||
cursor.getColumnIndexOrThrow(TracksColumns.TOTALTIME);
|
||||
final int idxMovingTime =
|
||||
cursor.getColumnIndexOrThrow(TracksColumns.MOVINGTIME);
|
||||
final int idxTotalDistance =
|
||||
cursor.getColumnIndexOrThrow(TracksColumns.TOTALDISTANCE);
|
||||
final int idxAverageSpeed =
|
||||
cursor.getColumnIndexOrThrow(TracksColumns.AVGSPEED);
|
||||
final int idxAverageMovingSpeed =
|
||||
cursor.getColumnIndexOrThrow(TracksColumns.AVGMOVINGSPEED);
|
||||
final int idxMaxSpeed =
|
||||
cursor.getColumnIndexOrThrow(TracksColumns.MAXSPEED);
|
||||
final int idxMinElevation =
|
||||
cursor.getColumnIndexOrThrow(TracksColumns.MINELEVATION);
|
||||
final int idxMaxElevation =
|
||||
cursor.getColumnIndexOrThrow(TracksColumns.MAXELEVATION);
|
||||
final int idxElevationGain =
|
||||
cursor.getColumnIndexOrThrow(TracksColumns.ELEVATIONGAIN);
|
||||
final int idxMinGrade =
|
||||
cursor.getColumnIndexOrThrow(TracksColumns.MINGRADE);
|
||||
final int idxMaxGrade =
|
||||
cursor.getColumnIndexOrThrow(TracksColumns.MAXGRADE);
|
||||
startTime = cursor.getLong(idxStartTime);
|
||||
if (!selectedTrackIsRecording()) {
|
||||
utils.setTime(R.id.total_time_register, cursor.getLong(idxTotalTime));
|
||||
}
|
||||
utils.setAllStats(cursor.getLong(idxMovingTime),
|
||||
cursor.getFloat(idxTotalDistance), cursor.getFloat(idxAverageSpeed),
|
||||
cursor.getFloat(idxAverageMovingSpeed),
|
||||
cursor.getFloat(idxMaxSpeed), cursor.getFloat(idxMinElevation),
|
||||
cursor.getFloat(idxMaxElevation), cursor.getFloat(idxElevationGain),
|
||||
cursor.getFloat(idxMinGrade), cursor.getFloat(idxMaxGrade));
|
||||
}
|
||||
} finally {
|
||||
if (cursor != null) {
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this activity needs to update live track data or not.
|
||||
* If so, make sure that:
|
||||
* a) a thread keeps updating the total time
|
||||
* b) a location listener is registered
|
||||
* c) a content observer is registered
|
||||
* Otherwise unregister listeners, observers, and kill update thread.
|
||||
*/
|
||||
private void checkLiveTrack() {
|
||||
final boolean isRecording = selectedTrackIsRecording();
|
||||
final boolean startThread =
|
||||
(thread == null) && isRecording && activityOnTop;
|
||||
final boolean killThread =
|
||||
(thread != null) && (!isRecording || !activityOnTop);
|
||||
if (startThread) {
|
||||
thread = new UIUpdateThread();
|
||||
thread.start();
|
||||
getContentResolver().registerContentObserver(
|
||||
TracksColumns.CONTENT_URI, false, observer);
|
||||
getContentResolver().registerContentObserver(
|
||||
WaypointsColumns.CONTENT_URI, false, observer);
|
||||
registerLocationListener();
|
||||
} else if (killThread) {
|
||||
thread.interrupt();
|
||||
thread = null;
|
||||
getContentResolver().unregisterContentObserver(observer);
|
||||
unregisterLocationListener();
|
||||
}
|
||||
}
|
||||
|
||||
public void updateTotalTime() {
|
||||
if (selectedTrackIsRecording()) {
|
||||
utils.setTime(R.id.total_time_register,
|
||||
System.currentTimeMillis() - startTime);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the given location fields (latitude, longitude, altitude) and all
|
||||
* other fields.
|
||||
*
|
||||
* @param l may be null (will set location fields to unknown)
|
||||
*/
|
||||
private void updateLocation(Location l) {
|
||||
if (l == null) {
|
||||
utils.setUnknown(R.id.elevation_register);
|
||||
utils.setUnknown(R.id.latitude_register);
|
||||
utils.setUnknown(R.id.longitude_register);
|
||||
utils.setUnknown(R.id.speed_register);
|
||||
} else {
|
||||
utils.setAltitude(R.id.elevation_register, l.getAltitude());
|
||||
utils.setLatLong(R.id.latitude_register, l.getLatitude());
|
||||
utils.setLatLong(R.id.longitude_register, l.getLongitude());
|
||||
utils.setSpeed(R.id.speed_register, l.getSpeed() * 3.6);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,274 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
import com.google.android.apps.mytracks.content.Track;
|
||||
import com.google.android.apps.mytracks.content.Waypoint;
|
||||
import com.google.android.apps.mytracks.util.StringUtils;
|
||||
import com.google.android.apps.mytracks.util.UnitConversions;
|
||||
import com.google.android.maps.mytracks.R;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.util.Log;
|
||||
import android.widget.TextView;
|
||||
|
||||
import java.text.DecimalFormat;
|
||||
import java.text.NumberFormat;
|
||||
|
||||
/**
|
||||
* Various utility functions for views that display statistics information.
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
*/
|
||||
public class StatsUtilities {
|
||||
|
||||
private final Activity activity;
|
||||
private static final NumberFormat LAT_LONG_FORMAT =
|
||||
new DecimalFormat("##,###.00000");
|
||||
private static final NumberFormat ALTITUDE_FORMAT =
|
||||
new DecimalFormat("###,###");
|
||||
private static final NumberFormat SPEED_FORMAT =
|
||||
new DecimalFormat("#,###,###.00");
|
||||
private static final NumberFormat GRADE_FORMAT =
|
||||
new DecimalFormat("##.0%");
|
||||
|
||||
/**
|
||||
* True if distances should be displayed in metric units (from shared
|
||||
* preferences).
|
||||
*/
|
||||
private boolean metricUnits = true;
|
||||
|
||||
/**
|
||||
* True - report speed
|
||||
* False - report pace
|
||||
*/
|
||||
private boolean reportSpeed = true;
|
||||
|
||||
public StatsUtilities(Activity a) {
|
||||
this.activity = a;
|
||||
}
|
||||
|
||||
public boolean isMetricUnits() {
|
||||
return metricUnits;
|
||||
}
|
||||
|
||||
public void setMetricUnits(boolean metricUnits) {
|
||||
this.metricUnits = metricUnits;
|
||||
}
|
||||
|
||||
public boolean isReportSpeed() {
|
||||
return reportSpeed;
|
||||
}
|
||||
|
||||
public void setReportSpeed(boolean reportSpeed) {
|
||||
this.reportSpeed = reportSpeed;
|
||||
}
|
||||
|
||||
public void setUnknown(int id) {
|
||||
((TextView) activity.findViewById(id)).setText(R.string.unknown);
|
||||
}
|
||||
|
||||
public void setText(int id, double d, NumberFormat format) {
|
||||
if (!Double.isNaN(d) && !Double.isInfinite(d)) {
|
||||
setText(id, format.format(d));
|
||||
}
|
||||
}
|
||||
|
||||
public void setText(int id, String s) {
|
||||
int lengthLimit = 8;
|
||||
String displayString = s.length() > lengthLimit
|
||||
? s.substring(0, lengthLimit - 3) + "..."
|
||||
: s;
|
||||
((TextView) activity.findViewById(id)).setText(displayString);
|
||||
}
|
||||
|
||||
public void setLatLong(int id, double d) {
|
||||
TextView msgTextView = (TextView) activity.findViewById(id);
|
||||
msgTextView.setText(LAT_LONG_FORMAT.format(d));
|
||||
}
|
||||
|
||||
public void setAltitude(int id, double d) {
|
||||
setText(id, (metricUnits ? d : (d * UnitConversions.M_TO_FT)),
|
||||
ALTITUDE_FORMAT);
|
||||
}
|
||||
|
||||
public void setDistance(int id, double d) {
|
||||
setText(id, (metricUnits ? d : (d * UnitConversions.KM_TO_MI)),
|
||||
SPEED_FORMAT);
|
||||
}
|
||||
|
||||
public void setSpeed(int id, double d) {
|
||||
if (d == 0) {
|
||||
setUnknown(id);
|
||||
return;
|
||||
}
|
||||
double speed = metricUnits ? d : d * UnitConversions.KM_TO_MI;
|
||||
if (reportSpeed) {
|
||||
setText(id, speed, SPEED_FORMAT);
|
||||
} else {
|
||||
// Format as milliseconds per unit
|
||||
long pace = (long) (3600000.0 / speed);
|
||||
setTime(id, pace);
|
||||
}
|
||||
}
|
||||
|
||||
public void setAltitudeUnits(int unitLabelId) {
|
||||
TextView unitTextView = (TextView) activity.findViewById(unitLabelId);
|
||||
unitTextView.setText(metricUnits ? R.string.meter : R.string.feet);
|
||||
}
|
||||
|
||||
public void setDistanceUnits(int unitLabelId) {
|
||||
TextView unitTextView = (TextView) activity.findViewById(unitLabelId);
|
||||
unitTextView.setText(metricUnits ? R.string.kilometer : R.string.mile);
|
||||
}
|
||||
|
||||
public void setSpeedUnits(int unitLabelId, int unitLabelBottomId) {
|
||||
TextView unitTextView = (TextView) activity.findViewById(unitLabelId);
|
||||
unitTextView.setText(reportSpeed
|
||||
? (metricUnits ? R.string.kilometer : R.string.mile)
|
||||
: R.string.min);
|
||||
|
||||
unitTextView = (TextView) activity.findViewById(unitLabelBottomId);
|
||||
unitTextView.setText(reportSpeed
|
||||
? R.string.hr
|
||||
: (metricUnits ? R.string.kilometer : R.string.mile));
|
||||
}
|
||||
|
||||
public void setTime(int id, long l) {
|
||||
setText(id, StringUtils.formatTime(l));
|
||||
}
|
||||
|
||||
public void setGrade(int id, double d) {
|
||||
setText(id, d, GRADE_FORMAT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the unit fields.
|
||||
*/
|
||||
public void updateUnits() {
|
||||
setSpeedUnits(R.id.speed_unit_label_top, R.id.speed_unit_label_bottom);
|
||||
updateWaypointUnits();
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the units fields used by waypoints.
|
||||
*/
|
||||
public void updateWaypointUnits() {
|
||||
setSpeedUnits(R.id.average_moving_speed_unit_label_top,
|
||||
R.id.average_moving_speed_unit_label_bottom);
|
||||
setSpeedUnits(R.id.average_speed_unit_label_top,
|
||||
R.id.average_speed_unit_label_bottom);
|
||||
setDistanceUnits(R.id.total_distance_unit_label);
|
||||
setSpeedUnits(R.id.max_speed_unit_label_top,
|
||||
R.id.max_speed_unit_label_bottom);
|
||||
setAltitudeUnits(R.id.elevation_unit_label);
|
||||
setAltitudeUnits(R.id.elevation_gain_unit_label);
|
||||
setAltitudeUnits(R.id.min_elevation_unit_label);
|
||||
setAltitudeUnits(R.id.max_elevation_unit_label);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets all fields to "-" (unknown).
|
||||
*/
|
||||
public void setAllToUnknown() {
|
||||
// "Instant" values:
|
||||
setUnknown(R.id.elevation_register);
|
||||
setUnknown(R.id.latitude_register);
|
||||
setUnknown(R.id.longitude_register);
|
||||
setUnknown(R.id.speed_register);
|
||||
// Values from provider:
|
||||
setUnknown(R.id.total_time_register);
|
||||
setUnknown(R.id.moving_time_register);
|
||||
setUnknown(R.id.total_distance_register);
|
||||
setUnknown(R.id.average_speed_register);
|
||||
setUnknown(R.id.average_moving_speed_register);
|
||||
setUnknown(R.id.max_speed_register);
|
||||
setUnknown(R.id.min_elevation_register);
|
||||
setUnknown(R.id.max_elevation_register);
|
||||
setUnknown(R.id.elevation_gain_register);
|
||||
setUnknown(R.id.min_grade_register);
|
||||
setUnknown(R.id.max_grade_register);
|
||||
}
|
||||
|
||||
public void setAllStats(long movingTime, double totalDistance,
|
||||
double averageSpeed, double averageMovingSpeed, double maxSpeed,
|
||||
double minElevation, double maxElevation, double elevationGain,
|
||||
double minGrade, double maxGrade) {
|
||||
setTime(R.id.moving_time_register, movingTime);
|
||||
setDistance(R.id.total_distance_register, totalDistance / 1000);
|
||||
setSpeed(R.id.average_speed_register, averageSpeed * 3.6);
|
||||
setSpeed(R.id.average_moving_speed_register, averageMovingSpeed * 3.6);
|
||||
setSpeed(R.id.max_speed_register, maxSpeed * 3.6);
|
||||
setAltitude(R.id.min_elevation_register, minElevation);
|
||||
setAltitude(R.id.max_elevation_register, maxElevation);
|
||||
setAltitude(R.id.elevation_gain_register, elevationGain);
|
||||
setGrade(R.id.min_grade_register, minGrade);
|
||||
setGrade(R.id.max_grade_register, maxGrade);
|
||||
}
|
||||
|
||||
public void setAllStats(Waypoint wpt) {
|
||||
setTime(R.id.moving_time_register, wpt.getMovingTime());
|
||||
setDistance(R.id.total_distance_register, wpt.getTotalDistance() / 1000);
|
||||
setSpeed(R.id.average_speed_register, wpt.getAverageSpeed() * 3.6);
|
||||
setSpeed(R.id.average_moving_speed_register,
|
||||
wpt.getAverageMovingSpeed() * 3.6);
|
||||
setSpeed(R.id.max_speed_register, wpt.getMaxSpeed() * 3.6);
|
||||
setAltitude(R.id.min_elevation_register, wpt.getMinElevation());
|
||||
setAltitude(R.id.max_elevation_register, wpt.getMaxElevation());
|
||||
setAltitude(R.id.elevation_gain_register, wpt.getTotalElevationGain());
|
||||
setGrade(R.id.min_grade_register, wpt.getMinGrade());
|
||||
setGrade(R.id.max_grade_register, wpt.getMaxGrade());
|
||||
}
|
||||
|
||||
public void setAllStats(Track track) {
|
||||
setTime(R.id.moving_time_register, track.getMovingTime());
|
||||
setDistance(R.id.total_distance_register, track.getTotalDistance() / 1000);
|
||||
setSpeed(R.id.average_speed_register, track.getAverageSpeed() * 3.6);
|
||||
setSpeed(R.id.average_moving_speed_register,
|
||||
track.getAverageMovingSpeed() * 3.6);
|
||||
setSpeed(R.id.max_speed_register, track.getMaxSpeed() * 3.6);
|
||||
setAltitude(R.id.min_elevation_register, track.getMinElevation());
|
||||
setAltitude(R.id.max_elevation_register, track.getMaxElevation());
|
||||
setAltitude(R.id.elevation_gain_register, track.getTotalElevationGain());
|
||||
setGrade(R.id.min_grade_register, track.getMinGrade());
|
||||
setGrade(R.id.max_grade_register, track.getMaxGrade());
|
||||
}
|
||||
|
||||
public void setSpeedLabel(int id, int speedString, int paceString) {
|
||||
Log.w(MyTracksConstants.TAG, "Setting view " + id +
|
||||
" to " + reportSpeed +
|
||||
" speed: " + speedString +
|
||||
" pace: " + paceString);
|
||||
TextView tv = ((TextView) activity.findViewById(id));
|
||||
if (tv != null) {
|
||||
tv.setText(reportSpeed ? speedString : paceString);
|
||||
} else {
|
||||
Log.w(MyTracksConstants.TAG, "Could not find id: " + id);
|
||||
}
|
||||
}
|
||||
|
||||
public void setSpeedLabels() {
|
||||
setSpeedLabel(R.id.average_speed_label,
|
||||
R.string.average_speed_label,
|
||||
R.string.average_pace_label);
|
||||
setSpeedLabel(R.id.average_moving_speed_label,
|
||||
R.string.average_moving_speed_label,
|
||||
R.string.average_moving_pace_label);
|
||||
setSpeedLabel(R.id.max_speed_label,
|
||||
R.string.max_speed_label,
|
||||
R.string.min_pace_label);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
* Copyright 2008 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
package com.google.android.apps.mytracks;
|
||||
|
||||
import com.google.android.maps.mytracks.R;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.View.OnClickListener;
|
||||
import android.view.Window;
|
||||
import android.webkit.WebView;
|
||||
|
||||
/**
|
||||
* An activity that displays a welcome screen.
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
*/
|
||||
public class WelcomeActivity extends Activity {
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
requestWindowFeature(Window.FEATURE_NO_TITLE);
|
||||
requestWindowFeature(Window.FEATURE_PROGRESS);
|
||||
|
||||
setContentView(R.layout.welcome);
|
||||
|
||||
WebView web = (WebView) findViewById(R.id.welcome_web);
|
||||
web.loadUrl("file:///android_asset/welcome.html");
|
||||
|
||||
findViewById(R.id.welcome_read_later).setOnClickListener(
|
||||
new OnClickListener() {
|
||||
public void onClick(View v) {
|
||||
finish();
|
||||
}
|
||||
});
|
||||
|
||||
findViewById(R.id.welcome_about).setOnClickListener(new OnClickListener() {
|
||||
public void onClick(View v) {
|
||||
about();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows the "about" dialog.
|
||||
*
|
||||
* TODO: Add a menu option for showing the same thing.
|
||||
*/
|
||||
public void about() {
|
||||
LayoutInflater li = LayoutInflater.from(this);
|
||||
View view = li.inflate(R.layout.about, null);
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(this);
|
||||
builder.setView(view);
|
||||
builder.setPositiveButton(R.string.ok, null);
|
||||
builder.setIcon(R.drawable.arrow_icon);
|
||||
AlertDialog dialog = builder.create();
|
||||
dialog.show();
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,12 @@
|
||||
/*
|
||||
* 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
|
||||
@@ -19,16 +19,16 @@ import java.util.Vector;
|
||||
|
||||
/**
|
||||
* An interface for an object that can generate descriptions of waypoints.
|
||||
*
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
*/
|
||||
public interface DescriptionGenerator {
|
||||
|
||||
|
||||
/**
|
||||
* Generate a description of the waypoint.
|
||||
*/
|
||||
public String generateWaypointDescription(Waypoint waypoint);
|
||||
|
||||
|
||||
/**
|
||||
* Generates a description for a track (with information about the
|
||||
* statistics).
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
/*
|
||||
* Copyright 2008 Google Inc.
|
||||
*
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
/*
|
||||
* 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
|
||||
@@ -60,7 +60,7 @@ public interface MyTracksProviderUtils {
|
||||
* Finds the next statistics waypoint after the given waypoint.
|
||||
*
|
||||
* @param waypoint a given waypoint
|
||||
* @return the next statistics waypoint, or null if none found
|
||||
* @return the next statistics waypoint, or null if none found.
|
||||
*/
|
||||
Waypoint getNextStatisticsWaypointAfter(Waypoint waypoint);
|
||||
|
||||
@@ -275,7 +275,7 @@ public interface MyTracksProviderUtils {
|
||||
/**
|
||||
* Creates a waypoint object from a given cursor.
|
||||
*
|
||||
* @param cursor a cursor pointing at a db or provider with waypoints
|
||||
* @param cursor a cursor pointing at a db or provider with waypoints.
|
||||
* @return a new waypoint object
|
||||
*/
|
||||
public Waypoint createWaypoint(Cursor cursor);
|
||||
|
||||
+4
-4
@@ -1,12 +1,12 @@
|
||||
/*
|
||||
* Copyright 2008 Google Inc.
|
||||
*
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
@@ -768,7 +768,7 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils {
|
||||
@Override
|
||||
public Cursor getLocationsCursor(long trackId, long minTrackPointId,
|
||||
int maxLocations, boolean descending) {
|
||||
String selection;
|
||||
String selection;
|
||||
if (minTrackPointId > 0) {
|
||||
selection = String.format("%s=%d AND %s>=%d",
|
||||
TrackPointsColumns.TRACKID, trackId,
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
/*
|
||||
* Copyright 2008 Google Inc.
|
||||
*
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
@@ -31,7 +31,7 @@ public class Track implements Parcelable {
|
||||
/**
|
||||
* Creator for a Track object
|
||||
*/
|
||||
public class Creator implements Parcelable.Creator<Track> {
|
||||
public static class Creator implements Parcelable.Creator<Track> {
|
||||
|
||||
public Track createFromParcel(Parcel source) {
|
||||
Track track = new Track();
|
||||
@@ -55,10 +55,10 @@ public class Track implements Parcelable {
|
||||
track.totalElevationGain = source.readDouble();
|
||||
track.minGrade = source.readDouble();
|
||||
track.maxGrade = source.readDouble();
|
||||
left = source.readInt();
|
||||
top = source.readInt();
|
||||
right = source.readInt();
|
||||
bottom = source.readInt();
|
||||
track.left = source.readInt();
|
||||
track.top = source.readInt();
|
||||
track.right = source.readInt();
|
||||
track.bottom = source.readInt();
|
||||
track.numberOfPoints = source.readInt();
|
||||
for (int i = 0; i < track.numberOfPoints; ++i) {
|
||||
Location loc = source.readParcelable(null);
|
||||
@@ -72,7 +72,7 @@ public class Track implements Parcelable {
|
||||
}
|
||||
}
|
||||
|
||||
public static final Creator CREATOR = new Track().new Creator();
|
||||
public static final Creator CREATOR = new Creator();
|
||||
|
||||
/**
|
||||
* The track points.
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
/*
|
||||
* 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
|
||||
@@ -87,8 +87,8 @@ public class TrackBuffer {
|
||||
|
||||
/**
|
||||
* Adds a location to the end of the buffer.
|
||||
* @param location The location to add
|
||||
* @param id The id of the location to be added
|
||||
* @param location The location to add.
|
||||
* @param id The id of the location to be added.
|
||||
*/
|
||||
public void add(Location location, long id) {
|
||||
buffer[locationsLoaded++] = location;
|
||||
@@ -103,7 +103,7 @@ public class TrackBuffer {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The number of locations loaded into the buffer
|
||||
* @return The number of locations loaded into the buffer.
|
||||
*/
|
||||
public int getLocationsLoaded() {
|
||||
return locationsLoaded;
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
/*
|
||||
* Copyright 2008 Google Inc.
|
||||
*
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
/*
|
||||
* Copyright 2008 Google Inc.
|
||||
*
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
/*
|
||||
* 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
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
/*
|
||||
* 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
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
* 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.io;
|
||||
|
||||
import android.content.Intent;
|
||||
|
||||
/**
|
||||
* This interface describes a class that will fetch and maintain a Google
|
||||
* authentication token.
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
*/
|
||||
public interface AuthManager {
|
||||
|
||||
/**
|
||||
* Initializes the login process. The user should be asked to login if they
|
||||
* haven't already. The {@link Runnable} provided will be executed when the
|
||||
* auth token is successfully fetched.
|
||||
*
|
||||
* @param whenFinished A {@link Runnable} to execute when the auth token
|
||||
* has been successfully fetched and is available via
|
||||
* {@link #getAuthToken()}
|
||||
*/
|
||||
public abstract void doLogin(Runnable whenFinished, Object o);
|
||||
|
||||
/**
|
||||
* The {@link android.app.Activity} owner of this class should call this
|
||||
* function when it gets {@link android.app.Activity#onActivityResult} with
|
||||
* the request code passed into the constructor. The resultCode and results
|
||||
* should come directly from the {@link android.app.Activity#onActivityResult}
|
||||
* function. This function will return true if an auth token was successfully
|
||||
* fetched or the process is not finished.
|
||||
*
|
||||
* @param resultCode The result code passed in to the
|
||||
* {@link android.app.Activity}'s
|
||||
* {@link android.app.Activity#onActivityResult} function
|
||||
* @param results The data passed in to the {@link android.app.Activity}'s
|
||||
* {@link android.app.Activity#onActivityResult} function
|
||||
* @return True if the auth token was fetched or we aren't done fetching
|
||||
* the auth token, or False if there was an error or the request was
|
||||
* canceled
|
||||
*/
|
||||
public abstract boolean authResult(int resultCode, Intent results);
|
||||
|
||||
/**
|
||||
* Returns the current auth token. Response may be null if no valid auth
|
||||
* token has been fetched.
|
||||
*
|
||||
* @return The current auth token or null if no auth token has been
|
||||
* fetched
|
||||
*/
|
||||
public abstract String getAuthToken();
|
||||
|
||||
/**
|
||||
* Invalidates the existing auth token and request a new one. The
|
||||
* {@link Runnable} provided will be executed when the new auth token is
|
||||
* successfully fetched.
|
||||
*
|
||||
* @param whenFinished A {@link Runnable} to execute when a new auth token
|
||||
* is successfully fetched
|
||||
*/
|
||||
public abstract void invalidateAndRefresh(Runnable whenFinished);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* 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.io;
|
||||
|
||||
import com.google.android.apps.mytracks.MyTracksConstants;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
|
||||
/**
|
||||
* A factory for getting the platform specific AuthManager.
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
*/
|
||||
public class AuthManagerFactory {
|
||||
|
||||
private AuthManagerFactory() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the modern AuthManager should be used
|
||||
*/
|
||||
public static boolean useModernAuthManager() {
|
||||
return Integer.parseInt(Build.VERSION.SDK) >= 7;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a right {@link AuthManager} for the platform.
|
||||
* @return A new AuthManager
|
||||
*/
|
||||
public static AuthManager getAuthManager(Activity activity, int code,
|
||||
Bundle extras, boolean requireGoogle, String service) {
|
||||
if (useModernAuthManager()) {
|
||||
Log.i(MyTracksConstants.TAG,
|
||||
"Creating modern auth manager: " + service);
|
||||
return new ModernAuthManager(activity, code, extras, requireGoogle, service);
|
||||
} else {
|
||||
Log.i(MyTracksConstants.TAG,
|
||||
"Creating legacy auth manager: " + service);
|
||||
return new AuthManagerOld(activity, code, extras, requireGoogle, service);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
/*
|
||||
* 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.io;
|
||||
|
||||
import com.google.android.googlelogindist.GoogleLoginServiceConstants;
|
||||
import com.google.android.googlelogindist.GoogleLoginServiceHelper;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
|
||||
import java.util.Iterator;
|
||||
import java.util.Vector;
|
||||
|
||||
/**
|
||||
* AuthManager keeps track of the current auth token for a user. The advantage
|
||||
* over just passing around a String is that this class can renew the auth
|
||||
* token if necessary, and it will change for all classes using this
|
||||
* AuthManager.
|
||||
*/
|
||||
public class AuthManagerOld implements AuthManager {
|
||||
/** The activity that will handle auth result callbacks. */
|
||||
private final Activity activity;
|
||||
|
||||
/** The code used to tell the activity that it is an auth result. */
|
||||
private final int code;
|
||||
|
||||
/** Extras to pass into the getCredentials function. */
|
||||
private final Bundle extras;
|
||||
|
||||
/** True if the account must be a Google account (not a domain account). */
|
||||
private final boolean requireGoogle;
|
||||
|
||||
/** The name of the service to authorize for. */
|
||||
private final String service;
|
||||
|
||||
/** A list of handlers to call when a new auth token is fetched. */
|
||||
private final Vector<Runnable> newTokenListeners = new Vector<Runnable>();
|
||||
|
||||
/** The most recently fetched auth token or null if none is available. */
|
||||
private String authToken;
|
||||
|
||||
/**
|
||||
* The number of handlers at the beginning of the above list that shouldn't
|
||||
* be removed after they are called.
|
||||
*/
|
||||
private int stickyNewTokenListenerCount;
|
||||
|
||||
/**
|
||||
* AuthManager requires many of the same parameters as
|
||||
* {@link GoogleLoginServiceHelper#getCredentials(Activity, int, Bundle,
|
||||
* boolean, String, boolean)}. The activity must have
|
||||
* a handler in {@link Activity#onActivityResult} that calls
|
||||
* {@link #authResult(int, Intent)} if the request code is the code given
|
||||
* here.
|
||||
*
|
||||
* @param activity An activity with a handler in
|
||||
* {@link Activity#onActivityResult} that calls
|
||||
* {@link #authResult(int, Intent)} when {@literal code} is the request
|
||||
* code
|
||||
* @param code The request code to pass to
|
||||
* {@link Activity#onActivityResult} when
|
||||
* {@link #authResult(int, Intent)} should be called
|
||||
* @param extras A {@link Bundle} of extras for
|
||||
* {@link GoogleLoginServiceHelper}
|
||||
* @param requireGoogle True if the account must be a Google account
|
||||
* @param service The name of the service to authenticate as
|
||||
*/
|
||||
public AuthManagerOld(Activity activity, int code, Bundle extras,
|
||||
boolean requireGoogle, String service) {
|
||||
this.activity = activity;
|
||||
this.code = code;
|
||||
this.extras = extras;
|
||||
this.requireGoogle = requireGoogle;
|
||||
this.service = service;
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see com.google.android.apps.mytracks.io.AuthManager#doLogin(java.lang.Runnable)
|
||||
*/
|
||||
public void doLogin(Runnable whenFinished, Object o) {
|
||||
synchronized (newTokenListeners) {
|
||||
if (whenFinished != null) {
|
||||
newTokenListeners.add(whenFinished);
|
||||
}
|
||||
}
|
||||
activity.runOnUiThread(new LoginRunnable());
|
||||
}
|
||||
|
||||
/**
|
||||
* Runnable which actually gets login credentials.
|
||||
*/
|
||||
private class LoginRunnable implements Runnable {
|
||||
@Override
|
||||
public void run() {
|
||||
GoogleLoginServiceHelper.getCredentials(
|
||||
activity, code, extras, requireGoogle, service, true);
|
||||
}
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see com.google.android.apps.mytracks.io.AuthManager#authResult(int, android.content.Intent)
|
||||
*/
|
||||
public boolean authResult(int resultCode, Intent results) {
|
||||
if (resultCode == Activity.RESULT_OK) {
|
||||
authToken = results.getStringExtra(
|
||||
GoogleLoginServiceConstants.AUTHTOKEN_KEY);
|
||||
if (authToken == null) {
|
||||
GoogleLoginServiceHelper.getCredentials(
|
||||
activity, code, extras, requireGoogle, service, false);
|
||||
return true;
|
||||
} else {
|
||||
// Notify all active listeners that we have a new auth token.
|
||||
synchronized (newTokenListeners) {
|
||||
Iterator<Runnable> iter = newTokenListeners.iterator();
|
||||
while (iter.hasNext()) {
|
||||
iter.next().run();
|
||||
}
|
||||
iter = null;
|
||||
// Remove anything not in the sticky part of the list.
|
||||
newTokenListeners.setSize(stickyNewTokenListenerCount);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see com.google.android.apps.mytracks.io.AuthManager#getAuthToken()
|
||||
*/
|
||||
public String getAuthToken() {
|
||||
return authToken;
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see com.google.android.apps.mytracks.io.AuthManager#invalidateAndRefresh(java.lang.Runnable)
|
||||
*/
|
||||
public void invalidateAndRefresh(Runnable whenFinished) {
|
||||
synchronized (newTokenListeners) {
|
||||
if (whenFinished != null) {
|
||||
newTokenListeners.add(whenFinished);
|
||||
}
|
||||
}
|
||||
activity.runOnUiThread(new Runnable() {
|
||||
public void run() {
|
||||
GoogleLoginServiceHelper.invalidateAuthToken(activity, code, authToken);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a {@link Runnable} to be executed every time the auth token is
|
||||
* updated. The {@link Runnable} will not be removed until manually removed
|
||||
* with {@link #removeStickyNewTokenListener(Runnable)}.
|
||||
*
|
||||
* @param listener The {@link Runnable} to execute every time a new auth
|
||||
* token is fetched
|
||||
*/
|
||||
public void addStickyNewTokenListener(Runnable listener) {
|
||||
synchronized (newTokenListeners) {
|
||||
newTokenListeners.add(0, listener);
|
||||
stickyNewTokenListenerCount++;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops executing the given {@link Runnable} every time the auth token is
|
||||
* updated. This {@link Runnable} must have been added with
|
||||
* {@link #addStickyNewTokenListener(Runnable)} above. If the
|
||||
* {@link Runnable} was added more than once, only the first occurrence
|
||||
* will be removed.
|
||||
*
|
||||
* @param listener The {@link Runnable} to stop executing every time a new
|
||||
* auth token is fetched
|
||||
*/
|
||||
public void removeStickyNewTokenListener(Runnable listener) {
|
||||
synchronized (newTokenListeners) {
|
||||
if (stickyNewTokenListenerCount > 0
|
||||
&& newTokenListeners.remove(listener)) {
|
||||
stickyNewTokenListenerCount--;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
/*
|
||||
* 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.io;
|
||||
|
||||
import com.google.android.apps.mytracks.content.Track;
|
||||
import com.google.android.apps.mytracks.content.Waypoint;
|
||||
import com.google.android.apps.mytracks.io.TrackWriterFactory.TrackFileFormat;
|
||||
|
||||
import android.location.Location;
|
||||
import android.os.Environment;
|
||||
|
||||
import java.io.OutputStream;
|
||||
import java.io.PrintWriter;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.TimeZone;
|
||||
|
||||
/**
|
||||
* Exports a track as a CSV file, according to RFC 4180.
|
||||
*
|
||||
* The first field is a type:
|
||||
* TRACK - track description
|
||||
* P - point
|
||||
* WAYPOINT - waypoint
|
||||
*
|
||||
* For each type, the fields are:
|
||||
*
|
||||
* TRACK,,,,,,,,name,description,
|
||||
* P,time,lat,lon,alt,bearing,accurancy,speed,,,segmentIdx
|
||||
* WAYPOINT,time,lat,lon,alt,bearing,accuracy,speed,name,description,
|
||||
*
|
||||
* @author Rodrigo Damazio
|
||||
*/
|
||||
public class CsvTrackWriter implements TrackFormatWriter {
|
||||
static final SimpleDateFormat TIMESTAMP_FORMAT =
|
||||
new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
|
||||
{
|
||||
TIMESTAMP_FORMAT.setTimeZone(TimeZone.getTimeZone("UTC"));
|
||||
}
|
||||
|
||||
private int segmentIdx = 0;
|
||||
private int numFields = -1;
|
||||
private PrintWriter pw;
|
||||
private Track track;
|
||||
|
||||
@Override
|
||||
public String getDefaultDirectory() {
|
||||
return Environment.getExternalStorageDirectory()
|
||||
+ System.getProperty("file.separator")
|
||||
+ getExtension();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getExtension() {
|
||||
return TrackFileFormat.CSV.getExtension();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void prepare(Track track, OutputStream out) {
|
||||
this.track = track;
|
||||
this.pw = new PrintWriter(out);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeHeader() {
|
||||
writeCommaSeparatedLine("TYPE", "TIME", "LAT", "LON", "ALT", "BEARING",
|
||||
"ACCURACY", "SPEED", "NAME", "DESCRIPTION", "SEGMENT");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeBeginTrack(Location firstPoint) {
|
||||
writeCommaSeparatedLine("TRACK",
|
||||
null, null, null, null, null, null, null,
|
||||
track.getName(), track.getDescription(),
|
||||
null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeOpenSegment() {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeLocation(Location location) {
|
||||
String timeStr = TIMESTAMP_FORMAT.format(new Date(location.getTime()));
|
||||
writeCommaSeparatedLine("P",
|
||||
timeStr,
|
||||
Double.toString(location.getLatitude()),
|
||||
Double.toString(location.getLongitude()),
|
||||
Double.toString(location.getAltitude()),
|
||||
Double.toString(location.getBearing()),
|
||||
Double.toString(location.getAccuracy()),
|
||||
Double.toString(location.getSpeed()),
|
||||
null, null,
|
||||
Integer.toString(segmentIdx));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeWaypoint(Waypoint waypoint) {
|
||||
Location location = waypoint.getLocation();
|
||||
String timeStr = TIMESTAMP_FORMAT.format(new Date(location.getTime()));
|
||||
writeCommaSeparatedLine("WAYPOINT",
|
||||
timeStr,
|
||||
Double.toString(location.getLatitude()),
|
||||
Double.toString(location.getLongitude()),
|
||||
Double.toString(location.getAltitude()),
|
||||
Double.toString(location.getBearing()),
|
||||
Double.toString(location.getAccuracy()),
|
||||
Double.toString(location.getSpeed()),
|
||||
waypoint.getName(),
|
||||
waypoint.getDescription(),
|
||||
null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes a single line of a comma-separated-value file.
|
||||
*
|
||||
* @param strs the values to be written as comma-separated
|
||||
*/
|
||||
private void writeCommaSeparatedLine(String... strs) {
|
||||
if (numFields == -1) {
|
||||
numFields = strs.length;
|
||||
} else if (strs.length != numFields) {
|
||||
throw new IllegalArgumentException(
|
||||
"CSV lines with different number of fields");
|
||||
}
|
||||
|
||||
boolean isFirst = true;
|
||||
for (String str : strs) {
|
||||
if (!isFirst) {
|
||||
pw.print(',');
|
||||
}
|
||||
isFirst = false;
|
||||
|
||||
if (str != null) {
|
||||
pw.print('"');
|
||||
pw.print(str.replaceAll("\"", "\"\""));
|
||||
pw.print('"');
|
||||
}
|
||||
}
|
||||
pw.println();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeCloseSegment() {
|
||||
segmentIdx++;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeEndTrack(Location lastPoint) {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeFooter() {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
pw.close();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,237 @@
|
||||
/*
|
||||
* Copyright 2008 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
package com.google.android.apps.mytracks.io;
|
||||
|
||||
import com.google.android.apps.mytracks.content.Track;
|
||||
import com.google.android.apps.mytracks.stats.TripStatistics;
|
||||
import com.google.android.apps.mytracks.util.MyTracksUtils;
|
||||
|
||||
import android.location.Location;
|
||||
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.NamedNodeMap;
|
||||
import org.w3c.dom.Node;
|
||||
import org.w3c.dom.NodeList;
|
||||
import org.xml.sax.SAXException;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.text.ParseException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
|
||||
import javax.xml.parsers.DocumentBuilder;
|
||||
import javax.xml.parsers.DocumentBuilderFactory;
|
||||
import javax.xml.parsers.ParserConfigurationException;
|
||||
|
||||
/**
|
||||
* Helper class to parse a GPX file or string and convert it into a track
|
||||
* object.
|
||||
*
|
||||
* TODO: See if we can use a SAX style parser as the DOM style
|
||||
* parsing uses too much memory and will not allow import of very large GPX
|
||||
* files (limit currently set to 500KB).
|
||||
*
|
||||
* @author Leif Hendrik Wilden
|
||||
*/
|
||||
public class GpxImport {
|
||||
private static final SimpleDateFormat DATE_FORMAT1 =
|
||||
new SimpleDateFormat("yyyy-MM-dd'T'hh:mm:ssZ");
|
||||
private static final SimpleDateFormat DATE_FORMAT2 =
|
||||
new SimpleDateFormat("yyyy-MM-dd'T'hh:mm:ss'Z'");
|
||||
private static final SimpleDateFormat DATE_FORMAT3 =
|
||||
new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ");
|
||||
|
||||
/**
|
||||
* Reads GPS tracks from a GPX file and append tracks and their coordinates to
|
||||
* the given list of tracks.
|
||||
*
|
||||
* @param tracks a list of tracks
|
||||
* @param filename a file name
|
||||
* @throws SAXException a parsing error
|
||||
* @throws ParserConfigurationException internal error
|
||||
* @throws IOException a file reading problem
|
||||
*/
|
||||
public static void importGPXFile(
|
||||
final String filename, final ArrayList<Track> tracks)
|
||||
throws SAXException, ParserConfigurationException, IOException,
|
||||
OutOfMemoryError {
|
||||
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
|
||||
DocumentBuilder builder = factory.newDocumentBuilder();
|
||||
File file = new File(filename);
|
||||
if (file.length() > 500 * 1024) {
|
||||
// Better to throw an error now then, to let the VM do it. If the VM does
|
||||
// it, it will throw another exception while throwing the OutOfMemoryError
|
||||
// and that will kill out app with a nasty error message. In that way we
|
||||
// can at least display a reasonable message to the user and let her know
|
||||
// what the problem is.
|
||||
throw new OutOfMemoryError();
|
||||
}
|
||||
Document doc = builder.parse(file);
|
||||
importGPXDocument(tracks, doc);
|
||||
}
|
||||
|
||||
/**
|
||||
* This is a utility class with only static members.
|
||||
*/
|
||||
private GpxImport() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads GPS tracks from a GPX document and append then to the given list of
|
||||
* tracks.
|
||||
*
|
||||
* TODO: Break this method down into smaller ones
|
||||
*
|
||||
* @param tracks an array list of tracks
|
||||
* @param doc a document
|
||||
*/
|
||||
public static void importGPXDocument(
|
||||
final ArrayList<Track> tracks, final Document doc) {
|
||||
NodeList trackNodes = doc.getElementsByTagName("trk");
|
||||
for (int n = 0; n < trackNodes.getLength(); n++) {
|
||||
Track track = new Track();
|
||||
tracks.add(track);
|
||||
ArrayList<Location> locations = track.getLocations();
|
||||
Location lastLocation = null;
|
||||
|
||||
Node trkNode = trackNodes.item(n);
|
||||
NodeList segmentNodes = trkNode.getChildNodes();
|
||||
ArrayList<Node> nodes = new ArrayList<Node>();
|
||||
int nSegments = 0;
|
||||
for (int k = 0; k < segmentNodes.getLength(); k++) {
|
||||
Node segmentNode = segmentNodes.item(k);
|
||||
if (segmentNode.getNodeName().equals("name")) {
|
||||
track.setName(segmentNode.getFirstChild().getNodeValue());
|
||||
} else if (segmentNode.getNodeName().equals("description")) {
|
||||
track.setDescription(segmentNode.getFirstChild().getNodeValue());
|
||||
} else if (segmentNode.getNodeName().equals("trkseg")) {
|
||||
long pointTime = locations.get(locations.size() - 1).getTime();
|
||||
if (nSegments > 0) {
|
||||
// Add a segment separator:
|
||||
Location location = new Location("gps");
|
||||
location.setLatitude(100.0);
|
||||
location.setLongitude(100.0);
|
||||
location.setAltitude(0);
|
||||
if (locations.size() > 0) {
|
||||
location.setTime(pointTime);
|
||||
}
|
||||
track.addLocation(location);
|
||||
lastLocation = null;
|
||||
}
|
||||
nSegments++;
|
||||
NodeList segmentChildren = segmentNode.getChildNodes();
|
||||
nodes.clear();
|
||||
for (int j = 0; j < segmentChildren.getLength(); j++) {
|
||||
Node child = segmentChildren.item(j);
|
||||
if (child.getNodeName().equals("trkpt")) {
|
||||
nodes.add(child);
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < nodes.size(); i++) {
|
||||
NamedNodeMap namedNodes = nodes.get(i).getAttributes();
|
||||
double lat = Double.parseDouble(namedNodes.getNamedItem("lat")
|
||||
.getNodeValue());
|
||||
double lon = Double.parseDouble(namedNodes.getNamedItem("lon")
|
||||
.getNodeValue());
|
||||
NodeList children = nodes.get(i).getChildNodes();
|
||||
Node elementNode = null;
|
||||
Node timeNode = null;
|
||||
for (int j = 0; j < children.getLength(); j++) {
|
||||
Node child = children.item(j);
|
||||
if (child.getNodeName().equals("ele")) {
|
||||
elementNode = child;
|
||||
} else if (child.getNodeName().equals("time")) {
|
||||
timeNode = child;
|
||||
}
|
||||
}
|
||||
String altitudeStr = null;
|
||||
if (elementNode != null) {
|
||||
altitudeStr = elementNode.getFirstChild().getNodeValue();
|
||||
} else {
|
||||
altitudeStr = "0";
|
||||
}
|
||||
String timeContents = null;
|
||||
if (timeNode != null) {
|
||||
timeContents = timeNode.getFirstChild().getNodeValue();
|
||||
} else {
|
||||
timeContents = "";
|
||||
}
|
||||
double altitude = Double.parseDouble(altitudeStr);
|
||||
long t = -1;
|
||||
try {
|
||||
// 1st try with time zone at end a la "+0000"
|
||||
t = DATE_FORMAT1.parse(timeContents).getTime();
|
||||
} catch (ParseException e) {
|
||||
// if that fails, try with a literal "Z" at the end (this is not
|
||||
// according to xml standard, but some gpx files are like that):
|
||||
try {
|
||||
t = DATE_FORMAT2.parse(timeContents).getTime();
|
||||
} catch (ParseException ex) {
|
||||
// some gpx timestamps have 3 additional digits at the end.
|
||||
try {
|
||||
t = DATE_FORMAT3.parse(timeContents).getTime();
|
||||
} catch (ParseException exc) {
|
||||
t = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Location location = new Location("gps");
|
||||
location.setLatitude(lat);
|
||||
location.setLongitude(lon);
|
||||
location.setAltitude(altitude);
|
||||
location.setTime(t);
|
||||
// We don't have a speed and bearing in GPX, make something up from
|
||||
// the last two points:
|
||||
if (lastLocation != null) {
|
||||
final long dt = location.getTime() - lastLocation.getTime();
|
||||
if (dt > 0) {
|
||||
final float speed =
|
||||
location.distanceTo(lastLocation) / (dt / 1000);
|
||||
location.setSpeed(speed);
|
||||
}
|
||||
location.setBearing(lastLocation.bearingTo(location));
|
||||
}
|
||||
lastLocation = location;
|
||||
if (MyTracksUtils.isValidLocation(location)) {
|
||||
track.addLocation(location);
|
||||
}
|
||||
}
|
||||
|
||||
if (locations.size() > 0) {
|
||||
long startTime = locations.get(0).getTime();
|
||||
track.setStartTime(startTime);
|
||||
|
||||
// Calculate statistics for the imported track
|
||||
int numberOfPoints = locations.size();
|
||||
TripStatistics stats = new TripStatistics(startTime);
|
||||
for (Location location : locations) {
|
||||
if (MyTracksUtils.isValidLocation(location)) {
|
||||
/* Any time works here. The totalTime will be set by "pauseAt" later: */
|
||||
stats.addLocation(location, location.getTime());
|
||||
}
|
||||
}
|
||||
stats.pauseAt(pointTime);
|
||||
track.setStopTime(pointTime);
|
||||
|
||||
stats.fillStatisticsForTrack(track);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
/*
|
||||
* Copyright 2008 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
package com.google.android.apps.mytracks.io;
|
||||
|
||||
import com.google.android.apps.mytracks.content.Track;
|
||||
import com.google.android.apps.mytracks.content.Waypoint;
|
||||
import com.google.android.apps.mytracks.io.TrackWriterFactory.TrackFileFormat;
|
||||
import com.google.android.apps.mytracks.util.StringUtils;
|
||||
|
||||
import android.location.Location;
|
||||
import android.os.Environment;
|
||||
|
||||
import java.io.OutputStream;
|
||||
import java.io.PrintWriter;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.TimeZone;
|
||||
|
||||
/**
|
||||
* Log of one track.
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
*/
|
||||
public class GpxTrackWriter implements TrackFormatWriter {
|
||||
|
||||
static final SimpleDateFormat TIMESTAMP_FORMAT =
|
||||
new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
|
||||
{
|
||||
TIMESTAMP_FORMAT.setTimeZone(TimeZone.getTimeZone("UTC"));
|
||||
}
|
||||
|
||||
private PrintWriter pw = null;
|
||||
private Track track;
|
||||
|
||||
@Override
|
||||
public void prepare(Track track, OutputStream out) {
|
||||
this.track = track;
|
||||
this.pw = new PrintWriter(out);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDefaultDirectory() {
|
||||
return Environment.getExternalStorageDirectory()
|
||||
+ System.getProperty("file.separator")
|
||||
+ getExtension();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getExtension() {
|
||||
return TrackFileFormat.GPX.getExtension();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeHeader() {
|
||||
if (pw != null) {
|
||||
pw.println("<?xml version=\"1.0\" encoding=\"ISO-8859-1\" ");
|
||||
pw.println("standalone=\"yes\"?>");
|
||||
pw.println("<?xml-stylesheet type=\"text/xsl\" href=\"details.xsl\"?>");
|
||||
pw.println("<gpx");
|
||||
pw.println(" version=\"1.0\"");
|
||||
pw.println(" creator=\"My Tracks for the G1 running Android\"");
|
||||
pw.println(" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"");
|
||||
pw.println(" xmlns=\"http://www.topografix.com/GPX/1/0\"");
|
||||
pw.print(" xmlns:topografix=\"http://www.topografix.com/GPX/Private/"
|
||||
+ "TopoGrafix/0/1\"");
|
||||
pw.print(" xsi:schemaLocation=\"http://www.topografix.com/GPX/1/0 ");
|
||||
pw.print("http://www.topografix.com/GPX/1/0/gpx.xsd ");
|
||||
pw.print("http://www.topografix.com/GPX/Private/TopoGrafix/0/1 ");
|
||||
pw.println("http://www.topografix.com/GPX/Private/TopoGrafix/0/1/"
|
||||
+ "topografix.xsd\">");
|
||||
// TODO: Author etc.
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeFooter() {
|
||||
if (pw != null) {
|
||||
pw.println("</gpx>");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeBeginTrack(Location firstPoint) {
|
||||
if (pw != null) {
|
||||
pw.println("<trk>");
|
||||
pw.println("<name>" + StringUtils.stringAsCData(track.getName())
|
||||
+ "</name>");
|
||||
pw.println("<desc>" + StringUtils.stringAsCData(track.getDescription())
|
||||
+ "</desc>");
|
||||
pw.println("<number>" + track.getId() + "</number>");
|
||||
pw.println("<topografix:color>c0c0c0</topografix:color>");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeEndTrack(Location lastPoint) {
|
||||
if (pw != null) {
|
||||
pw.println("</trk>");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeOpenSegment() {
|
||||
pw.println("<trkseg>");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeCloseSegment() {
|
||||
pw.println("</trkseg>");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeLocation(Location l) {
|
||||
if (pw != null) {
|
||||
pw.println("<trkpt lat=\"" + l.getLatitude() + "\" lon=\""
|
||||
+ l.getLongitude() + "\">");
|
||||
Date d = new Date(l.getTime());
|
||||
pw.println("<ele>" + l.getAltitude() + "</ele>");
|
||||
pw.println("<time>" + TIMESTAMP_FORMAT.format(d) + "</time>");
|
||||
pw.println("</trkpt>");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
if (pw != null) {
|
||||
pw.close();
|
||||
pw = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeWaypoint(Waypoint waypoint) {
|
||||
if (pw != null) {
|
||||
Location l = waypoint.getLocation();
|
||||
if (l != null) {
|
||||
pw.println("<wpt lat=\"" + l.getLatitude() + "\" lon=\""
|
||||
+ l.getLongitude() + "\">");
|
||||
pw.println("<name>" + StringUtils.stringAsCData(waypoint.getName())
|
||||
+ "</name>");
|
||||
pw.println("<desc>"
|
||||
+ StringUtils.stringAsCData(waypoint.getDescription()) + "</desc>");
|
||||
pw.println("<time>" + TIMESTAMP_FORMAT.format(l.getTime()) + "</time>");
|
||||
pw.println("<ele>" + l.getAltitude() + "</ele>");
|
||||
pw.println("</wpt>");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,239 @@
|
||||
/*
|
||||
* Copyright 2008 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
package com.google.android.apps.mytracks.io;
|
||||
|
||||
import com.google.android.apps.mytracks.content.Track;
|
||||
import com.google.android.apps.mytracks.content.Waypoint;
|
||||
import com.google.android.apps.mytracks.io.TrackWriterFactory.TrackFileFormat;
|
||||
import com.google.android.apps.mytracks.util.StringUtils;
|
||||
|
||||
import android.content.Context;
|
||||
import android.location.Location;
|
||||
import android.os.Environment;
|
||||
|
||||
import java.io.OutputStream;
|
||||
import java.io.PrintWriter;
|
||||
import java.util.Vector;
|
||||
|
||||
/**
|
||||
* Write track as KML to a file.
|
||||
*
|
||||
* @author Leif Hendrik Wilden
|
||||
*/
|
||||
public class KmlTrackWriter implements TrackFormatWriter {
|
||||
|
||||
private final Vector<Double> distances = new Vector<Double>();
|
||||
private final Vector<Double> elevations = new Vector<Double>();
|
||||
private final StringUtils stringUtils;
|
||||
private PrintWriter pw = null;
|
||||
private Track track;
|
||||
|
||||
public KmlTrackWriter(Context context) {
|
||||
stringUtils = new StringUtils(context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Testing constructor.
|
||||
*/
|
||||
KmlTrackWriter(StringUtils stringUtils) {
|
||||
this.stringUtils = stringUtils;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void prepare(Track track, OutputStream out) {
|
||||
this.track = track;
|
||||
this.pw = new PrintWriter(out);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDefaultDirectory() {
|
||||
return Environment.getExternalStorageDirectory()
|
||||
+ System.getProperty("file.separator") + getExtension();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getExtension() {
|
||||
return TrackFileFormat.KML.getExtension();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeHeader() {
|
||||
if (pw != null) {
|
||||
pw.println("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
|
||||
pw.print("<kml");
|
||||
pw.print(" xmlns=\"http://earth.google.com/kml/2.0\"");
|
||||
pw.println(" xmlns:atom=\"http://www.w3.org/2005/Atom\">");
|
||||
pw.println("<Document>");
|
||||
pw.println("<atom:author><atom:name>My Tracks running on Android"
|
||||
+ "</atom:name></atom:author>");
|
||||
pw.println("<name>" + StringUtils.stringAsCData(track.getName())
|
||||
+ "</name>");
|
||||
pw.println("<description>"
|
||||
+ StringUtils.stringAsCData(track.getDescription())
|
||||
+ "</description>");
|
||||
writeStyles();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeFooter() {
|
||||
if (pw != null) {
|
||||
pw.println("</Document>");
|
||||
pw.println("</kml>");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeBeginTrack(Location firstPoint) {
|
||||
if (pw != null) {
|
||||
writePlacemark("(Start)", track.getDescription(), "#sh_green-circle",
|
||||
firstPoint);
|
||||
pw.println("<Placemark>");
|
||||
pw.println("<name>" + StringUtils.stringAsCData(track.getName())
|
||||
+ "</name>");
|
||||
pw.println("<description>"
|
||||
+ StringUtils.stringAsCData(track.getDescription())
|
||||
+ "</description>");
|
||||
pw.println("<styleUrl>#track</styleUrl>");
|
||||
pw.println("<MultiGeometry>");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeEndTrack(Location lastPoint) {
|
||||
if (pw != null) {
|
||||
pw.println("</MultiGeometry>");
|
||||
pw.println("</Placemark>");
|
||||
String description = stringUtils.generateTrackDescription(
|
||||
track, distances, elevations);
|
||||
writePlacemark("(End)", description, "#sh_red-circle", lastPoint);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeOpenSegment() {
|
||||
if (pw != null) {
|
||||
pw.print("<LineString><coordinates>");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeCloseSegment() {
|
||||
if (pw != null) {
|
||||
pw.println("</coordinates></LineString>");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeLocation(Location l) {
|
||||
if (pw != null) {
|
||||
pw.print(l.getLongitude() + "," + l.getLatitude() + ","
|
||||
+ l.getAltitude() + " ");
|
||||
}
|
||||
}
|
||||
|
||||
private String getPinStyle(Waypoint waypoint) {
|
||||
if (waypoint.getType() == Waypoint.TYPE_STATISTICS) {
|
||||
return "#sh_ylw-pushpin";
|
||||
}
|
||||
// Try to find the icon color.
|
||||
// The string should be of the form:
|
||||
// "http://maps.google.com/mapfiles/ms/micons/XXX.png"
|
||||
int slash = waypoint.getIcon().lastIndexOf('/');
|
||||
int png = waypoint.getIcon().lastIndexOf('.');
|
||||
if ((slash != -1) && (slash < png)) {
|
||||
String color = waypoint.getIcon().substring(slash + 1, png);
|
||||
return "#sh_" + color + "-pushpin";
|
||||
}
|
||||
return "#sh_blue-pushpin";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeWaypoint(Waypoint waypoint) {
|
||||
if (pw != null) {
|
||||
writePlacemark(
|
||||
waypoint.getName(),
|
||||
waypoint.getDescription(),
|
||||
getPinStyle(waypoint),
|
||||
waypoint.getLocation());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
if (pw != null) {
|
||||
pw.close();
|
||||
pw = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void writeStyles() {
|
||||
pw.println("<Style id=\"track\"><LineStyle><color>7f0000ff</color>"
|
||||
+ "<width>4</width></LineStyle></Style>");
|
||||
|
||||
pw.print("<Style id=\"sh_green-circle\"><IconStyle><scale>1.3</scale>");
|
||||
pw.print("<Icon><href>http://maps.google.com/mapfiles/kml/paddle/"
|
||||
+ "grn-circle.png</href></Icon>");
|
||||
pw.println("<hotSpot x=\"32\" y=\"1\" xunits=\"pixels\" yunits=\"pixels\"/>"
|
||||
+ "</IconStyle></Style>");
|
||||
|
||||
pw.print("<Style id=\"sh_red-circle\"><IconStyle><scale>1.3</scale>");
|
||||
pw.print("<Icon><href>http://maps.google.com/mapfiles/kml/paddle/"
|
||||
+ "red-circle.png</href></Icon>");
|
||||
pw.println("<hotSpot x=\"32\" y=\"1\" xunits=\"pixels\" yunits=\"pixels\"/>"
|
||||
+ "</IconStyle></Style>");
|
||||
|
||||
pw.print("<Style id=\"sh_ylw-pushpin\"><IconStyle><scale>1.3</scale>");
|
||||
pw.print("<Icon><href>http://maps.google.com/mapfiles/kml/pushpin/"
|
||||
+ "ylw-pushpin.png</href></Icon>");
|
||||
pw.println("<hotSpot x=\"20\" y=\"2\" xunits=\"pixels\" yunits=\"pixels\"/>"
|
||||
+ "</IconStyle></Style>");
|
||||
|
||||
pw.print("<Style id=\"sh_blue-pushpin\"><IconStyle><scale>1.3</scale>");
|
||||
pw.print("<Icon><href>http://maps.google.com/mapfiles/kml/pushpin/"
|
||||
+ "blue-pushpin.png</href></Icon>");
|
||||
pw.println("<hotSpot x=\"20\" y=\"2\" xunits=\"pixels\" yunits=\"pixels\"/>"
|
||||
+ "</IconStyle></Style>");
|
||||
|
||||
pw.print("<Style id=\"sh_green-pushpin\"><IconStyle><scale>1.3</scale>");
|
||||
pw.print("<Icon><href>http://maps.google.com/mapfiles/kml/pushpin/"
|
||||
+ "grn-pushpin.png</href></Icon>");
|
||||
pw.println("<hotSpot x=\"20\" y=\"2\" xunits=\"pixels\" yunits=\"pixels\"/>"
|
||||
+ "</IconStyle></Style>");
|
||||
|
||||
pw.print("<Style id=\"sh_red-pushpin\"><IconStyle><scale>1.3</scale>");
|
||||
pw.print("<Icon><href>http://maps.google.com/mapfiles/kml/pushpin/"
|
||||
+ "red-pushpin.png</href></Icon>");
|
||||
pw.println("<hotSpot x=\"20\" y=\"2\" xunits=\"pixels\" yunits=\"pixels\"/>"
|
||||
+ "</IconStyle></Style>");
|
||||
}
|
||||
|
||||
private void writePlacemark(String name, String description, String style,
|
||||
Location location) {
|
||||
if (location != null) {
|
||||
pw.println("<Placemark>");
|
||||
pw.println(" <name>" + StringUtils.stringAsCData(name) + "</name>");
|
||||
pw.println(" <description>" + StringUtils.stringAsCData(description)
|
||||
+ "</description>");
|
||||
pw.println(" <styleUrl>" + style + "</styleUrl>");
|
||||
pw.println(" <Point>");
|
||||
pw.println(" <coordinates>" + location.getLongitude() + ","
|
||||
+ location.getLatitude() + "</coordinates>");
|
||||
pw.println(" </Point>");
|
||||
pw.println("</Placemark>");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
/*
|
||||
* 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.io;
|
||||
|
||||
import com.google.android.accounts.Account;
|
||||
import com.google.android.accounts.AccountManager;
|
||||
import com.google.android.accounts.AccountManagerCallback;
|
||||
import com.google.android.accounts.AccountManagerFuture;
|
||||
import com.google.android.accounts.AuthenticatorException;
|
||||
import com.google.android.accounts.OperationCanceledException;
|
||||
import com.google.android.apps.mytracks.AccountChooser;
|
||||
import com.google.android.apps.mytracks.MyTracks;
|
||||
import com.google.android.apps.mytracks.MyTracksConstants;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* AuthManager keeps track of the current auth token for a user. The advantage
|
||||
* over just passing around a String is that this class can renew the auth
|
||||
* token if necessary, and it will change for all classes using this
|
||||
* AuthManager.
|
||||
*/
|
||||
public class ModernAuthManager implements AuthManager {
|
||||
/** The activity that will handle auth result callbacks. */
|
||||
private final Activity activity;
|
||||
|
||||
/** The name of the service to authorize for. */
|
||||
private final String service;
|
||||
|
||||
/** The most recently fetched auth token or null if none is available. */
|
||||
private String authToken;
|
||||
|
||||
private final AccountManager accountManager;
|
||||
|
||||
private Runnable whenFinished;
|
||||
|
||||
/**
|
||||
* AuthManager requires many of the same parameters as
|
||||
* {@link com.google.android.googlelogindist.GoogleLoginServiceHelper
|
||||
* #getCredentials(Activity, int, Bundle, boolean, String, boolean)}.
|
||||
* The activity must have a handler in {@link Activity#onActivityResult} that
|
||||
* calls {@link #authResult(int, Intent)} if the request code is the code
|
||||
* given here.
|
||||
*
|
||||
* @param activity An activity with a handler in
|
||||
* {@link Activity#onActivityResult} that calls
|
||||
* {@link #authResult(int, Intent)} when {@literal code} is the request
|
||||
* code
|
||||
* @param code The request code to pass to
|
||||
* {@link Activity#onActivityResult} when
|
||||
* {@link #authResult(int, Intent)} should be called
|
||||
* @param extras A {@link Bundle} of extras for
|
||||
* {@link com.google.android.googlelogindist.GoogleLoginServiceHelper}
|
||||
* @param requireGoogle True if the account must be a Google account
|
||||
* @param service The name of the service to authenticate as
|
||||
*/
|
||||
public ModernAuthManager(Activity activity, int code, Bundle extras,
|
||||
boolean requireGoogle, String service) {
|
||||
this.activity = activity;
|
||||
this.service = service;
|
||||
this.accountManager = AccountManager.get(activity);
|
||||
}
|
||||
|
||||
/**
|
||||
* Call this to do the initial login. The user will be asked to login if
|
||||
* they haven't already. The {@link Runnable} provided will be executed
|
||||
* when the auth token is successfully fetched.
|
||||
*
|
||||
* @param runnable A {@link Runnable} to execute when the auth token
|
||||
* has been successfully fetched and is available via
|
||||
* {@link #getAuthToken()}
|
||||
*/
|
||||
public void doLogin(final Runnable runnable, Object o) {
|
||||
this.whenFinished = runnable;
|
||||
if (!(o instanceof Account)) {
|
||||
throw new IllegalArgumentException("FroyoAuthManager requires an account.");
|
||||
}
|
||||
Account account = (Account) o;
|
||||
accountManager.getAuthToken(account, service, true,
|
||||
new AccountManagerCallback<Bundle>() {
|
||||
public void run(AccountManagerFuture<Bundle> future) {
|
||||
try {
|
||||
Bundle result = future.getResult();
|
||||
|
||||
// AccountManager needs user to grant permission
|
||||
if (result.containsKey(AccountManager.KEY_INTENT)) {
|
||||
Intent intent = (Intent) result.get(AccountManager.KEY_INTENT);
|
||||
clearNewTaskFlag(intent);
|
||||
activity.startActivityForResult(intent, MyTracksConstants.GET_LOGIN);
|
||||
return;
|
||||
}
|
||||
|
||||
authToken = result.getString(
|
||||
AccountManager.KEY_AUTHTOKEN);
|
||||
Log.e(MyTracksConstants.TAG, "Got auth token.");
|
||||
runWhenFinished();
|
||||
} catch (OperationCanceledException e) {
|
||||
Log.e(MyTracksConstants.TAG, "Operation Canceled", e);
|
||||
} catch (IOException e) {
|
||||
Log.e(MyTracksConstants.TAG, "IOException", e);
|
||||
} catch (AuthenticatorException e) {
|
||||
Log.e(MyTracksConstants.TAG, "Authentication Failed", e);
|
||||
}
|
||||
}
|
||||
}, null /* handler */);
|
||||
}
|
||||
|
||||
private static void clearNewTaskFlag(Intent intent) {
|
||||
int flags = intent.getFlags();
|
||||
flags &= ~Intent.FLAG_ACTIVITY_NEW_TASK;
|
||||
intent.setFlags(flags);
|
||||
}
|
||||
|
||||
/**
|
||||
* The {@link Activity} passed into the constructor should call this
|
||||
* function when it gets {@link Activity#onActivityResult} with the request
|
||||
* code passed into the constructor. The resultCode and results should
|
||||
* come directly from the {@link Activity#onActivityResult} function. This
|
||||
* function will return true if an auth token was successfully fetched or
|
||||
* the process is not finished.
|
||||
*
|
||||
* @param resultCode The result code passed in to the {@link Activity}'s
|
||||
* {@link Activity#onActivityResult} function
|
||||
* @param results The data passed in to the {@link Activity}'s
|
||||
* {@link Activity#onActivityResult} function
|
||||
* @return True if the auth token was fetched or we aren't done fetching
|
||||
* the auth token, or False if there was an error or the request was
|
||||
* canceled
|
||||
*/
|
||||
public boolean authResult(int resultCode, Intent results) {
|
||||
if (results != null) {
|
||||
authToken = results.getStringExtra(
|
||||
AccountManager.KEY_AUTHTOKEN);
|
||||
Log.w(MyTracksConstants.TAG, "authResult: " + authToken);
|
||||
} else {
|
||||
Log.e(MyTracksConstants.TAG, "No auth result results!!");
|
||||
}
|
||||
runWhenFinished();
|
||||
return authToken != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current auth token. Response may be null if no valid auth
|
||||
* token has been fetched.
|
||||
*
|
||||
* @return The current auth token or null if no auth token has been
|
||||
* fetched
|
||||
*/
|
||||
public String getAuthToken() {
|
||||
return authToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidates the existing auth token and request a new one. The
|
||||
* {@link Runnable} provided will be executed when the new auth token is
|
||||
* successfully fetched.
|
||||
*
|
||||
* @param runnable A {@link Runnable} to execute when a new auth token
|
||||
* is successfully fetched
|
||||
*/
|
||||
public void invalidateAndRefresh(final Runnable runnable) {
|
||||
this.whenFinished = runnable;
|
||||
|
||||
activity.runOnUiThread(new Runnable() {
|
||||
public void run() {
|
||||
accountManager.invalidateAuthToken(MyTracksConstants.ACCOUNT_TYPE,
|
||||
authToken);
|
||||
MyTracks.getInstance().getAccountChooser().chooseAccount(activity,
|
||||
new AccountChooser.AccountHandler() {
|
||||
@Override
|
||||
public void handleAccountSelected(Account account) {
|
||||
if (account != null) {
|
||||
doLogin(whenFinished, account);
|
||||
} else {
|
||||
runWhenFinished();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void runWhenFinished() {
|
||||
if (whenFinished != null) {
|
||||
(new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
whenFinished.run();
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,525 @@
|
||||
/*
|
||||
* Copyright 2008 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
package com.google.android.apps.mytracks.io;
|
||||
|
||||
import com.google.android.apps.mymaps.MyMapsConstants;
|
||||
import com.google.android.apps.mytracks.MyTracks;
|
||||
import com.google.android.apps.mytracks.MyTracksConstants;
|
||||
import com.google.android.apps.mytracks.MyTracksSettings;
|
||||
import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
|
||||
import com.google.android.apps.mytracks.content.Track;
|
||||
import com.google.android.apps.mytracks.io.gdata.GDataClientFactory;
|
||||
import com.google.android.apps.mytracks.io.gdata.GDataWrapper;
|
||||
import com.google.android.apps.mytracks.io.gdata.GDataWrapper.QueryFunction;
|
||||
import com.google.android.apps.mytracks.util.ResourceUtils;
|
||||
import com.google.android.apps.mytracks.util.StringUtils;
|
||||
import com.google.android.apps.mytracks.util.UnitConversions;
|
||||
import com.google.android.common.gdata.AndroidXmlParserFactory;
|
||||
import com.google.android.maps.mytracks.R;
|
||||
import com.google.wireless.gdata.client.GDataClient;
|
||||
import com.google.wireless.gdata.client.GDataServiceClient;
|
||||
import com.google.wireless.gdata.client.HttpException;
|
||||
import com.google.wireless.gdata.data.Entry;
|
||||
import com.google.wireless.gdata.docs.DocumentsClient;
|
||||
import com.google.wireless.gdata.docs.SpreadsheetsClient;
|
||||
import com.google.wireless.gdata.docs.SpreadsheetsClient.WorksheetEntry;
|
||||
import com.google.wireless.gdata.maps.xml.XmlMapsGDataParserFactory;
|
||||
import com.google.wireless.gdata.parser.GDataParser;
|
||||
import com.google.wireless.gdata.parser.ParseException;
|
||||
import com.google.wireless.gdata2.client.AuthenticationException;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Handler;
|
||||
import android.os.HandlerThread;
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStream;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.net.URL;
|
||||
import java.net.URLConnection;
|
||||
import java.text.DecimalFormat;
|
||||
import java.text.NumberFormat;
|
||||
|
||||
/**
|
||||
* A helper class used to transmit tracks statistics to Google Docs/Trix.
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
*/
|
||||
public class SendToDocs {
|
||||
private static final NumberFormat LARGE_UNIT_FORMAT =
|
||||
new DecimalFormat("#,###,###.00");
|
||||
private static final NumberFormat SMALL_UNIT_FORMAT =
|
||||
new DecimalFormat("###,###");
|
||||
private static final String DOCS_FEED_URL =
|
||||
"http://docs.google.com/feeds/documents/private/full";
|
||||
private static final String DOCS_MY_SPREADSHEETS_FEED_URL =
|
||||
"http://docs.google.com/feeds/documents/private/full?"
|
||||
+ "category=mine,spreadsheet";
|
||||
private static final String DOCS_SPREADSHEET_URL =
|
||||
"http://docs.google.com/feeds/documents/private/full/spreadsheet%3A";
|
||||
private static final String DOCS_SPREADSHEET_URL_FORMAT =
|
||||
"http://spreadsheets.google.com/feeds/list/%s/%s/private/full";
|
||||
protected static final String DOCS_WORKSHEETS_URL_FORMAT =
|
||||
"http://spreadsheets.google.com/feeds/worksheets/%s/private/full";
|
||||
private static final String CONTENT_TYPE_PARAM = "Content-Type";
|
||||
private 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 final Activity activity;
|
||||
private final AuthManager wiseAuth;
|
||||
private final AuthManager writelyAuth;
|
||||
private final long trackId;
|
||||
private final boolean metricUnits;
|
||||
private final HandlerThread handlerThread;
|
||||
private final Handler handler;
|
||||
|
||||
private boolean createdNewSpreadSheet = false;
|
||||
|
||||
private String spreadSheetId = null;
|
||||
private String workSheetId = null;
|
||||
|
||||
private boolean success = true;
|
||||
private String statusMessage = "";
|
||||
private Runnable onCompletion = null;
|
||||
|
||||
public SendToDocs(Activity activity, AuthManager wiseAuth,
|
||||
AuthManager writelyAuth, long trackId) {
|
||||
this.activity = activity;
|
||||
this.wiseAuth = wiseAuth;
|
||||
this.writelyAuth = writelyAuth;
|
||||
this.trackId = trackId;
|
||||
|
||||
SharedPreferences preferences = activity.getSharedPreferences(
|
||||
MyTracksSettings.SETTINGS_NAME, 0);
|
||||
if (preferences != null) {
|
||||
metricUnits = preferences.getBoolean(MyTracksSettings.METRIC_UNITS, true);
|
||||
} else {
|
||||
metricUnits = true;
|
||||
}
|
||||
|
||||
Log.d(MyTracksConstants.TAG,
|
||||
"Sending to Google Docs: trackId = " + trackId);
|
||||
handlerThread = new HandlerThread("SendToGoogleDocs");
|
||||
handlerThread.start();
|
||||
handler = new Handler(handlerThread.getLooper());
|
||||
}
|
||||
|
||||
public void run() {
|
||||
handler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
doUpload();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void doUpload() {
|
||||
statusMessage = activity.getString(R.string.error_sending_to_mymap);
|
||||
success = false;
|
||||
|
||||
try {
|
||||
if (trackId == -1) {
|
||||
Log.w(MyTracksConstants.TAG, "Cannot get track id.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the track from the provider:
|
||||
Track track =
|
||||
MyTracksProviderUtils.Factory.get(activity).getTrack(trackId);
|
||||
if (track == null) {
|
||||
Log.w(MyTracksConstants.TAG, "Cannot get track.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Transmit track stats via GData feed:
|
||||
// -------------------------------
|
||||
|
||||
Log.d(MyTracksConstants.TAG, "SendToDocs: Uploading to spreadsheet");
|
||||
success = uploadToDocs(track);
|
||||
if (success) {
|
||||
if (createdNewSpreadSheet) {
|
||||
statusMessage = activity.getString(
|
||||
R.string.status_tracks_have_been_uploaded_to_new_doc);
|
||||
} else {
|
||||
statusMessage = activity.getString(
|
||||
R.string.status_tracks_have_been_uploaded_to_docs);
|
||||
}
|
||||
} else {
|
||||
statusMessage = activity.getString(R.string.error_sending_to_docs);
|
||||
}
|
||||
Log.d(MyTracksConstants.TAG, "SendToDocs: Done.");
|
||||
} finally {
|
||||
if (onCompletion != null) {
|
||||
activity.runOnUiThread(onCompletion);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public boolean wasSuccess() {
|
||||
return success;
|
||||
}
|
||||
|
||||
public String getStatusMessage() {
|
||||
return statusMessage;
|
||||
}
|
||||
|
||||
public void setOnCompletion(Runnable onCompletion) {
|
||||
this.onCompletion = onCompletion;
|
||||
}
|
||||
|
||||
/**
|
||||
* Uploads the statistics about a track to Google Docs using the docs GData
|
||||
* feed.
|
||||
*
|
||||
* @param track the track
|
||||
*/
|
||||
private boolean uploadToDocs(Track track) {
|
||||
GDataWrapper wiseWrapper = new GDataWrapper();
|
||||
wiseWrapper.setAuthManager(wiseAuth);
|
||||
wiseWrapper.setRetryOnAuthFailure(true);
|
||||
|
||||
GDataWrapper writelyWrapper = new GDataWrapper();
|
||||
writelyWrapper.setAuthManager(writelyAuth);
|
||||
writelyWrapper.setRetryOnAuthFailure(true);
|
||||
|
||||
GDataClient androidClient = null;
|
||||
try {
|
||||
androidClient = GDataClientFactory.getGDataClient(activity);
|
||||
SpreadsheetsClient gdataClient = new SpreadsheetsClient(androidClient,
|
||||
new XmlMapsGDataParserFactory(new AndroidXmlParserFactory()));
|
||||
wiseWrapper.setClient(androidClient, gdataClient);
|
||||
Log.d(MyTracksConstants.TAG,
|
||||
"GData connection prepared: " + this.writelyAuth);
|
||||
String sheetTitle = "My Tracks";
|
||||
|
||||
if (track.getCategory() != null && !track.getCategory().equals("")) {
|
||||
sheetTitle += "-" + track.getCategory();
|
||||
}
|
||||
|
||||
DocumentsClient docsGdataClient = new DocumentsClient(androidClient,
|
||||
new XmlMapsGDataParserFactory(new AndroidXmlParserFactory()));
|
||||
writelyWrapper.setClient(androidClient, docsGdataClient);
|
||||
|
||||
// First try to find the spreadsheet:
|
||||
if (!getSpreadsheetId(writelyWrapper, sheetTitle)) {
|
||||
Log.i(MyTracksConstants.TAG, "Spreadsheet lookup failed.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (spreadSheetId == null) {
|
||||
MyTracks.getInstance().setProgressValue(65);
|
||||
// Waiting a few seconds and trying again. Maybe the server just had a
|
||||
// hickup (unfortunately that happens quite a lot...).
|
||||
try {
|
||||
Thread.sleep(5000);
|
||||
} catch (InterruptedException e) {
|
||||
Log.e(MyTracksConstants.TAG, "Sleep interrupted", e);
|
||||
}
|
||||
if (!getSpreadsheetId(writelyWrapper, sheetTitle)) {
|
||||
Log.i(MyTracksConstants.TAG, "2nd spreadsheet lookup failed.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
MyTracks.getInstance().setProgressValue(70);
|
||||
if (spreadSheetId == null) {
|
||||
Log.i(MyTracksConstants.TAG, "Creating new spreadsheet: " + sheetTitle);
|
||||
|
||||
if (!createSpreadSheet(writelyWrapper, sheetTitle)) {
|
||||
return false;
|
||||
}
|
||||
MyTracks.getInstance().setProgressValue(80);
|
||||
|
||||
if (spreadSheetId == null) {
|
||||
MyTracks.getInstance().setProgressValue(85);
|
||||
// The previous creation might have succeeded even though GData
|
||||
// reported an error. Seems to be a know bug,
|
||||
// see http://code.google.com/p/gdata-issues/issues/detail?id=929
|
||||
// Try to find the created spreadsheet:
|
||||
Log.w(MyTracksConstants.TAG,
|
||||
"Create might have failed. Trying to find created document.");
|
||||
try {
|
||||
Thread.sleep(5000);
|
||||
} catch (InterruptedException e) {
|
||||
Log.e(MyTracksConstants.TAG, "Sleep interrupted", e);
|
||||
}
|
||||
if (!getSpreadsheetId(writelyWrapper, sheetTitle)) {
|
||||
return false;
|
||||
}
|
||||
if (spreadSheetId == null) {
|
||||
MyTracks.getInstance().setProgressValue(87);
|
||||
// Re-try
|
||||
try {
|
||||
Thread.sleep(5000);
|
||||
} catch (InterruptedException e) {
|
||||
Log.e(MyTracksConstants.TAG, "Sleep interrupted", e);
|
||||
}
|
||||
if (!getSpreadsheetId(writelyWrapper, sheetTitle)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (spreadSheetId == null) {
|
||||
Log.i(MyTracksConstants.TAG,
|
||||
"Creating new spreadsheet really failed.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!getWorkSheetId(wiseWrapper)) {
|
||||
Log.i(MyTracksConstants.TAG, "Looking up worksheet id failed.");
|
||||
return false;
|
||||
}
|
||||
|
||||
MyTracks.getInstance().setProgressValue(90);
|
||||
|
||||
insertRowNet(track, spreadSheetId, workSheetId);
|
||||
Log.i(MyTracksConstants.TAG, "Done uploading to docs.");
|
||||
} catch (IOException e) {
|
||||
Log.e(MyTracksConstants.TAG, "Unable to upload docs.", e);
|
||||
return false;
|
||||
} finally {
|
||||
if (androidClient != null) {
|
||||
androidClient.close();
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean getSpreadsheetId(GDataWrapper wrapper, final String title) {
|
||||
spreadSheetId = null;
|
||||
return wrapper.runQuery(new QueryFunction() {
|
||||
@Override
|
||||
public void query(GDataServiceClient client)
|
||||
throws IOException, ParseException, HttpException {
|
||||
GDataParser listParser;
|
||||
listParser = client.getParserForFeed(Entry.class,
|
||||
DOCS_MY_SPREADSHEETS_FEED_URL, writelyAuth.getAuthToken());
|
||||
listParser.init();
|
||||
while (listParser.hasMoreData()) {
|
||||
Entry entry = listParser.readNextEntry(null);
|
||||
String entryTitle = entry.getTitle();
|
||||
Log.i(MyTracksConstants.TAG, "Found docs entry: " + entryTitle);
|
||||
if (entryTitle.equals(title)) {
|
||||
String entryId = entry.getId();
|
||||
int lastSlash = entryId.lastIndexOf('/');
|
||||
spreadSheetId = entryId.substring(lastSlash + 15);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private boolean getWorkSheetId(GDataWrapper wrapper) {
|
||||
workSheetId = null;
|
||||
return wrapper.runQuery(new QueryFunction() {
|
||||
@Override
|
||||
public void query(GDataServiceClient client)
|
||||
throws AuthenticationException, IOException, ParseException {
|
||||
String uri = String.format(DOCS_WORKSHEETS_URL_FORMAT, spreadSheetId);
|
||||
GDataParser sheetParser =
|
||||
((SpreadsheetsClient) client).getParserForWorksheetsFeed(uri,
|
||||
wiseAuth.getAuthToken());
|
||||
sheetParser.init();
|
||||
if (!sheetParser.hasMoreData()) {
|
||||
Log.i(MyTracksConstants.TAG, "Found no worksheets");
|
||||
return; // failure
|
||||
}
|
||||
|
||||
// just grab the first.
|
||||
WorksheetEntry worksheetEntry =
|
||||
(WorksheetEntry) sheetParser.readNextEntry(new WorksheetEntry());
|
||||
|
||||
int lastSlash = worksheetEntry.getId().lastIndexOf('/');
|
||||
workSheetId = worksheetEntry.getId().substring(lastSlash + 1);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new MyTracks spreadsheet with the given name.
|
||||
*/
|
||||
private boolean createSpreadSheet(GDataWrapper writelyWrapper,
|
||||
final String name) {
|
||||
spreadSheetId = null;
|
||||
|
||||
return writelyWrapper.runQuery(new QueryFunction() {
|
||||
@Override
|
||||
public void query(GDataServiceClient client) throws IOException {
|
||||
// Construct data
|
||||
// Send data
|
||||
URL url = new URL(DOCS_FEED_URL);
|
||||
URLConnection conn = url.openConnection();
|
||||
conn.addRequestProperty(CONTENT_TYPE_PARAM,
|
||||
OPENDOCUMENT_SPREADSHEET_MIME_TYPE);
|
||||
conn.addRequestProperty("Slug", name);
|
||||
conn.addRequestProperty("Authorization",
|
||||
"GoogleLogin auth=" + writelyAuth.getAuthToken());
|
||||
conn.setDoOutput(true);
|
||||
OutputStream os = conn.getOutputStream();
|
||||
ResourceUtils.readBinaryFileToOutputStream(
|
||||
activity, R.raw.mytracks_empty_spreadsheet, os);
|
||||
|
||||
// Get the response
|
||||
// TODO: The following is a horrible ugly hack.
|
||||
// Hopefully we can retire it when there is a proper gdata api.
|
||||
BufferedReader rd = null;
|
||||
String line;
|
||||
StringBuilder resultBuilder = new StringBuilder();
|
||||
try {
|
||||
rd = new BufferedReader(new InputStreamReader(conn.getInputStream()));
|
||||
while ((line = rd.readLine()) != null) {
|
||||
resultBuilder.append(line);
|
||||
}
|
||||
os.close();
|
||||
rd.close();
|
||||
} catch (FileNotFoundException e) {
|
||||
// The GData API sometimes throws an error, even though creation of
|
||||
// the document succeeded. In that case let's just return. The caller
|
||||
// then needs to check if the doc actually exists.
|
||||
return;
|
||||
} finally {
|
||||
os.close();
|
||||
if (rd != null) {
|
||||
rd.close();
|
||||
}
|
||||
}
|
||||
|
||||
String result = resultBuilder.toString();
|
||||
// Try to find the id.
|
||||
int idTagIndex = result.indexOf("<id>");
|
||||
if (idTagIndex == -1) {
|
||||
return;
|
||||
}
|
||||
int idTagCloseIndex = result.indexOf("</id>", idTagIndex);
|
||||
if (idTagCloseIndex == -1) {
|
||||
return;
|
||||
}
|
||||
int idStringStart = result.indexOf(DOCS_SPREADSHEET_URL, idTagIndex);
|
||||
if (idStringStart == -1) {
|
||||
return;
|
||||
}
|
||||
spreadSheetId = result.substring(
|
||||
idStringStart + DOCS_SPREADSHEET_URL.length(), idTagCloseIndex);
|
||||
Log.i(MyTracksConstants.TAG, "Created new spreadsheet: " + spreadSheetId);
|
||||
}});
|
||||
}
|
||||
|
||||
private void insertRowNet(Track track, String spreadsheetId,
|
||||
String worksheetId) throws IOException {
|
||||
String worksheetUri = String.format(DOCS_SPREADSHEET_URL_FORMAT,
|
||||
spreadsheetId, worksheetId);
|
||||
|
||||
/* Prepare the Post-Text we are going to send. */
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("<entry xmlns='http://www.w3.org/2005/Atom' ");
|
||||
sb.append("xmlns:gsx='http://schemas.google.com/spreadsheets/"
|
||||
+ "2006/extended'>");
|
||||
appendTag("name", track.getName(), sb);
|
||||
appendTag("description", track.getDescription(), sb);
|
||||
appendTag("date", String.format("%tc", track.getStartTime()), sb);
|
||||
appendTag("totaltime", StringUtils.formatTimeAlwaysShowingHours(
|
||||
track.getTotalTime()), sb);
|
||||
appendTag("movingtime", StringUtils.formatTimeAlwaysShowingHours(
|
||||
track.getMovingTime()), sb);
|
||||
appendLargeUnitsTag("distance", track.getTotalDistance() / 1000, sb);
|
||||
appendTag("distanceunit",
|
||||
metricUnits
|
||||
? activity.getString(R.string.kilometer)
|
||||
: activity.getString(R.string.mile),
|
||||
sb);
|
||||
appendLargeUnitsTag("averagespeed", track.getAverageSpeed() * 3.6, sb);
|
||||
appendLargeUnitsTag("averagemovingspeed",
|
||||
track.getAverageMovingSpeed() * 3.6, sb);
|
||||
appendLargeUnitsTag("maxspeed", track.getMaxSpeed() * 3.6, sb);
|
||||
appendTag("speedunit",
|
||||
metricUnits
|
||||
? activity.getString(R.string.kilometer_per_hour)
|
||||
: activity.getString(R.string.mile_per_hour),
|
||||
sb);
|
||||
appendSmallUnitsTag("elevationgain", track.getTotalElevationGain(), sb);
|
||||
appendSmallUnitsTag("minelevation", track.getMinElevation(), sb);
|
||||
appendSmallUnitsTag("maxelevation", track.getMaxElevation(), sb);
|
||||
appendTag("elevationunit",
|
||||
metricUnits
|
||||
? activity.getString(R.string.meter)
|
||||
: activity.getString(R.string.feet),
|
||||
sb);
|
||||
if (track.getMapId().length() > 0) {
|
||||
appendTag("map", MyMapsConstants.MAPSHOP_BASE_URL + "?msa=0&msid="
|
||||
+ track.getMapId(), sb);
|
||||
}
|
||||
sb.append("</entry>");
|
||||
Log.i(MyTracksConstants.TAG,
|
||||
"Inserting at: " + spreadsheetId + " => " + worksheetUri);
|
||||
|
||||
String postText = sb.toString();
|
||||
Log.i(MyTracksConstants.TAG, postText);
|
||||
|
||||
// Send data
|
||||
// No need for a wrapper because we know that the authorization was good
|
||||
// enough to get this far
|
||||
URL url = new URL(worksheetUri);
|
||||
URLConnection conn = url.openConnection();
|
||||
conn.addRequestProperty(CONTENT_TYPE_PARAM, ATOM_FEED_MIME_TYPE);
|
||||
conn.addRequestProperty("Authorization",
|
||||
"GoogleLogin auth=" + wiseAuth.getAuthToken());
|
||||
conn.setDoOutput(true);
|
||||
OutputStreamWriter wr = new OutputStreamWriter(conn.getOutputStream());
|
||||
wr.write(postText);
|
||||
wr.flush();
|
||||
|
||||
// Get the response
|
||||
BufferedReader rd =
|
||||
new BufferedReader(new InputStreamReader(conn.getInputStream()));
|
||||
String line;
|
||||
while ((line = rd.readLine()) != null) {
|
||||
// Process line...
|
||||
Log.i(MyTracksConstants.TAG, "r: " + line);
|
||||
}
|
||||
wr.close();
|
||||
rd.close();
|
||||
|
||||
Log.i(MyTracksConstants.TAG, "Post finished.");
|
||||
}
|
||||
|
||||
private void appendTag(String name, String value, StringBuilder sb) {
|
||||
sb.append("<gsx:");
|
||||
sb.append(name);
|
||||
sb.append('>');
|
||||
sb.append(StringUtils.stringAsCData(value));
|
||||
sb.append("</gsx:");
|
||||
sb.append(name);
|
||||
sb.append('>');
|
||||
}
|
||||
|
||||
private void appendLargeUnitsTag(String name, double d, StringBuilder sb) {
|
||||
double value = metricUnits ? d : (d * UnitConversions.KM_TO_MI);
|
||||
appendTag(name, LARGE_UNIT_FORMAT.format(value), sb);
|
||||
}
|
||||
|
||||
private void appendSmallUnitsTag(String name, double d, StringBuilder sb) {
|
||||
double value = metricUnits ? d : (d * UnitConversions.M_TO_FT);
|
||||
appendTag(name, SMALL_UNIT_FORMAT.format(value), sb);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,689 @@
|
||||
/*
|
||||
* Copyright 2008 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
package com.google.android.apps.mytracks.io;
|
||||
|
||||
import com.google.android.apps.mymaps.MyMapsFeature;
|
||||
import com.google.android.apps.mymaps.MyMapsGDataConverter;
|
||||
import com.google.android.apps.mymaps.MyMapsGDataWrapper;
|
||||
import com.google.android.apps.mymaps.MyMapsGDataWrapper.QueryFunction;
|
||||
import com.google.android.apps.mymaps.MyMapsMapMetadata;
|
||||
import com.google.android.apps.mytracks.MyTracksConstants;
|
||||
import com.google.android.apps.mytracks.MyTracksSettings;
|
||||
import com.google.android.apps.mytracks.ProgressIndicator;
|
||||
import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
|
||||
import com.google.android.apps.mytracks.content.Track;
|
||||
import com.google.android.apps.mytracks.content.TrackBuffer;
|
||||
import com.google.android.apps.mytracks.content.Waypoint;
|
||||
import com.google.android.apps.mytracks.stats.DoubleBuffer;
|
||||
import com.google.android.apps.mytracks.util.MyTracksUtils;
|
||||
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.wireless.gdata.client.HttpException;
|
||||
import com.google.wireless.gdata.data.Entry;
|
||||
import com.google.wireless.gdata.maps.MapsClient;
|
||||
import com.google.wireless.gdata.parser.ParseException;
|
||||
import com.google.wireless.gdata2.client.AuthenticationException;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.database.Cursor;
|
||||
import android.location.Location;
|
||||
import android.os.Handler;
|
||||
import android.os.HandlerThread;
|
||||
import android.util.Log;
|
||||
|
||||
import org.xmlpull.v1.XmlPullParserException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Vector;
|
||||
|
||||
/**
|
||||
* A helper class used to transmit tracks to Google MyMaps.
|
||||
*
|
||||
* @author Leif Hendrik Wilden
|
||||
*/
|
||||
public class SendToMyMaps {
|
||||
|
||||
private static final String END_ICON_URL =
|
||||
"http://maps.google.com/mapfiles/ms/micons/red-dot.png";
|
||||
private static final String START_ICON_URL =
|
||||
"http://maps.google.com/mapfiles/ms/micons/green-dot.png";
|
||||
|
||||
private final Activity context;
|
||||
private final AuthManager auth;
|
||||
private final long trackId;
|
||||
private final ProgressIndicator progressIndicator;
|
||||
private final StringUtils stringUtils;
|
||||
private final MyTracksProviderUtils providerUtils;
|
||||
private String mapId;
|
||||
|
||||
private String statusMessage = "";
|
||||
private boolean success = true;
|
||||
private Runnable onCompletion;
|
||||
|
||||
private HandlerThread handlerThread;
|
||||
private Handler handler;
|
||||
|
||||
private String originalDescription;
|
||||
|
||||
private MyMapsGDataWrapper wrapper;
|
||||
private MyMapsGDataConverter gdataConverter;
|
||||
|
||||
private Location last = null;
|
||||
|
||||
private Vector<Double> distances;
|
||||
private Vector<Double> elevations;
|
||||
private DoubleBuffer elevationBuffer;
|
||||
|
||||
/**
|
||||
* Prepares a buffer of locations for transmission to google maps.
|
||||
*
|
||||
* @param track the original track with meta data
|
||||
* @param buffer a buffer of locations on the track
|
||||
* @return an array of tracks each with a sub section of the points in the
|
||||
* original buffer
|
||||
*/
|
||||
private ArrayList<Track> prepareLocations(Track track,
|
||||
TrackBuffer buffer) {
|
||||
ArrayList<Track> splitTracks = new ArrayList<Track>();
|
||||
|
||||
// Create segments from each full track:
|
||||
Track segment = new Track();
|
||||
segment.setId(track.getId());
|
||||
segment.setName(track.getName());
|
||||
segment.setDescription(/* track.getDescription() */ "");
|
||||
segment.setCategory(track.getCategory());
|
||||
segment.setStartTime(track.getStartTime());
|
||||
segment.setStopTime(track.getStopTime());
|
||||
boolean startNewTrackSegment = false;
|
||||
for (int i = 0; i < buffer.getLocationsLoaded(); ++i) {
|
||||
Location loc = buffer.get(i);
|
||||
if (loc.getLatitude() > 90) {
|
||||
startNewTrackSegment = true;
|
||||
}
|
||||
|
||||
if (startNewTrackSegment) {
|
||||
// Close up the last segment.
|
||||
prepareTrackSegment(segment, splitTracks);
|
||||
|
||||
Log.d(MyTracksConstants.TAG,
|
||||
"MyTracksSendToMyMaps: Starting new track segment...");
|
||||
startNewTrackSegment = false;
|
||||
segment = new Track();
|
||||
segment.setId(track.getId());
|
||||
segment.setName(track.getName());
|
||||
segment.setDescription(/* track.getDescription() */ "");
|
||||
segment.setCategory(track.getCategory());
|
||||
}
|
||||
|
||||
if (loc.getLatitude() <= 90) {
|
||||
segment.addLocation(loc);
|
||||
if (segment.getStartTime() < 0) {
|
||||
segment.setStartTime(loc.getTime());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
prepareTrackSegment(segment, splitTracks);
|
||||
|
||||
return splitTracks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares a track segment for sending to google maps.
|
||||
* The main steps are:
|
||||
* - correcting end time
|
||||
* - decimating locations
|
||||
* - splitting into smaller tracks.
|
||||
*
|
||||
* The final track pieces will be put in the array list splitTracks.
|
||||
*
|
||||
* @param segment the original segment of the track
|
||||
* @param splitTracks an array of smaller track segments
|
||||
*/
|
||||
private void prepareTrackSegment(
|
||||
Track segment, ArrayList<Track> splitTracks) {
|
||||
if (segment.getStopTime() < 0 && segment.getLocations().size() > 0) {
|
||||
segment.setStopTime(segment.getLocations().size() - 1);
|
||||
}
|
||||
|
||||
/*
|
||||
* Decimate to 2 meter precision. Mapshop doesn't like too many
|
||||
* points:
|
||||
*/
|
||||
MyTracksUtils.decimate(segment, 2.0);
|
||||
|
||||
/* It the track still has > 500 points, split it in pieces: */
|
||||
if (segment.getLocations().size() > 500) {
|
||||
splitTracks.addAll(MyTracksUtils.split(segment, 500));
|
||||
} else if (segment.getLocations().size() >= 2) {
|
||||
splitTracks.add(segment);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts a place mark. Second try if 1st try fails. Will throw exception on
|
||||
* 2nd failure.
|
||||
*/
|
||||
private void insertMarker(Context context, MapsClient client,
|
||||
String featureFeed, Track track, Location loc,
|
||||
boolean isStart) throws IOException, Exception {
|
||||
Entry entry = gdataConverter.getEntryForFeature(
|
||||
buildMyMapsPlacemarkFeature(context, track, loc, isStart));
|
||||
Log.d(MyTracksConstants.TAG, "SendToMyMaps: Creating placemark "
|
||||
+ entry.getTitle());
|
||||
try {
|
||||
client.createEntry(featureFeed, auth.getAuthToken(), entry);
|
||||
Log.d(MyTracksConstants.TAG, "SendToMyMaps: createEntry success!");
|
||||
} catch (IOException e) {
|
||||
Log.w(MyTracksConstants.TAG,
|
||||
"SendToMyMaps: createEntry 1st try failed. Trying again.");
|
||||
// Retry once (often IOException is thrown on a timeout):
|
||||
client.createEntry(featureFeed, auth.getAuthToken(), entry);
|
||||
Log.d(MyTracksConstants.TAG,
|
||||
"SendToMyMaps: createEntry success on 2nd try!");
|
||||
}
|
||||
}
|
||||
|
||||
private boolean uploadMarker(final Track track,
|
||||
final Location location,
|
||||
final boolean isStart) {
|
||||
boolean okay = wrapper.runQuery(new QueryFunction() {
|
||||
@Override
|
||||
public void query(MapsClient client)
|
||||
throws AuthenticationException, IOException, Exception {
|
||||
String featureFeed = MapsClient.getFeaturesFeed(mapId);
|
||||
insertMarker(context, client, featureFeed, track, location, isStart);
|
||||
}
|
||||
});
|
||||
return okay;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the current upload progress.
|
||||
*
|
||||
* @param track The track being uploaded
|
||||
* @param totalLocationsRead The total number of locations already read and
|
||||
* uploaded
|
||||
* @param segmentSize The percent of total that each segment corresponds to
|
||||
* @param progressInSegment The approximate progress in this segment
|
||||
*/
|
||||
private void setProgress(final Track track, int totalLocationsRead,
|
||||
double segmentSize, double progressInSegment) {
|
||||
long totalLocations = track.getStopId() - track.getStartId();
|
||||
|
||||
// The percent of the total that represents the completed part of this
|
||||
// segment.
|
||||
double percentSegmentInTotal = (segmentSize * progressInSegment);
|
||||
|
||||
int progress = (int) ((100.0 * totalLocationsRead / totalLocations)
|
||||
+ percentSegmentInTotal);
|
||||
progress = Math.min(99, progress);
|
||||
progressIndicator.setProgressValue(progress);
|
||||
}
|
||||
|
||||
private boolean uploadAllTrackPoints(final Track track) {
|
||||
int totalLocationsRead = 0;
|
||||
// TODO: This might not actually be accurate.
|
||||
long totalLocations = track.getStopId() - track.getStartId();
|
||||
|
||||
// Limit the number of elevation readings. Ideally we would want around 250.
|
||||
int elevationSamplingFrequency =
|
||||
Math.max(1, (int) (totalLocations / 250.0));
|
||||
Log.d(MyTracksConstants.TAG,
|
||||
"Using elevation sampling factor: " + elevationSamplingFrequency
|
||||
+ " on " + totalLocations);
|
||||
double totalDistance = 0;
|
||||
Location lastLocation = null;
|
||||
|
||||
TrackBuffer buffer = new TrackBuffer((int) Math.min(2048, totalLocations));
|
||||
int part = 1;
|
||||
|
||||
SharedPreferences preferences = context.getSharedPreferences(
|
||||
MyTracksSettings.SETTINGS_NAME, 0);
|
||||
boolean metricUnits = true;
|
||||
if (preferences != null) {
|
||||
metricUnits = preferences.getBoolean(MyTracksSettings.METRIC_UNITS, true);
|
||||
}
|
||||
|
||||
// For the progress monitor:
|
||||
// For each track segment 33% = read, 33% = prep & 33% = upload
|
||||
// Note: These numbers are very rough.
|
||||
|
||||
// The size of each segment in the 100% progress
|
||||
double segmentSize = buffer.getSize() * 100.0 / totalLocations;
|
||||
while (success && buffer.getLastLocationRead() < track.getStopId()) {
|
||||
setProgress(track, totalLocationsRead, segmentSize, 0.0);
|
||||
progressIndicator.setProgressMessage(
|
||||
context.getString(R.string.progress_message_reading_track));
|
||||
|
||||
providerUtils.getTrackPoints(track, buffer);
|
||||
if (buffer.getLocationsLoaded() <= 0) {
|
||||
continue;
|
||||
}
|
||||
if (totalLocationsRead == 0) {
|
||||
// Put a marker at the first point of the first valid segment:
|
||||
uploadMarker(track, buffer.get(0), true);
|
||||
}
|
||||
last = buffer.get(buffer.getLocationsLoaded() - 1);
|
||||
|
||||
// Extract the elevation profile.
|
||||
for (int i = 0; i < buffer.getLocationsLoaded(); i++) {
|
||||
// Add all points to the smoothing buffer.
|
||||
Location l = buffer.get(i);
|
||||
if (l != null && MyTracksUtils.isValidLocation(l)) {
|
||||
elevationBuffer.setNext(metricUnits ? l.getAltitude()
|
||||
: l.getAltitude() * UnitConversions.M_TO_FT);
|
||||
if (lastLocation != null) {
|
||||
double dist = lastLocation.distanceTo(l);
|
||||
totalDistance += dist;
|
||||
}
|
||||
lastLocation = l;
|
||||
|
||||
// Only add some data points to keep the url short.
|
||||
if (i % elevationSamplingFrequency == 0) {
|
||||
distances.add(totalDistance);
|
||||
elevations.add(elevationBuffer.getAverage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setProgress(track, totalLocationsRead, segmentSize, 0.3333);
|
||||
progressIndicator.setProgressMessage(
|
||||
context.getString(R.string.progress_message_preparing_track));
|
||||
|
||||
ArrayList<Track> splitTracks = prepareLocations(track, buffer);
|
||||
|
||||
int i = 0;
|
||||
for (Track t : splitTracks) {
|
||||
if (part > 1) {
|
||||
t.setName(t.getName() + " "
|
||||
+ String.format(context.getString(R.string.part), part));
|
||||
}
|
||||
part++;
|
||||
Log.d(MyTracksConstants.TAG,
|
||||
"SendToMyMaps: Prepared feature for upload w/ "
|
||||
+ t.getLocations().size() + " points.");
|
||||
|
||||
//
|
||||
// Transmit tracks via GData feed:
|
||||
// -------------------------------
|
||||
Log.d(MyTracksConstants.TAG,
|
||||
"SendToMyMaps: Uploading to map " + mapId + " w/ auth " + auth);
|
||||
if (!uploadTrackPoints(track, t,
|
||||
totalLocationsRead, segmentSize)) {
|
||||
success = false;
|
||||
return false;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
totalLocationsRead += buffer.getLocationsLoaded();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Uploads a given list of tracks to Google MyMaps using the maps GData feed.
|
||||
*
|
||||
* @param track the track meta-data is used for the map name
|
||||
*/
|
||||
private boolean uploadTrackPoints(final Track track, final Track currentTrack,
|
||||
int totalLocationsRead, double segmentSize) {
|
||||
setProgress(track, totalLocationsRead, segmentSize, 0.6666);
|
||||
progressIndicator.setProgressMessage(
|
||||
context.getString(R.string.progress_message_sending_mymaps));
|
||||
return wrapper.runQuery(new QueryFunction() {
|
||||
@Override
|
||||
public void query(MapsClient client)
|
||||
throws AuthenticationException, IOException, Exception {
|
||||
String featureFeed = MapsClient.getFeaturesFeed(mapId);
|
||||
Log.d(MyTracksConstants.TAG, "Feature feed url: " + featureFeed);
|
||||
uploadTrackPoints(track, currentTrack, client, featureFeed);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new map for the given track.
|
||||
*
|
||||
* @param track The track that will be uploaded to this map
|
||||
* @return True on success.
|
||||
*/
|
||||
private boolean createNewMap(final Track track) {
|
||||
progressIndicator.setProgressMessage(context
|
||||
.getString(R.string.progress_message_creating_map));
|
||||
return wrapper.runQuery(new QueryFunction() {
|
||||
@Override
|
||||
public void query(MapsClient client) throws IOException, Exception {
|
||||
Log.d(MyTracksConstants.TAG, "Creating a new map.");
|
||||
String mapFeed = MapsClient.getMapsFeed();
|
||||
Log.d(MyTracksConstants.TAG, "Map feed is " + mapFeed);
|
||||
MyMapsMapMetadata metaData = new MyMapsMapMetadata();
|
||||
metaData.setTitle(track.getName());
|
||||
metaData.setDescription(originalDescription + " - "
|
||||
+ track.getCategory() + " - "
|
||||
+ context.getString(R.string.new_map_description));
|
||||
SharedPreferences preferences = context.getSharedPreferences(
|
||||
MyTracksSettings.SETTINGS_NAME, 0);
|
||||
boolean mapPublic = true;
|
||||
if (preferences != null) {
|
||||
mapPublic = preferences.getBoolean(
|
||||
MyTracksSettings.DEFAULT_MAP_PUBLIC, true);
|
||||
}
|
||||
metaData.setSearchable(mapPublic);
|
||||
Entry entry = MyMapsGDataConverter.getMapEntryForMetadata(metaData);
|
||||
Log.d(MyTracksConstants.TAG, "Title: " + entry.getTitle());
|
||||
Entry map = client.createEntry(mapFeed, auth.getAuthToken(), entry);
|
||||
mapId = MapsClient.getMapIdFromMapEntryId(map.getId());
|
||||
Log.d(MyTracksConstants.TAG, "New map id is: " + mapId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private boolean uploadTrackPoints(final Track track,
|
||||
Track splitTrack,
|
||||
MapsClient client,
|
||||
String featureFeed)
|
||||
throws IOException, Exception {
|
||||
Entry entry = null;
|
||||
if (splitTrack.getId() != track.getId()
|
||||
|| splitTrack.getLocations().size() < 2) {
|
||||
// Need at least two points for a polyline:
|
||||
return true;
|
||||
}
|
||||
|
||||
// Put the line:
|
||||
entry = gdataConverter.getEntryForFeature(
|
||||
buildMyMapsLineFeature(splitTrack));
|
||||
Log.d(MyTracksConstants.TAG,
|
||||
"SendToMyMaps: Creating line " + entry.getTitle());
|
||||
try {
|
||||
client.createEntry(featureFeed, auth.getAuthToken(), entry);
|
||||
Log.d(MyTracksConstants.TAG, "SendToMyMaps: createEntry success!");
|
||||
} catch (IOException e) {
|
||||
Log.w(MyTracksConstants.TAG,
|
||||
"SendToMyMaps: createEntry 1st try failed. Trying again.");
|
||||
// Retry once (often IOException is thrown on a timeout):
|
||||
client.createEntry(featureFeed, auth.getAuthToken(), entry);
|
||||
Log.d(MyTracksConstants.TAG,
|
||||
"SendToMyMaps: createEntry success on 2nd try!");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Uploads all of the waypoints associated with this track to map.
|
||||
*
|
||||
* @param track The track to upload waypoints for.
|
||||
*
|
||||
* @return True on success.
|
||||
*/
|
||||
private boolean uploadWaypoints(final Track track) {
|
||||
return wrapper.runQuery(new QueryFunction() {
|
||||
public void query(MapsClient client) {
|
||||
// TODO: Stream through he waypoints in chunks.
|
||||
// I am leaving the number of waypoints very high which should not be a
|
||||
// problem because we don't try to load them into objects all at the
|
||||
// same time.
|
||||
Cursor c = null;
|
||||
c = providerUtils.getWaypointsCursor(
|
||||
track.getId(), 0,
|
||||
MyTracksConstants.MAX_LOADED_WAYPOINTS_POINTS);
|
||||
String featureFeed = MapsClient.getFeaturesFeed(mapId);
|
||||
if (c != null) {
|
||||
try {
|
||||
if (c.moveToFirst()) {
|
||||
// This will skip the 1st waypoint (it carries the stats for the
|
||||
// last segment).
|
||||
while (c.moveToNext()) {
|
||||
Waypoint wpt = providerUtils.createWaypoint(c);
|
||||
Entry entry = gdataConverter.getEntryForFeature(
|
||||
buildMyMapsPlacemarkFeature(wpt));
|
||||
Log.d(MyTracksConstants.TAG,
|
||||
"SendToMyMaps: Creating waypoint.");
|
||||
try {
|
||||
client.createEntry(featureFeed, auth.getAuthToken(), entry);
|
||||
Log.d(MyTracksConstants.TAG,
|
||||
"SendToMyMaps: createEntry success!");
|
||||
} catch (IOException e) {
|
||||
Log.w(MyTracksConstants.TAG,
|
||||
"SendToMyMaps: createEntry 1st try failed. Retrying.");
|
||||
|
||||
// Retry once (often IOException is thrown on a timeout):
|
||||
client.createEntry(featureFeed, auth.getAuthToken(), entry);
|
||||
Log.d(MyTracksConstants.TAG,
|
||||
"SendToMyMaps: createEntry success on 2nd try!");
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (ParseException e) {
|
||||
Log.w(MyTracksConstants.TAG, "ParseException caught.", e);
|
||||
} catch (HttpException e) {
|
||||
Log.w(MyTracksConstants.TAG, "HttpException caught.", e);
|
||||
} catch (IOException e) {
|
||||
Log.w(MyTracksConstants.TAG, "IOException caught.", e);
|
||||
} finally {
|
||||
c.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public SendToMyMaps(Activity context, String mapId, AuthManager auth,
|
||||
long trackId, ProgressIndicator progressIndicator) {
|
||||
this.context = context;
|
||||
this.mapId = mapId;
|
||||
this.auth = auth;
|
||||
this.trackId = trackId;
|
||||
this.progressIndicator = progressIndicator;
|
||||
this.stringUtils = new StringUtils(context);
|
||||
this.providerUtils = MyTracksProviderUtils.Factory.get(context);
|
||||
|
||||
Log.d(MyTracksConstants.TAG, "Sending to MyMap: trackId = " + trackId);
|
||||
handlerThread = new HandlerThread("SendToMyMaps");
|
||||
handlerThread.start();
|
||||
handler = new Handler(handlerThread.getLooper());
|
||||
}
|
||||
|
||||
public void run() {
|
||||
handler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
doUpload();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void setOnCompletion(Runnable onCompletion) {
|
||||
this.onCompletion = onCompletion;
|
||||
}
|
||||
|
||||
public String getMapId() {
|
||||
return mapId;
|
||||
}
|
||||
|
||||
public boolean wasSuccess() {
|
||||
return success;
|
||||
}
|
||||
|
||||
public String getStatusMessage() {
|
||||
return statusMessage;
|
||||
}
|
||||
|
||||
private void doUpload() {
|
||||
statusMessage = context.getString(R.string.error_sending_to_mymap);
|
||||
success = true;
|
||||
try {
|
||||
gdataConverter = new MyMapsGDataConverter();
|
||||
|
||||
progressIndicator.setProgressValue(1);
|
||||
progressIndicator.setProgressMessage(
|
||||
context.getString(R.string.progress_message_reading_track));
|
||||
|
||||
// Get the track meta-data
|
||||
Track track = providerUtils.getTrack(trackId);
|
||||
originalDescription = track.getDescription();
|
||||
track.setDescription("<p>" + track.getDescription() + "</p><p>"
|
||||
+ stringUtils.generateTrackDescription(track, null, null) + "</p>");
|
||||
wrapper = new MyMapsGDataWrapper(context);
|
||||
wrapper.setAuthManager(auth);
|
||||
wrapper.setRetryOnAuthFailure(true);
|
||||
|
||||
distances = new Vector<Double>();
|
||||
elevations = new Vector<Double>();
|
||||
elevationBuffer =
|
||||
new DoubleBuffer(MyTracksConstants.ELEVATION_SMOOTHING_FACTOR);
|
||||
|
||||
// Create a new map if necessary:
|
||||
if (mapId.equals("new")) {
|
||||
if (!createNewMap(track)) {
|
||||
success = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Upload all of the segments of the track.
|
||||
if (success) {
|
||||
if (!uploadAllTrackPoints(track)) {
|
||||
success = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Start end end markers.
|
||||
if (success) {
|
||||
// Put an end marker at the last point of the last valid segment:
|
||||
if (last != null) {
|
||||
track.setDescription("<p>" + originalDescription + "</p><p>"
|
||||
+ stringUtils.generateTrackDescription(
|
||||
track, distances, elevations)
|
||||
+ "</p>");
|
||||
if (!uploadMarker(track, last, false)) {
|
||||
success = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Put waypoints.
|
||||
if (success) {
|
||||
if (!uploadWaypoints(track)) {
|
||||
Log.w(MyTracksConstants.TAG,
|
||||
"SendToMyMaps: upload waypoints failed.");
|
||||
success = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (success) {
|
||||
if (mapId.equals("new")) {
|
||||
statusMessage =
|
||||
context.getString(R.string.status_new_mymap_has_been_created);
|
||||
} else {
|
||||
statusMessage =
|
||||
context.getString(R.string.status_tracks_have_been_uploaded);
|
||||
}
|
||||
}
|
||||
Log.d(MyTracksConstants.TAG, "SendToMyMaps: Done: " + success);
|
||||
progressIndicator.setProgressValue(100);
|
||||
} catch (XmlPullParserException e) {
|
||||
Log.e(MyTracksConstants.TAG, "Caught an unexpected exception.", e);
|
||||
} finally {
|
||||
if (wrapper != null) {
|
||||
wrapper.cleanUp();
|
||||
}
|
||||
|
||||
context.runOnUiThread(new Runnable() {
|
||||
public void run() {
|
||||
if (onCompletion != null) {
|
||||
onCompletion.run();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a MyMapsFeature from a track.
|
||||
*
|
||||
* @param track the track
|
||||
* @return a MyMapsFeature
|
||||
*/
|
||||
private static MyMapsFeature buildMyMapsLineFeature(Track track) {
|
||||
MyMapsFeature myMapsFeature = new MyMapsFeature();
|
||||
myMapsFeature.generateAndroidId();
|
||||
myMapsFeature.setType(MyMapsFeature.LINE);
|
||||
if (track.getName().length() < 1) {
|
||||
// Features must have a name (otherwise GData upload may fail):
|
||||
myMapsFeature.setTitle("-");
|
||||
} else {
|
||||
myMapsFeature.setTitle(track.getName());
|
||||
}
|
||||
myMapsFeature.setColor(0x80FF0000);
|
||||
for (Location loc : track.getLocations()) {
|
||||
myMapsFeature.addPoint(MyTracksUtils.getGeoPoint(loc));
|
||||
}
|
||||
return myMapsFeature;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a placemark MyMapsFeature from a track.
|
||||
*
|
||||
* @param track the track
|
||||
* @param isStart true if it's the start of the track, or false for end
|
||||
* @return a MyMapsFeature
|
||||
*/
|
||||
private static MyMapsFeature buildMyMapsPlacemarkFeature(Context context,
|
||||
Track track, Location loc, boolean isStart) {
|
||||
MyMapsFeature myMapsFeature = new MyMapsFeature();
|
||||
myMapsFeature.generateAndroidId();
|
||||
myMapsFeature.setType(MyMapsFeature.MARKER);
|
||||
if (isStart) {
|
||||
myMapsFeature.setIconUrl(START_ICON_URL);
|
||||
} else {
|
||||
myMapsFeature.setIconUrl(END_ICON_URL);
|
||||
}
|
||||
myMapsFeature.addPoint(MyTracksUtils.getGeoPoint(loc));
|
||||
String name = track.getName() + " "
|
||||
+ (isStart ? context.getString(R.string.start)
|
||||
: context.getString(R.string.end));
|
||||
myMapsFeature.setTitle(name);
|
||||
return myMapsFeature;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a MyMapsFeature from a track.
|
||||
*
|
||||
* @param wpt the waypoint
|
||||
* @return a MyMapsFeature
|
||||
*/
|
||||
private static MyMapsFeature buildMyMapsPlacemarkFeature(Waypoint wpt) {
|
||||
MyMapsFeature myMapsFeature = new MyMapsFeature();
|
||||
myMapsFeature.generateAndroidId();
|
||||
myMapsFeature.setType(MyMapsFeature.MARKER);
|
||||
myMapsFeature.setIconUrl(wpt.getIcon());
|
||||
myMapsFeature.setDescription(wpt.getDescription());
|
||||
myMapsFeature.addPoint(MyTracksUtils.getGeoPoint(wpt.getLocation()));
|
||||
if (wpt.getName().length() < 1) {
|
||||
// Features must have a name (otherwise GData upload may fail):
|
||||
myMapsFeature.setTitle("-");
|
||||
} else {
|
||||
myMapsFeature.setTitle(wpt.getName());
|
||||
}
|
||||
return myMapsFeature;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
/*
|
||||
* 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.io;
|
||||
|
||||
import com.google.android.apps.mytracks.content.Track;
|
||||
import com.google.android.apps.mytracks.content.Waypoint;
|
||||
|
||||
import android.location.Location;
|
||||
|
||||
import java.io.OutputStream;
|
||||
|
||||
/**
|
||||
* Interface for writing data to a specific track file format.
|
||||
*
|
||||
* The expected sequence of calls is:
|
||||
* <ol>
|
||||
* <li>{@link #prepare}
|
||||
* <li>{@link #writeHeader}
|
||||
* <li>{@link #writeBeginTrack}
|
||||
* <li>For each segment:
|
||||
* <ol>
|
||||
* <li>{@link #writeOpenSegment}
|
||||
* <li>For each location in the segment: {@link #writeLocation}
|
||||
* <li>{@link #writeCloseSegment}
|
||||
* </ol>
|
||||
* <li>{@link #writeEndTrack}
|
||||
* <li>For each waypoint: {@link #writeWaypoint}
|
||||
* <li>{@link #writeFooter}
|
||||
* </ol>
|
||||
*
|
||||
* @author Rodrigo Damazio
|
||||
*/
|
||||
public interface TrackFormatWriter {
|
||||
|
||||
/**
|
||||
* Sets up the writer to write the given track to the given output.
|
||||
*
|
||||
* @param track the track to write
|
||||
* @param out the stream to write the track contents to
|
||||
*/
|
||||
void prepare(Track track, OutputStream out);
|
||||
|
||||
/**
|
||||
* Determines the default location that will be written to (if no other is
|
||||
* specified).
|
||||
*
|
||||
* @return The absolute path for the the file created, or null on failure
|
||||
*/
|
||||
String getDefaultDirectory();
|
||||
|
||||
/**
|
||||
* @return The file extentsion (i.e. gpx, kml, ...)
|
||||
*/
|
||||
String getExtension();
|
||||
|
||||
/**
|
||||
* Writes the header.
|
||||
* This is chance for classes to write out opening information.
|
||||
*/
|
||||
void writeHeader();
|
||||
|
||||
/**
|
||||
* Writes the footer.
|
||||
* This is chance for classes to write out closing information.
|
||||
*/
|
||||
void writeFooter();
|
||||
|
||||
/**
|
||||
* Write the given location object.
|
||||
*
|
||||
* TODO Add some flexible handling of other sensor data.
|
||||
*
|
||||
* @param location the location to write
|
||||
*/
|
||||
void writeLocation(Location location);
|
||||
|
||||
/**
|
||||
* Write a way point.
|
||||
*
|
||||
* @param waypoint
|
||||
*/
|
||||
void writeWaypoint(Waypoint waypoint);
|
||||
|
||||
/**
|
||||
* Write the beginning of a track.
|
||||
*/
|
||||
void writeBeginTrack(Location firstPoint);
|
||||
|
||||
/**
|
||||
* Write the end of a track.
|
||||
*/
|
||||
void writeEndTrack(Location lastPoint);
|
||||
|
||||
/**
|
||||
* Write the statements necessary to open a new segment.
|
||||
*/
|
||||
void writeOpenSegment();
|
||||
|
||||
/**
|
||||
* Write the statements necessary to close a segment.
|
||||
*/
|
||||
void writeCloseSegment();
|
||||
|
||||
/**
|
||||
* Close the underlying file handle.
|
||||
*/
|
||||
void close();
|
||||
}
|
||||
@@ -0,0 +1,365 @@
|
||||
/*
|
||||
* Copyright 2008 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
package com.google.android.apps.mytracks.io;
|
||||
|
||||
import com.google.android.apps.mytracks.MyTracksConstants;
|
||||
import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
|
||||
import com.google.android.apps.mytracks.content.Track;
|
||||
import com.google.android.apps.mytracks.content.TrackBuffer;
|
||||
import com.google.android.apps.mytracks.content.Waypoint;
|
||||
import com.google.android.apps.mytracks.util.MyTracksUtils;
|
||||
import com.google.android.maps.mytracks.R;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.location.Location;
|
||||
import android.os.Environment;
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.util.HashSet;
|
||||
|
||||
/**
|
||||
* The class which exports tracks to the SD card.
|
||||
* This writer is format-neutral - it handles creating the output file
|
||||
* and reading the track to be exported, but requires an instance of
|
||||
* {@link TrackFormatWriter} to actually format the data.
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
* @author Rodrigo Damazio
|
||||
*/
|
||||
public class TrackWriter {
|
||||
|
||||
private final Context context;
|
||||
private final MyTracksProviderUtils providerUtils;
|
||||
private final Track track;
|
||||
private final TrackFormatWriter writer;
|
||||
private Runnable onCompletion = null;
|
||||
private boolean success = false;
|
||||
private int errorMessage = -1;
|
||||
private File directory = null;
|
||||
private File file = null;
|
||||
|
||||
/**
|
||||
* A set of characters that are prohibited from being in file names.
|
||||
*/
|
||||
private static final HashSet<Character> PROHIBITED_CHARACTERS =
|
||||
new HashSet<Character>();
|
||||
|
||||
static {
|
||||
for (int i = 0; i < 48; i++) {
|
||||
PROHIBITED_CHARACTERS.add(new Character((char) i));
|
||||
}
|
||||
for (int i = 58; i < 65; i++) {
|
||||
PROHIBITED_CHARACTERS.add(new Character((char) i));
|
||||
}
|
||||
for (int i = 91; i < 97; i++) {
|
||||
PROHIBITED_CHARACTERS.add(new Character((char) i));
|
||||
}
|
||||
for (int i = 123; i < 128; i++) {
|
||||
PROHIBITED_CHARACTERS.add(new Character((char) i));
|
||||
}
|
||||
}
|
||||
|
||||
public TrackWriter(Context context, MyTracksProviderUtils providerUtils,
|
||||
Track track, TrackFormatWriter writer) {
|
||||
this.context = context;
|
||||
this.providerUtils = providerUtils;
|
||||
this.track = track;
|
||||
this.writer = writer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a completion callback.
|
||||
*
|
||||
* @param onCompletion Runnable that will be executed when finished
|
||||
*/
|
||||
public void setOnCompletion(Runnable onCompletion) {
|
||||
this.onCompletion = onCompletion;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a custom directory where the file will be written.
|
||||
*/
|
||||
public void setDirectory(File directory) {
|
||||
this.directory = directory;
|
||||
}
|
||||
|
||||
public String getAbsolutePath() {
|
||||
return file.getAbsolutePath();
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the given track id to the SD card.
|
||||
* This is non-blocking.
|
||||
*/
|
||||
public void writeTrackAsync() {
|
||||
Thread t = new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
writeTrack();
|
||||
}
|
||||
};
|
||||
t.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the given track id to the SD card.
|
||||
* This is blocking.
|
||||
*/
|
||||
public void writeTrack() {
|
||||
// Open the input and output
|
||||
success = false;
|
||||
errorMessage = R.string.error_track_does_not_exist;
|
||||
if (track != null) {
|
||||
if (openFile()) {
|
||||
writeDocument();
|
||||
}
|
||||
}
|
||||
finished();
|
||||
}
|
||||
|
||||
public int getErrorMessage() {
|
||||
return errorMessage;
|
||||
}
|
||||
|
||||
public boolean wasSuccess() {
|
||||
return success;
|
||||
}
|
||||
|
||||
/*
|
||||
* Helper methods:
|
||||
* ===============
|
||||
*/
|
||||
|
||||
/**
|
||||
* Makes sure the given directory exists, creating it if necessary.
|
||||
*
|
||||
* @param dir the directory to ensure exists
|
||||
* @return True on success
|
||||
*/
|
||||
private static boolean ensureExists(File dir) {
|
||||
if (dir.exists()) {
|
||||
return true;
|
||||
}
|
||||
File parent = dir.getParentFile();
|
||||
if (parent == null) {
|
||||
return true;
|
||||
}
|
||||
if (!ensureExists(parent)) {
|
||||
return false;
|
||||
}
|
||||
return dir.mkdir();
|
||||
}
|
||||
|
||||
private void finished() {
|
||||
if (onCompletion != null) {
|
||||
runOnUiThread(onCompletion);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the given runnable in the UI thread.
|
||||
*/
|
||||
protected void runOnUiThread(Runnable runnable) {
|
||||
if (context instanceof Activity) {
|
||||
((Activity) context).runOnUiThread(runnable);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the file and prepares the format writer for it.
|
||||
*
|
||||
* @return true on success, false otherwise (and errorMessage is set)
|
||||
*/
|
||||
protected boolean openFile() {
|
||||
if (!canWriteFile()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Make sure the name will work on FAT
|
||||
String fileName =
|
||||
sanitizeName(track.getName()) + "." + writer.getExtension();
|
||||
Log.i(MyTracksConstants.TAG, "Writing track to: " + fileName);
|
||||
try {
|
||||
writer.prepare(track, newOutputStream(fileName));
|
||||
} catch (FileNotFoundException e) {
|
||||
Log.e(MyTracksConstants.TAG, "Failed to open output file.", e);
|
||||
errorMessage = R.string.io_write_failed;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks and returns whether we're ready to create the output file.
|
||||
*/
|
||||
protected boolean canWriteFile() {
|
||||
if (directory == null) {
|
||||
directory = newFile(writer.getDefaultDirectory());
|
||||
}
|
||||
|
||||
if (!Environment.getExternalStorageState()
|
||||
.equals(Environment.MEDIA_MOUNTED)) {
|
||||
Log.i(MyTracksConstants.TAG, "Could not find SD card.");
|
||||
errorMessage = R.string.io_no_external_storage_found;
|
||||
return false;
|
||||
}
|
||||
if (!ensureExists(directory)) {
|
||||
Log.i(MyTracksConstants.TAG, "Could not create GPX directory.");
|
||||
errorMessage = R.string.io_create_dir_failed;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new output stream to write to the given filename.
|
||||
*
|
||||
* @throws FileNotFoundException if the file could't be created
|
||||
*/
|
||||
protected OutputStream newOutputStream(String fileName)
|
||||
throws FileNotFoundException {
|
||||
file = new File(directory, fileName);
|
||||
return new FileOutputStream(file);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new file object for the given path.
|
||||
*/
|
||||
protected File newFile(String path) {
|
||||
return new File(path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes the input string and make sure it is a valid fat32 file name.
|
||||
*/
|
||||
static String sanitizeName(String name) {
|
||||
StringBuilder cleaned = new StringBuilder();
|
||||
for (int i = 0; i < name.length(); i++) {
|
||||
char c = name.charAt(i);
|
||||
if (!PROHIBITED_CHARACTERS.contains(c)) {
|
||||
cleaned.append(c);
|
||||
}
|
||||
}
|
||||
|
||||
// Max = 260
|
||||
// Boiler plate /[gpx|kml]/ .[gpx|kml] = 8
|
||||
return (cleaned.length() > 252)
|
||||
? cleaned.substring(0, 252)
|
||||
: cleaned.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the waypoints for the given track.
|
||||
*
|
||||
* @param trackId the ID of the track to write waypoints for
|
||||
*/
|
||||
private void writeWaypoints(long trackId) {
|
||||
// TODO: Stream through he waypoints in chunks.
|
||||
// I am leaving the number of waypoints very high which should not be a
|
||||
// problem because we don't try to load them into objects all at the
|
||||
// same time.
|
||||
Cursor cursor = null;
|
||||
cursor = providerUtils.getWaypointsCursor(trackId, 0,
|
||||
MyTracksConstants.MAX_LOADED_WAYPOINTS_POINTS);
|
||||
if (cursor != null) {
|
||||
try {
|
||||
if (cursor.moveToFirst()) {
|
||||
// Yes, this will skip the 1st way point and that is intentional
|
||||
// as the 1st points holds the stats for the current/last segment.
|
||||
while (cursor.moveToNext()) {
|
||||
Waypoint wpt = providerUtils.createWaypoint(cursor);
|
||||
writer.writeWaypoint(wpt);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
if (cursor != null) {
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Does the actual work of writing the track to the now open file.
|
||||
*/
|
||||
void writeDocument() {
|
||||
Log.d(MyTracksConstants.TAG, "Started writing track.");
|
||||
writer.writeHeader();
|
||||
TrackBuffer buffer = new TrackBuffer(1024);
|
||||
Location last = null;
|
||||
boolean wroteFirst = false;
|
||||
boolean segmentOpen = false;
|
||||
int nValidLocations = 0;
|
||||
|
||||
// Fetch small pieces of the track.
|
||||
while (buffer.getLastLocationRead() < track.getStopId()) {
|
||||
Log.d(MyTracksConstants.TAG,
|
||||
"Reading track points starting at: " + buffer.getLastLocationRead());
|
||||
providerUtils.getTrackPoints(track, buffer);
|
||||
if (!wroteFirst) {
|
||||
Location first = buffer.findStartLocation();
|
||||
writer.writeBeginTrack(first);
|
||||
wroteFirst = true;
|
||||
}
|
||||
|
||||
Log.d(MyTracksConstants.TAG,
|
||||
"Reading " + buffer.getLocationsLoaded()
|
||||
+ " Ending at: " + buffer.getLastLocationRead());
|
||||
for (int i = 0; i < buffer.getLocationsLoaded(); i++) {
|
||||
Location location = buffer.get(i);
|
||||
if (MyTracksUtils.isValidLocation(location)) {
|
||||
nValidLocations++;
|
||||
if (!segmentOpen) {
|
||||
writer.writeOpenSegment();
|
||||
segmentOpen = true;
|
||||
}
|
||||
writer.writeLocation(location);
|
||||
if (nValidLocations >= 2) {
|
||||
last = location;
|
||||
}
|
||||
} else {
|
||||
nValidLocations = 0;
|
||||
last = null;
|
||||
if (segmentOpen) {
|
||||
writer.writeCloseSegment();
|
||||
segmentOpen = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (segmentOpen) {
|
||||
writer.writeCloseSegment();
|
||||
segmentOpen = false;
|
||||
}
|
||||
if (wroteFirst) {
|
||||
writer.writeEndTrack(last);
|
||||
}
|
||||
writeWaypoints(track.getId());
|
||||
writer.writeFooter();
|
||||
writer.close();
|
||||
success = true;
|
||||
Log.d(MyTracksConstants.TAG, "Done writing track.");
|
||||
errorMessage = R.string.io_write_finished;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
* 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.io;
|
||||
|
||||
import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
|
||||
import com.google.android.apps.mytracks.content.Track;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
/**
|
||||
* A factory to produce track writers for any format.
|
||||
*
|
||||
* @author Rodrigo Damazio
|
||||
*/
|
||||
public class TrackWriterFactory {
|
||||
|
||||
/**
|
||||
* Definition of all possible track formats.
|
||||
*/
|
||||
public enum TrackFileFormat {
|
||||
GPX {
|
||||
@Override
|
||||
TrackFormatWriter newFormatWriter(Context context) {
|
||||
return new GpxTrackWriter();
|
||||
}
|
||||
},
|
||||
KML {
|
||||
@Override
|
||||
TrackFormatWriter newFormatWriter(Context context) {
|
||||
return new KmlTrackWriter(context);
|
||||
}
|
||||
},
|
||||
CSV {
|
||||
@Override
|
||||
public TrackFormatWriter newFormatWriter(Context context) {
|
||||
return new CsvTrackWriter();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates and returns a new format writer for each format.
|
||||
*/
|
||||
abstract TrackFormatWriter newFormatWriter(Context context);
|
||||
|
||||
/**
|
||||
* Returns the mime type for each format.
|
||||
*/
|
||||
public String getMimeType() {
|
||||
return "application/" + getExtension() + "+xml";
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the file extension for each format.
|
||||
*/
|
||||
public String getExtension() {
|
||||
return this.name().toLowerCase();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new track writer to write the track with the given ID.
|
||||
*
|
||||
* @param context the context in which the track will be read
|
||||
* @param providerUtils the data provider utils to read the track with
|
||||
* @param trackId the ID of the track to be written
|
||||
* @param format the output format to write in
|
||||
* @return the new track writer
|
||||
*/
|
||||
public static TrackWriter newWriter(Context context,
|
||||
MyTracksProviderUtils providerUtils,
|
||||
long trackId, TrackFileFormat format) {
|
||||
Track track = providerUtils.getTrack(trackId);
|
||||
return newWriter(context, providerUtils, track, format);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new track writer to write the given track.
|
||||
*
|
||||
* @param context the context in which the track will be read
|
||||
* @param providerUtils the data provider utils to read the track with
|
||||
* @param track the track to be written
|
||||
* @param format the output format to write in
|
||||
* @return the new track writer
|
||||
*/
|
||||
public static TrackWriter newWriter(Context context,
|
||||
MyTracksProviderUtils providerUtils,
|
||||
Track track, TrackFileFormat format) {
|
||||
TrackFormatWriter writer = format.newFormatWriter(context);
|
||||
return new TrackWriter(context, providerUtils, track, writer);
|
||||
}
|
||||
|
||||
private TrackWriterFactory() { }
|
||||
}
|
||||
@@ -0,0 +1,429 @@
|
||||
/*
|
||||
* 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.io.gdata;
|
||||
|
||||
import com.google.wireless.gdata.client.GDataClient;
|
||||
import com.google.wireless.gdata.client.HttpException;
|
||||
import com.google.wireless.gdata.client.QueryParams;
|
||||
import com.google.wireless.gdata.data.StringUtils;
|
||||
import com.google.wireless.gdata.parser.ParseException;
|
||||
import com.google.wireless.gdata.serializer.GDataSerializer;
|
||||
|
||||
import android.text.TextUtils;
|
||||
import android.util.Config;
|
||||
import android.util.Log;
|
||||
|
||||
import org.apache.http.Header;
|
||||
import org.apache.http.HttpEntity;
|
||||
import org.apache.http.HttpResponse;
|
||||
import org.apache.http.StatusLine;
|
||||
import org.apache.http.client.HttpClient;
|
||||
import org.apache.http.client.methods.HttpGet;
|
||||
import org.apache.http.client.methods.HttpPost;
|
||||
import org.apache.http.client.methods.HttpUriRequest;
|
||||
import org.apache.http.entity.AbstractHttpEntity;
|
||||
import org.apache.http.entity.BasicHttpEntity;
|
||||
import org.apache.http.entity.InputStreamEntity;
|
||||
import org.apache.http.impl.client.DefaultHttpClient;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.URLEncoder;
|
||||
|
||||
/**
|
||||
* Implementation of a GDataClient using GoogleHttpClient to make HTTP requests.
|
||||
* Always issues GETs and POSTs, using the X-HTTP-Method-Override header when a
|
||||
* PUT or DELETE is desired, to avoid issues with firewalls, etc., that do not
|
||||
* allow methods other than GET or POST.
|
||||
*/
|
||||
public class AndroidGDataClient implements GDataClient {
|
||||
|
||||
private static final String TAG = "GDataClient";
|
||||
private static final boolean DEBUG = false;
|
||||
private static final boolean LOCAL_LOGV = DEBUG ? Config.LOGD : Config.LOGV;
|
||||
|
||||
private static final String X_HTTP_METHOD_OVERRIDE = "X-HTTP-Method-Override";
|
||||
|
||||
private static final int MAX_REDIRECTS = 10;
|
||||
|
||||
private final HttpClient httpClient;
|
||||
|
||||
/**
|
||||
* Interface for creating HTTP requests. Used by
|
||||
* {@link AndroidGDataClient#createAndExecuteMethod}, since HttpUriRequest
|
||||
* does not allow for changing the URI after creation, e.g., when you want to
|
||||
* follow a redirect.
|
||||
*/
|
||||
private interface HttpRequestCreator {
|
||||
HttpUriRequest createRequest(URI uri);
|
||||
}
|
||||
|
||||
private static class GetRequestCreator implements HttpRequestCreator {
|
||||
public HttpUriRequest createRequest(URI uri) {
|
||||
return new HttpGet(uri);
|
||||
}
|
||||
}
|
||||
|
||||
private static class PostRequestCreator implements HttpRequestCreator {
|
||||
private final String mMethodOverride;
|
||||
private final HttpEntity mEntity;
|
||||
|
||||
public PostRequestCreator(String methodOverride, HttpEntity entity) {
|
||||
mMethodOverride = methodOverride;
|
||||
mEntity = entity;
|
||||
}
|
||||
|
||||
public HttpUriRequest createRequest(URI uri) {
|
||||
HttpPost post = new HttpPost(uri);
|
||||
if (mMethodOverride != null) {
|
||||
post.addHeader(X_HTTP_METHOD_OVERRIDE, mMethodOverride);
|
||||
}
|
||||
post.setEntity(mEntity);
|
||||
return post;
|
||||
}
|
||||
}
|
||||
|
||||
// MAJOR TODO: make this work across redirects (if we can reset the
|
||||
// InputStream).
|
||||
// OR, read the bits into a local buffer (yuck, the media could be large).
|
||||
private static class MediaPutRequestCreator implements HttpRequestCreator {
|
||||
private final InputStream mMediaInputStream;
|
||||
private final String mContentType;
|
||||
|
||||
public MediaPutRequestCreator(InputStream mediaInputStream,
|
||||
String contentType) {
|
||||
mMediaInputStream = mediaInputStream;
|
||||
mContentType = contentType;
|
||||
}
|
||||
|
||||
public HttpUriRequest createRequest(URI uri) {
|
||||
HttpPost post = new HttpPost(uri);
|
||||
post.addHeader(X_HTTP_METHOD_OVERRIDE, "PUT");
|
||||
InputStreamEntity entity =
|
||||
new InputStreamEntity(mMediaInputStream, -1 /* read until EOF */);
|
||||
entity.setContentType(mContentType);
|
||||
post.setEntity(entity);
|
||||
return post;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new AndroidGDataClient.
|
||||
*/
|
||||
public AndroidGDataClient() {
|
||||
httpClient = new DefaultHttpClient();
|
||||
}
|
||||
|
||||
public void close() {
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
*
|
||||
* @see GDataClient#encodeUri(java.lang.String)
|
||||
*/
|
||||
public String encodeUri(String uri) {
|
||||
String encodedUri;
|
||||
try {
|
||||
encodedUri = URLEncoder.encode(uri, "UTF-8");
|
||||
} catch (UnsupportedEncodingException uee) {
|
||||
// should not happen.
|
||||
Log.e("JakartaGDataClient", "UTF-8 not supported -- should not happen. "
|
||||
+ "Using default encoding.", uee);
|
||||
encodedUri = URLEncoder.encode(uri);
|
||||
}
|
||||
return encodedUri;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
*
|
||||
* @see com.google.wireless.gdata.client.GDataClient#createQueryParams()
|
||||
*/
|
||||
public QueryParams createQueryParams() {
|
||||
return new QueryParamsImpl();
|
||||
}
|
||||
|
||||
// follows redirects
|
||||
private InputStream createAndExecuteMethod(HttpRequestCreator creator,
|
||||
String uriString, String authToken) throws HttpException, IOException {
|
||||
|
||||
HttpResponse response = null;
|
||||
int status = 500;
|
||||
int redirectsLeft = MAX_REDIRECTS;
|
||||
|
||||
URI uri;
|
||||
try {
|
||||
uri = new URI(uriString);
|
||||
} catch (URISyntaxException use) {
|
||||
Log.w(TAG, "Unable to parse " + uriString + " as URI.", use);
|
||||
throw new IOException("Unable to parse " + uriString + " as URI: "
|
||||
+ use.getMessage());
|
||||
}
|
||||
|
||||
// we follow redirects ourselves, since we want to follow redirects even on
|
||||
// POSTs, which
|
||||
// the HTTP library does not do. following redirects ourselves also allows
|
||||
// us to log
|
||||
// the redirects using our own logging.
|
||||
while (redirectsLeft > 0) {
|
||||
|
||||
HttpUriRequest request = creator.createRequest(uri);
|
||||
|
||||
// only add the auth token if not null (to allow for GData feeds that do
|
||||
// not require
|
||||
// authentication.)
|
||||
if (!TextUtils.isEmpty(authToken)) {
|
||||
request.addHeader("Authorization", "GoogleLogin auth=" + authToken);
|
||||
}
|
||||
if (LOCAL_LOGV) {
|
||||
for (Header h : request.getAllHeaders()) {
|
||||
Log.v(TAG, h.getName() + ": " + h.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
if (Log.isLoggable(TAG, Log.DEBUG)) {
|
||||
Log.d(TAG, "Executing " + request.getRequestLine().toString());
|
||||
}
|
||||
|
||||
response = null;
|
||||
|
||||
try {
|
||||
response = httpClient.execute(request);
|
||||
} catch (IOException ioe) {
|
||||
Log.w(TAG, "Unable to execute HTTP request." + ioe);
|
||||
throw ioe;
|
||||
}
|
||||
|
||||
StatusLine statusLine = response.getStatusLine();
|
||||
if (statusLine == null) {
|
||||
Log.w(TAG, "StatusLine is null.");
|
||||
throw new NullPointerException(
|
||||
"StatusLine is null -- should not happen.");
|
||||
}
|
||||
|
||||
if (Log.isLoggable(TAG, Log.DEBUG)) {
|
||||
Log.d(TAG, response.getStatusLine().toString());
|
||||
for (Header h : response.getAllHeaders()) {
|
||||
Log.d(TAG, h.getName() + ": " + h.getValue());
|
||||
}
|
||||
}
|
||||
status = statusLine.getStatusCode();
|
||||
|
||||
HttpEntity entity = response.getEntity();
|
||||
|
||||
// TODO: handle 301, 307?
|
||||
// TODO: let the http client handle the redirects, if we can be sure we'll
|
||||
// never get a
|
||||
// redirect on POST.
|
||||
if (status == 302) {
|
||||
// consume the content, so the connection can be closed.
|
||||
entity.consumeContent();
|
||||
Header location = response.getFirstHeader("Location");
|
||||
if (location == null) {
|
||||
if (Log.isLoggable(TAG, Log.DEBUG)) {
|
||||
Log.d(TAG, "Redirect requested but no Location " + "specified.");
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (Log.isLoggable(TAG, Log.DEBUG)) {
|
||||
Log.d(TAG, "Following redirect to " + location.getValue());
|
||||
}
|
||||
try {
|
||||
uri = new URI(location.getValue());
|
||||
} catch (URISyntaxException use) {
|
||||
if (Log.isLoggable(TAG, Log.DEBUG)) {
|
||||
Log.d(TAG, "Unable to parse " + location.getValue() + " as URI.",
|
||||
use);
|
||||
throw new IOException("Unable to parse " + location.getValue()
|
||||
+ " as URI.");
|
||||
}
|
||||
break;
|
||||
}
|
||||
--redirectsLeft;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (Log.isLoggable(TAG, Log.VERBOSE)) {
|
||||
Log.v(TAG, "Received " + status + " status code.");
|
||||
}
|
||||
String errorMessage = null;
|
||||
HttpEntity entity = response.getEntity();
|
||||
try {
|
||||
if (response != null && entity != null) {
|
||||
InputStream in = entity.getContent();
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
byte[] buf = new byte[8192];
|
||||
int bytesRead = -1;
|
||||
while ((bytesRead = in.read(buf)) != -1) {
|
||||
baos.write(buf, 0, bytesRead);
|
||||
}
|
||||
// TODO: use appropriate encoding, picked up from Content-Type.
|
||||
errorMessage = new String(baos.toByteArray());
|
||||
if (Log.isLoggable(TAG, Log.VERBOSE)) {
|
||||
Log.v(TAG, errorMessage);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
if (entity != null) {
|
||||
entity.consumeContent();
|
||||
}
|
||||
}
|
||||
String exceptionMessage = "Received " + status + " status code";
|
||||
if (errorMessage != null) {
|
||||
exceptionMessage += (": " + errorMessage);
|
||||
}
|
||||
throw new HttpException(exceptionMessage, status, null /* InputStream */);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
*
|
||||
* @see GDataClient#getFeedAsStream(java.lang.String, java.lang.String)
|
||||
*/
|
||||
public InputStream getFeedAsStream(String feedUrl, String authToken)
|
||||
throws HttpException, IOException {
|
||||
|
||||
InputStream in =
|
||||
createAndExecuteMethod(new GetRequestCreator(), feedUrl, authToken);
|
||||
if (in != null) {
|
||||
return in;
|
||||
}
|
||||
throw new IOException("Unable to access feed.");
|
||||
}
|
||||
|
||||
public InputStream getMediaEntryAsStream(String mediaEntryUrl,
|
||||
String authToken) throws HttpException, IOException {
|
||||
|
||||
InputStream in =
|
||||
createAndExecuteMethod(new GetRequestCreator(), mediaEntryUrl,
|
||||
authToken);
|
||||
|
||||
if (in != null) {
|
||||
return in;
|
||||
}
|
||||
throw new IOException("Unable to access media entry.");
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
*
|
||||
* @see GDataClient#createEntry
|
||||
*/
|
||||
public InputStream createEntry(String feedUrl, String authToken,
|
||||
GDataSerializer entry) throws HttpException, IOException {
|
||||
|
||||
HttpEntity entity =
|
||||
createEntityForEntry(entry, GDataSerializer.FORMAT_CREATE);
|
||||
InputStream in =
|
||||
createAndExecuteMethod(new PostRequestCreator(null /* override */,
|
||||
entity), feedUrl, authToken);
|
||||
if (in != null) {
|
||||
return in;
|
||||
}
|
||||
throw new IOException("Unable to create entry.");
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
*
|
||||
* @see GDataClient#updateEntry
|
||||
*/
|
||||
public InputStream updateEntry(String editUri, String authToken,
|
||||
GDataSerializer entry) throws HttpException, IOException {
|
||||
HttpEntity entity =
|
||||
createEntityForEntry(entry, GDataSerializer.FORMAT_UPDATE);
|
||||
InputStream in =
|
||||
createAndExecuteMethod(new PostRequestCreator("PUT", entity), editUri,
|
||||
authToken);
|
||||
if (in != null) {
|
||||
return in;
|
||||
}
|
||||
throw new IOException("Unable to update entry.");
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
*
|
||||
* @see GDataClient#deleteEntry
|
||||
*/
|
||||
public void deleteEntry(String editUri, String authToken)
|
||||
throws HttpException, IOException {
|
||||
if (StringUtils.isEmpty(editUri)) {
|
||||
throw new IllegalArgumentException(
|
||||
"you must specify an non-empty edit url");
|
||||
}
|
||||
InputStream in =
|
||||
createAndExecuteMethod(
|
||||
new PostRequestCreator("DELETE", null /* entity */), editUri,
|
||||
authToken);
|
||||
if (in == null) {
|
||||
throw new IOException("Unable to delete entry.");
|
||||
}
|
||||
try {
|
||||
in.close();
|
||||
} catch (IOException ioe) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
public InputStream updateMediaEntry(String editUri, String authToken,
|
||||
InputStream mediaEntryInputStream, String contentType)
|
||||
throws HttpException, IOException {
|
||||
InputStream in =
|
||||
createAndExecuteMethod(new MediaPutRequestCreator(
|
||||
mediaEntryInputStream, contentType), editUri, authToken);
|
||||
if (in != null) {
|
||||
return in;
|
||||
}
|
||||
throw new IOException("Unable to write media entry.");
|
||||
}
|
||||
|
||||
private HttpEntity createEntityForEntry(GDataSerializer entry, int format)
|
||||
throws IOException {
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
try {
|
||||
entry.serialize(baos, format);
|
||||
} catch (IOException ioe) {
|
||||
Log.e(TAG, "Unable to serialize entry.", ioe);
|
||||
throw ioe;
|
||||
} catch (ParseException pe) {
|
||||
Log.e(TAG, "Unable to serialize entry.", pe);
|
||||
throw new IOException("Unable to serialize entry: " + pe.getMessage());
|
||||
}
|
||||
|
||||
byte[] entryBytes = baos.toByteArray();
|
||||
|
||||
if (entryBytes != null && Log.isLoggable(TAG, Log.DEBUG)) {
|
||||
try {
|
||||
Log.d(TAG, "Serialized entry: " + new String(entryBytes, "UTF-8"));
|
||||
} catch (UnsupportedEncodingException uee) {
|
||||
// should not happen
|
||||
throw new IllegalStateException("UTF-8 should be supported!", uee);
|
||||
}
|
||||
}
|
||||
|
||||
AbstractHttpEntity entity = new BasicHttpEntity();
|
||||
entity.setContentType(entry.getContentType());
|
||||
return entity;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* 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.io.gdata;
|
||||
|
||||
import com.google.android.apps.mytracks.MyTracksConstants;
|
||||
import com.google.wireless.gdata.client.GDataClient;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
|
||||
/**
|
||||
* This factory will fetch the right class for the platform.
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
*/
|
||||
public class GDataClientFactory {
|
||||
|
||||
private GDataClientFactory() { }
|
||||
|
||||
/**
|
||||
* Creates a new GData client.
|
||||
* This factory will fetch the right class for the platform.
|
||||
* @return A GDataClient appropriate for this platform
|
||||
*/
|
||||
public static GDataClient getGDataClient(Context context) {
|
||||
try {
|
||||
// Try to use the official unbundled gdata client implementation.
|
||||
// This should work on Froyo and beyond.
|
||||
return new com.google.android.common.gdata.AndroidGDataClient(context);
|
||||
} catch (LinkageError e) {
|
||||
// On all other platforms use the client implementation packaged in the
|
||||
// apk.
|
||||
Log.i(MyTracksConstants.TAG, "Using mytracks AndroidGDataClient.", e);
|
||||
return new com.google.android.apps.mytracks.io.gdata.AndroidGDataClient();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,224 @@
|
||||
/*
|
||||
* Copyright 2008 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
package com.google.android.apps.mytracks.io.gdata;
|
||||
|
||||
import com.google.android.apps.mytracks.MyTracksConstants;
|
||||
import com.google.android.apps.mytracks.io.AuthManager;
|
||||
import com.google.wireless.gdata.client.GDataClient;
|
||||
import com.google.wireless.gdata.client.GDataServiceClient;
|
||||
import com.google.wireless.gdata.client.HttpException;
|
||||
import com.google.wireless.gdata.parser.ParseException;
|
||||
import com.google.wireless.gdata2.ConflictDetectedException;
|
||||
import com.google.wireless.gdata2.client.AuthenticationException;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
|
||||
|
||||
/**
|
||||
* GDataWrapper provides a wrapper around GData operations that maintains the
|
||||
* GData client, and provides a method to run GData queries with proper error
|
||||
* handling. After a query is run, the wrapper can be queried about the error
|
||||
* that occurred.
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
*/
|
||||
public class GDataWrapper {
|
||||
|
||||
/**
|
||||
* A QueryFunction is passed in when executing a query. The query function of
|
||||
* the class is called with the GData client as a parameter. The function
|
||||
* should execute whatever operations it desires on the client without concern
|
||||
* for whether the client will throw an error.
|
||||
*/
|
||||
public interface QueryFunction {
|
||||
public abstract void query(GDataServiceClient client)
|
||||
throws AuthenticationException, IOException, ParseException,
|
||||
ConflictDetectedException, HttpException;
|
||||
}
|
||||
|
||||
/**
|
||||
* A AuthenticatedFunction is passed in when executing the google
|
||||
* authenticated service. The authenticated function of the class is called
|
||||
* with the current authentication token for the service. The function should
|
||||
* execute whatever operations with the google service without concern for
|
||||
* whether the client will throw an error.
|
||||
*/
|
||||
public interface AuthenticatedFunction {
|
||||
public abstract void run(String authenticationToken)
|
||||
throws AuthenticationException, IOException;
|
||||
}
|
||||
|
||||
// The types of error that may be encountered
|
||||
// No error occurred.
|
||||
public static final int ERROR_NO_ERROR = 0;
|
||||
// There was an authentication error, the auth token may be invalid.
|
||||
public static final int ERROR_AUTH = 1;
|
||||
// There was an internal error on the server side.
|
||||
public static final int ERROR_INTERNAL = 2;
|
||||
// There was an error connecting to the server.
|
||||
public static final int ERROR_CONNECTION = 3;
|
||||
// The item queried did not exit.
|
||||
public static final int ERROR_NOT_FOUND = 4;
|
||||
// There was an error parsing or serializing locally.
|
||||
public static final int ERROR_LOCAL = 5;
|
||||
// There was a conflict, update the entry and try again.
|
||||
public static final int ERROR_CONFLICT = 6;
|
||||
// A query was run after cleaning up the wrapper, so the client was invalid.
|
||||
public static final int ERROR_CLEANED_UP = 7;
|
||||
// An unknown error occurred.
|
||||
public static final int ERROR_UNKNOWN = 100;
|
||||
|
||||
private String errorMessage;
|
||||
private int errorType;
|
||||
private GDataClient androidGDataClient;
|
||||
private GDataServiceClient gdataServiceClient;
|
||||
private AuthManager auth;
|
||||
private boolean retryOnAuthFailure;
|
||||
private int retriesPending;
|
||||
private boolean cleanupCalled;
|
||||
|
||||
public GDataWrapper() {
|
||||
errorType = ERROR_NO_ERROR;
|
||||
errorMessage = null;
|
||||
auth = null;
|
||||
retryOnAuthFailure = false;
|
||||
retriesPending = 0;
|
||||
cleanupCalled = false;
|
||||
}
|
||||
|
||||
public void setClient(GDataClient androidGDataClient,
|
||||
GDataServiceClient gdataServiceClient) {
|
||||
this.androidGDataClient = androidGDataClient;
|
||||
this.gdataServiceClient = gdataServiceClient;
|
||||
}
|
||||
|
||||
public boolean runAuthenticatedFunction(
|
||||
final AuthenticatedFunction function) {
|
||||
return runOne(function, null);
|
||||
}
|
||||
|
||||
public boolean runQuery(final QueryFunction query) {
|
||||
return runOne(null, query);
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs an arbitrary piece of code.
|
||||
*/
|
||||
private boolean runOne(final AuthenticatedFunction function,
|
||||
final QueryFunction query) {
|
||||
try {
|
||||
if (function != null) {
|
||||
function.run(this.auth.getAuthToken());
|
||||
} else if (query != null) {
|
||||
query.query(gdataServiceClient);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
errorType = ERROR_NO_ERROR;
|
||||
errorMessage = null;
|
||||
return true;
|
||||
} catch (AuthenticationException e) {
|
||||
Log.e(MyTracksConstants.TAG, "Exception", e);
|
||||
errorType = ERROR_AUTH;
|
||||
errorMessage = e.getMessage();
|
||||
} catch (HttpException e) {
|
||||
Log.e(MyTracksConstants.TAG, "HttpException", e);
|
||||
errorMessage = e.getMessage();
|
||||
if (errorMessage.contains("401")) {
|
||||
errorType = ERROR_AUTH;
|
||||
} else {
|
||||
errorType = ERROR_CONNECTION;
|
||||
}
|
||||
} catch (FileNotFoundException e) {
|
||||
Log.e(MyTracksConstants.TAG, "Exception", e);
|
||||
errorType = ERROR_AUTH;
|
||||
errorMessage = e.getMessage();
|
||||
} catch (IOException e) {
|
||||
Log.e(MyTracksConstants.TAG, "Exception", e);
|
||||
errorMessage = e.getMessage();
|
||||
if (errorMessage.contains("503")) {
|
||||
errorType = ERROR_INTERNAL;
|
||||
} else {
|
||||
errorType = ERROR_CONNECTION;
|
||||
}
|
||||
} catch (ParseException e) {
|
||||
Log.e(MyTracksConstants.TAG, "Exception", e);
|
||||
errorType = ERROR_LOCAL;
|
||||
errorMessage = e.getMessage();
|
||||
} catch (ConflictDetectedException e) {
|
||||
Log.e(MyTracksConstants.TAG, "Exception", e);
|
||||
errorType = ERROR_CONFLICT;
|
||||
errorMessage = e.getMessage();
|
||||
}
|
||||
|
||||
Log.d(MyTracksConstants.TAG, "GData error encountered: " + errorMessage);
|
||||
if (errorType == ERROR_AUTH && auth != null) {
|
||||
Runnable whenFinished = null;
|
||||
if (retryOnAuthFailure) {
|
||||
retriesPending++;
|
||||
// This is a little odd. This method cannot return in the the thread
|
||||
// that called the original runQuery(). We should look at reworking this
|
||||
// API.
|
||||
whenFinished = new Runnable() {
|
||||
public void run() {
|
||||
retriesPending--;
|
||||
try {
|
||||
runOne(function, query);
|
||||
} catch (IllegalStateException ise) {
|
||||
// This can happen if the connection pool was shut down.
|
||||
Log.e(MyTracksConstants.TAG, "Failed to rerun query.", ise);
|
||||
}
|
||||
if (cleanupCalled && retriesPending == 0) {
|
||||
cleanUp();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
auth.invalidateAndRefresh(whenFinished);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public int getErrorType() {
|
||||
return errorType;
|
||||
}
|
||||
|
||||
public String getErrorMessage() {
|
||||
return errorMessage;
|
||||
}
|
||||
|
||||
// cleanUp must be called when done using this wrapper to close the client.
|
||||
// Note that the cleanup will be delayed if auth failure retries were
|
||||
// requested and there is a pending retry.
|
||||
public void cleanUp() {
|
||||
cleanupCalled = true;
|
||||
if (retriesPending == 0) {
|
||||
androidGDataClient.close();
|
||||
androidGDataClient = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void setAuthManager(AuthManager auth) {
|
||||
this.auth = auth;
|
||||
}
|
||||
|
||||
public void setRetryOnAuthFailure(boolean retry) {
|
||||
retryOnAuthFailure = retry;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
/*
|
||||
* 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.io.gdata;
|
||||
|
||||
import com.google.wireless.gdata.client.QueryParams;
|
||||
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.URLEncoder;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Simple implementation of the QueryParams interface.
|
||||
*/
|
||||
// TODO: deal with categories
|
||||
public class QueryParamsImpl extends QueryParams {
|
||||
|
||||
private final Map<String, String> mParams = new HashMap<String, String>();
|
||||
|
||||
@Override
|
||||
public void clear() {
|
||||
setEntryId(null);
|
||||
mParams.clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String generateQueryUrl(String feedUrl) {
|
||||
|
||||
if (TextUtils.isEmpty(getEntryId()) && mParams.isEmpty()) {
|
||||
// nothing to do
|
||||
return feedUrl;
|
||||
}
|
||||
|
||||
// handle entry IDs
|
||||
if (!TextUtils.isEmpty(getEntryId())) {
|
||||
if (!mParams.isEmpty()) {
|
||||
throw new IllegalStateException("Cannot set both an entry ID "
|
||||
+ "and other query paramters.");
|
||||
}
|
||||
return feedUrl + '/' + getEntryId();
|
||||
}
|
||||
|
||||
// otherwise, append the querystring params.
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append(feedUrl);
|
||||
Set<String> params = mParams.keySet();
|
||||
boolean first = true;
|
||||
if (feedUrl.contains("?")) {
|
||||
first = false;
|
||||
} else {
|
||||
sb.append('?');
|
||||
}
|
||||
for (String param : params) {
|
||||
if (first) {
|
||||
first = false;
|
||||
} else {
|
||||
sb.append('&');
|
||||
}
|
||||
sb.append(param);
|
||||
sb.append('=');
|
||||
String value = mParams.get(param);
|
||||
String encodedValue = null;
|
||||
|
||||
try {
|
||||
encodedValue = URLEncoder.encode(value, "UTF-8");
|
||||
} catch (UnsupportedEncodingException uee) {
|
||||
// should not happen.
|
||||
Log.w("QueryParamsImpl", "UTF-8 not supported -- should not happen. "
|
||||
+ "Using default encoding.", uee);
|
||||
encodedValue = URLEncoder.encode(value);
|
||||
}
|
||||
sb.append(encodedValue);
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getParamValue(String param) {
|
||||
if (!(mParams.containsKey(param))) {
|
||||
return null;
|
||||
}
|
||||
return mParams.get(param);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setParamValue(String param, String value) {
|
||||
mParams.put(param, value);
|
||||
}
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
public AbsoluteLocationListenerPolicy(final long interval) {
|
||||
this.interval = interval;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The interval given in the constructor
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
public int getMinDistance() {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @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.
|
||||
*/
|
||||
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
|
||||
*/
|
||||
public AdaptiveLocationListenerPolicy(long min, long max, int minDistance) {
|
||||
this.minInterval = min;
|
||||
this.maxInterval = max;
|
||||
this.minDistance = minDistance;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return An interval bounded by min and max which is half of the idle time
|
||||
*/
|
||||
public long getDesiredPollingInterval() {
|
||||
long desiredInterval = idleTime / 2;
|
||||
// Round to avoid setting the interval too often.
|
||||
desiredInterval = (desiredInterval / 1000) * 1000;
|
||||
return Math.max(Math.min(maxInterval, desiredInterval),
|
||||
minInterval);
|
||||
}
|
||||
|
||||
public void updateIdleTime(long newIdleTime) {
|
||||
this.idleTime = newIdleTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the minimum distance between updates.
|
||||
*/
|
||||
public int getMinDistance() {
|
||||
return minDistance;
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@
|
||||
package com.google.android.apps.mytracks.services;
|
||||
|
||||
import com.google.android.apps.mytracks.content.Waypoint;
|
||||
import com.google.android.apps.mytracks.stats.TripStatisticsData;
|
||||
|
||||
/**
|
||||
* MyTracks service.
|
||||
@@ -35,10 +36,18 @@ interface ITrackRecordingService {
|
||||
*/
|
||||
boolean isRecording();
|
||||
|
||||
/**
|
||||
* Returns the track ID of the track currently being recorded, or -1 if none
|
||||
* is being recorded. This ID can then be used to read track data from the
|
||||
* content source.
|
||||
*/
|
||||
long getRecordingTrackId();
|
||||
|
||||
/**
|
||||
* Inserts a waypoint marker in the track being recorded.
|
||||
*
|
||||
* @param waypoint the waypoint to insert
|
||||
* @return the unique ID of the inserted marker
|
||||
*/
|
||||
long insertWaypointMarker(in Waypoint waypoint);
|
||||
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* This is an interface for classes that will manage the location listener policy.
|
||||
* Different policy options are:
|
||||
* Absolute
|
||||
* Addaptive
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
*/
|
||||
public interface LocationListenerPolicy {
|
||||
|
||||
/**
|
||||
* Returns the polling time this policy would like at this time.
|
||||
*
|
||||
* @return The polling that this policy dictates
|
||||
*/
|
||||
public long getDesiredPollingInterval();
|
||||
|
||||
/**
|
||||
* Returns the minimum distance between updates.
|
||||
*/
|
||||
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
|
||||
*/
|
||||
public void updateIdleTime(long idleTime);
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* This is interface for a task that will be executed on some schedule.
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
*/
|
||||
public interface PeriodicTask {
|
||||
/**
|
||||
* Sets up this task for subsequent calls to the run method.
|
||||
*/
|
||||
public void start();
|
||||
|
||||
/**
|
||||
* This method will be called periodically.
|
||||
*/
|
||||
public void run(TrackRecordingService service);
|
||||
|
||||
/**
|
||||
* Shuts down this task and clean up resources.
|
||||
*/
|
||||
public void shutdown();
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
import com.google.android.apps.mytracks.MyTracksConstants;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.Timer;
|
||||
import java.util.TimerTask;
|
||||
|
||||
/**
|
||||
* This class will periodically announce the user's trip statitics.
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
*/
|
||||
public class PeriodicTaskExecuter {
|
||||
|
||||
private final TrackRecordingService service;
|
||||
|
||||
/**
|
||||
* A timer to schedule the announcements.
|
||||
*/
|
||||
private Timer timer = new Timer();
|
||||
|
||||
private final PeriodicTask task;
|
||||
|
||||
public PeriodicTaskExecuter(PeriodicTask task,
|
||||
TrackRecordingService service) {
|
||||
this.task = task;
|
||||
this.service = service;
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedules the task at the given interval.
|
||||
*
|
||||
* @param interval The interval in milliseconds
|
||||
*/
|
||||
public void scheduleTask(long interval) {
|
||||
timer.cancel();
|
||||
timer.purge();
|
||||
timer = new Timer();
|
||||
if (interval <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
long now = System.currentTimeMillis();
|
||||
long next = service.getTripStatistics().getStartTime();
|
||||
while (next < now) next += interval;
|
||||
|
||||
Date start = new Date(next);
|
||||
Log.i(MyTracksConstants.TAG,
|
||||
"StatusAnnouncer scheduled to start at " + start + " every "
|
||||
+ interval + " milliseconds.");
|
||||
timer.scheduleAtFixedRate(new PeriodicTimerTask(), start, interval);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleans up this object.
|
||||
*/
|
||||
public void shutdown() {
|
||||
timer.cancel();
|
||||
timer.purge();
|
||||
timer = null;
|
||||
task.shutdown();
|
||||
}
|
||||
|
||||
/**
|
||||
* The timer task to announce the trip status.
|
||||
*/
|
||||
private class PeriodicTimerTask extends TimerTask {
|
||||
@Override
|
||||
public void run() {
|
||||
task.run(service);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
/**
|
||||
* This class will periodically announce the user's trip statitics. This class
|
||||
* is a wrapper for cupcake devices.
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
*/
|
||||
public class SafeStatusAnnouncerTask implements PeriodicTask {
|
||||
|
||||
private StatusAnnouncerTask announcer;
|
||||
|
||||
/* class initialization fails when this throws an exception */
|
||||
static {
|
||||
try {
|
||||
Class.forName(
|
||||
"com.google.android.apps.mytracks.services.StatusAnnouncerTask");
|
||||
} catch (ClassNotFoundException ex) {
|
||||
throw new RuntimeException(ex);
|
||||
} catch (LinkageError er) {
|
||||
throw new RuntimeException(er);
|
||||
}
|
||||
}
|
||||
|
||||
/* calling here forces class initialization */
|
||||
public static void checkAvailable() {
|
||||
}
|
||||
|
||||
public SafeStatusAnnouncerTask(Context context) {
|
||||
announcer = new StatusAnnouncerTask(context);
|
||||
}
|
||||
|
||||
public void run(TrackRecordingService service) {
|
||||
announcer.run(service);
|
||||
}
|
||||
|
||||
public void shutdown() {
|
||||
announcer.shutdown();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
/*
|
||||
* 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.MyTracksConstants;
|
||||
import com.google.android.apps.mytracks.content.Waypoint;
|
||||
|
||||
import android.content.Context;
|
||||
import android.location.Location;
|
||||
import android.telephony.CellLocation;
|
||||
import android.telephony.NeighboringCellInfo;
|
||||
import android.telephony.PhoneStateListener;
|
||||
import android.telephony.TelephonyManager;
|
||||
import android.util.Log;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A class to monitor the network signal strength.
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
*/
|
||||
public class SignalStrengthTask extends PhoneStateListener
|
||||
implements PeriodicTask {
|
||||
|
||||
private final Context context;
|
||||
private TelephonyManager manager;
|
||||
private int signalStrength = -1;
|
||||
|
||||
public SignalStrengthTask(Context c) {
|
||||
context = c;
|
||||
}
|
||||
|
||||
public void start() {
|
||||
manager =
|
||||
(TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
|
||||
if (manager == null) {
|
||||
Log.e(MyTracksConstants.TAG, "Cannot get telephony manager.");
|
||||
} else {
|
||||
manager.listen(this, getListenEvents());
|
||||
}
|
||||
}
|
||||
|
||||
protected int getListenEvents() {
|
||||
return PhoneStateListener.LISTEN_SIGNAL_STRENGTHS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSignalStrengthChanged(int asu) {
|
||||
this.signalStrength = asu;
|
||||
Log.d(MyTracksConstants.TAG, "Signal Strength: " + signalStrength);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run(TrackRecordingService service) {
|
||||
Log.d(MyTracksConstants.TAG, "Adding signal marker");
|
||||
Location location = service.getLastLocation();
|
||||
if (manager == null) {
|
||||
Log.d(MyTracksConstants.TAG, "Adding signal marker: marker null");
|
||||
return;
|
||||
}
|
||||
if (location == null) {
|
||||
Log.d(MyTracksConstants.TAG, "Adding signal marker: location null");
|
||||
return;
|
||||
}
|
||||
Waypoint wpt = new Waypoint();
|
||||
wpt.setName("Signal Strength");
|
||||
wpt.setType(Waypoint.TYPE_WAYPOINT);
|
||||
wpt.setTrackId(service.getRecordingTrackId());
|
||||
int networkType = manager.getNetworkType();
|
||||
wpt.setIcon(getIcon(networkType));
|
||||
wpt.setLocation(location);
|
||||
wpt.setDescription(getDescription());
|
||||
long waypointId = service.insertWaypointMarker(wpt);
|
||||
if (waypointId >= 0) {
|
||||
Log.d(MyTracksConstants.TAG, "Added signal marker");
|
||||
} else {
|
||||
Log.e(MyTracksConstants.TAG, "Cannot insert waypoint marker?");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a human readable description for the network type.
|
||||
*
|
||||
* @param type The integer constant for the network type
|
||||
* @return A human readable description of the network type
|
||||
*/
|
||||
protected String getTypeAsString(int type) {
|
||||
switch (type) {
|
||||
case TelephonyManager.NETWORK_TYPE_EDGE:
|
||||
return "EDGE";
|
||||
case TelephonyManager.NETWORK_TYPE_GPRS:
|
||||
return "GPRS";
|
||||
case TelephonyManager.NETWORK_TYPE_UMTS:
|
||||
return "UMTS";
|
||||
case TelephonyManager.NETWORK_TYPE_UNKNOWN:
|
||||
default:
|
||||
return "UNKNOWN";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a description for the current signal strength.
|
||||
*
|
||||
* @return A human readable description of the network state
|
||||
*/
|
||||
private String getDescription() {
|
||||
StringBuffer sb = new StringBuffer();
|
||||
sb.append(getStrengthAsString());
|
||||
|
||||
sb.append("Network Type: ");
|
||||
sb.append(getTypeAsString(manager.getNetworkType()));
|
||||
sb.append('\n');
|
||||
|
||||
sb.append("Operator: ");
|
||||
sb.append(manager.getNetworkOperatorName());
|
||||
sb.append(" / ");
|
||||
sb.append(manager.getNetworkOperator());
|
||||
sb.append('\n');
|
||||
|
||||
sb.append("Roaming: ");
|
||||
sb.append(manager.isNetworkRoaming());
|
||||
sb.append('\n');
|
||||
|
||||
List<NeighboringCellInfo> infos = manager.getNeighboringCellInfo();
|
||||
Log.i(MyTracksConstants.TAG, "Found " + infos.size() + " cells.");
|
||||
if (infos.size() > 0) {
|
||||
sb.append("Neighbors: ");
|
||||
for (NeighboringCellInfo info : infos) {
|
||||
sb.append(info.toString());
|
||||
sb.append(' ');
|
||||
}
|
||||
sb.append('\n');
|
||||
}
|
||||
|
||||
CellLocation cell = manager.getCellLocation();
|
||||
if (cell != null) {
|
||||
sb.append("Cell: ");
|
||||
sb.append(cell.toString());
|
||||
sb.append('\n');
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the url for the waypoint icon for the current network type.
|
||||
*
|
||||
* @param type The network type
|
||||
* @return A url to a image to use as the waypoint icon
|
||||
*/
|
||||
protected String getIcon(int type) {
|
||||
switch (type) {
|
||||
case TelephonyManager.NETWORK_TYPE_GPRS:
|
||||
case TelephonyManager.NETWORK_TYPE_EDGE:
|
||||
return "http://maps.google.com/mapfiles/ms/micons/green.png";
|
||||
case TelephonyManager.NETWORK_TYPE_UMTS:
|
||||
return "http://maps.google.com/mapfiles/ms/micons/blue.png";
|
||||
case TelephonyManager.NETWORK_TYPE_UNKNOWN:
|
||||
default:
|
||||
return "http://maps.google.com/mapfiles/ms/micons/red.png";
|
||||
}
|
||||
}
|
||||
|
||||
public void shutdown() {
|
||||
if (manager != null) {
|
||||
manager.listen(this, PhoneStateListener.LISTEN_NONE);
|
||||
manager = null;
|
||||
}
|
||||
}
|
||||
|
||||
public String getStrengthAsString() {
|
||||
return "Strength: " + signalStrength + "\n";
|
||||
}
|
||||
|
||||
protected Context getContext() {
|
||||
return context;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
/*
|
||||
* 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.MyTracksConstants;
|
||||
import com.google.android.maps.mytracks.R;
|
||||
|
||||
import android.content.Context;
|
||||
import android.telephony.PhoneStateListener;
|
||||
import android.telephony.SignalStrength;
|
||||
import android.telephony.TelephonyManager;
|
||||
import android.util.Log;
|
||||
|
||||
/**
|
||||
* A class to monitor the network signal strength.
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
*/
|
||||
public class SignalStrengthTaskModern extends SignalStrengthTask {
|
||||
|
||||
private SignalStrength signalStrength = null;
|
||||
|
||||
public SignalStrengthTaskModern(Context c) {
|
||||
super(c);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getListenEvents() {
|
||||
return PhoneStateListener.LISTEN_SIGNAL_STRENGTHS;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a human readable description for the network type.
|
||||
*
|
||||
* @param type The integer constant for the network type
|
||||
* @return A human readable description of the network type
|
||||
*/
|
||||
@Override
|
||||
protected String getTypeAsString(int type) {
|
||||
switch (type) {
|
||||
case TelephonyManager.NETWORK_TYPE_1xRTT:
|
||||
return "1xRTT";
|
||||
case TelephonyManager.NETWORK_TYPE_CDMA:
|
||||
return "CDMA";
|
||||
case TelephonyManager.NETWORK_TYPE_EDGE:
|
||||
return "EDGE";
|
||||
case TelephonyManager.NETWORK_TYPE_EVDO_0:
|
||||
return "EVDO 0";
|
||||
case TelephonyManager.NETWORK_TYPE_EVDO_A:
|
||||
return "EVDO A";
|
||||
case TelephonyManager.NETWORK_TYPE_GPRS:
|
||||
return "GPRS";
|
||||
case TelephonyManager.NETWORK_TYPE_HSDPA:
|
||||
return "HSDPA";
|
||||
case TelephonyManager.NETWORK_TYPE_HSPA:
|
||||
return "HSPA";
|
||||
case TelephonyManager.NETWORK_TYPE_HSUPA:
|
||||
return "HSUPA";
|
||||
case TelephonyManager.NETWORK_TYPE_UMTS:
|
||||
return "UMTS";
|
||||
case TelephonyManager.NETWORK_TYPE_UNKNOWN:
|
||||
default:
|
||||
return "UNKNOWN";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the url for the waypoint icon for the current network type.
|
||||
*
|
||||
* @param type The network type
|
||||
* @return A url to a image to use as the waypoint icon
|
||||
*/
|
||||
@Override
|
||||
protected String getIcon(int type) {
|
||||
switch (type) {
|
||||
case TelephonyManager.NETWORK_TYPE_1xRTT:
|
||||
case TelephonyManager.NETWORK_TYPE_CDMA:
|
||||
case TelephonyManager.NETWORK_TYPE_GPRS:
|
||||
case TelephonyManager.NETWORK_TYPE_EDGE:
|
||||
return "http://maps.google.com/mapfiles/ms/micons/green.png";
|
||||
case TelephonyManager.NETWORK_TYPE_EVDO_0:
|
||||
case TelephonyManager.NETWORK_TYPE_EVDO_A:
|
||||
case TelephonyManager.NETWORK_TYPE_HSDPA:
|
||||
case TelephonyManager.NETWORK_TYPE_HSPA:
|
||||
case TelephonyManager.NETWORK_TYPE_HSUPA:
|
||||
case TelephonyManager.NETWORK_TYPE_UMTS:
|
||||
return "http://maps.google.com/mapfiles/ms/micons/blue.png";
|
||||
case TelephonyManager.NETWORK_TYPE_UNKNOWN:
|
||||
default:
|
||||
return "http://maps.google.com/mapfiles/ms/micons/red.png";
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getStrengthAsString() {
|
||||
if (signalStrength == null) {
|
||||
return "Strength: " + getContext().getString(R.string.unknown) + "\n";
|
||||
}
|
||||
StringBuffer sb = new StringBuffer();
|
||||
if (signalStrength.isGsm()) {
|
||||
appendSignal(signalStrength.getGsmSignalStrength(),
|
||||
R.string.gsm_strength,
|
||||
sb);
|
||||
maybeAppendSignal(signalStrength.getGsmBitErrorRate(),
|
||||
R.string.error_rate,
|
||||
sb);
|
||||
} else {
|
||||
appendSignal(signalStrength.getCdmaDbm(), R.string.cdma_strength, sb);
|
||||
appendSignal(signalStrength.getCdmaEcio() / 10.0, R.string.ecio, sb);
|
||||
appendSignal(signalStrength.getEvdoDbm(), R.string.evdo_strength, sb);
|
||||
appendSignal(signalStrength.getEvdoEcio() / 10.0, R.string.ecio, sb);
|
||||
appendSignal(signalStrength.getEvdoSnr(),
|
||||
R.string.signal_to_noise_ratio,
|
||||
sb);
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private void maybeAppendSignal(
|
||||
int signal, int signalFormat, StringBuffer sb) {
|
||||
if (signal > 0) {
|
||||
sb.append(getContext().getString(signalFormat, signal));
|
||||
}
|
||||
}
|
||||
|
||||
private void appendSignal(int signal, int signalFormat, StringBuffer sb) {
|
||||
sb.append(getContext().getString(signalFormat, signal));
|
||||
sb.append("\n");
|
||||
}
|
||||
|
||||
private void appendSignal(double signal, int signalFormat, StringBuffer sb) {
|
||||
sb.append(getContext().getString(signalFormat, signal));
|
||||
sb.append("\n");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSignalStrengthsChanged(SignalStrength signalStrength) {
|
||||
Log.d(MyTracksConstants.TAG, "Signal Strength Modern: " + signalStrength);
|
||||
this.signalStrength = signalStrength;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
/*
|
||||
* 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.MyTracksConstants;
|
||||
import com.google.android.apps.mytracks.util.UnitConversions;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
/**
|
||||
* This class manages inserting time or distance splits.
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
*/
|
||||
public class SplitManager {
|
||||
|
||||
/**
|
||||
* The frequency of splits.
|
||||
*/
|
||||
private int splitFrequency = 0;
|
||||
|
||||
/**
|
||||
* The next distance when a split should be taken.
|
||||
*/
|
||||
private double nextSplitDistance = 0;
|
||||
|
||||
/**
|
||||
* Splits executer.
|
||||
*/
|
||||
private PeriodicTaskExecuter splitExecuter = null;
|
||||
|
||||
private boolean metricUnits;
|
||||
|
||||
private final TrackRecordingService service;
|
||||
|
||||
public SplitManager(TrackRecordingService service) {
|
||||
this.service = service;
|
||||
}
|
||||
|
||||
/**
|
||||
* Restores the manager.
|
||||
*/
|
||||
public void restore() {
|
||||
if ((splitFrequency > 0) && (splitExecuter != null)) {
|
||||
splitExecuter.scheduleTask(splitFrequency * 60000);
|
||||
}
|
||||
calculateNextSplit();
|
||||
}
|
||||
|
||||
/**
|
||||
* Shuts down the manager.
|
||||
*/
|
||||
public void shutdown() {
|
||||
if (splitExecuter != null) {
|
||||
splitExecuter.shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the next distance that a split should be inserted at.
|
||||
*/
|
||||
public void calculateNextSplit() {
|
||||
if (splitFrequency >= 0) {
|
||||
nextSplitDistance = Double.MAX_VALUE;
|
||||
Log.d(MyTracksConstants.TAG,
|
||||
"SplitManager: Distance splits dissabled.");
|
||||
return;
|
||||
}
|
||||
|
||||
double dist = service.getTripStatistics().getTotalDistance() / 1000;
|
||||
if (!metricUnits) {
|
||||
dist *= UnitConversions.KM_TO_MI;
|
||||
}
|
||||
// The index will be negative since the frequency is negative.
|
||||
int index = (int) (dist / splitFrequency);
|
||||
index -= 1;
|
||||
nextSplitDistance = splitFrequency * index;
|
||||
Log.d(MyTracksConstants.TAG,
|
||||
"SplitManager: Next split distance: " + nextSplitDistance);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates split manager with new trip statistics.
|
||||
*/
|
||||
public void updateSplits() {
|
||||
if (this.splitFrequency >= 0) {
|
||||
return;
|
||||
}
|
||||
// Convert the distance in meters to km or mi.
|
||||
double distance = service.getTripStatistics().getTotalDistance() / 1000.0;
|
||||
if (!metricUnits) {
|
||||
distance *= UnitConversions.KM_TO_MI;
|
||||
}
|
||||
|
||||
if (distance > this.nextSplitDistance) {
|
||||
service.insertStatisticsMarker(service.getLastLocation());
|
||||
calculateNextSplit();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the split frequency.
|
||||
* < 0 Use the absolute value as a distance in the current measurement km
|
||||
* or mi
|
||||
* 0 Turn off splits
|
||||
* > 0 Use the value as a time in minutes
|
||||
* @param splitFrequency The frequency in time or distance
|
||||
*/
|
||||
public void setSplitFrequency(int splitFrequency) {
|
||||
this.splitFrequency = splitFrequency;
|
||||
if (splitFrequency < 1) {
|
||||
if (splitExecuter != null) {
|
||||
splitExecuter.shutdown();
|
||||
splitExecuter = null;
|
||||
}
|
||||
}
|
||||
if (splitFrequency > 0) {
|
||||
if (splitExecuter == null) {
|
||||
TimeSplitTask splitter = new TimeSplitTask();
|
||||
splitExecuter = new PeriodicTaskExecuter(splitter, service);
|
||||
}
|
||||
splitExecuter.scheduleTask(splitFrequency * 60000);
|
||||
} else {
|
||||
// For distance based splits.
|
||||
calculateNextSplit();
|
||||
}
|
||||
}
|
||||
|
||||
public void setMetricUnits(boolean metricUnits) {
|
||||
this.metricUnits = metricUnits;
|
||||
calculateNextSplit();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
import com.google.android.apps.mytracks.MyTracksConstants;
|
||||
import com.google.android.apps.mytracks.MyTracksSettings;
|
||||
import com.google.android.apps.mytracks.stats.TripStatistics;
|
||||
import com.google.android.apps.mytracks.util.StringUtils;
|
||||
import com.google.android.apps.mytracks.util.UnitConversions;
|
||||
import com.google.android.maps.mytracks.R;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.speech.tts.TextToSpeech;
|
||||
import android.util.Log;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* This class will periodically announce the user's trip statitics.
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
*/
|
||||
public class StatusAnnouncerTask
|
||||
implements TextToSpeech.OnInitListener, PeriodicTask {
|
||||
|
||||
/**
|
||||
* The rate at which announcements are spoken.
|
||||
*/
|
||||
private static final float TTS_SPEECH_RATE = 0.9f;
|
||||
|
||||
/**
|
||||
* A pointer to the service context.
|
||||
*/
|
||||
private final Context context;
|
||||
|
||||
/**
|
||||
* String utilities.
|
||||
*/
|
||||
private final StringUtils stringUtils;
|
||||
|
||||
/**
|
||||
* The interface to the text to speech engine.
|
||||
*/
|
||||
private TextToSpeech tts = null;
|
||||
|
||||
/**
|
||||
* The response recieved from the TTS engine ater initialization.
|
||||
*/
|
||||
private int ttsStatus = TextToSpeech.ERROR;
|
||||
|
||||
/**
|
||||
* Constructs the announcer and start the TTS engine.
|
||||
*/
|
||||
public StatusAnnouncerTask(Context context) {
|
||||
this.context = context;
|
||||
this.stringUtils = new StringUtils(context);
|
||||
|
||||
tts = new TextToSpeech(context, this);
|
||||
// Force the language to be the same as the string we will be speaking.
|
||||
tts.setLanguage(Locale.getDefault());
|
||||
// Slow down the speed just a bit as it is hard to hear when exercising.
|
||||
tts.setSpeechRate(TTS_SPEECH_RATE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies that the tts engine is done with initialization.
|
||||
*/
|
||||
public void onInit(int status) {
|
||||
this.ttsStatus = status;
|
||||
Log.i(MyTracksConstants.TAG, "TrackRecordingService.TTS init: " + status);
|
||||
}
|
||||
|
||||
/**
|
||||
* Announces the trip status.
|
||||
*/
|
||||
@Override
|
||||
public void run(TrackRecordingService service) {
|
||||
if (ttsStatus != TextToSpeech.SUCCESS && tts != null) {
|
||||
Log.e(MyTracksConstants.TAG, "StatusAnnouncer Tts not initialized.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (service == null || service.getTripStatistics() == null) {
|
||||
Log.e(MyTracksConstants.TAG, "StatusAnnouncer stats not initialized.");
|
||||
return;
|
||||
}
|
||||
|
||||
String announcement = getAnnouncement(service.getTripStatistics());
|
||||
Log.d(MyTracksConstants.TAG, "Announcement: " + announcement);
|
||||
tts.speak(announcement, TextToSpeech.QUEUE_FLUSH, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the announcement string.
|
||||
*
|
||||
* @return The string that will be read to the user
|
||||
*/
|
||||
private String getAnnouncement(final TripStatistics stats) {
|
||||
SharedPreferences preferences =
|
||||
context.getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0);
|
||||
boolean metricUnits = true;
|
||||
boolean reportSpeed = true;
|
||||
if (preferences != null) {
|
||||
metricUnits = preferences.getBoolean(MyTracksSettings.METRIC_UNITS,
|
||||
true);
|
||||
reportSpeed = preferences.getBoolean(MyTracksSettings.REPORT_SPEED,
|
||||
true);
|
||||
}
|
||||
|
||||
double d = stats.getTotalDistance() / 1000;
|
||||
double s = stats.getAverageMovingSpeed() * 3.6;
|
||||
if (d == 0) {
|
||||
return context.getString(R.string.announce_no_distance);
|
||||
}
|
||||
|
||||
int speedLabel;
|
||||
if (metricUnits) {
|
||||
if (reportSpeed) {
|
||||
speedLabel = R.string.kilometer_per_hour_long;
|
||||
} else {
|
||||
speedLabel = R.string.per_kilometer;
|
||||
}
|
||||
} else {
|
||||
s *= UnitConversions.KMH_TO_MPH;
|
||||
d *= UnitConversions.KM_TO_MI;
|
||||
if (reportSpeed) {
|
||||
speedLabel = R.string.mile_per_hour_long;
|
||||
} else {
|
||||
speedLabel = R.string.per_mile;
|
||||
}
|
||||
}
|
||||
|
||||
String speed = null;
|
||||
if ((s == 0) || Double.isNaN(s)) {
|
||||
speed = context.getString(R.string.unknown);
|
||||
} else {
|
||||
if (reportSpeed) {
|
||||
speed = String.format("%.1f", s);
|
||||
} else {
|
||||
double pace = 3600000.0 / s;
|
||||
Log.w(MyTracksConstants.TAG,
|
||||
"Converted speed: " + s + " to pace: " + pace);
|
||||
speed = stringUtils.formatTimeLong((long) pace);
|
||||
}
|
||||
}
|
||||
|
||||
return context.getString(R.string.announce_template,
|
||||
context.getString(R.string.total_distance_label),
|
||||
d,
|
||||
context.getString(metricUnits
|
||||
? R.string.kilometers_long
|
||||
: R.string.miles_long),
|
||||
stringUtils.formatTimeLong(stats.getMovingTime()),
|
||||
speed,
|
||||
context.getString(speedLabel));
|
||||
}
|
||||
|
||||
public void shutdown() {
|
||||
tts.shutdown();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
* 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 android.util.Log;
|
||||
|
||||
import com.google.android.apps.mytracks.MyTracksConstants;
|
||||
|
||||
/**
|
||||
* This class will manage a period task executer.
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
*/
|
||||
public class TaskExecuterManager {
|
||||
|
||||
int frequency;
|
||||
PeriodicTask task;
|
||||
PeriodicTaskExecuter executer;
|
||||
|
||||
public TaskExecuterManager(int frequency,
|
||||
PeriodicTask task,
|
||||
TrackRecordingService service) {
|
||||
this.task = task;
|
||||
setFrequency(frequency, service);
|
||||
}
|
||||
|
||||
public int getFrequency() {
|
||||
return frequency;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the frequency that the task should be run at.
|
||||
* If needed the task will be scheduled.
|
||||
*
|
||||
* @param frequency The frequency in minutes for the task to run
|
||||
* @param service The service to run the task on
|
||||
*/
|
||||
public void setFrequency(int frequency, TrackRecordingService service) {
|
||||
this.frequency = frequency;
|
||||
Log.i(MyTracksConstants.TAG, "Frequency set to: " + frequency);
|
||||
|
||||
if (frequency == -1) {
|
||||
if (executer != null) {
|
||||
executer.shutdown();
|
||||
executer = null;
|
||||
Log.i(MyTracksConstants.TAG,
|
||||
"Shut down service: " + task.getClass().getSimpleName());
|
||||
}
|
||||
} else {
|
||||
if (executer == null) {
|
||||
task.start();
|
||||
executer = new PeriodicTaskExecuter(task, service);
|
||||
}
|
||||
executer.scheduleTask(frequency * 60000);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore the task at the current frequency.
|
||||
*/
|
||||
public void restore() {
|
||||
if (frequency > 0) {
|
||||
task.start();
|
||||
executer.scheduleTask(frequency * 60000);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* A simple task to insert statistics markers every n minutes.
|
||||
* @author Sandor Dornbush
|
||||
*/
|
||||
public class TimeSplitTask implements PeriodicTask {
|
||||
|
||||
@Override
|
||||
public void run(TrackRecordingService service) {
|
||||
service.insertStatisticsMarker(service.getLastLocation());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void shutdown() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,964 @@
|
||||
/*
|
||||
* Copyright 2008 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
package com.google.android.apps.mytracks.services;
|
||||
|
||||
import com.google.android.apps.mytracks.MyTracks;
|
||||
import com.google.android.apps.mytracks.MyTracksConstants;
|
||||
import com.google.android.apps.mytracks.MyTracksSettings;
|
||||
import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
|
||||
import com.google.android.apps.mytracks.content.Track;
|
||||
import com.google.android.apps.mytracks.content.TracksColumns;
|
||||
import com.google.android.apps.mytracks.content.Waypoint;
|
||||
import com.google.android.apps.mytracks.content.WaypointsColumns;
|
||||
import com.google.android.apps.mytracks.stats.TripStatistics;
|
||||
import com.google.android.apps.mytracks.util.MyTracksUtils;
|
||||
import com.google.android.apps.mytracks.util.StringUtils;
|
||||
import com.google.android.maps.mytracks.R;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.app.Service;
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.database.Cursor;
|
||||
import android.database.sqlite.SQLiteException;
|
||||
import android.location.Location;
|
||||
import android.location.LocationListener;
|
||||
import android.location.LocationManager;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.IBinder;
|
||||
import android.os.PowerManager;
|
||||
import android.os.PowerManager.WakeLock;
|
||||
import android.util.Log;
|
||||
|
||||
import java.util.Timer;
|
||||
import java.util.TimerTask;
|
||||
|
||||
/**
|
||||
* A background service that registers a location listener and records track
|
||||
* points. Track points are saved to the MyTracksProvider.
|
||||
*
|
||||
* @author Leif Hendrik Wilden
|
||||
*/
|
||||
public class TrackRecordingService extends Service implements LocationListener {
|
||||
|
||||
private static final String STATISTICS_ICON_URL =
|
||||
"http://maps.google.com/mapfiles/ms/micons/ylw-pushpin.png";
|
||||
|
||||
private NotificationManager notificationManager;
|
||||
private LocationManager locationManager;
|
||||
private WakeLock wakeLock;
|
||||
|
||||
private int minRecordingInterval =
|
||||
MyTracksSettings.DEFAULT_MIN_RECORDING_INTERVAL;
|
||||
private int minRecordingDistance =
|
||||
MyTracksSettings.DEFAULT_MIN_RECORDING_DISTANCE;
|
||||
private int maxRecordingDistance =
|
||||
MyTracksSettings.DEFAULT_MAX_RECORDING_DISTANCE;
|
||||
private int minRequiredAccuracy =
|
||||
MyTracksSettings.DEFAULT_MIN_REQUIRED_ACCURACY;
|
||||
private long recordingTrackId = -1;
|
||||
|
||||
private long currentWaypointId = -1;
|
||||
|
||||
/**
|
||||
* For debugging. Keep track of calls to onCreate().
|
||||
*/
|
||||
private boolean onCreateWasCalled = false;
|
||||
|
||||
/** The timer posts a runnable to the main thread via this handler. */
|
||||
private final Handler handler = new Handler();
|
||||
|
||||
/**
|
||||
* Utilities to deal with the database.
|
||||
*/
|
||||
private MyTracksProviderUtils providerUtils;
|
||||
|
||||
private TripStatistics stats = new TripStatistics();
|
||||
private TripStatistics waypointStats = new TripStatistics();
|
||||
|
||||
/**
|
||||
* Current length of the recorded track. This length is calculated from the
|
||||
* recorded points (as compared to each location fix). It's used to overlay
|
||||
* waypoints precisely in the elevation profile chart.
|
||||
*/
|
||||
private double length = 0;
|
||||
|
||||
/**
|
||||
* Status announcer executer.
|
||||
*/
|
||||
private PeriodicTaskExecuter executer;
|
||||
private TaskExecuterManager signalManager;
|
||||
private SplitManager splitManager;
|
||||
|
||||
/**
|
||||
* The interval in milliseconds that we have requested to be notfied of gps
|
||||
* readings.
|
||||
*/
|
||||
private long currentRecordingInterval = 0;
|
||||
|
||||
/**
|
||||
* The policy used to decide how often we should request gps updates.
|
||||
*/
|
||||
private LocationListenerPolicy locationListenerPolicy =
|
||||
new AbsoluteLocationListenerPolicy(0);
|
||||
|
||||
/**
|
||||
* Task invoked by a timer periodically to make sure the location listener is
|
||||
* still registered.
|
||||
*/
|
||||
private TimerTask checkLocationListener = new TimerTask() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (!onCreateWasCalled) {
|
||||
Log.e(MyTracksConstants.TAG,
|
||||
"TrackRecordingService is running, but onCreate not called.");
|
||||
}
|
||||
if (isRecording) {
|
||||
handler.post(new Runnable() {
|
||||
public void run() {
|
||||
Log.d(MyTracksConstants.TAG,
|
||||
"Re-registering location listener with TrackRecordingService.");
|
||||
unregisterLocationListener();
|
||||
registerLocationListener();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
Log.w(MyTracksConstants.TAG,
|
||||
"Track recording service is paused. That should not be.");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* This timer invokes periodically the checkLocationListener timer task.
|
||||
*/
|
||||
private final Timer timer = new Timer();
|
||||
|
||||
/**
|
||||
* Is the phone currently moving?
|
||||
*/
|
||||
private boolean isMoving = true;
|
||||
|
||||
/**
|
||||
* Is the service currently recording a track?
|
||||
*/
|
||||
private boolean isRecording = false;
|
||||
|
||||
/**
|
||||
* Last good location the service has received from the location listener
|
||||
*/
|
||||
private Location lastLocation = null;
|
||||
|
||||
/**
|
||||
* Last valid location (i.e. not a marker) that was recorded.
|
||||
*/
|
||||
private Location lastValidLocation = null;
|
||||
|
||||
/**
|
||||
* The frequency of status announcements.
|
||||
*/
|
||||
private int announcementFrequency = -1;
|
||||
|
||||
private static boolean mTTSAvailable;
|
||||
|
||||
/* establish whether the "new" class is available to us */
|
||||
static {
|
||||
try {
|
||||
SafeStatusAnnouncerTask.checkAvailable();
|
||||
mTTSAvailable = true;
|
||||
} catch (Throwable t) {
|
||||
mTTSAvailable = false;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Utility functions
|
||||
*/
|
||||
|
||||
/**
|
||||
* Inserts a new location in the track points db and updates the corresponding
|
||||
* track in the track db.
|
||||
*
|
||||
* @param recordingTrack the track that is currently being recorded
|
||||
* @param location the location to be inserted
|
||||
* @param lastRecordedLocation the last recorded location before this one (or
|
||||
* null if none)
|
||||
* @param lastRecordedLocationId the id of the last recorded location (or -1
|
||||
* if none)
|
||||
* @param trackId the id of the track
|
||||
* @return true if successful. False if SQLite3 threw an exception.
|
||||
*/
|
||||
private boolean insertLocation(Track recordingTrack, Location location,
|
||||
Location lastRecordedLocation, long lastRecordedLocationId,
|
||||
long trackId) {
|
||||
|
||||
// Keep track of length along recorded track (needed when a waypoint is
|
||||
// inserted):
|
||||
if (MyTracksUtils.isValidLocation(location)) {
|
||||
if (lastValidLocation != null) {
|
||||
length += location.distanceTo(lastValidLocation);
|
||||
}
|
||||
lastValidLocation = location;
|
||||
}
|
||||
|
||||
// Insert the new location:
|
||||
try {
|
||||
Uri pointUri = providerUtils.insertTrackPoint(location, trackId);
|
||||
int pointId = Integer.parseInt(pointUri.getLastPathSegment());
|
||||
|
||||
// Update the current track:
|
||||
if (lastRecordedLocation != null
|
||||
&& lastRecordedLocation.getLatitude() < 90) {
|
||||
ContentValues values = new ContentValues();
|
||||
if (recordingTrack.getStartId() < 0) {
|
||||
values.put(TracksColumns.STARTID, pointId);
|
||||
recordingTrack.setStartId(pointId);
|
||||
}
|
||||
values.put(TracksColumns.STOPID, pointId);
|
||||
values.put(TracksColumns.STOPTIME, System.currentTimeMillis());
|
||||
values.put(TracksColumns.NUMPOINTS,
|
||||
recordingTrack.getNumberOfPoints() + 1);
|
||||
values.put(TracksColumns.MINLAT, stats.getBottom());
|
||||
values.put(TracksColumns.MAXLAT, stats.getTop());
|
||||
values.put(TracksColumns.MINLON, stats.getLeft());
|
||||
values.put(TracksColumns.MAXLON, stats.getRight());
|
||||
values.put(TracksColumns.TOTALDISTANCE, stats.getTotalDistance());
|
||||
values.put(TracksColumns.TOTALTIME, stats.getTotalTime());
|
||||
values.put(TracksColumns.MOVINGTIME, stats.getMovingTime());
|
||||
values.put(TracksColumns.AVGSPEED, stats.getAverageSpeed());
|
||||
values.put(TracksColumns.AVGMOVINGSPEED, stats.getAverageMovingSpeed());
|
||||
values.put(TracksColumns.MAXSPEED, stats.getMaxSpeed());
|
||||
values.put(TracksColumns.MINELEVATION, stats.getMinElevation());
|
||||
values.put(TracksColumns.MAXELEVATION, stats.getMaxElevation());
|
||||
values.put(TracksColumns.ELEVATIONGAIN, stats.getTotalElevationGain());
|
||||
values.put(TracksColumns.MINGRADE, stats.getMinGrade());
|
||||
values.put(TracksColumns.MAXGRADE, stats.getMaxGrade());
|
||||
getContentResolver().update(TracksColumns.CONTENT_URI,
|
||||
values, "_id=" + recordingTrack.getId(), null);
|
||||
updateCurrentWaypoint();
|
||||
}
|
||||
} catch (SQLiteException e) {
|
||||
// Insert failed, most likely because of SqlLite error code 5
|
||||
// (SQLite_BUSY). This is expected to happen extremely rarely (if our
|
||||
// listener gets invoked twice at about the same time).
|
||||
Log.w(MyTracksConstants.TAG,
|
||||
"Caught SQLiteException: " + e.getMessage(), e);
|
||||
return false;
|
||||
}
|
||||
splitManager.updateSplits();
|
||||
return true;
|
||||
}
|
||||
|
||||
private void updateCurrentWaypoint() {
|
||||
if (currentWaypointId >= 0) {
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(WaypointsColumns.STARTTIME, waypointStats.getStartTime());
|
||||
values.put(WaypointsColumns.LENGTH, length);
|
||||
values.put(WaypointsColumns.DURATION,
|
||||
System.currentTimeMillis() - stats.getStartTime());
|
||||
values.put(WaypointsColumns.TOTALDISTANCE,
|
||||
waypointStats.getTotalDistance());
|
||||
values.put(WaypointsColumns.TOTALTIME, waypointStats.getTotalTime());
|
||||
values.put(WaypointsColumns.MOVINGTIME, waypointStats.getMovingTime());
|
||||
values.put(WaypointsColumns.AVGSPEED, waypointStats.getAverageSpeed());
|
||||
values.put(WaypointsColumns.AVGMOVINGSPEED,
|
||||
waypointStats.getAverageMovingSpeed());
|
||||
values.put(WaypointsColumns.MAXSPEED, waypointStats.getMaxSpeed());
|
||||
values.put(WaypointsColumns.MINELEVATION,
|
||||
waypointStats.getMinElevation());
|
||||
values.put(WaypointsColumns.MAXELEVATION,
|
||||
waypointStats.getMaxElevation());
|
||||
values.put(WaypointsColumns.ELEVATIONGAIN,
|
||||
waypointStats.getTotalElevationGain());
|
||||
values.put(WaypointsColumns.MINGRADE, waypointStats.getMinGrade());
|
||||
values.put(WaypointsColumns.MAXGRADE, waypointStats.getMaxGrade());
|
||||
getContentResolver().update(WaypointsColumns.CONTENT_URI,
|
||||
values, "_id=" + currentWaypointId, null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to acquire a partial wake lock if not already acquired. Logs errors
|
||||
* and gives up trying in case the wake lock cannot be acquired.
|
||||
*/
|
||||
public void acquireWakeLock() {
|
||||
try {
|
||||
PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
|
||||
if (pm == null) {
|
||||
Log.e(MyTracksConstants.TAG,
|
||||
"TrackRecordingService: Power manager not found!");
|
||||
return;
|
||||
}
|
||||
if (wakeLock == null) {
|
||||
wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,
|
||||
MyTracksConstants.TAG);
|
||||
if (wakeLock == null) {
|
||||
Log.e(MyTracksConstants.TAG,
|
||||
"TrackRecordingService: Could not create wake lock (null).");
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (!wakeLock.isHeld()) {
|
||||
wakeLock.acquire();
|
||||
if (!wakeLock.isHeld()) {
|
||||
Log.e(MyTracksConstants.TAG,
|
||||
"TrackRecordingService: Could not acquire wake lock.");
|
||||
}
|
||||
}
|
||||
} catch (RuntimeException e) {
|
||||
Log.e(MyTracksConstants.TAG,
|
||||
"TrackRecordingService: Caught unexpected exception: "
|
||||
+ e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows the notification message and icon in the notification bar.
|
||||
*/
|
||||
public void showNotification() {
|
||||
if (isRecording) {
|
||||
Notification notification = new Notification(
|
||||
R.drawable.arrow_320, null /* tickerText */,
|
||||
System.currentTimeMillis());
|
||||
PendingIntent contentIntent = PendingIntent.getActivity(
|
||||
this, 0 /* requestCode */, new Intent(this, MyTracks.class),
|
||||
0 /* flags */);
|
||||
notification.setLatestEventInfo(this, getString(R.string.app_name),
|
||||
getString(R.string.recording_your_track), contentIntent);
|
||||
notification.flags += Notification.FLAG_NO_CLEAR;
|
||||
notificationManager.notify(1, notification);
|
||||
} else {
|
||||
notificationManager.cancelAll();
|
||||
}
|
||||
}
|
||||
|
||||
public void registerLocationListener() {
|
||||
if (locationManager == null) {
|
||||
Log.e(MyTracksConstants.TAG,
|
||||
"TrackRecordingService: Do not have any location manager.");
|
||||
return;
|
||||
}
|
||||
Log.d(MyTracksConstants.TAG,
|
||||
"Preparing to register location listener w/ TrackRecordingService...");
|
||||
try {
|
||||
long desiredInterval = locationListenerPolicy.getDesiredPollingInterval();
|
||||
locationManager.requestLocationUpdates(
|
||||
MyTracksConstants.GPS_PROVIDER, desiredInterval,
|
||||
locationListenerPolicy.getMinDistance(),
|
||||
// , 0 /* minDistance, get all updates to properly time pauses */
|
||||
TrackRecordingService.this);
|
||||
currentRecordingInterval = desiredInterval;
|
||||
Log.d(MyTracksConstants.TAG,
|
||||
"...location listener now registered w/ TrackRecordingService @ "
|
||||
+ currentRecordingInterval);
|
||||
} catch (RuntimeException e) {
|
||||
Log.e(MyTracksConstants.TAG,
|
||||
"Could not register location listener: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
public void unregisterLocationListener() {
|
||||
if (locationManager == null) {
|
||||
Log.e(MyTracksConstants.TAG,
|
||||
"TrackRecordingService: Do not have any location manager.");
|
||||
return;
|
||||
}
|
||||
locationManager.removeUpdates(this);
|
||||
Log.d(MyTracksConstants.TAG,
|
||||
"Location listener now unregistered w/ TrackRecordingService.");
|
||||
}
|
||||
|
||||
private void restoreStats() {
|
||||
if (recordingTrackId < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
Track track = providerUtils.getTrack(recordingTrackId);
|
||||
if (track == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
stats = new TripStatistics(track.getStartTime());
|
||||
if (executer != null) {
|
||||
executer.scheduleTask(announcementFrequency * 60000);
|
||||
}
|
||||
|
||||
signalManager.restore();
|
||||
splitManager.restore();
|
||||
length = 0;
|
||||
lastValidLocation = null;
|
||||
|
||||
Waypoint waypoint = providerUtils.getFirstWaypoint(recordingTrackId);
|
||||
if (waypoint != null) {
|
||||
currentWaypointId = waypoint.getId();
|
||||
waypointStats = new TripStatistics(waypoint);
|
||||
} else {
|
||||
// This should never happen, but we got to do something so life goes on:
|
||||
waypointStats = new TripStatistics(track.getStartTime());
|
||||
currentWaypointId = -1;
|
||||
}
|
||||
|
||||
Cursor cursor = null;
|
||||
try {
|
||||
cursor = providerUtils.getLocationsCursor(
|
||||
recordingTrackId, -1, MyTracksConstants.MAX_LOADED_TRACK_POINTS,
|
||||
true);
|
||||
if (cursor != null) {
|
||||
if (cursor.moveToLast()) {
|
||||
do {
|
||||
Location location = providerUtils.createLocation(cursor);
|
||||
if (MyTracksUtils.isValidLocation(location)) {
|
||||
stats.addLocation(location, location.getTime());
|
||||
if (lastValidLocation != null) {
|
||||
length += location.distanceTo(lastValidLocation);
|
||||
}
|
||||
lastValidLocation = location;
|
||||
}
|
||||
} while (cursor.moveToPrevious());
|
||||
}
|
||||
stats.setMovingTime(track.getMovingTime());
|
||||
stats.pauseAt(track.getStopTime());
|
||||
stats.resume();
|
||||
} else {
|
||||
Log.e(MyTracksConstants.TAG, "Could not get track points cursor.");
|
||||
}
|
||||
} catch (RuntimeException e) {
|
||||
Log.e(MyTracksConstants.TAG, "Error while restoring track.", e);
|
||||
} finally {
|
||||
if (cursor != null) {
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
|
||||
splitManager.calculateNextSplit();
|
||||
}
|
||||
|
||||
/*
|
||||
* Location listener implementation: =================================
|
||||
*/
|
||||
|
||||
@Override
|
||||
public void onLocationChanged(Location location) {
|
||||
Log.d(MyTracksConstants.TAG, "TrackRecordingService.onLocationChanged");
|
||||
|
||||
try {
|
||||
// Don't record if the service has been asked to pause recording:
|
||||
if (!isRecording) {
|
||||
Log.w(MyTracksConstants.TAG,
|
||||
"Not recording because recording has been paused.");
|
||||
return;
|
||||
}
|
||||
|
||||
// This should never happen, but just in case (we really don't want the
|
||||
// service
|
||||
// to crash):
|
||||
if (location == null) {
|
||||
Log.w(MyTracksConstants.TAG,
|
||||
"Location changed, but location is null.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't record if the accuracy is too bad:
|
||||
if (location.getAccuracy() > minRequiredAccuracy) {
|
||||
Log.d(MyTracksConstants.TAG,
|
||||
"Not recording. Bad accuracy.");
|
||||
return;
|
||||
}
|
||||
|
||||
// At least one track must be available for appending points:
|
||||
Track recordingTrack = providerUtils.getTrack(recordingTrackId);
|
||||
if (recordingTrack == null) {
|
||||
Log.d(MyTracksConstants.TAG,
|
||||
"Not recording. No track to append to available.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (MyTracksUtils.isValidLocation(location)) {
|
||||
long now = System.currentTimeMillis();
|
||||
stats.addLocation(location, now);
|
||||
waypointStats.addLocation(location, now);
|
||||
}
|
||||
|
||||
// Update the idle time if needed.
|
||||
locationListenerPolicy.updateIdleTime(stats.getIdleTime());
|
||||
if (currentRecordingInterval
|
||||
!= locationListenerPolicy.getDesiredPollingInterval()) {
|
||||
registerLocationListener();
|
||||
}
|
||||
|
||||
Location lastRecordedLocation = providerUtils.getLastLocation();
|
||||
long lastRecordedLocationId =
|
||||
providerUtils.getLastLocationId(recordingTrackId);
|
||||
double distanceToLastRecorded = Double.POSITIVE_INFINITY;
|
||||
if (lastRecordedLocation != null) {
|
||||
distanceToLastRecorded = location.distanceTo(lastRecordedLocation);
|
||||
}
|
||||
double distanceToLast = Double.POSITIVE_INFINITY;
|
||||
if (lastLocation != null) {
|
||||
distanceToLast = location.distanceTo(lastLocation);
|
||||
}
|
||||
|
||||
// If the user has been stationary for two recording just record the first
|
||||
// two and ignore the rest. This code will only have an effect if the
|
||||
// maxRecordingDistance = 0
|
||||
if (distanceToLast == 0) {
|
||||
if (isMoving) {
|
||||
Log.d(MyTracksConstants.TAG, "Found two identical locations.");
|
||||
isMoving = false;
|
||||
if (lastLocation != null && lastRecordedLocation != null
|
||||
&& !lastRecordedLocation.equals(lastLocation)) {
|
||||
// Need to write the last location. This will happen when
|
||||
// lastRecordedLocation.distance(lastLocation) <
|
||||
// minRecordingDistance
|
||||
if (!insertLocation(recordingTrack, lastLocation,
|
||||
lastRecordedLocation, lastRecordedLocationId,
|
||||
recordingTrackId)) {
|
||||
return;
|
||||
}
|
||||
lastRecordedLocationId++;
|
||||
}
|
||||
} else {
|
||||
Log.d(MyTracksConstants.TAG,
|
||||
"Not recording. More than two identical locations.");
|
||||
}
|
||||
} else if (distanceToLastRecorded > minRecordingDistance) {
|
||||
if (lastLocation != null && !isMoving) {
|
||||
// Last location was the last stationary location. Need to go back and
|
||||
// add it.
|
||||
if (!insertLocation(recordingTrack, lastLocation,
|
||||
lastRecordedLocation, lastRecordedLocationId, recordingTrackId)) {
|
||||
return;
|
||||
}
|
||||
lastRecordedLocationId++;
|
||||
isMoving = true;
|
||||
}
|
||||
|
||||
// If separation from last recorded point is too large insert a
|
||||
// separator
|
||||
// to indicate end of a segment:
|
||||
boolean startNewSegment =
|
||||
lastRecordedLocation != null
|
||||
&& lastRecordedLocation.getLatitude() < 90
|
||||
&& distanceToLastRecorded > maxRecordingDistance
|
||||
&& recordingTrack.getStartId() >= 0;
|
||||
if (startNewSegment) {
|
||||
// Insert a separator point to indicate start of new track:
|
||||
Log.d(MyTracksConstants.TAG, "Inserting a separator.");
|
||||
Location separator = new Location(MyTracksConstants.GPS_PROVIDER);
|
||||
separator.setLongitude(0);
|
||||
separator.setLatitude(100);
|
||||
separator.setTime(lastRecordedLocation.getTime());
|
||||
providerUtils.insertTrackPoint(separator, recordingTrackId);
|
||||
}
|
||||
|
||||
if (!insertLocation(recordingTrack, location, lastRecordedLocation,
|
||||
lastRecordedLocationId, recordingTrackId)) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
Log.d(MyTracksConstants.TAG, String.format(
|
||||
"Not recording. Distance to last recorded point (%f m) is less than"
|
||||
+ " %d m.", distanceToLastRecorded, minRecordingDistance));
|
||||
}
|
||||
} catch (Error e) {
|
||||
// Probably important enough to rethrow.
|
||||
Log.e(MyTracksConstants.TAG, "Error in onLocationChanged", e);
|
||||
throw e;
|
||||
} catch (RuntimeException e) {
|
||||
// Safe usually to trap exceptions.
|
||||
Log.e(MyTracksConstants.TAG,
|
||||
"Trapping exception in onLocationChanged", e);
|
||||
throw e;
|
||||
}
|
||||
lastLocation = location;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onProviderDisabled(String provider) {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onProviderEnabled(String provider) {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStatusChanged(String provider, int status, Bundle extras) {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
/*
|
||||
* SharedPreferencesChangeListener interface implementation. Note that
|
||||
* services don't currently receive this event (Android platform limitation).
|
||||
* This should be called from an activity whenever settings change.
|
||||
*/
|
||||
|
||||
/**
|
||||
* 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)
|
||||
*/
|
||||
public void onSharedPreferenceChanged(String key) {
|
||||
Log.d(MyTracksConstants.TAG,
|
||||
"TrackRecordingService.onSharedPreferenceChanged");
|
||||
SharedPreferences sharedPreferences =
|
||||
getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0);
|
||||
if (sharedPreferences == null) {
|
||||
Log.w(MyTracksConstants.TAG,
|
||||
"TrackRecordingService: Couldn't get shared preferences.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (key == null || key.equals(MyTracksSettings.MIN_RECORDING_DISTANCE)) {
|
||||
minRecordingDistance = sharedPreferences.getInt(
|
||||
MyTracksSettings.MIN_RECORDING_DISTANCE,
|
||||
MyTracksSettings.DEFAULT_MIN_RECORDING_DISTANCE);
|
||||
Log.d(MyTracksConstants.TAG,
|
||||
"TrackRecordingService: minRecordingDistance = "
|
||||
+ minRecordingDistance);
|
||||
}
|
||||
if (key == null || key.equals(MyTracksSettings.MAX_RECORDING_DISTANCE)) {
|
||||
maxRecordingDistance = sharedPreferences.getInt(
|
||||
MyTracksSettings.MAX_RECORDING_DISTANCE,
|
||||
MyTracksSettings.DEFAULT_MAX_RECORDING_DISTANCE);
|
||||
}
|
||||
if (key == null || key.equals(MyTracksSettings.MIN_RECORDING_INTERVAL)) {
|
||||
minRecordingInterval = sharedPreferences.getInt(
|
||||
MyTracksSettings.MIN_RECORDING_INTERVAL,
|
||||
MyTracksSettings.DEFAULT_MIN_RECORDING_INTERVAL);
|
||||
switch (minRecordingInterval) {
|
||||
case -2:
|
||||
// Battery Miser
|
||||
// min: 30 seconds
|
||||
// max: 5 minutes
|
||||
// minDist: 5 meters Choose battery life over moving time accuracy.
|
||||
locationListenerPolicy =
|
||||
new AdaptiveLocationListenerPolicy(30000, 300000, 5);
|
||||
break;
|
||||
case -1:
|
||||
// High Accuracy
|
||||
// min: 1 second
|
||||
// max: 30 seconds
|
||||
// minDist: 0 meters get all updates to properly measure moving time.
|
||||
locationListenerPolicy =
|
||||
new AdaptiveLocationListenerPolicy(1000, 30000, 0);
|
||||
break;
|
||||
default:
|
||||
locationListenerPolicy =
|
||||
new AbsoluteLocationListenerPolicy(minRecordingInterval * 1000);
|
||||
}
|
||||
}
|
||||
if (key == null || key.equals(MyTracksSettings.MIN_REQUIRED_ACCURACY)) {
|
||||
minRequiredAccuracy = sharedPreferences.getInt(
|
||||
MyTracksSettings.MIN_REQUIRED_ACCURACY,
|
||||
MyTracksSettings.DEFAULT_MIN_REQUIRED_ACCURACY);
|
||||
}
|
||||
if (key == null || key.equals(MyTracksSettings.RECORDING_TRACK)) {
|
||||
recordingTrackId =
|
||||
sharedPreferences.getLong(MyTracksSettings.RECORDING_TRACK, -1);
|
||||
}
|
||||
if (key == null || key.equals(MyTracksSettings.ANNOUNCEMENT_FREQUENCY)) {
|
||||
announcementFrequency =
|
||||
sharedPreferences.getInt(MyTracksSettings.ANNOUNCEMENT_FREQUENCY, -1);
|
||||
if (mTTSAvailable) {
|
||||
if (announcementFrequency == -1) {
|
||||
if (executer != null) {
|
||||
executer.shutdown();
|
||||
executer = null;
|
||||
}
|
||||
} else {
|
||||
if (executer == null) {
|
||||
SafeStatusAnnouncerTask announcer =
|
||||
new SafeStatusAnnouncerTask(this);
|
||||
executer = new PeriodicTaskExecuter(announcer, this);
|
||||
}
|
||||
executer.scheduleTask(announcementFrequency * 60000);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (key == null || key.equals(MyTracksSettings.SPLIT_FREQUENCY)) {
|
||||
splitManager.setSplitFrequency(
|
||||
sharedPreferences.getInt(MyTracksSettings.SPLIT_FREQUENCY, 0));
|
||||
}
|
||||
if (key == null || key.equals(MyTracksSettings.SIGNAL_SAMPLING_FREQUENCY)) {
|
||||
signalManager.setFrequency(sharedPreferences.getInt(
|
||||
MyTracksSettings.SIGNAL_SAMPLING_FREQUENCY, -1), this);
|
||||
}
|
||||
if (key == null || key.equals(MyTracksSettings.METRIC_UNITS)) {
|
||||
splitManager.setMetricUnits(
|
||||
sharedPreferences.getBoolean(MyTracksSettings.METRIC_UNITS, true));
|
||||
}
|
||||
|
||||
if (isRecording) {
|
||||
registerLocationListener();
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Application lifetime events: ============================
|
||||
*/
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
Log.d(MyTracksConstants.TAG, "TrackRecordingService.onCreate");
|
||||
super.onCreate();
|
||||
onCreateWasCalled = true;
|
||||
setForeground(true);
|
||||
providerUtils = MyTracksProviderUtils.Factory.get(this);
|
||||
notificationManager =
|
||||
(NotificationManager) getSystemService(NOTIFICATION_SERVICE);
|
||||
locationManager =
|
||||
(LocationManager) getSystemService(Context.LOCATION_SERVICE);
|
||||
splitManager = new SplitManager(this);
|
||||
try {
|
||||
signalManager =
|
||||
new TaskExecuterManager(-1, new SignalStrengthTaskModern(this), this);
|
||||
Log.d(MyTracksConstants.TAG,
|
||||
"TrackRecordingService using modern signal strength api.");
|
||||
} catch (LinkageError e) {
|
||||
Log.w(MyTracksConstants.TAG,
|
||||
"TrackRecordingService could not load modern signal strength.", e);
|
||||
signalManager =
|
||||
new TaskExecuterManager(-1, new SignalStrengthTask(this), this);
|
||||
}
|
||||
onSharedPreferenceChanged(null);
|
||||
restoreStats();
|
||||
registerLocationListener();
|
||||
acquireWakeLock();
|
||||
/**
|
||||
* After 5 min, check every minute that location listener still is
|
||||
* registered and spit out additional debugging info to the logs:
|
||||
*/
|
||||
timer.schedule(checkLocationListener, 1000 * 60 * 5, 1000 * 60);
|
||||
isRecording = true;
|
||||
if (mTTSAvailable && (announcementFrequency != -1)) {
|
||||
if (executer == null) {
|
||||
SafeStatusAnnouncerTask announcer = new SafeStatusAnnouncerTask(this);
|
||||
executer = new PeriodicTaskExecuter(announcer, this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
Log.d(MyTracksConstants.TAG, "TrackRecordingService.onDestroy");
|
||||
if (wakeLock != null && wakeLock.isHeld()) {
|
||||
wakeLock.release();
|
||||
}
|
||||
isRecording = false;
|
||||
showNotification();
|
||||
unregisterLocationListener();
|
||||
if (executer != null) {
|
||||
executer.shutdown();
|
||||
}
|
||||
splitManager.shutdown();
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
Log.d(MyTracksConstants.TAG, "TrackRecordingService.onBind");
|
||||
return binder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onUnbind(Intent intent) {
|
||||
Log.d(MyTracksConstants.TAG, "TrackRecordingService.onUnbind");
|
||||
return super.onUnbind(intent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean stopService(Intent name) {
|
||||
Log.d(MyTracksConstants.TAG, "TrackRecordingService.stopService");
|
||||
unregisterLocationListener();
|
||||
return super.stopService(name);
|
||||
}
|
||||
|
||||
public long insertWaypointMarker(Waypoint waypoint) {
|
||||
if (waypoint.getLocation() != null) {
|
||||
waypoint.setLength(length);
|
||||
waypoint.setDuration(
|
||||
waypoint.getLocation().getTime() - stats.getStartTime());
|
||||
Uri uri = providerUtils.insertWaypoint(waypoint);
|
||||
return Long.parseLong(uri.getLastPathSegment());
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts a statistics marker. A statistics marker holds the stats for the
|
||||
* last segment up to this marker.
|
||||
*
|
||||
* @param location the location where to insert
|
||||
* @return the unique id of the inserted marker
|
||||
*/
|
||||
public long insertStatisticsMarker(Location location) {
|
||||
Waypoint waypoint = new Waypoint();
|
||||
waypoint.setTrackId(recordingTrackId);
|
||||
waypoint.setType(Waypoint.TYPE_STATISTICS);
|
||||
waypoint.setName(TrackRecordingService.this.getString(R.string.statistics));
|
||||
waypointStats.fillStatisticsForWaypoint(waypoint);
|
||||
StringUtils utils = new StringUtils(TrackRecordingService.this);
|
||||
waypoint.setDescription(utils.generateWaypointDescription(waypoint));
|
||||
waypoint.setLocation(location);
|
||||
waypoint.setIcon(STATISTICS_ICON_URL);
|
||||
waypoint.setLength(length);
|
||||
final long time = System.currentTimeMillis();
|
||||
waypoint.setDuration(time - stats.getStartTime());
|
||||
waypoint.setStartTime(waypointStats.getStartTime());
|
||||
waypoint.setStartId(providerUtils.getLastLocationId(recordingTrackId));
|
||||
Uri uri = providerUtils.insertWaypoint(waypoint);
|
||||
waypointStats = new TripStatistics(time);
|
||||
updateCurrentWaypoint();
|
||||
return Long.parseLong(uri.getLastPathSegment());
|
||||
}
|
||||
|
||||
/**
|
||||
* The ITrackRecordingService is defined through IDL.
|
||||
*/
|
||||
private final ITrackRecordingService.Stub binder =
|
||||
new ITrackRecordingService.Stub() {
|
||||
@Override
|
||||
public boolean isRecording() {
|
||||
return isRecording;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getRecordingTrackId() {
|
||||
return recordingTrackId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasRecorded() {
|
||||
return providerUtils.getLastTrackId() >= 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long startNewTrack() {
|
||||
Log.d(MyTracksConstants.TAG, "TrackRecordingService.startNewTrack");
|
||||
Track track = new Track();
|
||||
track.setName("new");
|
||||
track.setStartTime(System.currentTimeMillis());
|
||||
track.setStartId(-1);
|
||||
Uri trackUri = providerUtils.insertTrack(track);
|
||||
long trackId = Long.parseLong(trackUri.getLastPathSegment());
|
||||
track.setId(trackId);
|
||||
track.setName(String.format(getString(R.string.new_track), trackId));
|
||||
providerUtils.updateTrack(track);
|
||||
recordingTrackId = trackId;
|
||||
currentWaypointId = insertStatisticsMarker(null);
|
||||
isRecording = true;
|
||||
isMoving = true;
|
||||
stats = new TripStatistics(track.getStartTime());
|
||||
if (announcementFrequency != -1 && executer != null) {
|
||||
executer.scheduleTask(announcementFrequency * 60000);
|
||||
}
|
||||
length = 0;
|
||||
showNotification();
|
||||
registerLocationListener();
|
||||
splitManager.restore();
|
||||
signalManager.restore();
|
||||
return trackId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert the given waypoint marker. Users can insert waypoint markers
|
||||
* to tag locations with a name, description, category etc.
|
||||
*
|
||||
* @param waypoint a waypoint
|
||||
* @return the unique id of the inserted marker
|
||||
*/
|
||||
@Override
|
||||
public long insertWaypointMarker(Waypoint waypoint) {
|
||||
return TrackRecordingService.this.insertWaypointMarker(waypoint);
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert a statistics marker. A statistics marker holds the stats for
|
||||
* the last segment up to this marker.
|
||||
*
|
||||
* @param location the location where to insert
|
||||
* @return the unique id of the inserted marker
|
||||
*/
|
||||
@Override
|
||||
public long insertStatisticsMarker(Location location) {
|
||||
return TrackRecordingService.this.insertStatisticsMarker(location);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void endCurrentTrack() {
|
||||
Log.d(MyTracksConstants.TAG, "TrackRecordingService.endCurrentTrack");
|
||||
isRecording = false;
|
||||
Track recordingTrack = providerUtils.getTrack(recordingTrackId);
|
||||
if (recordingTrack != null) {
|
||||
recordingTrack.setStopTime(System.currentTimeMillis());
|
||||
recordingTrack.setTotalTime(
|
||||
recordingTrack.getStopTime() - recordingTrack.getStartTime());
|
||||
long lastRecordedLocationId =
|
||||
providerUtils.getLastLocationId(recordingTrackId);
|
||||
ContentValues values = new ContentValues();
|
||||
if (lastRecordedLocationId >= 0
|
||||
&& recordingTrack.getStopId() >= 0) {
|
||||
values.put(TracksColumns.STOPID, lastRecordedLocationId);
|
||||
}
|
||||
values.put(TracksColumns.STOPTIME, recordingTrack.getStopTime());
|
||||
values.put(TracksColumns.TOTALTIME, recordingTrack.getTotalTime());
|
||||
getContentResolver().update(TracksColumns.CONTENT_URI, values,
|
||||
"_id=" + recordingTrack.getId(), null);
|
||||
}
|
||||
showNotification();
|
||||
recordingTrackId = -1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteAllTracks() {
|
||||
endCurrentTrack();
|
||||
providerUtils.deleteAllTracks();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void recordLocation(Location loc) {
|
||||
onLocationChanged(loc);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sharedPreferenceChanged(String key) {
|
||||
Log.d(MyTracksConstants.TAG,
|
||||
"TrackRecordingService.sharedPrefereneChange");
|
||||
onSharedPreferenceChanged(key);
|
||||
}
|
||||
};
|
||||
|
||||
TripStatistics getTripStatistics() {
|
||||
return stats;
|
||||
}
|
||||
|
||||
Location getLastLocation() {
|
||||
return lastLocation;
|
||||
}
|
||||
|
||||
long getRecordingTrackId() {
|
||||
return recordingTrackId;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
/*
|
||||
* 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.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.
|
||||
*/
|
||||
private int index;
|
||||
|
||||
/**
|
||||
* The sliding buffer of doubles.
|
||||
*/
|
||||
private final double[] buffer;
|
||||
|
||||
/**
|
||||
* Have all of the slots in the buffer been filled?
|
||||
*/
|
||||
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
|
||||
*/
|
||||
public DoubleBuffer(int size) {
|
||||
if (size < 1) {
|
||||
throw new IllegalArgumentException("The buffer size must be positive.");
|
||||
}
|
||||
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.
|
||||
*/
|
||||
public void reset() {
|
||||
index = 0;
|
||||
isFull = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the average of values from the buffer.
|
||||
*
|
||||
* @return 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];
|
||||
}
|
||||
return sum / numberOfEntries;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
public double[] getAverageAndVariance() {
|
||||
int numberOfEntries = isFull ? buffer.length : index;
|
||||
if (numberOfEntries == 0) {
|
||||
return new double[]{0, 0};
|
||||
}
|
||||
|
||||
double sum = 0;
|
||||
double sumSquares = 0;
|
||||
for (int i = 0; i < numberOfEntries; i++) {
|
||||
sum += buffer[i];
|
||||
sumSquares += Math.pow(buffer[i], 2);
|
||||
}
|
||||
|
||||
double average = sum / numberOfEntries;
|
||||
return new double[]{average,
|
||||
sumSquares / numberOfEntries - Math.pow(average, 2)};
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuffer stringBuffer = new StringBuffer("Full: ");
|
||||
stringBuffer.append(isFull);
|
||||
stringBuffer.append("\n");
|
||||
for (int i = 0; i < buffer.length; i++) {
|
||||
stringBuffer.append((i == index) ? "<<" : "[");
|
||||
stringBuffer.append(buffer[i]);
|
||||
stringBuffer.append((i == index) ? ">> " : "] ");
|
||||
}
|
||||
return stringBuffer.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
* 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.stats;
|
||||
|
||||
/**
|
||||
* A helper class that tracks a minimum and a maximum of a variable.
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
*/
|
||||
public class ExtremityMonitor {
|
||||
|
||||
/**
|
||||
* The smallest value seen so far.
|
||||
*/
|
||||
private double min;
|
||||
|
||||
/**
|
||||
* The largest value seen so far.
|
||||
*/
|
||||
private double max;
|
||||
|
||||
public ExtremityMonitor() {
|
||||
reset();
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the min and the max with the new value.
|
||||
*
|
||||
* @param value the new value for the monitor
|
||||
* @return true if an extremity was found
|
||||
*/
|
||||
public boolean update(double value) {
|
||||
boolean changed = false;
|
||||
if (value < min) {
|
||||
min = value;
|
||||
changed = true;
|
||||
}
|
||||
if (value > max) {
|
||||
max = value;
|
||||
changed = true;
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the minimum value seen.
|
||||
*
|
||||
* @return The minimum value passed into the update() function
|
||||
*/
|
||||
public double getMin() {
|
||||
return min;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the maximum value seen.
|
||||
*
|
||||
* @return The maximum value passed into the update() function
|
||||
*/
|
||||
public double getMax() {
|
||||
return max;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets this object to it's initial state where the min and max are unknown.
|
||||
*/
|
||||
public void reset() {
|
||||
min = Double.POSITIVE_INFINITY;
|
||||
max = Double.NEGATIVE_INFINITY;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the minimum and maximum values.
|
||||
* Useful when restoring state.
|
||||
*/
|
||||
public void set(double min, double max) {
|
||||
this.min = min;
|
||||
this.max = max;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,565 @@
|
||||
/*
|
||||
* 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.stats;
|
||||
|
||||
import com.google.android.apps.mytracks.MyTracksConstants;
|
||||
import com.google.android.apps.mytracks.content.Track;
|
||||
import com.google.android.apps.mytracks.content.Waypoint;
|
||||
|
||||
import android.location.Location;
|
||||
import android.util.Log;
|
||||
|
||||
/**
|
||||
* Statistics keeper for a trip.
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
* @author Rodrigo Damazio
|
||||
*/
|
||||
public class TripStatistics {
|
||||
/**
|
||||
* Statistical data about the trip, which can be displayed to the user.
|
||||
*/
|
||||
private final TripStatisticsData data = new TripStatisticsData();
|
||||
|
||||
/**
|
||||
* The last location that the gps reported.
|
||||
*/
|
||||
private Location lastLocation = null;
|
||||
|
||||
/**
|
||||
* The last location that contributed to the stats. It is also the last
|
||||
* location the user was found to be moving.
|
||||
*/
|
||||
private Location lastMovingLocation = null;
|
||||
|
||||
/**
|
||||
* 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(MyTracksConstants.SPEED_SMOOTHING_FACTOR);
|
||||
|
||||
/**
|
||||
* A buffer of the recent elevation readings in meters.
|
||||
*/
|
||||
private final DoubleBuffer elevationBuffer =
|
||||
new DoubleBuffer(MyTracksConstants.ELEVATION_SMOOTHING_FACTOR);
|
||||
|
||||
/**
|
||||
* A buffer of the distance between recent gps readings in meters.
|
||||
*/
|
||||
private final DoubleBuffer distanceBuffer =
|
||||
new DoubleBuffer(MyTracksConstants.DISTANCE_SMOOTHING_FACTOR);
|
||||
|
||||
/**
|
||||
* A buffer of the recent grade calculations.
|
||||
*/
|
||||
private final DoubleBuffer gradeBuffer =
|
||||
new DoubleBuffer(MyTracksConstants.GRADE_SMOOTHING_FACTOR);
|
||||
|
||||
/**
|
||||
* The total number of locations in this trip.
|
||||
*/
|
||||
private long totalLocations = 0;
|
||||
|
||||
/**
|
||||
* Creates a new trip starting at the current system time.
|
||||
*/
|
||||
public TripStatistics() {
|
||||
data.startTime = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new trip at the given start time.
|
||||
*
|
||||
* @param startTime the time that the trip started
|
||||
*/
|
||||
public TripStatistics(long startTime) {
|
||||
data.startTime = startTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new trip using the waypoint for start time and other information.
|
||||
*
|
||||
* @param waypoint the waypoint to get starting information from
|
||||
*/
|
||||
public TripStatistics(Waypoint waypoint) {
|
||||
data.startTime = waypoint.getStartTime();
|
||||
data.totalTime = waypoint.getTotalTime();
|
||||
data.movingTime = waypoint.getMovingTime();
|
||||
data.totalDistance = waypoint.getTotalDistance();
|
||||
data.totalElevationGain = waypoint.getTotalElevationGain();
|
||||
data.maxSpeed = waypoint.getMaxSpeed();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a location to the current trip. This will update all of the internal
|
||||
* variables with this new location.
|
||||
*
|
||||
* @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
|
||||
*/
|
||||
public boolean addLocation(Location currentLocation, long systemTime) {
|
||||
totalLocations++;
|
||||
|
||||
double elevationDifference = updateElevation(currentLocation.getAltitude());
|
||||
|
||||
// Update the "instant" values:
|
||||
data.totalTime = systemTime - data.startTime;
|
||||
data.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 < MyTracksConstants.MAX_NO_MOVEMENT_DISTANCE
|
||||
&& data.currentSpeed < MyTracksConstants.MAX_NO_MOVEMENT_SPEED) {
|
||||
lastLocation = currentLocation;
|
||||
return false;
|
||||
}
|
||||
|
||||
data.totalDistance += lastMovingLocation.distanceTo(currentLocation);
|
||||
updateSpeed(currentLocation.getTime(), data.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.latitudeExtremities.update(location.getLatitude());
|
||||
data.longitudeExtremities.update(location.getLongitude());
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the elevation measurements.
|
||||
*
|
||||
* @param elevation the current elevation
|
||||
*/
|
||||
// @VisibleForTesting
|
||||
double updateElevation(double elevation) {
|
||||
double oldSmoothedElevation = getSmoothedElevation();
|
||||
elevationBuffer.setNext(elevation);
|
||||
double smoothedElevation = getSmoothedElevation();
|
||||
data.elevationExtremities.update(smoothedElevation);
|
||||
double elevationDifference = elevationBuffer.isFull()
|
||||
? smoothedElevation - oldSmoothedElevation
|
||||
: 0.0;
|
||||
if (elevationDifference > 0) {
|
||||
data.totalElevationGain += elevationDifference;
|
||||
}
|
||||
return elevationDifference;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the speed measurements.
|
||||
*
|
||||
* @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
|
||||
*/
|
||||
// @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(MyTracksConstants.TAG,
|
||||
"Found negative time change: " + timeDifference);
|
||||
}
|
||||
data.movingTime += timeDifference;
|
||||
|
||||
if (isValidSpeed(updateTime, speed, lastLocationTime, lastLocationSpeed,
|
||||
speedBuffer)) {
|
||||
speedBuffer.setNext(speed);
|
||||
if (speed > data.maxSpeed) {
|
||||
data.maxSpeed = speed;
|
||||
}
|
||||
double movingSpeed = getAverageMovingSpeed();
|
||||
if (speedBuffer.isFull() && (movingSpeed > data.maxSpeed)) {
|
||||
data.maxSpeed = movingSpeed;
|
||||
}
|
||||
} else {
|
||||
Log.d(MyTracksConstants.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 > MyTracksConstants.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 < MyTracksConstants.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) {
|
||||
return;
|
||||
}
|
||||
data.currentGrade = elevationDifference / smoothedDistance;
|
||||
gradeBuffer.setNext(data.currentGrade);
|
||||
data.gradeExtremities.update(gradeBuffer.getAverage());
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
public double getSmoothedElevation() {
|
||||
return elevationBuffer.getAverage();
|
||||
}
|
||||
|
||||
/**
|
||||
* Pauses the track at the current time.
|
||||
*/
|
||||
public void pause() {
|
||||
pauseAt(System.currentTimeMillis());
|
||||
}
|
||||
|
||||
/**
|
||||
* Pauses the track at the give time.
|
||||
*
|
||||
* @param time the time to pause at
|
||||
*/
|
||||
public void pauseAt(long time) {
|
||||
data.totalTime = time - data.startTime;
|
||||
lastLocation = null; // Make sure the counter restarts.
|
||||
paused = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resumes the current track.
|
||||
*/
|
||||
public void resume() {
|
||||
// TODO: The total time is bogus after this, it will include the paused time
|
||||
paused = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the total time that this track has been active.
|
||||
*
|
||||
* @return The total number of milliseconds the track was active
|
||||
*/
|
||||
public long getTotalTime() {
|
||||
return paused
|
||||
? data.totalTime
|
||||
: System.currentTimeMillis() - data.startTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the moving time.
|
||||
*
|
||||
* @return The total number of milliseconds the user was moving
|
||||
*/
|
||||
public long getMovingTime() {
|
||||
return data.movingTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the moving time.
|
||||
*
|
||||
* @parm movingTime The total number of milliseconds the user was moving
|
||||
*/
|
||||
public void setMovingTime(long movingTime) {
|
||||
data.movingTime = movingTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the total distance the user traveled.
|
||||
*
|
||||
* @return The total distance traveled in meters
|
||||
*/
|
||||
public double getTotalDistance() {
|
||||
return data.totalDistance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the the average speed the user traveled.
|
||||
*
|
||||
* @return The average speed in m/s
|
||||
*/
|
||||
public double getAverageSpeed() {
|
||||
return getTotalDistance() / ((double) getTotalTime() / 1000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the the average speed the user traveled when they were actively
|
||||
* moving.
|
||||
*
|
||||
* @return The average moving speed in m/s
|
||||
*/
|
||||
public double getAverageMovingSpeed() {
|
||||
return getTotalDistance() / ((double) getMovingTime() / 1000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the the maximum speed for this track.
|
||||
*
|
||||
* @return The maximum speed in m/s.
|
||||
*/
|
||||
public double getMaxSpeed() {
|
||||
return data.maxSpeed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the the current speed for this track.
|
||||
*
|
||||
* @return The current speed in m/s
|
||||
*/
|
||||
public double getCurrentSpeed() {
|
||||
return data.currentSpeed;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "TripStatistics:" + " Start Time: " + data.startTime
|
||||
+ " Total Time: " + data.totalTime + " Moving Time: " + data.movingTime
|
||||
+ " Total Distance: " + data.totalDistance
|
||||
+ " Elevation Gain: " + data.totalElevationGain
|
||||
+ " Min Elevation: " + getMinElevation()
|
||||
+ " Max Elevation: " + getMaxElevation()
|
||||
+ " Average Speed: " + getAverageMovingSpeed()
|
||||
+ " Min Grade: " + getMinGrade() + " Max Grade: " + getMaxGrade()
|
||||
+ " Total Locations: " + totalLocations;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the minimum elevation seen on this trip. This is calculated from the
|
||||
* smoothed elevation so this can actually be more than the current elevation.
|
||||
*
|
||||
* @return The smallest elevation reading for this trip
|
||||
*/
|
||||
public double getMinElevation() {
|
||||
return data.elevationExtremities.getMin();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the maximum elevation seen on this trip. This is calculated from the
|
||||
* smoothed elevation so this can actually be less than the current elevation.
|
||||
*
|
||||
* @return The largest elevation reading for this trip
|
||||
*/
|
||||
public double getMaxElevation() {
|
||||
return data.elevationExtremities.getMax();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the total elevation gain for this trip. This is calculated as the sum
|
||||
* of all positive differences in the smoothed elevation.
|
||||
*
|
||||
* @return The elevation gain in meters for this trip
|
||||
*/
|
||||
public double getTotalElevationGain() {
|
||||
return data.totalElevationGain;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the time that this track started.
|
||||
*
|
||||
* @return The number of milliseconds since epoch to the time when this track
|
||||
* started
|
||||
*/
|
||||
public long getStartTime() {
|
||||
return data.startTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current grade.
|
||||
*
|
||||
* @return The current grade
|
||||
*/
|
||||
public double getGrade() {
|
||||
return data.currentGrade;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the maximum grade for this trip.
|
||||
*
|
||||
* @return The maximum grade for this trip
|
||||
*/
|
||||
public double getMaxGrade() {
|
||||
return data.gradeExtremities.getMax();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the minimum grade for this trip.
|
||||
*
|
||||
* @return The minimum grade for this trip
|
||||
*/
|
||||
public double getMinGrade() {
|
||||
return data.gradeExtremities.getMin();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the amount of time the user has been idle or 0 if they are moving.
|
||||
*/
|
||||
public long getIdleTime() {
|
||||
return lastLocation.getTime() - lastMovingLocation.getTime();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
* Returns the leftmost position (lowest longitude) of the track.
|
||||
*/
|
||||
public int getLeft() {
|
||||
return (int) (data.longitudeExtremities.getMin() * 1E6);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the rightmost position (highest longitude) of the track.
|
||||
*/
|
||||
public int getRight() {
|
||||
return (int) (data.longitudeExtremities.getMax() * 1E6);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the bottommost position (lowest latitude) of the track.
|
||||
*/
|
||||
public int getBottom() {
|
||||
return (int) (data.latitudeExtremities.getMin() * 1E6);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the topmost position (highest latitude) of the track.
|
||||
*/
|
||||
public int getTop() {
|
||||
return (int) (data.latitudeExtremities.getMax() * 1E6);
|
||||
}
|
||||
|
||||
public TripStatisticsData getData() {
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fills the given track with statistics about itself, calculated by this
|
||||
* statitics class.
|
||||
*/
|
||||
public void fillStatisticsForTrack(Track track) {
|
||||
track.setTotalDistance(getTotalDistance());
|
||||
track.setTotalTime(getTotalTime());
|
||||
track.setMovingTime(getMovingTime());
|
||||
track.setAverageSpeed(getAverageSpeed());
|
||||
track.setAverageMovingSpeed(getAverageMovingSpeed());
|
||||
track.setMaxSpeed(getMaxSpeed());
|
||||
track.setMinElevation(getMinElevation());
|
||||
track.setMaxElevation(getMaxElevation());
|
||||
track.setTotalElevationGain(getTotalElevationGain());
|
||||
track.setMinGrade(getMinGrade());
|
||||
track.setMaxGrade(getMaxGrade());
|
||||
|
||||
track.setBounds(getLeft(), getTop(), getRight(), getBottom());
|
||||
}
|
||||
|
||||
/**
|
||||
* Write all of the statistics fields to the waypoint.
|
||||
* @param waypoint The waypoint to write the trip statistics to
|
||||
*/
|
||||
public void fillStatisticsForWaypoint(Waypoint waypoint) {
|
||||
waypoint.setTotalDistance(getTotalDistance());
|
||||
waypoint.setTotalTime(getTotalTime());
|
||||
waypoint.setMovingTime(getMovingTime());
|
||||
waypoint.setAverageSpeed(getAverageSpeed());
|
||||
waypoint.setAverageMovingSpeed(getAverageMovingSpeed());
|
||||
waypoint.setMaxSpeed(getMaxSpeed());
|
||||
waypoint.setMinElevation(getMinElevation());
|
||||
waypoint.setMaxElevation(getMaxElevation());
|
||||
waypoint.setTotalElevationGain(getTotalElevationGain());
|
||||
waypoint.setMinGrade(getMinGrade());
|
||||
waypoint.setMaxGrade(getMaxGrade());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
package com.google.android.apps.mytracks.stats;
|
||||
parcelable TripStatisticsData;
|
||||
@@ -0,0 +1,309 @@
|
||||
/*
|
||||
* 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.stats;
|
||||
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
|
||||
/**
|
||||
* Statistical data about a trip.
|
||||
* The data in this class should be filled out by {@link TripStatistics}.
|
||||
*
|
||||
* TODO: Remove delegate methods from TripStatistics
|
||||
* (detail is getTotalTime)
|
||||
* TODO: Make Waypoint and Track use this instead of having the same fields
|
||||
*
|
||||
* @author Rodrigo Damazio
|
||||
*/
|
||||
public class TripStatisticsData implements Parcelable {
|
||||
|
||||
/**
|
||||
* The start time for the trip. This is system time which might not match gps
|
||||
* time.
|
||||
*/
|
||||
long startTime;
|
||||
|
||||
/**
|
||||
* The total time that we believe the user was traveling in milliseconds.
|
||||
*/
|
||||
long movingTime;
|
||||
|
||||
/**
|
||||
* The total time of the trip in milliseconds.
|
||||
* This is only updated when new points are received, so it may be stale.
|
||||
*/
|
||||
long totalTime;
|
||||
|
||||
/**
|
||||
* The total distance in meters that the user traveled on this trip.
|
||||
*/
|
||||
double totalDistance;
|
||||
|
||||
/**
|
||||
* The total elevation gained on this trip in meters.
|
||||
*/
|
||||
double totalElevationGain;
|
||||
|
||||
/**
|
||||
* The maximum speed in meters/second reported that we believe to be a valid
|
||||
* speed.
|
||||
*/
|
||||
double maxSpeed;
|
||||
|
||||
/**
|
||||
* The current speed in meters/second as reported by the gps.
|
||||
*/
|
||||
double currentSpeed;
|
||||
|
||||
/**
|
||||
* The current grade. This value is very noisy and not reported to the user.
|
||||
*/
|
||||
double currentGrade;
|
||||
|
||||
/**
|
||||
* The min and max latitude values seen in this trip.
|
||||
*/
|
||||
final ExtremityMonitor latitudeExtremities = new ExtremityMonitor();
|
||||
|
||||
/**
|
||||
* The min and max longitude values seen in this trip.
|
||||
*/
|
||||
final ExtremityMonitor longitudeExtremities = new ExtremityMonitor();
|
||||
|
||||
/**
|
||||
* The min and max elevation seen on this trip in meters.
|
||||
*/
|
||||
final ExtremityMonitor elevationExtremities = new ExtremityMonitor();
|
||||
|
||||
/**
|
||||
* The minimum and maximum grade calculations on this trip.
|
||||
*/
|
||||
final ExtremityMonitor gradeExtremities = new ExtremityMonitor();
|
||||
|
||||
/**
|
||||
* Gets the time that this track started.
|
||||
*
|
||||
* @return The number of milliseconds since epoch to the time when this track
|
||||
* started
|
||||
*/
|
||||
public long getStartTime() {
|
||||
return startTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the total time that this track has been active.
|
||||
*
|
||||
* @return The total number of milliseconds the track was active
|
||||
*/
|
||||
public long getTotalTime() {
|
||||
return totalTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the total distance the user traveled.
|
||||
*
|
||||
* @return The total distance traveled in meters
|
||||
*/
|
||||
public double getTotalDistance() {
|
||||
return totalDistance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the the maximum speed for this track.
|
||||
*
|
||||
* @return The maximum speed in m/s.
|
||||
*/
|
||||
public double getMaxSpeed() {
|
||||
return maxSpeed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the the current speed for this track.
|
||||
*
|
||||
* @return The current speed in m/s
|
||||
*/
|
||||
public double getCurrentSpeed() {
|
||||
return currentSpeed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the moving time.
|
||||
*
|
||||
* @return The total number of milliseconds the user was moving
|
||||
*/
|
||||
public long getMovingTime() {
|
||||
return movingTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the total elevation gain for this trip. This is calculated as the sum
|
||||
* of all positive differences in the smoothed elevation.
|
||||
*
|
||||
* @return The elevation gain in meters for this trip
|
||||
*/
|
||||
public double getTotalElevationGain() {
|
||||
return totalElevationGain;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current grade.
|
||||
*
|
||||
* @return The current grade
|
||||
*/
|
||||
public double getCurrentGrade() {
|
||||
return currentGrade;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the leftmost position (lowest longitude) of the track.
|
||||
*/
|
||||
public int getLeft() {
|
||||
return (int) (longitudeExtremities.getMin() * 1E6);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the rightmost position (highest longitude) of the track.
|
||||
*/
|
||||
public int getRight() {
|
||||
return (int) (longitudeExtremities.getMax() * 1E6);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the bottommost position (lowest latitude) of the track.
|
||||
*/
|
||||
public int getBottom() {
|
||||
return (int) (latitudeExtremities.getMin() * 1E6);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the topmost position (highest latitude) of the track.
|
||||
*/
|
||||
public int getTop() {
|
||||
return (int) (latitudeExtremities.getMax() * 1E6);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the minimum elevation seen on this trip. This is calculated from the
|
||||
* smoothed elevation so this can actually be more than the current elevation.
|
||||
*
|
||||
* @return The smallest elevation reading for this trip
|
||||
*/
|
||||
public double getMinElevation() {
|
||||
return elevationExtremities.getMin();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the maximum elevation seen on this trip. This is calculated from the
|
||||
* smoothed elevation so this can actually be less than the current elevation.
|
||||
*
|
||||
* @return The largest elevation reading for this trip
|
||||
*/
|
||||
public double getMaxElevation() {
|
||||
return elevationExtremities.getMax();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the maximum grade for this trip.
|
||||
*
|
||||
* @return The maximum grade for this trip
|
||||
*/
|
||||
public double getMaxGrade() {
|
||||
return gradeExtremities.getMax();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the minimum grade for this trip.
|
||||
*
|
||||
* @return The minimum grade for this trip
|
||||
*/
|
||||
public double getMinGrade() {
|
||||
return gradeExtremities.getMin();
|
||||
}
|
||||
|
||||
// Parcelable interface and creator
|
||||
|
||||
/**
|
||||
* Creator of statistics data from parcels.
|
||||
*/
|
||||
public static class Creator
|
||||
implements Parcelable.Creator<TripStatisticsData> {
|
||||
|
||||
@Override
|
||||
public TripStatisticsData createFromParcel(Parcel source) {
|
||||
TripStatisticsData data = new TripStatisticsData();
|
||||
|
||||
data.startTime = source.readLong();
|
||||
data.movingTime = source.readLong();
|
||||
data.totalTime = source.readLong();
|
||||
data.totalDistance = source.readDouble();
|
||||
data.totalElevationGain = source.readDouble();
|
||||
data.maxSpeed = source.readDouble();
|
||||
data.currentSpeed = source.readDouble();
|
||||
data.currentGrade = source.readDouble();
|
||||
|
||||
double minLat = source.readDouble();
|
||||
double maxLat = source.readDouble();
|
||||
data.latitudeExtremities.set(minLat, maxLat);
|
||||
|
||||
double minLong = source.readDouble();
|
||||
double maxLong = source.readDouble();
|
||||
data.longitudeExtremities.set(minLong, maxLong);
|
||||
|
||||
double minElev = source.readDouble();
|
||||
double maxElev = source.readDouble();
|
||||
data.elevationExtremities.set(minElev, maxElev);
|
||||
|
||||
double minGrade = source.readDouble();
|
||||
double maxGrade = source.readDouble();
|
||||
data.gradeExtremities.set(minGrade, maxGrade);
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TripStatisticsData[] newArray(int size) {
|
||||
return new TripStatisticsData[size];
|
||||
}
|
||||
}
|
||||
|
||||
public static final Creator CREATOR = new Creator();
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel dest, int flags) {
|
||||
dest.writeLong(startTime);
|
||||
dest.writeLong(movingTime);
|
||||
dest.writeLong(totalTime);
|
||||
dest.writeDouble(totalDistance);
|
||||
dest.writeDouble(totalElevationGain);
|
||||
dest.writeDouble(maxSpeed);
|
||||
dest.writeDouble(currentSpeed);
|
||||
dest.writeDouble(currentGrade);
|
||||
|
||||
dest.writeDouble(latitudeExtremities.getMin());
|
||||
dest.writeDouble(latitudeExtremities.getMax());
|
||||
dest.writeDouble(longitudeExtremities.getMin());
|
||||
dest.writeDouble(longitudeExtremities.getMax());
|
||||
dest.writeDouble(elevationExtremities.getMin());
|
||||
dest.writeDouble(elevationExtremities.getMax());
|
||||
dest.writeDouble(gradeExtremities.getMin());
|
||||
dest.writeDouble(gradeExtremities.getMax());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
/*
|
||||
* 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.util;
|
||||
|
||||
import com.google.android.apps.mytracks.MyTracksSettings;
|
||||
import com.google.android.apps.mytracks.content.Track;
|
||||
import com.google.android.maps.mytracks.R;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
|
||||
import java.util.Vector;
|
||||
|
||||
/**
|
||||
* This class will generate google chart server url's.
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
*/
|
||||
public class ChartURLGenerator {
|
||||
|
||||
private static final String CHARTS_BASE_URL =
|
||||
"http://chart.apis.google.com/chart?";
|
||||
|
||||
private ChartURLGenerator() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a chart of a track.
|
||||
*
|
||||
* @param distances An array of distance measurements
|
||||
* @param elevations A matching array of elevation measurements
|
||||
* @param track The track for this chart
|
||||
* @param context The current appplication context
|
||||
*/
|
||||
public static String getChartUrl(Vector<Double> distances,
|
||||
Vector<Double> elevations, Track track, Context context) {
|
||||
SharedPreferences preferences =
|
||||
context.getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0);
|
||||
boolean metricUnits = true;
|
||||
if (preferences != null) {
|
||||
metricUnits = preferences.getBoolean(MyTracksSettings.METRIC_UNITS, true);
|
||||
}
|
||||
|
||||
return getChartUrl(distances, elevations, track,
|
||||
context.getString(R.string.elevation_label), metricUnits, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a chart of a track.
|
||||
* This form is for testing without contexts.
|
||||
*
|
||||
* @param distances An array of distance measurements
|
||||
* @param elevations A matching array of elevation measurements
|
||||
* @param track The track for this chart
|
||||
* @param title The title for the chart
|
||||
* @param metricUnits Should the data be displayed in metric units
|
||||
* @param escapePipe Should the pipe character be escaped
|
||||
*/
|
||||
public static String getChartUrl(
|
||||
Vector<Double> distances, Vector<Double> elevations,
|
||||
Track track, String title, boolean metricUnits, boolean escapePipe) {
|
||||
if (distances == null || elevations == null || track == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (distances.size() != elevations.size()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Round it up.
|
||||
double effectiveMaxY = metricUnits
|
||||
? track.getMaxElevation()
|
||||
: track.getMaxElevation() * UnitConversions.M_TO_FT;
|
||||
effectiveMaxY = ((int) (effectiveMaxY / 100)) * 100 + 100;
|
||||
// Round it down.
|
||||
double effectiveMinY = 0;
|
||||
double minElevation = metricUnits
|
||||
? track.getMinElevation()
|
||||
: track.getMinElevation() * UnitConversions.M_TO_FT;
|
||||
|
||||
effectiveMinY = ((int) (minElevation / 100)) * 100;
|
||||
if (track.getMinElevation() < 0) {
|
||||
effectiveMinY -= 100;
|
||||
}
|
||||
double ySpread = effectiveMaxY - effectiveMinY;
|
||||
|
||||
StringBuilder sb = new StringBuilder(CHARTS_BASE_URL);
|
||||
sb.append("&chs=600x350");
|
||||
sb.append("&cht=lxy");
|
||||
|
||||
// Title
|
||||
sb.append("&chtt=");
|
||||
sb.append(title);
|
||||
|
||||
// Labels
|
||||
sb.append("&chxt=x,y");
|
||||
double distKM = track.getTotalDistance() / 1000.0;
|
||||
double distDisplay =
|
||||
metricUnits ? distKM : (distKM * UnitConversions.KM_TO_MI);
|
||||
int xInterval = ((int) (distDisplay / 6));
|
||||
int yInterval = ((int) (ySpread / 600)) * 100;
|
||||
if (yInterval < 100) {
|
||||
yInterval = 25;
|
||||
}
|
||||
// Range
|
||||
sb.append("&chxr=0,0,");
|
||||
sb.append((int) distDisplay);
|
||||
sb.append(',');
|
||||
sb.append(xInterval);
|
||||
|
||||
// This is an ugly hack. We really want a pipe but we must double escape it.
|
||||
sb.append(escapePipe ? "%257C" : "|");
|
||||
sb.append("1,");
|
||||
sb.append(effectiveMinY);
|
||||
sb.append(',');
|
||||
sb.append(effectiveMaxY);
|
||||
sb.append(',');
|
||||
sb.append(yInterval);
|
||||
|
||||
// Line color
|
||||
sb.append("&chco=009A00");
|
||||
|
||||
// Fill
|
||||
sb.append("&chm=B,00AA00,0,0,0");
|
||||
|
||||
// Grid lines
|
||||
double desiredGrids = ySpread / yInterval;
|
||||
sb.append("&chg=100000,");
|
||||
sb.append(100.0 / desiredGrids);
|
||||
sb.append(",1,0");
|
||||
|
||||
// Data
|
||||
sb.append("&chd=e:");
|
||||
for (int i = 0; i < distances.size(); i++) {
|
||||
int normalized =
|
||||
(int) (getNormalizedDistance(distances.elementAt(i), track) * 4095);
|
||||
sb.append(ChartsExtendedEncoder.getEncodedValue(normalized));
|
||||
}
|
||||
|
||||
sb.append(ChartsExtendedEncoder.getSeparator());
|
||||
for (int i = 0; i < elevations.size(); i++) {
|
||||
int normalized =
|
||||
(int) (getNormalizedElevation(
|
||||
elevations.elementAt(i), effectiveMinY, ySpread) * 4095);
|
||||
sb.append(ChartsExtendedEncoder.getEncodedValue(normalized));
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
protected static double getNormalizedDistance(double d, Track track) {
|
||||
return d / track.getTotalDistance();
|
||||
}
|
||||
|
||||
protected static double getNormalizedElevation(
|
||||
double d, double effectiveMinY, double ySpread) {
|
||||
return (d - effectiveMinY) / ySpread;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* 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.util;
|
||||
|
||||
/**
|
||||
* The singleton class representing the extended encoding of chart data.
|
||||
*/
|
||||
public class ChartsExtendedEncoder {
|
||||
|
||||
// ChartServer data encoding in extended mode
|
||||
private static final String CHARTSERVER_EXTENDED_ENCODING_SEPARATOR = ",";
|
||||
private static final String CHARTSERVER_EXTENDED_ENCODING =
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-.";
|
||||
private static final int CHARTSERVER_EXTENDED_SINGLE_CHARACTER_VALUES =
|
||||
CHARTSERVER_EXTENDED_ENCODING.length();
|
||||
private static final String MISSING_POINT_EXTENDED_ENCODING = "__";
|
||||
|
||||
private ChartsExtendedEncoder() { }
|
||||
|
||||
public static String getEncodedValue(int scaled) {
|
||||
int index1 = scaled / CHARTSERVER_EXTENDED_SINGLE_CHARACTER_VALUES;
|
||||
int index2 = scaled % CHARTSERVER_EXTENDED_SINGLE_CHARACTER_VALUES;
|
||||
|
||||
if (index1 >= CHARTSERVER_EXTENDED_SINGLE_CHARACTER_VALUES) {
|
||||
return MISSING_POINT_EXTENDED_ENCODING;
|
||||
}
|
||||
if (index2 >= CHARTSERVER_EXTENDED_SINGLE_CHARACTER_VALUES) {
|
||||
return MISSING_POINT_EXTENDED_ENCODING;
|
||||
}
|
||||
|
||||
return String.valueOf(CHARTSERVER_EXTENDED_ENCODING.charAt(index1))
|
||||
+ String.valueOf(CHARTSERVER_EXTENDED_ENCODING.charAt(index2));
|
||||
}
|
||||
|
||||
public static String getSeparator() {
|
||||
return CHARTSERVER_EXTENDED_ENCODING_SEPARATOR;
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,12 @@
|
||||
/*
|
||||
* Copyright 2008 Google Inc.
|
||||
*
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
@@ -18,6 +18,8 @@ package com.google.android.apps.mytracks.util;
|
||||
import com.google.android.apps.mytracks.MyTracksConstants;
|
||||
import com.google.android.apps.mytracks.content.Track;
|
||||
|
||||
import com.google.android.maps.GeoPoint;
|
||||
|
||||
import android.location.Location;
|
||||
import android.util.Log;
|
||||
|
||||
@@ -27,28 +29,224 @@ import java.util.Stack;
|
||||
|
||||
/**
|
||||
* Utility class for decimating tracks at a given level of precision.
|
||||
*
|
||||
*
|
||||
* @author Leif Hendrik Wilden
|
||||
*/
|
||||
public class MyTracksUtils {
|
||||
|
||||
/**
|
||||
* Computes the distance on the two sphere between the point c0 and the line
|
||||
* segment c1 to c2.
|
||||
*
|
||||
* @param c0 the first coordinate
|
||||
* @param c1 the beginning of the line segment
|
||||
* @param c2 the end of the lone segment
|
||||
* @return the distance in m (assuming spherical earth)
|
||||
*/
|
||||
public static double distance(
|
||||
final Location c0, final Location c1, final Location c2) {
|
||||
if (c1.equals(c2)) {
|
||||
return c2.distanceTo(c0);
|
||||
}
|
||||
|
||||
final double s0lat = c0.getLatitude() * UnitConversions.TO_RADIANS;
|
||||
final double s0lng = c0.getLongitude() * UnitConversions.TO_RADIANS;
|
||||
final double s1lat = c1.getLatitude() * UnitConversions.TO_RADIANS;
|
||||
final double s1lng = c1.getLongitude() * UnitConversions.TO_RADIANS;
|
||||
final double s2lat = c2.getLatitude() * UnitConversions.TO_RADIANS;
|
||||
final double s2lng = c2.getLongitude() * UnitConversions.TO_RADIANS;
|
||||
|
||||
double s2s1lat = s2lat - s1lat;
|
||||
double s2s1lng = s2lng - s1lng;
|
||||
final double u =
|
||||
((s0lat - s1lat) * s2s1lat + (s0lng - s1lng) * s2s1lng)
|
||||
/ (s2s1lat * s2s1lat + s2s1lng * s2s1lng);
|
||||
if (u <= 0) {
|
||||
return c0.distanceTo(c1);
|
||||
}
|
||||
if (u >= 1) {
|
||||
return c0.distanceTo(c2);
|
||||
}
|
||||
Location sa = new Location("");
|
||||
sa.setLatitude(c0.getLatitude() - c1.getLatitude());
|
||||
sa.setLongitude(c0.getLongitude() - c1.getLongitude());
|
||||
Location sb = new Location("");
|
||||
sb.setLatitude(u * (c2.getLatitude() - c1.getLatitude()));
|
||||
sb.setLongitude(u * (c2.getLongitude() - c1.getLongitude()));
|
||||
return sa.distanceTo(sb);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decimates the given locations for a given zoom level. This uses a
|
||||
* Douglas-Peucker decimation algorithm.
|
||||
*
|
||||
* @param tolerance in meters
|
||||
* @param locations input
|
||||
* @param decimated output
|
||||
*/
|
||||
public static void decimate(double tolerance, ArrayList<Location> locations,
|
||||
ArrayList<Location> decimated) {
|
||||
final int n = locations.size();
|
||||
if (n < 1) {
|
||||
return;
|
||||
}
|
||||
int idx;
|
||||
int maxIdx = 0;
|
||||
Stack<int[]> stack = new Stack<int[]>();
|
||||
double[] dists = new double[n];
|
||||
dists[0] = 1;
|
||||
dists[n - 1] = 1;
|
||||
double maxDist;
|
||||
double dist = 0.0;
|
||||
int[] current;
|
||||
|
||||
if (n > 2) {
|
||||
int[] stackVal = new int[] {0, (n - 1)};
|
||||
stack.push(stackVal);
|
||||
while (stack.size() > 0) {
|
||||
current = stack.pop();
|
||||
maxDist = 0;
|
||||
for (idx = current[0] + 1; idx < current[1]; ++idx) {
|
||||
dist = MyTracksUtils.distance(
|
||||
locations.get(idx),
|
||||
locations.get(current[0]),
|
||||
locations.get(current[1]));
|
||||
if (dist > maxDist) {
|
||||
maxDist = dist;
|
||||
maxIdx = idx;
|
||||
}
|
||||
}
|
||||
if (maxDist > tolerance) {
|
||||
dists[maxIdx] = maxDist;
|
||||
int[] stackValCurMax = {current[0], maxIdx};
|
||||
stack.push(stackValCurMax);
|
||||
int[] stackValMaxCur = {maxIdx, current[1]};
|
||||
stack.push(stackValMaxCur);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int i = 0;
|
||||
idx = 0;
|
||||
decimated.clear();
|
||||
for (Location l : locations) {
|
||||
if (dists[idx] != 0) {
|
||||
decimated.add(l);
|
||||
i++;
|
||||
}
|
||||
idx++;
|
||||
}
|
||||
Log.d(MyTracksConstants.TAG, "Decimating " + n + " points to " + i
|
||||
+ " w/ tolerance = " + tolerance);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decimates the given track for the given precision.
|
||||
*
|
||||
* @param track a track
|
||||
* @param precision desired precision in meters
|
||||
*/
|
||||
public static void decimate(Track track, double precision) {
|
||||
ArrayList<Location> decimated = new ArrayList<Location>();
|
||||
decimate(precision, track.getLocations(), decimated);
|
||||
track.setLocations(decimated);
|
||||
}
|
||||
|
||||
/**
|
||||
* Limits number of points by dropping any points beyond the given number of
|
||||
* points. Note: That'll actually discard points.
|
||||
*
|
||||
* @param track a track
|
||||
* @param numberOfPoints maximum number of points
|
||||
*/
|
||||
public static void cut(Track track, int numberOfPoints) {
|
||||
ArrayList<Location> locations = track.getLocations();
|
||||
while (locations.size() > numberOfPoints) {
|
||||
locations.remove(locations.size() - 1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Splits a track in multiple tracks where each piece has less or equal than
|
||||
* maxPoints.
|
||||
*
|
||||
* @param track the track to split
|
||||
* @param maxPoints maximum number of points for each piece
|
||||
* @return a list of one or more track pieces
|
||||
*/
|
||||
public static ArrayList<Track> split(Track track, int maxPoints) {
|
||||
ArrayList<Track> result = new ArrayList<Track>();
|
||||
final int nTotal = track.getLocations().size();
|
||||
int n = 0;
|
||||
Track piece = null;
|
||||
do {
|
||||
piece = new Track();
|
||||
piece.setId(track.getId());
|
||||
piece.setName(track.getName());
|
||||
piece.setDescription(track.getDescription());
|
||||
piece.setCategory(track.getCategory());
|
||||
List<Location> pieceLocations = piece.getLocations();
|
||||
for (int i = n; i < nTotal && pieceLocations.size() < maxPoints; i++) {
|
||||
piece.addLocation(track.getLocations().get(i));
|
||||
}
|
||||
int nPointsPiece = pieceLocations.size();
|
||||
if (nPointsPiece >= 2) {
|
||||
piece.setStartTime(pieceLocations.get(0).getTime());
|
||||
piece.setStopTime(pieceLocations.get(nPointsPiece - 1).getTime());
|
||||
result.add(piece);
|
||||
}
|
||||
n += (pieceLocations.size() - 1);
|
||||
} while (n < nTotal && piece != null && piece.getLocations().size() > 1);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if a given GeoPoint is valid, i.e. within physical bounds.
|
||||
*
|
||||
* @param geoPoint the point to be tested
|
||||
* @return true, if it is a physical location on earth.
|
||||
*/
|
||||
public static boolean isValidGeoPoint(GeoPoint geoPoint) {
|
||||
return Math.abs(geoPoint.getLatitudeE6()) < 90E6
|
||||
&& Math.abs(geoPoint.getLongitudeE6()) <= 180E6;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a given location is a valid (i.e. physically possible) location
|
||||
* on Earth. Note: The special separator locations (which have latitude =
|
||||
* 100) will not qualify as valid. Neither will locations with lat=0 and lng=0
|
||||
* as these are most likely "bad" measurements which often cause trouble.
|
||||
*
|
||||
*
|
||||
* @param location the location to test
|
||||
* @return true if the location is a valid location
|
||||
* @return true if the location is a valid location.
|
||||
*/
|
||||
public static boolean isValidLocation(Location location) {
|
||||
return location != null && Math.abs(location.getLatitude()) <= 90
|
||||
&& Math.abs(location.getLongitude()) <= 180;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a location from a GeoPoint.
|
||||
*
|
||||
* @param p a GeoPoint
|
||||
* @return the corresponding location
|
||||
*/
|
||||
public static Location getLocation(GeoPoint p) {
|
||||
Location result = new Location("");
|
||||
result.setLatitude(p.getLatitudeE6() / 1.0E6);
|
||||
result.setLongitude(p.getLongitudeE6() / 1.0E6);
|
||||
return result;
|
||||
}
|
||||
|
||||
public static GeoPoint getGeoPoint(Location location) {
|
||||
return new GeoPoint((int) (location.getLatitude() * 1E6),
|
||||
(int) (location.getLongitude() * 1E6));
|
||||
}
|
||||
|
||||
/**
|
||||
* This is a utility class w/ only static memebers.
|
||||
*/
|
||||
private MyTracksUtils() {
|
||||
protected MyTracksUtils() {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
* Copyright 2008 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
package com.google.android.apps.mytracks.util;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.BufferedOutputStream;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStream;
|
||||
|
||||
/**
|
||||
* Utility functions for android resources.
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
*/
|
||||
public class ResourceUtils {
|
||||
public static CharSequence readFile(Context activity, int id) {
|
||||
BufferedReader in = null;
|
||||
try {
|
||||
in = new BufferedReader(new InputStreamReader(
|
||||
activity.getResources().openRawResource(id)));
|
||||
String line;
|
||||
StringBuilder buffer = new StringBuilder();
|
||||
while ((line = in.readLine()) != null) {
|
||||
buffer.append(line).append('\n');
|
||||
}
|
||||
return buffer;
|
||||
} catch (IOException e) {
|
||||
return "";
|
||||
} finally {
|
||||
if (in != null) {
|
||||
try {
|
||||
in.close();
|
||||
} catch (IOException e) {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void readBinaryFileToOutputStream(
|
||||
Context activity, int id, OutputStream os) {
|
||||
BufferedInputStream in = null;
|
||||
try {
|
||||
in = new BufferedInputStream(
|
||||
activity.getResources().openRawResource(id));
|
||||
BufferedOutputStream out = new BufferedOutputStream(os);
|
||||
int b;
|
||||
while ((b = in.read()) != -1) {
|
||||
out.write(b);
|
||||
}
|
||||
out.flush();
|
||||
} catch (IOException e) {
|
||||
return;
|
||||
} finally {
|
||||
if (in != null) {
|
||||
try {
|
||||
in.close();
|
||||
} catch (IOException e) {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private ResourceUtils() {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,399 @@
|
||||
/*
|
||||
* Copyright 2008 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
package com.google.android.apps.mytracks.util;
|
||||
|
||||
import com.google.android.apps.mytracks.MyTracksSettings;
|
||||
import com.google.android.apps.mytracks.content.Track;
|
||||
import com.google.android.apps.mytracks.content.Waypoint;
|
||||
import com.google.android.apps.mytracks.content.DescriptionGenerator;
|
||||
import com.google.android.maps.mytracks.R;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.Vector;
|
||||
|
||||
/**
|
||||
* Various string manipulation methods.
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
* @author Rodrigo Damazio
|
||||
*/
|
||||
public class StringUtils implements DescriptionGenerator {
|
||||
|
||||
private final Context context;
|
||||
|
||||
/**
|
||||
* Formats a number of milliseconds as a string.
|
||||
*
|
||||
* @param time - A period of time in milliseconds.
|
||||
* @return A string of the format M:SS, MM:SS or HH:MM:SS
|
||||
*/
|
||||
public static String formatTime(long time) {
|
||||
return formatTimeInternal(time, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a number of milliseconds as a string. To be used when we need the
|
||||
* hours to be shown even when it is zero, e.g. exporting data to a
|
||||
* spreadsheet.
|
||||
*
|
||||
* @param time - A period of time in milliseconds
|
||||
* @return A string of the format HH:MM:SS even if time is less than 1 hour
|
||||
*/
|
||||
public static String formatTimeAlwaysShowingHours(long time) {
|
||||
return formatTimeInternal(time, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats the given text as a CDATA element to be used in a XML file. This
|
||||
* includes adding the starting and ending CDATA tags. Please notice that this
|
||||
* may result in multiple consecutive CDATA tags.
|
||||
*
|
||||
* @param unescaped the unescaped text to be formatted
|
||||
* @return the formatted text, inside one or more CDATA tags
|
||||
*/
|
||||
public static String stringAsCData(String unescaped) {
|
||||
// "]]>" needs to be broken into multiple CDATA segments, like:
|
||||
// "Foo]]>Bar" becomes "<![CDATA[Foo]]]]><![CDATA[>Bar]]>"
|
||||
// (the end of the first CDATA has the "]]", the other has ">")
|
||||
String escaped = unescaped.replaceAll("]]>", "]]]]><![CDATA[>");
|
||||
return "<![CDATA[" + escaped + "]]>";
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a number of milliseconds as a string.
|
||||
*
|
||||
* @param time - A period of time in milliseconds
|
||||
* @param alwaysShowHours - Whether to display 00 hours if time is less than 1
|
||||
* hour
|
||||
* @return A string of the format HH:MM:SS
|
||||
*/
|
||||
private static String formatTimeInternal(long time, boolean alwaysShowHours) {
|
||||
int[] parts = getTimeParts(time);
|
||||
StringBuilder builder = new StringBuilder();
|
||||
if (parts[2] > 0 || alwaysShowHours) {
|
||||
builder.append(parts[2]);
|
||||
builder.append(':');
|
||||
if (parts[1] <= 9) {
|
||||
builder.append("0");
|
||||
}
|
||||
}
|
||||
|
||||
builder.append(parts[1]);
|
||||
builder.append(':');
|
||||
if (parts[0] <= 9) {
|
||||
builder.append("0");
|
||||
}
|
||||
builder.append(parts[0]);
|
||||
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the time as an array of parts.
|
||||
*/
|
||||
public static int[] getTimeParts(long time) {
|
||||
if (time < 0) {
|
||||
int[] parts = getTimeParts(time * -1);
|
||||
parts[0] *= -1;
|
||||
parts[1] *= -1;
|
||||
parts[2] *= -1;
|
||||
return parts;
|
||||
}
|
||||
int[] parts = new int[3];
|
||||
|
||||
long seconds = time / 1000;
|
||||
parts[0] = (int) (seconds % 60);
|
||||
int tmp = (int) (seconds / 60);
|
||||
parts[1] = tmp % 60;
|
||||
parts[2] = tmp / 60;
|
||||
|
||||
return parts;
|
||||
}
|
||||
|
||||
public StringUtils(Context context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
public String formatTimeLong(long time) {
|
||||
int[] parts = getTimeParts(time);
|
||||
String secLabel =
|
||||
context.getString(parts[0] == 1 ? R.string.second : R.string.seconds);
|
||||
String minLabel =
|
||||
context.getString(parts[1] == 1 ? R.string.minute : R.string.minutes);
|
||||
String hourLabel =
|
||||
context.getString(parts[2] == 1 ? R.string.hour : R.string.hours);
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
if (parts[2] != 0) {
|
||||
sb.append(parts[2]);
|
||||
sb.append(" ");
|
||||
sb.append(hourLabel);
|
||||
sb.append(" ");
|
||||
sb.append(parts[1]);
|
||||
sb.append(minLabel);
|
||||
} else {
|
||||
sb.append(parts[1]);
|
||||
sb.append(" ");
|
||||
sb.append(minLabel);
|
||||
sb.append(" ");
|
||||
sb.append(parts[0]);
|
||||
sb.append(secLabel);
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a description for a track (with information about the
|
||||
* statistics).
|
||||
*
|
||||
* @param track the track
|
||||
* @return a track description
|
||||
*/
|
||||
public String generateTrackDescription(Track track, Vector<Double> distances,
|
||||
Vector<Double> elevations) {
|
||||
boolean displaySpeed = true;
|
||||
SharedPreferences preferences =
|
||||
context.getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0);
|
||||
if (preferences != null) {
|
||||
displaySpeed =
|
||||
preferences.getBoolean(MyTracksSettings.REPORT_SPEED, true);
|
||||
}
|
||||
|
||||
final double distanceInKm = track.getTotalDistance() / 1000;
|
||||
final double distanceInMiles = distanceInKm * UnitConversions.KM_TO_MI;
|
||||
final long minElevationInMeters = Math.round(track.getMinElevation());
|
||||
final long minElevationInFeet =
|
||||
Math.round(track.getMinElevation() * UnitConversions.M_TO_FT);
|
||||
final long maxElevationInMeters = Math.round(track.getMaxElevation());
|
||||
final long maxElevationInFeet =
|
||||
Math.round(track.getMaxElevation() * UnitConversions.M_TO_FT);
|
||||
final long elevationGainInMeters =
|
||||
Math.round(track.getTotalElevationGain());
|
||||
final long elevationGainInFeet =
|
||||
Math.round(track.getTotalElevationGain() * UnitConversions.M_TO_FT);
|
||||
|
||||
long minGrade = 0;
|
||||
long maxGrade = 0;
|
||||
double trackMaxGrade = track.getMaxGrade();
|
||||
double trackMinGrade = track.getMinGrade();
|
||||
if (!Double.isNaN(trackMaxGrade)
|
||||
&& !Double.isInfinite(trackMaxGrade)) {
|
||||
maxGrade = Math.round(trackMaxGrade * 100);
|
||||
}
|
||||
if (!Double.isNaN(trackMinGrade) && !Double.isInfinite(trackMinGrade)) {
|
||||
minGrade = Math.round(trackMinGrade * 100);
|
||||
}
|
||||
|
||||
String category = context.getString(R.string.unknown);
|
||||
String trackCategory = track.getCategory();
|
||||
if (trackCategory != null && trackCategory.length() > 0) {
|
||||
category = trackCategory;
|
||||
}
|
||||
|
||||
String averageSpeed =
|
||||
getSpeedString(track.getAverageSpeed(),
|
||||
R.string.average_speed_label,
|
||||
R.string.average_pace_label,
|
||||
displaySpeed);
|
||||
|
||||
String averageMovingSpeed =
|
||||
getSpeedString(track.getAverageMovingSpeed(),
|
||||
R.string.average_moving_speed_label,
|
||||
R.string.average_moving_pace_label,
|
||||
displaySpeed);
|
||||
|
||||
String maxSpeed =
|
||||
getSpeedString(track.getMaxSpeed(),
|
||||
R.string.max_speed_label,
|
||||
R.string.min_pace_label,
|
||||
displaySpeed);
|
||||
|
||||
return String.format("%s<p>"
|
||||
+ "%s: %.2f %s (%.1f %s)<br>"
|
||||
+ "%s: %s<br>"
|
||||
+ "%s: %s<br>"
|
||||
+ "%s %s %s"
|
||||
+ "%s: %d %s (%d %s)<br>"
|
||||
+ "%s: %d %s (%d %s)<br>"
|
||||
+ "%s: %d %s (%d %s)<br>"
|
||||
+ "%s: %d %%<br>"
|
||||
+ "%s: %d %%<br>"
|
||||
+ "%s: %tc<br>"
|
||||
+ "%s: %s<br>"
|
||||
+ "<img border=\"0\" src=\"%s\"/>",
|
||||
|
||||
// Line 1
|
||||
context.getString(R.string.my_map_link),
|
||||
|
||||
// Line 2
|
||||
context.getString(R.string.total_distance_label),
|
||||
distanceInKm, context.getString(R.string.kilometer),
|
||||
distanceInMiles, context.getString(R.string.mile),
|
||||
|
||||
// Line 3
|
||||
context.getString(R.string.total_time_label),
|
||||
StringUtils.formatTime(track.getTotalTime()),
|
||||
|
||||
// Line 4
|
||||
context.getString(R.string.moving_time_label),
|
||||
StringUtils.formatTime(track.getMovingTime()),
|
||||
|
||||
// Line 5
|
||||
averageSpeed, averageMovingSpeed, maxSpeed,
|
||||
|
||||
// Line 6
|
||||
context.getString(R.string.min_elevation_label),
|
||||
minElevationInMeters, context.getString(R.string.meter),
|
||||
minElevationInFeet, context.getString(R.string.feet),
|
||||
|
||||
// Line 7
|
||||
context.getString(R.string.max_elevation_label),
|
||||
maxElevationInMeters, context.getString(R.string.meter),
|
||||
maxElevationInFeet, context.getString(R.string.feet),
|
||||
|
||||
// Line 8
|
||||
context.getString(R.string.elevation_gain_label),
|
||||
elevationGainInMeters, context.getString(R.string.meter),
|
||||
elevationGainInFeet, context.getString(R.string.feet),
|
||||
|
||||
// Line 9
|
||||
context.getString(R.string.max_grade_label), maxGrade,
|
||||
|
||||
// Line 10
|
||||
context.getString(R.string.min_grade_label), minGrade,
|
||||
|
||||
// Line 11
|
||||
context.getString(R.string.recorded_date),
|
||||
new Date(track.getStartTime()),
|
||||
|
||||
// Line 12
|
||||
context.getString(R.string.category), category,
|
||||
|
||||
// Line 13
|
||||
ChartURLGenerator.getChartUrl(distances, elevations, track, context));
|
||||
}
|
||||
|
||||
private String getSpeedString(double speed, int speedLabel, int paceLabel,
|
||||
boolean displaySpeed) {
|
||||
double speedInKph = speed * 3.6;
|
||||
double speedInMph = speedInKph * UnitConversions.KMH_TO_MPH;
|
||||
if (displaySpeed) {
|
||||
return String.format("%s: %.2f %s (%.1f %s)<br>",
|
||||
context.getString(speedLabel),
|
||||
speedInKph, context.getString(R.string.kilometer_per_hour),
|
||||
speedInMph, context.getString(R.string.mile_per_hour));
|
||||
} else {
|
||||
double paceInKm;
|
||||
double paceInMi;
|
||||
if (speed == 0) {
|
||||
paceInKm = 0.0;
|
||||
paceInMi = 0.0;
|
||||
} else {
|
||||
paceInKm = 60.0 / speedInKph;
|
||||
paceInMi = 60.0 / speedInMph;
|
||||
}
|
||||
return String.format("%s: %.2f %s (%.1f %s)<br>",
|
||||
context.getString(paceLabel),
|
||||
paceInKm, context.getString(R.string.min_per_kilometer),
|
||||
paceInMi, context.getString(R.string.min_per_mile));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a description for a waypoint (with information about the
|
||||
* statistics).
|
||||
*
|
||||
* @return a track description
|
||||
*/
|
||||
public String generateWaypointDescription(Waypoint waypoint) {
|
||||
final double distanceInKm = waypoint.getTotalDistance() / 1000;
|
||||
final double distanceInMiles = distanceInKm * UnitConversions.KM_TO_MI;
|
||||
final double averageSpeedInKmh = waypoint.getAverageSpeed() * 3.6;
|
||||
final double averageSpeedInMph =
|
||||
averageSpeedInKmh * UnitConversions.KMH_TO_MPH;
|
||||
final double movingSpeedInKmh = waypoint.getAverageMovingSpeed() * 3.6;
|
||||
final double movingSpeedInMph =
|
||||
movingSpeedInKmh * UnitConversions.KMH_TO_MPH;
|
||||
final double maxSpeedInKmh = waypoint.getMaxSpeed() * 3.6;
|
||||
final double maxSpeedInMph = maxSpeedInKmh * UnitConversions.KMH_TO_MPH;
|
||||
final long minElevationInMeters = Math.round(waypoint.getMinElevation());
|
||||
final long minElevationInFeet =
|
||||
Math.round(waypoint.getMinElevation() * UnitConversions.M_TO_FT);
|
||||
final long maxElevationInMeters = Math.round(waypoint.getMaxElevation());
|
||||
final long maxElevationInFeet =
|
||||
Math.round(waypoint.getMaxElevation() * UnitConversions.M_TO_FT);
|
||||
final long elevationGainInMeters =
|
||||
Math.round(waypoint.getTotalElevationGain());
|
||||
final long elevationGainInFeet = Math.round(waypoint.getTotalElevationGain()
|
||||
* UnitConversions.M_TO_FT);
|
||||
long theMinGrade = 0;
|
||||
long theMaxGrade = 0;
|
||||
if (!Double.isNaN(waypoint.getMaxGrade()) &&
|
||||
!Double.isInfinite(waypoint.getMaxGrade())) {
|
||||
theMaxGrade = Math.round(waypoint.getMaxGrade() * 100);
|
||||
}
|
||||
if (!Double.isNaN(waypoint.getMinGrade()) &&
|
||||
!Double.isInfinite(waypoint.getMinGrade())) {
|
||||
theMinGrade = Math.round(waypoint.getMinGrade() * 100);
|
||||
}
|
||||
final String percent = "%";
|
||||
|
||||
return String.format(
|
||||
"%s: %.2f %s (%.1f %s)\n"
|
||||
+ "%s: %s\n"
|
||||
+ "%s: %s\n"
|
||||
+ "%s: %.2f %s (%.1f %s)\n"
|
||||
+ "%s: %.2f %s (%.1f %s)\n"
|
||||
+ "%s: %.2f %s (%.1f %s)\n"
|
||||
+ "%s: %d %s (%d %s)\n"
|
||||
+ "%s: %d %s (%d %s)\n"
|
||||
+ "%s: %d %s (%d %s)\n"
|
||||
+ "%s: %d %s\n"
|
||||
+ "%s: %d %s\n",
|
||||
context.getString(R.string.distance_label),
|
||||
distanceInKm, context.getString(R.string.kilometer),
|
||||
distanceInMiles, context.getString(R.string.mile),
|
||||
context.getString(R.string.time_label),
|
||||
StringUtils.formatTime(waypoint.getTotalTime()),
|
||||
context.getString(R.string.moving_time_label),
|
||||
StringUtils.formatTime(waypoint.getMovingTime()),
|
||||
context.getString(R.string.average_speed_label),
|
||||
averageSpeedInKmh, context.getString(R.string.kilometer_per_hour),
|
||||
averageSpeedInMph, context.getString(R.string.mile_per_hour),
|
||||
context.getString(R.string.average_moving_speed_label),
|
||||
movingSpeedInKmh, context.getString(R.string.kilometer_per_hour),
|
||||
movingSpeedInMph, context.getString(R.string.mile_per_hour),
|
||||
context.getString(R.string.max_speed_label),
|
||||
maxSpeedInKmh, context.getString(R.string.kilometer_per_hour),
|
||||
maxSpeedInMph, context.getString(R.string.mile_per_hour),
|
||||
context.getString(R.string.min_elevation_label),
|
||||
minElevationInMeters, context.getString(R.string.meter),
|
||||
minElevationInFeet, context.getString(R.string.feet),
|
||||
context.getString(R.string.max_elevation_label),
|
||||
maxElevationInMeters, context.getString(R.string.meter),
|
||||
maxElevationInFeet, context.getString(R.string.feet),
|
||||
context.getString(R.string.elevation_gain_label),
|
||||
elevationGainInMeters, context.getString(R.string.meter),
|
||||
elevationGainInFeet, context.getString(R.string.feet),
|
||||
context.getString(R.string.max_grade_label),
|
||||
theMaxGrade, percent,
|
||||
context.getString(R.string.min_grade_label),
|
||||
theMinGrade, percent);
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,12 @@
|
||||
/*
|
||||
* Copyright 2008 Google Inc.
|
||||
*
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
@@ -17,7 +17,7 @@ package com.google.android.apps.mytracks.util;
|
||||
|
||||
/**
|
||||
* Unit conversion constants.
|
||||
*
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
*/
|
||||
public abstract class UnitConversions {
|
||||
@@ -29,6 +29,6 @@ public abstract class UnitConversions {
|
||||
public static final double KMH_TO_MPH = 1000 * M_TO_FT / MI_TO_FEET;
|
||||
public static final double TO_RADIANS = Math.PI / 180.0;
|
||||
|
||||
private UnitConversions() {
|
||||
protected UnitConversions() {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* 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.wireless.gdata.docs;
|
||||
|
||||
import com.google.wireless.gdata.client.GDataClient;
|
||||
import com.google.wireless.gdata.client.GDataParserFactory;
|
||||
import com.google.wireless.gdata.client.GDataServiceClient;
|
||||
|
||||
/**
|
||||
* GDataServiceClient for accessing Google Documents. This is not a full
|
||||
* implementation.
|
||||
*/
|
||||
public class DocumentsClient extends GDataServiceClient {
|
||||
/** The name of the service, dictated to be 'wise' by the protocol. */
|
||||
private static final String SERVICE = "writely";
|
||||
|
||||
/**
|
||||
* Creates a new DocumentsClient.
|
||||
*
|
||||
* @param client The GDataClient that should be used to authenticate requests,
|
||||
* retrieve feeds, etc
|
||||
* @param parserFactory The GDataParserFactory that should be used to obtain
|
||||
* GDataParsers used by this client
|
||||
*/
|
||||
public DocumentsClient(GDataClient client, GDataParserFactory parserFactory) {
|
||||
super(client, parserFactory);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getServiceName() {
|
||||
return SERVICE;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,270 @@
|
||||
/*
|
||||
* Copyright 2007 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.wireless.gdata.docs;
|
||||
|
||||
import com.google.wireless.gdata.client.GDataClient;
|
||||
import com.google.wireless.gdata.client.GDataParserFactory;
|
||||
import com.google.wireless.gdata.client.GDataServiceClient;
|
||||
import com.google.wireless.gdata.client.HttpException;
|
||||
import com.google.wireless.gdata.data.Entry;
|
||||
import com.google.wireless.gdata.data.StringUtils;
|
||||
import com.google.wireless.gdata.parser.GDataParser;
|
||||
import com.google.wireless.gdata.parser.ParseException;
|
||||
import com.google.wireless.gdata.serializer.GDataSerializer;
|
||||
import com.google.wireless.gdata2.client.AuthenticationException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
/**
|
||||
* GDataServiceClient for accessing Google Spreadsheets. This client can access
|
||||
* and parse all of the Spreadsheets feed types: Spreadsheets feed, Worksheets
|
||||
* feed, List feed, and Cells feed. Read operations are supported on all feed
|
||||
* types, but only the List and Cells feeds support write operations. (This is a
|
||||
* limitation of the protocol, not this API. Such write access may be added to
|
||||
* the protocol in the future, requiring changes to this implementation.)
|
||||
*
|
||||
* Only 'private' visibility and 'full' projections are currently supported.
|
||||
*/
|
||||
public class SpreadsheetsClient extends GDataServiceClient {
|
||||
/** The name of the service, dictated to be 'wise' by the protocol. */
|
||||
private static final String SERVICE = "wise";
|
||||
|
||||
/** Standard base feed url for spreadsheets. */
|
||||
public static final String SPREADSHEETS_BASE_FEED_URL =
|
||||
"http://spreadsheets.google.com/feeds/spreadsheets/private/full";
|
||||
|
||||
/**
|
||||
* Represents an entry in a GData Spreadsheets meta-feed.
|
||||
*/
|
||||
public static class SpreadsheetEntry extends Entry { }
|
||||
|
||||
/**
|
||||
* Represents an entry in a GData Worksheets meta-feed.
|
||||
*/
|
||||
public static class WorksheetEntry extends Entry { }
|
||||
|
||||
/**
|
||||
* Creates a new SpreadsheetsClient. Uses the standard base URL for
|
||||
* spreadsheets feeds.
|
||||
*
|
||||
* @param client The GDataClient that should be used to authenticate requests,
|
||||
* retrieve feeds, etc
|
||||
*/
|
||||
public SpreadsheetsClient(GDataClient client,
|
||||
GDataParserFactory spreadsheetFactory) {
|
||||
super(client, spreadsheetFactory);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getServiceName() {
|
||||
return SERVICE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a parser for the specified feed type.
|
||||
*
|
||||
* @param feedEntryClass the Class of entry type that will be parsed, which
|
||||
* lets this method figure out which parser to create
|
||||
* @param feedUri the URI of the feed to be fetched and parsed
|
||||
* @param authToken the current authToken to use for the request
|
||||
* @return a parser for the indicated feed
|
||||
* @throws AuthenticationException if the authToken is not valid
|
||||
* @throws ParseException if the response from the server could not be parsed
|
||||
*/
|
||||
private GDataParser getParserForTypedFeed(
|
||||
Class<? extends Entry> feedEntryClass, String feedUri, String authToken)
|
||||
throws AuthenticationException, ParseException, IOException {
|
||||
GDataClient gDataClient = getGDataClient();
|
||||
GDataParserFactory gDataParserFactory = getGDataParserFactory();
|
||||
|
||||
try {
|
||||
InputStream is = gDataClient.getFeedAsStream(feedUri, authToken);
|
||||
return gDataParserFactory.createParser(feedEntryClass, is);
|
||||
} catch (HttpException e) {
|
||||
convertHttpExceptionForReads("Could not fetch parser feed.", e);
|
||||
return null; // never reached
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an HTTP exception that happened while reading into the equivalent
|
||||
* local exception.
|
||||
*/
|
||||
public void convertHttpExceptionForReads(String message, HttpException cause)
|
||||
throws AuthenticationException, IOException {
|
||||
switch (cause.getStatusCode()) {
|
||||
case HttpException.SC_FORBIDDEN:
|
||||
case HttpException.SC_UNAUTHORIZED:
|
||||
throw new AuthenticationException(message, cause);
|
||||
case HttpException.SC_GONE:
|
||||
default:
|
||||
throw new IOException(message + ": " + cause.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Entry createEntry(String feedUri, String authToken, Entry entry)
|
||||
throws ParseException, IOException {
|
||||
|
||||
GDataParserFactory factory = getGDataParserFactory();
|
||||
GDataSerializer serializer = factory.createSerializer(entry);
|
||||
|
||||
InputStream is;
|
||||
try {
|
||||
is = getGDataClient().createEntry(feedUri, authToken, serializer);
|
||||
} catch (HttpException e) {
|
||||
convertHttpExceptionForWrites(entry.getClass(),
|
||||
"Could not update entry.", e);
|
||||
return null; // never reached.
|
||||
}
|
||||
|
||||
GDataParser parser = factory.createParser(entry.getClass(), is);
|
||||
try {
|
||||
return parser.parseStandaloneEntry();
|
||||
} finally {
|
||||
parser.close();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches a GDataParser for the indicated feed. The parser can be used to
|
||||
* access the contents of URI. WARNING: because we cannot reliably infer the
|
||||
* feed type from the URI alone, this method assumes the default feed type!
|
||||
* This is probably NOT what you want. Please use the getParserFor[Type]Feed
|
||||
* methods.
|
||||
*
|
||||
* @param feedEntryClass
|
||||
* @param feedUri the URI of the feed to be fetched and parsed
|
||||
* @param authToken the current authToken to use for the request
|
||||
* @return a parser for the indicated feed
|
||||
* @throws ParseException if the response from the server could not be parsed
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public GDataParser getParserForFeed(
|
||||
Class feedEntryClass, String feedUri, String authToken)
|
||||
throws ParseException, IOException {
|
||||
try {
|
||||
return getParserForTypedFeed(SpreadsheetEntry.class, feedUri, authToken);
|
||||
} catch (AuthenticationException e) {
|
||||
throw new IOException("Authentication Failure: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a parser for a Worksheets meta-feed.
|
||||
*
|
||||
* @param feedUri the URI of the feed to be fetched and parsed
|
||||
* @param authToken the current authToken to use for the request
|
||||
* @return a parser for the indicated feed
|
||||
* @throws AuthenticationException if the authToken is not valid
|
||||
* @throws ParseException if the response from the server could not be parsed
|
||||
*/
|
||||
public GDataParser getParserForWorksheetsFeed(
|
||||
String feedUri, String authToken)
|
||||
throws AuthenticationException, ParseException, IOException {
|
||||
return getParserForTypedFeed(WorksheetEntry.class, feedUri, authToken);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates an entry. The URI to be updated is taken from <code>entry</code>.
|
||||
* Note that only entries in List and Cells feeds can be updated, so
|
||||
* <code>entry</code> must be of the corresponding type; other types will
|
||||
* result in an exception.
|
||||
*
|
||||
* @param entry the entry to be updated; must include its URI
|
||||
* @param authToken the current authToken to be used for the operation
|
||||
* @return An Entry containing the re-parsed version of the entry returned by
|
||||
* the server in response to the update
|
||||
* @throws ParseException if the server returned an error, if the server's
|
||||
* response was unparseable (unlikely), or if <code>entry</code> is of
|
||||
* a read-only type
|
||||
* @throws IOException on network error
|
||||
*/
|
||||
@Override
|
||||
public Entry updateEntry(Entry entry, String authToken)
|
||||
throws ParseException, IOException {
|
||||
GDataParserFactory factory = getGDataParserFactory();
|
||||
GDataSerializer serializer = factory.createSerializer(entry);
|
||||
|
||||
String editUri = entry.getEditUri();
|
||||
if (StringUtils.isEmpty(editUri)) {
|
||||
throw new ParseException("No edit URI -- cannot update.");
|
||||
}
|
||||
|
||||
InputStream is;
|
||||
try {
|
||||
is = getGDataClient().updateEntry(editUri, authToken, serializer);
|
||||
} catch (HttpException e) {
|
||||
convertHttpExceptionForWrites(entry.getClass(),
|
||||
"Could not update entry.", e);
|
||||
return null; // never reached
|
||||
}
|
||||
|
||||
GDataParser parser = factory.createParser(entry.getClass(), is);
|
||||
try {
|
||||
return parser.parseStandaloneEntry();
|
||||
} finally {
|
||||
parser.close();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an HTTP exception which happened while writing to the equivalent
|
||||
* local exception.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
private void convertHttpExceptionForWrites(
|
||||
Class entryClass, String message, HttpException cause)
|
||||
throws ParseException, IOException {
|
||||
switch (cause.getStatusCode()) {
|
||||
case HttpException.SC_CONFLICT:
|
||||
Entry entry = null;
|
||||
if (entryClass != null) {
|
||||
InputStream is = cause.getResponseStream();
|
||||
if (is != null) {
|
||||
entry = parseEntry(entryClass, cause.getResponseStream());
|
||||
}
|
||||
}
|
||||
throw new IOException(message);
|
||||
case HttpException.SC_BAD_REQUEST:
|
||||
throw new ParseException(message + ": " + cause);
|
||||
case HttpException.SC_FORBIDDEN:
|
||||
case HttpException.SC_UNAUTHORIZED:
|
||||
throw new IOException(message);
|
||||
default:
|
||||
throw new IOException(message + ": " + cause.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses one entry from the input stream.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
private Entry parseEntry(Class entryClass, InputStream is)
|
||||
throws ParseException, IOException {
|
||||
GDataParser parser = null;
|
||||
try {
|
||||
parser = getGDataParserFactory().createParser(entryClass, is);
|
||||
return parser.parseStandaloneEntry();
|
||||
} finally {
|
||||
if (parser != null) {
|
||||
parser.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* 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.wireless.gdata.maps;
|
||||
|
||||
import com.google.wireless.gdata.data.Entry;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* GData entry for a map feature.
|
||||
*/
|
||||
public class MapFeatureEntry extends Entry {
|
||||
|
||||
private String mPrivacy = null;
|
||||
private Map<String, String> mAttributes = new HashMap<String, String>();
|
||||
|
||||
public void setPrivacy(String privacy) {
|
||||
mPrivacy = privacy;
|
||||
}
|
||||
|
||||
public String getPrivacy() {
|
||||
return mPrivacy;
|
||||
}
|
||||
|
||||
public void setAttribute(String name, String value) {
|
||||
mAttributes.put(name, value);
|
||||
}
|
||||
|
||||
public void removeAttribute(String name) {
|
||||
mAttributes.remove(name);
|
||||
}
|
||||
|
||||
public Map<String, String> getAllAttributes() {
|
||||
return mAttributes;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
/*
|
||||
* 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.wireless.gdata.maps;
|
||||
|
||||
import com.google.wireless.gdata.client.GDataClient;
|
||||
import com.google.wireless.gdata.client.GDataParserFactory;
|
||||
import com.google.wireless.gdata.client.GDataServiceClient;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
/**
|
||||
* Client to talk to Google Maps via GData.
|
||||
*/
|
||||
public class MapsClient extends GDataServiceClient {
|
||||
public static final String SERVICE = "local";
|
||||
|
||||
private static final boolean DEBUG = false;
|
||||
public static final boolean LOG_COMMUNICATION = false;
|
||||
|
||||
private static final String MAPS_BASE_FEED_URL =
|
||||
"http://maps.google.com/maps/feeds/";
|
||||
private static final String MAPS_MAP_FEED_PATH = "maps/default/full";
|
||||
private static final String MAPS_FEATURE_FEED_PATH_BEFORE_MAPID = "features/";
|
||||
private static final String MAPS_FEATURE_FEED_PATH_AFTER_MAPID = "/full";
|
||||
private static final String MAPS_VERSION_FEED_PATH_FORMAT =
|
||||
"%smaps/%s/versions/%s/full/%s";
|
||||
|
||||
private static final String MAP_ENTRY_ID_BEFORE_USER_ID = "maps/feeds/maps/";
|
||||
private static final String MAP_ENTRY_ID_BETWEEN_USER_ID_AND_MAP_ID = "/";
|
||||
private static final String V2_ONLY_PARAM = "?v=2.0";
|
||||
|
||||
public MapsClient(GDataClient dataClient,
|
||||
GDataParserFactory dataParserFactory) {
|
||||
super(dataClient, dataParserFactory);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getServiceName() {
|
||||
return SERVICE;
|
||||
}
|
||||
|
||||
public static String getMapsFeed() {
|
||||
if (DEBUG) {
|
||||
Log.d("Maps Client", "Requesting map feed:");
|
||||
}
|
||||
return MAPS_BASE_FEED_URL + MAPS_MAP_FEED_PATH + V2_ONLY_PARAM;
|
||||
}
|
||||
|
||||
public static String getFeaturesFeed(String mapid) {
|
||||
StringBuilder feed = new StringBuilder();
|
||||
feed.append(MAPS_BASE_FEED_URL);
|
||||
feed.append(MAPS_FEATURE_FEED_PATH_BEFORE_MAPID);
|
||||
feed.append(mapid);
|
||||
feed.append(MAPS_FEATURE_FEED_PATH_AFTER_MAPID);
|
||||
feed.append(V2_ONLY_PARAM);
|
||||
return feed.toString();
|
||||
}
|
||||
|
||||
public static String getMapIdFromMapEntryId(String entryId) {
|
||||
String userId = null;
|
||||
String mapId = null;
|
||||
if (DEBUG) {
|
||||
Log.d("Maps GData Client", "Getting mapid from entry id: " + entryId);
|
||||
}
|
||||
int userIdStart =
|
||||
entryId.indexOf(MAP_ENTRY_ID_BEFORE_USER_ID)
|
||||
+ MAP_ENTRY_ID_BEFORE_USER_ID.length();
|
||||
int userIdEnd =
|
||||
entryId.indexOf(MAP_ENTRY_ID_BETWEEN_USER_ID_AND_MAP_ID, userIdStart);
|
||||
if (userIdStart >= 0 && userIdEnd < entryId.length()
|
||||
&& userIdStart <= userIdEnd) {
|
||||
userId = entryId.substring(userIdStart, userIdEnd);
|
||||
}
|
||||
int mapIdStart =
|
||||
entryId.indexOf(MAP_ENTRY_ID_BETWEEN_USER_ID_AND_MAP_ID, userIdEnd)
|
||||
+ MAP_ENTRY_ID_BETWEEN_USER_ID_AND_MAP_ID.length();
|
||||
if (mapIdStart >= 0 && mapIdStart < entryId.length()) {
|
||||
mapId = entryId.substring(mapIdStart);
|
||||
}
|
||||
if (userId == null) {
|
||||
userId = "";
|
||||
}
|
||||
if (mapId == null) {
|
||||
mapId = "";
|
||||
}
|
||||
if (DEBUG) {
|
||||
Log.d("Maps GData Client", "Got user id: " + userId);
|
||||
Log.d("Maps GData Client", "Got map id: " + mapId);
|
||||
}
|
||||
return userId + "." + mapId;
|
||||
}
|
||||
|
||||
public static String getVersionFeed(String versionUserId,
|
||||
String versionClient, String currentVersion) {
|
||||
return String.format(MAPS_VERSION_FEED_PATH_FORMAT,
|
||||
MAPS_BASE_FEED_URL, versionUserId,
|
||||
versionClient, currentVersion);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
/*
|
||||
* 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.wireless.gdata.maps.xml;
|
||||
|
||||
import com.google.wireless.gdata.data.Entry;
|
||||
import com.google.wireless.gdata.data.Feed;
|
||||
import com.google.wireless.gdata.data.XmlUtils;
|
||||
import com.google.wireless.gdata.maps.MapFeatureEntry;
|
||||
import com.google.wireless.gdata.parser.ParseException;
|
||||
import com.google.wireless.gdata.parser.xml.XmlGDataParser;
|
||||
|
||||
import org.xmlpull.v1.XmlPullParser;
|
||||
import org.xmlpull.v1.XmlPullParserException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
/**
|
||||
* Parser for XML gdata maps data.
|
||||
*/
|
||||
public class XmlMapsGDataParser extends XmlGDataParser {
|
||||
|
||||
public XmlMapsGDataParser(InputStream is, XmlPullParser xpp)
|
||||
throws ParseException {
|
||||
super(is, xpp);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Feed createFeed() {
|
||||
return new Feed();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Entry createEntry() {
|
||||
return new MapFeatureEntry();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void handleExtraElementInFeed(Feed feed) {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void handleExtraLinkInEntry(
|
||||
String rel, String type, String href, Entry entry)
|
||||
throws XmlPullParserException, IOException {
|
||||
if (!(entry instanceof MapFeatureEntry)) {
|
||||
throw new IllegalArgumentException("Expected MapFeatureEntry!");
|
||||
}
|
||||
if (rel.endsWith("#view")) {
|
||||
return;
|
||||
}
|
||||
super.handleExtraLinkInEntry(rel, type, href, entry);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the current entry in the XML document. Assumes that the parser is
|
||||
* currently pointing just after an <entry>.
|
||||
*
|
||||
* @param plainEntry The entry that will be filled.
|
||||
* @throws XmlPullParserException Thrown if the XML cannot be parsed.
|
||||
* @throws IOException Thrown if the underlying inputstream cannot be read.
|
||||
*/
|
||||
@Override
|
||||
protected void handleEntry(Entry plainEntry)
|
||||
throws XmlPullParserException, IOException, ParseException {
|
||||
XmlPullParser parser = getParser();
|
||||
if (!(plainEntry instanceof MapFeatureEntry)) {
|
||||
throw new IllegalArgumentException("Expected MapFeatureEntry!");
|
||||
}
|
||||
MapFeatureEntry entry = (MapFeatureEntry) plainEntry;
|
||||
|
||||
int eventType = parser.getEventType();
|
||||
entry.setPrivacy("public");
|
||||
while (eventType != XmlPullParser.END_DOCUMENT) {
|
||||
switch (eventType) {
|
||||
case XmlPullParser.START_TAG:
|
||||
String name = parser.getName();
|
||||
if ("entry".equals(name)) {
|
||||
// stop parsing here.
|
||||
return;
|
||||
} else if ("id".equals(name)) {
|
||||
entry.setId(XmlUtils.extractChildText(parser));
|
||||
} else if ("title".equals(name)) {
|
||||
entry.setTitle(XmlUtils.extractChildText(parser));
|
||||
} else if ("link".equals(name)) {
|
||||
String rel = parser.getAttributeValue(null /* ns */, "rel");
|
||||
String type = parser.getAttributeValue(null /* ns */, "type");
|
||||
String href = parser.getAttributeValue(null /* ns */, "href");
|
||||
if ("edit".equals(rel)) {
|
||||
entry.setEditUri(href);
|
||||
} else if ("alternate".equals(rel) && "text/html".equals(type)) {
|
||||
entry.setHtmlUri(href);
|
||||
} else {
|
||||
handleExtraLinkInEntry(rel, type, href, entry);
|
||||
}
|
||||
} else if ("summary".equals(name)) {
|
||||
entry.setSummary(XmlUtils.extractChildText(parser));
|
||||
} else if ("content".equals(name)) {
|
||||
StringBuilder contentBuilder = new StringBuilder();
|
||||
int parentDepth = parser.getDepth();
|
||||
while (parser.getEventType() != XmlPullParser.END_DOCUMENT) {
|
||||
int etype = parser.next();
|
||||
switch (etype) {
|
||||
case XmlPullParser.START_TAG:
|
||||
contentBuilder.append('<');
|
||||
contentBuilder.append(parser.getName());
|
||||
contentBuilder.append('>');
|
||||
break;
|
||||
case XmlPullParser.TEXT:
|
||||
contentBuilder.append("<![CDATA[");
|
||||
contentBuilder.append(parser.getText());
|
||||
contentBuilder.append("]]>");
|
||||
break;
|
||||
case XmlPullParser.END_TAG:
|
||||
if (parser.getDepth() > parentDepth) {
|
||||
contentBuilder.append("</");
|
||||
contentBuilder.append(parser.getName());
|
||||
contentBuilder.append('>');
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (etype == XmlPullParser.END_TAG
|
||||
&& parser.getDepth() == parentDepth) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
entry.setContent(contentBuilder.toString());
|
||||
} else if ("category".equals(name)) {
|
||||
String category = parser.getAttributeValue(null /* ns */, "term");
|
||||
if (category != null && category.length() > 0) {
|
||||
entry.setCategory(category);
|
||||
}
|
||||
String categoryScheme =
|
||||
parser.getAttributeValue(null /* ns */, "scheme");
|
||||
if (categoryScheme != null && category.length() > 0) {
|
||||
entry.setCategoryScheme(categoryScheme);
|
||||
}
|
||||
} else if ("published".equals(name)) {
|
||||
entry.setPublicationDate(XmlUtils.extractChildText(parser));
|
||||
} else if ("updated".equals(name)) {
|
||||
entry.setUpdateDate(XmlUtils.extractChildText(parser));
|
||||
} else if ("deleted".equals(name)) {
|
||||
entry.setDeleted(true);
|
||||
} else if ("draft".equals(name)) {
|
||||
String draft = XmlUtils.extractChildText(parser);
|
||||
entry.setPrivacy("yes".equals(draft) ? "unlisted" : "public");
|
||||
} else if ("customProperty".equals(name)) {
|
||||
String attrName = parser.getAttributeValue(null, "name");
|
||||
String attrValue = XmlUtils.extractChildText(parser);
|
||||
entry.setAttribute(attrName, attrValue);
|
||||
} else if ("deleted".equals(name)) {
|
||||
entry.setDeleted(true);
|
||||
} else {
|
||||
handleExtraElementInEntry(entry);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
eventType = parser.next();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
/*
|
||||
* 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.wireless.gdata.maps.xml;
|
||||
|
||||
import com.google.wireless.gdata.client.GDataParserFactory;
|
||||
import com.google.wireless.gdata.data.Entry;
|
||||
import com.google.wireless.gdata.maps.MapFeatureEntry;
|
||||
import com.google.wireless.gdata.maps.MapsClient;
|
||||
import com.google.wireless.gdata.parser.GDataParser;
|
||||
import com.google.wireless.gdata.parser.ParseException;
|
||||
import com.google.wireless.gdata.parser.xml.XmlGDataParser;
|
||||
import com.google.wireless.gdata.parser.xml.XmlParserFactory;
|
||||
import com.google.wireless.gdata.serializer.GDataSerializer;
|
||||
import com.google.wireless.gdata.serializer.xml.XmlEntryGDataSerializer;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import org.xmlpull.v1.XmlPullParserException;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
/**
|
||||
* Factory of Xml parsers for gdata maps data.
|
||||
*/
|
||||
public class XmlMapsGDataParserFactory implements GDataParserFactory {
|
||||
private XmlParserFactory xmlFactory;
|
||||
|
||||
public XmlMapsGDataParserFactory(XmlParserFactory xmlFactory) {
|
||||
this.xmlFactory = xmlFactory;
|
||||
}
|
||||
|
||||
@Override
|
||||
public GDataParser createParser(InputStream is) throws ParseException {
|
||||
is = maybeLogCommunication(is);
|
||||
try {
|
||||
return new XmlGDataParser(is, xmlFactory.createParser());
|
||||
} catch (XmlPullParserException e) {
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public GDataParser createParser(Class cls, InputStream is)
|
||||
throws ParseException {
|
||||
is = maybeLogCommunication(is);
|
||||
try {
|
||||
return createParserForClass(cls, is);
|
||||
} catch (XmlPullParserException e) {
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private InputStream maybeLogCommunication(InputStream is)
|
||||
throws ParseException {
|
||||
if (MapsClient.LOG_COMMUNICATION) {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
byte[] buffer = new byte[2048];
|
||||
try {
|
||||
for (int n = is.read(buffer); n >= 0; n = is.read(buffer)) {
|
||||
String part = new String(buffer, 0, n);
|
||||
builder.append(part);
|
||||
Log.d("Response part", part);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new ParseException("Could not read stream", e);
|
||||
}
|
||||
String whole = builder.toString();
|
||||
Log.d("Response", whole);
|
||||
is = new ByteArrayInputStream(whole.getBytes());
|
||||
}
|
||||
return is;
|
||||
}
|
||||
|
||||
private GDataParser createParserForClass(
|
||||
Class<? extends Entry> cls, InputStream is)
|
||||
throws ParseException, XmlPullParserException {
|
||||
if (cls == MapFeatureEntry.class) {
|
||||
return new XmlMapsGDataParser(is, xmlFactory.createParser());
|
||||
} else {
|
||||
return new XmlGDataParser(is, xmlFactory.createParser());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public GDataSerializer createSerializer(Entry en) {
|
||||
if (en.getClass() == MapFeatureEntry.class) {
|
||||
return new XmlMapsGDataSerializer(xmlFactory, en);
|
||||
} else {
|
||||
return new XmlEntryGDataSerializer(xmlFactory, en);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,277 @@
|
||||
/*
|
||||
* 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.wireless.gdata.maps.xml;
|
||||
|
||||
import com.google.wireless.gdata.data.Entry;
|
||||
import com.google.wireless.gdata.data.StringUtils;
|
||||
import com.google.wireless.gdata.maps.MapFeatureEntry;
|
||||
import com.google.wireless.gdata.maps.MapsClient;
|
||||
import com.google.wireless.gdata.parser.ParseException;
|
||||
import com.google.wireless.gdata.parser.xml.XmlGDataParser;
|
||||
import com.google.wireless.gdata.parser.xml.XmlParserFactory;
|
||||
import com.google.wireless.gdata.serializer.xml.XmlEntryGDataSerializer;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import org.xmlpull.v1.XmlPullParserException;
|
||||
import org.xmlpull.v1.XmlSerializer;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Serializer of maps data for GData.
|
||||
*/
|
||||
public class XmlMapsGDataSerializer extends XmlEntryGDataSerializer {
|
||||
|
||||
private static final String APP_NAMESPACE = "http://www.w3.org/2007/app";
|
||||
|
||||
private MapFeatureEntry entry;
|
||||
private XmlParserFactory factory;
|
||||
private OutputStream stream;
|
||||
|
||||
public XmlMapsGDataSerializer(XmlParserFactory factory, Entry entry) {
|
||||
super(factory, entry);
|
||||
|
||||
this.factory = factory;
|
||||
this.entry = (MapFeatureEntry) entry;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void serialize(OutputStream out, int format)
|
||||
throws IOException, ParseException {
|
||||
XmlSerializer serializer = null;
|
||||
try {
|
||||
serializer = factory.createSerializer();
|
||||
} catch (XmlPullParserException e) {
|
||||
throw new ParseException("Unable to create XmlSerializer.", e);
|
||||
}
|
||||
|
||||
// TODO: make the output compact
|
||||
|
||||
ByteArrayOutputStream printStream;
|
||||
|
||||
if (MapsClient.LOG_COMMUNICATION) {
|
||||
printStream = new ByteArrayOutputStream();
|
||||
|
||||
serializer.setOutput(printStream, "UTF-8");
|
||||
} else {
|
||||
serializer.setOutput(out, "UTF-8");
|
||||
}
|
||||
|
||||
serializer.startDocument("UTF-8", Boolean.FALSE);
|
||||
|
||||
declareEntryNamespaces(serializer);
|
||||
serializer.startTag(XmlGDataParser.NAMESPACE_ATOM_URI, "entry");
|
||||
|
||||
stream = MapsClient.LOG_COMMUNICATION ? printStream : out;
|
||||
serializeEntryContents(serializer, format);
|
||||
|
||||
serializer.endTag(XmlGDataParser.NAMESPACE_ATOM_URI, "entry");
|
||||
serializer.endDocument();
|
||||
serializer.flush();
|
||||
|
||||
if (MapsClient.LOG_COMMUNICATION) {
|
||||
Log.d("Request", printStream.toString());
|
||||
out.write(printStream.toByteArray());
|
||||
stream = out;
|
||||
}
|
||||
}
|
||||
|
||||
private final void declareEntryNamespaces(XmlSerializer serializer)
|
||||
throws IOException {
|
||||
serializer.setPrefix(
|
||||
"" /* default ns */, XmlGDataParser.NAMESPACE_ATOM_URI);
|
||||
serializer.setPrefix(
|
||||
XmlGDataParser.NAMESPACE_GD, XmlGDataParser.NAMESPACE_GD_URI);
|
||||
declareExtraEntryNamespaces(serializer);
|
||||
}
|
||||
|
||||
private final void serializeEntryContents(XmlSerializer serializer,
|
||||
int format) throws IOException {
|
||||
if (format != FORMAT_CREATE) {
|
||||
serializeId(serializer, entry.getId());
|
||||
}
|
||||
|
||||
serializeTitle(serializer, entry.getTitle());
|
||||
|
||||
if (format != FORMAT_CREATE) {
|
||||
serializeLink(serializer,
|
||||
"edit" /* rel */, entry.getEditUri(), null /* type */);
|
||||
serializeLink(serializer,
|
||||
"alternate" /* rel */, entry.getHtmlUri(), "text/html" /* type */);
|
||||
}
|
||||
|
||||
serializeSummary(serializer, entry.getSummary());
|
||||
serializeContent(serializer, entry.getContent());
|
||||
serializeAuthor(serializer, entry.getAuthor(), entry.getEmail());
|
||||
serializeCategory(serializer,
|
||||
entry.getCategory(), entry.getCategoryScheme());
|
||||
|
||||
if (format == FORMAT_FULL) {
|
||||
serializePublicationDate(serializer, entry.getPublicationDate());
|
||||
}
|
||||
|
||||
if (format != FORMAT_CREATE) {
|
||||
serializeUpdateDate(serializer, entry.getUpdateDate());
|
||||
}
|
||||
|
||||
serializeExtraEntryContents(serializer, format);
|
||||
}
|
||||
|
||||
private static void serializeId(XmlSerializer serializer, String id)
|
||||
throws IOException {
|
||||
if (StringUtils.isEmpty(id)) {
|
||||
return;
|
||||
}
|
||||
serializer.startTag(null /* ns */, "id");
|
||||
serializer.text(id);
|
||||
serializer.endTag(null /* ns */, "id");
|
||||
}
|
||||
|
||||
private static void serializeTitle(XmlSerializer serializer, String title)
|
||||
throws IOException {
|
||||
if (StringUtils.isEmpty(title)) {
|
||||
return;
|
||||
}
|
||||
serializer.startTag(null /* ns */, "title");
|
||||
serializer.text(title);
|
||||
serializer.endTag(null /* ns */, "title");
|
||||
}
|
||||
|
||||
public static void serializeLink(XmlSerializer serializer, String rel,
|
||||
String href, String type) throws IOException {
|
||||
if (StringUtils.isEmpty(href)) {
|
||||
return;
|
||||
}
|
||||
serializer.startTag(null /* ns */, "link");
|
||||
serializer.attribute(null /* ns */, "rel", rel);
|
||||
serializer.attribute(null /* ns */, "href", href);
|
||||
if (!StringUtils.isEmpty(type)) {
|
||||
serializer.attribute(null /* ns */, "type", type);
|
||||
}
|
||||
serializer.endTag(null /* ns */, "link");
|
||||
}
|
||||
|
||||
private static void serializeSummary(XmlSerializer serializer, String summary)
|
||||
throws IOException {
|
||||
if (StringUtils.isEmpty(summary)) {
|
||||
return;
|
||||
}
|
||||
serializer.startTag(null /* ns */, "summary");
|
||||
serializer.text(summary);
|
||||
serializer.endTag(null /* ns */, "summary");
|
||||
}
|
||||
|
||||
private void serializeContent(XmlSerializer serializer, String content)
|
||||
throws IOException {
|
||||
if (content == null) {
|
||||
return;
|
||||
}
|
||||
serializer.startTag(null /* ns */, "content");
|
||||
if (content.contains("</Placemark>")) {
|
||||
serializer.attribute(
|
||||
null /* ns */, "type", "application/vnd.google-earth.kml+xml");
|
||||
serializer.flush();
|
||||
stream.write(content.getBytes());
|
||||
} else {
|
||||
serializer.text(content);
|
||||
}
|
||||
serializer.endTag(null /* ns */, "content");
|
||||
}
|
||||
|
||||
private static void serializeAuthor(XmlSerializer serializer, String author,
|
||||
String email) throws IOException {
|
||||
if (StringUtils.isEmpty(author) || StringUtils.isEmpty(email)) {
|
||||
return;
|
||||
}
|
||||
serializer.startTag(null /* ns */, "author");
|
||||
serializer.startTag(null /* ns */, "name");
|
||||
serializer.text(author);
|
||||
serializer.endTag(null /* ns */, "name");
|
||||
serializer.startTag(null /* ns */, "email");
|
||||
serializer.text(email);
|
||||
serializer.endTag(null /* ns */, "email");
|
||||
serializer.endTag(null /* ns */, "author");
|
||||
}
|
||||
|
||||
private static void serializeCategory(XmlSerializer serializer,
|
||||
String category, String categoryScheme) throws IOException {
|
||||
if (StringUtils.isEmpty(category) && StringUtils.isEmpty(categoryScheme)) {
|
||||
return;
|
||||
}
|
||||
serializer.startTag(null /* ns */, "category");
|
||||
if (!StringUtils.isEmpty(category)) {
|
||||
serializer.attribute(null /* ns */, "term", category);
|
||||
}
|
||||
if (!StringUtils.isEmpty(categoryScheme)) {
|
||||
serializer.attribute(null /* ns */, "scheme", categoryScheme);
|
||||
}
|
||||
serializer.endTag(null /* ns */, "category");
|
||||
}
|
||||
|
||||
private static void serializePublicationDate(XmlSerializer serializer,
|
||||
String publicationDate) throws IOException {
|
||||
if (StringUtils.isEmpty(publicationDate)) {
|
||||
return;
|
||||
}
|
||||
serializer.startTag(null /* ns */, "published");
|
||||
serializer.text(publicationDate);
|
||||
serializer.endTag(null /* ns */, "published");
|
||||
}
|
||||
|
||||
private static void serializeUpdateDate(XmlSerializer serializer,
|
||||
String updateDate) throws IOException {
|
||||
if (StringUtils.isEmpty(updateDate)) {
|
||||
return;
|
||||
}
|
||||
serializer.startTag(null /* ns */, "updated");
|
||||
serializer.text(updateDate);
|
||||
serializer.endTag(null /* ns */, "updated");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void serializeExtraEntryContents(XmlSerializer serializer,
|
||||
int format) throws IOException {
|
||||
Map<String, String> attrs = entry.getAllAttributes();
|
||||
for (Map.Entry<String, String> attr : attrs.entrySet()) {
|
||||
serializer.startTag("http://schemas.google.com/g/2005", "customProperty");
|
||||
serializer.attribute(null, "name", attr.getKey());
|
||||
serializer.text(attr.getValue());
|
||||
serializer.endTag("http://schemas.google.com/g/2005", "customProperty");
|
||||
}
|
||||
String privacy = entry.getPrivacy();
|
||||
if (!StringUtils.isEmpty(privacy)) {
|
||||
serializer.setPrefix("app", APP_NAMESPACE);
|
||||
if ("public".equals(privacy)) {
|
||||
serializer.startTag(APP_NAMESPACE, "control");
|
||||
serializer.startTag(APP_NAMESPACE, "draft");
|
||||
serializer.text("no");
|
||||
serializer.endTag(APP_NAMESPACE, "draft");
|
||||
serializer.endTag(APP_NAMESPACE, "control");
|
||||
}
|
||||
if ("unlisted".equals(privacy)) {
|
||||
serializer.startTag(APP_NAMESPACE, "control");
|
||||
serializer.startTag(APP_NAMESPACE, "draft");
|
||||
serializer.text("yes");
|
||||
serializer.endTag(APP_NAMESPACE, "draft");
|
||||
serializer.endTag(APP_NAMESPACE, "control");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user