diff --git a/MyTracks/AndroidManifest.xml b/MyTracks/AndroidManifest.xml index 578bd1e48..5f52f7262 100755 --- a/MyTracks/AndroidManifest.xml +++ b/MyTracks/AndroidManifest.xml @@ -161,7 +161,7 @@ limitations under the License. - + diff --git a/MyTracks/res/layout/about.xml b/MyTracks/res/layout/about.xml index 51f432b90..753d860e8 100644 --- a/MyTracks/res/layout/about.xml +++ b/MyTracks/res/layout/about.xml @@ -58,6 +58,11 @@ limitations under the License. android:layout_height="wrap_content" android:layout_width="wrap_content" android:orientation="vertical"> + + + + + + Inactiu + 1 min + 2 min + 5 min + 10 min + 15 min + 30 min + 60 min + + + + + 1 m + 2 m + 3 m + 4 m + 5 m (recomanat) + 10 m + 100 m + + + + 3 ft + 6 ft + 10 ft + 13 ft + 16 ft (recomanat) + 33 ft + 328 ft + + + + + 50 m + 100 m + 200 m (recomanat) + 500 m + 1000 m + 5000 m + + + + 164 ft + 328 ft + 656 ft (recomanat) + 1640 ft + 0.6 miles + 3.1 miles + + + + + poca precisió (adaptable) + gran precisió (adaptable) + el millor (recomanat) + 2 s + 3 s + 4 s + 5 s + 10 s + 20 s + 30 s + 1 min + 2 min + 5 min + 10 min + 15 min + 30 min + + + + + 10 m (senyal GPS excel·lent) + 20 m + 50 m + 100 m + 200 m (recomanat) + 500 m + 1000 m + 2000 m + 5000 m (senyal GPS pobre) + + + + 33 ft (senyal GPS excel·lent) + 66 ft + 165 ft + 328 ft + 656 ft (recomanat) + 1640 ft + 0.6 miles + 1.2 miles + 3.1 miles (senyal GPS pobre) + + + + + Sense divisió + 100 km + 50 km + 25 km + 10 km + 5 km + 1 km + 60 min + 30 min + 15 min + 10 min + 5 min + 2 min + 1 min + + + + No splitting + 100 mi + 50 mi + 25 mi + 10 mi + 5 mi + 1 mi + 60 min + 30 min + 15 min + 10 min + 5 min + 2 min + 1 min + + + + + Mai + 5 min + 10 min + 15 min + Always + + + + + desconegut + tipus barrejats + altre + + córrer + córrer al carrer + pista d\'atletisme + córrer en ruta + a peu + caminar de presa + senderisme + ruta de senderisme + senderisme fora de ruta + escalada + + ciclisme + bici al carrer + bici de muntanya + ruta de bici + + patinatge + patinatge en línia + skate + + snow shoeing + skiing + snow boarding + sled + ice sailing + + pesca + rem + caiac + rem + llanxa motora + ferry + RC boat + surf + windsurf + kitesurf + esquí aquàtic + wakeboard + + avió + hidroavió + avió comercial + helicòpter + globus aerostàtic + dirigible + avió RC + helicòpter RC + salt d\'esquí + vol sense motor + ala delta + parapent + + riding + horse back riding + donkey back riding + + conducció d\'automòbil + conducció de bus + tren + moto + dirtbike + ATV + + landsailing + frisbee + ultimate frisbee + + + + ATM + banc + bar + barbeque + edifici + parada de bús + cafeteria + acampada + càmping + església + cafeteria + botiga de conveniència + aigua potable + caiguda de roques + ferry + llar de foc + estació de bombers + pesca + benzinera + camp de golf + botiga de queviures + perruqueria + port + ruta de senderisme + hotel + casa + informació + intersecció + bústia + mercat + punt de trobada + opera + parking + oficina de correus + sot + comissaria dels mossos + encreuament ferroviari + restaurant + compres + vista + magatzem + piscina + telèfon + teatre + serveis + informació turística + túnel + arbre + cascada + font d\'aigua + punt de referència + + + + Cap + Zephyr Bluetooth + + + diff --git a/MyTracks/res/values-ca/strings.xml b/MyTracks/res/values-ca/strings.xml new file mode 100644 index 000000000..1a001c3e2 --- /dev/null +++ b/MyTracks/res/values-ca/strings.xml @@ -0,0 +1,449 @@ + + + + + Enregistra la ruta + Atura l\'enregistrament + Mode de satèl·lit + Mode de mapa + La meva ubicació + Rutes + Marcadors + Elimina totes les rutes + Paràmetres + Elevació + Seleccioneu la ruta que es visualitzarà + Ruta actual + Darrer segment + Estat del sensor + Estadístiques agregades + + + No s\'ha trobat cap gestor d\'ubicacions en aquest dispositiu. + No s\'ha trobat cap proveïdor d\'ubicació GPS en aquest dispositiu. + S\'ha produït un error quan s\'enviava la ruta a Google My Map. Proveu més tard. + S\'ha prdouït un error quan s\'enviava la ruta a Google Docs. Proveu més tard. + No es pot iniciar l\'enregistrament. Proveu de nou. + No es pot aturar l\'enregistrament. Proveu de nou. + No es pot llegir des del fitxer. + El fitxer no conté un format GPX vàlid. + S\'ha produït un error. Proveu de nou. + No existeix la ruta. + Aquest fitxer és massa gran (ha d\'ésser inferior a 500KB. S\'està cancel·lant. + No es pot inserir un marcador. No hi ha cap ubicació fixa disponible. + + + S\'està esperant per tenir una posició GPS fixa. Assegureu-vos que el cel és visible. + Ara s\'està enregistrant una ruta nova. + S\'ha aturat l\'enregistrament de la ruta. + S\'ha creat un Google My Map nou a maps.google.com. + S\'ha enviat la ruta a maps.google.com. + S\'ha enviat la ruta a un full de càlcul a docs.google.com. + S\'ha enviat la ruta a docs.google.com. + Poden passar uns minuts abans que les rutes noves apareguin. + Piqueu aquí per anar als paràmetres de seguretat i ubicació i activeu \'habilita els satèl·lits GPS\'. Premeu el botó d\'anar enrere per tornar. + Nota: Es mostra una ubicació determinada a partir de les xarxes sense fils i antenes telefòniques. + No es pot determinar la ubicació. Habiliteu més fonts d\'ubicació (vegeu els paràmetres de seguretat i ubicació) i assegureu-vos que el cel és visible. Proveu de nou més tard. + S\'ha inserit un marcader de punt de ruta. + S\'ha inserit un marcador d\'estadístiques. + + + Espereu... + S\'està enviant la ruta a Google. + S\'està llegint la ruta... + S\'està preparant la ruta... + S\'està to Google Maps... + S\'està autenticant a Google Docs... + S\'està enviant la ruta a Google Docs... + S\'està enviant a Google Maps... + S\'està creant un My Map nou... + S\'està imporant una ruta GPS. Això pot trigar una estona. + S\'està escrivint a la targeta SD. Això pot trigar una estona. + S\'estan actualitzant les estadístiques de la ruta... + + + Esteu segur? + Tots els marcadors i rutes s\'eliminaran d\'aquest telèfon. + La ruta i els seus marcadors s\'eliminaran de forma permanent d\'aquest telèfon. + El marcador s\'eliminarà de forma permanent d\'aquest telèfon. + + + Creat per My Tracks a l\'Android. + Creat per <a href=\'http://mytracks.appspot.com\'>My Tracks</a> a l\'Android. + Mapa nou + + + Punts + Enregistrat + Tipus d\'activitat + Tipus de marcador + (Part %d) + + + S\'està enregistrant la ruta... + Ruta %d + + + No + Sí + D\'acord + Error + Fet + Punt de referència + + + (Inici) + (Fi) + + + Mostra + Envia a Google My Maps + Envia a un Google My Map nou + Envia a Google Docs + Envia a Google... + Elimina del telèfon... + Edita els detalls + Escriu a la targeta SD... + Anomena i desa com un fitxer GPX + Anomena i desa com un fitxer KML + Anomena i desa com un fitxer CSV + Anomena i desa com un fitxer TCX + Envia a Google My Maps + Elimina totes les rutes + + Exporta totes les rutes + Exporta les finalitzades + Seleccioneu una ruta. Mantingueu el dit per veure més opcions. + Totes les rutes: + Aquesta ruta + Comparteix amb amics... + Un enllaç a Google My Map + Un fitxer GPX + Un fitxer KML + Un fitxer CSV + Un fitxer TCX + Neteja el mapa + + + Elimina tots els marcadors + Aquest marcador + Elimina + Edita els detalls + Mostra al mapa + Insereix un punt de referència + Insereix estadístiques + Seleccioneu un marcador. Mantingueu el dit per veure més opcions. + + + Detalls de la ruta + Nom de la ruta + Descripció de la ruta + Tipus d\'activitat (p.e. senderisme, bicicleta, còrrer, escalada...) + + + Detalls del marcador + Nom del marcador + Descripció del marcador + Tipus de marcador (e.g. edifici, sot, vista...) + + + Paràmetres de My Tracks + Visualització + Enregistrament + Compartició + + No es troba disponible a la vostra versió de l\'Android + + Freqüència d\'avís + Temps mínin entre dos avisos de veu + + Distància mínima entre punts + La distància més petita entre dos punts enregistrats de la vostra posició + + Distància màxima entre punts + Si dos punts estan més lluny d\'això, es dividirà la ruta + + Temps mínim entre punts + Interval de temps mínim entre dos enregistraments de la vostra posició + + Precisió míima + S\'aturarà l\'enregistrament si la vostra posició no és així de precisa com a mínim + + Fes servir unitats mètriques + Totes les distàncies es mostraran en unitats mètriques + Totes les distàncies es mostraran en unitats imperials + + Fes el mapa públic + Els mapes pujats seran visibles per tothom a maps.google.com + Els mapes pujats no es llistaran públicament a maps.google.com + + Mostra la velocitat + Es mostrarà la velocitat + Es mostrarà el ritme de pas + + Comparteix només l\'URL + Només s\'enviarà l\'URL del mapa quan es comparteixi + També s\'inclouran els continguts de la ruta quan es comparteixi + + Freqüència de divisió + Temps o distància màxima a partir de la qual s\'insertarà un marcador de divisió nou + + Temps d\'espera per reprende la ruta + Temps màxim en minuts a partir del qual l\'anterior ruta enregistrada no es reprendrà en actualitzar el telèfon + + Data i hora com a nom de la ruta + El nom per defecte de la ruta serà la data i hora en que s\'ha iniciat. + Els noms de les rutes seran números que s\'incrementaran per a cada nova ruta. + + Ritme de pas + + Suportat només en Android 2.0 i superior + Tipus de sensor + El tipus de sensor al qual es connecta. + Identificador del sensor + L\'identificador enviat als sensors. + L\'identificador ha de ser un valor sencer. + Sensor bluetooth + El sensor bluetooth al qual es connecta. + Pantalla d\'estadístiques modulars + La pantalla d\'estadístiques es divideix en mòduls diferents. + Mostra totes les estadístiques en una sola pantalla. + + Còpia de seguretat (BETA) + Còpia de seguretat a la targeta SD + Fes la còpia ara + Escriu una còpia completa de totes les rutes i els paràmetres a la targeta SD + Restaura ara + Restaura totes les rutes i els paràmetres des de la targeta SD. + No es poden gestionar les còpies de seguretat mentre s\'enregistra una ruta. + + + La restauració d\'una còpia de seguretat des de la targeta SD sobreescriurà totes les rutes i paràmetres. Esteu segur? + Seleccioneu la còpia que es restaurarà + No s\'ha trobat cap còpia de seguretat. + S\'estan escrivint totes les rutes i els paràmetres a la targeta SD. Això pot trigar una estona... + S\'estan llegint totes les rutes i els paràmetres de la targeta SD. Això pot trigar una estona... + + + Satèl·lits GPS + + + Seleccioneu un mapa del vostre compte: + Crea un mapa nou... + Envia a Google My Maps + S\'estan carregant els mapes... + No s\'han pogut carregar els vostres mapes en aquest moment + Selecciona aquest mapa + Edita els detalls del mapa + Comparteix el mapa amb amics + Comparteix la ruta amb amics + Elimina el mapa del vostre compte + Títol del mapa: + Descripció del mapa: + Fes que aquest mapa i els seus continguts apareguen als resultats de cerca + No permetis que aquest mapa ni els seus continguts es mostrin als resultats de cerca, tot i que sí es podrà visualitzar per altres amb l\'adreça URL directa + Aquest mapa i els seus continguts es mostraran als resultats de cerca + Ni els mapes ni els seus continguts es mostraran als resultats de cerca, però podran visualitzar-se per altres a través d\'un enllaç directe. + Tanca + Edita + Cancel·la + Desa + + + M\'agradaria compartir un mapa amb tu + Crec que estaràs interessat en aquest mapa: + + %s + M\'agradaria compartir una ruta amb tu + Crec que estaràs interessat en aquesta ruta. + + Per verue aquesta ruta fes servir Google Earth o altre programari de mapes. + + + Públic + No llistat + Encara no teniu cap mapa. + + + + Average Moving Speed + Average Moving Pace + + + Velocitat mitjana + Ritme de pas mitjà + + Gràfic d\'elevació + Guany d\'elevació + Elevació + Latitud + Longitud + + + Elevació mínima + Grau mínim + Ritme de pas mínima + + + Elevació màxima + Grau màxim + Velocitat màxima + + S\'està carregant la ruta... + + + Configuració del gràfic + + Eix X + Per distància + Per temps + + + Eix Y + + + + hora + hores + minut + minuts + segon + segons + + + kilòmetres + milles + + + kilòmetres per hora + per kilòmetre + milles per hora + per milla + + + Temps en moviment + Temps total + Temps + La targeta SD no està disponible + Velocitat + Heu d\'iniciar la marxa. + Estadístiques + Distància total + Distància + Energia + Cadència + Freqüència cardíaca + Freqüència cardíaca màxima + S\'està esperant... + + + Acord de llicencia d\'usuari final + Accepta + Rebutja + + + Quant a + Ajuda + Equip: + Versió: + Assistència + Assistència: + + + Envia a... + Envia ara + Crea un mapa nou + Seleccioneu un mapa existent + Envia les estadístiques de la ruta + Envia les estadístiques i els punts + + + No s\'ha trobat cap compte + No s\'ha trobat cap compte on pujar les rutes. + Trieu un compte + + + No s\'ha trobat cap dispositiu d\'emmagatzematge extern. Assegureu-vos que la targeta SD està inserida i que el telèfon no està connectat a un ordinador. + No es pot escriure a un fitxer. + No es pot llegir des del fitxer. + No es pot crear el directori a la targeta SD. + S\'ha finalitzat l\'escriptura a la targeta SD. + S\'ha finalitzat la lectura a la targeta SD. + + + Comprovació d\'actualitzacions + Actualitza ara + Continua sense actualitzar + La vostra versió de %1$s podria no funcionar correctament a partir d\'ara i no està suportada. Actualitzeu per continuar fent servir %1$s. + La vostra versió de %1$s podria no funcionar correctament a partir d\'ara. Heu d\'actualitzar-la ara o continuar fent-la servir pel vostre compte. Algunes funcionalitats com la sincronització podrien no funcionar fins que no actualitzeu. + Hi ha una nova versió disponible de %1$s. Podeu continuar fent servir la vostra versió sense cap problema, però no aconseguireu les darreres funcionalitats fins que no actualitzeu. + + + Benvingut + D\'acord + Quant a My Tracks + + + Sense distància My Tracks + + My Tracks %1$s %2$.2f %3$s en %4$s at %5$s %6$s + + + Avís de les unitats de mesura + Les unitats són mètriques per defecte. Voleu canviar aquest paràmetre? + + + Rati d\'error: %1$d + Relació senyal/soroll: %1$d + Ec/lo: %1$.1f dB + Intensistat GSM: %1$d + Intensitat CDMA: %1$d dBm + Intensitat EVDO: %1$d dBm + + + Opcions del Bluetooth + Associació de dispositius + Obriu els paràmetres de bluetooth de l\'Android per associar un dispositiu + bluetoothSensorsEnabled + Opcions del sensor + + + Connectat a %s + Connexió del sensor bluetooth... + El sensor bluetooth no està connectat + + + Dades del sensor de temps + S\'ha habilitat el mesurador de potència + S\'ha habilitat el monitor de la freqüència cardíaca + Cap gestor de sensors + Habilitat + S\'està connectant + S\'ha connectat + S\'ha desconnectat + Cap servei de sensor + S\'està cercant + S\'està enviant + + diff --git a/MyTracks/res/values-pl/strings.xml b/MyTracks/res/values-pl/strings.xml index 2d597a9e2..017176bb3 100644 --- a/MyTracks/res/values-pl/strings.xml +++ b/MyTracks/res/values-pl/strings.xml @@ -361,6 +361,8 @@ limitations under the License. Wersja: Wsparcie Wsparcie: + + Bartłomiej Niechwiej Wyślij do... diff --git a/MyTracks/res/values/strings.xml b/MyTracks/res/values/strings.xml index b837dcc82..c2d8886e9 100644 --- a/MyTracks/res/values/strings.xml +++ b/MyTracks/res/values/strings.xml @@ -388,6 +388,7 @@ limitations under the License. mytracks@googlegroups.com mytracks.appspot.com Copyright 2009 Google Inc. + Bartlomiej Niechwiej Dylan Casey Leif Hendrik Wilden Rodrigo Damazio Bovendorp diff --git a/MyTracks/src/com/google/android/apps/mytracks/MyTracks.java b/MyTracks/src/com/google/android/apps/mytracks/MyTracks.java index c7ade2cdc..ddf9306eb 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/MyTracks.java +++ b/MyTracks/src/com/google/android/apps/mytracks/MyTracks.java @@ -25,7 +25,6 @@ import com.google.android.apps.mymaps.MyMapsConstants; import com.google.android.apps.mymaps.MyMapsList; 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; @@ -94,9 +93,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 */ 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..2dd891ac2 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; /** @@ -309,6 +310,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 a8ac9aa4a..33bfb6eef 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; @@ -186,55 +189,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) { @@ -792,13 +818,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"); @@ -806,8 +831,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 @@ -951,4 +975,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/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/services/TrackRecordingServiceTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/services/TrackRecordingServiceTest.java index 3391e3957..9f3e0e186 100644 --- a/MyTracksTest/src/com/google/android/apps/mytracks/services/TrackRecordingServiceTest.java +++ b/MyTracksTest/src/com/google/android/apps/mytracks/services/TrackRecordingServiceTest.java @@ -78,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) {