Compare commits

..

166 Commits

Author SHA1 Message Date
Rodrigo Damazio 2eb36aedeb Releasing version 1.0.20. 2010-11-23 15:18:39 -08:00
Bartlomiej Niechwiej 0e623ebfeb Fix a memory leak that was observed each time a recording session was stopped. 2010-11-21 23:49:17 -08:00
Rodrigo Damazio ffb73569e9 Merging Bart's fixes. 2010-11-21 21:11:35 -08:00
Bartlomiej Niechwiej 713cda9455 Addressed Rodrigo's comment. 2010-11-21 21:07:59 -08:00
Bartlomiej Niechwiej 223721cca8 Nullify bineder/checkListener on destroy. 2010-11-21 20:55:44 -08:00
Rodrigo Damazio e7479c8a93 Fixing periodic task restarting. 2010-11-21 20:45:25 -08:00
Bartlomiej Niechwiej 224fe0069b Make sure there is only one instance of TrackRecordingService at a time. 2010-11-21 19:33:14 -08:00
Rodrigo Damazio cb97847392 Addressing comments from a475cd4d33. 2010-11-21 00:44:34 -08:00
Rodrigo Damazio 6a29531467 Second attempt at proper task startup. 2010-11-20 16:21:59 -08:00
Rodrigo Damazio 262e249d2c Also fixing splitter initialization, just for completeness. 2010-11-20 05:46:38 -08:00
Rodrigo Damazio 77479efaef Fixing TTS initialization. 2010-11-20 05:43:09 -08:00
Rodrigo Damazio 898863a8d9 Merging Sandor's second head by ignoring it 2010-11-19 18:37:27 -08:00
Sandor Dornbush 05d555f98d Fix the end track confirmation dialog. 2010-11-19 17:23:27 -08:00
Rodrigo Damazio 868092db10 Fixing intiialization before context is available. 2010-11-19 14:26:11 -08:00
Rodrigo Damazio 7a738c2eca Moving the end recording method to more reasonable location. 2010-11-18 17:14:56 -08:00
Rodrigo Damazio b304dbcba6 Fixing wake lock being held for too long in the service. 2010-11-18 17:13:20 -08:00
Rodrigo Damazio 6e28a5daa4 Merging Matthew's fix for issue 137. 2010-11-18 16:41:21 -08:00
Bartlomiej Niechwiej 8f4f9660ed Addressed Sandor's comments. 2010-11-18 15:42:02 -08:00
Bartlomiej Niechwiej 2d01535651 Fixed a NPE introduced after refactoring dialogManager. 2010-11-17 22:39:50 -08:00
Bartlomiej Niechwiej e57d4669fc Revert API key changes. 2010-11-17 22:25:20 -08:00
Bartlomiej Niechwiej f47c599dea Merge with 63eb7915dafe329e8f06cc33c56122ee15ea6df7 2010-11-17 22:20:15 -08:00
Bartlomiej Niechwiej 7a693a90f7 Make the app single process only, including the following fixes:
1) Simplify SharedPreferences management
2) Shut down signalManager/splitManager to avoid random crashes
3) Get rid of sharedPreferencesChanged from the service interface

All tests pass.
2010-11-17 22:18:50 -08:00
Bartlomiej Niechwiej 9cf51de3f7 Fix a typo in onDestroy. 2010-11-17 20:07:34 -08:00
Matthew Simmons 5089c20cd6 Code review fixes 2010-11-16 11:39:08 -05:00
Bartlomiej Niechwiej a0da23f86a Various fixes to shared preferences. 2010-11-15 23:09:18 -08:00
Rodrigo Damazio 44d24b57c6 Making telephony support optional. 2010-11-15 22:04:48 -08:00
Rodrigo Damazio c143d774d4 Merging Aaron's changes. 2010-11-15 21:50:46 -08:00
Matthew Simmons 9e92240dd0 More self-review 2010-11-15 21:46:19 -05:00
Sandor Dornbush d2c586b096 Fix StatusAnnouncer thread for initialization and prefs in service. 2010-11-15 18:20:10 -08:00
Matthew Simmons fbfbb9408a Self-review 2010-11-15 18:46:23 -05:00
Matthew Simmons fb56665dec Rework authorization failure retry. The retry needs to be synchronous with
the original runone.
2010-11-15 18:37:42 -05:00
Bartlomiej Niechwiej 5b65c216b6 More tests. 2010-11-12 22:18:38 -08:00
Rodrigo Damazio 9ad98c5970 Merging Sandor's changes 2010-11-12 14:37:04 -08:00
Aaron Brown cae9d0f2e5 updated MyTracks/AndroidManifest.xml for inclusion of uses-feature 2010-11-12 17:11:56 +10:00
Sandor Dornbush c0d267c63e Minor formating fixes. 2010-11-11 21:41:39 -08:00
Sandor Dornbush 70704b0a5e Move navcontrols icon decleration to xml. 2010-11-11 21:38:24 -08:00
Sandor Dornbush b135275201 Minor fix for dialog manager. 2010-11-11 16:15:11 -08:00
Bartlomiej Niechwiej 7e1eae6681 Addressed Sandor's comments and added more unit tests and fixed 2 NPEs. 2010-11-11 10:55:15 -08:00
Bartlomiej Niechwiej 30f5d6dc85 Fix two NPEs and add more test coverage. 2010-11-11 00:31:56 -08:00
Sandor Dornbush a50dd1168b Fix format error. 2010-11-09 13:57:19 -08:00
Sandor Dornbush 62fa1620aa Actually add the dialog tests. 2010-11-08 11:31:14 -08:00
Sandor Dornbush a0d42c8b59 Add basic tests to the DialogManager. 2010-11-08 11:28:39 -08:00
Bartlomiej Niechwiej a21663da61 Fix a NPE (replace MyTracks.getInstance() with the parent activity). 2010-11-06 23:37:14 -07:00
Bartlomiej Niechwiej 67a0382942 Fixed another bug reported by users (division by zero).
The issue was most likely related to a race condition (selectedTrackId can change in the UI thread, while the track is being processed in the handler thread).

Simulated slow drawing by adding 50ms, works pretty cool :)
2010-11-06 00:31:52 -07:00
Bartlomiej Niechwiej f5c2467d01 Fix StringIndexOutOfBoundsException in ChartsExtendedEncoder.getEncodedValue().
Found in user error reports (3rd top issue):
java.lang.StringIndexOutOfBoundsException
at com.google.android.apps.mytracks.util.ChartsExtendedEncoder.getEncodedValue(ChartsExtendedEncoder.java:45)
at com.google.android.apps.mytracks.util.ChartURLGenerator.getChartUrl(ChartURLGenerator.java:162)
at com.google.android.apps.mytracks.util.ChartURLGenerator.getChartUrl(ChartURLGenerator.java:60)
at com.google.android.apps.mytracks.util.StringUtils.generateTrackDescription(StringUtils.java:366)
at com.google.android.apps.mytracks.io.SendToMyMaps.doUpload(SendToMyMaps.java:581)
at com.google.android.apps.mytracks.io.SendToMyMaps.access$10(SendToMyMaps.java:538)
at com.google.android.apps.mytracks.io.SendToMyMaps$5.run(SendToMyMaps.java:517)
at android.os.Handler.handleCallback(Handler.java:587)
at android.os.Handler.dispatchMessage(Handler.java:92)
at android.os.Looper.loop(Looper.java:123)
at android.os.HandlerThread.run(HandlerThread.java:60)
2010-11-05 22:52:42 -07:00
Bartlomiej Niechwiej a3abcc8094 Provide a workaround for binding to the service once it has been started. It fixes the failing tests (4). 2010-11-05 20:45:06 -07:00
Sandor Dornbush edf439f301 Merge Bart's bug fixes and tests. 2010-11-05 15:37:20 -07:00
Sandor Dornbush 2245563250 Fix broken build. 2010-11-05 14:51:06 -07:00
Bartlomiej Niechwiej bca21d5515 Addressed Rodrigo's comments. 2010-11-05 10:11:55 -07:00
Bartlomiej Niechwiej 89cae2e7c9 Fix a regression related refactoring recordingTrackId management.
In particular:
1) Add a workaround to {start/end}Recording to update recordingTrackId to trigger a notification
2) Add a bunch of unit tests for MyTracks
3) Clear minor style issues in a few classes
2010-11-04 23:32:31 -07:00
Sandor Dornbush 26c381a4c4 Address Bart's comments and more dialog clean up. 2010-11-04 17:44:08 -07:00
Sandor Dornbush 4bc5aa050c Add tests for the TempFileCleaner. 2010-11-04 17:07:59 -07:00
Rodrigo Damazio cd44b0d180 Merging Sandor's changes. 2010-11-03 21:08:50 -02:00
Rodrigo Damazio 59ee91ca5d Merging my own changes. 2010-11-03 20:59:32 -02:00
Rodrigo Damazio 59165a7033 Ignoring Matthew's second head by no-op merging it. 2010-11-03 20:53:50 -02:00
Rodrigo Damazio 93d242c05f Merging Matthew's first head. 2010-11-03 20:42:35 -02:00
Rodrigo Damazio 4bbb6591e3 Merging Bart's changes. 2010-11-03 19:59:35 -02:00
Sandor Dornbush 7df8025f6a Merge with head and address comments. 2010-11-03 12:53:12 -07:00
Bartlomiej Niechwiej f19c52f40e Addressed Sandor's comments. 2010-11-02 08:31:21 -07:00
Bartlomiej Niechwiej b7840779fe Revert API Key Change. 2010-11-02 08:26:31 -07:00
Rodrigo Damazio 28042a8cc9 Adding tests to voice announcements.
If anyone asks, I didn't write this code.
2010-11-02 02:19:43 -02:00
Matthew Simmons b2907226bf Code review comments 2010-11-01 15:21:33 -04:00
Matthew Simmons a2f46821f5 Fix author line 2010-11-01 14:32:19 -04:00
Matthew Simmons b7385cd4cc Combine b2f2f9e9e9 and 8774b3663c
Allow users to choose the default track name policy.  By default, we'll switch
to a new, timestamp-based name.  If they change the pref, they'll get the
old-style 'track n' style.
2010-11-01 13:48:33 -04:00
Matthew Simmons 718c65818c Allow users to choose the track name policy. 2010-11-01 12:59:56 -04:00
Rodrigo Damazio 91e2293606 Adding proper AndroidMock configuration - requires setting eclipse variables 2010-11-01 05:30:16 -02:00
Rodrigo Damazio 3c4a921f9a Addressing review suggestions 2010-11-01 05:27:04 -02:00
Rodrigo Damazio d04b333126 Merging 2010-11-01 02:05:42 -02:00
Rodrigo Damazio 0a4bef4d9a Fixing concurrent access to the content provider by making it have a single instance in the same process as the service. 2010-10-29 17:35:54 -02:00
Bartlomiej Niechwiej a0f090a83a Bug fixing and refactoring:
1) Make sure no one but service changes recordingTrackId (which is owned by it)
2) Cache sharedPreferences
3) Clear recordingTrackId if it doesn't correspond to a valid track or the service is not recording
4) More unit tests
2010-10-28 22:49:01 -07:00
Bartlomiej Niechwiej dc8ee5943c Merge with c10bc69662325f00bf166b783cc3ab1b78ccfed9 2010-10-28 20:48:01 -07:00
Bartlomiej Niechwiej 9132c36aa7 Use a test android:apiKey instead of the official one. 2010-10-28 20:47:42 -07:00
Sandor Dornbush 224c0d759f Minor fixes noticed on first review. 2010-10-28 17:13:59 -07:00
Sandor Dornbush ffcaca2db1 Refactor a few 100 lines of code out of the main Activity. 2010-10-28 17:08:59 -07:00
Sandor Dornbush 409fc4a43b Address Bart's comments. 2010-10-28 15:07:01 -07:00
Sandor Dornbush 16968f3f2b Fix copyright notice. 2010-10-28 14:50:03 -07:00
Sandor Dornbush e3fd0dce21 Undo changes to the eclipse prefs. 2010-10-28 14:47:00 -07:00
Sandor Dornbush 53b6b6deca Add tests to ChartValueSeries.
Also other minor fixes.
2010-10-28 14:43:31 -07:00
Rodrigo Damazio 26f47a50d0 Fixing issue 34: Not making voice announcements during a phone call 2010-10-27 03:07:17 -02:00
Bartlomiej Niechwiej 3cf77374c4 Fix merge. 2010-10-26 20:19:54 -07:00
Bartlomiej Niechwiej a1129c3f0f Merge with c58e4b8519d3ed6959a96192286b83aa1b77f6f3 2010-10-26 20:17:15 -07:00
Bartlomiej Niechwiej 2fe74603a3 Merge with 96ee47b2d8639a0cf62a387f3360640643fbeb0a 2010-10-26 20:14:41 -07:00
Bartlomiej Niechwiej 54d517363d Merge with e34329d612571554d6424d5941dfad57e35612a0 2010-10-26 19:39:46 -07:00
Bartlomiej Niechwiej 948fe0c0e6 Merge with 68eb8dff3a87698ace057447dff40a07cf673f2e 2010-10-26 19:39:20 -07:00
Bartlomiej Niechwiej dbd8e6c0e9 Merge with 5ed61c63ba59f4748231ea14bbbc222423d0b08a 2010-10-26 19:38:51 -07:00
Bartlomiej Niechwiej 02b5601933 Remove unnecessary log. 2010-10-26 19:32:24 -07:00
Sandor Dornbush 3ed40608a2 Merging from head. 2010-10-26 14:44:31 -07:00
Bartlomiej Niechwiej 36d7c2dd91 Allow to move the app to SD card (by default goes to internal storage). This is a frequently requested feature (based on market feedback). 2010-10-26 00:33:38 -07:00
Bartlomiej Niechwiej fa7ef68851 Addressed Sandor's comments. 2010-10-25 22:35:38 -07:00
Matthew Simmons f84877a4fc Issue 86: Remove - from the list of disallowed characters for filenames. 2010-10-25 14:59:20 -04:00
Matthew Simmons 261fb62e24 Changed the default new track name from "track %d" to "date time", where "date"
is a short, locale-aware version of the track's start date, and "time" is the
track's start time.
2010-10-25 14:15:16 -04:00
Bartlomiej Niechwiej 48d8aa4cd8 Introduce an interface to decouple platform specific API from the classes that require it. Also:
1) Put the service into foreground to prevent from being killed by the system
2) Fix a bug the prevented MyTracks from running on pre-Eclair devices
2010-10-24 21:24:11 -07:00
Bartlomiej Niechwiej 95ff0d3db6 Fix a typo in shared preferences and minor cleanup. 2010-10-24 18:55:35 -07:00
Bartlomiej Niechwiej 2dc1d914c0 Several fixes to the service and more test coverage.
Notably:
1) Fixed isRecording to not return false if service is down (for instance, it may be being restarted)
2) Added more checks to prevent from invalid starting/stopping (if in already in progress/not started).
2010-10-24 14:16:43 -07:00
Bartlomiej Niechwiej fc06701009 Fix several bugs and clean up statistics management.
1) Decouple clock from TripStatisticsBuilder.
2) Introduce a new param to constructor to automatically resume at given time
3) Fix various statsbuilder initialization issues in the service (wrong order, missing initialization)
2010-10-24 03:30:58 -07:00
Bartlomiej Niechwiej ee4f334296 Fixed a NPE under certain circumstances (move initialization to onCreate()). 2010-10-24 03:26:32 -07:00
Bartlomiej Niechwiej cac2395237 More unit tests for TracksRecordingService and fix a few bugs. Made the service less forgiving rather than silently ignoring invalid state. 2010-10-24 00:48:53 -07:00
Bartlomiej Niechwiej 5afc3f13cf Minor refactoring: remove dependency on Context and replace it with ContentResolver. 2010-10-23 19:04:57 -07:00
Rodrigo Damazio 9030917c14 Merging Bart's changes. 2010-10-22 15:34:24 -02:00
Bartlomiej Niechwiej 9905def352 Terminate tracker update thread on destroy. 2010-10-22 10:14:36 -07:00
Bartlomiej Niechwiej a3793aa92b Minor updates. 2010-10-22 10:00:15 -07:00
Nulled 76c8498956 updated zh-cn translation 2010-10-21 17:51:40 +08:00
Nulled 75bd8ea5a2 Updated zh-cn translation. fixed some errors. 2010-10-15 18:15:43 +08:00
Nulled 50a5e864ac updated zh-cn translation 2010-10-15 18:04:56 +08:00
Rodrigo Damazio 352fc26f7e Fixing tests 2010-10-14 23:25:42 -03:00
Rodrigo Damazio 44afa1623e Making locations be reused when writing tracks. 2010-10-14 21:06:59 -03:00
Rodrigo Damazio 14acafabfc Getting rid of TrackBuffer. 2010-10-14 20:42:02 -03:00
Rodrigo Damazio 773af58d03 Fixing issue 190 (third step: reordering methods in execution order - no other changes) 2010-10-14 18:44:55 -03:00
Rodrigo Damazio 352b851e9c Fixing issue 190 (second step: refactoring mymaps uploading) 2010-10-14 18:35:51 -03:00
Rodrigo Damazio 8353a01082 Addressing review suggestions. 2010-10-14 16:41:20 -03:00
Bartlomiej Niechwiej b70e001206 Addressed Rodrigo's comments. 2010-10-13 21:58:26 -07:00
Rodrigo Damazio a741bb4d55 Fixing issue 190 (first step: dirty hack) 2010-10-13 22:03:31 -03:00
Rodrigo Damazio 24adee3d54 Addressing review suggestions and adding tests. 2010-10-13 21:53:16 -03:00
Bartlomiej Niechwiej 09203ffb95 Improved test coverage and minor refactoring/improvements. 2010-10-13 12:29:00 -07:00
Bartlomiej Niechwiej e94338f4c6 Fix a drawing lag during track recording.
For a track with ~2,500 points, the average drawign would take ~30-50ms, now it takes about 20ms (21ms if software clipping is enabled).

Removed all possible congestion points by introducing a blocking queue.

