diff --git a/.hgignore b/.hgignore index 7220e4cb0..3bf9706a8 100644 --- a/.hgignore +++ b/.hgignore @@ -2,5 +2,6 @@ syntax:glob */bin */gen/** +*/proguard/** .metadata MyTracksTest/annotations.log diff --git a/MyTracks/AndroidManifest.xml b/MyTracks/AndroidManifest.xml index 25bdf9c10..51487fe03 100755 --- a/MyTracks/AndroidManifest.xml +++ b/MyTracks/AndroidManifest.xml @@ -114,6 +114,9 @@ limitations under the License. + + + @@ -156,7 +159,7 @@ limitations under the License. - + @@ -167,6 +170,7 @@ limitations under the License. - + + - + - + - + + + + + + + + diff --git a/MyTracks/res/layout/mytracks_elevation.xml b/MyTracks/res/layout/mytracks_charts.xml similarity index 94% rename from MyTracks/res/layout/mytracks_elevation.xml rename to MyTracks/res/layout/mytracks_charts.xml index 823ee8be1..e758d30bd 100644 --- a/MyTracks/res/layout/mytracks_elevation.xml +++ b/MyTracks/res/layout/mytracks_charts.xml @@ -30,13 +30,15 @@ limitations under the License. android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent" + android:padding="10dip" android:layout_weight="0"> + android:layout_centerHorizontal="true" + android:layout_alignParentRight="true"/> - Off + Aus 1 min 2 min 5 min @@ -70,8 +70,8 @@ limitations under the License. - batterie sparend (adaptiv) - hohe genauigkeit (adaptiv) + batteriesparend (adaptiv) + hohe Genauigkeit (adaptiv) maximal (empfohlen) 2 s 3 s @@ -133,12 +133,12 @@ limitations under the License. Nicht zerteilen - 100 meilen - 50 meilen - 25 meilen - 10 meilen - 5 meilen - 1 meile + 100 Meilen + 50 Meilen + 25 Meilen + 10 Meilen + 5 Meilen + 1 Meile 60 min 30 min 15 min @@ -159,7 +159,7 @@ limitations under the License. Laufbahn Geländelauf Gehen - Schnelles Gehen + Gehen (Sport) Wandern Bergwandern Bergsteigen @@ -181,11 +181,11 @@ limitations under the License. Eissegeln Segeln - Kanadierfahren - Kajakfahren + Paddeln + Kajak fahren Kanuwandern Rudern - Motorbootfahren + Motorboot fahren Fähre Ferngesteuertes Boot Surfen @@ -229,13 +229,14 @@ limitations under the License. Bank Kneipe Grill + Imbiss Gebäude Bushaltestelle Straßenbahnhaltestelle Bahnhof S-Bahnhof Café - Lagerfeuer + Feuerstelle Campingplatz Zeltplatz Kirche @@ -278,6 +279,7 @@ limitations under the License. Tunnel Baum Wasserfall + Springbrunnen Wegpunkt diff --git a/MyTracks/res/values-de/strings.xml b/MyTracks/res/values-de/strings.xml index 7f4afe125..db2957635 100644 --- a/MyTracks/res/values-de/strings.xml +++ b/MyTracks/res/values-de/strings.xml @@ -26,51 +26,54 @@ limitations under the License. Alle Tracks löschen Einstellungen Höhe - Bitte wählen Sie einen Track zur Ansicht aus + Bitte wähle einen Track zur Ansicht aus Aktueller Track Letztes Segment + Sensorstatus + Aggregierte Statistiken - Es wurde kein \'Location Manager\' auf diesem Gerät gefunden. - Konnte keinen GPS-Empfänger auf diesem Gerät finden. - Beim Senden Ihres Tracks an Google Fusion Tables ist ein Fehler aufgetreten. Bitte versuchen Sie es später nochmal. - Beim Senden Ihres Tracks an Google Docs ist ein Fehler aufgetreten. Bitte versuchen Sie es später nochmal. - Aufzeichnen fehlgeschlagen. Bitte versuchen Sie es nochmal. - Anhalten unmöglich. Bitte versuchen Sie es nochmal. - Datei nicht lesbar. + 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 versuchen Sie es nochmal. + Ein Fehler ist aufgetreten. Bitte versuche es noch einmal. Track existiert nicht. - Die Datei ist zu groß (Obergrenze sind 500 KB). Operation abgebrochen. + Die Datei ist zu groß (maximal 500 KB). Operation abgebrochen. Der Wegpunkt kann nicht einfügt werden. Keine Positionsbestimmung möglich. - Warten auf ein besseres GPS-Signal. Bitte stellen Sie sicher, dass der Himmel sichtbar ist. + Warte auf ein besseres GPS-Signal. Bitte stelle sicher, dass der Himmel sichtbar ist. Ein neuer Track wird aufgezeichnet. - Die Aufzeichnung wurde beendet. - Eine neue Tabelle wurde auf google.com/fusiontables erstellt. - Ihr Track wurde auf maps.google.com gespeichert. - Ihr Track wurde in einer neuen Tabelle bei docs.google.com gespeichert. - Ihr Track wurde an docs.google.com gesendet. - Berühren Sie hier um zu den Sicherheits- und Positionseinstellungen zu gelangen. Aktiviere \'GPS-Satelliten\'. Drücken Sie dann den Zurück-Knopf um zurückzukehren. - Anmerkung: Eine ungefähre Position wird angezeigt, bestimmt mittels Funktürmen und WiFi-Netzwerken. - Ihre Position kann derzeit nicht bestimmt werden. Aktivieren Sie mehr Positionsquellen (siehe Sicherheits- und Positionseinstellungen) und stellen Sie sicher, dass der Himmel sichtbar ist. + Die Aufzeichnung wurde angehalten. + Eine neue Karte 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. + Manchmal dauert es ein paar Minuten, bis neue Tracks bei maps.google.com erscheinen. + 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… - Ihr GPS-Track wird an Google gesendet. - Lese Track - Bereite Track vor - Anmelden bei Google Fusion Tables - Anmelden bei Google Docs - Track an Google Docs senden - Track an Google Fusion Tables senden - Neue \'Fusion Tabelle\' erstellen - Importiere GPS-Track. Das dauert ein wenig. - Schreibe Datei auf SD-Karte. Das dauert ein wenig. - Aktualisiere Trackstatistik + Dein Track wird an Google gesendet... + Lese Track... + Bereite Track vor... + Anmeldung bei Google Maps... + 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? @@ -79,16 +82,18 @@ limitations under the License. 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)" + Aktivität + Art des Wegpunktes + (Teil %d) - "Ihr Track wird aufgezeichnet…" + Dein Track wird aufgezeichnet… Nein @@ -99,9 +104,10 @@ limitations under the License. Abbrechen Bearbeiten Sichern + Schließen - (Anfang) + (Beginn) (Ende) @@ -115,15 +121,22 @@ limitations under the License. Auf SD-Karte speichern… GPX-Datei exportieren KML-Datei exportieren + CSV-Datei exportieren + TCX-Datei exportieren An Google MyMaps senden Alle Tracks löschen - Eine Track auswählen. Lange berühren für mehr Optionen. - Alle Tracks: + + 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. @@ -151,43 +164,78 @@ limitations under the License. Einstellungen für My Tracks Anzeige - Aufzeichnen - Mit Freunden Teilen + Aufzeichnung + Mit Freunden teilen - Dauer zwischen Ansagen: - Kürzeste Zeit zwichen zwei Ansagen + In dieser Android-Version nicht verfügbar + + Dauer zwischen Ansagen + Kürzeste Zeit zwischen zwei Ansagen - Kleinste Entfernung zwischen Punkten: + 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. + 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. + Aufzeichnungsfrequenz + Kürzeste Zeit zwischen zwei Positionsaufzeichnungen - Aufzeichnung pausieren wenn Genauigkeit kleiner als: - Die Aufzeichnung wird kurzfristig angehalten, wenn Ihre Position nicht genauer als hier angegeben ermittelt werden kann. + 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 britischen Längeneinheiten 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: + Geschwindigkeitsanzeige Geschwindigkeit wird angezeigt. - Gangart 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. + Beim Teilen mit Freunden wird nur die Karten-URL gesendet. + Beim Teilen mit Freunden wird der GPS-Track selbst gesendet. - Zerteilungs Frequenz - Längste Dauer oder Entfernung nach der automatisch ein Marker eingefügt wird. - Tempo + 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 + Sensor-Id + Die zum Sensor zu sendende Id + Die Id muss eine Ganzzahl sein. + 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 @@ -212,11 +260,11 @@ limitations under the License. Durchschnitt in Bew. - Durchschnitt in Bew. + Pace in Bew. Durchschnittsgeschw. - Durchschnittstempo + Durchschnitts-Pace Höhenprofil Höhengewinn @@ -227,7 +275,7 @@ limitations under the License. Niedrigste Höhenlage Minimale Steigung - Schnellstes Tempo + Minimaler Pace Höchste Höhenlage @@ -237,14 +285,14 @@ limitations under the License. Lade Track… - Diagram Einstellungen + Diagramm-Einstellungen - X Achse + X-Achse Nach Entfernung Nach Zeit - Y Achse + Y-Achse @@ -265,7 +313,7 @@ limitations under the License. Kilometer pro Stunde pro Kilometer Meilen pro Stunde - pro meile + pro Meile Zeit in Bewegung @@ -273,10 +321,14 @@ limitations under the License. Zeit SD-Karte nicht verfügbar Geschwindigkeit - Sie müssen erst eine Strecke zurücklegen. + Du musst erst eine Aufzeichnung beginnen. Statistik Gesamtentfernung Entfernung + Energie + Kadenz + Herzfrequenz + Höchste Herzfrequenz Bitte warten… @@ -297,22 +349,24 @@ limitations under the License. Kein Konto vorhanden - Kein Konto gefunden um Tracks zu versenden. - Wählen Sei ein Konto + Kein Konto gefunden, um Tracks zu versenden. + Wähle ein Konto - Keine externes Speichermedium verfügbar. Stellen Sie sicher, dass eine SD-Karte vorhanden ist und nicht bereits von einem Computer darauf zugegriffen wird. + 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 - Ihre Version von %1$s funktioniert möglicherweise nicht mehr richtig und wird nicht mehr unterstützt. Bitte aktualisieren Sie %1$s. - Ihre Version von %1$s funktioniert möglicherweise nicht mehr richtig. Sie können sie jetzt aktualisieren, oder auf sie auf eigene Gefahr weiterverwenden. Einige Funktionen wie Datenaustausch mit Google-Diensten wird möglicherweise nicht mehr funktionieren, solange Sie die Software nicht aktualisieren. - Eine neue Version von %1$s ist jetzt verfügbar. Sie können Ihre aktuelle Version ohne Probleme weiterverwenden, aber um die neuesten Funktionen in Anspruch nehmen zu können, müssen sie die Software 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 @@ -329,4 +383,40 @@ limitations under the License. $6 : Speed units (mph) : Pace units (per mile) --> 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? + + + Bluetootheinstellungen + Geräteverbindung + Öffnet Androids Bluetootheinstellungen, um Geräte zu verbinden. + Sensoreinstellungen + + + Verbunden mit %s + Verbinde mit Bluetoothsensor ... + Bluetoothsensor nicht verbunden. + + + Sensor Data Time + Energiemessung 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-pl/strings.xml b/MyTracks/res/values-pl/strings.xml index 762a867cb..8a639cbdf 100644 --- a/MyTracks/res/values-pl/strings.xml +++ b/MyTracks/res/values-pl/strings.xml @@ -343,6 +343,8 @@ limitations under the License. Wersja: Wsparcie Wsparcie: + + Bartłomiej Niechwiej Wyślij do... diff --git a/MyTracks/res/values/arrays.xml b/MyTracks/res/values/arrays.xml index 3eb41e8fe..964befc7d 100644 --- a/MyTracks/res/values/arrays.xml +++ b/MyTracks/res/values/arrays.xml @@ -393,12 +393,16 @@ limitations under the License. None + ANT+ Sensors + SRM ANT+ Bridge Zephyr Bluetooth @string/none + @string/ant_sensor_type + @string/srm_ant_bridge_sensor_type @string/zephyr_sensor_type diff --git a/MyTracks/res/values/colors.xml b/MyTracks/res/values/colors.xml index 3ce80b119..049bd4e4e 100644 --- a/MyTracks/res/values/colors.xml +++ b/MyTracks/res/values/colors.xml @@ -17,20 +17,34 @@ limitations under the License. #FF000000 #FF0000FF - #400000FF #FF444444 #FFffd700 #FFAAAAAA - #FF00AA00 #FF00FFFF #70000000 - #FF401465 - #40401465 #FFFF0000 - #40FF0000 #FFF0F0F0 #FF0F0F0F #FFFFFFFF - #FFF8FF00 - #40F8FF00 + + + + #FF339933 + #40B3CFB3 + + + #FF0066FF + #40C8E0FF + + + #FFCC3300 + #40E88A70 + + + #FFF128F3 + #40F128F3 + + + #FF401465 + #40401465 diff --git a/MyTracks/res/values/do_not_translate.xml b/MyTracks/res/values/do_not_translate.xml new file mode 100644 index 000000000..861738bf7 --- /dev/null +++ b/MyTracks/res/values/do_not_translate.xml @@ -0,0 +1,37 @@ + + + + + My Tracks + + + mytracks@googlegroups.com + mytracks.appspot.com + m.google.com/privacy + Copyright 2009 Google Inc. + Bartlomiej Niechwiej + Dylan Casey + Leif Hendrik Wilden + Rodrigo Damazio Bovendorp + Sandor Dornbush + + + NONE + ANT + SRM_ANT_BRIDGE + ZEPHYR + diff --git a/MyTracks/res/values/keys.xml b/MyTracks/res/values/keys.xml index ebf7cdf07..b8e43f668 100644 --- a/MyTracks/res/values/keys.xml +++ b/MyTracks/res/values/keys.xml @@ -17,6 +17,9 @@ limitations under the License. announcementFrequency + antHeartRateSensorId + antPowerSensorId + antSRMBridgeSensorId autoResumeTrackTimeout bluetoothDevice bluetoothPairing diff --git a/MyTracks/res/values/strings.xml b/MyTracks/res/values/strings.xml index 3665c56b5..b5565dc76 100644 --- a/MyTracks/res/values/strings.xml +++ b/MyTracks/res/values/strings.xml @@ -15,9 +15,6 @@ See the License for the specific language governing permissions and limitations under the License. --> - - My Tracks - Record track Stop recording @@ -105,6 +102,7 @@ limitations under the License. Error Success Waypoint + License Cancel Close Edit @@ -218,15 +216,15 @@ limitations under the License. Only supported in Android 2.0 or above Sensor Type - The type of sensor to connect to. - Sensor Id - The Id sent to sensors. - The Id must be an integer value. + The type of sensor to connect to + Clear ANT heart rate sensor pairing + Clear ANT power sensor pairing + Clear ANT srm bridge sensor pairing + ANT sensor not paired + ANT sensor paired with: %d Bluetooth sensor - The bluetooth sensor to connect to. + The bluetooth sensor to connect to Modular Stats Display - The stats screen is split in various modes. - Display all stats on one screen Backup (BETA) SD Card Backup @@ -363,17 +361,8 @@ limitations under the License. Help Team: Version: - Support Support: - - - mytracks@googlegroups.com - mytracks.appspot.com - Copyright 2009 Google Inc. - Dylan Casey - Leif Hendrik Wilden - Rodrigo Damazio Bovendorp - Sandor Dornbush + Privacy: Send to... @@ -433,10 +422,6 @@ limitations under the License. bluetoothSensorsEnabled Sensor Options - - NONE - ZEPHYR - Connected to %s Bluetooth sensor connecting ... diff --git a/MyTracks/res/values/urls.xml b/MyTracks/res/values/urls.xml new file mode 100644 index 000000000..14ab85638 --- /dev/null +++ b/MyTracks/res/values/urls.xml @@ -0,0 +1,21 @@ + + + + + http://maps.google.com/mapfiles/ms/micons/blue-pushpin.png + http://maps.google.com/mapfiles/ms/micons/ylw-pushpin.png + \ No newline at end of file diff --git a/MyTracks/res/xml/preferences.xml b/MyTracks/res/xml/preferences.xml index c0b519386..7e9ec97c9 100644 --- a/MyTracks/res/xml/preferences.xml +++ b/MyTracks/res/xml/preferences.xml @@ -122,6 +122,18 @@ limitations under the License. android:summary="@string/bluetooth_pairing_summary" android:persistent="false" /> + + + + + + diff --git a/MyTracks/src/com/dsi/ant/AntDefine.java b/MyTracks/src/com/dsi/ant/AntDefine.java new file mode 100644 index 000000000..4aabb7eb2 --- /dev/null +++ b/MyTracks/src/com/dsi/ant/AntDefine.java @@ -0,0 +1,276 @@ +/* + * Copyright 2010 Dynastream Innovations 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.dsi.ant; + + + +/** + * The Android Ant API is not finalized, and *will* change. Use at your + * own risk. + * + * Public API for controlling the Ant Service. + * AntDefines contains definitions commonly used in ANT messaging. + * + * @hide + */ +public class AntDefine { + + ////////////////////////////////////////////// + // Library Version Information + ////////////////////////////////////////////// + public static final int ANT_LIBRARY_VERSION_CODE = 5; + public static final int ANT_LIBRARY_VERSION_MAJOR = 1; + public static final int ANT_LIBRARY_VERSION_MINOR = 4; + public static final String ANT_LIBRARY_VERSION_NAME = String.valueOf(ANT_LIBRARY_VERSION_MAJOR) + "." + String.valueOf(ANT_LIBRARY_VERSION_MINOR); + + ////////////////////////////////////////////// + // Valid Configuration Values + ////////////////////////////////////////////// + public static final byte MIN_BIN = 0; + public static final byte MAX_BIN = 10; + + public static final short MIN_DEVICE_ID = 0; + public static final short MAX_DEVICE_ID = (short)65535; + + public static final short MIN_BUFFER_THRESHOLD = 0; + public static final short MAX_BUFFER_THRESHOLD = 996; + + ////////////////////////////////////////////// + // ANT Message Payload Size + ////////////////////////////////////////////// + public static final byte ANT_STANDARD_DATA_PAYLOAD_SIZE =((byte)8); + + ////////////////////////////////////////////// + // ANT LIBRARY Extended Data Message Fields + // NOTE: You must check the extended message + // bitfield first to find out which fields + // are present before accessing them! + ////////////////////////////////////////////// + public static final byte ANT_EXT_MESG_DEVICE_ID_FIELD_SIZE =((byte)4); + //public static final byte ANT_EXT_STRING_SIZE =((byte)19); // this is the additional buffer space required required for setting USB Descriptor strings + public static final byte ANT_EXT_STRING_SIZE =((byte)0); // changed to 0 as we will not be dealing with ANT USB parts. + + ////////////////////////////////////////////// + // ANT Extended Data Message Bifield Definitions + ////////////////////////////////////////////// + public static final byte ANT_EXT_MESG_BITFIELD_DEVICE_ID =((byte)0x80); // first field after bitfield + public static final byte ANT_EXT_MESG_BITFIELD_RSSI =((byte)0x40); // next field after ID, if there is one + public static final byte ANT_EXT_MESG_BITFIELD_TIME_STAMP =((byte)0x20); // next field after RSSI, if there is one + + // 4 bits free reserved set to 0 + public static final byte ANT_EXT_MESG_BIFIELD_EXTENSION =((byte)0x01); + + // extended message input bitfield defines + public static final byte ANT_EXT_MESG_BITFIELD_OVERWRITE_SHARED_ADR =((byte)0x10); + public static final byte ANT_EXT_MESG_BITFIELD_TRANSMISSION_TYPE =((byte)0x08); + + ////////////////////////////////////////////// + // ID Definitions + ////////////////////////////////////////////// + public static final byte ANT_ID_SIZE =((byte)4); + public static final byte ANT_ID_TRANS_TYPE_OFFSET =((byte)3); + public static final byte ANT_ID_DEVICE_TYPE_OFFSET =((byte)2); + public static final byte ANT_ID_DEVICE_NUMBER_HIGH_OFFSET =((byte)1); + public static final byte ANT_ID_DEVICE_NUMBER_LOW_OFFSET =((byte)0); + public static final byte ANT_ID_DEVICE_TYPE_PAIRING_FLAG =((byte)0x80); + + public static final byte ANT_TRANS_TYPE_SHARED_ADDR_MASK =((byte)0x03); + public static final byte ANT_TRANS_TYPE_1_BYTE_SHARED_ADDRESS =((byte)0x02); + public static final byte ANT_TRANS_TYPE_2_BYTE_SHARED_ADDRESS =((byte)0x03); + + ////////////////////////////////////////////// + // Assign Channel Parameters + ////////////////////////////////////////////// + public static final byte PARAMETER_RX_NOT_TX =((byte)0x00); + public static final byte PARAMETER_TX_NOT_RX =((byte)0x10); + public static final byte PARAMETER_SHARED_CHANNEL =((byte)0x20); + public static final byte PARAMETER_NO_TX_GUARD_BAND =((byte)0x40); + public static final byte PARAMETER_ALWAYS_RX_WILD_CARD_SEARCH_ID =((byte)0x40); //Pre-AP2 + public static final byte PARAMETER_RX_ONLY =((byte)0x40); + + ////////////////////////////////////////////// + // Ext. Assign Channel Parameters + ////////////////////////////////////////////// + public static final byte EXT_PARAM_ALWAYS_SEARCH =((byte)0x01); + public static final byte EXT_PARAM_FREQUENCY_AGILITY =((byte)0x04); + + ////////////////////////////////////////////// + // Radio TX Power Definitions + ////////////////////////////////////////////// + public static final byte RADIO_TX_POWER_LVL_MASK =((byte)0x03); + + public static final byte RADIO_TX_POWER_LVL_0 =((byte)0x00); //(formerly: RADIO_TX_POWER_MINUS20DB); lowest + public static final byte RADIO_TX_POWER_LVL_1 =((byte)0x01); //(formerly: RADIO_TX_POWER_MINUS10DB); + public static final byte RADIO_TX_POWER_LVL_2 =((byte)0x02); //(formerly: RADIO_TX_POWER_MINUS5DB); + public static final byte RADIO_TX_POWER_LVL_3 =((byte)0x03); //(formerly: RADIO_TX_POWER_0DB); highest + + ////////////////////////////////////////////// + // Channel Status + ////////////////////////////////////////////// + public static final byte STATUS_CHANNEL_STATE_MASK =((byte)0x03); + public static final byte STATUS_UNASSIGNED_CHANNEL =((byte)0x00); + public static final byte STATUS_ASSIGNED_CHANNEL =((byte)0x01); + public static final byte STATUS_SEARCHING_CHANNEL =((byte)0x02); + public static final byte STATUS_TRACKING_CHANNEL =((byte)0x03); + + ////////////////////////////////////////////// + // Standard capabilities defines + ////////////////////////////////////////////// + public static final byte CAPABILITIES_NO_RX_CHANNELS =((byte)0x01); + public static final byte CAPABILITIES_NO_TX_CHANNELS =((byte)0x02); + public static final byte CAPABILITIES_NO_RX_MESSAGES =((byte)0x04); + public static final byte CAPABILITIES_NO_TX_MESSAGES =((byte)0x08); + public static final byte CAPABILITIES_NO_ACKD_MESSAGES =((byte)0x10); + public static final byte CAPABILITIES_NO_BURST_TRANSFER =((byte)0x20); + + ////////////////////////////////////////////// + // Advanced capabilities defines + ////////////////////////////////////////////// + public static final byte CAPABILITIES_OVERUN_UNDERRUN =((byte)0x01); // Support for this functionality has been dropped + public static final byte CAPABILITIES_NETWORK_ENABLED =((byte)0x02); + public static final byte CAPABILITIES_AP1_VERSION_2 =((byte)0x04); // This Version of the AP1 does not support transmit and only had a limited release + public static final byte CAPABILITIES_SERIAL_NUMBER_ENABLED =((byte)0x08); + public static final byte CAPABILITIES_PER_CHANNEL_TX_POWER_ENABLED =((byte)0x10); + public static final byte CAPABILITIES_LOW_PRIORITY_SEARCH_ENABLED =((byte)0x20); + public static final byte CAPABILITIES_SCRIPT_ENABLED =((byte)0x40); + public static final byte CAPABILITIES_SEARCH_LIST_ENABLED =((byte)0x80); + + ////////////////////////////////////////////// + // Advanced capabilities 2 defines + ////////////////////////////////////////////// + public static final byte CAPABILITIES_LED_ENABLED =((byte)0x01); + public static final byte CAPABILITIES_EXT_MESSAGE_ENABLED =((byte)0x02); + public static final byte CAPABILITIES_SCAN_MODE_ENABLED =((byte)0x04); + public static final byte CAPABILITIES_RESERVED =((byte)0x08); + public static final byte CAPABILITIES_PROX_SEARCH_ENABLED =((byte)0x10); + public static final byte CAPABILITIES_EXT_ASSIGN_ENABLED =((byte)0x20); + public static final byte CAPABILITIES_FREE_1 =((byte)0x40); + public static final byte CAPABILITIES_FIT1_ENABLED =((byte)0x80); + + ////////////////////////////////////////////// + // Advanced capabilities 3 defines + ////////////////////////////////////////////// + public static final byte CAPABILITIES_SENSRCORE_ENABLED =((byte)0x01); + public static final byte CAPABILITIES_RESERVED_1 =((byte)0x02); + public static final byte CAPABILITIES_RESERVED_2 =((byte)0x04); + public static final byte CAPABILITIES_RESERVED_3 =((byte)0x08); + + + ////////////////////////////////////////////// + // Burst Message Sequence + ////////////////////////////////////////////// + public static final byte CHANNEL_NUMBER_MASK =((byte)0x1F); + public static final byte SEQUENCE_NUMBER_MASK =((byte)0xE0); + public static final byte SEQUENCE_NUMBER_ROLLOVER =((byte)0x60); + public static final byte SEQUENCE_FIRST_MESSAGE =((byte)0x00); + public static final byte SEQUENCE_LAST_MESSAGE =((byte)0x80); + public static final byte SEQUENCE_NUMBER_INC =((byte)0x20); + + ////////////////////////////////////////////// + // Control Message Flags + ////////////////////////////////////////////// + public static final byte BROADCAST_CONTROL_BYTE =((byte)0x00); + public static final byte ACKNOWLEDGED_CONTROL_BYTE =((byte)0xA0); + + ////////////////////////////////////////////// + // Response / Event Codes + ////////////////////////////////////////////// + public static final byte RESPONSE_NO_ERROR =((byte)0x00); + public static final byte NO_EVENT =((byte)0x00); + + public static final byte EVENT_RX_SEARCH_TIMEOUT =((byte)0x01); + public static final byte EVENT_RX_FAIL =((byte)0x02); + public static final byte EVENT_TX =((byte)0x03); + public static final byte EVENT_TRANSFER_RX_FAILED =((byte)0x04); + public static final byte EVENT_TRANSFER_TX_COMPLETED =((byte)0x05); + public static final byte EVENT_TRANSFER_TX_FAILED =((byte)0x06); + public static final byte EVENT_CHANNEL_CLOSED =((byte)0x07); + public static final byte EVENT_RX_FAIL_GO_TO_SEARCH =((byte)0x08); + public static final byte EVENT_CHANNEL_COLLISION =((byte)0x09); + public static final byte EVENT_TRANSFER_TX_START =((byte)0x0A); // a pending transmit transfer has begun + + public static final byte EVENT_CHANNEL_ACTIVE =((byte)0x0F); + + public static final byte EVENT_TRANSFER_TX_NEXT_MESSAGE =((byte)0x11); // only enabled in FIT1 + + public static final byte CHANNEL_IN_WRONG_STATE =((byte)0x15); // returned on attempt to perform an action from the wrong channel state + public static final byte CHANNEL_NOT_OPENED =((byte)0x16); // returned on attempt to communicate on a channel that is not open + public static final byte CHANNEL_ID_NOT_SET =((byte)0x18); // returned on attempt to open a channel without setting the channel ID + public static final byte CLOSE_ALL_CHANNELS =((byte)0x19); // returned when attempting to start scanning mode, when channels are still open + + public static final byte TRANSFER_IN_PROGRESS =((byte)0x1F); // returned on attempt to communicate on a channel with a TX transfer in progress + public static final byte TRANSFER_SEQUENCE_NUMBER_ERROR =((byte)0x20); // returned when sequence number is out of order on a Burst transfer + public static final byte TRANSFER_IN_ERROR =((byte)0x21); + public static final byte TRANSFER_BUSY =((byte)0x22); + + public static final byte INVALID_MESSAGE_CRC =((byte)0x26); // returned if there is a framing error on an incomming message + public static final byte MESSAGE_SIZE_EXCEEDS_LIMIT =((byte)0x27); // returned if a data message is provided that is too large + public static final byte INVALID_MESSAGE =((byte)0x28); // returned when the message has an invalid parameter + public static final byte INVALID_NETWORK_NUMBER =((byte)0x29); // returned when an invalid network number is provided + public static final byte INVALID_LIST_ID =((byte)0x30); // returned when the provided list ID or size exceeds the limit + public static final byte INVALID_SCAN_TX_CHANNEL =((byte)0x31); // returned when attempting to transmit on channel 0 when in scan mode + + public static final byte INVALID_PARAMETER_PROVIDED =((byte)0x33); // returned when an invalid parameter is specified in a configuration message + + public static final byte EVENT_SERIAL_QUE_OVERFLOW =((byte)0x34); + public static final byte EVENT_QUE_OVERFLOW =((byte)0x35); // ANT event que has overflowed and lost 1 or more events + + public static final byte EVENT_CLK_ERROR =((byte)0x36); // debug event for XOSC16M on LE1 + + public static final byte SCRIPT_FULL_ERROR =((byte)0x40); // error writing to script, memory is full + public static final byte SCRIPT_WRITE_ERROR =((byte)0x41); // error writing to script, bytes not written correctly + public static final byte SCRIPT_INVALID_PAGE_ERROR =((byte)0x42); // error accessing script page + public static final byte SCRIPT_LOCKED_ERROR =((byte)0x43); // the scripts are locked and can't be dumped + + public static final byte NO_RESPONSE_MESSAGE =((byte)0x50); // returned to the Command_SerialMessageProcess function, so no reply message is generated + public static final byte RETURN_TO_MFG =((byte)0x51); // default return to any mesg when the module determines that the mfg procedure has not been fully completed + + public static final byte FIT_ACTIVE_SEARCH_TIMEOUT =((byte)0x60); // Fit1 only event added for timeout of the pairing state after the Fit module becomes active + public static final byte FIT_WATCH_PAIR =((byte)0x61); // Fit1 only + public static final byte FIT_WATCH_UNPAIR =((byte)0x62); // Fit1 only + + public static final byte USB_STRING_WRITE_FAIL =((byte)0x70); + + // Internal only events below this point + public static final byte INTERNAL_ONLY_EVENTS =((byte)0x80); + public static final byte EVENT_RX =((byte)0x80); // INTERNAL: Event for a receive message + public static final byte EVENT_NEW_CHANNEL =((byte)0x81); // INTERNAL: EVENT for a new active channel + public static final byte EVENT_PASS_THRU =((byte)0x82); // INTERNAL: Event to allow an upper stack events to pass through lower stacks + + public static final byte EVENT_BLOCKED =((byte)0xFF); // INTERNAL: Event to replace any event we do not wish to go out, will also zero the size of the Tx message + + /////////////////////////////////////////////////////// + // Script Command Codes + /////////////////////////////////////////////////////// + public static final byte SCRIPT_CMD_FORMAT =((byte)0x00); + public static final byte SCRIPT_CMD_DUMP =((byte)0x01); + public static final byte SCRIPT_CMD_SET_DEFAULT_SECTOR =((byte)0x02); + public static final byte SCRIPT_CMD_END_SECTOR =((byte)0x03); + public static final byte SCRIPT_CMD_END_DUMP =((byte)0x04); + public static final byte SCRIPT_CMD_LOCK =((byte)0x05); + + /////////////////////////////////////////////////////// + // Reset Mesg Codes + /////////////////////////////////////////////////////// + public static final byte RESET_FLAGS_MASK =((byte)0xE0); + public static final byte RESET_SUSPEND =((byte)0x80); // this must follow bitfield def + public static final byte RESET_SYNC =((byte)0x40); // this must follow bitfield def + public static final byte RESET_CMD =((byte)0x20); // this must follow bitfield def + public static final byte RESET_WDT =((byte)0x02); + public static final byte RESET_RST =((byte)0x01); + public static final byte RESET_POR =((byte)0x00); + +} \ No newline at end of file diff --git a/MyTracks/src/com/dsi/ant/AntInterface.java b/MyTracks/src/com/dsi/ant/AntInterface.java new file mode 100644 index 000000000..20d30349a --- /dev/null +++ b/MyTracks/src/com/dsi/ant/AntInterface.java @@ -0,0 +1,1134 @@ +/* + * Copyright 2010 Dynastream Innovations 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.dsi.ant; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.net.Uri; +import android.os.IBinder; +import android.os.RemoteException; +import android.util.Log; + + +// TODO: Auto-generated Javadoc + +/** + * Public API for controlling the Ant Service. AntInterface is a proxy + * object for controlling the Ant Service via IPC. Creating a AntInterface + * object will create a binding with the Ant service. + * + * @hide + */ +public class AntInterface { + + /** The Log Tag. */ + public static final String TAG = "ANTInterface"; + + /** Enable debug logging. */ + 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"; + + /** Inter-process communication with the ANT Radio Proxy Service. */ + public static IAnt sAntReceiver = null; + public static IServiceSettings sServiceSettingsReceiver = null; + + /** Singleton instance of this class. */ + private static AntInterface INSTANCE; + + /** Used when obtaining a reference to the singleton instance. */ + private static Object INSTANCE_LOCK = new Object(); + + /** The context to use. */ + private static Context sContext = null; + + /** Listens to changes to service connection status. */ + private static ServiceListener sServiceListener; + + /** Is the ANT Radio Proxy Service connected. */ + private static boolean sServiceConnected = false; + private static boolean sServiceSettingsConnected = false; + + private static int mServiceLibraryVersionCode = 0; + + /** + * An interface for notifying AntInterface IPC clients when they have + * been connected to the ANT service. + * + * @see ServiceEvent + */ + public interface ServiceListener + { + /** + * Called to notify the client when this proxy object has been + * connected to the ANT service. Clients must wait for + * this callback before making IPC calls on the ANT + * service. + */ + public void onServiceConnected(); + + /** + * Called to notify the client that this proxy object has been + * disconnected from the ANT service. Clients must not + * make IPC calls on the ANT service after this callback. + * This callback will currently only occur if the application hosting + * the BluetoothAg service, but may be called more often in future. + */ + public void onServiceDisconnected(); + } + + + //Constructor + /** + * Instantiates a new ant interface. + * + * @param context the context + * @param listener the listener + * @since 1.0 + */ + private AntInterface(Context context, ServiceListener listener) + { + // This will be around as long as this process is + sContext = context; + sServiceListener = listener; + } + + /** + * Gets the single instance of AntInterface, creating it if it doesn't exist. + * + * @param context the context. + * @param listener the listener to be notified of status changes. + * @return the AntInterface instance. + * @since 1.0 + */ + public static AntInterface getInstance(Context context,ServiceListener listener) + { + if(DEBUG) Log.d(TAG, "getInstance"); + + synchronized (INSTANCE_LOCK) + { + if (INSTANCE == null) + { + if(DEBUG) Log.d(TAG, "getInstance: Creating new instance"); + + // TODO: rohan: bug, each new request for an instance will not have context and listener set to the requested objects + INSTANCE = new AntInterface(context,listener); + } + else + { + if(DEBUG) Log.d(TAG, "getInstance: Using existing instance"); + } + + if(!sServiceConnected) + { + if(DEBUG) Log.d(TAG, "getInstance: No connection to proxy service, attempting connection"); + + if(!INSTANCE.initService()) + { + Log.e(TAG, "getInstance: No connection to proxy service"); + + INSTANCE.destroy(); + INSTANCE = null; + } + } + + return INSTANCE; + } + } + + /** + * Go to market. + * + * @param pContext the context + * @param pSearchText the search text + * @since 1.2 + */ + public static void goToMarket(Context pContext, String pSearchText) + { + if(null == pSearchText) + { + goToMarket(pContext); + } + else + { + if(DEBUG) Log.i(TAG, "goToMarket: Search text = "+ pSearchText); + + Intent goToMarket = null; + goToMarket = new Intent(Intent.ACTION_VIEW,Uri.parse("http://market.android.com/search?q=" + pSearchText)); + goToMarket.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + pContext.startActivity(goToMarket); + } + } + + /** + * Go to market. + * + * @param pContext the context + * @since 1.2 + */ + public static void goToMarket(Context pContext) + { + if(DEBUG) Log.d(TAG, "goToMarket"); + + goToMarket(pContext, MARKET_SEARCH_TEXT_DEFAULT); + } + + /** + * Class for interacting with the ANT interface. + */ + private static ServiceConnection sIAntConnection = new ServiceConnection() { + public void onServiceConnected(ComponentName pClassName, IBinder pService) { + // This is called when the connection with the service has been + // established, giving us the service object we can use to + // interact with the service. We are communicating with our + // service through an IDL interface, so get a client-side + // representation of that from the raw service object. + if(DEBUG) Log.d(TAG, "sIAntConnection onServiceConnected()"); + sAntReceiver = IAnt.Stub.asInterface(pService); + + sServiceConnected = true; + + try + { + mServiceLibraryVersionCode = sAntReceiver.getServiceLibraryVersionCode(); + } + catch(RemoteException e) + { + Log.e(TAG, "sIAntConnection onServiceConnected: Could not get service library version"); + } + + switch(mServiceLibraryVersionCode) + { + // Fall through from newer versions + case 5: + { + if(!sServiceSettingsConnected) + { + boolean boundServiceSettings = sContext.bindService(new Intent(IServiceSettings.class.getName()), sIServiceSettingsConnection, Context.BIND_AUTO_CREATE); + Log.i(TAG, "sIAntConnection onServiceConnected: Bound with ANT Service Settings: " + boundServiceSettings); + } + else + { + if(DEBUG) Log.d(TAG, "sIAntConnection onServiceConnected: Already initialised Service Settings connection"); + } + } + } + + // Notify the attached application if it is registered + if (sServiceListener != null) + { + sServiceListener.onServiceConnected(); + } + else + { + if(DEBUG) Log.d(TAG, "sIAntConnection onServiceConnected: No service listener registered"); + } + } + + public void onServiceDisconnected(ComponentName pClassName) { + // This is called when the connection with the service has been + // unexpectedly disconnected -- that is, its process crashed. + if(DEBUG) Log.d(TAG, "sIAntConnection onServiceDisconnected()"); + sAntReceiver = null; + + sServiceConnected = false; + + // Notify the attached application if it is registered + if (sServiceListener != null) + { + sServiceListener.onServiceDisconnected(); + } + else + { + if(DEBUG) Log.d(TAG, "sIAntConnection onServiceDisconnected: No service listener registered"); + } + + // Try and rebind to the service + INSTANCE.releaseService(); + INSTANCE.initService(); + } + }; + + /** + * Class for interacting with the ANT interface through the ANTLib version 5 functions. + */ + private static ServiceConnection sIServiceSettingsConnection = new ServiceConnection() { + public void onServiceConnected(ComponentName pClassName, IBinder pService) { + // This is called when the connection with the service has been + // established, giving us the service object we can use to + // interact with the service. We are communicating with our + // service through an IDL interface, so get a client-side + // representation of that from the raw service object. + if(DEBUG) Log.d(TAG, "sIServiceSettingsConnection onServiceConnected()"); + sServiceSettingsReceiver = IServiceSettings.Stub.asInterface(pService); + + sServiceSettingsConnected = true; + } + + public void onServiceDisconnected(ComponentName pClassName) { + // This is called when the connection with the service has been + // unexpectedly disconnected -- that is, its process crashed. + if(DEBUG) Log.d(TAG, "sIServiceSettingsConnection onServiceDisconnected()"); + sServiceSettingsReceiver = null; + + sServiceSettingsConnected = false; + + // Try and rebind to the service + INSTANCE.releaseService(); + INSTANCE.initService(); + } + }; + + + /** + * Binds this activity to the ANT service. + * + * @return true, if successful + */ + private boolean initService() { + if(DEBUG) Log.d(TAG, "initService() entered"); + + boolean ret = false; + + if(!sServiceConnected) + { + ret = sContext.bindService(new Intent(IAnt.class.getName()), sIAntConnection, Context.BIND_AUTO_CREATE); + Log.i(TAG, "initService(): Bound with ANT service: " + ret); + } + else + { + if(DEBUG) Log.d(TAG, "initService: already initialised service"); + ret = true; + } + + return ret; + } + + /** Unbinds this activity from the ANT service. */ + private void releaseService() { + if(DEBUG) Log.d(TAG, "releaseService() entered"); + + if(sServiceConnected) + { + sContext.unbindService(sIAntConnection); + sServiceConnected = false; + } + + if(sServiceSettingsConnected) + { + sContext.unbindService(sIServiceSettingsConnection); + sServiceSettingsConnected = false; + } + + if(DEBUG) Log.d(TAG, "releaseService() unbound."); + } + + /** + * True if this activity can communicate with the ANT service. + * + * @return true, if is service connected + * @since 1.2 + */ + public boolean isServiceConnected() + { + return sServiceConnected; + } + + /** + * Destroy. + * + * @return true, if successful + * @since 1.0 + */ + public boolean destroy() + { + if(DEBUG) Log.d(TAG, "destroy"); + + releaseService(); + + INSTANCE = null; + + return true; + } + + + /** + * Ant service connection lost. + */ + private void antServiceConnectionLost() + { + Log.e(TAG, "Connection to ANT service lost"); + } + + /** + * Ant service connection to settings binder lost. + */ + private void antServiceSettingsConnectionLost() + { + Log.e(TAG, "Connection to ANT service settings lost"); + } + + + ////------------------------------------------------- + + /** + * Enable. + * + * @return true, if successful + * @since 1.0 + */ + public boolean enable() + { + if(DEBUG) Log.d(TAG, "enable"); + + if(!sServiceConnected) + { + // Haven't received 'onConnected' notification yet + return false; + } + + boolean result = false; + try { + result = sAntReceiver.enable(); + } catch (RemoteException ex) { + antServiceConnectionLost(); + } + return result; + } + + /** + * Disable. + * + * @return true, if successful + * @since 1.0 + */ + public boolean disable() + { + if(DEBUG) Log.d(TAG, "disable"); + + if(!sServiceConnected) + { + return false; + } + + boolean result = false; + try { + result = sAntReceiver.disable(); + } catch (RemoteException ex) { + antServiceConnectionLost(); + } + return result; + } + + /** + * Checks if is enabled. + * + * @return true, if is enabled + * @since 1.0 + */ + public boolean isEnabled() + { + if(DEBUG) Log.d(TAG, "isEnabled"); + + if(!sServiceConnected) + { + return false; + } + + boolean result = false; + try { + result = sAntReceiver.isEnabled(); + } catch (RemoteException ex) { + antServiceConnectionLost(); + } + return result; + } + + /** + * ANT tx message. + * + * @param message the message + * @return true, if successful + * @since 1.0 + */ + public boolean ANTTxMessage(byte[] message) + { + if(DEBUG) Log.d(TAG, "ANTTxMessage"); + + if(!sServiceConnected) + { + return false; + } + + boolean result = false; + try { + result = sAntReceiver.ANTTxMessage(message); + } catch (RemoteException ex) { + antServiceConnectionLost(); + } + return result; + } + + /** + * ANT reset system. + * + * @return true, if successful + * @since 1.0 + */ + public boolean ANTResetSystem() + { + if(!sServiceConnected) + { + return false; + } + + boolean result = false; + try { + result = sAntReceiver.ANTResetSystem(); + } catch (RemoteException ex) { + antServiceConnectionLost(); + } + return result; + } + + /** + * ANT unassign channel. + * + * @param channelNumber the channel number + * @return true, if successful + * @since 1.0 + */ + public boolean ANTUnassignChannel(byte channelNumber) + { + if(!sServiceConnected) + { + return false; + } + + boolean result = false; + try { + result = sAntReceiver.ANTUnassignChannel(channelNumber); + } catch (RemoteException ex) { + antServiceConnectionLost(); + } + return result; + } + + /** + * ANT assign channel. + * + * @param channelNumber the channel number + * @param channelType the channel type + * @param networkNumber the network number + * @return true, if successful + * @since 1.0 + */ + public boolean ANTAssignChannel(byte channelNumber, byte channelType, byte networkNumber) + { + if(!sServiceConnected) + { + return false; + } + + boolean result = false; + try { + result = sAntReceiver.ANTAssignChannel(channelNumber, channelType, networkNumber); + } catch (RemoteException ex) { + antServiceConnectionLost(); + } + return result; + } + + /** + * ANT set channel id. + * + * @param channelNumber the channel number + * @param deviceNumber the device number + * @param deviceType the device type + * @param txType the tx type + * @return true, if successful + * @since 1.0 + */ + public boolean ANTSetChannelId(byte channelNumber, short deviceNumber, byte deviceType, byte txType) + { + if(!sServiceConnected) + { + return false; + } + + boolean result = false; + try { + result = sAntReceiver.ANTSetChannelId(channelNumber, deviceNumber, deviceType, txType); + } catch (RemoteException ex) { + antServiceConnectionLost(); + } + return result; + } + + /** + * ANT set channel period. + * + * @param channelNumber the channel number + * @param channelPeriod the channel period + * @return true, if successful + * @since 1.0 + */ + public boolean ANTSetChannelPeriod(byte channelNumber, short channelPeriod) + { + if(!sServiceConnected) + { + return false; + } + + boolean result = false; + try { + result = sAntReceiver.ANTSetChannelPeriod(channelNumber, channelPeriod); + } catch (RemoteException ex) { + antServiceConnectionLost(); + } + return result; + } + + /** + * ANT set channel rf freq. + * + * @param channelNumber the channel number + * @param radioFrequency the radio frequency + * @return true, if successful + * @since 1.0 + */ + public boolean ANTSetChannelRFFreq(byte channelNumber, byte radioFrequency) + { + if(!sServiceConnected) + { + return false; + } + + boolean result = false; + try { + result = sAntReceiver.ANTSetChannelRFFreq(channelNumber, radioFrequency); + } catch (RemoteException ex) { + antServiceConnectionLost(); + } + return result; + } + + /** + * ANT set channel search timeout. + * + * @param channelNumber the channel number + * @param searchTimeout the search timeout + * @return true, if successful + * @since 1.0 + */ + public boolean ANTSetChannelSearchTimeout(byte channelNumber, byte searchTimeout) + { + if(!sServiceConnected) + { + return false; + } + + boolean result = false; + try { + result = sAntReceiver.ANTSetChannelSearchTimeout(channelNumber, searchTimeout); + } catch (RemoteException ex) { + antServiceConnectionLost(); + } + return result; + } + + /** + * ANT set low priority channel search timeout. + * + * @param channelNumber the channel number + * @param searchTimeout the search timeout + * @return true, if successful + * @since 1.0 + */ + public boolean ANTSetLowPriorityChannelSearchTimeout(byte channelNumber, byte searchTimeout) + { + if(!sServiceConnected) + { + return false; + } + + boolean result = false; + try { + result = sAntReceiver.ANTSetLowPriorityChannelSearchTimeout(channelNumber, searchTimeout); + } catch (RemoteException ex) { + antServiceConnectionLost(); + } + return result; + } + + /** + * ANT set proximity search. + * + * @param channelNumber the channel number + * @param searchThreshold the search threshold + * @return true, if successful + * @since 1.0 + */ + public boolean ANTSetProximitySearch(byte channelNumber, byte searchThreshold) + { + if(!sServiceConnected) + { + return false; + } + + boolean result = false; + try { + result = sAntReceiver.ANTSetProximitySearch(channelNumber, searchThreshold); + } catch (RemoteException ex) { + antServiceConnectionLost(); + } + return result; + } + + /** + * ANT set channel transmit power + * @param channelNumber the channel number + * @param txPower the transmit power level + * @return true, if successful + * @since 1.0 + */ + public boolean ANTSetChannelTxPower(byte channelNumber, byte txPower) + { + if(!sServiceConnected) + { + return false; + } + + boolean result = false; + try { + result = sAntReceiver.ANTSetChannelTxPower(channelNumber, txPower); + } catch (RemoteException ex) + { + antServiceConnectionLost(); + } + return result; + } + + + /** + * ANT add channel id. + * + * @param channelNumber the channel number + * @param deviceNumber the device number + * @param deviceType the device type + * @param txType the tx type + * @param listIndex the list index + * @return true, if successful + * @since 1.0 + */ + public boolean ANTAddChannelId(byte channelNumber, short deviceNumber, byte deviceType, byte txType, byte listIndex) + { + if(!sServiceConnected) + { + return false; + } + + boolean result = false; + try { + result = sAntReceiver.ANTAddChannelId(channelNumber, deviceNumber, deviceType, txType, listIndex); + } catch (RemoteException ex) { + antServiceConnectionLost(); + } + return result; + } + + /** + * ANT config list. + * + * @param channelNumber the channel number + * @param listSize the list size + * @param exclude the exclude + * @return true, if successful + * @since 1.0 + */ + public boolean ANTConfigList(byte channelNumber, byte listSize, byte exclude) + { + if(!sServiceConnected) + { + return false; + } + + boolean result = false; + try { + result = sAntReceiver.ANTConfigList(channelNumber, listSize, exclude); + } catch (RemoteException ex) { + antServiceConnectionLost(); + } + return result; + } + + /** + * ANT config event buffering. + * + * @param screenOnFlushTimerInterval the screen on flush timer interval + * @param screenOnFlushBufferThreshold the screen on flush buffer threshold + * @param screenOffFlushTimerInterval the screen off flush timer interval + * @param screenOffFlushBufferThreshold the screen off flush buffer threshold + * @return true, if successful + * @since 1.3 + */ + public boolean ANTConfigEventBuffering(short screenOnFlushTimerInterval, short screenOnFlushBufferThreshold, short screenOffFlushTimerInterval, short screenOffFlushBufferThreshold) + { + if(!sServiceConnected) + { + return false; + } + + boolean result = false; + try { + result = sAntReceiver.ANTConfigEventBuffering((int)screenOnFlushTimerInterval, (int)screenOnFlushBufferThreshold, (int)screenOffFlushTimerInterval, (int)screenOffFlushBufferThreshold); + } catch (RemoteException ex) { + antServiceConnectionLost(); + } + return result; + } + + /** + * ANT disable event buffering. + * + * @return true, if successful + * @since 1.1 + */ + public boolean ANTDisableEventBuffering() + { + if(!sServiceConnected) + { + return false; + } + + boolean result = false; + try { + result = sAntReceiver.ANTDisableEventBuffering(); + } catch (RemoteException ex) { + antServiceConnectionLost(); + } + return result; + } + + /** + * ANT open channel. + * + * @param channelNumber the channel number + * @return true, if successful + * @since 1.0 + */ + public boolean ANTOpenChannel(byte channelNumber) + { + if(!sServiceConnected) + { + return false; + } + + boolean result = false; + try { + result = sAntReceiver.ANTOpenChannel(channelNumber); + } catch (RemoteException ex) { + antServiceConnectionLost(); + } + return result; + } + + /** + * ANT close channel. + * + * @param channelNumber the channel number + * @return true, if successful + * @since 1.0 + */ + public boolean ANTCloseChannel(byte channelNumber) + { + if(!sServiceConnected) + { + return false; + } + + boolean result = false; + try { + result = sAntReceiver.ANTCloseChannel(channelNumber); + } catch (RemoteException ex) { + antServiceConnectionLost(); + } + return result; + } + + /** + * ANT request message. + * + * @param channelNumber the channel number + * @param messageID the message id + * @return true, if successful + * @since 1.0 + */ + public boolean ANTRequestMessage(byte channelNumber, byte messageID) + { + if(!sServiceConnected) + { + return false; + } + + boolean result = false; + try { + result = sAntReceiver.ANTRequestMessage(channelNumber, messageID); + } catch (RemoteException ex) { + antServiceConnectionLost(); + } + return result; + } + + /** + * ANT send broadcast data. + * + * @param channelNumber the channel number + * @param txBuffer the tx buffer + * @return true, if successful + * @since 1.0 + */ + public boolean ANTSendBroadcastData(byte channelNumber, byte[] txBuffer) + { + if(!sServiceConnected) + { + return false; + } + + boolean result = false; + try { + result = sAntReceiver.ANTSendBroadcastData(channelNumber, txBuffer); + } catch (RemoteException ex) { + antServiceConnectionLost(); + } + return result; + } + + /** + * ANT send acknowledged data. + * + * @param channelNumber the channel number + * @param txBuffer the tx buffer + * @return true, if successful + * @since 1.0 + */ + public boolean ANTSendAcknowledgedData(byte channelNumber, byte[] txBuffer) + { + if(!sServiceConnected) + { + return false; + } + + boolean result = false; + try { + result = sAntReceiver.ANTSendAcknowledgedData(channelNumber, txBuffer); + } catch (RemoteException ex) { + antServiceConnectionLost(); + } + return result; + } + + /** + * ANT send burst transfer packet. + * + * @param control the control + * @param txBuffer the tx buffer + * @return true, if successful + * @since 1.0 + */ + public boolean ANTSendBurstTransferPacket(byte control, byte[] txBuffer) + { + if(!sServiceConnected) + { + return false; + } + + boolean result = false; + try { + result = sAntReceiver.ANTSendBurstTransferPacket(control, txBuffer); + } catch (RemoteException ex) { + antServiceConnectionLost(); + } + return result; + } + + /** + * Transmits the given data on channelNumber as part of a burst message. + * + * @param channelNumber Which channel to transmit on. + * @param txBuffer The data to send. + * @param initialPacket Which packet in the burst sequence does the data begin in, 1 is the first. + * @param containsEndOfBurst Is this the last of the data to be sent in burst. + * @return The number of bytes still to be sent (approximately). 0 if success. + */ + public int ANTSendBurstTransfer(byte channelNumber, byte[] txBuffer) + { + int result = txBuffer.length; + + if(sServiceConnected) + { + try { + result = sAntReceiver.ANTSendBurstTransfer(channelNumber, txBuffer); + } catch (RemoteException ex) { + antServiceConnectionLost(); + } + } + return result; + } + + /** + * ANT send partial burst. + * + * @param channelNumber the channel number + * @param txBuffer the tx buffer + * @param initialPacket the initial packet + * @param containsEndOfBurst the contains end of burst + * @return the int + * @since 1.0 + */ + public int ANTSendPartialBurst(byte channelNumber, byte[] txBuffer, int initialPacket, boolean containsEndOfBurst) + { + int result = txBuffer.length; + + if(sServiceConnected) + { + try { + result = sAntReceiver.ANTTransmitBurst(channelNumber, txBuffer, initialPacket, containsEndOfBurst); + } catch (RemoteException ex) { + antServiceConnectionLost(); + } + } + return result; + } + + /** + * Returns the version code (eg. 1) of ANTLib used by the ANT application service + * + * @return the service library version code + * @throws RemoteException the remote exception + * @since 1.2 + */ + public int getServiceLibraryVersionCode() throws RemoteException + { + if(mServiceLibraryVersionCode == 0) + { + mServiceLibraryVersionCode = sAntReceiver.getServiceLibraryVersionCode(); + } + + return mServiceLibraryVersionCode; + } + + /** + * Returns the version name (eg "1.0") of ANTLib used by the ANT application service + * + * @return the service library version name + * @throws RemoteException the remote exception + * @since 1.2 + */ + public String getServiceLibraryVersionName() throws RemoteException + { + return sAntReceiver.getServiceLibraryVersionName(); + } + + + // + // ------------------- The below functions are for debugging/development and will likely disappear. + // + + /** + * Turn verbose logging on or off in the service. + * + * @param debug Whether debug logging should be enabled. + * @return true, if successful + * @since 1.4 + */ + public boolean debugLogging(boolean debug) + { + if(DEBUG) Log.d(TAG, "debugLogging"); + + if(!sServiceSettingsConnected) + { + // Haven't received 'onConnected' notification yet + return false; + } + + boolean result = false; + try { + sServiceSettingsReceiver.debugLogging(debug); + result = true; + } catch (RemoteException ex) { + antServiceSettingsConnectionLost(); + } + return result; + } + + /** + * Set how many ANT packets should be combined in one request to the hardware during a burst transfer. + * + * @param numPackets Combined packet count. + * @return true, if successful + * @since 1.4 + */ + boolean setNumCombinedBurstPackets(int numPackets) + { + if(DEBUG) Log.d(TAG, "setNumCombinedBurstPackets"); + + if(!sServiceSettingsConnected) + { + // Haven't received 'onConnected' notification yet + return false; + } + + boolean result = false; + try { + sServiceSettingsReceiver.setNumCombinedBurstPackets(numPackets); + result = true; + } catch (RemoteException ex) { + antServiceSettingsConnectionLost(); + } + + return result; + } + + /** + * Get the maximum number of ANT packets which will be combined in one request to the hardware during a burst transfer. + * + * @return The number of combined packets, or -1 on failure. + * @since 1.4 + */ + int getNumCombinedBurstPackets() + { + if(DEBUG) Log.d(TAG, "getNumCombinedBurstPackets"); + + if(!sServiceSettingsConnected) + { + // Haven't received 'onConnected' notification yet + return -1; + } + + int result = -1; + try { + result = sServiceSettingsReceiver.getNumCombinedBurstPackets(); + } catch (RemoteException ex) { + antServiceSettingsConnectionLost(); + } + + return result; + } +} diff --git a/MyTracks/src/com/dsi/ant/AntInterfaceIntent.java b/MyTracks/src/com/dsi/ant/AntInterfaceIntent.java new file mode 100644 index 000000000..1b0b7f7e7 --- /dev/null +++ b/MyTracks/src/com/dsi/ant/AntInterfaceIntent.java @@ -0,0 +1,34 @@ +/* + * Copyright 2010 Dynastream Innovations 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.dsi.ant; + +/** + * Manages the ANT Interface + * + * @hide + */ +public interface AntInterfaceIntent { + + public static final String STATUS = "com.dsi.ant.rx.intent.STATUS"; + public static final String ANT_MESSAGE = "com.dsi.ant.intent.ANT_MESSAGE"; + + public static final String ANT_ENABLED_ACTION = "com.dsi.ant.intent.action.ANT_ENABLED"; + public static final String ANT_DISABLED_ACTION = "com.dsi.ant.intent.action.ANT_DISABLED"; + public static final String ANT_RESET_ACTION = "com.dsi.ant.intent.action.ANT_RESET"; + + public static final String ANT_RX_MESSAGE_ACTION = "com.dsi.ant.intent.action.ANT_RX_MESSAGE_ACTION"; + +} diff --git a/MyTracks/src/com/dsi/ant/AntMesg.java b/MyTracks/src/com/dsi/ant/AntMesg.java new file mode 100644 index 000000000..cf2ae70d9 --- /dev/null +++ b/MyTracks/src/com/dsi/ant/AntMesg.java @@ -0,0 +1,279 @@ +/* + * Copyright 2010 Dynastream Innovations 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.dsi.ant; + +/** + * Public API for controlling the Ant Service. + * AntMesg contains definitions for ANT message IDs + * + * @hide + */ +public class AntMesg { + + ///////////////////////////////////////////////////////////////////////////// + // HCI VS Message Format + // Messages are in the format: + // + // Outgoing ANT messages (host -> ANT chip) + // 01 D1 FD XX YY YY II JJ ------ + // ^ ^ ^ ^ + // | HCI framing | | ANT Mesg | + // + // where: 01 is the 1 byte HCI packet Identifier (HCI Command packet) + // D1 FD is the 2 byte HCI op code (0xFDD1 stored in little endian) + // XX is the 1 byte Length of all parameters in bytes (number of bytes in the HCI packet after this byte) + // YY YY is the 2 byte Parameter describing the length of the entire ANT message (II JJ ------) stored in little endian + // II is the 1 byte size of the ANT message (0-249) + // JJ is the 1 byte ID of the ANT message (1-255, 0 is invalid) + // ------ is the data of the ANT message (0-249 bytes of data) + // + // Incoming HCI Command Complete for ANT message command (host <- ANT chip) + // 04 0E 04 01 D1 FD ZZ + // + // where: 04 is the 1 byte HCI packet Identifier (HCI Event packet) + // 0E is the 1 byte HCI event (Command Complete) + // 04 is the 1 byte Length of all parameters in bytes (there are 4 bytes) + // 01 is the 1 byte Number of parameters in the packet (there is 1 parameter) + // D1 FD is the 2 byte HCI Op code of the command (0xFDD1 stored in little endian) + // ZZ is the 1 byte response to the command (0x00 - Command Successful + // 0x1F - Returned if the receive message queue of the ANT chip is full, the command should be retried + // Other - Any other non-zero response code indicates an error) + // + // Incoming ANT messages (host <- ANT chip) + // 04 FF XX 00 05 YY YY II JJ ------ + // ^ ^ ^ ^ + // | HCI framing | | ANT Mesg | + // + // where: 04 is the 1 byte HCI packet Identifier (HCI Event packet) + // FF is the 1 byte HCI event code (0xFF Vendor Specific event) + // XX is the 1 byte Length of all parameters in bytes (number of bytes in the HCI packet after this byte) + // 00 05 is the 2 byte vendor specific event code for ANT messages (0x0500 stored in little endian) + // YY YY is the 2 byte Parameter describing the length of the entire ANT message (II JJ ------) stored in little endian + // II is the 1 byte size of the ANT message (0-249) + // JJ is the 1 byte ID of the ANT message (1-255, 0 is invalid) + // ------ is the data of the ANT message (0-249 bytes of data) + // + ///////////////////////////////////////////////////////////////////////////// + + public static final byte MESG_SYNC_SIZE =((byte)0); // Ant messages are embedded in HCI messages we do not include a sync byte + public static final byte MESG_SIZE_SIZE =((byte)1); + public static final byte MESG_ID_SIZE =((byte)1); + public static final byte MESG_CHANNEL_NUM_SIZE =((byte)1); + public static final byte MESG_EXT_MESG_BF_SIZE =((byte)1); // NOTE: this could increase in the future + public static final byte MESG_CHECKSUM_SIZE =((byte)0); // Ant messages are embedded in HCI messages we do not include a checksum + public static final byte MESG_DATA_SIZE =((byte)9); + + // The largest serial message is an ANT data message with all of the extended fields + public static final byte MESG_ANT_MAX_PAYLOAD_SIZE =AntDefine.ANT_STANDARD_DATA_PAYLOAD_SIZE; + + public static final byte MESG_MAX_EXT_DATA_SIZE =(AntDefine.ANT_EXT_MESG_DEVICE_ID_FIELD_SIZE + AntDefine.ANT_EXT_STRING_SIZE); // ANT device ID (4 bytes) + Padding for ANT EXT string size(19 bytes) + + public static final byte MESG_MAX_DATA_SIZE =(MESG_ANT_MAX_PAYLOAD_SIZE + MESG_EXT_MESG_BF_SIZE + MESG_MAX_EXT_DATA_SIZE); // ANT data payload (8 bytes) + extended bitfield (1 byte) + extended data (10 bytes) + public static final byte MESG_MAX_SIZE_VALUE =(MESG_MAX_DATA_SIZE + MESG_CHANNEL_NUM_SIZE); // this is the maximum value that the serial message size value is allowed to be + public static final byte MESG_BUFFER_SIZE =(MESG_SIZE_SIZE + MESG_ID_SIZE + MESG_CHANNEL_NUM_SIZE + MESG_MAX_DATA_SIZE + MESG_CHECKSUM_SIZE); + public static final byte MESG_FRAMED_SIZE =(MESG_ID_SIZE + MESG_CHANNEL_NUM_SIZE + MESG_MAX_DATA_SIZE); + public static final byte MESG_HEADER_SIZE =(MESG_SYNC_SIZE + MESG_SIZE_SIZE + MESG_ID_SIZE); + public static final byte MESG_FRAME_SIZE =(MESG_HEADER_SIZE + MESG_CHECKSUM_SIZE); + public static final byte MESG_MAX_SIZE =(MESG_MAX_DATA_SIZE + MESG_FRAME_SIZE); + + public static final byte MESG_SIZE_OFFSET =(MESG_SYNC_SIZE); + public static final byte MESG_ID_OFFSET =(MESG_SYNC_SIZE + MESG_SIZE_SIZE); + public static final byte MESG_DATA_OFFSET =(MESG_HEADER_SIZE); + public static final byte MESG_RECOMMENDED_BUFFER_SIZE =((byte) 64); // This is the recommended size for serial message buffers if there are no RAM restrictions on the system + + ////////////////////////////////////////////// + // Message ID's + ////////////////////////////////////////////// + public static final byte MESG_INVALID_ID =((byte)0x00); + public static final byte MESG_EVENT_ID =((byte)0x01); + + public static final byte MESG_VERSION_ID =((byte)0x3E); + public static final byte MESG_RESPONSE_EVENT_ID =((byte)0x40); + + public static final byte MESG_UNASSIGN_CHANNEL_ID =((byte)0x41); + public static final byte MESG_ASSIGN_CHANNEL_ID =((byte)0x42); + public static final byte MESG_CHANNEL_MESG_PERIOD_ID =((byte)0x43); + public static final byte MESG_CHANNEL_SEARCH_TIMEOUT_ID =((byte)0x44); + public static final byte MESG_CHANNEL_RADIO_FREQ_ID =((byte)0x45); + public static final byte MESG_NETWORK_KEY_ID =((byte)0x46); + public static final byte MESG_RADIO_TX_POWER_ID =((byte)0x47); + public static final byte MESG_RADIO_CW_MODE_ID =((byte)0x48); + + public static final byte MESG_SYSTEM_RESET_ID =((byte)0x4A); + public static final byte MESG_OPEN_CHANNEL_ID =((byte)0x4B); + public static final byte MESG_CLOSE_CHANNEL_ID =((byte)0x4C); + public static final byte MESG_REQUEST_ID =((byte)0x4D); + + public static final byte MESG_BROADCAST_DATA_ID =((byte)0x4E); + public static final byte MESG_ACKNOWLEDGED_DATA_ID =((byte)0x4F); + public static final byte MESG_BURST_DATA_ID =((byte)0x50); + + public static final byte MESG_CHANNEL_ID_ID =((byte)0x51); + public static final byte MESG_CHANNEL_STATUS_ID =((byte)0x52); + public static final byte MESG_RADIO_CW_INIT_ID =((byte)0x53); + public static final byte MESG_CAPABILITIES_ID =((byte)0x54); + + public static final byte MESG_STACKLIMIT_ID =((byte)0x55); + + public static final byte MESG_SCRIPT_DATA_ID =((byte)0x56); + public static final byte MESG_SCRIPT_CMD_ID =((byte)0x57); + + public static final byte MESG_ID_LIST_ADD_ID =((byte)0x59); + public static final byte MESG_ID_LIST_CONFIG_ID =((byte)0x5A); + public static final byte MESG_OPEN_RX_SCAN_ID =((byte)0x5B); + + public static final byte MESG_EXT_CHANNEL_RADIO_FREQ_ID =((byte)0x5C); // OBSOLETE: (for 905 radio) + public static final byte MESG_EXT_BROADCAST_DATA_ID =((byte)0x5D); + public static final byte MESG_EXT_ACKNOWLEDGED_DATA_ID =((byte)0x5E); + public static final byte MESG_EXT_BURST_DATA_ID =((byte)0x5F); + + public static final byte MESG_CHANNEL_RADIO_TX_POWER_ID =((byte)0x60); + public static final byte MESG_GET_SERIAL_NUM_ID =((byte)0x61); + public static final byte MESG_GET_TEMP_CAL_ID =((byte)0x62); + public static final byte MESG_SET_LP_SEARCH_TIMEOUT_ID =((byte)0x63); + public static final byte MESG_SET_TX_SEARCH_ON_NEXT_ID =((byte)0x64); + public static final byte MESG_SERIAL_NUM_SET_CHANNEL_ID_ID =((byte)0x65); + public static final byte MESG_RX_EXT_MESGS_ENABLE_ID =((byte)0x66); + public static final byte MESG_RADIO_CONFIG_ALWAYS_ID =((byte)0x67); + public static final byte MESG_ENABLE_LED_FLASH_ID =((byte)0x68); + + public static final byte MESG_XTAL_ENABLE_ID =((byte)0x6D); + + public static final byte MESG_STARTUP_MESG_ID =((byte)0x6F); + public static final byte MESG_AUTO_FREQ_CONFIG_ID =((byte)0x70); + public static final byte MESG_PROX_SEARCH_CONFIG_ID =((byte)0x71); + public static final byte MESG_EVENT_BUFFERING_CONFIG_ID =((byte)0x74); + + + public static final byte MESG_CUBE_CMD_ID =((byte)0x80); + + public static final byte MESG_GET_PIN_DIODE_CONTROL_ID =((byte)0x8D); + public static final byte MESG_PIN_DIODE_CONTROL_ID =((byte)0x8E); + public static final byte MESG_FIT1_SET_AGC_ID =((byte)0x8F); + + public static final byte MESG_FIT1_SET_EQUIP_STATE_ID =((byte)0x91); // *** CONFLICT: w/ Sensrcore, Fit1 will never have sensrcore enabled + + // Sensrcore Messages + public static final byte MESG_SET_CHANNEL_INPUT_MASK_ID =((byte)0x90); + public static final byte MESG_SET_CHANNEL_DATA_TYPE_ID =((byte)0x91); + public static final byte MESG_READ_PINS_FOR_SECT_ID =((byte)0x92); + public static final byte MESG_TIMER_SELECT_ID =((byte)0x93); + public static final byte MESG_ATOD_SETTINGS_ID =((byte)0x94); + public static final byte MESG_SET_SHARED_ADDRESS_ID =((byte)0x95); + public static final byte MESG_ATOD_EXTERNAL_ENABLE_ID =((byte)0x96); + public static final byte MESG_ATOD_PIN_SETUP_ID =((byte)0x97); + public static final byte MESG_SETUP_ALARM_ID =((byte)0x98); + public static final byte MESG_ALARM_VARIABLE_MODIFY_TEST_ID =((byte)0x99); + public static final byte MESG_PARTIAL_RESET_ID =((byte)0x9A); + public static final byte MESG_OVERWRITE_TEMP_CAL_ID =((byte)0x9B); + public static final byte MESG_SERIAL_PASSTHRU_SETTINGS_ID =((byte)0x9C); + + public static final byte MESG_BIST_ID =((byte)0xAA); + + public static final byte MESG_UNLOCK_INTERFACE_ID =((byte)0xAD); + public static final byte MESG_SERIAL_ERROR_ID =((byte)0xAE); + public static final byte MESG_SET_ID_STRING_ID =((byte)0xAF); + + public static final byte MESG_PORT_GET_IO_STATE_ID =((byte)0xB4); + public static final byte MESG_PORT_SET_IO_STATE_ID =((byte)0xB5); + + public static final byte MESG_SLEEP_ID =((byte)0xC5); + public static final byte MESG_GET_GRMN_ESN_ID =((byte)0xC6); + public static final byte MESG_SET_USB_INFO_ID =((byte)0xC7); + + public static final byte MESG_COMMAND_COMPLETE_RESPONSE_ID =((byte)0xC8); + + ////////////////////////////////////////////// + // Command complete results + ////////////////////////////////////////////// + public static final byte MESG_COMMAND_COMPLETE_SUCCESS =((byte)0x00); + public static final byte MESG_COMMAND_COMPLETE_RETRY =((byte)0x1F); + + ////////////////////////////////////////////// + // Message Sizes + ////////////////////////////////////////////// + public static final byte MESG_INVALID_SIZE =((byte)0); + + public static final byte MESG_VERSION_SIZE =((byte)13); + public static final byte MESG_RESPONSE_EVENT_SIZE =((byte)3); + public static final byte MESG_CHANNEL_STATUS_SIZE =((byte)2); + + public static final byte MESG_UNASSIGN_CHANNEL_SIZE =((byte)1); + public static final byte MESG_ASSIGN_CHANNEL_SIZE =((byte)3); + public static final byte MESG_CHANNEL_ID_SIZE =((byte)5); + public static final byte MESG_CHANNEL_MESG_PERIOD_SIZE =((byte)3); + public static final byte MESG_CHANNEL_SEARCH_TIMEOUT_SIZE =((byte)2); + public static final byte MESG_CHANNEL_RADIO_FREQ_SIZE =((byte)2); + public static final byte MESG_CHANNEL_RADIO_TX_POWER_SIZE =((byte)2); + public static final byte MESG_NETWORK_KEY_SIZE =((byte)9); + public static final byte MESG_RADIO_TX_POWER_SIZE =((byte)2); + public static final byte MESG_RADIO_CW_MODE_SIZE =((byte)3); + public static final byte MESG_RADIO_CW_INIT_SIZE =((byte)1); + + public static final byte MESG_SYSTEM_RESET_SIZE =((byte)1); + public static final byte MESG_OPEN_CHANNEL_SIZE =((byte)1); + public static final byte MESG_CLOSE_CHANNEL_SIZE =((byte)1); + public static final byte MESG_REQUEST_SIZE =((byte)2); + + public static final byte MESG_CAPABILITIES_SIZE =((byte)6); + public static final byte MESG_STACKLIMIT_SIZE =((byte)2); + + public static final byte MESG_SCRIPT_DATA_SIZE =((byte)10); + public static final byte MESG_SCRIPT_CMD_SIZE =((byte)3); + + public static final byte MESG_ID_LIST_ADD_SIZE =((byte)6); + public static final byte MESG_ID_LIST_CONFIG_SIZE =((byte)3); + public static final byte MESG_OPEN_RX_SCAN_SIZE =((byte)1); + public static final byte MESG_EXT_CHANNEL_RADIO_FREQ_SIZE =((byte)3); + + public static final byte MESG_RADIO_CONFIG_ALWAYS_SIZE =((byte)2); + public static final byte MESG_RX_EXT_MESGS_ENABLE_SIZE =((byte)2); + public static final byte MESG_SET_TX_SEARCH_ON_NEXT_SIZE =((byte)2); + public static final byte MESG_SET_LP_SEARCH_TIMEOUT_SIZE =((byte)2); + + public static final byte MESG_SERIAL_NUM_SET_CHANNEL_ID_SIZE =((byte)3); + public static final byte MESG_ENABLE_LED_FLASH_SIZE =((byte)2); + public static final byte MESG_GET_SERIAL_NUM_SIZE =((byte)4); + public static final byte MESG_GET_TEMP_CAL_SIZE =((byte)4); + + public static final byte MESG_XTAL_ENABLE_SIZE =((byte)1); + public static final byte MESG_STARTUP_MESG_SIZE =((byte)1); + public static final byte MESG_AUTO_FREQ_CONFIG_SIZE =((byte)4); + public static final byte MESG_PROX_SEARCH_CONFIG_SIZE =((byte)2); + + public static final byte MESG_GET_PIN_DIODE_CONTROL_SIZE =((byte)1); + public static final byte MESG_PIN_DIODE_CONTROL_ID_SIZE =((byte)2); + public static final byte MESG_FIT1_SET_EQUIP_STATE_SIZE =((byte)2); + public static final byte MESG_FIT1_SET_AGC_SIZE =((byte)3); + + public static final byte MESG_BIST_SIZE =((byte)6); + public static final byte MESG_UNLOCK_INTERFACE_SIZE =((byte)1); + public static final byte MESG_SET_SHARED_ADDRESS_SIZE =((byte)3); + + public static final byte MESG_GET_GRMN_ESN_SIZE =((byte)5); + + public static final byte MESG_PORT_SET_IO_STATE_SIZE =((byte)5); + + public static final byte MESG_EVENT_BUFFERING_CONFIG_SIZE =((byte)6); + + public static final byte MESG_SLEEP_SIZE =((byte)1); + + + public static final byte MESG_EXT_DATA_SIZE =((byte)13); + + protected AntMesg() + { } +} diff --git a/MyTracks/src/com/dsi/ant/IAnt.aidl b/MyTracks/src/com/dsi/ant/IAnt.aidl new file mode 100644 index 000000000..8ee999070 --- /dev/null +++ b/MyTracks/src/com/dsi/ant/IAnt.aidl @@ -0,0 +1,70 @@ +/* + * Copyright 2010 Dynastream Innovations 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.dsi.ant; + +/** + * The Android Ant API is not finalized, and *will* be updated and expanded. + * This is the base level ANT messaging API and gives any application full + * control over the ANT radio HW. Caution should be exercised when using + * this interface. + * + * Public API for controlling the Ant Service. + * + * {@hide} + */ +interface IAnt { +// Since version 1 (1.0): + boolean enable(); + boolean disable(); + boolean isEnabled(); + + boolean ANTTxMessage(in byte[] message); + + boolean ANTResetSystem(); + boolean ANTUnassignChannel(byte channelNumber); + boolean ANTAssignChannel(byte channelNumber, byte channelType, byte networkNumber); + boolean ANTSetChannelId(byte channelNumber, int deviceNumber, byte deviceType, byte txType); + boolean ANTSetChannelPeriod(byte channelNumber, int channelPeriod); + boolean ANTSetChannelRFFreq(byte channelNumber, byte radioFrequency); + boolean ANTSetChannelSearchTimeout(byte channelNumber, byte searchTimeout); + boolean ANTSetLowPriorityChannelSearchTimeout(byte channelNumber, byte searchTimeout); + boolean ANTSetProximitySearch(byte channelNumber, byte searchThreshold); + boolean ANTSetChannelTxPower(byte channelNumber, byte txPower); + boolean ANTAddChannelId(byte channelNumber, int deviceNumber, byte deviceType, byte txType, byte listIndex); + boolean ANTConfigList(byte channelNumber, byte listSize, byte exclude); + + boolean ANTOpenChannel(byte channelNumber); + boolean ANTCloseChannel(byte channelNumber); + + boolean ANTRequestMessage(byte channelNumber, byte messageID); + boolean ANTSendBroadcastData(byte channelNumber, in byte[] txBuffer); + boolean ANTSendAcknowledgedData(byte channelNumber, in byte[] txBuffer); + + boolean ANTSendBurstTransferPacket(byte control, in byte[] txBuffer); + + int ANTSendBurstTransfer(byte channelNumber, in byte[] txBuffer); + int ANTTransmitBurst(byte channelNumber, in byte[] txBuffer, int initialPacket, boolean containsEndOfBurst); + +// Since version 4 (1.3): + boolean ANTConfigEventBuffering(int screenOnFlushTimerInterval, int screenOnFlushBufferThreshold, int screenOffFlushTimerInterval, int screenOffFlushBufferThreshold); + +// Since version 2 (1.1): + boolean ANTDisableEventBuffering(); + +// Since version 3 (1.2): + int getServiceLibraryVersionCode(); + String getServiceLibraryVersionName(); +} diff --git a/MyTracks/src/com/dsi/ant/IServiceSettings.aidl b/MyTracks/src/com/dsi/ant/IServiceSettings.aidl new file mode 100644 index 000000000..3d0cf61c8 --- /dev/null +++ b/MyTracks/src/com/dsi/ant/IServiceSettings.aidl @@ -0,0 +1,29 @@ +/* + * Copyright 2010 Dynastream Innovations 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.dsi.ant; + +/** + * + * API for configuring the non-ANT components of the ANT Service. + * + * {@hide} + */ +interface IServiceSettings { + void debugLogging(boolean debug); + + boolean setNumCombinedBurstPackets(int numPackets); + int getNumCombinedBurstPackets(); +} diff --git a/MyTracks/src/com/google/android/apps/mytracks/AntPreference.java b/MyTracks/src/com/google/android/apps/mytracks/AntPreference.java new file mode 100644 index 000000000..6d6a4b853 --- /dev/null +++ b/MyTracks/src/com/google/android/apps/mytracks/AntPreference.java @@ -0,0 +1,62 @@ +/* + * 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; + +import com.google.android.maps.mytracks.R; + +import android.content.Context; +import android.preference.Preference; +import android.util.AttributeSet; + +/** + * A preference for an ANT device pairing. + * Currently this shows the ID and lets the user clear that ID for future pairing. + * TODO: Support pairing from this preference. + * + * @author Sandor Dornbush + */ +public class AntPreference extends Preference { + + public AntPreference(Context context) { + super(context); + init(); + } + + public AntPreference(Context context, AttributeSet attrs) { + super(context, attrs); + init(); + } + + private void init() { + int sensorId = getPersistedInt(0); + if (sensorId == 0) { + setSummary(R.string.settings_ant_not_paired); + } else { + setSummary(String.format(getContext().getString(R.string.settings_ant_paired), sensorId)); + } + + // Add actions to allow repairing. + setOnPreferenceClickListener( + new OnPreferenceClickListener() { + @Override + public boolean onPreferenceClick(Preference preference) { + AntPreference.this.persistInt(0); + setSummary(R.string.settings_ant_not_paired); + return true; + } + }); + } +} diff --git a/MyTracks/src/com/google/android/apps/mytracks/ChartActivity.java b/MyTracks/src/com/google/android/apps/mytracks/ChartActivity.java index 121615121..25757e541 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/ChartActivity.java +++ b/MyTracks/src/com/google/android/apps/mytracks/ChartActivity.java @@ -252,7 +252,7 @@ public class ChartActivity extends Activity implements setVolumeControlStream(volumeStream); requestWindowFeature(Window.FEATURE_NO_TITLE); - setContentView(R.layout.mytracks_elevation); + setContentView(R.layout.mytracks_charts); ViewGroup layout = (ViewGroup) findViewById(R.id.elevation_chart); cv = new ChartView(this); cv.setMode(this.mode); diff --git a/MyTracks/src/com/google/android/apps/mytracks/ChartValueSeries.java b/MyTracks/src/com/google/android/apps/mytracks/ChartValueSeries.java index acf61ab16..57b88125d 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/ChartValueSeries.java +++ b/MyTracks/src/com/google/android/apps/mytracks/ChartValueSeries.java @@ -21,6 +21,7 @@ import android.content.Context; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Path; +import android.graphics.Paint.Style; import java.text.DecimalFormat; @@ -36,16 +37,96 @@ public class ChartValueSeries { private final Path path = new Path(); private final Paint fillPaint; private final Paint strokePaint; - private final int rounding; + private final Paint labelPaint; + private final ZoomSettings zoomSettings; private String title; - private double min = 0; - private double max = 1; - private int effectiveMax = 0; - private int absoluteMax = Integer.MIN_VALUE; - private int effectiveMin = 0; - private double spread = 0; + private double min; + private double max = 1.0; + private int effectiveMax; + private int effectiveMin; + private double spread; + private int interval; private boolean enabled = true; + + /** + * This class controls how effective min/max values of a {@link ChartValueSeries} are calculated. + */ + public static class ZoomSettings { + private int intervals; + private final int absoluteMin; + private final int absoluteMax; + private final int[] zoomLevels; + + public ZoomSettings(int intervals, int[] zoomLevels) { + this.intervals = intervals; + this.absoluteMin = Integer.MAX_VALUE; + this.absoluteMax = Integer.MIN_VALUE; + this.zoomLevels = zoomLevels; + checkArgs(); + } + + public ZoomSettings(int intervals, int absoluteMin, int absoluteMax, int[] zoomLevels) { + this.intervals = intervals; + this.absoluteMin = absoluteMin; + this.absoluteMax = absoluteMax; + this.zoomLevels = zoomLevels; + checkArgs(); + } + + private void checkArgs() { + if (intervals <= 0 || zoomLevels == null || zoomLevels.length == 0) { + throw new IllegalArgumentException("Expecing positive intervals and non-empty zoom levels"); + } + for (int i = 1; i < zoomLevels.length; ++i) { + if (zoomLevels[i] <= zoomLevels[i - 1]) { + throw new IllegalArgumentException("Expecting zoom levels in ascending order"); + } + } + } + + public int getIntervals() { + return intervals; + } + + public int getAbsoluteMin() { + return absoluteMin; + } + + public int getAbsoluteMax() { + return absoluteMax; + } + + public int[] getZoomLevels() { + return zoomLevels; + } + + /** + * Calculates the interval between markings given the min and max values. + * This function attempts to find the smallest zoom level that fits [min,max] after rounding + * it to the current zoom level. + * + * @param min the minimum value in the series + * @param max the maximum value in the series + * @return the calculated interval for the given range + */ + public int calculateInterval(double min, double max) { + min = Math.min(min, absoluteMin); + max = Math.max(max, absoluteMax); + for (int i = 0; i < zoomLevels.length; ++i) { + int zoomLevel = zoomLevels[i]; + int roundedMin = (int)(min / zoomLevel) * zoomLevel; + if (roundedMin > min) { + roundedMin -= zoomLevel; + } + double interval = (max - roundedMin) / intervals; + if (zoomLevel >= interval) { + return zoomLevel; + } + } + return zoomLevels[zoomLevels.length - 1]; + } + } /** * Constructs a new chart value series. @@ -54,18 +135,30 @@ public class ChartValueSeries { * @param formatString The format of the decimal format for this series * @param fill The paint for filling the chart * @param stroke The paint for stroking the outside the chart, optional - * @param rounding The factor to round the values by + * @param zoomSettings The settings related to zooming + * + * TODO: Get rid of Context and inject appropriate values instead. */ - public ChartValueSeries(Context context, - String formatString, - Paint fill, - Paint stroke, - int rounding, - int titleId) { + public ChartValueSeries(Context context, String formatString, int fillColor, int strokeColor, + ZoomSettings zoomSettings, int titleId) { this.format = new DecimalFormat(formatString); - this.fillPaint = fill; - this.strokePaint = stroke; - this.rounding = rounding; + fillPaint = new Paint(); + fillPaint.setStyle(Style.FILL); + fillPaint.setColor(context.getResources().getColor(fillColor)); + fillPaint.setAntiAlias(true); + if (strokeColor != -1) { + strokePaint = new Paint(); + strokePaint.setStyle(Style.STROKE); + strokePaint.setColor(context.getResources().getColor(strokeColor)); + strokePaint.setAntiAlias(true); + // Make a copy of the stroke paint with the default thickness. + labelPaint = new Paint(strokePaint); + strokePaint.setStrokeWidth(2f); + } else { + strokePaint = null; + labelPaint = fillPaint; + } + this.zoomSettings = zoomSettings; this.title = context.getString(titleId); } @@ -97,20 +190,7 @@ public class ChartValueSeries { * @return The interval between markers */ public int getInterval() { - // Try to find 5 even looking intervals. - int interval = (int) (spread / 5); - int minInterval = rounding / 4; - if (interval < minInterval) { - // We won't be able to find 5 even intervals. - return minInterval; - } else if (interval < rounding) { - // The desired interval is less than the rounding value. - // We will have less than 5 intervals. - return rounding; - } else { - // Round the interval. - return (interval / rounding) * rounding; - } + return interval; } /** @@ -125,14 +205,18 @@ public class ChartValueSeries { min = monitor.getMin(); max = monitor.getMax(); } + min = Math.min(min, zoomSettings.getAbsoluteMin()); + max = Math.max(max, zoomSettings.getAbsoluteMax()); + + this.interval = zoomSettings.calculateInterval(min, max); // Round it up. - effectiveMax = ((int) (max / rounding)) * rounding + rounding; + effectiveMax = ((int) (max / interval)) * interval + interval; // Round it down. - effectiveMin = ((int) (min / rounding)) * rounding; + effectiveMin = ((int) (min / interval)) * interval; if (min < 0) { - effectiveMin -= rounding; + effectiveMin -= interval; } - spread = getMax() - effectiveMin; + spread = effectiveMax - effectiveMin; } /** @@ -152,16 +236,10 @@ public class ChartValueSeries { } /** - * @return The minimum of the rounded up max value and the effective max. + * @return The rounded up maximum value */ public int getMax() { - // Return the effective max if no absolute is set - // or the effective max is less than the absolute. - if ((absoluteMax == Integer.MIN_VALUE) || (effectiveMax < absoluteMax)) { - return effectiveMax; - } else { - return absoluteMax; - } + return effectiveMax; } /** @@ -189,9 +267,11 @@ public class ChartValueSeries { * @return The paint for this series */ Paint getPaint() { - return (strokePaint == null) - ? fillPaint - : strokePaint; + return strokePaint == null ? fillPaint : strokePaint; + } + + public Paint getLabelPaint() { + return labelPaint; } /** @@ -219,14 +299,6 @@ public class ChartValueSeries { this.enabled = enabled; } - public int getAbsoluteMax() { - return absoluteMax; - } - - public void setAbsoluteMax(int absoluteMax) { - this.absoluteMax = absoluteMax; - } - public boolean hasData() { return monitor.hasData(); } diff --git a/MyTracks/src/com/google/android/apps/mytracks/ChartView.java b/MyTracks/src/com/google/android/apps/mytracks/ChartView.java index 06cc848bf..948b5439d 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/ChartView.java +++ b/MyTracks/src/com/google/android/apps/mytracks/ChartView.java @@ -15,6 +15,7 @@ */ package com.google.android.apps.mytracks; +import com.google.android.apps.mytracks.ChartValueSeries.ZoomSettings; import com.google.android.apps.mytracks.content.Waypoint; import com.google.android.apps.mytracks.stats.ExtremityMonitor; import com.google.android.apps.mytracks.util.StringUtils; @@ -25,9 +26,11 @@ import android.content.Context; import android.content.Intent; import android.graphics.Canvas; import android.graphics.Color; +import android.graphics.DashPathEffect; import android.graphics.Paint; -import android.graphics.Paint.Style; import android.graphics.Path; +import android.graphics.Paint.Align; +import android.graphics.Paint.Style; import android.graphics.drawable.Drawable; import android.view.MotionEvent; import android.view.VelocityTracker; @@ -60,14 +63,35 @@ public class ChartView extends View { */ private int zoomLevel = 1; private final int minZoomLevel = 1; - private int maxZoomLevel = 5; + private int maxZoomLevel = 10; + private static final int MAX_INTERVALS = 5; + /* * Borders, margins, dimensions (in pixels): */ private int leftBorder = -1; + + /** + * Unscaled top border of the chart. + */ private static final int TOP_BORDER = 15; - private static final int BOTTOM_BORDER = 135; + + /** + * Device scaled top border of the chart. + */ + private int topBorder; + + /** + * Unscaled bottom border of the chart. + */ + private static final float BOTTOM_BORDER = 40; + + /** + * Device scaled bottom border of the chart. + */ + private int bottomBorder; + private static final int RIGHT_BORDER = 17; /** Space to leave for drawing the unit labels */ private static final int UNIT_BORDER = 15; @@ -103,6 +127,7 @@ public class ChartView extends View { private final Paint borderPaint = new Paint(); private final Paint labelPaint = new Paint(); private final Paint gridPaint = new Paint(); + private final Paint gridBarPaint = new Paint(); private final Paint clearPaint = new Paint(); private final Drawable pointer; private final Drawable statsMarker; @@ -123,8 +148,6 @@ public class ChartView extends View { private boolean metricUnits = true; private boolean showPointer = false; - private boolean yLabelMask[] = null; - /** Display chart versus distance or time */ public enum Mode { BY_DISTANCE, BY_TIME @@ -149,6 +172,9 @@ public class ChartView extends View { gridPaint.setColor(context.getResources().getColor(R.color.gray)); gridPaint.setAntiAlias(false); + gridBarPaint.set(gridPaint); + gridBarPaint.setPathEffect(new DashPathEffect(new float[] {3, 2}, 0)); + clearPaint.setStyle(Style.FILL); clearPaint.setColor(context.getResources().getColor(R.color.white)); clearPaint.setAntiAlias(false); @@ -174,45 +200,47 @@ public class ChartView extends View { public void setUpChartValueSeries(Context context) { series = new ChartValueSeries[NUM_SERIES]; - // Create the paints - Paint fillPaint = new Paint(); - fillPaint.setStyle(Style.FILL); - fillPaint.setColor(context.getResources().getColor(R.color.green)); - fillPaint.setAntiAlias(true); - - // Create the value series + // Create the value series. series[ELEVATION_SERIES] = new ChartValueSeries(context, "###,###", - fillPaint, - null, - 100, + R.color.elevation_fill, + R.color.elevation_border, + new ZoomSettings(MAX_INTERVALS, + new int[] {5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000}), R.string.elevation); - series[SPEED_SERIES] = getSeries(context, R.color.blue_transparent, - R.color.blue, "###,###.0", R.string.speed); - series[POWER_SERIES] = getSeries(context, R.color.red_transparent, - R.color.red, "###,###", R.string.power); - series[POWER_SERIES].setAbsoluteMax(1500); - series[CADENCE_SERIES] = getSeries(context, R.color.purple_transparent, - R.color.purple, "###,###", R.string.cadence); - series[HEART_RATE_SERIES] = getSeries(context, R.color.yellow_transparent, - R.color.yellow, "###,###", R.string.heart_rate); - } - - private ChartValueSeries getSeries(Context context, - int fillColor, int strokeColor, String format, int seriesName) { - Paint fillPaint = new Paint(); - fillPaint.setStyle(Style.FILL); - fillPaint.setColor(context.getResources().getColor(fillColor)); - fillPaint.setAntiAlias(true); - - Paint strokePaint = new Paint(); - strokePaint.setStyle(Style.STROKE); - strokePaint.setColor(context.getResources().getColor(strokeColor)); - strokePaint.setAntiAlias(true); - return new ChartValueSeries(context, format, fillPaint, strokePaint, 5, - seriesName); + series[SPEED_SERIES] = + new ChartValueSeries(context, + "###,###", + R.color.speed_fill, + R.color.speed_border, + new ZoomSettings(MAX_INTERVALS, 0, Integer.MIN_VALUE, + new int[] {5, 10, 20, 50}), + R.string.speed); + series[POWER_SERIES] = + new ChartValueSeries(context, + "###,###", + R.color.power_fill, + R.color.power_border, + new ZoomSettings(MAX_INTERVALS, 0, 1000, new int[] {5, 50, 100, 200}), + R.string.power); + series[CADENCE_SERIES] = + new ChartValueSeries(context, + "###,###", + R.color.cadence_fill, + R.color.cadence_border, + new ZoomSettings(MAX_INTERVALS, 0, Integer.MIN_VALUE, + new int[] {5, 10, 25, 50}), + R.string.cadence); + series[HEART_RATE_SERIES] = + new ChartValueSeries(context, + "###,###", + R.color.heartrate_fill, + R.color.heartrate_border, + new ZoomSettings(MAX_INTERVALS, 0, Integer.MIN_VALUE, + new int[] {25, 50}), + R.string.heart_rate); } public void clearWaypoints() { @@ -277,7 +305,7 @@ public class ChartView extends View { data.add(theData); addDataPointInternal(theData); updateDimensions(); - setupPath(); + setUpPath(); } private void addDataPointInternal(double[] theData) { @@ -308,7 +336,7 @@ public class ChartView extends View { addDataPointInternal(d); } updateDimensions(); - setupPath(); + setUpPath(); } /** @@ -342,7 +370,7 @@ public class ChartView extends View { public void zoomIn() { if (canZoomIn()) { zoomLevel++; - setupPath(); + setUpPath(); invalidate(); } } @@ -359,7 +387,7 @@ public class ChartView extends View { scrollX = effectiveWidth * (zoomLevel - 1); scrollTo(scrollX, 0); } - setupPath(); + setUpPath(); invalidate(); } } @@ -525,12 +553,12 @@ public class ChartView extends View { @Override protected synchronized void onDraw(Canvas c) { if (w != c.getWidth() || h != c.getHeight()) { - // Dimensions have changed (for example due to orientation change): + // Dimensions have changed (for example due to orientation change). w = c.getWidth(); h = c.getHeight(); effectiveWidth = Math.max(0, w - leftBorder - RIGHT_BORDER); - effectiveHeight = Math.max(0, h - TOP_BORDER - BOTTOM_BORDER); - setupPath(); + effectiveHeight = Math.max(0, h - topBorder - bottomBorder); + setUpPath(); } c.save(); c.drawColor(Color.WHITE); @@ -540,16 +568,21 @@ public class ChartView extends View { c.restore(); return; } + + c.save(); + c.clipRect(leftBorder + 1 + getScrollX(), topBorder + 1, + w - RIGHT_BORDER + getScrollX() - 1, h - bottomBorder - 1); - // Draw the data series + drawGrid(c); + + // Draw the data series. for (ChartValueSeries cvs : series) { if (cvs.isEnabled() && cvs.hasData()) { cvs.drawPath(c); } } - drawGrid(c); - - // Draw the waypoints: + + // Draw the waypoints. for (int i = 1; i < waypoints.size(); i++) { final Waypoint waypoint = waypoints.get(i); if (waypoint.getLocation() == null) { @@ -558,8 +591,8 @@ public class ChartView extends View { c.save(); final float x = getWaypointX(waypoint); - c.drawLine(x, h - BOTTOM_BORDER, x, TOP_BORDER, gridPaint); - c.translate(x - (markerWidth / 2), markerHeight); + c.drawLine(x, h - bottomBorder, x, topBorder, gridPaint); + c.translate(x - markerWidth / 2, markerHeight); if (waypoints.get(i).getType() == Waypoint.TYPE_STATISTICS) { statsMarker.draw(c); } else { @@ -568,24 +601,27 @@ public class ChartView extends View { c.restore(); } - // Draw the axis and labels: + c.restore(); + + // Draw the axis and labels. drawXLabels(c); drawXAxis(c); drawSeriesTitles(c); - - yLabelMask = new boolean[effectiveHeight / FONT_HEIGHT + 1]; + c.translate(getScrollX(), 0); drawYAxis(c); + final int spacer = 5; + int x = leftBorder - spacer; for (ChartValueSeries cvs : series) { if (cvs.isEnabled() && cvs.hasData()) { - drawYLabels(cvs, c); + x -= drawYLabels(cvs, c, x) + spacer; } } c.restore(); - if (showPointer && data.size() > 0) { + if (showPointer && !data.isEmpty()) { c.translate(getX(maxX) - pointer.getIntrinsicWidth() / 2, - (getY(series[0], data.get(data.size() - 1)[1]) - - pointer.getIntrinsicHeight() / 2 - 12)); + getY(series[0], data.get(data.size() - 1)[1]) + - pointer.getIntrinsicHeight() / 2 - 12); pointer.draw(c); } } @@ -600,8 +636,8 @@ public class ChartView extends View { int j = 0; for (ChartValueSeries cvs : series) { if (cvs.isEnabled() && cvs.hasData()) { - int y = (int) (w * zoomLevel * ((double) (++j) / sections)); - c.drawText(cvs.getTitle(), y, TOP_BORDER, cvs.getPaint()); + int x = (int) (w * (double) ++j / sections) + getScrollX(); + c.drawText(cvs.getTitle(), x, topBorder, cvs.getLabelPaint()); } } } @@ -610,28 +646,35 @@ public class ChartView extends View { * Sets up the path that is used to draw the histogram in onDraw(). The path * needs to be updated any time after the data or histogram dimensions change. */ - private synchronized void setupPath() { + private synchronized void setUpPath() { for (ChartValueSeries cvs : series) { cvs.getPath().reset(); } - if (data.size() < 1) { + if (data.isEmpty()) { return; } // All of the data points to the respective series. - for (int i = 0; i < data.size(); i++) { + // TODO: Come up with a better sampling than Math.max(1, (maxZoomLevel - zoomLevel + 1) / 2); + int sampling = 1; + for (int i = 0; i < data.size(); i += sampling) { double[] d = data.get(i); int min = Math.min(series.length, d.length - 1); - - for (int j = 0; j < min; j++) { + for (int j = 0; j < min; ++j) { ChartValueSeries cvs = series[j]; Path path = cvs.getPath(); - path.lineTo(getX(d[0]), getY(cvs, d[j + 1])); + int x = getX(d[0]); + int y = getY(cvs, d[j + 1]); + if (i == 0) { + path.moveTo(x, y); + } else { + path.lineTo(x, y); + } } } // Close the path. - int yCorner = TOP_BORDER + effectiveHeight; + int yCorner = topBorder + effectiveHeight; int xCorner = getX(data.get(0)[0]); int min = series.length; for (int j = 0; j < min; j++) { @@ -675,14 +718,20 @@ public class ChartView extends View { for (ChartValueSeries cvs : series) { cvs.updateDimension(); } + // TODO: This is totally broken. Make sure that we calculate based on measureText for each + // grid line, as the labels may vary across intervals. int maxLength = 0; for (ChartValueSeries cvs : series) { - maxLength = Math.max(maxLength, - cvs.getMaxLabelLength()); + if (cvs.isEnabled() && cvs.hasData()) { + maxLength += cvs.getMaxLabelLength(); + } } - leftBorder = 4 + 7 * maxLength; + leftBorder = 4 + 8 * maxLength; effectiveWidth = w - leftBorder - RIGHT_BORDER; - effectiveHeight = h - TOP_BORDER - BOTTOM_BORDER; + float density = getContext().getResources().getDisplayMetrics().density; + bottomBorder = (int) (density * BOTTOM_BORDER); + topBorder = (int) (density * TOP_BORDER); + effectiveHeight = h - topBorder - bottomBorder; } private int getX(double distance) { @@ -690,28 +739,25 @@ public class ChartView extends View { } private int getY(ChartValueSeries cvs, double y) { - return TOP_BORDER + effectiveHeight + return topBorder + effectiveHeight - (int) ((y - cvs.getMin()) * effectiveHeight / cvs.getSpread()); } private void drawXLabels(Canvas c) { - int numLabels = 0; double interval = (int) (maxX / zoomLevel / 4); boolean shortFormat = false; if (interval < 1) { interval = .5; shortFormat = true; + } else if (interval < 5) { + interval = 2; + } else if (interval < 10) { + interval = 5; } else { - if (interval < 5) { - interval = 2; - } else if (interval < 10) { - interval = 5; - } else { - interval = (interval / 10) * 10; - } + interval = (interval / 10) * 10; } drawXLabel(c, 0, shortFormat); - numLabels++; + int numLabels = 1; for (int i = 1; i * interval < maxX; i++) { drawXLabel(c, i * interval, shortFormat); numLabels++; @@ -721,102 +767,78 @@ public class ChartView extends View { } } - private void drawYLabels(ChartValueSeries cvs, Canvas c) { - int numLabels = 0; - drawYLabel(cvs, c, 0); - final int y0 = getY(cvs, 0); - numLabels++; + private float drawYLabels(ChartValueSeries cvs, Canvas c, int x) { int interval = cvs.getInterval(); - for (int i = 0; i * interval < cvs.getSpread(); i++) { - final int y = i * interval + cvs.getMin(); - final int yi = getY(cvs, y); - if (Math.abs(y0 - yi) > 15) { - drawYLabel(cvs, c, y); - numLabels++; - } - } - if (numLabels < 3) { - drawYLabel(cvs, c, cvs.getMax()); + float maxTextWidth = 0; + for (int i = 0; i < MAX_INTERVALS; ++i) { + maxTextWidth = Math.max(maxTextWidth, drawYLabel(cvs, c, x, i * interval + cvs.getMin())); } + return maxTextWidth; } private void drawXLabel(Canvas c, double x, boolean shortFormat) { + if (x < 0) { + return; + } String s = (mode == Mode.BY_DISTANCE) ? (shortFormat ? xShortFormat.format(x) : xFormat.format(x)) : StringUtils.formatTime((long) x); c.drawText(s, getX(x), - effectiveHeight + UNIT_BORDER + TOP_BORDER, + effectiveHeight + UNIT_BORDER + topBorder, labelPaint); } - private void drawYLabel(ChartValueSeries cvs, Canvas c, int y) { - int desiredY = - (int) ((y - cvs.getMin()) * effectiveHeight / cvs.getSpread()); - - // Make sure we don't write one label on top of another. - int slot = desiredY / FONT_HEIGHT; - if (slot < 0) { - return; - } - if (yLabelMask[slot]) { - desiredY = ++slot * FONT_HEIGHT; - } - desiredY = TOP_BORDER + effectiveHeight + 5 - desiredY; - if (slot < yLabelMask.length) { - yLabelMask[slot] = true; - } - c.drawText(cvs.getFormat().format(y), - 2, desiredY, - cvs.getPaint()); + private float drawYLabel(ChartValueSeries cvs, Canvas c, int x, int y) { + int desiredY = (int) ((y - cvs.getMin()) * effectiveHeight / + (cvs.getInterval() * MAX_INTERVALS)); + desiredY = topBorder + effectiveHeight + FONT_HEIGHT / 2 - desiredY - 1; + Paint p = new Paint(cvs.getLabelPaint()); + p.setTextAlign(Align.RIGHT); + String text = cvs.getFormat().format(y); + c.drawText(text, x, desiredY, p); + return p.measureText(text); } private void drawXAxis(Canvas canvas) { float rightEdge = getX(maxX); - canvas.drawLine(leftBorder, effectiveHeight + TOP_BORDER, rightEdge, - effectiveHeight + TOP_BORDER, borderPaint); + final int y = effectiveHeight + topBorder; + canvas.drawLine(leftBorder, y, rightEdge, y, borderPaint); Context c = getContext(); - String s = - (mode == Mode.BY_DISTANCE) - ? (metricUnits ? c.getString(R.string.kilometer) - : c.getString(R.string.mile)) : (c.getString(R.string.min)); - canvas.drawText(s, - rightEdge, - effectiveHeight + .2f * UNIT_BORDER + TOP_BORDER, - labelPaint); + String s = mode == Mode.BY_DISTANCE + ? (metricUnits ? c.getString(R.string.kilometer) : c.getString(R.string.mile)) + : c.getString(R.string.min); + canvas.drawText(s, rightEdge, effectiveHeight + .2f * UNIT_BORDER + topBorder, labelPaint); } private void drawYAxis(Canvas canvas) { - canvas.drawLine(leftBorder, UNIT_BORDER + TOP_BORDER, - leftBorder, effectiveHeight + TOP_BORDER, - borderPaint); canvas.drawRect(0, 0, - leftBorder - 1, effectiveHeight + TOP_BORDER + 1, - clearPaint); + leftBorder - 1, effectiveHeight + topBorder + UNIT_BORDER + 1, + clearPaint); + canvas.drawLine(leftBorder, UNIT_BORDER + topBorder, + leftBorder, effectiveHeight + topBorder, + borderPaint); + for (int i = 1; i < MAX_INTERVALS; ++i) { + int y = i * effectiveHeight / MAX_INTERVALS + topBorder; + canvas.drawLine(leftBorder - 5, y, leftBorder, y, gridPaint); + } + Context c = getContext(); // TODO: This should really show units for all series. - String s = metricUnits - ? c.getString(R.string.meter) - : c.getString(R.string.feet); - canvas.drawText(s, leftBorder - UNIT_BORDER * .2f, - UNIT_BORDER * .8f + TOP_BORDER, labelPaint); + String s = metricUnits ? c.getString(R.string.meter) : c.getString(R.string.feet); + canvas.drawText(s, leftBorder - UNIT_BORDER * .2f, UNIT_BORDER * .8f + topBorder, labelPaint); } - + private synchronized void drawGrid(Canvas c) { - if (data.size() < 1) { + if (data.isEmpty()) { return; } - for (ChartValueSeries cvs : series) { - if (cvs.isEnabled()) { - int interval = cvs.getInterval(); - for (int i = 1; i * interval < cvs.getSpread(); i++) { - int y = getY(cvs, i * interval + cvs.getMin()); - c.drawLine(getX(data.get(0)[0]), y, - getX(data.get(data.size() - 1)[0]), y, - cvs.getPaint()); - } - } + + float rightEdge = getX(maxX); + for (int i = 1; i < MAX_INTERVALS; ++i) { + int y = i * effectiveHeight / MAX_INTERVALS + topBorder; + c.drawLine(leftBorder, y, rightEdge, y, gridBarPaint); } } diff --git a/MyTracks/src/com/google/android/apps/mytracks/CheckUnits.java b/MyTracks/src/com/google/android/apps/mytracks/CheckUnits.java index 5947f8900..d1fc56029 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/CheckUnits.java +++ b/MyTracks/src/com/google/android/apps/mytracks/CheckUnits.java @@ -16,6 +16,7 @@ package com.google.android.apps.mytracks; +import com.google.android.apps.mytracks.util.ApiFeatures; import com.google.android.maps.mytracks.R; import android.app.Activity; @@ -82,7 +83,8 @@ class CheckUnits { } private static void recordCheckPerformed(SharedPreferences preferences) { - preferences.edit().putBoolean(PREFERENCE_UNITS_CHECKED, true).commit(); + ApiFeatures.getInstance().getApiPlatformAdapter().applyPreferenceChanges( + preferences.edit().putBoolean(PREFERENCE_UNITS_CHECKED, true)); } private CheckUnits() { diff --git a/MyTracks/src/com/google/android/apps/mytracks/Eula.java b/MyTracks/src/com/google/android/apps/mytracks/Eula.java index f070e310a..aed5a40a0 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/Eula.java +++ b/MyTracks/src/com/google/android/apps/mytracks/Eula.java @@ -15,43 +15,43 @@ */ package com.google.android.apps.mytracks; +import com.google.android.apps.mytracks.util.ApiFeatures; import com.google.android.apps.mytracks.util.ResourceUtils; import com.google.android.maps.mytracks.R; import android.app.Activity; import android.app.AlertDialog; +import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences; /** - * Displays an EULA ("End User License Agreement") that the user has to accept - * before using the application. Your application should call - * {@link Eula#showEula(android.app.Activity)} in the onCreate() method of the - * first activity. If the user accepts the EULA, it will never be shown again. - * If the user refuses, {@link android.app.Activity#finish()} is invoked on your - * activity. + * This class handles display of EULAs ("End User License Agreements") to the + * user. */ class Eula { private static final String PREFERENCE_EULA_ACCEPTED = "eula.accepted"; private static final String PREFERENCES_EULA = "eula"; + private Eula() {} + /** * Displays the EULA if necessary. This method should be called from the - * onCreate() method of your main Activity. + * onCreate() method of your main Activity. If the user accepts, the EULA + * will never be displayed again. If the user refuses, the activity will + * finish (exit). * * @param activity The Activity to finish if the user rejects the EULA */ - static void showEula(final Activity activity) { + static void showEulaRequireAcceptance(final Activity activity) { final SharedPreferences preferences = activity.getSharedPreferences(PREFERENCES_EULA, Activity.MODE_PRIVATE); if (preferences.getBoolean(PREFERENCE_EULA_ACCEPTED, false)) { return; } - final AlertDialog.Builder builder = new AlertDialog.Builder(activity); - builder.setTitle(R.string.eula_title); - builder.setCancelable(true); + final AlertDialog.Builder builder = initDialog(activity); builder.setPositiveButton(R.string.accept, new DialogInterface.OnClickListener() { @Override @@ -72,12 +72,31 @@ class Eula { refuse(activity); } }); - builder.setMessage(ResourceUtils.readFile(activity, R.raw.eula)); builder.show(); } + /** + * Display the EULA to the user in an informational context. They won't be + * given the choice of accepting or declining the EULA -- we're simply + * displaying it for them to read. + */ + static void showEula(Context context) { + AlertDialog.Builder builder = initDialog(context); + builder.setPositiveButton(R.string.ok, null); + builder.show(); + } + + private static AlertDialog.Builder initDialog(Context context) { + AlertDialog.Builder builder = new AlertDialog.Builder(context); + builder.setCancelable(true); + builder.setTitle(R.string.eula_title); + builder.setMessage(ResourceUtils.readFile(context, R.raw.eula)); + return builder; + } + private static void accept(Activity activity, SharedPreferences preferences) { - preferences.edit().putBoolean(PREFERENCE_EULA_ACCEPTED, true).commit(); + ApiFeatures.getInstance().getApiPlatformAdapter().applyPreferenceChanges( + preferences.edit().putBoolean(PREFERENCE_EULA_ACCEPTED, true)); Intent startIntent = new Intent(activity, WelcomeActivity.class); activity.startActivityForResult(startIntent, MyTracksConstants.WELCOME); } @@ -85,7 +104,4 @@ class Eula { private static void refuse(Activity activity) { activity.finish(); } - - private Eula() { - } } diff --git a/MyTracks/src/com/google/android/apps/mytracks/ExportAllTracks.java b/MyTracks/src/com/google/android/apps/mytracks/ExportAllTracks.java index 3a276f86e..1eb86831a 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/ExportAllTracks.java +++ b/MyTracks/src/com/google/android/apps/mytracks/ExportAllTracks.java @@ -46,6 +46,7 @@ public class ExportAllTracks { public static final int GPX_OPTION_INDEX = 0; public static final int KML_OPTION_INDEX = 1; public static final int CSV_OPTION_INDEX = 2; + public static final int TCX_OPTION_INDEX = 3; private final Activity activity; private WakeLock wakeLock; @@ -67,6 +68,11 @@ public class ExportAllTracks { case CSV_OPTION_INDEX: format = TrackFileFormat.CSV; break; + case TCX_OPTION_INDEX: + format = TrackFileFormat.TCX; + break; + default: + Log.w(MyTracksConstants.TAG, "Unknown export format: " + which); } } }; diff --git a/MyTracks/src/com/google/android/apps/mytracks/MyTracks.java b/MyTracks/src/com/google/android/apps/mytracks/MyTracks.java index 83750d600..34f6a7361 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/MyTracks.java +++ b/MyTracks/src/com/google/android/apps/mytracks/MyTracks.java @@ -23,7 +23,7 @@ import static com.google.android.apps.mytracks.DialogManager.DIALOG_WRITE_PROGRE import com.google.android.accounts.Account; 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.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; @@ -39,6 +39,7 @@ import com.google.android.apps.mytracks.services.StatusAnnouncerFactory; import com.google.android.apps.mytracks.services.TrackRecordingService; 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.maps.mytracks.R; import android.app.Activity; @@ -60,6 +61,7 @@ import android.os.Bundle; import android.os.Handler; import android.os.HandlerThread; import android.os.IBinder; +import android.os.RemoteException; import android.util.Log; import android.view.Menu; import android.view.MenuItem; @@ -89,10 +91,6 @@ import org.xml.sax.SAXException; */ public class MyTracks extends TabActivity implements OnTouchListener, OnSharedPreferenceChangeListener, ProgressIndicator { - - private static final String WAYPOINT_ICON_URL = - "http://maps.google.com/mapfiles/ms/micons/blue-pushpin.png"; - /** * Singleton instance */ @@ -252,6 +250,11 @@ public class MyTracks extends TabActivity implements OnTouchListener, Log.d(MyTracksConstants.TAG, "MyTracks.onCreate"); super.onCreate(savedInstanceState); instance = this; + ApiFeatures apiFeatures = ApiFeatures.getInstance(); + if (!MyTracksUtils.isRelease(this)) { + apiFeatures.getApiPlatformAdapter().enableStrictMode(); + } + providerUtils = MyTracksProviderUtils.Factory.get(this); menuManager = new MenuManager(this); sharedPreferences = getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0); @@ -259,7 +262,7 @@ public class MyTracks extends TabActivity implements OnTouchListener, // The volume we want to control is the Text-To-Speech volume int volumeStream = - new StatusAnnouncerFactory(ApiFeatures.getInstance()).getVolumeStream(); + new StatusAnnouncerFactory(apiFeatures).getVolumeStream(); setVolumeControlStream(volumeStream); // We don't need a window title bar: @@ -310,7 +313,7 @@ public class MyTracks extends TabActivity implements OnTouchListener, } // This will show the eula until the user accepts or quits the app. - Eula.showEula(this); + Eula.showEulaRequireAcceptance(this); // Check if we got invoked via the VIEW intent: Intent intent = getIntent(); @@ -425,7 +428,13 @@ public class MyTracks extends TabActivity implements OnTouchListener, public boolean onTrackballEvent(MotionEvent event) { if (isRecording()) { if (event.getAction() == MotionEvent.ACTION_DOWN) { - insertStatisticsMarker(); + try { + insertWaypoint(WaypointCreationRequest.DEFAULT_STATISTICS); + } catch (RemoteException e) { + Log.e(MyTracksConstants.TAG, "Cannot insert statistics marker.", e); + } catch (IllegalStateException e) { + Log.e(MyTracksConstants.TAG, "Cannot insert statistics marker.", e); + } return true; } } @@ -506,13 +515,14 @@ public class MyTracks extends TabActivity implements OnTouchListener, } break; } - case MyTracksConstants.AUTHENTICATE_TO_DOCS: { + case MyTracksConstants.AUTHENTICATE_TO_DOCLIST: { if (resultCode == RESULT_OK) { setProgressValue(0); setProgressMessage( R.string.progress_message_authenticating_docs); authenticate(results, - MyTracksConstants.AUTHENTICATE_TO_TRIX, "writely"); + MyTracksConstants.AUTHENTICATE_TO_TRIX, + SendToDocs.GDATA_SERVICE_NAME_DOCLIST); } else { dialogManager.dismissDialogSafely(DIALOG_PROGRESS); } @@ -523,7 +533,8 @@ public class MyTracks extends TabActivity implements OnTouchListener, setProgressValue(30); setProgressMessage( R.string.progress_message_authenticating_docs); - authenticate(results, MyTracksConstants.SEND_TO_DOCS, "wise"); + authenticate(results, MyTracksConstants.SEND_TO_DOCS, + SendToDocs.GDATA_SERVICE_NAME_TRIX); } else { dialogManager.dismissDialogSafely(DIALOG_PROGRESS); } @@ -535,9 +546,10 @@ public class MyTracks extends TabActivity implements OnTouchListener, 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; - final SendToDocs sender = new SendToDocs(this, authMap.get("wise"), - authMap.get("writely"), trackId); Runnable onCompletion = new Runnable() { public void run() { setProgressValue(100); @@ -591,7 +603,7 @@ public class MyTracks extends TabActivity implements OnTouchListener, } } if (dialogManager.getSendToGoogleDialog().getSendToDocs()) { - onActivityResult(MyTracksConstants.AUTHENTICATE_TO_DOCS, + onActivityResult(MyTracksConstants.AUTHENTICATE_TO_DOCLIST, RESULT_OK, new Intent()); } else { dialogManager.dismissDialogSafely(DIALOG_PROGRESS); @@ -877,90 +889,25 @@ public class MyTracks extends TabActivity implements OnTouchListener, /** * Inserts a waypoint marker. * - * @return the id of the inserted statistics marker, or - * -1 unable to find location - * -2 track recording service is not running? - * -3 remote exception when contacting track recording service - * -4 inserting marker into provider failed + * @return Id of the inserted statistics marker. + * @throws RemoteException If the call on the service failed. */ - public long insertWaypointMarker() { - Location location = getCurrentLocation(); - if (location == null) { + public long insertWaypoint(WaypointCreationRequest request) throws RemoteException { + if (trackRecordingService == null) { + throw new IllegalStateException("The recording service is not bound."); + } + try { + long waypointId = trackRecordingService.insertWaypoint(request); + if (waypointId >= 0) { + Toast.makeText(this, R.string.status_statistics_inserted, + Toast.LENGTH_LONG).show(); + } + return waypointId; + } catch (RemoteException e) { Toast.makeText(this, R.string.error_unable_to_insert_marker, Toast.LENGTH_LONG).show(); - return -1; + throw e; } - if (trackRecordingService != null) { - try { - Waypoint wpt = new Waypoint(); - wpt.setName(getString(R.string.waypoint)); - wpt.setType(Waypoint.TYPE_WAYPOINT); - wpt.setTrackId(recordingTrackId); - wpt.setIcon(WAYPOINT_ICON_URL); - wpt.setLocation(location); - long waypointId = trackRecordingService.insertWaypointMarker(wpt); - if (waypointId >= 0) { - Toast.makeText(this, R.string.status_waypoint_inserted, - Toast.LENGTH_LONG).show(); - return waypointId; - } else { - Toast.makeText(this, R.string.error_unable_to_insert_marker, - Toast.LENGTH_LONG).show(); - Log.e(MyTracksConstants.TAG, "Cannot insert waypoint marker?"); - return -4; - } - // 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) { - Toast.makeText(this, R.string.error_unable_to_insert_marker, - Toast.LENGTH_LONG).show(); - Log.e(MyTracksConstants.TAG, "Cannot insert waypoint marker.", e); - } - return -3; - } - return -2; - } - - /** - * Inserts a statistics marker. - * - * @return the id of the inserted statistics marker, or - * -1 unable to find location - * -2 track recording service is not running? - * -3 remote exception when contacting track recording service - * -4 inserting marker into provider failed - */ - public long insertStatisticsMarker() { - Location location = getLastLocation(); - if (location == null) { - Toast.makeText(this, R.string.error_unable_to_insert_marker, - Toast.LENGTH_LONG).show(); - return -1; - } - if (trackRecordingService != null) { - try { - long waypointId = - trackRecordingService.insertStatisticsMarker(location); - if (waypointId >= 0) { - Toast.makeText(this, R.string.status_statistics_inserted, - Toast.LENGTH_LONG).show(); - return waypointId; - } else { - Toast.makeText(this, R.string.error_unable_to_insert_marker, - Toast.LENGTH_LONG).show(); - Log.e(MyTracksConstants.TAG, "Cannot insert statistics marker?"); - return -4; - } - // 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) { - Toast.makeText(this, R.string.error_unable_to_insert_marker, - Toast.LENGTH_LONG).show(); - Log.e(MyTracksConstants.TAG, "Cannot insert statistics marker?", e); - } - return -3; - } - return -2; } /** @@ -980,7 +927,7 @@ public class MyTracks extends TabActivity implements OnTouchListener, service); authMap.put(service, auth); } - Log.d(MyTracksConstants.TAG, "Loggin in to " + service + "..."); + Log.d(MyTracksConstants.TAG, "Logging in to " + service + "..."); if (AuthManagerFactory.useModernAuthManager()) { runOnUiThread(new Runnable() { @Override @@ -1096,7 +1043,7 @@ public class MyTracks extends TabActivity implements OnTouchListener, authenticate(new Intent(), MyTracksConstants.SEND_TO_GOOGLE, SendToFusionTables.SERVICE_ID); } else { - onActivityResult(MyTracksConstants.AUTHENTICATE_TO_DOCS, RESULT_OK, + onActivityResult(MyTracksConstants.AUTHENTICATE_TO_DOCLIST, RESULT_OK, new Intent()); } } @@ -1108,10 +1055,10 @@ public class MyTracks extends TabActivity implements OnTouchListener, * @param trackId the id of the track */ public void setSelectedTrackId(final long trackId) { - sharedPreferences - .edit() - .putLong(getString(R.string.selected_track_key), trackId) - .commit(); + ApiFeatures.getInstance().getApiPlatformAdapter().applyPreferenceChanges( + sharedPreferences + .edit() + .putLong(getString(R.string.selected_track_key), trackId)); } long getSelectedTrackId() { diff --git a/MyTracks/src/com/google/android/apps/mytracks/MyTracksConstants.java b/MyTracks/src/com/google/android/apps/mytracks/MyTracksConstants.java index 80726e5fc..b9d11009b 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/MyTracksConstants.java +++ b/MyTracks/src/com/google/android/apps/mytracks/MyTracksConstants.java @@ -50,7 +50,7 @@ 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_DOCS = 8; + 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; @@ -213,6 +213,9 @@ public abstract class MyTracksConstants { } } + public static final String MAPSHOP_BASE_URL = + "http://maps.google.com/maps/ms"; + /** * This is an abstract utility class. */ diff --git a/MyTracks/src/com/google/android/apps/mytracks/MyTracksDeleteAllTracks.java b/MyTracks/src/com/google/android/apps/mytracks/MyTracksDeleteAllTracks.java index 8a5f255f1..1e939b144 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/MyTracksDeleteAllTracks.java +++ b/MyTracks/src/com/google/android/apps/mytracks/MyTracksDeleteAllTracks.java @@ -16,6 +16,7 @@ package com.google.android.apps.mytracks; import com.google.android.apps.mytracks.content.MyTracksProviderUtils; +import com.google.android.apps.mytracks.util.ApiFeatures; import com.google.android.maps.mytracks.R; import android.app.AlertDialog; @@ -63,7 +64,7 @@ public class MyTracksDeleteAllTracks extends Handler { context.getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0); SharedPreferences.Editor editor = prefs.edit(); editor.putLong(context.getString(R.string.selected_track_key), -1); - editor.commit(); + ApiFeatures.getInstance().getApiPlatformAdapter().applyPreferenceChanges(editor); if (done != null) { Handler h = new Handler(); h.post(done); diff --git a/MyTracks/src/com/google/android/apps/mytracks/MyTracksWaypointDetails.java b/MyTracks/src/com/google/android/apps/mytracks/MyTracksWaypointDetails.java index d159fb236..5e9f110b7 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/MyTracksWaypointDetails.java +++ b/MyTracks/src/com/google/android/apps/mytracks/MyTracksWaypointDetails.java @@ -42,6 +42,8 @@ import android.widget.ImageView; public class MyTracksWaypointDetails extends Activity implements OnClickListener { + public static final String WAYPOINT_ID_EXTRA = "com.google.android.apps.mytracks.WAYPOINT_ID"; + /** * The id of the way point being edited (taken from bundle, "waypointid") */ @@ -78,7 +80,7 @@ public class MyTracksWaypointDetails extends Activity } // Required extra when launching this intent: - waypointId = getIntent().getLongExtra("waypointid", -1); + waypointId = getIntent().getLongExtra(WAYPOINT_ID_EXTRA, -1); if (waypointId < 0) { Log.d(MyTracksConstants.TAG, "MyTracksWaypointsDetails intent was launched w/o waypoint id."); diff --git a/MyTracks/src/com/google/android/apps/mytracks/MyTracksWaypointsList.java b/MyTracks/src/com/google/android/apps/mytracks/MyTracksWaypointsList.java index 594c9fb98..f3c151169 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/MyTracksWaypointsList.java +++ b/MyTracks/src/com/google/android/apps/mytracks/MyTracksWaypointsList.java @@ -17,6 +17,7 @@ package com.google.android.apps.mytracks; import com.google.android.apps.mytracks.content.MyTracksProviderUtils; import com.google.android.apps.mytracks.content.Waypoint; +import com.google.android.apps.mytracks.content.WaypointCreationRequest; import com.google.android.apps.mytracks.content.WaypointsColumns; import com.google.android.apps.mytracks.util.StringUtils; import com.google.android.maps.mytracks.R; @@ -28,6 +29,8 @@ import android.content.Intent; import android.content.SharedPreferences; import android.database.Cursor; import android.os.Bundle; +import android.os.RemoteException; +import android.util.Log; import android.view.ContextMenu; import android.view.ContextMenu.ContextMenuInfo; import android.view.MenuItem; @@ -87,7 +90,7 @@ 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("waypointid", id); + result.putExtra(MyTracksWaypointDetails.WAYPOINT_ID_EXTRA, id); setResult(MyTracksConstants.EDIT_WAYPOINT, result); finish(); } @@ -103,7 +106,7 @@ public class MyTracksWaypointsList extends ListActivity case MyTracksConstants.MENU_EDIT: { Intent intent = new Intent(this, MyTracksWaypointDetails.class); intent.putExtra("trackid", trackId); - intent.putExtra("waypointid", waypointId); + intent.putExtra(MyTracksWaypointDetails.WAYPOINT_ID_EXTRA, waypointId); startActivity(intent); return true; } @@ -165,27 +168,34 @@ public class MyTracksWaypointsList extends ListActivity @Override public void onClick(View v) { + WaypointCreationRequest request; switch (v.getId()) { - case R.id.waypointslist_btn_insert_waypoint: { - long id = MyTracks.getInstance().insertWaypointMarker(); - if (id >= 0) { - Intent intent = new Intent(this, MyTracksWaypointDetails.class); - intent.putExtra("waypointid", id); - startActivity(intent); - } + case R.id.waypointslist_btn_insert_waypoint: + request = WaypointCreationRequest.DEFAULT_MARKER; break; - } - case R.id.waypointslist_btn_insert_statistics: { - long id = MyTracks.getInstance().insertStatisticsMarker(); - if (id >= 0) { - Intent intent = new Intent(this, MyTracksWaypointDetails.class); - intent.putExtra("waypointid", id); - startActivity(intent); - } + case R.id.waypointslist_btn_insert_statistics: + request = WaypointCreationRequest.DEFAULT_STATISTICS; break; - } - + default: + return; } + long id; + try { + id = MyTracks.getInstance().insertWaypoint(request); + } catch (RemoteException e) { + Log.e(MyTracksConstants.TAG, "Cannot insert marker.", e); + return; + } catch (IllegalStateException e) { + Log.e(MyTracksConstants.TAG, "Cannot insert marker.", e); + return; + } + if (id < 0) { + Log.e(MyTracksConstants.TAG, "Failed to insert marker."); + return; + } + Intent intent = new Intent(this, MyTracksWaypointDetails.class); + intent.putExtra(MyTracksWaypointDetails.WAYPOINT_ID_EXTRA, id); + startActivity(intent); } private void setListAdapter() { diff --git a/MyTracks/src/com/google/android/apps/mytracks/WelcomeActivity.java b/MyTracks/src/com/google/android/apps/mytracks/WelcomeActivity.java index 0acf875fc..45251a3b7 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/WelcomeActivity.java +++ b/MyTracks/src/com/google/android/apps/mytracks/WelcomeActivity.java @@ -20,6 +20,7 @@ import com.google.android.maps.mytracks.R; import android.app.Activity; import android.app.AlertDialog; +import android.content.DialogInterface; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; @@ -71,6 +72,12 @@ public class WelcomeActivity extends Activity { AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setView(view); builder.setPositiveButton(R.string.ok, null); + builder.setNeutralButton(R.string.license, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + Eula.showEula(WelcomeActivity.this); + } + }); builder.setIcon(R.drawable.arrow_icon); AlertDialog dialog = builder.create(); dialog.show(); 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 a9689e72f..e034ed721 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/content/MyTracksProvider.java +++ b/MyTracks/src/com/google/android/apps/mytracks/content/MyTracksProvider.java @@ -137,6 +137,7 @@ public class MyTracksProvider extends ContentProvider { @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { if (oldVersion < 17) { + // Wipe the old data. Log.w(TAG, "Upgrading database from version " + oldVersion + " to " + newVersion + ", which will destroy all old data"); db.execSQL("DROP TABLE IF EXISTS " + TRACKPOINTS_TABLE); @@ -144,11 +145,15 @@ public class MyTracksProvider extends ContentProvider { db.execSQL("DROP TABLE IF EXISTS " + WAYPOINTS_TABLE); onCreate(db); } else { - Log.w(TAG, "Upgrading database from version " + oldVersion + " to " - + newVersion + ", adding sensor column."); - if (oldVersion == 17) { + // Incremental updates go here. + // Each time you increase the DB version, add a corresponding if clause. + + // Sensor data. + if (oldVersion <= 17) { + Log.w(TAG, "Upgrading database from version " + oldVersion + " to " + + newVersion + ", adding sensor column."); db.execSQL("ALTER TABLE " + TRACKPOINTS_TABLE - + " ADD " + TrackPointsColumns.SENSOR + " BLOB"); + + " ADD " + TrackPointsColumns.SENSOR + " BLOB"); }; if (oldVersion >= 17) { db.execSQL("ALTER TABLE " + TRACKS_TABLE diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/GpxTrackWriter.java b/MyTracks/src/com/google/android/apps/mytracks/io/GpxTrackWriter.java index 6054dc9a5..6c7978b11 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/io/GpxTrackWriter.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/GpxTrackWriter.java @@ -24,6 +24,7 @@ import android.location.Location; import java.io.OutputStream; import java.io.PrintWriter; +import java.nio.charset.Charset; import java.text.SimpleDateFormat; import java.util.Date; import java.util.TimeZone; @@ -58,8 +59,8 @@ public class GpxTrackWriter implements TrackFormatWriter { @Override public void writeHeader() { if (pw != null) { - pw.println(""); + pw.format("\n", + Charset.defaultCharset().name()); pw.println(""); pw.println(" wiseWrapper = new GDataWrapper(); - wiseWrapper.setAuthManager(wiseAuth); - wiseWrapper.setRetryOnAuthFailure(true); + GDataWrapper docListWrapper = new GDataWrapper(); + docListWrapper.setAuthManager(docListAuth); + docListWrapper.setRetryOnAuthFailure(true); + + GDataWrapper trixWrapper = new GDataWrapper(); + trixWrapper.setAuthManager(trixAuth); + trixWrapper.setRetryOnAuthFailure(true); - GDataWrapper writelyWrapper = new GDataWrapper(); - writelyWrapper.setAuthManager(writelyAuth); - writelyWrapper.setRetryOnAuthFailure(true); + DocsHelper docsHelper = new DocsHelper(); GDataClient androidClient = null; try { androidClient = GDataClientFactory.getGDataClient(activity); SpreadsheetsClient gdataClient = new SpreadsheetsClient(androidClient, new XmlMapsGDataParserFactory(new AndroidXmlParserFactory())); - wiseWrapper.setClient(gdataClient); + trixWrapper.setClient(gdataClient); Log.d(MyTracksConstants.TAG, - "GData connection prepared: " + this.writelyAuth); + "GData connection prepared: " + this.docListAuth); String sheetTitle = "My Tracks"; if (track.getCategory() != null && !track.getCategory().equals("")) { @@ -219,15 +185,19 @@ public class SendToDocs { DocumentsClient docsGdataClient = new DocumentsClient(androidClient, new XmlMapsGDataParserFactory(new AndroidXmlParserFactory())); - writelyWrapper.setClient(docsGdataClient); + docListWrapper.setClient(docsGdataClient); // First try to find the spreadsheet: - if (!getSpreadsheetId(writelyWrapper, sheetTitle)) { - Log.i(MyTracksConstants.TAG, "Spreadsheet lookup failed."); + String spreadsheetId = null; + try { + spreadsheetId = docsHelper.requestSpreadsheetId(docListWrapper, + sheetTitle); + } catch (IOException e) { + Log.i(MyTracksConstants.TAG, "Spreadsheet lookup failed.", e); return false; } - if (spreadSheetId == null) { + if (spreadsheetId == null) { MyTracks.getInstance().setProgressValue(65); // Waiting a few seconds and trying again. Maybe the server just had a // hickup (unfortunately that happens quite a lot...). @@ -236,22 +206,33 @@ public class SendToDocs { } catch (InterruptedException e) { Log.e(MyTracksConstants.TAG, "Sleep interrupted", e); } - if (!getSpreadsheetId(writelyWrapper, sheetTitle)) { - Log.i(MyTracksConstants.TAG, "2nd spreadsheet lookup failed."); + + try { + spreadsheetId = docsHelper.requestSpreadsheetId(docListWrapper, + sheetTitle); + } catch (IOException e) { + Log.i(MyTracksConstants.TAG, "2nd spreadsheet lookup failed.", e); return false; } } - + + // We were unable to find an existing spreadsheet, so create a new one. MyTracks.getInstance().setProgressValue(70); - if (spreadSheetId == null) { + if (spreadsheetId == null) { Log.i(MyTracksConstants.TAG, "Creating new spreadsheet: " + sheetTitle); - if (!createSpreadSheet(writelyWrapper, sheetTitle)) { + try { + spreadsheetId = docsHelper.createSpreadsheet(activity, docListWrapper, + sheetTitle); + } catch (IOException e) { + Log.i(MyTracksConstants.TAG, "Failed to create new spreadsheet " + + sheetTitle, e); return false; } MyTracks.getInstance().setProgressValue(80); + createdNewSpreadSheet = true; - if (spreadSheetId == null) { + if (spreadsheetId == null) { MyTracks.getInstance().setProgressValue(85); // The previous creation might have succeeded even though GData // reported an error. Seems to be a know bug, @@ -264,10 +245,16 @@ public class SendToDocs { } catch (InterruptedException e) { Log.e(MyTracksConstants.TAG, "Sleep interrupted", e); } - if (!getSpreadsheetId(writelyWrapper, sheetTitle)) { + + try { + spreadsheetId = docsHelper.requestSpreadsheetId(docListWrapper, + sheetTitle); + } catch (IOException e) { + Log.i(MyTracksConstants.TAG, "Failed create-failed lookup", e); return false; } - if (spreadSheetId == null) { + + if (spreadsheetId == null) { MyTracks.getInstance().setProgressValue(87); // Re-try try { @@ -275,11 +262,16 @@ public class SendToDocs { } catch (InterruptedException e) { Log.e(MyTracksConstants.TAG, "Sleep interrupted", e); } - if (!getSpreadsheetId(writelyWrapper, sheetTitle)) { + + try { + spreadsheetId = docsHelper.requestSpreadsheetId(docListWrapper, + sheetTitle); + } catch (IOException e) { + Log.i(MyTracksConstants.TAG, "Failed create-failed relookup", e); return false; } } - if (spreadSheetId == null) { + if (spreadsheetId == null) { Log.i(MyTracksConstants.TAG, "Creating new spreadsheet really failed."); return false; @@ -287,14 +279,21 @@ public class SendToDocs { } } - if (!getWorkSheetId(wiseWrapper)) { - Log.i(MyTracksConstants.TAG, "Looking up worksheet id failed."); + String worksheetId = null; + try { + worksheetId = docsHelper.getWorksheetId(trixWrapper, spreadsheetId); + if (worksheetId == null) { + throw new IOException("Worksheet ID lookup returned empty"); + } + } catch (IOException e) { + Log.i(MyTracksConstants.TAG, "Looking up worksheet id failed.", e); return false; } MyTracks.getInstance().setProgressValue(90); - insertRowNet(track, spreadSheetId, workSheetId); + docsHelper.addTrackRow(activity, trixAuth, spreadsheetId, worksheetId, + track, metricUnits); Log.i(MyTracksConstants.TAG, "Done uploading to docs."); } catch (IOException e) { Log.e(MyTracksConstants.TAG, "Unable to upload docs.", e); @@ -306,235 +305,4 @@ public class SendToDocs { } return true; } - - private boolean getSpreadsheetId(GDataWrapper wrapper, final String title) { - spreadSheetId = null; - return wrapper.runQuery(new QueryFunction() { - @Override - public void query(GDataServiceClient client) - throws IOException, GDataWrapper.ParseException, GDataWrapper.HttpException { - GDataParser listParser; - try { - listParser = client.getParserForFeed(Entry.class, - DOCS_MY_SPREADSHEETS_FEED_URL, writelyAuth.getAuthToken()); - listParser.init(); - while (listParser.hasMoreData()) { - Entry entry = listParser.readNextEntry(null); - String entryTitle = entry.getTitle(); - Log.i(MyTracksConstants.TAG, "Found docs entry: " + entryTitle); - if (entryTitle.equals(title)) { - String entryId = entry.getId(); - int lastSlash = entryId.lastIndexOf('/'); - spreadSheetId = entryId.substring(lastSlash + 15); - break; - } - } - } catch (ParseException e) { - throw new GDataWrapper.ParseException(e); - } catch (HttpException e) { - throw new GDataWrapper.HttpException(e.getStatusCode(), e.getMessage()); - } - } - }); - } - - private boolean getWorkSheetId(GDataWrapper wrapper) { - workSheetId = null; - return wrapper.runQuery(new QueryFunction() { - @Override - public void query(GDataServiceClient client) - throws GDataWrapper.AuthenticationException, IOException, GDataWrapper.ParseException { - String uri = String.format(DOCS_WORKSHEETS_URL_FORMAT, spreadSheetId); - GDataParser sheetParser; - try { - sheetParser = ((SpreadsheetsClient) client).getParserForWorksheetsFeed(uri, - wiseAuth.getAuthToken()); - sheetParser.init(); - if (!sheetParser.hasMoreData()) { - Log.i(MyTracksConstants.TAG, "Found no worksheets"); - return; // failure - } - - // just grab the first. - WorksheetEntry worksheetEntry = - (WorksheetEntry) sheetParser.readNextEntry(new WorksheetEntry()); - - int lastSlash = worksheetEntry.getId().lastIndexOf('/'); - workSheetId = worksheetEntry.getId().substring(lastSlash + 1); - - } catch (AuthenticationException e) { - throw new GDataWrapper.AuthenticationException(e); - } catch (ParseException e) { - throw new GDataWrapper.ParseException(e); - } - } - }); - } - - /** - * Creates a new MyTracks spreadsheet with the given name. - */ - private boolean createSpreadSheet(GDataWrapper writelyWrapper, - final String name) { - spreadSheetId = null; - - return writelyWrapper.runQuery(new QueryFunction() { - @Override - public void query(GDataServiceClient client) throws IOException { - // Construct data - // Send data - URL url = new URL(DOCS_FEED_URL); - URLConnection conn = url.openConnection(); - conn.addRequestProperty(CONTENT_TYPE_PARAM, - OPENDOCUMENT_SPREADSHEET_MIME_TYPE); - conn.addRequestProperty("Slug", name); - conn.addRequestProperty("Authorization", - "GoogleLogin auth=" + writelyAuth.getAuthToken()); - conn.setDoOutput(true); - OutputStream os = conn.getOutputStream(); - ResourceUtils.readBinaryFileToOutputStream( - activity, R.raw.mytracks_empty_spreadsheet, os); - - // Get the response - // TODO: The following is a horrible ugly hack. - // Hopefully we can retire it when there is a proper gdata api. - BufferedReader rd = null; - String line; - StringBuilder resultBuilder = new StringBuilder(); - try { - rd = new BufferedReader(new InputStreamReader(conn.getInputStream())); - while ((line = rd.readLine()) != null) { - resultBuilder.append(line); - } - os.close(); - rd.close(); - } catch (FileNotFoundException e) { - // The GData API sometimes throws an error, even though creation of - // the document succeeded. In that case let's just return. The caller - // then needs to check if the doc actually exists. - return; - } finally { - os.close(); - if (rd != null) { - rd.close(); - } - } - - String result = resultBuilder.toString(); - // Try to find the id. - int idTagIndex = result.indexOf(""); - if (idTagIndex == -1) { - return; - } - int idTagCloseIndex = result.indexOf("", idTagIndex); - if (idTagCloseIndex == -1) { - return; - } - int idStringStart = result.indexOf(DOCS_SPREADSHEET_URL, idTagIndex); - if (idStringStart == -1) { - return; - } - spreadSheetId = result.substring( - idStringStart + DOCS_SPREADSHEET_URL.length(), idTagCloseIndex); - Log.i(MyTracksConstants.TAG, "Created new spreadsheet: " + spreadSheetId); - }}); - } - - private void insertRowNet(Track track, String spreadsheetId, - String worksheetId) throws IOException { - String worksheetUri = String.format(DOCS_SPREADSHEET_URL_FORMAT, - spreadsheetId, worksheetId); - TripStatistics stats = track.getStatistics(); - - /* Prepare the Post-Text we are going to send. */ - StringBuilder sb = new StringBuilder(); - sb.append(""); - appendTag("name", track.getName(), sb); - appendTag("description", track.getDescription(), sb); - appendTag("date", String.format("%tc", stats.getStartTime()), sb); - appendTag("totaltime", StringUtils.formatTimeAlwaysShowingHours( - stats.getTotalTime()), sb); - appendTag("movingtime", StringUtils.formatTimeAlwaysShowingHours( - stats.getMovingTime()), sb); - appendLargeUnitsTag("distance", stats.getTotalDistance() / 1000, sb); - appendTag("distanceunit", - metricUnits - ? activity.getString(R.string.kilometer) - : activity.getString(R.string.mile), - sb); - appendLargeUnitsTag("averagespeed", stats.getAverageSpeed() * 3.6, sb); - appendLargeUnitsTag("averagemovingspeed", - stats.getAverageMovingSpeed() * 3.6, sb); - appendLargeUnitsTag("maxspeed", stats.getMaxSpeed() * 3.6, sb); - appendTag("speedunit", - metricUnits - ? activity.getString(R.string.kilometer_per_hour) - : activity.getString(R.string.mile_per_hour), - sb); - appendSmallUnitsTag("elevationgain", stats.getTotalElevationGain(), sb); - appendSmallUnitsTag("minelevation", stats.getMinElevation(), sb); - appendSmallUnitsTag("maxelevation", stats.getMaxElevation(), sb); - appendTag("elevationunit", - metricUnits - ? activity.getString(R.string.meter) - : activity.getString(R.string.feet), - sb); - if (track.getMapId().length() > 0) { - appendTag("map", SendToFusionTables.getMapVisualizationUrl(track), sb); - } - sb.append(""); - Log.i(MyTracksConstants.TAG, - "Inserting at: " + spreadsheetId + " => " + worksheetUri); - - String postText = sb.toString(); - Log.i(MyTracksConstants.TAG, postText); - - // Send data - // No need for a wrapper because we know that the authorization was good - // enough to get this far - URL url = new URL(worksheetUri); - URLConnection conn = url.openConnection(); - conn.addRequestProperty(CONTENT_TYPE_PARAM, ATOM_FEED_MIME_TYPE); - conn.addRequestProperty("Authorization", - "GoogleLogin auth=" + wiseAuth.getAuthToken()); - conn.setDoOutput(true); - OutputStreamWriter wr = new OutputStreamWriter(conn.getOutputStream()); - wr.write(postText); - wr.flush(); - - // Get the response - BufferedReader rd = - new BufferedReader(new InputStreamReader(conn.getInputStream())); - String line; - while ((line = rd.readLine()) != null) { - // Process line... - Log.i(MyTracksConstants.TAG, "r: " + line); - } - wr.close(); - rd.close(); - - Log.i(MyTracksConstants.TAG, "Post finished."); - } - - private void appendTag(String name, String value, StringBuilder sb) { - sb.append("'); - sb.append(StringUtils.stringAsCData(value)); - sb.append("'); - } - - private void appendLargeUnitsTag(String name, double d, StringBuilder sb) { - double value = metricUnits ? d : (d * UnitConversions.KM_TO_MI); - appendTag(name, LARGE_UNIT_FORMAT.format(value), sb); - } - - private void appendSmallUnitsTag(String name, double d, StringBuilder sb) { - double value = metricUnits ? d : (d * UnitConversions.M_TO_FT); - appendTag(name, SMALL_UNIT_FORMAT.format(value), sb); - } } diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/TcxTrackWriter.java b/MyTracks/src/com/google/android/apps/mytracks/io/TcxTrackWriter.java index a09e2fd60..dfd3513ad 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/io/TcxTrackWriter.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/TcxTrackWriter.java @@ -27,6 +27,7 @@ import android.os.Build; import java.io.OutputStream; import java.io.PrintWriter; +import java.nio.charset.Charset; import java.text.SimpleDateFormat; import java.util.Date; import java.util.TimeZone; @@ -77,7 +78,8 @@ public class TcxTrackWriter implements TrackFormatWriter { if (pw == null) { return; } - pw.println(""); + pw.format("\n", + Charset.defaultCharset().name()); pw.print(" docListWrapper, final String name) throws IOException { + final AtomicReference idSaver = new AtomicReference(); + + boolean result = docListWrapper.runQuery(new QueryFunction() { + @Override + public void query(GDataServiceClient client) throws IOException { + // Construct and send request + URL url = new URL(DOCS_FEED_URL); + URLConnection conn = url.openConnection(); + conn.addRequestProperty(CONTENT_TYPE_PARAM, + OPENDOCUMENT_SPREADSHEET_MIME_TYPE); + conn.addRequestProperty("Slug", name); + conn.addRequestProperty("Authorization", + "GoogleLogin auth=" + + docListWrapper.getAuthManager().getAuthToken()); + conn.setDoOutput(true); + OutputStream os = conn.getOutputStream(); + ResourceUtils.readBinaryFileToOutputStream( + context, R.raw.mytracks_empty_spreadsheet, os); + + // Get the response + // TODO: The following is a horrible ugly hack. + // Hopefully we can retire it when there is a proper gdata api. + BufferedReader rd = null; + String line; + StringBuilder resultBuilder = new StringBuilder(); + try { + rd = new BufferedReader(new InputStreamReader(conn.getInputStream())); + while ((line = rd.readLine()) != null) { + resultBuilder.append(line); + } + os.close(); + rd.close(); + } catch (FileNotFoundException e) { + // The GData API sometimes throws an error, even though creation of + // the document succeeded. In that case let's just return. The caller + // then needs to check if the doc actually exists. + return; + } finally { + os.close(); + if (rd != null) { + rd.close(); + } + } + + String result = resultBuilder.toString(); + // Try to find the id. + int idTagIndex = result.indexOf(""); + if (idTagIndex == -1) { + return; + } + int idTagCloseIndex = result.indexOf("", idTagIndex); + if (idTagCloseIndex == -1) { + return; + } + int idStringStart = result.indexOf(DOCS_SPREADSHEET_URL, idTagIndex); + if (idStringStart == -1) { + return; + } + + String id = result.substring( + idStringStart + DOCS_SPREADSHEET_URL.length(), idTagCloseIndex); + Log.i(MyTracksConstants.TAG, "Created new spreadsheet: " + id); + idSaver.set(id); + }}); + + if (!result) { + throw newIOException(docListWrapper, + "Failed to create new spreadsheet."); + } + + return idSaver.get(); + } + + /** + * Retrieve the ID of a spreadsheet with the given name. + * + * @param docListWrapper The GData handle for the Document List service. + * @param title The name of the spreadsheet whose ID is to be retrieved. + * @return The spreadsheet ID, if it can be retrieved. {@code null} will + * be returned if no spreadsheet exists by the given name. + * @throws IOException If an error occurs during the GData request. + */ + public String requestSpreadsheetId(final GDataWrapper docListWrapper, + final String title) throws IOException { + final AtomicReference idSaver = new AtomicReference(); + + boolean result = docListWrapper.runQuery(new QueryFunction() { + @Override + public void query(GDataServiceClient client) + throws IOException, GDataWrapper.ParseException, GDataWrapper.HttpException { + GDataParser listParser; + try { + listParser = client.getParserForFeed(Entry.class, + DOCS_MY_SPREADSHEETS_FEED_URL, + docListWrapper.getAuthManager().getAuthToken()); + listParser.init(); + + while (listParser.hasMoreData()) { + Entry entry = listParser.readNextEntry(null); + String entryTitle = entry.getTitle(); + Log.i(MyTracksConstants.TAG, "Found docs entry: " + entryTitle); + if (entryTitle.equals(title)) { + String entryId = entry.getId(); + int lastSlash = entryId.lastIndexOf('/'); + idSaver.set(entryId.substring(lastSlash + 15)); + break; + } + } + } catch (ParseException e) { + throw new GDataWrapper.ParseException(e); + } catch (HttpException e) { + throw new GDataWrapper.HttpException(e.getStatusCode(), e.getMessage()); + } + } + }); + + if (!result) { + throw newIOException(docListWrapper, + "Failed to retrieve spreadsheet list."); + } + + return idSaver.get(); + } + + /** + * Retrieve the ID of the first worksheet in the named spreadsheet. + * + * @param trixWrapper The GData handle for the spreadsheet service. + * @param spreadsheetId The GData ID for the given spreadsheet. + * @return The worksheet ID, if it can be retrieved. {@code null} will be + * returned if the GData request returns without error, but without an + * ID. + * @throws IOException If an error occurs during the GData request. + */ + public String getWorksheetId(final GDataWrapper trixWrapper, + final String spreadsheetId) throws IOException { + final AtomicReference idSaver = new AtomicReference(); + + boolean result = trixWrapper.runQuery(new QueryFunction() { + @Override + public void query(GDataServiceClient client) + throws GDataWrapper.AuthenticationException, IOException, GDataWrapper.ParseException, GDataWrapper.HttpException { + String uri = String.format(DOCS_WORKSHEETS_URL_FORMAT, spreadsheetId); + GDataParser sheetParser; + try { + sheetParser = ((SpreadsheetsClient) client).getParserForWorksheetsFeed(uri, + trixWrapper.getAuthManager().getAuthToken()); + sheetParser.init(); + if (!sheetParser.hasMoreData()) { + Log.i(MyTracksConstants.TAG, "Found no worksheets"); + return; + } + + // Grab the first worksheet. + WorksheetEntry worksheetEntry = + (WorksheetEntry) sheetParser.readNextEntry(new WorksheetEntry()); + + int lastSlash = worksheetEntry.getId().lastIndexOf('/'); + idSaver.set(worksheetEntry.getId().substring(lastSlash + 1)); + } catch (ParseException e) { + throw new GDataWrapper.ParseException(e); + } catch (AuthenticationException e) { + throw new GDataWrapper.AuthenticationException(e); + } + } + }); + + if (!result) { + throw newIOException(trixWrapper, "Failed to retrieve worksheet ID."); + } + + return idSaver.get(); + } + + /** + * Add a row to a worksheet containing the stats for a given track. + * + * @param context The context associated with this request. + * @param trixAuth The GData authorization for the spreadsheet service. + * @param spreadsheetId The spreadsheet to be modified. + * @param worksheetId The worksheet to be modified. + * @param track The track whose stats are to be written. + * @param metricUnits True if metric units are to be used. If false, + * imperial units will be used. + * @throws IOException If an error occurs while updating the worksheet. + */ + public void addTrackRow(Context context, AuthManager trixAuth, + String spreadsheetId, String worksheetId, Track track, + boolean metricUnits) throws IOException { + + String worksheetUri = String.format(DOCS_SPREADSHEET_URL_FORMAT, + spreadsheetId, worksheetId); + TripStatistics stats = track.getStatistics(); + + String distanceUnit = context.getString(metricUnits ? + R.string.kilometer : R.string.mile); + String speedUnit = context.getString(metricUnits ? + R.string.kilometer_per_hour : R.string.mile_per_hour); + String elevationUnit = context.getString(metricUnits ? + R.string.meter : R.string.feet); + + // Prepare the Post-Text we are going to send. + DocsTagBuilder tagBuilder = new DocsTagBuilder(metricUnits) + .append("name", track.getName()) + .append("description", track.getDescription()) + .append("date", String.format("%tc", stats.getStartTime())) + .append("totaltime", StringUtils.formatTimeAlwaysShowingHours( + stats.getTotalTime())) + .append("movingtime", StringUtils.formatTimeAlwaysShowingHours( + stats.getMovingTime())) + .appendLargeUnits("distance", stats.getTotalDistance() / 1000) + .append("distanceunit", distanceUnit) + .appendLargeUnits("averagespeed", stats.getAverageSpeed() * 3.6) + .appendLargeUnits("averagemovingspeed", + stats.getAverageMovingSpeed() * 3.6) + .appendLargeUnits("maxspeed", stats.getMaxSpeed() * 3.6) + .append("speedunit", speedUnit) + .appendSmallUnits("elevationgain", stats.getTotalElevationGain()) + .appendSmallUnits("minelevation", stats.getMinElevation()) + .appendSmallUnits("maxelevation", stats.getMaxElevation()) + .append("elevationunit", elevationUnit); + + if (track.getMapId().length() > 0) { + tagBuilder.append("map", String.format("%s?msa=0&msid=%s", + MyTracksConstants.MAPSHOP_BASE_URL, track.getMapId())); + } + + String postText = new StringBuilder() + .append("") + .append(tagBuilder.build()) + .append("") + .toString(); + + Log.i(MyTracksConstants.TAG, + "Inserting at: " + spreadsheetId + " => " + worksheetUri); + + Log.i(MyTracksConstants.TAG, postText); + + writeRowData(trixAuth, worksheetUri, postText); + + Log.i(MyTracksConstants.TAG, "Post finished."); + } + + /** + * Writes spreadsheet row data to the indicated worksheet. + * + * @param trixAuth The GData authorization for the spreadsheet service. + * @param worksheetUri The URI of the worksheet to be altered. + * @param postText The XML tags describing the change to be made. + * @throws IOException Thrown if an error occurs during the write. + */ + protected void writeRowData(AuthManager trixAuth, String worksheetUri, + String postText) throws IOException { + // No need for a wrapper because we know that the authorization was good + // enough to get this far. + URL url = new URL(worksheetUri); + URLConnection conn = url.openConnection(); + conn.addRequestProperty(CONTENT_TYPE_PARAM, ATOM_FEED_MIME_TYPE); + conn.addRequestProperty("Authorization", + "GoogleLogin auth=" + trixAuth.getAuthToken()); + conn.setDoOutput(true); + OutputStreamWriter wr = new OutputStreamWriter(conn.getOutputStream()); + wr.write(postText); + wr.flush(); + + // Get the response. + // TODO: Should we parse the response, rather than simply throwing it away? + BufferedReader rd = + new BufferedReader(new InputStreamReader(conn.getInputStream())); + String line; + while ((line = rd.readLine()) != null) { + // Process line. + Log.i(MyTracksConstants.TAG, "r: " + line); + } + wr.close(); + rd.close(); + } + + private static IOException newIOException(GDataWrapper wrapper, + String message) { + return new IOException(String.format("%s: %d: %s", message, + wrapper.getErrorType(), wrapper.getErrorMessage())); + } +} diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/docs/DocsTagBuilder.java b/MyTracks/src/com/google/android/apps/mytracks/io/docs/DocsTagBuilder.java new file mode 100644 index 000000000..50ab27057 --- /dev/null +++ b/MyTracks/src/com/google/android/apps/mytracks/io/docs/DocsTagBuilder.java @@ -0,0 +1,113 @@ +/* + * 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.docs; + +import com.google.android.apps.mytracks.util.StringUtils; +import com.google.android.apps.mytracks.util.UnitConversions; + +import java.text.DecimalFormat; +import java.text.NumberFormat; + +/** + *

This class builds a string of XML tags used to talk to Docs using GData. + * + *

Sample Usage: + * + * + * String tags = new DocsTagBuilder(false) + * .append("tagName", "tagValue") + * .appendLargeUnits("bigTagName", 1.0) + * .build(); + * + * + *

results in: + * + * + * + * + * + * + * @author Matthew Simmons + */ +class DocsTagBuilder { + // TODO(simmonmt): These formats aren't I18N-compatible. Not everyone uses + // commas for thousands and dots for the decimal point. + private static final NumberFormat LARGE_UNIT_FORMAT = + new DecimalFormat("#,###,###.00"); + private static final NumberFormat SMALL_UNIT_FORMAT = + new DecimalFormat("###,###"); + + protected final boolean metricUnits; + protected final StringBuilder stringBuilder; + + /** + * @param metricUnits True if metric units are to be used. If false, + * imperial units will be used. + */ + DocsTagBuilder(boolean metricUnits) { + this.metricUnits = metricUnits; + stringBuilder = new StringBuilder(); + } + + /** Appends a tag containing a string value */ + DocsTagBuilder append(String name, String value) { + appendTag(name, value); + return this; + } + + /** + * Appends a tag containing a numeric value. The value will be formatted + * according to the large distance of the specified measurement system (i.e. + * kilometers or miles). + * + * @param name The tag name. + * @param d The value to be formatted, in kilometers. + */ + DocsTagBuilder appendLargeUnits(String name, double d) { + double value = metricUnits ? d : (d * UnitConversions.KM_TO_MI); + appendTag(name, LARGE_UNIT_FORMAT.format(value)); + return this; + } + + /** + * Appends a tag containing a numeric value. The value will be formatted + * according to the small distance of the specified measurement system (i.e. + * meters or feet). + * + * @param name The tag name. + * @param d The value to be formatted, in meters. + */ + DocsTagBuilder appendSmallUnits(String name, double d) { + double value = metricUnits ? d : (d * UnitConversions.M_TO_FT); + appendTag(name, SMALL_UNIT_FORMAT.format(value)); + return this; + } + + /** Returns a string containing all tags which have been added. */ + String build() { + return stringBuilder.toString(); + } + + private void appendTag(String name, String value) { + stringBuilder.append("") + .append(StringUtils.stringAsCData(value)) + .append(""); + } +} 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 48de71ebe..f56497876 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 @@ -289,6 +289,10 @@ public class GDataWrapper { this.auth = auth; } + public AuthManager getAuthManager() { + return auth; + } + public void setRetryOnAuthFailure(boolean retry) { retryOnAuthFailure = retry; } diff --git a/MyTracks/src/com/google/android/apps/mytracks/services/SplitManager.java b/MyTracks/src/com/google/android/apps/mytracks/services/SplitManager.java index e8a92bfc7..f63e87531 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/services/SplitManager.java +++ b/MyTracks/src/com/google/android/apps/mytracks/services/SplitManager.java @@ -16,6 +16,7 @@ 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; @@ -111,7 +112,7 @@ public class SplitManager { } if (distance > this.nextSplitDistance) { - service.insertStatisticsMarker(service.getLastLocation()); + service.insertWaypoint(WaypointCreationRequest.DEFAULT_STATISTICS); calculateNextSplit(); } } diff --git a/MyTracks/src/com/google/android/apps/mytracks/services/TimeSplitTask.java b/MyTracks/src/com/google/android/apps/mytracks/services/TimeSplitTask.java index 9535ad4b0..333ce6ae4 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/services/TimeSplitTask.java +++ b/MyTracks/src/com/google/android/apps/mytracks/services/TimeSplitTask.java @@ -16,6 +16,9 @@ package com.google.android.apps.mytracks.services; +import com.google.android.apps.mytracks.content.WaypointCreationRequest; + + /** * A simple task to insert statistics markers every n minutes. * @author Sandor Dornbush @@ -24,7 +27,7 @@ public class TimeSplitTask implements PeriodicTask { @Override public void run(TrackRecordingService service) { - service.insertStatisticsMarker(service.getLastLocation()); + service.insertWaypoint(WaypointCreationRequest.DEFAULT_STATISTICS); } @Override 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 b3610bd44..f44823bce 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/services/TrackRecordingService.java +++ b/MyTracks/src/com/google/android/apps/mytracks/services/TrackRecordingService.java @@ -26,6 +26,7 @@ import com.google.android.apps.mytracks.content.Sensor; 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.WaypointCreationRequest; 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; @@ -70,9 +71,6 @@ import java.util.TimerTask; */ public class TrackRecordingService extends Service implements LocationListener { - private static final String STATISTICS_ICON_URL = - "http://maps.google.com/mapfiles/ms/micons/ylw-pushpin.png"; - static final int MAX_AUTO_RESUME_TRACK_RETRY_ATTEMPTS = 3; private NotificationManager notificationManager; @@ -429,7 +427,7 @@ public class TrackRecordingService extends Service implements LocationListener { lastValidLocation = null; Waypoint waypoint = providerUtils.getFirstWaypoint(recordingTrackId); - if (waypoint != null) { + if (waypoint != null && waypoint.getStatistics() != null) { currentWaypointId = waypoint.getId(); waypointStatsBuilder = new TripStatisticsBuilder( waypoint.getStatistics()); @@ -646,7 +644,6 @@ public class TrackRecordingService extends Service implements LocationListener { locationManager = (LocationManager) getSystemService(LOCATION_SERVICE); splitManager = new SplitManager(this); - sensorManager = SensorManagerFactory.getSensorManager(this); prefManager = new PreferenceManager(this); registerLocationListener(); @@ -733,7 +730,7 @@ public class TrackRecordingService extends Service implements LocationListener { splitManager.shutdown(); splitManager = null; if (sensorManager != null) { - sensorManager.onDestroy(); + sensorManager.shutdown(); sensorManager = null; } @@ -860,50 +857,62 @@ public class TrackRecordingService extends Service implements LocationListener { return isRecording; } - public long insertWaypointMarker(Waypoint waypoint) { + public long insertWaypoint(WaypointCreationRequest request) { if (!isRecording()) { throw new IllegalStateException( "Unable to insert waypoint marker while not recording!"); } - - if (waypoint.getLocation() == null) { - if (lastValidLocation == null) { - Log.w(MyTracksConstants.TAG, "Cannot insert waypoint with no location"); - return -1; - } - - waypoint.setLocation(lastValidLocation); + if (request == null) { + request = WaypointCreationRequest.DEFAULT_MARKER; } - - if (waypoint.getTrackId() < 0) { - waypoint.setTrackId(recordingTrackId); + Waypoint wpt = new Waypoint(); + switch (request.getType()) { + case MARKER: + buildMarker(wpt, request); + break; + case STATISTICS: + buildStatisticsMarker(wpt); + break; } - - waypoint.setLength(length); - waypoint.setDuration(waypoint.getLocation().getTime() - - statsBuilder.getStatistics().getStartTime()); - Uri uri = providerUtils.insertWaypoint(waypoint); + wpt.setTrackId(recordingTrackId); + wpt.setLength(length); + if (lastValidLocation != null) { + // A null location is ok, and expected on track start. + wpt.setLocation(lastLocation); + wpt.setDuration(lastLocation.getTime() + - statsBuilder.getStatistics().getStartTime()); + } + Uri uri = providerUtils.insertWaypoint(wpt); return Long.parseLong(uri.getLastPathSegment()); } + private void buildMarker(Waypoint wpt, WaypointCreationRequest request) { + wpt.setType(Waypoint.TYPE_WAYPOINT); + if (request.getIconUrl() == null) { + wpt.setIcon(getString(R.string.waypoint_icon_url)); + } else { + wpt.setIcon(request.getIconUrl()); + } + if (request.getName() == null) { + wpt.setName(getString(R.string.waypoint)); + } else { + wpt.setName(request.getName()); + } + if (request.getDescription() != null) { + wpt.setDescription(request.getDescription()); + } + } + /** - * Inserts a statistics marker. A statistics marker holds the stats for the - * last segment up to this marker. + * Build a statistics marker. + * A statistics marker holds the stats for the* last segment up to this marker. * - * @param location the location where to insert + * @param Waypoint The waypoint which will be populated with stats data. * @return the unique id of the inserted marker */ - public long insertStatisticsMarker(Location location) { - if (!isRecording()) { - throw new IllegalStateException( - "Unable to insert statistics marker while not recording!"); - } - + private void buildStatisticsMarker(Waypoint waypoint) { StringUtils utils = new StringUtils(TrackRecordingService.this); - // Create a new waypoint to save - Waypoint waypoint = new Waypoint(); - // Set stop and total time in the stats data final long time = System.currentTimeMillis(); waypointStatsBuilder.pauseAt(time); @@ -913,24 +922,18 @@ public class TrackRecordingService extends Service implements LocationListener { waypoint.setDuration(time - statsBuilder.getStatistics().getStartTime()); // Set the rest of the waypoint data - waypoint.setTrackId(recordingTrackId); waypoint.setType(Waypoint.TYPE_STATISTICS); - waypoint.setName(TrackRecordingService.this.getString(R.string.statistics)); + waypoint.setName(getString(R.string.statistics)); waypoint.setStatistics(waypointStatsBuilder.getStatistics()); waypoint.setDescription(utils.generateWaypointDescription(waypoint)); - waypoint.setLocation(location); - waypoint.setIcon(STATISTICS_ICON_URL); - waypoint.setLength(length); + waypoint.setIcon(getString(R.string.stats_icon_url)); waypoint.setStartId(providerUtils.getLastLocationId(recordingTrackId)); - Uri uri = providerUtils.insertWaypoint(waypoint); - // Create a new stats keeper for the next marker + // Create a new stats keeper for the next marker. waypointStatsBuilder = new TripStatisticsBuilder(time); - updateCurrentWaypoint(); - return Long.parseLong(uri.getLastPathSegment()); } - + private ServiceBinder binder = new ServiceBinder(this); /** @@ -961,79 +964,54 @@ public class TrackRecordingService extends Service implements LocationListener { @Override public boolean isRecording() { + checkService(); + return service.isRecording(); + } + + private void checkService() { if (service == null) { throw new IllegalStateException("The service has been already detached!"); } - return service.isRecording(); } @Override public long getRecordingTrackId() { - if (service == null) { - throw new IllegalStateException("The service has been already detached!"); - } + checkService(); return service.recordingTrackId; } @Override public boolean hasRecorded() { - if (service == null) { - throw new IllegalStateException("The service has been already detached!"); - } + checkService(); return service.providerUtils.getLastTrackId() >= 0; } @Override public long startNewTrack() { - if (service == null) { - throw new IllegalStateException("The service has been already detached!"); - } + checkService(); return service.startNewTrack(); } /** - * Insert the given waypoint marker. Users can insert waypoint markers - * to tag locations with a name, description, category etc. + * Inserts a waypoint marker in the track being recorded. * - * @param waypoint a waypoint - * @return the unique id of the inserted marker + * @param request Details of the waypoint to insert + * @return the unique ID of the inserted marker */ - @Override - public long insertWaypointMarker(Waypoint waypoint) { - if (service == null) { - throw new IllegalStateException("The service has been already detached!"); - } - return service.insertWaypointMarker(waypoint); - } - - /** - * Insert a statistics marker. A statistics marker holds the stats for - * the last segment up to this marker. - * - * @param location the location where to insert - * @return the unique id of the inserted marker - */ - @Override - public long insertStatisticsMarker(Location location) { - if (service == null) { - throw new IllegalStateException("The service has been already detached!"); - } - return service.insertStatisticsMarker(location); + public long insertWaypoint(WaypointCreationRequest request) { + checkService(); + return service.insertWaypoint(request); } @Override public void endCurrentTrack() { - if (service == null) { - throw new IllegalStateException("The service has been already detached!"); - } + checkService(); service.endCurrentTrack(); } @Override public void deleteAllTracks() { - if (service == null) { - throw new IllegalStateException("The service has been already detached!"); - } + checkService(); if (isRecording()) { throw new IllegalStateException("Cannot delete all tracks while recording!"); } @@ -1042,14 +1020,13 @@ public class TrackRecordingService extends Service implements LocationListener { @Override public void recordLocation(Location loc) { - if (service == null) { - throw new IllegalStateException("The service has been already detached!"); - } + checkService(); service.onLocationChanged(loc); } @Override public byte[] getSensorData() { + checkService(); if (service.sensorManager == null) { Log.d(MyTracksConstants.TAG, "No sensor manager for data."); return null; @@ -1063,6 +1040,7 @@ public class TrackRecordingService extends Service implements LocationListener { @Override public int getSensorState() { + checkService(); if (service.sensorManager == null) { Log.d(MyTracksConstants.TAG, "No sensor manager for data."); return Sensor.SensorState.NONE.getNumber(); @@ -1095,12 +1073,13 @@ public class TrackRecordingService extends Service implements LocationListener { providerUtils.updateTrack(track); statsBuilder = new TripStatisticsBuilder(startTime); waypointStatsBuilder = new TripStatisticsBuilder(startTime); - currentWaypointId = insertStatisticsMarker(null); + currentWaypointId = insertWaypoint(WaypointCreationRequest.DEFAULT_STATISTICS); setUpAnnouncer(); length = 0; showNotification(); registerLocationListener(); splitManager.restore(); + sensorManager = SensorManagerFactory.getSensorManager(this); if (sensorManager != null) { sensorManager.onStartTrack(); } @@ -1149,6 +1128,7 @@ public class TrackRecordingService extends Service implements LocationListener { if (sensorManager != null) { sensorManager.shutdown(); + sensorManager = null; } releaseWakeLock(); diff --git a/MyTracks/src/com/google/android/apps/mytracks/services/sensors/AntDirectSensorManager.java b/MyTracks/src/com/google/android/apps/mytracks/services/sensors/AntDirectSensorManager.java new file mode 100644 index 000000000..a26d0e8cd --- /dev/null +++ b/MyTracks/src/com/google/android/apps/mytracks/services/sensors/AntDirectSensorManager.java @@ -0,0 +1,169 @@ +/* + * 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.android.apps.mytracks.services.sensors; + +import static com.google.android.apps.mytracks.MyTracksConstants.TAG; +import com.google.android.apps.mytracks.MyTracksSettings; +import com.google.android.apps.mytracks.content.Sensor; +import com.google.android.maps.mytracks.R; + +import com.dsi.ant.AntDefine; +import com.dsi.ant.AntMesg; +import android.content.Context; +import android.content.SharedPreferences; +import android.util.Log; + +/** + * A sensor manager to connect ANT+ sensors. + * This can include heart rate monitors. + * + * @author Sandor Dornbush + */ +public class AntDirectSensorManager extends AntSensorManager { + + /* + * These constants are defined by the ANT+ heart rate monitor spec. + */ + public static final byte HRM_CHANNEL = 0; + public static final byte NETWORK_NUMBER = 1; + public static final byte HEART_RATE_DEVICE_TYPE = 120; + public static final byte POWER_DEVICE_TYPE = 11; + public static final byte MANUFACTURER_ID = 1; + public static final short CHANNEL_PERIOD = 8070; + public static final byte RF_FREQUENCY = 57; + + private short deviceNumberHRM; + + public AntDirectSensorManager(Context context) { + super(context); + + deviceNumberHRM = WILDCARD; + + // First read the the device id that we will be pairing with. + SharedPreferences prefs = context.getSharedPreferences( + MyTracksSettings.SETTINGS_NAME, 0); + if (prefs != null) { + deviceNumberHRM = + (short) prefs.getInt(context.getString(R.string.ant_heart_rate_sensor_id_key), 0); + } + Log.i(TAG, "Pairing with heart rate monitor: " + deviceNumberHRM); + } + + @Override + public void handleMessage(byte[] antMessage) { + int channel = + antMessage[AntMesg.MESG_DATA_OFFSET] & AntDefine.CHANNEL_NUMBER_MASK; + switch (channel) { + case HRM_CHANNEL: + antDecodeHRM(antMessage); + break; + default: + Log.d(TAG, "Unhandled message: " + channel); + } + } + + /** + * Decode an ant heart rate monitor message. + * @param antMessage The byte array received from the heart rate monitor. + */ + private void antDecodeHRM(byte[] antMessage) { + // message ID == broadcast + switch (antMessage[AntMesg.MESG_ID_OFFSET]) { + case AntMesg.MESG_BROADCAST_DATA_ID: + handleBroadcastData(antMessage); + break; + case AntMesg.MESG_RESPONSE_EVENT_ID: + handleMessageResponse(antMessage); + break; + case AntMesg.MESG_CHANNEL_ID_ID: + handleChannelId(antMessage); + break; + default: + Log.e(TAG, "Unexpected message id: " + antMessage[3]); + } + } + + private void handleBroadcastData(byte[] antMessage) { + if (deviceNumberHRM == WILDCARD) { + getAntReceiver().ANTRequestMessage(HRM_CHANNEL, + AntMesg.MESG_CHANNEL_ID_ID); + Log.d(TAG, "Requesting channel id id."); + } + + setSensorState(Sensor.SensorState.CONNECTED); + int bpm = (int) antMessage[10] & 0xFF; + Sensor.SensorData.Builder b = Sensor.SensorData.newBuilder() + .setValue(bpm) + .setState(Sensor.SensorState.SENDING); + sensorData = + Sensor.SensorDataSet.newBuilder() + .setCreationTime(System.currentTimeMillis()) + .setHeartRate(b) + .build(); + } + + void handleChannelId(byte[] antMessage) { + // Store the device id. + deviceNumberHRM = + (short) (((int) antMessage[3] & 0xFF | + ((int) (antMessage[4] & 0xFF) << 8)) & 0xFFFF); + Log.i(TAG, "Found device id: " + deviceNumberHRM); + + SharedPreferences prefs = context.getSharedPreferences( + MyTracksSettings.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(); + } + + private void handleMessageResponse(byte[] antMessage) { + if (antMessage[3] == AntMesg.MESG_EVENT_ID && + antMessage[4] == AntDefine.EVENT_RX_SEARCH_TIMEOUT) { + // Search timeout + Log.w(TAG, "Search timed out. Unassigning channel."); + getAntReceiver().ANTUnassignChannel((byte) 0); + setSensorState(Sensor.SensorState.DISCONNECTED); + } else if (antMessage[3] == AntMesg.MESG_UNASSIGN_CHANNEL_ID) { + setSensorState(Sensor.SensorState.DISCONNECTED); + Log.i(TAG, "Disconnected from the sensor: " + getSensorState()); + } + } + + /** + * Open a channel to the SRM head unit. + */ + @Override + public void setupChannel() { + super.setupChannel(); + antChannelSetup(NETWORK_NUMBER, + HRM_CHANNEL, + deviceNumberHRM, + HEART_RATE_DEVICE_TYPE, + (byte) 0x01, + CHANNEL_PERIOD, + RF_FREQUENCY, + (byte) 0); + setSensorState(Sensor.SensorState.CONNECTING); + } + + public short getDeviceNumberHRM() { + return deviceNumberHRM; + } + + void setDeviceNumberHRM(short deviceNumberHRM) { + this.deviceNumberHRM = deviceNumberHRM; + } +} diff --git a/MyTracks/src/com/google/android/apps/mytracks/services/sensors/AntSRMSensorManager.java b/MyTracks/src/com/google/android/apps/mytracks/services/sensors/AntSRMSensorManager.java new file mode 100644 index 000000000..3d038a922 --- /dev/null +++ b/MyTracks/src/com/google/android/apps/mytracks/services/sensors/AntSRMSensorManager.java @@ -0,0 +1,226 @@ +/* + * 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.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.content.Sensor; +import com.google.android.maps.mytracks.R; + +import com.dsi.ant.AntDefine; +import com.dsi.ant.AntMesg; +import android.content.Context; +import android.content.SharedPreferences; +import android.util.Log; + +/** + * A sensor manager that can connect to an SRM Power Control 7 head unit. + * + * @author Sandor Dornbush + */ +public class AntSRMSensorManager extends AntSensorManager { + + /* + * These constants are defined by the SRM Headunit spec. + */ + public static final byte NETWORK_NUMBER = (byte) 0; + public static final byte DEVICE_TYPE = (byte) 12; + public static final byte MANUFACTURER_ID = (byte) 1; + public static final short CHANNEL_PERIOD = (short) 8192; + public static final byte RF_FREQUENCY = (byte) 50; + + public static final int MESSAGE_TYPE_INDEX = 3; + public static final int INITIALS_MESSAGE_TYPE = 5; + public static final int SENSOR_DATA_MESSAGE_TYPE = 6; + + public static final int MESSAGE_ID_INDEX = 4; + + /** + * This can be any value between 0..8 + */ + private byte channel = (byte) 5; + + /** + * This is the id of this device. + * The head unit must be programmed to expect this number. + */ + private byte deviceId; + + /** + * The id of the last message received from the SRM head unit. + */ + private int lastMessageId; + + public AntSRMSensorManager(Context context) { + super(context); + + // First read the the device id that we will be announcing. + SharedPreferences prefs = context.getSharedPreferences( + MyTracksSettings.SETTINGS_NAME, 0); + if (prefs != null) { + deviceId = (byte) prefs.getInt( + context.getString(R.string.ant_srm_bridge_sensor_id_key), + WILDCARD); + } + } + + @Override + public void handleMessage(byte[] antMessage) { + // Parse channel number + byte recievedChannel = (byte) (antMessage[AntMesg.MESG_DATA_OFFSET] + & AntDefine.CHANNEL_NUMBER_MASK); + if (recievedChannel != channel) { + Log.d(MyTracksConstants.TAG, "Unexpected channel: " + recievedChannel); + return; + } + switch (antMessage[AntMesg.MESG_ID_OFFSET]) { + case AntMesg.MESG_BROADCAST_DATA_ID: + handleBroadcastData(antMessage); + break; + case AntMesg.MESG_RESPONSE_EVENT_ID: + handleMessageResponse(antMessage); + break; + case AntMesg.MESG_CHANNEL_ID_ID: + handleChannelId(antMessage); + break; + default: + Log.e(MyTracksConstants.TAG, + "Unexpected message id: " + antMessage[3]); + } + } + + private void handleBroadcastData(byte[] antMessage) { + if (deviceId == WILDCARD) { + getAntReceiver().ANTRequestMessage(channel, AntMesg.MESG_CHANNEL_ID_ID); + Log.d(MyTracksConstants.TAG, "Requesting channel id id."); + } + setSensorState(Sensor.SensorState.CONNECTED); + switch(antMessage[MESSAGE_TYPE_INDEX]) { + case INITIALS_MESSAGE_TYPE: + // TODO handle initials message. + break; + case SENSOR_DATA_MESSAGE_TYPE: + parseSensorData(antMessage); + break; + default: + Log.e(MyTracksConstants.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); + + SharedPreferences prefs = context.getSharedPreferences( + MyTracksSettings.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(); + } + + private void handleMessageResponse(byte[] antMessage) { + 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."); + getAntReceiver().ANTUnassignChannel(channel); + 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()); + } + } + + private void parseSensorData(byte[] antMessage) { + if (antMessage.length != 11) { + Log.e(MyTracksConstants.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)); + return; + } + if (newMessageId < lastMessageId) { + if (!(newMessageId < 20 && lastMessageId > 200)) { + Log.i(MyTracksConstants.TAG, + String.format("SRM ignoring repeat: 0x%X", newMessageId)); + return; + } // else assume the byte overflowed to 0. + } + lastMessageId = newMessageId; + + Sensor.SensorDataSet.Builder builder = Sensor.SensorDataSet.newBuilder() + .setCreationTime(System.currentTimeMillis()); + + int power = SensorUtils.unsignedShortToInt(antMessage, 5); + if (power >= 0) { + Sensor.SensorData.Builder b = Sensor.SensorData.newBuilder() + .setValue(power) + .setState(Sensor.SensorState.SENDING); + builder.setPower(b); + } + + int speed = SensorUtils.unsignedShortToInt(antMessage, 7); + if (speed >= 0) { + // The speed from srm is in 1/10 km/hr. + // Sensor data expects the speed as m/s. + // TODO finish this. + //sensorData.setWheelSpeed(UnitConversions.TENTH_KMH_TO_MPS * speed); + } + + int cadence = antMessage[9] & 0xFF; + if (cadence >= 0) { + Sensor.SensorData.Builder b = Sensor.SensorData.newBuilder() + .setValue(cadence) + .setState(Sensor.SensorState.SENDING); + builder.setCadence(b); + } + + int bpm = antMessage[10] & 0xFF; + if (bpm > 0) { + Sensor.SensorData.Builder b = Sensor.SensorData.newBuilder() + .setValue(bpm) + .setState(Sensor.SensorState.SENDING); + builder.setHeartRate(b); + } + sensorData = builder.build(); + } + + /** + * Open a channel to the SRM head unit. + */ + @Override + public void setupChannel() { + antChannelSetup(NETWORK_NUMBER, + channel, + deviceId, + DEVICE_TYPE, + MANUFACTURER_ID, + CHANNEL_PERIOD, + RF_FREQUENCY, + (byte) 0); + lastMessageId = 0; + setSensorState(Sensor.SensorState.CONNECTING); + } +} diff --git a/MyTracks/src/com/google/android/apps/mytracks/services/sensors/AntSensorManager.java b/MyTracks/src/com/google/android/apps/mytracks/services/sensors/AntSensorManager.java new file mode 100644 index 000000000..eb2230f10 --- /dev/null +++ b/MyTracks/src/com/google/android/apps/mytracks/services/sensors/AntSensorManager.java @@ -0,0 +1,228 @@ +/* + * 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.android.apps.mytracks.services.sensors; + +import com.google.android.apps.mytracks.MyTracksConstants; +import com.google.android.apps.mytracks.content.Sensor.SensorDataSet; + +import com.dsi.ant.AntDefine; +import com.dsi.ant.AntInterface; +import com.dsi.ant.AntInterfaceIntent; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.Handler; +import android.os.Message; +import android.util.Log; + +/** + * An abstract class for ANT+ based sensors. + * + * @author Sandor Dornbush + */ +public abstract class AntSensorManager extends SensorManager { + + // ANT Service + public static final int EVENT_ANT_ENABLED = 1; + public static final int EVENT_ANT_DISABLED = 2; + + // Pairing + protected static final short WILDCARD = 0; + + private AntInterface antReceiver; + + private boolean status; + + // Flag to know if the ANT App was interrupted + // TODO: This code path is not used but probably should be. + private boolean antInterrupted; + + /** + * The data from the sensors. + */ + protected SensorDataSet sensorData; + + protected Context context; + + private static final boolean DEBUGGING = false; + + /** + * Receives all of the ANT intents and dispatches to the proper handler. + */ + private final BroadcastReceiver receiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + String antAction = intent.getAction(); + + Log.i(MyTracksConstants.TAG, "enter onReceive" + antAction); + if (antAction.equals(AntInterfaceIntent.ANT_ENABLED_ACTION)) { + Log.i(MyTracksConstants.TAG, "enter onReceive ANT_ENABLED_ACTION " + + antAction); + handler.sendMessage(handler.obtainMessage(EVENT_ANT_ENABLED, 0)); + } + if (antAction.equals(AntInterfaceIntent.ANT_DISABLED_ACTION)) { + Log.i(MyTracksConstants.TAG, "enter onReceive ANT_DISABLED_ACTION " + + antAction); + handler.sendMessage(handler.obtainMessage(EVENT_ANT_DISABLED, 0)); + } + + if (antAction.equals(AntInterfaceIntent.ANT_RX_MESSAGE_ACTION)) { + byte[] antMessage = + intent.getByteArrayExtra(AntInterfaceIntent.ANT_MESSAGE); + if (DEBUGGING) { + StringBuilder sb = + new StringBuilder("ANT_RX_MESSAGE_ACTION Rx:"); + + for (int i = 0; i < antMessage.length; i++) + sb.append(String.format("[%X]]", (antMessage[i] & 0xFF))); + + Log.d(MyTracksConstants.TAG, sb.toString()); + } + + handleMessage(antMessage); + } + } + }; + + /** + * Handler for all the ANT related events. The events will be handled only + * when we are in the ANT application main menu + **/ + private Handler handler = new Handler() { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case EVENT_ANT_ENABLED: + Log.i(MyTracksConstants.TAG, + "enter handleMessage ----EVENT_ANT_ENABLED"); + break; + case EVENT_ANT_DISABLED: + Log.i(MyTracksConstants.TAG, + "enter handleMessage ----EVENT_ANT_DISABLED"); + break; + } + } + }; + + public AntSensorManager(Context context) { + this.context = context; + + // Need to enable the ANT if it is not enabled earlier + antReceiver = AntInterface.getInstance(context, null); + if (antReceiver == null) { + return; + } + + status = antReceiver.isEnabled(); + if (!status) { + // Make sure not to call AntInterface.enable() again, if it has been + // already called before + if (antInterrupted == false) { + status = antReceiver.enable(); + if (!status) { + Log.e(MyTracksConstants.TAG, "Can not enable ANT interface"); + } else { + Log.i(MyTracksConstants.TAG, "Powering on Radio"); + } + } else { + Log.i(MyTracksConstants.TAG, "Radio already enabled"); + } + } + } + + @Override + public void setupChannel() { + Log.i(MyTracksConstants.TAG, "Registering for ant intents."); + // Register for ANT intent broadcasts. + IntentFilter intentFilter = new IntentFilter(); + intentFilter.addAction(AntInterfaceIntent.ANT_ENABLED_ACTION); + intentFilter.addAction(AntInterfaceIntent.ANT_DISABLED_ACTION); + intentFilter.addAction(AntInterfaceIntent.ANT_RX_MESSAGE_ACTION); + context.registerReceiver(receiver, intentFilter); + } + + @Override + public SensorDataSet getSensorDataSet() { + return sensorData; + } + + @Override + public boolean isEnabled() { + return true; + } + + public abstract void handleMessage(byte[] antMessage); + + @Override + public void onDestroy() { + Log.i(MyTracksConstants.TAG, "Unregistering for ant intents."); + context.unregisterReceiver(receiver); + } + + /** + * Set up an ANT+ channel. + * + * @return True on success. + */ + protected boolean antChannelSetup(byte networkNumber, byte channelNumber, + short deviceNumber, byte deviceType, byte txType, short channelPeriod, + byte radioFreq, byte proxSearch) { + + // Assign as slave channel on selected network (0 = public, 1 = ANT+, 2 = + // ANTFS) + if (!antReceiver.ANTAssignChannel(channelNumber, + AntDefine.PARAMETER_RX_NOT_TX, networkNumber)) { + return false; + } + + if (!antReceiver.ANTSetChannelId(channelNumber, deviceNumber, deviceType, + txType)) { + return false; + } + + if (!antReceiver.ANTSetChannelPeriod(channelNumber, channelPeriod)) { + return false; + } + + if (!antReceiver.ANTSetChannelRFFreq(channelNumber, radioFreq)) { + return false; + } + + // Disable high priority search + if (!antReceiver.ANTSetChannelSearchTimeout(channelNumber, (byte) 0)) { + return false; + } + + // Set search timeout to 30 seconds (low priority search)) + if (!antReceiver.ANTSetLowPriorityChannelSearchTimeout(channelNumber, (byte) 12)) { + return false; + } + + if (deviceNumber == WILDCARD) { + // Configure proximity search, if using wild card search + if (!antReceiver.ANTSetProximitySearch(channelNumber, proxSearch)) { + return false; + } + } + + return !antReceiver.ANTOpenChannel(channelNumber); + } + + public AntInterface getAntReceiver() { + return antReceiver; + } +} 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 01a8d8bf5..cad3e707b 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 @@ -13,7 +13,6 @@ * License for the specific language governing permissions and limitations under * the License. */ - package com.google.android.apps.mytracks.services.sensors; import com.google.android.apps.mytracks.MyTracksConstants; @@ -46,15 +45,19 @@ public class SensorManagerFactory { return null; } - String sensor = prefs.getString(context.getString(R.string.sensor_type_key), - null); + String sensor = prefs.getString(context.getString(R.string.sensor_type_key), null); Log.i(MyTracksConstants.TAG, "Creating sensor of type: " + sensor); if (sensor == null) { return null; + } else if (sensor.equals(context.getString(R.string.ant_sensor_type))) { + return new AntDirectSensorManager(context); + } else if (sensor.equals(context.getString(R.string.srm_ant_bridge_sensor_type))) { + return new AntSRMSensorManager(context); } 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); 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 8cf8f951f..3a2fbf9e4 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 @@ -35,8 +35,9 @@ public class ZephyrMessageParser implements MessageParser { } Log.w(MyTracksConstants.TAG, "Got zephyr data: " + sb); // The provided units are 1/16 strides per minute. - int cadence = SensorUtils.unsignedShortToInt(buffer, 56); - // Heart Rate + // TODO: Fix the cadence calculation. + // int cadence = SensorUtils.unsignedShortToInt(buffer, 56); + // Heart Rate Sensor.SensorData.Builder b = Sensor.SensorData.newBuilder() .setValue(buffer[12] & 0xFF) .setState(Sensor.SensorState.SENDING); 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 06727cd2d..7f1016856 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/util/ApiFeatures.java +++ b/MyTracks/src/com/google/android/apps/mytracks/util/ApiFeatures.java @@ -62,24 +62,18 @@ public class ApiFeatures { * Allow subclasses for mocking, but no direct instantiation. */ protected ApiFeatures() { - if (getApiLevel() >= 5) { - try { - Class clazz = Class.forName( - "com.google.android.apps.mytracks.util.EclairPlatformAdapter"); - apiPlatformAdapter = (ApiPlatformAdapter) clazz.newInstance(); - } catch (Exception e) { - Log.i(MyTracksConstants.TAG, "ApiFeatures: Unable to instantiate Eclair" - + " platform adapter", e); - } - } - if (apiPlatformAdapter == null) { + if (getApiLevel() >= 9) { + apiPlatformAdapter = new GingerbreadPlatformAdapter(); + } else if (getApiLevel() >= 5) { + apiPlatformAdapter = new EclairPlatformAdapter(); + } else { Log.i(MyTracksConstants.TAG, "ApiFeatures: Using default platform adapter"); // Cupcake adapter is always supported, so it's safe to do static linkage. apiPlatformAdapter = new CupcakePlatformAdapter(); } } - + public ApiPlatformAdapter getApiPlatformAdapter() { return apiPlatformAdapter; } @@ -107,7 +101,11 @@ public class ApiFeatures { return true; } - + + public boolean hasStrictMode() { + return getApiLevel() >= 9; + } + // Visible for testing. protected int getApiLevel() { return ANDROID_API_LEVEL; diff --git a/MyTracks/src/com/google/android/apps/mytracks/util/ApiPlatformAdapter.java b/MyTracks/src/com/google/android/apps/mytracks/util/ApiPlatformAdapter.java index 1e2a5210a..c68832735 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/util/ApiPlatformAdapter.java +++ b/MyTracks/src/com/google/android/apps/mytracks/util/ApiPlatformAdapter.java @@ -18,6 +18,7 @@ package com.google.android.apps.mytracks.util; import android.app.Notification; import android.app.NotificationManager; import android.app.Service; +import android.content.SharedPreferences; import android.os.HandlerThread; /** @@ -57,4 +58,15 @@ public interface ApiPlatformAdapter { * @return true whether the handler has been stopped or not. */ boolean stopHandlerThread(HandlerThread handlerThread); + + /** + * Applies all changes done to the given preferences editor. + * Changes may or may not be applied immediately. + */ + void applyPreferenceChanges(SharedPreferences.Editor editor); + + /** + * Enables strict mode where supported, only if this is a development build. + */ + void enableStrictMode(); } 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 272b47218..354f0a307 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/util/CupcakePlatformAdapter.java +++ b/MyTracks/src/com/google/android/apps/mytracks/util/CupcakePlatformAdapter.java @@ -18,6 +18,7 @@ package com.google.android.apps.mytracks.util; import android.app.Notification; import android.app.NotificationManager; import android.app.Service; +import android.content.SharedPreferences.Editor; import android.os.HandlerThread; /** @@ -50,4 +51,14 @@ public class CupcakePlatformAdapter implements ApiPlatformAdapter { // Do nothing, as Cupcake doesn't provide quit(). return false; } + + @Override + public void applyPreferenceChanges(Editor editor) { + editor.commit(); + } + + @Override + public void enableStrictMode() { + // Not supported + } } diff --git a/MyTracks/src/com/google/android/apps/mytracks/util/EclairPlatformAdapter.java b/MyTracks/src/com/google/android/apps/mytracks/util/EclairPlatformAdapter.java index 9c32946d3..91c1ea1d3 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/util/EclairPlatformAdapter.java +++ b/MyTracks/src/com/google/android/apps/mytracks/util/EclairPlatformAdapter.java @@ -18,6 +18,7 @@ package com.google.android.apps.mytracks.util; import android.app.Notification; import android.app.NotificationManager; import android.app.Service; +import android.content.SharedPreferences.Editor; import android.os.HandlerThread; /** @@ -45,4 +46,14 @@ public class EclairPlatformAdapter implements ApiPlatformAdapter { public boolean stopHandlerThread(HandlerThread handlerThread) { return handlerThread.quit(); } + + @Override + public void applyPreferenceChanges(Editor editor) { + editor.commit(); + } + + @Override + public void enableStrictMode() { + // Not supported + } } diff --git a/MyTracks/src/com/google/android/apps/mytracks/util/GingerbreadPlatformAdapter.java b/MyTracks/src/com/google/android/apps/mytracks/util/GingerbreadPlatformAdapter.java new file mode 100644 index 000000000..caa73a170 --- /dev/null +++ b/MyTracks/src/com/google/android/apps/mytracks/util/GingerbreadPlatformAdapter.java @@ -0,0 +1,36 @@ +package com.google.android.apps.mytracks.util; + +import com.google.android.apps.mytracks.MyTracksConstants; + +import android.content.SharedPreferences.Editor; +import android.os.StrictMode; +import android.util.Log; + +/** + * The Gingerbread (API level 9) specific implememntation of the + * {@link ApiPlatformAdapter}. + * + * @author Rodrigo Damazio + */ +public class GingerbreadPlatformAdapter extends EclairPlatformAdapter { + @Override + public void applyPreferenceChanges(Editor editor) { + // Apply asynchronously + editor.apply(); + } + + @Override + public void enableStrictMode() { + Log.d(MyTracksConstants.TAG, "Enabling strict mode"); + StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder() + .detectDiskWrites() + .detectNetwork() + .penaltyLog() + .build()); + 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/MyTracksUtils.java index 4d83ba992..4d6da6786 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/util/MyTracksUtils.java +++ b/MyTracks/src/com/google/android/apps/mytracks/util/MyTracksUtils.java @@ -25,6 +25,7 @@ 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.util.Log; @@ -38,6 +39,7 @@ import java.util.Stack; * @author Leif Hendrik Wilden */ public class MyTracksUtils { + private static final int RELEASE_SIGNATURE_HASHCODE = -1855564782; /** * Computes the distance on the two sphere between the point c0 and the line @@ -250,6 +252,24 @@ 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. * @@ -258,17 +278,17 @@ public class MyTracksUtils { public static String getMyTracksVersion(Context context) { try { PackageInfo pi = context.getPackageManager().getPackageInfo( - "com.google.android.maps.mytracks", + "com.google.android.maps.mytracks", PackageManager.GET_META_DATA); return pi.versionName; - } catch (NameNotFoundException e) { + } catch (NameNotFoundException e) { Log.w(MyTracksConstants.TAG, "Failed to get version info.", e); return ""; } } /** - * This is a utility class w/ only static memebers. + * This is a utility class w/ only static members. */ protected MyTracksUtils() { } diff --git a/MyTracksLib/default.properties b/MyTracksLib/default.properties index 948be06ed..1694c805b 100644 --- a/MyTracksLib/default.properties +++ b/MyTracksLib/default.properties @@ -8,5 +8,5 @@ # project structure. # Project target. -target=android-8 +target=android-9 android.library=true 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 56c70db13..586228b25 100644 --- a/MyTracksLib/src/com/google/android/apps/mytracks/content/MyTracksProviderUtils.java +++ b/MyTracksLib/src/com/google/android/apps/mytracks/content/MyTracksProviderUtils.java @@ -21,6 +21,7 @@ import android.database.Cursor; import android.location.Location; import android.net.Uri; +import java.util.Iterator; import java.util.List; /** @@ -179,8 +180,9 @@ public interface MyTracksProviderUtils { /** * Finds a track by given unique track id. - * Note that the returned track object does not have any track points - * attached. Use {@link #getTrackPoints(Track, int)} to load the track points. + * Note that the returned track object does not have any track points attached. + * Use {@link #getLocationIterator(long, long, boolean, LocationFactory)} to load + * the track points. * * @param id desired unique track id * @return a Track object, or null if not found @@ -188,24 +190,14 @@ public interface MyTracksProviderUtils { Track getTrack(long id); /** - * Retrieves all tracks without track points. If no tracks exist an empty - * list will be returned. Use {@link #getTrackPoints(Track, int)} to load - * the track points. + * Retrieves all tracks without track points. If no tracks exist, an empty + * list will be returned. Use {@link #getLocationIterator(long, long, boolean, LocationFactory)} + * to load the track points. * * @return a list of all the recorded tracks */ List getAllTracks(); - /** - * Loads the track points for a given track. - * - * @param track assumes that startId and stopId are filled in correctly - * @param maxPoints maximum number of points to load, or -1 for no limit - * (oldest ones will be discarded) - * @return the id of the last location in the track - */ - long getTrackPoints(Track track, int maxPoints); - /** * Creates a cursor over the tracks provider with a given selection. * Caller gets to own the returned cursor. Don't forget to close it. @@ -309,6 +301,87 @@ 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. + */ + interface LocationIterator extends Iterator { + /** + * Returns ID of the most recently retrieved track point through a call to {@link #next()}. + * + * @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. + */ + void close(); + } + + /** + * A factory for creating new {@class Location}s. + */ + interface LocationFactory { + /** + * Creates a new {@link Location} object to be populated from the underlying database record. + * It's up to the implementing class to decide whether to create a new instance or reuse + * existing to optimize for speed. + * + * @return a {@link Location} to be populated from the database. + */ + Location createLocation(); + } + + /** + * The default {@class Location}s factory, which creates a new location of 'gps' type. + */ + LocationFactory DEFAULT_LOCATION_FACTORY = new LocationFactory() { + @Override + public Location createLocation() { + return new Location("gps"); + } + }; + + /** + * Creates a new read-only iterator over all track points for the given track. It provides + * a lightweight way of iterating over long tracks without failing due to the underlying cursor + * limitations. Since it's a read-only iterator, {@link Iterator#remove()} always throws + * {@class UnsupportedOperationException}. + * + * Each call to {@link LocationIterator#next()} may advance to the next DB record, and if so, + * the iterator calls {@link LocationFactory#createLocation()} and populates it with information + * retrieved from the record. + * + * When done with iteration, you must call {@link LocationIterator#close()} to make sure that all + * resources are properly deallocated. + * + * Example use: + * + * ... + * LocationIterator it = providerUtils.getLocationIterator( + * 1, MyTracksProviderUtils.DEFAULT_LOCATION_FACTORY); + * try { + * for (Location loc : it) { + * ... // Do something useful with the location. + * } + * } finally { + * it.close(); + * } + * ... + * + * + * @param trackId the ID of a track to retrieve locations for. + * @param startTrackPointId the ID of the first track point to load, or -1 to start from + * the first point. + * @param descending if true the results will be returned in descending ID + * order (latest location first). + * @param locationFactory the factory for creating new locations. + * + * @return the read-only iterator over the given track's points. + */ + LocationIterator getLocationIterator(long trackId, long startTrackPointId, boolean descending, + LocationFactory locationFactory); /** * A factory which can produce instances of {@link MyTracksProviderUtils}, 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 2938abf2b..4d4ec7d8f 100644 --- a/MyTracksLib/src/com/google/android/apps/mytracks/content/MyTracksProviderUtilsImpl.java +++ b/MyTracksLib/src/com/google/android/apps/mytracks/content/MyTracksProviderUtilsImpl.java @@ -15,7 +15,7 @@ */ package com.google.android.apps.mytracks.content; -import static com.google.android.apps.mytracks.lib.MyTracksLibConstants.*; +import static com.google.android.apps.mytracks.lib.MyTracksLibConstants.TAG; import com.google.android.apps.mytracks.content.Sensor; import com.google.android.apps.mytracks.stats.TripStatistics; import com.google.protobuf.InvalidProtocolBufferException; @@ -29,6 +29,7 @@ import android.util.Log; import java.util.ArrayList; import java.util.List; +import java.util.NoSuchElementException; /** * Helper class providing easy access to locations and tracks in the @@ -39,6 +40,8 @@ import java.util.List; public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils { private final ContentResolver contentResolver; + + private int defaultCursorBatchSize = 2000; public MyTracksProviderUtilsImpl(ContentResolver contentResolver) { this.contentResolver = contentResolver; @@ -125,7 +128,6 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils { private static ContentValues createContentValues(Waypoint waypoint) { ContentValues values = new ContentValues(); - TripStatistics stats = waypoint.getStatistics(); // Values id < 0 indicate no id is available: if (waypoint.getId() >= 0) { @@ -139,21 +141,24 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils { values.put(WaypointsColumns.TYPE, waypoint.getType()); values.put(WaypointsColumns.LENGTH, waypoint.getLength()); values.put(WaypointsColumns.DURATION, waypoint.getDuration()); - values.put(WaypointsColumns.STARTTIME, stats.getStartTime()); values.put(WaypointsColumns.STARTID, waypoint.getStartId()); values.put(WaypointsColumns.STOPID, waypoint.getStopId()); - values.put(WaypointsColumns.TOTALDISTANCE, stats.getTotalDistance()); - values.put(WaypointsColumns.TOTALTIME, stats.getTotalTime()); - values.put(WaypointsColumns.MOVINGTIME, stats.getMovingTime()); - values.put(WaypointsColumns.AVGSPEED, stats.getAverageSpeed()); - values.put(WaypointsColumns.AVGMOVINGSPEED, stats.getAverageMovingSpeed()); - values.put(WaypointsColumns.MAXSPEED, stats.getMaxSpeed()); - values.put(WaypointsColumns.MINELEVATION, stats.getMinElevation()); - values.put(WaypointsColumns.MAXELEVATION, stats.getMaxElevation()); - values.put(WaypointsColumns.ELEVATIONGAIN, stats.getTotalElevationGain()); - values.put(WaypointsColumns.MINGRADE, stats.getMinGrade()); - values.put(WaypointsColumns.MAXGRADE, stats.getMaxGrade()); + TripStatistics stats = waypoint.getStatistics(); + if (stats != null) { + values.put(WaypointsColumns.TOTALDISTANCE, stats.getTotalDistance()); + values.put(WaypointsColumns.TOTALTIME, stats.getTotalTime()); + values.put(WaypointsColumns.MOVINGTIME, stats.getMovingTime()); + values.put(WaypointsColumns.AVGSPEED, stats.getAverageSpeed()); + values.put(WaypointsColumns.AVGMOVINGSPEED, stats.getAverageMovingSpeed()); + values.put(WaypointsColumns.MAXSPEED, stats.getMaxSpeed()); + values.put(WaypointsColumns.MINELEVATION, stats.getMinElevation()); + values.put(WaypointsColumns.MAXELEVATION, stats.getMaxElevation()); + values.put(WaypointsColumns.ELEVATIONGAIN, stats.getTotalElevationGain()); + values.put(WaypointsColumns.MINGRADE, stats.getMinGrade()); + values.put(WaypointsColumns.MAXGRADE, stats.getMaxGrade()); + values.put(WaypointsColumns.STARTTIME, stats.getStartTime()); + } Location location = waypoint.getLocation(); if (location != null) { @@ -185,55 +190,78 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils { fillLocation(cursor, location); return location; } + + /** + * A cache of track column indices. + */ + private static class CachedTrackColumnIndices { + public final int idxId; + public final int idxLatitude; + public final int idxLongitude; + public final int idxAltitude; + public final int idxTime; + public final int idxBearing; + public final int idxAccuracy; + public final int idxSpeed; + public final int idxSensor; - @Override - public void fillLocation(Cursor cursor, Location location) { + public CachedTrackColumnIndices(Cursor cursor) { + idxId = cursor.getColumnIndex(TrackPointsColumns._ID); + idxLatitude = cursor.getColumnIndexOrThrow(TrackPointsColumns.LATITUDE); + idxLongitude = cursor.getColumnIndexOrThrow(TrackPointsColumns.LONGITUDE); + idxAltitude = cursor.getColumnIndexOrThrow(TrackPointsColumns.ALTITUDE); + idxTime = cursor.getColumnIndexOrThrow(TrackPointsColumns.TIME); + idxBearing = cursor.getColumnIndexOrThrow(TrackPointsColumns.BEARING); + idxAccuracy = cursor.getColumnIndexOrThrow(TrackPointsColumns.ACCURACY); + idxSpeed = cursor.getColumnIndexOrThrow(TrackPointsColumns.SPEED); + idxSensor = cursor.getColumnIndexOrThrow(TrackPointsColumns.SENSOR); + } + } + + private void fillLocation(Cursor cursor, CachedTrackColumnIndices columnIndices, + Location location) { location.reset(); - - int idxLatitude = cursor.getColumnIndexOrThrow(TrackPointsColumns.LATITUDE); - int idxLongitude = - cursor.getColumnIndexOrThrow(TrackPointsColumns.LONGITUDE); - int idxAltitude = cursor.getColumnIndexOrThrow(TrackPointsColumns.ALTITUDE); - int idxTime = cursor.getColumnIndexOrThrow(TrackPointsColumns.TIME); - int idxBearing = cursor.getColumnIndexOrThrow(TrackPointsColumns.BEARING); - int idxAccuracy = cursor.getColumnIndexOrThrow(TrackPointsColumns.ACCURACY); - int idxSpeed = cursor.getColumnIndexOrThrow(TrackPointsColumns.SPEED); - int idxSensor = cursor.getColumnIndexOrThrow(TrackPointsColumns.SENSOR); - - if (!cursor.isNull(idxLatitude)) { - location.setLatitude(1. * cursor.getInt(idxLatitude) / 1E6); + + if (!cursor.isNull(columnIndices.idxLatitude)) { + location.setLatitude(1. * cursor.getInt(columnIndices.idxLatitude) / 1E6); } - if (!cursor.isNull(idxLongitude)) { - location.setLongitude(1. * cursor.getInt(idxLongitude) / 1E6); + if (!cursor.isNull(columnIndices.idxLongitude)) { + location.setLongitude(1. * cursor.getInt(columnIndices.idxLongitude) / 1E6); } - if (!cursor.isNull(idxAltitude)) { - location.setAltitude(cursor.getFloat(idxAltitude)); + if (!cursor.isNull(columnIndices.idxAltitude)) { + location.setAltitude(cursor.getFloat(columnIndices.idxAltitude)); } - if (!cursor.isNull(idxTime)) { - location.setTime(cursor.getLong(idxTime)); + if (!cursor.isNull(columnIndices.idxTime)) { + location.setTime(cursor.getLong(columnIndices.idxTime)); } - if (!cursor.isNull(idxBearing)) { - location.setBearing(cursor.getFloat(idxBearing)); + if (!cursor.isNull(columnIndices.idxBearing)) { + location.setBearing(cursor.getFloat(columnIndices.idxBearing)); } - if (!cursor.isNull(idxSpeed)) { - location.setSpeed(cursor.getFloat(idxSpeed)); + if (!cursor.isNull(columnIndices.idxSpeed)) { + location.setSpeed(cursor.getFloat(columnIndices.idxSpeed)); } - if (!cursor.isNull(idxAccuracy)) { - location.setAccuracy(cursor.getFloat(idxAccuracy)); + if (!cursor.isNull(columnIndices.idxAccuracy)) { + location.setAccuracy(cursor.getFloat(columnIndices.idxAccuracy)); } if (location instanceof MyTracksLocation && - !cursor.isNull(idxSensor)) { + !cursor.isNull(columnIndices.idxSensor)) { MyTracksLocation mtLocation = (MyTracksLocation) location; // TODO get the right buffer. Sensor.SensorDataSet sensorData; try { - sensorData = Sensor.SensorDataSet.parseFrom(cursor.getBlob(idxSensor)); + sensorData = Sensor.SensorDataSet.parseFrom(cursor.getBlob(columnIndices.idxSensor)); mtLocation.setSensorData(sensorData); } catch (InvalidProtocolBufferException e) { Log.w(TAG, "Failed to parse sensor data.", e); } } } + + @Override + public void fillLocation(Cursor cursor, Location location) { + CachedTrackColumnIndices columnIndicies = new CachedTrackColumnIndices(cursor); + fillLocation(cursor, columnIndicies, location); + } @Override public Track createTrack(Cursor cursor) { @@ -383,7 +411,6 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils { int idxSpeed = cursor.getColumnIndexOrThrow(WaypointsColumns.SPEED); Waypoint waypoint = new Waypoint(); - TripStatistics stats = waypoint.getStatistics(); if (!cursor.isNull(idxId)) { waypoint.setId(cursor.getLong(idxId)); @@ -412,41 +439,57 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils { if (!cursor.isNull(idxDuration)) { waypoint.setDuration(cursor.getLong(idxDuration)); } - if (!cursor.isNull(idxStartTime)) { - stats.setStartTime(cursor.getLong(idxStartTime)); - } if (!cursor.isNull(idxStartId)) { waypoint.setStartId(cursor.getLong(idxStartId)); } if (!cursor.isNull(idxStopId)) { waypoint.setStopId(cursor.getLong(idxStopId)); } + + TripStatistics stats = new TripStatistics(); + boolean hasStats = false; + if (!cursor.isNull(idxStartTime)) { + stats.setStartTime(cursor.getLong(idxStartTime)); + hasStats = true; + } if (!cursor.isNull(idxTotalDistance)) { stats.setTotalDistance(cursor.getFloat(idxTotalDistance)); + hasStats = true; } if (!cursor.isNull(idxTotalTime)) { stats.setTotalTime(cursor.getLong(idxTotalTime)); + hasStats = true; } if (!cursor.isNull(idxMovingTime)) { stats.setMovingTime(cursor.getLong(idxMovingTime)); + hasStats = true; } if (!cursor.isNull(idxMaxSpeed)) { stats.setMaxSpeed(cursor.getFloat(idxMaxSpeed)); + hasStats = true; } if (!cursor.isNull(idxMinElevation)) { stats.setMinElevation(cursor.getFloat(idxMinElevation)); + hasStats = true; } if (!cursor.isNull(idxMaxElevation)) { stats.setMaxElevation(cursor.getFloat(idxMaxElevation)); + hasStats = true; } if (!cursor.isNull(idxElevationGain)) { stats.setTotalElevationGain(cursor.getFloat(idxElevationGain)); + hasStats = true; } if (!cursor.isNull(idxMinGrade)) { stats.setMinGrade(cursor.getFloat(idxMinGrade)); + hasStats = true; } if (!cursor.isNull(idxMaxGrade)) { stats.setMaxGrade(cursor.getFloat(idxMaxGrade)); + hasStats = true; + } + if (hasStats) { + waypoint.setStatistics(stats); } Location location = new Location(""); @@ -780,13 +823,12 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils { public Cursor getLocationsCursor(long trackId, long minTrackPointId, int maxLocations, boolean descending) { String selection; - if (minTrackPointId > 0) { - selection = String.format("%s=%d AND %s>=%d", - TrackPointsColumns.TRACKID, trackId, - TrackPointsColumns._ID, minTrackPointId); + if (minTrackPointId >= 0) { + selection = String.format("%s=%d AND %s%s%d", + TrackPointsColumns.TRACKID, trackId, TrackPointsColumns._ID, + descending ? "<=" : ">=", minTrackPointId); } else { - selection = String.format("%s=%d", - TrackPointsColumns.TRACKID, trackId); + selection = String.format("%s=%d", TrackPointsColumns.TRACKID, trackId); } String sortOrder = "_id " + (descending ? "DESC" : "ASC"); @@ -794,8 +836,7 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils { sortOrder += " LIMIT " + maxLocations; } - return contentResolver.query( - TrackPointsColumns.CONTENT_URI, null, selection, null, sortOrder); + return contentResolver.query(TrackPointsColumns.CONTENT_URI, null, selection, null, sortOrder); } @Override @@ -825,49 +866,24 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils { String select = TracksColumns._ID + "=" + id; return findTrackBy(select); } - + @Override public List getAllTracks() { Cursor cursor = getTracksCursor(null); - if (cursor == null || !cursor.moveToFirst()) { - return new ArrayList(); - } + ArrayList tracks = new ArrayList(); + if (cursor != null) { + tracks.ensureCapacity(cursor.getCount()); - List tracks = new ArrayList(cursor.getCount()); - do { - tracks.add(createTrack(cursor)); - } while(cursor.moveToNext()); - - return tracks; - } - - @Override - public long getTrackPoints(Track track, int maxPoints) { - long lastId = -1; - Cursor cursor = getLocationsCursor(track.getId(), -1, maxPoints, true); - if (cursor == null) { - Log.w(TAG, "Cannot get a locations cursor!"); - return lastId; - } - try { - final int idColumnIdx = - cursor.getColumnIndexOrThrow(TrackPointsColumns._ID); - if (cursor.moveToLast()) { + if (cursor.moveToFirst()) { do { - Location location = createLocation(cursor); - if (location == null) { - continue; - } - track.addLocation(location); - lastId = cursor.getLong(idColumnIdx); - } while (cursor.moveToPrevious()); + tracks.add(createTrack(cursor)); + } while(cursor.moveToNext()); } - } catch (RuntimeException e) { - Log.w(TAG, "Caught unexpected exception.", e); - } finally { + cursor.close(); } - return lastId; + + return tracks; } @Override @@ -939,4 +955,85 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils { contentResolver.update(TracksColumns.CONTENT_URI, createContentValues(track), "_id=" + track.getId(), null); } + + @Override + public LocationIterator getLocationIterator(final long trackId, final long startTrackPointId, + final boolean descending, final LocationFactory locationFactory) { + if (locationFactory == null) { + throw new IllegalArgumentException("Expecting non-null locationFactory"); + } + return new LocationIterator() { + private long lastTrackPointId = startTrackPointId; + private Cursor cursor = getCursor(startTrackPointId); + private final CachedTrackColumnIndices columnIndices = cursor != null ? + new CachedTrackColumnIndices(cursor) : null; + + private Cursor getCursor(long trackPointId) { + return getLocationsCursor(trackId, trackPointId, defaultCursorBatchSize, descending); + } + + private boolean advanceCursorToNextBatch() { + long pointId = lastTrackPointId + (descending ? -1 : 1); + Log.d(TAG, "Advancing cursor point ID: " + pointId); + cursor.close(); + cursor = getCursor(pointId); + return cursor != null; + } + + @Override + public long getLocationId() { + return lastTrackPointId; + } + + @Override + public boolean hasNext() { + if (cursor == null) { + return false; + } + if (cursor.isAfterLast()) { + return false; + } + if (cursor.isLast()) { + // If the current batch size was less that max, we can safely return, otherwise + // we need to advance to the next batch. + return cursor.getCount() == defaultCursorBatchSize && + advanceCursorToNextBatch() && !cursor.isAfterLast(); + } + + return true; + } + + @Override + public Location next() { + if (cursor == null || + !(cursor.moveToNext() || advanceCursorToNextBatch() || cursor.moveToNext())) { + throw new NoSuchElementException(); + } + + lastTrackPointId = cursor.getLong(columnIndices.idxId); + Location location = locationFactory.createLocation(); + fillLocation(cursor, columnIndices, location); + + return location; + } + + @Override + public void close() { + if (cursor != null) { + cursor.close(); + cursor = null; + } + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + }; + } + + // @VisibleForTesting + void setDefaultCursorBatchSize(int defaultCursorBatchSize) { + this.defaultCursorBatchSize = defaultCursorBatchSize; + } } diff --git a/MyTracksLib/src/com/google/android/apps/mytracks/content/Waypoint.java b/MyTracksLib/src/com/google/android/apps/mytracks/content/Waypoint.java index a2649a501..3d020eee2 100644 --- a/MyTracksLib/src/com/google/android/apps/mytracks/content/Waypoint.java +++ b/MyTracksLib/src/com/google/android/apps/mytracks/content/Waypoint.java @@ -85,7 +85,7 @@ public final class Waypoint implements Parcelable { /** Stop track point id */ private long stopId = -1; - private TripStatistics stats = new TripStatistics(); + private TripStatistics stats; /** The length of the track, without smoothing. */ private double length; diff --git a/MyTracksLib/src/com/google/android/apps/mytracks/content/WaypointCreationRequest.aidl b/MyTracksLib/src/com/google/android/apps/mytracks/content/WaypointCreationRequest.aidl new file mode 100644 index 000000000..c0d008f36 --- /dev/null +++ b/MyTracksLib/src/com/google/android/apps/mytracks/content/WaypointCreationRequest.aidl @@ -0,0 +1,2 @@ +package com.google.android.apps.mytracks.content; +parcelable WaypointCreationRequest; \ No newline at end of file diff --git a/MyTracksLib/src/com/google/android/apps/mytracks/content/WaypointCreationRequest.java b/MyTracksLib/src/com/google/android/apps/mytracks/content/WaypointCreationRequest.java new file mode 100644 index 000000000..e1b8463d9 --- /dev/null +++ b/MyTracksLib/src/com/google/android/apps/mytracks/content/WaypointCreationRequest.java @@ -0,0 +1,105 @@ +/* + * 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.content; + +import android.os.Parcel; +import android.os.Parcelable; + +/** + * A request for the service to create a waypoint at the current location. + * + * @author Sandor Dornbush + */ +public class WaypointCreationRequest implements Parcelable { + + public static enum WaypointType { + MARKER, + STATISTICS; + } + + private WaypointType type; + private String name; + private String description; + private String iconUrl; + + public final static WaypointCreationRequest DEFAULT_MARKER = + new WaypointCreationRequest(WaypointType.MARKER); + public final static WaypointCreationRequest DEFAULT_STATISTICS = + new WaypointCreationRequest(WaypointType.STATISTICS); + + private WaypointCreationRequest(WaypointType type) { + this.type = type; + } + + public WaypointCreationRequest(WaypointType type, String name, + String description, String iconUrl) { + this.type = type; + this.name = name; + this.description = description; + this.iconUrl = iconUrl; + } + + public static class Creator implements Parcelable.Creator { + + @Override + public WaypointCreationRequest createFromParcel(Parcel source) { + int i = source.readInt(); + if (i > WaypointType.values().length) { + throw new IllegalArgumentException("Could not find waypoint type: " + i); + } + WaypointCreationRequest request = new WaypointCreationRequest(WaypointType.values()[i]); + request.description = source.readString(); + request.iconUrl = source.readString(); + request.name = source.readString(); + return request; + } + + public WaypointCreationRequest[] newArray(int size) { + return new WaypointCreationRequest[size]; + } + } + + public static final Creator CREATOR = new Creator(); + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel parcel, int arg1) { + parcel.writeInt(type.ordinal()); + parcel.writeString(description); + parcel.writeString(iconUrl); + parcel.writeString(name); + } + + public WaypointType getType() { + return type; + } + + public String getName() { + return name; + } + + public String getDescription() { + return description; + } + + public String getIconUrl() { + return iconUrl; + } +} \ No newline at end of file diff --git a/MyTracksLib/src/com/google/android/apps/mytracks/services/ITrackRecordingService.aidl b/MyTracksLib/src/com/google/android/apps/mytracks/services/ITrackRecordingService.aidl index 39484aef0..b4ffcec0e 100644 --- a/MyTracksLib/src/com/google/android/apps/mytracks/services/ITrackRecordingService.aidl +++ b/MyTracksLib/src/com/google/android/apps/mytracks/services/ITrackRecordingService.aidl @@ -15,7 +15,7 @@ */ package com.google.android.apps.mytracks.services; -import com.google.android.apps.mytracks.content.Waypoint; +import com.google.android.apps.mytracks.content.WaypointCreationRequest; /** * MyTracks service. @@ -45,18 +45,10 @@ interface ITrackRecordingService { /** * Inserts a waypoint marker in the track being recorded. * - * @param waypoint the waypoint to insert + * @param request Details for the waypoint to be inserted. * @return the unique ID of the inserted marker */ - long insertWaypointMarker(in Waypoint waypoint); - - /** - * Inserts a statistics marker in the track being recorded. - * - * @param location the location at which to insert the marker - * @return the unique ID of the inserted marker - */ - long insertStatisticsMarker(in Location location); + long insertWaypoint(in WaypointCreationRequest request); /** * Inserts a location in the track being recorded. diff --git a/MyTracksTest/default.properties b/MyTracksTest/default.properties index c4fb0851b..85cbfc4dd 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:8 +target=Google Inc.:Google APIs:9 diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/ChartValueSeriesTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/ChartValueSeriesTest.java index afefd4910..491f181bc 100644 --- a/MyTracksTest/src/com/google/android/apps/mytracks/ChartValueSeriesTest.java +++ b/MyTracksTest/src/com/google/android/apps/mytracks/ChartValueSeriesTest.java @@ -15,9 +15,9 @@ */ package com.google.android.apps.mytracks; +import com.google.android.apps.mytracks.ChartValueSeries.ZoomSettings; import com.google.android.maps.mytracks.R; -import android.graphics.Paint; import android.graphics.Paint.Style; import android.test.AndroidTestCase; @@ -26,29 +26,24 @@ import android.test.AndroidTestCase; */ public class ChartValueSeriesTest extends AndroidTestCase { private ChartValueSeries series; - private Paint fillPaint1; @Override protected void setUp() throws Exception { - fillPaint1 = new Paint(); - fillPaint1.setStyle(Style.FILL); - fillPaint1.setColor(getContext().getResources().getColor(R.color.green)); - fillPaint1.setAntiAlias(true); series = new ChartValueSeries(getContext(), "###,###", - fillPaint1, - null, - 100, + R.color.elevation_fill, + R.color.elevation_border, + new ZoomSettings(5, new int[] {100}), R.string.elevation); } public void testInitialConditions() { - assertEquals(25, series.getInterval()); + assertEquals(0, series.getInterval()); assertEquals(1, series.getMaxLabelLength()); assertEquals(0, series.getMin()); assertEquals(0, series.getMax()); assertEquals(0.0, series.getSpread()); - assertEquals(fillPaint1, series.getPaint()); + assertEquals(Style.STROKE, series.getPaint().getStyle()); assertEquals(getContext().getString(R.string.elevation), series.getTitle()); assertTrue(series.isEnabled()); @@ -63,7 +58,7 @@ public class ChartValueSeriesTest extends AndroidTestCase { series.update(0); series.update(10); series.updateDimension(); - assertEquals(25, series.getInterval()); + assertEquals(100, series.getInterval()); assertEquals(3, series.getMaxLabelLength()); assertEquals(0, series.getMin()); assertEquals(100, series.getMax()); @@ -74,7 +69,7 @@ public class ChartValueSeriesTest extends AndroidTestCase { series.update(0); series.update(901); series.updateDimension(); - assertEquals(200, series.getInterval()); + assertEquals(100, series.getInterval()); assertEquals(5, series.getMaxLabelLength()); assertEquals(0, series.getMin()); assertEquals(1000, series.getMax()); @@ -85,10 +80,60 @@ public class ChartValueSeriesTest extends AndroidTestCase { series.update(500); series.update(1401); series.updateDimension(); - assertEquals(200, series.getInterval()); + assertEquals(100, series.getInterval()); assertEquals(5, series.getMaxLabelLength()); assertEquals(500, series.getMin()); assertEquals(1500, series.getMax()); assertEquals(1000.0, series.getSpread()); } + + public void testZoomSettings_invalidArgs() { + try { + new ZoomSettings(0, new int[] {10, 50, 100}); + fail("Expected IllegalArgumentException"); + } catch (IllegalArgumentException e) { + // OK. + } + try { + new ZoomSettings(1, null); + fail("Expected IllegalArgumentException"); + } catch (IllegalArgumentException e) { + // OK. + } + try { + new ZoomSettings(1, new int[] {}); + fail("Expected IllegalArgumentException"); + } catch (IllegalArgumentException e) { + // OK. + } + try { + new ZoomSettings(1, new int[] {1, 3, 2}); + fail("Expected IllegalArgumentException"); + } catch (IllegalArgumentException e) { + // OK. + } + } + + public void testZoomSettings_minAligned() { + ZoomSettings settings = new ZoomSettings(5, new int[] {10, 50, 100}); + assertEquals(10, settings.calculateInterval(0, 15)); + assertEquals(10, settings.calculateInterval(0, 50)); + assertEquals(50, settings.calculateInterval(0, 111)); + assertEquals(50, settings.calculateInterval(0, 250)); + assertEquals(100, settings.calculateInterval(0, 251)); + assertEquals(100, settings.calculateInterval(0, 10000)); + } + + public void testZoomSettings_minNotAligned() { + ZoomSettings settings = new ZoomSettings(5, new int[] {10, 50, 100}); + assertEquals(50, settings.calculateInterval(5, 55)); + assertEquals(10, settings.calculateInterval(10, 60)); + assertEquals(50, settings.calculateInterval(7, 250)); + assertEquals(100, settings.calculateInterval(7, 257)); + assertEquals(100, settings.calculateInterval(11, 10000)); + + // A regression test. + settings = new ZoomSettings(5, new int[] {5, 10, 20}); + assertEquals(10, settings.calculateInterval(-37.14, -11.89)); + } } diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/content/MyTracksProviderUtilsImplTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/content/MyTracksProviderUtilsImplTest.java new file mode 100644 index 000000000..9107221a9 --- /dev/null +++ b/MyTracksTest/src/com/google/android/apps/mytracks/content/MyTracksProviderUtilsImplTest.java @@ -0,0 +1,171 @@ +/* + * 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.content; + +import com.google.android.apps.mytracks.content.MyTracksProviderUtils.LocationFactory; +import com.google.android.apps.mytracks.content.MyTracksProviderUtils.LocationIterator; +import com.google.android.apps.mytracks.services.TrackRecordingServiceTest.MockContext; + +import android.content.Context; +import android.location.Location; +import android.test.AndroidTestCase; +import android.test.RenamingDelegatingContext; +import android.test.mock.MockContentResolver; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * A unit test for {@link MyTracksProviderUtilsImpl}. + * + * @author Bartlomiej Niechwiej + */ +public class MyTracksProviderUtilsImplTest extends AndroidTestCase { + private Context context; + private MyTracksProviderUtils providerUtils; + + @Override + protected void setUp() throws Exception { + super.setUp(); + + MockContentResolver mockContentResolver = new MockContentResolver(); + RenamingDelegatingContext targetContext = new RenamingDelegatingContext( + getContext(), getContext(), "test."); + context = new MockContext(mockContentResolver, targetContext); + MyTracksProvider provider = new MyTracksProvider(); + provider.attachInfo(context, null); + mockContentResolver.addProvider(MyTracksProviderUtils.AUTHORITY, provider); + setContext(context); + + providerUtils = MyTracksProviderUtils.Factory.get(context); + } + + public void testLocationIterator_noPoints() { + testIterator(1, 0, 1, false, MyTracksProviderUtils.DEFAULT_LOCATION_FACTORY); + } + + public void testLocationIterator_customFactory() { + final Location location = new Location("test_location"); + final AtomicInteger counter = new AtomicInteger(); + testIterator(1, 15, 4, false, new LocationFactory() { + @Override + public Location createLocation() { + counter.incrementAndGet(); + return location; + } + }); + // Make sure we were called exactly as many times as we had track points. + assertEquals(15, counter.get()); + } + + public void testLocationIterator_nullFactory() { + try { + testIterator(1, 15, 4, false, null); + fail("Expecting IllegalArgumentException"); + } catch (IllegalArgumentException e) { + // Expected. + } + } + + public void testLocationIterator_noBatchAscending() { + testIterator(1, 50, 100, false, MyTracksProviderUtils.DEFAULT_LOCATION_FACTORY); + testIterator(2, 50, 50, false, MyTracksProviderUtils.DEFAULT_LOCATION_FACTORY); + } + + public void testLocationIterator_noBatchDescending() { + testIterator(1, 50, 100, true, MyTracksProviderUtils.DEFAULT_LOCATION_FACTORY); + testIterator(2, 50, 50, true, MyTracksProviderUtils.DEFAULT_LOCATION_FACTORY); + } + + public void testLocationIterator_batchAscending() { + testIterator(1, 50, 11, false, MyTracksProviderUtils.DEFAULT_LOCATION_FACTORY); + testIterator(2, 50, 25, false, MyTracksProviderUtils.DEFAULT_LOCATION_FACTORY); + } + + public void testLocationIterator_batchDescending() { + testIterator(1, 50, 11, true, MyTracksProviderUtils.DEFAULT_LOCATION_FACTORY); + testIterator(2, 50, 25, true, MyTracksProviderUtils.DEFAULT_LOCATION_FACTORY); + } + + public void testLocationIterator_largeTrack() { + testIterator(1, 20000, 2000, false, MyTracksProviderUtils.DEFAULT_LOCATION_FACTORY); + } + + private List testIterator(long trackId, int numPoints, int batchSize, + boolean descending, LocationFactory locationFactory) { + long lastPointId = initializeTrack(trackId, numPoints); + ((MyTracksProviderUtilsImpl) providerUtils).setDefaultCursorBatchSize(batchSize); + List locations = new ArrayList(numPoints); + LocationIterator it = providerUtils.getLocationIterator(trackId, -1, descending, locationFactory); + try { + while (it.hasNext()) { + Location loc = it.next(); + assertNotNull(loc); + locations.add(loc); + // Make sure the IDs are returned in the right order. + assertEquals(descending ? lastPointId - locations.size() + 1 + : lastPointId - numPoints + locations.size(), it.getLocationId()); + } + assertEquals(numPoints, locations.size()); + } finally { + it.close(); + } + return locations; + } + + private long initializeTrack(long id, int numPoints) { + Track track = new Track(); + track.setId(id); + track.setName("Test: " + id); + track.setNumberOfPoints(numPoints); + providerUtils.insertTrack(track); + track = providerUtils.getTrack(id); + assertNotNull(track); + + Location[] locations = new Location[numPoints]; + for (int i = 0; i < numPoints; ++i) { + Location loc = new Location("test"); + loc.setLatitude(37.0 + (double) i / 10000.0); + loc.setLongitude(57.0 - (double) i / 10000.0); + loc.setAccuracy((float) i / 100.0f); + loc.setAltitude(i * 2.5); + locations[i] = loc; + } + providerUtils.bulkInsertTrackPoints(locations, numPoints, id); + + // Load all inserted locations. + long lastPointId = -1; + int counter = 0; + LocationIterator it = providerUtils.getLocationIterator(id, -1, false, + MyTracksProviderUtils.DEFAULT_LOCATION_FACTORY); + try { + while (it.hasNext()) { + it.next(); + lastPointId = it.getLocationId(); + counter++; + } + } finally { + it.close(); + } + + assertTrue(numPoints == 0 || lastPointId > 0); + assertEquals(numPoints, track.getNumberOfPoints()); + assertEquals(numPoints, counter); + + return lastPointId; + } +} diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/content/WaypointCreationRequestTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/content/WaypointCreationRequestTest.java new file mode 100644 index 000000000..2cc88b895 --- /dev/null +++ b/MyTracksTest/src/com/google/android/apps/mytracks/content/WaypointCreationRequestTest.java @@ -0,0 +1,55 @@ +/* + * 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.content; + +import com.google.android.apps.mytracks.content.WaypointCreationRequest.WaypointType; + +import android.os.Parcel; +import android.test.AndroidTestCase; + +/** + * Tests for the WaypointCreationRequest class. + * {@link WaypointCreationRequest} + * + * @author Sandor Dornbush + */ +public class WaypointCreationRequestTest extends AndroidTestCase { + + public void testTypeParceling() { + WaypointCreationRequest original = WaypointCreationRequest.DEFAULT_MARKER; + Parcel p = Parcel.obtain(); + original.writeToParcel(p, 0); + p.setDataPosition(0); + WaypointCreationRequest copy = WaypointCreationRequest.CREATOR.createFromParcel(p); + assertEquals(original.getType(), copy.getType()); + assertNull(copy.getName()); + assertNull(copy.getDescription()); + assertNull(copy.getIconUrl()); + } + + public void testAllAttributesParceling() { + WaypointCreationRequest original = + new WaypointCreationRequest(WaypointType.MARKER, "name", "description", "img.png"); + Parcel p = Parcel.obtain(); + original.writeToParcel(p, 0); + p.setDataPosition(0); + WaypointCreationRequest copy = WaypointCreationRequest.CREATOR.createFromParcel(p); + assertEquals(original.getType(), copy.getType()); + assertEquals("name", copy.getName()); + assertEquals("description", copy.getDescription()); + assertEquals("img.png", copy.getIconUrl()); + } +} diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/io/TrackWriterTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/io/TrackWriterTest.java index 12542199b..73721214e 100644 --- a/MyTracksTest/src/com/google/android/apps/mytracks/io/TrackWriterTest.java +++ b/MyTracksTest/src/com/google/android/apps/mytracks/io/TrackWriterTest.java @@ -2,31 +2,27 @@ package com.google.android.apps.mytracks.io; -import static org.easymock.EasyMock.eq; import static org.easymock.EasyMock.expect; -import static org.easymock.EasyMock.isA; -import static org.easymock.EasyMock.leq; -import static org.easymock.EasyMock.same; -import com.google.android.apps.mytracks.MyTracksConstants; +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 android.content.Context; -import android.database.MatrixCursor; import android.location.Location; -import android.provider.BaseColumns; import android.test.AndroidTestCase; +import android.test.RenamingDelegatingContext; +import android.test.mock.MockContentResolver; import java.io.ByteArrayOutputStream; import java.io.FileNotFoundException; import java.io.OutputStream; import org.easymock.EasyMock; -import org.easymock.IAnswer; import org.easymock.IArgumentMatcher; import org.easymock.IMocksControl; @@ -138,12 +134,19 @@ public class TrackWriterTest extends AndroidTestCase { protected void setUp() throws Exception { super.setUp(); + MockContentResolver mockContentResolver = new MockContentResolver(); + RenamingDelegatingContext targetContext = new RenamingDelegatingContext( + getContext(), getContext(), "test."); + Context context = new MockContext(mockContentResolver, targetContext); + MyTracksProvider provider = new MyTracksProvider(); + provider.attachInfo(context, null); + mockContentResolver.addProvider(MyTracksProviderUtils.AUTHORITY, provider); + setContext(context); + providerUtils = MyTracksProviderUtils.Factory.get(context); + oldProviderUtilsFactory = TestingProviderUtilsFactory.installWithInstance(providerUtils); + mocksControl = EasyMock.createStrictControl(); formatWriter = mocksControl.createMock(TrackFormatWriter.class); - providerUtils = mocksControl.createMock(MyTracksProviderUtils.class); - oldProviderUtilsFactory = - TestingProviderUtilsFactory.installWithInstance(providerUtils); - expect(formatWriter.getExtension()).andStubReturn(EXTENSION); track = new Track(); @@ -225,13 +228,6 @@ public class TrackWriterTest extends AndroidTestCase { public void testWriteDocument_emptyTrack() { writer = new TrackWriter(getContext(), providerUtils, track, formatWriter); - // Don't let it write any waypoints - expect(providerUtils.getWaypointsCursor( - TRACK_ID, 0, MyTracksConstants.MAX_LOADED_WAYPOINTS_POINTS)) - .andStubReturn(null); - expect(providerUtils.getLocationsCursor( - eq(TRACK_ID), leq(0L), leq(0), eq(false))).andStubReturn(null); - // Set expected mock behavior formatWriter.writeHeader(); formatWriter.writeFooter(); @@ -253,7 +249,7 @@ public class TrackWriterTest extends AndroidTestCase { new Location("fake2"), new Location("fake3"), new Location("fake4"), - new Location("fake5"), + new Location("fake5") }; Waypoint[] wps = { new Waypoint(), new Waypoint(), new Waypoint() }; @@ -262,37 +258,14 @@ public class TrackWriterTest extends AndroidTestCase { // Make location 3 invalid locs[2].setLatitude(100); - - // Set up cursors - // We use fake columns since the cursor is only read by the provider utils - final MatrixCursor locCursor = - new MatrixCursor(new String[] { BaseColumns._ID }, 6); - for (int i = 1; i <= 6; i++) { - locCursor.newRow().add(i); + + assertEquals(locs.length, providerUtils.bulkInsertTrackPoints(locs, locs.length, TRACK_ID)); + for (int i = 0; i < wps.length; ++i) { + Waypoint wpt = wps[i]; + wpt.setTrackId(TRACK_ID); + assertNotNull(providerUtils.insertWaypoint(wpt)); + wpt.setId(i + 1); } - expect(providerUtils.getLocationsCursor( - eq(TRACK_ID), leq(0L), leq(0), eq(false))).andStubReturn(locCursor); - providerUtils.fillLocation(same(locCursor), isA(Location.class)); - EasyMock.expectLastCall().andStubAnswer(new IAnswer() { - @Override - public Void answer() throws Throwable { - Location loc = (Location) EasyMock.getCurrentArguments()[1]; - loc.set(locs[locCursor.getPosition()]); - return null; - } - }); - - MatrixCursor wpCursor = - new MatrixCursor(new String[] { BaseColumns._ID }, 3); - wpCursor.newRow().add(1); - wpCursor.newRow().add(2); - wpCursor.newRow().add(3); - expect(providerUtils.getWaypointsCursor( - eq(TRACK_ID), leq(0L), - eq(MyTracksConstants.MAX_LOADED_WAYPOINTS_POINTS))) - .andStubReturn(wpCursor); - expect(providerUtils.createWaypoint(wpCursor)) - .andStubAnswer(stubCursorToArray(wpCursor, wps)); // Begin the track formatWriter.writeHeader(); @@ -317,8 +290,8 @@ public class TrackWriterTest extends AndroidTestCase { formatWriter.writeEndTrack(locEq(locs[5])); // Expect reading/writing of the waypoints (except the first) - formatWriter.writeWaypoint(wps[1]); - formatWriter.writeWaypoint(wps[2]); + formatWriter.writeWaypoint(wptEq(wps[1])); + formatWriter.writeWaypoint(wptEq(wps[2])); formatWriter.writeFooter(); formatWriter.close(); @@ -330,6 +303,26 @@ public class TrackWriterTest extends AndroidTestCase { mocksControl.verify(); } + private static Waypoint wptEq(final Waypoint wpt) { + EasyMock.reportMatcher(new IArgumentMatcher() { + @Override + public boolean matches(Object wptObj2) { + if (wptObj2 == null || wpt == null) return wpt == wptObj2; + Waypoint wpt2 = (Waypoint) wptObj2; + + return wpt.getId() == wpt2.getId(); + } + + @Override + public void appendTo(StringBuffer buffer) { + buffer.append("wptEq("); + buffer.append(wpt); + buffer.append(")"); + } + }); + return null; + } + private static Location locEq(final Location loc) { EasyMock.reportMatcher(new IArgumentMatcher() { @Override @@ -359,23 +352,14 @@ public class TrackWriterTest extends AndroidTestCase { }); return null; } - - private IAnswer stubCursorToArray( - final MatrixCursor cursor, final T[] values) { - return new IAnswer() { - @Override - public T answer() throws Throwable { - return values[cursor.getPosition()]; - } - }; - } - + private void fillLocations(Location... locs) { assertTrue(locs.length < 90); for (int i = 0; i < locs.length; i++) { Location location = locs[i]; location.setLatitude(i + 1); location.setLongitude(i + 1); + location.setTime(i + 1000); } } } 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 0cdd7c626..e2c8b3b48 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 @@ -38,7 +38,7 @@ public class PreferenceBackupHelperTest extends TestCase { */ private class MockPreferenceEditor implements SharedPreferences.Editor { private Map newPreferences = new HashMap(preferenceValues); - + @Override public Editor clear() { newPreferences.clear(); @@ -51,6 +51,11 @@ public class PreferenceBackupHelperTest extends TestCase { return true; } + @Override + public void apply() { + commit(); + } + @Override public Editor putBoolean(String key, boolean value) { return put(key, value); diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/io/docs/DocsHelperTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/io/docs/DocsHelperTest.java new file mode 100644 index 000000000..b1402c06f --- /dev/null +++ b/MyTracksTest/src/com/google/android/apps/mytracks/io/docs/DocsHelperTest.java @@ -0,0 +1,101 @@ +/* + * 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.docs; + +import com.google.android.apps.mytracks.io.gdata.GDataWrapper; + +import android.content.Context; +import android.test.mock.MockContext; + +import junit.framework.TestCase; + +import java.io.IOException; + +/** + * Tests for {@link DocsHelper}, with the exception of + * {@link DocsHelper#addTrackRow}, which is in + * {@link DocsHelper_AddTrackRowTest}. + * + * @author Matthew Simmons + */ +public class DocsHelperTest extends TestCase { + private Context mockContext = new MockContext(); + + // TODO(simmonmt): Use AndroidMock to mock this class. We do it the hard + // way because use of AndroidMock to mock GDataWrapper triggers a compile + // error in an unrelated file. Specifically, it causes a NoClassDefFound + // exception for com.google.wireless.gdata2.client.AuthenticationException, + // (wrongly) attributed to the first source file in the project. Using the + // @UsesMocks(GDataWrapper.class) annotation is enough -- you don't have to + // touch AndroidMock at all to get this failure. + // The bug is filed with Android Mock as + // http://code.google.com/p/android-mock/issues/detail?id=3 + private class MockGDataWrapper extends GDataWrapper { + private final boolean returnValue; + + MockGDataWrapper(boolean returnValue) { + this.returnValue = returnValue; + } + + @Override + public boolean runQuery(QueryFunction queryFunction) { + return returnValue; + } + } + + public void testCreateSpreadsheet_noException() throws Exception { + // Our mock GDataWrapper isn't able to affect the return value from + // DocsHelper#createSpreadsheet. As such, we're only able to simulate the + // case where there weren't any GData errors, but not spreadsheet ID was + // actually returned. createSpreadsheet is defined to return null in that + // situation. + assertNull(new DocsHelper().createSpreadsheet( + mockContext, new MockGDataWrapper(true), "sheetName")); + } + + public void testCreateSpreadsheet_exception() throws Exception { + try { + new DocsHelper().createSpreadsheet( + mockContext, new MockGDataWrapper(false), "sheetName"); + fail(); + } catch (IOException expected) {} + } + + public void testRequestSpreadsheetId_noException() throws Exception { + assertNull(new DocsHelper().requestSpreadsheetId( + new MockGDataWrapper(true), "sheetName")); + } + + public void testRequestSpreadsheetId_exception() throws Exception { + try { + new DocsHelper().requestSpreadsheetId(new MockGDataWrapper(false), + "sheetName"); + fail(); + } catch (IOException expected) {} + } + + public void testGetWorksheetId_noException() throws Exception { + assertNull(new DocsHelper().getWorksheetId(new MockGDataWrapper(true), + "sheetId")); + } + + public void testGetWorksheetId_exception() throws Exception { + try { + new DocsHelper().getWorksheetId(new MockGDataWrapper(false), "sheetId"); + fail(); + } catch (IOException expected) {} + } +} 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 new file mode 100644 index 000000000..e357605b4 --- /dev/null +++ b/MyTracksTest/src/com/google/android/apps/mytracks/io/docs/DocsHelper_AddTrackRowTest.java @@ -0,0 +1,159 @@ +/* + * 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.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.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; + +/** + * 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) { + writtenSheetUri = worksheetUri; + writtenData = postText; + } + } + + public void testAddTrackRow_imperial() throws Exception { + StringWritingDocsHelper docsHelper = new StringWritingDocsHelper(); + addTrackRow(docsHelper, false); + + String expectedData = + "" + + "" + + "" + // We format the date here because we can't guarantee/force the timezone + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + ""; + + assertEquals( + "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); + + // The imperial test verifies that the tags come out in the proper order, + // and with the proper names. We need only verify that the labels are + // correct, and that at least one of the unit-dependent value tags is + // correct. + assertTrue(docsHelper.writtenData.contains( + "")); + assertTrue(docsHelper.writtenData.contains( + "")); + 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) + throws IOException { + Resources mockResources = AndroidMock.createMock(MockResources.class); + + if (useMetric) { + AndroidMock.expect(mockResources.getString(R.string.kilometer)) + .andReturn("km"); + AndroidMock.expect(mockResources.getString(R.string.kilometer_per_hour)) + .andReturn("kph"); + AndroidMock.expect(mockResources.getString(R.string.meter)) + .andReturn("meter"); + } else { + AndroidMock.expect(mockResources.getString(R.string.mile)) + .andReturn("mile"); + AndroidMock.expect(mockResources.getString(R.string.mile_per_hour)) + .andReturn("mph"); + AndroidMock.expect(mockResources.getString(R.string.feet)) + .andReturn("feet"); + } + AndroidMock.replay(mockResources); + + Context mockContext = AndroidMock.createMock(MockContext.class); + AndroidMock.expect(mockContext.getResources()) + .andReturn(mockResources).anyTimes(); + AndroidMock.replay(mockContext); + + AuthManager mockAuthManager = AndroidMock.createMock(AuthManager.class); + AndroidMock.replay(mockAuthManager); + + TripStatistics stats = new TripStatistics(); + stats.setStartTime(TIME); + stats.setTotalTime(5000); + stats.setMovingTime(4000); + stats.setTotalDistance(20000); + stats.setMaxSpeed(1500); + 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", + track, useMetric); + } +} diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/io/docs/DocsTagBuilderTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/io/docs/DocsTagBuilderTest.java new file mode 100644 index 000000000..0145f6a9e --- /dev/null +++ b/MyTracksTest/src/com/google/android/apps/mytracks/io/docs/DocsTagBuilderTest.java @@ -0,0 +1,60 @@ +/* + * 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.docs; + +import junit.framework.TestCase; + +/** + * This class tests {@link DocsTagBuilder} + * + * @author Matthew Simmons + */ +public class DocsTagBuilderTest extends TestCase { + public void testAppend() { + assertEquals("", + new DocsTagBuilder(false).append("tag", "value").build()); + } + + public void testMultiAppend() { + String actual = new DocsTagBuilder(false) + .append("tag1", "value1") + .append("tag2", "value2") + .build(); + + assertEquals("" + + "", actual); + } + + public void testAppendLargeUnits_imperial() { + assertEquals("", + new DocsTagBuilder(false).appendLargeUnits("tag", 100.0).build()); + } + + public void testAppendLargeUnits_metric() { + assertEquals("", + new DocsTagBuilder(true).appendLargeUnits("tag", 100.0).build()); + } + + public void testAppendSmallUnits_imperial() { + assertEquals("", + new DocsTagBuilder(false).appendSmallUnits("tag", 100.0).build()); + } + + public void testAppendSmallUnits_metric() { + assertEquals("", + new DocsTagBuilder(true).appendSmallUnits("tag", 100.0).build()); + } +} 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 f54f6d6c1..db0236aa2 100644 --- a/MyTracksTest/src/com/google/android/apps/mytracks/services/TrackRecordingServiceTest.java +++ b/MyTracksTest/src/com/google/android/apps/mytracks/services/TrackRecordingServiceTest.java @@ -22,6 +22,7 @@ import com.google.android.apps.mytracks.content.MyTracksProvider; 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.content.WaypointCreationRequest; import com.google.android.apps.mytracks.stats.TripStatistics; import com.google.android.apps.mytracks.util.ApiFeatures; import com.google.android.maps.mytracks.R; @@ -77,9 +78,11 @@ public class TrackRecordingServiceTest } /** - * A context wrapper with the user provided {@link ContentResolver}. + * A context wrapper with the user provided {@link ContentResolver}. + * + * TODO: Move to test utils package. */ - private static class MockContext extends ContextWrapper { + public static class MockContext extends ContextWrapper { private final ContentResolver contentResolver; public MockContext(ContentResolver contentResolver, Context base) { @@ -499,7 +502,7 @@ public class TrackRecordingServiceTest public void testDeleteAllTracks_trackInProgress() throws Exception { createDummyTrack(123, -1, true); assertEquals(1, providerUtils.getAllTracks().size()); - + ITrackRecordingService service = bindAndGetService(createStartIntent()); assertTrue(service.isRecording()); @@ -546,9 +549,8 @@ public class TrackRecordingServiceTest ITrackRecordingService service = bindAndGetService(createStartIntent()); assertFalse(service.isRecording()); - Location loc = new Location("gps"); try { - service.insertStatisticsMarker(loc); + service.insertWaypoint(WaypointCreationRequest.DEFAULT_STATISTICS); fail("Expecting IllegalStateException"); } catch (IllegalStateException e) { // Expected. @@ -562,11 +564,22 @@ public class TrackRecordingServiceTest ITrackRecordingService service = bindAndGetService(createStartIntent()); assertTrue(service.isRecording()); - Location loc = new Location("gps"); - assertEquals(1, service.insertStatisticsMarker(loc)); - assertEquals(2, service.insertStatisticsMarker(loc)); + assertEquals(1, service.insertWaypoint(WaypointCreationRequest.DEFAULT_STATISTICS)); + assertEquals(2, service.insertWaypoint(WaypointCreationRequest.DEFAULT_STATISTICS)); - // TODO: Add more checks. + Waypoint wpt = providerUtils.getWaypoint(1); + assertEquals(getContext().getString(R.string.stats_icon_url), + wpt.getIcon()); + assertEquals(getContext().getString(R.string.statistics), + wpt.getName()); + assertEquals(Waypoint.TYPE_STATISTICS, wpt.getType()); + assertEquals(123, wpt.getTrackId()); + assertEquals(0.0, wpt.getLength()); + assertNotNull(wpt.getLocation()); + assertNotNull(wpt.getStatistics()); + // TODO check the rest of the params. + + // TODO: Check waypoint 2. } @MediumTest @@ -574,29 +587,14 @@ public class TrackRecordingServiceTest ITrackRecordingService service = bindAndGetService(createStartIntent()); assertFalse(service.isRecording()); - Location loc = new Location("gps"); - Waypoint waypoint = new Waypoint(); - waypoint.setId(1); - waypoint.setLocation(loc); try { - service.insertWaypointMarker(waypoint); + service.insertWaypoint(WaypointCreationRequest.DEFAULT_MARKER); fail("Expecting IllegalStateException"); } catch (IllegalStateException e) { // Expected. } } - @MediumTest - public void testInsertWaypointMarker_invalidWaypoint() throws Exception { - createDummyTrack(123, -1, true); - - ITrackRecordingService service = bindAndGetService(createStartIntent()); - assertTrue(service.isRecording()); - - Waypoint waypoint = new Waypoint(); - assertEquals(-1, service.insertWaypointMarker(waypoint)); - } - @MediumTest public void testInsertWaypointMarker_validWaypoint() throws Exception { createDummyTrack(123, -1, true); @@ -604,11 +602,17 @@ public class TrackRecordingServiceTest ITrackRecordingService service = bindAndGetService(createStartIntent()); assertTrue(service.isRecording()); - Location loc = new Location("gps"); - Waypoint waypoint = new Waypoint(); - waypoint.setId(1); - waypoint.setLocation(loc); - assertEquals(1, service.insertWaypointMarker(waypoint)); + assertEquals(1, service.insertWaypoint(WaypointCreationRequest.DEFAULT_MARKER)); + Waypoint wpt = providerUtils.getWaypoint(1); + assertEquals(getContext().getString(R.string.waypoint_icon_url), + wpt.getIcon()); + assertEquals(getContext().getString(R.string.waypoint), + wpt.getName()); + assertEquals(Waypoint.TYPE_WAYPOINT, wpt.getType()); + assertEquals(123, wpt.getTrackId()); + assertEquals(0.0, wpt.getLength()); + assertNotNull(wpt.getLocation()); + assertNull(wpt.getStatistics()); } @MediumTest @@ -804,11 +808,9 @@ public class TrackRecordingServiceTest service.recordLocation(loc); if (i % 10 == 0) { - service.insertStatisticsMarker(loc); + service.insertWaypoint(WaypointCreationRequest.DEFAULT_STATISTICS); } else if (i % 7 == 0) { - Waypoint waypoint = new Waypoint(); - waypoint.setLocation(loc); - service.insertWaypointMarker(waypoint); + service.insertWaypoint(WaypointCreationRequest.DEFAULT_MARKER); } } 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 new file mode 100644 index 000000000..22c2f0fa1 --- /dev/null +++ b/MyTracksTest/src/com/google/android/apps/mytracks/services/sensors/AntDirectSensorManagerTest.java @@ -0,0 +1,97 @@ +/* + * 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.android.apps.mytracks.services.sensors; + +import com.dsi.ant.AntMesg; +import com.google.android.apps.mytracks.MyTracksSettings; +import com.google.android.apps.mytracks.content.Sensor; +import com.google.android.maps.mytracks.R; + +import android.content.SharedPreferences; +import android.test.AndroidTestCase; +import android.test.suitebuilder.annotation.SmallTest; + +public class AntDirectSensorManagerTest extends AndroidTestCase { + + private SharedPreferences sharedPreferences; + private AntDirectSensorManager manager; + + public void setUp() { + sharedPreferences = getContext().getSharedPreferences( + MyTracksSettings.SETTINGS_NAME, 0); + // Let's use default values. + sharedPreferences.edit().clear().commit(); + manager = new AntDirectSensorManager(getContext()); + } + + @SmallTest + public void testBroadcastData() { + manager.setDeviceNumberHRM((short) 42); + byte[] buff = new byte[11]; + buff[1] = AntMesg.MESG_BROADCAST_DATA_ID; + buff[2] = 0; // HRM CHANNEL + buff[10] = (byte) 220; + manager.handleMessage(buff); + + Sensor.SensorDataSet sds = manager.getSensorDataSet(); + assertNotNull(sds); + assertTrue(sds.hasHeartRate()); + assertEquals(Sensor.SensorState.SENDING, + sds.getHeartRate().getState()); + assertEquals(220, sds.getHeartRate().getValue()); + + assertFalse(sds.hasCadence()); + assertFalse(sds.hasPower()); + } + + @SmallTest + public void testHandleChannelId() { + assertEquals(0, manager.getDeviceNumberHRM()); + short id = 42; + byte[] buff = new byte[5]; + buff[3] = 42; + manager.handleChannelId(buff); + assertEquals(id, manager.getDeviceNumberHRM()); + } + + @SmallTest + public void testChannelId() { + byte[] buff = new byte[11]; + buff[1] = AntMesg.MESG_CHANNEL_ID_ID; + buff[3] = 42; + manager.handleMessage(buff); + + assertEquals(42, manager.getDeviceNumberHRM()); + assertEquals(42, + sharedPreferences.getInt( + getContext().getString(R.string.ant_heart_rate_sensor_id_key), -1)); + assertNull(manager.getSensorDataSet()); + } + + @SmallTest + public void testResponseEvent() { + assertEquals(Sensor.SensorState.NONE, manager.getSensorState()); + byte[] buff = new byte[11]; + buff[1] = AntMesg.MESG_RESPONSE_EVENT_ID; + buff[3] = AntMesg.MESG_UNASSIGN_CHANNEL_ID; + manager.handleMessage(buff); + + assertEquals(Sensor.SensorState.DISCONNECTED, manager.getSensorState()); + assertNull(manager.getSensorDataSet()); + } + + // TODO: Test timeout too. +} 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 4dcc0b9d9..3951aa3c1 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 @@ -28,12 +28,26 @@ public class SensorManagerFactoryTest extends AndroidTestCase { @SmallTest public void testCreateZephyr() throws Exception { + assertClassForName(ZephyrSensorManager.class, R.string.zephyr_sensor_type); + } + + @SmallTest + public void testCreateAnt() throws Exception { + assertClassForName(AntDirectSensorManager.class, R.string.ant_sensor_type); + } + + @SmallTest + public void testCreateAntSRM() throws Exception { + assertClassForName(AntSRMSensorManager.class, R.string.srm_ant_bridge_sensor_type); + } + + private void assertClassForName(Class c, int i) { sharedPreferences.edit() - .putString(getContext().getString(R.string.sensor_type_key), - getContext().getString(R.string.zephyr_sensor_type)) - .commit(); + .putString(getContext().getString(R.string.sensor_type_key), + getContext().getString(i)) + .commit(); SensorManager sm = SensorManagerFactory.getSensorManager(getContext()); assertNotNull(sm); - assertTrue(sm instanceof ZephyrSensorManager); + assertTrue(c.isInstance(sm)); } } diff --git a/SignalStrengthSampler/.classpath b/SignalStrengthSampler/.classpath index 57972d993..f9bca44bc 100644 --- a/SignalStrengthSampler/.classpath +++ b/SignalStrengthSampler/.classpath @@ -3,7 +3,7 @@ - + diff --git a/SignalStrengthSampler/AndroidManifest.xml b/SignalStrengthSampler/AndroidManifest.xml index 03d216a34..9ede9ed88 100644 --- a/SignalStrengthSampler/AndroidManifest.xml +++ b/SignalStrengthSampler/AndroidManifest.xml @@ -54,7 +54,8 @@ limitations under the License. - diff --git a/SignalStrengthSampler/default.properties b/SignalStrengthSampler/default.properties index 45aae789b..903536e41 100644 --- a/SignalStrengthSampler/default.properties +++ b/SignalStrengthSampler/default.properties @@ -8,5 +8,6 @@ # project structure. # Project target. -target=Google Inc.:Google APIs:8 +target=Google Inc.:Google APIs:9 +proguard.config=proguard.cfg android.library.reference.1=../MyTracksLib/ diff --git a/SignalStrengthSampler/src/com/google/android/apps/mytracks/signalstrength/SignalStrengthService.java b/SignalStrengthSampler/src/com/google/android/apps/mytracks/signalstrength/SignalStrengthService.java index 7f80c7433..8a9cf4cd3 100644 --- a/SignalStrengthSampler/src/com/google/android/apps/mytracks/signalstrength/SignalStrengthService.java +++ b/SignalStrengthSampler/src/com/google/android/apps/mytracks/signalstrength/SignalStrengthService.java @@ -17,7 +17,9 @@ package com.google.android.apps.mytracks.signalstrength; import static com.google.android.apps.mytracks.signalstrength.SignalStrengthConstants.*; -import com.google.android.apps.mytracks.content.Waypoint; +import com.google.android.apps.mytracks.content.MyTracksProviderUtils; +import com.google.android.apps.mytracks.content.MyTracksProviderUtilsImpl; +import com.google.android.apps.mytracks.content.WaypointCreationRequest; import com.google.android.apps.mytracks.services.ITrackRecordingService; import com.google.android.apps.mytracks.signalstrength.SignalStrengthListener.SignalStrengthCallback; @@ -154,13 +156,6 @@ public class SignalStrengthService extends Service return; } - // We don't set location or track id - My Tracks can fill those - Waypoint wpt = new Waypoint(); - wpt.setName("Signal Strength"); - wpt.setType(Waypoint.TYPE_WAYPOINT); - wpt.setIcon(icon); - wpt.setDescription(description); - try { long waypointId; synchronized (this) { @@ -169,7 +164,11 @@ public class SignalStrengthService extends Service return; } - waypointId = mytracksService.insertWaypointMarker(wpt); + // Create a waypoint. + WaypointCreationRequest request = + new WaypointCreationRequest(WaypointCreationRequest.WaypointType.MARKER, + "Signal Strength", icon, description); + waypointId = mytracksService.insertWaypoint(request); } if (waypointId >= 0) { diff --git a/SignalStrengthSamplerTest/.classpath b/SignalStrengthSamplerTest/.classpath index bee7810d7..e091a2041 100644 --- a/SignalStrengthSamplerTest/.classpath +++ b/SignalStrengthSamplerTest/.classpath @@ -1,9 +1,9 @@ + - - + diff --git a/SignalStrengthSamplerTest/default.properties b/SignalStrengthSamplerTest/default.properties index 420db56e3..77f7ad75b 100644 --- a/SignalStrengthSamplerTest/default.properties +++ b/SignalStrengthSamplerTest/default.properties @@ -8,4 +8,4 @@ # project structure. # Project target. -target=Google Inc.:Google APIs:8 +target=Google Inc.:Google APIs:9