Compare commits

..

84 Commits

Author SHA1 Message Date
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
Dennis Guse b34805272f Bugfix: not connecting to heart rate sensor - need to start BluetoothRemoteSensorManager.
Introduced in bc5ab0d7e5
2019-11-09 00:02:20 +01:00
Dennis Guse 98443dba99 Cleanup. 2019-11-08 22:45:34 +01:00
Dennis Guse dd6e733d8c Release v3.2.5 2019-11-08 22:31:49 +01:00
Dennis Guse 92d3439c3b Re-enable SummaryProvider for default_activity. 2019-11-08 07:58:30 +01:00
Dennis Guse bd155cd600 Use SummaryProvider.
Fixed #46.
2019-11-08 07:36:45 +01:00
Dennis Guse e14e53c0ae Fix hack to update summary of statsRate if statsUnit change. 2019-11-07 23:32:28 +01:00
Dennis Guse 77a6a63174 Re-initialize PreferenceFragment, when sharedpreferences were reset. 2019-11-07 22:24:15 +01:00
Dennis Guse 40f2463f09 Fail safety: if settings cannot loaded (invalid data in sharedpreferences), reset it and try again. 2019-11-07 19:15:17 +01:00
Dennis Guse e5d1048c8d Bugfix reset settings: really clear sharedPreferences before settings default values. 2019-11-07 19:15:17 +01:00
Dennis Guse bf3b529fb2 Add link to Transifex for localization.
Fixes #27.
2019-11-07 19:15:17 +01:00
Dennis Guse d0fde46027 Android does not allow referencing integer in string-arrays: store everything as string and parse it later (also for tests). 2019-11-06 18:40:58 +01:00
Dennis Guse 0f348dff10 Android does not allow referencing integer in string-arrays: store everything as string and parse it later. 2019-11-06 18:16:40 +01:00
Dennis Guse d3f5080e73 Fixed issues introduced in 353c958ae0 and 2a60aa8f59. 2019-11-06 07:42:15 +01:00
Dennis Guse a453c8ec1e TrackDetailActivity: disable activity type changer in view mode.
Fix #3.
2019-11-05 22:45:29 +01:00
Dennis Guse 2a60aa8f59 Settings: removed some default definitions from code into xml. 2019-11-05 22:02:23 +01:00
Dennis Guse 41e7870509 Settings: removed some default definitions from code into xml. 2019-11-05 21:13:51 +01:00
Dennis Guse 57f66b77ab Reorganized settings (sharedpreferences and keys); xml only. 2019-11-05 21:13:51 +01:00
Dennis Guse 53572406e9 Mark foreground service to use location and connection to other devices, so it can run properly on Android 10+ when app is in the background.
Might fix #61.
2019-11-05 21:13:51 +01:00
Dennis Guse a288255ab1 Added issue and feature templates. 2019-11-05 19:33:57 +01:00
Dennis Guse 353c958ae0 Moved accessing sharedPreferences into PreferencesUtils. 2019-11-03 01:44:46 +01:00
Dennis Guse bc5ab0d7e5 Removed unnecessary RemoteSensorManagerFactory.
TrackRecordingService is only using BluetoothRemoteSensorManager and thus factory pattern is not required.
2019-11-03 00:21:51 +01:00
Dennis Guse 9d915b5640 Cleanup. 2019-11-03 00:13:14 +01:00
Dennis Guse 3aaee270eb Release v3.2.4 2019-11-01 23:54:34 +01:00
Dennis Guse d53a7156e2 Bugfix: removed custom dialogs to set voice/split frequency preferences from the context menu directly.
Fixed #49.
2019-11-01 23:38:07 +01:00
Dennis Guse 3af24ea3d0 Release v3.2.3 2019-10-31 22:07:21 +01:00
Dennis Guse 5483212b06 Fixed StringUtils.formatDecimal to work with decimals of 0. 2019-10-30 23:02:02 +01:00
Dennis Guse 419279fe5e Cleanup. 2019-10-30 22:36:22 +01:00
Dennis Guse 501e899b27 StatsFragment: need to reconnect to TrackRecordingService in onResume as it is unbound in onStop.
Fixed #50.
2019-10-30 22:36:16 +01:00
Dennis Guse 3bb9edb009 Connect to new sensor if settings are changed.
Fixed #52.
2019-10-30 07:48:29 +01:00
Dennis Guse e52b1188a3 Cleanup. 2019-10-29 23:05:17 +01:00
Dennis Guse 9e877e4762 Removed stale verify. 2019-10-29 22:39:51 +01:00
Dennis Guse 5711757110 If sharing multiple tracks, share them as separate files. 2019-10-29 17:43:23 +01:00
Dennis Guse e1c33dad43 Move to Android ShareSheet. 2019-10-29 17:34:21 +01:00
Dennis Guse 4b2400774c TravisCI: start emulator, which is required for testing. 2019-10-28 23:25:07 +01:00
Dennis Guse afd4a59622 CI: enabled tests.
Fixed #6.
2019-10-27 16:50:28 +01:00
Dennis Guse bdce437470 Removed AnnoucementPeriodTaskTest as these were broken beyond repair. 2019-10-27 16:46:56 +01:00
Dennis Guse 33479cda11 Fixed TrackRecordingService tests: start service properly if needed. 2019-10-27 15:58:50 +01:00
Dennis Guse 385cee3dc7 Fixed Date parsing test (account for
rounding up microseconds).
2019-10-27 13:45:43 +01:00
Dennis Guse d0bbd7655b Ignore KML waypoints that do not have a type. 2019-10-27 13:40:06 +01:00
Dennis Guse a83b8f62fe Repaired Track importer tests. 2019-10-27 13:24:20 +01:00
Dennis Guse 0cd9176703 Heart rate must be larger than zero to be shown while tracking. 2019-10-26 14:35:03 +02:00
Dennis Guse d8e526923d Removed RemoteSensorManager as abstraction was not used anymore. 2019-10-26 14:26:16 +02:00
Dennis Guse 3a6f417662 Added disconnect toast message. 2019-10-26 14:23:38 +02:00
Dennis Guse 7d807a7f11 Refactor Bluetooth LE stack to use autoReconnect.
Removed additional timerTask to reconnect (was not working anymore).

