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/AndroidManifest.xml b/MyTracks/AndroidManifest.xml index e618828c3..1545eb1c9 100755 --- a/MyTracks/AndroidManifest.xml +++ b/MyTracks/AndroidManifest.xml @@ -17,8 +17,8 @@ limitations under the License. + android:versionCode="26" + android:versionName="1.1.3"> @@ -61,16 +61,16 @@ limitations under the License. android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|orientation|fontScale" android:exported="false" /> - - - - - + 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/res/drawable-hdpi/failure.png b/MyTracks/res/drawable-hdpi/failure.png new file mode 100644 index 000000000..127f55275 Binary files /dev/null and b/MyTracks/res/drawable-hdpi/failure.png differ diff --git a/MyTracks/res/drawable-hdpi/success.png b/MyTracks/res/drawable-hdpi/success.png new file mode 100644 index 000000000..977d08fbe Binary files /dev/null and b/MyTracks/res/drawable-hdpi/success.png differ diff --git a/MyTracks/res/drawable-ldpi/failure.png b/MyTracks/res/drawable-ldpi/failure.png new file mode 100644 index 000000000..3722b0794 Binary files /dev/null and b/MyTracks/res/drawable-ldpi/failure.png differ diff --git a/MyTracks/res/drawable-ldpi/success.png b/MyTracks/res/drawable-ldpi/success.png new file mode 100644 index 000000000..1d44ba697 Binary files /dev/null and b/MyTracks/res/drawable-ldpi/success.png differ diff --git a/MyTracks/res/drawable-mdpi/failure.png b/MyTracks/res/drawable-mdpi/failure.png new file mode 100644 index 000000000..5ff6b1ebb Binary files /dev/null and b/MyTracks/res/drawable-mdpi/failure.png differ diff --git a/MyTracks/res/drawable-mdpi/success.png b/MyTracks/res/drawable-mdpi/success.png new file mode 100644 index 000000000..31f2ee563 Binary files /dev/null and b/MyTracks/res/drawable-mdpi/success.png differ diff --git a/MyTracks/res/layout/send_to_google_result.xml b/MyTracks/res/layout/send_to_google_result.xml new file mode 100644 index 000000000..f2d93780f --- /dev/null +++ b/MyTracks/res/layout/send_to_google_result.xml @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + diff --git a/MyTracks/res/layout/send_to_google_result_list_item.xml b/MyTracks/res/layout/send_to_google_result_list_item.xml new file mode 100644 index 000000000..045a60584 --- /dev/null +++ b/MyTracks/res/layout/send_to_google_result_list_item.xml @@ -0,0 +1,49 @@ + + + + + + + + + + diff --git a/MyTracks/res/values-ca/strings.xml b/MyTracks/res/values-ca/strings.xml index e2ca5f58d..6ac1e8525 100644 --- a/MyTracks/res/values-ca/strings.xml +++ b/MyTracks/res/values-ca/strings.xml @@ -225,7 +225,6 @@ limitations under the License. Escriu una còpia completa de totes les rutes i els paràmetres a la targeta SD Restaura ara Restaura totes les rutes i els paràmetres des de la targeta SD. - No es poden gestionar les còpies de seguretat mentre s\'enregistra una ruta. La restauració d\'una còpia de seguretat des de la targeta SD sobreescriurà totes les rutes i paràmetres. Esteu segur? diff --git a/MyTracks/res/values-de/strings.xml b/MyTracks/res/values-de/strings.xml index 5c0809ca1..cfd7e873c 100644 --- a/MyTracks/res/values-de/strings.xml +++ b/MyTracks/res/values-de/strings.xml @@ -1,254 +1,259 @@ - + - - - Track aufzeichnen - Aufzeichnung anhalten - Satellitenansicht - Kartenansicht - Mein Aufenthaltsort +--> + + + Track aufzeichnen + Aufzeichnung anhalten + Satellitenansicht + Kartenansicht + Mein Aufenthaltsort + Wegpunkte - Alle Tracks löschen - Einstellungen - Höhe - Bitte wähle einen Track zur Ansicht aus - Aktueller Track - Letztes Segment - Sensorstatus - Aggregierte Statistiken - - - Es wurde kein Dienst zur Positionsermittlung auf diesem Gerät gefunden. - Es wurde kein GPS-Empfänger auf diesem Gerät gefunden. - Beim Senden des Tracks an Google Fusion Tables ist ein Fehler aufgetreten. Bitte versuche es später noch einmal. - Beim Senden des Tracks an Google Docs ist ein Fehler aufgetreten. Bitte versuche es später noch einmal. - Start der Aufzeichnung fehlgeschlagen. Bitte versuche es noch einmal. - Anhalten unmöglich. Bitte versuche es noch einmal. - Konnte Datei nicht lesen. - Datei ist nicht im GPX-Format. - Ein Fehler ist aufgetreten. Bitte versuche es noch einmal. - Track existiert nicht. - Die Datei ist zu groß (maximal 500 KB). Operation abgebrochen. - Der Wegpunkt kann nicht einfügt werden. Keine Positionsbestimmung möglich. - - - Warte auf ein besseres GPS-Signal. Bitte stelle sicher, dass der Himmel sichtbar ist. - Ein neuer Track wird aufgezeichnet. - Die Aufzeichnung wurde angehalten. - Eine neue Tabelle wurde auf google.com/fusiontables erstellt. - Dein Track wurde auf maps.google.com gespeichert. - Dein Track wurde in einer neuen Tabelle bei docs.google.com gespeichert. - Dein Track wurde an docs.google.com gesendet. - Hier berühren, um zu den Sicherheits- und Positionseinstellungen zu gelangen. Aktiviere \'GPS-Satelliten\'. Drücke dann den Zurück-Knopf, um hierher zurückzukehren. - Anmerkung: Eine ungefähre Position wird angezeigt, bestimmt mittels Mobilfunkstationen und WiFi-Netzwerken. - Die Position kann derzeit nicht bestimmt werden. Aktiviere mehr Positionsquellen (siehe Sicherheits- und Positionseinstellungen) und stelle sicher, dass der Himmel sichtbar ist. - Ein Wegpunkt wurde eingefügt. - Ein Statistikwegpunkt wurde eingefügt. - - - Bitte warten… - Dein Track wird an Google gesendet... - Lese Track... - Bereite Track vor... - Anmeldung bei Google Fusion Tables... - Anmeldung bei Google Docs... - Track wird an Google Docs gesendet... - Track wird an Google Fusion Tables gesendet... - Neue \'Fusion Tabelle\' wird erstellt... - Importiere GPS-Track. Das kann ein wenig dauern. - Schreibe Datei auf SD-Karte. Das kann ein wenig dauern. - Aktualisiere Trackstatistik... - - - Bist Du Dir wirklich sicher? - Alle Tracks und Wegpunkte werden endgültig von diesem Gerät gelöscht. - Dieser Track und seine Wegpunkte werden endgültig von diesem Gerät gelöscht. - Dieser Wegpunkt wird endgültig von diesem Gerät gelöscht. - - - Erstellt von My Tracks für Android. + Alle Tracks löschen + Einstellungen + Höhe + Bitte wähle einen Track zur Ansicht aus + Aktueller Track + Letztes Segment + Sensorstatus + Aggregierte Statistiken + + + Es wurde kein Dienst zur Positionsermittlung auf diesem Gerät gefunden. + Es wurde kein GPS-Empfänger auf diesem Gerät gefunden. + Beim Senden des Tracks an Google Fusion Tables ist ein Fehler aufgetreten. Bitte versuche es später noch einmal. + Beim Senden des Tracks an Google Text & Tabellen ist ein Fehler aufgetreten. Bitte versuche es später noch einmal. + Start der Aufzeichnung fehlgeschlagen. Bitte versuche es noch einmal. + Anhalten unmöglich. Bitte versuche es noch einmal. + Konnte Datei nicht lesen. + Datei ist nicht im GPX-Format. + Ein Fehler ist aufgetreten. Bitte versuche es noch einmal. + Track existiert nicht. + Die Datei ist zu groß (maximal 500 KB). Operation abgebrochen. + Der Wegpunkt kann nicht einfügt werden. Keine Positionsbestimmung möglich. + + + Warte auf ein besseres GPS-Signal. Bitte stelle sicher, dass der Himmel sichtbar ist. + Ein neuer Track wird aufgezeichnet. + Die Aufzeichnung wurde angehalten. + Eine neue Tabelle wurde auf google.com/fusiontables erstellt. + Dein Track wurde auf maps.google.com gespeichert. + Dein Track wurde in einer neuen Tabelle bei docs.google.com gespeichert. + Dein Track wurde an docs.google.com gesendet. + Hier berühren, um zu den Sicherheits- und Positionseinstellungen zu gelangen. Aktiviere \'GPS-Satelliten\'. Drücke dann den Zurück-Knopf, um hierher zurückzukehren. + Anmerkung: Eine ungefähre Position wird angezeigt, bestimmt mittels Mobilfunkstationen und WiFi-Netzwerken. + Die Position kann derzeit nicht bestimmt werden. Aktiviere mehr Positionsquellen (siehe Sicherheits- und Positionseinstellungen) und stelle sicher, dass der Himmel sichtbar ist. + Ein Wegpunkt wurde eingefügt. + Ein Statistikwegpunkt wurde eingefügt. + + + Bitte warten… + Dein Track wird an Google gesendet... + Lese Track... + Bereite Track vor... + Anmeldung bei Google Fusion Tables... + Anmeldung bei Google Text & Tabellen... + Track wird an Google Text & Tabellen gesendet... + Track wird an Google Fusion Tables gesendet... + Neue \'Fusion Tabelle\' wird erstellt... + Importiere GPS-Track. Das kann ein wenig dauern. + Schreibe Datei auf SD-Karte. Das kann ein wenig dauern. + Aktualisiere Trackstatistik... + + + Bist Du Dir wirklich sicher? + Alle Tracks und Wegpunkte werden endgültig von diesem Gerät gelöscht. + Dieser Track und seine Wegpunkte werden endgültig von diesem Gerät gelöscht. + Dieser Wegpunkt wird endgültig von diesem Gerät gelöscht. + + + Erstellt von My Tracks für Android. Erstellt von <a href=\'http://mytracks.appspot.com\'>My Tracks</a> für Android. - - - Punkte - Aufgezeichnet am - Aktivität - Art des Wegpunktes - (Teil %d) - - + + + Punkte + Aufgezeichnet am + Aktivität + Art des Wegpunktes + (Teil %d) + + Dein Track wird aufgezeichnet… - - - Nein - Ja - Fehler - Erfolgreich - Wegpunkt + + + + Nein + Ja + + Fehler + Erfolgreich + Wegpunkt Lizenz Abbrechen Schließen Bearbeiten Sichern - - (Ende) - - - Anzeigen + + + (Ende) + + + Anzeigen An eine Google Fusion Tables-Karte senden - An eine neue Google Fusion Tables-Karte senden - An Google Docs senden - An Google senden… - Vom Gerät löschen… - Bearbeiten - Auf SD-Karte speichern… - GPX-Datei exportieren - KML-Datei exportieren - CSV-Datei exportieren - TCX-Datei exportieren - An Google Fusion Tables senden - Alle Tracks löschen + An eine neue Google Fusion Tables-Karte senden + An Google Text & Tabellen senden + An Google senden… + Vom Gerät löschen… + Bearbeiten + Auf SD-Karte speichern… + GPX-Datei exportieren + KML-Datei exportieren + CSV-Datei exportieren + TCX-Datei exportieren + An Google Fusion Tables senden + Alle Tracks löschen - Alle Tracks exportieren - Exportieren beendet - Einen Track auswählen. Lange berühren für mehr Optionen. - Alle Tracks - Dieser Track + Alle Tracks exportieren + Exportieren beendet + Einen Track auswählen. Lange berühren für mehr Optionen. + Alle Tracks + Dieser Track Mit Freunden teilen… - Eine GPX-Datei - Eine KML-Datei - Eine CSV-Datei - Eine TCX-Datei - Karte zurücksetzen. - - - Alle Wegpunkte löschen - Dieser Wegpunkt - Löschen - Details bearbeiten - Auf der Karte zeigen - Wegpunkt einfügen - Statistik einfügen - Wegpunkt auswählen. Lange berühren für mehr Optionen. - - - Trackdetails - Name - Beschreibung - Aktivität (z. B. Wandern, Fahrradfahren, Laufen, Klettern…) - - - Wegpunktdetails - Wegpunktname - Wegpunktbeschreibung - Art des Wegpunktes (z. B. Gebäude, Schlagloch, Sehenswürdigkeit…) - - - Einstellungen für My Tracks - Anzeige - Aufzeichnung - Mit Freunden teilen - - In dieser Android-Version nicht verfügbar - - Dauer zwischen Ansagen - Kürzeste Zeit zwischen zwei Ansagen - - Kleinste Entfernung zwischen Punkten - Kleinste Entfernung zwischen zwei Positionsaufzeichnungen - - Neues Segment, wenn Punkte weiter entfernt als - Der Track wird in zwei geteilt, wenn zwei folgende Punkte weiter entfernt sind als hier angegeben. - - Aufzeichnungsfrequenz - Kürzeste Zeit zwischen zwei Positionsaufzeichnungen - - Aufzeichnung pausieren, wenn Genauigkeit kleiner als - Die Aufzeichnung wird kurzfristig angehalten, wenn Deine Position nicht genauer als hier angegeben ermittelt werden kann. - - Entfernungen in metrischen Einheiten anzeigen - Alle Entfernungen werden in metrischen Einheiten angezeigt. - Alle Entfernungen werden in imperialen Längeneinheiten angezeigt. - - Karten auf maps.google.com veröffentlichen - Hochgeladene Karten auf maps.google.com sind für alle sichtbar. - Hochgeladene Karten auf maps.google.com werden nicht öffentlich sichtbar aufgelistet. - - Geschwindigkeitsanzeige - Geschwindigkeit wird angezeigt. - Pace wird angezeigt. - - Nur URL als Einladung zum Teilen senden - Beim Teilen mit Freunden wird nur die Karten-URL gesendet. - Beim Teilen mit Freunden wird der GPS-Track selbst gesendet. - - Zerteilungsfrequenz - Längste Dauer oder Entfernung, nach der automatisch ein Marker eingefügt wird. - - Zeitbegrenzung der Auto-Wiederaufnahme - Maximale Zeit in Minuten, nach der eine abgebrochene Aufzeichnung nach einem Telefonneustart wiederaufgenommen wird. - - Zeitstempel als Trackname - Der Trackname wird aus Datum und Zeit zum Aufzeichnungsbeginn zusammengesetzt. - Tracknamen erhalten eine fortlaufende Nummer. - - Pace - - Wird nur unter Android 2.0 oder höher unterstützt - Sensortyp + Ein Link zur Google Fusion Tables-Karte + Eine GPX-Datei + Eine KML-Datei + Eine CSV-Datei + Eine TCX-Datei + Karte zurücksetzen. + + + Alle Wegpunkte löschen + Dieser Wegpunkt + Löschen + Details bearbeiten + Auf der Karte zeigen + Wegpunkt einfügen + Statistik einfügen + Wegpunkt auswählen. Lange berühren für mehr Optionen. + + + Trackdetails + Name + Beschreibung + Aktivität (z. B. Wandern, Fahrradfahren, Laufen, Klettern…) + + + Wegpunktdetails + Wegpunktname + Wegpunktbeschreibung + Art des Wegpunktes (z. B. Gebäude, Schlagloch, Sehenswürdigkeit…) + + + Einstellungen für My Tracks + Anzeige + Aufzeichnung + Mit Freunden teilen + + In dieser Android-Version nicht verfügbar + + Dauer zwischen Ansagen + Kürzeste Zeit zwischen zwei Ansagen + + Kleinste Entfernung zwischen Punkten + Kleinste Entfernung zwischen zwei Positionsaufzeichnungen + + Neues Segment, wenn Punkte weiter entfernt als + Der Track wird in zwei geteilt, wenn zwei folgende Punkte weiter entfernt sind als hier angegeben. + + Aufzeichnungsfrequenz + Kürzeste Zeit zwischen zwei Positionsaufzeichnungen + + Aufzeichnung pausieren, wenn Genauigkeit kleiner als + Die Aufzeichnung wird kurzfristig angehalten, wenn Deine Position nicht genauer als hier angegeben ermittelt werden kann. + + Entfernungen in metrischen Einheiten anzeigen + Alle Entfernungen werden in metrischen Einheiten angezeigt. + Alle Entfernungen werden in imperialen Längeneinheiten angezeigt. + + Karten auf maps.google.com veröffentlichen + Hochgeladene Karten auf maps.google.com sind für alle sichtbar. + Hochgeladene Karten auf maps.google.com werden nicht öffentlich sichtbar aufgelistet. + + Geschwindigkeitsanzeige + Geschwindigkeit wird angezeigt. + Pace wird angezeigt. + + Nur URL als Einladung zum Teilen senden + Beim Teilen mit Freunden wird nur die Karten-URL gesendet. + Beim Teilen mit Freunden wird der GPS-Track selbst gesendet. + + Zerteilungsfrequenz + Längste Dauer oder Entfernung, nach der automatisch ein Marker eingefügt wird. + + Zeitbegrenzung der Auto-Wiederaufnahme + Maximale Zeit in Minuten, nach der eine abgebrochene Aufzeichnung nach einem Telefonneustart wiederaufgenommen wird. + + Zeitstempel als Trackname + Der Trackname wird aus Datum und Zeit zum Aufzeichnungsbeginn zusammengesetzt. + Tracknamen erhalten eine fortlaufende Nummer. + + Pace + + Wird nur unter Android 2.0 oder höher unterstützt + Sensortyp Der Typ des zu verbindenden Sensors Verbindung mit ANT Herzqrequenzmesser aufheben Verbindung mit ANT Kraftmesser aufheben Verbindung mit ANT srm bridge aufheben ANT-Sensor nicht verbunden ANT-Sensor verbunden mit: %d - Bluetooth-Sensor - Der zu verbindende Bluetoothsensor - Art der Datenanzeige - - - Datensicherung (BETA) - SD-Karten-Sicherung - Jetzt sichern - Eine Kopie aller Deiner Tracks und Einstellungen auf die SD-Karte schreiben - Jetzt wiederherstellen - Alle Tracks und Einstellungen von der SD-Karte holen. - Während ein Track aufgezeichnet wird, kann nicht mit der Datensicherung gearbeitet werden. - - Eine Wiederherstellung von der SD-Karte ÜBERSCHREIBT alle vorhandenen Tracks und Einstellungen. Willst Du das wirklich? - Wähle die wiederherzustellende Datensicherung aus - Keine Datensicherungen gefunden. - Schreibe alle Tracks und Einstellungen auf die SD-Karte. Dies kann eine Weile dauern... - Lese alle Tracks und Einstellungen von der SD-Karte. Dies kann eine Weile dauern... - - - GPS-Satelliten + Bluetooth-Sensor + Der zu verbindende Bluetoothsensor + Art der Datenanzeige - + + Datensicherung (BETA) + SD-Karten-Sicherung + Jetzt sichern + Eine Kopie aller Deiner Tracks und Einstellungen auf die SD-Karte schreiben + Jetzt wiederherstellen + Alle Tracks und Einstellungen von der SD-Karte holen. + Während ein Track aufgezeichnet wird, kann nicht mit der Datensicherung gearbeitet werden. + + Eine Wiederherstellung von der SD-Karte ÜBERSCHREIBT alle vorhandenen Tracks und Einstellungen. Willst Du das wirklich? + Wähle die wiederherzustellende Datensicherung aus + Keine Datensicherungen gefunden. + Schreibe alle Tracks und Einstellungen auf die SD-Karte. Dies kann eine Weile dauern... + Lese alle Tracks und Einstellungen von der SD-Karte. Dies kann eine Weile dauern... + + + GPS-Satelliten + + Karte mit Freunden teilen - Ich möchte gerne eine Karte mit Dir teilen. + Ich möchte gerne eine Karte mit Dir teilen. Vielleicht interessierst Du Dich für diese Karte: - - %s - Ich möchte einen GPS-Track mit Dir teilen + + %s + Ich möchte einen GPS-Track mit Dir teilen Vielleicht interessierst Du Dich für diese Track: - - %s. + + Der Track kann mit Google Earth oder anderer Karten-Software visualisiert werden. Track mit Freunden teilen @@ -256,126 +261,145 @@ limitations under the License. Nicht in Suchergebnissen enthalten. Du hast noch keine Karten. - - - Durchschnitt in Bew. - Pace in Bew. - - - Durchschnittsgeschw. - Durchschnitts-Pace - - Höhenprofil - Höhengewinn - Höhe - Breitengrad - Längengrad - - - Niedrigste Höhenlage - Minimale Steigung - Minimaler Pace - - - Höchste Höhenlage - Maximale Steigung - Max. Geschwindigkeit - - Lade Track… - - - Diagramm-Einstellungen - - X-Achse - Nach Entfernung - Nach Zeit - - - Y-Achse - - - - Stunde - Stunden - Std - Minute - Minuten - Sekunde - Sekunden - - + + + Durchschnitt in Bew. + Pace in Bew. + + + Durchschnittsgeschw. + Durchschnitts-Pace + + Höhenprofil + Höhengewinn + Höhe + Breitengrad + Längengrad + + + Niedrigste Höhenlage + Minimale Steigung + Minimaler Pace + + + Höchste Höhenlage + Maximale Steigung + Max. Geschwindigkeit + + Lade Track… + + + Diagramm-Einstellungen + + X-Achse + Nach Entfernung + Nach Zeit + + + Y-Achse + + + + Stunde + Stunden + Std + Min + Minute + Minuten + Sekunde + Sekunden + + + + + Kilometer Fuß - Meilen - - - Kilometer pro Stunde - pro Kilometer - Meilen pro Stunde - pro Meile + + Meilen - - Zeit in Bewegung - Gesamtzeit - Zeit - SD-Karte nicht verfügbar - Geschwindigkeit - Du musst erst eine Aufzeichnung beginnen. - Statistik - Gesamtentfernung - Entfernung - Kraft - Kadenz - Herzfrequenz - Höchste Herzfrequenz - Bitte warten… - - - Lizenzvereinbarung für Endverbraucher - Akzeptieren - Ablehnen - - - Über - Hilfe + + + Kilometer pro Stunde + + pro Kilometer + + Meilen pro Stunde + + pro Meile + + + + + + + + + Zeit in Bewegung + Gesamtzeit + Zeit + SD-Karte nicht verfügbar + Geschwindigkeit + Du musst erst eine Aufzeichnung beginnen. + Statistik + Gesamtentfernung + Entfernung + Kraft + Kadenz + Herzfrequenz + Höchste Herzfrequenz + + Bitte warten… + + + Lizenzvereinbarung für Endverbraucher + Akzeptieren + Ablehnen + + + Über + Hilfe + + Unterstützung: Datenschutz: - + Senden an… + Google Text & Tabellen - Jetzt senden - Trackstatistik senden - Statistik und Punkte senden - - - Kein Konto vorhanden - Kein Konto gefunden, um Tracks zu versenden. - Wähle ein Konto - - - Keine externes Speichermedium verfügbar. Stelle sicher, dass eine SD-Karte vorhanden ist und nicht bereits von einem Computer darauf zugegriffen wird. - Die Datei kann nicht geschrieben werden. - Die Datei konnte nicht gelesen werden. - Das Verzeichnis auf der SD-Karte kann nicht erstellt werden. - Der Track wurde auf die SD-Karte geschrieben. - Lesen von der SD-Karte beendet. - - - Suche nach neuer Version - Jetzt aktualisieren - Fortfahren, ohne zu aktualisieren - Deine Version von %1$s funktioniert möglicherweise nicht mehr richtig und wird nicht mehr unterstützt. Bitte aktualisiere %1$s. - Deine Version von %1$s funktioniert möglicherweise nicht mehr richtig. Du kannst sie jetzt aktualisieren, oder auf sie auf eigene Gefahr weiterverwenden. Einige Funktionen wie Datenaustausch mit Google-Diensten werden möglicherweise nicht mehr funktionieren, solange Du die Software nicht aktualisierst. - Eine neue Version von %1$s ist jetzt verfügbar. Du kannst Deine installierte Version ohne Probleme weiterverwenden, aber um die neuesten Funktionen in Anspruch nehmen zu können, musst Du die Software aktualisieren. - - - Willkommen + Jetzt senden + Trackstatistik senden + Statistik und Punkte senden + + + Kein Konto vorhanden + Kein Konto gefunden, um Tracks zu versenden. + Wähle ein Konto + + + Keine externes Speichermedium verfügbar. Stelle sicher, dass eine SD-Karte vorhanden ist und nicht bereits von einem Computer darauf zugegriffen wird. + Die Datei kann nicht geschrieben werden. + Die Datei konnte nicht gelesen werden. + Das Verzeichnis auf der SD-Karte kann nicht erstellt werden. + Der Track wurde auf die SD-Karte geschrieben. + Lesen von der SD-Karte beendet. + + + Suche nach neuer Version + Jetzt aktualisieren + Fortfahren, ohne zu aktualisieren + Deine Version von %1$s funktioniert möglicherweise nicht mehr richtig und wird nicht mehr unterstützt. Bitte aktualisiere %1$s. + Deine Version von %1$s funktioniert möglicherweise nicht mehr richtig. Du kannst sie jetzt aktualisieren, oder auf sie auf eigene Gefahr weiterverwenden. Einige Funktionen wie Datenaustausch mit Google-Diensten werden möglicherweise nicht mehr funktionieren, solange Du die Software nicht aktualisierst. + Eine neue Version von %1$s ist jetzt verfügbar. Du kannst Deine installierte Version ohne Probleme weiterverwenden, aber um die neuesten Funktionen in Anspruch nehmen zu können, musst Du die Software aktualisieren. + + + Willkommen OK - Über My Tracks - - - My Tracks keine Entfernung + Über My Tracks + + + My Tracks keine Entfernung - My Tracks %1$s %2$.2f %3$s in %4$s mit %5$s %6$s - - - Einheitenbenachrichtigung - Voreingestellt sind metrische Einheiten. Willst Du diese Einstellung ändern? - + --> + My Tracks %1$s %2$.2f %3$s in %4$s mit %5$s %6$s + + + Einheitenbenachrichtigung + Voreingestellt sind metrische Einheiten. Willst Du diese Einstellung ändern? + - Bluetooth-Einstellungen - Geräteverbindung - Öffnet Androids Bluetooth-Einstellungen, um Geräte zu verbinden. - Sensor-Einstellungen - - - Verbunden mit %s - Verbinde mit Bluetooth-Sensor ... - Bluetooth-Sensor nicht verbunden. - - - Uhrzeit - Kraftmesser aktiviert - Herzfrequenzmessung aktiviert - Keine Sensoren verfügbar - Aktiviert - Verbinde... - Verbunden - Getrennt - Kein Sensor-Dienst - Suche... - Sende... - - - Track-Aufzeichnungs-Benachrichtigungen empfangen - Aufgezeichnete Trackdaten lesen - Aufgezeichnete Trackdaten schreiben, Starten/Anhalten des Aufzeichnens von Tracks - Werde benachrichtigt, wenn die Aufzeichnung eines Tracks beginnt oder anhält. - Ermöglicht das Lesen von Trackdaten wie Messpunkten oder Wegpunkten. - Ermöglicht das Schreiben von Trackdaten wie beim Starten der Aufzeichnung oder beim Hinzufügen von Wegpunkten. - + Bluetooth + Bluetooth-Einstellungen + Geräteverbindung + Öffnet Androids Bluetooth-Einstellungen, um Geräte zu verbinden. + + Sensor-Einstellungen + + + Verbunden mit %s + Verbinde mit Bluetooth-Sensor ... + Bluetooth-Sensor nicht verbunden. + + + Uhrzeit + Kraftmesser aktiviert + Herzfrequenzmessung aktiviert + Keine Sensoren verfügbar + Aktiviert + Verbinde... + Verbunden + Getrennt + Kein Sensor-Dienst + Suche... + Sende... + + + Track-Aufzeichnungs-Benachrichtigungen empfangen + Aufgezeichnete Trackdaten lesen + Aufgezeichnete Trackdaten schreiben, Starten/Anhalten des Aufzeichnens von Tracks + Werde benachrichtigt, wenn die Aufzeichnung eines Tracks beginnt oder anhält. + Ermöglicht das Lesen von Trackdaten wie Messpunkten oder Wegpunkten. + Ermöglicht das Schreiben von Trackdaten wie beim Starten der Aufzeichnung oder beim Hinzufügen von Wegpunkten. + diff --git a/MyTracks/res/values-es/strings.xml b/MyTracks/res/values-es/strings.xml index e4cc06155..f9e786542 100644 --- a/MyTracks/res/values-es/strings.xml +++ b/MyTracks/res/values-es/strings.xml @@ -212,7 +212,6 @@ limitations under the License. Hacer una copia total de sus recorridos y configuraciones a la tarjeta SD Restaurar Restaurar todos los recorridos y configuraciones desde la tarjeta SD - No se pueden manejar backups mientras se está registrando un recorrido. Restaurar un backup desde la tarjeta SD SOBREESCRIBIRÁ todos sus recorridos y configuraciones actuales. ¿Está usted seguro? Seleccionar el backup para restaurar diff --git a/MyTracks/res/values-fr/strings.xml b/MyTracks/res/values-fr/strings.xml index 86c470297..f2e634c30 100644 --- a/MyTracks/res/values-fr/strings.xml +++ b/MyTracks/res/values-fr/strings.xml @@ -213,8 +213,6 @@ limitations under the License. Copier tous vos parcours et paramètres sur la carte SD Restaurer Restaurer tous les parcours et paramètres depuis la carte SD - Sauvegarde impossible lors de l\'enregistrement d\'un parcours. - Restaurer une sauvegarde depuis la carte SD écrasera tous vos parcours et paramètres actuels. Choisissez la sauvegarde à restaurer diff --git a/MyTracks/res/values-ja/arrays.xml b/MyTracks/res/values-ja/arrays.xml index a3b6e1fde..582056996 100644 --- a/MyTracks/res/values-ja/arrays.xml +++ b/MyTracks/res/values-ja/arrays.xml @@ -287,4 +287,11 @@ limitations under the License. 噴水 ウェイポイント + + + なし + ANT+ センサー + SRM ANT+ ブリッジ + Zephyr Bluetooth + diff --git a/MyTracks/res/values-ja/strings.xml b/MyTracks/res/values-ja/strings.xml index 52719208a..2a8023462 100644 --- a/MyTracks/res/values-ja/strings.xml +++ b/MyTracks/res/values-ja/strings.xml @@ -35,6 +35,7 @@ limitations under the License. この端末でロケーションマネージャを発見できませんでした. この端末でGPSロケーションプロバイダを発見できました. + Googleマイマップへのトラック送信でエラーが発生しました.しばらく待って試して下さい. Google Fusion Tablesへのトラック送信でエラーが発生しました.しばらく待って試して下さい. Google Docsへのトラック送信でエラーが発生しました.しばらく待って試して下さい. 記録作成開始できません.もう一度試して下さい. @@ -50,11 +51,13 @@ limitations under the License. GPS位置精度がよくなるまで待機しています.空が見えるところにいて下さい. 新しいトラックを記録中です. トラック記録を停止しました. + 新しいマイマップをmap.google.comに作成しました. 新しいGoogle Fusion Tableをgoogle.com/fusiontablesに作成しました. トラックをmaps.google.comに送信しました. トラックをdocs.google.comの新しいスプレッドシートに送信しました. トラックをdocs.google.comに送信しました. - ここをタップして「位置情報とセキュリティの設定」を開いて「GPS機能を使用」にチェックして下さい.バックボタンで戻ってください. + 新しいトラックが表示されるまでに数分かかることがあります. + ここをタップして「位置情報とセキュリティの設定」を開いて「GPS機能を使用」にチェックを入れて下さい.バックボタンで戻ってください. ご注意: 携帯電話中継局とWi-Fiネットワークから大まかな位置を特定します. 現在位置決定ができません.位置取得源を増やし(位置情報とセキュリティの設定 をご覧ください)、空が見えるようにして下さい.もう一度試して下さい. ウェイポイントマーカーを挿入しました. @@ -62,14 +65,17 @@ limitations under the License. お待ちください... - トラックをGoogleに送信しました. + トラックをGoogleに送信しています. トラックを読んでいます... トラックを準備しています... Google Fusion Tablesへの認証を行っています... + Googleマップへの認証を行っています... Google Docsへの認証を行っています... トラックをGoogle Docsに送信しています... + トラックをGoogleマップに送信しています... + 新しいマイマップを作成しています... トラックをFusion Tablesに送信しています... - 新しいFusion Tablesを生成しています... + 新しいFusionテーブルを生成しています... GPSトラックをインポートしています.若干時間がかかります. SDカードにデータを書き込んでいます.若干時間がかかります. トラック統計情報を更新しています... @@ -84,6 +90,10 @@ limitations under the License. Android用My Tracksで作成 Android用<a href=\'http://mytracks.appspot.com\'>My Tracks</a>で作成しました. + + Android用<a href=\'http://mytracks.appspot.com\'>My Tracks</a>で作成しました. + 新規マップ + ポイント 記録日 @@ -113,10 +123,12 @@ limitations under the License. 表示 - Google Fusion Tablesへ送信 - 新規Google Fusion Tableへ送信 - Google Docsへ送信 - Googleへ送信... + Googleマイマップに送信 + 新規Googleマイマップに送信 + Google Fusion Tablesに送信 + 新規Google Fusion Tableに送信 + Google Docsに送信 + Googleに送信... 端末から削除... 詳細編集 SDへの書き込み... @@ -124,20 +136,33 @@ limitations under the License. KMLファイルの保存 CVSファイルの保存 TCXファイルの保存 - Google Fusion Tablesへの送信 - 全トラックの削除 + Google Fusion Tablesに送信 + Googleマイマップに送信 + 全て削除 - 全トラックの出力 + 全て出力 出力終了 トラックを選択します.オプションを表示するには長押しして下さい. - 全てのトラック: + 全トラック: このトラック 共有... + マップへのリンク GPXファイル KMLファイル CSVファイル TCXファイル - マップの消去 + マップのクリア + + 全てインポート + インポート終了 + + %3$s からGPXファイル %2$d件のうち %1$d件をインポートしました + %s ではGPXファイルがありません.SDカードを読み込めないかディレクトリにGPXファイルが無いかも知れません. + ファイル %s のインポートエラー 全てのマーカーの削除 @@ -190,7 +215,7 @@ limitations under the License. マップの公開 アップロードされたマップはmaps.google.comで誰でも閲覧できます - アップロードされたマップはmaps.google.comのリストに公開されません. + アップロードされたマップはmaps.google.comのリストに公開されません 速度表示 速度が表示されます @@ -231,7 +256,6 @@ limitations under the License. SDカードに全てのトラックと設定をバックアップします 今すぐ復元 SDカードから全てのトラックと設定を復元します - トラック記録中はバックアップ操作はできません. SDカードからの復元を実行すると、現在のトラックと設定は *全て上書き* されてしまいます.よろしいですか? 復元するバックアップの選択 @@ -242,6 +266,23 @@ limitations under the License. GPS衛星 + + アカウントからのマップ選択: + 新規マップ作成... + Googleマイマップに送信 + マップをロード中... + 今回はマップをロードできませんでした! + このマップを選択 + マップ詳細編集 + トラック共有 + アカウントからマップを削除 + マップタイトル: + マップ説明: + このマップまたはコンテンツを検索結果に表示 + このマップまたはコンテンツを検索結果に表示しない(直接URLで見られる可能性があります) + このマップまたはコンテンツは検索結果に表示されます + このマップもコンテンツも検索結果に表示されませんが、直接リンクで見られる可能性があります + マップ共有 マップを共有したいと思います @@ -332,7 +373,6 @@ limitations under the License. ケイデンス 心拍数 最大心拍数 - - 待機中... @@ -350,8 +390,10 @@ limitations under the License. 送信... - Googleマイマップ + Googleマイマップ 今すぐ送信 + 新規マップ作成 + 既存マップ選択 トラック統計情報送信 統計情報とポイント送信 @@ -364,7 +406,7 @@ limitations under the License. 外部ストレージを発見できませんでした.SDカードが刺さっているか、端末がコンピュータにマウントされていないか確認してください. ファイルへの書き込みができません. ファイルからの読み込みができません. - SDカードにディレクトリを作成できません. + SDカード上でディレクトリを作成できません. SDカードへの書き込み終了. SDカードからの読み込み終了. @@ -397,11 +439,11 @@ limitations under the License. デフォルトの単位はメートルです.設定を変更しますか? - Bluetoothオプション ペアリング ペアリングを行うには「Bluetooth設定」を開いて下さい bluetoothSensorsEnabled + ANTオプション センサーオプション @@ -425,7 +467,7 @@ limitations under the License. トラック記録通知受信 トラック読み込み - トラック書き込み トラック記録の開始・終了 + トラック書き込みとトラック記録の開始・終了 トラック記録の開始・終了時に通知されます. ポイントやウェイポイントといったトラックの読み込みを可能にします. 記録開始やウェイポイント追加といったトラック記録を可能にします. diff --git a/MyTracks/res/values-pl/strings.xml b/MyTracks/res/values-pl/strings.xml index 63b9cde9c..d019ce3b4 100644 --- a/MyTracks/res/values-pl/strings.xml +++ b/MyTracks/res/values-pl/strings.xml @@ -217,7 +217,6 @@ limitations under the License. Tworzy kopię zapasową wszystkich tras i ustawień i zapisuje je na karcie SD Odtwórz z kopii Odtwarza wszystkie trasy i ustawienia z kopii zapasowej z karty SD - Nie można tworzyć/odtwarzać kopii zapasowej podczas rejestrowania trasy. Odtworzenie z kopii zapasowej z karty SD zastąpi wszystkie bieżące trasy i ustawienia. Jesteś tego pewien? diff --git a/MyTracks/res/values-pt-rBR/strings.xml b/MyTracks/res/values-pt-rBR/strings.xml index f9ee674ca..2f60a4a9c 100644 --- a/MyTracks/res/values-pt-rBR/strings.xml +++ b/MyTracks/res/values-pt-rBR/strings.xml @@ -157,6 +157,8 @@ limitations under the License. Exibição Gravação Compartilhamento + Não disponível na sua versão do Android + Não disponível durante a gravação de uma trilha Tempo entre anúncios Tempo mínimo entre dois anúncios de voz Dist. mín. entre pontos diff --git a/MyTracks/res/values-zh-rCN/strings.xml b/MyTracks/res/values-zh-rCN/strings.xml index 0da162428..befedd9c7 100644 --- a/MyTracks/res/values-zh-rCN/strings.xml +++ b/MyTracks/res/values-zh-rCN/strings.xml @@ -219,7 +219,6 @@ limitations under the License. 将你所有轨迹和设置的完整备份写入到 SD 存储卡 立即恢复 从 SD 存储卡恢复所有轨迹和设置 - 在记录轨迹时无法处理备份数据. 从 SD 存储卡恢复备份数据将覆盖掉你当前的所有轨迹和设置. 确定要继续吗? diff --git a/MyTracks/res/values/do_not_translate.xml b/MyTracks/res/values/do_not_translate.xml index 861738bf7..116175d63 100644 --- a/MyTracks/res/values/do_not_translate.xml +++ b/MyTracks/res/values/do_not_translate.xml @@ -34,4 +34,9 @@ limitations under the License. ANT SRM_ANT_BRIDGE ZEPHYR + + + google.com/fusiontables + maps.google.com + docs.google.com diff --git a/MyTracks/res/values/keys.xml b/MyTracks/res/values/keys.xml index 54c476596..01f56da24 100644 --- a/MyTracks/res/values/keys.xml +++ b/MyTracks/res/values/keys.xml @@ -50,8 +50,7 @@ limitations under the License. backupToSd restoreFromSd - - + resetToDefaults sensorOptions antOptions diff --git a/MyTracks/res/values/strings.xml b/MyTracks/res/values/strings.xml index 40eca8a08..8d7c37b0f 100644 --- a/MyTracks/res/values/strings.xml +++ b/MyTracks/res/values/strings.xml @@ -35,9 +35,10 @@ limitations under the License. No location manager was found on this device! No GPS location provider was found on this device. - There was an error while sending your track to Google My Map. Please try again later. - There was an error while sending your track to Google Fusion Tables. Please try again later. - There was an error while sending your track to Google Docs. Please try again later. + There was an error while sending your track to Google My Map. + There was an error while sending your track to Google Fusion Tables. + There was an error while sending your track to Google Docs. + Please try again later. Unable to start recording. Try again. Unable to stop recording. Try again. Unable to read from file. @@ -56,12 +57,13 @@ limitations under the License. Your track has been sent to maps.google.com. Your track has been sent to a new spreadsheet at docs.google.com. Your track has been sent to docs.google.com. - It may sometimes take a few minutes before new tracks appear. + It may take a few minutes before new tracks appear. Please tap here to go to Security and Location Settings and check \'enable GPS satellites\'. Press back button to return. Note: An approximate location as determined from cell towers and WIFI networks is shown. A location can currently not be determined. Enable more location sources (see Security and Location Settings) and make sure the sky is visible. Try again later. A way point marker was inserted. A statistics marker was inserted. + Upload status: Please wait... @@ -194,6 +196,7 @@ limitations under the License. Sharing Not available in your version of Android + Not available while recording a track Announcement frequency Minimum time between two voice announcements @@ -236,6 +239,11 @@ limitations under the License. The default track name will be the date and time at the start of the track Track names will have numbers which increment for each new track. + Reset to defaults + Revert all your settings to the default values + All your settings will be erased and reverted to the default values. Saved tracks will be preserved. Are you sure? + All settings have been reverted to their defaults. + Pace Only supported in Android 2.0 or above @@ -257,7 +265,6 @@ limitations under the License. Write a full copy of all your tracks and settings to the SD card Restore now Restore all tracks and settings from the SD card - Cannot manipulate backups while recording a track. Restoring a backup from the SD card will OVERWRITE all your current tracks and settings. Are you sure? Select the backup to restore @@ -276,7 +283,7 @@ limitations under the License. Your maps could not be loaded at this time! Select this Map Edit Map Details - Share Track with Friends + Share Track Delete Map from your Account Map Title: Map Description: @@ -295,7 +302,6 @@ limitations under the License. I think you might be interested in this track. To view this track use Google Earth or other mapping software. - Share track with friends Public diff --git a/MyTracks/res/xml/preferences.xml b/MyTracks/res/xml/preferences.xml index f377cf863..e4d0b093f 100644 --- a/MyTracks/res/xml/preferences.xml +++ b/MyTracks/res/xml/preferences.xml @@ -173,4 +173,11 @@ limitations under the License. android:persistent="false" /> + + + diff --git a/MyTracks/src/com/google/android/apps/mytracks/DialogManager.java b/MyTracks/src/com/google/android/apps/mytracks/DialogManager.java index 740534e84..57c107a97 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/DialogManager.java +++ b/MyTracks/src/com/google/android/apps/mytracks/DialogManager.java @@ -1,12 +1,12 @@ /* * Copyright 2010 Google Inc. - * + * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of * the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the @@ -15,20 +15,22 @@ */ package com.google.android.apps.mytracks; +import static com.google.android.apps.mytracks.Constants.TAG; + +import com.google.android.apps.mytracks.io.sendtogoogle.SendDialog; import com.google.android.maps.mytracks.R; +import android.app.Activity; import android.app.AlertDialog; import android.app.Dialog; import android.app.ProgressDialog; -import android.content.DialogInterface; import android.os.Bundle; import android.util.Log; -import android.view.View; import android.view.WindowManager.BadTokenException; /** * A class to handle all dialog related events for My Tracks. - * + * * @author Sandor Dornbush */ public class DialogManager { @@ -37,14 +39,12 @@ public class DialogManager { public static final int DIALOG_IMPORT_PROGRESS = 2; public static final int DIALOG_PROGRESS = 3; public static final int DIALOG_SEND_TO_GOOGLE = 4; - public static final int DIALOG_SEND_TO_GOOGLE_RESULT = 5; - public static final int DIALOG_WRITE_PROGRESS = 6; + public static final int DIALOG_WRITE_PROGRESS = 5; private ProgressDialog progressDialog; private ProgressDialog importProgressDialog; private ProgressDialog writeProgressDialog; - private SendToGoogleDialog sendToGoogleDialog; - private AlertDialog sendToGoogleResultDialog; + private SendDialog sendToGoogleDialog; private ChartSettingsDialog chartSettingsDialog; private MyTracks activity; @@ -52,7 +52,7 @@ public class DialogManager { public DialogManager(MyTracks activity) { this.activity = activity; } - + protected Dialog onCreateDialog(int id, Bundle args) { switch (id) { case DIALOG_CHART_SETTINGS: @@ -77,26 +77,8 @@ public class DialogManager { progressDialog.setProgress(10); return progressDialog; case DIALOG_SEND_TO_GOOGLE: - sendToGoogleDialog = new SendToGoogleDialog(activity); + sendToGoogleDialog = new SendDialog(activity); return sendToGoogleDialog; - case DIALOG_SEND_TO_GOOGLE_RESULT: - AlertDialog.Builder builder = new AlertDialog.Builder(activity); - builder.setIcon(android.R.drawable.ic_dialog_info); - builder.setTitle("Title"); - builder.setMessage("Message"); - builder.setPositiveButton(activity.getString(R.string.ok), null); - builder.setNeutralButton(activity.getString(R.string.share_track), - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int which) { - SendToGoogleDialog sendToGoogleDialog = getSendToGoogleDialog(); - final boolean sentToMyMaps = sendToGoogleDialog.getSendToMyMaps(); - final boolean sentToFusionTables = sendToGoogleDialog.getSendToFusionTables(); - activity.shareLinkToMap(sentToMyMaps, sentToFusionTables); - dialog.dismiss(); - } - }); - sendToGoogleResultDialog = builder.create(); - return sendToGoogleResultDialog; case DIALOG_WRITE_PROGRESS: writeProgressDialog = new ProgressDialog(activity); writeProgressDialog.setIcon(android.R.drawable.ic_dialog_info); @@ -115,26 +97,8 @@ public class DialogManager { case DIALOG_SEND_TO_GOOGLE: activity.resetSendToGoogleStatus(); break; - case DIALOG_SEND_TO_GOOGLE_RESULT: - boolean success = activity.getSendToGoogleSuccess(); - sendToGoogleResultDialog.setTitle( - success ? R.string.success : R.string.error); - sendToGoogleResultDialog.setIcon(success - ? android.R.drawable.ic_dialog_info - : android.R.drawable.ic_dialog_alert); - sendToGoogleResultDialog.setMessage(activity.getSendToGoogleResultMessage()); - - boolean canShare = - activity.getSendToFusionTablesTableId() != null && - activity.getSendToMyMapsMapId() != null; - View share = - sendToGoogleResultDialog.findViewById(android.R.id.button3); - if (share != null) { - share.setVisibility(canShare ? View.VISIBLE : View.GONE); - } - break; case DIALOG_CHART_SETTINGS: - Log.d(Constants.TAG, "MyTracks.onPrepare chart dialog"); + Log.d(TAG, "MyTracks.onPrepare chart dialog"); chartSettingsDialog.setup(activity.getChartActivity()); break; } @@ -151,7 +115,7 @@ public class DialogManager { } }); } - + public void setProgressValue(final int percent) { activity.runOnUiThread(new Runnable() { public void run() { @@ -167,7 +131,7 @@ public class DialogManager { /** * @return the sendToGoogleDialog */ - public SendToGoogleDialog getSendToGoogleDialog() { + public SendDialog getSendToGoogleDialog() { return sendToGoogleDialog; } @@ -196,9 +160,9 @@ public class DialogManager { } /** - * Just like showDialog, but will catch a BadTokenException that sometimes - * (very rarely) gets thrown. This might happen if the user hits the "back" - * button immediately after sending tracks to google. + * Just like showDialog, but will catch a {@link BadTokenException} that + * sometimes (very rarely) gets thrown. This might happen if the user hits + * the "back" button immediately after sending tracks to google. * * @param id the dialog id */ @@ -208,11 +172,27 @@ public class DialogManager { try { activity.showDialog(id); } catch (BadTokenException e) { - Log.w(Constants.TAG, - "Could not display dialog with id " + id, e); + Log.w(TAG, "Could not display dialog with id " + id, e); } catch (IllegalStateException e) { - Log.w(Constants.TAG, - "Could not display dialog with id " + id, e); + Log.w(TAG, "Could not display dialog with id " + id, e); + } + } + }); + } + + /** + * The equivalent of {@link #showDialogSafely(int)}, but for a specific + * dialog instance. + */ + public static void showDialogSafely(Activity activity, final Dialog dialog) { + activity.runOnUiThread(new Runnable() { + public void run() { + try { + dialog.show(); + } catch (BadTokenException e) { + Log.w(TAG, "Could not display dialog", e); + } catch (IllegalStateException e) { + Log.w(TAG, "Could not display dialog", e); } } }); 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 75c069677..486fa5022 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/MyTracks.java +++ b/MyTracks/src/com/google/android/apps/mytracks/MyTracks.java @@ -15,6 +15,7 @@ */ package com.google.android.apps.mytracks; +import static com.google.android.apps.mytracks.Constants.TAG; import static com.google.android.apps.mytracks.DialogManager.DIALOG_IMPORT_PROGRESS; import static com.google.android.apps.mytracks.DialogManager.DIALOG_PROGRESS; import static com.google.android.apps.mytracks.DialogManager.DIALOG_SEND_TO_GOOGLE; @@ -35,7 +36,12 @@ 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; +import com.google.android.apps.mytracks.io.sendtogoogle.SendType; import com.google.android.apps.mytracks.services.ITrackRecordingService; import com.google.android.apps.mytracks.services.StatusAnnouncerFactory; import com.google.android.apps.mytracks.services.TrackRecordingService; @@ -81,7 +87,9 @@ import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import javax.xml.parsers.ParserConfigurationException; @@ -188,7 +196,7 @@ public class MyTracks extends TabActivity implements OnTouchListener, @Override public void onServiceDisconnected(ComponentName className) { - Log.d(Constants.TAG, "MyTracks: Service now disconnected."); + Log.d(TAG, "MyTracks: Service now disconnected."); trackRecordingService = null; } }; @@ -233,7 +241,7 @@ public class MyTracks extends TabActivity implements OnTouchListener, // TODO: We catch Exception, because after eliminating the service process // all exceptions it may throw are no longer wrapped in a RemoteException. } catch (Exception e) { - Log.e(Constants.TAG, "MyTracks: Remote exception.", e); + Log.e(TAG, "MyTracks: Remote exception.", e); // Fall back to alternative check method. return isRecordingBasedOnSharedPreferences(); @@ -253,7 +261,7 @@ public class MyTracks extends TabActivity implements OnTouchListener, @Override protected void onCreate(Bundle savedInstanceState) { - Log.d(Constants.TAG, "MyTracks.onCreate"); + Log.d(TAG, "MyTracks.onCreate"); super.onCreate(savedInstanceState); instance = this; ApiFeatures apiFeatures = ApiFeatures.getInstance(); @@ -310,7 +318,7 @@ public class MyTracks extends TabActivity implements OnTouchListener, recordingTrackId = sharedPreferences.getLong( getString(R.string.recording_track_key), -1); sharedPreferences.registerOnSharedPreferenceChangeListener(this); - Log.d(Constants.TAG, "recordingTrackId: " + recordingTrackId + Log.d(TAG, "recordingTrackId: " + recordingTrackId + ", selectedTrackId: " + selectedTrackId); if (recordingTrackId > 0) { Intent startIntent = new Intent(this, TrackRecordingService.class); @@ -329,26 +337,22 @@ public class MyTracks extends TabActivity implements OnTouchListener, // Do nothing. } else if (action.equals(Intent.ACTION_VIEW)) { if (intent.getScheme() != null && intent.getScheme().equals("file")) { - Log.w(Constants.TAG, - "Received a VIEW intent with file scheme. Importing."); + Log.w(TAG, "Received a VIEW intent with file scheme. Importing."); importGpxFile(intent.getData().getPath()); } else { - Log.w(Constants.TAG, - "Received a VIEW intent with unsupported scheme " - + intent.getScheme()); + Log.w(TAG, "Received a VIEW intent with unsupported scheme " + intent.getScheme()); } } else { - Log.w(Constants.TAG, - "Received an intent with unsupported action " + action); + Log.w(TAG, "Received an intent with unsupported action " + action); } } else { - Log.d(Constants.TAG, "Received an intent with no action."); + Log.d(TAG, "Received an intent with no action."); } } @Override protected void onDestroy() { - Log.d(Constants.TAG, "MyTracks.onDestroy"); + Log.d(TAG, "MyTracks.onDestroy"); tryUnbindTrackRecordingService(); super.onDestroy(); } @@ -357,7 +361,7 @@ public class MyTracks extends TabActivity implements OnTouchListener, protected void onPause() { // Called when activity is going into the background, but has not (yet) been // killed. Shouldn't block longer than approx. 2 seconds. - Log.d(Constants.TAG, "MyTracks.onPause"); + Log.d(TAG, "MyTracks.onPause"); tryUnbindTrackRecordingService(); super.onPause(); } @@ -366,14 +370,14 @@ public class MyTracks extends TabActivity implements OnTouchListener, protected void onResume() { // Called when the current activity is being displayed or re-displayed // to the user. - Log.d(Constants.TAG, "MyTracks.onResume"); + Log.d(TAG, "MyTracks.onResume"); tryBindTrackRecordingService(); super.onResume(); } @Override protected void onStop() { - Log.d(Constants.TAG, "MyTracks.onStop"); + Log.d(TAG, "MyTracks.onStop"); // Clean up any temporary track files. TempFileCleaner.clean(); super.onStop(); @@ -437,9 +441,9 @@ public class MyTracks extends TabActivity implements OnTouchListener, try { insertWaypoint(WaypointCreationRequest.DEFAULT_STATISTICS); } catch (RemoteException e) { - Log.e(Constants.TAG, "Cannot insert statistics marker.", e); + Log.e(TAG, "Cannot insert statistics marker.", e); } catch (IllegalStateException e) { - Log.e(Constants.TAG, "Cannot insert statistics marker.", e); + Log.e(TAG, "Cannot insert statistics marker.", e); } return true; } @@ -600,7 +604,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 { @@ -640,8 +644,7 @@ public class MyTracks extends TabActivity implements OnTouchListener, } default: { - Log.w(Constants.TAG, - "Warning unhandled request code: " + requestCode); + Log.w(TAG, "Warning unhandled request code: " + requestCode); } } } @@ -695,19 +698,19 @@ public class MyTracks extends TabActivity implements OnTouchListener, InputStream is = new FileInputStream(fileName); trackIdsImported = GpxImporter.importGPXFile(is, providerUtils); } catch (SAXException e) { - Log.e(Constants.TAG, "Caught an unexpected exception.", e); + Log.e(TAG, "Caught an unexpected exception.", e); message = R.string.error_generic; } catch (ParserConfigurationException e) { - Log.e(Constants.TAG, "Caught an unexpected exception.", e); + Log.e(TAG, "Caught an unexpected exception.", e); message = R.string.error_generic; } catch (IOException e) { - Log.e(Constants.TAG, "Caught an unexpected exception.", e); + Log.e(TAG, "Caught an unexpected exception.", e); message = R.string.error_unable_to_read_file; } catch (NullPointerException e) { - Log.e(Constants.TAG, "Caught an unexpected exception.", e); + Log.e(TAG, "Caught an unexpected exception.", e); message = R.string.error_invalid_gpx_format; } catch (OutOfMemoryError e) { - Log.e(Constants.TAG, "Caught an unexpected exception.", e); + Log.e(TAG, "Caught an unexpected exception.", e); message = R.string.error_out_of_memory; } if (trackIdsImported != null && trackIdsImported.length > 0) { @@ -870,8 +873,7 @@ public class MyTracks extends TabActivity implements OnTouchListener, final String service) { auth = authMap.get(service); if (auth == null) { - Log.i(Constants.TAG, - "Creating a new authentication for service: " + service); + Log.i(TAG, "Creating a new authentication for service: " + service); auth = AuthManagerFactory.getAuthManager(this, Constants.GET_LOGIN, null, @@ -879,7 +881,7 @@ public class MyTracks extends TabActivity implements OnTouchListener, service); authMap.put(service, auth); } - Log.d(Constants.TAG, "Logging in to " + service + "..."); + Log.d(TAG, "Logging in to " + service + "..."); if (AuthManagerFactory.useModernAuthManager()) { runOnUiThread(new Runnable() { @Override @@ -906,7 +908,7 @@ public class MyTracks extends TabActivity implements OnTouchListener, final String service, final Account account) { auth.doLogin(new Runnable() { public void run() { - Log.d(Constants.TAG, "Loggin success for " + service + "!"); + Log.d(TAG, "Loggin success for " + service + "!"); onActivityResult(requestCode, RESULT_OK, results); } }, account); @@ -926,7 +928,7 @@ public class MyTracks extends TabActivity implements OnTouchListener, Toast.makeText(this, getString(R.string.error_unable_to_start_recording), Toast.LENGTH_SHORT).show(); - Log.w(Constants.TAG, "Unable to start recording.", e); + Log.w(TAG, "Unable to start recording.", e); } } @@ -958,7 +960,7 @@ public class MyTracks extends TabActivity implements OnTouchListener, // TODO: We catch Exception, because after eliminating the service process // all exceptions it may throw are no longer wrapped in a RemoteException. } catch (Exception e) { - Log.e(Constants.TAG, "Unable to stop recording.", e); + Log.e(TAG, "Unable to stop recording.", e); } Intent intent = new Intent(MyTracks.this, TrackDetails.class); intent.putExtra("trackid", currentTrackId); @@ -969,8 +971,7 @@ public class MyTracks extends TabActivity implements OnTouchListener, try { stopService(new Intent(MyTracks.this, TrackRecordingService.class)); } catch (SecurityException e) { - Log.e(Constants.TAG, - "Encountered a security exception when trying to stop service.", e); + Log.e(TAG, "Encountered a security exception when trying to stop service.", e); } trackRecordingService = null; } @@ -983,7 +984,7 @@ public class MyTracks extends TabActivity implements OnTouchListener, * TODO: Change this whole flow to an actual state machine. */ public void sendToGoogle() { - SendToGoogleDialog sendToGoogleDialog = + SendDialog sendToGoogleDialog = dialogManager.getSendToGoogleDialog(); if (sendToGoogleDialog == null) { return; @@ -999,12 +1000,12 @@ public class MyTracks extends TabActivity implements OnTouchListener, } else if (sendToGoogleDialog.getSendToDocs()) { authenticateToGoogleDocs(); } else { - Log.w(Constants.TAG, "Nowhere to upload to"); + Log.w(TAG, "Nowhere to upload to"); onSendToGoogleDone(); } } - private void sendToGoogleMapsOrPickMap(SendToGoogleDialog sendToGoogleDialog) { + private void sendToGoogleMapsOrPickMap(SendDialog sendToGoogleDialog) { if (!sendToGoogleDialog.getCreateNewMap()) { // Ask the user to choose a map to upload into Intent listIntent = new Intent(this, MyMapsList.class); @@ -1022,7 +1023,7 @@ public class MyTracks extends TabActivity implements OnTouchListener, setProgressMessage( R.string.progress_message_authenticating_mymaps); authenticate(results, Constants.AUTHENTICATE_TO_MY_MAPS, - MapsService.getServiceName()); + MyMapsConstants.SERVICE_NAME); // AUTHENTICATE_TO_MY_MAPS callback calls sendToGoogleMaps } @@ -1042,7 +1043,7 @@ public class MyTracks extends TabActivity implements OnTouchListener, } catch (RuntimeException e) { // If that fails whatever reasons we'll just log an error, but // continue. - Log.w(Constants.TAG, "Updating map id failed.", e); + Log.w(TAG, "Updating map id failed.", e); } } @@ -1059,7 +1060,7 @@ public class MyTracks extends TabActivity implements OnTouchListener, } private void onSendToGoogleMapsDone() { - SendToGoogleDialog sendToGoogleDialog = dialogManager.getSendToGoogleDialog(); + SendDialog sendToGoogleDialog = dialogManager.getSendToGoogleDialog(); if (sendToGoogleDialog.getSendToFusionTables()) { authenticateToFusionTables(null); } else if (sendToGoogleDialog.getSendToDocs()) { @@ -1096,7 +1097,7 @@ public class MyTracks extends TabActivity implements OnTouchListener, } catch (RuntimeException e) { // If that fails whatever reasons we'll just log an error, but // continue. - Log.w(Constants.TAG, "Updating table id failed.", e); + Log.w(TAG, "Updating table id failed.", e); } } @@ -1114,7 +1115,7 @@ public class MyTracks extends TabActivity implements OnTouchListener, } private void onSendToFusionTablesDone() { - SendToGoogleDialog sendToGoogleDialog = dialogManager.getSendToGoogleDialog(); + SendDialog sendToGoogleDialog = dialogManager.getSendToGoogleDialog(); if (sendToGoogleDialog.getSendToDocs()) { authenticateToGoogleDocs(); } else { @@ -1143,7 +1144,7 @@ public class MyTracks extends TabActivity implements OnTouchListener, } private void sendToGoogleDocs(final long trackId) { - Log.d(Constants.TAG, "Sending to Docs...."); + Log.d(TAG, "Sending to Docs...."); setProgressValue(50); setProgressMessage(R.string.progress_message_sending_docs); final SendToDocs sender = new SendToDocs(this, @@ -1169,34 +1170,57 @@ public class MyTracks extends TabActivity implements OnTouchListener, } private void onSendToGoogleDone() { - SendToGoogleDialog sendToGoogleDialog = dialogManager.getSendToGoogleDialog(); + SendDialog sendToGoogleDialog = dialogManager.getSendToGoogleDialog(); final boolean sentToMyMaps = sendToGoogleDialog.getSendToMyMaps(); final boolean sentToFusionTables = sendToGoogleDialog.getSendToFusionTables(); - dialogManager.dismissDialogSafely(DIALOG_PROGRESS); - runOnUiThread(new Runnable() { - public void run() { - if (shareRequested) { - Toast.makeText(MyTracks.this, getSendToGoogleResultMessage(), Toast.LENGTH_LONG) - .show(); - if (shareLinkToMap(sentToMyMaps, sentToFusionTables)) { - return; - } + // We've finished sending the track to the user-selected services. Now + // we tell them the results of the upload, and optionally share the track. + // There are a few different paths through this code: + // + // 1. The user pre-requested a share (shareRequested == true). We're going + // to display the result dialog *without* the share button (the share + // listener will be null). The OK button listener will initiate the + // share. + // + // 2. The user did not pre-request a share, and the set of services to + // which we succeeded in uploading the track are compatible with + // sharing. We'll display a share button (the share listener will be + // non-null), and will share the link if the user clicks it. + // + // 3. The user did not pre-request a share, and the set of services to + // which we succeeded in uploading the track are incompatible with + // sharing. We won't display a share button. + + final boolean canShare = getSendToFusionTablesTableId() != null + || getSendToMyMapsMapId() != null; + + DialogInterface.OnClickListener doShareListener = null; + if (canShare) { + doShareListener = new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + shareLinkToMap(sentToMyMaps, sentToFusionTables); } + }; + } - // If anything failed or sharing was not requested, show the dialog - dialogManager.showDialogSafely( - DialogManager.DIALOG_SEND_TO_GOOGLE_RESULT); - } - }); + DialogInterface.OnClickListener onOkListener = (canShare && shareRequested) + ? doShareListener : null; + DialogInterface.OnClickListener onShareListener = (canShare && !shareRequested) + ? doShareListener : null; + + AlertDialog sendToGoogleResultDialog = ResultDialogFactory.makeDialog(this, + makeSendToGoogleResults(sendToGoogleDialog), onOkListener, onShareListener); + DialogManager.showDialogSafely(this, sendToGoogleResultDialog); } boolean shareLinkToMap(boolean sentToMyMaps, boolean sentToFusionTables) { 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); @@ -1215,9 +1239,29 @@ public class MyTracks extends TabActivity implements OnTouchListener, return SendToFusionTables.getMapVisualizationUrl(track); } + /** + * Creates a list of {@link SendResult} instances based on the set of + * services selected in {@link SendDialog} and the results as known to + * this class. + */ + private List makeSendToGoogleResults(SendDialog dialog) { + List results = new ArrayList(); + if (dialog.getSendToMyMaps()) { + results.add(new SendResult(SendType.MYMAPS, sendToMyMapsSuccess)); + } + if (dialog.getSendToFusionTables()) { + results.add(new SendResult(SendType.FUSION_TABLES, sendToFusionTablesSuccess)); + } + if (dialog.getSendToDocs()) { + results.add(new SendResult(SendType.DOCS, sendToDocsSuccess)); + } + + return results; + } + String getSendToGoogleResultMessage() { StringBuilder message = new StringBuilder(); - SendToGoogleDialog sendToGoogleDialog = + SendDialog sendToGoogleDialog = dialogManager.getSendToGoogleDialog(); if (sendToGoogleDialog.getSendToMyMaps()) { message.append(sendToMyMapsMessage); @@ -1259,11 +1303,11 @@ public class MyTracks extends TabActivity implements OnTouchListener, * Binds to track recording service if it is running. */ private void tryBindTrackRecordingService() { - Log.d(Constants.TAG, + Log.d(TAG, "MyTracks: Trying to bind to track recording service..."); bindService(new Intent(this, TrackRecordingService.class), serviceConnection, 0); - Log.d(Constants.TAG, "MyTracks: ...bind finished!"); + Log.d(TAG, "MyTracks: ...bind finished!"); isBound = true; } @@ -1273,14 +1317,12 @@ public class MyTracks extends TabActivity implements OnTouchListener, */ private void tryUnbindTrackRecordingService() { if (isBound) { - Log.d(Constants.TAG, - "MyTracks: Trying to unbind from track recording service..."); + Log.d(TAG, "MyTracks: Trying to unbind from track recording service..."); try { unbindService(serviceConnection); - Log.d(Constants.TAG, "MyTracks: ...unbind finished!"); + Log.d(TAG, "MyTracks: ...unbind finished!"); } catch (IllegalArgumentException e) { - Log.d(Constants.TAG, - "MyTracks: Tried unbinding, but service was not registered.", e); + Log.d(TAG, "MyTracks: Tried unbinding, but service was not registered.", e); } isBound = false; } diff --git a/MyTracks/src/com/google/android/apps/mytracks/MyTracksMap.java b/MyTracks/src/com/google/android/apps/mytracks/MyTracksMap.java index 208670b94..4c71935fa 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/MyTracksMap.java +++ b/MyTracks/src/com/google/android/apps/mytracks/MyTracksMap.java @@ -15,6 +15,8 @@ */ package com.google.android.apps.mytracks; +import static com.google.android.apps.mytracks.Constants.TAG; + import com.google.android.apps.mytracks.content.MyTracksProviderUtils; import com.google.android.apps.mytracks.content.Track; import com.google.android.apps.mytracks.content.TrackPointsColumns; @@ -208,7 +210,7 @@ public class MyTracksMap extends MapActivity } while (cursor.moveToNext()); } } catch (RuntimeException e) { - Log.w(Constants.TAG, "Caught an unexpected exception.", e); + Log.w(TAG, "Caught an unexpected exception.", e); } finally { if (cursor != null) { cursor.close(); @@ -285,7 +287,7 @@ public class MyTracksMap extends MapActivity @Override public void onCreate(Bundle bundle) { - Log.d(Constants.TAG, "MyTracksMap.onCreate"); + Log.d(TAG, "MyTracksMap.onCreate"); super.onCreate(bundle); // The volume we want to control is the Text-To-Speech volume @@ -335,7 +337,7 @@ public class MyTracksMap extends MapActivity observer = new ContentObserver(contentHandler) { @Override public void onChange(boolean selfChange) { - Log.d(Constants.TAG, "MyTracksMap: ContentObserver.onChange"); + Log.d(TAG, "MyTracksMap: ContentObserver.onChange"); if (!isRecordingSelected()) { // No track, or one other than the recording track is selected, // don't bother. @@ -350,7 +352,7 @@ public class MyTracksMap extends MapActivity waypointObserver = new ContentObserver(contentHandler) { @Override public void onChange(boolean selfChange) { - Log.d(Constants.TAG, + Log.d(TAG, "MyTracksMap: ContentObserver.onChange waypoints"); if (!isATrackSelected()) { return; @@ -371,7 +373,7 @@ public class MyTracksMap extends MapActivity @Override protected void onDestroy() { - Log.d(Constants.TAG, "MyTracksMap.onDestroy"); + Log.d(TAG, "MyTracksMap.onDestroy"); if (updateTrackThread != null) { ApiFeatures.getInstance().getApiPlatformAdapter().stopHandlerThread( @@ -407,7 +409,7 @@ public class MyTracksMap extends MapActivity protected void onStart() { // Called after onCreate or onStop. // Will be followed by onRestart. - Log.d(Constants.TAG, "MyTracksMap.onStart"); + Log.d(TAG, "MyTracksMap.onStart"); super.onStart(); } @@ -416,7 +418,7 @@ public class MyTracksMap extends MapActivity // Called when activity is no longer visible to user. // Next either onStart, onDestroy or nothing will be called. // This method may never be called in low memory situations. - Log.d(Constants.TAG, "MyTracksMap.onStop"); + Log.d(TAG, "MyTracksMap.onStop"); super.onStop(); } @@ -424,7 +426,7 @@ public class MyTracksMap extends MapActivity protected void onRestart() { // Called when the current activity is being re-displayed. // Will be followed by onResume. - Log.d(Constants.TAG, "MyTracksMap.onRestart"); + Log.d(TAG, "MyTracksMap.onRestart"); super.onRestart(); } @@ -432,7 +434,7 @@ public class MyTracksMap extends MapActivity protected void onPause() { // Called when activity is going into the background, but has not (yet) been // killed. Shouldn't block longer than approx. 2 seconds. - Log.d(Constants.TAG, "MyTracksMap.onPause"); + Log.d(TAG, "MyTracksMap.onPause"); unregisterLocationAndSensorListeners(); unregisterContentObservers(); super.onPause(); @@ -442,7 +444,7 @@ public class MyTracksMap extends MapActivity protected void onResume() { // Called when the current activity is being displayed or re-displayed // to the user. - Log.d(Constants.TAG, "MyTracksMap.onResume"); + Log.d(TAG, "MyTracksMap.onResume"); super.onResume(); // Reload all preferences as they might have changed since last run. @@ -477,7 +479,7 @@ public class MyTracksMap extends MapActivity @Override protected void onSaveInstanceState(Bundle outState) { - Log.d(Constants.TAG, "MyTracksMap.onSaveInstanceState"); + Log.d(TAG, "MyTracksMap.onSaveInstanceState"); outState.putBoolean(KEY_HAVE_GOOD_FIX, haveGoodFix); outState.putBoolean(KEY_KEEP_MY_LOCATION_VISIBLE, keepMyLocationVisible); if (currentLocation != null) { @@ -488,7 +490,7 @@ public class MyTracksMap extends MapActivity @Override protected void onRestoreInstanceState(Bundle bundle) { - Log.d(Constants.TAG, "MyTracksMap.onRestoreInstanceState"); + Log.d(TAG, "MyTracksMap.onRestoreInstanceState"); if (bundle != null) { super.onRestoreInstanceState(bundle); haveGoodFix = bundle.getBoolean(KEY_HAVE_GOOD_FIX, false); @@ -528,7 +530,7 @@ public class MyTracksMap extends MapActivity alert(getString(R.string.error_no_gps_location_provider)); return; } else { - Log.d(Constants.TAG, "MyTracksMap: Using location provider " + Log.d(TAG, "MyTracksMap: Using location provider " + gpsProvider.getName()); } locationManager.requestLocationUpdates(gpsProvider.getName(), @@ -539,7 +541,7 @@ public class MyTracksMap extends MapActivity } catch (RuntimeException e) { // If anything at all goes wrong with getting a cell location do not // abort. Cell location is not essential to this app. - Log.w(Constants.TAG, + Log.w(TAG, "Could not register network location listener."); } } @@ -550,7 +552,7 @@ public class MyTracksMap extends MapActivity if (compass == null) { return; } - Log.d(Constants.TAG, + Log.d(TAG, "MyTracksMap: Now registering sensor listeners."); sensorManager.registerListener( sensorListener, compass, SensorManager.SENSOR_DELAY_UI); @@ -561,12 +563,12 @@ public class MyTracksMap extends MapActivity */ void unregisterLocationAndSensorListeners() { if (locationManager != null) { - Log.d(Constants.TAG, + Log.d(TAG, "MyTracksMap: Now unregistering location listeners."); locationManager.removeUpdates(locationListener); } if (sensorManager != null) { - Log.d(Constants.TAG, + Log.d(TAG, "MyTracksMap: Now unregistering sensor listeners."); sensorManager.unregisterListener(sensorListener); } @@ -704,7 +706,7 @@ public class MyTracksMap extends MapActivity * @param trackId a given track id */ public void setSelectedTrack(final long trackId) { - Log.d(Constants.TAG, "MyTracksMap.setSelectedTrack: " + Log.d(TAG, "MyTracksMap.setSelectedTrack: " + "selectedTrackId = " + selectedTrackId + ", trackId = " + trackId); if (selectedTrackId == trackId) { @@ -763,7 +765,7 @@ public class MyTracksMap extends MapActivity timestamp); variation = field.getDeclination(); - Log.d(Constants.TAG, + Log.d(TAG, "MyTracksMap: Variation reset to " + variation + " degrees."); } @@ -906,7 +908,7 @@ public class MyTracksMap extends MapActivity @Override public void onSharedPreferenceChanged( final SharedPreferences sharedPreferences, final String key) { - Log.d(Constants.TAG, + Log.d(TAG, "MyTracksMap.onSharedPreferenceChanged: " + key); if (key != null) { uiHandler.post(new Runnable() { @@ -954,7 +956,7 @@ public class MyTracksMap extends MapActivity } showCurrentLocation(); } else { - Log.d(Constants.TAG, + Log.d(TAG, "MyTracksMap: Network location update received."); } } @@ -1060,7 +1062,7 @@ public class MyTracksMap extends MapActivity // TODO: This shouldn't happen after adding currentSelectedTrackId, // but just to be safe until we have 100% confidence. if (samplingFrequency <= 0) { - Log.w(Constants.TAG, + Log.w(TAG, "readAllNewTrackPoints: samplingFreq <= 0, numTotalPoints = " + numTotalPoints + ", trackId = " + currentSelectedTrackId); samplingFrequency = 1; diff --git a/MyTracks/src/com/google/android/apps/mytracks/MyTracksSettings.java b/MyTracks/src/com/google/android/apps/mytracks/MyTracksSettings.java index 85e6a04b5..e16cd64dd 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/MyTracksSettings.java +++ b/MyTracks/src/com/google/android/apps/mytracks/MyTracksSettings.java @@ -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.Constants.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(); + } } diff --git a/MyTracks/src/com/google/android/apps/mytracks/content/MyTracksProvider.java b/MyTracks/src/com/google/android/apps/mytracks/content/MyTracksProvider.java index ece4f0065..7c437a172 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/content/MyTracksProvider.java +++ b/MyTracks/src/com/google/android/apps/mytracks/content/MyTracksProvider.java @@ -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; } diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/GpxTrackWriter.java b/MyTracks/src/com/google/android/apps/mytracks/io/GpxTrackWriter.java index 65636ce54..86c27bf9e 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/io/GpxTrackWriter.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/GpxTrackWriter.java @@ -1,12 +1,12 @@ /* * Copyright 2008 Google Inc. - * + * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of * the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the @@ -25,9 +25,10 @@ import android.location.Location; import java.io.OutputStream; import java.io.PrintWriter; import java.nio.charset.Charset; +import java.text.NumberFormat; import java.text.SimpleDateFormat; -import java.text.DecimalFormat; import java.util.Date; +import java.util.Locale; import java.util.TimeZone; /** @@ -36,22 +37,33 @@ import java.util.TimeZone; * @author Sandor Dornbush */ public class GpxTrackWriter implements TrackFormatWriter { + private static final String TIMESTAMP_FORMAT = "yyyy-MM-dd'T'HH:mm:ss'Z'"; - private static final int LATLONG_FORMAT = Location.FORMAT_DEGREES; - private static final DecimalFormat ELEVATION_FORMATTER = - new DecimalFormat("#.#"); - private static final SimpleDateFormat TIMESTAMP_FORMATTER = - new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); - static { - TIMESTAMP_FORMATTER.setTimeZone(TimeZone.getTimeZone("UTC")); - } - + private final NumberFormat elevationFormatter; + private final NumberFormat coordinateFormatter; + private final SimpleDateFormat timestampFormatter; private PrintWriter pw = null; private Track track; + public GpxTrackWriter() { + // GPX readers expect to see fractional numbers with US-style punctuation. + // That is, they want periods for decimal points, rather than commas. + elevationFormatter = NumberFormat.getInstance(Locale.US); + elevationFormatter.setMaximumFractionDigits(1); + elevationFormatter.setGroupingUsed(false); + + coordinateFormatter = NumberFormat.getInstance(Locale.US); + coordinateFormatter.setMaximumFractionDigits(5); + coordinateFormatter.setMaximumIntegerDigits(3); + coordinateFormatter.setGroupingUsed(false); + + timestampFormatter = new SimpleDateFormat(TIMESTAMP_FORMAT); + timestampFormatter.setTimeZone(TimeZone.getTimeZone("UTC")); + } + private String formatLocation(Location l) { - return "lat=\"" + Location.convert(l.getLatitude(), LATLONG_FORMAT) - + "\" lon=\"" + Location.convert(l.getLongitude(), LATLONG_FORMAT) + "\""; + return "lat=\"" + coordinateFormatter.format(l.getLatitude()) + + "\" lon=\"" + coordinateFormatter.format(l.getLongitude()) + "\""; } @Override @@ -129,8 +141,8 @@ public class GpxTrackWriter implements TrackFormatWriter { if (pw != null) { pw.println(""); Date d = new Date(l.getTime()); - pw.println("" + ELEVATION_FORMATTER.format(l.getAltitude()) + ""); - pw.println(""); + pw.println("" + elevationFormatter.format(l.getAltitude()) + ""); + pw.println(""); pw.println(""); } } @@ -149,8 +161,8 @@ public class GpxTrackWriter implements TrackFormatWriter { Location l = waypoint.getLocation(); if (l != null) { pw.println(""); - pw.println("" + ELEVATION_FORMATTER.format(l.getAltitude()) + ""); - pw.println(""); + pw.println("" + elevationFormatter.format(l.getAltitude()) + ""); + pw.println(""); pw.println("" + StringUtils.stringAsCData(waypoint.getName()) + ""); pw.println("" 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 36e017bc2..4145f3a6f 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/io/SendToMyMaps.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/SendToMyMaps.java @@ -23,7 +23,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; @@ -112,7 +111,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); @@ -149,11 +148,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/TcxTrackWriter.java b/MyTracks/src/com/google/android/apps/mytracks/io/TcxTrackWriter.java index dfd3513ad..cf965b4c6 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/io/TcxTrackWriter.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/TcxTrackWriter.java @@ -1,12 +1,12 @@ /* * Copyright 2010 Google Inc. - * + * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of * the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the @@ -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(" "); - pw.print(" "); - pw.print(" "); - pw.print(TIMESTAMP_FORMAT.format(track.getStatistics().getStartTime())); - pw.println(""); - pw.println(" "); + pw.format(" \n", categoryToTcxSport(track.getCategory())); + pw.format(" %s\n", startTime); + pw.format(" \n", startTime); pw.print(" "); pw.print(track.getStatistics().getTotalTime() / 1000); pw.println(""); @@ -113,7 +128,7 @@ public class TcxTrackWriter implements TrackFormatWriter { // Calories are a required element just put in 0. pw.print("0"); pw.println("Active"); - pw.println("Manual)"); + pw.println("Manual"); } @Override @@ -130,7 +145,7 @@ public class TcxTrackWriter implements TrackFormatWriter { } pw.println(" "); Date d = new Date(location.getTime()); - pw.println(" "); + pw.println(" "); pw.println(" "); pw.print(" "); @@ -194,10 +209,18 @@ public class TcxTrackWriter implements TrackFormatWriter { } pw.println(" "); pw.print(" "); - pw.print(""); - pw.print(Build.MODEL); - pw.print(""); - pw.println(")"); + pw.format("%s\n", Build.MODEL); + + // The following code is correct. ID is inconsistently capitalized in the + // TCX schema. + pw.println("0"); + pw.println("0"); + + pw.println(""); + pw.println("0"); + pw.println("0"); + pw.println(""); + pw.println(""); pw.println(" "); pw.println(" "); } @@ -207,8 +230,31 @@ public class TcxTrackWriter implements TrackFormatWriter { if (pw == null) { return; } - pw.print(" "); - pw.print("My Tracks by Google"); + pw.println(" "); + + // 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("My Tracks %s by Google\n", MyTracksUtils.getMyTracksVersion(context)); + + pw.println(""); + + // 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(""); + pw.format("%d\n", MyTracksUtils.getMyTracksVersion(context)); + pw.format("%d\n", MyTracksUtils.isRelease(context) ? 0 : 1); + pw.println(""); + + pw.format("%s\n", MyTracksUtils.isRelease(context) ? TCX_TYPE_RELEASE + : TCX_TYPE_INTERNAL); + pw.println(""); + pw.format("%s\n", Locale.getDefault().getLanguage()); + pw.println("000-00000-00"); pw.println(""); pw.println(""); } @@ -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; + } + } } diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/TrackWriterFactory.java b/MyTracks/src/com/google/android/apps/mytracks/io/TrackWriterFactory.java index 55261f8a5..785454b0b 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/io/TrackWriterFactory.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/TrackWriterFactory.java @@ -1,12 +1,12 @@ /* * Copyright 2010 Google Inc. - * + * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of * the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the @@ -53,7 +53,7 @@ public class TrackWriterFactory { TCX { @Override public TrackFormatWriter newFormatWriter(Context context) { - return new TcxTrackWriter(); + return new TcxTrackWriter(context); } }; 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"); + } + } + } +} diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/sendtogoogle/ResultDialogFactory.java b/MyTracks/src/com/google/android/apps/mytracks/io/sendtogoogle/ResultDialogFactory.java new file mode 100644 index 000000000..f4d77892e --- /dev/null +++ b/MyTracks/src/com/google/android/apps/mytracks/io/sendtogoogle/ResultDialogFactory.java @@ -0,0 +1,106 @@ +/* + * Copyright 2011 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.google.android.apps.mytracks.io.sendtogoogle; + +import com.google.android.maps.mytracks.R; + +import android.app.Activity; +import android.app.AlertDialog; +import android.app.Dialog; +import android.content.DialogInterface; +import android.view.View; +import android.widget.ListView; + +import java.util.List; + +/** + * Creates the dialog used to display the results of sending track data to + * Google. The dialog lists the services to which data was uploaded, along + * with an indicator of success/failure. If possible, a button is displayed + * offering to share the uploaded track. + * + * Implementation note: This class is a factory, rather than a {@link Dialog} + * subclass, because alert dialogs have to be created with + * {@link AlertDialog.Builder}. Attempts to subclass {@link AlertDialog} + * directly have been unsuccessful, as {@link AlertDialog.Builder} uses a + * private subclass to implement most of the interesting behavior. + * + * @author Matthew Simmons + */ +public class ResultDialogFactory { + private ResultDialogFactory() {} + + /** + * Create a send-to-Google result dialog. The caller is responsible for + * showing it. + * + * @param activity the activity associated with the dialog + * @param results the results to be displayed in the dialog + * @param onOkClickListener the listener to invoke if the OK button is + * clicked + * @param onShareClickListener the listener to invoke if the Share button is + * clicked. If no share listener is provided, the Share button will not + * be displayed. + * @return the created dialog + */ + public static AlertDialog makeDialog(Activity activity, List results, + DialogInterface.OnClickListener onOkClickListener, + DialogInterface.OnClickListener onShareClickListener) { + boolean success = true; + for (SendResult result : results) { + if (!result.isSuccess()) { + success = false; + break; + } + } + + AlertDialog.Builder builder = new AlertDialog.Builder(activity) + .setView(makeDialogContent(activity, results, success)); + + if (success) { + builder.setTitle(R.string.success); + builder.setIcon(android.R.drawable.ic_dialog_info); + } else { + builder.setTitle(R.string.error); + builder.setIcon(android.R.drawable.ic_dialog_alert); + } + + builder.setPositiveButton(activity.getString(R.string.ok), onOkClickListener); + if (onShareClickListener != null) { + builder.setNeutralButton(activity.getString(R.string.share_track), + onShareClickListener); + } + + return builder.create(); + } + + private static View makeDialogContent(Activity activity, List results, + boolean success) { + ResultListAdapter resultListAdapter = new ResultListAdapter(activity, + R.layout.send_to_google_result_list_item, results); + + View content = activity.getLayoutInflater().inflate(R.layout.send_to_google_result, null); + ListView resultList = (ListView) content.findViewById(R.id.send_to_google_result_list); + resultList.setAdapter(resultListAdapter); + + content.findViewById(R.id.send_to_google_result_comment) + .setVisibility(success ? View.VISIBLE : View.GONE); + content.findViewById(R.id.send_to_google_result_error) + .setVisibility(success ? View.GONE : View.VISIBLE); + + return content; + } +} diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/sendtogoogle/ResultListAdapter.java b/MyTracks/src/com/google/android/apps/mytracks/io/sendtogoogle/ResultListAdapter.java new file mode 100644 index 000000000..68653067b --- /dev/null +++ b/MyTracks/src/com/google/android/apps/mytracks/io/sendtogoogle/ResultListAdapter.java @@ -0,0 +1,85 @@ +/* + * Copyright 2011 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.google.android.apps.mytracks.io.sendtogoogle; + +import com.google.android.maps.mytracks.R; + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.ImageView; +import android.widget.ListAdapter; +import android.widget.TextView; + +import java.util.List; + +/** + * This class implements the {@link ListAdapter} used by the send-to-Google + * result dialog created by {@link ResultDialogFactory}. It generates views + * for each entry in an array of {@link SendResult} instances. + * @author Matthew Simmons + */ +class ResultListAdapter extends ArrayAdapter { + public ResultListAdapter(Context context, int textViewResourceId, List results) { + super(context, textViewResourceId, results); + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + View view = convertView; + if (view == null) { + LayoutInflater inflater + = (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE); + view = inflater.inflate(R.layout.send_to_google_result_list_item, null); + } + + SendResult result = getItem(position); + setImage(view, result.isSuccess() ? R.drawable.success : R.drawable.failure); + setName(view, result.getType().getServiceName()); + setUrl(view, result.getType().getServiceUrl()); + return view; + } + + @Override + public boolean areAllItemsEnabled() { + // We don't want the displayed items to be clickable + return false; + } + + // The following protected methods exist to be overridden for testing + // purposes. Doing so insulates the test class from the details of the + // layout. + + protected void setImage(View content, int drawableId) { + ImageView imageView = (ImageView) content.findViewById(R.id.send_to_google_result_icon); + imageView.setImageDrawable(getContext().getResources().getDrawable(drawableId)); + } + + protected void setName(View content, int nameId) { + setTextViewText(content, R.id.send_to_google_result_name, nameId); + } + + protected void setUrl(View content, int urlId) { + setTextViewText(content, R.id.send_to_google_result_url, urlId); + } + + private void setTextViewText(View content, int viewId, int textId) { + TextView textView = (TextView) content.findViewById(viewId); + textView.setText(getContext().getString(textId)); + } +}; diff --git a/MyTracks/src/com/google/android/apps/mytracks/SendToGoogleDialog.java b/MyTracks/src/com/google/android/apps/mytracks/io/sendtogoogle/SendDialog.java similarity index 95% rename from MyTracks/src/com/google/android/apps/mytracks/SendToGoogleDialog.java rename to MyTracks/src/com/google/android/apps/mytracks/io/sendtogoogle/SendDialog.java index be5b83e81..9296de275 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/SendToGoogleDialog.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/sendtogoogle/SendDialog.java @@ -1,20 +1,23 @@ /* * Copyright 2008 Google Inc. - * + * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of * the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations under * the License. */ -package com.google.android.apps.mytracks; +package com.google.android.apps.mytracks.io.sendtogoogle; +import com.google.android.apps.mytracks.DialogManager; +import com.google.android.apps.mytracks.MyTracks; +import com.google.android.apps.mytracks.MyTracksSettings; import com.google.android.maps.mytracks.R; import android.app.Dialog; @@ -37,7 +40,7 @@ import android.widget.RadioGroup; * * @author Leif Hendrik Wilden */ -public class SendToGoogleDialog extends Dialog { +public class SendDialog extends Dialog { private RadioGroup groupMyMaps; private RadioButton createNewMapRadioButton; @@ -49,7 +52,7 @@ public class SendToGoogleDialog extends Dialog { private RadioButton sendStatsAndPointsRadioButton; private Button sendButton; - public SendToGoogleDialog(Context context) { + public SendDialog(Context context) { super(context); } @@ -81,8 +84,6 @@ public class SendToGoogleDialog extends Dialog { sendToFusionTablesCheckBox.isChecked() || sendToDocsCheckBox.isChecked()); groupMyMaps.setVisibility(sendToMyMapsCheckBox.isChecked() ? View.VISIBLE : View.INVISIBLE); - //groupDocs.setVisibility( - // sendToDocsCheckBox.isChecked() ? View.VISIBLE : View.INVISIBLE); } }; diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/sendtogoogle/SendResult.java b/MyTracks/src/com/google/android/apps/mytracks/io/sendtogoogle/SendResult.java new file mode 100644 index 000000000..34ad4b13d --- /dev/null +++ b/MyTracks/src/com/google/android/apps/mytracks/io/sendtogoogle/SendResult.java @@ -0,0 +1,46 @@ +/* + * Copyright 2011 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.google.android.apps.mytracks.io.sendtogoogle; + +/** + * This class represents the the result of the uploading of track data to a + * single service. + * + * @author Matthew Simmons + */ +public class SendResult { + private final SendType type; + private final boolean success; + + /** + * @param type the service to which track data was uploaded + * @param success true if the uploading succeeded + */ + public SendResult(SendType type, boolean success) { + this.type = type; + this.success = success; + } + + /** Returns the service to which the track data was uploaded */ + public SendType getType() { + return type; + } + + /** Returns true if the uploading succeeded */ + public boolean isSuccess() { + return success; + } +} diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/sendtogoogle/SendType.java b/MyTracks/src/com/google/android/apps/mytracks/io/sendtogoogle/SendType.java new file mode 100644 index 000000000..66feccf41 --- /dev/null +++ b/MyTracks/src/com/google/android/apps/mytracks/io/sendtogoogle/SendType.java @@ -0,0 +1,50 @@ +/* + * Copyright 2011 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.google.android.apps.mytracks.io.sendtogoogle; + +import com.google.android.maps.mytracks.R; + +/** + * Enumerates the services to which we can upload track data. + * + * @author Matthew Simmons + */ +public enum SendType { + MYMAPS(R.string.sendtogoogle_google_mymaps, + R.string.sendtogoogle_google_mymaps_url), + FUSION_TABLES(R.string.sendtogoogle_google_fusiontables, + R.string.sendtogoogle_google_fusiontables_url), + DOCS(R.string.sendtogoogle_google_docs, + R.string.sendtogoogle_google_docs_url); + + private int serviceName; + private int serviceUrl; + + private SendType(int serviceName, int serviceUrl) { + this.serviceName = serviceName; + this.serviceUrl = serviceUrl; + } + + /** Returns the resource ID for the printable (short) name of the service */ + public int getServiceName() { + return serviceName; + } + + /** Returns the resource ID for the service's URL */ + public int getServiceUrl() { + return serviceUrl; + } +} diff --git a/MyTracks/src/com/google/android/apps/mytracks/services/StatusAnnouncerTask.java b/MyTracks/src/com/google/android/apps/mytracks/services/StatusAnnouncerTask.java index eb05de53c..8f7f1cc52 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/services/StatusAnnouncerTask.java +++ b/MyTracks/src/com/google/android/apps/mytracks/services/StatusAnnouncerTask.java @@ -16,6 +16,8 @@ package com.google.android.apps.mytracks.services; +import static com.google.android.apps.mytracks.Constants.TAG; + import com.google.android.apps.mytracks.Constants; import com.google.android.apps.mytracks.MyTracksSettings; import com.google.android.apps.mytracks.stats.TripStatistics; @@ -99,7 +101,7 @@ public class StatusAnnouncerTask implements PeriodicTask { * Called when the TTS engine is initialized. */ protected void onTtsInit(int status) { - Log.i(Constants.TAG, "TrackRecordingService.TTS init: " + status); + Log.i(TAG, "TrackRecordingService.TTS init: " + status); this.ready = status == TextToSpeech.SUCCESS; if (ready) { @@ -111,7 +113,7 @@ public class StatusAnnouncerTask implements PeriodicTask { languageAvailability == TextToSpeech.LANG_NOT_SUPPORTED) { // English is probably supported. // TODO: Somehow use announcement strings from English too. - Log.w(Constants.TAG, "Default language not available, using English."); + Log.w(TAG, "Default language not available, using English."); speechLanguage = Locale.ENGLISH; } tts.setLanguage(speechLanguage); @@ -128,8 +130,26 @@ public class StatusAnnouncerTask implements PeriodicTask { */ @Override public void run(TrackRecordingService service) { + if (service == null) { + Log.e(TAG, "StatusAnnouncer TrackRecordingService not initialized"); + return; + } + + runWithStatistics(service.getTripStatistics()); + } + + /** + * This method exists as a convenience for testing code, allowing said code + * to avoid needing to instantiate an entire {@link TrackRecordingService} + * just to test the announcer. + */ + protected void runWithStatistics(TripStatistics statistics) { + if (statistics == null) { + Log.e(TAG, "StatusAnnouncer stats not initialized."); + return; + } if (!ready || tts == null) { - Log.e(Constants.TAG, "StatusAnnouncer Tts not ready."); + Log.e(TAG, "StatusAnnouncer Tts not ready."); return; } @@ -139,12 +159,7 @@ public class StatusAnnouncerTask implements PeriodicTask { return; } - if (service == null || service.getTripStatistics() == null) { - Log.e(Constants.TAG, "StatusAnnouncer stats not initialized."); - return; - } - - String announcement = getAnnouncement(service.getTripStatistics()); + String announcement = getAnnouncement(statistics); Log.d(Constants.TAG, "Announcement: " + announcement); speakAnnouncment(announcement); } diff --git a/MyTracks/src/com/google/android/apps/mytracks/services/TrackRecordingService.java b/MyTracks/src/com/google/android/apps/mytracks/services/TrackRecordingService.java index 64f712af5..57815d323 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/services/TrackRecordingService.java +++ b/MyTracks/src/com/google/android/apps/mytracks/services/TrackRecordingService.java @@ -15,6 +15,7 @@ */ package com.google.android.apps.mytracks.services; +import static com.google.android.apps.mytracks.Constants.TAG; import static com.google.android.apps.mytracks.Constants.RESUME_TRACK_EXTRA_NAME; import com.google.android.apps.mytracks.MyTracks; @@ -269,7 +270,7 @@ public class TrackRecordingService extends Service implements LocationListener { // Insert failed, most likely because of SqlLite error code 5 // (SQLite_BUSY). This is expected to happen extremely rarely (if our // listener gets invoked twice at about the same time). - Log.w(Constants.TAG, + Log.w(TAG, "Caught SQLiteException: " + e.getMessage(), e); return false; } @@ -314,15 +315,15 @@ public class TrackRecordingService extends Service implements LocationListener { try { PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE); if (pm == null) { - Log.e(Constants.TAG, + Log.e(TAG, "TrackRecordingService: Power manager not found!"); return; } if (wakeLock == null) { wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, - Constants.TAG); + TAG); if (wakeLock == null) { - Log.e(Constants.TAG, + Log.e(TAG, "TrackRecordingService: Could not create wake lock (null)."); return; } @@ -330,12 +331,12 @@ public class TrackRecordingService extends Service implements LocationListener { if (!wakeLock.isHeld()) { wakeLock.acquire(); if (!wakeLock.isHeld()) { - Log.e(Constants.TAG, + Log.e(TAG, "TrackRecordingService: Could not acquire wake lock."); } } } catch (RuntimeException e) { - Log.e(Constants.TAG, + Log.e(TAG, "TrackRecordingService: Caught unexpected exception: " + e.getMessage(), e); } @@ -376,11 +377,11 @@ public class TrackRecordingService extends Service implements LocationListener { public void registerLocationListener() { if (locationManager == null) { - Log.e(Constants.TAG, + Log.e(TAG, "TrackRecordingService: Do not have any location manager."); return; } - Log.d(Constants.TAG, + Log.d(TAG, "Preparing to register location listener w/ TrackRecordingService..."); try { long desiredInterval = locationListenerPolicy.getDesiredPollingInterval(); @@ -390,23 +391,23 @@ public class TrackRecordingService extends Service implements LocationListener { // , 0 /* minDistance, get all updates to properly time pauses */ TrackRecordingService.this); currentRecordingInterval = desiredInterval; - Log.d(Constants.TAG, + Log.d(TAG, "...location listener now registered w/ TrackRecordingService @ " + currentRecordingInterval); } catch (RuntimeException e) { - Log.e(Constants.TAG, + Log.e(TAG, "Could not register location listener: " + e.getMessage(), e); } } public void unregisterLocationListener() { if (locationManager == null) { - Log.e(Constants.TAG, + Log.e(TAG, "TrackRecordingService: Do not have any location manager."); return; } locationManager.removeUpdates(this); - Log.d(Constants.TAG, + Log.d(TAG, "Location listener now unregistered w/ TrackRecordingService."); } @@ -419,7 +420,7 @@ public class TrackRecordingService extends Service implements LocationListener { } private void restoreStats(Track track) { - Log.d(Constants.TAG, + Log.d(TAG, "Restoring stats of track with ID: " + track.getId()); TripStatistics stats = track.getStatistics(); @@ -465,10 +466,10 @@ public class TrackRecordingService extends Service implements LocationListener { statsBuilder.pauseAt(stats.getStopTime()); statsBuilder.resumeAt(System.currentTimeMillis()); } else { - Log.e(Constants.TAG, "Could not get track points cursor."); + Log.e(TAG, "Could not get track points cursor."); } } catch (RuntimeException e) { - Log.e(Constants.TAG, "Error while restoring track.", e); + Log.e(TAG, "Error while restoring track.", e); } finally { if (cursor != null) { cursor.close(); @@ -493,12 +494,12 @@ public class TrackRecordingService extends Service implements LocationListener { } private void onLocationChangedAsync(Location location) { - Log.d(Constants.TAG, "TrackRecordingService.onLocationChanged"); + Log.d(TAG, "TrackRecordingService.onLocationChanged"); try { // Don't record if the service has been asked to pause recording: if (!isRecording) { - Log.w(Constants.TAG, + Log.w(TAG, "Not recording because recording has been paused."); return; } @@ -506,14 +507,14 @@ public class TrackRecordingService extends Service implements LocationListener { // This should never happen, but just in case (we really don't want the // service to crash): if (location == null) { - Log.w(Constants.TAG, + Log.w(TAG, "Location changed, but location is null."); return; } // Don't record if the accuracy is too bad: if (location.getAccuracy() > minRequiredAccuracy) { - Log.d(Constants.TAG, + Log.d(TAG, "Not recording. Bad accuracy."); return; } @@ -521,7 +522,7 @@ public class TrackRecordingService extends Service implements LocationListener { // At least one track must be available for appending points: recordingTrack = getRecordingTrack(); if (recordingTrack == null) { - Log.d(Constants.TAG, + Log.d(TAG, "Not recording. No track to append to available."); return; } @@ -555,7 +556,7 @@ public class TrackRecordingService extends Service implements LocationListener { // maxRecordingDistance = 0 if (distanceToLast == 0 && !hasSensorData) { if (isMoving) { - Log.d(Constants.TAG, "Found two identical locations."); + Log.d(TAG, "Found two identical locations."); isMoving = false; if (lastLocation != null && lastRecordedLocation != null && !lastRecordedLocation.equals(lastLocation)) { @@ -570,7 +571,7 @@ public class TrackRecordingService extends Service implements LocationListener { lastRecordedLocationId++; } } else { - Log.d(Constants.TAG, + Log.d(TAG, "Not recording. More than two identical locations."); } } else if (distanceToLastRecorded > minRecordingDistance @@ -595,7 +596,7 @@ public class TrackRecordingService extends Service implements LocationListener { && recordingTrack.getStartId() >= 0; if (startNewSegment) { // Insert a separator point to indicate start of new track: - Log.d(Constants.TAG, "Inserting a separator."); + Log.d(TAG, "Inserting a separator."); Location separator = new Location(Constants.GPS_PROVIDER); separator.setLongitude(0); separator.setLatitude(100); @@ -608,7 +609,7 @@ public class TrackRecordingService extends Service implements LocationListener { return; } } else { - Log.d(Constants.TAG, String.format( + Log.d(TAG, String.format( "Not recording. Distance to last recorded point (%f m) is less than" + " %d m.", distanceToLastRecorded, minRecordingDistance)); // Return here so that the location is NOT recorded as the last location. @@ -616,11 +617,11 @@ public class TrackRecordingService extends Service implements LocationListener { } } catch (Error e) { // Probably important enough to rethrow. - Log.e(Constants.TAG, "Error in onLocationChanged", e); + Log.e(TAG, "Error in onLocationChanged", e); throw e; } catch (RuntimeException e) { // Safe usually to trap exceptions. - Log.e(Constants.TAG, + Log.e(TAG, "Trapping exception in onLocationChanged", e); throw e; } @@ -657,7 +658,7 @@ public class TrackRecordingService extends Service implements LocationListener { @Override public void onCreate() { super.onCreate(); - Log.d(Constants.TAG, "TrackRecordingService.onCreate"); + Log.d(TAG, "TrackRecordingService.onCreate"); providerUtils = MyTracksProviderUtils.Factory.get(this); notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); @@ -682,7 +683,7 @@ public class TrackRecordingService extends Service implements LocationListener { } else { if (recordingTrackId != -1) { // Make sure we have consistent state in shared preferences. - Log.w(Constants.TAG, "TrackRecordingService.onCreate: " + Log.w(TAG, "TrackRecordingService.onCreate: " + "Resetting an orphaned recording track = " + recordingTrackId); } prefManager.setRecordingTrack(recordingTrackId = -1); @@ -697,7 +698,7 @@ public class TrackRecordingService extends Service implements LocationListener { * the announcements, otherwise this method is no-op. */ private void setUpAnnouncer() { - Log.d(Constants.TAG, "TrackRecordingService.setUpAnnouncer: " + Log.d(TAG, "TrackRecordingService.setUpAnnouncer: " + announcementExecuter); if (announcementFrequency != -1 && recordingTrackId != -1) { handler.post(new Runnable() { @@ -723,7 +724,7 @@ public class TrackRecordingService extends Service implements LocationListener { } private void shutdownAnnouncer() { - Log.d(Constants.TAG, "TrackRecordingService.shutdownAnnouncer: " + Log.d(TAG, "TrackRecordingService.shutdownAnnouncer: " + announcementExecuter); if (announcementExecuter != null) { try { @@ -736,7 +737,7 @@ public class TrackRecordingService extends Service implements LocationListener { @Override public void onDestroy() { - Log.d(Constants.TAG, "TrackRecordingService.onDestroy"); + Log.d(TAG, "TrackRecordingService.onDestroy"); isRecording = false; showNotification(); @@ -770,19 +771,19 @@ public class TrackRecordingService extends Service implements LocationListener { @Override public IBinder onBind(Intent intent) { - Log.d(Constants.TAG, "TrackRecordingService.onBind"); + Log.d(TAG, "TrackRecordingService.onBind"); return binder; } @Override public boolean onUnbind(Intent intent) { - Log.d(Constants.TAG, "TrackRecordingService.onUnbind"); + Log.d(TAG, "TrackRecordingService.onUnbind"); return super.onUnbind(intent); } @Override public boolean stopService(Intent name) { - Log.d(Constants.TAG, "TrackRecordingService.stopService"); + Log.d(TAG, "TrackRecordingService.stopService"); unregisterLocationListener(); return super.stopService(name); } @@ -799,17 +800,17 @@ public class TrackRecordingService extends Service implements LocationListener { } private void handleStartCommand(Intent intent, int startId) { - Log.d(Constants.TAG, + Log.d(TAG, "TrackRecordingService.handleStartCommand: " + startId); // Check if called on phone reboot with resume intent. if (intent != null && intent.getBooleanExtra(RESUME_TRACK_EXTRA_NAME, false)) { - Log.d(Constants.TAG, "TrackRecordingService: requested resume"); + Log.d(TAG, "TrackRecordingService: requested resume"); // Make sure that the current track exists and is fresh enough. if (recordingTrack == null || !shouldResumeTrack(recordingTrack)) { - Log.i(Constants.TAG, + Log.i(TAG, "TrackRecordingService: Not resuming, because the previous track (" + recordingTrack + ") doesn't exist or is too old"); isRecording = false; @@ -818,19 +819,19 @@ public class TrackRecordingService extends Service implements LocationListener { return; } - Log.i(Constants.TAG, "TrackRecordingService: resuming"); + Log.i(TAG, "TrackRecordingService: resuming"); } } private void setAutoResumeTrackRetries( SharedPreferences sharedPreferences, int retryAttempts) { - Log.d(Constants.TAG, + Log.d(TAG, "Updating auto-resume retry attempts to: " + retryAttempts); prefManager.setAutoResumeTrackCurrentRetry(retryAttempts); } private boolean shouldResumeTrack(Track track) { - Log.d(Constants.TAG, "shouldResumeTrack: autoResumeTrackTimeout = " + Log.d(TAG, "shouldResumeTrack: autoResumeTrackTimeout = " + autoResumeTrackTimeout); // Check if we haven't exceeded the maximum number of retry attempts. @@ -838,11 +839,11 @@ public class TrackRecordingService extends Service implements LocationListener { getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0); int retries = sharedPreferences.getInt( getString(R.string.auto_resume_track_current_retry_key), 0); - Log.d(Constants.TAG, + Log.d(TAG, "shouldResumeTrack: Attempting to auto-resume the track (" + (retries + 1) + "/" + MAX_AUTO_RESUME_TRACK_RETRY_ATTEMPTS + ")"); if (retries >= MAX_AUTO_RESUME_TRACK_RETRY_ATTEMPTS) { - Log.i(Constants.TAG, + Log.i(TAG, "shouldResumeTrack: Not resuming because exceeded the maximum " + "number of auto-resume retries"); return false; @@ -854,12 +855,12 @@ public class TrackRecordingService extends Service implements LocationListener { // Check for special cases. if (autoResumeTrackTimeout == 0) { // Never resume. - Log.d(Constants.TAG, + Log.d(TAG, "shouldResumeTrack: Auto-resume disabled (never resume)"); return false; } else if (autoResumeTrackTimeout == -1) { // Always resume. - Log.d(Constants.TAG, + Log.d(TAG, "shouldResumeTrack: Auto-resume forced (always resume)"); return true; } @@ -867,7 +868,7 @@ public class TrackRecordingService extends Service implements LocationListener { // Check if the last modified time is within the acceptable range. long lastModified = track.getStatistics() != null ? track.getStatistics().getStopTime() : 0; - Log.d(Constants.TAG, + Log.d(TAG, "shouldResumeTrack: lastModified = " + lastModified + ", autoResumeTrackTimeout: " + autoResumeTrackTimeout); return lastModified > 0 && System.currentTimeMillis() - lastModified <= @@ -897,7 +898,7 @@ public class TrackRecordingService extends Service implements LocationListener { } wpt.setTrackId(recordingTrackId); wpt.setLength(length); - if (lastValidLocation == null + if (lastLocation == null || statsBuilder == null || statsBuilder.getStatistics() == null) { // A null location is ok, and expected on track start. // Make it an impossible location. @@ -1056,11 +1057,11 @@ public class TrackRecordingService extends Service implements LocationListener { public byte[] getSensorData() { checkService(); if (service.sensorManager == null) { - Log.d(Constants.TAG, "No sensor manager for data."); + Log.d(TAG, "No sensor manager for data."); return null; } if (service.sensorManager.getSensorDataSet() == null) { - Log.d(Constants.TAG, "Sensor data set is null."); + Log.d(TAG, "Sensor data set is null."); return null; } return service.sensorManager.getSensorDataSet().toByteArray(); @@ -1070,7 +1071,7 @@ public class TrackRecordingService extends Service implements LocationListener { public int getSensorState() { checkService(); if (service.sensorManager == null) { - Log.d(Constants.TAG, "No sensor manager for data."); + Log.d(TAG, "No sensor manager for data."); return Sensor.SensorState.NONE.getNumber(); } return service.sensorManager.getSensorState().getNumber(); @@ -1078,7 +1079,7 @@ public class TrackRecordingService extends Service implements LocationListener { } public long startNewTrack() { - Log.d(Constants.TAG, "TrackRecordingService.startNewTrack"); + Log.d(TAG, "TrackRecordingService.startNewTrack"); if (recordingTrackId != -1 || isRecording) { throw new IllegalStateException("A track is already in progress!"); } @@ -1128,7 +1129,7 @@ public class TrackRecordingService extends Service implements LocationListener { } private void endCurrentTrack() { - Log.d(Constants.TAG, "TrackRecordingService.endCurrentTrack"); + Log.d(TAG, "TrackRecordingService.endCurrentTrack"); if (recordingTrackId == -1 || !isRecording) { throw new IllegalStateException("No recording track in progress!"); } diff --git a/MyTracksTest/libs/AndroidMockGenerator.jar b/MyTracksTest/libs/AndroidMockGenerator.jar old mode 100755 new mode 100644 index d58e0d248..ef8b3612c Binary files a/MyTracksTest/libs/AndroidMockGenerator.jar and b/MyTracksTest/libs/AndroidMockGenerator.jar differ diff --git a/MyTracksTest/libs/AndroidMockRuntime.jar b/MyTracksTest/libs/AndroidMockRuntime.jar old mode 100755 new mode 100644 index d7c2a95a7..40fa9e173 Binary files a/MyTracksTest/libs/AndroidMockRuntime.jar and b/MyTracksTest/libs/AndroidMockRuntime.jar differ diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/DialogManagerTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/DialogManagerTest.java index bacd00edf..92cf13598 100644 --- a/MyTracksTest/src/com/google/android/apps/mytracks/DialogManagerTest.java +++ b/MyTracksTest/src/com/google/android/apps/mytracks/DialogManagerTest.java @@ -1,12 +1,12 @@ /* * Copyright 2010 Google Inc. - * + * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of * the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the @@ -15,7 +15,8 @@ */ package com.google.android.apps.mytracks; -import android.app.AlertDialog; +import com.google.android.apps.mytracks.io.sendtogoogle.SendDialog; + import android.app.Dialog; import android.app.ProgressDialog; import android.test.ActivityInstrumentationTestCase2; @@ -66,15 +67,7 @@ public class DialogManagerTest Dialog d = getActivity().onCreateDialog(DialogManager.DIALOG_SEND_TO_GOOGLE, null); assertNotNull(d); - assertTrue(d instanceof SendToGoogleDialog); - } - - public void test_onCreateSendToGoogleResult() { - Dialog d = - getActivity().onCreateDialog( - DialogManager.DIALOG_SEND_TO_GOOGLE_RESULT, null); - assertNotNull(d); - assertTrue(d instanceof AlertDialog); + assertTrue(d instanceof SendDialog); } public void test_onCreateWriteProgress() { diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/io/GpxTrackWriterTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/io/GpxTrackWriterTest.java index c9c3e7f42..9601875cb 100644 --- a/MyTracksTest/src/com/google/android/apps/mytracks/io/GpxTrackWriterTest.java +++ b/MyTracksTest/src/com/google/android/apps/mytracks/io/GpxTrackWriterTest.java @@ -1,14 +1,9 @@ // Copyright 2010 Google Inc. All Rights Reserved. package com.google.android.apps.mytracks.io; -import com.google.android.apps.mytracks.content.Waypoint; - -import android.location.Location; - import org.w3c.dom.Document; import org.w3c.dom.Element; -import java.util.Date; import java.util.List; /** diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/io/TcxTrackWriterTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/io/TcxTrackWriterTest.java index 897df2792..e07f1ae55 100644 --- a/MyTracksTest/src/com/google/android/apps/mytracks/io/TcxTrackWriterTest.java +++ b/MyTracksTest/src/com/google/android/apps/mytracks/io/TcxTrackWriterTest.java @@ -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(getContext()); 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")); diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/io/docs/DocsHelper_AddTrackRowTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/io/docs/DocsHelper_AddTrackRowTest.java index e357605b4..89c0cfa3a 100644 --- a/MyTracksTest/src/com/google/android/apps/mytracks/io/docs/DocsHelper_AddTrackRowTest.java +++ b/MyTracksTest/src/com/google/android/apps/mytracks/io/docs/DocsHelper_AddTrackRowTest.java @@ -18,31 +18,31 @@ package com.google.android.apps.mytracks.io.docs; import com.google.android.apps.mytracks.content.Track; import com.google.android.apps.mytracks.io.AuthManager; import com.google.android.apps.mytracks.stats.TripStatistics; +import com.google.android.maps.mytracks.R; import com.google.android.testing.mocking.AndroidMock; import com.google.android.testing.mocking.UsesMocks; -import com.google.android.maps.mytracks.R; import android.content.Context; import android.content.res.Resources; import android.test.mock.MockContext; import android.test.mock.MockResources; -import junit.framework.TestCase; - import java.io.IOException; +import junit.framework.TestCase; + /** * Tests for {@link DocsHelper#addTrackRow} - * + * * @author Matthew Simmons */ public class DocsHelper_AddTrackRowTest extends TestCase { private static final long TIME = 1288721514000L; - + private static class StringWritingDocsHelper extends DocsHelper { String writtenSheetUri = null; String writtenData = null; - + @Override protected void writeRowData(AuthManager trixAuth, String worksheetUri, String postText) { @@ -54,7 +54,7 @@ public class DocsHelper_AddTrackRowTest extends TestCase { public void testAddTrackRow_imperial() throws Exception { StringWritingDocsHelper docsHelper = new StringWritingDocsHelper(); addTrackRow(docsHelper, false); - + String expectedData = "" @@ -81,11 +81,11 @@ public class DocsHelper_AddTrackRowTest extends TestCase { + ""; assertEquals( - "http://spreadsheets.google.com/feeds/list/ssid/wsid/private/full", + "http://spreadsheets.google.com/feeds/list/ssid/wsid/private/full", docsHelper.writtenSheetUri); assertEquals(expectedData, docsHelper.writtenData); } - + public void testAddTrackRow_metric() throws Exception { StringWritingDocsHelper docsHelper = new StringWritingDocsHelper(); addTrackRow(docsHelper, true); @@ -100,18 +100,17 @@ public class DocsHelper_AddTrackRowTest extends TestCase { "")); assertTrue(docsHelper.writtenData.contains( "")); - + assertTrue(docsHelper.writtenData.contains( "")); } - @UsesMocks({AuthManager.class, MockContext.class, MockResources.class, - Track.class}) /** Adds a row to the spreadsheet, using the provided helper. */ - private void addTrackRow(DocsHelper docsHelper, boolean useMetric) + @UsesMocks({AuthManager.class, MockResources.class, Track.class}) + private void addTrackRow(DocsHelper docsHelper, boolean useMetric) throws IOException { - Resources mockResources = AndroidMock.createMock(MockResources.class); - + final Resources mockResources = AndroidMock.createMock(MockResources.class); + if (useMetric) { AndroidMock.expect(mockResources.getString(R.string.kilometer)) .andReturn("km"); @@ -128,15 +127,17 @@ public class DocsHelper_AddTrackRowTest extends TestCase { .andReturn("feet"); } AndroidMock.replay(mockResources); - - Context mockContext = AndroidMock.createMock(MockContext.class); - AndroidMock.expect(mockContext.getResources()) - .andReturn(mockResources).anyTimes(); - AndroidMock.replay(mockContext); + + Context mockContext = new MockContext() { + @Override + public Resources getResources() { + return mockResources; + } + }; AuthManager mockAuthManager = AndroidMock.createMock(AuthManager.class); AndroidMock.replay(mockAuthManager); - + TripStatistics stats = new TripStatistics(); stats.setStartTime(TIME); stats.setTotalTime(5000); @@ -146,14 +147,14 @@ public class DocsHelper_AddTrackRowTest extends TestCase { stats.setTotalElevationGain(6000); stats.setMinElevation(-500); stats.setMaxElevation(550); - + Track track = new Track(); track.setName("trackName"); track.setDescription("trackDescription"); track.setMapId("trackMapId"); track.setStatistics(stats); - - docsHelper.addTrackRow(mockContext, mockAuthManager, "ssid", "wsid", + + docsHelper.addTrackRow(mockContext, mockAuthManager, "ssid", "wsid", track, useMetric); } } diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/io/sendtogoogle/ResultDialogFactoryTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/io/sendtogoogle/ResultDialogFactoryTest.java new file mode 100644 index 000000000..c88035e71 --- /dev/null +++ b/MyTracksTest/src/com/google/android/apps/mytracks/io/sendtogoogle/ResultDialogFactoryTest.java @@ -0,0 +1,121 @@ +/* + * Copyright 2011 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.google.android.apps.mytracks.io.sendtogoogle; + +import com.google.android.apps.mytracks.MyTracks; +import com.google.android.maps.mytracks.R; + +import android.app.AlertDialog; +import android.content.DialogInterface; +import android.test.ActivityInstrumentationTestCase2; +import android.view.View; +import android.widget.ListAdapter; +import android.widget.ListView; + +import java.util.ArrayList; +import java.util.List; + +/** + * Tests {@link ResultDialogFactory}. + * + * @author Matthew Simmons + */ +public class ResultDialogFactoryTest extends ActivityInstrumentationTestCase2 { + public ResultDialogFactoryTest() { + super(MyTracks.class); + } + + private List makeResults(SendResult... results) { + List resultsList = new ArrayList(); + for (SendResult result : results) { + resultsList.add(result); + } + return resultsList; + } + + private DialogInterface.OnClickListener clickListener = new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) {} + }; + + public void testSuccess_noShare() { + List results = makeResults(new SendResult(SendType.MYMAPS, true), + new SendResult(SendType.DOCS, true)); + + AlertDialog dialog = ResultDialogFactory.makeDialog(getActivity(), results, clickListener, null); + dialog.show(); + + ListView listView = (ListView) dialog.findViewById(R.id.send_to_google_result_list); + ListAdapter listAdapter = listView.getAdapter(); + assertEquals(2, listAdapter.getCount()); + + assertEquals(SendType.MYMAPS, ((SendResult) listAdapter.getItem(0)).getType()); + assertEquals(SendType.DOCS, ((SendResult) listAdapter.getItem(1)).getType()); + + // Checking the return code of Dialog#getButton doesn't appear to be + // sufficient, as a Button will be returned even if it doesn't appear in + // the rendered dialog. + assertEquals(View.VISIBLE, dialog.getButton(AlertDialog.BUTTON_POSITIVE).getVisibility()); + assertEquals(View.GONE, dialog.getButton(AlertDialog.BUTTON_NEUTRAL).getVisibility()); + + assertEquals(View.VISIBLE, + dialog.findViewById(R.id.send_to_google_result_comment).getVisibility()); + assertEquals(View.GONE, + dialog.findViewById(R.id.send_to_google_result_error).getVisibility()); + + // TODO: Test correct listener invocation. Neither TouchUtils#clickView + // nor Button#performClick appear to cause listener invocation. This + // might be Android bug + // http://code.google.com/p/android/issues/detail?id=6564 + } + + public void testSuccess_share() { + List results = makeResults(new SendResult(SendType.MYMAPS, true), + new SendResult(SendType.DOCS, true)); + + AlertDialog dialog = ResultDialogFactory.makeDialog(getActivity(), results, + clickListener, clickListener); + dialog.show(); + + assertEquals(View.VISIBLE, dialog.getButton(AlertDialog.BUTTON_POSITIVE).getVisibility()); + assertEquals(View.VISIBLE, dialog.getButton(AlertDialog.BUTTON_NEUTRAL).getVisibility()); + } + + public void testFailure_noShare() { + List results = makeResults(new SendResult(SendType.MYMAPS, true), + new SendResult(SendType.DOCS, false)); + + AlertDialog dialog = ResultDialogFactory.makeDialog(getActivity(), results, clickListener, null); + dialog.show(); + + assertEquals(View.GONE, + dialog.findViewById(R.id.send_to_google_result_comment).getVisibility()); + assertEquals(View.VISIBLE, + dialog.findViewById(R.id.send_to_google_result_error).getVisibility()); + } + + public void testFailure_share() { + List results = makeResults(new SendResult(SendType.MYMAPS, true), + new SendResult(SendType.DOCS, false)); + + AlertDialog dialog = ResultDialogFactory.makeDialog(getActivity(), results, + clickListener, clickListener); + dialog.show(); + + assertEquals(View.VISIBLE, dialog.getButton(AlertDialog.BUTTON_POSITIVE).getVisibility()); + assertEquals(View.VISIBLE, dialog.getButton(AlertDialog.BUTTON_NEUTRAL).getVisibility()); + } +} diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/io/sendtogoogle/ResultListAdapterTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/io/sendtogoogle/ResultListAdapterTest.java new file mode 100644 index 000000000..eb0dcb5f2 --- /dev/null +++ b/MyTracksTest/src/com/google/android/apps/mytracks/io/sendtogoogle/ResultListAdapterTest.java @@ -0,0 +1,116 @@ +/* + * Copyright 2011 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.google.android.apps.mytracks.io.sendtogoogle; + +import com.google.android.maps.mytracks.R; + +import android.content.Context; +import android.test.AndroidTestCase; +import android.view.LayoutInflater; +import android.view.View; + +import java.util.ArrayList; +import java.util.List; + +/** + * Tests {@link ResultListAdapter}. + * + * @author Matthew Simmons + */ +public class ResultListAdapterTest extends AndroidTestCase { + private static class TestResultListAdapter extends ResultListAdapter { + public View contentView; + public int drawableId; + public int nameId; + public int urlId; + + public TestResultListAdapter(Context context, List results) { + super(context, 0, results); + } + + @Override + protected void setImage(View content, int drawableId) { + saveContentView(content); + this.drawableId = drawableId; + } + + @Override + protected void setName(View content, int nameId) { + saveContentView(content); + this.nameId = nameId; + } + + @Override + protected void setUrl(View content, int urlId) { + saveContentView(content); + this.urlId = urlId; + } + + private void saveContentView(View contentView) { + if (this.contentView == null) { + this.contentView = contentView; + } else { + AndroidTestCase.assertEquals(this.contentView, contentView); + } + } + } + + private final SendType sendType = SendType.MYMAPS; + + private View inflateContentView(int viewId) { + LayoutInflater layoutInflater + = (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE); + return layoutInflater.inflate(viewId, null); + } + + private List makeResult(SendType type, boolean success) { + List results = new ArrayList(); + results.add(new SendResult(type, success)); + return results; + } + + public void testSuccess_createsNewView() { + TestResultListAdapter adapter = new TestResultListAdapter(getContext(), + makeResult(sendType, true)); + adapter.getView(0, null, null); + + assertEquals(R.drawable.success, adapter.drawableId); + assertEquals(sendType.getServiceName(), adapter.nameId); + assertEquals(sendType.getServiceUrl(), adapter.urlId); + } + + public void testSuccess_reusesView() { + View contentView = inflateContentView(R.layout.send_to_google_result_list_item); + TestResultListAdapter adapter = new TestResultListAdapter(getContext(), + makeResult(sendType, true)); + adapter.getView(0, contentView, null); + + assertEquals(contentView, adapter.contentView); + assertEquals(R.drawable.success, adapter.drawableId); + assertEquals(sendType.getServiceName(), adapter.nameId); + assertEquals(sendType.getServiceUrl(), adapter.urlId); + } + + public void testFailure() { + TestResultListAdapter adapter = new TestResultListAdapter(getContext(), + makeResult(sendType, false)); + adapter.getView(0, null, null); + + assertEquals(R.drawable.failure, adapter.drawableId); + assertEquals(sendType.getServiceName(), adapter.nameId); + assertEquals(sendType.getServiceUrl(), adapter.urlId); + } +} diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/services/StatusAnnouncerTaskTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/services/StatusAnnouncerTaskTest.java index 59ef6853c..4bc7a3664 100644 --- a/MyTracksTest/src/com/google/android/apps/mytracks/services/StatusAnnouncerTaskTest.java +++ b/MyTracksTest/src/com/google/android/apps/mytracks/services/StatusAnnouncerTaskTest.java @@ -1,12 +1,12 @@ /* * Copyright 2010 Google Inc. - * + * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of * the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the @@ -255,7 +255,6 @@ public class StatusAnnouncerTaskTest extends AndroidTestCase { @UsesMocks({ StatusAnnouncerTask.class, StringUtils.class, - TrackRecordingService.class, }) @Override protected void setUp() throws Exception { @@ -390,9 +389,6 @@ public class StatusAnnouncerTaskTest extends AndroidTestCase { public void testRun() throws Exception { // Expect service data calls TripStatistics stats = new TripStatistics(); - TrackRecordingService service = - AndroidMock.createMock(TrackRecordingService.class); - expect(service.getTripStatistics()).andStubReturn(stats); // Expect announcement building call expect(mockTask.getAnnouncement(same(stats))).andStubReturn(ANNOUNCEMENT); @@ -407,75 +403,63 @@ public class StatusAnnouncerTaskTest extends AndroidTestCase { .andReturn(TextToSpeech.SUCCESS); // Run the announcement - AndroidMock.replay(tts, stringUtils, service); - task.run(service); - AndroidMock.verify(mockTask, tts, stringUtils, service); + AndroidMock.replay(tts, stringUtils); + task.runWithStatistics(stats); + AndroidMock.verify(mockTask, tts, stringUtils); } public void testRun_notReady() throws Exception { - TrackRecordingService service = - AndroidMock.createMock(TrackRecordingService.class); - // Put task in "not ready" state startTask(TextToSpeech.ERROR); // Run the announcement - AndroidMock.replay(tts, stringUtils, service); - task.run(service); - AndroidMock.verify(mockTask, tts, stringUtils, service); + AndroidMock.replay(tts, stringUtils); + task.runWithStatistics(null); + AndroidMock.verify(mockTask, tts, stringUtils); } public void testRun_duringCall() throws Exception { - TrackRecordingService service = - AndroidMock.createMock(TrackRecordingService.class); - startTask(TextToSpeech.SUCCESS); expect(tts.isSpeaking()).andStubReturn(false); // Run the announcement - AndroidMock.replay(tts, stringUtils, service); + AndroidMock.replay(tts, stringUtils); PhoneStateListener phoneListener = phoneListenerCapture.getValue(); phoneListener.onCallStateChanged(TelephonyManager.CALL_STATE_OFFHOOK, null); - task.run(service); - AndroidMock.verify(mockTask, tts, stringUtils, service); + task.runWithStatistics(null); + AndroidMock.verify(mockTask, tts, stringUtils); } public void testRun_ringWhileSpeaking() throws Exception { - TrackRecordingService service = - AndroidMock.createMock(TrackRecordingService.class); - startTask(TextToSpeech.SUCCESS); expect(tts.isSpeaking()).andStubReturn(true); expect(tts.stop()).andReturn(TextToSpeech.SUCCESS); - AndroidMock.replay(tts, stringUtils, service); + AndroidMock.replay(tts, stringUtils); // Update the state to ringing - this should stop the current announcement. PhoneStateListener phoneListener = phoneListenerCapture.getValue(); phoneListener.onCallStateChanged(TelephonyManager.CALL_STATE_RINGING, null); // Run the announcement - this should do nothing. - task.run(service); + task.runWithStatistics(null); - AndroidMock.verify(mockTask, tts, stringUtils, service); + AndroidMock.verify(mockTask, tts, stringUtils); } public void testRun_whileRinging() throws Exception { - TrackRecordingService service = - AndroidMock.createMock(TrackRecordingService.class); - startTask(TextToSpeech.SUCCESS); expect(tts.isSpeaking()).andStubReturn(false); // Run the announcement - AndroidMock.replay(tts, stringUtils, service); + AndroidMock.replay(tts, stringUtils); PhoneStateListener phoneListener = phoneListenerCapture.getValue(); phoneListener.onCallStateChanged(TelephonyManager.CALL_STATE_RINGING, null); - task.run(service); - AndroidMock.verify(mockTask, tts, stringUtils, service); + task.runWithStatistics(null); + AndroidMock.verify(mockTask, tts, stringUtils); } public void testRun_noService() throws Exception { @@ -489,16 +473,13 @@ public class StatusAnnouncerTaskTest extends AndroidTestCase { public void testRun_noStats() throws Exception { // Expect service data calls - TrackRecordingService service = - AndroidMock.createMock(TrackRecordingService.class); - expect(service.getTripStatistics()).andStubReturn(null); startTask(TextToSpeech.SUCCESS); // Run the announcement - AndroidMock.replay(tts, stringUtils, service); - task.run(service); - AndroidMock.verify(mockTask, tts, stringUtils, service); + AndroidMock.replay(tts, stringUtils); + task.runWithStatistics(null); + AndroidMock.verify(mockTask, tts, stringUtils); } private void startTask(int state) { diff --git a/scripts/cat_message b/scripts/cat_message new file mode 100755 index 000000000..a99e19627 --- /dev/null +++ b/scripts/cat_message @@ -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