This change includes many fixes, including:
1) Major refactoring and clean up of the overlay, sped up drawing by a factor of 2x
2) Disable software clipping as it was slower by about 5% than w/o clipping and would trigger a bug related to map view spanning
3) Add waypoints synchronization  and fixed a bug related to skipping the first wpt
4) Add a simple unit test
2010-10-11 21:55:12 -07:00
Rodrigo Damazio dd9dd8ee81 Refactoring API level detection. 2010-10-11 22:28:02 -03:00
Rodrigo Damazio 01d6dcac59 Merging Bart's Polish fix. 2010-10-07 21:59:11 -03:00
Rodrigo Damazio ad0ee6b0d6 Merging Bart's Polish fix. 2010-10-07 21:58:07 -03:00
Rodrigo Damazio 769c8c228e Added tag v1.0.19 for changeset b096086934d6 2010-10-07 21:21:52 -03:00
Rodrigo Damazio 50fcd67085 Releasing version 1.0.19 (bugfix release) 2010-10-07 21:21:23 -03:00
Sandor Dornbush 52d03e6b7d Address Rodrigo's comments. 2010-10-07 16:28:42 -07:00
Sandor Dornbush b949cd0039 Really fix bug. 2010-10-07 12:39:05 -07:00
Sandor Dornbush e527170315 Fix recording error. 2010-10-07 12:01:32 -07:00
Sandor Dornbush ec7d210c36 Fix recording error. 2010-10-07 11:51:54 -07:00
Bartlomiej Niechwiej 74a4089464 Fix incorrect Polish translation related to delete track(s)/marker confirmation dialog (Issue 176). 2010-10-07 11:36:39 -07:00
Rodrigo Damazio fa9e14f574 Added tag v1.0.18 for changeset 7c67cd917be3 2010-10-05 17:12:20 -03:00
Rodrigo Damazio cdbeefcd16 Releasing version 1.0.18 2010-10-05 17:11:52 -03:00
Rodrigo Damazio ffdd5f4091 Merging Bart's changes 2010-10-05 14:48:43 -03:00
Bartlomiej Niechwiej bb60525780 Rename intent name to be uppercase. 2010-10-05 10:39:07 -07:00
Bartlomiej Niechwiej 82634af649 Minor refactoring. 2010-10-05 09:47:15 -07:00
Bartlomiej Niechwiej 3643ed9f13 Add missing Polish translations. 2010-10-04 23:07:38 -07:00
Bartlomiej Niechwiej fac59ab705 Partially fix a nasty bug related to losing a tracking session on service crash. 2010-10-04 21:53:34 -07:00
Bartlomiej Niechwiej 1e343b0d12 Limit the maximum number of auto-resume retry attempts to 3. 2010-10-04 21:17:56 -07:00
Bartlomiej Niechwiej 34bb467b97 Merge with 25704964a526fcfe09919af8f29c1e526b5409ec 2010-10-04 16:34:38 -07:00
Sandor Dornbush b092525c10 More swedish fixes. 2010-10-04 16:33:25 -07:00
Bartlomiej Niechwiej 256dac3a82 Moved showNotification() outside if clause. 2010-10-03 23:34:19 -07:00
Bartlomiej Niechwiej dfe947f54d Fixed an exception in MyTracks (added isBound), moved resumeState from onStart to onCreate in TrackRecordingService and some minor refactoring. 2010-10-03 23:28:12 -07:00
Andreas Bockert ce09407816 Fixed a number incorrect translation in Swedish language. 2010-10-01 22:33:16 +02:00
Bartlomiej Niechwiej beee6af79d Apply review feedback. 2010-09-30 23:46:47 -07:00
Bartlomiej Niechwiej c282562867 Introduce getLastModified() in Tracks class and fixe a NullPointerException in service if null intent is passed by the system. 2010-09-30 21:54:07 -07:00
Rodrigo Damazio 2aed9b3a33 Fixing track writer test after memory changes 2010-09-29 18:49:13 -03:00
Rodrigo Damazio 210910f21e Merging aggregated statistics change 2010-09-29 18:46:45 -03:00
Bartlomiej Niechwiej 1d666318ae Fix a bug in maybeResumeTrack. Still not sure if it works correctly (need to verify on real device). 2010-09-29 00:33:28 -07:00
Bartlomiej Niechwiej 4df6095089 Implement automatic restart of MyTracks on phone reboot.
In addition fix the following issues/add functionality:
0) Introduce MyTracksReceiver for listening on broadcast messages
1) Remove unnecessary call to VerifyChecker to avoid 403.
2) Clean up TrackRecordingService by adding private methods for common functionality
3) Fix a bug related to not canceling a timer after service was stopped
4) Add a new preference item to control behavior of new functionality
5) Add a unit test for the service (still need to figure out one issue with mocking content resolver).
2010-09-28 22:46:24 -07:00
Rodrigo Damazio d4edcce224 Uncommenting overlooked XML. 2010-09-27 21:42:59 -03:00
Rodrigo Damazio d7c6761841 Addressing comments I originally made on Sandor's change. 2010-09-27 21:02:52 -03:00
Rodrigo Damazio 5733224f8d Merging Sandor's memory footprint improvement 2010-09-27 17:41:34 -03:00
Rodrigo Damazio 22fd5042bc Merging Sandor's memory footprint improvement 2010-09-27 17:37:20 -03:00
Rodrigo Damazio 44a6f2d448 Further improving/fixing drawing, and adding a bounding box for debugging. 2010-09-27 16:14:04 -03:00
Rodrigo Damazio cd9c17f9b3 Fixing leftover from another commit - commenting out testing method. 2010-09-27 16:11:17 -03:00
Rodrigo Damazio dc07bcf0fa Fixing call to HandlerThread.quit() which only exists in API level 5+. 2010-09-27 14:54:36 -03:00
Rodrigo Damazio 248b8fb4fd Fixing bad resource in preferences (only used in 1.5). 2010-09-27 14:53:30 -03:00
Sandor Dornbush 35182a1483 Merge changes. 2010-09-23 14:30:44 -07:00
Sandor Dornbush 21b0a517a9 Merged Rodrigo's changes. 2010-09-23 11:07:31 -07:00
Sandor Dornbush 19a770445e Submit my changes before merging. 2010-09-23 11:02:45 -07:00
Rodrigo Damazio c6512f1376 Preparing to further isolate non-UI code from map drawing. 2010-09-22 22:10:36 -03:00
Rodrigo Damazio 2cb3963138 Refactoring track drawing 2010-09-22 22:04:07 -03:00
Sandor Dornbush d05c394c68 Trying to track down the missing end of track bug. 2010-09-21 15:10:56 -07:00
Sandor Dornbush 797de31960 Merging fixes. 2010-09-14 10:52:55 -07:00
Sandor Dornbush 872d2d4337 Correctly take preference as a long and stop dividing bounds by E6. 2010-09-08 17:36:58 -07:00
FNelson b84da40365 Merge with 28a6c6c0e8f12e489fa501609be4b564eb822f55
(Merge to Head)
2010-09-02 12:23:32 +01:00
FNelson 236bcba2bd Changing retrieveAllTracks to getAllTracks 2010-08-12 14:31:03 +01:00
FNelson 0347877290 Tidy up to adhere to style guidlines 2010-08-05 12:51:46 +01:00
FNelson b464526d5e Aggregated stats in first working state. 2010-08-04 11:58:43 +01:00
FNelson 0ccd05e605 First stab at aggregated stats. Still needs work. Check in to work on home computer 2010-07-30 15:04:35 +01:00
FNelson 916e4586c5 First HG commit test. 2010-07-28 10:38:26 +01:00
114 changed files with 6316 additions and 2743 deletions
+1
View File
@@ -3,3 +3,4 @@ syntax:glob
*/bin
*/gen/**
.metadata
MyTracksTest/annotations.log
+10 -10
View File
@@ -1,10 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="con" path="com.android.ide.eclipse.adt.ANDROID_FRAMEWORK"/>
<classpathentry kind="lib" path="libs/googleloginclient-helper.jar"/>
<classpathentry kind="lib" path="libs/google-common.jar"/>
<classpathentry kind="lib" path="libs/accounts.jar"/>
<classpathentry kind="src" path="src"/>
<classpathentry kind="src" path="gen"/>
<classpathentry kind="output" path="bin"/>
</classpath>
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="con" path="com.android.ide.eclipse.adt.ANDROID_FRAMEWORK"/>
<classpathentry kind="lib" path="libs/googleloginclient-helper.jar"/>
<classpathentry kind="lib" path="libs/google-common.jar"/>
<classpathentry kind="lib" path="libs/accounts.jar"/>
<classpathentry kind="src" path="src"/>
<classpathentry kind="src" path="gen"/>
<classpathentry kind="output" path="bin"/>
</classpath>
@@ -1,4 +1,4 @@
#Thu Aug 12 18:54:52 BRT 2010
#Wed Sep 29 18:44:16 BRT 2010
eclipse.preferences.version=1
org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6
+31 -6
View File
@@ -1,8 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.google.android.maps.mytracks"
android:versionCode="18"
android:versionName="1.0.17">
android:installLocation="auto"
android:versionCode="21"
android:versionName="1.0.20">
<application android:icon="@drawable/arrow_icon"
android:label="@string/app_name"
android:backupAgent="com.google.android.apps.mytracks.io.backup.MyTracksBackupAgent">
@@ -12,7 +13,6 @@
android:value="AEdPqrEAAAAIi-_QiwoRSc9_bAC9cmuNXTQyU8ajJmGtKdhskQ" />
<provider android:name="com.google.android.apps.mytracks.content.MyTracksProvider"
android:multiprocess="true"
android:authorities="com.google.android.maps.mytracks" />
<activity android:name="com.google.android.apps.mytracks.MyTracks"
@@ -56,8 +56,11 @@
<activity android:name="com.google.android.apps.mytracks.MyTracksSettings"
android:label="@string/mytracks_settings" />
<activity android:name="com.google.android.apps.mytracks.AggregatedStatsActivity"
android:label="@string/aggregated_stats" />
<activity android:name="com.google.android.apps.mytracks.StatsActivity"
android:label="@string/statistics"/>
android:label="@string/statistics" />
<activity android:name="com.google.android.apps.mytracks.ChartActivity"
android:label="@string/elevation_label"/>
@@ -66,14 +69,21 @@
android:label="@string/welcome"/>
<service android:name="com.google.android.apps.mytracks.services.TrackRecordingService"
android:process=":trackrecord"
android:label="Track Recording Service"
android:icon="@drawable/arrow_320"
android:enabled="true"
android:exported="true" />
<receiver android:name="com.google.android.apps.mytracks.MyTracksReceiver">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
</application>
<!-- Permissions to initialize service -->
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<!-- Permissions for recording tracks and showing maps -->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
@@ -90,6 +100,22 @@
<uses-permission android:name="com.google.android.googleapps.permission.GOOGLE_AUTH.writely" />
<uses-permission android:name="com.google.android.providers.gsf.permission.READ_GSERVICES" />
<!-- Permissions for misc services -->
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<!-- Features required -->
<uses-feature android:name="android.hardware.location" android:required="true"/>
<uses-feature android:name="android.hardware.location.gps" android:required="true"/>
<uses-feature android:name="android.hardware.touchscreen" android:required="true"/>
<!-- Features optional -->
<uses-feature android:name="android.hardware.bluetooth" android:required="false"/>
<uses-feature android:name="android.hardware.location.network" android:required="false"/>
<uses-feature android:name="android.hardware.sensor.compass" android:required="false"/>
<uses-feature android:name="android.hardware.telephony" android:required="false"/>
<uses-feature android:name="android.hardware.telephony.gsm" android:required="false"/>
<uses-feature android:name="android.hardware.touchscreen.multitouch" android:required="false"/>
<uses-sdk android:minSdkVersion="3"/>
<supports-screens
android:largeScreens="true"
@@ -97,4 +123,3 @@
android:smallScreens="true"
android:anyDensity="true" />
</manifest>
+2
View File
@@ -9,9 +9,11 @@
<booleanAttribute key="com.android.ide.eclipse.adt.wipedata" value="false"/>
<listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_PATHS">
<listEntry value="/MyTracks"/>
<listEntry value="/MyTracks/AndroidManifest.xml"/>
</listAttribute>
<listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_TYPES">
<listEntry value="4"/>
<listEntry value="1"/>
</listAttribute>
<listAttribute key="org.eclipse.debug.ui.favoriteGroups">
<listEntry value="org.eclipse.debug.ui.launchGroup.debug"/>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

@@ -17,11 +17,11 @@ limitations under the License.
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_window_focused="false"
android:drawable="@drawable/btn_arrow_left_default" />
android:drawable="@drawable/left_arrow_chart" />
<item android:state_pressed="true"
android:drawable="@drawable/btn_arrow_left_press" />
android:drawable="@drawable/left_arrow_chart_press" />
<item android:drawable="@drawable/btn_arrow_left_default" />
<item android:drawable="@drawable/left_arrow_chart" />
</selector>
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

@@ -17,11 +17,11 @@ limitations under the License.
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_window_focused="false"
android:drawable="@drawable/btn_arrow_right_default" />
android:drawable="@drawable/left_arrow_stats" />
<item android:state_pressed="true"
android:drawable="@drawable/btn_arrow_right_press" />
android:drawable="@drawable/left_arrow_stats_press" />
<item android:drawable="@drawable/btn_arrow_right_default" />
<item android:drawable="@drawable/left_arrow_stats" />
</selector>
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2010 Google Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_window_focused="false"
android:drawable="@drawable/left_arrow_track" />
<item android:state_pressed="true"
android:drawable="@drawable/left_arrow_track_press" />
<item android:drawable="@drawable/left_arrow_track" />
</selector>
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2010 Google Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_window_focused="false"
android:drawable="@drawable/right_arrow_chart" />
<item android:state_pressed="true"
android:drawable="@drawable/right_arrow_chart_press" />
<item android:drawable="@drawable/right_arrow_chart" />
</selector>
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2010 Google Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_window_focused="false"
android:drawable="@drawable/right_arrow_stats" />
<item android:state_pressed="true"
android:drawable="@drawable/right_arrow_stats_press" />
<item android:drawable="@drawable/right_arrow_stats" />
</selector>
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2010 Google Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_window_focused="false"
android:drawable="@drawable/right_arrow_track" />
<item android:state_pressed="true"
android:drawable="@drawable/right_arrow_track_press" />
<item android:drawable="@drawable/right_arrow_track" />
</selector>
+6 -1
View File
@@ -20,10 +20,15 @@
android:icon="@android:drawable/ic_menu_preferences"
android:orderInCategory="1001"
android:title="@string/settings"/>
<item android:id="@+id/menu_aggregated_stats"
android:icon="@android:drawable/ic_menu_info_details"
android:orderInCategory="1002"
android:title="@string/aggregated_stats"/>
<item android:id="@+id/menu_help"
android:icon="@android:drawable/ic_menu_info_details"
android:orderInCategory="1002"
android:orderInCategory="1003"
android:title="@string/help"/>
</menu>
-1
View File
@@ -155,7 +155,6 @@ limitations under the License.
<string name="settings_announcement_frequency">Dauer zwischen Ansagen:</string>
<string name="settings_announcement_frequency_summary">Kürzeste Zeit zwichen zwei Ansagen</string>
<string name="settings_announcement_not_available_summary">Gesprochene Ansagen sind mit Ihrer Version von Android nicht verfügbar.</string>
<string name="settings_min_recording_distance">Kleinste Entfernung zwischen Punkten:</string>
<string name="settings_min_recording_distance_summary">Kleinste Entfernung zwischen zwei Positionsaufzeichnungen</string>
-1
View File
@@ -158,7 +158,6 @@ limitations under the License.
<string name="settings_announcement_frequency">Tiempo entre anuncios</string>
<string name="settings_announcement_frequency_summary">Tiempo mínimo entre dos anuncios de voz</string>
<string name="settings_announcement_not_available_summary">Conversión de anuncios de texto a voz no esta disponible en su versión de Android</string>
<string name="settings_min_recording_distance">Distancia mínima entre puntos consecutivos</string>
<string name="settings_min_recording_distance_summary">Distancia mínima entre dos grabaciones de posición</string>
-1
View File
@@ -158,7 +158,6 @@ limitations under the License.
<string name="settings_announcement_frequency">Fréquence des annonces</string>
<string name="settings_announcement_frequency_summary">Temps minimum entre eux annonces vocales</string>
<string name="settings_announcement_not_available_summary">La synthèse vocale n\'est pas disponible pour cette version d\'Android</string>
<string name="settings_min_recording_distance">Distance min. entre 2 points</string>
<string name="settings_min_recording_distance_summary">Plus petite distance entre deux enregistrements de votre position</string>
-1
View File
@@ -164,7 +164,6 @@ limitations under the License.
<string name="settings_announcement_frequency">Intervallo fra due annunci:</string>
<string name="settings_announcement_frequency_summary">Tempo minimo fra due annunci audio</string>
<string name="settings_announcement_not_available_summary">Gli annunci audio non sono disponibili in questa varsione di Android</string>
<string name="settings_min_recording_distance">Distanza minima fra due punti successivi:</string>
<string name="settings_min_recording_distance_summary">Distanza minima fra due successive registrazioni della posizione</string>
+1 -1
View File
@@ -164,7 +164,7 @@ limitations under the License.
<string name="settings_announcement_frequency">定期通知</string>
<string name="settings_announcement_frequency_summary">音声通知間隔の最小時間</string>
<string name="settings_announcement_not_available_summary">お使いのAndroidの版では音声読み上げ機能がありません</string>
<string name="settings_min_recording_distance">ポイント間の最短距離</string>
<string name="settings_min_recording_distance_summary">位置記録間の最短距離</string>
+9
View File
@@ -146,6 +146,15 @@ limitations under the License.
<item>2 min</item>
<item>1 min</item>
</string-array>
<!-- Auto-resume track timeout -->
<string-array name="auto_resume_track_timeout_options">
<item>Nigdy</item>
<item>5 min</item>
<item>10 min</item>
<item>15 min</item>
<item>Zawsze</item>
</string-array>
<!-- Other non-preference arrays -->
<array name="activity_types">
+40 -6
View File
@@ -29,6 +29,7 @@ limitations under the License.
<string name="choose_track_message">Wybierz trasę do podglądu</string>
<string name="current_track">Obecna trasa</string>
<string name="current_segment">Ostatni segment</string>
<string name="aggregated_stats">Statystyki Grupowe</string>
<!-- Error messages -->
<string name="error_no_location_manager">Brak menedżera lokalizacji na urządzeniu!</string>
@@ -75,9 +76,9 @@ limitations under the License.
<!-- Used by DeleteAllDataHandler -->
<string name="are_you_sure_question">Jesteś pewien?</string>
<string name="all_data_will_be_permanently_deleted">Wszystkie trasy i znaczniki zostaną usunięte z tego telefonu.</string>
<string name="track_will_be_permanently_deleted">Trasy i ich znaczniki zostaną całkowicie usunięte z tego telefonu.</string>
<string name="marker_will_be_permanently_deleted">Znacznik zostanie całkowicie usunięty z tego telefonu.</string>
<string name="all_data_will_be_permanently_deleted">Wszystkie trasy i znaczniki zostaną usunięte z telefonu.</string>
<string name="track_will_be_permanently_deleted">Trasa i jej znaczniki zostanie całkowicie usunięta z telefonu.</string>
<string name="marker_will_be_permanently_deleted">Znacznik zostanie całkowicie usunięty z telefonu.</string>
<!-- Text and links when uploading to Google My Maps -->
<string name="new_map_description">Utworzono za pomocą My Tracks na Android.</string>
@@ -164,7 +165,7 @@ limitations under the License.
<string name="settings_announcement_frequency">Częstotliwość komunikatów</string>
<string name="settings_announcement_frequency_summary">Minimalny czas między dwoma komunikatami głosowymi</string>
<string name="settings_announcement_not_available_summary">Brak wsparcia dla syntezy mowy w twojej wersji Androida</string>
<string name="settings_min_recording_distance">Min. odległość między punktami</string>
<string name="settings_min_recording_distance_summary">Najmniejsza odległość między dwoma zapisami twojej pozycji</string>
@@ -192,14 +193,45 @@ limitations under the License.
<string name="settings_share_url_only">Dziel się tylko odnośnikiem</string>
<string name="settings_share_url_only_summary_on">Tylko odnośnik do mapy będzie wysyłany podczas dzielenia się</string>
<string name="settings_share_url_only_summary_off">Zawartość trasy również będzie przesyłana</string>
<string name="settings_share_url_only_summary_off">Zawartość trasy będzie również przesyłana</string>
<string name="settings_signal_sampling_frequency">Częstotliowść próbkowania sygnału</string>
<string name="settings_signal_sampling_frequency_summary">Czas między dwoma zapisami siły sygnału telefonu</string>
<string name="settings_split_frequency">Częstotliwość rozdzielania</string>
<string name="settings_split_frequency_summary">Maksymalny czas lub odległość po którym wstawiony zostanie znacznik rozdzielenia</string>
<string name="settings_auto_resume_track_timeout">Automatyczne wznawianie rejestracji trasy</string>
<string name="settings_auto_resume_track_timeout_summary">Maksymalny czas w minutach, który upłynął od ostatniego restartu telefonu, a po którym nie zostania wznowiona poprzednia trasa</string>
<string name="pace_label">Tempo</string>
<string name="eclair_required">Dostępne tylko w wersji Android 2.0 lub nowszej</string>
<string name="settings_sensor_type">Typ Czujnika</string>
<string name="settings_sensor_type_summary">Typ czujnika do podłączenia.</string>
<string name="settings_sensor_id">Id Czujnika</string>
<string name="settings_sensor_id_summary">Identyfikator wysyłany do czujników.</string>
<string name="sensor_id_malformed">Identyfikator musi być liczbą całkowitą.</string>
<string name="bluetooth_sensor_title">Czujnik bluetooth</string>
<string name="bluetooth_sensor_summary">Czujnik bluetooth do podłączenia.</string>
<string name="settings_modular_stats_display_title">Widok Statystyk Modularnych</string>
<string name="settings_modular_stats_display_summary_on">Ekran statystyk jest podzielony w zależności od trybu.</string>
<string name="settings_modular_stats_display_summary_off">Wyświetl wszystkie statystyki na jednym ekranie.</string>
<string name="settings_backup">Kopia zapasowa (BETA)</string>
<string name="settings_sd_backup">Kopia zapasowa na kartę SD</string>
<string name="settings_backup_to_sd">Utwórz kopię</string>
<string name="settings_backup_to_sd_summary">Tworzy kopię zapasową wszystkich tras i ustawień i zapisuje je na karcie SD</string>
<string name="settings_restore_from_sd">Odtwórz z kopii</string>
<string name="settings_restore_from_sd_summary">Odtwarza wszystkie trasy i ustawienia z kopii zapasowej z karty SD</string>
<string name="settings_no_backup_while_recording">Nie można tworzyć/odtwarzać kopii zapasowej podczas rejestrowania trasy.</string>
<!-- Backups -->
<string name="restore_overwrites_warning">Odtworzenie z kopii zapasowej z karty SD zastąpi wszystkie bieżące trasy i ustawienia. Jesteś tego pewien?</string>
<string name="select_backup_to_restore">Wybierz kopię do odtworzenia</string>
<string name="no_backups">Nie znaleziono kopii zapasowych.</string>
<string name="backup_write_progress_message">Zapisywanie wszystkich tras i ustawień na kartę SD. To może trochę potrwać...</string>
<string name="backup_import_progress_message">Odczytywanie wszystkich tras i ustawień z karty SD. To może trochę potrwać...</string>
<!-- MyTracksSky -->
<string name="mytracks_sky">Satelity GPS</string>
@@ -351,8 +383,10 @@ limitations under the License.
<!-- SD card -->
<string name="io_no_external_storage_found">Nie znaleziono zewnętrznej pamięci. Upewniej się, że karta SD jest w telefonie i nie jest podłączona do komputera.</string>
<string name="io_write_failed">Nie można zapisać pliku.</string>
<string name="io_read_failed">Nie można odczytać pliku.</string>
<string name="io_create_dir_failed">Nie można utworzyć katalogu na karcie SD.</string>
<string name="io_write_finished">Zakończono zapisywanie trasy na karcie SD.</string>
<string name="io_write_finished">Zakończono zapisywanie na karcie SD.</string>
<string name="io_read_finished">Zakończono odczytywanie z karty SD.</string>
<!-- VersionChecker -->
<string name="version_check_title">Sprawdzanie aktualizacji</string>
-1
View File
@@ -157,7 +157,6 @@ limitations under the License.
<string name="settings_sharing">Compartilhamento</string>
<string name="settings_announcement_frequency">Tempo entre anúncios</string>
<string name="settings_announcement_frequency_summary">Tempo mínimo entre dois anúncios de voz</string>
<string name="settings_announcement_not_available_summary">Anúncios de voz não estão disponíveis em sua versão do Android</string>
<string name="settings_min_recording_distance">Dist. mín. entre pontos</string>
<string name="settings_min_recording_distance_summary">Menor distância entre duas gravações da sua posição</string>
<string name="settings_max_recording_distance">Dist. máx. entre pontos</string>
+44 -44
View File
@@ -34,8 +34,8 @@ limitations under the License.
<!-- Error messages -->
<string name="error_no_location_manager">Ingen platshanterare hittades på denna enhet!</string>
<string name="error_no_gps_location_provider">Ingen GPS-positioneringstjänst hittades på denna enhet.</string>
<string name="error_sending_to_mymap">Det uppstod ett fel i sändningen av ditt spår till Google My Map. Försök igen senare.</string>
<string name="error_sending_to_docs">Det uppstod ett fel i sändningen av ditt spår till Google Docs. Försök igen senare.</string>
<string name="error_sending_to_mymap">Det uppstod ett fel i sändningen av ditt spår till Google Mina Kartor. Försök igen senare.</string>
<string name="error_sending_to_docs">Det uppstod ett fel i sändningen av ditt spår till Google Dokument. Försök igen senare.</string>
<string name="error_unable_to_start_recording">Kan inte starta inspelningen. Försök igen.</string>
<string name="error_unable_to_stop_recording">Kan inte stoppa inspelningen. Försök igen.</string>
<string name="error_unable_to_read_file">Kan inte läsa från fil.</string>
@@ -43,19 +43,19 @@ limitations under the License.
<string name="error_generic">Ett fel uppstod. Försök igen.</string>
<string name="error_track_does_not_exist">Spåret finns inte.</string>
<string name="error_out_of_memory">Den här filen är för stor (måste vara mindre än 500 KB). Avbryter.</string>
<string name="error_unable_to_insert_marker">Kan inte skapa markör. Ingen possition tillgänglig.</string>
<string name="error_unable_to_insert_marker">Kan inte skapa markör. Ingen position tillgänglig.</string>
<!-- Status messages -->
<string name="wait_for_fix">Väntar på bättre GPS-posstion. Se till att himlen är synlig.</string>
<string name="wait_for_fix">Väntar på bättre GPS-position. Se till att himlen är synlig.</string>
<string name="status_now_recording">Spelar nu in ett nytt spår.</string>
<string name="status_stopped_recording">Stoppade inspelning av ditt spår.</string>
<string name="status_new_mymap_has_been_created">En ny Google My Map har skapats på maps.google.com.</string>
<string name="status_new_mymap_has_been_created">En ny Google Mina Kartor har skapats på maps.google.com.</string>
<string name="status_tracks_have_been_uploaded">Ditt spår har skickats till maps.google.com.</string>
<string name="status_tracks_have_been_uploaded_to_new_doc">Ditt spår har skockats till ett nytt kalkylblad på docs.google.com.</string>
<string name="status_tracks_have_been_uploaded_to_new_doc">Ditt spår har skickats till ett nytt kalkylblad på docs.google.com.</string>
<string name="status_tracks_have_been_uploaded_to_docs">Ditt spår har skickats till docs.google.com.</string>
<string name="status_mymap_info">Det kan ibland ta några minuter innan nya spår visas.</string>
<string name="status_enable_gps">Klicka här för att gå till Säkerhet och Platsinställningar och kryssa i \'aktivera gps-satteliter\'. Tryck på tillbakaknappen för att återgå.</string>
<string name="status_approximate_location">OBS: En ungerfärlig position som avgörs från mobilmaster och WIFI-nät visas.</string>
<string name="status_approximate_location">OBS: En ungefärlig position som avgörs från mobilmaster och WIFI-nät visas.</string>
<string name="status_no_location">En position kan för närvarande inte fastställas. Aktivera mer platskällor (se Säkerhet och Platsinställningar) och se till att himlen är synlig. Försök igen senare.</string>
<string name="status_waypoint_inserted">En positionsmarkör har lagts in.</string>
<string name="status_statistics_inserted">En statistikmarkör har lagts in.</string>
@@ -65,13 +65,13 @@ limitations under the License.
<string name="progress_message">Skickar ditt spår till Google.</string>
<string name="progress_message_reading_track">Läser spår...</string>
<string name="progress_message_preparing_track">Förbereder spår...</string>
<string name="progress_message_authenticating_mymaps">Autentiserar till Google Maps...</string>
<string name="progress_message_authenticating_docs">Autentiserar till Google Docs...</string>
<string name="progress_message_sending_docs">Skickar spår till Google Docs...</string>
<string name="progress_message_sending_mymaps">Skickar spår till Google Maps...</string>
<string name="progress_message_creating_map">Skapar en ny My Map...</string>
<string name="progress_message_authenticating_mymaps">Autentiserar till Google Kartor...</string>
<string name="progress_message_authenticating_docs">Autentiserar till Google Dokument...</string>
<string name="progress_message_sending_docs">Skickar spår till Google Dokument...</string>
<string name="progress_message_sending_mymaps">Skickar spår till Google Kartor...</string>
<string name="progress_message_creating_map">Skapar en ny Min Karta...</string>
<string name="import_progress_message">Importerar ett GPS-spår. Detta kan ta en stund.</string>
<string name="write_progress_message">Skriver til till SD-kortet. Detta kan ta en stund.</string>
<string name="write_progress_message">Skriver till SD-kortet. Detta kan ta en stund.</string>
<string name="progress_message_updating_track">Uppdaterar spårstatistik...</string>
<!-- Used by DeleteAllDataHandler -->
@@ -110,9 +110,9 @@ limitations under the License.
<!-- TrackList -->
<string name="tracklist_show_track">Visa</string>
<string name="tracklist_send_to_mymaps">Skicka till Google My Maps</string>
<string name="tracklist_send_to_new_mymap">Skicka till New Google My Map</string>
<string name="tracklist_send_to_docs">Skicka till Google Docs</string>
<string name="tracklist_send_to_mymaps">Skicka till Google Mina Kartor</string>
<string name="tracklist_send_to_new_mymap">Skicka till ny Google Mina Kartor</string>
<string name="tracklist_send_to_docs">Skicka till Google Dokument</string>
<string name="tracklist_send_to_google">Skicka till Google...</string>
<string name="tracklist_delete_track">Ta bort från telefonen...</string>
<string name="tracklist_edit_track">Ändra detaljer</string>
@@ -120,16 +120,16 @@ limitations under the License.
<string name="tracklist_save_as_gpx">Spara en GPX-fil</string>
<string name="tracklist_save_as_kml">Spara en KML-fil</string>
<string name="tracklist_save_as_csv">Spara en CSV-fil</string>
<string name="tracklist_btn_send_all_to_mymaps">Skicka till Google My Maps</string>
<string name="tracklist_btn_send_all_to_mymaps">Skicka till Google Mina Kartor</string>
<string name="tracklist_btn_delete_all">Ta bort alla spår</string>
<!-- Export All -->
<string name="tracklist_btn_export_all">Exportera alla spår</string>
<string name="export_done">Exporteringen slutfördes</string>
<string name="tracklist_pick_one">Välj ett spår. TryckPress&amp;håll för fler alternativ.</string>
<string name="tracklist_pick_one">Välj ett spår. Tryck &amp; håll för fler alternativ.</string>
<string name="tracklist_all_tracks">Alla spår:</string>
<string name="tracklist_this_track">Det här spåret</string>
<string name="tracklist_share_track">Dela med vänner...</string>
<string name="tracklist_share_link">En länk till Google My Map</string>
<string name="tracklist_share_link">En länk till Google Mina Kartor</string>
<string name="tracklist_share_gpx_file">En GPX-fil</string>
<string name="tracklist_share_kml_file">En KML-fil</string>
<string name="tracklist_share_csv_file">En CSV-fil</string>
@@ -143,13 +143,13 @@ limitations under the License.
<string name="waypointslist_show_waypoint">Visa på karta</string>
<string name="waypointslist_btn_insert_waypoint">Infoga positionspunkt</string>
<string name="waypointslist_btn_insert_statistics">Infoga statistik</string>
<string name="waypointslist_pick_one">Välj en markör. TryckPress&amp;håll för fler alternativ.</string>
<string name="waypointslist_pick_one">Välj en markör. Tryck &amp; håll för fler alternativ.</string>
<!-- MyTracks Details -->
<string name="trackdetails_title">Spårdetaljer</string>
<string name="trackdetails_name">Spårnamn</string>
<string name="trackdetails_description">Spårbeskrivning</string>
<string name="trackdetails_category">Aktivitetstyp (t ex vandring, cyckling, låpning, klättring...)</string>
<string name="trackdetails_category">Aktivitetstyp (t ex vandring, cyckling, löpning, klättring...)</string>
<!-- MyTracks Waypoint Details -->
<string name="waypointdetails_title">Markördetaljer</string>
@@ -165,7 +165,7 @@ limitations under the License.
<string name="settings_announcement_frequency">Meddelandefrekvens</string>
<string name="settings_announcement_frequency_summary">Minimumtid mellan två röstmeddelanden</string>
<string name="settings_announcement_not_available_summary">Text-till-talmeddelanden är inte tillgängligt i din version av Android</string>
<string name="settings_min_recording_distance">Min distans mellan två punkter</string>
<string name="settings_min_recording_distance_summary">Minsta distans mellan två inspelningar av din position</string>
@@ -185,7 +185,7 @@ limitations under the License.
<string name="settings_map_public">Gör kartor offentliga</string>
<string name="settings_map_public_summary_on">Uppladdade kartor kommer att vara synliga för alla på maps.google.com</string>
<string name="settings_map_public_summary_off">Uppladdad kartor kommer inte att visas offentligt på maps.google.com</string>
<string name="settings_map_public_summary_off">Uppladdade kartor kommer inte att visas offentligt på maps.google.com</string>
<string name="settings_report_speed_or_pace">Visa hastighet</string>
<string name="settings_report_speed_or_pace_summary_on">Hastighet kommer att visas</string>
@@ -199,18 +199,18 @@ limitations under the License.
<string name="settings_signal_sampling_frequency_summary">Tid mellan två inspelningar av telefonens signalstyrka</string>
<string name="settings_split_frequency">Delningsfrekvens</string>
<string name="settings_split_frequency_summary">Maximala tiden eller distansen varefter en ny delnginsmarkör kommer infogas</string>
<string name="settings_split_frequency_summary">Maximala tiden eller distansen varefter en ny delningsmarkör kommer infogas</string>
<string name="pace_label">Tempo</string>
<!-- MyTracksSky -->
<string name="mytracks_sky">GPS-satteliter</string>
<string name="mytracks_sky">GPS-satelliter</string>
<!-- My Maps -->
<string name="select_map_message">Välje en karta från ditt konto:</string>
<string name="select_map_message">Välj en karta från ditt konto:</string>
<string name="create_new_map">Skapa en ny karta...</string>
<string name="map_list_title">Skicka till Google My Maps</string>
<string name="map_list_title">Skicka till Google Mina Kartor</string>
<string name="maps_list_loading">Laddar kartor...</string>
<string name="maps_list_failed">Din kartor kunde inte laddas just nu!</string>
<string name="maps_list_failed">Dina kartor kunde inte laddas just nu!</string>
<string name="open_map">Välj den här kartan</string>
<string name="edit_map_details">Ändra kartdetaljer</string>
<string name="share_map">Dela kartan med vänner</string>
@@ -219,7 +219,7 @@ limitations under the License.
<string name="map_title">Karttitel:</string>
<string name="map_description">Kartbeskrivning:</string>
<string name="map_searchable_edit">Gör så att kartan och dess innehåll visas i sökresultat</string>
<string name="map_not_searchable_edit">Låt inte kartan och dess innehåll visas i sökresultat, men de kan ändå ses av andra genom en direktlänken</string>
<string name="map_not_searchable_edit">Låt inte kartan och dess innehåll visas i sökresultat, men de kan ändå ses av andra genom en direktlänk</string>
<string name="map_searchable">Kartan och dess innehåll kommer att visas i sökresultat</string>
<string name="map_not_searchable">Varken kartan eller dess innehåll visas i sökresultat, men de kan ändå ses av andra genom en direktlänk</string>
<string name="close">Stäng</string>
@@ -240,7 +240,7 @@ limitations under the License.
<!-- Visibility -->
<string name="public_map">Publik</string>
<string name="unlisted_map">Olistat</string>
<string name="mapslist_no_maps_available">Du har inge kartor än.</string>
<string name="mapslist_no_maps_available">Du har inga kartor än.</string>
<!-- Statistics View -->
<!-- Average Moving -->
@@ -253,14 +253,14 @@ limitations under the License.
<string name="elevation_chart_name">Höjddiagram</string>
<string name="elevation_gain_label">Höjdvinst</string>
<string name="elevation_label">Höjt</string>
<string name="elevation_label">Höjd</string>
<string name="latitude_label">Latitud</string>
<string name="longitude_label">Longitud</string>
<!-- Min -->
<string name="min_elevation_label">Min höjd</string>
<string name="min_grade_label">Min lutning</string>
<string name="min_pace_label">Min tempo</string>
<string name="min_elevation_label">Minimihöjd</string>
<string name="min_grade_label">Minimilutning</string>
<string name="min_pace_label">Minimitempo</string>
<!-- Max -->
<string name="max_elevation_label">Maxhöjd</string>
@@ -272,12 +272,12 @@ limitations under the License.
<!-- Chart Settings -->
<string name="chart_settings">Diagraminställningar</string>
<!-- X Axis -->
<string name="x_axis">X Axel</string>
<string name="x_axis">X-axel</string>
<string name="by_distance">Efter distans</string>
<string name="by_time">Efter tid</string>
<!-- Y Axis -->
<string name="y_axis">Y Axel</string>
<string name="y_axis">Y-axel</string>
<!-- Units -->
<!-- Time -->
@@ -300,16 +300,16 @@ limitations under the License.
<!-- Speed and Pace -->
<string name="kilometer_per_hour">km/h</string>
<string name="kilometer_per_hour_long">kilometer per timme</string>
<string name="kilometer_per_hour_long">kilometer i timmen</string>
<string name="min_per_kilometer">min/km</string>
<string name="per_kilometer">per kilometer</string>
<string name="mile_per_hour">mi/h</string>
<string name="mile_per_hour_long">miles per timme</string>
<string name="mile_per_hour_long">miles i timmen</string>
<string name="min_per_mile">min/mi</string>
<string name="per_mile">per mile</string>
<!-- Stats Labels -->
<string name="moving_time_label">Rörelsetid</string>
<string name="moving_time_label">Rörlig tid</string>
<string name="total_time_label">Total tid</string>
<string name="time_label">Tid</string>
<string name="sd_unavail">SD otillgängligt</string>
@@ -336,8 +336,8 @@ limitations under the License.
<!-- Send to Google -->
<string name="sendtogoogle_title">Skicka till...</string>
<string name="sendtogoogle_google_mymaps">Google My Maps</string>
<string name="sendtogoogle_google_docs">Google Docs</string>
<string name="sendtogoogle_google_mymaps">Google Mina Kartor</string>
<string name="sendtogoogle_google_docs">Google Dokument</string>
<string name="sendtogoogle_send_now">Skicka nu</string>
<string name="sendtogoogle_create_new_map">Skapa en ny karta</string>
<string name="sendtogoogle_pick_existing_map">Välj en befintlig karta</string>
@@ -388,7 +388,7 @@ limitations under the License.
<string name="error_rate"> Felfrekvens: %1$d</string>
<string name="signal_to_noise_ratio"> Signal / brus-förhållande: %1$d</string>
<string name="ecio"> Ec/lo: %1$.1f dB</string>
<string name="gsm_strength">GSM styrka: %1$d</string>
<string name="cdma_strength">CDMA styrka: %1$d dBm</string>
<string name="evdo_strength">EVDO styrka: %1$d dBm</string>
<string name="gsm_strength">GSM-styrka: %1$d</string>
<string name="cdma_strength">CDMA-styrka: %1$d dBm</string>
<string name="evdo_strength">EVDO-styrka: %1$d dBm</string>
</resources>
+8
View File
@@ -147,6 +147,14 @@ limitations under the License.
<item>1 分钟</item>
</string-array>
<!-- Auto-resume track timeout -->
<string-array name="auto_resume_track_timeout_options">
<item>从不</item>
<item>5 分钟</item>
<item>10 分钟</item>
<item>15 分钟</item>
<item>总是</item>
</string-array>
<!-- Other non-preference arrays -->
<array name="activity_types">
<item>未知</item>
+106 -26
View File
@@ -15,8 +15,8 @@ See the License for the specific language governing permissions and
limitations under the License.
-->
<resources>
<!-- DO NOT TRANSLATE -->
<string name="app_name">My Tracks</string>
<!-- Used by the Menu -->
<string name="start_recording">记录轨迹</string>
@@ -32,6 +32,7 @@ limitations under the License.
<string name="choose_track_message">请选择要查看的轨迹</string>
<string name="current_track">当前轨迹</string>
<string name="current_segment">上一段轨迹</string>
<string name="aggregated_stats">累积统计</string>
<!-- Error messages -->
<string name="error_no_location_manager">此设备上没有位置管理器!</string>
@@ -122,6 +123,7 @@ limitations under the License.
<string name="tracklist_save_as_gpx">保存为 GPX 文件</string>
<string name="tracklist_save_as_kml">保存为 KML 文件</string>
<string name="tracklist_save_as_csv">保存为 CSV 文件</string>
<string name="tracklist_save_as_tcx">保存为 TCX 文件</string>
<string name="tracklist_btn_send_all_to_mymaps">发送到 Google My Maps</string>
<string name="tracklist_btn_delete_all">删除所有轨迹</string>
<!-- Export All -->
@@ -135,6 +137,7 @@ limitations under the License.
<string name="tracklist_share_gpx_file">GPX 文件</string>
<string name="tracklist_share_kml_file">KML 文件</string>
<string name="tracklist_share_csv_file">CSV 文件</string>
<string name="tracklist_share_tcx_file">TCX 文件</string>
<string name="tracklist_clear_map">清除地图</string>
<!-- Waypoints list -->
@@ -165,9 +168,10 @@ limitations under the License.
<string name="settings_recording">记录</string>
<string name="settings_sharing">分享</string>
<string name="settings_not_available_summary">你使用的 Android 版本无法支持</string>
<string name="settings_announcement_frequency">提示频度</string>
<string name="settings_announcement_frequency_summary">语音提示的最小时间间隔</string>
<string name="settings_announcement_not_available_summary">你所使用的 Android 版本不支持文本到语音转换</string>
<string name="settings_min_recording_distance">最小记录间距</string>
<string name="settings_min_recording_distance_summary">位置记录的最小间隔距离</string>
@@ -202,7 +206,38 @@ limitations under the License.
<string name="settings_split_frequency">切割频度</string>
<string name="settings_split_frequency_summary">插入新切割标记所需要达到的最大时间或距离</string>
<string name="pace_label">步数</string>
<string name="settings_auto_resume_track_timeout">自动恢复追踪等待时间</string>
<string name="settings_auto_resume_track_timeout_summary">手机重启后自动继续上次轨迹记录的最大间隔时间</string>
<string name="pace_label">节奏</string>
<string name="eclair_required">只在 Android 2.0 及以上版本支持</string>
<string name="settings_sensor_type">感应器类型</string>
<string name="settings_sensor_type_summary">要连接到的感应器类型</string>
<string name="settings_sensor_id">感应器ID</string>
<string name="settings_sensor_id_summary">发送到感应器的ID.</string>
<string name="sensor_id_malformed">ID必须是一个整数值.</string>
<string name="bluetooth_sensor_title">蓝牙感应器</string>
<string name="bluetooth_sensor_summary">要连接到的蓝牙感应器</string>
<string name="settings_backup">备份 (测试版)</string>
<string name="settings_sd_backup">SD 存储卡备份</string>
<string name="settings_backup_to_sd">立即备份</string>
<string name="settings_backup_to_sd_summary">将你所有轨迹和设置的完整备份写入到 SD 存储卡</string>
<string name="settings_restore_from_sd">立即恢复</string>
<string name="settings_restore_from_sd_summary">从 SD 存储卡恢复所有轨迹和设置</string>
<string name="settings_no_backup_while_recording">在记录轨迹时无法处理备份数据.</string>
<!-- Backups -->
<string name="restore_overwrites_warning">从 SD 存储卡恢复备份数据将覆盖掉你当前的所有轨迹和设置. 确定要继续吗?</string>
<string name="select_backup_to_restore">请选择要恢复的备份文件</string>
<string name="no_backups">没有找到备份文件.</string>
<string name="backup_write_progress_message">将所有轨迹和设置写入 SD 存储卡中. 这需要一些时间...</string>
<string name="backup_import_progress_message">从 SD 存储卡读取轨迹和设置中. 这需要一些时间...</string>
<!-- MyTracksSky -->
<string name="mytracks_sky">GPS 卫星</string>
@@ -241,7 +276,7 @@ limitations under the License.
<!-- Visibility -->
<string name="public_map">公开</string>
<string name="unlisted_map">Unlisted</string>
<string name="unlisted_map">未公布</string>
<string name="mapslist_no_maps_available">你还没有保存任何地图.</string>
<!-- Statistics View -->
@@ -292,24 +327,32 @@ limitations under the License.
<string name="second">秒</string>
<string name="seconds">秒</string>
<!-- Distance -->
<string name="meter">m</string>
<string name="kilometer">km</string>
<string name="kilometers_long">千米</string>
<string name="feet">ft</string>
<string name="mile">mi</string>
<string name="miles_long">英里</string>
<!-- Speed and Pace -->
<string name="kilometer_per_hour">km/h</string>
<string name="kilometer_per_hour_long">千米每小时</string>
<string name="min_per_kilometer">min/km</string>
<string name="per_kilometer">每千米</string>
<string name="mile_per_hour">mi/h</string>
<string name="mile_per_hour_long">英里每小时</string>
<string name="min_per_mile">min/mi</string>
<string name="per_mile">每英里</string>
<!-- Stats Labels -->
<string name="moving_time_label">移动时间</string>
<string name="total_time_label">总时间</string>
@@ -320,7 +363,11 @@ limitations under the License.
<string name="statistics">统计</string>
<string name="total_distance_label">总距离</string>
<string name="distance_label">距离</string>
<string name="unknown">-</string>
<string name="power">电源</string>
<string name="cadence">节奏</string>
<string name="heart_rate">心率</string>
<string name="max_heart_rate">最大心率</string>
<string name="waiting">请稍候...</string>
<!-- End User License Agreement -->
@@ -336,20 +383,27 @@ limitations under the License.
<string name="about_email">技术支持</string>
<string name="about_support">技术支持:</string>
<!-- DO NOT TRANSLATE -->
<string name="version">1.0.14</string>
<string name="about_googlegroups">mytracks@googlegroups.com</string>
<string name="about_weburl">mytracks.appspot.com</string>
<string name="about_copyright_message">Copyright 2009 Google Inc.</string>
<string name="about_dylan">Dylan Casey</string>
<string name="about_leif">Leif Hendrik Wilden</string>
<string name="about_rdamazio">Rodrigo Damazio Bovendorp</string>
<string name="about_sandor">Sandor Dornbush</string>
<!-- Send to Google -->
<string name="sendtogoogle_title">发送到...</string>
<string name="sendtogoogle_google_mymaps">Google My Maps</string>
<string name="sendtogoogle_google_docs">Google Docs</string>
<string name="sendtogoogle_send_now">立即发送</string>
<string name="sendtogoogle_create_new_map">创建新地图</string>
<string name="sendtogoogle_pick_existing_map">选择已有地图</string>
@@ -364,8 +418,9 @@ limitations under the License.
<!-- SD card -->
<string name="io_no_external_storage_found">没有发现存贮设备. 请确认插入了 SD 卡并且你的手机没有将挂载 SD 卡到电脑.</string>
<string name="io_write_failed">无法写入文件.</string>
<string name="io_create_dir_failed">无法在 SD 卡上创建目录.</string>
<string name="io_create_dir_failed">无法在 SD 存储卡上创建目录.</string>
<string name="io_write_finished">轨迹已导出到 SD 卡.</string>
<string name="io_read_finished">从 SD 存储卡读取完毕.</string>
<!-- VersionChecker -->
<string name="version_check_title">检查更新</string>
@@ -403,4 +458,29 @@ limitations under the License.
<string name="gsm_strength">GSM 强度 %1$d</string>
<string name="cdma_strength">CDMA 强度 %1$d dBm</string>
<string name="evdo_strength">EVDO 强度 %1$d dBm</string>
<!-- Fake preference keys - never saved, but show options in the preferences screen -->
<string name="method_bluetooth">蓝牙</string>
<string name="bluetooth_options">蓝牙设置</string>
<string name="bluetooth_pairing">设备关联</string>
<string name="bluetooth_pairing_summary">打开 Android 的蓝牙设置关联设备</string>
<string name="sensor_options">感应器选项</string>
<!-- Bluetooth Sensor -->
<string name="bluetooth_connected">连接到 %s</string>
<string name="bluetooth_connecting">蓝牙感应器连接中 ...</string>
<string name="bluetooth_not_connected">未连接蓝牙感应器</string>
<!-- Sensor State -->
<string name="sensor_data_time">感应器数据时间</string>
<string name="power_meter_enabled">电源计量器已启用</string>
<string name="heart_rate_monitor_enabled">心率监视器已启用</string>
</resources>
+18
View File
@@ -236,6 +236,24 @@ limitations under the License.
<item>1</item>
</string-array>
<!-- Auto-resume track timeout -->
<string-array name="auto_resume_track_timeout_options">
<item>Never</item>
<item>5 min</item>
<item>10 min</item>
<item>15 min</item>
<item>Always</item>
</string-array>
<!-- DO NOT TRANSLATE -->
<string-array name="auto_resume_track_timeout_values">
<item>0</item>
<item>5</item>
<item>10</item>
<item>15</item>
<item>-1</item>
</string-array>
<!-- Other non-preference arrays -->
<array name="activity_types">
+4 -1
View File
@@ -17,6 +17,7 @@ limitations under the License.
<resources>
<!-- Keys for application preferences - DO NOT TRANSLATE -->
<string name="announcement_frequency_key">announcementFrequency</string>
<string name="auto_resume_track_timeout_key">autoResumeTrackTimeout</string>
<string name="bluetooth_device_key">bluetoothDevice</string>
<string name="bluetooth_pairing_key">bluetoothPairing</string>
<string name="bluetooth_sensor_key">bluetoothSensor</string>
@@ -37,12 +38,14 @@ limitations under the License.
<string name="share_url_only_key">shareUrlOnly</string>
<string name="signal_sampling_frequency_key">signalSamplingFrequency</string>
<string name="split_frequency_key">splitFrequency</string>
<string name="timestamp_track_name_key">timestampTrackName</string>
<!-- Preference keys which keep transient state (not user preferences) - DO NOT TRANSLATE -->
<string name="auto_resume_track_current_retry_key">autoResumeTrackCurrentRetry</string>
<string name="recording_track_key">recordingTrack</string>
<string name="selected_track_key">selectedTrack</string>
<!-- Keys for non-persisted preferences - DO NOT TRANSLATE -->
<string name="backup_to_sd_key">backupToSd</string>
<string name="restore_from_sd_key">restoreFromSd</string>
</resources>
</resources>
+29
View File
@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2010 Google Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources>
<array name="left_icons">
<item>@drawable/left_arrow_chart_stateful</item>
<item>@drawable/left_arrow_track_stateful</item>
<item>@drawable/left_arrow_stats_stateful</item>
</array>
<array name="right_icons">
<item>@drawable/right_arrow_stats_stateful</item>
<item>@drawable/right_arrow_chart_stateful</item>
<item>@drawable/right_arrow_track_stateful</item>
</array>
</resources>
+11 -2
View File
@@ -32,6 +32,7 @@ limitations under the License.
<string name="choose_track_message">Please select the track to view</string>
<string name="current_track">Current track</string>
<string name="current_segment">Last segment</string>
<string name="aggregated_stats">Aggregated Statistics</string>
<!-- Error messages -->
<string name="error_no_location_manager">No location manager was found on this device!</string>
@@ -200,11 +201,19 @@ limitations under the License.
<string name="settings_share_url_only_summary_on">Only the URL of the map will be sent when sharing</string>
<string name="settings_share_url_only_summary_off">The contents of the track will also be included when sharing</string>
<string name="settings_signal_sampling_frequency">Signal Sampling frequency</string>
<string name="settings_signal_sampling_frequency">Signal sampling frequency</string>
<string name="settings_signal_sampling_frequency_summary">Time between two recordings of the phone\'s signal strength</string>
<string name="settings_split_frequency">Split Frequency</string>
<string name="settings_split_frequency">Split frequency</string>
<string name="settings_split_frequency_summary">Maximum time or distance after which a new split marker will be inserted</string>
<string name="settings_auto_resume_track_timeout">Auto-resume track timeout</string>
<string name="settings_auto_resume_track_timeout_summary">Maximum time in minutes after which a previously recording track will not be resumed on phone reboot</string>
<string name="settings_timestamp_track_name">Timestamp as track name</string>
<string name="settings_timestamp_track_name_summary_on">The default track name will be the date and time at the start of the track</string>
<string name="settings_timestamp_track_name_summary_off">Track names will have numbers which increment for each new track.</string>
<string name="pace_label">Pace</string>
<string name="eclair_required">Only supported in Android 2.0 or above</string>
+17 -1
View File
@@ -91,6 +91,22 @@ limitations under the License.
android:entries="@array/task_frequency_options"
android:entryValues="@array/task_frequency_values"
android:defaultValue="-1" />
<com.google.android.apps.mytracks.IntegerListPreference
android:key="@string/auto_resume_track_timeout_key"
android:title="@string/settings_auto_resume_track_timeout"
android:summary="@string/settings_auto_resume_track_timeout_summary"
android:entries="@array/auto_resume_track_timeout_options"
android:entryValues="@array/auto_resume_track_timeout_values"
android:defaultValue="10" />
<CheckBoxPreference
android:key="@string/timestamp_track_name_key"
android:title="@string/settings_timestamp_track_name"
android:summaryOn="@string/settings_timestamp_track_name_summary_on"
android:summaryOff="@string/settings_timestamp_track_name_summary_off"
android:defaultValue="true" />
</PreferenceScreen>
<!-- Sharing settings -->
@@ -118,7 +134,7 @@ limitations under the License.
android:title="@string/settings_backup_to_sd"
android:summary="@string/settings_backup_to_sd_summary"
android:persistent="false" />
<Preference
<Preference
android:key="@string/restore_from_sd_key"
android:title="@string/settings_restore_from_sd"
android:summary="@string/settings_restore_from_sd_summary"
@@ -25,5 +25,8 @@ public class MyMapsConstants {
"http://maps.google.com/maps/ms";
public static final String MAPSHOP_SERVICE = MapsClient.SERVICE;
/**
* Private constructor to prevent instantiation.
*/
private MyMapsConstants() { }
}
@@ -1,197 +0,0 @@
/*
* 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.mymaps;
import com.google.android.apps.mymaps.MyMapsGDataWrapper.QueryFunction;
import com.google.android.apps.mytracks.MyTracksConstants;
import com.google.android.maps.mytracks.R;
import com.google.wireless.gdata.GDataException;
import com.google.wireless.gdata.client.HttpException;
import com.google.wireless.gdata.data.Entry;
import com.google.wireless.gdata.maps.MapsClient;
import com.google.wireless.gdata.parser.GDataParser;
import android.app.AlertDialog;
import android.app.Dialog;
import android.app.TabActivity;
import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.Intent;
import android.net.Uri;
import android.util.Log;
import java.io.IOException;
public class VersionChecker {
/** The User id for the account storing MyTracks versions. */
private static final String VERSION_USER_ID = "116927647071651051525";
private static final String VERSION_CLIENT = "AndroidMyTracks";
private static final String VERSION_STATUS_LATEST = "latest";
private static final String VERSION_STATUS_OKAY = "okay";
private static final String VERSION_STATUS_OLD = "old";
private static final String VERSION_STATUS_BAD = "bad";
private final TabActivity parentActivity;
private Dialog dialog;
private boolean canceledAvailable;
private boolean canceledRecommended;
public VersionChecker(TabActivity parent) {
parentActivity = parent;
canceledAvailable = false;
canceledRecommended = false;
startMapsQueryThread();
}
private void startMapsQueryThread() {
new Thread() {
@Override
public void run() {
MyMapsGDataWrapper mapsGDataWrapper =
new MyMapsGDataWrapper(parentActivity);
mapsGDataWrapper.runQuery(new QueryFunction() {
@Override
public void query(MapsClient client) throws IOException, Exception {
doMapsQuery(client);
}
});
mapsGDataWrapper.cleanUp();
}
}.start();
}
private void doMapsQuery(MapsClient client)
throws GDataException, IOException, HttpException {
final boolean newVersion; // Returned new version available
final boolean newVersionRecommended; // Returned new version recommended
final boolean newVersionRequired; // Returned new version required
String currentVersion = "";
try {
PackageInfo pi =
parentActivity.getPackageManager().getPackageInfo(
"com.google.android.maps.mytracks",
PackageManager.GET_META_DATA);
currentVersion = pi.versionName;
} catch (NameNotFoundException e) {
Log.w(MyTracksConstants.TAG, "Failed to get version info.", e);
return;
}
GDataParser parser = client.getParserForFeed(
Entry.class,
MapsClient.getVersionFeed(
VERSION_USER_ID,
VERSION_CLIENT,
currentVersion),
"");
Entry en = parser.parseStandaloneEntry();
if (currentVersion.equals(en.getTitle())) {
if (VERSION_STATUS_LATEST.equals(en.getContent())) {
newVersion = false;
newVersionRecommended = false;
newVersionRequired = false;
} else if (VERSION_STATUS_OKAY.equals(en.getContent())) {
newVersion = true;
newVersionRecommended = false;
newVersionRequired = false;
} else if (VERSION_STATUS_OLD.equals(en.getContent())) {
newVersion = true;
newVersionRecommended = true;
newVersionRequired = false;
} else if (VERSION_STATUS_BAD.equals(en.getContent())) {
newVersion = true;
newVersionRecommended = true;
newVersionRequired = true;
} else {
Log.e(MyTracksConstants.TAG,
"Got unknown version status: " + en.getContent());
return;
}
} else {
Log.e(MyTracksConstants.TAG,
"Current version '" + currentVersion + "' doesn't match entry: "
+ en.getTitle());
return;
}
if (!newVersion) {
Log.d(MyTracksConstants.TAG, "No new version available.");
return;
}
final AlertDialog.Builder dialogBuilder =
new AlertDialog.Builder(parentActivity);
dialogBuilder.setTitle(R.string.version_check_title);
dialogBuilder.setPositiveButton(R.string.version_check_upgrade_button,
new OnClickListener() {
public void onClick(DialogInterface di, int number) {
Intent upgradeIntent = new Intent();
upgradeIntent.setAction(Intent.ACTION_VIEW);
upgradeIntent.setData(Uri.parse(String.format("market://search?q=%s",
parentActivity.getResources().getString(
R.string.app_name).toString())));
parentActivity.startActivity(upgradeIntent);
parentActivity.finish();
}
});
if (newVersionRequired) {
dialogBuilder.setCancelable(false);
} else {
final boolean recommended = newVersionRecommended;
dialogBuilder.setNegativeButton(R.string.version_check_continue_button,
new OnClickListener() {
public void onClick(DialogInterface di, int number) {
dialog.dismiss();
canceledAvailable = true;
if (recommended) {
canceledRecommended = true;
}
}
});
}
int messageResource;
if (newVersionRequired) {
messageResource = R.string.upgrade_required;
} else if (newVersionRecommended) {
messageResource = R.string.upgrade_recommended;
if (canceledRecommended) {
return;
}
} else {
messageResource = R.string.upgrade_available;
if (canceledAvailable) {
return;
}
}
String message = parentActivity.getResources().getString(messageResource);
dialogBuilder.setMessage(String.format(message,
parentActivity.getResources().getString(R.string.app_name)));
parentActivity.getTabHost().post(new Runnable() {
public void run() {
dialog = dialogBuilder.create();
dialog.show();
}
});
}
}
@@ -0,0 +1,115 @@
package com.google.android.apps.mytracks;
import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.stats.TripStatistics;
import com.google.android.maps.mytracks.R;
import android.app.Activity;
import android.content.SharedPreferences;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
import android.os.Bundle;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.Window;
import android.widget.ScrollView;
import android.widget.TextView;
import java.util.List;
/**
* Activity for viewing the combined statistics for all the recorded tracks.
*
* Other features to add - menu items to change setings.
*
* @author Fergus Nelson
*/
public class AggregatedStatsActivity extends Activity implements
OnSharedPreferenceChangeListener {
private final StatsUtilities utils;
private MyTracksProviderUtils tracksProvider;
private boolean metricUnits = true;
public AggregatedStatsActivity() {
this.utils = new StatsUtilities(this);
}
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences,
String key) {
Log.d(MyTracksConstants.TAG, "StatsActivity: onSharedPreferences changed "
+ key);
if (key != null) {
if (key.equals(R.string.metric_units_key)) {
metricUnits = sharedPreferences.getBoolean(
getString(R.string.metric_units_key), true);
utils.setMetricUnits(metricUnits);
utils.updateUnits();
loadAggregatedStats();
}
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
this.tracksProvider = MyTracksProviderUtils.Factory.get(this);
// We don't need a window title bar:
requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.stats);
ScrollView sv = ((ScrollView) findViewById(R.id.scrolly));
sv.setScrollBarStyle(ScrollView.SCROLLBARS_OUTSIDE_INSET);
SharedPreferences preferences = getSharedPreferences(
MyTracksSettings.SETTINGS_NAME, 0);
if (preferences != null) {
metricUnits = preferences.getBoolean(getString(R.string.metric_units_key), true);
preferences.registerOnSharedPreferenceChangeListener(this);
}
utils.setMetricUnits(metricUnits);
utils.updateUnits();
utils.setSpeedLabel(R.id.speed_label, R.string.speed, R.string.pace_label);
utils.setSpeedLabels();
DisplayMetrics metrics = new DisplayMetrics();
getWindowManager().getDefaultDisplay().getMetrics(metrics);
if (metrics.heightPixels > 600) {
((TextView) findViewById(R.id.speed_register)).setTextSize(80.0f);
}
loadAggregatedStats();
}
/**
* 1. Reads tracks from the db
* 2. Merges the trip stats from the tracks
* 3. Updates the view
*/
private void loadAggregatedStats() {
List<Track> tracks = retrieveTracks();
TripStatistics rollingStats = null;
if (!tracks.isEmpty()) {
rollingStats = new TripStatistics(tracks.iterator().next()
.getStatistics());
for (int i = 1; i < tracks.size(); i++) {
rollingStats.merge(tracks.get(i).getStatistics());
}
}
updateView(rollingStats);
}
private List<Track> retrieveTracks() {
return tracksProvider.getAllTracks();
}
private void updateView(TripStatistics aggStats) {
if (aggStats != null) {
utils.setAllStats(aggStats);
}
}
}
@@ -21,8 +21,10 @@ import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.content.TrackPointsColumns;
import com.google.android.apps.mytracks.content.Waypoint;
import com.google.android.apps.mytracks.content.WaypointsColumns;
import com.google.android.apps.mytracks.services.StatusAnnouncerFactory;
import com.google.android.apps.mytracks.stats.DoubleBuffer;
import com.google.android.apps.mytracks.stats.TripStatisticsBuilder;
import com.google.android.apps.mytracks.util.ApiFeatures;
import com.google.android.apps.mytracks.util.MyTracksUtils;
import com.google.android.apps.mytracks.util.UnitConversions;
import com.google.android.maps.mytracks.R;
@@ -35,7 +37,6 @@ import android.location.Location;
import android.os.Bundle;
import android.os.Handler;
import android.os.HandlerThread;
import android.speech.tts.TextToSpeech;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
@@ -176,11 +177,15 @@ public class ChartActivity extends Activity implements
final int idColumnIdx =
cursor.getColumnIndexOrThrow(TrackPointsColumns._ID);
ArrayList<double[]> data = new ArrayList<double[]>();
// Need two locations so we can keep track of the last location.
Location location = new Location("");
do {
lastSeenLocationId = cursor.getLong(idColumnIdx);
Location location = providerUtils.createLocation(cursor);
if (location != null && MyTracksUtils.isValidLocation(location)) {
data.add(getDataPoint(location, track));
providerUtils.fillLocation(cursor, location);
if (MyTracksUtils.isValidLocation(location)) {
double[] point = new double[3];
location = getDataPoint(location, track, point);
data.add(point);
}
} while (cursor.moveToPrevious());
cv.addDataPoints(data);
@@ -239,7 +244,9 @@ public class ChartActivity extends Activity implements
providerUtils = MyTracksProviderUtils.Factory.get(this);
// The volume we want to control is the Text-To-Speech volume
setVolumeControlStream(TextToSpeech.Engine.DEFAULT_STREAM);
int volumeStream =
new StatusAnnouncerFactory(ApiFeatures.getInstance()).getVolumeStream();
setVolumeControlStream(volumeStream);
requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.mytracks_elevation);
@@ -291,13 +298,13 @@ public class ChartActivity extends Activity implements
public void onChange(boolean selfChange) {
Log.d(MyTracksConstants.TAG, "ChartActivity: ContentObserver.onChange");
// Check for any new locations and append them to the currently
// recording track:
if (!MyTracks.getInstance().isRecording()) {
// recording track.
if (recordingTrackId < 0) {
// No track is being recorded. We should not be here.
return;
}
if (selectedTrackId != recordingTrackId) {
// no track, or one other than the recording track is selected, don't
// No track, or one other than the recording track is selected, don't
// bother.
return;
}
@@ -448,7 +455,8 @@ public class ChartActivity extends Activity implements
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case MyTracksConstants.MENU_CHART_SETTINGS:
MyTracks.getInstance().showDialogSafely(MyTracks.DIALOG_CHART_SETTINGS);
MyTracks.getInstance().getDialogManager().showDialogSafely(
DialogManager.DIALOG_CHART_SETTINGS);
return true;
}
return super.onOptionsItemSelected(item);
@@ -461,11 +469,14 @@ public class ChartActivity extends Activity implements
* data[1] = the elevation
* data[2] = the speed
*
* @param location a location
* @return the data point
* This must be called in order for each point.
*
* @param location the location to get data for (this method takes ownership of that location)
* @param track the track to get data from
* @param result the resulting point to fill out
* @return the previous location, now available for reuse
*/
public double[] getDataPoint(Location location, Track track) {
double[] result = new double[3];
private Location getDataPoint(Location location, Track track, double[] result) {
switch (mode) {
case BY_DISTANCE:
result[0] = profileLength;
@@ -512,8 +523,16 @@ public class ChartActivity extends Activity implements
// Format as hours per unit
result[2] = (60.0 / result[2]);
}
Location oldLastLocation = lastLocation;
lastLocation = location;
return result;
if (oldLastLocation == null) {
// No previous location, but return a blank one for reuse
return new Location("");
}
return oldLastLocation;
}
/**
@@ -556,6 +575,8 @@ public class ChartActivity extends Activity implements
try {
final ArrayList<double[]> theData = new ArrayList<double[]>();
int points = 0;
// Need two locations so we can keep track of the last location.
Location location = new Location("");
while (lastLocationRead < track.getStopId()) {
cursor = providerUtils.getLocationsCursor(
selectedTrackId, lastLocationRead, bufferSize, false);
@@ -567,11 +588,12 @@ public class ChartActivity extends Activity implements
cursor.getColumnIndexOrThrow(TrackPointsColumns._ID);
while (cursor.moveToNext()) {
points++;
Location location = providerUtils.createLocation(cursor);
providerUtils.fillLocation(cursor, location);
if (MyTracksUtils.isValidLocation(location)) {
lastLocationRead = lastSeenLocationId =
cursor.getLong(idColumnIdx);
double[] point = getDataPoint(location, track);
double[] point = new double[3];
location = getDataPoint(location, track, point);
if (points % chartSamplingFrequency == 0) {
theData.add(point);
}
@@ -25,7 +25,7 @@ import android.graphics.Path;
import java.text.DecimalFormat;
/**
* This class encasulates meta data about one series of values for a chart.
* This class encapsulates meta data about one series of values for a chart.
*
* @author Sandor Dornbush
*/
@@ -0,0 +1,231 @@
/*
* 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.app.AlertDialog;
import android.app.Dialog;
import android.app.ProgressDialog;
import android.content.DialogInterface;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.view.WindowManager.BadTokenException;
/**
* A class to handle all dialog related events for My Tracks.
*
* @author Sandor Dornbush
*/
public class DialogManager {
public static final int DIALOG_CHART_SETTINGS = 1;
public static final int DIALOG_IMPORT_PROGRESS = 2;
public static final int DIALOG_PROGRESS = 3;
public static final int DIALOG_SEND_TO_GOOGLE = 4;
public static final int DIALOG_SEND_TO_GOOGLE_RESULT = 5;
public static final int DIALOG_WRITE_PROGRESS = 6;
private ProgressDialog progressDialog;
private ProgressDialog importProgressDialog;
private ProgressDialog writeProgressDialog;
private SendToGoogleDialog sendToGoogleDialog;
private AlertDialog sendToGoogleResultDialog;
private ChartSettingsDialog chartSettingsDialog;
private MyTracks activity;
public DialogManager(MyTracks activity) {
this.activity = activity;
}
protected Dialog onCreateDialog(int id, Bundle args) {
switch (id) {
case DIALOG_CHART_SETTINGS:
chartSettingsDialog = new ChartSettingsDialog(activity);
return chartSettingsDialog;
case DIALOG_IMPORT_PROGRESS:
importProgressDialog = new ProgressDialog(activity);
importProgressDialog.setIcon(android.R.drawable.ic_dialog_info);
importProgressDialog.setTitle(
activity.getString(R.string.progress_title));
importProgressDialog.setProgressStyle(ProgressDialog.STYLE_SPINNER);
importProgressDialog.setMessage(
activity.getString(R.string.import_progress_message));
return importProgressDialog;
case DIALOG_PROGRESS:
progressDialog = new ProgressDialog(activity);
progressDialog.setIcon(android.R.drawable.ic_dialog_info);
progressDialog.setTitle(activity.getString(R.string.progress_title));
progressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
progressDialog.setMessage("");
progressDialog.setMax(100);
progressDialog.setProgress(10);
return progressDialog;
case DIALOG_SEND_TO_GOOGLE:
sendToGoogleDialog = new SendToGoogleDialog(activity);
return sendToGoogleDialog;
case DIALOG_SEND_TO_GOOGLE_RESULT:
AlertDialog.Builder builder = new AlertDialog.Builder(activity);
builder.setIcon(android.R.drawable.ic_dialog_info);
builder.setTitle("Title");
builder.setMessage("Message");
builder.setPositiveButton(activity.getString(R.string.ok), null);
builder.setNeutralButton(activity.getString(R.string.share_map),
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
activity.shareLinkToMyMap(activity.getSendToMyMapsMapId());
dialog.dismiss();
}
});
sendToGoogleResultDialog = builder.create();
return sendToGoogleResultDialog;
case DIALOG_WRITE_PROGRESS:
writeProgressDialog = new ProgressDialog(activity);
writeProgressDialog.setIcon(android.R.drawable.ic_dialog_info);
writeProgressDialog.setTitle(
activity.getString(R.string.progress_title));
writeProgressDialog.setProgressStyle(ProgressDialog.STYLE_SPINNER);
writeProgressDialog.setMessage(
activity.getString(R.string.write_progress_message));
return writeProgressDialog;
}
return null;
}
protected void onPrepareDialog(int id, Dialog dialog) {
switch (id) {
case DIALOG_SEND_TO_GOOGLE:
activity.resetSendToGoogleStatus();
break;
case DIALOG_SEND_TO_GOOGLE_RESULT:
boolean success = activity.getSendToGoogleSuccess();
sendToGoogleResultDialog.setTitle(
success ? R.string.success : R.string.error);
sendToGoogleResultDialog.setIcon(success
? android.R.drawable.ic_dialog_info
: android.R.drawable.ic_dialog_alert);
sendToGoogleResultDialog.setMessage(activity.getMapsResultMessage());
boolean canShare = activity.getSendToMyMapsMapId() != null;
View share =
sendToGoogleResultDialog.findViewById(android.R.id.button3);
if (share != null) {
share.setVisibility(canShare ? View.VISIBLE : View.GONE);
}
break;
case DIALOG_CHART_SETTINGS:
Log.d(MyTracksConstants.TAG, "MyTracks.onPrepare chart dialog");
chartSettingsDialog.setup(activity.getChartActivity());
break;
}
}
public void setProgressMessage(final String message) {
activity.runOnUiThread(new Runnable() {
public void run() {
synchronized (this) {
if (progressDialog != null) {
progressDialog.setMessage(message);
}
}
}
});
}
public void setProgressValue(final int percent) {
activity.runOnUiThread(new Runnable() {
public void run() {
synchronized (this) {
if (progressDialog != null) {
progressDialog.setProgress(percent);
}
}
}
});
}
/**
* @return the sendToGoogleDialog
*/
public SendToGoogleDialog getSendToGoogleDialog() {
return sendToGoogleDialog;
}
/**
* Shows a dialog with the given message.
* Does it on the UI thread.
*
* @param success if true, displays an info icon/title, otherwise an error
* icon/title
* @param message resource string id
*/
public void showMessageDialog(final int message, final boolean success) {
activity.runOnUiThread(new Runnable() {
public void run() {
AlertDialog dialog = null;
AlertDialog.Builder builder = new AlertDialog.Builder(activity);
builder.setMessage(activity.getString(message));
builder.setNegativeButton(activity.getString(R.string.ok), null);
builder.setIcon(success ? android.R.drawable.ic_dialog_info :
android.R.drawable.ic_dialog_alert);
builder.setTitle(success ? R.string.success : R.string.error);
dialog = builder.create();
dialog.show();
}
});
}
/**
* Just like showDialog, but will catch a BadTokenException that sometimes
* (very rarely) gets thrown. This might happen if the user hits the "back"
* button immediately after sending tracks to google.
*
* @param id the dialog id
*/
public void showDialogSafely(final int id) {
activity.runOnUiThread(new Runnable() {
public void run() {
try {
activity.showDialog(id);
} catch (BadTokenException e) {
Log.w(MyTracksConstants.TAG,
"Could not display dialog with id " + id, e);
} catch (IllegalStateException e) {
Log.w(MyTracksConstants.TAG,
"Could not display dialog with id " + id, e);
}
}
});
}
/**
* Dismisses the progress dialog if it is showing. Executed on the UI thread.
*/
public void dismissDialogSafely(final int id) {
activity.runOnUiThread(new Runnable() {
public void run() {
try {
activity.dismissDialog(id);
} catch (IllegalArgumentException e) {
// This will be thrown if this dialog was not shown before.
}
}
});
}
}
@@ -22,6 +22,7 @@ import com.google.android.apps.mytracks.io.TrackWriterFactory;
import com.google.android.apps.mytracks.io.TrackWriterFactory.TrackFileFormat;
import com.google.android.maps.mytracks.R;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.ProgressDialog;
import android.content.Context;
@@ -46,7 +47,7 @@ public class ExportAllTracks {
public static final int KML_OPTION_INDEX = 1;
public static final int CSV_OPTION_INDEX = 2;
private final Context context;
private final Activity activity;
private WakeLock wakeLock;
private ProgressDialog progress;
@@ -70,11 +71,11 @@ public class ExportAllTracks {
}
};
public ExportAllTracks(Context c) {
this.context = c;
public ExportAllTracks(Activity activity) {
this.activity = activity;
Log.i(MyTracksConstants.TAG, "ExportAllTracks: Starting");
AlertDialog.Builder builder = new AlertDialog.Builder(c);
AlertDialog.Builder builder = new AlertDialog.Builder(activity);
builder.setSingleChoiceItems(R.array.export_formats, 0, itemClick);
builder.setPositiveButton(R.string.ok, positiveClick);
builder.setNegativeButton(R.string.cancel, null);
@@ -107,11 +108,11 @@ public class ExportAllTracks {
*/
private void aquireLocksAndExport() {
SharedPreferences prefs =
context.getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0);
activity.getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0);
long recordingTrackId = -1;
if (prefs != null) {
recordingTrackId =
prefs.getLong(context.getString(R.string.recording_track_key), -1);
prefs.getLong(activity.getString(R.string.recording_track_key), -1);
}
if (recordingTrackId != -1) {
acquireWakeLock();
@@ -127,12 +128,12 @@ public class ExportAllTracks {
Log.i(MyTracksConstants.TAG, "ExportAllTracks: Releasing wake lock.");
}
Log.i(MyTracksConstants.TAG, "ExportAllTracks: Done");
Toast.makeText(context, R.string.export_done, Toast.LENGTH_SHORT).show();
Toast.makeText(activity, R.string.export_done, Toast.LENGTH_SHORT).show();
}
private void makeProgressDialog(final int trackCount) {
String exportMsg = context.getString(R.string.tracklist_btn_export_all);
progress = new ProgressDialog(context);
String exportMsg = activity.getString(R.string.tracklist_btn_export_all);
progress = new ProgressDialog(activity);
progress.setIcon(android.R.drawable.ic_dialog_info);
progress.setTitle(exportMsg);
progress.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
@@ -150,7 +151,7 @@ public class ExportAllTracks {
Cursor cursor = null;
try {
MyTracksProviderUtils providerUtils =
MyTracksProviderUtils.Factory.get(context);
MyTracksProviderUtils.Factory.get(activity);
cursor = providerUtils.getTracksCursor("");
if (cursor == null) {
return;
@@ -160,7 +161,7 @@ public class ExportAllTracks {
Log.i(MyTracksConstants.TAG,
"ExportAllTracks: Exporting: " + cursor.getCount() + " tracks.");
int idxTrackId = cursor.getColumnIndexOrThrow(TracksColumns._ID);
MyTracks.getInstance().runOnUiThread(new Runnable() {
activity.runOnUiThread(new Runnable() {
public void run() {
makeProgressDialog(trackCount);
}
@@ -168,7 +169,7 @@ public class ExportAllTracks {
for (int i = 0; cursor.moveToNext(); i++) {
final int status = i;
MyTracks.getInstance().runOnUiThread(new Runnable() {
activity.runOnUiThread(new Runnable() {
public void run() {
synchronized (this) {
if (progress == null) {
@@ -182,13 +183,13 @@ public class ExportAllTracks {
long id = cursor.getLong(idxTrackId);
Log.i(MyTracksConstants.TAG, "ExportAllTracks: exporting: " + id);
TrackWriter writer =
TrackWriterFactory.newWriter(context, providerUtils, id, format);
TrackWriterFactory.newWriter(activity, providerUtils, id, format);
writer.writeTrack();
if (!writer.wasSuccess()) {
// Abort the whole export on the first error.
int error = writer.getErrorMessage();
Toast.makeText(context, error, Toast.LENGTH_LONG).show();
Toast.makeText(activity, error, Toast.LENGTH_LONG).show();
return;
}
}
@@ -213,7 +214,7 @@ public class ExportAllTracks {
Log.i(MyTracksConstants.TAG, "ExportAllTracks: Aquiring wake lock.");
try {
PowerManager pm =
(PowerManager) context.getSystemService(Context.POWER_SERVICE);
(PowerManager) activity.getSystemService(Context.POWER_SERVICE);
if (pm == null) {
Log.e(MyTracksConstants.TAG,
"ExportAllTracks: Power manager not found!");
@@ -28,7 +28,7 @@ import android.view.MenuItem;
*/
class MenuManager {
private MyTracks activity;
private final MyTracks activity;
public MenuManager(MyTracks activity) {
this.activity = activity;
@@ -44,12 +44,12 @@ class MenuManager {
menu.findItem(R.id.menu_list_tracks).setEnabled(hasRecorded);
menu.findItem(R.id.menu_list_markers)
.setEnabled(hasRecorded && hasSelectedTrack);
MenuItem startRecording = menu.findItem(R.id.menu_start_recording);
startRecording.setEnabled(!isRecording);
startRecording.setVisible(!isRecording);
MenuItem stopRecording = menu.findItem(R.id.menu_stop_recording);
stopRecording.setEnabled(isRecording);
stopRecording.setVisible(isRecording);
menu.findItem(R.id.menu_start_recording)
.setEnabled(!isRecording)
.setVisible(!isRecording);
menu.findItem(R.id.menu_stop_recording)
.setEnabled(isRecording)
.setVisible(isRecording);
}
public boolean onOptionsItemSelected(MenuItem item) {
@@ -69,7 +69,7 @@ class MenuManager {
}
case R.id.menu_list_markers: {
Intent startIntent = new Intent(activity, MyTracksWaypointsList.class);
startIntent.putExtra("trackid", activity.getSelectedTrack());
startIntent.putExtra("trackid", activity.getSelectedTrackId());
activity.startActivityForResult(startIntent, MyTracksConstants.SHOW_WAYPOINT);
return true;
}
@@ -78,13 +78,18 @@ class MenuManager {
activity.startActivity(startIntent);
return true;
}
case R.id.menu_aggregated_stats: {
Intent startIntent = new Intent(activity, AggregatedStatsActivity.class);
activity.startActivity(startIntent);
return true;
}
case R.id.menu_help: {
Intent startIntent = new Intent(activity, WelcomeActivity.class);
activity.startActivity(startIntent);
return true;
}
case MyTracksConstants.MENU_CLEAR_MAP: {
activity.setSelectedTrack(-1);
activity.setSelectedTrackId(-1);
return true;
}
}
File diff suppressed because it is too large Load Diff
@@ -15,8 +15,6 @@
*/
package com.google.android.apps.mytracks;
import android.os.Build;
/**
* Constants used by the MyTracks application.
*
@@ -40,11 +38,6 @@ public abstract class MyTracksConstants {
*/
public static final String SDCARD_TOP_DIR = "MyTracks";
/**
* The API level of the Android version we're being run under.
*/
public static final int ANDROID_API_LEVEL = Integer.parseInt(Build.VERSION.SDK);
/*
* onActivityResult request codes:
*/
@@ -179,6 +172,13 @@ public abstract class MyTracksConstants {
* The type of account that we can use for gdata uploads.
*/
public static final String ACCOUNT_TYPE = "com.google";
/**
* The name of extra intent property to indicate whether we want to resume
* a previously recorded track.
*/
public static final String RESUME_TRACK_EXTRA_NAME =
"com.google.android.apps.mytracks.RESUME_TRACK";
public static int getActionFromMenuId(int menuId) {
switch (menuId) {
@@ -21,7 +21,9 @@ import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.content.TrackPointsColumns;
import com.google.android.apps.mytracks.content.Waypoint;
import com.google.android.apps.mytracks.content.WaypointsColumns;
import com.google.android.apps.mytracks.services.StatusAnnouncerFactory;
import com.google.android.apps.mytracks.stats.TripStatistics;
import com.google.android.apps.mytracks.util.ApiFeatures;
import com.google.android.apps.mytracks.util.MyTracksUtils;
import com.google.android.maps.GeoPoint;
import com.google.android.maps.MapActivity;
@@ -47,17 +49,16 @@ import android.os.Bundle;
import android.os.Handler;
import android.os.HandlerThread;
import android.provider.Settings;
import android.speech.tts.TextToSpeech;
import android.util.Log;
import android.view.ContextMenu;
import android.view.ContextMenu.ContextMenuInfo;
import android.view.Menu;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.SubMenu;
import android.view.View;
import android.view.View.OnCreateContextMenuListener;
import android.view.Window;
import android.view.ContextMenu.ContextMenuInfo;
import android.view.View.OnCreateContextMenuListener;
import android.widget.ImageButton;
import android.widget.LinearLayout;
import android.widget.RelativeLayout;
@@ -70,8 +71,9 @@ import android.widget.Toast;
* @author Leif Hendrik Wilden
*/
public class MyTracksMap extends MapActivity
implements LocationListener, SensorEventListener, View.OnTouchListener,
View.OnClickListener, SharedPreferences.OnSharedPreferenceChangeListener {
implements View.OnTouchListener, View.OnClickListener,
SharedPreferences.OnSharedPreferenceChangeListener {
private static final int TRACKPOINT_BUFFER_SIZE = 1024;
// Saved instance state keys:
// ---------------------------
@@ -82,9 +84,9 @@ public class MyTracksMap extends MapActivity
public static final String KEY_HAVE_GOOD_FIX = "haveGoodFix";
/**
* The currently selected track (or null if nothing selected).
* The ID of the currently selected track (or -1 if nothing selected).
*/
private Track selectedTrack;
private long selectedTrackId = -1;
/**
* The id of the currently recording track.
@@ -97,20 +99,26 @@ public class MyTracksMap extends MapActivity
*/
private boolean keepMyLocationVisible;
/**
* Id of the first location that was seen when reading tracks from the
* provider.
*/
private long firstSeenLocationId = -1;
/**
* Id of the last location that was seen when reading tracks from the
* provider. This is used to determine which locations are new compared to the
* last time the mapOverlay was updated.
*/
private long lastSeenLocationId = -1;
/**
* Magnetic variation.
*/
private double variation;
/**
* From the shared preferences:
* From the shared preferences.
*/
private int minRequiredAccuracy =
MyTracksSettings.DEFAULT_MIN_REQUIRED_ACCURACY;
@@ -127,32 +135,31 @@ public class MyTracksMap extends MapActivity
private Location currentLocation;
/**
* A thread with a looper. Post to updateTrackHandler to execute Runnables on
* this thread.
* A thread with a looper. Post to updateTrackHandler to execute
* {@link Runnable}s on this thread.
*/
private final HandlerThread updateTrackThread =
new HandlerThread("updateTrackThread");
private HandlerThread updateTrackThread;
/** Handler for updateTrackThread */
/**
* Handler for updateTrackThread.
*/
private Handler updateTrackHandler;
private MyTracksProviderUtils providerUtils;
private SharedPreferences sharedPreferences;
/**
* This value is used to decide how many points to display.
*/
private int samplingFrequency = 1;
/**
* A runnable that updates the track from the provider (looking for points
* added after "lastSeenLocationId".
* added after "lastSeenLocationId").
*/
private final Runnable updateTrackRunnable = new Runnable() {
@Override
public void run() {
if (selectedTrack == null) {
if (!isATrackSelected()) {
return;
}
readAllNewTrackPoints();
}
};
@@ -163,11 +170,13 @@ public class MyTracksMap extends MapActivity
private Runnable restoreTrackRunnable = new Runnable() {
@Override
public void run() {
if (selectedTrack == null) {
if (!isATrackSelected()) {
return;
}
mapOverlay.clearPoints();
lastSeenLocationId = selectedTrack.getStartId();
firstSeenLocationId = -1;
lastSeenLocationId = -1;
readAllNewTrackPoints();
}
};
@@ -178,7 +187,7 @@ public class MyTracksMap extends MapActivity
private final Runnable restoreWaypointsRunnable = new Runnable() {
@Override
public void run() {
if (selectedTrack == null) {
if (!isATrackSelected()) {
return;
}
@@ -188,15 +197,12 @@ public class MyTracksMap extends MapActivity
// We will silently drop extra waypoints to make the app responsive.
// TODO: Try to only load the waypoints in the view port.
cursor = providerUtils.getWaypointsCursor(
selectedTrack.getId(), 0,
selectedTrackId, 0,
MyTracksConstants.MAX_DISPLAYED_WAYPOINTS_POINTS);
if (cursor != null) {
if (cursor.moveToFirst()) {
do {
Waypoint wpt = providerUtils.createWaypoint(cursor);
mapOverlay.addWaypoint(wpt);
} while (cursor.moveToNext());
}
if (cursor != null && cursor.moveToFirst()) {
do {
mapOverlay.addWaypoint(providerUtils.createWaypoint(cursor));
} while (cursor.moveToNext());
}
} catch (RuntimeException e) {
Log.w(MyTracksConstants.TAG, "Caught an unexpected exception.", e);
@@ -210,19 +216,18 @@ public class MyTracksMap extends MapActivity
};
/**
* A runnable intended to be posted to the updateTrackThread after the
* selected track changes. It will post to the ui thread to update the screen
* elements and move the map to show the selected track.
* A runnable intended to be posted to the {@code #updateTrackThread} after
* the selected track changes. It will post to the UI thread to update
* the screen elements and move the map to show the selected track.
*/
private final Runnable setSelectedTrackRunnable = new Runnable() {
@Override
public void run() {
uiHandler.post(new Runnable() {
public void run() {
showTrack(selectedTrack);
mapOverlay.setSelectedTrack(selectedTrack);
mapOverlay.setShowEndMarker(selectedTrack == null ||
selectedTrack.getId() != recordingTrackId);
showTrack(selectedTrackId);
mapOverlay.setTrackDrawingEnabled(isATrackSelected());
mapOverlay.setShowEndMarker(!isRecordingSelected());
mapView.invalidate();
busyPane.setVisibility(View.GONE);
updateOptionsButton();
@@ -281,7 +286,9 @@ public class MyTracksMap extends MapActivity
super.onCreate(bundle);
// The volume we want to control is the Text-To-Speech volume
setVolumeControlStream(TextToSpeech.Engine.DEFAULT_STREAM);
int volumeStream =
new StatusAnnouncerFactory(ApiFeatures.getInstance()).getVolumeStream();
setVolumeControlStream(volumeStream);
providerUtils = MyTracksProviderUtils.Factory.get(this);
@@ -316,6 +323,7 @@ public class MyTracksMap extends MapActivity
locationManager =
(LocationManager) getSystemService(Context.LOCATION_SERVICE);
updateTrackThread = new HandlerThread("updateTrackThread");
updateTrackThread.start();
updateTrackHandler = new Handler(updateTrackThread.getLooper());
@@ -325,14 +333,7 @@ public class MyTracksMap extends MapActivity
@Override
public void onChange(boolean selfChange) {
Log.d(MyTracksConstants.TAG, "MyTracksMap: ContentObserver.onChange");
// Check for any new locations and append them to the currently
// recording track:
if (!MyTracks.getInstance().isRecording()) {
// No track is being recorded. We should not be here.
return;
}
if (selectedTrack == null
|| selectedTrack.getId() != recordingTrackId) {
if (!isRecordingSelected()) {
// No track, or one other than the recording track is selected,
// don't bother.
return;
@@ -348,7 +349,7 @@ public class MyTracksMap extends MapActivity
public void onChange(boolean selfChange) {
Log.d(MyTracksConstants.TAG,
"MyTracksMap: ContentObserver.onChange waypoints");
if (selectedTrack == null) {
if (!isATrackSelected()) {
return;
}
updateTrackHandler.post(restoreWaypointsRunnable);
@@ -356,37 +357,49 @@ public class MyTracksMap extends MapActivity
}
};
// Read shared preferences and register change listener:
SharedPreferences preferences =
getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0);
if (preferences != null) {
minRequiredAccuracy = preferences.getInt(
getString(R.string.min_required_accuracy_key),
MyTracksSettings.DEFAULT_MIN_REQUIRED_ACCURACY);
recordingTrackId =
preferences.getLong(getString(R.string.recording_track_key), -1);
long selectedTrackId =
preferences.getLong(getString(R.string.selected_track_key), -1);
if (selectedTrackId >= 0) {
setSelectedTrack(selectedTrackId);
}
// Read shared preferences and register change listener.
sharedPreferences = getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0);
if (sharedPreferences != null) {
reloadSharedPreferences(sharedPreferences, null);
updateOptionsButton();
preferences.registerOnSharedPreferenceChangeListener(this);
sharedPreferences.registerOnSharedPreferenceChangeListener(this);
}
}
protected void setupZoomControls() {
mapView.setBuiltInZoomControls(true);
}
@Override
protected void onDestroy() {
// Final cleanup before activity is destroyed.
// May not be called at all in some situations.
Log.d(MyTracksConstants.TAG, "MyTracksMap.onDestroy");
if (updateTrackThread != null) {
ApiFeatures.getInstance().getApiPlatformAdapter().stopHandlerThread(
updateTrackThread);
}
if (sharedPreferences != null) {
sharedPreferences.unregisterOnSharedPreferenceChangeListener(this);
}
super.onDestroy();
}
/**
* Returns whether there's a track currently selected for display.
*/
private boolean isATrackSelected() {
return selectedTrackId >= 0;
}
/**
* Returns whether we're currently recording the same track that's selected
* for display.
*/
private boolean isRecordingSelected() {
return isATrackSelected() && selectedTrackId == recordingTrackId;
}
protected void setupZoomControls() {
mapView.setBuiltInZoomControls(true);
}
@Override
protected void onStart() {
// Called after onCreate or onStop.
@@ -429,6 +442,9 @@ public class MyTracksMap extends MapActivity
Log.d(MyTracksConstants.TAG, "MyTracksMap.onResume");
super.onResume();
// Reload all preferences as they might have changed since last run.
reloadSharedPreferences(sharedPreferences, null);
// Make sure any updates that might have happened are propagated to the
// Map overlay:
observer.onChange(false);
@@ -449,10 +465,9 @@ public class MyTracksMap extends MapActivity
// While this activity was paused the user may have deleted the selected
// track. In that case the map overlay needs to be cleared:
Track track = mapOverlay.getSelectedTrack();
if (track != null && !providerUtils.trackExists(track.getId())) {
if (isATrackSelected() && !providerUtils.trackExists(selectedTrackId)) {
// The recording track must have been deleted meanwhile.
mapOverlay.setSelectedTrack(null);
mapOverlay.setTrackDrawingEnabled(false);
mapView.invalidate();
}
}
@@ -514,10 +529,10 @@ public class MyTracksMap extends MapActivity
+ gpsProvider.getName());
}
locationManager.requestLocationUpdates(gpsProvider.getName(),
0 /*minTime*/, 0 /*minDist*/, this);
0 /*minTime*/, 0 /*minDist*/, locationListener);
try {
locationManager.requestLocationUpdates(LocationManager.NETWORK_PROVIDER,
1000 * 60 * 5 /*minTime*/, 0 /*minDist*/, this);
1000 * 60 * 5 /*minTime*/, 0 /*minDist*/, locationListener);
} catch (RuntimeException e) {
// If anything at all goes wrong with getting a cell location do not
// abort. Cell location is not essential to this app.
@@ -535,7 +550,7 @@ public class MyTracksMap extends MapActivity
Log.d(MyTracksConstants.TAG,
"MyTracksMap: Now registering sensor listeners.");
sensorManager.registerListener(
this, compass, SensorManager.SENSOR_DELAY_UI);
sensorListener, compass, SensorManager.SENSOR_DELAY_UI);
}
/**
@@ -545,12 +560,12 @@ public class MyTracksMap extends MapActivity
if (locationManager != null) {
Log.d(MyTracksConstants.TAG,
"MyTracksMap: Now unregistering location listeners.");
locationManager.removeUpdates(this);
locationManager.removeUpdates(locationListener);
}
if (sensorManager != null) {
Log.d(MyTracksConstants.TAG,
"MyTracksMap: Now unregistering sensor listeners.");
sensorManager.unregisterListener(this);
sensorManager.unregisterListener(sensorListener);
}
}
@@ -579,7 +594,7 @@ public class MyTracksMap extends MapActivity
*/
private void updateOptionsButton() {
optionsBtn.setVisibility(
selectedTrack != null ? View.VISIBLE : View.INVISIBLE);
isATrackSelected() ? View.VISIBLE : View.INVISIBLE);
}
/**
@@ -632,12 +647,18 @@ public class MyTracksMap extends MapActivity
/**
* Zooms and pans the map so that the given track is visible.
*
* @param track a given track
* @param trackId a given track ID
*/
public void showTrack(Track track) {
if (track == null || mapView == null || track.getNumberOfPoints() < 2) {
public void showTrack(long trackId) {
if (mapView == null) {
return;
}
Track track = providerUtils.getTrack(trackId);
if (track == null || track.getNumberOfPoints() < 2) {
return;
}
TripStatistics stats = track.getStatistics();
int bottom = stats.getBottom();
int left = stats.getLeft();
@@ -680,24 +701,33 @@ public class MyTracksMap extends MapActivity
* @param trackId a given track id
*/
public void setSelectedTrack(final long trackId) {
if (selectedTrack != null && selectedTrack.getId() == trackId) {
Log.d(MyTracksConstants.TAG, "MyTracksMap.setSelectedTrack: "
+ "selectedTrackId = " + selectedTrackId + ", trackId = " + trackId);
if (selectedTrackId == trackId) {
// Selected track did not change, nothing to do.
mapOverlay.setSelectedTrack(selectedTrack);
mapView.invalidate();
mapOverlay.setTrackDrawingEnabled(isATrackSelected());
updateOptionsButton();
mapView.invalidate();
return;
}
if (trackId < 0) {
// Remove selection.
selectedTrack = null;
mapOverlay.setSelectedTrack(null);
selectedTrackId = -1;
mapOverlay.setTrackDrawingEnabled(false);
mapOverlay.clearWaypoints();
updateOptionsButton();
mapView.invalidate();
return;
}
busyPane.setVisibility(View.VISIBLE);
selectedTrack = providerUtils.getTrack(trackId);
selectedTrackId = trackId;
loadSelectedTrack();
}
private void loadSelectedTrack() {
updateTrackHandler.post(restoreTrackRunnable);
updateTrackHandler.post(restoreWaypointsRunnable);
updateTrackHandler.post(setSelectedTrackRunnable);
@@ -753,8 +783,7 @@ public class MyTracksMap extends MapActivity
menu.setHeaderTitle(R.string.tracklist_this_track);
menu.add(0, MyTracksConstants.MENU_EDIT, 0,
R.string.tracklist_edit_track);
if (!MyTracks.getInstance().isRecording() || (selectedTrack != null
&& selectedTrack.getId() != recordingTrackId)) {
if (!isRecordingSelected()) {
menu.add(0, MyTracksConstants.MENU_SEND_TO_GOOGLE, 0,
R.string.tracklist_send_to_google);
SubMenu share = menu.addSubMenu(0, MyTracksConstants.MENU_SHARE, 0,
@@ -791,7 +820,7 @@ public class MyTracksMap extends MapActivity
@Override
public boolean onMenuItemSelected(int featureId, MenuItem item) {
if (!super.onMenuItemSelected(featureId, item)) {
if (selectedTrack != null) {
if (isATrackSelected()) {
MyTracks.getInstance().onActivityResult(
MyTracksConstants.getActionFromMenuId(item.getItemId()), RESULT_OK,
new Intent());
@@ -852,8 +881,7 @@ public class MyTracksMap extends MapActivity
public void onClick(View v) {
if (v == messagePane) {
launchMyLocationSettings();
}
if (v == optionsBtn) {
} else if (v == optionsBtn) {
optionsBtn.performLongClick();
}
}
@@ -874,160 +902,189 @@ public class MyTracksMap extends MapActivity
@Override
public void onSharedPreferenceChanged(
final SharedPreferences sharedPreferences,
final String key) {
final SharedPreferences sharedPreferences, final String key) {
Log.d(MyTracksConstants.TAG,
"MyTracksMap.onSharedPreferenceChanged: " + key);
if (key != null) {
uiHandler.post(new Runnable() {
@Override
public void run() {
if (key.equals(getString(R.string.min_required_accuracy_key))) {
minRequiredAccuracy = sharedPreferences.getInt(
getString(R.string.min_required_accuracy_key),
MyTracksSettings.DEFAULT_MIN_REQUIRED_ACCURACY);
} else if (key.equals(getString(R.string.selected_track_key))) {
long selectedTrackId =
sharedPreferences.getLong(
getString(R.string.selected_track_key),
-1);
setSelectedTrack(selectedTrackId);
} else if (key.equals(getString(R.string.recording_track_key))) {
recordingTrackId =
sharedPreferences.getLong(
getString(R.string.recording_track_key),
-1);
if (selectedTrack != null) {
mapOverlay.setShowEndMarker(
selectedTrack.getId() != recordingTrackId);
mapView.postInvalidate();
}
}
reloadSharedPreferences(sharedPreferences, key);
}
});
}
}
@Override
public void onProviderEnabled(String provider) {
if (provider.equals(MyTracksConstants.GPS_PROVIDER)) {
messageText.setText(R.string.wait_for_fix);
}
}
@Override
public void onProviderDisabled(String provider) {
if (provider.equals(MyTracksConstants.GPS_PROVIDER)) {
messageText.setText(R.string.status_enable_gps);
messagePane.setVisibility(View.VISIBLE);
messagePane.setOnClickListener(this);
screen.requestLayout();
}
}
@Override
public void onLocationChanged(Location location) {
if (location.getProvider().equals(MyTracksConstants.GPS_PROVIDER)) {
// Recalculate the variation if there was a jump in location > 1km:
if (currentLocation == null
|| location.distanceTo(currentLocation) > 1000) {
setVariation(location);
private final LocationListener locationListener = new LocationListener() {
@Override
public void onProviderEnabled(String provider) {
if (provider.equals(MyTracksConstants.GPS_PROVIDER)) {
messageText.setText(R.string.wait_for_fix);
}
currentLocation = location;
boolean haveGoodFixNow =
currentLocation.getAccuracy() < minRequiredAccuracy;
if (haveGoodFixNow != haveGoodFix) {
haveGoodFix = haveGoodFixNow;
messagePane.setVisibility(haveGoodFix ? View.GONE : View.VISIBLE);
}
@Override
public void onProviderDisabled(String provider) {
if (provider.equals(MyTracksConstants.GPS_PROVIDER)) {
messageText.setText(R.string.status_enable_gps);
messagePane.setVisibility(View.VISIBLE);
messagePane.setOnClickListener(MyTracksMap.this);
screen.requestLayout();
}
showCurrentLocation();
} else {
Log.d(MyTracksConstants.TAG,
"MyTracksMap: Network location update received.");
}
}
@Override
public void onStatusChanged(String provider, int status, Bundle extras) {
if (provider.equals(MyTracksConstants.GPS_PROVIDER)) {
switch (status) {
case LocationProvider.OUT_OF_SERVICE:
case LocationProvider.TEMPORARILY_UNAVAILABLE:
haveGoodFix = false;
messagePane.setVisibility(View.VISIBLE);
@Override
public void onLocationChanged(Location location) {
if (location.getProvider().equals(MyTracksConstants.GPS_PROVIDER)) {
// Recalculate the variation if there was a jump in location > 1km:
if (currentLocation == null ||
location.distanceTo(currentLocation) > 1000) {
setVariation(location);
}
currentLocation = location;
boolean haveGoodFixNow =
currentLocation.getAccuracy() < minRequiredAccuracy;
if (haveGoodFixNow != haveGoodFix) {
haveGoodFix = haveGoodFixNow;
messagePane.setVisibility(haveGoodFix ? View.GONE : View.VISIBLE);
screen.requestLayout();
break;
}
showCurrentLocation();
} else {
Log.d(MyTracksConstants.TAG,
"MyTracksMap: Network location update received.");
}
}
@Override
public void onStatusChanged(String provider, int status, Bundle extras) {
if (provider.equals(MyTracksConstants.GPS_PROVIDER)) {
switch (status) {
case LocationProvider.OUT_OF_SERVICE:
case LocationProvider.TEMPORARILY_UNAVAILABLE:
haveGoodFix = false;
messagePane.setVisibility(View.VISIBLE);
screen.requestLayout();
break;
}
}
}
};
private final SensorEventListener sensorListener = new SensorEventListener() {
@Override
public void onSensorChanged(SensorEvent se) {
synchronized (this) {
float magneticHeading = se.values[0];
double heading = magneticHeading + variation;
if (mapOverlay.setHeading((float) heading)) {
mapView.invalidate();
}
}
}
@Override
public void onAccuracyChanged(Sensor s, int accuracy) {
// do nothing
}
};
private void reloadSharedPreferences(SharedPreferences sharedPreferences,
String key) {
if (key == null ||
key.equals(getString(R.string.min_required_accuracy_key))) {
minRequiredAccuracy = sharedPreferences.getInt(
getString(R.string.min_required_accuracy_key),
MyTracksSettings.DEFAULT_MIN_REQUIRED_ACCURACY);
}
if (key == null || key.equals(getString(R.string.recording_track_key))) {
recordingTrackId = sharedPreferences.getLong(
getString(R.string.recording_track_key), -1);
}
if (key == null || key.equals(getString(R.string.selected_track_key))) {
setSelectedTrack(sharedPreferences.getLong(
getString(R.string.selected_track_key), -1));
}
// Show end marker if the track has been selected and is not recording.
// Note: This check must be *after* a call to setSelectedTrack(...) above.
if (isATrackSelected()) {
mapOverlay.setShowEndMarker(!isRecordingSelected());
mapView.postInvalidate();
}
}
@Override
public void onSensorChanged(SensorEvent se) {
synchronized (this) {
float magneticHeading = se.values[0];
double heading = magneticHeading + variation;
if (mapOverlay.setHeading((float) heading)) {
mapView.invalidate();
}
}
}
@Override
public void onAccuracyChanged(Sensor s, int accuracy) {
// do nothing
}
/**
* Set the sampling frequency from the total number of points in the track.
*
* @param track The track to read the total number of points from.
*/
private void setSamplingFrequency(Track track) {
long totalLocations = track.getStopId() - track.getStartId();
// Limit the number of map points readings.
samplingFrequency =
(int) (1 +
totalLocations / MyTracksConstants.TARGET_DISPLAYED_TRACK_POINTS);
Log.i(MyTracksConstants.TAG, "Sampling locations: " + samplingFrequency);
}
private void readAllNewTrackPoints() {
Cursor cursor = null;
// Refetch the track to get the latest StopId
selectedTrack = providerUtils.getTrack(selectedTrack.getId());
long totalLocations = selectedTrack.getStopId() -
selectedTrack.getStartId();
int numPoints = mapOverlay.getNumLocations();
if (numPoints >= MyTracksConstants.MAX_DISPLAYED_TRACK_POINTS) {
// We're about to exceed the maximum allowed number of points, so reload
// the whole track with fewer points (the sampling frequency will be
// lower).
loadSelectedTrack();
return;
}
// Keep a copy of selectedTrackId, because it can change asynchronously.
long currentSelectedTrackId = selectedTrackId;
long lastStoredLocationId =
providerUtils.getLastLocationId(currentSelectedTrackId);
int samplingFrequency = -1;
Location location = new Location("");
while (currentSelectedTrackId == selectedTrackId) {
Cursor cursor = null;
try {
cursor = providerUtils.getLocationsCursor(currentSelectedTrackId,
lastSeenLocationId + 1, TRACKPOINT_BUFFER_SIZE, false);
if (cursor == null || !cursor.moveToFirst()) {
// No (more) data
break;
}
setSamplingFrequency(selectedTrack);
int bufferSize = 1024;
int points = 0;
while (lastSeenLocationId < selectedTrack.getStopId()) {
cursor = providerUtils.getLocationsCursor(
selectedTrack.getId(), lastSeenLocationId, bufferSize, false);
if (cursor != null && cursor.moveToFirst()) {
final int idColumnIdx = cursor.getColumnIndexOrThrow(
TrackPointsColumns._ID);
while (cursor.moveToNext()) {
points++;
Location location = providerUtils.createLocation(cursor);
lastSeenLocationId = cursor.getLong(idColumnIdx);
do {
long locationId = cursor.getLong(idColumnIdx);
lastSeenLocationId = locationId;
if (firstSeenLocationId == -1) {
// This was our first point, keep its ID
firstSeenLocationId = locationId;
}
if (samplingFrequency == -1) {
// Now we already have at least one point, calculate the sampling
// frequency
long numTotalPoints = lastStoredLocationId - firstSeenLocationId;
samplingFrequency = (int) (1 + numTotalPoints
/ MyTracksConstants.TARGET_DISPLAYED_TRACK_POINTS);
// TODO: This shouldn't happen after adding currentSelectedTrackId,
// but just to be safe until we have 100% confidence.
if (samplingFrequency <= 0) {
Log.w(MyTracksConstants.TAG,
"readAllNewTrackPoints: samplingFreq <= 0, numTotalPoints = "
+ numTotalPoints + ", trackId = " + currentSelectedTrackId);
samplingFrequency = 1;
}
}
providerUtils.fillLocation(cursor, location);
// Include a point if it fits one of the following criteria:
// - Has the mod for the sampling frequency.
// - Is the first point.
// - Has the mod for the sampling frequency (includes first point).
// - Is the last point and we are not recording this track.
if (!MyTracksUtils.isValidLocation(location) ||
points % samplingFrequency == 0 ||
points == 0 ||
(recordingTrackId != selectedTrack.getId() &&
points == (totalLocations - 1))) {
// - The point is a segment split
if (numPoints % samplingFrequency == 0 ||
(!isRecordingSelected() && locationId == lastStoredLocationId) ||
!MyTracksUtils.isValidLocation(location)) {
// No need to allocate a new location (we can safely reuse the existing).
mapOverlay.addLocation(location);
}
numPoints++;
} while (cursor.moveToNext() &&
currentSelectedTrackId == selectedTrackId);
} finally {
if (cursor != null) {
cursor.close();
}
} else {
lastSeenLocationId += bufferSize;
}
cursor.close();
cursor = null;
}
mapView.postInvalidate();
}
@@ -15,17 +15,17 @@
*/
package com.google.android.apps.mytracks;
import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.content.Waypoint;
import com.google.android.apps.mytracks.util.MyTracksUtils;
import com.google.android.maps.GeoPoint;
import com.google.android.maps.MapView;
import com.google.android.maps.Overlay;
import com.google.android.maps.Projection;
import com.google.android.maps.mytracks.R;
import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Path;
@@ -35,6 +35,9 @@ import android.graphics.drawable.Drawable;
import android.location.Location;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
/**
* A map overlay that displays a "MyLocation" arrow, an error circle, the
@@ -44,7 +47,7 @@ import java.util.ArrayList;
*/
public class MyTracksOverlay extends Overlay {
private final Drawable arrow[] = new Drawable[18];
private final Drawable[] arrows;
private final int arrowWidth, arrowHeight;
private final Drawable statsMarker;
private final Drawable waypointMarker;
@@ -54,102 +57,152 @@ public class MyTracksOverlay extends Overlay {
private final Paint selectedTrackPaint;
private final Paint errorCirclePaint;
private final Context context;
private final ArrayList<Waypoint> waypoints = new ArrayList<Waypoint>();
private final ArrayList<Location> points = new ArrayList<Location>(1024);
private final List<Waypoint> waypoints;
private final List<CachedLocation> points;
private final BlockingQueue<CachedLocation> pendingPoints;
private Track selectedTrack;
private boolean trackDrawingEnabled;
private int lastHeading = 0;
private Location myLocation;
private boolean showEndMarker = true;
// TODO: Remove it completely after completing performance tests.
private boolean alwaysVisible = true;
private GeoPoint lastReferencePoint;
private Rect lastViewRect;
private Path lastPath;
/**
* Represents a pre-processed {@code Location} to speed up drawing.
* This class is more like a data object and doesn't provide accessors.
*/
private static class CachedLocation {
public final boolean valid;
public final GeoPoint geoPoint;
public CachedLocation(Location location) {
this.valid = MyTracksUtils.isValidLocation(location);
this.geoPoint = valid ? MyTracksUtils.getGeoPoint(location) : null;
}
};
public MyTracksOverlay(Context context) {
this.context = context;
this.waypoints = new ArrayList<Waypoint>();
this.points = new ArrayList<CachedLocation>(1024);
this.pendingPoints = new ArrayBlockingQueue<CachedLocation>(
MyTracksConstants.MAX_DISPLAYED_TRACK_POINTS, true);
// TODO: Can we use a FrameAnimation or similar here rather
// than individual resources for each arrow direction?
arrow[0] = context.getResources().getDrawable(R.drawable.arrow_0);
arrow[1] = context.getResources().getDrawable(R.drawable.arrow_20);
arrow[2] = context.getResources().getDrawable(R.drawable.arrow_40);
arrow[3] = context.getResources().getDrawable(R.drawable.arrow_60);
arrow[4] = context.getResources().getDrawable(R.drawable.arrow_80);
arrow[5] = context.getResources().getDrawable(R.drawable.arrow_100);
arrow[6] = context.getResources().getDrawable(R.drawable.arrow_120);
arrow[7] = context.getResources().getDrawable(R.drawable.arrow_140);
arrow[8] = context.getResources().getDrawable(R.drawable.arrow_160);
arrow[9] = context.getResources().getDrawable(R.drawable.arrow_180);
arrow[10] = context.getResources().getDrawable(R.drawable.arrow_200);
arrow[11] = context.getResources().getDrawable(R.drawable.arrow_220);
arrow[12] = context.getResources().getDrawable(R.drawable.arrow_240);
arrow[13] = context.getResources().getDrawable(R.drawable.arrow_260);
arrow[14] = context.getResources().getDrawable(R.drawable.arrow_280);
arrow[15] = context.getResources().getDrawable(R.drawable.arrow_300);
arrow[16] = context.getResources().getDrawable(R.drawable.arrow_320);
arrow[17] = context.getResources().getDrawable(R.drawable.arrow_340);
arrowWidth = arrow[lastHeading].getIntrinsicWidth();
arrowHeight = arrow[lastHeading].getIntrinsicHeight();
for (int i = 0; i <= 17; i++) {
arrow[i].setBounds(0, 0, arrowWidth, arrowHeight);
final Resources resources = context.getResources();
arrows = new Drawable[] {
resources.getDrawable(R.drawable.arrow_0),
resources.getDrawable(R.drawable.arrow_20),
resources.getDrawable(R.drawable.arrow_40),
resources.getDrawable(R.drawable.arrow_60),
resources.getDrawable(R.drawable.arrow_80),
resources.getDrawable(R.drawable.arrow_100),
resources.getDrawable(R.drawable.arrow_120),
resources.getDrawable(R.drawable.arrow_140),
resources.getDrawable(R.drawable.arrow_160),
resources.getDrawable(R.drawable.arrow_180),
resources.getDrawable(R.drawable.arrow_200),
resources.getDrawable(R.drawable.arrow_220),
resources.getDrawable(R.drawable.arrow_240),
resources.getDrawable(R.drawable.arrow_260),
resources.getDrawable(R.drawable.arrow_280),
resources.getDrawable(R.drawable.arrow_300),
resources.getDrawable(R.drawable.arrow_320),
resources.getDrawable(R.drawable.arrow_340)
};
arrowWidth = arrows[lastHeading].getIntrinsicWidth();
arrowHeight = arrows[lastHeading].getIntrinsicHeight();
for (Drawable arrow : arrows) {
arrow.setBounds(0, 0, arrowWidth, arrowHeight);
}
statsMarker = context.getResources().getDrawable(R.drawable.ylw_pushpin);
statsMarker = resources.getDrawable(R.drawable.ylw_pushpin);
markerWidth = statsMarker.getIntrinsicWidth();
markerHeight = statsMarker.getIntrinsicHeight();
statsMarker.setBounds(0, 0, markerWidth, markerHeight);
startMarker = context.getResources().getDrawable(R.drawable.green_dot);
startMarker = resources.getDrawable(R.drawable.green_dot);
startMarker.setBounds(0, 0, markerWidth, markerHeight);
endMarker = context.getResources().getDrawable(R.drawable.red_dot);
endMarker = resources.getDrawable(R.drawable.red_dot);
endMarker.setBounds(0, 0, markerWidth, markerHeight);
waypointMarker =
context.getResources().getDrawable(R.drawable.blue_pushpin);
waypointMarker = resources.getDrawable(R.drawable.blue_pushpin);
waypointMarker.setBounds(0, 0, markerWidth, markerHeight);
selectedTrackPaint = new Paint();
selectedTrackPaint.setColor(context.getResources().getColor(R.color.red));
selectedTrackPaint.setColor(resources.getColor(R.color.red));
selectedTrackPaint.setStrokeWidth(3);
selectedTrackPaint.setStyle(Paint.Style.STROKE);
selectedTrackPaint.setAntiAlias(true);
errorCirclePaint = new Paint();
errorCirclePaint.setColor(context.getResources().getColor(R.color.blue));
errorCirclePaint.setColor(resources.getColor(R.color.blue));
errorCirclePaint.setStyle(Paint.Style.STROKE);
errorCirclePaint.setStrokeWidth(3);
errorCirclePaint.setAlpha(127);
errorCirclePaint.setAntiAlias(true);
}
public void setSelectedTrack(Track track) {
selectedTrack = track;
}
public Track getSelectedTrack() {
return selectedTrack;
}
/**
* Add a location to the map overlay.
*
* NOTE: This method doesn't take ownership of the given location, so it is
* safe to reuse the same location while calling this method.
*
* NOTE: this method takes ownership of this location and may change it.
*
* @param l the location to add
* @param l the location to add.
*/
public void addLocation(Location l) {
points.add(l);
// Queue up in the pending queue until it's merged with {@code #points}.
pendingPoints.offer(new CachedLocation(l));
}
public void addWaypoint(Waypoint wpt) {
waypoints.add(wpt);
// Note: We don't cache waypoints, because it's not worth the effort.
if (wpt != null && wpt.getLocation() != null) {
synchronized (waypoints) {
waypoints.add(wpt);
}
}
}
public int getNumLocations() {
synchronized (points) {
return points.size() + pendingPoints.size();
}
}
// Visible for testing.
int getNumWaypoints() {
synchronized (waypoints) {
return waypoints.size();
}
}
public void clearPoints() {
synchronized (points) {
points.clear();
pendingPoints.clear();
lastPath = null;
lastViewRect = null;
}
}
public void clearWaypoints() {
waypoints.clear();
synchronized (waypoints) {
waypoints.clear();
}
}
public void clearPoints() {
points.clear();
public void setTrackDrawingEnabled(boolean trackDrawingEnabled) {
this.trackDrawingEnabled = trackDrawingEnabled;
}
public void setShowEndMarker(boolean showEndMarker) {
@@ -158,159 +211,179 @@ public class MyTracksOverlay extends Overlay {
@Override
public void draw(Canvas canvas, MapView mapView, boolean shadow) {
if (shadow) {
return;
}
// Draw the selected track:
drawTrack(canvas, mapView, selectedTrack, true);
// It's safe to keep projection within a single draw operation.
final Projection projection = getMapProjection(mapView);
// Get the current viewing window.
if (trackDrawingEnabled) {
Rect viewRect = getMapViewRect(mapView);
// Draw the selected track:
drawTrack(canvas, projection, viewRect);
// Draw the waypoints:
ArrayList<Waypoint> currentWaypoints = waypoints;
for (int i = 1; i < currentWaypoints.size(); i++) {
Waypoint wpt = currentWaypoints.get(i);
if (wpt == null) {
continue;
}
Location loc = wpt.getLocation();
if (loc == null) {
continue;
}
GeoPoint geoPoint = MyTracksUtils.getGeoPoint(loc);
Point pt = new Point();
mapView.getProjection().toPixels(geoPoint, pt);
canvas.save();
canvas.translate(pt.x - (markerWidth / 2) + 3, pt.y - (markerHeight));
if (wpt.getType() == Waypoint.TYPE_STATISTICS) {
statsMarker.draw(canvas);
} else {
waypointMarker.draw(canvas);
}
canvas.restore();
// Draw the waypoints:
drawWaypoints(canvas, projection);
}
// Draw the arrow icon:
if (myLocation == null) {
return;
}
GeoPoint geoPoint = new GeoPoint(
(int) (myLocation.getLatitude() * 1E6),
(int) (myLocation.getLongitude() * 1E6));
Point pt = new Point();
mapView.getProjection().toPixels(geoPoint, pt);
canvas.save();
canvas.translate(pt.x - (arrowWidth / 2), pt.y - (arrowHeight / 2));
arrow[lastHeading].draw(canvas);
canvas.restore();
// Draw the error circle:
float radius =
mapView.getProjection().metersToEquatorPixels(myLocation.getAccuracy());
canvas.drawCircle(pt.x, pt.y, radius, errorCirclePaint);
// Draw the current location
drawMyLocation(canvas, projection);
}
public void drawTrack(Canvas canvas, MapView mapView, Track track,
boolean selected) {
if (track == null) {
return;
}
if (points.size() < 2) {
return;
}
// Get the current viewing window:
// Visible for testing.
Projection getMapProjection(MapView mapView) {
return mapView.getProjection();
}
// Visible for testing.
Rect getMapViewRect(MapView mapView) {
int w = mapView.getLongitudeSpan();
int h = mapView.getLatitudeSpan();
int cx = mapView.getMapCenter().getLongitudeE6();
int cy = mapView.getMapCenter().getLatitudeE6();
Rect rect = new Rect(cx - w, cy - h, cx + w, cy + h);
return new Rect(cx - w / 2, cy - h / 2, cx + w / 2, cy + h / 2);
}
// Visible for testing.
Path newPath() {
return new Path();
}
// Visible for testing.
Path getLastPath() {
return lastPath;
}
Point pt = new Point();
GeoPoint geoPoint;
Location loc;
Location lastValidLocation = null;
Path path;
boolean wasInside;
// Do as much allocation and preparation outside the loop over track
// points:
wasInside = false;
int locLon = 0, locLat = 0, minLon, maxLon, minLat, maxLat;
int lastLocLon = (int) (points.get(0).getLongitude() * 1E6);
int lastLocLat = (int) (points.get(0).getLatitude() * 1E6);
// Loop over track points:
path = new Path();
for (int i = 1; i < points.size(); i++) {
loc = points.get(i);
if (loc == null) {
continue;
}
locLon = (int) (loc.getLongitude() * 1E6);
locLat = (int) (loc.getLatitude() * 1E6);
// Draw a line segment if it's inside the viewing window:
if (locLat < 90E6 && lastLocLat < 90E6) {
lastValidLocation = loc;
minLon = Math.min(locLon, lastLocLon);
maxLon = Math.max(locLon, lastLocLon);
minLat = Math.min(locLat, lastLocLat);
maxLat = Math.max(locLat, lastLocLat);
if (rect.intersects(minLon, minLat, maxLon, maxLat)) {
if (!wasInside) {
geoPoint = new GeoPoint(lastLocLat, lastLocLon);
mapView.getProjection().toPixels(geoPoint, pt);
path.moveTo(pt.x, pt.y);
}
geoPoint = new GeoPoint(locLat, locLon);
mapView.getProjection().toPixels(geoPoint, pt);
path.lineTo(pt.x, pt.y);
wasInside = rect.contains(locLon, locLat);
}
} else {
wasInside = false;
}
lastLocLon = locLon;
lastLocLat = locLat;
}
canvas.drawPath(path, selectedTrackPaint);
// Draw the "End" marker:
Location currentLastValidLocation = lastValidLocation;
if (showEndMarker && currentLastValidLocation != null) {
canvas.save();
geoPoint = MyTracksUtils.getGeoPoint(currentLastValidLocation);
mapView.getProjection().toPixels(geoPoint, pt);
canvas.translate(pt.x - (markerWidth / 2), pt.y - markerHeight);
endMarker.draw(canvas);
canvas.restore();
}
// Draw the "Start" marker:
for (int i = 0; i < points.size(); i++) {
loc = points.get(i);
if (loc.getLatitude() < 90) {
locLon = (int) (loc.getLongitude() * 1E6);
locLat = (int) (loc.getLatitude() * 1E6);
geoPoint = new GeoPoint(locLat, locLon);
mapView.getProjection().toPixels(geoPoint, pt);
canvas.save();
canvas.translate(pt.x - (markerWidth / 2), pt.y - markerHeight);
startMarker.draw(canvas);
canvas.restore();
break;
private void drawWaypoints(Canvas canvas, Projection projection) {
synchronized (waypoints) {;
for (Waypoint wpt : waypoints) {
Location loc = wpt.getLocation();
drawElement(canvas, projection, MyTracksUtils.getGeoPoint(loc),
wpt.getType() == Waypoint.TYPE_STATISTICS ? statsMarker
: waypointMarker, -(markerWidth / 2) + 3, -markerHeight);
}
}
}
@Override
public boolean draw(Canvas canvas, MapView mapView, boolean shadow,
long when) {
draw(canvas, mapView, shadow);
return false;
private void drawMyLocation(Canvas canvas, Projection projection) {
// Draw the arrow icon.
if (myLocation == null) {
return;
}
Point pt = drawElement(canvas, projection,
MyTracksUtils.getGeoPoint(myLocation), arrows[lastHeading],
-(arrowWidth / 2) + 3, -(arrowHeight / 2));
// Draw the error circle.
float radius = projection.metersToEquatorPixels(myLocation.getAccuracy());
canvas.drawCircle(pt.x, pt.y, radius, errorCirclePaint);
}
private void drawTrack(Canvas canvas, Projection projection, Rect viewRect) {
Path path;
synchronized (points) {
// Merge the pending points with the list of cached locations.
final GeoPoint referencePoint = projection.fromPixels(0, 0);
int newPoints = pendingPoints.drainTo(points);
boolean newProjection = !viewRect.equals(lastViewRect) ||
!referencePoint.equals(lastReferencePoint);
if (newPoints == 0 && lastPath != null && !newProjection) {
// No need to recreate path (same points and viewing area).
path = lastPath;
} else {
int numPoints = points.size();
if (numPoints < 2) {
// Not enough points to draw a path.
path = null;
} else if (lastPath != null && !newProjection) {
// Incremental update of the path, without repositioning the view.
path = lastPath;
updatePath(projection, viewRect, path, numPoints - newPoints);
} else {
// The view has changed so we have to start from scratch.
path = newPath();
path.incReserve(numPoints);
updatePath(projection, viewRect, path, 0);
}
lastPath = path;
}
lastReferencePoint = referencePoint;
lastViewRect = viewRect;
}
if (path != null) {
canvas.drawPath(path, selectedTrackPaint);
}
// Draw the "End" marker.
if (showEndMarker) {
for (int i = points.size() - 1; i >= 0; --i) {
if (points.get(i).valid) {
drawElement(canvas, projection, points.get(i).geoPoint, endMarker,
-markerWidth / 2, -markerHeight);
break;
}
}
}
// Draw the "Start" marker.
for (int i = 0; i < points.size(); ++i) {
if (points.get(i).valid) {
drawElement(canvas, projection, points.get(i).geoPoint, startMarker,
-markerWidth / 2, -markerHeight);
break;
}
}
}
private void updatePath(Projection projection, Rect viewRect, Path path,
int startLocationIdx) {
// Whether to start a new segment on new valid and visible point.
boolean newSegment =
startLocationIdx > 0 ? !points.get(startLocationIdx - 1).valid : true;
boolean lastVisible = !newSegment;
final Point pt = new Point();
// Loop over track points.
for (int i = startLocationIdx; i < points.size(); ++i) {
CachedLocation loc = points.get(i);
// Check if valid, if not then indicate a new segment.
if (!loc.valid) {
newSegment = true;
continue;
}
final GeoPoint geoPoint = loc.geoPoint;
// Check if this breaks the existing segment.
boolean visible = alwaysVisible || viewRect.contains(
geoPoint.getLongitudeE6(), geoPoint.getLatitudeE6());
if (!visible && !lastVisible) {
// This is a point outside view not connected to a visible one.
newSegment = true;
}
lastVisible = visible;
// Either move to beginning of a new segment or continue the old one.
projection.toPixels(geoPoint, pt);
if (newSegment) {
path.moveTo(pt.x, pt.y);
newSegment = false;
} else {
path.lineTo(pt.x, pt.y);
}
}
}
// Visible for testing.
Point drawElement(Canvas canvas, Projection projection, GeoPoint geoPoint,
Drawable element, int offsetX, int offsetY) {
Point pt = new Point();
projection.toPixels(geoPoint, pt);
canvas.save();
canvas.translate(pt.x + offsetX, pt.y + offsetY);
element.draw(canvas);
canvas.restore();
return pt;
}
/**
@@ -353,20 +426,22 @@ public class MyTracksOverlay extends Overlay {
final Location tapLocation = MyTracksUtils.getLocation(p);
double dmin = Double.MAX_VALUE;
Waypoint waypoint = null;
for (int i = 0; i < waypoints.size(); i++) {
final Location waypointLocation = waypoints.get(i).getLocation();
if (waypointLocation == null) {
continue;
}
final double d = waypointLocation.distanceTo(tapLocation);
if (d < dmin) {
dmin = d;
waypoint = waypoints.get(i);
synchronized (waypoints) {
for (int i = 0; i < waypoints.size(); i++) {
final Location waypointLocation = waypoints.get(i).getLocation();
if (waypointLocation == null) {
continue;
}
final double d = waypointLocation.distanceTo(tapLocation);
if (d < dmin) {
dmin = d;
waypoint = waypoints.get(i);
}
}
}
if (waypoint != null
&& dmin < 15000000 / Math.pow(2, mapView.getZoomLevel())) {
if (waypoint != null &&
dmin < 15000000 / Math.pow(2, mapView.getZoomLevel())) {
Intent intent = new Intent(context, MyTracksWaypointDetails.class);
intent.putExtra("waypointid", waypoint.getId());
context.startActivity(intent);
@@ -0,0 +1,57 @@
/*
* 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 static android.content.Intent.ACTION_BOOT_COMPLETED;
import static com.google.android.apps.mytracks.MyTracksConstants.RESUME_TRACK_EXTRA_NAME;
import static com.google.android.apps.mytracks.MyTracksConstants.TAG;
import com.google.android.apps.mytracks.services.TrackRecordingService;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.util.Log;
/**
* This class handles MyTracks related broadcast messages.
*
* One example of a broadcast message that this class is interested in,
* is notification about the phone boot. We may want to resume a previously
* started tracking session if the phone crashed (hopefully not), or the user
* decided to swap the battery or some external event occurred which forced
* a phone reboot.
*
* This class simply delegates to {@link TrackRecordingService} to make a
* decision whether to continue with the previous track (if any), or just
* abandon it.
*
* @author Bartlomiej Niechwiej
*/
public class MyTracksReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
Log.d(TAG, "MyTracksReceiver.onReceive: " + intent.getAction());
if (ACTION_BOOT_COMPLETED.equals(intent.getAction())) {
Intent startIntent = new Intent(context, TrackRecordingService.class);
startIntent.putExtra(RESUME_TRACK_EXTRA_NAME, true);
context.startService(startIntent);
} else {
Log.w(TAG, "MyTracksReceiver: unsupported action");
}
}
}
@@ -17,7 +17,8 @@ package com.google.android.apps.mytracks;
import com.google.android.apps.mytracks.io.backup.BackupActivityHelper;
import com.google.android.apps.mytracks.io.backup.BackupPreferencesListener;
import com.google.android.apps.mytracks.services.SafeStatusAnnouncerTask;
import com.google.android.apps.mytracks.services.StatusAnnouncerFactory;
import com.google.android.apps.mytracks.util.ApiFeatures;
import com.google.android.maps.mytracks.R;
import android.content.SharedPreferences;
@@ -25,12 +26,10 @@ import android.os.Bundle;
import android.preference.CheckBoxPreference;
import android.preference.ListPreference;
import android.preference.Preference;
import android.preference.Preference.OnPreferenceChangeListener;
import android.preference.Preference.OnPreferenceClickListener;
import android.preference.PreferenceActivity;
import android.preference.PreferenceManager;
import android.speech.tts.TextToSpeech;
import android.util.Log;
import android.preference.Preference.OnPreferenceChangeListener;
import android.preference.Preference.OnPreferenceClickListener;
/**
* An activity that let's the user see and edit the settings.
@@ -45,6 +44,7 @@ public class MyTracksSettings extends PreferenceActivity {
/*
* Default values - keep in sync with those in preferences.xml.
*/
public static final int DEFAULT_AUTO_RESUME_TRACK_TIMEOUT = 10; // In min.
public static final int DEFAULT_ANNOUNCEMENT_FREQUENCY = -1;
public static final int DEFAULT_MAX_RECORDING_DISTANCE = 200;
public static final int DEFAULT_MIN_RECORDING_DISTANCE = 5;
@@ -52,29 +52,19 @@ public class MyTracksSettings extends PreferenceActivity {
public static final int DEFAULT_MIN_REQUIRED_ACCURACY = 200;
public static final int DEFAULT_SPLIT_FREQUENCY = 0;
private static boolean mTTSAvailable;
private BackupPreferencesListener backupListener;
private SharedPreferences preferences;
/* establish whether the tts class is available to us */
static {
try {
SafeStatusAnnouncerTask.checkAvailable();
mTTSAvailable = true;
} catch (Throwable t) {
Log.d(MyTracksConstants.TAG, "TTS not available.", t);
mTTSAvailable = false;
}
}
/** Called when the activity is first created. */
@Override
public void onCreate(Bundle icicle) {
super.onCreate(icicle);
// The volume we want to control is the Text-To-Speech volume
setVolumeControlStream(TextToSpeech.Engine.DEFAULT_STREAM);
ApiFeatures apiFeatures = ApiFeatures.getInstance();
int volumeStream =
new StatusAnnouncerFactory(apiFeatures).getVolumeStream();
setVolumeControlStream(volumeStream);
// Tell it where to read/write preferences
PreferenceManager preferenceManager = getPreferenceManager();
@@ -82,7 +72,7 @@ public class MyTracksSettings extends PreferenceActivity {
preferenceManager.setSharedPreferencesMode(0);
// Set up automatic preferences backup
backupListener = BackupPreferencesListener.create(this);
backupListener = BackupPreferencesListener.create(this, apiFeatures);
preferences = preferenceManager.getSharedPreferences();
preferences.registerOnSharedPreferenceChangeListener(backupListener);
@@ -107,14 +97,14 @@ public class MyTracksSettings extends PreferenceActivity {
updatePreferenceUnits(metricUnitsPreference.isChecked());
// Disable TTS announcement preference if not available
if (!mTTSAvailable) {
if (!apiFeatures.hasTextToSpeech()) {
IntegerListPreference announcementFrequency =
(IntegerListPreference) findPreference(
getString(R.string.announcement_frequency_key));
announcementFrequency.setEnabled(false);
announcementFrequency.setValue("-1");
announcementFrequency.setSummary(
R.string.settings_announcement_not_available_summary);
R.string.settings_not_available_summary);
}
}
@@ -168,7 +158,7 @@ public class MyTracksSettings extends PreferenceActivity {
getPreferenceManager().getSharedPreferences()
.unregisterOnSharedPreferenceChangeListener(backupListener);
super.onPause();
super.onDestroy();
}
/**
@@ -190,7 +180,7 @@ public class MyTracksSettings extends PreferenceActivity {
final ListPreference splitFrequency =
(ListPreference) findPreference(
getString(R.string.split_frequency_key));
minRecordingDistance.setEntries(isMetric
? R.array.min_recording_distance_options
: R.array.min_recording_distance_options_ft);
@@ -135,7 +135,6 @@ public class MyTracksWaypointDetails extends Activity
iconId = R.drawable.ylw_pushpin;
TripStatistics waypointStats = waypoint.getStatistics();
utils.setAllStats(waypointStats);
utils.setTime(R.id.total_time_register, waypointStats.getTotalTime());
utils.setAltitude(
R.id.elevation_register, waypoint.getLocation().getAltitude());
break;
@@ -15,9 +15,8 @@
*/
package com.google.android.apps.mytracks;
import com.google.android.maps.mytracks.R;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.drawable.Drawable;
import android.os.Handler;
import android.view.MotionEvent;
@@ -50,21 +49,18 @@ public class NavControls {
/**
* A touchable image view.
* When touched it changes the navigation control icons accordingly.
*/
public class TouchLayout extends RelativeLayout {
private final ImageView arrow;
private class TouchLayout extends RelativeLayout implements Runnable {
private final boolean isLeft;
private final ImageView icon;
public TouchLayout(Context context, boolean isLeft) {
super(context);
arrow = new ImageView(context);
arrow.setImageDrawable(context.getResources().getDrawable(
isLeft ? R.drawable.btn_arrow_left : R.drawable.btn_arrow_right));
icon = new ImageView(context);
this.isLeft = isLeft;
this.icon = new ImageView(context);
icon.setVisibility(View.GONE);
addView(arrow);
addView(icon);
icon.setPadding((isLeft ? 15 : 10), 27, 15, 0);
}
public void setIcon(Drawable drawable) {
@@ -77,18 +73,11 @@ public class NavControls {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
setPressed(true);
hide();
if (this == prevImage) {
if (prevRunnable != null) {
handler.post(prevRunnable);
return true;
}
} else if (this == nextImage) {
if (nextRunnable != null) {
handler.post(nextRunnable);
return true;
}
}
shiftIcons(isLeft);
// Call the user back
handler.post(this);
break;
case MotionEvent.ACTION_UP:
setPressed(false);
@@ -96,55 +85,69 @@ public class NavControls {
}
return super.onTouchEvent(event);
}
@Override
public void run() {
touchRunnable.run();
setPressed(false);
}
}
private final Context context;
private final Runnable prevRunnable;
private final Runnable nextRunnable;
private final Handler handler = new Handler();
private final Runnable dismissControls = new Runnable() {
public void run() {
hide();
}
};
private final TouchLayout prevImage;
private final TouchLayout nextImage;
private final TypedArray leftIcons;
private final TypedArray rightIcons;
private final Runnable touchRunnable;
private boolean isVisible = false;
private boolean hasNext = true;
private boolean hasPrev = true;
private int currentIcons;
public NavControls(Context context, ViewGroup container,
Runnable prevRunnable, Runnable nextRunnable) {
this.context = context;
this.prevRunnable = prevRunnable;
this.nextRunnable = nextRunnable;
TypedArray leftIcons, TypedArray rightIcons,
Runnable touchRunnable) {
this.leftIcons = leftIcons;
this.rightIcons = rightIcons;
this.touchRunnable = touchRunnable;
if (leftIcons.length() != rightIcons.length() || leftIcons.length() < 1) {
throw new IllegalArgumentException("Invalid icons specified");
}
if (touchRunnable == null) {
throw new NullPointerException("Runnable cannot be null");
}
LayoutParams prevParams = new LayoutParams(
LayoutParams.WRAP_CONTENT,
LayoutParams.WRAP_CONTENT);
prevParams.addRule(RelativeLayout.ALIGN_PARENT_LEFT);
prevParams.addRule(RelativeLayout.CENTER_VERTICAL);
prevImage = new TouchLayout(context, true);
prevImage.setLayoutParams(prevParams);
prevImage.setVisibility(View.INVISIBLE);
container.addView(prevImage);
LayoutParams nextParams = new LayoutParams(
LayoutParams.WRAP_CONTENT,
LayoutParams.WRAP_CONTENT);
prevParams.addRule(RelativeLayout.ALIGN_PARENT_LEFT);
nextParams.addRule(RelativeLayout.ALIGN_PARENT_RIGHT);
prevParams.addRule(RelativeLayout.CENTER_VERTICAL);
nextParams.addRule(RelativeLayout.CENTER_VERTICAL);
nextImage = new TouchLayout(context, false);
prevImage = new TouchLayout(context, true);
nextImage.setLayoutParams(nextParams);
prevImage.setLayoutParams(prevParams);
nextImage.setVisibility(View.INVISIBLE);
prevImage.setVisibility(View.INVISIBLE);
container.addView(prevImage);
container.addView(nextImage);
}
public void setHasNext(boolean hasNext) {
this.hasNext = hasNext;
}
public void setHasPrev(boolean hasPrev) {
this.hasPrev = hasPrev;
prevImage.setIcon(leftIcons.getDrawable(0));
nextImage.setIcon(rightIcons.getDrawable(0));
this.currentIcons = 0;
}
private void keepVisible() {
@@ -156,20 +159,18 @@ public class NavControls {
public void show() {
if (!isVisible) {
if (prevRunnable != null && hasPrev) {
SHOW_PREV_ANIMATION.setDuration(500);
SHOW_PREV_ANIMATION.startNow();
prevImage.setPressed(false);
prevImage.setAnimation(SHOW_PREV_ANIMATION);
prevImage.setVisibility(View.VISIBLE);
}
if (nextRunnable != null && hasNext) {
SHOW_NEXT_ANIMATION.setDuration(500);
SHOW_NEXT_ANIMATION.startNow();
nextImage.setPressed(false);
nextImage.setAnimation(SHOW_NEXT_ANIMATION);
nextImage.setVisibility(View.VISIBLE);
}
SHOW_PREV_ANIMATION.setDuration(500);
SHOW_PREV_ANIMATION.startNow();
prevImage.setPressed(false);
prevImage.setAnimation(SHOW_PREV_ANIMATION);
prevImage.setVisibility(View.VISIBLE);
SHOW_NEXT_ANIMATION.setDuration(500);
SHOW_NEXT_ANIMATION.startNow();
nextImage.setPressed(false);
nextImage.setAnimation(SHOW_NEXT_ANIMATION);
nextImage.setVisibility(View.VISIBLE);
isVisible = true;
keepVisible();
} else {
@@ -188,33 +189,26 @@ public class NavControls {
public void hide() {
isVisible = false;
if (prevRunnable != null) {
if (hasPrev) {
prevImage.setAnimation(HIDE_PREV_ANIMATION);
HIDE_PREV_ANIMATION.setDuration(500);
HIDE_PREV_ANIMATION.startNow();
} else {
prevImage.clearAnimation();
}
prevImage.setVisibility(View.INVISIBLE);
}
if (nextRunnable != null) {
if (hasNext) {
nextImage.setAnimation(HIDE_NEXT_ANIMATION);
HIDE_NEXT_ANIMATION.setDuration(500);
HIDE_NEXT_ANIMATION.startNow();
} else {
nextImage.clearAnimation();
}
nextImage.setVisibility(View.INVISIBLE);
}
prevImage.setAnimation(HIDE_PREV_ANIMATION);
HIDE_PREV_ANIMATION.setDuration(500);
HIDE_PREV_ANIMATION.startNow();
prevImage.setVisibility(View.INVISIBLE);
nextImage.setAnimation(HIDE_NEXT_ANIMATION);
HIDE_NEXT_ANIMATION.setDuration(500);
HIDE_NEXT_ANIMATION.startNow();
nextImage.setVisibility(View.INVISIBLE);
}
public void setLeftIcon(int resourceId) {
prevImage.setIcon(context.getResources().getDrawable(resourceId));
public int getCurrentIcons() {
return currentIcons;
}
public void setRightIcon(int resourceId) {
nextImage.setIcon(context.getResources().getDrawable(resourceId));
private void shiftIcons(boolean isLeft) {
// Increment or decrement by one, with wrap around
currentIcons = (currentIcons + leftIcons.length() +
(isLeft ? -1 : 1)) % leftIcons.length();
prevImage.setIcon(leftIcons.getDrawable(currentIcons));
nextImage.setIcon(rightIcons.getDrawable(currentIcons));
}
}
@@ -23,7 +23,8 @@ package com.google.android.apps.mytracks;
*/
public interface ProgressIndicator {
public void setProgressMessage(String text);
public void setProgressMessage(int resId);
public void clearProgressMessage();
public void setProgressValue(int percent);
@@ -61,7 +61,7 @@ public class SendToGoogleDialog extends Dialog {
Button cancel = (Button) findViewById(R.id.sendtogoogle_cancel);
cancel.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
MyTracks.getInstance().dismissDialog(MyTracks.DIALOG_SEND_TO_GOOGLE);
MyTracks.getInstance().dismissDialog(DialogManager.DIALOG_SEND_TO_GOOGLE);
}
});
@@ -20,6 +20,8 @@ import com.google.android.apps.mytracks.content.MyTracksProviderUtilsImpl;
import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.content.TracksColumns;
import com.google.android.apps.mytracks.content.WaypointsColumns;
import com.google.android.apps.mytracks.services.StatusAnnouncerFactory;
import com.google.android.apps.mytracks.util.ApiFeatures;
import com.google.android.maps.mytracks.R;
import android.app.Activity;
@@ -33,7 +35,6 @@ import android.location.LocationManager;
import android.location.LocationProvider;
import android.os.Bundle;
import android.os.Handler;
import android.speech.tts.TextToSpeech;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.Menu;
@@ -49,14 +50,12 @@ import android.widget.Toast;
* @author Sandor Dornbush
*/
public class StatsActivity extends Activity
implements OnSharedPreferenceChangeListener, LocationListener {
implements OnSharedPreferenceChangeListener {
private final StatsUtilities utils;
private StatsUtilities utils;
private UIUpdateThread thread;
private ContentObserver observer;
private LocationManager locationManager;
/**
* The id of the currently selected track.
@@ -95,8 +94,7 @@ public class StatsActivity extends Activity
*/
private boolean showCurrentSegment = false;
private final MyTracksProviderUtils providerUtils;
private Track track = null;
private MyTracksProviderUtils providerUtils;
/**
* A runnable for posting to the UI thread. Will update the total time field.
@@ -135,18 +133,18 @@ public class StatsActivity extends Activity
}
}
public StatsActivity() {
utils = new StatsUtilities(this);
providerUtils = new MyTracksProviderUtilsImpl(this);
}
/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
utils = new StatsUtilities(this);
providerUtils = new MyTracksProviderUtilsImpl(getContentResolver());
// The volume we want to control is the Text-To-Speech volume
setVolumeControlStream(TextToSpeech.Engine.DEFAULT_STREAM);
int volumeStream =
new StatusAnnouncerFactory(ApiFeatures.getInstance()).getVolumeStream();
setVolumeControlStream(volumeStream);
// We don't need a window title bar:
requestWindowFeature(Window.FEATURE_NO_TITLE);
@@ -179,11 +177,9 @@ public class StatsActivity extends Activity
preferences.getBoolean(getString(R.string.report_speed_key), true);
checkLiveTrack();
restoreStats();
updateLocation(null);
showUnknownLocation();
preferences.registerOnSharedPreferenceChangeListener(this);
}
locationManager =
(LocationManager) getSystemService(Context.LOCATION_SERVICE);
utils.setMetricUnits(metricUnits);
utils.setReportSpeed(displaySpeed);
utils.updateUnits();
@@ -214,7 +210,7 @@ public class StatsActivity extends Activity
activityOnTop = true;
checkLiveTrack();
restoreStats();
updateLocation(null);
showUnknownLocation();
super.onResume();
}
@@ -234,7 +230,7 @@ public class StatsActivity extends Activity
-1);
checkLiveTrack();
restoreStats();
updateLocation(null);
showUnknownLocation();
} else if (key.equals(getString(R.string.recording_track_key))) {
recordingTrackId =
sharedPreferences.getLong(
@@ -242,7 +238,7 @@ public class StatsActivity extends Activity
-1);
checkLiveTrack();
restoreStats();
updateLocation(null);
showUnknownLocation();
} else if (key.equals(getString(R.string.metric_units_key))) {
metricUnits =
sharedPreferences.getBoolean(
@@ -299,29 +295,36 @@ public class StatsActivity extends Activity
return super.onOptionsItemSelected(item);
}
@Override
public void onLocationChanged(Location l) {
if (selectedTrackIsRecording()) {
updateLocation(l);
private final LocationListener locationListener = new LocationListener() {
@Override
public void onLocationChanged(Location l) {
if (selectedTrackIsRecording()) {
showLocation(l);
}
}
}
@Override
public void onProviderDisabled(String provider) {
}
@Override
public void onProviderEnabled(String provider) {
}
@Override
public void onStatusChanged(String provider, int status, Bundle extras) {
}
@Override
public void onProviderDisabled(String provider) {
// Do nothing
}
@Override
public void onProviderEnabled(String provider) {
// Do nothing
}
@Override
public void onStatusChanged(String provider, int status, Bundle extras) {
// Do nothing
}
};
/**
* Registers to receive location updates from the GPS location provider.
*/
private void registerLocationListener() {
LocationManager locationManager =
(LocationManager) getSystemService(Context.LOCATION_SERVICE);
if (locationManager != null) {
LocationProvider gpsProvider =
locationManager.getProvider(MyTracksConstants.GPS_PROVIDER);
@@ -334,7 +337,7 @@ public class StatsActivity extends Activity
+ gpsProvider.getName());
}
locationManager.requestLocationUpdates(gpsProvider.getName(),
0/*minTime*/, 0/*minDist*/, this);
0/*minTime*/, 0/*minDist*/, locationListener);
}
}
@@ -342,8 +345,10 @@ public class StatsActivity extends Activity
* Unregisters all location listener.
*/
private void unregisterLocationListener() {
LocationManager locationManager =
(LocationManager) getSystemService(Context.LOCATION_SERVICE);
if (locationManager != null) {
locationManager.removeUpdates(this);
locationManager.removeUpdates(locationListener);
}
}
@@ -364,7 +369,7 @@ public class StatsActivity extends Activity
return;
}
track = providerUtils.getTrack(selectedTrackId);
Track track = providerUtils.getTrack(selectedTrackId);
if (track == null || track.getStatistics() == null) {
utils.setAllToUnknown();
return;
@@ -421,17 +426,17 @@ public class StatsActivity extends Activity
*
* @param l may be null (will set location fields to unknown)
*/
private void updateLocation(Location l) {
if (l == null) {
utils.setUnknown(R.id.elevation_register);
utils.setUnknown(R.id.latitude_register);
utils.setUnknown(R.id.longitude_register);
utils.setUnknown(R.id.speed_register);
} else {
utils.setAltitude(R.id.elevation_register, l.getAltitude());
utils.setLatLong(R.id.latitude_register, l.getLatitude());
utils.setLatLong(R.id.longitude_register, l.getLongitude());
utils.setSpeed(R.id.speed_register, l.getSpeed() * 3.6);
}
private void showLocation(Location l) {
utils.setAltitude(R.id.elevation_register, l.getAltitude());
utils.setLatLong(R.id.latitude_register, l.getLatitude());
utils.setLatLong(R.id.longitude_register, l.getLongitude());
utils.setSpeed(R.id.speed_register, l.getSpeed() * 3.6);
}
private void showUnknownLocation() {
utils.setUnknown(R.id.elevation_register);
utils.setUnknown(R.id.latitude_register);
utils.setUnknown(R.id.longitude_register);
utils.setUnknown(R.id.speed_register);
}
}
@@ -20,13 +20,13 @@ import com.google.android.apps.mytracks.util.StringUtils;
import com.google.android.apps.mytracks.util.UnitConversions;
import com.google.android.maps.mytracks.R;
import java.text.DecimalFormat;
import java.text.NumberFormat;
import android.app.Activity;
import android.util.Log;
import android.widget.TextView;
import java.text.DecimalFormat;
import java.text.NumberFormat;
/**
* Various utility functions for views that display statistics information.
*
@@ -218,7 +218,7 @@ public class StatsUtilities {
setGrade(R.id.max_grade_register, maxGrade);
}
public void setAllStats(TripStatistics stats) {
public void setAllStats(TripStatistics stats) {
setTime(R.id.moving_time_register, stats.getMovingTime());
setDistance(R.id.total_distance_register, stats.getTotalDistance() / 1000);
setSpeed(R.id.average_speed_register, stats.getAverageSpeed() * 3.6);
@@ -230,6 +230,8 @@ public class StatsUtilities {
setAltitude(R.id.elevation_gain_register, stats.getTotalElevationGain());
setGrade(R.id.min_grade_register, stats.getMinGrade());
setGrade(R.id.max_grade_register, stats.getMaxGrade());
setTime(R.id.total_time_register, stats.getTotalTime());
}
public void setSpeedLabel(int id, int speedString, int paceString) {
@@ -21,6 +21,8 @@ import android.database.Cursor;
import android.location.Location;
import android.net.Uri;
import java.util.List;
/**
* Utility to access data from the mytracks content provider.
*
@@ -173,7 +175,7 @@ public interface MyTracksProviderUtils {
* @return A cursor over the selected range of locations
*/
Cursor getWaypointsCursor(long trackId, long minWaypointId,
long maxWaypoints);
int maxWaypoints);
/**
* Finds a track by given unique track id.
@@ -184,6 +186,15 @@ public interface MyTracksProviderUtils {
* @return a Track object, or null if not found
*/
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.
*
* @return a list of all the recorded tracks
*/
List<Track> getAllTracks();
/**
* Loads the track points for a given track.
@@ -195,17 +206,6 @@ public interface MyTracksProviderUtils {
*/
long getTrackPoints(Track track, int maxPoints);
/**
* Fetches some number of locations for the given track.
*
* This is designed to be used to stream through large tracks without loading
* all points into memory.
*
* @param track to load locations for
* @param buffer an array of locations to fill
*/
void getTrackPoints(Track track, TrackBuffer buffer);
/**
* Creates a cursor over the tracks provider with a given selection.
* Caller gets to own the returned cursor. Don't forget to close it.
@@ -294,6 +294,14 @@ public interface MyTracksProviderUtils {
*/
Location createLocation(Cursor cursor);
/**
* Fill a location object with values from a given cursor.
*
* @param cursor a cursor pointing at a db or provider with locations
* @param location a location object to be overwritten
*/
void fillLocation(Cursor cursor, Location location);
/**
* Creates a waypoint object from a given cursor.
*
@@ -337,7 +345,7 @@ public interface MyTracksProviderUtils {
* Creates an instance of {@link MyTracksProviderUtils}.
*/
protected MyTracksProviderUtils newForContext(Context context) {
return new MyTracksProviderUtilsImpl(context);
return new MyTracksProviderUtilsImpl(context.getContentResolver());
}
}
}
@@ -17,13 +17,16 @@ package com.google.android.apps.mytracks.content;
import com.google.android.apps.mytracks.stats.TripStatistics;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.location.Location;
import android.net.Uri;
import android.util.Log;
import java.util.ArrayList;
import java.util.List;
/**
* Helper class providing easy access to locations and tracks in the
* MyTracksProvider. All static members.
@@ -32,10 +35,10 @@ import android.util.Log;
*/
public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils {
private final Context context;
private final ContentResolver contentResolver;
public MyTracksProviderUtilsImpl(Context context) {
this.context = context;
public MyTracksProviderUtilsImpl(ContentResolver contentResolver) {
this.contentResolver = contentResolver;
}
/**
@@ -168,6 +171,15 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils {
@Override
public Location createLocation(Cursor cursor) {
Location location = new Location("");
fillLocation(cursor, location);
return location;
}
@Override
public void fillLocation(Cursor cursor, Location location) {
location.reset();
int idxLatitude = cursor.getColumnIndexOrThrow(TrackPointsColumns.LATITUDE);
int idxLongitude =
cursor.getColumnIndexOrThrow(TrackPointsColumns.LONGITUDE);
@@ -177,7 +189,6 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils {
int idxAccuracy = cursor.getColumnIndexOrThrow(TrackPointsColumns.ACCURACY);
int idxSpeed = cursor.getColumnIndexOrThrow(TrackPointsColumns.SPEED);
Location location = new Location("");
if (!cursor.isNull(idxLatitude)) {
location.setLatitude(1. * cursor.getInt(idxLatitude) / 1E6);
}
@@ -199,7 +210,6 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils {
if (!cursor.isNull(idxAccuracy)) {
location.setAccuracy(cursor.getFloat(idxAccuracy));
}
return location;
}
@Override
@@ -438,25 +448,22 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils {
@Override
public void deleteAllTracks() {
context.getContentResolver().delete(TracksColumns.CONTENT_URI, null, null);
context.getContentResolver().delete(TrackPointsColumns.CONTENT_URI,
null, null);
context.getContentResolver().delete(
WaypointsColumns.CONTENT_URI, null, null);
contentResolver.delete(TracksColumns.CONTENT_URI, null, null);
contentResolver.delete(TrackPointsColumns.CONTENT_URI, null, null);
contentResolver.delete(WaypointsColumns.CONTENT_URI, null, null);
}
@Override
public void deleteTrack(long trackId) {
Track track = getTrack(trackId);
if (track != null) {
context.getContentResolver().delete(TrackPointsColumns.CONTENT_URI,
contentResolver.delete(TrackPointsColumns.CONTENT_URI,
"_id>=" + track.getStartId() + " AND _id<=" + track.getStopId(),
null);
}
context.getContentResolver().delete(WaypointsColumns.CONTENT_URI,
contentResolver.delete(WaypointsColumns.CONTENT_URI,
WaypointsColumns.TRACKID + "=" + trackId, null);
context.getContentResolver().delete(
TracksColumns.CONTENT_URI, "_id=" + trackId, null);
contentResolver.delete(TracksColumns.CONTENT_URI, "_id=" + trackId, null);
}
@Override
@@ -481,7 +488,7 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils {
"No statistics marker after the deleted one was found.");
}
}
context.getContentResolver().delete(
contentResolver.delete(
WaypointsColumns.CONTENT_URI, "_id=" + waypointId, null);
}
@@ -493,7 +500,7 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils {
final String sortOrder = WaypointsColumns._ID + " LIMIT 1";
Cursor cursor = null;
try {
cursor = context.getContentResolver().query(
cursor = contentResolver.query(
WaypointsColumns.CONTENT_URI,
null /*projection*/,
selection,
@@ -515,7 +522,7 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils {
@Override
public boolean updateWaypoint(Waypoint waypoint) {
try {
final int rows = context.getContentResolver().update(
final int rows = contentResolver.update(
WaypointsColumns.CONTENT_URI,
createContentValues(waypoint),
"_id=" + waypoint.getId(),
@@ -536,7 +543,7 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils {
private Location findLocationBy(String select) {
Cursor cursor = null;
try {
cursor = context.getContentResolver().query(
cursor = contentResolver.query(
TrackPointsColumns.CONTENT_URI, null, select, null, null);
if (cursor != null && cursor.moveToNext()) {
return createLocation(cursor);
@@ -560,7 +567,7 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils {
private Track findTrackBy(String select) {
Cursor cursor = null;
try {
cursor = context.getContentResolver().query(
cursor = contentResolver.query(
TracksColumns.CONTENT_URI, null, select, null, null);
if (cursor != null && cursor.moveToNext()) {
return createTrack(cursor);
@@ -582,7 +589,7 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils {
@Override
public Waypoint getFirstWaypoint(long trackId) {
Cursor cursor = context.getContentResolver().query(
Cursor cursor = contentResolver.query(
WaypointsColumns.CONTENT_URI,
null /*projection*/,
"trackid=" + trackId,
@@ -604,7 +611,7 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils {
@Override
public Waypoint getWaypoint(long waypointId) {
Cursor cursor = context.getContentResolver().query(
Cursor cursor = contentResolver.query(
WaypointsColumns.CONTENT_URI,
null /*projection*/,
"_id=" + waypointId,
@@ -627,7 +634,7 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils {
@Override
public long getLastLocationId(long trackId) {
final String[] projection = {"_id"};
Cursor cursor = context.getContentResolver().query(
Cursor cursor = contentResolver.query(
TrackPointsColumns.CONTENT_URI,
projection,
"_id=(select max(_id) from trackpoints WHERE trackid=" + trackId + ")",
@@ -651,7 +658,7 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils {
@Override
public long getFirstWaypointId(long trackId) {
final String[] projection = {"_id"};
Cursor cursor = context.getContentResolver().query(
Cursor cursor = contentResolver.query(
WaypointsColumns.CONTENT_URI,
projection,
"trackid=" + trackId,
@@ -675,7 +682,7 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils {
@Override
public long getLastWaypointId(long trackId) {
final String[] projection = {"_id"};
Cursor cursor = context.getContentResolver().query(
Cursor cursor = contentResolver.query(
WaypointsColumns.CONTENT_URI,
projection,
WaypointsColumns.TRACKID + "=" + trackId,
@@ -700,7 +707,7 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils {
public Track getLastTrack() {
Cursor cursor = null;
try {
cursor = context.getContentResolver().query(
cursor = contentResolver.query(
TracksColumns.CONTENT_URI, null, "_id=(select max(_id) from tracks)",
null, null);
if (cursor != null && cursor.moveToNext()) {
@@ -719,7 +726,7 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils {
@Override
public long getLastTrackId() {
String[] proj = { TracksColumns._ID };
Cursor cursor = context.getContentResolver().query(
Cursor cursor = contentResolver.query(
TracksColumns.CONTENT_URI, proj, "_id=(select max(_id) from tracks)",
null, null);
if (cursor != null) {
@@ -759,13 +766,13 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils {
sortOrder += " LIMIT " + maxLocations;
}
return context.getContentResolver().query(
return contentResolver.query(
TrackPointsColumns.CONTENT_URI, null, selection, null, sortOrder);
}
@Override
public Cursor getWaypointsCursor(long trackId, long minWaypointId,
long maxWaypoints) {
int maxWaypoints) {
String selection;
if (minWaypointId > 0) {
selection = String.format("%s=%d AND %s>=%d",
@@ -781,7 +788,7 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils {
sortOrder += " LIMIT " + maxWaypoints;
}
return context.getContentResolver().query(
return contentResolver.query(
WaypointsColumns.CONTENT_URI, null, selection, null, sortOrder);
}
@@ -790,6 +797,21 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils {
String select = TracksColumns._ID + "=" + id;
return findTrackBy(select);
}
@Override
public List<Track> getAllTracks() {
Cursor cursor = getTracksCursor(null);
if (cursor == null || !cursor.moveToFirst()) {
return new ArrayList<Track>();
}
List<Track> tracks = new ArrayList<Track>(cursor.getCount());
do {
tracks.add(createTrack(cursor));
} while(cursor.moveToNext());
return tracks;
}
@Override
public long getTrackPoints(Track track, int maxPoints) {
@@ -820,56 +842,9 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils {
return lastId;
}
@Override
public void getTrackPoints(Track track, TrackBuffer buffer) {
long startingPoint = buffer.getLastLocationRead() == 0 ? track.getStartId()
: buffer.getLastLocationRead();
buffer.reset();
Cursor cursor = getLocationsCursor(track.getId(),
startingPoint,
buffer.getSize(), false);
if (cursor == null) {
Log.w(MyTracksProvider.TAG, "Cannot get a locations cursor!");
buffer.setInvalid();
return;
}
try {
if (cursor.getCount() == 0) {
Log.w(MyTracksProvider.TAG, "No matching locations found.");
buffer.resetAt(startingPoint + buffer.getSize());
return;
}
if (!cursor.moveToFirst()) {
Log.w(MyTracksProvider.TAG, "Could not move to first.");
buffer.setInvalid();
return;
}
final int idColumnIdx =
cursor.getColumnIndexOrThrow(TrackPointsColumns._ID);
do {
Location location = createLocation(cursor);
if (location == null) {
continue;
}
buffer.add(location, cursor.getLong(idColumnIdx));
} while (cursor.moveToNext());
if (buffer.getLocationsLoaded() == 0) {
Log.w(MyTracksProvider.TAG, "No locations read.");
buffer.resetAt(startingPoint + buffer.getSize());
}
} catch (RuntimeException e) {
Log.w(MyTracksProvider.TAG, "Caught unexpected exception.", e);
} finally {
cursor.close();
}
}
@Override
public Cursor getTracksCursor(String selection) {
Cursor cursor = context.getContentResolver().query(
Cursor cursor = contentResolver.query(
TracksColumns.CONTENT_URI, null, selection, null, "_id");
return cursor;
}
@@ -877,14 +852,14 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils {
@Override
public Uri insertTrack(Track track) {
Log.d(MyTracksProvider.TAG, "MyTracksProviderUtilsImpl.insertTrack");
return context.getContentResolver().insert(TracksColumns.CONTENT_URI,
return contentResolver.insert(TracksColumns.CONTENT_URI,
createContentValues(track));
}
@Override
public Uri insertTrackPoint(Location location, long trackId) {
Log.d(MyTracksProvider.TAG, "MyTracksProviderUtilsImpl.insertTrackPoint");
return context.getContentResolver().insert(TrackPointsColumns.CONTENT_URI,
return contentResolver.insert(TrackPointsColumns.CONTENT_URI,
createContentValues(location, trackId));
}
@@ -897,14 +872,14 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils {
values[i] = createContentValues(locations[i], trackId);
}
return context.getContentResolver().bulkInsert(TrackPointsColumns.CONTENT_URI, values);
return contentResolver.bulkInsert(TrackPointsColumns.CONTENT_URI, values);
}
@Override
public Uri insertWaypoint(Waypoint waypoint) {
Log.d(MyTracksProvider.TAG, "MyTracksProviderUtilsImpl.insertWaypoint");
waypoint.setId(-1);
return context.getContentResolver().insert(WaypointsColumns.CONTENT_URI,
return contentResolver.insert(WaypointsColumns.CONTENT_URI,
createContentValues(waypoint));
}
@@ -913,7 +888,7 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils {
Cursor cursor = null;
try {
final String[] projection = { TracksColumns._ID };
cursor = context.getContentResolver().query(
cursor = contentResolver.query(
TracksColumns.CONTENT_URI,
projection,
TracksColumns._ID + "=" + id/*selection*/,
@@ -933,7 +908,7 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils {
@Override
public void updateTrack(Track track) {
Log.d(MyTracksProvider.TAG, "MyTracksProviderUtilsImpl.updateTrack");
context.getContentResolver().update(TracksColumns.CONTENT_URI,
contentResolver.update(TracksColumns.CONTENT_URI,
createContentValues(track), "_id=" + track.getId(), null);
}
}
@@ -1,133 +0,0 @@
/*
* 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.content;
import com.google.android.apps.mytracks.util.MyTracksUtils;
import android.location.Location;
/**
* A buffer of Locations from a track.
* This class contains some additional information about the loaded points.
*
* @author Sandor Dornbush
*/
public class TrackBuffer {
/**
* An array of location objects that are part of a track.
*/
private final Location[] buffer;
/**
* The id of the last location read from the track.
*/
private long lastLocationRead = 0;
/**
* The number of locations loaded into the buffer.
*/
private int locationsLoaded = 0;
/**
* Create a TrackBuffer with size elements.
*
* @param size The size of the buffer
*/
public TrackBuffer(int size) {
buffer = new Location[size];
}
/**
* Reset the track to a state with no locations.
*/
public void reset() {
resetAt(0);
}
/**
* Reset the track at the given starting location id.
*/
public void resetAt(long lastLocation) {
lastLocationRead = lastLocation;
locationsLoaded = 0;
}
public void setInvalid() {
lastLocationRead = Integer.MAX_VALUE;
}
/**
* @return The number of locations that can be stored in this buffer
*/
public int getSize() {
return buffer.length;
}
/**
* @param index The index of the location to fetch
* @return The location for the given index
*/
public Location get(int index) {
return buffer[index];
}
/**
* Adds a location to the end of the buffer.
* @param location The location to add.
* @param id The id of the location to be added.
*/
public void add(Location location, long id) {
buffer[locationsLoaded++] = location;
lastLocationRead = Math.max(lastLocationRead, id);
}
/**
* @return The id of the last location loaded into the buffer
*/
public long getLastLocationRead() {
return lastLocationRead;
}
/**
* @return The number of locations loaded into the buffer.
*/
public int getLocationsLoaded() {
return locationsLoaded;
}
/**
* Finds the start location, i.e. the one which is the first point of a
* segment with at least two points.
*
* @return the start location
*/
public Location findStartLocation() {
int numValidLocations = 0;
for (int i = 0; i < getLocationsLoaded(); i++) {
Location location = buffer[i];
if (MyTracksUtils.isValidLocation(location)) {
numValidLocations++;
if (numValidLocations == 2) {
return buffer[i - 1];
}
} else {
numValidLocations = 0;
}
}
return null;
}
}
@@ -468,8 +468,7 @@ public class GpxImporter extends DefaultHandler {
// initialize start time with time of first track point
if (statsBuilder == null) {
statsBuilder = new TripStatisticsBuilder();
statsBuilder.resumeAt(time);
statsBuilder = new TripStatisticsBuilder(time);
}
}
@@ -209,7 +209,7 @@ public class SendToDocs {
androidClient = GDataClientFactory.getGDataClient(activity);
SpreadsheetsClient gdataClient = new SpreadsheetsClient(androidClient,
new XmlMapsGDataParserFactory(new AndroidXmlParserFactory()));
wiseWrapper.setClient(androidClient, gdataClient);
wiseWrapper.setClient(gdataClient);
Log.d(MyTracksConstants.TAG,
"GData connection prepared: " + this.writelyAuth);
String sheetTitle = "My Tracks";
@@ -220,7 +220,7 @@ public class SendToDocs {
DocumentsClient docsGdataClient = new DocumentsClient(androidClient,
new XmlMapsGDataParserFactory(new AndroidXmlParserFactory()));
writelyWrapper.setClient(androidClient, docsGdataClient);
writelyWrapper.setClient(docsGdataClient);
// First try to find the spreadsheet:
if (!getSpreadsheetId(writelyWrapper, sheetTitle)) {
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,73 @@
/*
* Copyright 2010 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.io;
import android.os.Environment;
import java.io.File;
/**
* A class to clean up old temporary files.
* @author Sandor Dornbush
*/
public class TempFileCleaner {
private long currentTimeMillis;
public static void clean() {
(new TempFileCleaner(System.currentTimeMillis())).cleanImpl();
}
// @VisibleForTesting
TempFileCleaner(long time) {
currentTimeMillis = time;
}
private void cleanImpl() {
if (!Environment.getExternalStorageState().equals(
Environment.MEDIA_MOUNTED)) {
return; // Can't do anything now.
}
cleanTmpDirectory("csv");
cleanTmpDirectory("gpx");
cleanTmpDirectory("kml");
cleanTmpDirectory("tcx");
}
private void cleanTmpDirectory(String name) {
String sep = System.getProperty("file.separator");
cleanTmpDirectory(
new File(
Environment.getExternalStorageDirectory() + sep + name + sep
+ "tmp"));
}
// @VisibleForTesting
int cleanTmpDirectory(File dir) {
if (!dir.exists()) {
return 0;
}
int count = 0;
long oldest = currentTimeMillis - 1000 * 3600;
for (File f : dir.listFiles()) {
if (f.lastModified() < oldest) {
f.delete();
count++;
}
}
return count;
}
}
@@ -18,7 +18,6 @@ package com.google.android.apps.mytracks.io;
import com.google.android.apps.mytracks.MyTracksConstants;
import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.content.TrackBuffer;
import com.google.android.apps.mytracks.content.Waypoint;
import com.google.android.apps.mytracks.util.FileUtils;
import com.google.android.apps.mytracks.util.MyTracksUtils;
@@ -254,55 +253,8 @@ public class TrackWriter {
void writeDocument() {
Log.d(MyTracksConstants.TAG, "Started writing track.");
writer.writeHeader();
TrackBuffer buffer = new TrackBuffer(1024);
Location last = null;
boolean wroteFirst = false;
boolean segmentOpen = false;
int nValidLocations = 0;
// Fetch small pieces of the track.
while (buffer.getLastLocationRead() < track.getStopId()) {
Log.d(MyTracksConstants.TAG,
"Reading track points starting at: " + buffer.getLastLocationRead());
providerUtils.getTrackPoints(track, buffer);
if (!wroteFirst) {
Location first = buffer.findStartLocation();
writer.writeBeginTrack(first);
wroteFirst = true;
}
Log.d(MyTracksConstants.TAG,
"Reading " + buffer.getLocationsLoaded()
+ " Ending at: " + buffer.getLastLocationRead());
for (int i = 0; i < buffer.getLocationsLoaded(); i++) {
Location location = buffer.get(i);
if (MyTracksUtils.isValidLocation(location)) {
nValidLocations++;
if (!segmentOpen) {
writer.writeOpenSegment();
segmentOpen = true;
}
writer.writeLocation(location);
if (nValidLocations >= 2) {
last = location;
}
} else {
nValidLocations = 0;
last = null;
if (segmentOpen) {
writer.writeCloseSegment();
segmentOpen = false;
}
}
}
}
if (segmentOpen) {
writer.writeCloseSegment();
segmentOpen = false;
}
if (wroteFirst) {
writer.writeEndTrack(last);
}
// TODO: Fix ordering (in GPX waypoints should come first)
writeLocations();
writeWaypoints(track.getId());
writer.writeFooter();
writer.close();
@@ -310,4 +262,66 @@ public class TrackWriter {
Log.d(MyTracksConstants.TAG, "Done writing track.");
errorMessage = R.string.io_write_finished;
}
private void writeLocations() {
boolean wroteFirst = false;
boolean segmentOpen = false;
Location lastLoc = null, loc = null;
boolean isLastValid = false;
Cursor locationsCursor =
providerUtils.getLocationsCursor(track.getId(), 0, -1, false);
if (locationsCursor == null || !locationsCursor.moveToFirst()) {
Log.w(MyTracksConstants.TAG, "Unable to get any points to write");
return;
}
do {
if (loc == null) loc = new Location("");
providerUtils.fillLocation(locationsCursor, loc);
boolean isValid = MyTracksUtils.isValidLocation(loc);
boolean validSegment = isValid && isLastValid;
if (!wroteFirst && validSegment) {
// Found the first two consecutive points which are valid
writer.writeBeginTrack(lastLoc);
wroteFirst = true;
}
if (validSegment) {
if (!segmentOpen) {
// Start a segment for this point
writer.writeOpenSegment();
segmentOpen = true;
// Write the previous point, which we had previously skipped
writer.writeLocation(lastLoc);
}
// Write the current point
writer.writeLocation(loc);
} else {
if (segmentOpen) {
writer.writeCloseSegment();
segmentOpen = false;
}
}
// Swap loc and lastLoc (so lastLoc is reused)
Location tmp = lastLoc;
lastLoc = loc;
loc = tmp;
if (loc != null) loc.reset();
isLastValid = isValid;
} while (locationsCursor.moveToNext());
if (segmentOpen) {
writer.writeCloseSegment();
segmentOpen = false;
}
if (wroteFirst) {
writer.writeEndTrack(lastLoc);
}
}
}
@@ -102,7 +102,7 @@ public class TrackWriterFactory {
* @param format the output format to write in
* @return the new track writer
*/
public static TrackWriter newWriter(Context context,
private static TrackWriter newWriter(Context context,
MyTracksProviderUtils providerUtils,
Track track, TrackFileFormat format) {
TrackFormatWriter writer = format.newFormatWriter(context);
@@ -15,7 +15,7 @@
*/
package com.google.android.apps.mytracks.io.backup;
import com.google.android.apps.mytracks.MyTracksConstants;
import com.google.android.apps.mytracks.util.ApiFeatures;
import android.app.backup.BackupManager;
import android.content.Context;
@@ -66,8 +66,9 @@ public abstract class BackupPreferencesListener
/**
* Creates and returns a proper instance of the listener for this device.
*/
public static BackupPreferencesListener create(Context context) {
if (MyTracksConstants.ANDROID_API_LEVEL >= 8) {
public static BackupPreferencesListener create(
Context context, ApiFeatures apiFeatures) {
if (apiFeatures.hasBackup()) {
return new BackupPreferencesListenerImpl(context);
} else {
return new DummyBackupPreferencesListener();
@@ -17,7 +17,6 @@ package com.google.android.apps.mytracks.io.gdata;
import com.google.android.apps.mytracks.MyTracksConstants;
import com.google.android.apps.mytracks.io.AuthManager;
import com.google.wireless.gdata.client.GDataClient;
import com.google.wireless.gdata.client.GDataServiceClient;
import com.google.wireless.gdata.client.HttpException;
import com.google.wireless.gdata.parser.ParseException;
@@ -28,6 +27,10 @@ import android.util.Log;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
/**
@@ -83,44 +86,64 @@ public class GDataWrapper {
public static final int ERROR_CLEANED_UP = 7;
// An unknown error occurred.
public static final int ERROR_UNKNOWN = 100;
private static final int AUTH_TOKEN_INVALIDATE_REFRESH_NUM_RETRIES = 1;
private static final int AUTH_TOKEN_INVALIDATE_REFRESH_TIMEOUT = 5000;
private String errorMessage;
private int errorType;
private GDataClient androidGDataClient;
private GDataServiceClient gdataServiceClient;
private AuthManager auth;
private boolean retryOnAuthFailure;
private int retriesPending;
private boolean cleanupCalled;
public GDataWrapper() {
errorType = ERROR_NO_ERROR;
errorMessage = null;
auth = null;
retryOnAuthFailure = false;
retriesPending = 0;
cleanupCalled = false;
}
public void setClient(GDataClient androidGDataClient,
GDataServiceClient gdataServiceClient) {
this.androidGDataClient = androidGDataClient;
public void setClient(GDataServiceClient gdataServiceClient) {
this.gdataServiceClient = gdataServiceClient;
}
public boolean runAuthenticatedFunction(
final AuthenticatedFunction function) {
return runOne(function, null);
return runCommon(function, null);
}
public boolean runQuery(final QueryFunction query) {
return runOne(null, query);
return runCommon(null, query);
}
/**
* Runs an arbitrary piece of code.
*/
private boolean runOne(final AuthenticatedFunction function,
private boolean runCommon(final AuthenticatedFunction function,
final QueryFunction query) {
for (int i = 0; i <= AUTH_TOKEN_INVALIDATE_REFRESH_NUM_RETRIES; i++) {
runOne(function, query);
if (errorType == ERROR_NO_ERROR) {
return true;
}
Log.d(MyTracksConstants.TAG, "GData error encountered: " + errorMessage);
if (errorType == ERROR_AUTH && auth != null) {
if (!retryOnAuthFailure || !invalidateAndRefreshAuthToken()) {
return false;
}
}
Log.d(MyTracksConstants.TAG, "retrying function/query");
}
return false;
}
/**
* Execute a given function or query. If one is executed, errorType and
* errorMessage will contain the result/status of the function/query.
*/
private void runOne(final AuthenticatedFunction function,
final QueryFunction query) {
try {
if (function != null) {
@@ -128,11 +151,14 @@ public class GDataWrapper {
} else if (query != null) {
query.query(gdataServiceClient);
} else {
return false;
throw new IllegalArgumentException(
"invalid invocation of runOne; one of function/query " +
"must be non-null");
}
errorType = ERROR_NO_ERROR;
errorMessage = null;
return true;
} catch (AuthenticationException e) {
Log.e(MyTracksConstants.TAG, "Exception", e);
errorType = ERROR_AUTH;
@@ -166,32 +192,42 @@ public class GDataWrapper {
errorType = ERROR_CONFLICT;
errorMessage = e.getMessage();
}
}
Log.d(MyTracksConstants.TAG, "GData error encountered: " + errorMessage);
if (errorType == ERROR_AUTH && auth != null) {
Runnable whenFinished = null;
if (retryOnAuthFailure) {
retriesPending++;
// This is a little odd. This method cannot return in the the thread
// that called the original runQuery(). We should look at reworking this
// API.
whenFinished = new Runnable() {
public void run() {
retriesPending--;
try {
runOne(function, query);
} catch (IllegalStateException ise) {
// This can happen if the connection pool was shut down.
Log.e(MyTracksConstants.TAG, "Failed to rerun query.", ise);
}
if (cleanupCalled && retriesPending == 0) {
cleanUp();
}
}
};
}
auth.invalidateAndRefresh(whenFinished);
/**
* Invalidates and refreshes the auth token. Blocks until the refresh has
* completed or until we deem the refresh as having timed out.
*
* @return true If the invalidate/refresh succeeds, false if it fails or
* times out.
*/
private boolean invalidateAndRefreshAuthToken() {
Log.d(MyTracksConstants.TAG, "Retrying due to auth failure");
// This FutureTask doesn't do anything -- it exists simply to be
// blocked upon using get().
FutureTask<?> whenFinishedFuture = new FutureTask<Object>(new Runnable() {
public void run() {}
}, null);
auth.invalidateAndRefresh(whenFinishedFuture);
try {
Log.d(MyTracksConstants.TAG, "waiting for invalidate");
whenFinishedFuture.get(AUTH_TOKEN_INVALIDATE_REFRESH_TIMEOUT,
TimeUnit.MILLISECONDS);
Log.d(MyTracksConstants.TAG, "invalidate finished");
return true;
} catch (InterruptedException e) {
Log.e(MyTracksConstants.TAG, "Failed to invalidate", e);
} catch (ExecutionException e) {
Log.e(MyTracksConstants.TAG, "Failed to invalidate", e);
} catch (TimeoutException e) {
Log.e(MyTracksConstants.TAG, "Invalidate didn't complete in time", e);
} finally {
whenFinishedFuture.cancel(false);
}
return false;
}
@@ -203,21 +239,10 @@ public class GDataWrapper {
return errorMessage;
}
// cleanUp must be called when done using this wrapper to close the client.
// Note that the cleanup will be delayed if auth failure retries were
// requested and there is a pending retry.
public void cleanUp() {
cleanupCalled = true;
if (retriesPending == 0) {
androidGDataClient.close();
androidGDataClient = null;
}
}
public void setAuthManager(AuthManager auth) {
this.auth = auth;
}
public void setRetryOnAuthFailure(boolean retry) {
retryOnAuthFailure = retry;
}
@@ -0,0 +1,65 @@
/*
* Copyright 2010 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.services;
import com.google.android.apps.mytracks.MyTracksSettings;
import com.google.android.maps.mytracks.R;
import android.content.Context;
import android.content.SharedPreferences;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
* Creates a default track name based on the current default track name policy.
*
* @author Matthew Simmons
*/
class DefaultTrackNameFactory {
private static final String TIMESTAMP_DATE_FORMAT = "yyyy-MM-dd HH:mm";
private final Context context;
DefaultTrackNameFactory(Context context) {
this.context = context;
}
/**
* Creates a new track name.
*
* @param trackId The ID for the current track.
* @param startTime The start time, in milliseconds since the epoch, of the
* current track.
* @return The new track name.
*/
String newTrackName(long trackId, long startTime) {
if (useTimestampTrackName()) {
SimpleDateFormat formatter = new SimpleDateFormat(TIMESTAMP_DATE_FORMAT);
return formatter.format(new Date(startTime));
} else {
return String.format(context.getString(R.string.new_track), trackId);
}
}
/** Determines whether the preferences allow a timestamp-based track name */
protected boolean useTimestampTrackName() {
SharedPreferences prefs =
context.getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0);
return prefs.getBoolean(
context.getString(R.string.timestamp_track_name_key), true);
}
}
@@ -84,13 +84,4 @@ interface ITrackRecordingService {
* Deletes all the stored tracks.
*/
void deleteAllTracks();
/**
* Notifies the service that its preferences may have been changed.
* This is necessary because the service running on a separate process cannot
* listen to the changes itself.
*
* @param key the preference key which may have changed
*/
void sharedPreferenceChanged(in String key);
}
@@ -25,20 +25,20 @@ import java.util.Timer;
import java.util.TimerTask;
/**
* This class will periodically announce the user's trip statitics.
* This class will periodically announce the user's trip statistics.
*
* @author Sandor Dornbush
*/
public class PeriodicTaskExecuter {
private final PeriodicTask task;
private final TrackRecordingService service;
/**
* A timer to schedule the announcements.
* This is non-null if the task is in started (scheduled) state.
*/
private Timer timer = new Timer();
private final PeriodicTask task;
private Timer timer;
public PeriodicTaskExecuter(PeriodicTask task,
TrackRecordingService service) {
@@ -52,8 +52,19 @@ public class PeriodicTaskExecuter {
* @param interval The interval in milliseconds
*/
public void scheduleTask(long interval) {
timer.cancel();
timer.purge();
// TODO: Decouple service from this class once and forever.
if (!service.isRecording()) {
return;
}
if (timer != null) {
timer.cancel();
timer.purge();
} else {
// First start, or we were previously shut down.
task.start();
}
timer = new Timer();
if (interval <= 0) {
return;
@@ -61,12 +72,14 @@ public class PeriodicTaskExecuter {
long now = System.currentTimeMillis();
long next = service.getTripStatistics().getStartTime();
while (next < now) next += interval;
if (next < now) {
next = now + interval - ((now - next) % interval);
}
Date start = new Date(next);
Log.i(MyTracksConstants.TAG,
"StatusAnnouncer scheduled to start at " + start + " every "
+ interval + " milliseconds.");
task.getClass().getSimpleName() + " scheduled to start at " + start
+ " every " + interval + " milliseconds.");
timer.scheduleAtFixedRate(new PeriodicTimerTask(), start, interval);
}
@@ -74,10 +87,14 @@ public class PeriodicTaskExecuter {
* Cleans up this object.
*/
public void shutdown() {
timer.cancel();
timer.purge();
timer = null;
task.shutdown();
Log.i(MyTracksConstants.TAG,
task.getClass().getSimpleName() + " shutting down.");
if (timer != null) {
timer.cancel();
timer.purge();
timer = null;
task.shutdown();
}
}
/**
@@ -15,21 +15,25 @@
*/
package com.google.android.apps.mytracks.services;
import android.content.SharedPreferences;
import android.util.Log;
import com.google.android.apps.mytracks.MyTracksConstants;
import com.google.android.apps.mytracks.MyTracksSettings;
import com.google.android.maps.mytracks.R;
import android.content.SharedPreferences;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
import android.util.Log;
/**
* A class that manages reading the shared preferences for the service.
*
* @author Sandor Dornbush
*/
public class PreferenceManager {
public class PreferenceManager implements OnSharedPreferenceChangeListener {
private TrackRecordingService service;
private SharedPreferences sharedPreferences;
private final String announcementFrequencyKey;
private final String autoResumeTrackCurrentRetryKey;
private final String autoResumeTrackTimeoutKey;
private final String maxRecordingDistanceKey;
private final String metricUnitsKey;
private final String minRecordingDistanceKey;
@@ -41,9 +45,21 @@ public class PreferenceManager {
public PreferenceManager(TrackRecordingService service) {
this.service = service;
this.sharedPreferences = service.getSharedPreferences(
MyTracksSettings.SETTINGS_NAME, 0);
if (sharedPreferences == null) {
Log.w(MyTracksConstants.TAG,
"TrackRecordingService: Couldn't get shared preferences.");
throw new IllegalStateException("Couldn't get shared preferences");
}
sharedPreferences.registerOnSharedPreferenceChangeListener(this);
announcementFrequencyKey =
service.getString(R.string.announcement_frequency_key);
autoResumeTrackCurrentRetryKey =
service.getString(R.string.auto_resume_track_current_retry_key);
autoResumeTrackTimeoutKey =
service.getString(R.string.auto_resume_track_timeout_key);
maxRecordingDistanceKey =
service.getString(R.string.max_recording_distance_key);
metricUnitsKey =
@@ -54,12 +70,15 @@ public class PreferenceManager {
service.getString(R.string.min_recording_interval_key);
minRequiredAccuracyKey =
service.getString(R.string.min_required_accuracy_key);
splitFrequencyKey =
service.getString(R.string.split_frequency_key);
signalSamplingFrequencyKey =
service.getString(R.string.signal_sampling_frequency_key);
recordingTrackKey =
service.getString(R.string.recording_track_key);
signalSamplingFrequencyKey =
service.getString(R.string.signal_sampling_frequency_key);
splitFrequencyKey =
service.getString(R.string.split_frequency_key);
// Refresh all properties.
onSharedPreferenceChanged(sharedPreferences, null);
}
/**
@@ -68,15 +87,15 @@ public class PreferenceManager {
*
* @param key the key that changed (may be null to update all preferences)
*/
public void onSharedPreferenceChanged(String key) {
SharedPreferences sharedPreferences =
service.getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0);
if (sharedPreferences == null) {
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences,
String key) {
if (service == null) {
Log.w(MyTracksConstants.TAG,
"TrackRecordingService: Couldn't get shared preferences.");
"onSharedPreferenceChanged: a preference change (key = " + key
+ ") after a call to shutdown()");
return;
}
if (key == null || key.equals(minRecordingDistanceKey)) {
service.setMinRecordingDistance(
sharedPreferences.getInt(
@@ -87,10 +106,9 @@ public class PreferenceManager {
+ service.getMinRecordingDistance());
}
if (key == null || key.equals(maxRecordingDistanceKey)) {
service.setMaxRecordingDistance(
sharedPreferences.getInt(
maxRecordingDistanceKey,
MyTracksSettings.DEFAULT_MAX_RECORDING_DISTANCE));
service.setMaxRecordingDistance(sharedPreferences.getInt(
maxRecordingDistanceKey,
MyTracksSettings.DEFAULT_MAX_RECORDING_DISTANCE));
}
if (key == null || key.equals(minRecordingIntervalKey)) {
int minRecordingInterval = sharedPreferences.getInt(
@@ -119,19 +137,27 @@ public class PreferenceManager {
}
}
if (key == null || key.equals(minRequiredAccuracyKey)) {
service.setMinRequiredAccuracy(
sharedPreferences.getInt(
minRequiredAccuracyKey,
MyTracksSettings.DEFAULT_MIN_REQUIRED_ACCURACY));
service.setMinRequiredAccuracy(sharedPreferences.getInt(
minRequiredAccuracyKey,
MyTracksSettings.DEFAULT_MIN_REQUIRED_ACCURACY));
}
if (key == null || key.equals(announcementFrequencyKey)) {
service.setAnnouncementFrequency(
sharedPreferences.getInt(announcementFrequencyKey,
-1));
sharedPreferences.getInt(announcementFrequencyKey, -1));
}
if (key == null || key.equals(autoResumeTrackTimeoutKey)) {
service.setAutoResumeTrackTimeout(sharedPreferences.getInt(
autoResumeTrackTimeoutKey,
MyTracksSettings.DEFAULT_AUTO_RESUME_TRACK_TIMEOUT));
}
if (key == null || key.equals(recordingTrackKey)) {
service.setRecordingTrackId(
sharedPreferences.getLong(recordingTrackKey, -1));
long recordingTrackId = sharedPreferences.getLong(recordingTrackKey, -1);
// Only read the id if it is valid.
// Setting it to -1 should only happen in
// TrackRecordingService.endCurrentTrack()
if (recordingTrackId > 0) {
service.setRecordingTrackId(recordingTrackId);
}
}
if (key == null || key.equals(splitFrequencyKey)) {
service.getSplitManager().setSplitFrequency(
@@ -139,13 +165,30 @@ public class PreferenceManager {
}
if (key == null || key.equals(signalSamplingFrequencyKey)) {
service.getSignalManager().setFrequency(
sharedPreferences.getInt(
signalSamplingFrequencyKey, -1), service);
sharedPreferences.getInt(signalSamplingFrequencyKey, -1), service);
}
if (key == null || key.equals(metricUnitsKey)) {
service.getSplitManager().setMetricUnits(
sharedPreferences.getBoolean(
metricUnitsKey, true));
sharedPreferences.getBoolean(metricUnitsKey, true));
}
}
public void setAutoResumeTrackCurrentRetry(int retryAttempts) {
sharedPreferences
.edit()
.putInt(autoResumeTrackCurrentRetryKey, retryAttempts)
.commit();
}
public void setRecordingTrack(long id) {
sharedPreferences
.edit()
.putLong(recordingTrackKey, id)
.commit();
}
public void shutdown() {
sharedPreferences.unregisterOnSharedPreferenceChangeListener(this);
service = null;
}
}
@@ -1,62 +0,0 @@
/*
* 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;
import android.content.Context;
/**
* This class will periodically announce the user's trip statitics. This class
* is a wrapper for cupcake devices.
*
* @author Sandor Dornbush
*/
public class SafeStatusAnnouncerTask implements PeriodicTask {
private StatusAnnouncerTask announcer;
/* class initialization fails when this throws an exception */
static {
try {
Class.forName(
"com.google.android.apps.mytracks.services.StatusAnnouncerTask");
} catch (ClassNotFoundException ex) {
throw new RuntimeException(ex);
} catch (LinkageError er) {
throw new RuntimeException(er);
}
}
/* calling here forces class initialization */
public static void checkAvailable() {
}
public SafeStatusAnnouncerTask(Context context) {
announcer = new StatusAnnouncerTask(context);
}
public void run(TrackRecordingService service) {
announcer.run(service);
}
public void shutdown() {
announcer.shutdown();
}
@Override
public void start() {
}
}
@@ -44,7 +44,8 @@ public class SignalStrengthTask extends PhoneStateListener
public SignalStrengthTask(Context c) {
context = c;
}
@Override
public void start() {
manager =
(TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
@@ -176,6 +177,7 @@ public class SignalStrengthTask extends PhoneStateListener
}
}
@Override
public void shutdown() {
if (manager != null) {
manager.listen(this, PhoneStateListener.LISTEN_NONE);
@@ -0,0 +1,48 @@
/*
* Copyright 2010 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.services;
import com.google.android.apps.mytracks.MyTracksConstants;
import com.google.android.apps.mytracks.util.ApiFeatures;
import android.content.Context;
import android.util.Log;
/**
* Factory for producing a proper {@link SignalStrengthTask} according to the
* current API level.
*
* @author Rodrigo Damazio
*/
public class SignalStrengthTaskFactory {
private final boolean hasModernSignalStrength;
SignalStrengthTaskFactory(ApiFeatures apiFeatures) {
this.hasModernSignalStrength = apiFeatures.hasModernSignalStrength();
}
public PeriodicTask create(Context context) {
if (hasModernSignalStrength) {
Log.d(MyTracksConstants.TAG,
"TrackRecordingService using modern signal strength api.");
return new SignalStrengthTaskModern(context);
} else {
Log.w(MyTracksConstants.TAG,
"TrackRecordingService using legacy signal strength api.");
return new SignalStrengthTask(context);
}
}
}
@@ -73,6 +73,11 @@ public class SplitManager {
* Calculates the next distance that a split should be inserted at.
*/
public void calculateNextSplit() {
// TODO: Decouple service from this class once and forever.
if (!service.isRecording()) {
return;
}
if (splitFrequency >= 0) {
nextSplitDistance = Double.MAX_VALUE;
Log.d(MyTracksConstants.TAG,
@@ -120,7 +125,15 @@ public class SplitManager {
* @param splitFrequency The frequency in time or distance
*/
public void setSplitFrequency(int splitFrequency) {
Log.d(MyTracksConstants.TAG,
"setSplitFrequency: splitFrequency = " + splitFrequency);
this.splitFrequency = splitFrequency;
// TODO: Decouple service from this class once and forever.
if (!service.isRecording()) {
return;
}
if (splitFrequency < 1) {
if (splitExecuter != null) {
splitExecuter.shutdown();
@@ -0,0 +1,61 @@
/*
* Copyright 2010 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.services;
import com.google.android.apps.mytracks.util.ApiFeatures;
import android.content.Context;
import android.media.AudioManager;
/**
* Factory which wraps construction and setup of text-to-speech announcements in
* an API-level-safe way.
*
* @author Rodrigo Damazio
*/
public class StatusAnnouncerFactory {
private final boolean hasTts;
public StatusAnnouncerFactory(ApiFeatures apiFeatures) {
this.hasTts = apiFeatures.hasTextToSpeech();
}
/**
* Creates a periodic task which does voice announcements.
*
* @return the task, or null if announcements are not supported
*/
public PeriodicTask create(Context context) {
if (hasTts) {
return new StatusAnnouncerTask(context);
} else {
return null;
}
}
/**
* Returns the appropriate volume stream for controlling announcement
* volume.
*/
public int getVolumeStream() {
if (hasTts) {
return StatusAnnouncerTask.getVolumeStream();
} else {
return AudioManager.USE_DEFAULT_STREAM_TYPE;
}
}
}
@@ -26,22 +26,25 @@ import com.google.android.maps.mytracks.R;
import android.content.Context;
import android.content.SharedPreferences;
import android.speech.tts.TextToSpeech;
import android.speech.tts.TextToSpeech.OnInitListener;
import android.telephony.PhoneStateListener;
import android.telephony.TelephonyManager;
import android.util.Log;
import java.util.Locale;
/**
* This class will periodically announce the user's trip statitics.
* This class will periodically announce the user's trip statistics.
*
* @author Sandor Dornbush
*/
public class StatusAnnouncerTask
implements TextToSpeech.OnInitListener, PeriodicTask {
public class StatusAnnouncerTask implements PeriodicTask {
/**
* The rate at which announcements are spoken.
*/
private static final float TTS_SPEECH_RATE = 0.9f;
// @VisibleForTesting
static final float TTS_SPEECH_RATE = 0.9f;
/**
* A pointer to the service context.
@@ -56,42 +59,83 @@ public class StatusAnnouncerTask
/**
* The interface to the text to speech engine.
*/
private TextToSpeech tts = null;
private TextToSpeech tts;
/**
* The response recieved from the TTS engine ater initialization.
* The response received from the TTS engine after initialization.
*/
private int ttsStatus = TextToSpeech.ERROR;
private boolean ready = false;
/**
* Constructs the announcer and start the TTS engine.
* Whether we're allowed to speak right now.
*/
private boolean speechAllowed;
/**
* Listener which updates {@link #speechAllowed} when the phone state changes.
*/
private final PhoneStateListener phoneListener = new PhoneStateListener() {
@Override
public void onCallStateChanged(int state, String incomingNumber) {
speechAllowed = state == TelephonyManager.CALL_STATE_IDLE;
if (!speechAllowed && tts.isSpeaking()) {
// If we're already speaking, stop it.
tts.stop();
}
}
};
public StatusAnnouncerTask(Context context) {
this(context, new StringUtils(context));
}
public StatusAnnouncerTask(Context context, StringUtils stringUtils) {
this.context = context;
this.stringUtils = new StringUtils(context);
tts = new TextToSpeech(context, this);
// Force the language to be the same as the string we will be speaking.
tts.setLanguage(Locale.getDefault());
// Slow down the speed just a bit as it is hard to hear when exercising.
tts.setSpeechRate(TTS_SPEECH_RATE);
this.stringUtils = stringUtils;
}
/**
* Notifies that the tts engine is done with initialization.
* Called when the TTS engine is initialized.
*/
public void onInit(int status) {
this.ttsStatus = status;
private void onTtsInit(int status) {
Log.i(MyTracksConstants.TAG, "TrackRecordingService.TTS init: " + status);
this.ready = status == TextToSpeech.SUCCESS;
if (ready) {
// Force the language to be the same as the string we will be speaking,
// if that's available.
Locale speechLanguage = Locale.getDefault();
int languageAvailability = tts.isLanguageAvailable(speechLanguage);
if (languageAvailability == TextToSpeech.LANG_MISSING_DATA ||
languageAvailability == TextToSpeech.LANG_NOT_SUPPORTED) {
// English is probably supported.
// TODO: Somehow use announcement strings from English too.
Log.w(MyTracksConstants.TAG, "Default language not available, using English.");
speechLanguage = Locale.ENGLISH;
}
tts.setLanguage(speechLanguage);
// Slow down the speed just a bit as it is hard to hear when exercising.
tts.setSpeechRate(TTS_SPEECH_RATE);
}
}
/**
* {@inheritDoc}
*
* Announces the trip status.
*/
@Override
public void run(TrackRecordingService service) {
if (ttsStatus != TextToSpeech.SUCCESS && tts != null) {
Log.e(MyTracksConstants.TAG, "StatusAnnouncer Tts not initialized.");
if (!ready || tts == null) {
Log.e(MyTracksConstants.TAG, "StatusAnnouncer Tts not ready.");
return;
}
if (!speechAllowed) {
Log.i(MyTracksConstants.TAG,
"Not making announcement - not allowed at this time");
return;
}
@@ -110,7 +154,8 @@ public class StatusAnnouncerTask
*
* @return The string that will be read to the user
*/
private String getAnnouncement(final TripStatistics stats) {
// @VisibleForTesting
protected String getAnnouncement(TripStatistics stats) {
SharedPreferences preferences =
context.getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0);
boolean metricUnits = true;
@@ -172,11 +217,63 @@ public class StatusAnnouncerTask
context.getString(speedLabel));
}
public void shutdown() {
tts.shutdown();
@Override
public void start() {
Log.i(MyTracksConstants.TAG, "Starting TTS");
if (tts == null) {
// We can't have this class also be the listener, otherwise it's unsafe to
// reference it in Cupcake (even if we don't instantiate it).
tts = newTextToSpeech(context, new OnInitListener() {
@Override
public void onInit(int status) {
onTtsInit(status);
}
});
}
speechAllowed = true;
// Register ourselves as a listener so we won't speak during a call.
listenToPhoneState(phoneListener, PhoneStateListener.LISTEN_CALL_STATE);
}
@Override
public void start() {
public void shutdown() {
// Stop listening to phone state.
listenToPhoneState(phoneListener, PhoneStateListener.LISTEN_NONE);
if (tts != null) {
tts.shutdown();
tts = null;
}
Log.i(MyTracksConstants.TAG, "TTS shut down");
}
/**
* Wrapper for instantiating a {@link TextToSpeech} object, which causes
* several issues during testing.
*/
// @VisibleForTesting
protected TextToSpeech newTextToSpeech(Context ctx, OnInitListener onInitListener) {
return new TextToSpeech(ctx, onInitListener);
}
/**
* Wrapper for calls to the 100%-unmockable {@link TelephonyManager#listen}.
*/
// @VisibleForTesting
protected void listenToPhoneState(PhoneStateListener listener, int events) {
TelephonyManager telephony =
(TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
if (telephony != null) {
telephony.listen(listener, events);
}
}
/**
* Returns the volume stream to use for controlling announcement volume.
*/
public static int getVolumeStream() {
return TextToSpeech.Engine.DEFAULT_STREAM;
}
}
@@ -15,24 +15,23 @@
*/
package com.google.android.apps.mytracks.services;
import android.util.Log;
import com.google.android.apps.mytracks.MyTracksConstants;
import android.util.Log;
/**
* This class will manage a period task executer.
* This class manages a period task executer.
*
* @author Sandor Dornbush
*/
public class TaskExecuterManager {
int frequency;
PeriodicTask task;
PeriodicTaskExecuter executer;
private int frequency;
private final PeriodicTask task;
private PeriodicTaskExecuter executer;
public TaskExecuterManager(int frequency,
PeriodicTask task,
TrackRecordingService service) {
public TaskExecuterManager(int frequency, PeriodicTask task,
TrackRecordingService service) {
this.task = task;
setFrequency(frequency, service);
}
@@ -50,7 +49,8 @@ public class TaskExecuterManager {
*/
public void setFrequency(int frequency, TrackRecordingService service) {
this.frequency = frequency;
Log.i(MyTracksConstants.TAG, "Frequency set to: " + frequency);
Log.i(MyTracksConstants.TAG, "Frequency set to " + frequency
+ " for task " + task.getClass().getSimpleName());
if (frequency == -1) {
if (executer != null) {
@@ -61,7 +61,6 @@ public class TaskExecuterManager {
}
} else {
if (executer == null) {
task.start();
executer = new PeriodicTaskExecuter(task, service);
}
executer.scheduleTask(frequency * 60000);
@@ -69,12 +68,20 @@ public class TaskExecuterManager {
}
/**
* Restore the task at the current frequency.
* Restores the task at the current frequency.
*/
public void restore() {
if (frequency > 0) {
task.start();
executer.scheduleTask(frequency * 60000);
}
}
/**
* Shuts down this executer.
*/
public void shutdown() {
if (executer != null) {
executer.shutdown();
}
}
}
@@ -15,6 +15,8 @@
*/
package com.google.android.apps.mytracks.services;
import static com.google.android.apps.mytracks.MyTracksConstants.RESUME_TRACK_EXTRA_NAME;
import com.google.android.apps.mytracks.MyTracks;
import com.google.android.apps.mytracks.MyTracksConstants;
import com.google.android.apps.mytracks.MyTracksSettings;
@@ -25,6 +27,8 @@ import com.google.android.apps.mytracks.content.Waypoint;
import com.google.android.apps.mytracks.content.WaypointsColumns;
import com.google.android.apps.mytracks.stats.TripStatistics;
import com.google.android.apps.mytracks.stats.TripStatisticsBuilder;
import com.google.android.apps.mytracks.util.ApiFeatures;
import com.google.android.apps.mytracks.util.ApiPlatformAdapter;
import com.google.android.apps.mytracks.util.MyTracksUtils;
import com.google.android.apps.mytracks.util.StringUtils;
import com.google.android.maps.mytracks.R;
@@ -36,6 +40,7 @@ import android.app.Service;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.database.Cursor;
import android.database.sqlite.SQLiteException;
import android.location.Location;
@@ -63,6 +68,8 @@ 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;
private LocationManager locationManager;
private WakeLock wakeLock;
@@ -73,15 +80,13 @@ public class TrackRecordingService extends Service implements LocationListener {
MyTracksSettings.DEFAULT_MAX_RECORDING_DISTANCE;
private int minRequiredAccuracy =
MyTracksSettings.DEFAULT_MIN_REQUIRED_ACCURACY;
private int autoResumeTrackTimeout =
MyTracksSettings.DEFAULT_AUTO_RESUME_TRACK_TIMEOUT;
private long recordingTrackId = -1;
private long currentWaypointId = -1;
/**
* For debugging. Keep track of calls to onCreate().
*/
private boolean onCreateWasCalled = false;
/** The timer posts a runnable to the main thread via this handler. */
private final Handler handler = new Handler();
@@ -90,20 +95,20 @@ public class TrackRecordingService extends Service implements LocationListener {
*/
private MyTracksProviderUtils providerUtils;
private TripStatisticsBuilder statsBuilder = new TripStatisticsBuilder();
private TripStatisticsBuilder waypointStatsBuilder = new TripStatisticsBuilder();
private TripStatisticsBuilder statsBuilder;
private TripStatisticsBuilder waypointStatsBuilder;
/**
* Current length of the recorded track. This length is calculated from the
* recorded points (as compared to each location fix). It's used to overlay
* waypoints precisely in the elevation profile chart.
*/
private double length = 0;
private double length;
/**
* Status announcer executer.
*/
private PeriodicTaskExecuter executer;
private PeriodicTaskExecuter announcementExecuter;
private TaskExecuterManager signalManager;
private SplitManager splitManager;
@@ -113,7 +118,7 @@ public class TrackRecordingService extends Service implements LocationListener {
* The interval in milliseconds that we have requested to be notified of gps
* readings.
*/
private long currentRecordingInterval = 0;
private long currentRecordingInterval;
/**
* The policy used to decide how often we should request gps updates.
@@ -125,14 +130,12 @@ public class TrackRecordingService extends Service implements LocationListener {
* Task invoked by a timer periodically to make sure the location listener is
* still registered.
*/
private final TimerTask checkLocationListener = new TimerTask() {
private TimerTask checkLocationListener = new TimerTask() {
@Override
public void run() {
if (!onCreateWasCalled) {
Log.e(MyTracksConstants.TAG,
"TrackRecordingService is running, but onCreate not called.");
}
if (isRecording) {
// It's always safe to assume that if isRecording() is true, it implies
// that onCreate() has finished.
if (isRecording()) {
handler.post(new Runnable() {
public void run() {
Log.d(MyTracksConstants.TAG,
@@ -141,9 +144,6 @@ public class TrackRecordingService extends Service implements LocationListener {
registerLocationListener();
}
});
} else {
Log.w(MyTracksConstants.TAG,
"Track recording service is paused. That should not be.");
}
}
};
@@ -158,38 +158,31 @@ public class TrackRecordingService extends Service implements LocationListener {
*/
private boolean isMoving = true;
/**
* The most recent recording track.
*/
private Track recordingTrack;
/**
* Is the service currently recording a track?
*/
private boolean isRecording = false;
private boolean isRecording;
/**
* Last good location the service has received from the location listener
*/
private Location lastLocation = null;
private Location lastLocation;
/**
* Last valid location (i.e. not a marker) that was recorded.
*/
private Location lastValidLocation = null;
private Location lastValidLocation;
/**
* The frequency of status announcements.
*/
private int announcementFrequency = -1;
private static boolean mTTSAvailable;
/* establish whether the "new" class is available to us */
static {
try {
SafeStatusAnnouncerTask.checkAvailable();
mTTSAvailable = true;
} catch (Throwable t) {
mTTSAvailable = false;
}
}
/*
* Utility functions
*/
@@ -302,7 +295,7 @@ public class TrackRecordingService extends Service implements LocationListener {
* Tries to acquire a partial wake lock if not already acquired. Logs errors
* and gives up trying in case the wake lock cannot be acquired.
*/
public void acquireWakeLock() {
private void acquireWakeLock() {
try {
PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
if (pm == null) {
@@ -333,10 +326,22 @@ public class TrackRecordingService extends Service implements LocationListener {
}
}
/**
* Releases the wake lock if it's currently held.
*/
private void releaseWakeLock() {
if (wakeLock != null && wakeLock.isHeld()) {
wakeLock.release();
wakeLock = null;
}
}
/**
* Shows the notification message and icon in the notification bar.
*/
public void showNotification() {
final ApiPlatformAdapter apiPlatformAdapter =
ApiFeatures.getInstance().getApiPlatformAdapter();
if (isRecording) {
Notification notification = new Notification(
R.drawable.arrow_320, null /* tickerText */,
@@ -347,9 +352,10 @@ public class TrackRecordingService extends Service implements LocationListener {
notification.setLatestEventInfo(this, getString(R.string.app_name),
getString(R.string.recording_your_track), contentIntent);
notification.flags += Notification.FLAG_NO_CLEAR;
notificationManager.notify(1, notification);
apiPlatformAdapter.startForeground(this, notificationManager, 1,
notification);
} else {
notificationManager.cancelAll();
apiPlatformAdapter.stopForeground(this, notificationManager, 1);
}
}
@@ -388,23 +394,22 @@ public class TrackRecordingService extends Service implements LocationListener {
Log.d(MyTracksConstants.TAG,
"Location listener now unregistered w/ TrackRecordingService.");
}
private void restoreStats() {
private Track getRecordingTrack() {
if (recordingTrackId < 0) {
return;
return null;
}
Track track = providerUtils.getTrack(recordingTrackId);
if (track == null) {
return;
}
return providerUtils.getTrack(recordingTrackId);
}
private void restoreStats(Track track) {
Log.d(MyTracksConstants.TAG,
"Restoring stats of track with ID: " + track.getId());
TripStatistics stats = track.getStatistics();
statsBuilder = new TripStatisticsBuilder();
statsBuilder.resumeAt(stats.getStartTime());
if (executer != null) {
executer.scheduleTask(announcementFrequency * 60000);
}
statsBuilder = new TripStatisticsBuilder(stats.getStartTime());
setUpAnnouncer();
signalManager.restore();
splitManager.restore();
@@ -414,11 +419,11 @@ public class TrackRecordingService extends Service implements LocationListener {
Waypoint waypoint = providerUtils.getFirstWaypoint(recordingTrackId);
if (waypoint != null) {
currentWaypointId = waypoint.getId();
waypointStatsBuilder = new TripStatisticsBuilder(waypoint.getStatistics());
waypointStatsBuilder = new TripStatisticsBuilder(
waypoint.getStatistics());
} else {
// This should never happen, but we got to do something so life goes on:
waypointStatsBuilder = new TripStatisticsBuilder();
waypointStatsBuilder.resumeAt(stats.getStartTime());
waypointStatsBuilder = new TripStatisticsBuilder(stats.getStartTime());
currentWaypointId = -1;
}
@@ -442,7 +447,7 @@ public class TrackRecordingService extends Service implements LocationListener {
}
statsBuilder.getStatistics().setMovingTime(stats.getMovingTime());
statsBuilder.pauseAt(stats.getStopTime());
statsBuilder.resume();
statsBuilder.resumeAt(System.currentTimeMillis());
} else {
Log.e(MyTracksConstants.TAG, "Could not get track points cursor.");
}
@@ -489,7 +494,7 @@ public class TrackRecordingService extends Service implements LocationListener {
}
// At least one track must be available for appending points:
Track recordingTrack = providerUtils.getTrack(recordingTrackId);
recordingTrack = getRecordingTrack();
if (recordingTrack == null) {
Log.d(MyTracksConstants.TAG,
"Not recording. No track to append to available.");
@@ -504,8 +509,8 @@ public class TrackRecordingService extends Service implements LocationListener {
// Update the idle time if needed.
locationListenerPolicy.updateIdleTime(statsBuilder.getIdleTime());
if (currentRecordingInterval
!= locationListenerPolicy.getDesiredPollingInterval()) {
if (currentRecordingInterval !=
locationListenerPolicy.getDesiredPollingInterval()) {
registerLocationListener();
}
@@ -610,69 +615,89 @@ public class TrackRecordingService extends Service implements LocationListener {
// Do nothing
}
/*
* SharedPreferencesChangeListener interface implementation. Note that
* services don't currently receive this event (Android platform limitation).
* This should be called from an activity whenever settings change.
*/
/**
* Notifies that preferences have changed.
* Call this with key == null to update all preferences in one call.
*
* @param key the key that changed (may be null to update all preferences)
*/
public void onSharedPreferenceChanged(String key) {
Log.d(MyTracksConstants.TAG,
"TrackRecordingService.onSharedPreferenceChanged");
prefManager.onSharedPreferenceChanged(key);
if (isRecording) {
registerLocationListener();
}
}
/*
* Application lifetime events: ============================
*/
@Override
public void onCreate() {
Log.d(MyTracksConstants.TAG, "TrackRecordingService.onCreate");
super.onCreate();
onCreateWasCalled = true;
Log.d(MyTracksConstants.TAG, "TrackRecordingService.onCreate");
providerUtils = MyTracksProviderUtils.Factory.get(this);
notificationManager =
(NotificationManager) getSystemService(NOTIFICATION_SERVICE);
locationManager =
(LocationManager) getSystemService(Context.LOCATION_SERVICE);
locationManager = (LocationManager) getSystemService(LOCATION_SERVICE);
splitManager = new SplitManager(this);
try {
signalManager =
new TaskExecuterManager(-1, new SignalStrengthTaskModern(this), this);
Log.d(MyTracksConstants.TAG,
"TrackRecordingService using modern signal strength api.");
} catch (LinkageError e) {
Log.w(MyTracksConstants.TAG,
"TrackRecordingService could not load modern signal strength.", e);
signalManager =
new TaskExecuterManager(-1, new SignalStrengthTask(this), this);
}
SignalStrengthTaskFactory strengthTaskFactory =
new SignalStrengthTaskFactory(ApiFeatures.getInstance());
signalManager =
new TaskExecuterManager(-1, strengthTaskFactory.create(this), this);
prefManager = new PreferenceManager(this);
onSharedPreferenceChanged(null);
restoreStats();
registerLocationListener();
acquireWakeLock();
/**
* After 5 min, check every minute that location listener still is
* registered and spit out additional debugging info to the logs:
*/
timer.schedule(checkLocationListener, 1000 * 60 * 5, 1000 * 60);
isRecording = true;
if (mTTSAvailable && (announcementFrequency != -1)) {
if (executer == null) {
SafeStatusAnnouncerTask announcer = new SafeStatusAnnouncerTask(this);
executer = new PeriodicTaskExecuter(announcer, this);
// Try to restore previous recording state in case this service has been
// restarted by the system, which can sometimes happen.
recordingTrack = getRecordingTrack();
if (recordingTrack != null) {
restoreStats(recordingTrack);
isRecording = true;
} else {
if (recordingTrackId != -1) {
// Make sure we have consistent state in shared preferences.
Log.w(MyTracksConstants.TAG, "TrackRecordingService.onCreate: "
+ "Resetting an orphaned recording track = " + recordingTrackId);
}
prefManager.setRecordingTrack(recordingTrackId = -1);
}
showNotification();
}
/**
* Creates an {@link Executer} and schedules {@class SafeStatusAnnouncerTask}.
* The announcer requires a TTS service and user should have enabled
* the announcements, otherwise this method is no-op.
*/
private void setUpAnnouncer() {
Log.d(MyTracksConstants.TAG, "TrackRecordingService.setUpAnnouncer: "
+ announcementExecuter);
if (announcementFrequency != -1 && recordingTrackId != -1) {
handler.post(new Runnable() {
@Override
public void run() {
if (announcementExecuter == null) {
StatusAnnouncerFactory statusAnnouncerFactory =
new StatusAnnouncerFactory(ApiFeatures.getInstance());
PeriodicTask announcer = statusAnnouncerFactory.create(
TrackRecordingService.this);
if (announcer == null) {
return;
}
// TODO: Either use TaskExecuterManager everywhere, or get rid of it
announcementExecuter = new PeriodicTaskExecuter(announcer,
TrackRecordingService.this);
}
announcementExecuter.scheduleTask(announcementFrequency * 60000);
}
});
}
}
private void shutdownAnnouncer() {
Log.d(MyTracksConstants.TAG, "TrackRecordingService.shutdownAnnouncer: "
+ announcementExecuter);
if (announcementExecuter != null) {
try {
announcementExecuter.shutdown();
} finally {
announcementExecuter = null;
}
}
}
@@ -680,16 +705,32 @@ public class TrackRecordingService extends Service implements LocationListener {
@Override
public void onDestroy() {
Log.d(MyTracksConstants.TAG, "TrackRecordingService.onDestroy");
if (wakeLock != null && wakeLock.isHeld()) {
wakeLock.release();
}
isRecording = false;
showNotification();
prefManager.shutdown();
prefManager = null;
checkLocationListener.cancel();
checkLocationListener = null;
timer.cancel();
timer.purge();
unregisterLocationListener();
if (executer != null) {
executer.shutdown();
}
shutdownAnnouncer();
signalManager.shutdown();
signalManager = null;
splitManager.shutdown();
splitManager = null;
// Make sure we have no indirect references to this service.
locationManager = null;
notificationManager = null;
providerUtils = null;
binder.detachFromService();
binder = null;
// This should be the last operation.
releaseWakeLock();
super.onDestroy();
}
@@ -711,8 +752,104 @@ public class TrackRecordingService extends Service implements LocationListener {
unregisterLocationListener();
return super.stopService(name);
}
@Override
public void onStart(Intent intent, int startId) {
handleStartCommand(intent, startId);
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
handleStartCommand(intent, startId);
return START_STICKY;
}
private void handleStartCommand(Intent intent, int startId) {
Log.d(MyTracksConstants.TAG,
"TrackRecordingService.handleStartCommand: " + startId);
// Check if called on phone reboot with resume intent.
if (intent != null &&
intent.getBooleanExtra(RESUME_TRACK_EXTRA_NAME, false)) {
Log.d(MyTracksConstants.TAG, "TrackRecordingService: requested resume");
// Make sure that the current track exists and is fresh enough.
if (recordingTrack == null || !shouldResumeTrack(recordingTrack)) {
Log.i(MyTracksConstants.TAG,
"TrackRecordingService: Not resuming, because the previous track ("
+ recordingTrack + ") doesn't exist or is too old");
isRecording = false;
prefManager.setRecordingTrack(recordingTrackId = -1);
stopSelfResult(startId);
return;
}
Log.i(MyTracksConstants.TAG, "TrackRecordingService: resuming");
}
}
private void setAutoResumeTrackRetries(
SharedPreferences sharedPreferences, int retryAttempts) {
Log.d(MyTracksConstants.TAG,
"Updating auto-resume retry attempts to: " + retryAttempts);
prefManager.setAutoResumeTrackCurrentRetry(retryAttempts);
}
private boolean shouldResumeTrack(Track track) {
Log.d(MyTracksConstants.TAG, "shouldResumeTrack: autoResumeTrackTimeout = "
+ autoResumeTrackTimeout);
// Check if we haven't exceeded the maximum number of retry attempts.
SharedPreferences sharedPreferences =
getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0);
int retries = sharedPreferences.getInt(
getString(R.string.auto_resume_track_current_retry_key), 0);
Log.d(MyTracksConstants.TAG,
"shouldResumeTrack: Attempting to auto-resume the track ("
+ (retries + 1) + "/" + MAX_AUTO_RESUME_TRACK_RETRY_ATTEMPTS + ")");
if (retries >= MAX_AUTO_RESUME_TRACK_RETRY_ATTEMPTS) {
Log.i(MyTracksConstants.TAG,
"shouldResumeTrack: Not resuming because exceeded the maximum "
+ "number of auto-resume retries");
return false;
}
// Increase number of retry attempts.
setAutoResumeTrackRetries(sharedPreferences, retries + 1);
// Check for special cases.
if (autoResumeTrackTimeout == 0) {
// Never resume.
Log.d(MyTracksConstants.TAG,
"shouldResumeTrack: Auto-resume disabled (never resume)");
return false;
} else if (autoResumeTrackTimeout == -1) {
// Always resume.
Log.d(MyTracksConstants.TAG,
"shouldResumeTrack: Auto-resume forced (always resume)");
return true;
}
// Check if the last modified time is within the acceptable range.
long lastModified =
track.getStatistics() != null ? track.getStatistics().getStopTime() : 0;
Log.d(MyTracksConstants.TAG,
"shouldResumeTrack: lastModified = " + lastModified
+ ", autoResumeTrackTimeout: " + autoResumeTrackTimeout);
return lastModified > 0 && System.currentTimeMillis() - lastModified <=
autoResumeTrackTimeout * 60 * 1000;
}
public boolean isRecording() {
return isRecording;
}
public long insertWaypointMarker(Waypoint waypoint) {
if (!isRecording()) {
throw new IllegalStateException(
"Unable to insert waypoint marker while not recording!");
}
if (waypoint.getLocation() != null) {
waypoint.setLength(length);
waypoint.setDuration(waypoint.getLocation().getTime()
@@ -731,6 +868,11 @@ public class TrackRecordingService extends Service implements LocationListener {
* @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!");
}
StringUtils utils = new StringUtils(TrackRecordingService.this);
// Create a new waypoint to save
@@ -758,132 +900,175 @@ public class TrackRecordingService extends Service implements LocationListener {
Uri uri = providerUtils.insertWaypoint(waypoint);
// Create a new stats keeper for the next marker
waypointStatsBuilder = new TripStatisticsBuilder();
waypointStatsBuilder.resumeAt(time);
waypointStatsBuilder = new TripStatisticsBuilder(time);
updateCurrentWaypoint();
return Long.parseLong(uri.getLastPathSegment());
}
private ServiceBinder binder = new ServiceBinder(this);
/**
* The ITrackRecordingService is defined through IDL.
* TODO: There is a bug in Android that leaks Binder instances. This bug is
* especially visible if we have a non-static class, as there is no way to
* nullify reference to the outer class (the service).
* A workaround is to use a static class and explicitly clear service
* and detach it from the underlying Binder. With this approach, we minimize
* the leak to 24 bytes per each service instance.
*
* For more details, see the following bug:
* http://code.google.com/p/android/issues/detail?id=6426.
*/
private final ITrackRecordingService.Stub binder =
new ITrackRecordingService.Stub() {
@Override
public boolean isRecording() {
return isRecording;
}
private static class ServiceBinder extends ITrackRecordingService.Stub {
private TrackRecordingService service;
public ServiceBinder(TrackRecordingService service) {
this.service = service;
}
/**
* Clears the reference to the outer class to minimize the leak.
*/
public void detachFromService() {
this.service = null;
attachInterface(null, null);
}
@Override
public boolean isRecording() {
return service.isRecording();
}
@Override
public long getRecordingTrackId() {
return recordingTrackId;
}
@Override
public long getRecordingTrackId() {
return service.recordingTrackId;
}
@Override
public boolean hasRecorded() {
return providerUtils.getLastTrackId() >= 0;
}
@Override
public boolean hasRecorded() {
return service.providerUtils.getLastTrackId() >= 0;
}
@Override
public long startNewTrack() {
Log.d(MyTracksConstants.TAG, "TrackRecordingService.startNewTrack");
Track track = new Track();
TripStatistics trackStats = track.getStatistics();
track.setName("new");
long startTime = System.currentTimeMillis();
trackStats.setStartTime(startTime);
track.setStartId(-1);
Uri trackUri = providerUtils.insertTrack(track);
long trackId = Long.parseLong(trackUri.getLastPathSegment());
track.setId(trackId);
track.setName(String.format(getString(R.string.new_track), trackId));
providerUtils.updateTrack(track);
recordingTrackId = trackId;
currentWaypointId = insertStatisticsMarker(null);
isRecording = true;
isMoving = true;
statsBuilder = new TripStatisticsBuilder();
statsBuilder.resumeAt(startTime);
if (announcementFrequency != -1 && executer != null) {
executer.scheduleTask(announcementFrequency * 60000);
}
length = 0;
showNotification();
registerLocationListener();
splitManager.restore();
signalManager.restore();
return trackId;
}
@Override
public long startNewTrack() {
return service.startNewTrack();
}
/**
* Insert the given waypoint marker. Users can insert waypoint markers
* to tag locations with a name, description, category etc.
*
* @param waypoint a waypoint
* @return the unique id of the inserted marker
*/
@Override
public long insertWaypointMarker(Waypoint waypoint) {
return TrackRecordingService.this.insertWaypointMarker(waypoint);
}
/**
* Insert the given waypoint marker. Users can insert waypoint markers
* to tag locations with a name, description, category etc.
*
* @param waypoint a waypoint
* @return the unique id of the inserted marker
*/
@Override
public long insertWaypointMarker(Waypoint waypoint) {
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) {
return TrackRecordingService.this.insertStatisticsMarker(location);
}
/**
* 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) {
return service.insertStatisticsMarker(location);
}
@Override
public void endCurrentTrack() {
Log.d(MyTracksConstants.TAG, "TrackRecordingService.endCurrentTrack");
isRecording = false;
Track recordingTrack = providerUtils.getTrack(recordingTrackId);
if (recordingTrack != null) {
TripStatistics stats = recordingTrack.getStatistics();
stats.setStopTime(System.currentTimeMillis());
stats.setTotalTime(stats.getStopTime() - stats.getStartTime());
long lastRecordedLocationId =
providerUtils.getLastLocationId(recordingTrackId);
ContentValues values = new ContentValues();
if (lastRecordedLocationId >= 0
&& recordingTrack.getStopId() >= 0) {
values.put(TracksColumns.STOPID, lastRecordedLocationId);
}
values.put(TracksColumns.STOPTIME, stats.getStopTime());
values.put(TracksColumns.TOTALTIME, stats.getTotalTime());
getContentResolver().update(TracksColumns.CONTENT_URI, values,
"_id=" + recordingTrack.getId(), null);
}
showNotification();
recordingTrackId = -1;
}
@Override
public void endCurrentTrack() {
service.endCurrentTrack();
}
@Override
public void deleteAllTracks() {
endCurrentTrack();
providerUtils.deleteAllTracks();
}
@Override
public void deleteAllTracks() {
if (isRecording()) {
throw new IllegalStateException(
"Cannot delete all tracks while recording!");
}
service.providerUtils.deleteAllTracks();
}
@Override
public void recordLocation(Location loc) {
onLocationChanged(loc);
}
@Override
public void recordLocation(Location loc) {
service.onLocationChanged(loc);
}
}
@Override
public void sharedPreferenceChanged(String key) {
Log.d(MyTracksConstants.TAG,
"TrackRecordingService.sharedPrefereneChange");
onSharedPreferenceChanged(key);
}
};
public long startNewTrack() {
Log.d(MyTracksConstants.TAG, "TrackRecordingService.startNewTrack");
if (recordingTrackId != -1 || isRecording) {
throw new IllegalStateException("A track is already in progress!");
}
long startTime = System.currentTimeMillis();
acquireWakeLock();
TripStatistics getTripStatistics() {
Track track = new Track();
TripStatistics trackStats = track.getStatistics();
trackStats.setStartTime(startTime);
track.setStartId(-1);
Uri trackUri = providerUtils.insertTrack(track);
recordingTrackId = Long.parseLong(trackUri.getLastPathSegment());
track.setId(recordingTrackId);
track.setName(new DefaultTrackNameFactory(this).newTrackName(
recordingTrackId, startTime));
isRecording = true;
isMoving = true;
providerUtils.updateTrack(track);
statsBuilder = new TripStatisticsBuilder(startTime);
waypointStatsBuilder = new TripStatisticsBuilder(startTime);
currentWaypointId = insertStatisticsMarker(null);
setUpAnnouncer();
length = 0;
showNotification();
registerLocationListener();
splitManager.restore();
signalManager.restore();
// Reset the number of auto-resume retries.
setAutoResumeTrackRetries(
getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0), 0);
// Persist the current recording track.
prefManager.setRecordingTrack(recordingTrackId);
return recordingTrackId;
}
private void endCurrentTrack() {
Log.d(MyTracksConstants.TAG, "TrackRecordingService.endCurrentTrack");
if (recordingTrackId == -1 || !isRecording) {
throw new IllegalStateException("No recording track in progress!");
}
shutdownAnnouncer();
isRecording = false;
Track recordingTrack = providerUtils.getTrack(recordingTrackId);
if (recordingTrack != null) {
TripStatistics stats = recordingTrack.getStatistics();
stats.setStopTime(System.currentTimeMillis());
stats.setTotalTime(stats.getStopTime() - stats.getStartTime());
long lastRecordedLocationId =
providerUtils.getLastLocationId(recordingTrackId);
ContentValues values = new ContentValues();
if (lastRecordedLocationId >= 0
&& recordingTrack.getStopId() >= 0) {
values.put(TracksColumns.STOPID, lastRecordedLocationId);
}
values.put(TracksColumns.STOPTIME, stats.getStopTime());
values.put(TracksColumns.TOTALTIME, stats.getTotalTime());
getContentResolver().update(TracksColumns.CONTENT_URI, values,
"_id=" + recordingTrack.getId(), null);
}
showNotification();
prefManager.setRecordingTrack(recordingTrackId = -1);
releaseWakeLock();
}
public TripStatistics getTripStatistics() {
return statsBuilder.getStatistics();
}
@@ -905,20 +1090,10 @@ public class TrackRecordingService extends Service implements LocationListener {
public void setAnnouncementFrequency(int announcementFrequency) {
this.announcementFrequency = announcementFrequency;
if (mTTSAvailable) {
if (announcementFrequency == -1) {
if (executer != null) {
executer.shutdown();
executer = null;
}
} else {
if (executer == null) {
SafeStatusAnnouncerTask announcer =
new SafeStatusAnnouncerTask(this);
executer = new PeriodicTaskExecuter(announcer, this);
}
executer.scheduleTask(announcementFrequency * 60000);
}
if (announcementFrequency == -1) {
shutdownAnnouncer();
} else {
setUpAnnouncer();
}
}
@@ -954,6 +1129,14 @@ public class TrackRecordingService extends Service implements LocationListener {
LocationListenerPolicy locationListenerPolicy) {
this.locationListenerPolicy = locationListenerPolicy;
}
public int getAutoResumeTrackTimeout() {
return autoResumeTrackTimeout;
}
public void setAutoResumeTrackTimeout(int autoResumeTrackTimeout) {
this.autoResumeTrackTimeout = autoResumeTrackTimeout;
}
public SplitManager getSplitManager() {
return splitManager;
@@ -103,4 +103,9 @@ public class ExtremityMonitor {
public void setMax(double max) {
this.max = max;
}
@Override
public String toString() {
return "Min: " + min + " Max: " + max;
}
}
@@ -36,13 +36,13 @@ public class TripStatisticsBuilder {
/**
* The last location that the gps reported.
*/
private Location lastLocation = null;
private Location lastLocation;
/**
* The last location that contributed to the stats. It is also the last
* location the user was found to be moving.
*/
private Location lastMovingLocation = null;
private Location lastMovingLocation;
/**
* The current speed in meters/second as reported by the gps.
@@ -90,10 +90,13 @@ public class TripStatisticsBuilder {
private long totalLocations = 0;
/**
* Creates a new trip starting at the current system time.
* Creates a new trip starting at the given time.
*
* @param startTime the start time.
*/
public TripStatisticsBuilder() {
public TripStatisticsBuilder(long startTime) {
data = new TripStatistics();
resumeAt(startTime);
}
/**
@@ -103,6 +106,9 @@ public class TripStatisticsBuilder {
*/
public TripStatisticsBuilder(TripStatistics statsData) {
data = new TripStatistics(statsData);
if (data.getStartTime() > 0) {
resumeAt(data.getStartTime());
}
}
/**
@@ -140,8 +146,8 @@ public class TripStatisticsBuilder {
// Don't do anything if we didn't move since last fix:
double distance = lastLocation.distanceTo(currentLocation);
if (distance < MyTracksConstants.MAX_NO_MOVEMENT_DISTANCE
&& currentSpeed < MyTracksConstants.MAX_NO_MOVEMENT_SPEED) {
if (distance < MyTracksConstants.MAX_NO_MOVEMENT_DISTANCE &&
currentSpeed < MyTracksConstants.MAX_NO_MOVEMENT_SPEED) {
lastLocation = currentLocation;
return false;
}
@@ -291,13 +297,6 @@ public class TripStatisticsBuilder {
data.updateGradeExtremities(gradeBuffer.getAverage());
}
/**
* Pauses the track at the current time.
*/
public void pause() {
pauseAt(System.currentTimeMillis());
}
/**
* Pauses the track at the given time.
*
@@ -312,13 +311,6 @@ public class TripStatisticsBuilder {
paused = true;
}
/**
* Resumes the current track at the current time.
*/
public void resume() {
resumeAt(System.currentTimeMillis());
}
/**
* Resumes the current track at the given time.
*
@@ -364,8 +356,7 @@ public class TripStatisticsBuilder {
}
public TripStatistics getStatistics() {
// Take a snapshot - we do't want anyone messing with our internals
// Take a snapshot - we don't want anyone messing with our internals
return new TripStatistics(data);
}
}
@@ -0,0 +1,119 @@
/*
* 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.util;
import com.google.android.apps.mytracks.MyTracksConstants;
import android.os.Build;
import android.util.Log;
/**
* Utility class for determining if newer-API features are available on the
* current device.
*
* @author Rodrigo Damazio
*/
public class ApiFeatures {
/**
* The API level of the Android version we're being run under.
*/
public static final int ANDROID_API_LEVEL = Integer.parseInt(
Build.VERSION.SDK);
private static ApiFeatures instance;
/**
* The API platform adapter supported by this system.
*/
private ApiPlatformAdapter apiPlatformAdapter;
/**
* Returns the singleton instance of this class.
*/
public static ApiFeatures getInstance() {
if (instance == null) {
instance = new ApiFeatures();
}
return instance;
}
/**
* Injects a specific singleton instance, to be used for unit tests.
*/
public static void injectInstance(ApiFeatures instance) {
ApiFeatures.instance = instance;
}
/**
* 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) {
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;
}
/**
* Returns whether cloud backup (a.k.a. Froyo backup) is available.
*/
public boolean hasBackup() {
return getApiLevel() >= 8;
}
/**
* Returns whether text-to-speech is available.
*/
public boolean hasTextToSpeech() {
if (getApiLevel() < 4) return false;
try {
Class.forName("android.speech.tts.TextToSpeech");
} catch (ClassNotFoundException ex) {
return false;
} catch (LinkageError er) {
return false;
}
return true;
}
public boolean hasModernSignalStrength() {
return getApiLevel() >= 7;
}
// Visible for testing.
protected int getApiLevel() {
return ANDROID_API_LEVEL;
}
}
@@ -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.util;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.Service;
import android.os.HandlerThread;
/**
* A set of methods that may be implemented in a platform specific way.
*
* @author Bartlomiej Niechwiej
*/
public interface ApiPlatformAdapter {
/**
* Puts the specified service into foreground.
*
* @param service the service to be put in foreground.
* @param notificationManager the notification manager used to post the given
* notification.
* @param id the ID of the notification, unique within the application.
* @param notification the notification to post.
*/
void startForeground(Service service, NotificationManager notificationManager,
int id, Notification notification);
/**
* Puts the given service into background.
* @param service the service to put into background.
* @param notificationManager the notification manager to user when removing
* notifications.
* @param id the ID of the notification to be remove, or -1 if the
* notification shouldn't be removed.
*/
void stopForeground(Service service, NotificationManager notificationManager,
int id);
/**
* Terminates the given handler thread.
*
* @param handlerThread the thread to be terminated.
* @return true whether the handler has been stopped or not.
*/
boolean stopHandlerThread(HandlerThread handlerThread);
}
@@ -32,12 +32,12 @@ public class ChartsExtendedEncoder {
public static String getEncodedValue(int scaled) {
int index1 = scaled / CHARTSERVER_EXTENDED_SINGLE_CHARACTER_VALUES;
int index2 = scaled % CHARTSERVER_EXTENDED_SINGLE_CHARACTER_VALUES;
if (index1 >= CHARTSERVER_EXTENDED_SINGLE_CHARACTER_VALUES) {
if (index1 < 0 || index1 >= CHARTSERVER_EXTENDED_SINGLE_CHARACTER_VALUES) {
return MISSING_POINT_EXTENDED_ENCODING;
}
if (index2 >= CHARTSERVER_EXTENDED_SINGLE_CHARACTER_VALUES) {
int index2 = scaled % CHARTSERVER_EXTENDED_SINGLE_CHARACTER_VALUES;
if (index2 < 0 || index2 >= CHARTSERVER_EXTENDED_SINGLE_CHARACTER_VALUES) {
return MISSING_POINT_EXTENDED_ENCODING;
}
@@ -0,0 +1,53 @@
/*
* 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.util;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.Service;
import android.os.HandlerThread;
/**
* The Cupcake (API level 3) specific implementation of the
* {@link ApiPlatformAdapter}.
*
* @author Bartlomiej Niechwiej
*/
public class CupcakePlatformAdapter implements ApiPlatformAdapter {
@Override
public void startForeground(Service service,
NotificationManager notificationManager, int id,
Notification notification) {
service.setForeground(true);
notificationManager.notify(id, notification);
}
@Override
public void stopForeground(Service service,
NotificationManager notificationManager, int id) {
service.setForeground(false);
if (id != -1) {
notificationManager.cancel(id);
}
}
@Override
public boolean stopHandlerThread(HandlerThread handlerThread) {
// Do nothing, as Cupcake doesn't provide quit().
return false;
}
}
@@ -0,0 +1,48 @@
/*
* 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.util;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.Service;
import android.os.HandlerThread;
/**
* The Eclair (API level 5) specific implementation of the
* {@link ApiPlatformAdapter}.
*
* @author Bartlomiej Niechwiej
*/
public class EclairPlatformAdapter implements ApiPlatformAdapter {
@Override
public void startForeground(Service service,
NotificationManager notificationManager, int id,
Notification notification) {
service.startForeground(id, notification);
}
@Override
public void stopForeground(Service service,
NotificationManager notificationManager, int id) {
service.stopForeground(id != -1);
}
@Override
public boolean stopHandlerThread(HandlerThread handlerThread) {
return handlerThread.quit();
}
}
@@ -37,7 +37,7 @@ public class FileUtils {
* A set of characters that are prohibited from being in file names.
*/
private static final Pattern PROHIBITED_CHAR_PATTERN =
Pattern.compile("[^ A-Za-z0-9_.()]+");
Pattern.compile("[^ A-Za-z0-9_.()-]+");
/**
* Builds a path inside the My Tracks directory in the SD card.
@@ -69,8 +69,6 @@ public class FileUtils {
* Normalizes the input string and make sure it is a valid fat32 file name.
*
* @param name the name to normalize
* @param overheadSize the number of additional characters that will be added
* to the name after sanitization
* @return the sanitized name
*/
String sanitizeName(String name) {
@@ -233,11 +233,10 @@ public class SpreadsheetsClient extends GDataServiceClient {
throws ParseException, IOException {
switch (cause.getStatusCode()) {
case HttpException.SC_CONFLICT:
Entry entry = null;
if (entryClass != null) {
InputStream is = cause.getResponseStream();
if (is != null) {
entry = parseEntry(entryClass, cause.getResponseStream());
parseEntry(entryClass, cause.getResponseStream());
}
}
throw new IOException(message);
+3
View File
@@ -1,4 +1,7 @@
<factorypath>
<factorypathentry kind="PLUGIN" id="org.eclipse.jst.ws.annotations.core" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="WKSPJAR" id="/MyTracksTest/libs/AndroidMockGenerator.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="ANDROID_SDK/platforms/android-8/android.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="ANDROID_SDK/add-ons/addon_google_apis_google_inc_8/libs/maps.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="MYTRACKS_BIN" enabled="true" runInBatchMode="false"/>
</factorypath>
@@ -1,7 +1,7 @@
#Thu Aug 12 19:30:48 BRT 2010
#Mon Nov 01 02:42:03 BRST 2010
eclipse.preferences.version=1
org.eclipse.jdt.apt.aptEnabled=true
org.eclipse.jdt.apt.genSrcDir=.apt_generated
org.eclipse.jdt.apt.processorOptions/bin_dir=MyTracksTest/bin
org.eclipse.jdt.apt.processorOptions/logfile=MyTracksTest/annotations.log
org.eclipse.jdt.apt.processorOptions/bin_dir=%PROJECT.DIR%/bin
org.eclipse.jdt.apt.processorOptions/logfile=%PROJECT.DIR%/annotations.log
org.eclipse.jdt.apt.reconcileEnabled=false
@@ -0,0 +1,94 @@
/*
* 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.graphics.Paint;
import android.graphics.Paint.Style;
import android.test.AndroidTestCase;
/**
* @author Sandor Dornbush
*/
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.string.elevation);
}
public void testInitialConditions() {
assertEquals(25, series.getInterval());
assertEquals(1, series.getMaxLabelLength());
assertEquals(0, series.getMin());
assertEquals(0, series.getMax());
assertEquals(0.0, series.getSpread());
assertEquals(fillPaint1, series.getPaint());
assertEquals(getContext().getString(R.string.elevation),
series.getTitle());
assertTrue(series.isEnabled());
}
public void testEnabled() {
series.setEnabled(false);
assertFalse(series.isEnabled());
}
public void testSmallUpdates() {
series.update(0);
series.update(10);
series.updateDimension();
assertEquals(25, series.getInterval());
assertEquals(3, series.getMaxLabelLength());
assertEquals(0, series.getMin());
assertEquals(100, series.getMax());
assertEquals(100.0, series.getSpread());
}
public void testBigUpdates() {
series.update(0);
series.update(901);
series.updateDimension();
assertEquals(200, series.getInterval());
assertEquals(5, series.getMaxLabelLength());
assertEquals(0, series.getMin());
assertEquals(1000, series.getMax());
assertEquals(1000.0, series.getSpread());
}
public void testNotZeroBasedUpdates() {
series.update(500);
series.update(1401);
series.updateDimension();
assertEquals(200, series.getInterval());
assertEquals(5, series.getMaxLabelLength());
assertEquals(500, series.getMin());
assertEquals(1500, series.getMax());
assertEquals(1000.0, series.getSpread());
}
}
@@ -0,0 +1,88 @@
/*
* 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 android.app.AlertDialog;
import android.app.Dialog;
import android.app.ProgressDialog;
import android.test.ActivityInstrumentationTestCase2;
/**
* @author Sandor Dornbush
*/
public class DialogManagerTest
extends ActivityInstrumentationTestCase2<MyTracks> {
public DialogManagerTest() {
super(MyTracks.class);
}
@Override
protected void setUp() throws Exception {
super.setUp();
MyTracks.clearInstance();
assertNull(MyTracks.getInstance());
}
public void test_onCreateChartSettings() {
Dialog d =
getActivity().onCreateDialog(DialogManager.DIALOG_CHART_SETTINGS, null);
assertNotNull(d);
assertTrue(d instanceof ChartSettingsDialog);
}
public void test_onCreateImportProgress() {
Dialog d =
getActivity().onCreateDialog(
DialogManager.DIALOG_IMPORT_PROGRESS, null);
assertNotNull(d);
assertTrue(d instanceof ProgressDialog);
}
public void test_onCreateProgress() {
Dialog d =
getActivity().onCreateDialog(DialogManager.DIALOG_PROGRESS, null);
assertNotNull(d);
assertTrue(d instanceof ProgressDialog);
ProgressDialog pd = (ProgressDialog) d;
assertEquals(100, pd.getMax());
assertEquals(10, pd.getProgress());
}
public void test_onCreateSendToGoogle() {
Dialog d =
getActivity().onCreateDialog(DialogManager.DIALOG_SEND_TO_GOOGLE, null);
assertNotNull(d);
assertTrue(d instanceof SendToGoogleDialog);
}
public void test_onCreateSendToGoogleResult() {
Dialog d =
getActivity().onCreateDialog(
DialogManager.DIALOG_SEND_TO_GOOGLE_RESULT, null);
assertNotNull(d);
assertTrue(d instanceof AlertDialog);
}
public void test_onCreateWriteProgress() {
Dialog d =
getActivity().onCreateDialog(
DialogManager.DIALOG_WRITE_PROGRESS, null);
assertNotNull(d);
assertTrue(d instanceof ProgressDialog);
}
}

Some files were not shown because too many files have changed in this diff Show More