Fixed #41.
2019-10-26 14:11:55 +02:00
Dennis Guse e9d5718495 Cleanup. 2019-10-23 18:47:32 +02:00
Dennis Guse 0dd7f74a11 Removed temporary sensor manager (was not used anymore). 2019-10-22 21:15:11 +02:00
119 changed files with 3663 additions and 3610 deletions
+26
View File
@@ -0,0 +1,26 @@
---
name: Bug report
about: Report issues of OpenTracks features
title: ''
labels: potential bug
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
If applicable:
* add screenshots
* add logcat output
**Technical information**
- Device: [e.g. Nexus 7 (2013)]
- OS: [e.g. LineageOS 15.1]
- Version [e.g. v3.2.4]
+20
View File
@@ -0,0 +1,20 @@
---
name: Feature request
about: Suggest an idea for OpenTracks
title: ''
labels: enhancement
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.
+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
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>
+38 -5
View File
@@ -1,12 +1,45 @@
#See https://docs.travis-ci.com/user/languages/android/
#See https://github.com/ankidroid/Anki-Android/blob/master/.travis.yml
language: android language: android
dist: trusty
env:
global:
- ADB_INSTALL_TIMEOUT=8
- ABI=armeabi-v7a
- EMU_FLAVOR=default
# PATH order is incredibly important. e.g. the 'emulator' script exists in more than one place!
- ANDROID_HOME=/usr/local/android-sdk
- TOOLS=${ANDROID_HOME}/tools
- PATH=${ANDROID_HOME}:${ANDROID_HOME}/emulator:${TOOLS}:${TOOLS}/bin:${ANDROID_HOME}/platform-tools:${PATH}
matrix:
- API=27 ABI=x86_64
android: android:
components: components:
# installing tools to start, then use `sdkmanager` below to get the rest
- tools
- build-tools-28.0.3 - build-tools-28.0.3
- android-29 - android-28
# Emulator Management: Create, Start and Wait
install:
- echo y | sdkmanager "platform-tools"
- echo y | sdkmanager --channel=4 "emulator"
- echo y | sdkmanager "system-images;android-$API;$EMU_FLAVOR;$ABI"
- echo no | avdmanager create avd --force -n test -k "system-images;android-$API;$EMU_FLAVOR;$ABI" -c 10M
- emulator -verbose -avd test -no-accel -no-snapshot -no-window -camera-back none -camera-front none -selinux permissive -qemu -m 2048 &
- android-wait-for-emulator
- adb shell input keyevent 82 &
script: script:
- ./gradlew build - ./gradlew clean build
#TODO: When all tests are passing, CI should also execute them. - ./gradlew connectedCheck
#- ./gradlew connectedCheck
sbefore_cache:
- rm -f $HOME/.gradle/caches/modules-2/modules-2.lock
- rm -fr $HOME/.gradle/caches/*/plugin-resolution/
cache:
directories:
- $HOME/.gradle/caches/
- $HOME/.gradle/wrapper/
- $HOME/.android/build-cache
+7 -3
View File
@@ -7,6 +7,10 @@
3. Manually update _version code_ and _version name_ in `AndroidManifest.xml` (used by FDroid) 3. Manually update _version code_ and _version name_ in `AndroidManifest.xml` (used by FDroid)
4. Create changelog (`_version code_.txt`) 4. Create changelog (`_version code_.txt`)
5. Create commit with all changes 5. Create commit with all changes
6. Push commits 6. Tag newly create commit with _version name_ (e.g., v3.2.1)
7. Tag newly create commit with _version name_ (e.g., v3.2.1) 7. Push commits and tags to public repository
8. Push the tagged commit to public repository 8. Add changelog to tag (Github releases)
## Translations (localization)
Translations of the OpenTracks are handled in Transifex: [https://www.transifex.com/opentracks](https://www.transifex.com/opentracks/).
+8
View File
@@ -30,6 +30,14 @@ To show your tracks on a map, a 3rd party application (e.g., [OsmAnd](https://pl
<img alt="Donate using Liberapay" src="https://liberapay.com/assets/widgets/donate.svg" height="45"> <img alt="Donate using Liberapay" src="https://liberapay.com/assets/widgets/donate.svg" height="45">
</a> </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 ## Project history
_OpenTracks_ is based upon Google _My Tracks app_ ([code](https://code.google.com/archive/p/mytracks/)). _OpenTracks_ is based upon Google _My Tracks app_ ([code](https://code.google.com/archive/p/mytracks/)).
+7 -7
View File
@@ -6,7 +6,7 @@ buildscript {
jcenter() jcenter()
} }
dependencies { 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 { try {
def stdout = new ByteArrayOutputStream() def stdout = new ByteArrayOutputStream()
exec { exec {
commandLine 'git', 'describe', '--tags', '--dirty' commandLine 'git', 'describe', '--tags'
standardOutput = stdout 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) { catch (ignored) {
return null return null
@@ -75,10 +79,6 @@ android {
applicationVariants.all { variant -> applicationVariants.all { variant ->
variant.resValue "string", "applicationId", variant.applicationId variant.resValue "string", "applicationId", variant.applicationId
} }
lintOptions {
disable "AppCompatResource" // TODO Disable complaint to use androidx's SearchView as it is null.
}
} }
dependencies { dependencies {
@@ -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. Erfasst deine Kilometer, ohne dass es jemand anderes tut.
Funktionen & Features: 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,14 @@
v3.2.3: OpenTracks
Changes:
- Sharing: use Android Sharesheet
- Sharing: share multiple tracks as individual files
- Use Android ShareSheet
- Heart rate sensor (Bluetooth LE): automatic reconnect
Bugfix:
- Heart rate sensor (Bluetooth LE): disconnect properly
- Heart rate wasn't always shown while recording
Developer:
- Enabled TravisCI instrumentation tests
@@ -0,0 +1,5 @@
v3.2.4: OpenTracks
Bugfix:
- Settings activity crashes
Removed possibility to change voice/split frequency from the context menu
@@ -0,0 +1,15 @@
v3.2.5: OpenTracks
Changes:
- Disable activity change
- Add permission for Android 10: recording location data
Bugfix:
- Fix reset settings
Developer:
- Preferences
- if Settings cannot be loaded reset all preferences
- moved access to preferences to PreferencesUtil
- moved default of preferences from code to xml
- use SummaryProvider
@@ -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,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.
@@ -13,7 +13,7 @@
* License for the specific language governing permissions and limitations under * License for the specific language governing permissions and limitations under
* the License. * the License.
*/ */
package de.dennisguse.opentracks; package de.dennisguse.opentracks.chart;
import androidx.test.core.app.ApplicationProvider; import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -23,6 +23,8 @@ import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import de.dennisguse.opentracks.R;
/** /**
* Tests {@link ChartValueSeries}. * Tests {@link ChartValueSeries}.
* *
@@ -35,22 +35,19 @@ import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.concurrent.atomic.AtomicInteger; 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.content.Waypoint.WaypointType;
import de.dennisguse.opentracks.stats.TripStatistics; import de.dennisguse.opentracks.stats.TripStatistics;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
/** /**
* A unit test for {@link ContentProviderUtilsImpl}. * A unit test for {@link ContentProviderUtils}.
* *
* @author Bartlomiej Niechwiej * @author Bartlomiej Niechwiej
* @author Youtao Liu * @author Youtao Liu
*/ */
@RunWith(MockitoJUnitRunner.class) @RunWith(MockitoJUnitRunner.class)
public class CustomContentProviderUtilsImplTest { public class CustomContentProviderUtilsTest {
private static final String NAME_PREFIX = "test name"; private static final String NAME_PREFIX = "test name";
private static final String MOCK_DESC = "Mock Next Waypoint Desc!"; private static final String MOCK_DESC = "Mock Next Waypoint Desc!";
private static final String TEST_DESC = "Test Desc!"; private static final String TEST_DESC = "Test Desc!";
@@ -70,13 +67,13 @@ public class CustomContentProviderUtilsImplTest {
@Before @Before
public void setUp() { public void setUp() {
providerUtils = ContentProviderUtils.Factory.get(context); providerUtils = new ContentProviderUtils(context);
providerUtils.deleteAllTracks(context); providerUtils.deleteAllTracks(context);
} }
@Test @Test
public void testLocationIterator_noPoints() { public void testLocationIterator_noPoints() {
testIterator(1, 0, 1, false, ContentProviderUtils.DEFAULT_LOCATION_FACTORY); testIterator(1, 0, 1, false, LocationFactory.DEFAULT_LOCATION_FACTORY);
} }
@Test @Test
@@ -106,36 +103,36 @@ public class CustomContentProviderUtilsImplTest {
@Test @Test
public void testLocationIterator_noBatchAscending() { public void testLocationIterator_noBatchAscending() {
testIterator(1, 50, 100, false, ContentProviderUtils.DEFAULT_LOCATION_FACTORY); testIterator(1, 50, 100, false, LocationFactory.DEFAULT_LOCATION_FACTORY);
testIterator(2, 50, 50, false, ContentProviderUtils.DEFAULT_LOCATION_FACTORY); testIterator(2, 50, 50, false, LocationFactory.DEFAULT_LOCATION_FACTORY);
} }
@Test @Test
public void testLocationIterator_noBatchDescending() { public void testLocationIterator_noBatchDescending() {
testIterator(1, 50, 100, true, ContentProviderUtils.DEFAULT_LOCATION_FACTORY); testIterator(1, 50, 100, true, LocationFactory.DEFAULT_LOCATION_FACTORY);
testIterator(2, 50, 50, true, ContentProviderUtils.DEFAULT_LOCATION_FACTORY); testIterator(2, 50, 50, true, LocationFactory.DEFAULT_LOCATION_FACTORY);
} }
@Test @Test
public void testLocationIterator_batchAscending() { public void testLocationIterator_batchAscending() {
testIterator(1, 50, 11, false, ContentProviderUtils.DEFAULT_LOCATION_FACTORY); testIterator(1, 50, 11, false, LocationFactory.DEFAULT_LOCATION_FACTORY);
testIterator(2, 50, 25, false, ContentProviderUtils.DEFAULT_LOCATION_FACTORY); testIterator(2, 50, 25, false, LocationFactory.DEFAULT_LOCATION_FACTORY);
} }
@Test @Test
public void testLocationIterator_batchDescending() { public void testLocationIterator_batchDescending() {
testIterator(1, 50, 11, true, ContentProviderUtils.DEFAULT_LOCATION_FACTORY); testIterator(1, 50, 11, true, LocationFactory.DEFAULT_LOCATION_FACTORY);
testIterator(2, 50, 25, true, ContentProviderUtils.DEFAULT_LOCATION_FACTORY); testIterator(2, 50, 25, true, LocationFactory.DEFAULT_LOCATION_FACTORY);
} }
@Test @Test
public void testLocationIterator_largeTrack() { 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) { private List<Location> testIterator(long trackId, int numPoints, int batchSize, boolean descending, LocationFactory locationFactory) {
long lastPointId = initializeTrack(trackId, numPoints); long lastPointId = initializeTrack(trackId, numPoints);
((ContentProviderUtilsImpl) providerUtils).setDefaultCursorBatchSize(batchSize); ((ContentProviderUtils) providerUtils).setDefaultCursorBatchSize(batchSize);
List<Location> locations = new ArrayList<Location>(numPoints); List<Location> locations = new ArrayList<Location>(numPoints);
try (LocationIterator it = providerUtils.getTrackPointLocationIterator(trackId, -1L, descending, locationFactory)) { try (LocationIterator it = providerUtils.getTrackPointLocationIterator(trackId, -1L, descending, locationFactory)) {
while (it.hasNext()) { while (it.hasNext()) {
@@ -174,7 +171,7 @@ public class CustomContentProviderUtilsImplTest {
// Load all inserted locations. // Load all inserted locations.
long lastPointId = -1; long lastPointId = -1;
int counter = 0; 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()) { while (it.hasNext()) {
it.next(); it.next();
lastPointId = it.getLocationId(); lastPointId = it.getLocationId();
@@ -190,7 +187,7 @@ public class CustomContentProviderUtilsImplTest {
} }
/** /**
* Tests the method {@link ContentProviderUtilsImpl#createTrack(Cursor)}. * Tests the method {@link ContentProviderUtils#createTrack(Cursor)}.
*/ */
@Test @Test
public void testCreateTrack() { public void testCreateTrack() {
@@ -214,11 +211,10 @@ public class CustomContentProviderUtilsImplTest {
Track track = providerUtils.createTrack(cursorMock); Track track = providerUtils.createTrack(cursorMock);
Assert.assertEquals(trackId, track.getId()); Assert.assertEquals(trackId, track.getId());
Assert.assertEquals(name, track.getName()); Assert.assertEquals(name, track.getName());
verify(cursorMock);
} }
/** /**
* Tests the method {@link ContentProviderUtilsImpl#deleteAllTracks(Context)} * Tests the method {@link ContentProviderUtils#deleteAllTracks(Context)}
*/ */
@Test @Test
public void testDeleteAllTracks() { public void testDeleteAllTracks() {
@@ -247,7 +243,7 @@ public class CustomContentProviderUtilsImplTest {
} }
/** /**
* Tests the method {@link ContentProviderUtilsImpl#deleteTrack(Context, long)}. * Tests the method {@link ContentProviderUtils#deleteTrack(Context, long)}.
*/ */
@Test @Test
public void testDeleteTrack() { public void testDeleteTrack() {
@@ -282,7 +278,7 @@ public class CustomContentProviderUtilsImplTest {
} }
/** /**
* Tests the method {@link ContentProviderUtilsImpl#getAllTracks()} * Tests the method {@link ContentProviderUtils#getAllTracks()}
*/ */
@Test @Test
public void testGetAllTracks() { public void testGetAllTracks() {
@@ -295,7 +291,7 @@ public class CustomContentProviderUtilsImplTest {
} }
/** /**
* Tests the method {@link ContentProviderUtilsImpl#getLastTrack()} * Tests the method {@link ContentProviderUtils#getLastTrack()}
*/ */
@Test @Test
public void testGetLastTrack() { public void testGetLastTrack() {
@@ -305,7 +301,7 @@ public class CustomContentProviderUtilsImplTest {
} }
/** /**
* Tests the method {@link ContentProviderUtilsImpl#getTrack(long)} * Tests the method {@link ContentProviderUtils#getTrack(long)}
*/ */
@Test @Test
public void testGetTrack() { public void testGetTrack() {
@@ -315,7 +311,7 @@ public class CustomContentProviderUtilsImplTest {
} }
/** /**
* Tests the method {@link ContentProviderUtilsImpl#updateTrack(Track)} * Tests the method {@link ContentProviderUtils#updateTrack(Track)}
*/ */
@Test @Test
public void testUpdateTrack() { public void testUpdateTrack() {
@@ -332,7 +328,7 @@ public class CustomContentProviderUtilsImplTest {
} }
/** /**
* Tests the method {@link ContentProviderUtilsImpl#createContentValues(Waypoint)}. * Tests the method {@link ContentProviderUtils#createContentValues(Waypoint)}.
*/ */
@Test @Test
public void testCreateContentValues_waypoint() { public void testCreateContentValues_waypoint() {
@@ -370,7 +366,7 @@ public class CustomContentProviderUtilsImplTest {
waypoint.setLocation(location); waypoint.setLocation(location);
providerUtils.insertWaypoint(waypoint); providerUtils.insertWaypoint(waypoint);
ContentProviderUtilsImpl contentProviderUtils = new ContentProviderUtilsImpl(contentResolverMock); ContentProviderUtils contentProviderUtils = new ContentProviderUtils(contentResolverMock);
long waypointId = System.currentTimeMillis(); long waypointId = System.currentTimeMillis();
waypoint.setId(waypointId); waypoint.setId(waypointId);
@@ -383,7 +379,7 @@ public class CustomContentProviderUtilsImplTest {
} }
/** /**
* Tests the method {@link ContentProviderUtilsImpl#createWaypoint(Cursor)}. * Tests the method {@link ContentProviderUtils#createWaypoint(Cursor)}.
*/ */
@Test @Test
public void testCreateWaypoint() { public void testCreateWaypoint() {
@@ -414,12 +410,11 @@ public class CustomContentProviderUtilsImplTest {
Assert.assertEquals(id, waypoint.getId()); Assert.assertEquals(id, waypoint.getId());
Assert.assertEquals(name, waypoint.getName()); Assert.assertEquals(name, waypoint.getName());
Assert.assertEquals(trackId, waypoint.getTrackId()); Assert.assertEquals(trackId, waypoint.getTrackId());
verify(cursorMock);
} }
/** /**
* Tests the method * Tests the method
* {@link ContentProviderUtilsImpl#deleteWaypoint(Context, long, DescriptionGenerator)} * {@link ContentProviderUtils#deleteWaypoint(Context, long, DescriptionGenerator)}
* when there is only one waypoint in the track. * when there is only one waypoint in the track.
*/ */
@Test @Test
@@ -440,7 +435,7 @@ public class CustomContentProviderUtilsImplTest {
@Override @Override
public String generateWaypointDescription(TripStatistics tripStatistics) { public String generateWaypointDescription(TripStatistics tripStatistics) {
return CustomContentProviderUtilsImplTest.MOCK_DESC; return CustomContentProviderUtilsTest.MOCK_DESC;
} }
@Override @Override
@@ -455,7 +450,7 @@ public class CustomContentProviderUtilsImplTest {
/** /**
* Tests the method * 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. * when there is more than one waypoint in the track.
*/ */
@Test @Test
@@ -498,7 +493,7 @@ public class CustomContentProviderUtilsImplTest {
DescriptionGenerator descriptionGenerator = new DescriptionGenerator() { DescriptionGenerator descriptionGenerator = new DescriptionGenerator() {
@Override @Override
public String generateWaypointDescription(TripStatistics tripStatistics) { public String generateWaypointDescription(TripStatistics tripStatistics) {
return CustomContentProviderUtilsImplTest.MOCK_DESC; return CustomContentProviderUtilsTest.MOCK_DESC;
} }
@Override @Override
@@ -511,12 +506,12 @@ public class CustomContentProviderUtilsImplTest {
providerUtils.deleteWaypoint(context, waypoint1Id, descriptionGenerator); providerUtils.deleteWaypoint(context, waypoint1Id, descriptionGenerator);
Assert.assertNull(providerUtils.getWaypoint(waypoint1Id)); Assert.assertNull(providerUtils.getWaypoint(waypoint1Id));
Assert.assertEquals(CustomContentProviderUtilsImplTest.MOCK_DESC, providerUtils.getWaypoint(waypoint2Id) Assert.assertEquals(CustomContentProviderUtilsTest.MOCK_DESC, providerUtils.getWaypoint(waypoint2Id)
.getDescription()); .getDescription());
} }
/** /**
* Tests the method {@link ContentProviderUtilsImpl#getFirstWaypointId(long)}. * Tests the method {@link ContentProviderUtils#getFirstWaypointId(long)}.
*/ */
@Test @Test
public void testGetFirstWaypointId() { public void testGetFirstWaypointId() {
@@ -538,7 +533,7 @@ public class CustomContentProviderUtilsImplTest {
} }
/** /**
* Tests the method {@link ContentProviderUtilsImpl#getNextWaypointNumber(long, WaypointType)}. * Tests the method {@link ContentProviderUtils#getNextWaypointNumber(long, WaypointType)}.
*/ */
@Test @Test
public void testGetNextWaypointNumber() { public void testGetNextWaypointNumber() {
@@ -597,8 +592,8 @@ public class CustomContentProviderUtilsImplTest {
} }
/** /**
* Tests the method {@link ContentProviderUtilsImpl#insertWaypoint(Waypoint)} and * Tests the method {@link ContentProviderUtils#insertWaypoint(Waypoint)} and
* {@link ContentProviderUtilsImpl#getWaypoint(long)}. * {@link ContentProviderUtils#getWaypoint(long)}.
*/ */
@Test @Test
public void testInsertAndGetWaypoint() { public void testInsertAndGetWaypoint() {
@@ -615,7 +610,7 @@ public class CustomContentProviderUtilsImplTest {
} }
/** /**
* Tests the method {@link ContentProviderUtilsImpl#updateWaypoint(Waypoint)}. * Tests the method {@link ContentProviderUtils#updateWaypoint(Waypoint)}.
*/ */
@Test @Test
public void testUpdateWaypoint() { public void testUpdateWaypoint() {
@@ -637,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 @Test
public void testBulkInsertTrackPoint() { public void testBulkInsertTrackPoint() {
@@ -653,7 +648,7 @@ public class CustomContentProviderUtilsImplTest {
} }
/** /**
* Tests the method {@link ContentProviderUtilsImpl#createTrackPoint(Cursor)}. * Tests the method {@link ContentProviderUtils#createTrackPoint(Cursor)}.
*/ */
@Test @Test
public void testCreateTrackPoint() { public void testCreateTrackPoint() {
@@ -709,12 +704,11 @@ public class CustomContentProviderUtilsImplTest {
Assert.assertEquals((double) latitude, location.getLatitude(), 0.01); Assert.assertEquals((double) latitude, location.getLatitude(), 0.01);
Assert.assertEquals(time, location.getTime(), 0.01); Assert.assertEquals(time, location.getTime(), 0.01);
Assert.assertEquals(speed, location.getSpeed(), 0.01); Assert.assertEquals(speed, location.getSpeed(), 0.01);
verify(cursorMock);
} }
/** /**
* Tests the method * Tests the method
* {@link ContentProviderUtilsImpl#insertTrackPoint(Location, long)}. * {@link ContentProviderUtils#insertTrackPoint(Location, long)}.
*/ */
@Test @Test
public void testInsertTrackPoint() { public void testInsertTrackPoint() {
@@ -728,7 +722,7 @@ public class CustomContentProviderUtilsImplTest {
} }
/** /**
* Tests the method {@link ContentProviderUtilsImpl#getFirstTrackPointId(long)}. * Tests the method {@link ContentProviderUtils#getFirstTrackPointId(long)}.
*/ */
@Test @Test
public void testGetFirstTrackPointId() { public void testGetFirstTrackPointId() {
@@ -741,7 +735,7 @@ public class CustomContentProviderUtilsImplTest {
} }
/** /**
* Tests the method {@link ContentProviderUtilsImpl#getLastTrackPointId(long)}. * Tests the method {@link ContentProviderUtils#getLastTrackPointId(long)}.
*/ */
@Test @Test
public void testGetLastTrackPointId() { public void testGetLastTrackPointId() {
@@ -754,7 +748,7 @@ public class CustomContentProviderUtilsImplTest {
} }
/** /**
* Tests the method {@link ContentProviderUtilsImpl#getLastValidTrackPoint(long)}. * Tests the method {@link ContentProviderUtils#getLastValidTrackPoint(long)}.
*/ */
@Test @Test
public void testGetLastValidTrackPoint() { public void testGetLastValidTrackPoint() {
@@ -768,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 @Test
public void testGetTrackPointCursor_desc() { public void testGetTrackPointCursor_desc() {
@@ -787,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 @Test
public void testGetTrackPointCursor_asc() { public void testGetTrackPointCursor_asc() {
@@ -806,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 @Test
public void testGetTrackPointLocationIterator_desc() { public void testGetTrackPointLocationIterator_desc() {
@@ -822,7 +816,7 @@ public class CustomContentProviderUtilsImplTest {
long startTrackPointId = trackpointIds[9]; 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++) { for (int i = 0; i < trackpointIds.length; i++) {
Assert.assertTrue(locationIterator.hasNext()); Assert.assertTrue(locationIterator.hasNext());
Location location = locationIterator.next(); Location location = locationIterator.next();
@@ -833,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 @Test
public void testGetTrackPointLocationIterator_asc() { public void testGetTrackPointLocationIterator_asc() {
@@ -849,7 +843,7 @@ public class CustomContentProviderUtilsImplTest {
long startTrackPointId = trackpointIds[0]; 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++) { for (int i = 0; i < trackpointIds.length; i++) {
Assert.assertTrue(locationIterator.hasNext()); Assert.assertTrue(locationIterator.hasNext());
Location location = locationIterator.next(); Location location = locationIterator.next();
@@ -896,7 +890,7 @@ public class CustomContentProviderUtilsImplTest {
/** /**
* Checks the value of a location. * Checks the value of a location.
* *
* @param i the index of this location which created in the method {@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 * @param location the location to be checked
*/ */
private void checkLocation(int i, Location location) { private void checkLocation(int i, Location location) {
@@ -58,7 +58,7 @@ public class SearchEngineTest {
@Before @Before
public void setUp() { public void setUp() {
providerUtils = ContentProviderUtils.Factory.get(context); providerUtils = new ContentProviderUtils(context);
engine = new SearchEngine(providerUtils); engine = new SearchEngine(providerUtils);
} }
@@ -17,17 +17,20 @@
package de.dennisguse.opentracks.fragments; package de.dennisguse.opentracks.fragments;
import android.location.Location; import android.location.Location;
import android.os.Looper;
import androidx.test.core.app.ApplicationProvider; import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.AfterClass;
import org.junit.Assert; import org.junit.Assert;
import org.junit.Before; import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import de.dennisguse.opentracks.ChartView;
import de.dennisguse.opentracks.TrackStubUtils; import de.dennisguse.opentracks.TrackStubUtils;
import de.dennisguse.opentracks.chart.ChartView;
import de.dennisguse.opentracks.content.SensorDataSetLocation; import de.dennisguse.opentracks.content.SensorDataSetLocation;
import de.dennisguse.opentracks.content.sensor.SensorDataSet; import de.dennisguse.opentracks.content.sensor.SensorDataSet;
import de.dennisguse.opentracks.util.UnitConversions; import de.dennisguse.opentracks.util.UnitConversions;
@@ -44,10 +47,23 @@ public class ChartFragmentTest {
private ChartFragment chartFragment; 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 @Before
public void setUp() { public void setUp() {
chartFragment = new ChartFragment(); boolean chartByDistance = false;
chartFragment.setChartView(new ChartView(ApplicationProvider.getApplicationContext())); chartFragment = new ChartFragment(chartByDistance) {
};
chartFragment.setChartView(new ChartView(ApplicationProvider.getApplicationContext(), chartByDistance));
chartFragment.setTripStatisticsUpdater(TrackStubUtils.INITIAL_TIME); chartFragment.setTripStatisticsUpdater(TrackStubUtils.INITIAL_TIME);
} }
@@ -112,7 +112,7 @@ public abstract class AbstractTestFileTrackImporter {
* @param lastTrack true if it is the last track in the gpx * @param lastTrack true if it is the last track in the gpx
* @param trackId the track id * @param trackId the track id
*/ */
protected void expectUpdateTrack(ArgumentCaptor<Track> trackCaptor, boolean lastTrack, long trackId) { protected void expectTrackUpdate(ArgumentCaptor<Track> trackCaptor, boolean lastTrack, long trackId) {
contentProviderUtils.updateTrack(trackCaptor.capture()); contentProviderUtils.updateTrack(trackCaptor.capture());
when(contentProviderUtils.insertWaypoint((Waypoint) any())).thenReturn(WAYPOINT_ID_O_URI); when(contentProviderUtils.insertWaypoint((Waypoint) any())).thenReturn(WAYPOINT_ID_O_URI);
@@ -27,13 +27,13 @@ import org.mockito.junit.MockitoJUnitRunner;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.InputStream; import java.io.InputStream;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.Track; import de.dennisguse.opentracks.content.Track;
import de.dennisguse.opentracks.util.PreferencesUtils; import de.dennisguse.opentracks.util.PreferencesUtils;
import static org.mockito.Mockito.any; import static org.mockito.Mockito.any;
import static org.mockito.Mockito.anyInt; import static org.mockito.Mockito.anyInt;
import static org.mockito.Mockito.anyLong; import static org.mockito.Mockito.anyLong;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.eq; import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
@@ -84,8 +84,7 @@ public class GpxFileTrackImporterTest extends AbstractTestFileTrackImporter {
@Test @Test
public void testOneTrackOneSegment() throws Exception { public void testOneTrackOneSegment() throws Exception {
ArgumentCaptor<Track> trackCaptor = ArgumentCaptor.forClass(Track.class); // given
Location location0 = createLocation(0, DATE_FORMAT_0.parse(TRACK_TIME_0).getTime()); Location location0 = createLocation(0, DATE_FORMAT_0.parse(TRACK_TIME_0).getTime());
Location location1 = createLocation(1, DATE_FORMAT_1.parse(TRACK_TIME_1).getTime()); Location location1 = createLocation(1, DATE_FORMAT_1.parse(TRACK_TIME_1).getTime());
@@ -95,25 +94,28 @@ public class GpxFileTrackImporterTest extends AbstractTestFileTrackImporter {
// A flush happens at the end // A flush happens at the end
when(contentProviderUtils.bulkInsertTrackPoint((Location[]) any(), eq(1), eq(TRACK_ID_0))).thenReturn(1); when(contentProviderUtils.bulkInsertTrackPoint((Location[]) any(), eq(1), eq(TRACK_ID_0))).thenReturn(1);
when(contentProviderUtils.getLastTrackPointId(TRACK_ID_0)).thenReturn(TRACK_POINT_ID_1); when(contentProviderUtils.getLastTrackPointId(TRACK_ID_0)).thenReturn(TRACK_POINT_ID_1);
when(contentProviderUtils.getTrack(PreferencesUtils.getLong(context, R.string.recording_track_id_key))).thenReturn(null); when(contentProviderUtils.getTrack(PreferencesUtils.getRecordingTrackId(context))).thenReturn(null);
expectUpdateTrack(trackCaptor, true, TRACK_ID_0); ArgumentCaptor<Track> trackCaptor = ArgumentCaptor.forClass(Track.class);
expectTrackUpdate(trackCaptor, true, TRACK_ID_0);
// when
InputStream inputStream = new ByteArrayInputStream(VALID_ONE_TRACK_ONE_SEGMENT_GPX.getBytes()); InputStream inputStream = new ByteArrayInputStream(VALID_ONE_TRACK_ONE_SEGMENT_GPX.getBytes());
GpxFileTrackImporter gpxFileTrackImporter = new GpxFileTrackImporter(context, contentProviderUtils); GpxFileTrackImporter gpxFileTrackImporter = new GpxFileTrackImporter(context, contentProviderUtils);
long trackId = gpxFileTrackImporter.importFile(inputStream); long trackId = gpxFileTrackImporter.importFile(inputStream);
// then
Assert.assertEquals(TRACK_ID_0, trackId); Assert.assertEquals(TRACK_ID_0, trackId);
verify(contentProviderUtils, atLeastOnce()).updateTrack(trackCaptor.capture());
long time0 = DATE_FORMAT_0.parse(TRACK_TIME_0).getTime(); long time0 = DATE_FORMAT_0.parse(TRACK_TIME_0).getTime();
long time1 = DATE_FORMAT_1.parse(TRACK_TIME_1).getTime(); long time1 = DATE_FORMAT_1.parse(TRACK_TIME_1).getTime();
Assert.assertEquals(time1 - time0, trackCaptor.getValue().getTripStatistics().getTotalTime()); Assert.assertEquals(time1 - time0, trackCaptor.getValue().getTripStatistics().getTotalTime());
verify(contentProviderUtils);
verifyTrack(trackCaptor.getValue(), TRACK_NAME_0, TRACK_DESCRIPTION_0, time0); verifyTrack(trackCaptor.getValue(), TRACK_NAME_0, TRACK_DESCRIPTION_0, time0);
} }
@Test @Test
public void testOneTrackTwoSegments() throws Exception { public void testOneTrackTwoSegments() throws Exception {
ArgumentCaptor<Track> trackCaptor = ArgumentCaptor.forClass(Track.class); // given
Location location0 = createLocation(0, DATE_FORMAT_0.parse(TRACK_TIME_0).getTime()); Location location0 = createLocation(0, DATE_FORMAT_0.parse(TRACK_TIME_0).getTime());
when(contentProviderUtils.insertTrack((Track) any())).thenReturn(TRACK_ID_0_URI); when(contentProviderUtils.insertTrack((Track) any())).thenReturn(TRACK_ID_0_URI);
@@ -121,12 +123,18 @@ public class GpxFileTrackImporterTest extends AbstractTestFileTrackImporter {
// A flush happens at the end // A flush happens at the end
when(contentProviderUtils.bulkInsertTrackPoint((Location[]) any(), eq(5), eq(TRACK_ID_0))).thenReturn(5); when(contentProviderUtils.bulkInsertTrackPoint((Location[]) any(), eq(5), eq(TRACK_ID_0))).thenReturn(5);
when(contentProviderUtils.getLastTrackPointId(TRACK_ID_0)).thenReturn(TRACK_POINT_ID_3); when(contentProviderUtils.getLastTrackPointId(TRACK_ID_0)).thenReturn(TRACK_POINT_ID_3);
when(contentProviderUtils.getTrack(PreferencesUtils.getLong(context, R.string.recording_track_id_key))).thenReturn(null); when(contentProviderUtils.getTrack(PreferencesUtils.getRecordingTrackId(context))).thenReturn(null);
expectUpdateTrack(trackCaptor, true, TRACK_ID_0);
ArgumentCaptor<Track> trackCaptor = ArgumentCaptor.forClass(Track.class);
expectTrackUpdate(trackCaptor, true, TRACK_ID_0);
// when
InputStream inputStream = new ByteArrayInputStream(VALID_ONE_TRACK_TWO_SEGMENTS_GPX.getBytes()); InputStream inputStream = new ByteArrayInputStream(VALID_ONE_TRACK_TWO_SEGMENTS_GPX.getBytes());
GpxFileTrackImporter gpxFileTrackImporter = new GpxFileTrackImporter(context, contentProviderUtils); GpxFileTrackImporter gpxFileTrackImporter = new GpxFileTrackImporter(context, contentProviderUtils);
long trackId = gpxFileTrackImporter.importFile(inputStream); long trackId = gpxFileTrackImporter.importFile(inputStream);
// then
verify(contentProviderUtils, atLeastOnce()).updateTrack(trackCaptor.capture());
Assert.assertEquals(TRACK_ID_0, trackId); Assert.assertEquals(TRACK_ID_0, trackId);
long time0 = DATE_FORMAT_0.parse(TRACK_TIME_0).getTime(); long time0 = DATE_FORMAT_0.parse(TRACK_TIME_0).getTime();
@@ -135,30 +143,33 @@ public class GpxFileTrackImporterTest extends AbstractTestFileTrackImporter {
long time3 = DATE_FORMAT_1.parse(TRACK_TIME_3).getTime(); long time3 = DATE_FORMAT_1.parse(TRACK_TIME_3).getTime();
Assert.assertEquals(time1 - time0 + time3 - time2, trackCaptor.getValue().getTripStatistics().getTotalTime()); Assert.assertEquals(time1 - time0 + time3 - time2, trackCaptor.getValue().getTripStatistics().getTotalTime());
verify(contentProviderUtils);
verifyTrack(trackCaptor.getValue(), TRACK_NAME_0, TRACK_DESCRIPTION_0, DATE_FORMAT_0.parse(TRACK_TIME_0).getTime()); verifyTrack(trackCaptor.getValue(), TRACK_NAME_0, TRACK_DESCRIPTION_0, DATE_FORMAT_0.parse(TRACK_TIME_0).getTime());
} }
@Test @Test
public void testOneTrackTwoSegmentsNoTime() { public void testOneTrackTwoSegmentsNoTime() {
ArgumentCaptor<Track> trackCaptor = ArgumentCaptor.forClass(Track.class); // given
when(contentProviderUtils.insertTrack((Track) any())).thenReturn(TRACK_ID_0_URI); when(contentProviderUtils.insertTrack((Track) any())).thenReturn(TRACK_ID_0_URI);
expectFirstTrackPoint(null, TRACK_ID_0, TRACK_POINT_ID_0); expectFirstTrackPoint(null, TRACK_ID_0, TRACK_POINT_ID_0);
// A flush happens at the end // A flush happens at the end
when(contentProviderUtils.bulkInsertTrackPoint((Location[]) any(), eq(5), eq(TRACK_ID_0))).thenReturn(5); when(contentProviderUtils.bulkInsertTrackPoint((Location[]) any(), eq(5), eq(TRACK_ID_0))).thenReturn(5);
when(contentProviderUtils.getLastTrackPointId(TRACK_ID_0)).thenReturn(TRACK_POINT_ID_3); when(contentProviderUtils.getLastTrackPointId(TRACK_ID_0)).thenReturn(TRACK_POINT_ID_3);
when(contentProviderUtils.getTrack(PreferencesUtils.getLong(context, R.string.recording_track_id_key))).thenReturn(null); when(contentProviderUtils.getTrack(PreferencesUtils.getRecordingTrackId(context))).thenReturn(null);
expectUpdateTrack(trackCaptor, true, TRACK_ID_0);
ArgumentCaptor<Track> trackCaptor = ArgumentCaptor.forClass(Track.class);
expectTrackUpdate(trackCaptor, true, TRACK_ID_0);
// when
InputStream inputStream = new ByteArrayInputStream(VALID_ONE_TRACK_TWO_SEGMENTS_NO_TIME_GPX.getBytes()); InputStream inputStream = new ByteArrayInputStream(VALID_ONE_TRACK_TWO_SEGMENTS_NO_TIME_GPX.getBytes());
GpxFileTrackImporter gpxFileTrackImporter = new GpxFileTrackImporter(context, contentProviderUtils); GpxFileTrackImporter gpxFileTrackImporter = new GpxFileTrackImporter(context, contentProviderUtils);
long trackId = gpxFileTrackImporter.importFile(inputStream); long trackId = gpxFileTrackImporter.importFile(inputStream);
Assert.assertEquals(TRACK_ID_0, trackId);
Assert.assertEquals(0, trackCaptor.getValue().getTripStatistics().getTotalTime());
verify(contentProviderUtils); // then
verify(contentProviderUtils, atLeastOnce()).updateTrack(trackCaptor.capture());
Assert.assertEquals(TRACK_ID_0, trackId);
Assert.assertEquals(0, trackCaptor.getValue().getTripStatistics().getTotalTime());
verifyTrack(trackCaptor.getValue(), TRACK_NAME_0, TRACK_DESCRIPTION_0, -1L); verifyTrack(trackCaptor.getValue(), TRACK_NAME_0, TRACK_DESCRIPTION_0, -1L);
} }
@@ -199,7 +210,7 @@ public class GpxFileTrackImporterTest extends AbstractTestFileTrackImporter {
when(contentProviderUtils.bulkInsertTrackPoint((Location[]) any(), anyInt(), anyLong())).thenReturn(1); when(contentProviderUtils.bulkInsertTrackPoint((Location[]) any(), anyInt(), anyLong())).thenReturn(1);
when(contentProviderUtils.getFirstTrackPointId(TRACK_ID_0)).thenReturn(TRACK_POINT_ID_0); when(contentProviderUtils.getFirstTrackPointId(TRACK_ID_0)).thenReturn(TRACK_POINT_ID_0);
when(contentProviderUtils.getLastTrackPointId(TRACK_ID_0)).thenReturn(TRACK_POINT_ID_0); when(contentProviderUtils.getLastTrackPointId(TRACK_ID_0)).thenReturn(TRACK_POINT_ID_0);
when(contentProviderUtils.getTrack(PreferencesUtils.getLong(context, R.string.recording_track_id_key))).thenReturn(null); when(contentProviderUtils.getTrack(PreferencesUtils.getRecordingTrackId(context))).thenReturn(null);
contentProviderUtils.deleteTrack(context, TRACK_ID_0); contentProviderUtils.deleteTrack(context, TRACK_ID_0);
InputStream inputStream = new ByteArrayInputStream(xml.getBytes()); InputStream inputStream = new ByteArrayInputStream(xml.getBytes());
@@ -27,11 +27,11 @@ import org.mockito.junit.MockitoJUnitRunner;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.InputStream; import java.io.InputStream;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.Track; import de.dennisguse.opentracks.content.Track;
import de.dennisguse.opentracks.util.PreferencesUtils; import de.dennisguse.opentracks.util.PreferencesUtils;
import static org.mockito.Mockito.any; import static org.mockito.Mockito.any;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.eq; import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
@@ -68,8 +68,7 @@ public class KmlFileTrackImporterTest extends AbstractTestFileTrackImporter {
@Test @Test
public void testOneTrackOneSegment() throws Exception { public void testOneTrackOneSegment() throws Exception {
ArgumentCaptor<Track> trackCaptor = ArgumentCaptor.forClass(Track.class); // given
Location location0 = createLocation(0, DATE_FORMAT_0.parse(TRACK_TIME_0).getTime()); Location location0 = createLocation(0, DATE_FORMAT_0.parse(TRACK_TIME_0).getTime());
Location location1 = createLocation(1, DATE_FORMAT_1.parse(TRACK_TIME_1).getTime()); Location location1 = createLocation(1, DATE_FORMAT_1.parse(TRACK_TIME_1).getTime());
@@ -77,27 +76,31 @@ public class KmlFileTrackImporterTest extends AbstractTestFileTrackImporter {
expectFirstTrackPoint(location0, TRACK_ID_0, TRACK_POINT_ID_0); expectFirstTrackPoint(location0, TRACK_ID_0, TRACK_POINT_ID_0);
// A flush happens at the end // A flush happens at the end
when(contentProviderUtils.bulkInsertTrackPoint(new Location[]{location1}, eq(1), eq(TRACK_ID_0))).thenReturn(1); when(contentProviderUtils.bulkInsertTrackPoint((Location[]) any(), eq(1), eq(TRACK_ID_0))).thenReturn(1);
when(contentProviderUtils.getLastTrackPointId(TRACK_ID_0)).thenReturn(TRACK_POINT_ID_1); when(contentProviderUtils.getLastTrackPointId(TRACK_ID_0)).thenReturn(TRACK_POINT_ID_1);
when(contentProviderUtils.getTrack(PreferencesUtils.getLong(context, R.string.recording_track_id_key))).thenReturn(null); when(contentProviderUtils.getTrack(PreferencesUtils.getRecordingTrackId(context))).thenReturn(null);
expectUpdateTrack(trackCaptor, true, TRACK_ID_0);
ArgumentCaptor<Track> trackCaptor = ArgumentCaptor.forClass(Track.class);
expectTrackUpdate(trackCaptor, true, TRACK_ID_0);
// when
InputStream inputStream = new ByteArrayInputStream(VALID_ONE_TRACK_ONE_SEGMENT_GPX.getBytes()); InputStream inputStream = new ByteArrayInputStream(VALID_ONE_TRACK_ONE_SEGMENT_GPX.getBytes());
KmlFileTrackImporter kmlFileTrackImporter = new KmlFileTrackImporter(context, TRACK_ID_0, contentProviderUtils); KmlFileTrackImporter kmlFileTrackImporter = new KmlFileTrackImporter(context, TRACK_ID_0, contentProviderUtils);
long trackId = kmlFileTrackImporter.importFile(inputStream); long trackId = kmlFileTrackImporter.importFile(inputStream);
// then
verify(contentProviderUtils, atLeastOnce()).updateTrack(trackCaptor.capture());
Assert.assertEquals(TRACK_ID_0, trackId); Assert.assertEquals(TRACK_ID_0, trackId);
long time0 = DATE_FORMAT_0.parse(TRACK_TIME_0).getTime(); long time0 = DATE_FORMAT_0.parse(TRACK_TIME_0).getTime();
long time1 = DATE_FORMAT_1.parse(TRACK_TIME_1).getTime(); long time1 = DATE_FORMAT_1.parse(TRACK_TIME_1).getTime();
Assert.assertEquals(time1 - time0, trackCaptor.getValue().getTripStatistics().getTotalTime()); Assert.assertEquals(time1 - time0, trackCaptor.getValue().getTripStatistics().getTotalTime());
verify(contentProviderUtils);
verifyTrack(trackCaptor.getValue(), TRACK_NAME_0, TRACK_DESCRIPTION_0, time0); verifyTrack(trackCaptor.getValue(), TRACK_NAME_0, TRACK_DESCRIPTION_0, time0);
} }
@Test @Test
public void testOneTrackTwoSegments() throws Exception { public void testOneTrackTwoSegments() throws Exception {
ArgumentCaptor<Track> trackCaptor = ArgumentCaptor.forClass(Track.class); // given
Location location0 = createLocation(0, DATE_FORMAT_0.parse(TRACK_TIME_0).getTime()); Location location0 = createLocation(0, DATE_FORMAT_0.parse(TRACK_TIME_0).getTime());
contentProviderUtils.clearTrack(context, TRACK_ID_0); contentProviderUtils.clearTrack(context, TRACK_ID_0);
@@ -106,12 +109,18 @@ public class KmlFileTrackImporterTest extends AbstractTestFileTrackImporter {
// A flush happens at the end // A flush happens at the end
when(contentProviderUtils.bulkInsertTrackPoint((Location[]) any(), eq(5), eq(TRACK_ID_0))).thenReturn(5); when(contentProviderUtils.bulkInsertTrackPoint((Location[]) any(), eq(5), eq(TRACK_ID_0))).thenReturn(5);
when(contentProviderUtils.getLastTrackPointId(TRACK_ID_0)).thenReturn(TRACK_POINT_ID_3); when(contentProviderUtils.getLastTrackPointId(TRACK_ID_0)).thenReturn(TRACK_POINT_ID_3);
when(contentProviderUtils.getTrack(PreferencesUtils.getLong(context, R.string.recording_track_id_key))).thenReturn(null); when(contentProviderUtils.getTrack(PreferencesUtils.getRecordingTrackId(context))).thenReturn(null);
expectUpdateTrack(trackCaptor, true, TRACK_ID_0);
ArgumentCaptor<Track> trackCaptor = ArgumentCaptor.forClass(Track.class);
expectTrackUpdate(trackCaptor, true, TRACK_ID_0);
// when
InputStream inputStream = new ByteArrayInputStream(VALID_ONE_TRACK_TWO_SEGMENTS_GPX.getBytes()); InputStream inputStream = new ByteArrayInputStream(VALID_ONE_TRACK_TWO_SEGMENTS_GPX.getBytes());
KmlFileTrackImporter kmlFileTrackImporter = new KmlFileTrackImporter(context, TRACK_ID_0, contentProviderUtils); KmlFileTrackImporter kmlFileTrackImporter = new KmlFileTrackImporter(context, TRACK_ID_0, contentProviderUtils);
long trackId = kmlFileTrackImporter.importFile(inputStream); long trackId = kmlFileTrackImporter.importFile(inputStream);
// then
verify(contentProviderUtils, atLeastOnce()).updateTrack(trackCaptor.capture());
Assert.assertEquals(TRACK_ID_0, trackId); Assert.assertEquals(TRACK_ID_0, trackId);
long time0 = DATE_FORMAT_0.parse(TRACK_TIME_0).getTime(); long time0 = DATE_FORMAT_0.parse(TRACK_TIME_0).getTime();
@@ -120,7 +129,6 @@ public class KmlFileTrackImporterTest extends AbstractTestFileTrackImporter {
long time3 = DATE_FORMAT_1.parse(TRACK_TIME_3).getTime(); long time3 = DATE_FORMAT_1.parse(TRACK_TIME_3).getTime();
Assert.assertEquals(time1 - time0 + time3 - time2, trackCaptor.getValue().getTripStatistics().getTotalTime()); Assert.assertEquals(time1 - time0 + time3 - time2, trackCaptor.getValue().getTripStatistics().getTotalTime());
verify(contentProviderUtils);
verifyTrack(trackCaptor.getValue(), TRACK_NAME_0, TRACK_DESCRIPTION_0, DATE_FORMAT_0.parse(TRACK_TIME_0).getTime()); verifyTrack(trackCaptor.getValue(), TRACK_NAME_0, TRACK_DESCRIPTION_0, DATE_FORMAT_0.parse(TRACK_TIME_0).getTime());
} }
} }
@@ -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);
}
}
@@ -15,14 +15,12 @@
*/ */
package de.dennisguse.opentracks.services; package de.dennisguse.opentracks.services;
import android.content.BroadcastReceiver;
import android.content.ContentProvider; import android.content.ContentProvider;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.location.Location; import android.location.Location;
import android.os.IBinder; import android.os.IBinder;
import android.util.Log;
import androidx.test.core.app.ApplicationProvider; import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -38,7 +36,6 @@ import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Random; import java.util.Random;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@@ -77,14 +74,12 @@ public class TrackRecordingServiceTest {
private long trackId = Math.abs(new Random().nextLong()); private long trackId = Math.abs(new Random().nextLong());
static Intent createStartIntent(Context context) { static Intent createStartIntent(Context context) {
Intent startIntent = new Intent(); return new Intent(context, TrackRecordingService.class);
startIntent.setClass(context, TrackRecordingService.class);
return startIntent;
} }
static void updateAutoResumePrefs(Context context, int attempts, int timeoutMins) { static void updateAutoResumePrefs(Context context, int attempts, int timeoutMins) {
PreferencesUtils.setInt(context, R.string.auto_resume_track_current_retry_key, attempts); PreferencesUtils.setInt(context, R.string.auto_resume_track_current_retry_key, attempts);
PreferencesUtils.setInt(context, R.string.auto_resume_track_timeout_key, timeoutMins); PreferencesUtils.setString(context, R.string.auto_resume_track_timeout_key, "" + timeoutMins);
} }
@Before @Before
@@ -94,7 +89,7 @@ public class TrackRecordingServiceTest {
}; };
customContentProvider.attachInfo(context, null); customContentProvider.attachInfo(context, null);
providerUtils = ContentProviderUtils.Factory.get(context); providerUtils = new ContentProviderUtils(context);
// Let's use default values. // Let's use default values.
SharedPreferences sharedPreferences = PreferencesUtils.getSharedPreferences(context); SharedPreferences sharedPreferences = PreferencesUtils.getSharedPreferences(context);
@@ -140,12 +135,15 @@ public class TrackRecordingServiceTest {
createDummyTrack(trackId, System.currentTimeMillis(), true); createDummyTrack(trackId, System.currentTimeMillis(), true);
// Clear the number of attempts and set the timeout to 10 min. // Clear the number of attempts and set the timeout to 10 min.
updateAutoResumePrefs(context, PreferencesUtils.AUTO_RESUME_TRACK_CURRENT_RETRY_DEFAULT, PreferencesUtils.AUTO_RESUME_TRACK_TIMEOUT_DEFAULT); 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). // Start the service in "resume" mode (simulates the on-reboot action).
Intent startIntent = createStartIntent(context); Intent startIntent = createStartIntent(context);
startIntent.putExtra(TrackRecordingService.RESUME_TRACK_EXTRA_NAME, true); startIntent.putExtra(TrackRecordingService.RESUME_TRACK_EXTRA_NAME, true);
mServiceRule.startService(startIntent);
ITrackRecordingService service = ((ITrackRecordingService) mServiceRule.bindService(startIntent)); ITrackRecordingService service = ((ITrackRecordingService) mServiceRule.bindService(startIntent));
// then
Assert.assertNotNull(service); Assert.assertNotNull(service);
// We expect to resume the previous track. // We expect to resume the previous track.
@@ -156,7 +154,7 @@ public class TrackRecordingServiceTest {
@MediumTest @MediumTest
@Test @Test
public void testResumeAfterReboot_simulateReboot() throws Exception { public void testResumeAfterReboot_simulateReboot() throws Exception {
updateAutoResumePrefs(context, PreferencesUtils.AUTO_RESUME_TRACK_CURRENT_RETRY_DEFAULT, PreferencesUtils.AUTO_RESUME_TRACK_TIMEOUT_DEFAULT); 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))); ITrackRecordingService service = ((ITrackRecordingService) mServiceRule.bindService(createStartIntent(context)));
Assert.assertFalse(service.isRecording()); Assert.assertFalse(service.isRecording());
@@ -165,15 +163,17 @@ public class TrackRecordingServiceTest {
Assert.assertTrue(service.isRecording()); Assert.assertTrue(service.isRecording());
Assert.assertEquals(id, service.getRecordingTrackId()); Assert.assertEquals(id, service.getRecordingTrackId());
mServiceRule.unbindService(); mServiceRule.unbindService();
Assert.assertEquals(id, PreferencesUtils.getLong(context, R.string.recording_track_id_key)); Assert.assertEquals(id, PreferencesUtils.getRecordingTrackId(context));
// Start the service in "resume" mode (simulates the on-reboot action). // Start the service in "resume" mode (simulates the on-reboot action).
Intent startIntent = createStartIntent(context); Intent startIntent = createStartIntent(context);
startIntent.putExtra(TrackRecordingService.RESUME_TRACK_EXTRA_NAME, true); startIntent.putExtra(TrackRecordingService.RESUME_TRACK_EXTRA_NAME, true);
mServiceRule.startService(startIntent); mServiceRule.startService(startIntent);
service = ((ITrackRecordingService) mServiceRule.bindService(createStartIntent(context))); service = ((ITrackRecordingService) mServiceRule.bindService(createStartIntent(context)));
Assert.assertNotNull(service);
// then
Assert.assertNotNull(service);
Assert.assertTrue(service.isRecording()); Assert.assertTrue(service.isRecording());
} }
@@ -184,12 +184,15 @@ public class TrackRecordingServiceTest {
createDummyTrack(trackId, System.currentTimeMillis(), false); createDummyTrack(trackId, System.currentTimeMillis(), false);
// Clear the number of attempts and set the timeout to 10 min. // Clear the number of attempts and set the timeout to 10 min.
updateAutoResumePrefs(context, PreferencesUtils.AUTO_RESUME_TRACK_CURRENT_RETRY_DEFAULT, PreferencesUtils.AUTO_RESUME_TRACK_TIMEOUT_DEFAULT); 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). // Start the service in "resume" mode (simulates the on-reboot action).
Intent startIntent = createStartIntent(context); Intent startIntent = createStartIntent(context);
startIntent.putExtra(TrackRecordingService.RESUME_TRACK_EXTRA_NAME, true); startIntent.putExtra(TrackRecordingService.RESUME_TRACK_EXTRA_NAME, true);
mServiceRule.startService(startIntent);
ITrackRecordingService service = ((ITrackRecordingService) mServiceRule.bindService(startIntent)); ITrackRecordingService service = ((ITrackRecordingService) mServiceRule.bindService(startIntent));
// then
Assert.assertNotNull(service); Assert.assertNotNull(service);
// We don't expect to resume the previous track, because it was stopped. // We don't expect to resume the previous track, because it was stopped.
@@ -201,15 +204,18 @@ public class TrackRecordingServiceTest {
@Test @Test
public void testResumeAfterReboot_expiredTrack() throws Exception { public void testResumeAfterReboot_expiredTrack() throws Exception {
// Insert a dummy track last updated 20 min ago. // Insert a dummy track last updated 20 min ago.
createDummyTrack(trackId, System.currentTimeMillis() - 20 * 60 * 1000, true); createDummyTrack(trackId, System.currentTimeMillis() - 1500 * 60 * 1000, true);
// Clear the number of attempts and set the timeout to 10 min. // Clear the number of attempts and set the timeout to 10 min.
updateAutoResumePrefs(context, PreferencesUtils.AUTO_RESUME_TRACK_CURRENT_RETRY_DEFAULT, PreferencesUtils.AUTO_RESUME_TRACK_TIMEOUT_DEFAULT); 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). // Start the service in "resume" mode (simulates the on-reboot action).
Intent startIntent = createStartIntent(context); Intent startIntent = createStartIntent(context);
startIntent.putExtra(TrackRecordingService.RESUME_TRACK_EXTRA_NAME, true); startIntent.putExtra(TrackRecordingService.RESUME_TRACK_EXTRA_NAME, true);
mServiceRule.startService(startIntent);
ITrackRecordingService service = ((ITrackRecordingService) mServiceRule.bindService(startIntent)); ITrackRecordingService service = ((ITrackRecordingService) mServiceRule.bindService(startIntent));
// then
Assert.assertNotNull(service); Assert.assertNotNull(service);
// We don't expect to resume the previous track, because it has expired. // We don't expect to resume the previous track, because it has expired.
@@ -224,16 +230,15 @@ public class TrackRecordingServiceTest {
createDummyTrack(trackId, System.currentTimeMillis(), true); createDummyTrack(trackId, System.currentTimeMillis(), true);
// Set the number of attempts to max. // Set the number of attempts to max.
updateAutoResumePrefs(context, TrackRecordingService.MAX_AUTO_RESUME_TRACK_RETRY_ATTEMPTS, PreferencesUtils.AUTO_RESUME_TRACK_TIMEOUT_DEFAULT); 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). // Start the service in "resume" mode (simulates the on-reboot action).
Intent startIntent = createStartIntent(context); Intent startIntent = createStartIntent(context);
startIntent.putExtra(TrackRecordingService.RESUME_TRACK_EXTRA_NAME, true); startIntent.putExtra(TrackRecordingService.RESUME_TRACK_EXTRA_NAME, true);
mServiceRule.startService(startIntent);
//Explicit start service, so `startCommand()` is executed - which would be called by BootReceiver.
context.startService(startIntent);
ITrackRecordingService service = ((ITrackRecordingService) mServiceRule.bindService(startIntent)); ITrackRecordingService service = ((ITrackRecordingService) mServiceRule.bindService(startIntent));
//then
Assert.assertNotNull(service); Assert.assertNotNull(service);
// We don't expect to resume the previous track, because there were already too many attempts. // We don't expect to resume the previous track, because there were already too many attempts.
@@ -247,7 +252,10 @@ public class TrackRecordingServiceTest {
List<Track> tracks = providerUtils.getAllTracks(); List<Track> tracks = providerUtils.getAllTracks();
Assert.assertTrue(tracks.isEmpty()); Assert.assertTrue(tracks.isEmpty());
ITrackRecordingService service = ((ITrackRecordingService) mServiceRule.bindService(createStartIntent(context))); Intent startIntent = createStartIntent(context);
mServiceRule.startService(startIntent);
ITrackRecordingService service = ((ITrackRecordingService) mServiceRule.bindService(startIntent));
// Test if we start in no-recording mode by default. // Test if we start in no-recording mode by default.
Assert.assertFalse(service.isRecording()); Assert.assertFalse(service.isRecording());
Assert.assertEquals(-1L, service.getRecordingTrackId()); Assert.assertEquals(-1L, service.getRecordingTrackId());
@@ -266,11 +274,14 @@ public class TrackRecordingServiceTest {
@MediumTest @MediumTest
@Test @Test
public void testRecording_orphanedRecordingTrack() throws Exception { public void testRecording_orphanedRecordingTrack() throws Exception {
Intent startIntent = createStartIntent(context);
ITrackRecordingService service = ((ITrackRecordingService) mServiceRule.bindService(startIntent));
// Just set recording track to a bogus value. // Just set recording track to a bogus value.
// Make sure that the service will not start recording and will clear the bogus track.
PreferencesUtils.setLong(context, R.string.recording_track_id_key, 123L); PreferencesUtils.setLong(context, R.string.recording_track_id_key, 123L);
// Make sure that the service will not start recording and will clear the bogus track. // then
ITrackRecordingService service = ((ITrackRecordingService) mServiceRule.bindService(createStartIntent(context)));
Assert.assertFalse(service.isRecording()); Assert.assertFalse(service.isRecording());
Assert.assertEquals(PreferencesUtils.RECORDING_TRACK_ID_DEFAULT, service.getRecordingTrackId()); Assert.assertEquals(PreferencesUtils.RECORDING_TRACK_ID_DEFAULT, service.getRecordingTrackId());
} }
@@ -286,7 +297,7 @@ public class TrackRecordingServiceTest {
long newTrackId = service.startNewTrack(); long newTrackId = service.startNewTrack();
Assert.assertEquals(PreferencesUtils.RECORDING_TRACK_ID_DEFAULT, newTrackId); Assert.assertEquals(PreferencesUtils.RECORDING_TRACK_ID_DEFAULT, newTrackId);
Assert.assertEquals(trackId, PreferencesUtils.getLong(context, R.string.recording_track_id_key)); Assert.assertEquals(trackId, PreferencesUtils.getRecordingTrackId(context));
Assert.assertEquals(trackId, service.getRecordingTrackId()); Assert.assertEquals(trackId, service.getRecordingTrackId());
} }
@@ -299,7 +310,7 @@ public class TrackRecordingServiceTest {
// Ending the current track when there is no recording should not result in any error. // Ending the current track when there is no recording should not result in any error.
service.endCurrentTrack(); service.endCurrentTrack();
Assert.assertEquals(PreferencesUtils.RECORDING_TRACK_ID_DEFAULT, PreferencesUtils.getLong(context, R.string.recording_track_id_key)); Assert.assertFalse(PreferencesUtils.isRecording(context));
Assert.assertEquals(PreferencesUtils.RECORDING_TRACK_ID_DEFAULT, service.getRecordingTrackId()); Assert.assertEquals(PreferencesUtils.RECORDING_TRACK_ID_DEFAULT, service.getRecordingTrackId());
} }
@@ -405,45 +416,4 @@ public class TrackRecordingServiceTest {
Assert.assertNotNull(wpt.getLocation()); Assert.assertNotNull(wpt.getLocation());
Assert.assertNull(wpt.getTripStatistics()); Assert.assertNull(wpt.getTripStatistics());
} }
/**
* Synchronous/waitable broadcast receiver to be used in testing.
*/
private class BlockingBroadcastReceiver extends BroadcastReceiver {
private static final long MAX_WAIT_TIME_MS = 3000;
private final List<Intent> receivedIntents = new ArrayList<>();
public List<Intent> getReceivedIntents() {
return receivedIntents;
}
@Override
public void onReceive(Context ctx, Intent intent) {
Log.d("Test", "Got broadcast: " + intent);
synchronized (receivedIntents) {
receivedIntents.add(intent);
receivedIntents.notifyAll();
}
}
public boolean waitUntilReceived(int receiveCount) {
long deadline = System.currentTimeMillis() + MAX_WAIT_TIME_MS;
synchronized (receivedIntents) {
while (receivedIntents.size() < receiveCount) {
try {
// Wait releases synchronized lock until it returns
receivedIntents.wait(500);
} catch (InterruptedException e) {
// Do nothing
}
if (System.currentTimeMillis() > deadline) {
return false;
}
}
}
return true;
}
}
} }
@@ -68,7 +68,7 @@ public class TrackRecordingServiceTestLooper {
}; };
customContentProvider.attachInfo(context, null); customContentProvider.attachInfo(context, null);
providerUtils = ContentProviderUtils.Factory.get(context); providerUtils = new ContentProviderUtils(context);
// Let's use default values. // Let's use default values.
SharedPreferences sharedPreferences = PreferencesUtils.getSharedPreferences(context); SharedPreferences sharedPreferences = PreferencesUtils.getSharedPreferences(context);
@@ -103,7 +103,7 @@ public class TrackRecordingServiceTestLooper {
@MediumTest @MediumTest
@Test @Test
public void testWithProperties_voiceFrequencyDefault() throws TimeoutException { public void testWithProperties_voiceFrequencyDefault() throws TimeoutException {
PreferencesUtils.setInt(context, R.string.voice_frequency_key, PreferencesUtils.VOICE_FREQUENCY_DEFAULT); PreferencesUtils.setInt(context, R.string.voice_frequency_key, Integer.parseInt(context.getResources().getString(R.string.voice_frequency_default)));
fullRecordingSession(); fullRecordingSession();
} }
@@ -125,7 +125,7 @@ public class TrackRecordingServiceTestLooper {
@MediumTest @MediumTest
@Test @Test
public void testWithProperties_maxRecordingDistanceDefault() throws TimeoutException { public void testWithProperties_maxRecordingDistanceDefault() throws TimeoutException {
PreferencesUtils.setInt(context, R.string.max_recording_distance_key, PreferencesUtils.MAX_RECORDING_DISTANCE_DEFAULT); PreferencesUtils.setInt(context, R.string.max_recording_distance_key, Integer.parseInt(context.getResources().getString(R.string.max_recording_distance_default)));
fullRecordingSession(); fullRecordingSession();
} }
@@ -139,7 +139,8 @@ public class TrackRecordingServiceTestLooper {
@MediumTest @MediumTest
@Test @Test
public void testWithProperties_minRecordingDistanceDefault() throws TimeoutException { 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(); fullRecordingSession();
} }
@@ -153,7 +154,7 @@ public class TrackRecordingServiceTestLooper {
@MediumTest @MediumTest
@Test @Test
public void testWithProperties_splitFrequencyDefault() throws TimeoutException { public void testWithProperties_splitFrequencyDefault() throws TimeoutException {
PreferencesUtils.setInt(context, R.string.split_frequency_key, PreferencesUtils.SPLIT_FREQUENCY_DEFAULT); PreferencesUtils.setInt(context, R.string.split_frequency_key, Integer.parseInt(context.getResources().getString(R.string.split_frequency_default)));
fullRecordingSession(); fullRecordingSession();
} }
@@ -174,7 +175,7 @@ public class TrackRecordingServiceTestLooper {
@MediumTest @MediumTest
@Test @Test
public void testWithProperties_metricUnitsDefault() throws TimeoutException { public void testWithProperties_metricUnitsDefault() throws TimeoutException {
PreferencesUtils.setString(context, R.string.stats_units_key, PreferencesUtils.STATS_UNITS_DEFAULT); PreferencesUtils.setString(context, R.string.stats_units_key, context.getString(R.string.stats_units_default));
fullRecordingSession(); fullRecordingSession();
} }
@@ -188,7 +189,7 @@ public class TrackRecordingServiceTestLooper {
@MediumTest @MediumTest
@Test @Test
public void testWithProperties_minRecordingIntervalDefault() throws TimeoutException { public void testWithProperties_minRecordingIntervalDefault() throws TimeoutException {
PreferencesUtils.setInt(context, R.string.min_recording_interval_key, PreferencesUtils.MIN_RECORDING_INTERVAL_DEFAULT); PreferencesUtils.setInt(context, R.string.min_recording_interval_key, Integer.parseInt(context.getResources().getString(R.string.min_recording_interval_default)));
fullRecordingSession(); fullRecordingSession();
} }
@@ -202,7 +203,8 @@ public class TrackRecordingServiceTestLooper {
@MediumTest @MediumTest
@Test @Test
public void testWithProperties_minRequiredAccuracyDefault() throws TimeoutException { 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(); fullRecordingSession();
} }
@@ -225,7 +227,7 @@ public class TrackRecordingServiceTestLooper {
Track track = providerUtils.getTrack(id); Track track = providerUtils.getTrack(id);
Assert.assertNotNull(track); Assert.assertNotNull(track);
Assert.assertEquals(id, track.getId()); Assert.assertEquals(id, track.getId());
Assert.assertEquals(id, PreferencesUtils.getLong(context, R.string.recording_track_id_key)); Assert.assertEquals(id, PreferencesUtils.getRecordingTrackId(context));
Assert.assertEquals(id, service.getRecordingTrackId()); Assert.assertEquals(id, service.getRecordingTrackId());
// Insert a few points, markers and statistics. // Insert a few points, markers and statistics.
@@ -1,557 +0,0 @@
/*
* Copyright 2010 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package de.dennisguse.opentracks.services.tasks;
import android.content.Context;
import android.os.Looper;
import android.speech.tts.TextToSpeech;
import android.speech.tts.TextToSpeech.OnInitListener;
import android.speech.tts.TextToSpeech.OnUtteranceCompletedListener;
import android.telephony.PhoneStateListener;
import android.telephony.TelephonyManager;
import androidx.test.core.app.ApplicationProvider;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Assert;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import java.util.HashMap;
import java.util.Locale;
import java.util.concurrent.atomic.AtomicBoolean;
import de.dennisguse.opentracks.stats.TripStatistics;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.same;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
* Tests for {@link AnnouncementPeriodicTask}.
*
* WARNING: I'm not responsible if your eyes start bleeding while reading this code.
* You have been warned.
* It's still better than no test, though.
*
* @author Rodrigo Damazio
*/
@RunWith(MockitoJUnitRunner.class)
public class AnnouncementPeriodicTaskTest {
// Use something other than our hardcoded value
private static final Locale DEFAULT_LOCALE = Locale.KOREAN;
private static final String ANNOUNCEMENT = "I can haz cheeseburger?";
private Locale oldDefaultLocale;
private Context context = ApplicationProvider.getApplicationContext();
private AnnouncementPeriodicTask task;
@Mock
private AnnouncementPeriodicTask mockTask;
private ArgumentCaptor<OnInitListener> initListenerCapture = ArgumentCaptor.forClass(OnInitListener.class);
private ArgumentCaptor<PhoneStateListener> phoneListenerCapture = ArgumentCaptor.forClass(PhoneStateListener.class);
private TextToSpeechDelegate ttsDelegate;
@Mock
private TextToSpeechInterface tts;
@BeforeClass
public static void preSetUp() {
// Prepare looper for Android's message queue
if (Looper.myLooper() == null) Looper.prepare();
}
@AfterClass
public static void finalTearDown() {
if (Looper.myLooper() != null) Looper.myLooper().quit();
}
@Before
public void setUp() throws Exception {
oldDefaultLocale = Locale.getDefault();
Locale.setDefault(DEFAULT_LOCALE);
// Eww, the effort required just to mock TextToSpeech is insane
final AtomicBoolean listenerCalled = new AtomicBoolean();
OnInitListener blockingListener = new OnInitListener() {
@Override
public void onInit(int status) {
synchronized (this) {
listenerCalled.set(true);
notify();
}
}
};
ttsDelegate = new TextToSpeechDelegate(context, blockingListener);
// Wait for all async operations done in the constructor to finish.
synchronized (blockingListener) {
while (!listenerCalled.get()) {
// Releases the synchronized lock until we're woken up.
blockingListener.wait();
}
}
// Create a partial forwarding mock
task = new AnnouncementPeriodicTask(context) {
@Override
protected TextToSpeech newTextToSpeech(Context ctx, OnInitListener onInitListener) {
return mockTask.newTextToSpeech(ctx, onInitListener);
}
@Override
protected String getAnnouncement(TripStatistics stats) {
return mockTask.getAnnouncement(stats);
}
@Override
protected void listenToPhoneState(PhoneStateListener listener, int events) {
mockTask.listenToPhoneState(listener, events);
}
};
}
@After
public void tearDown() {
Locale.setDefault(oldDefaultLocale);
}
@Test
public void testStart() {
doStart();
OnInitListener ttsInitListener = initListenerCapture.getValue();
Assert.assertNotNull(ttsInitListener);
ttsInitListener.onInit(TextToSpeech.SUCCESS);
//TODO
//verify(mockTask, tts);
}
@Test
public void testStart_notReady() {
doStart();
OnInitListener ttsInitListener = initListenerCapture.getValue();
Assert.assertNotNull(ttsInitListener);
ttsInitListener.onInit(TextToSpeech.ERROR);
//TODO
//AndroidMock.verify(mockTask, tts);
}
@Test
public void testShutdown() {
// First, start
doStart();
verify(mockTask);
reset(mockTask);
// Then, shut down
PhoneStateListener phoneListener = phoneListenerCapture.getValue();
mockTask.listenToPhoneState(
same(phoneListener), eq(PhoneStateListener.LISTEN_NONE));
tts.shutdown();
task.shutdown();
//TODO
//AndroidMock.verify(mockTask, tts);
}
@Test
public void testRun() {
// when service data calls
TripStatistics stats = new TripStatistics();
// when announcement building call
when(mockTask.getAnnouncement(same(stats))).thenReturn(ANNOUNCEMENT);
// Put task in "ready" state
startTask(TextToSpeech.SUCCESS);
when(tts.isLanguageAvailable(DEFAULT_LOCALE)).thenReturn(TextToSpeech.LANG_AVAILABLE);
when(tts.setLanguage(DEFAULT_LOCALE)).thenReturn(TextToSpeech.LANG_AVAILABLE);
when(tts.setSpeechRate(AnnouncementPeriodicTask.TTS_SPEECH_RATE)).thenReturn(TextToSpeech.SUCCESS);
when(tts.setOnUtteranceCompletedListener((OnUtteranceCompletedListener) any())).thenReturn(0);
// Run the announcement
task.announce(stats);
//TODO
//AndroidMock.verify(mockTask, tts);
}
@Test
public void testRun_notReady() {
// Put task in "not ready" state
startTask(TextToSpeech.ERROR);
// Run the announcement
task.run(null);
//TODO
//verify(mockTask, tts);
}
@Test
public void testRun_duringCall() {
startTask(TextToSpeech.SUCCESS);
when(tts.isSpeaking()).thenReturn(false);
// Run the announcement
PhoneStateListener phoneListener = phoneListenerCapture.getValue();
phoneListener.onCallStateChanged(TelephonyManager.CALL_STATE_OFFHOOK, null);
task.run(null);
//TODO
//verify(mockTask, tts);
}
@Test
public void testRun_ringWhileSpeaking() {
startTask(TextToSpeech.SUCCESS);
when(tts.isSpeaking()).thenReturn(true);
when(tts.stop()).thenReturn(TextToSpeech.SUCCESS);
// Update the state to ringing - this should stop the current announcement.
PhoneStateListener phoneListener = phoneListenerCapture.getValue();
phoneListener.onCallStateChanged(TelephonyManager.CALL_STATE_RINGING, null);
// Run the announcement - this should do nothing.
task.run(null);
//TODO
//AndroidMock.verify(mockTask, tts);
}
@Test
public void testRun_whileRinging() {
startTask(TextToSpeech.SUCCESS);
when(tts.isSpeaking()).thenReturn(false);
// Run the announcement
PhoneStateListener phoneListener = phoneListenerCapture.getValue();
phoneListener.onCallStateChanged(TelephonyManager.CALL_STATE_RINGING, null);
task.run(null);
//TODO
//AndroidMock.verify(mockTask, tts);
}
@Test
public void testRun_noService() {
startTask(TextToSpeech.SUCCESS);
// Run the announcement
task.run(null);
//TODO
//AndroidMock.verify(mockTask, tts);
}
@Test
public void testRun_noStats() {
// when service data calls
startTask(TextToSpeech.SUCCESS);
// Run the announcement
task.run(null);
//TODO
//AndroidMock.verify(mockTask, tts);
}
/**
* Tests {@link AnnouncementPeriodicTask#getAnnounceTime(long)} with time zero.
*/
@Test
public void testGetAnnounceTime_time_zero() {
long time = 0; // 0 seconds
Assert.assertEquals("0 minutes 0 seconds", task.getAnnounceTime(time));
}
/**
* Tests {@link AnnouncementPeriodicTask#getAnnounceTime(long)} with time one.
*/
@Test
public void testGetAnnounceTime_time_one() {
long time = 1000; // 1 second
Assert.assertEquals("0 minutes 1 second", task.getAnnounceTime(time));
}
/**
* Tests {@link AnnouncementPeriodicTask#getAnnounceTime(long)} with singular
* numbers with the hour unit.
*/
@Test
public void testGetAnnounceTime_singular_has_hour() {
long time = (60 * 60 * 1000) + (60 * 1000) + (1000); // 1 hour 1 minute 1 second
Assert.assertEquals("1 hour 1 minute 1 second", task.getAnnounceTime(time));
}
/**
* Tests {@link AnnouncementPeriodicTask#getAnnounceTime(long)} with plural numbers
* with the hour unit.
*/
@Test
public void testGetAnnounceTime_plural_has_hour() {
long time = (2 * 60 * 60 * 1000) + (2 * 60 * 1000) + (2 * 1000); // 2 hours 2 minutes 2 seconds
Assert.assertEquals("2 hours 2 minutes 2 seconds", task.getAnnounceTime(time));
}
/**
* Tests {@link AnnouncementPeriodicTask#getAnnounceTime(long)} with singular
* numbers without the hour unit.
*/
@Test
public void testGetAnnounceTime_singular_no_hour() {
long time = (1 * 60 * 1000) + (1 * 1000); // 1 minute 1 second
Assert.assertEquals("1 minute 1 second", task.getAnnounceTime(time));
}
/**
* Tests {@link AnnouncementPeriodicTask#getAnnounceTime(long)} with plural numbers
* without the hour unit.
*/
@Test
public void testGetAnnounceTime_plural_no_hour() {
long time = (2 * 60 * 1000) + (2 * 1000); // 2 minutes 2 seconds
Assert.assertEquals("2 minutes 2 seconds", task.getAnnounceTime(time));
}
private void startTask(int state) {
reset(tts);
doStart();
OnInitListener ttsInitListener = initListenerCapture.getValue();
ttsInitListener.onInit(state);
//TODO
reset(tts);
//AndroidMock.resetToDefault(tts);
}
private void doStart() {
mockTask.listenToPhoneState(phoneListenerCapture.capture(), eq(PhoneStateListener.LISTEN_CALL_STATE));
when(mockTask.newTextToSpeech(same(context), initListenerCapture.capture())).thenReturn(ttsDelegate);
task.start();
}
/**
* Mockable interface that we delegate TTS calls to.
*/
interface TextToSpeechInterface {
int addEarcon(String earcon, String packagename, int resourceId);
int addEarcon(String earcon, String filename);
int addSpeech(String text, String packagename, int resourceId);
int addSpeech(String text, String filename);
boolean areDefaultsEnforced();
String getDefaultEngine();
Locale getLanguage();
int isLanguageAvailable(Locale loc);
boolean isSpeaking();
int playEarcon(String earcon, int queueMode,
HashMap<String, String> params);
int playSilence(long durationInMs, int queueMode, HashMap<String, String> params);
int setLanguage(Locale loc);
int setOnUtteranceCompletedListener(OnUtteranceCompletedListener listener);
int setPitch(float pitch);
int setSpeechRate(float speechRate);
void shutdown();
int speak(String text, int queueMode, HashMap<String, String> params);
int stop();
int synthesizeToFile(String text, HashMap<String, String> params, String filename);
}
/**
* Subclass of {@link TextToSpeech} which delegates calls to the interface
* above.
* The logic here is stupid and the author is ashamed of having to write it
* like this, but basically the issue is that TextToSpeech cannot be mocked
* without running its constructor, its constructor runs async operations
* which call other methods (and then if the methods are part of a mock we'd
* have to set a behavior, but we can't 'cause the object hasn't been fully
* built yet).
* The logic is that calls made during the constructor (when tts is not yet
* set) will go up to the original class, but after tts is set we'll forward
* them all to the mock.
*/
private class TextToSpeechDelegate extends TextToSpeech implements TextToSpeechInterface {
public TextToSpeechDelegate(Context context, OnInitListener listener) {
super(context, listener);
}
@Override
public int addEarcon(String earcon, String packagename, int resourceId) {
if (tts == null) {
return super.addEarcon(earcon, packagename, resourceId);
}
return tts.addEarcon(earcon, packagename, resourceId);
}
@Override
public int addEarcon(String earcon, String filename) {
if (tts == null) {
return super.addEarcon(earcon, filename);
}
return tts.addEarcon(earcon, filename);
}
@Override
public int addSpeech(String text, String packagename, int resourceId) {
if (tts == null) {
return super.addSpeech(text, packagename, resourceId);
}
return tts.addSpeech(text, packagename, resourceId);
}
@Override
public int addSpeech(String text, String filename) {
if (tts == null) {
return super.addSpeech(text, filename);
}
return tts.addSpeech(text, filename);
}
@Override
public Locale getLanguage() {
if (tts == null) {
return super.getLanguage();
}
return tts.getLanguage();
}
@Override
public int isLanguageAvailable(Locale loc) {
if (tts == null) {
return super.isLanguageAvailable(loc);
}
return tts.isLanguageAvailable(loc);
}
@Override
public boolean isSpeaking() {
if (tts == null) {
return super.isSpeaking();
}
return tts.isSpeaking();
}
@Override
public int playEarcon(String earcon, int queueMode, HashMap<String, String> params) {
if (tts == null) {
return super.playEarcon(earcon, queueMode, params);
}
return tts.playEarcon(earcon, queueMode, params);
}
@Override
public int playSilence(long durationInMs, int queueMode, HashMap<String, String> params) {
if (tts == null) {
return super.playSilence(durationInMs, queueMode, params);
}
return tts.playSilence(durationInMs, queueMode, params);
}
@Override
public int setLanguage(Locale loc) {
if (tts == null) {
return super.setLanguage(loc);
}
return tts.setLanguage(loc);
}
@Override
public int setOnUtteranceCompletedListener(OnUtteranceCompletedListener listener) {
if (tts == null) {
return super.setOnUtteranceCompletedListener(listener);
}
return tts.setOnUtteranceCompletedListener(listener);
}
@Override
public int setPitch(float pitch) {
if (tts == null) {
return super.setPitch(pitch);
}
return tts.setPitch(pitch);
}
@Override
public int setSpeechRate(float speechRate) {
if (tts == null) {
return super.setSpeechRate(speechRate);
}
return tts.setSpeechRate(speechRate);
}
@Override
public void shutdown() {
if (tts == null) {
super.shutdown();
return;
}
tts.shutdown();
}
@Override
public int speak(
String text, int queueMode, HashMap<String, String> params) {
if (tts == null) {
return super.speak(text, queueMode, params);
}
return tts.speak(text, queueMode, params);
}
@Override
public int stop() {
if (tts == null) {
return super.stop();
}
return tts.stop();
}
@Override
public int synthesizeToFile(String text, HashMap<String, String> params, String filename) {
if (tts == null) {
return super.synthesizeToFile(text, params, filename);
}
return tts.synthesizeToFile(text, params, filename);
}
}
}
@@ -60,10 +60,15 @@ public class FileUtilsTest extends TestCase {
*/ */
public void testSanitizeFileName() { public void testSanitizeFileName() {
String name = "Swim\10ming-^across:/the/ pacific (ocean)."; 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)); 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). * Tests {@link FileUtils#sanitizeFileName(String)} with i18n characters (in Chinese and Russian).
* Verifies that they are allowed. * Verifies that they are allowed.
@@ -131,7 +131,7 @@ public class StringUtilsTest {
assertGetTime("2010-05-04T03:02:01.3", 2010, 5, 4, 3, 2, 1, 300); assertGetTime("2010-05-04T03:02:01.3", 2010, 5, 4, 3, 2, 1, 300);
assertGetTime("2010-05-04T03:02:01.35", 2010, 5, 4, 3, 2, 1, 350); assertGetTime("2010-05-04T03:02:01.35", 2010, 5, 4, 3, 2, 1, 350);
assertGetTime("2010-05-04T03:02:01.352Z", 2010, 5, 4, 3, 2, 1, 352); assertGetTime("2010-05-04T03:02:01.352Z", 2010, 5, 4, 3, 2, 1, 352);
assertGetTime("2010-05-04T03:02:01.3525Z", 2010, 5, 4, 3, 2, 1, 352); assertGetTime("2010-05-04T03:02:01.3529Z", 2010, 5, 4, 3, 2, 1, 353);
} }
/** /**
@@ -199,4 +199,24 @@ public class StringUtilsTest {
Assert.assertEquals(-1, parts[1]); Assert.assertEquals(-1, parts[1]);
Assert.assertEquals(0, parts[2]); Assert.assertEquals(0, parts[2]);
} }
@Test
public void testFormatDecimal() {
Assert.assertEquals("0", StringUtils.formatDecimal(0.0, 0));
Assert.assertEquals("0", StringUtils.formatDecimal(0.1, 0));
Assert.assertEquals("1", StringUtils.formatDecimal(1.1, 0));
Assert.assertEquals("10", StringUtils.formatDecimal(10, 0));
Assert.assertEquals("10", StringUtils.formatDecimal(10.1, 0));
Assert.assertEquals("0", StringUtils.formatDecimal(-0.1, 0));
Assert.assertEquals("0", StringUtils.formatDecimal(0.0, 2));
Assert.assertEquals("0.1", StringUtils.formatDecimal(0.1, 2));
Assert.assertEquals("1.1", StringUtils.formatDecimal(1.1, 2));
Assert.assertEquals("10", StringUtils.formatDecimal(10, 2));
Assert.assertEquals("10.1", StringUtils.formatDecimal(10.1, 2));
Assert.assertEquals("10.11", StringUtils.formatDecimal(10.111, 2));
Assert.assertEquals("-0.1", StringUtils.formatDecimal(-0.1, 2));
Assert.assertEquals("1", StringUtils.formatDecimal(0.99, 1));
}
} }
+12 -5
View File
@@ -16,8 +16,8 @@ limitations under the License.
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="de.dennisguse.opentracks" package="de.dennisguse.opentracks"
android:installLocation="auto" android:installLocation="auto"
android:versionCode="3079" android:versionCode="3162"
android:versionName="v3.2.2"> <!-- For FDroid. --> android:versionName="v3.2.8"> <!-- For FDroid. -->
<!-- Permission to initialize services --> <!-- Permission to initialize services -->
<application <application
android:name=".Startup" android:name=".Startup"
@@ -32,6 +32,10 @@ limitations under the License.
<activity android:name=".DeleteActivity" /> <activity android:name=".DeleteActivity" />
<activity
android:name=".HelpActivity"
android:label="@string/menu_help" />
<activity <activity
android:name=".io.file.importer.ImportActivity" android:name=".io.file.importer.ImportActivity"
android:theme="@style/ThemeCustomTransparent"> android:theme="@style/ThemeCustomTransparent">
@@ -50,10 +54,10 @@ limitations under the License.
</intent-filter> </intent-filter>
</activity> </activity>
<activity android:name=".MarkerEditActivity" />
<activity android:name=".MarkerDetailActivity" /> <activity android:name=".MarkerDetailActivity" />
<activity android:name=".MarkerEditActivity" />
<activity <activity
android:name=".MarkerListActivity" android:name=".MarkerListActivity"
android:label="@string/menu_markers"> android:label="@string/menu_markers">
@@ -79,7 +83,9 @@ limitations under the License.
<activity android:name=".TrackEditActivity" /> <activity android:name=".TrackEditActivity" />
<activity android:name=".TrackListActivity"> <activity
android:name=".TrackListActivity"
android:screenOrientation="userPortrait">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
@@ -124,6 +130,7 @@ limitations under the License.
android:name=".services.TrackRecordingService" android:name=".services.TrackRecordingService"
android:enabled="true" android:enabled="true"
android:exported="false" android:exported="false"
android:foregroundServiceType="location|connectedDevice"
android:icon="@drawable/ic_logo_color_24dp" android:icon="@drawable/ic_logo_color_24dp"
android:label="@string/recording_service" /> android:label="@string/recording_service" />
</application> </application>
@@ -17,11 +17,12 @@
package de.dennisguse.opentracks; package de.dennisguse.opentracks;
import android.os.Bundle; import android.os.Bundle;
import android.speech.tts.TextToSpeech;
import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar; import androidx.appcompat.widget.Toolbar;
import de.dennisguse.opentracks.util.TTSUtils;
/** /**
* @author Jimmy Shih * @author Jimmy Shih
*/ */
@@ -32,7 +33,7 @@ public abstract class AbstractActivity extends AppCompatActivity {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
// Set volume control stream for text to speech // Set volume control stream for text to speech
setVolumeControlStream(TextToSpeech.Engine.DEFAULT_STREAM); setVolumeControlStream(TTSUtils.getTTSStream());
setContentView(getLayoutResId()); setContentView(getLayoutResId());
@@ -65,7 +65,7 @@ public abstract class AbstractListActivity extends AbstractActivity implements C
if (trackIds.length == 1 && trackIds[0] == -1L) { if (trackIds.length == 1 && trackIds[0] == -1L) {
stopRecording = true; stopRecording = true;
} else { } else {
long recordingTrackId = PreferencesUtils.getLong(this, R.string.recording_track_id_key); long recordingTrackId = PreferencesUtils.getRecordingTrackId(this);
for (long trackId : trackIds) { for (long trackId : trackIds) {
if (trackId == recordingTrackId) { if (trackId == recordingTrackId) {
stopRecording = true; stopRecording = true;
@@ -69,7 +69,7 @@ public class DeleteAsyncTask extends AsyncTask<Void, Integer, Boolean> {
@Override @Override
protected Boolean doInBackground(Void... params) { protected Boolean doInBackground(Void... params) {
ContentProviderUtils contentProviderUtils = ContentProviderUtils.Factory.get(context); ContentProviderUtils contentProviderUtils = new ContentProviderUtils(context);
if (trackIds.length == 1 && trackIds[0] == -1L) { if (trackIds.length == 1 && trackIds[0] == -1L) {
contentProviderUtils.deleteAllTracks(context); 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; return;
} }
ContentProviderUtils contentProviderUtils = ContentProviderUtils.Factory.get(this); ContentProviderUtils contentProviderUtils = new ContentProviderUtils(this);
Waypoint waypoint = contentProviderUtils.getWaypoint(markerId); Waypoint waypoint = contentProviderUtils.getWaypoint(markerId);
markerIds = new ArrayList<>(); markerIds = new ArrayList<>();
@@ -127,7 +127,7 @@ public class MarkerEditActivity extends AbstractActivity {
if (newMarker) { if (newMarker) {
statisticsSection.setVisibility(View.GONE); statisticsSection.setVisibility(View.GONE);
waypointSection.setVisibility(View.VISIBLE); waypointSection.setVisibility(View.VISIBLE);
int nextWaypointNumber = trackId == -1L ? -1 : ContentProviderUtils.Factory.get(this).getNextWaypointNumber(trackId, WaypointType.WAYPOINT); int nextWaypointNumber = trackId == -1L ? -1 : new ContentProviderUtils(this).getNextWaypointNumber(trackId, WaypointType.WAYPOINT);
if (nextWaypointNumber == -1) { if (nextWaypointNumber == -1) {
nextWaypointNumber = 0; nextWaypointNumber = 0;
} }
@@ -136,7 +136,7 @@ public class MarkerEditActivity extends AbstractActivity {
waypointMarkerType.setText(""); waypointMarkerType.setText("");
waypointDescription.setText(""); waypointDescription.setText("");
} else { } else {
waypoint = ContentProviderUtils.Factory.get(this).getWaypoint(markerId); waypoint = new ContentProviderUtils(this).getWaypoint(markerId);
if (waypoint == null) { if (waypoint == null) {
Log.d(TAG, "waypoint is null"); Log.d(TAG, "waypoint is null");
finish(); finish();
@@ -182,6 +182,6 @@ public class MarkerEditActivity extends AbstractActivity {
waypoint.setCategory(waypointMarkerType.getText().toString()); waypoint.setCategory(waypointMarkerType.getText().toString());
waypoint.setDescription(waypointDescription.getText().toString()); waypoint.setDescription(waypointDescription.getText().toString());
} }
ContentProviderUtils.Factory.get(this).updateWaypoint(waypoint); new ContentProviderUtils(this).updateWaypoint(waypoint);
} }
} }
@@ -69,7 +69,7 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
private SharedPreferences sharedPreferences; private SharedPreferences sharedPreferences;
private long recordingTrackId = PreferencesUtils.RECORDING_TRACK_ID_DEFAULT; private long recordingTrackId = PreferencesUtils.RECORDING_TRACK_ID_DEFAULT;
private boolean recordingTrackPaused = PreferencesUtils.RECORDING_TRACK_PAUSED_DEFAULT; private boolean recordingTrackPaused;
/* /*
* Note that sharedPreferenceChangeListener cannot be an anonymous inner * Note that sharedPreferenceChangeListener cannot be an anonymous inner
* class. Anonymous inner class will get garbage collected. * class. Anonymous inner class will get garbage collected.
@@ -79,11 +79,11 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
@Override @Override
public void onSharedPreferenceChanged(SharedPreferences preferences, String key) { public void onSharedPreferenceChanged(SharedPreferences preferences, String key) {
// Note that the key can be null // Note that the key can be null
if (key == null || key.equals(PreferencesUtils.getKey(MarkerListActivity.this, R.string.recording_track_id_key))) { if (PreferencesUtils.isKey(MarkerListActivity.this, R.string.recording_track_id_key, key)) {
recordingTrackId = PreferencesUtils.getLong(MarkerListActivity.this, R.string.recording_track_id_key); recordingTrackId = PreferencesUtils.getRecordingTrackId(MarkerListActivity.this);
} }
if (key == null || key.equals(PreferencesUtils.getKey(MarkerListActivity.this, R.string.recording_track_paused_key))) { if (PreferencesUtils.isKey(MarkerListActivity.this, R.string.recording_track_paused_key, key)) {
recordingTrackPaused = PreferencesUtils.getBoolean(MarkerListActivity.this, R.string.recording_track_paused_key, PreferencesUtils.RECORDING_TRACK_PAUSED_DEFAULT); recordingTrackPaused = PreferencesUtils.isRecordingTrackPaused(MarkerListActivity.this);
} }
if (key != null) { if (key != null) {
runOnUiThread(new Runnable() { runOnUiThread(new Runnable() {
@@ -128,7 +128,9 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
contentProviderUtils = ContentProviderUtils.Factory.get(this); recordingTrackPaused = PreferencesUtils.isRecordingTrackPausedDefault(this);
contentProviderUtils = new ContentProviderUtils(this);
sharedPreferences = PreferencesUtils.getSharedPreferences(this); sharedPreferences = PreferencesUtils.getSharedPreferences(this);
long trackId = getIntent().getLongExtra(EXTRA_TRACK_ID, -1L); long trackId = getIntent().getLongExtra(EXTRA_TRACK_ID, -1L);
@@ -57,7 +57,6 @@ import de.dennisguse.opentracks.util.IntentUtils;
import de.dennisguse.opentracks.util.ListItemUtils; import de.dennisguse.opentracks.util.ListItemUtils;
import de.dennisguse.opentracks.util.PreferencesUtils; import de.dennisguse.opentracks.util.PreferencesUtils;
import de.dennisguse.opentracks.util.StringUtils; import de.dennisguse.opentracks.util.StringUtils;
import de.dennisguse.opentracks.util.ToolbarUtils;
import de.dennisguse.opentracks.util.TrackIconUtils; import de.dennisguse.opentracks.util.TrackIconUtils;
/** /**
@@ -102,7 +101,7 @@ public class SearchListActivity extends AbstractListActivity implements DeleteMa
private long recordingTrackId = PreferencesUtils.RECORDING_TRACK_ID_DEFAULT; private long recordingTrackId = PreferencesUtils.RECORDING_TRACK_ID_DEFAULT;
private boolean recordingTrackPaused = PreferencesUtils.RECORDING_TRACK_PAUSED_DEFAULT; private boolean recordingTrackPaused;
// Callback when an item is selected in the contextual action mode // Callback when an item is selected in the contextual action mode
private ContextualActionModeCallback contextualActionModeCallback = new ContextualActionModeCallback() { private ContextualActionModeCallback contextualActionModeCallback = new ContextualActionModeCallback() {
@@ -123,10 +122,6 @@ public class SearchListActivity extends AbstractListActivity implements DeleteMa
MenuItem shareMenuItem = menu.findItem(R.id.list_context_menu_share); MenuItem shareMenuItem = menu.findItem(R.id.list_context_menu_share);
if (isSingleSelectionTrack) { if (isSingleSelectionTrack) {
shareMenuItem.setVisible(!isRecording); shareMenuItem.setVisible(!isRecording);
Map<String, Object> item = arrayAdapter.getItem(positions[0]);
Long trackId = (Long) item.get(TRACK_ID_FIELD);
ToolbarUtils.setupShareActionProvider(SearchListActivity.this, shareMenuItem, new long[]{trackId});
} }
// One item, item is a marker // One item, item is a marker
@@ -148,14 +143,14 @@ public class SearchListActivity extends AbstractListActivity implements DeleteMa
private final OnSharedPreferenceChangeListener sharedPreferenceChangeListener = new OnSharedPreferenceChangeListener() { private final OnSharedPreferenceChangeListener sharedPreferenceChangeListener = new OnSharedPreferenceChangeListener() {
@Override @Override
public void onSharedPreferenceChanged(SharedPreferences preferences, String key) { public void onSharedPreferenceChanged(SharedPreferences preferences, String key) {
if (key == null || key.equals(PreferencesUtils.getKey(SearchListActivity.this, R.string.stats_units_key))) { if (PreferencesUtils.isKey(SearchListActivity.this, R.string.stats_units_key, key)) {
metricUnits = PreferencesUtils.isMetricUnits(SearchListActivity.this); metricUnits = PreferencesUtils.isMetricUnits(SearchListActivity.this);
} }
if (key == null || key.equals(PreferencesUtils.getKey(SearchListActivity.this, R.string.recording_track_id_key))) { if (PreferencesUtils.isKey(SearchListActivity.this, R.string.recording_track_id_key, key)) {
recordingTrackId = PreferencesUtils.getLong(SearchListActivity.this, R.string.recording_track_id_key); recordingTrackId = PreferencesUtils.getRecordingTrackId(SearchListActivity.this);
} }
if (key == null || key.equals(PreferencesUtils.getKey(SearchListActivity.this, R.string.recording_track_paused_key))) { if (PreferencesUtils.isKey(SearchListActivity.this, R.string.recording_track_paused_key, key)) {
recordingTrackPaused = PreferencesUtils.getBoolean(SearchListActivity.this, R.string.recording_track_paused_key, PreferencesUtils.RECORDING_TRACK_PAUSED_DEFAULT); recordingTrackPaused = PreferencesUtils.isRecordingTrackPaused(SearchListActivity.this);
} }
if (key != null) { if (key != null) {
runOnUiThread(new Runnable() { runOnUiThread(new Runnable() {
@@ -173,7 +168,9 @@ public class SearchListActivity extends AbstractListActivity implements DeleteMa
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
setDefaultKeyMode(DEFAULT_KEYS_SEARCH_LOCAL); setDefaultKeyMode(DEFAULT_KEYS_SEARCH_LOCAL);
contentProviderUtils = ContentProviderUtils.Factory.get(this); recordingTrackPaused = PreferencesUtils.isRecordingTrackPausedDefault(this);
contentProviderUtils = new ContentProviderUtils(this);
sharedPreferences = PreferencesUtils.getSharedPreferences(this); sharedPreferences = PreferencesUtils.getSharedPreferences(this);
trackRecordingServiceConnection = new TrackRecordingServiceConnection(this, null); trackRecordingServiceConnection = new TrackRecordingServiceConnection(this, null);
searchEngine = new SearchEngine(contentProviderUtils); searchEngine = new SearchEngine(contentProviderUtils);
@@ -290,6 +287,11 @@ public class SearchListActivity extends AbstractListActivity implements DeleteMa
case R.id.list_context_menu_show_on_map: case R.id.list_context_menu_show_on_map:
IntentUtils.showCoordinateOnMap(this, (double) item.get(MARKER_LATITUDE_FIELD), (double) item.get(MARKER_LONGITUDE_FIELD), item.get(NAME_FIELD) + ""); IntentUtils.showCoordinateOnMap(this, (double) item.get(MARKER_LATITUDE_FIELD), (double) item.get(MARKER_LONGITUDE_FIELD), item.get(NAME_FIELD) + "");
return true; return true;
case R.id.list_context_menu_share:
intent = IntentUtils.newShareFileIntent(this, new long[]{trackId});
intent = Intent.createChooser(intent, null);
startActivity(intent);
return true;
case R.id.list_context_menu_edit: case R.id.list_context_menu_edit:
if (markerId != null) { if (markerId != null) {
intent = IntentUtils.newIntent(this, MarkerEditActivity.class) intent = IntentUtils.newIntent(this, MarkerEditActivity.class)
@@ -24,6 +24,7 @@ import android.widget.TabHost;
import android.widget.TabHost.TabContentFactory; import android.widget.TabHost.TabContentFactory;
import android.widget.TabWidget; import android.widget.TabWidget;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity; import androidx.fragment.app.FragmentActivity;
import androidx.fragment.app.FragmentPagerAdapter; import androidx.fragment.app.FragmentPagerAdapter;
@@ -79,6 +80,7 @@ public class TabsAdapter extends FragmentPagerAdapter implements TabHost.OnTabCh
} }
@Override @Override
@NonNull
public Fragment getItem(int position) { public Fragment getItem(int position) {
TabInfo info = tabInfos.get(position); TabInfo info = tabInfos.get(position);
return Fragment.instantiate(context, info.clss.getName(), info.bundle); return Fragment.instantiate(context, info.clss.getName(), info.bundle);
@@ -45,17 +45,17 @@ import de.dennisguse.opentracks.content.TrackDataHub;
import de.dennisguse.opentracks.content.Waypoint; import de.dennisguse.opentracks.content.Waypoint;
import de.dennisguse.opentracks.content.Waypoint.WaypointType; import de.dennisguse.opentracks.content.Waypoint.WaypointType;
import de.dennisguse.opentracks.content.WaypointCreationRequest; import de.dennisguse.opentracks.content.WaypointCreationRequest;
import de.dennisguse.opentracks.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.ChooseActivityTypeDialogFragment;
import de.dennisguse.opentracks.fragments.ConfirmDeleteDialogFragment; import de.dennisguse.opentracks.fragments.ConfirmDeleteDialogFragment;
import de.dennisguse.opentracks.fragments.FrequencyDialogFragment;
import de.dennisguse.opentracks.fragments.StatsFragment; import de.dennisguse.opentracks.fragments.StatsFragment;
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection; import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
import de.dennisguse.opentracks.settings.SettingsActivity; import de.dennisguse.opentracks.settings.SettingsActivity;
import de.dennisguse.opentracks.util.FileUtils; import de.dennisguse.opentracks.util.FileUtils;
import de.dennisguse.opentracks.util.IntentUtils; import de.dennisguse.opentracks.util.IntentUtils;
import de.dennisguse.opentracks.util.PreferencesUtils; import de.dennisguse.opentracks.util.PreferencesUtils;
import de.dennisguse.opentracks.util.ToolbarUtils; import de.dennisguse.opentracks.util.StatsUtils;
import de.dennisguse.opentracks.util.TrackIconUtils; import de.dennisguse.opentracks.util.TrackIconUtils;
import de.dennisguse.opentracks.util.TrackUtils; import de.dennisguse.opentracks.util.TrackUtils;
@@ -95,8 +95,7 @@ public class TrackDetailActivity extends AbstractListActivity implements ChooseA
// Preferences // Preferences
private long recordingTrackId = PreferencesUtils.RECORDING_TRACK_ID_DEFAULT; private long recordingTrackId = PreferencesUtils.RECORDING_TRACK_ID_DEFAULT;
private boolean recordingTrackPaused = PreferencesUtils.RECORDING_TRACK_PAUSED_DEFAULT; private boolean recordingTrackPaused;
private boolean showOnLockScreen = PreferencesUtils.SHOW_TRACKDETAIL_WHILE_RECORDING_ON_LOCKSCREEN;
private final Runnable bindChangedCallback = new Runnable() { private final Runnable bindChangedCallback = new Runnable() {
@Override @Override
@@ -123,20 +122,18 @@ public class TrackDetailActivity extends AbstractListActivity implements ChooseA
* Note that sharedPreferenceChangeListener cannot be an anonymous inner * Note that sharedPreferenceChangeListener cannot be an anonymous inner
* class. Anonymous inner class will get garbage collected. * class. Anonymous inner class will get garbage collected.
*/ */
private final OnSharedPreferenceChangeListener private final OnSharedPreferenceChangeListener sharedPreferenceChangeListener = new OnSharedPreferenceChangeListener() {
sharedPreferenceChangeListener = new OnSharedPreferenceChangeListener() {
@Override @Override
public void onSharedPreferenceChanged(SharedPreferences preferences, String key) { public void onSharedPreferenceChanged(SharedPreferences preferences, String key) {
if (key == null || key.equals(PreferencesUtils.getKey(TrackDetailActivity.this, R.string.recording_track_id_key))) { if (PreferencesUtils.isKey(TrackDetailActivity.this, R.string.recording_track_id_key, key)) {
recordingTrackId = PreferencesUtils.getLong(TrackDetailActivity.this, R.string.recording_track_id_key); recordingTrackId = PreferencesUtils.getRecordingTrackId(TrackDetailActivity.this);
} }
if (key == null || key.equals(PreferencesUtils.getKey(TrackDetailActivity.this, R.string.recording_track_paused_key))) { if (PreferencesUtils.isKey(TrackDetailActivity.this, R.string.recording_track_paused_key, key)) {
recordingTrackPaused = PreferencesUtils.getBoolean(TrackDetailActivity.this, R.string.recording_track_paused_key, PreferencesUtils.RECORDING_TRACK_PAUSED_DEFAULT); recordingTrackPaused = PreferencesUtils.isRecordingTrackPaused(TrackDetailActivity.this);
} }
if (key == null || key.equals(PreferencesUtils.getKey(TrackDetailActivity.this, R.string.trackdetail_show_on_lockscreen_while_recording_key))) { if (PreferencesUtils.isKey(TrackDetailActivity.this, R.string.stats_show_on_lockscreen_while_recording_key, key)) {
showOnLockScreen = PreferencesUtils.getBoolean(TrackDetailActivity.this, R.string.trackdetail_show_on_lockscreen_while_recording_key, PreferencesUtils.SHOW_TRACKDETAIL_WHILE_RECORDING_ON_LOCKSCREEN);
setLockscreenPolicy(); setLockscreenPolicy();
} }
@@ -157,8 +154,6 @@ public class TrackDetailActivity extends AbstractListActivity implements ChooseA
private MenuItem insertPhotoMenuItem; private MenuItem insertPhotoMenuItem;
private MenuItem markerListMenuItem; private MenuItem markerListMenuItem;
private MenuItem shareMenuItem; private MenuItem shareMenuItem;
private MenuItem voiceFrequencyMenuItem;
private MenuItem splitFrequencyMenuItem;
private final OnClickListener recordListener = new OnClickListener() { private final OnClickListener recordListener = new OnClickListener() {
@Override @Override
@@ -188,11 +183,13 @@ public class TrackDetailActivity extends AbstractListActivity implements ChooseA
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
recordingTrackPaused = PreferencesUtils.isRecordingTrackPausedDefault(this);
hasCamera = getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA); hasCamera = getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA);
photoUri = savedInstanceState != null ? (Uri) savedInstanceState.getParcelable(PHOTO_URI_KEY) : null; photoUri = savedInstanceState != null ? (Uri) savedInstanceState.getParcelable(PHOTO_URI_KEY) : null;
hasPhoto = savedInstanceState != null && savedInstanceState.getBoolean(HAS_PHOTO_KEY, false); hasPhoto = savedInstanceState != null && savedInstanceState.getBoolean(HAS_PHOTO_KEY, false);
contentProviderUtils = ContentProviderUtils.Factory.get(this); contentProviderUtils = new ContentProviderUtils(this);
handleIntent(getIntent()); handleIntent(getIntent());
sharedPreferences = PreferencesUtils.getSharedPreferences(this); sharedPreferences = PreferencesUtils.getSharedPreferences(this);
@@ -209,8 +206,11 @@ public class TrackDetailActivity extends AbstractListActivity implements ChooseA
TabSpec statsTabSpec = tabHost.newTabSpec(StatsFragment.STATS_FRAGMENT_TAG).setIndicator(getString(R.string.track_detail_stats_tab)); TabSpec statsTabSpec = tabHost.newTabSpec(StatsFragment.STATS_FRAGMENT_TAG).setIndicator(getString(R.string.track_detail_stats_tab));
tabsAdapter.addTab(statsTabSpec, StatsFragment.class, null); tabsAdapter.addTab(statsTabSpec, StatsFragment.class, null);
TabSpec chartTabSpec = tabHost.newTabSpec(ChartFragment.CHART_FRAGMENT_TAG).setIndicator(getString(R.string.track_detail_chart_tab)); TabSpec chartTimeTabSpec = tabHost.newTabSpec(ChartTimeFragment.CHART_FRAGMENT_TAG).setIndicator(getString(R.string.settings_chart_by_time));
tabsAdapter.addTab(chartTabSpec, ChartFragment.class, null); 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) { if (savedInstanceState != null) {
tabHost.setCurrentTabByTag(savedInstanceState.getString(CURRENT_TAB_TAG_KEY)); tabHost.setCurrentTabByTag(savedInstanceState.getString(CURRENT_TAB_TAG_KEY));
@@ -231,12 +231,14 @@ public class TrackDetailActivity extends AbstractListActivity implements ChooseA
} }
private void setLockscreenPolicy() { private void setLockscreenPolicy() {
boolean showOnLockScreen = PreferencesUtils.shouldShowStatsOnLockscreen(TrackDetailActivity.this);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
setShowWhenLocked(showOnLockScreen); setShowWhenLocked(showOnLockScreen);
} else if (showOnLockScreen) {
getWindow().addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED);
} else { } else {
if (showOnLockScreen) getWindow().clearFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED);
getWindow().addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED);
else getWindow().clearFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED);
} }
} }
@@ -276,7 +278,7 @@ public class TrackDetailActivity extends AbstractListActivity implements ChooseA
} }
@Override @Override
protected void onSaveInstanceState(Bundle outState) { protected void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState); super.onSaveInstanceState(outState);
outState.putString(CURRENT_TAB_TAG_KEY, tabHost.getCurrentTabTag()); outState.putString(CURRENT_TAB_TAG_KEY, tabHost.getCurrentTabTag());
if (photoUri != null) { if (photoUri != null) {
@@ -316,12 +318,7 @@ public class TrackDetailActivity extends AbstractListActivity implements ChooseA
insertMarkerMenuItem = menu.findItem(R.id.track_detail_insert_marker); insertMarkerMenuItem = menu.findItem(R.id.track_detail_insert_marker);
insertPhotoMenuItem = menu.findItem(R.id.track_detail_insert_photo); insertPhotoMenuItem = menu.findItem(R.id.track_detail_insert_photo);
insertPhotoMenuItem.setVisible(new Intent(MediaStore.ACTION_IMAGE_CAPTURE).resolveActivity(getPackageManager()) != null); insertPhotoMenuItem.setVisible(new Intent(MediaStore.ACTION_IMAGE_CAPTURE).resolveActivity(getPackageManager()) != null);
shareMenuItem = menu.findItem(R.id.track_detail_share); shareMenuItem = menu.findItem(R.id.track_detail_share);
ToolbarUtils.setupShareActionProviderCompat(this, shareMenuItem, new long[]{trackId});
voiceFrequencyMenuItem = menu.findItem(R.id.track_detail_voice_frequency);
splitFrequencyMenuItem = menu.findItem(R.id.track_detail_split_frequency);
markerListMenuItem = menu.findItem(R.id.track_detail_markers); markerListMenuItem = menu.findItem(R.id.track_detail_markers);
return super.onCreateOptionsMenu(menu); return super.onCreateOptionsMenu(menu);
@@ -343,6 +340,11 @@ public class TrackDetailActivity extends AbstractListActivity implements ChooseA
.putExtra(MarkerEditActivity.EXTRA_TRACK_ID, trackId); .putExtra(MarkerEditActivity.EXTRA_TRACK_ID, trackId);
startActivity(intent); startActivity(intent);
return true; return true;
case R.id.track_detail_share:
intent = IntentUtils.newShareFileIntent(this, new long[]{trackId});
intent = Intent.createChooser(intent, null);
startActivity(intent);
return true;
case R.id.track_detail_insert_photo: case R.id.track_detail_insert_photo:
if (!FileUtils.isExternalStorageWriteable() || ActivityCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) { if (!FileUtils.isExternalStorageWriteable() || ActivityCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) {
ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, EXTERNAL_STORAGE_PERMISSION_REQUEST_CODE); ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, EXTERNAL_STORAGE_PERMISSION_REQUEST_CODE);
@@ -358,12 +360,6 @@ public class TrackDetailActivity extends AbstractListActivity implements ChooseA
.putExtra(MarkerListActivity.EXTRA_TRACK_ID, trackId); .putExtra(MarkerListActivity.EXTRA_TRACK_ID, trackId);
startActivity(intent); startActivity(intent);
return true; return true;
case R.id.track_detail_voice_frequency:
FrequencyDialogFragment.showDialog(getSupportFragmentManager(), R.string.voice_frequency_key, PreferencesUtils.VOICE_FREQUENCY_DEFAULT, R.string.menu_voice_frequency);
return true;
case R.id.track_detail_split_frequency:
FrequencyDialogFragment.showDialog(getSupportFragmentManager(), R.string.split_frequency_key, PreferencesUtils.SPLIT_FREQUENCY_DEFAULT, R.string.menu_split_frequency);
return true;
case R.id.track_detail_edit: case R.id.track_detail_edit:
intent = IntentUtils.newIntent(this, TrackEditActivity.class) intent = IntentUtils.newIntent(this, TrackEditActivity.class)
.putExtra(TrackEditActivity.EXTRA_TRACK_ID, trackId); .putExtra(TrackEditActivity.EXTRA_TRACK_ID, trackId);
@@ -420,10 +416,16 @@ public class TrackDetailActivity extends AbstractListActivity implements ChooseA
/** /**
* Gets the track id. * Gets the track id.
*/ */
// TODO Seems to be no used.
@Deprecated
public long getTrackId() { public long getTrackId() {
return trackId; return trackId;
} }
private void setBluetoothHeartRateSensorName(String sensorName) {
StatsUtils.setBluetoothHeartRateSensorName(this, sensorName, isRecording());
}
/** /**
* Handles the data in the intent. * Handles the data in the intent.
*/ */
@@ -465,8 +467,6 @@ public class TrackDetailActivity extends AbstractListActivity implements ChooseA
insertPhotoMenuItem.setVisible(hasCamera && isRecording() && !isPaused); insertPhotoMenuItem.setVisible(hasCamera && isRecording() && !isPaused);
shareMenuItem.setVisible(!isRecording()); shareMenuItem.setVisible(!isRecording());
markerListMenuItem.setShowAsAction(isRecording() ? MenuItem.SHOW_AS_ACTION_NEVER : MenuItem.SHOW_AS_ACTION_IF_ROOM); markerListMenuItem.setShowAsAction(isRecording() ? MenuItem.SHOW_AS_ACTION_NEVER : MenuItem.SHOW_AS_ACTION_IF_ROOM);
voiceFrequencyMenuItem.setVisible(isRecording());
splitFrequencyMenuItem.setVisible(isRecording());
String title; String title;
if (isRecording()) { if (isRecording()) {
title = getString(isPaused ? R.string.generic_paused : R.string.generic_recording); title = getString(isPaused ? R.string.generic_paused : R.string.generic_recording);
@@ -73,7 +73,7 @@ public class TrackEditActivity extends AbstractActivity implements ChooseActivit
return; return;
} }
contentProviderUtils = ContentProviderUtils.Factory.get(this); contentProviderUtils = new ContentProviderUtils(this);
track = contentProviderUtils.getTrack(trackId); track = contentProviderUtils.getTrack(trackId);
if (track == null) { if (track == null) {
Log.e(TAG, "No track for " + trackId); Log.e(TAG, "No track for " + trackId);
@@ -60,8 +60,8 @@ import de.dennisguse.opentracks.util.ActivityUtils;
import de.dennisguse.opentracks.util.IntentUtils; import de.dennisguse.opentracks.util.IntentUtils;
import de.dennisguse.opentracks.util.ListItemUtils; import de.dennisguse.opentracks.util.ListItemUtils;
import de.dennisguse.opentracks.util.PreferencesUtils; import de.dennisguse.opentracks.util.PreferencesUtils;
import de.dennisguse.opentracks.util.ServiceUtils;
import de.dennisguse.opentracks.util.StringUtils; import de.dennisguse.opentracks.util.StringUtils;
import de.dennisguse.opentracks.util.ToolbarUtils;
import de.dennisguse.opentracks.util.TrackIconUtils; import de.dennisguse.opentracks.util.TrackIconUtils;
import de.dennisguse.opentracks.util.TrackUtils; import de.dennisguse.opentracks.util.TrackUtils;
@@ -115,9 +115,6 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
public void onPrepare(Menu menu, int[] positions, long[] trackIds, boolean showSelectAll) { public void onPrepare(Menu menu, int[] positions, long[] trackIds, boolean showSelectAll) {
boolean isSingleSelection = trackIds.length == 1; boolean isSingleSelection = trackIds.length == 1;
MenuItem shareMenuItem = menu.findItem(R.id.list_context_menu_share);
ToolbarUtils.setupShareActionProvider(TrackListActivity.this, shareMenuItem, trackIds);
menu.findItem(R.id.list_context_menu_edit).setVisible(isSingleSelection); menu.findItem(R.id.list_context_menu_edit).setVisible(isSingleSelection);
menu.findItem(R.id.list_context_menu_select_all).setVisible(showSelectAll); menu.findItem(R.id.list_context_menu_select_all).setVisible(showSelectAll);
} }
@@ -128,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. * Note that sharedPreferenceChangeListener cannot be an anonymous inner class.
@@ -138,22 +135,17 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
sharedPreferenceChangeListener = new OnSharedPreferenceChangeListener() { sharedPreferenceChangeListener = new OnSharedPreferenceChangeListener() {
@Override @Override
public void onSharedPreferenceChanged(SharedPreferences preferences, String key) { public void onSharedPreferenceChanged(SharedPreferences preferences, String key) {
if (key == null || key.equals(PreferencesUtils.getKey(TrackListActivity.this, R.string.stats_units_key))) { if (PreferencesUtils.isKey(TrackListActivity.this, R.string.stats_units_key, key)) {
metricUnits = PreferencesUtils.isMetricUnits(TrackListActivity.this); metricUnits = PreferencesUtils.isMetricUnits(TrackListActivity.this);
} }
if (key == null || key.equals( if (PreferencesUtils.isKey(TrackListActivity.this, R.string.recording_track_id_key, key)) {
PreferencesUtils.getKey(TrackListActivity.this, R.string.recording_track_id_key))) { recordingTrackId = PreferencesUtils.getRecordingTrackId(TrackListActivity.this);
recordingTrackId = PreferencesUtils.getLong(
TrackListActivity.this, R.string.recording_track_id_key);
if (key != null && PreferencesUtils.isRecording(recordingTrackId)) { if (key != null && PreferencesUtils.isRecording(recordingTrackId)) {
trackRecordingServiceConnection.startAndBind(); trackRecordingServiceConnection.startAndBind();
} }
} }
if (key == null || key.equals(PreferencesUtils.getKey( if (PreferencesUtils.isKey(TrackListActivity.this, R.string.recording_track_paused_key, key)) {
TrackListActivity.this, R.string.recording_track_paused_key))) { recordingTrackPaused = PreferencesUtils.isRecordingTrackPaused(TrackListActivity.this);
recordingTrackPaused = PreferencesUtils.getBoolean(TrackListActivity.this,
R.string.recording_track_paused_key,
PreferencesUtils.RECORDING_TRACK_PAUSED_DEFAULT);
} }
if (key != null) { if (key != null) {
runOnUiThread(new Runnable() { runOnUiThread(new Runnable() {
@@ -229,12 +221,11 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
private final OnClickListener recordListener = new OnClickListener() { private final OnClickListener recordListener = new OnClickListener() {
public void onClick(View v) { public void onClick(View v) {
if (recordingTrackId == PreferencesUtils.RECORDING_TRACK_ID_DEFAULT) { if (!PreferencesUtils.isRecording(recordingTrackId)) {
// Not recording -> Recording // Not recording -> Recording
updateMenuItems(false, true); updateMenuItems(false, true);
startRecording(); startRecording();
} else { } else if (recordingTrackPaused) {
if (recordingTrackPaused) {
// Paused -> Resume // Paused -> Resume
updateMenuItems(false, true); updateMenuItems(false, true);
trackRecordingServiceConnection.resumeTrack(); trackRecordingServiceConnection.resumeTrack();
@@ -245,7 +236,6 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
trackRecordingServiceConnection.pauseTrack(); trackRecordingServiceConnection.pauseTrack();
trackController.update(true, true); trackController.update(true, true);
} }
}
} }
}; };
@@ -253,7 +243,9 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
contentProviderUtils = ContentProviderUtils.Factory.get(this); recordingTrackPaused = PreferencesUtils.isRecordingTrackPausedDefault(this);
contentProviderUtils = new ContentProviderUtils(this);
sharedPreferences = PreferencesUtils.getSharedPreferences(this); sharedPreferences = PreferencesUtils.getSharedPreferences(this);
trackRecordingServiceConnection = new TrackRecordingServiceConnection(this, bindChangedCallback); trackRecordingServiceConnection = new TrackRecordingServiceConnection(this, bindChangedCallback);
@@ -386,7 +378,7 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
@Override @Override
public boolean onPrepareOptionsMenu(Menu menu) { public boolean onPrepareOptionsMenu(Menu menu) {
boolean isGpsStarted = TrackRecordingServiceConnection.isRecordingServiceRunning(this); boolean isGpsStarted = ServiceUtils.isTrackRecordingServiceRunning(this);
boolean isRecording = PreferencesUtils.isRecording(recordingTrackId); boolean isRecording = PreferencesUtils.isRecording(recordingTrackId);
updateMenuItems(isGpsStarted, isRecording); updateMenuItems(isGpsStarted, isRecording);
@@ -406,7 +398,7 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
intent = new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS); intent = new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS);
startActivity(intent); startActivity(intent);
} else { } else {
startGps = !TrackRecordingServiceConnection.isRecordingServiceRunning(this); startGps = !ServiceUtils.isTrackRecordingServiceRunning(this);
// Show toast // Show toast
Toast toast = Toast.makeText(this, startGps ? R.string.gps_starting : R.string.gps_stopping, Toast.LENGTH_SHORT); Toast toast = Toast.makeText(this, startGps ? R.string.gps_starting : R.string.gps_stopping, Toast.LENGTH_SHORT);
@@ -531,12 +523,18 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
* @return true if handled. * @return true if handled.
*/ */
private boolean handleContextItem(int itemId, long[] trackIds) { private boolean handleContextItem(int itemId, long[] trackIds) {
Intent intent;
switch (itemId) { switch (itemId) {
case R.id.list_context_menu_show_on_map: case R.id.list_context_menu_show_on_map:
IntentUtils.showTrackOnMap(this, trackIds); IntentUtils.showTrackOnMap(this, trackIds);
return true; return true;
case R.id.list_context_menu_share:
intent = IntentUtils.newShareFileIntent(this, trackIds);
intent = Intent.createChooser(intent, null);
startActivity(intent);
return true;
case R.id.list_context_menu_edit: case R.id.list_context_menu_edit:
Intent intent = IntentUtils.newIntent(this, TrackEditActivity.class) intent = IntentUtils.newIntent(this, TrackEditActivity.class)
.putExtra(TrackEditActivity.EXTRA_TRACK_ID, trackIds[0]); .putExtra(TrackEditActivity.EXTRA_TRACK_ID, trackIds[0]);
startActivity(intent); startActivity(intent);
return true; return true;
@@ -14,7 +14,7 @@
* the License. * the License.
*/ */
package de.dennisguse.opentracks; package de.dennisguse.opentracks.chart;
import android.content.Context; import android.content.Context;
import android.graphics.Canvas; import android.graphics.Canvas;
@@ -14,7 +14,7 @@
* the License. * the License.
*/ */
package de.dennisguse.opentracks; package de.dennisguse.opentracks.chart;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
@@ -27,15 +27,19 @@ import android.graphics.Paint.Style;
import android.graphics.Path; import android.graphics.Path;
import android.graphics.Rect; import android.graphics.Rect;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.view.GestureDetector;
import android.view.MotionEvent; import android.view.MotionEvent;
import android.view.VelocityTracker; import android.view.ScaleGestureDetector;
import android.view.View; import android.view.View;
import android.view.ViewConfiguration;
import android.widget.Scroller; import android.widget.Scroller;
import androidx.core.view.GestureDetectorCompat;
import java.text.NumberFormat; import java.text.NumberFormat;
import java.util.ArrayList; import java.util.ArrayList;
import de.dennisguse.opentracks.MarkerDetailActivity;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.Waypoint; import de.dennisguse.opentracks.content.Waypoint;
import de.dennisguse.opentracks.stats.ExtremityMonitor; import de.dennisguse.opentracks.stats.ExtremityMonitor;
import de.dennisguse.opentracks.util.IntentUtils; import de.dennisguse.opentracks.util.IntentUtils;
@@ -44,6 +48,7 @@ import de.dennisguse.opentracks.util.UnitConversions;
/** /**
* Visualization of the chart. * Visualization of the chart.
* Provides support for zooming (via pinch), scrolling, flinging, and selecting shown markers (single touch).
* *
* @author Sandor Dornbush * @author Sandor Dornbush
* @author Leif Hendrik Wilden * @author Leif Hendrik Wilden
@@ -91,8 +96,6 @@ public class ChartView extends View {
private final int markerHeight; private final int markerHeight;
private final Scroller scroller; private final Scroller scroller;
private double maxX = 1.0; private double maxX = 1.0;
private VelocityTracker velocityTracker = null;
private float lastMotionEventX = -1;
private int zoomLevel = 1; private int zoomLevel = 1;
private int leftBorder = BORDER; private int leftBorder = BORDER;
@@ -107,18 +110,84 @@ public class ChartView extends View {
private int effectiveWidth = 0; private int effectiveWidth = 0;
private int effectiveHeight = 0; private int effectiveHeight = 0;
private boolean chartByDistance = true; private boolean chartByDistance;
private boolean metricUnits = true; private boolean metricUnits = true;
private boolean reportSpeed = true; private boolean reportSpeed = true;
private boolean showPointer = false; private boolean showPointer = false;
/** private GestureDetectorCompat detectorScrollFlingTab = new GestureDetectorCompat(getContext(), new GestureDetector.SimpleOnGestureListener() {
* Constructor.
* @Override
* @param context the context public boolean onDown(MotionEvent e) {
*/ if (!scroller.isFinished()) {
public ChartView(Context context) { 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 / 2) < 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); super(context);
this.chartByDistance = chartByDistance;
series[ELEVATION_SERIES] = new ChartValueSeries(context, series[ELEVATION_SERIES] = new ChartValueSeries(context,
Integer.MIN_VALUE, Integer.MIN_VALUE,
@@ -222,16 +291,6 @@ public class ChartView extends View {
series[index].setEnabled(enabled); 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. * Sets metric units.
* *
@@ -410,79 +469,14 @@ public class ChartView extends View {
@Override @Override
public boolean onTouchEvent(MotionEvent event) { public boolean onTouchEvent(MotionEvent event) {
if (velocityTracker == null) { boolean isZoom = detectorZoom.onTouchEvent(event);
velocityTracker = VelocityTracker.obtain(); boolean isScrollTab = detectorScrollFlingTab.onTouchEvent(event);
} return isZoom || isScrollTab;
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;
} }
@Override @Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
updateEffectiveDimensionsIfChanged( updateEffectiveDimensionsIfChanged(View.MeasureSpec.getSize(widthMeasureSpec), View.MeasureSpec.getSize(heightMeasureSpec));
View.MeasureSpec.getSize(widthMeasureSpec), View.MeasureSpec.getSize(heightMeasureSpec));
super.onMeasure(widthMeasureSpec, heightMeasureSpec); super.onMeasure(widthMeasureSpec, heightMeasureSpec);
} }
@@ -632,8 +626,7 @@ public class ChartView extends View {
private String getXAxisLabel() { private String getXAxisLabel() {
Context context = getContext(); Context context = getContext();
if (chartByDistance) { if (chartByDistance) {
return metricUnits ? context.getString(R.string.unit_kilometer) return metricUnits ? context.getString(R.string.unit_kilometer) : context.getString(R.string.unit_mile);
: context.getString(R.string.unit_mile);
} else { } else {
return context.getString(R.string.description_time); return context.getString(R.string.description_time);
} }
@@ -733,8 +726,7 @@ public class ChartView extends View {
* @param yValue the y value * @param yValue the y value
* @return the marker width. * @return the marker width.
*/ */
private float drawYAxisMarker( private float drawYAxisMarker(ChartValueSeries chartValueSeries, Canvas canvas, int xPosition, int yValue) {
ChartValueSeries chartValueSeries, Canvas canvas, int xPosition, int yValue) {
String marker = chartValueSeries.formatMarker(yValue); String marker = chartValueSeries.formatMarker(yValue);
Paint paint = chartValueSeries.getMarkerPaint(); Paint paint = chartValueSeries.getMarkerPaint();
Rect rect = getRect(paint, marker); Rect rect = getRect(paint, marker);
@@ -869,8 +861,7 @@ public class ChartView extends View {
topBorder = (int) (density * BORDER + titleDimensions[0] * (titleDimensions[1] + spacer)); topBorder = (int) (density * BORDER + titleDimensions[0] * (titleDimensions[1] + spacer));
Rect xAxisLabelRect = getRect(axisPaint, getXAxisLabel()); Rect xAxisLabelRect = getRect(axisPaint, getXAxisLabel());
// border + x axis marker + spacer + .5 x axis label // border + x axis marker + spacer + .5 x axis label
bottomBorder = (int) (density * BORDER + getRect(xAxisMarkerPaint, "1").height() + spacer bottomBorder = (int) (density * BORDER + getRect(xAxisMarkerPaint, "1").height() + spacer + (xAxisLabelRect.height() / 2));
+ (xAxisLabelRect.height() / 2));
rightBorder = (int) (density * BORDER + xAxisLabelRect.width() + spacer); rightBorder = (int) (density * BORDER + xAxisLabelRect.width() + spacer);
updateEffectiveDimensions(); 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();
}
@@ -28,11 +28,6 @@ public class SensorDataSetLocation extends Location {
private SensorDataSet sensorDataSet = null; private SensorDataSet sensorDataSet = null;
/**
* The id of this location from the provider.
*/
private int id = -1;
public SensorDataSetLocation(Location location, SensorDataSet sensorDataSet) { public SensorDataSetLocation(Location location, SensorDataSet sensorDataSet) {
super(location); super(location);
this.sensorDataSet = sensorDataSet; this.sensorDataSet = sensorDataSet;
@@ -50,17 +45,8 @@ public class SensorDataSetLocation extends Location {
this.sensorDataSet = sensorDataSet; this.sensorDataSet = sensorDataSet;
} }
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public void reset() { public void reset() {
super.reset(); super.reset();
sensorDataSet = null; sensorDataSet = null;
id = -1;
} }
} }
@@ -198,7 +198,7 @@ public class ShareContentProvider extends CustomContentProvider implements ICont
@Nullable @Nullable
@Override @Override
public ParcelFileDescriptor openFile(@NonNull Uri uri, @NonNull String mode) throws FileNotFoundException { 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); long[] trackIds = parseURI(uri);
final Track[] tracks = new Track[trackIds.length]; final Track[] tracks = new Track[trackIds.length];
@@ -30,7 +30,6 @@ import java.util.EnumSet;
import java.util.Set; import java.util.Set;
import de.dennisguse.opentracks.R; import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.ContentProviderUtils.LocationIterator;
import de.dennisguse.opentracks.util.LocationUtils; import de.dennisguse.opentracks.util.LocationUtils;
import de.dennisguse.opentracks.util.PreferencesUtils; import de.dennisguse.opentracks.util.PreferencesUtils;
@@ -45,7 +44,7 @@ public class TrackDataHub implements DataSourceListener {
* Target number of track points displayed by the map overlay. * Target number of track points displayed by the map overlay.
* We may display more than this number of points. * We may display more than this number of points.
*/ */
static final int TARGET_DISPLAYED_TRACK_POINTS = 5000; private static final int TARGET_DISPLAYED_TRACK_POINTS = 5000;
/** /**
* Maximum number of waypoints to displayed. * Maximum number of waypoints to displayed.
@@ -97,8 +96,7 @@ public class TrackDataHub implements DataSourceListener {
} }
public synchronized static TrackDataHub newInstance(Context context) { public synchronized static TrackDataHub newInstance(Context context) {
return new TrackDataHub(context, new TrackDataManager(), ContentProviderUtils.Factory.get( return new TrackDataHub(context, new TrackDataManager(), new ContentProviderUtils(context), TARGET_DISPLAYED_TRACK_POINTS);
context), TARGET_DISPLAYED_TRACK_POINTS);
} }
public void start() { public void start() {
@@ -254,13 +252,13 @@ public class TrackDataHub implements DataSourceListener {
runInHandlerThread(new Runnable() { runInHandlerThread(new Runnable() {
@Override @Override
public void run() { public void run() {
if (key == null || key.equals(PreferencesUtils.getKey(context, R.string.recording_track_id_key))) { if (PreferencesUtils.isKey(context, R.string.recording_track_id_key, key)) {
recordingTrackId = PreferencesUtils.getLong(context, R.string.recording_track_id_key); recordingTrackId = PreferencesUtils.getRecordingTrackId(context);
} }
if (key == null || key.equals(PreferencesUtils.getKey(context, R.string.recording_track_paused_key))) { if (PreferencesUtils.isKey(context, R.string.recording_track_paused_key, key)) {
recordingTrackPaused = PreferencesUtils.getBoolean(context, R.string.recording_track_paused_key, PreferencesUtils.RECORDING_TRACK_PAUSED_DEFAULT); recordingTrackPaused = PreferencesUtils.isRecordingTrackPaused(context);
} }
if (key == null || key.equals(PreferencesUtils.getKey(context, R.string.stats_units_key))) { if (PreferencesUtils.isKey(context, R.string.stats_units_key, key)) {
metricUnits = PreferencesUtils.isMetricUnits(context); metricUnits = PreferencesUtils.isMetricUnits(context);
if (key != null) { if (key != null) {
for (TrackDataListener trackDataListener : trackDataManager.getListeners(TrackDataType.PREFERENCE)) { for (TrackDataListener trackDataListener : trackDataManager.getListeners(TrackDataType.PREFERENCE)) {
@@ -270,7 +268,7 @@ public class TrackDataHub implements DataSourceListener {
} }
} }
} }
if (key == null || key.equals(PreferencesUtils.getKey(context, R.string.stats_rate_key))) { if (PreferencesUtils.isKey(context, R.string.stats_rate_key, key)) {
reportSpeed = PreferencesUtils.isReportSpeed(context); reportSpeed = PreferencesUtils.isReportSpeed(context);
if (key != null) { if (key != null) {
for (TrackDataListener trackDataListener : trackDataManager.getListeners(TrackDataType.PREFERENCE)) { for (TrackDataListener trackDataListener : trackDataManager.getListeners(TrackDataType.PREFERENCE)) {
@@ -280,8 +278,8 @@ public class TrackDataHub implements DataSourceListener {
} }
} }
} }
if (key == null || key.equals(PreferencesUtils.getKey(context, R.string.recording_gps_accuracy_key))) { if (PreferencesUtils.isKey(context, R.string.recording_gps_accuracy_key, key)) {
recordingGpsAccuracy = PreferencesUtils.getInt(context, R.string.recording_gps_accuracy_key, PreferencesUtils.RECORDING_GPS_ACCURACY_DEFAULT); recordingGpsAccuracy = PreferencesUtils.getRecordingGPSAccuracy(context);
if (key != null) { if (key != null) {
for (TrackDataListener trackDataListener : trackDataManager.getListeners(TrackDataType.PREFERENCE)) { for (TrackDataListener trackDataListener : trackDataManager.getListeners(TrackDataType.PREFERENCE)) {
if (trackDataListener.onRecordingGpsAccuracy(recordingGpsAccuracy)) { if (trackDataListener.onRecordingGpsAccuracy(recordingGpsAccuracy)) {
@@ -290,8 +288,8 @@ public class TrackDataHub implements DataSourceListener {
} }
} }
} }
if (key == null || key.equals(PreferencesUtils.getKey(context, R.string.recording_distance_interval_key))) { if (PreferencesUtils.isKey(context, R.string.recording_distance_interval_key, key)) {
recordingDistanceInterval = PreferencesUtils.getInt(context, R.string.recording_distance_interval_key, PreferencesUtils.RECORDING_DISTANCE_INTERVAL_DEFAULT); recordingDistanceInterval = PreferencesUtils.getRecordingDistanceInterval(context);
if (key != null) { if (key != null) {
for (TrackDataListener trackDataListener : trackDataManager.getListeners(TrackDataType.PREFERENCE)) { for (TrackDataListener trackDataListener : trackDataManager.getListeners(TrackDataType.PREFERENCE)) {
if (trackDataListener.onRecordingDistanceIntervalChanged(recordingDistanceInterval)) { if (trackDataListener.onRecordingDistanceIntervalChanged(recordingDistanceInterval)) {
@@ -448,7 +446,7 @@ public class TrackDataHub implements DataSourceListener {
int samplingFrequency = -1; int samplingFrequency = -1;
boolean includeNextPoint = false; 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()) { while (locationIterator.hasNext()) {
Location location = locationIterator.next(); Location location = locationIterator.next();
@@ -4,7 +4,7 @@ public final class SensorDataSet {
public static final float DATA_UNAVAILABLE = Float.NaN; public static final float DATA_UNAVAILABLE = Float.NaN;
//TODO It might be necessary to consider: sensor if sensor was connected as well. //TODO It might be necessary to consider: if sensor was connected as well.
public String sensorName; public String sensorName;
public String sensorAddress; public String sensorAddress;
private float heartRate; private float heartRate;
@@ -40,7 +40,7 @@ public final class SensorDataSet {
} }
public boolean hasHeartRate() { public boolean hasHeartRate() {
return !Float.isNaN(heartRate); return !Float.isNaN(heartRate) && heartRate > 0;
} }
public float getHeartRate() { public float getHeartRate() {
@@ -8,6 +8,7 @@ public enum SensorState {
NONE, NONE,
CONNECTING, CONNECTING,
CONNECTED, CONNECTED,
DISCONNECTING,
DISCONNECTED; DISCONNECTED;
public static String getStateAsString(SensorState state, Context c) { public static String getStateAsString(SensorState state, Context c) {
@@ -18,6 +19,8 @@ public enum SensorState {
return c.getString(R.string.sensor_state_connecting); return c.getString(R.string.sensor_state_connecting);
case CONNECTED: case CONNECTED:
return c.getString(R.string.sensor_state_connected); return c.getString(R.string.sensor_state_connected);
case DISCONNECTING:
return c.getString(R.string.sensor_state_disconnected);
case DISCONNECTED: case DISCONNECTED:
return c.getString(R.string.sensor_state_disconnected); return c.getString(R.string.sensor_state_disconnected);
default: default:
@@ -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,9 +22,9 @@ import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.view.ViewGroup.LayoutParams; import android.view.ViewGroup.LayoutParams;
import android.widget.ZoomControls;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting; import androidx.annotation.VisibleForTesting;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity; import androidx.fragment.app.FragmentActivity;
@@ -32,9 +32,9 @@ import androidx.fragment.app.FragmentActivity;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.EnumSet; import java.util.EnumSet;
import de.dennisguse.opentracks.ChartView;
import de.dennisguse.opentracks.R; import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.TrackDetailActivity; import de.dennisguse.opentracks.TrackDetailActivity;
import de.dennisguse.opentracks.chart.ChartView;
import de.dennisguse.opentracks.content.SensorDataSetLocation; import de.dennisguse.opentracks.content.SensorDataSetLocation;
import de.dennisguse.opentracks.content.Track; import de.dennisguse.opentracks.content.Track;
import de.dennisguse.opentracks.content.TrackDataHub; import de.dennisguse.opentracks.content.TrackDataHub;
@@ -54,9 +54,9 @@ import de.dennisguse.opentracks.util.UnitConversions;
* @author Sandor Dornbush * @author Sandor Dornbush
* @author Rodrigo Damazio * @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 ArrayList<double[]> pendingPoints = new ArrayList<>();
@@ -68,18 +68,17 @@ public class ChartFragment extends Fragment implements TrackDataListener {
private boolean metricUnits = true; private boolean metricUnits = true;
private boolean reportSpeed = true; private boolean reportSpeed = true;
private int recordingDistanceInterval = PreferencesUtils.RECORDING_DISTANCE_INTERVAL_DEFAULT; private int recordingDistanceInterval;
// Modes of operation // Modes of operation
private boolean chartByDistance = true; private boolean chartByDistance;
private boolean[] chartShow = new boolean[]{true, true, true, true, true, true}; private boolean[] chartShow = new boolean[]{true, true, true, true, true, true};
// UI elements // UI elements
private ChartView chartView; 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() { private final Runnable updateChart = new Runnable() {
@Override @Override
@@ -88,46 +87,34 @@ public class ChartFragment extends Fragment implements TrackDataListener {
return; return;
} }
zoomControls.setIsZoomInEnabled(chartView.canZoomIn());
zoomControls.setIsZoomOutEnabled(chartView.canZoomOut());
chartView.setShowPointer(isSelectedTrackRecording()); chartView.setShowPointer(isSelectedTrackRecording());
chartView.invalidate(); chartView.invalidate();
} }
}; };
public ChartFragment(boolean chartByDistance) {
this.chartByDistance = chartByDistance;
}
@Override @Override
public void onCreate(Bundle savedInstanceState) { public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
/* 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.
*/ // 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()); chartView = new ChartView(getActivity(), chartByDistance);
} }
@Override @Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.chart, container, false); return 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;
} }
@Override @Override
public void onStart() { public void onStart() {
super.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); LayoutParams layoutParams = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
layout.addView(chartView, layoutParams); layout.addView(chartView, layoutParams);
} }
@@ -149,10 +136,24 @@ public class ChartFragment extends Fragment implements TrackDataListener {
@Override @Override
public void onStop() { public void onStop() {
super.onStop(); super.onStop();
ViewGroup layout = getActivity().findViewById(R.id.chart_view_layout); ViewGroup layout = getView().findViewById(R.id.chart_view_layout);
layout.removeView(chartView); 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 @Override
public void onTrackUpdated(Track track) { public void onTrackUpdated(Track track) {
if (isResumed()) { if (isResumed()) {
@@ -263,8 +264,7 @@ public class ChartFragment extends Fragment implements TrackDataListener {
} }
reportSpeed = speed; reportSpeed = speed;
chartView.setReportSpeed(reportSpeed); chartView.setReportSpeed(reportSpeed);
boolean chartShowSpeed = PreferencesUtils.getBoolean( boolean chartShowSpeed = PreferencesUtils.shouldChartShowSpeed(getActivity());
getActivity(), R.string.chart_show_speed_key, PreferencesUtils.CHART_SHOW_SPEED_DEFAULT);
setSeriesEnabled(ChartView.SPEED_SERIES, chartShowSpeed && reportSpeed); setSeriesEnabled(ChartView.SPEED_SERIES, chartShowSpeed && reportSpeed);
setSeriesEnabled(ChartView.PACE_SERIES, chartShowSpeed && !reportSpeed); setSeriesEnabled(ChartView.PACE_SERIES, chartShowSpeed && !reportSpeed);
runOnUiThread(new Runnable() { runOnUiThread(new Runnable() {
@@ -303,30 +303,24 @@ public class ChartFragment extends Fragment implements TrackDataListener {
*/ */
private void checkChartSettings() { private void checkChartSettings() {
boolean needUpdate = false; boolean needUpdate = false;
if (chartByDistance != PreferencesUtils.isChartByDistance(getActivity())) { if (setSeriesEnabled(ChartView.ELEVATION_SERIES, PreferencesUtils.shouldChartShowElevation(getActivity()))) {
chartByDistance = !chartByDistance;
chartView.setChartByDistance(chartByDistance);
reloadTrackDataHub();
needUpdate = true;
}
if (setSeriesEnabled(ChartView.ELEVATION_SERIES, PreferencesUtils.getBoolean(getActivity(), R.string.chart_show_elevation_key, PreferencesUtils.CHART_SHOW_ELEVATION_DEFAULT))) {
needUpdate = true; needUpdate = true;
} }
boolean chartShowSpeed = PreferencesUtils.getBoolean(getActivity(), R.string.chart_show_speed_key, PreferencesUtils.CHART_SHOW_SPEED_DEFAULT); boolean chartShowSpeed = PreferencesUtils.shouldChartShowSpeed(getActivity());
if (setSeriesEnabled(ChartView.SPEED_SERIES, chartShowSpeed && reportSpeed)) { if (setSeriesEnabled(ChartView.SPEED_SERIES, chartShowSpeed && reportSpeed)) {
needUpdate = true; needUpdate = true;
} }
if (setSeriesEnabled(ChartView.PACE_SERIES, chartShowSpeed && !reportSpeed)) { if (setSeriesEnabled(ChartView.PACE_SERIES, chartShowSpeed && !reportSpeed)) {
needUpdate = true; needUpdate = true;
} }
if (setSeriesEnabled(ChartView.POWER_SERIES, PreferencesUtils.getBoolean(getActivity(), R.string.chart_show_power_key, PreferencesUtils.CHART_SHOW_POWER_DEFAULT))) { if (setSeriesEnabled(ChartView.POWER_SERIES, PreferencesUtils.shouldChartShowPower(getActivity()))) {
needUpdate = true; needUpdate = true;
} }
if (setSeriesEnabled(ChartView.CADENCE_SERIES, PreferencesUtils.getBoolean(getActivity(), R.string.chart_show_cadence_key, PreferencesUtils.CHART_SHOW_CADENCE_DEFAULT))) { if (setSeriesEnabled(ChartView.CADENCE_SERIES, PreferencesUtils.shouldChartShowCadence(getActivity()))) {
needUpdate = true; needUpdate = true;
} }
if (setSeriesEnabled(ChartView.HEART_RATE_SERIES, PreferencesUtils.getBoolean(getActivity(), R.string.chart_show_heart_rate_key, PreferencesUtils.CHART_SHOW_HEART_RATE_DEFAULT))) { if (setSeriesEnabled(ChartView.HEART_RATE_SERIES, PreferencesUtils.shouldChartShowHeartRate(getActivity()))) {
needUpdate = true; needUpdate = true;
} }
if (needUpdate) { if (needUpdate) {
@@ -389,18 +383,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. * 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);
}
}
@@ -1,6 +1,5 @@
package de.dennisguse.opentracks.fragments; package de.dennisguse.opentracks.fragments;
import android.app.Activity;
import android.app.Dialog; import android.app.Dialog;
import android.content.Context; import android.content.Context;
import android.os.Bundle; import android.os.Bundle;
@@ -79,19 +78,6 @@ public class ChooseActivityTypeDialogFragment extends DialogFragment implements
return view; return view;
} }
/**
* TODO: Only used by obsolete Androidx preferences. Remove when onAttach(Context) is called.
*/
@Override
public void onAttach(@NonNull Activity activity) {
super.onAttach(activity);
try {
chooseActivityTypeCaller = (ChooseActivityTypeCaller) activity;
} catch (ClassCastException e) {
throw new ClassCastException(activity + " must implement " + ChooseActivityTypeCaller.class.getSimpleName());
}
}
public void onAttach(@NonNull Context context) { public void onAttach(@NonNull Context context) {
super.onAttach(context); super.onAttach(context);
try { try {
@@ -81,7 +81,7 @@ public class DeleteMarkerDialogFragment extends DialogFragment {
new Thread(new Runnable() { new Thread(new Runnable() {
@Override @Override
public void run() { public void run() {
ContentProviderUtils contentProviderUtils = ContentProviderUtils.Factory.get(fragmentActivity); ContentProviderUtils contentProviderUtils = new ContentProviderUtils(fragmentActivity);
for (long markerId : markerIds) { for (long markerId : markerIds) {
contentProviderUtils.deleteWaypoint(fragmentActivity, contentProviderUtils.deleteWaypoint(fragmentActivity,
markerId, new DescriptionGeneratorImpl(fragmentActivity)); markerId, new DescriptionGeneratorImpl(fragmentActivity));
@@ -1,105 +0,0 @@
/*
* Copyright 2012 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package de.dennisguse.opentracks.fragments;
import android.app.AlertDialog;
import android.app.Dialog;
import android.content.DialogInterface;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.fragment.app.DialogFragment;
import androidx.fragment.app.FragmentActivity;
import androidx.fragment.app.FragmentManager;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.util.PreferencesUtils;
import de.dennisguse.opentracks.util.StringUtils;
/**
* A DialogFragment to configure frequency.
*
* @author Jimmy Shih
*/
public class FrequencyDialogFragment extends DialogFragment {
private static final String FREQUENCY_DIALOG_TAG = "frequencyDialog";
private int preferenceId;
private int defaultValue;
private int titleId;
public FrequencyDialogFragment(int preferenceId, int defaultValue, int titleId) {
this.preferenceId = preferenceId;
this.defaultValue = defaultValue;
this.titleId = titleId;
}
public static void showDialog(FragmentManager fragmentManager, int preferenceId, int defaultValue, int titleId) {
new FrequencyDialogFragment(preferenceId, defaultValue, titleId).show(fragmentManager, FREQUENCY_DIALOG_TAG);
}
@Override
@NonNull
public Dialog onCreateDialog(Bundle savedInstanceState) {
FragmentActivity fragmentActivity = getActivity();
int frequencyValue = PreferencesUtils.getInt(fragmentActivity, preferenceId, defaultValue);
return new AlertDialog.Builder(fragmentActivity).setPositiveButton(
R.string.generic_ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
int listIndex = ((AlertDialog) dialog).getListView().getCheckedItemPosition();
PreferencesUtils.setInt(getActivity(), preferenceId, getFrequencyValue(listIndex));
}
}).setSingleChoiceItems(
getFrequencyDisplayOptions(fragmentActivity), getListIndex(frequencyValue), null)
.setTitle(titleId).create();
}
/**
* Gets the frequency display options.
*/
private String[] getFrequencyDisplayOptions(FragmentActivity fragmentActivity) {
boolean metricUnits = PreferencesUtils.isMetricUnits(fragmentActivity);
return StringUtils.getFrequencyOptions(fragmentActivity, metricUnits);
}
/**
* Gets the list index for a frequency value.
* Returns 0 if the value is not on the list.
*/
private int getListIndex(int frequencyValue) {
String[] values = getResources().getStringArray(R.array.frequency_values);
for (int i = 0; i < values.length; i++) {
if (frequencyValue == Integer.parseInt(values[i])) {
return i;
}
}
return 0;
}
/**
* Gets the frequency value from a list index.
*
* @param listIndex the list index
*/
private int getFrequencyValue(int listIndex) {
String[] values = getResources().getStringArray(R.array.frequency_values);
return Integer.parseInt(values[listIndex]);
}
}
@@ -110,7 +110,7 @@ public class MarkerDetailFragment extends Fragment {
getFragmentManager().popBackStack(); getFragmentManager().popBackStack();
return; return;
} }
contentProviderUtils = ContentProviderUtils.Factory.get(getActivity()); contentProviderUtils = new ContentProviderUtils(getActivity());
handler = new Handler(); handler = new Handler();
setHasOptionsMenu(true); setHasOptionsMenu(true);
} }
@@ -43,10 +43,8 @@ import de.dennisguse.opentracks.content.Waypoint;
import de.dennisguse.opentracks.content.sensor.SensorDataSet; import de.dennisguse.opentracks.content.sensor.SensorDataSet;
import de.dennisguse.opentracks.services.ITrackRecordingService; import de.dennisguse.opentracks.services.ITrackRecordingService;
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection; import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
import de.dennisguse.opentracks.services.sensors.RemoteSensorManager;
import de.dennisguse.opentracks.stats.TripStatistics; import de.dennisguse.opentracks.stats.TripStatistics;
import de.dennisguse.opentracks.util.LocationUtils; import de.dennisguse.opentracks.util.LocationUtils;
import de.dennisguse.opentracks.util.PreferencesUtils;
import de.dennisguse.opentracks.util.StatsUtils; import de.dennisguse.opentracks.util.StatsUtils;
import de.dennisguse.opentracks.util.TrackIconUtils; import de.dennisguse.opentracks.util.TrackIconUtils;
import de.dennisguse.opentracks.util.UnitConversions; import de.dennisguse.opentracks.util.UnitConversions;
@@ -59,7 +57,7 @@ import de.dennisguse.opentracks.util.UnitConversions;
*/ */
public class StatsFragment extends Fragment implements TrackDataListener { public class StatsFragment extends Fragment implements TrackDataListener {
public static final String STATS_FRAGMENT_TAG = "statsFragment"; public static final String STATS_FRAGMENT_TAG = StatsFragment.class.getSimpleName();
private TrackDataHub trackDataHub; private TrackDataHub trackDataHub;
private Handler handlerUpdateUI; private Handler handlerUpdateUI;
@@ -68,7 +66,6 @@ public class StatsFragment extends Fragment implements TrackDataListener {
private Location lastLocation = null; private Location lastLocation = null;
private TripStatistics lastTripStatistics = null; private TripStatistics lastTripStatistics = null;
private SensorDataSet sensorDataSet = null;
private final Runnable updateUIeachSecond = new Runnable() { private final Runnable updateUIeachSecond = new Runnable() {
public void run() { public void run() {
@@ -83,7 +80,7 @@ public class StatsFragment extends Fragment implements TrackDataListener {
} }
}; };
private String category = ""; private String category = "";
private int recordingGpsAccuracy = PreferencesUtils.RECORDING_GPS_ACCURACY_DEFAULT; private int recordingGpsAccuracy;
@Override @Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
@@ -93,9 +90,10 @@ public class StatsFragment extends Fragment implements TrackDataListener {
@Override @Override
public void onActivityCreated(Bundle savedInstanceState) { public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState); super.onActivityCreated(savedInstanceState);
recordingGpsAccuracy = Integer.parseInt(getContext().getResources().getString(R.string.recording_gps_accuracy_default));
handlerUpdateUI = new Handler(); handlerUpdateUI = new Handler();
trackRecordingServiceConnection = new TrackRecordingServiceConnection(getContext(), null);
trackRecordingServiceConnection.startConnection(getContext());
Spinner activityTypeIcon = getView().findViewById(R.id.stats_activity_type_icon); Spinner activityTypeIcon = getView().findViewById(R.id.stats_activity_type_icon);
activityTypeIcon.setAdapter(TrackIconUtils.getIconSpinnerAdapter(getActivity(), "")); activityTypeIcon.setAdapter(TrackIconUtils.getIconSpinnerAdapter(getActivity(), ""));
@@ -123,6 +121,10 @@ public class StatsFragment extends Fragment implements TrackDataListener {
public void onResume() { public void onResume() {
super.onResume(); super.onResume();
resumeTrackDataHub(); resumeTrackDataHub();
trackRecordingServiceConnection = new TrackRecordingServiceConnection(getContext(), null);
trackRecordingServiceConnection.startConnection(getContext());
updateUi(getActivity()); updateUi(getActivity());
if (isSelectedTrackRecording()) { if (isSelectedTrackRecording()) {
handlerUpdateUI.post(updateUIeachSecond); handlerUpdateUI.post(updateUIeachSecond);
@@ -140,6 +142,7 @@ public class StatsFragment extends Fragment implements TrackDataListener {
public void onStop() { public void onStop() {
super.onStop(); super.onStop();
trackRecordingServiceConnection.unbind(); trackRecordingServiceConnection.unbind();
trackRecordingServiceConnection = null;
} }
@Override @Override
@@ -298,13 +301,14 @@ public class StatsFragment extends Fragment implements TrackDataListener {
} }
/** /**
* Tries to fetch most recent {@link SensorDataSet} {@link RemoteSensorManager}. * Tries to fetch most recent {@link SensorDataSet} {@link de.dennisguse.opentracks.services.sensors.BluetoothRemoteSensorManager}.
*/ */
private void updateSensorDataUI() { private void updateSensorDataUI() {
ITrackRecordingService trackRecordingService = trackRecordingServiceConnection.getServiceIfBound(); ITrackRecordingService trackRecordingService = trackRecordingServiceConnection.getServiceIfBound();
SensorDataSet sensorDataSet = null;
if (trackRecordingService == null) { if (trackRecordingService == null) {
Log.d(STATS_FRAGMENT_TAG, "Cannot get the track recording service."); Log.d(STATS_FRAGMENT_TAG, "Cannot get the track recording service.");
sensorDataSet = null;
} else { } else {
//TODO sensorState = trackRecordingService.getSensorState(); //TODO sensorState = trackRecordingService.getSensorState();
sensorDataSet = trackRecordingService.getSensorData(); sensorDataSet = trackRecordingService.getSensorData();
@@ -173,10 +173,12 @@ public enum TrackFileFormat {
private static final String MIME_KML = "application/vnd.google-earth.kml+xml"; private static final String MIME_KML = "application/vnd.google-earth.kml+xml";
public TrackExporter newTrackExporter(Context context, Track[] tracks, TrackExporterListener trackExporterListener) { private static TrackExporter newKmzTrackExporter(Context context, TrackWriter trackWriter, Track[] tracks, TrackExporterListener trackExporterListener, boolean exportPhotos) {
ContentProviderUtils contentProviderUtils = ContentProviderUtils.Factory.get(context); ContentProviderUtils contentProviderUtils = new ContentProviderUtils(context);
TrackWriter trackWriter = this.newTrackWriter(context, tracks.length > 1);
return new FileTrackExporter(contentProviderUtils, trackWriter, tracks, trackExporterListener); 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(); public abstract String getMimeType();
private static TrackExporter newKmzTrackExporter(Context context, TrackWriter trackWriter, Track[] tracks, TrackExporterListener trackExporterListener, boolean exportPhotos) { public TrackExporter newTrackExporter(Context context, Track[] tracks, TrackExporterListener trackExporterListener) {
ContentProviderUtils contentProviderUtils = ContentProviderUtils.Factory.get(context); ContentProviderUtils contentProviderUtils = new ContentProviderUtils(context);
TrackWriter trackWriter = this.newTrackWriter(context, tracks.length > 1);
FileTrackExporter fileTrackExporter = new FileTrackExporter(contentProviderUtils, trackWriter, tracks, trackExporterListener); return new FileTrackExporter(contentProviderUtils, trackWriter, tracks, trackExporterListener);
return new KmzTrackExporter(contentProviderUtils, fileTrackExporter, tracks, exportPhotos);
} }
/** /**
@@ -27,7 +27,6 @@ import java.io.FileNotFoundException;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.ContentProviderUtils; import de.dennisguse.opentracks.content.ContentProviderUtils;
import de.dennisguse.opentracks.content.Track; import de.dennisguse.opentracks.content.Track;
import de.dennisguse.opentracks.content.TracksColumns; import de.dennisguse.opentracks.content.TracksColumns;
@@ -69,7 +68,7 @@ public class ExportAsyncTask extends AsyncTask<Void, Integer, Boolean> {
this.trackFileFormat = trackFileFormat; this.trackFileFormat = trackFileFormat;
this.directory = directory; this.directory = directory;
context = exportActivity.getApplicationContext(); context = exportActivity.getApplicationContext();
contentProviderUtils = ContentProviderUtils.Factory.get(context); contentProviderUtils = new ContentProviderUtils(context);
completed = false; completed = false;
processedTrackCount = 0; processedTrackCount = 0;
@@ -100,7 +99,7 @@ public class ExportAsyncTask extends AsyncTask<Void, Integer, Boolean> {
try { try {
Thread.currentThread().setPriority(Thread.MAX_PRIORITY); Thread.currentThread().setPriority(Thread.MAX_PRIORITY);
boolean isRecording = PreferencesUtils.isRecording(exportActivity); boolean isRecording = PreferencesUtils.isRecording(exportActivity);
boolean isPaused = PreferencesUtils.getBoolean(exportActivity, R.string.recording_track_paused_key, PreferencesUtils.RECORDING_TRACK_PAUSED_DEFAULT); boolean isPaused = PreferencesUtils.isRecordingTrackPaused(exportActivity);
// Get the wake lock if not recording or paused // Get the wake lock if not recording or paused
if (!isRecording || isPaused) { if (!isRecording || isPaused) {
wakeLock = SystemUtils.acquireWakeLock(exportActivity, wakeLock); wakeLock = SystemUtils.acquireWakeLock(exportActivity, wakeLock);
@@ -26,7 +26,8 @@ import androidx.annotation.NonNull;
import java.io.OutputStream; import java.io.OutputStream;
import de.dennisguse.opentracks.content.ContentProviderUtils; 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.SensorDataSetLocation;
import de.dennisguse.opentracks.content.Track; import de.dennisguse.opentracks.content.Track;
import de.dennisguse.opentracks.content.Waypoint; import de.dennisguse.opentracks.content.Waypoint;
@@ -63,7 +64,7 @@ public class FileTrackExporter implements TrackExporter {
} }
@Override @Override
public boolean writeTrack(Context context, @NonNull OutputStream outputStream) { public boolean writeTrack(@NonNull Context context, @NonNull OutputStream outputStream) {
try { try {
trackWriter.prepare(outputStream); trackWriter.prepare(outputStream);
trackWriter.writeHeader(tracks); trackWriter.writeHeader(tracks);
@@ -209,7 +210,7 @@ public class FileTrackExporter implements TrackExporter {
* *
* @author Jimmy Shih * @author Jimmy Shih
*/ */
private class TrackWriterLocationFactory implements ContentProviderUtils.LocationFactory { private class TrackWriterLocationFactory extends LocationFactory {
Location currentLocation; Location currentLocation;
Location lastLocation; Location lastLocation;
@@ -89,7 +89,7 @@ public class KmlTrackWriter implements TrackWriter {
this.exportSensorData = exportSensorData; this.exportSensorData = exportSensorData;
this.exportPhotos = exportPhotos; this.exportPhotos = exportPhotos;
this.descriptionGenerator = new DescriptionGeneratorImpl(context); this.descriptionGenerator = new DescriptionGeneratorImpl(context);
this.contentProviderUtils = ContentProviderUtils.Factory.get(context); this.contentProviderUtils = new ContentProviderUtils(context);
} }
@Override @Override
@@ -19,7 +19,6 @@ package de.dennisguse.opentracks.io.file.exporter;
import android.content.Context; import android.content.Context;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.io.OutputStream; import java.io.OutputStream;
@@ -37,5 +36,5 @@ public interface TrackExporter {
* @param context the context * @param context the context
* @param outputStream the output stream * @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.R;
import de.dennisguse.opentracks.content.ContentProviderUtils; import de.dennisguse.opentracks.content.ContentProviderUtils;
import de.dennisguse.opentracks.content.ContentProviderUtils.LocationIterator;
import de.dennisguse.opentracks.content.DescriptionGeneratorImpl; 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.Track;
import de.dennisguse.opentracks.content.Waypoint; import de.dennisguse.opentracks.content.Waypoint;
import de.dennisguse.opentracks.content.Waypoint.WaypointType; import de.dennisguse.opentracks.content.Waypoint.WaypointType;
@@ -102,7 +103,7 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
this.context = context; this.context = context;
this.importTrackId = importTrackId; this.importTrackId = importTrackId;
this.contentProviderUtils = contentProviderUtils; this.contentProviderUtils = contentProviderUtils;
this.recordingDistanceInterval = PreferencesUtils.getInt(context, R.string.recording_distance_interval_key, PreferencesUtils.RECORDING_DISTANCE_INTERVAL_DEFAULT); this.recordingDistanceInterval = PreferencesUtils.getRecordingDistanceInterval(context);
trackIds = new ArrayList<>(); trackIds = new ArrayList<>();
waypoints = new ArrayList<>(); waypoints = new ArrayList<>();
} }
@@ -165,7 +166,7 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
TripStatisticsUpdater trackTripStatisticstrackUpdater = new TripStatisticsUpdater(track.getTripStatistics().getStartTime()); TripStatisticsUpdater trackTripStatisticstrackUpdater = new TripStatisticsUpdater(track.getTripStatistics().getStartTime());
TripStatisticsUpdater markerTripStatisticsUpdater = 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) { while (true) {
if (waypoint == null) { if (waypoint == null) {
@@ -55,7 +55,7 @@ public class GpxFileTrackImporter extends AbstractFileTrackImporter {
* @param context the context * @param context the context
*/ */
public GpxFileTrackImporter(Context context) { public GpxFileTrackImporter(Context context) {
this(context, ContentProviderUtils.Factory.get(context)); this(context, new ContentProviderUtils(context));
} }
@VisibleForTesting @VisibleForTesting
@@ -28,7 +28,6 @@ import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.ContentProviderUtils; import de.dennisguse.opentracks.content.ContentProviderUtils;
import de.dennisguse.opentracks.content.Track; import de.dennisguse.opentracks.content.Track;
import de.dennisguse.opentracks.io.file.TrackFileFormat; import de.dennisguse.opentracks.io.file.TrackFileFormat;
@@ -46,7 +45,9 @@ public class ImportAsyncTask extends AsyncTask<Void, Integer, Boolean> {
private static final String TAG = ImportAsyncTask.class.getSimpleName(); private static final String TAG = ImportAsyncTask.class.getSimpleName();
private final TrackFileFormat trackFileFormat; private final TrackFileFormat trackFileFormat;
private final String path; private final String path;
private final Context context; // TODO Can be removed, or?
private Context context;
// TODO Use weak reference
private ImportActivity importActivity; private ImportActivity importActivity;
private WakeLock wakeLock; private WakeLock wakeLock;
@@ -101,10 +102,10 @@ public class ImportAsyncTask extends AsyncTask<Void, Integer, Boolean> {
@Override @Override
protected Boolean doInBackground(Void... params) { protected Boolean doInBackground(Void... params) {
try { 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 // Get the wake lock if not recording or paused
boolean isRecording = PreferencesUtils.isRecording(importActivity); boolean isRecording = PreferencesUtils.isRecording(importActivity);
boolean isPaused = PreferencesUtils.getBoolean(importActivity, R.string.recording_track_paused_key, PreferencesUtils.RECORDING_TRACK_PAUSED_DEFAULT); boolean isPaused = PreferencesUtils.isRecordingTrackPaused(importActivity);
if (!isRecording || isPaused) { if (!isRecording || isPaused) {
wakeLock = SystemUtils.acquireWakeLock(importActivity, wakeLock); wakeLock = SystemUtils.acquireWakeLock(importActivity, wakeLock);
} }
@@ -150,9 +151,11 @@ public class ImportAsyncTask extends AsyncTask<Void, Integer, Boolean> {
@Override @Override
protected void onCancelled() { protected void onCancelled() {
context = null;
completed = true; completed = true;
if (importActivity != null) { if (importActivity != null) {
importActivity.onAsyncTaskCompleted(importTrackCount, totalTrackCount); importActivity.onAsyncTaskCompleted(importTrackCount, totalTrackCount);
importActivity = null;
} }
} }
@@ -164,17 +167,17 @@ public class ImportAsyncTask extends AsyncTask<Void, Integer, Boolean> {
private boolean importFile(final File file) { private boolean importFile(final File file) {
TrackImporter trackImporter; TrackImporter trackImporter;
if (trackFileFormat == TrackFileFormat.GPX) { if (trackFileFormat == TrackFileFormat.GPX) {
trackImporter = new GpxFileTrackImporter(context); trackImporter = new GpxFileTrackImporter(importActivity);
} else { //KML or KMZ } else { //KML or KMZ
String extension = FileUtils.getExtension(file.getName()); String extension = FileUtils.getExtension(file.getName());
if (TrackFileFormat.KML_ONLY_TRACK.getExtension().equals(extension)) { if (TrackFileFormat.KML_ONLY_TRACK.getExtension().equals(extension)) {
trackImporter = new KmlFileTrackImporter(context, -1L); trackImporter = new KmlFileTrackImporter(importActivity, -1L);
} else { } else {
ContentProviderUtils contentProviderUtils = ContentProviderUtils.Factory.get(context); ContentProviderUtils contentProviderUtils = new ContentProviderUtils(importActivity);
Uri uri = contentProviderUtils.insertTrack(new Track()); Uri uri = contentProviderUtils.insertTrack(new Track());
long newId = Long.parseLong(uri.getLastPathSegment()); 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. * @param importTrackId track id to import to. -1L to import to a new track.
*/ */
public KmlFileTrackImporter(Context context, long importTrackId) { public KmlFileTrackImporter(Context context, long importTrackId) {
this(context, importTrackId, ContentProviderUtils.Factory.get(context)); this(context, importTrackId, new ContentProviderUtils(context));
} }
@VisibleForTesting @VisibleForTesting
@@ -182,6 +182,10 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter {
* On waypoint end. * On waypoint end.
*/ */
private void onWaypointEnd() throws SAXException { private void onWaypointEnd() throws SAXException {
if (waypointType == null) {
return;
}
// Add a waypoint if the waypointType matches // Add a waypoint if the waypointType matches
WaypointType type; WaypointType type;
switch (waypointType) { switch (waypointType) {
@@ -72,7 +72,7 @@ public class KmzTrackImporter implements TrackImporter {
return -1L; return -1L;
} }
String fileName = zipEntry.getName(); String fileName = zipEntry.getName();
if (fileName.equals(KmzTrackExporter.KMZ_KML_FILE)) { if (KmzTrackExporter.KMZ_KML_FILE.equals(fileName)) {
trackId = parseKml(zipInputStream); trackId = parseKml(zipInputStream);
if (trackId == -1L) { if (trackId == -1L) {
Log.d(TAG, "Unable to parse kml in kmz"); Log.d(TAG, "Unable to parse kml in kmz");
@@ -101,8 +101,8 @@ public class KmzTrackImporter implements TrackImporter {
* @param trackId the trackId * @param trackId the trackId
*/ */
private void cleanImport(long trackId) { private void cleanImport(long trackId) {
if (trackId != PreferencesUtils.RECORDING_TRACK_ID_DEFAULT) { if (PreferencesUtils.isRecording(trackId)) {
ContentProviderUtils contentProviderUtils = ContentProviderUtils.Factory.get(context); ContentProviderUtils contentProviderUtils = new ContentProviderUtils(context);
contentProviderUtils.deleteTrack(context, trackId); contentProviderUtils.deleteTrack(context, trackId);
} }
@@ -125,15 +125,10 @@ public class KmzTrackImporter implements TrackImporter {
* @return the imported track id or -1L * @return the imported track id or -1L
*/ */
private long parseKml(ZipInputStream zipInputStream) throws IOException { private long parseKml(ZipInputStream zipInputStream) throws IOException {
ByteArrayInputStream byteArrayInputStream = null; KmlFileTrackImporter kmlFileTrackImporter = new KmlFileTrackImporter(context, importTrackId);
try {
KmlFileTrackImporter kmlFileTrackImporter = new KmlFileTrackImporter(context, importTrackId); try (ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(getKml(zipInputStream))) {
byteArrayInputStream = new ByteArrayInputStream(getKml(zipInputStream));
return kmlFileTrackImporter.importFile(byteArrayInputStream); return kmlFileTrackImporter.importFile(byteArrayInputStream);
} finally {
if (byteArrayInputStream != null) {
byteArrayInputStream.close();
}
} }
} }
@@ -160,12 +155,10 @@ public class KmzTrackImporter implements TrackImporter {
* @param fileName the file name * @param fileName the file name
*/ */
private void readImageFile(ZipInputStream zipInputStream, String fileName) throws IOException { private void readImageFile(ZipInputStream zipInputStream, String fileName) throws IOException {
if (importTrackId == -1L) { if (importTrackId == -1L || fileName.equals("")) {
return;
}
if (fileName.equals("")) {
return; return;
} }
File dir = FileUtils.getPhotoDir(importTrackId); File dir = FileUtils.getPhotoDir(importTrackId);
FileUtils.ensureDirectoryExists(dir); FileUtils.ensureDirectoryExists(dir);
File file = new File(dir, fileName); File file = new File(dir, fileName);
@@ -33,10 +33,6 @@ public class LocationManagerConnector {
private final Handler handler; private final Handler handler;
private final LocationManager locationManager; private final LocationManager locationManager;
private LocationListener requestLocationUpdates;
private float requestLocationUpdatesDistance;
private long requestLocationUpdatesTime;
public LocationManagerConnector(Context context, Looper looper) { public LocationManagerConnector(Context context, Looper looper) {
this.handler = new Handler(looper); this.handler = new Handler(looper);
@@ -55,10 +51,7 @@ public class LocationManagerConnector {
@Override @Override
@SuppressLint("MissingPermission") @SuppressLint("MissingPermission")
public void run() { public void run() {
requestLocationUpdatesTime = minTime; locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, minTime, minDistance, locationListener);
requestLocationUpdatesDistance = minDistance;
requestLocationUpdates = locationListener;
locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, requestLocationUpdatesTime, requestLocationUpdatesDistance, locationListener);
} }
}); });
} }
@@ -70,6 +63,5 @@ public class LocationManagerConnector {
*/ */
public void removeLocationUpdates(final LocationListener locationListener) { public void removeLocationUpdates(final LocationListener locationListener) {
locationManager.removeUpdates(locationListener); locationManager.removeUpdates(locationListener);
requestLocationUpdates = null;
} }
} }
@@ -16,9 +16,6 @@
package de.dennisguse.opentracks.services; package de.dennisguse.opentracks.services;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent; import android.app.PendingIntent;
import android.app.Service; import android.app.Service;
import android.content.Context; import android.content.Context;
@@ -30,7 +27,6 @@ import android.location.Location;
import android.location.LocationListener; import android.location.LocationListener;
import android.location.LocationManager; import android.location.LocationManager;
import android.net.Uri; import android.net.Uri;
import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.os.Handler; import android.os.Handler;
import android.os.IBinder; import android.os.IBinder;
@@ -38,7 +34,6 @@ import android.os.PowerManager.WakeLock;
import android.util.Log; import android.util.Log;
import androidx.annotation.VisibleForTesting; import androidx.annotation.VisibleForTesting;
import androidx.core.app.NotificationCompat;
import androidx.core.app.TaskStackBuilder; import androidx.core.app.TaskStackBuilder;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
@@ -48,17 +43,17 @@ import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.TrackDetailActivity; import de.dennisguse.opentracks.TrackDetailActivity;
import de.dennisguse.opentracks.TrackListActivity; import de.dennisguse.opentracks.TrackListActivity;
import de.dennisguse.opentracks.content.ContentProviderUtils; import de.dennisguse.opentracks.content.ContentProviderUtils;
import de.dennisguse.opentracks.content.ContentProviderUtils.LocationIterator;
import de.dennisguse.opentracks.content.CustomContentProvider; import de.dennisguse.opentracks.content.CustomContentProvider;
import de.dennisguse.opentracks.content.DescriptionGeneratorImpl; 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.SensorDataSetLocation;
import de.dennisguse.opentracks.content.Track; import de.dennisguse.opentracks.content.Track;
import de.dennisguse.opentracks.content.Waypoint; import de.dennisguse.opentracks.content.Waypoint;
import de.dennisguse.opentracks.content.Waypoint.WaypointType; import de.dennisguse.opentracks.content.Waypoint.WaypointType;
import de.dennisguse.opentracks.content.WaypointCreationRequest; import de.dennisguse.opentracks.content.WaypointCreationRequest;
import de.dennisguse.opentracks.content.sensor.SensorDataSet; import de.dennisguse.opentracks.content.sensor.SensorDataSet;
import de.dennisguse.opentracks.services.sensors.RemoteSensorManager; import de.dennisguse.opentracks.services.sensors.BluetoothRemoteSensorManager;
import de.dennisguse.opentracks.services.sensors.RemoteSensorManagerFactory;
import de.dennisguse.opentracks.services.tasks.AnnouncementPeriodicTaskFactory; import de.dennisguse.opentracks.services.tasks.AnnouncementPeriodicTaskFactory;
import de.dennisguse.opentracks.services.tasks.PeriodicTaskExecutor; import de.dennisguse.opentracks.services.tasks.PeriodicTaskExecutor;
import de.dennisguse.opentracks.services.tasks.SplitPeriodicTaskFactory; import de.dennisguse.opentracks.services.tasks.SplitPeriodicTaskFactory;
@@ -81,13 +76,13 @@ import de.dennisguse.opentracks.util.UnitConversions;
public class TrackRecordingService extends Service { public class TrackRecordingService extends Service {
// The name of extra intent property to indicate whether we want to resume a previously recorded track. // 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"; public static final String RESUME_TRACK_EXTRA_NAME = "RESUME_TRACK";
private static final int NOTIFICATION_ID = 123;
public static final double PAUSE_LATITUDE = 100.0; public static final double PAUSE_LATITUDE = 100.0;
public static final double RESUME_LATITUDE = 200.0; public static final double RESUME_LATITUDE = 200.0;
// Anything faster than that (in meters per second) will be considered moving. // Anything faster than that (in meters per second) will be considered moving.
public static final double MAX_NO_MOVEMENT_SPEED = 0.224; public static final double MAX_NO_MOVEMENT_SPEED = 0.224;
@VisibleForTesting @VisibleForTesting
static final int MAX_AUTO_RESUME_TRACK_RETRY_ATTEMPTS = 3; static final int MAX_AUTO_RESUME_TRACK_RETRY_ATTEMPTS = 3;
@@ -96,13 +91,13 @@ public class TrackRecordingService extends Service {
private static final long ONE_MINUTE = (long) (UnitConversions.MIN_TO_S * UnitConversions.S_TO_MS); private static final long ONE_MINUTE = (long) (UnitConversions.MIN_TO_S * UnitConversions.S_TO_MS);
// The following variables are set in onCreate: // The following variables are set in onCreate:
private ExecutorService executorService; private ExecutorService executorService;
private Context context;
private ContentProviderUtils contentProviderUtils; private ContentProviderUtils contentProviderUtils;
private Handler handler; private Handler handler;
private LocationManagerConnector locationManagerConnector; private LocationManagerConnector locationManagerConnector;
private PeriodicTaskExecutor voiceExecutor; private PeriodicTaskExecutor voiceExecutor;
private PeriodicTaskExecutor splitExecutor; private PeriodicTaskExecutor splitExecutor;
private SharedPreferences sharedPreferences; private SharedPreferences sharedPreferences;
private TrackRecordingServiceNotificationManager notificationManager;
private long recordingTrackId; private long recordingTrackId;
private boolean recordingTrackPaused; private boolean recordingTrackPaused;
private LocationListenerPolicy locationListenerPolicy; private LocationListenerPolicy locationListenerPolicy;
@@ -118,58 +113,56 @@ public class TrackRecordingService extends Service {
private final OnSharedPreferenceChangeListener sharedPreferenceChangeListener = new OnSharedPreferenceChangeListener() { private final OnSharedPreferenceChangeListener sharedPreferenceChangeListener = new OnSharedPreferenceChangeListener() {
@Override @Override
public void onSharedPreferenceChanged(SharedPreferences preferences, String key) { public void onSharedPreferenceChanged(SharedPreferences preferences, String key) {
if (key == null || key.equals(PreferencesUtils.getKey(context, R.string.recording_track_id_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. // Only through the TrackRecordingService can one stop a recording and set the recordingTrackId to -1L.
if (PreferencesUtils.isRecording(context)) { if (PreferencesUtils.isRecording(TrackRecordingService.this)) {
recordingTrackId = PreferencesUtils.getLong(context, R.string.recording_track_id_key); recordingTrackId = PreferencesUtils.getRecordingTrackId(TrackRecordingService.this);
} }
} }
if (key == null || key.equals(PreferencesUtils.getKey(context, R.string.recording_track_paused_key))) { if (PreferencesUtils.isKey(context, R.string.recording_track_paused_key, key)) {
recordingTrackPaused = PreferencesUtils.getBoolean(context, R.string.recording_track_paused_key, PreferencesUtils.RECORDING_TRACK_PAUSED_DEFAULT); recordingTrackPaused = PreferencesUtils.isRecordingTrackPaused(context);
} }
if (key == null || key.equals(PreferencesUtils.getKey(context, R.string.stats_units_key))) { if (PreferencesUtils.isKey(context, R.string.stats_units_key, key)) {
boolean metricUnits = PreferencesUtils.isMetricUnits(context); boolean metricUnits = PreferencesUtils.isMetricUnits(context);
voiceExecutor.setMetricUnits(metricUnits); voiceExecutor.setMetricUnits(metricUnits);
splitExecutor.setMetricUnits(metricUnits); splitExecutor.setMetricUnits(metricUnits);
} }
if (key == null || key.equals(PreferencesUtils.getKey(context, R.string.voice_frequency_key))) { if (PreferencesUtils.isKey(context, R.string.voice_frequency_key, key)) {
voiceExecutor.setTaskFrequency(PreferencesUtils.getInt(context, R.string.voice_frequency_key, PreferencesUtils.VOICE_FREQUENCY_DEFAULT)); voiceExecutor.setTaskFrequency(PreferencesUtils.getVoiceFrequency(context));
} }
if (key == null || key.equals(PreferencesUtils.getKey(context, R.string.split_frequency_key))) { if (PreferencesUtils.isKey(context, R.string.split_frequency_key, key)) {
splitExecutor.setTaskFrequency(PreferencesUtils.getInt(context, R.string.split_frequency_key, PreferencesUtils.SPLIT_FREQUENCY_DEFAULT)); splitExecutor.setTaskFrequency(PreferencesUtils.getSplitFrequency(context));
} }
if (key == null || key.equals(PreferencesUtils.getKey(context, R.string.min_recording_interval_key))) { if (PreferencesUtils.isKey(context, R.string.min_recording_interval_key, key)) {
int minRecordingInterval = PreferencesUtils.getInt(context, R.string.min_recording_interval_key, PreferencesUtils.MIN_RECORDING_INTERVAL_DEFAULT); int minRecordingInterval = PreferencesUtils.getMinRecordingInterval(context);
switch (minRecordingInterval) { if (minRecordingInterval == PreferencesUtils.getMinRecordingIntervalAdaptBatteryLife(context)) {
case PreferencesUtils.MIN_RECORDING_INTERVAL_ADAPT_BATTERY_LIFE: // Choose battery life over moving time accuracy.
// Choose battery life over moving time accuracy. locationListenerPolicy = new AdaptiveLocationListenerPolicy(30 * UnitConversions.ONE_SECOND, 5 * ONE_MINUTE, 5);
locationListenerPolicy = new AdaptiveLocationListenerPolicy(30 * UnitConversions.ONE_SECOND, 5 * ONE_MINUTE, 5); } else if (minRecordingInterval == PreferencesUtils.getMinRecordingIntervalAdaptAccuracy(context)) {
break; // Get all the updates.
case PreferencesUtils.MIN_RECORDING_INTERVAL_ADAPT_ACCURACY: locationListenerPolicy = new AdaptiveLocationListenerPolicy(UnitConversions.ONE_SECOND, 30 * UnitConversions.ONE_SECOND, 0);
// Get all the updates. } else {
locationListenerPolicy = new AdaptiveLocationListenerPolicy(UnitConversions.ONE_SECOND, 30 * UnitConversions.ONE_SECOND, 0); locationListenerPolicy = new AbsoluteLocationListenerPolicy(minRecordingInterval * UnitConversions.ONE_SECOND);
break;
default:
locationListenerPolicy = new AbsoluteLocationListenerPolicy(minRecordingInterval * UnitConversions.ONE_SECOND);
} }
} }
if (key == null || key.equals(PreferencesUtils.getKey(context, R.string.recording_distance_interval_key))) { if (PreferencesUtils.isKey(context, R.string.recording_distance_interval_key, key)) {
recordingDistanceInterval = PreferencesUtils.getInt(context, R.string.recording_distance_interval_key, PreferencesUtils.RECORDING_DISTANCE_INTERVAL_DEFAULT); recordingDistanceInterval = PreferencesUtils.getRecordingDistanceInterval(context);
} }
if (key == null || key.equals(PreferencesUtils.getKey(context, R.string.max_recording_distance_key))) { if (PreferencesUtils.isKey(context, R.string.max_recording_distance_key, key)) {
maxRecordingDistance = PreferencesUtils.getInt(context, R.string.max_recording_distance_key, PreferencesUtils.MAX_RECORDING_DISTANCE_DEFAULT); maxRecordingDistance = PreferencesUtils.getMaxRecordingDistance(context);
} }
if (key == null || key.equals(PreferencesUtils.getKey(context, R.string.recording_gps_accuracy_key))) { if (PreferencesUtils.isKey(context, R.string.recording_gps_accuracy_key, key)) {
recordingGpsAccuracy = PreferencesUtils.getInt(context, R.string.recording_gps_accuracy_key, PreferencesUtils.RECORDING_GPS_ACCURACY_DEFAULT); recordingGpsAccuracy = PreferencesUtils.getRecordingGPSAccuracy(context);
} }
if (key == null || key.equals(PreferencesUtils.getKey(context, R.string.auto_resume_track_timeout_key))) { if (PreferencesUtils.isKey(context, R.string.auto_resume_track_timeout_key, key)) {
autoResumeTrackTimeout = PreferencesUtils.getInt(context, R.string.auto_resume_track_timeout_key, PreferencesUtils.AUTO_RESUME_TRACK_TIMEOUT_DEFAULT); autoResumeTrackTimeout = PreferencesUtils.getAutoResumeTrackTimeout(context);
} }
} }
}; };
private TripStatisticsUpdater markerTripStatisticsUpdater; private TripStatisticsUpdater markerTripStatisticsUpdater;
private WakeLock wakeLock; private WakeLock wakeLock;
private RemoteSensorManager remoteSensorManager; private BluetoothRemoteSensorManager remoteSensorManager;
private Location lastLocation; private Location lastLocation;
private boolean currentSegmentHasLocation; private boolean currentSegmentHasLocation;
private boolean isIdle; // true if idle private boolean isIdle; // true if idle
@@ -222,14 +215,14 @@ public class TrackRecordingService extends Service {
public void onCreate() { public void onCreate() {
super.onCreate(); super.onCreate();
executorService = Executors.newSingleThreadExecutor(); executorService = Executors.newSingleThreadExecutor();
context = this; contentProviderUtils = new ContentProviderUtils(this);
contentProviderUtils = ContentProviderUtils.Factory.get(this);
handler = new Handler(); handler = new Handler();
locationManagerConnector = new LocationManagerConnector(this, handler.getLooper()); locationManagerConnector = new LocationManagerConnector(this, handler.getLooper());
voiceExecutor = new PeriodicTaskExecutor(this, new AnnouncementPeriodicTaskFactory()); voiceExecutor = new PeriodicTaskExecutor(this, new AnnouncementPeriodicTaskFactory());
splitExecutor = new PeriodicTaskExecutor(this, new SplitPeriodicTaskFactory()); splitExecutor = new PeriodicTaskExecutor(this, new SplitPeriodicTaskFactory());
sharedPreferences = PreferencesUtils.getSharedPreferences(this); sharedPreferences = PreferencesUtils.getSharedPreferences(this);
sharedPreferences.registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener); sharedPreferences.registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
notificationManager = new TrackRecordingServiceNotificationManager(this);
// onSharedPreferenceChanged might not set recordingTrackId. // onSharedPreferenceChanged might not set recordingTrackId.
recordingTrackId = PreferencesUtils.RECORDING_TRACK_ID_DEFAULT; recordingTrackId = PreferencesUtils.RECORDING_TRACK_ID_DEFAULT;
@@ -266,12 +259,12 @@ public class TrackRecordingService extends Service {
@Override @Override
public void onDestroy() { public void onDestroy() {
if (remoteSensorManager != null) { if (remoteSensorManager != null) {
RemoteSensorManagerFactory.releaseSystemSensorManager(); remoteSensorManager.stop();
remoteSensorManager = null; remoteSensorManager = null;
} }
// Reverse order from onCreate // Reverse order from onCreate
showNotification(false); showNotification(false); //TODO Why?
handler.removeCallbacks(registerLocationRunnable); handler.removeCallbacks(registerLocationRunnable);
unregisterLocationListener(); unregisterLocationListener();
@@ -404,40 +397,6 @@ public class TrackRecordingService extends Service {
return Long.parseLong(uri.getLastPathSegment()); 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. * Handles start command.
* *
@@ -446,7 +405,8 @@ public class TrackRecordingService extends Service {
*/ */
private void handleStartCommand(Intent intent, int startId) { private void handleStartCommand(Intent intent, int startId) {
// Check if the service is called to resume track (from phone reboot) // Check if the service is called to resume track (from phone reboot)
if (intent != null && intent.getBooleanExtra(RESUME_TRACK_EXTRA_NAME, false) && !shouldResumeTrack()) { boolean resumeTrackRequested = intent.getBooleanExtra(RESUME_TRACK_EXTRA_NAME, false);
if (resumeTrackRequested && !shouldResumeTrack()) {
Log.i(TAG, "Stop resume track."); Log.i(TAG, "Stop resume track.");
updateRecordingState(PreferencesUtils.RECORDING_TRACK_ID_DEFAULT, true); updateRecordingState(PreferencesUtils.RECORDING_TRACK_ID_DEFAULT, true);
stopSelfResult(startId); stopSelfResult(startId);
@@ -464,17 +424,19 @@ public class TrackRecordingService extends Service {
return false; return false;
} }
int retries = PreferencesUtils.getInt(this, R.string.auto_resume_track_current_retry_key, PreferencesUtils.AUTO_RESUME_TRACK_CURRENT_RETRY_DEFAULT); int retries = PreferencesUtils.getAutoResumeTrackCurrentRetryDefault(this);
if (retries >= MAX_AUTO_RESUME_TRACK_RETRY_ATTEMPTS) { if (retries >= MAX_AUTO_RESUME_TRACK_RETRY_ATTEMPTS) {
Log.d(TAG, "Not resuming. Exceeded maximum retry attempts."); Log.d(TAG, "Not resuming. Exceeded maximum retry attempts.");
return false; return false;
} }
PreferencesUtils.setInt(this, R.string.auto_resume_track_current_retry_key, retries + 1); PreferencesUtils.incrementAutoResumeTrackCurrentRetryDefault(this);
if (autoResumeTrackTimeout == PreferencesUtils.AUTO_RESUME_TRACK_TIMEOUT_NEVER) { if (autoResumeTrackTimeout == Integer.parseInt(getResources().getString(R.string.auto_resume_track_timeout_never))) {
Log.d(TAG, "Not resuming. Auto-resume track timeout set to never."); Log.d(TAG, "Not resuming. Auto-resume track timeout set to never.");
return false; return false;
} else if (autoResumeTrackTimeout == PreferencesUtils.AUTO_RESUME_TRACK_TIMEOUT_ALWAYS) { }
if (autoResumeTrackTimeout == Integer.parseInt(getResources().getString(R.string.auto_resume_track_timeout_always))) {
Log.d(TAG, "Resuming. Auto-resume track timeout set to always."); Log.d(TAG, "Resuming. Auto-resume track timeout set to always.");
return true; return true;
} }
@@ -508,13 +470,13 @@ public class TrackRecordingService extends Service {
// Update shared preferences // Update shared preferences
updateRecordingState(trackId, false); updateRecordingState(trackId, false);
PreferencesUtils.setInt(this, R.string.auto_resume_track_current_retry_key, 0); PreferencesUtils.resetAutoResumeTrackCurrentRetryDefault(this);
// Update database // Update database
track.setId(trackId); track.setId(trackId);
track.setName(TrackNameUtils.getTrackName(this, trackId, now)); track.setName(TrackNameUtils.getTrackName(this, trackId, now));
String category = PreferencesUtils.getString(this, R.string.default_activity_key, PreferencesUtils.DEFAULT_ACTIVITY_DEFAULT); String category = PreferencesUtils.getDefaultActivity(this);
track.setCategory(category); track.setCategory(category);
track.setIcon(TrackIconUtils.getIconValue(this, category)); track.setIcon(TrackIconUtils.getIconValue(this, category));
track.setTripStatistics(trackTripStatisticsUpdater.getTripStatistics()); track.setTripStatistics(trackTripStatisticsUpdater.getTripStatistics());
@@ -545,7 +507,7 @@ public class TrackRecordingService extends Service {
} }
markerTripStatisticsUpdater = new TripStatisticsUpdater(markerStartTime); 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()) { while (locationIterator.hasNext()) {
Location location = locationIterator.next(); Location location = locationIterator.next();
@@ -594,7 +556,8 @@ public class TrackRecordingService extends Service {
*/ */
private void startRecording(boolean trackStarted) { private void startRecording(boolean trackStarted) {
// Update instance variables // Update instance variables
remoteSensorManager = RemoteSensorManagerFactory.getSystemSensorManager(this); remoteSensorManager = new BluetoothRemoteSensorManager(this);
remoteSensorManager.start();
lastLocation = null; lastLocation = null;
currentSegmentHasLocation = false; currentSegmentHasLocation = false;
isIdle = false; isIdle = false;
@@ -677,6 +640,8 @@ public class TrackRecordingService extends Service {
} }
endRecording(false, recordingTrackId); endRecording(false, recordingTrackId);
notificationManager.updateContent(getString(R.string.generic_paused));
} }
/** /**
@@ -692,7 +657,7 @@ public class TrackRecordingService extends Service {
// Update instance variables // Update instance variables
if (remoteSensorManager != null) { if (remoteSensorManager != null) {
RemoteSensorManagerFactory.releaseSystemSensorManager(); remoteSensorManager.stop();
remoteSensorManager = null; remoteSensorManager = null;
} }
lastLocation = null; lastLocation = null;
@@ -763,6 +728,8 @@ public class TrackRecordingService extends Service {
return; return;
} }
notificationManager.updateLocation(this, location, recordingGpsAccuracy);
if (!location.hasAccuracy() || location.getAccuracy() >= recordingGpsAccuracy) { if (!location.hasAccuracy() || location.getAccuracy() >= recordingGpsAccuracy) {
Log.d(TAG, "Ignore onLocationChangedAsync. Poor accuracy."); Log.d(TAG, "Ignore onLocationChangedAsync. Poor accuracy.");
return; return;
@@ -798,10 +765,7 @@ public class TrackRecordingService extends Service {
} }
if (!LocationUtils.isValidLocation(lastValidTrackPoint)) { 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); insertLocation(track, location, null);
lastLocation = location; lastLocation = location;
return; return;
@@ -948,33 +912,33 @@ public class TrackRecordingService extends Service {
} }
} }
/**
* Shows the notification.
*
* @param isGpsStarted true if GPS is started
*/
private void showNotification(boolean isGpsStarted) { private void showNotification(boolean isGpsStarted) {
if (isRecording()) { if ((isRecording() && isPaused()) || (!isRecording() && !isGpsStarted)) {
if (isPaused()) { stopForeground(true);
stopForegroundService(); }
} else {
Intent intent = IntentUtils.newIntent(this, TrackDetailActivity.class) if (isRecording() && !isPaused()) {
.putExtra(TrackDetailActivity.EXTRA_TRACK_ID, recordingTrackId); Intent intent = IntentUtils.newIntent(this, TrackDetailActivity.class)
PendingIntent pendingIntent = TaskStackBuilder.create(this) .putExtra(TrackDetailActivity.EXTRA_TRACK_ID, recordingTrackId);
.addParentStack(TrackDetailActivity.class).addNextIntent(intent) PendingIntent pendingIntent = TaskStackBuilder.create(this)
.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT); .addParentStack(TrackDetailActivity.class)
startForegroundService(pendingIntent, R.string.track_record_notification); .addNextIntent(intent)
} .getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT);
} else {
// Not recording notificationManager.updatePendingIntent(pendingIntent);
if (isGpsStarted) { notificationManager.updateContent(getString(R.string.gps_starting));
Intent intent = IntentUtils.newIntent(this, TrackListActivity.class); startForeground(NOTIFICATION_ID, notificationManager.getNotification());
PendingIntent pendingIntent = TaskStackBuilder.create(this) }
.addNextIntent(intent).getPendingIntent(0, 0); if (!isRecording() && isGpsStarted) {
startForegroundService(pendingIntent, R.string.gps_starting); Intent intent = IntentUtils.newIntent(this, TrackListActivity.class);
} else { PendingIntent pendingIntent = TaskStackBuilder.create(this)
stopForegroundService(); .addNextIntent(intent)
} .getPendingIntent(0, 0);
notificationManager.updatePendingIntent(pendingIntent);
notificationManager.updateContent(getString(R.string.gps_starting));
startForeground(NOTIFICATION_ID, notificationManager.getNotification());
} }
} }
@@ -16,7 +16,6 @@
package de.dennisguse.opentracks.services; package de.dennisguse.opentracks.services;
import android.app.ActivityManager;
import android.content.ComponentName; import android.content.ComponentName;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
@@ -29,14 +28,13 @@ import android.widget.Toast;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import java.util.List;
import de.dennisguse.opentracks.BuildConfig; import de.dennisguse.opentracks.BuildConfig;
import de.dennisguse.opentracks.R; import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.TrackEditActivity; import de.dennisguse.opentracks.TrackEditActivity;
import de.dennisguse.opentracks.content.WaypointCreationRequest; import de.dennisguse.opentracks.content.WaypointCreationRequest;
import de.dennisguse.opentracks.util.IntentUtils; import de.dennisguse.opentracks.util.IntentUtils;
import de.dennisguse.opentracks.util.PreferencesUtils; import de.dennisguse.opentracks.util.PreferencesUtils;
import de.dennisguse.opentracks.util.ServiceUtils;
/** /**
* Wrapper for the track recording service. * Wrapper for the track recording service.
@@ -154,7 +152,7 @@ public class TrackRecordingServiceConnection implements ServiceConnection, Death
return; return;
} }
if (!startIfNeeded && !isRecordingServiceRunning(context)) { if (!startIfNeeded && !ServiceUtils.isTrackRecordingServiceRunning(context)) {
Log.d(TAG, "Service is not started. Not binding it."); Log.d(TAG, "Service is not started. Not binding it.");
return; return;
} }
@@ -169,31 +167,6 @@ public class TrackRecordingServiceConnection implements ServiceConnection, Death
context.bindService(new Intent(context, TrackRecordingService.class), this, flags); context.bindService(new Intent(context, TrackRecordingService.class), this, flags);
} }
/**
* Returns true if the recording service is running.
*
* @param context the current context
*/
@Deprecated
public static boolean isRecordingServiceRunning(Context context) {
ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
if (activityManager == null) {
return false;
}
//TODO This approach is deprecated as of API level 26 and should be replaced.
List<ActivityManager.RunningServiceInfo> services = activityManager.getRunningServices(Integer.MAX_VALUE);
for (ActivityManager.RunningServiceInfo serviceInfo : services) {
ComponentName componentName = serviceInfo.service;
String serviceName = componentName.getClassName();
if (TrackRecordingService.class.getName().equals(serviceName)) {
return true;
}
}
return false;
}
/** /**
* Resumes the track recording service connection. * Resumes the track recording service connection.
* *
@@ -201,7 +174,7 @@ public class TrackRecordingServiceConnection implements ServiceConnection, Death
*/ */
public void startConnection(@NonNull Context context) { public void startConnection(@NonNull Context context) {
bindIfStarted(); bindIfStarted();
if (!isRecordingServiceRunning(context)) { if (!ServiceUtils.isTrackRecordingServiceRunning(context)) {
resetRecordingState(context); resetRecordingState(context);
} }
} }
@@ -230,9 +203,9 @@ public class TrackRecordingServiceConnection implements ServiceConnection, Death
if (PreferencesUtils.isRecording(context)) { if (PreferencesUtils.isRecording(context)) {
PreferencesUtils.setLong(context, R.string.recording_track_id_key, PreferencesUtils.RECORDING_TRACK_ID_DEFAULT); PreferencesUtils.setLong(context, R.string.recording_track_id_key, PreferencesUtils.RECORDING_TRACK_ID_DEFAULT);
} }
boolean recordingTrackPaused = PreferencesUtils.getBoolean(context, R.string.recording_track_paused_key, PreferencesUtils.RECORDING_TRACK_PAUSED_DEFAULT); boolean recordingTrackPaused = PreferencesUtils.isRecordingTrackPaused(context);
if (!recordingTrackPaused) { if (!recordingTrackPaused) {
PreferencesUtils.setBoolean(context, R.string.recording_track_paused_key, PreferencesUtils.RECORDING_TRACK_PAUSED_DEFAULT); PreferencesUtils.defaultRecordingTrackPaused(context);
} }
} }
@@ -275,7 +248,7 @@ public class TrackRecordingServiceConnection implements ServiceConnection, Death
try { try {
if (showEditor) { if (showEditor) {
// Need to remember the recordingTrackId before calling endCurrentTrack() as endCurrentTrack() sets the value to -1L. // Need to remember the recordingTrackId before calling endCurrentTrack() as endCurrentTrack() sets the value to -1L.
long recordingTrackId = PreferencesUtils.getLong(context, R.string.recording_track_id_key); long recordingTrackId = PreferencesUtils.getRecordingTrackId(context);
trackRecordingService.endCurrentTrack(); trackRecordingService.endCurrentTrack();
if (PreferencesUtils.isRecording(context)) { if (PreferencesUtils.isRecording(context)) {
Intent intent = IntentUtils.newIntent(context, TrackEditActivity.class) Intent intent = IntentUtils.newIntent(context, TrackEditActivity.class)
@@ -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,11 +21,10 @@ import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCallback; import android.bluetooth.BluetoothGattCallback;
import android.bluetooth.BluetoothGattCharacteristic; import android.bluetooth.BluetoothGattCharacteristic;
import android.bluetooth.BluetoothGattDescriptor; import android.bluetooth.BluetoothGattDescriptor;
import android.bluetooth.BluetoothGattService;
import android.bluetooth.BluetoothProfile; import android.bluetooth.BluetoothProfile;
import android.content.Context; import android.content.Context;
import android.os.Bundle;
import android.os.Handler; import android.os.Handler;
import android.os.Message;
import android.util.Log; import android.util.Log;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
@@ -34,20 +33,19 @@ import java.util.UUID;
import de.dennisguse.opentracks.content.sensor.SensorDataSet; import de.dennisguse.opentracks.content.sensor.SensorDataSet;
import de.dennisguse.opentracks.content.sensor.SensorState; import de.dennisguse.opentracks.content.sensor.SensorState;
import de.dennisguse.opentracks.util.BluetoothUtils;
/** /**
* Manages connection to Bluetooth LE heart rate monitor. * Manages connection to Bluetooth LE heart rate monitor.
* TODO: shutdown connection.
*/ */
public class BluetoothConnectionManager { public class BluetoothConnectionManager {
// Message types sent to handler // Message types sent to handler
static final int MESSAGE_DEVICE_NAME = 1; static final int MESSAGE_CONNECTING = 1;
static final int MESSAGE_READ = 2; static final int MESSAGE_CONNECTED = 2;
static final int MESSAGE_READ = 3;
static final int MESSAGE_DISCONNECTED = 4;
static final String KEY_DEVICE_NAME = "device_name";
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 HEART_RATE_MEASUREMENT_CHAR_UUID = new UUID(0x2A3700001000L, 0x800000805f9b34fbL);
private static final UUID CLIENT_CHARACTERISTIC_CONFIG_UUID = new UUID(0x290200001000L, 0x800000805f9b34fbL); private static final UUID CLIENT_CHARACTERISTIC_CONFIG_UUID = new UUID(0x290200001000L, 0x800000805f9b34fbL);
@@ -58,30 +56,52 @@ public class BluetoothConnectionManager {
private SensorState sensorState; private SensorState sensorState;
private BluetoothGatt bluetoothGatt;
private BluetoothDevice bluetoothDevice;
private BluetoothGattCallback connectCallback = new BluetoothGattCallback() { private BluetoothGattCallback connectCallback = new BluetoothGattCallback() {
@Override @Override
public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) { public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
if (newState == BluetoothProfile.STATE_CONNECTED) { switch (newState) {
gatt.discoverServices(); case BluetoothProfile.STATE_CONNECTING:
setState(SensorState.CONNECTED); Log.d(TAG, "Connecting to sensor: " + gatt.getDevice());
setState(SensorState.CONNECTING);
//Inform about status change handler.obtainMessage(MESSAGE_CONNECTING, gatt.getDevice().getName()).sendToTarget();
Message message = handler.obtainMessage(MESSAGE_DEVICE_NAME); case BluetoothProfile.STATE_CONNECTED:
Bundle bundle = new Bundle(); Log.d(TAG, "Connected to sensor: " + gatt.getDevice());
bundle.putString(KEY_DEVICE_NAME, gatt.getDevice().getName()); setState(SensorState.CONNECTED);
message.setData(bundle);
handler.sendMessage(message); gatt.discoverServices();
return;
handler.obtainMessage(MESSAGE_CONNECTED, gatt.getDevice().getName()).sendToTarget();
break;
case BluetoothProfile.STATE_DISCONNECTING:
Log.d(TAG, "Disconnecting from sensor: " + gatt.getDevice());
setState(SensorState.DISCONNECTING);
case BluetoothProfile.STATE_DISCONNECTED:
Log.d(TAG, "Disconnected from sensor: " + gatt.getDevice());
setState(SensorState.DISCONNECTED);
handler.obtainMessage(MESSAGE_DISCONNECTED, gatt.getDevice().getName()).sendToTarget();
} }
Log.d(TAG, "Could not connect to bluetooth sensor: " + gatt.getDevice());
} }
@Override @Override
public void onServicesDiscovered(@NonNull BluetoothGatt gatt, int status) { public void onServicesDiscovered(@NonNull BluetoothGatt gatt, int status) {
BluetoothGattCharacteristic characteristic = gatt BluetoothGattService service = gatt.getService(BluetoothUtils.HEART_RATE_SERVICE_UUID);
.getService(HEART_RATE_SERVICE_UUID) if (service == null) {
.getCharacteristic(HEART_RATE_MEASUREMENT_CHAR_UUID); 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); gatt.setCharacteristicNotification(characteristic, true);
BluetoothGattDescriptor descriptor = characteristic.getDescriptor(CLIENT_CHARACTERISTIC_CONFIG_UUID); BluetoothGattDescriptor descriptor = characteristic.getDescriptor(CLIENT_CHARACTERISTIC_CONFIG_UUID);
@@ -91,10 +111,11 @@ public class BluetoothConnectionManager {
@Override @Override
public void onCharacteristicChanged(BluetoothGatt gatt, @NonNull BluetoothGattCharacteristic characteristic) { public void onCharacteristicChanged(BluetoothGatt gatt, @NonNull BluetoothGattCharacteristic characteristic) {
int heartRate = BluetoothLEUtils.parseHeartRate(characteristic); int heartRate = BluetoothUtils.parseHeartRate(characteristic);
String deviceName = gatt.getDevice().getName();
Log.d(TAG, "Received heart beat rate: " + heartRate); Log.d(TAG, "Received heart beat rate " + deviceName + ": " + heartRate);
SensorDataSet sensorDataSet = new SensorDataSet(heartRate, gatt.getDevice().getName(), gatt.getDevice().getAddress()); SensorDataSet sensorDataSet = new SensorDataSet(heartRate, deviceName, gatt.getDevice().getAddress());
handler.obtainMessage(MESSAGE_READ, sensorDataSet).sendToTarget(); handler.obtainMessage(MESSAGE_READ, sensorDataSet).sendToTarget();
} }
}; };
@@ -104,45 +125,43 @@ public class BluetoothConnectionManager {
* *
* @param handler a handler for sending messages back to the UI activity * @param handler a handler for sending messages back to the UI activity
*/ */
BluetoothConnectionManager(Context context, Handler handler) { BluetoothConnectionManager(@NonNull Context context, @NonNull BluetoothDevice bluetoothDevice, @NonNull Handler handler) {
this.context = context; this.context = context;
this.bluetoothDevice = bluetoothDevice;
this.handler = handler; this.handler = handler;
this.sensorState = SensorState.NONE; this.sensorState = SensorState.NONE;
} }
/** public synchronized void connect() {
* Gets the sensor state. if (bluetoothGatt != null) {
*/ Log.w(TAG, "Already connected; ignoring.");
}
Log.d(TAG, "Connecting to: " + bluetoothDevice);
bluetoothGatt = bluetoothDevice.connectGatt(this.context, true, this.connectCallback);
setState(SensorState.CONNECTING);
}
public synchronized void disconnect() {
if (bluetoothGatt == null) {
Log.w(TAG, "Cannot disconnect if not connected.");
return;
}
bluetoothGatt.close();
bluetoothGatt = null;
}
public synchronized boolean isSameBluetoothDevice(String address) {
return this.bluetoothDevice.getAddress().equals(address);
}
synchronized SensorState getSensorState() { synchronized SensorState getSensorState() {
return sensorState; return sensorState;
} }
/**
* Sets the sensor state.
*
* @param sensorState the sensor state
*/
private synchronized void setState(SensorState sensorState) { private synchronized void setState(SensorState sensorState) {
this.sensorState = sensorState; this.sensorState = sensorState;
} }
/**
* Resets the bluetooth connection manager.
*/
public synchronized void reset() {
//TODO Disconnect
setState(SensorState.NONE);
}
/**
* Connects to a bluetooth device.
*
* @param bluetoothDevice the bluetooth device
*/
public synchronized void connect(BluetoothDevice bluetoothDevice) {
Log.d(TAG, "connect to: " + bluetoothDevice);
bluetoothDevice.connectGatt(this.context, false, this.connectCallback);
setState(SensorState.CONNECTING);
}
} }
@@ -1,17 +0,0 @@
package de.dennisguse.opentracks.services.sensors;
import android.bluetooth.BluetoothGattCharacteristic;
final class BluetoothLEUtils {
private BluetoothLEUtils() {
}
static int parseHeartRate(BluetoothGattCharacteristic characteristic) {
//DOCUMENTATION https://www.bluetooth.com/specifications/gatt/characteristics/
byte[] raw = characteristic.getValue();
int index = ((raw[0] & 0x1) == 1) ? 2 : 1;
int format = (index == 1) ? BluetoothGattCharacteristic.FORMAT_UINT8 : BluetoothGattCharacteristic.FORMAT_UINT16;
return characteristic.getIntValue(format, index);
}
}
@@ -19,44 +19,50 @@ package de.dennisguse.opentracks.services.sensors;
import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothDevice;
import android.content.Context; import android.content.Context;
import android.content.SharedPreferences;
import android.os.Handler; import android.os.Handler;
import android.os.Looper; import android.os.Looper;
import android.os.Message; import android.os.Message;
import android.util.Log; import android.util.Log;
import android.widget.Toast;
import java.util.ArrayList;
import de.dennisguse.opentracks.R; import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.sensor.SensorDataSet; import de.dennisguse.opentracks.content.sensor.SensorDataSet;
import de.dennisguse.opentracks.content.sensor.SensorState; import de.dennisguse.opentracks.util.BluetoothUtils;
import de.dennisguse.opentracks.util.PreferencesUtils; import de.dennisguse.opentracks.util.PreferencesUtils;
import de.dennisguse.opentracks.util.UnitConversions;
/** /**
* Bluetooth sensor manager. * Bluetooth LE sensor manager.
* TODO: Handle a BluetoothGatt.STATE_DISCONNECTED * Should only be instantiated once!
* *
* @author Sandor Dornbush * @author Sandor Dornbush
*/ */
public class BluetoothRemoteSensorManager extends RemoteSensorManager { public class BluetoothRemoteSensorManager {
public static final long MAX_SENSOR_DATE_SET_AGE_MS = 5000;
private static final String TAG = BluetoothConnectionManager.class.getSimpleName(); 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; private final Context context;
private final BluetoothConnectionManager bluetoothConnectionManager; private final SharedPreferences sharedPreferences;
private SensorDataSet sensorDataSet = null;
// Handler that gets information back from the bluetoothConnectionManager // Handler that gets information back from the bluetoothConnectionManager
private final Handler messageHandler = new Handler(Looper.getMainLooper()) { private final Handler messageHandler = new Handler(Looper.getMainLooper()) {
@Override @Override
public void handleMessage(Message message) { public void handleMessage(Message message) {
String toastMessage;
switch (message.what) { switch (message.what) {
case BluetoothConnectionManager.MESSAGE_DEVICE_NAME: case BluetoothConnectionManager.MESSAGE_CONNECTING:
String deviceName = message.getData().getString(BluetoothConnectionManager.KEY_DEVICE_NAME); //Ignore for now.
toastMessage = context.getString(R.string.settings_sensor_connecting, message.obj);
Toast.makeText(context, toastMessage, Toast.LENGTH_LONG).show();
break;
case BluetoothConnectionManager.MESSAGE_CONNECTED:
toastMessage = context.getString(R.string.settings_sensor_connected, message.obj);
Toast.makeText(context, toastMessage, Toast.LENGTH_LONG).show();
break; break;
case BluetoothConnectionManager.MESSAGE_READ: case BluetoothConnectionManager.MESSAGE_READ:
if (!(message.obj instanceof SensorDataSet)) { if (!(message.obj instanceof SensorDataSet)) {
@@ -66,98 +72,109 @@ public class BluetoothRemoteSensorManager extends RemoteSensorManager {
sensorDataSet = (SensorDataSet) message.obj; sensorDataSet = (SensorDataSet) message.obj;
} }
break; break;
case BluetoothConnectionManager.MESSAGE_DISCONNECTED:
toastMessage = context.getString(R.string.settings_sensor_disconnected, message.obj);
Toast.makeText(context, toastMessage, Toast.LENGTH_LONG).show();
break;
default: default:
Log.e(TAG, "Got an undefined case. Please check.");
break; break;
} }
} }
}; };
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_heart_rate_key, key)) {
String address = PreferencesUtils.getBluetoothHeartRateSensorAddress(context);
if (address.equals(PreferencesUtils.BLUETOOTH_SENSOR_DEFAULT)) {
stop();
return;
}
if (bluetoothConnectionManager.isSameBluetoothDevice(address)) {
return;
}
disconnect();
startCurrentSensor();
}
}
};
private SensorDataSet sensorDataSet = null;
private BluetoothConnectionManager bluetoothConnectionManager;
/** /**
* @param context the context * @param context the context
*/ */
BluetoothRemoteSensorManager(Context context) { public BluetoothRemoteSensorManager(Context context) {
this.context = context; this.context = context;
bluetoothConnectionManager = new BluetoothConnectionManager(context, messageHandler); sharedPreferences = PreferencesUtils.getSharedPreferences(context);
} }
private static BluetoothAdapter getDefaultBluetoothAdapter() { public void start() {
// If from the main application thread, return directly sharedPreferences.registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
if (Thread.currentThread().equals(Looper.getMainLooper().getThread())) { startCurrentSensor();
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);
} }
@Override public void stop() {
disconnect();
sharedPreferences.unregisterOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
}
public boolean isEnabled() { public boolean isEnabled() {
return bluetoothAdapter != null && bluetoothAdapter.isEnabled(); return bluetoothAdapter != null && bluetoothAdapter.isEnabled();
} }
@Override public SensorDataSet getSensorDataSet() {
protected void setUpChannel() { return sensorDataSet;
}
public boolean isSensorDataSetValid() {
SensorDataSet sensorDataSet = getSensorDataSet();
if (sensorDataSet == null) {
return false;
}
return sensorDataSet.isRecent(MAX_SENSOR_DATE_SET_AGE_MS);
}
private void startCurrentSensor() {
if (!isEnabled()) { if (!isEnabled()) {
Log.w(TAG, "Bluetooth not enabled."); Log.w(TAG, "Bluetooth not enabled.");
return; return;
} }
String address = PreferencesUtils.getString(context, R.string.bluetooth_sensor_key, PreferencesUtils.BLUETOOTH_SENSOR_DEFAULT); // String address = PreferencesUtils.getString(context, R.string.settings_sensor_bluetooth_heart_rate_key, PreferencesUtils.BLUETOOTH_SENSOR_DEFAULT);
String address = PreferencesUtils.getBluetoothHeartRateSensorAddress(context);
if (PreferencesUtils.BLUETOOTH_SENSOR_DEFAULT.equals(address)) { if (PreferencesUtils.BLUETOOTH_SENSOR_DEFAULT.equals(address)) {
Log.w(TAG, "No bluetooth address."); Log.w(TAG, "No bluetooth address.");
return; return;
} }
Log.w(TAG, "Connecting to bluetooth address: " + address); Log.i(TAG, "Connecting to bluetooth address: " + address);
BluetoothDevice device; BluetoothDevice device;
try { try {
device = bluetoothAdapter.getRemoteDevice(address); device = bluetoothAdapter.getRemoteDevice(address);
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
Log.d(TAG, "Unable to get remote device for: " + address, e); Log.w(TAG, "Unable to get remote device for: " + address, e);
String toastMessage = context.getString(R.string.sensor_not_known, address);
Toast.makeText(context, toastMessage, Toast.LENGTH_LONG).show();
return; return;
} }
bluetoothConnectionManager.connect(device);
disconnect();
bluetoothConnectionManager = new BluetoothConnectionManager(context, device, messageHandler);
bluetoothConnectionManager.connect();
} }
@Override private void disconnect() {
protected void tearDownChannel() { if (bluetoothConnectionManager != null) {
bluetoothConnectionManager.reset(); bluetoothConnectionManager.disconnect();
} bluetoothConnectionManager = null;
}
@Override
public SensorState getSensorState() {
return bluetoothConnectionManager.getSensorState();
}
@Override
public SensorDataSet getSensorDataSet() {
return sensorDataSet;
} }
} }
@@ -1,135 +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.sensors;
import android.util.Log;
import java.util.Timer;
import java.util.TimerTask;
import de.dennisguse.opentracks.content.sensor.SensorDataSet;
import de.dennisguse.opentracks.content.sensor.SensorState;
/**
* Manage the connection to a remote sensor.
*
* @author Sandor Dornbush
*/
public abstract class RemoteSensorManager {
public static final long MAX_SENSOR_DATE_SET_AGE = 5000;
public static final long MAX_SENSOR_STATE_AGE = 20000;
private static final String TAG = RemoteSensorManager.class.getSimpleName();
private static final int RETRY_PERIOD = 20000;
private SensorState sensorState = SensorState.NONE;
private long sensorStateTimestamp = System.currentTimeMillis();
private TimerTask timerTask;
private Timer timer;
/**
* Returns true if the sensor is enabled.
*/
public abstract boolean isEnabled();
/**
* Sets up the sensor channel.
*/
protected abstract void setUpChannel();
/**
* Tears down the sensor channel.
*/
protected abstract void tearDownChannel();
/**
* Gets the sensor data set.
*/
public abstract SensorDataSet getSensorDataSet();
/**
* Starts the sensor.
*/
public void startSensor() {
setUpChannel();
timerTask = new TimerTask() {
@Override
public void run() {
switch (getSensorState()) {
case CONNECTING:
if (System.currentTimeMillis() - sensorStateTimestamp > MAX_SENSOR_STATE_AGE) {
Log.i(TAG, "Retry setUpChannel");
setUpChannel();
}
break;
case DISCONNECTED:
setUpChannel();
break;
default: // NONE, CONNECTED or SENDING
}
}
};
timer = new Timer(RemoteSensorManager.class.getSimpleName());
timer.schedule(timerTask, RETRY_PERIOD, RETRY_PERIOD);
}
/**
* Stops the sensor.
*/
public void stopSensor() {
if (timerTask != null) {
timerTask.cancel();
timerTask = null;
}
if (timer != null) {
timer.cancel();
timer.purge();
timer = null;
}
tearDownChannel();
}
/**
* Gets the sensor state.
*/
public SensorState getSensorState() {
return sensorState;
}
/**
* Sets the sensor state.
*
* @param sensorState the sensor state
*/
public void setSensorState(SensorState sensorState) {
sensorStateTimestamp = System.currentTimeMillis();
this.sensorState = sensorState;
}
/**
* Returns true if the sensor data set is valid.
*/
public boolean isSensorDataSetValid() {
SensorDataSet sensorDataSet = getSensorDataSet();
if (sensorDataSet == null) {
return false;
}
return sensorDataSet.isRecent(MAX_SENSOR_DATE_SET_AGE);
}
}
@@ -1,90 +0,0 @@
/*
* Copyright 2009 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package de.dennisguse.opentracks.services.sensors;
import android.content.Context;
/**
* A factory of {@link RemoteSensorManager}.
*
* @author Sandor Dornbush
*/
public class RemoteSensorManagerFactory {
private static RemoteSensorManager remoteSensorManagerSystem = null;
//TODO Check if still needed? Is there a missing features?
private static RemoteSensorManager remoteSensorManagerTemporary = null;
private RemoteSensorManagerFactory() {
}
/**
* Gets the system sensor manager.
*
* @param context the context
*/
public static RemoteSensorManager getSystemSensorManager(Context context) {
releaseSensorManagerTemporary();
releaseSystemSensorManager();
remoteSensorManagerSystem = getSensorManager(context);
remoteSensorManagerSystem.startSensor();
return remoteSensorManagerSystem;
}
/**
* Releases the system sensor manager.
*/
public static void releaseSystemSensorManager() {
if (remoteSensorManagerSystem != null) {
remoteSensorManagerSystem.stopSensor();
}
remoteSensorManagerSystem = null;
}
/**
* Gets the temp sensor manager.
*/
public static RemoteSensorManager getSensorManagerTemporary(Context context) {
releaseSensorManagerTemporary();
if (remoteSensorManagerSystem != null) {
return null;
}
remoteSensorManagerTemporary = getSensorManager(context);
remoteSensorManagerTemporary.startSensor();
return remoteSensorManagerTemporary;
}
/**
* Releases the temp sensor manager.
*/
private static void releaseSensorManagerTemporary() {
if (remoteSensorManagerTemporary != null) {
remoteSensorManagerTemporary.stopSensor();
}
remoteSensorManagerTemporary = null;
}
/**
* Gets the sensor manager.
*
* @param context the context
*/
private static RemoteSensorManager getSensorManager(Context context) {
return new BluetoothRemoteSensorManager(context);
}
}
@@ -34,6 +34,7 @@ import de.dennisguse.opentracks.services.TrackRecordingService;
import de.dennisguse.opentracks.stats.TripStatistics; import de.dennisguse.opentracks.stats.TripStatistics;
import de.dennisguse.opentracks.util.PreferencesUtils; import de.dennisguse.opentracks.util.PreferencesUtils;
import de.dennisguse.opentracks.util.StringUtils; import de.dennisguse.opentracks.util.StringUtils;
import de.dennisguse.opentracks.util.TTSUtils;
import de.dennisguse.opentracks.util.UnitConversions; import de.dennisguse.opentracks.util.UnitConversions;
/** /**
@@ -193,7 +194,7 @@ public class AnnouncementPeriodicTask implements PeriodicTask {
* @param announcement the announcement * @param announcement the announcement
*/ */
private void speakAnnouncement(String announcement) { private void speakAnnouncement(String announcement) {
int result = audioManager.requestAudioFocus(null, TextToSpeech.Engine.DEFAULT_STREAM, AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK); int result = audioManager.requestAudioFocus(null, TTSUtils.getTTSStream(), AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK);
if (result == AudioManager.AUDIOFOCUS_REQUEST_FAILED) { if (result == AudioManager.AUDIOFOCUS_REQUEST_FAILED) {
Log.w(TAG, "Failed to request audio focus."); Log.w(TAG, "Failed to request audio focus.");
} }
@@ -17,9 +17,9 @@ package de.dennisguse.opentracks.services.tasks;
import android.util.Log; import android.util.Log;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.services.TrackRecordingService; import de.dennisguse.opentracks.services.TrackRecordingService;
import de.dennisguse.opentracks.stats.TripStatistics; import de.dennisguse.opentracks.stats.TripStatistics;
import de.dennisguse.opentracks.util.PreferencesUtils;
import de.dennisguse.opentracks.util.UnitConversions; import de.dennisguse.opentracks.util.UnitConversions;
/** /**
@@ -35,13 +35,14 @@ public class PeriodicTaskExecutor {
private final TrackRecordingService trackRecordingService; private final TrackRecordingService trackRecordingService;
private final PeriodicTaskFactory periodicTaskFactory; private final PeriodicTaskFactory periodicTaskFactory;
private final int TASK_FREQUENCY_OFF;
/** /**
* The task frequency. * The task frequency.
* A positive value is a time frequency (minutes). * A positive value is a time frequency (minutes).
* A negative value is a distance frequency (km or mi). * A negative value is a distance frequency (km or mi).
* A zero value is to turn off periodic task. * A zero value is to turn off periodic task.
*/ */
private int taskFrequency = PreferencesUtils.FREQUENCY_OFF; private int taskFrequency;
private PeriodicTask periodicTask; private PeriodicTask periodicTask;
@@ -56,6 +57,9 @@ public class PeriodicTaskExecutor {
public PeriodicTaskExecutor(TrackRecordingService trackRecordingService, PeriodicTaskFactory periodicTaskFactory) { public PeriodicTaskExecutor(TrackRecordingService trackRecordingService, PeriodicTaskFactory periodicTaskFactory) {
this.trackRecordingService = trackRecordingService; this.trackRecordingService = trackRecordingService;
this.periodicTaskFactory = periodicTaskFactory; 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.shutdown();
timerTaskExecutor = null; timerTaskExecutor = null;
} }
if (taskFrequency == PreferencesUtils.FREQUENCY_OFF) { if (taskFrequency == TASK_FREQUENCY_OFF) {
Log.d(TAG, "Task frequency is off."); Log.d(TAG, "Task frequency is off.");
return; return;
} }
@@ -32,6 +32,7 @@ import androidx.preference.PreferenceDialogFragmentCompat;
import de.dennisguse.opentracks.R; import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.fragments.ChooseActivityTypeDialogFragment; import de.dennisguse.opentracks.fragments.ChooseActivityTypeDialogFragment;
import de.dennisguse.opentracks.util.HackUtils;
import de.dennisguse.opentracks.util.PreferencesUtils; import de.dennisguse.opentracks.util.PreferencesUtils;
import de.dennisguse.opentracks.util.TrackIconUtils; import de.dennisguse.opentracks.util.TrackIconUtils;
@@ -42,14 +43,6 @@ import de.dennisguse.opentracks.util.TrackIconUtils;
*/ */
public class ActivityTypePreference extends DialogPreference { public class ActivityTypePreference extends DialogPreference {
private SummaryProvider<DialogPreference> summaryProvider = new SummaryProvider<DialogPreference>() {
@Override
public CharSequence provideSummary(DialogPreference preference) {
String defaultActivity = PreferencesUtils.getString(ActivityTypePreference.this.getContext(), R.string.default_activity_key, PreferencesUtils.DEFAULT_ACTIVITY_DEFAULT);
return defaultActivity != null && !defaultActivity.equals(PreferencesUtils.DEFAULT_ACTIVITY_DEFAULT) ? defaultActivity : getContext().getString(R.string.value_unknown);
}
};
public ActivityTypePreference(Context context, AttributeSet attrs) { public ActivityTypePreference(Context context, AttributeSet attrs) {
super(context, attrs); super(context, attrs);
setDialogLayoutResource(R.layout.preference_activity_type); setDialogLayoutResource(R.layout.preference_activity_type);
@@ -57,8 +50,14 @@ public class ActivityTypePreference extends DialogPreference {
setNegativeButtonText(android.R.string.cancel); setNegativeButtonText(android.R.string.cancel);
setDialogIcon(null); setDialogIcon(null);
setPersistent(true); setPersistent(true);
//TODO: for some reason the summary is not updated; after changing it. Enable summaryprovider, when fixed.
// setSummaryProvider(summaryProvider); SummaryProvider<DialogPreference> summaryProvider = new SummaryProvider<DialogPreference>() {
@Override
public CharSequence provideSummary(DialogPreference preference) {
return PreferencesUtils.getDefaultActivity(ActivityTypePreference.this.getContext());
}
};
setSummaryProvider(summaryProvider);
} }
@Override @Override
@@ -87,7 +86,7 @@ public class ActivityTypePreference extends DialogPreference {
final Context context = getActivity(); final Context context = getActivity();
textView = view.findViewById(R.id.activity_type_preference_text_view); textView = view.findViewById(R.id.activity_type_preference_text_view);
String category = PreferencesUtils.getString(context, R.string.default_activity_key, PreferencesUtils.DEFAULT_ACTIVITY_DEFAULT); String category = PreferencesUtils.getDefaultActivity(context);
textView.setText(category); textView.setText(category);
ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(context, R.array.activity_types, android.R.layout.simple_dropdown_item_1line); ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(context, R.array.activity_types, android.R.layout.simple_dropdown_item_1line);
textView.setAdapter(adapter); textView.setAdapter(adapter);
@@ -132,7 +131,7 @@ public class ActivityTypePreference extends DialogPreference {
} }
private void showIconSelectDialog() { private void showIconSelectDialog() {
String category = PreferencesUtils.getString(getActivity(), R.string.default_activity_key, PreferencesUtils.DEFAULT_ACTIVITY_DEFAULT); String category = PreferencesUtils.getDefaultActivity(getActivity());
ChooseActivityTypeDialogFragment.showDialog(getActivity().getSupportFragmentManager(), category); ChooseActivityTypeDialogFragment.showDialog(getActivity().getSupportFragmentManager(), category);
} }
@@ -141,7 +140,8 @@ public class ActivityTypePreference extends DialogPreference {
if (positiveResult) { if (positiveResult) {
String newDefaultActivity = textView.getText().toString(); String newDefaultActivity = textView.getText().toString();
if (getPreference().callChangeListener(newDefaultActivity)) { if (getPreference().callChangeListener(newDefaultActivity)) {
PreferencesUtils.setString(getActivity(), R.string.default_activity_key, newDefaultActivity); PreferencesUtils.setDefaultActivity(getActivity(), newDefaultActivity);
HackUtils.invalidatePreference(getPreference());
} }
} }
} }
@@ -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, null);
}
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,199 @@
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;
/**
* Preference to select a discoverable Bluetooth LE device.
* Based upon ListPreference.
*/
public class BluetoothLePreference extends DialogPreference {
private final static String TAG = BluetoothLePreference.class.getSimpleName();
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() {
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(R.string.value_none), 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,213 +1,163 @@
package de.dennisguse.opentracks.settings; package de.dennisguse.opentracks.settings;
import android.bluetooth.BluetoothAdapter;
import android.content.Context; 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.R;
import de.dennisguse.opentracks.util.BluetoothDeviceUtils;
import de.dennisguse.opentracks.util.PreferencesUtils; import de.dennisguse.opentracks.util.PreferencesUtils;
import de.dennisguse.opentracks.util.UnitConversions; import de.dennisguse.opentracks.util.UnitConversions;
final class PreferenceHelper { final class PreferenceHelper {
/** static String[] getMinRecordingIntervalEntries(Context context) {
* Sets the min recording interval options. String[] entryValues = context.getResources().getStringArray(R.array.min_recording_interval_values);
* String[] entries = new String[entryValues.length];
* @param options the options for (int i = 0; i < entryValues.length; i++) {
* @param values the values int value = Integer.parseInt(entryValues[i]);
*/
static void setMinRecordingIntervalOptions(Context context, String[] options, String[] values) { if (value == PreferencesUtils.getMinRecordingIntervalAdaptAccuracy(context)) {
for (int i = 0; i < values.length; i++) { entries[i] = context.getString(R.string.value_adapt_accuracy);
int value = Integer.parseInt(values[i]); } else if (value == PreferencesUtils.getMinRecordingIntervalAdaptBatteryLife(context)) {
switch (value) { entries[i] = context.getString(R.string.value_adapt_battery_life);
case PreferencesUtils.MIN_RECORDING_INTERVAL_ADAPT_BATTERY_LIFE: } else if (value == PreferencesUtils.getMinRecordingIntervalDefault(context)) {
options[i] = context.getString(R.string.value_adapt_battery_life); entries[i] = context.getString(R.string.value_smallest_recommended);
break; } else {
case PreferencesUtils.MIN_RECORDING_INTERVAL_ADAPT_ACCURACY: entries[i] = value < 60 ? context.getString(R.string.value_integer_second, value) : context.getString(R.string.value_integer_minute, value / 60);
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);
} }
} }
return entries;
} }
/** static String[] getRecordingDistanceIntervalEntries(Context context, boolean metricUnits) {
* Sets the recording distance interval options. String[] entryValues = context.getResources().getStringArray(R.array.recording_distance_interval_values);
* String[] entries = new String[entryValues.length];
* @param options the options
* @param values the values final int recordingDistanceIntervalDefault = PreferencesUtils.getRecordingDistanceIntervalDefault(context);
* @param metricUnits true for metric units
*/ for (int i = 0; i < entryValues.length; i++) {
static void setRecordingDistanceIntervalOptions(Context context, String[] options, String[] values, boolean metricUnits) { int value = Integer.parseInt(entryValues[i]);
for (int i = 0; i < values.length; i++) {
int value = Integer.parseInt(values[i]);
String displayValue; String displayValue;
if (metricUnits) { if (metricUnits) {
displayValue = context.getString(R.string.value_integer_meter, value); displayValue = context.getString(R.string.value_integer_meter, value);
if (value == PreferencesUtils.RECORDING_DISTANCE_INTERVAL_DEFAULT) { if (value == recordingDistanceIntervalDefault) {
options[i] = context.getString(R.string.value_integer_meter_recommended, value); entries[i] = context.getString(R.string.value_integer_meter_recommended, value);
} else { } else {
options[i] = displayValue; entries[i] = displayValue;
} }
} else { } else {
int feet = (int) (value * UnitConversions.M_TO_FT); int feet = (int) (value * UnitConversions.M_TO_FT);
displayValue = context.getString(R.string.value_integer_feet, feet); displayValue = context.getString(R.string.value_integer_feet, feet);
if (value == PreferencesUtils.RECORDING_DISTANCE_INTERVAL_DEFAULT) { if (value == recordingDistanceIntervalDefault) {
options[i] = context.getString(R.string.value_integer_feet_recommended, feet); entries[i] = context.getString(R.string.value_integer_feet_recommended, feet);
} else { } else {
options[i] = displayValue; entries[i] = displayValue;
} }
} }
} }
return entries;
} }
/** static String[] getMaxRecordingDistanceEntries(Context context, boolean metricUnits) {
* Sets the max recording distance options. String[] entryValues = context.getResources().getStringArray(R.array.max_recording_distance_values);
* String[] entries = new String[entryValues.length];
* @param options the options
* @param values the values final int maxRecordingDistanceDefault = Integer.parseInt(context.getResources().getString(R.string.max_recording_distance_default));
* @param metricUnits true for metric units
*/ for (int i = 0; i < entryValues.length; i++) {
static void setMaxRecordingDistanceOptions(Context context, String[] options, String[] values, boolean metricUnits) { int value = Integer.parseInt(entryValues[i]);
for (int i = 0; i < values.length; i++) {
int value = Integer.parseInt(values[i]);
String displayValue; String displayValue;
if (metricUnits) { if (metricUnits) {
displayValue = context.getString(R.string.value_integer_meter, value); displayValue = context.getString(R.string.value_integer_meter, value);
if (value == PreferencesUtils.MAX_RECORDING_DISTANCE_DEFAULT) { if (value == maxRecordingDistanceDefault) {
options[i] = context.getString(R.string.value_integer_meter_recommended, value); entries[i] = context.getString(R.string.value_integer_meter_recommended, value);
} else { } else {
options[i] = displayValue; entries[i] = displayValue;
} }
} else { } else {
int feet = (int) (value * UnitConversions.M_TO_FT); int feet = (int) (value * UnitConversions.M_TO_FT);
if (feet < 2000) { if (feet < 2000) {
displayValue = context.getString(R.string.value_integer_feet, feet); displayValue = context.getString(R.string.value_integer_feet, feet);
if (value == PreferencesUtils.MAX_RECORDING_DISTANCE_DEFAULT) { if (value == maxRecordingDistanceDefault) {
options[i] = context.getString(R.string.value_integer_feet_recommended, feet); entries[i] = context.getString(R.string.value_integer_feet_recommended, feet);
} else { } else {
options[i] = displayValue; entries[i] = displayValue;
} }
} else { } else {
double mile = feet * UnitConversions.FT_TO_MI; double mile = feet * UnitConversions.FT_TO_MI;
displayValue = context.getString(R.string.value_float_mile, mile); displayValue = context.getString(R.string.value_float_mile, mile);
options[i] = displayValue; entries[i] = displayValue;
} }
} }
} }
return entries;
} }
/** static String[] getRecordingGpsAccuracyEntries(Context context, boolean metricUnits) {
* Sets the recording gps accuracy options. String[] entryValues = context.getResources().getStringArray(R.array.recording_gps_accuracy_values);
* String[] entries = new String[entryValues.length];
* @param options the options
* @param values the values final int recordingGPSAccuracyDefault = Integer.parseInt(context.getResources().getString(R.string.recording_gps_accuracy_default));
* @param metricUnits true for metric units 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));
static void setRecordingGpsAccuracyOptions(Context context, String[] options, String[] values, boolean metricUnits) {
for (int i = 0; i < values.length; i++) { for (int i = 0; i < entryValues.length; i++) {
int value = Integer.parseInt(values[i]); int value = Integer.parseInt(entryValues[i]);
String displayValue; String displayValue;
if (metricUnits) { if (metricUnits) {
displayValue = context.getString(R.string.value_integer_meter, value); displayValue = context.getString(R.string.value_integer_meter, value);
switch (value) { if (value == recordingGPSAccuracyDefault) {
case PreferencesUtils.RECORDING_GPS_ACCURACY_DEFAULT: entries[i] = context.getString(R.string.value_integer_meter_recommended, value);
options[i] = context.getString(R.string.value_integer_meter_recommended, value); } else if (value == recordingGPSAccuracyExcellent) {
break; entries[i] = context.getString(R.string.value_integer_meter_excellent_gps, value);
case PreferencesUtils.RECORDING_GPS_ACCURACY_EXCELLENT: } else if (value == recordingGPSAccuracyPoor) {
options[i] = context.getString(R.string.value_integer_meter_excellent_gps, value); entries[i] = context.getString(R.string.value_integer_meter_poor_gps, value);
break; } else {
case PreferencesUtils.RECORDING_GPS_ACCURACY_POOR: entries[i] = displayValue;
options[i] = context.getString(R.string.value_integer_meter_poor_gps, value);
break;
default:
options[i] = displayValue;
} }
} else { } else {
int feet = (int) (value * UnitConversions.M_TO_FT); int feet = (int) (value * UnitConversions.M_TO_FT);
if (feet < 2000) { if (feet < 2000) {
displayValue = context.getString(R.string.value_integer_feet, feet); displayValue = context.getString(R.string.value_integer_feet, feet);
switch (value) {
case PreferencesUtils.RECORDING_GPS_ACCURACY_DEFAULT: if (value == recordingGPSAccuracyDefault) {
options[i] = context.getString(R.string.value_integer_feet_recommended, feet); entries[i] = context.getString(R.string.value_integer_feet_recommended, feet);
break; } else if (value == recordingGPSAccuracyExcellent) {
case PreferencesUtils.RECORDING_GPS_ACCURACY_EXCELLENT: entries[i] = context.getString(R.string.value_integer_feet_excellent_gps, feet);
options[i] = context.getString(R.string.value_integer_feet_excellent_gps, feet); } else {
break; entries[i] = displayValue;
default:
options[i] = displayValue;
} }
} else { } else {
double mile = feet * UnitConversions.FT_TO_MI; double mile = feet * UnitConversions.FT_TO_MI;
displayValue = context.getString(R.string.value_float_mile, mile); displayValue = context.getString(R.string.value_float_mile, mile);
if (value == PreferencesUtils.RECORDING_GPS_ACCURACY_POOR) { if (value == recordingGPSAccuracyPoor) {
options[i] = context.getString(R.string.value_float_mile_poor_gps, mile); entries[i] = context.getString(R.string.value_float_mile_poor_gps, mile);
} else { } else {
options[i] = displayValue; entries[i] = displayValue;
} }
} }
} }
} }
return entries;
} }
/** static String[] getAutoResumeTrackTimeoutEntries(Context context) {
* Sets the auto resume track timeout options. String[] entryValues = context.getResources().getStringArray(R.array.auto_resume_track_timeout_values);
* String[] entries = new String[entryValues.length];
* @param options the options
* @param values the values for (int i = 0; i < entryValues.length; i++) {
*/ int value = Integer.parseInt(entryValues[i]);
static void setAutoResumeTrackTimeoutOptions(Context context, String[] options, String[] values) { if (value == Integer.parseInt(context.getResources().getString(R.string.auto_resume_track_timeout_never))) {
for (int i = 0; i < values.length; i++) { entries[i] = context.getString(R.string.value_never);
int value = Integer.parseInt(values[i]); } else if (value == Integer.parseInt(context.getResources().getString(R.string.auto_resume_track_timeout_always))) {
switch (value) { entries[i] = context.getString(R.string.value_always);
case PreferencesUtils.AUTO_RESUME_TRACK_TIMEOUT_NEVER: } else {
options[i] = context.getString(R.string.value_never); entries[i] = context.getString(R.string.value_integer_minute, value);
break;
case PreferencesUtils.AUTO_RESUME_TRACK_TIMEOUT_ALWAYS:
options[i] = context.getString(R.string.value_always);
break;
default:
options[i] = context.getString(R.string.value_integer_minute, value);
} }
} }
}
/** return entries;
* Configures the bluetooth sensor.
*/
static void configureBluetoothSensorList(ListPreference preference) {
Context context = preference.getContext();
String value = PreferencesUtils.getString(context, R.string.bluetooth_sensor_key, PreferencesUtils.BLUETOOTH_SENSOR_DEFAULT);
List<String> devicesNameList = new ArrayList<>();
List<String> devicesAddressList = new ArrayList<>();
BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
if (bluetoothAdapter != null) {
BluetoothDeviceUtils.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.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);
} }
} }
@@ -5,6 +5,7 @@ import android.os.Bundle;
import android.util.AttributeSet; import android.util.AttributeSet;
import android.widget.Toast; import android.widget.Toast;
import androidx.fragment.app.FragmentActivity;
import androidx.preference.DialogPreference; import androidx.preference.DialogPreference;
import androidx.preference.PreferenceDialogFragmentCompat; import androidx.preference.PreferenceDialogFragmentCompat;
@@ -17,6 +18,10 @@ public class ResetDialogPreference extends DialogPreference {
super(context, attrs); super(context, attrs);
} }
interface ResetCallback {
void onReset();
}
public static class ResetPreferenceDialog extends PreferenceDialogFragmentCompat { public static class ResetPreferenceDialog extends PreferenceDialogFragmentCompat {
static PreferenceDialogFragmentCompat newInstance(String preferenceKey) { static PreferenceDialogFragmentCompat newInstance(String preferenceKey) {
@@ -31,9 +36,14 @@ public class ResetDialogPreference extends DialogPreference {
@Override @Override
public void onDialogClosed(boolean positiveResult) { public void onDialogClosed(boolean positiveResult) {
if (positiveResult) { if (positiveResult) {
PreferencesUtils.resetPreferences(getActivity(), true); FragmentActivity activity = getActivity();
Toast.makeText(getActivity(), R.string.settings_reset_done, Toast.LENGTH_SHORT).show(); PreferencesUtils.resetPreferences(activity, true);
Toast.makeText(activity, R.string.settings_reset_done, Toast.LENGTH_SHORT).show();
if (activity instanceof ResetCallback) {
((ResetCallback) activity).onReset();
}
} }
} }
} }
@@ -2,6 +2,7 @@ package de.dennisguse.opentracks.settings;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.os.Bundle; import android.os.Bundle;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar; import androidx.appcompat.widget.Toolbar;
@@ -12,12 +13,16 @@ import androidx.preference.PreferenceFragmentCompat;
import de.dennisguse.opentracks.R; import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.fragments.ChooseActivityTypeDialogFragment; 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.PreferencesUtils;
import de.dennisguse.opentracks.util.StringUtils; import de.dennisguse.opentracks.util.StringUtils;
public class SettingsActivity extends AppCompatActivity implements ChooseActivityTypeDialogFragment.ChooseActivityTypeCaller { public class SettingsActivity extends AppCompatActivity implements ChooseActivityTypeDialogFragment.ChooseActivityTypeCaller, ResetDialogPreference.ResetCallback {
private PrefsFragment prefsFragment = new PrefsFragment(); private static final String TAG = SettingsActivity.class.getSimpleName();
private PrefsFragment prefsFragment;
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
@@ -29,7 +34,7 @@ public class SettingsActivity extends AppCompatActivity implements ChooseActivit
toolbar.setTitle(R.string.menu_settings); toolbar.setTitle(R.string.menu_settings);
setSupportActionBar(toolbar); setSupportActionBar(toolbar);
getSupportFragmentManager().beginTransaction().replace(R.id.settings_fragment, prefsFragment).commit(); onReset();
} }
@Override @Override
@@ -37,6 +42,12 @@ public class SettingsActivity extends AppCompatActivity implements ChooseActivit
prefsFragment.setDefaultActivity(iconValue); prefsFragment.setDefaultActivity(iconValue);
} }
@Override
public void onReset() {
prefsFragment = new PrefsFragment();
getSupportFragmentManager().beginTransaction().replace(R.id.settings_fragment, prefsFragment).commit();
}
public static class PrefsFragment extends PreferenceFragmentCompat { public static class PrefsFragment extends PreferenceFragmentCompat {
/* /*
@@ -46,14 +57,15 @@ public class SettingsActivity extends AppCompatActivity implements ChooseActivit
private final SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener = new SharedPreferences.OnSharedPreferenceChangeListener() { private final SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener = new SharedPreferences.OnSharedPreferenceChangeListener() {
@Override @Override
public void onSharedPreferenceChanged(SharedPreferences preferences, String key) { public void onSharedPreferenceChanged(SharedPreferences preferences, String key) {
getActivity().runOnUiThread(new Runnable() { if (PreferencesUtils.isKey(getActivity(), R.string.recording_track_id_key, key)) {
@Override getActivity().runOnUiThread(new Runnable() {
public void run() { @Override
updateUI(); public void run() {
} updateReset();
}); }
});
if (key == null || key.equals(PreferencesUtils.getKey(getActivity(), R.string.stats_units_key))) { }
if (PreferencesUtils.isKey(getActivity(), R.string.stats_units_key, key)) {
getActivity().runOnUiThread(new Runnable() { getActivity().runOnUiThread(new Runnable() {
@Override @Override
public void run() { public void run() {
@@ -61,6 +73,14 @@ public class SettingsActivity extends AppCompatActivity implements ChooseActivit
} }
}); });
} }
if (PreferencesUtils.isKey(getActivity(), R.string.stats_rate_key, key)) {
getActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
updateSpeed();
}
});
}
} }
}; };
@@ -69,15 +89,25 @@ public class SettingsActivity extends AppCompatActivity implements ChooseActivit
@Override @Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
setPreferencesFromResource(R.xml.settings, rootKey); try {
updateUnits(); setPreferencesFromResource(R.xml.settings, rootKey);
} catch (ClassCastException e) {
// Some sharedPreference is broken: delete all and reset them; it is just a last resort...
Toast.makeText(getContext(), R.string.settings_error_initial_values_restored, Toast.LENGTH_LONG).show();
PreferencesUtils.resetPreferences(getContext(), true);
setPreferencesFromResource(R.xml.settings, rootKey);
}
} }
@Override @Override
public void onResume() { public void onResume() {
super.onResume(); super.onResume();
PreferencesUtils.getSharedPreferences(getActivity()).registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener); PreferencesUtils.getSharedPreferences(getActivity()).registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
updateUI(); updateSpeed();
updateUnits();
updateReset();
updateBluetooth();
} }
@Override @Override
@@ -86,41 +116,6 @@ public class SettingsActivity extends AppCompatActivity implements ChooseActivit
PreferencesUtils.getSharedPreferences(getActivity()).unregisterOnSharedPreferenceChangeListener(sharedPreferenceChangeListener); 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) {
//TODO Can we make values an int array?
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 @Override
public void onDisplayPreferenceDialog(Preference preference) { public void onDisplayPreferenceDialog(Preference preference) {
DialogFragment dialogFragment = null; DialogFragment dialogFragment = null;
@@ -129,6 +124,8 @@ public class SettingsActivity extends AppCompatActivity implements ChooseActivit
} else if (preference instanceof ActivityTypePreference) { } else if (preference instanceof ActivityTypePreference) {
activityPreferenceDialog = ActivityTypePreference.ActivityPreferenceDialog.newInstance(preference.getKey()); activityPreferenceDialog = ActivityTypePreference.ActivityPreferenceDialog.newInstance(preference.getKey());
dialogFragment = activityPreferenceDialog; dialogFragment = activityPreferenceDialog;
} else if (preference instanceof BluetoothLePreference) {
dialogFragment = BluetoothLePreference.BluetoothLePreferenceDialog.newInstance(preference.getKey());
} }
if (dialogFragment != null) { if (dialogFragment != null) {
@@ -146,37 +143,53 @@ public class SettingsActivity extends AppCompatActivity implements ChooseActivit
} }
} }
private void updateUI() { private void updateReset() {
final boolean isRecording = PreferencesUtils.isRecording(getActivity()); final boolean isRecording = PreferencesUtils.isRecording(getActivity());
Preference resetPreference = findPreference(getString(R.string.settings_reset_key)); Preference resetPreference = findPreference(getString(R.string.settings_reset_key));
resetPreference.setSummary(isRecording ? getString(R.string.settings_not_while_recording) : ""); resetPreference.setSummary(isRecording ? getString(R.string.settings_not_while_recording) : "");
resetPreference.setEnabled(!isRecording); resetPreference.setEnabled(!isRecording);
}
private void updateSpeed() {
Preference speedCheckBoxPreference = findPreference(getString(R.string.chart_show_speed_key)); Preference speedCheckBoxPreference = findPreference(getString(R.string.chart_show_speed_key));
speedCheckBoxPreference.setTitle(PreferencesUtils.isReportSpeed(getActivity()) ? R.string.stats_speed : R.string.stats_pace); speedCheckBoxPreference.setTitle(PreferencesUtils.isReportSpeed(getActivity()) ? R.string.stats_speed : R.string.stats_pace);
}
ListPreference bluetoothPreference = findPreference(getString(R.string.bluetooth_sensor_key)); private void updateBluetooth() {
PreferenceHelper.configureBluetoothSensorList(bluetoothPreference); // 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() { private void updateUnits() {
boolean metricUnits = PreferencesUtils.isMetricUnits(getActivity()); boolean metricUnits = PreferencesUtils.isMetricUnits(getActivity());
configFrequencyPreference(R.string.voice_frequency_key, metricUnits); ListPreference voiceFrequency = findPreference(getString(R.string.voice_frequency_key));
configFrequencyPreference(R.string.split_frequency_key, metricUnits); voiceFrequency.setEntries(StringUtils.getFrequencyOptions(getActivity(), 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);
final ListPreference statsRatePreferences = findPreference(getString(R.string.stats_rate_key)); ListPreference splitFrequency = findPreference(getString(R.string.split_frequency_key));
String[] options = getResources().getStringArray(metricUnits ? R.array.stats_rate_metric_options : R.array.stats_rate_imperial_options); splitFrequency.setEntries(StringUtils.getFrequencyOptions(getActivity(), metricUnits));
statsRatePreferences.setEntries(options);
//TODO This is a hack!!! Need to manually updated the summary as otherwise it will only be updated after scrolling down and up again (bring the object out of view). ListPreference minRecordingInterval = findPreference(getString(R.string.min_recording_interval_key));
//Check if this is still needed after upgraded PreferenceFragment. minRecordingInterval.setEntries(PreferenceHelper.getMinRecordingIntervalEntries(getActivity()));
statsRatePreferences.setSummary(statsRatePreferences.getEntry());
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 autoresumeTrack = findPreference(getString(R.string.auto_resume_track_timeout_key));
autoresumeTrack.setEntries(PreferenceHelper.getAutoResumeTrackTimeoutEntries(getActivity()));
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);
} }
} }
} }
@@ -1,56 +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.util;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothClass;
import android.bluetooth.BluetoothDevice;
import java.util.List;
import java.util.Set;
/**
* Utilities for dealing with bluetooth devices.
*
* @author Rodrigo Damazio
*/
public class BluetoothDeviceUtils {
private BluetoothDeviceUtils() {
}
/**
* Populates the device names and the device addresses with all the suitable
* bluetooth devices.
*
* @param bluetoothAdapter the bluetooth adapter
* @param deviceNames list of device names
* @param deviceAddresses list of device addresses
*/
public static void populateDeviceLists(BluetoothAdapter bluetoothAdapter, List<String> deviceNames, List<String> deviceAddresses) {
// Ensure the bluetooth adapter is not in discovery mode.
bluetoothAdapter.cancelDiscovery();
Set<BluetoothDevice> pairedDevices = bluetoothAdapter.getBondedDevices();
for (BluetoothDevice device : pairedDevices) {
BluetoothClass bluetoothClass = device.getBluetoothClass();
if (bluetoothClass != null && (bluetoothClass.getDeviceClass() & BluetoothClass.Device.HEALTH_PULSE_RATE) != 0) {
deviceAddresses.add(device.getAddress());
deviceNames.add(device.getName());
}
}
}
}
@@ -0,0 +1,88 @@
/*
* Copyright 2010 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package de.dennisguse.opentracks.util;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothGattCharacteristic;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import java.util.ArrayList;
import java.util.UUID;
/**
* Utilities for dealing with bluetooth devices.
*
* @author Rodrigo Damazio
*/
public class BluetoothUtils {
public static final UUID HEART_RATE_SERVICE_UUID = new UUID(0x180D00001000L, 0x800000805f9b34fbL);
private BluetoothUtils() {
}
public static BluetoothAdapter getDefaultBluetoothAdapter(final String TAG) {
// 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 static boolean hasBluetooth(final String TAG) {
return BluetoothUtils.getDefaultBluetoothAdapter(TAG) != null;
}
public static int parseHeartRate(BluetoothGattCharacteristic characteristic) {
//DOCUMENTATION https://www.bluetooth.com/specifications/gatt/characteristics/
byte[] raw = characteristic.getValue();
int index = ((raw[0] & 0x1) == 1) ? 2 : 1;
int format = (index == 1) ? BluetoothGattCharacteristic.FORMAT_UINT8 : BluetoothGattCharacteristic.FORMAT_UINT16;
return characteristic.getIntValue(format, index);
}
}
@@ -204,12 +204,13 @@ public class FileUtils {
* *
* @param name name * @param name name
*/ */
// TODO Check if this function is still needed.
public static String sanitizeFileName(String name) { public static String sanitizeFileName(String name) {
StringBuilder builder = new StringBuilder(name.length()); StringBuilder builder = new StringBuilder(name.length());
for (int i = 0; i < name.length(); i++) { for (int i = 0; i < name.length(); i++) {
int codePoint = name.codePointAt(i); int codePoint = name.codePointAt(i);
char character = name.charAt(i); char character = name.charAt(i);
if (Character.isLetterOrDigit(character) || codePoint > 127 || isSpecialFat32(character)) { if (Character.isLetterOrDigit(character) || codePoint > 127 || isSpecialFat32(character) || character == '.') {
builder.appendCodePoint(codePoint); builder.appendCodePoint(codePoint);
} else { } else {
builder.append("_"); builder.append("_");
@@ -0,0 +1,22 @@
package de.dennisguse.opentracks.util;
import androidx.annotation.NonNull;
import androidx.preference.Preference;
public class HackUtils {
private HackUtils() {
}
/**
* Triggers a redraw of the summary of a preference if it was set programmatically.
* Need to trigger androidx.preference.preferences.notifyChanged() to trigger a redraw, but method is protected.
* This workaround also works when this preference has not changed, but it's entries (see R.string.stats_rate_key).
* TODO
*/
public static void invalidatePreference(@NonNull Preference preference) {
boolean isEnabled = preference.isEnabled();
preference.setEnabled(!isEnabled);
preference.setEnabled(isEnabled);
}
}
@@ -29,6 +29,7 @@ import androidx.core.content.FileProvider;
import java.io.File; import java.io.File;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date; import java.util.Date;
import de.dennisguse.opentracks.R; import de.dennisguse.opentracks.R;
@@ -70,19 +71,31 @@ public class IntentUtils {
* @param trackIds the track ids * @param trackIds the track ids
*/ */
public static Intent newShareFileIntent(Context context, long[] trackIds) { public static Intent newShareFileIntent(Context context, long[] trackIds) {
if (trackIds.length == 0) {
throw new RuntimeException("Need to share at least one track.");
}
String trackDescription = ""; String trackDescription = "";
if (trackIds.length == 1) { if (trackIds.length == 1) {
Track track = ContentProviderUtils.Factory.get(context).getTrack(trackIds[0]); Track track = new ContentProviderUtils(context).getTrack(trackIds[0]);
trackDescription = track == null ? "" : new DescriptionGeneratorImpl(context).generateTrackDescription(track, false); trackDescription = track == null ? "" : new DescriptionGeneratorImpl(context).generateTrackDescription(track, false);
} }
Pair<Uri, String> uriAndMime = ShareContentProvider.createURI(trackIds, TrackFileFormat.KMZ_WITH_TRACKDETAIL_AND_SENSORDATA_AND_PICTURES);
return new Intent(Intent.ACTION_SEND) String action = trackIds.length == 1 ? Intent.ACTION_SEND : Intent.ACTION_SEND_MULTIPLE;
.putExtra(Intent.EXTRA_STREAM, uriAndMime.first) String mime = "";
ArrayList<Uri> uris = new ArrayList<>();
for (long trackId : trackIds) {
Pair<Uri, String> uriAndMime = ShareContentProvider.createURI(new long[]{trackId}, TrackFileFormat.KMZ_WITH_TRACKDETAIL_AND_SENSORDATA_AND_PICTURES);
uris.add(uriAndMime.first);
mime = uriAndMime.second;
}
return new Intent(action)
.setType(mime)
.setAction(Intent.ACTION_SEND_MULTIPLE)
.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uris)
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
.putExtra(Intent.EXTRA_SUBJECT, context.getString(R.string.share_track_subject)) .putExtra(Intent.EXTRA_SUBJECT, context.getString(R.string.share_track_subject))
.putExtra(Intent.EXTRA_TEXT, context.getString(R.string.share_track_share_file_body, trackDescription)) .putExtra(Intent.EXTRA_TEXT, context.getString(R.string.share_track_share_file_body, trackDescription));
.setType(uriAndMime.second)
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
} }
public static void showCoordinateOnMap(Context context, Waypoint waypoint) { public static void showCoordinateOnMap(Context context, Waypoint waypoint) {
@@ -29,7 +29,6 @@ import de.dennisguse.opentracks.content.Track;
* *
* @author Leif Hendrik Wilden * @author Leif Hendrik Wilden
*/ */
//TODO Cleanup: rename variables
public class LocationUtils { public class LocationUtils {
private static final String TAG = LocationUtils.class.getSimpleName(); private static final String TAG = LocationUtils.class.getSimpleName();
@@ -158,7 +157,7 @@ public class LocationUtils {
} }
/** /**
* Checks if a given location is a valid (i.e. physically possible) locationon Earth. * Checks if a given location is a valid (i.e. physically possible) location on Earth.
* Note: The special separator locations (which have latitude = 100) will not qualify as valid. * Note: The special separator locations (which have latitude = 100) will not qualify as valid.
* Neither will locations with lat=0 and lng=0 as these are most likely "bad" measurements which often cause trouble. * Neither will locations with lat=0 and lng=0 as these are most likely "bad" measurements which often cause trouble.
* *
@@ -20,6 +20,7 @@ import android.content.Context;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor; import android.content.SharedPreferences.Editor;
import androidx.annotation.VisibleForTesting;
import androidx.preference.PreferenceManager; import androidx.preference.PreferenceManager;
import de.dennisguse.opentracks.R; import de.dennisguse.opentracks.R;
@@ -31,67 +32,8 @@ import de.dennisguse.opentracks.R;
*/ */
public class PreferencesUtils { public class PreferencesUtils {
/*
* Preferences values.
* The defaults need to match the defaults in the xml files.
*/
public static final int AUTO_RESUME_TRACK_CURRENT_RETRY_DEFAULT = 0;
// Values for auto_resume_track_timeout_key
public static final int AUTO_RESUME_TRACK_TIMEOUT_ALWAYS = -1;
public static final int AUTO_RESUME_TRACK_TIMEOUT_DEFAULT = 10;
public static final int AUTO_RESUME_TRACK_TIMEOUT_NEVER = 0;
public static final String BLUETOOTH_SENSOR_DEFAULT = ""; public static final String BLUETOOTH_SENSOR_DEFAULT = "";
public static final boolean CHART_SHOW_CADENCE_DEFAULT = true;
public static final boolean CHART_SHOW_ELEVATION_DEFAULT = true;
public static final boolean CHART_SHOW_HEART_RATE_DEFAULT = true;
public static final boolean CHART_SHOW_POWER_DEFAULT = true;
public static final boolean CHART_SHOW_SPEED_DEFAULT = true;
@Deprecated
//NOTE: is at the moment still used to determine if a track is currently recorded; better ask the service directly.
//NOTE: This is also used to recover from a reboot, but this data should not be exposed to the whole application.
public static final long RECORDING_TRACK_ID_DEFAULT = -1L;
public static final String DEFAULT_ACTIVITY_DEFAULT = "";
// Value for split_frequency_key and voice_frequency_key
public static final int FREQUENCY_OFF = 0;
public static final int MAX_RECORDING_DISTANCE_DEFAULT = 200;
// Values for min_recording_interval_key
public static final int MIN_RECORDING_INTERVAL_ADAPT_ACCURACY = -1;
public static final int MIN_RECORDING_INTERVAL_ADAPT_BATTERY_LIFE = -2;
public static final int MIN_RECORDING_INTERVAL_DEFAULT = 0;
public static final int RECORDING_DISTANCE_INTERVAL_DEFAULT = 10;
// Values for recording_gps_accuracy
public static final int RECORDING_GPS_ACCURACY_DEFAULT = 50;
public static final int RECORDING_GPS_ACCURACY_EXCELLENT = 10;
public static final int RECORDING_GPS_ACCURACY_POOR = 2000;
static final boolean STATS_SHOW_COORDINATE_DEFAULT = false;
public static final boolean RECORDING_TRACK_PAUSED_DEFAULT = true;
public static final int SPLIT_FREQUENCY_DEFAULT = 0;
static final boolean STATS_SHOW_ELEVATION_DEFAULT = false;
static final String TRACK_NAME_DEFAULT = "DATE_ISO_8601";
private static final String CHART_X_AXIS_DEFAULT = "DISTANCE";
public static final String STATS_UNITS_DEFAULT = "METRIC";
// Stats
private static final String STATS_RATE_DEFAULT = "SPEED";
public static final boolean SHOW_TRACKDETAIL_WHILE_RECORDING_ON_LOCKSCREEN = false;
// Track widget
public static final int TRACK_WIDGET_ITEM1_DEFAULT = 3; // moving time
public static final int TRACK_WIDGET_ITEM2_DEFAULT = 0; // distance
public static final int TRACK_WIDGET_ITEM3_DEFAULT = 1; // total time
public static final int TRACK_WIDGET_ITEM4_DEFAULT = 2; // average speed
public static final int VOICE_FREQUENCY_DEFAULT = 0;
private PreferencesUtils() { private PreferencesUtils() {
} }
@@ -99,16 +41,60 @@ public class PreferencesUtils {
return PreferenceManager.getDefaultSharedPreferences(context); return PreferenceManager.getDefaultSharedPreferences(context);
} }
@Deprecated
//NOTE: is at the moment still used to determine if a track is currently recorded; better ask the service directly.
//NOTE: This is also used to recover from a reboot, but this data should not be exposed to the whole application.
public static final long RECORDING_TRACK_ID_DEFAULT = -1L;
public static long getRecordingTrackId(Context context) {
return PreferencesUtils.getLong(context, R.string.recording_track_id_key, RECORDING_TRACK_ID_DEFAULT);
}
@VisibleForTesting
public static final int AUTO_RESUME_TRACK_CURRENT_RETRY_DEFAULT = 0;
public static int getAutoResumeTrackCurrentRetryDefault(Context context) {
return PreferencesUtils.getInt(context, R.string.auto_resume_track_current_retry_key, PreferencesUtils.AUTO_RESUME_TRACK_CURRENT_RETRY_DEFAULT);
}
public static void resetAutoResumeTrackCurrentRetryDefault(Context context) {
PreferencesUtils.setInt(context, R.string.auto_resume_track_current_retry_key, PreferencesUtils.AUTO_RESUME_TRACK_CURRENT_RETRY_DEFAULT);
}
public static void incrementAutoResumeTrackCurrentRetryDefault(Context context) {
PreferencesUtils.setInt(context, R.string.auto_resume_track_current_retry_key, getAutoResumeTrackCurrentRetryDefault(context) + 1);
}
public static String getDefaultActivity(Context context) {
return PreferencesUtils.getString(context, R.string.default_activity_key, context.getString(R.string.default_activity_default));
}
public static void setDefaultActivity(Context context, String newDefaultActivity) {
PreferencesUtils.setString(context, R.string.default_activity_key, newDefaultActivity);
}
/** /**
* Gets a preference key * Gets a preference key
* *
* @param context the context * @param context the context
* @param keyId the key id * @param keyId the key id
*/ */
public static String getKey(Context context, int keyId) { private static String getKey(Context context, int keyId) {
return context.getString(keyId); return context.getString(keyId);
} }
/**
* Compares if keyId and key belong to the same shared preference key.
*
* @param keyId The resource id of the key
* @param key
* @return true if key == null or key belongs to keyId
*/
public static boolean isKey(Context context, int keyId, String key) {
return key == null || key.equals(PreferencesUtils.getKey(context, keyId));
}
/** /**
* Gets a boolean preference value. * Gets a boolean preference value.
* *
@@ -180,9 +166,9 @@ public class PreferencesUtils {
* @param context the context * @param context the context
* @param keyId the key id * @param keyId the key id
*/ */
public static long getLong(Context context, int keyId) { private static long getLong(Context context, int keyId, long defaultValue) {
SharedPreferences sharedPreferences = getSharedPreferences(context); SharedPreferences sharedPreferences = getSharedPreferences(context);
return sharedPreferences.getLong(getKey(context, keyId), -1L); return sharedPreferences.getLong(getKey(context, keyId), defaultValue);
} }
/** /**
@@ -231,7 +217,8 @@ public class PreferencesUtils {
* @param context the context * @param context the context
*/ */
public static boolean isMetricUnits(Context context) { public static boolean isMetricUnits(Context context) {
return STATS_UNITS_DEFAULT.equals(getString(context, R.string.stats_units_key, STATS_UNITS_DEFAULT)); final String STATS_UNIT = context.getString(R.string.stats_units_default);
return STATS_UNIT.equals(getString(context, R.string.stats_units_key, STATS_UNIT));
} }
/** /**
@@ -241,20 +228,129 @@ public class PreferencesUtils {
* @param context the context * @param context the context
*/ */
public static boolean isReportSpeed(Context context) { public static boolean isReportSpeed(Context context) {
final String STATS_RATE_DEFAULT = context.getString(R.string.stats_rate_default);
return STATS_RATE_DEFAULT.equals(getString(context, R.string.stats_rate_key, STATS_RATE_DEFAULT)); return STATS_RATE_DEFAULT.equals(getString(context, R.string.stats_rate_key, STATS_RATE_DEFAULT));
} }
/** public static int getAutoResumeTrackTimeout(Context context) {
* Returns true if chart x axis is by distance, false if by time. final int AUTO_RESUME_TRACK_TIMEOUT = Integer.parseInt(context.getResources().getString(R.string.auto_resume_track_timeout_default));
* return PreferencesUtils.getInt(context, R.string.auto_resume_track_timeout_key, AUTO_RESUME_TRACK_TIMEOUT);
* @param context the context }
*/
public static boolean isChartByDistance(Context context) {
return CHART_X_AXIS_DEFAULT.equals(getString(context, R.string.chart_x_axis_key, CHART_X_AXIS_DEFAULT)); public static boolean isRecordingTrackPaused(Context context) {
return PreferencesUtils.getBoolean(context, R.string.recording_track_paused_key, isRecordingTrackPausedDefault(context));
}
public static boolean isRecordingTrackPausedDefault(Context context) {
return context.getResources().getBoolean(R.bool.recording_track_paused_default);
}
public static void defaultRecordingTrackPaused(Context context) {
final boolean RECORDING_TRACK_PAUSED = context.getResources().getBoolean(R.bool.recording_track_paused_default);
PreferencesUtils.setBoolean(context, R.string.recording_track_paused_key, RECORDING_TRACK_PAUSED);
}
public static boolean isBluetoothHeartRateSensorAddressDefault(Context context) {
return PreferencesUtils.BLUETOOTH_SENSOR_DEFAULT.equals(getBluetoothHeartRateSensorAddress(context));
}
public static String getBluetoothHeartRateSensorAddress(Context context) {
return PreferencesUtils.getString(context, R.string.settings_sensor_bluetooth_heart_rate_key, PreferencesUtils.BLUETOOTH_SENSOR_DEFAULT);
}
public static boolean shouldChartShowCadence(Context context) {
final boolean CHART_SHOW_CADENCE = context.getResources().getBoolean(R.bool.chart_show_cadence_default);
return PreferencesUtils.getBoolean(context, R.string.chart_show_cadence_key, CHART_SHOW_CADENCE);
}
public static boolean shouldChartShowElevation(Context context) {
final boolean CHART_SHOW_ELEVATION = context.getResources().getBoolean(R.bool.chart_show_elevation_default);
return PreferencesUtils.getBoolean(context, R.string.chart_show_elevation_key, CHART_SHOW_ELEVATION);
}
public static boolean shouldChartShowHeartRate(Context context) {
final boolean CHART_SHOW_HEARTRATE = context.getResources().getBoolean(R.bool.chart_show_heart_rate_default);
return PreferencesUtils.getBoolean(context, R.string.chart_show_heart_rate_key, CHART_SHOW_HEARTRATE);
}
public static boolean shouldChartShowPower(Context context) {
final boolean CHART_SHOW_POWER = context.getResources().getBoolean(R.bool.chart_show_power_default);
return PreferencesUtils.getBoolean(context, R.string.chart_show_power_key, CHART_SHOW_POWER);
}
public static boolean shouldChartShowSpeed(Context context) {
final boolean CHART_SHOW_SPEED = context.getResources().getBoolean(R.bool.chart_show_speed_default);
return PreferencesUtils.getBoolean(context, R.string.chart_show_speed_key, CHART_SHOW_SPEED);
}
public static boolean shouldShowStatsOnLockscreen(Context context) {
final boolean STATS_SHOW_ON_LOCKSCREEN_DEFAULT = context.getResources().getBoolean(R.bool.stats_show_on_lockscreen_while_recording_default);
return getBoolean(context, R.string.stats_show_on_lockscreen_while_recording_key, STATS_SHOW_ON_LOCKSCREEN_DEFAULT);
}
public static boolean isShowStatsGradeElevation(Context context) {
final boolean STATS_SHOW_GRADE_ELEVATION = context.getResources().getBoolean(R.bool.stats_show_grade_elevation_default);
return PreferencesUtils.getBoolean(context, R.string.stats_show_grade_elevation_key, STATS_SHOW_GRADE_ELEVATION);
}
public static boolean isStatsShowCoordinate(Context context) {
final boolean STATS_SHOW_COORDINATE = context.getResources().getBoolean(R.bool.stats_show_coordinate_default);
return PreferencesUtils.getBoolean(context, R.string.stats_show_coordinate_key, STATS_SHOW_COORDINATE);
}
public static int getVoiceFrequency(Context context) {
final int VOICE_FREQUENCY_DEFAULT = Integer.parseInt(context.getResources().getString(R.string.voice_frequency_default));
return PreferencesUtils.getInt(context, R.string.voice_frequency_key, VOICE_FREQUENCY_DEFAULT);
}
public static int getSplitFrequency(Context context) {
final int SPLIT_FREQUENCY_DEFAULT = Integer.parseInt(context.getResources().getString(R.string.split_frequency_default));
return PreferencesUtils.getInt(context, R.string.split_frequency_key, SPLIT_FREQUENCY_DEFAULT);
}
public static int getRecordingDistanceInterval(Context context) {
final int RECORDING_DISTANCE_INTERVAL = Integer.parseInt(context.getResources().getString(R.string.recording_distance_interval_default));
return PreferencesUtils.getInt(context, R.string.recording_distance_interval_key, getRecordingDistanceIntervalDefault(context));
}
public static int getRecordingDistanceIntervalDefault(Context context) {
return Integer.parseInt(context.getResources().getString(R.string.recording_distance_interval_default));
}
public static int getMaxRecordingDistance(Context context) {
final int MAX_RECORDING_DISTANCE = Integer.parseInt(context.getResources().getString(R.string.max_recording_distance_default));
return PreferencesUtils.getInt(context, R.string.max_recording_distance_key, MAX_RECORDING_DISTANCE);
}
public static int getMinRecordingInterval(Context context) {
final int MIN_RECORDING_INTERVAL = Integer.parseInt(context.getResources().getString(R.string.min_recording_interval_default));
return PreferencesUtils.getInt(context, R.string.min_recording_interval_key, MIN_RECORDING_INTERVAL);
}
public static int getMinRecordingIntervalAdaptAccuracy(Context context) {
return Integer.parseInt(context.getResources().getString(R.string.min_recording_interval_adapt_accuracy));
}
public static int getMinRecordingIntervalAdaptBatteryLife(Context context) {
return Integer.parseInt(context.getResources().getString(R.string.min_recording_interval_adapt_battery_life));
}
public static int getMinRecordingIntervalDefault(Context context) {
return Integer.parseInt(context.getResources().getString(R.string.min_recording_interval_default));
}
public static int getRecordingGPSAccuracy(Context context) {
final int RECORDING_GPS_ACCURACY = Integer.parseInt(context.getResources().getString(R.string.recording_gps_accuracy_default));
return PreferencesUtils.getInt(context, R.string.recording_gps_accuracy_key, RECORDING_GPS_ACCURACY);
} }
public static boolean isRecording(Context context) { public static boolean isRecording(Context context) {
long recordingTrackId = PreferencesUtils.getLong(context, R.string.recording_track_id_key); long recordingTrackId = PreferencesUtils.getRecordingTrackId(context);
return recordingTrackId != PreferencesUtils.RECORDING_TRACK_ID_DEFAULT; return recordingTrackId != PreferencesUtils.RECORDING_TRACK_ID_DEFAULT;
} }
@@ -263,6 +359,10 @@ public class PreferencesUtils {
} }
public static void resetPreferences(Context context, boolean readAgain) { public static void resetPreferences(Context context, boolean readAgain) {
if (readAgain) {
// We want to really clear settings now.
getSharedPreferences(context).edit().clear().commit();
}
PreferenceManager.setDefaultValues(context, R.xml.settings, readAgain); PreferenceManager.setDefaultValues(context, R.xml.settings, readAgain);
} }
} }

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