Importing all the source code :)

This commit is contained in:
Rodrigo Damazio
2010-05-19 17:32:31 -07:00
parent 6d5d406ce9
commit 27dd5298f7
93 changed files with 18381 additions and 61 deletions
@@ -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);
@@ -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);
}
}
@@ -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;
}
}
@@ -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.
* &lt; 0 Use the absolute value as a distance in the current measurement km
* or mi
* 0 Turn off splits
* &gt; 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 &lt;entry&gt;.
*
* @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");
}
}
}
}