diff --git a/.hgignore b/.hgignore
index 1bbe85f92..3bf9706a8 100644
--- a/.hgignore
+++ b/.hgignore
@@ -5,4 +5,3 @@ syntax:glob
*/proguard/**
.metadata
MyTracksTest/annotations.log
-MyTracks/src/com/google/android/apps/mytracks/io/mymaps
diff --git a/MyTracks/.classpath b/MyTracks/.classpath
index 72de4ae42..636d59be6 100644
--- a/MyTracks/.classpath
+++ b/MyTracks/.classpath
@@ -8,7 +8,6 @@
-
diff --git a/MyTracks/libs/libmymaps.jar b/MyTracks/libs/libmymaps.jar
deleted file mode 100644
index 72659cdc6..000000000
Binary files a/MyTracks/libs/libmymaps.jar and /dev/null differ
diff --git a/MyTracks/src/com/google/android/apps/mytracks/MyMapsList.java b/MyTracks/src/com/google/android/apps/mytracks/MyMapsList.java
index 949c90026..7b74ac606 100644
--- a/MyTracks/src/com/google/android/apps/mytracks/MyMapsList.java
+++ b/MyTracks/src/com/google/android/apps/mytracks/MyMapsList.java
@@ -18,9 +18,8 @@ package com.google.android.apps.mytracks;
import com.google.android.accounts.Account;
import com.google.android.apps.mytracks.io.AuthManager;
import com.google.android.apps.mytracks.io.AuthManagerFactory;
-import com.google.android.apps.mytracks.io.MyMapsFactory;
import com.google.android.apps.mytracks.io.mymaps.MapsFacade;
-import com.google.android.apps.mytracks.io.mymaps.MapsService;
+import com.google.android.apps.mytracks.io.mymaps.MyMapsConstants;
import com.google.android.maps.mytracks.R;
import android.app.Activity;
@@ -84,7 +83,7 @@ public class MyMapsList extends Activity implements MapsFacade.MapsListCallback
super.onCreate(icicle);
auth = AuthManagerFactory.getAuthManager(this, GET_LOGIN, null, true,
- MapsService.getServiceName());
+ MyMapsConstants.SERVICE_NAME);
setContentView(R.layout.list);
@@ -125,7 +124,7 @@ public class MyMapsList extends Activity implements MapsFacade.MapsListCallback
auth.doLogin(new Runnable() {
public void run() {
// Runs in UI thread.
- mapsClient = MyMapsFactory.newMapsClient(MyMapsList.this, auth);
+ mapsClient = new MapsFacade(MyMapsList.this, auth);
startLookup();
}
@@ -200,7 +199,7 @@ public class MyMapsList extends Activity implements MapsFacade.MapsListCallback
shareIntent.putExtra(Intent.EXTRA_TEXT, String.format(
getText(R.string.share_map_body_format).toString(),
listItem[1],
- MapsService.buildMapUrl(listItem[0])));
+ MapsFacade.buildMapUrl(listItem[0])));
startActivity(Intent.createChooser(shareIntent,
getText(R.string.share_map).toString()));
return true;
diff --git a/MyTracks/src/com/google/android/apps/mytracks/MyTracks.java b/MyTracks/src/com/google/android/apps/mytracks/MyTracks.java
index f964f2fc0..64db86daf 100644
--- a/MyTracks/src/com/google/android/apps/mytracks/MyTracks.java
+++ b/MyTracks/src/com/google/android/apps/mytracks/MyTracks.java
@@ -35,7 +35,8 @@ 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.TrackWriterFactory.TrackFileFormat;
-import com.google.android.apps.mytracks.io.mymaps.MapsService;
+import com.google.android.apps.mytracks.io.mymaps.MapsFacade;
+import com.google.android.apps.mytracks.io.mymaps.MyMapsConstants;
import com.google.android.apps.mytracks.io.sendtogoogle.ResultDialogFactory;
import com.google.android.apps.mytracks.io.sendtogoogle.SendDialog;
import com.google.android.apps.mytracks.io.sendtogoogle.SendResult;
@@ -606,7 +607,7 @@ public class MyTracks extends TabActivity implements OnTouchListener,
Track selectedTrack = providerUtils.getTrack(selectedTrackId);
if (selectedTrack != null) {
if (!TextUtils.isEmpty(selectedTrack.getMapId())) {
- shareLinkToMap(MapsService.buildMapUrl(selectedTrack.getMapId()));
+ shareLinkToMap(MapsFacade.buildMapUrl(selectedTrack.getMapId()));
} else if (!TextUtils.isEmpty(selectedTrack.getTableId())) {
shareLinkToMap(getFusionTablesUrl(selectedTrackId));
} else {
@@ -1028,7 +1029,7 @@ public class MyTracks extends TabActivity implements OnTouchListener,
setProgressMessage(
R.string.progress_message_authenticating_mymaps);
authenticate(results, MyTracksConstants.AUTHENTICATE_TO_MY_MAPS,
- MapsService.getServiceName());
+ MyMapsConstants.SERVICE_NAME);
// AUTHENTICATE_TO_MY_MAPS callback calls sendToGoogleMaps
}
@@ -1225,7 +1226,7 @@ public class MyTracks extends TabActivity implements OnTouchListener,
String url = null;
if (sentToMyMaps && sendToMyMapsSuccess) {
// Prefer a link to My Maps
- url = MapsService.buildMapUrl(sendToMyMapsMapId);
+ url = MapsFacade.buildMapUrl(sendToMyMapsMapId);
} else if (sentToFusionTables && sendToFusionTablesSuccess) {
// Otherwise try using the link to fusion tables
url = getFusionTablesUrl(sendToTrackId);
diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/MyMapsFactory.java b/MyTracks/src/com/google/android/apps/mytracks/io/MyMapsFactory.java
deleted file mode 100644
index 8d17e1667..000000000
--- a/MyTracks/src/com/google/android/apps/mytracks/io/MyMapsFactory.java
+++ /dev/null
@@ -1,68 +0,0 @@
-package com.google.android.apps.mytracks.io;
-
-import com.google.android.apps.mytracks.io.gdata.GDataClientFactory;
-import com.google.android.apps.mytracks.io.mymaps.MapsFacade;
-import com.google.android.apps.mytracks.io.mymaps.MapsService;
-import com.google.android.apps.mytracks.io.mymaps.MapsStringsProvider;
-import com.google.android.apps.mytracks.io.mymaps.MapsFacade.AuthenticationRefresher;
-import com.google.android.maps.mytracks.R;
-
-import android.app.Activity;
-import android.content.Context;
-
-/**
- * Factory to easily instantiate a {@link MapsFacade}.
- *
- * @author Rodrigo Damazio
- */
-public class MyMapsFactory {
-
- /**
- * Simple provider for reading strings from resources.
- */
- private static class MyMapsStringProvider implements MapsStringsProvider {
- private final Context context;
-
- public MyMapsStringProvider(Context context) {
- this.context = context;
- }
-
- @Override
- public String getNewMapDescription() {
- return context.getString(R.string.new_map_description);
- }
-
- @Override
- public String getStart() {
- return context.getString(R.string.start);
- }
-
- @Override
- public String getEnd() {
- return context.getString(R.string.end);
- }
- }
-
- /**
- * Creates a new Maps faŤade interface instance.
- *
- * @param context current context
- * @param auth a valid authentication manager
- * @return the faŤade
- */
- public static MapsFacade newMapsClient(Activity context, final AuthManager auth) {
- MapsFacade client = MapsService.newClient(context,
- GDataClientFactory.getGDataClient(context),
- new MyMapsStringProvider(context),
- auth.getAuthToken());
- client.setAuthenticationRefresher(new AuthenticationRefresher() {
- @Override
- public void invalidateAndRefresh(Runnable done) {
- auth.invalidateAndRefresh(done);
- }
- });
- return client;
- }
-
- private MyMapsFactory() {}
-}
diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/SendToMyMaps.java b/MyTracks/src/com/google/android/apps/mytracks/io/SendToMyMaps.java
index 542606426..3891b0322 100644
--- a/MyTracks/src/com/google/android/apps/mytracks/io/SendToMyMaps.java
+++ b/MyTracks/src/com/google/android/apps/mytracks/io/SendToMyMaps.java
@@ -22,7 +22,6 @@ 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.mymaps.MapsFacade;
-import com.google.android.apps.mytracks.io.mymaps.MapsFacade.WaypointData;
import com.google.android.apps.mytracks.stats.DoubleBuffer;
import com.google.android.apps.mytracks.stats.TripStatistics;
import com.google.android.apps.mytracks.util.MyTracksUtils;
@@ -111,7 +110,7 @@ public class SendToMyMaps implements Runnable {
track.setDescription("
" + track.getDescription() + "
"
+ stringUtils.generateTrackDescription(track, null, null) + "
");
- mapsClient = MyMapsFactory.newMapsClient(context, auth);
+ mapsClient = new MapsFacade(context, auth);
// Create a new map if necessary:
boolean isNewMap = mapId.equals(NEW_MAP_ID);
@@ -148,11 +147,10 @@ public class SendToMyMaps implements Runnable {
if (c.getCount() > 1 && c.moveToFirst()) {
// This will skip the 1st waypoint (it carries the stats for the
// last segment).
- ArrayList waypoints = new ArrayList(c.getCount() - 1);
+ ArrayList waypoints = new ArrayList(c.getCount() - 1);
while (c.moveToNext()) {
Waypoint wpt = providerUtils.createWaypoint(c);
- waypoints.add(new WaypointData(
- wpt.getName(), wpt.getDescription(), wpt.getIcon(), wpt.getLocation()));
+ waypoints.add(wpt);
}
success = mapsClient.uploadWaypoints(mapId, waypoints);
diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/mymaps/GeoRect.java b/MyTracks/src/com/google/android/apps/mytracks/io/mymaps/GeoRect.java
new file mode 100644
index 000000000..a990e95bc
--- /dev/null
+++ b/MyTracks/src/com/google/android/apps/mytracks/io/mymaps/GeoRect.java
@@ -0,0 +1,50 @@
+// Copyright 2009 Google Inc. All Rights Reserved.
+package com.google.android.apps.mytracks.io.mymaps;
+
+import com.google.android.maps.GeoPoint;
+
+/**
+ * A rectangle in geographical space.
+ */
+class GeoRect {
+ public int top;
+ public int left;
+ public int bottom;
+ public int right;
+
+ public GeoRect() {
+ top = 0;
+ left = 0;
+ bottom = 0;
+ right = 0;
+ }
+
+ public GeoRect(GeoPoint center, int latSpan, int longSpan) {
+ top = center.getLatitudeE6() - latSpan / 2;
+ left = center.getLongitudeE6() - longSpan / 2;
+ bottom = center.getLatitudeE6() + latSpan / 2;
+ right = center.getLongitudeE6() + longSpan / 2;
+ }
+
+ public GeoPoint getCenter() {
+ return new GeoPoint(top / 2 + bottom / 2, left / 2 + right / 2);
+ }
+
+ public int getLatSpan() {
+ return bottom - top;
+ }
+
+ public int getLongSpan() {
+ return right - left;
+ }
+
+ public boolean contains(GeoPoint geoPoint) {
+ if (geoPoint.getLatitudeE6() >= top
+ && geoPoint.getLatitudeE6() <= bottom
+ && geoPoint.getLongitudeE6() >= left
+ && geoPoint.getLongitudeE6() <= right) {
+ return true;
+ }
+ return false;
+ }
+}
diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/mymaps/MapFeatureEntry.java b/MyTracks/src/com/google/android/apps/mytracks/io/mymaps/MapFeatureEntry.java
new file mode 100644
index 000000000..e667c99b4
--- /dev/null
+++ b/MyTracks/src/com/google/android/apps/mytracks/io/mymaps/MapFeatureEntry.java
@@ -0,0 +1,36 @@
+// Copyright 2010 Google Inc. All Rights Reserved.
+package com.google.android.apps.mytracks.io.mymaps;
+
+import com.google.wireless.gdata.data.Entry;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * GData entry for a map feature.
+ */
+class MapFeatureEntry extends Entry {
+
+ private String mPrivacy = null;
+ private Map mAttributes = new HashMap();
+
+ public void setPrivacy(String privacy) {
+ mPrivacy = privacy;
+ }
+
+ public String getPrivacy() {
+ return mPrivacy;
+ }
+
+ public void setAttribute(String name, String value) {
+ mAttributes.put(name, value);
+ }
+
+ public void removeAttribute(String name) {
+ mAttributes.remove(name);
+ }
+
+ public Map getAllAttributes() {
+ return mAttributes;
+ }
+}
diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/mymaps/MapsClient.java b/MyTracks/src/com/google/android/apps/mytracks/io/mymaps/MapsClient.java
new file mode 100644
index 000000000..7d1194b8c
--- /dev/null
+++ b/MyTracks/src/com/google/android/apps/mytracks/io/mymaps/MapsClient.java
@@ -0,0 +1,101 @@
+// Copyright 2010 Google Inc. All Rights Reserved.
+package com.google.android.apps.mytracks.io.mymaps;
+
+import com.google.wireless.gdata.client.GDataClient;
+import com.google.wireless.gdata.client.GDataParserFactory;
+import com.google.wireless.gdata.client.GDataServiceClient;
+
+import android.util.Log;
+
+/**
+ * Client to talk to Google Maps via GData.
+ */
+class MapsClient extends GDataServiceClient {
+ private static final boolean DEBUG = false;
+ public static final boolean LOG_COMMUNICATION = false;
+
+ private static final String MAPS_BASE_FEED_URL =
+ "http://maps.google.com/maps/feeds/";
+ private static final String MAPS_MAP_FEED_PATH = "maps/default/full";
+ private static final String MAPS_FEATURE_FEED_PATH_BEFORE_MAPID = "features/";
+ private static final String MAPS_FEATURE_FEED_PATH_AFTER_MAPID = "/full";
+ private static final String MAPS_VERSION_FEED_PATH_FORMAT =
+ "%smaps/%s/versions/%s/full/%s";
+
+ private static final String MAP_ENTRY_ID_BEFORE_USER_ID = "maps/feeds/maps/";
+ private static final String MAP_ENTRY_ID_BETWEEN_USER_ID_AND_MAP_ID = "/";
+ private static final String V2_ONLY_PARAM = "?v=2.0";
+
+ public MapsClient(GDataClient dataClient,
+ GDataParserFactory dataParserFactory) {
+ super(dataClient, dataParserFactory);
+ }
+
+ @Override
+ public String getServiceName() {
+ return MyMapsConstants.SERVICE_NAME;
+ }
+
+ public static String buildMapUrl(String mapId) {
+ return MyMapsConstants.MAPSHOP_BASE_URL + "?msa=0&msid=" + mapId;
+ }
+
+ public static String getMapsFeed() {
+ if (DEBUG) {
+ Log.d("Maps Client", "Requesting map feed:");
+ }
+ return MAPS_BASE_FEED_URL + MAPS_MAP_FEED_PATH + V2_ONLY_PARAM;
+ }
+
+ public static String getFeaturesFeed(String mapid) {
+ StringBuilder feed = new StringBuilder();
+ feed.append(MAPS_BASE_FEED_URL);
+ feed.append(MAPS_FEATURE_FEED_PATH_BEFORE_MAPID);
+ feed.append(mapid);
+ feed.append(MAPS_FEATURE_FEED_PATH_AFTER_MAPID);
+ feed.append(V2_ONLY_PARAM);
+ return feed.toString();
+ }
+
+ public static String getMapIdFromMapEntryId(String entryId) {
+ String userId = null;
+ String mapId = null;
+ if (DEBUG) {
+ Log.d("Maps GData Client", "Getting mapid from entry id: " + entryId);
+ }
+ int userIdStart =
+ entryId.indexOf(MAP_ENTRY_ID_BEFORE_USER_ID)
+ + MAP_ENTRY_ID_BEFORE_USER_ID.length();
+ int userIdEnd =
+ entryId.indexOf(MAP_ENTRY_ID_BETWEEN_USER_ID_AND_MAP_ID, userIdStart);
+ if (userIdStart >= 0 && userIdEnd < entryId.length()
+ && userIdStart <= userIdEnd) {
+ userId = entryId.substring(userIdStart, userIdEnd);
+ }
+ int mapIdStart =
+ entryId.indexOf(MAP_ENTRY_ID_BETWEEN_USER_ID_AND_MAP_ID, userIdEnd)
+ + MAP_ENTRY_ID_BETWEEN_USER_ID_AND_MAP_ID.length();
+ if (mapIdStart >= 0 && mapIdStart < entryId.length()) {
+ mapId = entryId.substring(mapIdStart);
+ }
+ if (userId == null) {
+ userId = "";
+ }
+ if (mapId == null) {
+ mapId = "";
+ }
+ if (DEBUG) {
+ Log.d("Maps GData Client", "Got user id: " + userId);
+ Log.d("Maps GData Client", "Got map id: " + mapId);
+ }
+ return userId + "." + mapId;
+ }
+
+ public static String getVersionFeed(String versionUserId,
+ String versionClient, String currentVersion) {
+ return String.format(MAPS_VERSION_FEED_PATH_FORMAT,
+ MAPS_BASE_FEED_URL, versionUserId,
+ versionClient, currentVersion);
+ }
+
+}
diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/mymaps/MapsFacade.java b/MyTracks/src/com/google/android/apps/mytracks/io/mymaps/MapsFacade.java
new file mode 100644
index 000000000..86a92ec5c
--- /dev/null
+++ b/MyTracks/src/com/google/android/apps/mytracks/io/mymaps/MapsFacade.java
@@ -0,0 +1,372 @@
+// Copyright 2011 Google Inc. All Rights Reserved.
+package com.google.android.apps.mytracks.io.mymaps;
+
+import com.google.android.apps.mytracks.content.Waypoint;
+import com.google.android.apps.mytracks.io.AuthManager;
+import com.google.android.maps.GeoPoint;
+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.parser.GDataParser;
+import com.google.wireless.gdata.parser.ParseException;
+
+import android.content.Context;
+import android.location.Location;
+import android.text.TextUtils;
+import android.util.Log;
+
+import java.io.IOException;
+import java.util.Collection;
+
+import org.xmlpull.v1.XmlPullParserException;
+
+/**
+ * Single interface which abstracts all access to the Google Maps service.
+ *
+ * @author Rodrigo Damazio
+ */
+public class MapsFacade {
+
+ /**
+ * Interface for receiving data back from getMapsList.
+ * All calls to the interface will happen before getMapsList returns.
+ */
+ public interface MapsListCallback {
+ void onReceivedMapListing(String mapId, String title, String description,
+ boolean isPublic);
+ }
+
+ private static final String END_ICON_URL =
+ "http://maps.google.com/mapfiles/ms/micons/red-dot.png";
+ private static final String START_ICON_URL =
+ "http://maps.google.com/mapfiles/ms/micons/green-dot.png";
+
+ private final Context context;
+ private final MyMapsGDataWrapper wrapper;
+ private final MyMapsGDataConverter gdataConverter;
+ private final String authToken;
+
+ public MapsFacade(Context context, AuthManager auth) {
+ this.context = context;
+ this.authToken = auth.getAuthToken();
+
+ wrapper = new MyMapsGDataWrapper(context, auth);
+ wrapper.setRetryOnAuthFailure(true);
+
+ try {
+ gdataConverter = new MyMapsGDataConverter();
+ } catch (XmlPullParserException e) {
+ throw new IllegalStateException("Unable to create maps data converter", e);
+ }
+ }
+
+ public static String buildMapUrl(String mapId) {
+ return MapsClient.buildMapUrl(mapId);
+ }
+
+ /**
+ * Returns a list of all maps for the current user.
+ *
+ * @param callback callback to call for each map returned
+ * @return true on success, false otherwise
+ */
+ public boolean getMapsList(final MapsListCallback callback) {
+ return wrapper.runQuery(new MyMapsGDataWrapper.QueryFunction() {
+ @Override
+ public void query(MapsClient client) throws IOException, Exception {
+ GDataParser listParser = client.getParserForFeed(
+ MapFeatureEntry.class, MapsClient.getMapsFeed(), authToken);
+ listParser.init();
+ while (listParser.hasMoreData()) {
+ MapFeatureEntry entry =
+ (MapFeatureEntry) listParser.readNextEntry(null);
+ MyMapsMapMetadata metadata =
+ MyMapsGDataConverter.getMapMetadataForEntry(entry);
+ String mapId = MyMapsGDataConverter.getMapidForEntry(entry);
+
+ callback.onReceivedMapListing(
+ mapId, metadata.getTitle(), metadata.getDescription(), metadata.getSearchable());
+ }
+ listParser.close();
+ listParser = null;
+ }
+ });
+ }
+
+ /**
+ * Creates a new map for the current user.
+ *
+ * @param title title of the map
+ * @param category category of the map
+ * @param description description for the map
+ * @param isPublic whether the map should be public
+ * @param mapIdBuilder builder to append the map ID to
+ * @return true on success, false otherwise
+ */
+ public boolean createNewMap(
+ final String title, final String category, final String description,
+ final boolean isPublic, final StringBuilder mapIdBuilder) {
+ if (mapIdBuilder.length() > 0) {
+ throw new IllegalArgumentException("mapIdBuilder should be empty");
+ }
+
+ return wrapper.runQuery(new MyMapsGDataWrapper.QueryFunction() {
+ @Override
+ public void query(MapsClient client) throws IOException, Exception {
+ Log.d(MyMapsConstants.TAG, "Creating a new map.");
+ String mapFeed = MapsClient.getMapsFeed();
+ Log.d(MyMapsConstants.TAG, "Map feed is " + mapFeed);
+ MyMapsMapMetadata metaData = new MyMapsMapMetadata();
+ metaData.setTitle(title);
+ metaData.setDescription(description + " - "
+ + category + " - " + context.getString(R.string.new_map_description));
+ metaData.setSearchable(isPublic);
+ Entry entry = MyMapsGDataConverter.getMapEntryForMetadata(metaData);
+ Log.d(MyMapsConstants.TAG, "Title: " + entry.getTitle());
+ Entry map = client.createEntry(mapFeed, authToken, entry);
+
+ String mapId = MapsClient.getMapIdFromMapEntryId(map.getId());
+ mapIdBuilder.append(mapId);
+ Log.d(MyMapsConstants.TAG, "New map id is: " + mapId);
+ }
+ });
+ }
+
+ /**
+ * Uploads a single start or end marker to the given map.
+ *
+ * @param mapId ID of the map to upload to
+ * @param trackName name of the track being started/ended
+ * @param trackDescription description of the track being started/ended
+ * @param location the location of the marker
+ * @param isStart true to add a start marker, false to add an end marker
+ * @return true on success, false otherwise
+ */
+ public boolean uploadMarker(final String mapId, final String trackName,
+ final String trackDescription, final Location loc, final boolean isStart) {
+ return wrapper.runQuery(new MyMapsGDataWrapper.QueryFunction() {
+ @Override
+ public void query(MapsClient client)
+ throws IOException, Exception {
+ String featureFeed = MapsClient.getFeaturesFeed(mapId);
+ GeoPoint geoPoint = getGeoPoint(loc);
+ insertMarker(client, featureFeed, trackName, trackDescription, geoPoint, isStart);
+ }
+ });
+ }
+
+ /**
+ * Inserts a place mark. Second try if 1st try fails. Will throw exception on
+ * 2nd failure.
+ */
+ private void insertMarker(MapsClient client,
+ String featureFeed,
+ String trackName, String trackDescription,
+ GeoPoint geoPoint,
+ boolean isStart) throws IOException, Exception {
+ MyMapsFeature feature =
+ buildMyMapsPlacemarkFeature(trackName, trackDescription, geoPoint, isStart);
+ Entry entry = gdataConverter.getEntryForFeature(feature);
+ Log.d(MyMapsConstants.TAG, "SendToMyMaps: Creating placemark "
+ + entry.getTitle());
+ try {
+ client.createEntry(featureFeed, authToken, entry);
+ Log.d(MyMapsConstants.TAG, "SendToMyMaps: createEntry success!");
+ } catch (IOException e) {
+ Log.w(MyMapsConstants.TAG,
+ "SendToMyMaps: createEntry 1st try failed. Trying again.");
+ // Retry once (often IOException is thrown on a timeout):
+ client.createEntry(featureFeed, authToken, entry);
+ Log.d(MyMapsConstants.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 MyMapsFeature buildMyMapsPlacemarkFeature(
+ String trackName, String trackDescription,
+ GeoPoint geoPoint, boolean isStart) {
+ String iconUrl;
+ if (isStart) {
+ iconUrl = START_ICON_URL;
+ } else {
+ iconUrl = END_ICON_URL;
+ }
+ String title = trackName + " "
+ + (isStart ? context.getString(R.string.start)
+ : context.getString(R.string.end));
+ String description = isStart ? "" : trackDescription;
+ return buildMyMapsPlacemarkFeature(title, description, iconUrl, geoPoint);
+ }
+
+ /**
+ * Builds a MyMapsFeature from a track.
+ *
+ * @param wpt the waypoint
+ * @return a MyMapsFeature
+ */
+ private static MyMapsFeature buildMyMapsPlacemarkFeature(
+ String title, String description, String iconUrl, GeoPoint geoPoint) {
+ MyMapsFeature myMapsFeature = new MyMapsFeature();
+ myMapsFeature.generateAndroidId();
+ myMapsFeature.setType(MyMapsFeature.MARKER);
+ myMapsFeature.setIconUrl(iconUrl);
+ myMapsFeature.setDescription(description);
+ myMapsFeature.addPoint(geoPoint);
+ if (TextUtils.isEmpty(title)) {
+ // Features must have a name (otherwise GData upload may fail):
+ myMapsFeature.setTitle("-");
+ } else {
+ myMapsFeature.setTitle(title);
+ }
+ myMapsFeature.setDescription(description.replaceAll("\n", "
"));
+ return myMapsFeature;
+ }
+
+ /**
+ * Uploads a series of waypoints to the given map.
+ *
+ * @param mapId ID of the map to upload to
+ * @param waypoints the waypoints to upload
+ * @return true on success, false otherwise
+ */
+ public boolean uploadWaypoints(
+ final String mapId, final Iterable waypoints) {
+ return wrapper.runQuery(new MyMapsGDataWrapper.QueryFunction() {
+ public void query(MapsClient client) {
+ // TODO(rdamazio): 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.
+ String featureFeed = MapsClient.getFeaturesFeed(mapId);
+
+ try {
+ for (Waypoint waypoint : waypoints) {
+ MyMapsFeature feature = buildMyMapsPlacemarkFeature(
+ waypoint.getName(), waypoint.getDescription(), waypoint.getIcon(),
+ getGeoPoint(waypoint.getLocation()));
+ Entry entry = gdataConverter.getEntryForFeature(feature);
+
+ Log.d(MyMapsConstants.TAG,
+ "SendToMyMaps: Creating waypoint.");
+ try {
+ client.createEntry(featureFeed, authToken, entry);
+ Log.d(MyMapsConstants.TAG,
+ "SendToMyMaps: createEntry success!");
+ } catch (IOException e) {
+ Log.w(MyMapsConstants.TAG,
+ "SendToMyMaps: createEntry 1st try failed. Retrying.");
+
+ // Retry once (often IOException is thrown on a timeout):
+ client.createEntry(featureFeed, authToken, entry);
+ Log.d(MyMapsConstants.TAG,
+ "SendToMyMaps: createEntry success on 2nd try!");
+ }
+ }
+ } catch (ParseException e) {
+ Log.w(MyMapsConstants.TAG, "ParseException caught.", e);
+ } catch (HttpException e) {
+ Log.w(MyMapsConstants.TAG, "HttpException caught.", e);
+ } catch (IOException e) {
+ Log.w(MyMapsConstants.TAG, "IOException caught.", e);
+ }
+ }
+ });
+ }
+
+ /**
+ * Uploads a series of points to the given map.
+ *
+ * @param mapId ID of the map to upload to
+ * @param trackName the name of the track
+ * @param locations the locations to upload
+ * @return true on success, false otherwise
+ */
+ public boolean uploadTrackPoints(
+ final String mapId, final String trackName, final Collection locations) {
+ return wrapper.runQuery(new MyMapsGDataWrapper.QueryFunction() {
+ @Override
+ public void query(MapsClient client)
+ throws IOException, Exception {
+ String featureFeed = MapsClient.getFeaturesFeed(mapId);
+ Log.d(MyMapsConstants.TAG, "Feature feed url: " + featureFeed);
+ uploadTrackPoints(client, featureFeed, trackName, locations);
+ }
+ });
+ }
+
+ private boolean uploadTrackPoints(
+ MapsClient client,
+ String featureFeed,
+ String trackName, Collection locations)
+ throws IOException, Exception {
+ Entry entry = null;
+ int numLocations = locations.size();
+ if (numLocations < 2) {
+ // Need at least two points for a polyline:
+ Log.w(MyMapsConstants.TAG, "Not uploading too few points");
+ return true;
+ }
+
+ // Put the line:
+ entry = gdataConverter.getEntryForFeature(
+ buildMyMapsLineFeature(trackName, locations));
+ Log.d(MyMapsConstants.TAG,
+ "SendToMyMaps: Creating line " + entry.getTitle());
+ try {
+ client.createEntry(featureFeed, authToken, entry);
+ Log.d(MyMapsConstants.TAG, "SendToMyMaps: createEntry success!");
+ } catch (IOException e) {
+ Log.w(MyMapsConstants.TAG,
+ "SendToMyMaps: createEntry 1st try failed. Trying again.");
+ // Retry once (often IOException is thrown on a timeout):
+ client.createEntry(featureFeed, authToken, entry);
+ Log.d(MyMapsConstants.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(String trackName,
+ Iterable locations) {
+ MyMapsFeature myMapsFeature = new MyMapsFeature();
+ myMapsFeature.generateAndroidId();
+ myMapsFeature.setType(MyMapsFeature.LINE);
+ if (TextUtils.isEmpty(trackName)) {
+ // Features must have a name (otherwise GData upload may fail):
+ myMapsFeature.setTitle("-");
+ } else {
+ myMapsFeature.setTitle(trackName);
+ }
+ myMapsFeature.setColor(0x80FF0000);
+ for (Location loc : locations) {
+ myMapsFeature.addPoint(getGeoPoint(loc));
+ }
+ return myMapsFeature;
+ }
+
+ /**
+ * Cleans up after a series of uploads.
+ * This closes the connection to Maps and resets retry counters.
+ */
+ public void cleanUp() {
+ wrapper.cleanUp();
+ }
+
+ private static GeoPoint getGeoPoint(Location location) {
+ return new GeoPoint((int) (location.getLatitude() * 1E6),
+ (int) (location.getLongitude() * 1E6));
+ }
+}
diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/mymaps/MyMapsConstants.java b/MyTracks/src/com/google/android/apps/mytracks/io/mymaps/MyMapsConstants.java
new file mode 100644
index 000000000..77cb3a330
--- /dev/null
+++ b/MyTracks/src/com/google/android/apps/mytracks/io/mymaps/MyMapsConstants.java
@@ -0,0 +1,17 @@
+// Copyright 2009 Google Inc. All Rights Reserved.
+package com.google.android.apps.mytracks.io.mymaps;
+
+/**
+ * Constants for My Maps.
+ */
+public class MyMapsConstants {
+ static final String TAG = "MapsApi";
+ static final String MAPSHOP_BASE_URL =
+ "http://maps.google.com/maps/ms";
+ public static final String SERVICE_NAME = "local";
+
+ /**
+ * Private constructor to prevent instantiation.
+ */
+ private MyMapsConstants() { }
+}
diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/mymaps/MyMapsFeature.java b/MyTracks/src/com/google/android/apps/mytracks/io/mymaps/MyMapsFeature.java
new file mode 100644
index 000000000..2cd24d641
--- /dev/null
+++ b/MyTracks/src/com/google/android/apps/mytracks/io/mymaps/MyMapsFeature.java
@@ -0,0 +1,233 @@
+// Copyright 2009 Google Inc. All Rights Reserved.
+package com.google.android.apps.mytracks.io.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.
+ */
+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 latitudeE6 = new Vector();
+
+ /**
+ * The longitudes of the points of this feature in order, specified in
+ * millionths of a degree east.
+ */
+ private final Vector longitudeE6 = new Vector();
+
+ /** 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);
+ }
+}
diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/mymaps/MyMapsFeatureMetadata.java b/MyTracks/src/com/google/android/apps/mytracks/io/mymaps/MyMapsFeatureMetadata.java
new file mode 100644
index 000000000..7e94d24b9
--- /dev/null
+++ b/MyTracks/src/com/google/android/apps/mytracks/io/mymaps/MyMapsFeatureMetadata.java
@@ -0,0 +1,87 @@
+// Copyright 2009 Google Inc. All Rights Reserved.
+package com.google.android.apps.mytracks.io.mymaps;
+
+/**
+ * Metadata about a maps feature.
+ */
+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);
+ }
+}
diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/mymaps/MyMapsGDataConverter.java b/MyTracks/src/com/google/android/apps/mytracks/io/mymaps/MyMapsGDataConverter.java
new file mode 100644
index 000000000..f2d7bad7e
--- /dev/null
+++ b/MyTracks/src/com/google/android/apps/mytracks/io/mymaps/MyMapsGDataConverter.java
@@ -0,0 +1,183 @@
+// Copyright 2009 Google Inc. All Rights Reserved.
+package com.google.android.apps.mytracks.io.mymaps;
+
+import com.google.wireless.gdata.data.Entry;
+import com.google.wireless.gdata.data.StringUtils;
+
+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.
+ */
+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));
+ }
+
+ String editUri = entry.getEditUri();
+ if (editUri != null) {
+ metadata.setGDataEditUri(editUri);
+ }
+
+ 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;
+ }
+}
diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/mymaps/MyMapsGDataWrapper.java b/MyTracks/src/com/google/android/apps/mytracks/io/mymaps/MyMapsGDataWrapper.java
new file mode 100644
index 000000000..3e5cb9c3f
--- /dev/null
+++ b/MyTracks/src/com/google/android/apps/mytracks/io/mymaps/MyMapsGDataWrapper.java
@@ -0,0 +1,169 @@
+// Copyright 2010 Google Inc. All Rights Reserved.
+package com.google.android.apps.mytracks.io.mymaps;
+
+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.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.
+ */
+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.
+ */
+ 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 AuthManager auth;
+ private final MapsClient client;
+
+ private String errorMessage;
+ private int errorType;
+ private boolean retryOnAuthFailure;
+ private int retriesPending;
+ private boolean cleanupCalled;
+
+ public MyMapsGDataWrapper(Context context, AuthManager auth) {
+ androidGdataClient = GDataClientFactory.getGDataClient(context);
+ this.auth = auth;
+ client =
+ new MapsClient(androidGdataClient, new XmlMapsGDataParserFactory(
+ new AndroidXmlParserFactory()));
+ errorType = ERROR_NO_ERROR;
+ errorMessage = 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(MyMapsConstants.TAG, "Exception", e);
+ errorType = ERROR_AUTH;
+ errorMessage = e.getMessage();
+ } catch (HttpException e) {
+ Log.e(MyMapsConstants.TAG, "HttpException", e);
+ errorMessage = e.getMessage();
+ if (errorMessage != null && errorMessage.contains("401")) {
+ errorType = ERROR_AUTH;
+ } else {
+ errorType = ERROR_CONNECTION;
+ }
+ } catch (IOException e) {
+ Log.e(MyMapsConstants.TAG, "Exception", e);
+ errorMessage = e.getMessage();
+ if (errorMessage != null && errorMessage.contains("503")) {
+ errorType = ERROR_INTERNAL;
+ } else {
+ errorType = ERROR_CONNECTION;
+ }
+ } catch (ParseException e) {
+ Log.e(MyMapsConstants.TAG, "Exception", e);
+ errorType = ERROR_LOCAL;
+ errorMessage = e.getMessage();
+ } catch (ConflictDetectedException e) {
+ Log.e(MyMapsConstants.TAG, "Exception", e);
+ errorType = ERROR_CONFLICT;
+ errorMessage = e.getMessage();
+ } catch (Exception e) {
+ Log.e(MyMapsConstants.TAG, "Exception", e);
+ errorType = ERROR_UNKNOWN;
+ errorMessage = e.getMessage();
+ e.printStackTrace();
+ }
+ Log.d(MyMapsConstants.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 setRetryOnAuthFailure(boolean retry) {
+ retryOnAuthFailure = retry;
+ }
+}
diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/mymaps/MyMapsMapMetadata.java b/MyTracks/src/com/google/android/apps/mytracks/io/mymaps/MyMapsMapMetadata.java
new file mode 100644
index 000000000..4ffb459b8
--- /dev/null
+++ b/MyTracks/src/com/google/android/apps/mytracks/io/mymaps/MyMapsMapMetadata.java
@@ -0,0 +1,52 @@
+// Copyright 2009 Google Inc. All Rights Reserved.
+package com.google.android.apps.mytracks.io.mymaps;
+
+/**
+ * Metadata about a "my maps" map.
+ */
+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);
+ }
+}
diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/mymaps/XmlMapsGDataParser.java b/MyTracks/src/com/google/android/apps/mytracks/io/mymaps/XmlMapsGDataParser.java
new file mode 100644
index 000000000..7af41d7ed
--- /dev/null
+++ b/MyTracks/src/com/google/android/apps/mytracks/io/mymaps/XmlMapsGDataParser.java
@@ -0,0 +1,163 @@
+// Copyright 2009 Google Inc. All Rights Reserved.
+package com.google.android.apps.mytracks.io.mymaps;
+
+import com.google.wireless.gdata.data.Entry;
+import com.google.wireless.gdata.data.Feed;
+import com.google.wireless.gdata.data.XmlUtils;
+import com.google.wireless.gdata.parser.ParseException;
+import com.google.wireless.gdata.parser.xml.XmlGDataParser;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * Parser for XML gdata maps data.
+ */
+class XmlMapsGDataParser extends XmlGDataParser {
+
+ public XmlMapsGDataParser(InputStream is, XmlPullParser xpp)
+ throws ParseException {
+ super(is, xpp);
+ }
+
+ @Override
+ protected Feed createFeed() {
+ return new Feed();
+ }
+
+ @Override
+ protected Entry createEntry() {
+ return new MapFeatureEntry();
+ }
+
+ @Override
+ protected void handleExtraElementInFeed(Feed feed) {
+ // Do nothing
+ }
+
+ @Override
+ protected void handleExtraLinkInEntry(
+ String rel, String type, String href, Entry entry)
+ throws XmlPullParserException, IOException {
+ if (!(entry instanceof MapFeatureEntry)) {
+ throw new IllegalArgumentException("Expected MapFeatureEntry!");
+ }
+ if (rel.endsWith("#view")) {
+ return;
+ }
+ super.handleExtraLinkInEntry(rel, type, href, entry);
+ }
+
+ /**
+ * Parses the current entry in the XML document. Assumes that the parser is
+ * currently pointing just after an <entry>.
+ *
+ * @param plainEntry The entry that will be filled.
+ * @throws XmlPullParserException Thrown if the XML cannot be parsed.
+ * @throws IOException Thrown if the underlying inputstream cannot be read.
+ */
+ @Override
+ protected void handleEntry(Entry plainEntry)
+ throws XmlPullParserException, IOException, ParseException {
+ XmlPullParser parser = getParser();
+ if (!(plainEntry instanceof MapFeatureEntry)) {
+ throw new IllegalArgumentException("Expected MapFeatureEntry!");
+ }
+ MapFeatureEntry entry = (MapFeatureEntry) plainEntry;
+
+ int eventType = parser.getEventType();
+ entry.setPrivacy("public");
+ while (eventType != XmlPullParser.END_DOCUMENT) {
+ switch (eventType) {
+ case XmlPullParser.START_TAG:
+ String name = parser.getName();
+ if ("entry".equals(name)) {
+ // stop parsing here.
+ return;
+ } else if ("id".equals(name)) {
+ entry.setId(XmlUtils.extractChildText(parser));
+ } else if ("title".equals(name)) {
+ entry.setTitle(XmlUtils.extractChildText(parser));
+ } else if ("link".equals(name)) {
+ String rel = parser.getAttributeValue(null /* ns */, "rel");
+ String type = parser.getAttributeValue(null /* ns */, "type");
+ String href = parser.getAttributeValue(null /* ns */, "href");
+ if ("edit".equals(rel)) {
+ entry.setEditUri(href);
+ } else if ("alternate".equals(rel) && "text/html".equals(type)) {
+ entry.setHtmlUri(href);
+ } else {
+ handleExtraLinkInEntry(rel, type, href, entry);
+ }
+ } else if ("summary".equals(name)) {
+ entry.setSummary(XmlUtils.extractChildText(parser));
+ } else if ("content".equals(name)) {
+ StringBuilder contentBuilder = new StringBuilder();
+ int parentDepth = parser.getDepth();
+ while (parser.getEventType() != XmlPullParser.END_DOCUMENT) {
+ int etype = parser.next();
+ switch (etype) {
+ case XmlPullParser.START_TAG:
+ contentBuilder.append('<');
+ contentBuilder.append(parser.getName());
+ contentBuilder.append('>');
+ break;
+ case XmlPullParser.TEXT:
+ contentBuilder.append("");
+ break;
+ case XmlPullParser.END_TAG:
+ if (parser.getDepth() > parentDepth) {
+ contentBuilder.append("");
+ contentBuilder.append(parser.getName());
+ contentBuilder.append('>');
+ }
+ break;
+ }
+ if (etype == XmlPullParser.END_TAG
+ && parser.getDepth() == parentDepth) {
+ break;
+ }
+ }
+ entry.setContent(contentBuilder.toString());
+ } else if ("category".equals(name)) {
+ String category = parser.getAttributeValue(null /* ns */, "term");
+ if (category != null && category.length() > 0) {
+ entry.setCategory(category);
+ }
+ String categoryScheme =
+ parser.getAttributeValue(null /* ns */, "scheme");
+ if (categoryScheme != null && category.length() > 0) {
+ entry.setCategoryScheme(categoryScheme);
+ }
+ } else if ("published".equals(name)) {
+ entry.setPublicationDate(XmlUtils.extractChildText(parser));
+ } else if ("updated".equals(name)) {
+ entry.setUpdateDate(XmlUtils.extractChildText(parser));
+ } else if ("deleted".equals(name)) {
+ entry.setDeleted(true);
+ } else if ("draft".equals(name)) {
+ String draft = XmlUtils.extractChildText(parser);
+ entry.setPrivacy("yes".equals(draft) ? "unlisted" : "public");
+ } else if ("customProperty".equals(name)) {
+ String attrName = parser.getAttributeValue(null, "name");
+ String attrValue = XmlUtils.extractChildText(parser);
+ entry.setAttribute(attrName, attrValue);
+ } else if ("deleted".equals(name)) {
+ entry.setDeleted(true);
+ } else {
+ handleExtraElementInEntry(entry);
+ }
+ break;
+ default:
+ break;
+ }
+
+ eventType = parser.next();
+ }
+ }
+}
diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/mymaps/XmlMapsGDataParserFactory.java b/MyTracks/src/com/google/android/apps/mytracks/io/mymaps/XmlMapsGDataParserFactory.java
new file mode 100644
index 000000000..15c0a884e
--- /dev/null
+++ b/MyTracks/src/com/google/android/apps/mytracks/io/mymaps/XmlMapsGDataParserFactory.java
@@ -0,0 +1,94 @@
+// Copyright 2010 Google Inc. All Rights Reserved.
+package com.google.android.apps.mytracks.io.mymaps;
+
+import com.google.wireless.gdata.client.GDataParserFactory;
+import com.google.wireless.gdata.data.Entry;
+import com.google.wireless.gdata.parser.GDataParser;
+import com.google.wireless.gdata.parser.ParseException;
+import com.google.wireless.gdata.parser.xml.XmlGDataParser;
+import com.google.wireless.gdata.parser.xml.XmlParserFactory;
+import com.google.wireless.gdata.serializer.GDataSerializer;
+import com.google.wireless.gdata.serializer.xml.XmlEntryGDataSerializer;
+
+import android.util.Log;
+
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * Factory of Xml parsers for gdata maps data.
+ */
+class XmlMapsGDataParserFactory implements GDataParserFactory {
+ private XmlParserFactory xmlFactory;
+
+ public XmlMapsGDataParserFactory(XmlParserFactory xmlFactory) {
+ this.xmlFactory = xmlFactory;
+ }
+
+ @Override
+ public GDataParser createParser(InputStream is) throws ParseException {
+ is = maybeLogCommunication(is);
+ try {
+ return new XmlGDataParser(is, xmlFactory.createParser());
+ } catch (XmlPullParserException e) {
+ e.printStackTrace();
+ return null;
+ }
+ }
+
+ @SuppressWarnings({ "unchecked", "rawtypes" })
+ @Override
+ public GDataParser createParser(Class cls, InputStream is)
+ throws ParseException {
+ is = maybeLogCommunication(is);
+ try {
+ return createParserForClass(cls, is);
+ } catch (XmlPullParserException e) {
+ e.printStackTrace();
+ return null;
+ }
+ }
+
+ private InputStream maybeLogCommunication(InputStream is)
+ throws ParseException {
+ if (MapsClient.LOG_COMMUNICATION) {
+ StringBuilder builder = new StringBuilder();
+ byte[] buffer = new byte[2048];
+ try {
+ for (int n = is.read(buffer); n >= 0; n = is.read(buffer)) {
+ String part = new String(buffer, 0, n);
+ builder.append(part);
+ Log.d("Response part", part);
+ }
+ } catch (IOException e) {
+ throw new ParseException("Could not read stream", e);
+ }
+ String whole = builder.toString();
+ Log.d("Response", whole);
+ is = new ByteArrayInputStream(whole.getBytes());
+ }
+ return is;
+ }
+
+ private GDataParser createParserForClass(
+ Class extends Entry> cls, InputStream is)
+ throws ParseException, XmlPullParserException {
+ if (cls == MapFeatureEntry.class) {
+ return new XmlMapsGDataParser(is, xmlFactory.createParser());
+ } else {
+ return new XmlGDataParser(is, xmlFactory.createParser());
+ }
+ }
+
+ @Override
+ public GDataSerializer createSerializer(Entry en) {
+ if (en.getClass() == MapFeatureEntry.class) {
+ return new XmlMapsGDataSerializer(xmlFactory, en);
+ } else {
+ return new XmlEntryGDataSerializer(xmlFactory, en);
+ }
+ }
+}
diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/mymaps/XmlMapsGDataSerializer.java b/MyTracks/src/com/google/android/apps/mytracks/io/mymaps/XmlMapsGDataSerializer.java
new file mode 100644
index 000000000..80598788d
--- /dev/null
+++ b/MyTracks/src/com/google/android/apps/mytracks/io/mymaps/XmlMapsGDataSerializer.java
@@ -0,0 +1,259 @@
+// Copyright 2010 Google Inc. All Rights Reserved.
+package com.google.android.apps.mytracks.io.mymaps;
+
+import com.google.wireless.gdata.data.Entry;
+import com.google.wireless.gdata.data.StringUtils;
+import com.google.wireless.gdata.parser.ParseException;
+import com.google.wireless.gdata.parser.xml.XmlGDataParser;
+import com.google.wireless.gdata.parser.xml.XmlParserFactory;
+import com.google.wireless.gdata.serializer.xml.XmlEntryGDataSerializer;
+
+import android.util.Log;
+
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlSerializer;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.Map;
+
+/**
+ * Serializer of maps data for GData.
+ */
+class XmlMapsGDataSerializer extends XmlEntryGDataSerializer {
+
+ private static final String APP_NAMESPACE = "http://www.w3.org/2007/app";
+
+ private MapFeatureEntry entry;
+ private XmlParserFactory factory;
+ private OutputStream stream;
+
+ public XmlMapsGDataSerializer(XmlParserFactory factory, Entry entry) {
+ super(factory, entry);
+
+ this.factory = factory;
+ this.entry = (MapFeatureEntry) entry;
+ }
+
+ @Override
+ public void serialize(OutputStream out, int format)
+ throws IOException, ParseException {
+ XmlSerializer serializer = null;
+ try {
+ serializer = factory.createSerializer();
+ } catch (XmlPullParserException e) {
+ throw new ParseException("Unable to create XmlSerializer.", e);
+ }
+
+ ByteArrayOutputStream printStream;
+
+ if (MapsClient.LOG_COMMUNICATION) {
+ printStream = new ByteArrayOutputStream();
+
+ serializer.setOutput(printStream, "UTF-8");
+ } else {
+ serializer.setOutput(out, "UTF-8");
+ }
+
+ serializer.startDocument("UTF-8", Boolean.FALSE);
+
+ declareEntryNamespaces(serializer);
+ serializer.startTag(XmlGDataParser.NAMESPACE_ATOM_URI, "entry");
+
+ stream = MapsClient.LOG_COMMUNICATION ? printStream : out;
+ serializeEntryContents(serializer, format);
+
+ serializer.endTag(XmlGDataParser.NAMESPACE_ATOM_URI, "entry");
+ serializer.endDocument();
+ serializer.flush();
+
+ if (MapsClient.LOG_COMMUNICATION) {
+ Log.d("Request", printStream.toString());
+ out.write(printStream.toByteArray());
+ stream = out;
+ }
+ }
+
+ private final void declareEntryNamespaces(XmlSerializer serializer)
+ throws IOException {
+ serializer.setPrefix(
+ "" /* default ns */, XmlGDataParser.NAMESPACE_ATOM_URI);
+ serializer.setPrefix(
+ XmlGDataParser.NAMESPACE_GD, XmlGDataParser.NAMESPACE_GD_URI);
+ declareExtraEntryNamespaces(serializer);
+ }
+
+ private final void serializeEntryContents(XmlSerializer serializer,
+ int format) throws IOException {
+ if (format != FORMAT_CREATE) {
+ serializeId(serializer, entry.getId());
+ }
+
+ serializeTitle(serializer, entry.getTitle());
+
+ if (format != FORMAT_CREATE) {
+ serializeLink(serializer,
+ "edit" /* rel */, entry.getEditUri(), null /* type */);
+ serializeLink(serializer,
+ "alternate" /* rel */, entry.getHtmlUri(), "text/html" /* type */);
+ }
+
+ serializeSummary(serializer, entry.getSummary());
+ serializeContent(serializer, entry.getContent());
+ serializeAuthor(serializer, entry.getAuthor(), entry.getEmail());
+ serializeCategory(serializer,
+ entry.getCategory(), entry.getCategoryScheme());
+
+ if (format == FORMAT_FULL) {
+ serializePublicationDate(serializer, entry.getPublicationDate());
+ }
+
+ if (format != FORMAT_CREATE) {
+ serializeUpdateDate(serializer, entry.getUpdateDate());
+ }
+
+ serializeExtraEntryContents(serializer, format);
+ }
+
+ private static void serializeId(XmlSerializer serializer, String id)
+ throws IOException {
+ if (StringUtils.isEmpty(id)) {
+ return;
+ }
+ serializer.startTag(null /* ns */, "id");
+ serializer.text(id);
+ serializer.endTag(null /* ns */, "id");
+ }
+
+ private static void serializeTitle(XmlSerializer serializer, String title)
+ throws IOException {
+ if (StringUtils.isEmpty(title)) {
+ return;
+ }
+ serializer.startTag(null /* ns */, "title");
+ serializer.text(title);
+ serializer.endTag(null /* ns */, "title");
+ }
+
+ public static void serializeLink(XmlSerializer serializer, String rel,
+ String href, String type) throws IOException {
+ if (StringUtils.isEmpty(href)) {
+ return;
+ }
+ serializer.startTag(null /* ns */, "link");
+ serializer.attribute(null /* ns */, "rel", rel);
+ serializer.attribute(null /* ns */, "href", href);
+ if (!StringUtils.isEmpty(type)) {
+ serializer.attribute(null /* ns */, "type", type);
+ }
+ serializer.endTag(null /* ns */, "link");
+ }
+
+ private static void serializeSummary(XmlSerializer serializer, String summary)
+ throws IOException {
+ if (StringUtils.isEmpty(summary)) {
+ return;
+ }
+ serializer.startTag(null /* ns */, "summary");
+ serializer.text(summary);
+ serializer.endTag(null /* ns */, "summary");
+ }
+
+ private void serializeContent(XmlSerializer serializer, String content)
+ throws IOException {
+ if (content == null) {
+ return;
+ }
+ serializer.startTag(null /* ns */, "content");
+ if (content.contains("")) {
+ serializer.attribute(
+ null /* ns */, "type", "application/vnd.google-earth.kml+xml");
+ serializer.flush();
+ stream.write(content.getBytes());
+ } else {
+ serializer.text(content);
+ }
+ serializer.endTag(null /* ns */, "content");
+ }
+
+ private static void serializeAuthor(XmlSerializer serializer, String author,
+ String email) throws IOException {
+ if (StringUtils.isEmpty(author) || StringUtils.isEmpty(email)) {
+ return;
+ }
+ serializer.startTag(null /* ns */, "author");
+ serializer.startTag(null /* ns */, "name");
+ serializer.text(author);
+ serializer.endTag(null /* ns */, "name");
+ serializer.startTag(null /* ns */, "email");
+ serializer.text(email);
+ serializer.endTag(null /* ns */, "email");
+ serializer.endTag(null /* ns */, "author");
+ }
+
+ private static void serializeCategory(XmlSerializer serializer,
+ String category, String categoryScheme) throws IOException {
+ if (StringUtils.isEmpty(category) && StringUtils.isEmpty(categoryScheme)) {
+ return;
+ }
+ serializer.startTag(null /* ns */, "category");
+ if (!StringUtils.isEmpty(category)) {
+ serializer.attribute(null /* ns */, "term", category);
+ }
+ if (!StringUtils.isEmpty(categoryScheme)) {
+ serializer.attribute(null /* ns */, "scheme", categoryScheme);
+ }
+ serializer.endTag(null /* ns */, "category");
+ }
+
+ private static void serializePublicationDate(XmlSerializer serializer,
+ String publicationDate) throws IOException {
+ if (StringUtils.isEmpty(publicationDate)) {
+ return;
+ }
+ serializer.startTag(null /* ns */, "published");
+ serializer.text(publicationDate);
+ serializer.endTag(null /* ns */, "published");
+ }
+
+ private static void serializeUpdateDate(XmlSerializer serializer,
+ String updateDate) throws IOException {
+ if (StringUtils.isEmpty(updateDate)) {
+ return;
+ }
+ serializer.startTag(null /* ns */, "updated");
+ serializer.text(updateDate);
+ serializer.endTag(null /* ns */, "updated");
+ }
+
+ @Override
+ protected void serializeExtraEntryContents(XmlSerializer serializer,
+ int format) throws IOException {
+ Map attrs = entry.getAllAttributes();
+ for (Map.Entry attr : attrs.entrySet()) {
+ serializer.startTag("http://schemas.google.com/g/2005", "customProperty");
+ serializer.attribute(null, "name", attr.getKey());
+ serializer.text(attr.getValue());
+ serializer.endTag("http://schemas.google.com/g/2005", "customProperty");
+ }
+ String privacy = entry.getPrivacy();
+ if (!StringUtils.isEmpty(privacy)) {
+ serializer.setPrefix("app", APP_NAMESPACE);
+ if ("public".equals(privacy)) {
+ serializer.startTag(APP_NAMESPACE, "control");
+ serializer.startTag(APP_NAMESPACE, "draft");
+ serializer.text("no");
+ serializer.endTag(APP_NAMESPACE, "draft");
+ serializer.endTag(APP_NAMESPACE, "control");
+ }
+ if ("unlisted".equals(privacy)) {
+ serializer.startTag(APP_NAMESPACE, "control");
+ serializer.startTag(APP_NAMESPACE, "draft");
+ serializer.text("yes");
+ serializer.endTag(APP_NAMESPACE, "draft");
+ serializer.endTag(APP_NAMESPACE, "control");
+ }
+ }
+ }
+}