Compare commits

...

149 Commits

Author SHA1 Message Date
Dennis Guse 50654c6baf Release v3.2.1 2019-10-15 18:36:40 +02:00
Dennis Guse 8c9bf60fc6 Update Settings if the units (metric/imperial) are changed. 2019-10-15 17:29:59 +02:00
Dennis Guse a83eb0b983 Added de-DE description. 2019-10-13 12:19:27 +02:00
Dennis Guse ab3cbfea0f Metadata: adjust to fastlane layout. 2019-10-13 12:19:27 +02:00
Dennis Guse c93e5ca1a6 Settings: moved to Androidx Compat and use one screen only. 2019-10-12 18:00:51 +02:00
Dennis Guse c2841092da Settings: moved setup into helper. 2019-09-19 22:30:28 +02:00
Dennis Guse 8b11e1d611 DialogFragments: do not save state. 2019-09-19 22:30:28 +02:00
Dennis Guse 6375a851b2 Moved ChooseActivityTypeDialog to be a real DialogFragment.
Note: This breaks RecordingsSettingsActivity.
2019-09-19 22:28:04 +02:00
Dennis Guse f21200a7ec UnitConversation: always use double for division.
Fixed wrong computation (00:00) of total time.
2019-09-19 19:29:49 +02:00
Dennis Guse 5752cebdec Released 3.2.0 2019-09-19 19:14:34 +02:00
Dennis Guse fa6329c157 Fdroid: downgraded to Gradle 5.5 2019-09-19 19:08:04 +02:00
Dennis Guse 98a9884798 Fdroid: added link in README and moved metadata to fastlane folder. 2019-09-19 19:04:17 +02:00
Dennis Guse 1f7f615411 Import/Export: first option is KMZ containing all data. 2019-09-16 23:43:30 +02:00
Dennis Guse a79345ccf9 Grant ACCESS_FINE_LOCATION for TrackRecordingServiceTest. 2019-09-16 22:47:37 +02:00
Dennis Guse 732e0e76f9 Removed custom styling of title divider of used dialogs (just too much code). 2019-09-16 22:37:37 +02:00
Dennis Guse 63d29f2097 Cleanup. 2019-09-16 19:44:34 +02:00
Dennis Guse 9dd2f7d1c1 KML/KMZ export and share: add configuration to export with trackdetail without the rest.
Fixed some issues in TRACK_ONLY mode as too much data was exposed.
2019-09-15 09:47:48 +02:00
Dennis Guse afd25363b3 For export and import: KMZ first as it contains all data incl. photos. 2019-09-15 09:22:10 +02:00
Dennis Guse 2eb5a42099 When it should be KMZ, make it KMZ. 2019-09-15 08:45:11 +02:00
Dennis Guse 9c753034b4 Update title in MarkerDetailActivity when viewpager changes. 2019-09-15 00:31:53 +02:00
Dennis Guse 95b7c701a2 Cleanup: do not assume first waypoint contains (always) TripStatistics. 2019-09-15 00:14:15 +02:00
Dennis Guse 56685a9dec Bugfix: export of images via KMZ is now working. 2019-09-14 23:35:28 +02:00
Dennis Guse 45b13ea813 Removed tab icons as they were not shown anyhow. 2019-09-14 22:43:26 +02:00
Dennis Guse b0feed84bb Removed voice frequency menu icon as it not shown anyhow. 2019-09-14 22:34:58 +02:00
Dennis Guse 0e2da83e1f Removed snowboarding and skiing due to lack of icons. 2019-09-14 22:20:29 +02:00
Dennis Guse fa364a150d Styling for progress bar via tint. 2019-09-14 21:50:10 +02:00
Dennis Guse 6ec78d77a9 Changed gps menu item icon to represent actual state of the gps (and not the action to be executed). 2019-09-14 13:25:23 +02:00
Dennis Guse 8853bb5408 Replaced drawables with vectors (SVG) for TrackController.
- ic_button_pause
- ic_button_record
- ic_button_stop
2019-09-14 02:58:05 +02:00
Dennis Guse e4d6355875 Replaced drawables with vectors (SVG) for TrackListActivity.
- ic_pause
- ic_record
- ic_arrow_long
2019-09-14 01:34:41 +02:00
Dennis Guse 1e08d72cc9 Removed resources of indetermined progressbar (unused). 2019-09-14 00:22:35 +02:00
Dennis Guse ea82d2df94 Fixed tabhost resource id. 2019-09-13 23:55:39 +02:00
Dennis Guse 71c0114f8f Fixed lint error. 2019-09-13 23:14:14 +02:00
Dennis Guse a3955c7c37 Made generic activity type selectable.
Decreased icon size.
2019-09-13 23:00:12 +02:00
Dennis Guse e610b70590 Use logo (white) for generic track (replaces similar a png with gradient). 2019-09-13 22:55:25 +02:00
Dennis Guse 7f4d47d7b5 Foreground service notification now uses the logo directly (vector). 2019-09-13 22:38:39 +02:00
Dennis Guse fd27ec11a2 Replaced chartViews current position marker with logo. 2019-09-12 23:52:04 +02:00
Dennis Guse 2aa036f3aa Material design icons for dialogs. 2019-09-12 23:44:21 +02:00
Dennis Guse 2c49a4f3f1 Introduced material design icons. 2019-09-12 23:24:19 +02:00
Dennis Guse 10e4a52947 Fixed stats_content. 2019-09-12 20:39:13 +02:00
Dennis Guse 9f3936e180 Removed unused icons. 2019-09-12 20:17:29 +02:00
Dennis Guse a3a76ce0c3 Disabled lint for AppCompatResource. 2019-09-12 20:15:03 +02:00
Dennis Guse 545db52f94 Cleanup. 2019-09-12 18:33:45 +02:00
Dennis Guse bfa02a97ee Upgrade appcompat library. 2019-09-12 17:57:33 +02:00
Dennis Guse 3a2eeaa204 Fixed lint warnings in stats_content layout. 2019-09-12 17:57:33 +02:00
Dennis Guse 42b3a5dfc9 Refactored stats_content layout to contain all content (not use include). 2019-09-12 17:57:28 +02:00
Dennis Guse b5cb47a0c7 Refactored TrackRecordingServiceConnection: made static methods object methods. 2019-09-10 22:45:20 +02:00
Dennis Guse 443e069a13 Refactored TrackRecordingServiceConnection.
- implemented interfaces directly
- moved static methods from TrackRecordingServiceConnectionUtil
2019-09-10 22:37:54 +02:00
Dennis Guse 9c432c3110 Removed unused menu from SearchListActivity. 2019-09-10 21:20:41 +02:00
Dennis Guse 79934d51ac Revert "Moved from onTouch to onClick listeners."
This reverts commit 025dee68
2019-09-10 21:20:41 +02:00
Dennis Guse 496cf9145b Removed usage of Broadcast (were used by Widget). 2019-09-10 21:20:40 +02:00
Dennis Guse 264bb3746f Removed ControlRecordingService.
It was implemented to enable other apps to start and stop track recordings.
2019-09-10 21:19:11 +02:00
Dennis Guse 7b1ccf4507 Cleanup AndroidManifest. 2019-09-10 21:18:55 +02:00
Dennis Guse a326265dba Fixed searchview, which did not trigger SearchListActivity. 2019-09-10 21:18:25 +02:00
Dennis Guse 0864bc8104 Cleaned up ContentProviderUtils's factory pattern. 2019-09-10 21:18:03 +02:00
Dennis Guse f5656470a9 Removed Parcelable from all data classes as it not needed anymore. 2019-09-10 21:17:45 +02:00
Dennis Guse 5bc773f1aa Removed modifiedtime from Track - was likely a Google Drive leftover. 2019-09-10 21:17:18 +02:00
Dennis Guse 6bfd8c8db8 Cleanup: Use Pair instead of String[2]. 2019-09-10 21:16:58 +02:00
Dennis Guse df2865456e Removed obsolete workaround for DateUtils.formatElapsedTime(). 2019-09-10 21:16:38 +02:00
Dennis Guse fc6d5f1f23 Removed Widget. 2019-09-10 21:16:16 +02:00
Dennis Guse 96dab8c81b Build: enabled abort on lint errors. 2019-09-10 21:16:00 +02:00
Dennis Guse 1d13d51f0c Cleanup. 2019-09-10 21:12:56 +02:00
Dennis Guse 87d92d25f9 Cleanup: mainly code formatting and made LocationIterator implement autoclosable. 2019-09-08 12:39:40 +02:00
Dennis Guse 025dee6821 Moved from onTouch to onClick listeners. 2019-09-08 12:39:40 +02:00
Dennis Guse 796d086615 Cleanup.
Cleanup.
2019-09-08 12:39:37 +02:00
Dennis Guse bb1ef08474 Splitted TrackRecordingServiceTest into two files and fixed issue about calling Looper.prepare() only once for all tests. 2019-09-05 22:44:27 +02:00
Dennis Guse cc770d8bb4 Fixed (some) AnnoucementPeriodicTask[Factory] tests. 2019-09-05 07:41:16 +02:00
Dennis Guse 8a56c262cb Cleanup. 2019-09-04 22:09:22 +02:00
Dennis Guse c0a9958b5b Fixed (some) TrackRecordingServiceTests. 2019-09-04 21:33:30 +02:00
Dennis Guse 7351cd3377 TrackDataHubTest is beyond repair and thus removed. 2019-09-04 20:41:39 +02:00
Dennis Guse 7b3d5848b9 Fixed ControlRecordingService: only unbind if connected. 2019-09-04 17:43:24 +02:00
Dennis Guse 0f311e062e Fixed ContentProviderUtils tests.
Account for using a real (non-empty) database and thus non-predictable object ids.
2019-09-03 21:32:35 +02:00
Dennis Guse 57930a8d86 Added changelog for 2991/v3.1.0 2019-09-03 21:32:35 +02:00
Dennis Guse 7c0069394f Release v3.1.0 2019-09-02 19:42:40 +02:00
Dennis Guse 379913fb3e Cleanup. 2019-09-02 19:39:58 +02:00
Dennis Guse 542d9ccb77 Request storage permission on import all and export all. 2019-09-02 19:37:55 +02:00
Dennis Guse c8ae3b83b5 Added option to show statistics while recording without unlocking the device. 2019-09-02 18:36:31 +02:00
Dennis Guse 0a046fc100 Cleanup. 2019-09-02 18:02:43 +02:00
Dennis Guse c551a38a8b Show full version name based upon git describe --tags on startup and also in AboutActivity. 2019-09-02 16:05:24 +02:00
Dennis Guse 72b1ca82ed Moved debug warnings into Startup. 2019-09-02 15:50:35 +02:00
Dennis Guse 1ef81c45db Build: do not minify code for release.
The amount of space saved is too small to be of a real benefit while stack traces become unreadable.
2019-09-02 15:50:35 +02:00
Dennis Guse 08e6ae76f2 Fix TravisCI for Android-29 (#34) 2019-09-02 15:44:21 +02:00
Dennis Guse 6e410e049d Bugfix: creating waypoint from camera works now.
Introduced in commit 677b691ef1
2019-08-26 22:57:00 +02:00
Dennis Guse 22b7962940 Upgraded to Gradle 5.6 and AndroidStudio 3.5.0. 2019-08-26 21:55:40 +02:00
Dennis Guse 5788f99b20 Catch ActivityNotFound exception for starting geo-data intents. 2019-08-25 22:20:08 +02:00
Dennis Guse 8685e43dc2 Fixed ShareContentProvider and KMZ mime type. 2019-08-25 21:37:47 +02:00
Dennis Guse b8bea27155 Added AndroidStudio coding style configuration. 2019-08-25 16:21:20 +02:00
Dennis Guse c9226d3b79 Fixed CustomContentProvider tests. 2019-08-25 16:20:27 +02:00
Dennis Guse 9701feff04 Removed duplicate registration of images at MediaScanner. 2019-08-25 08:42:36 +02:00
Dennis Guse b8b40a88ef Show Marker menu entry at defined position. 2019-08-25 08:41:11 +02:00
Dennis Guse adcf7b388b Finish MarkerDetailActivity after storing marker. 2019-08-25 00:26:04 +02:00
Dennis Guse 3a7562d96c Modified "show on map" functionality to only share locations and no other track data. 2019-08-25 00:21:36 +02:00
Dennis Guse 2753cf7d0b Removed search suggestion configuration. 2019-08-24 18:48:50 +02:00
Dennis Guse 5841415a0b Refactored TrackWriter instantiation. 2019-08-24 18:14:32 +02:00
Dennis Guse d79fa07834 Share multiple tracks from TrackListActivity. 2019-08-24 17:51:57 +02:00
Dennis Guse ff6caf706e Added show on map to TrackDetailActivity. 2019-08-24 17:51:57 +02:00
Dennis Guse c953fbc67a Implemented "Share" from TrackListActivity's context menu. 2019-08-24 09:37:38 +02:00
Dennis Guse 55870cca85 Bugfix: import/export all now react to back button. 2019-08-24 08:49:59 +02:00
Dennis Guse 0123242be6 Bugfix: use correct entries/values for track name. 2019-08-24 08:44:43 +02:00
Dennis Guse 81a6d7b53a Added release recipe for FDroid. 2019-08-24 08:39:13 +02:00
Dennis Guse f423b07cee Added Changelogs for v3.0.0, v3.0.1, and the upcoming release. 2019-08-24 08:27:31 +02:00
Dennis Guse 33231bdbb1 Renaming to AbstractListActivity. 2019-08-23 07:37:03 +02:00
Dennis Guse 677b691ef1 Moved special intent creation to IntentUtils. 2019-08-23 07:33:58 +02:00
Dennis Guse 5e74e80a9a Use correct mime type for KML and KMZ. 2019-08-23 07:19:40 +02:00
Dennis Guse 70b5d799a2 Share marker works everywhere and share track from TrackDetailActivity. 2019-08-23 07:01:14 +02:00
Dennis Guse 16f18fe16f Removed search suggestion provider as it only provided recent searches. 2019-08-21 22:25:16 +02:00
Dennis Guse 8538c79dd8 Extracted content uri definition. 2019-08-21 22:20:18 +02:00
Dennis Guse f8e3b28e51 Removed landscape mode for TrackDetailActivity as it was broken and portrait mode needs heavy refactoring first. 2019-08-21 07:18:57 +02:00
Dennis Guse 9f665e07fd Show on map: everything is handled by one content provider alone.
To avoid issues forwarding a requests from (temporarily) exported content provider to internal content provider only one content provider is used.
2019-08-20 22:55:32 +02:00
Dennis Guse b7c010c086 Show on map: draft via virtual file (with two content providers).
Exported content provider that temporary needs to grant access to the internal content provider.
2019-08-20 22:49:54 +02:00
Dennis Guse 8296c0194d Show on map: draft via sharing temporary file. 2019-08-20 22:48:05 +02:00
Dennis Guse f537cc7321 Cleanup: automatic code inspection via Android Studio. 2019-08-18 14:46:43 +02:00
Dennis Guse 4fa2401b27 Removed CustomContentProvider.openFile() as it was used for Google Earth integration. 2019-08-18 13:22:41 +02:00
Dennis Guse f30e3cd5cd Removed contentDescription for logo (was still MyTracks). 2019-08-18 13:13:11 +02:00
Dennis Guse b73353120f PreferenceCategory import/export all: now uses Tracks as title. 2019-08-18 11:00:04 +02:00
Dennis Guse 7fa9df1f99 Cleanup. 2019-08-17 00:58:52 +02:00
Dennis Guse ea3de8a987 Renamed SensorManager to RemoteSensorManager. 2019-08-16 21:20:00 +02:00
Dennis Guse c159e1eba4 Version v3.0.1 2019-08-16 19:53:38 +02:00
Dennis Guse 420d538a73 Preferences: Import all/Export all has now transparent background and back button works as desired. 2019-08-16 18:53:26 +02:00
Dennis Guse 36672d6857 Fixed ChartFragmentTest. 2019-08-16 07:49:47 +02:00
Dennis Guse 51dc6f8e8d Tests: accept permission automatically. 2019-08-16 07:13:58 +02:00
Dennis Guse 9a6c9eeace Set android:allowBackup=false to prevent backups as this also includes cloud-based storages (e.g., Google Drive). 2019-08-15 23:14:52 +02:00
Dennis Guse bfb7f781ec Enabled TravisCI (just building, no tests). 2019-08-15 23:03:30 +02:00
Dennis Guse 8ca24b877c Moved Import all/Export all functionality to Settings. 2019-08-15 22:38:51 +02:00
Dennis Guse 01ef5cfa6f All test are now executable (but all are not working). 2019-08-14 18:03:48 +02:00
Dennis Guse 08825fcf04 Cleanup: typo. 2019-08-14 08:05:37 +02:00
Dennis Guse 4f5e87f87d Enabled some TrackDataHub tests. 2019-08-14 08:05:00 +02:00
Dennis Guse 45b1fafdd5 Fixed test and implementation of DescriptionGenerator. 2019-08-13 23:04:09 +02:00
Dennis Guse e89258c173 Fixed some tests by using the application context.
Fixed some tests by using the application context.

Fixed some tests by using the application context.

Fixed some tests by using the application context.
2019-08-13 22:49:41 +02:00
Dennis Guse d47d37afd2 Import/Exporter tests now compile (do not work). 2019-08-13 22:09:56 +02:00
Dennis Guse d9effc80a7 Enabled TrackRecordingServiceTest (mainly failing). 2019-08-13 20:59:29 +02:00
Dennis Guse 03d2b05ffd Allows SharedPreferences to contain int as well as String. 2019-08-13 20:56:07 +02:00
Dennis Guse e2cac6ee60 More tests executable (but not passing). 2019-08-12 18:47:48 +02:00
Dennis Guse f749ddb5ae Cleanup. 2019-08-12 18:47:48 +02:00
DanielJorn 6bb409fed0 Fixed typo in Readme.md 2019-08-11 09:25:53 +02:00
Dennis Guse 6da2d9d104 Export to KML and KMZ as requested by user. 2019-08-11 09:07:31 +02:00
Dennis Guse cd267123e7 KML export: write close tag for last segment.
Closes #21.
2019-08-10 14:00:38 +02:00
Dennis Guse 617ee719c8 Add heart rate sensor UI for landscape (BUG: still not visible). 2019-08-07 07:26:39 +02:00
Dennis Guse 4edad610ee Moved SensorSettingsActivity into SettingsActivity. 2019-08-06 20:07:22 +02:00
Dennis Guse fda95bbec9 Integrated ChartSettingsActivity in SettingsActivity. 2019-08-06 19:52:57 +02:00
Dennis Guse 1028e09888 Removed actionbar back button.
It was complicating the code base too much.
2019-08-06 19:46:01 +02:00
Dennis Guse 4abd07694c Use a custom dialog to reset preferences. 2019-08-06 19:08:11 +02:00
Dennis Guse a53b3288f7 BREAKING CHANGE: Moved to getDefaultSharedPreferences(). 2019-08-06 18:34:07 +02:00
Dennis Guse 7ba771c417 Moved sharedPreferences accessor to PreferenceUtils. 2019-08-06 18:18:25 +02:00
Dennis Guse 2a145b4791 Cleanup list preferences. 2019-08-06 07:47:37 +02:00
Dennis Guse cf90462772 PreferenceList: let summary be set by Android.
Some preferences used customized summaries - these were also removed.
2019-08-06 07:41:17 +02:00
Dennis Guse d9a6701260 Initialize preferences with default values. 2019-08-06 07:41:17 +02:00
Dennis Guse 1dc9558541 Use Androidx to get version code from manifest. 2019-08-05 22:21:44 +02:00
Dennis Guse 972de5f92e Photo size for KMZ exports is not configurable anymore. 2019-08-05 22:12:40 +02:00
Dennis Guse 28270b202e Hide activityTypeLabel in statistic summary. 2019-08-05 22:05:49 +02:00
495 changed files with 7176 additions and 10815 deletions
+116
View File
@@ -0,0 +1,116 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<codeStyleSettings language="XML">
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" />
</indentOptions>
<arrangement>
<rules>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:android</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:id</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>style</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
<order>ANDROID_ATTRIBUTE_ORDER</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>.*</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
</rules>
</arrangement>
</codeStyleSettings>
</code_scheme>
</component>
+12
View File
@@ -0,0 +1,12 @@
language: android
dist: trusty
android:
components:
- build-tools-28.0.3
- android-29
script:
- ./gradlew build
#TODO: When all tests are passing, CI should also execute them.
#- ./gradlew connectedCheck
+12
View File
@@ -0,0 +1,12 @@
# OpenTrack: Developer information
## Create releases for FDroid
1. Get next _version code_ (master branch): `git rev-list HEAD --count master`
2. Decide on _version name_ (semantic versioning)
3. Manually update _version code_ and _version name_ in `AndroidManifest.xml` (used by FDroid)
4. Create changelog (`_version code_.txt`)
5. Create commit with all changes
6. Push commits
7. Tag newly create commit with _version name_
8. Push the tagged to public repository
+14 -10
View File
@@ -1,22 +1,22 @@
# OpenTracks: a sport tracker
![OpenTracks logo](LOGO.svg)
# <img src="drawable-svg/LOGO.svg" alt="OpenTracks logo" height="40"></img> OpenTracks: a sport tracker
_OpenTracks_ is a sport tracking application that completely respects your privacy.
<a href="https://liberapay.com/dennis.guse/donate"><img alt="Donate using Liberapay" src="https://liberapay.com/assets/widgets/donate.svg"></a>
<a href="https://f-droid.org/packages/de.dennisguse.opentracks">
<img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png" alt="Get it on F-Droid" height="60">
</a>
## Features:
* __Tracking:__ track your sport and outdoor activities
* __Photos and Markers:__ mark interesting locations while tracking
* __Photos and Markers:__ mark interesting locations while tracking (also with photos)
* __Heart rate:__ supports bluetooth LE sensors
* __Export data:__ Export tracks either as KML or GPX
* __Voice annoucements__
* __Export data:__ export tracks either as KMZ (incl. photos), KML or GPX
* __No Internet access:__ Internet is not used
* __Voice annoucements__
* __No advertising__
* __Privacy:__
* _contains in-app analytics_
* _does not Google Play Services_
* _does not contain any in-app analytics_
* _does not have Google Play Services_
* _has no cloud integration_
__Only required permissions:__
@@ -26,9 +26,13 @@ _Please note:_
_Tracks_ does not contain a _map_ (as this would required Internet access).
To show your tracks on a map, a 3rd party application (e.g., [OsmAnd](https://play.google.com/store/apps/details?id=net.osmand) or [MAPS.ME](https://play.google.com/store/apps/details?id=com.mapswithme.maps.pro)) needs to be installed.
<a href="https://liberapay.com/dennis.guse/donate">
<img alt="Donate using Liberapay" src="https://liberapay.com/assets/widgets/donate.svg" height="45">
</a>
## Project history
_OpenTracks_ is based upon Google _My Tracks app_ ([code](https://code.google.com/archive/p/mytracks/).
_OpenTracks_ is based upon Google _My Tracks app_ ([code](https://code.google.com/archive/p/mytracks/)).
Initially, _My Tracks_ was initially released by Google in 2010 as [open-source software](http://google-latlong.blogspot.fr/2010/05/code-for-my-tracks-is-now-yours.html).
In 2016, [Google decided to discontinue](https://support.google.com/maps/answer/6333516) _My Tracks_ and stopped distributing it via the Google Play store in April 2016.
The [Plonk42](https://github.com/plonk42) conducted some maintenance work until 2016, so _My Tracks_ could still be used (based upon version _Google's MyTracks_ version 2.0.6).
+24 -13
View File
@@ -6,7 +6,7 @@ buildscript {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.4.2'
classpath 'com.android.tools.build:gradle:3.5.0'
}
}
@@ -27,7 +27,7 @@ def getVersionCode = { ->
return Integer.parseInt(stdout.toString().trim())
}
catch (ignored) {
return -1;
return -1
}
}
@@ -41,21 +41,22 @@ def getVersionName = { ->
return stdout.toString().trim()
}
catch (ignored) {
return null;
return null
}
}
android {
compileSdkVersion 28
compileSdkVersion 29
defaultConfig {
applicationId "de.dennisguse.opentracks"
//Useful for debugging.
//versionCode getVersionCode()
//versionName getVersionName()
buildConfigField "String", "VERSION_NAME_FULL", "\"${getVersionName()}\""
minSdkVersion 21
targetSdkVersion 28
targetSdkVersion 29
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
@@ -67,21 +68,31 @@ android {
}
release {
minifyEnabled true
minifyEnabled false
}
}
applicationVariants.all { variant ->
variant.resValue "string", "applicationId", variant.applicationId
variant.resValue "string", "applicationId", variant.applicationId
}
lintOptions {
disable "AppCompatResource" // TODO Disable complaint to use androidx's SearchView as it is null.
}
}
dependencies {
implementation 'androidx.appcompat:appcompat:1.0.2'
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'androidx.preference:preference:1.1.0'
androidTestImplementation 'androidx.test:runner:1.2.0'
androidTestImplementation 'androidx.test:rules:1.2.0'
androidTestImplementation 'androidx.test:core:1.2.0'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
androidTestImplementation 'androidx.test.espresso:espresso-intents:3.2.0'
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
testImplementation 'androidx.test:core:1.2.0'
}
androidTestImplementation 'androidx.test:rules:1.2.0'
androidTestImplementation 'androidx.test:runner:1.2.0'
androidTestImplementation 'org.mockito:mockito-android:3.0.0'
testImplementation 'androidx.test:core:1.2.0'
testImplementation 'org.mockito:mockito-core:3.0.0'
}
View File

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

+41
View File
@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
height="188"
id="svg2"
version="1.1"
viewBox="0 0 78 188"
width="78"
xmlns="http://www.w3.org/2000/svg">
<defs id="defs6" />
<g id="layer1" />
<g
style="display:inline"
id="layer2" />
<metadata id="metadata8">
<rdf:RDF>
<cc:Work rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:title></dc:title>
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<path
style="fill:#cccccc;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;fill-opacity:0.7019608"
d="m 1,150 38,37 39,-37 H 50 V 97 L 29,81 v 69 z"
id="path31161" />
<path
style="fill:#cccccc;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;fill-opacity:0.7019608"
d="M 29,51 V 70 L 50,86 V 67 Z"
id="path31169" />
<path
style="fill:#cccccc;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;fill-opacity:0.7019608"
d="M 29,23 V 40 L 50,56 V 39 Z"
id="path31192" />
<path
style="fill:#cccccc;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;fill-opacity:0.7019608"
d="M 29,0 V 14 L 50,29 V 16 Z"
id="path31194" />
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

+71
View File
@@ -0,0 +1,71 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?><!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
height="125"
id="svg2"
version="1.1"
viewBox="0 0 125 125"
width="125"
xmlns="http://www.w3.org/2000/svg"
inkscape:version="0.92.4 (5da689c313, 2019-01-14)"
sodipodi:docname="ic_button_pause.svg">
<defs id="defs6" />
<g
id="layer1"
inkscape:groupmode="layer"
inkscape:label="Layer 1">
<circle
style="fill:none;fill-opacity:1;stroke:#a31a1f;stroke-width:5.03185225;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
cx="62.5"
cy="62.5"
id="path12"
r="59.984074" />
<rect
style="fill:#efefef;fill-opacity:0.87843137;stroke:none;stroke-width:4.82273912;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
height="17.940611"
id="rect837"
width="5"
x="54"
y="54.059391" />
<rect
style="fill:#efefef;fill-opacity:0.87843137;stroke:none;stroke-width:4.82273865;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
height="17.940611"
id="rect837-3"
width="5"
x="66"
y="54" />
</g>
<metadata id="metadata8">
<rdf:RDF>
<cc:Work rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:title></dc:title>
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<sodipodi:namedview
bordercolor="#666666"
borderopacity="1"
gridtolerance="10"
guidetolerance="10"
id="namedview4"
objecttolerance="10"
pagecolor="#ffffff"
showgrid="false"
inkscape:current-layer="layer1"
inkscape:cx="75.030293"
inkscape:cy="72.881196"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-height="1152"
inkscape:window-maximized="0"
inkscape:window-width="2048"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:zoom="5.3400704" />
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

+49
View File
@@ -0,0 +1,49 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
height="125"
id="svg2"
version="1.1"
viewBox="0 0 125 125"
width="125"
xmlns="http://www.w3.org/2000/svg">
<defs id="defs6" />
<g id="layer1">
<circle
style="fill:none;fill-opacity:1;stroke:#a31a1f;stroke-width:5.03185225;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
cx="62.5"
cy="62.5"
id="path12"
r="59.984074" />
<circle
style="fill:#414141;fill-opacity:0.4;stroke:none;stroke-width:6.9172821;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
cx="62.5"
cy="62.5"
id="path835"
r="50" />
<rect
style="fill:#efefef;fill-opacity:0.87843137;stroke:none;stroke-width:4.82273912;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
height="17.940611"
id="rect837"
width="5"
x="54"
y="54.059391" />
<rect
style="fill:#efefef;fill-opacity:0.87843137;stroke:none;stroke-width:4.82273865;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
height="17.940611"
id="rect837-3"
width="5"
x="66"
y="54" />
</g>
<metadata id="metadata8">
<rdf:RDF>
<cc:Work rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:title></dc:title>
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

+41
View File
@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
height="125"
id="svg2"
version="1.1"
viewBox="0 0 125 125"
width="125"
xmlns="http://www.w3.org/2000/svg">
<defs id="defs6" />
<g id="layer1">
<circle
style="fill:none;fill-opacity:1;stroke:#a31a1f;stroke-width:5.03185225;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
cx="62.5"
cy="62.5"
id="path12"
r="59.984074" />
<circle
style="fill:#9a191d;fill-opacity:1;stroke:none;stroke-width:6.9172821;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
cx="62.5"
cy="62.5"
id="path835"
r="50" />
<circle
style="fill:#ffffff;fill-opacity:1;stroke:#7e3b3d;stroke-width:1.62162137;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
cx="62.5"
cy="61.500004"
id="path831"
r="9.189189" />
</g>
<metadata id="metadata8">
<rdf:RDF>
<cc:Work rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:title></dc:title>
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

+55
View File
@@ -0,0 +1,55 @@
<?xml version="1.0" encoding="UTF-8"?><!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
height="125"
version="1.1"
viewBox="0 0 125 125"
width="125"
xmlns="http://www.w3.org/2000/svg">
<circle
cx="62.5"
cy="62.5"
fill="none"
r="59.984"
stroke="#a31a1f"
stroke-width="5.0319" />
<circle
cx="62.5"
cy="-62.5"
fill="url(#linearGradient934)"
r="50"
transform="rotate(90)" />
<circle
cx="62.5"
cy="61.5"
fill="#fff"
r="9.1892"
stroke="#7e3b3d"
stroke-width="1.6216" />
<defs>
<linearGradient
gradientUnits="userSpaceOnUse"
id="linearGradient934"
x1="12.5"
x2="112.5"
y1="-62.5"
y2="-62.5">
<stop
offset="0"
stop-color="#721212" />
<stop
offset="1"
stop-color="#a21a1f" />
</linearGradient>
</defs>
<metadata>
<rdf:RDF>
<cc:Work rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:title />
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

+54
View File
@@ -0,0 +1,54 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:osb="http://www.openswatchbook.org/uri/2009/osb"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
height="125"
id="svg2"
version="1.1"
viewBox="0 0 125 125"
width="125"
xmlns="http://www.w3.org/2000/svg">
<defs id="defs6">
<linearGradient
id="linearGradient884"
osb:paint="solid">
<stop
style="stop-color:#969696;stop-opacity:1;"
id="stop882"
offset="0" />
</linearGradient>
</defs>
<g id="layer1">
<circle
style="fill:#484848;fill-opacity:1;stroke:none;stroke-width:6.9172821;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
cx="62.5"
cy="62.5"
id="path835"
r="50" />
<ellipse
style="fill:none;fill-opacity:1;stroke:#555555;stroke-width:5.03194237;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
cx="62.502243"
cy="63.029659"
id="path12"
rx="59.986229"
ry="59.984074"
transform="matrix(0.99996409,-0.00847427,0,1,0,0)" />
</g>
<metadata id="metadata8">
<rdf:RDF>
<cc:Work rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:title></dc:title>
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<rect
style="opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#414141;stroke-width:1.04023623;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
height="17.459764"
id="rect844"
width="17.459761"
x="53.020119"
y="54.020119" />
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

+39
View File
@@ -0,0 +1,39 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
height="125"
version="1.1"
viewBox="0 0 125 125"
width="125"
xmlns="http://www.w3.org/2000/svg">
<circle
cx="62.5"
cy="62.5"
fill="#2a2a2a"
r="50" />
<ellipse
cx="62.502"
cy="63.03"
fill="none"
rx="59.986"
ry="59.984"
stroke="#2a2a2a"
stroke-width="5.0319"
transform="matrix(.99996 -.0084743 0 1 0 0)" />
<metadata>
<rdf:RDF>
<cc:Work rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:title />
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<rect
fill="#555"
height="15"
width="15"
x="54"
y="56" />
</svg>

After

Width:  |  Height:  |  Size: 1022 B

+59
View File
@@ -0,0 +1,59 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
height="125"
version="1.1"
viewBox="0 0 125 125"
width="125"
xmlns="http://www.w3.org/2000/svg">
<circle
cx="62.5"
cy="-62.5"
fill="url(#linearGradient1053)"
r="50"
transform="rotate(90)" />
<defs>
<linearGradient
gradientTransform="translate(0,-125)"
gradientUnits="userSpaceOnUse"
id="linearGradient1053"
x1="12.5"
x2="112.5"
y1="62.5"
y2="62.5">
<stop
offset="0"
stop-color="#404040" />
<stop
offset="1"
stop-color="#555" />
</linearGradient>
</defs>
<ellipse
cx="62.502"
cy="63.03"
fill="none"
rx="59.986"
ry="59.984"
stroke="#555"
stroke-width="5.0319"
transform="matrix(.99996 -.0084743 0 1 0 0)" />
<metadata>
<rdf:RDF>
<cc:Work rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:title />
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<rect
fill="#dbdbdb"
height="17.46"
stroke="#414141"
stroke-width="1.0402"
width="17.46"
x="53.02"
y="54.02" />
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

+64
View File
@@ -0,0 +1,64 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?><!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
height="48"
id="svg2"
version="1.1"
viewBox="0 0 48 48"
width="48"
xmlns="http://www.w3.org/2000/svg"
inkscape:version="0.92.4 (5da689c313, 2019-01-14)"
sodipodi:docname="ic_track_paused.svg">
<defs id="defs6" />
<metadata id="metadata8">
<rdf:RDF>
<cc:Work rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:title></dc:title>
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<rect
style="fill:#a31a1f;fill-opacity:1;stroke-width:1.01154423"
height="20"
id="rect12"
width="5"
x="15"
y="14" />
<rect
style="fill:#a31a1f;fill-opacity:1;stroke-width:1.01154423"
height="20"
id="rect12-3"
width="5"
x="27"
y="14" />
<sodipodi:namedview
bordercolor="#666666"
borderopacity="1"
gridtolerance="10"
guidetolerance="10"
id="namedview4"
objecttolerance="10"
pagecolor="#ffffff"
showgrid="true"
inkscape:current-layer="svg2"
inkscape:cx="21.262539"
inkscape:cy="30.645051"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-height="1152"
inkscape:window-maximized="0"
inkscape:window-width="2048"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:zoom="9.8333333">
<inkscape:grid
id="grid16"
type="xygrid" />
</sodipodi:namedview>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

+29
View File
@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
height="48"
id="svg2"
version="1.1"
viewBox="0 0 48 48"
width="48"
xmlns="http://www.w3.org/2000/svg">
<defs id="defs6" />
<g
style="display:inline"
id="layer1">
<path
style="display:inline;fill:#a31a1f;fill-opacity:1;stroke-width:0.03607465"
d="m 22.92317,31.9906 c -0.06948,-0.0081 -0.277422,-0.03235 -0.46209,-0.05384 -2.147889,-0.249888 -4.249982,-1.419727 -5.59965,-3.116269 -0.961167,-1.208194 -1.554689,-2.572853 -1.795388,-4.128058 -0.08907,-0.575498 -0.08782,-1.835731 0.0024,-2.415668 0.223761,-1.438345 0.765805,-2.731082 1.632716,-3.893914 0.353204,-0.473771 1.18254,-1.30549 1.649602,-1.654344 0.947391,-0.707615 1.928391,-1.18098 3.033216,-1.463629 0.649731,-0.166221 1.126538,-0.233843 1.827948,-0.259242 1.94015,-0.07026 3.814111,0.517516 5.397189,1.692835 0.484944,0.360035 1.31879,1.186156 1.663737,1.648325 0.890877,1.193617 1.438199,2.499896 1.661736,3.966023 0.07887,0.517263 0.08854,1.773309 0.01737,2.253423 -0.23912,1.612873 -0.846098,3.039393 -1.810255,4.254461 -0.293648,0.370069 -0.946591,1.022292 -1.317069,1.315618 -1.186276,0.939234 -2.594384,1.549011 -4.122093,1.785059 -0.343493,0.05308 -1.524586,0.09902 -1.779366,0.06922 z"
id="path85" />
</g>
<metadata id="metadata8">
<rdf:RDF>
<cc:Work rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:title></dc:title>
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

@@ -0,0 +1,9 @@
Open Tracks – ein GPS-Tracker, der deine Daten für sich behält.
Erfasst deine Kilometer, ohne dass es jemand anderes tut.
Funktionen & Features:
* zeichnet Sport und andere Outdooraktivitäten auf
* unterstützt das Markieren interessanter Orte, inkl. Foto-Funktion
* zeichnet deine Herzfrequenz auf (unterstützt Bluetooth-LE-Sensoren)
* exportiert deine Daten als KML oder GPX
* funktioniert ganz ohne Internet
@@ -0,0 +1 @@
Open Tracks – ein GPS-Tracker, der deine Daten für sich behält.
@@ -0,0 +1,15 @@
v3.0.0: OpenTracks
First release of OpenTracks (i.e., hard fork from MyTracks)
Changes:
- Rebranded from MyTracks to OpenTracks
- Added Bluetooth LE heart rate sensor
- Removed all features requiring Internet access
- Removed Google Maps integration
- Removed use of Google Play services
- Removed ANT+ sensors
- Removed Bluetooth classic sensors
- Removed Protobuf
- Removed calorie estimation and activity estimation
- Removed MyTracks Service API (i.e., no integration with other apps)
@@ -0,0 +1,17 @@
v3.0.1: OpenTracks
Changes:
- Share markers
- Restructured settings
- Removed actionbar back button
- Export: removed photo size settings option
- Export: KML as format is now selectable
Bugfixes:
- export directory is now /OpenTracks/
- landscape mode is not crashing
- fixed KML/KMZ export
Developer:
- Test are executable (but many failing)
- Enabled continuous integration with TravisCI (no tests)
@@ -0,0 +1,14 @@
v3.1.0: OpenTracks
Changes:
- Added show track in map apps (tested with OsmAnd and Maps.ME)
- Added while tracking, show track statistics without unlocking the device
- Removed landscape mode for track detail view (was buggy)
- Removed search suggestions history
Bugfix:
- Request storage permission for import all/export all
Developer:
- Upgrade to Gradle 5.6
- Do not minify release builds
@@ -0,0 +1,16 @@
v3.2.0: OpenTracks
Changes:
- Removed Widget
- Introduced material design icons
- Removed snowboarding and skiing (need new icons)
Bugfix:
- Fixed search functionality
Developer:
- Fixed (some) test cases
- Removed unused parcelable functionality
- Major cleanup and reformatting
- Do not use broadcast for app to widget communication
- stats_content.xml does not include other files
@@ -0,0 +1,7 @@
v3.2.1: OpenTracks
Changes:
- Settings are now on a single page
Developer:
- Settings moved to Androidx compat (uses fragments)
@@ -0,0 +1,10 @@
(tbd): OpenTracks
Changes:
-
Bugfix:
-
Developer:
-
@@ -3,6 +3,6 @@ OpenTracks is a sport tracking application that completely respects your privacy
Features:
* Sport tracking: track your sport and outdoor activities
* Photos and markers: mark interesting locations while tracking
* Heart rate: supports bluetooth LE sensors
* Export data: Export tracks either as KML or GPX
* Heart rate: supports Bluetooth LE sensors
* Export data: export tracks either as KML or GPX
* No Internet access: Internet is not used

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Before

Width:  |  Height:  |  Size: 129 KiB

After

Width:  |  Height:  |  Size: 129 KiB

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 76 KiB

Before

Width:  |  Height:  |  Size: 85 KiB

After

Width:  |  Height:  |  Size: 85 KiB

@@ -0,0 +1 @@
A sport tracker that completely respects your privacy.
@@ -0,0 +1 @@
OpenTracks
Binary file not shown.
+1 -2
View File
@@ -1,6 +1,5 @@
#Wed Jun 19 22:47:37 CEST 2019
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.5-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.1.1-all.zip
Vendored
+74 -46
View File
@@ -1,4 +1,20 @@
#!/usr/bin/env bash
#!/usr/bin/env sh
#
# Copyright 2015 the original author or authors.
#
# 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
#
# https://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.
#
##############################################################################
##
@@ -6,42 +22,6 @@
##
##############################################################################
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS=""
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn ( ) {
echo "$*"
}
die ( ) {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
esac
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
@@ -60,6 +40,46 @@ cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn () {
echo "$*"
}
die () {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
@@ -85,7 +105,7 @@ location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
@@ -105,8 +125,8 @@ if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin, switch paths to Windows format before running java
if $cygwin ; then
# For Cygwin or MSYS, switch paths to Windows format before running java
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
@@ -150,11 +170,19 @@ if $cygwin ; then
esac
fi
# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
function splitJvmOpts() {
JVM_OPTS=("$@")
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
APP_ARGS=$(save "$@")
exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
cd "$(dirname "$0")"
fi
exec "$JAVACMD" "$@"
Vendored
+20 -10
View File
@@ -1,3 +1,19 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@@ -8,14 +24,14 @@
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS=
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
@@ -46,10 +62,9 @@ echo location of your Java installation.
goto fail
:init
@rem Get command-line arguments, handling Windowz variants
@rem Get command-line arguments, handling Windows variants
if not "%OS%" == "Windows_NT" goto win9xME_args
if "%@eval[2+2]" == "4" goto 4NT_args
:win9xME_args
@rem Slurp the command line arguments.
@@ -60,11 +75,6 @@ set _SKIP=2
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
goto execute
:4NT_args
@rem Get arguments from the 4NT Shell from JP Software
set CMD_LINE_ARGS=%$
:execute
@rem Setup the command line
-1
View File
@@ -1 +0,0 @@
A sport tracker that completely respects your privacy
@@ -24,14 +24,14 @@ import android.content.Intent;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.platform.app.InstrumentationRegistry;
import de.dennisguse.opentracks.services.TrackRecordingService;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.util.List;
import de.dennisguse.opentracks.services.TrackRecordingService;
/**
* Tests for the BootReceiver.
*
@@ -41,22 +41,24 @@ import java.util.List;
public class BootReceiverTest {
private static final String SERVICE_NAME = "de.dennisguse.opentracks.services.TrackRecordingService";
private Context context = InstrumentationRegistry.getInstrumentation().getContext();
/**
* Tests the behavior when receive notification which is the phone boot.
*/
@Test
public void testOnReceive_startService() {
// Make sure no TrackRecordingService
Intent stopIntent = new Intent(InstrumentationRegistry.getInstrumentation().getContext(), TrackRecordingService.class);
InstrumentationRegistry.getInstrumentation().getContext().stopService(stopIntent);
Assert.assertFalse(isServiceExisted(InstrumentationRegistry.getInstrumentation().getContext(), SERVICE_NAME));
Intent stopIntent = new Intent(context, TrackRecordingService.class);
context.stopService(stopIntent);
Assert.assertFalse(isServiceExisted(context));
BootReceiver bootReceiver = new BootReceiver();
Intent intent = new Intent();
intent.setAction(Intent.ACTION_BOOT_COMPLETED);
bootReceiver.onReceive(InstrumentationRegistry.getInstrumentation().getContext(), intent);
bootReceiver.onReceive(context, intent);
// Check if the service is started
Assert.assertFalse(isServiceExisted(InstrumentationRegistry.getInstrumentation().getContext(), SERVICE_NAME));
Assert.assertFalse(isServiceExisted(context));
}
/**
@@ -65,33 +67,34 @@ public class BootReceiverTest {
@Test
public void testOnReceive_noStartService() {
// Make sure no TrackRecordingService
Intent stopIntent = new Intent(InstrumentationRegistry.getInstrumentation().getContext(), TrackRecordingService.class);
InstrumentationRegistry.getInstrumentation().getContext().stopService(stopIntent);
Assert.assertFalse(isServiceExisted(InstrumentationRegistry.getInstrumentation().getContext(), SERVICE_NAME));
Intent stopIntent = new Intent(context, TrackRecordingService.class);
context.stopService(stopIntent);
Assert.assertFalse(isServiceExisted(context));
BootReceiver bootReceiver = new BootReceiver();
Intent intent = new Intent();
intent.setAction(Intent.ACTION_BUG_REPORT);
bootReceiver.onReceive(InstrumentationRegistry.getInstrumentation().getContext(), intent);
bootReceiver.onReceive(context, intent);
// Check if the service is not started
Assert.assertFalse(isServiceExisted(InstrumentationRegistry.getInstrumentation().getContext(), SERVICE_NAME));
Assert.assertFalse(isServiceExisted(context));
}
/**
* Checks if a service is started in a context.
*
* @param context the context for checking a service
* @param serviceName the service name to find if existed
* @param context the context for checking a service
*/
private boolean isServiceExisted(Context context, String serviceName) {
private boolean isServiceExisted(Context context) {
ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
Assert.assertNotNull(activityManager);
List<ActivityManager.RunningServiceInfo> serviceList = activityManager.getRunningServices(Integer.MAX_VALUE);
for (int i = 0; i < serviceList.size(); i++) {
RunningServiceInfo serviceInfo = serviceList.get(i);
ComponentName componentName = serviceInfo.service;
if (componentName.getClassName().equals(serviceName)) {
if (componentName.getClassName().equals(SERVICE_NAME)) {
return true;
}
}
@@ -15,13 +15,12 @@
*/
package de.dennisguse.opentracks;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.platform.app.InstrumentationRegistry;
import de.dennisguse.opentracks.R;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
/**
@@ -34,9 +33,9 @@ public class ChartValueSeriesTest {
private ChartValueSeries series;
@Before
protected void setUp() {
public void setUp() {
series = new ChartValueSeries(
InstrumentationRegistry.getInstrumentation().getContext(),
ApplicationProvider.getApplicationContext(),
Integer.MIN_VALUE,
Integer.MAX_VALUE,
new int[]{100, 1000},
@@ -46,6 +45,7 @@ public class ChartValueSeriesTest {
R.color.chart_elevation_border);
}
@Test
public void testInitialConditions() {
Assert.assertEquals(1, series.getInterval());
Assert.assertEquals(0, series.getMinMarkerValue());
@@ -53,11 +53,13 @@ public class ChartValueSeriesTest {
Assert.assertTrue(series.isEnabled());
}
@Test
public void testEnabled() {
series.setEnabled(false);
Assert.assertFalse(series.isEnabled());
}
@Test
public void testSmallUpdates() {
series.update(0);
series.update(10);
@@ -67,6 +69,7 @@ public class ChartValueSeriesTest {
Assert.assertEquals(500, series.getMaxMarkerValue());
}
@Test
public void testBigUpdates() {
series.update(0);
series.update(901);
@@ -76,6 +79,7 @@ public class ChartValueSeriesTest {
Assert.assertEquals(5000, series.getMaxMarkerValue());
}
@Test
public void testNotZeroBasedUpdates() {
series.update(220);
series.update(250);
@@ -23,8 +23,7 @@ import de.dennisguse.opentracks.content.Track;
import de.dennisguse.opentracks.content.sensor.SensorDataSet;
/**
* Commons utilities for creating stubs of track, location. The class will be
* enriched if needs more similar stubs for test.
* Commons utilities for creating stubs of track, location.
*
* @author Youtao Liu
*/
@@ -32,14 +31,14 @@ public class TrackStubUtils {
public static final double INITIAL_ALTITUDE = 22;
public static final long INITIAL_TIME = 1000L;
static final String LOCATION_PROVIDER = "gps";
static final double INITIAL_LATITUDE = 22;
static final double INITIAL_LONGITUDE = 22;
static final float INITIAL_ACCURACY = 5;
static final float INITIAL_SPEED = 10;
static final float INITIAL_BEARING = 3.0f;
private static final String LOCATION_PROVIDER = "gps";
private static final double INITIAL_LATITUDE = 22;
private static final double INITIAL_LONGITUDE = 22;
private static final float INITIAL_ACCURACY = 5;
private static final float INITIAL_SPEED = 10;
private static final float INITIAL_BEARING = 3.0f;
// Used to change the value of latitude, longitude, and altitude.
static final double DIFFERENCE = 0.01;
private static final double DIFFERENCE = 0.01;
/**
* Gets a a {@link Track} stub with specified number of locations.
@@ -50,8 +49,7 @@ public class TrackStubUtils {
public static Track createTrack(int numberOfLocations) {
Track track = new Track();
for (int i = 0; i < numberOfLocations; i++) {
track.addLocation(createSensorDataSetLocation(INITIAL_LATITUDE + i * DIFFERENCE, INITIAL_LONGITUDE
+ i * DIFFERENCE, INITIAL_ALTITUDE + i * DIFFERENCE));
track.addLocation(createSensorDataSetLocation(INITIAL_LATITUDE + i * DIFFERENCE, INITIAL_LONGITUDE + i * DIFFERENCE, INITIAL_ALTITUDE + i * DIFFERENCE));
}
return track;
@@ -71,8 +69,7 @@ public class TrackStubUtils {
*
* @return a SensorDataSetLocation stub.
*/
public static SensorDataSetLocation createSensorDataSetLocation(double latitude, double longitude,
double altitude) {
private static SensorDataSetLocation createSensorDataSetLocation(double latitude, double longitude, double altitude) {
// Initial Location
Location loc = new Location(LOCATION_PROVIDER);
loc.setLatitude(latitude);
@@ -82,9 +79,8 @@ public class TrackStubUtils {
loc.setSpeed(INITIAL_SPEED);
loc.setTime(INITIAL_TIME);
loc.setBearing(INITIAL_BEARING);
SensorDataSet sd = new SensorDataSet(100, 100);
SensorDataSet sd = new SensorDataSet(Float.NaN, Float.NaN);
return new SensorDataSetLocation(loc, sd);
}
}
@@ -16,41 +16,43 @@
package de.dennisguse.opentracks.content;
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.net.Uri;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.platform.app.InstrumentationRegistry;
import de.dennisguse.opentracks.content.CustomContentProvider.DatabaseHelper;
import androidx.test.core.app.ApplicationProvider;
import org.junit.Assert;
import org.junit.Before;
import org.junit.runner.RunWith;
import org.junit.Test;
import de.dennisguse.opentracks.content.CustomContentProvider.DatabaseHelper;
/**
* Tests {@link CustomContentProvider}.
*
* @author Youtao Liu
*/
@RunWith(AndroidJUnit4.class)
public class CustomContentProviderTest {
private static final String DATABASE_NAME = "test.db";
private SQLiteDatabase db;
private CustomContentProvider customContentProvider;
private Context context = ApplicationProvider.getApplicationContext();
@Before
protected void setUp() {
InstrumentationRegistry.getInstrumentation().getContext().deleteDatabase(DATABASE_NAME);
db = (new DatabaseHelper(InstrumentationRegistry.getInstrumentation().getContext(), DATABASE_NAME)).getWritableDatabase();
customContentProvider = new CustomContentProvider();
public void setUp() {
context.deleteDatabase(DATABASE_NAME);
db = (new DatabaseHelper(context, DATABASE_NAME)).getWritableDatabase();
customContentProvider = new CustomContentProvider() {
};
}
/**
* Tests {@link CustomContentProvider.DatabaseHelper#onCreate(SQLiteDatabase)}.
*/
@Test
public void testDatabaseHelper_OnCreate() {
Assert.assertTrue(hasTable(TracksColumns.TABLE_NAME));
Assert.assertTrue(hasTable(TrackPointsColumns.TABLE_NAME));
@@ -60,19 +62,19 @@ public class CustomContentProviderTest {
/**
* Tests {@link CustomContentProvider#onCreate(android.content.Context)}.
*/
@Test
public void testOnCreate() {
Assert.assertTrue(customContentProvider.onCreate(InstrumentationRegistry.getInstrumentation().getContext()));
Assert.assertTrue(customContentProvider.onCreate(context));
}
/**
* Tests {@link CustomContentProvider#getType(Uri)}.
*/
@Test
public void testGetType() {
Assert.assertEquals(TracksColumns.CONTENT_TYPE, customContentProvider.getType(TracksColumns.CONTENT_URI));
Assert.assertEquals(
TrackPointsColumns.CONTENT_TYPE, customContentProvider.getType(TrackPointsColumns.CONTENT_URI));
Assert.assertEquals(
WaypointsColumns.CONTENT_TYPE, customContentProvider.getType(WaypointsColumns.CONTENT_URI));
Assert.assertEquals(TrackPointsColumns.CONTENT_TYPE, customContentProvider.getType(TrackPointsColumns.CONTENT_URI));
Assert.assertEquals(WaypointsColumns.CONTENT_TYPE, customContentProvider.getType(WaypointsColumns.CONTENT_URI));
}
/**
@@ -117,7 +119,7 @@ public class CustomContentProviderTest {
try {
db.execSQL("SElECT count(*) from " + table + " order by " + column);
} catch (Exception e) {
if (e.getMessage().indexOf("no such column") > -1) {
if (e.getMessage().contains("no such column")) {
return false;
}
}
@@ -137,7 +139,7 @@ public class CustomContentProviderTest {
createTable(TrackPointsColumns.TABLE_NAME);
createTable(WaypointsColumns.TABLE_NAME);
DatabaseHelper databaseHelper = new DatabaseHelper(InstrumentationRegistry.getInstrumentation().getContext());
DatabaseHelper databaseHelper = new DatabaseHelper(context);
databaseHelper.onUpgrade(db, oldVersion, CustomContentProvider.DATABASE_VERSION);
}
}
@@ -16,31 +16,32 @@
package de.dennisguse.opentracks.content;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.location.Location;
import android.test.RenamingDelegatingContext;
import android.test.mock.MockContentResolver;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.core.app.ApplicationProvider;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import de.dennisguse.opentracks.content.ContentProviderUtils.LocationFactory;
import de.dennisguse.opentracks.content.ContentProviderUtils.LocationIterator;
import de.dennisguse.opentracks.content.Waypoint.WaypointType;
import de.dennisguse.opentracks.services.TrackRecordingServiceTest.MockContext;
import de.dennisguse.opentracks.stats.TripStatistics;
import com.google.android.testing.mocking.AndroidMock;
import com.google.android.testing.mocking.UsesMocks;
import org.junit.Assert;
import org.junit.Before;
import org.junit.runner.RunWith;
import java.util.ArrayList;
import java.util.List;
import java.util.Vector;
import java.util.concurrent.atomic.AtomicInteger;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
* A unit test for {@link ContentProviderUtilsImpl}.
@@ -48,37 +49,37 @@ import java.util.concurrent.atomic.AtomicInteger;
* @author Bartlomiej Niechwiej
* @author Youtao Liu
*/
@RunWith(AndroidJUnit4.class)
@RunWith(MockitoJUnitRunner.class)
public class CustomContentProviderUtilsImplTest {
private static final String NAME_PREFIX = "test name";
private static final String MOCK_DESC = "Mock Next Waypoint Desc!";
private static final String TEST_DESC = "Test Desc!";
private static final String TEST_DESC_NEW = "Test Desc new!";
private Context context;
private static final double INITIAL_LATITUDE = 37.0;
private static final double INITIAL_LONGITUDE = -57.0;
private static final double ALTITUDE_INTERVAL = 2.5;
private Context context = ApplicationProvider.getApplicationContext();
private ContentProviderUtils providerUtils;
private double INITIAL_LATITUDE = 37.0;
private double INITIAL_LONGITUDE = -57.0;
private double ALTITUDE_INTERVAL = 2.5;
@Mock
private Cursor cursorMock;
@Mock
private ContentResolver contentResolverMock;
@Before
protected void setUp() {
MockContentResolver mockContentResolver = new MockContentResolver();
RenamingDelegatingContext targetContext = new RenamingDelegatingContext(
getContext(), getContext(), "test.");
context = new MockContext(mockContentResolver, targetContext);
CustomContentProvider provider = new CustomContentProvider();
provider.attachInfo(context, null);
mockContentResolver.addProvider(ContentProviderUtils.AUTHORITY, provider);
setContext(context);
public void setUp() {
providerUtils = ContentProviderUtils.Factory.get(context);
providerUtils.deleteAllTracks(context);
}
@Test
public void testLocationIterator_noPoints() {
testIterator(1, 0, 1, false, ContentProviderUtils.DEFAULT_LOCATION_FACTORY);
}
@Test
public void testLocationIterator_customFactory() {
final Location location = new Location("test_location");
final AtomicInteger counter = new AtomicInteger();
@@ -93,47 +94,50 @@ public class CustomContentProviderUtilsImplTest {
Assert.assertEquals(15, counter.get());
}
@Test
public void testLocationIterator_nullFactory() {
try {
testIterator(1, 15, 4, false, null);
fail("Expecting IllegalArgumentException");
Assert.fail("Expecting IllegalArgumentException");
} catch (IllegalArgumentException e) {
// Expected.
}
}
@Test
public void testLocationIterator_noBatchAscending() {
testIterator(1, 50, 100, false, ContentProviderUtils.DEFAULT_LOCATION_FACTORY);
testIterator(2, 50, 50, false, ContentProviderUtils.DEFAULT_LOCATION_FACTORY);
}
@Test
public void testLocationIterator_noBatchDescending() {
testIterator(1, 50, 100, true, ContentProviderUtils.DEFAULT_LOCATION_FACTORY);
testIterator(2, 50, 50, true, ContentProviderUtils.DEFAULT_LOCATION_FACTORY);
}
@Test
public void testLocationIterator_batchAscending() {
testIterator(1, 50, 11, false, ContentProviderUtils.DEFAULT_LOCATION_FACTORY);
testIterator(2, 50, 25, false, ContentProviderUtils.DEFAULT_LOCATION_FACTORY);
}
@Test
public void testLocationIterator_batchDescending() {
testIterator(1, 50, 11, true, ContentProviderUtils.DEFAULT_LOCATION_FACTORY);
testIterator(2, 50, 25, true, ContentProviderUtils.DEFAULT_LOCATION_FACTORY);
}
@Test
public void testLocationIterator_largeTrack() {
testIterator(1, 20000, 2000, false, ContentProviderUtils.DEFAULT_LOCATION_FACTORY);
}
private List<Location> testIterator(long trackId, int numPoints, int batchSize,
boolean descending, LocationFactory locationFactory) {
private List<Location> testIterator(long trackId, int numPoints, int batchSize, boolean descending, LocationFactory locationFactory) {
long lastPointId = initializeTrack(trackId, numPoints);
((ContentProviderUtilsImpl) providerUtils).setDefaultCursorBatchSize(batchSize);
List<Location> locations = new ArrayList<Location>(numPoints);
LocationIterator it = providerUtils.getTrackPointLocationIterator(
trackId, -1L, descending, locationFactory);
try {
try (LocationIterator it = providerUtils.getTrackPointLocationIterator(trackId, -1L, descending, locationFactory)) {
while (it.hasNext()) {
Location loc = it.next();
Assert.assertNotNull(loc);
@@ -143,8 +147,6 @@ public class CustomContentProviderUtilsImplTest {
: lastPointId - numPoints + locations.size(), it.getLocationId());
}
Assert.assertEquals(numPoints, locations.size());
} finally {
it.close();
}
return locations;
}
@@ -172,16 +174,12 @@ public class CustomContentProviderUtilsImplTest {
// Load all inserted locations.
long lastPointId = -1;
int counter = 0;
LocationIterator it = providerUtils.getTrackPointLocationIterator(id, -1L, false,
ContentProviderUtils.DEFAULT_LOCATION_FACTORY);
try {
try (LocationIterator it = providerUtils.getTrackPointLocationIterator(id, -1L, false, ContentProviderUtils.DEFAULT_LOCATION_FACTORY)) {
while (it.hasNext()) {
it.next();
lastPointId = it.getLocationId();
counter++;
}
} finally {
it.close();
}
Assert.assertTrue(numPoints == 0 || lastPointId > 0);
@@ -194,37 +192,35 @@ public class CustomContentProviderUtilsImplTest {
/**
* Tests the method {@link ContentProviderUtilsImpl#createTrack(Cursor)}.
*/
@UsesMocks(Cursor.class)
@Test
public void testCreateTrack() {
Cursor cursorMock = AndroidMock.createNiceMock(Cursor.class);
int startColumnIndex = 1;
int columnIndex = startColumnIndex;
AndroidMock.expect(cursorMock.getColumnIndexOrThrow(TracksColumns._ID))
.andReturn(columnIndex++);
AndroidMock.expect(cursorMock.getColumnIndexOrThrow(TracksColumns.NAME)).andReturn(
columnIndex++);
when(cursorMock.getColumnIndexOrThrow(TracksColumns._ID)).thenReturn(columnIndex++);
when(cursorMock.getColumnIndexOrThrow(TracksColumns.NAME)).thenReturn(columnIndex++);
columnIndex = startColumnIndex;
// Id
AndroidMock.expect(cursorMock.isNull(columnIndex++)).andReturn(false);
when(cursorMock.isNull(columnIndex++)).thenReturn(false);
// Name
AndroidMock.expect(cursorMock.isNull(columnIndex++)).andReturn(false);
when(cursorMock.isNull(columnIndex++)).thenReturn(false);
long trackId = System.currentTimeMillis();
columnIndex = startColumnIndex;
// Id
AndroidMock.expect(cursorMock.getLong(columnIndex++)).andReturn(trackId);
when(cursorMock.getLong(columnIndex++)).thenReturn(trackId);
// Name
String name = NAME_PREFIX + trackId;
AndroidMock.expect(cursorMock.getString(columnIndex++)).andReturn(name);
AndroidMock.replay(cursorMock);
when(cursorMock.getString(columnIndex++)).thenReturn(name);
Track track = providerUtils.createTrack(cursorMock);
Assert.assertEquals(trackId, track.getId());
Assert.assertEquals(name, track.getName());
AndroidMock.verify(cursorMock);
verify(cursorMock);
}
/**
* Tests the method {@link ContentProviderUtilsImpl#deleteAllTracks(Context)}
*/
@Test
public void testDeleteAllTracks() {
// Insert track, points and waypoint at first.
long trackId = System.currentTimeMillis();
@@ -233,32 +229,27 @@ public class CustomContentProviderUtilsImplTest {
Waypoint waypoint = new Waypoint();
providerUtils.insertWaypoint(waypoint);
ContentResolver contentResolver = context.getContentResolver();
Cursor tracksCursor = contentResolver.query(TracksColumns.CONTENT_URI, null, null, null,
TracksColumns._ID);
Cursor tracksCursor = contentResolver.query(TracksColumns.CONTENT_URI, null, null, null, TracksColumns._ID);
Assert.assertEquals(1, tracksCursor.getCount());
Cursor tracksPointsCursor = contentResolver.query(TrackPointsColumns.CONTENT_URI, null, null,
null, TrackPointsColumns._ID);
Cursor tracksPointsCursor = contentResolver.query(TrackPointsColumns.CONTENT_URI, null, null, null, TrackPointsColumns._ID);
Assert.assertEquals(10, tracksPointsCursor.getCount());
Cursor waypointCursor = contentResolver.query(WaypointsColumns.CONTENT_URI, null, null,
null, WaypointsColumns._ID);
Cursor waypointCursor = contentResolver.query(WaypointsColumns.CONTENT_URI, null, null, null, WaypointsColumns._ID);
Assert.assertEquals(1, waypointCursor.getCount());
// Delete all.
providerUtils.deleteAllTracks(context);
// Check whether all have been deleted.
tracksCursor = contentResolver.query(TracksColumns.CONTENT_URI, null, null, null,
TracksColumns._ID);
tracksCursor = contentResolver.query(TracksColumns.CONTENT_URI, null, null, null, TracksColumns._ID);
Assert.assertEquals(0, tracksCursor.getCount());
tracksPointsCursor = contentResolver.query(TrackPointsColumns.CONTENT_URI, null, null,
null, TrackPointsColumns._ID);
tracksPointsCursor = contentResolver.query(TrackPointsColumns.CONTENT_URI, null, null, null, TrackPointsColumns._ID);
Assert.assertEquals(0, tracksPointsCursor.getCount());
waypointCursor = contentResolver.query(WaypointsColumns.CONTENT_URI, null, null,
null, WaypointsColumns._ID);
waypointCursor = contentResolver.query(WaypointsColumns.CONTENT_URI, null, null, null, WaypointsColumns._ID);
Assert.assertEquals(0, waypointCursor.getCount());
}
/**
* Tests the method {@link ContentProviderUtilsImpl#deleteTrack(Context, long)}.
*/
@Test
public void testDeleteTrack() {
// Insert three tracks, points of two tracks and way point of one track.
long trackId = System.currentTimeMillis();
@@ -273,32 +264,27 @@ public class CustomContentProviderUtilsImplTest {
providerUtils.insertWaypoint(waypoint);
ContentResolver contentResolver = context.getContentResolver();
Cursor tracksCursor = contentResolver.query(TracksColumns.CONTENT_URI, null, null, null,
TracksColumns._ID);
Cursor tracksCursor = contentResolver.query(TracksColumns.CONTENT_URI, null, null, null, TracksColumns._ID);
Assert.assertEquals(3, tracksCursor.getCount());
Cursor tracksPointsCursor = contentResolver.query(TrackPointsColumns.CONTENT_URI, null, null,
null, TrackPointsColumns._ID);
Cursor tracksPointsCursor = contentResolver.query(TrackPointsColumns.CONTENT_URI, null, null, null, TrackPointsColumns._ID);
Assert.assertEquals(20, tracksPointsCursor.getCount());
Cursor waypointCursor = contentResolver.query(WaypointsColumns.CONTENT_URI, null, null,
null, WaypointsColumns._ID);
Cursor waypointCursor = contentResolver.query(WaypointsColumns.CONTENT_URI, null, null, null, WaypointsColumns._ID);
Assert.assertEquals(1, waypointCursor.getCount());
// Delete one track.
providerUtils.deleteTrack(context, trackId);
// Check whether all data of a track has been deleted.
tracksCursor = contentResolver.query(TracksColumns.CONTENT_URI, null, null, null,
TracksColumns._ID);
tracksCursor = contentResolver.query(TracksColumns.CONTENT_URI, null, null, null, TracksColumns._ID);
Assert.assertEquals(2, tracksCursor.getCount());
tracksPointsCursor = contentResolver.query(TrackPointsColumns.CONTENT_URI, null, null,
null, TrackPointsColumns._ID);
tracksPointsCursor = contentResolver.query(TrackPointsColumns.CONTENT_URI, null, null, null, TrackPointsColumns._ID);
Assert.assertEquals(20, tracksPointsCursor.getCount());
waypointCursor = contentResolver.query(WaypointsColumns.CONTENT_URI, null, null,
null, WaypointsColumns._ID);
waypointCursor = contentResolver.query(WaypointsColumns.CONTENT_URI, null, null, null, WaypointsColumns._ID);
Assert.assertEquals(0, waypointCursor.getCount());
}
/**
* Tests the method {@link ContentProviderUtilsImpl#getAllTracks()}
*/
@Test
public void testGetAllTracks() {
int initialTrackNumber = providerUtils.getAllTracks().size();
long trackId = System.currentTimeMillis();
@@ -311,6 +297,7 @@ public class CustomContentProviderUtilsImplTest {
/**
* Tests the method {@link ContentProviderUtilsImpl#getLastTrack()}
*/
@Test
public void testGetLastTrack() {
long trackId = System.currentTimeMillis();
providerUtils.insertTrack(getTrack(trackId, 0));
@@ -320,6 +307,7 @@ public class CustomContentProviderUtilsImplTest {
/**
* Tests the method {@link ContentProviderUtilsImpl#getTrack(long)}
*/
@Test
public void testGetTrack() {
long trackId = System.currentTimeMillis();
providerUtils.insertTrack(getTrack(trackId, 0));
@@ -329,6 +317,7 @@ public class CustomContentProviderUtilsImplTest {
/**
* Tests the method {@link ContentProviderUtilsImpl#updateTrack(Track)}
*/
@Test
public void testUpdateTrack() {
long trackId = System.currentTimeMillis();
Track track = getTrack(trackId, 0);
@@ -345,6 +334,7 @@ public class CustomContentProviderUtilsImplTest {
/**
* Tests the method {@link ContentProviderUtilsImpl#createContentValues(Waypoint)}.
*/
@Test
public void testCreateContentValues_waypoint() {
long trackId = System.currentTimeMillis();
Track track = getTrack(trackId, 10);
@@ -372,16 +362,15 @@ public class CustomContentProviderUtilsImplTest {
waypoint.setType(WaypointType.STATISTICS);
waypoint.setTripStatistics(statistics);
Location loc = new Location("test");
loc.setLatitude(22);
loc.setLongitude(22);
loc.setAccuracy((float) 1 / 100.0f);
loc.setAltitude(2.5);
waypoint.setLocation(loc);
Location location = new Location("test");
location.setLatitude(22);
location.setLongitude(22);
location.setAccuracy((float) 1 / 100.0f);
location.setAltitude(2.5);
waypoint.setLocation(location);
providerUtils.insertWaypoint(waypoint);
ContentProviderUtilsImpl contentProviderUtils = new ContentProviderUtilsImpl(
new MockContentResolver());
ContentProviderUtilsImpl contentProviderUtils = new ContentProviderUtilsImpl(contentResolverMock);
long waypointId = System.currentTimeMillis();
waypoint.setId(waypointId);
@@ -396,40 +385,36 @@ public class CustomContentProviderUtilsImplTest {
/**
* Tests the method {@link ContentProviderUtilsImpl#createWaypoint(Cursor)}.
*/
@UsesMocks(Cursor.class)
@Test
public void testCreateWaypoint() {
Cursor cursorMock = AndroidMock.createNiceMock(Cursor.class);
int startColumnIndex = 1;
int columnIndex = startColumnIndex;
AndroidMock.expect(cursorMock.getColumnIndexOrThrow(WaypointsColumns._ID))
.andReturn(columnIndex++);
AndroidMock.expect(cursorMock.getColumnIndexOrThrow(WaypointsColumns.NAME)).andReturn(
columnIndex++);
AndroidMock.expect(cursorMock.getColumnIndexOrThrow(WaypointsColumns.TRACKID)).andReturn(
columnIndex++);
when(cursorMock.getColumnIndexOrThrow(WaypointsColumns._ID)).thenReturn(columnIndex++);
when(cursorMock.getColumnIndexOrThrow(WaypointsColumns.NAME)).thenReturn(columnIndex++);
when(cursorMock.getColumnIndexOrThrow(WaypointsColumns.TRACKID)).thenReturn(columnIndex++);
columnIndex = startColumnIndex;
// Id
AndroidMock.expect(cursorMock.isNull(columnIndex++)).andReturn(false);
when(cursorMock.isNull(columnIndex++)).thenReturn(false);
// Name
AndroidMock.expect(cursorMock.isNull(columnIndex++)).andReturn(false);
when(cursorMock.isNull(columnIndex++)).thenReturn(false);
// trackIdIndex
AndroidMock.expect(cursorMock.isNull(columnIndex++)).andReturn(false);
when(cursorMock.isNull(columnIndex++)).thenReturn(false);
long id = System.currentTimeMillis();
columnIndex = startColumnIndex;
// Id
AndroidMock.expect(cursorMock.getLong(columnIndex++)).andReturn(id);
when(cursorMock.getLong(columnIndex++)).thenReturn(id);
// Name
String name = NAME_PREFIX + id;
AndroidMock.expect(cursorMock.getString(columnIndex++)).andReturn(name);
when(cursorMock.getString(columnIndex++)).thenReturn(name);
// trackIdIndex
long trackId = 11L;
AndroidMock.expect(cursorMock.getLong(columnIndex++)).andReturn(trackId);
AndroidMock.replay(cursorMock);
when(cursorMock.getLong(columnIndex++)).thenReturn(trackId);
Waypoint waypoint = providerUtils.createWaypoint(cursorMock);
Assert.assertEquals(id, waypoint.getId());
Assert.assertEquals(name, waypoint.getName());
Assert.assertEquals(trackId, waypoint.getTrackId());
AndroidMock.verify(cursorMock);
verify(cursorMock);
}
/**
@@ -437,6 +422,7 @@ public class CustomContentProviderUtilsImplTest {
* {@link ContentProviderUtilsImpl#deleteWaypoint(Context, long, DescriptionGenerator)}
* when there is only one waypoint in the track.
*/
@Test
public void testDeleteWaypoint_onlyOneWayPoint() {
long trackId = System.currentTimeMillis();
Track track = getTrack(trackId, 10);
@@ -458,8 +444,7 @@ public class CustomContentProviderUtilsImplTest {
}
@Override
public String generateTrackDescription(Track aTrack, Vector<Double> distances,
Vector<Double> elevations, boolean html) {
public String generateTrackDescription(Track aTrack, boolean html) {
return null;
}
};
@@ -473,6 +458,7 @@ public class CustomContentProviderUtilsImplTest {
* {@link ContentProviderUtilsImpl#deleteWaypoint(Context, long, DescriptionGenerator)}
* when there is more than one waypoint in the track.
*/
@Test
public void testDeleteWaypoint_hasNextWayPoint() {
long trackId = System.currentTimeMillis();
Track track = getTrack(trackId, 10);
@@ -491,19 +477,22 @@ public class CustomContentProviderUtilsImplTest {
statistics.setMaxGrade(15.0);
statistics.setMinGrade(-25.0);
statistics.setBounds(-10000, 20000, 30000, -40000);
// Insert at first.
Waypoint waypoint1 = new Waypoint();
waypoint1.setDescription(Long.toString(trackId));
waypoint1.setTrackId(trackId);
waypoint1.setType(WaypointType.STATISTICS);
waypoint1.setTripStatistics(statistics);
providerUtils.insertWaypoint(waypoint1);
long waypoint1Id = ContentUris.parseId(providerUtils.insertWaypoint(waypoint1));
Waypoint waypoint2 = new Waypoint();
waypoint2.setDescription(Long.toString(trackId));
waypoint2.setTrackId(trackId);
waypoint2.setType(WaypointType.STATISTICS);
waypoint2.setTripStatistics(statistics);
providerUtils.insertWaypoint(waypoint2);
long waypoint2Id = ContentUris.parseId(providerUtils.insertWaypoint(waypoint2));
// Delete
DescriptionGenerator descriptionGenerator = new DescriptionGenerator() {
@@ -513,21 +502,23 @@ public class CustomContentProviderUtilsImplTest {
}
@Override
public String generateTrackDescription(Track aTrack, Vector<Double> distances,
Vector<Double> elevations, boolean html) {
public String generateTrackDescription(Track aTrack, boolean html) {
return null;
}
};
providerUtils.deleteWaypoint(context, 1, descriptionGenerator);
Assert.assertNull(providerUtils.getWaypoint(1));
Assert.assertEquals(CustomContentProviderUtilsImplTest.MOCK_DESC, providerUtils.getWaypoint(2)
Assert.assertNotNull(providerUtils.getWaypoint(waypoint1Id));
providerUtils.deleteWaypoint(context, waypoint1Id, descriptionGenerator);
Assert.assertNull(providerUtils.getWaypoint(waypoint1Id));
Assert.assertEquals(CustomContentProviderUtilsImplTest.MOCK_DESC, providerUtils.getWaypoint(waypoint2Id)
.getDescription());
}
/**
* Tests the method {@link ContentProviderUtilsImpl#getFirstWaypointId(long)}.
*/
@Test
public void testGetFirstWaypointId() {
long trackId = System.currentTimeMillis();
Track track = getTrack(trackId, 10);
@@ -535,18 +526,21 @@ public class CustomContentProviderUtilsImplTest {
Waypoint waypoint1 = new Waypoint();
waypoint1.setTrackId(trackId);
long waypoint1id = ContentUris.parseId(providerUtils.insertWaypoint(waypoint1));
Waypoint waypoint2 = new Waypoint();
waypoint2.setTrackId(trackId);
providerUtils.insertWaypoint(waypoint1);
providerUtils.insertWaypoint(waypoint2);
long waypoint2id = ContentUris.parseId(providerUtils.insertWaypoint(waypoint2));
Assert.assertEquals(-1L, providerUtils.getFirstWaypointId(-1));
Assert.assertEquals(1L, providerUtils.getFirstWaypointId(trackId));
Assert.assertEquals(waypoint1id, providerUtils.getFirstWaypointId(trackId));
Assert.assertEquals(waypoint2id, providerUtils.getLastWaypoint(trackId, WaypointType.WAYPOINT).getId());
}
/**
* Tests the method {@link ContentProviderUtilsImpl#getNextWaypointNumber(long, WaypointType)}.
*/
@Test
public void testGetNextWaypointNumber() {
long trackId = System.currentTimeMillis();
Track track = getTrack(trackId, 10);
@@ -577,6 +571,7 @@ public class CustomContentProviderUtilsImplTest {
* Tests the method
* {@link ContentProviderUtils#getLastWaypoint(long, WaypointType)}.
*/
@Test
public void testGetLastStatisticsWaypoint() {
long trackId = System.currentTimeMillis();
Track track = getTrack(trackId, 10);
@@ -605,6 +600,7 @@ public class CustomContentProviderUtilsImplTest {
* Tests the method {@link ContentProviderUtilsImpl#insertWaypoint(Waypoint)} and
* {@link ContentProviderUtilsImpl#getWaypoint(long)}.
*/
@Test
public void testInsertAndGetWaypoint() {
long trackId = System.currentTimeMillis();
Track track = getTrack(trackId, 10);
@@ -613,14 +609,15 @@ public class CustomContentProviderUtilsImplTest {
Waypoint waypoint = new Waypoint();
waypoint.setDescription(TEST_DESC);
waypoint.setTrackId(trackId);
providerUtils.insertWaypoint(waypoint);
long waypointId = ContentUris.parseId(providerUtils.insertWaypoint(waypoint));
Assert.assertEquals(TEST_DESC, providerUtils.getWaypoint(1).getDescription());
Assert.assertEquals(TEST_DESC, providerUtils.getWaypoint(waypointId).getDescription());
}
/**
* Tests the method {@link ContentProviderUtilsImpl#updateWaypoint(Waypoint)}.
*/
@Test
public void testUpdateWaypoint() {
long trackId = System.currentTimeMillis();
Track track = getTrack(trackId, 10);
@@ -629,19 +626,20 @@ public class CustomContentProviderUtilsImplTest {
Waypoint waypoint = new Waypoint();
waypoint.setDescription(TEST_DESC);
waypoint.setTrackId(trackId);
providerUtils.insertWaypoint(waypoint);
long waypointId = ContentUris.parseId(providerUtils.insertWaypoint(waypoint));
// Update
waypoint = providerUtils.getWaypoint(1);
waypoint = providerUtils.getWaypoint(waypointId);
waypoint.setDescription(TEST_DESC_NEW);
providerUtils.updateWaypoint(waypoint);
Assert.assertEquals(TEST_DESC_NEW, providerUtils.getWaypoint(1).getDescription());
Assert.assertEquals(TEST_DESC_NEW, providerUtils.getWaypoint(waypointId).getDescription());
}
/**
* Tests the method {@link ContentProviderUtilsImpl#bulkInsertTrackPoint(Location[],
* int, long)}.
* Tests the method {@link ContentProviderUtilsImpl#bulkInsertTrackPoint(Location[], int, long)}.
*/
@Test
public void testBulkInsertTrackPoint() {
// Insert track, point at first.
long trackId = System.currentTimeMillis();
@@ -657,74 +655,68 @@ public class CustomContentProviderUtilsImplTest {
/**
* Tests the method {@link ContentProviderUtilsImpl#createTrackPoint(Cursor)}.
*/
@UsesMocks(Cursor.class)
@Test
public void testCreateTrackPoint() {
Cursor cursorMock = AndroidMock.createNiceMock(Cursor.class);
// Set index.
int index = 1;
// Id
AndroidMock.expect(cursorMock.getColumnIndex(TrackPointsColumns._ID)).andReturn(index++);
when(cursorMock.getColumnIndex(TrackPointsColumns._ID)).thenReturn(index++);
// Longitude
AndroidMock.expect(cursorMock.getColumnIndexOrThrow(TrackPointsColumns.LONGITUDE)).andReturn(
index++);
when(cursorMock.getColumnIndexOrThrow(TrackPointsColumns.LONGITUDE)).thenReturn(index++);
// Latitude
AndroidMock.expect(cursorMock.getColumnIndexOrThrow(TrackPointsColumns.LATITUDE)).andReturn(
index++);
when(cursorMock.getColumnIndexOrThrow(TrackPointsColumns.LATITUDE)).thenReturn(index++);
// Time
AndroidMock.expect(cursorMock.getColumnIndexOrThrow(TrackPointsColumns.TIME))
.andReturn(index++);
when(cursorMock.getColumnIndexOrThrow(TrackPointsColumns.TIME))
.thenReturn(index++);
// Speed
AndroidMock.expect(cursorMock.getColumnIndexOrThrow(TrackPointsColumns.SPEED)).andReturn(
index++);
when(cursorMock.getColumnIndexOrThrow(TrackPointsColumns.SPEED)).thenReturn(index++);
// Sensor
AndroidMock.expect(cursorMock.getColumnIndexOrThrow(TrackPointsColumns.SENSOR_HEARTRATE)).andReturn(
index++);
when(cursorMock.getColumnIndexOrThrow(TrackPointsColumns.SENSOR_HEARTRATE)).thenReturn(index++);
// Set return value of isNull().
index = 2;
// Longitude
AndroidMock.expect(cursorMock.isNull(index++)).andReturn(false);
when(cursorMock.isNull(index++)).thenReturn(false);
// Latitude
AndroidMock.expect(cursorMock.isNull(index++)).andReturn(false);
when(cursorMock.isNull(index++)).thenReturn(false);
// Time
AndroidMock.expect(cursorMock.isNull(index++)).andReturn(false);
when(cursorMock.isNull(index++)).thenReturn(false);
// Speed
AndroidMock.expect(cursorMock.isNull(index++)).andReturn(false);
when(cursorMock.isNull(index++)).thenReturn(false);
// Sensor
AndroidMock.expect(cursorMock.isNull(index++)).andReturn(false);
when(cursorMock.isNull(index++)).thenReturn(false);
// Set return value of isNull().
index = 2;
// Longitude
int longitude = 11;
AndroidMock.expect(cursorMock.getInt(index++)).andReturn(longitude * 1000000);
when(cursorMock.getInt(index++)).thenReturn(longitude * 1000000);
// Latitude.
int latitude = 22;
AndroidMock.expect(cursorMock.getInt(index++)).andReturn(latitude * 1000000);
when(cursorMock.getInt(index++)).thenReturn(latitude * 1000000);
// Time
long time = System.currentTimeMillis();
AndroidMock.expect(cursorMock.getLong(index++)).andReturn(time);
when(cursorMock.getLong(index++)).thenReturn(time);
// Speed
float speed = 2.2f;
AndroidMock.expect(cursorMock.getFloat(index++)).andReturn(speed);
when(cursorMock.getFloat(index++)).thenReturn(speed);
// Sensor
byte[] sensor = "Sensor state".getBytes();
AndroidMock.expect(cursorMock.getBlob(index++)).andReturn(sensor);
when(cursorMock.getBlob(index++)).thenReturn(sensor);
AndroidMock.replay(cursorMock);
Location location = providerUtils.createTrackPoint(cursorMock);
Assert.assertEquals((double) longitude, location.getLongitude());
Assert.assertEquals((double) latitude, location.getLatitude());
Assert.assertEquals(time, location.getTime());
Assert.assertEquals(speed, location.getSpeed());
AndroidMock.verify(cursorMock);
Assert.assertEquals((double) longitude, location.getLongitude(), 0.01);
Assert.assertEquals((double) latitude, location.getLatitude(), 0.01);
Assert.assertEquals(time, location.getTime(), 0.01);
Assert.assertEquals(speed, location.getSpeed(), 0.01);
verify(cursorMock);
}
/**
* Tests the method
* {@link ContentProviderUtilsImpl#insertTrackPoint(Location, long)}.
*/
@Test
public void testInsertTrackPoint() {
// Insert track, point at first.
long trackId = System.currentTimeMillis();
@@ -738,30 +730,33 @@ public class CustomContentProviderUtilsImplTest {
/**
* Tests the method {@link ContentProviderUtilsImpl#getFirstTrackPointId(long)}.
*/
@Test
public void testGetFirstTrackPointId() {
// Insert track, point at first.
long trackId = System.currentTimeMillis();
Track track = getTrack(trackId, 10);
insertTrackWithLocations(track);
Assert.assertEquals(1, providerUtils.getFirstTrackPointId(trackId));
Assert.assertNotEquals(-1L, providerUtils.getFirstTrackPointId(trackId));
}
/**
* Tests the method {@link ContentProviderUtilsImpl#getLastTrackPointId(long)}.
*/
@Test
public void testGetLastTrackPointId() {
// Insert track, point at first.
long trackId = System.currentTimeMillis();
Track track = getTrack(trackId, 10);
insertTrackWithLocations(track);
Assert.assertEquals(10, providerUtils.getLastTrackPointId(trackId));
Assert.assertTrue(providerUtils.getFirstTrackPointId(trackId) < providerUtils.getLastTrackPointId(trackId));
}
/**
* Tests the method {@link ContentProviderUtilsImpl#getLastValidTrackPoint(long)}.
*/
@Test
public void testGetLastValidTrackPoint() {
// Insert track, points at first.
long trackId = System.currentTimeMillis();
@@ -773,79 +768,93 @@ public class CustomContentProviderUtilsImplTest {
}
/**
* Tests the method
* {@link ContentProviderUtilsImpl#getTrackPointCursor(long, long, int, boolean)}
* in descending.
* Tests the method {@link ContentProviderUtilsImpl#getTrackPointCursor(long, long, int, boolean)} in descending.
*/
@Test
public void testGetTrackPointCursor_desc() {
// Insert track, points at first.
long trackId = System.currentTimeMillis();
Track track = getTrack(trackId, 10);
insertTrackWithLocations(track);
providerUtils.insertTrack(track);
Cursor cursor = providerUtils.getTrackPointCursor(trackId, 2L, 5, true);
long[] trackpointIds = new long[track.getNumberOfPoints()];
for (int i = 0; i < trackpointIds.length; i++) {
trackpointIds[i] = ContentUris.parseId(providerUtils.insertTrackPoint(track.getLocations().get(i), track.getId()));
}
Cursor cursor = providerUtils.getTrackPointCursor(trackId, trackpointIds[1], 5, true);
Assert.assertEquals(2, cursor.getCount());
}
/**
* Tests the method
* {@link ContentProviderUtilsImpl#getTrackPointCursor(long, long, int, boolean)}
* in ascending.
* Tests the method {@link ContentProviderUtilsImpl#getTrackPointCursor(long, long, int, boolean)} in ascending.
*/
@Test
public void testGetTrackPointCursor_asc() {
// Insert track, points at first.
long trackId = System.currentTimeMillis();
Track track = getTrack(trackId, 10);
insertTrackWithLocations(track);
providerUtils.insertTrack(track);
Cursor cursor = providerUtils.getTrackPointCursor(trackId, 2L, 5, false);
Assert.assertEquals(5, cursor.getCount());
long[] trackpointIds = new long[track.getNumberOfPoints()];
for (int i = 0; i < trackpointIds.length; i++) {
trackpointIds[i] = ContentUris.parseId(providerUtils.insertTrackPoint(track.getLocations().get(i), track.getId()));
}
Cursor cursor = providerUtils.getTrackPointCursor(trackId, trackpointIds[8], 5, false);
Assert.assertEquals(2, cursor.getCount());
}
/**
* Tests the method
* {@link ContentProviderUtilsImpl#getTrackPointLocationIterator(long, long, boolean, LocationFactory)}
* in descending.
* Tests the method {@link ContentProviderUtilsImpl#getTrackPointLocationIterator(long, long, boolean, LocationFactory)} in descending.
*/
@Test
public void testGetTrackPointLocationIterator_desc() {
// Insert track, points at first.
long trackId = System.currentTimeMillis();
Track track = getTrack(trackId, 10);
insertTrackWithLocations(track);
providerUtils.insertTrack(track);
long startTrackPointId = 2L;
long[] trackpointIds = new long[track.getNumberOfPoints()];
for (int i = 0; i < trackpointIds.length; i++) {
trackpointIds[i] = ContentUris.parseId(providerUtils.insertTrackPoint(track.getLocations().get(i), track.getId()));
}
LocationIterator locationIterator = providerUtils.getTrackPointLocationIterator(trackId,
startTrackPointId, true, ContentProviderUtils.DEFAULT_LOCATION_FACTORY);
for (int i = 1; i >= 0; i--) {
long startTrackPointId = trackpointIds[9];
LocationIterator locationIterator = providerUtils.getTrackPointLocationIterator(trackId, startTrackPointId, true, ContentProviderUtils.DEFAULT_LOCATION_FACTORY);
for (int i = 0; i < trackpointIds.length; i++) {
Assert.assertTrue(locationIterator.hasNext());
Location location = locationIterator.next();
Assert.assertEquals(2 + (i - 1), locationIterator.getLocationId());
checkLocation(i, location);
Assert.assertEquals(startTrackPointId - i, locationIterator.getLocationId());
checkLocation((trackpointIds.length - 1) - i, location);
}
Assert.assertFalse(locationIterator.hasNext());
}
/**
* Tests the method
* {@link ContentProviderUtilsImpl#getTrackPointLocationIterator(long, long, boolean, LocationFactory)}
* in ascending.
* Tests the method {@link ContentProviderUtilsImpl#getTrackPointLocationIterator(long, long, boolean, LocationFactory)} in ascending.
*/
@Test
public void testGetTrackPointLocationIterator_asc() {
// Insert track, point at first.
long trackId = System.currentTimeMillis();
Track track = getTrack(trackId, 10);
insertTrackWithLocations(track);
providerUtils.insertTrack(track);
long startTrackPointId = 2L;
long[] trackpointIds = new long[track.getNumberOfPoints()];
for (int i = 0; i < trackpointIds.length; i++) {
trackpointIds[i] = ContentUris.parseId(providerUtils.insertTrackPoint(track.getLocations().get(i), track.getId()));
}
LocationIterator locationIterator = providerUtils.getTrackPointLocationIterator(trackId,
startTrackPointId, false, ContentProviderUtils.DEFAULT_LOCATION_FACTORY);
long startTrackPointId = trackpointIds[0];
for (int i = 1; i < 10; i++) {
LocationIterator locationIterator = providerUtils.getTrackPointLocationIterator(trackId, startTrackPointId, false, ContentProviderUtils.DEFAULT_LOCATION_FACTORY);
for (int i = 0; i < trackpointIds.length; i++) {
Assert.assertTrue(locationIterator.hasNext());
Location location = locationIterator.next();
Assert.assertEquals(2 + (i - 1), locationIterator.getLocationId());
Assert.assertEquals(startTrackPointId + i, locationIterator.getLocationId());
checkLocation(i, location);
}
Assert.assertFalse(locationIterator.hasNext());
@@ -876,7 +885,6 @@ public class CustomContentProviderUtilsImplTest {
* @return created location
*/
private Location createLocation(int i) {
Location loc = new Location("test");
loc.setLatitude(INITIAL_LATITUDE + (double) i / 10000.0);
loc.setLongitude(INITIAL_LONGITUDE - (double) i / 10000.0);
@@ -888,15 +896,14 @@ public class CustomContentProviderUtilsImplTest {
/**
* Checks the value of a location.
*
* @param i the index of this location which created in the method
* {@link CustomContentProviderUtilsImplTest#getTrack(long, int)}
* @param i the index of this location which created in the method {@link CustomContentProviderUtilsImplTest#getTrack(long, int)}
* @param location the location to be checked
*/
private void checkLocation(int i, Location location) {
Assert.assertEquals(INITIAL_LATITUDE + (double) i / 10000.0, location.getLatitude());
Assert.assertEquals(INITIAL_LONGITUDE - (double) i / 10000.0, location.getLongitude());
Assert.assertEquals((float) i / 100.0f, location.getAccuracy());
Assert.assertEquals(i * ALTITUDE_INTERVAL, location.getAltitude());
Assert.assertEquals(INITIAL_LATITUDE + (double) i / 10000.0, location.getLatitude(), 0.01);
Assert.assertEquals(INITIAL_LONGITUDE - (double) i / 10000.0, location.getLongitude(), 0.01);
Assert.assertEquals((float) i / 100.0f, location.getAccuracy(), 0.01);
Assert.assertEquals(i * ALTITUDE_INTERVAL, location.getAltitude(), 0.01);
}
/**
@@ -906,7 +913,6 @@ public class CustomContentProviderUtilsImplTest {
*/
private void insertTrackWithLocations(Track track) {
providerUtils.insertTrack(track);
providerUtils.bulkInsertTrackPoint(track.getLocations().toArray(new Location[0]), track
.getLocations().size(), track.getId());
providerUtils.bulkInsertTrackPoint(track.getLocations().toArray(new Location[0]), track.getLocations().size(), track.getId());
}
}
@@ -16,17 +16,20 @@
package de.dennisguse.opentracks.content;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.platform.app.InstrumentationRegistry;
import android.content.Context;
import de.dennisguse.opentracks.stats.TripStatistics;
import de.dennisguse.opentracks.util.StringUtils;
import de.dennisguse.opentracks.R;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.stats.TripStatistics;
import de.dennisguse.opentracks.util.StringUtils;
/**
* Tests for {@link DescriptionGeneratorImpl}.
*
@@ -38,15 +41,17 @@ public class DescriptionGeneratorImplTest {
private static final long START_TIME = 1288721514000L;
private DescriptionGeneratorImpl descriptionGenerator;
private Context context = ApplicationProvider.getApplicationContext();
@Before
protected void setUp() throws Exception {
descriptionGenerator = new DescriptionGeneratorImpl(InstrumentationRegistry.getInstrumentation().getContext());
public void setUp() {
descriptionGenerator = new DescriptionGeneratorImpl(ApplicationProvider.getApplicationContext());
}
/**
* Tests {@link DescriptionGeneratorImpl#generateTrackDescription(Track,
* java.util.Vector, java.util.Vector, boolean)}.
* Tests {@link DescriptionGeneratorImpl#generateTrackDescription(Track, boolean)}.
*/
@Test
public void testGenerateTrackDescription() {
Track track = new Track();
TripStatistics stats = new TripStatistics();
@@ -62,8 +67,8 @@ public class DescriptionGeneratorImplTest {
stats.setStartTime(START_TIME);
track.setTripStatistics(stats);
track.setCategory("hiking");
String expected = "Created by"
+ " <a href='TODO'>TODO</a> on Android<p>"
String expected = //"Created by"
"<a href='https://github.com/dennisguse/opentracks'>OpenTracks</a><p>"
+ "Name: -<br>"
+ "Activity type: hiking<br>"
+ "Description: -<br>"
@@ -81,13 +86,15 @@ public class DescriptionGeneratorImplTest {
+ "Elevation gain: 6000 m (19685 ft)<br>"
+ "Max grade: 42 %<br>"
+ "Min grade: 11 %<br>"
+ "Recorded: " + StringUtils.formatDateTime(InstrumentationRegistry.getInstrumentation().getContext(), START_TIME) + "<br>";
Assert.assertEquals(expected, descriptionGenerator.generateTrackDescription(track, null, null, true));
+ "Recorded: " + StringUtils.formatDateTime(context, START_TIME) + "<br>";
Assert.assertEquals(expected, descriptionGenerator.generateTrackDescription(track, true));
}
/**
* Tests {@link DescriptionGeneratorImpl#generateWaypointDescription(TripStatistics)}.
*/
@Test
public void testGenerateWaypointDescription() {
Waypoint waypoint = new Waypoint();
TripStatistics stats = new TripStatistics();
@@ -116,15 +123,14 @@ public class DescriptionGeneratorImplTest {
+ "Elevation gain: 6000 m (19685 ft)\n"
+ "Max grade: 42 %\n"
+ "Min grade: 11 %\n"
+ "Recorded: " + StringUtils.formatDateTime(InstrumentationRegistry.getInstrumentation().getContext(), START_TIME) + "\n";
Assert.assertEquals(
expected, descriptionGenerator.generateWaypointDescription(waypoint.getTripStatistics()));
+ "Recorded: " + StringUtils.formatDateTime(context, START_TIME) + "\n";
Assert.assertEquals(expected, descriptionGenerator.generateWaypointDescription(waypoint.getTripStatistics()));
}
/**
* Tests {@link DescriptionGeneratorImpl#writeDistance(double, StringBuilder,
* int, String)}.
* Tests {@link DescriptionGeneratorImpl#writeDistance(double, StringBuilder, int, String)}.
*/
@Test
public void testWriteDistance() {
StringBuilder builder = new StringBuilder();
descriptionGenerator.writeDistance(1100, builder, R.string.description_total_distance, "<br>");
@@ -132,9 +138,9 @@ public class DescriptionGeneratorImplTest {
}
/**
* Tests {@link DescriptionGeneratorImpl#writeTime(long, StringBuilder, int,
* String)}.
* Tests {@link DescriptionGeneratorImpl#writeTime(long, StringBuilder, int, String)}.
*/
@Test
public void testWriteTime() {
StringBuilder builder = new StringBuilder();
descriptionGenerator.writeTime(1000, builder, R.string.description_total_time, "<br>");
@@ -142,9 +148,9 @@ public class DescriptionGeneratorImplTest {
}
/**
* Tests {@link DescriptionGeneratorImpl#writeSpeed(double, StringBuilder,
* int, String)}.
* Tests {@link DescriptionGeneratorImpl#writeSpeed(double, StringBuilder, int, String)}.
*/
@Test
public void testWriteSpeed() {
StringBuilder builder = new StringBuilder();
descriptionGenerator.writeSpeed(1.1, builder, R.string.description_average_speed, "\n");
@@ -152,9 +158,9 @@ public class DescriptionGeneratorImplTest {
}
/**
* Tests {@link DescriptionGeneratorImpl#writeElevation(double, StringBuilder,
* int, String)}.
* Tests {@link DescriptionGeneratorImpl#writeElevation(double, StringBuilder, int, String)}.
*/
@Test
public void testWriteElevation() {
StringBuilder builder = new StringBuilder();
descriptionGenerator.writeElevation(4.2, builder, R.string.description_min_elevation, "<br>");
@@ -162,9 +168,9 @@ public class DescriptionGeneratorImplTest {
}
/**
* Tests {@link DescriptionGeneratorImpl#writePace(double, StringBuilder, int,
* String)}.
* Tests {@link DescriptionGeneratorImpl#writePace(double, StringBuilder, int, String)}.
*/
@Test
public void testWritePace() {
StringBuilder builder = new StringBuilder();
descriptionGenerator.writePace(1.1, builder, R.string.description_average_pace_in_minute, "\n");
@@ -172,9 +178,9 @@ public class DescriptionGeneratorImplTest {
}
/**
* Tests {@link DescriptionGeneratorImpl#writeGrade(double, StringBuilder,
* int, String)}.
* Tests {@link DescriptionGeneratorImpl#writeGrade(double, StringBuilder, int, String)}.
*/
@Test
public void testWriteGrade() {
StringBuilder builder = new StringBuilder();
descriptionGenerator.writeGrade(.042, builder, R.string.description_max_grade, "<br>");
@@ -182,9 +188,9 @@ public class DescriptionGeneratorImplTest {
}
/**
* Tests {@link DescriptionGeneratorImpl#writeGrade(double, StringBuilder,
* int, String)} with a NaN.
* Tests {@link DescriptionGeneratorImpl#writeGrade(double, StringBuilder, int, String)} with a NaN.
*/
@Test
public void testWriteGrade_nan() {
StringBuilder builder = new StringBuilder();
descriptionGenerator.writeGrade(Double.NaN, builder, R.string.description_max_grade, "<br>");
@@ -192,13 +198,12 @@ public class DescriptionGeneratorImplTest {
}
/**
* Tests {@link DescriptionGeneratorImpl#writeGrade(double, StringBuilder,
* int, String)} with an infinite number.
* Tests {@link DescriptionGeneratorImpl#writeGrade(double, StringBuilder, int, String)} with an infinite number.
*/
@Test
public void testWriteGrade_infinite() {
StringBuilder builder = new StringBuilder();
descriptionGenerator.writeGrade(
Double.POSITIVE_INFINITY, builder, R.string.description_max_grade, "<br>");
descriptionGenerator.writeGrade(Double.POSITIVE_INFINITY, builder, R.string.description_max_grade, "<br>");
Assert.assertEquals("Max grade: 0 %<br>", builder.toString());
}
}
@@ -17,31 +17,31 @@
package de.dennisguse.opentracks.content;
import android.content.ContentUris;
import android.content.Context;
import android.location.Location;
import android.net.Uri;
import android.test.RenamingDelegatingContext;
import android.test.mock.MockContentResolver;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import de.dennisguse.opentracks.content.SearchEngine.ScoredResult;
import de.dennisguse.opentracks.content.SearchEngine.SearchQuery;
import de.dennisguse.opentracks.services.TrackRecordingServiceTest.MockContext;
import de.dennisguse.opentracks.stats.TripStatistics;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import de.dennisguse.opentracks.content.SearchEngine.ScoredResult;
import de.dennisguse.opentracks.content.SearchEngine.SearchQuery;
import de.dennisguse.opentracks.stats.TripStatistics;
/**
* Tests for {@link SearchEngine}.
* These are not meant to be quality tests, but instead feature-by-feature tests
* (in other words, they don't test the mixing of different score boostings, just
* each boosting separately)
* These are not meant to be quality tests, but instead feature-by-feature tests.
* In other words, they don't test the mixing of different score boostings, just each boosting separately.
*
* @author Rodrigo Damazio
*/
@@ -49,28 +49,22 @@ import java.util.List;
public class SearchEngineTest {
private static final Location HERE = new Location("gps");
private static final long NOW = 1234567890000L; // After OLDEST_ALLOWED_TIMESTAMP
private static final long NOW = SearchEngine.OLDEST_ALLOWED_TIMESTAMP + 1000; // After OLDEST_ALLOWED_TIMESTAMP
private ContentProviderUtils providerUtils;
private SearchEngine engine;
@Before
protected void setUp() {
MockContentResolver mockContentResolver = new MockContentResolver();
RenamingDelegatingContext targetContext = new RenamingDelegatingContext(
getContext(), getContext(), "test.");
MockContext context = new MockContext(mockContentResolver, targetContext);
CustomContentProvider provider = new CustomContentProvider();
provider.attachInfo(context, null);
mockContentResolver.addProvider(ContentProviderUtils.AUTHORITY, provider);
setContext(context);
private Context context = ApplicationProvider.getApplicationContext();
@Before
public void setUp() {
providerUtils = ContentProviderUtils.Factory.get(context);
engine = new SearchEngine(providerUtils);
}
@After
protected void tearDown() throws Exception {
providerUtils.deleteAllTracks(getContext());
public void tearDown() throws Exception {
providerUtils.deleteAllTracks(context);
}
private long insertTrack(String title, String description, String category, double distance, long hoursAgo) {
@@ -141,6 +135,7 @@ public class SearchEngineTest {
return insertWaypoint(title, "", "", 0.0, hoursAgo, trackId);
}
@Test
public void testSearchText() {
// Insert 7 tracks (purposefully out of result order):
// - one which won't match
@@ -162,11 +157,10 @@ public class SearchEngineTest {
ArrayList<ScoredResult> results = new ArrayList<ScoredResult>(engine.search(query));
// Title > Description > Category.
assertTrackResults(results,
allMatchId, titleDescriptionMatchId, titleCategoryMatchId, titleMatchId, descriptionMatchId,
categoryMatchId);
assertTrackResults(results, allMatchId, titleDescriptionMatchId, titleCategoryMatchId, titleMatchId, descriptionMatchId, categoryMatchId);
}
@Test
public void testSearchWaypointText() {
// Insert 7 waypoints (purposefully out of result order):
// - one which won't match
@@ -188,11 +182,10 @@ public class SearchEngineTest {
ArrayList<ScoredResult> results = new ArrayList<ScoredResult>(engine.search(query));
// Title > Description > Category.
assertWaypointResults(results,
allMatchId, titleDescriptionMatchId, titleCategoryMatchId, titleMatchId, descriptionMatchId,
categoryMatchId);
assertWaypointResults(results, allMatchId, titleDescriptionMatchId, titleCategoryMatchId, titleMatchId, descriptionMatchId, categoryMatchId);
}
@Test
public void testSearchMixedText() {
// Insert 5 entries (purposefully out of result order):
// - one waypoint which will match by description
@@ -210,12 +203,13 @@ public class SearchEngineTest {
ArrayList<ScoredResult> results = new ArrayList<ScoredResult>(engine.search(query));
// Title > Description > Category.
assertEquals(results.toString(), 3, results.size());
Assert.assertEquals(results.toString(), 3, results.size());
assertTrackResult(trackId, results.get(0));
assertWaypointResult(titleWaypointId, results.get(1));
assertWaypointResult(descriptionWaypointId, results.get(2));
}
@Test
public void testSearchTrackDistance() {
// All results match text, but they're at difference distances from the user.
long farFarAwayId = insertTrack("aa", 0.3);
@@ -229,6 +223,7 @@ public class SearchEngineTest {
assertTrackResults(results, nearId, farId, farFarAwayId);
}
@Test
public void testSearchWaypointDistance() {
// All results match text, but they're at difference distances from the user.
long farFarAwayId = insertWaypoint("aa", 0.3);
@@ -255,6 +250,7 @@ public class SearchEngineTest {
assertTrackResults(results, recentId, oldId, oldestId);
}
@Test
public void testSearchWaypointRecent() {
// All results match text, but they're were recorded at different times.
long oldestId = insertWaypoint("aa", 2);
@@ -268,6 +264,7 @@ public class SearchEngineTest {
assertWaypointResults(results, recentId, oldId, oldestId);
}
@Test
public void testSearchCurrentTrack() {
// All results match text, but one of them is the current track.
long currentId = insertTrack("ab", 1);
@@ -280,6 +277,7 @@ public class SearchEngineTest {
assertTrackResults(results, otherId, currentId);
}
@Test
public void testSearchCurrentTrackWaypoint() {
// All results match text, but one of them is in the current track.
long otherId = insertWaypoint("aa", 1, 456);
@@ -293,36 +291,36 @@ public class SearchEngineTest {
}
private void assertTrackResult(long trackId, ScoredResult result) {
assertNotNull("Not a track", result.track);
assertNull("Ambiguous result", result.waypoint);
assertEquals(trackId, result.track.getId());
Assert.assertNotNull("Not a track", result.track);
Assert.assertNull("Ambiguous result", result.waypoint);
Assert.assertEquals(trackId, result.track.getId());
}
private void assertTrackResults(List<ScoredResult> results, long... trackIds) {
String errMsg = "Expected IDs=" + Arrays.toString(trackIds) + "; results=" + results;
assertEquals(results.size(), trackIds.length);
Assert.assertEquals(results.size(), trackIds.length);
for (int i = 0; i < results.size(); i++) {
ScoredResult result = results.get(i);
assertNotNull(errMsg, result.track);
assertNull(errMsg, result.waypoint);
assertEquals(errMsg, trackIds[i], result.track.getId());
Assert.assertNotNull(errMsg, result.track);
Assert.assertNull(errMsg, result.waypoint);
Assert.assertEquals(errMsg, trackIds[i], result.track.getId());
}
}
private void assertWaypointResult(long waypointId, ScoredResult result) {
assertNotNull("Not a waypoint", result.waypoint);
assertNull("Ambiguous result", result.track);
assertEquals(waypointId, result.waypoint.getId());
Assert.assertNotNull("Not a waypoint", result.waypoint);
Assert.assertNull("Ambiguous result", result.track);
Assert.assertEquals(waypointId, result.waypoint.getId());
}
private void assertWaypointResults(List<ScoredResult> results, long... waypointIds) {
String errMsg = "Expected IDs=" + Arrays.toString(waypointIds) + "; results=" + results;
assertEquals(results.size(), waypointIds.length);
Assert.assertEquals(results.size(), waypointIds.length);
for (int i = 0; i < results.size(); i++) {
ScoredResult result = results.get(i);
assertNotNull(errMsg, result.waypoint);
assertNull(errMsg, result.track);
assertEquals(errMsg, waypointIds[i], result.waypoint.getId());
Assert.assertNotNull(errMsg, result.waypoint);
Assert.assertNull(errMsg, result.track);
Assert.assertEquals(errMsg, waypointIds[i], result.waypoint.getId());
}
}
}
@@ -1,808 +0,0 @@
/*
* Copyright 2011 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package de.dennisguse.opentracks.content;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
import android.database.ContentObserver;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.location.Location;
import android.provider.BaseColumns;
import android.test.RenamingDelegatingContext;
import android.test.mock.MockContentResolver;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import de.dennisguse.opentracks.Constants;
import de.dennisguse.opentracks.TrackStubUtils;
import de.dennisguse.opentracks.content.ContentProviderUtils.LocationFactory;
import de.dennisguse.opentracks.content.ContentProviderUtils.LocationIterator;
import de.dennisguse.opentracks.services.TrackRecordingServiceTest.MockContext;
import de.dennisguse.opentracks.util.PreferencesUtils;
import de.dennisguse.opentracks.R;
import com.google.android.testing.mocking.AndroidMock;
import com.google.android.testing.mocking.UsesMocks;
import org.easymock.Capture;
import org.easymock.IAnswer;
import org.junit.Before;
import org.junit.runner.RunWith;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.Set;
import static com.google.android.testing.mocking.AndroidMock.capture;
import static com.google.android.testing.mocking.AndroidMock.eq;
import static com.google.android.testing.mocking.AndroidMock.expect;
import static com.google.android.testing.mocking.AndroidMock.isA;
/**
* Tests for {@link TrackDataHub}.
*
* @author Rodrigo Damazio
*/
@RunWith(AndroidJUnit4.class)
public class TrackDataHubTest {
private static final long TRACK_ID = 42L;
private static final int TARGET_POINTS = 50;
private MockContext context;
private SharedPreferences sharedPreferences;
private ContentProviderUtils contentProviderUtils;
private DataSource dataSource;
private TrackDataManager trackDataManager;
private TrackDataHub trackDataHub;
private TrackDataListener trackDataListener1;
private TrackDataListener trackDataListener2;
private Capture<OnSharedPreferenceChangeListener> preferenceChangeListenerCapture = new Capture<
SharedPreferences.OnSharedPreferenceChangeListener>();
@Before
@UsesMocks({ContentProviderUtils.class, DataSource.class, TrackDataListener.class})
@Override
protected void setUp() {
context = new MockContext(new MockContentResolver(), new RenamingDelegatingContext(
getContext(), getContext(), "test."));
sharedPreferences = context.getSharedPreferences(Constants.SETTINGS_NAME, Context.MODE_PRIVATE);
contentProviderUtils = AndroidMock.createMock(ContentProviderUtils.class);
dataSource = AndroidMock.createMock(DataSource.class, context);
trackDataManager = new TrackDataManager();
trackDataHub = new TrackDataHub(
context, trackDataManager, contentProviderUtils, TARGET_POINTS) {
@Override
protected DataSource newDataSource() {
return dataSource;
}
@Override
protected void runInHanderThread(Runnable runnable) {
// Run everything in the same thread
runnable.run();
}
};
trackDataListener1 = AndroidMock.createStrictMock(
"trackDataListener1", TrackDataListener.class);
trackDataListener2 = AndroidMock.createStrictMock(
"trackDataListener2", TrackDataListener.class);
PreferencesUtils.setLong(context, R.string.recording_track_id_key, TRACK_ID);
}
@Override
protected void tearDown() throws Exception {
AndroidMock.reset(dataSource);
// Expect everything to be unregistered.
dataSource.unregisterContentObserver(isA(ContentObserver.class));
AndroidMock.expectLastCall().times(3);
dataSource.unregisterOnSharedPreferenceChangeListener(
isA(OnSharedPreferenceChangeListener.class));
AndroidMock.replay(dataSource);
trackDataHub.stop();
trackDataHub = null;
super.tearDown();
}
/**
* Tests registering for tracks table update.
*/
public void testTracksTableUpdate() {
// Register two listeners
Capture<ContentObserver> contentObserverCapture = new Capture<ContentObserver>();
Track track = new Track();
expect(contentProviderUtils.getTrack(TRACK_ID)).andStubReturn(track);
dataSource.registerContentObserver(
eq(TracksColumns.CONTENT_URI), capture(contentObserverCapture));
trackDataListener1.onTrackUpdated(track);
trackDataListener2.onTrackUpdated(track);
replay();
trackDataHub.start();
trackDataHub.loadTrack(TRACK_ID);
trackDataHub.registerTrackDataListener(
trackDataListener1, EnumSet.of(TrackDataType.TRACKS_TABLE));
trackDataHub.registerTrackDataListener(
trackDataListener2, EnumSet.of(TrackDataType.TRACKS_TABLE));
verifyAndReset();
// Causes tracks table update
ContentObserver contentObserver = contentObserverCapture.getValue();
expect(contentProviderUtils.getTrack(TRACK_ID)).andStubReturn(track);
trackDataListener1.onTrackUpdated(track);
trackDataListener2.onTrackUpdated(track);
replay();
contentObserver.onChange(false);
verifyAndReset();
// Unregister one listener
expect(contentProviderUtils.getTrack(TRACK_ID)).andStubReturn(track);
trackDataListener2.onTrackUpdated(track);
replay();
trackDataHub.unregisterTrackDataListener(trackDataListener1);
contentObserver.onChange(false);
verifyAndReset();
// Unregister the second listener
dataSource.unregisterContentObserver(contentObserver);
replay();
trackDataHub.unregisterTrackDataListener(trackDataListener2);
contentObserver.onChange(false);
verifyAndReset();
}
/**
* Tests registering for waypoints table update.
*/
public void testWaypointsTableUpdate() {
Waypoint waypoint1 = new Waypoint();
Waypoint waypoint2 = new Waypoint();
Waypoint waypoint3 = new Waypoint();
Waypoint waypoint4 = new Waypoint();
Location location = new Location("gps");
location.setLatitude(10.0);
location.setLongitude(8.0);
waypoint1.setLocation(location);
waypoint2.setLocation(location);
waypoint3.setLocation(location);
waypoint4.setLocation(location);
// Register two listeners
Capture<ContentObserver> contentObserverCapture = new Capture<ContentObserver>();
expect(contentProviderUtils.getWaypointCursor(
eq(TRACK_ID), AndroidMock.leq(-1L), eq(TrackDataHub.MAX_DISPLAYED_WAYPOINTS)))
.andStubAnswer(new FixedSizeCursorAnswer(2));
expect(contentProviderUtils.createWaypoint(isA(Cursor.class)))
.andReturn(waypoint1).andReturn(waypoint2).andReturn(waypoint1).andReturn(waypoint2);
dataSource.registerContentObserver(
eq(WaypointsColumns.CONTENT_URI), capture(contentObserverCapture));
trackDataListener1.clearWaypoints();
trackDataListener2.clearWaypoints();
trackDataListener1.onNewWaypoint(waypoint1);
trackDataListener2.onNewWaypoint(waypoint1);
trackDataListener1.onNewWaypoint(waypoint2);
trackDataListener2.onNewWaypoint(waypoint2);
trackDataListener1.onNewWaypointsDone();
trackDataListener2.onNewWaypointsDone();
replay();
trackDataHub.start();
trackDataHub.loadTrack(TRACK_ID);
trackDataHub.registerTrackDataListener(
trackDataListener1, EnumSet.of(TrackDataType.WAYPOINTS_TABLE));
trackDataHub.registerTrackDataListener(
trackDataListener2, EnumSet.of(TrackDataType.WAYPOINTS_TABLE));
verifyAndReset();
// Cause waypoints table update
ContentObserver contentObserver = contentObserverCapture.getValue();
expect(contentProviderUtils.getWaypointCursor(
eq(TRACK_ID), AndroidMock.leq(-1L), eq(TrackDataHub.MAX_DISPLAYED_WAYPOINTS)))
.andStubAnswer(new FixedSizeCursorAnswer(3));
expect(contentProviderUtils.createWaypoint(isA(Cursor.class)))
.andReturn(waypoint1).andReturn(waypoint2).andReturn(waypoint3);
trackDataListener1.clearWaypoints();
trackDataListener2.clearWaypoints();
trackDataListener1.onNewWaypoint(waypoint1);
trackDataListener2.onNewWaypoint(waypoint1);
trackDataListener1.onNewWaypoint(waypoint2);
trackDataListener2.onNewWaypoint(waypoint2);
trackDataListener1.onNewWaypoint(waypoint3);
trackDataListener2.onNewWaypoint(waypoint3);
trackDataListener1.onNewWaypointsDone();
trackDataListener2.onNewWaypointsDone();
replay();
contentObserver.onChange(false);
verifyAndReset();
// Unregister one listener
expect(contentProviderUtils.getWaypointCursor(
eq(TRACK_ID), AndroidMock.leq(-1L), eq(TrackDataHub.MAX_DISPLAYED_WAYPOINTS)))
.andStubAnswer(new FixedSizeCursorAnswer(4));
expect(contentProviderUtils.createWaypoint(isA(Cursor.class)))
.andReturn(waypoint1).andReturn(waypoint2).andReturn(waypoint3).andReturn(waypoint4);
trackDataListener2.clearWaypoints();
trackDataListener2.onNewWaypoint(waypoint1);
trackDataListener2.onNewWaypoint(waypoint2);
trackDataListener2.onNewWaypoint(waypoint3);
trackDataListener2.onNewWaypoint(waypoint4);
trackDataListener2.onNewWaypointsDone();
replay();
trackDataHub.unregisterTrackDataListener(trackDataListener1);
contentObserver.onChange(false);
verifyAndReset();
// Unregister the second listener
dataSource.unregisterContentObserver(contentObserver);
replay();
trackDataHub.unregisterTrackDataListener(trackDataListener2);
contentObserver.onChange(false);
verifyAndReset();
}
/**
* Tests track points table update.
*/
public void testTrackPointsTableUpdate() {
// Register one listener
Capture<ContentObserver> contentObserverCapture = new Capture<ContentObserver>();
dataSource.registerContentObserver(
eq(TrackPointsColumns.CONTENT_URI), capture(contentObserverCapture));
FixedSizeLocationIterator locationIterator = new FixedSizeLocationIterator(1, 10, 5);
expect(contentProviderUtils.getTrackPointLocationIterator(
eq(TRACK_ID), eq(0L), eq(false), isA(LocationFactory.class))).andReturn(locationIterator);
expect(contentProviderUtils.getLastTrackPointId(TRACK_ID)).andReturn(10L);
trackDataListener1.clearTrackPoints();
locationIterator.expectLocationsDelivered(trackDataListener1);
trackDataListener1.onNewTrackPointsDone();
replay();
trackDataHub.start();
trackDataHub.loadTrack(TRACK_ID);
trackDataHub.registerTrackDataListener(
trackDataListener1, EnumSet.of(TrackDataType.SAMPLED_IN_TRACK_POINTS_TABLE));
verifyAndReset();
// Register a second listener
locationIterator = new FixedSizeLocationIterator(1, 10, 5);
expect(contentProviderUtils.getTrackPointLocationIterator(
eq(TRACK_ID), eq(0L), eq(false), isA(LocationFactory.class))).andReturn(locationIterator);
expect(contentProviderUtils.getLastTrackPointId(TRACK_ID)).andReturn(10L);
trackDataListener2.clearTrackPoints();
locationIterator.expectLocationsDelivered(trackDataListener2);
trackDataListener2.onNewTrackPointsDone();
replay();
trackDataHub.registerTrackDataListener(
trackDataListener2, EnumSet.of(TrackDataType.SAMPLED_IN_TRACK_POINTS_TABLE));
verifyAndReset();
// Deliver more points - should go to both listeners without clearing
ContentObserver contentObserver = contentObserverCapture.getValue();
locationIterator = new FixedSizeLocationIterator(11, 10, 1);
expect(contentProviderUtils.getTrackPointLocationIterator(
eq(TRACK_ID), eq(11L), eq(false), isA(LocationFactory.class))).andReturn(locationIterator);
expect(contentProviderUtils.getLastTrackPointId(TRACK_ID)).andReturn(20L);
locationIterator.expectLocationsDelivered(trackDataListener1);
locationIterator.expectLocationsDelivered(trackDataListener2);
trackDataListener1.onNewTrackPointsDone();
trackDataListener2.onNewTrackPointsDone();
replay();
contentObserver.onChange(false);
verifyAndReset();
// Unregister one listener and change track
locationIterator = new FixedSizeLocationIterator(101, 10);
expect(contentProviderUtils.getTrackPointLocationIterator(
eq(TRACK_ID + 1), eq(0L), eq(false), isA(LocationFactory.class)))
.andReturn(locationIterator);
expect(contentProviderUtils.getLastTrackPointId(TRACK_ID + 1)).andReturn(110L);
trackDataListener2.clearTrackPoints();
locationIterator.expectLocationsDelivered(trackDataListener2);
trackDataListener2.onNewTrackPointsDone();
replay();
trackDataHub.unregisterTrackDataListener(trackDataListener1);
trackDataHub.loadTrack(TRACK_ID + 1);
verifyAndReset();
}
/**
* Tests track points table update with registering the same listener.
*/
public void testTrackPointsTableUpdate_reRegister() {
// Register one listener
Capture<ContentObserver> contentObserverCapture = new Capture<ContentObserver>();
dataSource.registerContentObserver(
eq(TrackPointsColumns.CONTENT_URI), capture(contentObserverCapture));
FixedSizeLocationIterator locationIterator = new FixedSizeLocationIterator(1, 10, 5);
expect(contentProviderUtils.getTrackPointLocationIterator(
eq(TRACK_ID), eq(0L), eq(false), isA(LocationFactory.class))).andReturn(locationIterator);
expect(contentProviderUtils.getLastTrackPointId(TRACK_ID)).andReturn(10L);
trackDataListener1.clearTrackPoints();
locationIterator.expectLocationsDelivered(trackDataListener1);
trackDataListener1.onNewTrackPointsDone();
replay();
trackDataHub.start();
trackDataHub.loadTrack(TRACK_ID);
trackDataHub.registerTrackDataListener(
trackDataListener1, EnumSet.of(TrackDataType.SAMPLED_IN_TRACK_POINTS_TABLE));
verifyAndReset();
// Unregister the listener
ContentObserver observer = contentObserverCapture.getValue();
dataSource.unregisterContentObserver(observer);
replay();
trackDataHub.unregisterTrackDataListener(trackDataListener1);
verifyAndReset();
// Register again
dataSource.registerContentObserver(
eq(TrackPointsColumns.CONTENT_URI), capture(contentObserverCapture));
locationIterator = new FixedSizeLocationIterator(1, 10, 5);
expect(contentProviderUtils.getTrackPointLocationIterator(
eq(TRACK_ID), eq(0L), eq(false), isA(LocationFactory.class))).andReturn(locationIterator);
expect(contentProviderUtils.getLastTrackPointId(TRACK_ID)).andReturn(10L);
trackDataListener1.clearTrackPoints();
locationIterator.expectLocationsDelivered(trackDataListener1);
trackDataListener1.onNewTrackPointsDone();
replay();
trackDataHub.registerTrackDataListener(
trackDataListener1, EnumSet.of(TrackDataType.SAMPLED_IN_TRACK_POINTS_TABLE));
verifyAndReset();
}
/**
* Tests tracks point able change. Register a listener after a track change.
*/
public void testTrackPointsTableUpdate_reRegisterAfterTrackChange() {
// Register one listener
Capture<ContentObserver> observerCapture = new Capture<ContentObserver>();
dataSource.registerContentObserver(
eq(TrackPointsColumns.CONTENT_URI), capture(observerCapture));
FixedSizeLocationIterator locationIterator = new FixedSizeLocationIterator(1, 10, 5);
expect(contentProviderUtils.getTrackPointLocationIterator(
eq(TRACK_ID), eq(0L), eq(false), isA(LocationFactory.class))).andReturn(locationIterator);
expect(contentProviderUtils.getLastTrackPointId(TRACK_ID)).andReturn(10L);
trackDataListener1.clearTrackPoints();
locationIterator.expectLocationsDelivered(trackDataListener1);
trackDataListener1.onNewTrackPointsDone();
replay();
trackDataHub.start();
trackDataHub.loadTrack(TRACK_ID);
trackDataHub.registerTrackDataListener(
trackDataListener1, EnumSet.of(TrackDataType.SAMPLED_IN_TRACK_POINTS_TABLE));
verifyAndReset();
// Unregister the listener
ContentObserver observer = observerCapture.getValue();
dataSource.unregisterContentObserver(observer);
replay();
trackDataHub.unregisterTrackDataListener(trackDataListener1);
verifyAndReset();
// Register the listener after a new track
dataSource.registerContentObserver(
eq(TrackPointsColumns.CONTENT_URI), capture(observerCapture));
locationIterator = new FixedSizeLocationIterator(1, 10);
expect(contentProviderUtils.getTrackPointLocationIterator(
eq(TRACK_ID + 1), eq(0L), eq(false), isA(LocationFactory.class)))
.andReturn(locationIterator);
expect(contentProviderUtils.getLastTrackPointId(TRACK_ID + 1)).andReturn(10L);
trackDataListener1.clearTrackPoints();
locationIterator.expectLocationsDelivered(trackDataListener1);
trackDataListener1.onNewTrackPointsDone();
replay();
trackDataHub.loadTrack(TRACK_ID + 1);
trackDataHub.registerTrackDataListener(
trackDataListener1, EnumSet.of(TrackDataType.SAMPLED_IN_TRACK_POINTS_TABLE));
verifyAndReset();
}
/**
* Tests track points table update with large track sampling.
*/
public void testTrackPointsTableUpdate_largeTrackSampling() {
Capture<ContentObserver> contentObserverCapture = new Capture<ContentObserver>();
dataSource.registerContentObserver(
eq(TrackPointsColumns.CONTENT_URI), capture(contentObserverCapture));
expect(contentProviderUtils.getLastTrackPointId(TRACK_ID)).andReturn(200L);
AndroidMock.expectLastCall().anyTimes();
FixedSizeLocationIterator locationIterator1 = new FixedSizeLocationIterator(
1, 200, 4, 25, 71, 120);
expect(contentProviderUtils.getTrackPointLocationIterator(
eq(TRACK_ID), eq(0L), eq(false), isA(LocationFactory.class))).andReturn(locationIterator1);
FixedSizeLocationIterator locationIterator2 = new FixedSizeLocationIterator(
1, 200, 4, 25, 71, 120);
expect(contentProviderUtils.getTrackPointLocationIterator(
eq(TRACK_ID), eq(0L), eq(false), isA(LocationFactory.class))).andReturn(locationIterator2);
trackDataListener1.clearTrackPoints();
locationIterator1.expectSampledLocationsDelivered(trackDataListener1, 4, false);
trackDataListener1.onNewTrackPointsDone();
trackDataListener2.clearTrackPoints();
locationIterator2.expectSampledLocationsDelivered(trackDataListener2, 4, true);
trackDataListener2.onNewTrackPointsDone();
replay();
trackDataHub.start();
trackDataHub.loadTrack(TRACK_ID);
trackDataHub.registerTrackDataListener(
trackDataListener1, EnumSet.of(TrackDataType.SAMPLED_IN_TRACK_POINTS_TABLE));
trackDataHub.registerTrackDataListener(trackDataListener2, EnumSet.of(
TrackDataType.SAMPLED_IN_TRACK_POINTS_TABLE, TrackDataType.SAMPLED_OUT_TRACK_POINTS_TABLE));
verifyAndReset();
}
/**
* Tests track points table update with resampling.
*/
public void testTrackPointsTableUpdate_resampling() {
Capture<ContentObserver> observerCapture = new Capture<ContentObserver>();
dataSource.registerContentObserver(
eq(TrackPointsColumns.CONTENT_URI), capture(observerCapture));
// Deliver 30 points (no sampling happens)
FixedSizeLocationIterator locationIterator = new FixedSizeLocationIterator(1, 30, 5);
expect(contentProviderUtils.getTrackPointLocationIterator(
eq(TRACK_ID), eq(0L), eq(false), isA(LocationFactory.class))).andReturn(locationIterator);
expect(contentProviderUtils.getLastTrackPointId(TRACK_ID)).andReturn(30L);
trackDataListener1.clearTrackPoints();
locationIterator.expectLocationsDelivered(trackDataListener1);
trackDataListener1.onNewTrackPointsDone();
replay();
trackDataHub.start();
trackDataHub.loadTrack(TRACK_ID);
trackDataHub.registerTrackDataListener(
trackDataListener1, EnumSet.of(TrackDataType.SAMPLED_IN_TRACK_POINTS_TABLE));
verifyAndReset();
// Now deliver 30 more (incrementally sampled)
ContentObserver observer = observerCapture.getValue();
locationIterator = new FixedSizeLocationIterator(31, 30);
expect(contentProviderUtils.getTrackPointLocationIterator(
eq(TRACK_ID), eq(31L), eq(false), isA(LocationFactory.class))).andReturn(locationIterator);
expect(contentProviderUtils.getLastTrackPointId(TRACK_ID)).andReturn(60L);
locationIterator.expectSampledLocationsDelivered(trackDataListener1, 2, false);
trackDataListener1.onNewTrackPointsDone();
replay();
observer.onChange(false);
verifyAndReset();
// Now another 30 (triggers resampling)
locationIterator = new FixedSizeLocationIterator(1, 90);
expect(contentProviderUtils.getTrackPointLocationIterator(
eq(TRACK_ID), eq(0L), eq(false), isA(LocationFactory.class))).andReturn(locationIterator);
expect(contentProviderUtils.getLastTrackPointId(TRACK_ID)).andReturn(90L);
trackDataListener1.clearTrackPoints();
locationIterator.expectSampledLocationsDelivered(trackDataListener1, 2, false);
trackDataListener1.onNewTrackPointsDone();
replay();
observer.onChange(false);
verifyAndReset();
}
/**
* Tests preferences change.
*/
public void testPreferencesChange() throws Exception {
// Register two listeners
PreferencesUtils.setString(
context, R.string.stats_rate_key, PreferencesUtils.STATS_RATE_DEFAULT);
PreferencesUtils.setString(
context, R.string.stats_units_key, PreferencesUtils.STATS_UNITS_DEFAULT);
PreferencesUtils.setInt(context, R.string.recording_gps_accuracy_key,
PreferencesUtils.RECORDING_GPS_ACCURACY_DEFAULT);
PreferencesUtils.setInt(context, R.string.recording_distance_interval_key,
PreferencesUtils.RECORDING_DISTANCE_INTERVAL_DEFAULT);
dataSource.registerOnSharedPreferenceChangeListener(capture(preferenceChangeListenerCapture));
expect(trackDataListener1.onMetricUnitsChanged(true)).andReturn(false);
expect(trackDataListener1.onReportSpeedChanged(true)).andReturn(false);
expect(
trackDataListener1.onRecordingGpsAccuracy(PreferencesUtils.RECORDING_GPS_ACCURACY_DEFAULT))
.andReturn(false);
expect(trackDataListener1.onRecordingDistanceIntervalChanged(
PreferencesUtils.RECORDING_DISTANCE_INTERVAL_DEFAULT)).andReturn(false);
expect(trackDataListener1.onMapTypeChanged(PreferencesUtils.MAP_TYPE_DEFAULT)).andReturn(false);
expect(trackDataListener2.onMetricUnitsChanged(true)).andReturn(false);
expect(trackDataListener2.onReportSpeedChanged(true)).andReturn(false);
expect(
trackDataListener2.onRecordingGpsAccuracy(PreferencesUtils.RECORDING_GPS_ACCURACY_DEFAULT))
.andReturn(false);
expect(trackDataListener2.onRecordingDistanceIntervalChanged(
PreferencesUtils.RECORDING_DISTANCE_INTERVAL_DEFAULT)).andReturn(false);
expect(trackDataListener2.onMapTypeChanged(PreferencesUtils.MAP_TYPE_DEFAULT)).andReturn(false);
replay();
trackDataHub.start();
trackDataHub.registerTrackDataListener(
trackDataListener1, EnumSet.of(TrackDataType.PREFERENCE));
trackDataHub.registerTrackDataListener(
trackDataListener2, EnumSet.of(TrackDataType.PREFERENCE));
verifyAndReset();
// Change report speed to false
expect(trackDataListener1.onReportSpeedChanged(false)).andReturn(false);
expect(trackDataListener2.onReportSpeedChanged(false)).andReturn(false);
replay();
PreferencesUtils.setString(
context, R.string.stats_rate_key, context.getString(R.string.stats_rate_pace));
OnSharedPreferenceChangeListener listener = preferenceChangeListenerCapture.getValue();
listener.onSharedPreferenceChanged(
sharedPreferences, PreferencesUtils.getKey(context, R.string.stats_rate_key));
verifyAndReset();
// Change metric units to false
expect(trackDataListener1.onMetricUnitsChanged(false)).andReturn(false);
expect(trackDataListener2.onMetricUnitsChanged(false)).andReturn(false);
replay();
String imperialUnits = context.getString(R.string.stats_units_imperial);
PreferencesUtils.setString(context, R.string.stats_units_key, imperialUnits);
listener.onSharedPreferenceChanged(
sharedPreferences, PreferencesUtils.getKey(context, R.string.stats_units_key));
verifyAndReset();
}
/**
* Replays mocks.
*/
private void replay() {
AndroidMock.replay(contentProviderUtils, dataSource, trackDataListener1, trackDataListener2);
}
/**
* Verifies and resets mocks.
*/
private void verifyAndReset() {
AndroidMock.verify(contentProviderUtils, dataSource, trackDataListener1, trackDataListener2);
AndroidMock.reset(contentProviderUtils, dataSource, trackDataListener1, trackDataListener2);
}
/**
* Tests the method {@link TrackDataHub#start()}.
*/
public void testRegisterTracksTableListener() {
Capture<ContentObserver> observerCapture = new Capture<ContentObserver>();
dataSource.registerContentObserver(eq(TracksColumns.CONTENT_URI), capture(observerCapture));
Track track = TrackStubUtils.createTrack(1);
expect(contentProviderUtils.getTrack(capture(new Capture<Long>()))).andReturn(track);
// Make the track id is unique.
PreferencesUtils.setLong(context, R.string.recording_track_id_key, System.currentTimeMillis());
trackDataListener1.onTrackUpdated(track);
replay();
trackDataHub.start();
trackDataHub.registerTrackDataListener(
trackDataListener1, EnumSet.of(TrackDataType.TRACKS_TABLE));
verifyAndReset();
}
/**
* Tests the method {@link TrackDataHub#start()}.
*/
public void testRegisterWaypointsTableListener() {
Capture<ContentObserver> observerCapture = new Capture<ContentObserver>();
dataSource.registerContentObserver(eq(WaypointsColumns.CONTENT_URI), capture(observerCapture));
expect(contentProviderUtils.getWaypointCursor(
capture(new Capture<Long>()), capture(new Capture<Long>()),
capture(new Capture<Integer>()))).andReturn(null);
trackDataListener1.clearWaypoints();
trackDataListener1.onNewWaypointsDone();
replay();
trackDataHub.start();
trackDataHub.registerTrackDataListener(
trackDataListener1, EnumSet.of(TrackDataType.WAYPOINTS_TABLE));
verifyAndReset();
}
/**
* Tests the method {@link TrackDataHub#notifyPreferenceChanged(String)} when
* the key is R.string.min_required_accuracy_key.
*/
public void testNotifyPreferenceChanged_minRequiredAccuracy() {
int value = 1;
PreferencesUtils.setInt(context, R.string.recording_gps_accuracy_key, value);
trackDataHub.notifyPreferenceChanged(PreferencesUtils
.getKey(context, R.string.recording_gps_accuracy_key));
assertEquals(value, trackDataHub.getRecordingGpsAccuracy());
PreferencesUtils.setInt(context, R.string.recording_gps_accuracy_key, value + 1);
trackDataHub.notifyPreferenceChanged(PreferencesUtils
.getKey(context, R.string.recording_gps_accuracy_key));
assertEquals(value + 1, trackDataHub.getRecordingGpsAccuracy());
}
/**
* Tests the method {@link TrackDataHub#notifyPreferenceChanged(String)} when
* the key is R.string.metric_units_key.
*/
public void testNotifyPreferenceChanged_metricUnitsNoNotify() {
String imperialUnits = context.getString(R.string.stats_units_imperial);
PreferencesUtils.setString(context, R.string.stats_units_key, imperialUnits);
trackDataHub.notifyPreferenceChanged(
PreferencesUtils.getKey(context, R.string.stats_units_key));
assertEquals(false, trackDataHub.isMetricUnits());
PreferencesUtils.setString(
context, R.string.stats_units_key, PreferencesUtils.STATS_UNITS_DEFAULT);
trackDataHub.notifyPreferenceChanged(
PreferencesUtils.getKey(context, R.string.stats_units_key));
assertEquals(true, trackDataHub.isMetricUnits());
}
/**
* Tests the method {@link TrackDataHub#notifyPreferenceChanged(String)} when
* the key is R.string.metric_units_key.
*/
public void testNotifyPreferenceChanged_reportSpeedNoNotify() {
String value = context.getString(R.string.stats_rate_pace);
PreferencesUtils.setString(context, R.string.stats_rate_key, value);
trackDataHub.notifyPreferenceChanged(PreferencesUtils
.getKey(context, R.string.stats_rate_key));
assertEquals(false, trackDataHub.isReportSpeed());
value = context.getString(R.string.stats_rate_speed);
PreferencesUtils.setString(context, R.string.stats_rate_key, value);
trackDataHub.notifyPreferenceChanged(PreferencesUtils
.getKey(context, R.string.stats_rate_key));
assertEquals(true, trackDataHub.isReportSpeed());
}
/**
* Fixed size cursor answer.
*
* @author Jimmy Shih
*/
private static class FixedSizeCursorAnswer implements IAnswer<Cursor> {
private final int size;
public FixedSizeCursorAnswer(int size) {
this.size = size;
}
@Override
public Cursor answer() throws Throwable {
MatrixCursor cursor = new MatrixCursor(new String[]{BaseColumns._ID});
for (long i = 0; i < size; i++) {
cursor.addRow(new Object[]{i});
}
return cursor;
}
}
/**
* Fixed size location iterator.
*
* @author Jimmy Shih
*/
private static class FixedSizeLocationIterator implements LocationIterator {
private final long startId;
private final Location[] locations;
private final Set<Integer> splitIndexSet = new HashSet<Integer>();
private int currentIndex = -1;
public FixedSizeLocationIterator(long startId, int size) {
this(startId, size, null);
}
public FixedSizeLocationIterator(long startId, int size, int... splitIndexes) {
this.startId = startId;
this.locations = new Location[size];
for (int i = 0; i < size; i++) {
Location location = new Location("gps");
location.setLatitude(-15.0 + i / 1000.0);
location.setLongitude(37 + i / 1000.0);
location.setAltitude(i);
locations[i] = location;
}
if (splitIndexes != null) {
for (int splitIndex : splitIndexes) {
splitIndexSet.add(splitIndex);
Location splitLocation = locations[splitIndex];
splitLocation.setLatitude(100.0);
splitLocation.setLongitude(200.0);
}
}
}
public void expectLocationsDelivered(TrackDataListener listener) {
for (int i = 0; i < locations.length; i++) {
if (splitIndexSet.contains(i)) {
listener.onSegmentSplit(locations[i]);
} else {
listener.onSampledInTrackPoint(locations[i]);
}
}
}
public void expectSampledLocationsDelivered(
TrackDataListener listener, int sampleFrequency, boolean includeSampledOut) {
boolean includeNext = false;
for (int i = 0; i < locations.length; i++) {
if (splitIndexSet.contains(i)) {
listener.onSegmentSplit(locations[i]);
includeNext = true;
} else if (includeNext || (i % sampleFrequency == 0)) {
listener.onSampledInTrackPoint(locations[i]);
includeNext = false;
} else if (includeSampledOut) {
listener.onSampledOutTrackPoint(locations[i]);
}
}
}
@Override
public boolean hasNext() {
return currentIndex < locations.length - 1;
}
@Override
public Location next() {
currentIndex++;
return locations[currentIndex];
}
@Override
public void remove() {
throw new UnsupportedOperationException();
}
@Override
public long getLocationId() {
return startId + currentIndex;
}
@Override
public void close() {
// Do nothing
}
}
}
@@ -1,63 +0,0 @@
/*
* Copyright 2010 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package de.dennisguse.opentracks.content;
import android.os.Parcel;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import de.dennisguse.opentracks.content.Waypoint.WaypointType;
import org.junit.Assert;
import org.junit.runner.RunWith;
/**
* Tests for the WaypointCreationRequest class.
* {@link WaypointCreationRequest}
*
* @author Sandor Dornbush
*/
@RunWith(AndroidJUnit4.class)
public class WaypointCreationRequestTest {
public void testTypeParceling() {
WaypointCreationRequest original = WaypointCreationRequest.DEFAULT_WAYPOINT;
Parcel p = Parcel.obtain();
original.writeToParcel(p, 0);
p.setDataPosition(0);
WaypointCreationRequest copy = WaypointCreationRequest.CREATOR.createFromParcel(p);
Assert.assertEquals(original.getType(), copy.getType());
Assert.assertFalse(copy.isTrackStatistics());
Assert.assertNull(copy.getName());
Assert.assertNull(copy.getDescription());
Assert.assertNull(copy.getIconUrl());
}
public void testAllAttributesParceling() {
WaypointCreationRequest original = new WaypointCreationRequest(
WaypointType.WAYPOINT, false, "name", "category", "description", "img.png", null);
Parcel p = Parcel.obtain();
original.writeToParcel(p, 0);
p.setDataPosition(0);
WaypointCreationRequest copy = WaypointCreationRequest.CREATOR.createFromParcel(p);
Assert.assertEquals(original.getType(), copy.getType());
Assert.assertFalse(copy.isTrackStatistics());
Assert.assertEquals("name", copy.getName());
Assert.assertEquals("category", copy.getCategory());
Assert.assertEquals("description", copy.getDescription());
Assert.assertEquals("img.png", copy.getIconUrl());
}
}
@@ -18,20 +18,20 @@ package de.dennisguse.opentracks.fragments;
import android.location.Location;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.platform.app.InstrumentationRegistry;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import de.dennisguse.opentracks.ChartView;
import de.dennisguse.opentracks.TrackStubUtils;
import de.dennisguse.opentracks.content.SensorDataSetLocation;
import de.dennisguse.opentracks.content.sensor.Sensor;
import de.dennisguse.opentracks.content.sensor.Sensor.SensorDataSet;
import de.dennisguse.opentracks.content.sensor.SensorDataSet;
import de.dennisguse.opentracks.util.UnitConversions;
import org.junit.Assert;
import org.junit.Before;
import org.junit.runner.RunWith;
/**
* Tests {@link ChartFragment}.
*
@@ -45,94 +45,76 @@ public class ChartFragmentTest {
private ChartFragment chartFragment;
@Before
protected void setUp() {
public void setUp() {
chartFragment = new ChartFragment();
chartFragment.setChartView(new ChartView(InstrumentationRegistry.getInstrumentation().getContext()));
chartFragment.setChartView(new ChartView(ApplicationProvider.getApplicationContext()));
chartFragment.setTripStatisticsUpdater(TrackStubUtils.INITIAL_TIME);
}
/**
* Tests the logic to get the incorrect values of sensor in {@link ChartFragment#fillDataPoint(Location,
* double[])}
* Tests the logic to get the incorrect values of sensor in {@link ChartFragment#fillDataPoint(Location, double[])}.
*/
@Test
public void testFillDataPoint_sensorIncorrect() {
SensorDataSetLocation sensorDataSetLocation = TrackStubUtils.createSensorDataSetLocation();
// No input.
double[] point = fillDataPointTestHelper(sensorDataSetLocation);
Assert.assertEquals(Double.NaN, point[ChartView.HEART_RATE_SERIES + 1]);
Assert.assertEquals(Double.NaN, point[ChartView.CADENCE_SERIES + 1]);
Assert.assertEquals(Double.NaN, point[ChartView.POWER_SERIES + 1]);
Assert.assertEquals(Float.NaN, point[ChartView.HEART_RATE_SERIES + 1], 0.01);
Assert.assertEquals(Float.NaN, point[ChartView.CADENCE_SERIES + 1], 0.01);
Assert.assertEquals(Float.NaN, point[ChartView.POWER_SERIES + 1], 0.01);
// Input incorrect state.
// Creates SensorData.
Sensor.SensorData.Builder heartRateData = Sensor.SensorData.newBuilder()
.setValue(100).setState(Sensor.SensorState.NONE);
Sensor.SensorData.Builder cadenceData = Sensor.SensorData.newBuilder()
.setValue(101).setState(Sensor.SensorState.NONE);
Sensor.SensorData.Builder powerData = Sensor.SensorData.newBuilder()
.setValue(102).setState(Sensor.SensorState.NONE);
// Creates SensorDataSet.
SensorDataSet sensorDataSet = sensorDataSetLocation.getSensorDataSet();
sensorDataSet = sensorDataSet.toBuilder()
.setHeartRate(heartRateData).setCadence(cadenceData).setPower(powerData).build();
SensorDataSet sensorDataSet = new SensorDataSet(SensorDataSet.DATA_UNAVAILABLE, SensorDataSet.DATA_UNAVAILABLE);
sensorDataSetLocation.setSensorDataSet(sensorDataSet);
// Test.
point = fillDataPointTestHelper(sensorDataSetLocation);
Assert.assertEquals(Double.NaN, point[ChartView.HEART_RATE_SERIES + 1]);
Assert.assertEquals(Double.NaN, point[ChartView.CADENCE_SERIES + 1]);
Assert.assertEquals(Double.NaN, point[ChartView.POWER_SERIES + 1]);
Assert.assertEquals(Float.NaN, point[ChartView.HEART_RATE_SERIES + 1], 0.01);
Assert.assertEquals(Float.NaN, point[ChartView.CADENCE_SERIES + 1], 0.01);
Assert.assertEquals(Float.NaN, point[ChartView.POWER_SERIES + 1], 0.01);
}
/**
* Tests the logic to get the correct values of sensor in {@link ChartFragment#fillDataPoint(Location,
* double[])}.
* Tests the logic to get the correct values of sensor in {@link ChartFragment#fillDataPoint(Location, double[])}.
*/
@Test
public void testFillDataPoint_sensorCorrect() {
SensorDataSetLocation sensorDataSetLocation = TrackStubUtils.createSensorDataSetLocation();
// No input.
double[] point = fillDataPointTestHelper(sensorDataSetLocation);
Assert.assertEquals(Double.NaN, point[ChartView.HEART_RATE_SERIES + 1]);
Assert.assertEquals(Double.NaN, point[ChartView.CADENCE_SERIES + 1]);
Assert.assertEquals(Double.NaN, point[ChartView.POWER_SERIES + 1]);
Assert.assertEquals(Float.NaN, point[ChartView.HEART_RATE_SERIES + 1], 0.01);
Assert.assertEquals(Float.NaN, point[ChartView.CADENCE_SERIES + 1], 0.01);
Assert.assertEquals(Float.NaN, point[ChartView.POWER_SERIES + 1], 0.01);
// Creates SensorData.
Sensor.SensorData.Builder heartRateData = Sensor.SensorData.newBuilder()
.setValue(100).setState(Sensor.SensorState.SENDING);
Sensor.SensorData.Builder cadenceData = Sensor.SensorData.newBuilder()
.setValue(101).setState(Sensor.SensorState.SENDING);
Sensor.SensorData.Builder powerData = Sensor.SensorData.newBuilder()
.setValue(102).setState(Sensor.SensorState.SENDING);
SensorDataSet sensorDataSet = new SensorDataSet(100, 101, 102);
// Creates SensorDataSet.
SensorDataSet sensorDataSet = sensorDataSetLocation.getSensorDataSet();
sensorDataSet = sensorDataSet.toBuilder()
.setHeartRate(heartRateData).setCadence(cadenceData).setPower(powerData).build();
sensorDataSetLocation.setSensorDataSet(sensorDataSet);
// Test.
point = fillDataPointTestHelper(sensorDataSetLocation);
Assert.assertEquals(100.0, point[ChartView.HEART_RATE_SERIES + 1]);
Assert.assertEquals(101.0, point[ChartView.CADENCE_SERIES + 1]);
Assert.assertEquals(102.0, point[ChartView.POWER_SERIES + 1]);
Assert.assertEquals(100.0, point[ChartView.HEART_RATE_SERIES + 1], 0.01);
Assert.assertEquals(101.0, point[ChartView.CADENCE_SERIES + 1], 0.01);
Assert.assertEquals(102.0, point[ChartView.POWER_SERIES + 1], 0.01);
}
/**
* Tests the logic to get the value of metric Distance in {@link ChartFragment#fillDataPoint(Location,
* double[])}.
* Tests the logic to get the value of metric Distance in {@link ChartFragment#fillDataPoint(Location, double[])}.
*/
@Test
public void testFillDataPoint_distanceMetric() {
// By distance.
chartFragment.setChartByDistance(true);
// Resets last location and writes first location.
SensorDataSetLocation sensorDataSetLocation1 = TrackStubUtils.createSensorDataSetLocation();
double[] point = fillDataPointTestHelper(sensorDataSetLocation1);
Assert.assertEquals(0.0, point[0]);
Assert.assertEquals(0.0, point[0], 0.01);
// The second is a same location, just different time.
SensorDataSetLocation sensorDataSetLocation2 = TrackStubUtils.createSensorDataSetLocation();
point = fillDataPointTestHelper(sensorDataSetLocation2);
Assert.assertEquals(0.0, point[0]);
Assert.assertEquals(0.0, point[0], 0.01);
// The third location is a new location, and use metric.
SensorDataSetLocation sensorDataSetLocation3 = TrackStubUtils.createSensorDataSetLocation();
@@ -144,7 +126,7 @@ public class ChartFragmentTest {
Location.distanceBetween(sensorDataSetLocation2.getLatitude(), sensorDataSetLocation2.getLongitude(),
sensorDataSetLocation3.getLatitude(), sensorDataSetLocation3.getLongitude(), results);
double distance1 = results[0] * UnitConversions.M_TO_KM;
Assert.assertEquals(distance1, point[0]);
Assert.assertEquals(distance1, point[0], 0.01);
// The fourth location is a new location, and use metric.
SensorDataSetLocation sensorDataSetLocation4 = TrackStubUtils.createSensorDataSetLocation();
@@ -155,13 +137,13 @@ public class ChartFragmentTest {
Location.distanceBetween(sensorDataSetLocation3.getLatitude(), sensorDataSetLocation3.getLongitude(),
sensorDataSetLocation4.getLatitude(), sensorDataSetLocation4.getLongitude(), results);
double distance2 = results[0] * UnitConversions.M_TO_KM;
Assert.assertEquals((distance1 + distance2), point[0]);
Assert.assertEquals((distance1 + distance2), point[0], 0.01);
}
/**
* Tests the logic to get the value of imperial Distance in {@link ChartFragment#fillDataPoint(Location,
* double[])}.
* Tests the logic to get the value of imperial Distance in {@link ChartFragment#fillDataPoint(Location, double[])}.
*/
@Test
public void testFillDataPoint_distanceImperial() {
// By distance.
chartFragment.setChartByDistance(true);
@@ -171,7 +153,7 @@ public class ChartFragmentTest {
// The first is a same location, just different time.
SensorDataSetLocation sensorDataSetLocation1 = TrackStubUtils.createSensorDataSetLocation();
double[] point = fillDataPointTestHelper(sensorDataSetLocation1);
Assert.assertEquals(0.0, point[0]);
Assert.assertEquals(0.0, point[0], 0.01);
// The second location is a new location, and use imperial.
SensorDataSetLocation sensorDataSetLocation2 = TrackStubUtils.createSensorDataSetLocation();
@@ -179,14 +161,13 @@ public class ChartFragmentTest {
point = fillDataPointTestHelper(sensorDataSetLocation2);
/*
* Computes the distance between Latitude 22 and 23. And for we set using
* imperial, the distance should be multiplied by UnitConversions.KM_TO_MI.
* Computes the distance between Latitude 22 and 23.
* And for we set using * imperial, the distance should be multiplied by UnitConversions.KM_TO_MI.
*/
float[] results = new float[4];
Location.distanceBetween(sensorDataSetLocation1.getLatitude(), sensorDataSetLocation1.getLongitude(),
sensorDataSetLocation2.getLatitude(), sensorDataSetLocation2.getLongitude(), results);
Location.distanceBetween(sensorDataSetLocation1.getLatitude(), sensorDataSetLocation1.getLongitude(), sensorDataSetLocation2.getLatitude(), sensorDataSetLocation2.getLongitude(), results);
double distance1 = results[0] * UnitConversions.M_TO_KM * UnitConversions.KM_TO_MI;
Assert.assertEquals(distance1, point[0]);
Assert.assertEquals(distance1, point[0], 0.01);
// The third location is a new location, and use imperial.
SensorDataSetLocation sensorDataSetLocation3 = TrackStubUtils.createSensorDataSetLocation();
@@ -194,92 +175,90 @@ public class ChartFragmentTest {
point = fillDataPointTestHelper(sensorDataSetLocation3);
/*
* Computes the distance between Latitude 23 and 24. And for we set using
* imperial, the distance should be multiplied by UnitConversions.KM_TO_MI.
* Computes the distance between Latitude 23 and 24.
* And for we set using * imperial, the distance should be multiplied by UnitConversions.KM_TO_MI.
*/
Location.distanceBetween(sensorDataSetLocation2.getLatitude(), sensorDataSetLocation2.getLongitude(),
sensorDataSetLocation3.getLatitude(), sensorDataSetLocation3.getLongitude(), results);
Location.distanceBetween(sensorDataSetLocation2.getLatitude(), sensorDataSetLocation2.getLongitude(), sensorDataSetLocation3.getLatitude(), sensorDataSetLocation3.getLongitude(), results);
double distance2 = results[0] * UnitConversions.M_TO_KM * UnitConversions.KM_TO_MI;
Assert.assertEquals(distance1 + distance2, point[0]);
Assert.assertEquals(distance1 + distance2, point[0], 0.01);
}
/**
* Tests the logic to get the values of time in {@link ChartFragment#fillDataPoint(Location,
* double[])}.
* Tests the logic to get the values of time in {@link ChartFragment#fillDataPoint(Location, double[])}.
*/
@Test
public void testFillDataPoint_time() {
// By time
chartFragment.setChartByDistance(false);
SensorDataSetLocation sensorDataSetLocation1 = TrackStubUtils.createSensorDataSetLocation();
double[] point = fillDataPointTestHelper(sensorDataSetLocation1);
Assert.assertEquals(0.0, point[0]);
Assert.assertEquals(0.0, point[0], 0.01);
long timeSpan = 222;
SensorDataSetLocation sensorDataSetLocation2 = TrackStubUtils.createSensorDataSetLocation();
sensorDataSetLocation2.setTime(sensorDataSetLocation1.getTime() + timeSpan);
point = fillDataPointTestHelper(sensorDataSetLocation2);
Assert.assertEquals((double) timeSpan, point[0]);
Assert.assertEquals((double) timeSpan, point[0], 0.01);
}
/**
* Tests the logic to get the value of elevation in {@link ChartFragment#fillDataPoint(Location,
* double[])} by one and two points.
* Tests the logic to get the value of elevation in {@link ChartFragment#fillDataPoint(Location, double[])} by one and two points.
*/
@Test
public void testFillDataPoint_elevation() {
SensorDataSetLocation sensorDataSetLocation1 = TrackStubUtils.createSensorDataSetLocation();
/*
* At first, clear old points of elevation, so give true to the second
* parameter. Then only one value INITIALLONGTITUDE in buffer.
* At first, clear old points of elevation, so give true to the second parameter.
* Then only one value INITIAL_ALTITUDE in buffer.
*/
double[] point = fillDataPointTestHelper(sensorDataSetLocation1);
Assert.assertEquals(TrackStubUtils.INITIAL_ALTITUDE, point[ChartView.ELEVATION_SERIES + 1]);
Assert.assertEquals(TrackStubUtils.INITIAL_ALTITUDE, point[ChartView.ELEVATION_SERIES + 1], 0.01);
/*
* Send another value to buffer, now there are two values, INITIALALTITUDE
* and INITIALALTITUDE * 2.
* Send another value to buffer, now there are two values, INITIAL_ALTITUDE and INITIAL_ALTITUDE * 2.
*/
SensorDataSetLocation sensorDataSetLocation2 = TrackStubUtils.createSensorDataSetLocation();
sensorDataSetLocation2.setAltitude(TrackStubUtils.INITIAL_ALTITUDE * 2);
point = fillDataPointTestHelper(sensorDataSetLocation2);
Assert.assertEquals((TrackStubUtils.INITIAL_ALTITUDE + TrackStubUtils.INITIAL_ALTITUDE * 2) / 2.0,
point[ChartView.ELEVATION_SERIES + 1]);
point[ChartView.ELEVATION_SERIES + 1], 0.01);
}
/**
* Tests the logic to get the value of speed in {@link ChartFragment#fillDataPoint(Location,
* double[])}. In this test, firstly remove all
* points in memory, and then fill in two points one by one. The speed values
* of these points are 129, 130.
* Tests the logic to get the value of speed in {@link ChartFragment#fillDataPoint(Location, double[])}.
* In this test, firstly remove all points in memory, and then fill in two points one by one.
* The speed values of these points are 129, 130.
*/
@Test
public void testFillDataPoint_speed() {
/*
* At first, clear old points of speed, so give true to the second
* parameter. It will not be filled in to the speed buffer.
* At first, clear old points of speed, so give true to the second parameter.
* It will not be filled in to the speed buffer.
*/
SensorDataSetLocation sensorDataSetLocation1 = TrackStubUtils.createSensorDataSetLocation();
sensorDataSetLocation1.setSpeed(128.5f);
double[] point = fillDataPointTestHelper(sensorDataSetLocation1);
Assert.assertEquals(0.0, point[ChartView.SPEED_SERIES + 1]);
Assert.assertEquals(0.0, point[ChartView.SPEED_SERIES + 1], 0.01);
/*
* Tests the logic when both metricUnits and reportSpeed are true.This
* location will be filled into speed buffer.
* Tests the logic when both metricUnits and reportSpeed are true.
* This location will be filled into speed buffer.
*/
SensorDataSetLocation sensorDataSetLocation2 = TrackStubUtils.createSensorDataSetLocation();
/*
* Add a time span here to make sure the second point is valid, the value
* 222 here is doesn't matter.
* Add a time span here to make sure the second point is valid, the value 222 here is doesn't matter.
*/
sensorDataSetLocation2.setTime(sensorDataSetLocation1.getTime() + 222);
sensorDataSetLocation2.setSpeed(130);
point = fillDataPointTestHelper(sensorDataSetLocation2);
Assert.assertEquals(130.0 * UnitConversions.MS_TO_KMH, point[ChartView.SPEED_SERIES + 1]);
Assert.assertEquals(130.0 * UnitConversions.MS_TO_KMH, point[ChartView.SPEED_SERIES + 1], 0.01);
}
/**
* Tests the logic to compute speed when use Imperial.
*/
@Test
public void testFillDataPoint_speedImperial() {
// Setups to use imperial.
chartFragment.setMetricUnits(false);
@@ -288,24 +267,24 @@ public class ChartFragmentTest {
SensorDataSetLocation sensorDataSetLocation1 = TrackStubUtils.createSensorDataSetLocation();
sensorDataSetLocation1.setSpeed(100.0f);
double[] point = fillDataPointTestHelper(sensorDataSetLocation1);
Assert.assertEquals(0.0, point[ChartView.SPEED_SERIES + 1]);
Assert.assertEquals(0.0, point[ChartView.SPEED_SERIES + 1], 0.01);
SensorDataSetLocation sensorDataSetLocation2 = TrackStubUtils.createSensorDataSetLocation();
/*
* Add a time span here to make sure the second point is valid and the speed
* is valid. Speed is valid if: speedDifference > Constants.MAX_ACCELERATION
* * timeDifference speedDifference = 102 -100 timeDifference = 222
* Add a time span here to make sure the second point and the speed is valid.
* Speed is valid if: speedDifference > Constants.MAX_ACCELERATION * timeDifference speedDifference = 102 -100 timeDifference = 222
*/
sensorDataSetLocation2.setTime(sensorDataSetLocation2.getTime() + 222);
sensorDataSetLocation2.setSpeed(102);
point = fillDataPointTestHelper(sensorDataSetLocation2);
Assert.assertEquals(102.0 * UnitConversions.MS_TO_KMH * UnitConversions.KM_TO_MI, point[ChartView.SPEED_SERIES + 1]);
Assert.assertEquals(102.0 * UnitConversions.MS_TO_KMH * UnitConversions.KM_TO_MI, point[ChartView.SPEED_SERIES + 1], 0.01);
}
/**
* Tests the logic to get pace value when reportSpeed is false.
*/
@Test
public void testFillDataPoint_pace_nonZeroSpeed() {
// Setups reportSpeed to false.
chartFragment.setReportSpeed(false);
@@ -314,32 +293,31 @@ public class ChartFragmentTest {
SensorDataSetLocation sensorDataSetLocation1 = TrackStubUtils.createSensorDataSetLocation();
sensorDataSetLocation1.setSpeed(100.0f);
double[] point = fillDataPointTestHelper(sensorDataSetLocation1);
Assert.assertEquals(0.0, point[ChartView.SPEED_SERIES + 1]);
Assert.assertEquals(0.0, point[ChartView.SPEED_SERIES + 1], 0.01);
SensorDataSetLocation sensorDataSetLocation2 = TrackStubUtils.createSensorDataSetLocation();
/*
* Add a time span here to make sure the second point is valid and the speed
* is valid. Speed is valid if: speedDifference > Constants.MAX_ACCELERATION
* * timeDifference speedDifference = 102 -100 timeDifference = 222
* Add a time span here to make sure the second point and the speed is valid.
* Speed is valid if: speedDifference > Constants.MAX_ACCELERATION * timeDifference speedDifference = 102 -100 timeDifference = 222
*/
sensorDataSetLocation2.setTime(sensorDataSetLocation2.getTime() + 222);
sensorDataSetLocation2.setSpeed(102);
point = fillDataPointTestHelper(sensorDataSetLocation2);
Assert.assertEquals(HOURS_PER_UNIT / (102.0 * UnitConversions.MS_TO_KMH), point[ChartView.PACE_SERIES + 1]);
Assert.assertEquals(HOURS_PER_UNIT / (102.0 * UnitConversions.MS_TO_KMH), point[ChartView.PACE_SERIES + 1], 0.01);
}
/**
* Tests the logic to get pace value when reportSpeed is false and average
* speed is zero.
* Tests the logic to get pace value when reportSpeed is false and average speed is zero.
*/
@Test
public void testFillDataPoint_pace_zeroSpeed() {
// Setups reportSpeed to false.
chartFragment.setReportSpeed(false);
SensorDataSetLocation sensorDataSetLocation = TrackStubUtils.createSensorDataSetLocation();
sensorDataSetLocation.setSpeed(0);
double[] point = fillDataPointTestHelper(sensorDataSetLocation);
Assert.assertEquals(0.0, point[ChartView.PACE_SERIES + 1]);
Assert.assertEquals(0.0, point[ChartView.PACE_SERIES + 1], 0.01);
}
/**
@@ -353,5 +331,4 @@ public class ChartFragmentTest {
chartFragment.fillDataPoint(location, point);
return point;
}
}
@@ -17,99 +17,73 @@
package de.dennisguse.opentracks.io.file.importer;
import android.content.ContentUris;
import android.content.Context;
import android.location.Location;
import android.location.LocationManager;
import android.net.Uri;
import android.test.AndroidTestCase;
import de.dennisguse.opentracks.content.ContentProviderUtils;
import de.dennisguse.opentracks.content.ContentProviderUtils.Factory;
import de.dennisguse.opentracks.content.Track;
import de.dennisguse.opentracks.content.TracksColumns;
import de.dennisguse.opentracks.content.Waypoint;
import de.dennisguse.opentracks.content.WaypointsColumns;
import de.dennisguse.opentracks.testing.TestingProviderUtilsFactory;
import com.google.android.testing.mocking.AndroidMock;
import com.google.android.testing.mocking.UsesMocks;
import androidx.test.core.app.ApplicationProvider;
import org.easymock.Capture;
import org.junit.Assert;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import java.text.SimpleDateFormat;
import java.util.Locale;
import java.util.SimpleTimeZone;
import static com.google.android.testing.mocking.AndroidMock.eq;
import static com.google.android.testing.mocking.AndroidMock.expect;
import de.dennisguse.opentracks.content.ContentProviderUtils;
import de.dennisguse.opentracks.content.Track;
import de.dennisguse.opentracks.content.TracksColumns;
import de.dennisguse.opentracks.content.Waypoint;
import de.dennisguse.opentracks.content.WaypointsColumns;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.when;
/**
* Abstract class for testing file track importers.
*
* @author Jimmy Shih.
*/
public class AbstractTestFileTrackImporter extends AndroidTestCase {
public abstract class AbstractTestFileTrackImporter {
protected static final String TRACK_NAME_0 = "blablub";
protected static final String TRACK_DESCRIPTION_0 = "s'Laebe isch koi Schlotzer";
static final String TRACK_NAME_0 = "blablub";
static final String TRACK_DESCRIPTION_0 = "s'Laebe isch koi Schlotzer";
protected static final String TRACK_NAME_1 = "another track";
protected static final String TRACK_DESCRIPTION_1 = "another description";
static final double TRACK_LATITUDE = 48.768364;
static final double TRACK_LONGITUDE = 9.177886;
static final double TRACK_ELEVATION = 324.0;
protected static final double TRACK_LATITUDE = 48.768364;
protected static final double TRACK_LONGITUDE = 9.177886;
protected static final double TRACK_ELEVATION = 324.0;
static final String TRACK_TIME_0 = "2010-04-22T18:21:00Z";
static final String TRACK_TIME_1 = "2010-04-22T18:21:50.123";
static final String TRACK_TIME_2 = "2010-04-22T18:23:00.123";
static final String TRACK_TIME_3 = "2010-04-22T18:24:50.123";
protected static final String TRACK_TIME_0 = "2010-04-22T18:21:00Z";
protected static final String TRACK_TIME_1 = "2010-04-22T18:21:50.123";
protected static final String TRACK_TIME_2 = "2010-04-22T18:23:00.123";
protected static final String TRACK_TIME_3 = "2010-04-22T18:24:50.123";
static final SimpleDateFormat DATE_FORMAT_0 = new SimpleDateFormat("yyyy-MM-dd'T'hh:mm:ss'Z'", Locale.US);
static final SimpleDateFormat DATE_FORMAT_1 = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS", Locale.US);
static final long TRACK_ID_0 = 1;
static final long TRACK_POINT_ID_0 = 1;
static final long TRACK_POINT_ID_1 = 2;
static final long TRACK_POINT_ID_3 = 4;
static final Uri TRACK_ID_0_URI = ContentUris.appendId(TracksColumns.CONTENT_URI.buildUpon(), TRACK_ID_0).build();
private static final long WAYPOINT_ID_0 = 1;
private static final Uri WAYPOINT_ID_O_URI = ContentUris.appendId(WaypointsColumns.CONTENT_URI.buildUpon(), WAYPOINT_ID_0).build();
protected static final SimpleDateFormat DATE_FORMAT_0 = new SimpleDateFormat(
"yyyy-MM-dd'T'hh:mm:ss'Z'", Locale.US);
protected static final SimpleDateFormat DATE_FORMAT_1 = new SimpleDateFormat(
"yyyy-MM-dd'T'HH:mm:ss.SSS", Locale.US);
protected static final long TRACK_ID_0 = 1;
protected static final long TRACK_ID_1 = 2;
protected static final long TRACK_POINT_ID_0 = 1;
protected static final long TRACK_POINT_ID_1 = 2;
protected static final long TRACK_POINT_ID_3 = 4;
protected static final long WAYPOINT_ID_0 = 1;
protected static final Uri TRACK_ID_0_URI = ContentUris.appendId(
TracksColumns.CONTENT_URI.buildUpon(), TRACK_ID_0).build();
protected static final Uri TRACK_ID_1_URI = ContentUris.appendId(
TracksColumns.CONTENT_URI.buildUpon(), TRACK_ID_1).build();
protected static final Uri WAYPOINT_ID_O_URI = ContentUris.appendId(
WaypointsColumns.CONTENT_URI.buildUpon(), WAYPOINT_ID_0).build();
protected final Context context = ApplicationProvider.getApplicationContext();
static {
/*
* We can't omit the timezones in the test, otherwise it'll use the local
* timezone and fail depending on where the test runner is.
*/
// We can't omit the timezones in the test, otherwise it'll use the local timezone and fail depending on where the test runner is.
SimpleTimeZone utc = new SimpleTimeZone(0, "UTC");
DATE_FORMAT_0.setTimeZone(utc);
DATE_FORMAT_1.setTimeZone(utc);
}
protected ContentProviderUtils contentProviderUtils;
@Mock
public ContentProviderUtils contentProviderUtils;
private Factory oldMyTracksProviderUtilsFactory;
@UsesMocks(ContentProviderUtils.class)
@Override
protected void setUp() throws Exception {
super.setUp();
contentProviderUtils = AndroidMock.createMock(ContentProviderUtils.class);
oldMyTracksProviderUtilsFactory = TestingProviderUtilsFactory.installWithInstance(
contentProviderUtils);
}
@Override
protected void tearDown() throws Exception {
TestingProviderUtilsFactory.restoreOldFactory(oldMyTracksProviderUtilsFactory);
super.tearDown();
}
protected Location createLocation(int index, long time) {
Location createLocation(int index, long time) {
Location location = new Location(LocationManager.GPS_PROVIDER);
location.setLatitude(TRACK_LATITUDE + index);
location.setLongitude(TRACK_LONGITUDE + index);
@@ -126,37 +100,35 @@ public class AbstractTestFileTrackImporter extends AndroidTestCase {
* @param trackPointId the track point id
*/
protected void expectFirstTrackPoint(Location location, long trackId, long trackPointId) {
expect(contentProviderUtils.bulkInsertTrackPoint(
location != null ? LocationsMatcher.eqLoc(location) : (Location[]) AndroidMock.anyObject(),
eq(1), eq(trackId))).andReturn(1);
expect(contentProviderUtils.getFirstTrackPointId(trackId)).andReturn(trackPointId);
expect(contentProviderUtils.getLastTrackPointId(trackId)).andReturn(trackPointId);
when(contentProviderUtils.bulkInsertTrackPoint(location != null ? (Location[]) any() : (Location[]) any(), eq(1), eq(trackId))).thenReturn(1);
when(contentProviderUtils.getFirstTrackPointId(trackId)).thenReturn(trackPointId);
when(contentProviderUtils.getLastTrackPointId(trackId)).thenReturn(trackPointId);
}
/**
* Expects the track to be updated.
*
* @param track the track
* @param lastTrack true if it is the last track in the gpx
* @param trackId the track id
* @param trackCaptor the track
* @param lastTrack true if it is the last track in the gpx
* @param trackId the track id
*/
protected void expectUpdateTrack(Capture<Track> track, boolean lastTrack, long trackId) {
contentProviderUtils.updateTrack(AndroidMock.capture(track));
expect(contentProviderUtils.insertWaypoint((Waypoint) AndroidMock.anyObject()))
.andReturn(WAYPOINT_ID_O_URI);
protected void expectUpdateTrack(ArgumentCaptor<Track> trackCaptor, boolean lastTrack, long trackId) {
contentProviderUtils.updateTrack(trackCaptor.capture());
when(contentProviderUtils.insertWaypoint((Waypoint) any())).thenReturn(WAYPOINT_ID_O_URI);
if (lastTrack) {
// Return null to not add waypoints
expect(contentProviderUtils.getTrack(trackId)).andReturn(null);
when(contentProviderUtils.getTrack(trackId)).thenReturn(null);
}
}
protected void verifyTrack(Track track, String name, String description, long time) {
assertEquals(name, track.getName());
assertEquals(description, track.getDescription());
Assert.assertEquals(name, track.getName());
Assert.assertEquals(description, track.getDescription());
if (time != -1L) {
assertEquals(time, track.getTripStatistics().getStartTime());
Assert.assertEquals(time, track.getTripStatistics().getStartTime());
}
assertNotSame(-1, track.getStartId());
assertNotSame(-1, track.getStopId());
Assert.assertNotSame(-1, track.getStartId());
Assert.assertNotSame(-1, track.getStopId());
}
}
@@ -18,24 +18,32 @@ package de.dennisguse.opentracks.io.file.importer;
import android.location.Location;
import de.dennisguse.opentracks.content.Track;
import de.dennisguse.opentracks.util.PreferencesUtils;
import de.dennisguse.opentracks.R;
import com.google.android.testing.mocking.AndroidMock;
import org.easymock.Capture;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.junit.MockitoJUnitRunner;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import static com.google.android.testing.mocking.AndroidMock.eq;
import static com.google.android.testing.mocking.AndroidMock.expect;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.Track;
import de.dennisguse.opentracks.util.PreferencesUtils;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.anyInt;
import static org.mockito.Mockito.anyLong;
import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
* Tests for {@link GpxFileTrackImporter}.
*
* @author Steffen Horlacher
*/
@RunWith(MockitoJUnitRunner.class)
public class GpxFileTrackImporterTest extends AbstractTestFileTrackImporter {
private static final String VALID_ONE_TRACK_ONE_SEGMENT_GPX = "<gpx><trk>"
@@ -49,18 +57,13 @@ public class GpxFileTrackImporterTest extends AbstractTestFileTrackImporter {
+ getNameAndDescription(TRACK_NAME_0, TRACK_DESCRIPTION_0) + "<trkseg>"
+ getTrackPoint(0, null) + getTrackPoint(1, null) + "</trkseg><trkseg>"
+ getTrackPoint(2, null) + getTrackPoint(3, null) + "</trkseg></trk></gpx>";
private static final String INVALID_XML_GPX = VALID_ONE_TRACK_ONE_SEGMENT_GPX.substring(
0, VALID_ONE_TRACK_ONE_SEGMENT_GPX.length() - 50);
private static final String INVALID_LOCATION_GPX = VALID_ONE_TRACK_ONE_SEGMENT_GPX.replaceAll(
Double.toString(TRACK_LATITUDE), "1000.0");
private static final String INVALID_TIME_GPX = VALID_ONE_TRACK_ONE_SEGMENT_GPX.replaceAll(
TRACK_TIME_0, "invalid");
private static final String INVALID_ALTITUDE_GPX = VALID_ONE_TRACK_ONE_SEGMENT_GPX.replaceAll(
Double.toString(TRACK_ELEVATION), "invalid");
private static final String INVALID_LATITUDE_GPX = VALID_ONE_TRACK_ONE_SEGMENT_GPX.replaceAll(
Double.toString(TRACK_LATITUDE), "invalid");
private static final String INVALID_LONGITUDE_GPX = VALID_ONE_TRACK_ONE_SEGMENT_GPX.replaceAll(
Double.toString(TRACK_LONGITUDE), "invalid");
private static final String INVALID_XML_GPX = VALID_ONE_TRACK_ONE_SEGMENT_GPX.substring(0, VALID_ONE_TRACK_ONE_SEGMENT_GPX.length() - 50);
private static final String INVALID_LOCATION_GPX = VALID_ONE_TRACK_ONE_SEGMENT_GPX.replaceAll(Double.toString(TRACK_LATITUDE), "1000.0");
private static final String INVALID_TIME_GPX = VALID_ONE_TRACK_ONE_SEGMENT_GPX.replaceAll(TRACK_TIME_0, "invalid");
private static final String INVALID_ALTITUDE_GPX = VALID_ONE_TRACK_ONE_SEGMENT_GPX.replaceAll(Double.toString(TRACK_ELEVATION), "invalid");
private static final String INVALID_LATITUDE_GPX = VALID_ONE_TRACK_ONE_SEGMENT_GPX.replaceAll(Double.toString(TRACK_LATITUDE), "invalid");
private static final String INVALID_LONGITUDE_GPX = VALID_ONE_TRACK_ONE_SEGMENT_GPX.replaceAll(Double.toString(TRACK_LONGITUDE), "invalid");
private static String getNameAndDescription(String name, String description) {
return "<name><![CDATA[" + name + "]]></name>" + "<desc><![CDATA[" + description + "]]></desc>";
@@ -70,9 +73,8 @@ public class GpxFileTrackImporterTest extends AbstractTestFileTrackImporter {
String latitude = Double.toString(TRACK_LATITUDE + index);
String longitude = Double.toString(TRACK_LONGITUDE + index);
String elevation = Double.toString(TRACK_ELEVATION + index);
StringBuffer buffer = new StringBuffer();
buffer.append(
"<trkpt lat=\"" + latitude + "\" lon=\"" + longitude + "\"><ele>" + elevation + "</ele>");
StringBuilder buffer = new StringBuilder();
buffer.append("<trkpt lat=\"" + latitude + "\" lon=\"" + longitude + "\"><ele>" + elevation + "</ele>");
if (time != null) {
buffer.append("<time>" + time + "</time>");
}
@@ -80,177 +82,129 @@ public class GpxFileTrackImporterTest extends AbstractTestFileTrackImporter {
return buffer.toString();
}
/**
* Tests one track with one segment.
*/
@Test
public void testOneTrackOneSegment() throws Exception {
Capture<Track> track = new Capture<Track>();
ArgumentCaptor<Track> trackCaptor = ArgumentCaptor.forClass(Track.class);
Location location0 = createLocation(0, DATE_FORMAT_0.parse(TRACK_TIME_0).getTime());
Location location1 = createLocation(1, DATE_FORMAT_1.parse(TRACK_TIME_1).getTime());
expect(contentProviderUtils.insertTrack((Track) AndroidMock.anyObject()))
.andReturn(TRACK_ID_0_URI);
when(contentProviderUtils.insertTrack((Track) any())).thenReturn(TRACK_ID_0_URI);
expectFirstTrackPoint(location0, TRACK_ID_0, TRACK_POINT_ID_0);
// A flush happens at the end
expect(contentProviderUtils.bulkInsertTrackPoint(
LocationsMatcher.eqLoc(location1), eq(1), eq(TRACK_ID_0))).andReturn(1);
expect(contentProviderUtils.getLastTrackPointId(TRACK_ID_0)).andReturn(TRACK_POINT_ID_1);
expect(
contentProviderUtils.getTrack(PreferencesUtils.getLong(getContext(),
R.string.recording_track_id_key))).andStubReturn(null);
expectUpdateTrack(track, true, TRACK_ID_0);
AndroidMock.replay(contentProviderUtils);
when(contentProviderUtils.bulkInsertTrackPoint((Location[]) any(), eq(1), eq(TRACK_ID_0))).thenReturn(1);
when(contentProviderUtils.getLastTrackPointId(TRACK_ID_0)).thenReturn(TRACK_POINT_ID_1);
when(contentProviderUtils.getTrack(PreferencesUtils.getLong(context, R.string.recording_track_id_key))).thenReturn(null);
expectUpdateTrack(trackCaptor, true, TRACK_ID_0);
InputStream inputStream = new ByteArrayInputStream(VALID_ONE_TRACK_ONE_SEGMENT_GPX.getBytes());
GpxFileTrackImporter gpxFileTrackImporter = new GpxFileTrackImporter(
getContext(), contentProviderUtils);
GpxFileTrackImporter gpxFileTrackImporter = new GpxFileTrackImporter(context, contentProviderUtils);
long trackId = gpxFileTrackImporter.importFile(inputStream);
assertEquals(TRACK_ID_0, trackId);
Assert.assertEquals(TRACK_ID_0, trackId);
long time0 = DATE_FORMAT_0.parse(TRACK_TIME_0).getTime();
long time1 = DATE_FORMAT_1.parse(TRACK_TIME_1).getTime();
assertEquals(time1 - time0, track.getValue().getTripStatistics().getTotalTime());
AndroidMock.verify(contentProviderUtils);
verifyTrack(track.getValue(), TRACK_NAME_0, TRACK_DESCRIPTION_0, time0);
Assert.assertEquals(time1 - time0, trackCaptor.getValue().getTripStatistics().getTotalTime());
verify(contentProviderUtils);
verifyTrack(trackCaptor.getValue(), TRACK_NAME_0, TRACK_DESCRIPTION_0, time0);
}
/**
* Tests one track with two segments.
*/
@Test
public void testOneTrackTwoSegments() throws Exception {
Capture<Track> track = new Capture<Track>();
ArgumentCaptor<Track> trackCaptor = ArgumentCaptor.forClass(Track.class);
Location location0 = createLocation(0, DATE_FORMAT_0.parse(TRACK_TIME_0).getTime());
expect(contentProviderUtils.insertTrack((Track) AndroidMock.anyObject()))
.andReturn(TRACK_ID_0_URI);
when(contentProviderUtils.insertTrack((Track) any())).thenReturn(TRACK_ID_0_URI);
expectFirstTrackPoint(location0, TRACK_ID_0, TRACK_POINT_ID_0);
// A flush happens at the end
expect(contentProviderUtils.bulkInsertTrackPoint(
(Location[]) AndroidMock.anyObject(), eq(5), eq(TRACK_ID_0))).andStubReturn(5);
expect(contentProviderUtils.getLastTrackPointId(TRACK_ID_0)).andReturn(TRACK_POINT_ID_3);
expect(
contentProviderUtils.getTrack(PreferencesUtils.getLong(getContext(),
R.string.recording_track_id_key))).andStubReturn(null);
expectUpdateTrack(track, true, TRACK_ID_0);
AndroidMock.replay(contentProviderUtils);
when(contentProviderUtils.bulkInsertTrackPoint((Location[]) any(), eq(5), eq(TRACK_ID_0))).thenReturn(5);
when(contentProviderUtils.getLastTrackPointId(TRACK_ID_0)).thenReturn(TRACK_POINT_ID_3);
when(contentProviderUtils.getTrack(PreferencesUtils.getLong(context, R.string.recording_track_id_key))).thenReturn(null);
expectUpdateTrack(trackCaptor, true, TRACK_ID_0);
InputStream inputStream = new ByteArrayInputStream(VALID_ONE_TRACK_TWO_SEGMENTS_GPX.getBytes());
GpxFileTrackImporter gpxFileTrackImporter = new GpxFileTrackImporter(
getContext(), contentProviderUtils);
GpxFileTrackImporter gpxFileTrackImporter = new GpxFileTrackImporter(context, contentProviderUtils);
long trackId = gpxFileTrackImporter.importFile(inputStream);
assertEquals(TRACK_ID_0, trackId);
Assert.assertEquals(TRACK_ID_0, trackId);
long time0 = DATE_FORMAT_0.parse(TRACK_TIME_0).getTime();
long time1 = DATE_FORMAT_1.parse(TRACK_TIME_1).getTime();
long time2 = DATE_FORMAT_1.parse(TRACK_TIME_2).getTime();
long time3 = DATE_FORMAT_1.parse(TRACK_TIME_3).getTime();
assertEquals(
time1 - time0 + time3 - time2, track.getValue().getTripStatistics().getTotalTime());
Assert.assertEquals(time1 - time0 + time3 - time2, trackCaptor.getValue().getTripStatistics().getTotalTime());
AndroidMock.verify(contentProviderUtils);
verifyTrack(track.getValue(), TRACK_NAME_0, TRACK_DESCRIPTION_0,
DATE_FORMAT_0.parse(TRACK_TIME_0).getTime());
verify(contentProviderUtils);
verifyTrack(trackCaptor.getValue(), TRACK_NAME_0, TRACK_DESCRIPTION_0, DATE_FORMAT_0.parse(TRACK_TIME_0).getTime());
}
/**
* Tests one track with two segments, but no time in the track points.
*/
public void testOneTrackTwoSegmentsNoTime() throws Exception {
Capture<Track> track = new Capture<Track>();
@Test
public void testOneTrackTwoSegmentsNoTime() {
ArgumentCaptor<Track> trackCaptor = ArgumentCaptor.forClass(Track.class);
expect(contentProviderUtils.insertTrack((Track) AndroidMock.anyObject()))
.andReturn(TRACK_ID_0_URI);
when(contentProviderUtils.insertTrack((Track) any())).thenReturn(TRACK_ID_0_URI);
expectFirstTrackPoint(null, TRACK_ID_0, TRACK_POINT_ID_0);
// A flush happens at the end
expect(contentProviderUtils.bulkInsertTrackPoint(
(Location[]) AndroidMock.anyObject(), eq(5), eq(TRACK_ID_0))).andStubReturn(5);
expect(contentProviderUtils.getLastTrackPointId(TRACK_ID_0)).andReturn(TRACK_POINT_ID_3);
expect(
contentProviderUtils.getTrack(PreferencesUtils.getLong(getContext(),
R.string.recording_track_id_key))).andStubReturn(null);
expectUpdateTrack(track, true, TRACK_ID_0);
AndroidMock.replay(contentProviderUtils);
when(contentProviderUtils.bulkInsertTrackPoint((Location[]) any(), eq(5), eq(TRACK_ID_0))).thenReturn(5);
when(contentProviderUtils.getLastTrackPointId(TRACK_ID_0)).thenReturn(TRACK_POINT_ID_3);
when(contentProviderUtils.getTrack(PreferencesUtils.getLong(context, R.string.recording_track_id_key))).thenReturn(null);
expectUpdateTrack(trackCaptor, true, TRACK_ID_0);
InputStream inputStream = new ByteArrayInputStream(
VALID_ONE_TRACK_TWO_SEGMENTS_NO_TIME_GPX.getBytes());
GpxFileTrackImporter gpxFileTrackImporter = new GpxFileTrackImporter(
getContext(), contentProviderUtils);
InputStream inputStream = new ByteArrayInputStream(VALID_ONE_TRACK_TWO_SEGMENTS_NO_TIME_GPX.getBytes());
GpxFileTrackImporter gpxFileTrackImporter = new GpxFileTrackImporter(context, contentProviderUtils);
long trackId = gpxFileTrackImporter.importFile(inputStream);
assertEquals(TRACK_ID_0, trackId);
assertEquals(0, track.getValue().getTripStatistics().getTotalTime());
Assert.assertEquals(TRACK_ID_0, trackId);
Assert.assertEquals(0, trackCaptor.getValue().getTripStatistics().getTotalTime());
AndroidMock.verify(contentProviderUtils);
verifyTrack(track.getValue(), TRACK_NAME_0, TRACK_DESCRIPTION_0, -1L);
verify(contentProviderUtils);
verifyTrack(trackCaptor.getValue(), TRACK_NAME_0, TRACK_DESCRIPTION_0, -1L);
}
/**
* Test an invalid xml input.
*/
public void testInvalidXml() throws Exception {
@Test
public void testInvalidXml() {
testInvalidGpx(INVALID_XML_GPX);
}
/**
* Test an invalid location.
*/
public void testInvalidLocation() throws Exception {
@Test
public void testInvalidLocation() {
testInvalidGpx(INVALID_LOCATION_GPX);
}
/**
* Test an invalid time.
*/
public void testInvalidTime() throws Exception {
@Test
public void testInvalidTime() {
testInvalidGpx(INVALID_TIME_GPX);
}
/**
* Test an invalid altitude.
*/
public void testInvalidAltitude() throws Exception {
@Test
public void testInvalidAltitude() {
testInvalidGpx(INVALID_ALTITUDE_GPX);
}
/**
* Test an invalid latitude.
*/
public void testInvalidLatitude() throws Exception {
@Test
public void testInvalidLatitude() {
testInvalidGpx(INVALID_LATITUDE_GPX);
}
/**
* Test an invalid longitude.
*/
public void testInvalidLongitude() throws Exception {
@Test
public void testInvalidLongitude() {
testInvalidGpx(INVALID_LONGITUDE_GPX);
}
private void testInvalidGpx(String xml) throws Exception {
expect(contentProviderUtils.insertTrack((Track) AndroidMock.anyObject()))
.andReturn(TRACK_ID_0_URI);
private void testInvalidGpx(String xml) {
when(contentProviderUtils.insertTrack((Track) any())).thenReturn(TRACK_ID_0_URI);
// For the following, use StubReturn since we don't care whether they are
// invoked or not.
expect(contentProviderUtils.bulkInsertTrackPoint(
(Location[]) AndroidMock.anyObject(), AndroidMock.anyInt(), AndroidMock.anyLong()))
.andStubReturn(1);
expect(contentProviderUtils.getFirstTrackPointId(TRACK_ID_0)).andStubReturn(TRACK_POINT_ID_0);
expect(contentProviderUtils.getLastTrackPointId(TRACK_ID_0)).andStubReturn(TRACK_POINT_ID_0);
expect(
contentProviderUtils.getTrack(PreferencesUtils.getLong(getContext(),
R.string.recording_track_id_key))).andStubReturn(null);
contentProviderUtils.deleteTrack(getContext(), TRACK_ID_0);
AndroidMock.replay(contentProviderUtils);
// For the following, use StubReturn since we don't care whether they are invoked or not.
when(contentProviderUtils.bulkInsertTrackPoint((Location[]) any(), anyInt(), anyLong())).thenReturn(1);
when(contentProviderUtils.getFirstTrackPointId(TRACK_ID_0)).thenReturn(TRACK_POINT_ID_0);
when(contentProviderUtils.getLastTrackPointId(TRACK_ID_0)).thenReturn(TRACK_POINT_ID_0);
when(contentProviderUtils.getTrack(PreferencesUtils.getLong(context, R.string.recording_track_id_key))).thenReturn(null);
contentProviderUtils.deleteTrack(context, TRACK_ID_0);
InputStream inputStream = new ByteArrayInputStream(xml.getBytes());
GpxFileTrackImporter gpxFileTrackImporter = new GpxFileTrackImporter(
getContext(), contentProviderUtils);
GpxFileTrackImporter gpxFileTrackImporter = new GpxFileTrackImporter(context, contentProviderUtils);
long trackId = gpxFileTrackImporter.importFile(inputStream);
assertEquals(-1L, trackId);
AndroidMock.verify(contentProviderUtils);
Assert.assertEquals(-1L, trackId);
}
}
@@ -18,33 +18,37 @@ package de.dennisguse.opentracks.io.file.importer;
import android.location.Location;
import de.dennisguse.opentracks.content.Track;
import de.dennisguse.opentracks.util.PreferencesUtils;
import de.dennisguse.opentracks.R;
import com.google.android.testing.mocking.AndroidMock;
import org.easymock.Capture;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.junit.MockitoJUnitRunner;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import static com.google.android.testing.mocking.AndroidMock.eq;
import static com.google.android.testing.mocking.AndroidMock.expect;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.Track;
import de.dennisguse.opentracks.util.PreferencesUtils;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
* Tests for {@link KmlFileTrackImporter}.
*
* @author Jimmy Shih
*/
@RunWith(MockitoJUnitRunner.class)
public class KmlFileTrackImporterTest extends AbstractTestFileTrackImporter {
private static final String VALID_ONE_TRACK_ONE_SEGMENT_GPX =
"<kml xmlns:gx=\"http://www.google.com/kml/ext/2.2\"><Placemark>"
private static final String VALID_ONE_TRACK_ONE_SEGMENT_GPX = "<kml xmlns:gx=\"http://www.google.com/kml/ext/2.2\"><Placemark>"
+ getNameAndDescription(TRACK_NAME_0, TRACK_DESCRIPTION_0) + "<gx:MultiTrack><gx:Track>"
+ getTrackPoint(0, TRACK_TIME_0) + getTrackPoint(1, TRACK_TIME_1)
+ "</gx:Track></gx:MultiTrack></Placemark></kml>";
private static final String VALID_ONE_TRACK_TWO_SEGMENTS_GPX =
"<kml xmlns:gx=\"http://www.google.com/kml/ext/2.2\"><Placemark>"
private static final String VALID_ONE_TRACK_TWO_SEGMENTS_GPX = "<kml xmlns:gx=\"http://www.google.com/kml/ext/2.2\"><Placemark>"
+ getNameAndDescription(TRACK_NAME_0, TRACK_DESCRIPTION_0) + "<gx:MultiTrack><gx:Track>"
+ getTrackPoint(0, TRACK_TIME_0) + getTrackPoint(1, TRACK_TIME_1) + "</gx:Track><gx:Track>"
+ getTrackPoint(2, TRACK_TIME_2) + getTrackPoint(3, TRACK_TIME_3)
@@ -59,83 +63,64 @@ public class KmlFileTrackImporterTest extends AbstractTestFileTrackImporter {
String latitude = Double.toString(TRACK_LATITUDE + index);
String longitude = Double.toString(TRACK_LONGITUDE + index);
String altitude = Double.toString(TRACK_ELEVATION + index);
StringBuffer buffer = new StringBuffer();
buffer.append("<when>" + time + "</when>" + "<gx:coord>" + longitude + " " + latitude + " "
+ altitude + "</gx:coord>");
return buffer.toString();
return "<when>" + time + "</when>" + "<gx:coord>" + longitude + " " + latitude + " " + altitude + "</gx:coord>";
}
/**
* Tests one track with one segment.
*/
@Test
public void testOneTrackOneSegment() throws Exception {
Capture<Track> track = new Capture<Track>();
ArgumentCaptor<Track> trackCaptor = ArgumentCaptor.forClass(Track.class);
Location location0 = createLocation(0, DATE_FORMAT_0.parse(TRACK_TIME_0).getTime());
Location location1 = createLocation(1, DATE_FORMAT_1.parse(TRACK_TIME_1).getTime());
contentProviderUtils.clearTrack(getContext(), TRACK_ID_0);
contentProviderUtils.clearTrack(context, TRACK_ID_0);
expectFirstTrackPoint(location0, TRACK_ID_0, TRACK_POINT_ID_0);
// A flush happens at the end
expect(contentProviderUtils.bulkInsertTrackPoint(
LocationsMatcher.eqLoc(location1), eq(1), eq(TRACK_ID_0))).andReturn(1);
expect(contentProviderUtils.getLastTrackPointId(TRACK_ID_0)).andReturn(TRACK_POINT_ID_1);
expect(
contentProviderUtils.getTrack(PreferencesUtils.getLong(getContext(),
R.string.recording_track_id_key))).andStubReturn(null);
expectUpdateTrack(track, true, TRACK_ID_0);
AndroidMock.replay(contentProviderUtils);
when(contentProviderUtils.bulkInsertTrackPoint(new Location[]{location1}, eq(1), eq(TRACK_ID_0))).thenReturn(1);
when(contentProviderUtils.getLastTrackPointId(TRACK_ID_0)).thenReturn(TRACK_POINT_ID_1);
when(contentProviderUtils.getTrack(PreferencesUtils.getLong(context, R.string.recording_track_id_key))).thenReturn(null);
expectUpdateTrack(trackCaptor, true, TRACK_ID_0);
InputStream inputStream = new ByteArrayInputStream(VALID_ONE_TRACK_ONE_SEGMENT_GPX.getBytes());
KmlFileTrackImporter kmlFileTrackImporter = new KmlFileTrackImporter(
getContext(), TRACK_ID_0, contentProviderUtils);
KmlFileTrackImporter kmlFileTrackImporter = new KmlFileTrackImporter(context, TRACK_ID_0, contentProviderUtils);
long trackId = kmlFileTrackImporter.importFile(inputStream);
assertEquals(TRACK_ID_0, trackId);
Assert.assertEquals(TRACK_ID_0, trackId);
long time0 = DATE_FORMAT_0.parse(TRACK_TIME_0).getTime();
long time1 = DATE_FORMAT_1.parse(TRACK_TIME_1).getTime();
assertEquals(time1 - time0, track.getValue().getTripStatistics().getTotalTime());
AndroidMock.verify(contentProviderUtils);
verifyTrack(track.getValue(), TRACK_NAME_0, TRACK_DESCRIPTION_0, time0);
Assert.assertEquals(time1 - time0, trackCaptor.getValue().getTripStatistics().getTotalTime());
verify(contentProviderUtils);
verifyTrack(trackCaptor.getValue(), TRACK_NAME_0, TRACK_DESCRIPTION_0, time0);
}
/**
* Tests one track with two segments.
*/
@Test
public void testOneTrackTwoSegments() throws Exception {
Capture<Track> track = new Capture<Track>();
ArgumentCaptor<Track> trackCaptor = ArgumentCaptor.forClass(Track.class);
Location location0 = createLocation(0, DATE_FORMAT_0.parse(TRACK_TIME_0).getTime());
contentProviderUtils.clearTrack(getContext(), TRACK_ID_0);
contentProviderUtils.clearTrack(context, TRACK_ID_0);
expectFirstTrackPoint(location0, TRACK_ID_0, TRACK_POINT_ID_0);
// A flush happens at the end
expect(contentProviderUtils.bulkInsertTrackPoint(
(Location[]) AndroidMock.anyObject(), eq(5), eq(TRACK_ID_0))).andStubReturn(5);
expect(contentProviderUtils.getLastTrackPointId(TRACK_ID_0)).andReturn(TRACK_POINT_ID_3);
expect(
contentProviderUtils.getTrack(PreferencesUtils.getLong(getContext(),
R.string.recording_track_id_key))).andStubReturn(null);
expectUpdateTrack(track, true, TRACK_ID_0);
AndroidMock.replay(contentProviderUtils);
when(contentProviderUtils.bulkInsertTrackPoint((Location[]) any(), eq(5), eq(TRACK_ID_0))).thenReturn(5);
when(contentProviderUtils.getLastTrackPointId(TRACK_ID_0)).thenReturn(TRACK_POINT_ID_3);
when(contentProviderUtils.getTrack(PreferencesUtils.getLong(context, R.string.recording_track_id_key))).thenReturn(null);
expectUpdateTrack(trackCaptor, true, TRACK_ID_0);
InputStream inputStream = new ByteArrayInputStream(VALID_ONE_TRACK_TWO_SEGMENTS_GPX.getBytes());
KmlFileTrackImporter kmlFileTrackImporter = new KmlFileTrackImporter(
getContext(), TRACK_ID_0, contentProviderUtils);
KmlFileTrackImporter kmlFileTrackImporter = new KmlFileTrackImporter(context, TRACK_ID_0, contentProviderUtils);
long trackId = kmlFileTrackImporter.importFile(inputStream);
assertEquals(TRACK_ID_0, trackId);
Assert.assertEquals(TRACK_ID_0, trackId);
long time0 = DATE_FORMAT_0.parse(TRACK_TIME_0).getTime();
long time1 = DATE_FORMAT_1.parse(TRACK_TIME_1).getTime();
long time2 = DATE_FORMAT_1.parse(TRACK_TIME_2).getTime();
long time3 = DATE_FORMAT_1.parse(TRACK_TIME_3).getTime();
assertEquals(
time1 - time0 + time3 - time2, track.getValue().getTripStatistics().getTotalTime());
Assert.assertEquals(time1 - time0 + time3 - time2, trackCaptor.getValue().getTripStatistics().getTotalTime());
AndroidMock.verify(contentProviderUtils);
verifyTrack(track.getValue(), TRACK_NAME_0, TRACK_DESCRIPTION_0,
DATE_FORMAT_0.parse(TRACK_TIME_0).getTime());
verify(contentProviderUtils);
verifyTrack(trackCaptor.getValue(), TRACK_NAME_0, TRACK_DESCRIPTION_0, DATE_FORMAT_0.parse(TRACK_TIME_0).getTime());
}
}
@@ -1,81 +0,0 @@
/*
* Copyright 2012 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 de.dennisguse.opentracks.io.file.importer;
import android.location.Location;
import com.google.android.testing.mocking.AndroidMock;
import org.easymock.IArgumentMatcher;
import java.util.Arrays;
/**
* Locations matcher. Workaround because of capture bug 2617107 in easymock:
* http://sourceforge.net/tracker/?func=detail&aid=2617107&group_id=82958&atid=567837
*
* @author Jimmy Shih
*/
public class LocationsMatcher implements IArgumentMatcher {
private final Location[] expectedLocations;
private LocationsMatcher(Location[] expected) {
this.expectedLocations = expected;
}
public static Location[] eqLoc(Location expected) {
return eqLoc(new Location[]{expected});
}
public static Location[] eqLoc(Location[] expected) {
IArgumentMatcher matcher = new LocationsMatcher(expected);
AndroidMock.reportMatcher(matcher);
return null;
}
@Override
public void appendTo(StringBuffer buf) {
buf.append("eqLoc(").append(Arrays.toString(expectedLocations)).append(")");
}
@Override
public boolean matches(Object obj) {
if (!(obj instanceof Location[])) {
return false;
}
Location[] locations = (Location[]) obj;
if (locations.length < expectedLocations.length) {
return false;
}
// Only check the first elements (those that will be taken into account)
for (int i = 0; i < expectedLocations.length; i++) {
if (!matchLocation(locations[i], expectedLocations[i])) {
return false;
}
}
return true;
}
private boolean matchLocation(Location location1, Location location2) {
return (location1.getTime() == location2.getTime())
&& (location1.getLatitude() == location2.getLatitude())
&& (location1.getLongitude() == location2.getLongitude())
&& (location1.getAltitude() == location2.getAltitude());
}
}
@@ -40,9 +40,7 @@ public class AdaptiveLocationListenerPolicyTest extends TestCase {
}
/**
* Tests the
* {@link AdaptiveLocationListenerPolicy#getDesiredPollingInterval()} in four
* situations.
* Tests the {@link AdaptiveLocationListenerPolicy#getDesiredPollingInterval()} in four situations.
* <ul>
* <li>The newIdleTime is bigger than max interval.</li>
* <li>The newIdleTime is between min and max interval.</li>
@@ -68,9 +66,9 @@ public class AdaptiveLocationListenerPolicyTest extends TestCase {
}
/**
* Tests the method {@link AdaptiveLocationListenerPolicy#getMinDistance()}.
* Tests the method {@link AdaptiveLocationListenerPolicy#getMinDistance_m()}.
*/
public void testGetMinDistance() {
assertEquals(MIN_DISTANCE, adocationListenerPolicy.getMinDistance());
assertEquals(MIN_DISTANCE, adocationListenerPolicy.getMinDistance_m());
}
}
@@ -1,105 +0,0 @@
/*
* Copyright 2012 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 de.dennisguse.opentracks.services;
import android.content.Context;
import android.content.Intent;
import android.os.RemoteException;
import android.test.ServiceTestCase;
import de.dennisguse.opentracks.R;
import com.google.android.testing.mocking.UsesMocks;
import org.easymock.EasyMock;
/**
* Tests {@link ControlRecordingService}.
*
* @author Youtao Liu
*/
public class ControlRecordingServiceTest extends ServiceTestCase<ControlRecordingService> {
private Context context;
private ControlRecordingService controlRecordingService;
public ControlRecordingServiceTest() {
super(ControlRecordingService.class);
}
@Override
protected void setUp() throws Exception {
super.setUp();
context = getContext();
}
/**
* Tests the start of {@link ControlRecordingService} and tests the method
* {@link ControlRecordingService#onHandleIntent(Intent, ITrackRecordingService)}
* to start a track recording.
*/
@UsesMocks(ITrackRecordingService.class)
public void testStartRecording() {
assertNull(controlRecordingService);
Intent intent = startControlRecordingService(context.getString(R.string.track_action_start));
assertNotNull(controlRecordingService);
ITrackRecordingService iTrackRecordingServiceMock = EasyMock
.createStrictMock(ITrackRecordingService.class);
try {
EasyMock.expect(iTrackRecordingServiceMock.startNewTrack()).andReturn(1L);
EasyMock.replay(iTrackRecordingServiceMock);
controlRecordingService.onHandleIntent(intent, iTrackRecordingServiceMock);
EasyMock.verify(iTrackRecordingServiceMock);
} catch (RemoteException e) {
fail();
}
}
/**
* Tests the method
* {@link ControlRecordingService#onHandleIntent(Intent, ITrackRecordingService)}
* to stop a track recording.
*/
@UsesMocks(ITrackRecordingService.class)
public void testStopRecording() {
Intent intent = startControlRecordingService(context.getString(R.string.track_action_end));
ITrackRecordingService iTrackRecordingServiceMock = EasyMock
.createStrictMock(ITrackRecordingService.class);
try {
iTrackRecordingServiceMock.endCurrentTrack();
EasyMock.replay(iTrackRecordingServiceMock);
controlRecordingService.onHandleIntent(intent, iTrackRecordingServiceMock);
EasyMock.verify(iTrackRecordingServiceMock);
} catch (RemoteException e) {
fail();
}
}
/**
* Starts a ControlRecordingService with a specified action.
*
* @param action the action string in the start intent
*/
private Intent startControlRecordingService(String action) {
Intent intent = new Intent(context, ControlRecordingService.class);
intent.setAction(action);
startService(intent);
controlRecordingService = getService();
return intent;
}
}
@@ -16,28 +16,35 @@
package de.dennisguse.opentracks.services;
import android.content.BroadcastReceiver;
import android.content.ContentResolver;
import android.content.ContentProvider;
import android.content.Context;
import android.content.ContextWrapper;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.database.Cursor;
import android.location.Location;
import android.net.Uri;
import android.os.Bundle;
import android.os.IBinder;
import android.os.RemoteException;
import android.provider.Settings;
import android.test.RenamingDelegatingContext;
import android.test.ServiceTestCase;
import android.test.mock.MockContentProvider;
import android.test.mock.MockContentResolver;
import android.test.suitebuilder.annotation.MediumTest;
import android.test.suitebuilder.annotation.SmallTest;
import android.util.Log;
import de.dennisguse.opentracks.Constants;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.MediumTest;
import androidx.test.filters.SmallTest;
import androidx.test.rule.GrantPermissionRule;
import androidx.test.rule.ServiceTestRule;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.ContentProviderUtils;
import de.dennisguse.opentracks.content.CustomContentProvider;
import de.dennisguse.opentracks.content.Track;
@@ -46,565 +53,295 @@ import de.dennisguse.opentracks.content.Waypoint.WaypointType;
import de.dennisguse.opentracks.content.WaypointCreationRequest;
import de.dennisguse.opentracks.stats.TripStatistics;
import de.dennisguse.opentracks.util.PreferencesUtils;
import de.dennisguse.opentracks.R;
import java.util.ArrayList;
import java.util.List;
/**
* Tests for the track recording service.
* <p>
* TODO: The original class, ServiceTestCase, has a few limitations, e.g. it's
* not possible to properly shutdown the service, unless tearDown() is called,
* which prevents from testing multiple scenarios in a single test (see
* runFunctionTest for more details).
*
* @author Bartlomiej Niechwiej
* <p>
* ATTENTION: This tests deletes all stored tracks in the database.
* So, if it is executed on a real device, data might be lost.
*/
public class TrackRecordingServiceTest extends ServiceTestCase<TestRecordingService> {
@RunWith(AndroidJUnit4.class)
public class TrackRecordingServiceTest {
private Context context;
@Rule
public final ServiceTestRule mServiceRule = ServiceTestRule.withTimeout(5, TimeUnit.SECONDS);
@Rule
public GrantPermissionRule mRuntimePermissionRule = GrantPermissionRule.grant(android.Manifest.permission.ACCESS_FINE_LOCATION);
private Context context = ApplicationProvider.getApplicationContext();
private ContentProviderUtils providerUtils;
/*
* In order to support starting and binding to the service in the same unit
* test, we provide a workaround, as the original class doesn't allow to bind
* after the service has been previously started.
*/
private long trackId = Math.abs(new Random().nextLong());
private boolean bound;
private Intent serviceIntent;
public TrackRecordingServiceTest() {
super(TestRecordingService.class);
static Intent createStartIntent(Context context) {
Intent startIntent = new Intent();
startIntent.setClass(context, TrackRecordingService.class);
return startIntent;
}
@Override
protected IBinder bindService(Intent intent) {
if (getService() != null) {
if (bound) {
throw new IllegalStateException("Service: " + getService() + " is already bound");
}
bound = true;
serviceIntent = intent.cloneFilter();
return getService().onBind(intent);
} else {
return super.bindService(intent);
}
static void updateAutoResumePrefs(Context context, int attempts, int timeoutMins) {
PreferencesUtils.setInt(context, R.string.auto_resume_track_current_retry_key, attempts);
PreferencesUtils.setInt(context, R.string.auto_resume_track_timeout_key, timeoutMins);
}
@Override
protected void shutdownService() {
if (bound) {
assertNotNull(getService());
getService().onUnbind(serviceIntent);
bound = false;
}
super.shutdownService();
}
@Override
protected void setUp() throws Exception {
super.setUp();
/*
* Create a mock context that uses a mock content resolver and a renaming
* delegating context.
*/
MockContentResolver mockContentResolver = new MockContentResolver();
RenamingDelegatingContext renamingDelegatingContext = new RenamingDelegatingContext(
getContext(), getContext(), "test.");
context = new MockContext(mockContentResolver, renamingDelegatingContext);
@Before
public void setUp() {
// Set up the mock content resolver
CustomContentProvider customContentProvider = new CustomContentProvider();
customContentProvider.attachInfo(context, null);
mockContentResolver.addProvider(ContentProviderUtils.AUTHORITY, customContentProvider);
MockContentProvider settingsProvider = new MockContentProvider(context) {
@Override
public Bundle call(String method, String arg, Bundle extras) {
return null;
}
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
String sortOrder) {
return null;
}
ContentProvider customContentProvider = new CustomContentProvider() {
};
mockContentResolver.addProvider(Settings.AUTHORITY, settingsProvider);
// Set the context
setContext(context);
customContentProvider.attachInfo(context, null);
providerUtils = ContentProviderUtils.Factory.get(context);
SharedPreferences sharedPreferences = context.getSharedPreferences(Constants.SETTINGS_NAME, Context.MODE_PRIVATE);
// Let's use default values.
SharedPreferences sharedPreferences = PreferencesUtils.getSharedPreferences(context);
sharedPreferences.edit().clear().apply();
// Disable auto resume by default.
updateAutoResumePrefs(PreferencesUtils.AUTO_RESUME_TRACK_CURRENT_RETRY_DEFAULT, 0);
// No recording track.
PreferencesUtils.setLong(
context, R.string.recording_track_id_key, PreferencesUtils.RECORDING_TRACK_ID_DEFAULT);
updateAutoResumePrefs(context, PreferencesUtils.AUTO_RESUME_TRACK_CURRENT_RETRY_DEFAULT, 0);
// Ensure that the database is empty before every test
providerUtils.deleteAllTracks(context);
}
@After
public void tearDown() throws TimeoutException {
// Reset service (if some previous test failed)
ITrackRecordingService service = ((ITrackRecordingService) mServiceRule.bindService(createStartIntent(context)));
if (service.isRecording() || service.isPaused()) {
service.endCurrentTrack();
}
// Ensure that the database is empty after every test
providerUtils.deleteAllTracks(context);
}
@SmallTest
public void testStartable() {
startService(createStartIntent());
assertNotNull(getService());
@Test
public void testStartable() throws TimeoutException {
mServiceRule.startService(createStartIntent(context));
Assert.assertNotNull(mServiceRule.bindService(createStartIntent(context)));
}
@MediumTest
public void testBindable() {
IBinder service = bindService(createStartIntent());
assertNotNull(service);
@Test
public void testBindable() throws TimeoutException {
IBinder service = mServiceRule.bindService(createStartIntent(context));
Assert.assertNotNull(service);
}
@MediumTest
@Test
public void testResumeAfterReboot_shouldResume() throws Exception {
// Insert a dummy track and mark it as recording track.
createDummyTrack(123L, System.currentTimeMillis(), true);
createDummyTrack(trackId, System.currentTimeMillis(), true);
// Clear the number of attempts and set the timeout to 10 min.
updateAutoResumePrefs(PreferencesUtils.AUTO_RESUME_TRACK_CURRENT_RETRY_DEFAULT,
PreferencesUtils.AUTO_RESUME_TRACK_TIMEOUT_DEFAULT);
updateAutoResumePrefs(context, PreferencesUtils.AUTO_RESUME_TRACK_CURRENT_RETRY_DEFAULT, PreferencesUtils.AUTO_RESUME_TRACK_TIMEOUT_DEFAULT);
// Start the service in "resume" mode (simulates the on-reboot action).
Intent startIntent = createStartIntent();
Intent startIntent = createStartIntent(context);
startIntent.putExtra(TrackRecordingService.RESUME_TRACK_EXTRA_NAME, true);
startService(startIntent);
assertNotNull(getService());
ITrackRecordingService service = ((ITrackRecordingService) mServiceRule.bindService(startIntent));
Assert.assertNotNull(service);
// We expect to resume the previous track.
assertTrue(getService().isRecording());
ITrackRecordingService service = bindAndGetService(createStartIntent());
assertEquals(123L, service.getRecordingTrackId());
Assert.assertTrue(service.isRecording());
Assert.assertEquals(trackId, service.getRecordingTrackId());
}
/*
* TODO: shutdownService() has a bug and doesn't set mServiceCreated to false,
* thus preventing from a second call to onCreate(). Report the bug to Android
* team. Until then, the following tests and checks must be commented out.
*
* TODO: If fixed, remove "disabled" prefix from the test name.
*/
@MediumTest
public void disabledTestResumeAfterReboot_simulateReboot() throws Exception {
updateAutoResumePrefs(PreferencesUtils.AUTO_RESUME_TRACK_CURRENT_RETRY_DEFAULT,
PreferencesUtils.AUTO_RESUME_TRACK_TIMEOUT_DEFAULT);
ITrackRecordingService service = bindAndGetService(createStartIntent());
assertFalse(service.isRecording());
@Test
public void testResumeAfterReboot_simulateReboot() throws Exception {
updateAutoResumePrefs(context, PreferencesUtils.AUTO_RESUME_TRACK_CURRENT_RETRY_DEFAULT, PreferencesUtils.AUTO_RESUME_TRACK_TIMEOUT_DEFAULT);
ITrackRecordingService service = ((ITrackRecordingService) mServiceRule.bindService(createStartIntent(context)));
Assert.assertFalse(service.isRecording());
// Simulate recording a track.
long id = service.startNewTrack();
assertTrue(service.isRecording());
assertEquals(id, service.getRecordingTrackId());
shutdownService();
assertEquals(id, PreferencesUtils.getLong(context, R.string.recording_track_id_key));
Assert.assertTrue(service.isRecording());
Assert.assertEquals(id, service.getRecordingTrackId());
mServiceRule.unbindService();
Assert.assertEquals(id, PreferencesUtils.getLong(context, R.string.recording_track_id_key));
// Start the service in "resume" mode (simulates the on-reboot action).
Intent startIntent = createStartIntent();
Intent startIntent = createStartIntent(context);
startIntent.putExtra(TrackRecordingService.RESUME_TRACK_EXTRA_NAME, true);
startService(startIntent);
assertNotNull(getService());
mServiceRule.startService(startIntent);
service = ((ITrackRecordingService) mServiceRule.bindService(createStartIntent(context)));
Assert.assertNotNull(service);
assertTrue(getService().isRecording());
Assert.assertTrue(service.isRecording());
}
@MediumTest
@Test
public void testResumeAfterReboot_noRecordingTrack() throws Exception {
// Insert a dummy track and mark it as recording track.
createDummyTrack(123L, System.currentTimeMillis(), false);
createDummyTrack(trackId, System.currentTimeMillis(), false);
// Clear the number of attempts and set the timeout to 10 min.
updateAutoResumePrefs(PreferencesUtils.AUTO_RESUME_TRACK_CURRENT_RETRY_DEFAULT,
PreferencesUtils.AUTO_RESUME_TRACK_TIMEOUT_DEFAULT);
updateAutoResumePrefs(context, PreferencesUtils.AUTO_RESUME_TRACK_CURRENT_RETRY_DEFAULT, PreferencesUtils.AUTO_RESUME_TRACK_TIMEOUT_DEFAULT);
// Start the service in "resume" mode (simulates the on-reboot action).
Intent startIntent = createStartIntent();
Intent startIntent = createStartIntent(context);
startIntent.putExtra(TrackRecordingService.RESUME_TRACK_EXTRA_NAME, true);
startService(startIntent);
assertNotNull(getService());
ITrackRecordingService service = ((ITrackRecordingService) mServiceRule.bindService(startIntent));
Assert.assertNotNull(service);
// We don't expect to resume the previous track, because it was stopped.
assertFalse(getService().isRecording());
ITrackRecordingService service = bindAndGetService(createStartIntent());
assertEquals(-1L, service.getRecordingTrackId());
Assert.assertFalse(service.isRecording());
Assert.assertEquals(-1L, service.getRecordingTrackId());
}
@MediumTest
@Test
public void testResumeAfterReboot_expiredTrack() throws Exception {
// Insert a dummy track last updated 20 min ago.
createDummyTrack(123L, System.currentTimeMillis() - 20 * 60 * 1000, true);
createDummyTrack(trackId, System.currentTimeMillis() - 20 * 60 * 1000, true);
// Clear the number of attempts and set the timeout to 10 min.
updateAutoResumePrefs(PreferencesUtils.AUTO_RESUME_TRACK_CURRENT_RETRY_DEFAULT,
PreferencesUtils.AUTO_RESUME_TRACK_TIMEOUT_DEFAULT);
updateAutoResumePrefs(context, PreferencesUtils.AUTO_RESUME_TRACK_CURRENT_RETRY_DEFAULT, PreferencesUtils.AUTO_RESUME_TRACK_TIMEOUT_DEFAULT);
// Start the service in "resume" mode (simulates the on-reboot action).
Intent startIntent = createStartIntent();
Intent startIntent = createStartIntent(context);
startIntent.putExtra(TrackRecordingService.RESUME_TRACK_EXTRA_NAME, true);
startService(startIntent);
assertNotNull(getService());
ITrackRecordingService service = ((ITrackRecordingService) mServiceRule.bindService(startIntent));
Assert.assertNotNull(service);
// We don't expect to resume the previous track, because it has expired.
assertFalse(getService().isRecording());
ITrackRecordingService service = bindAndGetService(createStartIntent());
assertEquals(-1L, service.getRecordingTrackId());
Assert.assertFalse(service.isRecording());
Assert.assertEquals(PreferencesUtils.RECORDING_TRACK_ID_DEFAULT, service.getRecordingTrackId());
}
@MediumTest
@Test
public void testResumeAfterReboot_tooManyAttempts() throws Exception {
// Insert a dummy track.
createDummyTrack(123L, System.currentTimeMillis(), true);
createDummyTrack(trackId, System.currentTimeMillis(), true);
// Set the number of attempts to max.
updateAutoResumePrefs(TrackRecordingService.MAX_AUTO_RESUME_TRACK_RETRY_ATTEMPTS,
PreferencesUtils.AUTO_RESUME_TRACK_TIMEOUT_DEFAULT);
updateAutoResumePrefs(context, TrackRecordingService.MAX_AUTO_RESUME_TRACK_RETRY_ATTEMPTS, PreferencesUtils.AUTO_RESUME_TRACK_TIMEOUT_DEFAULT);
// Start the service in "resume" mode (simulates the on-reboot action).
Intent startIntent = createStartIntent();
Intent startIntent = createStartIntent(context);
startIntent.putExtra(TrackRecordingService.RESUME_TRACK_EXTRA_NAME, true);
startService(startIntent);
assertNotNull(getService());
// We don't expect to resume the previous track, because there were already
// too many attempts.
assertFalse(getService().isRecording());
ITrackRecordingService service = bindAndGetService(createStartIntent());
assertEquals(-1L, service.getRecordingTrackId());
//Explicit start service, so `startCommand()` is executed - which would be called by BootReceiver.
context.startService(startIntent);
ITrackRecordingService service = ((ITrackRecordingService) mServiceRule.bindService(startIntent));
Assert.assertNotNull(service);
// We don't expect to resume the previous track, because there were already too many attempts.
Assert.assertFalse(service.isRecording());
Assert.assertEquals(PreferencesUtils.RECORDING_TRACK_ID_DEFAULT, service.getRecordingTrackId());
}
@MediumTest
@Test
public void testRecording_noTracks() throws Exception {
List<Track> tracks = providerUtils.getAllTracks();
assertTrue(tracks.isEmpty());
Assert.assertTrue(tracks.isEmpty());
ITrackRecordingService service = bindAndGetService(createStartIntent());
ITrackRecordingService service = ((ITrackRecordingService) mServiceRule.bindService(createStartIntent(context)));
// Test if we start in no-recording mode by default.
assertFalse(service.isRecording());
assertEquals(-1L, service.getRecordingTrackId());
Assert.assertFalse(service.isRecording());
Assert.assertEquals(-1L, service.getRecordingTrackId());
}
@MediumTest
@Test
public void testRecording_oldTracks() throws Exception {
createDummyTrack(123L, -1L, false);
createDummyTrack(trackId, -1L, false);
ITrackRecordingService service = bindAndGetService(createStartIntent());
assertFalse(service.isRecording());
assertEquals(-1L, service.getRecordingTrackId());
ITrackRecordingService service = ((ITrackRecordingService) mServiceRule.bindService(createStartIntent(context)));
Assert.assertFalse(service.isRecording());
Assert.assertEquals(PreferencesUtils.RECORDING_TRACK_ID_DEFAULT, service.getRecordingTrackId());
}
@MediumTest
@Test
public void testRecording_orphanedRecordingTrack() throws Exception {
// Just set recording track to a bogus value.
PreferencesUtils.setLong(context, R.string.recording_track_id_key, 256L);
PreferencesUtils.setLong(context, R.string.recording_track_id_key, 123L);
// Make sure that the service will not start recording and will clear
// the bogus track.
ITrackRecordingService service = bindAndGetService(createStartIntent());
assertFalse(service.isRecording());
assertEquals(-1L, service.getRecordingTrackId());
}
@MediumTest
public void testStartNewTrack_noRecording() throws Exception {
// NOTICE: due to the way Android permissions work, if this fails,
// uninstall the test apk then retry - the test must be installed *after* the app (go figure).
// Reference: http://code.google.com/p/android/issues/detail?id=5521
BlockingBroadcastReceiver startReceiver = new BlockingBroadcastReceiver();
String startAction = context.getString(R.string.track_started_broadcast_action);
context.registerReceiver(startReceiver, new IntentFilter(startAction));
List<Track> tracks = providerUtils.getAllTracks();
assertTrue(tracks.isEmpty());
ITrackRecordingService service = bindAndGetService(createStartIntent());
assertFalse(service.isRecording());
long id = service.startNewTrack();
assertTrue(id >= 0);
assertTrue(service.isRecording());
Track track = providerUtils.getTrack(id);
assertNotNull(track);
assertEquals(id, track.getId());
assertEquals(PreferencesUtils.getString(
context, R.string.default_activity_key, PreferencesUtils.DEFAULT_ACTIVITY_DEFAULT),
track.getCategory());
assertEquals(id, PreferencesUtils.getLong(context, R.string.recording_track_id_key));
assertEquals(id, service.getRecordingTrackId());
// Verify that the start broadcast was received.
assertTrue(startReceiver.waitUntilReceived(1));
List<Intent> receivedIntents = startReceiver.getReceivedIntents();
assertEquals(1, receivedIntents.size());
Intent broadcastIntent = receivedIntents.get(0);
assertEquals(startAction, broadcastIntent.getAction());
assertEquals(id,
broadcastIntent.getLongExtra(context.getString(R.string.track_id_broadcast_extra), -1L));
context.unregisterReceiver(startReceiver);
// Make sure that the service will not start recording and will clear the bogus track.
ITrackRecordingService service = ((ITrackRecordingService) mServiceRule.bindService(createStartIntent(context)));
Assert.assertFalse(service.isRecording());
Assert.assertEquals(PreferencesUtils.RECORDING_TRACK_ID_DEFAULT, service.getRecordingTrackId());
}
@MediumTest
@Test
public void testStartNewTrack_alreadyRecording() throws Exception {
createDummyTrack(123L, -1L, true);
createDummyTrack(trackId, -1L, true);
ITrackRecordingService service = bindAndGetService(createStartIntent());
assertTrue(service.isRecording());
ITrackRecordingService service = ((ITrackRecordingService) mServiceRule.bindService(createStartIntent(context)));
Assert.assertTrue(service.isRecording());
// Starting a new track when there is a recording should just return -1L.
long newTrack = service.startNewTrack();
assertEquals(-1L, newTrack);
long newTrackId = service.startNewTrack();
Assert.assertEquals(PreferencesUtils.RECORDING_TRACK_ID_DEFAULT, newTrackId);
assertEquals(123L, PreferencesUtils.getLong(context, R.string.recording_track_id_key));
assertEquals(123L, service.getRecordingTrackId());
}
@MediumTest
public void testEndCurrentTrack_alreadyRecording() throws Exception {
// See comment above if this fails randomly.
BlockingBroadcastReceiver stopReceiver = new BlockingBroadcastReceiver();
String stopAction = context.getString(R.string.track_stopped_broadcast_action);
context.registerReceiver(stopReceiver, new IntentFilter(stopAction));
createDummyTrack(123L, -1L, true);
ITrackRecordingService service = bindAndGetService(createStartIntent());
assertTrue(service.isRecording());
// End the current track.
service.endCurrentTrack();
assertFalse(service.isRecording());
assertEquals(PreferencesUtils.RECORDING_TRACK_ID_DEFAULT,
PreferencesUtils.getLong(context, R.string.recording_track_id_key));
assertEquals(PreferencesUtils.RECORDING_TRACK_ID_DEFAULT, service.getRecordingTrackId());
// Verify that the stop broadcast was received.
assertTrue(stopReceiver.waitUntilReceived(1));
List<Intent> receivedIntents = stopReceiver.getReceivedIntents();
assertEquals(1, receivedIntents.size());
Intent broadcastIntent = receivedIntents.get(0);
assertEquals(stopAction, broadcastIntent.getAction());
assertEquals(123L,
broadcastIntent.getLongExtra(context.getString(R.string.track_id_broadcast_extra), -1L));
context.unregisterReceiver(stopReceiver);
Assert.assertEquals(trackId, PreferencesUtils.getLong(context, R.string.recording_track_id_key));
Assert.assertEquals(trackId, service.getRecordingTrackId());
}
@MediumTest
@Test
public void testEndCurrentTrack_noRecording() throws Exception {
ITrackRecordingService service = bindAndGetService(createStartIntent());
assertFalse(service.isRecording());
ITrackRecordingService service = ((ITrackRecordingService) mServiceRule.bindService(createStartIntent(context)));
Assert.assertFalse(service.isRecording());
/*
* Ending the current track when there is no recording should not result in
* any error.
*/
// Ending the current track when there is no recording should not result in any error.
service.endCurrentTrack();
assertEquals(PreferencesUtils.RECORDING_TRACK_ID_DEFAULT,
PreferencesUtils.getLong(context, R.string.recording_track_id_key));
assertEquals(PreferencesUtils.RECORDING_TRACK_ID_DEFAULT, service.getRecordingTrackId());
}
@MediumTest
public void testIntegration_completeRecordingSession() throws Exception {
List<Track> tracks = providerUtils.getAllTracks();
assertTrue(tracks.isEmpty());
fullRecordingSession();
Assert.assertEquals(PreferencesUtils.RECORDING_TRACK_ID_DEFAULT, PreferencesUtils.getLong(context, R.string.recording_track_id_key));
Assert.assertEquals(PreferencesUtils.RECORDING_TRACK_ID_DEFAULT, service.getRecordingTrackId());
}
@MediumTest
@Test
public void testInsertStatisticsMarker_noRecordingTrack() throws Exception {
ITrackRecordingService service = bindAndGetService(createStartIntent());
assertFalse(service.isRecording());
ITrackRecordingService service = ((ITrackRecordingService) mServiceRule.bindService(createStartIntent(context)));
Assert.assertFalse(service.isRecording());
long waypointId = service.insertWaypoint(WaypointCreationRequest.DEFAULT_STATISTICS);
assertEquals(-1L, waypointId);
Assert.assertEquals(-1L, waypointId);
}
@MediumTest
@Test
public void testInsertStatisticsMarker_validLocation() throws Exception {
createDummyTrack(123L, -1L, true);
createDummyTrack(trackId, -1L, true);
ITrackRecordingService service = bindAndGetService(createStartIntent());
assertTrue(service.isRecording());
assertFalse(service.isPaused());
ITrackRecordingService service = ((ITrackRecordingService) mServiceRule.bindService(createStartIntent(context)));
Assert.assertTrue(service.isRecording());
Assert.assertFalse(service.isPaused());
insertLocation(service);
assertEquals(1, service.insertWaypoint(WaypointCreationRequest.DEFAULT_STATISTICS));
assertEquals(2, service.insertWaypoint(WaypointCreationRequest.DEFAULT_STATISTICS));
long waypointId1 = service.insertWaypoint(WaypointCreationRequest.DEFAULT_STATISTICS);
Assert.assertNotEquals(-1L, waypointId1);
long waypointId2 = service.insertWaypoint(WaypointCreationRequest.DEFAULT_STATISTICS);
Assert.assertNotEquals(-1L, waypointId2);
Waypoint wpt = providerUtils.getWaypoint(2);
assertEquals(getContext().getString(R.string.marker_statistics_icon_url), wpt.getIcon());
assertEquals(getContext().getString(R.string.marker_split_name_format, 1), wpt.getName());
assertEquals(WaypointType.STATISTICS, wpt.getType());
assertEquals(123L, wpt.getTrackId());
assertEquals(0.0, wpt.getLength());
assertNotNull(wpt.getLocation());
assertNotNull(wpt.getTripStatistics());
Waypoint wpt = providerUtils.getWaypoint(waypointId1);
Assert.assertEquals(context.getString(R.string.marker_statistics_icon_url), wpt.getIcon());
Assert.assertEquals(context.getString(R.string.marker_split_name_format, 0), wpt.getName());
Assert.assertEquals(WaypointType.STATISTICS, wpt.getType());
Assert.assertEquals(trackId, wpt.getTrackId());
Assert.assertEquals(0.0, wpt.getLength(), 0.01);
Assert.assertNotNull(wpt.getLocation());
Assert.assertNotNull(wpt.getTripStatistics());
// TODO check the rest of the params.
// TODO: Check waypoint 2.
}
@MediumTest
public void testInsertWaypointMarker_noRecordingTrack() throws Exception {
ITrackRecordingService service = bindAndGetService(createStartIntent());
assertFalse(service.isRecording());
long waypointId = service.insertWaypoint(WaypointCreationRequest.DEFAULT_WAYPOINT);
assertEquals(-1L, waypointId);
}
@MediumTest
public void testInsertWaypointMarker_validWaypoint() throws Exception {
createDummyTrack(123L, -1L, true);
ITrackRecordingService service = bindAndGetService(createStartIntent());
assertTrue(service.isRecording());
insertLocation(service);
assertEquals(1, service.insertWaypoint(WaypointCreationRequest.DEFAULT_WAYPOINT));
Waypoint wpt = providerUtils.getWaypoint(1);
assertEquals(getContext().getString(R.string.marker_waypoint_icon_url), wpt.getIcon());
assertEquals(getContext().getString(R.string.marker_name_format, 1), wpt.getName());
assertEquals(WaypointType.WAYPOINT, wpt.getType());
assertEquals(123L, wpt.getTrackId());
assertEquals(0.0, wpt.getLength());
assertNotNull(wpt.getLocation());
assertNull(wpt.getTripStatistics());
}
@MediumTest
public void testWithProperties_voiceFrequencyDefault() throws Exception {
PreferencesUtils.setInt(
context, R.string.voice_frequency_key, PreferencesUtils.VOICE_FREQUENCY_DEFAULT);
fullRecordingSession();
}
@MediumTest
public void testWithProperties_voiceFrequencyByDistance() throws Exception {
PreferencesUtils.setInt(context, R.string.voice_frequency_key, -1);
fullRecordingSession();
}
@MediumTest
public void testWithProperties_voiceFrequencyByTime() throws Exception {
PreferencesUtils.setInt(context, R.string.voice_frequency_key, 1);
fullRecordingSession();
}
@MediumTest
public void testWithProperties_maxRecordingDistanceDefault() throws Exception {
PreferencesUtils.setInt(context, R.string.max_recording_distance_key,
PreferencesUtils.MAX_RECORDING_DISTANCE_DEFAULT);
fullRecordingSession();
}
@MediumTest
public void testWithProperties_maxRecordingDistance() throws Exception {
PreferencesUtils.setInt(context, R.string.max_recording_distance_key, 50);
fullRecordingSession();
}
@MediumTest
public void testWithProperties_minRecordingDistanceDefault() throws Exception {
PreferencesUtils.setInt(context, R.string.recording_distance_interval_key,
PreferencesUtils.RECORDING_DISTANCE_INTERVAL_DEFAULT);
fullRecordingSession();
}
@MediumTest
public void testWithProperties_minRecordingDistance() throws Exception {
PreferencesUtils.setInt(context, R.string.recording_distance_interval_key, 2);
fullRecordingSession();
}
@MediumTest
public void testWithProperties_splitFrequencyDefault() throws Exception {
PreferencesUtils.setInt(
context, R.string.split_frequency_key, PreferencesUtils.SPLIT_FREQUENCY_DEFAULT);
fullRecordingSession();
}
@MediumTest
public void testWithProperties_splitFrequencyByDistance() throws Exception {
PreferencesUtils.setInt(context, R.string.split_frequency_key, -1);
fullRecordingSession();
}
@MediumTest
public void testWithProperties_splitFrequencyByTime() throws Exception {
PreferencesUtils.setInt(context, R.string.split_frequency_key, 1);
fullRecordingSession();
}
@MediumTest
public void testWithProperties_metricUnitsDefault() throws Exception {
PreferencesUtils.setString(
context, R.string.stats_units_key, PreferencesUtils.STATS_UNITS_DEFAULT);
fullRecordingSession();
}
@MediumTest
public void testWithProperties_metricUnitsDisabled() throws Exception {
PreferencesUtils.setString(
context, R.string.stats_units_key, context.getString(R.string.stats_units_imperial));
fullRecordingSession();
}
@MediumTest
public void testWithProperties_minRecordingIntervalDefault() throws Exception {
PreferencesUtils.setInt(context, R.string.min_recording_interval_key,
PreferencesUtils.MIN_RECORDING_INTERVAL_DEFAULT);
fullRecordingSession();
}
@MediumTest
public void testWithProperties_minRecordingInterval() throws Exception {
PreferencesUtils.setInt(context, R.string.min_recording_interval_key, 2);
fullRecordingSession();
}
@MediumTest
public void testWithProperties_minRequiredAccuracyDefault() throws Exception {
PreferencesUtils.setInt(context, R.string.recording_gps_accuracy_key,
PreferencesUtils.RECORDING_GPS_ACCURACY_DEFAULT);
fullRecordingSession();
}
@MediumTest
public void testWithProperties_minRequiredAccuracy() throws Exception {
PreferencesUtils.setInt(context, R.string.recording_gps_accuracy_key, 500);
fullRecordingSession();
}
@MediumTest
public void testWithProperties_sensorTypeDefault() throws Exception {
PreferencesUtils.setString(
context, R.string.sensor_type_key, PreferencesUtils.SENSOR_TYPE_DEFAULT);
fullRecordingSession();
}
@MediumTest
public void testWithProperties_sensorTypeZephyr() throws Exception {
PreferencesUtils.setString(
context, R.string.sensor_type_key, context.getString(R.string.sensor_type_value_zephyr));
fullRecordingSession();
}
private ITrackRecordingService bindAndGetService(Intent intent) {
ITrackRecordingService service = ITrackRecordingService.Stub.asInterface(bindService(intent));
assertNotNull(service);
return service;
}
private Track createDummyTrack(long id, long stopTime, boolean isRecording) {
private void createDummyTrack(long id, long stopTime, boolean isRecording) {
Track dummyTrack = new Track();
dummyTrack.setId(id);
dummyTrack.setName("Dummy Track");
@@ -612,82 +349,20 @@ public class TrackRecordingServiceTest extends ServiceTestCase<TestRecordingServ
tripStatistics.setStopTime(stopTime);
dummyTrack.setTripStatistics(tripStatistics);
addTrack(dummyTrack, isRecording);
return dummyTrack;
}
private void updateAutoResumePrefs(int attempts, int timeoutMins) {
PreferencesUtils.setInt(context, R.string.auto_resume_track_current_retry_key, attempts);
PreferencesUtils.setInt(context, R.string.auto_resume_track_timeout_key, timeoutMins);
}
private Intent createStartIntent() {
Intent startIntent = new Intent();
startIntent.setClass(context, TrackRecordingService.class);
return startIntent;
}
private void addTrack(Track track, boolean isRecording) {
assertTrue(track.getId() >= 0);
Assert.assertTrue(track.getId() >= 0);
providerUtils.insertTrack(track);
assertEquals(track.getId(), providerUtils.getTrack(track.getId()).getId());
PreferencesUtils.setLong(context, R.string.recording_track_id_key,
isRecording ? track.getId() : PreferencesUtils.RECORDING_TRACK_ID_DEFAULT);
Assert.assertEquals(track.getId(), providerUtils.getTrack(track.getId()).getId());
PreferencesUtils.setLong(context, R.string.recording_track_id_key, isRecording ? track.getId() : PreferencesUtils.RECORDING_TRACK_ID_DEFAULT);
PreferencesUtils.setBoolean(context, R.string.recording_track_paused_key, !isRecording);
}
private void fullRecordingSession() throws Exception {
ITrackRecordingService service = bindAndGetService(createStartIntent());
assertFalse(service.isRecording());
// Start a track.
long id = service.startNewTrack();
assertTrue(id >= 0);
assertTrue(service.isRecording());
Track track = providerUtils.getTrack(id);
assertNotNull(track);
assertEquals(id, track.getId());
assertEquals(id, PreferencesUtils.getLong(context, R.string.recording_track_id_key));
assertEquals(id, service.getRecordingTrackId());
// Insert a few points, markers and statistics.
long startTime = System.currentTimeMillis();
for (int i = 0; i < 30; i++) {
Location loc = new Location("gps");
loc.setLongitude(35.0f + i / 10.0f);
loc.setLatitude(45.0f - i / 5.0f);
loc.setAccuracy(5);
loc.setSpeed(10);
loc.setTime(startTime + i * 10000);
loc.setBearing(3.0f);
service.insertTrackPoint(loc);
if (i % 10 == 0) {
service.insertWaypoint(WaypointCreationRequest.DEFAULT_STATISTICS);
} else if (i % 7 == 0) {
service.insertWaypoint(WaypointCreationRequest.DEFAULT_WAYPOINT);
}
}
// Stop the track. Validate if it has correct data.
service.endCurrentTrack();
assertFalse(service.isRecording());
assertEquals(-1L, service.getRecordingTrackId());
track = providerUtils.getTrack(id);
assertNotNull(track);
assertEquals(id, track.getId());
TripStatistics tripStatistics = track.getTripStatistics();
assertNotNull(tripStatistics);
assertTrue(tripStatistics.getStartTime() > 0);
assertTrue(tripStatistics.getStopTime() >= tripStatistics.getStartTime());
}
/**
* Inserts a location and waits for 100ms.
*
* @param trackRecordingService the track recording service
* Inserts a location and waits for 200ms.
*/
private void insertLocation(ITrackRecordingService trackRecordingService)
throws RemoteException, InterruptedException {
private void insertLocation(ITrackRecordingService trackRecordingService) throws InterruptedException {
Location location = new Location("gps");
location.setLongitude(35.0f);
location.setLatitude(45.0f);
@@ -696,33 +371,47 @@ public class TrackRecordingServiceTest extends ServiceTestCase<TestRecordingServ
location.setTime(System.currentTimeMillis());
location.setBearing(3.0f);
trackRecordingService.insertTrackPoint(location);
Thread.sleep(200);
}
/**
* A context wrapper with the user provided {@link ContentResolver}. TODO:
* Move to test utils package.
*/
public static class MockContext extends ContextWrapper {
private final ContentResolver contentResolver;
@MediumTest
@Test
public void testInsertWaypointMarker_noRecordingTrack() throws Exception {
ITrackRecordingService service = ((ITrackRecordingService) mServiceRule.bindService(createStartIntent(context)));
Assert.assertFalse(service.isRecording());
public MockContext(ContentResolver contentResolver, Context base) {
super(base);
this.contentResolver = contentResolver;
}
long waypointId = service.insertWaypoint(WaypointCreationRequest.DEFAULT_WAYPOINT);
Assert.assertEquals(-1L, waypointId);
}
@Override
public ContentResolver getContentResolver() {
return contentResolver;
}
@MediumTest
@Test
public void testInsertWaypointMarker_validWaypoint() throws Exception {
createDummyTrack(trackId, -1L, true);
ITrackRecordingService service = ((ITrackRecordingService) mServiceRule.bindService(createStartIntent(context)));
Assert.assertTrue(service.isRecording());
insertLocation(service);
long waypointId = service.insertWaypoint(WaypointCreationRequest.DEFAULT_WAYPOINT);
Assert.assertNotEquals(-1L, waypointId);
Waypoint wpt = providerUtils.getWaypoint(waypointId);
Assert.assertEquals(context.getString(R.string.marker_waypoint_icon_url), wpt.getIcon());
Assert.assertEquals(context.getString(R.string.marker_name_format, 1), wpt.getName());
Assert.assertEquals(WaypointType.WAYPOINT, wpt.getType());
Assert.assertEquals(trackId, wpt.getTrackId());
Assert.assertEquals(0.0, wpt.getLength(), 0.01);
Assert.assertNotNull(wpt.getLocation());
Assert.assertNull(wpt.getTripStatistics());
}
/**
* Synchronous/waitable broadcast receiver to be used in testing.
*/
private class BlockingBroadcastReceiver extends BroadcastReceiver {
private static final long MAX_WAIT_TIME_MS = 10000;
private List<Intent> receivedIntents = new ArrayList<Intent>();
private static final long MAX_WAIT_TIME_MS = 3000;
private final List<Intent> receivedIntents = new ArrayList<>();
public List<Intent> getReceivedIntents() {
return receivedIntents;
@@ -0,0 +1,262 @@
package de.dennisguse.opentracks.services;
import android.content.ContentProvider;
import android.content.Context;
import android.content.SharedPreferences;
import android.location.Location;
import android.os.Looper;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.MediumTest;
import androidx.test.rule.ServiceTestRule;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Assert;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.ContentProviderUtils;
import de.dennisguse.opentracks.content.CustomContentProvider;
import de.dennisguse.opentracks.content.Track;
import de.dennisguse.opentracks.content.WaypointCreationRequest;
import de.dennisguse.opentracks.stats.TripStatistics;
import de.dennisguse.opentracks.util.PreferencesUtils;
/**
* Tests for the track recording service, which require a {@link Looper}.
*
* @author Bartlomiej Niechwiej
* <p>
* ATTENTION: This tests deletes all stored tracks in the database.
* So, if it is executed on a real device, data might be lost.
*/
@RunWith(AndroidJUnit4.class)
public class TrackRecordingServiceTestLooper {
@Rule
public final ServiceTestRule mServiceRule = ServiceTestRule.withTimeout(5, TimeUnit.SECONDS);
private Context context = ApplicationProvider.getApplicationContext();
private ContentProviderUtils providerUtils;
@BeforeClass
public static void preSetUp() {
// Prepare looper for Android's message queue
if (Looper.myLooper() == null) Looper.prepare();
}
@AfterClass
public static void finalTearDown() {
if (Looper.myLooper() != null) Looper.myLooper().quit();
}
@Before
public void setUp() {
// Set up the mock content resolver
ContentProvider customContentProvider = new CustomContentProvider() {
};
customContentProvider.attachInfo(context, null);
providerUtils = ContentProviderUtils.Factory.get(context);
// Let's use default values.
SharedPreferences sharedPreferences = PreferencesUtils.getSharedPreferences(context);
sharedPreferences.edit().clear().apply();
// Disable auto resume by default.
TrackRecordingServiceTest.updateAutoResumePrefs(context, PreferencesUtils.AUTO_RESUME_TRACK_CURRENT_RETRY_DEFAULT, 0);
// Ensure that the database is empty before every test
providerUtils.deleteAllTracks(context);
}
@After
public void tearDown() throws TimeoutException {
// Reset service (if some previous test failed)
ITrackRecordingService service = ((ITrackRecordingService) mServiceRule.bindService(TrackRecordingServiceTest.createStartIntent(context)));
if (service.isRecording() || service.isPaused()) {
service.endCurrentTrack();
}
// Ensure that the database is empty after every test
providerUtils.deleteAllTracks(context);
}
@MediumTest
@Test
public void testWithProperties_minRequiredAccuracy() throws TimeoutException {
PreferencesUtils.setInt(context, R.string.recording_gps_accuracy_key, 500);
fullRecordingSession();
}
@MediumTest
@Test
public void testWithProperties_voiceFrequencyDefault() throws TimeoutException {
PreferencesUtils.setInt(context, R.string.voice_frequency_key, PreferencesUtils.VOICE_FREQUENCY_DEFAULT);
fullRecordingSession();
}
@MediumTest
@Test
public void testWithProperties_voiceFrequencyByDistance() throws TimeoutException {
PreferencesUtils.setInt(context, R.string.voice_frequency_key, -1);
fullRecordingSession();
}
@MediumTest
@Test
public void testWithProperties_voiceFrequencyByTime() throws TimeoutException {
PreferencesUtils.setInt(context, R.string.voice_frequency_key, 1);
fullRecordingSession();
}
@MediumTest
@Test
public void testWithProperties_maxRecordingDistanceDefault() throws TimeoutException {
PreferencesUtils.setInt(context, R.string.max_recording_distance_key, PreferencesUtils.MAX_RECORDING_DISTANCE_DEFAULT);
fullRecordingSession();
}
@MediumTest
@Test
public void testWithProperties_maxRecordingDistance() throws TimeoutException {
PreferencesUtils.setInt(context, R.string.max_recording_distance_key, 50);
fullRecordingSession();
}
@MediumTest
@Test
public void testWithProperties_minRecordingDistanceDefault() throws TimeoutException {
PreferencesUtils.setInt(context, R.string.recording_distance_interval_key, PreferencesUtils.RECORDING_DISTANCE_INTERVAL_DEFAULT);
fullRecordingSession();
}
@MediumTest
@Test
public void testWithProperties_minRecordingDistance() throws TimeoutException {
PreferencesUtils.setInt(context, R.string.recording_distance_interval_key, 2);
fullRecordingSession();
}
@MediumTest
@Test
public void testWithProperties_splitFrequencyDefault() throws TimeoutException {
PreferencesUtils.setInt(context, R.string.split_frequency_key, PreferencesUtils.SPLIT_FREQUENCY_DEFAULT);
fullRecordingSession();
}
@MediumTest
@Test
public void testWithProperties_splitFrequencyByDistance() throws TimeoutException {
PreferencesUtils.setInt(context, R.string.split_frequency_key, -1);
fullRecordingSession();
}
@MediumTest
@Test
public void testWithProperties_splitFrequencyByTime() throws TimeoutException {
PreferencesUtils.setInt(context, R.string.split_frequency_key, 1);
fullRecordingSession();
}
@MediumTest
@Test
public void testWithProperties_metricUnitsDefault() throws TimeoutException {
PreferencesUtils.setString(context, R.string.stats_units_key, PreferencesUtils.STATS_UNITS_DEFAULT);
fullRecordingSession();
}
@MediumTest
@Test
public void testWithProperties_metricUnitsDisabled() throws TimeoutException {
PreferencesUtils.setString(context, R.string.stats_units_key, context.getString(R.string.stats_units_imperial));
fullRecordingSession();
}
@MediumTest
@Test
public void testWithProperties_minRecordingIntervalDefault() throws TimeoutException {
PreferencesUtils.setInt(context, R.string.min_recording_interval_key, PreferencesUtils.MIN_RECORDING_INTERVAL_DEFAULT);
fullRecordingSession();
}
@MediumTest
@Test
public void testWithProperties_minRecordingInterval() throws TimeoutException {
PreferencesUtils.setInt(context, R.string.min_recording_interval_key, 2);
fullRecordingSession();
}
@MediumTest
@Test
public void testWithProperties_minRequiredAccuracyDefault() throws TimeoutException {
PreferencesUtils.setInt(context, R.string.recording_gps_accuracy_key, PreferencesUtils.RECORDING_GPS_ACCURACY_DEFAULT);
fullRecordingSession();
}
@MediumTest
@Test
public void testIntegration_completeRecordingSession() throws TimeoutException {
List<Track> tracks = providerUtils.getAllTracks();
Assert.assertTrue(tracks.isEmpty());
fullRecordingSession();
}
private void fullRecordingSession() throws TimeoutException {
ITrackRecordingService service = ((ITrackRecordingService) mServiceRule.bindService(TrackRecordingServiceTest.createStartIntent(context)));
Assert.assertFalse(service.isRecording());
// Start a track.
long id = service.startNewTrack();
Assert.assertTrue(id >= 0);
Assert.assertTrue(service.isRecording());
Track track = providerUtils.getTrack(id);
Assert.assertNotNull(track);
Assert.assertEquals(id, track.getId());
Assert.assertEquals(id, PreferencesUtils.getLong(context, R.string.recording_track_id_key));
Assert.assertEquals(id, service.getRecordingTrackId());
// Insert a few points, markers and statistics.
long startTime = System.currentTimeMillis();
for (int i = 0; i < 30; i++) {
Location location = new Location("gps");
location.setLongitude(35.0f + i / 10.0f);
location.setLatitude(45.0f - i / 5.0f);
location.setAccuracy(5);
location.setSpeed(10);
location.setTime(startTime + i * 10000);
location.setBearing(3.0f);
service.insertTrackPoint(location);
if (i % 10 == 0) {
service.insertWaypoint(WaypointCreationRequest.DEFAULT_STATISTICS);
} else if (i % 7 == 0) {
service.insertWaypoint(WaypointCreationRequest.DEFAULT_WAYPOINT);
}
}
// Stop the track. Validate if it has correct data.
service.endCurrentTrack();
Assert.assertFalse(service.isRecording());
Assert.assertEquals(-1L, service.getRecordingTrackId());
track = providerUtils.getTrack(id);
Assert.assertNotNull(track);
Assert.assertEquals(id, track.getId());
TripStatistics tripStatistics = track.getTripStatistics();
Assert.assertNotNull(tripStatistics);
Assert.assertTrue(tripStatistics.getStartTime() > 0);
Assert.assertTrue(tripStatistics.getStopTime() >= tripStatistics.getStartTime());
}
}
@@ -16,23 +16,43 @@
package de.dennisguse.opentracks.services.tasks;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.platform.app.InstrumentationRegistry;
import android.content.Context;
import android.os.Looper;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.AfterClass;
import org.junit.Assert;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
/**
* Tests for {@link AnnouncementPeriodicTaskFactory}.
* These tests require Donut+ to run.
*
* @author Rodrigo Damazio
*/
@RunWith(AndroidJUnit4.class)
public class AnnouncementPeriodicTaskFactoryTest {
private Context context = ApplicationProvider.getApplicationContext();
@BeforeClass
public static void preSetUp() {
// Prepare looper for Android's message queue
if (Looper.myLooper() == null) Looper.prepare();
}
@AfterClass
public static void finalTearDown() {
if (Looper.myLooper() != null) Looper.myLooper().quit();
}
@Test
public void testCreate() {
PeriodicTaskFactory factory = new AnnouncementPeriodicTaskFactory();
PeriodicTask task = factory.create(InstrumentationRegistry.getInstrumentation().getContext());
PeriodicTask task = factory.create(context);
Assert.assertTrue(task instanceof AnnouncementPeriodicTask);
}
}
@@ -16,59 +16,81 @@
package de.dennisguse.opentracks.services.tasks;
import android.content.Context;
import android.os.Looper;
import android.speech.tts.TextToSpeech;
import android.speech.tts.TextToSpeech.OnInitListener;
import android.speech.tts.TextToSpeech.OnUtteranceCompletedListener;
import android.telephony.PhoneStateListener;
import android.telephony.TelephonyManager;
import android.test.AndroidTestCase;
import de.dennisguse.opentracks.stats.TripStatistics;
import de.dennisguse.opentracks.util.StringUtils;
import com.google.android.testing.mocking.AndroidMock;
import com.google.android.testing.mocking.UsesMocks;
import androidx.test.core.app.ApplicationProvider;
import org.easymock.Capture;
import org.easymock.EasyMock;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Assert;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import java.util.HashMap;
import java.util.Locale;
import java.util.concurrent.atomic.AtomicBoolean;
import static com.google.android.testing.mocking.AndroidMock.capture;
import static com.google.android.testing.mocking.AndroidMock.eq;
import static com.google.android.testing.mocking.AndroidMock.expect;
import static com.google.android.testing.mocking.AndroidMock.same;
import de.dennisguse.opentracks.stats.TripStatistics;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.same;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
* Tests for {@link AnnouncementPeriodicTask}.
* WARNING: I'm not responsible if your eyes start bleeding while reading this
* code. You have been warned. It's still better than no test, though.
*
* WARNING: I'm not responsible if your eyes start bleeding while reading this code.
* You have been warned.
* It's still better than no test, though.
*
* @author Rodrigo Damazio
*/
public class AnnouncementPeriodicTaskTest extends AndroidTestCase {
@RunWith(MockitoJUnitRunner.class)
public class AnnouncementPeriodicTaskTest {
// Use something other than our hardcoded value
private static final Locale DEFAULT_LOCALE = Locale.KOREAN;
private static final String ANNOUNCEMENT = "I can haz cheeseburger?";
private Locale oldDefaultLocale;
private Context context = ApplicationProvider.getApplicationContext();
private AnnouncementPeriodicTask task;
@Mock
private AnnouncementPeriodicTask mockTask;
private Capture<OnInitListener> initListenerCapture;
private Capture<PhoneStateListener> phoneListenerCapture;
private ArgumentCaptor<OnInitListener> initListenerCapture = ArgumentCaptor.forClass(OnInitListener.class);
private ArgumentCaptor<PhoneStateListener> phoneListenerCapture = ArgumentCaptor.forClass(PhoneStateListener.class);
private TextToSpeechDelegate ttsDelegate;
@Mock
private TextToSpeechInterface tts;
@UsesMocks({
AnnouncementPeriodicTask.class,
StringUtils.class,
})
@Override
protected void setUp() throws Exception {
super.setUp();
@BeforeClass
public static void preSetUp() {
// Prepare looper for Android's message queue
if (Looper.myLooper() == null) Looper.prepare();
}
@AfterClass
public static void finalTearDown() {
if (Looper.myLooper() != null) Looper.myLooper().quit();
}
@Before
public void setUp() throws Exception {
oldDefaultLocale = Locale.getDefault();
Locale.setDefault(DEFAULT_LOCALE);
@@ -84,7 +106,7 @@ public class AnnouncementPeriodicTaskTest extends AndroidTestCase {
}
};
ttsDelegate = new TextToSpeechDelegate(getContext(), blockingListener);
ttsDelegate = new TextToSpeechDelegate(context, blockingListener);
// Wait for all async operations done in the constructor to finish.
synchronized (blockingListener) {
@@ -94,18 +116,10 @@ public class AnnouncementPeriodicTaskTest extends AndroidTestCase {
}
}
// Phew, done, now we can start forwarding calls
tts = AndroidMock.createMock(TextToSpeechInterface.class);
initListenerCapture = new Capture<OnInitListener>();
phoneListenerCapture = new Capture<PhoneStateListener>();
// Create a partial forwarding mock
mockTask = AndroidMock.createMock(AnnouncementPeriodicTask.class, getContext());
task = new AnnouncementPeriodicTask(getContext()) {
task = new AnnouncementPeriodicTask(context) {
@Override
protected TextToSpeech newTextToSpeech(Context ctx,
OnInitListener onInitListener) {
protected TextToSpeech newTextToSpeech(Context ctx, OnInitListener onInitListener) {
return mockTask.newTextToSpeech(ctx, onInitListener);
}
@@ -115,116 +129,109 @@ public class AnnouncementPeriodicTaskTest extends AndroidTestCase {
}
@Override
protected void listenToPhoneState(
PhoneStateListener listener, int events) {
protected void listenToPhoneState(PhoneStateListener listener, int events) {
mockTask.listenToPhoneState(listener, events);
}
};
}
@Override
protected void tearDown() {
@After
public void tearDown() {
Locale.setDefault(oldDefaultLocale);
}
@Test
public void testStart() {
doStart();
OnInitListener ttsInitListener = initListenerCapture.getValue();
assertNotNull(ttsInitListener);
AndroidMock.replay(tts);
Assert.assertNotNull(ttsInitListener);
ttsInitListener.onInit(TextToSpeech.SUCCESS);
AndroidMock.verify(mockTask, tts);
//TODO
//verify(mockTask, tts);
}
@Test
public void testStart_notReady() {
doStart();
OnInitListener ttsInitListener = initListenerCapture.getValue();
assertNotNull(ttsInitListener);
AndroidMock.replay(tts);
Assert.assertNotNull(ttsInitListener);
ttsInitListener.onInit(TextToSpeech.ERROR);
AndroidMock.verify(mockTask, tts);
//TODO
//AndroidMock.verify(mockTask, tts);
}
@Test
public void testShutdown() {
// First, start
doStart();
AndroidMock.verify(mockTask);
AndroidMock.reset(mockTask);
verify(mockTask);
reset(mockTask);
// Then, shut down
PhoneStateListener phoneListener = phoneListenerCapture.getValue();
mockTask.listenToPhoneState(
same(phoneListener), eq(PhoneStateListener.LISTEN_NONE));
tts.shutdown();
AndroidMock.replay(mockTask, tts);
task.shutdown();
AndroidMock.verify(mockTask, tts);
//TODO
//AndroidMock.verify(mockTask, tts);
}
public void testRun() throws Exception {
// Expect service data calls
@Test
public void testRun() {
// when service data calls
TripStatistics stats = new TripStatistics();
// Expect announcement building call
expect(mockTask.getAnnouncement(same(stats))).andStubReturn(ANNOUNCEMENT);
// when announcement building call
when(mockTask.getAnnouncement(same(stats))).thenReturn(ANNOUNCEMENT);
// Put task in "ready" state
startTask(TextToSpeech.SUCCESS);
expect(tts.isLanguageAvailable(DEFAULT_LOCALE)).andStubReturn(TextToSpeech.LANG_AVAILABLE);
expect(tts.setLanguage(DEFAULT_LOCALE)).andReturn(TextToSpeech.LANG_AVAILABLE);
expect(tts.setSpeechRate(AnnouncementPeriodicTask.TTS_SPEECH_RATE)).andReturn(
TextToSpeech.SUCCESS);
expect(tts.setOnUtteranceCompletedListener((OnUtteranceCompletedListener) EasyMock.anyObject()))
.andReturn(0);
// Expect actual announcement call
expect(
tts.speak(eq(ANNOUNCEMENT), eq(TextToSpeech.QUEUE_FLUSH),
eq(AnnouncementPeriodicTask.SPEECH_PARAMS))).andReturn(0);
when(tts.isLanguageAvailable(DEFAULT_LOCALE)).thenReturn(TextToSpeech.LANG_AVAILABLE);
when(tts.setLanguage(DEFAULT_LOCALE)).thenReturn(TextToSpeech.LANG_AVAILABLE);
when(tts.setSpeechRate(AnnouncementPeriodicTask.TTS_SPEECH_RATE)).thenReturn(TextToSpeech.SUCCESS);
when(tts.setOnUtteranceCompletedListener((OnUtteranceCompletedListener) any())).thenReturn(0);
// Run the announcement
AndroidMock.replay(tts);
task.announce(stats);
AndroidMock.verify(mockTask, tts);
//TODO
//AndroidMock.verify(mockTask, tts);
}
public void testRun_notReady() throws Exception {
@Test
public void testRun_notReady() {
// Put task in "not ready" state
startTask(TextToSpeech.ERROR);
// Run the announcement
AndroidMock.replay(tts);
task.run(null);
AndroidMock.verify(mockTask, tts);
//TODO
//verify(mockTask, tts);
}
public void testRun_duringCall() throws Exception {
@Test
public void testRun_duringCall() {
startTask(TextToSpeech.SUCCESS);
expect(tts.isSpeaking()).andStubReturn(false);
when(tts.isSpeaking()).thenReturn(false);
// Run the announcement
AndroidMock.replay(tts);
PhoneStateListener phoneListener = phoneListenerCapture.getValue();
phoneListener.onCallStateChanged(TelephonyManager.CALL_STATE_OFFHOOK, null);
task.run(null);
AndroidMock.verify(mockTask, tts);
//TODO
//verify(mockTask, tts);
}
public void testRun_ringWhileSpeaking() throws Exception {
@Test
public void testRun_ringWhileSpeaking() {
startTask(TextToSpeech.SUCCESS);
expect(tts.isSpeaking()).andStubReturn(true);
expect(tts.stop()).andReturn(TextToSpeech.SUCCESS);
AndroidMock.replay(tts);
when(tts.isSpeaking()).thenReturn(true);
when(tts.stop()).thenReturn(TextToSpeech.SUCCESS);
// Update the state to ringing - this should stop the current announcement.
PhoneStateListener phoneListener = phoneListenerCapture.getValue();
@@ -232,111 +239,118 @@ public class AnnouncementPeriodicTaskTest extends AndroidTestCase {
// Run the announcement - this should do nothing.
task.run(null);
AndroidMock.verify(mockTask, tts);
//TODO
//AndroidMock.verify(mockTask, tts);
}
public void testRun_whileRinging() throws Exception {
@Test
public void testRun_whileRinging() {
startTask(TextToSpeech.SUCCESS);
expect(tts.isSpeaking()).andStubReturn(false);
when(tts.isSpeaking()).thenReturn(false);
// Run the announcement
AndroidMock.replay(tts);
PhoneStateListener phoneListener = phoneListenerCapture.getValue();
phoneListener.onCallStateChanged(TelephonyManager.CALL_STATE_RINGING, null);
task.run(null);
AndroidMock.verify(mockTask, tts);
//TODO
//AndroidMock.verify(mockTask, tts);
}
public void testRun_noService() throws Exception {
@Test
public void testRun_noService() {
startTask(TextToSpeech.SUCCESS);
// Run the announcement
AndroidMock.replay(tts);
task.run(null);
AndroidMock.verify(mockTask, tts);
//TODO
//AndroidMock.verify(mockTask, tts);
}
public void testRun_noStats() throws Exception {
// Expect service data calls
@Test
public void testRun_noStats() {
// when service data calls
startTask(TextToSpeech.SUCCESS);
// Run the announcement
AndroidMock.replay(tts);
task.run(null);
AndroidMock.verify(mockTask, tts);
//TODO
//AndroidMock.verify(mockTask, tts);
}
/**
* Tests {@link AnnouncementPeriodicTask#getAnnounceTime(long)} with time zero.
*/
@Test
public void testGetAnnounceTime_time_zero() {
long time = 0; // 0 seconds
assertEquals("0 minutes 0 seconds", task.getAnnounceTime(time));
Assert.assertEquals("0 minutes 0 seconds", task.getAnnounceTime(time));
}
/**
* Tests {@link AnnouncementPeriodicTask#getAnnounceTime(long)} with time one.
*/
@Test
public void testGetAnnounceTime_time_one() {
long time = 1 * 1000; // 1 second
assertEquals("0 minutes 1 second", task.getAnnounceTime(time));
long time = 1000; // 1 second
Assert.assertEquals("0 minutes 1 second", task.getAnnounceTime(time));
}
/**
* Tests {@link AnnouncementPeriodicTask#getAnnounceTime(long)} with singular
* numbers with the hour unit.
*/
@Test
public void testGetAnnounceTime_singular_has_hour() {
long time = (1 * 60 * 60 * 1000) + (1 * 60 * 1000) + (1 * 1000); // 1 hour 1 minute 1 second
assertEquals("1 hour 1 minute 1 second", task.getAnnounceTime(time));
long time = (60 * 60 * 1000) + (60 * 1000) + (1000); // 1 hour 1 minute 1 second
Assert.assertEquals("1 hour 1 minute 1 second", task.getAnnounceTime(time));
}
/**
* Tests {@link AnnouncementPeriodicTask#getAnnounceTime(long)} with plural numbers
* with the hour unit.
*/
@Test
public void testGetAnnounceTime_plural_has_hour() {
long time = (2 * 60 * 60 * 1000) + (2 * 60 * 1000) + (2 * 1000); // 2 hours 2 minutes 2 seconds
assertEquals("2 hours 2 minutes 2 seconds", task.getAnnounceTime(time));
Assert.assertEquals("2 hours 2 minutes 2 seconds", task.getAnnounceTime(time));
}
/**
* Tests {@link AnnouncementPeriodicTask#getAnnounceTime(long)} with singular
* numbers without the hour unit.
*/
@Test
public void testGetAnnounceTime_singular_no_hour() {
long time = (1 * 60 * 1000) + (1 * 1000); // 1 minute 1 second
assertEquals("1 minute 1 second", task.getAnnounceTime(time));
Assert.assertEquals("1 minute 1 second", task.getAnnounceTime(time));
}
/**
* Tests {@link AnnouncementPeriodicTask#getAnnounceTime(long)} with plural numbers
* without the hour unit.
*/
@Test
public void testGetAnnounceTime_plural_no_hour() {
long time = (2 * 60 * 1000) + (2 * 1000); // 2 minutes 2 seconds
assertEquals("2 minutes 2 seconds", task.getAnnounceTime(time));
Assert.assertEquals("2 minutes 2 seconds", task.getAnnounceTime(time));
}
private void startTask(int state) {
AndroidMock.resetToNice(tts);
AndroidMock.replay(tts);
reset(tts);
doStart();
OnInitListener ttsInitListener = initListenerCapture.getValue();
ttsInitListener.onInit(state);
AndroidMock.resetToDefault(tts);
//TODO
reset(tts);
//AndroidMock.resetToDefault(tts);
}
private void doStart() {
mockTask.listenToPhoneState(capture(phoneListenerCapture),
eq(PhoneStateListener.LISTEN_CALL_STATE));
expect(mockTask.newTextToSpeech(
same(getContext()), capture(initListenerCapture)))
.andStubReturn(ttsDelegate);
AndroidMock.replay(mockTask);
mockTask.listenToPhoneState(phoneListenerCapture.capture(), eq(PhoneStateListener.LISTEN_CALL_STATE));
when(mockTask.newTextToSpeech(same(context), initListenerCapture.capture())).thenReturn(ttsDelegate);
task.start();
}
@@ -365,10 +379,7 @@ public class AnnouncementPeriodicTaskTest extends AndroidTestCase {
int playEarcon(String earcon, int queueMode,
HashMap<String, String> params);
int playSilence(long durationInMs, int queueMode,
HashMap<String, String> params);
int setEngineByPackageName(String enginePackageName);
int playSilence(long durationInMs, int queueMode, HashMap<String, String> params);
int setLanguage(Locale loc);
@@ -384,8 +395,7 @@ public class AnnouncementPeriodicTaskTest extends AndroidTestCase {
int stop();
int synthesizeToFile(String text, HashMap<String, String> params,
String filename);
int synthesizeToFile(String text, HashMap<String, String> params, String filename);
}
/**
@@ -401,8 +411,7 @@ public class AnnouncementPeriodicTaskTest extends AndroidTestCase {
* set) will go up to the original class, but after tts is set we'll forward
* them all to the mock.
*/
private class TextToSpeechDelegate
extends TextToSpeech implements TextToSpeechInterface {
private class TextToSpeechDelegate extends TextToSpeech implements TextToSpeechInterface {
public TextToSpeechDelegate(Context context, OnInitListener listener) {
super(context, listener);
}
@@ -464,8 +473,7 @@ public class AnnouncementPeriodicTaskTest extends AndroidTestCase {
}
@Override
public int playEarcon(String earcon, int queueMode,
HashMap<String, String> params) {
public int playEarcon(String earcon, int queueMode, HashMap<String, String> params) {
if (tts == null) {
return super.playEarcon(earcon, queueMode, params);
}
@@ -473,8 +481,7 @@ public class AnnouncementPeriodicTaskTest extends AndroidTestCase {
}
@Override
public int playSilence(long durationInMs, int queueMode,
HashMap<String, String> params) {
public int playSilence(long durationInMs, int queueMode, HashMap<String, String> params) {
if (tts == null) {
return super.playSilence(durationInMs, queueMode, params);
}
@@ -490,8 +497,7 @@ public class AnnouncementPeriodicTaskTest extends AndroidTestCase {
}
@Override
public int setOnUtteranceCompletedListener(
OnUtteranceCompletedListener listener) {
public int setOnUtteranceCompletedListener(OnUtteranceCompletedListener listener) {
if (tts == null) {
return super.setOnUtteranceCompletedListener(listener);
}
@@ -541,8 +547,7 @@ public class AnnouncementPeriodicTaskTest extends AndroidTestCase {
}
@Override
public int synthesizeToFile(String text, HashMap<String, String> params,
String filename) {
public int synthesizeToFile(String text, HashMap<String, String> params, String filename) {
if (tts == null) {
return super.synthesizeToFile(text, params, filename);
}
@@ -32,16 +32,13 @@ public class FileUtilsTest extends TestCase {
* Tests {@link FileUtils#getPath(String...)}.
*/
public void testBuildExternalDirectoryPath() {
String expectedName = Environment.getExternalStorageDirectory() + File.separator
+ FileUtils.SDCARD_TOP_DIR + File.separator + "a" + File.separator + "b" + File.separator
+ "c";
String expectedName = Environment.getExternalStorageDirectory() + File.separator + FileUtils.SDCARD_TOP_DIR + File.separator + "a" + File.separator + "b" + File.separator + "c";
String dirName = FileUtils.getPath("a", "b", "c");
assertEquals(expectedName, dirName);
}
/**
* Tests {@link FileUtils#buildUniqueFileName(File, String, String)} when the
* file is new.
* Tests {@link FileUtils#buildUniqueFileName(File, String, String)} when the file is new.
*/
public void testBuildUniqueFileName_new() {
String filename = FileUtils.buildUniqueFileName(new File("/dir"), "Filename", "ext");
@@ -49,8 +46,7 @@ public class FileUtilsTest extends TestCase {
}
/**
* Tests {@link FileUtils#buildUniqueFileName(File, String, String)} when the
* file exists already.
* Tests {@link FileUtils#buildUniqueFileName(File, String, String)} when the file exists already.
*/
public void testBuildUniqueFileName_exist() {
// Expect "/default.prop" to exist on the phone/emulator
@@ -69,8 +65,8 @@ public class FileUtilsTest extends TestCase {
}
/**
* Tests {@link FileUtils#sanitizeFileName(String)} with i18n characters (in
* Chinese and Russian). Verifies that they are allowed.
* Tests {@link FileUtils#sanitizeFileName(String)} with i18n characters (in Chinese and Russian).
* Verifies that they are allowed.
*/
public void testSanitizeFileName_i18n() {
String name = "您好-привет";
@@ -79,8 +75,8 @@ public class FileUtilsTest extends TestCase {
}
/**
* Tests {@link FileUtils#sanitizeFileName(String)} with special FAT32
* characters. Verifies that they are allowed.
* Tests {@link FileUtils#sanitizeFileName(String)} with special FAT32 characters.
* Verifies that they are allowed.
*/
public void testSanitizeFileName_special_characters() {
String name = "$%'-_@~`!(){}^#&+,;=[] ";
@@ -89,8 +85,8 @@ public class FileUtilsTest extends TestCase {
}
/**
* Tests {@link FileUtils#sanitizeFileName(String)} with multiple escaped
* characters in a row. Verifies that they are collapsed into one underscore.
* Tests {@link FileUtils#sanitizeFileName(String)} with multiple escaped characters in a row.
* Verifies that they are collapsed into one underscore.
*/
public void testSanitizeFileName_collapse() {
String name = "hello//there";
@@ -99,8 +95,8 @@ public class FileUtilsTest extends TestCase {
}
/**
* Tests {@link FileUtils#truncateFileName(File, String, String)}. Verifies
* the a long file name is truncated.
* Tests {@link FileUtils#truncateFileName(File, String, String)}.
* Verifies the a long file name is truncated.
*/
public void testTruncateFileName() {
File directory = new File("/dir1/dir2/");
@@ -115,7 +111,6 @@ public class FileUtilsTest extends TestCase {
for (int i = 0; i < truncated.length(); i++) {
assertEquals('a', truncated.charAt(i));
}
assertEquals(FileUtils.MAX_FAT32_PATH_LENGTH,
new File(directory, truncated + suffix).getPath().length());
assertEquals(FileUtils.MAX_FAT32_PATH_LENGTH, new File(directory, truncated + suffix).getPath().length());
}
}
@@ -16,10 +16,13 @@
package de.dennisguse.opentracks.util;
import android.content.Context;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.platform.app.InstrumentationRegistry;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.util.GregorianCalendar;
@@ -33,9 +36,13 @@ import java.util.TimeZone;
@RunWith(AndroidJUnit4.class)
public class StringUtilsTest {
private Context context = ApplicationProvider.getApplicationContext();
/**
* Tests {@link StringUtils#formatDateTimeIso8601(long)}.
*/
@Test
public void testFormatDateTimeIso8601() {
Assert.assertEquals("1970-01-01T00:00:12.345Z", StringUtils.formatDateTimeIso8601(12345));
}
@@ -43,6 +50,7 @@ public class StringUtilsTest {
/**
* Tests {@link StringUtils#formatElapsedTime(long)}.
*/
@Test
public void testformatElapsedTime() {
// 1 second
Assert.assertEquals("00:01", StringUtils.formatElapsedTime(1000));
@@ -63,6 +71,7 @@ public class StringUtilsTest {
/**
* Tests {@link StringUtils#formatElapsedTimeWithHour(long)}.
*/
@Test
public void testformatElapsedTimeWithHour() {
// 1 second
Assert.assertEquals("0:00:01", StringUtils.formatElapsedTimeWithHour(1000));
@@ -84,20 +93,22 @@ public class StringUtilsTest {
* Tests {@link StringUtils#formatDistance(android.content.Context, double,
* boolean)}.
*/
@Test
public void testFormatDistance() {
// A large number in metric
Assert.assertEquals("5.00 km", StringUtils.formatDistance(InstrumentationRegistry.getInstrumentation().getContext(), 5000, true));
Assert.assertEquals("5.00 km", StringUtils.formatDistance(context, 5000, true));
// A large number in imperial
Assert.assertEquals("3.11 mi", StringUtils.formatDistance(InstrumentationRegistry.getInstrumentation().getContext(), 5000, false));
Assert.assertEquals("3.11 mi", StringUtils.formatDistance(context, 5000, false));
// A small number in metric
Assert.assertEquals("100.00 m", StringUtils.formatDistance(InstrumentationRegistry.getInstrumentation().getContext(), 100, true));
Assert.assertEquals("100.00 m", StringUtils.formatDistance(context, 100, true));
// A small number in imperial
Assert.assertEquals("328.08 ft", StringUtils.formatDistance(InstrumentationRegistry.getInstrumentation().getContext(), 100, false));
Assert.assertEquals("328.08 ft", StringUtils.formatDistance(context, 100, false));
}
/**
* Tests {@link StringUtils#formatCData(String)}.
*/
@Test
public void testFormatCData() {
Assert.assertEquals("<![CDATA[hello]]>", StringUtils.formatCData("hello"));
Assert.assertEquals("<![CDATA[hello]]]]><![CDATA[>there]]>", StringUtils.formatCData("hello]]>there"));
@@ -106,6 +117,7 @@ public class StringUtilsTest {
/**
* Tests {@link StringUtils#getTime(String)}.
*/
@Test
public void testGetTime() {
assertGetTime("2010-05-04T03:02:01", 2010, 5, 4, 3, 2, 1, 0);
assertGetTime("2010-05-04T03:02:01Z", 2010, 5, 4, 3, 2, 1, 0);
@@ -114,6 +126,7 @@ public class StringUtilsTest {
/**
* Tests {@link StringUtils#getTime(String)} with fractional seconds.
*/
@Test
public void testGetTime_fractional() {
assertGetTime("2010-05-04T03:02:01.3", 2010, 5, 4, 3, 2, 1, 300);
assertGetTime("2010-05-04T03:02:01.35", 2010, 5, 4, 3, 2, 1, 350);
@@ -124,6 +137,7 @@ public class StringUtilsTest {
/**
* Tests {@link StringUtils#getTime(String)} with time zone.
*/
@Test
public void testGetTime_timezone() {
assertGetTime("2010-05-04T03:02:01Z", 2010, 5, 4, 3, 2, 1, 0);
assertGetTime("2010-05-04T03:02:01+00:00", 2010, 5, 4, 3, 2, 1, 0);
@@ -135,9 +149,9 @@ public class StringUtilsTest {
}
/**
* Tests {@link StringUtils#getTime(String)} with fractional seconds and time
* zone.
* Tests {@link StringUtils#getTime(String)} with fractional seconds and time zone.
*/
@Test
public void testGetTime_fractionalAndTimezone() {
assertGetTime("2010-05-04T03:02:01.352Z", 2010, 5, 4, 3, 2, 1, 352);
assertGetTime("2010-05-04T03:02:01.47+00:00", 2010, 5, 4, 3, 2, 1, 470);
@@ -146,8 +160,7 @@ public class StringUtilsTest {
}
/**
* Asserts the {@link StringUtils#getTime(String)} returns the expected
* values.
* Asserts the {@link StringUtils#getTime(String)} returns the expected values.
*
* @param xmlDateTime the xml date time string
* @param year the expected year
@@ -158,8 +171,7 @@ public class StringUtilsTest {
* @param second the expected second
* @param millisecond the expected milliseconds
*/
private void assertGetTime(String xmlDateTime, int year, int month, int day, int hour, int minute,
int second, int millisecond) {
private void assertGetTime(String xmlDateTime, int year, int month, int day, int hour, int minute, int second, int millisecond) {
GregorianCalendar calendar = new GregorianCalendar(TimeZone.getTimeZone("UTC"));
calendar.set(year, month - 1, day, hour, minute, second);
calendar.set(GregorianCalendar.MILLISECOND, millisecond);
@@ -169,7 +181,8 @@ public class StringUtilsTest {
/**
* Tests {@link StringUtils#getTimeParts(long)} with a positive number.
*/
public void testGetTimeParts_postive() {
@Test
public void testGetTimeParts_positive() {
int[] parts = StringUtils.getTimeParts(61000);
Assert.assertEquals(1, parts[0]);
Assert.assertEquals(1, parts[1]);
@@ -179,6 +192,7 @@ public class StringUtilsTest {
/**
* Tests {@link StringUtils#getTimeParts(long)} with a negative number.
*/
@Test
public void testGetTimeParts_negative() {
int[] parts = StringUtils.getTimeParts(-61000);
Assert.assertEquals(-1, parts[0]);
@@ -18,18 +18,19 @@ package de.dennisguse.opentracks.util;
import android.content.Context;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.platform.app.InstrumentationRegistry;
import de.dennisguse.opentracks.R;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import de.dennisguse.opentracks.R;
/**
* Tests {@link TrackNameUtils}.
*
@@ -41,37 +42,33 @@ public class TrackNameUtilsTest {
private static final long TRACK_ID = 1L;
private static final long START_TIME = 1288213406000L;
private static final Context CONTEXT = InstrumentationRegistry.getInstrumentation().getContext();
private static final Context CONTEXT = ApplicationProvider.getApplicationContext();
/**
* Tests when the track_name_key is
* settings_recording_track_name_date_local_value.
* Tests when the track_name_key is settings_recording_track_name_date_local_value.
*/
@Test
public void testTrackName_date_local() {
PreferencesUtils.setString(CONTEXT, R.string.track_name_key, CONTEXT.getString(R.string.settings_recording_track_name_date_local_value));
Assert.assertEquals(StringUtils.formatDateTime(CONTEXT, START_TIME),
TrackNameUtils.getTrackName(CONTEXT, TRACK_ID, START_TIME, null));
Assert.assertEquals(StringUtils.formatDateTime(CONTEXT, START_TIME), TrackNameUtils.getTrackName(CONTEXT, TRACK_ID, START_TIME));
}
/**
* Tests when the track_name_key is
* settings_recording_track_name_date_iso_8601_value.
* Tests when the track_name_key is settings_recording_track_name_date_iso_8601_value.
*/
@Test
public void testTrackName_date_iso_8601() {
PreferencesUtils.setString(CONTEXT, R.string.track_name_key, CONTEXT.getString(R.string.settings_recording_track_name_date_iso_8601_value));
SimpleDateFormat simpleDateFormat = new SimpleDateFormat(
TrackNameUtils.ISO_8601_FORMAT, Locale.US);
Assert.assertEquals(simpleDateFormat.format(new Date(START_TIME)),
TrackNameUtils.getTrackName(CONTEXT, TRACK_ID, START_TIME, null));
SimpleDateFormat simpleDateFormat = new SimpleDateFormat(TrackNameUtils.ISO_8601_FORMAT, Locale.US);
Assert.assertEquals(simpleDateFormat.format(new Date(START_TIME)), TrackNameUtils.getTrackName(CONTEXT, TRACK_ID, START_TIME));
}
/**
* Tests when the track_name_key is
* settings_recording_track_name_number_value.
* Tests when the track_name_key is settings_recording_track_name_number_value.
*/
@Test
public void testTrackName_number() {
PreferencesUtils.setString(CONTEXT, R.string.track_name_key, CONTEXT.getString(R.string.settings_recording_track_name_number_value));
Assert.assertEquals(
"Track " + TRACK_ID, TrackNameUtils.getTrackName(CONTEXT, TRACK_ID, START_TIME, null));
Assert.assertEquals("Track " + TRACK_ID, TrackNameUtils.getTrackName(CONTEXT, TRACK_ID, START_TIME));
}
}
+97 -113
View File
@@ -14,75 +14,31 @@ See the License for the specific language governing permissions and
limitations under the License.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="de.dennisguse.opentracks"
android:installLocation="auto"
android:versionCode="2916"
android:versionName="v3.0.0"> <!-- For FDroid. -->
android:versionCode="3063"
android:versionName="v3.2.1"> <!-- For FDroid. -->
<!-- Permission to initialize services -->
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<!-- Permissions to record locations -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<!-- Permissions to access sensors -->
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<!-- Permission to read phone state so voice announcement will not interrupt ongoing calls -->
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<!-- Permission to read/write external storage -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<!-- Required features -->
<uses-feature
android:name="android.hardware.location.gps"
android:required="true" />
<!-- Optional features -->
<uses-feature
android:name="android.hardware.bluetooth"
android:required="false" />
<uses-feature
android:name="android.hardware.camera"
android:required="false" />
<uses-feature
android:name="android.hardware.faketouch"
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"
android:required="false" />
<uses-feature
android:name="android.hardware.touchscreen.multitouch"
android:required="false" />
<supports-screens
android:anyDensity="true"
android:largeScreens="true"
android:normalScreens="true"
android:smallScreens="true"
android:xlargeScreens="true" />
<application
android:allowBackup="true"
android:name=".Startup"
android:allowBackup="false"
android:hardwareAccelerated="true"
android:icon="@drawable/ic_logo"
android:icon="@drawable/ic_logo_color_24dp"
android:label="@string/app_name"
android:theme="@style/ThemeCustom"
tools:replace="android:label">
android:theme="@style/ThemeCustom">
<!-- Activities -->
<activity android:name=".AboutActivity" />
<activity
android:name=".AggregatedStatsActivity"
android:label="@string/menu_aggregated_statistics" />
<activity android:name=".DeleteActivity" />
<activity android:name=".io.file.importer.ImportActivity">
<activity
android:name=".io.file.importer.ImportActivity"
android:theme="@style/ThemeCustomTransparent">
<intent-filter>
<action android:name="android.intent.action.ATTACH_DATA" />
<action android:name="android.intent.action.VIEW" />
@@ -97,8 +53,11 @@ limitations under the License.
<data android:scheme="file" />
</intent-filter>
</activity>
<activity android:name=".MarkerEditActivity" />
<activity android:name=".MarkerDetailActivity" />
<activity
android:name=".MarkerListActivity"
android:label="@string/menu_markers">
@@ -106,6 +65,7 @@ limitations under the License.
android:name="android.app.default_searchable"
android:value=".SearchListActivity" />
</activity>
<activity
android:name=".SearchListActivity"
android:launchMode="singleTop">
@@ -114,16 +74,15 @@ limitations under the License.
</intent-filter>
<meta-data
android:name="android.app.searchable"
android:resource="@xml/search" />
android:resource="@xml/searchable" />
</activity>
<activity
android:name=".TrackDetailActivity"
android:parentActivityName=".TrackListActivity">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value=".TrackListActivity" />
</activity>
android:screenOrientation="userPortrait" />
<activity android:name=".TrackEditActivity" />
<activity android:name=".TrackListActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
@@ -133,60 +92,19 @@ limitations under the License.
android:name="android.app.default_searchable"
android:value=".SearchListActivity" />
</activity>
<activity android:name=".io.file.exporter.SaveActivity" />
<activity android:name=".settings.AdvancedSettingsActivity" />
<activity android:name=".settings.ChartSettingsActivity" />
<activity android:name=".settings.RecordingSettingsActivity" />
<activity android:name=".settings.SensorSettingsActivity" />
<activity android:name=".settings.SettingsActivity" />
<activity android:name=".settings.StatsSettingsActivity" />
<activity
android:name=".widgets.TrackWidgetConfigActivity"
android:label="@string/track_widget_config_title">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
</intent-filter>
</activity>
<service
android:name=".services.ControlRecordingService"
android:exported="false" />
<service
android:name=".services.TrackRecordingService"
android:enabled="true"
android:exported="false"
android:icon="@drawable/ic_arrow_320"
android:label="@string/recording_service" />
<!-- Receivers -->
<receiver android:name=".BootReceiver">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
<receiver android:name=".widgets.TrackWidgetProvider">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
<action android:name="TRACK_PAUSED" />
<action android:name="TRACK_RESUMED" />
<action android:name="TRACK_STARTED" />
<action android:name="TRACK_STOPPED" />
<action android:name="TRACK_UPDATE" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/track_widget_info" />
</receiver>
<!-- Providers -->
<activity
android:name=".io.file.exporter.ExportActivity"
android:theme="@style/ThemeCustomTransparent" />
<activity android:name=".settings.SettingsActivity" />
<!-- Services -->
<provider
android:name=".content.CustomContentProvider"
android:authorities="${applicationId}"
android:name=".content.ShareContentProvider"
android:authorities="${applicationId}.content"
android:exported="false"
android:grantUriPermissions="true" />
<!-- Search suggestion provider -->
<provider
android:name=".content.SearchEngineProvider"
android:authorities="${applicationId}.search"
android:exported="false" />
<provider
android:name="androidx.core.content.FileProvider"
@@ -197,5 +115,71 @@ limitations under the License.
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
<!-- Receivers -->
<receiver android:name=".BootReceiver">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
<!-- Providers -->
<service
android:name=".services.TrackRecordingService"
android:enabled="true"
android:exported="false"
android:icon="@drawable/ic_logo_color_24dp"
android:label="@string/recording_service" />
</application>
<supports-screens
android:anyDensity="true"
android:largeScreens="true"
android:normalScreens="true"
android:smallScreens="true"
android:xlargeScreens="true" />
<!-- Permissions to record locations -->
<uses-feature
android:name="android.hardware.location.gps"
android:required="true" />
<uses-feature
android:name="android.hardware.bluetooth"
android:required="false" />
<!-- Permissions to access sensors -->
<uses-feature
android:name="android.hardware.camera"
android:required="false" />
<uses-feature
android:name="android.hardware.faketouch"
android:required="false" />
<!-- Permission to read phone state so voice announcement will not interrupt ongoing calls -->
<uses-feature
android:name="android.hardware.sensor.compass"
android:required="false" />
<!-- Permission to read/write external storage -->
<uses-feature
android:name="android.hardware.telephony"
android:required="false" />
<uses-feature
android:name="android.hardware.telephony.gsm"
android:required="false" />
<!-- Required features -->
<uses-feature
android:name="android.hardware.touchscreen"
android:required="false" />
<!-- Optional features -->
<uses-feature
android:name="android.hardware.touchscreen.multitouch"
android:required="false" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
</manifest>
@@ -16,16 +16,11 @@
package de.dennisguse.opentracks;
import android.app.ActionBar;
import android.os.Bundle;
import android.speech.tts.TextToSpeech;
import android.view.Menu;
import android.view.MenuItem;
import androidx.fragment.app.FragmentActivity;
import de.dennisguse.opentracks.util.TrackIconUtils;
/**
* @author Jimmy Shih
*/
@@ -39,49 +34,10 @@ public abstract class AbstractActivity extends FragmentActivity {
setVolumeControlStream(TextToSpeech.Engine.DEFAULT_STREAM);
setContentView(getLayoutResId());
// Configure action bar must be after setContentView
if (configureActionBarHomeAsUp()) {
ActionBar actionBar = this.getActionBar();
if (actionBar != null) {
actionBar.setHomeButtonEnabled(true);
actionBar.setDisplayHomeAsUpEnabled(true);
}
}
}
/**
* Gets the layout resource id.
*/
protected abstract int getLayoutResId();
/**
* Returns true to configure the action bar home button as the up button (go to previous activity).
*/
protected boolean configureActionBarHomeAsUp() {
return true;
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
TrackIconUtils.setMenuIconColor(menu);
return super.onCreateOptionsMenu(menu);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
// Handles actionBar's home menu item (arrow top left corner) and triggers, up navigation to previous activity.
if (item.getItemId() == android.R.id.home) {
onHomeSelected();
return true;
}
return super.onOptionsItemSelected(item);
}
/**
* Callback when the home menu item is selected. E.g., setup the back stack when home is selected.
*/
protected void onHomeSelected() {
finish();
}
}
@@ -18,8 +18,10 @@ package de.dennisguse.opentracks;
import android.app.Activity;
import android.app.SearchManager;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.util.Log;
import android.util.SparseBooleanArray;
import android.view.ActionMode;
import android.view.Menu;
@@ -29,15 +31,15 @@ import android.widget.AbsListView;
import android.widget.ListView;
import android.widget.SearchView;
import androidx.annotation.NonNull;
import java.util.ArrayList;
import de.dennisguse.opentracks.fragments.ConfirmDeleteDialogFragment;
import de.dennisguse.opentracks.fragments.ConfirmDeleteDialogFragment.ConfirmDeleteCaller;
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
import de.dennisguse.opentracks.util.IntentUtils;
import de.dennisguse.opentracks.util.PreferencesUtils;
import de.dennisguse.opentracks.util.TrackRecordingServiceConnectionUtils;
import de.dennisguse.opentracks.R;
import java.util.ArrayList;
/**
* An abstract class for the following common tasks across
@@ -49,7 +51,9 @@ import java.util.ArrayList;
*
* @author Jimmy Shih
*/
public abstract class AbstractTrackActivity extends AbstractActivity implements ConfirmDeleteCaller {
public abstract class AbstractListActivity extends AbstractActivity implements ConfirmDeleteCaller {
private static final String TAG = AbstractListActivity.class.getSimpleName();
protected static final int GPS_REQUEST_CODE = 6;
private static final int DELETE_REQUEST_CODE = 3;
@@ -112,15 +116,21 @@ public abstract class AbstractTrackActivity extends AbstractActivity implements
}
public static void configureSearchWidget(Activity activity, final MenuItem menuItem, final TrackController trackController) {
final SearchView searchView = (SearchView) menuItem.getActionView();
SearchManager searchManager = (SearchManager) activity.getSystemService(Context.SEARCH_SERVICE);
SearchView searchView = (SearchView) menuItem.getActionView();
searchView.setSearchableInfo(searchManager.getSearchableInfo(activity.getComponentName()));
if (searchManager != null) {
//NOTE: for some reason activity.getComponentName() did not trigger the SearchListActivity
searchView.setSearchableInfo(searchManager.getSearchableInfo(new ComponentName(activity, SearchListActivity.class)));
} else {
Log.w(TAG, "Could not retrieve SearchManager.");
}
searchView.setQueryRefinementEnabled(true);
searchView.setSubmitButtonEnabled(true);
searchView.setOnQueryTextFocusChangeListener(new View.OnFocusChangeListener() {
@Override
public void onFocusChange(View v, boolean hasFocus) {
// Hide and show trackController when search widget has focus/no focus
// Hide and show trackController when searchable widget has focus/no focus
if (trackController != null) {
if (hasFocus) {
trackController.hide();
@@ -172,8 +182,7 @@ public abstract class AbstractTrackActivity extends AbstractActivity implements
* @param trackIds the track ids
*/
protected void deleteTracks(long[] trackIds) {
ConfirmDeleteDialogFragment.newInstance(trackIds)
.show(getSupportFragmentManager(), ConfirmDeleteDialogFragment.CONFIRM_DELETE_DIALOG_TAG);
ConfirmDeleteDialogFragment.showDialog(getSupportFragmentManager(), trackIds);
}
@Override
@@ -191,7 +200,7 @@ public abstract class AbstractTrackActivity extends AbstractActivity implements
}
}
if (stopRecording) {
TrackRecordingServiceConnectionUtils.stopRecording(this, getTrackRecordingServiceConnection(), false);
getTrackRecordingServiceConnection().stopRecording(this, false);
}
Intent intent = IntentUtils.newIntent(this, DeleteActivity.class);
intent.putExtra(DeleteActivity.EXTRA_TRACK_IDS, trackIds);
@@ -202,6 +211,7 @@ public abstract class AbstractTrackActivity extends AbstractActivity implements
* Gets the track recording service connection. For stopping the current
* recording if need to delete the current recording track.
*/
@NonNull
abstract protected TrackRecordingServiceConnection getTrackRecordingServiceConnection();
/**
@@ -18,13 +18,12 @@ package de.dennisguse.opentracks;
import android.os.Bundle;
import java.util.List;
import de.dennisguse.opentracks.content.ContentProviderUtils;
import de.dennisguse.opentracks.content.Track;
import de.dennisguse.opentracks.stats.TripStatistics;
import de.dennisguse.opentracks.util.StatsUtils;
import de.dennisguse.opentracks.R;
import java.util.List;
/**
* An activity to view aggregated stats from all recorded tracks.
@@ -36,8 +35,8 @@ public class AggregatedStatsActivity extends AbstractActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
StatsUtils.setTripStatisticsValues(this, this, null, getTripStatistics(), null);
StatsUtils.setLocationValues(this, this, null, null, false);
StatsUtils.setTripStatisticsValues(this, getTripStatistics(), null);
StatsUtils.setLocationValues(this, null, false);
}
@Override
@@ -25,16 +25,16 @@ import android.graphics.Path;
import androidx.annotation.VisibleForTesting;
import de.dennisguse.opentracks.stats.ExtremityMonitor;
import java.text.NumberFormat;
import de.dennisguse.opentracks.stats.ExtremityMonitor;
/**
* This class encapsulates the meta data for one series of the chart values.
*
* @author Sandor Dornbush
*/
public class ChartValueSeries {
class ChartValueSeries {
private static final float STROKE_WIDTH = 2f;
@@ -68,8 +68,7 @@ public class ChartValueSeries {
* @param fillColor the fill color
* @param strokeColor the stroke color
*/
public ChartValueSeries(Context context, int absoluteMin, int absoluteMax, int[] intervalValues,
int metricTitleId, int imperialTitleId, int fillColor, int strokeColor) {
ChartValueSeries(Context context, int absoluteMin, int absoluteMax, int[] intervalValues, int metricTitleId, int imperialTitleId, int fillColor, int strokeColor) {
this.absoluteMin = absoluteMin;
this.absoluteMax = absoluteMax;
this.intervalValues = intervalValues;
@@ -108,7 +107,7 @@ public class ChartValueSeries {
/**
* Returns true if the series is enabled.
*/
public boolean isEnabled() {
boolean isEnabled() {
return enabled;
}
@@ -117,14 +116,14 @@ public class ChartValueSeries {
*
* @param enabled true to enable
*/
public void setEnabled(boolean enabled) {
void setEnabled(boolean enabled) {
this.enabled = enabled;
}
/**
* Returns true if the series has data.
*/
public boolean hasData() {
boolean hasData() {
return extremityMonitor.hasData();
}
@@ -133,14 +132,14 @@ public class ChartValueSeries {
*
* @param value the new value
*/
public void update(double value) {
void update(double value) {
extremityMonitor.update(value);
}
/**
* Gets the path.
*/
public Path getPath() {
Path getPath() {
return path;
}
@@ -149,7 +148,7 @@ public class ChartValueSeries {
*
* @param canvas the canvas
*/
public void drawPath(Canvas canvas) {
void drawPath(Canvas canvas) {
canvas.drawPath(path, fillPaint);
canvas.drawPath(path, strokePaint);
}
@@ -157,7 +156,7 @@ public class ChartValueSeries {
/**
* Updates the y axis dimension.
*/
public void updateDimension() {
void updateDimension() {
double min = hasData() ? extremityMonitor.getMin() : 0.0;
double max = hasData() ? extremityMonitor.getMax() : 1.0;
min = Math.max(min, absoluteMin);
@@ -204,14 +203,15 @@ public class ChartValueSeries {
/**
* Gets the interval value.
*/
public int getInterval() {
@VisibleForTesting
int getInterval() {
return interval;
}
/**
* Gets the min marker value.
*/
public int getMinMarkerValue() {
int getMinMarkerValue() {
return minMarkerValue;
}
@@ -226,28 +226,28 @@ public class ChartValueSeries {
/**
* Gets the title id.
*/
public int getTitleId(boolean metricUnits) {
int getTitleId(boolean metricUnits) {
return metricUnits ? metricTitleId : imperialTitleId;
}
/**
* Gets the title paint.
*/
public Paint getTitlePaint() {
Paint getTitlePaint() {
return titlePaint;
}
/**
* Gets the marker paint.
*/
public Paint getMarkerPaint() {
Paint getMarkerPaint() {
return markerPaint;
}
/**
* Gets the largest marker.
*/
public String getLargestMarker() {
String getLargestMarker() {
String minMarker = numberFormat.format(getMinMarkerValue());
String maxMarker = numberFormat.format(getMaxMarkerValue());
return minMarker.length() >= maxMarker.length() ? minMarker : maxMarker;
@@ -258,7 +258,7 @@ public class ChartValueSeries {
*
* @param value the value
*/
public String formatMarker(int value) {
String formatMarker(int value) {
return numberFormat.format(value);
}
}
@@ -191,7 +191,7 @@ public class ChartView extends View {
markerPaint.setColor(context.getResources().getColor(android.R.color.darker_gray));
markerPaint.setAntiAlias(false);
pointer = context.getResources().getDrawable(R.drawable.ic_arrow_180);
pointer = context.getResources().getDrawable(R.drawable.ic_logo_color_24dp);
pointer.setBounds(0, 0, pointer.getIntrinsicWidth(), pointer.getIntrinsicHeight());
Drawable statisticsMarker = getResources().getDrawable(R.drawable.ic_marker_yellow_pushpin);
@@ -223,8 +223,8 @@ public class ChartView extends View {
}
/**
* Sets chart by distance. It is expected that after changing this value, data
* will be reloaded.
* Sets chart by distance.
* It is expected that after changing this value, data will be reloaded.
*
* @param value true for by distance, false for by time
*/
@@ -449,8 +449,7 @@ public class ChartView extends View {
synchronized (waypoints) {
for (int i = 0; i < waypoints.size(); i++) {
Waypoint waypoint = waypoints.get(i);
int distance = Math.abs(
getX(getWaypointXValue(waypoint)) - (int) event.getX() - getScrollX());
int distance = Math.abs(getX(getWaypointXValue(waypoint)) - (int) event.getX() - getScrollX());
if (distance < minDistance) {
minDistance = distance;
nearestWaypoint = waypoint;
@@ -567,7 +566,7 @@ public class ChartView extends View {
* @param canvas the canvas
*/
private void drawSeriesTitles(Canvas canvas) {
int[] titleDimensions = getTitleDimenions();
int[] titleDimensions = getTitleDimensions();
int lines = titleDimensions[0];
int lineHeight = titleDimensions[1];
int count = 0;
@@ -585,10 +584,10 @@ public class ChartView extends View {
}
/**
* Gets the title dimensions. Returns an array of 2 integers, first element is
* the number of lines and the second element is the line height.
* Gets the title dimensions.
* Returns an array of 2 integers, first element is the number of lines and the second element is the line height.
*/
private int[] getTitleDimenions() {
private int[] getTitleDimensions() {
int lines = 0;
int lineHeight = 0;
for (int i = 0; i < series.length; i++) {
@@ -768,8 +767,7 @@ public class ChartView extends View {
}
/**
* Updates paths. The path needs to be updated any time after the data or the
* dimensions change.
* Updates paths. The path needs to be updated any time after the data or the dimensions change.
*/
private void updatePaths() {
synchronized (chartData) {
@@ -867,7 +865,7 @@ public class ChartView extends View {
}
leftBorder = (int) (density * BORDER + markerLength);
int[] titleDimensions = getTitleDimenions();
int[] titleDimensions = getTitleDimensions();
topBorder = (int) (density * BORDER + titleDimensions[0] * (titleDimensions[1] + spacer));
Rect xAxisLabelRect = getRect(axisPaint, getXAxisLabel());
// border + x axis marker + spacer + .5 x axis label
@@ -1,37 +0,0 @@
/*
* Copyright 2008 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package de.dennisguse.opentracks;
/**
* Common constants.
*
* @author Leif Hendrik Wilden
*/
public class Constants {
/**
* Maximum number of waypoints that will be loaded at one time.
*/
public static final int MAX_LOADED_WAYPOINTS_POINTS = 10000;
/**
* The settings file name.
*/
public static final String SETTINGS_NAME = "SettingsActivity";
private Constants() {
}
}
@@ -20,30 +20,35 @@ import android.database.Cursor;
import android.os.Bundle;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentStatePagerAdapter;
import androidx.viewpager.widget.ViewPager;
import java.util.ArrayList;
import java.util.List;
import de.dennisguse.opentracks.content.ContentProviderUtils;
import de.dennisguse.opentracks.content.Waypoint;
import de.dennisguse.opentracks.fragments.DeleteMarkerDialogFragment.DeleteMarkerCaller;
import de.dennisguse.opentracks.fragments.MarkerDetailFragment;
import de.dennisguse.opentracks.R;
import java.util.ArrayList;
/**
* An activity to display marker detail info.
* <p>
* Allows to swipe to the next and previous marker.
*
* @author Leif Hendrik Wilden
*/
public class MarkerDetailActivity extends AbstractActivity implements DeleteMarkerCaller {
public static final String EXTRA_MARKER_ID = "marker_id";
private static final String TAG = MarkerDetailActivity.class.getSimpleName();
private ArrayList<Long> markerIds;
private List<Long> markerIds;
@Override
protected void onCreate(Bundle bundle) {
@@ -64,26 +69,40 @@ public class MarkerDetailActivity extends AbstractActivity implements DeleteMark
try (Cursor cursor = contentProviderUtils.getWaypointCursor(waypoint.getTrackId(), -1L, -1)) {
if (cursor != null && cursor.moveToFirst()) {
/*
* Yes, this will skip the first waypoint and that is intentional as the
* first waypoint holds the stats for the track.
*/
while (cursor.moveToNext()) {
Waypoint current = contentProviderUtils.createWaypoint(cursor);
for (int i = 0; i < cursor.getCount(); i++) {
Waypoint currentMarker = contentProviderUtils.createWaypoint(cursor);
markerIds.add(current.getId());
if (current.getId() == markerId) {
markerIndex = markerIds.size() - 1;
if (!currentMarker.isTripStatistics()) {
markerIds.add(currentMarker.getId());
if (currentMarker.getId() == markerId) {
markerIndex = markerIds.size() - 1;
}
}
cursor.moveToNext();
}
}
}
ViewPager viewPager = findViewById(R.id.maker_detail_activity_view_pager);
viewPager.setAdapter(new MarkerDetailPagerAdapter(getSupportFragmentManager()));
if (markerIndex != -1) {
viewPager.setCurrentItem(markerIndex);
}
final ViewPager viewPager = findViewById(R.id.maker_detail_activity_view_pager);
final MarkerDetailPagerAdapter markerAdapter = new MarkerDetailPagerAdapter(getSupportFragmentManager());
viewPager.setAdapter(markerAdapter);
viewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
}
@Override
public void onPageSelected(int position) {
setTitle(markerAdapter.getPageTitle(position));
}
@Override
public void onPageScrollStateChanged(int state) {
}
});
viewPager.setCurrentItem(markerIndex == -1 ? 0 : markerIndex);
}
@Override
@@ -108,14 +127,20 @@ public class MarkerDetailActivity extends AbstractActivity implements DeleteMark
*/
private class MarkerDetailPagerAdapter extends FragmentStatePagerAdapter {
public MarkerDetailPagerAdapter(FragmentManager fragmentManager) {
MarkerDetailPagerAdapter(FragmentManager fragmentManager) {
super(fragmentManager);
}
@Override
@NonNull
public Fragment getItem(int position) {
String title = getString(R.string.marker_title, position + 1, getCount());
return MarkerDetailFragment.newInstance(markerIds.get(position), title);
return MarkerDetailFragment.newInstance(markerIds.get(position));
}
@Nullable
@Override
public CharSequence getPageTitle(int position) {
return getString(R.string.marker_title, position + 1, getCount());
}
@Override
@@ -30,8 +30,6 @@ import de.dennisguse.opentracks.content.Waypoint;
import de.dennisguse.opentracks.content.Waypoint.WaypointType;
import de.dennisguse.opentracks.content.WaypointCreationRequest;
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
import de.dennisguse.opentracks.util.TrackRecordingServiceConnectionUtils;
import de.dennisguse.opentracks.R;
/**
* An activity to add/edit a marker.
@@ -42,6 +40,7 @@ public class MarkerEditActivity extends AbstractActivity {
public static final String EXTRA_TRACK_ID = "track_id";
public static final String EXTRA_MARKER_ID = "marker_id";
private static final String TAG = MarkerEditActivity.class.getSimpleName();
private long trackId;
private long markerId;
@@ -91,7 +90,7 @@ public class MarkerEditActivity extends AbstractActivity {
@Override
protected void onStart() {
super.onStart();
TrackRecordingServiceConnectionUtils.startConnection(this, trackRecordingServiceConnection);
trackRecordingServiceConnection.startConnection(this);
}
@Override
@@ -121,14 +120,14 @@ public class MarkerEditActivity extends AbstractActivity {
} else {
saveMarker();
}
finish();
}
});
if (newMarker) {
statisticsSection.setVisibility(View.GONE);
waypointSection.setVisibility(View.VISIBLE);
int nextWaypointNumber = trackId == -1L ? -1
: ContentProviderUtils.Factory.get(this).getNextWaypointNumber(trackId, WaypointType.WAYPOINT);
int nextWaypointNumber = trackId == -1L ? -1 : ContentProviderUtils.Factory.get(this).getNextWaypointNumber(trackId, WaypointType.WAYPOINT);
if (nextWaypointNumber == -1) {
nextWaypointNumber = 0;
}
@@ -168,8 +167,7 @@ public class MarkerEditActivity extends AbstractActivity {
waypointDescription.getText().toString(),
null,
null);
TrackRecordingServiceConnectionUtils.addMarker(
this, trackRecordingServiceConnection, waypointCreationRequest);
trackRecordingServiceConnection.addMarker(this, waypointCreationRequest);
}
/**
@@ -49,7 +49,6 @@ import de.dennisguse.opentracks.fragments.DeleteMarkerDialogFragment.DeleteMarke
import de.dennisguse.opentracks.util.IntentUtils;
import de.dennisguse.opentracks.util.ListItemUtils;
import de.dennisguse.opentracks.util.PreferencesUtils;
import de.dennisguse.opentracks.R;
/**
* Activity to show a list of markers in a track.
@@ -66,8 +65,11 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
WaypointsColumns.NAME, WaypointsColumns.DESCRIPTION, WaypointsColumns.CATEGORY,
WaypointsColumns.TYPE, WaypointsColumns.TIME, WaypointsColumns.PHOTOURL,
WaypointsColumns.LATITUDE, WaypointsColumns.LONGITUDE};
private ContentProviderUtils contentProviderUtils;
private SharedPreferences sharedPreferences;
private long recordingTrackId = PreferencesUtils.RECORDING_TRACK_ID_DEFAULT;
private boolean recordingTrackPaused = PreferencesUtils.RECORDING_TRACK_PAUSED_DEFAULT;
/*
@@ -129,7 +131,7 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
super.onCreate(savedInstanceState);
contentProviderUtils = ContentProviderUtils.Factory.get(this);
sharedPreferences = getSharedPreferences(Constants.SETTINGS_NAME, Context.MODE_PRIVATE);
sharedPreferences = PreferencesUtils.getSharedPreferences(this);
long trackId = getIntent().getLongExtra(EXTRA_TRACK_ID, -1L);
track = trackId != -1L ? contentProviderUtils.getTrack(trackId) : null;
@@ -173,7 +175,7 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
}
};
listView.setAdapter(resourceCursorAdapter);
AbstractTrackActivity.configureListViewContextualMenu(listView, contextualActionModeCallback);
AbstractListActivity.configureListViewContextualMenu(listView, contextualActionModeCallback);
LoaderManager.getInstance(this).initLoader(0, null, new LoaderCallbacks<Cursor>() {
@NonNull
@@ -232,7 +234,7 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
insertMarkerMenuItem = menu.findItem(R.id.marker_list_insert_marker);
searchMenuItem = menu.findItem(R.id.marker_list_search);
AbstractTrackActivity.configureSearchWidget(this, searchMenuItem, null);
AbstractListActivity.configureSearchWidget(this, searchMenuItem, null);
return super.onCreateOptionsMenu(menu);
}
@@ -284,10 +286,7 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
switch (itemId) {
case R.id.list_context_menu_show_on_map:
if (markerIds.length == 1) {
intent = IntentUtils.newIntent(this, TrackDetailActivity.class)
.putExtra(TrackDetailActivity.EXTRA_MARKER_ID, markerIds[0]);
//TODO Use IntentUtils.newShowOnMapIntent()
startActivity(intent);
IntentUtils.showCoordinateOnMap(this, contentProviderUtils.getWaypoint(markerIds[0]));
}
return true;
case R.id.list_context_menu_edit:
@@ -301,8 +300,7 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
if (markerIds.length > 1 && markerIds.length == listView.getCount()) {
markerIds = new long[]{-1L};
}
DeleteMarkerDialogFragment.newInstance(markerIds)
.show(getSupportFragmentManager(), DeleteMarkerDialogFragment.DELETE_MARKER_DIALOG_TAG);
DeleteMarkerDialogFragment.showDialog(getSupportFragmentManager(), markerIds);
return true;
case R.id.list_context_menu_select_all:
int size = listView.getCount();
@@ -17,12 +17,10 @@
package de.dennisguse.opentracks;
import android.app.SearchManager;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
import android.os.Bundle;
import android.provider.SearchRecentSuggestions;
import android.util.Log;
import android.view.ContextMenu;
import android.view.ContextMenu.ContextMenuInfo;
@@ -38,11 +36,17 @@ import android.widget.ListView;
import androidx.annotation.NonNull;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.SortedSet;
import de.dennisguse.opentracks.content.ContentProviderUtils;
import de.dennisguse.opentracks.content.SearchEngine;
import de.dennisguse.opentracks.content.SearchEngine.ScoredResult;
import de.dennisguse.opentracks.content.SearchEngine.SearchQuery;
import de.dennisguse.opentracks.content.SearchEngineProvider;
import de.dennisguse.opentracks.content.Track;
import de.dennisguse.opentracks.content.Waypoint;
import de.dennisguse.opentracks.content.Waypoint.WaypointType;
@@ -56,24 +60,15 @@ import de.dennisguse.opentracks.util.ListItemUtils;
import de.dennisguse.opentracks.util.PreferencesUtils;
import de.dennisguse.opentracks.util.StringUtils;
import de.dennisguse.opentracks.util.TrackIconUtils;
import de.dennisguse.opentracks.util.TrackRecordingServiceConnectionUtils;
import de.dennisguse.opentracks.R;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.SortedSet;
/**
* An activity to display a list of search results.
* An activity to display a list of searchable results.
*
* @author Rodrigo Damazio
* <p>
* TODO: allow to refine search (present search in context menu)
* TODO: allow to refine searchable (present searchable in context menu)
*/
public class SearchListActivity extends AbstractTrackActivity implements DeleteMarkerCaller, ConfirmDeleteDialogFragment.ConfirmDeleteCaller {
public class SearchListActivity extends AbstractListActivity implements DeleteMarkerCaller, ConfirmDeleteDialogFragment.ConfirmDeleteCaller {
private static final String TAG = SearchListActivity.class.getSimpleName();
@@ -91,27 +86,30 @@ public class SearchListActivity extends AbstractTrackActivity implements DeleteM
private static final String PHOTO_URL_FIELD = "photoUrl";
private static final String MARKER_LATITUDE_FIELD = "latitude";
private static final String MARKER_LONGITUDE_FIELD = "longitude";
private static final String TRACK_ID_FIELD = "trackId";
private static final String MARKER_ID_FIELD = "markerId";
private ContentProviderUtils contentProviderUtils;
private SharedPreferences sharedPreferences;
private TrackRecordingServiceConnection trackRecordingServiceConnection;
private SearchEngine searchEngine;
private SearchRecentSuggestions searchRecentSuggestions;
private ArrayAdapter<Map<String, Object>> arrayAdapter;
private boolean metricUnits = true;
private long recordingTrackId = PreferencesUtils.RECORDING_TRACK_ID_DEFAULT;
private boolean recordingTrackPaused = PreferencesUtils.RECORDING_TRACK_PAUSED_DEFAULT;
// Callback when an item is selected in the contextual action mode
private ContextualActionModeCallback contextualActionModeCallback = new ContextualActionModeCallback() {
@Override
public boolean onClick(int itemId, int[] positions, long[] ids) {
return handleContextItem(itemId, positions);
}
@Override
public void onPrepare(Menu menu, int[] positions, long[] ids, boolean showSelectAll) {
boolean isRecording = recordingTrackId != PreferencesUtils.RECORDING_TRACK_ID_DEFAULT;
boolean isRecording = PreferencesUtils.isRecording(recordingTrackId);
boolean isSingleSelection = positions.length == 1;
boolean isSingleSelectionTrack;
if (isSingleSelection) {
@@ -136,10 +134,14 @@ public class SearchListActivity extends AbstractTrackActivity implements DeleteM
// Disable select all, no action is available for multiple selection
menu.findItem(R.id.list_context_menu_select_all).setVisible(false);
}
@Override
public boolean onClick(int itemId, int[] positions, long[] ids) {
return handleContextItem(itemId, positions);
}
};
private boolean recordingTrackPaused = PreferencesUtils.RECORDING_TRACK_PAUSED_DEFAULT;
private final OnSharedPreferenceChangeListener
sharedPreferenceChangeListener = new OnSharedPreferenceChangeListener() {
private final OnSharedPreferenceChangeListener sharedPreferenceChangeListener = new OnSharedPreferenceChangeListener() {
@Override
public void onSharedPreferenceChanged(SharedPreferences preferences, String key) {
if (key == null || key.equals(PreferencesUtils.getKey(SearchListActivity.this, R.string.stats_units_key))) {
@@ -168,21 +170,19 @@ public class SearchListActivity extends AbstractTrackActivity implements DeleteM
setDefaultKeyMode(DEFAULT_KEYS_SEARCH_LOCAL);
contentProviderUtils = ContentProviderUtils.Factory.get(this);
sharedPreferences = getSharedPreferences(Constants.SETTINGS_NAME, Context.MODE_PRIVATE);
sharedPreferences = PreferencesUtils.getSharedPreferences(this);
trackRecordingServiceConnection = new TrackRecordingServiceConnection(this, null);
searchEngine = new SearchEngine(contentProviderUtils);
searchRecentSuggestions = SearchEngineProvider.newHelper(this);
arrayAdapter = new ArrayAdapter<Map<String, Object>>(this, R.layout.list_item, R.id.list_item_name) {
@NonNull
@Override
public View getView(int position, View convertView, @NonNull android.view.ViewGroup parent) {
View view;
View view = convertView;
if (convertView == null) {
view = getLayoutInflater().inflate(R.layout.list_item, parent, false);
} else {
view = convertView;
}
Map<String, Object> resultMap = getItem(position);
boolean isRecording = (boolean) resultMap.get(IS_RECORDING_FIELD);
boolean isPaused = (boolean) resultMap.get(IS_PAUSED_FIELD);
@@ -213,18 +213,16 @@ public class SearchListActivity extends AbstractTrackActivity implements DeleteM
Map<String, Object> item = arrayAdapter.getItem(position);
Long trackId = (Long) item.get(TRACK_ID_FIELD);
Long markerId = (Long) item.get(MARKER_ID_FIELD);
Intent intent;
Intent intent = IntentUtils.newIntent(SearchListActivity.this, TrackDetailActivity.class);
if (markerId != null) {
intent = IntentUtils.newIntent(SearchListActivity.this, TrackDetailActivity.class)
.putExtra(TrackDetailActivity.EXTRA_MARKER_ID, markerId);
intent = intent.putExtra(TrackDetailActivity.EXTRA_MARKER_ID, markerId);
} else {
intent = IntentUtils.newIntent(SearchListActivity.this, TrackDetailActivity.class)
.putExtra(TrackDetailActivity.EXTRA_TRACK_ID, trackId);
intent = intent.putExtra(TrackDetailActivity.EXTRA_TRACK_ID, trackId);
}
startActivity(intent);
}
});
AbstractTrackActivity.configureListViewContextualMenu(listView, contextualActionModeCallback);
AbstractListActivity.configureListViewContextualMenu(listView, contextualActionModeCallback);
handleIntent(getIntent());
}
@@ -233,7 +231,7 @@ public class SearchListActivity extends AbstractTrackActivity implements DeleteM
super.onStart();
sharedPreferences.registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
sharedPreferenceChangeListener.onSharedPreferenceChanged(null, null);
TrackRecordingServiceConnectionUtils.startConnection(this, trackRecordingServiceConnection);
trackRecordingServiceConnection.startConnection(this);
}
@Override
@@ -256,25 +254,11 @@ public class SearchListActivity extends AbstractTrackActivity implements DeleteM
@Override
public void onNewIntent(Intent intent) {
super.onNewIntent(intent);
setIntent(intent);
handleIntent(intent);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.search_list, menu);
menu.findItem(R.id.search_list_search).setVisible(false);
return super.onCreateOptionsMenu(menu);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == R.id.search_list_search) {
return false;
}
return super.onOptionsItemSelected(item);
}
@Override
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
super.onCreateContextMenu(menu, v, menuInfo);
@@ -322,8 +306,7 @@ public class SearchListActivity extends AbstractTrackActivity implements DeleteM
Log.e(TAG, "Not implemented");
return true;
case R.id.list_context_menu_show_on_map:
intent = IntentUtils.newShowOnMapIntent((double) item.get(MARKER_LATITUDE_FIELD), (double) item.get(MARKER_LONGITUDE_FIELD), item.get(NAME_FIELD) + "");
startActivity(intent);
IntentUtils.showCoordinateOnMap(this, (double) item.get(MARKER_LATITUDE_FIELD), (double) item.get(MARKER_LONGITUDE_FIELD), item.get(NAME_FIELD) + "");
return true;
case R.id.list_context_menu_edit:
if (markerId != null) {
@@ -335,12 +318,12 @@ public class SearchListActivity extends AbstractTrackActivity implements DeleteM
}
startActivity(intent);
// Close the search result since its content can change after edit.
// Close the searchable result since its content can change after edit.
finish();
return true;
case R.id.list_context_menu_delete:
if (markerId != null) {
DeleteMarkerDialogFragment.newInstance(new long[]{markerId}).show(getSupportFragmentManager(), DeleteMarkerDialogFragment.DELETE_MARKER_DIALOG_TAG);
DeleteMarkerDialogFragment.showDialog(getSupportFragmentManager(), new long[]{markerId});
} else {
deleteTracks(new long[]{trackId});
}
@@ -369,7 +352,7 @@ public class SearchListActivity extends AbstractTrackActivity implements DeleteM
}
/**
* Do the search.
* Do the searchable.
*
* @param textQuery the query
*/
@@ -386,9 +369,6 @@ public class SearchListActivity extends AbstractTrackActivity implements DeleteM
arrayAdapter.addAll(displayResults);
}
});
// Save the query as a suggestion for the future
searchRecentSuggestions.saveRecentQuery(query.textQuery, null);
}
/**
@@ -401,13 +381,11 @@ public class SearchListActivity extends AbstractTrackActivity implements DeleteM
//TODO Replace use of map<string, object>, but rather provide Track or Waypoint directly.
ArrayList<Map<String, Object>> output = new ArrayList<>(scoredResults.size());
for (ScoredResult result : scoredResults) {
Map<String, Object> resultMap = new HashMap<>();
if (result.track != null) {
prepareTrackForDisplay(result.track, resultMap);
output.add(prepareTrackForDisplay(result.track));
} else {
prepareMarkerForDisplay(result.waypoint, resultMap);
output.add(prepareMarkerForDisplay(result.waypoint));
}
output.add(resultMap);
}
return output;
}
@@ -415,15 +393,13 @@ public class SearchListActivity extends AbstractTrackActivity implements DeleteM
/**
* Prepares a marker for display by filling in a result map.
*
* @param waypoint the marker
* @param resultMap the result map
* @param waypoint the marker
* @return the result map
*/
@Deprecated
private void prepareMarkerForDisplay(Waypoint waypoint, Map<String, Object> resultMap) {
/*
* TODO: It may be more appropriate to obtain the track name as a join in
* the retrieval phase of the search.
*/
private Map<String, Object> prepareMarkerForDisplay(Waypoint waypoint) {
Map<String, Object> resultMap = new HashMap<>();
//TODO: It may be more appropriate to obtain the track name as a join in the retrieval phase of the searchable.
String trackName = null;
long trackId = waypoint.getTrackId();
if (trackId != -1L) {
@@ -453,17 +429,20 @@ public class SearchListActivity extends AbstractTrackActivity implements DeleteM
resultMap.put(MARKER_LATITUDE_FIELD, waypoint.getLocation().getLatitude());
resultMap.put(MARKER_LONGITUDE_FIELD, waypoint.getLocation().getLongitude());
return resultMap;
}
/**
* Prepares a track for display by filling in a result map.
*
* @param track the track
* @param resultMap the result map
* @param track the track
* @return the result map
*/
@Deprecated
private void prepareTrackForDisplay(Track track, Map<String, Object> resultMap) {
TripStatistics tripStatitics = track.getTripStatistics();
private Map<String, Object> prepareTrackForDisplay(Track track) {
Map<String, Object> resultMap = new HashMap<>();
TripStatistics tripStatistics = track.getTripStatistics();
String icon = track.getIcon();
String category = icon != null && !icon.equals("") ? null : track.getCategory();
@@ -472,15 +451,17 @@ public class SearchListActivity extends AbstractTrackActivity implements DeleteM
resultMap.put(ICON_ID_FIELD, TrackIconUtils.getIconDrawable(icon));
resultMap.put(ICON_CONTENT_DESCRIPTION_ID_FIELD, R.string.image_track);
resultMap.put(NAME_FIELD, track.getName());
resultMap.put(TOTAL_TIME_FIELD, StringUtils.formatElapsedTime(tripStatitics.getTotalTime()));
resultMap.put(TOTAL_DISTANCE_FIELD, StringUtils.formatDistance(this, tripStatitics.getTotalDistance(), metricUnits));
resultMap.put(TOTAL_TIME_FIELD, StringUtils.formatElapsedTime(tripStatistics.getTotalTime()));
resultMap.put(TOTAL_DISTANCE_FIELD, StringUtils.formatDistance(this, tripStatistics.getTotalDistance(), metricUnits));
resultMap.put(MARKER_COUNT_FIELD, contentProviderUtils.getWaypointCount(track.getId()));
resultMap.put(START_TIME_FIELD, tripStatitics.getStartTime());
resultMap.put(START_TIME_FIELD, tripStatistics.getStartTime());
resultMap.put(CATEGORY_FIELD, category);
resultMap.put(DESCRIPTION_FIELD, track.getDescription());
resultMap.put(PHOTO_URL_FIELD, null);
resultMap.put(TRACK_ID_FIELD, track.getId());
resultMap.put(MARKER_ID_FIELD, null);
return resultMap;
}
@Override
@@ -0,0 +1,43 @@
package de.dennisguse.opentracks;
import android.app.Application;
import android.os.StrictMode;
import android.util.Log;
import de.dennisguse.opentracks.util.PreferencesUtils;
/**
* Code that is executed when the application starts.
* <p>
* NOTE: How often actual application startup happens depends on the OS.
* Not every start of an activity will trigger this.
*/
public class Startup extends Application {
private final static String TAG = Startup.class.getSimpleName();
@Override
public void onCreate() {
super.onCreate();
//Include version information into stack traces.
Log.i(TAG, BuildConfig.APPLICATION_ID + "; BuildType: " + BuildConfig.BUILD_TYPE + "; VersionName: " + BuildConfig.VERSION_NAME + "/" + BuildConfig.VERSION_NAME_FULL + " VersionCode: " + BuildConfig.VERSION_CODE);
//Set default values of preferences on first start.
PreferencesUtils.resetPreferences(this, false);
//In debug builds: show thread and VM warnings.
if (BuildConfig.DEBUG) {
Log.d(TAG, "Enabling strict mode");
StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
.detectAll()
.penaltyLog()
.build());
StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()
.detectAll()
.penaltyLog()
.build());
}
}
}
@@ -97,9 +97,8 @@ public class TabsAdapter extends FragmentPagerAdapter implements TabHost.OnTabCh
@Override
public void onPageSelected(int position) {
/*
* Unfortunately when TabHost changes the current tab, it kindly also takes
* care of putting focus on it when not in touch mode. The jerk. This hack
* tries to prevent this from pulling focus out of our ViewPager.
* Unfortunately when TabHost changes the current tab, it kindly also takes care of putting focus on it when not in touch mode.
* The jerk. This hack tries to prevent this from pulling focus out of our ViewPager.
*/
TabWidget tabWidget = tabHost.getTabWidget();
int oldFocusability = tabWidget.getDescendantFocusability();
@@ -122,15 +121,14 @@ public class TabsAdapter extends FragmentPagerAdapter implements TabHost.OnTabCh
private final Class<?> clss;
private final Bundle bundle;
public TabInfo(Class<?> clss, Bundle bunlde) {
TabInfo(Class<?> clss, Bundle bundle) {
this.clss = clss;
this.bundle = bunlde;
this.bundle = bundle;
}
}
/**
* A dummy {@link TabContentFactory} that creates an empty view to satisfy the
* {@link TabHost} API.
* A dummy {@link TabContentFactory} that creates an empty view to satisfy the {@link TabHost} API.
*
* @author Jimmy Shih
*/
@@ -138,7 +136,7 @@ public class TabsAdapter extends FragmentPagerAdapter implements TabHost.OnTabCh
private final Context context;
public DummyTabFactory(Context context) {
DummyTabFactory(Context context) {
this.context = context;
}
@@ -27,7 +27,6 @@ import de.dennisguse.opentracks.services.ITrackRecordingService;
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
import de.dennisguse.opentracks.util.StringUtils;
import de.dennisguse.opentracks.util.UnitConversions;
import de.dennisguse.opentracks.R;
/**
* Track controller for record, pause, resume, and stop.
@@ -67,7 +66,7 @@ public class TrackController {
}
};
public TrackController(Activity activity, TrackRecordingServiceConnection trackRecordingServiceConnection, boolean alwaysShow, OnClickListener recordListener, OnClickListener stopListener) {
TrackController(Activity activity, TrackRecordingServiceConnection trackRecordingServiceConnection, boolean alwaysShow, OnClickListener recordListener, OnClickListener stopListener) {
this.activity = activity;
this.trackRecordingServiceConnection = trackRecordingServiceConnection;
this.alwaysShow = alwaysShow;
@@ -121,21 +120,21 @@ public class TrackController {
}
}
public void onResume(boolean recording, boolean paused) {
void onResume(boolean recording, boolean paused) {
isResumed = true;
update(recording, paused);
}
public void onPause() {
void onPause() {
isResumed = false;
handlerUpdateTotalTime.removeCallbacks(updateTotalTimeRunnable);
}
public void hide() {
void hide() {
containerView.setVisibility(View.GONE);
}
public void show() {
void show() {
containerView.setVisibility(View.VISIBLE);
}
@@ -17,19 +17,20 @@
package de.dennisguse.opentracks;
import android.Manifest;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.provider.MediaStore;
import android.util.Log;
import android.util.Pair;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.WindowManager;
import android.widget.ShareActionProvider;
import android.widget.TabHost;
import android.widget.TabHost.TabSpec;
@@ -37,8 +38,6 @@ import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.core.app.ActivityCompat;
import androidx.core.app.TaskStackBuilder;
import androidx.core.content.FileProvider;
import androidx.viewpager.widget.ViewPager;
import de.dennisguse.opentracks.content.ContentProviderUtils;
@@ -49,39 +48,33 @@ import de.dennisguse.opentracks.content.Waypoint.WaypointType;
import de.dennisguse.opentracks.content.WaypointCreationRequest;
import de.dennisguse.opentracks.fragments.ChartFragment;
import de.dennisguse.opentracks.fragments.ChooseActivityTypeDialogFragment;
import de.dennisguse.opentracks.fragments.ChooseActivityTypeDialogFragment.ChooseActivityTypeCaller;
import de.dennisguse.opentracks.fragments.ConfirmDeleteDialogFragment;
import de.dennisguse.opentracks.fragments.FrequencyDialogFragment;
import de.dennisguse.opentracks.fragments.StatsFragment;
import de.dennisguse.opentracks.io.file.TrackFileFormat;
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
import de.dennisguse.opentracks.settings.SettingsActivity;
import de.dennisguse.opentracks.util.FileUtils;
import de.dennisguse.opentracks.util.IntentUtils;
import de.dennisguse.opentracks.util.PreferencesUtils;
import de.dennisguse.opentracks.util.TrackIconUtils;
import de.dennisguse.opentracks.util.TrackRecordingServiceConnectionUtils;
import de.dennisguse.opentracks.util.TrackUtils;
import java.io.File;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
* An activity to show the track detail.
*
* @author Leif Hendrik Wilden
* @author Rodrigo Damazio
*/
public class TrackDetailActivity extends AbstractTrackActivity implements ChooseActivityTypeCaller, ConfirmDeleteDialogFragment.ConfirmDeleteCaller {
public class TrackDetailActivity extends AbstractListActivity implements ChooseActivityTypeDialogFragment.ChooseActivityTypeCaller, ConfirmDeleteDialogFragment.ConfirmDeleteCaller {
public static final String EXTRA_TRACK_ID = "track_id";
public static final String EXTRA_MARKER_ID = "marker_id";
private static final String TAG = TrackDetailActivity.class.getSimpleName();
private static final String CURRENT_TAB_TAG_KEY = "current_tab_tag_key";
private static final String PHOTO_URI_KEY = "photo_uri_key";
private static final String HAS_PHOTO_KEY = "has_photo_key";
private static final String JPEG_EXTENSION = "jpeg";
private static final int CAMERA_REQUEST_CODE = 5;
private static final int EXTERNAL_STORAGE_PERMISSION_REQUEST_CODE = 6;
@@ -103,11 +96,12 @@ public class TrackDetailActivity extends AbstractTrackActivity implements Choose
// Preferences
private long recordingTrackId = PreferencesUtils.RECORDING_TRACK_ID_DEFAULT;
private boolean recordingTrackPaused = PreferencesUtils.RECORDING_TRACK_PAUSED_DEFAULT;
private boolean showOnLockScreen = PreferencesUtils.SHOW_TRACKDETAIL_WHILE_RECORDING_ON_LOCKSCREEN;
private final Runnable bindChangedCallback = new Runnable() {
@Override
public void run() {
// After binding changes (is available), update the total time in
// trackController.
// After binding changes (is available), update the total time in trackController.
runOnUiThread(new Runnable() {
@Override
public void run() {
@@ -115,8 +109,9 @@ public class TrackDetailActivity extends AbstractTrackActivity implements Choose
if (hasPhoto && photoUri != null) {
hasPhoto = false;
WaypointCreationRequest waypointCreationRequest = new WaypointCreationRequest(WaypointType.WAYPOINT, false, null, null, null, null, photoUri.toString());
long markerId = TrackRecordingServiceConnectionUtils.addMarker(TrackDetailActivity.this, trackRecordingServiceConnection, waypointCreationRequest);
long markerId = trackRecordingServiceConnection.addMarker(TrackDetailActivity.this, waypointCreationRequest);
if (markerId != -1L) {
//TODO: Make configurable.
FileUtils.updateMediaScanner(TrackDetailActivity.this, photoUri);
}
}
@@ -140,6 +135,11 @@ public class TrackDetailActivity extends AbstractTrackActivity implements Choose
recordingTrackPaused = PreferencesUtils.getBoolean(TrackDetailActivity.this, R.string.recording_track_paused_key, PreferencesUtils.RECORDING_TRACK_PAUSED_DEFAULT);
}
if (key == null || key.equals(PreferencesUtils.getKey(TrackDetailActivity.this, R.string.trackdetail_show_on_lockscreen_while_recording_key))) {
showOnLockScreen = PreferencesUtils.getBoolean(TrackDetailActivity.this, R.string.trackdetail_show_on_lockscreen_while_recording_key, PreferencesUtils.SHOW_TRACKDETAIL_WHILE_RECORDING_ON_LOCKSCREEN);
setLockscreenPolicy();
}
if (key == null) return;
runOnUiThread(new Runnable() {
@@ -152,24 +152,26 @@ public class TrackDetailActivity extends AbstractTrackActivity implements Choose
});
}
};
private MenuItem insertMarkerMenuItem;
private MenuItem insertPhotoMenuItem;
private MenuItem markerListMenuItem;
private MenuItem shareMenuItem;
private MenuItem voiceFrequencyMenuItem;
private MenuItem splitFrequencyMenuItem;
private final OnClickListener recordListener = new OnClickListener() {
@Override
public void onClick(View v) {
if (recordingTrackPaused) {
// Paused -> Resume
updateMenuItems(false);
TrackRecordingServiceConnectionUtils.resumeTrack(trackRecordingServiceConnection);
trackRecordingServiceConnection.resumeTrack();
trackController.update(true, false);
} else {
// Recording -> Paused
updateMenuItems(true);
TrackRecordingServiceConnectionUtils.pauseTrack(trackRecordingServiceConnection);
trackRecordingServiceConnection.pauseTrack();
trackController.update(true, true);
}
}
@@ -178,7 +180,7 @@ public class TrackDetailActivity extends AbstractTrackActivity implements Choose
private final OnClickListener stopListener = new OnClickListener() {
@Override
public void onClick(View v) {
TrackRecordingServiceConnectionUtils.stopRecording(TrackDetailActivity.this, trackRecordingServiceConnection, true);
trackRecordingServiceConnection.stopRecording(TrackDetailActivity.this, true);
updateMenuItems(true);
}
};
@@ -193,21 +195,21 @@ public class TrackDetailActivity extends AbstractTrackActivity implements Choose
contentProviderUtils = ContentProviderUtils.Factory.get(this);
handleIntent(getIntent());
sharedPreferences = getSharedPreferences(Constants.SETTINGS_NAME, Context.MODE_PRIVATE);
sharedPreferences = PreferencesUtils.getSharedPreferences(this);
trackRecordingServiceConnection = new TrackRecordingServiceConnection(this, bindChangedCallback);
trackDataHub = TrackDataHub.newInstance(this);
tabHost = findViewById(android.R.id.tabhost);
tabHost = findViewById(R.id.tackdetail_tabhost);
tabHost.setup();
ViewPager viewPager = findViewById(R.id.pager);
TabsAdapter tabsAdapter = new TabsAdapter(this, tabHost, viewPager);
TabSpec statsTabSpec = tabHost.newTabSpec(StatsFragment.STATS_FRAGMENT_TAG).setIndicator(getString(R.string.track_detail_stats_tab), getResources().getDrawable(R.drawable.ic_tab_stats));
TabSpec statsTabSpec = tabHost.newTabSpec(StatsFragment.STATS_FRAGMENT_TAG).setIndicator(getString(R.string.track_detail_stats_tab));
tabsAdapter.addTab(statsTabSpec, StatsFragment.class, null);
TabSpec chartTabSpec = tabHost.newTabSpec(ChartFragment.CHART_FRAGMENT_TAG).setIndicator(getString(R.string.track_detail_chart_tab), getResources().getDrawable(R.drawable.ic_tab_chart));
TabSpec chartTabSpec = tabHost.newTabSpec(ChartFragment.CHART_FRAGMENT_TAG).setIndicator(getString(R.string.track_detail_chart_tab));
tabsAdapter.addTab(chartTabSpec, ChartFragment.class, null);
if (savedInstanceState != null) {
@@ -222,6 +224,22 @@ public class TrackDetailActivity extends AbstractTrackActivity implements Choose
trackController = new TrackController(this, trackRecordingServiceConnection, false, recordListener, stopListener);
}
@Override
public void onAttachedToWindow() {
setLockscreenPolicy();
super.onAttachedToWindow();
}
private void setLockscreenPolicy() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
setShowWhenLocked(showOnLockScreen);
} else {
if (showOnLockScreen)
getWindow().addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED);
else getWindow().clearFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED);
}
}
@Override
protected void onStart() {
super.onStart();
@@ -229,7 +247,7 @@ public class TrackDetailActivity extends AbstractTrackActivity implements Choose
sharedPreferences.registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
sharedPreferenceChangeListener.onSharedPreferenceChanged(null, null);
TrackRecordingServiceConnectionUtils.startConnection(this, trackRecordingServiceConnection);
trackRecordingServiceConnection.startConnection(this);
trackDataHub.start();
}
@@ -275,14 +293,6 @@ public class TrackDetailActivity extends AbstractTrackActivity implements Choose
return;
}
hasPhoto = resultCode == RESULT_OK;
if (hasPhoto) {
//Register photo in media scanner
Intent mediaScanIntent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
mediaScanIntent.setData(photoUri);
this.sendBroadcast(mediaScanIntent);
return;
}
}
super.onActivityResult(requestCode, resultCode, data);
}
@@ -292,22 +302,9 @@ public class TrackDetailActivity extends AbstractTrackActivity implements Choose
return R.layout.track_detail;
}
@Override
protected void onHomeSelected() {
/*
* TODO: Investigate
* According to
* http://developer.android.com/training/implementing-navigation
* /ancestral.html, we should use NavUtils.shouldUpRecreateTask instead of
* always creating a new back stack. However, NavUtils.shouldUpRecreateTask
* seems to always return false.
*/
TaskStackBuilder.create(this).addParentStack(TrackDetailActivity.class).startActivities();
finish();
}
@Override
public void onNewIntent(Intent intent) {
super.onNewIntent(intent);
setIntent(intent);
handleIntent(intent);
}
@@ -322,8 +319,7 @@ public class TrackDetailActivity extends AbstractTrackActivity implements Choose
shareMenuItem = menu.findItem(R.id.track_detail_share);
ShareActionProvider shareActionProvider = (ShareActionProvider) shareMenuItem.getActionProvider();
//TODO: Share the actual track when track is finished? How to get the file path or create a new file?
shareActionProvider.setShareIntent(IntentUtils.newShareFileIntent(this, trackId, "", TrackFileFormat.KML));
shareActionProvider.setShareIntent(IntentUtils.newShareFileIntent(this, new long[]{trackId}));
voiceFrequencyMenuItem = menu.findItem(R.id.track_detail_voice_frequency);
splitFrequencyMenuItem = menu.findItem(R.id.track_detail_split_frequency);
@@ -353,7 +349,10 @@ public class TrackDetailActivity extends AbstractTrackActivity implements Choose
ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, EXTERNAL_STORAGE_PERMISSION_REQUEST_CODE);
return false;
}
takePicture();
createWaypointWithPicture();
return true;
case R.id.track_detail_menu_show_on_map:
IntentUtils.showTrackOnMap(this, new long[]{trackId});
return true;
case R.id.track_detail_markers:
intent = IntentUtils.newIntent(this, MarkerListActivity.class)
@@ -361,12 +360,10 @@ public class TrackDetailActivity extends AbstractTrackActivity implements Choose
startActivity(intent);
return true;
case R.id.track_detail_voice_frequency:
FrequencyDialogFragment.newInstance(R.string.voice_frequency_key, PreferencesUtils.VOICE_FREQUENCY_DEFAULT, R.string.menu_voice_frequency)
.show(getSupportFragmentManager(), FrequencyDialogFragment.FREQUENCY_DIALOG_TAG);
FrequencyDialogFragment.showDialog(getSupportFragmentManager(), R.string.voice_frequency_key, PreferencesUtils.VOICE_FREQUENCY_DEFAULT, R.string.menu_voice_frequency);
return true;
case R.id.track_detail_split_frequency:
FrequencyDialogFragment.newInstance(R.string.split_frequency_key, PreferencesUtils.SPLIT_FREQUENCY_DEFAULT, R.string.menu_split_frequency)
.show(getSupportFragmentManager(), FrequencyDialogFragment.FREQUENCY_DIALOG_TAG);
FrequencyDialogFragment.showDialog(getSupportFragmentManager(), R.string.split_frequency_key, PreferencesUtils.SPLIT_FREQUENCY_DEFAULT, R.string.menu_split_frequency);
return true;
case R.id.track_detail_edit:
intent = IntentUtils.newIntent(this, TrackEditActivity.class)
@@ -385,29 +382,13 @@ public class TrackDetailActivity extends AbstractTrackActivity implements Choose
}
}
private void takePicture() {
File dir = FileUtils.getPhotoDir(trackId);
FileUtils.ensureDirectoryExists(dir);
String fileName = SimpleDateFormat.getDateTimeInstance().format(new Date());
File file = new File(dir, FileUtils.buildUniqueFileName(dir, fileName, JPEG_EXTENSION));
if (file != null) {
photoUri = FileProvider.getUriForFile(this, FileUtils.FILEPROVIDER, file);
Log.d(TAG, "Taking photo to URI: " + photoUri);
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE)
.putExtra(MediaStore.EXTRA_OUTPUT, photoUri);
startActivityForResult(intent, CAMERA_REQUEST_CODE);
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
if (requestCode == EXTERNAL_STORAGE_PERMISSION_REQUEST_CODE) {
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) {
Toast.makeText(this, R.string.external_storage_not_writable, Toast.LENGTH_LONG).show();
} else {
takePicture();
createWaypointWithPicture();
}
return;
}
@@ -496,8 +477,14 @@ public class TrackDetailActivity extends AbstractTrackActivity implements Choose
setTitle(title);
}
private void createWaypointWithPicture() {
Pair<Intent, Uri> intentAndPhotoUri = IntentUtils.createTakePictureIntent(this, trackId);
photoUri = intentAndPhotoUri.second;
startActivityForResult(intentAndPhotoUri.first, CAMERA_REQUEST_CODE);
}
public void chooseActivityType(String category) {
ChooseActivityTypeDialogFragment.newInstance(category).show(getSupportFragmentManager(), ChooseActivityTypeDialogFragment.CHOOSE_ACTIVITY_TYPE_DIALOG_TAG);
ChooseActivityTypeDialogFragment.showDialog(getSupportFragmentManager(), category);
}
@Override
@@ -28,28 +28,27 @@ import android.widget.Button;
import android.widget.EditText;
import android.widget.Spinner;
import androidx.annotation.NonNull;
import de.dennisguse.opentracks.content.ContentProviderUtils;
import de.dennisguse.opentracks.content.Track;
import de.dennisguse.opentracks.fragments.ChooseActivityTypeDialogFragment;
import de.dennisguse.opentracks.fragments.ChooseActivityTypeDialogFragment.ChooseActivityTypeCaller;
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
import de.dennisguse.opentracks.util.TrackIconUtils;
import de.dennisguse.opentracks.util.TrackRecordingServiceConnectionUtils;
import de.dennisguse.opentracks.util.TrackUtils;
import de.dennisguse.opentracks.R;
/**
* An activity that let's the user see and edit the user editable track meta
* data such as track name, activity type, and track description.
* An activity that let's the user see and edit the user editable track meta data such as track name, activity type, and track description.
*
* @author Leif Hendrik Wilden
*/
public class TrackEditActivity extends AbstractActivity implements ChooseActivityTypeCaller {
public class TrackEditActivity extends AbstractActivity implements ChooseActivityTypeDialogFragment.ChooseActivityTypeCaller {
public static final String EXTRA_TRACK_ID = "track_id";
public static final String EXTRA_NEW_TRACK = "new_track";
private static final String TAG = TrackEditActivity.class.getSimpleName();
private static final String ICON_VALUE_KEY = "icon_value_key";
private TrackRecordingServiceConnection trackRecordingServiceConnection;
@@ -57,7 +56,7 @@ public class TrackEditActivity extends AbstractActivity implements ChooseActivit
private Track track;
private String iconValue;
private EditText name;
private EditText nameEditText;
private AutoCompleteTextView activityType;
private Spinner activityTypeIcon;
private EditText description;
@@ -67,7 +66,7 @@ public class TrackEditActivity extends AbstractActivity implements ChooseActivit
super.onCreate(bundle);
trackRecordingServiceConnection = new TrackRecordingServiceConnection(this, null);
Long trackId = getIntent().getLongExtra(EXTRA_TRACK_ID, -1L);
long trackId = getIntent().getLongExtra(EXTRA_TRACK_ID, -1L);
if (trackId == -1L) {
Log.e(TAG, "invalid trackId");
finish();
@@ -82,14 +81,13 @@ public class TrackEditActivity extends AbstractActivity implements ChooseActivit
return;
}
name = findViewById(R.id.track_edit_name);
name.setText(track.getName());
nameEditText = findViewById(R.id.track_edit_name);
nameEditText.setText(track.getName());
activityType = findViewById(R.id.track_edit_activity_type);
activityType.setText(track.getCategory());
ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(
this, R.array.activity_types, android.R.layout.simple_dropdown_item_1line);
ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(this, R.array.activity_types, android.R.layout.simple_dropdown_item_1line);
activityType.setAdapter(adapter);
activityType.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
@@ -122,9 +120,7 @@ public class TrackEditActivity extends AbstractActivity implements ChooseActivit
@Override
public boolean onTouch(View v, MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_UP) {
ChooseActivityTypeDialogFragment.newInstance(activityType.getText().toString()).show(
getSupportFragmentManager(),
ChooseActivityTypeDialogFragment.CHOOSE_ACTIVITY_TYPE_DIALOG_TAG);
ChooseActivityTypeDialogFragment.showDialog(getSupportFragmentManager(), activityType.getText().toString());
}
return true;
}
@@ -133,9 +129,7 @@ public class TrackEditActivity extends AbstractActivity implements ChooseActivit
@Override
public boolean onKey(View v, int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) {
ChooseActivityTypeDialogFragment.newInstance(activityType.getText().toString()).show(
getSupportFragmentManager(),
ChooseActivityTypeDialogFragment.CHOOSE_ACTIVITY_TYPE_DIALOG_TAG);
ChooseActivityTypeDialogFragment.showDialog(getSupportFragmentManager(), activityType.getText().toString());
}
return true;
}
@@ -144,11 +138,11 @@ public class TrackEditActivity extends AbstractActivity implements ChooseActivit
description = findViewById(R.id.track_edit_description);
description.setText(track.getDescription());
Button save = findViewById(R.id.track_edit_save);
save.setOnClickListener(new View.OnClickListener() {
Button saveButton = findViewById(R.id.track_edit_save);
saveButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
TrackUtils.updateTrack(TrackEditActivity.this, track, name.getText().toString(),
TrackUtils.updateTrack(TrackEditActivity.this, track, nameEditText.getText().toString(),
activityType.getText().toString(), description.getText().toString(),
contentProviderUtils);
finish();
@@ -175,7 +169,7 @@ public class TrackEditActivity extends AbstractActivity implements ChooseActivit
@Override
protected void onStart() {
super.onStart();
TrackRecordingServiceConnectionUtils.startConnection(this, trackRecordingServiceConnection);
trackRecordingServiceConnection.startConnection(this);
}
@Override
@@ -185,7 +179,7 @@ public class TrackEditActivity extends AbstractActivity implements ChooseActivit
}
@Override
public void onSaveInstanceState(Bundle outState) {
public void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
outState.putString(ICON_VALUE_KEY, iconValue);
}
@@ -26,8 +26,6 @@ import android.content.pm.PackageManager;
import android.database.Cursor;
import android.location.LocationManager;
import android.os.Bundle;
import android.os.Parcelable;
import android.os.StrictMode;
import android.provider.Settings;
import android.util.Log;
import android.view.ContextMenu;
@@ -43,6 +41,7 @@ import android.widget.AdapterView.AdapterContextMenuInfo;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.ListView;
import android.widget.SearchView;
import android.widget.ShareActionProvider;
import android.widget.Toast;
import androidx.annotation.NonNull;
@@ -58,11 +57,6 @@ import java.util.Locale;
import de.dennisguse.opentracks.content.ContentProviderUtils;
import de.dennisguse.opentracks.content.TracksColumns;
import de.dennisguse.opentracks.fragments.ConfirmDeleteDialogFragment;
import de.dennisguse.opentracks.fragments.FileTypeDialogFragment;
import de.dennisguse.opentracks.fragments.FileTypeDialogFragment.FileTypeCaller;
import de.dennisguse.opentracks.io.file.TrackFileFormat;
import de.dennisguse.opentracks.io.file.exporter.SaveActivity;
import de.dennisguse.opentracks.io.file.importer.ImportActivity;
import de.dennisguse.opentracks.services.ITrackRecordingService;
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
import de.dennisguse.opentracks.settings.SettingsActivity;
@@ -71,7 +65,6 @@ import de.dennisguse.opentracks.util.ListItemUtils;
import de.dennisguse.opentracks.util.PreferencesUtils;
import de.dennisguse.opentracks.util.StringUtils;
import de.dennisguse.opentracks.util.TrackIconUtils;
import de.dennisguse.opentracks.util.TrackRecordingServiceConnectionUtils;
import de.dennisguse.opentracks.util.TrackUtils;
/**
@@ -79,12 +72,14 @@ import de.dennisguse.opentracks.util.TrackUtils;
*
* @author Leif Hendrik Wilden
*/
public class TrackListActivity extends AbstractTrackActivity implements FileTypeCaller, ConfirmDeleteDialogFragment.ConfirmDeleteCaller {
public class TrackListActivity extends AbstractListActivity implements ConfirmDeleteDialogFragment.ConfirmDeleteCaller {
private static final String TAG = TrackListActivity.class.getSimpleName();
private static final String[] PROJECTION = new String[]{TracksColumns._ID, TracksColumns.NAME,
TracksColumns.DESCRIPTION, TracksColumns.CATEGORY, TracksColumns.STARTTIME,
TracksColumns.TOTALDISTANCE, TracksColumns.TOTALTIME, TracksColumns.ICON};
// The following are set in onCreate
private ContentProviderUtils contentProviderUtils;
private SharedPreferences sharedPreferences;
@@ -92,6 +87,7 @@ public class TrackListActivity extends AbstractTrackActivity implements FileType
private TrackController trackController;
private ListView listView;
private ResourceCursorAdapter resourceCursorAdapter;
private final LoaderCallbacks<Cursor> loaderCallbacks = new LoaderCallbacks<Cursor>() {
@Override
public Loader<Cursor> onCreateLoader(int arg0, Bundle arg1) {
@@ -109,20 +105,22 @@ public class TrackListActivity extends AbstractTrackActivity implements FileType
resourceCursorAdapter.swapCursor(null);
}
};
// Preferences
private boolean metricUnits = true;
private long recordingTrackId = PreferencesUtils.RECORDING_TRACK_ID_DEFAULT;
// Callback when an item is selected in the contextual action mode
private final ContextualActionModeCallback
contextualActionModeCallback = new ContextualActionModeCallback() {
private final ContextualActionModeCallback contextualActionModeCallback = new ContextualActionModeCallback() {
@Override
public void onPrepare(Menu menu, int[] positions, long[] ids, boolean showSelectAll) {
boolean isRecording = recordingTrackId != PreferencesUtils.RECORDING_TRACK_ID_DEFAULT;
boolean isSingleSelection = ids.length == 1;
public void onPrepare(Menu menu, int[] positions, long[] trackIds, boolean showSelectAll) {
boolean isSingleSelection = trackIds.length == 1;
MenuItem shareMenuItem = menu.findItem(R.id.list_context_menu_share);
ShareActionProvider shareActionProvider = (ShareActionProvider) shareMenuItem.getActionProvider();
shareActionProvider.setShareIntent(trackIds.length == 0 ? null : IntentUtils.newShareFileIntent(TrackListActivity.this, trackIds));
menu.findItem(R.id.list_context_menu_share).setVisible(!isRecording && isSingleSelection);
menu.findItem(R.id.list_context_menu_show_on_map).setVisible(false);
menu.findItem(R.id.list_context_menu_edit).setVisible(isSingleSelection);
menu.findItem(R.id.list_context_menu_select_all).setVisible(showSelectAll);
}
@@ -132,9 +130,11 @@ public class TrackListActivity extends AbstractTrackActivity implements FileType
return handleContextItem(itemId, ids);
}
};
private boolean recordingTrackPaused = PreferencesUtils.RECORDING_TRACK_PAUSED_DEFAULT;
/*
* Note that sharedPreferenceChangeListenr cannot be an anonymous inner class.
* Note that sharedPreferenceChangeListener cannot be an anonymous inner class.
* Anonymous inner class will get garbage collected.
*/
private final OnSharedPreferenceChangeListener
@@ -148,7 +148,7 @@ public class TrackListActivity extends AbstractTrackActivity implements FileType
PreferencesUtils.getKey(TrackListActivity.this, R.string.recording_track_id_key))) {
recordingTrackId = PreferencesUtils.getLong(
TrackListActivity.this, R.string.recording_track_id_key);
if (key != null && recordingTrackId != PreferencesUtils.RECORDING_TRACK_ID_DEFAULT) {
if (key != null && PreferencesUtils.isRecording(recordingTrackId)) {
trackRecordingServiceConnection.startAndBind();
}
}
@@ -164,42 +164,43 @@ public class TrackListActivity extends AbstractTrackActivity implements FileType
public void run() {
TrackListActivity.this.invalidateOptionsMenu();
LoaderManager.getInstance(TrackListActivity.this).restartLoader(0, null, loaderCallbacks);
boolean isRecording = recordingTrackId != PreferencesUtils.RECORDING_TRACK_ID_DEFAULT;
boolean isRecording = PreferencesUtils.isRecording(recordingTrackId);
trackController.update(isRecording, recordingTrackPaused);
}
});
}
}
};
// Menu items
private MenuItem searchMenuItem;
private MenuItem startGpsMenuItem;
private MenuItem aggregatedStatisticsMenuItem;
private MenuItem exportAllMenuItem;
private MenuItem importAllMenuItem;
private MenuItem deleteAllMenuItem;
private final OnClickListener stopListener = new OnClickListener() {
@Override
public void onClick(View v) {
updateMenuItems(false, false);
TrackRecordingServiceConnectionUtils.stopRecording(
TrackListActivity.this, trackRecordingServiceConnection, true);
trackRecordingServiceConnection.stopRecording(TrackListActivity.this, true);
}
};
private boolean startGps = false; // true to start gps
private boolean startNewRecording = false; // true to start a new recording
// Callback when the trackRecordingServiceConnection binding changes.
private final Runnable bindChangedCallback = new Runnable() {
@Override
public void run() {
/*
* After binding changes (e.g., becomes available), update the total time
* in trackController.
* After binding changes (e.g., becomes available), update the total time in trackController.
*/
runOnUiThread(new Runnable() {
@Override
public void run() {
trackController.update(recordingTrackId != PreferencesUtils.RECORDING_TRACK_ID_DEFAULT, recordingTrackPaused);
trackController.update(PreferencesUtils.isRecording(recordingTrackId), recordingTrackPaused);
}
});
@@ -220,8 +221,7 @@ public class TrackListActivity extends AbstractTrackActivity implements FileType
Intent intent = IntentUtils.newIntent(TrackListActivity.this, TrackDetailActivity.class)
.putExtra(TrackDetailActivity.EXTRA_TRACK_ID, trackId);
startActivity(intent);
Toast.makeText(
TrackListActivity.this, R.string.track_list_record_success, Toast.LENGTH_SHORT)
Toast.makeText(TrackListActivity.this, R.string.track_list_record_success, Toast.LENGTH_SHORT)
.show();
}
if (startGps) {
@@ -230,6 +230,7 @@ public class TrackListActivity extends AbstractTrackActivity implements FileType
}
}
};
private final OnClickListener recordListener = new OnClickListener() {
public void onClick(View v) {
if (recordingTrackId == PreferencesUtils.RECORDING_TRACK_ID_DEFAULT) {
@@ -240,12 +241,12 @@ public class TrackListActivity extends AbstractTrackActivity implements FileType
if (recordingTrackPaused) {
// Paused -> Resume
updateMenuItems(false, true);
TrackRecordingServiceConnectionUtils.resumeTrack(trackRecordingServiceConnection);
trackRecordingServiceConnection.resumeTrack();
trackController.update(true, false);
} else {
// Recording -> Paused
updateMenuItems(false, true);
TrackRecordingServiceConnectionUtils.pauseTrack(trackRecordingServiceConnection);
trackRecordingServiceConnection.pauseTrack();
trackController.update(true, true);
}
}
@@ -256,33 +257,24 @@ public class TrackListActivity extends AbstractTrackActivity implements FileType
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (BuildConfig.DEBUG) {
Log.d(TAG, "Enabling strict mode");
StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
.detectAll()
.penaltyLog()
.build());
StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()
.detectAll()
.penaltyLog()
.build());
}
contentProviderUtils = ContentProviderUtils.Factory.get(this);
sharedPreferences = getSharedPreferences(Constants.SETTINGS_NAME, Context.MODE_PRIVATE);
sharedPreferences = PreferencesUtils.getSharedPreferences(this);
trackRecordingServiceConnection = new TrackRecordingServiceConnection(this, bindChangedCallback);
trackController = new TrackController(this, trackRecordingServiceConnection, true, recordListener, stopListener);
setDefaultKeyMode(DEFAULT_KEYS_SEARCH_LOCAL);
// Show trackController when search dialog is dismissed
SearchManager searchManager = (SearchManager) getSystemService(SEARCH_SERVICE);
searchManager.setOnDismissListener(new SearchManager.OnDismissListener() {
@Override
public void onDismiss() {
trackController.show();
}
});
if (searchManager != null) {
searchManager.setOnDismissListener(new SearchManager.OnDismissListener() {
@Override
public void onDismiss() {
trackController.show();
}
});
}
listView = findViewById(R.id.track_list);
listView.setEmptyView(findViewById(R.id.track_list_empty_view));
@@ -294,6 +286,7 @@ public class TrackListActivity extends AbstractTrackActivity implements FileType
startActivity(newIntent);
}
});
resourceCursorAdapter = new ResourceCursorAdapter(this, R.layout.list_item, null, 0) {
@Override
public void bindView(View view, Context context, Cursor cursor) {
@@ -324,7 +317,7 @@ public class TrackListActivity extends AbstractTrackActivity implements FileType
}
};
listView.setAdapter(resourceCursorAdapter);
AbstractTrackActivity.configureListViewContextualMenu(listView, contextualActionModeCallback);
AbstractListActivity.configureListViewContextualMenu(listView, contextualActionModeCallback);
LoaderManager.getInstance(this).initLoader(0, null, loaderCallbacks);
showStartupDialogs();
@@ -336,7 +329,7 @@ public class TrackListActivity extends AbstractTrackActivity implements FileType
sharedPreferences.registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
sharedPreferenceChangeListener.onSharedPreferenceChanged(null, null);
TrackRecordingServiceConnectionUtils.startConnection(this, trackRecordingServiceConnection);
trackRecordingServiceConnection.startConnection(this);
}
@Override
@@ -346,8 +339,7 @@ public class TrackListActivity extends AbstractTrackActivity implements FileType
// Update UI
this.invalidateOptionsMenu();
LoaderManager.getInstance(this).restartLoader(0, null, loaderCallbacks);
boolean isRecording = recordingTrackId != PreferencesUtils.RECORDING_TRACK_ID_DEFAULT;
trackController.onResume(isRecording, recordingTrackPaused);
trackController.onResume(PreferencesUtils.isRecording(recordingTrackId), recordingTrackPaused);
}
@Override
@@ -382,22 +374,15 @@ public class TrackListActivity extends AbstractTrackActivity implements FileType
return R.layout.track_list;
}
@Override
protected boolean configureActionBarHomeAsUp() {
return false;
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.track_list, menu);
searchMenuItem = menu.findItem(R.id.track_list_search);
AbstractTrackActivity.configureSearchWidget(this, searchMenuItem, trackController);
AbstractListActivity.configureSearchWidget(this, searchMenuItem, trackController);
startGpsMenuItem = menu.findItem(R.id.track_list_start_gps);
aggregatedStatisticsMenuItem = menu.findItem(R.id.track_list_aggregated_statistics);
exportAllMenuItem = menu.findItem(R.id.track_list_export_all);
importAllMenuItem = menu.findItem(R.id.track_list_import_all);
deleteAllMenuItem = menu.findItem(R.id.track_list_delete_all);
return super.onCreateOptionsMenu(menu);
@@ -405,8 +390,8 @@ public class TrackListActivity extends AbstractTrackActivity implements FileType
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
boolean isGpsStarted = TrackRecordingServiceConnectionUtils.isRecordingServiceRunning(this);
boolean isRecording = recordingTrackId != PreferencesUtils.RECORDING_TRACK_ID_DEFAULT;
boolean isGpsStarted = TrackRecordingServiceConnection.isRecordingServiceRunning(this);
boolean isRecording = PreferencesUtils.isRecording(recordingTrackId);
updateMenuItems(isGpsStarted, isRecording);
View searchView = searchMenuItem.getActionView();
@@ -422,11 +407,11 @@ public class TrackListActivity extends AbstractTrackActivity implements FileType
switch (item.getItemId()) {
case R.id.track_list_start_gps:
LocationManager locationManager = (LocationManager) this.getSystemService(Context.LOCATION_SERVICE);
if (!locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)) {
if (locationManager != null && !locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)) {
intent = new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS);
startActivity(intent);
} else {
startGps = !TrackRecordingServiceConnectionUtils.isRecordingServiceRunning(this);
startGps = !TrackRecordingServiceConnection.isRecordingServiceRunning(this);
// Show toast
Toast toast = Toast.makeText(this, startGps ? R.string.gps_starting : R.string.gps_stopping, Toast.LENGTH_SHORT);
@@ -457,16 +442,6 @@ public class TrackListActivity extends AbstractTrackActivity implements FileType
intent = IntentUtils.newIntent(this, AggregatedStatsActivity.class);
startActivity(intent);
return true;
case R.id.track_list_export_all:
FileTypeDialogFragment.newInstance(R.id.track_list_export_all,
R.string.export_all_title, R.string.export_all_option)
.show(getSupportFragmentManager(), FileTypeDialogFragment.FILE_TYPE_DIALOG_TAG);
return true;
case R.id.track_list_import_all:
FileTypeDialogFragment.newInstance(R.id.track_list_import_all,
R.string.import_selection_title, R.string.import_selection_option)
.show(getSupportFragmentManager(), FileTypeDialogFragment.FILE_TYPE_DIALOG_TAG);
return true;
case R.id.track_list_delete_all:
deleteTracks(new long[]{-1L});
return true;
@@ -484,8 +459,7 @@ public class TrackListActivity extends AbstractTrackActivity implements FileType
getMenuInflater().inflate(R.menu.list_context_menu, menu);
AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo;
contextualActionModeCallback.onPrepare(
menu, new int[]{info.position}, new long[]{info.id}, false);
contextualActionModeCallback.onPrepare(menu, new int[]{info.position}, new long[]{info.id}, false);
}
@Override
@@ -522,26 +496,6 @@ public class TrackListActivity extends AbstractTrackActivity implements FileType
// Do nothing
}
@Override
public void onFileTypeDone(int menuId, TrackFileFormat trackFileFormat) {
Intent intent;
switch (menuId) {
case R.id.track_list_export_all:
intent = IntentUtils.newIntent(this, SaveActivity.class)
.putExtra(SaveActivity.EXTRA_TRACK_IDS, new long[]{-1L})
.putExtra(SaveActivity.EXTRA_TRACK_FILE_FORMAT, (Parcelable) trackFileFormat);
startActivity(intent);
break;
case R.id.track_list_import_all:
intent = IntentUtils.newIntent(this, ImportActivity.class)
.putExtra(ImportActivity.EXTRA_IMPORT_ALL, true)
.putExtra(ImportActivity.EXTRA_TRACK_FILE_FORMAT, (Parcelable) trackFileFormat);
startActivity(intent);
break;
default:
}
}
/**
* Shows start up dialogs.
*/
@@ -573,19 +527,12 @@ public class TrackListActivity extends AbstractTrackActivity implements FileType
startGpsMenuItem.setVisible(!isRecording);
if (!isRecording) {
startGpsMenuItem.setTitle(isGpsStarted ? R.string.menu_stop_gps : R.string.menu_start_gps);
startGpsMenuItem.setIcon(isGpsStarted ? R.drawable.ic_menu_stop_gps : R.drawable.ic_menu_start_gps);
TrackIconUtils.setMenuIconColor(startGpsMenuItem);
startGpsMenuItem.setIcon(isGpsStarted ? R.drawable.ic_gps_fixed_24dp : R.drawable.ic_gps_off_24dp);
}
}
if (aggregatedStatisticsMenuItem != null) {
aggregatedStatisticsMenuItem.setVisible(hasTrack);
}
if (exportAllMenuItem != null) {
exportAllMenuItem.setVisible(hasTrack && !isRecording);
}
if (importAllMenuItem != null) {
importAllMenuItem.setVisible(!isRecording);
}
if (deleteAllMenuItem != null) {
deleteAllMenuItem.setVisible(hasTrack && !isRecording);
}
@@ -599,10 +546,9 @@ public class TrackListActivity extends AbstractTrackActivity implements FileType
trackRecordingServiceConnection.startAndBind();
/*
* If the binding has happened, then invoke the callback to start a new
* recording. If the binding hasn't happened, then invoking the callback
* will have no effect. But when the binding occurs, the callback will get
* invoked.
* If the binding has happened, then invoke the callback to start a new recording.
* If the binding hasn't happened, then invoking the callback will have no effect.
* But when the binding occurs, the callback will get invoked.
*/
bindChangedCallback.run();
}
@@ -616,9 +562,8 @@ public class TrackListActivity extends AbstractTrackActivity implements FileType
*/
private boolean handleContextItem(int itemId, long[] trackIds) {
switch (itemId) {
case R.id.list_context_menu_share:
//TODO
Log.e(TAG, "Not implemented");
case R.id.list_context_menu_show_on_map:
IntentUtils.showTrackOnMap(this, trackIds);
return true;
case R.id.list_context_menu_edit:
Intent intent = IntentUtils.newIntent(this, TrackEditActivity.class)
@@ -0,0 +1,43 @@
package de.dennisguse.opentracks.android;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.database.Cursor;
import android.net.Uri;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
/**
* Wraps a {@link ContentResolver} and provides access as {@link IContentResolver}.
*/
public class ContentResolverWrapper implements IContentResolver {
private ContentResolver contentResolver;
public ContentResolverWrapper(ContentResolver contentResolver) {
this.contentResolver = contentResolver;
}
@Nullable
public Uri insert(@NonNull Uri url, @Nullable ContentValues values) {
return contentResolver.insert(url, values);
}
public int bulkInsert(@NonNull Uri url, @NonNull ContentValues[] values) {
return contentResolver.bulkInsert(url, values);
}
@Nullable
public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) {
return contentResolver.query(uri, projection, selection, selectionArgs, sortOrder);
}
public int update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable String where, @Nullable String[] selectionArgs) {
return contentResolver.update(uri, values, where, selectionArgs);
}
public int delete(@NonNull Uri url, @Nullable String where, @Nullable String[] selectionArgs) {
return contentResolver.delete(url, where, selectionArgs);
}
}
@@ -0,0 +1,27 @@
package de.dennisguse.opentracks.android;
import android.content.ContentValues;
import android.database.Cursor;
import android.net.Uri;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresPermission;
/**
* Defines the interface actually shared by {@link android.content.ContentProvider} and {@link android.content.ContentResolver}.
* So, both can be used interchangeably.
*/
public interface IContentResolver {
@Nullable
Uri insert(@RequiresPermission.Write @NonNull Uri url, @Nullable ContentValues values);
int bulkInsert(@RequiresPermission.Write @NonNull Uri url, @NonNull ContentValues[] values);
@Nullable
Cursor query(@RequiresPermission.Read @NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder);
int update(@RequiresPermission.Write @NonNull Uri uri, @Nullable ContentValues values, @Nullable String where, @Nullable String[] selectionArgs);
int delete(@RequiresPermission.Write @NonNull Uri url, @Nullable String where, @Nullable String[] selectionArgs);
}
@@ -22,12 +22,12 @@ import android.location.Location;
import android.location.LocationManager;
import android.net.Uri;
import de.dennisguse.opentracks.BuildConfig;
import de.dennisguse.opentracks.content.Waypoint.WaypointType;
import java.util.Iterator;
import java.util.List;
import de.dennisguse.opentracks.BuildConfig;
import de.dennisguse.opentracks.content.Waypoint.WaypointType;
/**
* Utilities to access data from the app's content provider.
*
@@ -36,10 +36,20 @@ import java.util.List;
public interface ContentProviderUtils {
/**
* The authority (the first part of the URI) for the app's content
* provider.
* Maximum number of waypoints that will be loaded at one time.
*/
String AUTHORITY = BuildConfig.APPLICATION_ID;
int MAX_LOADED_WAYPOINTS_POINTS = 10000;
/**
* The authority (the first part of the URI) for the app's content provider.
*/
String AUTHORITY_PACKAGE = BuildConfig.APPLICATION_ID + ".content";
/**
* The base URI for the app's content provider.
*/
String CONTENT_BASE_URI = "content://" + AUTHORITY_PACKAGE;
/**
* The default {@link LocationFactory} which creates a location each time.
*/
@@ -51,7 +61,8 @@ public interface ContentProviderUtils {
};
/**
* Clears a track. Removes waypoints and trackpoints. Only keeps the track id.
* Clears a track: removes waypoints and trackpoints.
* Only keeps the track id.
*
* @param trackId the track id
*/
@@ -77,20 +88,19 @@ public interface ContentProviderUtils {
void deleteTrack(Context context, long trackId);
/**
* Gets all the tracks. If no track exists, an empty list is returned.
* <p>
* Note that the returned tracks do not have any track points attached.
* Gets all the tracks.
* If no track exists, an empty list is returned.
* NOTE: the returned tracks do not have any track points attached.
*/
List<Track> getAllTracks();
/**
* Gets the last track. Returns null if doesn't exist.
* Gets the last track or null.
*/
Track getLastTrack();
/**
* Gets a track by a track id. Returns null if not found.
* <p>
* Gets a track by a track id or null
* Note that the returned track doesn't have any track points attached.
*
* @param trackId the track id.
@@ -98,8 +108,8 @@ public interface ContentProviderUtils {
Track getTrack(long trackId);
/**
* Gets a track cursor. The caller owns the returned cursor and is responsible
* for closing it.
* Gets a track cursor.
* The caller owns the returned cursor and is responsible for closing it.
*
* @param selection the selection. Can be null
* @param selectionArgs the selection arguments. Can be null
@@ -109,8 +119,7 @@ public interface ContentProviderUtils {
/**
* Inserts a track.
* <p>
* Note: This doesn't insert any track points.
* NOTE: This doesn't insert any track points.
*
* @param track the track
* @return the content provider URI of the inserted track.
@@ -119,8 +128,7 @@ public interface ContentProviderUtils {
/**
* Updates a track.
* <p>
* Note: This doesn't update any track points.
* NOTE: This doesn't update any track points.
*
* @param track the track
*/
@@ -134,19 +142,19 @@ public interface ContentProviderUtils {
Waypoint createWaypoint(Cursor cursor);
/**
* Deletes a waypoint. If deleting a statistics waypoint, this will also
* correct the next statistics waypoint after the deleted one to reflect the
* deletion. The generator is used to update the next statistics waypoint.
* Deletes a waypoint.
* If deleting a statistics waypoint, this will also correct the next statistics waypoint after the deleted one to reflect the deletion.
* The generator is used to update the next statistics waypoint.
*
* @param waypointId the waypoint id
* @param descriptionGenerator the description generator. Can be null for
* waypoint marker
* @param descriptionGenerator the description generator. Can be null for waypoint marker
*/
void deleteWaypoint(Context context, long waypointId, DescriptionGenerator descriptionGenerator);
/**
* Gets the first waypoint id for a track. The first waypoint is special as it
* contains the stats for the track. Returns -1L if it doesn't exist.
* Gets the first waypoint id for a track.
* The first waypoint is special as it contains the stats for the track.
* Returns -1L if it doesn't exist.
*
* @param trackId the track id
*/
@@ -161,8 +169,8 @@ public interface ContentProviderUtils {
Waypoint getLastWaypoint(long trackId, WaypointType waypointType);
/**
* Gets the next waypoint number for a type. Returns -1 if not able to get the
* next waypoint number.
* Gets the next waypoint number for a type.
* Returns -1 if not able to get the next waypoint number.
*
* @param trackId the track id
* @param waypointType the waypoint type
@@ -170,15 +178,16 @@ public interface ContentProviderUtils {
int getNextWaypointNumber(long trackId, WaypointType waypointType);
/**
* Gets a waypoint from a waypoint id. Returns null if not found.
* Gets a waypoint from a waypoint id.
* Returns null if not found.
*
* @param waypointId the waypoint id
*/
Waypoint getWaypoint(long waypointId);
/**
* Gets a waypoint cursor. The caller owns the returned cursor and is
* responsible for closing it.
* Gets a waypoint cursor.
* he caller owns the returned cursor and is responsible for closing it.
*
* @param selection the selection. Can be null
* @param selectionArgs the selection arguments. Can be null
@@ -189,13 +198,12 @@ public interface ContentProviderUtils {
Cursor getWaypointCursor(String selection, String[] selectionArgs, String sortOrder, int maxWaypoints);
/**
* Gets a waypoint cursor for a track. The caller owns the returned cursor and
* is responsible for closing it.
* Gets a waypoint cursor for a track.
* The caller owns the returned cursor and is responsible for closing it.
*
* @param trackId the track id
* @param minWaypointId the minimum waypoint id. -1L to ignore
* @param maxWaypoints the maximum number of waypoints to return. -1 for no
* limit
* @param maxWaypoints the maximum number of waypoints to return. -1 for no limit
*/
Cursor getWaypointCursor(long trackId, long minWaypointId, int maxWaypoints);
@@ -215,7 +223,8 @@ public interface ContentProviderUtils {
Uri insertWaypoint(Waypoint waypoint);
/**
* Updates a waypoint. Returns true if successful.
* Updates a waypoint.
* Returns true if successful.
*
* @param waypoint the waypoint
*/
@@ -240,14 +249,16 @@ public interface ContentProviderUtils {
Location createTrackPoint(Cursor cursor);
/**
* Gets the first location id for a track. Returns -1L if it doesn't exist.
* Gets the first location id for a track.
* Returns -1L if it doesn't exist.
*
* @param trackId the track id
*/
long getFirstTrackPointId(long trackId);
/**
* Gets the last location id for a track. Returns -1L if it doesn't exist.
* Gets the last location id for a track.
* Returns -1L if it doesn't exist.
*
* @param trackId the track id
*/
@@ -263,57 +274,44 @@ public interface ContentProviderUtils {
long getTrackPointId(long trackId, Location location);
/**
* Gets the first valid location for a track. Returns null if it doesn't
* exist.
* Gets the first valid location for a track.
* Returns null if it doesn't exist.
*
* @param trackId the track id
*/
Location getFirstValidTrackPoint(long trackId);
/**
* Gets the last valid location for a track. Returns null if it doesn't exist.
* Gets the last valid location for a track.
* Returns null if it doesn't exist.
*
* @param trackId the track id
*/
Location getLastValidTrackPoint(long trackId);
/**
* Gets the last valid location.
*/
Location getLastValidTrackPoint();
/**
* Creates a location cursor. The caller owns the returned cursor and is
* responsible for closing it.
* Creates a location cursor. The caller owns the returned cursor and is responsible for closing it.
*
* @param trackId the track id
* @param startTrackPointId the starting track point id. -1L to ignore
* @param maxLocations maximum number of locations to return. -1 for no limit
* @param descending true to sort the result in descending order (latest
* location first)
* @param descending true to sort the result in descending order (latest location first)
*/
Cursor getTrackPointCursor(
long trackId, long startTrackPointId, int maxLocations, boolean descending);
Cursor getTrackPointCursor(long trackId, long startTrackPointId, int maxLocations, boolean descending);
/**
* Creates a new read-only iterator over a given track's points. It provides a
* lightweight way of iterating over long tracks without failing due to the
* underlying cursor limitations. Since it's a read-only iterator,
* {@link Iterator#remove()} always throws
* {@link UnsupportedOperationException}. Each call to
* {@link LocationIterator#next()} may advance to the next DB record, and if
* so, the iterator calls {@link LocationFactory#createLocation()} and
* populates it with information retrieved from the record. When done with
* iteration, {@link LocationIterator#close()} must be called.
* Creates a new read-only iterator over a given track's points.
* It provides a lightweight way of iterating over long tracks without failing due to the underlying cursor limitations.
* Since it's a read-only iterator, {@link Iterator#remove()} always throws {@link UnsupportedOperationException}.
* Each call to {@link LocationIterator#next()} may advance to the next DB record, and if so, the iterator calls {@link LocationFactory#createLocation()} and populates it with information retrieved from the record.
* When done with iteration, {@link LocationIterator#close()} must be called.
*
* @param trackId the track id
* @param startTrackPointId the starting track point id. -1L to ignore
* @param descending true to sort the result in descending order (latest
* location first)
* @param descending true to sort the result in descending order (latest location first)
* @param locationFactory the location factory
*/
LocationIterator getTrackPointLocationIterator(
long trackId, long startTrackPointId, boolean descending, LocationFactory locationFactory);
LocationIterator getTrackPointLocationIterator(long trackId, long startTrackPointId, boolean descending, LocationFactory locationFactory);
/**
* Inserts a track point.
@@ -325,10 +323,9 @@ public interface ContentProviderUtils {
Uri insertTrackPoint(Location location, long trackId);
/**
* A lightweight wrapper around the original {@link Cursor} with a method to
* clean up.
* A lightweight wrapper around the original {@link Cursor} with a method to clean up.
*/
interface LocationIterator extends Iterator<Location> {
interface LocationIterator extends Iterator<Location>, AutoCloseable {
/**
* Gets the most recently retrieved track point id by {@link #next()}.
@@ -347,53 +344,23 @@ public interface ContentProviderUtils {
interface LocationFactory {
/**
* Creates a new {@link Location}. An implementation can create new
* instances or reuse existing instances for optimization.
* Creates a new {@link Location}.
* An implementation can create new instances or reuse existing instances for optimization.
*/
Location createLocation();
}
/**
* A factory which can produce instances of {@link ContentProviderUtils}, and
* can be overridden for testing.
* A factory which can produce instances of {@link ContentProviderUtils}, and can be overridden for testing.
*/
class Factory {
private static Factory instance = new Factory();
/**
* Creates an instance of {@link ContentProviderUtils}.
*
* @param context the context
*/
public static ContentProviderUtils get(Context context) {
return instance.newForContext(context);
}
/**
* Returns the factory instance.
*/
public static Factory getInstance() {
return instance;
}
/**
* Overrides the factory instance for testing. Don't forget to set it back
* to the original value after testing.
*
* @param factory the factory
*/
public static void overrideInstance(Factory factory) {
instance = factory;
}
/**
* Creates an instance of {@link ContentProviderUtils}. Allows subclasses
* to override for testing.
*
* @param context the context
*/
protected ContentProviderUtils newForContext(Context context) {
return new ContentProviderUtilsImpl(context.getContentResolver());
}
}
@@ -24,18 +24,21 @@ import android.location.Location;
import android.net.Uri;
import android.util.Log;
import de.dennisguse.opentracks.content.Waypoint.WaypointType;
import de.dennisguse.opentracks.content.sensor.SensorDataSet;
import de.dennisguse.opentracks.stats.TripStatistics;
import de.dennisguse.opentracks.util.FileUtils;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.NoSuchElementException;
import de.dennisguse.opentracks.android.ContentResolverWrapper;
import de.dennisguse.opentracks.android.IContentResolver;
import de.dennisguse.opentracks.content.Waypoint.WaypointType;
import de.dennisguse.opentracks.content.sensor.SensorDataSet;
import de.dennisguse.opentracks.stats.TripStatistics;
import de.dennisguse.opentracks.util.FileUtils;
/**
* {@link ContentProviderUtils} implementation.
* Allows to use {@link ContentResolver} and {@link android.content.ContentProvider} interchangeably via {@link IContentResolver}.
*
* @author Leif Hendrik Wilden
*/
@@ -45,10 +48,14 @@ public class ContentProviderUtilsImpl implements ContentProviderUtils {
private static final int MAX_LATITUDE = 90000000;
private final ContentResolver contentResolver;
private final IContentResolver contentResolver;
private int defaultCursorBatchSize = 2000;
public ContentProviderUtilsImpl(ContentResolver contentResolver) {
this.contentResolver = new ContentResolverWrapper(contentResolver);
}
public ContentProviderUtilsImpl(IContentResolver contentResolver) {
this.contentResolver = contentResolver;
}
@@ -716,8 +723,7 @@ public class ContentProviderUtilsImpl implements ContentProviderUtils {
if (maxWaypoints >= 0) {
sortOrder += " LIMIT " + maxWaypoints;
}
return contentResolver.query(
WaypointsColumns.CONTENT_URI, projection, selection, selectionArgs, sortOrder);
return contentResolver.query(WaypointsColumns.CONTENT_URI, projection, selection, selectionArgs, sortOrder);
}
@Override
@@ -837,16 +843,7 @@ public class ContentProviderUtilsImpl implements ContentProviderUtils {
}
@Override
public Location getLastValidTrackPoint() {
String selection = TrackPointsColumns._ID + "=(select max(" + TrackPointsColumns._ID + ") from "
+ TrackPointsColumns.TABLE_NAME + " WHERE " + TrackPointsColumns.LATITUDE + "<="
+ MAX_LATITUDE + ")";
return findTrackPointBy(selection, null);
}
@Override
public Cursor getTrackPointCursor(
long trackId, long startTrackPointId, int maxLocations, boolean descending) {
public Cursor getTrackPointCursor(long trackId, long startTrackPointId, int maxLocations, boolean descending) {
if (trackId < 0) {
return null;
}
@@ -963,8 +960,7 @@ public class ContentProviderUtilsImpl implements ContentProviderUtils {
@Override
public Uri insertTrackPoint(Location location, long trackId) {
return contentResolver.insert(
TrackPointsColumns.CONTENT_URI, createContentValues(location, trackId));
return contentResolver.insert(TrackPointsColumns.CONTENT_URI, createContentValues(location, trackId));
}
/**
@@ -1074,10 +1070,8 @@ public class ContentProviderUtilsImpl implements ContentProviderUtils {
* @param selectionArgs the selection arguments
* @param sortOrder the sort order
*/
private Cursor getTrackPointCursor(
String[] projection, String selection, String[] selectionArgs, String sortOrder) {
return contentResolver.query(
TrackPointsColumns.CONTENT_URI, projection, selection, selectionArgs, sortOrder);
private Cursor getTrackPointCursor(String[] projection, String selection, String[] selectionArgs, String sortOrder) {
return contentResolver.query(TrackPointsColumns.CONTENT_URI, projection, selection, selectionArgs, sortOrder);
}
/**
@@ -1,35 +0,0 @@
/*
* Copyright 2010 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package de.dennisguse.opentracks.content;
/**
* Utilities for serializing primitive types.
*
* @author Rodrigo Damazio
*/
public class ContentTypeIds {
public static final byte BOOLEAN_TYPE_ID = 0;
public static final byte LONG_TYPE_ID = 1;
public static final byte INT_TYPE_ID = 2;
public static final byte FLOAT_TYPE_ID = 3;
public static final byte DOUBLE_TYPE_ID = 4;
public static final byte STRING_TYPE_ID = 5;
public static final byte BLOB_TYPE_ID = 6;
private ContentTypeIds() {
}
}
@@ -20,7 +20,6 @@ import android.content.ContentProvider;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.SQLException;
@@ -29,42 +28,39 @@ import android.database.sqlite.SQLiteException;
import android.database.sqlite.SQLiteOpenHelper;
import android.database.sqlite.SQLiteQueryBuilder;
import android.net.Uri;
import android.os.ParcelFileDescriptor;
import android.text.TextUtils;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import de.dennisguse.opentracks.util.FileUtils;
import java.io.File;
import java.io.FileNotFoundException;
/**
* A {@link ContentProvider} that handles access to track points, tracks, and
* waypoints tables.
* A {@link ContentProvider} that handles access to track points, tracks, and waypoints tables.
*
* @author Leif Hendrik Wilden
*/
public class CustomContentProvider extends ContentProvider {
public abstract class CustomContentProvider extends ContentProvider {
@VisibleForTesting
static final int DATABASE_VERSION = 23;
@VisibleForTesting
static final String DATABASE_NAME = "database.db";
private static final String TAG = CustomContentProvider.class.getSimpleName();
private final UriMatcher uriMatcher;
private SQLiteDatabase db;
public CustomContentProvider() {
uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
uriMatcher.addURI(ContentProviderUtils.AUTHORITY, TrackPointsColumns.TABLE_NAME, UrlType.TRACKPOINTS.ordinal());
uriMatcher.addURI(ContentProviderUtils.AUTHORITY, TrackPointsColumns.TABLE_NAME + "/#", UrlType.TRACKPOINTS_ID.ordinal());
uriMatcher.addURI(ContentProviderUtils.AUTHORITY, TracksColumns.TABLE_NAME, UrlType.TRACKS.ordinal());
uriMatcher.addURI(ContentProviderUtils.AUTHORITY, TracksColumns.TABLE_NAME + "/#", UrlType.TRACKS_ID.ordinal());
uriMatcher.addURI(ContentProviderUtils.AUTHORITY, WaypointsColumns.TABLE_NAME, UrlType.WAYPOINTS.ordinal());
uriMatcher.addURI(ContentProviderUtils.AUTHORITY, WaypointsColumns.TABLE_NAME + "/#", UrlType.WAYPOINTS_ID.ordinal());
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TrackPointsColumns.TABLE_NAME, UrlType.TRACKPOINTS.ordinal());
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TrackPointsColumns.TABLE_NAME + "/#", UrlType.TRACKPOINTS_ID.ordinal());
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TracksColumns.TABLE_NAME, UrlType.TRACKS.ordinal());
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TracksColumns.TABLE_NAME + "/#", UrlType.TRACKS_ID.ordinal());
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, WaypointsColumns.TABLE_NAME, UrlType.WAYPOINTS.ordinal());
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, WaypointsColumns.TABLE_NAME + "/#", UrlType.WAYPOINTS_ID.ordinal());
}
@Override
@@ -277,23 +273,6 @@ public class CustomContentProvider extends ContentProvider {
return count;
}
@Override
public ParcelFileDescriptor openFile(@NonNull Uri uri, @NonNull String mode) throws FileNotFoundException {
try {
File file = new File(uri.getPath());
if (file.exists()) {
return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
}
throw new FileNotFoundException(uri.getPath());
} finally {
File dir = new File(getContext().getCacheDir(), FileUtils.PLAY_TRACKS_DIR);
for (File file : dir.listFiles()) {
file.delete();
}
getContext().revokeUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION);
}
}
/**
* Gets the {@link UrlType} for a url.
*
@@ -23,21 +23,21 @@ import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
import android.database.ContentObserver;
import android.net.Uri;
import de.dennisguse.opentracks.Constants;
import de.dennisguse.opentracks.util.PreferencesUtils;
/**
* Data source on the phone.
*
* @author Rodrigo Damazio
*/
public class DataSource {
class DataSource {
private final ContentResolver contentResolver;
private final SharedPreferences sharedPreferences;
public DataSource(Context context) {
DataSource(Context context) {
contentResolver = context.getContentResolver();
sharedPreferences = context.getSharedPreferences(Constants.SETTINGS_NAME, Context.MODE_PRIVATE);
sharedPreferences = PreferencesUtils.getSharedPreferences(context);
}
/**
@@ -46,7 +46,7 @@ public class DataSource {
* @param uri the uri
* @param observer the observer
*/
public void registerContentObserver(Uri uri, ContentObserver observer) {
void registerContentObserver(Uri uri, ContentObserver observer) {
contentResolver.registerContentObserver(uri, false, observer);
}
@@ -55,7 +55,7 @@ public class DataSource {
*
* @param observer the observer
*/
public void unregisterContentObserver(ContentObserver observer) {
void unregisterContentObserver(ContentObserver observer) {
contentResolver.unregisterContentObserver(observer);
}
@@ -64,7 +64,7 @@ public class DataSource {
*
* @param listener the listener
*/
public void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {
void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {
sharedPreferences.registerOnSharedPreferenceChangeListener(listener);
}
@@ -73,8 +73,7 @@ public class DataSource {
*
* @param listener the listener
*/
public void unregisterOnSharedPreferenceChangeListener(
OnSharedPreferenceChangeListener listener) {
void unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {
sharedPreferences.unregisterOnSharedPreferenceChangeListener(listener);
}
}
@@ -26,17 +26,19 @@ import java.util.EnumSet;
import java.util.Set;
/**
* Data source manager. Creates observers/listeners and manages their
* registration with {@link DataSource}. The observers/listeners calls
* {@link DataSourceListener} when data changes.
* Creates observers/listeners and manages their registration with {@link DataSource}.
* The observers/listeners calls {@link DataSourceListener} when data changes.
*
* @author Rodrigo Damazio
*/
public class DataSourceManager {
class DataSourceManager {
private static final String TAG = DataSourceManager.class.getSimpleName();
private final DataSource dataSource;
private final DataSourceListener dataSourceListener;
// Registered listeners
private final Set<TrackDataType> registeredListeners = EnumSet.noneOf(TrackDataType.class);
private final Handler handler;
@@ -44,7 +46,8 @@ public class DataSourceManager {
private final WaypointsTableObserver waypointsTableObserver;
private final TrackPointsTableObserver trackPointsTableObserver;
private final PreferenceListener preferenceListener;
public DataSourceManager(DataSource dataSource, DataSourceListener dataSourceListener) {
DataSourceManager(DataSource dataSource, DataSourceListener dataSourceListener) {
this.dataSource = dataSource;
this.dataSourceListener = dataSourceListener;
@@ -60,7 +63,7 @@ public class DataSourceManager {
*
* @param listeners the listeners
*/
public void updateListeners(EnumSet<TrackDataType> listeners) {
void updateListeners(EnumSet<TrackDataType> listeners) {
EnumSet<TrackDataType> neededListeners = EnumSet.copyOf(listeners);
/*
@@ -155,7 +158,7 @@ public class DataSourceManager {
/**
* Unregisters all listeners with data source.
*/
public void unregisterAllListeners() {
void unregisterAllListeners() {
for (TrackDataType trackDataType : TrackDataType.values()) {
unregisterListener(trackDataType);
}
@@ -168,7 +171,7 @@ public class DataSourceManager {
*/
private class TracksTableObserver extends ContentObserver {
public TracksTableObserver() {
TracksTableObserver() {
super(handler);
}
@@ -185,7 +188,7 @@ public class DataSourceManager {
*/
private class WaypointsTableObserver extends ContentObserver {
public WaypointsTableObserver() {
WaypointsTableObserver() {
super(handler);
}
@@ -202,7 +205,7 @@ public class DataSourceManager {
*/
private class TrackPointsTableObserver extends ContentObserver {
public TrackPointsTableObserver() {
TrackPointsTableObserver() {
super(handler);
}
@@ -17,11 +17,8 @@ package de.dennisguse.opentracks.content;
import de.dennisguse.opentracks.stats.TripStatistics;
import java.util.Vector;
/**
* An interface for an object that can generate descriptions of track and
* waypoint.
* An interface for an object that can generate descriptions of track and waypoint.
*
* @author Sandor Dornbush
*/
@@ -31,12 +28,9 @@ public interface DescriptionGenerator {
* Generates a track description.
*
* @param track the track
* @param distances a vector of distances to generate the elevation chart
* @param elevations a vector of elevations to generate the elevation chart
* @param html true to output html, false to output plain text
*/
String generateTrackDescription(
Track track, Vector<Double> distances, Vector<Double> elevations, boolean html);
String generateTrackDescription(Track track, boolean html);
/**
* Generate a waypoint description from a trip statistics.
@@ -17,15 +17,14 @@
package de.dennisguse.opentracks.content;
import android.content.Context;
import android.util.Pair;
import androidx.annotation.VisibleForTesting;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.stats.TripStatistics;
import de.dennisguse.opentracks.util.StringUtils;
import de.dennisguse.opentracks.util.UnitConversions;
import de.dennisguse.opentracks.R;
import java.util.Vector;
/**
* @author Jimmy Shih
@@ -44,14 +43,14 @@ public class DescriptionGeneratorImpl implements DescriptionGenerator {
}
@Override
public String generateTrackDescription(Track track, Vector<Double> distances, Vector<Double> elevations, boolean html) {
public String generateTrackDescription(Track track, boolean html) {
String paragraphSeparator = html ? HTML_PARAGRAPH_SEPARATOR : TEXT_PARAGRAPH_SEPARATOR;
String lineBreak = html ? HTML_LINE_BREAK : TEXT_LINE_BREAK;
StringBuilder builder = new StringBuilder();
// Created by
String creator = html
? "<a href='http://" + context.getString(R.string.app_web_url) + "'>" + context.getString(R.string.app_name) + "</a>"
? "<a href='" + context.getString(R.string.app_web_url) + "'>" + context.getString(R.string.app_name) + "</a>"
: context.getString(R.string.app_name);
builder.append(creator);
@@ -97,8 +96,7 @@ public class DescriptionGeneratorImpl implements DescriptionGenerator {
StringBuilder builder = new StringBuilder();
// Total distance
writeDistance(
stats.getTotalDistance(), builder, R.string.description_total_distance, lineBreak);
writeDistance(stats.getTotalDistance(), builder, R.string.description_total_distance, lineBreak);
// Total time
writeTime(stats.getTotalTime(), builder, R.string.description_total_time, lineBreak);
@@ -110,19 +108,16 @@ public class DescriptionGeneratorImpl implements DescriptionGenerator {
writeSpeed(stats.getAverageSpeed(), builder, R.string.description_average_speed, lineBreak);
// Average moving speed
writeSpeed(stats.getAverageMovingSpeed(), builder, R.string.description_average_moving_speed,
lineBreak);
writeSpeed(stats.getAverageMovingSpeed(), builder, R.string.description_average_moving_speed, lineBreak);
// Max speed
writeSpeed(stats.getMaxSpeed(), builder, R.string.description_max_speed, lineBreak);
// Average pace
writePace(
stats.getAverageSpeed(), builder, R.string.description_average_pace_in_minute, lineBreak);
writePace(stats.getAverageSpeed(), builder, R.string.description_average_pace_in_minute, lineBreak);
// Average moving pace
writePace(stats.getAverageMovingSpeed(), builder,
R.string.description_average_moving_pace_in_minute, lineBreak);
writePace(stats.getAverageMovingSpeed(), builder, R.string.description_average_moving_pace_in_minute, lineBreak);
// Fastest pace
writePace(stats.getMaxSpeed(), builder, R.string.description_fastest_pace_in_minute, lineBreak);
@@ -134,8 +129,7 @@ public class DescriptionGeneratorImpl implements DescriptionGenerator {
writeElevation(stats.getMinElevation(), builder, R.string.description_min_elevation, lineBreak);
// Elevation gain
writeElevation(
stats.getTotalElevationGain(), builder, R.string.description_elevation_gain, lineBreak);
writeElevation(stats.getTotalElevationGain(), builder, R.string.description_elevation_gain, lineBreak);
// Max grade
writeGrade(stats.getMaxGrade(), builder, R.string.description_max_grade, lineBreak);
@@ -144,9 +138,7 @@ public class DescriptionGeneratorImpl implements DescriptionGenerator {
writeGrade(stats.getMinGrade(), builder, R.string.description_min_grade, lineBreak);
// Recorded time
builder.append(
context.getString(R.string.description_recorded_time,
StringUtils.formatDateTime(context, stats.getStartTime())));
builder.append(context.getString(R.string.description_recorded_time, StringUtils.formatDateTime(context, stats.getStartTime())));
builder.append(lineBreak);
return builder.toString();
@@ -191,8 +183,7 @@ public class DescriptionGeneratorImpl implements DescriptionGenerator {
* @param lineBreak line break string
*/
@VisibleForTesting
void writeSpeed(
double speed, StringBuilder builder, int resId, String lineBreak) {
void writeSpeed(double speed, StringBuilder builder, int resId, String lineBreak) {
double speedInKmHr = speed * UnitConversions.MS_TO_KMH;
double speedInMiHr = speedInKmHr * UnitConversions.KM_TO_MI;
builder.append(context.getString(resId, speedInKmHr, speedInMiHr));
@@ -209,18 +200,13 @@ public class DescriptionGeneratorImpl implements DescriptionGenerator {
*/
@VisibleForTesting
void writePace(double speed, StringBuilder builder, int resId, String lineBreak) {
String[] paceInMetrics = StringUtils.getSpeedParts(context, speed, true, false);
String[] paceInImperial = StringUtils.getSpeedParts(context, speed, false, false);
Pair<String, String> paceInMetrics = StringUtils.getSpeedParts(context, speed, true, false);
Pair<String, String> paceInImperial = StringUtils.getSpeedParts(context, speed, false, false);
if (paceInMetrics[0] == null) {
paceInMetrics[0] = context.getString(R.string.value_unknown);
}
String formattedPaceMetrics = paceInMetrics.first != null ? paceInMetrics.first : context.getString(R.string.value_unknown);
String formattedPaceImperial = paceInImperial.first != null ? paceInImperial.first : context.getString(R.string.value_unknown);
if (paceInImperial[0] == null) {
paceInImperial[0] = context.getString(R.string.value_unknown);
}
builder.append(context.getString(resId, paceInMetrics[0], paceInImperial[0]));
builder.append(context.getString(resId, formattedPaceMetrics, formattedPaceImperial));
builder.append(lineBreak);
}
@@ -233,8 +219,7 @@ public class DescriptionGeneratorImpl implements DescriptionGenerator {
* @param lineBreak line break string
*/
@VisibleForTesting
void writeElevation(
double elevation, StringBuilder builder, int resId, String lineBreak) {
void writeElevation(double elevation, StringBuilder builder, int resId, String lineBreak) {
long elevationInM = Math.round(elevation);
long elevationInFt = Math.round(elevation * UnitConversions.M_TO_FT);
builder.append(context.getString(resId, elevationInM, elevationInFt));
@@ -251,8 +236,7 @@ public class DescriptionGeneratorImpl implements DescriptionGenerator {
*/
@VisibleForTesting
void writeGrade(double grade, StringBuilder builder, int resId, String lineBreak) {
long gradeInPercent = Double.isNaN(grade) || Double.isInfinite(grade) ? 0L
: Math.round(grade * 100);
long gradeInPercent = Double.isNaN(grade) || Double.isInfinite(grade) ? 0L : Math.round(grade * 100);
builder.append(context.getString(resId, gradeInPercent));
builder.append(lineBreak);
}
@@ -20,17 +20,18 @@ import android.location.Location;
import androidx.annotation.NonNull;
import de.dennisguse.opentracks.stats.TripStatistics;
import de.dennisguse.opentracks.util.LocationUtils;
import de.dennisguse.opentracks.util.UnitConversions;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.List;
import java.util.Locale;
import java.util.SortedSet;
import java.util.TreeSet;
import de.dennisguse.opentracks.stats.TripStatistics;
import de.dennisguse.opentracks.util.LocationUtils;
import de.dennisguse.opentracks.util.UnitConversions;
/**
* Engine for searching for tracks and waypoints by text.
*
@@ -97,34 +98,35 @@ public class SearchEngine {
/**
* Oldest timestamp for which we rank based on time (2000-01-01 00:00:00.000)
*/
private static final long OLDEST_ALLOWED_TIMESTAMP = 946692000000L;
public static final long OLDEST_ALLOWED_TIMESTAMP = 946692000000L;
/**
* Comparador for scored results.
*/
private static final Comparator<ScoredResult> SCORED_RESULT_COMPARATOR =
new Comparator<ScoredResult>() {
@Override
public int compare(ScoredResult r1, ScoredResult r2) {
// Score ordering.
int scoreDiff = Double.compare(r2.score, r1.score);
if (scoreDiff != 0) {
return scoreDiff;
}
private static final Comparator<ScoredResult> SCORED_RESULT_COMPARATOR = new Comparator<ScoredResult>() {
@Override
public int compare(ScoredResult r1, ScoredResult r2) {
// Score ordering.
int scoreDiff = Double.compare(r2.score, r1.score);
if (scoreDiff != 0) {
return scoreDiff;
}
// Make tracks come before waypoints.
if (r1.waypoint != null && r2.track != null) {
return 1;
} else if (r1.track != null && r2.waypoint != null) {
return -1;
}
// Make tracks come before waypoints.
if (r1.waypoint != null && r2.track != null) {
return 1;
} else if (r1.track != null && r2.waypoint != null) {
return -1;
}
// Finally, use arbitrary ordering, by ID.
long id1 = r1.track != null ? r1.track.getId() : r1.waypoint.getId();
long id2 = r2.track != null ? r2.track.getId() : r2.waypoint.getId();
long idDiff = id2 - id1;
return Long.signum(idDiff);
}
};
// Finally, use arbitrary ordering, by ID.
long id1 = r1.track != null ? r1.track.getId() : r1.waypoint.getId();
long id2 = r2.track != null ? r2.track.getId() : r2.waypoint.getId();
long idDiff = id2 - id1;
return Long.signum(idDiff);
}
};
private final ContentProviderUtils providerUtils;
public SearchEngine(ContentProviderUtils providerUtils) {
@@ -134,26 +136,22 @@ public class SearchEngine {
/**
* Squashes a number by calculating 1 / log (1 + x).
*/
//TODO Why is this done?
private static double squash(double x) {
return 1.0 / Math.log1p(x);
}
/**
* Executes a search query and returns a set of sorted results.
* Executes a searchable query and returns a set of sorted results.
*
* @param query the query to execute
* @return a set of results, sorted according to their score
*/
public SortedSet<ScoredResult> search(SearchQuery query) {
ArrayList<Track> tracks = new ArrayList<>();
ArrayList<Waypoint> waypoints = new ArrayList<>();
TreeSet<ScoredResult> scoredResults = new TreeSet<>(SCORED_RESULT_COMPARATOR);
retrieveTracks(query, tracks);
retrieveWaypoints(query, waypoints);
scoreTrackResults(tracks, query, scoredResults);
scoreWaypointResults(waypoints, query, scoredResults);
scoreTrackResults(retrieveTracks(query), query, scoredResults);
scoreWaypointResults(retrieveWaypoints(query), query, scoredResults);
return scoredResults;
}
@@ -161,18 +159,19 @@ public class SearchEngine {
/**
* Retrieves tracks matching the given query from the database.
*
* @param query the query to retrieve for
* @param tracks list to fill with the resulting tracks
* @param query the query to retrieve for
*/
private void retrieveTracks(SearchQuery query, ArrayList<Track> tracks) {
private List<Track> retrieveTracks(SearchQuery query) {
ArrayList<Track> tracks = new ArrayList<>();
String queryLikeSelection = "%" + query.textQuery + "%";
String[] trackSelectionArgs = new String[]{
queryLikeSelection,
queryLikeSelection,
queryLikeSelection};
queryLikeSelection
};
try (Cursor cursor = providerUtils.getTrackCursor(
TRACK_SELECTION_QUERY, trackSelectionArgs, TRACK_SELECTION_ORDER)) {
try (Cursor cursor = providerUtils.getTrackCursor(TRACK_SELECTION_QUERY, trackSelectionArgs, TRACK_SELECTION_ORDER)) {
if (cursor != null) {
tracks.ensureCapacity(cursor.getCount());
while (cursor.moveToNext()) {
@@ -180,22 +179,26 @@ public class SearchEngine {
}
}
}
return tracks;
}
/**
* Retrieves waypoints matching the given query from the database.
*
* @param query the query to retrieve for
* @param waypoints list to fill with the resulting waypoints
* @param query the query to retrieve for
*/
private void retrieveWaypoints(SearchQuery query, ArrayList<Waypoint> waypoints) {
private List<Waypoint> retrieveWaypoints(SearchQuery query) {
ArrayList<Waypoint> waypoints = new ArrayList<>();
String queryLikeSelection2 = "%" + query.textQuery + "%";
String[] waypointSelectionArgs = new String[]{
queryLikeSelection2,
queryLikeSelection2,
queryLikeSelection2};
try (Cursor cursor = providerUtils.getWaypointCursor(WAYPOINT_SELECTION_QUERY, waypointSelectionArgs,
WAYPOINT_SELECTION_ORDER, MAX_SCORED_WAYPOINTS)) {
queryLikeSelection2
};
try (Cursor cursor = providerUtils.getWaypointCursor(WAYPOINT_SELECTION_QUERY, waypointSelectionArgs, WAYPOINT_SELECTION_ORDER, MAX_SCORED_WAYPOINTS)) {
if (cursor != null) {
waypoints.ensureCapacity(cursor.getCount());
while (cursor.moveToNext()) {
@@ -206,6 +209,8 @@ public class SearchEngine {
}
}
}
return waypoints;
}
/**
@@ -238,7 +243,6 @@ public class SearchEngine {
score *= getTitleBoost(query, track.getName(), track.getDescription(), track.getCategory());
TripStatistics statistics = track.getTripStatistics();
// TODO: Also boost for proximity to the currently-centered position on the map.
score *= getDistanceBoost(query, statistics.getMeanLatitude(), statistics.getMeanLongitude());
long meanTimestamp = (statistics.getStartTime() + statistics.getStopTime()) / 2L;
@@ -281,7 +285,6 @@ public class SearchEngine {
Location location = waypoint.getLocation();
score *= getTitleBoost(query, waypoint.getName(), waypoint.getDescription(), waypoint.getCategory());
// TODO: Also boost for proximity to the currently-centered position on the map.
score *= getDistanceBoost(query, location.getLatitude(), location.getLongitude());
score *= getTimeBoost(query, location.getTime());
@@ -302,8 +305,7 @@ public class SearchEngine {
* @param category the category of the track or waypoint
* @return the total boost to be applied to the result
*/
private double getTitleBoost(SearchQuery query,
String name, String description, String category) {
private double getTitleBoost(SearchQuery query, String name, String description, String category) {
// Title boost: track name > description > category.
double boost = 1.0;
if (name.toLowerCase(Locale.getDefault()).contains(query.textQuery)) {
@@ -332,8 +334,7 @@ public class SearchEngine {
}
// Score recent tracks higher.
long timeAgoHours = (long) ((query.currentTimestamp - timestamp) * UnitConversions.MS_TO_S
* UnitConversions.S_TO_MIN * UnitConversions.MIN_TO_HR);
long timeAgoHours = (long) ((query.currentTimestamp - timestamp) * UnitConversions.MS_TO_S * UnitConversions.S_TO_MIN * UnitConversions.MIN_TO_HR);
if (timeAgoHours > 0L) {
return squash(timeAgoHours);
} else {
@@ -364,6 +365,7 @@ public class SearchEngine {
// Score tracks close to the current location higher.
double distanceKm = distanceResults[0] * UnitConversions.M_TO_KM;
if (distanceKm > 0.0) {
// Use the inverse of the amortized distance.
return squash(distanceKm);
@@ -374,15 +376,15 @@ public class SearchEngine {
}
/**
* Description of a search query, along with all contextual data needed to execute it.
* Description of a searchable query, along with all contextual data needed to execute it.
*/
public static class SearchQuery {
public final String textQuery;
public final Location currentLocation;
public final long currentTrackId;
public final long currentTimestamp;
public SearchQuery(String textQuery, Location currentLocation, long currentTrackId,
long currentTimestamp) {
final String textQuery;
final Location currentLocation;
final long currentTrackId;
final long currentTimestamp;
public SearchQuery(String textQuery, Location currentLocation, long currentTrackId, long currentTimestamp) {
this.textQuery = textQuery.toLowerCase(Locale.getDefault());
this.currentLocation = currentLocation;
this.currentTrackId = currentTrackId;
@@ -391,17 +393,19 @@ public class SearchEngine {
}
/**
* Description of a search result which has been retrieved and scored.
* Description of a searchable result which has been retrieved and scored.
*/
public static class ScoredResult {
public final Track track;
public final Waypoint waypoint;
public final double score;
final double score;
ScoredResult(Track track, double score) {
this.track = track;
this.waypoint = null;
this.score = score;
}
ScoredResult(Waypoint waypoint, double score) {
this.track = null;
this.waypoint = waypoint;
@@ -1,46 +0,0 @@
/*
* Copyright 2012 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 de.dennisguse.opentracks.content;
import android.content.Context;
import android.content.SearchRecentSuggestionsProvider;
import android.provider.SearchRecentSuggestions;
import de.dennisguse.opentracks.BuildConfig;
/**
* Content provider for search suggestions.
*
* @author Rodrigo Damazio
*/
public class SearchEngineProvider extends SearchRecentSuggestionsProvider {
private static final String AUTHORITY = BuildConfig.APPLICATION_ID + ".search";
private static final int MODE = DATABASE_MODE_QUERIES;
public SearchEngineProvider() {
setupSuggestions(AUTHORITY, MODE);
}
// TODO: Also add suggestions from the database.
/**
* Creates and returns a helper for adding recent queries or clearing the recent query history.
*/
public static SearchRecentSuggestions newHelper(Context context) {
return new SearchRecentSuggestions(context, AUTHORITY, MODE);
}
}
@@ -0,0 +1,224 @@
package de.dennisguse.opentracks.content;
import android.content.Context;
import android.content.UriMatcher;
import android.content.pm.ProviderInfo;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.net.Uri;
import android.os.Bundle;
import android.os.ParcelFileDescriptor;
import android.provider.OpenableColumns;
import android.util.Log;
import android.util.Pair;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Arrays;
import de.dennisguse.opentracks.android.IContentResolver;
import de.dennisguse.opentracks.io.file.TrackFileFormat;
import de.dennisguse.opentracks.io.file.exporter.TrackExporter;
/**
* A content provider that mimics the behavior of {@link androidx.core.content.FileProvider}, which shares virtual (non-existing) KML-files.
* The actual content of the virtual files is generated by using the functionality defined in {@link CustomContentProvider}.
*
* Moreover, it manages access to OpenTrack's database via {@link CustomContentProvider}.
*
* Explanation:
* Although a request is handled by a {@link android.content.ContentProvider} (with temporarily granted permission), Android's security infrastructure prevents forwarding queries to non-exported {@link android.content.ContentProvider}.
* Thus, if {@link ShareContentProvider} and {@link CustomContentProvider} would be two different instances, the data would not be accessible to external apps.
* While handling a request {@link ShareContentProvider} could `grantPermissions()` to the calling app for {@link CustomContentProvider}'s URI.
* However, while handling the request this would allow the calling app to actually contact {@link CustomContentProvider} directly and get access to stored data that should remain private.
*/
public class ShareContentProvider extends CustomContentProvider implements IContentResolver {
private static final String[] COLUMNS = {OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE};
public static final String TAG = ShareContentProvider.class.getSimpleName();
private static final int URI_GPX = 0;
private static final int URI_KML_ONLY = 1;
private static final int URI_KML_WITH_TRACKDETAIL = 2;
private static final int URI_KML_WITH_TRACKDETAIL_SENSORDATA = 3;
private static final int URI_KMZ_ONLY_TRACK = 4;
private static final int URI_KMZ_WITH_TRACKDETAIL = 5;
private static final int URI_KMZ_WITH_TRACKDETAIL_AND_SENSORDATA = 6;
private static final int URI_KMZ_WITH_TRACKDETAIL_SENSORDATA_AND_PICTURES = 7;
private static final UriMatcher uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
private static final String TRACKID_DELIMITER = "_";
static {
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TracksColumns.TABLE_NAME + "/" + TrackFileFormat.GPX.getName() + "/*", URI_GPX);
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TracksColumns.TABLE_NAME + "/" + TrackFileFormat.KML_ONLY_TRACK.getName() + "/*", URI_KML_ONLY);
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TracksColumns.TABLE_NAME + "/" + TrackFileFormat.KML_WITH_TRACKDETAIL.getName() + "/*", URI_KML_WITH_TRACKDETAIL);
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TracksColumns.TABLE_NAME + "/" + TrackFileFormat.KML_WITH_TRACKDETAIL_AND_SENSORDATA.getName() + "/*", URI_KML_WITH_TRACKDETAIL_SENSORDATA);
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TracksColumns.TABLE_NAME + "/" + TrackFileFormat.KMZ_ONLY_TRACK.getName() + "/*", URI_KMZ_ONLY_TRACK);
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TracksColumns.TABLE_NAME + "/" + TrackFileFormat.KMZ_WITH_TRACKDETAIL.getName() + "/*", URI_KMZ_WITH_TRACKDETAIL);
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TracksColumns.TABLE_NAME + "/" + TrackFileFormat.KMZ_WITH_TRACKDETAIL_AND_SENSORDATA.getName() + "/*", URI_KMZ_WITH_TRACKDETAIL_AND_SENSORDATA);
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TracksColumns.TABLE_NAME + "/" + TrackFileFormat.KMZ_WITH_TRACKDETAIL_AND_SENSORDATA_AND_PICTURES.getName() + "/*", URI_KMZ_WITH_TRACKDETAIL_SENSORDATA_AND_PICTURES);
}
public static Pair<Uri, String> createURI(long[] trackIds, @NonNull TrackFileFormat trackFileFormat) {
if (trackIds.length == 0) {
throw new UnsupportedOperationException();
}
StringBuilder builder = new StringBuilder();
for (long trackId : trackIds) {
builder.append(trackId).append(TRACKID_DELIMITER);
}
builder.deleteCharAt(builder.lastIndexOf(TRACKID_DELIMITER));
Uri uri = Uri.parse(ContentProviderUtils.CONTENT_BASE_URI + "/" + TracksColumns.TABLE_NAME + "/" + trackFileFormat.getName() + "/" + builder + "." + trackFileFormat.getExtension());
String mime = getTypeMime(uri);
Log.d(TAG, "Created uri " + uri.toString() + " with MIME " + mime);
return new Pair<>(uri, mime);
}
private static long[] parseURI(Uri uri) {
String lastPathSegment = uri.getLastPathSegment();
if (lastPathSegment == null) {
return new long[]{};
}
String fileExtension = "." + getTrackFileFormat(uri).getExtension();
String[] lastPathSegmentSplit = lastPathSegment.replace(fileExtension, "").split(TRACKID_DELIMITER);
long[] trackIds = new long[lastPathSegmentSplit.length];
for (int i = 0; i < trackIds.length; i++) {
trackIds[i] = Long.valueOf(lastPathSegmentSplit[i]);
}
return trackIds;
}
/**
* Do not allow to be exported via AndroidManifest.
* Check that caller has permissions to access {@link CustomContentProvider}.
*/
@Override
public void attachInfo(@NonNull Context context, @NonNull ProviderInfo info) {
super.attachInfo(context, info);
// Sanity check our security
if (info.exported) {
throw new UnsupportedOperationException("Provider must not be exported");
}
if (!info.grantUriPermissions) {
throw new SecurityException("Provider must grant uri permissions");
}
}
private static TrackFileFormat getTrackFileFormat(@NonNull Uri uri) {
switch (uriMatcher.match(uri)) {
case URI_GPX:
return TrackFileFormat.GPX;
case URI_KML_ONLY:
return TrackFileFormat.KML_ONLY_TRACK;
case URI_KML_WITH_TRACKDETAIL:
return TrackFileFormat.KML_WITH_TRACKDETAIL;
case URI_KML_WITH_TRACKDETAIL_SENSORDATA:
return TrackFileFormat.KML_WITH_TRACKDETAIL_AND_SENSORDATA;
case URI_KMZ_ONLY_TRACK:
return TrackFileFormat.KMZ_ONLY_TRACK;
case URI_KMZ_WITH_TRACKDETAIL:
return TrackFileFormat.KMZ_WITH_TRACKDETAIL;
case URI_KMZ_WITH_TRACKDETAIL_AND_SENSORDATA:
return TrackFileFormat.KMZ_WITH_TRACKDETAIL_AND_SENSORDATA;
case URI_KMZ_WITH_TRACKDETAIL_SENSORDATA_AND_PICTURES:
return TrackFileFormat.KMZ_WITH_TRACKDETAIL_AND_SENSORDATA_AND_PICTURES;
default:
throw new RuntimeException("Could not derive TrackFileFormat from Uri " + uri);
}
}
@Nullable
public static String getTypeMime(@NonNull Uri uri) {
return getTrackFileFormat(uri).getMimeType();
}
@Override
public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) {
if (uriMatcher.match(uri) == -1) {
return super.query(uri, projection, selection, selectionArgs, sortOrder);
}
// ContentProvider has already checked granted permissions
if (projection == null) {
projection = COLUMNS;
}
String[] cols = new String[projection.length];
Object[] values = new Object[projection.length];
int i = 0;
for (String col : projection) {
if (OpenableColumns.DISPLAY_NAME.equals(col)) {
cols[i] = OpenableColumns.DISPLAY_NAME;
values[i++] = uri.getLastPathSegment();
} else if (OpenableColumns.SIZE.equals(col)) {
cols[i] = OpenableColumns.SIZE;
values[i++] = -1;
}
}
cols = Arrays.copyOf(cols, i);
values = Arrays.copyOf(values, i);
final MatrixCursor cursor = new MatrixCursor(cols, 1);
cursor.addRow(values);
return cursor;
}
@Nullable
@Override
public String getType(@NonNull Uri uri) {
String mime = getTypeMime(uri);
if (mime != null) {
return mime;
}
return super.getType(uri);
}
@Nullable
@Override
public ParcelFileDescriptor openFile(@NonNull Uri uri, @NonNull String mode) throws FileNotFoundException {
ContentProviderUtils contentProviderUtils = new ContentProviderUtilsImpl(this);
long[] trackIds = parseURI(uri);
final Track[] tracks = new Track[trackIds.length];
for (int i = 0; i < trackIds.length; i++) {
tracks[i] = contentProviderUtils.getTrack(trackIds[i]);
}
final TrackExporter trackExporter = getTrackFileFormat(uri).newTrackExporter(getContext(), tracks, null);
PipeDataWriter pipeDataWriter = new PipeDataWriter<String>() {
@Override
public void writeDataToPipe(@NonNull ParcelFileDescriptor output, @NonNull Uri uri, @NonNull String mimeType, @Nullable Bundle opts, @Nullable String args) {
try (FileOutputStream fileOutputStream = new FileOutputStream(output.getFileDescriptor())) {
trackExporter.writeTrack(getContext(), fileOutputStream);
} catch (IOException e) {
Log.w(TAG, "there occurred an error while sharing a file: " + e);
}
}
};
return openPipeHelper(uri, getType(uri), null, null, pipeDataWriter);
}
}
@@ -17,12 +17,11 @@
package de.dennisguse.opentracks.content;
import android.location.Location;
import android.os.Parcel;
import android.os.Parcelable;
import de.dennisguse.opentracks.stats.TripStatistics;
import java.util.ArrayList;
import java.util.List;
import de.dennisguse.opentracks.stats.TripStatistics;
/**
* A track.
@@ -30,82 +29,29 @@ import java.util.ArrayList;
* @author Leif Hendrik Wilden
* @author Rodrigo Damazio
*/
public class Track implements Parcelable {
public class Track {
public static final Parcelable.Creator<Track> CREATOR = new Parcelable.Creator<Track>() {
@Override
public Track createFromParcel(Parcel in) {
return new Track(in);
}
@Override
public Track[] newArray(int size) {
return new Track[size];
}
};
private long id = -1L;
private String name = "";
private String description = "";
private String category = "";
private long startId = -1L;
private long stopId = -1L;
/*
* The number of location points (present even if the points themselves are
* not loaded)
* The number of location points (present even if the points themselves are not loaded)
*/
private int numberOfPoints = 0;
private String icon = "";
private long modifiedTime = -1L;
private TripStatistics tripStatistics = new TripStatistics();
// Location points (which may not have been loaded)
private ArrayList<Location> locations = new ArrayList<>();
private List<Location> locations = new ArrayList<>();
public Track() {
}
private Track(Parcel in) {
id = in.readLong();
name = in.readString();
description = in.readString();
category = in.readString();
startId = in.readLong();
stopId = in.readLong();
numberOfPoints = in.readInt();
icon = in.readString();
modifiedTime = in.readLong();
ClassLoader classLoader = getClass().getClassLoader();
tripStatistics = in.readParcelable(classLoader);
for (int i = 0; i < numberOfPoints; ++i) {
Location location = in.readParcelable(classLoader);
locations.add(location);
}
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeLong(id);
dest.writeString(name);
dest.writeString(description);
dest.writeString(category);
dest.writeLong(startId);
dest.writeLong(stopId);
dest.writeInt(numberOfPoints);
dest.writeString(icon);
dest.writeLong(modifiedTime);
dest.writeParcelable(tripStatistics, 0);
for (int i = 0; i < numberOfPoints; ++i) {
dest.writeParcelable(locations.get(i), 0);
}
}
public long getId() {
return id;
}
@@ -170,14 +116,6 @@ public class Track implements Parcelable {
this.icon = icon;
}
public long getModifiedTime() {
return modifiedTime;
}
public void setModifiedTime(long modifiedTime) {
this.modifiedTime = modifiedTime;
}
public TripStatistics getTripStatistics() {
return tripStatistics;
}
@@ -190,7 +128,7 @@ public class Track implements Parcelable {
locations.add(location);
}
public ArrayList<Location> getLocations() {
public List<Location> getLocations() {
return locations;
}
@@ -25,34 +25,35 @@ import android.util.Log;
import androidx.annotation.VisibleForTesting;
import de.dennisguse.opentracks.content.ContentProviderUtils.LocationIterator;
import de.dennisguse.opentracks.util.LocationUtils;
import de.dennisguse.opentracks.util.PreferencesUtils;
import de.dennisguse.opentracks.R;
import java.util.Collections;
import java.util.EnumSet;
import java.util.Set;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.ContentProviderUtils.LocationIterator;
import de.dennisguse.opentracks.util.LocationUtils;
import de.dennisguse.opentracks.util.PreferencesUtils;
/**
* Track data hub. Receives data from {@link DataSource} and distributes it to
* {@link TrackDataListener} after some processing.
* Track data hub. Receives data from {@link DataSource} and distributes it to {@link TrackDataListener} after some processing.
*
* @author Rodrigo Damazio
*/
public class TrackDataHub implements DataSourceListener {
/**
* Target number of track points displayed by the map overlay. We may display
* more than this number of points.
* Target number of track points displayed by the map overlay.
* We may display more than this number of points.
*/
public static final int TARGET_DISPLAYED_TRACK_POINTS = 5000;
static final int TARGET_DISPLAYED_TRACK_POINTS = 5000;
/**
* Maximum number of waypoints to displayed.
*/
@VisibleForTesting
static final int MAX_DISPLAYED_WAYPOINTS = 128;
private static final int MAX_DISPLAYED_WAYPOINTS = 128;
private static final String TAG = TrackDataHub.class.getSimpleName();
private final Context context;
private final TrackDataManager trackDataManager;
private final ContentProviderUtils contentProviderUtils;
@@ -87,8 +88,7 @@ public class TrackDataHub implements DataSourceListener {
* @param targetNumPoints the target number of points
*/
@VisibleForTesting
TrackDataHub(Context context, TrackDataManager trackDataManager,
ContentProviderUtils contentProviderUtils, int targetNumPoints) {
private TrackDataHub(Context context, TrackDataManager trackDataManager, ContentProviderUtils contentProviderUtils, int targetNumPoints) {
this.context = context;
this.trackDataManager = trackDataManager;
this.contentProviderUtils = contentProviderUtils;
@@ -96,17 +96,11 @@ public class TrackDataHub implements DataSourceListener {
resetSamplingState();
}
/**
* Creates a new instance.
*/
public synchronized static TrackDataHub newInstance(Context context) {
return new TrackDataHub(context, new TrackDataManager(), ContentProviderUtils.Factory.get(
context), TARGET_DISPLAYED_TRACK_POINTS);
}
/**
* Starts.
*/
public void start() {
if (started) {
Log.i(TAG, "TrackDataHub already started, ignoring start.");
@@ -120,7 +114,7 @@ public class TrackDataHub implements DataSourceListener {
dataSourceManager = new DataSourceManager(dataSource, this);
notifyPreferenceChanged(null);
runInHanderThread(new Runnable() {
runInHandlerThread(new Runnable() {
@Override
public void run() {
if (dataSourceManager != null) {
@@ -131,9 +125,6 @@ public class TrackDataHub implements DataSourceListener {
});
}
/**
* Stops.
*/
public void stop() {
if (!started) {
Log.i(TAG, "TrackDataHub not started, ignoring stop.");
@@ -151,13 +142,8 @@ public class TrackDataHub implements DataSourceListener {
dataSourceManager = null;
}
/**
* Loads a track.
*
* @param trackId the track id
*/
public void loadTrack(final long trackId) {
runInHanderThread(new Runnable() {
runInHandlerThread(new Runnable() {
@Override
public void run() {
if (trackId == selectedTrackId) {
@@ -177,7 +163,7 @@ public class TrackDataHub implements DataSourceListener {
* @param trackDataTypes the track data types
*/
public void registerTrackDataListener(final TrackDataListener trackDataListener, final EnumSet<TrackDataType> trackDataTypes) {
runInHanderThread(new Runnable() {
runInHandlerThread(new Runnable() {
@Override
public void run() {
trackDataManager.registerListener(trackDataListener, trackDataTypes);
@@ -195,7 +181,7 @@ public class TrackDataHub implements DataSourceListener {
* @param trackDataListener the track data listener
*/
public void unregisterTrackDataListener(final TrackDataListener trackDataListener) {
runInHanderThread(new Runnable() {
runInHandlerThread(new Runnable() {
@Override
public void run() {
trackDataManager.unregisterListener(trackDataListener);
@@ -210,7 +196,7 @@ public class TrackDataHub implements DataSourceListener {
* Reloads data for a {@link TrackDataListener}.
*/
public void reloadDataForListener(final TrackDataListener trackDataListener) {
runInHanderThread(new Runnable() {
runInHandlerThread(new Runnable() {
@Override
public void run() {
loadDataForListener(trackDataListener);
@@ -222,8 +208,7 @@ public class TrackDataHub implements DataSourceListener {
* Returns true if the selected track is recording.
*/
public boolean isSelectedTrackRecording() {
return selectedTrackId == recordingTrackId
&& recordingTrackId != PreferencesUtils.RECORDING_TRACK_ID_DEFAULT;
return selectedTrackId == recordingTrackId && PreferencesUtils.isRecording(recordingTrackId);
}
/**
@@ -235,7 +220,7 @@ public class TrackDataHub implements DataSourceListener {
@Override
public void notifyTracksTableUpdated() {
runInHanderThread(new Runnable() {
runInHandlerThread(new Runnable() {
@Override
public void run() {
notifyTracksTableUpdate(trackDataManager.getListeners(TrackDataType.TRACKS_TABLE));
@@ -245,7 +230,7 @@ public class TrackDataHub implements DataSourceListener {
@Override
public void notifyWaypointsTableUpdated() {
runInHanderThread(new Runnable() {
runInHandlerThread(new Runnable() {
@Override
public void run() {
notifyWaypointsTableUpdate(trackDataManager.getListeners(TrackDataType.WAYPOINTS_TABLE));
@@ -255,11 +240,10 @@ public class TrackDataHub implements DataSourceListener {
@Override
public void notifyTrackPointsTableUpdated() {
runInHanderThread(new Runnable() {
runInHandlerThread(new Runnable() {
@Override
public void run() {
notifyTrackPointsTableUpdate(
true, trackDataManager.getListeners(TrackDataType.SAMPLED_IN_TRACK_POINTS_TABLE),
notifyTrackPointsTableUpdate(true, trackDataManager.getListeners(TrackDataType.SAMPLED_IN_TRACK_POINTS_TABLE),
trackDataManager.getListeners(TrackDataType.SAMPLED_OUT_TRACK_POINTS_TABLE));
}
});
@@ -267,50 +251,39 @@ public class TrackDataHub implements DataSourceListener {
@Override
public void notifyPreferenceChanged(final String key) {
runInHanderThread(new Runnable() {
runInHandlerThread(new Runnable() {
@Override
public void run() {
if (key == null
|| key.equals(PreferencesUtils.getKey(context, R.string.recording_track_id_key))) {
if (key == null || key.equals(PreferencesUtils.getKey(context, R.string.recording_track_id_key))) {
recordingTrackId = PreferencesUtils.getLong(context, R.string.recording_track_id_key);
}
if (key == null
|| key.equals(PreferencesUtils.getKey(context, R.string.recording_track_paused_key))) {
recordingTrackPaused = PreferencesUtils.getBoolean(
context, R.string.recording_track_paused_key,
PreferencesUtils.RECORDING_TRACK_PAUSED_DEFAULT);
if (key == null || key.equals(PreferencesUtils.getKey(context, R.string.recording_track_paused_key))) {
recordingTrackPaused = PreferencesUtils.getBoolean(context, R.string.recording_track_paused_key, PreferencesUtils.RECORDING_TRACK_PAUSED_DEFAULT);
}
if (key == null
|| key.equals(PreferencesUtils.getKey(context, R.string.stats_units_key))) {
if (key == null || key.equals(PreferencesUtils.getKey(context, R.string.stats_units_key))) {
metricUnits = PreferencesUtils.isMetricUnits(context);
if (key != null) {
for (TrackDataListener trackDataListener :
trackDataManager.getListeners(TrackDataType.PREFERENCE)) {
for (TrackDataListener trackDataListener : trackDataManager.getListeners(TrackDataType.PREFERENCE)) {
if (trackDataListener.onMetricUnitsChanged(metricUnits)) {
loadDataForListener(trackDataListener);
}
}
}
}
if (key == null
|| key.equals(PreferencesUtils.getKey(context, R.string.stats_rate_key))) {
if (key == null || key.equals(PreferencesUtils.getKey(context, R.string.stats_rate_key))) {
reportSpeed = PreferencesUtils.isReportSpeed(context);
if (key != null) {
for (TrackDataListener trackDataListener :
trackDataManager.getListeners(TrackDataType.PREFERENCE)) {
for (TrackDataListener trackDataListener : trackDataManager.getListeners(TrackDataType.PREFERENCE)) {
if (trackDataListener.onReportSpeedChanged(reportSpeed)) {
loadDataForListener(trackDataListener);
}
}
}
}
if (key == null
|| key.equals(PreferencesUtils.getKey(context, R.string.recording_gps_accuracy_key))) {
recordingGpsAccuracy = PreferencesUtils.getInt(context, R.string.recording_gps_accuracy_key,
PreferencesUtils.RECORDING_GPS_ACCURACY_DEFAULT);
if (key == null || key.equals(PreferencesUtils.getKey(context, R.string.recording_gps_accuracy_key))) {
recordingGpsAccuracy = PreferencesUtils.getInt(context, R.string.recording_gps_accuracy_key, PreferencesUtils.RECORDING_GPS_ACCURACY_DEFAULT);
if (key != null) {
for (TrackDataListener trackDataListener :
trackDataManager.getListeners(TrackDataType.PREFERENCE)) {
for (TrackDataListener trackDataListener : trackDataManager.getListeners(TrackDataType.PREFERENCE)) {
if (trackDataListener.onRecordingGpsAccuracy(recordingGpsAccuracy)) {
loadDataForListener(trackDataListener);
}
@@ -359,7 +332,7 @@ public class TrackDataHub implements DataSourceListener {
}
/**
* Loads data for a listener. To be run in the {@link #handler} thread.
* Loads data for a listener; to be run in the {@link #handler} thread.
*
* @param trackDataListener the track data listener.
*/
@@ -386,8 +359,7 @@ public class TrackDataHub implements DataSourceListener {
if (isOnlyListener) {
resetSamplingState();
}
Set<TrackDataListener> sampledOutListeners = hasSampledOut ? trackDataListeners
: Collections.<TrackDataListener>emptySet();
Set<TrackDataListener> sampledOutListeners = hasSampledOut ? trackDataListeners : Collections.<TrackDataListener>emptySet();
notifyTrackPointsTableUpdate(isOnlyListener, trackDataListeners, sampledOutListeners);
}
@@ -397,7 +369,7 @@ public class TrackDataHub implements DataSourceListener {
}
/**
* Notifies track table update. To be run in the {@link #handler} thread.
* Notifies track table update; to be run in the {@link #handler} thread.
*
* @param trackDataListeners the track data listeners to notify
*/
@@ -412,9 +384,8 @@ public class TrackDataHub implements DataSourceListener {
}
/**
* Notifies waypoint table update. Currently, reloads all the waypoints up to
* {@link #MAX_DISPLAYED_WAYPOINTS}. To be run in the {@link #handler}
* thread.
* Notifies waypoint table update.
* Currently, reloads all the waypoints up to {@link #MAX_DISPLAYED_WAYPOINTS}. To be run in the {@link #handler} thread.
*
* @param trackDataListeners the track data listeners to notify
*/
@@ -448,18 +419,17 @@ public class TrackDataHub implements DataSourceListener {
}
/**
* Notifies track points table update. To be run in the {@link #handler}
* thread.
* Notifies track points table update; to be run in the {@link #handler} thread.
*
* @param updateSamplingState true to update the sampling state
* @param sampledInListeners the sampled-in listeners
* @param sampledOutListeners the sampled-out listeners
*/
private void notifyTrackPointsTableUpdate(boolean updateSamplingState,
Set<TrackDataListener> sampledInListeners, Set<TrackDataListener> sampledOutListeners) {
private void notifyTrackPointsTableUpdate(boolean updateSamplingState, Set<TrackDataListener> sampledInListeners, Set<TrackDataListener> sampledOutListeners) {
if (sampledInListeners.isEmpty() && sampledOutListeners.isEmpty()) {
return;
}
if (updateSamplingState && numLoadedPoints >= targetNumPoints) {
// Reload and resample the track at a lower frequency.
Log.i(TAG, "Resampling track after " + numLoadedPoints + " points.");
@@ -477,11 +447,8 @@ public class TrackDataHub implements DataSourceListener {
long lastTrackPointId = contentProviderUtils.getLastTrackPointId(selectedTrackId);
int samplingFrequency = -1;
boolean includeNextPoint = false;
LocationIterator locationIterator = null;
try {
locationIterator = contentProviderUtils.getTrackPointLocationIterator(selectedTrackId,
localLastSeenLocationId + 1, false, ContentProviderUtils.DEFAULT_LOCATION_FACTORY);
try (LocationIterator locationIterator = contentProviderUtils.getTrackPointLocationIterator(selectedTrackId, localLastSeenLocationId + 1, false, ContentProviderUtils.DEFAULT_LOCATION_FACTORY)) {
while (locationIterator.hasNext()) {
Location location = locationIterator.next();
@@ -509,8 +476,7 @@ public class TrackDataHub implements DataSourceListener {
}
} else {
// Also include the last point if the selected track is not recording.
if (includeNextPoint || (localNumLoadedPoints % samplingFrequency == 0)
|| (locationId == lastTrackPointId && !isSelectedTrackRecording())) {
if (includeNextPoint || (localNumLoadedPoints % samplingFrequency == 0) || (locationId == lastTrackPointId && !isSelectedTrackRecording())) {
includeNextPoint = false;
for (TrackDataListener trackDataListener : sampledInListeners) {
trackDataListener.onSampledInTrackPoint(location);
@@ -525,10 +491,6 @@ public class TrackDataHub implements DataSourceListener {
localNumLoadedPoints++;
localLastSeenLocationId = locationId;
}
} finally {
if (locationIterator != null) {
locationIterator.close();
}
}
if (updateSamplingState) {
@@ -555,7 +517,7 @@ public class TrackDataHub implements DataSourceListener {
* Creates a {@link DataSource}.
*/
@VisibleForTesting
protected DataSource newDataSource() {
private DataSource newDataSource() {
return new DataSource(context);
}
@@ -565,7 +527,7 @@ public class TrackDataHub implements DataSourceListener {
* @param runnable the runnable
*/
@VisibleForTesting
protected void runInHanderThread(Runnable runnable) {
private void runInHandlerThread(Runnable runnable) {
if (handler == null) {
// Use a Throwable to ensure the stack trace is logged.
Log.d(TAG, "handler is null.", new Throwable());
@@ -57,10 +57,8 @@ public interface TrackDataListener {
void onSegmentSplit(Location location);
/**
* Called when finish sending new track points. This gets called after every
* batch of calls to {@link #onSampledInTrackPoint(Location)},
* {@link #onSampledOutTrackPoint(Location)} and
* {@link #onSegmentSplit(Location)}.
* Called when finish sending new track points.
* This gets called after every batch of calls to {@link #onSampledInTrackPoint(Location)}, {@link #onSampledOutTrackPoint(Location)} and {@link #onSegmentSplit(Location)}.
*/
void onNewTrackPointsDone();
@@ -77,9 +75,8 @@ public interface TrackDataListener {
void onNewWaypoint(Waypoint waypoint);
/**
* Called when finish sending new waypoints. This gets called after every
* batch of calls to {@link #clearWaypoints()} and
* {@link #onNewWaypoint(Waypoint)}.
* Called when finish sending new waypoints.
* This gets called after every batch of calls to {@link #clearWaypoints()} and {@link #onNewWaypoint(Waypoint)}.
*/
void onNewWaypointsDone();
@@ -26,24 +26,19 @@ import java.util.Map;
import java.util.Set;
/**
* Manages register/unregister {@link TrackDataListener} and keeping the state
* for each registered listener.
* Manages register/unregister {@link TrackDataListener} and keeping the state for each registered listener.
*
* @author Rodrigo Damazio
*/
public class TrackDataManager {
class TrackDataManager {
private static final String TAG = TrackDataManager.class.getSimpleName();
// Map of listener to its track data types
private final Map<TrackDataListener, EnumSet<TrackDataType>>
listenerToTypesMap = new HashMap<>();
private final Map<TrackDataListener, EnumSet<TrackDataType>> listenerToTypesMap = new HashMap<>();
// Map of track data type to listeners
private final Map<TrackDataType, Set<TrackDataListener>>
typeToListenersMap = new EnumMap<>(TrackDataType.class);
private final Map<TrackDataType, Set<TrackDataListener>> typeToListenersMap = new EnumMap<>(TrackDataType.class);
public TrackDataManager() {
TrackDataManager() {
for (TrackDataType trackDataType : TrackDataType.values()) {
typeToListenersMap.put(trackDataType, new LinkedHashSet<TrackDataListener>());
}
@@ -55,8 +50,7 @@ public class TrackDataManager {
* @param listener the listener
* @param trackDataTypes the track data types the listener is interested
*/
public void registerListener(
TrackDataListener listener, EnumSet<TrackDataType> trackDataTypes) {
void registerListener(TrackDataListener listener, EnumSet<TrackDataType> trackDataTypes) {
if (listenerToTypesMap.containsKey(listener)) {
Log.w(TAG, "Tried to register a listener that is already registered. Ignore.");
return;
@@ -72,7 +66,7 @@ public class TrackDataManager {
*
* @param listener the listener
*/
public void unregisterListener(TrackDataListener listener) {
void unregisterListener(TrackDataListener listener) {
EnumSet<TrackDataType> removedTypes = listenerToTypesMap.remove(listener);
if (removedTypes == null) {
Log.w(TAG, "Tried to unregister a listener that is not registered. Ignore.");
@@ -88,7 +82,7 @@ public class TrackDataManager {
/**
* Gets the number of {@link TrackDataListener}.
*/
public int getNumberOfListeners() {
int getNumberOfListeners() {
return listenerToTypesMap.size();
}
@@ -97,7 +91,7 @@ public class TrackDataManager {
*
* @param listener the listener
*/
public EnumSet<TrackDataType> getTrackDataTypes(TrackDataListener listener) {
EnumSet<TrackDataType> getTrackDataTypes(TrackDataListener listener) {
return listenerToTypesMap.get(listener);
}
@@ -106,14 +100,14 @@ public class TrackDataManager {
*
* @param type the type
*/
public Set<TrackDataListener> getListeners(TrackDataType type) {
Set<TrackDataListener> getListeners(TrackDataType type) {
return typeToListenersMap.get(type);
}
/**
* Gets all the registered {@link TrackDataType}.
*/
public EnumSet<TrackDataType> getRegisteredTrackDataTypes() {
EnumSet<TrackDataType> getRegisteredTrackDataTypes() {
EnumSet<TrackDataType> types = EnumSet.noneOf(TrackDataType.class);
for (EnumSet<TrackDataType> value : listenerToTypesMap.values()) {
types.addAll(value);
@@ -19,8 +19,6 @@ package de.dennisguse.opentracks.content;
import android.net.Uri;
import android.provider.BaseColumns;
import de.dennisguse.opentracks.BuildConfig;
/**
* Constants for the track points table.
*
@@ -29,7 +27,7 @@ import de.dennisguse.opentracks.BuildConfig;
public interface TrackPointsColumns extends BaseColumns {
String TABLE_NAME = "trackpoints";
Uri CONTENT_URI = Uri.parse("content://" + BuildConfig.APPLICATION_ID + "/trackpoints");
Uri CONTENT_URI = Uri.parse(ContentProviderUtils.CONTENT_BASE_URI + "/trackpoints");
String CONTENT_TYPE = "vnd.android.cursor.dir/vnd.de.dennisguse.trackpoint";
String CONTENT_ITEMTYPE = "vnd.android.cursor.item/vnd.de.dennisguse.trackpoint";
String DEFAULT_SORT_ORDER = "_id";
@@ -19,8 +19,6 @@ package de.dennisguse.opentracks.content;
import android.net.Uri;
import android.provider.BaseColumns;
import de.dennisguse.opentracks.BuildConfig;
/**
* Constants for the tracks table.
*
@@ -29,25 +27,9 @@ import de.dennisguse.opentracks.BuildConfig;
public interface TracksColumns extends BaseColumns {
String TABLE_NAME = "tracks";
/**
* Tracks provider uri.
*/
Uri CONTENT_URI = Uri.parse("content://" + BuildConfig.APPLICATION_ID + "/tracks");
/**
* Track content type.
*/
Uri CONTENT_URI = Uri.parse(ContentProviderUtils.CONTENT_BASE_URI + "/tracks");
String CONTENT_TYPE = "vnd.android.cursor.dir/vnd.de.dennisguse.track";
/**
* Track id content type.
*/
String CONTENT_ITEMTYPE = "vnd.android.cursor.item/vnd.de.dennisguse.track";
/**
* Tracks table default sort order.
*/
String DEFAULT_SORT_ORDER = "_id";
// Columns
@@ -18,8 +18,6 @@ package de.dennisguse.opentracks.content;
import android.location.Location;
import android.net.Uri;
import android.os.Parcel;
import android.os.Parcelable;
import de.dennisguse.opentracks.stats.TripStatistics;
@@ -29,19 +27,8 @@ import de.dennisguse.opentracks.stats.TripStatistics;
* @author Leif Hendrik Wilden
* @author Rodrigo Damazio
*/
public final class Waypoint implements Parcelable {
public final class Waypoint {
public static final Parcelable.Creator<Waypoint> CREATOR = new Parcelable.Creator<Waypoint>() {
@Override
public Waypoint createFromParcel(Parcel in) {
return new Waypoint(in);
}
@Override
public Waypoint[] newArray(int size) {
return new Waypoint[size];
}
};
private long id = -1L;
private String name = "";
private String description = "";
@@ -60,9 +47,7 @@ public final class Waypoint implements Parcelable {
public Waypoint() {
}
public Waypoint(String name, String description, String category, String icon, long trackId,
WaypointType type, double length, long duration, long startId, long stopId, Location location,
TripStatistics tripStatistics, String photoUrl) {
public Waypoint(String name, String description, String category, String icon, long trackId, WaypointType type, double length, long duration, long startId, long stopId, Location location, TripStatistics tripStatistics, String photoUrl) {
this.name = name;
this.description = description;
this.category = category;
@@ -78,60 +63,6 @@ public final class Waypoint implements Parcelable {
this.photoUrl = photoUrl;
}
private Waypoint(Parcel source) {
id = source.readLong();
name = source.readString();
description = source.readString();
category = source.readString();
icon = source.readString();
trackId = source.readLong();
type = WaypointType.values()[source.readInt()];
length = source.readDouble();
duration = source.readLong();
startId = source.readLong();
stopId = source.readLong();
ClassLoader classLoader = getClass().getClassLoader();
byte hasLocation = source.readByte();
if (hasLocation > 0) {
location = source.readParcelable(classLoader);
}
byte hasStats = source.readByte();
if (hasStats > 0) {
tripStatistics = source.readParcelable(classLoader);
}
photoUrl = source.readString();
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeLong(id);
dest.writeString(name);
dest.writeString(description);
dest.writeString(category);
dest.writeString(icon);
dest.writeLong(trackId);
dest.writeInt(type.ordinal());
dest.writeDouble(length);
dest.writeLong(duration);
dest.writeLong(startId);
dest.writeLong(stopId);
dest.writeByte(location == null ? (byte) 0 : (byte) 1);
if (location != null) {
dest.writeParcelable(location, 0);
}
dest.writeByte(tripStatistics == null ? (byte) 0 : (byte) 1);
if (tripStatistics != null) {
dest.writeParcelable(tripStatistics, 0);
}
dest.writeString(photoUrl);
}
public long getId() {
return id;
}
@@ -228,6 +159,10 @@ public final class Waypoint implements Parcelable {
this.location = location;
}
public boolean isTripStatistics() {
return tripStatistics != null;
}
public TripStatistics getTripStatistics() {
return tripStatistics;
}
@@ -15,9 +15,6 @@
*/
package de.dennisguse.opentracks.content;
import android.os.Parcel;
import android.os.Parcelable;
import de.dennisguse.opentracks.content.Waypoint.WaypointType;
/**
@@ -25,18 +22,19 @@ import de.dennisguse.opentracks.content.Waypoint.WaypointType;
*
* @author Sandor Dornbush
*/
public class WaypointCreationRequest implements Parcelable {
public class WaypointCreationRequest {
public final static WaypointCreationRequest DEFAULT_WAYPOINT = new WaypointCreationRequest(WaypointType.WAYPOINT, false);
public final static WaypointCreationRequest DEFAULT_STATISTICS = new WaypointCreationRequest(WaypointType.STATISTICS, false);
public final static WaypointCreationRequest DEFAULT_START_TRACK = new WaypointCreationRequest(WaypointType.STATISTICS, true);
public final static WaypointCreationRequest DEFAULT_WAYPOINT = new WaypointCreationRequest(
WaypointType.WAYPOINT, false);
public final static WaypointCreationRequest DEFAULT_STATISTICS = new WaypointCreationRequest(
WaypointType.STATISTICS, false);
public final static WaypointCreationRequest DEFAULT_START_TRACK = new WaypointCreationRequest(
WaypointType.STATISTICS, true);
public static final Creator CREATOR = new Creator();
private WaypointType type;
// true if this marker contains the track statistics
private boolean isTrackStatistics;
private String name;
private String category;
private String description;
@@ -47,8 +45,7 @@ public class WaypointCreationRequest implements Parcelable {
this(type, isTrackStatistics, null, null, null, null, null);
}
public WaypointCreationRequest(WaypointType type, boolean isTrackStatistics, String name,
String category, String description, String iconUrl, String photoUrl) {
public WaypointCreationRequest(WaypointType type, boolean isTrackStatistics, String name, String category, String description, String iconUrl, String photoUrl) {
this.type = type;
this.isTrackStatistics = isTrackStatistics;
this.name = name;
@@ -58,22 +55,6 @@ public class WaypointCreationRequest implements Parcelable {
this.photoUrl = photoUrl;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel parcel, int arg1) {
parcel.writeInt(type.ordinal());
parcel.writeByte((byte) (isTrackStatistics ? 1 : 0));
parcel.writeString(name);
parcel.writeString(category);
parcel.writeString(description);
parcel.writeString(iconUrl);
parcel.writeString(photoUrl);
}
public WaypointType getType() {
return type;
}
@@ -101,23 +82,4 @@ public class WaypointCreationRequest implements Parcelable {
public String getPhotoUrl() {
return photoUrl;
}
public static class Creator implements Parcelable.Creator<WaypointCreationRequest> {
@Override
public WaypointCreationRequest createFromParcel(Parcel source) {
return new WaypointCreationRequest(
WaypointType.values()[source.readInt()],
source.readByte() == 1,
source.readString(),
source.readString(),
source.readString(),
source.readString(),
source.readString());
}
public WaypointCreationRequest[] newArray(int size) {
return new WaypointCreationRequest[size];
}
}
}
@@ -19,8 +19,6 @@ package de.dennisguse.opentracks.content;
import android.net.Uri;
import android.provider.BaseColumns;
import de.dennisguse.opentracks.BuildConfig;
/**
* Constants for waypoints table.
*
@@ -29,7 +27,7 @@ import de.dennisguse.opentracks.BuildConfig;
public interface WaypointsColumns extends BaseColumns {
String TABLE_NAME = "waypoints";
Uri CONTENT_URI = Uri.parse("content://" + BuildConfig.APPLICATION_ID + "/waypoints");
Uri CONTENT_URI = Uri.parse(ContentProviderUtils.CONTENT_BASE_URI + "/waypoints");
String CONTENT_TYPE = "vnd.android.cursor.dir/vnd.de.dennisguse.waypoint";
String CONTENT_ITEMTYPE = "vnd.android.cursor.item/vnd.de.dennisguse.waypoint";
String DEFAULT_SORT_ORDER = "_id";
@@ -1,48 +0,0 @@
/*
* Copyright 2013 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 de.dennisguse.opentracks.fragments;
import android.app.Dialog;
import android.content.DialogInterface;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.fragment.app.DialogFragment;
import de.dennisguse.opentracks.util.DialogUtils;
/**
* @author Jimmy Shih
*/
public abstract class AbstractDialogFragment extends DialogFragment {
@NonNull
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
final Dialog dialog = createDialog();
dialog.setOnShowListener(new DialogInterface.OnShowListener() {
@Override
public void onShow(DialogInterface dialogInterface) {
DialogUtils.setDialogTitleDivider(getActivity(), dialog);
}
});
return dialog;
}
protected abstract Dialog createDialog();
}

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