Compare commits

..

112 Commits

Author SHA1 Message Date
Dennis Guse 3aaee270eb Release v3.2.4 2019-11-01 23:54:34 +01:00
Dennis Guse d53a7156e2 Bugfix: removed custom dialogs to set voice/split frequency preferences from the context menu directly.
Fixed #49.
2019-11-01 23:38:07 +01:00
Dennis Guse 3af24ea3d0 Release v3.2.3 2019-10-31 22:07:21 +01:00
Dennis Guse 5483212b06 Fixed StringUtils.formatDecimal to work with decimals of 0. 2019-10-30 23:02:02 +01:00
Dennis Guse 419279fe5e Cleanup. 2019-10-30 22:36:22 +01:00
Dennis Guse 501e899b27 StatsFragment: need to reconnect to TrackRecordingService in onResume as it is unbound in onStop.
Fixed #50.
2019-10-30 22:36:16 +01:00
Dennis Guse 3bb9edb009 Connect to new sensor if settings are changed.
Fixed #52.
2019-10-30 07:48:29 +01:00
Dennis Guse e52b1188a3 Cleanup. 2019-10-29 23:05:17 +01:00
Dennis Guse 9e877e4762 Removed stale verify. 2019-10-29 22:39:51 +01:00
Dennis Guse 5711757110 If sharing multiple tracks, share them as separate files. 2019-10-29 17:43:23 +01:00
Dennis Guse e1c33dad43 Move to Android ShareSheet. 2019-10-29 17:34:21 +01:00
Dennis Guse 4b2400774c TravisCI: start emulator, which is required for testing. 2019-10-28 23:25:07 +01:00
Dennis Guse afd4a59622 CI: enabled tests.
Fixed #6.
2019-10-27 16:50:28 +01:00
Dennis Guse bdce437470 Removed AnnoucementPeriodTaskTest as these were broken beyond repair. 2019-10-27 16:46:56 +01:00
Dennis Guse 33479cda11 Fixed TrackRecordingService tests: start service properly if needed. 2019-10-27 15:58:50 +01:00
Dennis Guse 385cee3dc7 Fixed Date parsing test (account for
rounding up microseconds).
2019-10-27 13:45:43 +01:00
Dennis Guse d0bbd7655b Ignore KML waypoints that do not have a type. 2019-10-27 13:40:06 +01:00
Dennis Guse a83b8f62fe Repaired Track importer tests. 2019-10-27 13:24:20 +01:00
Dennis Guse 0cd9176703 Heart rate must be larger than zero to be shown while tracking. 2019-10-26 14:35:03 +02:00
Dennis Guse d8e526923d Removed RemoteSensorManager as abstraction was not used anymore. 2019-10-26 14:26:16 +02:00
Dennis Guse 3a6f417662 Added disconnect toast message. 2019-10-26 14:23:38 +02:00
Dennis Guse 7d807a7f11 Refactor Bluetooth LE stack to use autoReconnect.
Removed additional timerTask to reconnect (was not working anymore).

Fixed #41.
2019-10-26 14:11:55 +02:00
Dennis Guse e9d5718495 Cleanup. 2019-10-23 18:47:32 +02:00
Dennis Guse 0dd7f74a11 Removed temporary sensor manager (was not used anymore). 2019-10-22 21:15:11 +02:00
Dennis Guse 9bd4ba489c Release: v3.2.2 2019-10-21 23:00:13 +02:00
Dennis Guse dd55b50dad Re-added actionbar (actually toolbar) to SettingsActivity. 2019-10-21 22:54:23 +02:00
Dennis Guse 1719d21a5b Bugfix: export now really includes sensor data (as double) and import is done as float. 2019-10-21 22:34:33 +02:00
Dennis Guse 75ef8d99d4 SensorDataSet: data unavailable is now NaN. 2019-10-21 22:34:28 +02:00
Dennis Guse ae06fd1680 Bugfix: export sensor data if available and allowed. 2019-10-20 18:39:31 +02:00
Dennis Guse 2b03f7dea8 Use LinearLayout for SearchActivity (show toolbar correctly). 2019-10-20 17:05:09 +02:00
Dennis Guse 7591b012b4 Extracted ShareActionProvider setup code into a separate class. 2019-10-20 16:55:28 +02:00
Dennis Guse 0e089f49f3 Re-added application logo to material design toolbar. 2019-10-19 12:27:11 +02:00
Dennis Guse 5ad1856b13 Removed context menu from ListActivities. 2019-10-17 21:32:55 +02:00
Dennis Guse 965dbbe829 Removed custom action bar style. 2019-10-17 21:26:31 +02:00
Dennis Guse 262c9886df Removed aggregated stats activity. 2019-10-17 21:22:07 +02:00
Dennis Guse ac5a9ea65d Use Androidx compat's toolbar instead of actionbars.
ListView's multiselect still uses actionbar.
2019-10-17 21:19:14 +02:00
Dennis Guse ed9b4c8f3d Add workaround for Compat's ShareActionProvider, which is not available when inflating in a AbsListView.MultiChoiceModeListener. 2019-10-17 18:33:45 +02:00
Dennis Guse 209aa6a8a5 Hide share action from SearchActivity as it is not (yet) functional. 2019-10-17 07:58:11 +02:00
Dennis Guse 81ee6ac1fa Do not use custom style for AlertDialogs, so PreferenceDialogs and AlertDialogs look the same. 2019-10-17 06:38:05 +02:00
Dennis Guse 024dd603cb Migration to Androidx PreferenceCompat. 2019-10-17 06:00:47 +02:00
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
475 changed files with 5529 additions and 9043 deletions
+38 -5
View File
@@ -1,12 +1,45 @@
#See https://docs.travis-ci.com/user/languages/android/
#See https://github.com/ankidroid/Anki-Android/blob/master/.travis.yml
language: android language: android
dist: trusty
env:
global:
- ADB_INSTALL_TIMEOUT=8
- ABI=armeabi-v7a
- EMU_FLAVOR=default
# PATH order is incredibly important. e.g. the 'emulator' script exists in more than one place!
- ANDROID_HOME=/usr/local/android-sdk
- TOOLS=${ANDROID_HOME}/tools
- PATH=${ANDROID_HOME}:${ANDROID_HOME}/emulator:${TOOLS}:${TOOLS}/bin:${ANDROID_HOME}/platform-tools:${PATH}
matrix:
- API=27 ABI=x86_64
android: android:
components: components:
# installing tools to start, then use `sdkmanager` below to get the rest
- tools
- build-tools-28.0.3 - build-tools-28.0.3
- android-29 - android-28
# Emulator Management: Create, Start and Wait
install:
- echo y | sdkmanager "platform-tools"
- echo y | sdkmanager --channel=4 "emulator"
- echo y | sdkmanager "system-images;android-$API;$EMU_FLAVOR;$ABI"
- echo no | avdmanager create avd --force -n test -k "system-images;android-$API;$EMU_FLAVOR;$ABI" -c 10M
- emulator -verbose -avd test -no-accel -no-snapshot -no-window -camera-back none -camera-front none -selinux permissive -qemu -m 2048 &
- android-wait-for-emulator
- adb shell input keyevent 82 &
script: script:
- ./gradlew build - ./gradlew clean build
#TODO: When all tests are passing, CI should also execute them. - ./gradlew connectedCheck
#- ./gradlew connectedCheck
sbefore_cache:
- rm -f $HOME/.gradle/caches/modules-2/modules-2.lock
- rm -fr $HOME/.gradle/caches/*/plugin-resolution/
cache:
directories:
- $HOME/.gradle/caches/
- $HOME/.gradle/wrapper/
- $HOME/.android/build-cache
+2 -2
View File
@@ -8,5 +8,5 @@
4. Create changelog (`_version code_.txt`) 4. Create changelog (`_version code_.txt`)
5. Create commit with all changes 5. Create commit with all changes
6. Push commits 6. Push commits
7. Tag newly create commit with _version name_ 7. Tag newly create commit with _version name_ (e.g., v3.2.1)
8. Push the tagged to public repository 8. Push the tagged commit to public repository
+12 -8
View File
@@ -1,21 +1,21 @@
# OpenTracks: a sport tracker # <img src="drawable-svg/LOGO.svg" alt="OpenTracks logo" height="40"></img> OpenTracks: a sport tracker
![OpenTracks logo](LOGO.svg)
_OpenTracks_ is a sport tracking application that completely respects your privacy. _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: ## Features:
* __Tracking:__ track your sport and outdoor activities * __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 * __Heart rate:__ supports bluetooth LE sensors
* __Export data:__ Export tracks either as KML or GPX * __Export data:__ export tracks either as KMZ (incl. photos), KML or GPX
* __Voice annoucements__
* __No Internet access:__ Internet is not used * __No Internet access:__ Internet is not used
* __Voice annoucements__
* __No advertising__ * __No advertising__
* __Privacy:__ * __Privacy:__
* _contains in-app analytics_ * _does not contain any in-app analytics_
* _does not have Google Play Services_ * _does not have Google Play Services_
* _has no cloud integration_ * _has no cloud integration_
@@ -26,6 +26,10 @@ _Please note:_
_Tracks_ does not contain a _map_ (as this would required Internet access). _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. 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 ## 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/)).
+3 -5
View File
@@ -76,14 +76,14 @@ android {
variant.resValue "string", "applicationId", variant.applicationId variant.resValue "string", "applicationId", variant.applicationId
} }
//TODO: Should be removed (when LINT does not throw any errors).
lintOptions { lintOptions {
abortOnError false disable "AppCompatResource" // TODO Disable complaint to use androidx's SearchView as it is null.
} }
} }
dependencies { 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:core:1.2.0' androidTestImplementation 'androidx.test:core:1.2.0'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
@@ -95,6 +95,4 @@ dependencies {
testImplementation 'androidx.test:core:1.2.0' testImplementation 'androidx.test:core:1.2.0'
testImplementation 'org.mockito:mockito-core:3.0.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,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,13 @@
v3.2.2: OpenTracks
Changes:
- Removed aggregated stats
- Migrated to Material Design
Bugfix:
- Fixed export of sensor data
- Fixed import of incomplete sensor data
Developer:
- Replaced actionbar with toolbar
- Migrated to androidx appcompat (incl. Material Design)
@@ -0,0 +1,14 @@
v3.2.3: OpenTracks
Changes:
- Sharing: use Android Sharesheet
- Sharing: share multiple tracks as individual files
- Use Android ShareSheet
- Heart rate sensor (Bluetooth LE): automatic reconnect
Bugfix:
- Heart rate sensor (Bluetooth LE): disconnect properly
- Heart rate wasn't always shown while recording
Developer:
- Enabled TravisCI instrumentation tests
@@ -0,0 +1,5 @@
v3.2.4: OpenTracks
Bugfix:
- Settings activity crashes
Removed possibility to change voice/split frequency from the context menu
@@ -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: Features:
* Sport tracking: track your sport and outdoor activities * Sport tracking: track your sport and outdoor activities
* Photos and markers: mark interesting locations while tracking * Photos and markers: mark interesting locations while tracking
* Heart rate: supports bluetooth LE sensors * Heart rate: supports Bluetooth LE sensors
* Export data: Export tracks either as KML or GPX * Export data: export tracks either as KML or GPX
* No Internet access: Internet is not used * 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 @@
OpenTracks
Binary file not shown.
+1 -2
View File
@@ -1,6 +1,5 @@
#Mon Aug 26 18:59:08 CEST 2019
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.5-bin.zip
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.6-all.zip
Vendored
+19 -3
View File
@@ -1,5 +1,21 @@
#!/usr/bin/env sh #!/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.
#
############################################################################## ##############################################################################
## ##
## Gradle start up script for UN*X ## Gradle start up script for UN*X
@@ -28,7 +44,7 @@ APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"` 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. # 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"' DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value. # Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum" MAX_FD="maximum"
@@ -109,8 +125,8 @@ if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi fi
# For Cygwin, switch paths to Windows format before running java # For Cygwin or MSYS, switch paths to Windows format before running java
if $cygwin ; then if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"` APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"` JAVACMD=`cygpath --unix "$JAVACMD"`
Vendored
+17 -1
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 @if "%DEBUG%" == "" @echo off
@rem ########################################################################## @rem ##########################################################################
@rem @rem
@@ -14,7 +30,7 @@ set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME% 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. @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" set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe @rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome if defined JAVA_HOME goto findJavaFromJavaHome
@@ -1,12 +0,0 @@
(tbd): OpenTracks
Changes:
- Added show track in map apps (tested with OsmAnd and Maps.ME)
- Removed landscape mode for track detail view (was buggy)
- Removed search suggestions history
Bugfix:
-
Developer:
-
@@ -87,6 +87,8 @@ public class BootReceiverTest {
*/ */
private boolean isServiceExisted(Context context) { private boolean isServiceExisted(Context context) {
ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
Assert.assertNotNull(activityManager);
List<ActivityManager.RunningServiceInfo> serviceList = activityManager.getRunningServices(Integer.MAX_VALUE); List<ActivityManager.RunningServiceInfo> serviceList = activityManager.getRunningServices(Integer.MAX_VALUE);
for (int i = 0; i < serviceList.size(); i++) { for (int i = 0; i < serviceList.size(); i++) {
@@ -23,8 +23,7 @@ import de.dennisguse.opentracks.content.Track;
import de.dennisguse.opentracks.content.sensor.SensorDataSet; import de.dennisguse.opentracks.content.sensor.SensorDataSet;
/** /**
* Commons utilities for creating stubs of track, location. The class will be * Commons utilities for creating stubs of track, location.
* enriched if needs more similar stubs for test.
* *
* @author Youtao Liu * @author Youtao Liu
*/ */
@@ -50,8 +49,7 @@ public class TrackStubUtils {
public static Track createTrack(int numberOfLocations) { public static Track createTrack(int numberOfLocations) {
Track track = new Track(); Track track = new Track();
for (int i = 0; i < numberOfLocations; i++) { for (int i = 0; i < numberOfLocations; i++) {
track.addLocation(createSensorDataSetLocation(INITIAL_LATITUDE + i * DIFFERENCE, INITIAL_LONGITUDE track.addLocation(createSensorDataSetLocation(INITIAL_LATITUDE + i * DIFFERENCE, INITIAL_LONGITUDE + i * DIFFERENCE, INITIAL_ALTITUDE + i * DIFFERENCE));
+ i * DIFFERENCE, INITIAL_ALTITUDE + i * DIFFERENCE));
} }
return track; return track;
@@ -71,7 +69,7 @@ public class TrackStubUtils {
* *
* @return a SensorDataSetLocation stub. * @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 // Initial Location
Location loc = new Location(LOCATION_PROVIDER); Location loc = new Location(LOCATION_PROVIDER);
loc.setLatitude(latitude); loc.setLatitude(latitude);
@@ -85,5 +83,4 @@ public class TrackStubUtils {
return new SensorDataSetLocation(loc, sd); return new SensorDataSetLocation(loc, sd);
} }
} }
@@ -16,6 +16,7 @@
package de.dennisguse.opentracks.content; package de.dennisguse.opentracks.content;
import android.content.ContentResolver; import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues; import android.content.ContentValues;
import android.content.Context; import android.content.Context;
import android.database.Cursor; import android.database.Cursor;
@@ -39,7 +40,6 @@ import de.dennisguse.opentracks.content.ContentProviderUtils.LocationIterator;
import de.dennisguse.opentracks.content.Waypoint.WaypointType; import de.dennisguse.opentracks.content.Waypoint.WaypointType;
import de.dennisguse.opentracks.stats.TripStatistics; import de.dennisguse.opentracks.stats.TripStatistics;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
/** /**
@@ -54,11 +54,12 @@ public class CustomContentProviderUtilsImplTest {
private static final String MOCK_DESC = "Mock Next Waypoint Desc!"; private static final String MOCK_DESC = "Mock Next Waypoint Desc!";
private static final String TEST_DESC = "Test Desc!"; private static final String TEST_DESC = "Test Desc!";
private static final String TEST_DESC_NEW = "Test Desc new!"; private static final String TEST_DESC_NEW = "Test Desc new!";
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 Context context = ApplicationProvider.getApplicationContext();
private ContentProviderUtils providerUtils; private ContentProviderUtils providerUtils;
private double INITIAL_LATITUDE = 37.0;
private double INITIAL_LONGITUDE = -57.0;
private double ALTITUDE_INTERVAL = 2.5;
@Mock @Mock
private Cursor cursorMock; private Cursor cursorMock;
@@ -135,8 +136,7 @@ public class CustomContentProviderUtilsImplTest {
long lastPointId = initializeTrack(trackId, numPoints); long lastPointId = initializeTrack(trackId, numPoints);
((ContentProviderUtilsImpl) providerUtils).setDefaultCursorBatchSize(batchSize); ((ContentProviderUtilsImpl) providerUtils).setDefaultCursorBatchSize(batchSize);
List<Location> locations = new ArrayList<Location>(numPoints); List<Location> locations = new ArrayList<Location>(numPoints);
LocationIterator it = providerUtils.getTrackPointLocationIterator(trackId, -1L, descending, locationFactory); try (LocationIterator it = providerUtils.getTrackPointLocationIterator(trackId, -1L, descending, locationFactory)) {
try {
while (it.hasNext()) { while (it.hasNext()) {
Location loc = it.next(); Location loc = it.next();
Assert.assertNotNull(loc); Assert.assertNotNull(loc);
@@ -146,8 +146,6 @@ public class CustomContentProviderUtilsImplTest {
: lastPointId - numPoints + locations.size(), it.getLocationId()); : lastPointId - numPoints + locations.size(), it.getLocationId());
} }
Assert.assertEquals(numPoints, locations.size()); Assert.assertEquals(numPoints, locations.size());
} finally {
it.close();
} }
return locations; return locations;
} }
@@ -175,15 +173,12 @@ public class CustomContentProviderUtilsImplTest {
// Load all inserted locations. // Load all inserted locations.
long lastPointId = -1; long lastPointId = -1;
int counter = 0; int counter = 0;
LocationIterator it = providerUtils.getTrackPointLocationIterator(id, -1L, false, ContentProviderUtils.DEFAULT_LOCATION_FACTORY); try (LocationIterator it = providerUtils.getTrackPointLocationIterator(id, -1L, false, ContentProviderUtils.DEFAULT_LOCATION_FACTORY)) {
try {
while (it.hasNext()) { while (it.hasNext()) {
it.next(); it.next();
lastPointId = it.getLocationId(); lastPointId = it.getLocationId();
counter++; counter++;
} }
} finally {
it.close();
} }
Assert.assertTrue(numPoints == 0 || lastPointId > 0); Assert.assertTrue(numPoints == 0 || lastPointId > 0);
@@ -218,7 +213,6 @@ public class CustomContentProviderUtilsImplTest {
Track track = providerUtils.createTrack(cursorMock); Track track = providerUtils.createTrack(cursorMock);
Assert.assertEquals(trackId, track.getId()); Assert.assertEquals(trackId, track.getId());
Assert.assertEquals(name, track.getName()); Assert.assertEquals(name, track.getName());
verify(cursorMock);
} }
/** /**
@@ -268,26 +262,20 @@ public class CustomContentProviderUtilsImplTest {
providerUtils.insertWaypoint(waypoint); providerUtils.insertWaypoint(waypoint);
ContentResolver contentResolver = context.getContentResolver(); ContentResolver contentResolver = context.getContentResolver();
Cursor tracksCursor = contentResolver.query(TracksColumns.CONTENT_URI, null, null, null, Cursor tracksCursor = contentResolver.query(TracksColumns.CONTENT_URI, null, null, null, TracksColumns._ID);
TracksColumns._ID);
Assert.assertEquals(3, tracksCursor.getCount()); Assert.assertEquals(3, tracksCursor.getCount());
Cursor tracksPointsCursor = contentResolver.query(TrackPointsColumns.CONTENT_URI, null, null, Cursor tracksPointsCursor = contentResolver.query(TrackPointsColumns.CONTENT_URI, null, null, null, TrackPointsColumns._ID);
null, TrackPointsColumns._ID);
Assert.assertEquals(20, tracksPointsCursor.getCount()); Assert.assertEquals(20, tracksPointsCursor.getCount());
Cursor waypointCursor = contentResolver.query(WaypointsColumns.CONTENT_URI, null, null, Cursor waypointCursor = contentResolver.query(WaypointsColumns.CONTENT_URI, null, null, null, WaypointsColumns._ID);
null, WaypointsColumns._ID);
Assert.assertEquals(1, waypointCursor.getCount()); Assert.assertEquals(1, waypointCursor.getCount());
// Delete one track. // Delete one track.
providerUtils.deleteTrack(context, trackId); providerUtils.deleteTrack(context, trackId);
// Check whether all data of a track has been deleted. // Check whether all data of a track has been deleted.
tracksCursor = contentResolver.query(TracksColumns.CONTENT_URI, null, null, null, tracksCursor = contentResolver.query(TracksColumns.CONTENT_URI, null, null, null, TracksColumns._ID);
TracksColumns._ID);
Assert.assertEquals(2, tracksCursor.getCount()); Assert.assertEquals(2, tracksCursor.getCount());
tracksPointsCursor = contentResolver.query(TrackPointsColumns.CONTENT_URI, null, null, tracksPointsCursor = contentResolver.query(TrackPointsColumns.CONTENT_URI, null, null, null, TrackPointsColumns._ID);
null, TrackPointsColumns._ID);
Assert.assertEquals(20, tracksPointsCursor.getCount()); Assert.assertEquals(20, tracksPointsCursor.getCount());
waypointCursor = contentResolver.query(WaypointsColumns.CONTENT_URI, null, null, waypointCursor = contentResolver.query(WaypointsColumns.CONTENT_URI, null, null, null, WaypointsColumns._ID);
null, WaypointsColumns._ID);
Assert.assertEquals(0, waypointCursor.getCount()); Assert.assertEquals(0, waypointCursor.getCount());
} }
@@ -372,12 +360,12 @@ public class CustomContentProviderUtilsImplTest {
waypoint.setType(WaypointType.STATISTICS); waypoint.setType(WaypointType.STATISTICS);
waypoint.setTripStatistics(statistics); waypoint.setTripStatistics(statistics);
Location loc = new Location("test"); Location location = new Location("test");
loc.setLatitude(22); location.setLatitude(22);
loc.setLongitude(22); location.setLongitude(22);
loc.setAccuracy((float) 1 / 100.0f); location.setAccuracy((float) 1 / 100.0f);
loc.setAltitude(2.5); location.setAltitude(2.5);
waypoint.setLocation(loc); waypoint.setLocation(location);
providerUtils.insertWaypoint(waypoint); providerUtils.insertWaypoint(waypoint);
ContentProviderUtilsImpl contentProviderUtils = new ContentProviderUtilsImpl(contentResolverMock); ContentProviderUtilsImpl contentProviderUtils = new ContentProviderUtilsImpl(contentResolverMock);
@@ -424,7 +412,6 @@ public class CustomContentProviderUtilsImplTest {
Assert.assertEquals(id, waypoint.getId()); Assert.assertEquals(id, waypoint.getId());
Assert.assertEquals(name, waypoint.getName()); Assert.assertEquals(name, waypoint.getName());
Assert.assertEquals(trackId, waypoint.getTrackId()); Assert.assertEquals(trackId, waypoint.getTrackId());
verify(cursorMock);
} }
/** /**
@@ -487,19 +474,22 @@ public class CustomContentProviderUtilsImplTest {
statistics.setMaxGrade(15.0); statistics.setMaxGrade(15.0);
statistics.setMinGrade(-25.0); statistics.setMinGrade(-25.0);
statistics.setBounds(-10000, 20000, 30000, -40000); statistics.setBounds(-10000, 20000, 30000, -40000);
// Insert at first. // Insert at first.
Waypoint waypoint1 = new Waypoint(); Waypoint waypoint1 = new Waypoint();
waypoint1.setDescription(Long.toString(trackId)); waypoint1.setDescription(Long.toString(trackId));
waypoint1.setTrackId(trackId); waypoint1.setTrackId(trackId);
waypoint1.setType(WaypointType.STATISTICS); waypoint1.setType(WaypointType.STATISTICS);
waypoint1.setTripStatistics(statistics); waypoint1.setTripStatistics(statistics);
providerUtils.insertWaypoint(waypoint1); long waypoint1Id = ContentUris.parseId(providerUtils.insertWaypoint(waypoint1));
Waypoint waypoint2 = new Waypoint(); Waypoint waypoint2 = new Waypoint();
waypoint2.setDescription(Long.toString(trackId)); waypoint2.setDescription(Long.toString(trackId));
waypoint2.setTrackId(trackId); waypoint2.setTrackId(trackId);
waypoint2.setType(WaypointType.STATISTICS); waypoint2.setType(WaypointType.STATISTICS);
waypoint2.setTripStatistics(statistics); waypoint2.setTripStatistics(statistics);
providerUtils.insertWaypoint(waypoint2); long waypoint2Id = ContentUris.parseId(providerUtils.insertWaypoint(waypoint2));
// Delete // Delete
DescriptionGenerator descriptionGenerator = new DescriptionGenerator() { DescriptionGenerator descriptionGenerator = new DescriptionGenerator() {
@@ -513,10 +503,12 @@ public class CustomContentProviderUtilsImplTest {
return null; return null;
} }
}; };
providerUtils.deleteWaypoint(context, 1, descriptionGenerator);
Assert.assertNull(providerUtils.getWaypoint(1)); Assert.assertNotNull(providerUtils.getWaypoint(waypoint1Id));
Assert.assertEquals(CustomContentProviderUtilsImplTest.MOCK_DESC, providerUtils.getWaypoint(2) providerUtils.deleteWaypoint(context, waypoint1Id, descriptionGenerator);
Assert.assertNull(providerUtils.getWaypoint(waypoint1Id));
Assert.assertEquals(CustomContentProviderUtilsImplTest.MOCK_DESC, providerUtils.getWaypoint(waypoint2Id)
.getDescription()); .getDescription());
} }
@@ -531,13 +523,15 @@ public class CustomContentProviderUtilsImplTest {
Waypoint waypoint1 = new Waypoint(); Waypoint waypoint1 = new Waypoint();
waypoint1.setTrackId(trackId); waypoint1.setTrackId(trackId);
long waypoint1id = ContentUris.parseId(providerUtils.insertWaypoint(waypoint1));
Waypoint waypoint2 = new Waypoint(); Waypoint waypoint2 = new Waypoint();
waypoint2.setTrackId(trackId); waypoint2.setTrackId(trackId);
providerUtils.insertWaypoint(waypoint1); long waypoint2id = ContentUris.parseId(providerUtils.insertWaypoint(waypoint2));
providerUtils.insertWaypoint(waypoint2);
Assert.assertEquals(-1L, providerUtils.getFirstWaypointId(-1)); 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());
} }
/** /**
@@ -612,9 +606,9 @@ public class CustomContentProviderUtilsImplTest {
Waypoint waypoint = new Waypoint(); Waypoint waypoint = new Waypoint();
waypoint.setDescription(TEST_DESC); waypoint.setDescription(TEST_DESC);
waypoint.setTrackId(trackId); 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());
} }
/** /**
@@ -629,13 +623,14 @@ public class CustomContentProviderUtilsImplTest {
Waypoint waypoint = new Waypoint(); Waypoint waypoint = new Waypoint();
waypoint.setDescription(TEST_DESC); waypoint.setDescription(TEST_DESC);
waypoint.setTrackId(trackId); waypoint.setTrackId(trackId);
providerUtils.insertWaypoint(waypoint); long waypointId = ContentUris.parseId(providerUtils.insertWaypoint(waypoint));
// Update // Update
waypoint = providerUtils.getWaypoint(1); waypoint = providerUtils.getWaypoint(waypointId);
waypoint.setDescription(TEST_DESC_NEW); waypoint.setDescription(TEST_DESC_NEW);
providerUtils.updateWaypoint(waypoint); providerUtils.updateWaypoint(waypoint);
Assert.assertEquals(TEST_DESC_NEW, providerUtils.getWaypoint(1).getDescription()); Assert.assertEquals(TEST_DESC_NEW, providerUtils.getWaypoint(waypointId).getDescription());
} }
/** /**
@@ -711,7 +706,6 @@ public class CustomContentProviderUtilsImplTest {
Assert.assertEquals((double) latitude, location.getLatitude(), 0.01); Assert.assertEquals((double) latitude, location.getLatitude(), 0.01);
Assert.assertEquals(time, location.getTime(), 0.01); Assert.assertEquals(time, location.getTime(), 0.01);
Assert.assertEquals(speed, location.getSpeed(), 0.01); Assert.assertEquals(speed, location.getSpeed(), 0.01);
verify(cursorMock);
} }
/** /**
@@ -739,7 +733,7 @@ public class CustomContentProviderUtilsImplTest {
Track track = getTrack(trackId, 10); Track track = getTrack(trackId, 10);
insertTrackWithLocations(track); insertTrackWithLocations(track);
Assert.assertEquals(1, providerUtils.getFirstTrackPointId(trackId)); Assert.assertNotEquals(-1L, providerUtils.getFirstTrackPointId(trackId));
} }
/** /**
@@ -752,7 +746,7 @@ public class CustomContentProviderUtilsImplTest {
Track track = getTrack(trackId, 10); Track track = getTrack(trackId, 10);
insertTrackWithLocations(track); insertTrackWithLocations(track);
Assert.assertEquals(10, providerUtils.getLastTrackPointId(trackId)); Assert.assertTrue(providerUtils.getFirstTrackPointId(trackId) < providerUtils.getLastTrackPointId(trackId));
} }
/** /**
@@ -770,35 +764,41 @@ public class CustomContentProviderUtilsImplTest {
} }
/** /**
* Tests the method * Tests the method {@link ContentProviderUtilsImpl#getTrackPointCursor(long, long, int, boolean)} in descending.
* {@link ContentProviderUtilsImpl#getTrackPointCursor(long, long, int, boolean)}
* in descending.
*/ */
@Test @Test
public void testGetTrackPointCursor_desc() { public void testGetTrackPointCursor_desc() {
// Insert track, points at first. // Insert track, points at first.
long trackId = System.currentTimeMillis(); long trackId = System.currentTimeMillis();
Track track = getTrack(trackId, 10); 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()); Assert.assertEquals(2, cursor.getCount());
} }
/** /**
* Tests the method * Tests the method {@link ContentProviderUtilsImpl#getTrackPointCursor(long, long, int, boolean)} in ascending.
* {@link ContentProviderUtilsImpl#getTrackPointCursor(long, long, int, boolean)}
* in ascending.
*/ */
@Test @Test
public void testGetTrackPointCursor_asc() { public void testGetTrackPointCursor_asc() {
// Insert track, points at first. // Insert track, points at first.
long trackId = System.currentTimeMillis(); long trackId = System.currentTimeMillis();
Track track = getTrack(trackId, 10); Track track = getTrack(trackId, 10);
insertTrackWithLocations(track); providerUtils.insertTrack(track);
Cursor cursor = providerUtils.getTrackPointCursor(trackId, 2L, 5, false); long[] trackpointIds = new long[track.getNumberOfPoints()];
Assert.assertEquals(5, cursor.getCount()); 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());
} }
/** /**
@@ -809,40 +809,48 @@ public class CustomContentProviderUtilsImplTest {
// Insert track, points at first. // Insert track, points at first.
long trackId = System.currentTimeMillis(); long trackId = System.currentTimeMillis();
Track track = getTrack(trackId, 10); 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()));
}
long startTrackPointId = trackpointIds[9];
LocationIterator locationIterator = providerUtils.getTrackPointLocationIterator(trackId, startTrackPointId, true, ContentProviderUtils.DEFAULT_LOCATION_FACTORY); LocationIterator locationIterator = providerUtils.getTrackPointLocationIterator(trackId, startTrackPointId, true, ContentProviderUtils.DEFAULT_LOCATION_FACTORY);
for (int i = 1; i >= 0; i--) { for (int i = 0; i < trackpointIds.length; i++) {
Assert.assertTrue(locationIterator.hasNext()); Assert.assertTrue(locationIterator.hasNext());
Location location = locationIterator.next(); Location location = locationIterator.next();
Assert.assertEquals(2 + (i - 1), locationIterator.getLocationId()); Assert.assertEquals(startTrackPointId - i, locationIterator.getLocationId());
checkLocation(i, location); checkLocation((trackpointIds.length - 1) - i, location);
} }
Assert.assertFalse(locationIterator.hasNext()); Assert.assertFalse(locationIterator.hasNext());
} }
/** /**
* Tests the method * Tests the method {@link ContentProviderUtilsImpl#getTrackPointLocationIterator(long, long, boolean, LocationFactory)} in ascending.
* {@link ContentProviderUtilsImpl#getTrackPointLocationIterator(long, long, boolean, LocationFactory)}
* in ascending.
*/ */
@Test @Test
public void testGetTrackPointLocationIterator_asc() { public void testGetTrackPointLocationIterator_asc() {
// Insert track, point at first. // Insert track, point at first.
long trackId = System.currentTimeMillis(); long trackId = System.currentTimeMillis();
Track track = getTrack(trackId, 10); 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()));
}
long startTrackPointId = trackpointIds[0];
LocationIterator locationIterator = providerUtils.getTrackPointLocationIterator(trackId, startTrackPointId, false, ContentProviderUtils.DEFAULT_LOCATION_FACTORY); LocationIterator locationIterator = providerUtils.getTrackPointLocationIterator(trackId, startTrackPointId, false, ContentProviderUtils.DEFAULT_LOCATION_FACTORY);
for (int i = 0; i < trackpointIds.length; i++) {
for (int i = 1; i < 10; i++) {
Assert.assertTrue(locationIterator.hasNext()); Assert.assertTrue(locationIterator.hasNext());
Location location = locationIterator.next(); Location location = locationIterator.next();
Assert.assertEquals(2 + (i - 1), locationIterator.getLocationId()); Assert.assertEquals(startTrackPointId + i, locationIterator.getLocationId());
checkLocation(i, location); checkLocation(i, location);
} }
Assert.assertFalse(locationIterator.hasNext()); Assert.assertFalse(locationIterator.hasNext());
@@ -884,8 +892,7 @@ public class CustomContentProviderUtilsImplTest {
/** /**
* Checks the value of a location. * Checks the value of a location.
* *
* @param i the index of this location which created in the method * @param i the index of this location which created in the method {@link CustomContentProviderUtilsImplTest#getTrack(long, int)}
* {@link CustomContentProviderUtilsImplTest#getTrack(long, int)}
* @param location the location to be checked * @param location the location to be checked
*/ */
private void checkLocation(int i, Location location) { private void checkLocation(int i, Location location) {
@@ -49,7 +49,7 @@ public class DescriptionGeneratorImplTest {
} }
/** /**
* Tests {@link DescriptionGeneratorImpl#generateTrackDescription(Track, java.util.Vector, java.util.Vector, boolean)}. * Tests {@link DescriptionGeneratorImpl#generateTrackDescription(Track, boolean)}.
*/ */
@Test @Test
public void testGenerateTrackDescription() { public void testGenerateTrackDescription() {
@@ -128,8 +128,7 @@ public class DescriptionGeneratorImplTest {
} }
/** /**
* Tests {@link DescriptionGeneratorImpl#writeDistance(double, StringBuilder, * Tests {@link DescriptionGeneratorImpl#writeDistance(double, StringBuilder, int, String)}.
* int, String)}.
*/ */
@Test @Test
public void testWriteDistance() { public void testWriteDistance() {
@@ -139,8 +138,7 @@ public class DescriptionGeneratorImplTest {
} }
/** /**
* Tests {@link DescriptionGeneratorImpl#writeTime(long, StringBuilder, int, * Tests {@link DescriptionGeneratorImpl#writeTime(long, StringBuilder, int, String)}.
* String)}.
*/ */
@Test @Test
public void testWriteTime() { public void testWriteTime() {
@@ -150,8 +148,7 @@ public class DescriptionGeneratorImplTest {
} }
/** /**
* Tests {@link DescriptionGeneratorImpl#writeSpeed(double, StringBuilder, * Tests {@link DescriptionGeneratorImpl#writeSpeed(double, StringBuilder, int, String)}.
* int, String)}.
*/ */
@Test @Test
public void testWriteSpeed() { public void testWriteSpeed() {
@@ -161,8 +158,7 @@ public class DescriptionGeneratorImplTest {
} }
/** /**
* Tests {@link DescriptionGeneratorImpl#writeElevation(double, StringBuilder, * Tests {@link DescriptionGeneratorImpl#writeElevation(double, StringBuilder, int, String)}.
* int, String)}.
*/ */
@Test @Test
public void testWriteElevation() { public void testWriteElevation() {
@@ -172,8 +168,7 @@ public class DescriptionGeneratorImplTest {
} }
/** /**
* Tests {@link DescriptionGeneratorImpl#writePace(double, StringBuilder, int, * Tests {@link DescriptionGeneratorImpl#writePace(double, StringBuilder, int, String)}.
* String)}.
*/ */
@Test @Test
public void testWritePace() { public void testWritePace() {
@@ -183,8 +178,7 @@ public class DescriptionGeneratorImplTest {
} }
/** /**
* Tests {@link DescriptionGeneratorImpl#writeGrade(double, StringBuilder, * Tests {@link DescriptionGeneratorImpl#writeGrade(double, StringBuilder, int, String)}.
* int, String)}.
*/ */
@Test @Test
public void testWriteGrade() { public void testWriteGrade() {
@@ -194,8 +188,7 @@ public class DescriptionGeneratorImplTest {
} }
/** /**
* Tests {@link DescriptionGeneratorImpl#writeGrade(double, StringBuilder, * Tests {@link DescriptionGeneratorImpl#writeGrade(double, StringBuilder, int, String)} with a NaN.
* int, String)} with a NaN.
*/ */
@Test @Test
public void testWriteGrade_nan() { public void testWriteGrade_nan() {
@@ -205,8 +198,7 @@ public class DescriptionGeneratorImplTest {
} }
/** /**
* Tests {@link DescriptionGeneratorImpl#writeGrade(double, StringBuilder, * Tests {@link DescriptionGeneratorImpl#writeGrade(double, StringBuilder, int, String)} with an infinite number.
* int, String)} with an infinite number.
*/ */
@Test @Test
public void testWriteGrade_infinite() { public void testWriteGrade_infinite() {
@@ -40,9 +40,8 @@ import de.dennisguse.opentracks.stats.TripStatistics;
/** /**
* Tests for {@link SearchEngine}. * Tests for {@link SearchEngine}.
* These are not meant to be quality tests, but instead feature-by-feature tests * 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 * In other words, they don't test the mixing of different score boostings, just each boosting separately.
* each boosting separately)
* *
* @author Rodrigo Damazio * @author Rodrigo Damazio
*/ */
@@ -50,7 +49,7 @@ import de.dennisguse.opentracks.stats.TripStatistics;
public class SearchEngineTest { public class SearchEngineTest {
private static final Location HERE = new Location("gps"); 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 ContentProviderUtils providerUtils;
private SearchEngine engine; private SearchEngine engine;
@@ -1,761 +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.location.Location;
import androidx.test.core.app.ApplicationProvider;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
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.EnumSet;
import java.util.HashSet;
import java.util.Set;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.ContentProviderUtils.LocationFactory;
import de.dennisguse.opentracks.content.ContentProviderUtils.LocationIterator;
import de.dennisguse.opentracks.util.PreferencesUtils;
import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.isA;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
* Tests for {@link TrackDataHub}.
*
* @author Rodrigo Damazio
*/
@RunWith(MockitoJUnitRunner.class)
public class TrackDataHubTest {
private static final long TRACK_ID = 42L;
private static final int TARGET_POINTS = 50;
private Context context = ApplicationProvider.getApplicationContext();
private SharedPreferences sharedPreferences;
@Mock
private ContentProviderUtils contentProviderUtils;
@Mock
private DataSource dataSource;
private TrackDataHub trackDataHub;
@Mock
private TrackDataListener trackDataListener1;
@Mock
private TrackDataListener trackDataListener2;
private ArgumentCaptor<OnSharedPreferenceChangeListener> preferenceChangeListenerCapture = ArgumentCaptor.forClass(OnSharedPreferenceChangeListener.class);
@Before
public void setUp() {
sharedPreferences = PreferencesUtils.getSharedPreferences(context);
trackDataHub = new TrackDataHub(context, new TrackDataManager(), contentProviderUtils, TARGET_POINTS) {
@Override
protected DataSource newDataSource() {
return dataSource;
}
@Override
protected void runInHandlerThread(Runnable runnable) {
// Run everything in the same thread
runnable.run();
}
};
PreferencesUtils.setLong(context, R.string.recording_track_id_key, TRACK_ID);
}
@After
public void tearDown() {
reset(dataSource);
// Expect everything to be unregistered.
dataSource.unregisterContentObserver(isA(ContentObserver.class));
//TODO
// AndroidMock.expectLastCall().times(3);
dataSource.unregisterOnSharedPreferenceChangeListener(isA(OnSharedPreferenceChangeListener.class));
verify(dataSource);
trackDataHub.stop();
trackDataHub = null;
}
/**
* Tests registering for tracks table update.
*/
@Test
public void testTracksTableUpdate() {
// Register two listeners
ArgumentCaptor<ContentObserver> contentObserverCapture = ArgumentCaptor.forClass(ContentObserver.class);
Track track = new Track();
when(contentProviderUtils.getTrack(TRACK_ID)).thenReturn(track);
dataSource.registerContentObserver(eq(TracksColumns.CONTENT_URI), contentObserverCapture.capture());
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();
when(contentProviderUtils.getTrack(TRACK_ID)).thenReturn(track);
trackDataListener1.onTrackUpdated(track);
trackDataListener2.onTrackUpdated(track);
replay();
contentObserver.onChange(false);
verifyAndReset();
// Unregister one listener
when(contentProviderUtils.getTrack(TRACK_ID)).thenReturn(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();
}
//TODO
/**
* Tests registering for waypoints table update.
*/
// @Test
// 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
// ArgumentCaptor<ContentObserver> contentObserverCapture = ArgumentCaptor.forClass(ContentObserver.class);
// when(contentProviderUtils.getWaypointCursor(
// eq(TRACK_ID), AndroidMock.leq(-1L), eq(TrackDataHub.MAX_DISPLAYED_WAYPOINTS)))
// .andStubAnswer(new FixedSizeCursorAnswer(2));
// when(contentProviderUtils.createWaypoint(isA(Cursor.class)))
// .thenReturn(waypoint1)
// .thenReturn(waypoint2)
// .thenReturn(waypoint1)
// .thenReturn(waypoint2);
// dataSource.registerContentObserver(eq(WaypointsColumns.CONTENT_BASE_URI), contentObserverCapture.capture());
// 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();
// when(contentProviderUtils.getWaypointCursor(
// eq(TRACK_ID), AndroidMock.leq(-1L), eq(TrackDataHub.MAX_DISPLAYED_WAYPOINTS)))
// .andStubAnswer(new FixedSizeCursorAnswer(3));
// when(contentProviderUtils.createWaypoint(isA(Cursor.class)))
// .thenReturn(waypoint1)
// .thenReturn(waypoint2)
// .thenReturn(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
// when(contentProviderUtils.getWaypointCursor(
// eq(TRACK_ID), AndroidMock.leq(-1L), eq(TrackDataHub.MAX_DISPLAYED_WAYPOINTS)))
// .andStubAnswer(new FixedSizeCursorAnswer(4));
// when(contentProviderUtils.createWaypoint(isA(Cursor.class)))
// .thenReturn(waypoint1)
// .thenReturn(waypoint2)
// .thenReturn(waypoint3)
// .thenReturn(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.
*/
@Test
public void testTrackPointsTableUpdate() {
// Register one listener
ArgumentCaptor<ContentObserver> contentObserverCapture = ArgumentCaptor.forClass(ContentObserver.class);
dataSource.registerContentObserver(eq(TrackPointsColumns.CONTENT_URI), contentObserverCapture.capture());
FixedSizeLocationIterator locationIterator = new FixedSizeLocationIterator(1, 10, 5);
when(contentProviderUtils.getTrackPointLocationIterator(eq(TRACK_ID), eq(0L), eq(false), isA(LocationFactory.class))).thenReturn(locationIterator);
when(contentProviderUtils.getLastTrackPointId(TRACK_ID)).thenReturn(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);
when(contentProviderUtils.getTrackPointLocationIterator(eq(TRACK_ID), eq(0L), eq(false), isA(LocationFactory.class))).thenReturn(locationIterator);
when(contentProviderUtils.getLastTrackPointId(TRACK_ID)).thenReturn(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);
when(contentProviderUtils.getTrackPointLocationIterator(eq(TRACK_ID), eq(11L), eq(false), isA(LocationFactory.class))).thenReturn(locationIterator);
when(contentProviderUtils.getLastTrackPointId(TRACK_ID)).thenReturn(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);
when(contentProviderUtils.getTrackPointLocationIterator(eq(TRACK_ID + 1), eq(0L), eq(false), isA(LocationFactory.class)))
.thenReturn(locationIterator);
when(contentProviderUtils.getLastTrackPointId(TRACK_ID + 1)).thenReturn(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.
*/
@Test
public void testTrackPointsTableUpdate_reRegister() {
// Register one listener
ArgumentCaptor<ContentObserver> contentObserverCapture = ArgumentCaptor.forClass(ContentObserver.class);
dataSource.registerContentObserver(eq(TrackPointsColumns.CONTENT_URI), contentObserverCapture.capture());
FixedSizeLocationIterator locationIterator = new FixedSizeLocationIterator(1, 10, 5);
when(contentProviderUtils.getTrackPointLocationIterator(
eq(TRACK_ID), eq(0L), eq(false), isA(LocationFactory.class))).thenReturn(locationIterator);
when(contentProviderUtils.getLastTrackPointId(TRACK_ID)).thenReturn(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), contentObserverCapture.capture());
locationIterator = new FixedSizeLocationIterator(1, 10, 5);
when(contentProviderUtils.getTrackPointLocationIterator(eq(TRACK_ID), eq(0L), eq(false), isA(LocationFactory.class))).thenReturn(locationIterator);
when(contentProviderUtils.getLastTrackPointId(TRACK_ID)).thenReturn(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.
*/
@Test
public void testTrackPointsTableUpdate_reRegisterAfterTrackChange() {
// Register one listener
ArgumentCaptor<ContentObserver> observerCapture = ArgumentCaptor.forClass(ContentObserver.class);
dataSource.registerContentObserver(eq(TrackPointsColumns.CONTENT_URI), observerCapture.capture());
FixedSizeLocationIterator locationIterator = new FixedSizeLocationIterator(1, 10, 5);
when(contentProviderUtils.getTrackPointLocationIterator(
eq(TRACK_ID), eq(0L), eq(false), isA(LocationFactory.class))).thenReturn(locationIterator);
when(contentProviderUtils.getLastTrackPointId(TRACK_ID)).thenReturn(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), observerCapture.capture());
locationIterator = new FixedSizeLocationIterator(1, 10);
when(contentProviderUtils.getTrackPointLocationIterator(eq(TRACK_ID + 1), eq(0L), eq(false), isA(LocationFactory.class)))
.thenReturn(locationIterator);
when(contentProviderUtils.getLastTrackPointId(TRACK_ID + 1)).thenReturn(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();
}
//TODO
/**
* Tests track points table update with large track sampling.
*/
// @Test
// public void testTrackPointsTableUpdate_largeTrackSampling() {
// ArgumentCaptor<ContentObserver> contentObserverCapture = ArgumentCaptor.forClass(ContentObserver.class);
// dataSource.registerContentObserver(eq(TrackPointsColumns.CONTENT_BASE_URI), contentObserverCapture.capture());
//
// when(contentProviderUtils.getLastTrackPointId(TRACK_ID)).thenReturn(200L);
// AndroidMock.expectLastCall().anyTimes();
// FixedSizeLocationIterator locationIterator1 = new FixedSizeLocationIterator(1, 200, 4, 25, 71, 120);
// when(contentProviderUtils.getTrackPointLocationIterator(eq(TRACK_ID), eq(0L), eq(false), isA(LocationFactory.class))).thenReturn(locationIterator1);
// FixedSizeLocationIterator locationIterator2 = new FixedSizeLocationIterator(1, 200, 4, 25, 71, 120);
// when(contentProviderUtils.getTrackPointLocationIterator(eq(TRACK_ID), eq(0L), eq(false), isA(LocationFactory.class))).thenReturn(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.
*/
@Test
public void testTrackPointsTableUpdate_resampling() {
ArgumentCaptor<ContentObserver> observerCapture = ArgumentCaptor.forClass(ContentObserver.class);
dataSource.registerContentObserver(eq(TrackPointsColumns.CONTENT_URI), observerCapture.capture());
// Deliver 30 points (no sampling happens)
FixedSizeLocationIterator locationIterator = new FixedSizeLocationIterator(1, 30, 5);
when(contentProviderUtils.getTrackPointLocationIterator(
eq(TRACK_ID), eq(0L), eq(false), isA(LocationFactory.class))).thenReturn(locationIterator);
when(contentProviderUtils.getLastTrackPointId(TRACK_ID)).thenReturn(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);
when(contentProviderUtils.getTrackPointLocationIterator(eq(TRACK_ID), eq(31L), eq(false), isA(LocationFactory.class))).thenReturn(locationIterator);
when(contentProviderUtils.getLastTrackPointId(TRACK_ID)).thenReturn(60L);
locationIterator.expectSampledLocationsDelivered(trackDataListener1, 2, false);
trackDataListener1.onNewTrackPointsDone();
replay();
observer.onChange(false);
verifyAndReset();
// Now another 30 (triggers resampling)
locationIterator = new FixedSizeLocationIterator(1, 90);
when(contentProviderUtils.getTrackPointLocationIterator(
eq(TRACK_ID), eq(0L), eq(false), isA(LocationFactory.class))).thenReturn(locationIterator);
when(contentProviderUtils.getLastTrackPointId(TRACK_ID)).thenReturn(90L);
trackDataListener1.clearTrackPoints();
locationIterator.expectSampledLocationsDelivered(trackDataListener1, 2, false);
trackDataListener1.onNewTrackPointsDone();
replay();
observer.onChange(false);
verifyAndReset();
}
/**
* Tests preferences change.
*/
@Test
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(preferenceChangeListenerCapture.capture());
when(trackDataListener1.onMetricUnitsChanged(true)).thenReturn(false);
when(trackDataListener1.onReportSpeedChanged(true)).thenReturn(false);
when(trackDataListener1.onRecordingGpsAccuracy(PreferencesUtils.RECORDING_GPS_ACCURACY_DEFAULT)).thenReturn(false);
when(trackDataListener1.onRecordingDistanceIntervalChanged(PreferencesUtils.RECORDING_DISTANCE_INTERVAL_DEFAULT)).thenReturn(false);
when(trackDataListener2.onMetricUnitsChanged(true)).thenReturn(false);
when(trackDataListener2.onReportSpeedChanged(true)).thenReturn(false);
when(trackDataListener2.onRecordingGpsAccuracy(PreferencesUtils.RECORDING_GPS_ACCURACY_DEFAULT)).thenReturn(false);
when(trackDataListener2.onRecordingDistanceIntervalChanged(PreferencesUtils.RECORDING_DISTANCE_INTERVAL_DEFAULT)).thenReturn(false);
replay();
trackDataHub.start();
trackDataHub.registerTrackDataListener(trackDataListener1, EnumSet.of(TrackDataType.PREFERENCE));
trackDataHub.registerTrackDataListener(trackDataListener2, EnumSet.of(TrackDataType.PREFERENCE));
verifyAndReset();
// Change report speed to false
when(trackDataListener1.onReportSpeedChanged(false)).thenReturn(false);
when(trackDataListener2.onReportSpeedChanged(false)).thenReturn(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
when(trackDataListener1.onMetricUnitsChanged(false)).thenReturn(false);
when(trackDataListener2.onMetricUnitsChanged(false)).thenReturn(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() {
verify(contentProviderUtils);
verify(dataSource);
verify(trackDataListener1);
verify(trackDataListener2);
reset(contentProviderUtils, dataSource, trackDataListener1, trackDataListener2);
}
//TODO
/**
* Tests the method {@link TrackDataHub#start()}.
*/
// @Test
// public void testRegisterTracksTableListener() {
// ArgumentCaptor<ContentObserver> observerCapture = ArgumentCaptor.forClass(ContentObserver.class);
// dataSource.registerContentObserver(eq(TracksColumns.CONTENT_BASE_URI), observerCapture.capture());
// Track track = TrackStubUtils.createTrack(1);
//
// when(contentProviderUtils.getTrack(capture(new Capture<Long>())).thenReturn(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();
// }
//TODO
/**
* Tests the method {@link TrackDataHub#start()}.
*/
// @Test
// public void testRegisterWaypointsTableListener() {
// ArgumentCaptor<ContentObserver> observerCapture = ArgumentCaptor.forClass(ContentObserver.class);
// dataSource.registerContentObserver(eq(WaypointsColumns.CONTENT_BASE_URI), observerCapture.capture());
// when(contentProviderUtils.getWaypointCursor(
// capture(new Capture<Long>()), capture(new Capture<Long>()),
// capture(new Capture<Integer>()))).thenReturn(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.
*/
@Test
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));
Assert.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));
Assert.assertEquals(value + 1, trackDataHub.getRecordingGpsAccuracy());
}
/**
* Tests the method {@link TrackDataHub#notifyPreferenceChanged(String)} when
* the key is R.string.metric_units_key.
*/
@Test
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));
Assert.assertFalse(trackDataHub.isMetricUnits());
PreferencesUtils.setString(context, R.string.stats_units_key, PreferencesUtils.STATS_UNITS_DEFAULT);
trackDataHub.notifyPreferenceChanged(PreferencesUtils.getKey(context, R.string.stats_units_key));
Assert.assertTrue(trackDataHub.isMetricUnits());
}
/**
* Tests the method {@link TrackDataHub#notifyPreferenceChanged(String)} when
* the key is R.string.metric_units_key.
*/
@Test
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));
Assert.assertFalse(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));
Assert.assertTrue(trackDataHub.isReportSpeed());
}
//TODO
/**
* Fixed size cursor answer.
*
* @author Jimmy Shih
*/
private static class FixedSizeCursorAnswer {
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,65 +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 org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import de.dennisguse.opentracks.content.Waypoint.WaypointType;
/**
* Tests for the WaypointCreationRequest class.
* {@link WaypointCreationRequest}
*
* @author Sandor Dornbush
*/
@RunWith(AndroidJUnit4.class)
public class WaypointCreationRequestTest {
@Test
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());
}
@Test
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());
}
}
@@ -52,7 +52,7 @@ public class ChartFragmentTest {
} }
/** /**
* 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 @Test
public void testFillDataPoint_sensorIncorrect() { public void testFillDataPoint_sensorIncorrect() {
@@ -161,12 +161,11 @@ public class ChartFragmentTest {
point = fillDataPointTestHelper(sensorDataSetLocation2); point = fillDataPointTestHelper(sensorDataSetLocation2);
/* /*
* Computes the distance between Latitude 22 and 23. And for we set using * Computes the distance between Latitude 22 and 23.
* imperial, the distance should be multiplied by UnitConversions.KM_TO_MI. * And for we set using * imperial, the distance should be multiplied by UnitConversions.KM_TO_MI.
*/ */
float[] results = new float[4]; float[] results = new float[4];
Location.distanceBetween(sensorDataSetLocation1.getLatitude(), sensorDataSetLocation1.getLongitude(), Location.distanceBetween(sensorDataSetLocation1.getLatitude(), sensorDataSetLocation1.getLongitude(), sensorDataSetLocation2.getLatitude(), sensorDataSetLocation2.getLongitude(), results);
sensorDataSetLocation2.getLatitude(), sensorDataSetLocation2.getLongitude(), results);
double distance1 = results[0] * UnitConversions.M_TO_KM * UnitConversions.KM_TO_MI; double distance1 = results[0] * UnitConversions.M_TO_KM * UnitConversions.KM_TO_MI;
Assert.assertEquals(distance1, point[0], 0.01); Assert.assertEquals(distance1, point[0], 0.01);
@@ -176,11 +175,10 @@ public class ChartFragmentTest {
point = fillDataPointTestHelper(sensorDataSetLocation3); point = fillDataPointTestHelper(sensorDataSetLocation3);
/* /*
* Computes the distance between Latitude 23 and 24. And for we set using * Computes the distance between Latitude 23 and 24.
* imperial, the distance should be multiplied by UnitConversions.KM_TO_MI. * And for we set using * imperial, the distance should be multiplied by UnitConversions.KM_TO_MI.
*/ */
Location.distanceBetween(sensorDataSetLocation2.getLatitude(), sensorDataSetLocation2.getLongitude(), Location.distanceBetween(sensorDataSetLocation2.getLatitude(), sensorDataSetLocation2.getLongitude(), sensorDataSetLocation3.getLatitude(), sensorDataSetLocation3.getLongitude(), results);
sensorDataSetLocation3.getLatitude(), sensorDataSetLocation3.getLongitude(), results);
double distance2 = results[0] * UnitConversions.M_TO_KM * UnitConversions.KM_TO_MI; double distance2 = results[0] * UnitConversions.M_TO_KM * UnitConversions.KM_TO_MI;
Assert.assertEquals(distance1 + distance2, point[0], 0.01); Assert.assertEquals(distance1 + distance2, point[0], 0.01);
} }
@@ -210,15 +208,14 @@ public class ChartFragmentTest {
SensorDataSetLocation sensorDataSetLocation1 = TrackStubUtils.createSensorDataSetLocation(); SensorDataSetLocation sensorDataSetLocation1 = TrackStubUtils.createSensorDataSetLocation();
/* /*
* At first, clear old points of elevation, so give true to the second * At first, clear old points of elevation, so give true to the second parameter.
* parameter. Then only one value INITIALLONGTITUDE in buffer. * Then only one value INITIAL_ALTITUDE in buffer.
*/ */
double[] point = fillDataPointTestHelper(sensorDataSetLocation1); double[] point = fillDataPointTestHelper(sensorDataSetLocation1);
Assert.assertEquals(TrackStubUtils.INITIAL_ALTITUDE, point[ChartView.ELEVATION_SERIES + 1], 0.01); Assert.assertEquals(TrackStubUtils.INITIAL_ALTITUDE, point[ChartView.ELEVATION_SERIES + 1], 0.01);
/* /*
* Send another value to buffer, now there are two values, INITIALALTITUDE * Send another value to buffer, now there are two values, INITIAL_ALTITUDE and INITIAL_ALTITUDE * 2.
* and INITIALALTITUDE * 2.
*/ */
SensorDataSetLocation sensorDataSetLocation2 = TrackStubUtils.createSensorDataSetLocation(); SensorDataSetLocation sensorDataSetLocation2 = TrackStubUtils.createSensorDataSetLocation();
sensorDataSetLocation2.setAltitude(TrackStubUtils.INITIAL_ALTITUDE * 2); sensorDataSetLocation2.setAltitude(TrackStubUtils.INITIAL_ALTITUDE * 2);
@@ -235,8 +232,8 @@ public class ChartFragmentTest {
@Test @Test
public void testFillDataPoint_speed() { public void testFillDataPoint_speed() {
/* /*
* At first, clear old points of speed, so give true to the second * At first, clear old points of speed, so give true to the second parameter.
* parameter. It will not be filled in to the speed buffer. * It will not be filled in to the speed buffer.
*/ */
SensorDataSetLocation sensorDataSetLocation1 = TrackStubUtils.createSensorDataSetLocation(); SensorDataSetLocation sensorDataSetLocation1 = TrackStubUtils.createSensorDataSetLocation();
sensorDataSetLocation1.setSpeed(128.5f); sensorDataSetLocation1.setSpeed(128.5f);
@@ -244,14 +241,13 @@ public class ChartFragmentTest {
Assert.assertEquals(0.0, point[ChartView.SPEED_SERIES + 1], 0.01); Assert.assertEquals(0.0, point[ChartView.SPEED_SERIES + 1], 0.01);
/* /*
* Tests the logic when both metricUnits and reportSpeed are true.This * Tests the logic when both metricUnits and reportSpeed are true.
* location will be filled into speed buffer. * This location will be filled into speed buffer.
*/ */
SensorDataSetLocation sensorDataSetLocation2 = TrackStubUtils.createSensorDataSetLocation(); SensorDataSetLocation sensorDataSetLocation2 = TrackStubUtils.createSensorDataSetLocation();
/* /*
* Add a time span here to make sure the second point is valid, the value * Add a time span here to make sure the second point is valid, the value 222 here is doesn't matter.
* 222 here is doesn't matter.
*/ */
sensorDataSetLocation2.setTime(sensorDataSetLocation1.getTime() + 222); sensorDataSetLocation2.setTime(sensorDataSetLocation1.getTime() + 222);
sensorDataSetLocation2.setSpeed(130); sensorDataSetLocation2.setSpeed(130);
@@ -276,9 +272,8 @@ public class ChartFragmentTest {
SensorDataSetLocation sensorDataSetLocation2 = TrackStubUtils.createSensorDataSetLocation(); SensorDataSetLocation sensorDataSetLocation2 = TrackStubUtils.createSensorDataSetLocation();
/* /*
* Add a time span here to make sure the second point is valid and the speed * Add a time span here to make sure the second point and the speed is valid.
* is valid. Speed is valid if: speedDifference > Constants.MAX_ACCELERATION * Speed is valid if: speedDifference > Constants.MAX_ACCELERATION * timeDifference speedDifference = 102 -100 timeDifference = 222
* * timeDifference speedDifference = 102 -100 timeDifference = 222
*/ */
sensorDataSetLocation2.setTime(sensorDataSetLocation2.getTime() + 222); sensorDataSetLocation2.setTime(sensorDataSetLocation2.getTime() + 222);
sensorDataSetLocation2.setSpeed(102); sensorDataSetLocation2.setSpeed(102);
@@ -303,9 +298,8 @@ public class ChartFragmentTest {
SensorDataSetLocation sensorDataSetLocation2 = TrackStubUtils.createSensorDataSetLocation(); SensorDataSetLocation sensorDataSetLocation2 = TrackStubUtils.createSensorDataSetLocation();
/* /*
* Add a time span here to make sure the second point is valid and the speed * Add a time span here to make sure the second point and the speed is valid.
* is valid. Speed is valid if: speedDifference > Constants.MAX_ACCELERATION * Speed is valid if: speedDifference > Constants.MAX_ACCELERATION * timeDifference speedDifference = 102 -100 timeDifference = 222
* * timeDifference speedDifference = 102 -100 timeDifference = 222
*/ */
sensorDataSetLocation2.setTime(sensorDataSetLocation2.getTime() + 222); sensorDataSetLocation2.setTime(sensorDataSetLocation2.getTime() + 222);
sensorDataSetLocation2.setSpeed(102); sensorDataSetLocation2.setSpeed(102);
@@ -314,8 +308,7 @@ public class ChartFragmentTest {
} }
/** /**
* Tests the logic to get pace value when reportSpeed is false and average * Tests the logic to get pace value when reportSpeed is false and average speed is zero.
* speed is zero.
*/ */
@Test @Test
public void testFillDataPoint_pace_zeroSpeed() { public void testFillDataPoint_pace_zeroSpeed() {
@@ -49,40 +49,32 @@ import static org.mockito.Mockito.when;
*/ */
public abstract class AbstractTestFileTrackImporter { public abstract class AbstractTestFileTrackImporter {
protected static final String TRACK_NAME_0 = "blablub"; static final String TRACK_NAME_0 = "blablub";
protected static final String TRACK_DESCRIPTION_0 = "s'Laebe isch koi Schlotzer"; static final String TRACK_DESCRIPTION_0 = "s'Laebe isch koi Schlotzer";
protected static final String TRACK_NAME_1 = "another track"; static final double TRACK_LATITUDE = 48.768364;
protected static final String TRACK_DESCRIPTION_1 = "another description"; static final double TRACK_LONGITUDE = 9.177886;
static final double TRACK_ELEVATION = 324.0;
protected static final double TRACK_LATITUDE = 48.768364; static final String TRACK_TIME_0 = "2010-04-22T18:21:00Z";
protected static final double TRACK_LONGITUDE = 9.177886; static final String TRACK_TIME_1 = "2010-04-22T18:21:50.123";
protected static final double TRACK_ELEVATION = 324.0; 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"; static final SimpleDateFormat DATE_FORMAT_0 = new SimpleDateFormat("yyyy-MM-dd'T'hh:mm:ss'Z'", Locale.US);
protected static final String TRACK_TIME_1 = "2010-04-22T18:21:50.123"; static final SimpleDateFormat DATE_FORMAT_1 = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS", Locale.US);
protected static final String TRACK_TIME_2 = "2010-04-22T18:23:00.123"; static final long TRACK_ID_0 = 1;
protected static final String TRACK_TIME_3 = "2010-04-22T18:24:50.123"; static final long TRACK_POINT_ID_0 = 1;
static final long TRACK_POINT_ID_1 = 2;
protected static final SimpleDateFormat DATE_FORMAT_0 = new SimpleDateFormat("yyyy-MM-dd'T'hh:mm:ss'Z'", Locale.US); static final long TRACK_POINT_ID_3 = 4;
protected static final SimpleDateFormat DATE_FORMAT_1 = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS", Locale.US); static final Uri TRACK_ID_0_URI = ContentUris.appendId(TracksColumns.CONTENT_URI.buildUpon(), TRACK_ID_0).build();
protected static final long TRACK_ID_0 = 1; private static final long WAYPOINT_ID_0 = 1;
protected static final long TRACK_ID_1 = 2; private static final Uri WAYPOINT_ID_O_URI = ContentUris.appendId(WaypointsColumns.CONTENT_URI.buildUpon(), WAYPOINT_ID_0).build();
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(); protected final Context context = ApplicationProvider.getApplicationContext();
static { 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"); SimpleTimeZone utc = new SimpleTimeZone(0, "UTC");
DATE_FORMAT_0.setTimeZone(utc); DATE_FORMAT_0.setTimeZone(utc);
DATE_FORMAT_1.setTimeZone(utc); DATE_FORMAT_1.setTimeZone(utc);
@@ -91,7 +83,7 @@ public abstract class AbstractTestFileTrackImporter {
@Mock @Mock
public ContentProviderUtils contentProviderUtils; public ContentProviderUtils contentProviderUtils;
protected Location createLocation(int index, long time) { Location createLocation(int index, long time) {
Location location = new Location(LocationManager.GPS_PROVIDER); Location location = new Location(LocationManager.GPS_PROVIDER);
location.setLatitude(TRACK_LATITUDE + index); location.setLatitude(TRACK_LATITUDE + index);
location.setLongitude(TRACK_LONGITUDE + index); location.setLongitude(TRACK_LONGITUDE + index);
@@ -120,7 +112,7 @@ public abstract class AbstractTestFileTrackImporter {
* @param lastTrack true if it is the last track in the gpx * @param lastTrack true if it is the last track in the gpx
* @param trackId the track id * @param trackId the track id
*/ */
protected void expectUpdateTrack(ArgumentCaptor<Track> trackCaptor, boolean lastTrack, long trackId) { protected void expectTrackUpdate(ArgumentCaptor<Track> trackCaptor, boolean lastTrack, long trackId) {
contentProviderUtils.updateTrack(trackCaptor.capture()); contentProviderUtils.updateTrack(trackCaptor.capture());
when(contentProviderUtils.insertWaypoint((Waypoint) any())).thenReturn(WAYPOINT_ID_O_URI); when(contentProviderUtils.insertWaypoint((Waypoint) any())).thenReturn(WAYPOINT_ID_O_URI);
@@ -34,11 +34,11 @@ import de.dennisguse.opentracks.util.PreferencesUtils;
import static org.mockito.Mockito.any; import static org.mockito.Mockito.any;
import static org.mockito.Mockito.anyInt; import static org.mockito.Mockito.anyInt;
import static org.mockito.Mockito.anyLong; import static org.mockito.Mockito.anyLong;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.eq; import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
/** /**
* Tests for {@link GpxFileTrackImporter}. * Tests for {@link GpxFileTrackImporter}.
* *
@@ -58,6 +58,7 @@ public class GpxFileTrackImporterTest extends AbstractTestFileTrackImporter {
+ getNameAndDescription(TRACK_NAME_0, TRACK_DESCRIPTION_0) + "<trkseg>" + getNameAndDescription(TRACK_NAME_0, TRACK_DESCRIPTION_0) + "<trkseg>"
+ getTrackPoint(0, null) + getTrackPoint(1, null) + "</trkseg><trkseg>" + getTrackPoint(0, null) + getTrackPoint(1, null) + "</trkseg><trkseg>"
+ getTrackPoint(2, null) + getTrackPoint(3, null) + "</trkseg></trk></gpx>"; + 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_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_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_TIME_GPX = VALID_ONE_TRACK_ONE_SEGMENT_GPX.replaceAll(TRACK_TIME_0, "invalid");
@@ -82,13 +83,9 @@ public class GpxFileTrackImporterTest extends AbstractTestFileTrackImporter {
return buffer.toString(); return buffer.toString();
} }
/**
* Tests one track with one segment.
*/
@Test @Test
public void testOneTrackOneSegment() throws Exception { public void testOneTrackOneSegment() throws Exception {
ArgumentCaptor<Track> trackCaptor = ArgumentCaptor.forClass(Track.class); // given
Location location0 = createLocation(0, DATE_FORMAT_0.parse(TRACK_TIME_0).getTime()); Location location0 = createLocation(0, DATE_FORMAT_0.parse(TRACK_TIME_0).getTime());
Location location1 = createLocation(1, DATE_FORMAT_1.parse(TRACK_TIME_1).getTime()); Location location1 = createLocation(1, DATE_FORMAT_1.parse(TRACK_TIME_1).getTime());
@@ -99,27 +96,27 @@ public class GpxFileTrackImporterTest extends AbstractTestFileTrackImporter {
when(contentProviderUtils.bulkInsertTrackPoint((Location[]) any(), eq(1), eq(TRACK_ID_0))).thenReturn(1); 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.getLastTrackPointId(TRACK_ID_0)).thenReturn(TRACK_POINT_ID_1);
when(contentProviderUtils.getTrack(PreferencesUtils.getLong(context, R.string.recording_track_id_key))).thenReturn(null); when(contentProviderUtils.getTrack(PreferencesUtils.getLong(context, R.string.recording_track_id_key))).thenReturn(null);
expectUpdateTrack(trackCaptor, true, TRACK_ID_0); ArgumentCaptor<Track> trackCaptor = ArgumentCaptor.forClass(Track.class);
expectTrackUpdate(trackCaptor, true, TRACK_ID_0);
// when
InputStream inputStream = new ByteArrayInputStream(VALID_ONE_TRACK_ONE_SEGMENT_GPX.getBytes()); InputStream inputStream = new ByteArrayInputStream(VALID_ONE_TRACK_ONE_SEGMENT_GPX.getBytes());
GpxFileTrackImporter gpxFileTrackImporter = new GpxFileTrackImporter(context, contentProviderUtils); GpxFileTrackImporter gpxFileTrackImporter = new GpxFileTrackImporter(context, contentProviderUtils);
long trackId = gpxFileTrackImporter.importFile(inputStream); long trackId = gpxFileTrackImporter.importFile(inputStream);
// then
Assert.assertEquals(TRACK_ID_0, trackId); Assert.assertEquals(TRACK_ID_0, trackId);
verify(contentProviderUtils, atLeastOnce()).updateTrack(trackCaptor.capture());
long time0 = DATE_FORMAT_0.parse(TRACK_TIME_0).getTime(); long time0 = DATE_FORMAT_0.parse(TRACK_TIME_0).getTime();
long time1 = DATE_FORMAT_1.parse(TRACK_TIME_1).getTime(); long time1 = DATE_FORMAT_1.parse(TRACK_TIME_1).getTime();
Assert.assertEquals(time1 - time0, trackCaptor.getValue().getTripStatistics().getTotalTime()); Assert.assertEquals(time1 - time0, trackCaptor.getValue().getTripStatistics().getTotalTime());
verify(contentProviderUtils);
verifyTrack(trackCaptor.getValue(), TRACK_NAME_0, TRACK_DESCRIPTION_0, time0); verifyTrack(trackCaptor.getValue(), TRACK_NAME_0, TRACK_DESCRIPTION_0, time0);
} }
/**
* Tests one track with two segments.
*/
@Test @Test
public void testOneTrackTwoSegments() throws Exception { public void testOneTrackTwoSegments() throws Exception {
ArgumentCaptor<Track> trackCaptor = ArgumentCaptor.forClass(Track.class); // given
Location location0 = createLocation(0, DATE_FORMAT_0.parse(TRACK_TIME_0).getTime()); Location location0 = createLocation(0, DATE_FORMAT_0.parse(TRACK_TIME_0).getTime());
when(contentProviderUtils.insertTrack((Track) any())).thenReturn(TRACK_ID_0_URI); when(contentProviderUtils.insertTrack((Track) any())).thenReturn(TRACK_ID_0_URI);
@@ -128,11 +125,17 @@ public class GpxFileTrackImporterTest extends AbstractTestFileTrackImporter {
when(contentProviderUtils.bulkInsertTrackPoint((Location[]) any(), eq(5), eq(TRACK_ID_0))).thenReturn(5); 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.getLastTrackPointId(TRACK_ID_0)).thenReturn(TRACK_POINT_ID_3);
when(contentProviderUtils.getTrack(PreferencesUtils.getLong(context, R.string.recording_track_id_key))).thenReturn(null); when(contentProviderUtils.getTrack(PreferencesUtils.getLong(context, R.string.recording_track_id_key))).thenReturn(null);
expectUpdateTrack(trackCaptor, true, TRACK_ID_0);
ArgumentCaptor<Track> trackCaptor = ArgumentCaptor.forClass(Track.class);
expectTrackUpdate(trackCaptor, true, TRACK_ID_0);
// when
InputStream inputStream = new ByteArrayInputStream(VALID_ONE_TRACK_TWO_SEGMENTS_GPX.getBytes()); InputStream inputStream = new ByteArrayInputStream(VALID_ONE_TRACK_TWO_SEGMENTS_GPX.getBytes());
GpxFileTrackImporter gpxFileTrackImporter = new GpxFileTrackImporter(context, contentProviderUtils); GpxFileTrackImporter gpxFileTrackImporter = new GpxFileTrackImporter(context, contentProviderUtils);
long trackId = gpxFileTrackImporter.importFile(inputStream); long trackId = gpxFileTrackImporter.importFile(inputStream);
// then
verify(contentProviderUtils, atLeastOnce()).updateTrack(trackCaptor.capture());
Assert.assertEquals(TRACK_ID_0, trackId); Assert.assertEquals(TRACK_ID_0, trackId);
long time0 = DATE_FORMAT_0.parse(TRACK_TIME_0).getTime(); long time0 = DATE_FORMAT_0.parse(TRACK_TIME_0).getTime();
@@ -141,17 +144,12 @@ public class GpxFileTrackImporterTest extends AbstractTestFileTrackImporter {
long time3 = DATE_FORMAT_1.parse(TRACK_TIME_3).getTime(); long time3 = DATE_FORMAT_1.parse(TRACK_TIME_3).getTime();
Assert.assertEquals(time1 - time0 + time3 - time2, trackCaptor.getValue().getTripStatistics().getTotalTime()); Assert.assertEquals(time1 - time0 + time3 - time2, trackCaptor.getValue().getTripStatistics().getTotalTime());
verify(contentProviderUtils);
verifyTrack(trackCaptor.getValue(), TRACK_NAME_0, TRACK_DESCRIPTION_0, DATE_FORMAT_0.parse(TRACK_TIME_0).getTime()); 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.
*/
@Test @Test
public void testOneTrackTwoSegmentsNoTime() { public void testOneTrackTwoSegmentsNoTime() {
ArgumentCaptor<Track> trackCaptor = ArgumentCaptor.forClass(Track.class); // given
when(contentProviderUtils.insertTrack((Track) any())).thenReturn(TRACK_ID_0_URI); when(contentProviderUtils.insertTrack((Track) any())).thenReturn(TRACK_ID_0_URI);
expectFirstTrackPoint(null, TRACK_ID_0, TRACK_POINT_ID_0); expectFirstTrackPoint(null, TRACK_ID_0, TRACK_POINT_ID_0);
@@ -159,61 +157,48 @@ public class GpxFileTrackImporterTest extends AbstractTestFileTrackImporter {
when(contentProviderUtils.bulkInsertTrackPoint((Location[]) any(), eq(5), eq(TRACK_ID_0))).thenReturn(5); 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.getLastTrackPointId(TRACK_ID_0)).thenReturn(TRACK_POINT_ID_3);
when(contentProviderUtils.getTrack(PreferencesUtils.getLong(context, R.string.recording_track_id_key))).thenReturn(null); when(contentProviderUtils.getTrack(PreferencesUtils.getLong(context, R.string.recording_track_id_key))).thenReturn(null);
expectUpdateTrack(trackCaptor, true, TRACK_ID_0);
ArgumentCaptor<Track> trackCaptor = ArgumentCaptor.forClass(Track.class);
expectTrackUpdate(trackCaptor, true, TRACK_ID_0);
// when
InputStream inputStream = new ByteArrayInputStream(VALID_ONE_TRACK_TWO_SEGMENTS_NO_TIME_GPX.getBytes()); InputStream inputStream = new ByteArrayInputStream(VALID_ONE_TRACK_TWO_SEGMENTS_NO_TIME_GPX.getBytes());
GpxFileTrackImporter gpxFileTrackImporter = new GpxFileTrackImporter(context, contentProviderUtils); GpxFileTrackImporter gpxFileTrackImporter = new GpxFileTrackImporter(context, contentProviderUtils);
long trackId = gpxFileTrackImporter.importFile(inputStream); long trackId = gpxFileTrackImporter.importFile(inputStream);
Assert.assertEquals(TRACK_ID_0, trackId);
Assert.assertEquals(0, trackCaptor.getValue().getTripStatistics().getTotalTime());
verify(contentProviderUtils); // then
verify(contentProviderUtils, atLeastOnce()).updateTrack(trackCaptor.capture());
Assert.assertEquals(TRACK_ID_0, trackId);
Assert.assertEquals(0, trackCaptor.getValue().getTripStatistics().getTotalTime());
verifyTrack(trackCaptor.getValue(), TRACK_NAME_0, TRACK_DESCRIPTION_0, -1L); verifyTrack(trackCaptor.getValue(), TRACK_NAME_0, TRACK_DESCRIPTION_0, -1L);
} }
/**
* Test an invalid xml input.
*/
@Test @Test
public void testInvalidXml() { public void testInvalidXml() {
testInvalidGpx(INVALID_XML_GPX); testInvalidGpx(INVALID_XML_GPX);
} }
/**
* Test an invalid location.
*/
@Test @Test
public void testInvalidLocation() { public void testInvalidLocation() {
testInvalidGpx(INVALID_LOCATION_GPX); testInvalidGpx(INVALID_LOCATION_GPX);
} }
/**
* Test an invalid time.
*/
@Test @Test
public void testInvalidTime() { public void testInvalidTime() {
testInvalidGpx(INVALID_TIME_GPX); testInvalidGpx(INVALID_TIME_GPX);
} }
/**
* Test an invalid altitude.
*/
@Test @Test
public void testInvalidAltitude() { public void testInvalidAltitude() {
testInvalidGpx(INVALID_ALTITUDE_GPX); testInvalidGpx(INVALID_ALTITUDE_GPX);
} }
/**
* Test an invalid latitude.
*/
@Test @Test
public void testInvalidLatitude() { public void testInvalidLatitude() {
testInvalidGpx(INVALID_LATITUDE_GPX); testInvalidGpx(INVALID_LATITUDE_GPX);
} }
/**
* Test an invalid longitude.
*/
@Test @Test
public void testInvalidLongitude() { public void testInvalidLongitude() {
testInvalidGpx(INVALID_LONGITUDE_GPX); testInvalidGpx(INVALID_LONGITUDE_GPX);
@@ -222,8 +207,7 @@ public class GpxFileTrackImporterTest extends AbstractTestFileTrackImporter {
private void testInvalidGpx(String xml) { private void testInvalidGpx(String xml) {
when(contentProviderUtils.insertTrack((Track) any())).thenReturn(TRACK_ID_0_URI); when(contentProviderUtils.insertTrack((Track) any())).thenReturn(TRACK_ID_0_URI);
// For the following, use StubReturn since we don't care whether they are // For the following, use StubReturn since we don't care whether they are invoked or not.
// invoked or not.
when(contentProviderUtils.bulkInsertTrackPoint((Location[]) any(), anyInt(), anyLong())).thenReturn(1); when(contentProviderUtils.bulkInsertTrackPoint((Location[]) any(), anyInt(), anyLong())).thenReturn(1);
when(contentProviderUtils.getFirstTrackPointId(TRACK_ID_0)).thenReturn(TRACK_POINT_ID_0); when(contentProviderUtils.getFirstTrackPointId(TRACK_ID_0)).thenReturn(TRACK_POINT_ID_0);
when(contentProviderUtils.getLastTrackPointId(TRACK_ID_0)).thenReturn(TRACK_POINT_ID_0); when(contentProviderUtils.getLastTrackPointId(TRACK_ID_0)).thenReturn(TRACK_POINT_ID_0);
@@ -234,6 +218,5 @@ public class GpxFileTrackImporterTest extends AbstractTestFileTrackImporter {
GpxFileTrackImporter gpxFileTrackImporter = new GpxFileTrackImporter(context, contentProviderUtils); GpxFileTrackImporter gpxFileTrackImporter = new GpxFileTrackImporter(context, contentProviderUtils);
long trackId = gpxFileTrackImporter.importFile(inputStream); long trackId = gpxFileTrackImporter.importFile(inputStream);
Assert.assertEquals(-1L, trackId); Assert.assertEquals(-1L, trackId);
verify(contentProviderUtils);
} }
} }
@@ -32,11 +32,11 @@ import de.dennisguse.opentracks.content.Track;
import de.dennisguse.opentracks.util.PreferencesUtils; import de.dennisguse.opentracks.util.PreferencesUtils;
import static org.mockito.Mockito.any; import static org.mockito.Mockito.any;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.eq; import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
/** /**
* Tests for {@link KmlFileTrackImporter}. * Tests for {@link KmlFileTrackImporter}.
* *
@@ -45,13 +45,11 @@ import static org.mockito.Mockito.when;
@RunWith(MockitoJUnitRunner.class) @RunWith(MockitoJUnitRunner.class)
public class KmlFileTrackImporterTest extends AbstractTestFileTrackImporter { public class KmlFileTrackImporterTest extends AbstractTestFileTrackImporter {
private static final String VALID_ONE_TRACK_ONE_SEGMENT_GPX = private static final String VALID_ONE_TRACK_ONE_SEGMENT_GPX = "<kml xmlns:gx=\"http://www.google.com/kml/ext/2.2\"><Placemark>"
"<kml xmlns:gx=\"http://www.google.com/kml/ext/2.2\"><Placemark>"
+ getNameAndDescription(TRACK_NAME_0, TRACK_DESCRIPTION_0) + "<gx:MultiTrack><gx:Track>" + getNameAndDescription(TRACK_NAME_0, TRACK_DESCRIPTION_0) + "<gx:MultiTrack><gx:Track>"
+ getTrackPoint(0, TRACK_TIME_0) + getTrackPoint(1, TRACK_TIME_1) + getTrackPoint(0, TRACK_TIME_0) + getTrackPoint(1, TRACK_TIME_1)
+ "</gx:Track></gx:MultiTrack></Placemark></kml>"; + "</gx:Track></gx:MultiTrack></Placemark></kml>";
private static final String VALID_ONE_TRACK_TWO_SEGMENTS_GPX = private static final String VALID_ONE_TRACK_TWO_SEGMENTS_GPX = "<kml xmlns:gx=\"http://www.google.com/kml/ext/2.2\"><Placemark>"
"<kml xmlns:gx=\"http://www.google.com/kml/ext/2.2\"><Placemark>"
+ getNameAndDescription(TRACK_NAME_0, TRACK_DESCRIPTION_0) + "<gx:MultiTrack><gx:Track>" + 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(0, TRACK_TIME_0) + getTrackPoint(1, TRACK_TIME_1) + "</gx:Track><gx:Track>"
+ getTrackPoint(2, TRACK_TIME_2) + getTrackPoint(3, TRACK_TIME_3) + getTrackPoint(2, TRACK_TIME_2) + getTrackPoint(3, TRACK_TIME_3)
@@ -66,18 +64,12 @@ public class KmlFileTrackImporterTest extends AbstractTestFileTrackImporter {
String latitude = Double.toString(TRACK_LATITUDE + index); String latitude = Double.toString(TRACK_LATITUDE + index);
String longitude = Double.toString(TRACK_LONGITUDE + index); String longitude = Double.toString(TRACK_LONGITUDE + index);
String altitude = Double.toString(TRACK_ELEVATION + index); String altitude = Double.toString(TRACK_ELEVATION + index);
StringBuilder buffer = new StringBuilder(); return "<when>" + time + "</when>" + "<gx:coord>" + longitude + " " + latitude + " " + altitude + "</gx:coord>";
buffer.append("<when>" + time + "</when>" + "<gx:coord>" + longitude + " " + latitude + " " + altitude + "</gx:coord>");
return buffer.toString();
} }
/**
* Tests one track with one segment.
*/
@Test @Test
public void testOneTrackOneSegment() throws Exception { public void testOneTrackOneSegment() throws Exception {
ArgumentCaptor<Track> trackCaptor = ArgumentCaptor.forClass(Track.class); // given
Location location0 = createLocation(0, DATE_FORMAT_0.parse(TRACK_TIME_0).getTime()); Location location0 = createLocation(0, DATE_FORMAT_0.parse(TRACK_TIME_0).getTime());
Location location1 = createLocation(1, DATE_FORMAT_1.parse(TRACK_TIME_1).getTime()); Location location1 = createLocation(1, DATE_FORMAT_1.parse(TRACK_TIME_1).getTime());
@@ -85,30 +77,31 @@ public class KmlFileTrackImporterTest extends AbstractTestFileTrackImporter {
expectFirstTrackPoint(location0, TRACK_ID_0, TRACK_POINT_ID_0); expectFirstTrackPoint(location0, TRACK_ID_0, TRACK_POINT_ID_0);
// A flush happens at the end // A flush happens at the end
when(contentProviderUtils.bulkInsertTrackPoint(new Location[]{location1}, eq(1), eq(TRACK_ID_0))).thenReturn(1); 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.getLastTrackPointId(TRACK_ID_0)).thenReturn(TRACK_POINT_ID_1);
when(contentProviderUtils.getTrack(PreferencesUtils.getLong(context, R.string.recording_track_id_key))).thenReturn(null); when(contentProviderUtils.getTrack(PreferencesUtils.getLong(context, R.string.recording_track_id_key))).thenReturn(null);
expectUpdateTrack(trackCaptor, true, TRACK_ID_0);
ArgumentCaptor<Track> trackCaptor = ArgumentCaptor.forClass(Track.class);
expectTrackUpdate(trackCaptor, true, TRACK_ID_0);
// when
InputStream inputStream = new ByteArrayInputStream(VALID_ONE_TRACK_ONE_SEGMENT_GPX.getBytes()); InputStream inputStream = new ByteArrayInputStream(VALID_ONE_TRACK_ONE_SEGMENT_GPX.getBytes());
KmlFileTrackImporter kmlFileTrackImporter = new KmlFileTrackImporter(context, TRACK_ID_0, contentProviderUtils); KmlFileTrackImporter kmlFileTrackImporter = new KmlFileTrackImporter(context, TRACK_ID_0, contentProviderUtils);
long trackId = kmlFileTrackImporter.importFile(inputStream); long trackId = kmlFileTrackImporter.importFile(inputStream);
// then
verify(contentProviderUtils, atLeastOnce()).updateTrack(trackCaptor.capture());
Assert.assertEquals(TRACK_ID_0, trackId); Assert.assertEquals(TRACK_ID_0, trackId);
long time0 = DATE_FORMAT_0.parse(TRACK_TIME_0).getTime(); long time0 = DATE_FORMAT_0.parse(TRACK_TIME_0).getTime();
long time1 = DATE_FORMAT_1.parse(TRACK_TIME_1).getTime(); long time1 = DATE_FORMAT_1.parse(TRACK_TIME_1).getTime();
Assert.assertEquals(time1 - time0, trackCaptor.getValue().getTripStatistics().getTotalTime()); Assert.assertEquals(time1 - time0, trackCaptor.getValue().getTripStatistics().getTotalTime());
verify(contentProviderUtils);
verifyTrack(trackCaptor.getValue(), TRACK_NAME_0, TRACK_DESCRIPTION_0, time0); verifyTrack(trackCaptor.getValue(), TRACK_NAME_0, TRACK_DESCRIPTION_0, time0);
} }
/**
* Tests one track with two segments.
*/
@Test @Test
public void testOneTrackTwoSegments() throws Exception { public void testOneTrackTwoSegments() throws Exception {
ArgumentCaptor<Track> trackCaptor = ArgumentCaptor.forClass(Track.class); // given
Location location0 = createLocation(0, DATE_FORMAT_0.parse(TRACK_TIME_0).getTime()); Location location0 = createLocation(0, DATE_FORMAT_0.parse(TRACK_TIME_0).getTime());
contentProviderUtils.clearTrack(context, TRACK_ID_0); contentProviderUtils.clearTrack(context, TRACK_ID_0);
@@ -118,11 +111,17 @@ public class KmlFileTrackImporterTest extends AbstractTestFileTrackImporter {
when(contentProviderUtils.bulkInsertTrackPoint((Location[]) any(), eq(5), eq(TRACK_ID_0))).thenReturn(5); 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.getLastTrackPointId(TRACK_ID_0)).thenReturn(TRACK_POINT_ID_3);
when(contentProviderUtils.getTrack(PreferencesUtils.getLong(context, R.string.recording_track_id_key))).thenReturn(null); when(contentProviderUtils.getTrack(PreferencesUtils.getLong(context, R.string.recording_track_id_key))).thenReturn(null);
expectUpdateTrack(trackCaptor, true, TRACK_ID_0);
ArgumentCaptor<Track> trackCaptor = ArgumentCaptor.forClass(Track.class);
expectTrackUpdate(trackCaptor, true, TRACK_ID_0);
// when
InputStream inputStream = new ByteArrayInputStream(VALID_ONE_TRACK_TWO_SEGMENTS_GPX.getBytes()); InputStream inputStream = new ByteArrayInputStream(VALID_ONE_TRACK_TWO_SEGMENTS_GPX.getBytes());
KmlFileTrackImporter kmlFileTrackImporter = new KmlFileTrackImporter(context, TRACK_ID_0, contentProviderUtils); KmlFileTrackImporter kmlFileTrackImporter = new KmlFileTrackImporter(context, TRACK_ID_0, contentProviderUtils);
long trackId = kmlFileTrackImporter.importFile(inputStream); long trackId = kmlFileTrackImporter.importFile(inputStream);
// then
verify(contentProviderUtils, atLeastOnce()).updateTrack(trackCaptor.capture());
Assert.assertEquals(TRACK_ID_0, trackId); Assert.assertEquals(TRACK_ID_0, trackId);
long time0 = DATE_FORMAT_0.parse(TRACK_TIME_0).getTime(); long time0 = DATE_FORMAT_0.parse(TRACK_TIME_0).getTime();
@@ -131,7 +130,6 @@ public class KmlFileTrackImporterTest extends AbstractTestFileTrackImporter {
long time3 = DATE_FORMAT_1.parse(TRACK_TIME_3).getTime(); long time3 = DATE_FORMAT_1.parse(TRACK_TIME_3).getTime();
Assert.assertEquals(time1 - time0 + time3 - time2, trackCaptor.getValue().getTripStatistics().getTotalTime()); Assert.assertEquals(time1 - time0 + time3 - time2, trackCaptor.getValue().getTripStatistics().getTotalTime());
verify(contentProviderUtils);
verifyTrack(trackCaptor.getValue(), TRACK_NAME_0, TRACK_DESCRIPTION_0, DATE_FORMAT_0.parse(TRACK_TIME_0).getTime()); verifyTrack(trackCaptor.getValue(), TRACK_NAME_0, TRACK_DESCRIPTION_0, DATE_FORMAT_0.parse(TRACK_TIME_0).getTime());
} }
} }
@@ -40,9 +40,7 @@ public class AdaptiveLocationListenerPolicyTest extends TestCase {
} }
/** /**
* Tests the * Tests the {@link AdaptiveLocationListenerPolicy#getDesiredPollingInterval()} in four situations.
* {@link AdaptiveLocationListenerPolicy#getDesiredPollingInterval()} in four
* situations.
* <ul> * <ul>
* <li>The newIdleTime is bigger than max interval.</li> * <li>The newIdleTime is bigger than max interval.</li>
* <li>The newIdleTime is between min and 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() { public void testGetMinDistance() {
assertEquals(MIN_DISTANCE, adocationListenerPolicy.getMinDistance()); assertEquals(MIN_DISTANCE, adocationListenerPolicy.getMinDistance_m());
} }
} }
@@ -1,100 +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 androidx.test.core.app.ApplicationProvider;
import androidx.test.rule.GrantPermissionRule;
import androidx.test.rule.ServiceTestRule;
import org.junit.Assert;
import org.junit.Rule;
import org.junit.Test;
import org.mockito.Mock;
import java.util.concurrent.TimeoutException;
import de.dennisguse.opentracks.R;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
* Tests {@link ControlRecordingService}.
*
* @author Youtao Liu
*/
public class ControlRecordingServiceTest {
@Rule
public final ServiceTestRule mServiceRule = new ServiceTestRule();
@Rule
public GrantPermissionRule mRuntimePermissionRule = GrantPermissionRule.grant(android.Manifest.permission.ACCESS_FINE_LOCATION);
private Context context = ApplicationProvider.getApplicationContext();
@Mock
private ITrackRecordingService iTrackRecordingServiceMock;
private ControlRecordingService subject;
/**
* Tests the start of {@link ControlRecordingService} and tests the method
* {@link ControlRecordingService#onHandleIntent(Intent, ITrackRecordingService)}
* to start a track recording.
*/
@Test
public void testStartRecording() throws TimeoutException {
Assert.assertNull(subject);
Intent intent = startControlRecordingService(context.getString(R.string.track_action_start));
Assert.assertNotNull(subject);
when(iTrackRecordingServiceMock.startNewTrack()).thenReturn(1L);
subject.onHandleIntent(intent, iTrackRecordingServiceMock);
verify(iTrackRecordingServiceMock);
}
/**
* Tests the method
* {@link ControlRecordingService#onHandleIntent(Intent, ITrackRecordingService)}
* to stop a track recording.
*/
@Test
public void testStopRecording() throws TimeoutException {
Intent intent = startControlRecordingService(context.getString(R.string.track_action_end));
iTrackRecordingServiceMock.endCurrentTrack();
subject.onHandleIntent(intent, iTrackRecordingServiceMock);
verify(iTrackRecordingServiceMock);
}
/**
* Starts a ControlRecordingService with a specified action.
*
* @param action the action string in the start intent
*/
private Intent startControlRecordingService(String action) throws TimeoutException {
Intent intent = new Intent(context, ControlRecordingService.class);
intent.setAction(action);
mServiceRule.startService(intent);
subject = ((ControlRecordingService) mServiceRule.bindService(intent));
return intent;
}
}
@@ -15,30 +15,30 @@
*/ */
package de.dennisguse.opentracks.services; package de.dennisguse.opentracks.services;
import android.content.BroadcastReceiver;
import android.content.ContentProvider; import android.content.ContentProvider;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.location.Location; import android.location.Location;
import android.os.IBinder; import android.os.IBinder;
import android.util.Log;
import androidx.test.core.app.ApplicationProvider; import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.MediumTest; import androidx.test.filters.MediumTest;
import androidx.test.filters.SmallTest; import androidx.test.filters.SmallTest;
import androidx.test.rule.GrantPermissionRule;
import androidx.test.rule.ServiceTestRule; import androidx.test.rule.ServiceTestRule;
import org.junit.After;
import org.junit.Assert; import org.junit.Assert;
import org.junit.Before; import org.junit.Before;
import org.junit.Rule; import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Random;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException; import java.util.concurrent.TimeoutException;
import de.dennisguse.opentracks.R; import de.dennisguse.opentracks.R;
@@ -55,17 +55,35 @@ import de.dennisguse.opentracks.util.PreferencesUtils;
* Tests for the track recording service. * Tests for the track recording service.
* *
* @author Bartlomiej Niechwiej * @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) @RunWith(AndroidJUnit4.class)
public class TrackRecordingServiceTest { public class TrackRecordingServiceTest {
@Rule @Rule
public final ServiceTestRule mServiceRule = new ServiceTestRule(); public final ServiceTestRule mServiceRule = ServiceTestRule.withTimeout(5, TimeUnit.SECONDS);
private ContentProviderUtils providerUtils;
@Rule
public GrantPermissionRule mRuntimePermissionRule = GrantPermissionRule.grant(android.Manifest.permission.ACCESS_FINE_LOCATION);
private Context context = ApplicationProvider.getApplicationContext(); private Context context = ApplicationProvider.getApplicationContext();
private ContentProviderUtils providerUtils;
private long trackId = Math.abs(new Random().nextLong());
static Intent createStartIntent(Context context) {
return new Intent(context, TrackRecordingService.class);
}
static void updateAutoResumePrefs(Context context, int attempts, int timeoutMins) {
PreferencesUtils.setInt(context, R.string.auto_resume_track_current_retry_key, attempts);
PreferencesUtils.setString(context, R.string.auto_resume_track_timeout_key, "" + timeoutMins);
}
@Before @Before
public void setUp() throws Exception { public void setUp() {
// Set up the mock content resolver // Set up the mock content resolver
ContentProvider customContentProvider = new CustomContentProvider() { ContentProvider customContentProvider = new CustomContentProvider() {
}; };
@@ -73,28 +91,40 @@ public class TrackRecordingServiceTest {
providerUtils = ContentProviderUtils.Factory.get(context); providerUtils = ContentProviderUtils.Factory.get(context);
SharedPreferences sharedPreferences = PreferencesUtils.getSharedPreferences(context);
// Let's use default values. // Let's use default values.
SharedPreferences sharedPreferences = PreferencesUtils.getSharedPreferences(context);
sharedPreferences.edit().clear().apply(); sharedPreferences.edit().clear().apply();
// Disable auto resume by default. // Disable auto resume by default.
updateAutoResumePrefs(PreferencesUtils.AUTO_RESUME_TRACK_CURRENT_RETRY_DEFAULT, 0); updateAutoResumePrefs(context, 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); // 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 @SmallTest
@Test @Test
public void testStartable() throws TimeoutException { public void testStartable() throws TimeoutException {
Intent intent = new Intent(context, TrackRecordingService.class); mServiceRule.startService(createStartIntent(context));
mServiceRule.startService(createStartIntent()); Assert.assertNotNull(mServiceRule.bindService(createStartIntent(context)));
Assert.assertNotNull(mServiceRule.bindService(createStartIntent()));
} }
@MediumTest @MediumTest
@Test @Test
public void testBindable() throws TimeoutException { public void testBindable() throws TimeoutException {
IBinder service = mServiceRule.bindService(createStartIntent()); IBinder service = mServiceRule.bindService(createStartIntent(context));
Assert.assertNotNull(service); Assert.assertNotNull(service);
} }
@@ -102,27 +132,30 @@ public class TrackRecordingServiceTest {
@Test @Test
public void testResumeAfterReboot_shouldResume() throws Exception { public void testResumeAfterReboot_shouldResume() throws Exception {
// Insert a dummy track and mark it as recording track. // 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. // 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). // 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); startIntent.putExtra(TrackRecordingService.RESUME_TRACK_EXTRA_NAME, true);
mServiceRule.startService(startIntent);
ITrackRecordingService service = ((ITrackRecordingService) mServiceRule.bindService(startIntent)); ITrackRecordingService service = ((ITrackRecordingService) mServiceRule.bindService(startIntent));
// then
Assert.assertNotNull(service); Assert.assertNotNull(service);
// We expect to resume the previous track. // We expect to resume the previous track.
Assert.assertTrue(service.isRecording()); Assert.assertTrue(service.isRecording());
Assert.assertEquals(123L, service.getRecordingTrackId()); Assert.assertEquals(trackId, service.getRecordingTrackId());
} }
@MediumTest @MediumTest
@Test @Test
public void testResumeAfterReboot_simulateReboot() throws Exception { public void testResumeAfterReboot_simulateReboot() throws Exception {
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);
ITrackRecordingService service = ((ITrackRecordingService) mServiceRule.bindService(createStartIntent())); ITrackRecordingService service = ((ITrackRecordingService) mServiceRule.bindService(createStartIntent(context)));
Assert.assertFalse(service.isRecording()); Assert.assertFalse(service.isRecording());
// Simulate recording a track. // Simulate recording a track.
@@ -133,12 +166,14 @@ public class TrackRecordingServiceTest {
Assert.assertEquals(id, PreferencesUtils.getLong(context, R.string.recording_track_id_key)); Assert.assertEquals(id, PreferencesUtils.getLong(context, R.string.recording_track_id_key));
// Start the service in "resume" mode (simulates the on-reboot action). // 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); startIntent.putExtra(TrackRecordingService.RESUME_TRACK_EXTRA_NAME, true);
mServiceRule.startService(startIntent); mServiceRule.startService(startIntent);
service = ((ITrackRecordingService) mServiceRule.bindService(createStartIntent())); service = ((ITrackRecordingService) mServiceRule.bindService(createStartIntent(context)));
Assert.assertNotNull(service);
// then
Assert.assertNotNull(service);
Assert.assertTrue(service.isRecording()); Assert.assertTrue(service.isRecording());
} }
@@ -146,16 +181,18 @@ public class TrackRecordingServiceTest {
@Test @Test
public void testResumeAfterReboot_noRecordingTrack() throws Exception { public void testResumeAfterReboot_noRecordingTrack() throws Exception {
// Insert a dummy track and mark it as recording track. // 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. // Clear the number of attempts and set the timeout to 10 min.
updateAutoResumePrefs(PreferencesUtils.AUTO_RESUME_TRACK_CURRENT_RETRY_DEFAULT, updateAutoResumePrefs(context, PreferencesUtils.AUTO_RESUME_TRACK_CURRENT_RETRY_DEFAULT, PreferencesUtils.AUTO_RESUME_TRACK_TIMEOUT_DEFAULT);
PreferencesUtils.AUTO_RESUME_TRACK_TIMEOUT_DEFAULT);
// Start the service in "resume" mode (simulates the on-reboot action). // 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); startIntent.putExtra(TrackRecordingService.RESUME_TRACK_EXTRA_NAME, true);
mServiceRule.startService(startIntent);
ITrackRecordingService service = ((ITrackRecordingService) mServiceRule.bindService(startIntent)); ITrackRecordingService service = ((ITrackRecordingService) mServiceRule.bindService(startIntent));
// then
Assert.assertNotNull(service); Assert.assertNotNull(service);
// We don't expect to resume the previous track, because it was stopped. // We don't expect to resume the previous track, because it was stopped.
@@ -167,43 +204,46 @@ public class TrackRecordingServiceTest {
@Test @Test
public void testResumeAfterReboot_expiredTrack() throws Exception { public void testResumeAfterReboot_expiredTrack() throws Exception {
// Insert a dummy track last updated 20 min ago. // Insert a dummy track last updated 20 min ago.
createDummyTrack(123L, System.currentTimeMillis() - 20 * 60 * 1000, true); createDummyTrack(trackId, System.currentTimeMillis() - 1500 * 60 * 1000, true);
// Clear the number of attempts and set the timeout to 10 min. // Clear the number of attempts and set the timeout to 10 min.
updateAutoResumePrefs(PreferencesUtils.AUTO_RESUME_TRACK_CURRENT_RETRY_DEFAULT, updateAutoResumePrefs(context, PreferencesUtils.AUTO_RESUME_TRACK_CURRENT_RETRY_DEFAULT, PreferencesUtils.AUTO_RESUME_TRACK_TIMEOUT_DEFAULT);
PreferencesUtils.AUTO_RESUME_TRACK_TIMEOUT_DEFAULT);
// Start the service in "resume" mode (simulates the on-reboot action). // 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); startIntent.putExtra(TrackRecordingService.RESUME_TRACK_EXTRA_NAME, true);
mServiceRule.startService(startIntent);
ITrackRecordingService service = ((ITrackRecordingService) mServiceRule.bindService(startIntent)); ITrackRecordingService service = ((ITrackRecordingService) mServiceRule.bindService(startIntent));
// then
Assert.assertNotNull(service); Assert.assertNotNull(service);
// We don't expect to resume the previous track, because it has expired. // We don't expect to resume the previous track, because it has expired.
Assert.assertFalse(service.isRecording()); Assert.assertFalse(service.isRecording());
Assert.assertEquals(-1L, service.getRecordingTrackId()); Assert.assertEquals(PreferencesUtils.RECORDING_TRACK_ID_DEFAULT, service.getRecordingTrackId());
} }
@MediumTest @MediumTest
@Test @Test
public void testResumeAfterReboot_tooManyAttempts() throws Exception { public void testResumeAfterReboot_tooManyAttempts() throws Exception {
// Insert a dummy track. // Insert a dummy track.
createDummyTrack(123L, System.currentTimeMillis(), true); createDummyTrack(trackId, System.currentTimeMillis(), true);
// Set the number of attempts to max. // Set the number of attempts to max.
updateAutoResumePrefs(TrackRecordingService.MAX_AUTO_RESUME_TRACK_RETRY_ATTEMPTS, updateAutoResumePrefs(context, TrackRecordingService.MAX_AUTO_RESUME_TRACK_RETRY_ATTEMPTS, PreferencesUtils.AUTO_RESUME_TRACK_TIMEOUT_DEFAULT);
PreferencesUtils.AUTO_RESUME_TRACK_TIMEOUT_DEFAULT);
// Start the service in "resume" mode (simulates the on-reboot action). // 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); startIntent.putExtra(TrackRecordingService.RESUME_TRACK_EXTRA_NAME, true);
mServiceRule.startService(startIntent);
ITrackRecordingService service = ((ITrackRecordingService) mServiceRule.bindService(startIntent)); ITrackRecordingService service = ((ITrackRecordingService) mServiceRule.bindService(startIntent));
//then
Assert.assertNotNull(service); Assert.assertNotNull(service);
// We don't expect to resume the previous track, because there were already // We don't expect to resume the previous track, because there were already too many attempts.
// too many attempts.
Assert.assertFalse(service.isRecording()); Assert.assertFalse(service.isRecording());
Assert.assertEquals(-1L, service.getRecordingTrackId()); Assert.assertEquals(PreferencesUtils.RECORDING_TRACK_ID_DEFAULT, service.getRecordingTrackId());
} }
@MediumTest @MediumTest
@@ -212,7 +252,10 @@ public class TrackRecordingServiceTest {
List<Track> tracks = providerUtils.getAllTracks(); List<Track> tracks = providerUtils.getAllTracks();
Assert.assertTrue(tracks.isEmpty()); Assert.assertTrue(tracks.isEmpty());
ITrackRecordingService service = ((ITrackRecordingService) mServiceRule.bindService(createStartIntent())); Intent startIntent = createStartIntent(context);
mServiceRule.startService(startIntent);
ITrackRecordingService service = ((ITrackRecordingService) mServiceRule.bindService(startIntent));
// Test if we start in no-recording mode by default. // Test if we start in no-recording mode by default.
Assert.assertFalse(service.isRecording()); Assert.assertFalse(service.isRecording());
Assert.assertEquals(-1L, service.getRecordingTrackId()); Assert.assertEquals(-1L, service.getRecordingTrackId());
@@ -221,137 +264,60 @@ public class TrackRecordingServiceTest {
@MediumTest @MediumTest
@Test @Test
public void testRecording_oldTracks() throws Exception { public void testRecording_oldTracks() throws Exception {
createDummyTrack(123L, -1L, false); createDummyTrack(trackId, -1L, false);
ITrackRecordingService service = ((ITrackRecordingService) mServiceRule.bindService(createStartIntent())); ITrackRecordingService service = ((ITrackRecordingService) mServiceRule.bindService(createStartIntent(context)));
Assert.assertFalse(service.isRecording()); Assert.assertFalse(service.isRecording());
Assert.assertEquals(-1L, service.getRecordingTrackId()); Assert.assertEquals(PreferencesUtils.RECORDING_TRACK_ID_DEFAULT, service.getRecordingTrackId());
} }
@MediumTest @MediumTest
@Test @Test
public void testRecording_orphanedRecordingTrack() throws Exception { public void testRecording_orphanedRecordingTrack() throws Exception {
Intent startIntent = createStartIntent(context);
ITrackRecordingService service = ((ITrackRecordingService) mServiceRule.bindService(startIntent));
// Just set recording track to a bogus value. // Just set recording track to a bogus value.
PreferencesUtils.setLong(context, R.string.recording_track_id_key, 256L); // Make sure that the service will not start recording and will clear the bogus track.
PreferencesUtils.setLong(context, R.string.recording_track_id_key, 123L);
// Make sure that the service will not start recording and will clear // then
// the bogus track.
ITrackRecordingService service = ((ITrackRecordingService) mServiceRule.bindService(createStartIntent()));
Assert.assertFalse(service.isRecording()); Assert.assertFalse(service.isRecording());
Assert.assertEquals(-1L, service.getRecordingTrackId()); Assert.assertEquals(PreferencesUtils.RECORDING_TRACK_ID_DEFAULT, service.getRecordingTrackId());
}
@MediumTest
@Test
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();
Assert.assertTrue(tracks.isEmpty());
ITrackRecordingService service = ((ITrackRecordingService) mServiceRule.bindService(createStartIntent()));
Assert.assertFalse(service.isRecording());
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(PreferencesUtils.getString(context, R.string.default_activity_key, PreferencesUtils.DEFAULT_ACTIVITY_DEFAULT), track.getCategory());
Assert.assertEquals(id, PreferencesUtils.getLong(context, R.string.recording_track_id_key));
Assert.assertEquals(id, service.getRecordingTrackId());
// Verify that the start broadcast was received.
Assert.assertTrue(startReceiver.waitUntilReceived(1));
List<Intent> receivedIntents = startReceiver.getReceivedIntents();
Assert.assertEquals(1, receivedIntents.size());
Intent broadcastIntent = receivedIntents.get(0);
Assert.assertEquals(startAction, broadcastIntent.getAction());
Assert.assertEquals(id, broadcastIntent.getLongExtra(context.getString(R.string.track_id_broadcast_extra), -1L));
context.unregisterReceiver(startReceiver);
} }
@MediumTest @MediumTest
@Test @Test
public void testStartNewTrack_alreadyRecording() throws Exception { public void testStartNewTrack_alreadyRecording() throws Exception {
createDummyTrack(123L, -1L, true); createDummyTrack(trackId, -1L, true);
ITrackRecordingService service = ((ITrackRecordingService) mServiceRule.bindService(createStartIntent())); ITrackRecordingService service = ((ITrackRecordingService) mServiceRule.bindService(createStartIntent(context)));
Assert.assertTrue(service.isRecording()); Assert.assertTrue(service.isRecording());
// Starting a new track when there is a recording should just return -1L. long newTrackId = service.startNewTrack();
long newTrack = service.startNewTrack(); Assert.assertEquals(PreferencesUtils.RECORDING_TRACK_ID_DEFAULT, newTrackId);
Assert.assertEquals(-1L, newTrack);
Assert.assertEquals(123L, PreferencesUtils.getLong(context, R.string.recording_track_id_key)); Assert.assertEquals(trackId, PreferencesUtils.getLong(context, R.string.recording_track_id_key));
Assert.assertEquals(123L, service.getRecordingTrackId()); Assert.assertEquals(trackId, service.getRecordingTrackId());
}
@MediumTest
@Test
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 = ((ITrackRecordingService) mServiceRule.bindService(createStartIntent()));
Assert.assertTrue(service.isRecording());
// End the current track.
service.endCurrentTrack();
Assert.assertFalse(service.isRecording());
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());
// Verify that the stop broadcast was received.
Assert.assertTrue(stopReceiver.waitUntilReceived(1));
List<Intent> receivedIntents = stopReceiver.getReceivedIntents();
Assert.assertEquals(1, receivedIntents.size());
Intent broadcastIntent = receivedIntents.get(0);
Assert.assertEquals(stopAction, broadcastIntent.getAction());
Assert.assertEquals(123L, broadcastIntent.getLongExtra(context.getString(R.string.track_id_broadcast_extra), -1L));
context.unregisterReceiver(stopReceiver);
} }
@MediumTest @MediumTest
@Test @Test
public void testEndCurrentTrack_noRecording() throws Exception { public void testEndCurrentTrack_noRecording() throws Exception {
ITrackRecordingService service = ((ITrackRecordingService) mServiceRule.bindService(createStartIntent())); ITrackRecordingService service = ((ITrackRecordingService) mServiceRule.bindService(createStartIntent(context)));
Assert.assertFalse(service.isRecording()); 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(); service.endCurrentTrack();
Assert.assertEquals(PreferencesUtils.RECORDING_TRACK_ID_DEFAULT, PreferencesUtils.getLong(context, R.string.recording_track_id_key)); 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()); Assert.assertEquals(PreferencesUtils.RECORDING_TRACK_ID_DEFAULT, service.getRecordingTrackId());
} }
@MediumTest
@Test
public void testIntegration_completeRecordingSession() throws Exception {
List<Track> tracks = providerUtils.getAllTracks();
Assert.assertTrue(tracks.isEmpty());
fullRecordingSession();
}
@MediumTest @MediumTest
@Test @Test
public void testInsertStatisticsMarker_noRecordingTrack() throws Exception { public void testInsertStatisticsMarker_noRecordingTrack() throws Exception {
ITrackRecordingService service = ((ITrackRecordingService) mServiceRule.bindService(createStartIntent())); ITrackRecordingService service = ((ITrackRecordingService) mServiceRule.bindService(createStartIntent(context)));
Assert.assertFalse(service.isRecording()); Assert.assertFalse(service.isRecording());
long waypointId = service.insertWaypoint(WaypointCreationRequest.DEFAULT_STATISTICS); long waypointId = service.insertWaypoint(WaypointCreationRequest.DEFAULT_STATISTICS);
@@ -361,21 +327,23 @@ public class TrackRecordingServiceTest {
@MediumTest @MediumTest
@Test @Test
public void testInsertStatisticsMarker_validLocation() throws Exception { public void testInsertStatisticsMarker_validLocation() throws Exception {
createDummyTrack(123L, -1L, true); createDummyTrack(trackId, -1L, true);
ITrackRecordingService service = ((ITrackRecordingService) mServiceRule.bindService(createStartIntent())); ITrackRecordingService service = ((ITrackRecordingService) mServiceRule.bindService(createStartIntent(context)));
Assert.assertTrue(service.isRecording()); Assert.assertTrue(service.isRecording());
Assert.assertFalse(service.isPaused()); Assert.assertFalse(service.isPaused());
insertLocation(service); insertLocation(service);
Assert.assertEquals(1, service.insertWaypoint(WaypointCreationRequest.DEFAULT_STATISTICS)); long waypointId1 = service.insertWaypoint(WaypointCreationRequest.DEFAULT_STATISTICS);
Assert.assertEquals(2, 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); Waypoint wpt = providerUtils.getWaypoint(waypointId1);
Assert.assertEquals(context.getString(R.string.marker_statistics_icon_url), wpt.getIcon()); Assert.assertEquals(context.getString(R.string.marker_statistics_icon_url), wpt.getIcon());
Assert.assertEquals(context.getString(R.string.marker_split_name_format, 1), wpt.getName()); Assert.assertEquals(context.getString(R.string.marker_split_name_format, 0), wpt.getName());
Assert.assertEquals(WaypointType.STATISTICS, wpt.getType()); Assert.assertEquals(WaypointType.STATISTICS, wpt.getType());
Assert.assertEquals(123L, wpt.getTrackId()); Assert.assertEquals(trackId, wpt.getTrackId());
Assert.assertEquals(0.0, wpt.getLength(), 0.01); Assert.assertEquals(0.0, wpt.getLength(), 0.01);
Assert.assertNotNull(wpt.getLocation()); Assert.assertNotNull(wpt.getLocation());
Assert.assertNotNull(wpt.getTripStatistics()); Assert.assertNotNull(wpt.getTripStatistics());
@@ -384,149 +352,7 @@ public class TrackRecordingServiceTest {
// TODO: Check waypoint 2. // TODO: Check waypoint 2.
} }
@MediumTest private void createDummyTrack(long id, long stopTime, boolean isRecording) {
@Test
public void testInsertWaypointMarker_noRecordingTrack() throws Exception {
ITrackRecordingService service = ((ITrackRecordingService) mServiceRule.bindService(createStartIntent()));
Assert.assertFalse(service.isRecording());
long waypointId = service.insertWaypoint(WaypointCreationRequest.DEFAULT_WAYPOINT);
Assert.assertEquals(-1L, waypointId);
}
@MediumTest
@Test
public void testInsertWaypointMarker_validWaypoint() throws Exception {
createDummyTrack(123L, -1L, true);
ITrackRecordingService service = ((ITrackRecordingService) mServiceRule.bindService(createStartIntent()));
Assert.assertTrue(service.isRecording());
insertLocation(service);
Assert.assertEquals(1, service.insertWaypoint(WaypointCreationRequest.DEFAULT_WAYPOINT));
Waypoint wpt = providerUtils.getWaypoint(1);
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(123L, wpt.getTrackId());
Assert.assertEquals(0.0, wpt.getLength(), 0.01);
Assert.assertNotNull(wpt.getLocation());
Assert.assertNull(wpt.getTripStatistics());
}
@MediumTest
@Test
public void testWithProperties_voiceFrequencyDefault() throws Exception {
PreferencesUtils.setInt(context, R.string.voice_frequency_key, PreferencesUtils.VOICE_FREQUENCY_DEFAULT);
fullRecordingSession();
}
@MediumTest
@Test
public void testWithProperties_voiceFrequencyByDistance() throws Exception {
PreferencesUtils.setInt(context, R.string.voice_frequency_key, -1);
fullRecordingSession();
}
@MediumTest
@Test
public void testWithProperties_voiceFrequencyByTime() throws Exception {
PreferencesUtils.setInt(context, R.string.voice_frequency_key, 1);
fullRecordingSession();
}
@MediumTest
@Test
public void testWithProperties_maxRecordingDistanceDefault() throws Exception {
PreferencesUtils.setInt(context, R.string.max_recording_distance_key, PreferencesUtils.MAX_RECORDING_DISTANCE_DEFAULT);
fullRecordingSession();
}
@MediumTest
@Test
public void testWithProperties_maxRecordingDistance() throws Exception {
PreferencesUtils.setInt(context, R.string.max_recording_distance_key, 50);
fullRecordingSession();
}
@MediumTest
@Test
public void testWithProperties_minRecordingDistanceDefault() throws Exception {
PreferencesUtils.setInt(context, R.string.recording_distance_interval_key, PreferencesUtils.RECORDING_DISTANCE_INTERVAL_DEFAULT);
fullRecordingSession();
}
@MediumTest
@Test
public void testWithProperties_minRecordingDistance() throws Exception {
PreferencesUtils.setInt(context, R.string.recording_distance_interval_key, 2);
fullRecordingSession();
}
@MediumTest
@Test
public void testWithProperties_splitFrequencyDefault() throws Exception {
PreferencesUtils.setInt(context, R.string.split_frequency_key, PreferencesUtils.SPLIT_FREQUENCY_DEFAULT);
fullRecordingSession();
}
@MediumTest
@Test
public void testWithProperties_splitFrequencyByDistance() throws Exception {
PreferencesUtils.setInt(context, R.string.split_frequency_key, -1);
fullRecordingSession();
}
@MediumTest
@Test
public void testWithProperties_splitFrequencyByTime() throws Exception {
PreferencesUtils.setInt(context, R.string.split_frequency_key, 1);
fullRecordingSession();
}
@MediumTest
@Test
public void testWithProperties_metricUnitsDefault() throws Exception {
PreferencesUtils.setString(context, R.string.stats_units_key, PreferencesUtils.STATS_UNITS_DEFAULT);
fullRecordingSession();
}
@MediumTest
@Test
public void testWithProperties_metricUnitsDisabled() throws Exception {
PreferencesUtils.setString(context, R.string.stats_units_key, context.getString(R.string.stats_units_imperial));
fullRecordingSession();
}
@MediumTest
@Test
public void testWithProperties_minRecordingIntervalDefault() throws Exception {
PreferencesUtils.setInt(context, R.string.min_recording_interval_key, PreferencesUtils.MIN_RECORDING_INTERVAL_DEFAULT);
fullRecordingSession();
}
@MediumTest
@Test
public void testWithProperties_minRecordingInterval() throws Exception {
PreferencesUtils.setInt(context, R.string.min_recording_interval_key, 2);
fullRecordingSession();
}
@MediumTest
@Test
public void testWithProperties_minRequiredAccuracyDefault() throws Exception {
PreferencesUtils.setInt(context, R.string.recording_gps_accuracy_key, PreferencesUtils.RECORDING_GPS_ACCURACY_DEFAULT);
fullRecordingSession();
}
@MediumTest
@Test
public void testWithProperties_minRequiredAccuracy() throws Exception {
PreferencesUtils.setInt(context, R.string.recording_gps_accuracy_key, 500);
fullRecordingSession();
}
private Track createDummyTrack(long id, long stopTime, boolean isRecording) {
Track dummyTrack = new Track(); Track dummyTrack = new Track();
dummyTrack.setId(id); dummyTrack.setId(id);
dummyTrack.setName("Dummy Track"); dummyTrack.setName("Dummy Track");
@@ -534,18 +360,6 @@ public class TrackRecordingServiceTest {
tripStatistics.setStopTime(stopTime); tripStatistics.setStopTime(stopTime);
dummyTrack.setTripStatistics(tripStatistics); dummyTrack.setTripStatistics(tripStatistics);
addTrack(dummyTrack, isRecording); 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) { private void addTrack(Track track, boolean isRecording) {
@@ -556,56 +370,8 @@ public class TrackRecordingServiceTest {
PreferencesUtils.setBoolean(context, R.string.recording_track_paused_key, !isRecording); PreferencesUtils.setBoolean(context, R.string.recording_track_paused_key, !isRecording);
} }
private void fullRecordingSession() throws Exception {
ITrackRecordingService service = ((ITrackRecordingService) mServiceRule.bindService(createStartIntent()));
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 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();
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());
}
/** /**
* Inserts a location and waits for 100ms. * Inserts a location and waits for 200ms.
*
* @param trackRecordingService the track recording service
*/ */
private void insertLocation(ITrackRecordingService trackRecordingService) throws InterruptedException { private void insertLocation(ITrackRecordingService trackRecordingService) throws InterruptedException {
Location location = new Location("gps"); Location location = new Location("gps");
@@ -616,47 +382,38 @@ public class TrackRecordingServiceTest {
location.setTime(System.currentTimeMillis()); location.setTime(System.currentTimeMillis());
location.setBearing(3.0f); location.setBearing(3.0f);
trackRecordingService.insertTrackPoint(location); trackRecordingService.insertTrackPoint(location);
Thread.sleep(200); Thread.sleep(200);
} }
/** @MediumTest
* Synchronous/waitable broadcast receiver to be used in testing. @Test
*/ public void testInsertWaypointMarker_noRecordingTrack() throws Exception {
private class BlockingBroadcastReceiver extends BroadcastReceiver { ITrackRecordingService service = ((ITrackRecordingService) mServiceRule.bindService(createStartIntent(context)));
private static final long MAX_WAIT_TIME_MS = 10000; Assert.assertFalse(service.isRecording());
private final List<Intent> receivedIntents = new ArrayList<>();
public List<Intent> getReceivedIntents() { long waypointId = service.insertWaypoint(WaypointCreationRequest.DEFAULT_WAYPOINT);
return receivedIntents; Assert.assertEquals(-1L, waypointId);
} }
@Override @MediumTest
public void onReceive(Context ctx, Intent intent) { @Test
Log.d("Test", "Got broadcast: " + intent); public void testInsertWaypointMarker_validWaypoint() throws Exception {
synchronized (receivedIntents) { createDummyTrack(trackId, -1L, true);
receivedIntents.add(intent);
receivedIntents.notifyAll();
}
}
public boolean waitUntilReceived(int receiveCount) { ITrackRecordingService service = ((ITrackRecordingService) mServiceRule.bindService(createStartIntent(context)));
long deadline = System.currentTimeMillis() + MAX_WAIT_TIME_MS; Assert.assertTrue(service.isRecording());
synchronized (receivedIntents) { insertLocation(service);
while (receivedIntents.size() < receiveCount) {
try {
// Wait releases synchronized lock until it returns
receivedIntents.wait(500);
} catch (InterruptedException e) {
// Do nothing
}
if (System.currentTimeMillis() > deadline) { long waypointId = service.insertWaypoint(WaypointCreationRequest.DEFAULT_WAYPOINT);
return false; 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());
return true; Assert.assertEquals(trackId, wpt.getTrackId());
} Assert.assertEquals(0.0, wpt.getLength(), 0.01);
Assert.assertNotNull(wpt.getLocation());
Assert.assertNull(wpt.getTripStatistics());
} }
} }
@@ -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());
}
}
@@ -17,17 +17,19 @@
package de.dennisguse.opentracks.services.tasks; package de.dennisguse.opentracks.services.tasks;
import android.content.Context; import android.content.Context;
import android.os.Looper;
import androidx.test.core.app.ApplicationProvider; import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.AfterClass;
import org.junit.Assert; import org.junit.Assert;
import org.junit.BeforeClass;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
/** /**
* Tests for {@link AnnouncementPeriodicTaskFactory}. * Tests for {@link AnnouncementPeriodicTaskFactory}.
* These tests require Donut+ to run.
* *
* @author Rodrigo Damazio * @author Rodrigo Damazio
*/ */
@@ -36,6 +38,17 @@ public class AnnouncementPeriodicTaskFactoryTest {
private Context context = ApplicationProvider.getApplicationContext(); 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 @Test
public void testCreate() { public void testCreate() {
PeriodicTaskFactory factory = new AnnouncementPeriodicTaskFactory(); PeriodicTaskFactory factory = new AnnouncementPeriodicTaskFactory();
@@ -1,549 +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.services.tasks;
import android.content.Context;
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 androidx.test.core.app.ApplicationProvider;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
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 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.
*
* @author Rodrigo Damazio
*/
@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 ArgumentCaptor<OnInitListener> initListenerCapture = ArgumentCaptor.forClass(OnInitListener.class);
private ArgumentCaptor<PhoneStateListener> phoneListenerCapture = ArgumentCaptor.forClass(PhoneStateListener.class);
private TextToSpeechDelegate ttsDelegate;
@Mock
private TextToSpeechInterface tts;
// @UsesMocks({
// StringUtils.class,
// })
@Before
public void setUp() throws Exception {
oldDefaultLocale = Locale.getDefault();
Locale.setDefault(DEFAULT_LOCALE);
// Eww, the effort required just to mock TextToSpeech is insane
final AtomicBoolean listenerCalled = new AtomicBoolean();
OnInitListener blockingListener = new OnInitListener() {
@Override
public void onInit(int status) {
synchronized (this) {
listenerCalled.set(true);
notify();
}
}
};
ttsDelegate = new TextToSpeechDelegate(context, blockingListener);
// Wait for all async operations done in the constructor to finish.
synchronized (blockingListener) {
while (!listenerCalled.get()) {
// Releases the synchronized lock until we're woken up.
blockingListener.wait();
}
}
// Create a partial forwarding mock
task = new AnnouncementPeriodicTask(context) {
@Override
protected TextToSpeech newTextToSpeech(Context ctx, OnInitListener onInitListener) {
return mockTask.newTextToSpeech(ctx, onInitListener);
}
@Override
protected String getAnnouncement(TripStatistics stats) {
return mockTask.getAnnouncement(stats);
}
@Override
protected void listenToPhoneState(PhoneStateListener listener, int events) {
mockTask.listenToPhoneState(listener, events);
}
};
}
@After
public void tearDown() {
Locale.setDefault(oldDefaultLocale);
}
@Test
public void testStart() {
doStart();
OnInitListener ttsInitListener = initListenerCapture.getValue();
Assert.assertNotNull(ttsInitListener);
ttsInitListener.onInit(TextToSpeech.SUCCESS);
//TODO
//verify(mockTask, tts);
}
@Test
public void testStart_notReady() {
doStart();
OnInitListener ttsInitListener = initListenerCapture.getValue();
Assert.assertNotNull(ttsInitListener);
ttsInitListener.onInit(TextToSpeech.ERROR);
//TODO
//AndroidMock.verify(mockTask, tts);
}
@Test
public void testShutdown() {
// First, start
doStart();
verify(mockTask);
reset(mockTask);
// Then, shut down
PhoneStateListener phoneListener = phoneListenerCapture.getValue();
mockTask.listenToPhoneState(
same(phoneListener), eq(PhoneStateListener.LISTEN_NONE));
tts.shutdown();
task.shutdown();
//TODO
//AndroidMock.verify(mockTask, tts);
}
@Test
public void testRun() {
// when service data calls
TripStatistics stats = new TripStatistics();
// when announcement building call
when(mockTask.getAnnouncement(same(stats))).thenReturn(ANNOUNCEMENT);
// Put task in "ready" state
startTask(TextToSpeech.SUCCESS);
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
task.announce(stats);
//TODO
//AndroidMock.verify(mockTask, tts);
}
@Test
public void testRun_notReady() {
// Put task in "not ready" state
startTask(TextToSpeech.ERROR);
// Run the announcement
task.run(null);
//TODO
//verify(mockTask, tts);
}
@Test
public void testRun_duringCall() {
startTask(TextToSpeech.SUCCESS);
when(tts.isSpeaking()).thenReturn(false);
// Run the announcement
PhoneStateListener phoneListener = phoneListenerCapture.getValue();
phoneListener.onCallStateChanged(TelephonyManager.CALL_STATE_OFFHOOK, null);
task.run(null);
//TODO
//verify(mockTask, tts);
}
@Test
public void testRun_ringWhileSpeaking() {
startTask(TextToSpeech.SUCCESS);
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();
phoneListener.onCallStateChanged(TelephonyManager.CALL_STATE_RINGING, null);
// Run the announcement - this should do nothing.
task.run(null);
//TODO
//AndroidMock.verify(mockTask, tts);
}
@Test
public void testRun_whileRinging() {
startTask(TextToSpeech.SUCCESS);
when(tts.isSpeaking()).thenReturn(false);
// Run the announcement
PhoneStateListener phoneListener = phoneListenerCapture.getValue();
phoneListener.onCallStateChanged(TelephonyManager.CALL_STATE_RINGING, null);
task.run(null);
//TODO
//AndroidMock.verify(mockTask, tts);
}
@Test
public void testRun_noService() {
startTask(TextToSpeech.SUCCESS);
// Run the announcement
task.run(null);
//TODO
//AndroidMock.verify(mockTask, tts);
}
@Test
public void testRun_noStats() {
// when service data calls
startTask(TextToSpeech.SUCCESS);
// Run the announcement
task.run(null);
//TODO
//AndroidMock.verify(mockTask, tts);
}
/**
* Tests {@link AnnouncementPeriodicTask#getAnnounceTime(long)} with time zero.
*/
@Test
public void testGetAnnounceTime_time_zero() {
long time = 0; // 0 seconds
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
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
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
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
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
Assert.assertEquals("2 minutes 2 seconds", task.getAnnounceTime(time));
}
private void startTask(int state) {
reset(tts);
doStart();
OnInitListener ttsInitListener = initListenerCapture.getValue();
ttsInitListener.onInit(state);
//TODO
reset(tts);
//AndroidMock.resetToDefault(tts);
}
private void doStart() {
mockTask.listenToPhoneState(phoneListenerCapture.capture(), eq(PhoneStateListener.LISTEN_CALL_STATE));
when(mockTask.newTextToSpeech(same(context), initListenerCapture.capture())).thenReturn(ttsDelegate);
task.start();
}
/**
* Mockable interface that we delegate TTS calls to.
*/
interface TextToSpeechInterface {
int addEarcon(String earcon, String packagename, int resourceId);
int addEarcon(String earcon, String filename);
int addSpeech(String text, String packagename, int resourceId);
int addSpeech(String text, String filename);
boolean areDefaultsEnforced();
String getDefaultEngine();
Locale getLanguage();
int isLanguageAvailable(Locale loc);
boolean isSpeaking();
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 setLanguage(Locale loc);
int setOnUtteranceCompletedListener(OnUtteranceCompletedListener listener);
int setPitch(float pitch);
int setSpeechRate(float speechRate);
void shutdown();
int speak(String text, int queueMode, HashMap<String, String> params);
int stop();
int synthesizeToFile(String text, HashMap<String, String> params,
String filename);
}
/**
* Subclass of {@link TextToSpeech} which delegates calls to the interface
* above.
* The logic here is stupid and the author is ashamed of having to write it
* like this, but basically the issue is that TextToSpeech cannot be mocked
* without running its constructor, its constructor runs async operations
* which call other methods (and then if the methods are part of a mock we'd
* have to set a behavior, but we can't 'cause the object hasn't been fully
* built yet).
* The logic is that calls made during the constructor (when tts is not yet
* 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 {
public TextToSpeechDelegate(Context context, OnInitListener listener) {
super(context, listener);
}
@Override
public int addEarcon(String earcon, String packagename, int resourceId) {
if (tts == null) {
return super.addEarcon(earcon, packagename, resourceId);
}
return tts.addEarcon(earcon, packagename, resourceId);
}
@Override
public int addEarcon(String earcon, String filename) {
if (tts == null) {
return super.addEarcon(earcon, filename);
}
return tts.addEarcon(earcon, filename);
}
@Override
public int addSpeech(String text, String packagename, int resourceId) {
if (tts == null) {
return super.addSpeech(text, packagename, resourceId);
}
return tts.addSpeech(text, packagename, resourceId);
}
@Override
public int addSpeech(String text, String filename) {
if (tts == null) {
return super.addSpeech(text, filename);
}
return tts.addSpeech(text, filename);
}
@Override
public Locale getLanguage() {
if (tts == null) {
return super.getLanguage();
}
return tts.getLanguage();
}
@Override
public int isLanguageAvailable(Locale loc) {
if (tts == null) {
return super.isLanguageAvailable(loc);
}
return tts.isLanguageAvailable(loc);
}
@Override
public boolean isSpeaking() {
if (tts == null) {
return super.isSpeaking();
}
return tts.isSpeaking();
}
@Override
public int playEarcon(String earcon, int queueMode, HashMap<String, String> params) {
if (tts == null) {
return super.playEarcon(earcon, queueMode, params);
}
return tts.playEarcon(earcon, queueMode, params);
}
@Override
public int playSilence(long durationInMs, int queueMode, HashMap<String, String> params) {
if (tts == null) {
return super.playSilence(durationInMs, queueMode, params);
}
return tts.playSilence(durationInMs, queueMode, params);
}
@Override
public int setLanguage(Locale loc) {
if (tts == null) {
return super.setLanguage(loc);
}
return tts.setLanguage(loc);
}
@Override
public int setOnUtteranceCompletedListener(OnUtteranceCompletedListener listener) {
if (tts == null) {
return super.setOnUtteranceCompletedListener(listener);
}
return tts.setOnUtteranceCompletedListener(listener);
}
@Override
public int setPitch(float pitch) {
if (tts == null) {
return super.setPitch(pitch);
}
return tts.setPitch(pitch);
}
@Override
public int setSpeechRate(float speechRate) {
if (tts == null) {
return super.setSpeechRate(speechRate);
}
return tts.setSpeechRate(speechRate);
}
@Override
public void shutdown() {
if (tts == null) {
super.shutdown();
return;
}
tts.shutdown();
}
@Override
public int speak(
String text, int queueMode, HashMap<String, String> params) {
if (tts == null) {
return super.speak(text, queueMode, params);
}
return tts.speak(text, queueMode, params);
}
@Override
public int stop() {
if (tts == null) {
return super.stop();
}
return tts.stop();
}
@Override
public int synthesizeToFile(String text, HashMap<String, String> params, String filename) {
if (tts == null) {
return super.synthesizeToFile(text, params, filename);
}
return tts.synthesizeToFile(text, params, filename);
}
}
}
@@ -32,16 +32,13 @@ public class FileUtilsTest extends TestCase {
* Tests {@link FileUtils#getPath(String...)}. * Tests {@link FileUtils#getPath(String...)}.
*/ */
public void testBuildExternalDirectoryPath() { public void testBuildExternalDirectoryPath() {
String expectedName = Environment.getExternalStorageDirectory() + File.separator String expectedName = Environment.getExternalStorageDirectory() + File.separator + FileUtils.SDCARD_TOP_DIR + File.separator + "a" + File.separator + "b" + File.separator + "c";
+ FileUtils.SDCARD_TOP_DIR + File.separator + "a" + File.separator + "b" + File.separator
+ "c";
String dirName = FileUtils.getPath("a", "b", "c"); String dirName = FileUtils.getPath("a", "b", "c");
assertEquals(expectedName, dirName); assertEquals(expectedName, dirName);
} }
/** /**
* Tests {@link FileUtils#buildUniqueFileName(File, String, String)} when the * Tests {@link FileUtils#buildUniqueFileName(File, String, String)} when the file is new.
* file is new.
*/ */
public void testBuildUniqueFileName_new() { public void testBuildUniqueFileName_new() {
String filename = FileUtils.buildUniqueFileName(new File("/dir"), "Filename", "ext"); 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 * Tests {@link FileUtils#buildUniqueFileName(File, String, String)} when the file exists already.
* file exists already.
*/ */
public void testBuildUniqueFileName_exist() { public void testBuildUniqueFileName_exist() {
// Expect "/default.prop" to exist on the phone/emulator // 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 * Tests {@link FileUtils#sanitizeFileName(String)} with i18n characters (in Chinese and Russian).
* Chinese and Russian). Verifies that they are allowed. * Verifies that they are allowed.
*/ */
public void testSanitizeFileName_i18n() { public void testSanitizeFileName_i18n() {
String name = "您好-привет"; String name = "您好-привет";
@@ -79,8 +75,8 @@ public class FileUtilsTest extends TestCase {
} }
/** /**
* Tests {@link FileUtils#sanitizeFileName(String)} with special FAT32 * Tests {@link FileUtils#sanitizeFileName(String)} with special FAT32 characters.
* characters. Verifies that they are allowed. * Verifies that they are allowed.
*/ */
public void testSanitizeFileName_special_characters() { public void testSanitizeFileName_special_characters() {
String name = "$%'-_@~`!(){}^#&+,;=[] "; String name = "$%'-_@~`!(){}^#&+,;=[] ";
@@ -89,8 +85,8 @@ public class FileUtilsTest extends TestCase {
} }
/** /**
* Tests {@link FileUtils#sanitizeFileName(String)} with multiple escaped * Tests {@link FileUtils#sanitizeFileName(String)} with multiple escaped characters in a row.
* characters in a row. Verifies that they are collapsed into one underscore. * Verifies that they are collapsed into one underscore.
*/ */
public void testSanitizeFileName_collapse() { public void testSanitizeFileName_collapse() {
String name = "hello//there"; String name = "hello//there";
@@ -99,8 +95,8 @@ public class FileUtilsTest extends TestCase {
} }
/** /**
* Tests {@link FileUtils#truncateFileName(File, String, String)}. Verifies * Tests {@link FileUtils#truncateFileName(File, String, String)}.
* the a long file name is truncated. * Verifies the a long file name is truncated.
*/ */
public void testTruncateFileName() { public void testTruncateFileName() {
File directory = new File("/dir1/dir2/"); File directory = new File("/dir1/dir2/");
@@ -115,7 +111,6 @@ public class FileUtilsTest extends TestCase {
for (int i = 0; i < truncated.length(); i++) { for (int i = 0; i < truncated.length(); i++) {
assertEquals('a', truncated.charAt(i)); assertEquals('a', truncated.charAt(i));
} }
assertEquals(FileUtils.MAX_FAT32_PATH_LENGTH, assertEquals(FileUtils.MAX_FAT32_PATH_LENGTH, new File(directory, truncated + suffix).getPath().length());
new File(directory, truncated + suffix).getPath().length());
} }
} }
@@ -131,7 +131,7 @@ public class StringUtilsTest {
assertGetTime("2010-05-04T03:02:01.3", 2010, 5, 4, 3, 2, 1, 300); 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); assertGetTime("2010-05-04T03:02:01.35", 2010, 5, 4, 3, 2, 1, 350);
assertGetTime("2010-05-04T03:02:01.352Z", 2010, 5, 4, 3, 2, 1, 352); assertGetTime("2010-05-04T03:02:01.352Z", 2010, 5, 4, 3, 2, 1, 352);
assertGetTime("2010-05-04T03:02:01.3525Z", 2010, 5, 4, 3, 2, 1, 352); assertGetTime("2010-05-04T03:02:01.3529Z", 2010, 5, 4, 3, 2, 1, 353);
} }
/** /**
@@ -149,8 +149,7 @@ public class StringUtilsTest {
} }
/** /**
* Tests {@link StringUtils#getTime(String)} with fractional seconds and time * Tests {@link StringUtils#getTime(String)} with fractional seconds and time zone.
* zone.
*/ */
@Test @Test
public void testGetTime_fractionalAndTimezone() { public void testGetTime_fractionalAndTimezone() {
@@ -161,8 +160,7 @@ public class StringUtilsTest {
} }
/** /**
* Asserts the {@link StringUtils#getTime(String)} returns the expected * Asserts the {@link StringUtils#getTime(String)} returns the expected values.
* values.
* *
* @param xmlDateTime the xml date time string * @param xmlDateTime the xml date time string
* @param year the expected year * @param year the expected year
@@ -173,8 +171,7 @@ public class StringUtilsTest {
* @param second the expected second * @param second the expected second
* @param millisecond the expected milliseconds * @param millisecond the expected milliseconds
*/ */
private void assertGetTime(String xmlDateTime, int year, int month, int day, int hour, int minute, private void assertGetTime(String xmlDateTime, int year, int month, int day, int hour, int minute, int second, int millisecond) {
int second, int millisecond) {
GregorianCalendar calendar = new GregorianCalendar(TimeZone.getTimeZone("UTC")); GregorianCalendar calendar = new GregorianCalendar(TimeZone.getTimeZone("UTC"));
calendar.set(year, month - 1, day, hour, minute, second); calendar.set(year, month - 1, day, hour, minute, second);
calendar.set(GregorianCalendar.MILLISECOND, millisecond); calendar.set(GregorianCalendar.MILLISECOND, millisecond);
@@ -185,7 +182,7 @@ public class StringUtilsTest {
* Tests {@link StringUtils#getTimeParts(long)} with a positive number. * Tests {@link StringUtils#getTimeParts(long)} with a positive number.
*/ */
@Test @Test
public void testGetTimeParts_postive() { public void testGetTimeParts_positive() {
int[] parts = StringUtils.getTimeParts(61000); int[] parts = StringUtils.getTimeParts(61000);
Assert.assertEquals(1, parts[0]); Assert.assertEquals(1, parts[0]);
Assert.assertEquals(1, parts[1]); Assert.assertEquals(1, parts[1]);
@@ -202,4 +199,24 @@ public class StringUtilsTest {
Assert.assertEquals(-1, parts[1]); Assert.assertEquals(-1, parts[1]);
Assert.assertEquals(0, parts[2]); Assert.assertEquals(0, parts[2]);
} }
@Test
public void testFormatDecimal() {
Assert.assertEquals("0", StringUtils.formatDecimal(0.0, 0));
Assert.assertEquals("0", StringUtils.formatDecimal(0.1, 0));
Assert.assertEquals("1", StringUtils.formatDecimal(1.1, 0));
Assert.assertEquals("10", StringUtils.formatDecimal(10, 0));
Assert.assertEquals("10", StringUtils.formatDecimal(10.1, 0));
Assert.assertEquals("0", StringUtils.formatDecimal(-0.1, 0));
Assert.assertEquals("0", StringUtils.formatDecimal(0.0, 2));
Assert.assertEquals("0.1", StringUtils.formatDecimal(0.1, 2));
Assert.assertEquals("1.1", StringUtils.formatDecimal(1.1, 2));
Assert.assertEquals("10", StringUtils.formatDecimal(10, 2));
Assert.assertEquals("10.1", StringUtils.formatDecimal(10.1, 2));
Assert.assertEquals("10.11", StringUtils.formatDecimal(10.111, 2));
Assert.assertEquals("-0.1", StringUtils.formatDecimal(-0.1, 2));
Assert.assertEquals("1", StringUtils.formatDecimal(0.99, 1));
}
} }
@@ -45,8 +45,7 @@ public class TrackNameUtilsTest {
private static final Context CONTEXT = ApplicationProvider.getApplicationContext(); private static final Context CONTEXT = ApplicationProvider.getApplicationContext();
/** /**
* Tests when the track_name_key is * Tests when the track_name_key is settings_recording_track_name_date_local_value.
* settings_recording_track_name_date_local_value.
*/ */
@Test @Test
public void testTrackName_date_local() { public void testTrackName_date_local() {
@@ -55,8 +54,7 @@ public class TrackNameUtilsTest {
} }
/** /**
* Tests when the track_name_key is * Tests when the track_name_key is settings_recording_track_name_date_iso_8601_value.
* settings_recording_track_name_date_iso_8601_value.
*/ */
@Test @Test
public void testTrackName_date_iso_8601() { public void testTrackName_date_iso_8601() {
@@ -66,8 +64,7 @@ public class TrackNameUtilsTest {
} }
/** /**
* Tests when the track_name_key is * Tests when the track_name_key is settings_recording_track_name_number_value.
* settings_recording_track_name_number_value.
*/ */
@Test @Test
public void testTrackName_number() { public void testTrackName_number() {
+86 -98
View File
@@ -14,75 +14,24 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
--> -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="de.dennisguse.opentracks" package="de.dennisguse.opentracks"
android:installLocation="auto" android:installLocation="auto"
android:versionCode="2991" android:versionCode="3101"
android:versionName="v3.1.0"> <!-- For FDroid. --> android:versionName="v3.2.4"> <!-- For FDroid. -->
<!-- Permission to initialize services --> <!-- 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 <application
android:name=".Startup" android:name=".Startup"
android:allowBackup="false" android:allowBackup="false"
android:hardwareAccelerated="true" android:hardwareAccelerated="true"
android:icon="@drawable/ic_logo" android:icon="@drawable/ic_logo_color_24dp"
android:label="@string/app_name" android:label="@string/app_name"
android:theme="@style/ThemeCustom" android:theme="@style/ThemeCustom">
tools:replace="android:label">
<!-- Activities --> <!-- Activities -->
<activity android:name=".AboutActivity" /> <activity android:name=".AboutActivity" />
<activity
android:name=".AggregatedStatsActivity"
android:label="@string/menu_aggregated_statistics" />
<activity android:name=".DeleteActivity" /> <activity android:name=".DeleteActivity" />
<activity <activity
android:name=".io.file.importer.ImportActivity" android:name=".io.file.importer.ImportActivity"
android:theme="@style/ThemeCustomTransparent"> android:theme="@style/ThemeCustomTransparent">
@@ -100,8 +49,11 @@ limitations under the License.
<data android:scheme="file" /> <data android:scheme="file" />
</intent-filter> </intent-filter>
</activity> </activity>
<activity android:name=".MarkerEditActivity" /> <activity android:name=".MarkerEditActivity" />
<activity android:name=".MarkerDetailActivity" /> <activity android:name=".MarkerDetailActivity" />
<activity <activity
android:name=".MarkerListActivity" android:name=".MarkerListActivity"
android:label="@string/menu_markers"> android:label="@string/menu_markers">
@@ -109,6 +61,7 @@ limitations under the License.
android:name="android.app.default_searchable" android:name="android.app.default_searchable"
android:value=".SearchListActivity" /> android:value=".SearchListActivity" />
</activity> </activity>
<activity <activity
android:name=".SearchListActivity" android:name=".SearchListActivity"
android:launchMode="singleTop"> android:launchMode="singleTop">
@@ -117,12 +70,15 @@ limitations under the License.
</intent-filter> </intent-filter>
<meta-data <meta-data
android:name="android.app.searchable" android:name="android.app.searchable"
android:resource="@xml/search" /> android:resource="@xml/searchable" />
</activity> </activity>
<activity <activity
android:name=".TrackDetailActivity" android:name=".TrackDetailActivity"
android:screenOrientation="userPortrait" /> android:screenOrientation="userPortrait" />
<activity android:name=".TrackEditActivity" /> <activity android:name=".TrackEditActivity" />
<activity android:name=".TrackListActivity"> <activity android:name=".TrackListActivity">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
@@ -132,54 +88,20 @@ limitations under the License.
android:name="android.app.default_searchable" android:name="android.app.default_searchable"
android:value=".SearchListActivity" /> android:value=".SearchListActivity" />
</activity> </activity>
<activity <activity
android:name=".io.file.exporter.ExportActivity" android:name=".io.file.exporter.ExportActivity"
android:theme="@style/ThemeCustomTransparent" /> android:theme="@style/ThemeCustomTransparent" />
<activity android:name=".settings.RecordingSettingsActivity" />
<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 <activity android:name=".settings.SettingsActivity" />
android:name=".services.ControlRecordingService"
android:exported="false" /> <!-- Services -->
<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 -->
<provider <provider
android:name=".content.ShareContentProvider" android:name=".content.ShareContentProvider"
android:authorities="${applicationId}.content" android:authorities="${applicationId}.content"
android:exported="false" android:exported="false"
android:grantUriPermissions="true" /> android:grantUriPermissions="true" />
<provider <provider
android:name="androidx.core.content.FileProvider" android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider" android:authorities="${applicationId}.fileprovider"
@@ -189,5 +111,71 @@ limitations under the License.
android:name="android.support.FILE_PROVIDER_PATHS" android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" /> android:resource="@xml/file_paths" />
</provider> </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> </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> </manifest>
@@ -18,16 +18,14 @@ package de.dennisguse.opentracks;
import android.os.Bundle; import android.os.Bundle;
import android.speech.tts.TextToSpeech; import android.speech.tts.TextToSpeech;
import android.view.Menu;
import androidx.fragment.app.FragmentActivity; import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import de.dennisguse.opentracks.util.TrackIconUtils;
/** /**
* @author Jimmy Shih * @author Jimmy Shih
*/ */
public abstract class AbstractActivity extends FragmentActivity { public abstract class AbstractActivity extends AppCompatActivity {
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
@@ -37,16 +35,13 @@ public abstract class AbstractActivity extends FragmentActivity {
setVolumeControlStream(TextToSpeech.Engine.DEFAULT_STREAM); setVolumeControlStream(TextToSpeech.Engine.DEFAULT_STREAM);
setContentView(getLayoutResId()); setContentView(getLayoutResId());
Toolbar toolbar = findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
} }
/** /**
* Gets the layout resource id. * Gets the layout resource id.
*/ */
protected abstract int getLayoutResId(); protected abstract int getLayoutResId();
@Override
public boolean onCreateOptionsMenu(Menu menu) {
TrackIconUtils.setMenuIconColor(menu);
return super.onCreateOptionsMenu(menu);
}
} }
@@ -16,27 +16,13 @@
package de.dennisguse.opentracks; package de.dennisguse.opentracks;
import android.app.Activity;
import android.app.SearchManager;
import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.util.SparseBooleanArray;
import android.view.ActionMode;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.AbsListView;
import android.widget.ListView;
import android.widget.SearchView;
import java.util.ArrayList;
import de.dennisguse.opentracks.fragments.ConfirmDeleteDialogFragment; import de.dennisguse.opentracks.fragments.ConfirmDeleteDialogFragment;
import de.dennisguse.opentracks.fragments.ConfirmDeleteDialogFragment.ConfirmDeleteCaller; import de.dennisguse.opentracks.fragments.ConfirmDeleteDialogFragment.ConfirmDeleteCaller;
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection; import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
import de.dennisguse.opentracks.util.IntentUtils; import de.dennisguse.opentracks.util.IntentUtils;
import de.dennisguse.opentracks.util.PreferencesUtils; import de.dennisguse.opentracks.util.PreferencesUtils;
import de.dennisguse.opentracks.util.TrackRecordingServiceConnectionUtils;
/** /**
* An abstract class for the following common tasks across * An abstract class for the following common tasks across
@@ -50,112 +36,11 @@ import de.dennisguse.opentracks.util.TrackRecordingServiceConnectionUtils;
*/ */
public abstract class AbstractListActivity 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; protected static final int GPS_REQUEST_CODE = 6;
private static final int DELETE_REQUEST_CODE = 3; private static final int DELETE_REQUEST_CODE = 3;
public static void configureListViewContextualMenu(final ListView listView, final ContextualActionModeCallback contextualActionModeCallback) {
listView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE_MODAL);
listView.setMultiChoiceModeListener(new AbsListView.MultiChoiceModeListener() {
@Override
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
mode.getMenuInflater().inflate(R.menu.list_context_menu, menu);
return true;
}
@Override
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
contextualActionModeCallback.onPrepare(menu, getCheckedPositions(listView), listView.getCheckedItemIds(), true);
return true;
}
@Override
public void onDestroyActionMode(ActionMode mode) {
// Do nothing
}
@Override
public void onItemCheckedStateChanged(ActionMode mode, int position, long id, boolean checked) {
mode.invalidate();
}
@Override
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
if (contextualActionModeCallback.onClick(item.getItemId(), getCheckedPositions(listView), listView.getCheckedItemIds())) {
mode.finish();
}
return true;
}
/**
* Gets the checked positions in a list view.
*
* @param list the list view
*/
private int[] getCheckedPositions(ListView list) {
SparseBooleanArray positions = list.getCheckedItemPositions();
ArrayList<Integer> arrayList = new ArrayList<>();
for (int i = 0; i < positions.size(); i++) {
int key = positions.keyAt(i);
if (positions.valueAt(i)) {
arrayList.add(key);
}
}
int[] result = new int[arrayList.size()];
for (int i = 0; i < arrayList.size(); i++) {
result[i] = arrayList.get(i);
}
return result;
}
});
}
public static void configureSearchWidget(Activity activity, final MenuItem menuItem, final TrackController trackController) {
SearchManager searchManager = (SearchManager) activity.getSystemService(Context.SEARCH_SERVICE);
SearchView searchView = (SearchView) menuItem.getActionView();
searchView.setSearchableInfo(searchManager.getSearchableInfo(activity.getComponentName()));
searchView.setQueryRefinementEnabled(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
if (trackController != null) {
if (hasFocus) {
trackController.hide();
} else {
trackController.show();
}
}
}
});
searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
@Override
public boolean onQueryTextSubmit(String query) {
menuItem.collapseActionView();
return false;
}
@Override
public boolean onQueryTextChange(String newText) {
return false;
}
});
searchView.setOnSuggestionListener(new SearchView.OnSuggestionListener() {
@Override
public boolean onSuggestionSelect(int position) {
return false;
}
@Override
public boolean onSuggestionClick(int position) {
menuItem.collapseActionView();
return false;
}
});
}
@Override @Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) { protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == DELETE_REQUEST_CODE) { if (requestCode == DELETE_REQUEST_CODE) {
@@ -171,8 +56,7 @@ public abstract class AbstractListActivity extends AbstractActivity implements C
* @param trackIds the track ids * @param trackIds the track ids
*/ */
protected void deleteTracks(long[] trackIds) { protected void deleteTracks(long[] trackIds) {
ConfirmDeleteDialogFragment.newInstance(trackIds) ConfirmDeleteDialogFragment.showDialog(getSupportFragmentManager(), trackIds);
.show(getSupportFragmentManager(), ConfirmDeleteDialogFragment.CONFIRM_DELETE_DIALOG_TAG);
} }
@Override @Override
@@ -190,7 +74,7 @@ public abstract class AbstractListActivity extends AbstractActivity implements C
} }
} }
if (stopRecording) { if (stopRecording) {
TrackRecordingServiceConnectionUtils.stopRecording(this, getTrackRecordingServiceConnection(), false); getTrackRecordingServiceConnection().stopRecording(this, false);
} }
Intent intent = IntentUtils.newIntent(this, DeleteActivity.class); Intent intent = IntentUtils.newIntent(this, DeleteActivity.class);
intent.putExtra(DeleteActivity.EXTRA_TRACK_IDS, trackIds); intent.putExtra(DeleteActivity.EXTRA_TRACK_IDS, trackIds);
@@ -198,8 +82,8 @@ public abstract class AbstractListActivity extends AbstractActivity implements C
} }
/** /**
* Gets the track recording service connection. For stopping the current * Gets the track recording service connection.
* recording if need to delete the current recording track. * For stopping the current recording if need to delete the current recording track.
*/ */
abstract protected TrackRecordingServiceConnection getTrackRecordingServiceConnection(); abstract protected TrackRecordingServiceConnection getTrackRecordingServiceConnection();
@@ -1,63 +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;
import android.os.Bundle;
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.
*
* @author Fergus Nelson
*/
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);
}
@Override
protected int getLayoutResId() {
return R.layout.stats;
}
/**
* Gets the aggregated trip statistics for all the recorded tracks or null if
* there is no track.
*/
private TripStatistics getTripStatistics() {
List<Track> tracks = ContentProviderUtils.Factory.get(this).getAllTracks();
TripStatistics tripStatistics = null;
if (!tracks.isEmpty()) {
tripStatistics = new TripStatistics(tracks.iterator().next().getTripStatistics());
for (int i = 1; i < tracks.size(); i++) {
tripStatistics.merge(tracks.get(i).getTripStatistics());
}
}
return tripStatistics;
}
}
@@ -25,16 +25,16 @@ import android.graphics.Path;
import androidx.annotation.VisibleForTesting; import androidx.annotation.VisibleForTesting;
import de.dennisguse.opentracks.stats.ExtremityMonitor;
import java.text.NumberFormat; import java.text.NumberFormat;
import de.dennisguse.opentracks.stats.ExtremityMonitor;
/** /**
* This class encapsulates the meta data for one series of the chart values. * This class encapsulates the meta data for one series of the chart values.
* *
* @author Sandor Dornbush * @author Sandor Dornbush
*/ */
public class ChartValueSeries { class ChartValueSeries {
private static final float STROKE_WIDTH = 2f; private static final float STROKE_WIDTH = 2f;
@@ -68,8 +68,7 @@ public class ChartValueSeries {
* @param fillColor the fill color * @param fillColor the fill color
* @param strokeColor the stroke color * @param strokeColor the stroke color
*/ */
public ChartValueSeries(Context context, int absoluteMin, int absoluteMax, int[] intervalValues, ChartValueSeries(Context context, int absoluteMin, int absoluteMax, int[] intervalValues, int metricTitleId, int imperialTitleId, int fillColor, int strokeColor) {
int metricTitleId, int imperialTitleId, int fillColor, int strokeColor) {
this.absoluteMin = absoluteMin; this.absoluteMin = absoluteMin;
this.absoluteMax = absoluteMax; this.absoluteMax = absoluteMax;
this.intervalValues = intervalValues; this.intervalValues = intervalValues;
@@ -108,7 +107,7 @@ public class ChartValueSeries {
/** /**
* Returns true if the series is enabled. * Returns true if the series is enabled.
*/ */
public boolean isEnabled() { boolean isEnabled() {
return enabled; return enabled;
} }
@@ -117,14 +116,14 @@ public class ChartValueSeries {
* *
* @param enabled true to enable * @param enabled true to enable
*/ */
public void setEnabled(boolean enabled) { void setEnabled(boolean enabled) {
this.enabled = enabled; this.enabled = enabled;
} }
/** /**
* Returns true if the series has data. * Returns true if the series has data.
*/ */
public boolean hasData() { boolean hasData() {
return extremityMonitor.hasData(); return extremityMonitor.hasData();
} }
@@ -133,14 +132,14 @@ public class ChartValueSeries {
* *
* @param value the new value * @param value the new value
*/ */
public void update(double value) { void update(double value) {
extremityMonitor.update(value); extremityMonitor.update(value);
} }
/** /**
* Gets the path. * Gets the path.
*/ */
public Path getPath() { Path getPath() {
return path; return path;
} }
@@ -149,7 +148,7 @@ public class ChartValueSeries {
* *
* @param canvas the canvas * @param canvas the canvas
*/ */
public void drawPath(Canvas canvas) { void drawPath(Canvas canvas) {
canvas.drawPath(path, fillPaint); canvas.drawPath(path, fillPaint);
canvas.drawPath(path, strokePaint); canvas.drawPath(path, strokePaint);
} }
@@ -157,7 +156,7 @@ public class ChartValueSeries {
/** /**
* Updates the y axis dimension. * Updates the y axis dimension.
*/ */
public void updateDimension() { void updateDimension() {
double min = hasData() ? extremityMonitor.getMin() : 0.0; double min = hasData() ? extremityMonitor.getMin() : 0.0;
double max = hasData() ? extremityMonitor.getMax() : 1.0; double max = hasData() ? extremityMonitor.getMax() : 1.0;
min = Math.max(min, absoluteMin); min = Math.max(min, absoluteMin);
@@ -204,14 +203,15 @@ public class ChartValueSeries {
/** /**
* Gets the interval value. * Gets the interval value.
*/ */
public int getInterval() { @VisibleForTesting
int getInterval() {
return interval; return interval;
} }
/** /**
* Gets the min marker value. * Gets the min marker value.
*/ */
public int getMinMarkerValue() { int getMinMarkerValue() {
return minMarkerValue; return minMarkerValue;
} }
@@ -226,28 +226,28 @@ public class ChartValueSeries {
/** /**
* Gets the title id. * Gets the title id.
*/ */
public int getTitleId(boolean metricUnits) { int getTitleId(boolean metricUnits) {
return metricUnits ? metricTitleId : imperialTitleId; return metricUnits ? metricTitleId : imperialTitleId;
} }
/** /**
* Gets the title paint. * Gets the title paint.
*/ */
public Paint getTitlePaint() { Paint getTitlePaint() {
return titlePaint; return titlePaint;
} }
/** /**
* Gets the marker paint. * Gets the marker paint.
*/ */
public Paint getMarkerPaint() { Paint getMarkerPaint() {
return markerPaint; return markerPaint;
} }
/** /**
* Gets the largest marker. * Gets the largest marker.
*/ */
public String getLargestMarker() { String getLargestMarker() {
String minMarker = numberFormat.format(getMinMarkerValue()); String minMarker = numberFormat.format(getMinMarkerValue());
String maxMarker = numberFormat.format(getMaxMarkerValue()); String maxMarker = numberFormat.format(getMaxMarkerValue());
return minMarker.length() >= maxMarker.length() ? minMarker : maxMarker; return minMarker.length() >= maxMarker.length() ? minMarker : maxMarker;
@@ -258,7 +258,7 @@ public class ChartValueSeries {
* *
* @param value the value * @param value the value
*/ */
public String formatMarker(int value) { String formatMarker(int value) {
return numberFormat.format(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.setColor(context.getResources().getColor(android.R.color.darker_gray));
markerPaint.setAntiAlias(false); 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()); pointer.setBounds(0, 0, pointer.getIntrinsicWidth(), pointer.getIntrinsicHeight());
Drawable statisticsMarker = getResources().getDrawable(R.drawable.ic_marker_yellow_pushpin); 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 * Sets chart by distance.
* will be reloaded. * It is expected that after changing this value, data will be reloaded.
* *
* @param value true for by distance, false for by time * @param value true for by distance, false for by time
*/ */
@@ -449,8 +449,7 @@ public class ChartView extends View {
synchronized (waypoints) { synchronized (waypoints) {
for (int i = 0; i < waypoints.size(); i++) { for (int i = 0; i < waypoints.size(); i++) {
Waypoint waypoint = waypoints.get(i); Waypoint waypoint = waypoints.get(i);
int distance = Math.abs( int distance = Math.abs(getX(getWaypointXValue(waypoint)) - (int) event.getX() - getScrollX());
getX(getWaypointXValue(waypoint)) - (int) event.getX() - getScrollX());
if (distance < minDistance) { if (distance < minDistance) {
minDistance = distance; minDistance = distance;
nearestWaypoint = waypoint; nearestWaypoint = waypoint;
@@ -567,7 +566,7 @@ public class ChartView extends View {
* @param canvas the canvas * @param canvas the canvas
*/ */
private void drawSeriesTitles(Canvas canvas) { private void drawSeriesTitles(Canvas canvas) {
int[] titleDimensions = getTitleDimenions(); int[] titleDimensions = getTitleDimensions();
int lines = titleDimensions[0]; int lines = titleDimensions[0];
int lineHeight = titleDimensions[1]; int lineHeight = titleDimensions[1];
int count = 0; 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 * Gets the title dimensions.
* the number of lines and the second element is the line height. * 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 lines = 0;
int lineHeight = 0; int lineHeight = 0;
for (int i = 0; i < series.length; i++) { 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 * Updates paths. The path needs to be updated any time after the data or the dimensions change.
* dimensions change.
*/ */
private void updatePaths() { private void updatePaths() {
synchronized (chartData) { synchronized (chartData) {
@@ -867,7 +865,7 @@ public class ChartView extends View {
} }
leftBorder = (int) (density * BORDER + markerLength); leftBorder = (int) (density * BORDER + markerLength);
int[] titleDimensions = getTitleDimenions(); int[] titleDimensions = getTitleDimensions();
topBorder = (int) (density * BORDER + titleDimensions[0] * (titleDimensions[1] + spacer)); topBorder = (int) (density * BORDER + titleDimensions[0] * (titleDimensions[1] + spacer));
Rect xAxisLabelRect = getRect(axisPaint, getXAxisLabel()); Rect xAxisLabelRect = getRect(axisPaint, getXAxisLabel());
// border + x axis marker + spacer + .5 x axis label // border + x axis marker + spacer + .5 x axis label
@@ -1,33 +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;
private Constants() {
}
}
@@ -20,30 +20,35 @@ import android.database.Cursor;
import android.os.Bundle; import android.os.Bundle;
import android.util.Log; import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentStatePagerAdapter; import androidx.fragment.app.FragmentStatePagerAdapter;
import androidx.viewpager.widget.ViewPager; import androidx.viewpager.widget.ViewPager;
import java.util.ArrayList;
import java.util.List;
import de.dennisguse.opentracks.content.ContentProviderUtils; import de.dennisguse.opentracks.content.ContentProviderUtils;
import de.dennisguse.opentracks.content.Waypoint; import de.dennisguse.opentracks.content.Waypoint;
import de.dennisguse.opentracks.fragments.DeleteMarkerDialogFragment.DeleteMarkerCaller; import de.dennisguse.opentracks.fragments.DeleteMarkerDialogFragment.DeleteMarkerCaller;
import de.dennisguse.opentracks.fragments.MarkerDetailFragment; import de.dennisguse.opentracks.fragments.MarkerDetailFragment;
import de.dennisguse.opentracks.R;
import java.util.ArrayList;
/** /**
* An activity to display marker detail info. * An activity to display marker detail info.
* <p>
* Allows to swipe to the next and previous marker.
* *
* @author Leif Hendrik Wilden * @author Leif Hendrik Wilden
*/ */
public class MarkerDetailActivity extends AbstractActivity implements DeleteMarkerCaller { public class MarkerDetailActivity extends AbstractActivity implements DeleteMarkerCaller {
public static final String EXTRA_MARKER_ID = "marker_id"; public static final String EXTRA_MARKER_ID = "marker_id";
private static final String TAG = MarkerDetailActivity.class.getSimpleName(); private static final String TAG = MarkerDetailActivity.class.getSimpleName();
private ArrayList<Long> markerIds; private List<Long> markerIds;
@Override @Override
protected void onCreate(Bundle bundle) { 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)) { try (Cursor cursor = contentProviderUtils.getWaypointCursor(waypoint.getTrackId(), -1L, -1)) {
if (cursor != null && cursor.moveToFirst()) { if (cursor != null && cursor.moveToFirst()) {
/* for (int i = 0; i < cursor.getCount(); i++) {
* Yes, this will skip the first waypoint and that is intentional as the Waypoint currentMarker = contentProviderUtils.createWaypoint(cursor);
* first waypoint holds the stats for the track.
*/
while (cursor.moveToNext()) {
Waypoint current = contentProviderUtils.createWaypoint(cursor);
markerIds.add(current.getId()); if (!currentMarker.isTripStatistics()) {
if (current.getId() == markerId) { markerIds.add(currentMarker.getId());
if (currentMarker.getId() == markerId) {
markerIndex = markerIds.size() - 1; markerIndex = markerIds.size() - 1;
} }
} }
cursor.moveToNext();
}
} }
} }
ViewPager viewPager = findViewById(R.id.maker_detail_activity_view_pager); final ViewPager viewPager = findViewById(R.id.maker_detail_activity_view_pager);
viewPager.setAdapter(new MarkerDetailPagerAdapter(getSupportFragmentManager())); final MarkerDetailPagerAdapter markerAdapter = new MarkerDetailPagerAdapter(getSupportFragmentManager());
if (markerIndex != -1) { viewPager.setAdapter(markerAdapter);
viewPager.setCurrentItem(markerIndex); 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 @Override
@@ -108,14 +127,20 @@ public class MarkerDetailActivity extends AbstractActivity implements DeleteMark
*/ */
private class MarkerDetailPagerAdapter extends FragmentStatePagerAdapter { private class MarkerDetailPagerAdapter extends FragmentStatePagerAdapter {
public MarkerDetailPagerAdapter(FragmentManager fragmentManager) { MarkerDetailPagerAdapter(FragmentManager fragmentManager) {
super(fragmentManager); super(fragmentManager);
} }
@Override @Override
@NonNull
public Fragment getItem(int position) { public Fragment getItem(int position) {
String title = getString(R.string.marker_title, position + 1, getCount()); return MarkerDetailFragment.newInstance(markerIds.get(position));
return MarkerDetailFragment.newInstance(markerIds.get(position), title); }
@Nullable
@Override
public CharSequence getPageTitle(int position) {
return getString(R.string.marker_title, position + 1, getCount());
} }
@Override @Override
@@ -30,7 +30,6 @@ import de.dennisguse.opentracks.content.Waypoint;
import de.dennisguse.opentracks.content.Waypoint.WaypointType; import de.dennisguse.opentracks.content.Waypoint.WaypointType;
import de.dennisguse.opentracks.content.WaypointCreationRequest; import de.dennisguse.opentracks.content.WaypointCreationRequest;
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection; import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
import de.dennisguse.opentracks.util.TrackRecordingServiceConnectionUtils;
/** /**
* An activity to add/edit a marker. * An activity to add/edit a marker.
@@ -41,6 +40,7 @@ public class MarkerEditActivity extends AbstractActivity {
public static final String EXTRA_TRACK_ID = "track_id"; public static final String EXTRA_TRACK_ID = "track_id";
public static final String EXTRA_MARKER_ID = "marker_id"; public static final String EXTRA_MARKER_ID = "marker_id";
private static final String TAG = MarkerEditActivity.class.getSimpleName(); private static final String TAG = MarkerEditActivity.class.getSimpleName();
private long trackId; private long trackId;
private long markerId; private long markerId;
@@ -90,7 +90,7 @@ public class MarkerEditActivity extends AbstractActivity {
@Override @Override
protected void onStart() { protected void onStart() {
super.onStart(); super.onStart();
TrackRecordingServiceConnectionUtils.startConnection(this, trackRecordingServiceConnection); trackRecordingServiceConnection.startConnection(this);
} }
@Override @Override
@@ -127,8 +127,7 @@ public class MarkerEditActivity extends AbstractActivity {
if (newMarker) { if (newMarker) {
statisticsSection.setVisibility(View.GONE); statisticsSection.setVisibility(View.GONE);
waypointSection.setVisibility(View.VISIBLE); waypointSection.setVisibility(View.VISIBLE);
int nextWaypointNumber = trackId == -1L ? -1 int nextWaypointNumber = trackId == -1L ? -1 : ContentProviderUtils.Factory.get(this).getNextWaypointNumber(trackId, WaypointType.WAYPOINT);
: ContentProviderUtils.Factory.get(this).getNextWaypointNumber(trackId, WaypointType.WAYPOINT);
if (nextWaypointNumber == -1) { if (nextWaypointNumber == -1) {
nextWaypointNumber = 0; nextWaypointNumber = 0;
} }
@@ -168,7 +167,7 @@ public class MarkerEditActivity extends AbstractActivity {
waypointDescription.getText().toString(), waypointDescription.getText().toString(),
null, null,
null); null);
TrackRecordingServiceConnectionUtils.addMarker(this, trackRecordingServiceConnection, waypointCreationRequest); trackRecordingServiceConnection.addMarker(this, waypointCreationRequest);
} }
/** /**
@@ -22,14 +22,11 @@ import android.content.SharedPreferences;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener; import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
import android.database.Cursor; import android.database.Cursor;
import android.os.Bundle; import android.os.Bundle;
import android.view.ContextMenu;
import android.view.ContextMenu.ContextMenuInfo;
import android.view.KeyEvent; import android.view.KeyEvent;
import android.view.Menu; import android.view.Menu;
import android.view.MenuItem; import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.widget.AdapterView; import android.widget.AdapterView;
import android.widget.AdapterView.AdapterContextMenuInfo;
import android.widget.AdapterView.OnItemClickListener; import android.widget.AdapterView.OnItemClickListener;
import android.widget.ListView; import android.widget.ListView;
@@ -46,6 +43,7 @@ import de.dennisguse.opentracks.content.Waypoint.WaypointType;
import de.dennisguse.opentracks.content.WaypointsColumns; import de.dennisguse.opentracks.content.WaypointsColumns;
import de.dennisguse.opentracks.fragments.DeleteMarkerDialogFragment; import de.dennisguse.opentracks.fragments.DeleteMarkerDialogFragment;
import de.dennisguse.opentracks.fragments.DeleteMarkerDialogFragment.DeleteMarkerCaller; import de.dennisguse.opentracks.fragments.DeleteMarkerDialogFragment.DeleteMarkerCaller;
import de.dennisguse.opentracks.util.ActivityUtils;
import de.dennisguse.opentracks.util.IntentUtils; import de.dennisguse.opentracks.util.IntentUtils;
import de.dennisguse.opentracks.util.ListItemUtils; import de.dennisguse.opentracks.util.ListItemUtils;
import de.dennisguse.opentracks.util.PreferencesUtils; import de.dennisguse.opentracks.util.PreferencesUtils;
@@ -65,8 +63,11 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
WaypointsColumns.NAME, WaypointsColumns.DESCRIPTION, WaypointsColumns.CATEGORY, WaypointsColumns.NAME, WaypointsColumns.DESCRIPTION, WaypointsColumns.CATEGORY,
WaypointsColumns.TYPE, WaypointsColumns.TIME, WaypointsColumns.PHOTOURL, WaypointsColumns.TYPE, WaypointsColumns.TIME, WaypointsColumns.PHOTOURL,
WaypointsColumns.LATITUDE, WaypointsColumns.LONGITUDE}; WaypointsColumns.LATITUDE, WaypointsColumns.LONGITUDE};
private ContentProviderUtils contentProviderUtils; private ContentProviderUtils contentProviderUtils;
private SharedPreferences sharedPreferences; private SharedPreferences sharedPreferences;
private long recordingTrackId = PreferencesUtils.RECORDING_TRACK_ID_DEFAULT; private long recordingTrackId = PreferencesUtils.RECORDING_TRACK_ID_DEFAULT;
private boolean recordingTrackPaused = PreferencesUtils.RECORDING_TRACK_PAUSED_DEFAULT; private boolean recordingTrackPaused = PreferencesUtils.RECORDING_TRACK_PAUSED_DEFAULT;
/* /*
@@ -172,7 +173,7 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
} }
}; };
listView.setAdapter(resourceCursorAdapter); listView.setAdapter(resourceCursorAdapter);
AbstractListActivity.configureListViewContextualMenu(listView, contextualActionModeCallback); ActivityUtils.configureListViewContextualMenu(listView, contextualActionModeCallback);
LoaderManager.getInstance(this).initLoader(0, null, new LoaderCallbacks<Cursor>() { LoaderManager.getInstance(this).initLoader(0, null, new LoaderCallbacks<Cursor>() {
@NonNull @NonNull
@@ -231,7 +232,7 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
insertMarkerMenuItem = menu.findItem(R.id.marker_list_insert_marker); insertMarkerMenuItem = menu.findItem(R.id.marker_list_insert_marker);
searchMenuItem = menu.findItem(R.id.marker_list_search); searchMenuItem = menu.findItem(R.id.marker_list_search);
AbstractListActivity.configureSearchWidget(this, searchMenuItem, null); ActivityUtils.configureSearchWidget(this, searchMenuItem, null);
return super.onCreateOptionsMenu(menu); return super.onCreateOptionsMenu(menu);
} }
@@ -253,24 +254,6 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
return super.onOptionsItemSelected(item); return super.onOptionsItemSelected(item);
} }
@Override
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
super.onCreateContextMenu(menu, v, menuInfo);
getMenuInflater().inflate(R.menu.list_context_menu, menu);
AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo;
contextualActionModeCallback.onPrepare(menu, new int[]{info.position}, new long[]{info.id}, false);
}
@Override
public boolean onContextItemSelected(MenuItem item) {
AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo();
if (handleContextItem(item.getItemId(), new long[]{info.id})) {
return true;
}
return super.onContextItemSelected(item);
}
/** /**
* Handles a context item selection. * Handles a context item selection.
* *
@@ -297,8 +280,7 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
if (markerIds.length > 1 && markerIds.length == listView.getCount()) { if (markerIds.length > 1 && markerIds.length == listView.getCount()) {
markerIds = new long[]{-1L}; markerIds = new long[]{-1L};
} }
DeleteMarkerDialogFragment.newInstance(markerIds) DeleteMarkerDialogFragment.showDialog(getSupportFragmentManager(), markerIds);
.show(getSupportFragmentManager(), DeleteMarkerDialogFragment.DELETE_MARKER_DIALOG_TAG);
return true; return true;
case R.id.list_context_menu_select_all: case R.id.list_context_menu_select_all:
int size = listView.getCount(); int size = listView.getCount();
@@ -22,14 +22,11 @@ import android.content.SharedPreferences;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener; import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
import android.os.Bundle; import android.os.Bundle;
import android.util.Log; import android.util.Log;
import android.view.ContextMenu;
import android.view.ContextMenu.ContextMenuInfo;
import android.view.KeyEvent; import android.view.KeyEvent;
import android.view.Menu; import android.view.Menu;
import android.view.MenuItem; import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.widget.AdapterView; import android.widget.AdapterView;
import android.widget.AdapterView.AdapterContextMenuInfo;
import android.widget.AdapterView.OnItemClickListener; import android.widget.AdapterView.OnItemClickListener;
import android.widget.ArrayAdapter; import android.widget.ArrayAdapter;
import android.widget.ListView; import android.widget.ListView;
@@ -55,19 +52,19 @@ import de.dennisguse.opentracks.fragments.DeleteMarkerDialogFragment;
import de.dennisguse.opentracks.fragments.DeleteMarkerDialogFragment.DeleteMarkerCaller; import de.dennisguse.opentracks.fragments.DeleteMarkerDialogFragment.DeleteMarkerCaller;
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection; import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
import de.dennisguse.opentracks.stats.TripStatistics; import de.dennisguse.opentracks.stats.TripStatistics;
import de.dennisguse.opentracks.util.ActivityUtils;
import de.dennisguse.opentracks.util.IntentUtils; import de.dennisguse.opentracks.util.IntentUtils;
import de.dennisguse.opentracks.util.ListItemUtils; import de.dennisguse.opentracks.util.ListItemUtils;
import de.dennisguse.opentracks.util.PreferencesUtils; import de.dennisguse.opentracks.util.PreferencesUtils;
import de.dennisguse.opentracks.util.StringUtils; import de.dennisguse.opentracks.util.StringUtils;
import de.dennisguse.opentracks.util.TrackIconUtils; import de.dennisguse.opentracks.util.TrackIconUtils;
import de.dennisguse.opentracks.util.TrackRecordingServiceConnectionUtils;
/** /**
* An activity to display a list of search results. * An activity to display a list of searchable results.
* *
* @author Rodrigo Damazio * @author Rodrigo Damazio
* <p> * <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 AbstractListActivity implements DeleteMarkerCaller, ConfirmDeleteDialogFragment.ConfirmDeleteCaller { public class SearchListActivity extends AbstractListActivity implements DeleteMarkerCaller, ConfirmDeleteDialogFragment.ConfirmDeleteCaller {
@@ -87,42 +84,52 @@ public class SearchListActivity extends AbstractListActivity implements DeleteMa
private static final String PHOTO_URL_FIELD = "photoUrl"; private static final String PHOTO_URL_FIELD = "photoUrl";
private static final String MARKER_LATITUDE_FIELD = "latitude"; private static final String MARKER_LATITUDE_FIELD = "latitude";
private static final String MARKER_LONGITUDE_FIELD = "longitude"; private static final String MARKER_LONGITUDE_FIELD = "longitude";
private static final String TRACK_ID_FIELD = "trackId"; private static final String TRACK_ID_FIELD = "trackId";
private static final String MARKER_ID_FIELD = "markerId"; private static final String MARKER_ID_FIELD = "markerId";
private ContentProviderUtils contentProviderUtils; private ContentProviderUtils contentProviderUtils;
private SharedPreferences sharedPreferences; private SharedPreferences sharedPreferences;
private TrackRecordingServiceConnection trackRecordingServiceConnection; private TrackRecordingServiceConnection trackRecordingServiceConnection;
private SearchEngine searchEngine; private SearchEngine searchEngine;
private ArrayAdapter<Map<String, Object>> arrayAdapter; private ArrayAdapter<Map<String, Object>> arrayAdapter;
private boolean metricUnits = true; private boolean metricUnits = true;
private long recordingTrackId = PreferencesUtils.RECORDING_TRACK_ID_DEFAULT; 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 // Callback when an item is selected in the contextual action mode
private ContextualActionModeCallback contextualActionModeCallback = new ContextualActionModeCallback() { private ContextualActionModeCallback contextualActionModeCallback = new ContextualActionModeCallback() {
@Override @Override
public void onPrepare(Menu menu, int[] positions, long[] ids, boolean showSelectAll) { 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 isSingleSelection = positions.length == 1;
boolean isSingleSelectionTrack; boolean isSingleSelectionTrack;
if (isSingleSelection) { if (isSingleSelection) {
Map<String, Object> item = arrayAdapter.getItem(positions[0]); Map<String, Object> item = arrayAdapter.getItem(positions[0]);
Long trackId = (Long) item.get(TRACK_ID_FIELD);
Track track = contentProviderUtils.getTrack(trackId);
isSingleSelectionTrack = item.get(MARKER_ID_FIELD) == null; isSingleSelectionTrack = item.get(MARKER_ID_FIELD) == null;
} else { } else {
isSingleSelectionTrack = false; isSingleSelectionTrack = false;
} }
// Not recording, one item, item is a track // Not recording, one item, item is a track
menu.findItem(R.id.list_context_menu_share).setVisible(!isRecording && isSingleSelection && isSingleSelectionTrack); MenuItem shareMenuItem = menu.findItem(R.id.list_context_menu_share);
//TODO Setup shareIntent. if (isSingleSelectionTrack) {
shareMenuItem.setVisible(!isRecording);
}
// One item, item is a marker // One item, item is a marker
menu.findItem(R.id.list_context_menu_show_on_map).setVisible(isSingleSelection && !isSingleSelectionTrack); menu.findItem(R.id.list_context_menu_show_on_map).setVisible(isSingleSelection && !isSingleSelectionTrack);
// One item, can be a track or a marker // One item, can be a track or a marker
menu.findItem(R.id.list_context_menu_edit).setVisible(isSingleSelection); menu.findItem(R.id.list_context_menu_edit).setVisible(isSingleSelection);
// One item. If track, no restriction. // One item. If track, no restriction.
menu.findItem(R.id.list_context_menu_delete).setVisible(isSingleSelection && (isSingleSelectionTrack)); menu.findItem(R.id.list_context_menu_delete).setVisible(isSingleSelection && isSingleSelectionTrack);
// Disable select all, no action is available for multiple selection // Disable select all, no action is available for multiple selection
menu.findItem(R.id.list_context_menu_select_all).setVisible(false); menu.findItem(R.id.list_context_menu_select_all).setVisible(false);
} }
@@ -132,9 +139,8 @@ public class SearchListActivity extends AbstractListActivity implements DeleteMa
return handleContextItem(itemId, positions); return handleContextItem(itemId, positions);
} }
}; };
private boolean recordingTrackPaused = PreferencesUtils.RECORDING_TRACK_PAUSED_DEFAULT;
private final OnSharedPreferenceChangeListener private final OnSharedPreferenceChangeListener sharedPreferenceChangeListener = new OnSharedPreferenceChangeListener() {
sharedPreferenceChangeListener = new OnSharedPreferenceChangeListener() {
@Override @Override
public void onSharedPreferenceChanged(SharedPreferences preferences, String key) { public void onSharedPreferenceChanged(SharedPreferences preferences, String key) {
if (key == null || key.equals(PreferencesUtils.getKey(SearchListActivity.this, R.string.stats_units_key))) { if (key == null || key.equals(PreferencesUtils.getKey(SearchListActivity.this, R.string.stats_units_key))) {
@@ -171,12 +177,11 @@ public class SearchListActivity extends AbstractListActivity implements DeleteMa
@NonNull @NonNull
@Override @Override
public View getView(int position, View convertView, @NonNull android.view.ViewGroup parent) { public View getView(int position, View convertView, @NonNull android.view.ViewGroup parent) {
View view; View view = convertView;
if (convertView == null) { if (convertView == null) {
view = getLayoutInflater().inflate(R.layout.list_item, parent, false); view = getLayoutInflater().inflate(R.layout.list_item, parent, false);
} else {
view = convertView;
} }
Map<String, Object> resultMap = getItem(position); Map<String, Object> resultMap = getItem(position);
boolean isRecording = (boolean) resultMap.get(IS_RECORDING_FIELD); boolean isRecording = (boolean) resultMap.get(IS_RECORDING_FIELD);
boolean isPaused = (boolean) resultMap.get(IS_PAUSED_FIELD); boolean isPaused = (boolean) resultMap.get(IS_PAUSED_FIELD);
@@ -207,18 +212,16 @@ public class SearchListActivity extends AbstractListActivity implements DeleteMa
Map<String, Object> item = arrayAdapter.getItem(position); Map<String, Object> item = arrayAdapter.getItem(position);
Long trackId = (Long) item.get(TRACK_ID_FIELD); Long trackId = (Long) item.get(TRACK_ID_FIELD);
Long markerId = (Long) item.get(MARKER_ID_FIELD); Long markerId = (Long) item.get(MARKER_ID_FIELD);
Intent intent; Intent intent = IntentUtils.newIntent(SearchListActivity.this, TrackDetailActivity.class);
if (markerId != null) { if (markerId != null) {
intent = IntentUtils.newIntent(SearchListActivity.this, TrackDetailActivity.class) intent = intent.putExtra(TrackDetailActivity.EXTRA_MARKER_ID, markerId);
.putExtra(TrackDetailActivity.EXTRA_MARKER_ID, markerId);
} else { } else {
intent = IntentUtils.newIntent(SearchListActivity.this, TrackDetailActivity.class) intent = intent.putExtra(TrackDetailActivity.EXTRA_TRACK_ID, trackId);
.putExtra(TrackDetailActivity.EXTRA_TRACK_ID, trackId);
} }
startActivity(intent); startActivity(intent);
} }
}); });
AbstractListActivity.configureListViewContextualMenu(listView, contextualActionModeCallback); ActivityUtils.configureListViewContextualMenu(listView, contextualActionModeCallback);
handleIntent(getIntent()); handleIntent(getIntent());
} }
@@ -227,7 +230,7 @@ public class SearchListActivity extends AbstractListActivity implements DeleteMa
super.onStart(); super.onStart();
sharedPreferences.registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener); sharedPreferences.registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
sharedPreferenceChangeListener.onSharedPreferenceChanged(null, null); sharedPreferenceChangeListener.onSharedPreferenceChanged(null, null);
TrackRecordingServiceConnectionUtils.startConnection(this, trackRecordingServiceConnection); trackRecordingServiceConnection.startConnection(this);
} }
@Override @Override
@@ -250,43 +253,11 @@ public class SearchListActivity extends AbstractListActivity implements DeleteMa
@Override @Override
public void onNewIntent(Intent intent) { public void onNewIntent(Intent intent) {
super.onNewIntent(intent);
setIntent(intent); setIntent(intent);
handleIntent(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);
getMenuInflater().inflate(R.menu.list_context_menu, menu);
AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo;
contextualActionModeCallback.onPrepare(menu, new int[]{info.position}, new long[]{info.id}, false);
}
@Override
public boolean onContextItemSelected(MenuItem item) {
AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo();
if (handleContextItem(item.getItemId(), new int[]{info.position})) {
return true;
}
return super.onContextItemSelected(item);
}
@Override @Override
public boolean onKeyUp(int keyCode, KeyEvent event) { public boolean onKeyUp(int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_SEARCH) { if (keyCode == KeyEvent.KEYCODE_SEARCH) {
@@ -311,13 +282,14 @@ public class SearchListActivity extends AbstractListActivity implements DeleteMa
Long markerId = (Long) item.get(MARKER_ID_FIELD); Long markerId = (Long) item.get(MARKER_ID_FIELD);
Intent intent; Intent intent;
switch (itemId) { switch (itemId) {
case R.id.list_context_menu_share:
//TODO
Log.e(TAG, "Not implemented");
return true;
case R.id.list_context_menu_show_on_map: case R.id.list_context_menu_show_on_map:
IntentUtils.showCoordinateOnMap(this, (double) item.get(MARKER_LATITUDE_FIELD), (double) item.get(MARKER_LONGITUDE_FIELD), item.get(NAME_FIELD) + ""); IntentUtils.showCoordinateOnMap(this, (double) item.get(MARKER_LATITUDE_FIELD), (double) item.get(MARKER_LONGITUDE_FIELD), item.get(NAME_FIELD) + "");
return true; return true;
case R.id.list_context_menu_share:
intent = IntentUtils.newShareFileIntent(this, new long[]{trackId});
intent = Intent.createChooser(intent, null);
startActivity(intent);
return true;
case R.id.list_context_menu_edit: case R.id.list_context_menu_edit:
if (markerId != null) { if (markerId != null) {
intent = IntentUtils.newIntent(this, MarkerEditActivity.class) intent = IntentUtils.newIntent(this, MarkerEditActivity.class)
@@ -328,12 +300,12 @@ public class SearchListActivity extends AbstractListActivity implements DeleteMa
} }
startActivity(intent); 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(); finish();
return true; return true;
case R.id.list_context_menu_delete: case R.id.list_context_menu_delete:
if (markerId != null) { if (markerId != null) {
DeleteMarkerDialogFragment.newInstance(new long[]{markerId}).show(getSupportFragmentManager(), DeleteMarkerDialogFragment.DELETE_MARKER_DIALOG_TAG); DeleteMarkerDialogFragment.showDialog(getSupportFragmentManager(), new long[]{markerId});
} else { } else {
deleteTracks(new long[]{trackId}); deleteTracks(new long[]{trackId});
} }
@@ -362,7 +334,7 @@ public class SearchListActivity extends AbstractListActivity implements DeleteMa
} }
/** /**
* Do the search. * Do the searchable.
* *
* @param textQuery the query * @param textQuery the query
*/ */
@@ -391,13 +363,11 @@ public class SearchListActivity extends AbstractListActivity implements DeleteMa
//TODO Replace use of map<string, object>, but rather provide Track or Waypoint directly. //TODO Replace use of map<string, object>, but rather provide Track or Waypoint directly.
ArrayList<Map<String, Object>> output = new ArrayList<>(scoredResults.size()); ArrayList<Map<String, Object>> output = new ArrayList<>(scoredResults.size());
for (ScoredResult result : scoredResults) { for (ScoredResult result : scoredResults) {
Map<String, Object> resultMap = new HashMap<>();
if (result.track != null) { if (result.track != null) {
prepareTrackForDisplay(result.track, resultMap); output.add(prepareTrackForDisplay(result.track));
} else { } else {
prepareMarkerForDisplay(result.waypoint, resultMap); output.add(prepareMarkerForDisplay(result.waypoint));
} }
output.add(resultMap);
} }
return output; return output;
} }
@@ -406,11 +376,12 @@ public class SearchListActivity extends AbstractListActivity implements DeleteMa
* Prepares a marker for display by filling in a result map. * Prepares a marker for display by filling in a result map.
* *
* @param waypoint the marker * @param waypoint the marker
* @param resultMap the result map * @return the result map
*/ */
@Deprecated private Map<String, Object> prepareMarkerForDisplay(Waypoint waypoint) {
private void prepareMarkerForDisplay(Waypoint waypoint, Map<String, Object> resultMap) { 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 search.
//TODO: It may be more appropriate to obtain the track name as a join in the retrieval phase of the searchable.
String trackName = null; String trackName = null;
long trackId = waypoint.getTrackId(); long trackId = waypoint.getTrackId();
if (trackId != -1L) { if (trackId != -1L) {
@@ -440,17 +411,20 @@ public class SearchListActivity extends AbstractListActivity implements DeleteMa
resultMap.put(MARKER_LATITUDE_FIELD, waypoint.getLocation().getLatitude()); resultMap.put(MARKER_LATITUDE_FIELD, waypoint.getLocation().getLatitude());
resultMap.put(MARKER_LONGITUDE_FIELD, waypoint.getLocation().getLongitude()); resultMap.put(MARKER_LONGITUDE_FIELD, waypoint.getLocation().getLongitude());
return resultMap;
} }
/** /**
* Prepares a track for display by filling in a result map. * Prepares a track for display by filling in a result map.
* *
* @param track the track * @param track the track
* @param resultMap the result map * @return the result map
*/ */
@Deprecated private Map<String, Object> prepareTrackForDisplay(Track track) {
private void prepareTrackForDisplay(Track track, Map<String, Object> resultMap) { Map<String, Object> resultMap = new HashMap<>();
TripStatistics tripStatitics = track.getTripStatistics();
TripStatistics tripStatistics = track.getTripStatistics();
String icon = track.getIcon(); String icon = track.getIcon();
String category = icon != null && !icon.equals("") ? null : track.getCategory(); String category = icon != null && !icon.equals("") ? null : track.getCategory();
@@ -459,15 +433,17 @@ public class SearchListActivity extends AbstractListActivity implements DeleteMa
resultMap.put(ICON_ID_FIELD, TrackIconUtils.getIconDrawable(icon)); resultMap.put(ICON_ID_FIELD, TrackIconUtils.getIconDrawable(icon));
resultMap.put(ICON_CONTENT_DESCRIPTION_ID_FIELD, R.string.image_track); resultMap.put(ICON_CONTENT_DESCRIPTION_ID_FIELD, R.string.image_track);
resultMap.put(NAME_FIELD, track.getName()); resultMap.put(NAME_FIELD, track.getName());
resultMap.put(TOTAL_TIME_FIELD, StringUtils.formatElapsedTime(tripStatitics.getTotalTime())); resultMap.put(TOTAL_TIME_FIELD, StringUtils.formatElapsedTime(tripStatistics.getTotalTime()));
resultMap.put(TOTAL_DISTANCE_FIELD, StringUtils.formatDistance(this, tripStatitics.getTotalDistance(), metricUnits)); resultMap.put(TOTAL_DISTANCE_FIELD, StringUtils.formatDistance(this, tripStatistics.getTotalDistance(), metricUnits));
resultMap.put(MARKER_COUNT_FIELD, contentProviderUtils.getWaypointCount(track.getId())); 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(CATEGORY_FIELD, category);
resultMap.put(DESCRIPTION_FIELD, track.getDescription()); resultMap.put(DESCRIPTION_FIELD, track.getDescription());
resultMap.put(PHOTO_URL_FIELD, null); resultMap.put(PHOTO_URL_FIELD, null);
resultMap.put(TRACK_ID_FIELD, track.getId()); resultMap.put(TRACK_ID_FIELD, track.getId());
resultMap.put(MARKER_ID_FIELD, null); resultMap.put(MARKER_ID_FIELD, null);
return resultMap;
} }
@Override @Override
@@ -6,6 +6,12 @@ import android.util.Log;
import de.dennisguse.opentracks.util.PreferencesUtils; 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 { public class Startup extends Application {
private final static String TAG = Startup.class.getSimpleName(); private final static String TAG = Startup.class.getSimpleName();
@@ -97,9 +97,8 @@ public class TabsAdapter extends FragmentPagerAdapter implements TabHost.OnTabCh
@Override @Override
public void onPageSelected(int position) { public void onPageSelected(int position) {
/* /*
* Unfortunately when TabHost changes the current tab, it kindly also takes * Unfortunately when TabHost changes the current tab, it kindly also takes care of putting focus on it when not in touch mode.
* care of putting focus on it when not in touch mode. The jerk. This hack * The jerk. This hack tries to prevent this from pulling focus out of our ViewPager.
* tries to prevent this from pulling focus out of our ViewPager.
*/ */
TabWidget tabWidget = tabHost.getTabWidget(); TabWidget tabWidget = tabHost.getTabWidget();
int oldFocusability = tabWidget.getDescendantFocusability(); int oldFocusability = tabWidget.getDescendantFocusability();
@@ -122,15 +121,14 @@ public class TabsAdapter extends FragmentPagerAdapter implements TabHost.OnTabCh
private final Class<?> clss; private final Class<?> clss;
private final Bundle bundle; private final Bundle bundle;
public TabInfo(Class<?> clss, Bundle bunlde) { TabInfo(Class<?> clss, Bundle bundle) {
this.clss = clss; this.clss = clss;
this.bundle = bunlde; this.bundle = bundle;
} }
} }
/** /**
* A dummy {@link TabContentFactory} that creates an empty view to satisfy the * A dummy {@link TabContentFactory} that creates an empty view to satisfy the {@link TabHost} API.
* {@link TabHost} API.
* *
* @author Jimmy Shih * @author Jimmy Shih
*/ */
@@ -138,7 +136,7 @@ public class TabsAdapter extends FragmentPagerAdapter implements TabHost.OnTabCh
private final Context context; private final Context context;
public DummyTabFactory(Context context) { DummyTabFactory(Context context) {
this.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.services.TrackRecordingServiceConnection;
import de.dennisguse.opentracks.util.StringUtils; import de.dennisguse.opentracks.util.StringUtils;
import de.dennisguse.opentracks.util.UnitConversions; import de.dennisguse.opentracks.util.UnitConversions;
import de.dennisguse.opentracks.R;
/** /**
* Track controller for record, pause, resume, and stop. * 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.activity = activity;
this.trackRecordingServiceConnection = trackRecordingServiceConnection; this.trackRecordingServiceConnection = trackRecordingServiceConnection;
this.alwaysShow = alwaysShow; this.alwaysShow = alwaysShow;
@@ -121,12 +120,12 @@ public class TrackController {
} }
} }
public void onResume(boolean recording, boolean paused) { void onResume(boolean recording, boolean paused) {
isResumed = true; isResumed = true;
update(recording, paused); update(recording, paused);
} }
public void onPause() { void onPause() {
isResumed = false; isResumed = false;
handlerUpdateTotalTime.removeCallbacks(updateTotalTimeRunnable); handlerUpdateTotalTime.removeCallbacks(updateTotalTimeRunnable);
} }
@@ -31,7 +31,6 @@ import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.view.View.OnClickListener; import android.view.View.OnClickListener;
import android.view.WindowManager; import android.view.WindowManager;
import android.widget.ShareActionProvider;
import android.widget.TabHost; import android.widget.TabHost;
import android.widget.TabHost.TabSpec; import android.widget.TabHost.TabSpec;
import android.widget.Toast; import android.widget.Toast;
@@ -48,9 +47,7 @@ import de.dennisguse.opentracks.content.Waypoint.WaypointType;
import de.dennisguse.opentracks.content.WaypointCreationRequest; import de.dennisguse.opentracks.content.WaypointCreationRequest;
import de.dennisguse.opentracks.fragments.ChartFragment; import de.dennisguse.opentracks.fragments.ChartFragment;
import de.dennisguse.opentracks.fragments.ChooseActivityTypeDialogFragment; import de.dennisguse.opentracks.fragments.ChooseActivityTypeDialogFragment;
import de.dennisguse.opentracks.fragments.ChooseActivityTypeDialogFragment.ChooseActivityTypeCaller;
import de.dennisguse.opentracks.fragments.ConfirmDeleteDialogFragment; import de.dennisguse.opentracks.fragments.ConfirmDeleteDialogFragment;
import de.dennisguse.opentracks.fragments.FrequencyDialogFragment;
import de.dennisguse.opentracks.fragments.StatsFragment; import de.dennisguse.opentracks.fragments.StatsFragment;
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection; import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
import de.dennisguse.opentracks.settings.SettingsActivity; import de.dennisguse.opentracks.settings.SettingsActivity;
@@ -58,7 +55,6 @@ import de.dennisguse.opentracks.util.FileUtils;
import de.dennisguse.opentracks.util.IntentUtils; import de.dennisguse.opentracks.util.IntentUtils;
import de.dennisguse.opentracks.util.PreferencesUtils; import de.dennisguse.opentracks.util.PreferencesUtils;
import de.dennisguse.opentracks.util.TrackIconUtils; import de.dennisguse.opentracks.util.TrackIconUtils;
import de.dennisguse.opentracks.util.TrackRecordingServiceConnectionUtils;
import de.dennisguse.opentracks.util.TrackUtils; import de.dennisguse.opentracks.util.TrackUtils;
/** /**
@@ -67,11 +63,13 @@ import de.dennisguse.opentracks.util.TrackUtils;
* @author Leif Hendrik Wilden * @author Leif Hendrik Wilden
* @author Rodrigo Damazio * @author Rodrigo Damazio
*/ */
public class TrackDetailActivity extends AbstractListActivity 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_TRACK_ID = "track_id";
public static final String EXTRA_MARKER_ID = "marker_id"; public static final String EXTRA_MARKER_ID = "marker_id";
private static final String TAG = TrackDetailActivity.class.getSimpleName(); private static final String TAG = TrackDetailActivity.class.getSimpleName();
private static final String CURRENT_TAB_TAG_KEY = "current_tab_tag_key"; 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 PHOTO_URI_KEY = "photo_uri_key";
private static final String HAS_PHOTO_KEY = "has_photo_key"; private static final String HAS_PHOTO_KEY = "has_photo_key";
@@ -101,8 +99,7 @@ public class TrackDetailActivity extends AbstractListActivity implements ChooseA
private final Runnable bindChangedCallback = new Runnable() { private final Runnable bindChangedCallback = new Runnable() {
@Override @Override
public void run() { public void run() {
// After binding changes (is available), update the total time in // After binding changes (is available), update the total time in trackController.
// trackController.
runOnUiThread(new Runnable() { runOnUiThread(new Runnable() {
@Override @Override
public void run() { public void run() {
@@ -110,7 +107,7 @@ public class TrackDetailActivity extends AbstractListActivity implements ChooseA
if (hasPhoto && photoUri != null) { if (hasPhoto && photoUri != null) {
hasPhoto = false; hasPhoto = false;
WaypointCreationRequest waypointCreationRequest = new WaypointCreationRequest(WaypointType.WAYPOINT, false, null, null, null, null, photoUri.toString()); 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) { if (markerId != -1L) {
//TODO: Make configurable. //TODO: Make configurable.
FileUtils.updateMediaScanner(TrackDetailActivity.this, photoUri); FileUtils.updateMediaScanner(TrackDetailActivity.this, photoUri);
@@ -153,24 +150,24 @@ public class TrackDetailActivity extends AbstractListActivity implements ChooseA
}); });
} }
}; };
private MenuItem insertMarkerMenuItem; private MenuItem insertMarkerMenuItem;
private MenuItem insertPhotoMenuItem; private MenuItem insertPhotoMenuItem;
private MenuItem markerListMenuItem; private MenuItem markerListMenuItem;
private MenuItem shareMenuItem; private MenuItem shareMenuItem;
private MenuItem voiceFrequencyMenuItem;
private MenuItem splitFrequencyMenuItem;
private final OnClickListener recordListener = new OnClickListener() { private final OnClickListener recordListener = new OnClickListener() {
@Override @Override
public void onClick(View v) { public void onClick(View v) {
if (recordingTrackPaused) { if (recordingTrackPaused) {
// Paused -> Resume // Paused -> Resume
updateMenuItems(false); updateMenuItems(false);
TrackRecordingServiceConnectionUtils.resumeTrack(trackRecordingServiceConnection); trackRecordingServiceConnection.resumeTrack();
trackController.update(true, false); trackController.update(true, false);
} else { } else {
// Recording -> Paused // Recording -> Paused
updateMenuItems(true); updateMenuItems(true);
TrackRecordingServiceConnectionUtils.pauseTrack(trackRecordingServiceConnection); trackRecordingServiceConnection.pauseTrack();
trackController.update(true, true); trackController.update(true, true);
} }
} }
@@ -179,7 +176,7 @@ public class TrackDetailActivity extends AbstractListActivity implements ChooseA
private final OnClickListener stopListener = new OnClickListener() { private final OnClickListener stopListener = new OnClickListener() {
@Override @Override
public void onClick(View v) { public void onClick(View v) {
TrackRecordingServiceConnectionUtils.stopRecording(TrackDetailActivity.this, trackRecordingServiceConnection, true); trackRecordingServiceConnection.stopRecording(TrackDetailActivity.this, true);
updateMenuItems(true); updateMenuItems(true);
} }
}; };
@@ -199,16 +196,16 @@ public class TrackDetailActivity extends AbstractListActivity implements ChooseA
trackRecordingServiceConnection = new TrackRecordingServiceConnection(this, bindChangedCallback); trackRecordingServiceConnection = new TrackRecordingServiceConnection(this, bindChangedCallback);
trackDataHub = TrackDataHub.newInstance(this); trackDataHub = TrackDataHub.newInstance(this);
tabHost = findViewById(android.R.id.tabhost); tabHost = findViewById(R.id.tackdetail_tabhost);
tabHost.setup(); tabHost.setup();
ViewPager viewPager = findViewById(R.id.pager); ViewPager viewPager = findViewById(R.id.pager);
TabsAdapter tabsAdapter = new TabsAdapter(this, tabHost, viewPager); 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); 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); tabsAdapter.addTab(chartTabSpec, ChartFragment.class, null);
if (savedInstanceState != null) { if (savedInstanceState != null) {
@@ -246,7 +243,7 @@ public class TrackDetailActivity extends AbstractListActivity implements ChooseA
sharedPreferences.registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener); sharedPreferences.registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
sharedPreferenceChangeListener.onSharedPreferenceChanged(null, null); sharedPreferenceChangeListener.onSharedPreferenceChanged(null, null);
TrackRecordingServiceConnectionUtils.startConnection(this, trackRecordingServiceConnection); trackRecordingServiceConnection.startConnection(this);
trackDataHub.start(); trackDataHub.start();
} }
@@ -303,6 +300,7 @@ public class TrackDetailActivity extends AbstractListActivity implements ChooseA
@Override @Override
public void onNewIntent(Intent intent) { public void onNewIntent(Intent intent) {
super.onNewIntent(intent);
setIntent(intent); setIntent(intent);
handleIntent(intent); handleIntent(intent);
} }
@@ -314,13 +312,7 @@ public class TrackDetailActivity extends AbstractListActivity implements ChooseA
insertMarkerMenuItem = menu.findItem(R.id.track_detail_insert_marker); insertMarkerMenuItem = menu.findItem(R.id.track_detail_insert_marker);
insertPhotoMenuItem = menu.findItem(R.id.track_detail_insert_photo); insertPhotoMenuItem = menu.findItem(R.id.track_detail_insert_photo);
insertPhotoMenuItem.setVisible(new Intent(MediaStore.ACTION_IMAGE_CAPTURE).resolveActivity(getPackageManager()) != null); insertPhotoMenuItem.setVisible(new Intent(MediaStore.ACTION_IMAGE_CAPTURE).resolveActivity(getPackageManager()) != null);
shareMenuItem = menu.findItem(R.id.track_detail_share); shareMenuItem = menu.findItem(R.id.track_detail_share);
ShareActionProvider shareActionProvider = (ShareActionProvider) shareMenuItem.getActionProvider();
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);
markerListMenuItem = menu.findItem(R.id.track_detail_markers); markerListMenuItem = menu.findItem(R.id.track_detail_markers);
return super.onCreateOptionsMenu(menu); return super.onCreateOptionsMenu(menu);
@@ -342,6 +334,11 @@ public class TrackDetailActivity extends AbstractListActivity implements ChooseA
.putExtra(MarkerEditActivity.EXTRA_TRACK_ID, trackId); .putExtra(MarkerEditActivity.EXTRA_TRACK_ID, trackId);
startActivity(intent); startActivity(intent);
return true; return true;
case R.id.track_detail_share:
intent = IntentUtils.newShareFileIntent(this, new long[]{trackId});
intent = Intent.createChooser(intent, null);
startActivity(intent);
return true;
case R.id.track_detail_insert_photo: case R.id.track_detail_insert_photo:
if (!FileUtils.isExternalStorageWriteable() || ActivityCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) { if (!FileUtils.isExternalStorageWriteable() || ActivityCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) {
ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, EXTERNAL_STORAGE_PERMISSION_REQUEST_CODE); ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, EXTERNAL_STORAGE_PERMISSION_REQUEST_CODE);
@@ -357,14 +354,6 @@ public class TrackDetailActivity extends AbstractListActivity implements ChooseA
.putExtra(MarkerListActivity.EXTRA_TRACK_ID, trackId); .putExtra(MarkerListActivity.EXTRA_TRACK_ID, trackId);
startActivity(intent); startActivity(intent);
return true; 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);
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);
return true;
case R.id.track_detail_edit: case R.id.track_detail_edit:
intent = IntentUtils.newIntent(this, TrackEditActivity.class) intent = IntentUtils.newIntent(this, TrackEditActivity.class)
.putExtra(TrackEditActivity.EXTRA_TRACK_ID, trackId); .putExtra(TrackEditActivity.EXTRA_TRACK_ID, trackId);
@@ -392,6 +381,7 @@ public class TrackDetailActivity extends AbstractListActivity implements ChooseA
} }
return; return;
} }
super.onRequestPermissionsResult(requestCode, permissions, grantResults); super.onRequestPermissionsResult(requestCode, permissions, grantResults);
} }
@@ -465,8 +455,6 @@ public class TrackDetailActivity extends AbstractListActivity implements ChooseA
insertPhotoMenuItem.setVisible(hasCamera && isRecording() && !isPaused); insertPhotoMenuItem.setVisible(hasCamera && isRecording() && !isPaused);
shareMenuItem.setVisible(!isRecording()); shareMenuItem.setVisible(!isRecording());
markerListMenuItem.setShowAsAction(isRecording() ? MenuItem.SHOW_AS_ACTION_NEVER : MenuItem.SHOW_AS_ACTION_IF_ROOM); markerListMenuItem.setShowAsAction(isRecording() ? MenuItem.SHOW_AS_ACTION_NEVER : MenuItem.SHOW_AS_ACTION_IF_ROOM);
voiceFrequencyMenuItem.setVisible(isRecording());
splitFrequencyMenuItem.setVisible(isRecording());
String title; String title;
if (isRecording()) { if (isRecording()) {
title = getString(isPaused ? R.string.generic_paused : R.string.generic_recording); title = getString(isPaused ? R.string.generic_paused : R.string.generic_recording);
@@ -484,7 +472,7 @@ public class TrackDetailActivity extends AbstractListActivity implements ChooseA
} }
public void chooseActivityType(String category) { public void chooseActivityType(String category) {
ChooseActivityTypeDialogFragment.newInstance(category).show(getSupportFragmentManager(), ChooseActivityTypeDialogFragment.CHOOSE_ACTIVITY_TYPE_DIALOG_TAG); ChooseActivityTypeDialogFragment.showDialog(getSupportFragmentManager(), category);
} }
@Override @Override
@@ -28,28 +28,27 @@ import android.widget.Button;
import android.widget.EditText; import android.widget.EditText;
import android.widget.Spinner; import android.widget.Spinner;
import androidx.annotation.NonNull;
import de.dennisguse.opentracks.content.ContentProviderUtils; import de.dennisguse.opentracks.content.ContentProviderUtils;
import de.dennisguse.opentracks.content.Track; import de.dennisguse.opentracks.content.Track;
import de.dennisguse.opentracks.fragments.ChooseActivityTypeDialogFragment; import de.dennisguse.opentracks.fragments.ChooseActivityTypeDialogFragment;
import de.dennisguse.opentracks.fragments.ChooseActivityTypeDialogFragment.ChooseActivityTypeCaller;
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection; import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
import de.dennisguse.opentracks.util.TrackIconUtils; import de.dennisguse.opentracks.util.TrackIconUtils;
import de.dennisguse.opentracks.util.TrackRecordingServiceConnectionUtils;
import de.dennisguse.opentracks.util.TrackUtils; 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 * 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.
* data such as track name, activity type, and track description.
* *
* @author Leif Hendrik Wilden * @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_TRACK_ID = "track_id";
public static final String EXTRA_NEW_TRACK = "new_track"; public static final String EXTRA_NEW_TRACK = "new_track";
private static final String TAG = TrackEditActivity.class.getSimpleName(); private static final String TAG = TrackEditActivity.class.getSimpleName();
private static final String ICON_VALUE_KEY = "icon_value_key"; private static final String ICON_VALUE_KEY = "icon_value_key";
private TrackRecordingServiceConnection trackRecordingServiceConnection; private TrackRecordingServiceConnection trackRecordingServiceConnection;
@@ -57,7 +56,7 @@ public class TrackEditActivity extends AbstractActivity implements ChooseActivit
private Track track; private Track track;
private String iconValue; private String iconValue;
private EditText name; private EditText nameEditText;
private AutoCompleteTextView activityType; private AutoCompleteTextView activityType;
private Spinner activityTypeIcon; private Spinner activityTypeIcon;
private EditText description; private EditText description;
@@ -67,7 +66,7 @@ public class TrackEditActivity extends AbstractActivity implements ChooseActivit
super.onCreate(bundle); super.onCreate(bundle);
trackRecordingServiceConnection = new TrackRecordingServiceConnection(this, null); trackRecordingServiceConnection = new TrackRecordingServiceConnection(this, null);
Long trackId = getIntent().getLongExtra(EXTRA_TRACK_ID, -1L); long trackId = getIntent().getLongExtra(EXTRA_TRACK_ID, -1L);
if (trackId == -1L) { if (trackId == -1L) {
Log.e(TAG, "invalid trackId"); Log.e(TAG, "invalid trackId");
finish(); finish();
@@ -82,14 +81,13 @@ public class TrackEditActivity extends AbstractActivity implements ChooseActivit
return; return;
} }
name = findViewById(R.id.track_edit_name); nameEditText = findViewById(R.id.track_edit_name);
name.setText(track.getName()); nameEditText.setText(track.getName());
activityType = findViewById(R.id.track_edit_activity_type); activityType = findViewById(R.id.track_edit_activity_type);
activityType.setText(track.getCategory()); activityType.setText(track.getCategory());
ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource( ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(this, R.array.activity_types, android.R.layout.simple_dropdown_item_1line);
this, R.array.activity_types, android.R.layout.simple_dropdown_item_1line);
activityType.setAdapter(adapter); activityType.setAdapter(adapter);
activityType.setOnItemClickListener(new AdapterView.OnItemClickListener() { activityType.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override @Override
@@ -122,9 +120,7 @@ public class TrackEditActivity extends AbstractActivity implements ChooseActivit
@Override @Override
public boolean onTouch(View v, MotionEvent event) { public boolean onTouch(View v, MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_UP) { if (event.getAction() == MotionEvent.ACTION_UP) {
ChooseActivityTypeDialogFragment.newInstance(activityType.getText().toString()).show( ChooseActivityTypeDialogFragment.showDialog(getSupportFragmentManager(), activityType.getText().toString());
getSupportFragmentManager(),
ChooseActivityTypeDialogFragment.CHOOSE_ACTIVITY_TYPE_DIALOG_TAG);
} }
return true; return true;
} }
@@ -133,9 +129,7 @@ public class TrackEditActivity extends AbstractActivity implements ChooseActivit
@Override @Override
public boolean onKey(View v, int keyCode, KeyEvent event) { public boolean onKey(View v, int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) { if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) {
ChooseActivityTypeDialogFragment.newInstance(activityType.getText().toString()).show( ChooseActivityTypeDialogFragment.showDialog(getSupportFragmentManager(), activityType.getText().toString());
getSupportFragmentManager(),
ChooseActivityTypeDialogFragment.CHOOSE_ACTIVITY_TYPE_DIALOG_TAG);
} }
return true; return true;
} }
@@ -144,11 +138,11 @@ public class TrackEditActivity extends AbstractActivity implements ChooseActivit
description = findViewById(R.id.track_edit_description); description = findViewById(R.id.track_edit_description);
description.setText(track.getDescription()); description.setText(track.getDescription());
Button save = findViewById(R.id.track_edit_save); Button saveButton = findViewById(R.id.track_edit_save);
save.setOnClickListener(new View.OnClickListener() { saveButton.setOnClickListener(new View.OnClickListener() {
@Override @Override
public void onClick(View v) { 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(), activityType.getText().toString(), description.getText().toString(),
contentProviderUtils); contentProviderUtils);
finish(); finish();
@@ -175,7 +169,7 @@ public class TrackEditActivity extends AbstractActivity implements ChooseActivit
@Override @Override
protected void onStart() { protected void onStart() {
super.onStart(); super.onStart();
TrackRecordingServiceConnectionUtils.startConnection(this, trackRecordingServiceConnection); trackRecordingServiceConnection.startConnection(this);
} }
@Override @Override
@@ -185,7 +179,7 @@ public class TrackEditActivity extends AbstractActivity implements ChooseActivit
} }
@Override @Override
public void onSaveInstanceState(Bundle outState) { public void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState); super.onSaveInstanceState(outState);
outState.putString(ICON_VALUE_KEY, iconValue); outState.putString(ICON_VALUE_KEY, iconValue);
} }
@@ -28,8 +28,6 @@ import android.location.LocationManager;
import android.os.Bundle; import android.os.Bundle;
import android.provider.Settings; import android.provider.Settings;
import android.util.Log; import android.util.Log;
import android.view.ContextMenu;
import android.view.ContextMenu.ContextMenuInfo;
import android.view.Gravity; import android.view.Gravity;
import android.view.KeyEvent; import android.view.KeyEvent;
import android.view.Menu; import android.view.Menu;
@@ -37,14 +35,12 @@ import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.view.View.OnClickListener; import android.view.View.OnClickListener;
import android.widget.AdapterView; import android.widget.AdapterView;
import android.widget.AdapterView.AdapterContextMenuInfo;
import android.widget.AdapterView.OnItemClickListener; import android.widget.AdapterView.OnItemClickListener;
import android.widget.ListView; import android.widget.ListView;
import android.widget.SearchView;
import android.widget.ShareActionProvider;
import android.widget.Toast; import android.widget.Toast;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.appcompat.widget.SearchView;
import androidx.core.app.ActivityCompat; import androidx.core.app.ActivityCompat;
import androidx.cursoradapter.widget.ResourceCursorAdapter; import androidx.cursoradapter.widget.ResourceCursorAdapter;
import androidx.loader.app.LoaderManager; import androidx.loader.app.LoaderManager;
@@ -60,12 +56,13 @@ import de.dennisguse.opentracks.fragments.ConfirmDeleteDialogFragment;
import de.dennisguse.opentracks.services.ITrackRecordingService; import de.dennisguse.opentracks.services.ITrackRecordingService;
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection; import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
import de.dennisguse.opentracks.settings.SettingsActivity; import de.dennisguse.opentracks.settings.SettingsActivity;
import de.dennisguse.opentracks.util.ActivityUtils;
import de.dennisguse.opentracks.util.IntentUtils; import de.dennisguse.opentracks.util.IntentUtils;
import de.dennisguse.opentracks.util.ListItemUtils; import de.dennisguse.opentracks.util.ListItemUtils;
import de.dennisguse.opentracks.util.PreferencesUtils; import de.dennisguse.opentracks.util.PreferencesUtils;
import de.dennisguse.opentracks.util.ServiceUtils;
import de.dennisguse.opentracks.util.StringUtils; import de.dennisguse.opentracks.util.StringUtils;
import de.dennisguse.opentracks.util.TrackIconUtils; import de.dennisguse.opentracks.util.TrackIconUtils;
import de.dennisguse.opentracks.util.TrackRecordingServiceConnectionUtils;
import de.dennisguse.opentracks.util.TrackUtils; import de.dennisguse.opentracks.util.TrackUtils;
/** /**
@@ -76,9 +73,11 @@ import de.dennisguse.opentracks.util.TrackUtils;
public class TrackListActivity extends AbstractListActivity implements ConfirmDeleteDialogFragment.ConfirmDeleteCaller { public class TrackListActivity extends AbstractListActivity implements ConfirmDeleteDialogFragment.ConfirmDeleteCaller {
private static final String TAG = TrackListActivity.class.getSimpleName(); private static final String TAG = TrackListActivity.class.getSimpleName();
private static final String[] PROJECTION = new String[]{TracksColumns._ID, TracksColumns.NAME, private static final String[] PROJECTION = new String[]{TracksColumns._ID, TracksColumns.NAME,
TracksColumns.DESCRIPTION, TracksColumns.CATEGORY, TracksColumns.STARTTIME, TracksColumns.DESCRIPTION, TracksColumns.CATEGORY, TracksColumns.STARTTIME,
TracksColumns.TOTALDISTANCE, TracksColumns.TOTALTIME, TracksColumns.ICON}; TracksColumns.TOTALDISTANCE, TracksColumns.TOTALTIME, TracksColumns.ICON};
// The following are set in onCreate // The following are set in onCreate
private ContentProviderUtils contentProviderUtils; private ContentProviderUtils contentProviderUtils;
private SharedPreferences sharedPreferences; private SharedPreferences sharedPreferences;
@@ -86,6 +85,7 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
private TrackController trackController; private TrackController trackController;
private ListView listView; private ListView listView;
private ResourceCursorAdapter resourceCursorAdapter; private ResourceCursorAdapter resourceCursorAdapter;
private final LoaderCallbacks<Cursor> loaderCallbacks = new LoaderCallbacks<Cursor>() { private final LoaderCallbacks<Cursor> loaderCallbacks = new LoaderCallbacks<Cursor>() {
@Override @Override
public Loader<Cursor> onCreateLoader(int arg0, Bundle arg1) { public Loader<Cursor> onCreateLoader(int arg0, Bundle arg1) {
@@ -103,9 +103,11 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
resourceCursorAdapter.swapCursor(null); resourceCursorAdapter.swapCursor(null);
} }
}; };
// Preferences // Preferences
private boolean metricUnits = true; private boolean metricUnits = true;
private long recordingTrackId = PreferencesUtils.RECORDING_TRACK_ID_DEFAULT; private long recordingTrackId = PreferencesUtils.RECORDING_TRACK_ID_DEFAULT;
// Callback when an item is selected in the contextual action mode // Callback when an item is selected in the contextual action mode
private final ContextualActionModeCallback contextualActionModeCallback = new ContextualActionModeCallback() { private final ContextualActionModeCallback contextualActionModeCallback = new ContextualActionModeCallback() {
@@ -113,10 +115,6 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
public void onPrepare(Menu menu, int[] positions, long[] trackIds, boolean showSelectAll) { public void onPrepare(Menu menu, int[] positions, long[] trackIds, boolean showSelectAll) {
boolean isSingleSelection = trackIds.length == 1; 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_edit).setVisible(isSingleSelection); menu.findItem(R.id.list_context_menu_edit).setVisible(isSingleSelection);
menu.findItem(R.id.list_context_menu_select_all).setVisible(showSelectAll); menu.findItem(R.id.list_context_menu_select_all).setVisible(showSelectAll);
} }
@@ -126,7 +124,9 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
return handleContextItem(itemId, ids); return handleContextItem(itemId, ids);
} }
}; };
private boolean recordingTrackPaused = PreferencesUtils.RECORDING_TRACK_PAUSED_DEFAULT; private boolean recordingTrackPaused = PreferencesUtils.RECORDING_TRACK_PAUSED_DEFAULT;
/* /*
* Note that sharedPreferenceChangeListener cannot be an anonymous inner class. * Note that sharedPreferenceChangeListener cannot be an anonymous inner class.
* Anonymous inner class will get garbage collected. * Anonymous inner class will get garbage collected.
@@ -142,7 +142,7 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
PreferencesUtils.getKey(TrackListActivity.this, R.string.recording_track_id_key))) { PreferencesUtils.getKey(TrackListActivity.this, R.string.recording_track_id_key))) {
recordingTrackId = PreferencesUtils.getLong( recordingTrackId = PreferencesUtils.getLong(
TrackListActivity.this, R.string.recording_track_id_key); 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(); trackRecordingServiceConnection.startAndBind();
} }
} }
@@ -158,40 +158,42 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
public void run() { public void run() {
TrackListActivity.this.invalidateOptionsMenu(); TrackListActivity.this.invalidateOptionsMenu();
LoaderManager.getInstance(TrackListActivity.this).restartLoader(0, null, loaderCallbacks); 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); trackController.update(isRecording, recordingTrackPaused);
} }
}); });
} }
} }
}; };
// Menu items // Menu items
private MenuItem searchMenuItem; private MenuItem searchMenuItem;
private MenuItem startGpsMenuItem; private MenuItem startGpsMenuItem;
private MenuItem aggregatedStatisticsMenuItem;
private MenuItem deleteAllMenuItem; private MenuItem deleteAllMenuItem;
private final OnClickListener stopListener = new OnClickListener() { private final OnClickListener stopListener = new OnClickListener() {
@Override @Override
public void onClick(View v) { public void onClick(View v) {
updateMenuItems(false, false); updateMenuItems(false, false);
TrackRecordingServiceConnectionUtils.stopRecording( trackRecordingServiceConnection.stopRecording(TrackListActivity.this, true);
TrackListActivity.this, trackRecordingServiceConnection, true);
} }
}; };
private boolean startGps = false; // true to start gps private boolean startGps = false; // true to start gps
private boolean startNewRecording = false; // true to start a new recording private boolean startNewRecording = false; // true to start a new recording
// Callback when the trackRecordingServiceConnection binding changes. // Callback when the trackRecordingServiceConnection binding changes.
private final Runnable bindChangedCallback = new Runnable() { private final Runnable bindChangedCallback = new Runnable() {
@Override @Override
public void run() { public void run() {
/* /*
* After binding changes (e.g., becomes available), update the total time * After binding changes (e.g., becomes available), update the total time in trackController.
* in trackController.
*/ */
runOnUiThread(new Runnable() { runOnUiThread(new Runnable() {
@Override @Override
public void run() { public void run() {
trackController.update(recordingTrackId != PreferencesUtils.RECORDING_TRACK_ID_DEFAULT, recordingTrackPaused); trackController.update(PreferencesUtils.isRecording(recordingTrackId), recordingTrackPaused);
} }
}); });
@@ -212,8 +214,7 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
Intent intent = IntentUtils.newIntent(TrackListActivity.this, TrackDetailActivity.class) Intent intent = IntentUtils.newIntent(TrackListActivity.this, TrackDetailActivity.class)
.putExtra(TrackDetailActivity.EXTRA_TRACK_ID, trackId); .putExtra(TrackDetailActivity.EXTRA_TRACK_ID, trackId);
startActivity(intent); startActivity(intent);
Toast.makeText( Toast.makeText(TrackListActivity.this, R.string.track_list_record_success, Toast.LENGTH_SHORT)
TrackListActivity.this, R.string.track_list_record_success, Toast.LENGTH_SHORT)
.show(); .show();
} }
if (startGps) { if (startGps) {
@@ -222,6 +223,7 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
} }
} }
}; };
private final OnClickListener recordListener = new OnClickListener() { private final OnClickListener recordListener = new OnClickListener() {
public void onClick(View v) { public void onClick(View v) {
if (recordingTrackId == PreferencesUtils.RECORDING_TRACK_ID_DEFAULT) { if (recordingTrackId == PreferencesUtils.RECORDING_TRACK_ID_DEFAULT) {
@@ -232,12 +234,12 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
if (recordingTrackPaused) { if (recordingTrackPaused) {
// Paused -> Resume // Paused -> Resume
updateMenuItems(false, true); updateMenuItems(false, true);
TrackRecordingServiceConnectionUtils.resumeTrack(trackRecordingServiceConnection); trackRecordingServiceConnection.resumeTrack();
trackController.update(true, false); trackController.update(true, false);
} else { } else {
// Recording -> Paused // Recording -> Paused
updateMenuItems(false, true); updateMenuItems(false, true);
TrackRecordingServiceConnectionUtils.pauseTrack(trackRecordingServiceConnection); trackRecordingServiceConnection.pauseTrack();
trackController.update(true, true); trackController.update(true, true);
} }
} }
@@ -255,14 +257,17 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
trackController = new TrackController(this, trackRecordingServiceConnection, true, recordListener, stopListener); trackController = new TrackController(this, trackRecordingServiceConnection, true, recordListener, stopListener);
setDefaultKeyMode(DEFAULT_KEYS_SEARCH_LOCAL); setDefaultKeyMode(DEFAULT_KEYS_SEARCH_LOCAL);
// Show trackController when search dialog is dismissed // Show trackController when search dialog is dismissed
SearchManager searchManager = (SearchManager) getSystemService(SEARCH_SERVICE); SearchManager searchManager = (SearchManager) getSystemService(SEARCH_SERVICE);
if (searchManager != null) {
searchManager.setOnDismissListener(new SearchManager.OnDismissListener() { searchManager.setOnDismissListener(new SearchManager.OnDismissListener() {
@Override @Override
public void onDismiss() { public void onDismiss() {
trackController.show(); trackController.show();
} }
}); });
}
listView = findViewById(R.id.track_list); listView = findViewById(R.id.track_list);
listView.setEmptyView(findViewById(R.id.track_list_empty_view)); listView.setEmptyView(findViewById(R.id.track_list_empty_view));
@@ -274,6 +279,7 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
startActivity(newIntent); startActivity(newIntent);
} }
}); });
resourceCursorAdapter = new ResourceCursorAdapter(this, R.layout.list_item, null, 0) { resourceCursorAdapter = new ResourceCursorAdapter(this, R.layout.list_item, null, 0) {
@Override @Override
public void bindView(View view, Context context, Cursor cursor) { public void bindView(View view, Context context, Cursor cursor) {
@@ -304,7 +310,8 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
} }
}; };
listView.setAdapter(resourceCursorAdapter); listView.setAdapter(resourceCursorAdapter);
AbstractListActivity.configureListViewContextualMenu(listView, contextualActionModeCallback);
ActivityUtils.configureListViewContextualMenu(listView, contextualActionModeCallback);
LoaderManager.getInstance(this).initLoader(0, null, loaderCallbacks); LoaderManager.getInstance(this).initLoader(0, null, loaderCallbacks);
showStartupDialogs(); showStartupDialogs();
@@ -316,7 +323,7 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
sharedPreferences.registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener); sharedPreferences.registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
sharedPreferenceChangeListener.onSharedPreferenceChanged(null, null); sharedPreferenceChangeListener.onSharedPreferenceChanged(null, null);
TrackRecordingServiceConnectionUtils.startConnection(this, trackRecordingServiceConnection); trackRecordingServiceConnection.startConnection(this);
} }
@Override @Override
@@ -326,8 +333,7 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
// Update UI // Update UI
this.invalidateOptionsMenu(); this.invalidateOptionsMenu();
LoaderManager.getInstance(this).restartLoader(0, null, loaderCallbacks); LoaderManager.getInstance(this).restartLoader(0, null, loaderCallbacks);
boolean isRecording = recordingTrackId != PreferencesUtils.RECORDING_TRACK_ID_DEFAULT; trackController.onResume(PreferencesUtils.isRecording(recordingTrackId), recordingTrackPaused);
trackController.onResume(isRecording, recordingTrackPaused);
} }
@Override @Override
@@ -367,10 +373,9 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
getMenuInflater().inflate(R.menu.track_list, menu); getMenuInflater().inflate(R.menu.track_list, menu);
searchMenuItem = menu.findItem(R.id.track_list_search); searchMenuItem = menu.findItem(R.id.track_list_search);
AbstractListActivity.configureSearchWidget(this, searchMenuItem, trackController); ActivityUtils.configureSearchWidget(this, searchMenuItem, trackController);
startGpsMenuItem = menu.findItem(R.id.track_list_start_gps); startGpsMenuItem = menu.findItem(R.id.track_list_start_gps);
aggregatedStatisticsMenuItem = menu.findItem(R.id.track_list_aggregated_statistics);
deleteAllMenuItem = menu.findItem(R.id.track_list_delete_all); deleteAllMenuItem = menu.findItem(R.id.track_list_delete_all);
return super.onCreateOptionsMenu(menu); return super.onCreateOptionsMenu(menu);
@@ -378,14 +383,13 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
@Override @Override
public boolean onPrepareOptionsMenu(Menu menu) { public boolean onPrepareOptionsMenu(Menu menu) {
boolean isGpsStarted = TrackRecordingServiceConnectionUtils.isRecordingServiceRunning(this); boolean isGpsStarted = ServiceUtils.isTrackRecordingServiceRunning(this);
boolean isRecording = recordingTrackId != PreferencesUtils.RECORDING_TRACK_ID_DEFAULT; boolean isRecording = PreferencesUtils.isRecording(recordingTrackId);
updateMenuItems(isGpsStarted, isRecording); updateMenuItems(isGpsStarted, isRecording);
View searchView = searchMenuItem.getActionView(); SearchView searchView = (SearchView) searchMenuItem.getActionView();
if (searchView instanceof SearchView) { searchView.setQuery("", false);
((SearchView) searchView).setQuery("", false);
}
return super.onPrepareOptionsMenu(menu); return super.onPrepareOptionsMenu(menu);
} }
@@ -395,11 +399,11 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
switch (item.getItemId()) { switch (item.getItemId()) {
case R.id.track_list_start_gps: case R.id.track_list_start_gps:
LocationManager locationManager = (LocationManager) this.getSystemService(Context.LOCATION_SERVICE); 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); intent = new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS);
startActivity(intent); startActivity(intent);
} else { } else {
startGps = !TrackRecordingServiceConnectionUtils.isRecordingServiceRunning(this); startGps = !ServiceUtils.isTrackRecordingServiceRunning(this);
// Show toast // Show toast
Toast toast = Toast.makeText(this, startGps ? R.string.gps_starting : R.string.gps_stopping, Toast.LENGTH_SHORT); Toast toast = Toast.makeText(this, startGps ? R.string.gps_starting : R.string.gps_stopping, Toast.LENGTH_SHORT);
@@ -426,10 +430,6 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
intent = IntentUtils.newIntent(this, MarkerListActivity.class); intent = IntentUtils.newIntent(this, MarkerListActivity.class);
startActivity(intent); startActivity(intent);
return true; return true;
case R.id.track_list_aggregated_statistics:
intent = IntentUtils.newIntent(this, AggregatedStatsActivity.class);
startActivity(intent);
return true;
case R.id.track_list_delete_all: case R.id.track_list_delete_all:
deleteTracks(new long[]{-1L}); deleteTracks(new long[]{-1L});
return true; return true;
@@ -441,24 +441,6 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
return super.onOptionsItemSelected(item); return super.onOptionsItemSelected(item);
} }
@Override
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
super.onCreateContextMenu(menu, v, menuInfo);
getMenuInflater().inflate(R.menu.list_context_menu, menu);
AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo;
contextualActionModeCallback.onPrepare(menu, new int[]{info.position}, new long[]{info.id}, false);
}
@Override
public boolean onContextItemSelected(MenuItem item) {
AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo();
if (handleContextItem(item.getItemId(), new long[]{info.id})) {
return true;
}
return super.onContextItemSelected(item);
}
@Override @Override
public boolean onKeyUp(int keyCode, KeyEvent event) { public boolean onKeyUp(int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_SEARCH && searchMenuItem != null) { if (keyCode == KeyEvent.KEYCODE_SEARCH && searchMenuItem != null) {
@@ -515,13 +497,9 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
startGpsMenuItem.setVisible(!isRecording); startGpsMenuItem.setVisible(!isRecording);
if (!isRecording) { if (!isRecording) {
startGpsMenuItem.setTitle(isGpsStarted ? R.string.menu_stop_gps : R.string.menu_start_gps); 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); startGpsMenuItem.setIcon(isGpsStarted ? R.drawable.ic_gps_fixed_24dp : R.drawable.ic_gps_off_24dp);
TrackIconUtils.setMenuIconColor(startGpsMenuItem);
} }
} }
if (aggregatedStatisticsMenuItem != null) {
aggregatedStatisticsMenuItem.setVisible(hasTrack);
}
if (deleteAllMenuItem != null) { if (deleteAllMenuItem != null) {
deleteAllMenuItem.setVisible(hasTrack && !isRecording); deleteAllMenuItem.setVisible(hasTrack && !isRecording);
} }
@@ -535,10 +513,9 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
trackRecordingServiceConnection.startAndBind(); trackRecordingServiceConnection.startAndBind();
/* /*
* If the binding has happened, then invoke the callback to start a new * If the binding has happened, then invoke the callback to start a new recording.
* recording. If the binding hasn't happened, then invoking the callback * If the binding hasn't happened, then invoking the callback will have no effect.
* will have no effect. But when the binding occurs, the callback will get * But when the binding occurs, the callback will get invoked.
* invoked.
*/ */
bindChangedCallback.run(); bindChangedCallback.run();
} }
@@ -551,12 +528,18 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
* @return true if handled. * @return true if handled.
*/ */
private boolean handleContextItem(int itemId, long[] trackIds) { private boolean handleContextItem(int itemId, long[] trackIds) {
Intent intent;
switch (itemId) { switch (itemId) {
case R.id.list_context_menu_show_on_map: case R.id.list_context_menu_show_on_map:
IntentUtils.showTrackOnMap(this, trackIds); IntentUtils.showTrackOnMap(this, trackIds);
return true; return true;
case R.id.list_context_menu_share:
intent = IntentUtils.newShareFileIntent(this, trackIds);
intent = Intent.createChooser(intent, null);
startActivity(intent);
return true;
case R.id.list_context_menu_edit: case R.id.list_context_menu_edit:
Intent intent = IntentUtils.newIntent(this, TrackEditActivity.class) intent = IntentUtils.newIntent(this, TrackEditActivity.class)
.putExtra(TrackEditActivity.EXTRA_TRACK_ID, trackIds[0]); .putExtra(TrackEditActivity.EXTRA_TRACK_ID, trackIds[0]);
startActivity(intent); startActivity(intent);
return true; return true;
@@ -8,6 +8,9 @@ import android.net.Uri;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
/**
* Wraps a {@link ContentResolver} and provides access as {@link IContentResolver}.
*/
public class ContentResolverWrapper implements IContentResolver { public class ContentResolverWrapper implements IContentResolver {
private ContentResolver contentResolver; private ContentResolver contentResolver;
@@ -35,6 +35,11 @@ import de.dennisguse.opentracks.content.Waypoint.WaypointType;
*/ */
public interface ContentProviderUtils { public interface ContentProviderUtils {
/**
* Maximum number of waypoints that will be loaded at one time.
*/
int MAX_LOADED_WAYPOINTS_POINTS = 10000;
/** /**
* The authority (the first part of the URI) for the app's content provider. * The authority (the first part of the URI) for the app's content provider.
*/ */
@@ -56,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 * @param trackId the track id
*/ */
@@ -82,20 +88,19 @@ public interface ContentProviderUtils {
void deleteTrack(Context context, long trackId); void deleteTrack(Context context, long trackId);
/** /**
* Gets all the tracks. If no track exists, an empty list is returned. * Gets all the tracks.
* <p> * If no track exists, an empty list is returned.
* Note that the returned tracks do not have any track points attached. * NOTE: the returned tracks do not have any track points attached.
*/ */
List<Track> getAllTracks(); List<Track> getAllTracks();
/** /**
* Gets the last track. Returns null if doesn't exist. * Gets the last track or null.
*/ */
Track getLastTrack(); Track getLastTrack();
/** /**
* Gets a track by a track id. Returns null if not found. * Gets a track by a track id or null
* <p>
* Note that the returned track doesn't have any track points attached. * Note that the returned track doesn't have any track points attached.
* *
* @param trackId the track id. * @param trackId the track id.
@@ -103,8 +108,8 @@ public interface ContentProviderUtils {
Track getTrack(long trackId); Track getTrack(long trackId);
/** /**
* Gets a track cursor. The caller owns the returned cursor and is responsible * Gets a track cursor.
* for closing it. * The caller owns the returned cursor and is responsible for closing it.
* *
* @param selection the selection. Can be null * @param selection the selection. Can be null
* @param selectionArgs the selection arguments. Can be null * @param selectionArgs the selection arguments. Can be null
@@ -114,8 +119,7 @@ public interface ContentProviderUtils {
/** /**
* Inserts a track. * Inserts a track.
* <p> * NOTE: This doesn't insert any track points.
* Note: This doesn't insert any track points.
* *
* @param track the track * @param track the track
* @return the content provider URI of the inserted track. * @return the content provider URI of the inserted track.
@@ -124,8 +128,7 @@ public interface ContentProviderUtils {
/** /**
* Updates a track. * Updates a track.
* <p> * NOTE: This doesn't update any track points.
* Note: This doesn't update any track points.
* *
* @param track the track * @param track the track
*/ */
@@ -139,19 +142,19 @@ public interface ContentProviderUtils {
Waypoint createWaypoint(Cursor cursor); Waypoint createWaypoint(Cursor cursor);
/** /**
* Deletes a waypoint. If deleting a statistics waypoint, this will also * Deletes a waypoint.
* correct the next statistics waypoint after the deleted one to reflect the * If deleting a statistics waypoint, this will also correct the next statistics waypoint after the deleted one to reflect the deletion.
* deletion. The generator is used to update the next statistics waypoint. * The generator is used to update the next statistics waypoint.
* *
* @param waypointId the waypoint id * @param waypointId the waypoint id
* @param descriptionGenerator the description generator. Can be null for * @param descriptionGenerator the description generator. Can be null for waypoint marker
* waypoint marker
*/ */
void deleteWaypoint(Context context, long waypointId, DescriptionGenerator descriptionGenerator); void deleteWaypoint(Context context, long waypointId, DescriptionGenerator descriptionGenerator);
/** /**
* Gets the first waypoint id for a track. The first waypoint is special as it * Gets the first waypoint id for a track.
* contains the stats for the track. Returns -1L if it doesn't exist. * 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 * @param trackId the track id
*/ */
@@ -166,8 +169,8 @@ public interface ContentProviderUtils {
Waypoint getLastWaypoint(long trackId, WaypointType waypointType); Waypoint getLastWaypoint(long trackId, WaypointType waypointType);
/** /**
* Gets the next waypoint number for a type. Returns -1 if not able to get the * Gets the next waypoint number for a type.
* next waypoint number. * Returns -1 if not able to get the next waypoint number.
* *
* @param trackId the track id * @param trackId the track id
* @param waypointType the waypoint type * @param waypointType the waypoint type
@@ -175,15 +178,16 @@ public interface ContentProviderUtils {
int getNextWaypointNumber(long trackId, WaypointType waypointType); 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 * @param waypointId the waypoint id
*/ */
Waypoint getWaypoint(long waypointId); Waypoint getWaypoint(long waypointId);
/** /**
* Gets a waypoint cursor. The caller owns the returned cursor and is * Gets a waypoint cursor.
* responsible for closing it. * he caller owns the returned cursor and is responsible for closing it.
* *
* @param selection the selection. Can be null * @param selection the selection. Can be null
* @param selectionArgs the selection arguments. Can be null * @param selectionArgs the selection arguments. Can be null
@@ -194,13 +198,12 @@ public interface ContentProviderUtils {
Cursor getWaypointCursor(String selection, String[] selectionArgs, String sortOrder, int maxWaypoints); Cursor getWaypointCursor(String selection, String[] selectionArgs, String sortOrder, int maxWaypoints);
/** /**
* Gets a waypoint cursor for a track. The caller owns the returned cursor and * Gets a waypoint cursor for a track.
* is responsible for closing it. * The caller owns the returned cursor and is responsible for closing it.
* *
* @param trackId the track id * @param trackId the track id
* @param minWaypointId the minimum waypoint id. -1L to ignore * @param minWaypointId the minimum waypoint id. -1L to ignore
* @param maxWaypoints the maximum number of waypoints to return. -1 for no * @param maxWaypoints the maximum number of waypoints to return. -1 for no limit
* limit
*/ */
Cursor getWaypointCursor(long trackId, long minWaypointId, int maxWaypoints); Cursor getWaypointCursor(long trackId, long minWaypointId, int maxWaypoints);
@@ -220,7 +223,8 @@ public interface ContentProviderUtils {
Uri insertWaypoint(Waypoint waypoint); Uri insertWaypoint(Waypoint waypoint);
/** /**
* Updates a waypoint. Returns true if successful. * Updates a waypoint.
* Returns true if successful.
* *
* @param waypoint the waypoint * @param waypoint the waypoint
*/ */
@@ -245,14 +249,16 @@ public interface ContentProviderUtils {
Location createTrackPoint(Cursor cursor); 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 * @param trackId the track id
*/ */
long getFirstTrackPointId(long trackId); 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 * @param trackId the track id
*/ */
@@ -268,57 +274,44 @@ public interface ContentProviderUtils {
long getTrackPointId(long trackId, Location location); long getTrackPointId(long trackId, Location location);
/** /**
* Gets the first valid location for a track. Returns null if it doesn't * Gets the first valid location for a track.
* exist. * Returns null if it doesn't exist.
* *
* @param trackId the track id * @param trackId the track id
*/ */
Location getFirstValidTrackPoint(long trackId); 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 * @param trackId the track id
*/ */
Location getLastValidTrackPoint(long trackId); Location getLastValidTrackPoint(long trackId);
/** /**
* Gets the last valid location. * Creates a location cursor. The caller owns the returned cursor and is responsible for closing it.
*/
Location getLastValidTrackPoint();
/**
* Creates a location cursor. The caller owns the returned cursor and is
* responsible for closing it.
* *
* @param trackId the track id * @param trackId the track id
* @param startTrackPointId the starting track point id. -1L to ignore * @param startTrackPointId the starting track point id. -1L to ignore
* @param maxLocations maximum number of locations to return. -1 for no limit * @param maxLocations maximum number of locations to return. -1 for no limit
* @param descending true to sort the result in descending order (latest * @param descending true to sort the result in descending order (latest location first)
* location first)
*/ */
Cursor getTrackPointCursor( Cursor getTrackPointCursor(long trackId, long startTrackPointId, int maxLocations, boolean descending);
long trackId, long startTrackPointId, int maxLocations, boolean descending);
/** /**
* Creates a new read-only iterator over a given track's points. It provides a * Creates a new read-only iterator over a given track's points.
* lightweight way of iterating over long tracks without failing due to the * It provides a lightweight way of iterating over long tracks without failing due to the underlying cursor limitations.
* underlying cursor limitations. Since it's a read-only iterator, * Since it's a read-only iterator, {@link Iterator#remove()} always throws {@link UnsupportedOperationException}.
* {@link Iterator#remove()} always throws * 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.
* {@link UnsupportedOperationException}. Each call to * When done with iteration, {@link LocationIterator#close()} must be called.
* {@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 trackId the track id
* @param startTrackPointId the starting track point id. -1L to ignore * @param startTrackPointId the starting track point id. -1L to ignore
* @param descending true to sort the result in descending order (latest * @param descending true to sort the result in descending order (latest location first)
* location first)
* @param locationFactory the location factory * @param locationFactory the location factory
*/ */
LocationIterator getTrackPointLocationIterator( LocationIterator getTrackPointLocationIterator(long trackId, long startTrackPointId, boolean descending, LocationFactory locationFactory);
long trackId, long startTrackPointId, boolean descending, LocationFactory locationFactory);
/** /**
* Inserts a track point. * Inserts a track point.
@@ -330,10 +323,9 @@ public interface ContentProviderUtils {
Uri insertTrackPoint(Location location, long trackId); Uri insertTrackPoint(Location location, long trackId);
/** /**
* A lightweight wrapper around the original {@link Cursor} with a method to * A lightweight wrapper around the original {@link Cursor} with a method to clean up.
* clean up.
*/ */
interface LocationIterator extends Iterator<Location> { interface LocationIterator extends Iterator<Location>, AutoCloseable {
/** /**
* Gets the most recently retrieved track point id by {@link #next()}. * Gets the most recently retrieved track point id by {@link #next()}.
@@ -352,8 +344,8 @@ public interface ContentProviderUtils {
interface LocationFactory { interface LocationFactory {
/** /**
* Creates a new {@link Location}. An implementation can create new * Creates a new {@link Location}.
* instances or reuse existing instances for optimization. * An implementation can create new instances or reuse existing instances for optimization.
*/ */
Location createLocation(); Location createLocation();
} }
@@ -363,41 +355,12 @@ public interface ContentProviderUtils {
*/ */
class Factory { class Factory {
private static Factory instance = new Factory();
/** /**
* Creates an instance of {@link ContentProviderUtils}. * Creates an instance of {@link ContentProviderUtils}.
* *
* @param context the context * @param context the context
*/ */
public static ContentProviderUtils get(Context 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()); return new ContentProviderUtilsImpl(context.getContentResolver());
} }
} }
@@ -843,16 +843,7 @@ public class ContentProviderUtilsImpl implements ContentProviderUtils {
} }
@Override @Override
public Location getLastValidTrackPoint() { public Cursor getTrackPointCursor(long trackId, long startTrackPointId, int maxLocations, boolean descending) {
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) {
if (trackId < 0) { if (trackId < 0) {
return null; return null;
} }
@@ -969,8 +960,7 @@ public class ContentProviderUtilsImpl implements ContentProviderUtils {
@Override @Override
public Uri insertTrackPoint(Location location, long trackId) { public Uri insertTrackPoint(Location location, long trackId) {
return contentResolver.insert( return contentResolver.insert(TrackPointsColumns.CONTENT_URI, createContentValues(location, trackId));
TrackPointsColumns.CONTENT_URI, createContentValues(location, trackId));
} }
/** /**
@@ -1080,10 +1070,8 @@ public class ContentProviderUtilsImpl implements ContentProviderUtils {
* @param selectionArgs the selection arguments * @param selectionArgs the selection arguments
* @param sortOrder the sort order * @param sortOrder the sort order
*/ */
private Cursor getTrackPointCursor( private Cursor getTrackPointCursor(String[] projection, String selection, String[] selectionArgs, String sortOrder) {
String[] projection, String selection, String[] selectionArgs, String sortOrder) { return contentResolver.query(TrackPointsColumns.CONTENT_URI, projection, selection, selectionArgs, sortOrder);
return contentResolver.query(
TrackPointsColumns.CONTENT_URI, projection, selection, selectionArgs, sortOrder);
} }
/** /**
@@ -35,8 +35,7 @@ import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting; import androidx.annotation.VisibleForTesting;
/** /**
* A {@link ContentProvider} that handles access to track points, tracks, and * A {@link ContentProvider} that handles access to track points, tracks, and waypoints tables.
* waypoints tables.
* *
* @author Leif Hendrik Wilden * @author Leif Hendrik Wilden
*/ */
@@ -44,10 +43,14 @@ public abstract class CustomContentProvider extends ContentProvider {
@VisibleForTesting @VisibleForTesting
static final int DATABASE_VERSION = 23; static final int DATABASE_VERSION = 23;
@VisibleForTesting @VisibleForTesting
static final String DATABASE_NAME = "database.db"; static final String DATABASE_NAME = "database.db";
private static final String TAG = CustomContentProvider.class.getSimpleName(); private static final String TAG = CustomContentProvider.class.getSimpleName();
private final UriMatcher uriMatcher; private final UriMatcher uriMatcher;
private SQLiteDatabase db; private SQLiteDatabase db;
public CustomContentProvider() { public CustomContentProvider() {
@@ -30,12 +30,12 @@ import de.dennisguse.opentracks.util.PreferencesUtils;
* *
* @author Rodrigo Damazio * @author Rodrigo Damazio
*/ */
public class DataSource { class DataSource {
private final ContentResolver contentResolver; private final ContentResolver contentResolver;
private final SharedPreferences sharedPreferences; private final SharedPreferences sharedPreferences;
public DataSource(Context context) { DataSource(Context context) {
contentResolver = context.getContentResolver(); contentResolver = context.getContentResolver();
sharedPreferences = PreferencesUtils.getSharedPreferences(context); sharedPreferences = PreferencesUtils.getSharedPreferences(context);
} }
@@ -46,7 +46,7 @@ public class DataSource {
* @param uri the uri * @param uri the uri
* @param observer the observer * @param observer the observer
*/ */
public void registerContentObserver(Uri uri, ContentObserver observer) { void registerContentObserver(Uri uri, ContentObserver observer) {
contentResolver.registerContentObserver(uri, false, observer); contentResolver.registerContentObserver(uri, false, observer);
} }
@@ -55,7 +55,7 @@ public class DataSource {
* *
* @param observer the observer * @param observer the observer
*/ */
public void unregisterContentObserver(ContentObserver observer) { void unregisterContentObserver(ContentObserver observer) {
contentResolver.unregisterContentObserver(observer); contentResolver.unregisterContentObserver(observer);
} }
@@ -64,7 +64,7 @@ public class DataSource {
* *
* @param listener the listener * @param listener the listener
*/ */
public void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) { void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {
sharedPreferences.registerOnSharedPreferenceChangeListener(listener); sharedPreferences.registerOnSharedPreferenceChangeListener(listener);
} }
@@ -73,8 +73,7 @@ public class DataSource {
* *
* @param listener the listener * @param listener the listener
*/ */
public void unregisterOnSharedPreferenceChangeListener( void unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {
OnSharedPreferenceChangeListener listener) {
sharedPreferences.unregisterOnSharedPreferenceChangeListener(listener); sharedPreferences.unregisterOnSharedPreferenceChangeListener(listener);
} }
} }
@@ -26,17 +26,19 @@ import java.util.EnumSet;
import java.util.Set; import java.util.Set;
/** /**
* Data source manager. Creates observers/listeners and manages their * Creates observers/listeners and manages their registration with {@link DataSource}.
* registration with {@link DataSource}. The observers/listeners calls * The observers/listeners calls {@link DataSourceListener} when data changes.
* {@link DataSourceListener} when data changes.
* *
* @author Rodrigo Damazio * @author Rodrigo Damazio
*/ */
public class DataSourceManager { class DataSourceManager {
private static final String TAG = DataSourceManager.class.getSimpleName(); private static final String TAG = DataSourceManager.class.getSimpleName();
private final DataSource dataSource; private final DataSource dataSource;
private final DataSourceListener dataSourceListener; private final DataSourceListener dataSourceListener;
// Registered listeners // Registered listeners
private final Set<TrackDataType> registeredListeners = EnumSet.noneOf(TrackDataType.class); private final Set<TrackDataType> registeredListeners = EnumSet.noneOf(TrackDataType.class);
private final Handler handler; private final Handler handler;
@@ -44,7 +46,8 @@ public class DataSourceManager {
private final WaypointsTableObserver waypointsTableObserver; private final WaypointsTableObserver waypointsTableObserver;
private final TrackPointsTableObserver trackPointsTableObserver; private final TrackPointsTableObserver trackPointsTableObserver;
private final PreferenceListener preferenceListener; private final PreferenceListener preferenceListener;
public DataSourceManager(DataSource dataSource, DataSourceListener dataSourceListener) {
DataSourceManager(DataSource dataSource, DataSourceListener dataSourceListener) {
this.dataSource = dataSource; this.dataSource = dataSource;
this.dataSourceListener = dataSourceListener; this.dataSourceListener = dataSourceListener;
@@ -60,7 +63,7 @@ public class DataSourceManager {
* *
* @param listeners the listeners * @param listeners the listeners
*/ */
public void updateListeners(EnumSet<TrackDataType> listeners) { void updateListeners(EnumSet<TrackDataType> listeners) {
EnumSet<TrackDataType> neededListeners = EnumSet.copyOf(listeners); EnumSet<TrackDataType> neededListeners = EnumSet.copyOf(listeners);
/* /*
@@ -155,7 +158,7 @@ public class DataSourceManager {
/** /**
* Unregisters all listeners with data source. * Unregisters all listeners with data source.
*/ */
public void unregisterAllListeners() { void unregisterAllListeners() {
for (TrackDataType trackDataType : TrackDataType.values()) { for (TrackDataType trackDataType : TrackDataType.values()) {
unregisterListener(trackDataType); unregisterListener(trackDataType);
} }
@@ -168,7 +171,7 @@ public class DataSourceManager {
*/ */
private class TracksTableObserver extends ContentObserver { private class TracksTableObserver extends ContentObserver {
public TracksTableObserver() { TracksTableObserver() {
super(handler); super(handler);
} }
@@ -185,7 +188,7 @@ public class DataSourceManager {
*/ */
private class WaypointsTableObserver extends ContentObserver { private class WaypointsTableObserver extends ContentObserver {
public WaypointsTableObserver() { WaypointsTableObserver() {
super(handler); super(handler);
} }
@@ -202,7 +205,7 @@ public class DataSourceManager {
*/ */
private class TrackPointsTableObserver extends ContentObserver { private class TrackPointsTableObserver extends ContentObserver {
public TrackPointsTableObserver() { TrackPointsTableObserver() {
super(handler); super(handler);
} }
@@ -18,8 +18,7 @@ package de.dennisguse.opentracks.content;
import de.dennisguse.opentracks.stats.TripStatistics; import de.dennisguse.opentracks.stats.TripStatistics;
/** /**
* An interface for an object that can generate descriptions of track and * An interface for an object that can generate descriptions of track and waypoint.
* waypoint.
* *
* @author Sandor Dornbush * @author Sandor Dornbush
*/ */
@@ -17,6 +17,7 @@
package de.dennisguse.opentracks.content; package de.dennisguse.opentracks.content;
import android.content.Context; import android.content.Context;
import android.util.Pair;
import androidx.annotation.VisibleForTesting; import androidx.annotation.VisibleForTesting;
@@ -95,8 +96,7 @@ public class DescriptionGeneratorImpl implements DescriptionGenerator {
StringBuilder builder = new StringBuilder(); StringBuilder builder = new StringBuilder();
// Total distance // Total distance
writeDistance( writeDistance(stats.getTotalDistance(), builder, R.string.description_total_distance, lineBreak);
stats.getTotalDistance(), builder, R.string.description_total_distance, lineBreak);
// Total time // Total time
writeTime(stats.getTotalTime(), builder, R.string.description_total_time, lineBreak); writeTime(stats.getTotalTime(), builder, R.string.description_total_time, lineBreak);
@@ -108,19 +108,16 @@ public class DescriptionGeneratorImpl implements DescriptionGenerator {
writeSpeed(stats.getAverageSpeed(), builder, R.string.description_average_speed, lineBreak); writeSpeed(stats.getAverageSpeed(), builder, R.string.description_average_speed, lineBreak);
// Average moving speed // Average moving speed
writeSpeed(stats.getAverageMovingSpeed(), builder, R.string.description_average_moving_speed, writeSpeed(stats.getAverageMovingSpeed(), builder, R.string.description_average_moving_speed, lineBreak);
lineBreak);
// Max speed // Max speed
writeSpeed(stats.getMaxSpeed(), builder, R.string.description_max_speed, lineBreak); writeSpeed(stats.getMaxSpeed(), builder, R.string.description_max_speed, lineBreak);
// Average pace // Average pace
writePace( writePace(stats.getAverageSpeed(), builder, R.string.description_average_pace_in_minute, lineBreak);
stats.getAverageSpeed(), builder, R.string.description_average_pace_in_minute, lineBreak);
// Average moving pace // Average moving pace
writePace(stats.getAverageMovingSpeed(), builder, writePace(stats.getAverageMovingSpeed(), builder, R.string.description_average_moving_pace_in_minute, lineBreak);
R.string.description_average_moving_pace_in_minute, lineBreak);
// Fastest pace // Fastest pace
writePace(stats.getMaxSpeed(), builder, R.string.description_fastest_pace_in_minute, lineBreak); writePace(stats.getMaxSpeed(), builder, R.string.description_fastest_pace_in_minute, lineBreak);
@@ -132,8 +129,7 @@ public class DescriptionGeneratorImpl implements DescriptionGenerator {
writeElevation(stats.getMinElevation(), builder, R.string.description_min_elevation, lineBreak); writeElevation(stats.getMinElevation(), builder, R.string.description_min_elevation, lineBreak);
// Elevation gain // Elevation gain
writeElevation( writeElevation(stats.getTotalElevationGain(), builder, R.string.description_elevation_gain, lineBreak);
stats.getTotalElevationGain(), builder, R.string.description_elevation_gain, lineBreak);
// Max grade // Max grade
writeGrade(stats.getMaxGrade(), builder, R.string.description_max_grade, lineBreak); writeGrade(stats.getMaxGrade(), builder, R.string.description_max_grade, lineBreak);
@@ -142,9 +138,7 @@ public class DescriptionGeneratorImpl implements DescriptionGenerator {
writeGrade(stats.getMinGrade(), builder, R.string.description_min_grade, lineBreak); writeGrade(stats.getMinGrade(), builder, R.string.description_min_grade, lineBreak);
// Recorded time // Recorded time
builder.append( builder.append(context.getString(R.string.description_recorded_time, StringUtils.formatDateTime(context, stats.getStartTime())));
context.getString(R.string.description_recorded_time,
StringUtils.formatDateTime(context, stats.getStartTime())));
builder.append(lineBreak); builder.append(lineBreak);
return builder.toString(); return builder.toString();
@@ -189,8 +183,7 @@ public class DescriptionGeneratorImpl implements DescriptionGenerator {
* @param lineBreak line break string * @param lineBreak line break string
*/ */
@VisibleForTesting @VisibleForTesting
void writeSpeed( void writeSpeed(double speed, StringBuilder builder, int resId, String lineBreak) {
double speed, StringBuilder builder, int resId, String lineBreak) {
double speedInKmHr = speed * UnitConversions.MS_TO_KMH; double speedInKmHr = speed * UnitConversions.MS_TO_KMH;
double speedInMiHr = speedInKmHr * UnitConversions.KM_TO_MI; double speedInMiHr = speedInKmHr * UnitConversions.KM_TO_MI;
builder.append(context.getString(resId, speedInKmHr, speedInMiHr)); builder.append(context.getString(resId, speedInKmHr, speedInMiHr));
@@ -207,18 +200,13 @@ public class DescriptionGeneratorImpl implements DescriptionGenerator {
*/ */
@VisibleForTesting @VisibleForTesting
void writePace(double speed, StringBuilder builder, int resId, String lineBreak) { void writePace(double speed, StringBuilder builder, int resId, String lineBreak) {
String[] paceInMetrics = StringUtils.getSpeedParts(context, speed, true, false); Pair<String, String> paceInMetrics = StringUtils.getSpeedParts(context, speed, true, false);
String[] paceInImperial = StringUtils.getSpeedParts(context, speed, false, false); Pair<String, String> paceInImperial = StringUtils.getSpeedParts(context, speed, false, false);
if (paceInMetrics[0] == null) { String formattedPaceMetrics = paceInMetrics.first != null ? paceInMetrics.first : context.getString(R.string.value_unknown);
paceInMetrics[0] = context.getString(R.string.value_unknown); String formattedPaceImperial = paceInImperial.first != null ? paceInImperial.first : context.getString(R.string.value_unknown);
}
if (paceInImperial[0] == null) { builder.append(context.getString(resId, formattedPaceMetrics, formattedPaceImperial));
paceInImperial[0] = context.getString(R.string.value_unknown);
}
builder.append(context.getString(resId, paceInMetrics[0], paceInImperial[0]));
builder.append(lineBreak); builder.append(lineBreak);
} }
@@ -231,8 +219,7 @@ public class DescriptionGeneratorImpl implements DescriptionGenerator {
* @param lineBreak line break string * @param lineBreak line break string
*/ */
@VisibleForTesting @VisibleForTesting
void writeElevation( void writeElevation(double elevation, StringBuilder builder, int resId, String lineBreak) {
double elevation, StringBuilder builder, int resId, String lineBreak) {
long elevationInM = Math.round(elevation); long elevationInM = Math.round(elevation);
long elevationInFt = Math.round(elevation * UnitConversions.M_TO_FT); long elevationInFt = Math.round(elevation * UnitConversions.M_TO_FT);
builder.append(context.getString(resId, elevationInM, elevationInFt)); builder.append(context.getString(resId, elevationInM, elevationInFt));
@@ -249,8 +236,7 @@ public class DescriptionGeneratorImpl implements DescriptionGenerator {
*/ */
@VisibleForTesting @VisibleForTesting
void writeGrade(double grade, StringBuilder builder, int resId, String lineBreak) { void writeGrade(double grade, StringBuilder builder, int resId, String lineBreak) {
long gradeInPercent = Double.isNaN(grade) || Double.isInfinite(grade) ? 0L long gradeInPercent = Double.isNaN(grade) || Double.isInfinite(grade) ? 0L : Math.round(grade * 100);
: Math.round(grade * 100);
builder.append(context.getString(resId, gradeInPercent)); builder.append(context.getString(resId, gradeInPercent));
builder.append(lineBreak); builder.append(lineBreak);
} }
@@ -23,6 +23,7 @@ import androidx.annotation.NonNull;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.Comparator; import java.util.Comparator;
import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.SortedSet; import java.util.SortedSet;
import java.util.TreeSet; import java.util.TreeSet;
@@ -97,12 +98,12 @@ public class SearchEngine {
/** /**
* Oldest timestamp for which we rank based on time (2000-01-01 00:00:00.000) * 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. * Comparador for scored results.
*/ */
private static final Comparator<ScoredResult> SCORED_RESULT_COMPARATOR = private static final Comparator<ScoredResult> SCORED_RESULT_COMPARATOR = new Comparator<ScoredResult>() {
new Comparator<ScoredResult>() {
@Override @Override
public int compare(ScoredResult r1, ScoredResult r2) { public int compare(ScoredResult r1, ScoredResult r2) {
// Score ordering. // Score ordering.
@@ -125,6 +126,7 @@ public class SearchEngine {
return Long.signum(idDiff); return Long.signum(idDiff);
} }
}; };
private final ContentProviderUtils providerUtils; private final ContentProviderUtils providerUtils;
public SearchEngine(ContentProviderUtils providerUtils) { public SearchEngine(ContentProviderUtils providerUtils) {
@@ -134,26 +136,22 @@ public class SearchEngine {
/** /**
* Squashes a number by calculating 1 / log (1 + x). * Squashes a number by calculating 1 / log (1 + x).
*/ */
//TODO Why is this done?
private static double squash(double x) { private static double squash(double x) {
return 1.0 / Math.log1p(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 * @param query the query to execute
* @return a set of results, sorted according to their score * @return a set of results, sorted according to their score
*/ */
public SortedSet<ScoredResult> search(SearchQuery query) { public SortedSet<ScoredResult> search(SearchQuery query) {
ArrayList<Track> tracks = new ArrayList<>();
ArrayList<Waypoint> waypoints = new ArrayList<>();
TreeSet<ScoredResult> scoredResults = new TreeSet<>(SCORED_RESULT_COMPARATOR); TreeSet<ScoredResult> scoredResults = new TreeSet<>(SCORED_RESULT_COMPARATOR);
retrieveTracks(query, tracks); scoreTrackResults(retrieveTracks(query), query, scoredResults);
retrieveWaypoints(query, waypoints); scoreWaypointResults(retrieveWaypoints(query), query, scoredResults);
scoreTrackResults(tracks, query, scoredResults);
scoreWaypointResults(waypoints, query, scoredResults);
return scoredResults; return scoredResults;
} }
@@ -162,17 +160,18 @@ public class SearchEngine {
* Retrieves tracks matching the given query from the database. * Retrieves tracks matching the given query from the database.
* *
* @param query the query to retrieve for * @param query the query to retrieve for
* @param tracks list to fill with the resulting tracks
*/ */
private void retrieveTracks(SearchQuery query, ArrayList<Track> tracks) { private List<Track> retrieveTracks(SearchQuery query) {
ArrayList<Track> tracks = new ArrayList<>();
String queryLikeSelection = "%" + query.textQuery + "%"; String queryLikeSelection = "%" + query.textQuery + "%";
String[] trackSelectionArgs = new String[]{ String[] trackSelectionArgs = new String[]{
queryLikeSelection, queryLikeSelection,
queryLikeSelection, queryLikeSelection,
queryLikeSelection}; queryLikeSelection
};
try (Cursor cursor = providerUtils.getTrackCursor( try (Cursor cursor = providerUtils.getTrackCursor(TRACK_SELECTION_QUERY, trackSelectionArgs, TRACK_SELECTION_ORDER)) {
TRACK_SELECTION_QUERY, trackSelectionArgs, TRACK_SELECTION_ORDER)) {
if (cursor != null) { if (cursor != null) {
tracks.ensureCapacity(cursor.getCount()); tracks.ensureCapacity(cursor.getCount());
while (cursor.moveToNext()) { while (cursor.moveToNext()) {
@@ -180,22 +179,26 @@ public class SearchEngine {
} }
} }
} }
return tracks;
} }
/** /**
* Retrieves waypoints matching the given query from the database. * Retrieves waypoints matching the given query from the database.
* *
* @param query the query to retrieve for * @param query the query to retrieve for
* @param waypoints list to fill with the resulting waypoints
*/ */
private void retrieveWaypoints(SearchQuery query, ArrayList<Waypoint> waypoints) { private List<Waypoint> retrieveWaypoints(SearchQuery query) {
ArrayList<Waypoint> waypoints = new ArrayList<>();
String queryLikeSelection2 = "%" + query.textQuery + "%"; String queryLikeSelection2 = "%" + query.textQuery + "%";
String[] waypointSelectionArgs = new String[]{ String[] waypointSelectionArgs = new String[]{
queryLikeSelection2, queryLikeSelection2,
queryLikeSelection2, queryLikeSelection2,
queryLikeSelection2}; queryLikeSelection2
try (Cursor cursor = providerUtils.getWaypointCursor(WAYPOINT_SELECTION_QUERY, waypointSelectionArgs, };
WAYPOINT_SELECTION_ORDER, MAX_SCORED_WAYPOINTS)) {
try (Cursor cursor = providerUtils.getWaypointCursor(WAYPOINT_SELECTION_QUERY, waypointSelectionArgs, WAYPOINT_SELECTION_ORDER, MAX_SCORED_WAYPOINTS)) {
if (cursor != null) { if (cursor != null) {
waypoints.ensureCapacity(cursor.getCount()); waypoints.ensureCapacity(cursor.getCount());
while (cursor.moveToNext()) { 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()); score *= getTitleBoost(query, track.getName(), track.getDescription(), track.getCategory());
TripStatistics statistics = track.getTripStatistics(); TripStatistics statistics = track.getTripStatistics();
// TODO: Also boost for proximity to the currently-centered position on the map.
score *= getDistanceBoost(query, statistics.getMeanLatitude(), statistics.getMeanLongitude()); score *= getDistanceBoost(query, statistics.getMeanLatitude(), statistics.getMeanLongitude());
long meanTimestamp = (statistics.getStartTime() + statistics.getStopTime()) / 2L; long meanTimestamp = (statistics.getStartTime() + statistics.getStopTime()) / 2L;
@@ -281,7 +285,6 @@ public class SearchEngine {
Location location = waypoint.getLocation(); Location location = waypoint.getLocation();
score *= getTitleBoost(query, waypoint.getName(), waypoint.getDescription(), waypoint.getCategory()); 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 *= getDistanceBoost(query, location.getLatitude(), location.getLongitude());
score *= getTimeBoost(query, location.getTime()); score *= getTimeBoost(query, location.getTime());
@@ -302,8 +305,7 @@ public class SearchEngine {
* @param category the category of the track or waypoint * @param category the category of the track or waypoint
* @return the total boost to be applied to the result * @return the total boost to be applied to the result
*/ */
private double getTitleBoost(SearchQuery query, private double getTitleBoost(SearchQuery query, String name, String description, String category) {
String name, String description, String category) {
// Title boost: track name > description > category. // Title boost: track name > description > category.
double boost = 1.0; double boost = 1.0;
if (name.toLowerCase(Locale.getDefault()).contains(query.textQuery)) { if (name.toLowerCase(Locale.getDefault()).contains(query.textQuery)) {
@@ -332,8 +334,7 @@ public class SearchEngine {
} }
// Score recent tracks higher. // Score recent tracks higher.
long timeAgoHours = (long) ((query.currentTimestamp - timestamp) * UnitConversions.MS_TO_S long timeAgoHours = (long) ((query.currentTimestamp - timestamp) * UnitConversions.MS_TO_S * UnitConversions.S_TO_MIN * UnitConversions.MIN_TO_HR);
* UnitConversions.S_TO_MIN * UnitConversions.MIN_TO_HR);
if (timeAgoHours > 0L) { if (timeAgoHours > 0L) {
return squash(timeAgoHours); return squash(timeAgoHours);
} else { } else {
@@ -364,6 +365,7 @@ public class SearchEngine {
// Score tracks close to the current location higher. // Score tracks close to the current location higher.
double distanceKm = distanceResults[0] * UnitConversions.M_TO_KM; double distanceKm = distanceResults[0] * UnitConversions.M_TO_KM;
if (distanceKm > 0.0) { if (distanceKm > 0.0) {
// Use the inverse of the amortized distance. // Use the inverse of the amortized distance.
return squash(distanceKm); return squash(distanceKm);
@@ -374,16 +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 static class SearchQuery {
public final String textQuery; final String textQuery;
public final Location currentLocation; final Location currentLocation;
public final long currentTrackId; final long currentTrackId;
public final long currentTimestamp; final long currentTimestamp;
public SearchQuery(String textQuery, Location currentLocation, long currentTrackId, public SearchQuery(String textQuery, Location currentLocation, long currentTrackId, long currentTimestamp) {
long currentTimestamp) {
this.textQuery = textQuery.toLowerCase(Locale.getDefault()); this.textQuery = textQuery.toLowerCase(Locale.getDefault());
this.currentLocation = currentLocation; this.currentLocation = currentLocation;
this.currentTrackId = currentTrackId; this.currentTrackId = currentTrackId;
@@ -392,12 +393,12 @@ 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 static class ScoredResult {
public final Track track; public final Track track;
public final Waypoint waypoint; public final Waypoint waypoint;
public final double score; final double score;
ScoredResult(Track track, double score) { ScoredResult(Track track, double score) {
this.track = track; this.track = track;
@@ -27,9 +27,9 @@ 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. * 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}. * The actual content of the virtual files is generated by using the functionality defined in {@link CustomContentProvider}.
* <p> *
* Moreover, it manages access to OpenTrack's database via {@link CustomContentProvider}. * Moreover, it manages access to OpenTrack's database via {@link CustomContentProvider}.
* <p> *
* Explanation: * 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}. * 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. * Thus, if {@link ShareContentProvider} and {@link CustomContentProvider} would be two different instances, the data would not be accessible to external apps.
@@ -44,21 +44,28 @@ public class ShareContentProvider extends CustomContentProvider implements ICont
private static final int URI_GPX = 0; private static final int URI_GPX = 0;
private static final int URI_KML_ONLY = 1; private static final int URI_KML_ONLY = 1;
private static final int URI_KML_WITH_SENSORDATA = 2; private static final int URI_KML_WITH_TRACKDETAIL = 2;
private static final int URI_KMZ_ONLY_TRACK = 3; private static final int URI_KML_WITH_TRACKDETAIL_SENSORDATA = 3;
private static final int URI_KMZ_WITH_SENSORDATA = 4;
private static final int URI_KMZ_WITH_SENSORDATA_AND_PICTURES = 5; 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 UriMatcher uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
private static final String TRACKID_DELIMITER = "_"; private static final String TRACKID_DELIMITER = "_";
static { static {
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_SENSORDATA.getName() + "/*", URI_KML_WITH_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_SENSORDATA.getName() + "/*", URI_KMZ_WITH_SENSORDATA);
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TracksColumns.TABLE_NAME + "/" + TrackFileFormat.KMZ_WITH_SENSORDATA_AND_PICTURES.getName() + "/*", URI_KMZ_WITH_SENSORDATA_AND_PICTURES);
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TracksColumns.TABLE_NAME + "/" + TrackFileFormat.GPX.getName() + "/*", URI_GPX); 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) { public static Pair<Uri, String> createURI(long[] trackIds, @NonNull TrackFileFormat trackFileFormat) {
@@ -118,18 +125,25 @@ public class ShareContentProvider extends CustomContentProvider implements ICont
switch (uriMatcher.match(uri)) { switch (uriMatcher.match(uri)) {
case URI_GPX: case URI_GPX:
return TrackFileFormat.GPX; return TrackFileFormat.GPX;
case URI_KML_ONLY: case URI_KML_ONLY:
return TrackFileFormat.KML_ONLY_TRACK; return TrackFileFormat.KML_ONLY_TRACK;
case URI_KML_WITH_SENSORDATA: case URI_KML_WITH_TRACKDETAIL:
return TrackFileFormat.KML_WITH_SENSORDATA; return TrackFileFormat.KML_WITH_TRACKDETAIL;
case URI_KML_WITH_TRACKDETAIL_SENSORDATA:
return TrackFileFormat.KML_WITH_TRACKDETAIL_AND_SENSORDATA;
case URI_KMZ_ONLY_TRACK: case URI_KMZ_ONLY_TRACK:
return TrackFileFormat.KMZ_ONLY_TRACK; return TrackFileFormat.KMZ_ONLY_TRACK;
case URI_KMZ_WITH_SENSORDATA: case URI_KMZ_WITH_TRACKDETAIL:
return TrackFileFormat.KMZ_WITH_SENSORDATA; return TrackFileFormat.KMZ_WITH_TRACKDETAIL;
case URI_KMZ_WITH_SENSORDATA_AND_PICTURES: case URI_KMZ_WITH_TRACKDETAIL_AND_SENSORDATA:
return TrackFileFormat.KMZ_WITH_SENSORDATA_AND_PICTURES; return TrackFileFormat.KMZ_WITH_TRACKDETAIL_AND_SENSORDATA;
default: //URI_KML_ONLY case URI_KMZ_WITH_TRACKDETAIL_SENSORDATA_AND_PICTURES:
return TrackFileFormat.KMZ_ONLY_TRACK; return TrackFileFormat.KMZ_WITH_TRACKDETAIL_AND_SENSORDATA_AND_PICTURES;
default:
throw new RuntimeException("Could not derive TrackFileFormat from Uri " + uri);
} }
} }
@@ -198,7 +212,7 @@ public class ShareContentProvider extends CustomContentProvider implements ICont
@Override @Override
public void writeDataToPipe(@NonNull ParcelFileDescriptor output, @NonNull Uri uri, @NonNull String mimeType, @Nullable Bundle opts, @Nullable String args) { 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())) { try (FileOutputStream fileOutputStream = new FileOutputStream(output.getFileDescriptor())) {
trackExporter.writeTrack(fileOutputStream); trackExporter.writeTrack(getContext(), fileOutputStream);
} catch (IOException e) { } catch (IOException e) {
Log.w(TAG, "there occurred an error while sharing a file: " + e); Log.w(TAG, "there occurred an error while sharing a file: " + e);
} }
@@ -17,12 +17,11 @@
package de.dennisguse.opentracks.content; package de.dennisguse.opentracks.content;
import android.location.Location; 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.ArrayList;
import java.util.List;
import de.dennisguse.opentracks.stats.TripStatistics;
/** /**
* A track. * A track.
@@ -30,82 +29,29 @@ import java.util.ArrayList;
* @author Leif Hendrik Wilden * @author Leif Hendrik Wilden
* @author Rodrigo Damazio * @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 long id = -1L;
private String name = ""; private String name = "";
private String description = ""; private String description = "";
private String category = ""; private String category = "";
private long startId = -1L; private long startId = -1L;
private long stopId = -1L; private long stopId = -1L;
/* /*
* The number of location points (present even if the points themselves are * The number of location points (present even if the points themselves are not loaded)
* not loaded)
*/ */
private int numberOfPoints = 0; private int numberOfPoints = 0;
private String icon = ""; private String icon = "";
private long modifiedTime = -1L;
private TripStatistics tripStatistics = new TripStatistics(); private TripStatistics tripStatistics = new TripStatistics();
// Location points (which may not have been loaded) // Location points (which may not have been loaded)
private ArrayList<Location> locations = new ArrayList<>(); private List<Location> locations = new ArrayList<>();
public Track() { 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() { public long getId() {
return id; return id;
} }
@@ -170,14 +116,6 @@ public class Track implements Parcelable {
this.icon = icon; this.icon = icon;
} }
public long getModifiedTime() {
return modifiedTime;
}
public void setModifiedTime(long modifiedTime) {
this.modifiedTime = modifiedTime;
}
public TripStatistics getTripStatistics() { public TripStatistics getTripStatistics() {
return tripStatistics; return tripStatistics;
} }
@@ -190,7 +128,7 @@ public class Track implements Parcelable {
locations.add(location); locations.add(location);
} }
public ArrayList<Location> getLocations() { public List<Location> getLocations() {
return locations; return locations;
} }
@@ -35,24 +35,25 @@ import de.dennisguse.opentracks.util.LocationUtils;
import de.dennisguse.opentracks.util.PreferencesUtils; import de.dennisguse.opentracks.util.PreferencesUtils;
/** /**
* Track data hub. Receives data from {@link DataSource} and distributes it to * Track data hub. Receives data from {@link DataSource} and distributes it to {@link TrackDataListener} after some processing.
* {@link TrackDataListener} after some processing.
* *
* @author Rodrigo Damazio * @author Rodrigo Damazio
*/ */
public class TrackDataHub implements DataSourceListener { public class TrackDataHub implements DataSourceListener {
/** /**
* Target number of track points displayed by the map overlay. We may display * Target number of track points displayed by the map overlay.
* more than this number of points. * 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. * Maximum number of waypoints to displayed.
*/ */
@VisibleForTesting @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 static final String TAG = TrackDataHub.class.getSimpleName();
private final Context context; private final Context context;
private final TrackDataManager trackDataManager; private final TrackDataManager trackDataManager;
private final ContentProviderUtils contentProviderUtils; private final ContentProviderUtils contentProviderUtils;
@@ -87,8 +88,7 @@ public class TrackDataHub implements DataSourceListener {
* @param targetNumPoints the target number of points * @param targetNumPoints the target number of points
*/ */
@VisibleForTesting @VisibleForTesting
TrackDataHub(Context context, TrackDataManager trackDataManager, private TrackDataHub(Context context, TrackDataManager trackDataManager, ContentProviderUtils contentProviderUtils, int targetNumPoints) {
ContentProviderUtils contentProviderUtils, int targetNumPoints) {
this.context = context; this.context = context;
this.trackDataManager = trackDataManager; this.trackDataManager = trackDataManager;
this.contentProviderUtils = contentProviderUtils; this.contentProviderUtils = contentProviderUtils;
@@ -96,17 +96,11 @@ public class TrackDataHub implements DataSourceListener {
resetSamplingState(); resetSamplingState();
} }
/**
* Creates a new instance.
*/
public synchronized static TrackDataHub newInstance(Context context) { public synchronized static TrackDataHub newInstance(Context context) {
return new TrackDataHub(context, new TrackDataManager(), ContentProviderUtils.Factory.get( return new TrackDataHub(context, new TrackDataManager(), ContentProviderUtils.Factory.get(
context), TARGET_DISPLAYED_TRACK_POINTS); context), TARGET_DISPLAYED_TRACK_POINTS);
} }
/**
* Starts.
*/
public void start() { public void start() {
if (started) { if (started) {
Log.i(TAG, "TrackDataHub already started, ignoring start."); Log.i(TAG, "TrackDataHub already started, ignoring start.");
@@ -131,9 +125,6 @@ public class TrackDataHub implements DataSourceListener {
}); });
} }
/**
* Stops.
*/
public void stop() { public void stop() {
if (!started) { if (!started) {
Log.i(TAG, "TrackDataHub not started, ignoring stop."); Log.i(TAG, "TrackDataHub not started, ignoring stop.");
@@ -151,11 +142,6 @@ public class TrackDataHub implements DataSourceListener {
dataSourceManager = null; dataSourceManager = null;
} }
/**
* Loads a track.
*
* @param trackId the track id
*/
public void loadTrack(final long trackId) { public void loadTrack(final long trackId) {
runInHandlerThread(new Runnable() { runInHandlerThread(new Runnable() {
@Override @Override
@@ -222,8 +208,7 @@ public class TrackDataHub implements DataSourceListener {
* Returns true if the selected track is recording. * Returns true if the selected track is recording.
*/ */
public boolean isSelectedTrackRecording() { public boolean isSelectedTrackRecording() {
return selectedTrackId == recordingTrackId return selectedTrackId == recordingTrackId && PreferencesUtils.isRecording(recordingTrackId);
&& recordingTrackId != PreferencesUtils.RECORDING_TRACK_ID_DEFAULT;
} }
/** /**
@@ -258,8 +243,7 @@ public class TrackDataHub implements DataSourceListener {
runInHandlerThread(new Runnable() { runInHandlerThread(new Runnable() {
@Override @Override
public void run() { public void run() {
notifyTrackPointsTableUpdate( notifyTrackPointsTableUpdate(true, trackDataManager.getListeners(TrackDataType.SAMPLED_IN_TRACK_POINTS_TABLE),
true, trackDataManager.getListeners(TrackDataType.SAMPLED_IN_TRACK_POINTS_TABLE),
trackDataManager.getListeners(TrackDataType.SAMPLED_OUT_TRACK_POINTS_TABLE)); trackDataManager.getListeners(TrackDataType.SAMPLED_OUT_TRACK_POINTS_TABLE));
} }
}); });
@@ -270,47 +254,36 @@ public class TrackDataHub implements DataSourceListener {
runInHandlerThread(new Runnable() { runInHandlerThread(new Runnable() {
@Override @Override
public void run() { public void run() {
if (key == null if (key == null || key.equals(PreferencesUtils.getKey(context, R.string.recording_track_id_key))) {
|| key.equals(PreferencesUtils.getKey(context, R.string.recording_track_id_key))) {
recordingTrackId = PreferencesUtils.getLong(context, R.string.recording_track_id_key); recordingTrackId = PreferencesUtils.getLong(context, R.string.recording_track_id_key);
} }
if (key == null if (key == null || key.equals(PreferencesUtils.getKey(context, R.string.recording_track_paused_key))) {
|| 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);
recordingTrackPaused = PreferencesUtils.getBoolean(
context, R.string.recording_track_paused_key,
PreferencesUtils.RECORDING_TRACK_PAUSED_DEFAULT);
} }
if (key == null if (key == null || key.equals(PreferencesUtils.getKey(context, R.string.stats_units_key))) {
|| key.equals(PreferencesUtils.getKey(context, R.string.stats_units_key))) {
metricUnits = PreferencesUtils.isMetricUnits(context); metricUnits = PreferencesUtils.isMetricUnits(context);
if (key != null) { if (key != null) {
for (TrackDataListener trackDataListener : for (TrackDataListener trackDataListener : trackDataManager.getListeners(TrackDataType.PREFERENCE)) {
trackDataManager.getListeners(TrackDataType.PREFERENCE)) {
if (trackDataListener.onMetricUnitsChanged(metricUnits)) { if (trackDataListener.onMetricUnitsChanged(metricUnits)) {
loadDataForListener(trackDataListener); loadDataForListener(trackDataListener);
} }
} }
} }
} }
if (key == null if (key == null || key.equals(PreferencesUtils.getKey(context, R.string.stats_rate_key))) {
|| key.equals(PreferencesUtils.getKey(context, R.string.stats_rate_key))) {
reportSpeed = PreferencesUtils.isReportSpeed(context); reportSpeed = PreferencesUtils.isReportSpeed(context);
if (key != null) { if (key != null) {
for (TrackDataListener trackDataListener : for (TrackDataListener trackDataListener : trackDataManager.getListeners(TrackDataType.PREFERENCE)) {
trackDataManager.getListeners(TrackDataType.PREFERENCE)) {
if (trackDataListener.onReportSpeedChanged(reportSpeed)) { if (trackDataListener.onReportSpeedChanged(reportSpeed)) {
loadDataForListener(trackDataListener); loadDataForListener(trackDataListener);
} }
} }
} }
} }
if (key == null if (key == null || key.equals(PreferencesUtils.getKey(context, R.string.recording_gps_accuracy_key))) {
|| 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);
recordingGpsAccuracy = PreferencesUtils.getInt(context, R.string.recording_gps_accuracy_key,
PreferencesUtils.RECORDING_GPS_ACCURACY_DEFAULT);
if (key != null) { if (key != null) {
for (TrackDataListener trackDataListener : for (TrackDataListener trackDataListener : trackDataManager.getListeners(TrackDataType.PREFERENCE)) {
trackDataManager.getListeners(TrackDataType.PREFERENCE)) {
if (trackDataListener.onRecordingGpsAccuracy(recordingGpsAccuracy)) { if (trackDataListener.onRecordingGpsAccuracy(recordingGpsAccuracy)) {
loadDataForListener(trackDataListener); 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. * @param trackDataListener the track data listener.
*/ */
@@ -386,8 +359,7 @@ public class TrackDataHub implements DataSourceListener {
if (isOnlyListener) { if (isOnlyListener) {
resetSamplingState(); resetSamplingState();
} }
Set<TrackDataListener> sampledOutListeners = hasSampledOut ? trackDataListeners Set<TrackDataListener> sampledOutListeners = hasSampledOut ? trackDataListeners : Collections.<TrackDataListener>emptySet();
: Collections.<TrackDataListener>emptySet();
notifyTrackPointsTableUpdate(isOnlyListener, trackDataListeners, sampledOutListeners); 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 * @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 * Notifies waypoint table update.
* {@link #MAX_DISPLAYED_WAYPOINTS}. To be run in the {@link #handler} * Currently, reloads all the waypoints up to {@link #MAX_DISPLAYED_WAYPOINTS}. To be run in the {@link #handler} thread.
* thread.
* *
* @param trackDataListeners the track data listeners to notify * @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} * Notifies track points table update; to be run in the {@link #handler} thread.
* thread.
* *
* @param updateSamplingState true to update the sampling state * @param updateSamplingState true to update the sampling state
* @param sampledInListeners the sampled-in listeners * @param sampledInListeners the sampled-in listeners
* @param sampledOutListeners the sampled-out listeners * @param sampledOutListeners the sampled-out listeners
*/ */
private void notifyTrackPointsTableUpdate(boolean updateSamplingState, private void notifyTrackPointsTableUpdate(boolean updateSamplingState, Set<TrackDataListener> sampledInListeners, Set<TrackDataListener> sampledOutListeners) {
Set<TrackDataListener> sampledInListeners, Set<TrackDataListener> sampledOutListeners) {
if (sampledInListeners.isEmpty() && sampledOutListeners.isEmpty()) { if (sampledInListeners.isEmpty() && sampledOutListeners.isEmpty()) {
return; return;
} }
if (updateSamplingState && numLoadedPoints >= targetNumPoints) { if (updateSamplingState && numLoadedPoints >= targetNumPoints) {
// Reload and resample the track at a lower frequency. // Reload and resample the track at a lower frequency.
Log.i(TAG, "Resampling track after " + numLoadedPoints + " points."); Log.i(TAG, "Resampling track after " + numLoadedPoints + " points.");
@@ -477,11 +447,8 @@ public class TrackDataHub implements DataSourceListener {
long lastTrackPointId = contentProviderUtils.getLastTrackPointId(selectedTrackId); long lastTrackPointId = contentProviderUtils.getLastTrackPointId(selectedTrackId);
int samplingFrequency = -1; int samplingFrequency = -1;
boolean includeNextPoint = false; boolean includeNextPoint = false;
LocationIterator locationIterator = null;
try { try (LocationIterator locationIterator = contentProviderUtils.getTrackPointLocationIterator(selectedTrackId, localLastSeenLocationId + 1, false, ContentProviderUtils.DEFAULT_LOCATION_FACTORY)) {
locationIterator = contentProviderUtils.getTrackPointLocationIterator(selectedTrackId,
localLastSeenLocationId + 1, false, ContentProviderUtils.DEFAULT_LOCATION_FACTORY);
while (locationIterator.hasNext()) { while (locationIterator.hasNext()) {
Location location = locationIterator.next(); Location location = locationIterator.next();
@@ -509,8 +476,7 @@ public class TrackDataHub implements DataSourceListener {
} }
} else { } else {
// Also include the last point if the selected track is not recording. // Also include the last point if the selected track is not recording.
if (includeNextPoint || (localNumLoadedPoints % samplingFrequency == 0) if (includeNextPoint || (localNumLoadedPoints % samplingFrequency == 0) || (locationId == lastTrackPointId && !isSelectedTrackRecording())) {
|| (locationId == lastTrackPointId && !isSelectedTrackRecording())) {
includeNextPoint = false; includeNextPoint = false;
for (TrackDataListener trackDataListener : sampledInListeners) { for (TrackDataListener trackDataListener : sampledInListeners) {
trackDataListener.onSampledInTrackPoint(location); trackDataListener.onSampledInTrackPoint(location);
@@ -525,10 +491,6 @@ public class TrackDataHub implements DataSourceListener {
localNumLoadedPoints++; localNumLoadedPoints++;
localLastSeenLocationId = locationId; localLastSeenLocationId = locationId;
} }
} finally {
if (locationIterator != null) {
locationIterator.close();
}
} }
if (updateSamplingState) { if (updateSamplingState) {
@@ -555,7 +517,7 @@ public class TrackDataHub implements DataSourceListener {
* Creates a {@link DataSource}. * Creates a {@link DataSource}.
*/ */
@VisibleForTesting @VisibleForTesting
protected DataSource newDataSource() { private DataSource newDataSource() {
return new DataSource(context); return new DataSource(context);
} }
@@ -565,7 +527,7 @@ public class TrackDataHub implements DataSourceListener {
* @param runnable the runnable * @param runnable the runnable
*/ */
@VisibleForTesting @VisibleForTesting
protected void runInHandlerThread(Runnable runnable) { private void runInHandlerThread(Runnable runnable) {
if (handler == null) { if (handler == null) {
// Use a Throwable to ensure the stack trace is logged. // Use a Throwable to ensure the stack trace is logged.
Log.d(TAG, "handler is null.", new Throwable()); Log.d(TAG, "handler is null.", new Throwable());
@@ -57,10 +57,8 @@ public interface TrackDataListener {
void onSegmentSplit(Location location); void onSegmentSplit(Location location);
/** /**
* Called when finish sending new track points. This gets called after every * Called when finish sending new track points.
* batch of calls to {@link #onSampledInTrackPoint(Location)}, * This gets called after every batch of calls to {@link #onSampledInTrackPoint(Location)}, {@link #onSampledOutTrackPoint(Location)} and {@link #onSegmentSplit(Location)}.
* {@link #onSampledOutTrackPoint(Location)} and
* {@link #onSegmentSplit(Location)}.
*/ */
void onNewTrackPointsDone(); void onNewTrackPointsDone();
@@ -77,9 +75,8 @@ public interface TrackDataListener {
void onNewWaypoint(Waypoint waypoint); void onNewWaypoint(Waypoint waypoint);
/** /**
* Called when finish sending new waypoints. This gets called after every * Called when finish sending new waypoints.
* batch of calls to {@link #clearWaypoints()} and * This gets called after every batch of calls to {@link #clearWaypoints()} and {@link #onNewWaypoint(Waypoint)}.
* {@link #onNewWaypoint(Waypoint)}.
*/ */
void onNewWaypointsDone(); void onNewWaypointsDone();
@@ -26,24 +26,19 @@ import java.util.Map;
import java.util.Set; import java.util.Set;
/** /**
* Manages register/unregister {@link TrackDataListener} and keeping the state * Manages register/unregister {@link TrackDataListener} and keeping the state for each registered listener.
* for each registered listener.
* *
* @author Rodrigo Damazio * @author Rodrigo Damazio
*/ */
public class TrackDataManager { class TrackDataManager {
private static final String TAG = TrackDataManager.class.getSimpleName(); 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()) { for (TrackDataType trackDataType : TrackDataType.values()) {
typeToListenersMap.put(trackDataType, new LinkedHashSet<TrackDataListener>()); typeToListenersMap.put(trackDataType, new LinkedHashSet<TrackDataListener>());
} }
@@ -55,8 +50,7 @@ public class TrackDataManager {
* @param listener the listener * @param listener the listener
* @param trackDataTypes the track data types the listener is interested * @param trackDataTypes the track data types the listener is interested
*/ */
public void registerListener( void registerListener(TrackDataListener listener, EnumSet<TrackDataType> trackDataTypes) {
TrackDataListener listener, EnumSet<TrackDataType> trackDataTypes) {
if (listenerToTypesMap.containsKey(listener)) { if (listenerToTypesMap.containsKey(listener)) {
Log.w(TAG, "Tried to register a listener that is already registered. Ignore."); Log.w(TAG, "Tried to register a listener that is already registered. Ignore.");
return; return;
@@ -72,7 +66,7 @@ public class TrackDataManager {
* *
* @param listener the listener * @param listener the listener
*/ */
public void unregisterListener(TrackDataListener listener) { void unregisterListener(TrackDataListener listener) {
EnumSet<TrackDataType> removedTypes = listenerToTypesMap.remove(listener); EnumSet<TrackDataType> removedTypes = listenerToTypesMap.remove(listener);
if (removedTypes == null) { if (removedTypes == null) {
Log.w(TAG, "Tried to unregister a listener that is not registered. Ignore."); 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}. * Gets the number of {@link TrackDataListener}.
*/ */
public int getNumberOfListeners() { int getNumberOfListeners() {
return listenerToTypesMap.size(); return listenerToTypesMap.size();
} }
@@ -97,7 +91,7 @@ public class TrackDataManager {
* *
* @param listener the listener * @param listener the listener
*/ */
public EnumSet<TrackDataType> getTrackDataTypes(TrackDataListener listener) { EnumSet<TrackDataType> getTrackDataTypes(TrackDataListener listener) {
return listenerToTypesMap.get(listener); return listenerToTypesMap.get(listener);
} }
@@ -106,14 +100,14 @@ public class TrackDataManager {
* *
* @param type the type * @param type the type
*/ */
public Set<TrackDataListener> getListeners(TrackDataType type) { Set<TrackDataListener> getListeners(TrackDataType type) {
return typeToListenersMap.get(type); return typeToListenersMap.get(type);
} }
/** /**
* Gets all the registered {@link TrackDataType}. * Gets all the registered {@link TrackDataType}.
*/ */
public EnumSet<TrackDataType> getRegisteredTrackDataTypes() { EnumSet<TrackDataType> getRegisteredTrackDataTypes() {
EnumSet<TrackDataType> types = EnumSet.noneOf(TrackDataType.class); EnumSet<TrackDataType> types = EnumSet.noneOf(TrackDataType.class);
for (EnumSet<TrackDataType> value : listenerToTypesMap.values()) { for (EnumSet<TrackDataType> value : listenerToTypesMap.values()) {
types.addAll(value); types.addAll(value);
@@ -27,25 +27,9 @@ import android.provider.BaseColumns;
public interface TracksColumns extends BaseColumns { public interface TracksColumns extends BaseColumns {
String TABLE_NAME = "tracks"; String TABLE_NAME = "tracks";
/**
* Tracks provider uri.
*/
Uri CONTENT_URI = Uri.parse(ContentProviderUtils.CONTENT_BASE_URI + "/tracks"); Uri CONTENT_URI = Uri.parse(ContentProviderUtils.CONTENT_BASE_URI + "/tracks");
/**
* Track content type.
*/
String CONTENT_TYPE = "vnd.android.cursor.dir/vnd.de.dennisguse.track"; 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"; String CONTENT_ITEMTYPE = "vnd.android.cursor.item/vnd.de.dennisguse.track";
/**
* Tracks table default sort order.
*/
String DEFAULT_SORT_ORDER = "_id"; String DEFAULT_SORT_ORDER = "_id";
// Columns // Columns
@@ -18,8 +18,6 @@ package de.dennisguse.opentracks.content;
import android.location.Location; import android.location.Location;
import android.net.Uri; import android.net.Uri;
import android.os.Parcel;
import android.os.Parcelable;
import de.dennisguse.opentracks.stats.TripStatistics; import de.dennisguse.opentracks.stats.TripStatistics;
@@ -29,19 +27,8 @@ import de.dennisguse.opentracks.stats.TripStatistics;
* @author Leif Hendrik Wilden * @author Leif Hendrik Wilden
* @author Rodrigo Damazio * @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 long id = -1L;
private String name = ""; private String name = "";
private String description = ""; private String description = "";
@@ -60,9 +47,7 @@ public final class Waypoint implements Parcelable {
public Waypoint() { public Waypoint() {
} }
public Waypoint(String name, String description, String category, String icon, long trackId, 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) {
WaypointType type, double length, long duration, long startId, long stopId, Location location,
TripStatistics tripStatistics, String photoUrl) {
this.name = name; this.name = name;
this.description = description; this.description = description;
this.category = category; this.category = category;
@@ -78,60 +63,6 @@ public final class Waypoint implements Parcelable {
this.photoUrl = photoUrl; 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() { public long getId() {
return id; return id;
} }
@@ -228,6 +159,10 @@ public final class Waypoint implements Parcelable {
this.location = location; this.location = location;
} }
public boolean isTripStatistics() {
return tripStatistics != null;
}
public TripStatistics getTripStatistics() { public TripStatistics getTripStatistics() {
return tripStatistics; return tripStatistics;
} }
@@ -15,9 +15,6 @@
*/ */
package de.dennisguse.opentracks.content; package de.dennisguse.opentracks.content;
import android.os.Parcel;
import android.os.Parcelable;
import de.dennisguse.opentracks.content.Waypoint.WaypointType; import de.dennisguse.opentracks.content.Waypoint.WaypointType;
/** /**
@@ -25,18 +22,19 @@ import de.dennisguse.opentracks.content.Waypoint.WaypointType;
* *
* @author Sandor Dornbush * @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; private WaypointType type;
// true if this marker contains the track statistics // true if this marker contains the track statistics
private boolean isTrackStatistics; private boolean isTrackStatistics;
private String name; private String name;
private String category; private String category;
private String description; private String description;
@@ -47,8 +45,7 @@ public class WaypointCreationRequest implements Parcelable {
this(type, isTrackStatistics, null, null, null, null, null); this(type, isTrackStatistics, null, null, null, null, null);
} }
public WaypointCreationRequest(WaypointType type, boolean isTrackStatistics, String name, public WaypointCreationRequest(WaypointType type, boolean isTrackStatistics, String name, String category, String description, String iconUrl, String photoUrl) {
String category, String description, String iconUrl, String photoUrl) {
this.type = type; this.type = type;
this.isTrackStatistics = isTrackStatistics; this.isTrackStatistics = isTrackStatistics;
this.name = name; this.name = name;
@@ -58,22 +55,6 @@ public class WaypointCreationRequest implements Parcelable {
this.photoUrl = photoUrl; 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() { public WaypointType getType() {
return type; return type;
} }
@@ -101,23 +82,4 @@ public class WaypointCreationRequest implements Parcelable {
public String getPhotoUrl() { public String getPhotoUrl() {
return photoUrl; 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];
}
}
} }
@@ -2,7 +2,7 @@ package de.dennisguse.opentracks.content.sensor;
public final class SensorDataSet { public final class SensorDataSet {
public static final float DATA_UNAVAILABLE = Integer.MAX_VALUE; public static final float DATA_UNAVAILABLE = Float.NaN;
//TODO It might be necessary to consider: sensor if sensor was connected as well. //TODO It might be necessary to consider: sensor if sensor was connected as well.
public String sensorName; public String sensorName;
@@ -40,7 +40,7 @@ public final class SensorDataSet {
} }
public boolean hasHeartRate() { public boolean hasHeartRate() {
return heartRate != DATA_UNAVAILABLE; return !Float.isNaN(heartRate) && heartRate > 0;
} }
public float getHeartRate() { public float getHeartRate() {
@@ -48,7 +48,7 @@ public final class SensorDataSet {
} }
public boolean hasCadence() { public boolean hasCadence() {
return cadence != DATA_UNAVAILABLE; return !Float.isNaN(cadence);
} }
public float getCadence() { public float getCadence() {
@@ -56,7 +56,7 @@ public final class SensorDataSet {
} }
public boolean hasPower() { public boolean hasPower() {
return power != DATA_UNAVAILABLE; return !Float.isNaN(power);
} }
public float getPower() { public float getPower() {
@@ -77,7 +77,7 @@ public final class SensorDataSet {
} }
public boolean hasBatteryLevel() { public boolean hasBatteryLevel() {
return batteryLevel != DATA_UNAVAILABLE; return !Float.isNaN(batteryLevel);
} }
public float getBatteryLevel() { public float getBatteryLevel() {
@@ -8,6 +8,7 @@ public enum SensorState {
NONE, NONE,
CONNECTING, CONNECTING,
CONNECTED, CONNECTED,
DISCONNECTING,
DISCONNECTED; DISCONNECTED;
public static String getStateAsString(SensorState state, Context c) { public static String getStateAsString(SensorState state, Context c) {
@@ -18,6 +19,8 @@ public enum SensorState {
return c.getString(R.string.sensor_state_connecting); return c.getString(R.string.sensor_state_connecting);
case CONNECTED: case CONNECTED:
return c.getString(R.string.sensor_state_connected); return c.getString(R.string.sensor_state_connected);
case DISCONNECTING:
return c.getString(R.string.sensor_state_disconnected);
case DISCONNECTED: case DISCONNECTED:
return c.getString(R.string.sensor_state_disconnected); return c.getString(R.string.sensor_state_disconnected);
default: default:
@@ -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();
}
@@ -29,7 +29,11 @@ import androidx.annotation.VisibleForTesting;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity; import androidx.fragment.app.FragmentActivity;
import java.util.ArrayList;
import java.util.EnumSet;
import de.dennisguse.opentracks.ChartView; import de.dennisguse.opentracks.ChartView;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.TrackDetailActivity; import de.dennisguse.opentracks.TrackDetailActivity;
import de.dennisguse.opentracks.content.SensorDataSetLocation; import de.dennisguse.opentracks.content.SensorDataSetLocation;
import de.dennisguse.opentracks.content.Track; import de.dennisguse.opentracks.content.Track;
@@ -43,10 +47,6 @@ import de.dennisguse.opentracks.stats.TripStatisticsUpdater;
import de.dennisguse.opentracks.util.LocationUtils; import de.dennisguse.opentracks.util.LocationUtils;
import de.dennisguse.opentracks.util.PreferencesUtils; import de.dennisguse.opentracks.util.PreferencesUtils;
import de.dennisguse.opentracks.util.UnitConversions; import de.dennisguse.opentracks.util.UnitConversions;
import de.dennisguse.opentracks.R;
import java.util.ArrayList;
import java.util.EnumSet;
/** /**
* A fragment to display track chart to the user. * A fragment to display track chart to the user.
@@ -79,8 +79,7 @@ public class ChartFragment extends Fragment implements TrackDataListener {
private ZoomControls zoomControls; private ZoomControls zoomControls;
/** /**
* A runnable that will enable/disable zoom controls and orange pointer as * A runnable that will enable/disable zoom controls and orange pointer as appropriate and redraw.
* appropriate and redraw.
*/ */
private final Runnable updateChart = new Runnable() { private final Runnable updateChart = new Runnable() {
@Override @Override
@@ -101,15 +100,13 @@ public class ChartFragment extends Fragment implements TrackDataListener {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
/* /*
* Create a chartView here to store data thus won't need to reload all the * Create a chartView here to store data thus won't need to reload all the data on every onStart or onResume.
* data on every onStart or onResume.
*/ */
chartView = new ChartView(getActivity()); chartView = new ChartView(getActivity());
} }
@Override @Override
public View onCreateView( public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.chart, container, false); View view = inflater.inflate(R.layout.chart, container, false);
zoomControls = view.findViewById(R.id.chart_zoom_controls); zoomControls = view.findViewById(R.id.chart_zoom_controls);
zoomControls.setOnZoomInClickListener(new View.OnClickListener() { zoomControls.setOnZoomInClickListener(new View.OnClickListener() {
@@ -131,8 +128,7 @@ public class ChartFragment extends Fragment implements TrackDataListener {
public void onStart() { public void onStart() {
super.onStart(); super.onStart();
ViewGroup layout = getActivity().findViewById(R.id.chart_view_layout); ViewGroup layout = getActivity().findViewById(R.id.chart_view_layout);
LayoutParams layoutParams = new LayoutParams( LayoutParams layoutParams = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
layout.addView(chartView, layoutParams); layout.addView(chartView, layoutParams);
} }
@@ -350,14 +346,14 @@ public class ChartFragment extends Fragment implements TrackDataListener {
chartShow[index] = value; chartShow[index] = value;
chartView.setChartValueSeriesEnabled(index, value); chartView.setChartValueSeriesEnabled(index, value);
return true; return true;
} else {
return false;
} }
return false;
} }
/** /**
* Resumes the trackDataHub. Needs to be synchronized because trackDataHub can * Resumes the trackDataHub.
* be accessed by multiple threads. * Needs to be synchronized because trackDataHub can be accessed by multiple threads.
*/ */
private synchronized void resumeTrackDataHub() { private synchronized void resumeTrackDataHub() {
trackDataHub = ((TrackDetailActivity) getActivity()).getTrackDataHub(); trackDataHub = ((TrackDetailActivity) getActivity()).getTrackDataHub();
@@ -367,8 +363,8 @@ public class ChartFragment extends Fragment implements TrackDataListener {
} }
/** /**
* Pauses the trackDataHub. Needs to be synchronized because trackDataHub can * Pauses the trackDataHub.
* be accessed by multiple threads. * Needs to be synchronized because trackDataHub can be accessed by multiple threads.
*/ */
private synchronized void pauseTrackDataHub() { private synchronized void pauseTrackDataHub() {
trackDataHub.unregisterTrackDataListener(this); trackDataHub.unregisterTrackDataListener(this);
@@ -376,16 +372,16 @@ public class ChartFragment extends Fragment implements TrackDataListener {
} }
/** /**
* Returns true if the selected track is recording. Needs to be synchronized * Returns true if the selected track is recording.
* because trackDataHub can be accessed by multiple threads. * Needs to be synchronized because trackDataHub can be accessed by multiple threads.
*/ */
private synchronized boolean isSelectedTrackRecording() { private synchronized boolean isSelectedTrackRecording() {
return trackDataHub != null && trackDataHub.isSelectedTrackRecording(); return trackDataHub != null && trackDataHub.isSelectedTrackRecording();
} }
/** /**
* Reloads the trackDataHub. Needs to be synchronized because trackDataHub can * Reloads the trackDataHub.
* be accessed by multiple threads. * Needs to be synchronized because trackDataHub can be accessed by multiple threads.
*/ */
private synchronized void reloadTrackDataHub() { private synchronized void reloadTrackDataHub() {
if (trackDataHub != null) { if (trackDataHub != null) {
@@ -393,18 +389,12 @@ public class ChartFragment extends Fragment implements TrackDataListener {
} }
} }
/**
* To zoom in.
*/
private void zoomIn() { private void zoomIn() {
chartView.zoomIn(); chartView.zoomIn();
zoomControls.setIsZoomInEnabled(chartView.canZoomIn()); zoomControls.setIsZoomInEnabled(chartView.canZoomIn());
zoomControls.setIsZoomOutEnabled(chartView.canZoomOut()); zoomControls.setIsZoomOutEnabled(chartView.canZoomOut());
} }
/**
* To zoom out.
*/
private void zoomOut() { private void zoomOut() {
chartView.zoomOut(); chartView.zoomOut();
zoomControls.setIsZoomInEnabled(chartView.canZoomIn()); zoomControls.setIsZoomInEnabled(chartView.canZoomIn());
@@ -442,9 +432,6 @@ public class ChartFragment extends Fragment implements TrackDataListener {
double elevation = Double.NaN; double elevation = Double.NaN;
double speed = Double.NaN; double speed = Double.NaN;
double pace = Double.NaN; double pace = Double.NaN;
double heartRate = Double.NaN;
double cadence = Double.NaN;
double power = Double.NaN;
if (tripStatisticsUpdater != null) { if (tripStatisticsUpdater != null) {
tripStatisticsUpdater.addLocation(location, recordingDistanceInterval); tripStatisticsUpdater.addLocation(location, recordingDistanceInterval);
@@ -470,6 +457,10 @@ public class ChartFragment extends Fragment implements TrackDataListener {
} }
pace = speed == 0 ? 0.0 : 60.0 / speed; pace = speed == 0 ? 0.0 : 60.0 / speed;
} }
double heartRate = Double.NaN;
double cadence = Double.NaN;
double power = Double.NaN;
if (location instanceof SensorDataSetLocation && ((SensorDataSetLocation) location).getSensorDataSet() != null) { if (location instanceof SensorDataSetLocation && ((SensorDataSetLocation) location).getSensorDataSet() != null) {
SensorDataSet sensorDataSet = ((SensorDataSetLocation) location).getSensorDataSet(); SensorDataSet sensorDataSet = ((SensorDataSetLocation) location).getSensorDataSet();
if (sensorDataSet.hasHeartRate()) { if (sensorDataSet.hasHeartRate()) {
@@ -483,6 +474,7 @@ public class ChartFragment extends Fragment implements TrackDataListener {
} }
} }
//TODO: Is related to ChartView.ELEVATION_SERIES etc.
if (data != null) { if (data != null) {
data[0] = timeOrDistance; data[0] = timeOrDistance;
data[1] = elevation; data[1] = elevation;
@@ -1,66 +1,64 @@
/*
* 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; package de.dennisguse.opentracks.fragments;
import android.app.Activity; import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog; import android.app.Dialog;
import android.content.Context; import android.content.Context;
import android.content.DialogInterface;
import android.graphics.BitmapFactory;
import android.graphics.BitmapFactory.Options;
import android.os.Bundle; import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView; import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.GridView; import android.widget.GridView;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.fragment.app.DialogFragment; import androidx.fragment.app.DialogFragment;
import androidx.fragment.app.FragmentManager;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import de.dennisguse.opentracks.R; import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.util.DialogUtils;
import de.dennisguse.opentracks.util.TrackIconUtils; import de.dennisguse.opentracks.util.TrackIconUtils;
/** /**
* A DialogFragment to choose an activity type. * A DialogFragment to choose an activity type.
*
* @author apoorvn
*/ */
public class ChooseActivityTypeDialogFragment extends DialogFragment { public class ChooseActivityTypeDialogFragment extends DialogFragment implements AdapterView.OnItemClickListener {
public static final String CHOOSE_ACTIVITY_TYPE_DIALOG_TAG = "chooseActivityType"; private static final String CHOOSE_ACTIVITY_TYPE_DIALOG_TAG = "chooseActivityType";
private static final String KEY_CATEGORY = "category";
private ChooseActivityTypeCaller caller;
public static ChooseActivityTypeDialogFragment newInstance(String category) { public static void showDialog(FragmentManager fragmentManager, String preselectedCategory) {
Bundle bundle = new Bundle(); new ChooseActivityTypeDialogFragment(preselectedCategory).show(fragmentManager, ChooseActivityTypeDialogFragment.CHOOSE_ACTIVITY_TYPE_DIALOG_TAG);
bundle.putString(KEY_CATEGORY, category);
ChooseActivityTypeDialogFragment fragment = new ChooseActivityTypeDialogFragment();
fragment.setArguments(bundle);
return fragment;
} }
public static Dialog getDialog(final Activity activity, final String category, final ChooseActivityTypeCaller caller) { private static int getPosition(Context context, String category) {
View view = activity.getLayoutInflater().inflate(R.layout.choose_activity_type, null); if (category == null) {
return -1;
}
String iconValue = TrackIconUtils.getIconValue(context, category);
return TrackIconUtils.getAllIconValues().indexOf(iconValue);
}
private String preselectedCategory;
private ChooseActivityTypeCaller chooseActivityTypeCaller;
private ChooseActivityTypeDialogFragment(String preselectedCategory) {
this.preselectedCategory = preselectedCategory;
}
@NonNull
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
Dialog dialog = super.onCreateDialog(savedInstanceState);
dialog.setTitle(R.string.track_edit_activity_type_hint);
return dialog;
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.choose_activity_type, container);
GridView gridView = view.findViewById(R.id.choose_activity_type_grid_view); GridView gridView = view.findViewById(R.id.choose_activity_type_grid_view);
List<Integer> imageIds = new ArrayList<>(); List<Integer> imageIds = new ArrayList<>();
@@ -68,83 +66,53 @@ public class ChooseActivityTypeDialogFragment extends DialogFragment {
imageIds.add(TrackIconUtils.getIconDrawable(iconValue)); imageIds.add(TrackIconUtils.getIconDrawable(iconValue));
} }
Options options = new BitmapFactory.Options(); final ChooseActivityTypeImageAdapter imageAdapter = new ChooseActivityTypeImageAdapter(imageIds);
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(activity.getResources(), R.drawable.ic_track_airplane, options);
int padding = 32;
int width = options.outWidth + 2 * padding;
int height = options.outHeight + 2 * padding;
gridView.setColumnWidth(width);
final ChooseActivityTypeImageAdapter imageAdapter = new ChooseActivityTypeImageAdapter(activity, imageIds, width, height, padding);
gridView.setAdapter(imageAdapter); gridView.setAdapter(imageAdapter);
final AlertDialog alertDialog = new AlertDialog.Builder(activity).setTitle(R.string.track_edit_activity_type_hint).setView(view).create(); int position = getPosition(getContext(), preselectedCategory);
alertDialog.setOnShowListener(new DialogInterface.OnShowListener() {
@Override
public void onShow(DialogInterface dialog) {
int position = getPosition(activity, category);
if (position != -1) { if (position != -1) {
imageAdapter.setSelected(position); imageAdapter.setSelected(position);
imageAdapter.notifyDataSetChanged(); imageAdapter.notifyDataSetChanged();
} }
DialogUtils.setDialogTitleDivider(activity, alertDialog);
}
});
gridView.setOnItemClickListener(new OnItemClickListener() { gridView.setOnItemClickListener(this);
return view;
}
/**
* TODO: Only used by obsolete Androidx preferences. Remove when onAttach(Context) is called.
*/
@Override @Override
public void onItemClick(AdapterView<?> parent, View v, int position, long id) { public void onAttach(@NonNull Activity activity) {
caller.onChooseActivityTypeDone(TrackIconUtils.getAllIconValues().get(position)); super.onAttach(activity);
alertDialog.dismiss(); try {
chooseActivityTypeCaller = (ChooseActivityTypeCaller) activity;
} catch (ClassCastException e) {
throw new ClassCastException(activity + " must implement " + ChooseActivityTypeCaller.class.getSimpleName());
} }
});
return alertDialog;
} }
private static int getPosition(Activity activity, String category) { public void onAttach(@NonNull Context context) {
if (category == null) {
return -1;
}
String iconValue = TrackIconUtils.getIconValue(activity, category);
if (iconValue.equals("")) {
return -1;
}
List<String> iconValues = TrackIconUtils.getAllIconValues();
for (int i = 0; i < iconValues.size(); i++) {
if (iconValues.get(i).equals(iconValue)) {
return i;
}
}
return -1;
}
@Override
public void onAttach(Context context) {
super.onAttach(context); super.onAttach(context);
try { try {
caller = (ChooseActivityTypeCaller) context; chooseActivityTypeCaller = (ChooseActivityTypeCaller) context;
} catch (ClassCastException e) { } catch (ClassCastException e) {
throw new ClassCastException(context + " must implement " + ChooseActivityTypeCaller.class.getSimpleName()); throw new ClassCastException(context + " must implement " + ChooseActivityTypeCaller.class.getSimpleName());
} }
} }
@NonNull
@Override @Override
public Dialog onCreateDialog(Bundle savedInstanceState) { public void onItemClick(AdapterView<?> adapterView, View view, int position, long id) {
return getDialog(getActivity(), getArguments().getString(KEY_CATEGORY), caller); chooseActivityTypeCaller.onChooseActivityTypeDone(TrackIconUtils.getAllIconValues().get(position));
dismiss();
} }
/** /**
* Interface for caller of this dialog fragment. * Interface for chooseActivityTypeCaller of this dialog fragment.
*
* @author apoorvn
*/ */
public interface ChooseActivityTypeCaller { public interface ChooseActivityTypeCaller {
/**
* Called when choose activity type is done.
*/
void onChooseActivityTypeDone(String iconValue); void onChooseActivityTypeDone(String iconValue);
} }
} }
@@ -16,7 +16,6 @@
package de.dennisguse.opentracks.fragments; package de.dennisguse.opentracks.fragments;
import android.content.Context;
import android.graphics.Color; import android.graphics.Color;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
@@ -26,6 +25,7 @@ import android.widget.ImageView;
import java.util.List; import java.util.List;
import de.dennisguse.opentracks.R; import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.util.ResourceUtils;
/** /**
* Image adapter for choosing an activity type. * Image adapter for choosing an activity type.
@@ -34,19 +34,11 @@ import de.dennisguse.opentracks.R;
*/ */
public class ChooseActivityTypeImageAdapter extends BaseAdapter { public class ChooseActivityTypeImageAdapter extends BaseAdapter {
private final Context context;
private final List<Integer> imageIds; private final List<Integer> imageIds;
private final int width;
private final int height;
private final int padding;
private int selected = -1; private int selected = -1;
public ChooseActivityTypeImageAdapter(Context context, List<Integer> imageIds, int width, int height, int padding) { ChooseActivityTypeImageAdapter(List<Integer> imageIds) {
this.context = context;
this.imageIds = imageIds; this.imageIds = imageIds;
this.width = width;
this.height = height;
this.padding = padding;
} }
@Override @Override
@@ -76,19 +68,23 @@ public class ChooseActivityTypeImageAdapter extends BaseAdapter {
public View getView(int position, View convertView, ViewGroup parent) { public View getView(int position, View convertView, ViewGroup parent) {
ImageView imageView; ImageView imageView;
if (convertView == null) { if (convertView == null) {
imageView = new ImageView(context); imageView = new ImageView(parent.getContext());
imageView.setScaleType(ImageView.ScaleType.FIT_CENTER);
imageView.setAdjustViewBounds(true);
int padding = ResourceUtils.dpToPx(parent.getContext(), 8);
imageView.setPaddingRelative(padding, padding, padding, padding);
} else { } else {
imageView = (ImageView) convertView; imageView = (ImageView) convertView;
} }
if (position == selected) { if (position == selected) {
imageView.setBackgroundResource(R.drawable.list_selector_background_transition_holo_dark); imageView.setBackgroundResource(R.drawable.list_selector_background_transition_holo_dark);
} else { } else {
imageView.setBackgroundColor(Color.TRANSPARENT); imageView.setBackgroundColor(Color.TRANSPARENT);
} }
imageView.setImageResource(imageIds.get(position)); imageView.setImageResource(imageIds.get(position));
imageView.setMinimumHeight(height);
imageView.setMinimumWidth(width);
imageView.setPadding(padding, padding, padding, padding);
return imageView; return imageView;
} }
} }
@@ -21,37 +21,39 @@ import android.content.Context;
import android.content.DialogInterface; import android.content.DialogInterface;
import android.os.Bundle; import android.os.Bundle;
import de.dennisguse.opentracks.util.DialogUtils; import androidx.annotation.NonNull;
import androidx.fragment.app.DialogFragment;
import androidx.fragment.app.FragmentManager;
import de.dennisguse.opentracks.R; import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.util.DialogUtils;
/** /**
* A DialogFragment to confirm delete. * A DialogFragment to confirm delete.
* *
* @author Jimmy Shih * @author Jimmy Shih
*/ */
public class ConfirmDeleteDialogFragment extends AbstractDialogFragment { public class ConfirmDeleteDialogFragment extends DialogFragment {
public static final String CONFIRM_DELETE_DIALOG_TAG = "confirmDeleteDialog"; private static final String CONFIRM_DELETE_DIALOG_TAG = "confirmDeleteDialog";
private static final String KEY_TRACK_IDS = "trackIds";
private ConfirmDeleteCaller caller; private ConfirmDeleteCaller caller;
private long[] trackIds;
public ConfirmDeleteDialogFragment(long[] trackIds) {
this.trackIds = trackIds;
}
/** /**
* Create a new instance. * Create a new instance.
* *
* @param trackIds list of track ids to delete. To delete all, set to size 1 * @param trackIds list of track ids to delete. To delete all, set to size 1 with trackIds[0] == -1L
* with trackIds[0] == -1L
*/ */
public static ConfirmDeleteDialogFragment newInstance(long[] trackIds) { public static void showDialog(FragmentManager fragmentManager, long[] trackIds) {
Bundle bundle = new Bundle(); new ConfirmDeleteDialogFragment(trackIds).show(fragmentManager, CONFIRM_DELETE_DIALOG_TAG);
bundle.putLongArray(KEY_TRACK_IDS, trackIds);
ConfirmDeleteDialogFragment deleteTrackDialogFragment = new ConfirmDeleteDialogFragment();
deleteTrackDialogFragment.setArguments(bundle);
return deleteTrackDialogFragment;
} }
@Override @Override
public void onAttach(Context context) { public void onAttach(@NonNull Context context) {
super.onAttach(context); super.onAttach(context);
try { try {
caller = (ConfirmDeleteCaller) context; caller = (ConfirmDeleteCaller) context;
@@ -61,14 +63,12 @@ public class ConfirmDeleteDialogFragment extends AbstractDialogFragment {
} }
@Override @Override
protected Dialog createDialog() { @NonNull
final long[] trackIds = getArguments().getLongArray(KEY_TRACK_IDS); public Dialog onCreateDialog(Bundle savedInstanceState) {
int titleId; int titleId;
int messageId; int messageId;
titleId = trackIds.length > 1 ? R.string.generic_delete_selected_confirm_title titleId = trackIds.length > 1 ? R.string.generic_delete_selected_confirm_title : R.string.track_delete_one_confirm_title;
: R.string.track_delete_one_confirm_title; messageId = trackIds.length > 1 ? R.string.track_delete_multiple_confirm_message : R.string.track_delete_one_confirm_message;
messageId = trackIds.length > 1 ? R.string.track_delete_multiple_confirm_message
: R.string.track_delete_one_confirm_message;
return DialogUtils.createConfirmationDialog( return DialogUtils.createConfirmationDialog(
getActivity(), titleId, getString(messageId), new DialogInterface.OnClickListener() { getActivity(), titleId, getString(messageId), new DialogInterface.OnClickListener() {
@Override @Override

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