Compare commits

...

70 Commits

Author SHA1 Message Date
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
Dennis Guse c0672c41df Bugfix: crash if heart rate sensor was null (i.e., preference not initialized). 2019-11-29 23:18:49 +01:00
Dennis Guse 4c6601fbc5 Release v3.2.8 2019-11-29 22:58:22 +01:00
edern-appere b99c87d1bd Added French translation for Fdroid. 2019-11-29 22:46:43 +01:00
Dennis Guse 66ffbfe951 Fixed ChartFragment tests.
Initialization of looper was missing.
2019-11-29 22:43:51 +01:00
Dennis Guse 8cb01a1027 Bugfix: sharing a track was not adding the file(s).
While migrating to enable sharing multiple tracks a configuration option was missed.
2019-11-29 22:39:36 +01:00
Dennis Guse 193e5eccbb Cleanup. 2019-11-29 22:39:36 +01:00
Dennis Guse bf7fc28bb0 Bugfix: picture name suffixes were malformed on export and thus could not be re-imported. 2019-11-29 22:39:31 +01:00
Dennis Guse e35cbc343a Add pull request template. 2019-11-29 07:54:39 +01:00
Dennis Guse 1c7b053c8c Fixed MarkerDetailActivity.
The orientation of the layout was not set and thus adding the toolbar broke the UI.
2019-11-28 18:09:11 +01:00
Dennis Guse 88cdcd4b47 Use GestureDetector rather than doing it on our own. 2019-11-27 21:42:05 +01:00
Dennis Guse 1432fed8f9 Removed zoom controls for ChartFragment. 2019-11-27 21:41:53 +01:00
Dennis Guse a14307b9a4 Moved Chart* into own package. 2019-11-27 20:19:12 +01:00
Dennis Guse 3bdce5aa08 Cleanup: use autocloseable. 2019-11-25 23:09:08 +01:00
Dennis Guse d7526f93b8 Cleanup: removed ContentProviderUtils interface. 2019-11-25 23:02:48 +01:00
Dennis Guse 8932578b90 Release v3.2.7 2019-11-25 22:03:40 +01:00
Dennis Guse 631ac9881d Wrap help into PreferenceCategory. 2019-11-25 22:03:02 +01:00
Dennis Guse 78334ed797 Fixed Spanish project description (short); was still German. 2019-11-24 21:12:07 +01:00
Dennis Guse b06da89654 Show chart for distance as well as time.
Also removed configuration option.
2019-11-24 19:55:00 +01:00
Dennis Guse d6c2e78993 Cleanup. 2019-11-24 18:48:31 +01:00
Dennis Guse 67d1ac551d Show GPS accuracy while recording in the notification of the TrackRecordingService and notify if accuracy goes beyond configured threshold. 2019-11-24 18:44:04 +01:00
Dennis Guse dcb6be717d Cleanup. 2019-11-23 22:46:39 +01:00
Dennis Guse f2c04eec1a Add help activity.
Can be accessed from Settings.
2019-11-23 21:43:07 +01:00
Dennis Guse a08311cf43 Temporary bugfix: disable landscape mode for TrackListActivity. 2019-11-22 07:44:01 +01:00
Dennis Guse 1e28f3aa86 Add TTSUtils.
(required by previous commit)
2019-11-22 07:43:19 +01:00
Dennis Guse 426af41f2c Move TTS stream definition into one place. 2019-11-20 19:27:41 +01:00
Dennis Guse fd08a219f9 Add Spanish store description. 2019-11-20 19:13:21 +01:00
Dennis Guse 25b6aec0e9 Release v3.2.6 2019-11-19 22:09:10 +01:00
Dennis Guse 3e40209d15 Refactoring. 2019-11-19 08:00:12 +01:00
Dennis Guse 54ff94f905 Settings: renamed Bluetooth sensor to "Heart rate". 2019-11-18 17:16:50 +01:00
Dennis Guse 7a4faf6b0d Fixed SettingsActivity. 2019-11-18 17:13:42 +01:00
Dennis Guse 8922b00a5f README: add screenshots. 2019-11-17 18:45:29 +01:00
Dennis Guse 702efa8693 Removed shortcut to Android's Bluetooth settings. 2019-11-17 18:25:01 +01:00
Dennis Guse e7a8c897a3 Settings: scan for Bluetooth sensors rather than using bonded devices.
Bonded devices do no provide information what services (e.g., heart rate) they support.
2019-11-17 18:24:02 +01:00
Dennis Guse c59d1d87bd Bluetooth: move getting adapter to BluetoothUtils. 2019-11-17 18:23:08 +01:00
Dennis Guse a706efb8b3 Bugfix. 2019-11-14 18:30:57 +01:00
Dennis Guse e391e83a00 Build: show version and tag in about page. 2019-11-14 18:04:50 +01:00
Dennis Guse 2940bf20d8 Settings: remove all defaults from code. 2019-11-12 21:33:04 +01:00
Dennis Guse 081db5a58b Settings: remove default setting for default activity. 2019-11-12 21:33:04 +01:00
130 changed files with 4834 additions and 3881 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
+1 -1
View File
@@ -1,6 +1,6 @@
---
name: Bug report
about: Report issues of Opentracks features
about: Report issues of OpenTracks features
title: ''
labels: potential bug
assignees: ''
+10
View File
@@ -0,0 +1,10 @@
# Thanks for your contribution.
**Describe the pull request**
A clear and concise description of what the pull request changes/adds.
**Link to the the issue**
(If available): The link to the issue that this pull request solves.
**License agreement**
By opening this pull request, you are providing your contribution under the _Apache License 2.0_ (see [LICENSE.md](LICENSE.md)).
+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
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RenderSettings">
<option name="showDecorations" value="true" />
</component>
</project>
+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/
+23 -3
View File
@@ -1,6 +1,6 @@
# OpenTrack: Developer information
# OpenTracks: Developer information
## Create releases for FDroid
## Craft new releases
1. Get next _version code_ (master branch): `git rev-list HEAD --count master`
2. Decide on _version name_ (semantic versioning)
@@ -9,7 +9,27 @@
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)
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. Update store meta data (e.g., description and screenshots)
`fastlane supply --version_code $(git rev-list HEAD --count master) --skip_upload_apk`
(requires API key)
2. Build signed apk
(requires keystore)
3. Upload signed apk to [Google Play Console](https://play.google.com/apps/publish)
## Translations (localization)
+3
View File
@@ -0,0 +1,3 @@
source "https://rubygems.org"
gem "fastlane"
+32 -6
View File
@@ -2,9 +2,30 @@
_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
@@ -26,9 +47,14 @@ _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>
## Screenshots
<div>
<img width="20%" src="fastlane/metadata/android/en-US/phoneScreenshots/screenshot_1.png">
<img width="20%" src="fastlane/metadata/android/en-US/phoneScreenshots/screenshot_2.png">
<img width="20%" src="fastlane/metadata/android/en-US/phoneScreenshots/screenshot_3.png">
<img width="20%" src="fastlane/metadata/android/en-US/phoneScreenshots/screenshot_4.png">
</div>
## Project history
+12 -7
View File
@@ -6,7 +6,7 @@ buildscript {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.5.0'
classpath 'com.android.tools.build:gradle:3.5.2'
}
}
@@ -35,10 +35,14 @@ def getVersionName = { ->
try {
def stdout = new ByteArrayOutputStream()
exec {
commandLine 'git', 'describe', '--tags', '--dirty'
commandLine 'git', 'describe', '--tags'
standardOutput = stdout
}
return stdout.toString().trim()
exec {
commandLine 'git', 'log', '--format="%h"', '-n 1'
standardOutput = stdout
}
return stdout.toString().replace('\n', '-').replace('"', '').trim()
}
catch (ignored) {
return null
@@ -70,15 +74,16 @@ android {
release {
minifyEnabled false
}
releasePlayStore {
applicationIdSuffix ".playstore"
versionNameSuffix "-PlayStore"
}
}
applicationVariants.all { variant ->
variant.resValue "string", "applicationId", variant.applicationId
}
lintOptions {
disable "AppCompatResource" // TODO Disable complaint to use androidx's SearchView as it is null.
}
}
dependencies {
+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
@@ -1,4 +1,4 @@
Open Tracks – ein GPS-Tracker, der deine Daten für sich behält.
OpenTracks – ein GPS-Tracker, der deine Daten für sich behält.
Erfasst deine Kilometer, ohne dass es jemand anderes tut.
Funktionen & Features:
@@ -1 +1 @@
Open Tracks – ein GPS-Tracker, der deine Daten für sich behält.
OpenTracks – ein GPS-Tracker, der deine Daten für sich behält.
@@ -0,0 +1,8 @@
v3.2.6: OpenTracks
Changes:
- Heart rate monitor: scan for available devices
- Remove shortcut to Bluetooth settings
Developer:
- Preferences: moved default of preferences from code to xml
@@ -0,0 +1,10 @@
v3.2.7: OpenTracks
Changes:
- Notification show GPS accuracy
- Show heads-up notification if GPS accuracy drops beyond threshold
- Always show chart by time as well as by distance
- Added help (in Settings)
Bugfix:
- TrackDetailActivity: disabled non-working landscape mode (temporary)
@@ -0,0 +1,10 @@
v3.2.8: OpenTracks
Changes:
- Chart: zoom via gestures
Bugfixes:
- Export via KMZ did not export picture filenames correctly
- Sharing multiple tracks did not include KMZ files
- MarkerDetailActivity's UI was not shown
- Settings crashed if preference of heart rate was not initialized
@@ -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)
Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

@@ -0,0 +1,8 @@
OpenTracks es un tracker deportivo que respeta tu privacidad.
Funciones:
* Tracker deportivo: haz un seguimiento de tus actividades al aire libre
* Marca lugares de interés, incluso agrega fotos a los marcadores
* Pulsómetro (compatible con dispositivos Bluetooth LE)
* Exporta tus rutas como KML o GPX
* Completamente offline, no necesitas internet
@@ -0,0 +1 @@
OpenTracks es un tracker deportivo que respeta tu privacidad.
@@ -0,0 +1 @@
OpenTracks
@@ -0,0 +1,9 @@
OpenTracks est une application de suivi d'activités sportives qui respecte totalement votre vie privée.
Fonctionnalités :
* Suivi d'activité : enregistre vos activités sportives et de plein air
* Photos et repères : ajout de photos et de repères pendant l'enregistrement de l'activité
* Rythme cardiaque : support des capteurs Bluetooth LE
* Export : export des traces au format KML ou GPX
* Pas d'accès internet requis : Internet n'est pas nécessaire pour le fonctionnement de l'application
@@ -0,0 +1 @@
OpenTracks est une application de suivi d'activités sportives qui respecte totalement 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;
}
}
@@ -13,7 +13,7 @@
* License for the specific language governing permissions and limitations under
* the License.
*/
package de.dennisguse.opentracks;
package de.dennisguse.opentracks.chart;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -23,6 +23,8 @@ import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import de.dennisguse.opentracks.R;
/**
* Tests {@link ChartValueSeries}.
*
@@ -35,21 +35,19 @@ import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import de.dennisguse.opentracks.content.ContentProviderUtils.LocationFactory;
import de.dennisguse.opentracks.content.ContentProviderUtils.LocationIterator;
import de.dennisguse.opentracks.content.Waypoint.WaypointType;
import de.dennisguse.opentracks.stats.TripStatistics;
import static org.mockito.Mockito.when;
/**
* A unit test for {@link ContentProviderUtilsImpl}.
* A unit test for {@link ContentProviderUtils}.
*
* @author Bartlomiej Niechwiej
* @author Youtao Liu
*/
@RunWith(MockitoJUnitRunner.class)
public class CustomContentProviderUtilsImplTest {
public class CustomContentProviderUtilsTest {
private static final String NAME_PREFIX = "test name";
private static final String MOCK_DESC = "Mock Next Waypoint Desc!";
private static final String TEST_DESC = "Test Desc!";
@@ -69,13 +67,13 @@ public class CustomContentProviderUtilsImplTest {
@Before
public void setUp() {
providerUtils = ContentProviderUtils.Factory.get(context);
providerUtils = new ContentProviderUtils(context);
providerUtils.deleteAllTracks(context);
}
@Test
public void testLocationIterator_noPoints() {
testIterator(1, 0, 1, false, ContentProviderUtils.DEFAULT_LOCATION_FACTORY);
testIterator(1, 0, 1, false, LocationFactory.DEFAULT_LOCATION_FACTORY);
}
@Test
@@ -105,36 +103,36 @@ public class CustomContentProviderUtilsImplTest {
@Test
public void testLocationIterator_noBatchAscending() {
testIterator(1, 50, 100, false, ContentProviderUtils.DEFAULT_LOCATION_FACTORY);
testIterator(2, 50, 50, false, ContentProviderUtils.DEFAULT_LOCATION_FACTORY);
testIterator(1, 50, 100, false, LocationFactory.DEFAULT_LOCATION_FACTORY);
testIterator(2, 50, 50, false, LocationFactory.DEFAULT_LOCATION_FACTORY);
}
@Test
public void testLocationIterator_noBatchDescending() {
testIterator(1, 50, 100, true, ContentProviderUtils.DEFAULT_LOCATION_FACTORY);
testIterator(2, 50, 50, true, ContentProviderUtils.DEFAULT_LOCATION_FACTORY);
testIterator(1, 50, 100, true, LocationFactory.DEFAULT_LOCATION_FACTORY);
testIterator(2, 50, 50, true, LocationFactory.DEFAULT_LOCATION_FACTORY);
}
@Test
public void testLocationIterator_batchAscending() {
testIterator(1, 50, 11, false, ContentProviderUtils.DEFAULT_LOCATION_FACTORY);
testIterator(2, 50, 25, false, ContentProviderUtils.DEFAULT_LOCATION_FACTORY);
testIterator(1, 50, 11, false, LocationFactory.DEFAULT_LOCATION_FACTORY);
testIterator(2, 50, 25, false, LocationFactory.DEFAULT_LOCATION_FACTORY);
}
@Test
public void testLocationIterator_batchDescending() {
testIterator(1, 50, 11, true, ContentProviderUtils.DEFAULT_LOCATION_FACTORY);
testIterator(2, 50, 25, true, ContentProviderUtils.DEFAULT_LOCATION_FACTORY);
testIterator(1, 50, 11, true, LocationFactory.DEFAULT_LOCATION_FACTORY);
testIterator(2, 50, 25, true, LocationFactory.DEFAULT_LOCATION_FACTORY);
}
@Test
public void testLocationIterator_largeTrack() {
testIterator(1, 20000, 2000, false, ContentProviderUtils.DEFAULT_LOCATION_FACTORY);
testIterator(1, 20000, 2000, false, LocationFactory.DEFAULT_LOCATION_FACTORY);
}
private List<Location> testIterator(long trackId, int numPoints, int batchSize, boolean descending, LocationFactory locationFactory) {
long lastPointId = initializeTrack(trackId, numPoints);
((ContentProviderUtilsImpl) providerUtils).setDefaultCursorBatchSize(batchSize);
((ContentProviderUtils) providerUtils).setDefaultCursorBatchSize(batchSize);
List<Location> locations = new ArrayList<Location>(numPoints);
try (LocationIterator it = providerUtils.getTrackPointLocationIterator(trackId, -1L, descending, locationFactory)) {
while (it.hasNext()) {
@@ -173,7 +171,7 @@ public class CustomContentProviderUtilsImplTest {
// Load all inserted locations.
long lastPointId = -1;
int counter = 0;
try (LocationIterator it = providerUtils.getTrackPointLocationIterator(id, -1L, false, ContentProviderUtils.DEFAULT_LOCATION_FACTORY)) {
try (LocationIterator it = providerUtils.getTrackPointLocationIterator(id, -1L, false, LocationFactory.DEFAULT_LOCATION_FACTORY)) {
while (it.hasNext()) {
it.next();
lastPointId = it.getLocationId();
@@ -189,7 +187,7 @@ public class CustomContentProviderUtilsImplTest {
}
/**
* Tests the method {@link ContentProviderUtilsImpl#createTrack(Cursor)}.
* Tests the method {@link ContentProviderUtils#createTrack(Cursor)}.
*/
@Test
public void testCreateTrack() {
@@ -216,7 +214,7 @@ public class CustomContentProviderUtilsImplTest {
}
/**
* Tests the method {@link ContentProviderUtilsImpl#deleteAllTracks(Context)}
* Tests the method {@link ContentProviderUtils#deleteAllTracks(Context)}
*/
@Test
public void testDeleteAllTracks() {
@@ -245,7 +243,7 @@ public class CustomContentProviderUtilsImplTest {
}
/**
* Tests the method {@link ContentProviderUtilsImpl#deleteTrack(Context, long)}.
* Tests the method {@link ContentProviderUtils#deleteTrack(Context, long)}.
*/
@Test
public void testDeleteTrack() {
@@ -280,7 +278,7 @@ public class CustomContentProviderUtilsImplTest {
}
/**
* Tests the method {@link ContentProviderUtilsImpl#getAllTracks()}
* Tests the method {@link ContentProviderUtils#getAllTracks()}
*/
@Test
public void testGetAllTracks() {
@@ -293,7 +291,7 @@ public class CustomContentProviderUtilsImplTest {
}
/**
* Tests the method {@link ContentProviderUtilsImpl#getLastTrack()}
* Tests the method {@link ContentProviderUtils#getLastTrack()}
*/
@Test
public void testGetLastTrack() {
@@ -303,7 +301,7 @@ public class CustomContentProviderUtilsImplTest {
}
/**
* Tests the method {@link ContentProviderUtilsImpl#getTrack(long)}
* Tests the method {@link ContentProviderUtils#getTrack(long)}
*/
@Test
public void testGetTrack() {
@@ -313,7 +311,7 @@ public class CustomContentProviderUtilsImplTest {
}
/**
* Tests the method {@link ContentProviderUtilsImpl#updateTrack(Track)}
* Tests the method {@link ContentProviderUtils#updateTrack(Track)}
*/
@Test
public void testUpdateTrack() {
@@ -330,7 +328,7 @@ public class CustomContentProviderUtilsImplTest {
}
/**
* Tests the method {@link ContentProviderUtilsImpl#createContentValues(Waypoint)}.
* Tests the method {@link ContentProviderUtils#createContentValues(Waypoint)}.
*/
@Test
public void testCreateContentValues_waypoint() {
@@ -368,7 +366,7 @@ public class CustomContentProviderUtilsImplTest {
waypoint.setLocation(location);
providerUtils.insertWaypoint(waypoint);
ContentProviderUtilsImpl contentProviderUtils = new ContentProviderUtilsImpl(contentResolverMock);
ContentProviderUtils contentProviderUtils = new ContentProviderUtils(contentResolverMock);
long waypointId = System.currentTimeMillis();
waypoint.setId(waypointId);
@@ -381,7 +379,7 @@ public class CustomContentProviderUtilsImplTest {
}
/**
* Tests the method {@link ContentProviderUtilsImpl#createWaypoint(Cursor)}.
* Tests the method {@link ContentProviderUtils#createWaypoint(Cursor)}.
*/
@Test
public void testCreateWaypoint() {
@@ -416,7 +414,7 @@ public class CustomContentProviderUtilsImplTest {
/**
* Tests the method
* {@link ContentProviderUtilsImpl#deleteWaypoint(Context, long, DescriptionGenerator)}
* {@link ContentProviderUtils#deleteWaypoint(Context, long, DescriptionGenerator)}
* when there is only one waypoint in the track.
*/
@Test
@@ -437,7 +435,7 @@ public class CustomContentProviderUtilsImplTest {
@Override
public String generateWaypointDescription(TripStatistics tripStatistics) {
return CustomContentProviderUtilsImplTest.MOCK_DESC;
return CustomContentProviderUtilsTest.MOCK_DESC;
}
@Override
@@ -452,7 +450,7 @@ public class CustomContentProviderUtilsImplTest {
/**
* Tests the method
* {@link ContentProviderUtilsImpl#deleteWaypoint(Context, long, DescriptionGenerator)}
* {@link ContentProviderUtils#deleteWaypoint(Context, long, DescriptionGenerator)}
* when there is more than one waypoint in the track.
*/
@Test
@@ -495,7 +493,7 @@ public class CustomContentProviderUtilsImplTest {
DescriptionGenerator descriptionGenerator = new DescriptionGenerator() {
@Override
public String generateWaypointDescription(TripStatistics tripStatistics) {
return CustomContentProviderUtilsImplTest.MOCK_DESC;
return CustomContentProviderUtilsTest.MOCK_DESC;
}
@Override
@@ -508,12 +506,12 @@ public class CustomContentProviderUtilsImplTest {
providerUtils.deleteWaypoint(context, waypoint1Id, descriptionGenerator);
Assert.assertNull(providerUtils.getWaypoint(waypoint1Id));
Assert.assertEquals(CustomContentProviderUtilsImplTest.MOCK_DESC, providerUtils.getWaypoint(waypoint2Id)
Assert.assertEquals(CustomContentProviderUtilsTest.MOCK_DESC, providerUtils.getWaypoint(waypoint2Id)
.getDescription());
}
/**
* Tests the method {@link ContentProviderUtilsImpl#getFirstWaypointId(long)}.
* Tests the method {@link ContentProviderUtils#getFirstWaypointId(long)}.
*/
@Test
public void testGetFirstWaypointId() {
@@ -535,7 +533,7 @@ public class CustomContentProviderUtilsImplTest {
}
/**
* Tests the method {@link ContentProviderUtilsImpl#getNextWaypointNumber(long, WaypointType)}.
* Tests the method {@link ContentProviderUtils#getNextWaypointNumber(long, WaypointType)}.
*/
@Test
public void testGetNextWaypointNumber() {
@@ -594,8 +592,8 @@ public class CustomContentProviderUtilsImplTest {
}
/**
* Tests the method {@link ContentProviderUtilsImpl#insertWaypoint(Waypoint)} and
* {@link ContentProviderUtilsImpl#getWaypoint(long)}.
* Tests the method {@link ContentProviderUtils#insertWaypoint(Waypoint)} and
* {@link ContentProviderUtils#getWaypoint(long)}.
*/
@Test
public void testInsertAndGetWaypoint() {
@@ -612,7 +610,7 @@ public class CustomContentProviderUtilsImplTest {
}
/**
* Tests the method {@link ContentProviderUtilsImpl#updateWaypoint(Waypoint)}.
* Tests the method {@link ContentProviderUtils#updateWaypoint(Waypoint)}.
*/
@Test
public void testUpdateWaypoint() {
@@ -634,7 +632,7 @@ public class CustomContentProviderUtilsImplTest {
}
/**
* Tests the method {@link ContentProviderUtilsImpl#bulkInsertTrackPoint(Location[], int, long)}.
* Tests the method {@link ContentProviderUtils#bulkInsertTrackPoint(Location[], int, long)}.
*/
@Test
public void testBulkInsertTrackPoint() {
@@ -650,7 +648,7 @@ public class CustomContentProviderUtilsImplTest {
}
/**
* Tests the method {@link ContentProviderUtilsImpl#createTrackPoint(Cursor)}.
* Tests the method {@link ContentProviderUtils#createTrackPoint(Cursor)}.
*/
@Test
public void testCreateTrackPoint() {
@@ -710,7 +708,7 @@ public class CustomContentProviderUtilsImplTest {
/**
* Tests the method
* {@link ContentProviderUtilsImpl#insertTrackPoint(Location, long)}.
* {@link ContentProviderUtils#insertTrackPoint(Location, long)}.
*/
@Test
public void testInsertTrackPoint() {
@@ -724,7 +722,7 @@ public class CustomContentProviderUtilsImplTest {
}
/**
* Tests the method {@link ContentProviderUtilsImpl#getFirstTrackPointId(long)}.
* Tests the method {@link ContentProviderUtils#getFirstTrackPointId(long)}.
*/
@Test
public void testGetFirstTrackPointId() {
@@ -737,7 +735,7 @@ public class CustomContentProviderUtilsImplTest {
}
/**
* Tests the method {@link ContentProviderUtilsImpl#getLastTrackPointId(long)}.
* Tests the method {@link ContentProviderUtils#getLastTrackPointId(long)}.
*/
@Test
public void testGetLastTrackPointId() {
@@ -750,7 +748,7 @@ public class CustomContentProviderUtilsImplTest {
}
/**
* Tests the method {@link ContentProviderUtilsImpl#getLastValidTrackPoint(long)}.
* Tests the method {@link ContentProviderUtils#getLastValidTrackPoint(long)}.
*/
@Test
public void testGetLastValidTrackPoint() {
@@ -764,7 +762,7 @@ public class CustomContentProviderUtilsImplTest {
}
/**
* Tests the method {@link ContentProviderUtilsImpl#getTrackPointCursor(long, long, int, boolean)} in descending.
* Tests the method {@link ContentProviderUtils#getTrackPointCursor(long, long, int, boolean)} in descending.
*/
@Test
public void testGetTrackPointCursor_desc() {
@@ -783,7 +781,7 @@ public class CustomContentProviderUtilsImplTest {
}
/**
* Tests the method {@link ContentProviderUtilsImpl#getTrackPointCursor(long, long, int, boolean)} in ascending.
* Tests the method {@link ContentProviderUtils#getTrackPointCursor(long, long, int, boolean)} in ascending.
*/
@Test
public void testGetTrackPointCursor_asc() {
@@ -802,7 +800,7 @@ public class CustomContentProviderUtilsImplTest {
}
/**
* Tests the method {@link ContentProviderUtilsImpl#getTrackPointLocationIterator(long, long, boolean, LocationFactory)} in descending.
* Tests the method {@link ContentProviderUtils#getTrackPointLocationIterator(long, long, boolean, LocationFactory)} in descending.
*/
@Test
public void testGetTrackPointLocationIterator_desc() {
@@ -818,7 +816,7 @@ public class CustomContentProviderUtilsImplTest {
long startTrackPointId = trackpointIds[9];
LocationIterator locationIterator = providerUtils.getTrackPointLocationIterator(trackId, startTrackPointId, true, ContentProviderUtils.DEFAULT_LOCATION_FACTORY);
LocationIterator locationIterator = providerUtils.getTrackPointLocationIterator(trackId, startTrackPointId, true, LocationFactory.DEFAULT_LOCATION_FACTORY);
for (int i = 0; i < trackpointIds.length; i++) {
Assert.assertTrue(locationIterator.hasNext());
Location location = locationIterator.next();
@@ -829,7 +827,7 @@ public class CustomContentProviderUtilsImplTest {
}
/**
* Tests the method {@link ContentProviderUtilsImpl#getTrackPointLocationIterator(long, long, boolean, LocationFactory)} in ascending.
* Tests the method {@link ContentProviderUtils#getTrackPointLocationIterator(long, long, boolean, LocationFactory)} in ascending.
*/
@Test
public void testGetTrackPointLocationIterator_asc() {
@@ -845,7 +843,7 @@ public class CustomContentProviderUtilsImplTest {
long startTrackPointId = trackpointIds[0];
LocationIterator locationIterator = providerUtils.getTrackPointLocationIterator(trackId, startTrackPointId, false, ContentProviderUtils.DEFAULT_LOCATION_FACTORY);
LocationIterator locationIterator = providerUtils.getTrackPointLocationIterator(trackId, startTrackPointId, false, LocationFactory.DEFAULT_LOCATION_FACTORY);
for (int i = 0; i < trackpointIds.length; i++) {
Assert.assertTrue(locationIterator.hasNext());
Location location = locationIterator.next();
@@ -892,7 +890,7 @@ public class CustomContentProviderUtilsImplTest {
/**
* Checks the value of a location.
*
* @param i the index of this location which created in the method {@link CustomContentProviderUtilsImplTest#getTrack(long, int)}
* @param i the index of this location which created in the method {@link CustomContentProviderUtilsTest#getTrack(long, int)}
* @param location the location to be checked
*/
private void checkLocation(int i, Location location) {
@@ -58,7 +58,7 @@ public class SearchEngineTest {
@Before
public void setUp() {
providerUtils = ContentProviderUtils.Factory.get(context);
providerUtils = new ContentProviderUtils(context);
engine = new SearchEngine(providerUtils);
}
@@ -17,17 +17,20 @@
package de.dennisguse.opentracks.fragments;
import android.location.Location;
import android.os.Looper;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.AfterClass;
import org.junit.Assert;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import de.dennisguse.opentracks.ChartView;
import de.dennisguse.opentracks.TrackStubUtils;
import de.dennisguse.opentracks.chart.ChartView;
import de.dennisguse.opentracks.content.SensorDataSetLocation;
import de.dennisguse.opentracks.content.sensor.SensorDataSet;
import de.dennisguse.opentracks.util.UnitConversions;
@@ -44,10 +47,23 @@ public class ChartFragmentTest {
private ChartFragment chartFragment;
@BeforeClass
public static void preSetUp() {
// Prepare looper for Android's message queue
if (Looper.myLooper() == null) Looper.prepare();
}
@AfterClass
public static void finalTearDown() {
if (Looper.myLooper() != null) Looper.myLooper().quit();
}
@Before
public void setUp() {
chartFragment = new ChartFragment();
chartFragment.setChartView(new ChartView(ApplicationProvider.getApplicationContext()));
boolean chartByDistance = false;
chartFragment = new ChartFragment(chartByDistance) {
};
chartFragment.setChartView(new ChartView(ApplicationProvider.getApplicationContext(), chartByDistance));
chartFragment.setTripStatisticsUpdater(TrackStubUtils.INITIAL_TIME);
}
@@ -0,0 +1,54 @@
package de.dennisguse.opentracks.services;
import android.app.NotificationManager;
import android.content.Context;
import android.location.Location;
import androidx.core.app.NotificationCompat;
import androidx.test.core.app.ApplicationProvider;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@RunWith(MockitoJUnitRunner.class)
public class TrackRecordingServiceNotificationManagerTest {
private Context context = ApplicationProvider.getApplicationContext();
@Mock
private Location locationMock;
@Mock
private NotificationCompat.Builder notificationCompatBuilder;
@Mock
private NotificationManager notificationManager;
@Test
public void updateLocation_triggersAlertOnlyOnFirstInaccurateLocation() {
when(locationMock.hasAccuracy()).thenReturn(true);
when(locationMock.getAccuracy()).thenReturn(999f);
when(notificationCompatBuilder.setContentText(anyString())).thenReturn(notificationCompatBuilder);
when(notificationCompatBuilder.setOnlyAlertOnce(anyBoolean())).thenReturn(notificationCompatBuilder);
TrackRecordingServiceNotificationManager subject = new TrackRecordingServiceNotificationManager(notificationManager, notificationCompatBuilder);
// when
subject.updateLocation(context, locationMock, 100);
subject.updateLocation(context, locationMock, 100);
subject.updateLocation(context, locationMock, 1000);
subject.updateLocation(context, locationMock, 100);
// then
verify(notificationCompatBuilder, times(6)).setOnlyAlertOnce(true);
verify(notificationCompatBuilder, times(2)).setOnlyAlertOnce(false);
}
}
@@ -71,17 +71,12 @@ public class TrackRecordingServiceTest {
private Context context = ApplicationProvider.getApplicationContext();
private ContentProviderUtils providerUtils;
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,15 +84,12 @@ public class TrackRecordingServiceTest {
};
customContentProvider.attachInfo(context, null);
providerUtils = ContentProviderUtils.Factory.get(context);
providerUtils = 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);
}
@@ -128,124 +120,6 @@ 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 {
@@ -289,16 +163,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 +190,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);
@@ -399,12 +238,12 @@ public class TrackRecordingServiceTest {
@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 trackId = service.getRecordingTrackId();
long waypointId = service.insertWaypoint(WaypointCreationRequest.DEFAULT_WAYPOINT);
Assert.assertNotEquals(-1L, waypointId);
Waypoint wpt = providerUtils.getWaypoint(waypointId);
@@ -415,5 +254,7 @@ public class TrackRecordingServiceTest {
Assert.assertEquals(0.0, wpt.getLength(), 0.01);
Assert.assertNotNull(wpt.getLocation());
Assert.assertNull(wpt.getTripStatistics());
service.endCurrentTrack();
}
}
@@ -68,15 +68,12 @@ public class TrackRecordingServiceTestLooper {
};
customContentProvider.attachInfo(context, null);
providerUtils = ContentProviderUtils.Factory.get(context);
providerUtils = new ContentProviderUtils(context);
// Let's use default values.
SharedPreferences sharedPreferences = PreferencesUtils.getSharedPreferences(context);
sharedPreferences.edit().clear().apply();
// Disable auto resume by default.
TrackRecordingServiceTest.updateAutoResumePrefs(context, PreferencesUtils.AUTO_RESUME_TRACK_CURRENT_RETRY_DEFAULT, 0);
// Ensure that the database is empty before every test
providerUtils.deleteAllTracks(context);
}
@@ -139,7 +136,8 @@ public class TrackRecordingServiceTestLooper {
@MediumTest
@Test
public void testWithProperties_minRecordingDistanceDefault() throws TimeoutException {
PreferencesUtils.setInt(context, R.string.recording_distance_interval_key, PreferencesUtils.RECORDING_DISTANCE_INTERVAL_DEFAULT);
int minRecordingIntervalDefault = Integer.parseInt(context.getResources().getString(R.string.min_recording_interval_default));
PreferencesUtils.setInt(context, R.string.recording_distance_interval_key, minRecordingIntervalDefault);
fullRecordingSession();
}
@@ -150,27 +148,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 {
@@ -202,7 +179,8 @@ public class TrackRecordingServiceTestLooper {
@MediumTest
@Test
public void testWithProperties_minRequiredAccuracyDefault() throws TimeoutException {
PreferencesUtils.setInt(context, R.string.recording_gps_accuracy_key, PreferencesUtils.RECORDING_GPS_ACCURACY_DEFAULT);
int recordingGPSAccuracyDefault = Integer.parseInt(context.getResources().getString(R.string.recording_gps_accuracy_default));
PreferencesUtils.setInt(context, R.string.recording_gps_accuracy_key, recordingGPSAccuracyDefault);
fullRecordingSession();
}
@@ -240,9 +218,7 @@ public class TrackRecordingServiceTestLooper {
location.setBearing(3.0f);
service.insertTrackPoint(location);
if (i % 10 == 0) {
service.insertWaypoint(WaypointCreationRequest.DEFAULT_STATISTICS);
} else if (i % 7 == 0) {
if (i % 7 == 0) {
service.insertWaypoint(WaypointCreationRequest.DEFAULT_WAYPOINT);
}
}
@@ -60,10 +60,15 @@ public class FileUtilsTest extends TestCase {
*/
public void testSanitizeFileName() {
String name = "Swim\10ming-^across:/the/ pacific (ocean).";
String expected = "Swim_ming-^across_the_ pacific (ocean)_";
String expected = "Swim_ming-^across_the_ pacific (ocean).";
assertEquals(expected, FileUtils.sanitizeFileName(name));
}
public void testSanitizeFileNameWithSuffix() {
String name = "FileName.jpeg";
assertEquals(name, FileUtils.sanitizeFileName(name));
}
/**
* Tests {@link FileUtils#sanitizeFileName(String)} with i18n characters (in Chinese and Russian).
* Verifies that they are allowed.
+10 -35
View File
@@ -16,8 +16,8 @@ limitations under the License.
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="de.dennisguse.opentracks"
android:installLocation="auto"
android:versionCode="3123"
android:versionName="v3.2.5"> <!-- For FDroid. -->
android:versionCode="3195"
android:versionName="v3.2.10"> <!-- For FDroid. -->
<!-- Permission to initialize services -->
<application
android:name=".Startup"
@@ -25,6 +25,7 @@ limitations under the License.
android:hardwareAccelerated="true"
android:icon="@drawable/ic_logo_color_24dp"
android:label="@string/app_name"
android:requestLegacyExternalStorage="true"
android:theme="@style/ThemeCustom">
<!-- Activities -->
@@ -32,6 +33,10 @@ limitations under the License.
<activity android:name=".DeleteActivity" />
<activity
android:name=".HelpActivity"
android:label="@string/menu_help" />
<activity
android:name=".io.file.importer.ImportActivity"
android:theme="@style/ThemeCustomTransparent">
@@ -50,10 +55,10 @@ limitations under the License.
</intent-filter>
</activity>
<activity android:name=".MarkerEditActivity" />
<activity android:name=".MarkerDetailActivity" />
<activity android:name=".MarkerEditActivity" />
<activity
android:name=".MarkerListActivity"
android:label="@string/menu_markers">
@@ -95,7 +100,6 @@ limitations under the License.
<activity android:name=".settings.SettingsActivity" />
<!-- Services -->
<provider
android:name=".content.ShareContentProvider"
android:authorities="${applicationId}.content"
@@ -112,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"
@@ -139,44 +135,23 @@ 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>
@@ -17,11 +17,12 @@
package de.dennisguse.opentracks;
import android.os.Bundle;
import android.speech.tts.TextToSpeech;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import de.dennisguse.opentracks.util.TTSUtils;
/**
* @author Jimmy Shih
*/
@@ -32,7 +33,7 @@ public abstract class AbstractActivity extends AppCompatActivity {
super.onCreate(savedInstanceState);
// Set volume control stream for text to speech
setVolumeControlStream(TextToSpeech.Engine.DEFAULT_STREAM);
setVolumeControlStream(TTSUtils.getTTSStream());
setContentView(getLayoutResId());
@@ -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");
}
}
}
@@ -69,7 +69,7 @@ public class DeleteAsyncTask extends AsyncTask<Void, Integer, Boolean> {
@Override
protected Boolean doInBackground(Void... params) {
ContentProviderUtils contentProviderUtils = ContentProviderUtils.Factory.get(context);
ContentProviderUtils contentProviderUtils = new ContentProviderUtils(context);
if (trackIds.length == 1 && trackIds[0] == -1L) {
contentProviderUtils.deleteAllTracks(context);
@@ -0,0 +1,9 @@
package de.dennisguse.opentracks;
public class HelpActivity extends AbstractActivity {
@Override
protected int getLayoutResId() {
return R.layout.help;
}
}
@@ -61,7 +61,7 @@ public class MarkerDetailActivity extends AbstractActivity implements DeleteMark
return;
}
ContentProviderUtils contentProviderUtils = ContentProviderUtils.Factory.get(this);
ContentProviderUtils contentProviderUtils = new ContentProviderUtils(this);
Waypoint waypoint = contentProviderUtils.getWaypoint(markerId);
markerIds = new ArrayList<>();
@@ -71,8 +71,7 @@ public class MarkerEditActivity extends AbstractActivity {
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);
@@ -127,7 +126,7 @@ public class MarkerEditActivity extends AbstractActivity {
if (newMarker) {
statisticsSection.setVisibility(View.GONE);
waypointSection.setVisibility(View.VISIBLE);
int nextWaypointNumber = trackId == -1L ? -1 : ContentProviderUtils.Factory.get(this).getNextWaypointNumber(trackId, WaypointType.WAYPOINT);
int nextWaypointNumber = trackId == -1L ? -1 : new ContentProviderUtils(this).getNextWaypointNumber(trackId, WaypointType.WAYPOINT);
if (nextWaypointNumber == -1) {
nextWaypointNumber = 0;
}
@@ -136,7 +135,7 @@ public class MarkerEditActivity extends AbstractActivity {
waypointMarkerType.setText("");
waypointDescription.setText("");
} else {
waypoint = ContentProviderUtils.Factory.get(this).getWaypoint(markerId);
waypoint = new ContentProviderUtils(this).getWaypoint(markerId);
if (waypoint == null) {
Log.d(TAG, "waypoint is null");
finish();
@@ -182,6 +181,6 @@ public class MarkerEditActivity extends AbstractActivity {
waypoint.setCategory(waypointMarkerType.getText().toString());
waypoint.setDescription(waypointDescription.getText().toString());
}
ContentProviderUtils.Factory.get(this).updateWaypoint(waypoint);
new ContentProviderUtils(this).updateWaypoint(waypoint);
}
}
@@ -39,13 +39,13 @@ 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.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;
/**
@@ -69,13 +69,12 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
private SharedPreferences sharedPreferences;
private long recordingTrackId = PreferencesUtils.RECORDING_TRACK_ID_DEFAULT;
private boolean recordingTrackPaused = PreferencesUtils.RECORDING_TRACK_PAUSED_DEFAULT;
private boolean recordingTrackPaused;
/*
* 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
@@ -128,7 +127,9 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
contentProviderUtils = ContentProviderUtils.Factory.get(this);
recordingTrackPaused = PreferencesUtils.isRecordingTrackPausedDefault(this);
contentProviderUtils = new ContentProviderUtils(this);
sharedPreferences = PreferencesUtils.getSharedPreferences(this);
long trackId = getIntent().getLongExtra(EXTRA_TRACK_ID, -1L);
@@ -150,7 +151,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);
@@ -159,13 +159,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);
@@ -244,7 +244,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());
@@ -46,7 +46,6 @@ 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.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;
@@ -101,7 +101,7 @@ public class SearchListActivity extends AbstractListActivity implements DeleteMa
private long recordingTrackId = PreferencesUtils.RECORDING_TRACK_ID_DEFAULT;
private boolean recordingTrackPaused = PreferencesUtils.RECORDING_TRACK_PAUSED_DEFAULT;
private boolean recordingTrackPaused;
// Callback when an item is selected in the contextual action mode
private ContextualActionModeCallback contextualActionModeCallback = new ContextualActionModeCallback() {
@@ -168,7 +168,9 @@ public class SearchListActivity extends AbstractListActivity implements DeleteMa
super.onCreate(savedInstanceState);
setDefaultKeyMode(DEFAULT_KEYS_SEARCH_LOCAL);
contentProviderUtils = ContentProviderUtils.Factory.get(this);
recordingTrackPaused = PreferencesUtils.isRecordingTrackPausedDefault(this);
contentProviderUtils = new ContentProviderUtils(this);
sharedPreferences = PreferencesUtils.getSharedPreferences(this);
trackRecordingServiceConnection = new TrackRecordingServiceConnection(this, null);
searchEngine = new SearchEngine(contentProviderUtils);
@@ -391,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
@@ -403,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());
@@ -45,7 +45,8 @@ 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.fragments.ChartFragment;
import de.dennisguse.opentracks.fragments.ChartDistanceFragment;
import de.dennisguse.opentracks.fragments.ChartTimeFragment;
import de.dennisguse.opentracks.fragments.ChooseActivityTypeDialogFragment;
import de.dennisguse.opentracks.fragments.ConfirmDeleteDialogFragment;
import de.dennisguse.opentracks.fragments.StatsFragment;
@@ -93,7 +94,7 @@ public class TrackDetailActivity extends AbstractListActivity implements ChooseA
// Preferences
private long recordingTrackId = PreferencesUtils.RECORDING_TRACK_ID_DEFAULT;
private boolean recordingTrackPaused = PreferencesUtils.RECORDING_TRACK_PAUSED_DEFAULT;
private boolean recordingTrackPaused;
private final Runnable bindChangedCallback = new Runnable() {
@Override
@@ -116,26 +117,31 @@ public class TrackDetailActivity extends AbstractListActivity implements ChooseA
});
}
};
/*
* Note that sharedPreferenceChangeListener cannot be an anonymous inner
* class. Anonymous inner class will get garbage collected.
*/
private final OnSharedPreferenceChangeListener
sharedPreferenceChangeListener = new OnSharedPreferenceChangeListener() {
// 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() {
@@ -182,11 +188,13 @@ public class TrackDetailActivity extends AbstractListActivity implements ChooseA
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
recordingTrackPaused = PreferencesUtils.isRecordingTrackPausedDefault(this);
hasCamera = getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA);
photoUri = savedInstanceState != null ? (Uri) savedInstanceState.getParcelable(PHOTO_URI_KEY) : null;
hasPhoto = savedInstanceState != null && savedInstanceState.getBoolean(HAS_PHOTO_KEY, false);
contentProviderUtils = ContentProviderUtils.Factory.get(this);
contentProviderUtils = new ContentProviderUtils(this);
handleIntent(getIntent());
sharedPreferences = PreferencesUtils.getSharedPreferences(this);
@@ -203,8 +211,11 @@ public class TrackDetailActivity extends AbstractListActivity implements ChooseA
TabSpec statsTabSpec = tabHost.newTabSpec(StatsFragment.STATS_FRAGMENT_TAG).setIndicator(getString(R.string.track_detail_stats_tab));
tabsAdapter.addTab(statsTabSpec, StatsFragment.class, null);
TabSpec chartTabSpec = tabHost.newTabSpec(ChartFragment.CHART_FRAGMENT_TAG).setIndicator(getString(R.string.track_detail_chart_tab));
tabsAdapter.addTab(chartTabSpec, ChartFragment.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);
if (savedInstanceState != null) {
tabHost.setCurrentTabByTag(savedInstanceState.getString(CURRENT_TAB_TAG_KEY));
@@ -221,11 +232,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);
@@ -236,6 +249,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();
@@ -407,16 +431,6 @@ public class TrackDetailActivity extends AbstractListActivity implements ChooseA
return trackDataHub;
}
/**
* Gets the track id.
*/
public long getTrackId() {
return trackId;
}
/**
* 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);
@@ -73,7 +73,7 @@ public class TrackEditActivity extends AbstractActivity implements ChooseActivit
return;
}
contentProviderUtils = ContentProviderUtils.Factory.get(this);
contentProviderUtils = new ContentProviderUtils(this);
track = contentProviderUtils.getTrack(trackId);
if (track == null) {
Log.e(TAG, "No track for " + trackId);
@@ -125,7 +125,7 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
}
};
private boolean recordingTrackPaused = PreferencesUtils.RECORDING_TRACK_PAUSED_DEFAULT;
private boolean recordingTrackPaused;
/*
* Note that sharedPreferenceChangeListener cannot be an anonymous inner class.
@@ -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
@@ -225,8 +225,7 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
// Not recording -> Recording
updateMenuItems(false, true);
startRecording();
} else {
if (recordingTrackPaused) {
} else if (recordingTrackPaused) {
// Paused -> Resume
updateMenuItems(false, true);
trackRecordingServiceConnection.resumeTrack();
@@ -237,7 +236,6 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
trackRecordingServiceConnection.pauseTrack();
trackController.update(true, true);
}
}
}
};
@@ -245,7 +243,9 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
contentProviderUtils = ContentProviderUtils.Factory.get(this);
recordingTrackPaused = PreferencesUtils.isRecordingTrackPausedDefault(this);
contentProviderUtils = new ContentProviderUtils(this);
sharedPreferences = PreferencesUtils.getSharedPreferences(this);
trackRecordingServiceConnection = new TrackRecordingServiceConnection(this, bindChangedCallback);
@@ -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);
}
@@ -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);
}
}
/**
@@ -14,7 +14,7 @@
* the License.
*/
package de.dennisguse.opentracks;
package de.dennisguse.opentracks.chart;
import android.content.Context;
import android.graphics.Canvas;
@@ -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;
}
@@ -14,7 +14,7 @@
* the License.
*/
package de.dennisguse.opentracks;
package de.dennisguse.opentracks.chart;
import android.content.Context;
import android.content.Intent;
@@ -27,33 +27,40 @@ import android.graphics.Paint.Style;
import android.graphics.Path;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.ScaleGestureDetector;
import android.view.View;
import android.view.ViewConfiguration;
import android.widget.Scroller;
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.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;
/**
* Visualization of the chart.
* Provides support for zooming (via pinch), scrolling, flinging, and selecting shown markers (single touch).
*
* @author Sandor Dornbush
* @author Leif Hendrik Wilden
*/
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;
@@ -74,25 +81,28 @@ 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;
private double maxX = 1.0;
private VelocityTracker velocityTracker = null;
private float lastMotionEventX = -1;
private int zoomLevel = 1;
private int leftBorder = BORDER;
@@ -107,18 +117,84 @@ public class ChartView extends View {
private int effectiveWidth = 0;
private int effectiveHeight = 0;
private boolean chartByDistance = true;
private boolean chartByDistance;
private boolean metricUnits = true;
private boolean reportSpeed = true;
private boolean showPointer = false;
/**
* Constructor.
*
* @param context the context
*/
public ChartView(Context context) {
private GestureDetectorCompat detectorScrollFlingTab = new GestureDetectorCompat(getContext(), new GestureDetector.SimpleOnGestureListener() {
@Override
public boolean onDown(MotionEvent e) {
if (!scroller.isFinished()) {
scroller.abortAnimation();
}
return true;
}
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
if (Math.abs(distanceX) > 0) {
int availableToScroll = effectiveWidth * (zoomLevel - 1) - getScrollX();
if (availableToScroll > 0) {
scrollBy(Math.min(availableToScroll, (int) distanceX));
}
}
return true;
}
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
fling((int) -velocityX);
return true;
}
@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 / 2f) < markerHeight) {
int minDistance = Integer.MAX_VALUE;
Waypoint nearestWaypoint = null;
synchronized (waypoints) {
for (Waypoint waypoint : waypoints) {
int distance = Math.abs(getX(getWaypointXValue(waypoint)) - (int) event.getX() - getScrollX());
if (distance < minDistance) {
minDistance = distance;
nearestWaypoint = waypoint;
}
}
}
if (nearestWaypoint != null && minDistance < markerWidth) {
Intent intent = IntentUtils.newIntent(getContext(), MarkerDetailActivity.class)
.putExtra(MarkerDetailActivity.EXTRA_MARKER_ID, nearestWaypoint.getId());
getContext().startActivity(intent);
return true;
}
}
return false;
}
});
private ScaleGestureDetector detectorZoom = new ScaleGestureDetector(getContext(), new ScaleGestureDetector.SimpleOnScaleGestureListener() {
@Override
public boolean onScale(ScaleGestureDetector detector) {
float scaleFactor = detector.getScaleFactor();
if (scaleFactor >= 1.1f) {
zoomIn();
return true;
} else if (scaleFactor <= 0.9) {
zoomOut();
return true;
}
return false;
}
});
public ChartView(Context context, boolean chartByDistance) {
super(context);
this.chartByDistance = chartByDistance;
series[ELEVATION_SERIES] = new ChartValueSeries(context,
Integer.MIN_VALUE,
@@ -186,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);
@@ -194,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);
@@ -222,16 +295,6 @@ public class ChartView extends View {
series[index].setEnabled(enabled);
}
/**
* Sets chart by distance.
* It is expected that after changing this value, data will be reloaded.
*
* @param value true for by distance, false for by time
*/
public void setChartByDistance(boolean value) {
chartByDistance = value;
}
/**
* Sets metric units.
*
@@ -264,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])) {
@@ -410,79 +472,14 @@ public class ChartView extends View {
@Override
public boolean onTouchEvent(MotionEvent event) {
if (velocityTracker == null) {
velocityTracker = VelocityTracker.obtain();
}
velocityTracker.addMovement(event);
float x = event.getX();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
// Stop the fling
if (!scroller.isFinished()) {
scroller.abortAnimation();
}
lastMotionEventX = x;
break;
case MotionEvent.ACTION_MOVE:
if (lastMotionEventX == -1) {
break;
}
// Scroll to follow the motion event
int deltaX = (int) (lastMotionEventX - x);
lastMotionEventX = x;
if (deltaX < 0) {
if (getScrollX() > 0) {
scrollBy(deltaX);
}
} else if (deltaX > 0) {
int availableToScroll = effectiveWidth * (zoomLevel - 1) - getScrollX();
if (availableToScroll > 0) {
scrollBy(Math.min(availableToScroll, deltaX));
}
}
break;
case MotionEvent.ACTION_UP:
// Check if the y event is within markerHeight of the marker center
if (Math.abs(event.getY() - topBorder - spacer - markerHeight / 2) < markerHeight) {
int minDistance = Integer.MAX_VALUE;
Waypoint nearestWaypoint = null;
synchronized (waypoints) {
for (int i = 0; i < waypoints.size(); i++) {
Waypoint waypoint = waypoints.get(i);
int distance = Math.abs(getX(getWaypointXValue(waypoint)) - (int) event.getX() - getScrollX());
if (distance < minDistance) {
minDistance = distance;
nearestWaypoint = waypoint;
}
}
}
if (nearestWaypoint != null && minDistance < markerWidth) {
Intent intent = IntentUtils.newIntent(getContext(), MarkerDetailActivity.class)
.putExtra(MarkerDetailActivity.EXTRA_MARKER_ID, nearestWaypoint.getId());
getContext().startActivity(intent);
return true;
}
}
VelocityTracker myVelocityTracker = velocityTracker;
myVelocityTracker.computeCurrentVelocity(1000);
int initialVelocity = (int) myVelocityTracker.getXVelocity();
if (Math.abs(initialVelocity) > ViewConfiguration.getMinimumFlingVelocity()) {
fling(-initialVelocity);
}
if (velocityTracker != null) {
velocityTracker.recycle();
velocityTracker = null;
}
break;
}
return true;
boolean isZoom = detectorZoom.onTouchEvent(event);
boolean isScrollTab = detectorScrollFlingTab.onTouchEvent(event);
return isZoom || isScrollTab;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
updateEffectiveDimensionsIfChanged(
View.MeasureSpec.getSize(widthMeasureSpec), View.MeasureSpec.getSize(heightMeasureSpec));
updateEffectiveDimensionsIfChanged(View.MeasureSpec.getSize(widthMeasureSpec), View.MeasureSpec.getSize(heightMeasureSpec));
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
@@ -498,6 +495,7 @@ public class ChartView extends View {
clipToGraphArea(canvas);
drawDataSeries(canvas);
drawWaypoints(canvas);
drawGrid(canvas);
canvas.restore();
@@ -538,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 - (float) (markerWidth * WAYPOINT_X_ANCHOR), topBorder + spacer);
waypointMarker.draw(canvas);
canvas.restore();
}
}
}
/**
* Draws the grid.
*
@@ -545,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
@@ -619,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);
}
}
@@ -632,8 +657,7 @@ public class ChartView extends View {
private String getXAxisLabel() {
Context context = getContext();
if (chartByDistance) {
return metricUnits ? context.getString(R.string.unit_kilometer)
: context.getString(R.string.unit_mile);
return metricUnits ? context.getString(R.string.unit_kilometer) : context.getString(R.string.unit_mile);
} else {
return context.getString(R.string.description_time);
}
@@ -673,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);
@@ -733,8 +757,7 @@ public class ChartView extends View {
* @param yValue the y value
* @return the marker width.
*/
private float drawYAxisMarker(
ChartValueSeries chartValueSeries, Canvas canvas, int xPosition, int yValue) {
private float drawYAxisMarker(ChartValueSeries chartValueSeries, Canvas canvas, int xPosition, int yValue) {
String marker = chartValueSeries.formatMarker(yValue);
Paint paint = chartValueSeries.getMarkerPaint();
Rect rect = getRect(paint, marker);
@@ -785,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)) {
@@ -869,8 +891,7 @@ public class ChartView extends View {
topBorder = (int) (density * BORDER + titleDimensions[0] * (titleDimensions[1] + spacer));
Rect xAxisLabelRect = getRect(axisPaint, getXAxisLabel());
// border + x axis marker + spacer + .5 x axis label
bottomBorder = (int) (density * BORDER + getRect(xAxisMarkerPaint, "1").height() + spacer
+ (xAxisLabelRect.height() / 2));
bottomBorder = (int) (density * BORDER + getRect(xAxisMarkerPaint, "1").height() + spacer + (xAxisLabelRect.height() / 2));
rightBorder = (int) (density * BORDER + xAxisLabelRect.width() + spacer);
updateEffectiveDimensions();
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,20 @@
package de.dennisguse.opentracks.content;
import android.location.Location;
import android.location.LocationManager;
/**
* Creates a new {@link SensorDataSetLocation}.
* An implementation can create new instances or reuse existing instances for optimization.
*/
public class LocationFactory {
/**
* The default {@link LocationFactory} which creates a location each time.
*/
public static LocationFactory DEFAULT_LOCATION_FACTORY = new LocationFactory();
public Location createLocation() {
return new SensorDataSetLocation(LocationManager.GPS_PROVIDER);
}
}
@@ -0,0 +1,22 @@
package de.dennisguse.opentracks.content;
import android.database.Cursor;
import android.location.Location;
import java.util.Iterator;
/**
* A lightweight wrapper around the original {@link Cursor} with a method to clean up.
*/
public interface LocationIterator extends Iterator<Location>, AutoCloseable {
/**
* Gets the most recently retrieved track point id by {@link #next()}.
*/
long getLocationId();
/**
* Closes the iterator.
*/
void close();
}
@@ -198,7 +198,7 @@ public class ShareContentProvider extends CustomContentProvider implements ICont
@Nullable
@Override
public ParcelFileDescriptor openFile(@NonNull Uri uri, @NonNull String mode) throws FileNotFoundException {
ContentProviderUtils contentProviderUtils = new ContentProviderUtilsImpl(this);
ContentProviderUtils contentProviderUtils = new ContentProviderUtils(this);
long[] trackIds = parseURI(uri);
final Track[] tracks = new Track[trackIds.length];
@@ -30,7 +30,6 @@ import java.util.EnumSet;
import java.util.Set;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.ContentProviderUtils.LocationIterator;
import de.dennisguse.opentracks.util.LocationUtils;
import de.dennisguse.opentracks.util.PreferencesUtils;
@@ -41,17 +40,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.
*/
static final int TARGET_DISPLAYED_TRACK_POINTS = 5000;
@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;
@@ -96,11 +102,6 @@ public class TrackDataHub implements DataSourceListener {
resetSamplingState();
}
public synchronized static TrackDataHub newInstance(Context context) {
return new TrackDataHub(context, new TrackDataManager(), ContentProviderUtils.Factory.get(
context), TARGET_DISPLAYED_TRACK_POINTS);
}
public void start() {
if (started) {
Log.i(TAG, "TrackDataHub already started, ignoring start.");
@@ -398,8 +399,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);
@@ -448,7 +448,7 @@ public class TrackDataHub implements DataSourceListener {
int samplingFrequency = -1;
boolean includeNextPoint = false;
try (LocationIterator locationIterator = contentProviderUtils.getTrackPointLocationIterator(selectedTrackId, localLastSeenLocationId + 1, false, ContentProviderUtils.DEFAULT_LOCATION_FACTORY)) {
try (LocationIterator locationIterator = contentProviderUtils.getTrackPointLocationIterator(selectedTrackId, localLastSeenLocationId + 1, false, LocationFactory.DEFAULT_LOCATION_FACTORY)) {
while (locationIterator.hasNext()) {
Location location = locationIterator.next();
@@ -468,12 +468,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())) {
@@ -51,14 +51,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();
@@ -188,6 +188,7 @@ public final class Waypoint {
}
public enum WaypointType {
WAYPOINT, STATISTICS
WAYPOINT,
STATISTICS //Always the first waypoint; only one can exist.
}
}
@@ -26,8 +26,7 @@ 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);
@Deprecated // TODO Do not create per
public final static WaypointCreationRequest DEFAULT_START_TRACK = new WaypointCreationRequest(WaypointType.STATISTICS, true);
private WaypointType type;
@@ -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;
@@ -0,0 +1,10 @@
package de.dennisguse.opentracks.fragments;
public class ChartDistanceFragment extends ChartFragment {
public static final String CHART_FRAGMENT_TAG = ChartDistanceFragment.class.getSimpleName();
public ChartDistanceFragment() {
super(true);
}
}
@@ -22,19 +22,20 @@ import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewGroup.LayoutParams;
import android.widget.ZoomControls;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.List;
import de.dennisguse.opentracks.ChartView;
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;
@@ -54,11 +55,11 @@ import de.dennisguse.opentracks.util.UnitConversions;
* @author Sandor Dornbush
* @author Rodrigo Damazio
*/
public class ChartFragment extends Fragment implements TrackDataListener {
public abstract class ChartFragment extends Fragment implements TrackDataListener {
public static final String CHART_FRAGMENT_TAG = "chartFragment";
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;
@@ -68,18 +69,17 @@ public class ChartFragment extends Fragment implements TrackDataListener {
private boolean metricUnits = true;
private boolean reportSpeed = true;
private int recordingDistanceInterval = PreferencesUtils.RECORDING_DISTANCE_INTERVAL_DEFAULT;
private int recordingDistanceInterval;
// Modes of operation
private boolean chartByDistance = true;
private boolean chartByDistance;
private boolean[] chartShow = new boolean[]{true, true, true, true, true, true};
// UI elements
private ChartView chartView;
private ZoomControls zoomControls;
/**
* A runnable that will enable/disable zoom controls and orange pointer as appropriate and redraw.
* A runnable that will set the orange pointer as appropriate and redraw.
*/
private final Runnable updateChart = new Runnable() {
@Override
@@ -88,46 +88,34 @@ public class ChartFragment extends Fragment implements TrackDataListener {
return;
}
zoomControls.setIsZoomInEnabled(chartView.canZoomIn());
zoomControls.setIsZoomOutEnabled(chartView.canZoomOut());
chartView.setShowPointer(isSelectedTrackRecording());
chartView.invalidate();
}
};
public ChartFragment(boolean chartByDistance) {
this.chartByDistance = chartByDistance;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
/*
* 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());
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(getContext(), chartByDistance);
}
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.chart, container, false);
zoomControls = view.findViewById(R.id.chart_zoom_controls);
zoomControls.setOnZoomInClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
zoomIn();
}
});
zoomControls.setOnZoomOutClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
zoomOut();
}
});
return view;
return inflater.inflate(R.layout.chart, container, false);
}
@Override
public void onStart() {
super.onStart();
ViewGroup layout = getActivity().findViewById(R.id.chart_view_layout);
ViewGroup layout = getView().findViewById(R.id.chart_view_layout);
LayoutParams layoutParams = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
layout.addView(chartView, layoutParams);
}
@@ -149,10 +137,24 @@ public class ChartFragment extends Fragment implements TrackDataListener {
@Override
public void onStop() {
super.onStop();
ViewGroup layout = getActivity().findViewById(R.id.chart_view_layout);
ViewGroup layout = getView().findViewById(R.id.chart_view_layout);
layout.removeView(chartView);
}
@Override
public void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
outState.putBoolean(STATE_CHART_VIEW_BY_DISTANCE_KEY, chartByDistance);
}
@Override
public void onViewStateRestored(@Nullable Bundle savedInstanceState) {
super.onViewStateRestored(savedInstanceState);
if (savedInstanceState != null) {
chartByDistance = savedInstanceState.getBoolean(STATE_CHART_VIEW_BY_DISTANCE_KEY);
}
}
@Override
public void onTrackUpdated(Track track) {
if (isResumed()) {
@@ -197,13 +199,6 @@ public class ChartFragment extends Fragment implements TrackDataListener {
}
}
@Override
public void onSegmentSplit(Location location) {
if (isResumed()) {
fillDataPoint(location, null);
}
}
@Override
public void onNewTrackPointsDone() {
if (isResumed()) {
@@ -263,9 +258,8 @@ public class ChartFragment extends Fragment implements TrackDataListener {
}
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() {
@@ -302,32 +296,14 @@ public class ChartFragment extends Fragment implements TrackDataListener {
*/
private void checkChartSettings() {
boolean needUpdate = false;
if (chartByDistance != PreferencesUtils.isChartByDistance(getActivity())) {
chartByDistance = !chartByDistance;
chartView.setChartByDistance(chartByDistance);
reloadTrackDataHub();
if (setSeriesEnabled(ChartView.SPEED_SERIES, reportSpeed)) {
needUpdate = true;
}
if (setSeriesEnabled(ChartView.ELEVATION_SERIES, PreferencesUtils.shouldChartShowElevation(getActivity()))) {
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();
}
@@ -388,18 +364,6 @@ public class ChartFragment extends Fragment implements TrackDataListener {
}
}
private void zoomIn() {
chartView.zoomIn();
zoomControls.setIsZoomInEnabled(chartView.canZoomIn());
zoomControls.setIsZoomOutEnabled(chartView.canZoomOut());
}
private void zoomOut() {
chartView.zoomOut();
zoomControls.setIsZoomInEnabled(chartView.canZoomIn());
zoomControls.setIsZoomOutEnabled(chartView.canZoomOut());
}
/**
* Runs a runnable on the UI thread if possible.
*
@@ -0,0 +1,10 @@
package de.dennisguse.opentracks.fragments;
public class ChartTimeFragment extends ChartFragment {
public static final String CHART_FRAGMENT_TAG = ChartTimeFragment.class.getSimpleName();
public ChartTimeFragment() {
super(false);
}
}
@@ -81,7 +81,7 @@ public class DeleteMarkerDialogFragment extends DialogFragment {
new Thread(new Runnable() {
@Override
public void run() {
ContentProviderUtils contentProviderUtils = ContentProviderUtils.Factory.get(fragmentActivity);
ContentProviderUtils contentProviderUtils = new ContentProviderUtils(fragmentActivity);
for (long markerId : markerIds) {
contentProviderUtils.deleteWaypoint(fragmentActivity,
markerId, new DescriptionGeneratorImpl(fragmentActivity));
@@ -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.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;
@@ -110,7 +108,7 @@ public class MarkerDetailFragment extends Fragment {
getFragmentManager().popBackStack();
return;
}
contentProviderUtils = ContentProviderUtils.Factory.get(getActivity());
contentProviderUtils = new ContentProviderUtils(getActivity());
handler = new Handler();
setHasOptionsMenu(true);
}
@@ -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,16 +20,18 @@ 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;
@@ -43,10 +45,11 @@ import de.dennisguse.opentracks.content.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.PreferencesUtils;
import de.dennisguse.opentracks.util.StatsUtils;
import de.dennisguse.opentracks.util.StringUtils;
import de.dennisguse.opentracks.util.TrackIconUtils;
import de.dennisguse.opentracks.util.UnitConversions;
@@ -60,37 +63,136 @@ 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 = PreferencesUtils.RECORDING_GPS_ACCURACY_DEFAULT;
private int recordingGpsAccuracy;
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
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);
recordingGpsAccuracy = Integer.parseInt(getContext().getResources().getString(R.string.recording_gps_accuracy_default));
handlerUpdateUI = new Handler();
Spinner activityTypeIcon = getView().findViewById(R.id.stats_activity_type_icon);
@@ -123,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
@@ -139,10 +238,66 @@ public class StatsFragment extends Fragment implements TrackDataListener {
@Override
public void onStop() {
super.onStop();
trackRecordingServiceConnection.unbind();
if (trackRecordingServiceConnection != null) {
trackRecordingServiceConnection.unbind();
}
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()) {
@@ -152,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();
}
}
});
@@ -174,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()) {
@@ -198,7 +348,7 @@ public class StatsFragment extends Fragment implements TrackDataListener {
lastLocation = null;
}
}
StatsUtils.setLocationValues(getActivity(), lastLocation, isSelectedTrackRecording());
setLocationValues();
}
}
});
@@ -227,7 +377,7 @@ public class StatsFragment extends Fragment implements TrackDataListener {
@Override
public void run() {
if (isResumed()) {
updateUi(getActivity());
updateUI();
}
}
});
@@ -242,7 +392,7 @@ public class StatsFragment extends Fragment implements TrackDataListener {
@Override
public void run() {
if (isResumed()) {
updateUi(getActivity());
updateUI();
}
}
});
@@ -298,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}.
*/
@@ -312,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);
}
}
}
@@ -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() {
@@ -173,10 +173,12 @@ public enum TrackFileFormat {
private static final String MIME_KML = "application/vnd.google-earth.kml+xml";
public TrackExporter newTrackExporter(Context context, Track[] tracks, TrackExporterListener trackExporterListener) {
ContentProviderUtils contentProviderUtils = ContentProviderUtils.Factory.get(context);
TrackWriter trackWriter = this.newTrackWriter(context, tracks.length > 1);
return new FileTrackExporter(contentProviderUtils, trackWriter, tracks, trackExporterListener);
private static TrackExporter newKmzTrackExporter(Context context, TrackWriter trackWriter, Track[] tracks, TrackExporterListener trackExporterListener, boolean exportPhotos) {
ContentProviderUtils contentProviderUtils = new ContentProviderUtils(context);
FileTrackExporter fileTrackExporter = new FileTrackExporter(contentProviderUtils, trackWriter, tracks, trackExporterListener);
return new KmzTrackExporter(contentProviderUtils, fileTrackExporter, tracks, exportPhotos);
}
/**
@@ -184,12 +186,10 @@ public enum TrackFileFormat {
*/
public abstract String getMimeType();
private static TrackExporter newKmzTrackExporter(Context context, TrackWriter trackWriter, Track[] tracks, TrackExporterListener trackExporterListener, boolean exportPhotos) {
ContentProviderUtils contentProviderUtils = ContentProviderUtils.Factory.get(context);
FileTrackExporter fileTrackExporter = new FileTrackExporter(contentProviderUtils, trackWriter, tracks, trackExporterListener);
return new KmzTrackExporter(contentProviderUtils, fileTrackExporter, tracks, exportPhotos);
public TrackExporter newTrackExporter(Context context, Track[] tracks, TrackExporterListener trackExporterListener) {
ContentProviderUtils contentProviderUtils = new ContentProviderUtils(context);
TrackWriter trackWriter = newTrackWriter(context, tracks.length > 1);
return new FileTrackExporter(contentProviderUtils, trackWriter, tracks, trackExporterListener);
}
/**
@@ -103,16 +103,8 @@ public class ExportActivity extends FragmentActivity implements FileTypeDialogFr
}
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);
exportAsyncTask.execute();
// }
}
@Override
@@ -68,7 +68,7 @@ public class ExportAsyncTask extends AsyncTask<Void, Integer, Boolean> {
this.trackFileFormat = trackFileFormat;
this.directory = directory;
context = exportActivity.getApplicationContext();
contentProviderUtils = ContentProviderUtils.Factory.get(context);
contentProviderUtils = new ContentProviderUtils(context);
completed = false;
processedTrackCount = 0;
@@ -26,7 +26,8 @@ import androidx.annotation.NonNull;
import java.io.OutputStream;
import de.dennisguse.opentracks.content.ContentProviderUtils;
import de.dennisguse.opentracks.content.ContentProviderUtils.LocationIterator;
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;
@@ -63,7 +64,7 @@ public class FileTrackExporter implements TrackExporter {
}
@Override
public boolean writeTrack(Context context, @NonNull OutputStream outputStream) {
public boolean writeTrack(@NonNull Context context, @NonNull OutputStream outputStream) {
try {
trackWriter.prepare(outputStream);
trackWriter.writeHeader(tracks);
@@ -209,7 +210,7 @@ public class FileTrackExporter implements TrackExporter {
*
* @author Jimmy Shih
*/
private class TrackWriterLocationFactory implements ContentProviderUtils.LocationFactory {
private class TrackWriterLocationFactory extends LocationFactory {
Location currentLocation;
Location lastLocation;
@@ -89,7 +89,7 @@ public class KmlTrackWriter implements TrackWriter {
this.exportSensorData = exportSensorData;
this.exportPhotos = exportPhotos;
this.descriptionGenerator = new DescriptionGeneratorImpl(context);
this.contentProviderUtils = ContentProviderUtils.Factory.get(context);
this.contentProviderUtils = new ContentProviderUtils(context);
}
@Override
@@ -19,7 +19,6 @@ package de.dennisguse.opentracks.io.file.exporter;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.io.OutputStream;
@@ -37,5 +36,5 @@ public interface TrackExporter {
* @param context the context
* @param outputStream the output stream
*/
boolean writeTrack(@Nullable Context context, @NonNull OutputStream outputStream);
boolean writeTrack(@NonNull Context context, @NonNull OutputStream outputStream);
}
@@ -39,8 +39,9 @@ import javax.xml.parsers.SAXParserFactory;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.ContentProviderUtils;
import de.dennisguse.opentracks.content.ContentProviderUtils.LocationIterator;
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;
@@ -165,7 +166,7 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
TripStatisticsUpdater trackTripStatisticstrackUpdater = new TripStatisticsUpdater(track.getTripStatistics().getStartTime());
TripStatisticsUpdater markerTripStatisticsUpdater = new TripStatisticsUpdater(track.getTripStatistics().getStartTime());
try (LocationIterator locationIterator = contentProviderUtils.getTrackPointLocationIterator(track.getId(), -1L, false, ContentProviderUtils.DEFAULT_LOCATION_FACTORY)) {
try (LocationIterator locationIterator = contentProviderUtils.getTrackPointLocationIterator(track.getId(), -1L, false, LocationFactory.DEFAULT_LOCATION_FACTORY)) {
while (true) {
if (waypoint == null) {
@@ -55,7 +55,7 @@ public class GpxFileTrackImporter extends AbstractFileTrackImporter {
* @param context the context
*/
public GpxFileTrackImporter(Context context) {
this(context, ContentProviderUtils.Factory.get(context));
this(context, new ContentProviderUtils(context));
}
@VisibleForTesting
@@ -45,7 +45,9 @@ 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 Context context;
// TODO Can be removed, or?
private Context context;
// TODO Use weak reference
private ImportActivity importActivity;
private WakeLock wakeLock;
@@ -100,7 +102,7 @@ public class ImportAsyncTask extends AsyncTask<Void, Integer, Boolean> {
@Override
protected Boolean doInBackground(Void... params) {
try {
Thread.currentThread().setPriority(Thread.MAX_PRIORITY);
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);
@@ -149,9 +151,11 @@ public class ImportAsyncTask extends AsyncTask<Void, Integer, Boolean> {
@Override
protected void onCancelled() {
context = null;
completed = true;
if (importActivity != null) {
importActivity.onAsyncTaskCompleted(importTrackCount, totalTrackCount);
importActivity = null;
}
}
@@ -163,17 +167,17 @@ public class ImportAsyncTask extends AsyncTask<Void, Integer, Boolean> {
private boolean importFile(final File file) {
TrackImporter trackImporter;
if (trackFileFormat == TrackFileFormat.GPX) {
trackImporter = new GpxFileTrackImporter(context);
trackImporter = new GpxFileTrackImporter(importActivity);
} else { //KML or KMZ
String extension = FileUtils.getExtension(file.getName());
if (TrackFileFormat.KML_ONLY_TRACK.getExtension().equals(extension)) {
trackImporter = new KmlFileTrackImporter(context, -1L);
trackImporter = new KmlFileTrackImporter(importActivity, -1L);
} else {
ContentProviderUtils contentProviderUtils = ContentProviderUtils.Factory.get(context);
ContentProviderUtils contentProviderUtils = new ContentProviderUtils(importActivity);
Uri uri = contentProviderUtils.insertTrack(new Track());
long newId = Long.parseLong(uri.getLastPathSegment());
trackImporter = new KmzTrackImporter(context, newId);
trackImporter = new KmzTrackImporter(importActivity, newId);
}
}
@@ -81,7 +81,7 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter {
* @param importTrackId track id to import to. -1L to import to a new track.
*/
public KmlFileTrackImporter(Context context, long importTrackId) {
this(context, importTrackId, ContentProviderUtils.Factory.get(context));
this(context, importTrackId, new ContentProviderUtils(context));
}
@VisibleForTesting
@@ -72,7 +72,7 @@ public class KmzTrackImporter implements TrackImporter {
return -1L;
}
String fileName = zipEntry.getName();
if (fileName.equals(KmzTrackExporter.KMZ_KML_FILE)) {
if (KmzTrackExporter.KMZ_KML_FILE.equals(fileName)) {
trackId = parseKml(zipInputStream);
if (trackId == -1L) {
Log.d(TAG, "Unable to parse kml in kmz");
@@ -102,7 +102,7 @@ public class KmzTrackImporter implements TrackImporter {
*/
private void cleanImport(long trackId) {
if (PreferencesUtils.isRecording(trackId)) {
ContentProviderUtils contentProviderUtils = ContentProviderUtils.Factory.get(context);
ContentProviderUtils contentProviderUtils = new ContentProviderUtils(context);
contentProviderUtils.deleteTrack(context, trackId);
}
@@ -125,15 +125,10 @@ public class KmzTrackImporter implements TrackImporter {
* @return the imported track id or -1L
*/
private long parseKml(ZipInputStream zipInputStream) throws IOException {
ByteArrayInputStream byteArrayInputStream = null;
try {
KmlFileTrackImporter kmlFileTrackImporter = new KmlFileTrackImporter(context, importTrackId);
byteArrayInputStream = new ByteArrayInputStream(getKml(zipInputStream));
KmlFileTrackImporter kmlFileTrackImporter = new KmlFileTrackImporter(context, importTrackId);
try (ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(getKml(zipInputStream))) {
return kmlFileTrackImporter.importFile(byteArrayInputStream);
} finally {
if (byteArrayInputStream != null) {
byteArrayInputStream.close();
}
}
}
@@ -160,12 +155,10 @@ public class KmzTrackImporter implements TrackImporter {
* @param fileName the file name
*/
private void readImageFile(ZipInputStream zipInputStream, String fileName) throws IOException {
if (importTrackId == -1L) {
return;
}
if (fileName.equals("")) {
if (importTrackId == -1L || fileName.equals("")) {
return;
}
File dir = FileUtils.getPhotoDir(importTrackId);
FileUtils.ensureDirectoryExists(dir);
File file = new File(dir, fileName);
@@ -33,10 +33,6 @@ public class LocationManagerConnector {
private final Handler handler;
private final LocationManager locationManager;
private LocationListener requestLocationUpdates;
private float requestLocationUpdatesDistance;
private long requestLocationUpdatesTime;
public LocationManagerConnector(Context context, Looper looper) {
this.handler = new Handler(looper);
@@ -55,10 +51,7 @@ public class LocationManagerConnector {
@Override
@SuppressLint("MissingPermission")
public void run() {
requestLocationUpdatesTime = minTime;
requestLocationUpdatesDistance = minDistance;
requestLocationUpdates = locationListener;
locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, requestLocationUpdatesTime, requestLocationUpdatesDistance, locationListener);
locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, minTime, minDistance, locationListener);
}
});
}
@@ -70,6 +63,5 @@ public class LocationManagerConnector {
*/
public void removeLocationUpdates(final LocationListener locationListener) {
locationManager.removeUpdates(locationListener);
requestLocationUpdates = null;
}
}
@@ -16,9 +16,6 @@
package de.dennisguse.opentracks.services;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Context;
@@ -30,7 +27,6 @@ import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
@@ -38,7 +34,6 @@ import android.os.PowerManager.WakeLock;
import android.util.Log;
import androidx.annotation.VisibleForTesting;
import androidx.core.app.NotificationCompat;
import androidx.core.app.TaskStackBuilder;
import java.util.concurrent.ExecutorService;
@@ -48,19 +43,17 @@ import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.TrackDetailActivity;
import de.dennisguse.opentracks.TrackListActivity;
import de.dennisguse.opentracks.content.ContentProviderUtils;
import de.dennisguse.opentracks.content.ContentProviderUtils.LocationIterator;
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.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;
@@ -79,36 +72,30 @@ 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 = "de.dennisguse.opentracks.RESUME_TRACK";
private static final int NOTIFICATION_ID = 123;
public static final double PAUSE_LATITUDE = 100.0;
public static final double RESUME_LATITUDE = 200.0;
// 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);
// The following variables are set in onCreate:
private ExecutorService executorService;
private Context context;
private ContentProviderUtils contentProviderUtils;
private Handler handler;
private LocationManagerConnector locationManagerConnector;
private PeriodicTaskExecutor voiceExecutor;
private PeriodicTaskExecutor splitExecutor;
private SharedPreferences sharedPreferences;
private TrackRecordingServiceNotificationManager notificationManager;
private long recordingTrackId;
private boolean recordingTrackPaused;
private LocationListenerPolicy locationListenerPolicy;
private int recordingDistanceInterval;
private int maxRecordingDistance;
private int recordingGpsAccuracy;
private int autoResumeTrackTimeout;
private long currentRecordingInterval;
// The following variables are set when recording:
@@ -117,10 +104,11 @@ public class TrackRecordingService extends Service {
private final OnSharedPreferenceChangeListener sharedPreferenceChangeListener = new OnSharedPreferenceChangeListener() {
@Override
public void onSharedPreferenceChanged(SharedPreferences preferences, String key) {
if (PreferencesUtils.isKey(context, R.string.recording_track_id_key, key)) {
Context context = TrackRecordingService.this;
if (PreferencesUtils.isKey(TrackRecordingService.this, R.string.recording_track_id_key, key)) {
// Only through the TrackRecordingService can one stop a recording and set the recordingTrackId to -1L.
if (PreferencesUtils.isRecording(context)) {
recordingTrackId = PreferencesUtils.getRecordingTrackId(context);
if (PreferencesUtils.isRecording(TrackRecordingService.this)) {
recordingTrackId = PreferencesUtils.getRecordingTrackId(TrackRecordingService.this);
}
}
if (PreferencesUtils.isKey(context, R.string.recording_track_paused_key, key)) {
@@ -129,27 +117,20 @@ 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);
switch (minRecordingInterval) {
case PreferencesUtils.MIN_RECORDING_INTERVAL_ADAPT_BATTERY_LIFE:
// Choose battery life over moving time accuracy.
locationListenerPolicy = new AdaptiveLocationListenerPolicy(30 * UnitConversions.ONE_SECOND, 5 * ONE_MINUTE, 5);
break;
case PreferencesUtils.MIN_RECORDING_INTERVAL_ADAPT_ACCURACY:
// Get all the updates.
locationListenerPolicy = new AdaptiveLocationListenerPolicy(UnitConversions.ONE_SECOND, 30 * UnitConversions.ONE_SECOND, 0);
break;
default:
locationListenerPolicy = new AbsoluteLocationListenerPolicy(minRecordingInterval * UnitConversions.ONE_SECOND);
if (minRecordingInterval == PreferencesUtils.getMinRecordingIntervalAdaptBatteryLife(context)) {
// Choose battery life over moving time accuracy.
locationListenerPolicy = new AdaptiveLocationListenerPolicy(30 * UnitConversions.ONE_SECOND, 5 * ONE_MINUTE, 5);
} else if (minRecordingInterval == PreferencesUtils.getMinRecordingIntervalAdaptAccuracy(context)) {
// Get all the updates.
locationListenerPolicy = new AdaptiveLocationListenerPolicy(UnitConversions.ONE_SECOND, 30 * UnitConversions.ONE_SECOND, 0);
} else {
locationListenerPolicy = new AbsoluteLocationListenerPolicy(minRecordingInterval * UnitConversions.ONE_SECOND);
}
}
if (PreferencesUtils.isKey(context, R.string.recording_distance_interval_key, key)) {
@@ -161,12 +142,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;
@@ -221,14 +199,13 @@ public class TrackRecordingService extends Service {
public void onCreate() {
super.onCreate();
executorService = Executors.newSingleThreadExecutor();
context = this;
contentProviderUtils = ContentProviderUtils.Factory.get(this);
contentProviderUtils = new ContentProviderUtils(this);
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);
// onSharedPreferenceChanged might not set recordingTrackId.
recordingTrackId = PreferencesUtils.RECORDING_TRACK_ID_DEFAULT;
@@ -253,7 +230,6 @@ public class TrackRecordingService extends Service {
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
handleStartCommand(intent, startId);
return START_STICKY;
}
@@ -270,7 +246,7 @@ public class TrackRecordingService extends Service {
}
// Reverse order from onCreate
showNotification(false);
showNotification(false); //TODO Why?
handler.removeCallbacks(registerLocationRunnable);
unregisterLocationListener();
@@ -278,12 +254,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 {
@@ -304,23 +274,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;
@@ -339,43 +300,19 @@ public class TrackRecordingService extends Service {
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);
String name = waypointCreationRequest.getName();
if (name == null) {
int nextWaypointNumber = contentProviderUtils.getNextWaypointNumber(recordingTrackId, waypointCreationRequest.getType());
if (nextWaypointNumber == -1) {
nextWaypointNumber = 0;
}
name = getString(isStatistics ? R.string.marker_split_name_format : R.string.marker_name_format, nextWaypointNumber);
name = getString(R.string.marker_name_format, nextWaypointNumber);
}
// Get category
String category = waypointCreationRequest.getCategory() != null ? waypointCreationRequest.getCategory() : "";
String description = waypointCreationRequest.getDescription() != null ? waypointCreationRequest.getDescription() : "";
String icon = getString(R.string.marker_waypoint_icon_url);
// 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);
@@ -398,97 +335,11 @@ public class TrackRecordingService extends Service {
String photoUrl = waypointCreationRequest.getPhotoUrl() != null ? waypointCreationRequest.getPhotoUrl() : "";
// 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, waypointCreationRequest.getType(), length, duration, -1L, -1L, location, null, photoUrl);
Uri uri = contentProviderUtils.insertWaypoint(waypoint);
return Long.parseLong(uri.getLastPathSegment());
}
/**
* Starts the service as a foreground service.
*
* @param pendingIntent the notification pending intent
* @param messageId the notification message id
*/
@VisibleForTesting
protected void startForegroundService(PendingIntent pendingIntent, int messageId) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationChannel notificationChannel = new NotificationChannel(getString(R.string.app_name), getString(R.string.app_name), NotificationManager.IMPORTANCE_DEFAULT);
NotificationManager manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
if (manager != null) {
manager.createNotificationChannel(notificationChannel);
}
}
NotificationCompat.Builder builder = new NotificationCompat.Builder(this, getString(R.string.app_name))
.setContentIntent(pendingIntent)
.setContentText(getString(messageId))
.setContentTitle(getString(R.string.app_name))
.setOngoing(true)
.setSmallIcon(R.drawable.ic_logo_color_24dp)
.setCategory(Notification.CATEGORY_SERVICE)
.setWhen(System.currentTimeMillis());
startForeground(1, builder.build());
}
/**
* Stops the service as a foreground service.
*/
@VisibleForTesting
protected void stopForegroundService() {
stopForeground(true);
}
/**
* 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(context.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(context.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.
*
@@ -501,7 +352,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();
@@ -510,7 +360,6 @@ public class TrackRecordingService extends Service {
// Update shared preferences
updateRecordingState(trackId, false);
PreferencesUtils.resetAutoResumeTrackCurrentRetryDefault(this);
// Update database
track.setId(trackId);
@@ -521,6 +370,8 @@ public class TrackRecordingService extends Service {
track.setIcon(TrackIconUtils.getIconValue(this, category));
track.setTripStatistics(trackTripStatisticsUpdater.getTripStatistics());
contentProviderUtils.updateTrack(track);
//TODO Do not use insertWaypoint
insertWaypoint(WaypointCreationRequest.DEFAULT_START_TRACK);
startRecording(true);
@@ -538,23 +389,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, ContentProviderUtils.DEFAULT_LOCATION_FACTORY)) {
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);
@@ -589,8 +428,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
*/
@@ -606,7 +444,6 @@ public class TrackRecordingService extends Service {
// Restore periodic tasks
voiceExecutor.restore();
splitExecutor.restore();
}
/**
@@ -651,7 +488,7 @@ public class TrackRecordingService extends Service {
contentProviderUtils.updateTrack(track);
}
}
endRecording(true, trackId);
endRecording(true);
}
/**
@@ -679,19 +516,19 @@ public class TrackRecordingService extends Service {
insertLocation(track, pause, null);
}
endRecording(false, recordingTrackId);
endRecording(false);
notificationManager.updateContent(getString(R.string.generic_paused));
}
/**
* 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) {
@@ -766,6 +603,8 @@ public class TrackRecordingService extends Service {
return;
}
notificationManager.updateLocation(this, location, recordingGpsAccuracy);
if (!location.hasAccuracy() || location.getAccuracy() >= recordingGpsAccuracy) {
Log.d(TAG, "Ignore onLocationChangedAsync. Poor accuracy.");
return;
@@ -801,10 +640,7 @@ public class TrackRecordingService extends Service {
}
if (!LocationUtils.isValidLocation(lastValidTrackPoint)) {
/*
* Should not happen. The current segment should have a location. Just
* insert the current location.
*/
// Should not happen. The current segment should have a location. Just insert the current location.
insertLocation(track, location, null);
lastLocation = location;
return;
@@ -869,7 +705,6 @@ public class TrackRecordingService extends Service {
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));
} catch (SQLiteException e) {
/*
@@ -879,7 +714,6 @@ public class TrackRecordingService extends Service {
Log.w(TAG, "SQLiteException", e);
}
voiceExecutor.update();
splitExecutor.update();
}
/**
@@ -951,33 +785,33 @@ public class TrackRecordingService extends Service {
}
}
/**
* Shows the notification.
*
* @param isGpsStarted true if GPS is started
*/
private void showNotification(boolean isGpsStarted) {
if (isRecording()) {
if (isPaused()) {
stopForegroundService();
} else {
Intent intent = IntentUtils.newIntent(this, TrackDetailActivity.class)
.putExtra(TrackDetailActivity.EXTRA_TRACK_ID, recordingTrackId);
PendingIntent pendingIntent = TaskStackBuilder.create(this)
.addParentStack(TrackDetailActivity.class).addNextIntent(intent)
.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT);
startForegroundService(pendingIntent, R.string.track_record_notification);
}
} else {
// Not recording
if (isGpsStarted) {
Intent intent = IntentUtils.newIntent(this, TrackListActivity.class);
PendingIntent pendingIntent = TaskStackBuilder.create(this)
.addNextIntent(intent).getPendingIntent(0, 0);
startForegroundService(pendingIntent, R.string.gps_starting);
} else {
stopForegroundService();
}
if ((isRecording() && isPaused()) || (!isRecording() && !isGpsStarted)) {
stopForeground(true);
}
if (isRecording() && !isPaused()) {
Intent intent = IntentUtils.newIntent(this, TrackDetailActivity.class)
.putExtra(TrackDetailActivity.EXTRA_TRACK_ID, recordingTrackId);
PendingIntent pendingIntent = TaskStackBuilder.create(this)
.addParentStack(TrackDetailActivity.class)
.addNextIntent(intent)
.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT);
notificationManager.updatePendingIntent(pendingIntent);
notificationManager.updateContent(getString(R.string.gps_starting));
startForeground(NOTIFICATION_ID, notificationManager.getNotification());
}
if (!isRecording() && isGpsStarted) {
Intent intent = IntentUtils.newIntent(this, TrackListActivity.class);
PendingIntent pendingIntent = TaskStackBuilder.create(this)
.addNextIntent(intent)
.getPendingIntent(0, 0);
notificationManager.updatePendingIntent(pendingIntent);
notificationManager.updateContent(getString(R.string.gps_starting));
startForeground(NOTIFICATION_ID, notificationManager.getNotification());
}
}
@@ -1070,15 +904,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();
}
/**
@@ -48,6 +48,8 @@ public class TrackRecordingServiceConnection implements ServiceConnection, Death
private static final String TAG = TrackRecordingServiceConnection.class.getSimpleName();
@Deprecated
//TODO Always the context of the owner of a service connection (i.e., let it be passed to every call).
private final Context context;
private final Runnable callback;
@@ -0,0 +1,94 @@
package de.dennisguse.opentracks.services;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.location.Location;
import android.os.Build;
import androidx.annotation.VisibleForTesting;
import androidx.core.app.NotificationCompat;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.util.PreferencesUtils;
import de.dennisguse.opentracks.util.StringUtils;
/**
* Manages the content of the notification shown by {@link TrackRecordingService}.
*/
class TrackRecordingServiceNotificationManager {
private final static int NOTIFICATION_ID = 123;
private final static String CHANNEL_ID = TrackRecordingServiceNotificationManager.class.getSimpleName();
private NotificationCompat.Builder notificationBuilder;
private NotificationManager notificationManager;
private boolean previousLocationWasAccurate = true;
TrackRecordingServiceNotificationManager(Context context) {
notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationChannel notificationChannel = new NotificationChannel(CHANNEL_ID, context.getString(R.string.app_name), NotificationManager.IMPORTANCE_HIGH);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
notificationChannel.setAllowBubbles(true);
}
notificationManager.createNotificationChannel(notificationChannel);
}
notificationBuilder = new NotificationCompat.Builder(context, CHANNEL_ID);
notificationBuilder
.setDefaults(NotificationCompat.DEFAULT_ALL)
.setPriority(NotificationCompat.PRIORITY_MAX)
.setOnlyAlertOnce(true)
.setOngoing(true)
.setCategory(NotificationCompat.CATEGORY_SERVICE)
.setContentTitle(context.getString(R.string.app_name))
.setSmallIcon(R.drawable.ic_logo_color_24dp);
}
@VisibleForTesting
TrackRecordingServiceNotificationManager(NotificationManager notificationManager, NotificationCompat.Builder notificationBuilder) {
this.notificationManager = notificationManager;
this.notificationBuilder = notificationBuilder;
}
void updateContent(String content) {
notificationBuilder.setContentText(content);
updateNotification();
}
void updateLocation(Context context, Location location, int recordingGpsAccuracy) {
String formattedAccuracy = context.getString(R.string.value_none);
if (location.hasAccuracy()) {
formattedAccuracy = StringUtils.formatDistance(context, location.getAccuracy(), PreferencesUtils.isMetricUnits(context));
boolean currentLocationWasAccurate = location.getAccuracy() < recordingGpsAccuracy;
boolean shouldAlert = !currentLocationWasAccurate && previousLocationWasAccurate;
notificationBuilder.setOnlyAlertOnce(!shouldAlert);
previousLocationWasAccurate = currentLocationWasAccurate;
}
notificationBuilder.setContentText(context.getString(R.string.track_recording_notification_accuracy, formattedAccuracy));
updateNotification();
notificationBuilder.setOnlyAlertOnce(true);
}
void updatePendingIntent(PendingIntent pendingIntent) {
notificationBuilder.setContentIntent(pendingIntent);
updateNotification();
}
Notification getNotification() {
return notificationBuilder.build();
}
private void updateNotification() {
notificationManager.notify(NOTIFICATION_ID, getNotification());
}
}
@@ -21,6 +21,7 @@ import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCallback;
import android.bluetooth.BluetoothGattCharacteristic;
import android.bluetooth.BluetoothGattDescriptor;
import android.bluetooth.BluetoothGattService;
import android.bluetooth.BluetoothProfile;
import android.content.Context;
import android.os.Handler;
@@ -45,7 +46,6 @@ public class BluetoothConnectionManager {
static final int MESSAGE_READ = 3;
static final int MESSAGE_DISCONNECTED = 4;
private static final UUID HEART_RATE_SERVICE_UUID = new UUID(0x180D00001000L, 0x800000805f9b34fbL);
private static final UUID HEART_RATE_MEASUREMENT_CHAR_UUID = new UUID(0x2A3700001000L, 0x800000805f9b34fbL);
private static final UUID CLIENT_CHARACTERISTIC_CONFIG_UUID = new UUID(0x290200001000L, 0x800000805f9b34fbL);
@@ -90,10 +90,18 @@ public class BluetoothConnectionManager {
@Override
public void onServicesDiscovered(@NonNull BluetoothGatt gatt, int status) {
BluetoothGattCharacteristic characteristic = gatt
.getService(HEART_RATE_SERVICE_UUID)
.getCharacteristic(HEART_RATE_MEASUREMENT_CHAR_UUID);
BluetoothGattService service = gatt.getService(BluetoothUtils.HEART_RATE_SERVICE_UUID);
if (service == null) {
Log.e(TAG, "Could not get heart rate service for " + gatt.getDevice().getAddress());
return;
}
BluetoothGattCharacteristic characteristic = service.getCharacteristic(HEART_RATE_MEASUREMENT_CHAR_UUID);
if (characteristic == null) {
Log.e(TAG, "Could not get BluetoothCharacteristic for " + gatt.getDevice().getAddress());
return;
}
gatt.setCharacteristicNotification(characteristic, true);
BluetoothGattDescriptor descriptor = characteristic.getDescriptor(CLIENT_CHARACTERISTIC_CONFIG_UUID);
@@ -131,8 +139,9 @@ public class BluetoothConnectionManager {
Log.d(TAG, "Connecting to: " + bluetoothDevice);
setState(SensorState.CONNECTING);
bluetoothGatt = bluetoothDevice.connectGatt(this.context, true, this.connectCallback);
setState(SensorState.CONNECTING);
}
public synchronized void disconnect() {
@@ -26,12 +26,10 @@ import android.os.Message;
import android.util.Log;
import android.widget.Toast;
import java.util.ArrayList;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.sensor.SensorDataSet;
import de.dennisguse.opentracks.util.BluetoothUtils;
import de.dennisguse.opentracks.util.PreferencesUtils;
import de.dennisguse.opentracks.util.UnitConversions;
/**
* Bluetooth LE sensor manager.
@@ -45,7 +43,7 @@ public class BluetoothRemoteSensorManager {
private static final String TAG = BluetoothConnectionManager.class.getSimpleName();
private static final BluetoothAdapter bluetoothAdapter = getDefaultBluetoothAdapter();
private static final BluetoothAdapter bluetoothAdapter = BluetoothUtils.getDefaultBluetoothAdapter(TAG);
private final Context context;
@@ -87,12 +85,12 @@ public class BluetoothRemoteSensorManager {
private final SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener = new SharedPreferences.OnSharedPreferenceChangeListener() {
@Override
public void onSharedPreferenceChanged(SharedPreferences preferences, String key) {
if (bluetoothConnectionManager != null && PreferencesUtils.isKey(context, R.string.settings_sensor_bluetooth_sensor_key, key)) {
String address = PreferencesUtils.getString(context, R.string.settings_sensor_bluetooth_sensor_key, PreferencesUtils.BLUETOOTH_SENSOR_DEFAULT);
if (address.equals(PreferencesUtils.BLUETOOTH_SENSOR_DEFAULT)) {
if (bluetoothConnectionManager != null && PreferencesUtils.isKey(context, R.string.settings_sensor_bluetooth_heart_rate_key, key)) {
if (PreferencesUtils.isBluetoothHeartRateSensorAddressDefault(context)) {
stop();
return;
}
String address = PreferencesUtils.getBluetoothHeartRateSensorAddress(context);
if (bluetoothConnectionManager.isSameBluetoothDevice(address)) {
return;
}
@@ -114,43 +112,6 @@ public class BluetoothRemoteSensorManager {
sharedPreferences = PreferencesUtils.getSharedPreferences(context);
}
private static BluetoothAdapter getDefaultBluetoothAdapter() {
// If from the main application thread, return directly
if (Thread.currentThread().equals(Looper.getMainLooper().getThread())) {
return BluetoothAdapter.getDefaultAdapter();
}
// Get the default adapter from the main application thread
final ArrayList<BluetoothAdapter> adapters = new ArrayList<>(1);
final Object mutex = new Object();
Handler handler = new Handler(Looper.getMainLooper());
handler.post(new Runnable() {
@Override
public void run() {
adapters.add(BluetoothAdapter.getDefaultAdapter());
synchronized (mutex) {
mutex.notify();
}
}
});
while (adapters.isEmpty()) {
synchronized (mutex) {
try {
mutex.wait(UnitConversions.ONE_SECOND);
} catch (InterruptedException e) {
Log.e(TAG, "Interrupted while waiting for default bluetooth adapter", e);
}
}
}
if (adapters.get(0) == null) {
Log.w(TAG, "No bluetooth adapter found.");
}
return adapters.get(0);
}
public void start() {
sharedPreferences.registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
startCurrentSensor();
@@ -184,11 +145,11 @@ public class BluetoothRemoteSensorManager {
return;
}
String address = PreferencesUtils.getString(context, R.string.settings_sensor_bluetooth_sensor_key, PreferencesUtils.BLUETOOTH_SENSOR_DEFAULT);
if (PreferencesUtils.BLUETOOTH_SENSOR_DEFAULT.equals(address)) {
if (PreferencesUtils.isBluetoothHeartRateSensorAddressDefault(context)) {
Log.w(TAG, "No bluetooth address.");
return;
}
String address = PreferencesUtils.getBluetoothHeartRateSensorAddress(context);
Log.i(TAG, "Connecting to bluetooth address: " + address);
BluetoothDevice device;
@@ -21,12 +21,8 @@ import android.media.AudioManager;
import android.speech.tts.TextToSpeech;
import android.speech.tts.TextToSpeech.OnInitListener;
import android.speech.tts.UtteranceProgressListener;
import android.telephony.PhoneStateListener;
import android.telephony.TelephonyManager;
import android.util.Log;
import androidx.annotation.VisibleForTesting;
import java.util.Locale;
import de.dennisguse.opentracks.R;
@@ -46,22 +42,54 @@ public class AnnouncementPeriodicTask implements PeriodicTask {
/**
* The rate at which announcements are spoken.
*/
@VisibleForTesting
static final float TTS_SPEECH_RATE = 0.9f;
private static final float TTS_SPEECH_RATE = 0.9f;
private static final String TAG = AnnouncementPeriodicTask.class.getSimpleName();
private final Context context;
private final AudioManager audioManager;
private final AudioManager.OnAudioFocusChangeListener audioFocusChangeListener = new AudioManager.OnAudioFocusChangeListener() {
@Override
public void onAudioFocusChange(int focusChange) {
Log.d(TAG, "Audio focus changed to " + focusChange);
boolean stop = false;
switch (focusChange) {
case AudioManager.AUDIOFOCUS_GAIN:
stop = false;
break;
case AudioManager.AUDIOFOCUS_LOSS:
stop = true;
break;
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
stop = true;
break;
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
stop = true;
break;
}
if (stop && tts != null && tts.isSpeaking()) {
tts.stop();
Log.i(TAG, "Aborting current tts due to focus change " + focusChange);
}
}
};
private final UtteranceProgressListener utteranceListener = new UtteranceProgressListener() {
@Override
public void onStart(String utteranceId) {
int result = audioManager.requestAudioFocus(audioFocusChangeListener, TextToSpeech.Engine.DEFAULT_STREAM, AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK);
if (result == AudioManager.AUDIOFOCUS_REQUEST_FAILED) {
Log.w(TAG, "Failed to request audio focus.");
}
}
@Override
public void onDone(String utteranceId) {
int result = audioManager.abandonAudioFocus(null);
int result = audioManager.abandonAudioFocus(audioFocusChangeListener);
if (result == AudioManager.AUDIOFOCUS_REQUEST_FAILED) {
Log.w(TAG, "Failed to relinquish audio focus.");
}
@@ -69,32 +97,16 @@ public class AnnouncementPeriodicTask implements PeriodicTask {
@Override
public void onError(String utteranceId) {
Log.e(TAG, "An error occurred for utteranceId " + utteranceId);
}
};
private TextToSpeech tts;
// Response from TTS after its initialization
private int initStatus = TextToSpeech.ERROR;
private int ttsInitStatus = TextToSpeech.ERROR;
private boolean ttsReady = false;
// True if speech is allowed
private boolean speechAllowed;
/**
* Listener which updates {@link #speechAllowed} when the phone state changes.
*/
private final PhoneStateListener phoneStateListener = new PhoneStateListener() {
@Override
public void onCallStateChanged(int state, String incomingNumber) {
speechAllowed = state == TelephonyManager.CALL_STATE_IDLE;
if (!speechAllowed && tts != null && tts.isSpeaking()) {
// If we're already speaking, stop it.
tts.stop();
}
}
};
AnnouncementPeriodicTask(Context context) {
this.context = context;
audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
@@ -102,16 +114,17 @@ public class AnnouncementPeriodicTask implements PeriodicTask {
@Override
public void start() {
Log.d(TAG, "Start");
if (tts == null) {
tts = newTextToSpeech(context, new OnInitListener() {
tts = new TextToSpeech(context, new OnInitListener() {
@Override
public void onInit(int status) {
initStatus = status;
Log.i(TAG, "TextToSpeech initialized with status " + status);
ttsInitStatus = status;
}
});
}
speechAllowed = true;
listenToPhoneState(phoneStateListener, PhoneStateListener.LISTEN_CALL_STATE);
}
@Override
@@ -128,8 +141,7 @@ public class AnnouncementPeriodicTask implements PeriodicTask {
*
* @param tripStatistics the trip statistics
*/
@VisibleForTesting
void announce(TripStatistics tripStatistics) {
private void announce(TripStatistics tripStatistics) {
if (tripStatistics == null) {
Log.e(TAG, "TripStatistics is null.");
return;
@@ -137,7 +149,7 @@ public class AnnouncementPeriodicTask implements PeriodicTask {
synchronized (this) {
if (!ttsReady) {
ttsReady = initStatus == TextToSpeech.SUCCESS;
ttsReady = ttsInitStatus == TextToSpeech.SUCCESS;
if (ttsReady) {
onTtsReady();
}
@@ -148,7 +160,7 @@ public class AnnouncementPeriodicTask implements PeriodicTask {
}
}
if (!speechAllowed) {
if (audioManager.getMode() == AudioManager.MODE_IN_CALL || audioManager.getMode() == AudioManager.MODE_IN_COMMUNICATION) {
Log.i(TAG, "Speech is not allowed at this time.");
return;
}
@@ -157,21 +169,18 @@ public class AnnouncementPeriodicTask implements PeriodicTask {
@Override
public void shutdown() {
listenToPhoneState(phoneStateListener, PhoneStateListener.LISTEN_NONE);
if (tts != null) {
tts.shutdown();
tts = null;
}
}
/**
* Called when TTS is ready.
*/
private void onTtsReady() {
Locale locale = Locale.getDefault();
int languageAvailability = tts.isLanguageAvailable(locale);
if (languageAvailability == TextToSpeech.LANG_MISSING_DATA
|| languageAvailability == TextToSpeech.LANG_NOT_SUPPORTED) {
if (languageAvailability == TextToSpeech.LANG_MISSING_DATA || languageAvailability == TextToSpeech.LANG_NOT_SUPPORTED) {
Log.w(TAG, "Default locale not available, use English.");
locale = Locale.ENGLISH;
/*
@@ -187,42 +196,12 @@ public class AnnouncementPeriodicTask implements PeriodicTask {
tts.setOnUtteranceProgressListener(utteranceListener);
}
/**
* Speaks the announcement.
*
* @param announcement the announcement
*/
private void speakAnnouncement(String announcement) {
int result = audioManager.requestAudioFocus(null, TextToSpeech.Engine.DEFAULT_STREAM, AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK);
if (result == AudioManager.AUDIOFOCUS_REQUEST_FAILED) {
Log.w(TAG, "Failed to request audio focus.");
}
/*
* We don't care about the utterance id. It is supplied here to force
* onUtteranceCompleted to be called.
*/
// We don't care about the utterance id. It is supplied here to force onUtteranceCompleted to be called.
tts.speak(announcement, TextToSpeech.QUEUE_FLUSH, null, "not used");
}
/**
* Create a new {@link TextToSpeech}.
*
* @param aContext a context
* @param onInitListener an on init listener
*/
@VisibleForTesting
protected TextToSpeech newTextToSpeech(Context aContext, OnInitListener onInitListener) {
return new TextToSpeech(aContext, onInitListener);
}
/**
* Gets the announcement.
*
* @param tripStatistics the trip statistics
*/
@VisibleForTesting
protected String getAnnouncement(TripStatistics tripStatistics) {
private String getAnnouncement(TripStatistics tripStatistics) {
boolean metricUnits = PreferencesUtils.isMetricUnits(context);
boolean reportSpeed = PreferencesUtils.isReportSpeed(context);
double distance = tripStatistics.getTotalDistance() * UnitConversions.M_TO_KM;
@@ -255,27 +234,7 @@ public class AnnouncementPeriodicTask implements PeriodicTask {
getAnnounceTime(tripStatistics.getMovingTime()), rate);
}
/**
* Listens to phone state.
*
* @param listener the listener
* @param events the interested events
*/
@VisibleForTesting
protected void listenToPhoneState(PhoneStateListener listener, int events) {
TelephonyManager telephony = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
if (telephony != null) {
telephony.listen(listener, events);
}
}
/**
* Gets the announce time.
*
* @param time the time
*/
@VisibleForTesting
String getAnnounceTime(long time) {
private String getAnnounceTime(long time) {
int[] parts = StringUtils.getTimeParts(time);
String seconds = context.getResources()
.getQuantityString(R.plurals.voiceSeconds, parts[0], parts[0]);
@@ -17,9 +17,9 @@ package de.dennisguse.opentracks.services.tasks;
import android.util.Log;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.services.TrackRecordingService;
import de.dennisguse.opentracks.stats.TripStatistics;
import de.dennisguse.opentracks.util.PreferencesUtils;
import de.dennisguse.opentracks.util.UnitConversions;
/**
@@ -35,13 +35,14 @@ public class PeriodicTaskExecutor {
private final TrackRecordingService trackRecordingService;
private final PeriodicTaskFactory periodicTaskFactory;
private final int TASK_FREQUENCY_OFF;
/**
* The task frequency.
* A positive value is a time frequency (minutes).
* A negative value is a distance frequency (km or mi).
* A zero value is to turn off periodic task.
*/
private int taskFrequency = PreferencesUtils.FREQUENCY_OFF;
private int taskFrequency;
private PeriodicTask periodicTask;
@@ -56,6 +57,9 @@ public class PeriodicTaskExecutor {
public PeriodicTaskExecutor(TrackRecordingService trackRecordingService, PeriodicTaskFactory periodicTaskFactory) {
this.trackRecordingService = trackRecordingService;
this.periodicTaskFactory = periodicTaskFactory;
TASK_FREQUENCY_OFF = Integer.parseInt(trackRecordingService.getBaseContext().getResources().getString(R.string.frequency_off));
taskFrequency = TASK_FREQUENCY_OFF;
}
/**
@@ -71,7 +75,7 @@ public class PeriodicTaskExecutor {
timerTaskExecutor.shutdown();
timerTaskExecutor = null;
}
if (taskFrequency == PreferencesUtils.FREQUENCY_OFF) {
if (taskFrequency == TASK_FREQUENCY_OFF) {
Log.d(TAG, "Task frequency is off.");
return;
}
@@ -1,41 +0,0 @@
/*
* Copyright 2010 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package de.dennisguse.opentracks.services.tasks;
import de.dennisguse.opentracks.content.WaypointCreationRequest;
import de.dennisguse.opentracks.services.TrackRecordingService;
/**
* A simple task to insert statistics markers periodically.
*
* @author Sandor Dornbush
*/
public class SplitPeriodicTask implements PeriodicTask {
@Override
public void start() {
}
@Override
public void run(TrackRecordingService trackRecordingService) {
trackRecordingService.insertWaypoint(WaypointCreationRequest.DEFAULT_STATISTICS);
}
@Override
public void shutdown() {
}
}
@@ -1,32 +0,0 @@
/*
* Copyright 2012 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package de.dennisguse.opentracks.services.tasks;
import android.content.Context;
/**
* A {@link PeriodicTaskFactory} for {@link SplitPeriodicTask}.
*
* @author Jimmy Shih
*/
public class SplitPeriodicTaskFactory implements PeriodicTaskFactory {
@Override
public PeriodicTask create(Context context) {
return new SplitPeriodicTask();
}
}
@@ -43,14 +43,6 @@ import de.dennisguse.opentracks.util.TrackIconUtils;
*/
public class ActivityTypePreference extends DialogPreference {
private SummaryProvider<DialogPreference> summaryProvider = new SummaryProvider<DialogPreference>() {
@Override
public CharSequence provideSummary(DialogPreference preference) {
String defaultActivity = PreferencesUtils.getDefaultActivity(ActivityTypePreference.this.getContext());
return defaultActivity != null && !defaultActivity.equals(PreferencesUtils.DEFAULT_ACTIVITY_DEFAULT) ? defaultActivity : getContext().getString(R.string.value_unknown);
}
};
public ActivityTypePreference(Context context, AttributeSet attrs) {
super(context, attrs);
setDialogLayoutResource(R.layout.preference_activity_type);
@@ -58,6 +50,13 @@ public class ActivityTypePreference extends DialogPreference {
setNegativeButtonText(android.R.string.cancel);
setDialogIcon(null);
setPersistent(true);
SummaryProvider<DialogPreference> summaryProvider = new SummaryProvider<DialogPreference>() {
@Override
public CharSequence provideSummary(DialogPreference preference) {
return PreferencesUtils.getDefaultActivity(ActivityTypePreference.this.getContext());
}
};
setSummaryProvider(summaryProvider);
}
@@ -0,0 +1,112 @@
package de.dennisguse.opentracks.settings;
import android.bluetooth.BluetoothDevice;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.TextView;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
public class BluetoothLeAdapter extends BaseAdapter {
/**
* Contains a unique list (by address) of devices.
*/
private final List<Device> devices = new ArrayList<>();
@Override
public int getCount() {
return devices.size();
}
@Override
public Object getItem(int position) {
return devices.get(position);
}
@Override
public long getItemId(int position) {
return devices.get(position).hashCode();
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
View currentView = convertView;
if (convertView == null) {
LayoutInflater inflater = LayoutInflater.from(parent.getContext());
//TODO Check if there is a better way to achieve identical look and feel to ListPreference.
//Use material design single choice; for old style use: android.R.layout.select_dialog_singlechoice
currentView = inflater.inflate(androidx.appcompat.R.layout.select_dialog_singlechoice_material, parent);
}
Device device = devices.get(position);
TextView textView = currentView.findViewById(android.R.id.text1);
textView.setText(device.getName());
return currentView;
}
public void add(String name, String address) {
Device device = new Device(name, address);
if (!devices.contains(device)) {
devices.add(new Device(name, address));
} else {
for (Device currentDevice : devices) {
if (currentDevice.getAddress().equals(address)) {
currentDevice.setName(name);
}
}
}
notifyDataSetChanged();
}
public void add(BluetoothDevice bluetoothDevice) {
add(bluetoothDevice.getName(), bluetoothDevice.getAddress());
}
public Device get(int index) {
return devices.get(index);
}
public class Device {
private String name;
private String address;
public Device(String name, String address) {
this.name = name;
this.address = address;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getAddress() {
return address;
}
/**
* Check if the address is identical.
*/
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Device)) return false;
Device device = (Device) o;
return address.equals(device.address);
}
@Override
public int hashCode() {
return Objects.hash(name, address);
}
}
}
@@ -0,0 +1,206 @@
package de.dennisguse.opentracks.settings;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.le.BluetoothLeScanner;
import android.bluetooth.le.ScanCallback;
import android.bluetooth.le.ScanFilter;
import android.bluetooth.le.ScanResult;
import android.bluetooth.le.ScanSettings;
import android.content.Context;
import android.content.DialogInterface;
import android.os.Bundle;
import android.os.ParcelUuid;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.Log;
import android.widget.Toast;
import androidx.appcompat.app.AlertDialog;
import androidx.preference.DialogPreference;
import androidx.preference.PreferenceDialogFragmentCompat;
import java.util.ArrayList;
import java.util.List;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.util.BluetoothUtils;
import de.dennisguse.opentracks.util.PreferencesUtils;
/**
* Preference to select a discoverable Bluetooth LE device.
* Based upon ListPreference.
*/
public class BluetoothLePreference extends DialogPreference {
private final static String TAG = BluetoothLePreference.class.getSimpleName();
private final static int DEVICE_NONE_RESOURCEID = R.string.value_none;
public BluetoothLePreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
public BluetoothLePreference(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public BluetoothLePreference(Context context, AttributeSet attrs) {
super(context, attrs);
}
public BluetoothLePreference(Context context) {
super(context);
}
private String value;
private boolean valueSet = false;
public String getValue() {
return value;
}
public void setValue(String value) {
final boolean changed = !TextUtils.equals(this.value, value);
if (changed || !valueSet) {
this.value = value;
valueSet = true;
persistString(value);
if (changed) {
notifyChanged();
}
}
}
@Override
protected void onSetInitialValue(Object defaultValue) {
setValue(getPersistedString((String) defaultValue));
}
@Override
public CharSequence getSummary() {
if (getValue() == null || PreferencesUtils.isBluetoothHeartRateSensorAddressDefault(getContext(), getValue())) {
return getContext().getString(DEVICE_NONE_RESOURCEID);
}
return getValue();
}
public static class BluetoothLePreferenceDialog extends PreferenceDialogFragmentCompat {
private int selectedEntryIndex;
private BluetoothLeAdapter listAdapter = new BluetoothLeAdapter();
private BluetoothLeScanner scanner = null;
private ScanCallback scanCallback = new ScanCallback() {
@Override
public void onScanResult(int callbackType, ScanResult result) {
super.onScanResult(callbackType, result);
Log.d(TAG, "Found device " + result.getDevice().getName() + " " + result);
listAdapter.add(result.getDevice());
}
@Override
public void onBatchScanResults(List<ScanResult> results) {
for (ScanResult scanResult : results) {
listAdapter.add(scanResult.getDevice());
}
}
@Override
public void onScanFailed(int errorCode) {
super.onScanFailed(errorCode);
Log.e(TAG, "Bluetooth scan failed with errorCode " + errorCode);
Toast.makeText(getContext(), R.string.sensor_could_not_scan, Toast.LENGTH_LONG).show();
dismiss();
}
};
static BluetoothLePreferenceDialog newInstance(String key) {
final BluetoothLePreferenceDialog fragment = new BluetoothLePreferenceDialog();
final Bundle b = new Bundle(1);
b.putString(ARG_KEY, key);
fragment.setArguments(b);
return fragment;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
BluetoothAdapter bluetoothAdapter = BluetoothUtils.getDefaultBluetoothAdapter(TAG);
if (bluetoothAdapter == null || !bluetoothAdapter.isEnabled()) {
Log.w(TAG, "Bluetooth adapter is present or not enabled.");
Toast.makeText(getContext(), R.string.bluetooth_disabled, Toast.LENGTH_SHORT).show();
dismiss();
return;
}
if (bluetoothAdapter.isDiscovering()) {
Log.i(TAG, "Cancelling ongoing bluetooth discovery.");
bluetoothAdapter.cancelDiscovery();
}
scanner = bluetoothAdapter.getBluetoothLeScanner();
if (scanner == null) {
Log.e(TAG, "BluetoothLeScanner is null.");
dismiss();
return;
}
String deviceNone = getContext().getString(R.string.sensor_type_value_none);
listAdapter.add(getContext().getString(DEVICE_NONE_RESOURCEID), deviceNone);
selectedEntryIndex = 0;
BluetoothLePreference preference = (BluetoothLePreference) getPreference();
String deviceSelected = preference.getValue();
if (deviceSelected != null && !deviceNone.equals(deviceSelected)) {
listAdapter.add(preference.getValue(), preference.getValue());
selectedEntryIndex = 1;
}
ScanFilter.Builder scanFilterBuilder = new ScanFilter.Builder().setServiceUuid(new ParcelUuid(BluetoothUtils.HEART_RATE_SERVICE_UUID));
List<ScanFilter> scanFilter = new ArrayList<>();
scanFilter.add(scanFilterBuilder.build());
ScanSettings.Builder scanSettingsBuilder = new ScanSettings.Builder();
scanSettingsBuilder.setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY);
scanner.startScan(scanFilter, scanSettingsBuilder.build(), scanCallback);
}
//Behave like ListPreferenceDialogFragmentCompat, but uses a custom listAdapter.
@Override
protected void onPrepareDialogBuilder(AlertDialog.Builder builder) {
super.onPrepareDialogBuilder(builder);
builder.setSingleChoiceItems(listAdapter, selectedEntryIndex,
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
selectedEntryIndex = which;
BluetoothLePreferenceDialog.this.onClick(dialog, DialogInterface.BUTTON_POSITIVE);
dialog.dismiss();
}
});
builder.setIcon(android.R.drawable.stat_sys_data_bluetooth);
builder.setPositiveButton(null, null);
}
@Override
public void onDialogClosed(boolean positiveResult) {
if (scanner != null) {
scanner.stopScan(scanCallback);
}
if (positiveResult && selectedEntryIndex >= 0) {
String value = listAdapter.get(selectedEntryIndex).getAddress();
BluetoothLePreference preference = (BluetoothLePreference) getPreference();
if (preference.callChangeListener(value)) {
preference.setValue(value);
}
}
}
}
}
@@ -1,210 +1,145 @@
package de.dennisguse.opentracks.settings;
import android.bluetooth.BluetoothAdapter;
import android.content.Context;
import androidx.preference.ListPreference;
import java.util.ArrayList;
import java.util.List;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.util.BluetoothUtils;
import de.dennisguse.opentracks.util.PreferencesUtils;
import de.dennisguse.opentracks.util.UnitConversions;
final class PreferenceHelper {
/**
* Sets the min recording interval options.
*
* @param options the options
* @param values the values
*/
static void setMinRecordingIntervalOptions(Context context, String[] options, String[] values) {
for (int i = 0; i < values.length; i++) {
int value = Integer.parseInt(values[i]);
switch (value) {
case PreferencesUtils.MIN_RECORDING_INTERVAL_ADAPT_BATTERY_LIFE:
options[i] = context.getString(R.string.value_adapt_battery_life);
break;
case PreferencesUtils.MIN_RECORDING_INTERVAL_ADAPT_ACCURACY:
options[i] = context.getString(R.string.value_adapt_accuracy);
break;
case PreferencesUtils.MIN_RECORDING_INTERVAL_DEFAULT:
options[i] = context.getString(R.string.value_smallest_recommended);
break;
default:
options[i] = value < 60 ? context.getString(R.string.value_integer_second, value) : context.getString(R.string.value_integer_minute, value / 60);
static String[] getMinRecordingIntervalEntries(Context context) {
String[] entryValues = context.getResources().getStringArray(R.array.min_recording_interval_values);
String[] entries = new String[entryValues.length];
for (int i = 0; i < entryValues.length; i++) {
int value = Integer.parseInt(entryValues[i]);
if (value == PreferencesUtils.getMinRecordingIntervalAdaptAccuracy(context)) {
entries[i] = context.getString(R.string.value_adapt_accuracy);
} else if (value == PreferencesUtils.getMinRecordingIntervalAdaptBatteryLife(context)) {
entries[i] = context.getString(R.string.value_adapt_battery_life);
} else if (value == PreferencesUtils.getMinRecordingIntervalDefault(context)) {
entries[i] = context.getString(R.string.value_smallest_recommended);
} else {
entries[i] = value < 60 ? context.getString(R.string.value_integer_second, value) : context.getString(R.string.value_integer_minute, value / 60);
}
}
return entries;
}
/**
* Sets the recording distance interval options.
*
* @param options the options
* @param values the values
* @param metricUnits true for metric units
*/
static void setRecordingDistanceIntervalOptions(Context context, String[] options, String[] values, boolean metricUnits) {
for (int i = 0; i < values.length; i++) {
int value = Integer.parseInt(values[i]);
static String[] getRecordingDistanceIntervalEntries(Context context, boolean metricUnits) {
String[] entryValues = context.getResources().getStringArray(R.array.recording_distance_interval_values);
String[] entries = new String[entryValues.length];
final int recordingDistanceIntervalDefault = PreferencesUtils.getRecordingDistanceIntervalDefault(context);
for (int i = 0; i < entryValues.length; i++) {
int value = Integer.parseInt(entryValues[i]);
String displayValue;
if (metricUnits) {
displayValue = context.getString(R.string.value_integer_meter, value);
if (value == PreferencesUtils.RECORDING_DISTANCE_INTERVAL_DEFAULT) {
options[i] = context.getString(R.string.value_integer_meter_recommended, value);
if (value == recordingDistanceIntervalDefault) {
entries[i] = context.getString(R.string.value_integer_meter_recommended, value);
} else {
options[i] = displayValue;
entries[i] = displayValue;
}
} else {
int feet = (int) (value * UnitConversions.M_TO_FT);
displayValue = context.getString(R.string.value_integer_feet, feet);
if (value == PreferencesUtils.RECORDING_DISTANCE_INTERVAL_DEFAULT) {
options[i] = context.getString(R.string.value_integer_feet_recommended, feet);
if (value == recordingDistanceIntervalDefault) {
entries[i] = context.getString(R.string.value_integer_feet_recommended, feet);
} else {
options[i] = displayValue;
entries[i] = displayValue;
}
}
}
return entries;
}
/**
* Sets the max recording distance options.
*
* @param options the options
* @param values the values
* @param metricUnits true for metric units
*/
static void setMaxRecordingDistanceOptions(Context context, String[] options, String[] values, boolean metricUnits) {
for (int i = 0; i < values.length; i++) {
int value = Integer.parseInt(values[i]);
static String[] getMaxRecordingDistanceEntries(Context context, boolean metricUnits) {
String[] entryValues = context.getResources().getStringArray(R.array.max_recording_distance_values);
String[] entries = new String[entryValues.length];
final int maxRecordingDistanceDefault = Integer.parseInt(context.getResources().getString(R.string.max_recording_distance_default));
for (int i = 0; i < entryValues.length; i++) {
int value = Integer.parseInt(entryValues[i]);
String displayValue;
if (metricUnits) {
displayValue = context.getString(R.string.value_integer_meter, value);
if (value == PreferencesUtils.MAX_RECORDING_DISTANCE_DEFAULT) {
options[i] = context.getString(R.string.value_integer_meter_recommended, value);
if (value == maxRecordingDistanceDefault) {
entries[i] = context.getString(R.string.value_integer_meter_recommended, value);
} else {
options[i] = displayValue;
entries[i] = displayValue;
}
} else {
int feet = (int) (value * UnitConversions.M_TO_FT);
if (feet < 2000) {
displayValue = context.getString(R.string.value_integer_feet, feet);
if (value == PreferencesUtils.MAX_RECORDING_DISTANCE_DEFAULT) {
options[i] = context.getString(R.string.value_integer_feet_recommended, feet);
if (value == maxRecordingDistanceDefault) {
entries[i] = context.getString(R.string.value_integer_feet_recommended, feet);
} else {
options[i] = displayValue;
entries[i] = displayValue;
}
} else {
double mile = feet * UnitConversions.FT_TO_MI;
displayValue = context.getString(R.string.value_float_mile, mile);
options[i] = displayValue;
entries[i] = displayValue;
}
}
}
return entries;
}
/**
* Sets the recording gps accuracy options.
*
* @param options the options
* @param values the values
* @param metricUnits true for metric units
*/
static void setRecordingGpsAccuracyOptions(Context context, String[] options, String[] values, boolean metricUnits) {
for (int i = 0; i < values.length; i++) {
int value = Integer.parseInt(values[i]);
static String[] getRecordingGpsAccuracyEntries(Context context, boolean metricUnits) {
String[] entryValues = context.getResources().getStringArray(R.array.recording_gps_accuracy_values);
String[] entries = new String[entryValues.length];
final int recordingGPSAccuracyDefault = Integer.parseInt(context.getResources().getString(R.string.recording_gps_accuracy_default));
final int recordingGPSAccuracyExcellent = Integer.parseInt(context.getResources().getString(R.string.recording_gps_accuracy_excellent));
final int recordingGPSAccuracyPoor = Integer.parseInt(context.getResources().getString(R.string.recording_gps_accuracy_poor));
for (int i = 0; i < entryValues.length; i++) {
int value = Integer.parseInt(entryValues[i]);
String displayValue;
if (metricUnits) {
displayValue = context.getString(R.string.value_integer_meter, value);
switch (value) {
case PreferencesUtils.RECORDING_GPS_ACCURACY_DEFAULT:
options[i] = context.getString(R.string.value_integer_meter_recommended, value);
break;
case PreferencesUtils.RECORDING_GPS_ACCURACY_EXCELLENT:
options[i] = context.getString(R.string.value_integer_meter_excellent_gps, value);
break;
case PreferencesUtils.RECORDING_GPS_ACCURACY_POOR:
options[i] = context.getString(R.string.value_integer_meter_poor_gps, value);
break;
default:
options[i] = displayValue;
if (value == recordingGPSAccuracyDefault) {
entries[i] = context.getString(R.string.value_integer_meter_recommended, value);
} else if (value == recordingGPSAccuracyExcellent) {
entries[i] = context.getString(R.string.value_integer_meter_excellent_gps, value);
} else if (value == recordingGPSAccuracyPoor) {
entries[i] = context.getString(R.string.value_integer_meter_poor_gps, value);
} else {
entries[i] = displayValue;
}
} else {
int feet = (int) (value * UnitConversions.M_TO_FT);
if (feet < 2000) {
displayValue = context.getString(R.string.value_integer_feet, feet);
switch (value) {
case PreferencesUtils.RECORDING_GPS_ACCURACY_DEFAULT:
options[i] = context.getString(R.string.value_integer_feet_recommended, feet);
break;
case PreferencesUtils.RECORDING_GPS_ACCURACY_EXCELLENT:
options[i] = context.getString(R.string.value_integer_feet_excellent_gps, feet);
break;
default:
options[i] = displayValue;
if (value == recordingGPSAccuracyDefault) {
entries[i] = context.getString(R.string.value_integer_feet_recommended, feet);
} else if (value == recordingGPSAccuracyExcellent) {
entries[i] = context.getString(R.string.value_integer_feet_excellent_gps, feet);
} else {
entries[i] = displayValue;
}
} else {
double mile = feet * UnitConversions.FT_TO_MI;
displayValue = context.getString(R.string.value_float_mile, mile);
if (value == PreferencesUtils.RECORDING_GPS_ACCURACY_POOR) {
options[i] = context.getString(R.string.value_float_mile_poor_gps, mile);
if (value == recordingGPSAccuracyPoor) {
entries[i] = context.getString(R.string.value_float_mile_poor_gps, mile);
} else {
options[i] = displayValue;
entries[i] = displayValue;
}
}
}
}
}
/**
* Sets the auto resume track timeout options.
*
* @param options the options
* @param values the values
*/
static void setAutoResumeTrackTimeoutOptions(Context context, String[] options, String[] values) {
for (int i = 0; i < values.length; i++) {
int value = Integer.parseInt(values[i]);
if (value == Integer.parseInt(context.getResources().getString(R.string.auto_resume_track_timeout_never))) {
options[i] = context.getString(R.string.value_never);
} else if (value == Integer.parseInt(context.getResources().getString(R.string.auto_resume_track_timeout_always))) {
options[i] = context.getString(R.string.value_always);
} else {
options[i] = context.getString(R.string.value_integer_minute, value);
}
}
}
/**
* Configures the bluetooth sensor.
*/
static void configureBluetoothSensorList(ListPreference preference) {
Context context = preference.getContext();
String value = PreferencesUtils.getString(context, R.string.settings_sensor_bluetooth_sensor_key, PreferencesUtils.BLUETOOTH_SENSOR_DEFAULT);
List<String> devicesNameList = new ArrayList<>();
List<String> devicesAddressList = new ArrayList<>();
BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
if (bluetoothAdapter != null) {
BluetoothUtils.populateDeviceLists(bluetoothAdapter, devicesNameList, devicesAddressList);
}
// Was the previously configured device unpaired? Then forget it.
if (!devicesAddressList.contains(value)) {
value = PreferencesUtils.BLUETOOTH_SENSOR_DEFAULT;
PreferencesUtils.setString(context, R.string.settings_sensor_bluetooth_sensor_key, value);
}
devicesNameList.add(0, context.getString(R.string.value_none));
devicesAddressList.add(0, PreferencesUtils.BLUETOOTH_SENSOR_DEFAULT);
String[] values = devicesAddressList.toArray(new String[0]);
preference.setEntryValues(values);
String[] options = devicesNameList.toArray(new String[0]);
preference.setEntries(options);
return entries;
}
}
@@ -13,12 +13,15 @@ import androidx.preference.PreferenceFragmentCompat;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.fragments.ChooseActivityTypeDialogFragment;
import de.dennisguse.opentracks.util.BluetoothUtils;
import de.dennisguse.opentracks.util.HackUtils;
import de.dennisguse.opentracks.util.PreferencesUtils;
import de.dennisguse.opentracks.util.StringUtils;
public class SettingsActivity extends AppCompatActivity implements ChooseActivityTypeDialogFragment.ChooseActivityTypeCaller, ResetDialogPreference.ResetCallback {
private static final String TAG = SettingsActivity.class.getSimpleName();
private PrefsFragment prefsFragment;
@Override
@@ -54,13 +57,14 @@ public class SettingsActivity extends AppCompatActivity implements ChooseActivit
private final SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener = new SharedPreferences.OnSharedPreferenceChangeListener() {
@Override
public void onSharedPreferenceChanged(SharedPreferences preferences, String key) {
getActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
updateUI();
}
});
if (PreferencesUtils.isKey(getActivity(), R.string.recording_track_id_key, key)) {
getActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
updateReset();
}
});
}
if (PreferencesUtils.isKey(getActivity(), R.string.stats_units_key, key)) {
getActivity().runOnUiThread(new Runnable() {
@Override
@@ -86,14 +90,15 @@ public class SettingsActivity extends AppCompatActivity implements ChooseActivit
setPreferencesFromResource(R.xml.settings, rootKey);
}
updateUnits();
}
@Override
public void onResume() {
super.onResume();
PreferencesUtils.getSharedPreferences(getActivity()).registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
updateUI();
updateUnits();
updateReset();
updateBluetooth();
}
@Override
@@ -102,40 +107,6 @@ public class SettingsActivity extends AppCompatActivity implements ChooseActivit
PreferencesUtils.getSharedPreferences(getActivity()).unregisterOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
}
private void configFrequencyPreference(int key, boolean metricUnits) {
ListPreference preference = findPreference(getString(key));
String[] options = StringUtils.getFrequencyOptions(getActivity(), metricUnits);
preference.setEntries(options);
}
private void configListPreference(int key, int valueArray, boolean metricUnits) {
String[] values = getResources().getStringArray(valueArray);
final String[] options = new String[values.length];
switch (key) {
case R.string.min_recording_interval_key:
PreferenceHelper.setMinRecordingIntervalOptions(getActivity(), options, values);
break;
case R.string.recording_distance_interval_key:
PreferenceHelper.setRecordingDistanceIntervalOptions(getActivity(), options, values, metricUnits);
break;
case R.string.max_recording_distance_key:
PreferenceHelper.setMaxRecordingDistanceOptions(getActivity(), options, values, metricUnits);
break;
case R.string.recording_gps_accuracy_key:
PreferenceHelper.setRecordingGpsAccuracyOptions(getActivity(), options, values, metricUnits);
break;
case R.string.auto_resume_track_timeout_key:
PreferenceHelper.setAutoResumeTrackTimeoutOptions(getActivity(), options, values);
break;
default:
return;
}
final ListPreference listPreference = findPreference(getString(key));
listPreference.setEntries(options);
}
@Override
public void onDisplayPreferenceDialog(Preference preference) {
DialogFragment dialogFragment = null;
@@ -144,6 +115,8 @@ public class SettingsActivity extends AppCompatActivity implements ChooseActivit
} else if (preference instanceof ActivityTypePreference) {
activityPreferenceDialog = ActivityTypePreference.ActivityPreferenceDialog.newInstance(preference.getKey());
dialogFragment = activityPreferenceDialog;
} else if (preference instanceof BluetoothLePreference) {
dialogFragment = BluetoothLePreference.BluetoothLePreferenceDialog.newInstance(preference.getKey());
}
if (dialogFragment != null) {
@@ -161,35 +134,40 @@ public class SettingsActivity extends AppCompatActivity implements ChooseActivit
}
}
private void updateUI() {
private void updateReset() {
final boolean isRecording = PreferencesUtils.isRecording(getActivity());
Preference resetPreference = findPreference(getString(R.string.settings_reset_key));
resetPreference.setSummary(isRecording ? getString(R.string.settings_not_while_recording) : "");
resetPreference.setEnabled(!isRecording);
}
Preference speedCheckBoxPreference = findPreference(getString(R.string.chart_show_speed_key));
speedCheckBoxPreference.setTitle(PreferencesUtils.isReportSpeed(getActivity()) ? R.string.stats_speed : R.string.stats_pace);
ListPreference bluetoothPreference = findPreference(getString(R.string.settings_sensor_bluetooth_sensor_key));
PreferenceHelper.configureBluetoothSensorList(bluetoothPreference);
private void updateBluetooth() {
// Disable Bluetooth preference if device does not have Bluetooth
BluetoothLePreference bluetoothPreference = findPreference(getString(R.string.settings_sensor_bluetooth_heart_rate_key));
bluetoothPreference.setVisible(BluetoothUtils.hasBluetooth(TAG));
}
private void updateUnits() {
boolean metricUnits = PreferencesUtils.isMetricUnits(getActivity());
//TODO Refactor this!
configFrequencyPreference(R.string.voice_frequency_key, metricUnits);
configFrequencyPreference(R.string.split_frequency_key, metricUnits);
configListPreference(R.string.min_recording_interval_key, R.array.min_recording_interval_values, metricUnits);
configListPreference(R.string.recording_distance_interval_key, R.array.recording_distance_interval_values, metricUnits);
configListPreference(R.string.max_recording_distance_key, R.array.max_recording_distance_values, metricUnits);
configListPreference(R.string.recording_gps_accuracy_key, R.array.recording_gps_accuracy_values, metricUnits);
configListPreference(R.string.auto_resume_track_timeout_key, R.array.auto_resume_track_timeout_values, metricUnits);
configListPreference(R.string.auto_resume_track_timeout_key, R.array.auto_resume_track_timeout_values, metricUnits);
ListPreference voiceFrequency = findPreference(getString(R.string.voice_frequency_key));
voiceFrequency.setEntries(StringUtils.getFrequencyOptions(getActivity(), metricUnits));
final ListPreference statsRatePreferences = findPreference(getString(R.string.stats_rate_key));
String[] options = getResources().getStringArray(metricUnits ? R.array.stats_rate_metric_options : R.array.stats_rate_imperial_options);
statsRatePreferences.setEntries(options);
ListPreference minRecordingInterval = findPreference(getString(R.string.min_recording_interval_key));
minRecordingInterval.setEntries(PreferenceHelper.getMinRecordingIntervalEntries(getActivity()));
ListPreference recordingDistanceInterval = findPreference(getString(R.string.recording_distance_interval_key));
recordingDistanceInterval.setEntries(PreferenceHelper.getRecordingDistanceIntervalEntries(getActivity(), metricUnits));
ListPreference maxRecordingDistance = findPreference(getString(R.string.max_recording_distance_key));
maxRecordingDistance.setEntries(PreferenceHelper.getMaxRecordingDistanceEntries(getActivity(), metricUnits));
ListPreference recordingGpsAccuracy = findPreference(getString(R.string.recording_gps_accuracy_key));
recordingGpsAccuracy.setEntries(PreferenceHelper.getRecordingGpsAccuracyEntries(getActivity(), metricUnits));
ListPreference statsRatePreferences = findPreference(getString(R.string.stats_rate_key));
String[] entries = getResources().getStringArray(metricUnits ? R.array.stats_rate_metric_options : R.array.stats_rate_imperial_options);
statsRatePreferences.setEntries(entries);
HackUtils.invalidatePreference(statsRatePreferences);
}

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