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("'); + } + 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 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"); + } + } + } +}