Compare commits

...

88 Commits

Author SHA1 Message Date
Dennis Guse bbb2f874ee Release v3.3.1 2020-01-03 18:11:57 +01:00
Dennis Guse 9af3759b07 Bugfix: app name (release) not shown.
Introduced in 6cc7271a41.
2020-01-03 17:54:15 +01:00
Dennis Guse 1f6fe6bf0c Android 10: on uninstall ask the user if the app data should be kept. 2020-01-03 17:50:07 +01:00
Dennis Guse 7b291e85d7 Renamed DeleteActivity to TrackDeleteActivity. 2020-01-03 17:04:51 +01:00
Dennis Guse 0f72066bc0 Add IntentChooser for dashboards. 2020-01-02 19:56:24 +01:00
Dennis Guse 70e0e7b216 Add README for data API. 2020-01-02 19:54:19 +01:00
Peter Storch 7e50bb1b6d Merge pull request #114 from pstorch/79_osm_plugin
integrate dashboard plugin
2020-01-02 17:56:07 +01:00
Peter Storch a3eb107068 integrate dashboard plugin mechanism
To be able to integrate the OSMDashboard https://github.com/OpenTracksApp/OSMDashboard,
which addresses #79. Also other Dashboards will be possible.
2020-01-02 17:45:01 +01:00
Dennis Guse 6133ef121a Release v3.3.0 2020-01-01 14:46:28 +01:00
Dennis Guse c6d24e9534 Pictures: removed MediaScanner. 2020-01-01 14:44:23 +01:00
Dennis Guse 441d3f721f Debug version: use a black logo. 2020-01-01 14:39:16 +01:00
Dennis Guse 3c212e4bcc Bugfix: for taking pictures; provide access to external/pictures. 2020-01-01 14:28:58 +01:00
Dennis Guse 50eb71bc1f Cleanup. 2020-01-01 14:16:02 +01:00
Dennis Guse 0806821b5f Fixed import/export message to show Storage Access Framework selected path. 2019-12-29 23:33:10 +01:00
Dennis Guse 36d0f17953 Removed french Congo translation (identical to values-fr). 2019-12-29 21:57:39 +01:00
Dennis Guse 700adb7972 Export dialog: removed "from folder" text. 2019-12-29 21:56:48 +01:00
Dennis Guse a156369f8b Import dialog: removed "from folder" text. 2019-12-29 21:32:01 +01:00
Dennis Guse c0f6dc511b Removed values-ar-rXB/strings.xml as it was for testing only. 2019-12-29 21:28:36 +01:00
Dennis Guse 63e1461eba Removed values-en-rXA/strings.xml as it was for testing only. 2019-12-29 20:59:30 +01:00
Dennis Guse 804e5f10c8 Removed values-bn/strings.xml as translations were invalid. 2019-12-29 20:53:37 +01:00
Dennis Guse 6cc7271a41 Build: debug version uses different icon and appLabel. 2019-12-29 20:46:46 +01:00
Dennis Guse f66194189b Removed translation description from values/strings.xml 2019-12-29 20:15:37 +01:00
Dennis Guse 5d97d6d492 Use Android 7.0 localization: merge latin american and present en-rGB as default for en. 2019-12-29 19:13:26 +01:00
Dennis Guse f3a308e669 BREAKING CHANGE: pictures are now stored in getExternalFilesDir(); no migration takes place! 2019-12-29 15:43:06 +01:00
Dennis Guse a230504925 Export & import via Storage Access Framework. 2019-12-29 15:41:28 +01:00
Dennis Guse 90e19e282c Upgraded tests to Junit4. 2019-12-29 15:31:53 +01:00
Dennis Guse b585bfe44c Moved to Github organization: OpenTracksApp. 2019-12-27 23:54:29 +01:00
Dennis Guse 475e04eeb4 Add PlantUML model for data structures. 2019-12-27 18:07:29 +01:00
Dennis Guse f89b2b0efb Removed TripStatists min/max latitude/longitude.
This also removed: search does not account for closer tracks (to current position).
2019-12-27 18:01:11 +01:00
Dennis Guse b94f3c176b Include unit in variable names. 2019-12-27 18:01:10 +01:00
Dennis Guse d1d729d287 Removed unused ContentProvider.getFirstTrackPointId(). 2019-12-27 18:01:07 +01:00
Dennis Guse e23bbcc405 Removed unnecessary Track.startId and Track.stopId. 2019-12-27 18:01:05 +01:00
Dennis Guse 4129985b0f Renamed SensorDataSetLocation to TrackPoint (similar to SQLite table). 2019-12-27 18:00:51 +01:00
Dennis Guse 5cb7b623d9 Removed TripStatistics from Waypoints. 2019-12-26 21:15:42 +01:00
Dennis Guse 3c52efadd6 Removed unused Waypoint.TripStatistics. 2019-12-26 21:15:41 +01:00
Dennis Guse 73b4977051 Removed unused Waypoint.startId and Waypoint.stopId. 2019-12-26 21:15:41 +01:00
Dennis Guse 422c2c02ce Assume GPS provides always time (removed workaround). 2019-12-26 21:15:41 +01:00
Dennis Guse d692329975 Settings: hide advanced recording settings.
And use switches rather than checkboxes.
2019-12-25 23:22:20 +01:00
Dennis Guse 931574bc11 Release v3.2.11 2019-12-25 10:19:22 +01:00
Dennis Guse caddbd8501 MarkerDetailActivity: removed leftover statistic functionality. 2019-12-23 21:44:22 +01:00
Dennis Guse 8ece2c8612 Removed WaypointCreationRequest. 2019-12-23 21:39:37 +01:00
Dennis Guse 3a219321d1 Removed country-specific translations that are identical. 2019-12-23 07:43:05 +01:00
Dennis Guse 35cb924a17 StatsFragment: show elevation without decimal digits. 2019-12-22 20:39:43 +01:00
Dennis Guse b67e5bbd20 StatsFragment: left align current elevation. 2019-12-22 20:33:23 +01:00
Dennis Guse 1db275230e Bugfix: crash on opening heart rate sensor dialog (settings). 2019-12-22 19:09:01 +01:00
Dennis Guse 5ac72092fb Test: export and import tracks via KML and GPX; now includes waypoints.
TripStatistics is not tested.
2019-12-22 18:59:23 +01:00
Dennis Guse bdd9ded1e8 Test: export and import tracks via KML and GPX.
KML now exports track icon (name; not the url).
2019-12-22 18:59:23 +01:00
Dennis Guse 759aeb6ed6 Removed DescriptionGenerator interface. 2019-12-22 18:59:23 +01:00
Dennis Guse 12023d51ae Move data classes into separate package. 2019-12-22 18:59:23 +01:00
Dennis Guse c426785b90 Cleanup. 2019-12-22 18:59:23 +01:00
Dennis Guse 09d960c7a1 First waypoint of a track does not store the statistics of the track. 2019-12-22 18:59:23 +01:00
Dennis Guse f37fe4a503 Removed WaypointType. 2019-12-22 18:59:23 +01:00
Dennis Guse 013e56bef0 Replaced TabHost by Material design TabsLayout. 2019-12-22 18:59:23 +01:00
Dennis Guse d6835ba766 TrackRecordingService does not save the context. 2019-12-22 18:59:23 +01:00
Dennis Guse d7890cbc81 Shortened fr-FR short description.
Thanks to Edern.
2019-12-17 20:35:41 +01:00
Dennis Guse 2b6886c534 Update release procedure. 2019-12-16 22:41:16 +01:00
Dennis Guse e77590a125 Release v3.2.10 2019-12-15 22:11:19 +01:00
Dennis Guse ca330b2d80 Cleanup. 2019-12-15 21:28:03 +01:00
Dennis Guse 20f1524701 Cleanup. 2019-12-15 17:09:44 +01:00
Dennis Guse 96b2a15643 Bugfix: import and export on Android 10 requires requesting (privacy-unfriendly) legacy behavior. 2019-12-15 16:50:13 +01:00
Dennis Guse ad59392e02 Fix travisCI. 2019-12-14 23:58:43 +01:00
Dennis Guse 29f92b887c Fixed TrackRecordingService.insertWaypoint() to allow deprecated behavior. 2019-12-14 23:55:04 +01:00
Dennis Guse 46328211c2 Added promotion sticker. 2019-12-14 23:55:04 +01:00
Dennis Guse 3760130d1f Github: add donation. 2019-12-14 23:42:07 +01:00
Dennis Guse 1094827fac TrackListActivity: fixed landscape layout. 2019-12-11 23:05:31 +01:00
Dennis Guse 36fcb44af5 TripRecordingService: removed unused trip statistics marker updater. 2019-12-11 23:05:31 +01:00
Dennis Guse a683fbac4b Device reboot: do not restart a not successfully ended track recording. 2019-12-11 23:05:31 +01:00
Dennis Guse 64ad9019d1 ChartView: is not configurable anymore. 2019-12-11 23:05:31 +01:00
Dennis Guse 1e80d5c8a8 ChartView: draw waypoints (again).
Functionality was (accidentally) removed in 5a264c5c53
2019-12-11 23:05:31 +01:00
Matthias Lisin ac765b1ee8 Revert duration hint to MAY_DUCK.
Pass OnAudioFocusChangeListener to abandonAudioFocus
2019-12-10 12:58:57 +01:00
Dennis Guse 850c4f0a9a TravisCI: disabled non-functioning installation of Android emulator. 2019-12-10 06:53:22 +01:00
Dennis Guse 4c32999752 Add Google Play link. 2019-12-10 06:50:58 +01:00
Dennis Guse 76823b167a TrackListActivity: removed delete all from action menu. 2019-12-08 13:12:15 +01:00
Dennis Guse 9994cfd260 Bugfix: NPE. 2019-12-07 13:37:06 +01:00
Dennis Guse 1750236474 Added automation for Google Play publication (using fastlane). 2019-12-06 22:26:40 +01:00
Dennis Guse fed6e31f9e Cleanup. 2019-12-06 08:00:20 +01:00
Dennis Guse 9bafc7d4a1 Release v3.2.9 2019-12-04 19:57:27 +01:00
Dennis Guse be58b48d20 Bugfix: StatsFragment (onResume and track was recording) overwrote in the total time UI until next UI update. 2019-12-03 22:21:45 +01:00
Dennis Guse 303abac980 Removed READ_PHONE_STATE permission and use AudioManager.MODE_IN_CALL instead. 2019-12-03 22:01:01 +01:00
Dennis Guse d1a459efc2 Get audio focus onStart. 2019-12-03 20:28:22 +01:00
Dennis Guse 96069e6f09 Cleanup. 2019-12-03 20:24:57 +01:00
Dennis Guse 301a721329 Settings: localize no heart rate sensor selected. 2019-12-03 19:53:14 +01:00
Dennis Guse e8c6801a32 Refactor StatsFragment. 2019-12-03 19:19:19 +01:00
Dennis Guse 355f3ef614 Added "screen always on"-feature to TrackDetailActivity.
And the "show on lockscreen"-feature is only used while recording.
2019-12-03 16:57:56 +01:00
Dennis Guse eaa6083586 Bugfix: Heart rate was not shown in stats. 2019-12-03 07:31:29 +01:00
Dennis Guse a0c5d02c51 Fixed and removed device features (AndroidManifest). 2019-12-03 07:31:29 +01:00
Dennis Guse 76f9f5fc82 MarkerDetailFragment now shows the blue pin as default image. 2019-12-03 07:31:29 +01:00
Dennis Guse 8e2598e93b Removed split markers. 2019-12-03 07:31:24 +01:00
225 changed files with 4509 additions and 19937 deletions
+4
View File
@@ -0,0 +1,4 @@
# These are supported funding model platforms
liberapay: dennis.guse
custom: https://play.google.com/store/apps/details?id=de.dennisguse.opentracks.playstore
+6 -2
View File
@@ -73,8 +73,12 @@ captures/
.idea/workspace.xml
# Keystore files
# Uncomment the following line if you do not want to check your keystore files in.
#*.jks
*.jks
# External native build folder generated in Android Studio 2.2 and later
.externalNativeBuild
# Fastlane
Appfile
releasePlayStore/*
Gemfile.lock
+11 -10
View File
@@ -19,25 +19,26 @@ android:
# installing tools to start, then use `sdkmanager` below to get the rest
- tools
- build-tools-28.0.3
- android-28
- android-29
# 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 &
#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:
- ./gradlew clean build
- ./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/
-16
View File
@@ -1,16 +0,0 @@
# OpenTrack: Developer information
## Create releases for FDroid
1. Get next _version code_ (master branch): `git rev-list HEAD --count master`
2. Decide on _version name_ (semantic versioning)
3. Manually update _version code_ and _version name_ in `AndroidManifest.xml` (used by FDroid)
4. Create changelog (`_version code_.txt`)
5. Create commit with all changes
6. Tag newly create commit with _version name_ (e.g., v3.2.1)
7. Push commits and tags to public repository
8. Add changelog to tag (Github releases)
## Translations (localization)
Translations of the OpenTracks are handled in Transifex: [https://www.transifex.com/opentracks](https://www.transifex.com/opentracks/).
+3
View File
@@ -0,0 +1,3 @@
source "https://rubygems.org"
gem "fastlane"
+29 -8
View File
@@ -2,17 +2,38 @@
_OpenTracks_ is a sport tracking application that completely respects your privacy.
<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>
<table>
<tr>
<th>Free</th>
<th>Paid</th>
<th>Donations</th>
</tr>
<tr>
<td>
<a href="https://f-droid.org/packages/de.dennisguse.opentracks">
<img alt="Get it on F-Droid" src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png" height="60" align="middle">
</a>
</td>
<td>
<a href="https://play.google.com/store/apps/details?id=de.dennisguse.opentracks.playstore">
<img alt="Get it on Google Play" src="https://play.google.com/intl/en_us/badges/static/images/badges/en_badge_web_generic.png" height="60" align="middle">
</a>
</td>
<td>
<a href="https://liberapay.com/dennis.guse/donate">
<img alt="Donate using Liberapay" src="https://liberapay.com/assets/widgets/donate.svg" height="45" align="middle">
</a>
</td>
</tr>
</table>
## Features:
* __Tracking:__ track your sport and outdoor activities
* __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 KMZ (incl. photos), KML or GPX
* __No Internet access:__ Internet is not used
* __Voice annoucements__
* __Voice announcements__
* __No advertising__
* __Privacy:__
* _does not contain any in-app analytics_
@@ -26,9 +47,9 @@ _Please note:_
_Tracks_ does not contain a _map_ (as this would required Internet access).
To show your tracks on a map, a 3rd party application (e.g., [OsmAnd](https://play.google.com/store/apps/details?id=net.osmand) or [MAPS.ME](https://play.google.com/store/apps/details?id=com.mapswithme.maps.pro)) needs to be installed.
<a href="https://liberapay.com/dennis.guse/donate">
<img alt="Donate using Liberapay" src="https://liberapay.com/assets/widgets/donate.svg" height="45">
</a>
## Custom Dashboards
As of v3.3.1 OpenTracks enables to use custom dashboards.
The reference implementation is [OSMDashboard](https://github.com/OpenTracksApp/OSMDashboard), which presents an OpenStreetMap map (showing the current track, incl. updates).
## Screenshots
<div>
+30
View File
@@ -0,0 +1,30 @@
# OpenTracks Data API
## Reason
OpenTracks by itself only provides basic features for data visualization and also lacks functionality for analytics.
By itself OpenTracks provides a basic dashboard that can show summary of a track (while recording as well as afterwards).
Although this might be (mostly) sufficient, it has some limitations.
This include limited configurability (e.g., ordering of shown elements) and the lack of Internet access.
In fact, the later is considered a feature but severely limits the implementation presenting a map (at least in an easy way).
_NOTE:_ for showing a track on a map a workaround was implemented in OpenTracks.
Data could be exported as KMZ and presented in 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)).
For the time being, the OpenTracks Data API can be used to implement mainly custom dashboards.
## Implementation
OpenTracks by default does not expose any data to 3rd-party applications.
On the request of the user (i.e., pressing a button), an Intent is created that exposes the `Track` data (it's summary) as well as the `TrackPoint`s.
For each, one URI is provided.
These URIs notify observers if on content changes (e.g., a new `TrackPoint` was added).
Limitations:
* only tracks selected by the user is exposed,
* `Waypoints` are not exposed,
* access is only granted temporarily and automatically revoked (see `FLAG_GRANT_READ_URI_PERMISSION`),
* no write access is impossible, and
* neither track recordings can be started, paused, or stopped.
All required information can be found in `IntentDashboardUtils.java`.
A _reference implementation_ is the [OSMDashboard](https://github.com/OpenTracksApp/OSMDashboard).
+37
View File
@@ -0,0 +1,37 @@
# OpenTracks: Developer information
## Craft new releases
1. Get next _version code_ (master branch): `git rev-list HEAD --count master`
2. Decide on _version name_ (semantic versioning)
3. Manually update _version code_ and _version name_ in `AndroidManifest.xml` (used by FDroid)
4. Create changelog (`_version code_.txt`)
5. Create commit with all changes
6. Tag newly create commit with _version name_ (e.g., v3.2.1)
7. Push commits and tags to public repository
8. Add changelog to tag (Github releases)
## F-Droid
Application id: de.dennisguse.opentracks
It if it properly tagged F-Droid takes care of building the app and publishing.
Usually, it takes two days before the update is published (build, signed, and published).
The recent build activity can be found [here](https://f-droid.org/wiki/index.php?title=Special:RecentChanges&days=7&from=&hidebots=0&hideanons=1&hideliu=1&limit=500)
## Google Play Store
Application id: de.dennisguse.opentracks.playstore
Requirement: `gem install fastlane`
1. Build signed apk
(requires keystore)
2. Upload signed apk to [Google Play Console](https://play.google.com/apps/publish)
3. Update store meta data (e.g., description and screenshots)
`fastlane supply --version_code VERSION_CODE --skip_upload_apk`
(use the version code from the manifest)
(requires API key)
## Translations (localization)
Translations of the OpenTracks are handled in Transifex: [https://www.transifex.com/opentracks](https://www.transifex.com/opentracks/).
+16 -1
View File
@@ -6,7 +6,7 @@ buildscript {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.5.2'
classpath 'com.android.tools.build:gradle:3.5.3'
}
}
@@ -54,6 +54,11 @@ android {
defaultConfig {
applicationId "de.dennisguse.opentracks"
manifestPlaceholders = [
applicationLogo: "@drawable/ic_logo_color_24dp",
applicationName: "@string/app_name"
]
//Useful for debugging.
//versionCode getVersionCode()
@@ -69,11 +74,20 @@ android {
debug {
applicationIdSuffix ".debug"
versionNameSuffix "-debug"
manifestPlaceholders = [
applicationLogo: "@drawable/ic_logo_black_24dp",
applicationName: "OpenTracks (Debug)"
]
}
release {
minifyEnabled false
}
releasePlayStore {
applicationIdSuffix ".playstore"
versionNameSuffix "-PlayStore"
}
}
applicationVariants.all { variant ->
@@ -95,4 +109,5 @@ dependencies {
testImplementation 'androidx.test:core:1.2.0'
testImplementation 'org.mockito:mockito-core:3.0.0'
implementation 'com.google.android.material:material:1.0.0'
}
+52
View File
@@ -0,0 +1,52 @@
@startuml
class Track {
-id
-name
-description
-category
-numberOfPoints
-icon
}
Track o-- TrackPoint
Track o-- Waypoint
Track "1" - "1" TripStatistics
class TripStatistics {
- startTime
- stopTime
- totalDistance
- totalTime
- movingTime
- maxSpeed
- totalElevationGain
}
class TrackPoint extends android.location.Location {
- id
- trackId
- longitude
- latitude
- time
- altitude
- accuracy
- speed
- bearing
- sensor_heartrate
- sensor_cadence
- sensor_power
}
class Waypoint {
- id
- trackId
- name
- description
- category
- icon
- length
- duration
- location
- tripStatistics
- photoUrl
}
@enduml
+1 -1
View File
@@ -17,7 +17,7 @@
<rdf:RDF>
<cc:Work rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:title></dc:title>
<dc:title />
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

+1 -1
View File
@@ -43,7 +43,7 @@
<rdf:RDF>
<cc:Work rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:title></dc:title>
<dc:title />
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

+1 -1
View File
@@ -41,7 +41,7 @@
<rdf:RDF>
<cc:Work rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:title></dc:title>
<dc:title />
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

+1 -1
View File
@@ -33,7 +33,7 @@
<rdf:RDF>
<cc:Work rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:title></dc:title>
<dc:title />
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

+1 -1
View File
@@ -39,7 +39,7 @@
<rdf:RDF>
<cc:Work rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:title></dc:title>
<dc:title />
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

+1 -1
View File
@@ -18,7 +18,7 @@
<rdf:RDF>
<cc:Work rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:title></dc:title>
<dc:title />
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

+1 -1
View File
@@ -21,7 +21,7 @@
<rdf:RDF>
<cc:Work rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:title></dc:title>
<dc:title />
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

+38
View File
@@ -0,0 +1,38 @@
# This file contains the fastlane.tools configuration
# You can find the documentation at https://docs.fastlane.tools
#
# For a list of all available actions, check out
#
# https://docs.fastlane.tools/actions
#
# For a list of all available plugins, check out
#
# https://docs.fastlane.tools/plugins/available-plugins
#
# Uncomment the line if you want fastlane to automatically update itself
# update_fastlane
default_platform(:android)
platform :android do
desc "Runs all the tests"
lane :test do
gradle(task: "test")
end
desc "Submit a new Beta Build to Crashlytics Beta"
lane :beta do
gradle(task: "clean assembleRelease")
crashlytics
# sh "your_script.sh"
# You can also use other beta testing services here
end
desc "Deploy a new version to the Google Play"
lane :deploy do
gradle(task: "clean assembleRelease")
upload_to_play_store
end
end
@@ -0,0 +1,10 @@
v3.2.9: OpenTracks
Changes:
- Option to keep screen on while recording
- Markers: removed split markers completely
- Voice announcement now work without READ_PHONE_STATE permission
Bugfixes:
- Statistics: heart rate was not shown
- Statistics: time was briefly reset to 0s
@@ -0,0 +1,11 @@
v3.2.10: OpenTracks
Changes:
- ChartView is not configurable anymore
- After reboot: removed automatic restart of an unfinished recording
Bugfix:
- Speech announcements use MAY_DUCK (again)
- ChartView shows markers (again)
- Fixed landscape layout of TrackListActivity
- Request legacy file API behavior (necessary for Android 10)
@@ -0,0 +1,12 @@
v3.2.11: OpenTracks
Bug:
- Max speed is identical to avg speed
Bugfix:
- Crash on heart rate sensor selection
Developer:
- Replaced TabHost by TabsLayout
- Removed special first waypoint
- Removed duplicate translations
@@ -0,0 +1,13 @@
v3.3.0: OpenTracks
Breaking changes:
- Pictures are now stored in app-private storage (do not show up other picture apps)
NOTE: Pictures taking with previous version cannot be shown
- Localization for Spanish (Latin America) only works on Android 7.0 or later
Changes:
- Import/Export via Storage Access Framework (avoids READ/WRITE_EXTERNAL_STORAGE permission)
Developer:
- Cleaned internal data structures
- Use separate icons/app names for debug version
@@ -0,0 +1,12 @@
v3.3.1: OpenTracks
Changes:
- Android 10: on uninstall allow to keep app data
- Introduced Dashboard API: map icon can be used to open current track in external app
(requires installation of another app like OSMDashboard)
Bugfix:
- App label was broken (introduced in v3.3.0)
Developer:
- Documentation for Dashboard API can be found in README_DEVELOPER.md
Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

@@ -1 +1 @@
OpenTracks est une application de suivi d'activités sportives qui respecte totalement votre vie privée.
OpenTracks - Suivi d'activités sportives respectueux de votre vie privée.
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.
Binary file not shown.
@@ -1,103 +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.app.ActivityManager;
import android.app.ActivityManager.RunningServiceInfo;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.platform.app.InstrumentationRegistry;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.util.List;
import de.dennisguse.opentracks.services.TrackRecordingService;
/**
* Tests for the BootReceiver.
*
* @author Youtao Liu
*/
@RunWith(AndroidJUnit4.class)
public class BootReceiverTest {
private static final String SERVICE_NAME = "de.dennisguse.opentracks.services.TrackRecordingService";
private Context context = InstrumentationRegistry.getInstrumentation().getContext();
/**
* Tests the behavior when receive notification which is the phone boot.
*/
@Test
public void testOnReceive_startService() {
// Make sure no TrackRecordingService
Intent stopIntent = new Intent(context, TrackRecordingService.class);
context.stopService(stopIntent);
Assert.assertFalse(isServiceExisted(context));
BootReceiver bootReceiver = new BootReceiver();
Intent intent = new Intent();
intent.setAction(Intent.ACTION_BOOT_COMPLETED);
bootReceiver.onReceive(context, intent);
// Check if the service is started
Assert.assertFalse(isServiceExisted(context));
}
/**
* Tests the behavior when receive notification which is not the phone boot.
*/
@Test
public void testOnReceive_noStartService() {
// Make sure no TrackRecordingService
Intent stopIntent = new Intent(context, TrackRecordingService.class);
context.stopService(stopIntent);
Assert.assertFalse(isServiceExisted(context));
BootReceiver bootReceiver = new BootReceiver();
Intent intent = new Intent();
intent.setAction(Intent.ACTION_BUG_REPORT);
bootReceiver.onReceive(context, intent);
// Check if the service is not started
Assert.assertFalse(isServiceExisted(context));
}
/**
* Checks if a service is started in a context.
*
* @param context the context for checking a service
*/
private boolean isServiceExisted(Context context) {
ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
Assert.assertNotNull(activityManager);
List<ActivityManager.RunningServiceInfo> serviceList = activityManager.getRunningServices(Integer.MAX_VALUE);
for (int i = 0; i < serviceList.size(); i++) {
RunningServiceInfo serviceInfo = serviceList.get(i);
ComponentName componentName = serviceInfo.service;
if (componentName.getClassName().equals(SERVICE_NAME)) {
return true;
}
}
return false;
}
}
@@ -18,8 +18,8 @@ package de.dennisguse.opentracks;
import android.location.Location;
import de.dennisguse.opentracks.content.SensorDataSetLocation;
import de.dennisguse.opentracks.content.Track;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.content.sensor.SensorDataSet;
/**
@@ -60,16 +60,16 @@ public class TrackStubUtils {
*
* @return a track stub.
*/
public static SensorDataSetLocation createSensorDataSetLocation() {
public static TrackPoint createSensorDataSetLocation() {
return createSensorDataSetLocation(INITIAL_LATITUDE, INITIAL_LONGITUDE, INITIAL_ALTITUDE);
}
/**
* Creates a {@link SensorDataSetLocation} stub with specified values.
* Creates a {@link TrackPoint} stub with specified values.
*
* @return a SensorDataSetLocation stub.
*/
private static SensorDataSetLocation createSensorDataSetLocation(double latitude, double longitude, double altitude) {
private static TrackPoint createSensorDataSetLocation(double latitude, double longitude, double altitude) {
// Initial Location
Location loc = new Location(LOCATION_PROVIDER);
loc.setLatitude(latitude);
@@ -81,6 +81,6 @@ public class TrackStubUtils {
loc.setBearing(INITIAL_BEARING);
SensorDataSet sd = new SensorDataSet(Float.NaN, Float.NaN);
return new SensorDataSetLocation(loc, sd);
return new TrackPoint(loc, sd);
}
}
@@ -27,6 +27,9 @@ import org.junit.Before;
import org.junit.Test;
import de.dennisguse.opentracks.content.CustomContentProvider.DatabaseHelper;
import de.dennisguse.opentracks.content.data.TrackPointsColumns;
import de.dennisguse.opentracks.content.data.TracksColumns;
import de.dennisguse.opentracks.content.data.WaypointsColumns;
/**
* Tests {@link CustomContentProvider}.
@@ -35,7 +35,12 @@ import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import de.dennisguse.opentracks.content.Waypoint.WaypointType;
import de.dennisguse.opentracks.content.data.TestDataUtil;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.TrackPointsColumns;
import de.dennisguse.opentracks.content.data.TracksColumns;
import de.dennisguse.opentracks.content.data.Waypoint;
import de.dennisguse.opentracks.content.data.WaypointsColumns;
import de.dennisguse.opentracks.stats.TripStatistics;
import static org.mockito.Mockito.when;
@@ -52,12 +57,9 @@ public class CustomContentProviderUtilsTest {
private static final String MOCK_DESC = "Mock Next Waypoint Desc!";
private static final String TEST_DESC = "Test Desc!";
private static final String TEST_DESC_NEW = "Test Desc new!";
private static final double INITIAL_LATITUDE = 37.0;
private static final double INITIAL_LONGITUDE = -57.0;
private static final double ALTITUDE_INTERVAL = 2.5;
private Context context = ApplicationProvider.getApplicationContext();
private ContentProviderUtils providerUtils;
private ContentProviderUtils contentProviderUtils;
@Mock
private Cursor cursorMock;
@@ -67,8 +69,8 @@ public class CustomContentProviderUtilsTest {
@Before
public void setUp() {
providerUtils = new ContentProviderUtils(context);
providerUtils.deleteAllTracks(context);
contentProviderUtils = new ContentProviderUtils(context);
contentProviderUtils.deleteAllTracks(context);
}
@Test
@@ -132,9 +134,9 @@ public class CustomContentProviderUtilsTest {
private List<Location> testIterator(long trackId, int numPoints, int batchSize, boolean descending, LocationFactory locationFactory) {
long lastPointId = initializeTrack(trackId, numPoints);
((ContentProviderUtils) providerUtils).setDefaultCursorBatchSize(batchSize);
((ContentProviderUtils) contentProviderUtils).setDefaultCursorBatchSize(batchSize);
List<Location> locations = new ArrayList<Location>(numPoints);
try (LocationIterator it = providerUtils.getTrackPointLocationIterator(trackId, -1L, descending, locationFactory)) {
try (LocationIterator it = contentProviderUtils.getTrackPointLocationIterator(trackId, -1L, descending, locationFactory)) {
while (it.hasNext()) {
Location loc = it.next();
Assert.assertNotNull(loc);
@@ -153,8 +155,8 @@ public class CustomContentProviderUtilsTest {
track.setId(id);
track.setName("Test: " + id);
track.setNumberOfPoints(numPoints);
providerUtils.insertTrack(track);
track = providerUtils.getTrack(id);
contentProviderUtils.insertTrack(track);
track = contentProviderUtils.getTrack(id);
Assert.assertNotNull(track);
Location[] locations = new Location[numPoints];
@@ -166,12 +168,12 @@ public class CustomContentProviderUtilsTest {
loc.setAltitude(i * 2.5);
locations[i] = loc;
}
providerUtils.bulkInsertTrackPoint(locations, numPoints, id);
contentProviderUtils.bulkInsertTrackPoint(locations, numPoints, id);
// Load all inserted locations.
long lastPointId = -1;
int counter = 0;
try (LocationIterator it = providerUtils.getTrackPointLocationIterator(id, -1L, false, LocationFactory.DEFAULT_LOCATION_FACTORY)) {
try (LocationIterator it = contentProviderUtils.getTrackPointLocationIterator(id, -1L, false, LocationFactory.DEFAULT_LOCATION_FACTORY)) {
while (it.hasNext()) {
it.next();
lastPointId = it.getLocationId();
@@ -208,7 +210,7 @@ public class CustomContentProviderUtilsTest {
String name = NAME_PREFIX + trackId;
when(cursorMock.getString(columnIndex++)).thenReturn(name);
Track track = providerUtils.createTrack(cursorMock);
Track track = contentProviderUtils.createTrack(cursorMock);
Assert.assertEquals(trackId, track.getId());
Assert.assertEquals(name, track.getName());
}
@@ -220,10 +222,10 @@ public class CustomContentProviderUtilsTest {
public void testDeleteAllTracks() {
// Insert track, points and waypoint at first.
long trackId = System.currentTimeMillis();
Track track = getTrack(trackId, 10);
Track track = TestDataUtil.getTrack(trackId, 10);
insertTrackWithLocations(track);
Waypoint waypoint = new Waypoint();
providerUtils.insertWaypoint(waypoint);
contentProviderUtils.insertWaypoint(waypoint);
ContentResolver contentResolver = context.getContentResolver();
Cursor tracksCursor = contentResolver.query(TracksColumns.CONTENT_URI, null, null, null, TracksColumns._ID);
Assert.assertEquals(1, tracksCursor.getCount());
@@ -232,7 +234,7 @@ public class CustomContentProviderUtilsTest {
Cursor waypointCursor = contentResolver.query(WaypointsColumns.CONTENT_URI, null, null, null, WaypointsColumns._ID);
Assert.assertEquals(1, waypointCursor.getCount());
// Delete all.
providerUtils.deleteAllTracks(context);
contentProviderUtils.deleteAllTracks(context);
// Check whether all have been deleted.
tracksCursor = contentResolver.query(TracksColumns.CONTENT_URI, null, null, null, TracksColumns._ID);
Assert.assertEquals(0, tracksCursor.getCount());
@@ -249,15 +251,15 @@ public class CustomContentProviderUtilsTest {
public void testDeleteTrack() {
// Insert three tracks, points of two tracks and way point of one track.
long trackId = System.currentTimeMillis();
Track track = getTrack(trackId, 10);
Track track = TestDataUtil.getTrack(trackId, 10);
providerUtils.insertTrack(track);
insertTrackWithLocations(getTrack(trackId + 1, 10));
insertTrackWithLocations(getTrack(trackId + 2, 10));
contentProviderUtils.insertTrack(track);
insertTrackWithLocations(TestDataUtil.getTrack(trackId + 1, 10));
insertTrackWithLocations(TestDataUtil.getTrack(trackId + 2, 10));
Waypoint waypoint = new Waypoint();
waypoint.setTrackId(trackId);
providerUtils.insertWaypoint(waypoint);
contentProviderUtils.insertWaypoint(waypoint);
ContentResolver contentResolver = context.getContentResolver();
Cursor tracksCursor = contentResolver.query(TracksColumns.CONTENT_URI, null, null, null, TracksColumns._ID);
@@ -267,7 +269,7 @@ public class CustomContentProviderUtilsTest {
Cursor waypointCursor = contentResolver.query(WaypointsColumns.CONTENT_URI, null, null, null, WaypointsColumns._ID);
Assert.assertEquals(1, waypointCursor.getCount());
// Delete one track.
providerUtils.deleteTrack(context, trackId);
contentProviderUtils.deleteTrack(context, trackId);
// Check whether all data of a track has been deleted.
tracksCursor = contentResolver.query(TracksColumns.CONTENT_URI, null, null, null, TracksColumns._ID);
Assert.assertEquals(2, tracksCursor.getCount());
@@ -282,10 +284,10 @@ public class CustomContentProviderUtilsTest {
*/
@Test
public void testGetAllTracks() {
int initialTrackNumber = providerUtils.getAllTracks().size();
int initialTrackNumber = contentProviderUtils.getAllTracks().size();
long trackId = System.currentTimeMillis();
providerUtils.insertTrack(getTrack(trackId, 0));
List<Track> allTracks = providerUtils.getAllTracks();
contentProviderUtils.insertTrack(TestDataUtil.getTrack(trackId, 0));
List<Track> allTracks = contentProviderUtils.getAllTracks();
Assert.assertEquals(initialTrackNumber + 1, allTracks.size());
Assert.assertEquals(trackId, allTracks.get(allTracks.size() - 1).getId());
}
@@ -296,8 +298,8 @@ public class CustomContentProviderUtilsTest {
@Test
public void testGetLastTrack() {
long trackId = System.currentTimeMillis();
providerUtils.insertTrack(getTrack(trackId, 0));
Assert.assertEquals(trackId, providerUtils.getLastTrack().getId());
contentProviderUtils.insertTrack(TestDataUtil.getTrack(trackId, 0));
Assert.assertEquals(trackId, contentProviderUtils.getLastTrack().getId());
}
/**
@@ -306,8 +308,8 @@ public class CustomContentProviderUtilsTest {
@Test
public void testGetTrack() {
long trackId = System.currentTimeMillis();
providerUtils.insertTrack(getTrack(trackId, 0));
Assert.assertNotNull(providerUtils.getTrack(trackId));
contentProviderUtils.insertTrack(TestDataUtil.getTrack(trackId, 0));
Assert.assertNotNull(contentProviderUtils.getTrack(trackId));
}
/**
@@ -316,15 +318,15 @@ public class CustomContentProviderUtilsTest {
@Test
public void testUpdateTrack() {
long trackId = System.currentTimeMillis();
Track track = getTrack(trackId, 0);
Track track = TestDataUtil.getTrack(trackId, 0);
String nameOld = "name1";
String nameNew = "name2";
track.setName(nameOld);
providerUtils.insertTrack(track);
Assert.assertEquals(nameOld, providerUtils.getTrack(trackId).getName());
contentProviderUtils.insertTrack(track);
Assert.assertEquals(nameOld, contentProviderUtils.getTrack(trackId).getName());
track.setName(nameNew);
providerUtils.updateTrack(track);
Assert.assertEquals(nameNew, providerUtils.getTrack(trackId).getName());
contentProviderUtils.updateTrack(track);
Assert.assertEquals(nameNew, contentProviderUtils.getTrack(trackId).getName());
}
/**
@@ -333,8 +335,7 @@ public class CustomContentProviderUtilsTest {
@Test
public void testCreateContentValues_waypoint() {
long trackId = System.currentTimeMillis();
Track track = getTrack(trackId, 10);
providerUtils.insertTrack(track);
Track track = TestDataUtil.getTrack(trackId, 10);
// Bottom
long startTime = 1000L;
// AverageSpeed
@@ -351,12 +352,13 @@ public class CustomContentProviderUtilsTest {
statistics.setMinElevation(1200.0);
statistics.setMaxGrade(15.0);
statistics.setMinGrade(minGrade);
statistics.setBounds(-10000, 20000, 30000, -40000);
track.setTripStatistics(statistics);
contentProviderUtils.insertTrack(track);
// Insert at first.
Waypoint waypoint = new Waypoint();
waypoint.setDescription(TEST_DESC);
waypoint.setType(WaypointType.STATISTICS);
waypoint.setTripStatistics(statistics);
Location location = new Location("test");
location.setLatitude(22);
@@ -364,7 +366,7 @@ public class CustomContentProviderUtilsTest {
location.setAccuracy((float) 1 / 100.0f);
location.setAltitude(2.5);
waypoint.setLocation(location);
providerUtils.insertWaypoint(waypoint);
contentProviderUtils.insertWaypoint(waypoint);
ContentProviderUtils contentProviderUtils = new ContentProviderUtils(contentResolverMock);
@@ -374,8 +376,6 @@ public class CustomContentProviderUtilsTest {
Assert.assertEquals(waypointId, contentValues.get(WaypointsColumns._ID));
Assert.assertEquals(22 * 1000000, contentValues.get(WaypointsColumns.LONGITUDE));
Assert.assertEquals(TEST_DESC, contentValues.get(WaypointsColumns.DESCRIPTION));
Assert.assertEquals(startTime, contentValues.get(WaypointsColumns.STARTTIME));
Assert.assertEquals(minGrade, contentValues.get(WaypointsColumns.MINGRADE));
}
/**
@@ -406,7 +406,7 @@ public class CustomContentProviderUtilsTest {
long trackId = 11L;
when(cursorMock.getLong(columnIndex++)).thenReturn(trackId);
Waypoint waypoint = providerUtils.createWaypoint(cursorMock);
Waypoint waypoint = contentProviderUtils.createWaypoint(cursorMock);
Assert.assertEquals(id, waypoint.getId());
Assert.assertEquals(name, waypoint.getName());
Assert.assertEquals(trackId, waypoint.getTrackId());
@@ -414,50 +414,36 @@ public class CustomContentProviderUtilsTest {
/**
* Tests the method
* {@link ContentProviderUtils#deleteWaypoint(Context, long, DescriptionGenerator)}
* {@link ContentProviderUtils#deleteWaypoint(Context, long)}
* when there is only one waypoint in the track.
*/
@Test
public void testDeleteWaypoint_onlyOneWayPoint() {
long trackId = System.currentTimeMillis();
Track track = getTrack(trackId, 10);
providerUtils.insertTrack(track);
Track track = TestDataUtil.getTrack(trackId, 10);
contentProviderUtils.insertTrack(track);
// Insert at first.
Waypoint waypoint1 = new Waypoint();
waypoint1.setDescription(TEST_DESC);
waypoint1.setTrackId(trackId);
waypoint1.setType(WaypointType.STATISTICS);
providerUtils.insertWaypoint(waypoint1);
contentProviderUtils.insertWaypoint(waypoint1);
// Delete
DescriptionGenerator descriptionGenerator = new DescriptionGenerator() {
contentProviderUtils.deleteWaypoint(context, 1);
@Override
public String generateWaypointDescription(TripStatistics tripStatistics) {
return CustomContentProviderUtilsTest.MOCK_DESC;
}
@Override
public String generateTrackDescription(Track aTrack, boolean html) {
return null;
}
};
providerUtils.deleteWaypoint(context, 1, descriptionGenerator);
Assert.assertNull(providerUtils.getWaypoint(1));
Assert.assertNull(contentProviderUtils.getWaypoint(1));
}
/**
* Tests the method
* {@link ContentProviderUtils#deleteWaypoint(Context, long, DescriptionGenerator)}
* {@link ContentProviderUtils#deleteWaypoint(Context, long)}
* when there is more than one waypoint in the track.
*/
@Test
public void testDeleteWaypoint_hasNextWayPoint() {
long trackId = System.currentTimeMillis();
Track track = getTrack(trackId, 10);
providerUtils.insertTrack(track);
Track track = TestDataUtil.getTrack(trackId, 10);
TripStatistics statistics = new TripStatistics();
statistics.setStartTime(1000L);
@@ -471,124 +457,79 @@ public class CustomContentProviderUtilsTest {
statistics.setMinElevation(1200.0);
statistics.setMaxGrade(15.0);
statistics.setMinGrade(-25.0);
statistics.setBounds(-10000, 20000, 30000, -40000);
track.setTripStatistics(statistics);
contentProviderUtils.insertTrack(track);
// Insert at first.
Waypoint waypoint1 = new Waypoint();
waypoint1.setDescription(Long.toString(trackId));
waypoint1.setDescription(MOCK_DESC);
waypoint1.setTrackId(trackId);
waypoint1.setType(WaypointType.STATISTICS);
waypoint1.setTripStatistics(statistics);
long waypoint1Id = ContentUris.parseId(providerUtils.insertWaypoint(waypoint1));
long waypoint1Id = ContentUris.parseId(contentProviderUtils.insertWaypoint(waypoint1));
Waypoint waypoint2 = new Waypoint();
waypoint2.setDescription(Long.toString(trackId));
waypoint2.setDescription(MOCK_DESC);
waypoint2.setTrackId(trackId);
waypoint2.setType(WaypointType.STATISTICS);
waypoint2.setTripStatistics(statistics);
long waypoint2Id = ContentUris.parseId(providerUtils.insertWaypoint(waypoint2));
long waypoint2Id = ContentUris.parseId(contentProviderUtils.insertWaypoint(waypoint2));
// Delete
DescriptionGenerator descriptionGenerator = new DescriptionGenerator() {
@Override
public String generateWaypointDescription(TripStatistics tripStatistics) {
return CustomContentProviderUtilsTest.MOCK_DESC;
}
Assert.assertNotNull(contentProviderUtils.getWaypoint(waypoint1Id));
contentProviderUtils.deleteWaypoint(context, waypoint1Id);
Assert.assertNull(contentProviderUtils.getWaypoint(waypoint1Id));
@Override
public String generateTrackDescription(Track aTrack, boolean html) {
return null;
}
};
Assert.assertNotNull(providerUtils.getWaypoint(waypoint1Id));
providerUtils.deleteWaypoint(context, waypoint1Id, descriptionGenerator);
Assert.assertNull(providerUtils.getWaypoint(waypoint1Id));
Assert.assertEquals(CustomContentProviderUtilsTest.MOCK_DESC, providerUtils.getWaypoint(waypoint2Id)
.getDescription());
Assert.assertEquals(MOCK_DESC, contentProviderUtils.getWaypoint(waypoint2Id).getDescription());
}
/**
* Tests the method {@link ContentProviderUtils#getFirstWaypointId(long)}.
*/
@Test
public void testGetFirstWaypointId() {
long trackId = System.currentTimeMillis();
Track track = getTrack(trackId, 10);
providerUtils.insertTrack(track);
Waypoint waypoint1 = new Waypoint();
waypoint1.setTrackId(trackId);
long waypoint1id = ContentUris.parseId(providerUtils.insertWaypoint(waypoint1));
Waypoint waypoint2 = new Waypoint();
waypoint2.setTrackId(trackId);
long waypoint2id = ContentUris.parseId(providerUtils.insertWaypoint(waypoint2));
Assert.assertEquals(-1L, providerUtils.getFirstWaypointId(-1));
Assert.assertEquals(waypoint1id, providerUtils.getFirstWaypointId(trackId));
Assert.assertEquals(waypoint2id, providerUtils.getLastWaypoint(trackId, WaypointType.WAYPOINT).getId());
}
/**
* Tests the method {@link ContentProviderUtils#getNextWaypointNumber(long, WaypointType)}.
* Tests the method {@link ContentProviderUtils#getNextWaypointNumber(long)}.
*/
@Test
public void testGetNextWaypointNumber() {
long trackId = System.currentTimeMillis();
Track track = getTrack(trackId, 10);
providerUtils.insertTrack(track);
Track track = TestDataUtil.getTrack(trackId, 10);
contentProviderUtils.insertTrack(track);
Waypoint waypoint1 = new Waypoint();
waypoint1.setType(WaypointType.STATISTICS);
waypoint1.setTrackId(trackId);
Waypoint waypoint2 = new Waypoint();
waypoint2.setType(WaypointType.WAYPOINT);
waypoint2.setTrackId(trackId);
Waypoint waypoint3 = new Waypoint();
waypoint3.setType(WaypointType.STATISTICS);
waypoint3.setTrackId(trackId);
Waypoint waypoint4 = new Waypoint();
waypoint4.setType(WaypointType.WAYPOINT);
waypoint4.setTrackId(trackId);
providerUtils.insertWaypoint(waypoint1);
providerUtils.insertWaypoint(waypoint2);
providerUtils.insertWaypoint(waypoint3);
providerUtils.insertWaypoint(waypoint4);
contentProviderUtils.insertWaypoint(waypoint1);
contentProviderUtils.insertWaypoint(waypoint2);
contentProviderUtils.insertWaypoint(waypoint3);
contentProviderUtils.insertWaypoint(waypoint4);
Assert.assertEquals(2, providerUtils.getNextWaypointNumber(trackId, WaypointType.STATISTICS));
Assert.assertEquals(3, providerUtils.getNextWaypointNumber(trackId, WaypointType.WAYPOINT));
Assert.assertEquals(4, contentProviderUtils.getNextWaypointNumber(trackId));
}
/**
* Tests the method
* {@link ContentProviderUtils#getLastWaypoint(long, WaypointType)}.
* {@link ContentProviderUtils#getLastWaypoint(long)}.
*/
@Test
public void testGetLastStatisticsWaypoint() {
public void testGetLastWaypoint() {
long trackId = System.currentTimeMillis();
Track track = getTrack(trackId, 10);
providerUtils.insertTrack(track);
Track track = TestDataUtil.getTrack(trackId, 10);
contentProviderUtils.insertTrack(track);
Waypoint waypoint1 = new Waypoint();
waypoint1.setTrackId(trackId);
waypoint1.setType(WaypointType.STATISTICS);
waypoint1.setDescription("Desc1");
Waypoint waypoint2 = new Waypoint();
waypoint2.setTrackId(trackId);
waypoint2.setType(WaypointType.STATISTICS);
waypoint2.setDescription("Desc2");
Waypoint waypoint3 = new Waypoint();
waypoint3.setTrackId(trackId);
waypoint3.setType(WaypointType.WAYPOINT);
waypoint3.setDescription("Desc3");
providerUtils.insertWaypoint(waypoint1);
providerUtils.insertWaypoint(waypoint2);
providerUtils.insertWaypoint(waypoint3);
contentProviderUtils.insertWaypoint(waypoint1);
contentProviderUtils.insertWaypoint(waypoint2);
contentProviderUtils.insertWaypoint(waypoint3);
Assert.assertEquals("Desc2", providerUtils.getLastWaypoint(trackId, WaypointType.STATISTICS).getDescription());
Assert.assertEquals("Desc3", contentProviderUtils.getLastWaypoint(trackId).getDescription());
}
/**
@@ -598,15 +539,15 @@ public class CustomContentProviderUtilsTest {
@Test
public void testInsertAndGetWaypoint() {
long trackId = System.currentTimeMillis();
Track track = getTrack(trackId, 10);
providerUtils.insertTrack(track);
Track track = TestDataUtil.getTrack(trackId, 10);
contentProviderUtils.insertTrack(track);
Waypoint waypoint = new Waypoint();
waypoint.setDescription(TEST_DESC);
waypoint.setTrackId(trackId);
long waypointId = ContentUris.parseId(providerUtils.insertWaypoint(waypoint));
long waypointId = ContentUris.parseId(contentProviderUtils.insertWaypoint(waypoint));
Assert.assertEquals(TEST_DESC, providerUtils.getWaypoint(waypointId).getDescription());
Assert.assertEquals(TEST_DESC, contentProviderUtils.getWaypoint(waypointId).getDescription());
}
/**
@@ -615,20 +556,20 @@ public class CustomContentProviderUtilsTest {
@Test
public void testUpdateWaypoint() {
long trackId = System.currentTimeMillis();
Track track = getTrack(trackId, 10);
providerUtils.insertTrack(track);
Track track = TestDataUtil.getTrack(trackId, 10);
contentProviderUtils.insertTrack(track);
// Insert at first.
Waypoint waypoint = new Waypoint();
waypoint.setDescription(TEST_DESC);
waypoint.setTrackId(trackId);
long waypointId = ContentUris.parseId(providerUtils.insertWaypoint(waypoint));
long waypointId = ContentUris.parseId(contentProviderUtils.insertWaypoint(waypoint));
// Update
waypoint = providerUtils.getWaypoint(waypointId);
waypoint = contentProviderUtils.getWaypoint(waypointId);
waypoint.setDescription(TEST_DESC_NEW);
providerUtils.updateWaypoint(waypoint);
contentProviderUtils.updateWaypoint(waypoint);
Assert.assertEquals(TEST_DESC_NEW, providerUtils.getWaypoint(waypointId).getDescription());
Assert.assertEquals(TEST_DESC_NEW, contentProviderUtils.getWaypoint(waypointId).getDescription());
}
/**
@@ -638,13 +579,13 @@ public class CustomContentProviderUtilsTest {
public void testBulkInsertTrackPoint() {
// Insert track, point at first.
long trackId = System.currentTimeMillis();
Track track = getTrack(trackId, 10);
Track track = TestDataUtil.getTrack(trackId, 10);
insertTrackWithLocations(track);
providerUtils.bulkInsertTrackPoint(track.getLocations().toArray(new Location[0]), -1, trackId);
Assert.assertEquals(20, providerUtils.getTrackPointCursor(trackId, -1L, 1000, false).getCount());
providerUtils.bulkInsertTrackPoint(track.getLocations().toArray(new Location[0]), 8, trackId);
Assert.assertEquals(28, providerUtils.getTrackPointCursor(trackId, -1L, 1000, false).getCount());
contentProviderUtils.bulkInsertTrackPoint(track.getLocations().toArray(new Location[0]), -1, trackId);
Assert.assertEquals(20, contentProviderUtils.getTrackPointCursor(trackId, -1L, 1000, false).getCount());
contentProviderUtils.bulkInsertTrackPoint(track.getLocations().toArray(new Location[0]), 8, trackId);
Assert.assertEquals(28, contentProviderUtils.getTrackPointCursor(trackId, -1L, 1000, false).getCount());
}
/**
@@ -699,7 +640,7 @@ public class CustomContentProviderUtilsTest {
byte[] sensor = "Sensor state".getBytes();
when(cursorMock.getBlob(index++)).thenReturn(sensor);
Location location = providerUtils.createTrackPoint(cursorMock);
Location location = contentProviderUtils.createTrackPoint(cursorMock);
Assert.assertEquals((double) longitude, location.getLongitude(), 0.01);
Assert.assertEquals((double) latitude, location.getLatitude(), 0.01);
Assert.assertEquals(time, location.getTime(), 0.01);
@@ -714,37 +655,11 @@ public class CustomContentProviderUtilsTest {
public void testInsertTrackPoint() {
// Insert track, point at first.
long trackId = System.currentTimeMillis();
Track track = getTrack(trackId, 10);
Track track = TestDataUtil.getTrack(trackId, 10);
insertTrackWithLocations(track);
providerUtils.insertTrackPoint(createLocation(22), trackId);
Assert.assertEquals(11, providerUtils.getTrackPointCursor(trackId, -1L, 1000, false).getCount());
}
/**
* Tests the method {@link ContentProviderUtils#getFirstTrackPointId(long)}.
*/
@Test
public void testGetFirstTrackPointId() {
// Insert track, point at first.
long trackId = System.currentTimeMillis();
Track track = getTrack(trackId, 10);
insertTrackWithLocations(track);
Assert.assertNotEquals(-1L, providerUtils.getFirstTrackPointId(trackId));
}
/**
* Tests the method {@link ContentProviderUtils#getLastTrackPointId(long)}.
*/
@Test
public void testGetLastTrackPointId() {
// Insert track, point at first.
long trackId = System.currentTimeMillis();
Track track = getTrack(trackId, 10);
insertTrackWithLocations(track);
Assert.assertTrue(providerUtils.getFirstTrackPointId(trackId) < providerUtils.getLastTrackPointId(trackId));
contentProviderUtils.insertTrackPoint(TestDataUtil.createLocation(22), trackId);
Assert.assertEquals(11, contentProviderUtils.getTrackPointCursor(trackId, -1L, 1000, false).getCount());
}
/**
@@ -754,10 +669,10 @@ public class CustomContentProviderUtilsTest {
public void testGetLastValidTrackPoint() {
// Insert track, points at first.
long trackId = System.currentTimeMillis();
Track track = getTrack(trackId, 10);
Track track = TestDataUtil.getTrack(trackId, 10);
insertTrackWithLocations(track);
Location lastLocation = providerUtils.getLastValidTrackPoint(trackId);
Location lastLocation = contentProviderUtils.getLastValidTrackPoint(trackId);
checkLocation(9, lastLocation);
}
@@ -768,15 +683,15 @@ public class CustomContentProviderUtilsTest {
public void testGetTrackPointCursor_desc() {
// Insert track, points at first.
long trackId = System.currentTimeMillis();
Track track = getTrack(trackId, 10);
providerUtils.insertTrack(track);
Track track = TestDataUtil.getTrack(trackId, 10);
contentProviderUtils.insertTrack(track);
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()));
trackpointIds[i] = ContentUris.parseId(contentProviderUtils.insertTrackPoint(track.getLocations().get(i), track.getId()));
}
Cursor cursor = providerUtils.getTrackPointCursor(trackId, trackpointIds[1], 5, true);
Cursor cursor = contentProviderUtils.getTrackPointCursor(trackId, trackpointIds[1], 5, true);
Assert.assertEquals(2, cursor.getCount());
}
@@ -787,15 +702,15 @@ public class CustomContentProviderUtilsTest {
public void testGetTrackPointCursor_asc() {
// Insert track, points at first.
long trackId = System.currentTimeMillis();
Track track = getTrack(trackId, 10);
providerUtils.insertTrack(track);
Track track = TestDataUtil.getTrack(trackId, 10);
contentProviderUtils.insertTrack(track);
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()));
trackpointIds[i] = ContentUris.parseId(contentProviderUtils.insertTrackPoint(track.getLocations().get(i), track.getId()));
}
Cursor cursor = providerUtils.getTrackPointCursor(trackId, trackpointIds[8], 5, false);
Cursor cursor = contentProviderUtils.getTrackPointCursor(trackId, trackpointIds[8], 5, false);
Assert.assertEquals(2, cursor.getCount());
}
@@ -806,17 +721,17 @@ public class CustomContentProviderUtilsTest {
public void testGetTrackPointLocationIterator_desc() {
// Insert track, points at first.
long trackId = System.currentTimeMillis();
Track track = getTrack(trackId, 10);
providerUtils.insertTrack(track);
Track track = TestDataUtil.getTrack(trackId, 10);
contentProviderUtils.insertTrack(track);
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()));
trackpointIds[i] = ContentUris.parseId(contentProviderUtils.insertTrackPoint(track.getLocations().get(i), track.getId()));
}
long startTrackPointId = trackpointIds[9];
LocationIterator locationIterator = providerUtils.getTrackPointLocationIterator(trackId, startTrackPointId, true, LocationFactory.DEFAULT_LOCATION_FACTORY);
LocationIterator locationIterator = contentProviderUtils.getTrackPointLocationIterator(trackId, startTrackPointId, true, LocationFactory.DEFAULT_LOCATION_FACTORY);
for (int i = 0; i < trackpointIds.length; i++) {
Assert.assertTrue(locationIterator.hasNext());
Location location = locationIterator.next();
@@ -833,17 +748,17 @@ public class CustomContentProviderUtilsTest {
public void testGetTrackPointLocationIterator_asc() {
// Insert track, point at first.
long trackId = System.currentTimeMillis();
Track track = getTrack(trackId, 10);
providerUtils.insertTrack(track);
Track track = TestDataUtil.getTrack(trackId, 10);
contentProviderUtils.insertTrack(track);
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()));
trackpointIds[i] = ContentUris.parseId(contentProviderUtils.insertTrackPoint(track.getLocations().get(i), track.getId()));
}
long startTrackPointId = trackpointIds[0];
LocationIterator locationIterator = providerUtils.getTrackPointLocationIterator(trackId, startTrackPointId, false, LocationFactory.DEFAULT_LOCATION_FACTORY);
LocationIterator locationIterator = contentProviderUtils.getTrackPointLocationIterator(trackId, startTrackPointId, false, LocationFactory.DEFAULT_LOCATION_FACTORY);
for (int i = 0; i < trackpointIds.length; i++) {
Assert.assertTrue(locationIterator.hasNext());
Location location = locationIterator.next();
@@ -854,50 +769,17 @@ public class CustomContentProviderUtilsTest {
Assert.assertFalse(locationIterator.hasNext());
}
/**
* Simulates a track which is used for testing.
*
* @param id the id of the track
* @param numPoints the location number in the track
* @return the simulated track
*/
private Track getTrack(long id, int numPoints) {
Track track = new Track();
track.setId(id);
track.setName("Test: " + id);
track.setNumberOfPoints(numPoints);
for (int i = 0; i < numPoints; i++) {
track.addLocation(createLocation(i));
}
return track;
}
/**
* Creates a location.
*
* @param i the index to set the value of location.
* @return created location
*/
private Location createLocation(int i) {
Location loc = new Location("test");
loc.setLatitude(INITIAL_LATITUDE + (double) i / 10000.0);
loc.setLongitude(INITIAL_LONGITUDE - (double) i / 10000.0);
loc.setAccuracy((float) i / 100.0f);
loc.setAltitude(i * ALTITUDE_INTERVAL);
return loc;
}
/**
* Checks the value of a location.
*
* @param i the index of this location which created in the method {@link CustomContentProviderUtilsTest#getTrack(long, int)}
* @param i the index of this location which created in the method {@link TestDataUtil#getTrack(long, int)}
* @param location the location to be checked
*/
private void checkLocation(int i, Location location) {
Assert.assertEquals(INITIAL_LATITUDE + (double) i / 10000.0, location.getLatitude(), 0.01);
Assert.assertEquals(INITIAL_LONGITUDE - (double) i / 10000.0, location.getLongitude(), 0.01);
Assert.assertEquals(TestDataUtil.INITIAL_LATITUDE + (double) i / 10000.0, location.getLatitude(), 0.01);
Assert.assertEquals(TestDataUtil.INITIAL_LONGITUDE - (double) i / 10000.0, location.getLongitude(), 0.01);
Assert.assertEquals((float) i / 100.0f, location.getAccuracy(), 0.01);
Assert.assertEquals(i * ALTITUDE_INTERVAL, location.getAltitude(), 0.01);
Assert.assertEquals(i * TestDataUtil.ALTITUDE_INTERVAL, location.getAltitude(), 0.01);
}
/**
@@ -906,7 +788,7 @@ public class CustomContentProviderUtilsTest {
* @param track track to be inserted
*/
private void insertTrackWithLocations(Track track) {
providerUtils.insertTrack(track);
providerUtils.bulkInsertTrackPoint(track.getLocations().toArray(new Location[0]), track.getLocations().size(), track.getId());
contentProviderUtils.insertTrack(track);
contentProviderUtils.bulkInsertTrackPoint(track.getLocations().toArray(new Location[0]), track.getLocations().size(), track.getId());
}
}
@@ -27,29 +27,30 @@ import org.junit.Test;
import org.junit.runner.RunWith;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.stats.TripStatistics;
import de.dennisguse.opentracks.util.StringUtils;
/**
* Tests for {@link DescriptionGeneratorImpl}.
* Tests for {@link DescriptionGenerator}.
*
* @author Jimmy Shih
*/
@RunWith(AndroidJUnit4.class)
public class DescriptionGeneratorImplTest {
public class DescriptionGeneratorTest {
private static final long START_TIME = 1288721514000L;
private DescriptionGeneratorImpl descriptionGenerator;
private DescriptionGenerator descriptionGenerator;
private Context context = ApplicationProvider.getApplicationContext();
@Before
public void setUp() {
descriptionGenerator = new DescriptionGeneratorImpl(ApplicationProvider.getApplicationContext());
descriptionGenerator = new DescriptionGenerator(ApplicationProvider.getApplicationContext());
}
/**
* Tests {@link DescriptionGeneratorImpl#generateTrackDescription(Track, boolean)}.
* Tests {@link DescriptionGenerator#generateTrackDescription(Track, boolean)}.
*/
@Test
public void testGenerateTrackDescription() {
@@ -68,7 +69,7 @@ public class DescriptionGeneratorImplTest {
track.setTripStatistics(stats);
track.setCategory("hiking");
String expected = //"Created by"
"<a href='https://github.com/dennisguse/opentracks'>OpenTracks</a><p>"
"<a href='https://github.com/OpenTracksApp/OpenTracks'>OpenTracks</a><p>"
+ "Name: -<br>"
+ "Activity type: hiking<br>"
+ "Description: -<br>"
@@ -91,44 +92,9 @@ public class DescriptionGeneratorImplTest {
Assert.assertEquals(expected, descriptionGenerator.generateTrackDescription(track, true));
}
/**
* Tests {@link DescriptionGeneratorImpl#generateWaypointDescription(TripStatistics)}.
*/
@Test
public void testGenerateWaypointDescription() {
Waypoint waypoint = new Waypoint();
TripStatistics stats = new TripStatistics();
stats.setTotalDistance(20000);
stats.setTotalTime(600000);
stats.setMovingTime(300000);
stats.setMaxSpeed(100);
stats.setMaxElevation(550);
stats.setMinElevation(-500);
stats.setTotalElevationGain(6000);
stats.setMaxGrade(0.42);
stats.setMinGrade(0.11);
stats.setStartTime(START_TIME);
waypoint.setTripStatistics(stats);
String expected = "Total distance: 20.00 km (12.4 mi)\n"
+ "Total time: 10:00\n"
+ "Moving time: 05:00\n"
+ "Average speed: 120.00 km/h (74.6 mi/h)\n"
+ "Average moving speed: 240.00 km/h (149.1 mi/h)\n"
+ "Max speed: 360.00 km/h (223.7 mi/h)\n"
+ "Average pace: 0:30 min/km (0:48 min/mi)\n"
+ "Average moving pace: 0:15 min/km (0:24 min/mi)\n"
+ "Fastest pace: 0:10 min/km (0:16 min/mi)\n"
+ "Max elevation: 550 m (1804 ft)\n"
+ "Min elevation: -500 m (-1640 ft)\n"
+ "Elevation gain: 6000 m (19685 ft)\n"
+ "Max grade: 42 %\n"
+ "Min grade: 11 %\n"
+ "Recorded: " + StringUtils.formatDateTime(context, START_TIME) + "\n";
Assert.assertEquals(expected, descriptionGenerator.generateWaypointDescription(waypoint.getTripStatistics()));
}
/**
* Tests {@link DescriptionGeneratorImpl#writeDistance(double, StringBuilder, int, String)}.
* Tests {@link DescriptionGenerator#writeDistance(double, StringBuilder, int, String)}.
*/
@Test
public void testWriteDistance() {
@@ -138,7 +104,7 @@ public class DescriptionGeneratorImplTest {
}
/**
* Tests {@link DescriptionGeneratorImpl#writeTime(long, StringBuilder, int, String)}.
* Tests {@link DescriptionGenerator#writeTime(long, StringBuilder, int, String)}.
*/
@Test
public void testWriteTime() {
@@ -148,7 +114,7 @@ public class DescriptionGeneratorImplTest {
}
/**
* Tests {@link DescriptionGeneratorImpl#writeSpeed(double, StringBuilder, int, String)}.
* Tests {@link DescriptionGenerator#writeSpeed(double, StringBuilder, int, String)}.
*/
@Test
public void testWriteSpeed() {
@@ -158,7 +124,7 @@ public class DescriptionGeneratorImplTest {
}
/**
* Tests {@link DescriptionGeneratorImpl#writeElevation(double, StringBuilder, int, String)}.
* Tests {@link DescriptionGenerator#writeElevation(double, StringBuilder, int, String)}.
*/
@Test
public void testWriteElevation() {
@@ -168,7 +134,7 @@ public class DescriptionGeneratorImplTest {
}
/**
* Tests {@link DescriptionGeneratorImpl#writePace(double, StringBuilder, int, String)}.
* Tests {@link DescriptionGenerator#writePace(double, StringBuilder, int, String)}.
*/
@Test
public void testWritePace() {
@@ -178,7 +144,7 @@ public class DescriptionGeneratorImplTest {
}
/**
* Tests {@link DescriptionGeneratorImpl#writeGrade(double, StringBuilder, int, String)}.
* Tests {@link DescriptionGenerator#writeGrade(double, StringBuilder, int, String)}.
*/
@Test
public void testWriteGrade() {
@@ -188,7 +154,7 @@ public class DescriptionGeneratorImplTest {
}
/**
* Tests {@link DescriptionGeneratorImpl#writeGrade(double, StringBuilder, int, String)} with a NaN.
* Tests {@link DescriptionGenerator#writeGrade(double, StringBuilder, int, String)} with a NaN.
*/
@Test
public void testWriteGrade_nan() {
@@ -198,7 +164,7 @@ public class DescriptionGeneratorImplTest {
}
/**
* Tests {@link DescriptionGeneratorImpl#writeGrade(double, StringBuilder, int, String)} with an infinite number.
* Tests {@link DescriptionGenerator#writeGrade(double, StringBuilder, int, String)} with an infinite number.
*/
@Test
public void testWriteGrade_infinite() {
@@ -36,6 +36,8 @@ import java.util.List;
import de.dennisguse.opentracks.content.SearchEngine.ScoredResult;
import de.dennisguse.opentracks.content.SearchEngine.SearchQuery;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.Waypoint;
import de.dennisguse.opentracks.stats.TripStatistics;
/**
@@ -80,10 +82,6 @@ public class SearchEngineTest {
stats.setStopTime(NOW);
}
int latitude = (int) ((HERE.getLatitude() + distance) * 1E6);
int longitude = (int) ((HERE.getLongitude() + distance) * 1E6);
stats.setBounds(latitude, longitude, latitude, longitude);
Uri uri = providerUtils.insertTrack(track);
return ContentUris.parseId(uri);
}
@@ -209,20 +207,6 @@ public class SearchEngineTest {
assertWaypointResult(descriptionWaypointId, results.get(2));
}
@Test
public void testSearchTrackDistance() {
// All results match text, but they're at difference distances from the user.
long farFarAwayId = insertTrack("aa", 0.3);
long nearId = insertTrack("ab", 0.1);
long farId = insertTrack("ac", 0.2);
SearchQuery query = new SearchQuery("a", HERE, -1, NOW);
ArrayList<ScoredResult> results = new ArrayList<ScoredResult>(engine.search(query));
// Distance order.
assertTrackResults(results, nearId, farId, farFarAwayId);
}
@Test
public void testSearchWaypointDistance() {
// All results match text, but they're at difference distances from the user.
@@ -0,0 +1,44 @@
package de.dennisguse.opentracks.content.data;
import android.location.Location;
public class TestDataUtil {
public static final double INITIAL_LATITUDE = 37.0;
public static final double INITIAL_LONGITUDE = -57.0;
public static final double ALTITUDE_INTERVAL = 2.5;
/**
* Simulates a track which is used for testing.
*
* @param id the id of the track
* @param numPoints the location number in the track
* @return the simulated track
*/
public static Track getTrack(long id, int numPoints) {
Track track = new Track();
track.setId(id);
track.setName("Test: " + id);
track.setNumberOfPoints(numPoints);
for (int i = 0; i < numPoints; i++) {
track.addLocation(createLocation(i));
}
return track;
}
/**
* Creates a location.
*
* @param i the index to set the value of location.
* @return created location
*/
public static Location createLocation(int i) {
Location loc = new Location("test");
loc.setLatitude(INITIAL_LATITUDE + (double) i / 10000.0);
loc.setLongitude(INITIAL_LONGITUDE - (double) i / 10000.0);
loc.setAccuracy((float) i / 100.0f);
loc.setAltitude(i * ALTITUDE_INTERVAL);
loc.setTime(i + 1);
return loc;
}
}
@@ -31,7 +31,7 @@ import org.junit.runner.RunWith;
import de.dennisguse.opentracks.TrackStubUtils;
import de.dennisguse.opentracks.chart.ChartView;
import de.dennisguse.opentracks.content.SensorDataSetLocation;
import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.content.sensor.SensorDataSet;
import de.dennisguse.opentracks.util.UnitConversions;
@@ -72,7 +72,7 @@ public class ChartFragmentTest {
*/
@Test
public void testFillDataPoint_sensorIncorrect() {
SensorDataSetLocation sensorDataSetLocation = TrackStubUtils.createSensorDataSetLocation();
TrackPoint sensorDataSetLocation = TrackStubUtils.createSensorDataSetLocation();
// No input.
double[] point = fillDataPointTestHelper(sensorDataSetLocation);
@@ -96,7 +96,7 @@ public class ChartFragmentTest {
*/
@Test
public void testFillDataPoint_sensorCorrect() {
SensorDataSetLocation sensorDataSetLocation = TrackStubUtils.createSensorDataSetLocation();
TrackPoint sensorDataSetLocation = TrackStubUtils.createSensorDataSetLocation();
// No input.
double[] point = fillDataPointTestHelper(sensorDataSetLocation);
Assert.assertEquals(Float.NaN, point[ChartView.HEART_RATE_SERIES + 1], 0.01);
@@ -123,17 +123,17 @@ public class ChartFragmentTest {
// By distance.
chartFragment.setChartByDistance(true);
// Resets last location and writes first location.
SensorDataSetLocation sensorDataSetLocation1 = TrackStubUtils.createSensorDataSetLocation();
TrackPoint sensorDataSetLocation1 = TrackStubUtils.createSensorDataSetLocation();
double[] point = fillDataPointTestHelper(sensorDataSetLocation1);
Assert.assertEquals(0.0, point[0], 0.01);
// The second is a same location, just different time.
SensorDataSetLocation sensorDataSetLocation2 = TrackStubUtils.createSensorDataSetLocation();
TrackPoint sensorDataSetLocation2 = TrackStubUtils.createSensorDataSetLocation();
point = fillDataPointTestHelper(sensorDataSetLocation2);
Assert.assertEquals(0.0, point[0], 0.01);
// The third location is a new location, and use metric.
SensorDataSetLocation sensorDataSetLocation3 = TrackStubUtils.createSensorDataSetLocation();
TrackPoint sensorDataSetLocation3 = TrackStubUtils.createSensorDataSetLocation();
sensorDataSetLocation3.setLatitude(23);
point = fillDataPointTestHelper(sensorDataSetLocation3);
@@ -145,7 +145,7 @@ public class ChartFragmentTest {
Assert.assertEquals(distance1, point[0], 0.01);
// The fourth location is a new location, and use metric.
SensorDataSetLocation sensorDataSetLocation4 = TrackStubUtils.createSensorDataSetLocation();
TrackPoint sensorDataSetLocation4 = TrackStubUtils.createSensorDataSetLocation();
sensorDataSetLocation4.setLatitude(24);
point = fillDataPointTestHelper(sensorDataSetLocation4);
@@ -167,12 +167,12 @@ public class ChartFragmentTest {
chartFragment.setMetricUnits(false);
// The first is a same location, just different time.
SensorDataSetLocation sensorDataSetLocation1 = TrackStubUtils.createSensorDataSetLocation();
TrackPoint sensorDataSetLocation1 = TrackStubUtils.createSensorDataSetLocation();
double[] point = fillDataPointTestHelper(sensorDataSetLocation1);
Assert.assertEquals(0.0, point[0], 0.01);
// The second location is a new location, and use imperial.
SensorDataSetLocation sensorDataSetLocation2 = TrackStubUtils.createSensorDataSetLocation();
TrackPoint sensorDataSetLocation2 = TrackStubUtils.createSensorDataSetLocation();
sensorDataSetLocation2.setLatitude(23);
point = fillDataPointTestHelper(sensorDataSetLocation2);
@@ -186,7 +186,7 @@ public class ChartFragmentTest {
Assert.assertEquals(distance1, point[0], 0.01);
// The third location is a new location, and use imperial.
SensorDataSetLocation sensorDataSetLocation3 = TrackStubUtils.createSensorDataSetLocation();
TrackPoint sensorDataSetLocation3 = TrackStubUtils.createSensorDataSetLocation();
sensorDataSetLocation3.setLatitude(24);
point = fillDataPointTestHelper(sensorDataSetLocation3);
@@ -206,11 +206,11 @@ public class ChartFragmentTest {
public void testFillDataPoint_time() {
// By time
chartFragment.setChartByDistance(false);
SensorDataSetLocation sensorDataSetLocation1 = TrackStubUtils.createSensorDataSetLocation();
TrackPoint sensorDataSetLocation1 = TrackStubUtils.createSensorDataSetLocation();
double[] point = fillDataPointTestHelper(sensorDataSetLocation1);
Assert.assertEquals(0.0, point[0], 0.01);
long timeSpan = 222;
SensorDataSetLocation sensorDataSetLocation2 = TrackStubUtils.createSensorDataSetLocation();
TrackPoint sensorDataSetLocation2 = TrackStubUtils.createSensorDataSetLocation();
sensorDataSetLocation2.setTime(sensorDataSetLocation1.getTime() + timeSpan);
point = fillDataPointTestHelper(sensorDataSetLocation2);
Assert.assertEquals((double) timeSpan, point[0], 0.01);
@@ -221,7 +221,7 @@ public class ChartFragmentTest {
*/
@Test
public void testFillDataPoint_elevation() {
SensorDataSetLocation sensorDataSetLocation1 = TrackStubUtils.createSensorDataSetLocation();
TrackPoint sensorDataSetLocation1 = TrackStubUtils.createSensorDataSetLocation();
/*
* At first, clear old points of elevation, so give true to the second parameter.
@@ -233,7 +233,7 @@ public class ChartFragmentTest {
/*
* Send another value to buffer, now there are two values, INITIAL_ALTITUDE and INITIAL_ALTITUDE * 2.
*/
SensorDataSetLocation sensorDataSetLocation2 = TrackStubUtils.createSensorDataSetLocation();
TrackPoint sensorDataSetLocation2 = TrackStubUtils.createSensorDataSetLocation();
sensorDataSetLocation2.setAltitude(TrackStubUtils.INITIAL_ALTITUDE * 2);
point = fillDataPointTestHelper(sensorDataSetLocation2);
Assert.assertEquals((TrackStubUtils.INITIAL_ALTITUDE + TrackStubUtils.INITIAL_ALTITUDE * 2) / 2.0,
@@ -251,7 +251,7 @@ public class ChartFragmentTest {
* At first, clear old points of speed, so give true to the second parameter.
* It will not be filled in to the speed buffer.
*/
SensorDataSetLocation sensorDataSetLocation1 = TrackStubUtils.createSensorDataSetLocation();
TrackPoint sensorDataSetLocation1 = TrackStubUtils.createSensorDataSetLocation();
sensorDataSetLocation1.setSpeed(128.5f);
double[] point = fillDataPointTestHelper(sensorDataSetLocation1);
Assert.assertEquals(0.0, point[ChartView.SPEED_SERIES + 1], 0.01);
@@ -260,7 +260,7 @@ public class ChartFragmentTest {
* Tests the logic when both metricUnits and reportSpeed are true.
* This location will be filled into speed buffer.
*/
SensorDataSetLocation sensorDataSetLocation2 = TrackStubUtils.createSensorDataSetLocation();
TrackPoint sensorDataSetLocation2 = TrackStubUtils.createSensorDataSetLocation();
/*
* Add a time span here to make sure the second point is valid, the value 222 here is doesn't matter.
@@ -280,12 +280,12 @@ public class ChartFragmentTest {
chartFragment.setMetricUnits(false);
// First data point is not added to the speed buffer
SensorDataSetLocation sensorDataSetLocation1 = TrackStubUtils.createSensorDataSetLocation();
TrackPoint sensorDataSetLocation1 = TrackStubUtils.createSensorDataSetLocation();
sensorDataSetLocation1.setSpeed(100.0f);
double[] point = fillDataPointTestHelper(sensorDataSetLocation1);
Assert.assertEquals(0.0, point[ChartView.SPEED_SERIES + 1], 0.01);
SensorDataSetLocation sensorDataSetLocation2 = TrackStubUtils.createSensorDataSetLocation();
TrackPoint sensorDataSetLocation2 = TrackStubUtils.createSensorDataSetLocation();
/*
* Add a time span here to make sure the second point and the speed is valid.
@@ -306,12 +306,12 @@ public class ChartFragmentTest {
chartFragment.setReportSpeed(false);
// First data point is not added to the speed buffer
SensorDataSetLocation sensorDataSetLocation1 = TrackStubUtils.createSensorDataSetLocation();
TrackPoint sensorDataSetLocation1 = TrackStubUtils.createSensorDataSetLocation();
sensorDataSetLocation1.setSpeed(100.0f);
double[] point = fillDataPointTestHelper(sensorDataSetLocation1);
Assert.assertEquals(0.0, point[ChartView.SPEED_SERIES + 1], 0.01);
SensorDataSetLocation sensorDataSetLocation2 = TrackStubUtils.createSensorDataSetLocation();
TrackPoint sensorDataSetLocation2 = TrackStubUtils.createSensorDataSetLocation();
/*
* Add a time span here to make sure the second point and the speed is valid.
@@ -330,7 +330,7 @@ public class ChartFragmentTest {
public void testFillDataPoint_pace_zeroSpeed() {
// Setups reportSpeed to false.
chartFragment.setReportSpeed(false);
SensorDataSetLocation sensorDataSetLocation = TrackStubUtils.createSensorDataSetLocation();
TrackPoint sensorDataSetLocation = TrackStubUtils.createSensorDataSetLocation();
sensorDataSetLocation.setSpeed(0);
double[] point = fillDataPointTestHelper(sensorDataSetLocation);
Assert.assertEquals(0.0, point[ChartView.PACE_SERIES + 1], 0.01);
@@ -33,10 +33,10 @@ import java.util.Locale;
import java.util.SimpleTimeZone;
import de.dennisguse.opentracks.content.ContentProviderUtils;
import de.dennisguse.opentracks.content.Track;
import de.dennisguse.opentracks.content.TracksColumns;
import de.dennisguse.opentracks.content.Waypoint;
import de.dennisguse.opentracks.content.WaypointsColumns;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.TracksColumns;
import de.dennisguse.opentracks.content.data.Waypoint;
import de.dennisguse.opentracks.content.data.WaypointsColumns;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.eq;
@@ -101,8 +101,6 @@ public abstract class AbstractTestFileTrackImporter {
*/
protected void expectFirstTrackPoint(Location location, long trackId, long trackPointId) {
when(contentProviderUtils.bulkInsertTrackPoint(location != null ? (Location[]) any() : (Location[]) any(), eq(1), eq(trackId))).thenReturn(1);
when(contentProviderUtils.getFirstTrackPointId(trackId)).thenReturn(trackPointId);
when(contentProviderUtils.getLastTrackPointId(trackId)).thenReturn(trackPointId);
}
/**
@@ -128,7 +126,5 @@ public abstract class AbstractTestFileTrackImporter {
if (time != -1L) {
Assert.assertEquals(time, track.getTripStatistics().getStartTime());
}
Assert.assertNotSame(-1, track.getStartId());
Assert.assertNotSame(-1, track.getStopId());
}
}
@@ -0,0 +1,219 @@
package de.dennisguse.opentracks.io.file.importer;
import android.content.Context;
import android.location.Location;
import android.util.Log;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.filters.LargeTest;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import de.dennisguse.opentracks.content.ContentProviderUtils;
import de.dennisguse.opentracks.content.data.TestDataUtil;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.Waypoint;
import de.dennisguse.opentracks.io.file.TrackFileFormat;
import de.dennisguse.opentracks.io.file.exporter.TrackExporter;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
/**
* Export a track to {@link TrackFileFormat} and verify that the import is identical.
* <p>
* TODO: test ignores {@link de.dennisguse.opentracks.stats.TripStatistics} for now.
*/
@RunWith(JUnit4.class)
public class ExportImportTest {
private static final String TAG = ExportImportTest.class.getSimpleName();
private Context context = ApplicationProvider.getApplicationContext();
private ContentProviderUtils contentProviderUtils = new ContentProviderUtils(context);
private static final String TRACK_ICON = "the track icon";
private static final String TRACK_CATEGORY = "the category";
private static final String TRACK_DESCRIPTION = "the description";
private final List<Waypoint> waypoints = new ArrayList<>();
private long importTrackId;
private long trackId = System.currentTimeMillis();
@Before
public void setUp() {
Track track = TestDataUtil.getTrack(trackId, 10);
track.setIcon(TRACK_ICON);
track.setCategory(TRACK_CATEGORY);
track.setDescription(TRACK_DESCRIPTION);
contentProviderUtils.insertTrack(track);
contentProviderUtils.bulkInsertTrackPoint(track.getLocations().toArray(new Location[0]), track.getLocations().size(), track.getId());
for (int i = 0; i < 3; i++) {
Waypoint waypoint = new Waypoint();
waypoint.setName("the waypoint " + i);
waypoint.setDescription("the waypoint description " + i);
waypoint.setCategory("the waypoint category" + i);
waypoint.setIcon("the waypoing icon" + i);
waypoint.setPhotoUrl("the photo url" + i);
waypoint.setTrackId(trackId);
waypoint.setLocation(track.getLocations().get(i));
contentProviderUtils.insertWaypoint(waypoint);
waypoints.add(waypoint);
}
assertEquals(waypoints.size(), contentProviderUtils.getWaypointCount(trackId));
}
@After
public void tearDown() {
contentProviderUtils.deleteTrack(context, trackId);
contentProviderUtils.deleteTrack(context, importTrackId);
}
@LargeTest
@Test
public void kml_only_track() {
// TODO
Log.e(TAG, "Test not implemented.");
}
@LargeTest
@Test
public void kml_with_trackdetail() {
// given
Track track = contentProviderUtils.getTrack(trackId);
TrackFileFormat trackFileFormat = TrackFileFormat.KML_WITH_TRACKDETAIL;
TrackExporter trackExporter = trackFileFormat.newTrackExporter(context, new Track[]{track}, null);
// when
// 1. export
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
trackExporter.writeTrack(context, outputStream);
System.out.println(outputStream.toString());
// 2. import
InputStream inputStream = new ByteArrayInputStream(outputStream.toByteArray());
AbstractFileTrackImporter trackImporter = new KmlFileTrackImporter(context, -1L);
importTrackId = trackImporter.importFile(inputStream);
// then
// 1. track
Track importedTrack = contentProviderUtils.getTrack(importTrackId);
assertNotNull(importedTrack);
assertEquals(track.getLocations(), importedTrack.getLocations());
assertEquals(track.getCategory(), importedTrack.getCategory());
assertEquals(track.getDescription(), importedTrack.getDescription());
assertEquals(track.getName(), importedTrack.getName());
assertEquals(track.getIcon(), importedTrack.getIcon());
// 2. waypoints
assertWaypoints();
//TODO Check absolute time of trackpoints
}
@LargeTest
@Test
public void kml_with_trackdetail_and_sensordata() {
// TODO
Log.e(TAG, "Test not implemented.");
}
@LargeTest
@Test
public void kmz_only_track() {
// TODO
Log.e(TAG, "Test not implemented.");
}
@LargeTest
@Test
public void kmz_with_trackdetail() {
// TODO
Log.e(TAG, "Test not implemented.");
}
@LargeTest
@Test
public void kmz_with_trackdetail_and_sensordata() {
// TODO
Log.e(TAG, "Test not implemented.");
}
@LargeTest
@Test
public void kmz_with_trackdetail_and_sensordata_and_pictures() {
// TODO
Log.e(TAG, "Test not implemented.");
}
@LargeTest
@Test
public void gpx() {
// given
Track track = contentProviderUtils.getTrack(trackId);
TrackFileFormat trackFileFormat = TrackFileFormat.GPX;
TrackExporter trackExporter = trackFileFormat.newTrackExporter(context, new Track[]{track}, null);
// when
// 1. export
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
trackExporter.writeTrack(context, outputStream);
// 2. import
InputStream inputStream = new ByteArrayInputStream(outputStream.toByteArray());
AbstractFileTrackImporter trackImporter = new GpxFileTrackImporter(context, contentProviderUtils);
importTrackId = trackImporter.importFile(inputStream);
// then
// 1. track
Track trackImported = contentProviderUtils.getTrack(importTrackId);
assertNotNull(trackImported);
assertEquals(track.getLocations(), trackImported.getLocations());
assertEquals(track.getCategory(), trackImported.getCategory());
assertEquals(track.getDescription(), trackImported.getDescription());
assertEquals(track.getName(), trackImported.getName());
//TODO exporting and importing a track icon is not yet supported by GpxTrackWriter.
//assertEquals(track.getIcon(), trackImported.getIcon());
// 2. waypoints
assertWaypoints();
//TODO Check absolute time of trackpoints
}
private void assertWaypoints() {
assertEquals(waypoints.size(), contentProviderUtils.getWaypointCount(importTrackId));
List<Waypoint> importedWaypoints = contentProviderUtils.getWaypoints(importTrackId);
for (int i = 0; i < waypoints.size(); i++) {
Waypoint waypoint = waypoints.get(i);
Waypoint importedWaypoint = importedWaypoints.get(i);
assertEquals(waypoint.getCategory(), importedWaypoint.getCategory());
assertEquals(waypoint.getDescription(), importedWaypoint.getDescription());
// assertEquals(waypoint.getIcon(), importedWaypoint.getIcon()); // TODO for KML
assertEquals(waypoint.getName(), importedWaypoint.getName());
assertEquals("", importedWaypoint.getPhotoUrl());
assertEquals(waypoint.getLocation().getLatitude(), importedWaypoint.getLocation().getLatitude(), 0.001);
assertEquals(waypoint.getLocation().getLongitude(), importedWaypoint.getLocation().getLongitude(), 0.001);
assertEquals(waypoint.getLocation().getAltitude(), importedWaypoint.getLocation().getAltitude(), 0.001);
}
}
}
@@ -27,7 +27,7 @@ import org.mockito.junit.MockitoJUnitRunner;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import de.dennisguse.opentracks.content.Track;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.util.PreferencesUtils;
import static org.mockito.Mockito.any;
@@ -208,8 +208,6 @@ public class GpxFileTrackImporterTest extends AbstractTestFileTrackImporter {
// For the following, use StubReturn since we don't care whether they are invoked or not.
when(contentProviderUtils.bulkInsertTrackPoint((Location[]) any(), anyInt(), anyLong())).thenReturn(1);
when(contentProviderUtils.getFirstTrackPointId(TRACK_ID_0)).thenReturn(TRACK_POINT_ID_0);
when(contentProviderUtils.getLastTrackPointId(TRACK_ID_0)).thenReturn(TRACK_POINT_ID_0);
when(contentProviderUtils.getTrack(PreferencesUtils.getRecordingTrackId(context))).thenReturn(null);
contentProviderUtils.deleteTrack(context, TRACK_ID_0);
@@ -27,7 +27,7 @@ import org.mockito.junit.MockitoJUnitRunner;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import de.dennisguse.opentracks.content.Track;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.util.PreferencesUtils;
import static org.mockito.Mockito.any;
@@ -15,14 +15,19 @@
*/
package de.dennisguse.opentracks.services;
import junit.framework.TestCase;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
/**
* Tests the {@link AdaptiveLocationListenerPolicy}.
*
* @author youtaol
*/
public class AdaptiveLocationListenerPolicyTest extends TestCase {
@RunWith(JUnit4.class)
public class AdaptiveLocationListenerPolicyTest {
private static final long MIN = 1000;
private static final long MAX = 3000;
@@ -31,12 +36,11 @@ public class AdaptiveLocationListenerPolicyTest extends TestCase {
private static final long NEW_IDLE_TIME_NORMAL = 5000;
private static final long NEW_IDLE_TIME_SMALL = 2000;
private static final long NEW_IDLE_TIME_LESS_THAN_MIN = 500;
private AdaptiveLocationListenerPolicy adocationListenerPolicy;
private AdaptiveLocationListenerPolicy adaptiveLocationListenerPolicy;
@Override
protected void setUp() throws Exception {
super.setUp();
adocationListenerPolicy = new AdaptiveLocationListenerPolicy(MIN, MAX, MIN_DISTANCE);
@Before
public void setUp() {
adaptiveLocationListenerPolicy = new AdaptiveLocationListenerPolicy(MIN, MAX, MIN_DISTANCE);
}
/**
@@ -48,27 +52,27 @@ public class AdaptiveLocationListenerPolicyTest extends TestCase {
* <li>The newIdleTime is smaller than the smallest interval unit.</li>
* </ul>
*/
@Test
public void testGetDesiredPollingInterval() {
adocationListenerPolicy.updateIdleTime(NEW_IDLE_TIME_BIG);
assertEquals(MAX, adocationListenerPolicy.getDesiredPollingInterval());
adaptiveLocationListenerPolicy.updateIdleTime(NEW_IDLE_TIME_BIG);
Assert.assertEquals(MAX, adaptiveLocationListenerPolicy.getDesiredPollingInterval());
adocationListenerPolicy.updateIdleTime(NEW_IDLE_TIME_NORMAL);
// First get the half of NEW_IDLE_TIME_NORMAL, and then round it to the
// nearest second.
assertEquals((NEW_IDLE_TIME_NORMAL / 2 / 1000) * 1000,
adocationListenerPolicy.getDesiredPollingInterval());
adaptiveLocationListenerPolicy.updateIdleTime(NEW_IDLE_TIME_NORMAL);
// First get the half of NEW_IDLE_TIME_NORMAL, and then round it to the nearest second.
Assert.assertEquals((NEW_IDLE_TIME_NORMAL / 2 / 1000) * 1000, adaptiveLocationListenerPolicy.getDesiredPollingInterval());
adocationListenerPolicy.updateIdleTime(NEW_IDLE_TIME_SMALL);
assertEquals(MIN, adocationListenerPolicy.getDesiredPollingInterval());
adaptiveLocationListenerPolicy.updateIdleTime(NEW_IDLE_TIME_SMALL);
Assert.assertEquals(MIN, adaptiveLocationListenerPolicy.getDesiredPollingInterval());
adocationListenerPolicy.updateIdleTime(NEW_IDLE_TIME_LESS_THAN_MIN);
assertEquals(MIN, adocationListenerPolicy.getDesiredPollingInterval());
adaptiveLocationListenerPolicy.updateIdleTime(NEW_IDLE_TIME_LESS_THAN_MIN);
Assert.assertEquals(MIN, adaptiveLocationListenerPolicy.getDesiredPollingInterval());
}
/**
* Tests the method {@link AdaptiveLocationListenerPolicy#getMinDistance_m()}.
*/
@Test
public void testGetMinDistance() {
assertEquals(MIN_DISTANCE, adocationListenerPolicy.getMinDistance_m());
Assert.assertEquals(MIN_DISTANCE, adaptiveLocationListenerPolicy.getMinDistance_m());
}
}
@@ -44,10 +44,8 @@ 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.Waypoint;
import de.dennisguse.opentracks.content.Waypoint.WaypointType;
import de.dennisguse.opentracks.content.WaypointCreationRequest;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.Waypoint;
import de.dennisguse.opentracks.stats.TripStatistics;
import de.dennisguse.opentracks.util.PreferencesUtils;
@@ -56,7 +54,7 @@ import de.dennisguse.opentracks.util.PreferencesUtils;
*
* @author Bartlomiej Niechwiej
* <p>
* ATTENTION: This tests deletes all stored tracks in the database.
* //TODO 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)
@@ -69,19 +67,14 @@ public class TrackRecordingServiceTest {
public GrantPermissionRule mRuntimePermissionRule = GrantPermissionRule.grant(android.Manifest.permission.ACCESS_FINE_LOCATION);
private Context context = ApplicationProvider.getApplicationContext();
private ContentProviderUtils providerUtils;
private ContentProviderUtils contentProviderUtils;
private long trackId = Math.abs(new Random().nextLong());
private final 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
public void setUp() {
// Set up the mock content resolver
@@ -89,17 +82,14 @@ public class TrackRecordingServiceTest {
};
customContentProvider.attachInfo(context, null);
providerUtils = new ContentProviderUtils(context);
contentProviderUtils = new ContentProviderUtils(context);
// Let's use default values.
SharedPreferences sharedPreferences = PreferencesUtils.getSharedPreferences(context);
sharedPreferences.edit().clear().apply();
// Disable auto resume by default.
updateAutoResumePrefs(context, PreferencesUtils.AUTO_RESUME_TRACK_CURRENT_RETRY_DEFAULT, 0);
// Ensure that the database is empty before every test
providerUtils.deleteAllTracks(context);
contentProviderUtils.deleteAllTracks(context);
}
@After
@@ -111,7 +101,7 @@ public class TrackRecordingServiceTest {
}
// Ensure that the database is empty after every test
providerUtils.deleteAllTracks(context);
contentProviderUtils.deleteAllTracks(context);
}
@SmallTest
@@ -128,128 +118,10 @@ public class TrackRecordingServiceTest {
Assert.assertNotNull(service);
}
@MediumTest
@Test
public void testResumeAfterReboot_shouldResume() throws Exception {
// Insert a dummy track and mark it as recording track.
createDummyTrack(trackId, System.currentTimeMillis(), true);
// Clear the number of attempts and set the timeout to 10 min.
updateAutoResumePrefs(context, PreferencesUtils.AUTO_RESUME_TRACK_CURRENT_RETRY_DEFAULT, Integer.parseInt(context.getResources().getString(R.string.auto_resume_track_timeout_default)));
// Start the service in "resume" mode (simulates the on-reboot action).
Intent startIntent = createStartIntent(context);
startIntent.putExtra(TrackRecordingService.RESUME_TRACK_EXTRA_NAME, true);
mServiceRule.startService(startIntent);
ITrackRecordingService service = ((ITrackRecordingService) mServiceRule.bindService(startIntent));
// then
Assert.assertNotNull(service);
// We expect to resume the previous track.
Assert.assertTrue(service.isRecording());
Assert.assertEquals(trackId, service.getRecordingTrackId());
}
@MediumTest
@Test
public void testResumeAfterReboot_simulateReboot() throws Exception {
updateAutoResumePrefs(context, PreferencesUtils.AUTO_RESUME_TRACK_CURRENT_RETRY_DEFAULT, Integer.parseInt(context.getResources().getString(R.string.auto_resume_track_timeout_default)));
ITrackRecordingService service = ((ITrackRecordingService) mServiceRule.bindService(createStartIntent(context)));
Assert.assertFalse(service.isRecording());
// Simulate recording a track.
long id = service.startNewTrack();
Assert.assertTrue(service.isRecording());
Assert.assertEquals(id, service.getRecordingTrackId());
mServiceRule.unbindService();
Assert.assertEquals(id, PreferencesUtils.getRecordingTrackId(context));
// Start the service in "resume" mode (simulates the on-reboot action).
Intent startIntent = createStartIntent(context);
startIntent.putExtra(TrackRecordingService.RESUME_TRACK_EXTRA_NAME, true);
mServiceRule.startService(startIntent);
service = ((ITrackRecordingService) mServiceRule.bindService(createStartIntent(context)));
// then
Assert.assertNotNull(service);
Assert.assertTrue(service.isRecording());
}
@MediumTest
@Test
public void testResumeAfterReboot_noRecordingTrack() throws Exception {
// Insert a dummy track and mark it as recording track.
createDummyTrack(trackId, System.currentTimeMillis(), false);
// Clear the number of attempts and set the timeout to 10 min.
updateAutoResumePrefs(context, PreferencesUtils.AUTO_RESUME_TRACK_CURRENT_RETRY_DEFAULT, Integer.parseInt(context.getResources().getString(R.string.auto_resume_track_timeout_default)));
// Start the service in "resume" mode (simulates the on-reboot action).
Intent startIntent = createStartIntent(context);
startIntent.putExtra(TrackRecordingService.RESUME_TRACK_EXTRA_NAME, true);
mServiceRule.startService(startIntent);
ITrackRecordingService service = ((ITrackRecordingService) mServiceRule.bindService(startIntent));
// then
Assert.assertNotNull(service);
// We don't expect to resume the previous track, because it was stopped.
Assert.assertFalse(service.isRecording());
Assert.assertEquals(-1L, service.getRecordingTrackId());
}
@MediumTest
@Test
public void testResumeAfterReboot_expiredTrack() throws Exception {
// Insert a dummy track last updated 20 min ago.
createDummyTrack(trackId, System.currentTimeMillis() - 1500 * 60 * 1000, true);
// Clear the number of attempts and set the timeout to 10 min.
updateAutoResumePrefs(context, PreferencesUtils.AUTO_RESUME_TRACK_CURRENT_RETRY_DEFAULT, Integer.parseInt(context.getResources().getString(R.string.auto_resume_track_timeout_default)));
// Start the service in "resume" mode (simulates the on-reboot action).
Intent startIntent = createStartIntent(context);
startIntent.putExtra(TrackRecordingService.RESUME_TRACK_EXTRA_NAME, true);
mServiceRule.startService(startIntent);
ITrackRecordingService service = ((ITrackRecordingService) mServiceRule.bindService(startIntent));
// then
Assert.assertNotNull(service);
// We don't expect to resume the previous track, because it has expired.
Assert.assertFalse(service.isRecording());
Assert.assertEquals(PreferencesUtils.RECORDING_TRACK_ID_DEFAULT, service.getRecordingTrackId());
}
@MediumTest
@Test
public void testResumeAfterReboot_tooManyAttempts() throws Exception {
// Insert a dummy track.
createDummyTrack(trackId, System.currentTimeMillis(), true);
// Set the number of attempts to max.
updateAutoResumePrefs(context, TrackRecordingService.MAX_AUTO_RESUME_TRACK_RETRY_ATTEMPTS, Integer.parseInt(context.getResources().getString(R.string.auto_resume_track_timeout_default)));
// Start the service in "resume" mode (simulates the on-reboot action).
Intent startIntent = createStartIntent(context);
startIntent.putExtra(TrackRecordingService.RESUME_TRACK_EXTRA_NAME, true);
mServiceRule.startService(startIntent);
ITrackRecordingService service = ((ITrackRecordingService) mServiceRule.bindService(startIntent));
//then
Assert.assertNotNull(service);
// We don't expect to resume the previous track, because there were already too many attempts.
Assert.assertFalse(service.isRecording());
Assert.assertEquals(PreferencesUtils.RECORDING_TRACK_ID_DEFAULT, service.getRecordingTrackId());
}
@MediumTest
@Test
public void testRecording_noTracks() throws Exception {
List<Track> tracks = providerUtils.getAllTracks();
List<Track> tracks = contentProviderUtils.getAllTracks();
Assert.assertTrue(tracks.isEmpty());
Intent startIntent = createStartIntent(context);
@@ -289,16 +161,18 @@ public class TrackRecordingServiceTest {
@MediumTest
@Test
public void testStartNewTrack_alreadyRecording() throws Exception {
createDummyTrack(trackId, -1L, true);
ITrackRecordingService service = ((ITrackRecordingService) mServiceRule.bindService(createStartIntent(context)));
service.startNewTrack();
Assert.assertTrue(service.isRecording());
long trackId = service.getRecordingTrackId();
long newTrackId = service.startNewTrack();
Assert.assertEquals(PreferencesUtils.RECORDING_TRACK_ID_DEFAULT, newTrackId);
Assert.assertEquals(trackId, PreferencesUtils.getRecordingTrackId(context));
Assert.assertEquals(trackId, service.getRecordingTrackId());
service.endCurrentTrack();
}
@MediumTest
@@ -314,44 +188,7 @@ public class TrackRecordingServiceTest {
Assert.assertEquals(PreferencesUtils.RECORDING_TRACK_ID_DEFAULT, service.getRecordingTrackId());
}
@MediumTest
@Test
public void testInsertStatisticsMarker_noRecordingTrack() throws Exception {
ITrackRecordingService service = ((ITrackRecordingService) mServiceRule.bindService(createStartIntent(context)));
Assert.assertFalse(service.isRecording());
long waypointId = service.insertWaypoint(WaypointCreationRequest.DEFAULT_STATISTICS);
Assert.assertEquals(-1L, waypointId);
}
@MediumTest
@Test
public void testInsertStatisticsMarker_validLocation() throws Exception {
createDummyTrack(trackId, -1L, true);
ITrackRecordingService service = ((ITrackRecordingService) mServiceRule.bindService(createStartIntent(context)));
Assert.assertTrue(service.isRecording());
Assert.assertFalse(service.isPaused());
insertLocation(service);
long waypointId1 = service.insertWaypoint(WaypointCreationRequest.DEFAULT_STATISTICS);
Assert.assertNotEquals(-1L, waypointId1);
long waypointId2 = service.insertWaypoint(WaypointCreationRequest.DEFAULT_STATISTICS);
Assert.assertNotEquals(-1L, waypointId2);
Waypoint wpt = providerUtils.getWaypoint(waypointId1);
Assert.assertEquals(context.getString(R.string.marker_statistics_icon_url), wpt.getIcon());
Assert.assertEquals(context.getString(R.string.marker_split_name_format, 0), wpt.getName());
Assert.assertEquals(WaypointType.STATISTICS, wpt.getType());
Assert.assertEquals(trackId, wpt.getTrackId());
Assert.assertEquals(0.0, wpt.getLength(), 0.01);
Assert.assertNotNull(wpt.getLocation());
Assert.assertNotNull(wpt.getTripStatistics());
// TODO check the rest of the params.
// TODO: Check waypoint 2.
}
// NOTE: Do not use to create a track that is currently recording.
private void createDummyTrack(long id, long stopTime, boolean isRecording) {
Track dummyTrack = new Track();
dummyTrack.setId(id);
@@ -364,8 +201,8 @@ public class TrackRecordingServiceTest {
private void addTrack(Track track, boolean isRecording) {
Assert.assertTrue(track.getId() >= 0);
providerUtils.insertTrack(track);
Assert.assertEquals(track.getId(), providerUtils.getTrack(track.getId()).getId());
contentProviderUtils.insertTrack(track);
Assert.assertEquals(track.getId(), contentProviderUtils.getTrack(track.getId()).getId());
PreferencesUtils.setLong(context, R.string.recording_track_id_key, isRecording ? track.getId() : PreferencesUtils.RECORDING_TRACK_ID_DEFAULT);
PreferencesUtils.setBoolean(context, R.string.recording_track_paused_key, !isRecording);
}
@@ -392,28 +229,28 @@ public class TrackRecordingServiceTest {
ITrackRecordingService service = ((ITrackRecordingService) mServiceRule.bindService(createStartIntent(context)));
Assert.assertFalse(service.isRecording());
long waypointId = service.insertWaypoint(WaypointCreationRequest.DEFAULT_WAYPOINT);
long waypointId = service.insertWaypoint(null, null, null, null);
Assert.assertEquals(-1L, waypointId);
}
@MediumTest
@Test
public void testInsertWaypointMarker_validWaypoint() throws Exception {
createDummyTrack(trackId, -1L, true);
ITrackRecordingService service = ((ITrackRecordingService) mServiceRule.bindService(createStartIntent(context)));
service.startNewTrack();
Assert.assertTrue(service.isRecording());
insertLocation(service);
long waypointId = service.insertWaypoint(WaypointCreationRequest.DEFAULT_WAYPOINT);
long trackId = service.getRecordingTrackId();
long waypointId = service.insertWaypoint(null, null, null, null);
Assert.assertNotEquals(-1L, waypointId);
Waypoint wpt = providerUtils.getWaypoint(waypointId);
Waypoint wpt = contentProviderUtils.getWaypoint(waypointId);
Assert.assertEquals(context.getString(R.string.marker_waypoint_icon_url), wpt.getIcon());
Assert.assertEquals(context.getString(R.string.marker_name_format, 1), wpt.getName());
Assert.assertEquals(WaypointType.WAYPOINT, wpt.getType());
Assert.assertEquals(trackId, wpt.getTrackId());
Assert.assertEquals(0.0, wpt.getLength(), 0.01);
Assert.assertNotNull(wpt.getLocation());
Assert.assertNull(wpt.getTripStatistics());
service.endCurrentTrack();
}
}
@@ -27,8 +27,7 @@ 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.content.data.Track;
import de.dennisguse.opentracks.stats.TripStatistics;
import de.dennisguse.opentracks.util.PreferencesUtils;
@@ -74,9 +73,6 @@ public class TrackRecordingServiceTestLooper {
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);
}
@@ -151,27 +147,6 @@ public class TrackRecordingServiceTestLooper {
fullRecordingSession();
}
@MediumTest
@Test
public void testWithProperties_splitFrequencyDefault() throws TimeoutException {
PreferencesUtils.setInt(context, R.string.split_frequency_key, Integer.parseInt(context.getResources().getString(R.string.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 {
@@ -242,10 +217,8 @@ public class TrackRecordingServiceTestLooper {
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);
if (i % 7 == 0) {
service.insertWaypoint(null, null, null, null);
}
}
@@ -15,7 +15,11 @@
*/
package de.dennisguse.opentracks.stats;
import junit.framework.TestCase;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
/**
* Tests for {@link TripStatistics}.
@@ -23,26 +27,17 @@ import junit.framework.TestCase;
*
* @author Rodrigo Damazio
*/
public class TripStatisticsTest extends TestCase {
@RunWith(JUnit4.class)
public class TripStatisticsTest {
private TripStatistics statistics;
@Override
protected void setUp() throws Exception {
super.setUp();
@Before
public void setUp() {
statistics = new TripStatistics();
}
public void testSetBounds() {
// This is not a trivial setter, conversion happens in it
statistics.setBounds(12345, -34567, 56789, -98765);
assertEquals(12345, statistics.getLeft());
assertEquals(-34567, statistics.getTop());
assertEquals(56789, statistics.getRight());
assertEquals(-98765, statistics.getBottom());
}
@Test
public void testMerge() {
TripStatistics statistics2 = new TripStatistics();
statistics.setStartTime(1000L); // Resulting start time
@@ -68,38 +63,32 @@ public class TripStatisticsTest extends TestCase {
statistics2.setMaxGrade(35.0); // Resulting max grade
statistics2.setMinGrade(0.0);
// Resulting bounds: -10000, 35000, 30000, -40000
statistics.setBounds(-10000, 20000, 30000, -40000);
statistics2.setBounds(-5000, 35000, 0, 20000);
statistics.merge(statistics2);
assertEquals(1000L, statistics.getStartTime());
assertEquals(4000L, statistics.getStopTime());
assertEquals(2500L, statistics.getTotalTime());
assertEquals(1300L, statistics.getMovingTime());
assertEquals(1100.0, statistics.getTotalDistance());
assertEquals(900.0, statistics.getTotalElevationGain());
assertEquals(statistics.getTotalDistance() / (statistics.getMovingTime() / 1000.0), statistics.getMaxSpeed());
assertEquals(-10000, statistics.getLeft());
assertEquals(30000, statistics.getRight());
assertEquals(35000, statistics.getTop());
assertEquals(-40000, statistics.getBottom());
assertEquals(1200.0, statistics.getMinElevation());
assertEquals(3575.0, statistics.getMaxElevation());
assertEquals(-25.0, statistics.getMinGrade());
assertEquals(35.0, statistics.getMaxGrade());
Assert.assertEquals(1000L, statistics.getStartTime());
Assert.assertEquals(4000L, statistics.getStopTime());
Assert.assertEquals(2500L, statistics.getTotalTime());
Assert.assertEquals(1300L, statistics.getMovingTime());
Assert.assertEquals(1100.0, statistics.getTotalDistance(), 0.001);
Assert.assertEquals(900.0, statistics.getTotalElevationGain(), 0.001);
Assert.assertEquals(statistics.getTotalDistance() / (statistics.getMovingTime() / 1000.0), statistics.getMaxSpeed(), 0.001);
Assert.assertEquals(1200.0, statistics.getMinElevation(), 0.001);
Assert.assertEquals(3575.0, statistics.getMaxElevation(), 0.001);
Assert.assertEquals(-25.0, statistics.getMinGrade(), 0.001);
Assert.assertEquals(35.0, statistics.getMaxGrade(), 0.001);
}
@Test
public void testGetAverageSpeed() {
statistics.setTotalDistance(1000.0);
statistics.setTotalTime(50000); // in milliseconds
assertEquals(20.0, statistics.getAverageSpeed());
Assert.assertEquals(20.0, statistics.getAverageSpeed(), 0.001);
}
@Test
public void testGetAverageMovingSpeed() {
statistics.setTotalDistance(1000.0);
statistics.setMovingTime(20000); // in milliseconds
assertEquals(50.0, statistics.getAverageMovingSpeed());
Assert.assertEquals(50.0, statistics.getAverageMovingSpeed(), 0.001);
}
}
@@ -15,9 +15,10 @@
*/
package de.dennisguse.opentracks.util;
import android.os.Environment;
import junit.framework.TestCase;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import java.io.File;
@@ -26,83 +27,83 @@ import java.io.File;
*
* @author Rodrigo Damazio
*/
public class FileUtilsTest extends TestCase {
/**
* Tests {@link FileUtils#getPath(String...)}.
*/
public void testBuildExternalDirectoryPath() {
String expectedName = Environment.getExternalStorageDirectory() + File.separator + FileUtils.SDCARD_TOP_DIR + File.separator + "a" + File.separator + "b" + File.separator + "c";
String dirName = FileUtils.getPath("a", "b", "c");
assertEquals(expectedName, dirName);
}
@RunWith(JUnit4.class)
public class FileUtilsTest {
/**
* Tests {@link FileUtils#buildUniqueFileName(File, String, String)} when the file is new.
*/
@Test
public void testBuildUniqueFileName_new() {
String filename = FileUtils.buildUniqueFileName(new File("/dir"), "Filename", "ext");
assertEquals("Filename.ext", filename);
Assert.assertEquals("Filename.ext", filename);
}
/**
* Tests {@link FileUtils#buildUniqueFileName(File, String, String)} when the file exists already.
*/
@Test
public void testBuildUniqueFileName_exist() {
// Expect "/default.prop" to exist on the phone/emulator
String filename = FileUtils.buildUniqueFileName(new File("/"), "default", "prop");
assertEquals("default(1).prop", filename);
Assert.assertEquals("default(1).prop", filename);
}
/**
* Tests {@link FileUtils#sanitizeFileName(String)} with special characters.
* Verifies that they are sanitized.
*/
@Test
public void testSanitizeFileName() {
String name = "Swim\10ming-^across:/the/ pacific (ocean).";
String expected = "Swim_ming-^across_the_ pacific (ocean).";
assertEquals(expected, FileUtils.sanitizeFileName(name));
Assert.assertEquals(expected, FileUtils.sanitizeFileName(name));
}
@Test
public void testSanitizeFileNameWithSuffix() {
String name = "FileName.jpeg";
assertEquals(name, FileUtils.sanitizeFileName(name));
Assert.assertEquals(name, FileUtils.sanitizeFileName(name));
}
/**
* Tests {@link FileUtils#sanitizeFileName(String)} with i18n characters (in Chinese and Russian).
* Verifies that they are allowed.
*/
@Test
public void testSanitizeFileName_i18n() {
String name = "您好-привет";
String expected = "您好-привет";
assertEquals(expected, FileUtils.sanitizeFileName(name));
Assert.assertEquals(expected, FileUtils.sanitizeFileName(name));
}
/**
* Tests {@link FileUtils#sanitizeFileName(String)} with special FAT32 characters.
* Verifies that they are allowed.
*/
@Test
public void testSanitizeFileName_special_characters() {
String name = "$%'-_@~`!(){}^#&+,;=[] ";
String expected = "$%'-_@~`!(){}^#&+,;=[] ";
assertEquals(expected, FileUtils.sanitizeFileName(name));
Assert.assertEquals(expected, FileUtils.sanitizeFileName(name));
}
/**
* Tests {@link FileUtils#sanitizeFileName(String)} with multiple escaped characters in a row.
* Verifies that they are collapsed into one underscore.
*/
@Test
public void testSanitizeFileName_collapse() {
String name = "hello//there";
String expected = "hello_there";
assertEquals(expected, FileUtils.sanitizeFileName(name));
Assert.assertEquals(expected, FileUtils.sanitizeFileName(name));
}
/**
* Tests {@link FileUtils#truncateFileName(File, String, String)}.
* Verifies the a long file name is truncated.
*/
@Test
public void testTruncateFileName() {
File directory = new File("/dir1/dir2/");
String suffix = ".gpx";
@@ -114,8 +115,8 @@ public class FileUtilsTest extends TestCase {
String truncated = FileUtils.truncateFileName(directory, nameString, suffix);
for (int i = 0; i < truncated.length(); i++) {
assertEquals('a', truncated.charAt(i));
Assert.assertEquals('a', truncated.charAt(i));
}
assertEquals(FileUtils.MAX_FAT32_PATH_LENGTH, new File(directory, truncated + suffix).getPath().length());
Assert.assertEquals(FileUtils.MAX_FAT32_PATH_LENGTH, new File(directory, truncated + suffix).getPath().length());
}
}
+8 -41
View File
@@ -16,21 +16,22 @@ limitations under the License.
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="de.dennisguse.opentracks"
android:installLocation="auto"
android:versionCode="3162"
android:versionName="v3.2.8"> <!-- For FDroid. -->
android:versionCode="3251"
android:versionName="v3.3.1"> <!-- For FDroid. -->
<!-- Permission to initialize services -->
<application
android:name=".Startup"
android:allowBackup="false"
android:hasFragileUserData="true"
android:hardwareAccelerated="true"
android:icon="@drawable/ic_logo_color_24dp"
android:label="@string/app_name"
android:icon="${applicationLogo}"
android:label="${applicationName}"
android:theme="@style/ThemeCustom">
<!-- Activities -->
<activity android:name=".AboutActivity" />
<activity android:name=".DeleteActivity" />
<activity android:name=".TrackDeleteActivity" />
<activity
android:name=".HelpActivity"
@@ -83,9 +84,7 @@ limitations under the License.
<activity android:name=".TrackEditActivity" />
<activity
android:name=".TrackListActivity"
android:screenOrientation="userPortrait">
<activity android:name=".TrackListActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
@@ -101,7 +100,6 @@ limitations under the License.
<activity android:name=".settings.SettingsActivity" />
<!-- Services -->
<provider
android:name=".content.ShareContentProvider"
android:authorities="${applicationId}.content"
@@ -118,14 +116,6 @@ limitations under the License.
android:resource="@xml/file_paths" />
</provider>
<!-- Receivers -->
<receiver android:name=".BootReceiver">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
<!-- Providers -->
<service
android:name=".services.TrackRecordingService"
android:enabled="true"
@@ -145,44 +135,21 @@ limitations under the License.
android:name="android.hardware.location.gps"
android:required="true" />
<uses-feature
android:name="android.hardware.bluetooth"
android:name="android.hardware.android.hardware.bluetooth_le"
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>
@@ -76,8 +76,8 @@ public abstract class AbstractListActivity extends AbstractActivity implements C
if (stopRecording) {
getTrackRecordingServiceConnection().stopRecording(this, false);
}
Intent intent = IntentUtils.newIntent(this, DeleteActivity.class);
intent.putExtra(DeleteActivity.EXTRA_TRACK_IDS, trackIds);
Intent intent = IntentUtils.newIntent(this, TrackDeleteActivity.class);
intent.putExtra(TrackDeleteActivity.EXTRA_TRACK_IDS, trackIds);
startActivityForResult(intent, DELETE_REQUEST_CODE);
}
@@ -88,7 +88,7 @@ public abstract class AbstractListActivity extends AbstractActivity implements C
abstract protected TrackRecordingServiceConnection getTrackRecordingServiceConnection();
/**
* Called after {@link DeleteActivity} returns its result.
* Called after {@link TrackDeleteActivity} returns its result.
*/
abstract protected void onDeleted();
}
@@ -1,57 +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;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.util.Log;
import de.dennisguse.opentracks.services.TrackRecordingService;
import static android.content.Intent.ACTION_BOOT_COMPLETED;
/**
* This class react to the BOOT_COMPLETED broadcast.
* <p>
* One example of a broadcast message that this class is interested in,
* is notification about the phone boot. We may want to resume a previously
* started tracking session if the phone crashed (hopefully not), or the user
* decided to swap the battery or some external event occurred which forced
* a phone reboot.
* <p>
* This class simply delegates to {@link TrackRecordingService} to make a
* decision whether to continue with the previous track (if any), or just
* abandon it.
*
* @author Bartlomiej Niechwiej
*/
public class BootReceiver extends BroadcastReceiver {
private static final String TAG = BootReceiver.class.getSimpleName();
@Override
public void onReceive(Context context, Intent intent) {
Log.d(TAG, "BootReceiver.onReceive: " + intent.getAction());
if (ACTION_BOOT_COMPLETED.equals(intent.getAction())) {
Intent startIntent = new Intent(context, TrackRecordingService.class)
.putExtra(TrackRecordingService.RESUME_TRACK_EXTRA_NAME, true);
context.startService(startIntent);
} else {
Log.w(TAG, "BootReceiver: unsupported action");
}
}
}
@@ -30,7 +30,7 @@ public class DeleteAsyncTask extends AsyncTask<Void, Integer, Boolean> {
private final long[] trackIds;
private final Context context;
private DeleteActivity deleteActivity;
private TrackDeleteActivity deleteActivity;
// true if the AsyncTask has completed
private boolean completed;
@@ -41,7 +41,7 @@ public class DeleteAsyncTask extends AsyncTask<Void, Integer, Boolean> {
* @param trackIds the track ids to delete. To delete all, set to size 1 with
* trackIds[0] == -1L
*/
public DeleteAsyncTask(DeleteActivity deleteActivity, long[] trackIds) {
public DeleteAsyncTask(TrackDeleteActivity deleteActivity, long[] trackIds) {
this.deleteActivity = deleteActivity;
this.trackIds = trackIds;
context = deleteActivity.getApplicationContext();
@@ -53,7 +53,7 @@ public class DeleteAsyncTask extends AsyncTask<Void, Integer, Boolean> {
*
* @param deleteActivity the current activity, can be null
*/
public void setActivity(DeleteActivity deleteActivity) {
public void setActivity(TrackDeleteActivity deleteActivity) {
this.deleteActivity = deleteActivity;
if (completed && deleteActivity != null) {
deleteActivity.onAsyncTaskCompleted();
@@ -31,7 +31,7 @@ import java.util.ArrayList;
import java.util.List;
import de.dennisguse.opentracks.content.ContentProviderUtils;
import de.dennisguse.opentracks.content.Waypoint;
import de.dennisguse.opentracks.content.data.Waypoint;
import de.dennisguse.opentracks.fragments.DeleteMarkerDialogFragment.DeleteMarkerCaller;
import de.dennisguse.opentracks.fragments.MarkerDetailFragment;
@@ -71,12 +71,9 @@ public class MarkerDetailActivity extends AbstractActivity implements DeleteMark
if (cursor != null && cursor.moveToFirst()) {
for (int i = 0; i < cursor.getCount(); i++) {
Waypoint currentMarker = contentProviderUtils.createWaypoint(cursor);
if (!currentMarker.isTripStatistics()) {
markerIds.add(currentMarker.getId());
if (currentMarker.getId() == markerId) {
markerIndex = markerIds.size() - 1;
}
markerIds.add(currentMarker.getId());
if (currentMarker.getId() == markerId) {
markerIndex = markerIds.size() - 1;
}
cursor.moveToNext();
@@ -26,9 +26,7 @@ import android.widget.Button;
import android.widget.EditText;
import de.dennisguse.opentracks.content.ContentProviderUtils;
import de.dennisguse.opentracks.content.Waypoint;
import de.dennisguse.opentracks.content.Waypoint.WaypointType;
import de.dennisguse.opentracks.content.WaypointCreationRequest;
import de.dennisguse.opentracks.content.data.Waypoint;
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
/**
@@ -48,9 +46,6 @@ public class MarkerEditActivity extends AbstractActivity {
private Waypoint waypoint;
// UI elements
private View statisticsSection;
private EditText statisticsName;
private View waypointSection;
private EditText waypointName;
private AutoCompleteTextView waypointMarkerType;
private EditText waypointDescription;
@@ -65,14 +60,9 @@ public class MarkerEditActivity extends AbstractActivity {
trackRecordingServiceConnection = new TrackRecordingServiceConnection(this, null);
// Setup UI elements
statisticsSection = findViewById(R.id.marker_edit_statistics_section);
statisticsName = findViewById(R.id.marker_edit_statistics_name);
waypointSection = findViewById(R.id.marker_edit_waypoint_section);
waypointName = findViewById(R.id.marker_edit_waypoint_name);
waypointMarkerType = findViewById(R.id.marker_edit_waypoint_marker_type);
ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(
this, R.array.waypoint_types, android.R.layout.simple_dropdown_item_1line);
ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(this, R.array.waypoint_types, android.R.layout.simple_dropdown_item_1line);
waypointMarkerType.setAdapter(adapter);
waypointDescription = findViewById(R.id.marker_edit_waypoint_description);
@@ -96,7 +86,7 @@ public class MarkerEditActivity extends AbstractActivity {
@Override
protected void onStop() {
super.onStop();
trackRecordingServiceConnection.unbind();
trackRecordingServiceConnection.unbind(this);
}
@Override
@@ -125,9 +115,7 @@ public class MarkerEditActivity extends AbstractActivity {
});
if (newMarker) {
statisticsSection.setVisibility(View.GONE);
waypointSection.setVisibility(View.VISIBLE);
int nextWaypointNumber = trackId == -1L ? -1 : new ContentProviderUtils(this).getNextWaypointNumber(trackId, WaypointType.WAYPOINT);
int nextWaypointNumber = trackId == -1L ? -1 : new ContentProviderUtils(this).getNextWaypointNumber(trackId);
if (nextWaypointNumber == -1) {
nextWaypointNumber = 0;
}
@@ -142,16 +130,9 @@ public class MarkerEditActivity extends AbstractActivity {
finish();
return;
}
boolean statistics = waypoint.getType() == WaypointType.STATISTICS;
statisticsSection.setVisibility(statistics ? View.VISIBLE : View.GONE);
waypointSection.setVisibility(statistics ? View.GONE : View.VISIBLE);
if (statistics) {
statisticsName.setText(waypoint.getName());
} else {
waypointName.setText(waypoint.getName());
waypointMarkerType.setText(waypoint.getCategory());
waypointDescription.setText(waypoint.getDescription());
}
waypointName.setText(waypoint.getName());
waypointMarkerType.setText(waypoint.getCategory());
waypointDescription.setText(waypoint.getDescription());
}
}
@@ -159,29 +140,21 @@ public class MarkerEditActivity extends AbstractActivity {
* Adds a marker.
*/
private void addMarker() {
WaypointCreationRequest waypointCreationRequest = new WaypointCreationRequest(
WaypointType.WAYPOINT,
false,
trackRecordingServiceConnection.addMarker(this,
waypointName.getText().toString(),
waypointMarkerType.getText().toString(),
waypointDescription.getText().toString(),
null,
null);
trackRecordingServiceConnection.addMarker(this, waypointCreationRequest);
}
/**
* Saves a marker.
*/
private void saveMarker() {
boolean statistics = waypoint.getType() == WaypointType.STATISTICS;
if (statistics) {
waypoint.setName(statisticsName.getText().toString());
} else {
waypoint.setName(waypointName.getText().toString());
waypoint.setCategory(waypointMarkerType.getText().toString());
waypoint.setDescription(waypointDescription.getText().toString());
}
waypoint.setName(waypointName.getText().toString());
waypoint.setCategory(waypointMarkerType.getText().toString());
waypoint.setDescription(waypointDescription.getText().toString());
new ContentProviderUtils(this).updateWaypoint(waypoint);
}
}
@@ -38,14 +38,14 @@ import androidx.loader.content.CursorLoader;
import androidx.loader.content.Loader;
import de.dennisguse.opentracks.content.ContentProviderUtils;
import de.dennisguse.opentracks.content.Track;
import de.dennisguse.opentracks.content.Waypoint.WaypointType;
import de.dennisguse.opentracks.content.WaypointsColumns;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.WaypointsColumns;
import de.dennisguse.opentracks.fragments.DeleteMarkerDialogFragment;
import de.dennisguse.opentracks.fragments.DeleteMarkerDialogFragment.DeleteMarkerCaller;
import de.dennisguse.opentracks.util.ActivityUtils;
import de.dennisguse.opentracks.util.IntentUtils;
import de.dennisguse.opentracks.util.ListItemUtils;
import de.dennisguse.opentracks.util.MarkerUtils;
import de.dennisguse.opentracks.util.PreferencesUtils;
/**
@@ -74,8 +74,7 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
* Note that sharedPreferenceChangeListener cannot be an anonymous inner
* class. Anonymous inner class will get garbage collected.
*/
private final OnSharedPreferenceChangeListener
sharedPreferenceChangeListener = new OnSharedPreferenceChangeListener() {
private final OnSharedPreferenceChangeListener sharedPreferenceChangeListener = new OnSharedPreferenceChangeListener() {
@Override
public void onSharedPreferenceChanged(SharedPreferences preferences, String key) {
// Note that the key can be null
@@ -127,17 +126,14 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setDefaultKeyMode(DEFAULT_KEYS_SEARCH_LOCAL);
long trackId = getIntent().getLongExtra(EXTRA_TRACK_ID, -1L);
recordingTrackPaused = PreferencesUtils.isRecordingTrackPausedDefault(this);
contentProviderUtils = new ContentProviderUtils(this);
sharedPreferences = PreferencesUtils.getSharedPreferences(this);
long trackId = getIntent().getLongExtra(EXTRA_TRACK_ID, -1L);
track = trackId != -1L ? contentProviderUtils.getTrack(trackId) : null;
final long trackFirstWaypointId = trackId != -1 ? contentProviderUtils.getFirstWaypointId(trackId) : -1;
setDefaultKeyMode(DEFAULT_KEYS_SEARCH_LOCAL);
listView = findViewById(R.id.marker_list);
listView.setEmptyView(findViewById(R.id.marker_list_empty));
@@ -152,7 +148,6 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
resourceCursorAdapter = new ResourceCursorAdapter(this, R.layout.list_item, null, 0) {
@Override
public void bindView(View view, Context context, Cursor cursor) {
int typeIndex = cursor.getColumnIndex(WaypointsColumns.TYPE);
int nameIndex = cursor.getColumnIndex(WaypointsColumns.NAME);
int timeIndex = cursor.getColumnIndexOrThrow(WaypointsColumns.TIME);
int categoryIndex = cursor.getColumnIndex(WaypointsColumns.CATEGORY);
@@ -161,13 +156,13 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
int latitudeIndex = cursor.getColumnIndex(WaypointsColumns.LATITUDE);
int longitudeIndex = cursor.getColumnIndex(WaypointsColumns.LONGITUDE);
boolean statistics = WaypointType.values()[cursor.getInt(typeIndex)] == WaypointType.STATISTICS;
int iconId = statistics ? R.drawable.ic_marker_yellow_pushpin : R.drawable.ic_marker_blue_pushpin;
int iconId = MarkerUtils.ICON_ID;
String name = cursor.getString(nameIndex);
long time = cursor.getLong(timeIndex);
String category = statistics ? null : cursor.getString(categoryIndex);
String description = statistics ? null : cursor.getString(descriptionIndex);
String category = cursor.getString(categoryIndex);
String description = cursor.getString(descriptionIndex);
String photoUrl = cursor.getString(photoUrlIndex);
//TODO also show latitude and longitude in list
double latitude = cursor.getDouble(latitudeIndex);
double longitude = cursor.getDouble(longitudeIndex);
@@ -183,8 +178,8 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
public Loader<Cursor> onCreateLoader(int arg0, Bundle arg1) {
if (track != null) {
return new CursorLoader(MarkerListActivity.this, WaypointsColumns.CONTENT_URI, PROJECTION,
WaypointsColumns.TRACKID + "=? AND " + WaypointsColumns._ID + "!=?",
new String[]{String.valueOf(track.getId()), String.valueOf(trackFirstWaypointId)}, null);
WaypointsColumns.TRACKID + "=?",
new String[]{String.valueOf(track.getId())}, null);
} else {
return new CursorLoader(MarkerListActivity.this, WaypointsColumns.CONTENT_URI, PROJECTION,
WaypointsColumns.STARTTIME + " IS NULL", null, null);
@@ -246,7 +241,7 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
if (track != null && item.getItemId() == R.id.marker_list_insert_marker) {
Intent intent = IntentUtils.newIntent(this, MarkerEditActivity.class)
.putExtra(MarkerEditActivity.EXTRA_TRACK_ID, track.getId());
@@ -44,9 +44,8 @@ import de.dennisguse.opentracks.content.ContentProviderUtils;
import de.dennisguse.opentracks.content.SearchEngine;
import de.dennisguse.opentracks.content.SearchEngine.ScoredResult;
import de.dennisguse.opentracks.content.SearchEngine.SearchQuery;
import de.dennisguse.opentracks.content.Track;
import de.dennisguse.opentracks.content.Waypoint;
import de.dennisguse.opentracks.content.Waypoint.WaypointType;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.Waypoint;
import de.dennisguse.opentracks.fragments.ConfirmDeleteDialogFragment;
import de.dennisguse.opentracks.fragments.DeleteMarkerDialogFragment;
import de.dennisguse.opentracks.fragments.DeleteMarkerDialogFragment.DeleteMarkerCaller;
@@ -55,6 +54,7 @@ import de.dennisguse.opentracks.stats.TripStatistics;
import de.dennisguse.opentracks.util.ActivityUtils;
import de.dennisguse.opentracks.util.IntentUtils;
import de.dennisguse.opentracks.util.ListItemUtils;
import de.dennisguse.opentracks.util.MarkerUtils;
import de.dennisguse.opentracks.util.PreferencesUtils;
import de.dennisguse.opentracks.util.StringUtils;
import de.dennisguse.opentracks.util.TrackIconUtils;
@@ -245,7 +245,7 @@ public class SearchListActivity extends AbstractListActivity implements DeleteMa
protected void onStop() {
super.onStop();
sharedPreferences.unregisterOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
trackRecordingServiceConnection.unbind();
trackRecordingServiceConnection.unbind(this);
}
@Override
@@ -393,11 +393,9 @@ public class SearchListActivity extends AbstractListActivity implements DeleteMa
}
}
boolean statistics = waypoint.getType() == WaypointType.STATISTICS;
resultMap.put(IS_RECORDING_FIELD, false);
resultMap.put(IS_PAUSED_FIELD, true);
resultMap.put(ICON_ID_FIELD, statistics ? R.drawable.ic_marker_yellow_pushpin : R.drawable.ic_marker_blue_pushpin);
resultMap.put(ICON_ID_FIELD, MarkerUtils.ICON_ID);
resultMap.put(ICON_CONTENT_DESCRIPTION_ID_FIELD, R.string.image_marker);
resultMap.put(NAME_FIELD, waypoint.getName());
// Display the marker's track name in the total time field
@@ -405,8 +403,8 @@ public class SearchListActivity extends AbstractListActivity implements DeleteMa
resultMap.put(TOTAL_DISTANCE_FIELD, null);
resultMap.put(MARKER_COUNT_FIELD, 0);
resultMap.put(START_TIME_FIELD, waypoint.getLocation().getTime());
resultMap.put(CATEGORY_FIELD, statistics ? null : waypoint.getCategory());
resultMap.put(DESCRIPTION_FIELD, statistics ? null : waypoint.getDescription());
resultMap.put(CATEGORY_FIELD, waypoint.getCategory());
resultMap.put(DESCRIPTION_FIELD, waypoint.getDescription());
resultMap.put(PHOTO_URL_FIELD, waypoint.getPhotoUrl());
resultMap.put(TRACK_ID_FIELD, waypoint.getTrackId());
resultMap.put(MARKER_ID_FIELD, waypoint.getId());
@@ -1,153 +0,0 @@
/*
* Copyright 2013q 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.content.Context;
import android.os.Bundle;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TabHost;
import android.widget.TabHost.TabContentFactory;
import android.widget.TabWidget;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import androidx.fragment.app.FragmentPagerAdapter;
import androidx.viewpager.widget.ViewPager;
import java.util.ArrayList;
/**
* This is a helper class that implements the management of tabs and all details
* of connecting a ViewPager with associated TabHost. It relies on a trick.
* Normally a tab host has a simple API for supplying a View or Intent that each
* tab will show. This is not sufficient for switching between pages. So instead
* we make the content part of the tab host 0dp high (it is not shown) and the
* TabsAdapter supplies its own dummy view to show as the tab content. It
* listens to changes in tabs, and takes care of switch to the correct paged in
* the ViewPager whenever the selected tab changes.
* <p>
* Copied from the FragmentTabsPager sample in the support library.
*
* @author Jimmy Shih
*/
//TODO Rework or better remove.
public class TabsAdapter extends FragmentPagerAdapter implements TabHost.OnTabChangeListener, ViewPager.OnPageChangeListener {
private final Context context;
private final TabHost tabHost;
private final ViewPager viewPager;
private final ArrayList<TabInfo> tabInfos = new ArrayList<>();
public TabsAdapter(FragmentActivity activity, TabHost tabHost, ViewPager viewPager) {
super(activity.getSupportFragmentManager());
this.context = activity;
this.tabHost = tabHost;
this.viewPager = viewPager;
this.tabHost.setOnTabChangedListener(this);
this.viewPager.setAdapter(this);
this.viewPager.setOnPageChangeListener(this);
}
public void addTab(TabHost.TabSpec tabSpec, Class<?> clss, Bundle bundle) {
tabSpec.setContent(new DummyTabFactory(context));
TabInfo info = new TabInfo(clss, bundle);
tabInfos.add(info);
tabHost.addTab(tabSpec);
notifyDataSetChanged();
}
@Override
public int getCount() {
return tabInfos.size();
}
@Override
@NonNull
public Fragment getItem(int position) {
TabInfo info = tabInfos.get(position);
return Fragment.instantiate(context, info.clss.getName(), info.bundle);
}
@Override
public void onTabChanged(String tabId) {
int position = tabHost.getCurrentTab();
viewPager.setCurrentItem(position);
}
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
}
@Override
public void onPageSelected(int position) {
/*
* Unfortunately when TabHost changes the current tab, it kindly also takes care of putting focus on it when not in touch mode.
* The jerk. This hack tries to prevent this from pulling focus out of our ViewPager.
*/
TabWidget tabWidget = tabHost.getTabWidget();
int oldFocusability = tabWidget.getDescendantFocusability();
tabWidget.setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS);
tabHost.setCurrentTab(position);
tabWidget.setDescendantFocusability(oldFocusability);
}
@Override
public void onPageScrollStateChanged(int state) {
}
/**
* An object to hold a tab's info.
*
* @author Jimmy Shih
*/
private static final class TabInfo {
private final Class<?> clss;
private final Bundle bundle;
TabInfo(Class<?> clss, Bundle bundle) {
this.clss = clss;
this.bundle = bundle;
}
}
/**
* A dummy {@link TabContentFactory} that creates an empty view to satisfy the {@link TabHost} API.
*
* @author Jimmy Shih
*/
private static class DummyTabFactory implements TabHost.TabContentFactory {
private final Context context;
DummyTabFactory(Context context) {
this.context = context;
}
@Override
public View createTabContent(String tag) {
View view = new View(context);
view.setMinimumWidth(0);
view.setMinimumHeight(0);
return view;
}
}
}
@@ -23,14 +23,13 @@ import android.content.Intent;
import android.os.Bundle;
import de.dennisguse.opentracks.util.DialogUtils;
import de.dennisguse.opentracks.R;
/**
* An activity for delete tracks.
*
* @author Jimmy Shih
*/
public class DeleteActivity extends Activity {
public class TrackDeleteActivity extends Activity {
public static final String EXTRA_TRACK_IDS = "track_ids";
@@ -31,20 +31,21 @@ import android.view.MenuItem;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.WindowManager;
import android.widget.TabHost;
import android.widget.TabHost.TabSpec;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.app.ActivityCompat;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentPagerAdapter;
import androidx.viewpager.widget.ViewPager;
import com.google.android.material.tabs.TabLayout;
import de.dennisguse.opentracks.content.ContentProviderUtils;
import de.dennisguse.opentracks.content.Track;
import de.dennisguse.opentracks.content.TrackDataHub;
import de.dennisguse.opentracks.content.Waypoint;
import de.dennisguse.opentracks.content.Waypoint.WaypointType;
import de.dennisguse.opentracks.content.WaypointCreationRequest;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.Waypoint;
import de.dennisguse.opentracks.fragments.ChartDistanceFragment;
import de.dennisguse.opentracks.fragments.ChartTimeFragment;
import de.dennisguse.opentracks.fragments.ChooseActivityTypeDialogFragment;
@@ -52,10 +53,8 @@ import de.dennisguse.opentracks.fragments.ConfirmDeleteDialogFragment;
import de.dennisguse.opentracks.fragments.StatsFragment;
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
import de.dennisguse.opentracks.settings.SettingsActivity;
import de.dennisguse.opentracks.util.FileUtils;
import de.dennisguse.opentracks.util.IntentUtils;
import de.dennisguse.opentracks.util.PreferencesUtils;
import de.dennisguse.opentracks.util.StatsUtils;
import de.dennisguse.opentracks.util.TrackIconUtils;
import de.dennisguse.opentracks.util.TrackUtils;
@@ -87,7 +86,7 @@ public class TrackDetailActivity extends AbstractListActivity implements ChooseA
private SharedPreferences sharedPreferences;
private TrackRecordingServiceConnection trackRecordingServiceConnection;
private TrackDataHub trackDataHub;
private TabHost tabHost;
private ViewPager pager;
private TrackController trackController;
// From intent
@@ -107,36 +106,37 @@ public class TrackDetailActivity extends AbstractListActivity implements ChooseA
trackController.update(isRecording(), recordingTrackPaused);
if (hasPhoto && photoUri != null) {
hasPhoto = false;
WaypointCreationRequest waypointCreationRequest = new WaypointCreationRequest(WaypointType.WAYPOINT, false, null, null, null, null, photoUri.toString());
long markerId = trackRecordingServiceConnection.addMarker(TrackDetailActivity.this, waypointCreationRequest);
if (markerId != -1L) {
//TODO: Make configurable.
FileUtils.updateMediaScanner(TrackDetailActivity.this, photoUri);
}
long markerId = trackRecordingServiceConnection.addMarker(TrackDetailActivity.this, null, null, null, photoUri.toString());
}
}
});
}
};
/*
* Note that sharedPreferenceChangeListener cannot be an anonymous inner
* class. Anonymous inner class will get garbage collected.
*/
// Note that sharedPreferenceChangeListener cannot be an anonymous inner class. Anonymous inner class will get garbage collected.
private final OnSharedPreferenceChangeListener sharedPreferenceChangeListener = new OnSharedPreferenceChangeListener() {
@Override
public void onSharedPreferenceChanged(SharedPreferences preferences, String key) {
if (PreferencesUtils.isKey(TrackDetailActivity.this, R.string.recording_track_id_key, key)) {
recordingTrackId = PreferencesUtils.getRecordingTrackId(TrackDetailActivity.this);
setLockscreenPolicy();
setScreenOnPolicy();
}
if (PreferencesUtils.isKey(TrackDetailActivity.this, R.string.recording_track_paused_key, key)) {
recordingTrackPaused = PreferencesUtils.isRecordingTrackPaused(TrackDetailActivity.this);
setLockscreenPolicy();
setScreenOnPolicy();
}
if (PreferencesUtils.isKey(TrackDetailActivity.this, R.string.stats_show_on_lockscreen_while_recording_key, key)) {
setLockscreenPolicy();
}
if (PreferencesUtils.isKey(TrackDetailActivity.this, R.string.stats_keep_screen_on_while_recording_key, key)) {
setScreenOnPolicy();
}
if (key == null) return;
runOnUiThread(new Runnable() {
@@ -197,28 +197,46 @@ public class TrackDetailActivity extends AbstractListActivity implements ChooseA
trackRecordingServiceConnection = new TrackRecordingServiceConnection(this, bindChangedCallback);
trackDataHub = TrackDataHub.newInstance(this);
tabHost = findViewById(R.id.tackdetail_tabhost);
tabHost.setup();
FragmentPagerAdapter adapter = new FragmentPagerAdapter(getSupportFragmentManager(), 1) {
@Override
public int getCount() {
return 3;
}
ViewPager viewPager = findViewById(R.id.pager);
TabsAdapter tabsAdapter = new TabsAdapter(this, tabHost, viewPager);
TabSpec statsTabSpec = tabHost.newTabSpec(StatsFragment.STATS_FRAGMENT_TAG).setIndicator(getString(R.string.track_detail_stats_tab));
tabsAdapter.addTab(statsTabSpec, StatsFragment.class, null);
TabSpec chartTimeTabSpec = tabHost.newTabSpec(ChartTimeFragment.CHART_FRAGMENT_TAG).setIndicator(getString(R.string.settings_chart_by_time));
tabsAdapter.addTab(chartTimeTabSpec, ChartTimeFragment.class, null);
TabSpec chartDistanceTabSpec = tabHost.newTabSpec(ChartDistanceFragment.CHART_FRAGMENT_TAG).setIndicator(getString(R.string.settings_chart_by_distance));
tabsAdapter.addTab(chartDistanceTabSpec, ChartDistanceFragment.class, null);
@NonNull
@Override
public Fragment getItem(int position) {
switch (position) {
case 0:
return new StatsFragment();
case 1:
return new ChartTimeFragment();
case 2:
return new ChartDistanceFragment();
}
return null;
}
@Nullable
@Override
public CharSequence getPageTitle(int position) {
switch (position) {
case 0:
return getString(R.string.track_detail_stats_tab);
case 1:
return getString(R.string.settings_chart_by_time);
case 2:
return getString(R.string.settings_chart_by_distance);
}
return "Unknown Tab";
}
};
pager = findViewById(R.id.track_detail_activity_view_pager);
pager.setAdapter(adapter);
TabLayout tabs = findViewById(R.id.track_detail_activity_tablayout);
tabs.setupWithViewPager(pager);
if (savedInstanceState != null) {
tabHost.setCurrentTabByTag(savedInstanceState.getString(CURRENT_TAB_TAG_KEY));
}
// Set the background after all three tabs are added
for (int i = 0; i < tabHost.getTabWidget().getChildCount(); i++) {
tabHost.getTabWidget().getChildAt(i).setBackgroundResource(R.drawable.tab_indicator);
pager.setCurrentItem(savedInstanceState.getInt(CURRENT_TAB_TAG_KEY));
}
trackController = new TrackController(this, trackRecordingServiceConnection, false, recordListener, stopListener);
@@ -227,11 +245,13 @@ public class TrackDetailActivity extends AbstractListActivity implements ChooseA
@Override
public void onAttachedToWindow() {
setLockscreenPolicy();
setScreenOnPolicy();
super.onAttachedToWindow();
}
private void setLockscreenPolicy() {
boolean showOnLockScreen = PreferencesUtils.shouldShowStatsOnLockscreen(TrackDetailActivity.this);
boolean showOnLockScreen = PreferencesUtils.shouldShowStatsOnLockscreen(TrackDetailActivity.this)
&& PreferencesUtils.isRecording(this);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
setShowWhenLocked(showOnLockScreen);
@@ -242,6 +262,17 @@ public class TrackDetailActivity extends AbstractListActivity implements ChooseA
}
}
private void setScreenOnPolicy() {
boolean keepScreenOn = PreferencesUtils.shouldKeepScreenOn(TrackDetailActivity.this)
&& PreferencesUtils.isRecording(this);
if (keepScreenOn) {
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
} else {
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
}
}
@Override
protected void onStart() {
super.onStart();
@@ -273,14 +304,15 @@ public class TrackDetailActivity extends AbstractListActivity implements ChooseA
protected void onStop() {
super.onStop();
sharedPreferences.unregisterOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
trackRecordingServiceConnection.unbind();
trackRecordingServiceConnection.unbind(this);
trackDataHub.stop();
}
@Override
protected void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
outState.putString(CURRENT_TAB_TAG_KEY, tabHost.getCurrentTabTag());
outState.putInt(CURRENT_TAB_TAG_KEY, pager.getCurrentItem());
if (photoUri != null) {
outState.putParcelable(PHOTO_URI_KEY, photoUri);
}
@@ -346,10 +378,6 @@ public class TrackDetailActivity extends AbstractListActivity implements ChooseA
startActivity(intent);
return true;
case R.id.track_detail_insert_photo:
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);
return false;
}
createWaypointWithPicture();
return true;
case R.id.track_detail_menu_show_on_map:
@@ -413,22 +441,6 @@ public class TrackDetailActivity extends AbstractListActivity implements ChooseA
return trackDataHub;
}
/**
* Gets the track id.
*/
// TODO Seems to be no used.
@Deprecated
public long getTrackId() {
return trackId;
}
private void setBluetoothHeartRateSensorName(String sensorName) {
StatsUtils.setBluetoothHeartRateSensorName(this, sensorName, isRecording());
}
/**
* Handles the data in the intent.
*/
private void handleIntent(Intent intent) {
trackId = intent.getLongExtra(EXTRA_TRACK_ID, -1L);
long markerId = intent.getLongExtra(EXTRA_MARKER_ID, -1L);
@@ -31,7 +31,7 @@ import android.widget.Spinner;
import androidx.annotation.NonNull;
import de.dennisguse.opentracks.content.ContentProviderUtils;
import de.dennisguse.opentracks.content.Track;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.fragments.ChooseActivityTypeDialogFragment;
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
import de.dennisguse.opentracks.util.TrackIconUtils;
@@ -175,7 +175,7 @@ public class TrackEditActivity extends AbstractActivity implements ChooseActivit
@Override
protected void onStop() {
super.onStop();
trackRecordingServiceConnection.unbind();
trackRecordingServiceConnection.unbind(this);
}
@Override
@@ -51,7 +51,7 @@ import androidx.loader.content.Loader;
import java.util.Locale;
import de.dennisguse.opentracks.content.ContentProviderUtils;
import de.dennisguse.opentracks.content.TracksColumns;
import de.dennisguse.opentracks.content.data.TracksColumns;
import de.dennisguse.opentracks.fragments.ConfirmDeleteDialogFragment;
import de.dennisguse.opentracks.services.ITrackRecordingService;
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
@@ -141,7 +141,7 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
if (PreferencesUtils.isKey(TrackListActivity.this, R.string.recording_track_id_key, key)) {
recordingTrackId = PreferencesUtils.getRecordingTrackId(TrackListActivity.this);
if (key != null && PreferencesUtils.isRecording(recordingTrackId)) {
trackRecordingServiceConnection.startAndBind();
trackRecordingServiceConnection.startAndBind(TrackListActivity.this);
}
}
if (PreferencesUtils.isKey(TrackListActivity.this, R.string.recording_track_paused_key, key)) {
@@ -164,7 +164,7 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
// Menu items
private MenuItem searchMenuItem;
private MenuItem startGpsMenuItem;
private MenuItem deleteAllMenuItem;
// private MenuItem deleteAllMenuItem;
private final OnClickListener stopListener = new OnClickListener() {
@Override
@@ -344,7 +344,7 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
super.onStop();
sharedPreferences.unregisterOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
trackRecordingServiceConnection.unbind();
trackRecordingServiceConnection.unbind(this);
}
@Override
@@ -371,7 +371,6 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
ActivityUtils.configureSearchWidget(this, searchMenuItem, trackController);
startGpsMenuItem = menu.findItem(R.id.track_list_start_gps);
deleteAllMenuItem = menu.findItem(R.id.track_list_delete_all);
return super.onCreateOptionsMenu(menu);
}
@@ -407,14 +406,14 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
// Invoke trackRecordingService
if (startGps) {
trackRecordingServiceConnection.startAndBind();
trackRecordingServiceConnection.startAndBind(this);
bindChangedCallback.run();
} else {
ITrackRecordingService trackRecordingService = trackRecordingServiceConnection.getServiceIfBound();
if (trackRecordingService != null) {
trackRecordingService.stopGps();
}
trackRecordingServiceConnection.unbindAndStop();
trackRecordingServiceConnection.unbindAndStop(this);
}
// Update menu after starting or stopping gps
@@ -425,9 +424,6 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
intent = IntentUtils.newIntent(this, MarkerListActivity.class);
startActivity(intent);
return true;
case R.id.track_list_delete_all:
deleteTracks(new long[]{-1L});
return true;
case R.id.track_list_settings:
intent = IntentUtils.newIntent(this, SettingsActivity.class);
startActivity(intent);
@@ -495,9 +491,6 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
startGpsMenuItem.setIcon(isGpsStarted ? R.drawable.ic_gps_fixed_24dp : R.drawable.ic_gps_off_24dp);
}
}
if (deleteAllMenuItem != null) {
deleteAllMenuItem.setVisible(hasTrack && !isRecording);
}
}
/**
@@ -505,7 +498,7 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
*/
private void startRecording() {
startNewRecording = true;
trackRecordingServiceConnection.startAndBind();
trackRecordingServiceConnection.startAndBind(this);
/*
* If the binding has happened, then invoke the callback to start a new recording.
@@ -23,8 +23,6 @@ import android.graphics.Paint.Align;
import android.graphics.Paint.Style;
import android.graphics.Path;
import androidx.annotation.VisibleForTesting;
import java.text.NumberFormat;
import de.dennisguse.opentracks.stats.ExtremityMonitor;
@@ -203,7 +201,6 @@ class ChartValueSeries {
/**
* Gets the interval value.
*/
@VisibleForTesting
int getInterval() {
return interval;
}
@@ -218,7 +215,6 @@ class ChartValueSeries {
/**
* Gets the max marker value.
*/
@VisibleForTesting
int getMaxMarkerValue() {
return maxMarkerValue;
}
@@ -37,12 +37,14 @@ import androidx.core.view.GestureDetectorCompat;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.List;
import de.dennisguse.opentracks.MarkerDetailActivity;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.Waypoint;
import de.dennisguse.opentracks.content.data.Waypoint;
import de.dennisguse.opentracks.stats.ExtremityMonitor;
import de.dennisguse.opentracks.util.IntentUtils;
import de.dennisguse.opentracks.util.MarkerUtils;
import de.dennisguse.opentracks.util.StringUtils;
import de.dennisguse.opentracks.util.UnitConversions;
@@ -55,10 +57,10 @@ import de.dennisguse.opentracks.util.UnitConversions;
*/
public class ChartView extends View {
public static final float MEDIUM_TEXT_SIZE = 18f;
public static final float SMALL_TEXT_SIZE = 12f;
static final float MEDIUM_TEXT_SIZE = 18f;
static final float SMALL_TEXT_SIZE = 12f;
public static final int Y_AXIS_INTERVALS = 5;
static final int Y_AXIS_INTERVALS = 5;
public static final int NUM_SERIES = 6;
public static final int ELEVATION_SERIES = 0;
@@ -79,19 +81,24 @@ public class ChartView extends View {
private static final int SPACER = 4;
private static final int Y_AXIS_OFFSET = 16;
//TODO Determine from actual size of the used drawable
private static final float WAYPOINT_X_ANCHOR = 13f / 48f;
static {
X_FRACTION_FORMAT.setMaximumFractionDigits(1);
X_FRACTION_FORMAT.setMinimumFractionDigits(1);
}
private final ChartValueSeries[] series = new ChartValueSeries[NUM_SERIES];
private final ArrayList<double[]> chartData = new ArrayList<>();
private final ArrayList<Waypoint> waypoints = new ArrayList<>();
private final List<double[]> chartData = new ArrayList<>();
private final List<Waypoint> waypoints = new ArrayList<>();
private final ExtremityMonitor xExtremityMonitor = new ExtremityMonitor();
private final Paint axisPaint;
private final Paint xAxisMarkerPaint;
private final Paint gridPaint;
private final Paint markerPaint;
private final Drawable pointer;
private final Drawable waypointMarker;
private final int markerWidth;
private final int markerHeight;
private final Scroller scroller;
@@ -145,7 +152,7 @@ public class ChartView extends View {
@Override
public boolean onSingleTapConfirmed(MotionEvent event) {
// Check if the y event is within markerHeight of the marker center
if (Math.abs(event.getY() - topBorder - spacer - markerHeight / 2) < markerHeight) {
if (Math.abs(event.getY() - topBorder - spacer - markerHeight / 2f) < markerHeight) {
int minDistance = Integer.MAX_VALUE;
Waypoint nearestWaypoint = null;
synchronized (waypoints) {
@@ -255,7 +262,7 @@ public class ChartView extends View {
gridPaint.setAntiAlias(false);
gridPaint.setPathEffect(new DashPathEffect(new float[]{3, 2}, 0));
Paint markerPaint = new Paint();
markerPaint = new Paint();
markerPaint.setStyle(Style.STROKE);
markerPaint.setColor(context.getResources().getColor(android.R.color.darker_gray));
markerPaint.setAntiAlias(false);
@@ -263,12 +270,9 @@ public class ChartView extends View {
pointer = context.getResources().getDrawable(R.drawable.ic_logo_color_24dp);
pointer.setBounds(0, 0, pointer.getIntrinsicWidth(), pointer.getIntrinsicHeight());
Drawable statisticsMarker = getResources().getDrawable(R.drawable.ic_marker_yellow_pushpin);
markerWidth = statisticsMarker.getIntrinsicWidth();
markerHeight = statisticsMarker.getIntrinsicHeight();
statisticsMarker.setBounds(0, 0, markerWidth, markerHeight);
Drawable waypointMarker = getResources().getDrawable(R.drawable.ic_marker_blue_pushpin);
waypointMarker = MarkerUtils.getDefaultPhoto(context);
markerWidth = waypointMarker.getIntrinsicWidth();
markerHeight = waypointMarker.getIntrinsicHeight();
waypointMarker.setBounds(0, 0, markerWidth, markerHeight);
scroller = new Scroller(context);
@@ -323,11 +327,10 @@ public class ChartView extends View {
*
* @param dataPoints an array of data points to be added
*/
public void addDataPoints(ArrayList<double[]> dataPoints) {
public void addDataPoints(List<double[]> dataPoints) {
synchronized (chartData) {
chartData.addAll(dataPoints);
for (int i = 0; i < dataPoints.size(); i++) {
double[] dataPoint = dataPoints.get(i);
for (double[] dataPoint : dataPoints) {
xExtremityMonitor.update(dataPoint[0]);
for (int j = 0; j < series.length; j++) {
if (!Double.isNaN(dataPoint[j + 1])) {
@@ -492,6 +495,7 @@ public class ChartView extends View {
clipToGraphArea(canvas);
drawDataSeries(canvas);
drawWaypoints(canvas);
drawGrid(canvas);
canvas.restore();
@@ -532,6 +536,33 @@ public class ChartView extends View {
}
}
/**
* Draws the waypoints.
*
* @param canvas the canvas
*/
private void drawWaypoints(Canvas canvas) {
synchronized (waypoints) {
for (int i = 0; i < waypoints.size(); i++) {
final Waypoint waypoint = waypoints.get(i);
if (waypoint.getLocation() == null) {
continue;
}
double xValue = getWaypointXValue(waypoint);
if (xValue > maxX) {
continue;
}
canvas.save();
float x = getX(getWaypointXValue(waypoint));
canvas.drawLine(x, topBorder + spacer + markerHeight / 2, x, topBorder + effectiveHeight, markerPaint);
canvas.translate(x - (markerWidth * WAYPOINT_X_ANCHOR), topBorder + spacer);
waypointMarker.draw(canvas);
canvas.restore();
}
}
}
/**
* Draws the grid.
*
@@ -539,9 +570,9 @@ public class ChartView extends View {
*/
private void drawGrid(Canvas canvas) {
// X axis grid
ArrayList<Double> xAxisMarkerPositions = getXAxisMarkerPositions(getXAxisInterval());
for (int i = 0; i < xAxisMarkerPositions.size(); i++) {
int x = getX(xAxisMarkerPositions.get(i));
List<Double> xAxisMarkerPositions = getXAxisMarkerPositions(getXAxisInterval());
for (double position : xAxisMarkerPositions) {
int x = getX(position);
canvas.drawLine(x, topBorder, x, topBorder + effectiveHeight, gridPaint);
}
// Y axis grid
@@ -613,10 +644,10 @@ public class ChartView extends View {
canvas.drawText(label, x + effectiveWidth + spacer, y + yOffset, axisPaint);
double interval = getXAxisInterval();
ArrayList<Double> markerPositions = getXAxisMarkerPositions(interval);
NumberFormat numberFormat = interval < 1 ? X_FRACTION_FORMAT : X_NUMBER_FORMAT;
for (int i = 0; i < markerPositions.size(); i++) {
drawXAxisMarker(canvas, markerPositions.get(i), numberFormat, spacer + yOffset);
for (double markerPosition : getXAxisMarkerPositions(interval)) {
drawXAxisMarker(canvas, markerPosition, numberFormat, spacer + yOffset);
}
}
@@ -666,8 +697,8 @@ public class ChartView extends View {
/**
* Gets the x axis marker positions.
*/
private ArrayList<Double> getXAxisMarkerPositions(double interval) {
ArrayList<Double> markers = new ArrayList<>();
private List<Double> getXAxisMarkerPositions(double interval) {
List<Double> markers = new ArrayList<>();
markers.add(0d);
for (int i = 1; i * interval < maxX; i++) {
markers.add(i * interval);
@@ -777,8 +808,7 @@ public class ChartView extends View {
private void drawPaths() {
boolean[] hasMoved = new boolean[series.length];
for (int i = 0; i < chartData.size(); i++) {
double[] dataPoint = chartData.get(i);
for (double[] dataPoint : chartData) {
for (int j = 0; j < series.length; j++) {
double value = dataPoint[j + 1];
if (Double.isNaN(value)) {
@@ -35,7 +35,12 @@ import java.util.NoSuchElementException;
import de.dennisguse.opentracks.BuildConfig;
import de.dennisguse.opentracks.android.ContentResolverWrapper;
import de.dennisguse.opentracks.android.IContentResolver;
import de.dennisguse.opentracks.content.Waypoint.WaypointType;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.content.data.TrackPointsColumns;
import de.dennisguse.opentracks.content.data.TracksColumns;
import de.dennisguse.opentracks.content.data.Waypoint;
import de.dennisguse.opentracks.content.data.WaypointsColumns;
import de.dennisguse.opentracks.content.sensor.SensorDataSet;
import de.dennisguse.opentracks.stats.TripStatistics;
import de.dennisguse.opentracks.util.FileUtils;
@@ -51,11 +56,6 @@ public class ContentProviderUtils {
private static final String TAG = ContentProviderUtils.class.getSimpleName();
private static final int MAX_LATITUDE = 90000000;
/**
* Maximum number of waypoints that will be loaded at one time.
*/
public static int MAX_LOADED_WAYPOINTS_POINTS = 10000;
/**
* The authority (the first part of the URI) for the app's content provider.
*/
@@ -64,7 +64,12 @@ public class ContentProviderUtils {
/**
* The base URI for the app's content provider.
*/
static String CONTENT_BASE_URI = "content://" + AUTHORITY_PACKAGE;
public static String CONTENT_BASE_URI = "content://" + AUTHORITY_PACKAGE;
/**
* Maximum number of waypoints that will be loaded at one time.
*/
public static int MAX_LOADED_WAYPOINTS_POINTS = 10000;
private final IContentResolver contentResolver;
private int defaultCursorBatchSize = 2000;
@@ -112,10 +117,6 @@ public class ContentProviderUtils {
int totalDistanceIndex = cursor.getColumnIndexOrThrow(TracksColumns.TOTALDISTANCE);
int totalTimeIndex = cursor.getColumnIndexOrThrow(TracksColumns.TOTALTIME);
int movingTimeIndex = cursor.getColumnIndexOrThrow(TracksColumns.MOVINGTIME);
int minLatIndex = cursor.getColumnIndexOrThrow(TracksColumns.MINLAT);
int maxLatIndex = cursor.getColumnIndexOrThrow(TracksColumns.MAXLAT);
int minLonIndex = cursor.getColumnIndexOrThrow(TracksColumns.MINLON);
int maxLonIndex = cursor.getColumnIndexOrThrow(TracksColumns.MAXLON);
int maxSpeedIndex = cursor.getColumnIndexOrThrow(TracksColumns.MAXSPEED);
int minElevationIndex = cursor.getColumnIndexOrThrow(TracksColumns.MINELEVATION);
int maxElevationIndex = cursor.getColumnIndexOrThrow(TracksColumns.MAXELEVATION);
@@ -138,12 +139,6 @@ public class ContentProviderUtils {
if (!cursor.isNull(categoryIndex)) {
track.setCategory(cursor.getString(categoryIndex));
}
if (!cursor.isNull(startIdIndex)) {
track.setStartId(cursor.getLong(startIdIndex));
}
if (!cursor.isNull(stopIdIndex)) {
track.setStopId(cursor.getLong(stopIdIndex));
}
if (!cursor.isNull(startTimeIndex)) {
tripStatistics.setStartTime(cursor.getLong(startTimeIndex));
}
@@ -162,13 +157,6 @@ public class ContentProviderUtils {
if (!cursor.isNull(movingTimeIndex)) {
tripStatistics.setMovingTime(cursor.getLong(movingTimeIndex));
}
if (!cursor.isNull(minLatIndex) && !cursor.isNull(maxLatIndex) && !cursor.isNull(minLonIndex) && !cursor.isNull(maxLonIndex)) {
int bottom = cursor.getInt(minLatIndex);
int top = cursor.getInt(maxLatIndex);
int left = cursor.getInt(minLonIndex);
int right = cursor.getInt(maxLonIndex);
tripStatistics.setBounds(left, top, right, bottom);
}
if (!cursor.isNull(maxSpeedIndex)) {
tripStatistics.setMaxSpeed(cursor.getFloat(maxSpeedIndex));
}
@@ -202,7 +190,7 @@ public class ContentProviderUtils {
// Delete tracks last since it triggers a database vaccum call
contentResolver.delete(TracksColumns.CONTENT_URI, null, null);
File dir = FileUtils.getPhotoDir();
File dir = FileUtils.getPhotoDir(context);
deleteDirectoryRecurse(context, dir);
}
@@ -220,23 +208,19 @@ public class ContentProviderUtils {
}
/**
* Deletes track points and waypoints of a track. Assumes
* {@link TracksColumns#STARTID}, {@link TracksColumns#STOPID}, and
* {@link TracksColumns#NUMPOINTS} will be updated by the caller.
* Deletes track points and waypoints of a track.
* Assumes {@link TracksColumns#NUMPOINTS} will be updated by the caller.
*
* @param trackId the track id
*/
private void deleteTrackPointsAndWaypoints(Context context, long trackId) {
Track track = getTrack(trackId);
if (track != null) {
String where = TrackPointsColumns._ID + ">=? AND " + TrackPointsColumns._ID + "<=?";
String[] selectionArgs = new String[]{
Long.toString(track.getStartId()), Long.toString(track.getStopId())};
contentResolver.delete(TrackPointsColumns.CONTENT_URI, where, selectionArgs);
}
String where = TrackPointsColumns.TRACKID + "=?";
String[] selectionArgs = new String[]{Long.toString(trackId)};
contentResolver.delete(TrackPointsColumns.CONTENT_URI, where, selectionArgs);
contentResolver.delete(WaypointsColumns.CONTENT_URI, WaypointsColumns.TRACKID + "=?",
new String[]{Long.toString(trackId)});
deleteDirectoryRecurse(context, FileUtils.getPhotoDir(trackId));
deleteDirectoryRecurse(context, FileUtils.getPhotoDir(context, trackId));
}
/**
@@ -245,14 +229,11 @@ public class ContentProviderUtils {
* @param dir the directory
*/
private void deleteDirectoryRecurse(Context context, File dir) {
if (FileUtils.isDirectory(dir)) {
if (dir.exists() && dir.isDirectory()) {
for (File child : dir.listFiles()) {
deleteDirectoryRecurse(context, child);
}
}
if (dir.exists()) {
dir.delete();
FileUtils.updateMediaScanner(context, Uri.fromFile(dir));
}
}
@@ -350,18 +331,12 @@ public class ContentProviderUtils {
values.put(TracksColumns.NAME, track.getName());
values.put(TracksColumns.DESCRIPTION, track.getDescription());
values.put(TracksColumns.CATEGORY, track.getCategory());
values.put(TracksColumns.STARTID, track.getStartId());
values.put(TracksColumns.STOPID, track.getStopId());
values.put(TracksColumns.STARTTIME, tripStatistics.getStartTime());
values.put(TracksColumns.STOPTIME, tripStatistics.getStopTime());
values.put(TracksColumns.NUMPOINTS, track.getNumberOfPoints());
values.put(TracksColumns.TOTALDISTANCE, tripStatistics.getTotalDistance());
values.put(TracksColumns.TOTALTIME, tripStatistics.getTotalTime());
values.put(TracksColumns.MOVINGTIME, tripStatistics.getMovingTime());
values.put(TracksColumns.MINLAT, tripStatistics.getBottom());
values.put(TracksColumns.MAXLAT, tripStatistics.getTop());
values.put(TracksColumns.MINLON, tripStatistics.getLeft());
values.put(TracksColumns.MAXLON, tripStatistics.getRight());
values.put(TracksColumns.AVGSPEED, tripStatistics.getAverageSpeed());
values.put(TracksColumns.AVGMOVINGSPEED, tripStatistics.getAverageMovingSpeed());
values.put(TracksColumns.MAXSPEED, tripStatistics.getMaxSpeed());
@@ -399,12 +374,8 @@ public class ContentProviderUtils {
int categoryIndex = cursor.getColumnIndexOrThrow(WaypointsColumns.CATEGORY);
int iconIndex = cursor.getColumnIndexOrThrow(WaypointsColumns.ICON);
int trackIdIndex = cursor.getColumnIndexOrThrow(WaypointsColumns.TRACKID);
int typeIndex = cursor.getColumnIndexOrThrow(WaypointsColumns.TYPE);
int lengthIndex = cursor.getColumnIndexOrThrow(WaypointsColumns.LENGTH);
int durationIndex = cursor.getColumnIndexOrThrow(WaypointsColumns.DURATION);
int startTimeIndex = cursor.getColumnIndexOrThrow(WaypointsColumns.STARTTIME);
int startIdIndex = cursor.getColumnIndexOrThrow(WaypointsColumns.STARTID);
int stopIdIndex = cursor.getColumnIndexOrThrow(WaypointsColumns.STOPID);
int longitudeIndex = cursor.getColumnIndexOrThrow(WaypointsColumns.LONGITUDE);
int latitudeIndex = cursor.getColumnIndexOrThrow(WaypointsColumns.LATITUDE);
int timeIndex = cursor.getColumnIndexOrThrow(WaypointsColumns.TIME);
@@ -412,15 +383,6 @@ public class ContentProviderUtils {
int accuracyIndex = cursor.getColumnIndexOrThrow(WaypointsColumns.ACCURACY);
int speedIndex = cursor.getColumnIndexOrThrow(WaypointsColumns.SPEED);
int bearingIndex = cursor.getColumnIndexOrThrow(WaypointsColumns.BEARING);
int totalDistanceIndex = cursor.getColumnIndexOrThrow(WaypointsColumns.TOTALDISTANCE);
int totalTimeIndex = cursor.getColumnIndexOrThrow(WaypointsColumns.TOTALTIME);
int movingTimeIndex = cursor.getColumnIndexOrThrow(WaypointsColumns.MOVINGTIME);
int maxSpeedIndex = cursor.getColumnIndexOrThrow(WaypointsColumns.MAXSPEED);
int minElevationIndex = cursor.getColumnIndexOrThrow(WaypointsColumns.MINELEVATION);
int maxElevationIndex = cursor.getColumnIndexOrThrow(WaypointsColumns.MAXELEVATION);
int elevationGainIndex = cursor.getColumnIndexOrThrow(WaypointsColumns.ELEVATIONGAIN);
int minGradeIndex = cursor.getColumnIndexOrThrow(WaypointsColumns.MINGRADE);
int maxGradeIndex = cursor.getColumnIndexOrThrow(WaypointsColumns.MAXGRADE);
int photoUrlIndex = cursor.getColumnIndexOrThrow(WaypointsColumns.PHOTOURL);
Waypoint waypoint = new Waypoint();
@@ -443,21 +405,12 @@ public class ContentProviderUtils {
if (!cursor.isNull(trackIdIndex)) {
waypoint.setTrackId(cursor.getLong(trackIdIndex));
}
if (!cursor.isNull(typeIndex)) {
waypoint.setType(WaypointType.values()[cursor.getInt(typeIndex)]);
}
if (!cursor.isNull(lengthIndex)) {
waypoint.setLength(cursor.getFloat(lengthIndex));
}
if (!cursor.isNull(durationIndex)) {
waypoint.setDuration(cursor.getLong(durationIndex));
}
if (!cursor.isNull(startIdIndex)) {
waypoint.setStartId(cursor.getLong(startIdIndex));
}
if (!cursor.isNull(stopIdIndex)) {
waypoint.setStopId(cursor.getLong(stopIdIndex));
}
Location location = new Location("");
if (!cursor.isNull(longitudeIndex) && !cursor.isNull(latitudeIndex)) {
@@ -481,52 +434,6 @@ public class ContentProviderUtils {
}
waypoint.setLocation(location);
TripStatistics tripStatistics = new TripStatistics();
boolean hasTripStatistics = false;
if (!cursor.isNull(startTimeIndex)) {
tripStatistics.setStartTime(cursor.getLong(startTimeIndex));
hasTripStatistics = true;
}
if (!cursor.isNull(totalDistanceIndex)) {
tripStatistics.setTotalDistance(cursor.getFloat(totalDistanceIndex));
hasTripStatistics = true;
}
if (!cursor.isNull(totalTimeIndex)) {
tripStatistics.setTotalTime(cursor.getLong(totalTimeIndex));
hasTripStatistics = true;
}
if (!cursor.isNull(movingTimeIndex)) {
tripStatistics.setMovingTime(cursor.getLong(movingTimeIndex));
hasTripStatistics = true;
}
if (!cursor.isNull(maxSpeedIndex)) {
tripStatistics.setMaxSpeed(cursor.getFloat(maxSpeedIndex));
hasTripStatistics = true;
}
if (!cursor.isNull(minElevationIndex)) {
tripStatistics.setMinElevation(cursor.getFloat(minElevationIndex));
hasTripStatistics = true;
}
if (!cursor.isNull(maxElevationIndex)) {
tripStatistics.setMaxElevation(cursor.getFloat(maxElevationIndex));
hasTripStatistics = true;
}
if (!cursor.isNull(elevationGainIndex)) {
tripStatistics.setTotalElevationGain(cursor.getFloat(elevationGainIndex));
hasTripStatistics = true;
}
if (!cursor.isNull(minGradeIndex)) {
tripStatistics.setMinGrade(cursor.getFloat(minGradeIndex));
hasTripStatistics = true;
}
if (!cursor.isNull(maxGradeIndex)) {
tripStatistics.setMaxGrade(cursor.getFloat(maxGradeIndex));
hasTripStatistics = true;
}
if (hasTripStatistics) {
waypoint.setTripStatistics(tripStatistics);
}
if (!cursor.isNull(photoUrlIndex)) {
waypoint.setPhotoUrl(cursor.getString(photoUrlIndex));
}
@@ -538,33 +445,17 @@ public class ContentProviderUtils {
* If deleting a statistics waypoint, this will also correct the next statistics waypoint after the deleted one to reflect the deletion.
* The generator is used to update the next statistics waypoint.
*
* @param waypointId the waypoint id
* @param descriptionGenerator the description generator. Can be null for waypoint marker
* @param waypointId the waypoint id
*/
public void deleteWaypoint(Context context, long waypointId, DescriptionGenerator descriptionGenerator) {
public void deleteWaypoint(Context context, long waypointId) {
final Waypoint waypoint = getWaypoint(waypointId);
if (waypoint != null && waypoint.getType() == WaypointType.STATISTICS
&& descriptionGenerator != null) {
final Waypoint nextWaypoint = getNextStatisticsWaypointAfter(waypoint);
if (nextWaypoint == null) {
Log.d(TAG, "Unable to find the next statistics marker after deleting one.");
} else {
nextWaypoint.getTripStatistics().merge(waypoint.getTripStatistics());
nextWaypoint.setDescription(
descriptionGenerator.generateWaypointDescription(nextWaypoint.getTripStatistics()));
if (!updateWaypoint(nextWaypoint)) {
Log.e(TAG, "Unable to update the next statistics marker after deleting one.");
}
}
}
if (waypoint != null && waypoint.hasPhoto()) {
Uri uri = waypoint.getPhotoURI();
File file = new File(uri.getPath());
if (file.exists()) {
File parent = file.getParentFile();
file.delete();
FileUtils.updateMediaScanner(context, uri);
if (parent.listFiles().length == 0) {
parent.delete();
}
@@ -574,36 +465,16 @@ public class ContentProviderUtils {
}
/**
* Gets the first waypoint id for a track.
* The first waypoint is special as it contains the stats for the track.
* Returns -1L if it doesn't exist.
* Gets the last waypoint for a type. Returns null if it doesn't exist.
*
* @param trackId the track id
*/
public long getFirstWaypointId(long trackId) {
if (trackId < 0) {
return -1L;
}
try (Cursor cursor = getWaypointCursor(new String[]{WaypointsColumns._ID}, WaypointsColumns.TRACKID + "=?", new String[]{Long.toString(trackId)}, WaypointsColumns._ID, 1)) {
if (cursor != null && cursor.moveToFirst()) {
return cursor.getLong(cursor.getColumnIndexOrThrow(WaypointsColumns._ID));
}
}
return -1L;
}
/**
* Gets the last waypoint for a type. Returns null if it doesn't exist.
*
* @param trackId the track id
* @param waypointType the waypoint type
*/
public Waypoint getLastWaypoint(long trackId, WaypointType waypointType) {
public Waypoint getLastWaypoint(long trackId) {
if (trackId < 0) {
return null;
}
String selection = WaypointsColumns.TRACKID + "=? AND " + WaypointsColumns.TYPE + "=?";
String[] selectionArgs = new String[]{Long.toString(trackId), Integer.toString(waypointType.ordinal())};
String selection = WaypointsColumns.TRACKID + "=?";
String[] selectionArgs = new String[]{Long.toString(trackId)};
try (Cursor cursor = getWaypointCursor(null, selection, selectionArgs, WaypointsColumns._ID + " DESC", 1)) {
if (cursor != null && cursor.moveToFirst()) {
@@ -617,21 +488,18 @@ public class ContentProviderUtils {
* Gets the next waypoint number for a type.
* Returns -1 if not able to get the next waypoint number.
*
* @param trackId the track id
* @param waypointType the waypoint type
* @param trackId the track id
*/
public int getNextWaypointNumber(long trackId, WaypointType waypointType) {
public int getNextWaypointNumber(long trackId) {
if (trackId < 0) {
return -1;
}
String[] projection = {WaypointsColumns._ID};
String selection = WaypointsColumns.TRACKID + "=? AND " + WaypointsColumns.TYPE + "=?";
String[] selectionArgs = new String[]{Long.toString(trackId), Integer.toString(waypointType.ordinal())};
String selection = WaypointsColumns.TRACKID + "=?";
String[] selectionArgs = new String[]{Long.toString(trackId)};
try (Cursor cursor = getWaypointCursor(projection, selection, selectionArgs, WaypointsColumns._ID, -1)) {
if (cursor != null) {
int count = cursor.getCount();
// For statistics markers, the first marker is for the track statistics, so return the count as the next user visible number.
return waypointType == WaypointType.STATISTICS ? count : count + 1;
return cursor.getCount();
}
}
return -1;
@@ -695,6 +563,19 @@ public class ContentProviderUtils {
return getWaypointCursor(null, selection, selectionArgs, WaypointsColumns._ID, maxWaypoints);
}
@VisibleForTesting
public List<Waypoint> getWaypoints(long trackId) {
ArrayList<Waypoint> waypoints = new ArrayList<>();
try (Cursor cursor = getWaypointCursor(trackId, -1L, -1)) {
if (cursor.moveToFirst()) {
do {
waypoints.add(createWaypoint(cursor));
} while (cursor.moveToNext());
}
}
return waypoints;
}
/**
* Gets the number of waypoints for a track.
*
@@ -713,8 +594,7 @@ public class ContentProviderUtils {
cursor.moveToFirst();
int count = cursor.getInt(0);
cursor.close();
// not count the first waypoint
return count > 0 ? count - 1 : 0;
return count;
}
/**
@@ -751,11 +631,8 @@ public class ContentProviderUtils {
values.put(WaypointsColumns.CATEGORY, waypoint.getCategory());
values.put(WaypointsColumns.ICON, waypoint.getIcon());
values.put(WaypointsColumns.TRACKID, waypoint.getTrackId());
values.put(WaypointsColumns.TYPE, waypoint.getType().ordinal());
values.put(WaypointsColumns.LENGTH, waypoint.getLength());
values.put(WaypointsColumns.DURATION, waypoint.getDuration());
values.put(WaypointsColumns.STARTID, waypoint.getStartId());
values.put(WaypointsColumns.STOPID, waypoint.getStopId());
Location location = waypoint.getLocation();
if (location != null) {
@@ -776,38 +653,10 @@ public class ContentProviderUtils {
}
}
TripStatistics tripStatistics = waypoint.getTripStatistics();
if (tripStatistics != null) {
values.put(WaypointsColumns.STARTTIME, tripStatistics.getStartTime());
values.put(WaypointsColumns.TOTALDISTANCE, tripStatistics.getTotalDistance());
values.put(WaypointsColumns.TOTALTIME, tripStatistics.getTotalTime());
values.put(WaypointsColumns.MOVINGTIME, tripStatistics.getMovingTime());
values.put(WaypointsColumns.AVGSPEED, tripStatistics.getAverageSpeed());
values.put(WaypointsColumns.AVGMOVINGSPEED, tripStatistics.getAverageMovingSpeed());
values.put(WaypointsColumns.MAXSPEED, tripStatistics.getMaxSpeed());
values.put(WaypointsColumns.MINELEVATION, tripStatistics.getMinElevation());
values.put(WaypointsColumns.MAXELEVATION, tripStatistics.getMaxElevation());
values.put(WaypointsColumns.ELEVATIONGAIN, tripStatistics.getTotalElevationGain());
values.put(WaypointsColumns.MINGRADE, tripStatistics.getMinGrade());
values.put(WaypointsColumns.MAXGRADE, tripStatistics.getMaxGrade());
}
values.put(WaypointsColumns.PHOTOURL, waypoint.getPhotoUrl());
return values;
}
private Waypoint getNextStatisticsWaypointAfter(Waypoint waypoint) {
String selection = WaypointsColumns._ID + ">? AND " + WaypointsColumns.TRACKID + "=? AND "
+ WaypointsColumns.TYPE + "=" + WaypointType.STATISTICS.ordinal();
String[] selectionArgs = new String[]{Long.toString(waypoint.getId()), Long.toString(waypoint.getTrackId())};
try (Cursor cursor = getWaypointCursor(null, selection, selectionArgs, WaypointsColumns._ID, 1)) {
if (cursor != null && cursor.moveToFirst()) {
return createWaypoint(cursor);
}
}
return null;
}
/**
* Gets a waypoint cursor.
*
@@ -853,7 +702,7 @@ public class ContentProviderUtils {
* @param cursor the cursor pointing to the location
*/
public Location createTrackPoint(Cursor cursor) {
Location location = new SensorDataSetLocation("");
Location location = new TrackPoint("");
fillTrackPoint(cursor, new CachedTrackPointsIndexes(cursor), location);
return location;
}
@@ -864,6 +713,7 @@ public class ContentProviderUtils {
*
* @param trackId the track id
*/
@Deprecated
public long getFirstTrackPointId(long trackId) {
if (trackId < 0) {
return -1L;
@@ -886,6 +736,7 @@ public class ContentProviderUtils {
*
* @param trackId the track id
*/
@Deprecated
public long getLastTrackPointId(long trackId) {
if (trackId < 0) {
return -1L;
@@ -914,8 +765,8 @@ public class ContentProviderUtils {
return -1L;
}
String selection = TrackPointsColumns._ID + "=(select max(" + TrackPointsColumns._ID
+ ") from " + TrackPointsColumns.TABLE_NAME + " WHERE " + TrackPointsColumns.TRACKID
+ "=? AND " + TrackPointsColumns.TIME + "=?)";
+ ") from " + TrackPointsColumns.TABLE_NAME
+ " WHERE " + TrackPointsColumns.TRACKID + "=? AND " + TrackPointsColumns.TIME + "=?)";
String[] selectionArgs = new String[]{Long.toString(trackId), Long.toString(location.getTime())};
try (Cursor cursor = getTrackPointCursor(new String[]{TrackPointsColumns._ID}, selection, selectionArgs, TrackPointsColumns._ID)) {
if (cursor != null && cursor.moveToFirst()) {
@@ -925,29 +776,13 @@ public class ContentProviderUtils {
return -1L;
}
/**
* Gets the first valid location for a track.
* Returns null if it doesn't exist.
*
* @param trackId the track id
*/
public Location getFirstValidTrackPoint(long trackId) {
if (trackId < 0) {
return null;
}
String selection = TrackPointsColumns._ID + "=(select min(" + TrackPointsColumns._ID + ") from "
+ TrackPointsColumns.TABLE_NAME + " WHERE " + TrackPointsColumns.TRACKID + "=? AND "
+ TrackPointsColumns.LATITUDE + "<=" + MAX_LATITUDE + ")";
String[] selectionArgs = new String[]{Long.toString(trackId)};
return findTrackPointBy(selection, selectionArgs);
}
/**
* Gets the last valid location for a track.
* Returns null if it doesn't exist.
*
* @param trackId the track id
*/
@Deprecated
public Location getLastValidTrackPoint(long trackId) {
if (trackId < 0) {
return null;
@@ -1117,12 +952,7 @@ public class ContentProviderUtils {
values.put(TrackPointsColumns.LONGITUDE, (int) (location.getLongitude() * 1E6));
values.put(TrackPointsColumns.LATITUDE, (int) (location.getLatitude() * 1E6));
// Hack for Samsung phones that don't properly populate the time field
long time = location.getTime();
if (time == 0) {
time = System.currentTimeMillis();
}
values.put(TrackPointsColumns.TIME, time);
values.put(TrackPointsColumns.TIME, location.getTime());
if (location.hasAltitude()) {
values.put(TrackPointsColumns.ALTITUDE, location.getAltitude());
}
@@ -1137,8 +967,8 @@ public class ContentProviderUtils {
}
//SensorData
if (location instanceof SensorDataSetLocation) {
SensorDataSetLocation sensorDataSetLocation = (SensorDataSetLocation) location;
if (location instanceof TrackPoint) {
TrackPoint sensorDataSetLocation = (TrackPoint) location;
SensorDataSet sensorDataSet = sensorDataSetLocation.getSensorDataSet();
if (sensorDataSet != null && sensorDataSet.hasHeartRate()) {
values.put(TrackPointsColumns.SENSOR_HEARTRATE, sensorDataSetLocation.getSensorDataSet().getHeartRate());
@@ -1184,8 +1014,8 @@ public class ContentProviderUtils {
if (!cursor.isNull(indexes.bearingIndex)) {
location.setBearing(cursor.getFloat(indexes.bearingIndex));
}
if (location instanceof SensorDataSetLocation) {
SensorDataSetLocation sensorDataSetLocation = (SensorDataSetLocation) location;
if (location instanceof TrackPoint) {
TrackPoint sensorDataSetLocation = (TrackPoint) location;
float heartRate = cursor.isNull(indexes.sensorHeartRateIndex) ? SensorDataSet.DATA_UNAVAILABLE : cursor.getFloat(indexes.sensorHeartRateIndex);
float cadence = cursor.isNull(indexes.sensorCadenceIndex) ? SensorDataSet.DATA_UNAVAILABLE : cursor.getFloat(indexes.sensorCadenceIndex);
@@ -1256,4 +1086,4 @@ public class ContentProviderUtils {
sensorPowerIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.SENSOR_POWER);
}
}
}
}
@@ -34,6 +34,10 @@ import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import de.dennisguse.opentracks.content.data.TrackPointsColumns;
import de.dennisguse.opentracks.content.data.TracksColumns;
import de.dennisguse.opentracks.content.data.WaypointsColumns;
/**
* A {@link ContentProvider} that handles access to track points, tracks, and waypoints tables.
*
@@ -56,11 +60,12 @@ public abstract class CustomContentProvider extends ContentProvider {
public CustomContentProvider() {
uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TrackPointsColumns.TABLE_NAME, UrlType.TRACKPOINTS.ordinal());
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TrackPointsColumns.TABLE_NAME + "/#", UrlType.TRACKPOINTS_ID.ordinal());
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TrackPointsColumns.TABLE_NAME + "/#", UrlType.TRACKPOINTS_BY_ID.ordinal());
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TrackPointsColumns.TABLE_NAME + "/#", UrlType.TRACKPOINTS_BY_TRACKID.ordinal());
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TracksColumns.TABLE_NAME, UrlType.TRACKS.ordinal());
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TracksColumns.TABLE_NAME + "/#", UrlType.TRACKS_ID.ordinal());
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TracksColumns.TABLE_NAME + "/#", UrlType.TRACKS_BY_ID.ordinal());
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, WaypointsColumns.TABLE_NAME, UrlType.WAYPOINTS.ordinal());
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, WaypointsColumns.TABLE_NAME + "/#", UrlType.WAYPOINTS_ID.ordinal());
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, WaypointsColumns.TABLE_NAME + "/#", UrlType.WAYPOINTS_BY_ID.ordinal());
}
@Override
@@ -128,15 +133,17 @@ public abstract class CustomContentProvider extends ContentProvider {
switch (getUrlType(url)) {
case TRACKPOINTS:
return TrackPointsColumns.CONTENT_TYPE;
case TRACKPOINTS_ID:
case TRACKPOINTS_BY_ID:
return TrackPointsColumns.CONTENT_ITEMTYPE;
case TRACKPOINTS_BY_TRACKID:
return TrackPointsColumns.CONTENT_ITEMTYPE;
case TRACKS:
return TracksColumns.CONTENT_TYPE;
case TRACKS_ID:
case TRACKS_BY_ID:
return TracksColumns.CONTENT_ITEMTYPE;
case WAYPOINTS:
return WaypointsColumns.CONTENT_TYPE;
case WAYPOINTS_ID:
case WAYPOINTS_BY_ID:
return WaypointsColumns.CONTENT_ITEMTYPE;
default:
throw new IllegalArgumentException("Unknown URL " + url);
@@ -192,25 +199,29 @@ public abstract class CustomContentProvider extends ContentProvider {
queryBuilder.setTables(TrackPointsColumns.TABLE_NAME);
sortOrder = sort != null ? sort : TrackPointsColumns.DEFAULT_SORT_ORDER;
break;
case TRACKPOINTS_ID:
case TRACKPOINTS_BY_ID:
queryBuilder.setTables(TrackPointsColumns.TABLE_NAME);
queryBuilder.appendWhere("_id=" + url.getPathSegments().get(1));
queryBuilder.appendWhere(TrackPointsColumns._ID + "=" + url.getPathSegments().get(1));
break;
case TRACKPOINTS_BY_TRACKID:
queryBuilder.setTables(TrackPointsColumns.TABLE_NAME);
queryBuilder.appendWhere(TrackPointsColumns.TRACKID + "=" + url.getPathSegments().get(1));
break;
case TRACKS:
queryBuilder.setTables(TracksColumns.TABLE_NAME);
sortOrder = sort != null ? sort : TracksColumns.DEFAULT_SORT_ORDER;
break;
case TRACKS_ID:
case TRACKS_BY_ID:
queryBuilder.setTables(TracksColumns.TABLE_NAME);
queryBuilder.appendWhere("_id=" + url.getPathSegments().get(1));
queryBuilder.appendWhere(TracksColumns._ID + "=" + url.getPathSegments().get(1));
break;
case WAYPOINTS:
queryBuilder.setTables(WaypointsColumns.TABLE_NAME);
sortOrder = sort != null ? sort : WaypointsColumns.DEFAULT_SORT_ORDER;
break;
case WAYPOINTS_ID:
case WAYPOINTS_BY_ID:
queryBuilder.setTables(WaypointsColumns.TABLE_NAME);
queryBuilder.appendWhere("_id=" + url.getPathSegments().get(1));
queryBuilder.appendWhere(WaypointsColumns._ID + "=" + url.getPathSegments().get(1));
break;
default:
throw new IllegalArgumentException("Unknown url " + url);
@@ -222,6 +233,7 @@ public abstract class CustomContentProvider extends ContentProvider {
@Override
public int update(@NonNull Uri url, ContentValues values, String where, String[] selectionArgs) {
// TODO Use SQLiteQueryBuilder
String table;
String whereClause;
switch (getUrlType(url)) {
@@ -229,7 +241,7 @@ public abstract class CustomContentProvider extends ContentProvider {
table = TrackPointsColumns.TABLE_NAME;
whereClause = where;
break;
case TRACKPOINTS_ID:
case TRACKPOINTS_BY_ID:
table = TrackPointsColumns.TABLE_NAME;
whereClause = TrackPointsColumns._ID + "=" + url.getPathSegments().get(1);
if (!TextUtils.isEmpty(where)) {
@@ -240,7 +252,7 @@ public abstract class CustomContentProvider extends ContentProvider {
table = TracksColumns.TABLE_NAME;
whereClause = where;
break;
case TRACKS_ID:
case TRACKS_BY_ID:
table = TracksColumns.TABLE_NAME;
whereClause = TracksColumns._ID + "=" + url.getPathSegments().get(1);
if (!TextUtils.isEmpty(where)) {
@@ -251,7 +263,7 @@ public abstract class CustomContentProvider extends ContentProvider {
table = WaypointsColumns.TABLE_NAME;
whereClause = where;
break;
case WAYPOINTS_ID:
case WAYPOINTS_BY_ID:
table = WaypointsColumns.TABLE_NAME;
whereClause = WaypointsColumns._ID + "=" + url.getPathSegments().get(1);
if (!TextUtils.isEmpty(where)) {
@@ -330,8 +342,7 @@ public abstract class CustomContentProvider extends ContentProvider {
*/
private Uri insertTrack(Uri url, ContentValues contentValues) {
boolean hasStartTime = contentValues.containsKey(TracksColumns.STARTTIME);
boolean hasStartId = contentValues.containsKey(TracksColumns.STARTID);
if (!hasStartTime || !hasStartId) {
if (!hasStartTime) {
throw new IllegalArgumentException("Both start time and start id values are required.");
}
long rowId = db.insert(TracksColumns.TABLE_NAME, TracksColumns._ID, contentValues);
@@ -362,7 +373,13 @@ public abstract class CustomContentProvider extends ContentProvider {
*/
@VisibleForTesting
enum UrlType {
TRACKPOINTS, TRACKPOINTS_ID, TRACKS, TRACKS_ID, WAYPOINTS, WAYPOINTS_ID
TRACKPOINTS,
TRACKPOINTS_BY_ID,
TRACKPOINTS_BY_TRACKID,
TRACKS,
TRACKS_BY_ID,
WAYPOINTS,
WAYPOINTS_BY_ID
}
/**
@@ -25,6 +25,10 @@ import android.util.Log;
import java.util.EnumSet;
import java.util.Set;
import de.dennisguse.opentracks.content.data.TrackPointsColumns;
import de.dennisguse.opentracks.content.data.TracksColumns;
import de.dennisguse.opentracks.content.data.WaypointsColumns;
/**
* Creates observers/listeners and manages their registration with {@link DataSource}.
* The observers/listeners calls {@link DataSourceListener} when data changes.
@@ -1,5 +1,5 @@
/*
* Copyright 2010 Google Inc.
* 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
@@ -13,16 +13,37 @@
* License for the specific language governing permissions and limitations under
* the License.
*/
package de.dennisguse.opentracks.content;
import android.content.Context;
import android.util.Pair;
import androidx.annotation.VisibleForTesting;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.stats.TripStatistics;
import de.dennisguse.opentracks.util.StringUtils;
import de.dennisguse.opentracks.util.UnitConversions;
/**
* An interface for an object that can generate descriptions of track and waypoint.
* Generates descriptions for tracks and waypoints.
*
* @author Sandor Dornbush
* @author Jimmy Shih
*/
public interface DescriptionGenerator {
public class DescriptionGenerator {
private static final String HTML_LINE_BREAK = "<br>";
private static final String HTML_PARAGRAPH_SEPARATOR = "<p>";
private static final String TEXT_LINE_BREAK = "\n";
private static final String TEXT_PARAGRAPH_SEPARATOR = "\n\n";
private Context context;
public DescriptionGenerator(Context context) {
this.context = context;
}
/**
* Generates a track description.
@@ -30,12 +51,196 @@ public interface DescriptionGenerator {
* @param track the track
* @param html true to output html, false to output plain text
*/
String generateTrackDescription(Track track, boolean html);
public String generateTrackDescription(Track track, boolean html) {
String paragraphSeparator = html ? HTML_PARAGRAPH_SEPARATOR : TEXT_PARAGRAPH_SEPARATOR;
String lineBreak = html ? HTML_LINE_BREAK : TEXT_LINE_BREAK;
StringBuilder builder = new StringBuilder();
// Created by
String creator = html
? "<a href='" + context.getString(R.string.app_web_url) + "'>" + context.getString(R.string.app_name) + "</a>"
: context.getString(R.string.app_name);
builder.append(creator);
builder.append(paragraphSeparator);
writeString(track.getName(), builder, R.string.generic_name_line, lineBreak);
writeString(track.getCategory(), builder, R.string.description_activity_type, lineBreak);
writeString(track.getDescription(), builder, R.string.generic_description_line, lineBreak);
builder.append(generateTripStatisticsDescription(track.getTripStatistics(), html));
return builder.toString();
}
/**
* Generate a waypoint description from a trip statistics.
* Writes a string to a string builder.
*
* @param tripStatistics the trip statistics
* @param text the string
* @param builder the string builder
* @param resId the resource id containing one string placeholder
* @param lineBreak the line break
*/
String generateWaypointDescription(TripStatistics tripStatistics);
private void writeString(String text, StringBuilder builder, int resId, String lineBreak) {
if (text == null || text.length() == 0) {
text = context.getString(R.string.value_unknown);
}
builder.append(context.getString(resId, text));
builder.append(lineBreak);
}
/**
* Generates a description for a {@link TripStatistics}.
*
* @param stats the trip statistics
* @param html true to use "<br>" for line break instead of "\n"
*/
private String generateTripStatisticsDescription(TripStatistics stats, boolean html) {
String lineBreak = html ? HTML_LINE_BREAK : TEXT_LINE_BREAK;
StringBuilder builder = new StringBuilder();
// Total distance
writeDistance(stats.getTotalDistance(), builder, R.string.description_total_distance, lineBreak);
// Total time
writeTime(stats.getTotalTime(), builder, R.string.description_total_time, lineBreak);
// Moving time
writeTime(stats.getMovingTime(), builder, R.string.description_moving_time, lineBreak);
// Average speed
writeSpeed(stats.getAverageSpeed(), builder, R.string.description_average_speed, lineBreak);
// Average moving speed
writeSpeed(stats.getAverageMovingSpeed(), builder, R.string.description_average_moving_speed, lineBreak);
// Max speed
writeSpeed(stats.getMaxSpeed(), builder, R.string.description_max_speed, lineBreak);
// Average pace
writePace(stats.getAverageSpeed(), builder, R.string.description_average_pace_in_minute, lineBreak);
// Average moving pace
writePace(stats.getAverageMovingSpeed(), builder, R.string.description_average_moving_pace_in_minute, lineBreak);
// Fastest pace
writePace(stats.getMaxSpeed(), builder, R.string.description_fastest_pace_in_minute, lineBreak);
// Max elevation
writeElevation(stats.getMaxElevation(), builder, R.string.description_max_elevation, lineBreak);
// Min elevation
writeElevation(stats.getMinElevation(), builder, R.string.description_min_elevation, lineBreak);
// Elevation gain
writeElevation(stats.getTotalElevationGain(), builder, R.string.description_elevation_gain, lineBreak);
// Max grade
writeGrade(stats.getMaxGrade(), builder, R.string.description_max_grade, lineBreak);
// Min grade
writeGrade(stats.getMinGrade(), builder, R.string.description_min_grade, lineBreak);
// Recorded time
builder.append(context.getString(R.string.description_recorded_time, StringUtils.formatDateTime(context, stats.getStartTime())));
builder.append(lineBreak);
return builder.toString();
}
/**
* Writes distance.
*
* @param distance distance in meters
* @param builder StringBuilder to append distance
* @param resId resource id of distance string
* @param lineBreak line break string
*/
@VisibleForTesting
void writeDistance(double distance, StringBuilder builder, int resId, String lineBreak) {
double distanceInKm = distance * UnitConversions.M_TO_KM;
double distanceInMi = distanceInKm * UnitConversions.KM_TO_MI;
builder.append(context.getString(resId, distanceInKm, distanceInMi));
builder.append(lineBreak);
}
/**
* Writes time.
*
* @param time time in milliseconds.
* @param builder StringBuilder to append time
* @param resId resource id of time string
* @param lineBreak line break string
*/
@VisibleForTesting
void writeTime(long time, StringBuilder builder, int resId, String lineBreak) {
builder.append(context.getString(resId, StringUtils.formatElapsedTime(time)));
builder.append(lineBreak);
}
/**
* Writes speed.
*
* @param speed speed in meters per second
* @param builder StringBuilder to append speed
* @param resId resource id of speed string
* @param lineBreak line break string
*/
@VisibleForTesting
void writeSpeed(double speed, StringBuilder builder, int resId, String lineBreak) {
double speedInKmHr = speed * UnitConversions.MS_TO_KMH;
double speedInMiHr = speedInKmHr * UnitConversions.KM_TO_MI;
builder.append(context.getString(resId, speedInKmHr, speedInMiHr));
builder.append(lineBreak);
}
/**
* Writes pace.
*
* @param speed speed in meters per second
* @param builder StringBuilder to append pace
* @param resId resource id of pace string
* @param lineBreak line break string
*/
@VisibleForTesting
void writePace(double speed, StringBuilder builder, int resId, String lineBreak) {
Pair<String, String> paceInMetrics = StringUtils.getSpeedParts(context, speed, true, false);
Pair<String, String> paceInImperial = StringUtils.getSpeedParts(context, speed, false, false);
String formattedPaceMetrics = paceInMetrics.first != null ? paceInMetrics.first : context.getString(R.string.value_unknown);
String formattedPaceImperial = paceInImperial.first != null ? paceInImperial.first : context.getString(R.string.value_unknown);
builder.append(context.getString(resId, formattedPaceMetrics, formattedPaceImperial));
builder.append(lineBreak);
}
/**
* Writes elevation.
*
* @param elevation elevation in meters
* @param builder StringBuilder to append elevation
* @param resId resource id of elevation string
* @param lineBreak line break string
*/
@VisibleForTesting
void writeElevation(double elevation, StringBuilder builder, int resId, String lineBreak) {
long elevationInM = Math.round(elevation);
long elevationInFt = Math.round(elevation * UnitConversions.M_TO_FT);
builder.append(context.getString(resId, elevationInM, elevationInFt));
builder.append(lineBreak);
}
/**
* Writes grade.
*
* @param grade grade in fraction
* @param builder StringBuilder to append grade
* @param resId resource id grade string
* @param lineBreak line break string
*/
@VisibleForTesting
void writeGrade(double grade, StringBuilder builder, int resId, String lineBreak) {
long gradeInPercent = Double.isNaN(grade) || Double.isInfinite(grade) ? 0L : Math.round(grade * 100);
builder.append(context.getString(resId, gradeInPercent));
builder.append(lineBreak);
}
}
@@ -1,243 +0,0 @@
/*
* Copyright 2012 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package de.dennisguse.opentracks.content;
import android.content.Context;
import android.util.Pair;
import androidx.annotation.VisibleForTesting;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.stats.TripStatistics;
import de.dennisguse.opentracks.util.StringUtils;
import de.dennisguse.opentracks.util.UnitConversions;
/**
* @author Jimmy Shih
*/
public class DescriptionGeneratorImpl implements DescriptionGenerator {
private static final String HTML_LINE_BREAK = "<br>";
private static final String HTML_PARAGRAPH_SEPARATOR = "<p>";
private static final String TEXT_LINE_BREAK = "\n";
private static final String TEXT_PARAGRAPH_SEPARATOR = "\n\n";
private Context context;
public DescriptionGeneratorImpl(Context context) {
this.context = context;
}
@Override
public String generateTrackDescription(Track track, boolean html) {
String paragraphSeparator = html ? HTML_PARAGRAPH_SEPARATOR : TEXT_PARAGRAPH_SEPARATOR;
String lineBreak = html ? HTML_LINE_BREAK : TEXT_LINE_BREAK;
StringBuilder builder = new StringBuilder();
// Created by
String creator = html
? "<a href='" + context.getString(R.string.app_web_url) + "'>" + context.getString(R.string.app_name) + "</a>"
: context.getString(R.string.app_name);
builder.append(creator);
builder.append(paragraphSeparator);
writeString(track.getName(), builder, R.string.generic_name_line, lineBreak);
writeString(track.getCategory(), builder, R.string.description_activity_type, lineBreak);
writeString(track.getDescription(), builder, R.string.generic_description_line, lineBreak);
builder.append(generateTripStatisticsDescription(track.getTripStatistics(), html));
return builder.toString();
}
@Override
public String generateWaypointDescription(TripStatistics tripStatistics) {
return generateTripStatisticsDescription(tripStatistics, false);
}
/**
* Writes a string to a string builder.
*
* @param text the string
* @param builder the string builder
* @param resId the resource id containing one string placeholder
* @param lineBreak the line break
*/
private void writeString(String text, StringBuilder builder, int resId, String lineBreak) {
if (text == null || text.length() == 0) {
text = context.getString(R.string.value_unknown);
}
builder.append(context.getString(resId, text));
builder.append(lineBreak);
}
/**
* Generates a description for a {@link TripStatistics}.
*
* @param stats the trip statistics
* @param html true to use "<br>" for line break instead of "\n"
*/
private String generateTripStatisticsDescription(TripStatistics stats, boolean html) {
String lineBreak = html ? HTML_LINE_BREAK : TEXT_LINE_BREAK;
StringBuilder builder = new StringBuilder();
// Total distance
writeDistance(stats.getTotalDistance(), builder, R.string.description_total_distance, lineBreak);
// Total time
writeTime(stats.getTotalTime(), builder, R.string.description_total_time, lineBreak);
// Moving time
writeTime(stats.getMovingTime(), builder, R.string.description_moving_time, lineBreak);
// Average speed
writeSpeed(stats.getAverageSpeed(), builder, R.string.description_average_speed, lineBreak);
// Average moving speed
writeSpeed(stats.getAverageMovingSpeed(), builder, R.string.description_average_moving_speed, lineBreak);
// Max speed
writeSpeed(stats.getMaxSpeed(), builder, R.string.description_max_speed, lineBreak);
// Average pace
writePace(stats.getAverageSpeed(), builder, R.string.description_average_pace_in_minute, lineBreak);
// Average moving pace
writePace(stats.getAverageMovingSpeed(), builder, R.string.description_average_moving_pace_in_minute, lineBreak);
// Fastest pace
writePace(stats.getMaxSpeed(), builder, R.string.description_fastest_pace_in_minute, lineBreak);
// Max elevation
writeElevation(stats.getMaxElevation(), builder, R.string.description_max_elevation, lineBreak);
// Min elevation
writeElevation(stats.getMinElevation(), builder, R.string.description_min_elevation, lineBreak);
// Elevation gain
writeElevation(stats.getTotalElevationGain(), builder, R.string.description_elevation_gain, lineBreak);
// Max grade
writeGrade(stats.getMaxGrade(), builder, R.string.description_max_grade, lineBreak);
// Min grade
writeGrade(stats.getMinGrade(), builder, R.string.description_min_grade, lineBreak);
// Recorded time
builder.append(context.getString(R.string.description_recorded_time, StringUtils.formatDateTime(context, stats.getStartTime())));
builder.append(lineBreak);
return builder.toString();
}
/**
* Writes distance.
*
* @param distance distance in meters
* @param builder StringBuilder to append distance
* @param resId resource id of distance string
* @param lineBreak line break string
*/
@VisibleForTesting
void writeDistance(double distance, StringBuilder builder, int resId, String lineBreak) {
double distanceInKm = distance * UnitConversions.M_TO_KM;
double distanceInMi = distanceInKm * UnitConversions.KM_TO_MI;
builder.append(context.getString(resId, distanceInKm, distanceInMi));
builder.append(lineBreak);
}
/**
* Writes time.
*
* @param time time in milliseconds.
* @param builder StringBuilder to append time
* @param resId resource id of time string
* @param lineBreak line break string
*/
@VisibleForTesting
void writeTime(long time, StringBuilder builder, int resId, String lineBreak) {
builder.append(context.getString(resId, StringUtils.formatElapsedTime(time)));
builder.append(lineBreak);
}
/**
* Writes speed.
*
* @param speed speed in meters per second
* @param builder StringBuilder to append speed
* @param resId resource id of speed string
* @param lineBreak line break string
*/
@VisibleForTesting
void writeSpeed(double speed, StringBuilder builder, int resId, String lineBreak) {
double speedInKmHr = speed * UnitConversions.MS_TO_KMH;
double speedInMiHr = speedInKmHr * UnitConversions.KM_TO_MI;
builder.append(context.getString(resId, speedInKmHr, speedInMiHr));
builder.append(lineBreak);
}
/**
* Writes pace.
*
* @param speed speed in meters per second
* @param builder StringBuilder to append pace
* @param resId resource id of pace string
* @param lineBreak line break string
*/
@VisibleForTesting
void writePace(double speed, StringBuilder builder, int resId, String lineBreak) {
Pair<String, String> paceInMetrics = StringUtils.getSpeedParts(context, speed, true, false);
Pair<String, String> paceInImperial = StringUtils.getSpeedParts(context, speed, false, false);
String formattedPaceMetrics = paceInMetrics.first != null ? paceInMetrics.first : context.getString(R.string.value_unknown);
String formattedPaceImperial = paceInImperial.first != null ? paceInImperial.first : context.getString(R.string.value_unknown);
builder.append(context.getString(resId, formattedPaceMetrics, formattedPaceImperial));
builder.append(lineBreak);
}
/**
* Writes elevation.
*
* @param elevation elevation in meters
* @param builder StringBuilder to append elevation
* @param resId resource id of elevation string
* @param lineBreak line break string
*/
@VisibleForTesting
void writeElevation(double elevation, StringBuilder builder, int resId, String lineBreak) {
long elevationInM = Math.round(elevation);
long elevationInFt = Math.round(elevation * UnitConversions.M_TO_FT);
builder.append(context.getString(resId, elevationInM, elevationInFt));
builder.append(lineBreak);
}
/**
* Writes grade.
*
* @param grade grade in fraction
* @param builder StringBuilder to append grade
* @param resId resource id grade string
* @param lineBreak line break string
*/
@VisibleForTesting
void writeGrade(double grade, StringBuilder builder, int resId, String lineBreak) {
long gradeInPercent = Double.isNaN(grade) || Double.isInfinite(grade) ? 0L : Math.round(grade * 100);
builder.append(context.getString(resId, gradeInPercent));
builder.append(lineBreak);
}
}
@@ -3,8 +3,10 @@ package de.dennisguse.opentracks.content;
import android.location.Location;
import android.location.LocationManager;
import de.dennisguse.opentracks.content.data.TrackPoint;
/**
* Creates a new {@link SensorDataSetLocation}.
* Creates a new {@link TrackPoint}.
* An implementation can create new instances or reuse existing instances for optimization.
*/
public class LocationFactory {
@@ -15,6 +17,6 @@ public class LocationFactory {
public static LocationFactory DEFAULT_LOCATION_FACTORY = new LocationFactory();
public Location createLocation() {
return new SensorDataSetLocation(LocationManager.GPS_PROVIDER);
return new TrackPoint(LocationManager.GPS_PROVIDER);
}
}
@@ -28,6 +28,10 @@ import java.util.Locale;
import java.util.SortedSet;
import java.util.TreeSet;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.TracksColumns;
import de.dennisguse.opentracks.content.data.Waypoint;
import de.dennisguse.opentracks.content.data.WaypointsColumns;
import de.dennisguse.opentracks.stats.TripStatistics;
import de.dennisguse.opentracks.util.LocationUtils;
import de.dennisguse.opentracks.util.UnitConversions;
@@ -243,7 +247,6 @@ public class SearchEngine {
score *= getTitleBoost(query, track.getName(), track.getDescription(), track.getCategory());
TripStatistics statistics = track.getTripStatistics();
score *= getDistanceBoost(query, statistics.getMeanLatitude(), statistics.getMeanLongitude());
long meanTimestamp = (statistics.getStartTime() + statistics.getStopTime()) / 2L;
score *= getTimeBoost(query, meanTimestamp);
@@ -21,6 +21,8 @@ import java.io.IOException;
import java.util.Arrays;
import de.dennisguse.opentracks.android.IContentResolver;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.TracksColumns;
import de.dennisguse.opentracks.io.file.TrackFileFormat;
import de.dennisguse.opentracks.io.file.exporter.TrackExporter;
@@ -79,7 +81,7 @@ public class ShareContentProvider extends CustomContentProvider implements ICont
}
builder.deleteCharAt(builder.lastIndexOf(TRACKID_DELIMITER));
Uri uri = Uri.parse(ContentProviderUtils.CONTENT_BASE_URI + "/" + TracksColumns.TABLE_NAME + "/" + trackFileFormat.getName() + "/" + builder + "." + trackFileFormat.getExtension());
Uri uri = Uri.parse(TracksColumns.CONTENT_URI + "/" + trackFileFormat.getName() + "/" + builder + "." + trackFileFormat.getExtension());
String mime = getTypeMime(uri);
Log.d(TAG, "Created uri " + uri.toString() + " with MIME " + mime);
@@ -30,6 +30,8 @@ import java.util.EnumSet;
import java.util.Set;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.Waypoint;
import de.dennisguse.opentracks.util.LocationUtils;
import de.dennisguse.opentracks.util.PreferencesUtils;
@@ -40,17 +42,24 @@ import de.dennisguse.opentracks.util.PreferencesUtils;
*/
public class TrackDataHub implements DataSourceListener {
public synchronized static TrackDataHub newInstance(Context context) {
return new TrackDataHub(context, new TrackDataManager(), new ContentProviderUtils(context), TARGET_DISPLAYED_TRACK_POINTS);
}
/**
* Target number of track points displayed by the map overlay.
* We may display more than this number of points.
*/
@Deprecated
private static final int TARGET_DISPLAYED_TRACK_POINTS = 5000;
/**
* Maximum number of waypoints to displayed.
*/
@VisibleForTesting
@Deprecated
private static final int MAX_DISPLAYED_WAYPOINTS = 128;
private static final String TAG = TrackDataHub.class.getSimpleName();
private final Context context;
@@ -95,10 +104,6 @@ public class TrackDataHub implements DataSourceListener {
resetSamplingState();
}
public synchronized static TrackDataHub newInstance(Context context) {
return new TrackDataHub(context, new TrackDataManager(), new ContentProviderUtils(context), TARGET_DISPLAYED_TRACK_POINTS);
}
public void start() {
if (started) {
Log.i(TAG, "TrackDataHub already started, ignoring start.");
@@ -396,8 +401,7 @@ public class TrackDataHub implements DataSourceListener {
trackDataListener.clearWaypoints();
}
try (Cursor cursor = contentProviderUtils.getWaypointCursor(
selectedTrackId, -1L, MAX_DISPLAYED_WAYPOINTS)) {
try (Cursor cursor = contentProviderUtils.getWaypointCursor(selectedTrackId, -1L, MAX_DISPLAYED_WAYPOINTS)) {
if (cursor != null && cursor.moveToFirst()) {
do {
Waypoint waypoint = contentProviderUtils.createWaypoint(cursor);
@@ -466,12 +470,8 @@ public class TrackDataHub implements DataSourceListener {
samplingFrequency = 1 + (int) (numTotalPoints / targetNumPoints);
}
if (!LocationUtils.isValidLocation(location)) {
// TODO: also include the last valid point before a split
for (TrackDataListener trackDataListener : sampledInListeners) {
trackDataListener.onSegmentSplit(location);
includeNextPoint = true;
}
if (!LocationUtils.isValidLocation(location)) { //This can be split markers (not anymore supported feature)
includeNextPoint = true;
} else {
// Also include the last point if the selected track is not recording.
if (includeNextPoint || (localNumLoadedPoints % samplingFrequency == 0) || (locationId == lastTrackPointId && !isSelectedTrackRecording())) {
@@ -18,6 +18,9 @@ package de.dennisguse.opentracks.content;
import android.location.Location;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.Waypoint;
/**
* Listener for track data changes.
*
@@ -51,14 +54,9 @@ public interface TrackDataListener {
*/
void onSampledOutTrackPoint(Location location);
/**
* Called when an invalid track point representing a segment split is read.
*/
void onSegmentSplit(Location location);
/**
* Called when finish sending new track points.
* This gets called after every batch of calls to {@link #onSampledInTrackPoint(Location)}, {@link #onSampledOutTrackPoint(Location)} and {@link #onSegmentSplit(Location)}.
* This gets called after every batch of calls to {@link #onSampledInTrackPoint(Location)} and {@link #onSampledOutTrackPoint(Location)}.
*/
void onNewTrackPointsDone();
@@ -1,85 +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 de.dennisguse.opentracks.content.Waypoint.WaypointType;
/**
* A request for the service to create a waypoint at the current location.
*
* @author Sandor Dornbush
*/
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);
private WaypointType type;
// true if this marker contains the track statistics
private boolean isTrackStatistics;
private String name;
private String category;
private String description;
private String iconUrl;
private String photoUrl;
private WaypointCreationRequest(WaypointType type, boolean isTrackStatistics) {
this(type, isTrackStatistics, null, null, null, null, null);
}
public WaypointCreationRequest(WaypointType type, boolean isTrackStatistics, String name, String category, String description, String iconUrl, String photoUrl) {
this.type = type;
this.isTrackStatistics = isTrackStatistics;
this.name = name;
this.category = category;
this.description = description;
this.iconUrl = iconUrl;
this.photoUrl = photoUrl;
}
public WaypointType getType() {
return type;
}
public boolean isTrackStatistics() {
return isTrackStatistics;
}
public String getName() {
return name;
}
public String getCategory() {
return category;
}
public String getDescription() {
return description;
}
public String getIconUrl() {
return iconUrl;
}
public String getPhotoUrl() {
return photoUrl;
}
}
@@ -14,7 +14,7 @@
* the License.
*/
package de.dennisguse.opentracks.content;
package de.dennisguse.opentracks.content.data;
import android.location.Location;
@@ -35,8 +35,6 @@ public class Track {
private String name = "";
private String description = "";
private String category = "";
private long startId = -1L;
private long stopId = -1L;
/*
* The number of location points (present even if the points themselves are not loaded)
@@ -84,22 +82,6 @@ public class Track {
this.category = category;
}
public long getStartId() {
return startId;
}
public void setStartId(long startId) {
this.startId = startId;
}
public long getStopId() {
return stopId;
}
public void setStopId(long stopId) {
this.stopId = stopId;
}
public int getNumberOfPoints() {
return numberOfPoints;
}
@@ -13,7 +13,7 @@
* License for the specific language governing permissions and limitations under
* the License.
*/
package de.dennisguse.opentracks.content;
package de.dennisguse.opentracks.content.data;
import android.location.Location;
@@ -24,16 +24,16 @@ import de.dennisguse.opentracks.content.sensor.SensorDataSet;
*
* @author Sandor Dornbush
*/
public class SensorDataSetLocation extends Location {
public class TrackPoint extends Location {
private SensorDataSet sensorDataSet = null;
public SensorDataSetLocation(Location location, SensorDataSet sensorDataSet) {
public TrackPoint(Location location, SensorDataSet sensorDataSet) {
super(location);
this.sensorDataSet = sensorDataSet;
}
public SensorDataSetLocation(String provider) {
public TrackPoint(String provider) {
super(provider);
}
@@ -14,11 +14,13 @@
* the License.
*/
package de.dennisguse.opentracks.content;
package de.dennisguse.opentracks.content.data;
import android.net.Uri;
import android.provider.BaseColumns;
import de.dennisguse.opentracks.content.ContentProviderUtils;
/**
* Constants for the track points table.
*
@@ -27,7 +29,7 @@ import android.provider.BaseColumns;
public interface TrackPointsColumns extends BaseColumns {
String TABLE_NAME = "trackpoints";
Uri CONTENT_URI = Uri.parse(ContentProviderUtils.CONTENT_BASE_URI + "/trackpoints");
Uri CONTENT_URI = Uri.parse(ContentProviderUtils.CONTENT_BASE_URI + "/" + TABLE_NAME);
String CONTENT_TYPE = "vnd.android.cursor.dir/vnd.de.dennisguse.trackpoint";
String CONTENT_ITEMTYPE = "vnd.android.cursor.item/vnd.de.dennisguse.trackpoint";
String DEFAULT_SORT_ORDER = "_id";
@@ -14,11 +14,13 @@
* the License.
*/
package de.dennisguse.opentracks.content;
package de.dennisguse.opentracks.content.data;
import android.net.Uri;
import android.provider.BaseColumns;
import de.dennisguse.opentracks.content.ContentProviderUtils;
/**
* Constants for the tracks table.
*
@@ -27,7 +29,7 @@ import android.provider.BaseColumns;
public interface TracksColumns extends BaseColumns {
String TABLE_NAME = "tracks";
Uri CONTENT_URI = Uri.parse(ContentProviderUtils.CONTENT_BASE_URI + "/tracks");
Uri CONTENT_URI = Uri.parse(ContentProviderUtils.CONTENT_BASE_URI + "/" + TABLE_NAME);
String CONTENT_TYPE = "vnd.android.cursor.dir/vnd.de.dennisguse.track";
String CONTENT_ITEMTYPE = "vnd.android.cursor.item/vnd.de.dennisguse.track";
String DEFAULT_SORT_ORDER = "_id";
@@ -36,22 +38,28 @@ public interface TracksColumns extends BaseColumns {
String NAME = "name"; // track name
String DESCRIPTION = "description"; // track description
String CATEGORY = "category"; // track activity type
String STARTID = "startid"; // first track point id
String STOPID = "stopid"; // last track point id
@Deprecated
String STARTID = "startid"; // first track point id // TODO UNUSED
@Deprecated
String STOPID = "stopid"; // last track point id // TODO UNUSED
String STARTTIME = "starttime"; // track start time
String STOPTIME = "stoptime"; // track stop time
String NUMPOINTS = "numpoints"; // number of track points
String TOTALDISTANCE = "totaldistance"; // total distance
String TOTALTIME = "totaltime"; // total time
String MOVINGTIME = "movingtime"; // moving time
String MINLAT = "minlat"; // minimum latitude
String MAXLAT = "maxlat"; // maximum latitude
String MINLON = "minlon"; // minimum longitude
String MAXLON = "maxlon"; // maximum longitude
String AVGSPEED = "avgspeed"; // average speed
// average moving speed
String AVGMOVINGSPEED = "avgmovingspeed";
@Deprecated
String MINLAT = "minlat"; // minimum latitude // TODO UNUSED
@Deprecated
String MAXLAT = "maxlat"; // maximum latitude // TODO UNUSED
@Deprecated
String MINLON = "minlon"; // minimum longitude //TODO UNUSED
@Deprecated
String MAXLON = "maxlon"; // maximum longitude //TODO UNUSED
String AVGSPEED = "avgspeed"; // average speed
String AVGMOVINGSPEED = "avgmovingspeed"; // average moving speed
String MAXSPEED = "maxspeed"; // maximum speed
String MINELEVATION = "minelevation"; // minimum elevation
String MAXELEVATION = "maxelevation"; // maximum elevation
@@ -14,13 +14,11 @@
* the License.
*/
package de.dennisguse.opentracks.content;
package de.dennisguse.opentracks.content.data;
import android.location.Location;
import android.net.Uri;
import de.dennisguse.opentracks.stats.TripStatistics;
/**
* A waypoint.
*
@@ -35,31 +33,23 @@ public final class Waypoint {
private String category = "";
private String icon = "";
private long trackId = -1L;
private WaypointType type = WaypointType.WAYPOINT;
private double length = 0.0;
private long duration = 0;
private long startId = -1L;
private long stopId = -1L;
private Location location = null;
private TripStatistics tripStatistics = null;
private String photoUrl = "";
public Waypoint() {
}
public Waypoint(String name, String description, String category, String icon, long trackId, WaypointType type, double length, long duration, long startId, long stopId, Location location, TripStatistics tripStatistics, String photoUrl) {
public Waypoint(String name, String description, String category, String icon, long trackId, double length, long duration, Location location, String photoUrl) {
this.name = name;
this.description = description;
this.category = category;
this.icon = icon;
this.trackId = trackId;
this.type = type;
this.length = length;
this.duration = duration;
this.startId = startId;
this.stopId = stopId;
this.location = location;
this.tripStatistics = tripStatistics;
this.photoUrl = photoUrl;
}
@@ -111,14 +101,6 @@ public final class Waypoint {
this.trackId = trackId;
}
public WaypointType getType() {
return type;
}
public void setType(WaypointType type) {
this.type = type;
}
public double getLength() {
return length;
}
@@ -135,22 +117,6 @@ public final class Waypoint {
this.duration = duration;
}
public long getStartId() {
return startId;
}
public void setStartId(long startId) {
this.startId = startId;
}
public long getStopId() {
return stopId;
}
public void setStopId(long stopId) {
this.stopId = stopId;
}
public Location getLocation() {
return location;
}
@@ -159,18 +125,6 @@ public final class Waypoint {
this.location = location;
}
public boolean isTripStatistics() {
return tripStatistics != null;
}
public TripStatistics getTripStatistics() {
return tripStatistics;
}
public void setTripStatistics(TripStatistics tripStatistics) {
this.tripStatistics = tripStatistics;
}
public String getPhotoUrl() {
return photoUrl;
}
@@ -186,8 +140,4 @@ public final class Waypoint {
public boolean hasPhoto() {
return photoUrl != null && !"".equals(photoUrl);
}
public enum WaypointType {
WAYPOINT, STATISTICS
}
}
@@ -14,11 +14,13 @@
* the License.
*/
package de.dennisguse.opentracks.content;
package de.dennisguse.opentracks.content.data;
import android.net.Uri;
import android.provider.BaseColumns;
import de.dennisguse.opentracks.content.ContentProviderUtils;
/**
* Constants for waypoints table.
*
@@ -27,7 +29,7 @@ import android.provider.BaseColumns;
public interface WaypointsColumns extends BaseColumns {
String TABLE_NAME = "waypoints";
Uri CONTENT_URI = Uri.parse(ContentProviderUtils.CONTENT_BASE_URI + "/waypoints");
Uri CONTENT_URI = Uri.parse(ContentProviderUtils.CONTENT_BASE_URI + "/" + TABLE_NAME);
String CONTENT_TYPE = "vnd.android.cursor.dir/vnd.de.dennisguse.waypoint";
String CONTENT_ITEMTYPE = "vnd.android.cursor.item/vnd.de.dennisguse.waypoint";
String DEFAULT_SORT_ORDER = "_id";
@@ -38,12 +40,16 @@ public interface WaypointsColumns extends BaseColumns {
String CATEGORY = "category"; // waypoint category
String ICON = "icon"; // waypoint icon
String TRACKID = "trackid"; // track id
String TYPE = "type"; // type
@Deprecated
String TYPE = "type"; // type // TODO UNUSED
String LENGTH = "length"; // length of the track (without smoothing)
String DURATION = "duration"; // total duration of the track (not from last waypoint)
String STARTTIME = "starttime"; // start time of the trip statistics
String STARTID = "startid"; // start track point id
String STOPID = "stopid"; // stop track point id
@Deprecated
String STARTID = "startid"; // start track point id // TODO UNUSED
@Deprecated
String STOPID = "stopid"; // stop track point id // TODO UNUSED
String LONGITUDE = "longitude"; // longitude
String LATITUDE = "latitude"; // latitude
@@ -53,17 +59,29 @@ public interface WaypointsColumns extends BaseColumns {
String SPEED = "speed"; // speed
String BEARING = "bearing"; // bearing
@Deprecated
String TOTALDISTANCE = "totaldistance"; // total distance
@Deprecated
String TOTALTIME = "totaltime"; // total time
@Deprecated
String MOVINGTIME = "movingtime"; // moving time
@Deprecated
String AVGSPEED = "avgspeed"; // average speed
String AVGMOVINGSPEED = "avgmovingspeed"; // average moving speed
String MAXSPEED = "maxspeed"; // max speed
String MINELEVATION = "minelevation"; // min elevation
String MAXELEVATION = "maxelevation"; // max elevation
String ELEVATIONGAIN = "elevationgain"; // elevation gain
String MINGRADE = "mingrade"; // min grade
String MAXGRADE = "maxgrade"; // max grade
@Deprecated
String AVGMOVINGSPEED = "avgmovingspeed"; // average moving speed // TODO UNUSED
@Deprecated
String MAXSPEED = "maxspeed"; // max speed // TODO UNUSED
@Deprecated
String MINELEVATION = "minelevation"; // min elevation // TODO UNUSED
@Deprecated
String MAXELEVATION = "maxelevation"; // max elevation // TODO UNUSED
@Deprecated
String ELEVATIONGAIN = "elevationgain"; // elevation gain // TODO UNUSED
@Deprecated
String MINGRADE = "mingrade"; // min grade // TODO UNUSED
@Deprecated
String MAXGRADE = "maxgrade"; // max grade // TODO UNUSED
String PHOTOURL = "photoUrl"; // url for the photo
@@ -5,8 +5,8 @@ public final class SensorDataSet {
public static final float DATA_UNAVAILABLE = Float.NaN;
//TODO It might be necessary to consider: if sensor was connected as well.
public String sensorName;
public String sensorAddress;
private String sensorName;
private String sensorAddress;
private float heartRate;
private float cadence;
private float power;
@@ -31,16 +31,17 @@ import androidx.fragment.app.FragmentActivity;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.List;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.TrackDetailActivity;
import de.dennisguse.opentracks.chart.ChartView;
import de.dennisguse.opentracks.content.SensorDataSetLocation;
import de.dennisguse.opentracks.content.Track;
import de.dennisguse.opentracks.content.TrackDataHub;
import de.dennisguse.opentracks.content.TrackDataListener;
import de.dennisguse.opentracks.content.TrackDataType;
import de.dennisguse.opentracks.content.Waypoint;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.content.data.Waypoint;
import de.dennisguse.opentracks.content.sensor.SensorDataSet;
import de.dennisguse.opentracks.stats.TripStatistics;
import de.dennisguse.opentracks.stats.TripStatisticsUpdater;
@@ -58,7 +59,7 @@ public abstract class ChartFragment extends Fragment implements TrackDataListene
private static final String STATE_CHART_VIEW_BY_DISTANCE_KEY = "chartViewByDistance";
private final ArrayList<double[]> pendingPoints = new ArrayList<>();
private final List<double[]> pendingPoints = new ArrayList<>();
private TrackDataHub trackDataHub;
@@ -103,7 +104,7 @@ public abstract class ChartFragment extends Fragment implements TrackDataListene
recordingDistanceInterval = PreferencesUtils.getRecordingDistanceIntervalDefault(getContext());
// Create a chartView here to store data thus won't need to reload all the data on every onStart or onResume.
chartView = new ChartView(getActivity(), chartByDistance);
chartView = new ChartView(getContext(), chartByDistance);
}
@Override
@@ -198,13 +199,6 @@ public abstract class ChartFragment extends Fragment implements TrackDataListene
}
}
@Override
public void onSegmentSplit(Location location) {
if (isResumed()) {
fillDataPoint(location, null);
}
}
@Override
public void onNewTrackPointsDone() {
if (isResumed()) {
@@ -264,9 +258,8 @@ public abstract class ChartFragment extends Fragment implements TrackDataListene
}
reportSpeed = speed;
chartView.setReportSpeed(reportSpeed);
boolean chartShowSpeed = PreferencesUtils.shouldChartShowSpeed(getActivity());
setSeriesEnabled(ChartView.SPEED_SERIES, chartShowSpeed && reportSpeed);
setSeriesEnabled(ChartView.PACE_SERIES, chartShowSpeed && !reportSpeed);
setSeriesEnabled(ChartView.SPEED_SERIES, reportSpeed);
setSeriesEnabled(ChartView.PACE_SERIES, !reportSpeed);
runOnUiThread(new Runnable() {
@Override
public void run() {
@@ -303,26 +296,14 @@ public abstract class ChartFragment extends Fragment implements TrackDataListene
*/
private void checkChartSettings() {
boolean needUpdate = false;
if (setSeriesEnabled(ChartView.ELEVATION_SERIES, PreferencesUtils.shouldChartShowElevation(getActivity()))) {
if (setSeriesEnabled(ChartView.SPEED_SERIES, reportSpeed)) {
needUpdate = true;
}
if (setSeriesEnabled(ChartView.PACE_SERIES, !reportSpeed)) {
needUpdate = true;
}
boolean chartShowSpeed = PreferencesUtils.shouldChartShowSpeed(getActivity());
if (setSeriesEnabled(ChartView.SPEED_SERIES, chartShowSpeed && reportSpeed)) {
needUpdate = true;
}
if (setSeriesEnabled(ChartView.PACE_SERIES, chartShowSpeed && !reportSpeed)) {
needUpdate = true;
}
if (setSeriesEnabled(ChartView.POWER_SERIES, PreferencesUtils.shouldChartShowPower(getActivity()))) {
needUpdate = true;
}
if (setSeriesEnabled(ChartView.CADENCE_SERIES, PreferencesUtils.shouldChartShowCadence(getActivity()))) {
needUpdate = true;
}
if (setSeriesEnabled(ChartView.HEART_RATE_SERIES, PreferencesUtils.shouldChartShowHeartRate(getActivity()))) {
needUpdate = true;
}
if (needUpdate) {
chartView.postInvalidate();
}
@@ -443,8 +424,8 @@ public abstract class ChartFragment extends Fragment implements TrackDataListene
double heartRate = Double.NaN;
double cadence = Double.NaN;
double power = Double.NaN;
if (location instanceof SensorDataSetLocation && ((SensorDataSetLocation) location).getSensorDataSet() != null) {
SensorDataSet sensorDataSet = ((SensorDataSetLocation) location).getSensorDataSet();
if (location instanceof TrackPoint && ((TrackPoint) location).getSensorDataSet() != null) {
SensorDataSet sensorDataSet = ((TrackPoint) location).getSensorDataSet();
if (sensorDataSet.hasHeartRate()) {
heartRate = sensorDataSet.getHeartRate();
}
@@ -28,7 +28,6 @@ import androidx.fragment.app.FragmentManager;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.ContentProviderUtils;
import de.dennisguse.opentracks.content.DescriptionGeneratorImpl;
import de.dennisguse.opentracks.util.DialogUtils;
/**
@@ -83,8 +82,7 @@ public class DeleteMarkerDialogFragment extends DialogFragment {
public void run() {
ContentProviderUtils contentProviderUtils = new ContentProviderUtils(fragmentActivity);
for (long markerId : markerIds) {
contentProviderUtils.deleteWaypoint(fragmentActivity,
markerId, new DescriptionGeneratorImpl(fragmentActivity));
contentProviderUtils.deleteWaypoint(fragmentActivity, markerId);
}
caller.onDeleteMarkerDone();
}
@@ -31,7 +31,6 @@ import java.util.Locale;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.io.file.TrackFileFormat;
import de.dennisguse.opentracks.util.FileUtils;
/**
* A DialogFragment to select a file type, gpx, kml, etc.
@@ -73,7 +72,7 @@ public class FileTypeDialogFragment extends DialogFragment {
for (int i = 0; i < choices.length; i++) {
TrackFileFormat trackFileFormat = trackFileFormats[i];
String trackFileFormatUpperCase = trackFileFormat.getExtension().toUpperCase(Locale.US); //ASCII upper case
choices[i] = getString(optionId, trackFileFormatUpperCase, FileUtils.getPathDisplayName(trackFileFormat.getExtension()));
choices[i] = getString(optionId, trackFileFormatUpperCase);
}
return new AlertDialog.Builder(getActivity())
.setNegativeButton(R.string.generic_cancel, new OnClickListener() {
@@ -41,12 +41,10 @@ import androidx.fragment.app.FragmentActivity;
import de.dennisguse.opentracks.MarkerEditActivity;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.ContentProviderUtils;
import de.dennisguse.opentracks.content.Track;
import de.dennisguse.opentracks.content.Waypoint;
import de.dennisguse.opentracks.content.Waypoint.WaypointType;
import de.dennisguse.opentracks.content.data.Waypoint;
import de.dennisguse.opentracks.util.IntentUtils;
import de.dennisguse.opentracks.util.ListItemUtils;
import de.dennisguse.opentracks.util.StatsUtils;
import de.dennisguse.opentracks.util.MarkerUtils;
import de.dennisguse.opentracks.util.StringUtils;
import de.dennisguse.opentracks.util.UnitConversions;
@@ -62,7 +60,7 @@ public class MarkerDetailFragment extends Fragment {
private static final long HIDE_TEXT_DELAY = 4 * UnitConversions.ONE_SECOND;
private ContentProviderUtils contentProviderUtils;
private Handler handler;
private ImageView photo;
private ImageView photoView;
private ImageView textGradient;
private LinearLayout waypointInfo;
private Waypoint waypoint;
@@ -119,11 +117,11 @@ public class MarkerDetailFragment extends Fragment {
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.marker_detail_fragment, container, false);
photo = view.findViewById(R.id.marker_detail_waypoint_photo);
photoView = view.findViewById(R.id.marker_detail_waypoint_photo);
textGradient = view.findViewById(R.id.marker_detail_waypoint_text_gradient);
waypointInfo = view.findViewById(R.id.marker_detail_waypoint_info);
photo.setOnClickListener(new View.OnClickListener() {
photoView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
handler.removeCallbacks(hideText);
@@ -156,11 +154,8 @@ public class MarkerDetailFragment extends Fragment {
@Override
public void setMenuVisibility(boolean menuVisible) {
super.setMenuVisibility(menuVisible);
/*
* View pager caches the neighboring fragments in the resumed state. If
* becoming visible from the resumed state, update the UI to display the
* text above the image.
*/
// View pager caches the neighboring fragments in the resumed state.
// If becoming visible from the resumed state, update the UI to display the text above the image.
if (isResumed()) {
if (menuVisible) {
updateUi();
@@ -214,47 +209,25 @@ public class MarkerDetailFragment extends Fragment {
}
}
/**
* Updates the UI.
*/
private void updateUi() {
View waypointView = getView().findViewById(R.id.marker_detail_waypoint);
View statisticsView = getView().findViewById(R.id.marker_detail_statistics);
boolean isWaypoint = waypoint.getType() == WaypointType.WAYPOINT;
waypointView.setVisibility(isWaypoint ? View.VISIBLE : View.GONE);
statisticsView.setVisibility(isWaypoint ? View.GONE : View.VISIBLE);
if (isWaypoint) {
boolean hasPhoto = waypoint.hasPhoto();
photo.setVisibility(hasPhoto ? View.VISIBLE : View.GONE);
textGradient.setVisibility(hasPhoto ? View.VISIBLE : View.GONE);
waypointInfo.setVisibility(View.VISIBLE);
if (hasPhoto) {
handler.removeCallbacks(hideText);
photo.setImageURI(waypoint.getPhotoURI());
handler.postDelayed(hideText, HIDE_TEXT_DELAY);
}
setName(R.id.marker_detail_waypoint_name, hasPhoto);
TextView category = getView().findViewById(R.id.marker_detail_waypoint_category);
ListItemUtils.setTextView(getActivity(), category, StringUtils.getCategory(waypoint.getCategory()), hasPhoto);
TextView description = getView().findViewById(R.id.marker_detail_waypoint_description);
ListItemUtils.setTextView(getActivity(), description, waypoint.getDescription(), hasPhoto);
setLocation(R.id.marker_detail_waypoint_location, hasPhoto);
boolean hasPhoto = waypoint.hasPhoto();
if (hasPhoto) {
handler.removeCallbacks(hideText);
photoView.setImageURI(waypoint.getPhotoURI());
handler.postDelayed(hideText, HIDE_TEXT_DELAY);
} else {
setName(R.id.marker_detail_statistics_name, false);
setLocation(R.id.marker_detail_statistics_location, false);
Track track = contentProviderUtils.getTrack(waypoint.getTrackId());
StatsUtils.setTripStatisticsValues(getActivity(), waypoint.getTripStatistics(), null);
StatsUtils.setLocationValues(getActivity(), waypoint.getLocation(), false);
photoView.setImageResource(MarkerUtils.ICON_ID);
}
setName(R.id.marker_detail_waypoint_name, hasPhoto);
TextView category = getView().findViewById(R.id.marker_detail_waypoint_category);
ListItemUtils.setTextView(getActivity(), category, StringUtils.getCategory(waypoint.getCategory()), hasPhoto);
TextView description = getView().findViewById(R.id.marker_detail_waypoint_description);
ListItemUtils.setTextView(getActivity(), description, waypoint.getDescription(), hasPhoto);
setLocation(R.id.marker_detail_waypoint_location, hasPhoto);
}
private void setName(int resId, boolean addShadow) {
@@ -20,32 +20,36 @@ import android.location.Location;
import android.os.Bundle;
import android.os.Handler;
import android.util.Log;
import android.util.Pair;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Spinner;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import java.util.EnumSet;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.TrackDetailActivity;
import de.dennisguse.opentracks.content.Track;
import de.dennisguse.opentracks.content.TrackDataHub;
import de.dennisguse.opentracks.content.TrackDataListener;
import de.dennisguse.opentracks.content.TrackDataType;
import de.dennisguse.opentracks.content.Waypoint;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.Waypoint;
import de.dennisguse.opentracks.content.sensor.SensorDataSet;
import de.dennisguse.opentracks.services.ITrackRecordingService;
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
import de.dennisguse.opentracks.services.sensors.BluetoothRemoteSensorManager;
import de.dennisguse.opentracks.stats.TripStatistics;
import de.dennisguse.opentracks.util.LocationUtils;
import de.dennisguse.opentracks.util.StatsUtils;
import de.dennisguse.opentracks.util.PreferencesUtils;
import de.dennisguse.opentracks.util.StringUtils;
import de.dennisguse.opentracks.util.TrackIconUtils;
import de.dennisguse.opentracks.util.UnitConversions;
@@ -59,26 +63,17 @@ public class StatsFragment extends Fragment implements TrackDataListener {
public static final String STATS_FRAGMENT_TAG = StatsFragment.class.getSimpleName();
private static final long UI_UPDATE_INTERVAL = UnitConversions.ONE_SECOND;
private TrackDataHub trackDataHub;
private Handler handlerUpdateUI;
//TODO Initialize immediately and remove in onDestroy()
private TrackRecordingServiceConnection trackRecordingServiceConnection;
private Location lastLocation = null;
private TripStatistics lastTripStatistics = null;
private final Runnable updateUIeachSecond = new Runnable() {
public void run() {
if (isResumed() && isSelectedTrackRecording()) {
if (!isSelectedTrackPaused() && lastTripStatistics != null) {
StatsUtils.setTotalTimeValue(getActivity(), System.currentTimeMillis() - lastTripStatistics.getStopTime() + lastTripStatistics.getTotalTime());
updateSensorDataUI();
}
handlerUpdateUI.postDelayed(this, UnitConversions.ONE_SECOND);
}
}
};
private String category = "";
private int recordingGpsAccuracy;
@@ -87,6 +82,111 @@ public class StatsFragment extends Fragment implements TrackDataListener {
return inflater.inflate(R.layout.stats, container, false);
}
/* Views */
private View sensorContainerView;
private TextView heartRateValueView;
private TextView heartRateSensorView;
private TextView totalTimeValueView;
private final Runnable updateUIeachSecond = new Runnable() {
public void run() {
if (isResumed() && isSelectedTrackRecording()) {
if (!isSelectedTrackPaused() && lastTripStatistics != null) {
updateTotalTime();
updateSensorDataUI();
}
handlerUpdateUI.postDelayed(this, UI_UPDATE_INTERVAL);
}
}
};
private TextView distanceValue;
private TextView distanceUnit;
private View activityLabel;
private Spinner activitySpinner;
private TextView movingTimeValue;
private TextView speedAvgLabel;
private TextView speedAvgValue;
private TextView speedAvgUnit;
private TextView speedMaxLabel;
private TextView speedMaxValue;
private TextView speedMaxUnit;
private TextView speedMovingLabel;
private TextView speedMovingValue;
private TextView speedMovingUnit;
private View gradeElevationSeparator;
private View gradeElevationContainer;
private TextView elevationMinValue;
private TextView elevationMinUnit;
private TextView elevationMaxValue;
private TextView elevationMaxUnit;
private View speedContainer;
private TextView speedLabel;
private TextView speedValue;
private TextView speedUnit;
private View elevationContainer;
private TextView elevationValue;
private TextView elevationUnit;
private View coordinateSeparator;
private View coordinateContainer;
private TextView latitudeValue;
private TextView longitudeValue;
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
sensorContainerView = view.findViewById(R.id.stats_sensor_container);
heartRateValueView = view.findViewById(R.id.stats_sensor_heart_rate_value);
heartRateSensorView = view.findViewById(R.id.stats_sensor_heart_rate_sensor_value);
totalTimeValueView = view.findViewById(R.id.stats_total_time_value);
distanceValue = view.findViewById(R.id.stats_distance_value);
distanceUnit = view.findViewById(R.id.stats_distance_unit);
activityLabel = view.findViewById(R.id.stats_activity_type_label);
activitySpinner = view.findViewById(R.id.stats_activity_type_icon);
movingTimeValue = view.findViewById(R.id.stats_moving_time_value);
speedAvgLabel = view.findViewById(R.id.stats_average_speed_label);
speedAvgValue = view.findViewById(R.id.stats_average_speed_value);
speedAvgUnit = view.findViewById(R.id.stats_average_speed_unit);
speedMaxLabel = view.findViewById(R.id.stats_max_speed_label);
speedMaxValue = view.findViewById(R.id.stats_max_speed_value);
speedMaxUnit = view.findViewById(R.id.stats_max_speed_unit);
speedMovingLabel = view.findViewById(R.id.stats_moving_speed_label);
speedMovingValue = view.findViewById(R.id.stats_moving_speed_value);
speedMovingUnit = view.findViewById(R.id.stats_moving_speed_unit);
gradeElevationSeparator = view.findViewById(R.id.stats_elevation_separator);
gradeElevationContainer = view.findViewById(R.id.stats_elevation_container);
elevationMinValue = view.findViewById(R.id.stats_elevation_min_value);
elevationMinUnit = view.findViewById(R.id.stats_elevation_min_unit);
elevationMaxValue = view.findViewById(R.id.stats_elevation_max_value);
elevationMaxUnit = view.findViewById(R.id.stats_elevation_max_unit);
speedContainer = view.findViewById(R.id.stats_speed);
speedLabel = view.findViewById(R.id.stats_speed_label);
speedValue = view.findViewById(R.id.stats_speed_value);
speedUnit = view.findViewById(R.id.stats_speed_unit);
elevationContainer = view.findViewById(R.id.stats_elevation);
elevationValue = view.findViewById(R.id.stats_elevation_current_value);
elevationUnit = view.findViewById(R.id.stats_elevation_current_unit);
coordinateSeparator = view.findViewById(R.id.stats_coordinate_separator);
coordinateContainer = view.findViewById(R.id.stats_coordinate_container);
latitudeValue = view.findViewById(R.id.stats_latitude_value);
longitudeValue = view.findViewById(R.id.stats_longitude_value);
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
@@ -125,10 +225,7 @@ public class StatsFragment extends Fragment implements TrackDataListener {
trackRecordingServiceConnection = new TrackRecordingServiceConnection(getContext(), null);
trackRecordingServiceConnection.startConnection(getContext());
updateUi(getActivity());
if (isSelectedTrackRecording()) {
handlerUpdateUI.post(updateUIeachSecond);
}
handlerUpdateUI.post(updateUIeachSecond);
}
@Override
@@ -141,10 +238,66 @@ public class StatsFragment extends Fragment implements TrackDataListener {
@Override
public void onStop() {
super.onStop();
trackRecordingServiceConnection.unbind();
if (trackRecordingServiceConnection != null) {
trackRecordingServiceConnection.unbind(getContext());
}
trackRecordingServiceConnection = null;
}
@Override
public void onDestroyView() {
super.onDestroyView();
sensorContainerView = null;
heartRateValueView = null;
heartRateSensorView = null;
totalTimeValueView = null;
distanceValue = null;
distanceUnit = null;
activityLabel = null;
activitySpinner = null;
movingTimeValue = null;
speedAvgLabel = null;
speedAvgValue = null;
speedAvgUnit = null;
speedMaxLabel = null;
speedMaxValue = null;
speedMaxUnit = null;
speedMovingLabel = null;
speedMovingValue = null;
speedMovingUnit = null;
gradeElevationSeparator = null;
gradeElevationContainer = null;
elevationMinValue = null;
elevationMinUnit = null;
elevationMaxValue = null;
elevationMaxUnit = null;
speedContainer = null;
speedLabel = null;
speedValue = null;
speedUnit = null;
elevationContainer = null;
elevationValue = null;
elevationUnit = null;
coordinateSeparator = null;
coordinateContainer = null;
latitudeValue = null;
longitudeValue = null;
}
@Override
public void onTrackUpdated(final Track track) {
if (isResumed()) {
@@ -154,7 +307,7 @@ public class StatsFragment extends Fragment implements TrackDataListener {
if (isResumed()) {
lastTripStatistics = track != null ? track.getTripStatistics() : null;
category = track != null ? track.getCategory() : "";
updateUi(getActivity());
updateUI();
}
}
});
@@ -176,11 +329,6 @@ public class StatsFragment extends Fragment implements TrackDataListener {
lastLocation = location;
}
@Override
public void onSegmentSplit(Location location) {
// We don't care.
}
@Override
public void onNewTrackPointsDone() {
if (isResumed()) {
@@ -200,7 +348,7 @@ public class StatsFragment extends Fragment implements TrackDataListener {
lastLocation = null;
}
}
StatsUtils.setLocationValues(getActivity(), lastLocation, isSelectedTrackRecording());
setLocationValues();
}
}
});
@@ -229,7 +377,7 @@ public class StatsFragment extends Fragment implements TrackDataListener {
@Override
public void run() {
if (isResumed()) {
updateUi(getActivity());
updateUI();
}
}
});
@@ -244,7 +392,7 @@ public class StatsFragment extends Fragment implements TrackDataListener {
@Override
public void run() {
if (isResumed()) {
updateUi(getActivity());
updateUI();
}
}
});
@@ -300,6 +448,12 @@ public class StatsFragment extends Fragment implements TrackDataListener {
return trackDataHub != null && trackDataHub.isSelectedTrackPaused();
}
private void updateUI() {
updateStats();
setLocationValues();
updateSensorDataUI();
}
/**
* Tries to fetch most recent {@link SensorDataSet} {@link de.dennisguse.opentracks.services.sensors.BluetoothRemoteSensorManager}.
*/
@@ -314,16 +468,176 @@ public class StatsFragment extends Fragment implements TrackDataListener {
sensorDataSet = trackRecordingService.getSensorData();
}
StatsUtils.setSensorData(getActivity(), sensorDataSet, isSelectedTrackRecording());
setHeartRateSensorData(sensorDataSet, isSelectedTrackRecording());
}
/**
* Updates the UI.
*/
private void updateUi(FragmentActivity activity) {
String trackIconValue = TrackIconUtils.getIconValue(activity, category);
StatsUtils.setTripStatisticsValues(activity, lastTripStatistics, trackIconValue);
StatsUtils.setLocationValues(activity, lastLocation, isSelectedTrackRecording());
updateSensorDataUI();
private void setHeartRateSensorData(SensorDataSet sensorDataSet, boolean isRecording) {
// heart rate
int isVisible = View.VISIBLE;
if (!isRecording || PreferencesUtils.isBluetoothHeartRateSensorAddressDefault(getContext())) {
isVisible = View.INVISIBLE;
}
sensorContainerView.setVisibility(isVisible);
if (isRecording) {
String heartRate = getContext().getString(R.string.value_unknown);
String sensorName = getContext().getString(R.string.value_unknown);
if (sensorDataSet != null && sensorDataSet.isRecent(BluetoothRemoteSensorManager.MAX_SENSOR_DATE_SET_AGE_MS)) {
sensorName = sensorDataSet.getSensorName();
if (sensorDataSet.hasHeartRate()) {
heartRate = StringUtils.formatDecimal(sensorDataSet.getHeartRate(), 0);
}
}
heartRateSensorView.setText(sensorName);
heartRateValueView.setText(heartRate);
}
}
private void updateStats() {
String trackIconValue = TrackIconUtils.getIconValue(getContext(), category);
boolean metricUnits = PreferencesUtils.isMetricUnits(getContext());
boolean reportSpeed = PreferencesUtils.isReportSpeed(getContext());
boolean isRecording = isSelectedTrackRecording();
// Set total distance
{
double totalDistance = lastTripStatistics == null ? Double.NaN : lastTripStatistics.getTotalDistance();
Pair<String, String> parts = StringUtils.getDistanceParts(getContext(), totalDistance, metricUnits);
distanceValue.setText(parts.first);
distanceUnit.setText(parts.second);
}
// Set activity type
{
activityLabel.setVisibility(trackIconValue != null ? View.VISIBLE : View.GONE);
activitySpinner.setVisibility(trackIconValue != null ? View.VISIBLE : View.GONE);
activitySpinner.setEnabled(isRecording);
if (trackIconValue != null) {
TrackIconUtils.setIconSpinner(activitySpinner, trackIconValue);
}
}
// Set time
if (lastTripStatistics != null) {
movingTimeValue.setText(StringUtils.formatElapsedTime(lastTripStatistics.getMovingTime()));
updateTotalTime();
}
// Set average speed/pace
{
double speed = lastTripStatistics != null ? lastTripStatistics.getAverageSpeed() : Double.NaN;
speedAvgLabel.setText(reportSpeed ? R.string.stats_average_speed : R.string.stats_average_pace);
Pair<String, String> parts = StringUtils.getSpeedParts(getContext(), speed, metricUnits, reportSpeed);
speedAvgValue.setText(parts.first);
speedAvgUnit.setText(parts.second);
}
// Set max speed/pace
{
double speed = lastTripStatistics == null ? Double.NaN : lastTripStatistics.getMaxSpeed();
speedMaxLabel.setText(reportSpeed ? R.string.stats_max_speed : R.string.stats_fastest_pace);
Pair<String, String> parts = StringUtils.getSpeedParts(getContext(), speed, metricUnits, reportSpeed);
speedMaxValue.setText(parts.first);
speedMaxUnit.setText(parts.second);
}
// Set moving speed/pace
{
double speed = lastTripStatistics != null ? lastTripStatistics.getAverageMovingSpeed() : Double.NaN;
speedMovingLabel.setText(reportSpeed ? R.string.stats_average_moving_speed : R.string.stats_average_moving_pace);
Pair<String, String> parts = StringUtils.getSpeedParts(getContext(), speed, metricUnits, reportSpeed);
speedMovingValue.setText(parts.first);
speedMovingUnit.setText(parts.second);
}
// Set elevation
{
boolean showElevation = PreferencesUtils.isShowStatsGradeElevation(getContext());
gradeElevationSeparator.setVisibility(showElevation ? View.VISIBLE : View.GONE);
gradeElevationContainer.setVisibility(showElevation ? View.VISIBLE : View.GONE);
if (showElevation) {
{
double elevation = lastTripStatistics == null ? Double.NaN : lastTripStatistics.getMinElevation();
Pair<String, String> parts = StringUtils.formatElevation(getContext(), elevation, metricUnits);
elevationMinValue.setText(parts.first);
elevationMinUnit.setText(parts.second);
}
{
double elevation = lastTripStatistics == null ? Double.NaN : lastTripStatistics.getMaxElevation();
Pair<String, String> parts = StringUtils.formatElevation(getContext(), elevation, metricUnits);
elevationMaxValue.setText(parts.first);
elevationMaxUnit.setText(parts.second);
}
}
}
}
private void updateTotalTime() {
long totalTime;
if (isSelectedTrackRecording()) {
totalTime = System.currentTimeMillis() - lastTripStatistics.getStopTime() + lastTripStatistics.getTotalTime();
} else {
totalTime = lastTripStatistics.getTotalTime();
}
totalTimeValueView.setText(StringUtils.formatElapsedTime(totalTime));
}
private void setLocationValues() {
boolean metricUnits = PreferencesUtils.isMetricUnits(getContext());
boolean reportSpeed = PreferencesUtils.isReportSpeed(getContext());
boolean isRecording = isSelectedTrackRecording();
// Set speed/pace
speedContainer.setVisibility(isRecording ? View.VISIBLE : View.INVISIBLE);
if (isRecording) {
speedLabel.setText(reportSpeed ? R.string.stats_speed : R.string.stats_pace);
double speed = lastLocation != null && lastLocation.hasSpeed() ? lastLocation.getSpeed() : Double.NaN;
Pair<String, String> parts = StringUtils.getSpeedParts(getContext(), speed, metricUnits, reportSpeed);
speedValue.setText(parts.first);
speedUnit.setText(parts.second);
}
// Set elevation
boolean showGradeElevation = isRecording && PreferencesUtils.isShowStatsGradeElevation(getContext());
elevationContainer.setVisibility(showGradeElevation ? View.VISIBLE : View.GONE);
if (showGradeElevation) {
double altitude = lastLocation != null && lastLocation.hasAltitude() ? lastLocation.getAltitude() : Double.NaN;
Pair<String, String> parts = StringUtils.formatElevation(getContext(), altitude, metricUnits);
elevationValue.setText(parts.first);
elevationUnit.setText(parts.second);
}
// Set coordinate
boolean showCoordinate = isRecording && PreferencesUtils.isStatsShowCoordinate(getContext());
coordinateSeparator.setVisibility(showCoordinate ? View.VISIBLE : View.GONE);
coordinateContainer.setVisibility(showCoordinate ? View.VISIBLE : View.GONE);
if (showCoordinate) {
double latitude = lastLocation != null ? lastLocation.getLatitude() : Double.NaN;
String latitudeText = Double.isNaN(latitude) || Double.isInfinite(latitude) ? getContext().getString(R.string.value_unknown) : StringUtils.formatCoordinate(latitude);
latitudeValue.setText(latitudeText);
double longitude = lastLocation != null ? lastLocation.getLongitude() : Double.NaN;
String longitudeText = Double.isNaN(longitude) || Double.isInfinite(longitude) ? getContext().getString(R.string.value_unknown) : StringUtils.formatCoordinate(longitude);
longitudeValue.setText(longitudeText);
}
}
}
@@ -6,7 +6,7 @@ import java.util.Locale;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.ContentProviderUtils;
import de.dennisguse.opentracks.content.Track;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.io.file.exporter.FileTrackExporter;
import de.dennisguse.opentracks.io.file.exporter.GpxTrackWriter;
import de.dennisguse.opentracks.io.file.exporter.KmlTrackWriter;
@@ -146,7 +146,7 @@ public enum TrackFileFormat {
}
public TrackExporter newTrackExporter(Context context, Track[] tracks, TrackExporterListener trackExporterListener) {
return newKmzTrackExporter(context, this.newTrackWriter(context, tracks.length > 1), tracks, trackExporterListener, exportPhotos);
return newKmzTrackExporter(context, newTrackWriter(context, tracks.length > 1), tracks, trackExporterListener, exportPhotos);
}
public String getExtension() {
@@ -188,7 +188,7 @@ public enum TrackFileFormat {
public TrackExporter newTrackExporter(Context context, Track[] tracks, TrackExporterListener trackExporterListener) {
ContentProviderUtils contentProviderUtils = new ContentProviderUtils(context);
TrackWriter trackWriter = this.newTrackWriter(context, tracks.length > 1);
TrackWriter trackWriter = newTrackWriter(context, tracks.length > 1);
return new FileTrackExporter(contentProviderUtils, trackWriter, tracks, trackExporterListener);
}
@@ -16,21 +16,20 @@
package de.dennisguse.opentracks.io.file.exporter;
import android.Manifest;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.app.ProgressDialog;
import android.content.DialogInterface;
import android.content.pm.PackageManager;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.core.app.ActivityCompat;
import androidx.annotation.Nullable;
import androidx.documentfile.provider.DocumentFile;
import androidx.fragment.app.FragmentActivity;
import java.io.File;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.fragments.FileTypeDialogFragment;
import de.dennisguse.opentracks.io.file.TrackFileFormat;
@@ -39,7 +38,6 @@ import de.dennisguse.opentracks.util.FileUtils;
/**
* An activity for saving tracks to the external storage.
* If saving a specific track, option to save it to a temp directory and play the track afterward.
*
* @author Rodrigo Damazio
*/
@@ -53,34 +51,34 @@ public class ExportActivity extends FragmentActivity implements FileTypeDialogFr
private String directoryDisplayName;
private ExportAsyncTask exportAsyncTask;
private ProgressDialog progressDialog;
private int processedTrackCount;
private int totalTrackCount;
private Uri directoryUri;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
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);
} else {
fileTypeDialogStart();
}
startActivityForResult(intent, EXTERNAL_STORAGE_PERMISSION_REQUEST_CODE);
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent resultData) {
super.onActivityResult(requestCode, resultCode, resultData);
if (requestCode == EXTERNAL_STORAGE_PERMISSION_REQUEST_CODE) {
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) {
Toast.makeText(this, R.string.external_storage_not_writable, Toast.LENGTH_LONG).show();
} else {
if (resultCode == Activity.RESULT_OK) {
directoryUri = resultData.getData();
fileTypeDialogStart();
} else {
Toast.makeText(this, R.string.external_storage_not_writable, Toast.LENGTH_LONG).show();
finish();
}
return;
}
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
private void fileTypeDialogStart() {
@@ -89,30 +87,15 @@ public class ExportActivity extends FragmentActivity implements FileTypeDialogFr
@Override
public void onFileTypeDone(TrackFileFormat trackFileFormat) {
if (!FileUtils.isExternalStorageWriteable()) {
Toast.makeText(this, R.string.external_storage_not_writable, Toast.LENGTH_LONG).show();
finish();
return;
DocumentFile pickedDirectory = DocumentFile.fromTreeUri(this, directoryUri);
DocumentFile exportDirectory = pickedDirectory.findFile(FileUtils.EXPORT_DIR);
if (exportDirectory == null) {
exportDirectory = pickedDirectory.createDirectory(FileUtils.EXPORT_DIR);
}
File directory = new File(FileUtils.getPath(trackFileFormat.getExtension()));
if (!FileUtils.ensureDirectoryExists(directory)) {
Toast.makeText(this, R.string.external_storage_not_writable, Toast.LENGTH_LONG).show();
finish();
return;
}
directoryDisplayName = FileUtils.getPathDisplayName(trackFileFormat.getExtension());
//TODO (still needed?): getLastNonConfiguration instance returned ExportAsyncTask before
// Object retained = getLastNonConfigurationInstance();
// if (retained instanceof ExportAsyncTask) {
// exportAsyncTask = (ExportAsyncTask) retained;
// exportAsyncTask.setActivity(this);
// } else {
exportAsyncTask = new ExportAsyncTask(this, trackFileFormat, directory);
directoryDisplayName = FileUtils.getPath(exportDirectory);
exportAsyncTask = new ExportAsyncTask(this, trackFileFormat, exportDirectory);
exportAsyncTask.execute();
// }
}
@Override
@@ -149,8 +132,7 @@ public class ExportActivity extends FragmentActivity implements FileTypeDialogFr
} else {
iconId = R.drawable.ic_dialog_error_24dp;
titleId = R.string.generic_error_title;
message = getString(R.string.export_external_storage_error, processedTrackCount, totalTracks,
directoryDisplayName);
message = getString(R.string.export_external_storage_error, processedTrackCount, totalTracks, directoryDisplayName);
}
AlertDialog.Builder builder = new AlertDialog.Builder(this).setCancelable(true)
.setIcon(iconId).setMessage(message)
@@ -22,16 +22,16 @@ import android.os.AsyncTask;
import android.os.PowerManager.WakeLock;
import android.util.Log;
import java.io.File;
import androidx.documentfile.provider.DocumentFile;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import de.dennisguse.opentracks.content.ContentProviderUtils;
import de.dennisguse.opentracks.content.Track;
import de.dennisguse.opentracks.content.TracksColumns;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.TracksColumns;
import de.dennisguse.opentracks.io.file.TrackFileFormat;
import de.dennisguse.opentracks.util.FileUtils;
import de.dennisguse.opentracks.util.PreferencesUtils;
import de.dennisguse.opentracks.util.SystemUtils;
@@ -45,7 +45,7 @@ public class ExportAsyncTask extends AsyncTask<Void, Integer, Boolean> {
private static final String TAG = ExportAsyncTask.class.getSimpleName();
private final TrackFileFormat trackFileFormat;
private final File directory;
private final DocumentFile directory;
private final Context context;
private final ContentProviderUtils contentProviderUtils;
private ExportActivity exportActivity;
@@ -63,7 +63,7 @@ public class ExportAsyncTask extends AsyncTask<Void, Integer, Boolean> {
* @param trackFileFormat the track file format
* @param directory the directory to write the file
*/
public ExportAsyncTask(ExportActivity exportActivity, TrackFileFormat trackFileFormat, File directory) {
public ExportAsyncTask(ExportActivity exportActivity, TrackFileFormat trackFileFormat, DocumentFile directory) {
this.exportActivity = exportActivity;
this.trackFileFormat = trackFileFormat;
this.directory = directory;
@@ -156,13 +156,13 @@ public class ExportAsyncTask extends AsyncTask<Void, Integer, Boolean> {
};
TrackExporter trackExporter = trackFileFormat.newTrackExporter(context, tracks, trackExporterListener);
Track track = tracks[0];
String fileName = FileUtils.buildUniqueFileName(directory, track.getName(), trackFileFormat.getExtension());
File file = new File(directory, fileName);
try (FileOutputStream fileOutputStream = new FileOutputStream(file)) {
if (trackExporter.writeTrack(context, fileOutputStream)) {
String fileName = track.getId() + "." + trackFileFormat.getExtension();
DocumentFile file = directory.createFile(trackFileFormat.getMimeType(), fileName);
try (OutputStream outputStream = context.getContentResolver().openOutputStream(file.getUri())) {
if (trackExporter.writeTrack(context, outputStream)) {
return true;
} else {
if (!file.delete()) {
@@ -28,9 +28,9 @@ import java.io.OutputStream;
import de.dennisguse.opentracks.content.ContentProviderUtils;
import de.dennisguse.opentracks.content.LocationFactory;
import de.dennisguse.opentracks.content.LocationIterator;
import de.dennisguse.opentracks.content.SensorDataSetLocation;
import de.dennisguse.opentracks.content.Track;
import de.dennisguse.opentracks.content.Waypoint;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.content.data.Waypoint;
import de.dennisguse.opentracks.util.LocationUtils;
/**
@@ -99,7 +99,6 @@ public class FileTrackExporter implements TrackExporter {
boolean hasWaypoints = false;
try (Cursor cursor = contentProviderUtils.getWaypointCursor(track.getId(), -1L, ContentProviderUtils.MAX_LOADED_WAYPOINTS_POINTS)) {
if (cursor != null && cursor.moveToFirst()) {
// Intentionally skip first waypoint (contains statistics).
for (int i = 0; i < cursor.getCount(); i++) {
if (Thread.interrupted()) {
throw new InterruptedException();
@@ -109,9 +108,7 @@ public class FileTrackExporter implements TrackExporter {
hasWaypoints = true;
}
Waypoint waypoint = contentProviderUtils.createWaypoint(cursor);
if (!waypoint.isTripStatistics()) {
trackWriter.writeWaypoint(waypoint);
}
trackWriter.writeWaypoint(waypoint);
cursor.moveToNext();
}
@@ -217,7 +214,7 @@ public class FileTrackExporter implements TrackExporter {
@Override
public Location createLocation() {
if (currentLocation == null) {
currentLocation = new SensorDataSetLocation("");
currentLocation = new TrackPoint("");
}
return currentLocation;
}
@@ -23,8 +23,8 @@ import java.io.PrintWriter;
import java.text.NumberFormat;
import java.util.Locale;
import de.dennisguse.opentracks.content.Track;
import de.dennisguse.opentracks.content.Waypoint;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.Waypoint;
import de.dennisguse.opentracks.util.StringUtils;
/**
@@ -119,12 +119,9 @@ public class GpxTrackWriter implements TrackWriter {
if (location.hasAltitude()) {
printWriter.println("<ele>" + ELEVATION_FORMAT.format(location.getAltitude()) + "</ele>");
}
printWriter.println(
"<time>" + StringUtils.formatDateTimeIso8601(location.getTime()) + "</time>");
printWriter.println("<time>" + StringUtils.formatDateTimeIso8601(location.getTime()) + "</time>");
printWriter.println("<name>" + StringUtils.formatCData(waypoint.getName()) + "</name>");
printWriter.println("<cmt>" + StringUtils.formatCData(waypoint.getType().name()) + "</cmt>");
printWriter.println(
"<desc>" + StringUtils.formatCData(waypoint.getDescription()) + "</desc>");
printWriter.println("<desc>" + StringUtils.formatCData(waypoint.getDescription()) + "</desc>");
printWriter.println("<type>" + StringUtils.formatCData(waypoint.getCategory()) + "</type>");
printWriter.println("</wpt>");
}
@@ -28,11 +28,9 @@ import java.util.List;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.ContentProviderUtils;
import de.dennisguse.opentracks.content.DescriptionGenerator;
import de.dennisguse.opentracks.content.DescriptionGeneratorImpl;
import de.dennisguse.opentracks.content.SensorDataSetLocation;
import de.dennisguse.opentracks.content.Track;
import de.dennisguse.opentracks.content.Waypoint;
import de.dennisguse.opentracks.content.Waypoint.WaypointType;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.content.data.Waypoint;
import de.dennisguse.opentracks.content.sensor.SensorDataSet;
import de.dennisguse.opentracks.util.StringUtils;
@@ -43,19 +41,17 @@ import de.dennisguse.opentracks.util.StringUtils;
*/
public class KmlTrackWriter implements TrackWriter {
private static final String WAYPOINT_STYLE = "waypoint";
private static final String STATISTICS_STYLE = "statistics";
public static final String WAYPOINT_STYLE = "waypoint";
private static final String START_STYLE = "start";
private static final String END_STYLE = "end";
private static final String TRACK_STYLE = "track";
private static final String SCHEMA_ID = "schema";
private static final String SENSOR_TYPE_CADENCE = "cadence";
private static final String SENSOR_TYPE_HEART_RATE = "heart_rate";
private static final String SENSOR_TYPE_POWER = "power";
public static final String SENSOR_TYPE_CADENCE = "cadence";
public static final String SENSOR_TYPE_HEART_RATE = "heart_rate";
public static final String SENSOR_TYPE_POWER = "power";
private static final String WAYPOINT_ICON = "http://maps.google.com/mapfiles/kml/pushpin/blue-pushpin.png";
private static final String STATISTICS_ICON = "http://maps.google.com/mapfiles/kml/pushpin/ylw-pushpin.png";
private static final String START_ICON = "http://maps.google.com/mapfiles/kml/paddle/grn-circle.png";
private static final String END_ICON = "http://maps.google.com/mapfiles/kml/paddle/red-circle.png";
private static final String TRACK_ICON = "http://earth.google.com/images/kml-icons/track-directional/track-0.png";
@@ -88,7 +84,7 @@ public class KmlTrackWriter implements TrackWriter {
this.exportTrackDetail = exportTrackDetail;
this.exportSensorData = exportSensorData;
this.exportPhotos = exportPhotos;
this.descriptionGenerator = new DescriptionGeneratorImpl(context);
this.descriptionGenerator = new DescriptionGenerator(context);
this.contentProviderUtils = new ContentProviderUtils(context);
}
@@ -125,7 +121,6 @@ public class KmlTrackWriter implements TrackWriter {
writeTrackStyle();
writePlacemarkerStyle(START_STYLE, START_ICON, 32, 1);
writePlacemarkerStyle(END_STYLE, END_ICON, 32, 1);
writePlacemarkerStyle(STATISTICS_STYLE, STATISTICS_ICON, 20, 2);
writePlacemarkerStyle(WAYPOINT_STYLE, WAYPOINT_ICON, 20, 2);
printWriter.println("<Schema id=\"" + SCHEMA_ID + "\">");
@@ -167,13 +162,11 @@ public class KmlTrackWriter implements TrackWriter {
@Override
public void writeWaypoint(Waypoint waypoint) {
if (printWriter != null && exportTrackDetail) {
String styleName = waypoint.getType() == WaypointType.STATISTICS ? STATISTICS_STYLE : WAYPOINT_STYLE;
if (waypoint.hasPhoto() && exportPhotos) {
float heading = getHeading(waypoint.getTrackId(), waypoint.getLocation());
writePhotoOverlay(waypoint.getName(), waypoint.getCategory(), waypoint.getDescription(), styleName, waypoint.getLocation(), waypoint.getPhotoUrl(), heading);
writePhotoOverlay(waypoint.getName(), waypoint.getCategory(), waypoint.getDescription(), WAYPOINT_STYLE, waypoint.getLocation(), waypoint.getPhotoUrl(), heading);
} else {
writePlacemark(waypoint.getName(), waypoint.getCategory(), waypoint.getDescription(), styleName, waypoint.getLocation());
writePlacemark(waypoint.getName(), waypoint.getCategory(), waypoint.getDescription(), WAYPOINT_STYLE, waypoint.getLocation());
}
}
}
@@ -205,6 +198,7 @@ public class KmlTrackWriter implements TrackWriter {
if (exportTrackDetail) {
printWriter.println("<name>" + StringUtils.formatCData(track.getName()) + "</name>");
printWriter.println("<description>" + StringUtils.formatCData(track.getDescription()) + "</description>");
printWriter.println("<icon>" + StringUtils.formatCData(track.getIcon()) + "</icon>");
}
printWriter.println("<styleUrl>#" + TRACK_STYLE + "</styleUrl>");
@@ -270,8 +264,8 @@ public class KmlTrackWriter implements TrackWriter {
printWriter.println("<gx:coord>" + getCoordinates(location, " ") + "</gx:coord>");
if (exportSensorData && location instanceof SensorDataSetLocation) {
SensorDataSet sensorDataSet = ((SensorDataSetLocation) location).getSensorDataSet();
if (exportSensorData && location instanceof TrackPoint) {
SensorDataSet sensorDataSet = ((TrackPoint) location).getSensorDataSet();
if (sensorDataSet != null) {
if (sensorDataSet.hasHeartRate()) {
heartRateList.add(sensorDataSet.getHeartRate());
@@ -32,8 +32,8 @@ import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
import de.dennisguse.opentracks.content.ContentProviderUtils;
import de.dennisguse.opentracks.content.Track;
import de.dennisguse.opentracks.content.Waypoint;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.Waypoint;
import de.dennisguse.opentracks.util.FileUtils;
/**
@@ -19,8 +19,8 @@ import android.location.Location;
import java.io.OutputStream;
import de.dennisguse.opentracks.content.Track;
import de.dennisguse.opentracks.content.Waypoint;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.Waypoint;
/**
* Interface for writing tracks to a file. The expected sequence of calls is:
@@ -39,14 +39,11 @@ import javax.xml.parsers.SAXParserFactory;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.ContentProviderUtils;
import de.dennisguse.opentracks.content.DescriptionGeneratorImpl;
import de.dennisguse.opentracks.content.LocationFactory;
import de.dennisguse.opentracks.content.LocationIterator;
import de.dennisguse.opentracks.content.Track;
import de.dennisguse.opentracks.content.Waypoint;
import de.dennisguse.opentracks.content.Waypoint.WaypointType;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.Waypoint;
import de.dennisguse.opentracks.services.TrackRecordingService;
import de.dennisguse.opentracks.stats.TripStatistics;
import de.dennisguse.opentracks.stats.TripStatisticsUpdater;
import de.dennisguse.opentracks.util.FileUtils;
import de.dennisguse.opentracks.util.LocationUtils;
@@ -71,12 +68,13 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
private final ContentProviderUtils contentProviderUtils;
private final int recordingDistanceInterval;
private final List<Long> trackIds;
private final List<Waypoint> waypoints;
private final List<Long> trackIds = new ArrayList<>();
private final List<Waypoint> waypoints = new ArrayList<>();
// The current element content
//TODO Should be made private and getter be used by child classes.
protected String content;
protected String icon;
protected String name;
protected String description;
protected String category;
@@ -104,8 +102,6 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
this.importTrackId = importTrackId;
this.contentProviderUtils = contentProviderUtils;
this.recordingDistanceInterval = PreferencesUtils.getRecordingDistanceInterval(context);
trackIds = new ArrayList<>();
waypoints = new ArrayList<>();
}
@Override
@@ -177,6 +173,7 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
return;
}
}
if (location == null) {
if (!locationIterator.hasNext()) {
// No more track points. Ignore the rest of the waypoints.
@@ -186,9 +183,11 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
trackTripStatisticstrackUpdater.addLocation(location, recordingDistanceInterval);
markerTripStatisticsUpdater.addLocation(location, recordingDistanceInterval);
}
if (waypoint.getLocation().getTime() > location.getTime()) {
location = null;
} else if (waypoint.getLocation().getTime() < location.getTime()) {
Log.w(TAG, "Ignoring waypoint: current location was after waypoint.");
waypoint = null;
} else {
// The waypoint location time matches the track point time
@@ -200,28 +199,13 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
// Valid location
if (location.getLatitude() == waypoint.getLocation().getLatitude() && location.getLongitude() == waypoint.getLocation().getLongitude()) {
// Get tripStatistics, description, and icon
TripStatistics tripStatistics;
String waypointDescription;
String icon;
if (waypoint.getType() == WaypointType.STATISTICS) {
tripStatistics = markerTripStatisticsUpdater.getTripStatistics();
markerTripStatisticsUpdater = new TripStatisticsUpdater(location.getTime());
waypointDescription = new DescriptionGeneratorImpl(context).generateWaypointDescription(tripStatistics);
icon = context.getString(R.string.marker_statistics_icon_url);
} else {
tripStatistics = null;
waypointDescription = waypoint.getDescription();
icon = context.getString(R.string.marker_waypoint_icon_url);
}
// Get length and duration
String waypointDescription = waypoint.getDescription();
String icon = context.getString(R.string.marker_waypoint_icon_url);
double length = trackTripStatisticstrackUpdater.getTripStatistics().getTotalDistance();
long duration = trackTripStatisticstrackUpdater.getTripStatistics().getTotalTime();
// Insert waypoint
Waypoint newWaypoint = new Waypoint(waypoint.getName(), waypointDescription, waypoint.getCategory(), icon, track.getId(), waypoint.getType(), length, duration, -1L, -1L, location, tripStatistics, waypoint.getPhotoUrl());
Waypoint newWaypoint = new Waypoint(waypoint.getName(), waypointDescription, waypoint.getCategory(), icon, track.getId(), length, duration, location, waypoint.getPhotoUrl());
contentProviderUtils.insertWaypoint(newWaypoint);
}
@@ -265,7 +249,12 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
}
if (category != null) {
trackData.track.setCategory(category);
trackData.track.setIcon(TrackIconUtils.getIconValue(context, category));
//TODO remove when GPX and KML support reading this property.
if (icon == null)
trackData.track.setIcon(TrackIconUtils.getIconValue(context, category));
}
if (icon != null) {
trackData.track.setIcon(icon);
}
if (trackData.tripStatisticsUpdater == null) {
trackData.tripStatisticsUpdater = new TripStatisticsUpdater(trackData.importTime);
@@ -274,7 +263,6 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
trackData.track.setTripStatistics(trackData.tripStatisticsUpdater.getTripStatistics());
trackData.track.setNumberOfPoints(trackData.numberOfLocations);
contentProviderUtils.updateTrack(trackData.track);
insertFirstWaypoint(trackData.track);
}
/**
@@ -292,10 +280,8 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
/**
* Adds a waypoint.
*
* @param type the waypoint type
*/
protected void addWaypoint(WaypointType type) throws SAXException {
protected void addWaypoint() throws SAXException {
// Waypoint must have a time, else cannot match to the track points
if (time == null) {
return;
@@ -318,7 +304,6 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
if (category != null) {
waypoint.setCategory(category);
}
waypoint.setType(type);
if (photoUrl != null) {
waypoint.setPhotoUrl(photoUrl);
@@ -372,11 +357,6 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
*/
protected void insertTrackPoint(Location location) {
insertLocation(location);
if (trackData.track.getStartId() == -1L) {
// Flush the location to set the track start id and the track end id
flushLocations(trackData);
}
}
/**
@@ -397,7 +377,7 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
if (importTrackId == -1L) {
return null;
}
File dir = FileUtils.getPhotoDir(importTrackId);
File dir = FileUtils.getPhotoDir(context, importTrackId);
File file = new File(dir, fileName);
return Uri.fromFile(file).toString();
}
@@ -494,31 +474,6 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
}
contentProviderUtils.bulkInsertTrackPoint(data.bufferedLocations, data.numBufferedLocations, data.track.getId());
data.numBufferedLocations = 0;
if (data.track.getStartId() == -1L) {
data.track.setStartId(contentProviderUtils.getFirstTrackPointId(data.track.getId()));
}
data.track.setStopId(contentProviderUtils.getLastTrackPointId(data.track.getId()));
}
/**
* Inserts the first waypoint, the track statistics waypoint.
*
* @param track the track
*/
private void insertFirstWaypoint(Track track) {
String waypointName = context.getString(R.string.marker_split_name_format, 0);
String waypointCategory = "";
TripStatisticsUpdater updater = new TripStatisticsUpdater(track.getTripStatistics().getStartTime());
TripStatistics tripStatistics = updater.getTripStatistics();
String waypointDescription = new DescriptionGeneratorImpl(context).generateWaypointDescription(tripStatistics);
String icon = context.getString(R.string.marker_statistics_icon_url);
double length = 0.0;
long duration = 0L;
Location waypointLocation = new Location("");
waypointLocation.setLatitude(100);
waypointLocation.setLongitude(180);
Waypoint waypoint = new Waypoint(waypointName, waypointDescription, waypointCategory, icon, track.getId(), WaypointType.STATISTICS, length, duration, -1L, -1L, waypointLocation, tripStatistics, "");
contentProviderUtils.insertWaypoint(waypoint);
}
/**
@@ -535,6 +490,7 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
*
* @author Jimmy Shih
*/
// TODO Why private inner class?
private class TrackData {
// The current track
Track track = new Track();
@@ -25,7 +25,6 @@ import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import de.dennisguse.opentracks.content.ContentProviderUtils;
import de.dennisguse.opentracks.content.Waypoint.WaypointType;
/**
* Imports a GPX file.
@@ -184,6 +183,6 @@ public class GpxFileTrackImporter extends AbstractFileTrackImporter {
* On waypoint end.
*/
private void onWaypointEnd() throws SAXException {
addWaypoint(WaypointType.STATISTICS.name().equals(waypointType) ? WaypointType.STATISTICS : WaypointType.WAYPOINT);
addWaypoint();
}
}
@@ -16,21 +16,20 @@
package de.dennisguse.opentracks.io.file.importer;
import android.Manifest;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.app.ProgressDialog;
import android.content.DialogInterface;
import android.content.pm.PackageManager;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.core.app.ActivityCompat;
import androidx.annotation.Nullable;
import androidx.documentfile.provider.DocumentFile;
import androidx.fragment.app.FragmentActivity;
import java.io.File;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.fragments.FileTypeDialogFragment;
import de.dennisguse.opentracks.io.file.TrackFileFormat;
@@ -38,8 +37,7 @@ import de.dennisguse.opentracks.util.DialogUtils;
import de.dennisguse.opentracks.util.FileUtils;
/**
* An activity to import files from the external storage. Optionally to import
* one specific file.
* An activity to import files from the external storage.
*
* @author Rodrigo Damazio
*/
@@ -50,70 +48,53 @@ public class ImportActivity extends FragmentActivity implements FileTypeDialogFr
private static final int DIALOG_PROGRESS_ID = 0;
private static final int DIALOG_RESULT_ID = 1;
private String directoryDisplayName;
private ImportAsyncTask importAsyncTask;
private ProgressDialog progressDialog;
// the path on the external storage to import
private String directoryDisplayName;
private int importedTrackCount;
private int totalTrackCount;
private Uri directoryUri;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (!FileUtils.isExternalStorageWriteable() || ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) {
ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, EXTERNAL_STORAGE_PERMISSION_REQUEST_CODE);
} else {
fileTypeDialogStart();
}
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
startActivityForResult(intent, EXTERNAL_STORAGE_PERMISSION_REQUEST_CODE);
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent resultData) {
super.onActivityResult(requestCode, resultCode, resultData);
if (requestCode == EXTERNAL_STORAGE_PERMISSION_REQUEST_CODE) {
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) {
Toast.makeText(this, R.string.external_storage_not_readable, Toast.LENGTH_LONG).show();
} else {
if (resultCode == Activity.RESULT_OK) {
directoryUri = resultData.getData();
fileTypeDialogStart();
} else {
Toast.makeText(this, R.string.external_storage_not_writable, Toast.LENGTH_LONG).show();
finish();
}
return;
}
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
private void fileTypeDialogStart() {
FileTypeDialogFragment.showDialog(getSupportFragmentManager(), R.string.import_selection_title, R.string.import_selection_option);
}
@Override
public void onFileTypeDone(TrackFileFormat trackFileFormat) {
if (!FileUtils.isExternalStorageAvailable()) {
Toast.makeText(this, R.string.external_storage_not_available, Toast.LENGTH_LONG).show();
finish();
return;
}
DocumentFile pickedDirectory = DocumentFile.fromTreeUri(this, directoryUri);
directoryDisplayName = FileUtils.getPathDisplayName(trackFileFormat.getExtension());
String directoryPath = FileUtils.getPath(trackFileFormat.getExtension());
if (!FileUtils.isDirectory(new File(directoryPath))) {
Toast.makeText(this, getString(R.string.import_no_directory, directoryDisplayName), Toast.LENGTH_LONG)
.show();
finish();
return;
}
directoryDisplayName = FileUtils.getPath(pickedDirectory);
//TODO (still needed?): getLastNonConfiguration instance returned ExportAsyncTask before
// Object retained = getLastNonConfigurationInstance();
// if (retained instanceof ImportAsyncTask) {
// importAsyncTask = (ImportAsyncTask) retained;
// importAsyncTask.setActivity(this);
// } else {
importAsyncTask = new ImportAsyncTask(this, trackFileFormat, directoryPath);
importAsyncTask = new ImportAsyncTask(this, trackFileFormat, pickedDirectory);
importAsyncTask.execute();
// }
}
@Override
@@ -154,8 +135,7 @@ public class ImportActivity extends FragmentActivity implements FileTypeDialogFr
} else {
iconId = R.drawable.ic_dialog_error_24dp;
titleId = R.string.generic_error_title;
message = getString(
R.string.import_error, importedTrackCount, totalFiles, directoryDisplayName);
message = getString(R.string.import_error, importedTrackCount, totalFiles, directoryDisplayName);
}
return new AlertDialog.Builder(this).setCancelable(true).setIcon(iconId)
.setMessage(message).setOnCancelListener(new DialogInterface.OnCancelListener() {
@@ -22,14 +22,15 @@ import android.os.AsyncTask;
import android.os.PowerManager.WakeLock;
import android.util.Log;
import java.io.File;
import java.io.FileInputStream;
import androidx.documentfile.provider.DocumentFile;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import de.dennisguse.opentracks.content.ContentProviderUtils;
import de.dennisguse.opentracks.content.Track;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.io.file.TrackFileFormat;
import de.dennisguse.opentracks.util.FileUtils;
import de.dennisguse.opentracks.util.PreferencesUtils;
@@ -44,7 +45,7 @@ public class ImportAsyncTask extends AsyncTask<Void, Integer, Boolean> {
private static final String TAG = ImportAsyncTask.class.getSimpleName();
private final TrackFileFormat trackFileFormat;
private final String path;
private final DocumentFile directory;
// TODO Can be removed, or?
private Context context;
// TODO Use weak reference
@@ -66,12 +67,12 @@ public class ImportAsyncTask extends AsyncTask<Void, Integer, Boolean> {
*
* @param importActivity the activity currently associated with this AsyncTask
* @param trackFileFormat the track file format
* @param path path to import GPX files
* @param directory path to import GPX files
*/
public ImportAsyncTask(ImportActivity importActivity, TrackFileFormat trackFileFormat, String path) {
public ImportAsyncTask(ImportActivity importActivity, TrackFileFormat trackFileFormat, DocumentFile directory) {
this.importActivity = importActivity;
this.trackFileFormat = trackFileFormat;
this.path = path;
this.directory = directory;
context = importActivity.getApplicationContext();
completed = false;
@@ -99,39 +100,26 @@ public class ImportAsyncTask extends AsyncTask<Void, Integer, Boolean> {
}
}
@Override
protected Boolean doInBackground(Void... params) {
try {
Thread.currentThread().setPriority(Thread.MAX_PRIORITY); // TODO Should be set to previous level at the end of this method.
// Get the wake lock if not recording or paused
boolean isRecording = PreferencesUtils.isRecording(importActivity);
boolean isPaused = PreferencesUtils.isRecordingTrackPaused(importActivity);
if (!isRecording || isPaused) {
wakeLock = SystemUtils.acquireWakeLock(importActivity, wakeLock);
}
/**
* Gets a list of files.
* If importAll is true, returns a list of the files under the path directory.
* If importAll is false, returns a list containing just the path file.
*/
private static List<DocumentFile> getFiles(DocumentFile file, TrackFileFormat trackFileFormat) {
List<DocumentFile> files = new ArrayList<>();
List<File> files = getFiles();
totalTrackCount = files.size();
if (totalTrackCount == 0) {
return true;
}
for (int i = 0; i < totalTrackCount; i++) {
if (isCancelled()) {
// If cancelled, return true to show the number of files imported
return true;
for (DocumentFile candidate : file.listFiles()) {
if (!candidate.isDirectory()) {
String extension = FileUtils.getExtension(candidate.getName());
if (trackFileFormat.getExtension().equals(extension)) {
files.add(candidate);
}
if (importFile(files.get(i))) {
importTrackCount++;
}
publishProgress(i + 1, totalTrackCount);
}
return true;
} finally {
if (wakeLock != null && wakeLock.isHeld()) {
wakeLock.release();
} else {
files.addAll(getFiles(candidate, trackFileFormat));
}
}
return files;
}
@Override
@@ -159,16 +147,49 @@ public class ImportAsyncTask extends AsyncTask<Void, Integer, Boolean> {
}
}
@Override
protected Boolean doInBackground(Void... params) {
try {
Thread.currentThread().setPriority(Thread.MAX_PRIORITY); // TODO Should be set to previous level at the end of this method.
// Get the wake lock if not recording or paused
boolean isRecording = PreferencesUtils.isRecording(importActivity);
boolean isPaused = PreferencesUtils.isRecordingTrackPaused(importActivity);
if (!isRecording || isPaused) {
wakeLock = SystemUtils.acquireWakeLock(importActivity, wakeLock);
}
List<DocumentFile> files = getFiles(directory, trackFileFormat);
totalTrackCount = files.size();
for (int i = 0; i < totalTrackCount; i++) {
if (isCancelled()) {
// If cancelled, return true to show the number of files imported
return true;
}
if (importFile(files.get(i))) {
importTrackCount++;
}
publishProgress(i + 1, totalTrackCount);
}
return true;
} finally {
if (wakeLock != null && wakeLock.isHeld()) {
wakeLock.release();
}
}
}
/**
* Imports a file.
*
* @param file the file
*/
private boolean importFile(final File file) {
private boolean importFile(final DocumentFile file) {
TrackImporter trackImporter;
if (trackFileFormat == TrackFileFormat.GPX) {
trackImporter = new GpxFileTrackImporter(importActivity);
} else { //KML or KMZ
} else {
//KML or KMZ
String extension = FileUtils.getExtension(file.getName());
if (TrackFileFormat.KML_ONLY_TRACK.getExtension().equals(extension)) {
trackImporter = new KmlFileTrackImporter(importActivity, -1L);
@@ -181,37 +202,12 @@ public class ImportAsyncTask extends AsyncTask<Void, Integer, Boolean> {
}
}
try (FileInputStream fileInputStream = new FileInputStream(file)) {
trackId = trackImporter.importFile(fileInputStream);
try (InputStream inputStream = context.getContentResolver().openInputStream(file.getUri())) {
trackId = trackImporter.importFile(inputStream);
return trackId != -1L;
} catch (IOException e) {
Log.e(TAG, "Unable to import file", e);
return false;
}
}
/**
* Gets a list of files.
* If importAll is true, returns a list of the files under the path directory.
* If importAll is false, returns a list containing just the path file.
*/
private List<File> getFiles() {
List<File> files = new ArrayList<>();
File file = new File(path);
File[] candidates = file.listFiles();
if (candidates == null) {
return files;
}
for (File candidate : candidates) {
if (!FileUtils.isDirectory(candidate)) {
String extension = FileUtils.getExtension(candidate.getName());
if (trackFileFormat.getExtension().equals(extension)) {
files.add(candidate);
}
}
}
return files;
}
}
@@ -29,9 +29,9 @@ import org.xml.sax.SAXException;
import java.util.ArrayList;
import de.dennisguse.opentracks.content.ContentProviderUtils;
import de.dennisguse.opentracks.content.SensorDataSetLocation;
import de.dennisguse.opentracks.content.Waypoint.WaypointType;
import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.content.sensor.SensorDataSet;
import de.dennisguse.opentracks.io.file.exporter.KmlTrackWriter;
/**
* Imports a KML file.
@@ -42,15 +42,11 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter {
private static final String TAG = KmlFileTrackImporter.class.getSimpleName();
private static final String CADENCE = "cadence";
private static final String HEART_RATE = "heart_rate";
private static final String POWER = "power";
private static final String STATISTICS_STYLE = "#statistics";
private static final String WAYPOINT_STYLE = "#waypoint";
private static final String WAYPOINT_STYLE = "#" + KmlTrackWriter.WAYPOINT_STYLE;
private static final String TAG_COORDINATES = "coordinates";
private static final String TAG_DESCRIPTION = "description";
private static final String TAG_ICON = "icon";
private static final String TAG_GX_COORD = "gx:coord";
private static final String TAG_GX_MULTI_TRACK = "gx:MultiTrack";
private static final String TAG_GX_SIMPLE_ARRAY_DATA = "gx:SimpleArrayData";
@@ -140,6 +136,10 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter {
if (content != null) {
description = content.trim();
}
} else if (localName.equals(TAG_ICON)) {
if (content != null) {
icon = content.trim();
}
} else if (localName.equals(TAG_VALUE)) {
if (content != null) {
category = content.trim();
@@ -168,6 +168,7 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter {
private void onWaypointStart() {
// Reset all Placemark variables
name = null;
icon = null;
description = null;
category = null;
photoUrl = null;
@@ -182,29 +183,16 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter {
* On waypoint end.
*/
private void onWaypointEnd() throws SAXException {
if (waypointType == null) {
if (!WAYPOINT_STYLE.equals(waypointType)) {
return;
}
// Add a waypoint if the waypointType matches
WaypointType type;
switch (waypointType) {
case WAYPOINT_STYLE:
type = WaypointType.WAYPOINT;
break;
case STATISTICS_STYLE:
type = WaypointType.STATISTICS;
break;
default:
return;
}
if (photoUrl != null) {
Uri uri = Uri.parse(photoUrl);
photoUrl = getPhotoUrl(uri.getLastPathSegment());
}
addWaypoint(type);
addWaypoint();
}
/**
@@ -260,7 +248,7 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter {
if (!hasSensorData) {
insertTrackPoint(location);
} else {
SensorDataSetLocation sensorDataSetLocation = new SensorDataSetLocation(location, new SensorDataSet(heartrate, cadence, power, SensorDataSet.DATA_UNAVAILABLE, location.getTime()));
TrackPoint sensorDataSetLocation = new TrackPoint(location, new SensorDataSet(heartrate, cadence, power, SensorDataSet.DATA_UNAVAILABLE, location.getTime()));
insertTrackPoint(sensorDataSetLocation);
}
}
@@ -315,13 +303,13 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter {
throw new SAXException(createErrorMessage("Unable to parse gx:value:" + content), e);
}
switch (sensorType) {
case POWER:
case KmlTrackWriter.SENSOR_TYPE_POWER:
powerList.add(value);
break;
case HEART_RATE:
case KmlTrackWriter.SENSOR_TYPE_HEART_RATE:
heartRateList.add(value);
break;
case CADENCE:
case KmlTrackWriter.SENSOR_TYPE_CADENCE:
cadenceList.add(value);
break;
default:
@@ -17,7 +17,6 @@
package de.dennisguse.opentracks.io.file.importer;
import android.content.Context;
import android.net.Uri;
import android.util.Log;
import java.io.ByteArrayInputStream;
@@ -68,7 +67,7 @@ public class KmzTrackImporter implements TrackImporter {
while ((zipEntry = zipInputStream.getNextEntry()) != null) {
if (Thread.interrupted()) {
Log.d(TAG, "Thread interrupted");
cleanImport(trackId);
cleanImport(context, trackId);
return -1L;
}
String fileName = zipEntry.getName();
@@ -76,7 +75,7 @@ public class KmzTrackImporter implements TrackImporter {
trackId = parseKml(zipInputStream);
if (trackId == -1L) {
Log.d(TAG, "Unable to parse kml in kmz");
cleanImport(trackId);
cleanImport(context, trackId);
return -1L;
}
} else {
@@ -90,7 +89,7 @@ public class KmzTrackImporter implements TrackImporter {
return trackId;
} catch (IOException e) {
Log.e(TAG, "Unable to import file", e);
cleanImport(trackId);
cleanImport(context, trackId);
return -1L;
}
}
@@ -100,20 +99,19 @@ public class KmzTrackImporter implements TrackImporter {
*
* @param trackId the trackId
*/
private void cleanImport(long trackId) {
private void cleanImport(Context context, long trackId) {
if (PreferencesUtils.isRecording(trackId)) {
ContentProviderUtils contentProviderUtils = new ContentProviderUtils(context);
contentProviderUtils.deleteTrack(context, trackId);
}
if (importTrackId != -1L) {
File dir = FileUtils.getPhotoDir(importTrackId);
if (FileUtils.isDirectory(dir)) {
File dir = FileUtils.getPhotoDir(context, importTrackId);
if (dir.exists() && dir.isDirectory()) {
for (File file : dir.listFiles()) {
file.delete();
}
dir.delete();
FileUtils.updateMediaScanner(context, Uri.fromFile(dir));
}
}
}
@@ -159,8 +157,7 @@ public class KmzTrackImporter implements TrackImporter {
return;
}
File dir = FileUtils.getPhotoDir(importTrackId);
FileUtils.ensureDirectoryExists(dir);
File dir = FileUtils.getPhotoDir(context, importTrackId);
File file = new File(dir, fileName);
try (FileOutputStream fileOutputStream = new FileOutputStream(file)) {
@@ -169,8 +166,6 @@ public class KmzTrackImporter implements TrackImporter {
while ((count = zipInputStream.read(buffer)) != -1) {
fileOutputStream.write(buffer, 0, count);
}
FileUtils.updateMediaScanner(context, Uri.fromFile(file));
}
}
}
@@ -19,7 +19,6 @@ import android.location.Location;
import androidx.annotation.VisibleForTesting;
import de.dennisguse.opentracks.content.WaypointCreationRequest;
import de.dennisguse.opentracks.content.sensor.SensorDataSet;
/**
@@ -83,10 +82,9 @@ public interface ITrackRecordingService {
/**
* Inserts a waypoint in the current recording track.
*
* @param request the details of the waypoint to be inserted
* @return the ID of the inserted waypoint
*/
long insertWaypoint(WaypointCreationRequest request);
long insertWaypoint(String name, String category, String description, String photoUrl);
/**
* ONLY FOR TESTING
@@ -44,19 +44,15 @@ import de.dennisguse.opentracks.TrackDetailActivity;
import de.dennisguse.opentracks.TrackListActivity;
import de.dennisguse.opentracks.content.ContentProviderUtils;
import de.dennisguse.opentracks.content.CustomContentProvider;
import de.dennisguse.opentracks.content.DescriptionGeneratorImpl;
import de.dennisguse.opentracks.content.LocationFactory;
import de.dennisguse.opentracks.content.LocationIterator;
import de.dennisguse.opentracks.content.SensorDataSetLocation;
import de.dennisguse.opentracks.content.Track;
import de.dennisguse.opentracks.content.Waypoint;
import de.dennisguse.opentracks.content.Waypoint.WaypointType;
import de.dennisguse.opentracks.content.WaypointCreationRequest;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.content.data.Waypoint;
import de.dennisguse.opentracks.content.sensor.SensorDataSet;
import de.dennisguse.opentracks.services.sensors.BluetoothRemoteSensorManager;
import de.dennisguse.opentracks.services.tasks.AnnouncementPeriodicTaskFactory;
import de.dennisguse.opentracks.services.tasks.PeriodicTaskExecutor;
import de.dennisguse.opentracks.services.tasks.SplitPeriodicTaskFactory;
import de.dennisguse.opentracks.stats.TripStatistics;
import de.dennisguse.opentracks.stats.TripStatisticsUpdater;
import de.dennisguse.opentracks.util.IntentUtils;
@@ -75,8 +71,6 @@ import de.dennisguse.opentracks.util.UnitConversions;
*/
public class TrackRecordingService extends Service {
// The name of extra intent property to indicate whether we want to resume a previously recorded track.
public static final String RESUME_TRACK_EXTRA_NAME = "RESUME_TRACK";
private static final int NOTIFICATION_ID = 123;
public static final double PAUSE_LATITUDE = 100.0;
@@ -84,8 +78,6 @@ public class TrackRecordingService extends Service {
// Anything faster than that (in meters per second) will be considered moving.
public static final double MAX_NO_MOVEMENT_SPEED = 0.224;
@VisibleForTesting
static final int MAX_AUTO_RESUME_TRACK_RETRY_ATTEMPTS = 3;
private static final String TAG = TrackRecordingService.class.getSimpleName();
// 1 minute in milliseconds
private static final long ONE_MINUTE = (long) (UnitConversions.MIN_TO_S * UnitConversions.S_TO_MS);
@@ -95,7 +87,6 @@ public class TrackRecordingService extends Service {
private Handler handler;
private LocationManagerConnector locationManagerConnector;
private PeriodicTaskExecutor voiceExecutor;
private PeriodicTaskExecutor splitExecutor;
private SharedPreferences sharedPreferences;
private TrackRecordingServiceNotificationManager notificationManager;
private long recordingTrackId;
@@ -104,7 +95,6 @@ public class TrackRecordingService extends Service {
private int recordingDistanceInterval;
private int maxRecordingDistance;
private int recordingGpsAccuracy;
private int autoResumeTrackTimeout;
private long currentRecordingInterval;
// The following variables are set when recording:
@@ -126,14 +116,10 @@ public class TrackRecordingService extends Service {
if (PreferencesUtils.isKey(context, R.string.stats_units_key, key)) {
boolean metricUnits = PreferencesUtils.isMetricUnits(context);
voiceExecutor.setMetricUnits(metricUnits);
splitExecutor.setMetricUnits(metricUnits);
}
if (PreferencesUtils.isKey(context, R.string.voice_frequency_key, key)) {
voiceExecutor.setTaskFrequency(PreferencesUtils.getVoiceFrequency(context));
}
if (PreferencesUtils.isKey(context, R.string.split_frequency_key, key)) {
splitExecutor.setTaskFrequency(PreferencesUtils.getSplitFrequency(context));
}
if (PreferencesUtils.isKey(context, R.string.min_recording_interval_key, key)) {
int minRecordingInterval = PreferencesUtils.getMinRecordingInterval(context);
if (minRecordingInterval == PreferencesUtils.getMinRecordingIntervalAdaptBatteryLife(context)) {
@@ -155,12 +141,9 @@ public class TrackRecordingService extends Service {
if (PreferencesUtils.isKey(context, R.string.recording_gps_accuracy_key, key)) {
recordingGpsAccuracy = PreferencesUtils.getRecordingGPSAccuracy(context);
}
if (PreferencesUtils.isKey(context, R.string.auto_resume_track_timeout_key, key)) {
autoResumeTrackTimeout = PreferencesUtils.getAutoResumeTrackTimeout(context);
}
}
};
private TripStatisticsUpdater markerTripStatisticsUpdater;
private WakeLock wakeLock;
private BluetoothRemoteSensorManager remoteSensorManager;
private Location lastLocation;
@@ -219,7 +202,6 @@ public class TrackRecordingService extends Service {
handler = new Handler();
locationManagerConnector = new LocationManagerConnector(this, handler.getLooper());
voiceExecutor = new PeriodicTaskExecutor(this, new AnnouncementPeriodicTaskFactory());
splitExecutor = new PeriodicTaskExecutor(this, new SplitPeriodicTaskFactory());
sharedPreferences = PreferencesUtils.getSharedPreferences(this);
sharedPreferences.registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
notificationManager = new TrackRecordingServiceNotificationManager(this);
@@ -247,7 +229,6 @@ public class TrackRecordingService extends Service {
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
handleStartCommand(intent, startId);
return START_STICKY;
}
@@ -272,12 +253,6 @@ public class TrackRecordingService extends Service {
// unregister sharedPreferences before shutting down splitExecutor and voiceExecutor
sharedPreferences.unregisterOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
try {
splitExecutor.shutdown();
} finally {
splitExecutor = null;
}
try {
voiceExecutor.shutdown();
} finally {
@@ -298,23 +273,14 @@ public class TrackRecordingService extends Service {
super.onDestroy();
}
/**
* Returns true if the service is recording.
*/
public boolean isRecording() {
return PreferencesUtils.isRecording(recordingTrackId);
}
/**
* Returns true if the current recording is paused.
*/
public boolean isPaused() {
return recordingTrackPaused;
}
/**
* Gets the trip statistics.
*/
public TripStatistics getTripStatistics() {
if (trackTripStatisticsUpdater == null) {
return null;
@@ -325,130 +291,42 @@ public class TrackRecordingService extends Service {
/**
* Inserts a waypoint.
*
* @param waypointCreationRequest the waypoint creation request
* @return the waypoint id
*/
public long insertWaypoint(WaypointCreationRequest waypointCreationRequest) {
public long insertWaypoint(String name, String category, String description, String photoUrl) {
if (!isRecording() || isPaused()) {
return -1L;
}
WaypointType waypointType = waypointCreationRequest.getType();
boolean isStatistics = waypointType == WaypointType.STATISTICS;
// Get name
String name;
if (waypointCreationRequest.getName() != null) {
name = waypointCreationRequest.getName();
} else {
int nextWaypointNumber = contentProviderUtils.getNextWaypointNumber(recordingTrackId, waypointType);
if (name == null) {
int nextWaypointNumber = contentProviderUtils.getNextWaypointNumber(recordingTrackId);
if (nextWaypointNumber == -1) {
nextWaypointNumber = 0;
nextWaypointNumber = 1;
}
name = getString(isStatistics ? R.string.marker_split_name_format : R.string.marker_name_format, nextWaypointNumber);
name = getString(R.string.marker_name_format, nextWaypointNumber + 1);
}
// Get category
String category = waypointCreationRequest.getCategory() != null ? waypointCreationRequest.getCategory() : "";
// Get tripStatistics, description, and icon
TripStatistics tripStatistics;
String description;
String icon;
if (isStatistics) {
long now = System.currentTimeMillis();
markerTripStatisticsUpdater.updateTime(now);
tripStatistics = markerTripStatisticsUpdater.getTripStatistics();
markerTripStatisticsUpdater = new TripStatisticsUpdater(now);
description = new DescriptionGeneratorImpl(this).generateWaypointDescription(tripStatistics);
icon = getString(R.string.marker_statistics_icon_url);
} else {
tripStatistics = null;
description = waypointCreationRequest.getDescription() != null ? waypointCreationRequest.getDescription() : "";
//TODO Bundle icon?
icon = getString(R.string.marker_waypoint_icon_url);
}
// Get length and duration
double length;
long duration;
Location location = getLastValidTrackPointInCurrentSegment(recordingTrackId);
if (location != null && trackTripStatisticsUpdater != null) {
TripStatistics stats = trackTripStatisticsUpdater.getTripStatistics();
length = stats.getTotalDistance();
duration = stats.getTotalTime();
} else {
if (!waypointCreationRequest.isTrackStatistics()) {
return -1L;
}
// For track statistics, make it an impossible location
location = new Location("");
location.setLatitude(100);
location.setLongitude(180);
length = 0.0;
duration = 0L;
if (location == null) {
Log.i(TAG, "Could not create a waypoint as location is unknown.");
return -1L;
}
String photoUrl = waypointCreationRequest.getPhotoUrl() != null ? waypointCreationRequest.getPhotoUrl() : "";
category = category != null ? category : "";
description = description != null ? description : "";
String icon = getString(R.string.marker_waypoint_icon_url);
photoUrl = photoUrl != null ? photoUrl : "";
TripStatistics stats = trackTripStatisticsUpdater.getTripStatistics();
double length = stats.getTotalDistance();
long duration = stats.getTotalTime();
// Insert waypoint
Waypoint waypoint = new Waypoint(name, description, category, icon, recordingTrackId, waypointType, length, duration, -1L, -1L, location, tripStatistics, photoUrl);
Waypoint waypoint = new Waypoint(name, description, category, icon, recordingTrackId, length, duration, location, photoUrl);
Uri uri = contentProviderUtils.insertWaypoint(waypoint);
return Long.parseLong(uri.getLastPathSegment());
}
/**
* Handles start command.
*
* @param intent the intent
* @param startId the start id
*/
private void handleStartCommand(Intent intent, int startId) {
// Check if the service is called to resume track (from phone reboot)
boolean resumeTrackRequested = intent.getBooleanExtra(RESUME_TRACK_EXTRA_NAME, false);
if (resumeTrackRequested && !shouldResumeTrack()) {
Log.i(TAG, "Stop resume track.");
updateRecordingState(PreferencesUtils.RECORDING_TRACK_ID_DEFAULT, true);
stopSelfResult(startId);
}
}
/**
* Returns true if should resume.
*/
private boolean shouldResumeTrack() {
Track track = contentProviderUtils.getTrack(recordingTrackId);
if (track == null) {
Log.d(TAG, "Not resuming. Track is null.");
return false;
}
int retries = PreferencesUtils.getAutoResumeTrackCurrentRetryDefault(this);
if (retries >= MAX_AUTO_RESUME_TRACK_RETRY_ATTEMPTS) {
Log.d(TAG, "Not resuming. Exceeded maximum retry attempts.");
return false;
}
PreferencesUtils.incrementAutoResumeTrackCurrentRetryDefault(this);
if (autoResumeTrackTimeout == Integer.parseInt(getResources().getString(R.string.auto_resume_track_timeout_never))) {
Log.d(TAG, "Not resuming. Auto-resume track timeout set to never.");
return false;
}
if (autoResumeTrackTimeout == Integer.parseInt(getResources().getString(R.string.auto_resume_track_timeout_always))) {
Log.d(TAG, "Resuming. Auto-resume track timeout set to always.");
return true;
}
if (track.getTripStatistics() == null) {
Log.d(TAG, "Not resuming. No trip statistics.");
return false;
}
long stopTime = track.getTripStatistics().getStopTime();
return stopTime > 0 && (System.currentTimeMillis() - stopTime) <= autoResumeTrackTimeout * ONE_MINUTE;
}
/**
* Starts a new track.
*
@@ -461,7 +339,6 @@ public class TrackRecordingService extends Service {
}
long now = System.currentTimeMillis();
trackTripStatisticsUpdater = new TripStatisticsUpdater(now);
markerTripStatisticsUpdater = new TripStatisticsUpdater(now);
// Insert a track
Track track = new Track();
@@ -470,7 +347,6 @@ public class TrackRecordingService extends Service {
// Update shared preferences
updateRecordingState(trackId, false);
PreferencesUtils.resetAutoResumeTrackCurrentRetryDefault(this);
// Update database
track.setId(trackId);
@@ -481,7 +357,6 @@ public class TrackRecordingService extends Service {
track.setIcon(TrackIconUtils.getIconValue(this, category));
track.setTripStatistics(trackTripStatisticsUpdater.getTripStatistics());
contentProviderUtils.updateTrack(track);
insertWaypoint(WaypointCreationRequest.DEFAULT_START_TRACK);
startRecording(true);
return trackId;
@@ -498,23 +373,11 @@ public class TrackRecordingService extends Service {
TripStatistics tripStatistics = track.getTripStatistics();
trackTripStatisticsUpdater = new TripStatisticsUpdater(tripStatistics.getStartTime());
long markerStartTime;
Waypoint waypoint = contentProviderUtils.getLastWaypoint(recordingTrackId, WaypointType.STATISTICS);
if (waypoint != null && waypoint.getTripStatistics() != null) {
markerStartTime = waypoint.getTripStatistics().getStopTime();
} else {
markerStartTime = tripStatistics.getStartTime();
}
markerTripStatisticsUpdater = new TripStatisticsUpdater(markerStartTime);
try (LocationIterator locationIterator = contentProviderUtils.getTrackPointLocationIterator(track.getId(), -1L, false, LocationFactory.DEFAULT_LOCATION_FACTORY)) {
while (locationIterator.hasNext()) {
Location location = locationIterator.next();
trackTripStatisticsUpdater.addLocation(location, recordingDistanceInterval);
if (location.getTime() > markerStartTime) {
markerTripStatisticsUpdater.addLocation(location, recordingDistanceInterval);
}
}
} catch (RuntimeException e) {
Log.e(TAG, "RuntimeException", e);
@@ -549,8 +412,7 @@ public class TrackRecordingService extends Service {
}
/**
* Common code for starting a new track, resuming a track, or restarting after
* phone reboot.
* Common code for starting a new track, resuming a track, or restarting after phone reboot.
*
* @param trackStarted true if track is started, false if track is resumed
*/
@@ -566,7 +428,6 @@ public class TrackRecordingService extends Service {
// Restore periodic tasks
voiceExecutor.restore();
splitExecutor.restore();
}
/**
@@ -602,7 +463,7 @@ public class TrackRecordingService extends Service {
insertLocation(track, lastLocation, getLastValidTrackPointInCurrentSegment(trackId));
// Update the recording track time
updateRecordingTrack(track, contentProviderUtils.getLastTrackPointId(trackId), false);
updateRecordingTrack(track, false);
}
String trackName = TrackNameUtils.getTrackName(this, trackId, track.getTripStatistics().getStartTime());
@@ -611,7 +472,7 @@ public class TrackRecordingService extends Service {
contentProviderUtils.updateTrack(track);
}
}
endRecording(true, trackId);
endRecording(true);
}
/**
@@ -639,7 +500,7 @@ public class TrackRecordingService extends Service {
insertLocation(track, pause, null);
}
endRecording(false, recordingTrackId);
endRecording(false);
notificationManager.updateContent(getString(R.string.generic_paused));
}
@@ -648,12 +509,10 @@ public class TrackRecordingService extends Service {
* Common code for ending a track or pausing a track.
*
* @param trackStopped true if track is stopped, false if track is paused
* @param trackId the track id
*/
private void endRecording(boolean trackStopped, long trackId) {
private void endRecording(boolean trackStopped) {
// Shutdown periodic tasks
voiceExecutor.shutdown();
splitExecutor.shutdown();
// Update instance variables
if (remoteSensorManager != null) {
@@ -753,7 +612,7 @@ public class TrackRecordingService extends Service {
SensorDataSet sensorDataSet = getSensorDataSet();
if (sensorDataSet != null) {
location = new SensorDataSetLocation(location, sensorDataSet);
location = new TrackPoint(location, sensorDataSet);
}
// Always insert the first segment location
@@ -828,10 +687,8 @@ public class TrackRecordingService extends Service {
try {
Uri uri = contentProviderUtils.insertTrackPoint(location, track.getId());
long trackPointId = Long.parseLong(uri.getLastPathSegment());
trackTripStatisticsUpdater.addLocation(location, recordingDistanceInterval);
markerTripStatisticsUpdater.addLocation(location, recordingDistanceInterval);
updateRecordingTrack(track, trackPointId, LocationUtils.isValidLocation(location));
updateRecordingTrack(track, LocationUtils.isValidLocation(location));
} catch (SQLiteException e) {
/*
* Insert failed, most likely because of SqlLite error code 5 (SQLite_BUSY).
@@ -840,7 +697,6 @@ public class TrackRecordingService extends Service {
Log.w(TAG, "SQLiteException", e);
}
voiceExecutor.update();
splitExecutor.update();
}
/**
@@ -848,16 +704,9 @@ public class TrackRecordingService extends Service {
* Increase the number of points if it is a new and valid track point.
*
* @param track the track
* @param lastTrackPointId the last track point id
* @param increaseNumberOfPoints true to increase the number of points
*/
private void updateRecordingTrack(Track track, long lastTrackPointId, boolean increaseNumberOfPoints) {
if (lastTrackPointId >= 0) {
if (track.getStartId() < 0) {
track.setStartId(lastTrackPointId);
}
track.setStopId(lastTrackPointId);
}
private void updateRecordingTrack(Track track, boolean increaseNumberOfPoints) {
if (increaseNumberOfPoints) {
track.setNumberOfPoints(track.getNumberOfPoints() + 1);
}
@@ -1019,8 +868,8 @@ public class TrackRecordingService extends Service {
}
@Override
public long insertWaypoint(WaypointCreationRequest waypointCreationRequest) {
return trackRecordingService.insertWaypoint(waypointCreationRequest);
public long insertWaypoint(String name, String category, String description, String photoUrl) {
return trackRecordingService.insertWaypoint(name, category, description, photoUrl);
}
@VisibleForTesting
@@ -1031,15 +880,7 @@ public class TrackRecordingService extends Service {
@Override
public SensorDataSet getSensorData() {
if (trackRecordingService.remoteSensorManager == null) {
Log.d(TAG, "remoteSensorManager is null.");
return null;
}
if (trackRecordingService.getSensorDataSet() == null) {
Log.d(TAG, "Sensor data set is null.");
return null;
}
return trackRecordingService.remoteSensorManager.getSensorDataSet();
return trackRecordingService.getSensorDataSet();
}
/**

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