Compare commits

..

1 Commits

Author SHA1 Message Date
Matthew Simmons c892704ee3 update for 1.1.4 2011-03-21 19:09:13 -04:00
39 changed files with 2201 additions and 231 deletions
-1
View File
@@ -5,4 +5,3 @@ syntax:glob
*/proguard/**
.metadata
MyTracksTest/annotations.log
MyTracks/src/com/google/android/apps/mytracks/io/mymaps
-1
View File
@@ -8,7 +8,6 @@
<classpathentry kind="src" path="src"/>
<classpathentry kind="src" path="gen"/>
<classpathentry exported="true" kind="lib" path="/MyTracksLib/libs/protobuf-java-2.3.0-lite.jar"/>
<classpathentry kind="lib" path="libs/libmymaps.jar"/>
<classpathentry kind="src" path="MyTracksLib_src"/>
<classpathentry kind="output" path="bin"/>
</classpath>
+2 -2
View File
@@ -17,8 +17,8 @@ limitations under the License.
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.google.android.maps.mytracks"
android:installLocation="auto"
android:versionCode="26"
android:versionName="1.1.3">
android:versionCode="27"
android:versionName="1.1.4">
<application android:icon="@drawable/arrow_icon"
android:label="@string/app_name"
android:backupAgent="com.google.android.apps.mytracks.io.backup.MyTracksBackupAgent">
Binary file not shown.
-1
View File
@@ -225,7 +225,6 @@ limitations under the License.
<string name="settings_backup_to_sd_summary">Escriu una còpia completa de totes les rutes i els paràmetres a la targeta SD</string>
<string name="settings_restore_from_sd">Restaura ara</string>
<string name="settings_restore_from_sd_summary">Restaura totes les rutes i els paràmetres des de la targeta SD.</string>
<string name="settings_no_backup_while_recording">No es poden gestionar les còpies de seguretat mentre s\'enregistra una ruta.</string>
<!-- Backups -->
<string name="restore_overwrites_warning">La restauració d\'una còpia de seguretat des de la targeta SD sobreescriurà totes les rutes i paràmetres. Esteu segur?</string>
+1 -1
View File
@@ -253,7 +253,7 @@ limitations under the License.
<string name="send_track_subject">Ich möchte einen GPS-Track mit Dir teilen</string>
<string name="send_track_body_format">Vielleicht interessierst Du Dich für diese Track:
%s.</string>
Der Track kann mit Google Earth oder anderer Karten-Software visualisiert werden.</string>
<string name="share_track">Track mit Freunden teilen</string>
<!-- Visibility -->
-1
View File
@@ -212,7 +212,6 @@ limitations under the License.
<string name="settings_backup_to_sd_summary">Hacer una copia total de sus recorridos y configuraciones a la tarjeta SD</string>
<string name="settings_restore_from_sd">Restaurar</string>
<string name="settings_restore_from_sd_summary">Restaurar todos los recorridos y configuraciones desde la tarjeta SD</string>
<string name="settings_no_backup_while_recording">No se pueden manejar backups mientras se está registrando un recorrido.</string>
<string name="restore_overwrites_warning">Restaurar un backup desde la tarjeta SD SOBREESCRIBIRÁ todos sus recorridos y configuraciones actuales. ¿Está usted seguro?</string>
<string name="select_backup_to_restore">Seleccionar el backup para restaurar</string>
-2
View File
@@ -213,8 +213,6 @@ limitations under the License.
<string name="settings_backup_to_sd_summary">Copier tous vos parcours et paramètres sur la carte SD</string>
<string name="settings_restore_from_sd">Restaurer</string>
<string name="settings_restore_from_sd_summary">Restaurer tous les parcours et paramètres depuis la carte SD</string>
<string name="settings_no_backup_while_recording">Sauvegarde impossible lors de l\'enregistrement d\'un parcours.</string>
<string name="restore_overwrites_warning">Restaurer une sauvegarde depuis la carte SD écrasera tous vos parcours et paramètres actuels.</string>
<string name="select_backup_to_restore">Choisissez la sauvegarde à restaurer</string>
-1
View File
@@ -256,7 +256,6 @@ limitations under the License.
<string name="settings_backup_to_sd_summary">SDカードに全てのトラックと設定をバックアップします</string>
<string name="settings_restore_from_sd">今すぐ復元</string>
<string name="settings_restore_from_sd_summary">SDカードから全てのトラックと設定を復元します</string>
<string name="settings_no_backup_while_recording">トラック記録中はバックアップ操作はできません.</string>
<string name="restore_overwrites_warning">SDカードからの復元を実行すると、現在のトラックと設定は *全て上書き* されてしまいます.よろしいですか?</string>
<string name="select_backup_to_restore">復元するバックアップの選択</string>
-1
View File
@@ -217,7 +217,6 @@ limitations under the License.
<string name="settings_backup_to_sd_summary">Tworzy kopię zapasową wszystkich tras i ustawień i zapisuje je na karcie SD</string>
<string name="settings_restore_from_sd">Odtwórz z kopii</string>
<string name="settings_restore_from_sd_summary">Odtwarza wszystkie trasy i ustawienia z kopii zapasowej z karty SD</string>
<string name="settings_no_backup_while_recording">Nie można tworzyć/odtwarzać kopii zapasowej podczas rejestrowania trasy.</string>
<!-- Backups -->
<string name="restore_overwrites_warning">Odtworzenie z kopii zapasowej z karty SD zastąpi wszystkie bieżące trasy i ustawienia. Jesteś tego pewien?</string>
+2
View File
@@ -157,6 +157,8 @@ limitations under the License.
<string name="settings_display">Exibição</string>
<string name="settings_recording">Gravação</string>
<string name="settings_sharing">Compartilhamento</string>
<string name="settings_not_available_summary">Não disponível na sua versão do Android</string>
<string name="settings_not_while_recording">Não disponível durante a gravação de uma trilha</string>
<string name="settings_announcement_frequency">Tempo entre anúncios</string>
<string name="settings_announcement_frequency_summary">Tempo mínimo entre dois anúncios de voz</string>
<string name="settings_min_recording_distance">Dist. mín. entre pontos</string>
-1
View File
@@ -219,7 +219,6 @@ limitations under the License.
<string name="settings_backup_to_sd_summary">将你所有轨迹和设置的完整备份写入到 SD 存储卡</string>
<string name="settings_restore_from_sd">立即恢复</string>
<string name="settings_restore_from_sd_summary">从 SD 存储卡恢复所有轨迹和设置</string>
<string name="settings_no_backup_while_recording">在记录轨迹时无法处理备份数据.</string>
<!-- Backups -->
<string name="restore_overwrites_warning">从 SD 存储卡恢复备份数据将覆盖掉你当前的所有轨迹和设置. 确定要继续吗?</string>
+1 -2
View File
@@ -50,8 +50,7 @@ limitations under the License.
<!-- Keys for non-persisted preferences - DO NOT TRANSLATE -->
<string name="backup_to_sd_key">backupToSd</string>
<string name="restore_from_sd_key">restoreFromSd</string>
<!-- Inoperative keys used for the location of preferences items -->
<string name="reset_key">resetToDefaults</string>
<string name="sensor_options_key">sensorOptions</string>
<string name="ant_options_key">antOptions</string>
</resources>
+6 -1
View File
@@ -196,6 +196,7 @@ limitations under the License.
<string name="settings_sharing">Sharing</string>
<string name="settings_not_available_summary">Not available in your version of Android</string>
<string name="settings_not_while_recording">Not available while recording a track</string>
<string name="settings_announcement_frequency">Announcement frequency</string>
<string name="settings_announcement_frequency_summary">Minimum time between two voice announcements</string>
@@ -238,6 +239,11 @@ limitations under the License.
<string name="settings_timestamp_track_name_summary_on">The default track name will be the date and time at the start of the track</string>
<string name="settings_timestamp_track_name_summary_off">Track names will have numbers which increment for each new track.</string>
<string name="settings_reset">Reset to defaults</string>
<string name="settings_reset_summary">Revert all your settings to the default values</string>
<string name="settings_reset_dialog_message">All your settings will be erased and reverted to the default values. Saved tracks will be preserved. Are you sure?</string>
<string name="settings_reset_done">All settings have been reverted to their defaults.</string>
<string name="pace_label">Pace</string>
<string name="eclair_required">Only supported in Android 2.0 or above</string>
@@ -259,7 +265,6 @@ limitations under the License.
<string name="settings_backup_to_sd_summary">Write a full copy of all your tracks and settings to the SD card</string>
<string name="settings_restore_from_sd">Restore now</string>
<string name="settings_restore_from_sd_summary">Restore all tracks and settings from the SD card</string>
<string name="settings_no_backup_while_recording">Cannot manipulate backups while recording a track.</string>
<string name="restore_overwrites_warning">Restoring a backup from the SD card will OVERWRITE all your current tracks and settings. Are you sure?</string>
<string name="select_backup_to_restore">Select the backup to restore</string>
+7
View File
@@ -173,4 +173,11 @@ limitations under the License.
android:persistent="false" />
</PreferenceCategory>
</PreferenceScreen>
<!-- Reset to defaults -->
<Preference
android:title="@string/settings_reset"
android:key="@string/reset_key"
android:summary="@string/settings_reset_summary"
android:persistent="false" />
</PreferenceScreen>
@@ -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;
@@ -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);
@@ -15,10 +15,7 @@
*/
package com.google.android.apps.mytracks;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import static com.google.android.apps.mytracks.MyTracksConstants.TAG;
import com.google.android.apps.mytracks.io.backup.BackupActivityHelper;
import com.google.android.apps.mytracks.io.backup.BackupPreferencesListener;
@@ -28,18 +25,28 @@ import com.google.android.apps.mytracks.util.ApiFeatures;
import com.google.android.apps.mytracks.util.BluetoothDeviceUtils;
import com.google.android.maps.mytracks.R;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.preference.CheckBoxPreference;
import android.preference.ListPreference;
import android.preference.Preference;
import android.preference.PreferenceActivity;
import android.preference.PreferenceManager;
import android.preference.Preference.OnPreferenceChangeListener;
import android.preference.Preference.OnPreferenceClickListener;
import android.preference.PreferenceActivity;
import android.preference.PreferenceManager;
import android.preference.PreferenceScreen;
import android.provider.Settings;
import android.util.Log;
import android.widget.Toast;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* An activity that let's the user see and edit the settings.
@@ -119,6 +126,16 @@ public class MyTracksSettings extends PreferenceActivity {
announcementFrequency.setSummary(
R.string.settings_not_available_summary);
}
// Hook up action for resetting all settings
Preference resetPreference = findPreference(getString(R.string.reset_key));
resetPreference.setOnPreferenceClickListener(new OnPreferenceClickListener() {
@Override
public boolean onPreferenceClick(Preference arg0) {
onResetPreferencesClick();
return true;
}
});
}
private void customizeSensorOptionsPreferences() {
@@ -177,19 +194,24 @@ public class MyTracksSettings extends PreferenceActivity {
findPreference(getString(R.string.backup_to_sd_key));
Preference restoreNowPreference =
findPreference(getString(R.string.restore_from_sd_key));
Preference resetPreference = findPreference(getString(R.string.reset_key));
// If recording, disable backup/restore
// If recording, disable backup/restore/reset
// (we don't want to get to inconsistent states)
boolean recording =
preferences.getLong(getString(R.string.recording_track_key), -1) != -1;
backupNowPreference.setEnabled(!recording);
restoreNowPreference.setEnabled(!recording);
resetPreference.setEnabled(!recording);
backupNowPreference.setSummary(
recording ? R.string.settings_no_backup_while_recording
recording ? R.string.settings_not_while_recording
: R.string.settings_backup_to_sd_summary);
restoreNowPreference.setSummary(
recording ? R.string.settings_no_backup_while_recording
recording ? R.string.settings_not_while_recording
: R.string.settings_restore_from_sd_summary);
resetPreference.setSummary(
recording ? R.string.settings_not_while_recording
: R.string.settings_reset_summary);
// Add actions to the backup preferences
backupNowPreference.setOnPreferenceClickListener(
@@ -315,4 +337,53 @@ public class MyTracksSettings extends PreferenceActivity {
devicesPreference.setEntryValues(entryValuesArray);
devicesPreference.setEntries(entriesArray);
}
/** Callback for when user asks to reset all settings. */
private void onResetPreferencesClick() {
AlertDialog dialog = new AlertDialog.Builder(this)
.setCancelable(true)
.setTitle(R.string.settings_reset)
.setMessage(R.string.settings_reset_dialog_message)
.setPositiveButton(android.R.string.ok,
new OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int button) {
onResetPreferencesConfirmed();
}
})
.setNegativeButton(android.R.string.cancel, null)
.create();
dialog.show();
}
/** Callback for when user confirms resetting all settings. */
private void onResetPreferencesConfirmed() {
// Change preferences in a separate thread.
new Thread() {
@Override
public void run() {
Log.i(TAG, "Resetting all settings");
// Actually wipe preferences (and save synchronously).
preferences.edit().clear().commit();
// Give UI feedback in the UI thread.
runOnUiThread(new Runnable() {
@Override
public void run() {
// Give feedback to the user.
Toast.makeText(
MyTracksSettings.this,
R.string.settings_reset_done,
Toast.LENGTH_SHORT).show();
// Restart the settings activity so all changes are loaded.
Intent intent = getIntent();
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
startActivity(intent);
}
});
}
}.start();
}
}
@@ -193,12 +193,14 @@ public class MyTracksProvider extends ContentProvider {
@Override
public int delete(Uri url, String where, String[] selectionArgs) {
String table;
boolean shouldVacuum = false;
switch (urlMatcher.match(url)) {
case TRACKPOINTS:
table = TRACKPOINTS_TABLE;
break;
case TRACKS:
table = TRACKS_TABLE;
shouldVacuum = true;
break;
case WAYPOINTS:
table = WAYPOINTS_TABLE;
@@ -210,6 +212,13 @@ public class MyTracksProvider extends ContentProvider {
Log.w(MyTracksProvider.TAG, "provider delete in " + table + "!");
int count = db.delete(table, where, selectionArgs);
getContext().getContentResolver().notifyChange(url, null, true);
if (shouldVacuum) {
// If a potentially large amount of data was deleted, we want to reclaim its space.
Log.i(TAG, "Vacuuming the database");
db.execSQL("VACUUM");
}
return count;
}
@@ -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() {}
}
@@ -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("<p>" + track.getDescription() + "</p><p>"
+ stringUtils.generateTrackDescription(track, null, null) + "</p>");
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<WaypointData> waypoints = new ArrayList<WaypointData>(c.getCount() - 1);
ArrayList<Waypoint> waypoints = new ArrayList<Waypoint>(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);
@@ -17,11 +17,13 @@ package com.google.android.apps.mytracks.io;
import com.google.android.apps.mytracks.content.MyTracksLocation;
import com.google.android.apps.mytracks.content.Sensor;
import com.google.android.apps.mytracks.content.Sensor.SensorDataSet;
import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.content.Waypoint;
import com.google.android.apps.mytracks.content.Sensor.SensorDataSet;
import com.google.android.apps.mytracks.io.TrackWriterFactory.TrackFileFormat;
import com.google.android.apps.mytracks.util.MyTracksUtils;
import android.content.Context;
import android.location.Location;
import android.os.Build;
@@ -30,6 +32,7 @@ import java.io.PrintWriter;
import java.nio.charset.Charset;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import java.util.TimeZone;
/**
@@ -37,20 +40,35 @@ import java.util.TimeZone;
* As defined by:
* http://www.garmin.com/xmlschemas/TrainingCenterDatabase/v2
*
* The TCX file written by this class has been verified as compatible with
* Garmin Training Center 3.5.3.
*
* @author Sandor Dornbush
*/
public class TcxTrackWriter implements TrackFormatWriter {
protected static final String TIMESTAMP_FORMAT = "yyyy-MM-dd'T'HH:mm:ss'Z'";
// These are the only sports allowed by the TCX v2 specification for fields
// of type Sport_t.
private static final String TCX_SPORT_BIKING = "Biking";
private static final String TCX_SPORT_RUNNING = "Running";
private static final String TCX_SPORT_OTHER = "Other";
// Values for fields of type Build_t/Type.
private static final String TCX_TYPE_RELEASE = "Release";
private static final String TCX_TYPE_INTERNAL = "Internal";
private final SimpleDateFormat timestampFormatter;
private final Context context;
private PrintWriter pw = null;
private Track track;
static final SimpleDateFormat TIMESTAMP_FORMAT =
new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
static {
TIMESTAMP_FORMAT.setTimeZone(TimeZone.getTimeZone("UTC"));
}
public TcxTrackWriter(Context context) {
this.context = context;
public TcxTrackWriter() {
timestampFormatter = new SimpleDateFormat(TIMESTAMP_FORMAT);
timestampFormatter.setTimeZone(TimeZone.getTimeZone("UTC"));
}
@Override
@@ -93,16 +111,13 @@ public class TcxTrackWriter implements TrackFormatWriter {
if (pw == null) {
return;
}
String startTime = timestampFormatter.format(track.getStatistics().getStartTime());
pw.println(" <Activities>");
pw.print(" <Activity Sport=\"");
if (track.getCategory() != null) {
pw.print(track.getCategory());
}
pw.println("\">");
pw.print(" <Id>");
pw.print(TIMESTAMP_FORMAT.format(track.getStatistics().getStartTime()));
pw.println("</Id>");
pw.println(" <Lap>");
pw.format(" <Activity Sport=\"%s\">\n", categoryToTcxSport(track.getCategory()));
pw.format(" <Id>%s</Id>\n", startTime);
pw.format(" <Lap StartTime=\"%s\">\n", startTime);
pw.print(" <TotalTimeSeconds>");
pw.print(track.getStatistics().getTotalTime() / 1000);
pw.println("</TotalTimeSeconds>");
@@ -113,7 +128,7 @@ public class TcxTrackWriter implements TrackFormatWriter {
// Calories are a required element just put in 0.
pw.print("<Calories>0</Calories>");
pw.println("<Intensity>Active</Intensity>");
pw.println("<TriggerMethod>Manual</TriggerMethod>)");
pw.println("<TriggerMethod>Manual</TriggerMethod>");
}
@Override
@@ -130,7 +145,7 @@ public class TcxTrackWriter implements TrackFormatWriter {
}
pw.println(" <Trackpoint>");
Date d = new Date(location.getTime());
pw.println(" <Time>" + TIMESTAMP_FORMAT.format(d) + "</Time>");
pw.println(" <Time>" + timestampFormatter.format(d) + "</Time>");
pw.println(" <Position>");
pw.print(" <LatitudeDegrees>");
@@ -194,10 +209,18 @@ public class TcxTrackWriter implements TrackFormatWriter {
}
pw.println(" </Lap>");
pw.print(" <Creator xsi:type=\"Device_t\">");
pw.print("<Name>");
pw.print(Build.MODEL);
pw.print("</Name>");
pw.println("</Creator>)");
pw.format("<Name>%s</Name>\n", Build.MODEL);
// The following code is correct. ID is inconsistently capitalized in the
// TCX schema.
pw.println("<UnitId>0</UnitId>");
pw.println("<ProductID>0</ProductID>");
pw.println("<Version>");
pw.println("<VersionMajor>0</VersionMajor>");
pw.println("<VersionMinor>0</VersionMinor>");
pw.println("</Version>");
pw.println("</Creator>");
pw.println(" </Activity>");
pw.println(" </Activities>");
}
@@ -207,8 +230,31 @@ public class TcxTrackWriter implements TrackFormatWriter {
if (pw == null) {
return;
}
pw.print(" <Author xsi:type=\"Application_t\">");
pw.print("<Name>My Tracks by Google</Name>");
pw.println(" <Author xsi:type=\"Application_t\">");
// We put the version in the name because there isn't a better place for
// it. The TCX schema tightly defined the Version tag, so we can't put it
// there. They've similarly constrained the PartNumber tag, so it can't go
// there either.
pw.format("<Name>My Tracks %s by Google</Name>\n", MyTracksUtils.getMyTracksVersion(context));
pw.println("<Build>");
// The TCX spec for the Version tag is too strict to allow us to use our
// user-visible version number, so we use our version code instead. We
// also set the minor version to 1 if this is a development build to
// signify that this build is newer than the one associated with the
// version code given in VersionMajor.
pw.println("<Version>");
pw.format("<VersionMajor>%d</VersionMajor>\n", MyTracksUtils.getMyTracksVersionCode(context));
pw.format("<VersionMinor>%d</VersionMinor>\n", MyTracksUtils.isRelease(context) ? 0 : 1);
pw.println("</Version>");
pw.format("<Type>%s</Type>\n", MyTracksUtils.isRelease(context) ? TCX_TYPE_RELEASE
: TCX_TYPE_INTERNAL);
pw.println("</Build>");
pw.format("<LangID>%s</LangID>\n", Locale.getDefault().getLanguage());
pw.println("<PartNumber>000-00000-00</PartNumber>");
pw.println("</Author>");
pw.println("</TrainingCenterDatabase>");
}
@@ -217,4 +263,15 @@ public class TcxTrackWriter implements TrackFormatWriter {
public void writeWaypoint(Waypoint waypoint) {
// TODO Write out the waypoints somewhere.
}
private String categoryToTcxSport(String category) {
category = category.trim();
if (category.equalsIgnoreCase(TCX_SPORT_RUNNING)) {
return TCX_SPORT_RUNNING;
} else if (category.equalsIgnoreCase(TCX_SPORT_BIKING)) {
return TCX_SPORT_BIKING;
} else {
return TCX_SPORT_OTHER;
}
}
}
@@ -53,7 +53,7 @@ public class TrackWriterFactory {
TCX {
@Override
public TrackFormatWriter newFormatWriter(Context context) {
return new TcxTrackWriter();
return new TcxTrackWriter(context);
}
};
@@ -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;
}
}
@@ -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<String, String> mAttributes = new HashMap<String, String>();
public void setPrivacy(String privacy) {
mPrivacy = privacy;
}
public String getPrivacy() {
return mPrivacy;
}
public void setAttribute(String name, String value) {
mAttributes.put(name, value);
}
public void removeAttribute(String name) {
mAttributes.remove(name);
}
public Map<String, String> getAllAttributes() {
return mAttributes;
}
}
@@ -0,0 +1,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);
}
}
@@ -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", "<br>"));
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<Waypoint> 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<Location> 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<Location> 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<Location> 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));
}
}
@@ -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() { }
}
@@ -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<Integer> latitudeE6 = new Vector<Integer>();
/**
* The longitudes of the points of this feature in order, specified in
* millionths of a degree east.
*/
private final Vector<Integer> longitudeE6 = new Vector<Integer>();
/** The metadata of this feature in a format efficient for transmission. */
private MyMapsFeatureMetadata featureInfo = new MyMapsFeatureMetadata();
/**
* Initializes a valid but empty feature. It will default to a
* {@link #MARKER} with a blue placemark with a dot as an icon at the
* location (0, 0).
*/
public MyMapsFeature() {
}
/**
* Adds a new point to the end of this feature.
*
* @param point The new point to add
*/
public synchronized void addPoint(GeoPoint point) {
latitudeE6.add(point.getLatitudeE6());
longitudeE6.add(point.getLongitudeE6());
}
/**
* Generates a new local id for this feature based on the current time and
* a random number.
*/
public synchronized void generateAndroidId() {
Long time = System.currentTimeMillis();
Integer random = (int) (Math.random() * 10000);
androidId = time.toString() + "." + random.toString();
}
/**
* Retrieves the current local id for this feature if one is available.
*
* @return The local id for this feature
*/
public String getAndroidId() {
return androidId;
}
/**
* Retrieves the current (html) description of this feature. The description
* is stored in the feature metadata.
*
* @return The description of this feature
*/
public String getDescription() {
return featureInfo.getDescription();
}
/**
* Sets the description of this feature. That description is stored in the
* feature metadata.
*
* @param description The new description of this feature
*/
public synchronized void setDescription(String description) {
featureInfo.setDescription(description);
}
/**
* Retrieves the point at the given index for this feature.
*
* @param index The index of the point desired
* @return A {@link GeoPoint} representing the point or null if that point
* doesn't exist
*/
public GeoPoint getPoint(int index) {
if (latitudeE6.size() <= index) {
return null;
}
return new GeoPoint(latitudeE6.get(index), longitudeE6.get(index));
}
/**
* Counts the number of points in this feature and return that count.
*
* @return The number of points in this feature
*/
public int getPointCount() {
return latitudeE6.size();
}
/**
* Retrieves the title of this feature. That title is stored in the feature
* metadata.
*
* @return the current title of this feature
*/
public String getTitle() {
return featureInfo.getTitle();
}
/**
* Retrieves the type of this feature. That type is stored in the feature
* metadata.
*
* @return One of {@link #MARKER}, {@link #LINE}, or {@link #SHAPE}
* identifying the type of this feature
*/
public int getType() {
return featureInfo.getType();
}
/**
* Retrieves the current color of this feature as an ARGB color integer.
* That color is stored in the feature metadata.
*
* @return The ARGB color of this feature
*/
public int getColor() {
return featureInfo.getColor();
}
/**
* Retrieves the current line width of this feature. That line width is
* stored in the feature metadata.
*
* @return The line width of this feature
*/
public int getLineWidth() {
return featureInfo.getLineWidth();
}
/**
* Retrieves the current fill color of this feature as an ARGB color
* integer. That color is stored in the feature metadata.
*
* @return The ARGB fill color of this feature
*/
public int getFillColor() {
return featureInfo.getFillColor();
}
/**
* Retrieves the current icon url of this feature. That icon url is stored
* in the feature metadata.
*
* @return The icon url for this feature
*/
public String getIconUrl() {
return featureInfo.getIconUrl();
}
/**
* Sets the title of this feature. That title is stored in the feature
* metadata.
*
* @param title The new title of this feature
*/
public synchronized void setTitle(String title) {
featureInfo.setTitle(title);
}
/**
* Sets the type of this feature. That type is stored in the feature
* metadata.
*
* @param type The new type of the feature. That type must be one of
* {@link #MARKER}, {@link #LINE}, or {@link #SHAPE}
*/
public synchronized void setType(int type) {
featureInfo.setType(type);
}
/**
* Sets the ARGB color of this feature. That color is stored in the feature
* metadata.
*
* @param color The new ARGB color of this feature
*/
public synchronized void setColor(int color) {
featureInfo.setColor(color);
}
/**
* Sets the icon url of this feature. That icon url is stored in the feature
* metadata.
*
* @param url The new icon url of the feature
*/
public synchronized void setIconUrl(String url) {
featureInfo.setIconUrl(url);
}
}
@@ -0,0 +1,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);
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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);
}
}
@@ -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 &lt;entry&gt;.
*
* @param plainEntry The entry that will be filled.
* @throws XmlPullParserException Thrown if the XML cannot be parsed.
* @throws IOException Thrown if the underlying inputstream cannot be read.
*/
@Override
protected void handleEntry(Entry plainEntry)
throws XmlPullParserException, IOException, ParseException {
XmlPullParser parser = getParser();
if (!(plainEntry instanceof MapFeatureEntry)) {
throw new IllegalArgumentException("Expected MapFeatureEntry!");
}
MapFeatureEntry entry = (MapFeatureEntry) plainEntry;
int eventType = parser.getEventType();
entry.setPrivacy("public");
while (eventType != XmlPullParser.END_DOCUMENT) {
switch (eventType) {
case XmlPullParser.START_TAG:
String name = parser.getName();
if ("entry".equals(name)) {
// stop parsing here.
return;
} else if ("id".equals(name)) {
entry.setId(XmlUtils.extractChildText(parser));
} else if ("title".equals(name)) {
entry.setTitle(XmlUtils.extractChildText(parser));
} else if ("link".equals(name)) {
String rel = parser.getAttributeValue(null /* ns */, "rel");
String type = parser.getAttributeValue(null /* ns */, "type");
String href = parser.getAttributeValue(null /* ns */, "href");
if ("edit".equals(rel)) {
entry.setEditUri(href);
} else if ("alternate".equals(rel) && "text/html".equals(type)) {
entry.setHtmlUri(href);
} else {
handleExtraLinkInEntry(rel, type, href, entry);
}
} else if ("summary".equals(name)) {
entry.setSummary(XmlUtils.extractChildText(parser));
} else if ("content".equals(name)) {
StringBuilder contentBuilder = new StringBuilder();
int parentDepth = parser.getDepth();
while (parser.getEventType() != XmlPullParser.END_DOCUMENT) {
int etype = parser.next();
switch (etype) {
case XmlPullParser.START_TAG:
contentBuilder.append('<');
contentBuilder.append(parser.getName());
contentBuilder.append('>');
break;
case XmlPullParser.TEXT:
contentBuilder.append("<![CDATA[");
contentBuilder.append(parser.getText());
contentBuilder.append("]]>");
break;
case XmlPullParser.END_TAG:
if (parser.getDepth() > parentDepth) {
contentBuilder.append("</");
contentBuilder.append(parser.getName());
contentBuilder.append('>');
}
break;
}
if (etype == XmlPullParser.END_TAG
&& parser.getDepth() == parentDepth) {
break;
}
}
entry.setContent(contentBuilder.toString());
} else if ("category".equals(name)) {
String category = parser.getAttributeValue(null /* ns */, "term");
if (category != null && category.length() > 0) {
entry.setCategory(category);
}
String categoryScheme =
parser.getAttributeValue(null /* ns */, "scheme");
if (categoryScheme != null && category.length() > 0) {
entry.setCategoryScheme(categoryScheme);
}
} else if ("published".equals(name)) {
entry.setPublicationDate(XmlUtils.extractChildText(parser));
} else if ("updated".equals(name)) {
entry.setUpdateDate(XmlUtils.extractChildText(parser));
} else if ("deleted".equals(name)) {
entry.setDeleted(true);
} else if ("draft".equals(name)) {
String draft = XmlUtils.extractChildText(parser);
entry.setPrivacy("yes".equals(draft) ? "unlisted" : "public");
} else if ("customProperty".equals(name)) {
String attrName = parser.getAttributeValue(null, "name");
String attrValue = XmlUtils.extractChildText(parser);
entry.setAttribute(attrName, attrValue);
} else if ("deleted".equals(name)) {
entry.setDeleted(true);
} else {
handleExtraElementInEntry(entry);
}
break;
default:
break;
}
eventType = parser.next();
}
}
}
@@ -0,0 +1,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);
}
}
}
@@ -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("</Placemark>")) {
serializer.attribute(
null /* ns */, "type", "application/vnd.google-earth.kml+xml");
serializer.flush();
stream.write(content.getBytes());
} else {
serializer.text(content);
}
serializer.endTag(null /* ns */, "content");
}
private static void serializeAuthor(XmlSerializer serializer, String author,
String email) throws IOException {
if (StringUtils.isEmpty(author) || StringUtils.isEmpty(email)) {
return;
}
serializer.startTag(null /* ns */, "author");
serializer.startTag(null /* ns */, "name");
serializer.text(author);
serializer.endTag(null /* ns */, "name");
serializer.startTag(null /* ns */, "email");
serializer.text(email);
serializer.endTag(null /* ns */, "email");
serializer.endTag(null /* ns */, "author");
}
private static void serializeCategory(XmlSerializer serializer,
String category, String categoryScheme) throws IOException {
if (StringUtils.isEmpty(category) && StringUtils.isEmpty(categoryScheme)) {
return;
}
serializer.startTag(null /* ns */, "category");
if (!StringUtils.isEmpty(category)) {
serializer.attribute(null /* ns */, "term", category);
}
if (!StringUtils.isEmpty(categoryScheme)) {
serializer.attribute(null /* ns */, "scheme", categoryScheme);
}
serializer.endTag(null /* ns */, "category");
}
private static void serializePublicationDate(XmlSerializer serializer,
String publicationDate) throws IOException {
if (StringUtils.isEmpty(publicationDate)) {
return;
}
serializer.startTag(null /* ns */, "published");
serializer.text(publicationDate);
serializer.endTag(null /* ns */, "published");
}
private static void serializeUpdateDate(XmlSerializer serializer,
String updateDate) throws IOException {
if (StringUtils.isEmpty(updateDate)) {
return;
}
serializer.startTag(null /* ns */, "updated");
serializer.text(updateDate);
serializer.endTag(null /* ns */, "updated");
}
@Override
protected void serializeExtraEntryContents(XmlSerializer serializer,
int format) throws IOException {
Map<String, String> attrs = entry.getAllAttributes();
for (Map.Entry<String, String> attr : attrs.entrySet()) {
serializer.startTag("http://schemas.google.com/g/2005", "customProperty");
serializer.attribute(null, "name", attr.getKey());
serializer.text(attr.getValue());
serializer.endTag("http://schemas.google.com/g/2005", "customProperty");
}
String privacy = entry.getPrivacy();
if (!StringUtils.isEmpty(privacy)) {
serializer.setPrefix("app", APP_NAMESPACE);
if ("public".equals(privacy)) {
serializer.startTag(APP_NAMESPACE, "control");
serializer.startTag(APP_NAMESPACE, "draft");
serializer.text("no");
serializer.endTag(APP_NAMESPACE, "draft");
serializer.endTag(APP_NAMESPACE, "control");
}
if ("unlisted".equals(privacy)) {
serializer.startTag(APP_NAMESPACE, "control");
serializer.startTag(APP_NAMESPACE, "draft");
serializer.text("yes");
serializer.endTag(APP_NAMESPACE, "draft");
serializer.endTag(APP_NAMESPACE, "control");
}
}
}
}
@@ -18,7 +18,6 @@ package com.google.android.apps.mytracks.util;
import com.google.android.apps.mytracks.MyTracksConstants;
import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.stats.TripStatistics;
import com.google.android.maps.GeoPoint;
import android.app.Activity;
@@ -259,17 +258,17 @@ public class MyTracksUtils {
* Returns whether or not this is a release build.
*/
public static boolean isRelease(Context context) {
try {
Signature [] sigs = context.getPackageManager().getPackageInfo(
context.getPackageName(), PackageManager.GET_SIGNATURES).signatures;
for (Signature sig : sigs) {
PackageInfo packageInfo = getPackageInfo(context, PackageManager.GET_SIGNATURES);
if (packageInfo == null) {
return false;
}
for (Signature sig : packageInfo.signatures) {
if (sig.hashCode() == RELEASE_SIGNATURE_HASHCODE) {
return true;
}
}
} catch (NameNotFoundException e) {
Log.e(MyTracksConstants.TAG, "Unable to get signatures", e);
}
return false;
}
@@ -279,14 +278,31 @@ public class MyTracksUtils {
* @return the version, or an empty string in case of failure.
*/
public static String getMyTracksVersion(Context context) {
PackageInfo packageInfo = getPackageInfo(context, PackageManager.GET_META_DATA);
return packageInfo == null ? "" : packageInfo.versionName;
}
/**
* Return the My Tracks version code from the manifest.
*
* @return the version code, or an empty string in case of failure.
*/
public static int getMyTracksVersionCode(Context context) {
PackageInfo packageInfo = getPackageInfo(context, PackageManager.GET_META_DATA);
return packageInfo == null ? -1 : packageInfo.versionCode;
}
private static PackageInfo getPackageInfo(Context context, int flags) {
if (context == null) {
Log.w(MyTracksConstants.TAG, "No context found when attempting to get PackageInfo");
return null;
}
try {
PackageInfo pi = context.getPackageManager().getPackageInfo(
"com.google.android.maps.mytracks",
PackageManager.GET_META_DATA);
return pi.versionName;
return context.getPackageManager().getPackageInfo(
"com.google.android.maps.mytracks", flags);
} catch (NameNotFoundException e) {
Log.w(MyTracksConstants.TAG, "Failed to get version info.", e);
return "";
return null;
}
}
@@ -4,21 +4,32 @@ package com.google.android.apps.mytracks.io;
import com.google.android.apps.mytracks.content.MyTracksLocation;
import com.google.android.apps.mytracks.content.Sensor;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.List;
import java.util.TimeZone;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import java.util.Date;
import java.util.List;
/**
* Tests for the GPX track exporter.
*
* @author Sandor Dornbush
*/
public class TcxTrackWriterTest extends TrackFormatWriterTest {
private DateFormat timestampFormatter;
@Override
public void setUp() throws Exception {
super.setUp();
timestampFormatter = new SimpleDateFormat(TcxTrackWriter.TIMESTAMP_FORMAT);
timestampFormatter.setTimeZone(TimeZone.getTimeZone("UTC"));
}
public void testXmlOutput() throws Exception {
TrackFormatWriter writer = new TcxTrackWriter();
TrackFormatWriter writer = new TcxTrackWriter(null);
String result = writeTrack(writer);
Document doc = parseXmlDocument(result);
@@ -58,8 +69,8 @@ public class TcxTrackWriterTest extends TrackFormatWriterTest {
getChildTextValue(posTag, "LatitudeDegrees"));
assertEquals(Double.toString(loc.getLongitude()),
getChildTextValue(posTag, "LongitudeDegrees"));
assertEquals(
TcxTrackWriter.TIMESTAMP_FORMAT.format(new Date(loc.getTime())),
assertEquals(timestampFormatter.format(loc.getTime()),
getChildTextValue(tag, "Time"));
assertEquals(Double.toString(loc.getAltitude()),
getChildTextValue(tag, "AltitudeMeters"));
+56
View File
@@ -0,0 +1,56 @@
#!/bin/bash
#
# Copyright 2010 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# This script retrieves the value of a given named string from all indicated
# strings.xml files. If invoked in a project directory (a directory with a
# 'res' subdirectory), and if no strings.xml files are provided, the script
# will automatically analyze res/values*/strings.xml
PROGNAME=$(basename "$0")
function usage() {
echo "Usage: ${PROGNAME} string_name [file file..]" >&2
exit 2
}
function die() {
echo "${PROGNAME}: $@" >&2
exit 1
}
if [[ "$#" -lt 1 ]] ; then
usage
fi
name=$1
shift
files=
if [[ $# -eq 0 ]] ; then
if [[ -d res ]] ; then
files=res/values*/strings.xml
else
die "invoked outside of project root with no file arguments"
fi
else
files="$@"
fi
for file in $files ; do
echo === $file
xmllint --xpath /resources/string[@name=\"$name\"] $file
echo
done