Merge from head.

This commit is contained in:
Sandor Dornbush
2011-01-13 21:07:15 -08:00
79 changed files with 2219 additions and 3126 deletions
@@ -1,32 +0,0 @@
/*
* Copyright 2009 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.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 constructor to prevent instantiation.
*/
private MyMapsConstants() { }
}
@@ -1,247 +0,0 @@
/*
* Copyright 2009 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.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);
}
}
@@ -1,101 +0,0 @@
/*
* Copyright 2009 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.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);
}
}
@@ -1,195 +0,0 @@
/*
* Copyright 2009 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.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;
}
}
@@ -1,190 +0,0 @@
/*
* Copyright 2010 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.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 != null && errorMessage.contains("401")) {
errorType = ERROR_AUTH;
} else {
errorType = ERROR_CONNECTION;
}
} catch (IOException e) {
Log.e(MyTracksConstants.TAG, "Exception", e);
errorMessage = e.getMessage();
if (errorMessage != null && 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;
}
}
@@ -1,350 +0,0 @@
/*
* Copyright 2009 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.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);
}
}
@@ -1,66 +0,0 @@
/*
* Copyright 2009 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.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);
}
}
@@ -85,13 +85,13 @@ public class DialogManager {
builder.setTitle("Title");
builder.setMessage("Message");
builder.setPositiveButton(activity.getString(R.string.ok), null);
builder.setNeutralButton(activity.getString(R.string.share_map),
builder.setNeutralButton(activity.getString(R.string.share_track),
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
activity.shareLinkToMyMap(activity.getSendToMyMapsMapId());
dialog.dismiss();
}
});
public void onClick(DialogInterface dialog, int which) {
activity.shareLinkToFusionTable(activity.sendToTrackId);
dialog.dismiss();
}
});
sendToGoogleResultDialog = builder.create();
return sendToGoogleResultDialog;
case DIALOG_WRITE_PROGRESS:
@@ -119,9 +119,9 @@ public class DialogManager {
sendToGoogleResultDialog.setIcon(success
? android.R.drawable.ic_dialog_info
: android.R.drawable.ic_dialog_alert);
sendToGoogleResultDialog.setMessage(activity.getMapsResultMessage());
sendToGoogleResultDialog.setMessage(activity.getFusionTablesResultMessage());
boolean canShare = activity.getSendToMyMapsMapId() != null;
boolean canShare = activity.getSendToFusionTablesTableId() != null;
View share =
sendToGoogleResultDialog.findViewById(android.R.id.button3);
if (share != null) {
@@ -21,8 +21,6 @@ import static com.google.android.apps.mytracks.DialogManager.DIALOG_SEND_TO_GOOG
import static com.google.android.apps.mytracks.DialogManager.DIALOG_WRITE_PROGRESS;
import com.google.android.accounts.Account;
import com.google.android.apps.mymaps.MyMapsConstants;
import com.google.android.apps.mymaps.MyMapsList;
import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.content.WaypointCreationRequest;
@@ -30,11 +28,11 @@ import com.google.android.apps.mytracks.io.AuthManager;
import com.google.android.apps.mytracks.io.AuthManagerFactory;
import com.google.android.apps.mytracks.io.GpxImporter;
import com.google.android.apps.mytracks.io.SendToDocs;
import com.google.android.apps.mytracks.io.SendToMyMaps;
import com.google.android.apps.mytracks.io.SendToFusionTables;
import com.google.android.apps.mytracks.io.TempFileCleaner;
import com.google.android.apps.mytracks.io.TrackWriter;
import com.google.android.apps.mytracks.io.TrackWriterFactory;
import com.google.android.apps.mytracks.io.SendToMyMaps.OnSendCompletedListener;
import com.google.android.apps.mytracks.io.SendToFusionTables.OnSendCompletedListener;
import com.google.android.apps.mytracks.io.TrackWriterFactory.TrackFileFormat;
import com.google.android.apps.mytracks.services.ITrackRecordingService;
import com.google.android.apps.mytracks.services.StatusAnnouncerFactory;
@@ -93,7 +91,6 @@ import org.xml.sax.SAXException;
*/
public class MyTracks extends TabActivity implements OnTouchListener,
OnSharedPreferenceChangeListener, ProgressIndicator {
/**
* Singleton instance
*/
@@ -124,10 +121,11 @@ public class MyTracks extends TabActivity implements OnTouchListener,
* Used by SendToGoogleResultDialog.
*/
public boolean sendToMyMapsSuccess = false;
public long sendToTrackId = -1;
public boolean sendToFusionTablesSuccess = false;
public boolean sendToDocsSuccess = false;
public String sendToMyMapsMapId;
public String sendToMyMapsMessage = "";
public String sendToFusionTablesTableId;
public String sendToFusionTablesMessage = "";
public String sendToDocsMessage = "";
/**
@@ -148,9 +146,9 @@ public class MyTracks extends TabActivity implements OnTouchListener,
*/
private long selectedTrackId = -1;
/**
* Does the user want to share the current track.
*/
/**
* Does the user want to share the current track.
*/
private boolean shareRequested = false;
/**
@@ -451,13 +449,13 @@ public class MyTracks extends TabActivity implements OnTouchListener,
case MyTracksConstants.GET_MAP: {
if (resultCode == RESULT_OK) {
results.putExtra("trackid", selectedTrackId);
if (results.hasExtra("mapid")) {
sendToMyMapsMapId = results.getStringExtra("mapid");
if (results.hasExtra("tableid")) {
sendToFusionTablesTableId = results.getStringExtra("tableid");
}
setProgressMessage(
R.string.progress_message_authenticating_mymaps);
R.string.progress_message_authenticating_fusiontables);
authenticate(results, MyTracksConstants.SEND_TO_GOOGLE,
MyMapsConstants.MAPSHOP_SERVICE);
SendToFusionTables.SERVICE_ID);
} else {
dialogManager.dismissDialogSafely(DIALOG_PROGRESS);
}
@@ -517,13 +515,14 @@ public class MyTracks extends TabActivity implements OnTouchListener,
}
break;
}
case MyTracksConstants.AUTHENTICATE_TO_DOCS: {
case MyTracksConstants.AUTHENTICATE_TO_DOCLIST: {
if (resultCode == RESULT_OK) {
setProgressValue(0);
setProgressMessage(
R.string.progress_message_authenticating_docs);
authenticate(results,
MyTracksConstants.AUTHENTICATE_TO_TRIX, "writely");
MyTracksConstants.AUTHENTICATE_TO_TRIX,
SendToDocs.GDATA_SERVICE_NAME_DOCLIST);
} else {
dialogManager.dismissDialogSafely(DIALOG_PROGRESS);
}
@@ -534,7 +533,8 @@ public class MyTracks extends TabActivity implements OnTouchListener,
setProgressValue(30);
setProgressMessage(
R.string.progress_message_authenticating_docs);
authenticate(results, MyTracksConstants.SEND_TO_DOCS, "wise");
authenticate(results, MyTracksConstants.SEND_TO_DOCS,
SendToDocs.GDATA_SERVICE_NAME_TRIX);
} else {
dialogManager.dismissDialogSafely(DIALOG_PROGRESS);
}
@@ -546,8 +546,10 @@ public class MyTracks extends TabActivity implements OnTouchListener,
setProgressValue(50);
setProgressMessage(R.string.progress_message_sending_docs);
final long trackId = results.getLongExtra("trackid", selectedTrackId);
final SendToDocs sender = new SendToDocs(this, authMap.get("wise"),
authMap.get("writely"), trackId);
final SendToDocs sender = new SendToDocs(this,
authMap.get(SendToDocs.GDATA_SERVICE_NAME_TRIX),
authMap.get(SendToDocs.GDATA_SERVICE_NAME_DOCLIST), trackId);
sendToTrackId = trackId;
Runnable onCompletion = new Runnable() {
public void run() {
setProgressValue(100);
@@ -556,7 +558,7 @@ public class MyTracks extends TabActivity implements OnTouchListener,
public void run() {
sendToDocsMessage = sender.getStatusMessage();
sendToDocsSuccess = sender.wasSuccess();
handleMapsFinish();
handleFusionTablesFinish(trackId);
}
});
}
@@ -569,61 +571,55 @@ public class MyTracks extends TabActivity implements OnTouchListener,
break;
}
case MyTracksConstants.SEND_TO_GOOGLE_DIALOG: {
shareRequested = false;
shareRequested = false;
dialogManager.showDialogSafely(DIALOG_SEND_TO_GOOGLE);
break;
}
case MyTracksConstants.SEND_TO_GOOGLE: {
if (results != null && resultCode == RESULT_OK) {
final String mapId;
final long trackId;
if (results.hasExtra("mapid")) {
mapId = results.getStringExtra("mapid");
} else {
mapId = "new";
}
if (results.hasExtra("trackid")) {
trackId = results.getLongExtra("trackid", -1);
} else {
trackId = selectedTrackId;
}
OnSendCompletedListener onCompletion = new OnSendCompletedListener() {
@Override
public void onSendCompleted(String mapId, boolean success,
public void onSendCompleted(String tableId, boolean success,
int statusMessage) {
sendToMyMapsMessage = getString(statusMessage);
sendToMyMapsSuccess = success;
if (sendToMyMapsSuccess) {
sendToMyMapsMapId = mapId;
// Update the map id for this track:
sendToFusionTablesMessage = getString(statusMessage);
sendToFusionTablesSuccess = success;
if (sendToFusionTablesSuccess) {
sendToFusionTablesTableId = tableId;
// Update the table id for this track:
try {
Track track = providerUtils.getTrack(trackId);
track.setMapId(mapId);
track.setTableId(tableId);
providerUtils.updateTrack(track);
} catch (RuntimeException e) {
// If that fails whatever reasons we'll just log an error, but
// continue.
Log.w(MyTracksConstants.TAG, "Updating map id failed.", e);
Log.w(MyTracksConstants.TAG, "Updating table id failed.", e);
}
}
if (dialogManager.getSendToGoogleDialog().getSendToDocs()) {
onActivityResult(MyTracksConstants.AUTHENTICATE_TO_DOCS,
onActivityResult(MyTracksConstants.AUTHENTICATE_TO_DOCLIST,
RESULT_OK, new Intent());
} else {
dialogManager.dismissDialogSafely(DIALOG_PROGRESS);
runOnUiThread(new Runnable() {
public void run() {
handleMapsFinish();
handleFusionTablesFinish(trackId);
}
});
}
}
};
final SendToMyMaps sender = new SendToMyMaps(this, mapId, auth,
sendToTrackId = trackId;
final SendToFusionTables sender = new SendToFusionTables(this, auth,
trackId, this/*progressIndicator*/, onCompletion);
HandlerThread handlerThread = new HandlerThread("SendToMyMaps");
HandlerThread handlerThread = new HandlerThread("SendToFusionTables");
handlerThread.start();
Handler handler = new Handler(handlerThread.getLooper());
handler.post(sender);
@@ -654,8 +650,8 @@ public class MyTracks extends TabActivity implements OnTouchListener,
case MyTracksConstants.SHARE_LINK: {
Track selectedTrack = providerUtils.getTrack(selectedTrackId);
if (selectedTrack != null) {
if (selectedTrack.getMapId().length() > 0) {
shareLinkToMyMap(selectedTrack.getMapId());
if (selectedTrack.getTableId().length() > 0) {
shareLinkToFusionTable(selectedTrackId);
} else {
shareRequested = true;
dialogManager.showDialogSafely(DIALOG_SEND_TO_GOOGLE);
@@ -724,11 +720,11 @@ public class MyTracks extends TabActivity implements OnTouchListener,
* Resets status information for sending to MyMaps/Docs.
*/
public void resetSendToGoogleStatus() {
sendToMyMapsMessage = "";
sendToMyMapsSuccess = true;
sendToFusionTablesMessage = "";
sendToFusionTablesSuccess = true;
sendToDocsMessage = "";
sendToDocsSuccess = true;
sendToMyMapsMapId = null;
sendToFusionTablesTableId = null;
}
private void importGpxFile(final String fileName) {
@@ -796,10 +792,10 @@ public class MyTracks extends TabActivity implements OnTouchListener,
}
/**
* Shares a link to a My Map via external app (email, gmail, ...)
* Shares a link to a fusion table via external app (email, gmail, ...)
* A chooser with apps that support text/plain will be shown to the user.
*/
public void shareLinkToMyMap(String mapId) {
public void shareLinkToFusionTable(long trackId) {
Intent shareIntent = new Intent(Intent.ACTION_SEND);
shareIntent.setType("text/plain");
shareIntent.putExtra(Intent.EXTRA_SUBJECT,
@@ -811,7 +807,8 @@ public class MyTracks extends TabActivity implements OnTouchListener,
getString(R.string.share_url_only_key), false);
}
String url = MyMapsConstants.MAPSHOP_BASE_URL + "?msa=0&msid=" + mapId;
Track track = providerUtils.getTrack(trackId);
String url = SendToFusionTables.getMapVisualizationUrl(track);
String msg = shareUrlOnly ? url : String.format(
getResources().getText(R.string.share_map_body_format).toString(), url);
shareIntent.putExtra(Intent.EXTRA_TEXT, msg);
@@ -930,7 +927,7 @@ public class MyTracks extends TabActivity implements OnTouchListener,
service);
authMap.put(service, auth);
}
Log.d(MyTracksConstants.TAG, "Loggin in to " + service + "...");
Log.d(MyTracksConstants.TAG, "Logging in to " + service + "...");
if (AuthManagerFactory.useModernAuthManager()) {
runOnUiThread(new Runnable() {
@Override
@@ -1040,19 +1037,13 @@ public class MyTracks extends TabActivity implements OnTouchListener,
setProgressValue(0);
clearProgressMessage();
dialogManager.showDialogSafely(DIALOG_PROGRESS);
if (sendToGoogleDialog.getSendToMyMaps()) {
if (!sendToGoogleDialog.getCreateNewMap()) {
Intent listIntent = new Intent(this, MyMapsList.class);
startActivityForResult(listIntent, MyTracksConstants.GET_MAP);
} else {
setProgressValue(0);
setProgressMessage(
R.string.progress_message_authenticating_mymaps);
authenticate(new Intent(), MyTracksConstants.SEND_TO_GOOGLE,
MyMapsConstants.MAPSHOP_SERVICE);
}
if (sendToGoogleDialog.getSendToFusionTables()) {
setProgressValue(0);
setProgressMessage(R.string.progress_message_authenticating_fusiontables);
authenticate(new Intent(), MyTracksConstants.SEND_TO_GOOGLE,
SendToFusionTables.SERVICE_ID);
} else {
onActivityResult(MyTracksConstants.AUTHENTICATE_TO_DOCS, RESULT_OK,
onActivityResult(MyTracksConstants.AUTHENTICATE_TO_DOCLIST, RESULT_OK,
new Intent());
}
}
@@ -1167,25 +1158,24 @@ public class MyTracks extends TabActivity implements OnTouchListener,
}
/**
* Notifies that uploading to maps is finished.
* Notifies that uploading to fusion tables is finished.
*/
private void handleMapsFinish() {
if (shareRequested && sendToMyMapsSuccess) {
private void handleFusionTablesFinish(long trackId) {
if (shareRequested && sendToFusionTablesSuccess) {
// Just share
Toast.makeText(this, getMapsResultMessage(), Toast.LENGTH_LONG).show();
shareLinkToMyMap(sendToMyMapsMapId);
Toast.makeText(this, getFusionTablesResultMessage(), Toast.LENGTH_LONG).show();
shareLinkToFusionTable(trackId);
} else {
dialogManager.showDialogSafely(
DialogManager.DIALOG_SEND_TO_GOOGLE_RESULT);
dialogManager.showDialogSafely(DialogManager.DIALOG_SEND_TO_GOOGLE_RESULT);
}
}
public String getMapsResultMessage() {
public String getFusionTablesResultMessage() {
StringBuilder message = new StringBuilder();
SendToGoogleDialog sendToGoogleDialog =
dialogManager.getSendToGoogleDialog();
if (sendToGoogleDialog.getSendToMyMaps()) {
message.append(sendToMyMapsMessage);
if (sendToGoogleDialog.getSendToFusionTables()) {
message.append(sendToFusionTablesMessage);
}
if (sendToGoogleDialog.getSendToDocs()) {
if (message.length() > 0) {
@@ -1193,10 +1183,6 @@ public class MyTracks extends TabActivity implements OnTouchListener,
}
message.append(sendToDocsMessage);
}
if (sendToMyMapsSuccess && sendToDocsSuccess) {
message.append(' ');
message.append(getString(R.string.status_mymap_info));
}
return message.toString();
}
@@ -1216,12 +1202,12 @@ public class MyTracks extends TabActivity implements OnTouchListener,
return dialogManager;
}
public String getSendToMyMapsMapId() {
return sendToMyMapsMapId;
public String getSendToFusionTablesTableId() {
return sendToFusionTablesTableId;
}
public boolean getSendToGoogleSuccess() {
return sendToMyMapsSuccess && sendToDocsSuccess;
return sendToFusionTablesSuccess && sendToDocsSuccess;
}
// @VisibleForTesting
@@ -50,7 +50,7 @@ public abstract class MyTracksConstants {
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_DOCLIST = 8;
public static final int AUTHENTICATE_TO_TRIX = 9;
public static final int SEND_TO_DOCS = 10;
public static final int DELETE_TRACK = 11;
@@ -213,6 +213,9 @@ public abstract class MyTracksConstants {
}
}
public static final String MAPSHOP_BASE_URL =
"http://maps.google.com/maps/ms";
/**
* This is an abstract utility class.
*/
@@ -15,7 +15,6 @@
*/
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;
@@ -24,6 +23,7 @@ import com.google.android.apps.mytracks.content.WaypointsColumns;
import com.google.android.apps.mytracks.services.StatusAnnouncerFactory;
import com.google.android.apps.mytracks.stats.TripStatistics;
import com.google.android.apps.mytracks.util.ApiFeatures;
import com.google.android.apps.mytracks.util.GeoRect;
import com.google.android.apps.mytracks.util.MyTracksUtils;
import com.google.android.maps.GeoPoint;
import com.google.android.maps.MapActivity;
@@ -789,7 +789,7 @@ public class MyTracksMap extends MapActivity
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);
R.string.tracklist_share_linkfusion);
share.add(0, MyTracksConstants.MENU_SHARE_GPX_FILE, 0,
R.string.tracklist_share_gpx_file);
share.add(0, MyTracksConstants.MENU_SHARE_KML_FILE, 0,
@@ -29,7 +29,6 @@ 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.
@@ -39,10 +38,7 @@ import android.widget.RadioGroup;
*/
public class SendToGoogleDialog extends Dialog {
private RadioGroup groupMyMaps;
private RadioButton createNewMapRadioButton;
private RadioButton pickMapRadioButton;
private CheckBox sendToMyMapsCheckBox;
private CheckBox sendToFusionTablesCheckBox;
private CheckBox sendToDocsCheckBox;
private RadioButton sendStatsRadioButton;
private RadioButton sendStatsAndPointsRadioButton;
@@ -76,26 +72,19 @@ public class SendToGoogleDialog extends Dialog {
OnCheckedChangeListener checkBoxListener = new OnCheckedChangeListener() {
public void onCheckedChanged(CompoundButton button, boolean checked) {
sendButton.setEnabled(
sendToMyMapsCheckBox.isChecked() ||
sendToFusionTablesCheckBox.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);
sendToFusionTablesCheckBox =
(CheckBox) findViewById(R.id.sendtogoogle_google_fusiontables);
sendToFusionTablesCheckBox.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(
@@ -104,18 +93,12 @@ public class SendToGoogleDialog extends Dialog {
SharedPreferences prefs =
getContext().getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0);
if (prefs != null) {
sendToMyMapsCheckBox.setChecked(
sendToFusionTablesCheckBox.setChecked(
prefs.getBoolean(
getContext().getString(R.string.send_to_my_maps_key), true));
sendToDocsCheckBox.setChecked(
prefs.getBoolean(
getContext().getString(R.string.send_to_docs_key), true));
pickMapRadioButton.setChecked(
prefs.getBoolean(
getContext().getString(R.string.pick_existing_map_key), false));
createNewMapRadioButton.setChecked(
!prefs.getBoolean(
getContext().getString(R.string.pick_existing_map_key), false));
sendStatsAndPointsRadioButton.setChecked(
prefs.getBoolean(
getContext().getString(R.string.send_stats_and_points_key),
@@ -136,13 +119,10 @@ public class SendToGoogleDialog extends Dialog {
if (editor != null) {
editor.putBoolean(
getContext().getString(R.string.send_to_my_maps_key),
sendToMyMapsCheckBox.isChecked());
sendToFusionTablesCheckBox.isChecked());
editor.putBoolean(
getContext().getString(R.string.send_to_docs_key),
sendToDocsCheckBox.isChecked());
editor.putBoolean(
getContext().getString(R.string.pick_existing_map_key),
pickMapRadioButton.isChecked());
editor.putBoolean(
getContext().getString(R.string.send_stats_and_points_key),
sendStatsAndPointsRadioButton.isChecked());
@@ -152,18 +132,14 @@ public class SendToGoogleDialog extends Dialog {
super.onStop();
}
public boolean getSendToMyMaps() {
return sendToMyMapsCheckBox.isChecked();
public boolean getSendToFusionTables() {
return sendToFusionTablesCheckBox.isChecked();
}
public boolean getSendToDocs() {
return sendToDocsCheckBox.isChecked();
}
public boolean getCreateNewMap() {
return createNewMapRadioButton.isChecked();
}
public boolean getSendStatsAndPoints() {
return sendStatsAndPointsRadioButton.isChecked();
}
@@ -269,7 +269,7 @@ public class StatsActivity extends Activity
super.onCreateOptionsMenu(menu);
MenuItem currentSegment = menu.add(0,
MyTracksConstants.MENU_CURRENT_SEGMENT, 0, R.string.current_segment);
currentSegment.setIcon(R.drawable.menu_by_distance);
currentSegment.setIcon(R.drawable.ic_menu_lastsegment);
return true;
}
@@ -15,16 +15,13 @@
*/
package com.google.android.apps.mytracks;
import com.google.android.apps.mytracks.util.MyTracksUtils;
import com.google.android.maps.mytracks.R;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
@@ -84,16 +81,7 @@ public class WelcomeActivity extends Activity {
builder.setIcon(R.drawable.arrow_icon);
AlertDialog dialog = builder.create();
dialog.show();
// Get the version from the manifest.
try {
PackageInfo pi =
getPackageManager().getPackageInfo("com.google.android.maps.mytracks",
PackageManager.GET_META_DATA);
((TextView) dialog.findViewById(R.id.about_version_register)).
setText(pi.versionName);
} catch (NameNotFoundException e) {
Log.w(MyTracksConstants.TAG, "Failed to get version info.", e);
}
((TextView) dialog.findViewById(R.id.about_version_register)).
setText(MyTracksUtils.getMyTracksVersion(this));
}
}
@@ -99,7 +99,8 @@ public class MyTracksProvider extends ContentProvider {
+ TracksColumns.ELEVATIONGAIN + " FLOAT, "
+ TracksColumns.MINGRADE + " FLOAT, "
+ TracksColumns.MAXGRADE + " FLOAT, "
+ TracksColumns.MAPID + " STRING);");
+ TracksColumns.MAPID + " STRING, "
+ TracksColumns.TABLEID + " STRING);");
db.execSQL("CREATE TABLE " + WAYPOINTS_TABLE + " ("
+ WaypointsColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT, "
+ WaypointsColumns.NAME + " STRING, "
@@ -146,13 +147,15 @@ public class MyTracksProvider extends ContentProvider {
} else {
// Incremental updates go here.
// Each time you increase the DB version, add a corresponding if clause.
// Sensor data.
if (oldVersion <= 17) {
Log.w(TAG, "Upgrading database from version " + oldVersion + " to "
+ newVersion + ", adding sensor column.");
+ newVersion + ", adding sensor and tableid columns.");
db.execSQL("ALTER TABLE " + TRACKPOINTS_TABLE
+ " ADD " + TrackPointsColumns.SENSOR + " BLOB");
db.execSQL("ALTER TABLE " + TRACKS_TABLE
+ " ADD " + TracksColumns.TABLEID + " STRING");
}
}
}
@@ -24,6 +24,7 @@ import android.location.Location;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.nio.charset.Charset;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.TimeZone;
@@ -58,8 +59,8 @@ public class GpxTrackWriter implements TrackFormatWriter {
@Override
public void writeHeader() {
if (pw != null) {
pw.println("<?xml version=\"1.0\" encoding=\"ISO-8859-1\" ");
pw.println("standalone=\"yes\"?>");
pw.format("<?xml version=\"1.0\" encoding=\"%s\" standalone=\"yes\"?>\n",
Charset.defaultCharset().name());
pw.println("<?xml-stylesheet type=\"text/xsl\" href=\"details.xsl\"?>");
pw.println("<gpx");
pw.println(" version=\"1.0\"");
@@ -15,32 +15,21 @@
*/
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.docs.DocsHelper;
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.stats.TripStatistics;
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 com.google.wireless.gdata.client.GDataServiceClient;
import android.app.Activity;
import android.content.SharedPreferences;
@@ -48,16 +37,7 @@ 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.
@@ -65,29 +45,15 @@ import java.text.NumberFormat;
* @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";
/** The GData service name for Google Spreadsheets (aka Trix) */
public static final String GDATA_SERVICE_NAME_TRIX = "wise";
/** The GData service name for the Google Docs Document List */
public static final String GDATA_SERVICE_NAME_DOCLIST = "writely";
private final Activity activity;
private final AuthManager wiseAuth;
private final AuthManager writelyAuth;
private final AuthManager trixAuth;
private final AuthManager docListAuth;
private final long trackId;
private final boolean metricUnits;
private final HandlerThread handlerThread;
@@ -95,18 +61,15 @@ public class SendToDocs {
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) {
public SendToDocs(Activity activity, AuthManager trixAuth,
AuthManager docListAuth, long trackId) {
this.activity = activity;
this.wiseAuth = wiseAuth;
this.writelyAuth = writelyAuth;
this.trixAuth = trixAuth;
this.docListAuth = docListAuth;
this.trackId = trackId;
SharedPreferences preferences = activity.getSharedPreferences(
@@ -196,22 +159,24 @@ public class SendToDocs {
* @param track the track
*/
private boolean uploadToDocs(Track track) {
GDataWrapper wiseWrapper = new GDataWrapper();
wiseWrapper.setAuthManager(wiseAuth);
wiseWrapper.setRetryOnAuthFailure(true);
GDataWrapper<GDataServiceClient> docListWrapper = new GDataWrapper<GDataServiceClient>();
docListWrapper.setAuthManager(docListAuth);
docListWrapper.setRetryOnAuthFailure(true);
GDataWrapper<GDataServiceClient> trixWrapper = new GDataWrapper<GDataServiceClient>();
trixWrapper.setAuthManager(trixAuth);
trixWrapper.setRetryOnAuthFailure(true);
GDataWrapper writelyWrapper = new GDataWrapper();
writelyWrapper.setAuthManager(writelyAuth);
writelyWrapper.setRetryOnAuthFailure(true);
DocsHelper docsHelper = new DocsHelper();
GDataClient androidClient = null;
try {
androidClient = GDataClientFactory.getGDataClient(activity);
SpreadsheetsClient gdataClient = new SpreadsheetsClient(androidClient,
new XmlMapsGDataParserFactory(new AndroidXmlParserFactory()));
wiseWrapper.setClient(gdataClient);
trixWrapper.setClient(gdataClient);
Log.d(MyTracksConstants.TAG,
"GData connection prepared: " + this.writelyAuth);
"GData connection prepared: " + this.docListAuth);
String sheetTitle = "My Tracks";
if (track.getCategory() != null && !track.getCategory().equals("")) {
@@ -220,15 +185,19 @@ public class SendToDocs {
DocumentsClient docsGdataClient = new DocumentsClient(androidClient,
new XmlMapsGDataParserFactory(new AndroidXmlParserFactory()));
writelyWrapper.setClient(docsGdataClient);
docListWrapper.setClient(docsGdataClient);
// First try to find the spreadsheet:
if (!getSpreadsheetId(writelyWrapper, sheetTitle)) {
Log.i(MyTracksConstants.TAG, "Spreadsheet lookup failed.");
String spreadsheetId = null;
try {
spreadsheetId = docsHelper.requestSpreadsheetId(docListWrapper,
sheetTitle);
} catch (IOException e) {
Log.i(MyTracksConstants.TAG, "Spreadsheet lookup failed.", e);
return false;
}
if (spreadSheetId == null) {
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...).
@@ -237,22 +206,33 @@ public class SendToDocs {
} catch (InterruptedException e) {
Log.e(MyTracksConstants.TAG, "Sleep interrupted", e);
}
if (!getSpreadsheetId(writelyWrapper, sheetTitle)) {
Log.i(MyTracksConstants.TAG, "2nd spreadsheet lookup failed.");
try {
spreadsheetId = docsHelper.requestSpreadsheetId(docListWrapper,
sheetTitle);
} catch (IOException e) {
Log.i(MyTracksConstants.TAG, "2nd spreadsheet lookup failed.", e);
return false;
}
}
// We were unable to find an existing spreadsheet, so create a new one.
MyTracks.getInstance().setProgressValue(70);
if (spreadSheetId == null) {
if (spreadsheetId == null) {
Log.i(MyTracksConstants.TAG, "Creating new spreadsheet: " + sheetTitle);
if (!createSpreadSheet(writelyWrapper, sheetTitle)) {
try {
spreadsheetId = docsHelper.createSpreadsheet(activity, docListWrapper,
sheetTitle);
} catch (IOException e) {
Log.i(MyTracksConstants.TAG, "Failed to create new spreadsheet "
+ sheetTitle, e);
return false;
}
MyTracks.getInstance().setProgressValue(80);
createdNewSpreadSheet = true;
if (spreadSheetId == null) {
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,
@@ -265,10 +245,16 @@ public class SendToDocs {
} catch (InterruptedException e) {
Log.e(MyTracksConstants.TAG, "Sleep interrupted", e);
}
if (!getSpreadsheetId(writelyWrapper, sheetTitle)) {
try {
spreadsheetId = docsHelper.requestSpreadsheetId(docListWrapper,
sheetTitle);
} catch (IOException e) {
Log.i(MyTracksConstants.TAG, "Failed create-failed lookup", e);
return false;
}
if (spreadSheetId == null) {
if (spreadsheetId == null) {
MyTracks.getInstance().setProgressValue(87);
// Re-try
try {
@@ -276,11 +262,16 @@ public class SendToDocs {
} catch (InterruptedException e) {
Log.e(MyTracksConstants.TAG, "Sleep interrupted", e);
}
if (!getSpreadsheetId(writelyWrapper, sheetTitle)) {
try {
spreadsheetId = docsHelper.requestSpreadsheetId(docListWrapper,
sheetTitle);
} catch (IOException e) {
Log.i(MyTracksConstants.TAG, "Failed create-failed relookup", e);
return false;
}
}
if (spreadSheetId == null) {
if (spreadsheetId == null) {
Log.i(MyTracksConstants.TAG,
"Creating new spreadsheet really failed.");
return false;
@@ -288,14 +279,21 @@ public class SendToDocs {
}
}
if (!getWorkSheetId(wiseWrapper)) {
Log.i(MyTracksConstants.TAG, "Looking up worksheet id failed.");
String worksheetId = null;
try {
worksheetId = docsHelper.getWorksheetId(trixWrapper, spreadsheetId);
if (worksheetId == null) {
throw new IOException("Worksheet ID lookup returned empty");
}
} catch (IOException e) {
Log.i(MyTracksConstants.TAG, "Looking up worksheet id failed.", e);
return false;
}
MyTracks.getInstance().setProgressValue(90);
insertRowNet(track, spreadSheetId, workSheetId);
docsHelper.addTrackRow(activity, trixAuth, spreadsheetId, worksheetId,
track, metricUnits);
Log.i(MyTracksConstants.TAG, "Done uploading to docs.");
} catch (IOException e) {
Log.e(MyTracksConstants.TAG, "Unable to upload docs.", e);
@@ -307,223 +305,4 @@ public class SendToDocs {
}
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);
TripStatistics stats = track.getStatistics();
/* 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", stats.getStartTime()), sb);
appendTag("totaltime", StringUtils.formatTimeAlwaysShowingHours(
stats.getTotalTime()), sb);
appendTag("movingtime", StringUtils.formatTimeAlwaysShowingHours(
stats.getMovingTime()), sb);
appendLargeUnitsTag("distance", stats.getTotalDistance() / 1000, sb);
appendTag("distanceunit",
metricUnits
? activity.getString(R.string.kilometer)
: activity.getString(R.string.mile),
sb);
appendLargeUnitsTag("averagespeed", stats.getAverageSpeed() * 3.6, sb);
appendLargeUnitsTag("averagemovingspeed",
stats.getAverageMovingSpeed() * 3.6, sb);
appendLargeUnitsTag("maxspeed", stats.getMaxSpeed() * 3.6, sb);
appendTag("speedunit",
metricUnits
? activity.getString(R.string.kilometer_per_hour)
: activity.getString(R.string.mile_per_hour),
sb);
appendSmallUnitsTag("elevationgain", stats.getTotalElevationGain(), sb);
appendSmallUnitsTag("minelevation", stats.getMinElevation(), sb);
appendSmallUnitsTag("maxelevation", stats.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,658 @@
/*
* 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 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.Waypoint;
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.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.StringUtils;
import com.google.android.apps.mytracks.util.UnitConversions;
import com.google.android.maps.mytracks.R;
import com.google.api.client.googleapis.GoogleHeaders;
import com.google.api.client.googleapis.GoogleTransport;
import com.google.api.client.http.GenericUrl;
import com.google.api.client.http.HttpRequest;
import com.google.api.client.http.HttpResponse;
import com.google.api.client.http.HttpResponseException;
import com.google.api.client.http.HttpTransport;
import com.google.api.client.http.InputStreamContent;
import com.google.api.client.util.Strings;
import android.app.Activity;
import android.content.SharedPreferences;
import android.database.Cursor;
import android.location.Location;
import android.util.Log;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.List;
import java.util.Vector;
/**
* A helper class used to transmit tracks to Google Fusion Tables.
* A new instance should be used for each upload.
*
* @author Leif Hendrik Wilden
*/
public class SendToFusionTables implements Runnable {
/**
* Listener invoked when sending to fusion tables completes.
*/
public interface OnSendCompletedListener {
void onSendCompleted(String tableId, boolean success, int statusMessage);
}
/** The GData service id for Fusion Tables. */
public static final String SERVICE_ID = "fusiontables";
/** The path for viewing a map visualization of a table. */
private static final String FUSIONTABLES_MAP =
"http://www.google.com/fusiontables/embedviz?" +
"viz=MAP&q=select+col0,+col1,+col2,+col3+from+%s+&h=false&" +
"lat=%f&lng=%f&z=%d&t=1&l=col2";
/** Standard base feed url for Fusion Tables. */
private static final String FUSIONTABLES_BASE_FEED_URL =
"http://www.google.com/fusiontables/api/query";
private static final int MAX_POINTS_PER_UPLOAD = 2048;
private static final String GDATA_VERSION = "2";
private final Activity context;
private final AuthManager auth;
private final long trackId;
private final ProgressIndicator progressIndicator;
private final OnSendCompletedListener onCompletion;
private final StringUtils stringUtils;
private final MyTracksProviderUtils providerUtils;
// Progress status
private int totalLocationsRead;
private int totalLocationsPrepared;
private int totalLocationsUploaded;
private int totalLocations;
private int totalSegmentsUploaded;
private HttpTransport transport;
private String tableId;
private static String MARKER_TYPE_START = "large_green";
private static String MARKER_TYPE_END = "large_red";
private static String MARKER_TYPE_WAYPOINT = "large_yellow";
public SendToFusionTables(Activity context, AuthManager auth,
long trackId, ProgressIndicator progressIndicator,
OnSendCompletedListener onCompletion) {
this.context = context;
this.auth = auth;
this.trackId = trackId;
this.progressIndicator = progressIndicator;
this.onCompletion = onCompletion;
this.stringUtils = new StringUtils(context);
this.providerUtils = MyTracksProviderUtils.Factory.get(context);
transport = GoogleTransport.create();
GoogleHeaders headers = (GoogleHeaders) transport.defaultHeaders;
headers.setApplicationName("Google-MyTracks-" + MyTracksUtils.getMyTracksVersion(context));
headers.gdataVersion = GDATA_VERSION;
}
@Override
public void run() {
Log.d(MyTracksConstants.TAG, "Sending to Fusion tables: trackId = " + trackId);
doUpload();
}
public static String getMapVisualizationUrl(Track track) {
// TODO(leifhendrik): Determine correct bounding box and zoom level that will show the entire track.
TripStatistics stats = track.getStatistics();
double latE6 = stats.getBottom() + (stats.getTop() - stats.getBottom()) / 2;
double lonE6 = stats.getLeft() + (stats.getRight() - stats.getLeft()) / 2;
int z = 15;
return String.format(FUSIONTABLES_MAP, track.getTableId(), latE6 / 1.E6, lonE6 / 1.E6, z);
}
private void doUpload() {
((GoogleHeaders) transport.defaultHeaders).setGoogleLogin(auth.getAuthToken());
int statusMessageId = R.string.error_sending_to_fusiontables;
boolean success = true;
try {
progressIndicator.setProgressValue(1);
progressIndicator.setProgressMessage(R.string.progress_message_reading_track);
// Get the track meta-data
Track track = providerUtils.getTrack(trackId);
String originalDescription = track.getDescription();
// Create a new table:
progressIndicator.setProgressMessage(R.string.progress_message_creating_fusiontable);
success = createNewTable(track);
if (success) {
success = makeTableUnlisted(tableId);
}
// Upload all of the segments of the track plus start/end markers
if (success) {
success = uploadAllTrackPoints(track, originalDescription);
}
// Upload all the waypoints.
if (success) {
success = uploadWaypoints(track);
statusMessageId = R.string.status_new_fusiontable_has_been_created;
}
Log.d(MyTracksConstants.TAG, "SendToFusionTables: Done: " + success);
progressIndicator.setProgressValue(100);
} finally {
final boolean finalSuccess = success;
final int finalStatusMessageId = statusMessageId;
context.runOnUiThread(new Runnable() {
public void run() {
if (onCompletion != null) {
onCompletion.onSendCompleted(
tableId, finalSuccess, finalStatusMessageId);
}
}
});
}
}
/**
* Creates a new table.
* If successful sets {@link #tableId}.
*
* @return true in case of success.
*/
private boolean createNewTable(Track track) {
Log.d(MyTracksConstants.TAG, "Creating a new fusion table.");
String query = "CREATE TABLE '" + track.getName() +
"' (name:STRING,description:STRING,geometry:LOCATION,marker:STRING)";
return runUpdate(query);
}
private boolean makeTableUnlisted(String tableId) {
Log.d(MyTracksConstants.TAG, "Setting visibility to unlisted.");
String query = "UPDATE TABLE " + tableId + " SET VISIBILITY = unlisted";
return runUpdate(query);
}
/**
* Formats given values SQL style. Escapes single quotes with a backslash.
*
* @param values the values to format
* @return the values formatted as: ('value1','value2',...,'value_n').
*/
private static String values(String... values) {
StringBuilder builder = new StringBuilder("(");
for (int i = 0; i < values.length; i++) {
if (i > 0) {
builder.append(',');
}
builder.append('\'');
builder.append(values[i].replaceAll("'", "''"));
builder.append('\'');
}
builder.append(')');
return builder.toString();
}
/**
* Creates a new row representing a marker.
*
* @param name the marker name
* @param description the marker description
* @param the marker location
* @return true in case of success.
*/
private boolean createNewPoint(String name, String description, Location location,
String marker) {
Log.d(MyTracksConstants.TAG, "Creating a new row with a point.");
String query = "INSERT INTO " + tableId + " (name,description,geometry,marker) VALUES "
+ values(name, description, getKmlPoint(location), marker);
return runUpdate(query);
}
/**
* Creates a new row representing a line segment.
*
* @param track the track/segment to draw
* @return true in case of success.
*/
private boolean createNewLineString(Track track) {
Log.d(MyTracksConstants.TAG, "Creating a new row with a point.");
String query = "INSERT INTO " + tableId
+ " (name,description,geometry) VALUES "
+ values(track.getName(), track.getDescription(), getKmlLineString(track));
return runUpdate(query);
}
private boolean uploadAllTrackPoints(final Track track, String originalDescription) {
SharedPreferences preferences = context.getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0);
boolean metricUnits = true;
if (preferences != null) {
metricUnits = preferences.getBoolean(context.getString(R.string.metric_units_key), true);
}
Cursor locationsCursor = providerUtils.getLocationsCursor(track.getId(), 0, -1, false);
try {
if (!locationsCursor.moveToFirst()) {
Log.w(MyTracksConstants.TAG, "Unable to get any points to upload");
return false;
}
totalLocationsRead = 0;
totalLocationsPrepared = 0;
totalLocationsUploaded = 0;
totalLocations = locationsCursor.getCount();
totalSegmentsUploaded = 0;
// 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;
Vector<Double> distances = new Vector<Double>();
Vector<Double> elevations = new Vector<Double>();
DoubleBuffer elevationBuffer = new DoubleBuffer(MyTracksConstants.ELEVATION_SMOOTHING_FACTOR);
List<Location> locations = new ArrayList<Location>(MAX_POINTS_PER_UPLOAD);
progressIndicator.setProgressMessage(R.string.progress_message_reading_track);
Location lastLocation = null;
do {
if (totalLocationsRead % 100 == 0) {
updateProgress();
}
Location loc = providerUtils.createLocation(locationsCursor);
locations.add(loc);
if (totalLocationsRead == 0) {
// Put a marker at the first point of the first valid segment:
String name = track.getName() + " " + context.getString(R.string.start);
createNewPoint(name, "", loc, MARKER_TYPE_START);
}
// Add to the elevation profile.
if (loc != null && MyTracksUtils.isValidLocation(loc)) {
// All points go into the smoothing buffer...
elevationBuffer.setNext(metricUnits ? loc.getAltitude()
: loc.getAltitude() * UnitConversions.M_TO_FT);
if (lastLocation != null) {
double dist = lastLocation.distanceTo(loc);
totalDistance += dist;
}
// ...but only a few points are really used to keep the url short.
if (totalLocationsRead % elevationSamplingFrequency == 0) {
distances.add(totalDistance);
elevations.add(elevationBuffer.getAverage());
}
}
// If the location was not valid, it's a segment split, so make sure the
// distance between the previous segment and the new one is not accounted
// for in the next iteration.
lastLocation = loc;
// Every now and then, upload the accumulated points
if (totalLocationsRead % MAX_POINTS_PER_UPLOAD == MAX_POINTS_PER_UPLOAD - 1) {
if (!prepareAndUploadPoints(track, locations)) {
return false;
}
}
totalLocationsRead++;
} while (locationsCursor.moveToNext());
// Do a final upload with what's left
if (!prepareAndUploadPoints(track, locations)) {
return false;
}
// Put an end marker at the last point of the last valid segment:
if (lastLocation != null) {
track.setDescription("<p>" + originalDescription + "</p><p>"
+ stringUtils.generateTrackDescription(track, distances, elevations)
+ "</p>");
String name = track.getName() + " " + context.getString(R.string.end);
return createNewPoint(name, track.getDescription(), lastLocation, MARKER_TYPE_END);
}
return true;
} finally {
locationsCursor.close();
}
}
/**
* Appends the given location to the string in the format:
* longitude,latitude[,altitude]
*
* @param location the location to be added
* @param builder the string builder to use
*/
private void appendCoordinate(Location location, StringBuilder builder) {
builder
.append(location.getLongitude())
.append(",")
.append(location.getLatitude());
if (location.hasAltitude()) {
builder.append(",");
builder.append(location.getAltitude());
}
}
/**
* Gets a KML Point tag for the given location.
*
* @param location The location.
* @return the kml.
*/
private String getKmlPoint(Location location) {
StringBuilder builder = new StringBuilder("<Point><coordinates>");
appendCoordinate(location, builder);
builder.append("</coordinates></Point>");
return builder.toString();
}
/**
* Returns a KML Point tag for the given location.
*
* @param location The location.
* @return the kml.
*/
private String getKmlLineString(Track track) {
StringBuilder builder = new StringBuilder("<LineString><coordinates>");
for (Location location : track.getLocations()) {
appendCoordinate(location, builder);
builder.append(' ');
}
builder.append("</coordinates></LineString>");
return builder.toString();
}
private boolean prepareAndUploadPoints(Track track, List<Location> locations) {
progressIndicator.setProgressMessage(R.string.progress_message_preparing_track);
updateProgress();
int numLocations = locations.size();
if (numLocations < 2) {
Log.d(MyTracksConstants.TAG, "Not preparing/uploading too few points");
totalLocationsUploaded += numLocations;
return true;
}
// Prepare/pre-process the points
ArrayList<Track> splitTracks = prepareLocations(track, locations);
// Start uploading them
progressIndicator.setProgressMessage(R.string.progress_message_sending_fusiontables);
for (Track splitTrack : splitTracks) {
if (totalSegmentsUploaded > 1) {
splitTrack.setName(splitTrack.getName() + " "
+ String.format(
context.getString(R.string.part), totalSegmentsUploaded));
}
totalSegmentsUploaded++;
Log.d(MyTracksConstants.TAG,
"SendToFusionTables: Prepared feature for upload w/ "
+ splitTrack.getLocations().size() + " points.");
// Transmit tracks via GData feed:
// -------------------------------
Log.d(MyTracksConstants.TAG,
"SendToFusionTables: Uploading to table " + tableId + " w/ auth " + auth);
if (!uploadTrackPoints(splitTrack)) {
Log.e(MyTracksConstants.TAG, "Uploading failed");
return false;
}
}
locations.clear();
totalLocationsUploaded += numLocations;
updateProgress();
return true;
}
/**
* Prepares a buffer of locations for transmission to google fusion tables.
*
* @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, Iterable<Location> locations) {
ArrayList<Track> splitTracks = new ArrayList<Track>();
// Create segments from each full track:
Track segment = new Track();
TripStatistics segmentStats = segment.getStatistics();
TripStatistics trackStats = track.getStatistics();
segment.setId(track.getId());
segment.setName(track.getName());
segment.setDescription(/* track.getDescription() */ "");
segment.setCategory(track.getCategory());
segmentStats.setStartTime(trackStats.getStartTime());
segmentStats.setStopTime(trackStats.getStopTime());
boolean startNewTrackSegment = false;
for (Location loc : locations) {
if (totalLocationsPrepared % 100 == 0) {
updateProgress();
}
if (loc.getLatitude() > 90) {
startNewTrackSegment = true;
}
if (startNewTrackSegment) {
// Close up the last segment.
prepareTrackSegment(segment, splitTracks);
Log.d(MyTracksConstants.TAG,
"MyTracksSendToFusionTables: 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 (segmentStats.getStartTime() < 0) {
segmentStats.setStartTime(loc.getTime());
}
}
totalLocationsPrepared++;
}
prepareTrackSegment(segment, splitTracks);
return splitTracks;
}
/**
* Prepares a track segment for sending to google fusion tables.
* 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) {
TripStatistics segmentStats = segment.getStatistics();
if (segmentStats.getStopTime() < 0
&& segment.getLocations().size() > 0) {
segmentStats.setStopTime(segment.getLocations().size() - 1);
}
/*
* Decimate to 2 meter precision. Fusion tables doesn't like too many
* points:
*/
MyTracksUtils.decimate(segment, 2.0);
/* If the track still has > 2500 points, split it in pieces: */
final int maxPoints = 2500;
if (segment.getLocations().size() > maxPoints) {
splitTracks.addAll(MyTracksUtils.split(segment, maxPoints));
} else if (segment.getLocations().size() >= 2) {
splitTracks.add(segment);
}
}
private boolean uploadTrackPoints(Track splitTrack) {
int numLocations = splitTrack.getLocations().size();
if (numLocations < 2) {
// Need at least two points for a polyline:
Log.w(MyTracksConstants.TAG, "Not uploading too few points");
return true;
}
return createNewLineString(splitTrack);
}
/**
* Uploads all of the waypoints associated with this track to a table.
*
* @param track The track to upload waypoints for.
*
* @return True on success.
*/
private boolean uploadWaypoints(final Track track) {
// TODO: Stream through the 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.
boolean success = true;
Cursor c = null;
c = providerUtils.getWaypointsCursor(
track.getId(), 0,
MyTracksConstants.MAX_LOADED_WAYPOINTS_POINTS);
if (c != null) {
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);
Log.d(MyTracksConstants.TAG, "SendToFusionTables: Creating waypoint.");
success = createNewPoint(wpt.getName(), wpt.getDescription(), wpt.getLocation(),
MARKER_TYPE_WAYPOINT);
if (!success) {
break;
}
}
}
}
if (!success) {
Log.w(MyTracksConstants.TAG, "SendToFusionTables: upload waypoints failed.");
}
return success;
}
/**
* Sets the current upload progress.
*/
private void updateProgress() {
// The percent of the total that represents the completed part of this
// segment.
int totalPercentage =
(totalLocationsRead + totalLocationsPrepared + totalLocationsUploaded)
/ (totalLocations * 3);
totalPercentage = Math.min(99, totalPercentage);
progressIndicator.setProgressValue(totalPercentage);
}
/**
* Runs an update query. Handles authentication.
*
* @param query The given SQL like query
* @return true in case of success
*/
private boolean runUpdate(final String query) {
GDataWrapper<HttpTransport> wrapper = new GDataWrapper<HttpTransport>();
wrapper.setAuthManager(auth);
wrapper.setRetryOnAuthFailure(true);
wrapper.setClient(transport);
Log.d(MyTracksConstants.TAG, "GData connection prepared: " + this.auth);
wrapper.runQuery(new QueryFunction<HttpTransport>() {
@Override
public void query(HttpTransport client)
throws IOException, GDataWrapper.ParseException, GDataWrapper.HttpException,
GDataWrapper.AuthenticationException {
HttpRequest request = transport.buildPostRequest();
request.headers.contentType = "application/x-www-form-urlencoded";
GenericUrl url = new GenericUrl(FUSIONTABLES_BASE_FEED_URL);
request.url = url;
InputStreamContent isc = new InputStreamContent();
String sql = "sql=" + URLEncoder.encode(query, "UTF-8");
isc.inputStream = new ByteArrayInputStream(Strings.toBytesUtf8(sql));
request.content = isc;
Log.d(MyTracksConstants.TAG, "Running update query " + url.toString() + ": " + sql);
try {
HttpResponse response = request.execute();
boolean success = response.isSuccessStatusCode;
if (success) {
byte[] result = new byte[1024];
response.getContent().read(result);
String s = Strings.fromBytesUtf8(result);
String[] lines = s.split(Strings.LINE_SEPARATOR);
if (lines[0].equals("tableid")) {
tableId = lines[1];
Log.d(MyTracksConstants.TAG, "tableId = " + tableId);
} else {
Log.w(MyTracksConstants.TAG, "Unrecognized response: " + lines[0]);
}
} else {
Log.d(MyTracksConstants.TAG, "Query failed: " + response.statusMessage + " (" +
response.statusCode + ")");
throw new GDataWrapper.HttpException(response.statusCode, response.statusMessage);
}
} catch (HttpResponseException e) {
if (e.response.statusCode == 401) {
throw new GDataWrapper.AuthenticationException(e);
}
}
}
});
return wrapper.getErrorType() == GDataWrapper.ERROR_NO_ERROR;
}
}
@@ -1,689 +0,0 @@
/*
* 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.Waypoint;
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.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.util.Log;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Vector;
import org.xmlpull.v1.XmlPullParserException;
/**
* A helper class used to transmit tracks to Google MyMaps.
* A new instance should be used for each upload.
*
* @author Leif Hendrik Wilden
*/
public class SendToMyMaps implements Runnable {
public static final String NEW_MAP_ID = "new";
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 static final int MAX_POINTS_PER_UPLOAD = 2048;
private final Activity context;
private final AuthManager auth;
private final long trackId;
private final ProgressIndicator progressIndicator;
private final OnSendCompletedListener onCompletion;
private final StringUtils stringUtils;
private final MyTracksProviderUtils providerUtils;
private String mapId;
// GData service
private MyMapsGDataWrapper wrapper;
private MyMapsGDataConverter gdataConverter;
// Progress status
private int totalLocationsRead;
private int totalLocationsPrepared;
private int totalLocationsUploaded;
private int totalLocations;
private int totalSegmentsUploaded;
public interface OnSendCompletedListener {
void onSendCompleted(String mapId, boolean success, int statusMessage);
}
public SendToMyMaps(Activity context, String mapId, AuthManager auth,
long trackId, ProgressIndicator progressIndicator,
OnSendCompletedListener onCompletion) {
this.context = context;
this.mapId = mapId;
this.auth = auth;
this.trackId = trackId;
this.progressIndicator = progressIndicator;
this.onCompletion = onCompletion;
this.stringUtils = new StringUtils(context);
this.providerUtils = MyTracksProviderUtils.Factory.get(context);
}
@Override
public void run() {
Log.d(MyTracksConstants.TAG, "Sending to MyMaps: trackId = " + trackId);
doUpload();
}
private void doUpload() {
int statusMessageId = R.string.error_sending_to_mymap;
boolean success = true;
try {
gdataConverter = new MyMapsGDataConverter();
progressIndicator.setProgressValue(1);
progressIndicator.setProgressMessage(
R.string.progress_message_reading_track);
// Get the track meta-data
Track track = providerUtils.getTrack(trackId);
String 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);
// Create a new map if necessary:
boolean isNewMap = mapId.equals(NEW_MAP_ID);
if (isNewMap) {
success = createNewMap(track, originalDescription);
}
// Upload all of the segments of the track plus start/end markers
if (success) {
success = uploadAllTrackPoints(track, originalDescription);
}
// Put waypoints.
if (success) {
success = uploadWaypoints(track);
if (!success) {
Log.w(MyTracksConstants.TAG,
"SendToMyMaps: upload waypoints failed.");
}
}
if (success) {
statusMessageId = isNewMap
? R.string.status_new_mymap_has_been_created
: 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();
}
final boolean finalSuccess = success;
final int finalStatusMessageId = statusMessageId;
context.runOnUiThread(new Runnable() {
public void run() {
if (onCompletion != null) {
onCompletion.onSendCompleted(
mapId, finalSuccess, finalStatusMessageId);
}
}
});
}
}
/**
* 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, final String description) {
progressIndicator.setProgressMessage(
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(description + " - "
+ 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(
context.getString(R.string.default_map_public_key), 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 uploadAllTrackPoints(
final Track track, String originalDescription) {
SharedPreferences preferences = context.getSharedPreferences(
MyTracksSettings.SETTINGS_NAME, 0);
boolean metricUnits = true;
if (preferences != null) {
metricUnits =
preferences.getBoolean(context.getString(R.string.metric_units_key),
true);
}
Cursor locationsCursor =
providerUtils.getLocationsCursor(track.getId(), 0, -1, false);
try {
if (!locationsCursor.moveToFirst()) {
Log.w(MyTracksConstants.TAG, "Unable to get any points to upload");
return false;
}
totalLocationsRead = 0;
totalLocationsPrepared = 0;
totalLocationsUploaded = 0;
totalLocations = locationsCursor.getCount();
totalSegmentsUploaded = 0;
// 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;
Vector<Double> distances = new Vector<Double>();
Vector<Double> elevations = new Vector<Double>();
DoubleBuffer elevationBuffer =
new DoubleBuffer(MyTracksConstants.ELEVATION_SMOOTHING_FACTOR);
List<Location> locations = new ArrayList<Location>(MAX_POINTS_PER_UPLOAD);
progressIndicator.setProgressMessage(
R.string.progress_message_reading_track);
Location lastLocation = null;
do {
if (totalLocationsRead % 100 == 0) {
updateProgress();
}
Location loc = providerUtils.createLocation(locationsCursor);
locations.add(loc);
if (totalLocationsRead == 0) {
// Put a marker at the first point of the first valid segment:
uploadMarker(track, loc, true);
}
// Add to the elevation profile.
if (loc != null && MyTracksUtils.isValidLocation(loc)) {
// All points go into the smoothing buffer...
elevationBuffer.setNext(metricUnits ? loc.getAltitude()
: loc.getAltitude() * UnitConversions.M_TO_FT);
if (lastLocation != null) {
double dist = lastLocation.distanceTo(loc);
totalDistance += dist;
}
// ...but only a few points are really used to keep the url short.
if (totalLocationsRead % elevationSamplingFrequency == 0) {
distances.add(totalDistance);
elevations.add(elevationBuffer.getAverage());
}
}
// If the location was not valid, it's a segment split, so make sure the
// distance between the previous segment and the new one is not accounted
// for in the next iteration.
lastLocation = loc;
// Every now and then, upload the accumulated points
if (totalLocationsRead % MAX_POINTS_PER_UPLOAD
== MAX_POINTS_PER_UPLOAD - 1) {
if (!prepareAndUploadPoints(track, locations)) {
return false;
}
}
totalLocationsRead++;
} while (locationsCursor.moveToNext());
// Do a final upload with what's left
if (!prepareAndUploadPoints(track, locations)) {
return false;
}
// Put an end marker at the last point of the last valid segment:
if (lastLocation != null) {
track.setDescription("<p>" + originalDescription + "</p><p>"
+ stringUtils.generateTrackDescription(
track, distances, elevations)
+ "</p>");
return uploadMarker(track, lastLocation, false);
}
return true;
} finally {
locationsCursor.close();
}
}
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;
}
/**
* 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!");
}
}
/**
* 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);
myMapsFeature.setDescription(isStart ? "" : track.getDescription());
return myMapsFeature;
}
private boolean prepareAndUploadPoints(Track track, List<Location> locations) {
progressIndicator.setProgressMessage(
R.string.progress_message_preparing_track);
updateProgress();
int numLocations = locations.size();
if (numLocations < 2) {
Log.d(MyTracksConstants.TAG, "Not preparing/uploading too few points");
totalLocationsUploaded += numLocations;
return true;
}
// Prepare/pre-process the points
ArrayList<Track> splitTracks = prepareLocations(track, locations);
// Start uploading them
progressIndicator.setProgressMessage(
R.string.progress_message_sending_mymaps);
for (Track splitTrack : splitTracks) {
if (totalSegmentsUploaded > 1) {
splitTrack.setName(splitTrack.getName() + " "
+ String.format(
context.getString(R.string.part), totalSegmentsUploaded));
}
totalSegmentsUploaded++;
Log.d(MyTracksConstants.TAG,
"SendToMyMaps: Prepared feature for upload w/ "
+ splitTrack.getLocations().size() + " points.");
// Transmit tracks via GData feed:
// -------------------------------
Log.d(MyTracksConstants.TAG,
"SendToMyMaps: Uploading to map " + mapId + " w/ auth " + auth);
if (!uploadTrackPoints(splitTrack)) {
Log.e(MyTracksConstants.TAG, "Uploading failed");
return false;
}
}
locations.clear();
totalLocationsUploaded += numLocations;
updateProgress();
return true;
}
/**
* 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, Iterable<Location> locations) {
ArrayList<Track> splitTracks = new ArrayList<Track>();
// Create segments from each full track:
Track segment = new Track();
TripStatistics segmentStats = segment.getStatistics();
TripStatistics trackStats = track.getStatistics();
segment.setId(track.getId());
segment.setName(track.getName());
segment.setDescription(/* track.getDescription() */ "");
segment.setCategory(track.getCategory());
segmentStats.setStartTime(trackStats.getStartTime());
segmentStats.setStopTime(trackStats.getStopTime());
boolean startNewTrackSegment = false;
for (Location loc : locations) {
if (totalLocationsPrepared % 100 == 0) {
updateProgress();
}
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 (segmentStats.getStartTime() < 0) {
segmentStats.setStartTime(loc.getTime());
}
}
totalLocationsPrepared++;
}
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) {
TripStatistics segmentStats = segment.getStatistics();
if (segmentStats.getStopTime() < 0
&& segment.getLocations().size() > 0) {
segmentStats.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);
}
}
/**
* Uploads a given list of tracks to Google MyMaps using the maps GData feed.
*/
private boolean uploadTrackPoints(final Track track) {
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, client, featureFeed);
}
});
}
private boolean uploadTrackPoints(Track splitTrack,
MapsClient client,
String featureFeed)
throws IOException, Exception {
Entry entry = null;
int numLocations = splitTrack.getLocations().size();
if (numLocations < 2) {
// Need at least two points for a polyline:
Log.w(MyTracksConstants.TAG, "Not uploading too few points");
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;
}
/**
* 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;
}
/**
* 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();
}
}
}
});
}
/**
* 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());
}
myMapsFeature.setDescription(wpt.getDescription().replaceAll("\n", "<br>"));
return myMapsFeature;
}
/**
* Sets the current upload progress.
*/
private void updateProgress() {
// The percent of the total that represents the completed part of this
// segment.
int totalPercentage =
(totalLocationsRead + totalLocationsPrepared + totalLocationsUploaded)
/ (totalLocations * 3);
totalPercentage = Math.min(99, totalPercentage);
progressIndicator.setProgressValue(totalPercentage);
}
}
@@ -27,6 +27,7 @@ import android.os.Build;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.nio.charset.Charset;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.TimeZone;
@@ -77,7 +78,8 @@ public class TcxTrackWriter implements TrackFormatWriter {
if (pw == null) {
return;
}
pw.println("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\" ?>");
pw.format("<?xml version=\"1.0\" encoding=\"%s\" standalone=\"no\" ?>\n",
Charset.defaultCharset().name());
pw.print("<TrainingCenterDatabase ");
pw.print("xmlns=\"http://www.garmin.com/xmlschemas/TrainingCenterDatabase/v2\" ");
pw.print("xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" ");
@@ -37,13 +37,15 @@ public class BackupColumns {
TracksColumns.MOVINGTIME, TracksColumns.AVGSPEED, TracksColumns.AVGMOVINGSPEED,
TracksColumns.MAXSPEED, TracksColumns.MINELEVATION, TracksColumns.MAXELEVATION,
TracksColumns.ELEVATIONGAIN, TracksColumns.MINGRADE, TracksColumns.MAXGRADE,
TracksColumns.MINLAT, TracksColumns.MAXLAT, TracksColumns.MINLON, TracksColumns.MAXLON };
TracksColumns.MINLAT, TracksColumns.MAXLAT, TracksColumns.MINLON, TracksColumns.MAXLON,
TracksColumns.MAPID, TracksColumns.TABLEID};
public static final byte[] TRACKS_BACKUP_COLUMN_TYPES = {
LONG_TYPE_ID, STRING_TYPE_ID, STRING_TYPE_ID, STRING_TYPE_ID,
LONG_TYPE_ID, LONG_TYPE_ID, LONG_TYPE_ID, LONG_TYPE_ID, INT_TYPE_ID,
FLOAT_TYPE_ID, LONG_TYPE_ID, LONG_TYPE_ID, FLOAT_TYPE_ID, FLOAT_TYPE_ID,
FLOAT_TYPE_ID, FLOAT_TYPE_ID, FLOAT_TYPE_ID, FLOAT_TYPE_ID, FLOAT_TYPE_ID,
FLOAT_TYPE_ID, INT_TYPE_ID, INT_TYPE_ID, INT_TYPE_ID, INT_TYPE_ID };
FLOAT_TYPE_ID, INT_TYPE_ID, INT_TYPE_ID, INT_TYPE_ID, INT_TYPE_ID, STRING_TYPE_ID,
STRING_TYPE_ID};
public static final String[] WAYPOINTS_BACKUP_COLUMNS = {
WaypointsColumns._ID, WaypointsColumns.TRACKID, WaypointsColumns.NAME,
@@ -0,0 +1,372 @@
/*
* 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.docs;
import com.google.android.apps.mytracks.MyTracksConstants;
import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.io.AuthManager;
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.stats.TripStatistics;
import com.google.android.apps.mytracks.util.ResourceUtils;
import com.google.android.apps.mytracks.util.StringUtils;
import com.google.android.maps.mytracks.R;
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.SpreadsheetsClient;
import com.google.wireless.gdata.docs.SpreadsheetsClient.WorksheetEntry;
import com.google.wireless.gdata.parser.GDataParser;
import com.google.wireless.gdata.parser.ParseException;
import com.google.wireless.gdata2.client.AuthenticationException;
import android.content.Context;
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.util.concurrent.atomic.AtomicReference;
/**
* This class contains helper methods for interacting with Google Docs and
* Google Spreadsheets.
*
* @author Sandor Dornbush
* @author Matthew Simmons
*/
public class DocsHelper {
private static final String DOCS_FEED_URL =
"http://docs.google.com/feeds/documents/private/full";
private static final String DOCS_SPREADSHEET_URL =
"http://docs.google.com/feeds/documents/private/full/spreadsheet%3A";
private static final String DOCS_WORKSHEETS_URL_FORMAT =
"http://spreadsheets.google.com/feeds/worksheets/%s/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_FORMAT =
"http://spreadsheets.google.com/feeds/list/%s/%s/private/full";
private static final String CONTENT_TYPE_PARAM = "Content-Type";
private static final String OPENDOCUMENT_SPREADSHEET_MIME_TYPE =
"application/x-vnd.oasis.opendocument.spreadsheet";
private static final String ATOM_FEED_MIME_TYPE = "application/atom+xml";
/**
* Creates a new MyTracks spreadsheet with the given name.
*
* @param context The context associated with this request.
* @param docListWrapper The GData handle for the Document List service.
* @param name The name for the newly-created spreadsheet.
* @return The spreadsheet ID, if one is created. {@code null} will be
* returned if a GData error didn't occur, but no spreadsheet ID was
* returned.
*/
public String createSpreadsheet(final Context context,
final GDataWrapper<GDataServiceClient> docListWrapper, final String name) throws IOException {
final AtomicReference<String> idSaver = new AtomicReference<String>();
boolean result = docListWrapper.runQuery(new QueryFunction<GDataServiceClient>() {
@Override
public void query(GDataServiceClient client) throws IOException,
GDataWrapper.AuthenticationException {
// Construct and send request
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=" +
docListWrapper.getAuthManager().getAuthToken());
conn.setDoOutput(true);
OutputStream os = conn.getOutputStream();
ResourceUtils.readBinaryFileToOutputStream(
context, 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;
}
String id = result.substring(
idStringStart + DOCS_SPREADSHEET_URL.length(), idTagCloseIndex);
Log.i(MyTracksConstants.TAG, "Created new spreadsheet: " + id);
idSaver.set(id);
}});
if (!result) {
throw newIOException(docListWrapper,
"Failed to create new spreadsheet.");
}
return idSaver.get();
}
/**
* Retrieve the ID of a spreadsheet with the given name.
*
* @param docListWrapper The GData handle for the Document List service.
* @param title The name of the spreadsheet whose ID is to be retrieved.
* @return The spreadsheet ID, if it can be retrieved. {@code null} will
* be returned if no spreadsheet exists by the given name.
* @throws IOException If an error occurs during the GData request.
*/
public String requestSpreadsheetId(final GDataWrapper<GDataServiceClient> docListWrapper,
final String title) throws IOException {
final AtomicReference<String> idSaver = new AtomicReference<String>();
boolean result = docListWrapper.runQuery(new QueryFunction<GDataServiceClient>() {
@Override
public void query(GDataServiceClient client)
throws IOException, GDataWrapper.ParseException, GDataWrapper.HttpException,
GDataWrapper.AuthenticationException {
GDataParser listParser;
try {
listParser = client.getParserForFeed(Entry.class,
DOCS_MY_SPREADSHEETS_FEED_URL,
docListWrapper.getAuthManager().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('/');
idSaver.set(entryId.substring(lastSlash + 15));
break;
}
}
} catch (ParseException e) {
throw new GDataWrapper.ParseException(e);
} catch (HttpException e) {
throw new GDataWrapper.HttpException(e.getStatusCode(), e.getMessage());
}
}
});
if (!result) {
throw newIOException(docListWrapper,
"Failed to retrieve spreadsheet list.");
}
return idSaver.get();
}
/**
* Retrieve the ID of the first worksheet in the named spreadsheet.
*
* @param trixWrapper The GData handle for the spreadsheet service.
* @param spreadsheetId The GData ID for the given spreadsheet.
* @return The worksheet ID, if it can be retrieved. {@code null} will be
* returned if the GData request returns without error, but without an
* ID.
* @throws IOException If an error occurs during the GData request.
*/
public String getWorksheetId(final GDataWrapper<GDataServiceClient> trixWrapper,
final String spreadsheetId) throws IOException {
final AtomicReference<String> idSaver = new AtomicReference<String>();
boolean result = trixWrapper.runQuery(new QueryFunction<GDataServiceClient>() {
@Override
public void query(GDataServiceClient client)
throws GDataWrapper.AuthenticationException, IOException, GDataWrapper.ParseException, GDataWrapper.HttpException {
String uri = String.format(DOCS_WORKSHEETS_URL_FORMAT, spreadsheetId);
GDataParser sheetParser;
try {
sheetParser = ((SpreadsheetsClient) client).getParserForWorksheetsFeed(uri,
trixWrapper.getAuthManager().getAuthToken());
sheetParser.init();
if (!sheetParser.hasMoreData()) {
Log.i(MyTracksConstants.TAG, "Found no worksheets");
return;
}
// Grab the first worksheet.
WorksheetEntry worksheetEntry =
(WorksheetEntry) sheetParser.readNextEntry(new WorksheetEntry());
int lastSlash = worksheetEntry.getId().lastIndexOf('/');
idSaver.set(worksheetEntry.getId().substring(lastSlash + 1));
} catch (ParseException e) {
throw new GDataWrapper.ParseException(e);
} catch (AuthenticationException e) {
throw new GDataWrapper.AuthenticationException(e);
}
}
});
if (!result) {
throw newIOException(trixWrapper, "Failed to retrieve worksheet ID.");
}
return idSaver.get();
}
/**
* Add a row to a worksheet containing the stats for a given track.
*
* @param context The context associated with this request.
* @param trixAuth The GData authorization for the spreadsheet service.
* @param spreadsheetId The spreadsheet to be modified.
* @param worksheetId The worksheet to be modified.
* @param track The track whose stats are to be written.
* @param metricUnits True if metric units are to be used. If false,
* imperial units will be used.
* @throws IOException If an error occurs while updating the worksheet.
*/
public void addTrackRow(Context context, AuthManager trixAuth,
String spreadsheetId, String worksheetId, Track track,
boolean metricUnits) throws IOException {
String worksheetUri = String.format(DOCS_SPREADSHEET_URL_FORMAT,
spreadsheetId, worksheetId);
TripStatistics stats = track.getStatistics();
String distanceUnit = context.getString(metricUnits ?
R.string.kilometer : R.string.mile);
String speedUnit = context.getString(metricUnits ?
R.string.kilometer_per_hour : R.string.mile_per_hour);
String elevationUnit = context.getString(metricUnits ?
R.string.meter : R.string.feet);
// Prepare the Post-Text we are going to send.
DocsTagBuilder tagBuilder = new DocsTagBuilder(metricUnits)
.append("name", track.getName())
.append("description", track.getDescription())
.append("date", String.format("%tc", stats.getStartTime()))
.append("totaltime", StringUtils.formatTimeAlwaysShowingHours(
stats.getTotalTime()))
.append("movingtime", StringUtils.formatTimeAlwaysShowingHours(
stats.getMovingTime()))
.appendLargeUnits("distance", stats.getTotalDistance() / 1000)
.append("distanceunit", distanceUnit)
.appendLargeUnits("averagespeed", stats.getAverageSpeed() * 3.6)
.appendLargeUnits("averagemovingspeed",
stats.getAverageMovingSpeed() * 3.6)
.appendLargeUnits("maxspeed", stats.getMaxSpeed() * 3.6)
.append("speedunit", speedUnit)
.appendSmallUnits("elevationgain", stats.getTotalElevationGain())
.appendSmallUnits("minelevation", stats.getMinElevation())
.appendSmallUnits("maxelevation", stats.getMaxElevation())
.append("elevationunit", elevationUnit);
if (track.getMapId().length() > 0) {
tagBuilder.append("map", String.format("%s?msa=0&msid=%s",
MyTracksConstants.MAPSHOP_BASE_URL, track.getMapId()));
}
String postText = new StringBuilder()
.append("<entry xmlns='http://www.w3.org/2005/Atom' "
+ "xmlns:gsx='http://schemas.google.com/spreadsheets/"
+ "2006/extended'>")
.append(tagBuilder.build())
.append("</entry>")
.toString();
Log.i(MyTracksConstants.TAG,
"Inserting at: " + spreadsheetId + " => " + worksheetUri);
Log.i(MyTracksConstants.TAG, postText);
writeRowData(trixAuth, worksheetUri, postText);
Log.i(MyTracksConstants.TAG, "Post finished.");
}
/**
* Writes spreadsheet row data to the indicated worksheet.
*
* @param trixAuth The GData authorization for the spreadsheet service.
* @param worksheetUri The URI of the worksheet to be altered.
* @param postText The XML tags describing the change to be made.
* @throws IOException Thrown if an error occurs during the write.
*/
protected void writeRowData(AuthManager trixAuth, String worksheetUri,
String postText) throws IOException {
// 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=" + trixAuth.getAuthToken());
conn.setDoOutput(true);
OutputStreamWriter wr = new OutputStreamWriter(conn.getOutputStream());
wr.write(postText);
wr.flush();
// Get the response.
// TODO: Should we parse the response, rather than simply throwing it away?
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();
}
private static IOException newIOException(GDataWrapper wrapper,
String message) {
return new IOException(String.format("%s: %d: %s", message,
wrapper.getErrorType(), wrapper.getErrorMessage()));
}
}
@@ -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.android.apps.mytracks.io.docs;
import com.google.android.apps.mytracks.util.StringUtils;
import com.google.android.apps.mytracks.util.UnitConversions;
import java.text.DecimalFormat;
import java.text.NumberFormat;
/**
* <p>This class builds a string of XML tags used to talk to Docs using GData.
*
* <p>Sample Usage:
*
* <code>
* String tags = new DocsTagBuilder(false)
* .append("tagName", "tagValue")
* .appendLargeUnits("bigTagName", 1.0)
* .build();
* </code>
*
* <p>results in:
*
* <code>
* <gsx:tagName><![CDATA[tagValue]]></gsx:tagName>
* <gsx:bigTagName><![CDATA[1.00]]></gsx:bigTagName>
* </code>
*
* @author Matthew Simmons
*/
class DocsTagBuilder {
// TODO(simmonmt): These formats aren't I18N-compatible. Not everyone uses
// commas for thousands and dots for the decimal point.
private static final NumberFormat LARGE_UNIT_FORMAT =
new DecimalFormat("#,###,###.00");
private static final NumberFormat SMALL_UNIT_FORMAT =
new DecimalFormat("###,###");
protected final boolean metricUnits;
protected final StringBuilder stringBuilder;
/**
* @param metricUnits True if metric units are to be used. If false,
* imperial units will be used.
*/
DocsTagBuilder(boolean metricUnits) {
this.metricUnits = metricUnits;
stringBuilder = new StringBuilder();
}
/** Appends a tag containing a string value */
DocsTagBuilder append(String name, String value) {
appendTag(name, value);
return this;
}
/**
* Appends a tag containing a numeric value. The value will be formatted
* according to the large distance of the specified measurement system (i.e.
* kilometers or miles).
*
* @param name The tag name.
* @param d The value to be formatted, in kilometers.
*/
DocsTagBuilder appendLargeUnits(String name, double d) {
double value = metricUnits ? d : (d * UnitConversions.KM_TO_MI);
appendTag(name, LARGE_UNIT_FORMAT.format(value));
return this;
}
/**
* Appends a tag containing a numeric value. The value will be formatted
* according to the small distance of the specified measurement system (i.e.
* meters or feet).
*
* @param name The tag name.
* @param d The value to be formatted, in meters.
*/
DocsTagBuilder appendSmallUnits(String name, double d) {
double value = metricUnits ? d : (d * UnitConversions.M_TO_FT);
appendTag(name, SMALL_UNIT_FORMAT.format(value));
return this;
}
/** Returns a string containing all tags which have been added. */
String build() {
return stringBuilder.toString();
}
private void appendTag(String name, String value) {
stringBuilder.append("<gsx:")
.append(name)
.append(">")
.append(StringUtils.stringAsCData(value))
.append("</gsx:")
.append(name)
.append(">");
}
}
@@ -17,11 +17,6 @@ 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.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;
@@ -39,9 +34,60 @@ import java.util.concurrent.TimeoutException;
* handling. After a query is run, the wrapper can be queried about the error
* that occurred.
*
* @param C the GData service client
* @author Sandor Dornbush
*/
public class GDataWrapper {
public class GDataWrapper<C> {
public static class AuthenticationException extends Exception {
private Exception exception;
private static final long serialVersionUID = 1L;
public AuthenticationException(Exception caught) {
this.exception = caught;
}
public Exception getException() {
return exception;
}
};
public static class ParseException extends Exception {
private Exception exception;
private static final long serialVersionUID = 1L;
public ParseException(Exception caught) {
this.exception = caught;
}
public Exception getException() {
return exception;
}
};
public static class ConflictDetectedException extends Exception {
private Exception exception;
private static final long serialVersionUID = 1L;
public ConflictDetectedException(Exception caught) {
this.exception = caught;
}
public Exception getException() {
return exception;
}
};
public static class HttpException extends Exception {
private static final long serialVersionUID = 1L;
private int statusCode;
private String statusMessage;
public HttpException(int statusCode, String statusMessage) {
super();
this.statusCode = statusCode;
this.statusMessage = statusMessage;
}
public int getStatusCode() {
return statusCode;
}
public String getStatusMessage() {
return statusMessage;
}
};
/**
* A QueryFunction is passed in when executing a query. The query function of
@@ -49,8 +95,8 @@ public class GDataWrapper {
* 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)
public interface QueryFunction<C> {
public abstract void query(C client)
throws AuthenticationException, IOException, ParseException,
ConflictDetectedException, HttpException;
}
@@ -92,7 +138,7 @@ public class GDataWrapper {
private String errorMessage;
private int errorType;
private GDataServiceClient gdataServiceClient;
private C gdataServiceClient;
private AuthManager auth;
private boolean retryOnAuthFailure;
@@ -103,7 +149,7 @@ public class GDataWrapper {
retryOnAuthFailure = false;
}
public void setClient(GDataServiceClient gdataServiceClient) {
public void setClient(C gdataServiceClient) {
this.gdataServiceClient = gdataServiceClient;
}
@@ -112,7 +158,7 @@ public class GDataWrapper {
return runCommon(function, null);
}
public boolean runQuery(final QueryFunction query) {
public boolean runQuery(final QueryFunction<C> query) {
return runCommon(null, query);
}
@@ -120,7 +166,7 @@ public class GDataWrapper {
* Runs an arbitrary piece of code.
*/
private boolean runCommon(final AuthenticatedFunction function,
final QueryFunction query) {
final QueryFunction<C> query) {
for (int i = 0; i <= AUTH_TOKEN_INVALIDATE_REFRESH_NUM_RETRIES; i++) {
runOne(function, query);
if (errorType == ERROR_NO_ERROR) {
@@ -144,7 +190,7 @@ public class GDataWrapper {
* errorMessage will contain the result/status of the function/query.
*/
private void runOne(final AuthenticatedFunction function,
final QueryFunction query) {
final QueryFunction<C> query) {
try {
if (function != null) {
function.run(this.auth.getAuthToken());
@@ -160,7 +206,7 @@ public class GDataWrapper {
errorMessage = null;
} catch (AuthenticationException e) {
Log.e(MyTracksConstants.TAG, "Exception", e);
Log.e(MyTracksConstants.TAG, "AuthenticationException", e);
errorType = ERROR_AUTH;
errorMessage = e.getMessage();
} catch (HttpException e) {
@@ -243,6 +289,10 @@ public class GDataWrapper {
this.auth = auth;
}
public AuthManager getAuthManager() {
return auth;
}
public void setRetryOnAuthFailure(boolean retry) {
retryOnAuthFailure = retry;
}
@@ -13,7 +13,7 @@
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mymaps;
package com.google.android.apps.mytracks.util;
import com.google.android.maps.GeoPoint;
@@ -61,4 +61,4 @@ public class GeoRect {
}
return false;
}
}
}
@@ -22,11 +22,11 @@ import com.google.android.apps.mytracks.stats.TripStatistics;
import com.google.android.maps.GeoPoint;
import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.Signature;
import android.location.Location;
import android.os.StrictMode;
import android.util.Log;
import java.util.ArrayList;
@@ -267,12 +267,28 @@ public class MyTracksUtils {
} catch (NameNotFoundException e) {
Log.e(MyTracksConstants.TAG, "Unable to get signatures", e);
}
return false;
}
/**
* This is a utility class w/ only static memebers.
* Get the My Tracks version from the manifest.
*
* @return the version, or an empty string in case of failure.
*/
public static String getMyTracksVersion(Context context) {
try {
PackageInfo pi = context.getPackageManager().getPackageInfo(
"com.google.android.maps.mytracks",
PackageManager.GET_META_DATA);
return pi.versionName;
} catch (NameNotFoundException e) {
Log.w(MyTracksConstants.TAG, "Failed to get version info.", e);
return "";
}
}
/**
* This is a utility class w/ only static members.
*/
protected MyTracksUtils() {
}
@@ -316,7 +316,7 @@ public class StringUtils implements DescriptionGenerator {
+ "<img border=\"0\" src=\"%s\"/>",
// Line 1
context.getString(R.string.my_map_link),
context.getString(R.string.fusiontable_link),
// Line 2
context.getString(R.string.total_distance_label),