diff --git a/.hgignore b/.hgignore index 3bf9706a8..1c3f1f5a7 100644 --- a/.hgignore +++ b/.hgignore @@ -5,3 +5,5 @@ syntax:glob */proguard/** .metadata MyTracksTest/annotations.log +**/*.pyc +**/*.pyo diff --git a/MyTracks/AndroidManifest.xml b/MyTracks/AndroidManifest.xml index 0df7cc479..40c852901 100755 --- a/MyTracks/AndroidManifest.xml +++ b/MyTracks/AndroidManifest.xml @@ -17,8 +17,8 @@ limitations under the License. + android:versionCode="27" + android:versionName="1.1.4"> @@ -52,23 +52,28 @@ limitations under the License. - - + + - - - - - + @@ -105,7 +110,7 @@ limitations under the License. - + @@ -159,14 +164,15 @@ limitations under the License. - - + + + diff --git a/MyTracks/default.properties b/MyTracks/default.properties index 019a98ced..061e30159 100644 --- a/MyTracks/default.properties +++ b/MyTracks/default.properties @@ -10,7 +10,7 @@ # Indicates whether an apk should be generated for each density. split.density=false # Project target. -target=Google Inc.:Google APIs:9 +target=Google Inc.:Google APIs:11 android.library.reference.1=../MyTracksLib/ # ProGuard is turned off while we investigate the removal of code that diff --git a/MyTracks/libs/README.txt b/MyTracks/libs/README.txt new file mode 100644 index 000000000..6e03a45b1 --- /dev/null +++ b/MyTracks/libs/README.txt @@ -0,0 +1,17 @@ +The libraries contained here in binary format are licensed according to their +own licenses, which allow their usage for the My Tracks application. Any other +applications that wish to use these files should consult the appropriate +licenses or the JAR's authors. + +References are provided to the libraries: + +accounts.jar (Android accounts library): +http://code.google.com/p/libs-for-android/wiki/AccountsLibrary + +google-api-client-*.jar (GData client): +http://code.google.com/p/google-api-java-client/ + +google-common.jar, +googleloginclient-helper.jar, +libmymaps.jar (Google My Maps upload library): +Licensed exclusively for use in My Tracks and derived Android applications. 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/list.xml b/MyTracks/res/layout/list.xml new file mode 100644 index 000000000..33567b8d2 --- /dev/null +++ b/MyTracks/res/layout/list.xml @@ -0,0 +1,103 @@ + + + + + + + + + + + + + + + + + + + diff --git a/MyTracks/res/layout/mytracks_send_to_google.xml b/MyTracks/res/layout/mytracks_send_to_google.xml index 276bd58ac..e0ead4c75 100644 --- a/MyTracks/res/layout/mytracks_send_to_google.xml +++ b/MyTracks/res/layout/mytracks_send_to_google.xml @@ -41,6 +41,39 @@ limitations under the License. android:stretchColumns="1" android:layout_marginTop="5dip" android:layout_marginLeft="5dip"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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/arrays.xml b/MyTracks/res/values-ca/arrays.xml index 2eddecf5b..2bba332fa 100644 --- a/MyTracks/res/values-ca/arrays.xml +++ b/MyTracks/res/values-ca/arrays.xml @@ -112,9 +112,9 @@ limitations under the License. 3.1 miles (senyal GPS pobre) - - - Sense divisió + + + Inactiu 100 km 50 km 25 km @@ -130,8 +130,8 @@ limitations under the License. 1 min - - No splitting + + Inactiu 100 mi 50 mi 25 mi @@ -286,6 +286,8 @@ limitations under the License. Cap + ANT+ Sensors + SRM ANT+ Bridge Zephyr Bluetooth diff --git a/MyTracks/res/values-ca/strings.xml b/MyTracks/res/values-ca/strings.xml index 7a7751a01..6ac1e8525 100644 --- a/MyTracks/res/values-ca/strings.xml +++ b/MyTracks/res/values-ca/strings.xml @@ -133,7 +133,6 @@ limitations under the License. Totes les rutes: Aquesta ruta Comparteix amb amics... - Un enllaç a Google Fusion Table Un fitxer GPX Un fitxer KML Un fitxer CSV @@ -226,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/arrays.xml b/MyTracks/res/values-de/arrays.xml index ed7915fc8..f33ae9fe1 100644 --- a/MyTracks/res/values-de/arrays.xml +++ b/MyTracks/res/values-de/arrays.xml @@ -48,7 +48,6 @@ limitations under the License. 328 Fuß - 50 m @@ -113,9 +112,9 @@ limitations under the License. 3,1 Meilen (sehr schlechtes GPS-Signal) - - - Nicht zerteilen + + + Aus 100 km 50 km 25 km @@ -132,7 +131,7 @@ limitations under the License. - Nicht zerteilen + Aus 100 Meilen 50 Meilen 25 Meilen diff --git a/MyTracks/res/values-de/strings.xml b/MyTracks/res/values-de/strings.xml index c742a76d8..cfd7e873c 100644 --- a/MyTracks/res/values-de/strings.xml +++ b/MyTracks/res/values-de/strings.xml @@ -1,255 +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 - Mit Freunden teilen… - 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 + Alle Tracks exportieren + Exportieren beendet + Einen Track auswählen. Lange berühren für mehr Optionen. + Alle Tracks + Dieser Track + Mit Freunden teilen… + 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 @@ -257,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-rES/strings.xml b/MyTracks/res/values-es-rES/strings.xml index 1a660a31e..2cb837ae1 100644 --- a/MyTracks/res/values-es-rES/strings.xml +++ b/MyTracks/res/values-es-rES/strings.xml @@ -47,7 +47,6 @@ limitations under the License. Guardar en archivo TCX Enviar a Google Fusion Tables - Un enlace a Google Fusion Tables Un archivo CSV Un archivo TCX diff --git a/MyTracks/res/values-es/arrays.xml b/MyTracks/res/values-es/arrays.xml index 25bd36439..2c9ac030a 100644 --- a/MyTracks/res/values-es/arrays.xml +++ b/MyTracks/res/values-es/arrays.xml @@ -15,18 +15,6 @@ See the License for the specific language governing permissions and limitations under the License. --> - - - Apagado - 1 min - 2 min - 5 min - 10 min - 15 min - 30 min - 60 min - - 1 m @@ -112,9 +100,9 @@ limitations under the License. 3.1 miles (señal GPS débil) - - - No dividir + + + Apagado 100 km 50 km 25 km @@ -130,8 +118,8 @@ limitations under the License. 1 min - - No dividir + + Apagado 100 mi 50 mi 25 mi diff --git a/MyTracks/res/values-es/strings.xml b/MyTracks/res/values-es/strings.xml index 3fba9958b..fdb13e168 100644 --- a/MyTracks/res/values-es/strings.xml +++ b/MyTracks/res/values-es/strings.xml @@ -25,37 +25,37 @@ limitations under the License. Borrar todos los recorridos Configuraciones Elevación - Por favor seleccione el recorrido que desea ver + Por favor, seleccione el recorrido que desea ver Recorrido actual Último segmento - Ningún administrador de ubicación fue hallado en este aparato! - Ningún proveedor GPS de ubicación fue hallado en este aparato. - Hubo un error al mandar su recorrido a Google Fusion Tables. Por favor inténtelo de nuevo más tarde. - Hubo un error al mandar su recorrido a Google Docs. Por favor inténtelo de nuevo más tarde. - No pude empezar a grabar. Por favor inténtelo de nuevo. - No pude dejar de grabar. Por favor inténtelo de nuevo. + Ningún administrador de ubicación fue hallado en este dispositivo! + Ningún proveedor de ubicación GPS fue hallado en este dispositivo. + Se produjo un error al enviar su recorrido a Google Fusion Tables. + Se produjo un error al enviar su recorrido a Google Docs. + No se puede iniciar la grabación. Inténtelo de nuevo. + No se puede detener la grabación. Inténtelo de nuevo. No pude leer del archivo. El archivo no tiene un format GPX válido. - Error. Por favor inténtelo de nuevo. + A ocurrido un error. Por favor, inténtelo de nuevo. El recorrido no existe. El archivo es demasiado grande (tiene que ser menor a 500 KB). Abortando. - No pude insertar la señal. La ubicación no esta disponible. + No se puede insertar el marcador. La ubicación no está disponible. Esperando una ubicación mas exacta del GPS. Asegúrese que el cielo sea visible. Grabando un nuevo recorrido. - Grabacíon interrumpida. - Un mapa Google Fusion Tables nuevo has sido creado en google.com/fusiontables. + Grabación interrumpida. + Una nueva tabla de Google Fusion ha sido creada en google.com/fusiontables. Su recorrido ha sido enviado a maps.google.com. - Su recorrido ha sido enviado a una nueva hoja de cálculo en docs.google.com. + Su recorrido ha sido enviado a una nueva hoja de cálculo de docs.google.com. Su recorrido ha sido enviado a docs.google.com. - Por favor presione aquí para ir al menú de Seguridad y Ubicación y asegúrese que el GPS esté habilitado. Presione el botón de regreso para regresar. - Nota: Una ubicación aproximada es mostrada, basada en informacion de torres de telefonía celular y red WIFI. - La ubicación actual no puede ser determinada. Habilite más fuentes de datos (vea configuraciones de Seguridad y Ubicación) y asegúrese que el cielo sea visible. Inténtelo mas tarde. - Una señal de recorrido fue insertada. - Una señal con estadísticas fue insertada. + Por favor, presione aquí para ir al menú de Seguridad y Ubicación, y asegúrese que el GPS esté habilitado. Presione el botón "atrás" para regresar. + Nota: Una ubicación aproximada es determinada a partir de torres de telefonía celular y redes WIFI. + No puede ser determinada la ubicación actual. Habilite más fuentes de datos (vea las configuraciones de Seguridad y Ubicación) y asegúrese que el cielo sea visible. Inténtelo mas tarde. + Un marcador de recorrido fue insertado. + Un marcador de estadísticas fue insertado. Por favor espere... @@ -121,7 +121,6 @@ limitations under the License. Todos los recorridos:" Este recorrido" Compartir con amigos..." - Un enlace a Google Fusion Tables Un archivo GPX" Un archivo KML" "Limpiar mapa" @@ -213,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/arrays.xml b/MyTracks/res/values-fr/arrays.xml index 0c9eedf72..18740a364 100644 --- a/MyTracks/res/values-fr/arrays.xml +++ b/MyTracks/res/values-fr/arrays.xml @@ -15,18 +15,6 @@ See the License for the specific language governing permissions and limitations under the License. --> - - - Absent - 1 min - 2 min - 5 min - 10 min - 15 min - 30 min - 60 min - - 1 m @@ -112,10 +100,9 @@ limitations under the License. 3.1 miles (mauvais signal GPS) - - - - Pas de séparation + + + Absent 100 km 50 km 25 km @@ -131,8 +118,8 @@ limitations under the License. 1 min - - Pas de séparation + + Absent 100 mi 50 mi 25 mi @@ -310,5 +297,5 @@ limitations under the License. Pont SRM ANT+ Bluetooth Zephyr - + diff --git a/MyTracks/res/values-fr/strings.xml b/MyTracks/res/values-fr/strings.xml index eaabdce72..a1c00f1fc 100644 --- a/MyTracks/res/values-fr/strings.xml +++ b/MyTracks/res/values-fr/strings.xml @@ -127,7 +127,6 @@ limitations under the License. Tous les parcours: Ce parcours Partager... - Lien sur Google Fusion Tables Fichier GPX Fichier KML Effacer la carte @@ -161,7 +160,7 @@ limitations under the License. Partage Fréquence des annonces - Temps minimum entre eux annonces vocales + Temps minimum entre deux annonces vocales Distance min. entre 2 points Plus petite distance entre deux enregistrements de votre position @@ -214,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-it/arrays.xml b/MyTracks/res/values-it/arrays.xml index 4b8e02cb5..9974d2fc2 100644 --- a/MyTracks/res/values-it/arrays.xml +++ b/MyTracks/res/values-it/arrays.xml @@ -15,18 +15,6 @@ See the License for the specific language governing permissions and limitations under the License. --> - - - Disattivato - 1 min - 2 min - 5 min - 10 min - 15 min - 30 min - 60 min - - 1 m @@ -112,9 +100,9 @@ limitations under the License. 3.1 mi (segnale GPS debole) - - - Non dividere + + + Disattivato 100 km 50 km 25 km @@ -130,8 +118,8 @@ limitations under the License. 1 min - - Non dividere + + Disattivato 100 mi 50 mi 25 mi diff --git a/MyTracks/res/values-it/strings.xml b/MyTracks/res/values-it/strings.xml index 6451b836a..a3419cafd 100644 --- a/MyTracks/res/values-it/strings.xml +++ b/MyTracks/res/values-it/strings.xml @@ -129,7 +129,7 @@ limitations under the License. Tutti i percorsi: Questo percorso Condividi con amici... - + Un link alla mappa Un file GPX Un file KML Cancella la mappa diff --git a/MyTracks/res/values-ja/arrays.xml b/MyTracks/res/values-ja/arrays.xml index a3b6e1fde..31e45abee 100644 --- a/MyTracks/res/values-ja/arrays.xml +++ b/MyTracks/res/values-ja/arrays.xml @@ -15,19 +15,7 @@ See the License for the specific language governing permissions and limitations under the License. --> - - - 不使用 - 1 分 - 2 分 - 5 分 - 10 分 - 15 分 - 30 分 - 60 分 - - - + 1 m 2 m @@ -48,7 +36,6 @@ limitations under the License. 328 ft - 50 m @@ -68,7 +55,6 @@ limitations under the License. 3.1 mi - 電源節約 (動的制御) @@ -89,7 +75,6 @@ limitations under the License. 30 分 - 10 m (GPS信号強度良好) @@ -115,10 +100,9 @@ limitations under the License. 3.1 mi (GPS信号強度不良) - - - - 分割なし + + + 不使用 100 km 50 km 25 km @@ -134,8 +118,8 @@ limitations under the License. 1 分 - - 分割なし + + 不使用 100 mi 50 mi 25 mi @@ -287,4 +271,12 @@ 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 8895c3c22..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,21 +136,33 @@ limitations under the License. KMLファイルの保存 CVSファイルの保存 TCXファイルの保存 - Google Fusion Tablesへの送信 - 全トラックの削除 + Google Fusion Tablesに送信 + Googleマイマップに送信 + 全て削除 - 全トラックの出力 + 全て出力 出力終了 トラックを選択します.オプションを表示するには長押しして下さい. - 全てのトラック: + 全トラック: このトラック 共有... - Google Fusion Tablesへのリンク + マップへのリンク GPXファイル KMLファイル CSVファイル TCXファイル - マップの消去 + マップのクリア + + 全てインポート + インポート終了 + + %3$s からGPXファイル %2$d件のうち %1$d件をインポートしました + %s ではGPXファイルがありません.SDカードを読み込めないかディレクトリにGPXファイルが無いかも知れません. + ファイル %s のインポートエラー 全てのマーカーの削除 @@ -191,7 +215,7 @@ limitations under the License. マップの公開 アップロードされたマップはmaps.google.comで誰でも閲覧できます - アップロードされたマップはmaps.google.comのリストに公開されません. + アップロードされたマップはmaps.google.comのリストに公開されません 速度表示 速度が表示されます @@ -232,7 +256,6 @@ limitations under the License. SDカードに全てのトラックと設定をバックアップします 今すぐ復元 SDカードから全てのトラックと設定を復元します - トラック記録中はバックアップ操作はできません. SDカードからの復元を実行すると、現在のトラックと設定は *全て上書き* されてしまいます.よろしいですか? 復元するバックアップの選択 @@ -243,6 +266,23 @@ limitations under the License. GPS衛星 + + アカウントからのマップ選択: + 新規マップ作成... + Googleマイマップに送信 + マップをロード中... + 今回はマップをロードできませんでした! + このマップを選択 + マップ詳細編集 + トラック共有 + アカウントからマップを削除 + マップタイトル: + マップ説明: + このマップまたはコンテンツを検索結果に表示 + このマップまたはコンテンツを検索結果に表示しない(直接URLで見られる可能性があります) + このマップまたはコンテンツは検索結果に表示されます + このマップもコンテンツも検索結果に表示されませんが、直接リンクで見られる可能性があります + マップ共有 マップを共有したいと思います @@ -333,7 +373,6 @@ limitations under the License. ケイデンス 心拍数 最大心拍数 - - 待機中... @@ -351,8 +390,10 @@ limitations under the License. 送信... - Googleマイマップ + Googleマイマップ 今すぐ送信 + 新規マップ作成 + 既存マップ選択 トラック統計情報送信 統計情報とポイント送信 @@ -365,7 +406,7 @@ limitations under the License. 外部ストレージを発見できませんでした.SDカードが刺さっているか、端末がコンピュータにマウントされていないか確認してください. ファイルへの書き込みができません. ファイルからの読み込みができません. - SDカードにディレクトリを作成できません. + SDカード上でディレクトリを作成できません. SDカードへの書き込み終了. SDカードからの読み込み終了. @@ -398,11 +439,11 @@ limitations under the License. デフォルトの単位はメートルです.設定を変更しますか? - Bluetoothオプション ペアリング ペアリングを行うには「Bluetooth設定」を開いて下さい bluetoothSensorsEnabled + ANTオプション センサーオプション @@ -426,7 +467,7 @@ limitations under the License. トラック記録通知受信 トラック読み込み - トラック書き込み トラック記録の開始・終了 + トラック書き込みとトラック記録の開始・終了 トラック記録の開始・終了時に通知されます. ポイントやウェイポイントといったトラックの読み込みを可能にします. 記録開始やウェイポイント追加といったトラック記録を可能にします. diff --git a/MyTracks/res/values-pl/arrays.xml b/MyTracks/res/values-pl/arrays.xml index 80c7d0dbd..bf55da1a4 100644 --- a/MyTracks/res/values-pl/arrays.xml +++ b/MyTracks/res/values-pl/arrays.xml @@ -15,18 +15,6 @@ See the License for the specific language governing permissions and limitations under the License. --> - - - Brak - 1 min - 2 min - 5 min - 10 min - 15 min - 30 min - 60 min - - 1 m @@ -112,9 +100,9 @@ limitations under the License. 3.1 mili (słaby sygnał GPS) - - - Brak dzielenia + + + Brak 100 km 50 km 25 km @@ -130,8 +118,8 @@ limitations under the License. 1 min - - Brak dzielenia + + Brak 100 mi 50 mi 25 mi @@ -146,7 +134,7 @@ limitations under the License. 2 min 1 min - + Nigdy diff --git a/MyTracks/res/values-pl/strings.xml b/MyTracks/res/values-pl/strings.xml index 264a87990..d019ce3b4 100644 --- a/MyTracks/res/values-pl/strings.xml +++ b/MyTracks/res/values-pl/strings.xml @@ -131,7 +131,6 @@ limitations under the License. Wszystkie trasy: Ta trasa Podziel się z przyjaciółmi... - Plik GPX PLik KML Plik CSV @@ -218,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/arrays.xml b/MyTracks/res/values-pt-rBR/arrays.xml index 8f442d0fd..60090ae40 100644 --- a/MyTracks/res/values-pt-rBR/arrays.xml +++ b/MyTracks/res/values-pt-rBR/arrays.xml @@ -15,21 +15,8 @@ See the License for the specific language governing permissions and limitations under the License. --> - - - Desligado - 1 min - 2 min - 5 min - 10 min - 15 min - 30 min - 60 min - - - - + 1 m 2 m 3 m @@ -37,9 +24,9 @@ limitations under the License. 5 m (recomendada) 10 m 100 m - + - + 3 pés 6 pés 10 pés @@ -47,31 +34,29 @@ limitations under the License. 16 pés (recomendado) 33 pés 328 pés - - + - + 50 m 100 m 200 m (recomendado) 500 m 1000 m 5000 m - + - + 164 pés 328 pés 656 pés (recomendado) 1640 pés 0.6 milhas 3.1 milhas - - + - + economia de bateria (adaptativo) alta precisão (adaptativo) máxima (recomendada) @@ -88,11 +73,10 @@ limitations under the License. 10 min 15 min 30 min - - + - + 10 m (sinal de GPS excelente) 20 m 50 m @@ -102,9 +86,9 @@ limitations under the License. 1000 m 2000 m 5000 m (sinal de GPS ruim) - + - + 33 pés (sinal de GPS excelente) 66 pés 165 pés @@ -114,12 +98,11 @@ limitations under the License. 0.6 milhas 1.2 milhas 3.1 milhas (sinal de GPS ruim) - + - - - - Sem divisões + + + Desligado 100 km 50 km 25 km @@ -135,8 +118,8 @@ limitations under the License. 1 min - - Sem divisões + + Desligado 100 mi 50 mi 25 mi diff --git a/MyTracks/res/values-pt-rBR/strings.xml b/MyTracks/res/values-pt-rBR/strings.xml index 5dfb033b3..46f347e11 100644 --- a/MyTracks/res/values-pt-rBR/strings.xml +++ b/MyTracks/res/values-pt-rBR/strings.xml @@ -18,9 +18,9 @@ limitations under the License. Gravar caminho Para gravação - Modo do satélite" - Modo do mapa" - Minha Localização" + Modo do satélite + Modo do mapa + Minha Localização Caminhos Marcadores Apagar todos os caminhos @@ -31,10 +31,10 @@ limitations under the License. Último segmento - Gerenciador de localização não encontrado neste dispositivo!" - "Provedor de posição de GPS não encontrado neste dispositivo." - Ocorreu um erro ao enviar seu caminho para o Google Fusion Tables. Por favor tente novamente mais tarde." - Ocorreu um erro ao enviar seu caminho para o Google Docs. Por favor tente novamente mais tarde." + Gerenciador de localização não encontrado neste dispositivo! + "Provedor de posição de GPS não encontrado neste dispositivo. + Ocorreu um erro ao enviar seu caminho para o Google Fusion Tables. Por favor tente novamente mais tarde. + Ocorreu um erro ao enviar seu caminho para o Google Docs. Por favor tente novamente mais tarde. Impossível iniciar a gravação. Tente novamente. Impossível interromper a gravação. Tente novamente. Impossível ler do arquivo. @@ -52,7 +52,7 @@ limitations under the License. Seu caminho foi enviado para maps.google.com. Seu caminho foi enviado para uma nova planilha em docs.google.com. Seu caminho foi enviado para docs.google.com. - "Por favor pressione aqui para ir aos ajustes de segurança e localização e verifique que o GPS esteja habilitado. Pressione o botão de voltar para retornar." + "Por favor pressione aqui para ir aos ajustes de segurança e localização e verifique que o GPS esteja habilitado. Pressione o botão de voltar para retornar. Nota: Uma localização aproximada, determinada por torres de telefonia celular, está sendo mostrada. Sua localização não pôde ser determinada. Habilite fontes adicionais de localização (veja configurações de Segurança e Localização) e assegure-se de que o céu está visível. Um marcador de ponto de passagem foi inserido. @@ -79,23 +79,24 @@ limitations under the License. O marcador será permanentemente apagado deste telefone. - "Criado por My Tracks no Android." - "Criado por <a href='http://mytracks.appspot.com'>My Tracks</a> no Android." + "Criado por My Tracks no Android. + "Criado por <a href='http://mytracks.appspot.com'>My Tracks</a> no Android. Pontos - Gravado" - "Tipo de atividade" - "Tipo de marcador" - "(Parte %d)" + Gravado + "Tipo de atividade + "Tipo de marcador + "(Parte %d) - "Gravando seu caminho..." - "Caminho %d" + "Gravando seu caminho... + "Caminho %d Não Sim + Erro Successo Ponto de passagem @@ -122,22 +123,22 @@ limitations under the License. Enviar para o Google Fusion Tables Apagar todos os caminhos Escolha um caminho. Pressione&segure para mais opções. - Todos os Caminhos:" - Este Caminho" - Compartilhar com amigos..." - Um link para a Google Fusion Table" - Um arquivo GPX" - Um arquivo KML" - "Limpar mapa" + Todos os Caminhos: + Este Caminho + Compartilhar com amigos... + Um link para a mapa + Um arquivo GPX + Um arquivo KML + Limpar mapa Apagar todos os marcadores Este Marcador Apagar - Editar detalhes" - Mostrar no mapa" - Inserir ponto no caminho" - Inserir estatísticas" + Editar detalhes + Mostrar no mapa + Inserir ponto no caminho + Inserir estatísticas Escolha um marcador. Pressione&segure para mais opções. @@ -157,6 +158,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-ru/arrays.xml b/MyTracks/res/values-ru/arrays.xml new file mode 100755 index 000000000..feb32549f --- /dev/null +++ b/MyTracks/res/values-ru/arrays.xml @@ -0,0 +1,282 @@ + + + + + + 1 м + 2 м + 3 м + 4 м + 5 м (рекомендуется) + 10 м + 100 м + + + + 3 фута + 6 футов + 10 футов + 13 футов + 16 футов (рекомендуется) + 33 фута + 328 футов + + + + + 50 м + 100 м + 200 м (рекомендуется) + 500 м + 1000 м + 5000 м + + + + 164 фута + 328 футов + 656 фута (рекомендуется) + 1640 футов + 0,6 мили + 3,1 мили + + + + + экономия батареи (адаптивная) + высокая точность (адаптивная) + высочайшая точность (рекомендуется) + 2 с + 3 с + 4 с + 5 с + 10 с + 20 с + 30 с + 1 мин + 2 мин + 5 мин + 10 мин + 15 мин + 30 мин + + + + + 10 м (отличный сигнал GPS) + 20 м + 50 м + 100 м + 200 м (рекомендуется) + 500 м + 1000 м + 2000 м + 5000 м (плохой сигнал GPS) + + + + 33 фута (отличный сигнал GPS) + 66 футов + 165 футов + 328 футов + 656 футов (рекомендуется) + 1640 футов + 0,6 мили + 1,2 мили + 3,1 мили (плохой сигнал GPS) + + + + + Выкл. + 100 км + 50 км + 25 км + 10 км + 5 км + 1 км + 60 мин + 30 мин + 15 мин + 10 мин + 5 мин + 2 мин + 1 мин + + + + Выкл. + 100 миль + 50 миль + 25 миль + 10 миль + 5 миль + 1 миля + 60 мин + 30 мин + 15 мин + 10 мин + 5 мин + 2 мин + 1 мин + + + + + Никогда + 5 мин + 10 мин + 15 мин + Всегда + + + + + не определен + смешанного типа + другое + + бег + бег по улице + бег по треку + бег по тропе + ходьба + быстрая ходьба + пешая прогулка + прогулка по туристической тропе + прогулка по бездорожью + восхождение + + велосипедная прогулка + шоссейный велосипед + горный велосипед + велотрек + + катание на коньках + катания на роликовых коньках + катания на роликовых коньках + + прогулка на снегоступах + лыжи + сноубординг + сани + ледяной парусный спорт + + парусный спорт + гребля + каякинг + академическая гребля + моторная лодка + паром + RC лодка + серфинг + виндсерфинг + кайтсерфинг + водные лыжи + вейкбординг + + самолет + гидроплан + коммерческий самолет + вертолет + воздушный шар + дирижабль + RC самолет + RC вертолет + прыжки + планеризм + дельтаплан + парный планеризм + + верховая езда + катание на лошадях + прогулка на осле + + вождение автомобиля + вождение автобуса + поезд + мотоцикл + Байк + ATV + + парусный спорт на земле + фрисби + алтимат фризби + + + + банкомат + банк + бар + барбекю + здание + автобусная остановка + кафе + костер + лагерь + церковь + кофейня + удобный магазин + питьевая вода + оползень + паром + камин + пожарная станция + рыболовство + автозаправочная станция + поле для гольфа + продуктовый магазин + парикмахерская + гавань + тропа + отель + дом + информация + пересечение + почтовый ящик + рынок + место встречи + опера + парковка + почтовое отделение + выбоина + полицейский участок + железнодорожный переезд + ресторан + покупки + вид + магазин + бассейн + телефон + театр + туалет + туристическая информация + туннель + дерево + водопад + фонтан + путевая точка + + + + Нет + ANT+ Sensors + SRM ANT+ Bridge + Zephyr Bluetooth + + + diff --git a/MyTracks/res/values-ru/strings.xml b/MyTracks/res/values-ru/strings.xml new file mode 100644 index 000000000..062383b40 --- /dev/null +++ b/MyTracks/res/values-ru/strings.xml @@ -0,0 +1,501 @@ + + + + + Запись трека + Остановить запись + Режим спутника + Режим карты + Мое местоположение + Треки + Маркеры + Удалить все треки + Настройки + Высота + Пожалуйста, выберите трек для просмотра + Текущий трек + Последний сегмент + Состояние датчика + Совокупная статистика + + + На этом устройстве не был обнаружен диспетчер местоположения! + На этом устройстве не был обнаружен поставщик GPS местоположения. + Ошибка при отправке вашего трека в Google \"Мои карты\". + Ошибка при отправке вашего трека в Google Fusion Tables. + Ошибка при отправке вашего трека в Google Документы. + Пожалуйста, повторите попытку позже. + Не удалось начать запись. Попробуйте еще раз. + Не удалось остановить запись. Попробуйте еще раз. + Не удалось прочитать из файла. + Неверный формат GPX файла. + Произошла ошибка. Пожалуйста, попробуйте еще раз. + Трек не существует. + Этот файл слишком большой (должен быть меньше, чем 500 КБ). Прервано. + Невозможно вставить маркер. Местоположение неизвестно. + + + Ожидание сигнала GPS. Убедитесь в том, что вы находитесь под открытым небом. + Записывается новый трек. + Запись вашего трека остановлена. + Новая Google \"Мои карты\" была создана на maps.google.com. + Новая таблица Google Fusion была создана на google.com/fusiontables. + Твой трек был отправлен на maps.google.com. + Ваш трек был отправлен в новую электронную таблицу на docs.google.com. + Ваш трек был отправлен в docs.google.com. + Это может занять несколько минут, прежде чем появятся новые треки. + Пожалуйста, нажмите здесь для перехода к разделу настроек Координаты и отметьте пункт \"Спутники GPS\". Для возвращения нажмите кнопку Назад. + Примечание: приблизительное местоположение определено относительно вышек сотовой связи и беспроводных сетей. + В настоящее время местоположение не может быть определено. Включите больше источников местоположение (см. раздел настроек Координаты) и убедитесь в том, что находитесь под открытым небом. Повторите попытку позднее. + Маркер путевой точки был вставлен. + Маркер статистики был вставлен. + Статус загрузки: + + + Пожалуйста, подождите... + Отправка вашего трека в Google. + Чтение трека... + Подготовка трека... + Аутентификация на Google Fusion Tables... + Аутентификация на Google Карты... + Аутентификация в Google Документы... + Отправка трека в Google Документы... + Отправка трека в Google Карты... + Создание новой \"Моей карты\" ... + Отправка трека в Fusion Tables... + Создание новой Fusion table... + Импорт GPS трека. Это может занять некоторое время. + Запись файла на SD карту. Это может занять некоторое время. + Обновление статистики трека... + + + Вы уверены? + Все треки и маркеры будут удалены с этого телефона. + Трек и его маркеры будут удалены из этого телефона. + Маркер будет удален с этого телефона. + + + Создано My Tracks на Android. + Создано <a href=\'http://mytracks.appspot.com\'>My Tracks</a> на Android. + + + Создано <a href=\'http://mytracks.appspot.com\'>My Tracks</a> на Android. + Новая карта + + + Точки + Записан + Тип деятельности + Тип маркера + (Часть %d) + + + Запись трека... + Трек %d + + + Нет + Да + ОК + Ошибка + Готово + Путевая точка + Лицензия + Отмена + Закрыть + Правка + Сохранить + + + (Старт) + (Конец) + + + Показать + Отправить в Google \"Мои карты\" + Отправить в новый Google \"Мои карты\" + Отправить в Google Fusion Tables + Отправить в новую Google Fusion Table + Отправить в Google Документы + Отправить в Google ... + Удалить из телефона... + Править детали + Записать на SD карту... + Сохранить файл GPX + Сохранить файл KML + Сохранить файл CSV + Сохранить файл TCX + Отправить в Google Fusion Tables + Отправить в Google \"Мои карты\" + Удалить все + + Экспорт всех + Экспорт закончен + Выберите трек. Нажмите и удерживайте для получения дополнительных возможностей. + Все треки: + Этот трек + Поделиться с друзьями... + Ссылка на карту + Файл GPX + Файл KML + Файла CSV + Файл TCX + Очистить карту + + Импорт всех + Импорт закончен + + Импортировано %1$d из %2$d GPX файлов с %3$s + GPX файлы не найденны в %s. Возможно не читается SD-карта или каталог не содержит GPX файлы. + Ошибка импорта файла %s + + + Удалить все маркеры + Этот маркер + Удалить + Править детали + Показать на карте + Вставить путевую точку + Вставить статистику + Выберите маркер. Нажмите и удерживайте для получения дополнительных возможностей. + + + Трек детали + Название трека + Описание трека + Тип деятельности (например: пешая прогулка, велосипедная прогулка, бег, восхождение...) + + + Маркер детали + Имя маркера + Описание маркера + Тип маркера (например: здание, выбоина, достопримечательность...) + + + Настройки My Tracks + Отображение + Запись + Обмен + + Не доступен в вашем версии Android + Не доступно во время записи трека + + Частота объявлений + Минимальное время между двумя голосовыми объявлениями + + Минимальное расстояние между точками + Наименьшее расстояние между двумя записями вашей позиции + + Максимальное расстояние между точками + Если два пункта находятся дальше, чем указано, то трек будет разделен + + Минимальное время между точками + Наименьший интервал времени между двумя записями вашей позиции + + Минимальная точность + Запись будет приостановлена, если ваша позиция не является достаточно точной + + Использовать метрические единицы + Все расстояния будут отображаться в метрических единицах + Все расстояния будут отображаться в имперских единицах + + Обнародовать карты + Загруженные карты будут общедоступны на maps.google.com + Загруженные карты не будут перечислены публично на maps.google.com + + Отображать скорость + Скорость будет отображаться + Темп будет отображаться + + Поделиться только URL + При обмене будет отправлен только URL-адрес + Содержимое трека будет также включено при обмене + + Частота деления + Максимальное время или расстояние, после которого будет вставлен новый маркер деления + + Автоматическое возобновление трек тайм-аут + Максимальное время в минутах, после которого предыдущая запись трека не будет возобновлена на перезагруженном телефоне + + Штамп времени как название дорожки + По умолчанию имя трека будет складываться из даты и времени старта трека + Названия треков будут иметь прирост чисел для каждого нового трека. + + Восстановить значения по умолчанию + Восстановить значения по умолчанию во всех настройках + Все ваши настройки будут стерты и восстановлены значения по умолчанию. Записанные треки будут сохранены. Вы уверены? + Все настройки были возвращены к значениям по умолчанию. + + Темп + + Поддерживается только в Android 2.0 или выше + Тип датчика + Тип подключенного датчика + Очистить ANT подсоединенного датчика пульса + Очистить ANT подсоединенного датчика мощности + Очистить ANT подсоединенного датчика SRM + ANT датчик не подсоединен + ANT датчик подсоединен с: %d + Bluetooth датчик + Подключенный bluetooth датчик + Отображение модульной статистики + + + Резервное копирование (BETA) + Резервное копирование SD карты + Резервировать + Создать полную копию всех ваших треков и настроек на SD карту + Восстановить + Восстановить все треки и параметры с SD карты + + Восстановление резервной копии с SD карты ПЕРЕЗАПИШЕТ все текущие треки и настройки. Вы уверены? + Выберите резервную копию для восстановления + Резервные копии не найдены. + Запись на SD-карту всех треков и настроек. Это может занять некоторое время... + Чтение всех треков и настроек с SD-карты. Это может занять некоторое время... + + + Спутники GPS + + + Выберите карту из вашей учетной записи: + Создать новую карту... + Отправить в Google \"Мои карты\" + Загрузка карты ... + Ваши карты не могут быть загружены в данный момент! + Выберите эту карту + Править детали карты + Поделиться треком + Удалить Карту из вашей учетной записи + Название карты: + Описание карты: + Сделать чтобы карта и ее содержимое отображались в результатах поиска + Не позволить этой карте или ее содержимому отображаться в результатах поиска, хотя они все еще могут просматриваться другими через прямой URL-адрес + Эта карта и ее содержимое будут отображаться в результатах поиска + Ни эта карта, ни ее содержимое не будут отображаться в результатах поиска, но их могут по-прежнему просматривать другие через прямую ссылку + + + Поделиться картой с друзьями + Я хотел бы поделиться с вами треком + Я думаю, вы можете быть заинтересованы в этом треке: + +%s + Я хотел бы поделиться с вами треком + Я думаю, вы можете быть заинтересованы в этом треке. + +Просмотреть трек можно с помощью Google Планета Земля или другого программного обоспечения для карт. + + + Общественные + Частные + У вас пока еще нет карт. + + + + Средняя скорость движения + Средний темп движения + + + Средняя скорость + Средний темп + + Диаграмма высоты + Набор высоты + Высота + Широта + Долгота + + + Минимальная высота + Минимальный уклон + Минимальный темп + + + Максимальная высота + Максимальный уклон + Максимальная скорость + + Загрузка трека... + + + Настройки диаграммы + + Ось X + По расстоянию + По времени + + + Ось Y + + + + час + часов + ч + мин + минута + минут + секунда + секунд + мс + + + м + км + километров + футов + миль + мили + + + км/ч + километров в час + мин/км + за километр + миль/ч + миль в час + мин/миля + за милю + + + Вт + + об/мин + bpm + + + Время движения + Общее время + Время + SD карта недоступна + Скорость + Вы должны начать путешествие. + Статистика + Общее расстояние + Расстояние + Мощность + Каденс + Пульс + Максимальный пульс + - + Ожидание... + + + Лицензионное соглашение + Принять + Отклонить + + + О программе + Справка + Команда: + Версия: + Поддержка: + Приватность: + + + Отправить... + Google Fusion Tables + Google \"Мои карты\" + Google Документы + Отправить + Создать новую карту + Выбрать существующую карту + Отправить статистику трека + Отправить статистику и точки + + + Учетная запись не найдена + Не найдена учетная запись для загрузки треков. + Выбор учетной записи + + + Не обнаружено внешнее устройство хранения данных. Убедитесь, что SD карта вставлена, и что ваш телефон не подключен к компьютеру. + Невозможно записать файл. + Невозможно прочитать из файла. + Невозможно создать папку на SD карте. + Запись на SD карту закончена. + Чтение с SD карты закончено. + + + Проверить обновления + Обновить сейчас + Продолжить без обновления + Ваша версия %1$S больше не может работать должным образом, и больше не поддерживается. Обновите %1$s для продолжения использования. + Ваша версия %1$S больше не может работать должным образом. Вы можете обновить ее сейчас, либо продолжать использовать ее на свой страх и риск. Некоторые функции, такие как синхронизация с сервером может не функционировать до обновления. + Доступна новая версия %1$s. Вы можете продолжать использовать текущую версию без проблем, но вы не получите новейшие функции, до тех пор, пока не обновитесь. + + + Добро пожаловать + Ок + О My Tracks + + + My Tracks нет расстояния + + Мои треки %1$s %2$.2f %3$s в %4$s на %5$s %6$s + + + Единицы измерения обратите внимание + По умолчанию используются метрические единицы. Вы хотели бы изменить этот параметр? + + + Bluetooth + Параметры Bluetooth + Подключение устройства + Открыть настройки Android по bluetooth для подключения устройства + bluetoothSensorsEnabled + Параметры ANT + Параметры датчика + + + Подключен к %s + Подключение Bluetooth датчика... + Bluetooth датчик не подключен + + + Датчик времени + Измеритель мощности включен + Монитор пульса включен + Нет диспетчера датчиков + Включено + Подключение + Подключено + Отключено + Нет службы датчиков + Поиск + Отправка + + + получать уведомления о записи трека + чтение записанных данных трека + запись данных трека, старт/стоп записи треков + Получать уведомления, при старте или останове записи трека. + Иметь возможность читать данные трека, такие как точки и путевые точки. + Иметь возможность записать данные трека, такие как начало записи или добавление путевых точек. + diff --git a/MyTracks/res/values-sv/arrays.xml b/MyTracks/res/values-sv/arrays.xml index eb24d0ef3..fef8a4ec9 100644 --- a/MyTracks/res/values-sv/arrays.xml +++ b/MyTracks/res/values-sv/arrays.xml @@ -15,18 +15,6 @@ See the License for the specific language governing permissions and limitations under the License. --> - - - Av - 1 min - 2 min - 5 min - 10 min - 15 min - 30 min - 60 min - - 1 m @@ -67,7 +55,6 @@ limitations under the License. 3.1 miles - batterioptimerad (anpassningsbar) @@ -113,9 +100,9 @@ limitations under the License. 3.1 miles (dålig GPS-signal) - - - Ingen delning + + + Av 100 km 50 km 25 km @@ -131,8 +118,8 @@ limitations under the License. 1 min - - Ingen delning + + Av 100 mi 50 mi 25 mi diff --git a/MyTracks/res/values-sv/strings.xml b/MyTracks/res/values-sv/strings.xml index a74ef8b99..950e8b1dc 100644 --- a/MyTracks/res/values-sv/strings.xml +++ b/MyTracks/res/values-sv/strings.xml @@ -131,7 +131,6 @@ limitations under the License. Alla spår: Det här spåret Dela med vänner... - En GPX-fil En KML-fil En CSV-fil diff --git a/MyTracks/res/values-zh-rCN/arrays.xml b/MyTracks/res/values-zh-rCN/arrays.xml index 48f15e956..e3108f9e2 100644 --- a/MyTracks/res/values-zh-rCN/arrays.xml +++ b/MyTracks/res/values-zh-rCN/arrays.xml @@ -15,18 +15,6 @@ See the License for the specific language governing permissions and limitations under the License. --> - - - 关闭 - 1 分钟 - 2 分钟 - 5 分钟 - 10 分钟 - 15 分钟 - 30 分钟 - 60 分钟 - - 1 米 @@ -112,9 +100,9 @@ limitations under the License. 3.1 英里 (糟糕的 GPS 信号) - - - 不分割 + + + 关闭 100 千米 50 千米 25 千米 @@ -130,8 +118,8 @@ limitations under the License. 1 分钟 - - 不分割 + + 关闭 100 英里 50 英里 25 英里 @@ -155,6 +143,7 @@ limitations under the License. 15 分钟 总是 + 未知 diff --git a/MyTracks/res/values-zh-rCN/strings.xml b/MyTracks/res/values-zh-rCN/strings.xml index d9b27a43c..befedd9c7 100644 --- a/MyTracks/res/values-zh-rCN/strings.xml +++ b/MyTracks/res/values-zh-rCN/strings.xml @@ -132,7 +132,6 @@ limitations under the License. 全部轨迹: 此轨迹 与朋友分享... - 链接到 Google Fusion Table GPX 文件 KML 文件 CSV 文件 @@ -220,7 +219,6 @@ limitations under the License. 将你所有轨迹和设置的完整备份写入到 SD 存储卡 立即恢复 从 SD 存储卡恢复所有轨迹和设置 - 在记录轨迹时无法处理备份数据. 从 SD 存储卡恢复备份数据将覆盖掉你当前的所有轨迹和设置. 确定要继续吗? diff --git a/MyTracks/res/values/arrays.xml b/MyTracks/res/values/arrays.xml index 964befc7d..783668362 100644 --- a/MyTracks/res/values/arrays.xml +++ b/MyTracks/res/values/arrays.xml @@ -15,31 +15,6 @@ See the License for the specific language governing permissions and limitations under the License. --> - - - Off - 1 min - 2 min - 5 min - 10 min - 15 min - 30 min - 60 min - - - - - -1 - 1 - 2 - 5 - 10 - 15 - 30 - 60 - - - 1 m @@ -61,18 +36,6 @@ limitations under the License. 328 ft - - - 1 - 2 - 3 - 4 - 5 - 10 - 100 - - - 50 m @@ -92,17 +55,6 @@ limitations under the License. 3.1 miles - - - 50 - 100 - 200 - 500 - 1000 - 5000 - - - battery miser (adaptive) @@ -123,27 +75,6 @@ limitations under the License. 30 min - - - -2 - -1 - 0 - 2 - 3 - 4 - 5 - 10 - 20 - 30 - 60 - 120 - 300 - 480 - 900 - 1800 - - - 10 m (excellent GPS signal) @@ -169,23 +100,9 @@ limitations under the License. 3.1 miles (poor GPS signal) - - - 10 - 20 - 50 - 100 - 200 - 500 - 1000 - 2000 - 5000 - - - - - - No splitting + + + Off 100 km 50 km 25 km @@ -201,8 +118,8 @@ limitations under the License. 1 min - - No splitting + + Off 100 mi 50 mi 25 mi @@ -218,24 +135,6 @@ limitations under the License. 1 min - - - 0 - -100 - -50 - -25 - -10 - -5 - -1 - 60 - 30 - 15 - 10 - 5 - 2 - 1 - - Never @@ -245,16 +144,6 @@ limitations under the License. Always - - - 0 - 5 - 10 - 15 - -1 - - - unknown @@ -383,14 +272,6 @@ limitations under the License. way point - - - GPX - KML - CSV - TCX - - None ANT+ Sensors @@ -398,12 +279,4 @@ limitations under the License. Zephyr Bluetooth - - - @string/none - @string/ant_sensor_type - @string/srm_ant_bridge_sensor_type - @string/zephyr_sensor_type - - diff --git a/MyTracks/res/values/arrays_do_not_translate.xml b/MyTracks/res/values/arrays_do_not_translate.xml new file mode 100644 index 000000000..219049f4f --- /dev/null +++ b/MyTracks/res/values/arrays_do_not_translate.xml @@ -0,0 +1,122 @@ + + + + + + 10 + 20 + 50 + 100 + 200 + 500 + 1000 + 2000 + 5000 + + + + 1 + 2 + 3 + 4 + 5 + 10 + 100 + + + + 1 + 2 + 3 + 4 + 5 + 10 + 100 + + + + 50 + 100 + 200 + 500 + 1000 + 5000 + + + + -2 + -1 + 0 + 2 + 3 + 4 + 5 + 10 + 20 + 30 + 60 + 120 + 300 + 480 + 900 + 1800 + + + + 0 + -100 + -50 + -25 + -10 + -5 + -1 + 60 + 30 + 15 + 10 + 5 + 2 + 1 + + + + 0 + 5 + 10 + 15 + -1 + + + + GPX + KML + CSV + TCX + + + + @string/none + @string/ant_sensor_type + @string/srm_ant_bridge_sensor_type + @string/zephyr_sensor_type + + + + @string/ant_sensor_type + @string/srm_ant_bridge_sensor_type + + 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 b8e43f668..01f56da24 100644 --- a/MyTracks/res/values/keys.xml +++ b/MyTracks/res/values/keys.xml @@ -36,6 +36,7 @@ limitations under the License. sendStatsAndPoints sendToDocs sendToMyMaps + sendToFusionTables sensorType shareUrlOnly splitFrequency @@ -49,4 +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 bd8063280..b7afaee25 100644 --- a/MyTracks/res/values/strings.xml +++ b/MyTracks/res/values/strings.xml @@ -35,14 +35,17 @@ 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 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. File does not have a valid GPX format. An error occurred. Please try again. Track does not exist. + The operation was cancelled. This file is too big (must be less than 500 KB). Aborting. Unable to insert marker. No location fix available. @@ -50,23 +53,30 @@ limitations under the License. Waiting for a better GPS fix. Make sure the sky is visible. Now recording a new track. Stopped recording your track. + A new Google My Map has been created at maps.google.com. A new Google Fusion table has been created at google.com/fusiontables. 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 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... Sending your track to Google. Reading track... + Preparing track... Authenticating to Google Fusion Tables... + Authenticating to Google Maps... Authenticating to Google Docs... Sending track to Google Docs... + Sending track to Google Maps... + Creating new My Map... Sending track to Fusion Tables... Creating new Fusion table... Importing a GPS track. This may take a while. @@ -83,6 +93,10 @@ limitations under the License. Created by My Tracks on Android. Created by <a href=\'http://mytracks.appspot.com\'>My Tracks</a> on Android. + + Created by <a href=\'http://mytracks.appspot.com\'>My Tracks</a> on Android. + New map + Points Recorded @@ -113,6 +127,8 @@ limitations under the License. Show + Send to Google My Maps + Send to New Google My Map Send to Google Fusion Tables Send to New Google Fusion Table Send to Google Docs @@ -125,6 +141,7 @@ limitations under the License. Save a CSV file Save a TCX file Send to Google Fusion Tables + Send to Google My Maps Delete all Export all @@ -133,7 +150,7 @@ limitations under the License. All Tracks: This Track Share with Friends... - A link to Google Fusion Tables + A link to the map A GPX file A KML file A CSV file @@ -180,6 +197,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 @@ -222,6 +240,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 @@ -229,7 +252,7 @@ limitations under the License. The type of sensor to connect to Clear ANT heart rate sensor pairing Clear ANT power sensor pairing - Clear ANT srm bridge sensor pairing + Clear ANT SRM bridge sensor pairing ANT sensor not paired ANT sensor paired with: %d Bluetooth sensor @@ -243,7 +266,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 @@ -254,6 +276,23 @@ limitations under the License. GPS Satellites + + Pick a map from your account: + Create new map... + Send to Google My Maps + Loading maps... + Your maps could not be loaded at this time! + Select this Map + Edit Map Details + Share Track + Delete Map from your Account + Map Title: + Map Description: + Make this map and its contents show up in search results + Do not let this map or its contents show up in search results, though they may still be viewed be others through the direct URL + This map and its contents will show in search results + Neither this map nor its contents will show in search results, but they may still be viewed by others through a direct link + Share map with friends I would like to share a track with you @@ -264,7 +303,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 @@ -377,8 +415,11 @@ limitations under the License. Send to... Google Fusion Tables + Google My Maps Google Docs Send now + Create a new map + Pick an existing map Send track statistics Send statistics and points @@ -430,6 +471,7 @@ limitations under the License. Device pairing Open Android\'s bluetooth settings to pair a device bluetoothSensorsEnabled + ANT options Sensor Options diff --git a/MyTracks/res/xml/preferences.xml b/MyTracks/res/xml/preferences.xml index 9a4c9392e..d964f124b 100644 --- a/MyTracks/res/xml/preferences.xml +++ b/MyTracks/res/xml/preferences.xml @@ -48,8 +48,8 @@ limitations under the License. android:key="@string/split_frequency_key" android:title="@string/settings_split_frequency" android:summary="@string/settings_split_frequency_summary" - android:entries="@array/split_frequency_options" - android:entryValues="@array/split_frequency_values" + android:entries="@array/task_frequency_options" + android:entryValues="@array/task_frequency_values" android:defaultValue="0" /> - + - - + + - - + + + @@ -170,4 +173,11 @@ limitations under the License. android:persistent="false" /> + + + diff --git a/MyTracks/src/com/dsi/ant/AntInterface.java b/MyTracks/src/com/dsi/ant/AntInterface.java index f527c20f5..05583d857 100644 --- a/MyTracks/src/com/dsi/ant/AntInterface.java +++ b/MyTracks/src/com/dsi/ant/AntInterface.java @@ -45,7 +45,7 @@ public class AntInterface { public static final String TAG = "ANTInterface"; /** Enable debug logging. */ - public static boolean DEBUG = true; + public static boolean DEBUG = false; /** Search string to find ANT Radio Proxy Service in the Android Marketplace */ private static final String MARKET_SEARCH_TEXT_DEFAULT = "ANT Radio Service Dynastream Innovations Inc"; diff --git a/MyTracks/src/com/google/android/apps/mytracks/AccountChooser.java b/MyTracks/src/com/google/android/apps/mytracks/AccountChooser.java index cfd1cda39..7a648a304 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/AccountChooser.java +++ b/MyTracks/src/com/google/android/apps/mytracks/AccountChooser.java @@ -59,7 +59,7 @@ public class AccountChooser { public void chooseAccount(final Activity activity, final AccountHandler handler) { final Account[] accounts = AccountManager.get(activity) - .getAccountsByType(MyTracksConstants.ACCOUNT_TYPE); + .getAccountsByType(Constants.ACCOUNT_TYPE); if (accounts.length < 1) { alertNoAccounts(activity, handler); return; @@ -76,7 +76,7 @@ public class AccountChooser { } // Let the user choose. - Log.e(MyTracksConstants.TAG, "Multiple matching accounts found."); + Log.e(Constants.TAG, "Multiple matching accounts found."); final AlertDialog.Builder builder = new AlertDialog.Builder(activity); builder.setTitle(R.string.choose_account_title); builder.setCancelable(false); @@ -112,7 +112,7 @@ public class AccountChooser { */ private void alertNoAccounts(final Activity activity, final AccountHandler handler) { - Log.e(MyTracksConstants.TAG, "No matching accounts found."); + Log.e(Constants.TAG, "No matching accounts found."); final AlertDialog.Builder builder = new AlertDialog.Builder(activity); builder.setTitle(R.string.no_account_found_title); builder.setMessage(R.string.no_account_found); diff --git a/MyTracks/src/com/google/android/apps/mytracks/AggregatedStatsActivity.java b/MyTracks/src/com/google/android/apps/mytracks/AggregatedStatsActivity.java index c4e4e0728..cb385f35c 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/AggregatedStatsActivity.java +++ b/MyTracks/src/com/google/android/apps/mytracks/AggregatedStatsActivity.java @@ -40,7 +40,7 @@ public class AggregatedStatsActivity extends Activity implements @Override public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { - Log.d(MyTracksConstants.TAG, "StatsActivity: onSharedPreferences changed " + Log.d(Constants.TAG, "StatsActivity: onSharedPreferences changed " + key); if (key != null) { if (key.equals(R.string.metric_units_key)) { @@ -67,7 +67,7 @@ public class AggregatedStatsActivity extends Activity implements sv.setScrollBarStyle(ScrollView.SCROLLBARS_OUTSIDE_INSET); SharedPreferences preferences = getSharedPreferences( - MyTracksSettings.SETTINGS_NAME, 0); + Constants.SETTINGS_NAME, 0); if (preferences != null) { metricUnits = preferences.getBoolean(getString(R.string.metric_units_key), true); preferences.registerOnSharedPreferenceChangeListener(this); diff --git a/MyTracks/src/com/google/android/apps/mytracks/MyTracksReceiver.java b/MyTracks/src/com/google/android/apps/mytracks/BootReceiver.java similarity index 84% rename from MyTracks/src/com/google/android/apps/mytracks/MyTracksReceiver.java rename to MyTracks/src/com/google/android/apps/mytracks/BootReceiver.java index 9847ab2ee..3ed021fb8 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/MyTracksReceiver.java +++ b/MyTracks/src/com/google/android/apps/mytracks/BootReceiver.java @@ -16,8 +16,8 @@ package com.google.android.apps.mytracks; import static android.content.Intent.ACTION_BOOT_COMPLETED; -import static com.google.android.apps.mytracks.MyTracksConstants.RESUME_TRACK_EXTRA_NAME; -import static com.google.android.apps.mytracks.MyTracksConstants.TAG; +import static com.google.android.apps.mytracks.Constants.RESUME_TRACK_EXTRA_NAME; +import static com.google.android.apps.mytracks.Constants.TAG; import com.google.android.apps.mytracks.services.TrackRecordingService; @@ -41,17 +41,17 @@ import android.util.Log; * * @author Bartlomiej Niechwiej */ -public class MyTracksReceiver extends BroadcastReceiver { +public class BootReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { - Log.d(TAG, "MyTracksReceiver.onReceive: " + intent.getAction()); + Log.d(TAG, "BootReceiver.onReceive: " + intent.getAction()); if (ACTION_BOOT_COMPLETED.equals(intent.getAction())) { Intent startIntent = new Intent(context, TrackRecordingService.class); startIntent.putExtra(RESUME_TRACK_EXTRA_NAME, true); context.startService(startIntent); } else { - Log.w(TAG, "MyTracksReceiver: unsupported action"); + Log.w(TAG, "BootReceiver: unsupported action"); } } } diff --git a/MyTracks/src/com/google/android/apps/mytracks/ChartActivity.java b/MyTracks/src/com/google/android/apps/mytracks/ChartActivity.java index 73f201776..0e648d44a 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/ChartActivity.java +++ b/MyTracks/src/com/google/android/apps/mytracks/ChartActivity.java @@ -15,31 +15,26 @@ */ package com.google.android.apps.mytracks; +import static com.google.android.apps.mytracks.Constants.TAG; + import com.google.android.apps.mytracks.ChartView.Mode; +import com.google.android.apps.mytracks.TrackDataHub.ListenerDataType; import com.google.android.apps.mytracks.content.MyTracksLocation; -import com.google.android.apps.mytracks.content.MyTracksProviderUtils; import com.google.android.apps.mytracks.content.Sensor; -import com.google.android.apps.mytracks.content.Track; -import com.google.android.apps.mytracks.content.TrackPointsColumns; -import com.google.android.apps.mytracks.content.Waypoint; -import com.google.android.apps.mytracks.content.WaypointsColumns; import com.google.android.apps.mytracks.content.Sensor.SensorDataSet; -import com.google.android.apps.mytracks.services.StatusAnnouncerFactory; +import com.google.android.apps.mytracks.content.Track; +import com.google.android.apps.mytracks.content.Waypoint; +import com.google.android.apps.mytracks.services.tasks.StatusAnnouncerFactory; import com.google.android.apps.mytracks.stats.DoubleBuffer; import com.google.android.apps.mytracks.stats.TripStatisticsBuilder; import com.google.android.apps.mytracks.util.ApiFeatures; -import com.google.android.apps.mytracks.util.MyTracksUtils; +import com.google.android.apps.mytracks.util.LocationUtils; import com.google.android.apps.mytracks.util.UnitConversions; import com.google.android.maps.mytracks.R; import android.app.Activity; -import android.content.SharedPreferences; -import android.database.ContentObserver; -import android.database.Cursor; import android.location.Location; import android.os.Bundle; -import android.os.Handler; -import android.os.HandlerThread; import android.util.Log; import android.view.Menu; import android.view.MenuItem; @@ -51,53 +46,34 @@ import android.widget.LinearLayout; import android.widget.ZoomControls; import java.util.ArrayList; +import java.util.EnumSet; /** * An activity that displays a chart from the track point provider. * * @author Sandor Dornbush + * @author Rodrigo Damazio */ -public class ChartActivity extends Activity implements - SharedPreferences.OnSharedPreferenceChangeListener { - - private final static int BUFFER_SIZE = 1024; - private double profileLength = 0; - - private boolean metricUnits = true; - private boolean reportSpeed = true; - - /** - * The track id that is displayed. - */ - private long selectedTrackId = -1; - - /** - * Id of the last location that was seen when reading tracks from the - * provider. This is used to determine which locations are new compared to the - * last time the chart was updated. - */ - private long lastSeenLocationId = -1; - - private long startTime = -1; - - private Location lastLocation; - - /** - * The id of the track currently being recorded. - */ - private long recordingTrackId = -1; +public class ChartActivity extends Activity implements TrackDataListener { private final DoubleBuffer elevationBuffer = - new DoubleBuffer(MyTracksConstants.ELEVATION_SMOOTHING_FACTOR); + new DoubleBuffer(Constants.ELEVATION_SMOOTHING_FACTOR); private final DoubleBuffer speedBuffer = - new DoubleBuffer(MyTracksConstants.SPEED_SMOOTHING_FACTOR); + new DoubleBuffer(Constants.SPEED_SMOOTHING_FACTOR); + private final ArrayList pendingPoints = new ArrayList(); + private TrackDataHub dataHub; + + // Stats gathered from received data. + private double profileLength = 0; + private long startTime = -1; + private Location lastLocation; + private double trackMaxSpeed; + + // Modes of operation private Mode mode = Mode.BY_DISTANCE; - - /** - * Utilities to deal with the database. - */ - private MyTracksProviderUtils providerUtils; + private boolean metricUnits; + private boolean reportSpeed; /* * UI elements: @@ -107,9 +83,6 @@ public class ChartActivity extends Activity implements private LinearLayout busyPane; private ZoomControls zoomControls; - /** Handler for callbacks to the UI thread */ - private final Handler uiHandler = new Handler(); - /** * A runnable that can be posted to the UI thread. It will remove the spinner * (if any), enable/disable zoom controls and orange pointer as appropriate @@ -121,89 +94,17 @@ public class ChartActivity extends Activity implements busyPane.setVisibility(View.GONE); zoomControls.setIsZoomInEnabled(chartView.canZoomIn()); zoomControls.setIsZoomOutEnabled(chartView.canZoomOut()); - chartView.setShowPointer(selectedTrackIsRecording()); + chartView.setShowPointer(dataHub.isRecordingSelected()); chartView.invalidate(); } }; - /** - * A runnable that can be posted to the UI thread. It will show the spinner. - */ - private final Runnable showSpinner = new Runnable() { - @Override - public void run() { - busyPane.setVisibility(View.VISIBLE); - } - }; - - /** - * An observer for the tracks provider. Will listen to new track points being - * added and update the chart if necessary. - */ - private ContentObserver observer; - - /** - * An observer for the waypoints provider. Will listen to new way points being - * added and update the chart if necessary. - */ - private ContentObserver waypointObserver; - - /** - * A thread with a looper. Post to updateTrackHandler to execute Runnables on - * this thread. - */ - private final HandlerThread updateTrackThread = - new HandlerThread("updateTrackThread"); - - /** Handler for updateTrackThread */ - private Handler updateTrackHandler; - - /** - * A runnable that updates the profile from the provider. - */ - private final Runnable updateTrackRunnable = new Runnable() { - @Override - public void run() { - readNewTrackPoints(); - } - }; - - @Override - public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, - String key) { - if (key != null) { - if (key.equals(getString(R.string.selected_track_key))) { - selectedTrackId = - sharedPreferences.getLong(getString(R.string.selected_track_key), - -1); - readProfileAsync(); - } else if (key.equals(getString(R.string.metric_units_key))) { - metricUnits = - sharedPreferences.getBoolean(getString(R.string.metric_units_key), - true); - chartView.setMetricUnits(metricUnits); - readProfileAsync(); - } else if (key.equals(getString(R.string.report_speed_key))) { - reportSpeed = - sharedPreferences.getBoolean(getString(R.string.report_speed_key), - true); - chartView.setReportSpeed(reportSpeed, this); - readProfileAsync(); - } else if (key.equals(getString(R.string.recording_track_key))) { - recordingTrackId = - sharedPreferences.getLong(getString(R.string.recording_track_key), - -1); - runOnUiThread(updateChart); - } - } - } - @Override protected void onCreate(Bundle savedInstanceState) { - Log.w(MyTracksConstants.TAG, "ChartActivity.onCreate"); + Log.w(TAG, "ChartActivity.onCreate"); super.onCreate(savedInstanceState); MyTracks.getInstance().setChartActivity(this); - providerUtils = MyTracksProviderUtils.Factory.get(this); + dataHub = MyTracks.getInstance().getDataHub(); // The volume we want to control is the Text-To-Speech volume int volumeStream = @@ -219,22 +120,6 @@ public class ChartActivity extends Activity implements new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT); layout.addView(chartView, params); - SharedPreferences preferences = - getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0); - if (preferences != null) { - selectedTrackId = - preferences.getLong(getString(R.string.selected_track_key), -1); - recordingTrackId = - preferences.getLong(getString(R.string.recording_track_key), -1); - metricUnits = preferences.getBoolean(getString(R.string.metric_units_key), - true); - chartView.setMetricUnits(metricUnits); - reportSpeed = preferences.getBoolean(getString(R.string.report_speed_key), - true); - chartView.setReportSpeed(reportSpeed, this); - preferences.registerOnSharedPreferenceChangeListener(this); - } - busyPane = (LinearLayout) findViewById(R.id.elevation_busypane); zoomControls = (ZoomControls) findViewById(R.id.elevation_zoom); zoomControls.setOnZoomInClickListener(new View.OnClickListener() { @@ -249,97 +134,25 @@ public class ChartActivity extends Activity implements zoomOut(); } }); - - updateTrackThread.start(); - updateTrackHandler = new Handler(updateTrackThread.getLooper()); - - // Register observer for the track point provider: - Handler contentHandler = new Handler(); - observer = new ContentObserver(contentHandler) { - @Override - public void onChange(boolean selfChange) { - Log.d(MyTracksConstants.TAG, "ChartActivity: ContentObserver.onChange"); - // Check for any new locations and append them to the currently - // recording track. - if (recordingTrackId < 0) { - // No track is being recorded. We should not be here. - return; - } - if (selectedTrackId != recordingTrackId) { - // No track, or one other than the recording track is selected, don't - // bother. - return; - } - // Update can potentially be lengthy, put it in its own thread: - updateTrackHandler.post(updateTrackRunnable); - super.onChange(selfChange); - } - }; - - waypointObserver = new ContentObserver(contentHandler) { - @Override - public void onChange(boolean selfChange) { - Log.d(MyTracksConstants.TAG, - "MyTracksMap: ContentObserver.onChange waypoints"); - if (selectedTrackId < 0) { - return; - } - Thread t = new Thread() { - @Override - public void run() { - readWaypoints(); - ChartActivity.this.runOnUiThread(new Runnable() { - @Override - public void run() { - chartView.invalidate(); - } - }); - } - }; - t.start(); - super.onChange(selfChange); - } - }; - - readProfileAsync(); } @Override - protected void onPause() { - super.onPause(); - unregisterContentObservers(); + protected void onStart() { + super.onStart(); + + dataHub.registerTrackDataListener(this, EnumSet.of( + ListenerDataType.SELECTED_TRACK_CHANGED, + ListenerDataType.POINT_UPDATES, + ListenerDataType.SAMPLED_OUT_POINT_UPDATES, + ListenerDataType.WAYPOINT_UPDATES, + ListenerDataType.DISPLAY_PREFERENCES)); } @Override - protected void onResume() { - super.onResume(); - // Make sure any updates that might have happened are propagated to this - // activity: - observer.onChange(false); - waypointObserver.onChange(false); - registerContentObservers(); - } + protected void onStop() { + dataHub.unregisterTrackDataListener(this); - /** - * Register the content observer for the map overlay. - */ - private void registerContentObservers() { - getContentResolver().registerContentObserver(TrackPointsColumns.CONTENT_URI, - false/* notifyForDescendents */, observer); - getContentResolver().registerContentObserver(WaypointsColumns.CONTENT_URI, - false/* notifyForDescendents */, waypointObserver); - } - - /** - * Unregister the content observer for the map overlay. - */ - private void unregisterContentObservers() { - getContentResolver().unregisterContentObserver(observer); - getContentResolver().unregisterContentObserver(waypointObserver); - } - - private boolean selectedTrackIsRecording() { - return selectedTrackId == recordingTrackId; + super.onStop(); } private void zoomIn() { @@ -358,7 +171,7 @@ public class ChartActivity extends Activity implements if (this.mode != newMode) { this.mode = newMode; chartView.setMode(this.mode); - readProfileAsync(); + dataHub.reloadDataForListener(this); } } @@ -375,39 +188,11 @@ public class ChartActivity extends Activity implements return chartView.getChartValueSeries(index).isEnabled(); } - private void readWaypoints() { - if (selectedTrackId < 0) { - return; - } - Cursor cursor = null; - chartView.clearWaypoints(); - try { - // We will silently drop extra waypoints to make the app responsive. - cursor = - providerUtils.getWaypointsCursor(selectedTrackId, 0, - MyTracksConstants.MAX_DISPLAYED_TRACK_POINTS); - if (cursor != null) { - if (cursor.moveToFirst()) { - do { - Waypoint wpt = providerUtils.createWaypoint(cursor); - chartView.addWaypoint(wpt); - } while (cursor.moveToNext()); - } - } - } catch (RuntimeException e) { - Log.w(MyTracksConstants.TAG, "Caught an unexpected exception.", e); - } finally { - if (cursor != null) { - cursor.close(); - } - } - } - @Override public boolean onCreateOptionsMenu(Menu menu) { super.onCreateOptionsMenu(menu); chartSettingsMenuItem = - menu.add(0, MyTracksConstants.MENU_CHART_SETTINGS, 0, + menu.add(0, Constants.MENU_CHART_SETTINGS, 0, R.string.chart_settings); chartSettingsMenuItem.setIcon(R.drawable.chart_settings); return true; @@ -416,7 +201,7 @@ public class ChartActivity extends Activity implements @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { - case MyTracksConstants.MENU_CHART_SETTINGS: + case Constants.MENU_CHART_SETTINGS: MyTracks.getInstance().getDialogManager().showDialogSafely( DialogManager.DIALOG_CHART_SETTINGS); return true; @@ -438,43 +223,41 @@ public class ChartActivity extends Activity implements * This must be called in order for each point. * * @param location the location to get data for (this method takes ownership of that location) - * @param track the track to get data from * @param result the resulting point to fill out * @return the previous location, now available for reuse */ - private Location getDataPoint(Location location, Track track, double[] result) { + private void fillDataPoint(Location location, double result[]) { + double timeOrDistance = Double.NaN, + elevation = Double.NaN, + speed = Double.NaN, + power = Double.NaN, + cadence = Double.NaN, + heartRate = Double.NaN; + if (location instanceof MyTracksLocation && ((MyTracksLocation) location).getSensorDataSet() != null) { SensorDataSet sensorData = ((MyTracksLocation) location).getSensorDataSet(); if (sensorData.hasPower() && sensorData.getPower().getState() == Sensor.SensorState.SENDING && sensorData.getPower().hasValue()) { - result[3] = sensorData.getPower().getValue(); - } else { - result[3] = Double.NaN; + power = sensorData.getPower().getValue(); } if (sensorData.hasCadence() && sensorData.getCadence().getState() == Sensor.SensorState.SENDING && sensorData.getCadence().hasValue()) { - result[4] = sensorData.getCadence().getValue(); - } else { - result[4] = Double.NaN; + cadence = sensorData.getCadence().getValue(); } if (sensorData.hasHeartRate() && sensorData.getHeartRate().getState() == Sensor.SensorState.SENDING && sensorData.getHeartRate().hasValue()) { - result[5] = sensorData.getHeartRate().getValue(); - } else { - result[5] = Double.NaN; + heartRate = sensorData.getHeartRate().getValue(); } - } else { - result[3] = Double.NaN; - result[4] = Double.NaN; - result[5] = Double.NaN; } + + // TODO: Account for segment splits? switch (mode) { case BY_DISTANCE: - result[0] = profileLength; + timeOrDistance = profileLength; if (lastLocation != null) { double d = lastLocation.distanceTo(location); if (metricUnits) { @@ -489,16 +272,16 @@ public class ChartActivity extends Activity implements // Base case startTime = location.getTime(); } - result[0] = (location.getTime() - startTime); + timeOrDistance = (location.getTime() - startTime); break; default: - Log.w(MyTracksConstants.TAG, "ChartActivity unknown mode: " + mode); + Log.w(TAG, "ChartActivity unknown mode: " + mode); } elevationBuffer.setNext(metricUnits ? location.getAltitude() : location.getAltitude() * UnitConversions.M_TO_FT); - result[1] = elevationBuffer.getAverage(); + elevation = elevationBuffer.getAverage(); if (lastLocation == null) { if (Math.abs(location.getSpeed() - 128) > 1) { @@ -507,151 +290,134 @@ public class ChartActivity extends Activity implements } else if (TripStatisticsBuilder.isValidSpeed( location.getTime(), location.getSpeed(), lastLocation.getTime(), lastLocation.getSpeed(), speedBuffer) - && (location.getSpeed() <= track.getStatistics().getMaxSpeed())) { + && (location.getSpeed() <= trackMaxSpeed)) { speedBuffer.setNext(location.getSpeed()); } - result[2] = speedBuffer.getAverage() * 3.6; + speed = speedBuffer.getAverage() * 3.6; if (!metricUnits) { - result[2] *= UnitConversions.KM_TO_MI; + speed *= UnitConversions.KM_TO_MI; } - if (!reportSpeed && (result[2] != 0)) { - // Format as hours per unit - result[2] = (60.0 / result[2]); + if (!reportSpeed) { + if (speed != 0) { + // Format as hours per unit + speed = (60.0 / speed); + } else { + speed = Double.NaN; + } } - Location oldLastLocation = lastLocation; + // Keep a copy so the location can be reused. lastLocation = location; - if (oldLastLocation == null) { - // No previous location, but return a blank one for reuse - return new MyTracksLocation(""); + if (result != null) { + result[0] = timeOrDistance; + result[1] = elevation; + result[2] = speed; + result[3] = power; + result[4] = cadence; + result[5] = heartRate; } - - return oldLastLocation; } - /** - * Sets the chart data points reading from the provider. This is non-blocking. - */ - private void readProfileAsync() { - chartView.reset(); - updateTrackHandler.post(new Runnable() { + @Override + public void onProviderStateChange(ProviderState state) { + // We don't care. + } + + @Override + public void onCurrentLocationChanged(Location loc) { + // We don't care. + } + + @Override + public void onCurrentHeadingChanged(double heading) { + // We don't care. + } + + @Override + public void onSelectedTrackChanged(Track track, boolean isRecording) { + Log.e(TAG, "Visible", new Throwable()); + runOnUiThread(new Runnable() { + @Override public void run() { - runOnUiThread(showSpinner); - readProfile(); - readWaypoints(); - runOnUiThread(updateChart); + busyPane.setVisibility(View.VISIBLE); } }); } - /** - * Reads the track profile from the provider. This is a blocking function and - * should not be run from the UI thread. - */ - private void readProfile() { + @Override + public void onTrackUpdated(Track track) { + trackMaxSpeed = track.getStatistics().getMaxSpeed(); + } + + @Override + public void clearTrackPoints() { profileLength = 0; lastLocation = null; startTime = -1; - if (selectedTrackId < 0) { - return; - } - Track track = providerUtils.getTrack(selectedTrackId); - if (track == null) { - return; - } - lastSeenLocationId = track.getStartId(); - final ArrayList theData = readPointsToList(track); - runOnUiThread(new Runnable() { - public void run() { - chartView.setDataPoints(theData); - } - }); + elevationBuffer.reset(); + speedBuffer.reset(); + chartView.reset(); + pendingPoints.clear(); } - /** - * Read all new track points. - */ - private void readNewTrackPoints() { - Log.i(MyTracksConstants.TAG, "MyTracks: Updating chart last seen: " + lastSeenLocationId); - Track track = providerUtils.getTrack(recordingTrackId); - if (track == null) { - Log.w(MyTracksConstants.TAG, "MyTracks: track not found"); - return; + @Override + public void onNewTrackPoint(Location loc) { + if (LocationUtils.isValidLocation(loc)) { + double[] point = new double[6]; + fillDataPoint(loc, point); + pendingPoints.add(point); } - chartView.addDataPoints(readPointsToList(track)); - uiHandler.post(new Runnable() { - public void run() { - chartView.invalidate(); - } - }); - Log.i(MyTracksConstants.TAG, "MyTracks: Updated chart last seen: " + lastSeenLocationId); } - /** - * Get the frequency at which points should be displayed. - * Limit the number of chart readings. Ideally we would want around 1024. - * @param track The track which will be displayed. - * @return The inverse of the frequency of points to be displayed. - */ - private int getSamplingFrequency(Track track) { - long totalLocations = track.getStopId() - track.getStartId(); - return Math.max(1, (int) (totalLocations / 1024.0)); + @Override + public void onSampledOutTrackPoint(Location loc) { + // Still account for the point in the smoothing buffers. + fillDataPoint(loc, null); } - private Cursor getLocationsCursor(long lastLocationRead) { - return providerUtils.getLocationsCursor(selectedTrackId, lastLocationRead, BUFFER_SIZE, false); + @Override + public void onSegmentSplit() { + // Do nothing. } - /** - * Read all of the points to a list. - * @param track The track which will be displayed. - * @return - */ - private ArrayList readPointsToList(Track track) { - Cursor cursor = null; - long lastLocationRead = lastSeenLocationId; - int points = 0; - int chartSamplingFrequency = getSamplingFrequency(track); - ArrayList result = new ArrayList(); - // Need two locations so we can keep track of the last location. - Location location = new MyTracksLocation(""); - try { - while (lastSeenLocationId < track.getStopId()) { - cursor = getLocationsCursor(lastLocationRead); - if (cursor != null) { - elevationBuffer.reset(); - speedBuffer.reset(); - if (cursor.moveToFirst()) { - final int idColumnIdx = cursor.getColumnIndexOrThrow(TrackPointsColumns._ID); - while (cursor.moveToNext()) { - points++; - providerUtils.fillLocation(cursor, location); - if (MyTracksUtils.isValidLocation(location)) { - lastLocationRead = lastSeenLocationId = - cursor.getLong(idColumnIdx); - // TODO Can we be smarter about choosing 3 or 6 entries? - double[] point = new double[6]; - location = getDataPoint(location, track, point); - if (points % chartSamplingFrequency == 0) { - result.add(point); - } - } - } - } else { - lastLocationRead += BUFFER_SIZE; - } - } else { - lastLocationRead += BUFFER_SIZE; - } - cursor.close(); - cursor = null; - } - return result; - } finally { - if (cursor != null) { - cursor.close(); - } - } + @Override + public void onNewTrackPointsDone() { + chartView.addDataPoints(pendingPoints); + pendingPoints.clear(); + runOnUiThread(updateChart); + } + + @Override + public void clearWaypoints() { + chartView.clearWaypoints(); + } + + @Override + public void onNewWaypoint(Waypoint wpt) { + chartView.addWaypoint(wpt); + } + + @Override + public void onNewWaypointsDone() { + runOnUiThread(updateChart); + } + + @Override + public boolean onUnitsChanged(boolean metric) { + this.metricUnits = metric; + + chartView.setMetricUnits(metric); + + return true; // Reload data + } + + @Override + public boolean onReportSpeedChanged(boolean reportSpeed) { + this.reportSpeed = reportSpeed; + + chartView.setReportSpeed(reportSpeed, this); + + return true; // Reload data } } diff --git a/MyTracks/src/com/google/android/apps/mytracks/ChartView.java b/MyTracks/src/com/google/android/apps/mytracks/ChartView.java index 753a4a753..5e50b2fa0 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/ChartView.java +++ b/MyTracks/src/com/google/android/apps/mytracks/ChartView.java @@ -527,8 +527,8 @@ public class ChartView extends View { } if (nearestWaypoint != null && dmin < 100) { Intent intent = - new Intent(getContext(), MyTracksWaypointDetails.class); - intent.putExtra("waypointid", nearestWaypoint.getId()); + new Intent(getContext(), WaypointDetails.class); + intent.putExtra(WaypointDetails.WAYPOINT_ID_EXTRA, nearestWaypoint.getId()); getContext().startActivity(intent); return true; } @@ -728,7 +728,7 @@ public class ChartView extends View { } } float density = getContext().getResources().getDisplayMetrics().density; - // TODO: This should be function of the number of variables displayed. + maxLength = Math.max(maxLength, 1); leftBorder = (int) (density * (4 + 8 * maxLength)); effectiveWidth = w - leftBorder - RIGHT_BORDER; bottomBorder = (int) (density * BOTTOM_BORDER); diff --git a/MyTracks/src/com/google/android/apps/mytracks/CheckUnits.java b/MyTracks/src/com/google/android/apps/mytracks/CheckUnits.java index d1fc56029..556ad8f7d 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/CheckUnits.java +++ b/MyTracks/src/com/google/android/apps/mytracks/CheckUnits.java @@ -78,7 +78,7 @@ class CheckUnits { private static void accept(Context context, SharedPreferences preferences) { recordCheckPerformed(preferences); - Intent startIntent = new Intent(context, MyTracksSettings.class); + Intent startIntent = new Intent(context, SettingsActivity.class); context.startActivity(startIntent); } diff --git a/MyTracks/src/com/google/android/apps/mytracks/MyTracksConstants.java b/MyTracks/src/com/google/android/apps/mytracks/Constants.java similarity index 62% rename from MyTracks/src/com/google/android/apps/mytracks/MyTracksConstants.java rename to MyTracks/src/com/google/android/apps/mytracks/Constants.java index b9d11009b..c3fc58acf 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/MyTracksConstants.java +++ b/MyTracks/src/com/google/android/apps/mytracks/Constants.java @@ -20,18 +20,13 @@ package com.google.android.apps.mytracks; * * @author Leif Hendrik Wilden */ -public abstract class MyTracksConstants { +public abstract class Constants { /** * Should be used by all log statements */ public static final String TAG = "MyTracks"; - - /** - * Name of the gps location provider: - */ - public static final String GPS_PROVIDER = "gps"; - + /** * Name of the top-level directory inside the SD card where our files will * be read from/written to. @@ -50,26 +45,26 @@ public abstract class MyTracksConstants { public static final int FEATURE_DETAILS = 5; public static final int START_RECORDING = 6; public static final int STOP_RECORDING = 7; - public static final int AUTHENTICATE_TO_DOCLIST = 8; - public static final int AUTHENTICATE_TO_TRIX = 9; - public static final int SEND_TO_DOCS = 10; - public static final int DELETE_TRACK = 11; - public static final int SEND_TO_GOOGLE = 12; - public static final int SEND_TO_GOOGLE_DIALOG = 13; - public static final int SHARE_LINK = 14; - public static final int SHARE_GPX_FILE = 15; - public static final int SHARE_KML_FILE = 16; - public static final int SHARE_CSV_FILE = 17; - public static final int SHARE_TCX_FILE = 18; - public static final int EDIT_DETAILS = 19; - public static final int SAVE_GPX_FILE = 20; - public static final int SAVE_KML_FILE = 21; - public static final int SAVE_CSV_FILE = 22; - public static final int SAVE_TCX_FILE = 23; - public static final int CLEAR_MAP = 24; - public static final int SHOW_WAYPOINT = 25; - public static final int EDIT_WAYPOINT = 26; - public static final int WELCOME = 27; + public static final int AUTHENTICATE_TO_MY_MAPS = 8; + public static final int AUTHENTICATE_TO_FUSION_TABLES = 9; + public static final int AUTHENTICATE_TO_DOCLIST = 10; + public static final int AUTHENTICATE_TO_TRIX = 11; + public static final int DELETE_TRACK = 13; + public static final int SEND_TO_GOOGLE_DIALOG = 14; + public static final int SHARE_LINK = 15; + public static final int SHARE_GPX_FILE = 16; + public static final int SHARE_KML_FILE = 17; + public static final int SHARE_CSV_FILE = 18; + public static final int SHARE_TCX_FILE = 19; + public static final int EDIT_DETAILS = 20; + public static final int SAVE_GPX_FILE = 21; + public static final int SAVE_KML_FILE = 22; + public static final int SAVE_CSV_FILE = 23; + public static final int SAVE_TCX_FILE = 24; + public static final int CLEAR_MAP = 25; + public static final int SHOW_WAYPOINT = 26; + public static final int EDIT_WAYPOINT = 27; + public static final int WELCOME = 28; /* * Menu ids: @@ -140,6 +135,12 @@ public abstract class MyTracksConstants { */ public static final int MAX_LOADED_TRACK_POINTS = 20000; + /** + * Maximum number of track points ever loaded at once from the provider into + * memory in a single call to read points. + */ + public static final int MAX_LOADED_TRACK_POINTS_PER_BATCH = 1000; + /** * Maximum number of way points displayed by the map overlay. */ @@ -168,6 +169,12 @@ public abstract class MyTracksConstants { */ public static final double MAX_ACCELERATION = 0.02; + /** Maximum age of a GPS location to be considered current. */ + public static final long MAX_LOCATION_AGE_MS = 60 * 1000; // 1 minute + + /** Maximum age of a network location to be considered current. */ + public static final long MAX_NETWORK_AGE_MS = 1000 * 60 * 10; // 10 minutes + /** * The type of account that we can use for gdata uploads. */ @@ -182,32 +189,32 @@ public abstract class MyTracksConstants { public static int getActionFromMenuId(int menuId) { switch (menuId) { - case MyTracksConstants.MENU_SEND_TO_GOOGLE: - return MyTracksConstants.SEND_TO_GOOGLE_DIALOG; - case MyTracksConstants.MENU_EDIT: - return MyTracksConstants.EDIT_DETAILS; - case MyTracksConstants.MENU_DELETE: - return MyTracksConstants.DELETE_TRACK; - case MyTracksConstants.MENU_SHARE_LINK: - return MyTracksConstants.SHARE_LINK; - case MyTracksConstants.MENU_SHARE_KML_FILE: - return MyTracksConstants.SHARE_KML_FILE; - case MyTracksConstants.MENU_SHARE_GPX_FILE: - return MyTracksConstants.SHARE_GPX_FILE; - case MyTracksConstants.MENU_SHARE_CSV_FILE: - return MyTracksConstants.SHARE_CSV_FILE; - case MyTracksConstants.MENU_SHARE_TCX_FILE: - return MyTracksConstants.SHARE_TCX_FILE; - case MyTracksConstants.MENU_SAVE_GPX_FILE: - return MyTracksConstants.SAVE_GPX_FILE; - case MyTracksConstants.MENU_SAVE_KML_FILE: - return MyTracksConstants.SAVE_KML_FILE; - case MyTracksConstants.MENU_SAVE_CSV_FILE: - return MyTracksConstants.SAVE_CSV_FILE; - case MyTracksConstants.MENU_SAVE_TCX_FILE: - return MyTracksConstants.SAVE_TCX_FILE; - case MyTracksConstants.MENU_CLEAR_MAP: - return MyTracksConstants.CLEAR_MAP; + case Constants.MENU_SEND_TO_GOOGLE: + return Constants.SEND_TO_GOOGLE_DIALOG; + case Constants.MENU_EDIT: + return Constants.EDIT_DETAILS; + case Constants.MENU_DELETE: + return Constants.DELETE_TRACK; + case Constants.MENU_SHARE_LINK: + return Constants.SHARE_LINK; + case Constants.MENU_SHARE_KML_FILE: + return Constants.SHARE_KML_FILE; + case Constants.MENU_SHARE_GPX_FILE: + return Constants.SHARE_GPX_FILE; + case Constants.MENU_SHARE_CSV_FILE: + return Constants.SHARE_CSV_FILE; + case Constants.MENU_SHARE_TCX_FILE: + return Constants.SHARE_TCX_FILE; + case Constants.MENU_SAVE_GPX_FILE: + return Constants.SAVE_GPX_FILE; + case Constants.MENU_SAVE_KML_FILE: + return Constants.SAVE_KML_FILE; + case Constants.MENU_SAVE_CSV_FILE: + return Constants.SAVE_CSV_FILE; + case Constants.MENU_SAVE_TCX_FILE: + return Constants.SAVE_TCX_FILE; + case Constants.MENU_CLEAR_MAP: + return Constants.CLEAR_MAP; default: return -1; } @@ -216,8 +223,22 @@ public abstract class MyTracksConstants { public static final String MAPSHOP_BASE_URL = "http://maps.google.com/maps/ms"; + /* + * Default values - keep in sync with those in preferences.xml. + */ + + public static final int DEFAULT_ANNOUNCEMENT_FREQUENCY = -1; + public static final int DEFAULT_AUTO_RESUME_TRACK_TIMEOUT = 10; // In min. + public static final int DEFAULT_MAX_RECORDING_DISTANCE = 200; + public static final int DEFAULT_MIN_RECORDING_DISTANCE = 5; + public static final int DEFAULT_MIN_RECORDING_INTERVAL = 0; + public static final int DEFAULT_MIN_REQUIRED_ACCURACY = 200; + public static final int DEFAULT_SPLIT_FREQUENCY = 0; + + public static final String SETTINGS_NAME = "SettingsActivity"; + /** * This is an abstract utility class. */ - protected MyTracksConstants() { } + protected Constants() { } } diff --git a/MyTracks/src/com/google/android/apps/mytracks/MyTracksDeleteAllTracks.java b/MyTracks/src/com/google/android/apps/mytracks/DeleteAllTracks.java similarity index 90% rename from MyTracks/src/com/google/android/apps/mytracks/MyTracksDeleteAllTracks.java rename to MyTracks/src/com/google/android/apps/mytracks/DeleteAllTracks.java index 1e939b144..13c1598ed 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/MyTracksDeleteAllTracks.java +++ b/MyTracks/src/com/google/android/apps/mytracks/DeleteAllTracks.java @@ -34,12 +34,12 @@ import android.util.Log; * * @author Leif Hendrik Wilden */ -public class MyTracksDeleteAllTracks extends Handler { +public class DeleteAllTracks extends Handler { private final Context context; private final Runnable done; - public MyTracksDeleteAllTracks(Context context, Runnable done) { + public DeleteAllTracks(Context context, Runnable done) { this.context = context; this.done = done; } @@ -58,11 +58,12 @@ public class MyTracksDeleteAllTracks extends Handler { new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int i) { dialog.dismiss(); - Log.w(MyTracksConstants.TAG, "deleting all!"); + Log.w(Constants.TAG, "deleting all!"); MyTracksProviderUtils.Factory.get(context).deleteAllTracks(); SharedPreferences prefs = - context.getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0); + context.getSharedPreferences(Constants.SETTINGS_NAME, 0); SharedPreferences.Editor editor = prefs.edit(); + // TODO: Go through data manager editor.putLong(context.getString(R.string.selected_track_key), -1); ApiFeatures.getInstance().getApiPlatformAdapter().applyPreferenceChanges(editor); if (done != null) { diff --git a/MyTracks/src/com/google/android/apps/mytracks/DialogManager.java b/MyTracks/src/com/google/android/apps/mytracks/DialogManager.java index e8512ac3b..679685edf 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,10 @@ 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; 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 +50,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,32 +75,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) { - activity.shareLinkToFusionTable(activity.sendToTrackId); - dialog.dismiss(); - } - }); - sendToGoogleResultDialog = builder.create(); - return sendToGoogleResultDialog; - case DIALOG_WRITE_PROGRESS: - writeProgressDialog = new ProgressDialog(activity); - writeProgressDialog.setIcon(android.R.drawable.ic_dialog_info); - writeProgressDialog.setTitle( - activity.getString(R.string.progress_title)); - writeProgressDialog.setProgressStyle(ProgressDialog.STYLE_SPINNER); - writeProgressDialog.setMessage( - activity.getString(R.string.write_progress_message)); - return writeProgressDialog; } return null; } @@ -112,24 +86,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.getFusionTablesResultMessage()); - - boolean canShare = activity.getSendToFusionTablesTableId() != 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(MyTracksConstants.TAG, "MyTracks.onPrepare chart dialog"); + Log.d(TAG, "MyTracks.onPrepare chart dialog"); chartSettingsDialog.setup(activity.getChartActivity()); break; } @@ -146,7 +104,7 @@ public class DialogManager { } }); } - + public void setProgressValue(final int percent) { activity.runOnUiThread(new Runnable() { public void run() { @@ -162,7 +120,7 @@ public class DialogManager { /** * @return the sendToGoogleDialog */ - public SendToGoogleDialog getSendToGoogleDialog() { + public SendDialog getSendToGoogleDialog() { return sendToGoogleDialog; } @@ -191,9 +149,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 */ @@ -203,11 +161,27 @@ public class DialogManager { try { activity.showDialog(id); } catch (BadTokenException e) { - Log.w(MyTracksConstants.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(MyTracksConstants.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); } } }); @@ -228,4 +202,19 @@ public class DialogManager { }); } + /** + * The equivalent of {@link #dismissDialogSafely(int)}, but for a specific + * dialog instance. + */ + public static void dismissDialogSafely(Activity activity, final Dialog dialog) { + activity.runOnUiThread(new Runnable() { + public void run() { + try { + dialog.dismiss(); + } catch (IllegalArgumentException e) { + // This will be thrown if this dialog was not shown before. + } + } + }); + } } diff --git a/MyTracks/src/com/google/android/apps/mytracks/Eula.java b/MyTracks/src/com/google/android/apps/mytracks/Eula.java index aed5a40a0..c54926795 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/Eula.java +++ b/MyTracks/src/com/google/android/apps/mytracks/Eula.java @@ -98,7 +98,7 @@ class Eula { ApiFeatures.getInstance().getApiPlatformAdapter().applyPreferenceChanges( preferences.edit().putBoolean(PREFERENCE_EULA_ACCEPTED, true)); Intent startIntent = new Intent(activity, WelcomeActivity.class); - activity.startActivityForResult(startIntent, MyTracksConstants.WELCOME); + activity.startActivityForResult(startIntent, Constants.WELCOME); } private static void refuse(Activity activity) { diff --git a/MyTracks/src/com/google/android/apps/mytracks/ExportAllTracks.java b/MyTracks/src/com/google/android/apps/mytracks/ExportAllTracks.java index 86c5bc9cf..93f2e9aa5 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/ExportAllTracks.java +++ b/MyTracks/src/com/google/android/apps/mytracks/ExportAllTracks.java @@ -17,10 +17,10 @@ package com.google.android.apps.mytracks; import com.google.android.apps.mytracks.content.MyTracksProviderUtils; import com.google.android.apps.mytracks.content.TracksColumns; -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.util.MyTracksUtils; +import com.google.android.apps.mytracks.io.file.TrackWriter; +import com.google.android.apps.mytracks.io.file.TrackWriterFactory; +import com.google.android.apps.mytracks.io.file.TrackWriterFactory.TrackFileFormat; +import com.google.android.apps.mytracks.util.SystemUtils; import com.google.android.maps.mytracks.R; import android.app.Activity; @@ -71,14 +71,14 @@ public class ExportAllTracks { format = TrackFileFormat.TCX; break; default: - Log.w(MyTracksConstants.TAG, "Unknown export format: " + which); + Log.w(Constants.TAG, "Unknown export format: " + which); } } }; public ExportAllTracks(Activity activity) { this.activity = activity; - Log.i(MyTracksConstants.TAG, "ExportAllTracks: Starting"); + Log.i(Constants.TAG, "ExportAllTracks: Starting"); AlertDialog.Builder builder = new AlertDialog.Builder(activity); builder.setSingleChoiceItems(R.array.export_formats, 0, itemClick); @@ -113,14 +113,14 @@ public class ExportAllTracks { */ private void aquireLocksAndExport() { SharedPreferences prefs = - activity.getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0); + activity.getSharedPreferences(Constants.SETTINGS_NAME, 0); long recordingTrackId = -1; if (prefs != null) { recordingTrackId = prefs.getLong(activity.getString(R.string.recording_track_key), -1); } if (recordingTrackId != -1) { - wakeLock = MyTracksUtils.acquireWakeLock(activity, wakeLock); + wakeLock = SystemUtils.acquireWakeLock(activity, wakeLock); } // Now we can safely export everything. @@ -130,9 +130,9 @@ public class ExportAllTracks { // TODO check what happens if we started recording after getting this lock. if (wakeLock != null && wakeLock.isHeld()) { wakeLock.release(); - Log.i(MyTracksConstants.TAG, "ExportAllTracks: Releasing wake lock."); + Log.i(Constants.TAG, "ExportAllTracks: Releasing wake lock."); } - Log.i(MyTracksConstants.TAG, "ExportAllTracks: Done"); + Log.i(Constants.TAG, "ExportAllTracks: Done"); Toast.makeText(activity, R.string.export_done, Toast.LENGTH_SHORT).show(); } @@ -163,7 +163,7 @@ public class ExportAllTracks { } final int trackCount = cursor.getCount(); - Log.i(MyTracksConstants.TAG, + Log.i(Constants.TAG, "ExportAllTracks: Exporting: " + cursor.getCount() + " tracks."); int idxTrackId = cursor.getColumnIndexOrThrow(TracksColumns._ID); activity.runOnUiThread(new Runnable() { @@ -186,7 +186,7 @@ public class ExportAllTracks { }); long id = cursor.getLong(idxTrackId); - Log.i(MyTracksConstants.TAG, "ExportAllTracks: exporting: " + id); + Log.i(Constants.TAG, "ExportAllTracks: exporting: " + id); TrackWriter writer = TrackWriterFactory.newWriter(activity, providerUtils, id, format); writer.writeTrack(); diff --git a/MyTracks/src/com/google/android/apps/mytracks/ImportAllTracks.java b/MyTracks/src/com/google/android/apps/mytracks/ImportAllTracks.java index 799a91d5a..8826e40c7 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/ImportAllTracks.java +++ b/MyTracks/src/com/google/android/apps/mytracks/ImportAllTracks.java @@ -16,9 +16,9 @@ package com.google.android.apps.mytracks; import com.google.android.apps.mytracks.content.MyTracksProviderUtils; -import com.google.android.apps.mytracks.io.GpxImporter; +import com.google.android.apps.mytracks.io.file.GpxImporter; import com.google.android.apps.mytracks.util.FileUtils; -import com.google.android.apps.mytracks.util.MyTracksUtils; +import com.google.android.apps.mytracks.util.SystemUtils; import com.google.android.maps.mytracks.R; import android.app.Activity; @@ -60,7 +60,7 @@ public class ImportAllTracks { public ImportAllTracks(Activity activity) { this.activity = activity; - Log.i(MyTracksConstants.TAG, "ImportAllTracks: Starting"); + Log.i(Constants.TAG, "ImportAllTracks: Starting"); fileUtils = new FileUtils(); gpxPath = fileUtils.buildExternalDirectoryPath("gpx"); HandlerThread handlerThread; @@ -82,13 +82,13 @@ public class ImportAllTracks { * track. Acquire a wake lock if there is no current track. */ private void aquireLocksAndImport() { - SharedPreferences prefs = activity.getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0); + SharedPreferences prefs = activity.getSharedPreferences(Constants.SETTINGS_NAME, 0); long recordingTrackId = -1; if (prefs != null) { recordingTrackId = prefs.getLong(activity.getString(R.string.recording_track_key), -1); } if (recordingTrackId != -1) { - wakeLock = MyTracksUtils.acquireWakeLock(activity, wakeLock); + wakeLock = SystemUtils.acquireWakeLock(activity, wakeLock); } // Now we can safely import everything. @@ -98,10 +98,10 @@ public class ImportAllTracks { // TODO check what happens if we started recording after getting this lock. if (wakeLock != null && wakeLock.isHeld()) { wakeLock.release(); - Log.i(MyTracksConstants.TAG, "ImportAllTracks: Releasing wake lock."); + Log.i(Constants.TAG, "ImportAllTracks: Releasing wake lock."); } - Log.i(MyTracksConstants.TAG, "ImportAllTracks: Done"); + Log.i(Constants.TAG, "ImportAllTracks: Done"); AlertDialog.Builder builder = new AlertDialog.Builder(activity); if (gpxFileCount == 0) { builder.setMessage(activity.getString(R.string.import_empty, gpxPath + "/")); @@ -141,7 +141,7 @@ public class ImportAllTracks { return; } - Log.i(MyTracksConstants.TAG, "ImportAllTracks: Importing: " + gpxFileCount + " tracks."); + Log.i(Constants.TAG, "ImportAllTracks: Importing: " + gpxFileCount + " tracks."); activity.runOnUiThread(new Runnable() { public void run() { makeProgressDialog(gpxFileCount); @@ -180,19 +180,19 @@ public class ImportAllTracks { * notifications and returns false on failure. */ private boolean importFile(File gpxFile, MyTracksProviderUtils providerUtils) { - Log.i(MyTracksConstants.TAG, "ImportAllTracks: importing: " + gpxFile.getName()); + Log.i(Constants.TAG, "ImportAllTracks: importing: " + gpxFile.getName()); try { GpxImporter.importGPXFile(new FileInputStream(gpxFile), providerUtils); return true; } catch (FileNotFoundException e) { - Log.w(MyTracksConstants.TAG, "GPX file wasn't found/went missing: " + Log.w(Constants.TAG, "GPX file wasn't found/went missing: " + gpxFile.getAbsolutePath(), e); } catch (ParserConfigurationException e) { - Log.w(MyTracksConstants.TAG, "Error parsing file: " + gpxFile.getAbsolutePath(), e); + Log.w(Constants.TAG, "Error parsing file: " + gpxFile.getAbsolutePath(), e); } catch (SAXException e) { - Log.w(MyTracksConstants.TAG, "Error parsing file: " + gpxFile.getAbsolutePath(), e); + Log.w(Constants.TAG, "Error parsing file: " + gpxFile.getAbsolutePath(), e); } catch (IOException e) { - Log.w(MyTracksConstants.TAG, "Error reading file: " + gpxFile.getAbsolutePath(), e); + Log.w(Constants.TAG, "Error reading file: " + gpxFile.getAbsolutePath(), e); } Toast.makeText(activity, activity.getString(R.string.import_error, gpxFile.getName()), Toast.LENGTH_LONG).show(); diff --git a/MyTracks/src/com/google/android/apps/mytracks/MapActivity.java b/MyTracks/src/com/google/android/apps/mytracks/MapActivity.java new file mode 100644 index 000000000..772096d65 --- /dev/null +++ b/MyTracks/src/com/google/android/apps/mytracks/MapActivity.java @@ -0,0 +1,580 @@ +/* + * 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; + +import static com.google.android.apps.mytracks.Constants.TAG; + +import com.google.android.apps.mytracks.TrackDataHub.ListenerDataType; +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.services.tasks.StatusAnnouncerFactory; +import com.google.android.apps.mytracks.stats.TripStatistics; +import com.google.android.apps.mytracks.util.ApiFeatures; +import com.google.android.apps.mytracks.util.GeoRect; +import com.google.android.apps.mytracks.util.LocationUtils; +import com.google.android.maps.GeoPoint; +import com.google.android.maps.MapController; +import com.google.android.maps.MapView; +import com.google.android.maps.mytracks.R; + +import android.content.Intent; +import android.location.Location; +import android.location.LocationManager; +import android.os.Bundle; +import android.provider.Settings; +import android.util.Log; +import android.view.ContextMenu; +import android.view.ContextMenu.ContextMenuInfo; +import android.view.Menu; +import android.view.MenuItem; +import android.view.MotionEvent; +import android.view.SubMenu; +import android.view.View; +import android.view.View.OnCreateContextMenuListener; +import android.view.Window; +import android.widget.ImageButton; +import android.widget.LinearLayout; +import android.widget.RelativeLayout; +import android.widget.TextView; + +import java.util.EnumSet; + +/** + * The map view activity of the MyTracks application. + * + * @author Leif Hendrik Wilden + * @author Rodrigo Damazio + */ +public class MapActivity extends com.google.android.maps.MapActivity + implements View.OnTouchListener, View.OnClickListener, + TrackDataListener { + + // Saved instance state keys: + // --------------------------- + + private static final String KEY_CURRENT_LOCATION = "currentLocation"; + private static final String KEY_KEEP_MY_LOCATION_VISIBLE = "keepMyLocationVisible"; + + private TrackDataHub dataHub; + + /** + * True if the map should be scrolled so that the pointer is always in the + * visible area. + */ + private boolean keepMyLocationVisible; + + /** + * The current pointer location. + * This is kept to quickly center on it when the user requests. + */ + private Location currentLocation; + + // UI elements: + // ------------- + + private RelativeLayout screen; + private MapView mapView; + private MapOverlay mapOverlay; + private LinearLayout messagePane; + private TextView messageText; + private LinearLayout busyPane; + private ImageButton optionsBtn; + + private MenuItem myLocation; + private MenuItem toggleLayers; + + /** + * We are not displaying driving directions. Just an arbitrary track that is + * not associated to any licensed mapping data. Therefore it should be okay to + * return false here and still comply with the terms of service. + */ + @Override + protected boolean isRouteDisplayed() { + return false; + } + + /** + * We are displaying a location. This needs to return true in order to comply + * with the terms of service. + */ + @Override + protected boolean isLocationDisplayed() { + return true; + } + + // Application life cycle: + // ------------------------ + + @Override + public void onCreate(Bundle bundle) { + Log.d(TAG, "MapActivity.onCreate"); + super.onCreate(bundle); + + // The volume we want to control is the Text-To-Speech volume + int volumeStream = + new StatusAnnouncerFactory(ApiFeatures.getInstance()).getVolumeStream(); + setVolumeControlStream(volumeStream); + + dataHub = MyTracks.getInstance().getDataHub(); + + // We don't need a window title bar: + requestWindowFeature(Window.FEATURE_NO_TITLE); + + // Inflate the layout: + setContentView(R.layout.mytracks_layout); + + // Remove the window's background because the MapView will obscure it + getWindow().setBackgroundDrawable(null); + + // Set up a map overlay: + screen = (RelativeLayout) findViewById(R.id.screen); + mapView = (MapView) findViewById(R.id.map); + mapView.requestFocus(); + mapOverlay = new MapOverlay(this); + mapView.getOverlays().add(mapOverlay); + mapView.setOnTouchListener(this); + mapView.setBuiltInZoomControls(true); + messagePane = (LinearLayout) findViewById(R.id.messagepane); + messageText = (TextView) findViewById(R.id.messagetext); + busyPane = (LinearLayout) findViewById(R.id.busypane); + optionsBtn = (ImageButton) findViewById(R.id.showOptions); + + optionsBtn.setOnCreateContextMenuListener(contextMenuListener); + optionsBtn.setOnClickListener(this); + } + + @Override + protected void onRestoreInstanceState(Bundle bundle) { + Log.d(TAG, "MapActivity.onRestoreInstanceState"); + if (bundle != null) { + super.onRestoreInstanceState(bundle); + keepMyLocationVisible = + bundle.getBoolean(KEY_KEEP_MY_LOCATION_VISIBLE, false); + if (bundle.containsKey(KEY_CURRENT_LOCATION)) { + currentLocation = (Location) bundle.getParcelable(KEY_CURRENT_LOCATION); + if (currentLocation != null) { + showCurrentLocation(); + } + } else { + currentLocation = null; + } + } + } + + @Override + protected void onStart() { + Log.d(TAG, "MapActivity.onStart"); + super.onStart(); + + dataHub.registerTrackDataListener(this, EnumSet.of( + ListenerDataType.SELECTED_TRACK_CHANGED, + ListenerDataType.POINT_UPDATES, + ListenerDataType.WAYPOINT_UPDATES, + ListenerDataType.LOCATION_UPDATES, + ListenerDataType.COMPASS_UPDATES)); + } + + @Override + protected void onSaveInstanceState(Bundle outState) { + Log.d(TAG, "MapActivity.onSaveInstanceState"); + outState.putBoolean(KEY_KEEP_MY_LOCATION_VISIBLE, keepMyLocationVisible); + if (currentLocation != null) { + outState.putParcelable(KEY_CURRENT_LOCATION, currentLocation); + } + super.onSaveInstanceState(outState); + } + + @Override + protected void onStop() { + Log.d(TAG, "MapActivity.onStop"); + + dataHub.unregisterTrackDataListener(this); + + super.onStop(); + } + + // Utility functions: + // ------------------- + + /** + * Shows the options button if a track is selected, or hide it if not. + */ + private void updateOptionsButton(boolean trackSelected) { + optionsBtn.setVisibility( + trackSelected ? View.VISIBLE : View.INVISIBLE); + } + + /** + * Tests if a location is visible. + * + * @param location a given location + * @return true if the given location is within the visible map area + */ + private boolean locationIsVisible(Location location) { + if (location == null || mapView == null) { + return false; + } + GeoPoint center = mapView.getMapCenter(); + int latSpan = mapView.getLatitudeSpan(); + int lonSpan = mapView.getLongitudeSpan(); + + // Bottom of map view is obscured by zoom controls/buttons. + // Subtract a margin from the visible area: + GeoPoint marginBottom = mapView.getProjection().fromPixels( + 0, mapView.getHeight()); + GeoPoint marginTop = mapView.getProjection().fromPixels(0, + mapView.getHeight() + - mapView.getZoomButtonsController().getZoomControls().getHeight()); + int margin = + Math.abs(marginTop.getLatitudeE6() - marginBottom.getLatitudeE6()); + GeoRect r = new GeoRect(center, latSpan, lonSpan); + r.top += margin; + + GeoPoint geoPoint = LocationUtils.getGeoPoint(location); + return r.contains(geoPoint); + } + + /** + * Moves the location pointer to the current location and center the map if + * the current location is outside the visible area. + */ + private void showCurrentLocation() { + if (currentLocation == null || mapOverlay == null || mapView == null) { + return; + } + mapOverlay.setMyLocation(currentLocation); + mapView.postInvalidate(); + if (keepMyLocationVisible && !locationIsVisible(currentLocation)) { + GeoPoint geoPoint = LocationUtils.getGeoPoint(currentLocation); + MapController controller = mapView.getController(); + controller.animateTo(geoPoint); + } + + } + + @Override + public void onTrackUpdated(Track track) { + // We don't care. + } + + /** + * Zooms and pans the map so that the given track is visible. + * + * @param track the track + */ + private void zoomMapToBoundaries(Track track) { + if (mapView == null) { + return; + } + + if (track == null || track.getNumberOfPoints() < 2) { + return; + } + + TripStatistics stats = track.getStatistics(); + int bottom = stats.getBottom(); + int left = stats.getLeft(); + int latSpanE6 = stats.getTop() - bottom; + int lonSpanE6 = stats.getRight() - left; + if (latSpanE6 > 0 + && latSpanE6 < 180E6 + && lonSpanE6 > 0 + && lonSpanE6 < 360E6) { + keepMyLocationVisible = false; + GeoPoint center = new GeoPoint( + bottom + latSpanE6 / 2, + left + lonSpanE6 / 2); + if (LocationUtils.isValidGeoPoint(center)) { + mapView.getController().setCenter(center); + mapView.getController().zoomToSpan(latSpanE6, lonSpanE6); + } + } + } + + /** + * Zooms and pans the map so that the given waypoint is visible. + */ + public void showWaypoint(long waypointId) { + MyTracksProviderUtils providerUtils = MyTracksProviderUtils.Factory.get(this); + Waypoint wpt = providerUtils.getWaypoint(waypointId); + if (wpt != null && wpt.getLocation() != null) { + keepMyLocationVisible = false; + GeoPoint center = new GeoPoint( + (int) (wpt.getLocation().getLatitude() * 1E6), + (int) (wpt.getLocation().getLongitude() * 1E6)); + mapView.getController().setCenter(center); + mapView.getController().setZoom(20); + mapView.invalidate(); + } + } + + @Override + public void onSelectedTrackChanged(final Track track, final boolean isRecording) { + runOnUiThread(new Runnable() { + @Override + public void run() { + boolean trackSelected = track != null; + updateOptionsButton(trackSelected); + + mapOverlay.setTrackDrawingEnabled(trackSelected); + + if (trackSelected) { + busyPane.setVisibility(View.VISIBLE); + + zoomMapToBoundaries(track); + + mapOverlay.setShowEndMarker(!isRecording); + busyPane.setVisibility(View.GONE); + } + mapView.invalidate(); + } + }); + } + + private final OnCreateContextMenuListener contextMenuListener = + new OnCreateContextMenuListener() { + @Override + public void onCreateContextMenu(ContextMenu menu, View v, + ContextMenuInfo menuInfo) { + menu.setHeaderTitle(R.string.tracklist_this_track); + menu.add(0, Constants.MENU_EDIT, 0, + R.string.tracklist_edit_track); + if (!dataHub.isRecordingSelected()) { + menu.add(0, Constants.MENU_SEND_TO_GOOGLE, 0, + R.string.tracklist_send_to_google); + SubMenu share = menu.addSubMenu(0, Constants.MENU_SHARE, 0, + R.string.tracklist_share_track); + share.add(0, Constants.MENU_SHARE_LINK, 0, + R.string.tracklist_share_link); + share.add(0, Constants.MENU_SHARE_GPX_FILE, 0, + R.string.tracklist_share_gpx_file); + share.add(0, Constants.MENU_SHARE_KML_FILE, 0, + R.string.tracklist_share_kml_file); + share.add(0, Constants.MENU_SHARE_CSV_FILE, 0, + R.string.tracklist_share_csv_file); + share.add(0, Constants.MENU_SHARE_TCX_FILE, 0, + R.string.tracklist_share_tcx_file); + SubMenu save = menu.addSubMenu(0, + Constants.MENU_WRITE_TO_SD_CARD, 0, + R.string.tracklist_write_to_sd); + save.add(0, Constants.MENU_SAVE_GPX_FILE, 0, + R.string.tracklist_save_as_gpx); + save.add(0, Constants.MENU_SAVE_KML_FILE, 0, + R.string.tracklist_save_as_kml); + save.add(0, Constants.MENU_SAVE_CSV_FILE, 0, + R.string.tracklist_save_as_csv); + save.add(0, Constants.MENU_SAVE_TCX_FILE, 0, + R.string.tracklist_save_as_tcx); + menu.add(0, Constants.MENU_CLEAR_MAP, 0, + R.string.tracklist_clear_map); + menu.add(0, Constants.MENU_DELETE, 0, + R.string.tracklist_delete_track); + } + } + }; + + @Override + public boolean onMenuItemSelected(int featureId, MenuItem item) { + if (!super.onMenuItemSelected(featureId, item)) { + MyTracks.getInstance().onActivityResult( + Constants.getActionFromMenuId(item.getItemId()), RESULT_OK, + new Intent()); + return true; + } + return false; + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + super.onCreateOptionsMenu(menu); + myLocation = menu.add(0, Constants.MENU_MY_LOCATION, 0, + R.string.mylocation); + myLocation.setIcon(android.R.drawable.ic_menu_mylocation); + toggleLayers = menu.add(0, Constants.MENU_TOGGLE_LAYERS, 0, + R.string.switch_to_sat); + toggleLayers.setIcon(android.R.drawable.ic_menu_mapmode); + return true; + } + + @Override + public boolean onPrepareOptionsMenu(Menu menu) { + toggleLayers.setTitle(mapView.isSatellite() ? + R.string.switch_to_map : R.string.switch_to_sat); + return super.onPrepareOptionsMenu(menu); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case Constants.MENU_MY_LOCATION: { + dataHub.forceUpdateLocation(); + keepMyLocationVisible = true; + if (mapView.getZoomLevel() < 18) { + mapView.getController().setZoom(18); + } + return true; + } + case Constants.MENU_TOGGLE_LAYERS: { + mapView.setSatellite(!mapView.isSatellite()); + return true; + } + } + return super.onOptionsItemSelected(item); + } + + @Override + public void onClick(View v) { + if (v == messagePane) { + startActivity(new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS)); + } else if (v == optionsBtn) { + optionsBtn.performLongClick(); + } + } + + /** + * We want the pointer to become visible again in case of the next location + * update: + */ + @Override + public boolean onTouch(View view, MotionEvent event) { + if (keepMyLocationVisible && event.getAction() == MotionEvent.ACTION_MOVE) { + if (!locationIsVisible(currentLocation)) { + keepMyLocationVisible = false; + } + } + return false; + } + + @Override + public void onProviderStateChange(ProviderState state) { + final int messageId; + final boolean bindClick; + switch (state) { + case DISABLED: + messageId = R.string.status_enable_gps; + bindClick = true; + break; + case NO_FIX: + case BAD_FIX: + messageId = R.string.wait_for_fix; + bindClick = false; + break; + case GOOD_FIX: + // Nothing to show. + messageId = -1; + bindClick = false; + break; + default: + throw new IllegalArgumentException("Unexpected state: " + state); + } + + runOnUiThread(new Runnable() { + @Override + public void run() { + if (messageId != -1) { + messageText.setText(messageId); + messagePane.setVisibility(View.VISIBLE); + + if (bindClick) { + messagePane.setOnClickListener(MapActivity.this); + } else { + messagePane.setOnClickListener(null); + } + } else { + messagePane.setVisibility(View.GONE); + } + + screen.requestLayout(); + } + }); + } + + @Override + public void onCurrentLocationChanged(Location location) { + if (!location.getProvider().equals(LocationManager.GPS_PROVIDER)) { + Log.d(TAG, + "MapActivity: Network location update received (provider '" + location.getProvider() + "'."); + } + + currentLocation = location; + showCurrentLocation(); + } + + @Override + public void onCurrentHeadingChanged(double heading) { + synchronized (this) { + if (mapOverlay.setHeading((float) heading)) { + mapView.postInvalidate(); + } + } + } + + @Override + public void clearWaypoints() { + mapOverlay.clearWaypoints(); + } + + @Override + public void onNewWaypoint(Waypoint waypoint) { + if (LocationUtils.isValidLocation(waypoint.getLocation())) { + // TODO: Optimize locking inside addWaypoint + mapOverlay.addWaypoint(waypoint); + } + } + + @Override + public void onNewWaypointsDone() { + mapView.postInvalidate(); + } + + @Override + public void clearTrackPoints() { + mapOverlay.clearPoints(); + } + + @Override + public void onNewTrackPoint(Location loc) { + mapOverlay.addLocation(loc); + } + + @Override + public void onSegmentSplit() { + mapOverlay.addSegmentSplit(); + } + + @Override + public void onSampledOutTrackPoint(Location loc) { + // We don't care. + } + + @Override + public void onNewTrackPointsDone() { + mapView.postInvalidate(); + } + + @Override + public boolean onUnitsChanged(boolean metric) { + // We don't care. + return false; + } + + @Override + public boolean onReportSpeedChanged(boolean reportSpeed) { + // We don't care. + return false; + } +} diff --git a/MyTracks/src/com/google/android/apps/mytracks/MyTracksOverlay.java b/MyTracks/src/com/google/android/apps/mytracks/MapOverlay.java similarity index 93% rename from MyTracks/src/com/google/android/apps/mytracks/MyTracksOverlay.java rename to MyTracks/src/com/google/android/apps/mytracks/MapOverlay.java index 95abbc93b..c6e8acfa3 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/MyTracksOverlay.java +++ b/MyTracks/src/com/google/android/apps/mytracks/MapOverlay.java @@ -16,7 +16,7 @@ package com.google.android.apps.mytracks; import com.google.android.apps.mytracks.content.Waypoint; -import com.google.android.apps.mytracks.util.MyTracksUtils; +import com.google.android.apps.mytracks.util.LocationUtils; import com.google.android.maps.GeoPoint; import com.google.android.maps.MapView; import com.google.android.maps.Overlay; @@ -45,7 +45,7 @@ import java.util.concurrent.BlockingQueue; * * @author Leif Hendrik Wilden */ -public class MyTracksOverlay extends Overlay { +public class MapOverlay extends Overlay { private final Drawable[] arrows; private final int arrowWidth, arrowHeight; @@ -79,20 +79,31 @@ public class MyTracksOverlay extends Overlay { private static class CachedLocation { public final boolean valid; public final GeoPoint geoPoint; - + + /** + * Constructor for an invalid cached location. + */ + public CachedLocation() { + this.valid = false; + this.geoPoint = null; + } + + /** + * Constructor for a potentially valid cached location. + */ public CachedLocation(Location location) { - this.valid = MyTracksUtils.isValidLocation(location); - this.geoPoint = valid ? MyTracksUtils.getGeoPoint(location) : null; + this.valid = LocationUtils.isValidLocation(location); + this.geoPoint = valid ? LocationUtils.getGeoPoint(location) : null; } }; - - public MyTracksOverlay(Context context) { + + public MapOverlay(Context context) { this.context = context; this.waypoints = new ArrayList(); this.points = new ArrayList(1024); this.pendingPoints = new ArrayBlockingQueue( - MyTracksConstants.MAX_DISPLAYED_TRACK_POINTS, true); + Constants.MAX_DISPLAYED_TRACK_POINTS, true); // TODO: Can we use a FrameAnimation or similar here rather // than individual resources for each arrow direction? @@ -164,6 +175,13 @@ public class MyTracksOverlay extends Overlay { pendingPoints.offer(new CachedLocation(l)); } + /** + * Adds a segment split to the map overlay. + */ + public void addSegmentSplit() { + pendingPoints.offer(new CachedLocation()); + } + public void addWaypoint(Waypoint wpt) { // Note: We don't cache waypoints, because it's not worth the effort. if (wpt != null && wpt.getLocation() != null) { @@ -172,7 +190,7 @@ public class MyTracksOverlay extends Overlay { } } } - + public int getNumLocations() { synchronized (points) { return points.size() + pendingPoints.size(); @@ -260,7 +278,7 @@ public class MyTracksOverlay extends Overlay { synchronized (waypoints) {; for (Waypoint wpt : waypoints) { Location loc = wpt.getLocation(); - drawElement(canvas, projection, MyTracksUtils.getGeoPoint(loc), + drawElement(canvas, projection, LocationUtils.getGeoPoint(loc), wpt.getType() == Waypoint.TYPE_STATISTICS ? statsMarker : waypointMarker, -(markerWidth / 2) + 3, -markerHeight); } @@ -274,7 +292,7 @@ public class MyTracksOverlay extends Overlay { } Point pt = drawElement(canvas, projection, - MyTracksUtils.getGeoPoint(myLocation), arrows[lastHeading], + LocationUtils.getGeoPoint(myLocation), arrows[lastHeading], -(arrowWidth / 2) + 3, -(arrowHeight / 2)); // Draw the error circle. float radius = projection.metersToEquatorPixels(myLocation.getAccuracy()); @@ -423,7 +441,7 @@ public class MyTracksOverlay extends Overlay { return false; } - final Location tapLocation = MyTracksUtils.getLocation(p); + final Location tapLocation = LocationUtils.getLocation(p); double dmin = Double.MAX_VALUE; Waypoint waypoint = null; synchronized (waypoints) { @@ -442,8 +460,8 @@ public class MyTracksOverlay extends Overlay { if (waypoint != null && dmin < 15000000 / Math.pow(2, mapView.getZoomLevel())) { - Intent intent = new Intent(context, MyTracksWaypointDetails.class); - intent.putExtra("waypointid", waypoint.getId()); + Intent intent = new Intent(context, WaypointDetails.class); + intent.putExtra(WaypointDetails.WAYPOINT_ID_EXTRA, waypoint.getId()); context.startActivity(intent); return true; } diff --git a/MyTracks/src/com/google/android/apps/mytracks/MenuManager.java b/MyTracks/src/com/google/android/apps/mytracks/MenuManager.java index ded2aae1b..97ef2adae 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/MenuManager.java +++ b/MyTracks/src/com/google/android/apps/mytracks/MenuManager.java @@ -64,21 +64,21 @@ class MenuManager { return true; } case R.id.menu_list_tracks: { - activity.startActivityForResult(new Intent(activity, MyTracksList.class), - MyTracksConstants.SHOW_TRACK); + activity.startActivityForResult(new Intent(activity, TrackList.class), + Constants.SHOW_TRACK); return true; } case R.id.menu_list_markers: { - Intent startIntent = new Intent(activity, MyTracksWaypointsList.class); + Intent startIntent = new Intent(activity, WaypointsList.class); startIntent.putExtra("trackid", activity.getSelectedTrackId()); - activity.startActivityForResult(startIntent, MyTracksConstants.SHOW_WAYPOINT); + activity.startActivityForResult(startIntent, Constants.SHOW_WAYPOINT); return true; } case R.id.menu_sensor_state: { return startActivity(SensorStateActivity.class); } case R.id.menu_settings: { - return startActivity(MyTracksSettings.class); + return startActivity(SettingsActivity.class); } case R.id.menu_aggregated_stats: { return startActivity(AggregatedStatsActivity.class); @@ -86,8 +86,8 @@ class MenuManager { case R.id.menu_help: { return startActivity(WelcomeActivity.class); } - case MyTracksConstants.MENU_CLEAR_MAP: { - activity.setSelectedTrackId(-1); + case Constants.MENU_CLEAR_MAP: { + activity.clearSelectedTrack(); return true; } } diff --git a/MyTracks/src/com/google/android/apps/mytracks/MyMapsList.java b/MyTracks/src/com/google/android/apps/mytracks/MyMapsList.java new file mode 100644 index 000000000..7b74ac606 --- /dev/null +++ b/MyTracks/src/com/google/android/apps/mytracks/MyMapsList.java @@ -0,0 +1,209 @@ +/* + * 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; + +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.mymaps.MapsFacade; +import com.google.android.apps.mytracks.io.mymaps.MyMapsConstants; +import com.google.android.maps.mytracks.R; + +import android.app.Activity; +import android.content.Intent; +import android.os.Bundle; +import android.view.ContextMenu; +import android.view.ContextMenu.ContextMenuInfo; +import android.view.MenuItem; +import android.view.View; +import android.view.View.OnCreateContextMenuListener; +import android.widget.AdapterView; +import android.widget.AdapterView.OnItemClickListener; +import android.widget.ListView; +import android.widget.TextView; + +/** + * Activity which displays the list of current My Maps tracks for the user. + * Returns RESULT_OK if the user picked a map, and returns "mapid" as an extra. + * + * @author Rodrigo Damazio + */ +public class MyMapsList extends Activity implements MapsFacade.MapsListCallback { + private static final int MENU_OPEN = 0; + private static final int MENU_SHARE = 2; + private static final int GET_LOGIN = 1; + + private MapsFacade mapsClient; + private AuthManager auth; + private MyMapsListAdapter listAdapter; + + private int contextPosition; + + private final OnCreateContextMenuListener contextMenuListener = + new OnCreateContextMenuListener() { + @Override + public void onCreateContextMenu(ContextMenu menu, View v, + ContextMenuInfo menuInfo) { + AdapterView.AdapterContextMenuInfo info = + (AdapterView.AdapterContextMenuInfo) menuInfo; + contextPosition = info.position; + menu.add(0, MENU_OPEN, 0, R.string.open_map); + menu.add(0, MENU_SHARE, 0, R.string.share_map); + } + }; + + private final OnItemClickListener clickListener = + new OnItemClickListener() { + @Override + public void onItemClick(AdapterView parent, View v, int position, + long id) { + Intent result = new Intent(); + result.putExtra("mapid", (String) listAdapter.getItem(position)); + setResult(RESULT_OK, result); + finish(); + } + }; + + /** Called when the activity is first created. */ + @Override + public void onCreate(Bundle icicle) { + super.onCreate(icicle); + + auth = AuthManagerFactory.getAuthManager(this, GET_LOGIN, null, true, + MyMapsConstants.SERVICE_NAME); + + setContentView(R.layout.list); + + listAdapter = new MyMapsListAdapter(this); + + ListView list = (ListView) findViewById(R.id.maplist); + list.setOnItemClickListener(clickListener); + list.setOnCreateContextMenuListener(contextMenuListener); + list.setAdapter(listAdapter); + + startLogin(); + } + + private void startLogin() { + // Starts in the UI thread. + // TODO fix this for non-froyo devices. + if (AuthManagerFactory.useModernAuthManager()) { + MyTracks.getInstance().getAccountChooser().chooseAccount( + MyMapsList.this, + new AccountChooser.AccountHandler() { + @Override + public void handleAccountSelected(Account account) { + // Account selection happens in the UI thread. + if (account != null) { + // The user did not quit and there was a valid google + // account. + doLogin(account); + } + } + }); + } else { + doLogin(null); + } + } + + private void doLogin(final Account account) { + // Starts in the UI thread. + auth.doLogin(new Runnable() { + public void run() { + // Runs in UI thread. + mapsClient = new MapsFacade(MyMapsList.this, auth); + + startLookup(); + } + }, account); + } + + private void startLookup() { + // Starts in the UI thread. + new Thread() { + @Override + public void run() { + // Communication with Maps happens in its own thread. + // This will call onReceivedMapListing below. + final boolean success = mapsClient.getMapsList(MyMapsList.this); + + runOnUiThread(new Runnable() { + @Override + public void run() { + // Updating the UI when done happens in the UI thread. + onLookupDone(success); + } + }); + } + }.start(); + } + + @Override + public void onReceivedMapListing(final String mapId, final String title, + final String description, final boolean isPublic) { + // Starts in the communication thread. + runOnUiThread(new Runnable() { + @Override + public void run() { + // Updating the list with new contents happens in the UI thread. + listAdapter.addMapListing(mapId, title, description, isPublic); + } + }); + } + + private void onLookupDone(boolean success) { + // Starts in the UI thread. + findViewById(R.id.loading).setVisibility(View.GONE); + if (!success) { + findViewById(R.id.failed).setVisibility(View.VISIBLE); + } + TextView emptyView = (TextView) findViewById(R.id.mapslist_empty); + ListView list = (ListView) findViewById(R.id.maplist); + list.setEmptyView(emptyView); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, + Intent data) { + if (requestCode == GET_LOGIN) { + auth.authResult(resultCode, data); + } + super.onActivityResult(requestCode, resultCode, data); + } + + @Override + public boolean onMenuItemSelected(int featureId, MenuItem item) { + switch (item.getItemId()) { + case MENU_OPEN: + clickListener.onItemClick(null, null, contextPosition, 0); + return true; + case MENU_SHARE: + Intent shareIntent = new Intent(Intent.ACTION_SEND); + shareIntent.setType("text/plain"); + shareIntent.putExtra(Intent.EXTRA_SUBJECT, + getText(R.string.share_map_subject)); + String[] listItem = (String[]) listAdapter.getMapListingArray(contextPosition); + shareIntent.putExtra(Intent.EXTRA_TEXT, String.format( + getText(R.string.share_map_body_format).toString(), + listItem[1], + MapsFacade.buildMapUrl(listItem[0]))); + startActivity(Intent.createChooser(shareIntent, + getText(R.string.share_map).toString())); + return true; + } + return false; + } +} diff --git a/MyTracks/src/com/google/android/apps/mytracks/MyMapsListAdapter.java b/MyTracks/src/com/google/android/apps/mytracks/MyMapsListAdapter.java new file mode 100644 index 000000000..5780a950b --- /dev/null +++ b/MyTracks/src/com/google/android/apps/mytracks/MyMapsListAdapter.java @@ -0,0 +1,135 @@ +package com.google.android.apps.mytracks; + +import com.google.android.maps.mytracks.R; + +import android.app.Activity; +import android.database.DataSetObserver; +import android.graphics.Color; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ListAdapter; +import android.widget.TextView; + +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; +import java.util.Vector; + +public class MyMapsListAdapter implements ListAdapter { + private Vector mapsList; + private Vector publicList; + private Set observerSet; + private final Activity activity; + + public MyMapsListAdapter(Activity activity) { + this.activity = activity; + + mapsList = new Vector(); + publicList = new Vector(); + observerSet = new HashSet(); + } + + public void addMapListing(String mapId, String title, + String description, boolean isPublic) { + synchronized (mapsList) { + // Search through the maps list to see if it has the mapid and + // remove it if so, so that we can replace it with updated info + for (int i = 0; i < mapsList.size(); ++i) { + if (mapId.equals(mapsList.get(i)[0])) { + mapsList.remove(i); + publicList.remove(i); + --i; + } + } + mapsList.add(new String[] { mapId, title, description }); + publicList.add(isPublic); + } + + Iterator iter = observerSet.iterator(); + while (iter.hasNext()) { + iter.next().onChanged(); + } + } + + public String[] getMapListingArray(int position) { + return mapsList.get(position); + } + + @Override + public boolean areAllItemsEnabled() { + return true; + } + + @Override + public boolean isEnabled(int position) { + return true; + } + + @Override + public int getCount() { + return mapsList.size(); + } + + @Override + public Object getItem(int position) { + return mapsList.get(position)[0]; + } + + @Override + public long getItemId(int position) { + return position; + } + + @Override + public int getItemViewType(int position) { + return 0; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + if (convertView == null) { + convertView = + activity.getLayoutInflater().inflate(R.layout.listitem, parent, false); + } + + String[] map = mapsList.get(position); + ((TextView) convertView.findViewById(R.id.maplistitem)).setText(map[1]); + ((TextView) convertView.findViewById(R.id.maplistdesc)).setText(map[2]); + TextView publicUnlisted = + (TextView) convertView.findViewById(R.id.maplistpublic); + if (publicList.get(position)) { + publicUnlisted.setTextColor(Color.RED); + publicUnlisted.setText(R.string.public_map); + } else { + publicUnlisted.setTextColor(Color.GREEN); + publicUnlisted.setText(R.string.unlisted_map); + } + + return convertView; + } + + @Override + public int getViewTypeCount() { + return 1; + } + + @Override + public boolean hasStableIds() { + return false; + } + + @Override + public boolean isEmpty() { + return mapsList.isEmpty(); + } + + @Override + public void registerDataSetObserver(DataSetObserver observer) { + observerSet.add(observer); + } + + @Override + public void unregisterDataSetObserver(DataSetObserver observer) { + observerSet.remove(observer); + } +} diff --git a/MyTracks/src/com/google/android/apps/mytracks/MyTracks.java b/MyTracks/src/com/google/android/apps/mytracks/MyTracks.java index 34f6a7361..a69eb6910 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/MyTracks.java +++ b/MyTracks/src/com/google/android/apps/mytracks/MyTracks.java @@ -15,10 +15,10 @@ */ 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; -import static com.google.android.apps.mytracks.DialogManager.DIALOG_WRITE_PROGRESS; import com.google.android.accounts.Account; import com.google.android.apps.mytracks.content.MyTracksProviderUtils; @@ -26,20 +26,27 @@ import com.google.android.apps.mytracks.content.Track; import com.google.android.apps.mytracks.content.WaypointCreationRequest; import com.google.android.apps.mytracks.io.AuthManager; import com.google.android.apps.mytracks.io.AuthManagerFactory; -import com.google.android.apps.mytracks.io.GpxImporter; import com.google.android.apps.mytracks.io.SendToDocs; import com.google.android.apps.mytracks.io.SendToFusionTables; -import com.google.android.apps.mytracks.io.TempFileCleaner; -import com.google.android.apps.mytracks.io.TrackWriter; -import com.google.android.apps.mytracks.io.TrackWriterFactory; import com.google.android.apps.mytracks.io.SendToFusionTables.OnSendCompletedListener; -import com.google.android.apps.mytracks.io.TrackWriterFactory.TrackFileFormat; +import com.google.android.apps.mytracks.io.SendToMyMaps; +import com.google.android.apps.mytracks.io.file.GpxImporter; +import com.google.android.apps.mytracks.io.file.TempFileCleaner; +import com.google.android.apps.mytracks.io.file.TrackWriter; +import com.google.android.apps.mytracks.io.file.TrackWriterFactory; +import com.google.android.apps.mytracks.io.file.TrackWriterFactory.TrackFileFormat; +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; +import com.google.android.apps.mytracks.services.tasks.StatusAnnouncerFactory; import com.google.android.apps.mytracks.util.ApiFeatures; import com.google.android.apps.mytracks.util.FileUtils; -import com.google.android.apps.mytracks.util.MyTracksUtils; +import com.google.android.apps.mytracks.util.SystemUtils; import com.google.android.maps.mytracks.R; import android.app.Activity; @@ -47,29 +54,26 @@ import android.app.AlertDialog; import android.app.Dialog; import android.app.TabActivity; import android.content.ComponentName; -import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.ServiceConnection; import android.content.SharedPreferences; -import android.content.SharedPreferences.OnSharedPreferenceChangeListener; import android.content.res.Resources; -import android.location.Location; -import android.location.LocationManager; import android.net.Uri; import android.os.Bundle; import android.os.Handler; import android.os.HandlerThread; import android.os.IBinder; import android.os.RemoteException; +import android.text.TextUtils; import android.util.Log; import android.view.Menu; import android.view.MenuItem; import android.view.MotionEvent; import android.view.View; -import android.view.Window; import android.view.View.OnTouchListener; import android.view.ViewGroup.LayoutParams; +import android.view.Window; import android.widget.RelativeLayout; import android.widget.TabHost; import android.widget.Toast; @@ -78,7 +82,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; @@ -90,12 +96,13 @@ import org.xml.sax.SAXException; * @author Leif Hendrik Wilden */ public class MyTracks extends TabActivity implements OnTouchListener, - OnSharedPreferenceChangeListener, ProgressIndicator { + ProgressIndicator { /** * Singleton instance */ private static MyTracks instance; + private TrackDataHub dataHub; private ChartActivity chartActivity; /* @@ -122,8 +129,11 @@ public class MyTracks extends TabActivity implements OnTouchListener, */ public long sendToTrackId = -1; + public boolean sendToMyMapsSuccess = false; public boolean sendToFusionTablesSuccess = false; public boolean sendToDocsSuccess = false; + public String sendToMyMapsMapId; + public String sendToMyMapsMessage = ""; public String sendToFusionTablesTableId; public String sendToFusionTablesMessage = ""; public String sendToDocsMessage = ""; @@ -137,18 +147,8 @@ public class MyTracks extends TabActivity implements OnTouchListener, private ITrackRecordingService trackRecordingService; /** - * The id of the currently recording track. + * Does the user want to share the current track. */ - private long recordingTrackId = -1; - - /** - * The id of the currently selected track. - */ - private long selectedTrackId = -1; - - /** - * Does the user want to share the current track. - */ private boolean shareRequested = false; /** @@ -164,7 +164,7 @@ public class MyTracks extends TabActivity implements OnTouchListener, private final ServiceConnection serviceConnection = new ServiceConnection() { @Override public void onServiceConnected(ComponentName className, IBinder service) { - Log.d(MyTracksConstants.TAG, "MyTracks: Service now connected."); + Log.d(Constants.TAG, "MyTracks: Service now connected."); // Delay setting the service until we are done with initialization. ITrackRecordingService trackRecordingService = ITrackRecordingService.Stub.asInterface(service); @@ -182,7 +182,7 @@ public class MyTracks extends TabActivity implements OnTouchListener, @Override public void onServiceDisconnected(ComponentName className) { - Log.d(MyTracksConstants.TAG, "MyTracks: Service now disconnected."); + Log.d(TAG, "MyTracks: Service now disconnected."); trackRecordingService = null; } }; @@ -220,26 +220,20 @@ public class MyTracks extends TabActivity implements OnTouchListener, public boolean isRecording() { if (trackRecordingService == null) { // Fall back to alternative check method. - return isRecordingBasedOnSharedPreferences(); + return dataHub.isRecording(); } try { return trackRecordingService.isRecording(); // 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(MyTracksConstants.TAG, "MyTracks: Remote exception.", e); + Log.e(TAG, "MyTracks: Remote exception.", e); // Fall back to alternative check method. - return isRecordingBasedOnSharedPreferences(); + return dataHub.isRecording(); } } - private boolean isRecordingBasedOnSharedPreferences() { - // TrackRecordingService guarantees that recordingTrackId is set to - // -1 if the track has been stopped. - return recordingTrackId >= 0; - } - /* * Application lifetime events: * ============================ @@ -247,17 +241,18 @@ public class MyTracks extends TabActivity implements OnTouchListener, @Override protected void onCreate(Bundle savedInstanceState) { - Log.d(MyTracksConstants.TAG, "MyTracks.onCreate"); + Log.d(TAG, "MyTracks.onCreate"); super.onCreate(savedInstanceState); instance = this; ApiFeatures apiFeatures = ApiFeatures.getInstance(); - if (!MyTracksUtils.isRelease(this)) { + if (!SystemUtils.isRelease(this)) { apiFeatures.getApiPlatformAdapter().enableStrictMode(); } providerUtils = MyTracksProviderUtils.Factory.get(this); + sharedPreferences = getSharedPreferences(Constants.SETTINGS_NAME, 0); + dataHub = new TrackDataHub(this, sharedPreferences, providerUtils); menuManager = new MenuManager(this); - sharedPreferences = getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0); dialogManager = new DialogManager(this); // The volume we want to control is the Text-To-Speech volume @@ -273,7 +268,7 @@ public class MyTracks extends TabActivity implements OnTouchListener, tabHost.addTab(tabHost.newTabSpec("tab1") .setIndicator("Map", res.getDrawable( android.R.drawable.ic_menu_mapmode)) - .setContent(new Intent(this, MyTracksMap.class))); + .setContent(new Intent(this, MapActivity.class))); tabHost.addTab(tabHost.newTabSpec("tab2") .setIndicator("Stats", res.getDrawable(R.drawable.menu_stats)) .setContent(new Intent(this, StatsActivity.class))); @@ -298,20 +293,6 @@ public class MyTracks extends TabActivity implements OnTouchListener, tabHost.addView(layout); layout.setOnTouchListener(this); - if (sharedPreferences != null) { - selectedTrackId = - sharedPreferences.getLong(getString(R.string.selected_track_key), -1); - recordingTrackId = sharedPreferences.getLong( - getString(R.string.recording_track_key), -1); - sharedPreferences.registerOnSharedPreferenceChangeListener(this); - Log.d(MyTracksConstants.TAG, "recordingTrackId: " + recordingTrackId - + ", selectedTrackId: " + selectedTrackId); - if (recordingTrackId > 0) { - Intent startIntent = new Intent(this, TrackRecordingService.class); - startService(startIntent); - } - } - // This will show the eula until the user accepts or quits the app. Eula.showEulaRequireAcceptance(this); @@ -323,35 +304,45 @@ 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(MyTracksConstants.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(MyTracksConstants.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(MyTracksConstants.TAG, - "Received an intent with unsupported action " + action); + Log.w(TAG, "Received an intent with unsupported action " + action); } } else { - Log.d(MyTracksConstants.TAG, "Received an intent with no action."); + Log.d(TAG, "Received an intent with no action."); } } @Override protected void onDestroy() { - Log.d(MyTracksConstants.TAG, "MyTracks.onDestroy"); + Log.d(TAG, "MyTracks.onDestroy"); + + dataHub.destroy(); + tryUnbindTrackRecordingService(); super.onDestroy(); } + @Override + protected void onStop() { + Log.d(TAG, "MyTracks.onStop"); + + dataHub.stop(); + + // Clean up any temporary track files. + TempFileCleaner.clean(); + super.onStop(); + } + @Override 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(MyTracksConstants.TAG, "MyTracks.onPause"); + Log.d(TAG, "MyTracks.onPause"); tryUnbindTrackRecordingService(); super.onPause(); } @@ -360,17 +351,22 @@ 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(MyTracksConstants.TAG, "MyTracks.onResume"); + Log.d(TAG, "MyTracks.onResume"); tryBindTrackRecordingService(); super.onResume(); } @Override - protected void onStop() { - Log.d(MyTracksConstants.TAG, "MyTracks.onStop"); - // Clean up any temporary track files. - TempFileCleaner.clean(); - super.onStop(); + protected void onStart() { + Log.d(TAG, "MyTracks.onStart"); + super.onStart(); + dataHub.start(); + + // Ensure that service is running if we're supposed to be recording + if (dataHub.isRecording()) { + Intent startIntent = new Intent(this, TrackRecordingService.class); + startService(startIntent); + } } /* @@ -387,7 +383,7 @@ public class MyTracks extends TabActivity implements OnTouchListener, @Override public boolean onPrepareOptionsMenu(Menu menu) { menuManager.onPrepareOptionsMenu(menu, providerUtils.getLastTrack() != null, - isRecording(), selectedTrackId >= 0); + isRecording(), dataHub.isATrackSelected()); return super.onPrepareOptionsMenu(menu); } @@ -431,9 +427,9 @@ public class MyTracks extends TabActivity implements OnTouchListener, try { insertWaypoint(WaypointCreationRequest.DEFAULT_STATISTICS); } catch (RemoteException e) { - Log.e(MyTracksConstants.TAG, "Cannot insert statistics marker.", e); + Log.e(TAG, "Cannot insert statistics marker.", e); } catch (IllegalStateException e) { - Log.e(MyTracksConstants.TAG, "Cannot insert statistics marker.", e); + Log.e(TAG, "Cannot insert statistics marker.", e); } return true; } @@ -444,53 +440,40 @@ public class MyTracks extends TabActivity implements OnTouchListener, @Override public void onActivityResult(int requestCode, int resultCode, final Intent results) { + Log.d(TAG, "MyTracks.onActivityResult"); TrackFileFormat exportFormat = null; + long trackId = dataHub.getSelectedTrackId(); + if (results != null) { + trackId = results.getLongExtra("trackid", trackId); + } + switch (requestCode) { - case MyTracksConstants.GET_MAP: { - if (resultCode == RESULT_OK) { - results.putExtra("trackid", selectedTrackId); - if (results.hasExtra("tableid")) { - sendToFusionTablesTableId = results.getStringExtra("tableid"); - } - setProgressMessage( - R.string.progress_message_authenticating_fusiontables); - authenticate(results, MyTracksConstants.SEND_TO_GOOGLE, - SendToFusionTables.SERVICE_ID); - } else { + case Constants.GET_LOGIN: { + if (resultCode != RESULT_OK || auth == null || !auth.authResult(resultCode, results)) { dialogManager.dismissDialogSafely(DIALOG_PROGRESS); } break; } - case MyTracksConstants.GET_LOGIN: { - if (resultCode == RESULT_OK && auth != null) { - if (!auth.authResult(resultCode, results)) { - dialogManager.dismissDialogSafely(DIALOG_PROGRESS); - } - } else { - dialogManager.dismissDialogSafely(DIALOG_PROGRESS); - } - break; - } - case MyTracksConstants.SHOW_TRACK: { + case Constants.SHOW_TRACK: { if (results != null) { - final long trackId = results.getLongExtra("trackid", -1); if (trackId >= 0) { - setSelectedTrackId(trackId); + dataHub.loadTrack(trackId); + // The track list passed the requested action as result code. Hand // it off to the onAcitivtyResult for further processing: - if (resultCode != MyTracksConstants.SHOW_TRACK) { + if (resultCode != Constants.SHOW_TRACK) { onActivityResult(resultCode, Activity.RESULT_OK, results); } } } break; } - case MyTracksConstants.SHOW_WAYPOINT: { + case Constants.SHOW_WAYPOINT: { if (results != null) { final long waypointId = results.getLongExtra("waypointid", -1); if (waypointId >= 0) { - MyTracksMap map = - (MyTracksMap) getLocalActivityManager().getActivity("tab1"); + MapActivity map = + (MapActivity) getLocalActivityManager().getActivity("tab1"); if (map != null) { getTabHost().setCurrentTab(0); map.showWaypoint(waypointId); @@ -499,159 +482,106 @@ public class MyTracks extends TabActivity implements OnTouchListener, } break; } - case MyTracksConstants.DELETE_TRACK: { + case Constants.DELETE_TRACK: { if (results != null && resultCode == RESULT_OK) { - final long trackId = results.getLongExtra("trackid", selectedTrackId); deleteTrack(trackId); } break; } - case MyTracksConstants.EDIT_DETAILS: { + case Constants.EDIT_DETAILS: { if (results != null && resultCode == RESULT_OK) { - final long trackId = results.getLongExtra("trackid", selectedTrackId); - Intent intent = new Intent(this, MyTracksDetails.class); + Intent intent = new Intent(this, TrackDetails.class); intent.putExtra("trackid", trackId); startActivity(intent); } break; } - case MyTracksConstants.AUTHENTICATE_TO_DOCLIST: { - if (resultCode == RESULT_OK) { - setProgressValue(0); - setProgressMessage( - R.string.progress_message_authenticating_docs); - authenticate(results, - MyTracksConstants.AUTHENTICATE_TO_TRIX, - SendToDocs.GDATA_SERVICE_NAME_DOCLIST); - } else { - dialogManager.dismissDialogSafely(DIALOG_PROGRESS); - } - break; - } - case MyTracksConstants.AUTHENTICATE_TO_TRIX: { - if (resultCode == RESULT_OK) { - setProgressValue(30); - setProgressMessage( - R.string.progress_message_authenticating_docs); - authenticate(results, MyTracksConstants.SEND_TO_DOCS, - SendToDocs.GDATA_SERVICE_NAME_TRIX); - } else { - dialogManager.dismissDialogSafely(DIALOG_PROGRESS); - } - break; - } - case MyTracksConstants.SEND_TO_DOCS: { - if (results != null && resultCode == RESULT_OK) { - Log.d(MyTracksConstants.TAG, "Sending to Docs...."); - setProgressValue(50); - setProgressMessage(R.string.progress_message_sending_docs); - final long trackId = results.getLongExtra("trackid", selectedTrackId); - final SendToDocs sender = new SendToDocs(this, - authMap.get(SendToDocs.GDATA_SERVICE_NAME_TRIX), - authMap.get(SendToDocs.GDATA_SERVICE_NAME_DOCLIST), trackId); - sendToTrackId = trackId; - Runnable onCompletion = new Runnable() { - public void run() { - setProgressValue(100); - dialogManager.dismissDialogSafely(DIALOG_PROGRESS); - runOnUiThread(new Runnable() { - public void run() { - sendToDocsMessage = sender.getStatusMessage(); - sendToDocsSuccess = sender.wasSuccess(); - handleFusionTablesFinish(trackId); - } - }); - } - }; - sender.setOnCompletion(onCompletion); - sender.run(); - } else { - dialogManager.dismissDialogSafely(DIALOG_PROGRESS); - } - break; - } - case MyTracksConstants.SEND_TO_GOOGLE_DIALOG: { - shareRequested = false; + case Constants.SEND_TO_GOOGLE_DIALOG: { + shareRequested = false; dialogManager.showDialogSafely(DIALOG_SEND_TO_GOOGLE); break; } - case MyTracksConstants.SEND_TO_GOOGLE: { - if (results != null && resultCode == RESULT_OK) { - final long trackId; - if (results.hasExtra("trackid")) { - trackId = results.getLongExtra("trackid", -1); - } else { - trackId = selectedTrackId; + case Constants.GET_MAP: { + // User picked a map to upload to + if (resultCode == RESULT_OK) { + results.putExtra("trackid", dataHub.getSelectedTrackId()); + if (results.hasExtra("mapid")) { + sendToMyMapsMapId = results.getStringExtra("mapid"); } - OnSendCompletedListener onCompletion = new OnSendCompletedListener() { - @Override - public void onSendCompleted(String tableId, boolean success, - int statusMessage) { - sendToFusionTablesMessage = getString(statusMessage); - sendToFusionTablesSuccess = success; - if (sendToFusionTablesSuccess) { - sendToFusionTablesTableId = tableId; - // Update the table id for this track: - try { - Track track = providerUtils.getTrack(trackId); - track.setTableId(tableId); - providerUtils.updateTrack(track); - } catch (RuntimeException e) { - // If that fails whatever reasons we'll just log an error, but - // continue. - Log.w(MyTracksConstants.TAG, "Updating table id failed.", e); - } - } - if (dialogManager.getSendToGoogleDialog().getSendToDocs()) { - onActivityResult(MyTracksConstants.AUTHENTICATE_TO_DOCLIST, - RESULT_OK, new Intent()); - } else { - dialogManager.dismissDialogSafely(DIALOG_PROGRESS); - runOnUiThread(new Runnable() { - public void run() { - handleFusionTablesFinish(trackId); - } - }); - } - } - }; - sendToTrackId = trackId; - final SendToFusionTables sender = new SendToFusionTables(this, auth, - trackId, this/*progressIndicator*/, onCompletion); - - HandlerThread handlerThread = new HandlerThread("SendToFusionTables"); - handlerThread.start(); - Handler handler = new Handler(handlerThread.getLooper()); - handler.post(sender); + authenticateToGoogleMaps(results); } else { - dialogManager.dismissDialogSafely(DIALOG_PROGRESS); + onSendToGoogleDone(); } break; } - case MyTracksConstants.SAVE_GPX_FILE: + case Constants.AUTHENTICATE_TO_MY_MAPS: { + // Authenticated with Google My Maps + if (results != null && resultCode == RESULT_OK) { + final String mapId; + if (results.hasExtra("mapid")) { + mapId = results.getStringExtra("mapid"); + } else { + mapId = "new"; + } + + sendToGoogleMaps(trackId, mapId); + } else { + onSendToGoogleDone(); + } + break; + } + case Constants.AUTHENTICATE_TO_FUSION_TABLES: { + // Authenticated with Google Fusion Tables + if (results != null && resultCode == RESULT_OK) { + sendToFusionTables(trackId); + } else { + onSendToGoogleDone(); + } + break; + } + case Constants.AUTHENTICATE_TO_DOCLIST: { + // Authenticated with Google Docs + if (resultCode == RESULT_OK) { + authenticateToGoogleTrix(); + } else { + onSendToGoogleDone(); + } + break; + } + case Constants.AUTHENTICATE_TO_TRIX: { + // Authenticated with Trix + if (resultCode == RESULT_OK) { + sendToGoogleDocs(trackId); + } else { + onSendToGoogleDone(); + } + break; + } + case Constants.SAVE_GPX_FILE: if (exportFormat == null) { exportFormat = TrackFileFormat.GPX; } //$FALL-THROUGH$ - case MyTracksConstants.SAVE_KML_FILE: + case Constants.SAVE_KML_FILE: if (exportFormat == null) { exportFormat = TrackFileFormat.KML; } //$FALL-THROUGH$ - case MyTracksConstants.SAVE_CSV_FILE: + case Constants.SAVE_CSV_FILE: if (exportFormat == null) { exportFormat = TrackFileFormat.CSV; } //$FALL-THROUGH$ - case MyTracksConstants.SAVE_TCX_FILE: + case Constants.SAVE_TCX_FILE: if (exportFormat == null) { exportFormat = TrackFileFormat.TCX; } if (results != null && resultCode == Activity.RESULT_OK) { - final long trackId = results.getLongExtra("trackid", selectedTrackId); if (trackId >= 0) { saveTrack(trackId, exportFormat); } } break; - case MyTracksConstants.SHARE_LINK: { - Track selectedTrack = providerUtils.getTrack(selectedTrackId); + case Constants.SHARE_LINK: { + Track selectedTrack = providerUtils.getTrack(dataHub.getSelectedTrackId()); if (selectedTrack != null) { - if (selectedTrack.getTableId().length() > 0) { - shareLinkToFusionTable(selectedTrackId); + if (!TextUtils.isEmpty(selectedTrack.getMapId())) { + shareLinkToMap(MapsFacade.buildMapUrl(selectedTrack.getMapId())); + } else if (!TextUtils.isEmpty(selectedTrack.getTableId())) { + shareLinkToMap(getFusionTablesUrl(dataHub.getSelectedTrackId())); } else { shareRequested = true; dialogManager.showDialogSafely(DIALOG_SEND_TO_GOOGLE); @@ -659,38 +589,36 @@ public class MyTracks extends TabActivity implements OnTouchListener, } break; } - case MyTracksConstants.SHARE_GPX_FILE: + case Constants.SHARE_GPX_FILE: if (exportFormat == null) { exportFormat = TrackFileFormat.GPX; } //$FALL-THROUGH$ - case MyTracksConstants.SHARE_KML_FILE: + case Constants.SHARE_KML_FILE: if (exportFormat == null) { exportFormat = TrackFileFormat.KML; } //$FALL-THROUGH$ - case MyTracksConstants.SHARE_CSV_FILE: + case Constants.SHARE_CSV_FILE: if (exportFormat == null) { exportFormat = TrackFileFormat.CSV; } //$FALL-THROUGH$ - case MyTracksConstants.SHARE_TCX_FILE: { + case Constants.SHARE_TCX_FILE: { if (exportFormat == null) { exportFormat = TrackFileFormat.TCX; } if (results != null && resultCode == Activity.RESULT_OK) { - final long trackId = results.getLongExtra("trackid", selectedTrackId); if (trackId >= 0) { sendTrack(trackId, exportFormat); } } break; } - case MyTracksConstants.CLEAR_MAP: { - setSelectedTrackId(-1); + case Constants.CLEAR_MAP: { + dataHub.unloadCurrentTrack(); break; } - case MyTracksConstants.WELCOME: { + case Constants.WELCOME: { CheckUnits.check(this); break; } default: { - Log.w(MyTracksConstants.TAG, - "Warning unhandled request code: " + requestCode); + Log.w(TAG, "Warning unhandled request code: " + requestCode); } } } @@ -703,23 +631,13 @@ public class MyTracks extends TabActivity implements OnTouchListener, return false; } - @Override - public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, - String key) { - if (key != null && key.equals(getString(R.string.selected_track_key))) { - selectedTrackId = sharedPreferences.getLong( - getString(R.string.selected_track_key), -1); - } - if (key != null && key.equals(getString(R.string.recording_track_key))) { - recordingTrackId = sharedPreferences.getLong( - getString(R.string.recording_track_key), -1); - } - } - /** * Resets status information for sending to MyMaps/Docs. */ public void resetSendToGoogleStatus() { + sendToMyMapsMapId = null; + sendToMyMapsMessage = ""; + sendToMyMapsSuccess = true; sendToFusionTablesMessage = ""; sendToFusionTablesSuccess = true; sendToDocsMessage = ""; @@ -741,24 +659,24 @@ public class MyTracks extends TabActivity implements OnTouchListener, InputStream is = new FileInputStream(fileName); trackIdsImported = GpxImporter.importGPXFile(is, providerUtils); } catch (SAXException e) { - Log.e(MyTracksConstants.TAG, "Caught an unexpected exception.", e); + Log.e(TAG, "Caught an unexpected exception.", e); message = R.string.error_generic; } catch (ParserConfigurationException e) { - Log.e(MyTracksConstants.TAG, "Caught an unexpected exception.", e); + Log.e(TAG, "Caught an unexpected exception.", e); message = R.string.error_generic; } catch (IOException e) { - Log.e(MyTracksConstants.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(MyTracksConstants.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(MyTracksConstants.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) { // select last track from import file - setSelectedTrackId(trackIdsImported[trackIdsImported.length - 1]); + dataHub.loadTrack(trackIdsImported[trackIdsImported.length - 1]); } else { dialogManager.showMessageDialog(message, false/* success */); } @@ -792,10 +710,10 @@ public class MyTracks extends TabActivity implements OnTouchListener, } /** - * Shares a link to a fusion table via external app (email, gmail, ...) + * Shares a link to a My Map or Fusion Table via external app (email, gmail, ...) * A chooser with apps that support text/plain will be shown to the user. */ - public void shareLinkToFusionTable(long trackId) { + public void shareLinkToMap(String url) { Intent shareIntent = new Intent(Intent.ACTION_SEND); shareIntent.setType("text/plain"); shareIntent.putExtra(Intent.EXTRA_SUBJECT, @@ -807,8 +725,6 @@ public class MyTracks extends TabActivity implements OnTouchListener, getString(R.string.share_url_only_key), false); } - Track track = providerUtils.getTrack(trackId); - String url = SendToFusionTables.getMapVisualizationUrl(track); String msg = shareUrlOnly ? url : String.format( getResources().getText(R.string.share_map_body_format).toString(), url); shareIntent.putExtra(Intent.EXTRA_TEXT, msg); @@ -832,8 +748,8 @@ public class MyTracks extends TabActivity implements OnTouchListener, public void onClick(DialogInterface dialog, int i) { dialog.dismiss(); providerUtils.deleteTrack(trackId); - if (trackId == selectedTrackId) { - setSelectedTrackId(-1); + if (trackId == dataHub.getSelectedTrackId()) { + dataHub.unloadCurrentTrack(); } }}); builder.setNegativeButton(getString(R.string.no), @@ -846,46 +762,6 @@ public class MyTracks extends TabActivity implements OnTouchListener, dialog.show(); } - public Location getCurrentLocation() { - // TODO: Let's look at more advanced algorithms to determine the best - // current location. - LocationManager locationManager = - (LocationManager) getSystemService(Context.LOCATION_SERVICE); - if (locationManager == null) { - return null; - } - final long maxAgeMilliSeconds = 1000 * 60 * 1; // 1 minute - final long maxAgeNetworkMilliSeconds = 1000 * 60 * 10; // 10 minutes - final long now = System.currentTimeMillis(); - Location loc = locationManager.getLastKnownLocation( - MyTracksConstants.GPS_PROVIDER); - if (loc == null || loc.getTime() < now - maxAgeMilliSeconds) { - // We don't have a recent GPS fix, just use cell towers if available - loc = locationManager.getLastKnownLocation( - LocationManager.NETWORK_PROVIDER); - if (loc == null || loc.getTime() < now - maxAgeNetworkMilliSeconds) { - // We don't have a recent cell tower location, let the user know: - Toast.makeText(this, getString(R.string.status_no_location), - Toast.LENGTH_LONG).show(); - return null; - } else { - // Let the user know we have only an approximate location: - Toast.makeText(this, getString(R.string.status_approximate_location), - Toast.LENGTH_LONG).show(); - } - } - return loc; - } - - public Location getLastLocation() { - if (providerUtils.getLastLocationId(recordingTrackId) < 0) { - return null; - } - LocationManager locationManager = - (LocationManager) getSystemService(Context.LOCATION_SERVICE); - return locationManager.getLastKnownLocation(MyTracksConstants.GPS_PROVIDER); - } - /** * Inserts a waypoint marker. * @@ -918,16 +794,15 @@ public class MyTracks extends TabActivity implements OnTouchListener, final String service) { auth = authMap.get(service); if (auth == null) { - Log.i(MyTracksConstants.TAG, - "Creating a new authentication for service: " + service); + Log.i(TAG, "Creating a new authentication for service: " + service); auth = AuthManagerFactory.getAuthManager(this, - MyTracksConstants.GET_LOGIN, + Constants.GET_LOGIN, null, true, service); authMap.put(service, auth); } - Log.d(MyTracksConstants.TAG, "Logging in to " + service + "..."); + Log.d(TAG, "Logging in to " + service + "..."); if (AuthManagerFactory.useModernAuthManager()) { runOnUiThread(new Runnable() { @Override @@ -954,7 +829,7 @@ public class MyTracks extends TabActivity implements OnTouchListener, final String service, final Account account) { auth.doLogin(new Runnable() { public void run() { - Log.d(MyTracksConstants.TAG, "Loggin success for " + service + "!"); + Log.d(TAG, "Loggin success for " + service + "!"); onActivityResult(requestCode, RESULT_OK, results); } }, account); @@ -963,9 +838,9 @@ public class MyTracks extends TabActivity implements OnTouchListener, private void startRecordingNewTrack( ITrackRecordingService trackRecordingService) { try { - recordingTrackId = trackRecordingService.startNewTrack(); + long recordingTrackId = trackRecordingService.startNewTrack(); // Select the recording track. - setSelectedTrackId(recordingTrackId); + dataHub.loadTrack(recordingTrackId); Toast.makeText(this, getString(R.string.status_now_recording), Toast.LENGTH_SHORT).show(); // TODO: We catch Exception, because after eliminating the service process @@ -974,7 +849,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(MyTracksConstants.TAG, "Unable to start recording.", e); + Log.w(TAG, "Unable to start recording.", e); } } @@ -1000,25 +875,27 @@ public class MyTracks extends TabActivity implements OnTouchListener, public void stopRecording() { if (trackRecordingService != null) { // Save the track id as the shared preference will overwrite the recording track id. - long currentTrackId = recordingTrackId; + long currentTrackId = sharedPreferences.getLong(getString(R.string.recording_track_key), -1); try { trackRecordingService.endCurrentTrack(); // 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(MyTracksConstants.TAG, "Unable to stop recording.", e); + Log.e(TAG, "Unable to stop recording.", e); + } + + if (currentTrackId > 0) { + Intent intent = new Intent(MyTracks.this, TrackDetails.class); + intent.putExtra("trackid", currentTrackId); + intent.putExtra("hasCancelButton", false); + startActivity(intent); } - Intent intent = new Intent(MyTracks.this, MyTracksDetails.class); - intent.putExtra("trackid", currentTrackId); - intent.putExtra("hasCancelButton", false); - startActivity(intent); } tryUnbindTrackRecordingService(); try { stopService(new Intent(MyTracks.this, TrackRecordingService.class)); } catch (SecurityException e) { - Log.e(MyTracksConstants.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; } @@ -1027,9 +904,11 @@ public class MyTracks extends TabActivity implements OnTouchListener, * Initiates the process to send tracks to google. * This is called once the user has selected sending options via the * SendToGoogleDialog. + * + * TODO: Change this whole flow to an actual state machine. */ public void sendToGoogle() { - SendToGoogleDialog sendToGoogleDialog = + SendDialog sendToGoogleDialog = dialogManager.getSendToGoogleDialog(); if (sendToGoogleDialog == null) { return; @@ -1037,43 +916,290 @@ public class MyTracks extends TabActivity implements OnTouchListener, setProgressValue(0); clearProgressMessage(); dialogManager.showDialogSafely(DIALOG_PROGRESS); - if (sendToGoogleDialog.getSendToFusionTables()) { - setProgressValue(0); - setProgressMessage(R.string.progress_message_authenticating_fusiontables); - authenticate(new Intent(), MyTracksConstants.SEND_TO_GOOGLE, - SendToFusionTables.SERVICE_ID); - } else { - onActivityResult(MyTracksConstants.AUTHENTICATE_TO_DOCLIST, RESULT_OK, - new Intent()); + + if (sendToGoogleDialog.getSendToMyMaps()) { + sendToGoogleMapsOrPickMap(sendToGoogleDialog); + } else if (sendToGoogleDialog.getSendToFusionTables()) { + authenticateToFusionTables(null); + } else if (sendToGoogleDialog.getSendToDocs()) { + authenticateToGoogleDocs(); + } else { + Log.w(TAG, "Nowhere to upload to"); + onSendToGoogleDone(); } } + 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); + startActivityForResult(listIntent, Constants.GET_MAP); + // The callback for GET_MAP calls authenticateToGoogleMaps + } else { + authenticateToGoogleMaps(null); + } + } + + private void authenticateToGoogleMaps(Intent results) { + if (results == null) { results = new Intent(); } + + setProgressValue(0); + setProgressMessage( + R.string.progress_message_authenticating_mymaps); + authenticate(results, Constants.AUTHENTICATE_TO_MY_MAPS, + MyMapsConstants.SERVICE_NAME); + // AUTHENTICATE_TO_MY_MAPS callback calls sendToGoogleMaps + } + + private void sendToGoogleMaps(final long trackId, String mapId) { + SendToMyMaps.OnSendCompletedListener onCompletion = new SendToMyMaps.OnSendCompletedListener() { + @Override + public void onSendCompleted(String mapId, boolean success, int statusMessage) { + sendToMyMapsMessage = getString(statusMessage); + sendToMyMapsSuccess = success; + if (sendToMyMapsSuccess) { + sendToMyMapsMapId = mapId; + // Update the map id for this track: + try { + Track track = providerUtils.getTrack(trackId); + track.setMapId(mapId); + providerUtils.updateTrack(track); + } catch (RuntimeException e) { + // If that fails whatever reasons we'll just log an error, but + // continue. + Log.w(TAG, "Updating map id failed.", e); + } + } + + onSendToGoogleMapsDone(); + } + }; + final SendToMyMaps sender = new SendToMyMaps(this, mapId, auth, + trackId, this /*progressIndicator*/, onCompletion); + + HandlerThread handlerThread = new HandlerThread("SendToMyMaps"); + handlerThread.start(); + Handler handler = new Handler(handlerThread.getLooper()); + handler.post(sender); + } + + private void onSendToGoogleMapsDone() { + SendDialog sendToGoogleDialog = dialogManager.getSendToGoogleDialog(); + if (sendToGoogleDialog.getSendToFusionTables()) { + authenticateToFusionTables(null); + } else if (sendToGoogleDialog.getSendToDocs()) { + authenticateToGoogleDocs(); + } else { + onSendToGoogleDone(); + } + } + + private void authenticateToFusionTables(Intent results) { + if (results == null) { results = new Intent(); } + + setProgressValue(0); + setProgressMessage(R.string.progress_message_authenticating_fusiontables); + authenticate(results, Constants.AUTHENTICATE_TO_FUSION_TABLES, + SendToFusionTables.SERVICE_ID); + // AUTHENTICATE_TO_FUSION_TABLES callback calls sendToFusionTables + } + + private void sendToFusionTables(final long trackId) { + OnSendCompletedListener onCompletion = new OnSendCompletedListener() { + @Override + public void onSendCompleted(String tableId, boolean success, + int statusMessage) { + sendToFusionTablesMessage = getString(statusMessage); + sendToFusionTablesSuccess = success; + if (sendToFusionTablesSuccess) { + sendToFusionTablesTableId = tableId; + // Update the table id for this track: + try { + Track track = providerUtils.getTrack(trackId); + track.setTableId(tableId); + providerUtils.updateTrack(track); + } catch (RuntimeException e) { + // If that fails whatever reasons we'll just log an error, but + // continue. + Log.w(TAG, "Updating table id failed.", e); + } + } + + onSendToFusionTablesDone(); + } + }; + sendToTrackId = trackId; + final SendToFusionTables sender = new SendToFusionTables(this, auth, + trackId, this/*progressIndicator*/, onCompletion); + + HandlerThread handlerThread = new HandlerThread("SendToFusionTables"); + handlerThread.start(); + Handler handler = new Handler(handlerThread.getLooper()); + handler.post(sender); + } + + private void onSendToFusionTablesDone() { + SendDialog sendToGoogleDialog = dialogManager.getSendToGoogleDialog(); + if (sendToGoogleDialog.getSendToDocs()) { + authenticateToGoogleDocs(); + } else { + onSendToGoogleDone(); + } + } + + private void authenticateToGoogleDocs() { + setProgressValue(0); + setProgressMessage( + R.string.progress_message_authenticating_docs); + authenticate(new Intent(), + Constants.AUTHENTICATE_TO_DOCLIST, + SendToDocs.GDATA_SERVICE_NAME_DOCLIST); + // AUTHENTICATE_TO_DOCLIST callback calls authenticateToGoogleTrix + } + + private void authenticateToGoogleTrix() { + setProgressValue(30); + setProgressMessage( + R.string.progress_message_authenticating_docs); + authenticate(new Intent(), + Constants.AUTHENTICATE_TO_TRIX, + SendToDocs.GDATA_SERVICE_NAME_TRIX); + // AUTHENTICATE_TO_TRIX callback calls sendToGoogleDocs + } + + private void sendToGoogleDocs(final long trackId) { + Log.d(TAG, "Sending to Docs...."); + setProgressValue(50); + setProgressMessage(R.string.progress_message_sending_docs); + final SendToDocs sender = new SendToDocs(this, + authMap.get(SendToDocs.GDATA_SERVICE_NAME_TRIX), + authMap.get(SendToDocs.GDATA_SERVICE_NAME_DOCLIST), trackId); + sendToTrackId = trackId; + Runnable onCompletion = new Runnable() { + public void run() { + setProgressValue(100); + dialogManager.dismissDialogSafely(DIALOG_PROGRESS); + sendToDocsMessage = sender.getStatusMessage(); + sendToDocsSuccess = sender.wasSuccess(); + + onSendToGoogleDocsDone(); + } + }; + sender.setOnCompletion(onCompletion); + sender.run(); + } + + private void onSendToGoogleDocsDone() { + onSendToGoogleDone(); + } + + private void onSendToGoogleDone() { + SendDialog sendToGoogleDialog = dialogManager.getSendToGoogleDialog(); + final boolean sentToMyMaps = sendToGoogleDialog.getSendToMyMaps(); + final boolean sentToFusionTables = sendToGoogleDialog.getSendToFusionTables(); + dialogManager.dismissDialogSafely(DIALOG_PROGRESS); + + // 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); + } + }; + } + + 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 = MapsFacade.buildMapUrl(sendToMyMapsMapId); + } else if (sentToFusionTables && sendToFusionTablesSuccess) { + // Otherwise try using the link to fusion tables + url = getFusionTablesUrl(sendToTrackId); + } + + if (url != null) { + shareLinkToMap(url); + return true; + } + + return false; + } + + protected String getFusionTablesUrl(long trackId) { + Track track = providerUtils.getTrack(trackId); + return track == null ? "" : SendToFusionTables.getMapVisualizationUrl(track); + } + /** - * Writes the selected track id to the shared preferences. - * Executed on the UI thread. - * - * @param trackId the id of the 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. */ - public void setSelectedTrackId(final long trackId) { - ApiFeatures.getInstance().getApiPlatformAdapter().applyPreferenceChanges( - sharedPreferences - .edit() - .putLong(getString(R.string.selected_track_key), trackId)); + 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; + } + + void clearSelectedTrack() { + dataHub.unloadCurrentTrack(); } long getSelectedTrackId() { - return selectedTrackId; + return dataHub.getSelectedTrackId(); } /** * Binds to track recording service if it is running. */ private void tryBindTrackRecordingService() { - Log.d(MyTracksConstants.TAG, + Log.d(TAG, "MyTracks: Trying to bind to track recording service..."); bindService(new Intent(this, TrackRecordingService.class), serviceConnection, 0); - Log.d(MyTracksConstants.TAG, "MyTracks: ...bind finished!"); + Log.d(TAG, "MyTracks: ...bind finished!"); isBound = true; } @@ -1083,14 +1209,12 @@ public class MyTracks extends TabActivity implements OnTouchListener, */ private void tryUnbindTrackRecordingService() { if (isBound) { - Log.d(MyTracksConstants.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(MyTracksConstants.TAG, "MyTracks: ...unbind finished!"); + Log.d(TAG, "MyTracks: ...unbind finished!"); } catch (IllegalArgumentException e) { - Log.d(MyTracksConstants.TAG, - "MyTracks: Tried unbinding, but service was not registered.", e); + Log.d(TAG, "MyTracks: Tried unbinding, but service was not registered.", e); } isBound = false; } @@ -1102,17 +1226,15 @@ public class MyTracks extends TabActivity implements OnTouchListener, * @param trackId The id of the track to be sent */ public void saveTrack(long trackId, TrackFileFormat format) { - dialogManager.showDialogSafely(DIALOG_WRITE_PROGRESS); - final TrackWriter writer = - TrackWriterFactory.newWriter(this, providerUtils, trackId, format); - writer.setOnCompletion(new Runnable() { - public void run() { - dialogManager.dismissDialogSafely(DIALOG_WRITE_PROGRESS); - dialogManager.showMessageDialog(writer.getErrorMessage(), - writer.wasSuccess()); + TrackWriter writer = TrackWriterFactory.newWriter(this, providerUtils, trackId, format); + WriteProgressController controller = new WriteProgressController(this, writer); + controller.setOnCompletionListener(new WriteProgressController.OnCompletionListener() { + @Override + public void onComplete(TrackWriter writer) { + dialogManager.showMessageDialog(writer.getErrorMessage(), writer.wasSuccess()); } }); - writer.writeTrackAsync(); + controller.startWrite(); } /** @@ -1123,9 +1245,7 @@ public class MyTracks extends TabActivity implements OnTouchListener, * @param trackId The id of the track to be sent */ public void sendTrack(long trackId, final TrackFileFormat format) { - dialogManager.showDialogSafely(DIALOG_WRITE_PROGRESS); - final TrackWriter writer = - TrackWriterFactory.newWriter(this, providerUtils, trackId, format); + TrackWriter writer = TrackWriterFactory.newWriter(this, providerUtils, trackId, format); FileUtils fileUtils = new FileUtils(); String extension = format.getExtension(); @@ -1133,57 +1253,31 @@ public class MyTracks extends TabActivity implements OnTouchListener, File dir = new File(dirName); writer.setDirectory(dir); - writer.setOnCompletion(new Runnable() { - public void run() { - dialogManager.dismissDialogSafely(DIALOG_WRITE_PROGRESS); + + WriteProgressController controller = new WriteProgressController(this, writer); + controller.setOnCompletionListener(new WriteProgressController.OnCompletionListener() { + @Override + public void onComplete(TrackWriter writer) { if (!writer.wasSuccess()) { - dialogManager.showMessageDialog(writer.getErrorMessage(), - writer.wasSuccess()); - } else { - Intent shareIntent = new Intent(Intent.ACTION_SEND); - shareIntent.putExtra(Intent.EXTRA_SUBJECT, - getResources().getText(R.string.send_track_subject).toString()); - shareIntent.putExtra(Intent.EXTRA_TEXT, - getResources().getText(R.string.send_track_body_format) - .toString()); - shareIntent.setType(format.getMimeType()); - Uri u = Uri.fromFile(new File(writer.getAbsolutePath())); - shareIntent.putExtra(Intent.EXTRA_STREAM, u); - startActivity(Intent.createChooser(shareIntent, - getResources().getText(R.string.share_track).toString())); + dialogManager.showMessageDialog(writer.getErrorMessage(), writer.wasSuccess()); + return; } + + Intent shareIntent = new Intent(Intent.ACTION_SEND); + shareIntent.putExtra(Intent.EXTRA_SUBJECT, + getResources().getText(R.string.send_track_subject).toString()); + shareIntent.putExtra(Intent.EXTRA_TEXT, + getResources().getText(R.string.send_track_body_format) + .toString()); + shareIntent.setType(format.getMimeType()); + Uri u = Uri.fromFile(new File(writer.getAbsolutePath())); + shareIntent.putExtra(Intent.EXTRA_STREAM, u); + startActivity(Intent.createChooser(shareIntent, + getResources().getText(R.string.share_track).toString())); } }); - writer.writeTrackAsync(); - } - /** - * Notifies that uploading to fusion tables is finished. - */ - private void handleFusionTablesFinish(long trackId) { - if (shareRequested && sendToFusionTablesSuccess) { - // Just share - Toast.makeText(this, getFusionTablesResultMessage(), Toast.LENGTH_LONG).show(); - shareLinkToFusionTable(trackId); - } else { - dialogManager.showDialogSafely(DialogManager.DIALOG_SEND_TO_GOOGLE_RESULT); - } - } - - public String getFusionTablesResultMessage() { - StringBuilder message = new StringBuilder(); - SendToGoogleDialog sendToGoogleDialog = - dialogManager.getSendToGoogleDialog(); - if (sendToGoogleDialog.getSendToFusionTables()) { - message.append(sendToFusionTablesMessage); - } - if (sendToGoogleDialog.getSendToDocs()) { - if (message.length() > 0) { - message.append(' '); - } - message.append(sendToDocsMessage); - } - return message.toString(); + controller.startWrite(); } public AccountChooser getAccountChooser() { @@ -1202,6 +1296,10 @@ public class MyTracks extends TabActivity implements OnTouchListener, return dialogManager; } + public String getSendToMyMapsMapId() { + return sendToMyMapsMapId; + } + public String getSendToFusionTablesTableId() { return sendToFusionTablesTableId; } @@ -1212,7 +1310,7 @@ public class MyTracks extends TabActivity implements OnTouchListener, // @VisibleForTesting long getRecordingTrackId() { - return recordingTrackId; + return sharedPreferences.getLong(getString(R.string.recording_track_key), -1); } // @VisibleForTesting @@ -1229,4 +1327,8 @@ public class MyTracks extends TabActivity implements OnTouchListener, ITrackRecordingService getTrackRecordingService() { return trackRecordingService; } + + public TrackDataHub getDataHub() { + return dataHub; + } } diff --git a/MyTracks/src/com/google/android/apps/mytracks/MyTracksMap.java b/MyTracks/src/com/google/android/apps/mytracks/MyTracksMap.java deleted file mode 100644 index 3c019f950..000000000 --- a/MyTracks/src/com/google/android/apps/mytracks/MyTracksMap.java +++ /dev/null @@ -1,1094 +0,0 @@ -/* - * Copyright 2008 Google Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ -package com.google.android.apps.mytracks; - -import com.google.android.apps.mytracks.content.MyTracksProviderUtils; -import com.google.android.apps.mytracks.content.Track; -import com.google.android.apps.mytracks.content.TrackPointsColumns; -import com.google.android.apps.mytracks.content.Waypoint; -import com.google.android.apps.mytracks.content.WaypointsColumns; -import com.google.android.apps.mytracks.services.StatusAnnouncerFactory; -import com.google.android.apps.mytracks.stats.TripStatistics; -import com.google.android.apps.mytracks.util.ApiFeatures; -import com.google.android.apps.mytracks.util.GeoRect; -import com.google.android.apps.mytracks.util.MyTracksUtils; -import com.google.android.maps.GeoPoint; -import com.google.android.maps.MapActivity; -import com.google.android.maps.MapController; -import com.google.android.maps.MapView; -import com.google.android.maps.mytracks.R; - -import android.content.Context; -import android.content.Intent; -import android.content.SharedPreferences; -import android.database.ContentObserver; -import android.database.Cursor; -import android.hardware.GeomagneticField; -import android.hardware.Sensor; -import android.hardware.SensorEvent; -import android.hardware.SensorEventListener; -import android.hardware.SensorManager; -import android.location.Location; -import android.location.LocationListener; -import android.location.LocationManager; -import android.location.LocationProvider; -import android.os.Bundle; -import android.os.Handler; -import android.os.HandlerThread; -import android.provider.Settings; -import android.util.Log; -import android.view.ContextMenu; -import android.view.Menu; -import android.view.MenuItem; -import android.view.MotionEvent; -import android.view.SubMenu; -import android.view.View; -import android.view.Window; -import android.view.ContextMenu.ContextMenuInfo; -import android.view.View.OnCreateContextMenuListener; -import android.widget.ImageButton; -import android.widget.LinearLayout; -import android.widget.RelativeLayout; -import android.widget.TextView; -import android.widget.Toast; - -/** - * The map view activity of the MyTracks application. - * - * @author Leif Hendrik Wilden - */ -public class MyTracksMap extends MapActivity - implements View.OnTouchListener, View.OnClickListener, - SharedPreferences.OnSharedPreferenceChangeListener { - private static final int TRACKPOINT_BUFFER_SIZE = 1024; - - // Saved instance state keys: - // --------------------------- - - public static final String KEY_CURRENT_LOCATION = "currentLocation"; - public static final String KEY_KEEP_MY_LOCATION_VISIBLE = - "keepMyLocationVisible"; - public static final String KEY_HAVE_GOOD_FIX = "haveGoodFix"; - - /** - * The ID of the currently selected track (or -1 if nothing selected). - */ - private long selectedTrackId = -1; - - /** - * The id of the currently recording track. - */ - private long recordingTrackId = -1; - - /** - * True if the map should be scrolled so that the pointer is always in the - * visible area. - */ - private boolean keepMyLocationVisible; - - /** - * Id of the first location that was seen when reading tracks from the - * provider. - */ - private long firstSeenLocationId = -1; - - /** - * Id of the last location that was seen when reading tracks from the - * provider. This is used to determine which locations are new compared to the - * last time the mapOverlay was updated. - */ - private long lastSeenLocationId = -1; - - /** - * Magnetic variation. - */ - private double variation; - - /** - * From the shared preferences. - */ - private int minRequiredAccuracy = - MyTracksSettings.DEFAULT_MIN_REQUIRED_ACCURACY; - - /** - * True, if the application thinks it has a good fix, i.e. accuracy is better - * than the required accuracy. - */ - private boolean haveGoodFix; - - /** - * The current pointer location. - */ - private Location currentLocation; - - /** - * A thread with a looper. Post to updateTrackHandler to execute - * {@link Runnable}s on this thread. - */ - private HandlerThread updateTrackThread; - - /** - * Handler for updateTrackThread. - */ - private Handler updateTrackHandler; - - private MyTracksProviderUtils providerUtils; - - private SharedPreferences sharedPreferences; - - /** - * A runnable that updates the track from the provider (looking for points - * added after "lastSeenLocationId"). - */ - private final Runnable updateTrackRunnable = new Runnable() { - @Override - public void run() { - if (!isATrackSelected()) { - return; - } - - readAllNewTrackPoints(); - } - }; - - /** - * A runnable that restores all track points from the provider. - */ - private Runnable restoreTrackRunnable = new Runnable() { - @Override - public void run() { - if (!isATrackSelected()) { - return; - } - - mapOverlay.clearPoints(); - firstSeenLocationId = -1; - lastSeenLocationId = -1; - readAllNewTrackPoints(); - } - }; - - /** - * A runnable that restores all waypoints from the provider. - */ - private final Runnable restoreWaypointsRunnable = new Runnable() { - @Override - public void run() { - if (!isATrackSelected()) { - return; - } - - Cursor cursor = null; - mapOverlay.clearWaypoints(); - try { - // We will silently drop extra waypoints to make the app responsive. - // TODO: Try to only load the waypoints in the view port. - cursor = providerUtils.getWaypointsCursor( - selectedTrackId, 0, - MyTracksConstants.MAX_DISPLAYED_WAYPOINTS_POINTS); - if (cursor != null && cursor.moveToFirst()) { - do { - Waypoint waypoint = providerUtils.createWaypoint(cursor); - if (MyTracksUtils.isValidLocation(waypoint.getLocation())) { - mapOverlay.addWaypoint(waypoint); - } - } while (cursor.moveToNext()); - } - } catch (RuntimeException e) { - Log.w(MyTracksConstants.TAG, "Caught an unexpected exception.", e); - } finally { - if (cursor != null) { - cursor.close(); - } - } - mapView.postInvalidate(); - } - }; - - /** - * A runnable intended to be posted to the {@code #updateTrackThread} after - * the selected track changes. It will post to the UI thread to update - * the screen elements and move the map to show the selected track. - */ - private final Runnable setSelectedTrackRunnable = new Runnable() { - @Override - public void run() { - uiHandler.post(new Runnable() { - public void run() { - showTrack(selectedTrackId); - mapOverlay.setTrackDrawingEnabled(isATrackSelected()); - mapOverlay.setShowEndMarker(!isRecordingSelected()); - mapView.invalidate(); - busyPane.setVisibility(View.GONE); - updateOptionsButton(); - } - }); - } - }; - - // UI elements: - // ------------- - - private RelativeLayout screen; - private MapView mapView; - private MyTracksOverlay mapOverlay; - private LinearLayout messagePane; - private TextView messageText; - private LinearLayout busyPane; - private ImageButton optionsBtn; - - private MenuItem myLocation; - private MenuItem toggleLayers; - - private SensorManager sensorManager; - private LocationManager locationManager; - private ContentObserver observer; - private ContentObserver waypointObserver; - - /** Handler for callbacks to the UI thread */ - private final Handler uiHandler = new Handler(); - - /** - * We are not displaying driving directions. Just an arbitrary track that is - * not associated to any licensed mapping data. Therefore it should be okay to - * return false here and still comply with the terms of service. - */ - @Override - protected boolean isRouteDisplayed() { - return false; - } - - /** - * We are displaying a location. This needs to return true in order to comply - * with the terms of service. - */ - @Override - protected boolean isLocationDisplayed() { - return true; - } - - // Application life cycle: - // ------------------------ - - @Override - public void onCreate(Bundle bundle) { - Log.d(MyTracksConstants.TAG, "MyTracksMap.onCreate"); - super.onCreate(bundle); - - // The volume we want to control is the Text-To-Speech volume - int volumeStream = - new StatusAnnouncerFactory(ApiFeatures.getInstance()).getVolumeStream(); - setVolumeControlStream(volumeStream); - - providerUtils = MyTracksProviderUtils.Factory.get(this); - - // We don't need a window title bar: - requestWindowFeature(Window.FEATURE_NO_TITLE); - - // Inflate the layout: - setContentView(R.layout.mytracks_layout); - - // Remove the window's background because the MapView will obscure it - getWindow().setBackgroundDrawable(null); - - // Set up a map overlay: - screen = (RelativeLayout) findViewById(R.id.screen); - mapView = (MapView) findViewById(R.id.map); - mapView.requestFocus(); - mapOverlay = new MyTracksOverlay(this); - mapView.getOverlays().add(mapOverlay); - mapView.setOnTouchListener(this); - messagePane = (LinearLayout) findViewById(R.id.messagepane); - messageText = (TextView) findViewById(R.id.messagetext); - busyPane = (LinearLayout) findViewById(R.id.busypane); - optionsBtn = (ImageButton) findViewById(R.id.showOptions); - - optionsBtn.setOnCreateContextMenuListener(contextMenuListener); - optionsBtn.setOnClickListener(this); - - setupZoomControls(); - - // Get the sensor and location managers: - sensorManager = (SensorManager) getSystemService(SENSOR_SERVICE); - locationManager = - (LocationManager) getSystemService(Context.LOCATION_SERVICE); - - updateTrackThread = new HandlerThread("updateTrackThread"); - updateTrackThread.start(); - updateTrackHandler = new Handler(updateTrackThread.getLooper()); - - // Register observer for the track point provider: - Handler contentHandler = new Handler(); - observer = new ContentObserver(contentHandler) { - @Override - public void onChange(boolean selfChange) { - Log.d(MyTracksConstants.TAG, "MyTracksMap: ContentObserver.onChange"); - if (!isRecordingSelected()) { - // No track, or one other than the recording track is selected, - // don't bother. - return; - } - // Update can potentially be lengthy, put it in its own thread: - updateTrackHandler.post(updateTrackRunnable); - super.onChange(selfChange); - } - }; - - waypointObserver = new ContentObserver(contentHandler) { - @Override - public void onChange(boolean selfChange) { - Log.d(MyTracksConstants.TAG, - "MyTracksMap: ContentObserver.onChange waypoints"); - if (!isATrackSelected()) { - return; - } - updateTrackHandler.post(restoreWaypointsRunnable); - super.onChange(selfChange); - } - }; - - // Read shared preferences and register change listener. - sharedPreferences = getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0); - if (sharedPreferences != null) { - reloadSharedPreferences(sharedPreferences, null); - updateOptionsButton(); - sharedPreferences.registerOnSharedPreferenceChangeListener(this); - } - } - - @Override - protected void onDestroy() { - Log.d(MyTracksConstants.TAG, "MyTracksMap.onDestroy"); - - if (updateTrackThread != null) { - ApiFeatures.getInstance().getApiPlatformAdapter().stopHandlerThread( - updateTrackThread); - } - if (sharedPreferences != null) { - sharedPreferences.unregisterOnSharedPreferenceChangeListener(this); - } - - super.onDestroy(); - } - - /** - * Returns whether there's a track currently selected for display. - */ - private boolean isATrackSelected() { - return selectedTrackId >= 0; - } - - /** - * Returns whether we're currently recording the same track that's selected - * for display. - */ - private boolean isRecordingSelected() { - return isATrackSelected() && selectedTrackId == recordingTrackId; - } - - protected void setupZoomControls() { - mapView.setBuiltInZoomControls(true); - } - - @Override - protected void onStart() { - // Called after onCreate or onStop. - // Will be followed by onRestart. - Log.d(MyTracksConstants.TAG, "MyTracksMap.onStart"); - super.onStart(); - } - - @Override - protected void onStop() { - // 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(MyTracksConstants.TAG, "MyTracksMap.onStop"); - super.onStop(); - } - - @Override - protected void onRestart() { - // Called when the current activity is being re-displayed. - // Will be followed by onResume. - Log.d(MyTracksConstants.TAG, "MyTracksMap.onRestart"); - super.onRestart(); - } - - @Override - 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(MyTracksConstants.TAG, "MyTracksMap.onPause"); - unregisterLocationAndSensorListeners(); - unregisterContentObservers(); - super.onPause(); - } - - @Override - protected void onResume() { - // Called when the current activity is being displayed or re-displayed - // to the user. - Log.d(MyTracksConstants.TAG, "MyTracksMap.onResume"); - super.onResume(); - - // Reload all preferences as they might have changed since last run. - reloadSharedPreferences(sharedPreferences, null); - - // Make sure any updates that might have happened are propagated to the - // Map overlay: - observer.onChange(false); - waypointObserver.onChange(false); - - registerContentObservers(); - registerLocationAndSensorListeners(); - - if (locationManager.isProviderEnabled(MyTracksConstants.GPS_PROVIDER)) { - messageText.setText(R.string.wait_for_fix); - messagePane.setOnClickListener(null); - } else { - messageText.setText(R.string.status_enable_gps); - messagePane.setVisibility(View.VISIBLE); - messagePane.setOnClickListener(this); - screen.requestLayout(); - } - - // While this activity was paused the user may have deleted the selected - // track. In that case the map overlay needs to be cleared: - if (isATrackSelected() && !providerUtils.trackExists(selectedTrackId)) { - // The recording track must have been deleted meanwhile. - mapOverlay.setTrackDrawingEnabled(false); - mapView.invalidate(); - } - } - - @Override - protected void onSaveInstanceState(Bundle outState) { - Log.d(MyTracksConstants.TAG, "MyTracksMap.onSaveInstanceState"); - outState.putBoolean(KEY_HAVE_GOOD_FIX, haveGoodFix); - outState.putBoolean(KEY_KEEP_MY_LOCATION_VISIBLE, keepMyLocationVisible); - if (currentLocation != null) { - outState.putParcelable(KEY_CURRENT_LOCATION, currentLocation); - } - super.onSaveInstanceState(outState); - } - - @Override - protected void onRestoreInstanceState(Bundle bundle) { - Log.d(MyTracksConstants.TAG, "MyTracksMap.onRestoreInstanceState"); - if (bundle != null) { - super.onRestoreInstanceState(bundle); - haveGoodFix = bundle.getBoolean(KEY_HAVE_GOOD_FIX, false); - keepMyLocationVisible = - bundle.getBoolean(KEY_KEEP_MY_LOCATION_VISIBLE, false); - if (bundle.containsKey(KEY_CURRENT_LOCATION)) { - currentLocation = (Location) bundle.getParcelable(KEY_CURRENT_LOCATION); - if (currentLocation != null) { - setVariation(currentLocation); - showCurrentLocation(); - } - } else { - currentLocation = null; - } - } - } - - // Utility functions: - // ------------------- - - /** - * Toggles between satellite and map view. - */ - public void toggleLayer() { - mapView.setSatellite(!mapView.isSatellite()); - } - - /** - * Registers to receive location updates from the GPS location provider and - * sensor updated from the compass. - */ - void registerLocationAndSensorListeners() { - if (locationManager != null) { - LocationProvider gpsProvider = - locationManager.getProvider(MyTracksConstants.GPS_PROVIDER); - if (gpsProvider == null) { - alert(getString(R.string.error_no_gps_location_provider)); - return; - } else { - Log.d(MyTracksConstants.TAG, "MyTracksMap: Using location provider " - + gpsProvider.getName()); - } - locationManager.requestLocationUpdates(gpsProvider.getName(), - 0 /*minTime*/, 0 /*minDist*/, locationListener); - try { - locationManager.requestLocationUpdates(LocationManager.NETWORK_PROVIDER, - 1000 * 60 * 5 /*minTime*/, 0 /*minDist*/, locationListener); - } 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(MyTracksConstants.TAG, - "Could not register network location listener."); - } - } - if (sensorManager == null) { - return; - } - Sensor compass = sensorManager.getDefaultSensor(Sensor.TYPE_ORIENTATION); - if (compass == null) { - return; - } - Log.d(MyTracksConstants.TAG, - "MyTracksMap: Now registering sensor listeners."); - sensorManager.registerListener( - sensorListener, compass, SensorManager.SENSOR_DELAY_UI); - } - - /** - * Unregisters all location and sensor listeners - */ - void unregisterLocationAndSensorListeners() { - if (locationManager != null) { - Log.d(MyTracksConstants.TAG, - "MyTracksMap: Now unregistering location listeners."); - locationManager.removeUpdates(locationListener); - } - if (sensorManager != null) { - Log.d(MyTracksConstants.TAG, - "MyTracksMap: Now unregistering sensor listeners."); - sensorManager.unregisterListener(sensorListener); - } - } - - /** - * Registers the content observer for the map overlay. - */ - private void registerContentObservers() { - getContentResolver().registerContentObserver( - TrackPointsColumns.CONTENT_URI, false /* notifyForDescendents */, - observer); - getContentResolver().registerContentObserver( - WaypointsColumns.CONTENT_URI, false /* notifyForDescendents */, - waypointObserver); - } - - /** - * Unregisters the content observer for the map overlay. - */ - private void unregisterContentObservers() { - getContentResolver().unregisterContentObserver(observer); - getContentResolver().unregisterContentObserver(waypointObserver); - } - - /** - * Shows the options button if a track is selected, or hide it if not. - */ - private void updateOptionsButton() { - optionsBtn.setVisibility( - isATrackSelected() ? View.VISIBLE : View.INVISIBLE); - } - - /** - * Tests if a location is visible. - * - * @param location a given location - * @return true if the given location is within the visible map area - */ - private boolean locationIsVisible(Location location) { - if (location == null || mapView == null) { - return false; - } - GeoPoint center = mapView.getMapCenter(); - int latSpan = mapView.getLatitudeSpan(); - int lonSpan = mapView.getLongitudeSpan(); - - // Bottom of map view is obscured by zoom controls/buttons. - // Subtract a margin from the visible area: - GeoPoint marginBottom = mapView.getProjection().fromPixels( - 0, mapView.getHeight()); - GeoPoint marginTop = mapView.getProjection().fromPixels(0, - mapView.getHeight() - - mapView.getZoomButtonsController().getZoomControls().getHeight()); - int margin = - Math.abs(marginTop.getLatitudeE6() - marginBottom.getLatitudeE6()); - GeoRect r = new GeoRect(center, latSpan, lonSpan); - r.top += margin; - - GeoPoint geoPoint = MyTracksUtils.getGeoPoint(location); - return r.contains(geoPoint); - } - - /** - * Moves the location pointer to the current location and center the map if - * the current location is outside the visible area. - */ - private void showCurrentLocation() { - if (currentLocation == null || mapOverlay == null || mapView == null) { - return; - } - mapOverlay.setMyLocation(currentLocation); - mapView.invalidate(); - if (keepMyLocationVisible && !locationIsVisible(currentLocation)) { - MapController controller = mapView.getController(); - GeoPoint geoPoint = MyTracksUtils.getGeoPoint(currentLocation); - controller.animateTo(geoPoint); - } - } - - /** - * Zooms and pans the map so that the given track is visible. - * - * @param trackId a given track ID - */ - public void showTrack(long trackId) { - if (mapView == null) { - return; - } - - Track track = providerUtils.getTrack(trackId); - if (track == null || track.getNumberOfPoints() < 2) { - return; - } - - TripStatistics stats = track.getStatistics(); - int bottom = stats.getBottom(); - int left = stats.getLeft(); - int latSpanE6 = stats.getTop() - bottom; - int lonSpanE6 = stats.getRight() - left; - if (latSpanE6 > 0 - && latSpanE6 < 180E6 - && lonSpanE6 > 0 - && lonSpanE6 < 360E6) { - keepMyLocationVisible = false; - GeoPoint center = new GeoPoint( - bottom + latSpanE6 / 2, - left + lonSpanE6 / 2); - if (MyTracksUtils.isValidGeoPoint(center)) { - mapView.getController().setCenter(center); - mapView.getController().zoomToSpan(latSpanE6, lonSpanE6); - } - } - } - - /** - * Zooms and pans the map so that the given waypoint is visible. - */ - public void showWaypoint(long waypointId) { - Waypoint wpt = providerUtils.getWaypoint(waypointId); - if (wpt != null && wpt.getLocation() != null) { - keepMyLocationVisible = false; - GeoPoint center = new GeoPoint( - (int) (wpt.getLocation().getLatitude() * 1E6), - (int) (wpt.getLocation().getLongitude() * 1E6)); - mapView.getController().setCenter(center); - mapView.getController().setZoom(20); - mapView.invalidate(); - } - } - - /** - * Sets the selected track and zoom and pan the map so that it is visible. - * - * @param trackId a given track id - */ - public void setSelectedTrack(final long trackId) { - Log.d(MyTracksConstants.TAG, "MyTracksMap.setSelectedTrack: " - + "selectedTrackId = " + selectedTrackId + ", trackId = " + trackId); - - if (selectedTrackId == trackId) { - // Selected track did not change, nothing to do. - mapOverlay.setTrackDrawingEnabled(isATrackSelected()); - updateOptionsButton(); - mapView.invalidate(); - return; - } - - if (trackId < 0) { - // Remove selection. - selectedTrackId = -1; - mapOverlay.setTrackDrawingEnabled(false); - mapOverlay.clearWaypoints(); - updateOptionsButton(); - mapView.invalidate(); - return; - } - - busyPane.setVisibility(View.VISIBLE); - selectedTrackId = trackId; - loadSelectedTrack(); - } - - private void loadSelectedTrack() { - updateTrackHandler.post(restoreTrackRunnable); - updateTrackHandler.post(restoreWaypointsRunnable); - updateTrackHandler.post(setSelectedTrackRunnable); - } - - /** - * Displays an alert for a few seconds. - * - * @param txt The text to be displayed - */ - public void alert(String txt) { - Toast.makeText(this, txt, Toast.LENGTH_LONG).show(); - } - - public void launchMyLocationSettings() { - startActivity(new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS)); - } - - public void setVariation(Location location) { - long timestamp = location.getTime(); - if (timestamp == 0) { - // Hack for Samsung phones which don't populate the time field - timestamp = System.currentTimeMillis(); - } - - GeomagneticField field = new GeomagneticField( - (float) location.getLatitude(), - (float) location.getLongitude(), - (float) location.getAltitude(), - timestamp); - variation = field.getDeclination(); - - Log.d(MyTracksConstants.TAG, - "MyTracksMap: Variation reset to " + variation + " degrees."); - } - - public MyTracksOverlay getMapOverlay() { - return mapOverlay; - } - - public MapView getMapView() { - return mapView; - } - - // Event listeners: - // ----------------- - - private final OnCreateContextMenuListener contextMenuListener = - new OnCreateContextMenuListener() { - @Override - public void onCreateContextMenu(ContextMenu menu, View v, - ContextMenuInfo menuInfo) { - menu.setHeaderTitle(R.string.tracklist_this_track); - menu.add(0, MyTracksConstants.MENU_EDIT, 0, - R.string.tracklist_edit_track); - if (!isRecordingSelected()) { - menu.add(0, MyTracksConstants.MENU_SEND_TO_GOOGLE, 0, - R.string.tracklist_send_to_google); - SubMenu share = menu.addSubMenu(0, MyTracksConstants.MENU_SHARE, 0, - R.string.tracklist_share_track); - share.add(0, MyTracksConstants.MENU_SHARE_LINK, 0, - R.string.tracklist_share_linkfusion); - share.add(0, MyTracksConstants.MENU_SHARE_GPX_FILE, 0, - R.string.tracklist_share_gpx_file); - share.add(0, MyTracksConstants.MENU_SHARE_KML_FILE, 0, - R.string.tracklist_share_kml_file); - share.add(0, MyTracksConstants.MENU_SHARE_CSV_FILE, 0, - R.string.tracklist_share_csv_file); - share.add(0, MyTracksConstants.MENU_SHARE_TCX_FILE, 0, - R.string.tracklist_share_tcx_file); - SubMenu save = menu.addSubMenu(0, - MyTracksConstants.MENU_WRITE_TO_SD_CARD, 0, - R.string.tracklist_write_to_sd); - save.add(0, MyTracksConstants.MENU_SAVE_GPX_FILE, 0, - R.string.tracklist_save_as_gpx); - save.add(0, MyTracksConstants.MENU_SAVE_KML_FILE, 0, - R.string.tracklist_save_as_kml); - save.add(0, MyTracksConstants.MENU_SAVE_CSV_FILE, 0, - R.string.tracklist_save_as_csv); - save.add(0, MyTracksConstants.MENU_SAVE_TCX_FILE, 0, - R.string.tracklist_save_as_tcx); - menu.add(0, MyTracksConstants.MENU_CLEAR_MAP, 0, - R.string.tracklist_clear_map); - menu.add(0, MyTracksConstants.MENU_DELETE, 0, - R.string.tracklist_delete_track); - } - } - }; - - @Override - public boolean onMenuItemSelected(int featureId, MenuItem item) { - if (!super.onMenuItemSelected(featureId, item)) { - if (isATrackSelected()) { - MyTracks.getInstance().onActivityResult( - MyTracksConstants.getActionFromMenuId(item.getItemId()), RESULT_OK, - new Intent()); - return true; - } - } - return false; - } - - @Override - public boolean onCreateOptionsMenu(Menu menu) { - super.onCreateOptionsMenu(menu); - myLocation = menu.add(0, MyTracksConstants.MENU_MY_LOCATION, 0, - R.string.mylocation); - myLocation.setIcon(android.R.drawable.ic_menu_mylocation); - toggleLayers = menu.add(0, MyTracksConstants.MENU_TOGGLE_LAYERS, 0, - R.string.switch_to_sat); - toggleLayers.setIcon(android.R.drawable.ic_menu_mapmode); - return true; - } - - @Override - public boolean onPrepareOptionsMenu(Menu menu) { - toggleLayers.setTitle(mapView.isSatellite() ? - R.string.switch_to_map : R.string.switch_to_sat); - return super.onPrepareOptionsMenu(menu); - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case MyTracksConstants.MENU_MY_LOCATION: { - Location loc = MyTracks.getInstance().getCurrentLocation(); - if (loc != null) { - currentLocation = loc; - setVariation(currentLocation); - mapOverlay.setMyLocation(loc); - mapView.invalidate(); - GeoPoint geoPoint = MyTracksUtils.getGeoPoint(loc); - MapController controller = mapView.getController(); - controller.animateTo(geoPoint); - if (mapView.getZoomLevel() < 18) { - controller.setZoom(18); - } - keepMyLocationVisible = true; - } - return true; - } - case MyTracksConstants.MENU_TOGGLE_LAYERS: { - toggleLayer(); - return true; - } - } - return super.onOptionsItemSelected(item); - } - - @Override - public void onClick(View v) { - if (v == messagePane) { - launchMyLocationSettings(); - } else if (v == optionsBtn) { - optionsBtn.performLongClick(); - } - } - - /** - * We want the pointer to become visible again in case of the next location - * update: - */ - @Override - public boolean onTouch(View view, MotionEvent event) { - if (keepMyLocationVisible && event.getAction() == MotionEvent.ACTION_MOVE) { - if (!locationIsVisible(currentLocation)) { - keepMyLocationVisible = false; - } - } - return false; - } - - @Override - public void onSharedPreferenceChanged( - final SharedPreferences sharedPreferences, final String key) { - Log.d(MyTracksConstants.TAG, - "MyTracksMap.onSharedPreferenceChanged: " + key); - if (key != null) { - uiHandler.post(new Runnable() { - @Override - public void run() { - reloadSharedPreferences(sharedPreferences, key); - } - }); - } - } - - private final LocationListener locationListener = new LocationListener() { - @Override - public void onProviderEnabled(String provider) { - if (provider.equals(MyTracksConstants.GPS_PROVIDER)) { - messageText.setText(R.string.wait_for_fix); - } - } - - @Override - public void onProviderDisabled(String provider) { - if (provider.equals(MyTracksConstants.GPS_PROVIDER)) { - messageText.setText(R.string.status_enable_gps); - messagePane.setVisibility(View.VISIBLE); - messagePane.setOnClickListener(MyTracksMap.this); - screen.requestLayout(); - } - } - - @Override - public void onLocationChanged(Location location) { - if (location.getProvider().equals(MyTracksConstants.GPS_PROVIDER)) { - // Recalculate the variation if there was a jump in location > 1km: - if (currentLocation == null || - location.distanceTo(currentLocation) > 1000) { - setVariation(location); - } - currentLocation = location; - boolean haveGoodFixNow = - currentLocation.getAccuracy() < minRequiredAccuracy; - if (haveGoodFixNow != haveGoodFix) { - haveGoodFix = haveGoodFixNow; - messagePane.setVisibility(haveGoodFix ? View.GONE : View.VISIBLE); - screen.requestLayout(); - } - showCurrentLocation(); - } else { - Log.d(MyTracksConstants.TAG, - "MyTracksMap: Network location update received."); - } - } - - @Override - public void onStatusChanged(String provider, int status, Bundle extras) { - if (provider.equals(MyTracksConstants.GPS_PROVIDER)) { - switch (status) { - case LocationProvider.OUT_OF_SERVICE: - case LocationProvider.TEMPORARILY_UNAVAILABLE: - haveGoodFix = false; - messagePane.setVisibility(View.VISIBLE); - screen.requestLayout(); - break; - } - } - } - }; - - private final SensorEventListener sensorListener = new SensorEventListener() { - @Override - public void onSensorChanged(SensorEvent se) { - synchronized (this) { - float magneticHeading = se.values[0]; - double heading = magneticHeading + variation; - if (mapOverlay.setHeading((float) heading)) { - mapView.invalidate(); - } - } - } - - @Override - public void onAccuracyChanged(Sensor s, int accuracy) { - // do nothing - } - }; - - private void reloadSharedPreferences(SharedPreferences sharedPreferences, - String key) { - if (key == null || - key.equals(getString(R.string.min_required_accuracy_key))) { - minRequiredAccuracy = sharedPreferences.getInt( - getString(R.string.min_required_accuracy_key), - MyTracksSettings.DEFAULT_MIN_REQUIRED_ACCURACY); - } - if (key == null || key.equals(getString(R.string.recording_track_key))) { - recordingTrackId = sharedPreferences.getLong( - getString(R.string.recording_track_key), -1); - } - if (key == null || key.equals(getString(R.string.selected_track_key))) { - setSelectedTrack(sharedPreferences.getLong( - getString(R.string.selected_track_key), -1)); - } - - // Show end marker if the track has been selected and is not recording. - // Note: This check must be *after* a call to setSelectedTrack(...) above. - if (isATrackSelected()) { - mapOverlay.setShowEndMarker(!isRecordingSelected()); - mapView.postInvalidate(); - } - } - - private void readAllNewTrackPoints() { - int numPoints = mapOverlay.getNumLocations(); - if (numPoints >= MyTracksConstants.MAX_DISPLAYED_TRACK_POINTS) { - // We're about to exceed the maximum allowed number of points, so reload - // the whole track with fewer points (the sampling frequency will be - // lower). - loadSelectedTrack(); - return; - } - // Keep a copy of selectedTrackId, because it can change asynchronously. - long currentSelectedTrackId = selectedTrackId; - long lastStoredLocationId = - providerUtils.getLastLocationId(currentSelectedTrackId); - int samplingFrequency = -1; - Location location = new Location(""); - while (currentSelectedTrackId == selectedTrackId) { - Cursor cursor = null; - try { - cursor = providerUtils.getLocationsCursor(currentSelectedTrackId, - lastSeenLocationId + 1, TRACKPOINT_BUFFER_SIZE, false); - if (cursor == null || !cursor.moveToFirst()) { - // No (more) data - break; - } - - final int idColumnIdx = cursor.getColumnIndexOrThrow( - TrackPointsColumns._ID); - do { - long locationId = cursor.getLong(idColumnIdx); - lastSeenLocationId = locationId; - if (firstSeenLocationId == -1) { - // This was our first point, keep its ID - firstSeenLocationId = locationId; - } - if (samplingFrequency == -1) { - // Now we already have at least one point, calculate the sampling - // frequency - long numTotalPoints = lastStoredLocationId - firstSeenLocationId; - samplingFrequency = (int) (1 + numTotalPoints - / MyTracksConstants.TARGET_DISPLAYED_TRACK_POINTS); - // TODO: This shouldn't happen after adding currentSelectedTrackId, - // but just to be safe until we have 100% confidence. - if (samplingFrequency <= 0) { - Log.w(MyTracksConstants.TAG, - "readAllNewTrackPoints: samplingFreq <= 0, numTotalPoints = " - + numTotalPoints + ", trackId = " + currentSelectedTrackId); - samplingFrequency = 1; - } - } - - providerUtils.fillLocation(cursor, location); - - // Include a point if it fits one of the following criteria: - // - Has the mod for the sampling frequency (includes first point). - // - Is the last point and we are not recording this track. - // - The point is a segment split - if (numPoints % samplingFrequency == 0 || - (!isRecordingSelected() && locationId == lastStoredLocationId) || - !MyTracksUtils.isValidLocation(location)) { - // No need to allocate a new location (we can safely reuse the existing). - mapOverlay.addLocation(location); - } - - numPoints++; - } while (cursor.moveToNext() && - currentSelectedTrackId == selectedTrackId); - } finally { - if (cursor != null) { - cursor.close(); - } - } - } - mapView.postInvalidate(); - } -} diff --git a/MyTracks/src/com/google/android/apps/mytracks/SensorStateActivity.java b/MyTracks/src/com/google/android/apps/mytracks/SensorStateActivity.java index fb9559d26..5302ad80b 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/SensorStateActivity.java +++ b/MyTracks/src/com/google/android/apps/mytracks/SensorStateActivity.java @@ -15,7 +15,7 @@ */ package com.google.android.apps.mytracks; -import static com.google.android.apps.mytracks.MyTracksConstants.TAG; +import static com.google.android.apps.mytracks.Constants.TAG; import com.google.android.apps.mytracks.content.Sensor; import com.google.android.apps.mytracks.services.ITrackRecordingService; import com.google.android.apps.mytracks.services.sensors.SensorUtils; @@ -103,7 +103,7 @@ public class SensorStateActivity extends Activity { ITrackRecordingService service = mt == null ? null : mt.getTrackRecordingService(); if (service == null) { - Log.d(MyTracksConstants.TAG, "Could not get track recording service."); + Log.d(Constants.TAG, "Could not get track recording service."); updateSensorState(Sensor.SensorState.NONE); updateSensorData(null); return; @@ -116,9 +116,9 @@ public class SensorStateActivity extends Activity { updateSensorData(sds); } } catch (RemoteException e) { - Log.e(MyTracksConstants.TAG, "Could not read sensor data.", e); + Log.e(Constants.TAG, "Could not read sensor data.", e); } catch (InvalidProtocolBufferException e) { - Log.e(MyTracksConstants.TAG, "Could not read sensor data.", e); + Log.e(Constants.TAG, "Could not read sensor data.", e); } updateSensorData(sds); @@ -126,7 +126,7 @@ public class SensorStateActivity extends Activity { int i = service.getSensorState(); updateSensorState(Sensor.SensorState.valueOf(i)); } catch (RemoteException e) { - Log.e(MyTracksConstants.TAG, "Could not read sensor state.", e); + Log.e(Constants.TAG, "Could not read sensor state.", e); updateSensorState(Sensor.SensorState.NONE); } } diff --git a/MyTracks/src/com/google/android/apps/mytracks/MyTracksSettings.java b/MyTracks/src/com/google/android/apps/mytracks/SettingsActivity.java similarity index 61% rename from MyTracks/src/com/google/android/apps/mytracks/MyTracksSettings.java rename to MyTracks/src/com/google/android/apps/mytracks/SettingsActivity.java index 1c47ec4d9..e3b2084cd 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/MyTracksSettings.java +++ b/MyTracks/src/com/google/android/apps/mytracks/SettingsActivity.java @@ -15,27 +15,38 @@ */ package com.google.android.apps.mytracks; -import java.util.ArrayList; -import java.util.List; +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; -import com.google.android.apps.mytracks.services.StatusAnnouncerFactory; +import com.google.android.apps.mytracks.services.sensors.ant.AntUtils; +import com.google.android.apps.mytracks.services.tasks.StatusAnnouncerFactory; 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. @@ -43,28 +54,14 @@ import android.provider.Settings; * @author Leif Hendrik Wilden * @author Rodrigo Damazio */ -public class MyTracksSettings extends PreferenceActivity { - - public static final String SETTINGS_NAME = "MyTracksSettings"; - - /* - * Default values - keep in sync with those in preferences.xml. - */ - public static final int DEFAULT_AUTO_RESUME_TRACK_TIMEOUT = 10; // In min. - public static final int DEFAULT_ANNOUNCEMENT_FREQUENCY = -1; - public static final int DEFAULT_MAX_RECORDING_DISTANCE = 200; - public static final int DEFAULT_MIN_RECORDING_DISTANCE = 5; - public static final int DEFAULT_MIN_RECORDING_INTERVAL = 0; - public static final int DEFAULT_MIN_REQUIRED_ACCURACY = 200; - public static final int DEFAULT_SPLIT_FREQUENCY = 0; +public class SettingsActivity extends PreferenceActivity { private BackupPreferencesListener backupListener; - private SharedPreferences preferences; /** Called when the activity is first created. */ @Override - public void onCreate(Bundle icicle) { + protected void onCreate(Bundle icicle) { super.onCreate(icicle); // The volume we want to control is the Text-To-Speech volume @@ -75,7 +72,7 @@ public class MyTracksSettings extends PreferenceActivity { // Tell it where to read/write preferences PreferenceManager preferenceManager = getPreferenceManager(); - preferenceManager.setSharedPreferencesName(SETTINGS_NAME); + preferenceManager.setSharedPreferencesName(Constants.SETTINGS_NAME); preferenceManager.setSharedPreferencesMode(0); // Set up automatic preferences backup @@ -103,6 +100,31 @@ public class MyTracksSettings extends PreferenceActivity { }); updatePreferenceUnits(metricUnitsPreference.isChecked()); + customizeSensorOptionsPreferences(); + + // Disable TTS announcement preference if not available + if (!apiFeatures.hasTextToSpeech()) { + IntegerListPreference announcementFrequency = + (IntegerListPreference) findPreference( + getString(R.string.announcement_frequency_key)); + announcementFrequency.setEnabled(false); + announcementFrequency.setValue("-1"); + 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() { ListPreference sensorTypePreference = (ListPreference) findPreference(getString(R.string.sensor_type_key)); sensorTypePreference.setOnPreferenceChangeListener( @@ -115,16 +137,37 @@ public class MyTracksSettings extends PreferenceActivity { } }); updateSensorSettings(sensorTypePreference.getValue()); - - // Disable TTS announcement preference if not available - if (!apiFeatures.hasTextToSpeech()) { - IntegerListPreference announcementFrequency = - (IntegerListPreference) findPreference( - getString(R.string.announcement_frequency_key)); - announcementFrequency.setEnabled(false); - announcementFrequency.setValue("-1"); - announcementFrequency.setSummary( - R.string.settings_not_available_summary); + + if (!AntUtils.hasAntSupport(this)) { + // The sensor options screen has a few ANT-specific options which we + // need to remove. First, we need to remove the ANT sensor types. + // Second, we need to remove the ANT unpairing options. + + Set toRemove = new HashSet(); + + String[] antValues = getResources().getStringArray(R.array.ant_sensor_type_values); + for (String antValue : antValues) { + toRemove.add(sensorTypePreference.findIndexOfValue(antValue)); + } + + CharSequence[] entries = sensorTypePreference.getEntries(); + CharSequence[] entryValues = sensorTypePreference.getEntryValues(); + + CharSequence[] filteredEntries = new CharSequence[entries.length - toRemove.size()]; + CharSequence[] filteredEntryValues = new CharSequence[filteredEntries.length]; + for (int i = 0, last = 0; i < entries.length; i++) { + if (!toRemove.contains(i)) { + filteredEntries[last] = entries[i]; + filteredEntryValues[last++] = entryValues[i]; + } + } + + sensorTypePreference.setEntries(filteredEntries); + sensorTypePreference.setEntryValues(filteredEntryValues); + + PreferenceScreen sensorOptionsScreen = + (PreferenceScreen) findPreference(getString(R.string.sensor_options_key)); + sensorOptionsScreen.removePreference(findPreference(getString(R.string.ant_options_key))); } } @@ -137,19 +180,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( @@ -157,7 +205,7 @@ public class MyTracksSettings extends PreferenceActivity { @Override public boolean onPreferenceClick(Preference preference) { BackupActivityHelper backupHelper = - new BackupActivityHelper(MyTracksSettings.this); + new BackupActivityHelper(SettingsActivity.this); backupHelper.writeBackup(); return true; } @@ -167,7 +215,7 @@ public class MyTracksSettings extends PreferenceActivity { @Override public boolean onPreferenceClick(Preference preference) { BackupActivityHelper backupHelper = - new BackupActivityHelper(MyTracksSettings.this); + new BackupActivityHelper(SettingsActivity.this); backupHelper.restoreBackup(); return true; } @@ -192,10 +240,14 @@ public class MyTracksSettings extends PreferenceActivity { // Update the ANT+ sensors. // TODO: Only enable on phones that have ANT+. - findPreference(getString(R.string.ant_heart_rate_sensor_id_key)) - .setEnabled(getString(R.string.ant_sensor_type).equals(sensorType)); - findPreference(getString(R.string.ant_srm_bridge_sensor_id_key)) - .setEnabled(getString(R.string.srm_ant_bridge_sensor_type).equals(sensorType)); + Preference antHrm = findPreference(getString(R.string.ant_heart_rate_sensor_id_key)); + Preference antSrm = findPreference(getString(R.string.ant_srm_bridge_sensor_id_key)); + if (antHrm != null && antSrm != null) { + antHrm + .setEnabled(getString(R.string.ant_sensor_type).equals(sensorType)); + antSrm + .setEnabled(getString(R.string.srm_ant_bridge_sensor_type).equals(sensorType)); + } } /** @@ -214,10 +266,7 @@ public class MyTracksSettings extends PreferenceActivity { final ListPreference minRequiredAccuracy = (ListPreference) findPreference( getString(R.string.min_required_accuracy_key)); - final ListPreference splitFrequency = - (ListPreference) findPreference( - getString(R.string.split_frequency_key)); - + minRecordingDistance.setEntries(isMetric ? R.array.min_recording_distance_options : R.array.min_recording_distance_options_ft); @@ -227,11 +276,20 @@ public class MyTracksSettings extends PreferenceActivity { minRequiredAccuracy.setEntries(isMetric ? R.array.min_required_accuracy_options : R.array.min_required_accuracy_options_ft); - splitFrequency.setEntries(isMetric - ? R.array.split_frequency_options - : R.array.split_frequency_options_ft); + setTaskOptions(isMetric, R.string.announcement_frequency_key); + setTaskOptions(isMetric, R.string.split_frequency_key); } + /** + * Set the user visible options for a periodic task. + */ + private void setTaskOptions(boolean isMetric, int listId) { + final ListPreference taskFrequency = + (ListPreference) findPreference(getString(listId)); + taskFrequency.setEntries(isMetric + ? R.array.task_frequency_options + : R.array.task_frequency_options_ft); + } /** * Configures preference actions related to bluetooth. @@ -271,4 +329,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( + SettingsActivity.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/StatsActivity.java b/MyTracks/src/com/google/android/apps/mytracks/StatsActivity.java index d185c12db..45ac98624 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/StatsActivity.java +++ b/MyTracks/src/com/google/android/apps/mytracks/StatsActivity.java @@ -15,26 +15,18 @@ */ package com.google.android.apps.mytracks; -import com.google.android.apps.mytracks.content.MyTracksProviderUtils; -import com.google.android.apps.mytracks.content.MyTracksProviderUtilsImpl; +import static com.google.android.apps.mytracks.Constants.TAG; + +import com.google.android.apps.mytracks.TrackDataHub.ListenerDataType; import com.google.android.apps.mytracks.content.Track; -import com.google.android.apps.mytracks.content.TracksColumns; -import com.google.android.apps.mytracks.content.WaypointsColumns; -import com.google.android.apps.mytracks.services.StatusAnnouncerFactory; +import com.google.android.apps.mytracks.content.Waypoint; +import com.google.android.apps.mytracks.services.tasks.StatusAnnouncerFactory; import com.google.android.apps.mytracks.util.ApiFeatures; import com.google.android.maps.mytracks.R; import android.app.Activity; -import android.content.Context; -import android.content.SharedPreferences; -import android.content.SharedPreferences.OnSharedPreferenceChangeListener; -import android.database.ContentObserver; import android.location.Location; -import android.location.LocationListener; -import android.location.LocationManager; -import android.location.LocationProvider; import android.os.Bundle; -import android.os.Handler; import android.util.DisplayMetrics; import android.util.Log; import android.view.Menu; @@ -42,66 +34,41 @@ import android.view.MenuItem; import android.view.Window; import android.widget.ScrollView; import android.widget.TextView; -import android.widget.Toast; + +import java.util.EnumSet; /** * An activity that displays track statistics to the user. * * @author Sandor Dornbush + * @author Rodrigo Damazio */ -public class StatsActivity extends Activity - implements OnSharedPreferenceChangeListener { - +public class StatsActivity extends Activity implements TrackDataListener { private StatsUtilities utils; private UIUpdateThread thread; - private ContentObserver observer; - - /** - * The id of the currently selected track. - */ - private long selectedTrackId = -1; - - /** - * The id of the currently recording track. - */ - private long recordingTrackId = -1; - /** * The start time of the selected track. */ private long startTime = -1; - /** - * True if distances should be displayed in metric units (from shared - * preferences). - */ - private boolean metricUnits = true; - - /** - * True if pace should be displayed as dist/time (from shared preferences). - */ - private boolean displaySpeed = true; - - /** - * true if activity has resumed and is on top - */ - private boolean activityOnTop = false; - /** * If true, the statistics for the current segment are shown, otherwise * for the full track. */ private boolean showCurrentSegment = false; - private MyTracksProviderUtils providerUtils; + private TrackDataHub dataHub; /** * A runnable for posting to the UI thread. Will update the total time field. */ private final Runnable updateResults = new Runnable() { public void run() { - updateTotalTime(); + if (dataHub.isRecordingSelected()) { + utils.setTime(R.id.total_time_register, + System.currentTimeMillis() - startTime); + } } }; @@ -112,24 +79,22 @@ public class StatsActivity extends Activity public UIUpdateThread() { super(); - Log.i(MyTracksConstants.TAG, "Created UI update thread"); + Log.i(TAG, "Created UI update thread"); } @Override public void run() { - Log.i(MyTracksConstants.TAG, "Started UI update thread"); + Log.i(TAG, "Started UI update thread"); while (MyTracks.getInstance().isRecording()) { - long sleeptime = 1000; runOnUiThread(updateResults); try { - Thread.sleep(sleeptime); + Thread.sleep(1000L); } catch (InterruptedException e) { - Log.w(MyTracksConstants.TAG, - "StatsActivity: Caught exception on sleep.", e); + Log.w(TAG, "StatsActivity: Caught exception on sleep.", e); break; } } - Log.w(MyTracksConstants.TAG, "UIUpdateThread finished."); + Log.w(TAG, "UIUpdateThread finished."); } } @@ -138,9 +103,9 @@ public class StatsActivity extends Activity public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + dataHub = MyTracks.getInstance().getDataHub(); utils = new StatsUtilities(this); - providerUtils = new MyTracksProviderUtilsImpl(getContentResolver()); - + // The volume we want to control is the Text-To-Speech volume int volumeStream = new StatusAnnouncerFactory(ApiFeatures.getInstance()).getVolumeStream(); @@ -151,40 +116,10 @@ public class StatsActivity extends Activity setContentView(R.layout.stats); - Handler contentHandler = new Handler(); - observer = new ContentObserver(contentHandler) { - @Override - public void onChange(boolean selfChange) { - Log.d(MyTracksConstants.TAG, "StatsActivity: ContentObserver.onChange"); - restoreStats(); - super.onChange(selfChange); - } - }; - ScrollView sv = ((ScrollView) findViewById(R.id.scrolly)); sv.setScrollBarStyle(ScrollView.SCROLLBARS_OUTSIDE_INSET); - SharedPreferences preferences = - getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0); - if (preferences != null) { - selectedTrackId = preferences.getLong( - getString(R.string.selected_track_key), -1); - recordingTrackId = preferences.getLong( - getString(R.string.recording_track_key), -1); - metricUnits = preferences.getBoolean( - getString(R.string.metric_units_key), true); - displaySpeed = - preferences.getBoolean(getString(R.string.report_speed_key), true); - checkLiveTrack(); - restoreStats(); - showUnknownLocation(); - preferences.registerOnSharedPreferenceChangeListener(this); - } - utils.setMetricUnits(metricUnits); - utils.setReportSpeed(displaySpeed); - utils.updateUnits(); - utils.setSpeedLabel(R.id.speed_label, R.string.speed, R.string.pace_label); - utils.setSpeedLabels(); + showUnknownLocation(); DisplayMetrics metrics = new DisplayMetrics(); getWindowManager().getDefaultDisplay().getMetrics(metrics); @@ -194,88 +129,67 @@ public class StatsActivity extends Activity } @Override - protected void onPause() { - unregisterLocationListener(); + protected void onStart() { + dataHub.registerTrackDataListener(this, EnumSet.of( + ListenerDataType.SELECTED_TRACK_CHANGED, + ListenerDataType.TRACK_UPDATES, + ListenerDataType.LOCATION_UPDATES, + ListenerDataType.DISPLAY_PREFERENCES)); + + super.onStart(); + } + + @Override + protected void onStop() { + dataHub.unregisterTrackDataListener(this); + if (thread != null) { thread.interrupt(); thread = null; } - getContentResolver().unregisterContentObserver(observer); - activityOnTop = false; - super.onPause(); + + super.onStop(); } @Override - protected void onResume() { - activityOnTop = true; - checkLiveTrack(); - restoreStats(); - showUnknownLocation(); - super.onResume(); + public boolean onUnitsChanged(boolean metric) { + utils.setMetricUnits(metric); + updateLabels(); + + return true; // Reload data } @Override - public void onSharedPreferenceChanged( - final SharedPreferences sharedPreferences, final String key) { - Log.d(MyTracksConstants.TAG, - "StatsActivity: onSharedPreferences changed " + key); - if (key != null) { - runOnUiThread(new Runnable() { - @Override - public void run() { - if (key.equals(getString(R.string.selected_track_key))) { - selectedTrackId = - sharedPreferences.getLong( - getString(R.string.selected_track_key), - -1); - checkLiveTrack(); - restoreStats(); - showUnknownLocation(); - } else if (key.equals(getString(R.string.recording_track_key))) { - recordingTrackId = - sharedPreferences.getLong( - getString(R.string.recording_track_key), - -1); - checkLiveTrack(); - restoreStats(); - showUnknownLocation(); - } else if (key.equals(getString(R.string.metric_units_key))) { - metricUnits = - sharedPreferences.getBoolean( - getString(R.string.metric_units_key), true); - utils.setMetricUnits(metricUnits); - utils.updateUnits(); - restoreStats(); - } else if (key.equals(getString(R.string.report_speed_key))) { - displaySpeed = - sharedPreferences.getBoolean( - getString(R.string.report_speed_key), - true); - utils.setReportSpeed(displaySpeed); - utils.updateUnits(); - utils.setSpeedLabel( - R.id.speed_label, R.string.speed, R.string.pace_label); - Log.w(MyTracksConstants.TAG, "Setting speed labels"); - utils.setSpeedLabels(); - restoreStats(); - } - } - }); - } + public boolean onReportSpeedChanged(boolean displaySpeed) { + utils.setReportSpeed(displaySpeed); + updateLabels(); + + return true; // Reload data + } + + private void updateLabels() { + runOnUiThread(new Runnable() { + @Override + public void run() { + utils.updateUnits(); + utils.setSpeedLabel(R.id.speed_label, R.string.speed, R.string.pace_label); + utils.setSpeedLabels(); + } + }); } @Override public boolean onCreateOptionsMenu(Menu menu) { super.onCreateOptionsMenu(menu); MenuItem currentSegment = menu.add(0, - MyTracksConstants.MENU_CURRENT_SEGMENT, 0, R.string.current_segment); + Constants.MENU_CURRENT_SEGMENT, 0, R.string.current_segment); currentSegment.setIcon(R.drawable.ic_menu_lastsegment); return true; } @Override public boolean onPrepareOptionsMenu(Menu menu) { - MenuItem item = menu.findItem(MyTracksConstants.MENU_CURRENT_SEGMENT); + MenuItem item = menu.findItem(Constants.MENU_CURRENT_SEGMENT); if (item != null) { item.setTitle(showCurrentSegment ? getString(R.string.current_track) @@ -287,139 +201,14 @@ public class StatsActivity extends Activity @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { - case MyTracksConstants.MENU_CURRENT_SEGMENT: + case Constants.MENU_CURRENT_SEGMENT: showCurrentSegment = !showCurrentSegment; - restoreStats(); + // TODO: Re-read only the data that interests us return true; } return super.onOptionsItemSelected(item); } - private final LocationListener locationListener = new LocationListener() { - @Override - public void onLocationChanged(Location l) { - if (selectedTrackIsRecording()) { - showLocation(l); - } - } - - @Override - public void onProviderDisabled(String provider) { - // Do nothing - } - - @Override - public void onProviderEnabled(String provider) { - // Do nothing - } - - @Override - public void onStatusChanged(String provider, int status, Bundle extras) { - // Do nothing - } - }; - - /** - * Registers to receive location updates from the GPS location provider. - */ - private void registerLocationListener() { - LocationManager locationManager = - (LocationManager) getSystemService(Context.LOCATION_SERVICE); - if (locationManager != null) { - LocationProvider gpsProvider = - locationManager.getProvider(MyTracksConstants.GPS_PROVIDER); - if (gpsProvider == null) { - Toast.makeText(this, getString(R.string.error_no_gps_location_provider), - Toast.LENGTH_LONG).show(); - return; - } else { - Log.d(MyTracksConstants.TAG, "StatsActivity: Using location provider " - + gpsProvider.getName()); - } - locationManager.requestLocationUpdates(gpsProvider.getName(), - 0/*minTime*/, 0/*minDist*/, locationListener); - } - } - - /** - * Unregisters all location listener. - */ - private void unregisterLocationListener() { - LocationManager locationManager = - (LocationManager) getSystemService(Context.LOCATION_SERVICE); - if (locationManager != null) { - locationManager.removeUpdates(locationListener); - } - } - - /** - * @return true if the selected track is the currently recording track - */ - private boolean selectedTrackIsRecording() { - return MyTracks.getInstance().isRecording() - && selectedTrackId == recordingTrackId; - } - - /** - * Reads values for selected tracks from provider and update the UI. - */ - private void restoreStats() { - if (selectedTrackId < 0) { - utils.setAllToUnknown(); - return; - } - - Track track = providerUtils.getTrack(selectedTrackId); - if (track == null || track.getStatistics() == null) { - utils.setAllToUnknown(); - return; - } - - startTime = track.getStatistics().getStartTime(); - if (!selectedTrackIsRecording()) { - utils.setTime(R.id.total_time_register, - track.getStatistics().getTotalTime()); - } - utils.setAllStats(track.getStatistics()); - } - - /** - * Checks if this activity needs to update live track data or not. - * If so, make sure that: - * a) a thread keeps updating the total time - * b) a location listener is registered - * c) a content observer is registered - * Otherwise unregister listeners, observers, and kill update thread. - */ - private void checkLiveTrack() { - final boolean isRecording = selectedTrackIsRecording(); - final boolean startThread = - (thread == null) && isRecording && activityOnTop; - final boolean killThread = - (thread != null) && (!isRecording || !activityOnTop); - if (startThread) { - thread = new UIUpdateThread(); - thread.start(); - getContentResolver().registerContentObserver( - TracksColumns.CONTENT_URI, false, observer); - getContentResolver().registerContentObserver( - WaypointsColumns.CONTENT_URI, false, observer); - registerLocationListener(); - } else if (killThread) { - thread.interrupt(); - thread = null; - getContentResolver().unregisterContentObserver(observer); - unregisterLocationListener(); - } - } - - public void updateTotalTime() { - if (selectedTrackIsRecording()) { - utils.setTime(R.id.total_time_register, - System.currentTimeMillis() - startTime); - } - } - /** * Updates the given location fields (latitude, longitude, altitude) and all * other fields. @@ -439,4 +228,112 @@ public class StatsActivity extends Activity utils.setUnknown(R.id.longitude_register); utils.setUnknown(R.id.speed_register); } + + @Override + public void onSelectedTrackChanged(Track track, boolean isRecording) { + /* + * Checks if this activity needs to update live track data or not. + * If so, make sure that: + * a) a thread keeps updating the total time + * b) a location listener is registered + * c) a content observer is registered + * Otherwise unregister listeners, observers, and kill update thread. + */ + final boolean startThread = (thread == null) && isRecording; + final boolean killThread = (thread != null) && (!isRecording); + if (startThread) { + thread = new UIUpdateThread(); + thread.start(); + } else if (killThread) { + thread.interrupt(); + thread = null; + } + + if (track == null || track.getStatistics() == null) { + runOnUiThread(new Runnable() { + @Override + public void run() { + utils.setAllToUnknown(); + } + }); + } + } + + @Override + public void onCurrentLocationChanged(Location loc) { + if (dataHub.isRecordingSelected()) { + showLocation(loc); + } + } + + @Override + public void onCurrentHeadingChanged(double heading) { + // We don't care. + } + + @Override + public void onProviderStateChange(ProviderState state) { + switch (state) { + case DISABLED: + case NO_FIX: + showUnknownLocation(); + break; + } + } + + @Override + public void onTrackUpdated(final Track track) { + runOnUiThread(new Runnable() { + @Override + public void run() { + startTime = track.getStatistics().getStartTime(); + if (!dataHub.isRecordingSelected()) { + utils.setTime(R.id.total_time_register, + track.getStatistics().getTotalTime()); + showUnknownLocation(); + } + utils.setAllStats(track.getStatistics()); + } + }); + } + + @Override + public void clearWaypoints() { + // We don't care. + } + + @Override + public void onNewWaypoint(Waypoint wpt) { + // We don't care. + } + + @Override + public void onNewWaypointsDone() { + // We don't care. + } + + @Override + public void clearTrackPoints() { + // We don't care. + } + + @Override + public void onNewTrackPoint(Location loc) { + // We don't care. + } + + @Override + public void onSegmentSplit() { + // We don't care. + } + + @Override + public void onSampledOutTrackPoint(Location loc) { + // We don't care. + } + + @Override + public void onNewTrackPointsDone() { + // We don't care. + } } diff --git a/MyTracks/src/com/google/android/apps/mytracks/StatsUtilities.java b/MyTracks/src/com/google/android/apps/mytracks/StatsUtilities.java index c37ecab65..f6327dace 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/StatsUtilities.java +++ b/MyTracks/src/com/google/android/apps/mytracks/StatsUtilities.java @@ -235,7 +235,7 @@ public class StatsUtilities { } public void setSpeedLabel(int id, int speedString, int paceString) { - Log.w(MyTracksConstants.TAG, "Setting view " + id + + Log.w(Constants.TAG, "Setting view " + id + " to " + reportSpeed + " speed: " + speedString + " pace: " + paceString); @@ -243,7 +243,7 @@ public class StatsUtilities { if (tv != null) { tv.setText(reportSpeed ? speedString : paceString); } else { - Log.w(MyTracksConstants.TAG, "Could not find id: " + id); + Log.w(Constants.TAG, "Could not find id: " + id); } } diff --git a/MyTracks/src/com/google/android/apps/mytracks/TrackDataHub.java b/MyTracks/src/com/google/android/apps/mytracks/TrackDataHub.java new file mode 100644 index 000000000..eac3f3df5 --- /dev/null +++ b/MyTracks/src/com/google/android/apps/mytracks/TrackDataHub.java @@ -0,0 +1,1221 @@ +/* + * 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; + +import static com.google.android.apps.mytracks.Constants.MAX_LOCATION_AGE_MS; +import static com.google.android.apps.mytracks.Constants.TAG; + +import com.google.android.apps.mytracks.TrackDataListener.ProviderState; +import com.google.android.apps.mytracks.content.MyTracksProviderUtils; +import com.google.android.apps.mytracks.content.MyTracksProviderUtils.LocationIterator; +import com.google.android.apps.mytracks.content.Track; +import com.google.android.apps.mytracks.content.TrackPointsColumns; +import com.google.android.apps.mytracks.content.TracksColumns; +import com.google.android.apps.mytracks.content.Waypoint; +import com.google.android.apps.mytracks.content.WaypointsColumns; +import com.google.android.apps.mytracks.util.ApiFeatures; +import com.google.android.apps.mytracks.util.LocationUtils; +import com.google.android.maps.mytracks.R; + +import android.content.Context; +import android.content.SharedPreferences; +import android.content.SharedPreferences.OnSharedPreferenceChangeListener; +import android.database.ContentObserver; +import android.database.Cursor; +import android.hardware.GeomagneticField; +import android.hardware.Sensor; +import android.hardware.SensorEvent; +import android.hardware.SensorEventListener; +import android.hardware.SensorManager; +import android.location.Location; +import android.location.LocationListener; +import android.location.LocationManager; +import android.location.LocationProvider; +import android.os.Bundle; +import android.os.Handler; +import android.os.HandlerThread; +import android.util.Log; +import android.widget.Toast; + +import java.util.Collections; +import java.util.EnumMap; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; + +/** + * Track data hub, which receives data (both live and recorded) from many + * different sources and distributes it to those interested after some standard + * processing. + * + * @author Rodrigo Damazio + */ +public class TrackDataHub { + + // Preference keys + private final String SELECTED_TRACK_KEY; + private final String RECORDING_TRACK_KEY; + private final String MIN_REQUIRED_ACCURACY_KEY; + private final String METRIC_UNITS_KEY; + private final String SPEED_REPORTING_KEY; + + /** Types of data that we can expose. */ + public static enum ListenerDataType { + /** Listen to when the selected track changes. */ + SELECTED_TRACK_CHANGED, + + /** Listen to when the tracks change. */ + TRACK_UPDATES, + + /** Listen to when the waypoints change. */ + WAYPOINT_UPDATES, + + /** Listen to when the current track points change. */ + POINT_UPDATES, + + /** + * Listen to sampled-out points. + * Listening to this without listening to {@link #SAMPLED_POINT_UPDATES} + * makes no sense and may yield unexpected results. + */ + SAMPLED_OUT_POINT_UPDATES, + + /** Listen to updates to the current location. */ + LOCATION_UPDATES, + + /** Listen to updates to the current heading. */ + COMPASS_UPDATES, + + /** Listens to changes in display preferences. */ + DISPLAY_PREFERENCES; + } + + // Application services + private final Context context; + private final TrackDataSources dataSources; + private final MyTracksProviderUtils providerUtils; + private final SharedPreferences preferences; + + // Internal listeners (to receive data from the system) + private final ContentObserver pointObserver; + private final ContentObserver waypointObserver; + private final ContentObserver trackObserver; + private final LocationListener locationListener; + private final OnSharedPreferenceChangeListener preferenceListener; + private final SensorEventListener compassListener; + + /** Set of internal listeners which are already registered. */ + private final Set registeredInternalListeners = + EnumSet.noneOf(ListenerDataType.class); + + /** Map of external listener to its registration details. */ + private final Map registeredListeners = + new HashMap(); + + /** Map of data type to external listeners interested in it. */ + private final Map> listenerSetsPerType = + new EnumMap>(ListenerDataType.class); + + // Get content notifications on the main thread, send listener callbacks in another. + // This ensures listener calls are serialized. + private final HandlerThread listenerHandlerThread; + private final Handler listenerHandler; + + /** Whether we've been started. */ + private boolean started; + + // Cached preference values + private int minRequiredAccuracy; + private boolean useMetricUnits; + private boolean reportSpeed; + + // Cached sensor readings + private float declination; + private long lastDeclinationUpdate; + private float lastSeenMagneticHeading; + + // Cached GPS readings + private Location lastSeenLocation; + private boolean hasProviderEnabled; + private boolean hasFix; + private boolean hasGoodFix; + + // Transient state about the selected track + private long selectedTrackId; + private long recordingTrackId; + private long firstSeenLocationId; + private long lastSeenLocationId; + private int numLoadedPoints; + + /** Internal representation of a listener's registration. */ + private static class ListenerRegistration { + final TrackDataListener listener; + final EnumSet types; + // TODO: Add the last-notified point ID here, to allow pausing/resuming. + + public ListenerRegistration(TrackDataListener listener, + EnumSet types) { + this.listener = listener; + this.types = types; + } + + public boolean isInterestedIn(ListenerDataType type) { + return types.contains(type); + } + } + + /** Callback for when the tracks table is updated. */ + private class TrackObserverCallback implements Runnable { + @Override + public void run() { + notifyTrackUpdated(getListenersFor(ListenerDataType.TRACK_UPDATES)); + } + } + + /** Callback for when the waypoints table is updated. */ + private class WaypointObserverCallback implements Runnable { + @Override + public void run() { + notifyWaypointUpdated(getListenersFor(ListenerDataType.WAYPOINT_UPDATES)); + } + } + + /** Callback for when the points table is updated. */ + private class PointObserverCallback implements Runnable { + @Override + public void run() { + notifyPointsUpdated(true, + getListenersFor(ListenerDataType.POINT_UPDATES), + getListenersFor(ListenerDataType.SAMPLED_OUT_POINT_UPDATES)); + } + } + + /** Listener for when preferences change. */ + private class HubSharedPreferenceListener implements OnSharedPreferenceChangeListener { + @Override + public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { + notifyPreferenceChanged(key); + } + } + + /** + * Generic content observer which will call a given {@link Runnable} in the + * given handler if the content has changed and we're recording the selected + * track. + */ + private class TrackContentObserver extends ContentObserver { + private final Runnable callback; + + public TrackContentObserver(Handler contentHandler, Runnable callback) { + super(contentHandler); + + this.callback = callback; + } + + @Override + public void onChange(boolean selfChange) { + Log.v(TAG, "TrackContentObserver.onChange"); + + // We want to filter only updates from the selected track, but since + // we can't see what the update is, we'll let two cases pass: + // 1 - The point(s) was(ere) changed because it's a recording track + // (and thus we care about it if the recording is the selected one) + // 2 - The point(s) was(ere) changed because it's syncing a track + // (and thus there will be no new points for the selected one) + if (!isRecordingSelected()) { + return; + } + + // Update can potentially be lengthy, put it in its own thread: + runInListenerThread(callback); + } + } + + /** Listener for the current location (independent from track data). */ + private class CurrentLocationListener implements + LocationListener { + @Override + public void onStatusChanged(String provider, int status, Bundle extras) { + if (!LocationManager.GPS_PROVIDER.equals(provider)) return; + + hasProviderEnabled = (status == LocationProvider.AVAILABLE); + notifyFixType(); + } + + @Override + public void onProviderEnabled(String provider) { + if (!LocationManager.GPS_PROVIDER.equals(provider)) return; + + hasProviderEnabled = true; + notifyFixType(); + } + + @Override + public void onProviderDisabled(String provider) { + if (!LocationManager.GPS_PROVIDER.equals(provider)) return; + + hasProviderEnabled = false; + notifyFixType(); + } + + @Override + public void onLocationChanged(Location location) { + notifyLocationChanged(location, + getListenersFor(ListenerDataType.LOCATION_UPDATES)); + } + } + + /** Listener for compass readings. */ + private class CompassListener implements + SensorEventListener { + @Override + public void onSensorChanged(SensorEvent event) { + lastSeenMagneticHeading = event.values[0]; + maybeUpdateDeclination(); + notifyHeadingChanged(getListenersFor(ListenerDataType.COMPASS_UPDATES)); + } + + @Override + public void onAccuracyChanged(Sensor sensor, int accuracy) { + // Do nothing + } + } + + /** + * Default constructor. + */ + public TrackDataHub(Context ctx, SharedPreferences preferences, + MyTracksProviderUtils providerUtils) { + this(ctx, new TrackDataSourcesImpl(ctx, preferences), preferences, providerUtils); + } + + /** + * Injection constructor. + */ + // @VisibleForTesting + TrackDataHub(Context ctx, TrackDataSources dataSources, SharedPreferences preferences, + MyTracksProviderUtils providerUtils) { + this.context = ctx; + this.dataSources = dataSources; + this.preferences = preferences; + this.providerUtils = providerUtils; + + SELECTED_TRACK_KEY = context.getString(R.string.selected_track_key); + RECORDING_TRACK_KEY = context.getString(R.string.recording_track_key); + MIN_REQUIRED_ACCURACY_KEY = context.getString(R.string.min_required_accuracy_key); + METRIC_UNITS_KEY = context.getString(R.string.metric_units_key); + SPEED_REPORTING_KEY = context.getString(R.string.report_speed_key); + + // Create sets for all data type at startup. + for (ListenerDataType type : ListenerDataType.values()) { + listenerSetsPerType.put(type, new LinkedHashSet()); + } + + listenerHandlerThread = new HandlerThread("trackDataContentThread"); + listenerHandlerThread.start(); + listenerHandler = new Handler(listenerHandlerThread.getLooper()); + + preferenceListener = new HubSharedPreferenceListener(); + + Handler contentHandler = new Handler(); + pointObserver = new TrackContentObserver(contentHandler, new PointObserverCallback()); + waypointObserver = new TrackContentObserver(contentHandler, new WaypointObserverCallback()); + trackObserver = new TrackContentObserver(contentHandler, new TrackObserverCallback()); + + compassListener = new CompassListener(); + locationListener = new CurrentLocationListener(); + } + + /** + * Starts listening to data sources and reporting the data to external + * listeners. + */ + public void start() { + Log.i(TAG, "TrackDataHub.start"); + if (started) { + Log.w(TAG, "Already started, ignoring"); + return; + } + started = true; + + dataSources.registerOnSharedPreferenceChangeListener(preferenceListener); + loadSharedPreferences(); + + // This may or may not register internal listeners, depending on whether + // we already had external listeners. + updateInternalListeners(); + + // If there were listeners already registered, make sure they become up-to-date. + // TODO: This should really only send new data (in a start-stop-start cycle). + loadDataForAllListeners(); + } + + private void loadSharedPreferences() { + selectedTrackId = preferences.getLong(SELECTED_TRACK_KEY, -1); + recordingTrackId = preferences.getLong(RECORDING_TRACK_KEY, -1); + useMetricUnits = preferences.getBoolean(METRIC_UNITS_KEY, true); + reportSpeed = preferences.getBoolean(SPEED_REPORTING_KEY, true); + minRequiredAccuracy = preferences.getInt(MIN_REQUIRED_ACCURACY_KEY, + Constants.DEFAULT_MIN_REQUIRED_ACCURACY); + } + + /** + * Stops listening to data sources and reporting the data to external + * listeners. + */ + public void stop() { + Log.i(TAG, "TrackDataHub.stop"); + if (!started) { + Log.w(TAG, "Not started, ignoring"); + return; + } + + dataSources.unregisterOnSharedPreferenceChangeListener(preferenceListener); + + // Unregister internal listeners even if there are external listeners registered. + unregisterInternalListeners(); + + started = false; + } + + /** Permanently invalidates and throws away all resources used by this class. */ + public void destroy() { + if (started) { + throw new IllegalStateException("Can only destroy the data hub after it's been stopped"); + } + + ApiFeatures.getInstance().getApiPlatformAdapter() + .stopHandlerThread(listenerHandlerThread); + } + + /** Updates known magnetic declination if needed. */ + private void maybeUpdateDeclination() { + if (lastSeenLocation == null) { + // We still don't know where we are. + return; + } + + // Update the declination every hour + long now = System.currentTimeMillis(); + if (now - lastDeclinationUpdate < 60 * 60 * 1000) { + return; + } + + lastDeclinationUpdate = now; + long timestamp = lastSeenLocation.getTime(); + if (timestamp == 0) { + // Hack for Samsung phones which don't populate the time field + timestamp = now; + } + + declination = getDeclinationFor(lastSeenLocation, timestamp); + Log.i(TAG, "Updated magnetic declination to " + declination); + } + + // @VisibleForTesting + protected float getDeclinationFor(Location location, long timestamp) { + GeomagneticField field = new GeomagneticField( + (float) location.getLatitude(), + (float) location.getLongitude(), + (float) location.getAltitude(), + timestamp); + return field.getDeclination(); + } + + /** + * Forces the current location to be updated and reported to all listeners. + * The reported location may be from the network provider if the GPS provider + * is not available or doesn't have a fix. + */ + public void forceUpdateLocation() { + Log.i(TAG, "Forcing location update"); + + Location loc = dataSources.getLastKnownLocation(); + if (loc != null) { + notifyLocationChanged(loc, getListenersFor(ListenerDataType.LOCATION_UPDATES)); + } + } + + + /** Returns the ID of the currently-selected track. */ + public long getSelectedTrackId() { + if (!started) { + loadSharedPreferences(); + } + return selectedTrackId; + } + + /** Returns whether there's a track currently selected. */ + public boolean isATrackSelected() { + return getSelectedTrackId() > 0; + } + + /** Returns whether we're currently recording a track. */ + public boolean isRecording() { + if (!started) { + loadSharedPreferences(); + } + return recordingTrackId > 0; + } + + /** Returns whether the selected track is still being recorded. */ + public boolean isRecordingSelected() { + if (!started) { + loadSharedPreferences(); + } + return recordingTrackId > 0 && recordingTrackId == selectedTrackId; + } + + /** + * Loads the given track and makes it the currently-selected one. + * It is ok to call this method before {@link start}, and in that case + * the data will only be passed to listeners when {@link start} is called. + * + * @param trackId the ID of the track to load + */ + public void loadTrack(long trackId) { + if (trackId == selectedTrackId) { + Log.w(TAG, "Not reloading track, id=" + trackId); + return; + } + + // Save the selection to memory and flash. + ApiFeatures.getInstance().getApiPlatformAdapter().applyPreferenceChanges( + preferences.edit().putLong(SELECTED_TRACK_KEY, trackId)); + selectedTrackId = trackId; + + // Force it to reload data from the beginning. + firstSeenLocationId = -1; + lastSeenLocationId = -1; + numLoadedPoints = 0; + + loadDataForAllListeners(); + } + + /** + * Unloads the currently-selected track. + */ + public void unloadCurrentTrack() { + loadTrack(-1); + } + + /** + * Registers a listener to send data to. + * It is ok to call this method before {@link start}, and in that case + * the data will only be passed to listeners when {@link start} is called. + * + * @param listener the listener to register + * @param dataTypes the type of data that the listener is interested in + */ + public void registerTrackDataListener(final TrackDataListener listener, EnumSet dataTypes) { + Log.d(TAG, "Registered track data listener: " + listener); + ListenerRegistration registration = new ListenerRegistration(listener, dataTypes); + synchronized (registeredListeners) { + if (registeredListeners.get(listener) != null) { + throw new IllegalStateException("Listener already registered"); + } + registeredListeners.put(listener, registration); + + for (ListenerDataType type : dataTypes) { + // This is guaranteed not to be null. + Set typeSet = listenerSetsPerType.get(type); + typeSet.add(listener); + } + + // Don't load any data or start internal listeners if start() hasn't been + // called. When it is called, we'll do both things. + if (!started) return; + + reloadDataForListener(registration); + } + + updateInternalListeners(); + } + + /** + * Unregisters a listener to send data to. + * + * @param listener the listener to unregister + */ + public void unregisterTrackDataListener(TrackDataListener listener) { + Log.d(TAG, "Unregistered track data listener: " + listener); + synchronized (registeredListeners) { + // Remove and keep the corresponding registration. + ListenerRegistration match = registeredListeners.remove(listener); + if (match == null) { + Log.w(TAG, "Tried to unregister listener which is not registered."); + return; + } + + // Remove it from the per-type sets + for (ListenerDataType type : match.types) { + listenerSetsPerType.get(type).remove(listener); + } + + // Don't load any data or start internal listeners if start() hasn't been + // called. When it is called, we'll do both things. + if (!started) return; + } + updateInternalListeners(); + } + + /** Updates the internal (sensor, position, etc) listeners. */ + private void updateInternalListeners() { + synchronized (registeredListeners) { + Set registeredListeners = registeredInternalListeners; + Set neededListeners = EnumSet.noneOf(ListenerDataType.class); + for (ListenerRegistration registration : this.registeredListeners.values()) { + neededListeners.addAll(registration.types); + } + + // Unnecessary = registered - needed + Set unnecessaryListeners = EnumSet.copyOf(registeredListeners); + unnecessaryListeners.removeAll(neededListeners); + + // Missing = needed - registered + Set missingListeners = EnumSet.copyOf(neededListeners); + missingListeners.removeAll(registeredListeners); + + // Remove all unnecessary listeners. + for (ListenerDataType type : unnecessaryListeners) { + switch (type) { + case COMPASS_UPDATES: + dataSources.unregisterSensorListener(compassListener); + break; + case LOCATION_UPDATES: + dataSources.removeLocationUpdates(locationListener); + break; + case POINT_UPDATES: + case SAMPLED_OUT_POINT_UPDATES: + // Special case - don't unregister if the other type is needed. + if (!neededListeners.contains(ListenerDataType.POINT_UPDATES) && + !neededListeners.contains(ListenerDataType.SAMPLED_OUT_POINT_UPDATES)) { + dataSources.unregisterContentObserver(pointObserver); + } + break; + case TRACK_UPDATES: + dataSources.unregisterContentObserver(trackObserver); + break; + case WAYPOINT_UPDATES: + dataSources.unregisterContentObserver(waypointObserver); + break; + } + } + + // Add all missing listeners. + for (ListenerDataType type : missingListeners) { + switch (type) { + case COMPASS_UPDATES: { + // Listen to compass + Sensor compass = dataSources.getSensor(Sensor.TYPE_ORIENTATION); + if (compass != null) { + Log.d(Constants.TAG, "TrackDataHub: Now registering sensor listener."); + dataSources.registerSensorListener(compassListener, compass, SensorManager.SENSOR_DELAY_UI); + } + break; + } + case LOCATION_UPDATES: + dataSources.requestLocationUpdates(locationListener); + break; + case POINT_UPDATES: + case SAMPLED_OUT_POINT_UPDATES: + // Special case - don't register if the other type was already registered. + if (!registeredListeners.contains(ListenerDataType.POINT_UPDATES) && + !registeredListeners.contains(ListenerDataType.SAMPLED_OUT_POINT_UPDATES)) { + dataSources.registerContentObserver( + TrackPointsColumns.CONTENT_URI, false, pointObserver); + } + break; + case TRACK_UPDATES: + dataSources.registerContentObserver(TracksColumns.CONTENT_URI, false, trackObserver); + break; + case WAYPOINT_UPDATES: + dataSources.registerContentObserver( + WaypointsColumns.CONTENT_URI, false, waypointObserver); + break; + } + } + + // Now all needed types are registered. + registeredInternalListeners.clear(); + registeredInternalListeners.addAll(neededListeners); + } // synchronized + } + + /** Unregisters all internal (sensor, position, etc.) listeners. */ + private void unregisterInternalListeners() { + dataSources.removeLocationUpdates(locationListener); + dataSources.unregisterSensorListener(compassListener); + dataSources.unregisterContentObserver(trackObserver); + dataSources.unregisterContentObserver(waypointObserver); + dataSources.unregisterContentObserver(pointObserver); + } + + /** + * Reloads all track data received so far into the specified listeners. + */ + public void reloadDataForListener(TrackDataListener listener) { + reloadDataForListener(registeredListeners.get(listener)); + } + + /** + * Reloads all track data received so far into the specified listeners. + */ + private void reloadDataForListener(final ListenerRegistration registration) { + if (!started) { + Log.w(TAG, "Not started, not reloading"); + return; + } + + runInListenerThread(new Runnable() { + @SuppressWarnings("unchecked") + @Override + public void run() { + TrackDataListener listener = registration.listener; + Set listenerSet = Collections.singleton(listener); + + if (registration.isInterestedIn(ListenerDataType.DISPLAY_PREFERENCES)) { + // Ignore the return values here, we're already sending the full data set anyway + listener.onUnitsChanged(useMetricUnits); + listener.onReportSpeedChanged(reportSpeed); + } + + if (registration.isInterestedIn(ListenerDataType.SELECTED_TRACK_CHANGED)) { + notifySelectedTrackChanged(selectedTrackId, listenerSet); + } + + if (registration.isInterestedIn(ListenerDataType.TRACK_UPDATES)) { + notifyTrackUpdated(listenerSet); + } + + boolean interestedInPoints = + registration.isInterestedIn(ListenerDataType.POINT_UPDATES); + boolean interestedInSampledOutPoints = + registration.isInterestedIn(ListenerDataType.SAMPLED_OUT_POINT_UPDATES); + if (interestedInPoints || interestedInSampledOutPoints) { + notifyPointsCleared(listenerSet); + notifyPointsUpdated(false, + listenerSet, + interestedInSampledOutPoints ? listenerSet : Collections.EMPTY_SET); + } + + if (registration.isInterestedIn(ListenerDataType.WAYPOINT_UPDATES)) { + notifyWaypointUpdated(listenerSet); + } + + if (registration.isInterestedIn(ListenerDataType.LOCATION_UPDATES)) { + if (lastSeenLocation != null) { + notifyLocationChanged(lastSeenLocation, true, listenerSet); + } else { + notifyFixType(); + } + } + + if (registration.isInterestedIn(ListenerDataType.COMPASS_UPDATES)) { + notifyHeadingChanged(listenerSet); + } + } + }); + } + + /** + * Reloads all track data received so far into the specified listeners. + */ + private void loadDataForAllListeners() { + if (registeredListeners.isEmpty()) { + Log.d(TAG, "No listeners, not reloading"); + return; + } + if (!started) { + Log.w(TAG, "Not started, not reloading"); + return; + } + + runInListenerThread(new Runnable() { + @Override + public void run() { + // Ignore the return values here, we're already sending the full data set anyway + for (TrackDataListener listener : getListenersFor(ListenerDataType.DISPLAY_PREFERENCES)) { + listener.onUnitsChanged(useMetricUnits); + listener.onReportSpeedChanged(reportSpeed); + } + + notifySelectedTrackChanged(selectedTrackId, + getListenersFor(ListenerDataType.SELECTED_TRACK_CHANGED)); + + notifyTrackUpdated(getListenersFor(ListenerDataType.TRACK_UPDATES)); + + Set pointListeners = + getListenersFor(ListenerDataType.POINT_UPDATES); + Set sampledOutPointListeners = + getListenersFor(ListenerDataType.SAMPLED_OUT_POINT_UPDATES); + notifyPointsCleared(pointListeners); + notifyPointsUpdated(true, pointListeners, sampledOutPointListeners); + + notifyWaypointUpdated(getListenersFor(ListenerDataType.WAYPOINT_UPDATES)); + + if (lastSeenLocation != null) { + notifyLocationChanged(lastSeenLocation, true, + getListenersFor(ListenerDataType.LOCATION_UPDATES)); + } else { + notifyFixType(); + } + + notifyHeadingChanged(getListenersFor(ListenerDataType.COMPASS_UPDATES)); + } + }); + } + + /** + * Called when a preference changes. + * + * @param key the key to the preference that changed + */ + private void notifyPreferenceChanged(String key) { + if (RECORDING_TRACK_KEY.equals(key)) { + recordingTrackId = preferences.getLong(RECORDING_TRACK_KEY, -1); + } else if (MIN_REQUIRED_ACCURACY_KEY.equals(key)) { + minRequiredAccuracy = preferences.getInt(MIN_REQUIRED_ACCURACY_KEY, + Constants.DEFAULT_MIN_REQUIRED_ACCURACY); + } else if (METRIC_UNITS_KEY.equals(key)) { + useMetricUnits = preferences.getBoolean(METRIC_UNITS_KEY, true); + notifyUnitsChanged(); + } else if (SPEED_REPORTING_KEY.equals(key)) { + reportSpeed = preferences.getBoolean(SPEED_REPORTING_KEY, true); + notifySpeedReportingChanged(); + } + } + + /** Called when the speed/pace reporting preference changes. */ + private void notifySpeedReportingChanged() { + if (!started) return; + + runInListenerThread(new Runnable() { + @Override + public void run() { + Set listeners = getListenersFor(ListenerDataType.DISPLAY_PREFERENCES); + + for (TrackDataListener listener : listeners) { + // TODO: Do the reloading just once for all interested listeners + if (listener.onReportSpeedChanged(reportSpeed)) { + reloadDataForListener(registeredListeners.get(listener)); + } + } + } + }); + } + + /** Called when the metric units setting changes. */ + private void notifyUnitsChanged() { + if (!started) return; + + runInListenerThread(new Runnable() { + @Override + public void run() { + Set listeners = getListenersFor(ListenerDataType.DISPLAY_PREFERENCES); + + for (TrackDataListener listener : listeners) { + if (listener.onUnitsChanged(useMetricUnits)) { + reloadDataForListener(registeredListeners.get(listener)); + } + } + } + }); + } + + /** Notifies about the current GPS fix state. */ + private void notifyFixType() { + final TrackDataListener.ProviderState state; + if (!hasProviderEnabled) { + state = ProviderState.DISABLED; + + // Give a global warning about this state. + Toast.makeText(context, R.string.error_no_gps_location_provider, Toast.LENGTH_LONG).show(); + } else if (!hasFix) { + state = ProviderState.NO_FIX; + } else if (!hasGoodFix) { + state = ProviderState.BAD_FIX; + } else { + state = ProviderState.GOOD_FIX; + } + + runInListenerThread(new Runnable() { + @Override + public void run() { + // Notify to everyone. + Log.d(TAG, "Notifying fix type: " + state); + for (TrackDataListener listener : getListenersFor(ListenerDataType.LOCATION_UPDATES)) { + listener.onProviderStateChange(state); + } + } + }); + } + + /** + * Notifies the the current location has changed, without any filtering. + * If the state of GPS fix has changed, that will also be reported. + * + * @param location the current location + * @param listeners the listeners to notify + */ + private void notifyLocationChanged(Location location, Set listeners) { + notifyLocationChanged(location, false, listeners); + } + + /** + * Notifies that the current location has changed, without any filtering. + * If the state of GPS fix has changed, that will also be reported. + * + * @param location the current location + * @param forceUpdate whether to force the notifications to happen + * @param listeners the listeners to notify + */ + private void notifyLocationChanged(final Location location, boolean forceUpdate, + final Set listeners) { + if (location == null) return; + if (listeners.isEmpty()) return; + + boolean isGpsLocation = location.getProvider().equals(LocationManager.GPS_PROVIDER); + + boolean oldHasFix = hasFix; + boolean oldHasGoodFix = hasGoodFix; + + // We consider a good fix to be a recent one with reasonable accuracy. + if (isGpsLocation) { + lastSeenLocation = location; + hasFix = (location != null && System.currentTimeMillis() - location.getTime() <= MAX_LOCATION_AGE_MS); + hasGoodFix = (location != null && location.getAccuracy() <= minRequiredAccuracy); + if (hasFix != oldHasFix || hasGoodFix != oldHasGoodFix || forceUpdate) { + notifyFixType(); + } + } + + runInListenerThread(new Runnable() { + @Override + public void run() { + for (TrackDataListener listener : listeners) { + listener.onCurrentLocationChanged(location); + } + } + }); + } + + /** + * Notifies that the current heading has changed. + * + * @param listeners the listeners to notify + */ + private void notifyHeadingChanged(final Set listeners) { + if (listeners.isEmpty()) return; + + runInListenerThread(new Runnable() { + @Override + public void run() { + float heading = lastSeenMagneticHeading + declination; + for (TrackDataListener listener : listeners) { + listener.onCurrentHeadingChanged(heading); + } + } + }); + } + + /** + * Notifies that a new track has been selected.. + * + * @param track the new selected track + * @param listeners the listeners to notify + */ + private void notifySelectedTrackChanged(long trackId, + final Set listeners) { + if (listeners.isEmpty()) return; + + Log.i(TAG, "New track selected, id=" + trackId); + final Track track = providerUtils.getTrack(trackId); + + runInListenerThread(new Runnable() { + @Override + public void run() { + for (TrackDataListener listener : listeners) { + listener.onSelectedTrackChanged(track, isRecordingSelected()); + + if (track != null) { + listener.onTrackUpdated(track); + } + } + } + }); + } + + /** + * Notifies that the currently-selected track's data has been updated. + * + * @param listeners the listeners to notify + */ + private void notifyTrackUpdated(final Set listeners) { + if (listeners.isEmpty()) return; + + final Track track = providerUtils.getTrack(selectedTrackId); + + runInListenerThread(new Runnable() { + @Override + public void run() { + for (TrackDataListener listener : listeners) { + listener.onTrackUpdated(track); + } + } + }); + } + + /** + * Notifies that waypoints have been updated. + * We assume few waypoints, so we reload them all every time. + * + * @param listeners the listeners to notify + */ + private void notifyWaypointUpdated(final Set listeners) { + if (listeners.isEmpty()) return; + + // Always reload all the waypoints. + final Cursor cursor = providerUtils.getWaypointsCursor( + selectedTrackId, 0L, Constants.MAX_DISPLAYED_WAYPOINTS_POINTS); + + runInListenerThread(new Runnable() { + @Override + public void run() { + Log.d(TAG, "Reloading waypoints"); + for (TrackDataListener listener : listeners) { + listener.clearWaypoints(); + } + + try { + if (cursor != null && cursor.moveToFirst()) { + do { + Waypoint waypoint = providerUtils.createWaypoint(cursor); + if (!LocationUtils.isValidLocation(waypoint.getLocation())) { + continue; + } + + for (TrackDataListener listener : listeners) { + listener.onNewWaypoint(waypoint); + } + } while (cursor.moveToNext()); + } + } finally { + if (cursor != null) { + cursor.close(); + } + } + + for (TrackDataListener listener : listeners) { + listener.onNewWaypointsDone(); + } + } + }); + } + + /** + * Tells listeners to clear the current list of points. + * + * @param listeners the listeners to notify + */ + private void notifyPointsCleared(final Set listeners) { + if (listeners.isEmpty()) return; + + runInListenerThread(new Runnable() { + @Override + public void run() { + for (TrackDataListener listener : listeners) { + listener.clearTrackPoints(); + } + } + }); + } + + /** + * Notifies the given listeners about track points in the given ID range. + * + * @param minPointId the first point ID to notify, inclusive + * @param maxPointId the last poind ID to notify, inclusive + * @param keepState whether to load and save state about the already-notified points. + * If true, only new points are reported. + * If false, then the whole track will be loaded, without affecting the store. + * @param listeners the listeners to notify + * @param trackDataListeners + */ + private void notifyPointsUpdated(final boolean keepState, + final Set sampledListeners, + final Set sampledOutListeners) { + if (sampledListeners.isEmpty() && sampledOutListeners.isEmpty()) return; + + runInListenerThread(new Runnable() { + @Override + public void run() { + notifyPointsUpdatedSync(keepState, sampledListeners, sampledOutListeners); + } + }); + } + + /** + * Asynchronous version of the above method. + */ + private void notifyPointsUpdatedSync(boolean keepState, + Set sampledListeners, + Set sampledOutListeners) { + // If we're loading state, start from after the last seen point up to the last recorded one + // (all new points) + // If we're not loading state, then notify about all the previously-seen points. + long minPointId = keepState ? lastSeenLocationId + 1 : 0; + long maxPointId = keepState ? -1 : lastSeenLocationId; + + // TODO: Move (re)sampling to a separate class. + if (numLoadedPoints >= Constants.MAX_DISPLAYED_TRACK_POINTS) { + // We're about to exceed the maximum allowed number of points, so reload + // the whole track with fewer points (the sampling frequency will be + // lower). We do this for every listener even if we were loading just for + // a few of them (why miss the oportunity?). + + Log.i(TAG, "Resampling point set after " + numLoadedPoints + " points."); + firstSeenLocationId = -1; + lastSeenLocationId = -1; + numLoadedPoints = 0; + sampledListeners = getListenersFor(ListenerDataType.POINT_UPDATES); + sampledOutListeners = getListenersFor(ListenerDataType.SAMPLED_OUT_POINT_UPDATES); + maxPointId = -1; + minPointId = 0; + keepState = true; + + for (TrackDataListener listener : sampledListeners) { + listener.clearTrackPoints(); + } + } + + // Keep the originally selected track ID so we can stop if it changes. + long currentSelectedTrackId = selectedTrackId; + + // If we're ignoring state, start from the beginning of the track + int localNumLoadedPoints = keepState ? numLoadedPoints : 0; + long localFirstSeenLocationId = keepState ? firstSeenLocationId : 0; + long localLastSeenLocationId = minPointId; + long lastStoredLocationId = providerUtils.getLastLocationId(currentSelectedTrackId); + int pointSamplingFrequency = -1; + + // Create a double-buffering location provider. + MyTracksProviderUtils.DoubleBufferedLocationFactory locationFactory = + new MyTracksProviderUtils.DoubleBufferedLocationFactory(); + LocationIterator it = providerUtils.getLocationIterator( + currentSelectedTrackId, minPointId, false, locationFactory); + + while (it.hasNext()) { + if (currentSelectedTrackId != selectedTrackId) { + // The selected track changed beneath us, stop. + break; + } + + Location location = it.next(); + long locationId = it.getLocationId(); + + // If past the last wanted point, stop. + // This happens when adding a new listener after data has already been loaded, + // in which case we only want to bring that listener up to the point where the others + // were. In case it does happen, we should be wasting few points (only the ones not + // yet notified to other listeners). + if (maxPointId > 0 && locationId > maxPointId) { + break; + } + + if (localFirstSeenLocationId == -1) { + // This was our first point, keep its ID + localFirstSeenLocationId = locationId; + } + + if (pointSamplingFrequency == -1) { + // Now we already have at least one point, calculate the sampling + // frequency. + long numTotalPoints = lastStoredLocationId - localFirstSeenLocationId; + pointSamplingFrequency = + (int) (1 + numTotalPoints / Constants.TARGET_DISPLAYED_TRACK_POINTS); + } + + notifyNewPoint(location, locationId, lastStoredLocationId, + localNumLoadedPoints, pointSamplingFrequency, sampledListeners, sampledOutListeners); + + localNumLoadedPoints++; + localLastSeenLocationId = locationId; + } + it.close(); + + if (keepState) { + numLoadedPoints = localNumLoadedPoints; + firstSeenLocationId = localFirstSeenLocationId; + lastSeenLocationId = localLastSeenLocationId; + } + + for (TrackDataListener listener : sampledListeners) { + listener.onNewTrackPointsDone(); + } + } + + private void notifyNewPoint(Location location, + long locationId, + long lastStoredLocationId, + int numLoadedPoints, + int pointSamplingFrequency, + Set sampledListeners, + Set sampledOutListeners) { + boolean isValid = LocationUtils.isValidLocation(location); + if (!isValid) { + // Invalid points are segment splits - report those separately. + // TODO: Always send last valid point before and first valid point after a split + for (TrackDataListener listener : sampledListeners) { + listener.onSegmentSplit(); + } + return; + } + + // Include a point if it fits one of the following criteria: + // - Has the mod for the sampling frequency (includes first point). + // - Is the last point and we are not recording this track. + boolean includeInSample = + (numLoadedPoints % pointSamplingFrequency == 0 || + (!isRecordingSelected() && locationId == lastStoredLocationId)); + + if (!includeInSample) { + for (TrackDataListener listener : sampledOutListeners) { + listener.onSampledOutTrackPoint(location); + } + return; + } + + // Point is valid and included in sample. + for (TrackDataListener listener : sampledListeners) { + // No need to allocate a new location (we can safely reuse the existing). + listener.onNewTrackPoint(location); + } + } + + private Set getListenersFor(ListenerDataType type) { + synchronized (registeredListeners) { + return listenerSetsPerType.get(type); + } + } + + // @VisibleForTesting + protected void runInListenerThread(Runnable runnable) { + listenerHandler.post(runnable); + } +} diff --git a/MyTracks/src/com/google/android/apps/mytracks/TrackDataListener.java b/MyTracks/src/com/google/android/apps/mytracks/TrackDataListener.java new file mode 100644 index 000000000..4ba1539bc --- /dev/null +++ b/MyTracks/src/com/google/android/apps/mytracks/TrackDataListener.java @@ -0,0 +1,152 @@ +/* + * 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; + +import com.google.android.apps.mytracks.content.Track; +import com.google.android.apps.mytracks.content.Waypoint; + +import android.location.Location; + +/** + * Listener for track data, for both initial and incremental loading. + * + * TODO: Allow listening to just part of the data received here. + * + * @author Rodrigo Damazio + */ +public interface TrackDataListener { + + /** States for the GPS location provider. */ + public enum ProviderState { + DISABLED, + NO_FIX, + BAD_FIX, + GOOD_FIX; + } + + /** + * Called when the location provider changes state. + */ + void onProviderStateChange(ProviderState state); + + /** + * Called when the current location changes. + * This is meant for immediate location display only - track point data is + * delivered by other methods below, such as {@link #onNewTrackPoint}. + * + * @param loc the last known location + */ + void onCurrentLocationChanged(Location loc); + + /** + * Called when the current heading changes. + * + * @param heading the current heading, already accounting magnetic declination + */ + void onCurrentHeadingChanged(double heading); + + /** + * Called when the currently-selected track changes. + * This will be followed by calls to data methods such as + * {@link #onTrackUpdated}, {@link #clearTrackPoints}, + * {@link #onNewTrackPoint(Location)}, etc., even if no track is currently + * selected (in which case you'll only get calls to clear the current data). + * + * @param track the selected track, or null if no track is selected + * @param isRecording whether we're currently recording the selected track + */ + void onSelectedTrackChanged(Track track, boolean isRecording); + + /** + * Called when the track and/or its statistics have been updated. + * + * @param track the updated version of the track + */ + void onTrackUpdated(Track track); + + /** + * Called to clear any previously-sent track points. + * This can be called at any time that we decide the data needs to be + * reloaded, such as when it needs to be resampled. + */ + void clearTrackPoints(); + + /** + * Called when a new interesting track point is read. + * In this case, interesting means that the point has already undergone + * sampling and invalid point filtering. + * + * @param loc the new track point + */ + void onNewTrackPoint(Location loc); + + /** + * Called when a uninteresting track point is read. + * Uninteresting points are all points that get sampled out of the track. + * + * @param loc the new track point + */ + void onSampledOutTrackPoint(Location loc); + + /** + * Called when an invalid point (representing a segment split) is read. + */ + void onSegmentSplit(); + + /** + * Called when we're done (for the time being) sending new points. + * This gets called after every batch of calls to {@link #onNewTrackPoint}, + * {@link #onSampledOutTrackPoint} and {@link #onSegmentSplit}. + */ + void onNewTrackPointsDone(); + + /** + * Called to clear any previously-sent waypoints. + * This can be called at any time that we decide the data needs to be + * reloaded. + */ + void clearWaypoints(); + + /** + * Called when a new waypoint is read. + * + * @param wpt the new waypoint + */ + void onNewWaypoint(Waypoint wpt); + + /** + * Called when we're done (for the time being) sending new waypoints. + * This gets called after every batch of calls to {@link #clearWaypoints} and + * {@link #onNewWaypoint}. + */ + void onNewWaypointsDone(); + + /** + * Called when the display units are changed by the user. + * + * @param metric true if the units are metric, false if imperial + * @return true to reload all the data, false otherwise + */ + boolean onUnitsChanged(boolean metric); + + /** + * Called when the speed/pace display unit is changed by the user. + * + * @param reportSpeed true to report speed, false for pace + * @return true to reload all the data, false otherwise + */ + boolean onReportSpeedChanged(boolean reportSpeed); +} diff --git a/MyTracks/src/com/google/android/apps/mytracks/TrackDataSources.java b/MyTracks/src/com/google/android/apps/mytracks/TrackDataSources.java new file mode 100644 index 000000000..8801532d8 --- /dev/null +++ b/MyTracks/src/com/google/android/apps/mytracks/TrackDataSources.java @@ -0,0 +1,54 @@ +/* + * 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; + +import android.content.SharedPreferences.OnSharedPreferenceChangeListener; +import android.database.ContentObserver; +import android.hardware.Sensor; +import android.hardware.SensorEventListener; +import android.location.Location; +import android.location.LocationListener; +import android.net.Uri; + +/** + * Interface for abstracting external data sources. + * + * @author Rodrigo Damazio + */ +interface TrackDataSources { + // Preferences + void registerOnSharedPreferenceChangeListener( + OnSharedPreferenceChangeListener listener); + void unregisterOnSharedPreferenceChangeListener( + OnSharedPreferenceChangeListener listener); + + // Content provider + void registerContentObserver(Uri contentUri, boolean descendents, + ContentObserver observer); + void unregisterContentObserver(ContentObserver observer); + + // Sensors + Sensor getSensor(int type); + void registerSensorListener(SensorEventListener listener, + Sensor sensor, int sensorDelay); + void unregisterSensorListener(SensorEventListener listener); + + // Location + boolean isLocationProviderEnabled(String provider); + void requestLocationUpdates(LocationListener listener); + void removeLocationUpdates(LocationListener listener); + Location getLastKnownLocation(); +} \ No newline at end of file diff --git a/MyTracks/src/com/google/android/apps/mytracks/TrackDataSourcesImpl.java b/MyTracks/src/com/google/android/apps/mytracks/TrackDataSourcesImpl.java new file mode 100644 index 000000000..420440beb --- /dev/null +++ b/MyTracks/src/com/google/android/apps/mytracks/TrackDataSourcesImpl.java @@ -0,0 +1,165 @@ +/* + * 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; + +import static com.google.android.apps.mytracks.Constants.MAX_LOCATION_AGE_MS; +import static com.google.android.apps.mytracks.Constants.MAX_NETWORK_AGE_MS; + +import com.google.android.maps.mytracks.R; + +import android.content.ContentResolver; +import android.content.Context; +import android.content.SharedPreferences; +import android.content.SharedPreferences.OnSharedPreferenceChangeListener; +import android.database.ContentObserver; +import android.hardware.Sensor; +import android.hardware.SensorEventListener; +import android.hardware.SensorManager; +import android.location.Location; +import android.location.LocationListener; +import android.location.LocationManager; +import android.location.LocationProvider; +import android.net.Uri; +import android.util.Log; +import android.widget.Toast; + +/** + * Real implementation of the data sources, which talks to system services. + * + * @author Rodrigo Damazio + */ +class TrackDataSourcesImpl implements TrackDataSources { + // System services + private final SensorManager sensorManager; + private final LocationManager locationManager; + private final ContentResolver contentResolver; + private final SharedPreferences sharedPreferences; + private final Context context; + + TrackDataSourcesImpl(Context context, SharedPreferences sharedPreferences) { + this.context = context; + this.sensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE); + this.locationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE); + this.contentResolver = context.getContentResolver(); + this.sharedPreferences = sharedPreferences; + } + + @Override + public void registerOnSharedPreferenceChangeListener( + OnSharedPreferenceChangeListener listener) { + sharedPreferences.registerOnSharedPreferenceChangeListener(listener); + } + + @Override + public void unregisterOnSharedPreferenceChangeListener( + OnSharedPreferenceChangeListener listener) { + sharedPreferences.unregisterOnSharedPreferenceChangeListener(listener); + } + + @Override + public void registerContentObserver(Uri contentUri, boolean descendents, + ContentObserver observer) { + contentResolver.registerContentObserver(contentUri, descendents, observer); + } + + @Override + public void unregisterContentObserver(ContentObserver observer) { + contentResolver.unregisterContentObserver(observer); + } + + @Override + public Sensor getSensor(int type) { + return sensorManager.getDefaultSensor(type); + } + + @Override + public void registerSensorListener(SensorEventListener listener, + Sensor sensor, int sensorDelay) { + sensorManager.registerListener(listener, sensor, sensorDelay); + } + + @Override + public void unregisterSensorListener(SensorEventListener listener) { + sensorManager.unregisterListener(listener); + } + + @Override + public boolean isLocationProviderEnabled(String provider) { + return locationManager.isProviderEnabled(provider); + } + + @Override + public void requestLocationUpdates(LocationListener listener) { + // Check if the provider exists. + LocationProvider gpsProvider = locationManager.getProvider(LocationManager.GPS_PROVIDER); + if (gpsProvider == null) { + listener.onProviderDisabled(LocationManager.GPS_PROVIDER); + locationManager.removeUpdates(listener); + return; + } + + // Listen to GPS location. + String providerName = gpsProvider.getName(); + Log.d(Constants.TAG, "TrackDataHub: Using location provider " + providerName); + locationManager.requestLocationUpdates(providerName, + 0 /*minTime*/, 0 /*minDist*/, listener); + + // Give an initial update on provider state. + if (locationManager.isProviderEnabled(providerName)) { + listener.onProviderEnabled(providerName); + } else { + listener.onProviderDisabled(providerName); + } + + // Listen to network location + try { + locationManager.requestLocationUpdates(LocationManager.NETWORK_PROVIDER, + 1000 * 60 * 5 /*minTime*/, 0 /*minDist*/, listener); + } 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, "Could not register network location listener.", e); + } + } + + @Override + public Location getLastKnownLocation() { + // TODO: Let's look at more advanced algorithms to determine the best + // current location. + + Location loc = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER); + final long now = System.currentTimeMillis(); + if (loc == null || loc.getTime() < now - MAX_LOCATION_AGE_MS) { + // We don't have a recent GPS fix, just use cell towers if available + loc = locationManager.getLastKnownLocation(LocationManager.NETWORK_PROVIDER); + + int toastResId = R.string.status_approximate_location; + if (loc == null || loc.getTime() < now - MAX_NETWORK_AGE_MS) { + // We don't have a recent cell tower location, let the user know: + toastResId = R.string.status_no_location; + } + + // Let the user know we have only an approximate location: + Toast.makeText(context, context.getString(toastResId), Toast.LENGTH_LONG).show(); + } + return loc; + } + + @Override + public void removeLocationUpdates(LocationListener listener) { + locationManager.removeUpdates(listener); + } +} \ No newline at end of file diff --git a/MyTracks/src/com/google/android/apps/mytracks/MyTracksDetails.java b/MyTracks/src/com/google/android/apps/mytracks/TrackDetails.java similarity index 97% rename from MyTracks/src/com/google/android/apps/mytracks/MyTracksDetails.java rename to MyTracks/src/com/google/android/apps/mytracks/TrackDetails.java index 47d387d37..fcdd4dfa0 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/MyTracksDetails.java +++ b/MyTracks/src/com/google/android/apps/mytracks/TrackDetails.java @@ -37,7 +37,7 @@ import android.widget.EditText; * * @author Leif Hendrik Wilden */ -public class MyTracksDetails extends Activity implements OnClickListener { +public class TrackDetails extends Activity implements OnClickListener { /** * The id of the track being edited (taken from bundle, "trackid") @@ -56,7 +56,7 @@ public class MyTracksDetails extends Activity implements OnClickListener { // Required extra when launching this intent: trackId = getIntent().getLongExtra("trackid", -1); if (trackId < 0) { - Log.d(MyTracksConstants.TAG, + Log.d(Constants.TAG, "MyTracksDetails intent was launched w/o track id."); finish(); return; diff --git a/MyTracks/src/com/google/android/apps/mytracks/MyTracksList.java b/MyTracks/src/com/google/android/apps/mytracks/TrackList.java similarity index 82% rename from MyTracks/src/com/google/android/apps/mytracks/MyTracksList.java rename to MyTracks/src/com/google/android/apps/mytracks/TrackList.java index 6fe18df2a..d39fbc032 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/MyTracksList.java +++ b/MyTracks/src/com/google/android/apps/mytracks/TrackList.java @@ -45,7 +45,7 @@ import android.widget.TextView; * * @author Leif Hendrik Wilden */ -public class MyTracksList extends ListActivity +public class TrackList extends ListActivity implements SharedPreferences.OnSharedPreferenceChangeListener, View.OnClickListener { @@ -70,40 +70,40 @@ public class MyTracksList extends ListActivity AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuInfo; contextPosition = info.position; - trackId = MyTracksList.this.listView.getAdapter().getItemId( + trackId = TrackList.this.listView.getAdapter().getItemId( contextPosition); - menu.add(0, MyTracksConstants.MENU_SHOW, 0, + menu.add(0, Constants.MENU_SHOW, 0, R.string.tracklist_show_track); - menu.add(0, MyTracksConstants.MENU_EDIT, 0, + menu.add(0, Constants.MENU_EDIT, 0, R.string.tracklist_edit_track); if (!MyTracks.getInstance().isRecording() || trackId != recordingTrackId) { - menu.add(0, MyTracksConstants.MENU_SEND_TO_GOOGLE, 0, + menu.add(0, Constants.MENU_SEND_TO_GOOGLE, 0, R.string.tracklist_send_to_google); - SubMenu share = menu.addSubMenu(0, MyTracksConstants.MENU_SHARE, 0, + SubMenu share = menu.addSubMenu(0, Constants.MENU_SHARE, 0, R.string.tracklist_share_track); - share.add(0, MyTracksConstants.MENU_SHARE_LINK, 0, - R.string.tracklist_share_linkfusion); - share.add(0, MyTracksConstants.MENU_SHARE_GPX_FILE, 0, + share.add(0, Constants.MENU_SHARE_LINK, 0, + R.string.tracklist_share_link); + share.add(0, Constants.MENU_SHARE_GPX_FILE, 0, R.string.tracklist_share_gpx_file); - share.add(0, MyTracksConstants.MENU_SHARE_KML_FILE, 0, + share.add(0, Constants.MENU_SHARE_KML_FILE, 0, R.string.tracklist_share_kml_file); - share.add(0, MyTracksConstants.MENU_SHARE_CSV_FILE, 0, + share.add(0, Constants.MENU_SHARE_CSV_FILE, 0, R.string.tracklist_share_csv_file); - share.add(0, MyTracksConstants.MENU_SHARE_TCX_FILE, 0, + share.add(0, Constants.MENU_SHARE_TCX_FILE, 0, R.string.tracklist_share_tcx_file); SubMenu save = menu.addSubMenu(0, - MyTracksConstants.MENU_WRITE_TO_SD_CARD, 0, + Constants.MENU_WRITE_TO_SD_CARD, 0, R.string.tracklist_write_to_sd); - save.add(0, MyTracksConstants.MENU_SAVE_GPX_FILE, 0, + save.add(0, Constants.MENU_SAVE_GPX_FILE, 0, R.string.tracklist_save_as_gpx); - save.add(0, MyTracksConstants.MENU_SAVE_KML_FILE, 0, + save.add(0, Constants.MENU_SAVE_KML_FILE, 0, R.string.tracklist_save_as_kml); - save.add(0, MyTracksConstants.MENU_SAVE_CSV_FILE, 0, + save.add(0, Constants.MENU_SAVE_CSV_FILE, 0, R.string.tracklist_save_as_csv); - save.add(0, MyTracksConstants.MENU_SAVE_TCX_FILE, 0, + save.add(0, Constants.MENU_SAVE_TCX_FILE, 0, R.string.tracklist_save_as_tcx); - menu.add(0, MyTracksConstants.MENU_DELETE, 0, + menu.add(0, Constants.MENU_DELETE, 0, R.string.tracklist_delete_track); } } @@ -132,7 +132,7 @@ public class MyTracksList extends ListActivity protected void onListItemClick(ListView l, View v, int position, long id) { Intent result = new Intent(); result.putExtra("trackid", id); - setResult(MyTracksConstants.SHOW_TRACK, result); + setResult(Constants.SHOW_TRACK, result); finish(); } @@ -140,24 +140,24 @@ public class MyTracksList extends ListActivity public boolean onMenuItemSelected(int featureId, MenuItem item) { if (!super.onMenuItemSelected(featureId, item)) { switch (item.getItemId()) { - case MyTracksConstants.MENU_SHOW: { + case Constants.MENU_SHOW: { onListItemClick(null, null, 0, trackId); return true; } - case MyTracksConstants.MENU_EDIT: { - Intent intent = new Intent(this, MyTracksDetails.class); + case Constants.MENU_EDIT: { + Intent intent = new Intent(this, TrackDetails.class); intent.putExtra("trackid", trackId); startActivity(intent); return true; } - case MyTracksConstants.MENU_SHARE: - case MyTracksConstants.MENU_WRITE_TO_SD_CARD: + case Constants.MENU_SHARE: + case Constants.MENU_WRITE_TO_SD_CARD: return false; default: { Intent result = new Intent(); result.putExtra("trackid", trackId); setResult( - MyTracksConstants.getActionFromMenuId(item.getItemId()), result); + Constants.getActionFromMenuId(item.getItemId()), result); finish(); return true; } @@ -170,7 +170,7 @@ public class MyTracksList extends ListActivity public void onClick(View v) { switch (v.getId()) { case R.id.tracklist_btn_delete_all: { - Handler h = new MyTracksDeleteAllTracks(this, null); + Handler h = new DeleteAllTracks(this, null); h.handleMessage(null); break; } @@ -197,12 +197,17 @@ public class MyTracksList extends ListActivity listView = getListView(); listView.setOnCreateContextMenuListener(contextMenuListener); - findViewById(R.id.tracklist_btn_delete_all).setOnClickListener(this); - findViewById(R.id.tracklist_btn_export_all).setOnClickListener(this); + View deleteAll = findViewById(R.id.tracklist_btn_delete_all); + View exportAll = findViewById(R.id.tracklist_btn_export_all); + boolean notRecording = !MyTracks.getInstance().isRecording(); + deleteAll.setOnClickListener(this); + deleteAll.setEnabled(notRecording); + exportAll.setOnClickListener(this); + exportAll.setEnabled(notRecording); findViewById(R.id.tracklist_btn_import_all).setOnClickListener(this); SharedPreferences preferences = - getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0); + getSharedPreferences(Constants.SETTINGS_NAME, 0); preferences.registerOnSharedPreferenceChangeListener(this); metricUnits = preferences.getBoolean(getString(R.string.metric_units_key), true); diff --git a/MyTracks/src/com/google/android/apps/mytracks/MyTracksWaypointDetails.java b/MyTracks/src/com/google/android/apps/mytracks/WaypointDetails.java similarity index 97% rename from MyTracks/src/com/google/android/apps/mytracks/MyTracksWaypointDetails.java rename to MyTracks/src/com/google/android/apps/mytracks/WaypointDetails.java index 5e9f110b7..a4a32436d 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/MyTracksWaypointDetails.java +++ b/MyTracks/src/com/google/android/apps/mytracks/WaypointDetails.java @@ -39,7 +39,7 @@ import android.widget.ImageView; * * @author Leif Hendrik Wilden */ -public class MyTracksWaypointDetails extends Activity +public class WaypointDetails extends Activity implements OnClickListener { public static final String WAYPOINT_ID_EXTRA = "com.google.android.apps.mytracks.WAYPOINT_ID"; @@ -65,7 +65,7 @@ public class MyTracksWaypointDetails extends Activity utils = new StatsUtilities(this); SharedPreferences preferences = - getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0); + getSharedPreferences(Constants.SETTINGS_NAME, 0); if (preferences != null) { boolean useMetric = preferences.getBoolean(getString(R.string.metric_units_key), true); @@ -82,7 +82,7 @@ public class MyTracksWaypointDetails extends Activity // Required extra when launching this intent: waypointId = getIntent().getLongExtra(WAYPOINT_ID_EXTRA, -1); if (waypointId < 0) { - Log.d(MyTracksConstants.TAG, + Log.d(Constants.TAG, "MyTracksWaypointsDetails intent was launched w/o waypoint id."); finish(); return; diff --git a/MyTracks/src/com/google/android/apps/mytracks/MyTracksWaypointsList.java b/MyTracks/src/com/google/android/apps/mytracks/WaypointsList.java similarity index 88% rename from MyTracks/src/com/google/android/apps/mytracks/MyTracksWaypointsList.java rename to MyTracks/src/com/google/android/apps/mytracks/WaypointsList.java index f3c151169..cbaad0481 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/MyTracksWaypointsList.java +++ b/MyTracks/src/com/google/android/apps/mytracks/WaypointsList.java @@ -49,7 +49,7 @@ import android.widget.TextView; * * @author Leif Hendrik Wilden */ -public class MyTracksWaypointsList extends ListActivity +public class WaypointsList extends ListActivity implements View.OnClickListener { private int contextPosition = -1; @@ -59,7 +59,6 @@ public class MyTracksWaypointsList extends ListActivity private Button insertWaypointButton = null; private Button insertStatisticsButton = null; private long recordingTrackId = -1; - private long selectedTrackId = -1; private MyTracksProviderUtils providerUtils; private Cursor waypointsCursor = null; @@ -72,14 +71,14 @@ public class MyTracksWaypointsList extends ListActivity AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuInfo; contextPosition = info.position; - waypointId = MyTracksWaypointsList.this.listView.getAdapter() + waypointId = WaypointsList.this.listView.getAdapter() .getItemId(contextPosition); int type = providerUtils.getWaypoint(info.id).getType(); - menu.add(0, MyTracksConstants.MENU_SHOW, 0, + menu.add(0, Constants.MENU_SHOW, 0, R.string.waypointslist_show_waypoint); - menu.add(0, MyTracksConstants.MENU_EDIT, 0, + menu.add(0, Constants.MENU_EDIT, 0, R.string.waypointslist_edit_waypoint); - menu.add(0, MyTracksConstants.MENU_DELETE, 0, + menu.add(0, Constants.MENU_DELETE, 0, R.string.waypointslist_delete_waypoint).setEnabled( recordingTrackId < 0 || type == Waypoint.TYPE_WAYPOINT || info.id != providerUtils.getLastWaypointId(recordingTrackId)); @@ -90,8 +89,8 @@ public class MyTracksWaypointsList extends ListActivity protected void onListItemClick(ListView l, View v, int position, long id) { Intent result = new Intent(); result.putExtra("trackid", trackId); - result.putExtra(MyTracksWaypointDetails.WAYPOINT_ID_EXTRA, id); - setResult(MyTracksConstants.EDIT_WAYPOINT, result); + result.putExtra(WaypointDetails.WAYPOINT_ID_EXTRA, id); + setResult(Constants.EDIT_WAYPOINT, result); finish(); } @@ -99,18 +98,18 @@ public class MyTracksWaypointsList extends ListActivity public boolean onMenuItemSelected(int featureId, MenuItem item) { if (!super.onMenuItemSelected(featureId, item)) { switch (item.getItemId()) { - case MyTracksConstants.MENU_SHOW: { + case Constants.MENU_SHOW: { onListItemClick(null, null, 0, waypointId); return true; } - case MyTracksConstants.MENU_EDIT: { - Intent intent = new Intent(this, MyTracksWaypointDetails.class); + case Constants.MENU_EDIT: { + Intent intent = new Intent(this, WaypointDetails.class); intent.putExtra("trackid", trackId); - intent.putExtra(MyTracksWaypointDetails.WAYPOINT_ID_EXTRA, waypointId); + intent.putExtra(WaypointDetails.WAYPOINT_ID_EXTRA, waypointId); startActivity(intent); return true; } - case MyTracksConstants.MENU_DELETE: { + case Constants.MENU_DELETE: { deleteWaypoint(waypointId); } } @@ -139,7 +138,10 @@ public class MyTracksWaypointsList extends ListActivity (Button) findViewById(R.id.waypointslist_btn_insert_statistics); insertStatisticsButton.setOnClickListener(this); SharedPreferences preferences = - getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0); + getSharedPreferences(Constants.SETTINGS_NAME, 0); + + // TODO: Get rid of selected and recording track IDs + long selectedTrackId = -1; if (preferences != null) { recordingTrackId = preferences.getLong(getString(R.string.recording_track_key), -1); @@ -183,18 +185,18 @@ public class MyTracksWaypointsList extends ListActivity try { id = MyTracks.getInstance().insertWaypoint(request); } catch (RemoteException e) { - Log.e(MyTracksConstants.TAG, "Cannot insert marker.", e); + Log.e(Constants.TAG, "Cannot insert marker.", e); return; } catch (IllegalStateException e) { - Log.e(MyTracksConstants.TAG, "Cannot insert marker.", e); + Log.e(Constants.TAG, "Cannot insert marker.", e); return; } if (id < 0) { - Log.e(MyTracksConstants.TAG, "Failed to insert marker."); + Log.e(Constants.TAG, "Failed to insert marker."); return; } - Intent intent = new Intent(this, MyTracksWaypointDetails.class); - intent.putExtra(MyTracksWaypointDetails.WAYPOINT_ID_EXTRA, id); + Intent intent = new Intent(this, WaypointDetails.class); + intent.putExtra(WaypointDetails.WAYPOINT_ID_EXTRA, id); startActivity(intent); } @@ -259,7 +261,7 @@ public class MyTracksWaypointsList extends ListActivity public void onClick(DialogInterface dialog, int i) { dialog.dismiss(); providerUtils.deleteWaypoint(waypointId, - new StringUtils(MyTracksWaypointsList.this)); + new StringUtils(WaypointsList.this)); } }); builder.setNegativeButton(getString(R.string.no), diff --git a/MyTracks/src/com/google/android/apps/mytracks/WelcomeActivity.java b/MyTracks/src/com/google/android/apps/mytracks/WelcomeActivity.java index 45251a3b7..113253c5c 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/WelcomeActivity.java +++ b/MyTracks/src/com/google/android/apps/mytracks/WelcomeActivity.java @@ -15,7 +15,7 @@ */ package com.google.android.apps.mytracks; -import com.google.android.apps.mytracks.util.MyTracksUtils; +import com.google.android.apps.mytracks.util.SystemUtils; import com.google.android.maps.mytracks.R; import android.app.Activity; @@ -82,6 +82,6 @@ public class WelcomeActivity extends Activity { AlertDialog dialog = builder.create(); dialog.show(); ((TextView) dialog.findViewById(R.id.about_version_register)). - setText(MyTracksUtils.getMyTracksVersion(this)); + setText(SystemUtils.getMyTracksVersion(this)); } } diff --git a/MyTracks/src/com/google/android/apps/mytracks/WriteProgressController.java b/MyTracks/src/com/google/android/apps/mytracks/WriteProgressController.java new file mode 100644 index 000000000..3f0356756 --- /dev/null +++ b/MyTracks/src/com/google/android/apps/mytracks/WriteProgressController.java @@ -0,0 +1,123 @@ +/* + * 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; + +import com.google.android.apps.mytracks.io.file.TrackWriter; +import com.google.android.maps.mytracks.R; + +import android.app.Activity; +import android.app.ProgressDialog; +import android.content.DialogInterface; + +/** + * Given a {@link TrackWriter}, this class manages the process of writing the + * data in the track represented by the writer. This includes the display of + * a progress dialog, updating the progress bar in said dialog, and notifying + * interested parties when the write completes. + * + * @author Matthew Simmons + */ +class WriteProgressController { + /** + * This listener is used to notify interested parties when the write has + * completed. + */ + public interface OnCompletionListener { + /** + * When this method is invoked, the write has completed, and the progress + * dialog has been dismissed. Whether the write succeeded can be + * determined by examining the {@link TrackWriter}. + */ + public void onComplete(TrackWriter writer); + } + + private final Activity activity; + private final TrackWriter writer; + private final ProgressDialog dialog; + + private OnCompletionListener onCompletionListener; + + /** + * @param activity the activity associated with this write + * @param writer the writer which writes the track to disk. Note that this + * class will use the writer's completion listener. If callers are + * interested in notification upon completion of the write, they should + * use {@link #setOnCompletionListener}. + */ + public WriteProgressController(Activity activity, TrackWriter writer) { + this.activity = activity; + this.writer = writer; + + dialog = new ProgressDialog(activity); + dialog.setIcon(android.R.drawable.ic_dialog_info); + dialog.setTitle(activity.getString(R.string.progress_title)); + dialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL); + dialog.setMessage(activity.getString(R.string.write_progress_message)); + dialog.setIndeterminate(true); + dialog.setOnCancelListener(dialogCancelListener); + + writer.setOnCompletionListener(writerCompleteListener); + writer.setOnWriteListener(writerWriteListener); + } + + /** Set a listener to be invoked when the write completes. */ + public void setOnCompletionListener(OnCompletionListener onCompletionListener) { + this.onCompletionListener = onCompletionListener; + } + + /** Initiate an asynchronous write. */ + public void startWrite() { + DialogManager.showDialogSafely(activity, dialog); + writer.writeTrackAsync(); + } + + /** VisibleForTesting */ + ProgressDialog getDialog() { + return dialog; + } + + private final DialogInterface.OnCancelListener dialogCancelListener = + new DialogInterface.OnCancelListener() { + @Override + public void onCancel(DialogInterface dialog) { + writer.stopWriteTrack(); + } + }; + + private final TrackWriter.OnCompletionListener writerCompleteListener = + new TrackWriter.OnCompletionListener() { + @Override + public void onComplete() { + DialogManager.dismissDialogSafely(activity, dialog); + + if (onCompletionListener != null) { + onCompletionListener.onComplete(writer); + } + } + }; + + private final TrackWriter.OnWriteListener writerWriteListener = + new TrackWriter.OnWriteListener() { + @Override + public void onWrite(int number, int max) { + if (number % 500 == 0) { + dialog.setIndeterminate(false); + dialog.setMax(max); + dialog.setProgress(Math.min(number, max)); + } + } + }; +} 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 d94be1329..7c437a172 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/content/MyTracksProvider.java +++ b/MyTracks/src/com/google/android/apps/mytracks/content/MyTracksProvider.java @@ -15,7 +15,7 @@ */ package com.google.android.apps.mytracks.content; -import com.google.android.apps.mytracks.MyTracksConstants; +import com.google.android.apps.mytracks.Constants; import com.google.android.apps.mytracks.util.ApiFeatures; import android.content.ContentProvider; @@ -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; } @@ -373,7 +382,7 @@ public class MyTracksProvider extends ContentProvider { } if (ApiFeatures.getInstance().canReuseSQLiteQueryBuilder()) { - Log.i(MyTracksConstants.TAG, + Log.i(Constants.TAG, "Build query: " + qb.buildQuery(projection, selection, selectionArgs, null, null, sortOrder, null)); } diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/AuthManagerFactory.java b/MyTracks/src/com/google/android/apps/mytracks/io/AuthManagerFactory.java index d701a530c..8532e807d 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/io/AuthManagerFactory.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/AuthManagerFactory.java @@ -15,7 +15,7 @@ */ package com.google.android.apps.mytracks.io; -import com.google.android.apps.mytracks.MyTracksConstants; +import com.google.android.apps.mytracks.Constants; import android.app.Activity; import android.os.Build; @@ -46,11 +46,11 @@ public class AuthManagerFactory { public static AuthManager getAuthManager(Activity activity, int code, Bundle extras, boolean requireGoogle, String service) { if (useModernAuthManager()) { - Log.i(MyTracksConstants.TAG, + Log.i(Constants.TAG, "Creating modern auth manager: " + service); return new ModernAuthManager(activity, code, extras, requireGoogle, service); } else { - Log.i(MyTracksConstants.TAG, + Log.i(Constants.TAG, "Creating legacy auth manager: " + service); return new AuthManagerOld(activity, code, extras, requireGoogle, service); } diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/ModernAuthManager.java b/MyTracks/src/com/google/android/apps/mytracks/io/ModernAuthManager.java index 511584541..24c721bc4 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/io/ModernAuthManager.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/ModernAuthManager.java @@ -23,7 +23,7 @@ import com.google.android.accounts.AuthenticatorException; import com.google.android.accounts.OperationCanceledException; import com.google.android.apps.mytracks.AccountChooser; import com.google.android.apps.mytracks.MyTracks; -import com.google.android.apps.mytracks.MyTracksConstants; +import com.google.android.apps.mytracks.Constants; import android.app.Activity; import android.content.Intent; @@ -104,20 +104,20 @@ public class ModernAuthManager implements AuthManager { if (result.containsKey(AccountManager.KEY_INTENT)) { Intent intent = (Intent) result.get(AccountManager.KEY_INTENT); clearNewTaskFlag(intent); - activity.startActivityForResult(intent, MyTracksConstants.GET_LOGIN); + activity.startActivityForResult(intent, Constants.GET_LOGIN); return; } authToken = result.getString( AccountManager.KEY_AUTHTOKEN); - Log.e(MyTracksConstants.TAG, "Got auth token."); + Log.e(Constants.TAG, "Got auth token."); runWhenFinished(); } catch (OperationCanceledException e) { - Log.e(MyTracksConstants.TAG, "Operation Canceled", e); + Log.e(Constants.TAG, "Operation Canceled", e); } catch (IOException e) { - Log.e(MyTracksConstants.TAG, "IOException", e); + Log.e(Constants.TAG, "IOException", e); } catch (AuthenticatorException e) { - Log.e(MyTracksConstants.TAG, "Authentication Failed", e); + Log.e(Constants.TAG, "Authentication Failed", e); } } }, null /* handler */); @@ -149,9 +149,9 @@ public class ModernAuthManager implements AuthManager { if (results != null) { authToken = results.getStringExtra( AccountManager.KEY_AUTHTOKEN); - Log.w(MyTracksConstants.TAG, "authResult: " + authToken); + Log.w(Constants.TAG, "authResult: " + authToken); } else { - Log.e(MyTracksConstants.TAG, "No auth result results!!"); + Log.e(Constants.TAG, "No auth result results!!"); } runWhenFinished(); return authToken != null; @@ -181,7 +181,7 @@ public class ModernAuthManager implements AuthManager { activity.runOnUiThread(new Runnable() { public void run() { - accountManager.invalidateAuthToken(MyTracksConstants.ACCOUNT_TYPE, + accountManager.invalidateAuthToken(Constants.ACCOUNT_TYPE, authToken); MyTracks.getInstance().getAccountChooser().chooseAccount(activity, new AccountChooser.AccountHandler() { diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/SendToDocs.java b/MyTracks/src/com/google/android/apps/mytracks/io/SendToDocs.java index 1bbbf8cf0..5105285e4 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/io/SendToDocs.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/SendToDocs.java @@ -16,8 +16,7 @@ package com.google.android.apps.mytracks.io; import com.google.android.apps.mytracks.MyTracks; -import com.google.android.apps.mytracks.MyTracksConstants; -import com.google.android.apps.mytracks.MyTracksSettings; +import com.google.android.apps.mytracks.Constants; import com.google.android.apps.mytracks.content.MyTracksProviderUtils; import com.google.android.apps.mytracks.content.Track; import com.google.android.apps.mytracks.io.docs.DocsHelper; @@ -28,7 +27,7 @@ import com.google.android.maps.mytracks.R; import com.google.wireless.gdata.client.GDataClient; import com.google.wireless.gdata.docs.DocumentsClient; import com.google.wireless.gdata.docs.SpreadsheetsClient; -import com.google.wireless.gdata.maps.xml.XmlMapsGDataParserFactory; +import com.google.wireless.gdata.docs.XmlDocsGDataParserFactory; import com.google.wireless.gdata.client.GDataServiceClient; import android.app.Activity; @@ -73,7 +72,7 @@ public class SendToDocs { this.trackId = trackId; SharedPreferences preferences = activity.getSharedPreferences( - MyTracksSettings.SETTINGS_NAME, 0); + Constants.SETTINGS_NAME, 0); if (preferences != null) { metricUnits = preferences.getBoolean(activity.getString(R.string.metric_units_key), @@ -82,7 +81,7 @@ public class SendToDocs { metricUnits = true; } - Log.d(MyTracksConstants.TAG, + Log.d(Constants.TAG, "Sending to Google Docs: trackId = " + trackId); handlerThread = new HandlerThread("SendToGoogleDocs"); handlerThread.start(); @@ -99,12 +98,13 @@ public class SendToDocs { } private void doUpload() { + // TODO statusMessage = activity.getString(R.string.error_sending_to_fusiontables); success = false; try { if (trackId == -1) { - Log.w(MyTracksConstants.TAG, "Cannot get track id."); + Log.w(Constants.TAG, "Cannot get track id."); return; } @@ -112,14 +112,14 @@ public class SendToDocs { Track track = MyTracksProviderUtils.Factory.get(activity).getTrack(trackId); if (track == null) { - Log.w(MyTracksConstants.TAG, "Cannot get track."); + Log.w(Constants.TAG, "Cannot get track."); return; } // Transmit track stats via GData feed: // ------------------------------- - Log.d(MyTracksConstants.TAG, "SendToDocs: Uploading to spreadsheet"); + Log.d(Constants.TAG, "SendToDocs: Uploading to spreadsheet"); success = uploadToDocs(track); if (success) { if (createdNewSpreadSheet) { @@ -132,7 +132,7 @@ public class SendToDocs { } else { statusMessage = activity.getString(R.string.error_sending_to_docs); } - Log.d(MyTracksConstants.TAG, "SendToDocs: Done."); + Log.d(Constants.TAG, "SendToDocs: Done."); } finally { if (onCompletion != null) { activity.runOnUiThread(onCompletion); @@ -173,9 +173,9 @@ public class SendToDocs { try { androidClient = GDataClientFactory.getGDataClient(activity); SpreadsheetsClient gdataClient = new SpreadsheetsClient(androidClient, - new XmlMapsGDataParserFactory(new AndroidXmlParserFactory())); + new XmlDocsGDataParserFactory(new AndroidXmlParserFactory())); trixWrapper.setClient(gdataClient); - Log.d(MyTracksConstants.TAG, + Log.d(Constants.TAG, "GData connection prepared: " + this.docListAuth); String sheetTitle = "My Tracks"; @@ -184,7 +184,7 @@ public class SendToDocs { } DocumentsClient docsGdataClient = new DocumentsClient(androidClient, - new XmlMapsGDataParserFactory(new AndroidXmlParserFactory())); + new XmlDocsGDataParserFactory(new AndroidXmlParserFactory())); docListWrapper.setClient(docsGdataClient); // First try to find the spreadsheet: @@ -193,7 +193,7 @@ public class SendToDocs { spreadsheetId = docsHelper.requestSpreadsheetId(docListWrapper, sheetTitle); } catch (IOException e) { - Log.i(MyTracksConstants.TAG, "Spreadsheet lookup failed.", e); + Log.i(Constants.TAG, "Spreadsheet lookup failed.", e); return false; } @@ -204,14 +204,14 @@ public class SendToDocs { try { Thread.sleep(5000); } catch (InterruptedException e) { - Log.e(MyTracksConstants.TAG, "Sleep interrupted", e); + Log.e(Constants.TAG, "Sleep interrupted", e); } try { spreadsheetId = docsHelper.requestSpreadsheetId(docListWrapper, sheetTitle); } catch (IOException e) { - Log.i(MyTracksConstants.TAG, "2nd spreadsheet lookup failed.", e); + Log.i(Constants.TAG, "2nd spreadsheet lookup failed.", e); return false; } } @@ -219,13 +219,13 @@ public class SendToDocs { // We were unable to find an existing spreadsheet, so create a new one. MyTracks.getInstance().setProgressValue(70); if (spreadsheetId == null) { - Log.i(MyTracksConstants.TAG, "Creating new spreadsheet: " + sheetTitle); + Log.i(Constants.TAG, "Creating new spreadsheet: " + sheetTitle); try { spreadsheetId = docsHelper.createSpreadsheet(activity, docListWrapper, sheetTitle); } catch (IOException e) { - Log.i(MyTracksConstants.TAG, "Failed to create new spreadsheet " + Log.i(Constants.TAG, "Failed to create new spreadsheet " + sheetTitle, e); return false; } @@ -238,19 +238,19 @@ public class SendToDocs { // reported an error. Seems to be a know bug, // see http://code.google.com/p/gdata-issues/issues/detail?id=929 // Try to find the created spreadsheet: - Log.w(MyTracksConstants.TAG, + Log.w(Constants.TAG, "Create might have failed. Trying to find created document."); try { Thread.sleep(5000); } catch (InterruptedException e) { - Log.e(MyTracksConstants.TAG, "Sleep interrupted", e); + Log.e(Constants.TAG, "Sleep interrupted", e); } try { spreadsheetId = docsHelper.requestSpreadsheetId(docListWrapper, sheetTitle); } catch (IOException e) { - Log.i(MyTracksConstants.TAG, "Failed create-failed lookup", e); + Log.i(Constants.TAG, "Failed create-failed lookup", e); return false; } @@ -260,19 +260,19 @@ public class SendToDocs { try { Thread.sleep(5000); } catch (InterruptedException e) { - Log.e(MyTracksConstants.TAG, "Sleep interrupted", e); + Log.e(Constants.TAG, "Sleep interrupted", e); } try { spreadsheetId = docsHelper.requestSpreadsheetId(docListWrapper, sheetTitle); } catch (IOException e) { - Log.i(MyTracksConstants.TAG, "Failed create-failed relookup", e); + Log.i(Constants.TAG, "Failed create-failed relookup", e); return false; } } if (spreadsheetId == null) { - Log.i(MyTracksConstants.TAG, + Log.i(Constants.TAG, "Creating new spreadsheet really failed."); return false; } @@ -286,7 +286,7 @@ public class SendToDocs { throw new IOException("Worksheet ID lookup returned empty"); } } catch (IOException e) { - Log.i(MyTracksConstants.TAG, "Looking up worksheet id failed.", e); + Log.i(Constants.TAG, "Looking up worksheet id failed.", e); return false; } @@ -294,9 +294,9 @@ public class SendToDocs { docsHelper.addTrackRow(activity, trixAuth, spreadsheetId, worksheetId, track, metricUnits); - Log.i(MyTracksConstants.TAG, "Done uploading to docs."); + Log.i(Constants.TAG, "Done uploading to docs."); } catch (IOException e) { - Log.e(MyTracksConstants.TAG, "Unable to upload docs.", e); + Log.e(Constants.TAG, "Unable to upload docs.", e); return false; } finally { if (androidClient != null) { diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/SendToFusionTables.java b/MyTracks/src/com/google/android/apps/mytracks/io/SendToFusionTables.java index 9b9224e3c..eeb769b88 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/io/SendToFusionTables.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/SendToFusionTables.java @@ -15,8 +15,7 @@ */ package com.google.android.apps.mytracks.io; -import com.google.android.apps.mytracks.MyTracksConstants; -import com.google.android.apps.mytracks.MyTracksSettings; +import com.google.android.apps.mytracks.Constants; import com.google.android.apps.mytracks.ProgressIndicator; import com.google.android.apps.mytracks.content.MyTracksProviderUtils; import com.google.android.apps.mytracks.content.Track; @@ -25,8 +24,9 @@ import com.google.android.apps.mytracks.io.gdata.GDataWrapper; import com.google.android.apps.mytracks.io.gdata.GDataWrapper.QueryFunction; import com.google.android.apps.mytracks.stats.DoubleBuffer; import com.google.android.apps.mytracks.stats.TripStatistics; -import com.google.android.apps.mytracks.util.MyTracksUtils; +import com.google.android.apps.mytracks.util.LocationUtils; import com.google.android.apps.mytracks.util.StringUtils; +import com.google.android.apps.mytracks.util.SystemUtils; import com.google.android.apps.mytracks.util.UnitConversions; import com.google.android.maps.mytracks.R; import com.google.api.client.googleapis.GoogleHeaders; @@ -141,7 +141,7 @@ public class SendToFusionTables implements Runnable { this.providerUtils = MyTracksProviderUtils.Factory.get(context); GoogleHeaders headers = new GoogleHeaders(); - headers.setApplicationName("Google-MyTracks-" + MyTracksUtils.getMyTracksVersion(context)); + headers.setApplicationName("Google-MyTracks-" + SystemUtils.getMyTracksVersion(context)); headers.gdataVersion = GDATA_VERSION; transport = new HttpTransport(); @@ -151,7 +151,7 @@ public class SendToFusionTables implements Runnable { @Override public void run() { - Log.d(MyTracksConstants.TAG, "Sending to Fusion tables: trackId = " + trackId); + Log.d(Constants.TAG, "Sending to Fusion tables: trackId = " + trackId); doUpload(); } @@ -204,7 +204,7 @@ public class SendToFusionTables implements Runnable { } statusMessageId = R.string.status_new_fusiontable_has_been_created; - Log.d(MyTracksConstants.TAG, "SendToFusionTables: Done: " + success); + Log.d(Constants.TAG, "SendToFusionTables: Done: " + success); progressIndicator.setProgressValue(PROGRESS_COMPLETE); } finally { @@ -228,14 +228,14 @@ public class SendToFusionTables implements Runnable { * @return true in case of success. */ private boolean createNewTable(Track track) { - Log.d(MyTracksConstants.TAG, "Creating a new fusion table."); + Log.d(Constants.TAG, "Creating a new fusion table."); String query = "CREATE TABLE '" + sqlEscape(track.getName()) + "' (name:STRING,description:STRING,geometry:LOCATION,marker:STRING)"; return runUpdate(query); } private boolean makeTableUnlisted(String tableId) { - Log.d(MyTracksConstants.TAG, "Setting visibility to unlisted."); + Log.d(Constants.TAG, "Setting visibility to unlisted."); String query = "UPDATE TABLE " + tableId + " SET VISIBILITY = UNLISTED"; return runUpdate(query); } @@ -274,7 +274,7 @@ public class SendToFusionTables implements Runnable { */ private boolean createNewPoint(String name, String description, Location location, String marker) { - Log.d(MyTracksConstants.TAG, "Creating a new row with a point."); + Log.d(Constants.TAG, "Creating a new row with a point."); String query = "INSERT INTO " + tableId + " (name,description,geometry,marker) VALUES " + values(name, description, getKmlPoint(location), marker); return runUpdate(query); @@ -287,7 +287,7 @@ public class SendToFusionTables implements Runnable { * @return true in case of success. */ private boolean createNewLineString(Track track) { - Log.d(MyTracksConstants.TAG, "Creating a new row with a point."); + Log.d(Constants.TAG, "Creating a new row with a point."); String query = "INSERT INTO " + tableId + " (name,description,geometry) VALUES " + values(track.getName(), track.getDescription(), getKmlLineString(track)); @@ -296,7 +296,7 @@ public class SendToFusionTables implements Runnable { private boolean uploadAllTrackPoints(final Track track, String originalDescription) { - SharedPreferences preferences = context.getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0); + SharedPreferences preferences = context.getSharedPreferences(Constants.SETTINGS_NAME, 0); boolean metricUnits = true; if (preferences != null) { metricUnits = preferences.getBoolean(context.getString(R.string.metric_units_key), true); @@ -305,7 +305,7 @@ public class SendToFusionTables implements Runnable { Cursor locationsCursor = providerUtils.getLocationsCursor(track.getId(), 0, -1, false); try { if (!locationsCursor.moveToFirst()) { - Log.w(MyTracksConstants.TAG, "Unable to get any points to upload"); + Log.w(Constants.TAG, "Unable to get any points to upload"); return false; } @@ -318,14 +318,14 @@ public class SendToFusionTables implements Runnable { // Limit the number of elevation readings. Ideally we would want around 250. int elevationSamplingFrequency = Math.max(1, (int) (totalLocations / 250.0)); - Log.d(MyTracksConstants.TAG, + Log.d(Constants.TAG, "Using elevation sampling factor: " + elevationSamplingFrequency + " on " + totalLocations); double totalDistance = 0; Vector distances = new Vector(); Vector elevations = new Vector(); - DoubleBuffer elevationBuffer = new DoubleBuffer(MyTracksConstants.ELEVATION_SMOOTHING_FACTOR); + DoubleBuffer elevationBuffer = new DoubleBuffer(Constants.ELEVATION_SMOOTHING_FACTOR); List locations = new ArrayList(MAX_POINTS_PER_UPLOAD); Location lastLocation = null; @@ -344,7 +344,7 @@ public class SendToFusionTables implements Runnable { } // Add to the elevation profile. - if (loc != null && MyTracksUtils.isValidLocation(loc)) { + if (loc != null && LocationUtils.isValidLocation(loc)) { // All points go into the smoothing buffer... elevationBuffer.setNext(metricUnits ? loc.getAltitude() : loc.getAltitude() * UnitConversions.M_TO_FT); @@ -447,7 +447,7 @@ public class SendToFusionTables implements Runnable { int numLocations = locations.size(); if (numLocations < 2) { - Log.d(MyTracksConstants.TAG, "Not preparing/uploading too few points"); + Log.d(Constants.TAG, "Not preparing/uploading too few points"); totalLocationsUploaded += numLocations; return true; } @@ -463,16 +463,16 @@ public class SendToFusionTables implements Runnable { context.getString(R.string.part), totalSegmentsUploaded)); } totalSegmentsUploaded++; - Log.d(MyTracksConstants.TAG, + Log.d(Constants.TAG, "SendToFusionTables: Prepared feature for upload w/ " + splitTrack.getLocations().size() + " points."); // Transmit tracks via GData feed: // ------------------------------- - Log.d(MyTracksConstants.TAG, + Log.d(Constants.TAG, "SendToFusionTables: Uploading to table " + tableId + " w/ auth " + auth); if (!uploadTrackPoints(splitTrack)) { - Log.e(MyTracksConstants.TAG, "Uploading failed"); + Log.e(Constants.TAG, "Uploading failed"); return false; } } @@ -518,7 +518,7 @@ public class SendToFusionTables implements Runnable { // Close up the last segment. prepareTrackSegment(segment, splitTracks); - Log.d(MyTracksConstants.TAG, + Log.d(Constants.TAG, "MyTracksSendToFusionTables: Starting new track segment..."); startNewTrackSegment = false; segment = new Track(); @@ -566,12 +566,12 @@ public class SendToFusionTables implements Runnable { * Decimate to 2 meter precision. Fusion tables doesn't like too many * points: */ - MyTracksUtils.decimate(segment, 2.0); + LocationUtils.decimate(segment, 2.0); /* If the track still has > 2500 points, split it in pieces: */ final int maxPoints = 2500; if (segment.getLocations().size() > maxPoints) { - splitTracks.addAll(MyTracksUtils.split(segment, maxPoints)); + splitTracks.addAll(LocationUtils.split(segment, maxPoints)); } else if (segment.getLocations().size() >= 2) { splitTracks.add(segment); } @@ -581,7 +581,7 @@ public class SendToFusionTables implements Runnable { int numLocations = splitTrack.getLocations().size(); if (numLocations < 2) { // Need at least two points for a polyline: - Log.w(MyTracksConstants.TAG, "Not uploading too few points"); + Log.w(Constants.TAG, "Not uploading too few points"); return true; } return createNewLineString(splitTrack); @@ -604,14 +604,14 @@ public class SendToFusionTables implements Runnable { try { c = providerUtils.getWaypointsCursor( track.getId(), 0, - MyTracksConstants.MAX_LOADED_WAYPOINTS_POINTS); + Constants.MAX_LOADED_WAYPOINTS_POINTS); if (c != null) { if (c.moveToFirst()) { // This will skip the 1st waypoint (it carries the stats for the // last segment). while (c.moveToNext()) { Waypoint wpt = providerUtils.createWaypoint(c); - Log.d(MyTracksConstants.TAG, "SendToFusionTables: Creating waypoint."); + Log.d(Constants.TAG, "SendToFusionTables: Creating waypoint."); success = createNewPoint(wpt.getName(), wpt.getDescription(), wpt.getLocation(), MARKER_TYPE_WAYPOINT); if (!success) { @@ -621,7 +621,7 @@ public class SendToFusionTables implements Runnable { } } if (!success) { - Log.w(MyTracksConstants.TAG, "SendToFusionTables: upload waypoints failed."); + Log.w(Constants.TAG, "SendToFusionTables: upload waypoints failed."); } return success; } finally { @@ -656,7 +656,7 @@ public class SendToFusionTables implements Runnable { wrapper.setAuthManager(auth); wrapper.setRetryOnAuthFailure(true); wrapper.setClient(transport); - Log.d(MyTracksConstants.TAG, "GData connection prepared: " + this.auth); + Log.d(Constants.TAG, "GData connection prepared: " + this.auth); wrapper.runQuery(new QueryFunction() { @Override public void query(HttpTransport client) @@ -671,7 +671,7 @@ public class SendToFusionTables implements Runnable { isc.inputStream = new ByteArrayInputStream(Strings.toBytesUtf8(sql)); request.content = isc; - Log.d(MyTracksConstants.TAG, "Running update query " + url.toString() + ": " + sql); + Log.d(Constants.TAG, "Running update query " + url.toString() + ": " + sql); HttpResponse response; try { response = request.execute(); @@ -686,12 +686,12 @@ public class SendToFusionTables implements Runnable { String[] lines = s.split(Strings.LINE_SEPARATOR); if (lines[0].equals("tableid")) { tableId = lines[1]; - Log.d(MyTracksConstants.TAG, "tableId = " + tableId); + Log.d(Constants.TAG, "tableId = " + tableId); } else { - Log.w(MyTracksConstants.TAG, "Unrecognized response: " + lines[0]); + Log.w(Constants.TAG, "Unrecognized response: " + lines[0]); } } else { - Log.d(MyTracksConstants.TAG, "Query failed: " + response.statusMessage + " (" + + Log.d(Constants.TAG, "Query failed: " + response.statusMessage + " (" + response.statusCode + ")"); throw new GDataWrapper.HttpException(response.statusCode, response.statusMessage); } diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/SendToMyMaps.java b/MyTracks/src/com/google/android/apps/mytracks/io/SendToMyMaps.java new file mode 100644 index 000000000..ffc895da6 --- /dev/null +++ b/MyTracks/src/com/google/android/apps/mytracks/io/SendToMyMaps.java @@ -0,0 +1,455 @@ +/* + * Copyright 2008 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.google.android.apps.mytracks.io; + +import static com.google.android.apps.mytracks.Constants.TAG; +import com.google.android.apps.mytracks.Constants; +import com.google.android.apps.mytracks.ProgressIndicator; +import com.google.android.apps.mytracks.content.MyTracksProviderUtils; +import com.google.android.apps.mytracks.content.Track; +import com.google.android.apps.mytracks.content.Waypoint; +import com.google.android.apps.mytracks.io.mymaps.MapsFacade; +import com.google.android.apps.mytracks.stats.DoubleBuffer; +import com.google.android.apps.mytracks.stats.TripStatistics; +import com.google.android.apps.mytracks.util.LocationUtils; +import com.google.android.apps.mytracks.util.StringUtils; +import com.google.android.apps.mytracks.util.UnitConversions; +import com.google.android.maps.mytracks.R; + +import android.app.Activity; +import android.content.SharedPreferences; +import android.database.Cursor; +import android.location.Location; +import android.util.Log; + +import java.util.ArrayList; +import java.util.List; +import java.util.Vector; + +/** + * A helper class used to transmit tracks to Google MyMaps. + * A new instance should be used for each upload. + * + * IMPORTANT: while this code is Apache-licensed, please notice that usage of + * the Google Maps servers through this API is only allowed for the My Tracks + * application. Other applications looking to upload maps data should look + * into using the Fusion Tables API. + * + * @author Leif Hendrik Wilden + */ +public class SendToMyMaps implements Runnable { + + public static final String NEW_MAP_ID = "new"; + private static final int MAX_POINTS_PER_UPLOAD = 2048; + + private final Activity context; + private final AuthManager auth; + private final long trackId; + private final ProgressIndicator progressIndicator; + private final OnSendCompletedListener onCompletion; + private final StringUtils stringUtils; + private final MyTracksProviderUtils providerUtils; + private String mapId; + + private MapsFacade mapsClient; + + // Progress status + private int totalLocationsRead; + private int totalLocationsPrepared; + private int totalLocationsUploaded; + private int totalLocations; + private int totalSegmentsUploaded; + + public interface OnSendCompletedListener { + void onSendCompleted(String mapId, boolean success, int statusMessage); + } + + public SendToMyMaps(Activity context, String mapId, AuthManager auth, + long trackId, ProgressIndicator progressIndicator, + OnSendCompletedListener onCompletion) { + this.context = context; + this.mapId = mapId; + this.auth = auth; + this.trackId = trackId; + this.progressIndicator = progressIndicator; + this.onCompletion = onCompletion; + this.stringUtils = new StringUtils(context); + this.providerUtils = MyTracksProviderUtils.Factory.get(context); + } + + @Override + public void run() { + Log.d(TAG, "Sending to MyMaps: trackId = " + trackId); + doUpload(); + } + + private void doUpload() { + int statusMessageId = R.string.error_sending_to_mymap; + boolean success = true; + try { + progressIndicator.setProgressValue(1); + progressIndicator.setProgressMessage( + R.string.progress_message_reading_track); + + // Get the track meta-data + Track track = providerUtils.getTrack(trackId); + String originalDescription = track.getDescription(); + track.setDescription("

" + track.getDescription() + "

" + + stringUtils.generateTrackDescription(track, null, null) + "

"); + + mapsClient = new MapsFacade(context, auth); + + // Create a new map if necessary: + boolean isNewMap = mapId.equals(NEW_MAP_ID); + if (isNewMap) { + SharedPreferences preferences = context.getSharedPreferences( + Constants.SETTINGS_NAME, 0); + boolean mapPublic = true; + if (preferences != null) { + mapPublic = preferences.getBoolean( + context.getString(R.string.default_map_public_key), true); + } + + progressIndicator.setProgressMessage( + R.string.progress_message_creating_map); + + StringBuilder mapIdBuilder = new StringBuilder(); + success = mapsClient.createNewMap( + track.getName(), track.getCategory(), originalDescription, mapPublic, mapIdBuilder); + mapId = mapIdBuilder.toString(); + } + + // Upload all of the segments of the track plus start/end markers + if (success) { + success = uploadAllTrackPoints(track, originalDescription); + } + + // Put waypoints. + if (success) { + Cursor c = providerUtils.getWaypointsCursor( + track.getId(), 0, + Constants.MAX_LOADED_WAYPOINTS_POINTS); + if (c != null) { + try { + 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); + while (c.moveToNext()) { + Waypoint wpt = providerUtils.createWaypoint(c); + waypoints.add(wpt); + } + + success = mapsClient.uploadWaypoints(mapId, waypoints); + } + } finally { + c.close(); + } + } else { + success = false; + } + + if (!success) { + Log.w(TAG, "SendToMyMaps: upload waypoints failed."); + } + } + + if (success) { + statusMessageId = isNewMap + ? R.string.status_new_mymap_has_been_created + : R.string.status_tracks_have_been_uploaded; + } + Log.d(TAG, "SendToMyMaps: Done: " + success); + progressIndicator.setProgressValue(100); + } finally { + if (mapsClient != null) { + mapsClient.cleanUp(); + } + + final boolean finalSuccess = success; + final int finalStatusMessageId = statusMessageId; + context.runOnUiThread(new Runnable() { + public void run() { + if (onCompletion != null) { + onCompletion.onSendCompleted( + mapId, finalSuccess, finalStatusMessageId); + } + } + }); + } + } + + private boolean uploadAllTrackPoints( + final Track track, String originalDescription) { + SharedPreferences preferences = context.getSharedPreferences( + Constants.SETTINGS_NAME, 0); + boolean metricUnits = true; + if (preferences != null) { + metricUnits = + preferences.getBoolean(context.getString(R.string.metric_units_key), + true); + } + + Cursor locationsCursor = + providerUtils.getLocationsCursor(track.getId(), 0, -1, false); + try { + if (!locationsCursor.moveToFirst()) { + Log.w(TAG, "Unable to get any points to upload"); + return false; + } + + totalLocationsRead = 0; + totalLocationsPrepared = 0; + totalLocationsUploaded = 0; + totalLocations = locationsCursor.getCount(); + totalSegmentsUploaded = 0; + + // Limit the number of elevation readings. Ideally we would want around 250. + int elevationSamplingFrequency = + Math.max(1, (int) (totalLocations / 250.0)); + Log.d(TAG, + "Using elevation sampling factor: " + elevationSamplingFrequency + + " on " + totalLocations); + double totalDistance = 0; + + Vector distances = new Vector(); + Vector elevations = new Vector(); + DoubleBuffer elevationBuffer = + new DoubleBuffer(Constants.ELEVATION_SMOOTHING_FACTOR); + + List locations = new ArrayList(MAX_POINTS_PER_UPLOAD); + progressIndicator.setProgressMessage( + R.string.progress_message_reading_track); + Location lastLocation = null; + do { + if (totalLocationsRead % 100 == 0) { + updateProgress(); + } + + Location loc = providerUtils.createLocation(locationsCursor); + locations.add(loc); + + if (totalLocationsRead == 0) { + // Put a marker at the first point of the first valid segment: + mapsClient.uploadMarker(mapId, track.getName(), track.getDescription(), loc, true); + } + + // Add to the elevation profile. + if (loc != null && LocationUtils.isValidLocation(loc)) { + // All points go into the smoothing buffer... + elevationBuffer.setNext(metricUnits ? loc.getAltitude() + : loc.getAltitude() * UnitConversions.M_TO_FT); + if (lastLocation != null) { + double dist = lastLocation.distanceTo(loc); + totalDistance += dist; + } + + // ...but only a few points are really used to keep the url short. + if (totalLocationsRead % elevationSamplingFrequency == 0) { + distances.add(totalDistance); + elevations.add(elevationBuffer.getAverage()); + } + } + + // If the location was not valid, it's a segment split, so make sure the + // distance between the previous segment and the new one is not accounted + // for in the next iteration. + lastLocation = loc; + + // Every now and then, upload the accumulated points + if (totalLocationsRead % MAX_POINTS_PER_UPLOAD + == MAX_POINTS_PER_UPLOAD - 1) { + if (!prepareAndUploadPoints(track, locations)) { + return false; + } + } + + totalLocationsRead++; + } while (locationsCursor.moveToNext()); + + // Do a final upload with what's left + if (!prepareAndUploadPoints(track, locations)) { + return false; + } + + // Put an end marker at the last point of the last valid segment: + if (lastLocation != null) { + track.setDescription("

" + originalDescription + "

" + + stringUtils.generateTrackDescription( + track, distances, elevations) + + "

"); + return mapsClient.uploadMarker(mapId, track.getName(), track.getDescription(), + lastLocation, false); + } + + return true; + } finally { + locationsCursor.close(); + } + } + + + private boolean prepareAndUploadPoints(Track track, List locations) { + progressIndicator.setProgressMessage( + R.string.progress_message_preparing_track); + updateProgress(); + + int numLocations = locations.size(); + if (numLocations < 2) { + Log.d(TAG, "Not preparing/uploading too few points"); + totalLocationsUploaded += numLocations; + return true; + } + + // Prepare/pre-process the points + ArrayList splitTracks = prepareLocations(track, locations); + + // Start uploading them + progressIndicator.setProgressMessage( + R.string.progress_message_sending_mymaps); + for (Track splitTrack : splitTracks) { + if (totalSegmentsUploaded > 1) { + splitTrack.setName(splitTrack.getName() + " " + + String.format( + context.getString(R.string.part), totalSegmentsUploaded)); + } + totalSegmentsUploaded++; + Log.d(TAG, + "SendToMyMaps: Prepared feature for upload w/ " + + splitTrack.getLocations().size() + " points."); + + // Transmit tracks via GData feed: + // ------------------------------- + Log.d(TAG, + "SendToMyMaps: Uploading to map " + mapId + " w/ auth " + auth); + if (!mapsClient.uploadTrackPoints(mapId, splitTrack.getName(), splitTrack.getLocations())) { + Log.e(TAG, "Uploading failed"); + return false; + } + } + + locations.clear(); + totalLocationsUploaded += numLocations; + updateProgress(); + return true; + } + + /** + * Prepares a buffer of locations for transmission to google maps. + * + * @param track the original track with meta data + * @param buffer a buffer of locations on the track + * @return an array of tracks each with a sub section of the points in the + * original buffer + */ + private ArrayList prepareLocations( + Track track, Iterable locations) { + ArrayList splitTracks = new ArrayList(); + + // Create segments from each full track: + Track segment = new Track(); + TripStatistics segmentStats = segment.getStatistics(); + TripStatistics trackStats = track.getStatistics(); + segment.setId(track.getId()); + segment.setName(track.getName()); + segment.setDescription(/* track.getDescription() */ ""); + segment.setCategory(track.getCategory()); + segmentStats.setStartTime(trackStats.getStartTime()); + segmentStats.setStopTime(trackStats.getStopTime()); + boolean startNewTrackSegment = false; + for (Location loc : locations) { + if (totalLocationsPrepared % 100 == 0) { + updateProgress(); + } + if (loc.getLatitude() > 90) { + startNewTrackSegment = true; + } + + if (startNewTrackSegment) { + // Close up the last segment. + prepareTrackSegment(segment, splitTracks); + + Log.d(TAG, + "MyTracksSendToMyMaps: Starting new track segment..."); + startNewTrackSegment = false; + segment = new Track(); + segment.setId(track.getId()); + segment.setName(track.getName()); + segment.setDescription(/* track.getDescription() */ ""); + segment.setCategory(track.getCategory()); + } + + if (loc.getLatitude() <= 90) { + segment.addLocation(loc); + if (segmentStats.getStartTime() < 0) { + segmentStats.setStartTime(loc.getTime()); + } + } + totalLocationsPrepared++; + } + + prepareTrackSegment(segment, splitTracks); + + return splitTracks; + } + + /** + * Prepares a track segment for sending to google maps. + * The main steps are: + * - correcting end time + * - decimating locations + * - splitting into smaller tracks. + * + * The final track pieces will be put in the array list splitTracks. + * + * @param segment the original segment of the track + * @param splitTracks an array of smaller track segments + */ + private void prepareTrackSegment( + Track segment, ArrayList splitTracks) { + TripStatistics segmentStats = segment.getStatistics(); + if (segmentStats.getStopTime() < 0 + && segment.getLocations().size() > 0) { + segmentStats.setStopTime(segment.getLocations().size() - 1); + } + + /* + * Decimate to 2 meter precision. Mapshop doesn't like too many + * points: + */ + LocationUtils.decimate(segment, 2.0); + + /* It the track still has > 500 points, split it in pieces: */ + if (segment.getLocations().size() > 500) { + splitTracks.addAll(LocationUtils.split(segment, 500)); + } else if (segment.getLocations().size() >= 2) { + splitTracks.add(segment); + } + } + + /** + * Sets the current upload progress. + */ + private void updateProgress() { + // The percent of the total that represents the completed part of this + // segment. + int totalPercentage = + (totalLocationsRead + totalLocationsPrepared + totalLocationsUploaded) + / (totalLocations * 3); + totalPercentage = Math.min(99, totalPercentage); + progressIndicator.setProgressValue(totalPercentage); + } +} diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/backup/BackupActivityHelper.java b/MyTracks/src/com/google/android/apps/mytracks/io/backup/BackupActivityHelper.java index e66cd29d8..0de343479 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/io/backup/BackupActivityHelper.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/backup/BackupActivityHelper.java @@ -16,7 +16,7 @@ package com.google.android.apps.mytracks.io.backup; import com.google.android.apps.mytracks.MyTracks; -import com.google.android.apps.mytracks.MyTracksConstants; +import com.google.android.apps.mytracks.Constants; import com.google.android.apps.mytracks.util.FileUtils; import com.google.android.maps.mytracks.R; @@ -97,7 +97,7 @@ public class BackupActivityHelper { backup.writeToDefaultFile(); showToast(R.string.io_write_finished); } catch (IOException e) { - Log.e(MyTracksConstants.TAG, "Failed to write backup", e); + Log.e(Constants.TAG, "Failed to write backup", e); showToast(R.string.io_write_failed); } finally { dismissDialog(progressDialog); @@ -199,7 +199,7 @@ public class BackupActivityHelper { backup.restoreFromDate(date); showToast(R.string.io_read_finished); } catch (IOException e) { - Log.e(MyTracksConstants.TAG, "Failed to restore backup", e); + Log.e(Constants.TAG, "Failed to restore backup", e); showToast(R.string.io_read_failed); } finally { dismissDialog(progressDialog); diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/backup/ExternalFileBackup.java b/MyTracks/src/com/google/android/apps/mytracks/io/backup/ExternalFileBackup.java index cc346e2f7..1fbcf9485 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/io/backup/ExternalFileBackup.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/backup/ExternalFileBackup.java @@ -15,8 +15,7 @@ */ package com.google.android.apps.mytracks.io.backup; -import com.google.android.apps.mytracks.MyTracksConstants; -import com.google.android.apps.mytracks.MyTracksSettings; +import com.google.android.apps.mytracks.Constants; import com.google.android.apps.mytracks.content.TrackPointsColumns; import com.google.android.apps.mytracks.content.TracksColumns; import com.google.android.apps.mytracks.content.WaypointsColumns; @@ -88,7 +87,7 @@ class ExternalFileBackup { private File getBackupsDirectory(boolean create) { String dirName = fileUtils.buildExternalDirectoryPath(BACKUPS_SUBDIR); final File dir = new File(dirName); - Log.d(MyTracksConstants.TAG, "Dir: " + dir.getAbsolutePath()); + Log.d(Constants.TAG, "Dir: " + dir.getAbsolutePath()); if (create) { // Try to create - if that fails, return null return fileUtils.ensureDirectoryExists(dir) ? dir : null; @@ -147,7 +146,7 @@ class ExternalFileBackup { * Synchronously writes a backup to the given file. */ private void writeToFile(File outputFile) throws IOException { - Log.d(MyTracksConstants.TAG, + Log.d(Constants.TAG, "Writing backup to file " + outputFile.getAbsolutePath()); // Create all the auxiliary classes that will do the writing @@ -201,7 +200,7 @@ class ExternalFileBackup { // Dump preferences SharedPreferences preferences = - context.getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0); + context.getSharedPreferences(Constants.SETTINGS_NAME, 0); preferencesHelper.exportPreferences(preferences, outWriter); } catch (IOException e) { // We tried to delete the partially created file, but do nothing @@ -219,7 +218,7 @@ class ExternalFileBackup { * Synchronously restores the backup from the given file. */ private void restoreFromFile(File inputFile) throws IOException { - Log.d(MyTracksConstants.TAG, + Log.d(Constants.TAG, "Restoring from file " + inputFile.getAbsolutePath()); PreferenceBackupHelper preferencesHelper = new PreferenceBackupHelper(); @@ -253,7 +252,7 @@ class ExternalFileBackup { // Restore preferences SharedPreferences preferences = - context.getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0); + context.getSharedPreferences(Constants.SETTINGS_NAME, 0); preferencesHelper.importPreferences(reader, preferences); } finally { compressedStream.close(); diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/backup/MyTracksBackupAgent.java b/MyTracks/src/com/google/android/apps/mytracks/io/backup/MyTracksBackupAgent.java index f9bb2f352..4438ca47f 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/io/backup/MyTracksBackupAgent.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/backup/MyTracksBackupAgent.java @@ -15,8 +15,7 @@ */ package com.google.android.apps.mytracks.io.backup; -import com.google.android.apps.mytracks.MyTracksConstants; -import com.google.android.apps.mytracks.MyTracksSettings; +import com.google.android.apps.mytracks.Constants; import android.app.backup.BackupAgent; import android.app.backup.BackupDataInput; @@ -40,12 +39,12 @@ public class MyTracksBackupAgent extends BackupAgent { @Override public void onBackup(ParcelFileDescriptor oldState, BackupDataOutput data, ParcelFileDescriptor newState) throws IOException { - Log.i(MyTracksConstants.TAG, "Performing backup"); + Log.i(Constants.TAG, "Performing backup"); SharedPreferences preferences = this.getSharedPreferences( - MyTracksSettings.SETTINGS_NAME, 0); + Constants.SETTINGS_NAME, 0); backupPreferences(data, preferences); - Log.i(MyTracksConstants.TAG, "Backup complete"); + Log.i(Constants.TAG, "Backup complete"); } private void backupPreferences(BackupDataOutput data, @@ -63,18 +62,18 @@ public class MyTracksBackupAgent extends BackupAgent { @Override public void onRestore(BackupDataInput data, int appVersionCode, ParcelFileDescriptor newState) throws IOException { - Log.i(MyTracksConstants.TAG, "Restoring from backup"); + Log.i(Constants.TAG, "Restoring from backup"); while (data.readNextHeader()) { String key = data.getKey(); - Log.d(MyTracksConstants.TAG, "Restoring entity " + key); + Log.d(Constants.TAG, "Restoring entity " + key); if (key.equals(PREFERENCES_ENTITY)) { restorePreferences(data); } else { - Log.e(MyTracksConstants.TAG, "Found unknown backup entity: " + key); + Log.e(Constants.TAG, "Found unknown backup entity: " + key); data.skipEntityData(); } } - Log.i(MyTracksConstants.TAG, "Done restoring from backup"); + Log.i(Constants.TAG, "Done restoring from backup"); } /** @@ -92,7 +91,7 @@ public class MyTracksBackupAgent extends BackupAgent { } SharedPreferences preferences = this.getSharedPreferences( - MyTracksSettings.SETTINGS_NAME, 0); + Constants.SETTINGS_NAME, 0); PreferenceBackupHelper importer = createPreferenceBackupHelper(); importer.importPreferences(dataBuffer, preferences); } diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/docs/DocsHelper.java b/MyTracks/src/com/google/android/apps/mytracks/io/docs/DocsHelper.java index e46a2573d..ce9752f39 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/io/docs/DocsHelper.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/docs/DocsHelper.java @@ -15,7 +15,7 @@ */ package com.google.android.apps.mytracks.io.docs; -import com.google.android.apps.mytracks.MyTracksConstants; +import com.google.android.apps.mytracks.Constants; import com.google.android.apps.mytracks.content.Track; import com.google.android.apps.mytracks.io.AuthManager; import com.google.android.apps.mytracks.io.gdata.GDataWrapper; @@ -144,7 +144,7 @@ public class DocsHelper { String id = result.substring( idStringStart + DOCS_SPREADSHEET_URL.length(), idTagCloseIndex); - Log.i(MyTracksConstants.TAG, "Created new spreadsheet: " + id); + Log.i(Constants.TAG, "Created new spreadsheet: " + id); idSaver.set(id); }}); @@ -184,7 +184,7 @@ public class DocsHelper { while (listParser.hasMoreData()) { Entry entry = listParser.readNextEntry(null); String entryTitle = entry.getTitle(); - Log.i(MyTracksConstants.TAG, "Found docs entry: " + entryTitle); + Log.i(Constants.TAG, "Found docs entry: " + entryTitle); if (entryTitle.equals(title)) { String entryId = entry.getId(); int lastSlash = entryId.lastIndexOf('/'); @@ -233,7 +233,7 @@ public class DocsHelper { trixWrapper.getAuthManager().getAuthToken()); sheetParser.init(); if (!sheetParser.hasMoreData()) { - Log.i(MyTracksConstants.TAG, "Found no worksheets"); + Log.i(Constants.TAG, "Found no worksheets"); return; } @@ -308,7 +308,7 @@ public class DocsHelper { if (track.getMapId().length() > 0) { tagBuilder.append("map", String.format("%s?msa=0&msid=%s", - MyTracksConstants.MAPSHOP_BASE_URL, track.getMapId())); + Constants.MAPSHOP_BASE_URL, track.getMapId())); } String postText = new StringBuilder() @@ -319,14 +319,14 @@ public class DocsHelper { .append("") .toString(); - Log.i(MyTracksConstants.TAG, + Log.i(Constants.TAG, "Inserting at: " + spreadsheetId + " => " + worksheetUri); - Log.i(MyTracksConstants.TAG, postText); + Log.i(Constants.TAG, postText); writeRowData(trixAuth, worksheetUri, postText); - Log.i(MyTracksConstants.TAG, "Post finished."); + Log.i(Constants.TAG, "Post finished."); } /** @@ -358,7 +358,7 @@ public class DocsHelper { String line; while ((line = rd.readLine()) != null) { // Process line. - Log.i(MyTracksConstants.TAG, "r: " + line); + Log.i(Constants.TAG, "r: " + line); } wr.close(); rd.close(); diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/CsvTrackWriter.java b/MyTracks/src/com/google/android/apps/mytracks/io/file/CsvTrackWriter.java similarity index 97% rename from MyTracks/src/com/google/android/apps/mytracks/io/CsvTrackWriter.java rename to MyTracks/src/com/google/android/apps/mytracks/io/file/CsvTrackWriter.java index 4cfcdbe4f..0cf49dfe8 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/io/CsvTrackWriter.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/file/CsvTrackWriter.java @@ -13,11 +13,11 @@ * License for the specific language governing permissions and limitations under * the License. */ -package com.google.android.apps.mytracks.io; +package com.google.android.apps.mytracks.io.file; import com.google.android.apps.mytracks.content.Track; import com.google.android.apps.mytracks.content.Waypoint; -import com.google.android.apps.mytracks.io.TrackWriterFactory.TrackFileFormat; +import com.google.android.apps.mytracks.io.file.TrackWriterFactory.TrackFileFormat; import android.location.Location; diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/GpxImporter.java b/MyTracks/src/com/google/android/apps/mytracks/io/file/GpxImporter.java similarity index 97% rename from MyTracks/src/com/google/android/apps/mytracks/io/GpxImporter.java rename to MyTracks/src/com/google/android/apps/mytracks/io/file/GpxImporter.java index 02f5af832..66afa09e1 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/io/GpxImporter.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/file/GpxImporter.java @@ -13,13 +13,13 @@ * License for the specific language governing permissions and limitations under * the License. */ -package com.google.android.apps.mytracks.io; +package com.google.android.apps.mytracks.io.file; -import com.google.android.apps.mytracks.MyTracksConstants; +import com.google.android.apps.mytracks.Constants; import com.google.android.apps.mytracks.content.MyTracksProviderUtils; import com.google.android.apps.mytracks.content.Track; import com.google.android.apps.mytracks.stats.TripStatisticsBuilder; -import com.google.android.apps.mytracks.util.MyTracksUtils; +import com.google.android.apps.mytracks.util.LocationUtils; import com.google.android.apps.mytracks.util.StringUtils; import android.location.Location; @@ -174,7 +174,7 @@ public class GpxImporter extends DefaultHandler { parser.parse(is, handler); long end = System.currentTimeMillis(); - Log.d(MyTracksConstants.TAG, "Total import time: " + (end - start) + "ms"); + Log.d(Constants.TAG, "Total import time: " + (end - start) + "ms"); trackIds = handler.getImportedTrackIds(); } finally { @@ -362,7 +362,7 @@ public class GpxImporter extends DefaultHandler { * @throws SAXException - thrown if track point is invalid */ private void onTrackPointElementEnd() throws SAXException { - if (MyTracksUtils.isValidLocation(location)) { + if (LocationUtils.isValidLocation(location)) { if (statsBuilder == null) { // first point did not have a time, start stats builder without it statsBuilder = new TripStatisticsBuilder(0); @@ -462,7 +462,7 @@ public class GpxImporter extends DefaultHandler { // check for negative time change if (timeDifference < 0) { - Log.w(MyTracksConstants.TAG, "Found negative time change."); + Log.w(Constants.TAG, "Found negative time change."); } else { // We don't have a speed and bearing in GPX, make something up from diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/GpxTrackWriter.java b/MyTracks/src/com/google/android/apps/mytracks/io/file/GpxTrackWriter.java similarity index 72% rename from MyTracks/src/com/google/android/apps/mytracks/io/GpxTrackWriter.java rename to MyTracks/src/com/google/android/apps/mytracks/io/file/GpxTrackWriter.java index 65636ce54..88fde6c3c 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/io/GpxTrackWriter.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/file/GpxTrackWriter.java @@ -1,23 +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.io; +package com.google.android.apps.mytracks.io.file; import com.google.android.apps.mytracks.content.Track; import com.google.android.apps.mytracks.content.Waypoint; -import com.google.android.apps.mytracks.io.TrackWriterFactory.TrackFileFormat; +import com.google.android.apps.mytracks.io.file.TrackWriterFactory.TrackFileFormat; import com.google.android.apps.mytracks.util.StringUtils; import android.location.Location; @@ -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/KmlTrackWriter.java b/MyTracks/src/com/google/android/apps/mytracks/io/file/KmlTrackWriter.java similarity index 98% rename from MyTracks/src/com/google/android/apps/mytracks/io/KmlTrackWriter.java rename to MyTracks/src/com/google/android/apps/mytracks/io/file/KmlTrackWriter.java index 8ee29c1ee..2b6d980b6 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/io/KmlTrackWriter.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/file/KmlTrackWriter.java @@ -13,11 +13,11 @@ * License for the specific language governing permissions and limitations under * the License. */ -package com.google.android.apps.mytracks.io; +package com.google.android.apps.mytracks.io.file; import com.google.android.apps.mytracks.content.Track; import com.google.android.apps.mytracks.content.Waypoint; -import com.google.android.apps.mytracks.io.TrackWriterFactory.TrackFileFormat; +import com.google.android.apps.mytracks.io.file.TrackWriterFactory.TrackFileFormat; import com.google.android.apps.mytracks.util.StringUtils; import android.content.Context; diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/TcxTrackWriter.java b/MyTracks/src/com/google/android/apps/mytracks/io/file/TcxTrackWriter.java similarity index 61% rename from MyTracks/src/com/google/android/apps/mytracks/io/TcxTrackWriter.java rename to MyTracks/src/com/google/android/apps/mytracks/io/file/TcxTrackWriter.java index dfd3513ad..eaf45a7cc 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/io/TcxTrackWriter.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/file/TcxTrackWriter.java @@ -1,27 +1,29 @@ /* * Copyright 2010 Google Inc. - * + * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of * the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations under * the License. */ -package com.google.android.apps.mytracks.io; +package com.google.android.apps.mytracks.io.file; 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.io.file.TrackWriterFactory.TrackFileFormat; +import com.google.android.apps.mytracks.util.SystemUtils; +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", SystemUtils.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("%s\n", SystemUtils.getMyTracksVersion(context)); + pw.format("%d\n", SystemUtils.isRelease(context) ? 0 : 1); + pw.println(""); + + pw.format("%s\n", SystemUtils.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/TempFileCleaner.java b/MyTracks/src/com/google/android/apps/mytracks/io/file/TempFileCleaner.java similarity index 97% rename from MyTracks/src/com/google/android/apps/mytracks/io/TempFileCleaner.java rename to MyTracks/src/com/google/android/apps/mytracks/io/file/TempFileCleaner.java index 21621225d..6f590342b 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/io/TempFileCleaner.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/file/TempFileCleaner.java @@ -13,7 +13,7 @@ * License for the specific language governing permissions and limitations under * the License. */ -package com.google.android.apps.mytracks.io; +package com.google.android.apps.mytracks.io.file; import android.os.Environment; diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/TrackFormatWriter.java b/MyTracks/src/com/google/android/apps/mytracks/io/file/TrackFormatWriter.java similarity index 93% rename from MyTracks/src/com/google/android/apps/mytracks/io/TrackFormatWriter.java rename to MyTracks/src/com/google/android/apps/mytracks/io/file/TrackFormatWriter.java index c5ac02514..0b7101c6f 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/io/TrackFormatWriter.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/file/TrackFormatWriter.java @@ -1,19 +1,19 @@ /* * Copyright 2010 Google Inc. - * + * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of * the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations under * the License. */ -package com.google.android.apps.mytracks.io; +package com.google.android.apps.mytracks.io.file; import com.google.android.apps.mytracks.content.Track; import com.google.android.apps.mytracks.content.Waypoint; @@ -54,7 +54,7 @@ public interface TrackFormatWriter { void prepare(Track track, OutputStream out); /** - * @return The file extentsion (i.e. gpx, kml, ...) + * @return The file extension (i.e. gpx, kml, ...) */ String getExtension(); @@ -77,7 +77,7 @@ public interface TrackFormatWriter { * * @param location the location to write */ - void writeLocation(Location location); + void writeLocation(Location location) throws InterruptedException; /** * Write a way point. diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/file/TrackWriter.java b/MyTracks/src/com/google/android/apps/mytracks/io/file/TrackWriter.java new file mode 100644 index 000000000..bace24d33 --- /dev/null +++ b/MyTracks/src/com/google/android/apps/mytracks/io/file/TrackWriter.java @@ -0,0 +1,92 @@ +/* + * 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.file; + +import java.io.File; + +/** + * Implementations of this class export tracks to the SD card. This class is + * intended to be format-neutral - it handles creating the output file and + * reading the track to be exported, but requires an instance of + * {@link TrackFormatWriter} to actually format the data. + * + * @author Sandor Dornbush + * @author Rodrigo Damazio + */ +public interface TrackWriter { + /** This listener is used to signal completion of track write */ + public interface OnCompletionListener { + public void onComplete(); + } + + /** This listener is used to signal track writes. */ + public interface OnWriteListener { + /** + * This method is invoked whenever a location within a track is written. + * @param number the location number + * @param max the maximum number of locations, for calculation of + * completion percentage + */ + public void onWrite(int number, int max); + } + + /** + * Sets listener to be invoked when the writer has finished. + */ + void setOnCompletionListener(OnCompletionListener onCompletionListener); + + /** + * Sets a listener to be invoked for each location writer. + */ + void setOnWriteListener(OnWriteListener onWriteListener); + + /** + * Sets a custom directory where the file will be written. + */ + void setDirectory(File directory); + + /** + * Returns the absolute path to the file which was created. + */ + String getAbsolutePath(); + + /** + * Writes the given track id to the SD card. + * This is non-blocking. + */ + void writeTrackAsync(); + + /** + * Writes the given track id to the SD card. + * This is blocking. + */ + void writeTrack(); + + /** + * Stop any in-progress writes + */ + void stopWriteTrack(); + + /** + * Returns true if the write completed successfully. + */ + boolean wasSuccess(); + + /** + * Returns the error message (if any) generated by a writer failure. + */ + int getErrorMessage(); +} diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/TrackWriterFactory.java b/MyTracks/src/com/google/android/apps/mytracks/io/file/TrackWriterFactory.java similarity index 94% rename from MyTracks/src/com/google/android/apps/mytracks/io/TrackWriterFactory.java rename to MyTracks/src/com/google/android/apps/mytracks/io/file/TrackWriterFactory.java index 55261f8a5..5aabf6133 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/io/TrackWriterFactory.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/file/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 @@ -14,7 +14,7 @@ * the License. */ -package com.google.android.apps.mytracks.io; +package com.google.android.apps.mytracks.io.file; import com.google.android.apps.mytracks.content.MyTracksProviderUtils; import com.google.android.apps.mytracks.content.Track; @@ -53,7 +53,7 @@ public class TrackWriterFactory { TCX { @Override public TrackFormatWriter newFormatWriter(Context context) { - return new TcxTrackWriter(); + return new TcxTrackWriter(context); } }; @@ -106,7 +106,7 @@ public class TrackWriterFactory { MyTracksProviderUtils providerUtils, Track track, TrackFileFormat format) { TrackFormatWriter writer = format.newFormatWriter(context); - return new TrackWriter(context, providerUtils, track, writer); + return new TrackWriterImpl(context, providerUtils, track, writer); } private TrackWriterFactory() { } diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/TrackWriter.java b/MyTracks/src/com/google/android/apps/mytracks/io/file/TrackWriterImpl.java similarity index 71% rename from MyTracks/src/com/google/android/apps/mytracks/io/TrackWriter.java rename to MyTracks/src/com/google/android/apps/mytracks/io/file/TrackWriterImpl.java index 26fb19f87..d74a9c8f4 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/io/TrackWriter.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/file/TrackWriterImpl.java @@ -1,28 +1,28 @@ /* * Copyright 2008 Google Inc. - * + * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of * the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations under * the License. */ -package com.google.android.apps.mytracks.io; +package com.google.android.apps.mytracks.io.file; -import com.google.android.apps.mytracks.MyTracksConstants; +import com.google.android.apps.mytracks.Constants; import com.google.android.apps.mytracks.content.MyTracksLocation; import com.google.android.apps.mytracks.content.MyTracksProviderUtils; +import com.google.android.apps.mytracks.content.MyTracksProviderUtils.LocationIterator; import com.google.android.apps.mytracks.content.Track; import com.google.android.apps.mytracks.content.Waypoint; -import com.google.android.apps.mytracks.content.MyTracksProviderUtils.LocationIterator; import com.google.android.apps.mytracks.util.FileUtils; -import com.google.android.apps.mytracks.util.MyTracksUtils; +import com.google.android.apps.mytracks.util.LocationUtils; import com.google.android.maps.mytracks.R; import android.app.Activity; @@ -37,28 +37,29 @@ import java.io.FileOutputStream; import java.io.OutputStream; /** - * The class which exports tracks to the SD card. - * This writer is format-neutral - it handles creating the output file - * and reading the track to be exported, but requires an instance of - * {@link TrackFormatWriter} to actually format the data. + * This class exports tracks to the SD card. It is intended to be format- + * neutral -- it handles creating the output file and reading the track to be + * exported, but requires an instance of {@link TrackFormatWriter} to actually + * format the data. * * @author Sandor Dornbush * @author Rodrigo Damazio */ -public class TrackWriter { - +class TrackWriterImpl implements TrackWriter { private final Context context; private final MyTracksProviderUtils providerUtils; private final Track track; private final TrackFormatWriter writer; private final FileUtils fileUtils; - private Runnable onCompletion = null; private boolean success = false; private int errorMessage = -1; private File directory = null; private File file = null; + private OnCompletionListener onCompletionListener; + private OnWriteListener onWriteListener; + private Thread writeThread; - public TrackWriter(Context context, MyTracksProviderUtils providerUtils, + TrackWriterImpl(Context context, MyTracksProviderUtils providerUtils, Track track, TrackFormatWriter writer) { this.context = context; this.providerUtils = providerUtils; @@ -67,60 +68,88 @@ public class TrackWriter { this.fileUtils = new FileUtils(); } - /** - * Sets a completion callback. - * - * @param onCompletion Runnable that will be executed when finished - */ - public void setOnCompletion(Runnable onCompletion) { - this.onCompletion = onCompletion; + @Override + public void setOnCompletionListener(OnCompletionListener onCompletionListener) { + this.onCompletionListener = onCompletionListener; } - /** - * Sets a custom directory where the file will be written. - */ + @Override + public void setOnWriteListener(OnWriteListener onWriteListener) { + this.onWriteListener = onWriteListener; + } + + @Override public void setDirectory(File directory) { this.directory = directory; } + @Override public String getAbsolutePath() { return file.getAbsolutePath(); } - /** - * Writes the given track id to the SD card. - * This is non-blocking. - */ + @Override public void writeTrackAsync() { - Thread t = new Thread() { + writeThread = new Thread() { @Override public void run() { - writeTrack(); + doWriteTrack(); } }; - t.start(); + writeThread.start(); } - /** - * Writes the given track id to the SD card. - * This is blocking. - */ + @Override public void writeTrack() { + writeTrackAsync(); + try { + writeThread.join(); + } catch (InterruptedException e) { + Log.e(Constants.TAG, "Interrupted waiting for write to complete", e); + } + } + + private void doWriteTrack() { // Open the input and output success = false; errorMessage = R.string.error_track_does_not_exist; if (track != null) { if (openFile()) { - writeDocument(); + try { + writeDocument(); + } catch (InterruptedException e) { + Log.i(Constants.TAG, "The track write was interrupted"); + if (file != null) { + file.delete(); + } + success = false; + errorMessage = R.string.error_operation_cancelled; + } } } finished(); } + public void stopWriteTrack() { + if (writeThread != null && writeThread.isAlive()) { + Log.i(Constants.TAG, "Attempting to stop track write"); + writeThread.interrupt(); + + try { + writeThread.join(); + Log.i(Constants.TAG, "Track write stopped"); + } catch (InterruptedException e) { + Log.e(Constants.TAG, "Failed to wait for writer to stop", e); + } + } + } + + @Override public int getErrorMessage() { return errorMessage; } + @Override public boolean wasSuccess() { return success; } @@ -131,8 +160,13 @@ public class TrackWriter { */ private void finished() { - if (onCompletion != null) { - runOnUiThread(onCompletion); + if (onCompletionListener != null) { + runOnUiThread(new Runnable() { + @Override + public void run() { + onCompletionListener.onComplete(); + } + }); return; } } @@ -147,8 +181,8 @@ public class TrackWriter { } /** - * Opens the file and prepares the format writer for it. - * + * Opens the file and prepares the format writer for it. + * * @return true on success, false otherwise (and errorMessage is set) */ protected boolean openFile() { @@ -160,16 +194,16 @@ public class TrackWriter { String fileName = fileUtils.buildUniqueFileName( directory, track.getName(), writer.getExtension()); if (fileName == null) { - Log.e(MyTracksConstants.TAG, + Log.e(Constants.TAG, "Unable to get a unique filename for " + fileName); return false; } - Log.i(MyTracksConstants.TAG, "Writing track to: " + fileName); + Log.i(Constants.TAG, "Writing track to: " + fileName); try { writer.prepare(track, newOutputStream(fileName)); } catch (FileNotFoundException e) { - Log.e(MyTracksConstants.TAG, "Failed to open output file.", e); + Log.e(Constants.TAG, "Failed to open output file.", e); errorMessage = R.string.io_write_failed; return false; } @@ -187,12 +221,12 @@ public class TrackWriter { } if (!fileUtils.isSdCardAvailable()) { - Log.i(MyTracksConstants.TAG, "Could not find SD card."); + Log.i(Constants.TAG, "Could not find SD card."); errorMessage = R.string.io_no_external_storage_found; return false; } if (!fileUtils.ensureDirectoryExists(directory)) { - Log.i(MyTracksConstants.TAG, "Could not create export directory."); + Log.i(Constants.TAG, "Could not create export directory."); errorMessage = R.string.io_create_dir_failed; return false; } @@ -230,7 +264,7 @@ public class TrackWriter { // same time. Cursor cursor = null; cursor = providerUtils.getWaypointsCursor(trackId, 0, - MyTracksConstants.MAX_LOADED_WAYPOINTS_POINTS); + Constants.MAX_LOADED_WAYPOINTS_POINTS); if (cursor != null) { try { if (cursor.moveToFirst()) { @@ -252,27 +286,27 @@ public class TrackWriter { /** * Does the actual work of writing the track to the now open file. */ - void writeDocument() { - Log.d(MyTracksConstants.TAG, "Started writing track."); + void writeDocument() throws InterruptedException { + Log.d(Constants.TAG, "Started writing track."); writer.writeHeader(); writeWaypoints(track.getId()); writeLocations(); writer.writeFooter(); writer.close(); success = true; - Log.d(MyTracksConstants.TAG, "Done writing track."); + Log.d(Constants.TAG, "Done writing track."); errorMessage = R.string.io_write_finished; } - private void writeLocations() { + private void writeLocations() throws InterruptedException { boolean wroteFirst = false; boolean segmentOpen = false; boolean isLastValid = false; - + class TrackWriterLocationFactory implements MyTracksProviderUtils.LocationFactory { Location currentLocation; Location lastLocation; - + @Override public Location createLocation() { if (currentLocation == null) { @@ -280,7 +314,7 @@ public class TrackWriter { } return currentLocation; } - + public void swapLocations() { Location tmpLoc = lastLocation; lastLocation = currentLocation; @@ -290,45 +324,54 @@ public class TrackWriter { } } }; - + TrackWriterLocationFactory locationFactory = new TrackWriterLocationFactory(); LocationIterator it = providerUtils.getLocationIterator(track.getId(), 0, false, locationFactory); try { if (!it.hasNext()) { - Log.w(MyTracksConstants.TAG, "Unable to get any points to write"); + Log.w(Constants.TAG, "Unable to get any points to write"); return; } + int pointNumber = 0; while (it.hasNext()) { Location loc = it.next(); - - boolean isValid = MyTracksUtils.isValidLocation(loc); + if (Thread.interrupted()) { + throw new InterruptedException(); + } + + pointNumber++; + + boolean isValid = LocationUtils.isValidLocation(loc); boolean validSegment = isValid && isLastValid; if (!wroteFirst && validSegment) { // Found the first two consecutive points which are valid writer.writeBeginTrack(locationFactory.lastLocation); wroteFirst = true; } - + if (validSegment) { if (!segmentOpen) { // Start a segment for this point writer.writeOpenSegment(); segmentOpen = true; - + // Write the previous point, which we had previously skipped writer.writeLocation(locationFactory.lastLocation); } - + // Write the current point writer.writeLocation(loc); + if (onWriteListener != null) { + onWriteListener.onWrite(pointNumber, track.getNumberOfPoints()); + } } else { if (segmentOpen) { writer.writeCloseSegment(); segmentOpen = false; } } - + locationFactory.swapLocations(); isLastValid = isValid; } diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/gdata/AndroidGDataClient.java b/MyTracks/src/com/google/android/apps/mytracks/io/gdata/AndroidGDataClient.java index 20c4498d7..7b037bb75 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/io/gdata/AndroidGDataClient.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/gdata/AndroidGDataClient.java @@ -187,6 +187,7 @@ public class AndroidGDataClient implements GDataClient { while (redirectsLeft > 0) { HttpUriRequest request = creator.createRequest(uri); + request.addHeader("User-Agent", "Android-GData"); request.addHeader("Accept-Encoding", "gzip"); // only add the auth token if not null (to allow for GData feeds that do diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/gdata/GDataClientFactory.java b/MyTracks/src/com/google/android/apps/mytracks/io/gdata/GDataClientFactory.java index 15dd588d0..a0a717bb1 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/io/gdata/GDataClientFactory.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/gdata/GDataClientFactory.java @@ -15,7 +15,7 @@ */ package com.google.android.apps.mytracks.io.gdata; -import com.google.android.apps.mytracks.MyTracksConstants; +import com.google.android.apps.mytracks.Constants; import com.google.wireless.gdata.client.GDataClient; import android.content.Context; @@ -43,7 +43,7 @@ public class GDataClientFactory { } catch (LinkageError e) { // On all other platforms use the client implementation packaged in the // apk. - Log.i(MyTracksConstants.TAG, "Using mytracks AndroidGDataClient.", e); + Log.i(Constants.TAG, "Using mytracks AndroidGDataClient.", e); return new com.google.android.apps.mytracks.io.gdata.AndroidGDataClient(); } } diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/gdata/GDataWrapper.java b/MyTracks/src/com/google/android/apps/mytracks/io/gdata/GDataWrapper.java index 75626c825..e26cbf947 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/io/gdata/GDataWrapper.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/gdata/GDataWrapper.java @@ -15,7 +15,7 @@ */ package com.google.android.apps.mytracks.io.gdata; -import com.google.android.apps.mytracks.MyTracksConstants; +import com.google.android.apps.mytracks.Constants; import com.google.android.apps.mytracks.io.AuthManager; import android.util.Log; @@ -173,14 +173,14 @@ public class GDataWrapper { return true; } - Log.d(MyTracksConstants.TAG, "GData error encountered: " + errorMessage); + Log.d(Constants.TAG, "GData error encountered: " + errorMessage); if (errorType == ERROR_AUTH && auth != null) { if (!retryOnAuthFailure || !invalidateAndRefreshAuthToken()) { return false; } } - Log.d(MyTracksConstants.TAG, "retrying function/query"); + Log.d(Constants.TAG, "retrying function/query"); } return false; } @@ -206,11 +206,11 @@ public class GDataWrapper { errorMessage = null; } catch (AuthenticationException e) { - Log.e(MyTracksConstants.TAG, "AuthenticationException", e); + Log.e(Constants.TAG, "AuthenticationException", e); errorType = ERROR_AUTH; errorMessage = e.getMessage(); } catch (HttpException e) { - Log.e(MyTracksConstants.TAG, + Log.e(Constants.TAG, "HttpException, code " + e.getStatusCode() + " message " + e.getMessage(), e); errorMessage = e.getMessage(); if (e.getStatusCode() == 401) { @@ -219,11 +219,11 @@ public class GDataWrapper { errorType = ERROR_CONNECTION; } } catch (FileNotFoundException e) { - Log.e(MyTracksConstants.TAG, "Exception", e); + Log.e(Constants.TAG, "Exception", e); errorType = ERROR_AUTH; errorMessage = e.getMessage(); } catch (IOException e) { - Log.e(MyTracksConstants.TAG, "Exception", e); + Log.e(Constants.TAG, "Exception", e); errorMessage = e.getMessage(); if (errorMessage != null && errorMessage.contains("503")) { errorType = ERROR_INTERNAL; @@ -231,11 +231,11 @@ public class GDataWrapper { errorType = ERROR_CONNECTION; } } catch (ParseException e) { - Log.e(MyTracksConstants.TAG, "Exception", e); + Log.e(Constants.TAG, "Exception", e); errorType = ERROR_LOCAL; errorMessage = e.getMessage(); } catch (ConflictDetectedException e) { - Log.e(MyTracksConstants.TAG, "Exception", e); + Log.e(Constants.TAG, "Exception", e); errorType = ERROR_CONFLICT; errorMessage = e.getMessage(); } @@ -249,7 +249,7 @@ public class GDataWrapper { * times out. */ private boolean invalidateAndRefreshAuthToken() { - Log.d(MyTracksConstants.TAG, "Retrying due to auth failure"); + Log.d(Constants.TAG, "Retrying due to auth failure"); // This FutureTask doesn't do anything -- it exists simply to be // blocked upon using get(). FutureTask whenFinishedFuture = new FutureTask(new Runnable() { @@ -259,18 +259,18 @@ public class GDataWrapper { auth.invalidateAndRefresh(whenFinishedFuture); try { - Log.d(MyTracksConstants.TAG, "waiting for invalidate"); + Log.d(Constants.TAG, "waiting for invalidate"); whenFinishedFuture.get(AUTH_TOKEN_INVALIDATE_REFRESH_TIMEOUT, TimeUnit.MILLISECONDS); - Log.d(MyTracksConstants.TAG, "invalidate finished"); + Log.d(Constants.TAG, "invalidate finished"); return true; } catch (InterruptedException e) { - Log.e(MyTracksConstants.TAG, "Failed to invalidate", e); + Log.e(Constants.TAG, "Failed to invalidate", e); } catch (ExecutionException e) { - Log.e(MyTracksConstants.TAG, "Failed to invalidate", e); + Log.e(Constants.TAG, "Failed to invalidate", e); } catch (TimeoutException e) { - Log.e(MyTracksConstants.TAG, "Invalidate didn't complete in time", e); + Log.e(Constants.TAG, "Invalidate didn't complete in time", e); } finally { whenFinishedFuture.cancel(false); } 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/wireless/gdata/maps/MapsClient.java b/MyTracks/src/com/google/android/apps/mytracks/io/mymaps/MapsClient.java similarity index 80% rename from MyTracks/src/com/google/wireless/gdata/maps/MapsClient.java rename to MyTracks/src/com/google/android/apps/mytracks/io/mymaps/MapsClient.java index dcf0b45e2..7d1194b8c 100644 --- a/MyTracks/src/com/google/wireless/gdata/maps/MapsClient.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/mymaps/MapsClient.java @@ -1,19 +1,5 @@ -/* - * Copyright 2010 Google Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ -package com.google.wireless.gdata.maps; +// 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; @@ -24,9 +10,7 @@ import android.util.Log; /** * Client to talk to Google Maps via GData. */ -public class MapsClient extends GDataServiceClient { - public static final String SERVICE = "local"; - +class MapsClient extends GDataServiceClient { private static final boolean DEBUG = false; public static final boolean LOG_COMMUNICATION = false; @@ -49,7 +33,11 @@ public class MapsClient extends GDataServiceClient { @Override public String getServiceName() { - return SERVICE; + return MyMapsConstants.SERVICE_NAME; + } + + public static String buildMapUrl(String mapId) { + return MyMapsConstants.MAPSHOP_BASE_URL + "?msa=0&msid=" + mapId; } public static String getMapsFeed() { 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/wireless/gdata/maps/xml/XmlMapsGDataParser.java b/MyTracks/src/com/google/android/apps/mytracks/io/mymaps/XmlMapsGDataParser.java similarity index 88% rename from MyTracks/src/com/google/wireless/gdata/maps/xml/XmlMapsGDataParser.java rename to MyTracks/src/com/google/android/apps/mytracks/io/mymaps/XmlMapsGDataParser.java index e7fe6e7e6..7af41d7ed 100644 --- a/MyTracks/src/com/google/wireless/gdata/maps/xml/XmlMapsGDataParser.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/mymaps/XmlMapsGDataParser.java @@ -1,24 +1,9 @@ -/* - * Copyright 2009 Google Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ -package com.google.wireless.gdata.maps.xml; +// 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.maps.MapFeatureEntry; import com.google.wireless.gdata.parser.ParseException; import com.google.wireless.gdata.parser.xml.XmlGDataParser; @@ -31,7 +16,7 @@ import java.io.InputStream; /** * Parser for XML gdata maps data. */ -public class XmlMapsGDataParser extends XmlGDataParser { +class XmlMapsGDataParser extends XmlGDataParser { public XmlMapsGDataParser(InputStream is, XmlPullParser xpp) throws ParseException { diff --git a/MyTracks/src/com/google/wireless/gdata/maps/xml/XmlMapsGDataParserFactory.java b/MyTracks/src/com/google/android/apps/mytracks/io/mymaps/XmlMapsGDataParserFactory.java similarity index 76% rename from MyTracks/src/com/google/wireless/gdata/maps/xml/XmlMapsGDataParserFactory.java rename to MyTracks/src/com/google/android/apps/mytracks/io/mymaps/XmlMapsGDataParserFactory.java index ef2f72b6b..15c0a884e 100644 --- a/MyTracks/src/com/google/wireless/gdata/maps/xml/XmlMapsGDataParserFactory.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/mymaps/XmlMapsGDataParserFactory.java @@ -1,24 +1,8 @@ -/* - * Copyright 2010 Google Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ -package com.google.wireless.gdata.maps.xml; +// 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.maps.MapFeatureEntry; -import com.google.wireless.gdata.maps.MapsClient; import com.google.wireless.gdata.parser.GDataParser; import com.google.wireless.gdata.parser.ParseException; import com.google.wireless.gdata.parser.xml.XmlGDataParser; @@ -37,7 +21,7 @@ import java.io.InputStream; /** * Factory of Xml parsers for gdata maps data. */ -public class XmlMapsGDataParserFactory implements GDataParserFactory { +class XmlMapsGDataParserFactory implements GDataParserFactory { private XmlParserFactory xmlFactory; public XmlMapsGDataParserFactory(XmlParserFactory xmlFactory) { @@ -55,7 +39,7 @@ public class XmlMapsGDataParserFactory implements GDataParserFactory { } } - @SuppressWarnings("unchecked") + @SuppressWarnings({ "unchecked", "rawtypes" }) @Override public GDataParser createParser(Class cls, InputStream is) throws ParseException { diff --git a/MyTracks/src/com/google/wireless/gdata/maps/xml/XmlMapsGDataSerializer.java b/MyTracks/src/com/google/android/apps/mytracks/io/mymaps/XmlMapsGDataSerializer.java similarity index 90% rename from MyTracks/src/com/google/wireless/gdata/maps/xml/XmlMapsGDataSerializer.java rename to MyTracks/src/com/google/android/apps/mytracks/io/mymaps/XmlMapsGDataSerializer.java index 7001a0f6b..80598788d 100644 --- a/MyTracks/src/com/google/wireless/gdata/maps/xml/XmlMapsGDataSerializer.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/mymaps/XmlMapsGDataSerializer.java @@ -1,24 +1,8 @@ -/* - * Copyright 2010 Google Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ -package com.google.wireless.gdata.maps.xml; +// 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.maps.MapFeatureEntry; -import com.google.wireless.gdata.maps.MapsClient; import com.google.wireless.gdata.parser.ParseException; import com.google.wireless.gdata.parser.xml.XmlGDataParser; import com.google.wireless.gdata.parser.xml.XmlParserFactory; @@ -37,7 +21,7 @@ import java.util.Map; /** * Serializer of maps data for GData. */ -public class XmlMapsGDataSerializer extends XmlEntryGDataSerializer { +class XmlMapsGDataSerializer extends XmlEntryGDataSerializer { private static final String APP_NAMESPACE = "http://www.w3.org/2007/app"; @@ -62,8 +46,6 @@ public class XmlMapsGDataSerializer extends XmlEntryGDataSerializer { throw new ParseException("Unable to create XmlSerializer.", e); } - // TODO: make the output compact - ByteArrayOutputStream printStream; if (MapsClient.LOG_COMMUNICATION) { 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 68% 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 f39be1d84..16df9c5e3 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.Constants; +import com.google.android.apps.mytracks.DialogManager; +import com.google.android.apps.mytracks.MyTracks; import com.google.android.maps.mytracks.R; import android.app.Dialog; @@ -29,6 +32,7 @@ import android.widget.CheckBox; import android.widget.CompoundButton; import android.widget.CompoundButton.OnCheckedChangeListener; import android.widget.RadioButton; +import android.widget.RadioGroup; /** * A dialog where the user can choose where to send the tracks to, i.e. @@ -36,15 +40,19 @@ import android.widget.RadioButton; * * @author Leif Hendrik Wilden */ -public class SendToGoogleDialog extends Dialog { +public class SendDialog extends Dialog { + private RadioGroup groupMyMaps; + private RadioButton createNewMapRadioButton; + private RadioButton pickMapRadioButton; + private CheckBox sendToMyMapsCheckBox; private CheckBox sendToFusionTablesCheckBox; private CheckBox sendToDocsCheckBox; private RadioButton sendStatsRadioButton; private RadioButton sendStatsAndPointsRadioButton; private Button sendButton; - public SendToGoogleDialog(Context context) { + public SendDialog(Context context) { super(context); } @@ -72,33 +80,50 @@ public class SendToGoogleDialog extends Dialog { OnCheckedChangeListener checkBoxListener = new OnCheckedChangeListener() { public void onCheckedChanged(CompoundButton button, boolean checked) { sendButton.setEnabled( + sendToMyMapsCheckBox.isChecked() || sendToFusionTablesCheckBox.isChecked() || sendToDocsCheckBox.isChecked()); - //groupDocs.setVisibility( - // sendToDocsCheckBox.isChecked() ? View.VISIBLE : View.INVISIBLE); + groupMyMaps.setVisibility(sendToMyMapsCheckBox.isChecked() ? View.VISIBLE : View.INVISIBLE); } }; sendButton = (Button) findViewById(R.id.sendtogoogle_send_now); + groupMyMaps = (RadioGroup) findViewById(R.id.sendtogoogle_group_mymaps); + sendToMyMapsCheckBox = + (CheckBox) findViewById(R.id.sendtogoogle_google_mymaps); + sendToMyMapsCheckBox.setOnCheckedChangeListener(checkBoxListener); sendToFusionTablesCheckBox = (CheckBox) findViewById(R.id.sendtogoogle_google_fusiontables); sendToFusionTablesCheckBox.setOnCheckedChangeListener(checkBoxListener); sendToDocsCheckBox = (CheckBox) findViewById(R.id.sendtogoogle_google_docs); sendToDocsCheckBox.setOnCheckedChangeListener(checkBoxListener); + createNewMapRadioButton = + (RadioButton) findViewById(R.id.sendtogoogle_create_new_map); + pickMapRadioButton = + (RadioButton) findViewById(R.id.sendtogoogle_pick_existing_map); sendStatsRadioButton = (RadioButton) findViewById(R.id.sendtogoogle_send_stats); sendStatsAndPointsRadioButton = (RadioButton) findViewById( R.id.sendtogoogle_send_stats_and_points); SharedPreferences prefs = - getContext().getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0); + getContext().getSharedPreferences(Constants.SETTINGS_NAME, 0); if (prefs != null) { - sendToFusionTablesCheckBox.setChecked( + sendToMyMapsCheckBox.setChecked( prefs.getBoolean( getContext().getString(R.string.send_to_my_maps_key), true)); + sendToFusionTablesCheckBox.setChecked( + prefs.getBoolean( + getContext().getString(R.string.send_to_fusion_tables_key), true)); sendToDocsCheckBox.setChecked( prefs.getBoolean( getContext().getString(R.string.send_to_docs_key), true)); + pickMapRadioButton.setChecked( + prefs.getBoolean( + getContext().getString(R.string.pick_existing_map_key), false)); + createNewMapRadioButton.setChecked( + !prefs.getBoolean( + getContext().getString(R.string.pick_existing_map_key), false)); sendStatsAndPointsRadioButton.setChecked( prefs.getBoolean( getContext().getString(R.string.send_stats_and_points_key), @@ -113,16 +138,22 @@ public class SendToGoogleDialog extends Dialog { @Override protected void onStop() { SharedPreferences prefs = - getContext().getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0); + getContext().getSharedPreferences(Constants.SETTINGS_NAME, 0); if (prefs != null) { Editor editor = prefs.edit(); if (editor != null) { editor.putBoolean( getContext().getString(R.string.send_to_my_maps_key), + sendToMyMapsCheckBox.isChecked()); + editor.putBoolean( + getContext().getString(R.string.send_to_fusion_tables_key), sendToFusionTablesCheckBox.isChecked()); editor.putBoolean( getContext().getString(R.string.send_to_docs_key), sendToDocsCheckBox.isChecked()); + editor.putBoolean( + getContext().getString(R.string.pick_existing_map_key), + pickMapRadioButton.isChecked()); editor.putBoolean( getContext().getString(R.string.send_stats_and_points_key), sendStatsAndPointsRadioButton.isChecked()); @@ -132,6 +163,10 @@ public class SendToGoogleDialog extends Dialog { super.onStop(); } + public boolean getSendToMyMaps() { + return sendToMyMapsCheckBox.isChecked(); + } + public boolean getSendToFusionTables() { return sendToFusionTablesCheckBox.isChecked(); } @@ -140,6 +175,10 @@ public class SendToGoogleDialog extends Dialog { return sendToDocsCheckBox.isChecked(); } + public boolean getCreateNewMap() { + return createNewMapRadioButton.isChecked(); + } + public boolean getSendStatsAndPoints() { return sendStatsAndPointsRadioButton.isChecked(); } 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/DefaultTrackNameFactory.java b/MyTracks/src/com/google/android/apps/mytracks/services/DefaultTrackNameFactory.java index 79d2f5328..a83658854 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/services/DefaultTrackNameFactory.java +++ b/MyTracks/src/com/google/android/apps/mytracks/services/DefaultTrackNameFactory.java @@ -15,7 +15,7 @@ */ package com.google.android.apps.mytracks.services; -import com.google.android.apps.mytracks.MyTracksSettings; +import com.google.android.apps.mytracks.Constants; import com.google.android.maps.mytracks.R; import android.content.Context; @@ -58,7 +58,7 @@ class DefaultTrackNameFactory { /** Determines whether the preferences allow a timestamp-based track name */ protected boolean useTimestampTrackName() { SharedPreferences prefs = - context.getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0); + context.getSharedPreferences(Constants.SETTINGS_NAME, 0); return prefs.getBoolean( context.getString(R.string.timestamp_track_name_key), true); } diff --git a/MyTracks/src/com/google/android/apps/mytracks/services/PreferenceManager.java b/MyTracks/src/com/google/android/apps/mytracks/services/PreferenceManager.java index fee208d54..3f5b666d5 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/services/PreferenceManager.java +++ b/MyTracks/src/com/google/android/apps/mytracks/services/PreferenceManager.java @@ -15,8 +15,7 @@ */ package com.google.android.apps.mytracks.services; -import com.google.android.apps.mytracks.MyTracksConstants; -import com.google.android.apps.mytracks.MyTracksSettings; +import com.google.android.apps.mytracks.Constants; import com.google.android.maps.mytracks.R; import android.content.SharedPreferences; @@ -45,9 +44,9 @@ public class PreferenceManager implements OnSharedPreferenceChangeListener { public PreferenceManager(TrackRecordingService service) { this.service = service; this.sharedPreferences = service.getSharedPreferences( - MyTracksSettings.SETTINGS_NAME, 0); + Constants.SETTINGS_NAME, 0); if (sharedPreferences == null) { - Log.w(MyTracksConstants.TAG, + Log.w(Constants.TAG, "TrackRecordingService: Couldn't get shared preferences."); throw new IllegalStateException("Couldn't get shared preferences"); } @@ -88,7 +87,7 @@ public class PreferenceManager implements OnSharedPreferenceChangeListener { public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { if (service == null) { - Log.w(MyTracksConstants.TAG, + Log.w(Constants.TAG, "onSharedPreferenceChanged: a preference change (key = " + key + ") after a call to shutdown()"); return; @@ -97,20 +96,20 @@ public class PreferenceManager implements OnSharedPreferenceChangeListener { service.setMinRecordingDistance( sharedPreferences.getInt( minRecordingDistanceKey, - MyTracksSettings.DEFAULT_MIN_RECORDING_DISTANCE)); - Log.d(MyTracksConstants.TAG, + Constants.DEFAULT_MIN_RECORDING_DISTANCE)); + Log.d(Constants.TAG, "TrackRecordingService: minRecordingDistance = " + service.getMinRecordingDistance()); } if (key == null || key.equals(maxRecordingDistanceKey)) { service.setMaxRecordingDistance(sharedPreferences.getInt( maxRecordingDistanceKey, - MyTracksSettings.DEFAULT_MAX_RECORDING_DISTANCE)); + Constants.DEFAULT_MAX_RECORDING_DISTANCE)); } if (key == null || key.equals(minRecordingIntervalKey)) { int minRecordingInterval = sharedPreferences.getInt( minRecordingIntervalKey, - MyTracksSettings.DEFAULT_MIN_RECORDING_INTERVAL); + Constants.DEFAULT_MIN_RECORDING_INTERVAL); switch (minRecordingInterval) { case -2: // Battery Miser @@ -136,7 +135,7 @@ public class PreferenceManager implements OnSharedPreferenceChangeListener { if (key == null || key.equals(minRequiredAccuracyKey)) { service.setMinRequiredAccuracy(sharedPreferences.getInt( minRequiredAccuracyKey, - MyTracksSettings.DEFAULT_MIN_REQUIRED_ACCURACY)); + Constants.DEFAULT_MIN_REQUIRED_ACCURACY)); } if (key == null || key.equals(announcementFrequencyKey)) { service.setAnnouncementFrequency( @@ -145,7 +144,7 @@ public class PreferenceManager implements OnSharedPreferenceChangeListener { if (key == null || key.equals(autoResumeTrackTimeoutKey)) { service.setAutoResumeTrackTimeout(sharedPreferences.getInt( autoResumeTrackTimeoutKey, - MyTracksSettings.DEFAULT_AUTO_RESUME_TRACK_TIMEOUT)); + Constants.DEFAULT_AUTO_RESUME_TRACK_TIMEOUT)); } if (key == null || key.equals(recordingTrackKey)) { long recordingTrackId = sharedPreferences.getLong(recordingTrackKey, -1); @@ -157,11 +156,11 @@ public class PreferenceManager implements OnSharedPreferenceChangeListener { } } if (key == null || key.equals(splitFrequencyKey)) { - service.getSplitManager().setSplitFrequency( + service.setSplitFrequency( sharedPreferences.getInt(splitFrequencyKey, 0)); } if (key == null || key.equals(metricUnitsKey)) { - service.getSplitManager().setMetricUnits( + service.setMetricUnits( sharedPreferences.getBoolean(metricUnitsKey, true)); } } diff --git a/MyTracks/src/com/google/android/apps/mytracks/services/SplitManager.java b/MyTracks/src/com/google/android/apps/mytracks/services/SplitManager.java deleted file mode 100644 index f63e87531..000000000 --- a/MyTracks/src/com/google/android/apps/mytracks/services/SplitManager.java +++ /dev/null @@ -1,160 +0,0 @@ -/* - * Copyright 2010 Google Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ -package com.google.android.apps.mytracks.services; - -import com.google.android.apps.mytracks.MyTracksConstants; -import com.google.android.apps.mytracks.content.WaypointCreationRequest; -import com.google.android.apps.mytracks.util.UnitConversions; - -import android.util.Log; - -/** - * This class manages inserting time or distance splits. - * - * @author Sandor Dornbush - */ -public class SplitManager { - - /** - * The frequency of splits. - */ - private int splitFrequency = 0; - - /** - * The next distance when a split should be taken. - */ - private double nextSplitDistance = 0; - - /** - * Splits executer. - */ - private PeriodicTaskExecuter splitExecuter = null; - - private boolean metricUnits; - - private final TrackRecordingService service; - - public SplitManager(TrackRecordingService service) { - this.service = service; - } - - /** - * Restores the manager. - */ - public void restore() { - if ((splitFrequency > 0) && (splitExecuter != null)) { - splitExecuter.scheduleTask(splitFrequency * 60000); - } - calculateNextSplit(); - } - - /** - * Shuts down the manager. - */ - public void shutdown() { - if (splitExecuter != null) { - splitExecuter.shutdown(); - } - } - - /** - * Calculates the next distance that a split should be inserted at. - */ - public void calculateNextSplit() { - // TODO: Decouple service from this class once and forever. - if (!service.isRecording()) { - return; - } - - if (splitFrequency >= 0) { - nextSplitDistance = Double.MAX_VALUE; - Log.d(MyTracksConstants.TAG, - "SplitManager: Distance splits disabled."); - return; - } - - double dist = service.getTripStatistics().getTotalDistance() / 1000; - if (!metricUnits) { - dist *= UnitConversions.KM_TO_MI; - } - // The index will be negative since the frequency is negative. - int index = (int) (dist / splitFrequency); - index -= 1; - nextSplitDistance = splitFrequency * index; - Log.d(MyTracksConstants.TAG, - "SplitManager: Next split distance: " + nextSplitDistance); - } - - /** - * Updates split manager with new trip statistics. - */ - public void updateSplits() { - if (this.splitFrequency >= 0) { - return; - } - // Convert the distance in meters to km or mi. - double distance = service.getTripStatistics().getTotalDistance() / 1000.0; - if (!metricUnits) { - distance *= UnitConversions.KM_TO_MI; - } - - if (distance > this.nextSplitDistance) { - service.insertWaypoint(WaypointCreationRequest.DEFAULT_STATISTICS); - calculateNextSplit(); - } - } - - /** - * Sets the split frequency. - * < 0 Use the absolute value as a distance in the current measurement km - * or mi - * 0 Turn off splits - * > 0 Use the value as a time in minutes - * @param splitFrequency The frequency in time or distance - */ - public void setSplitFrequency(int splitFrequency) { - Log.d(MyTracksConstants.TAG, - "setSplitFrequency: splitFrequency = " + splitFrequency); - this.splitFrequency = splitFrequency; - - // TODO: Decouple service from this class once and forever. - if (!service.isRecording()) { - return; - } - - if (splitFrequency < 1) { - if (splitExecuter != null) { - splitExecuter.shutdown(); - splitExecuter = null; - } - } - if (splitFrequency > 0) { - if (splitExecuter == null) { - TimeSplitTask splitter = new TimeSplitTask(); - splitExecuter = new PeriodicTaskExecuter(splitter, service); - } - splitExecuter.scheduleTask(splitFrequency * 60000); - } else { - // For distance based splits. - calculateNextSplit(); - } - } - - public void setMetricUnits(boolean metricUnits) { - this.metricUnits = metricUnits; - calculateNextSplit(); - } -} diff --git a/MyTracks/src/com/google/android/apps/mytracks/services/TaskExecuterManager.java b/MyTracks/src/com/google/android/apps/mytracks/services/TaskExecuterManager.java deleted file mode 100644 index dc28c69f6..000000000 --- a/MyTracks/src/com/google/android/apps/mytracks/services/TaskExecuterManager.java +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright 2010 Google Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ -package com.google.android.apps.mytracks.services; - -import com.google.android.apps.mytracks.MyTracksConstants; - -import android.util.Log; - -/** - * This class manages a period task executer. - * - * @author Sandor Dornbush - */ -public class TaskExecuterManager { - - private int frequency; - private final PeriodicTask task; - private PeriodicTaskExecuter executer; - - public TaskExecuterManager(int frequency, PeriodicTask task, - TrackRecordingService service) { - this.task = task; - setFrequency(frequency, service); - } - - public int getFrequency() { - return frequency; - } - - /** - * Sets the frequency that the task should be run at. - * If needed the task will be scheduled. - * - * @param frequency The frequency in minutes for the task to run - * @param service The service to run the task on - */ - public void setFrequency(int frequency, TrackRecordingService service) { - this.frequency = frequency; - Log.i(MyTracksConstants.TAG, "Frequency set to " + frequency - + " for task " + task.getClass().getSimpleName()); - - if (frequency == -1) { - if (executer != null) { - executer.shutdown(); - executer = null; - Log.i(MyTracksConstants.TAG, - "Shut down service: " + task.getClass().getSimpleName()); - } - } else { - if (executer == null) { - executer = new PeriodicTaskExecuter(task, service); - } - executer.scheduleTask(frequency * 60000); - } - } - - /** - * Restores the task at the current frequency. - */ - public void restore() { - if (frequency > 0) { - executer.scheduleTask(frequency * 60000); - } - } - - /** - * Shuts down this executer. - */ - public void shutdown() { - if (executer != null) { - executer.shutdown(); - } - } -} 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 a3ece7d66..6dd5a76f3 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/services/TrackRecordingService.java +++ b/MyTracks/src/com/google/android/apps/mytracks/services/TrackRecordingService.java @@ -15,11 +15,11 @@ */ package com.google.android.apps.mytracks.services; -import static com.google.android.apps.mytracks.MyTracksConstants.RESUME_TRACK_EXTRA_NAME; +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; -import com.google.android.apps.mytracks.MyTracksConstants; -import com.google.android.apps.mytracks.MyTracksSettings; +import com.google.android.apps.mytracks.Constants; import com.google.android.apps.mytracks.content.MyTracksLocation; import com.google.android.apps.mytracks.content.MyTracksProviderUtils; import com.google.android.apps.mytracks.content.Sensor; @@ -31,11 +31,14 @@ import com.google.android.apps.mytracks.content.WaypointsColumns; import com.google.android.apps.mytracks.content.Sensor.SensorDataSet; import com.google.android.apps.mytracks.services.sensors.SensorManager; import com.google.android.apps.mytracks.services.sensors.SensorManagerFactory; +import com.google.android.apps.mytracks.services.tasks.PeriodicTaskExecutor; +import com.google.android.apps.mytracks.services.tasks.SplitTask; +import com.google.android.apps.mytracks.services.tasks.StatusAnnouncerFactory; import com.google.android.apps.mytracks.stats.TripStatistics; import com.google.android.apps.mytracks.stats.TripStatisticsBuilder; import com.google.android.apps.mytracks.util.ApiFeatures; import com.google.android.apps.mytracks.util.ApiPlatformAdapter; -import com.google.android.apps.mytracks.util.MyTracksUtils; +import com.google.android.apps.mytracks.util.LocationUtils; import com.google.android.apps.mytracks.util.StringUtils; import com.google.android.maps.mytracks.R; @@ -80,13 +83,13 @@ public class TrackRecordingService extends Service implements LocationListener { private WakeLock wakeLock; private int minRecordingDistance = - MyTracksSettings.DEFAULT_MIN_RECORDING_DISTANCE; + Constants.DEFAULT_MIN_RECORDING_DISTANCE; private int maxRecordingDistance = - MyTracksSettings.DEFAULT_MAX_RECORDING_DISTANCE; + Constants.DEFAULT_MAX_RECORDING_DISTANCE; private int minRequiredAccuracy = - MyTracksSettings.DEFAULT_MIN_REQUIRED_ACCURACY; + Constants.DEFAULT_MIN_REQUIRED_ACCURACY; private int autoResumeTrackTimeout = - MyTracksSettings.DEFAULT_AUTO_RESUME_TRACK_TIMEOUT; + Constants.DEFAULT_AUTO_RESUME_TRACK_TIMEOUT; private long recordingTrackId = -1; @@ -111,10 +114,10 @@ public class TrackRecordingService extends Service implements LocationListener { private double length; /** - * Status announcer executer. + * Status announcer executor. */ - private PeriodicTaskExecuter announcementExecuter; - private SplitManager splitManager; + private PeriodicTaskExecutor announcementExecutor; + private PeriodicTaskExecutor splitExecutor; private SensorManager sensorManager; @@ -144,7 +147,7 @@ public class TrackRecordingService extends Service implements LocationListener { if (isRecording()) { handler.post(new Runnable() { public void run() { - Log.d(MyTracksConstants.TAG, + Log.d(Constants.TAG, "Re-registering location listener with TrackRecordingService."); unregisterLocationListener(); registerLocationListener(); @@ -185,11 +188,9 @@ public class TrackRecordingService extends Service implements LocationListener { private Location lastValidLocation; /** - * The frequency of status announcements. + * A service to run tasks outside of the main thread. */ - private int announcementFrequency = -1; - - private ExecutorService executerServce; + private ExecutorService executorService; /* * Utility functions @@ -214,7 +215,7 @@ public class TrackRecordingService extends Service implements LocationListener { // Keep track of length along recorded track (needed when a waypoint is // inserted): - if (MyTracksUtils.isValidLocation(location)) { + if (LocationUtils.isValidLocation(location)) { if (lastValidLocation != null) { length += location.distanceTo(lastValidLocation); } @@ -269,11 +270,12 @@ 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(MyTracksConstants.TAG, + Log.w(TAG, "Caught SQLiteException: " + e.getMessage(), e); return false; } - splitManager.updateSplits(); + announcementExecutor.update(); + splitExecutor.update(); return true; } @@ -314,15 +316,15 @@ public class TrackRecordingService extends Service implements LocationListener { try { PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE); if (pm == null) { - Log.e(MyTracksConstants.TAG, + Log.e(TAG, "TrackRecordingService: Power manager not found!"); return; } if (wakeLock == null) { wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, - MyTracksConstants.TAG); + TAG); if (wakeLock == null) { - Log.e(MyTracksConstants.TAG, + Log.e(TAG, "TrackRecordingService: Could not create wake lock (null)."); return; } @@ -330,12 +332,12 @@ public class TrackRecordingService extends Service implements LocationListener { if (!wakeLock.isHeld()) { wakeLock.acquire(); if (!wakeLock.isHeld()) { - Log.e(MyTracksConstants.TAG, + Log.e(TAG, "TrackRecordingService: Could not acquire wake lock."); } } } catch (RuntimeException e) { - Log.e(MyTracksConstants.TAG, + Log.e(TAG, "TrackRecordingService: Caught unexpected exception: " + e.getMessage(), e); } @@ -376,37 +378,37 @@ public class TrackRecordingService extends Service implements LocationListener { public void registerLocationListener() { if (locationManager == null) { - Log.e(MyTracksConstants.TAG, + Log.e(TAG, "TrackRecordingService: Do not have any location manager."); return; } - Log.d(MyTracksConstants.TAG, + Log.d(TAG, "Preparing to register location listener w/ TrackRecordingService..."); try { long desiredInterval = locationListenerPolicy.getDesiredPollingInterval(); locationManager.requestLocationUpdates( - MyTracksConstants.GPS_PROVIDER, desiredInterval, + LocationManager.GPS_PROVIDER, desiredInterval, locationListenerPolicy.getMinDistance(), // , 0 /* minDistance, get all updates to properly time pauses */ TrackRecordingService.this); currentRecordingInterval = desiredInterval; - Log.d(MyTracksConstants.TAG, + Log.d(TAG, "...location listener now registered w/ TrackRecordingService @ " + currentRecordingInterval); } catch (RuntimeException e) { - Log.e(MyTracksConstants.TAG, + Log.e(TAG, "Could not register location listener: " + e.getMessage(), e); } } public void unregisterLocationListener() { if (locationManager == null) { - Log.e(MyTracksConstants.TAG, + Log.e(TAG, "TrackRecordingService: Do not have any location manager."); return; } locationManager.removeUpdates(this); - Log.d(MyTracksConstants.TAG, + Log.d(TAG, "Location listener now unregistered w/ TrackRecordingService."); } @@ -419,15 +421,13 @@ public class TrackRecordingService extends Service implements LocationListener { } private void restoreStats(Track track) { - Log.d(MyTracksConstants.TAG, + Log.d(TAG, "Restoring stats of track with ID: " + track.getId()); TripStatistics stats = track.getStatistics(); statsBuilder = new TripStatisticsBuilder(stats.getStartTime()); statsBuilder.setMinRecordingDistance(minRecordingDistance); - setUpAnnouncer(); - splitManager.restore(); length = 0; lastValidLocation = null; @@ -446,13 +446,13 @@ public class TrackRecordingService extends Service implements LocationListener { Cursor cursor = null; try { cursor = providerUtils.getLocationsCursor( - recordingTrackId, -1, MyTracksConstants.MAX_LOADED_TRACK_POINTS, + recordingTrackId, -1, Constants.MAX_LOADED_TRACK_POINTS, true); if (cursor != null) { if (cursor.moveToLast()) { do { Location location = providerUtils.createLocation(cursor); - if (MyTracksUtils.isValidLocation(location)) { + if (LocationUtils.isValidLocation(location)) { statsBuilder.addLocation(location, location.getTime()); if (lastValidLocation != null) { length += location.distanceTo(lastValidLocation); @@ -465,17 +465,18 @@ public class TrackRecordingService extends Service implements LocationListener { statsBuilder.pauseAt(stats.getStopTime()); statsBuilder.resumeAt(System.currentTimeMillis()); } else { - Log.e(MyTracksConstants.TAG, "Could not get track points cursor."); + Log.e(TAG, "Could not get track points cursor."); } } catch (RuntimeException e) { - Log.e(MyTracksConstants.TAG, "Error while restoring track.", e); + Log.e(TAG, "Error while restoring track.", e); } finally { if (cursor != null) { cursor.close(); } } - splitManager.calculateNextSplit(); + announcementExecutor.restore(); + splitExecutor.restore(); } /* @@ -483,7 +484,7 @@ public class TrackRecordingService extends Service implements LocationListener { */ @Override public void onLocationChanged(final Location location) { - this.executerServce.submit( + executorService.submit( new Runnable() { @Override public void run() { @@ -493,12 +494,12 @@ public class TrackRecordingService extends Service implements LocationListener { } private void onLocationChangedAsync(Location location) { - Log.d(MyTracksConstants.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(MyTracksConstants.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(MyTracksConstants.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(MyTracksConstants.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(MyTracksConstants.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(MyTracksConstants.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(MyTracksConstants.TAG, + Log.d(TAG, "Not recording. More than two identical locations."); } } else if (distanceToLastRecorded > minRecordingDistance @@ -595,8 +596,8 @@ public class TrackRecordingService extends Service implements LocationListener { && recordingTrack.getStartId() >= 0; if (startNewSegment) { // Insert a separator point to indicate start of new track: - Log.d(MyTracksConstants.TAG, "Inserting a separator."); - Location separator = new Location(MyTracksConstants.GPS_PROVIDER); + Log.d(TAG, "Inserting a separator."); + Location separator = new Location(LocationManager.GPS_PROVIDER); separator.setLongitude(0); separator.setLatitude(100); separator.setTime(lastRecordedLocation.getTime()); @@ -608,7 +609,7 @@ public class TrackRecordingService extends Service implements LocationListener { return; } } else { - Log.d(MyTracksConstants.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(MyTracksConstants.TAG, "Error in onLocationChanged", e); + Log.e(TAG, "Error in onLocationChanged", e); throw e; } catch (RuntimeException e) { // Safe usually to trap exceptions. - Log.e(MyTracksConstants.TAG, + Log.e(TAG, "Trapping exception in onLocationChanged", e); throw e; } @@ -628,7 +629,7 @@ public class TrackRecordingService extends Service implements LocationListener { } private void addLocationToStats(Location location) { - if (MyTracksUtils.isValidLocation(location)) { + if (LocationUtils.isValidLocation(location)) { long now = System.currentTimeMillis(); statsBuilder.addLocation(location, now); waypointStatsBuilder.addLocation(location, now); @@ -657,13 +658,13 @@ public class TrackRecordingService extends Service implements LocationListener { @Override public void onCreate() { super.onCreate(); - Log.d(MyTracksConstants.TAG, "TrackRecordingService.onCreate"); + Log.d(TAG, "TrackRecordingService.onCreate"); providerUtils = MyTracksProviderUtils.Factory.get(this); notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); locationManager = (LocationManager) getSystemService(LOCATION_SERVICE); - splitManager = new SplitManager(this); + setUpTaskExecutors(); prefManager = new PreferenceManager(this); registerLocationListener(); @@ -682,61 +683,41 @@ public class TrackRecordingService extends Service implements LocationListener { } else { if (recordingTrackId != -1) { // Make sure we have consistent state in shared preferences. - Log.w(MyTracksConstants.TAG, "TrackRecordingService.onCreate: " + Log.w(TAG, "TrackRecordingService.onCreate: " + "Resetting an orphaned recording track = " + recordingTrackId); } prefManager.setRecordingTrack(recordingTrackId = -1); } showNotification(); - executerServce = Executors.newSingleThreadExecutor(); + executorService = Executors.newSingleThreadExecutor(); } /** - * Creates an {@link Executer} and schedules {@class SafeStatusAnnouncerTask}. - * The announcer requires a TTS service and user should have enabled - * the announcements, otherwise this method is no-op. + * Creates the periodic task executors. */ - private void setUpAnnouncer() { - Log.d(MyTracksConstants.TAG, "TrackRecordingService.setUpAnnouncer: " - + announcementExecuter); - if (announcementFrequency != -1 && recordingTrackId != -1) { - handler.post(new Runnable() { - @Override - public void run() { - if (announcementExecuter == null) { - StatusAnnouncerFactory statusAnnouncerFactory = - new StatusAnnouncerFactory(ApiFeatures.getInstance()); - PeriodicTask announcer = statusAnnouncerFactory.create( - TrackRecordingService.this); - if (announcer == null) { - return; - } - - // TODO: Either use TaskExecuterManager everywhere, or get rid of it - announcementExecuter = new PeriodicTaskExecuter(announcer, - TrackRecordingService.this); - } - announcementExecuter.scheduleTask(announcementFrequency * 60000); - } - }); - } + private void setUpTaskExecutors() { + announcementExecutor = new PeriodicTaskExecutor( + this, new StatusAnnouncerFactory(ApiFeatures.getInstance())); + splitExecutor = new PeriodicTaskExecutor(this, new SplitTask.Factory()); } - private void shutdownAnnouncer() { - Log.d(MyTracksConstants.TAG, "TrackRecordingService.shutdownAnnouncer: " - + announcementExecuter); - if (announcementExecuter != null) { - try { - announcementExecuter.shutdown(); - } finally { - announcementExecuter = null; - } + private void shutdownTaskExecutors() { + Log.d(TAG, "TrackRecordingService.shutdownExecuters"); + try { + announcementExecutor.shutdown(); + } finally { + announcementExecutor = null; + } + try { + splitExecutor.shutdown(); + } finally { + splitExecutor = null; } } @Override public void onDestroy() { - Log.d(MyTracksConstants.TAG, "TrackRecordingService.onDestroy"); + Log.d(TAG, "TrackRecordingService.onDestroy"); isRecording = false; showNotification(); @@ -747,9 +728,7 @@ public class TrackRecordingService extends Service implements LocationListener { timer.cancel(); timer.purge(); unregisterLocationListener(); - shutdownAnnouncer(); - splitManager.shutdown(); - splitManager = null; + shutdownTaskExecutors(); if (sensorManager != null) { sensorManager.shutdown(); sensorManager = null; @@ -770,19 +749,19 @@ public class TrackRecordingService extends Service implements LocationListener { @Override public IBinder onBind(Intent intent) { - Log.d(MyTracksConstants.TAG, "TrackRecordingService.onBind"); + Log.d(TAG, "TrackRecordingService.onBind"); return binder; } @Override public boolean onUnbind(Intent intent) { - Log.d(MyTracksConstants.TAG, "TrackRecordingService.onUnbind"); + Log.d(TAG, "TrackRecordingService.onUnbind"); return super.onUnbind(intent); } @Override public boolean stopService(Intent name) { - Log.d(MyTracksConstants.TAG, "TrackRecordingService.stopService"); + Log.d(TAG, "TrackRecordingService.stopService"); unregisterLocationListener(); return super.stopService(name); } @@ -799,17 +778,17 @@ public class TrackRecordingService extends Service implements LocationListener { } private void handleStartCommand(Intent intent, int startId) { - Log.d(MyTracksConstants.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(MyTracksConstants.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(MyTracksConstants.TAG, + Log.i(TAG, "TrackRecordingService: Not resuming, because the previous track (" + recordingTrack + ") doesn't exist or is too old"); isRecording = false; @@ -818,31 +797,31 @@ public class TrackRecordingService extends Service implements LocationListener { return; } - Log.i(MyTracksConstants.TAG, "TrackRecordingService: resuming"); + Log.i(TAG, "TrackRecordingService: resuming"); } } private void setAutoResumeTrackRetries( SharedPreferences sharedPreferences, int retryAttempts) { - Log.d(MyTracksConstants.TAG, + Log.d(TAG, "Updating auto-resume retry attempts to: " + retryAttempts); prefManager.setAutoResumeTrackCurrentRetry(retryAttempts); } private boolean shouldResumeTrack(Track track) { - Log.d(MyTracksConstants.TAG, "shouldResumeTrack: autoResumeTrackTimeout = " + Log.d(TAG, "shouldResumeTrack: autoResumeTrackTimeout = " + autoResumeTrackTimeout); // Check if we haven't exceeded the maximum number of retry attempts. SharedPreferences sharedPreferences = - getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0); + getSharedPreferences(Constants.SETTINGS_NAME, 0); int retries = sharedPreferences.getInt( getString(R.string.auto_resume_track_current_retry_key), 0); - Log.d(MyTracksConstants.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(MyTracksConstants.TAG, + Log.i(TAG, "shouldResumeTrack: Not resuming because exceeded the maximum " + "number of auto-resume retries"); return false; @@ -854,12 +833,12 @@ public class TrackRecordingService extends Service implements LocationListener { // Check for special cases. if (autoResumeTrackTimeout == 0) { // Never resume. - Log.d(MyTracksConstants.TAG, + Log.d(TAG, "shouldResumeTrack: Auto-resume disabled (never resume)"); return false; } else if (autoResumeTrackTimeout == -1) { // Always resume. - Log.d(MyTracksConstants.TAG, + Log.d(TAG, "shouldResumeTrack: Auto-resume forced (always resume)"); return true; } @@ -867,7 +846,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(MyTracksConstants.TAG, + Log.d(TAG, "shouldResumeTrack: lastModified = " + lastModified + ", autoResumeTrackTimeout: " + autoResumeTrackTimeout); return lastModified > 0 && System.currentTimeMillis() - lastModified <= @@ -897,15 +876,15 @@ 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. Location l = new Location(""); l.setLatitude(100); - l.setLongitude(200); + l.setLongitude(180); wpt.setLocation(l); } else { - // A null location is ok, and expected on track start. wpt.setLocation(lastLocation); wpt.setDuration(lastLocation.getTime() - statsBuilder.getStatistics().getStartTime()); @@ -1056,11 +1035,11 @@ public class TrackRecordingService extends Service implements LocationListener { public byte[] getSensorData() { checkService(); if (service.sensorManager == null) { - Log.d(MyTracksConstants.TAG, "No sensor manager for data."); + Log.d(TAG, "No sensor manager for data."); return null; } if (service.sensorManager.getSensorDataSet() == null) { - Log.d(MyTracksConstants.TAG, "Sensor data set is null."); + Log.d(TAG, "Sensor data set is null."); return null; } return service.sensorManager.getSensorDataSet().toByteArray(); @@ -1070,7 +1049,7 @@ public class TrackRecordingService extends Service implements LocationListener { public int getSensorState() { checkService(); if (service.sensorManager == null) { - Log.d(MyTracksConstants.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 +1057,7 @@ public class TrackRecordingService extends Service implements LocationListener { } public long startNewTrack() { - Log.d(MyTracksConstants.TAG, "TrackRecordingService.startNewTrack"); + Log.d(TAG, "TrackRecordingService.startNewTrack"); if (recordingTrackId != -1 || isRecording) { throw new IllegalStateException("A track is already in progress!"); } @@ -1097,18 +1076,16 @@ public class TrackRecordingService extends Service implements LocationListener { recordingTrackId, startTime)); isRecording = true; isMoving = true; - + providerUtils.updateTrack(track); statsBuilder = new TripStatisticsBuilder(startTime); statsBuilder.setMinRecordingDistance(minRecordingDistance); waypointStatsBuilder = new TripStatisticsBuilder(startTime); waypointStatsBuilder.setMinRecordingDistance(minRecordingDistance); currentWaypointId = insertWaypoint(WaypointCreationRequest.DEFAULT_STATISTICS); - setUpAnnouncer(); length = 0; showNotification(); registerLocationListener(); - splitManager.restore(); sensorManager = SensorManagerFactory.getSensorManager(this); if (sensorManager != null) { sensorManager.onStartTrack(); @@ -1116,24 +1093,27 @@ public class TrackRecordingService extends Service implements LocationListener { // Reset the number of auto-resume retries. setAutoResumeTrackRetries( - getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0), 0); + getSharedPreferences(Constants.SETTINGS_NAME, 0), 0); // Persist the current recording track. prefManager.setRecordingTrack(recordingTrackId); // Notify the world that we're now recording. sendTrackBroadcast( R.string.track_started_broadcast_action, recordingTrackId); + announcementExecutor.restore(); + splitExecutor.restore(); return recordingTrackId; } private void endCurrentTrack() { - Log.d(MyTracksConstants.TAG, "TrackRecordingService.endCurrentTrack"); + Log.d(TAG, "TrackRecordingService.endCurrentTrack"); if (recordingTrackId == -1 || !isRecording) { throw new IllegalStateException("No recording track in progress!"); } - shutdownAnnouncer(); + announcementExecutor.shutdown(); + splitExecutor.shutdown(); isRecording = false; Track recordingTrack = providerUtils.getTrack(recordingTrackId); if (recordingTrack != null) { @@ -1193,19 +1173,6 @@ public class TrackRecordingService extends Service implements LocationListener { this.recordingTrackId = recordingTrackId; } - public int getAnnouncementFrequency() { - return announcementFrequency; - } - - public void setAnnouncementFrequency(int announcementFrequency) { - this.announcementFrequency = announcementFrequency; - if (announcementFrequency == -1) { - shutdownAnnouncer(); - } else { - setUpAnnouncer(); - } - } - public int getMaxRecordingDistance() { return maxRecordingDistance; } @@ -1251,7 +1218,16 @@ public class TrackRecordingService extends Service implements LocationListener { this.autoResumeTrackTimeout = autoResumeTrackTimeout; } - public SplitManager getSplitManager() { - return splitManager; + public void setAnnouncementFrequency(int announcementFrequency) { + announcementExecutor.setTaskFrequency(announcementFrequency); + } + + public void setSplitFrequency(int frequency) { + splitExecutor.setTaskFrequency(frequency); + } + + public void setMetricUnits(boolean metric) { + announcementExecutor.setMetricUnits(metric); + splitExecutor.setMetricUnits(metric); } } diff --git a/MyTracks/src/com/google/android/apps/mytracks/services/sensors/BluetoothConnectionManager.java b/MyTracks/src/com/google/android/apps/mytracks/services/sensors/BluetoothConnectionManager.java index 986ae773b..3f95db95f 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/services/sensors/BluetoothConnectionManager.java +++ b/MyTracks/src/com/google/android/apps/mytracks/services/sensors/BluetoothConnectionManager.java @@ -16,7 +16,7 @@ package com.google.android.apps.mytracks.services.sensors; -import com.google.android.apps.mytracks.MyTracksConstants; +import com.google.android.apps.mytracks.Constants; import com.google.android.apps.mytracks.content.Sensor; import android.bluetooth.BluetoothAdapter; @@ -85,7 +85,7 @@ public class BluetoothConnectionManager { */ private synchronized void setState(Sensor.SensorState state) { // TODO pretty print this. - Log.d(MyTracksConstants.TAG, "setState(" + state + ")"); + Log.d(Constants.TAG, "setState(" + state + ")"); this.state = state; // Give the new state to the Handler so the UI Activity can update @@ -104,7 +104,7 @@ public class BluetoothConnectionManager { * in listening (server) mode. Called by the Activity onResume() */ public synchronized void start() { - Log.d(MyTracksConstants.TAG, "BluetoothConnectionManager.start()"); + Log.d(Constants.TAG, "BluetoothConnectionManager.start()"); // Cancel any thread attempting to make a connection if (connectThread != null) { @@ -127,7 +127,7 @@ public class BluetoothConnectionManager { * @param device The BluetoothDevice to connect */ public synchronized void connect(BluetoothDevice device) { - Log.d(MyTracksConstants.TAG, "connect to: " + device); + Log.d(Constants.TAG, "connect to: " + device); // Cancel any thread attempting to make a connection if (state == Sensor.SensorState.CONNECTING) { @@ -157,7 +157,7 @@ public class BluetoothConnectionManager { */ public synchronized void connected(BluetoothSocket socket, BluetoothDevice device) { - Log.d(MyTracksConstants.TAG, "connected"); + Log.d(Constants.TAG, "connected"); // Cancel the thread that completed the connection if (connectThread != null) { @@ -189,7 +189,7 @@ public class BluetoothConnectionManager { * Stop all threads */ public synchronized void stop() { - Log.d(MyTracksConstants.TAG, "stop()"); + Log.d(Constants.TAG, "stop()"); if (connectThread != null) { connectThread.cancel(); connectThread = null; @@ -226,7 +226,7 @@ public class BluetoothConnectionManager { */ private void connectionFailed() { setState(Sensor.SensorState.DISCONNECTED); - Log.i(MyTracksConstants.TAG, "Bluetooth connection failed."); + Log.i(Constants.TAG, "Bluetooth connection failed."); } /** @@ -234,7 +234,7 @@ public class BluetoothConnectionManager { */ private void connectionLost() { setState(Sensor.SensorState.DISCONNECTED); - Log.i(MyTracksConstants.TAG, "Bluetooth connection lost."); + Log.i(Constants.TAG, "Bluetooth connection lost."); } /** @@ -255,7 +255,7 @@ public class BluetoothConnectionManager { try { tmp = getSocket(); } catch (IOException e) { - Log.e(MyTracksConstants.TAG, "create() failed", e); + Log.e(Constants.TAG, "create() failed", e); } socket = tmp; } @@ -268,22 +268,22 @@ public class BluetoothConnectionManager { insecure.setAccessible(true); return (BluetoothSocket) insecure.invoke(device, 1); } catch (SecurityException e) { - Log.e(MyTracksConstants.TAG, "Unable to get insecure connect.", e); + Log.e(Constants.TAG, "Unable to get insecure connect.", e); } catch (NoSuchMethodException e) { - Log.e(MyTracksConstants.TAG, "Unable to get insecure connect.", e); + Log.e(Constants.TAG, "Unable to get insecure connect.", e); } catch (IllegalArgumentException e) { - Log.e(MyTracksConstants.TAG, "Unable to get insecure connect.", e); + Log.e(Constants.TAG, "Unable to get insecure connect.", e); } catch (IllegalAccessException e) { - Log.e(MyTracksConstants.TAG, "Unable to get insecure connect.", e); + Log.e(Constants.TAG, "Unable to get insecure connect.", e); } catch (InvocationTargetException e) { - Log.e(MyTracksConstants.TAG, "Unable to get insecure connect.", e); + Log.e(Constants.TAG, "Unable to get insecure connect.", e); } return device.createRfcommSocketToServiceRecord(SPP_UUID); } @Override public void run() { - Log.d(MyTracksConstants.TAG, "BEGIN mConnectThread"); + Log.d(Constants.TAG, "BEGIN mConnectThread"); // Always cancel discovery because it will slow down a connection adapter.cancelDiscovery(); @@ -299,7 +299,7 @@ public class BluetoothConnectionManager { try { socket.close(); } catch (IOException e2) { - Log.e(MyTracksConstants.TAG, + Log.e(Constants.TAG, "unable to close() socket during connection failure", e2); } // Start the service over to restart listening mode @@ -320,7 +320,7 @@ public class BluetoothConnectionManager { try { socket.close(); } catch (IOException e) { - Log.e(MyTracksConstants.TAG, "close() of connect socket failed", e); + Log.e(Constants.TAG, "close() of connect socket failed", e); } } } @@ -335,7 +335,7 @@ public class BluetoothConnectionManager { private final OutputStream mmOutStream; public ConnectedThread(BluetoothSocket socket) { - Log.d(MyTracksConstants.TAG, "create ConnectedThread"); + Log.d(Constants.TAG, "create ConnectedThread"); btSocket = socket; InputStream tmpIn = null; OutputStream tmpOut = null; @@ -345,7 +345,7 @@ public class BluetoothConnectionManager { tmpIn = socket.getInputStream(); tmpOut = socket.getOutputStream(); } catch (IOException e) { - Log.e(MyTracksConstants.TAG, "temp sockets not created", e); + Log.e(Constants.TAG, "temp sockets not created", e); } mmInStream = tmpIn; @@ -354,7 +354,7 @@ public class BluetoothConnectionManager { @Override public void run() { - Log.i(MyTracksConstants.TAG, "BEGIN mConnectedThread"); + Log.i(Constants.TAG, "BEGIN mConnectedThread"); byte[] buffer = new byte[parser.getFrameSize()]; int bytes; int offset = 0; @@ -383,11 +383,11 @@ public class BluetoothConnectionManager { // re-align offset = parser.getFrameSize() - index; System.arraycopy(buffer, index, buffer, 0, offset); - Log.w(MyTracksConstants.TAG, "Misaligned data, found new message at " + + Log.w(Constants.TAG, "Misaligned data, found new message at " + index + " recovering..."); continue; } - Log.w(MyTracksConstants.TAG, "Could not find valid data, dropping data"); + Log.w(Constants.TAG, "Could not find valid data, dropping data"); offset = 0; continue; } @@ -398,7 +398,7 @@ public class BluetoothConnectionManager { handler.obtainMessage(MESSAGE_READ, bytes, -1, buffer) .sendToTarget(); } catch (IOException e) { - Log.e(MyTracksConstants.TAG, "disconnected", e); + Log.e(Constants.TAG, "disconnected", e); connectionLost(); break; } @@ -417,7 +417,7 @@ public class BluetoothConnectionManager { // Share the sent message back to the UI Activity handler.obtainMessage(MESSAGE_WRITE, -1, -1, buffer).sendToTarget(); } catch (IOException e) { - Log.e(MyTracksConstants.TAG, "Exception during write", e); + Log.e(Constants.TAG, "Exception during write", e); } } @@ -425,7 +425,7 @@ public class BluetoothConnectionManager { try { btSocket.close(); } catch (IOException e) { - Log.e(MyTracksConstants.TAG, "close() of connect socket failed", e); + Log.e(Constants.TAG, "close() of connect socket failed", e); } } } diff --git a/MyTracks/src/com/google/android/apps/mytracks/services/sensors/BluetoothSensorManager.java b/MyTracks/src/com/google/android/apps/mytracks/services/sensors/BluetoothSensorManager.java index 32b16d1e4..41fed2e52 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/services/sensors/BluetoothSensorManager.java +++ b/MyTracks/src/com/google/android/apps/mytracks/services/sensors/BluetoothSensorManager.java @@ -15,8 +15,7 @@ */ package com.google.android.apps.mytracks.services.sensors; -import com.google.android.apps.mytracks.MyTracksConstants; -import com.google.android.apps.mytracks.MyTracksSettings; +import com.google.android.apps.mytracks.Constants; import com.google.android.apps.mytracks.content.Sensor; import com.google.android.maps.mytracks.R; @@ -66,7 +65,7 @@ public class BluetoothSensorManager extends SensorManager { } private void setupSensor() { - Log.d(MyTracksConstants.TAG, "setupSensor()"); + Log.d(Constants.TAG, "setupSensor()"); // Initialize the BluetoothSensorAdapter to perform bluetooth connections. connectionManager = new BluetoothConnectionManager(messageHandler, parser); @@ -78,17 +77,17 @@ public class BluetoothSensorManager extends SensorManager { public void setupChannel() { if (!isEnabled() || connectionManager == null) { - Log.w(MyTracksConstants.TAG, "Disabled manager onStartTrack"); + Log.w(Constants.TAG, "Disabled manager onStartTrack"); return; } SharedPreferences prefs = - context.getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0); + context.getSharedPreferences(Constants.SETTINGS_NAME, 0); String address = prefs.getString(context.getString(R.string.bluetooth_sensor_key), null); if (address == null) { return; } - Log.w(MyTracksConstants.TAG, "Connecting to bluetooth sensor: " + address); + Log.w(Constants.TAG, "Connecting to bluetooth sensor: " + address); // Get the BluetoothDevice object BluetoothDevice device = bluetoothAdapter.getRemoteDevice(address); // Attempt to connect to the device @@ -102,7 +101,7 @@ public class BluetoothSensorManager extends SensorManager { // already if (connectionManager.getState() == Sensor.SensorState.NONE) { // Start the Bluetooth sensor services - Log.w(MyTracksConstants.TAG, "Disabled manager onStartTrack"); + Log.w(Constants.TAG, "Disabled manager onStartTrack"); connectionManager.start(); } } @@ -132,7 +131,7 @@ public class BluetoothSensorManager extends SensorManager { switch (msg.what) { case BluetoothConnectionManager.MESSAGE_STATE_CHANGE: // TODO should we update the SensorManager state var? - Log.i(MyTracksConstants.TAG, "MESSAGE_STATE_CHANGE: " + msg.arg1); + Log.i(Constants.TAG, "MESSAGE_STATE_CHANGE: " + msg.arg1); break; case BluetoothConnectionManager.MESSAGE_WRITE: break; @@ -141,15 +140,15 @@ public class BluetoothSensorManager extends SensorManager { try { readBuf = (byte[]) msg.obj; sensorDataSet = parser.parseBuffer(readBuf); - Log.d(MyTracksConstants.TAG, "MESSAGE_READ: " + sensorDataSet.toString()); + Log.d(Constants.TAG, "MESSAGE_READ: " + sensorDataSet.toString()); } catch (IllegalArgumentException iae) { sensorDataSet = null; - Log.i(MyTracksConstants.TAG, + Log.i(Constants.TAG, "Got bad sensor data: " + new String(readBuf, 0, readBuf.length), iae); } catch (RuntimeException re) { sensorDataSet = null; - Log.i(MyTracksConstants.TAG, "Unexpected exception on read.", re); + Log.i(Constants.TAG, "Unexpected exception on read.", re); } break; case BluetoothConnectionManager.MESSAGE_DEVICE_NAME: diff --git a/MyTracks/src/com/google/android/apps/mytracks/services/sensors/SensorManager.java b/MyTracks/src/com/google/android/apps/mytracks/services/sensors/SensorManager.java index 0ebd856a8..cb217e590 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/services/sensors/SensorManager.java +++ b/MyTracks/src/com/google/android/apps/mytracks/services/sensors/SensorManager.java @@ -20,7 +20,7 @@ import java.util.TimerTask; import android.util.Log; -import com.google.android.apps.mytracks.MyTracksConstants; +import com.google.android.apps.mytracks.Constants; import com.google.android.apps.mytracks.content.Sensor; /** @@ -50,18 +50,18 @@ public abstract class SensorManager { private TimerTask checkSensorManager = new TimerTask() { @Override public void run() { - Log.i(MyTracksConstants.TAG, + Log.i(Constants.TAG, "SensorManager state: " + getSensorState()); switch (getSensorState()) { case CONNECTING: long age = System.currentTimeMillis() - getSensorStateTimestamp(); if (age > 2 * RETRY_PERIOD) { - Log.i(MyTracksConstants.TAG, "Retrying connecting SensorManager."); + Log.i(Constants.TAG, "Retrying connecting SensorManager."); setupChannel(); } break; case DISCONNECTED: - Log.i(MyTracksConstants.TAG, + Log.i(Constants.TAG, "Re-registering disconnected SensoManager."); setupChannel(); break; diff --git a/MyTracks/src/com/google/android/apps/mytracks/services/sensors/SensorManagerFactory.java b/MyTracks/src/com/google/android/apps/mytracks/services/sensors/SensorManagerFactory.java index cad3e707b..43e0a4be0 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/services/sensors/SensorManagerFactory.java +++ b/MyTracks/src/com/google/android/apps/mytracks/services/sensors/SensorManagerFactory.java @@ -15,8 +15,9 @@ */ package com.google.android.apps.mytracks.services.sensors; -import com.google.android.apps.mytracks.MyTracksConstants; -import com.google.android.apps.mytracks.MyTracksSettings; +import com.google.android.apps.mytracks.Constants; +import com.google.android.apps.mytracks.services.sensors.ant.AntDirectSensorManager; +import com.google.android.apps.mytracks.services.sensors.ant.AntSRMSensorManager; import com.google.android.maps.mytracks.R; import android.content.Context; @@ -40,13 +41,13 @@ public class SensorManagerFactory { */ public static SensorManager getSensorManager(Context context) { SharedPreferences prefs = - context.getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0); + context.getSharedPreferences(Constants.SETTINGS_NAME, 0); if (prefs == null) { return null; } String sensor = prefs.getString(context.getString(R.string.sensor_type_key), null); - Log.i(MyTracksConstants.TAG, "Creating sensor of type: " + sensor); + Log.i(Constants.TAG, "Creating sensor of type: " + sensor); if (sensor == null) { return null; @@ -57,7 +58,7 @@ public class SensorManagerFactory { } else if (sensor.equals(context.getString(R.string.zephyr_sensor_type))) { return new ZephyrSensorManager(context); } else { - Log.w(MyTracksConstants.TAG, "Unable to find sensor type: " + sensor); + Log.w(Constants.TAG, "Unable to find sensor type: " + sensor); return null; } } diff --git a/MyTracks/src/com/google/android/apps/mytracks/services/sensors/ZephyrMessageParser.java b/MyTracks/src/com/google/android/apps/mytracks/services/sensors/ZephyrMessageParser.java index 3a2fbf9e4..da7a3672e 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/services/sensors/ZephyrMessageParser.java +++ b/MyTracks/src/com/google/android/apps/mytracks/services/sensors/ZephyrMessageParser.java @@ -17,7 +17,7 @@ package com.google.android.apps.mytracks.services.sensors; import android.util.Log; -import com.google.android.apps.mytracks.MyTracksConstants; +import com.google.android.apps.mytracks.Constants; import com.google.android.apps.mytracks.content.Sensor; /** @@ -33,7 +33,7 @@ public class ZephyrMessageParser implements MessageParser { for (int i = 0; i < buffer.length; i++) { sb.append(String.format("%02X", buffer[i])); } - Log.w(MyTracksConstants.TAG, "Got zephyr data: " + sb); + Log.w(Constants.TAG, "Got zephyr data: " + sb); // The provided units are 1/16 strides per minute. // TODO: Fix the cadence calculation. // int cadence = SensorUtils.unsignedShortToInt(buffer, 56); diff --git a/MyTracks/src/com/google/android/apps/mytracks/services/sensors/AntDirectSensorManager.java b/MyTracks/src/com/google/android/apps/mytracks/services/sensors/ant/AntDirectSensorManager.java similarity index 92% rename from MyTracks/src/com/google/android/apps/mytracks/services/sensors/AntDirectSensorManager.java rename to MyTracks/src/com/google/android/apps/mytracks/services/sensors/ant/AntDirectSensorManager.java index cc102537a..c8cc2c1ed 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/services/sensors/AntDirectSensorManager.java +++ b/MyTracks/src/com/google/android/apps/mytracks/services/sensors/ant/AntDirectSensorManager.java @@ -13,13 +13,12 @@ * License for the specific language governing permissions and limitations under * the License. */ -package com.google.android.apps.mytracks.services.sensors; +package com.google.android.apps.mytracks.services.sensors.ant; -import static com.google.android.apps.mytracks.MyTracksConstants.TAG; -import com.google.android.apps.mytracks.MyTracksSettings; +import static com.google.android.apps.mytracks.Constants.TAG; + +import com.google.android.apps.mytracks.Constants; import com.google.android.apps.mytracks.content.Sensor; -import com.google.android.apps.mytracks.services.sensors.ant.AntChannelIdMessage; -import com.google.android.apps.mytracks.services.sensors.ant.AntChannelResponseMessage; import com.google.android.maps.mytracks.R; import com.dsi.ant.AntDefine; @@ -58,7 +57,7 @@ public class AntDirectSensorManager extends AntSensorManager { // First read the the device id that we will be pairing with. SharedPreferences prefs = context.getSharedPreferences( - MyTracksSettings.SETTINGS_NAME, 0); + Constants.SETTINGS_NAME, 0); if (prefs != null) { deviceNumberHRM = (short) prefs.getInt(context.getString(R.string.ant_heart_rate_sensor_id_key), 0); @@ -133,7 +132,7 @@ public class AntDirectSensorManager extends AntSensorManager { Log.i(TAG, "Found device id: " + deviceNumberHRM); SharedPreferences prefs = context.getSharedPreferences( - MyTracksSettings.SETTINGS_NAME, Context.MODE_PRIVATE); + Constants.SETTINGS_NAME, Context.MODE_PRIVATE); SharedPreferences.Editor editor = prefs.edit(); editor.putInt(context.getString(R.string.ant_heart_rate_sensor_id_key), deviceNumberHRM); editor.commit(); diff --git a/MyTracks/src/com/google/android/apps/mytracks/services/sensors/AntSRMSensorManager.java b/MyTracks/src/com/google/android/apps/mytracks/services/sensors/ant/AntSRMSensorManager.java similarity index 82% rename from MyTracks/src/com/google/android/apps/mytracks/services/sensors/AntSRMSensorManager.java rename to MyTracks/src/com/google/android/apps/mytracks/services/sensors/ant/AntSRMSensorManager.java index d46718362..632080697 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/services/sensors/AntSRMSensorManager.java +++ b/MyTracks/src/com/google/android/apps/mytracks/services/sensors/ant/AntSRMSensorManager.java @@ -13,16 +13,17 @@ * License for the specific language governing permissions and limitations under * the License. */ -package com.google.android.apps.mytracks.services.sensors; +package com.google.android.apps.mytracks.services.sensors.ant; -import com.google.android.apps.mytracks.MyTracksConstants; -import com.google.android.apps.mytracks.MyTracksSettings; -import com.google.android.apps.mytracks.content.Sensor; -import com.google.android.maps.mytracks.R; +import static com.google.android.apps.mytracks.Constants.TAG; import com.dsi.ant.AntDefine; import com.dsi.ant.AntMesg; import com.dsi.ant.exception.AntInterfaceException; +import com.google.android.apps.mytracks.Constants; +import com.google.android.apps.mytracks.content.Sensor; +import com.google.android.apps.mytracks.services.sensors.SensorUtils; +import com.google.android.maps.mytracks.R; import android.content.Context; import android.content.SharedPreferences; @@ -71,7 +72,7 @@ public class AntSRMSensorManager extends AntSensorManager { // First read the the device id that we will be announcing. SharedPreferences prefs = context.getSharedPreferences( - MyTracksSettings.SETTINGS_NAME, 0); + Constants.SETTINGS_NAME, 0); if (prefs != null) { deviceId = (byte) prefs.getInt( context.getString(R.string.ant_srm_bridge_sensor_id_key), @@ -85,7 +86,7 @@ public class AntSRMSensorManager extends AntSensorManager { byte recievedChannel = (byte) (antMessage[AntMesg.MESG_DATA_OFFSET] & AntDefine.CHANNEL_NUMBER_MASK); if (recievedChannel != channel) { - Log.d(MyTracksConstants.TAG, "Unexpected channel: " + recievedChannel); + Log.d(TAG, "Unexpected channel: " + recievedChannel); return; } switch (antMessage[AntMesg.MESG_ID_OFFSET]) { @@ -99,18 +100,17 @@ public class AntSRMSensorManager extends AntSensorManager { handleChannelId(antMessage); break; default: - Log.e(MyTracksConstants.TAG, - "Unexpected message id: " + antMessage[3]); + Log.e(TAG, "Unexpected message id: " + antMessage[3]); } } private void handleBroadcastData(byte[] antMessage) { if (deviceId == WILDCARD) { try { - Log.d(MyTracksConstants.TAG, "Requesting channel id id."); + Log.d(TAG, "Requesting channel id id."); getAntReceiver().ANTRequestMessage(channel, AntMesg.MESG_CHANNEL_ID_ID); } catch (AntInterfaceException e) { - Log.e(MyTracksConstants.TAG, "Failed to request channel id id", e); + Log.e(TAG, "Failed to request channel id id", e); } } setSensorState(Sensor.SensorState.CONNECTED); @@ -122,18 +122,17 @@ public class AntSRMSensorManager extends AntSensorManager { parseSensorData(antMessage); break; default: - Log.e(MyTracksConstants.TAG, - "Unexpected message type: " + antMessage[MESSAGE_TYPE_INDEX]); + Log.e(TAG, "Unexpected message type: " + antMessage[MESSAGE_TYPE_INDEX]); } } private void handleChannelId(byte[] antMessage) { // Store the device id. deviceId = antMessage[3]; - Log.i(MyTracksConstants.TAG, "Found device id: " + deviceId); + Log.i(TAG, "Found device id: " + deviceId); SharedPreferences prefs = context.getSharedPreferences( - MyTracksSettings.SETTINGS_NAME, Context.MODE_PRIVATE); + Constants.SETTINGS_NAME, Context.MODE_PRIVATE); SharedPreferences.Editor editor = prefs.edit(); editor.putInt(context.getString(R.string.ant_srm_bridge_sensor_id_key), deviceId); editor.commit(); @@ -143,38 +142,34 @@ public class AntSRMSensorManager extends AntSensorManager { if (antMessage[3] == AntMesg.MESG_EVENT_ID && antMessage[4] == AntDefine.EVENT_RX_SEARCH_TIMEOUT) { // Search timeout - Log.w(MyTracksConstants.TAG, "Search timed out. Unassigning channel."); + Log.w(TAG, "Search timed out. Unassigning channel."); try { getAntReceiver().ANTUnassignChannel(channel); } catch (AntInterfaceException e) { - Log.e(MyTracksConstants.TAG, "Failed to unassign ANT channel", e); + Log.e(TAG, "Failed to unassign ANT channel", e); } setSensorState(Sensor.SensorState.DISCONNECTED); } else if (antMessage[3] == AntMesg.MESG_UNASSIGN_CHANNEL_ID) { setSensorState(Sensor.SensorState.DISCONNECTED); - Log.i(MyTracksConstants.TAG, - "Disconnected from the sensor: " + getSensorState()); + Log.i(TAG, "Disconnected from the sensor: " + getSensorState()); } } private void parseSensorData(byte[] antMessage) { if (antMessage.length != 11) { - Log.e(MyTracksConstants.TAG, - "Unexpected ant message length: " + antMessage.length); + Log.e(TAG, "Unexpected ant message length: " + antMessage.length); return; } int newMessageId = antMessage[MESSAGE_ID_INDEX] & 0xFF; if (lastMessageId == newMessageId) { // Repeated message. - Log.i(MyTracksConstants.TAG, - String.format("SRM ignoring repeat: 0x%X", newMessageId)); + Log.i(TAG, String.format("SRM ignoring repeat: 0x%X", newMessageId)); return; } if (newMessageId < lastMessageId) { if (!(newMessageId < 20 && lastMessageId > 200)) { - Log.i(MyTracksConstants.TAG, - String.format("SRM ignoring repeat: 0x%X", newMessageId)); + Log.i(TAG, String.format("SRM ignoring repeat: 0x%X", newMessageId)); return; } // else assume the byte overflowed to 0. } diff --git a/MyTracks/src/com/google/android/apps/mytracks/services/sensors/ant/AntSensor.java b/MyTracks/src/com/google/android/apps/mytracks/services/sensors/ant/AntSensor.java new file mode 100644 index 000000000..beac735a5 --- /dev/null +++ b/MyTracks/src/com/google/android/apps/mytracks/services/sensors/ant/AntSensor.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 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.services.sensors.ant; + +/** + * A enum which stores static data about ANT sensors. + * + * @author Matthew Simmons + */ +public enum AntSensor { + SENSOR_HEART_RATE (Constants.ANT_DEVICE_TYPE_HRM), + SENSOR_CADENCE (Constants.ANT_DEVICE_TYPE_CADENCE), + SENSOR_SPEED (Constants.ANT_DEVICE_TYPE_SPEED), + SENSOR_POWER (Constants.ANT_DEVICE_TYPE_POWER); + + private static class Constants { + public static byte ANT_DEVICE_TYPE_POWER = 11; + public static byte ANT_DEVICE_TYPE_HRM = 120; + public static byte ANT_DEVICE_TYPE_CADENCE = 122; + public static byte ANT_DEVICE_TYPE_SPEED = 123; + }; + + private final byte deviceType; + + private AntSensor(byte deviceType) { + this.deviceType = deviceType; + } + + public byte getDeviceType() { + return deviceType; + } +} diff --git a/MyTracks/src/com/google/android/apps/mytracks/services/sensors/AntSensorManager.java b/MyTracks/src/com/google/android/apps/mytracks/services/sensors/ant/AntSensorManager.java similarity index 88% rename from MyTracks/src/com/google/android/apps/mytracks/services/sensors/AntSensorManager.java rename to MyTracks/src/com/google/android/apps/mytracks/services/sensors/ant/AntSensorManager.java index 2b0b136b1..fdd5066a4 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/services/sensors/AntSensorManager.java +++ b/MyTracks/src/com/google/android/apps/mytracks/services/sensors/ant/AntSensorManager.java @@ -13,18 +13,18 @@ * License for the specific language governing permissions and limitations under * the License. */ -package com.google.android.apps.mytracks.services.sensors; +package com.google.android.apps.mytracks.services.sensors.ant; -import com.google.android.apps.mytracks.MyTracksConstants; -import com.google.android.apps.mytracks.content.Sensor; -import com.google.android.apps.mytracks.content.Sensor.SensorDataSet; -import com.google.android.apps.mytracks.services.sensors.ant.AntStartupMessage; +import static com.google.android.apps.mytracks.Constants.TAG; import com.dsi.ant.AntDefine; import com.dsi.ant.AntInterface; import com.dsi.ant.AntInterfaceIntent; import com.dsi.ant.AntMesg; import com.dsi.ant.exception.AntInterfaceException; +import com.google.android.apps.mytracks.content.Sensor; +import com.google.android.apps.mytracks.content.Sensor.SensorDataSet; +import com.google.android.apps.mytracks.services.sensors.SensorManager; import android.content.BroadcastReceiver; import android.content.Context; @@ -84,7 +84,7 @@ public abstract class AntSensorManager extends SensorManager { @Override public void onReceive(Context context, Intent intent) { String antAction = intent.getAction(); - Log.i(MyTracksConstants.TAG, "enter status onReceive" + antAction); + Log.i(TAG, "enter status onReceive" + antAction); } }; @@ -93,12 +93,12 @@ public abstract class AntSensorManager extends SensorManager { @Override public void onReceive(Context context, Intent intent) { String antAction = intent.getAction(); - Log.i(MyTracksConstants.TAG, "enter data onReceive" + antAction); + Log.i(TAG, "enter data onReceive" + antAction); if (antAction.equals(AntInterfaceIntent.ANT_RX_MESSAGE_ACTION)) { byte[] antMessage = intent.getByteArrayExtra(AntInterfaceIntent.ANT_MESSAGE); if (DEBUGGING) { - Log.d(MyTracksConstants.TAG, "Received RX message " + messageToString(antMessage)); + Log.d(TAG, "Received RX message " + messageToString(antMessage)); } handleMessage(antMessage); @@ -119,7 +119,7 @@ public abstract class AntSensorManager extends SensorManager { @Override public void onServiceDisconnected() { - Log.d(MyTracksConstants.TAG, "ANT interface reports disconnection"); + Log.d(TAG, "ANT interface reports disconnection"); } }; @@ -129,14 +129,14 @@ public abstract class AntSensorManager extends SensorManager { @Override public void onDestroy() { - Log.i(MyTracksConstants.TAG, "destroying AntSensorManager"); + Log.i(TAG, "destroying AntSensorManager"); context.unregisterReceiver(statusReceiver); context.unregisterReceiver(dataReceiver); try { antReceiver.releaseInterface(); } catch (AntInterfaceException e) { - Log.e(MyTracksConstants.TAG, "failed to release ANT interface", e); + Log.e(TAG, "failed to release ANT interface", e); } antReceiver.destroy(); @@ -177,7 +177,7 @@ public abstract class AntSensorManager extends SensorManager { antReceiver = AntInterface.getInstance(context, antServiceListener); if (antReceiver == null) { - Log.e(MyTracksConstants.TAG, "Failed to get ANT Receiver"); + Log.e(TAG, "Failed to get ANT Receiver"); return; } @@ -190,11 +190,11 @@ public abstract class AntSensorManager extends SensorManager { * to initiate any ANT commands. */ private synchronized void serviceConnected() { - Log.d(MyTracksConstants.TAG, "ANT service connected"); + Log.d(TAG, "ANT service connected"); try { if (!antReceiver.claimInterface()) { - Log.e(MyTracksConstants.TAG, "failed to claim ANT interface"); + Log.e(TAG, "failed to claim ANT interface"); return; } @@ -202,14 +202,14 @@ public abstract class AntSensorManager extends SensorManager { // Make sure not to call AntInterface.enable() again, if it has been // already called before if (antInterrupted == false) { - Log.i(MyTracksConstants.TAG, "Powering on Radio"); + Log.i(TAG, "Powering on Radio"); antReceiver.enable(); } } else { - Log.i(MyTracksConstants.TAG, "Radio already enabled"); + Log.i(TAG, "Radio already enabled"); } } catch (AntInterfaceException e) { - Log.e(MyTracksConstants.TAG, "failed to enable ANT", e); + Log.e(TAG, "failed to enable ANT", e); } try { @@ -220,7 +220,7 @@ public abstract class AntSensorManager extends SensorManager { // that message. antReceiver.ANTResetSystem(); } catch (AntInterfaceException e) { - Log.e(MyTracksConstants.TAG, "failed to reset ANT (expected exception)", e); + Log.e(TAG, "failed to reset ANT (expected exception)", e); } } @@ -232,7 +232,7 @@ public abstract class AntSensorManager extends SensorManager { protected void handleMessage(byte[] antMessage) { int len = antMessage[0]; if (len != antMessage.length - 2 || antMessage.length <= 2) { - Log.e(MyTracksConstants.TAG, "Invalid message: " + messageToString(antMessage)); + Log.e(TAG, "Invalid message: " + messageToString(antMessage)); return; } @@ -253,7 +253,7 @@ public abstract class AntSensorManager extends SensorManager { */ protected boolean handleMessage(byte messageId, byte[] messageData) { if (messageId == AntMesg.MESG_STARTUP_MESG_ID) { - Log.d(MyTracksConstants.TAG, String.format( + Log.d(TAG, String.format( "Received startup message (reason %02x); initializing channel", new AntStartupMessage(messageData).getMessage())); setupAntSensorChannels(); @@ -305,13 +305,13 @@ public abstract class AntSensorManager extends SensorManager { return true; } catch (AntInterfaceException e) { - Log.e(MyTracksConstants.TAG, "failed to setup ANT channel", e); + Log.e(TAG, "failed to setup ANT channel", e); return false; } } private void registerForAntIntents() { - Log.i(MyTracksConstants.TAG, "Registering for ant intents."); + Log.i(TAG, "Registering for ant intents."); // Register for ANT intent broadcasts. IntentFilter statusIntentFilter = new IntentFilter(); statusIntentFilter.addAction(AntInterfaceIntent.ANT_ENABLED_ACTION); diff --git a/MyTracks/src/com/google/android/apps/mytracks/services/sensors/ant/AntUtils.java b/MyTracks/src/com/google/android/apps/mytracks/services/sensors/ant/AntUtils.java new file mode 100644 index 000000000..a4e41d42c --- /dev/null +++ b/MyTracks/src/com/google/android/apps/mytracks/services/sensors/ant/AntUtils.java @@ -0,0 +1,35 @@ +/* + * 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.services.sensors.ant; + +import com.dsi.ant.AntInterface; + +import android.content.Context; + +/** + * Utility methods for ANT functionality. + * + * Prefer use of this class to importing DSI ANT classes into code outside of + * the sensors package. + */ +public class AntUtils { + private AntUtils() {} + + /** Returns true if this device supports ANT sensors. */ + public static boolean hasAntSupport(Context context) { + return AntInterface.hasAntSupport(context); + } +} diff --git a/MyTracks/src/com/google/android/apps/mytracks/services/tasks/FroyoStatusAnnouncerTask.java b/MyTracks/src/com/google/android/apps/mytracks/services/tasks/FroyoStatusAnnouncerTask.java new file mode 100644 index 000000000..5bdec230a --- /dev/null +++ b/MyTracks/src/com/google/android/apps/mytracks/services/tasks/FroyoStatusAnnouncerTask.java @@ -0,0 +1,79 @@ +/* + * 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.services.tasks; + +import static com.google.android.apps.mytracks.Constants.TAG; + +import android.content.Context; +import android.media.AudioManager; +import android.speech.tts.TextToSpeech; +import android.speech.tts.TextToSpeech.OnUtteranceCompletedListener; +import android.util.Log; + +import java.util.HashMap; + + +/** + * This class will periodically announce the user's trip statistics for Froyo and future handsets. + * This class will request and release audio focus. + * + * @author Sandor Dornbush + */ +public class FroyoStatusAnnouncerTask extends StatusAnnouncerTask { + private final static HashMap SPEECH_PARAMS = new HashMap(); + static { + SPEECH_PARAMS.put(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID, "not_used"); + } + + private final AudioManager audioManager; + private final OnUtteranceCompletedListener utteranceListener = + new OnUtteranceCompletedListener() { + @Override + public void onUtteranceCompleted(String utteranceId) { + if (audioManager != null) { + Log.d(TAG, "FroyoStatusAnnouncerTask: Abandoning audio focus."); + audioManager.abandonAudioFocus(null); + } + } + }; + + public FroyoStatusAnnouncerTask(Context context) { + super(context); + audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); + } + + @Override + protected void onTtsInit(int status) { + super.onTtsInit(status); + if (status == TextToSpeech.SUCCESS) { + tts.setOnUtteranceCompletedListener(utteranceListener); + } + } + + @Override + protected void speakAnnouncment(String announcement) { + int result = audioManager.requestAudioFocus(null, + TextToSpeech.Engine.DEFAULT_STREAM, + AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK); + if (result == AudioManager.AUDIOFOCUS_REQUEST_FAILED) { + Log.w(TAG, "FroyoStatusAnnouncerTask: Request audio focus failed."); + } + // We don't care about the utterance id. + // It is supplied here to force onUtteranceCompleted to be called. + tts.speak(announcement, TextToSpeech.QUEUE_FLUSH, SPEECH_PARAMS); + } +} diff --git a/MyTracks/src/com/google/android/apps/mytracks/services/PeriodicTask.java b/MyTracks/src/com/google/android/apps/mytracks/services/tasks/PeriodicTask.java similarity index 88% rename from MyTracks/src/com/google/android/apps/mytracks/services/PeriodicTask.java rename to MyTracks/src/com/google/android/apps/mytracks/services/tasks/PeriodicTask.java index 354b4bccd..1d351bec4 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/services/PeriodicTask.java +++ b/MyTracks/src/com/google/android/apps/mytracks/services/tasks/PeriodicTask.java @@ -14,7 +14,9 @@ * the License. */ -package com.google.android.apps.mytracks.services; +package com.google.android.apps.mytracks.services.tasks; + +import com.google.android.apps.mytracks.services.TrackRecordingService; /** * This is interface for a task that will be executed on some schedule. diff --git a/MyTracks/src/com/google/android/apps/mytracks/services/tasks/PeriodicTaskExecutor.java b/MyTracks/src/com/google/android/apps/mytracks/services/tasks/PeriodicTaskExecutor.java new file mode 100644 index 000000000..f307e4741 --- /dev/null +++ b/MyTracks/src/com/google/android/apps/mytracks/services/tasks/PeriodicTaskExecutor.java @@ -0,0 +1,189 @@ +/* + * Copyright 2010 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.google.android.apps.mytracks.services.tasks; + +import static com.google.android.apps.mytracks.Constants.TAG; + +import com.google.android.apps.mytracks.services.TrackRecordingService; +import com.google.android.apps.mytracks.util.UnitConversions; + +import android.util.Log; + +/** + * Execute a task on a time or distance schedule. + * + * @author Sandor Dornbush + */ +public class PeriodicTaskExecutor { + + /** + * The frequency of the task. + * A value greater than zero is a frequency in time. + * A value less than zero is considered a frequency in distance. + */ + private int taskFrequency = 0; + + /** + * The next distance when the task should execute. + */ + private double nextTaskDistance = 0; + + /** + * Time based executor. + */ + private TimerTaskExecutor timerExecutor = null; + + private boolean metricUnits; + + private final TrackRecordingService service; + + private final PeriodicTaskFactory factory; + + private PeriodicTask task; + + public PeriodicTaskExecutor(TrackRecordingService service, PeriodicTaskFactory factory) { + this.service = service; + this.factory = factory; + } + + /** + * Restores the manager. + */ + public void restore() { + // TODO: Decouple service from this class once and forever. + if (!service.isRecording()) { + return; + } + + if (!isTimeFrequency()) { + if (timerExecutor != null) { + timerExecutor.shutdown(); + timerExecutor = null; + } + } + if (taskFrequency == 0) { + return; + } + + // Try to make the task. + task = factory.create(service); + // Returning null is ok. + if (task == null) { + return; + } + task.start(); + + if (isTimeFrequency()) { + if (timerExecutor == null) { + timerExecutor = new TimerTaskExecutor(task, service); + } + timerExecutor.scheduleTask(taskFrequency * 60000); + } else { + // For distance based splits. + calculateNextTaskDistance(); + } + } + + /** + * Shuts down the manager. + */ + public void shutdown() { + if (task != null) { + task.shutdown(); + task = null; + } + if (timerExecutor != null) { + timerExecutor.shutdown(); + timerExecutor = null; + } + } + + /** + * Calculates the next distance when the task should execute. + */ + void calculateNextTaskDistance() { + // TODO: Decouple service from this class once and forever. + if (!service.isRecording() || task == null) { + return; + } + + if (!isDistanceFrequency()) { + nextTaskDistance = Double.MAX_VALUE; + Log.d(TAG, "SplitManager: Distance splits disabled."); + return; + } + + double distance = service.getTripStatistics().getTotalDistance() / 1000; + if (!metricUnits) { + distance *= UnitConversions.KM_TO_MI; + } + // The index will be negative since the frequency is negative. + int index = (int) (distance / taskFrequency); + index -= 1; + nextTaskDistance = taskFrequency * index; + Log.d(TAG, "SplitManager: Next split distance: " + nextTaskDistance); + } + + /** + * Updates executer with new trip statistics. + */ + public void update() { + if (!isDistanceFrequency() || task == null) { + return; + } + // Convert the distance in meters to km or mi. + double distance = service.getTripStatistics().getTotalDistance() / 1000.0; + if (!metricUnits) { + distance *= UnitConversions.KM_TO_MI; + } + + if (distance > nextTaskDistance) { + task.run(service); + calculateNextTaskDistance(); + } + } + + private boolean isTimeFrequency() { + return taskFrequency > 0; + } + + private boolean isDistanceFrequency() { + return taskFrequency < 0; + } + + /** + * Sets the task frequency. + * < 0 Use the absolute value as a distance in the current measurement km + * or mi + * 0 Turn off the task + * > 0 Use the value as a time in minutes + * @param taskFrequency The frequency in time or distance + */ + public void setTaskFrequency(int taskFrequency) { + Log.d(TAG, "setTaskFrequency: taskFrequency = " + taskFrequency); + this.taskFrequency = taskFrequency; + restore(); + } + + public void setMetricUnits(boolean metricUnits) { + this.metricUnits = metricUnits; + calculateNextTaskDistance(); + } + + double getNextTaskDistance() { + return nextTaskDistance; + } +} diff --git a/MyTracks/src/com/google/android/apps/mytracks/services/tasks/PeriodicTaskFactory.java b/MyTracks/src/com/google/android/apps/mytracks/services/tasks/PeriodicTaskFactory.java new file mode 100644 index 000000000..f3f8be445 --- /dev/null +++ b/MyTracks/src/com/google/android/apps/mytracks/services/tasks/PeriodicTaskFactory.java @@ -0,0 +1,33 @@ +/* + * Copyright 2010 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.google.android.apps.mytracks.services.tasks; + +import android.content.Context; + +/** + * An interface for classes that can create periodic tasks. + * + * @author Sandor Dornbush + */ +public interface PeriodicTaskFactory { + + /** + * Creates a periodic task which does voice announcements. + * + * @return the task, or null if task is not supported + */ + PeriodicTask create(Context context); +} \ No newline at end of file diff --git a/MyTracks/src/com/google/android/apps/mytracks/services/TimeSplitTask.java b/MyTracks/src/com/google/android/apps/mytracks/services/tasks/SplitTask.java similarity index 64% rename from MyTracks/src/com/google/android/apps/mytracks/services/TimeSplitTask.java rename to MyTracks/src/com/google/android/apps/mytracks/services/tasks/SplitTask.java index 333ce6ae4..763a922be 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/services/TimeSplitTask.java +++ b/MyTracks/src/com/google/android/apps/mytracks/services/tasks/SplitTask.java @@ -14,16 +14,22 @@ * the License. */ -package com.google.android.apps.mytracks.services; +package com.google.android.apps.mytracks.services.tasks; import com.google.android.apps.mytracks.content.WaypointCreationRequest; +import com.google.android.apps.mytracks.services.TrackRecordingService; + +import android.content.Context; /** - * A simple task to insert statistics markers every n minutes. + * A simple task to insert statistics markers periodically. * @author Sandor Dornbush */ -public class TimeSplitTask implements PeriodicTask { +public class SplitTask implements PeriodicTask { + + private SplitTask() { + } @Override public void run(TrackRecordingService service) { @@ -37,4 +43,15 @@ public class TimeSplitTask implements PeriodicTask { @Override public void start() { } + + /** + * Create new SplitTasks. + */ + public static class Factory implements PeriodicTaskFactory { + + @Override + public PeriodicTask create(Context context) { + return new SplitTask(); + } + } } diff --git a/MyTracks/src/com/google/android/apps/mytracks/services/StatusAnnouncerFactory.java b/MyTracks/src/com/google/android/apps/mytracks/services/tasks/StatusAnnouncerFactory.java similarity index 83% rename from MyTracks/src/com/google/android/apps/mytracks/services/StatusAnnouncerFactory.java rename to MyTracks/src/com/google/android/apps/mytracks/services/tasks/StatusAnnouncerFactory.java index 7927d636b..30c87d781 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/services/StatusAnnouncerFactory.java +++ b/MyTracks/src/com/google/android/apps/mytracks/services/tasks/StatusAnnouncerFactory.java @@ -13,7 +13,7 @@ * License for the specific language governing permissions and limitations under * the License. */ -package com.google.android.apps.mytracks.services; +package com.google.android.apps.mytracks.services.tasks; import com.google.android.apps.mytracks.util.ApiFeatures; @@ -26,7 +26,7 @@ import android.media.AudioManager; * * @author Rodrigo Damazio */ -public class StatusAnnouncerFactory { +public class StatusAnnouncerFactory implements PeriodicTaskFactory { private final boolean hasTts; @@ -34,14 +34,10 @@ public class StatusAnnouncerFactory { this.hasTts = apiFeatures.hasTextToSpeech(); } - /** - * Creates a periodic task which does voice announcements. - * - * @return the task, or null if announcements are not supported - */ + @Override public PeriodicTask create(Context context) { if (hasTts) { - if (ApiFeatures.getInstance().supportsAudioFocus()) { + if (ApiFeatures.getInstance().isAudioFocusSupported()) { return new FroyoStatusAnnouncerTask(context); } else { return new StatusAnnouncerTask(context); diff --git a/MyTracks/src/com/google/android/apps/mytracks/services/StatusAnnouncerTask.java b/MyTracks/src/com/google/android/apps/mytracks/services/tasks/StatusAnnouncerTask.java similarity index 84% rename from MyTracks/src/com/google/android/apps/mytracks/services/StatusAnnouncerTask.java rename to MyTracks/src/com/google/android/apps/mytracks/services/tasks/StatusAnnouncerTask.java index 756f1393e..bf0645b3e 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/services/StatusAnnouncerTask.java +++ b/MyTracks/src/com/google/android/apps/mytracks/services/tasks/StatusAnnouncerTask.java @@ -14,10 +14,12 @@ * the License. */ -package com.google.android.apps.mytracks.services; +package com.google.android.apps.mytracks.services.tasks; -import com.google.android.apps.mytracks.MyTracksConstants; -import com.google.android.apps.mytracks.MyTracksSettings; +import static com.google.android.apps.mytracks.Constants.TAG; + +import com.google.android.apps.mytracks.Constants; +import com.google.android.apps.mytracks.services.TrackRecordingService; import com.google.android.apps.mytracks.stats.TripStatistics; import com.google.android.apps.mytracks.util.StringUtils; import com.google.android.apps.mytracks.util.UnitConversions; @@ -99,8 +101,9 @@ public class StatusAnnouncerTask implements PeriodicTask { * Called when the TTS engine is initialized. */ protected void onTtsInit(int status) { - Log.i(MyTracksConstants.TAG, "TrackRecordingService.TTS init: " + status); - this.ready = status == TextToSpeech.SUCCESS; + Log.i(TAG, "TrackRecordingService.TTS init: " + status); + // TTS should be valid here but NPE exceptions were reported to the market. + this.ready = status == TextToSpeech.SUCCESS && tts != null; if (ready) { // Force the language to be the same as the string we will be speaking, @@ -111,7 +114,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(MyTracksConstants.TAG, "Default language not available, using English."); + Log.w(TAG, "Default language not available, using English."); speechLanguage = Locale.ENGLISH; } tts.setLanguage(speechLanguage); @@ -128,24 +131,37 @@ 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(MyTracksConstants.TAG, "StatusAnnouncer Tts not ready."); + Log.e(TAG, "StatusAnnouncer Tts not ready."); return; } if (!speechAllowed) { - Log.i(MyTracksConstants.TAG, + Log.i(Constants.TAG, "Not making announcement - not allowed at this time"); return; } - if (service == null || service.getTripStatistics() == null) { - Log.e(MyTracksConstants.TAG, "StatusAnnouncer stats not initialized."); - return; - } - - String announcement = getAnnouncement(service.getTripStatistics()); - Log.d(MyTracksConstants.TAG, "Announcement: " + announcement); + String announcement = getAnnouncement(statistics); + Log.d(Constants.TAG, "Announcement: " + announcement); speakAnnouncment(announcement); } @@ -161,7 +177,7 @@ public class StatusAnnouncerTask implements PeriodicTask { // @VisibleForTesting protected String getAnnouncement(TripStatistics stats) { SharedPreferences preferences = - context.getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0); + context.getSharedPreferences(Constants.SETTINGS_NAME, 0); boolean metricUnits = true; boolean reportSpeed = true; if (preferences != null) { @@ -204,7 +220,7 @@ public class StatusAnnouncerTask implements PeriodicTask { speed = String.format("%.1f", s); } else { double pace = 3600000.0 / s; - Log.w(MyTracksConstants.TAG, + Log.w(Constants.TAG, "Converted speed: " + s + " to pace: " + pace); speed = stringUtils.formatTimeLong((long) pace); } @@ -223,7 +239,7 @@ public class StatusAnnouncerTask implements PeriodicTask { @Override public void start() { - Log.i(MyTracksConstants.TAG, "Starting TTS"); + Log.i(Constants.TAG, "Starting TTS"); if (tts == null) { // We can't have this class also be the listener, otherwise it's unsafe to // reference it in Cupcake (even if we don't instantiate it). @@ -250,7 +266,7 @@ public class StatusAnnouncerTask implements PeriodicTask { tts = null; } - Log.i(MyTracksConstants.TAG, "TTS shut down"); + Log.i(Constants.TAG, "TTS shut down"); } /** diff --git a/MyTracks/src/com/google/android/apps/mytracks/services/PeriodicTaskExecuter.java b/MyTracks/src/com/google/android/apps/mytracks/services/tasks/TimerTaskExecutor.java similarity index 81% rename from MyTracks/src/com/google/android/apps/mytracks/services/PeriodicTaskExecuter.java rename to MyTracks/src/com/google/android/apps/mytracks/services/tasks/TimerTaskExecutor.java index e7af61c2d..ec7d66c05 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/services/PeriodicTaskExecuter.java +++ b/MyTracks/src/com/google/android/apps/mytracks/services/tasks/TimerTaskExecutor.java @@ -14,9 +14,9 @@ * the License. */ -package com.google.android.apps.mytracks.services; +package com.google.android.apps.mytracks.services.tasks; -import com.google.android.apps.mytracks.MyTracksConstants; +import static com.google.android.apps.mytracks.Constants.TAG; import android.util.Log; @@ -24,12 +24,14 @@ import java.util.Date; import java.util.Timer; import java.util.TimerTask; +import com.google.android.apps.mytracks.services.TrackRecordingService; + /** - * This class will periodically announce the user's trip statistics. + * This class will periodically perform a task. * * @author Sandor Dornbush */ -public class PeriodicTaskExecuter { +public class TimerTaskExecutor { private final PeriodicTask task; private final TrackRecordingService service; @@ -40,8 +42,8 @@ public class PeriodicTaskExecuter { */ private Timer timer; - public PeriodicTaskExecuter(PeriodicTask task, - TrackRecordingService service) { + public TimerTaskExecutor(PeriodicTask task, + TrackRecordingService service) { this.task = task; this.service = service; } @@ -77,8 +79,7 @@ public class PeriodicTaskExecuter { } Date start = new Date(next); - Log.i(MyTracksConstants.TAG, - task.getClass().getSimpleName() + " scheduled to start at " + start + Log.i(TAG, task.getClass().getSimpleName() + " scheduled to start at " + start + " every " + interval + " milliseconds."); timer.scheduleAtFixedRate(new PeriodicTimerTask(), start, interval); } @@ -87,8 +88,7 @@ public class PeriodicTaskExecuter { * Cleans up this object. */ public void shutdown() { - Log.i(MyTracksConstants.TAG, - task.getClass().getSimpleName() + " shutting down."); + Log.i(TAG, task.getClass().getSimpleName() + " shutting down."); if (timer != null) { timer.cancel(); timer.purge(); diff --git a/MyTracks/src/com/google/android/apps/mytracks/stats/TripStatisticsBuilder.java b/MyTracks/src/com/google/android/apps/mytracks/stats/TripStatisticsBuilder.java index 0fdbd0ff8..ddad2394a 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/stats/TripStatisticsBuilder.java +++ b/MyTracks/src/com/google/android/apps/mytracks/stats/TripStatisticsBuilder.java @@ -16,8 +16,10 @@ package com.google.android.apps.mytracks.stats; -import com.google.android.apps.mytracks.MyTracksConstants; -import com.google.android.apps.mytracks.MyTracksSettings; +import static com.google.android.apps.mytracks.Constants.TAG; + +import com.google.android.apps.mytracks.Constants; +import com.google.android.apps.mytracks.SettingsActivity; import android.location.Location; import android.util.Log; @@ -65,25 +67,25 @@ public class TripStatisticsBuilder { * A buffer of the last speed readings in meters/second. */ private final DoubleBuffer speedBuffer = - new DoubleBuffer(MyTracksConstants.SPEED_SMOOTHING_FACTOR); + new DoubleBuffer(Constants.SPEED_SMOOTHING_FACTOR); /** * A buffer of the recent elevation readings in meters. */ private final DoubleBuffer elevationBuffer = - new DoubleBuffer(MyTracksConstants.ELEVATION_SMOOTHING_FACTOR); + new DoubleBuffer(Constants.ELEVATION_SMOOTHING_FACTOR); /** * A buffer of the distance between recent gps readings in meters. */ private final DoubleBuffer distanceBuffer = - new DoubleBuffer(MyTracksConstants.DISTANCE_SMOOTHING_FACTOR); + new DoubleBuffer(Constants.DISTANCE_SMOOTHING_FACTOR); /** * A buffer of the recent grade calculations. */ private final DoubleBuffer gradeBuffer = - new DoubleBuffer(MyTracksConstants.GRADE_SMOOTHING_FACTOR); + new DoubleBuffer(Constants.GRADE_SMOOTHING_FACTOR); /** * The total number of locations in this trip. @@ -91,7 +93,7 @@ public class TripStatisticsBuilder { private long totalLocations = 0; private int minRecordingDistance = - MyTracksSettings.DEFAULT_MIN_RECORDING_DISTANCE; + Constants.DEFAULT_MIN_RECORDING_DISTANCE; /** * Creates a new trip starting at the given time. @@ -126,7 +128,7 @@ public class TripStatisticsBuilder { */ public boolean addLocation(Location currentLocation, long systemTime) { if (paused) { - Log.w(MyTracksConstants.TAG, + Log.w(TAG, "Tried to account for location while track is paused"); return false; } @@ -151,7 +153,7 @@ public class TripStatisticsBuilder { // Don't do anything if we didn't move since last fix: double distance = lastLocation.distanceTo(currentLocation); if (distance < minRecordingDistance && - currentSpeed < MyTracksConstants.MAX_NO_MOVEMENT_SPEED) { + currentSpeed < Constants.MAX_NO_MOVEMENT_SPEED) { lastLocation = currentLocation; return false; } @@ -208,7 +210,7 @@ public class TripStatisticsBuilder { // We are now sure the user is moving. long timeDifference = updateTime - lastLocationTime; if (timeDifference < 0) { - Log.e(MyTracksConstants.TAG, + Log.e(TAG, "Found negative time change: " + timeDifference); } data.addMovingTime(timeDifference); @@ -224,7 +226,7 @@ public class TripStatisticsBuilder { data.setMaxSpeed(movingSpeed); } } else { - Log.d(MyTracksConstants.TAG, + Log.d(TAG, "TripStatistics ignoring big change: Raw Speed: " + speed + " old: " + lastLocationSpeed + " [" + toString() + "]"); } @@ -263,7 +265,7 @@ public class TripStatisticsBuilder { // likely. Ignore any speeds that imply accelaration greater than 2g's // Really who can accelerate faster? double speedDifference = Math.abs(lastLocationSpeed - speed); - if (speedDifference > MyTracksConstants.MAX_ACCELERATION * timeDifference) { + if (speedDifference > Constants.MAX_ACCELERATION * timeDifference) { return false; } @@ -275,7 +277,7 @@ public class TripStatisticsBuilder { double smoothedDiff = Math.abs(smoothedSpeed - speed); return !speedBuffer.isFull() || (speed < smoothedSpeed * 10 - && smoothedDiff < MyTracksConstants.MAX_ACCELERATION * timeDifference); + && smoothedDiff < Constants.MAX_ACCELERATION * timeDifference); } /** diff --git a/MyTracks/src/com/google/android/apps/mytracks/util/ApiFeatures.java b/MyTracks/src/com/google/android/apps/mytracks/util/ApiFeatures.java index 55d516566..26fb3a31c 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/util/ApiFeatures.java +++ b/MyTracks/src/com/google/android/apps/mytracks/util/ApiFeatures.java @@ -15,7 +15,7 @@ */ package com.google.android.apps.mytracks.util; -import com.google.android.apps.mytracks.MyTracksConstants; +import com.google.android.apps.mytracks.Constants; import android.os.Build; import android.util.Log; @@ -72,7 +72,7 @@ public class ApiFeatures { apiPlatformAdapter = new CupcakePlatformAdapter(); } - Log.i(MyTracksConstants.TAG, "Using platform adapter " + apiPlatformAdapter.getClass()); + Log.i(Constants.TAG, "Using platform adapter " + apiPlatformAdapter.getClass()); } public ApiPlatformAdapter getApiPlatformAdapter() { @@ -107,7 +107,7 @@ public class ApiFeatures { return getApiLevel() >= 9; } - public boolean supportsAudioFocus() { + public boolean isAudioFocusSupported() { return getApiLevel() >= 8; } diff --git a/MyTracks/src/com/google/android/apps/mytracks/util/BluetoothDeviceUtils.java b/MyTracks/src/com/google/android/apps/mytracks/util/BluetoothDeviceUtils.java index a819a8473..96e8bd878 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/util/BluetoothDeviceUtils.java +++ b/MyTracks/src/com/google/android/apps/mytracks/util/BluetoothDeviceUtils.java @@ -15,7 +15,7 @@ */ package com.google.android.apps.mytracks.util; -import com.google.android.apps.mytracks.MyTracksConstants; +import com.google.android.apps.mytracks.Constants; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothClass; @@ -158,14 +158,14 @@ public abstract class BluetoothDeviceUtils { public static BluetoothDeviceUtils getInstance() { if (instance == null) { if (!isBluetoothMethodSupported()) { - Log.d(MyTracksConstants.TAG, "Using dummy bluetooth utils"); + Log.d(Constants.TAG, "Using dummy bluetooth utils"); instance = new DummyImpl(); } else { - Log.d(MyTracksConstants.TAG, "Using real bluetooth utils"); + Log.d(Constants.TAG, "Using real bluetooth utils"); try { instance = new RealImpl(); } catch (IllegalStateException ise) { - Log.w(MyTracksConstants.TAG, "Oops, I mean, using dummy bluetooth utils", ise); + Log.w(Constants.TAG, "Oops, I mean, using dummy bluetooth utils", ise); instance = new DummyImpl(); } } diff --git a/MyTracks/src/com/google/android/apps/mytracks/util/ChartURLGenerator.java b/MyTracks/src/com/google/android/apps/mytracks/util/ChartURLGenerator.java index bd721512a..5c0336192 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/util/ChartURLGenerator.java +++ b/MyTracks/src/com/google/android/apps/mytracks/util/ChartURLGenerator.java @@ -16,7 +16,7 @@ package com.google.android.apps.mytracks.util; -import com.google.android.apps.mytracks.MyTracksSettings; +import com.google.android.apps.mytracks.Constants; import com.google.android.apps.mytracks.content.Track; import com.google.android.apps.mytracks.stats.TripStatistics; import com.google.android.maps.mytracks.R; @@ -50,7 +50,7 @@ public class ChartURLGenerator { public static String getChartUrl(Vector distances, Vector elevations, Track track, Context context) { SharedPreferences preferences = - context.getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0); + context.getSharedPreferences(Constants.SETTINGS_NAME, 0); boolean metricUnits = true; if (preferences != null) { metricUnits = preferences.getBoolean( @@ -58,7 +58,7 @@ public class ChartURLGenerator { } return getChartUrl(distances, elevations, track, - context.getString(R.string.elevation_label), metricUnits, true); + context.getString(R.string.elevation_label), metricUnits); } /** @@ -70,11 +70,10 @@ public class ChartURLGenerator { * @param track The track for this chart * @param title The title for the chart * @param metricUnits Should the data be displayed in metric units - * @param escapePipe Should the pipe character be escaped */ public static String getChartUrl( Vector distances, Vector elevations, - Track track, String title, boolean metricUnits, boolean escapePipe) { + Track track, String title, boolean metricUnits) { if (distances == null || elevations == null || track == null) { return null; } @@ -125,9 +124,7 @@ public class ChartURLGenerator { sb.append(','); sb.append(xInterval); - // This is an ugly hack. We really want a pipe but we must double escape it. - sb.append(escapePipe ? "%257C" : "|"); - sb.append("1,"); + sb.append("|1,"); sb.append(effectiveMinY); sb.append(','); sb.append(effectiveMaxY); diff --git a/MyTracks/src/com/google/android/apps/mytracks/util/CupcakePlatformAdapter.java b/MyTracks/src/com/google/android/apps/mytracks/util/CupcakePlatformAdapter.java index 354f0a307..abdd9dfb7 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/util/CupcakePlatformAdapter.java +++ b/MyTracks/src/com/google/android/apps/mytracks/util/CupcakePlatformAdapter.java @@ -15,11 +15,16 @@ */ package com.google.android.apps.mytracks.util; +import static com.google.android.apps.mytracks.Constants.TAG; import android.app.Notification; import android.app.NotificationManager; import android.app.Service; import android.content.SharedPreferences.Editor; import android.os.HandlerThread; +import android.util.Log; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; /** * The Cupcake (API level 3) specific implementation of the @@ -33,19 +38,38 @@ public class CupcakePlatformAdapter implements ApiPlatformAdapter { public void startForeground(Service service, NotificationManager notificationManager, int id, Notification notification) { - service.setForeground(true); + setServiceForeground(service, true); + notificationManager.notify(id, notification); } @Override public void stopForeground(Service service, NotificationManager notificationManager, int id) { - service.setForeground(false); + setServiceForeground(service, false); if (id != -1) { notificationManager.cancel(id); } } + private void setServiceForeground(Service service, boolean foreground) { + // setForeground has been completely removed in API level 11, so we use reflection. + try { + Method setForegroundMethod = Service.class.getMethod("setForeground", boolean.class); + setForegroundMethod.invoke(service, foreground); + } catch (SecurityException e) { + Log.e(TAG, "Unable to set service foreground state", e); + } catch (NoSuchMethodException e) { + Log.e(TAG, "Unable to set service foreground state", e); + } catch (IllegalArgumentException e) { + Log.e(TAG, "Unable to set service foreground state", e); + } catch (IllegalAccessException e) { + Log.e(TAG, "Unable to set service foreground state", e); + } catch (InvocationTargetException e) { + Log.e(TAG, "Unable to set service foreground state", e); + } + } + @Override public boolean stopHandlerThread(HandlerThread handlerThread) { // Do nothing, as Cupcake doesn't provide quit(). diff --git a/MyTracks/src/com/google/android/apps/mytracks/util/FileUtils.java b/MyTracks/src/com/google/android/apps/mytracks/util/FileUtils.java index 6c6ca7af0..ac63cbba5 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/util/FileUtils.java +++ b/MyTracks/src/com/google/android/apps/mytracks/util/FileUtils.java @@ -15,7 +15,7 @@ */ package com.google.android.apps.mytracks.util; -import com.google.android.apps.mytracks.MyTracksConstants; +import com.google.android.apps.mytracks.Constants; import android.os.Environment; @@ -49,7 +49,7 @@ public class FileUtils { StringBuilder dirNameBuilder = new StringBuilder(); dirNameBuilder.append(Environment.getExternalStorageDirectory()); dirNameBuilder.append(File.separatorChar); - dirNameBuilder.append(MyTracksConstants.SDCARD_TOP_DIR); + dirNameBuilder.append(Constants.SDCARD_TOP_DIR); for (String component : components) { dirNameBuilder.append(File.separatorChar); dirNameBuilder.append(component); diff --git a/MyTracks/src/com/google/android/apps/mytracks/util/GingerbreadPlatformAdapter.java b/MyTracks/src/com/google/android/apps/mytracks/util/GingerbreadPlatformAdapter.java index caa73a170..35d7eb3d6 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/util/GingerbreadPlatformAdapter.java +++ b/MyTracks/src/com/google/android/apps/mytracks/util/GingerbreadPlatformAdapter.java @@ -1,6 +1,6 @@ package com.google.android.apps.mytracks.util; -import com.google.android.apps.mytracks.MyTracksConstants; +import com.google.android.apps.mytracks.Constants; import android.content.SharedPreferences.Editor; import android.os.StrictMode; @@ -21,7 +21,7 @@ public class GingerbreadPlatformAdapter extends EclairPlatformAdapter { @Override public void enableStrictMode() { - Log.d(MyTracksConstants.TAG, "Enabling strict mode"); + Log.d(Constants.TAG, "Enabling strict mode"); StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder() .detectDiskWrites() .detectNetwork() @@ -30,7 +30,6 @@ public class GingerbreadPlatformAdapter extends EclairPlatformAdapter { StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder() .detectAll() .penaltyLog() - .penaltyDeath() .build()); } } diff --git a/MyTracks/src/com/google/android/apps/mytracks/util/MyTracksUtils.java b/MyTracks/src/com/google/android/apps/mytracks/util/LocationUtils.java similarity index 72% rename from MyTracks/src/com/google/android/apps/mytracks/util/MyTracksUtils.java rename to MyTracks/src/com/google/android/apps/mytracks/util/LocationUtils.java index 59ba09b3d..1786cf3a6 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/util/MyTracksUtils.java +++ b/MyTracks/src/com/google/android/apps/mytracks/util/LocationUtils.java @@ -15,21 +15,13 @@ */ package com.google.android.apps.mytracks.util; -import com.google.android.apps.mytracks.MyTracksConstants; +import com.google.android.apps.mytracks.Constants; import com.google.android.apps.mytracks.content.Track; import com.google.android.apps.mytracks.stats.TripStatistics; import com.google.android.maps.GeoPoint; -import android.app.Activity; -import android.content.Context; -import android.content.pm.PackageInfo; -import android.content.pm.PackageManager; -import android.content.pm.PackageManager.NameNotFoundException; -import android.content.pm.Signature; import android.location.Location; -import android.os.PowerManager; -import android.os.PowerManager.WakeLock; import android.util.Log; import java.util.ArrayList; @@ -41,9 +33,7 @@ import java.util.Stack; * * @author Leif Hendrik Wilden */ -public class MyTracksUtils { - private static final int RELEASE_SIGNATURE_HASHCODE = -1855564782; - +public class LocationUtils { /** * Computes the distance on the two sphere between the point c0 and the line * segment c1 to c2. @@ -117,7 +107,7 @@ public class MyTracksUtils { current = stack.pop(); maxDist = 0; for (idx = current[0] + 1; idx < current[1]; ++idx) { - dist = MyTracksUtils.distance( + dist = LocationUtils.distance( locations.get(idx), locations.get(current[0]), locations.get(current[1])); @@ -146,7 +136,7 @@ public class MyTracksUtils { } idx++; } - Log.d(MyTracksConstants.TAG, "Decimating " + n + " points to " + i + Log.d(Constants.TAG, "Decimating " + n + " points to " + i + " w/ tolerance = " + tolerance); } @@ -255,80 +245,9 @@ public class MyTracksUtils { (int) (location.getLongitude() * 1E6)); } - /** - * Returns whether or not this is a release build. - */ - public static boolean isRelease(Context context) { - try { - Signature [] sigs = context.getPackageManager().getPackageInfo( - context.getPackageName(), PackageManager.GET_SIGNATURES).signatures; - for (Signature sig : sigs) { - if (sig.hashCode() == RELEASE_SIGNATURE_HASHCODE) { - return true; - } - } - } catch (NameNotFoundException e) { - Log.e(MyTracksConstants.TAG, "Unable to get signatures", e); - } - return false; - } - - /** - * Get the My Tracks version from the manifest. - * - * @return the version, or an empty string in case of failure. - */ - public static String getMyTracksVersion(Context context) { - try { - PackageInfo pi = context.getPackageManager().getPackageInfo( - "com.google.android.maps.mytracks", - PackageManager.GET_META_DATA); - return pi.versionName; - } catch (NameNotFoundException e) { - Log.w(MyTracksConstants.TAG, "Failed to get version info.", e); - return ""; - } - } - - /** - * Tries to acquire a partial wake lock if not already acquired. Logs errors - * and gives up trying in case the wake lock cannot be acquired. - */ - public static WakeLock acquireWakeLock(Activity activity, WakeLock wakeLock) { - Log.i(MyTracksConstants.TAG, "MyTracksUtils: Acquiring wake lock."); - try { - PowerManager pm = (PowerManager) activity - .getSystemService(Context.POWER_SERVICE); - if (pm == null) { - Log.e(MyTracksConstants.TAG, "MyTracksUtils: Power manager not found!"); - return wakeLock; - } - if (wakeLock == null) { - wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, - MyTracksConstants.TAG); - if (wakeLock == null) { - Log.e(MyTracksConstants.TAG, - "MyTracksUtils: Could not create wake lock (null)."); - } - return wakeLock; - } - if (!wakeLock.isHeld()) { - wakeLock.acquire(); - if (!wakeLock.isHeld()) { - Log.e(MyTracksConstants.TAG, - "MyTracksUtils: Could not acquire wake lock."); - } - } - } catch (RuntimeException e) { - Log.e(MyTracksConstants.TAG, - "MyTracksUtils: Caught unexpected exception: " + e.getMessage(), e); - } - return wakeLock; - } - /** * This is a utility class w/ only static members. */ - protected MyTracksUtils() { + private LocationUtils() { } } diff --git a/MyTracks/src/com/google/android/apps/mytracks/util/StringUtils.java b/MyTracks/src/com/google/android/apps/mytracks/util/StringUtils.java index b6b8baace..05d87c61d 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/util/StringUtils.java +++ b/MyTracks/src/com/google/android/apps/mytracks/util/StringUtils.java @@ -15,7 +15,7 @@ */ package com.google.android.apps.mytracks.util; -import com.google.android.apps.mytracks.MyTracksSettings; +import com.google.android.apps.mytracks.Constants; import com.google.android.apps.mytracks.content.DescriptionGenerator; import com.google.android.apps.mytracks.content.Track; import com.google.android.apps.mytracks.content.Waypoint; @@ -245,7 +245,7 @@ public class StringUtils implements DescriptionGenerator { Vector elevations) { boolean displaySpeed = true; SharedPreferences preferences = - context.getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0); + context.getSharedPreferences(Constants.SETTINGS_NAME, 0); if (preferences != null) { displaySpeed = preferences.getBoolean(context.getString(R.string.report_speed_key), true); diff --git a/MyTracks/src/com/google/android/apps/mytracks/util/SystemUtils.java b/MyTracks/src/com/google/android/apps/mytracks/util/SystemUtils.java new file mode 100644 index 000000000..c45c88308 --- /dev/null +++ b/MyTracks/src/com/google/android/apps/mytracks/util/SystemUtils.java @@ -0,0 +1,111 @@ +/* + * 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.util; + +import com.google.android.apps.mytracks.Constants; + +import android.app.Activity; +import android.content.Context; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.content.pm.Signature; +import android.os.PowerManager; +import android.os.PowerManager.WakeLock; +import android.util.Log; + +/** + * Utility class for acessing basic Android functionality. + * + * @author Rodrigo Damazio + */ +public class SystemUtils { + + private static final int RELEASE_SIGNATURE_HASHCODE = -1855564782; + + /** + * Returns whether or not this is a release build. + */ + public static boolean isRelease(Context context) { + try { + Signature [] sigs = context.getPackageManager().getPackageInfo( + context.getPackageName(), PackageManager.GET_SIGNATURES).signatures; + for (Signature sig : sigs) { + if (sig.hashCode() == RELEASE_SIGNATURE_HASHCODE) { + return true; + } + } + } catch (NameNotFoundException e) { + Log.e(Constants.TAG, "Unable to get signatures", e); + } + return false; + } + + /** + * Get the My Tracks version from the manifest. + * + * @return the version, or an empty string in case of failure. + */ + public static String getMyTracksVersion(Context context) { + try { + PackageInfo pi = context.getPackageManager().getPackageInfo( + "com.google.android.maps.mytracks", + PackageManager.GET_META_DATA); + return pi.versionName; + } catch (NameNotFoundException e) { + Log.w(Constants.TAG, "Failed to get version info.", e); + return ""; + } + } + + /** + * Tries to acquire a partial wake lock if not already acquired. Logs errors + * and gives up trying in case the wake lock cannot be acquired. + */ + public static WakeLock acquireWakeLock(Activity activity, WakeLock wakeLock) { + Log.i(Constants.TAG, "LocationUtils: Acquiring wake lock."); + try { + PowerManager pm = (PowerManager) activity + .getSystemService(Context.POWER_SERVICE); + if (pm == null) { + Log.e(Constants.TAG, "LocationUtils: Power manager not found!"); + return wakeLock; + } + if (wakeLock == null) { + wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, + Constants.TAG); + if (wakeLock == null) { + Log.e(Constants.TAG, + "LocationUtils: Could not create wake lock (null)."); + } + return wakeLock; + } + if (!wakeLock.isHeld()) { + wakeLock.acquire(); + if (!wakeLock.isHeld()) { + Log.e(Constants.TAG, + "LocationUtils: Could not acquire wake lock."); + } + } + } catch (RuntimeException e) { + Log.e(Constants.TAG, + "LocationUtils: Caught unexpected exception: " + e.getMessage(), e); + } + return wakeLock; + } + + private SystemUtils() {} +} \ No newline at end of file diff --git a/MyTracks/src/com/google/wireless/gdata/docs/XmlDocsGDataParserFactory.java b/MyTracks/src/com/google/wireless/gdata/docs/XmlDocsGDataParserFactory.java new file mode 100644 index 000000000..09840754e --- /dev/null +++ b/MyTracks/src/com/google/wireless/gdata/docs/XmlDocsGDataParserFactory.java @@ -0,0 +1,73 @@ +/* + * Copyright 2010 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.google.wireless.gdata.docs; + +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 java.io.InputStream; + +import org.xmlpull.v1.XmlPullParserException; + +/** + * Factory of Xml parsers for gdata maps data. + */ +public class XmlDocsGDataParserFactory implements GDataParserFactory { + private XmlParserFactory xmlFactory; + + public XmlDocsGDataParserFactory(XmlParserFactory xmlFactory) { + this.xmlFactory = xmlFactory; + } + + @Override + public GDataParser createParser(InputStream is) throws ParseException { + try { + return new XmlGDataParser(is, xmlFactory.createParser()); + } catch (XmlPullParserException e) { + e.printStackTrace(); + return null; + } + } + + @SuppressWarnings("unchecked") + @Override + public GDataParser createParser(Class cls, InputStream is) + throws ParseException { + try { + return createParserForClass(cls, is); + } catch (XmlPullParserException e) { + e.printStackTrace(); + return null; + } + } + + private GDataParser createParserForClass( + Class cls, InputStream is) + throws ParseException, XmlPullParserException { + return new XmlGDataParser(is, xmlFactory.createParser()); + } + + @Override + public GDataSerializer createSerializer(Entry en) { + return new XmlEntryGDataSerializer(xmlFactory, en); + } +} \ No newline at end of file diff --git a/MyTracks/src/com/google/wireless/gdata/maps/MapFeatureEntry.java b/MyTracks/src/com/google/wireless/gdata/maps/MapFeatureEntry.java deleted file mode 100644 index 4114f86d1..000000000 --- a/MyTracks/src/com/google/wireless/gdata/maps/MapFeatureEntry.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright 2010 Google Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ -package com.google.wireless.gdata.maps; - -import com.google.wireless.gdata.data.Entry; - -import java.util.HashMap; -import java.util.Map; - -/** - * GData entry for a map feature. - */ -public 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/MyTracksLib/default.properties b/MyTracksLib/default.properties index 1694c805b..2ba8b4cd2 100644 --- a/MyTracksLib/default.properties +++ b/MyTracksLib/default.properties @@ -8,5 +8,5 @@ # project structure. # Project target. -target=android-9 +target=android-11 android.library=true diff --git a/MyTracksLib/src/com/google/android/apps/mytracks/content/MyTracksLocation.java b/MyTracksLib/src/com/google/android/apps/mytracks/content/MyTracksLocation.java index 4566225d5..2aaa1c4f1 100644 --- a/MyTracksLib/src/com/google/android/apps/mytracks/content/MyTracksLocation.java +++ b/MyTracksLib/src/com/google/android/apps/mytracks/content/MyTracksLocation.java @@ -36,8 +36,8 @@ public class MyTracksLocation extends Location { this.sensorDataSet = sd; } - public MyTracksLocation(String string) { - super(string); + public MyTracksLocation(String provider) { + super(provider); } public Sensor.SensorDataSet getSensorDataSet() { diff --git a/MyTracksLib/src/com/google/android/apps/mytracks/content/MyTracksProviderUtils.java b/MyTracksLib/src/com/google/android/apps/mytracks/content/MyTracksProviderUtils.java index 586228b25..a9506f1fc 100644 --- a/MyTracksLib/src/com/google/android/apps/mytracks/content/MyTracksProviderUtils.java +++ b/MyTracksLib/src/com/google/android/apps/mytracks/content/MyTracksProviderUtils.java @@ -301,7 +301,7 @@ public interface MyTracksProviderUtils { * @return a new waypoint object */ Waypoint createWaypoint(Cursor cursor); - + /** * A lightweight wrapper around the original {@link Cursor} with a method to clean up. */ @@ -312,13 +312,14 @@ public interface MyTracksProviderUtils { * @return the ID of the most recent track point ID. */ long getLocationId(); - + /** * Should be called in case the underlying iterator hasn't reached the last record. + * Calling it if it has reached the last record is a no-op. */ void close(); } - + /** * A factory for creating new {@class Location}s. */ @@ -342,6 +343,24 @@ public interface MyTracksProviderUtils { return new Location("gps"); } }; + + /** + * A location factory which uses two location instances (one for the current location, + * and one for the previous), useful when we need to keep the last location. + */ + public class DoubleBufferedLocationFactory implements LocationFactory { + private final Location locs[] = new MyTracksLocation[] { + new MyTracksLocation("gps"), + new MyTracksLocation("gps") + }; + private int lastLoc = 0; + + @Override + public Location createLocation() { + lastLoc = (lastLoc + 1) % locs.length; + return locs[lastLoc]; + } + } /** * Creates a new read-only iterator over all track points for the given track. It provides diff --git a/MyTracksLib/src/com/google/android/apps/mytracks/content/MyTracksProviderUtilsImpl.java b/MyTracksLib/src/com/google/android/apps/mytracks/content/MyTracksProviderUtilsImpl.java index 4d4ec7d8f..64905da79 100644 --- a/MyTracksLib/src/com/google/android/apps/mytracks/content/MyTracksProviderUtilsImpl.java +++ b/MyTracksLib/src/com/google/android/apps/mytracks/content/MyTracksProviderUtilsImpl.java @@ -1024,7 +1024,7 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils { cursor = null; } } - + @Override public void remove() { throw new UnsupportedOperationException(); diff --git a/MyTracksTest/.factorypath b/MyTracksTest/.factorypath index 37f943be0..26bc0bdc0 100644 --- a/MyTracksTest/.factorypath +++ b/MyTracksTest/.factorypath @@ -1,6 +1,6 @@ - - + + diff --git a/MyTracksTest/default.properties b/MyTracksTest/default.properties index 85cbfc4dd..533ddb5df 100644 --- a/MyTracksTest/default.properties +++ b/MyTracksTest/default.properties @@ -10,4 +10,4 @@ # Indicates whether an apk should be generated for each density. split.density=false # Project target. -target=Google Inc.:Google APIs:9 +target=Google Inc.:Google APIs:11 diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/DialogManagerTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/DialogManagerTest.java index bacd00edf..1b23a0315 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,23 +67,6 @@ public class DialogManagerTest Dialog d = getActivity().onCreateDialog(DialogManager.DIALOG_SEND_TO_GOOGLE, null); assertNotNull(d); - assertTrue(d instanceof SendToGoogleDialog); + assertTrue(d instanceof SendDialog); } - - public void test_onCreateSendToGoogleResult() { - Dialog d = - getActivity().onCreateDialog( - DialogManager.DIALOG_SEND_TO_GOOGLE_RESULT, null); - assertNotNull(d); - assertTrue(d instanceof AlertDialog); - } - - public void test_onCreateWriteProgress() { - Dialog d = - getActivity().onCreateDialog( - DialogManager.DIALOG_WRITE_PROGRESS, null); - assertNotNull(d); - assertTrue(d instanceof ProgressDialog); - } - } diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/MyTracksOverlayTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/MapOverlayTest.java similarity index 97% rename from MyTracksTest/src/com/google/android/apps/mytracks/MyTracksOverlayTest.java rename to MyTracksTest/src/com/google/android/apps/mytracks/MapOverlayTest.java index 06049f4d7..3ea8d4a11 100644 --- a/MyTracksTest/src/com/google/android/apps/mytracks/MyTracksOverlayTest.java +++ b/MyTracksTest/src/com/google/android/apps/mytracks/MapOverlayTest.java @@ -39,17 +39,17 @@ import java.util.List; * * @author Bartlomiej Niechwiej */ -public class MyTracksOverlayTest extends AndroidTestCase { +public class MapOverlayTest extends AndroidTestCase { private Canvas canvas; private MockMyTracksOverlay myTracksOverlay; private MapView mockView; private Projection mockProjection; /** - * A mock version of {@code MyTracksOverlay} that does not use + * A mock version of {@code MapOverlay} that does not use * {@class MapView}. */ - private class MockMyTracksOverlay extends MyTracksOverlay { + private class MockMyTracksOverlay extends MapOverlay { public MockMyTracksOverlay(Context context) { super(context); } diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/MyTracksTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/MyTracksTest.java index 545c09450..7c5151944 100644 --- a/MyTracksTest/src/com/google/android/apps/mytracks/MyTracksTest.java +++ b/MyTracksTest/src/com/google/android/apps/mytracks/MyTracksTest.java @@ -165,7 +165,7 @@ public class MyTracksTest extends ActivityInstrumentationTestCase2{ // Watch for MyTracksDetails activity. ActivityMonitor monitor = getInstrumentation().addMonitor( - MyTracksDetails.class.getName(), null, false); + TrackDetails.class.getName(), null, false); // Now, stop the track and make sure that it is still selected, but // no longer recording. @@ -173,7 +173,7 @@ public class MyTracksTest extends ActivityInstrumentationTestCase2{ // Check if we got back MyTracksDetails activity. Activity activity = getInstrumentation().waitForMonitor(monitor); - assertTrue(activity instanceof MyTracksDetails); + assertTrue(activity instanceof TrackDetails); // TODO: Update track name and other properties and test if they were // properly saved. diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/TrackDataHubTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/TrackDataHubTest.java new file mode 100644 index 000000000..43f97b80b --- /dev/null +++ b/MyTracksTest/src/com/google/android/apps/mytracks/TrackDataHubTest.java @@ -0,0 +1,478 @@ +/* + * 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; + +import static com.google.android.testing.mocking.AndroidMock.anyInt; +import static com.google.android.testing.mocking.AndroidMock.capture; +import static com.google.android.testing.mocking.AndroidMock.eq; +import static com.google.android.testing.mocking.AndroidMock.expect; +import static com.google.android.testing.mocking.AndroidMock.isA; +import static com.google.android.testing.mocking.AndroidMock.leq; +import static com.google.android.testing.mocking.AndroidMock.same; + +import com.google.android.apps.mytracks.TrackDataHub.ListenerDataType; +import com.google.android.apps.mytracks.TrackDataListener.ProviderState; +import com.google.android.apps.mytracks.content.MyTracksProviderUtils; +import com.google.android.apps.mytracks.content.Track; +import com.google.android.apps.mytracks.content.TracksColumns; +import com.google.android.apps.mytracks.content.Waypoint; +import com.google.android.apps.mytracks.content.WaypointsColumns; +import com.google.android.apps.mytracks.services.TrackRecordingServiceTest.MockContext; +import com.google.android.maps.mytracks.R; +import com.google.android.testing.mocking.AndroidMock; + +import android.content.SharedPreferences; +import android.content.SharedPreferences.OnSharedPreferenceChangeListener; +import android.database.ContentObserver; +import android.database.Cursor; +import android.database.MatrixCursor; +import android.hardware.Sensor; +import android.hardware.SensorEvent; +import android.hardware.SensorEventListener; +import android.location.Location; +import android.location.LocationListener; +import android.provider.BaseColumns; +import android.test.AndroidTestCase; +import android.test.RenamingDelegatingContext; +import android.test.mock.MockContentResolver; + +import java.lang.reflect.Constructor; +import java.util.EnumSet; + +import org.easymock.Capture; +import org.easymock.IAnswer; + +/** + * Tests for {@link TrackDataHub}. + * + * @author Rodrigo Damazio + */ +public class TrackDataHubTest extends AndroidTestCase { + + private static final long TRACK_ID = 42; + private MyTracksProviderUtils providerUtils; + private TrackDataHub hub; + private TrackDataSources dataSources; + private SharedPreferences prefs; + private TrackDataListener listener1; + private TrackDataListener listener2; + private Capture preferenceListenerCapture = + new Capture(); + private MockContext context; + private float declination; + + @Override + protected void setUp() throws Exception { + super.setUp(); + + MockContentResolver mockContentResolver = new MockContentResolver(); + RenamingDelegatingContext targetContext = new RenamingDelegatingContext( + getContext(), getContext(), "test."); + context = new MockContext(mockContentResolver, targetContext); + + prefs = context.getSharedPreferences(Constants.SETTINGS_NAME, 0); + providerUtils = AndroidMock.createMock("providerUtils", MyTracksProviderUtils.class); + dataSources = AndroidMock.createNiceMock("dataSources", TrackDataSources.class); + + hub = new TrackDataHub(context, dataSources, prefs, providerUtils) { + @Override + protected void runInListenerThread(Runnable runnable) { + // Run everything in the same thread. + runnable.run(); + } + + @Override + protected float getDeclinationFor(Location location, long timestamp) { + return declination; + } + }; + + listener1 = AndroidMock.createMock("listener1", TrackDataListener.class); + listener2 = AndroidMock.createMock("listener2", TrackDataListener.class); + } + + @Override + protected void tearDown() throws Exception { + AndroidMock.reset(dataSources); + + // Expect everything to be unregistered. + if (preferenceListenerCapture.hasCaptured()) { + dataSources.unregisterOnSharedPreferenceChangeListener(preferenceListenerCapture.getValue()); + } + dataSources.removeLocationUpdates(isA(LocationListener.class)); + dataSources.unregisterSensorListener(isA(SensorEventListener.class)); + dataSources.unregisterContentObserver(isA(ContentObserver.class)); + AndroidMock.expectLastCall().times(3); + + AndroidMock.replay(dataSources); + + hub.stop(); + hub.destroy(); + + super.tearDown(); + } + + public void testTrackListen() { + Capture observerCapture = new Capture(); + Track track = new Track(); + prefs.edit().putLong("recordingTrack", TRACK_ID) + .putLong("selectedTrack", TRACK_ID).commit(); + expect(providerUtils.getTrack(TRACK_ID)).andStubReturn(track); + expectStart(); + dataSources.registerContentObserver( + eq(TracksColumns.CONTENT_URI), eq(false), capture(observerCapture)); + + // Expect the initial loading. + // Both listeners (registered before and after start) should get the same data. + listener1.onTrackUpdated(track); + listener2.onTrackUpdated(track); + + AndroidMock.replay(dataSources, providerUtils, listener1, listener2); + + hub.registerTrackDataListener(listener1, EnumSet.of(ListenerDataType.TRACK_UPDATES)); + hub.start(); + hub.registerTrackDataListener(listener2, EnumSet.of(ListenerDataType.TRACK_UPDATES)); + + AndroidMock.verify(listener1, listener2, dataSources); + AndroidMock.reset(listener1, listener2, dataSources); + + ContentObserver observer = observerCapture.getValue(); + + // Now expect an update. + listener1.onTrackUpdated(track); + listener2.onTrackUpdated(track); + + AndroidMock.replay(listener1, listener2, dataSources); + + observer.onChange(false); + + AndroidMock.verify(listener1, listener2, dataSources); + AndroidMock.reset(listener1, listener2, dataSources); + + // Unregister one, get another update. + listener2.onTrackUpdated(track); + + AndroidMock.replay(listener1, listener2, dataSources); + + hub.unregisterTrackDataListener(listener1); + + observer.onChange(false); + + AndroidMock.verify(listener1, listener2, dataSources); + AndroidMock.reset(listener1, listener2, dataSources); + + // Unregister the other, expect internal unregistration + dataSources.unregisterContentObserver(observer); + + AndroidMock.replay(listener1, listener2, dataSources); + + hub.unregisterTrackDataListener(listener2); + observer.onChange(false); + + AndroidMock.verify(listener1, listener2, dataSources); + } + + private static class FixedSizeCursorAnswer implements IAnswer { + private final int size; + + public FixedSizeCursorAnswer(int size) { + this.size = size; + } + + @Override + public Cursor answer() throws Throwable { + MatrixCursor cursor = new MatrixCursor(new String[] { BaseColumns._ID }); + for (long i = 1; i <= size; i++) { + cursor.addRow(new Object[] { i }); + } + return cursor; + } + } + + public void testWaypointListen() { + Capture observerCapture = new Capture(); + prefs.edit().putLong("recordingTrack", TRACK_ID) + .putLong("selectedTrack", TRACK_ID).commit(); + + Waypoint wpt1 = new Waypoint(), + wpt2 = new Waypoint(), + wpt3 = new Waypoint(), + wpt4 = new Waypoint(); + Location loc = new Location("gps"); + loc.setLatitude(10.0); + loc.setLongitude(8.0); + wpt1.setLocation(loc); + wpt2.setLocation(loc); + wpt3.setLocation(loc); + wpt4.setLocation(loc); + + expect(providerUtils.getWaypointsCursor( + eq(TRACK_ID), leq(0L), eq(Constants.MAX_DISPLAYED_WAYPOINTS_POINTS))) + .andStubAnswer(new FixedSizeCursorAnswer(2)); + expect(providerUtils.createWaypoint(isA(Cursor.class))) + .andReturn(wpt1) + .andReturn(wpt2) + .andReturn(wpt1) + .andReturn(wpt2); + + expectStart(); + dataSources.registerContentObserver( + eq(WaypointsColumns.CONTENT_URI), eq(false), capture(observerCapture)); + + // Expect the initial loading. + // Both listeners (registered before and after start) should get the same data. + listener1.clearWaypoints(); + listener1.onNewWaypoint(wpt1); + listener1.onNewWaypoint(wpt2); + listener1.onNewWaypointsDone(); + listener2.clearWaypoints(); + listener2.onNewWaypoint(wpt1); + listener2.onNewWaypoint(wpt2); + listener2.onNewWaypointsDone(); + + AndroidMock.replay(dataSources, providerUtils, listener1, listener2); + + hub.registerTrackDataListener(listener1, EnumSet.of(ListenerDataType.WAYPOINT_UPDATES)); + hub.start(); + hub.registerTrackDataListener(listener2, EnumSet.of(ListenerDataType.WAYPOINT_UPDATES)); + + AndroidMock.verify(listener1, listener2, dataSources, providerUtils); + AndroidMock.reset(listener1, listener2, dataSources, providerUtils); + + ContentObserver observer = observerCapture.getValue(); + + expect(providerUtils.getWaypointsCursor( + eq(TRACK_ID), leq(0L), eq(Constants.MAX_DISPLAYED_WAYPOINTS_POINTS))) + .andStubAnswer(new FixedSizeCursorAnswer(3)); + expect(providerUtils.createWaypoint(isA(Cursor.class))) + .andReturn(wpt1) + .andReturn(wpt2) + .andReturn(wpt3); + + // Now expect an update. + listener1.clearWaypoints(); + listener2.clearWaypoints(); + listener1.onNewWaypoint(wpt1); + listener2.onNewWaypoint(wpt1); + listener1.onNewWaypoint(wpt2); + listener2.onNewWaypoint(wpt2); + listener1.onNewWaypoint(wpt3); + listener2.onNewWaypoint(wpt3); + listener1.onNewWaypointsDone(); + listener2.onNewWaypointsDone(); + + AndroidMock.replay(listener1, listener2, dataSources, providerUtils); + + observer.onChange(false); + + AndroidMock.verify(listener1, listener2, dataSources, providerUtils); + AndroidMock.reset(listener1, listener2, dataSources, providerUtils); + + // Unregister one, get another update. + expect(providerUtils.getWaypointsCursor( + eq(TRACK_ID), leq(0L), eq(Constants.MAX_DISPLAYED_WAYPOINTS_POINTS))) + .andStubAnswer(new FixedSizeCursorAnswer(4)); + expect(providerUtils.createWaypoint(isA(Cursor.class))) + .andReturn(wpt1) + .andReturn(wpt2) + .andReturn(wpt3) + .andReturn(wpt4); + + // Now expect an update. + listener2.clearWaypoints(); + listener2.onNewWaypoint(wpt1); + listener2.onNewWaypoint(wpt2); + listener2.onNewWaypoint(wpt3); + listener2.onNewWaypoint(wpt4); + listener2.onNewWaypointsDone(); + + AndroidMock.replay(listener1, listener2, dataSources, providerUtils); + + hub.unregisterTrackDataListener(listener1); + + observer.onChange(false); + + AndroidMock.verify(listener1, listener2, dataSources, providerUtils); + AndroidMock.reset(listener1, listener2, dataSources, providerUtils); + + // Unregister the other, expect internal unregistration + dataSources.unregisterContentObserver(observer); + + AndroidMock.replay(listener1, listener2, dataSources, providerUtils); + + hub.unregisterTrackDataListener(listener2); + observer.onChange(false); + + AndroidMock.verify(listener1, listener2, dataSources, providerUtils); + } + + public void testPointsListen() { + // TODO + } + + public void testPointsListen_resample() { + // TODO + } + + public void testLocationListen() { + // TODO + } + + public void testCompassListen() throws Exception { + Sensor compass = newSensor(); + expect(dataSources.getSensor(Sensor.TYPE_ORIENTATION)).andReturn(compass); + Capture listenerCapture = new Capture(); + dataSources.registerSensorListener(capture(listenerCapture), same(compass), anyInt()); + + Capture locationListenerCapture = new Capture(); + dataSources.requestLocationUpdates(capture(locationListenerCapture)); + + SensorEvent event = newSensorEvent(); + event.sensor = compass; + + // First, get a dummy heading update. + listener1.onCurrentHeadingChanged(0.0); + + // Then, get a heading update without a known location (thus can't calculate declination). + listener1.onCurrentHeadingChanged(42.0f); + + // Also expect location updates which are not relevant to us. + listener1.onProviderStateChange(isA(ProviderState.class)); + AndroidMock.expectLastCall().anyTimes(); + + AndroidMock.replay(dataSources, providerUtils, listener1); + + hub.registerTrackDataListener(listener1, + EnumSet.of(ListenerDataType.COMPASS_UPDATES, ListenerDataType.LOCATION_UPDATES)); + hub.start(); + + SensorEventListener sensorListener = listenerCapture.getValue(); + LocationListener locationListener = locationListenerCapture.getValue(); + event.values[0] = 42.0f; + sensorListener.onSensorChanged(event); + + AndroidMock.verify(dataSources, providerUtils, listener1); + AndroidMock.reset(dataSources, providerUtils, listener1); + + // Expect the heading update to include declination. + listener1.onCurrentHeadingChanged(52.0); + + // Also expect location updates which are not relevant to us. + listener1.onProviderStateChange(isA(ProviderState.class)); + AndroidMock.expectLastCall().anyTimes(); + listener1.onCurrentLocationChanged(isA(Location.class)); + AndroidMock.expectLastCall().anyTimes(); + + AndroidMock.replay(dataSources, providerUtils, listener1); + + // Now try injecting a location update, triggering a declination update. + Location location = new Location("gps"); + location.setLatitude(10.0); + location.setLongitude(20.0); + location.setAltitude(30.0); + declination = 10.0f; + locationListener.onLocationChanged(location); + sensorListener.onSensorChanged(event); + + AndroidMock.verify(dataSources, providerUtils, listener1); + AndroidMock.reset(dataSources, providerUtils, listener1); + + listener1.onCurrentHeadingChanged(52.0); + + AndroidMock.replay(dataSources, providerUtils, listener1); + + // Now try changing the known declination - it should still return the old declination, since + // updates only happen sparsely. + declination = 20.0f; + sensorListener.onSensorChanged(event); + + AndroidMock.verify(dataSources, providerUtils, listener1); + } + + private Sensor newSensor() throws Exception { + Constructor constructor = Sensor.class.getDeclaredConstructor(); + constructor.setAccessible(true); + return constructor.newInstance(); + } + + private SensorEvent newSensorEvent() throws Exception { + Constructor constructor = SensorEvent.class.getDeclaredConstructor(int.class); + constructor.setAccessible(true); + return constructor.newInstance(3); + } + + public void testDisplayPreferencesListen() throws Exception { + String metricUnitsKey = context.getString(R.string.metric_units_key); + String speedKey = context.getString(R.string.report_speed_key); + + prefs.edit() + .putBoolean(metricUnitsKey, true) + .putBoolean(speedKey, true) + .commit(); + + Capture listenerCapture = + new Capture(); + dataSources.registerOnSharedPreferenceChangeListener(capture(listenerCapture)); + + expect(listener1.onUnitsChanged(true)).andReturn(false); + expect(listener2.onUnitsChanged(true)).andReturn(false); + expect(listener1.onReportSpeedChanged(true)).andReturn(false); + expect(listener2.onReportSpeedChanged(true)).andReturn(false); + + AndroidMock.replay(dataSources, providerUtils, listener1, listener2); + + hub.registerTrackDataListener(listener1, EnumSet.of(ListenerDataType.DISPLAY_PREFERENCES)); + hub.start(); + hub.registerTrackDataListener(listener2, EnumSet.of(ListenerDataType.DISPLAY_PREFERENCES)); + + AndroidMock.verify(dataSources, providerUtils, listener1, listener2); + AndroidMock.reset(dataSources, providerUtils, listener1, listener2); + + expect(listener1.onReportSpeedChanged(false)).andReturn(false); + expect(listener2.onReportSpeedChanged(false)).andReturn(false); + + AndroidMock.replay(dataSources, providerUtils, listener1, listener2); + + prefs.edit() + .putBoolean(speedKey, false) + .commit(); + OnSharedPreferenceChangeListener listener = listenerCapture.getValue(); + listener.onSharedPreferenceChanged(prefs, speedKey); + + AndroidMock.verify(dataSources, providerUtils, listener1, listener2); + AndroidMock.reset(dataSources, providerUtils, listener1, listener2); + + expect(listener1.onUnitsChanged(false)).andReturn(false); + expect(listener2.onUnitsChanged(false)).andReturn(false); + + AndroidMock.replay(dataSources, providerUtils, listener1, listener2); + + prefs.edit() + .putBoolean(metricUnitsKey, false) + .commit(); + listener.onSharedPreferenceChanged(prefs, metricUnitsKey); + + AndroidMock.verify(dataSources, providerUtils, listener1, listener2); + } + + public void testFullListener() { + // TODO: test loading a track, getting updates, loading another, unloading + } + + private void expectStart() { + dataSources.registerOnSharedPreferenceChangeListener(capture(preferenceListenerCapture)); + } +} diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/WriteProgressControllerTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/WriteProgressControllerTest.java new file mode 100644 index 000000000..2f4349b32 --- /dev/null +++ b/MyTracksTest/src/com/google/android/apps/mytracks/WriteProgressControllerTest.java @@ -0,0 +1,78 @@ +/* + * 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; + +import com.google.android.apps.mytracks.io.file.MockTrackWriter; +import com.google.android.apps.mytracks.io.file.TrackWriter; + +import android.app.ProgressDialog; +import android.test.ActivityInstrumentationTestCase2; + +import java.util.concurrent.Semaphore; +import java.util.concurrent.atomic.AtomicReference; + +/** + * Tests {@link WriteProgressController}. + * + * @author Matthew Simmons + */ +public class WriteProgressControllerTest extends ActivityInstrumentationTestCase2 { + public WriteProgressControllerTest() { + super(MyTracks.class); + } + + private static void assertProgress(ProgressDialog dialog, int expectedProgress, + int expectedMax) { + assertEquals(expectedProgress, dialog.getProgress()); + assertEquals(expectedMax, dialog.getMax()); + } + + public void testSimple() throws Exception { + final AtomicReference dialogRef = new AtomicReference(); + final AtomicReference controllerDoneRef = new AtomicReference(); + final Semaphore writerDone = new Semaphore(0); + + TrackWriter mockWriter = new MockTrackWriter() { + @Override + public void writeTrackAsync() { + onWriteListener.onWrite(1000, 10000); + assertProgress(dialogRef.get(), 1000, 10000); + + onCompletionListener.onComplete(); + writerDone.release(); + } + }; + + WriteProgressController controller = new WriteProgressController( + getActivity(), mockWriter); + controller.setOnCompletionListener(new WriteProgressController.OnCompletionListener() { + @Override + public void onComplete(TrackWriter writer) { + controllerDoneRef.set(true); + } + }); + + dialogRef.set(controller.getDialog()); + + controller.startWrite(); + + // wait for the writer to finish + writerDone.acquire(); + + assertFalse(dialogRef.get().isShowing()); + assertTrue(controllerDoneRef.get()); + } +} diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/io/backup/PreferenceBackupHelperTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/io/backup/PreferenceBackupHelperTest.java index e2c8b3b48..25a6c2504 100644 --- a/MyTracksTest/src/com/google/android/apps/mytracks/io/backup/PreferenceBackupHelperTest.java +++ b/MyTracksTest/src/com/google/android/apps/mytracks/io/backup/PreferenceBackupHelperTest.java @@ -20,6 +20,7 @@ import android.content.SharedPreferences.Editor; import java.util.HashMap; import java.util.Map; +import java.util.Set; import junit.framework.TestCase; @@ -81,6 +82,10 @@ public class PreferenceBackupHelperTest extends TestCase { return put(key, value); } + public Editor putStringSet(String key, Set value) { + return put(key, value); + } + private Editor put(String key, T value) { newPreferences.put(key, value); return this; @@ -137,6 +142,10 @@ public class PreferenceBackupHelperTest extends TestCase { return get(key, defValue); } + public Set getStringSet(String key, Set defValue) { + return get(key, defValue); + } + @Override public void registerOnSharedPreferenceChangeListener( OnSharedPreferenceChangeListener listener) { 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/CsvTrackWriterTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/io/file/CsvTrackWriterTest.java similarity index 79% rename from MyTracksTest/src/com/google/android/apps/mytracks/io/CsvTrackWriterTest.java rename to MyTracksTest/src/com/google/android/apps/mytracks/io/file/CsvTrackWriterTest.java index fa0d70deb..9fd40ab05 100644 --- a/MyTracksTest/src/com/google/android/apps/mytracks/io/CsvTrackWriterTest.java +++ b/MyTracksTest/src/com/google/android/apps/mytracks/io/file/CsvTrackWriterTest.java @@ -1,6 +1,6 @@ // Copyright 2010 Google Inc. All Rights Reserved. -package com.google.android.apps.mytracks.io; +package com.google.android.apps.mytracks.io.file; /** * Tests for the CSV track exporter. diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/io/GpxImporterTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/io/file/GpxImporterTest.java similarity index 98% rename from MyTracksTest/src/com/google/android/apps/mytracks/io/GpxImporterTest.java rename to MyTracksTest/src/com/google/android/apps/mytracks/io/file/GpxImporterTest.java index b819f5c2a..e676aae88 100644 --- a/MyTracksTest/src/com/google/android/apps/mytracks/io/GpxImporterTest.java +++ b/MyTracksTest/src/com/google/android/apps/mytracks/io/file/GpxImporterTest.java @@ -13,7 +13,7 @@ * License for the specific language governing permissions and limitations under * the License. */ -package com.google.android.apps.mytracks.io; +package com.google.android.apps.mytracks.io.file; import static com.google.android.testing.mocking.AndroidMock.eq; import static com.google.android.testing.mocking.AndroidMock.expect; @@ -22,6 +22,7 @@ import com.google.android.apps.mytracks.content.MyTracksProviderUtils; import com.google.android.apps.mytracks.content.Track; import com.google.android.apps.mytracks.content.TracksColumns; import com.google.android.apps.mytracks.content.MyTracksProviderUtils.Factory; +import com.google.android.apps.mytracks.io.file.GpxImporter; import com.google.android.apps.mytracks.testing.TestingProviderUtilsFactory; import com.google.android.testing.mocking.AndroidMock; import com.google.android.testing.mocking.UsesMocks; diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/io/GpxTrackWriterTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/io/file/GpxTrackWriterTest.java similarity index 93% rename from MyTracksTest/src/com/google/android/apps/mytracks/io/GpxTrackWriterTest.java rename to MyTracksTest/src/com/google/android/apps/mytracks/io/file/GpxTrackWriterTest.java index c9c3e7f42..36c78a33c 100644 --- a/MyTracksTest/src/com/google/android/apps/mytracks/io/GpxTrackWriterTest.java +++ b/MyTracksTest/src/com/google/android/apps/mytracks/io/file/GpxTrackWriterTest.java @@ -1,14 +1,12 @@ // Copyright 2010 Google Inc. All Rights Reserved. -package com.google.android.apps.mytracks.io; +package com.google.android.apps.mytracks.io.file; -import com.google.android.apps.mytracks.content.Waypoint; - -import android.location.Location; +import com.google.android.apps.mytracks.io.file.GpxTrackWriter; +import com.google.android.apps.mytracks.io.file.TrackFormatWriter; 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/KmlTrackWriterTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/io/file/KmlTrackWriterTest.java similarity index 97% rename from MyTracksTest/src/com/google/android/apps/mytracks/io/KmlTrackWriterTest.java rename to MyTracksTest/src/com/google/android/apps/mytracks/io/file/KmlTrackWriterTest.java index e3e735f2d..1a3deee16 100644 --- a/MyTracksTest/src/com/google/android/apps/mytracks/io/KmlTrackWriterTest.java +++ b/MyTracksTest/src/com/google/android/apps/mytracks/io/file/KmlTrackWriterTest.java @@ -1,7 +1,8 @@ // Copyright 2010 Google Inc. All Rights Reserved. -package com.google.android.apps.mytracks.io; +package com.google.android.apps.mytracks.io.file; import com.google.android.apps.mytracks.content.Track; +import com.google.android.apps.mytracks.io.file.KmlTrackWriter; import com.google.android.apps.mytracks.util.StringUtils; import android.location.Location; diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/io/file/MockTrackWriter.java b/MyTracksTest/src/com/google/android/apps/mytracks/io/file/MockTrackWriter.java new file mode 100644 index 000000000..813d42a52 --- /dev/null +++ b/MyTracksTest/src/com/google/android/apps/mytracks/io/file/MockTrackWriter.java @@ -0,0 +1,76 @@ +/* + * 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.file; + +import java.io.File; + +/** + * A simple, fake {@link TrackWriter} subclass with all methods mocked out. + * Tests are expected to override {@link #writeTrack} and/or + * {@link #writeTrackAsync}, at the very least. + * + * @author Matthew Simmons + * + */ +public class MockTrackWriter implements TrackWriter { + public OnCompletionListener onCompletionListener; + public OnWriteListener onWriteListener; + + @Override + public void setOnCompletionListener(OnCompletionListener onCompletionListener) { + this.onCompletionListener = onCompletionListener; + } + + @Override + public void setOnWriteListener(OnWriteListener onWriteListener) { + this.onWriteListener = onWriteListener; + } + + @Override + public void setDirectory(File directory) { + throw new UnsupportedOperationException("not implemented"); + } + + @Override + public String getAbsolutePath() { + throw new UnsupportedOperationException("not implemented"); + } + + @Override + public void writeTrackAsync() { + throw new UnsupportedOperationException("not implemented"); + } + + @Override + public void writeTrack() { + throw new UnsupportedOperationException("not implemented"); + } + + @Override + public void stopWriteTrack() { + throw new UnsupportedOperationException("not implemented"); + } + + @Override + public boolean wasSuccess() { + return false; + } + + @Override + public int getErrorMessage() { + return 0; + } +} diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/io/TcxTrackWriterTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/io/file/TcxTrackWriterTest.java similarity index 80% rename from MyTracksTest/src/com/google/android/apps/mytracks/io/TcxTrackWriterTest.java rename to MyTracksTest/src/com/google/android/apps/mytracks/io/file/TcxTrackWriterTest.java index 897df2792..380ca972f 100644 --- a/MyTracksTest/src/com/google/android/apps/mytracks/io/TcxTrackWriterTest.java +++ b/MyTracksTest/src/com/google/android/apps/mytracks/io/file/TcxTrackWriterTest.java @@ -1,24 +1,37 @@ // Copyright 2010 Google Inc. All Rights Reserved. -package com.google.android.apps.mytracks.io; +package com.google.android.apps.mytracks.io.file; import com.google.android.apps.mytracks.content.MyTracksLocation; import com.google.android.apps.mytracks.content.Sensor; +import com.google.android.apps.mytracks.io.file.TcxTrackWriter; +import com.google.android.apps.mytracks.io.file.TrackFormatWriter; + +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 +71,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/TempFileCleanerTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/io/file/TempFileCleanerTest.java similarity index 96% rename from MyTracksTest/src/com/google/android/apps/mytracks/io/TempFileCleanerTest.java rename to MyTracksTest/src/com/google/android/apps/mytracks/io/file/TempFileCleanerTest.java index 25d600cbe..cea67b571 100644 --- a/MyTracksTest/src/com/google/android/apps/mytracks/io/TempFileCleanerTest.java +++ b/MyTracksTest/src/com/google/android/apps/mytracks/io/file/TempFileCleanerTest.java @@ -13,9 +13,11 @@ * License for the specific language governing permissions and limitations under * the License. */ -package com.google.android.apps.mytracks.io; +package com.google.android.apps.mytracks.io.file; import static com.google.android.testing.mocking.AndroidMock.expect; + +import com.google.android.apps.mytracks.io.file.TempFileCleaner; import com.google.android.testing.mocking.AndroidMock; import com.google.android.testing.mocking.UsesMocks; diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/io/TrackFormatWriterTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/io/file/TrackFormatWriterTest.java similarity index 97% rename from MyTracksTest/src/com/google/android/apps/mytracks/io/TrackFormatWriterTest.java rename to MyTracksTest/src/com/google/android/apps/mytracks/io/file/TrackFormatWriterTest.java index a4827fecd..6af065c5d 100644 --- a/MyTracksTest/src/com/google/android/apps/mytracks/io/TrackFormatWriterTest.java +++ b/MyTracksTest/src/com/google/android/apps/mytracks/io/file/TrackFormatWriterTest.java @@ -1,5 +1,5 @@ // Copyright 2010 Google Inc. All Rights Reserved. -package com.google.android.apps.mytracks.io; +package com.google.android.apps.mytracks.io.file; import com.google.android.apps.mytracks.content.MyTracksLocation; import com.google.android.apps.mytracks.content.Sensor; @@ -8,14 +8,6 @@ import com.google.android.apps.mytracks.content.Waypoint; import android.test.AndroidTestCase; -import org.w3c.dom.Document; -import org.w3c.dom.Element; -import org.w3c.dom.Node; -import org.w3c.dom.NodeList; -import org.w3c.dom.Text; -import org.xml.sax.InputSource; -import org.xml.sax.SAXException; - import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.OutputStream; @@ -28,6 +20,14 @@ import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.FactoryConfigurationError; import javax.xml.parsers.ParserConfigurationException; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.w3c.dom.Text; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; + /** * Base class for track format writer tests, which sets up a fake track and * gives auxiliary methods for verifying XML output. @@ -104,9 +104,9 @@ public abstract class TrackFormatWriterTest extends AndroidTestCase { * track in {@link #track}. * * @param writer the writer to write to - * @return the written contents + * @return the written contents */ - protected String writeTrack(TrackFormatWriter writer) { + protected String writeTrack(TrackFormatWriter writer) throws Exception { OutputStream output = new ByteArrayOutputStream(BUFFER_SIZE); writer.prepare(track, output); writer.writeHeader(); diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/io/TrackWriterTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/io/file/TrackWriterTest.java similarity index 79% rename from MyTracksTest/src/com/google/android/apps/mytracks/io/TrackWriterTest.java rename to MyTracksTest/src/com/google/android/apps/mytracks/io/file/TrackWriterTest.java index c80013a68..7ec10b12a 100644 --- a/MyTracksTest/src/com/google/android/apps/mytracks/io/TrackWriterTest.java +++ b/MyTracksTest/src/com/google/android/apps/mytracks/io/file/TrackWriterTest.java @@ -1,16 +1,17 @@ // Copyright 2010 Google Inc. All Rights Reserved. -package com.google.android.apps.mytracks.io; +package com.google.android.apps.mytracks.io.file; import static org.easymock.EasyMock.expect; import com.google.android.apps.mytracks.content.MyTracksProvider; import com.google.android.apps.mytracks.content.MyTracksProviderUtils; +import com.google.android.apps.mytracks.content.MyTracksProviderUtils.Factory; import com.google.android.apps.mytracks.content.Track; import com.google.android.apps.mytracks.content.Waypoint; -import com.google.android.apps.mytracks.content.MyTracksProviderUtils.Factory; import com.google.android.apps.mytracks.services.TrackRecordingServiceTest.MockContext; import com.google.android.apps.mytracks.testing.TestingProviderUtilsFactory; +import com.google.android.maps.mytracks.R; import android.content.Context; import android.location.Location; @@ -23,6 +24,7 @@ import java.io.FileNotFoundException; import java.io.OutputStream; import org.easymock.EasyMock; +import org.easymock.IAnswer; import org.easymock.IArgumentMatcher; import org.easymock.IMocksControl; @@ -33,10 +35,10 @@ import org.easymock.IMocksControl; */ public class TrackWriterTest extends AndroidTestCase { /** - * {@link TrackWriter} subclass which mocks out methods called from - * {@link TrackWriter#openFile}. + * {@link TrackWriterImpl} subclass which mocks out methods called from + * {@link TrackWriterImpl#openFile}. */ - private static final class OpenFileTrackWriter extends TrackWriter { + private static final class OpenFileTrackWriter extends TrackWriterImpl { private final ByteArrayOutputStream stream; private final boolean canWrite; @@ -44,9 +46,9 @@ public class TrackWriterTest extends AndroidTestCase { * Constructor. * * @param stream the stream to return from - * {@link TrackWriter#newOutputStream}, or null to throw a + * {@link TrackWriterImpl#newOutputStream}, or null to throw a * {@link FileNotFoundException} - * @param canWrite the value that {@link TrackWriter#canWriteFile} will + * @param canWrite the value that {@link TrackWriterImpl#canWriteFile} will * return */ private OpenFileTrackWriter(Context context, @@ -78,16 +80,16 @@ public class TrackWriterTest extends AndroidTestCase { } /** - * {@link TrackWriter} subclass which mocks out methods called from - * {@link TrackWriter#writeTrack}. + * {@link TrackWriterImpl} subclass which mocks out methods called from + * {@link TrackWriterImpl#writeTrack}. */ - private final class WriteTracksTrackWriter extends TrackWriter { + private final class WriteTracksTrackWriter extends TrackWriterImpl { private final boolean openResult; /** * Constructor. * - * @param openResult the return value for {@link TrackWriter#openFile} + * @param openResult the return value for {@link TrackWriterImpl#openFile} */ private WriteTracksTrackWriter(Context context, MyTracksProviderUtils providerUtils, Track track, @@ -121,7 +123,7 @@ public class TrackWriterTest extends AndroidTestCase { private Track track; private TrackFormatWriter formatWriter; - private TrackWriter writer; + private TrackWriterImpl writer; private IMocksControl mocksControl; private MyTracksProviderUtils providerUtils; private Factory oldProviderUtilsFactory; @@ -144,7 +146,7 @@ public class TrackWriterTest extends AndroidTestCase { setContext(context); providerUtils = MyTracksProviderUtils.Factory.get(context); oldProviderUtilsFactory = TestingProviderUtilsFactory.installWithInstance(providerUtils); - + mocksControl = EasyMock.createStrictControl(); formatWriter = mocksControl.createMock(TrackFormatWriter.class); expect(formatWriter.getExtension()).andStubReturn(EXTENSION); @@ -164,12 +166,13 @@ public class TrackWriterTest extends AndroidTestCase { writer = new WriteTracksTrackWriter(getContext(), providerUtils, track, formatWriter, true); - // Expect the completion callback to be run - Runnable completionCallback = mocksControl.createMock(Runnable.class); - completionCallback.run(); + // Expect the completion listener to be run + TrackWriter.OnCompletionListener completionListener + = mocksControl.createMock(TrackWriter.OnCompletionListener.class); + completionListener.onComplete(); mocksControl.replay(); - writer.setOnCompletion(completionCallback); + writer.setOnCompletionListener(completionListener); writer.writeTrack(); assertEquals(1, writeDocumentCalls); @@ -177,23 +180,61 @@ public class TrackWriterTest extends AndroidTestCase { mocksControl.verify(); } + public void testWriteTrack_cancelled() throws Exception { + final ByteArrayOutputStream stream = new ByteArrayOutputStream(); + writer = new OpenFileTrackWriter( + getContext(), providerUtils, track, formatWriter, stream, true); + + formatWriter.prepare(track, stream); + + final Location[] locs = { + new Location("fake0"), + new Location("fake1"), + }; + fillLocations(locs); + assertEquals(locs.length, providerUtils.bulkInsertTrackPoints(locs, locs.length, TRACK_ID)); + + formatWriter.writeHeader(); + formatWriter.writeBeginTrack(locEq(locs[0])); + formatWriter.writeOpenSegment(); + + formatWriter.writeLocation(locEq(locs[0])); + //EasyMock.expectLastCall().andThrow(new InterruptedException()); + EasyMock.expectLastCall().andAnswer(new IAnswer() { + @Override + public Object answer() throws Throwable { + throw new InterruptedException(); + } + }); + + mocksControl.replay(); + + writer.writeTrack(); + + mocksControl.verify(); + + assertFalse(writer.wasSuccess()); + assertEquals(R.string.error_operation_cancelled, writer.getErrorMessage()); + } + public void testWriteTrack_openFails() { writer = new WriteTracksTrackWriter(getContext(), providerUtils, track, formatWriter, false); - // Expect the completion callback to be run - Runnable completionCallback = mocksControl.createMock(Runnable.class); - completionCallback.run(); + // Expect the completion listener to be run + TrackWriter.OnCompletionListener completionListener + = mocksControl.createMock(TrackWriter.OnCompletionListener.class); + completionListener.onComplete(); mocksControl.replay(); - writer.setOnCompletion(completionCallback); + writer.setOnCompletionListener(completionListener); writer.writeTrack(); assertEquals(0, writeDocumentCalls); assertEquals(1, openFileCalls); mocksControl.verify(); } - + public void testOpenFile() { final ByteArrayOutputStream stream = new ByteArrayOutputStream(); writer = new OpenFileTrackWriter( @@ -225,8 +266,8 @@ public class TrackWriterTest extends AndroidTestCase { mocksControl.verify(); } - public void testWriteDocument_emptyTrack() { - writer = new TrackWriter(getContext(), providerUtils, track, formatWriter); + public void testWriteDocument_emptyTrack() throws Exception { + writer = new TrackWriterImpl(getContext(), providerUtils, track, formatWriter); // Set expected mock behavior formatWriter.writeHeader(); @@ -240,8 +281,8 @@ public class TrackWriterTest extends AndroidTestCase { mocksControl.verify(); } - public void testWriteDocument() { - writer = new TrackWriter(getContext(), providerUtils, track, formatWriter); + public void testWriteDocument() throws Exception { + writer = new TrackWriterImpl(getContext(), providerUtils, track, formatWriter); final Location[] locs = { new Location("fake0"), @@ -258,7 +299,7 @@ public class TrackWriterTest extends AndroidTestCase { // Make location 3 invalid locs[2].setLatitude(100); - + assertEquals(locs.length, providerUtils.bulkInsertTrackPoints(locs, locs.length, TRACK_ID)); for (int i = 0; i < wps.length; ++i) { Waypoint wpt = wps[i]; @@ -313,7 +354,7 @@ public class TrackWriterTest extends AndroidTestCase { return wpt.getId() == wpt2.getId(); } - + @Override public void appendTo(StringBuffer buffer) { buffer.append("wptEq("); @@ -323,7 +364,7 @@ public class TrackWriterTest extends AndroidTestCase { }); return null; } - + private static Location locEq(final Location loc) { EasyMock.reportMatcher(new IArgumentMatcher() { @Override @@ -343,7 +384,7 @@ public class TrackWriterTest extends AndroidTestCase { && loc.getLongitude() == loc2.getLongitude() && loc.getTime() == loc2.getTime(); } - + @Override public void appendTo(StringBuffer buffer) { buffer.append("locEq("); @@ -353,7 +394,7 @@ public class TrackWriterTest extends AndroidTestCase { }); return null; } - + private void fillLocations(Location... locs) { assertTrue(locs.length < 90); for (int i = 0; i < locs.length; i++) { 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/TrackRecordingServiceTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/services/TrackRecordingServiceTest.java index db0236aa2..0f4c13ca8 100644 --- a/MyTracksTest/src/com/google/android/apps/mytracks/services/TrackRecordingServiceTest.java +++ b/MyTracksTest/src/com/google/android/apps/mytracks/services/TrackRecordingServiceTest.java @@ -15,9 +15,9 @@ */ package com.google.android.apps.mytracks.services; -import static com.google.android.apps.mytracks.MyTracksConstants.RESUME_TRACK_EXTRA_NAME; +import static com.google.android.apps.mytracks.Constants.RESUME_TRACK_EXTRA_NAME; -import com.google.android.apps.mytracks.MyTracksSettings; +import com.google.android.apps.mytracks.Constants; import com.google.android.apps.mytracks.content.MyTracksProvider; import com.google.android.apps.mytracks.content.MyTracksProviderUtils; import com.google.android.apps.mytracks.content.Track; @@ -152,7 +152,7 @@ public class TrackRecordingServiceTest providerUtils = MyTracksProviderUtils.Factory.get(context); sharedPreferences = context.getSharedPreferences( - MyTracksSettings.SETTINGS_NAME, 0); + Constants.SETTINGS_NAME, 0); // Let's use default values. sharedPreferences.edit().clear().commit(); diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/services/sensors/AntDirectSensorManagerTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/services/sensors/AntDirectSensorManagerTest.java index ea0e482ae..c50ecb985 100644 --- a/MyTracksTest/src/com/google/android/apps/mytracks/services/sensors/AntDirectSensorManagerTest.java +++ b/MyTracksTest/src/com/google/android/apps/mytracks/services/sensors/AntDirectSensorManagerTest.java @@ -16,8 +16,9 @@ package com.google.android.apps.mytracks.services.sensors; import com.dsi.ant.AntMesg; -import com.google.android.apps.mytracks.MyTracksSettings; +import com.google.android.apps.mytracks.Constants; import com.google.android.apps.mytracks.content.Sensor; +import com.google.android.apps.mytracks.services.sensors.ant.AntDirectSensorManager; import com.google.android.maps.mytracks.R; import android.content.SharedPreferences; @@ -31,7 +32,7 @@ public class AntDirectSensorManagerTest extends AndroidTestCase { public void setUp() { sharedPreferences = getContext().getSharedPreferences( - MyTracksSettings.SETTINGS_NAME, 0); + Constants.SETTINGS_NAME, 0); // Let's use default values. sharedPreferences.edit().clear().commit(); manager = new AntDirectSensorManager(getContext()); diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/services/sensors/AntSensorManagerTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/services/sensors/AntSensorManagerTest.java index c636d6eae..0019bf0ae 100644 --- a/MyTracksTest/src/com/google/android/apps/mytracks/services/sensors/AntSensorManagerTest.java +++ b/MyTracksTest/src/com/google/android/apps/mytracks/services/sensors/AntSensorManagerTest.java @@ -15,6 +15,8 @@ */ package com.google.android.apps.mytracks.services.sensors; +import com.google.android.apps.mytracks.services.sensors.ant.AntSensorManager; + import android.test.AndroidTestCase; import android.test.MoreAsserts; diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/services/sensors/SensorManagerFactoryTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/services/sensors/SensorManagerFactoryTest.java index 3951aa3c1..933ba333a 100644 --- a/MyTracksTest/src/com/google/android/apps/mytracks/services/sensors/SensorManagerFactoryTest.java +++ b/MyTracksTest/src/com/google/android/apps/mytracks/services/sensors/SensorManagerFactoryTest.java @@ -1,6 +1,8 @@ package com.google.android.apps.mytracks.services.sensors; -import com.google.android.apps.mytracks.MyTracksSettings; +import com.google.android.apps.mytracks.Constants; +import com.google.android.apps.mytracks.services.sensors.ant.AntDirectSensorManager; +import com.google.android.apps.mytracks.services.sensors.ant.AntSRMSensorManager; import com.google.android.maps.mytracks.R; import android.content.SharedPreferences; @@ -16,7 +18,7 @@ public class SensorManagerFactoryTest extends AndroidTestCase { super.setUp(); sharedPreferences = getContext().getSharedPreferences( - MyTracksSettings.SETTINGS_NAME, 0); + Constants.SETTINGS_NAME, 0); // Let's use default values. sharedPreferences.edit().clear().commit(); } diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/services/StatusAnnouncerFactoryTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/services/tasks/StatusAnnouncerFactoryTest.java similarity index 87% rename from MyTracksTest/src/com/google/android/apps/mytracks/services/StatusAnnouncerFactoryTest.java rename to MyTracksTest/src/com/google/android/apps/mytracks/services/tasks/StatusAnnouncerFactoryTest.java index 304002a8d..ea9a8d441 100644 --- a/MyTracksTest/src/com/google/android/apps/mytracks/services/StatusAnnouncerFactoryTest.java +++ b/MyTracksTest/src/com/google/android/apps/mytracks/services/tasks/StatusAnnouncerFactoryTest.java @@ -13,8 +13,11 @@ * License for the specific language governing permissions and limitations under * the License. */ -package com.google.android.apps.mytracks.services; +package com.google.android.apps.mytracks.services.tasks; +import com.google.android.apps.mytracks.services.tasks.PeriodicTask; +import com.google.android.apps.mytracks.services.tasks.StatusAnnouncerFactory; +import com.google.android.apps.mytracks.services.tasks.StatusAnnouncerTask; import com.google.android.apps.mytracks.util.ApiFeatures; import android.media.AudioManager; @@ -55,7 +58,7 @@ public class StatusAnnouncerFactoryTest extends AndroidTestCase { public void testCreate() { apiFeatures.setHasTextToSpeech(true); - StatusAnnouncerFactory factory = + PeriodicTaskFactory factory = new StatusAnnouncerFactory(apiFeatures); PeriodicTask task = factory.create(getContext()); assertTrue(task instanceof StatusAnnouncerTask); @@ -63,7 +66,7 @@ public class StatusAnnouncerFactoryTest extends AndroidTestCase { public void testCreate_notAvailable() { apiFeatures.setHasTextToSpeech(false); - StatusAnnouncerFactory factory = + PeriodicTaskFactory factory = new StatusAnnouncerFactory(apiFeatures); PeriodicTask task = factory.create(getContext()); assertNull(task); diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/services/StatusAnnouncerTaskTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/services/tasks/StatusAnnouncerTaskTest.java similarity index 90% rename from MyTracksTest/src/com/google/android/apps/mytracks/services/StatusAnnouncerTaskTest.java rename to MyTracksTest/src/com/google/android/apps/mytracks/services/tasks/StatusAnnouncerTaskTest.java index 59ef6853c..5d0bc0c87 100644 --- a/MyTracksTest/src/com/google/android/apps/mytracks/services/StatusAnnouncerTaskTest.java +++ b/MyTracksTest/src/com/google/android/apps/mytracks/services/tasks/StatusAnnouncerTaskTest.java @@ -1,25 +1,26 @@ /* * Copyright 2010 Google Inc. - * + * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of * the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations under * the License. */ -package com.google.android.apps.mytracks.services; +package com.google.android.apps.mytracks.services.tasks; import static com.google.android.testing.mocking.AndroidMock.capture; import static com.google.android.testing.mocking.AndroidMock.eq; import static com.google.android.testing.mocking.AndroidMock.expect; import static com.google.android.testing.mocking.AndroidMock.same; +import com.google.android.apps.mytracks.services.tasks.StatusAnnouncerTask; import com.google.android.apps.mytracks.stats.TripStatistics; import com.google.android.apps.mytracks.util.StringUtils; import com.google.android.testing.mocking.AndroidMock; @@ -255,7 +256,6 @@ public class StatusAnnouncerTaskTest extends AndroidTestCase { @UsesMocks({ StatusAnnouncerTask.class, StringUtils.class, - TrackRecordingService.class, }) @Override protected void setUp() throws Exception { @@ -390,9 +390,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 +404,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 +474,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/MyTracksTest/src/com/google/android/apps/mytracks/stats/TripStatisticsBuilderTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/stats/TripStatisticsBuilderTest.java index ccb3fa858..750bc31de 100644 --- a/MyTracksTest/src/com/google/android/apps/mytracks/stats/TripStatisticsBuilderTest.java +++ b/MyTracksTest/src/com/google/android/apps/mytracks/stats/TripStatisticsBuilderTest.java @@ -2,7 +2,7 @@ package com.google.android.apps.mytracks.stats; -import com.google.android.apps.mytracks.MyTracksConstants; +import com.google.android.apps.mytracks.Constants; import android.location.Location; @@ -56,18 +56,18 @@ public class TripStatisticsBuilderTest extends TestCase { assertEquals(10000 * i, stats.getTotalTime()); assertEquals(10000 * i, stats.getMovingTime()); assertEquals(i, builder.getSmoothedElevation(), - MyTracksConstants.ELEVATION_SMOOTHING_FACTOR / 2); + Constants.ELEVATION_SMOOTHING_FACTOR / 2); assertEquals(0.0, stats.getMinElevation()); assertEquals(i, stats.getMaxElevation(), - MyTracksConstants.ELEVATION_SMOOTHING_FACTOR / 2); + Constants.ELEVATION_SMOOTHING_FACTOR / 2); assertEquals(i, stats.getTotalElevationGain(), - MyTracksConstants.ELEVATION_SMOOTHING_FACTOR); + Constants.ELEVATION_SMOOTHING_FACTOR); - if (i > MyTracksConstants.SPEED_SMOOTHING_FACTOR) { + if (i > Constants.SPEED_SMOOTHING_FACTOR) { assertEquals(11.1f, stats.getMaxSpeed(), 0.1); } - if ((i > MyTracksConstants.GRADE_SMOOTHING_FACTOR) - && (i > MyTracksConstants.ELEVATION_SMOOTHING_FACTOR)) { + if ((i > Constants.GRADE_SMOOTHING_FACTOR) + && (i > Constants.ELEVATION_SMOOTHING_FACTOR)) { assertEquals(0.009, stats.getMinGrade(), 0.0001); assertEquals(0.009, stats.getMaxGrade(), 0.0001); } @@ -97,9 +97,9 @@ public class TripStatisticsBuilderTest extends TestCase { public void testElevationGain() throws Exception { for (double i = 0; i < 1000; i++) { double expectedGain; - if (i < (MyTracksConstants.ELEVATION_SMOOTHING_FACTOR - 1)) { + if (i < (Constants.ELEVATION_SMOOTHING_FACTOR - 1)) { expectedGain = 0; - } else if (i < MyTracksConstants.ELEVATION_SMOOTHING_FACTOR) { + } else if (i < Constants.ELEVATION_SMOOTHING_FACTOR) { expectedGain = 0.5; } else { expectedGain = 1.0; @@ -110,9 +110,9 @@ public class TripStatisticsBuilderTest extends TestCase { TripStatistics data = builder.getStatistics(); assertEquals(0.0, data.getMinElevation(), 0.0); assertEquals(i, data.getMaxElevation(), - MyTracksConstants.ELEVATION_SMOOTHING_FACTOR); + Constants.ELEVATION_SMOOTHING_FACTOR); assertEquals(i, data.getTotalElevationGain(), - MyTracksConstants.ELEVATION_SMOOTHING_FACTOR); + Constants.ELEVATION_SMOOTHING_FACTOR); } } @@ -122,8 +122,8 @@ public class TripStatisticsBuilderTest extends TestCase { // buffer. builder.updateElevation(i); builder.updateGrade(100, 100); - if ((i > MyTracksConstants.GRADE_SMOOTHING_FACTOR) - && (i > MyTracksConstants.ELEVATION_SMOOTHING_FACTOR)) { + if ((i > Constants.GRADE_SMOOTHING_FACTOR) + && (i > Constants.ELEVATION_SMOOTHING_FACTOR)) { assertEquals(1.0, builder.getStatistics().getMaxGrade()); assertEquals(1.0, builder.getStatistics().getMinGrade()); } @@ -133,8 +133,8 @@ public class TripStatisticsBuilderTest extends TestCase { // buffer. builder.updateElevation(i); builder.updateGrade(100, -100); - if ((i > MyTracksConstants.GRADE_SMOOTHING_FACTOR) - && (i > MyTracksConstants.ELEVATION_SMOOTHING_FACTOR)) { + if ((i > Constants.GRADE_SMOOTHING_FACTOR) + && (i > Constants.ELEVATION_SMOOTHING_FACTOR)) { assertEquals(1.0, builder.getStatistics().getMaxGrade()); assertEquals(-1.0, builder.getStatistics().getMinGrade()); } @@ -176,7 +176,7 @@ public class TripStatisticsBuilderTest extends TestCase { for (int i = 0; i < 1000; i++) { builder.updateSpeed(i + 1000, 4.0, i, 4.0); assertEquals((i + 1) * 1000, builder.getStatistics().getMovingTime()); - if (i > MyTracksConstants.SPEED_SMOOTHING_FACTOR) { + if (i > Constants.SPEED_SMOOTHING_FACTOR) { assertEquals(4.0, builder.getStatistics().getMaxSpeed()); } } diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/util/ChartURLGeneratorTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/util/ChartURLGeneratorTest.java index d54524d8a..ea0c9f582 100644 --- a/MyTracksTest/src/com/google/android/apps/mytracks/util/ChartURLGeneratorTest.java +++ b/MyTracksTest/src/com/google/android/apps/mytracks/util/ChartURLGeneratorTest.java @@ -63,8 +63,7 @@ public class ChartURLGeneratorTest extends TestCase { elevations, t, "Title", - true, - false); + true); assertEquals( "http://chart.apis.google.com/chart?&chs=600x350&cht=lxy&" diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/util/FileUtilsTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/util/FileUtilsTest.java index 01b44c6b6..9cd32f498 100644 --- a/MyTracksTest/src/com/google/android/apps/mytracks/util/FileUtilsTest.java +++ b/MyTracksTest/src/com/google/android/apps/mytracks/util/FileUtilsTest.java @@ -15,7 +15,7 @@ */ package com.google.android.apps.mytracks.util; -import com.google.android.apps.mytracks.MyTracksConstants; +import com.google.android.apps.mytracks.Constants; import android.os.Environment; @@ -53,7 +53,7 @@ public class FileUtilsTest extends TestCase { public void testBuildExternalDirectoryPath() { String expectedName = Environment.getExternalStorageDirectory() + File.separator - + MyTracksConstants.SDCARD_TOP_DIR + + Constants.SDCARD_TOP_DIR + File.separator + "a" + File.separator diff --git a/SignalStrengthSampler/default.properties b/SignalStrengthSampler/default.properties index 903536e41..4db050713 100644 --- a/SignalStrengthSampler/default.properties +++ b/SignalStrengthSampler/default.properties @@ -8,6 +8,6 @@ # project structure. # Project target. -target=Google Inc.:Google APIs:9 +target=Google Inc.:Google APIs:11 proguard.config=proguard.cfg android.library.reference.1=../MyTracksLib/ diff --git a/SignalStrengthSamplerTest/default.properties b/SignalStrengthSamplerTest/default.properties index 77f7ad75b..728f51f97 100644 --- a/SignalStrengthSamplerTest/default.properties +++ b/SignalStrengthSamplerTest/default.properties @@ -8,4 +8,4 @@ # project structure. # Project target. -target=Google Inc.:Google APIs:9 +target=Google Inc.:Google APIs:11 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 diff --git a/scripts/i18n/.project b/scripts/i18n/.project new file mode 100644 index 000000000..c21d8dd65 --- /dev/null +++ b/scripts/i18n/.project @@ -0,0 +1,17 @@ + + + i18n + + + + + + org.python.pydev.PyDevBuilder + + + + + + org.python.pydev.pythonNature + + diff --git a/scripts/i18n/.pydevproject b/scripts/i18n/.pydevproject new file mode 100644 index 000000000..94dbecf08 --- /dev/null +++ b/scripts/i18n/.pydevproject @@ -0,0 +1,10 @@ + + + + +Default +python 2.6 + +/i18n/src + + diff --git a/scripts/i18n/src/mytracks/__init__.py b/scripts/i18n/src/mytracks/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/scripts/i18n/src/mytracks/files.py b/scripts/i18n/src/mytracks/files.py new file mode 100644 index 000000000..1ec50da8a --- /dev/null +++ b/scripts/i18n/src/mytracks/files.py @@ -0,0 +1,45 @@ +''' +Module for dealing with resource files (but not their contents). + +@author: Rodrigo Damazio +''' +import os.path +from glob import glob +import re + +MYTRACKS_RES_DIR = 'MyTracks/res' +ANDROID_MASTER_VALUES = 'values' +ANDROID_VALUES_MASK = 'values-*' + + +def GetMyTracksDir(): + ''' + Returns the directory in which the MyTracks directory is located. + ''' + path = os.getcwd() + while not os.path.isdir(os.path.join(path, MYTRACKS_RES_DIR)): + if path == '/': + raise 'Not in My Tracks project' + + # Go up one level + path = os.path.split(path)[0] + + return path + + +def GetAllLanguageFiles(): + ''' + Returns a mapping from all found languages to their respective directories. + ''' + mytracks_path = GetMyTracksDir() + res_dir = os.path.join(mytracks_path, MYTRACKS_RES_DIR, ANDROID_VALUES_MASK) + language_dirs = glob(res_dir) + master_dir = os.path.join(mytracks_path, MYTRACKS_RES_DIR, ANDROID_MASTER_VALUES) + if len(language_dirs) == 0: + raise 'No languages found!' + if not os.path.isdir(master_dir): + raise 'Couldn\'t find master file' + + language_tuples = [(re.findall(r'.*values-([A-Za-z-]+)', dir)[0],dir) for dir in language_dirs] + language_tuples.append(('en', master_dir)) + return dict(language_tuples) diff --git a/scripts/i18n/src/mytracks/history.py b/scripts/i18n/src/mytracks/history.py new file mode 100644 index 000000000..a585a0105 --- /dev/null +++ b/scripts/i18n/src/mytracks/history.py @@ -0,0 +1,94 @@ +''' +Module which brings history information about files from Mercurial. + +@author: Rodrigo Damazio +''' + +import re +import subprocess + +REVISION_REGEX = re.compile(r'(?P[0-9a-f]{12}):.*') + +def _GetOutputLines(args): + ''' + Runs an external process and returns its output as a list of lines. + + @param args: the arguments to run + ''' + process = subprocess.Popen(args, + stdout=subprocess.PIPE, + universal_newlines = True, + shell = False) + output = process.communicate()[0] + return output.splitlines() + + +def FillMercurialRevisions(filename, parsed_file): + ''' + Fills the revs attribute of all strings in the given parsed file with + a list of revisions that touched the lines corresponding to that string. + + @param filename: the name of the file to get history for + @param parsed_file: the parsed file to modify + ''' + # Take output of hg annotate to get revision of each line + output_lines = _GetOutputLines(['hg', 'annotate', '-c', filename]) + + # Create a map of line -> revision (key is list index, line 0 doesn't exist) + line_revs = ['dummy'] + for line in output_lines: + rev_match = REVISION_REGEX.match(line) + if not rev_match: + raise 'Unexpected line of output from hg: %s' % line + rev_hash = rev_match.group('hash') + line_revs.append(rev_hash) + + for str in parsed_file.itervalues(): + # Get the lines that correspond to each string + start_line = str['startLine'] + end_line = str['endLine'] + + # Get the revisions that touched those lines + revs = [] + for line_number in range(start_line, end_line + 1): + revs.append(line_revs[line_number]) + + # Merge with any revisions that were already there + # (for explict revision specification) + if 'revs' in str: + revs += str['revs'] + + # Assign the revisions to the string + str['revs'] = frozenset(revs) + +def DoesRevisionSuperceed(filename, rev1, rev2): + ''' + Tells whether a revision superceeds another. + This essentially means that the older revision is an ancestor of the newer + one. + This also returns True if the two revisions are the same. + + @param rev1: the revision that may be superceeding the other + @param rev2: the revision that may be superceeded + @return: True if rev1 superceeds rev2 or they're the same + ''' + if rev1 == rev2: + return True + + # TODO: Add filename + args = ['hg', 'log', '-r', 'ancestors(%s)' % rev1, '--template', '{node|short}\n', filename] + output_lines = _GetOutputLines(args) + + return rev2 in output_lines + +def NewestRevision(filename, rev1, rev2): + ''' + Returns which of two revisions is closest to the head of the repository. + If none of them is the ancestor of the other, then we return either one. + + @param rev1: the first revision + @param rev2: the second revision + ''' + if DoesRevisionSuperceed(filename, rev1, rev2): + return rev1 + return rev2 \ No newline at end of file diff --git a/scripts/i18n/src/mytracks/main.py b/scripts/i18n/src/mytracks/main.py new file mode 100755 index 000000000..4841c6858 --- /dev/null +++ b/scripts/i18n/src/mytracks/main.py @@ -0,0 +1,96 @@ +#!/usr/bin/python +''' +Entry point for My Tracks i18n tool. + +@author: Rodrigo Damazio +''' + +import mytracks.files +import mytracks.translate +import mytracks.validate +import sys + +def Usage(): + print 'Usage: %s [ ...]\n' % sys.argv[0] + print 'Commands are:' + print ' cleanup' + print ' translate' + print ' validate' + sys.exit(1) + + +def Translate(languages): + ''' + Asks the user to interactively translate any missing or oudated strings from + the files for the given languages. + + @param languages: the languages to translate + ''' + validator = mytracks.validate.Validator(languages) + validator.Validate() + missing = validator.missing_in_lang() + outdated = validator.outdated_in_lang() + + for lang in languages: + untranslated = missing[lang] + outdated[lang] + + if len(untranslated) == 0: + continue + + translator = mytracks.translate.Translator(lang) + translator.Translate(untranslated) + + +def Validate(languages): + ''' + Computes and displays errors in the string files for the given languages. + + @param languages: the languages to compute for + ''' + validator = mytracks.validate.Validator(languages) + validator.Validate() + + error_count = 0 + if (validator.valid()): + print 'All files OK' + else: + for lang, missing in validator.missing_in_master().iteritems(): + print 'Missing in master, present in %s: %s:' % (lang, str(missing)) + error_count = error_count + len(missing) + for lang, missing in validator.missing_in_lang().iteritems(): + print 'Missing in %s, present in master: %s:' % (lang, str(missing)) + error_count = error_count + len(missing) + for lang, outdated in validator.outdated_in_lang().iteritems(): + print 'Outdated in %s: %s:' % (lang, str(outdated)) + error_count = error_count + len(outdated) + + return error_count + + +if __name__ == '__main__': + argv = sys.argv + argc = len(argv) + if argc < 2: + Usage() + + languages = mytracks.files.GetAllLanguageFiles() + if argc == 3: + langs = set(argv[2:]) + if not langs.issubset(languages): + raise 'Language(s) not found' + + # Filter just to the languages specified + languages = dict((lang, lang_file) + for lang, lang_file in languages.iteritems() + if lang in langs or lang == 'en' ) + + cmd = argv[1] + if cmd == 'translate': + Translate(languages) + elif cmd == 'validate': + error_count = Validate(languages) + else: + Usage() + error_count = 0 + + print '%d errors found.' % error_count diff --git a/scripts/i18n/src/mytracks/parser.py b/scripts/i18n/src/mytracks/parser.py new file mode 100644 index 000000000..9cfb96de8 --- /dev/null +++ b/scripts/i18n/src/mytracks/parser.py @@ -0,0 +1,115 @@ +''' +Module which parses a string XML file. + +@author: Rodrigo Damazio +''' + +from xml.parsers.expat import ParserCreate +import re +#import xml.etree.ElementTree as ET + +class StringsParser(object): + ''' + Parser for string XML files. + + This object is not thread-safe and should be used for parsing a single file at + a time, only. + ''' + + def Parse(self, file): + ''' + Parses the given file and returns a dictionary mapping keys to an object + with attributes for that key, such as the value, start/end line and explicit + revisions. + + In addition to the standard XML format of the strings file, this parser + supports an annotation inside comments, in one of these formats: + + + + + Such an annotation indicates that we're explicitly inheriting form the + master file (and the optional revision says that this decision is compatible + with the master file up to that revision). + + @param file: the name of the file to parse + ''' + self._Reset() + + # Unfortunately expat is the only parser that will give us line numbers + self._xml_parser = ParserCreate() + self._xml_parser.StartElementHandler = self._StartElementHandler + self._xml_parser.EndElementHandler = self._EndElementHandler + self._xml_parser.CharacterDataHandler = self._CharacterDataHandler + self._xml_parser.CommentHandler = self._CommentHandler + + file_obj = open(file) + self._xml_parser.ParseFile(file_obj) + file_obj.close() + + return self._all_strings + + def _Reset(self): + self._currentString = None + self._currentStringName = None + self._currentStringValue = None + self._all_strings = {} + + def _StartElementHandler(self, name, attrs): + if name != 'string': + return + + if 'name' not in attrs: + return + + assert not self._currentString + assert not self._currentStringName + self._currentString = { + 'startLine' : self._xml_parser.CurrentLineNumber, + } + + if 'rev' in attrs: + self._currentString['revs'] = [attrs['rev']] + + self._currentStringName = attrs['name'] + self._currentStringValue = '' + + def _EndElementHandler(self, name): + if name != 'string': + return + + assert self._currentString + assert self._currentStringName + self._currentString['value'] = self._currentStringValue + self._currentString['endLine'] = self._xml_parser.CurrentLineNumber + self._all_strings[self._currentStringName] = self._currentString + + self._currentString = None + self._currentStringName = None + self._currentStringValue = None + + def _CharacterDataHandler(self, data): + if not self._currentString: + return + + self._currentStringValue += data + + _KEEP_PARENT_REGEX = re.compile(r'\s*KEEP_PARENT\s+' + r'name\s*=\s*[\'"]?(?P[a-z0-9_]+)[\'"]?' + r'(?:\s+rev=[\'"]?(?P[0-9a-f]{12})[\'"]?)?\s*', + re.MULTILINE | re.DOTALL) + + def _CommentHandler(self, data): + keep_parent_match = self._KEEP_PARENT_REGEX.match(data) + if not keep_parent_match: + return + + name = keep_parent_match.group('name') + self._all_strings[name] = { + 'keepParent' : True, + 'startLine' : self._xml_parser.CurrentLineNumber, + 'endLine' : self._xml_parser.CurrentLineNumber + } + rev = keep_parent_match.group('rev') + if rev: + self._all_strings[name]['revs'] = [rev] \ No newline at end of file diff --git a/scripts/i18n/src/mytracks/translate.py b/scripts/i18n/src/mytracks/translate.py new file mode 100644 index 000000000..5e6e45a86 --- /dev/null +++ b/scripts/i18n/src/mytracks/translate.py @@ -0,0 +1,21 @@ +''' +Module which prompts the user for translations and saves them. + +TODO: implement + +@author: Rodrigo Damazio +''' + +class Translator(object): + ''' + classdocs + ''' + + def __init__(self, language): + ''' + Constructor + ''' + self._language = language + + def Translate(self, string_names): + print string_names \ No newline at end of file diff --git a/scripts/i18n/src/mytracks/validate.py b/scripts/i18n/src/mytracks/validate.py new file mode 100644 index 000000000..ba99b8f27 --- /dev/null +++ b/scripts/i18n/src/mytracks/validate.py @@ -0,0 +1,115 @@ +''' +Module which compares languague files to the master file and detects +issues. + +@author: Rodrigo Damazio +''' + +import os +from mytracks.parser import StringsParser +import mytracks.history + +class Validator(object): + + def __init__(self, languages): + ''' + Builds a strings file validator. + + Params: + @param languages: a dictionary mapping each language to its corresponding directory + ''' + self._langs = {} + self._master = None + self._language_paths = languages + + parser = StringsParser() + for lang, lang_dir in languages.iteritems(): + filename = os.path.join(lang_dir, 'strings.xml') + parsed_file = parser.Parse(filename) + mytracks.history.FillMercurialRevisions(filename, parsed_file) + + if lang == 'en': + self._master = parsed_file + else: + self._langs[lang] = parsed_file + + self._Reset() + + def Validate(self): + ''' + Computes whether all the data in the files for the given languages is valid. + ''' + self._Reset() + self._ValidateMissingKeys() + self._ValidateOutdatedKeys() + + def valid(self): + return (len(self._missing_in_master) == 0 and + len(self._missing_in_lang) == 0 and + len(self._outdated_in_lang) == 0) + + def missing_in_master(self): + return self._missing_in_master + + def missing_in_lang(self): + return self._missing_in_lang + + def outdated_in_lang(self): + return self._outdated_in_lang + + def _Reset(self): + # These are maps from language to string name list + self._missing_in_master = {} + self._missing_in_lang = {} + self._outdated_in_lang = {} + + def _ValidateMissingKeys(self): + ''' + Computes whether there are missing keys on either side. + ''' + master_keys = frozenset(self._master.iterkeys()) + for lang, file in self._langs.iteritems(): + keys = frozenset(file.iterkeys()) + missing_in_master = keys - master_keys + missing_in_lang = master_keys - keys + + if len(missing_in_master) > 0: + self._missing_in_master[lang] = missing_in_master + if len(missing_in_lang) > 0: + self._missing_in_lang[lang] = missing_in_lang + + def _ValidateOutdatedKeys(self): + ''' + Computers whether any of the language keys are outdated with relation to the + master keys. + ''' + for lang, file in self._langs.iteritems(): + outdated = [] + for key, str in file.iteritems(): + # Get all revisions that touched master and language files for this + # string. + master_str = self._master[key] + master_revs = master_str['revs'] + lang_revs = str['revs'] + if not master_revs or not lang_revs: + print 'WARNING: No revision for %s in %s' % (key, lang) + continue + + master_file = os.path.join(self._language_paths['en'], 'strings.xml') + lang_file = os.path.join(self._language_paths[lang], 'strings.xml') + + # Assume that the repository has a single head (TODO: check that), + # and as such there is always one revision which superceeds all others. + master_rev = reduce( + lambda r1, r2: mytracks.history.NewestRevision(master_file, r1, r2), + master_revs) + lang_rev = reduce( + lambda r1, r2: mytracks.history.NewestRevision(lang_file, r1, r2), + lang_revs) + + # If the master version is newer than the lang version + if mytracks.history.DoesRevisionSuperceed(lang_file, master_rev, lang_rev): + outdated.append(key) + + if len(outdated) > 0: + self._outdated_in_lang[lang] = outdated