Compare commits
82 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 25b6aec0e9 | |||
| 3e40209d15 | |||
| 54ff94f905 | |||
| 7a4faf6b0d | |||
| 8922b00a5f | |||
| 702efa8693 | |||
| e7a8c897a3 | |||
| c59d1d87bd | |||
| a706efb8b3 | |||
| e391e83a00 | |||
| 2940bf20d8 | |||
| 081db5a58b | |||
| b34805272f | |||
| 98443dba99 | |||
| dd6e733d8c | |||
| 92d3439c3b | |||
| bd155cd600 | |||
| e14e53c0ae | |||
| 77a6a63174 | |||
| 40f2463f09 | |||
| e5d1048c8d | |||
| bf3b529fb2 | |||
| d0fde46027 | |||
| 0f348dff10 | |||
| d3f5080e73 | |||
| a453c8ec1e | |||
| 2a60aa8f59 | |||
| 41e7870509 | |||
| 57f66b77ab | |||
| 53572406e9 | |||
| a288255ab1 | |||
| 353c958ae0 | |||
| bc5ab0d7e5 | |||
| 9d915b5640 | |||
| 3aaee270eb | |||
| d53a7156e2 | |||
| 3af24ea3d0 | |||
| 5483212b06 | |||
| 419279fe5e | |||
| 501e899b27 | |||
| 3bb9edb009 | |||
| e52b1188a3 | |||
| 9e877e4762 | |||
| 5711757110 | |||
| e1c33dad43 | |||
| 4b2400774c | |||
| afd4a59622 | |||
| bdce437470 | |||
| 33479cda11 | |||
| 385cee3dc7 | |||
| d0bbd7655b | |||
| a83b8f62fe | |||
| 0cd9176703 | |||
| d8e526923d | |||
| 3a6f417662 | |||
| 7d807a7f11 | |||
| e9d5718495 | |||
| 0dd7f74a11 | |||
| 9bd4ba489c | |||
| dd55b50dad | |||
| 1719d21a5b | |||
| 75ef8d99d4 | |||
| ae06fd1680 | |||
| 2b03f7dea8 | |||
| 7591b012b4 | |||
| 0e089f49f3 | |||
| 5ad1856b13 | |||
| 965dbbe829 | |||
| 262c9886df | |||
| ac5a9ea65d | |||
| ed9b4c8f3d | |||
| 209aa6a8a5 | |||
| 81ee6ac1fa | |||
| 024dd603cb | |||
| 50654c6baf | |||
| 8c9bf60fc6 | |||
| a83eb0b983 | |||
| ab3cbfea0f | |||
| c93e5ca1a6 | |||
| c2841092da | |||
| 8b11e1d611 | |||
| 6375a851b2 |
@@ -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]
|
||||
@@ -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.
|
||||
@@ -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
|
||||
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:
|
||||
components:
|
||||
# installing tools to start, then use `sdkmanager` below to get the rest
|
||||
- tools
|
||||
- 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:
|
||||
- ./gradlew build
|
||||
#TODO: When all tests are passing, CI should also execute them.
|
||||
#- ./gradlew connectedCheck
|
||||
- ./gradlew clean build
|
||||
- ./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,6 +7,10 @@
|
||||
3. Manually update _version code_ and _version name_ in `AndroidManifest.xml` (used by FDroid)
|
||||
4. Create changelog (`_version code_.txt`)
|
||||
5. Create commit with all changes
|
||||
6. Push commits
|
||||
7. Tag newly create commit with _version name_
|
||||
8. Push the tagged to public repository
|
||||
6. Tag newly create commit with _version name_ (e.g., v3.2.1)
|
||||
7. Push commits and tags to public repository
|
||||
8. Add changelog to tag (Github releases)
|
||||
|
||||
## Translations (localization)
|
||||
|
||||
Translations of the OpenTracks are handled in Transifex: [https://www.transifex.com/opentracks](https://www.transifex.com/opentracks/).
|
||||
@@ -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">
|
||||
</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
|
||||
|
||||
_OpenTracks_ is based upon Google _My Tracks app_ ([code](https://code.google.com/archive/p/mytracks/)).
|
||||
|
||||
@@ -6,7 +6,7 @@ buildscript {
|
||||
jcenter()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:3.5.0'
|
||||
classpath 'com.android.tools.build:gradle:3.5.2'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,10 +35,14 @@ def getVersionName = { ->
|
||||
try {
|
||||
def stdout = new ByteArrayOutputStream()
|
||||
exec {
|
||||
commandLine 'git', 'describe', '--tags', '--dirty'
|
||||
commandLine 'git', 'describe', '--tags'
|
||||
standardOutput = stdout
|
||||
}
|
||||
return stdout.toString().trim()
|
||||
exec {
|
||||
commandLine 'git', 'log', '--format="%h"', '-n 1'
|
||||
standardOutput = stdout
|
||||
}
|
||||
return stdout.toString().replace('\n', '-').replace('"', '').trim()
|
||||
}
|
||||
catch (ignored) {
|
||||
return null
|
||||
@@ -75,14 +79,11 @@ android {
|
||||
applicationVariants.all { variant ->
|
||||
variant.resValue "string", "applicationId", variant.applicationId
|
||||
}
|
||||
|
||||
lintOptions {
|
||||
disable "AppCompatResource" // TODO Disable complaint to use androidx's SearchView as it is null.
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'androidx.appcompat:appcompat:1.1.0'
|
||||
implementation 'androidx.preference:preference:1.1.0'
|
||||
|
||||
androidTestImplementation 'androidx.test:core:1.2.0'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
Open Tracks – ein GPS-Tracker, der deine Daten für sich behält.
|
||||
Erfasst deine Kilometer, ohne dass es jemand anderes tut.
|
||||
|
||||
Funktionen & Features:
|
||||
* zeichnet Sport und andere Outdooraktivitäten auf
|
||||
* unterstützt das Markieren interessanter Orte, inkl. Foto-Funktion
|
||||
* zeichnet deine Herzfrequenz auf (unterstützt Bluetooth-LE-Sensoren)
|
||||
* exportiert deine Daten als KML oder GPX
|
||||
* funktioniert ganz ohne Internet
|
||||
@@ -0,0 +1 @@
|
||||
Open Tracks – ein GPS-Tracker, der deine Daten für sich behält.
|
||||
@@ -1,4 +1,4 @@
|
||||
v3.2: OpenTracks
|
||||
v3.2.0: OpenTracks
|
||||
|
||||
Changes:
|
||||
- Removed Widget
|
||||
@@ -0,0 +1,7 @@
|
||||
v3.2.1: OpenTracks
|
||||
|
||||
Changes:
|
||||
- Settings are now on a single page
|
||||
|
||||
Developer:
|
||||
- Settings moved to Androidx compat (uses fragments)
|
||||
@@ -0,0 +1,13 @@
|
||||
v3.2.2: OpenTracks
|
||||
|
||||
Changes:
|
||||
- Removed aggregated stats
|
||||
- Migrated to Material Design
|
||||
|
||||
Bugfix:
|
||||
- Fixed export of sensor data
|
||||
- Fixed import of incomplete sensor data
|
||||
|
||||
Developer:
|
||||
- Replaced actionbar with toolbar
|
||||
- Migrated to androidx appcompat (incl. Material Design)
|
||||
@@ -0,0 +1,14 @@
|
||||
v3.2.3: OpenTracks
|
||||
|
||||
Changes:
|
||||
- Sharing: use Android Sharesheet
|
||||
- Sharing: share multiple tracks as individual files
|
||||
- Use Android ShareSheet
|
||||
- Heart rate sensor (Bluetooth LE): automatic reconnect
|
||||
|
||||
Bugfix:
|
||||
- Heart rate sensor (Bluetooth LE): disconnect properly
|
||||
- Heart rate wasn't always shown while recording
|
||||
|
||||
Developer:
|
||||
- Enabled TravisCI instrumentation tests
|
||||
@@ -0,0 +1,5 @@
|
||||
v3.2.4: OpenTracks
|
||||
|
||||
Bugfix:
|
||||
- Settings activity crashes
|
||||
Removed possibility to change voice/split frequency from the context menu
|
||||
@@ -0,0 +1,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
|
||||
@@ -3,6 +3,6 @@ OpenTracks is a sport tracking application that completely respects your privacy
|
||||
Features:
|
||||
* Sport tracking: track your sport and outdoor activities
|
||||
* Photos and markers: mark interesting locations while tracking
|
||||
* Heart rate: supports bluetooth LE sensors
|
||||
* Export data: Export tracks either as KML or GPX
|
||||
* Heart rate: supports Bluetooth LE sensors
|
||||
* Export data: export tracks either as KML or GPX
|
||||
* No Internet access: Internet is not used
|
||||
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 129 KiB After Width: | Height: | Size: 129 KiB |
|
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 76 KiB |
|
Before Width: | Height: | Size: 85 KiB After Width: | Height: | Size: 85 KiB |
@@ -0,0 +1 @@
|
||||
OpenTracks
|
||||
@@ -40,7 +40,6 @@ import de.dennisguse.opentracks.content.ContentProviderUtils.LocationIterator;
|
||||
import de.dennisguse.opentracks.content.Waypoint.WaypointType;
|
||||
import de.dennisguse.opentracks.stats.TripStatistics;
|
||||
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
@@ -214,7 +213,6 @@ public class CustomContentProviderUtilsImplTest {
|
||||
Track track = providerUtils.createTrack(cursorMock);
|
||||
Assert.assertEquals(trackId, track.getId());
|
||||
Assert.assertEquals(name, track.getName());
|
||||
verify(cursorMock);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -414,7 +412,6 @@ public class CustomContentProviderUtilsImplTest {
|
||||
Assert.assertEquals(id, waypoint.getId());
|
||||
Assert.assertEquals(name, waypoint.getName());
|
||||
Assert.assertEquals(trackId, waypoint.getTrackId());
|
||||
verify(cursorMock);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -709,7 +706,6 @@ public class CustomContentProviderUtilsImplTest {
|
||||
Assert.assertEquals((double) latitude, location.getLatitude(), 0.01);
|
||||
Assert.assertEquals(time, location.getTime(), 0.01);
|
||||
Assert.assertEquals(speed, location.getSpeed(), 0.01);
|
||||
verify(cursorMock);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -112,7 +112,7 @@ public abstract class AbstractTestFileTrackImporter {
|
||||
* @param lastTrack true if it is the last track in the gpx
|
||||
* @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());
|
||||
|
||||
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.InputStream;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.content.Track;
|
||||
import de.dennisguse.opentracks.util.PreferencesUtils;
|
||||
|
||||
import static org.mockito.Mockito.any;
|
||||
import static org.mockito.Mockito.anyInt;
|
||||
import static org.mockito.Mockito.anyLong;
|
||||
import static org.mockito.Mockito.atLeastOnce;
|
||||
import static org.mockito.Mockito.eq;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
@@ -84,8 +84,7 @@ public class GpxFileTrackImporterTest extends AbstractTestFileTrackImporter {
|
||||
|
||||
@Test
|
||||
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 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
|
||||
when(contentProviderUtils.bulkInsertTrackPoint((Location[]) any(), eq(1), eq(TRACK_ID_0))).thenReturn(1);
|
||||
when(contentProviderUtils.getLastTrackPointId(TRACK_ID_0)).thenReturn(TRACK_POINT_ID_1);
|
||||
when(contentProviderUtils.getTrack(PreferencesUtils.getLong(context, R.string.recording_track_id_key))).thenReturn(null);
|
||||
expectUpdateTrack(trackCaptor, true, TRACK_ID_0);
|
||||
when(contentProviderUtils.getTrack(PreferencesUtils.getRecordingTrackId(context))).thenReturn(null);
|
||||
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());
|
||||
GpxFileTrackImporter gpxFileTrackImporter = new GpxFileTrackImporter(context, contentProviderUtils);
|
||||
long trackId = gpxFileTrackImporter.importFile(inputStream);
|
||||
|
||||
// then
|
||||
Assert.assertEquals(TRACK_ID_0, trackId);
|
||||
|
||||
verify(contentProviderUtils, atLeastOnce()).updateTrack(trackCaptor.capture());
|
||||
long time0 = DATE_FORMAT_0.parse(TRACK_TIME_0).getTime();
|
||||
long time1 = DATE_FORMAT_1.parse(TRACK_TIME_1).getTime();
|
||||
Assert.assertEquals(time1 - time0, trackCaptor.getValue().getTripStatistics().getTotalTime());
|
||||
verify(contentProviderUtils);
|
||||
verifyTrack(trackCaptor.getValue(), TRACK_NAME_0, TRACK_DESCRIPTION_0, time0);
|
||||
}
|
||||
|
||||
@Test
|
||||
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());
|
||||
|
||||
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
|
||||
when(contentProviderUtils.bulkInsertTrackPoint((Location[]) any(), eq(5), eq(TRACK_ID_0))).thenReturn(5);
|
||||
when(contentProviderUtils.getLastTrackPointId(TRACK_ID_0)).thenReturn(TRACK_POINT_ID_3);
|
||||
when(contentProviderUtils.getTrack(PreferencesUtils.getLong(context, R.string.recording_track_id_key))).thenReturn(null);
|
||||
expectUpdateTrack(trackCaptor, true, TRACK_ID_0);
|
||||
when(contentProviderUtils.getTrack(PreferencesUtils.getRecordingTrackId(context))).thenReturn(null);
|
||||
|
||||
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());
|
||||
GpxFileTrackImporter gpxFileTrackImporter = new GpxFileTrackImporter(context, contentProviderUtils);
|
||||
long trackId = gpxFileTrackImporter.importFile(inputStream);
|
||||
|
||||
// then
|
||||
verify(contentProviderUtils, atLeastOnce()).updateTrack(trackCaptor.capture());
|
||||
Assert.assertEquals(TRACK_ID_0, trackId);
|
||||
|
||||
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();
|
||||
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());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOneTrackTwoSegmentsNoTime() {
|
||||
ArgumentCaptor<Track> trackCaptor = ArgumentCaptor.forClass(Track.class);
|
||||
|
||||
// given
|
||||
when(contentProviderUtils.insertTrack((Track) any())).thenReturn(TRACK_ID_0_URI);
|
||||
expectFirstTrackPoint(null, TRACK_ID_0, TRACK_POINT_ID_0);
|
||||
|
||||
// A flush happens at the end
|
||||
when(contentProviderUtils.bulkInsertTrackPoint((Location[]) any(), eq(5), eq(TRACK_ID_0))).thenReturn(5);
|
||||
when(contentProviderUtils.getLastTrackPointId(TRACK_ID_0)).thenReturn(TRACK_POINT_ID_3);
|
||||
when(contentProviderUtils.getTrack(PreferencesUtils.getLong(context, R.string.recording_track_id_key))).thenReturn(null);
|
||||
expectUpdateTrack(trackCaptor, true, TRACK_ID_0);
|
||||
when(contentProviderUtils.getTrack(PreferencesUtils.getRecordingTrackId(context))).thenReturn(null);
|
||||
|
||||
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());
|
||||
GpxFileTrackImporter gpxFileTrackImporter = new GpxFileTrackImporter(context, contentProviderUtils);
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -199,7 +210,7 @@ public class GpxFileTrackImporterTest extends AbstractTestFileTrackImporter {
|
||||
when(contentProviderUtils.bulkInsertTrackPoint((Location[]) any(), anyInt(), anyLong())).thenReturn(1);
|
||||
when(contentProviderUtils.getFirstTrackPointId(TRACK_ID_0)).thenReturn(TRACK_POINT_ID_0);
|
||||
when(contentProviderUtils.getLastTrackPointId(TRACK_ID_0)).thenReturn(TRACK_POINT_ID_0);
|
||||
when(contentProviderUtils.getTrack(PreferencesUtils.getLong(context, R.string.recording_track_id_key))).thenReturn(null);
|
||||
when(contentProviderUtils.getTrack(PreferencesUtils.getRecordingTrackId(context))).thenReturn(null);
|
||||
contentProviderUtils.deleteTrack(context, TRACK_ID_0);
|
||||
|
||||
InputStream inputStream = new ByteArrayInputStream(xml.getBytes());
|
||||
|
||||
@@ -27,11 +27,11 @@ import org.mockito.junit.MockitoJUnitRunner;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.InputStream;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.content.Track;
|
||||
import de.dennisguse.opentracks.util.PreferencesUtils;
|
||||
|
||||
import static org.mockito.Mockito.any;
|
||||
import static org.mockito.Mockito.atLeastOnce;
|
||||
import static org.mockito.Mockito.eq;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
@@ -68,8 +68,7 @@ public class KmlFileTrackImporterTest extends AbstractTestFileTrackImporter {
|
||||
|
||||
@Test
|
||||
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 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);
|
||||
|
||||
// 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.getTrack(PreferencesUtils.getLong(context, R.string.recording_track_id_key))).thenReturn(null);
|
||||
expectUpdateTrack(trackCaptor, true, TRACK_ID_0);
|
||||
when(contentProviderUtils.getTrack(PreferencesUtils.getRecordingTrackId(context))).thenReturn(null);
|
||||
|
||||
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());
|
||||
KmlFileTrackImporter kmlFileTrackImporter = new KmlFileTrackImporter(context, TRACK_ID_0, contentProviderUtils);
|
||||
long trackId = kmlFileTrackImporter.importFile(inputStream);
|
||||
|
||||
// then
|
||||
verify(contentProviderUtils, atLeastOnce()).updateTrack(trackCaptor.capture());
|
||||
Assert.assertEquals(TRACK_ID_0, trackId);
|
||||
|
||||
long time0 = DATE_FORMAT_0.parse(TRACK_TIME_0).getTime();
|
||||
long time1 = DATE_FORMAT_1.parse(TRACK_TIME_1).getTime();
|
||||
Assert.assertEquals(time1 - time0, trackCaptor.getValue().getTripStatistics().getTotalTime());
|
||||
verify(contentProviderUtils);
|
||||
verifyTrack(trackCaptor.getValue(), TRACK_NAME_0, TRACK_DESCRIPTION_0, time0);
|
||||
}
|
||||
|
||||
@Test
|
||||
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());
|
||||
|
||||
contentProviderUtils.clearTrack(context, TRACK_ID_0);
|
||||
@@ -106,12 +109,18 @@ public class KmlFileTrackImporterTest extends AbstractTestFileTrackImporter {
|
||||
// A flush happens at the end
|
||||
when(contentProviderUtils.bulkInsertTrackPoint((Location[]) any(), eq(5), eq(TRACK_ID_0))).thenReturn(5);
|
||||
when(contentProviderUtils.getLastTrackPointId(TRACK_ID_0)).thenReturn(TRACK_POINT_ID_3);
|
||||
when(contentProviderUtils.getTrack(PreferencesUtils.getLong(context, R.string.recording_track_id_key))).thenReturn(null);
|
||||
expectUpdateTrack(trackCaptor, true, TRACK_ID_0);
|
||||
when(contentProviderUtils.getTrack(PreferencesUtils.getRecordingTrackId(context))).thenReturn(null);
|
||||
|
||||
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());
|
||||
KmlFileTrackImporter kmlFileTrackImporter = new KmlFileTrackImporter(context, TRACK_ID_0, contentProviderUtils);
|
||||
long trackId = kmlFileTrackImporter.importFile(inputStream);
|
||||
|
||||
// then
|
||||
verify(contentProviderUtils, atLeastOnce()).updateTrack(trackCaptor.capture());
|
||||
Assert.assertEquals(TRACK_ID_0, trackId);
|
||||
|
||||
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();
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,14 +15,12 @@
|
||||
*/
|
||||
package de.dennisguse.opentracks.services;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.ContentProvider;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.location.Location;
|
||||
import android.os.IBinder;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
@@ -38,7 +36,6 @@ import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
@@ -77,14 +74,12 @@ public class TrackRecordingServiceTest {
|
||||
private long trackId = Math.abs(new Random().nextLong());
|
||||
|
||||
static Intent createStartIntent(Context context) {
|
||||
Intent startIntent = new Intent();
|
||||
startIntent.setClass(context, TrackRecordingService.class);
|
||||
return startIntent;
|
||||
return new Intent(context, TrackRecordingService.class);
|
||||
}
|
||||
|
||||
static void updateAutoResumePrefs(Context context, int attempts, int timeoutMins) {
|
||||
PreferencesUtils.setInt(context, R.string.auto_resume_track_current_retry_key, attempts);
|
||||
PreferencesUtils.setInt(context, R.string.auto_resume_track_timeout_key, timeoutMins);
|
||||
PreferencesUtils.setString(context, R.string.auto_resume_track_timeout_key, "" + timeoutMins);
|
||||
}
|
||||
|
||||
@Before
|
||||
@@ -140,12 +135,15 @@ public class TrackRecordingServiceTest {
|
||||
createDummyTrack(trackId, System.currentTimeMillis(), true);
|
||||
|
||||
// Clear the number of attempts and set the timeout to 10 min.
|
||||
updateAutoResumePrefs(context, PreferencesUtils.AUTO_RESUME_TRACK_CURRENT_RETRY_DEFAULT, 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).
|
||||
Intent startIntent = createStartIntent(context);
|
||||
startIntent.putExtra(TrackRecordingService.RESUME_TRACK_EXTRA_NAME, true);
|
||||
mServiceRule.startService(startIntent);
|
||||
ITrackRecordingService service = ((ITrackRecordingService) mServiceRule.bindService(startIntent));
|
||||
|
||||
// then
|
||||
Assert.assertNotNull(service);
|
||||
|
||||
// We expect to resume the previous track.
|
||||
@@ -156,7 +154,7 @@ public class TrackRecordingServiceTest {
|
||||
@MediumTest
|
||||
@Test
|
||||
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)));
|
||||
Assert.assertFalse(service.isRecording());
|
||||
|
||||
@@ -165,15 +163,17 @@ public class TrackRecordingServiceTest {
|
||||
Assert.assertTrue(service.isRecording());
|
||||
Assert.assertEquals(id, service.getRecordingTrackId());
|
||||
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).
|
||||
|
||||
Intent startIntent = createStartIntent(context);
|
||||
startIntent.putExtra(TrackRecordingService.RESUME_TRACK_EXTRA_NAME, true);
|
||||
mServiceRule.startService(startIntent);
|
||||
service = ((ITrackRecordingService) mServiceRule.bindService(createStartIntent(context)));
|
||||
Assert.assertNotNull(service);
|
||||
|
||||
// then
|
||||
Assert.assertNotNull(service);
|
||||
Assert.assertTrue(service.isRecording());
|
||||
}
|
||||
|
||||
@@ -184,12 +184,15 @@ public class TrackRecordingServiceTest {
|
||||
createDummyTrack(trackId, System.currentTimeMillis(), false);
|
||||
|
||||
// Clear the number of attempts and set the timeout to 10 min.
|
||||
updateAutoResumePrefs(context, PreferencesUtils.AUTO_RESUME_TRACK_CURRENT_RETRY_DEFAULT, 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).
|
||||
Intent startIntent = createStartIntent(context);
|
||||
startIntent.putExtra(TrackRecordingService.RESUME_TRACK_EXTRA_NAME, true);
|
||||
mServiceRule.startService(startIntent);
|
||||
ITrackRecordingService service = ((ITrackRecordingService) mServiceRule.bindService(startIntent));
|
||||
|
||||
// then
|
||||
Assert.assertNotNull(service);
|
||||
|
||||
// We don't expect to resume the previous track, because it was stopped.
|
||||
@@ -201,15 +204,18 @@ public class TrackRecordingServiceTest {
|
||||
@Test
|
||||
public void testResumeAfterReboot_expiredTrack() throws Exception {
|
||||
// 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.
|
||||
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).
|
||||
Intent startIntent = createStartIntent(context);
|
||||
startIntent.putExtra(TrackRecordingService.RESUME_TRACK_EXTRA_NAME, true);
|
||||
mServiceRule.startService(startIntent);
|
||||
ITrackRecordingService service = ((ITrackRecordingService) mServiceRule.bindService(startIntent));
|
||||
|
||||
// then
|
||||
Assert.assertNotNull(service);
|
||||
|
||||
// We don't expect to resume the previous track, because it has expired.
|
||||
@@ -224,16 +230,15 @@ public class TrackRecordingServiceTest {
|
||||
createDummyTrack(trackId, System.currentTimeMillis(), true);
|
||||
|
||||
// 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).
|
||||
Intent startIntent = createStartIntent(context);
|
||||
startIntent.putExtra(TrackRecordingService.RESUME_TRACK_EXTRA_NAME, true);
|
||||
|
||||
//Explicit start service, so `startCommand()` is executed - which would be called by BootReceiver.
|
||||
context.startService(startIntent);
|
||||
|
||||
mServiceRule.startService(startIntent);
|
||||
ITrackRecordingService service = ((ITrackRecordingService) mServiceRule.bindService(startIntent));
|
||||
|
||||
//then
|
||||
Assert.assertNotNull(service);
|
||||
|
||||
// We don't expect to resume the previous track, because there were already too many attempts.
|
||||
@@ -247,7 +252,10 @@ public class TrackRecordingServiceTest {
|
||||
List<Track> tracks = providerUtils.getAllTracks();
|
||||
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.
|
||||
Assert.assertFalse(service.isRecording());
|
||||
Assert.assertEquals(-1L, service.getRecordingTrackId());
|
||||
@@ -266,11 +274,14 @@ public class TrackRecordingServiceTest {
|
||||
@MediumTest
|
||||
@Test
|
||||
public void testRecording_orphanedRecordingTrack() throws Exception {
|
||||
Intent startIntent = createStartIntent(context);
|
||||
ITrackRecordingService service = ((ITrackRecordingService) mServiceRule.bindService(startIntent));
|
||||
|
||||
// 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);
|
||||
|
||||
// Make sure that the service will not start recording and will clear the bogus track.
|
||||
ITrackRecordingService service = ((ITrackRecordingService) mServiceRule.bindService(createStartIntent(context)));
|
||||
// then
|
||||
Assert.assertFalse(service.isRecording());
|
||||
Assert.assertEquals(PreferencesUtils.RECORDING_TRACK_ID_DEFAULT, service.getRecordingTrackId());
|
||||
}
|
||||
@@ -286,7 +297,7 @@ public class TrackRecordingServiceTest {
|
||||
long newTrackId = service.startNewTrack();
|
||||
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());
|
||||
}
|
||||
|
||||
@@ -299,7 +310,7 @@ public class TrackRecordingServiceTest {
|
||||
// Ending the current track when there is no recording should not result in any error.
|
||||
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());
|
||||
}
|
||||
|
||||
@@ -405,45 +416,4 @@ public class TrackRecordingServiceTest {
|
||||
Assert.assertNotNull(wpt.getLocation());
|
||||
Assert.assertNull(wpt.getTripStatistics());
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronous/waitable broadcast receiver to be used in testing.
|
||||
*/
|
||||
private class BlockingBroadcastReceiver extends BroadcastReceiver {
|
||||
private static final long MAX_WAIT_TIME_MS = 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,7 +103,7 @@ public class TrackRecordingServiceTestLooper {
|
||||
@MediumTest
|
||||
@Test
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -125,7 +125,7 @@ public class TrackRecordingServiceTestLooper {
|
||||
@MediumTest
|
||||
@Test
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -139,7 +139,8 @@ public class TrackRecordingServiceTestLooper {
|
||||
@MediumTest
|
||||
@Test
|
||||
public void testWithProperties_minRecordingDistanceDefault() throws TimeoutException {
|
||||
PreferencesUtils.setInt(context, R.string.recording_distance_interval_key, PreferencesUtils.RECORDING_DISTANCE_INTERVAL_DEFAULT);
|
||||
int minRecordingIntervalDefault = Integer.parseInt(context.getResources().getString(R.string.min_recording_interval_default));
|
||||
PreferencesUtils.setInt(context, R.string.recording_distance_interval_key, minRecordingIntervalDefault);
|
||||
fullRecordingSession();
|
||||
}
|
||||
|
||||
@@ -153,7 +154,7 @@ public class TrackRecordingServiceTestLooper {
|
||||
@MediumTest
|
||||
@Test
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -174,7 +175,7 @@ public class TrackRecordingServiceTestLooper {
|
||||
@MediumTest
|
||||
@Test
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -188,7 +189,7 @@ public class TrackRecordingServiceTestLooper {
|
||||
@MediumTest
|
||||
@Test
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -202,7 +203,8 @@ public class TrackRecordingServiceTestLooper {
|
||||
@MediumTest
|
||||
@Test
|
||||
public void testWithProperties_minRequiredAccuracyDefault() throws TimeoutException {
|
||||
PreferencesUtils.setInt(context, R.string.recording_gps_accuracy_key, PreferencesUtils.RECORDING_GPS_ACCURACY_DEFAULT);
|
||||
int recordingGPSAccuracyDefault = Integer.parseInt(context.getResources().getString(R.string.recording_gps_accuracy_default));
|
||||
PreferencesUtils.setInt(context, R.string.recording_gps_accuracy_key, recordingGPSAccuracyDefault);
|
||||
fullRecordingSession();
|
||||
}
|
||||
|
||||
@@ -225,7 +227,7 @@ public class TrackRecordingServiceTestLooper {
|
||||
Track track = providerUtils.getTrack(id);
|
||||
Assert.assertNotNull(track);
|
||||
Assert.assertEquals(id, track.getId());
|
||||
Assert.assertEquals(id, PreferencesUtils.getLong(context, R.string.recording_track_id_key));
|
||||
Assert.assertEquals(id, PreferencesUtils.getRecordingTrackId(context));
|
||||
Assert.assertEquals(id, service.getRecordingTrackId());
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.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.3525Z", 2010, 5, 4, 3, 2, 1, 352);
|
||||
assertGetTime("2010-05-04T03:02:01.3529Z", 2010, 5, 4, 3, 2, 1, 353);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -149,8 +149,7 @@ public class StringUtilsTest {
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests {@link StringUtils#getTime(String)} with fractional seconds and time
|
||||
* zone.
|
||||
* Tests {@link StringUtils#getTime(String)} with fractional seconds and time zone.
|
||||
*/
|
||||
@Test
|
||||
public void testGetTime_fractionalAndTimezone() {
|
||||
@@ -183,7 +182,7 @@ public class StringUtilsTest {
|
||||
* Tests {@link StringUtils#getTimeParts(long)} with a positive number.
|
||||
*/
|
||||
@Test
|
||||
public void testGetTimeParts_postive() {
|
||||
public void testGetTimeParts_positive() {
|
||||
int[] parts = StringUtils.getTimeParts(61000);
|
||||
Assert.assertEquals(1, parts[0]);
|
||||
Assert.assertEquals(1, parts[1]);
|
||||
@@ -200,4 +199,24 @@ public class StringUtilsTest {
|
||||
Assert.assertEquals(-1, parts[1]);
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,8 +16,8 @@ limitations under the License.
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="de.dennisguse.opentracks"
|
||||
android:installLocation="auto"
|
||||
android:versionCode="3054"
|
||||
android:versionName="v3.2.0"> <!-- For FDroid. -->
|
||||
android:versionCode="3137"
|
||||
android:versionName="v3.2.6"> <!-- For FDroid. -->
|
||||
<!-- Permission to initialize services -->
|
||||
<application
|
||||
android:name=".Startup"
|
||||
@@ -30,10 +30,6 @@ limitations under the License.
|
||||
<!-- Activities -->
|
||||
<activity android:name=".AboutActivity" />
|
||||
|
||||
<activity
|
||||
android:name=".AggregatedStatsActivity"
|
||||
android:label="@string/menu_aggregated_statistics" />
|
||||
|
||||
<activity android:name=".DeleteActivity" />
|
||||
|
||||
<activity
|
||||
@@ -97,12 +93,8 @@ limitations under the License.
|
||||
android:name=".io.file.exporter.ExportActivity"
|
||||
android:theme="@style/ThemeCustomTransparent" />
|
||||
|
||||
<activity android:name=".settings.RecordingSettingsActivity" />
|
||||
|
||||
<activity android:name=".settings.SettingsActivity" />
|
||||
|
||||
<activity android:name=".settings.StatsSettingsActivity" />
|
||||
|
||||
<!-- Services -->
|
||||
<provider
|
||||
android:name=".content.ShareContentProvider"
|
||||
@@ -128,11 +120,11 @@ limitations under the License.
|
||||
</receiver>
|
||||
|
||||
<!-- Providers -->
|
||||
|
||||
<service
|
||||
android:name=".services.TrackRecordingService"
|
||||
android:enabled="true"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="location|connectedDevice"
|
||||
android:icon="@drawable/ic_logo_color_24dp"
|
||||
android:label="@string/recording_service" />
|
||||
</application>
|
||||
|
||||
@@ -19,12 +19,13 @@ package de.dennisguse.opentracks;
|
||||
import android.os.Bundle;
|
||||
import android.speech.tts.TextToSpeech;
|
||||
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
|
||||
/**
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
public abstract class AbstractActivity extends FragmentActivity {
|
||||
public abstract class AbstractActivity extends AppCompatActivity {
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
@@ -34,6 +35,9 @@ public abstract class AbstractActivity extends FragmentActivity {
|
||||
setVolumeControlStream(TextToSpeech.Engine.DEFAULT_STREAM);
|
||||
|
||||
setContentView(getLayoutResId());
|
||||
|
||||
Toolbar toolbar = findViewById(R.id.toolbar);
|
||||
setSupportActionBar(toolbar);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -16,24 +16,7 @@
|
||||
|
||||
package de.dennisguse.opentracks;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.SearchManager;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.util.Log;
|
||||
import android.util.SparseBooleanArray;
|
||||
import android.view.ActionMode;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.widget.AbsListView;
|
||||
import android.widget.ListView;
|
||||
import android.widget.SearchView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
import de.dennisguse.opentracks.fragments.ConfirmDeleteDialogFragment;
|
||||
import de.dennisguse.opentracks.fragments.ConfirmDeleteDialogFragment.ConfirmDeleteCaller;
|
||||
@@ -58,115 +41,6 @@ public abstract class AbstractListActivity extends AbstractActivity implements C
|
||||
protected static final int GPS_REQUEST_CODE = 6;
|
||||
private static final int DELETE_REQUEST_CODE = 3;
|
||||
|
||||
public static void configureListViewContextualMenu(final ListView listView, final ContextualActionModeCallback contextualActionModeCallback) {
|
||||
listView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE_MODAL);
|
||||
listView.setMultiChoiceModeListener(new AbsListView.MultiChoiceModeListener() {
|
||||
|
||||
@Override
|
||||
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
|
||||
mode.getMenuInflater().inflate(R.menu.list_context_menu, menu);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
|
||||
contextualActionModeCallback.onPrepare(menu, getCheckedPositions(listView), listView.getCheckedItemIds(), true);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyActionMode(ActionMode mode) {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemCheckedStateChanged(ActionMode mode, int position, long id, boolean checked) {
|
||||
mode.invalidate();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
|
||||
if (contextualActionModeCallback.onClick(item.getItemId(), getCheckedPositions(listView), listView.getCheckedItemIds())) {
|
||||
mode.finish();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the checked positions in a list view.
|
||||
*
|
||||
* @param list the list view
|
||||
*/
|
||||
private int[] getCheckedPositions(ListView list) {
|
||||
SparseBooleanArray positions = list.getCheckedItemPositions();
|
||||
ArrayList<Integer> arrayList = new ArrayList<>();
|
||||
for (int i = 0; i < positions.size(); i++) {
|
||||
int key = positions.keyAt(i);
|
||||
if (positions.valueAt(i)) {
|
||||
arrayList.add(key);
|
||||
}
|
||||
}
|
||||
int[] result = new int[arrayList.size()];
|
||||
for (int i = 0; i < arrayList.size(); i++) {
|
||||
result[i] = arrayList.get(i);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static void configureSearchWidget(Activity activity, final MenuItem menuItem, final TrackController trackController) {
|
||||
final SearchView searchView = (SearchView) menuItem.getActionView();
|
||||
SearchManager searchManager = (SearchManager) activity.getSystemService(Context.SEARCH_SERVICE);
|
||||
if (searchManager != null) {
|
||||
//NOTE: for some reason activity.getComponentName() did not trigger the SearchListActivity
|
||||
searchView.setSearchableInfo(searchManager.getSearchableInfo(new ComponentName(activity, SearchListActivity.class)));
|
||||
} else {
|
||||
Log.w(TAG, "Could not retrieve SearchManager.");
|
||||
}
|
||||
searchView.setQueryRefinementEnabled(true);
|
||||
searchView.setSubmitButtonEnabled(true);
|
||||
searchView.setOnQueryTextFocusChangeListener(new View.OnFocusChangeListener() {
|
||||
|
||||
@Override
|
||||
public void onFocusChange(View v, boolean hasFocus) {
|
||||
// Hide and show trackController when searchable widget has focus/no focus
|
||||
if (trackController != null) {
|
||||
if (hasFocus) {
|
||||
trackController.hide();
|
||||
} else {
|
||||
trackController.show();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
|
||||
@Override
|
||||
public boolean onQueryTextSubmit(String query) {
|
||||
menuItem.collapseActionView();
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onQueryTextChange(String newText) {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
searchView.setOnSuggestionListener(new SearchView.OnSuggestionListener() {
|
||||
@Override
|
||||
public boolean onSuggestionSelect(int position) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onSuggestionClick(int position) {
|
||||
menuItem.collapseActionView();
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
if (requestCode == DELETE_REQUEST_CODE) {
|
||||
@@ -182,8 +56,7 @@ public abstract class AbstractListActivity extends AbstractActivity implements C
|
||||
* @param trackIds the track ids
|
||||
*/
|
||||
protected void deleteTracks(long[] trackIds) {
|
||||
ConfirmDeleteDialogFragment.newInstance(trackIds)
|
||||
.show(getSupportFragmentManager(), ConfirmDeleteDialogFragment.CONFIRM_DELETE_DIALOG_TAG);
|
||||
ConfirmDeleteDialogFragment.showDialog(getSupportFragmentManager(), trackIds);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -192,7 +65,7 @@ public abstract class AbstractListActivity extends AbstractActivity implements C
|
||||
if (trackIds.length == 1 && trackIds[0] == -1L) {
|
||||
stopRecording = true;
|
||||
} else {
|
||||
long recordingTrackId = PreferencesUtils.getLong(this, R.string.recording_track_id_key);
|
||||
long recordingTrackId = PreferencesUtils.getRecordingTrackId(this);
|
||||
for (long trackId : trackIds) {
|
||||
if (trackId == recordingTrackId) {
|
||||
stopRecording = true;
|
||||
@@ -209,10 +82,9 @@ public abstract class AbstractListActivity extends AbstractActivity implements C
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the track recording service connection. For stopping the current
|
||||
* recording if need to delete the current recording track.
|
||||
* Gets the track recording service connection.
|
||||
* For stopping the current recording if need to delete the current recording track.
|
||||
*/
|
||||
@NonNull
|
||||
abstract protected TrackRecordingServiceConnection getTrackRecordingServiceConnection();
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
/*
|
||||
* Copyright 2012 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
|
||||
package de.dennisguse.opentracks;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import de.dennisguse.opentracks.content.ContentProviderUtils;
|
||||
import de.dennisguse.opentracks.content.Track;
|
||||
import de.dennisguse.opentracks.stats.TripStatistics;
|
||||
import de.dennisguse.opentracks.util.StatsUtils;
|
||||
|
||||
/**
|
||||
* An activity to view aggregated stats from all recorded tracks.
|
||||
*
|
||||
* @author Fergus Nelson
|
||||
*/
|
||||
public class AggregatedStatsActivity extends AbstractActivity {
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
StatsUtils.setTripStatisticsValues(this, getTripStatistics(), null);
|
||||
StatsUtils.setLocationValues(this, null, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getLayoutResId() {
|
||||
return R.layout.stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the aggregated trip statistics for all the recorded tracks or null if
|
||||
* there is no track.
|
||||
*/
|
||||
private TripStatistics getTripStatistics() {
|
||||
List<Track> tracks = ContentProviderUtils.Factory.get(this).getAllTracks();
|
||||
TripStatistics tripStatistics = null;
|
||||
if (!tracks.isEmpty()) {
|
||||
tripStatistics = new TripStatistics(tracks.iterator().next().getTripStatistics());
|
||||
for (int i = 1; i < tracks.size(); i++) {
|
||||
tripStatistics.merge(tracks.get(i).getTripStatistics());
|
||||
}
|
||||
}
|
||||
return tripStatistics;
|
||||
}
|
||||
}
|
||||
@@ -223,8 +223,8 @@ public class ChartView extends View {
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets chart by distance. It is expected that after changing this value, data
|
||||
* will be reloaded.
|
||||
* Sets chart by distance.
|
||||
* It is expected that after changing this value, data will be reloaded.
|
||||
*
|
||||
* @param value true for by distance, false for by time
|
||||
*/
|
||||
|
||||
@@ -22,14 +22,11 @@ import android.content.SharedPreferences;
|
||||
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
|
||||
import android.database.Cursor;
|
||||
import android.os.Bundle;
|
||||
import android.view.ContextMenu;
|
||||
import android.view.ContextMenu.ContextMenuInfo;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.AdapterView.AdapterContextMenuInfo;
|
||||
import android.widget.AdapterView.OnItemClickListener;
|
||||
import android.widget.ListView;
|
||||
|
||||
@@ -46,6 +43,7 @@ import de.dennisguse.opentracks.content.Waypoint.WaypointType;
|
||||
import de.dennisguse.opentracks.content.WaypointsColumns;
|
||||
import de.dennisguse.opentracks.fragments.DeleteMarkerDialogFragment;
|
||||
import de.dennisguse.opentracks.fragments.DeleteMarkerDialogFragment.DeleteMarkerCaller;
|
||||
import de.dennisguse.opentracks.util.ActivityUtils;
|
||||
import de.dennisguse.opentracks.util.IntentUtils;
|
||||
import de.dennisguse.opentracks.util.ListItemUtils;
|
||||
import de.dennisguse.opentracks.util.PreferencesUtils;
|
||||
@@ -71,7 +69,7 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
|
||||
private SharedPreferences sharedPreferences;
|
||||
|
||||
private long recordingTrackId = PreferencesUtils.RECORDING_TRACK_ID_DEFAULT;
|
||||
private boolean recordingTrackPaused = PreferencesUtils.RECORDING_TRACK_PAUSED_DEFAULT;
|
||||
private boolean recordingTrackPaused;
|
||||
/*
|
||||
* Note that sharedPreferenceChangeListener cannot be an anonymous inner
|
||||
* class. Anonymous inner class will get garbage collected.
|
||||
@@ -81,11 +79,11 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
|
||||
@Override
|
||||
public void onSharedPreferenceChanged(SharedPreferences preferences, String key) {
|
||||
// Note that the key can be null
|
||||
if (key == null || key.equals(PreferencesUtils.getKey(MarkerListActivity.this, R.string.recording_track_id_key))) {
|
||||
recordingTrackId = PreferencesUtils.getLong(MarkerListActivity.this, R.string.recording_track_id_key);
|
||||
if (PreferencesUtils.isKey(MarkerListActivity.this, R.string.recording_track_id_key, key)) {
|
||||
recordingTrackId = PreferencesUtils.getRecordingTrackId(MarkerListActivity.this);
|
||||
}
|
||||
if (key == null || key.equals(PreferencesUtils.getKey(MarkerListActivity.this, R.string.recording_track_paused_key))) {
|
||||
recordingTrackPaused = PreferencesUtils.getBoolean(MarkerListActivity.this, R.string.recording_track_paused_key, PreferencesUtils.RECORDING_TRACK_PAUSED_DEFAULT);
|
||||
if (PreferencesUtils.isKey(MarkerListActivity.this, R.string.recording_track_paused_key, key)) {
|
||||
recordingTrackPaused = PreferencesUtils.isRecordingTrackPaused(MarkerListActivity.this);
|
||||
}
|
||||
if (key != null) {
|
||||
runOnUiThread(new Runnable() {
|
||||
@@ -130,6 +128,8 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
recordingTrackPaused = PreferencesUtils.isRecordingTrackPausedDefault(this);
|
||||
|
||||
contentProviderUtils = ContentProviderUtils.Factory.get(this);
|
||||
sharedPreferences = PreferencesUtils.getSharedPreferences(this);
|
||||
|
||||
@@ -175,7 +175,7 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
|
||||
}
|
||||
};
|
||||
listView.setAdapter(resourceCursorAdapter);
|
||||
AbstractListActivity.configureListViewContextualMenu(listView, contextualActionModeCallback);
|
||||
ActivityUtils.configureListViewContextualMenu(listView, contextualActionModeCallback);
|
||||
|
||||
LoaderManager.getInstance(this).initLoader(0, null, new LoaderCallbacks<Cursor>() {
|
||||
@NonNull
|
||||
@@ -234,7 +234,7 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
|
||||
insertMarkerMenuItem = menu.findItem(R.id.marker_list_insert_marker);
|
||||
|
||||
searchMenuItem = menu.findItem(R.id.marker_list_search);
|
||||
AbstractListActivity.configureSearchWidget(this, searchMenuItem, null);
|
||||
ActivityUtils.configureSearchWidget(this, searchMenuItem, null);
|
||||
|
||||
return super.onCreateOptionsMenu(menu);
|
||||
}
|
||||
@@ -256,24 +256,6 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
|
||||
super.onCreateContextMenu(menu, v, menuInfo);
|
||||
getMenuInflater().inflate(R.menu.list_context_menu, menu);
|
||||
|
||||
AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo;
|
||||
contextualActionModeCallback.onPrepare(menu, new int[]{info.position}, new long[]{info.id}, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onContextItemSelected(MenuItem item) {
|
||||
AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo();
|
||||
if (handleContextItem(item.getItemId(), new long[]{info.id})) {
|
||||
return true;
|
||||
}
|
||||
return super.onContextItemSelected(item);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a context item selection.
|
||||
*
|
||||
@@ -300,8 +282,7 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
|
||||
if (markerIds.length > 1 && markerIds.length == listView.getCount()) {
|
||||
markerIds = new long[]{-1L};
|
||||
}
|
||||
DeleteMarkerDialogFragment.newInstance(markerIds)
|
||||
.show(getSupportFragmentManager(), DeleteMarkerDialogFragment.DELETE_MARKER_DIALOG_TAG);
|
||||
DeleteMarkerDialogFragment.showDialog(getSupportFragmentManager(), markerIds);
|
||||
return true;
|
||||
case R.id.list_context_menu_select_all:
|
||||
int size = listView.getCount();
|
||||
|
||||
@@ -22,14 +22,11 @@ import android.content.SharedPreferences;
|
||||
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.view.ContextMenu;
|
||||
import android.view.ContextMenu.ContextMenuInfo;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.AdapterView.AdapterContextMenuInfo;
|
||||
import android.widget.AdapterView.OnItemClickListener;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.ListView;
|
||||
@@ -55,6 +52,7 @@ import de.dennisguse.opentracks.fragments.DeleteMarkerDialogFragment;
|
||||
import de.dennisguse.opentracks.fragments.DeleteMarkerDialogFragment.DeleteMarkerCaller;
|
||||
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
|
||||
import de.dennisguse.opentracks.stats.TripStatistics;
|
||||
import de.dennisguse.opentracks.util.ActivityUtils;
|
||||
import de.dennisguse.opentracks.util.IntentUtils;
|
||||
import de.dennisguse.opentracks.util.ListItemUtils;
|
||||
import de.dennisguse.opentracks.util.PreferencesUtils;
|
||||
@@ -103,34 +101,35 @@ public class SearchListActivity extends AbstractListActivity implements DeleteMa
|
||||
|
||||
private long recordingTrackId = PreferencesUtils.RECORDING_TRACK_ID_DEFAULT;
|
||||
|
||||
private boolean recordingTrackPaused = PreferencesUtils.RECORDING_TRACK_PAUSED_DEFAULT;
|
||||
private boolean recordingTrackPaused;
|
||||
|
||||
// Callback when an item is selected in the contextual action mode
|
||||
private ContextualActionModeCallback contextualActionModeCallback = new ContextualActionModeCallback() {
|
||||
@Override
|
||||
public void onPrepare(Menu menu, int[] positions, long[] ids, boolean showSelectAll) {
|
||||
boolean isRecording = recordingTrackId != PreferencesUtils.RECORDING_TRACK_ID_DEFAULT;
|
||||
boolean isRecording = PreferencesUtils.isRecording(recordingTrackId);
|
||||
boolean isSingleSelection = positions.length == 1;
|
||||
|
||||
boolean isSingleSelectionTrack;
|
||||
if (isSingleSelection) {
|
||||
Map<String, Object> item = arrayAdapter.getItem(positions[0]);
|
||||
Long trackId = (Long) item.get(TRACK_ID_FIELD);
|
||||
Track track = contentProviderUtils.getTrack(trackId);
|
||||
|
||||
isSingleSelectionTrack = item.get(MARKER_ID_FIELD) == null;
|
||||
} else {
|
||||
isSingleSelectionTrack = false;
|
||||
}
|
||||
|
||||
// Not recording, one item, item is a track
|
||||
menu.findItem(R.id.list_context_menu_share).setVisible(!isRecording && isSingleSelection && isSingleSelectionTrack);
|
||||
//TODO Setup shareIntent.
|
||||
MenuItem shareMenuItem = menu.findItem(R.id.list_context_menu_share);
|
||||
if (isSingleSelectionTrack) {
|
||||
shareMenuItem.setVisible(!isRecording);
|
||||
}
|
||||
|
||||
// One item, item is a marker
|
||||
menu.findItem(R.id.list_context_menu_show_on_map).setVisible(isSingleSelection && !isSingleSelectionTrack);
|
||||
// One item, can be a track or a marker
|
||||
menu.findItem(R.id.list_context_menu_edit).setVisible(isSingleSelection);
|
||||
// One item. If track, no restriction.
|
||||
menu.findItem(R.id.list_context_menu_delete).setVisible(isSingleSelection && (isSingleSelectionTrack));
|
||||
menu.findItem(R.id.list_context_menu_delete).setVisible(isSingleSelection && isSingleSelectionTrack);
|
||||
// Disable select all, no action is available for multiple selection
|
||||
menu.findItem(R.id.list_context_menu_select_all).setVisible(false);
|
||||
}
|
||||
@@ -144,14 +143,14 @@ public class SearchListActivity extends AbstractListActivity implements DeleteMa
|
||||
private final OnSharedPreferenceChangeListener sharedPreferenceChangeListener = new OnSharedPreferenceChangeListener() {
|
||||
@Override
|
||||
public void onSharedPreferenceChanged(SharedPreferences preferences, String key) {
|
||||
if (key == null || key.equals(PreferencesUtils.getKey(SearchListActivity.this, R.string.stats_units_key))) {
|
||||
if (PreferencesUtils.isKey(SearchListActivity.this, R.string.stats_units_key, key)) {
|
||||
metricUnits = PreferencesUtils.isMetricUnits(SearchListActivity.this);
|
||||
}
|
||||
if (key == null || key.equals(PreferencesUtils.getKey(SearchListActivity.this, R.string.recording_track_id_key))) {
|
||||
recordingTrackId = PreferencesUtils.getLong(SearchListActivity.this, R.string.recording_track_id_key);
|
||||
if (PreferencesUtils.isKey(SearchListActivity.this, R.string.recording_track_id_key, key)) {
|
||||
recordingTrackId = PreferencesUtils.getRecordingTrackId(SearchListActivity.this);
|
||||
}
|
||||
if (key == null || key.equals(PreferencesUtils.getKey(SearchListActivity.this, R.string.recording_track_paused_key))) {
|
||||
recordingTrackPaused = PreferencesUtils.getBoolean(SearchListActivity.this, R.string.recording_track_paused_key, PreferencesUtils.RECORDING_TRACK_PAUSED_DEFAULT);
|
||||
if (PreferencesUtils.isKey(SearchListActivity.this, R.string.recording_track_paused_key, key)) {
|
||||
recordingTrackPaused = PreferencesUtils.isRecordingTrackPaused(SearchListActivity.this);
|
||||
}
|
||||
if (key != null) {
|
||||
runOnUiThread(new Runnable() {
|
||||
@@ -169,6 +168,8 @@ public class SearchListActivity extends AbstractListActivity implements DeleteMa
|
||||
super.onCreate(savedInstanceState);
|
||||
setDefaultKeyMode(DEFAULT_KEYS_SEARCH_LOCAL);
|
||||
|
||||
recordingTrackPaused = PreferencesUtils.isRecordingTrackPausedDefault(this);
|
||||
|
||||
contentProviderUtils = ContentProviderUtils.Factory.get(this);
|
||||
sharedPreferences = PreferencesUtils.getSharedPreferences(this);
|
||||
trackRecordingServiceConnection = new TrackRecordingServiceConnection(this, null);
|
||||
@@ -222,7 +223,7 @@ public class SearchListActivity extends AbstractListActivity implements DeleteMa
|
||||
startActivity(intent);
|
||||
}
|
||||
});
|
||||
AbstractListActivity.configureListViewContextualMenu(listView, contextualActionModeCallback);
|
||||
ActivityUtils.configureListViewContextualMenu(listView, contextualActionModeCallback);
|
||||
handleIntent(getIntent());
|
||||
}
|
||||
|
||||
@@ -259,24 +260,6 @@ public class SearchListActivity extends AbstractListActivity implements DeleteMa
|
||||
handleIntent(intent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
|
||||
super.onCreateContextMenu(menu, v, menuInfo);
|
||||
getMenuInflater().inflate(R.menu.list_context_menu, menu);
|
||||
|
||||
AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo;
|
||||
contextualActionModeCallback.onPrepare(menu, new int[]{info.position}, new long[]{info.id}, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onContextItemSelected(MenuItem item) {
|
||||
AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo();
|
||||
if (handleContextItem(item.getItemId(), new int[]{info.position})) {
|
||||
return true;
|
||||
}
|
||||
return super.onContextItemSelected(item);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onKeyUp(int keyCode, KeyEvent event) {
|
||||
if (keyCode == KeyEvent.KEYCODE_SEARCH) {
|
||||
@@ -301,13 +284,14 @@ public class SearchListActivity extends AbstractListActivity implements DeleteMa
|
||||
Long markerId = (Long) item.get(MARKER_ID_FIELD);
|
||||
Intent intent;
|
||||
switch (itemId) {
|
||||
case R.id.list_context_menu_share:
|
||||
//TODO
|
||||
Log.e(TAG, "Not implemented");
|
||||
return true;
|
||||
case R.id.list_context_menu_show_on_map:
|
||||
IntentUtils.showCoordinateOnMap(this, (double) item.get(MARKER_LATITUDE_FIELD), (double) item.get(MARKER_LONGITUDE_FIELD), item.get(NAME_FIELD) + "");
|
||||
return true;
|
||||
case R.id.list_context_menu_share:
|
||||
intent = IntentUtils.newShareFileIntent(this, new long[]{trackId});
|
||||
intent = Intent.createChooser(intent, null);
|
||||
startActivity(intent);
|
||||
return true;
|
||||
case R.id.list_context_menu_edit:
|
||||
if (markerId != null) {
|
||||
intent = IntentUtils.newIntent(this, MarkerEditActivity.class)
|
||||
@@ -323,7 +307,7 @@ public class SearchListActivity extends AbstractListActivity implements DeleteMa
|
||||
return true;
|
||||
case R.id.list_context_menu_delete:
|
||||
if (markerId != null) {
|
||||
DeleteMarkerDialogFragment.newInstance(new long[]{markerId}).show(getSupportFragmentManager(), DeleteMarkerDialogFragment.DELETE_MARKER_DIALOG_TAG);
|
||||
DeleteMarkerDialogFragment.showDialog(getSupportFragmentManager(), new long[]{markerId});
|
||||
} else {
|
||||
deleteTracks(new long[]{trackId});
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ import android.widget.TabHost;
|
||||
import android.widget.TabHost.TabContentFactory;
|
||||
import android.widget.TabWidget;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
import androidx.fragment.app.FragmentPagerAdapter;
|
||||
@@ -79,6 +80,7 @@ public class TabsAdapter extends FragmentPagerAdapter implements TabHost.OnTabCh
|
||||
}
|
||||
|
||||
@Override
|
||||
@NonNull
|
||||
public Fragment getItem(int position) {
|
||||
TabInfo info = tabInfos.get(position);
|
||||
return Fragment.instantiate(context, info.clss.getName(), info.bundle);
|
||||
|
||||
@@ -130,11 +130,11 @@ public class TrackController {
|
||||
handlerUpdateTotalTime.removeCallbacks(updateTotalTimeRunnable);
|
||||
}
|
||||
|
||||
void hide() {
|
||||
public void hide() {
|
||||
containerView.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
void show() {
|
||||
public void show() {
|
||||
containerView.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
|
||||
@@ -31,7 +31,6 @@ import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.View.OnClickListener;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.ShareActionProvider;
|
||||
import android.widget.TabHost;
|
||||
import android.widget.TabHost.TabSpec;
|
||||
import android.widget.Toast;
|
||||
@@ -48,15 +47,14 @@ import de.dennisguse.opentracks.content.Waypoint.WaypointType;
|
||||
import de.dennisguse.opentracks.content.WaypointCreationRequest;
|
||||
import de.dennisguse.opentracks.fragments.ChartFragment;
|
||||
import de.dennisguse.opentracks.fragments.ChooseActivityTypeDialogFragment;
|
||||
import de.dennisguse.opentracks.fragments.ChooseActivityTypeDialogFragment.ChooseActivityTypeCaller;
|
||||
import de.dennisguse.opentracks.fragments.ConfirmDeleteDialogFragment;
|
||||
import de.dennisguse.opentracks.fragments.FrequencyDialogFragment;
|
||||
import de.dennisguse.opentracks.fragments.StatsFragment;
|
||||
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
|
||||
import de.dennisguse.opentracks.settings.SettingsActivity;
|
||||
import de.dennisguse.opentracks.util.FileUtils;
|
||||
import de.dennisguse.opentracks.util.IntentUtils;
|
||||
import de.dennisguse.opentracks.util.PreferencesUtils;
|
||||
import de.dennisguse.opentracks.util.StatsUtils;
|
||||
import de.dennisguse.opentracks.util.TrackIconUtils;
|
||||
import de.dennisguse.opentracks.util.TrackUtils;
|
||||
|
||||
@@ -66,7 +64,7 @@ import de.dennisguse.opentracks.util.TrackUtils;
|
||||
* @author Leif Hendrik Wilden
|
||||
* @author Rodrigo Damazio
|
||||
*/
|
||||
public class TrackDetailActivity extends AbstractListActivity implements ChooseActivityTypeCaller, ConfirmDeleteDialogFragment.ConfirmDeleteCaller {
|
||||
public class TrackDetailActivity extends AbstractListActivity implements ChooseActivityTypeDialogFragment.ChooseActivityTypeCaller, ConfirmDeleteDialogFragment.ConfirmDeleteCaller {
|
||||
|
||||
public static final String EXTRA_TRACK_ID = "track_id";
|
||||
public static final String EXTRA_MARKER_ID = "marker_id";
|
||||
@@ -96,8 +94,7 @@ public class TrackDetailActivity extends AbstractListActivity implements ChooseA
|
||||
|
||||
// Preferences
|
||||
private long recordingTrackId = PreferencesUtils.RECORDING_TRACK_ID_DEFAULT;
|
||||
private boolean recordingTrackPaused = PreferencesUtils.RECORDING_TRACK_PAUSED_DEFAULT;
|
||||
private boolean showOnLockScreen = PreferencesUtils.SHOW_TRACKDETAIL_WHILE_RECORDING_ON_LOCKSCREEN;
|
||||
private boolean recordingTrackPaused;
|
||||
|
||||
private final Runnable bindChangedCallback = new Runnable() {
|
||||
@Override
|
||||
@@ -124,20 +121,18 @@ public class TrackDetailActivity extends AbstractListActivity implements ChooseA
|
||||
* Note that sharedPreferenceChangeListener cannot be an anonymous inner
|
||||
* class. Anonymous inner class will get garbage collected.
|
||||
*/
|
||||
private final OnSharedPreferenceChangeListener
|
||||
sharedPreferenceChangeListener = new OnSharedPreferenceChangeListener() {
|
||||
private final OnSharedPreferenceChangeListener sharedPreferenceChangeListener = new OnSharedPreferenceChangeListener() {
|
||||
@Override
|
||||
public void onSharedPreferenceChanged(SharedPreferences preferences, String key) {
|
||||
if (key == null || key.equals(PreferencesUtils.getKey(TrackDetailActivity.this, R.string.recording_track_id_key))) {
|
||||
recordingTrackId = PreferencesUtils.getLong(TrackDetailActivity.this, R.string.recording_track_id_key);
|
||||
if (PreferencesUtils.isKey(TrackDetailActivity.this, R.string.recording_track_id_key, key)) {
|
||||
recordingTrackId = PreferencesUtils.getRecordingTrackId(TrackDetailActivity.this);
|
||||
}
|
||||
|
||||
if (key == null || key.equals(PreferencesUtils.getKey(TrackDetailActivity.this, R.string.recording_track_paused_key))) {
|
||||
recordingTrackPaused = PreferencesUtils.getBoolean(TrackDetailActivity.this, R.string.recording_track_paused_key, PreferencesUtils.RECORDING_TRACK_PAUSED_DEFAULT);
|
||||
if (PreferencesUtils.isKey(TrackDetailActivity.this, R.string.recording_track_paused_key, key)) {
|
||||
recordingTrackPaused = PreferencesUtils.isRecordingTrackPaused(TrackDetailActivity.this);
|
||||
}
|
||||
|
||||
if (key == null || key.equals(PreferencesUtils.getKey(TrackDetailActivity.this, R.string.trackdetail_show_on_lockscreen_while_recording_key))) {
|
||||
showOnLockScreen = PreferencesUtils.getBoolean(TrackDetailActivity.this, R.string.trackdetail_show_on_lockscreen_while_recording_key, PreferencesUtils.SHOW_TRACKDETAIL_WHILE_RECORDING_ON_LOCKSCREEN);
|
||||
if (PreferencesUtils.isKey(TrackDetailActivity.this, R.string.stats_show_on_lockscreen_while_recording_key, key)) {
|
||||
setLockscreenPolicy();
|
||||
}
|
||||
|
||||
@@ -158,8 +153,6 @@ public class TrackDetailActivity extends AbstractListActivity implements ChooseA
|
||||
private MenuItem insertPhotoMenuItem;
|
||||
private MenuItem markerListMenuItem;
|
||||
private MenuItem shareMenuItem;
|
||||
private MenuItem voiceFrequencyMenuItem;
|
||||
private MenuItem splitFrequencyMenuItem;
|
||||
|
||||
private final OnClickListener recordListener = new OnClickListener() {
|
||||
@Override
|
||||
@@ -189,6 +182,8 @@ public class TrackDetailActivity extends AbstractListActivity implements ChooseA
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
recordingTrackPaused = PreferencesUtils.isRecordingTrackPausedDefault(this);
|
||||
|
||||
hasCamera = getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA);
|
||||
photoUri = savedInstanceState != null ? (Uri) savedInstanceState.getParcelable(PHOTO_URI_KEY) : null;
|
||||
hasPhoto = savedInstanceState != null && savedInstanceState.getBoolean(HAS_PHOTO_KEY, false);
|
||||
@@ -232,12 +227,14 @@ public class TrackDetailActivity extends AbstractListActivity implements ChooseA
|
||||
}
|
||||
|
||||
private void setLockscreenPolicy() {
|
||||
boolean showOnLockScreen = PreferencesUtils.shouldShowStatsOnLockscreen(TrackDetailActivity.this);
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
|
||||
setShowWhenLocked(showOnLockScreen);
|
||||
} else if (showOnLockScreen) {
|
||||
getWindow().addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED);
|
||||
} else {
|
||||
if (showOnLockScreen)
|
||||
getWindow().addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED);
|
||||
else getWindow().clearFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED);
|
||||
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -277,7 +274,7 @@ public class TrackDetailActivity extends AbstractListActivity implements ChooseA
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSaveInstanceState(Bundle outState) {
|
||||
protected void onSaveInstanceState(@NonNull Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
outState.putString(CURRENT_TAB_TAG_KEY, tabHost.getCurrentTabTag());
|
||||
if (photoUri != null) {
|
||||
@@ -317,13 +314,7 @@ public class TrackDetailActivity extends AbstractListActivity implements ChooseA
|
||||
insertMarkerMenuItem = menu.findItem(R.id.track_detail_insert_marker);
|
||||
insertPhotoMenuItem = menu.findItem(R.id.track_detail_insert_photo);
|
||||
insertPhotoMenuItem.setVisible(new Intent(MediaStore.ACTION_IMAGE_CAPTURE).resolveActivity(getPackageManager()) != null);
|
||||
|
||||
shareMenuItem = menu.findItem(R.id.track_detail_share);
|
||||
ShareActionProvider shareActionProvider = (ShareActionProvider) shareMenuItem.getActionProvider();
|
||||
shareActionProvider.setShareIntent(IntentUtils.newShareFileIntent(this, new long[]{trackId}));
|
||||
|
||||
voiceFrequencyMenuItem = menu.findItem(R.id.track_detail_voice_frequency);
|
||||
splitFrequencyMenuItem = menu.findItem(R.id.track_detail_split_frequency);
|
||||
markerListMenuItem = menu.findItem(R.id.track_detail_markers);
|
||||
|
||||
return super.onCreateOptionsMenu(menu);
|
||||
@@ -345,6 +336,11 @@ public class TrackDetailActivity extends AbstractListActivity implements ChooseA
|
||||
.putExtra(MarkerEditActivity.EXTRA_TRACK_ID, trackId);
|
||||
startActivity(intent);
|
||||
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:
|
||||
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);
|
||||
@@ -360,14 +356,6 @@ public class TrackDetailActivity extends AbstractListActivity implements ChooseA
|
||||
.putExtra(MarkerListActivity.EXTRA_TRACK_ID, trackId);
|
||||
startActivity(intent);
|
||||
return true;
|
||||
case R.id.track_detail_voice_frequency:
|
||||
FrequencyDialogFragment.newInstance(R.string.voice_frequency_key, PreferencesUtils.VOICE_FREQUENCY_DEFAULT, R.string.menu_voice_frequency)
|
||||
.show(getSupportFragmentManager(), FrequencyDialogFragment.FREQUENCY_DIALOG_TAG);
|
||||
return true;
|
||||
case R.id.track_detail_split_frequency:
|
||||
FrequencyDialogFragment.newInstance(R.string.split_frequency_key, PreferencesUtils.SPLIT_FREQUENCY_DEFAULT, R.string.menu_split_frequency)
|
||||
.show(getSupportFragmentManager(), FrequencyDialogFragment.FREQUENCY_DIALOG_TAG);
|
||||
return true;
|
||||
case R.id.track_detail_edit:
|
||||
intent = IntentUtils.newIntent(this, TrackEditActivity.class)
|
||||
.putExtra(TrackEditActivity.EXTRA_TRACK_ID, trackId);
|
||||
@@ -395,6 +383,7 @@ public class TrackDetailActivity extends AbstractListActivity implements ChooseA
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||
}
|
||||
|
||||
@@ -423,10 +412,16 @@ public class TrackDetailActivity extends AbstractListActivity implements ChooseA
|
||||
/**
|
||||
* Gets the track id.
|
||||
*/
|
||||
// TODO Seems to be no used.
|
||||
@Deprecated
|
||||
public long getTrackId() {
|
||||
return trackId;
|
||||
}
|
||||
|
||||
private void setBluetoothHeartRateSensorName(String sensorName) {
|
||||
StatsUtils.setBluetoothHeartRateSensorName(this, sensorName, isRecording());
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the data in the intent.
|
||||
*/
|
||||
@@ -468,8 +463,6 @@ public class TrackDetailActivity extends AbstractListActivity implements ChooseA
|
||||
insertPhotoMenuItem.setVisible(hasCamera && isRecording() && !isPaused);
|
||||
shareMenuItem.setVisible(!isRecording());
|
||||
markerListMenuItem.setShowAsAction(isRecording() ? MenuItem.SHOW_AS_ACTION_NEVER : MenuItem.SHOW_AS_ACTION_IF_ROOM);
|
||||
voiceFrequencyMenuItem.setVisible(isRecording());
|
||||
splitFrequencyMenuItem.setVisible(isRecording());
|
||||
String title;
|
||||
if (isRecording()) {
|
||||
title = getString(isPaused ? R.string.generic_paused : R.string.generic_recording);
|
||||
@@ -487,7 +480,7 @@ public class TrackDetailActivity extends AbstractListActivity implements ChooseA
|
||||
}
|
||||
|
||||
public void chooseActivityType(String category) {
|
||||
ChooseActivityTypeDialogFragment.newInstance(category).show(getSupportFragmentManager(), ChooseActivityTypeDialogFragment.CHOOSE_ACTIVITY_TYPE_DIALOG_TAG);
|
||||
ChooseActivityTypeDialogFragment.showDialog(getSupportFragmentManager(), category);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -28,10 +28,11 @@ import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
import android.widget.Spinner;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import de.dennisguse.opentracks.content.ContentProviderUtils;
|
||||
import de.dennisguse.opentracks.content.Track;
|
||||
import de.dennisguse.opentracks.fragments.ChooseActivityTypeDialogFragment;
|
||||
import de.dennisguse.opentracks.fragments.ChooseActivityTypeDialogFragment.ChooseActivityTypeCaller;
|
||||
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
|
||||
import de.dennisguse.opentracks.util.TrackIconUtils;
|
||||
import de.dennisguse.opentracks.util.TrackUtils;
|
||||
@@ -41,7 +42,7 @@ import de.dennisguse.opentracks.util.TrackUtils;
|
||||
*
|
||||
* @author Leif Hendrik Wilden
|
||||
*/
|
||||
public class TrackEditActivity extends AbstractActivity implements ChooseActivityTypeCaller {
|
||||
public class TrackEditActivity extends AbstractActivity implements ChooseActivityTypeDialogFragment.ChooseActivityTypeCaller {
|
||||
|
||||
public static final String EXTRA_TRACK_ID = "track_id";
|
||||
public static final String EXTRA_NEW_TRACK = "new_track";
|
||||
@@ -119,9 +120,7 @@ public class TrackEditActivity extends AbstractActivity implements ChooseActivit
|
||||
@Override
|
||||
public boolean onTouch(View v, MotionEvent event) {
|
||||
if (event.getAction() == MotionEvent.ACTION_UP) {
|
||||
ChooseActivityTypeDialogFragment.newInstance(activityType.getText().toString()).show(
|
||||
getSupportFragmentManager(),
|
||||
ChooseActivityTypeDialogFragment.CHOOSE_ACTIVITY_TYPE_DIALOG_TAG);
|
||||
ChooseActivityTypeDialogFragment.showDialog(getSupportFragmentManager(), activityType.getText().toString());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -130,9 +129,7 @@ public class TrackEditActivity extends AbstractActivity implements ChooseActivit
|
||||
@Override
|
||||
public boolean onKey(View v, int keyCode, KeyEvent event) {
|
||||
if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) {
|
||||
ChooseActivityTypeDialogFragment.newInstance(activityType.getText().toString()).show(
|
||||
getSupportFragmentManager(),
|
||||
ChooseActivityTypeDialogFragment.CHOOSE_ACTIVITY_TYPE_DIALOG_TAG);
|
||||
ChooseActivityTypeDialogFragment.showDialog(getSupportFragmentManager(), activityType.getText().toString());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -182,7 +179,7 @@ public class TrackEditActivity extends AbstractActivity implements ChooseActivit
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(Bundle outState) {
|
||||
public void onSaveInstanceState(@NonNull Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
outState.putString(ICON_VALUE_KEY, iconValue);
|
||||
}
|
||||
|
||||
@@ -28,8 +28,6 @@ import android.location.LocationManager;
|
||||
import android.os.Bundle;
|
||||
import android.provider.Settings;
|
||||
import android.util.Log;
|
||||
import android.view.ContextMenu;
|
||||
import android.view.ContextMenu.ContextMenuInfo;
|
||||
import android.view.Gravity;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.Menu;
|
||||
@@ -37,14 +35,12 @@ import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.View.OnClickListener;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.AdapterView.AdapterContextMenuInfo;
|
||||
import android.widget.AdapterView.OnItemClickListener;
|
||||
import android.widget.ListView;
|
||||
import android.widget.SearchView;
|
||||
import android.widget.ShareActionProvider;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.widget.SearchView;
|
||||
import androidx.core.app.ActivityCompat;
|
||||
import androidx.cursoradapter.widget.ResourceCursorAdapter;
|
||||
import androidx.loader.app.LoaderManager;
|
||||
@@ -60,9 +56,11 @@ import de.dennisguse.opentracks.fragments.ConfirmDeleteDialogFragment;
|
||||
import de.dennisguse.opentracks.services.ITrackRecordingService;
|
||||
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
|
||||
import de.dennisguse.opentracks.settings.SettingsActivity;
|
||||
import de.dennisguse.opentracks.util.ActivityUtils;
|
||||
import de.dennisguse.opentracks.util.IntentUtils;
|
||||
import de.dennisguse.opentracks.util.ListItemUtils;
|
||||
import de.dennisguse.opentracks.util.PreferencesUtils;
|
||||
import de.dennisguse.opentracks.util.ServiceUtils;
|
||||
import de.dennisguse.opentracks.util.StringUtils;
|
||||
import de.dennisguse.opentracks.util.TrackIconUtils;
|
||||
import de.dennisguse.opentracks.util.TrackUtils;
|
||||
@@ -117,10 +115,6 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
|
||||
public void onPrepare(Menu menu, int[] positions, long[] trackIds, boolean showSelectAll) {
|
||||
boolean isSingleSelection = trackIds.length == 1;
|
||||
|
||||
MenuItem shareMenuItem = menu.findItem(R.id.list_context_menu_share);
|
||||
ShareActionProvider shareActionProvider = (ShareActionProvider) shareMenuItem.getActionProvider();
|
||||
shareActionProvider.setShareIntent(trackIds.length == 0 ? null : IntentUtils.newShareFileIntent(TrackListActivity.this, trackIds));
|
||||
|
||||
menu.findItem(R.id.list_context_menu_edit).setVisible(isSingleSelection);
|
||||
menu.findItem(R.id.list_context_menu_select_all).setVisible(showSelectAll);
|
||||
}
|
||||
@@ -131,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.
|
||||
@@ -141,22 +135,17 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
|
||||
sharedPreferenceChangeListener = new OnSharedPreferenceChangeListener() {
|
||||
@Override
|
||||
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);
|
||||
}
|
||||
if (key == null || key.equals(
|
||||
PreferencesUtils.getKey(TrackListActivity.this, R.string.recording_track_id_key))) {
|
||||
recordingTrackId = PreferencesUtils.getLong(
|
||||
TrackListActivity.this, R.string.recording_track_id_key);
|
||||
if (key != null && recordingTrackId != PreferencesUtils.RECORDING_TRACK_ID_DEFAULT) {
|
||||
if (PreferencesUtils.isKey(TrackListActivity.this, R.string.recording_track_id_key, key)) {
|
||||
recordingTrackId = PreferencesUtils.getRecordingTrackId(TrackListActivity.this);
|
||||
if (key != null && PreferencesUtils.isRecording(recordingTrackId)) {
|
||||
trackRecordingServiceConnection.startAndBind();
|
||||
}
|
||||
}
|
||||
if (key == null || key.equals(PreferencesUtils.getKey(
|
||||
TrackListActivity.this, R.string.recording_track_paused_key))) {
|
||||
recordingTrackPaused = PreferencesUtils.getBoolean(TrackListActivity.this,
|
||||
R.string.recording_track_paused_key,
|
||||
PreferencesUtils.RECORDING_TRACK_PAUSED_DEFAULT);
|
||||
if (PreferencesUtils.isKey(TrackListActivity.this, R.string.recording_track_paused_key, key)) {
|
||||
recordingTrackPaused = PreferencesUtils.isRecordingTrackPaused(TrackListActivity.this);
|
||||
}
|
||||
if (key != null) {
|
||||
runOnUiThread(new Runnable() {
|
||||
@@ -164,7 +153,7 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
|
||||
public void run() {
|
||||
TrackListActivity.this.invalidateOptionsMenu();
|
||||
LoaderManager.getInstance(TrackListActivity.this).restartLoader(0, null, loaderCallbacks);
|
||||
boolean isRecording = recordingTrackId != PreferencesUtils.RECORDING_TRACK_ID_DEFAULT;
|
||||
boolean isRecording = PreferencesUtils.isRecording(recordingTrackId);
|
||||
trackController.update(isRecording, recordingTrackPaused);
|
||||
}
|
||||
});
|
||||
@@ -175,7 +164,6 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
|
||||
// Menu items
|
||||
private MenuItem searchMenuItem;
|
||||
private MenuItem startGpsMenuItem;
|
||||
private MenuItem aggregatedStatisticsMenuItem;
|
||||
private MenuItem deleteAllMenuItem;
|
||||
|
||||
private final OnClickListener stopListener = new OnClickListener() {
|
||||
@@ -200,7 +188,7 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
|
||||
runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
trackController.update(recordingTrackId != PreferencesUtils.RECORDING_TRACK_ID_DEFAULT, recordingTrackPaused);
|
||||
trackController.update(PreferencesUtils.isRecording(recordingTrackId), recordingTrackPaused);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -233,12 +221,11 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
|
||||
|
||||
private final OnClickListener recordListener = new OnClickListener() {
|
||||
public void onClick(View v) {
|
||||
if (recordingTrackId == PreferencesUtils.RECORDING_TRACK_ID_DEFAULT) {
|
||||
if (!PreferencesUtils.isRecording(recordingTrackId)) {
|
||||
// Not recording -> Recording
|
||||
updateMenuItems(false, true);
|
||||
startRecording();
|
||||
} else {
|
||||
if (recordingTrackPaused) {
|
||||
} else if (recordingTrackPaused) {
|
||||
// Paused -> Resume
|
||||
updateMenuItems(false, true);
|
||||
trackRecordingServiceConnection.resumeTrack();
|
||||
@@ -249,7 +236,6 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
|
||||
trackRecordingServiceConnection.pauseTrack();
|
||||
trackController.update(true, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -257,6 +243,8 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
recordingTrackPaused = PreferencesUtils.isRecordingTrackPausedDefault(this);
|
||||
|
||||
contentProviderUtils = ContentProviderUtils.Factory.get(this);
|
||||
sharedPreferences = PreferencesUtils.getSharedPreferences(this);
|
||||
|
||||
@@ -317,7 +305,8 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
|
||||
}
|
||||
};
|
||||
listView.setAdapter(resourceCursorAdapter);
|
||||
AbstractListActivity.configureListViewContextualMenu(listView, contextualActionModeCallback);
|
||||
|
||||
ActivityUtils.configureListViewContextualMenu(listView, contextualActionModeCallback);
|
||||
|
||||
LoaderManager.getInstance(this).initLoader(0, null, loaderCallbacks);
|
||||
showStartupDialogs();
|
||||
@@ -339,8 +328,7 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
|
||||
// Update UI
|
||||
this.invalidateOptionsMenu();
|
||||
LoaderManager.getInstance(this).restartLoader(0, null, loaderCallbacks);
|
||||
boolean isRecording = recordingTrackId != PreferencesUtils.RECORDING_TRACK_ID_DEFAULT;
|
||||
trackController.onResume(isRecording, recordingTrackPaused);
|
||||
trackController.onResume(PreferencesUtils.isRecording(recordingTrackId), recordingTrackPaused);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -380,10 +368,9 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
|
||||
getMenuInflater().inflate(R.menu.track_list, menu);
|
||||
|
||||
searchMenuItem = menu.findItem(R.id.track_list_search);
|
||||
AbstractListActivity.configureSearchWidget(this, searchMenuItem, trackController);
|
||||
ActivityUtils.configureSearchWidget(this, searchMenuItem, trackController);
|
||||
|
||||
startGpsMenuItem = menu.findItem(R.id.track_list_start_gps);
|
||||
aggregatedStatisticsMenuItem = menu.findItem(R.id.track_list_aggregated_statistics);
|
||||
deleteAllMenuItem = menu.findItem(R.id.track_list_delete_all);
|
||||
|
||||
return super.onCreateOptionsMenu(menu);
|
||||
@@ -391,14 +378,13 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
|
||||
|
||||
@Override
|
||||
public boolean onPrepareOptionsMenu(Menu menu) {
|
||||
boolean isGpsStarted = TrackRecordingServiceConnection.isRecordingServiceRunning(this);
|
||||
boolean isRecording = recordingTrackId != PreferencesUtils.RECORDING_TRACK_ID_DEFAULT;
|
||||
boolean isGpsStarted = ServiceUtils.isTrackRecordingServiceRunning(this);
|
||||
boolean isRecording = PreferencesUtils.isRecording(recordingTrackId);
|
||||
updateMenuItems(isGpsStarted, isRecording);
|
||||
|
||||
View searchView = searchMenuItem.getActionView();
|
||||
if (searchView instanceof SearchView) {
|
||||
((SearchView) searchView).setQuery("", false);
|
||||
}
|
||||
SearchView searchView = (SearchView) searchMenuItem.getActionView();
|
||||
searchView.setQuery("", false);
|
||||
|
||||
return super.onPrepareOptionsMenu(menu);
|
||||
}
|
||||
|
||||
@@ -412,7 +398,7 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
|
||||
intent = new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS);
|
||||
startActivity(intent);
|
||||
} else {
|
||||
startGps = !TrackRecordingServiceConnection.isRecordingServiceRunning(this);
|
||||
startGps = !ServiceUtils.isTrackRecordingServiceRunning(this);
|
||||
|
||||
// Show toast
|
||||
Toast toast = Toast.makeText(this, startGps ? R.string.gps_starting : R.string.gps_stopping, Toast.LENGTH_SHORT);
|
||||
@@ -439,10 +425,6 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
|
||||
intent = IntentUtils.newIntent(this, MarkerListActivity.class);
|
||||
startActivity(intent);
|
||||
return true;
|
||||
case R.id.track_list_aggregated_statistics:
|
||||
intent = IntentUtils.newIntent(this, AggregatedStatsActivity.class);
|
||||
startActivity(intent);
|
||||
return true;
|
||||
case R.id.track_list_delete_all:
|
||||
deleteTracks(new long[]{-1L});
|
||||
return true;
|
||||
@@ -454,24 +436,6 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
|
||||
super.onCreateContextMenu(menu, v, menuInfo);
|
||||
getMenuInflater().inflate(R.menu.list_context_menu, menu);
|
||||
|
||||
AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo;
|
||||
contextualActionModeCallback.onPrepare(menu, new int[]{info.position}, new long[]{info.id}, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onContextItemSelected(MenuItem item) {
|
||||
AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo();
|
||||
if (handleContextItem(item.getItemId(), new long[]{info.id})) {
|
||||
return true;
|
||||
}
|
||||
return super.onContextItemSelected(item);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onKeyUp(int keyCode, KeyEvent event) {
|
||||
if (keyCode == KeyEvent.KEYCODE_SEARCH && searchMenuItem != null) {
|
||||
@@ -531,9 +495,6 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
|
||||
startGpsMenuItem.setIcon(isGpsStarted ? R.drawable.ic_gps_fixed_24dp : R.drawable.ic_gps_off_24dp);
|
||||
}
|
||||
}
|
||||
if (aggregatedStatisticsMenuItem != null) {
|
||||
aggregatedStatisticsMenuItem.setVisible(hasTrack);
|
||||
}
|
||||
if (deleteAllMenuItem != null) {
|
||||
deleteAllMenuItem.setVisible(hasTrack && !isRecording);
|
||||
}
|
||||
@@ -562,12 +523,18 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
|
||||
* @return true if handled.
|
||||
*/
|
||||
private boolean handleContextItem(int itemId, long[] trackIds) {
|
||||
Intent intent;
|
||||
switch (itemId) {
|
||||
case R.id.list_context_menu_show_on_map:
|
||||
IntentUtils.showTrackOnMap(this, trackIds);
|
||||
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:
|
||||
Intent intent = IntentUtils.newIntent(this, TrackEditActivity.class)
|
||||
intent = IntentUtils.newIntent(this, TrackEditActivity.class)
|
||||
.putExtra(TrackEditActivity.EXTRA_TRACK_ID, trackIds[0]);
|
||||
startActivity(intent);
|
||||
return true;
|
||||
|
||||
@@ -28,11 +28,6 @@ public class SensorDataSetLocation extends Location {
|
||||
|
||||
private SensorDataSet sensorDataSet = null;
|
||||
|
||||
/**
|
||||
* The id of this location from the provider.
|
||||
*/
|
||||
private int id = -1;
|
||||
|
||||
public SensorDataSetLocation(Location location, SensorDataSet sensorDataSet) {
|
||||
super(location);
|
||||
this.sensorDataSet = sensorDataSet;
|
||||
@@ -50,17 +45,8 @@ public class SensorDataSetLocation extends Location {
|
||||
this.sensorDataSet = sensorDataSet;
|
||||
}
|
||||
|
||||
public int getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(int id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public void reset() {
|
||||
super.reset();
|
||||
sensorDataSet = null;
|
||||
id = -1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -208,7 +208,7 @@ public class TrackDataHub implements DataSourceListener {
|
||||
* Returns true if the selected track is recording.
|
||||
*/
|
||||
public boolean isSelectedTrackRecording() {
|
||||
return selectedTrackId == recordingTrackId && recordingTrackId != PreferencesUtils.RECORDING_TRACK_ID_DEFAULT;
|
||||
return selectedTrackId == recordingTrackId && PreferencesUtils.isRecording(recordingTrackId);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -254,13 +254,13 @@ public class TrackDataHub implements DataSourceListener {
|
||||
runInHandlerThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (key == null || key.equals(PreferencesUtils.getKey(context, R.string.recording_track_id_key))) {
|
||||
recordingTrackId = PreferencesUtils.getLong(context, R.string.recording_track_id_key);
|
||||
if (PreferencesUtils.isKey(context, R.string.recording_track_id_key, key)) {
|
||||
recordingTrackId = PreferencesUtils.getRecordingTrackId(context);
|
||||
}
|
||||
if (key == null || key.equals(PreferencesUtils.getKey(context, R.string.recording_track_paused_key))) {
|
||||
recordingTrackPaused = PreferencesUtils.getBoolean(context, R.string.recording_track_paused_key, PreferencesUtils.RECORDING_TRACK_PAUSED_DEFAULT);
|
||||
if (PreferencesUtils.isKey(context, R.string.recording_track_paused_key, key)) {
|
||||
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);
|
||||
if (key != null) {
|
||||
for (TrackDataListener trackDataListener : trackDataManager.getListeners(TrackDataType.PREFERENCE)) {
|
||||
@@ -270,7 +270,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);
|
||||
if (key != null) {
|
||||
for (TrackDataListener trackDataListener : trackDataManager.getListeners(TrackDataType.PREFERENCE)) {
|
||||
@@ -280,8 +280,8 @@ public class TrackDataHub implements DataSourceListener {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (key == null || key.equals(PreferencesUtils.getKey(context, R.string.recording_gps_accuracy_key))) {
|
||||
recordingGpsAccuracy = PreferencesUtils.getInt(context, R.string.recording_gps_accuracy_key, PreferencesUtils.RECORDING_GPS_ACCURACY_DEFAULT);
|
||||
if (PreferencesUtils.isKey(context, R.string.recording_gps_accuracy_key, key)) {
|
||||
recordingGpsAccuracy = PreferencesUtils.getRecordingGPSAccuracy(context);
|
||||
if (key != null) {
|
||||
for (TrackDataListener trackDataListener : trackDataManager.getListeners(TrackDataType.PREFERENCE)) {
|
||||
if (trackDataListener.onRecordingGpsAccuracy(recordingGpsAccuracy)) {
|
||||
@@ -290,8 +290,8 @@ public class TrackDataHub implements DataSourceListener {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (key == null || key.equals(PreferencesUtils.getKey(context, R.string.recording_distance_interval_key))) {
|
||||
recordingDistanceInterval = PreferencesUtils.getInt(context, R.string.recording_distance_interval_key, PreferencesUtils.RECORDING_DISTANCE_INTERVAL_DEFAULT);
|
||||
if (PreferencesUtils.isKey(context, R.string.recording_distance_interval_key, key)) {
|
||||
recordingDistanceInterval = PreferencesUtils.getRecordingDistanceInterval(context);
|
||||
if (key != null) {
|
||||
for (TrackDataListener trackDataListener : trackDataManager.getListeners(TrackDataType.PREFERENCE)) {
|
||||
if (trackDataListener.onRecordingDistanceIntervalChanged(recordingDistanceInterval)) {
|
||||
|
||||
@@ -2,9 +2,9 @@ package de.dennisguse.opentracks.content.sensor;
|
||||
|
||||
public final class SensorDataSet {
|
||||
|
||||
public static final float DATA_UNAVAILABLE = Integer.MAX_VALUE;
|
||||
public static final float DATA_UNAVAILABLE = Float.NaN;
|
||||
|
||||
//TODO It might be necessary to consider: sensor if sensor was connected as well.
|
||||
//TODO It might be necessary to consider: if sensor was connected as well.
|
||||
public String sensorName;
|
||||
public String sensorAddress;
|
||||
private float heartRate;
|
||||
@@ -40,7 +40,7 @@ public final class SensorDataSet {
|
||||
}
|
||||
|
||||
public boolean hasHeartRate() {
|
||||
return heartRate != DATA_UNAVAILABLE;
|
||||
return !Float.isNaN(heartRate) && heartRate > 0;
|
||||
}
|
||||
|
||||
public float getHeartRate() {
|
||||
@@ -48,7 +48,7 @@ public final class SensorDataSet {
|
||||
}
|
||||
|
||||
public boolean hasCadence() {
|
||||
return cadence != DATA_UNAVAILABLE;
|
||||
return !Float.isNaN(cadence);
|
||||
}
|
||||
|
||||
public float getCadence() {
|
||||
@@ -56,7 +56,7 @@ public final class SensorDataSet {
|
||||
}
|
||||
|
||||
public boolean hasPower() {
|
||||
return power != DATA_UNAVAILABLE;
|
||||
return !Float.isNaN(power);
|
||||
}
|
||||
|
||||
public float getPower() {
|
||||
@@ -77,7 +77,7 @@ public final class SensorDataSet {
|
||||
}
|
||||
|
||||
public boolean hasBatteryLevel() {
|
||||
return batteryLevel != DATA_UNAVAILABLE;
|
||||
return !Float.isNaN(batteryLevel);
|
||||
}
|
||||
|
||||
public float getBatteryLevel() {
|
||||
|
||||
@@ -8,6 +8,7 @@ public enum SensorState {
|
||||
NONE,
|
||||
CONNECTING,
|
||||
CONNECTED,
|
||||
DISCONNECTING,
|
||||
DISCONNECTED;
|
||||
|
||||
public static String getStateAsString(SensorState state, Context c) {
|
||||
@@ -18,6 +19,8 @@ public enum SensorState {
|
||||
return c.getString(R.string.sensor_state_connecting);
|
||||
case CONNECTED:
|
||||
return c.getString(R.string.sensor_state_connected);
|
||||
case DISCONNECTING:
|
||||
return c.getString(R.string.sensor_state_disconnected);
|
||||
case DISCONNECTED:
|
||||
return c.getString(R.string.sensor_state_disconnected);
|
||||
default:
|
||||
|
||||
@@ -68,7 +68,7 @@ public class ChartFragment extends Fragment implements TrackDataListener {
|
||||
|
||||
private boolean metricUnits = true;
|
||||
private boolean reportSpeed = true;
|
||||
private int recordingDistanceInterval = PreferencesUtils.RECORDING_DISTANCE_INTERVAL_DEFAULT;
|
||||
private int recordingDistanceInterval;
|
||||
|
||||
// Modes of operation
|
||||
private boolean chartByDistance = true;
|
||||
@@ -99,6 +99,8 @@ public class ChartFragment extends Fragment implements TrackDataListener {
|
||||
public void onCreate(Bundle 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.
|
||||
*/
|
||||
@@ -263,8 +265,7 @@ public class ChartFragment extends Fragment implements TrackDataListener {
|
||||
}
|
||||
reportSpeed = speed;
|
||||
chartView.setReportSpeed(reportSpeed);
|
||||
boolean chartShowSpeed = PreferencesUtils.getBoolean(
|
||||
getActivity(), R.string.chart_show_speed_key, PreferencesUtils.CHART_SHOW_SPEED_DEFAULT);
|
||||
boolean chartShowSpeed = PreferencesUtils.shouldChartShowSpeed(getActivity());
|
||||
setSeriesEnabled(ChartView.SPEED_SERIES, chartShowSpeed && reportSpeed);
|
||||
setSeriesEnabled(ChartView.PACE_SERIES, chartShowSpeed && !reportSpeed);
|
||||
runOnUiThread(new Runnable() {
|
||||
@@ -309,24 +310,24 @@ public class ChartFragment extends Fragment implements TrackDataListener {
|
||||
reloadTrackDataHub();
|
||||
needUpdate = true;
|
||||
}
|
||||
if (setSeriesEnabled(ChartView.ELEVATION_SERIES, PreferencesUtils.getBoolean(getActivity(), R.string.chart_show_elevation_key, PreferencesUtils.CHART_SHOW_ELEVATION_DEFAULT))) {
|
||||
if (setSeriesEnabled(ChartView.ELEVATION_SERIES, PreferencesUtils.shouldChartShowElevation(getActivity()))) {
|
||||
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)) {
|
||||
needUpdate = true;
|
||||
}
|
||||
if (setSeriesEnabled(ChartView.PACE_SERIES, chartShowSpeed && !reportSpeed)) {
|
||||
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;
|
||||
}
|
||||
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;
|
||||
}
|
||||
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;
|
||||
}
|
||||
if (needUpdate) {
|
||||
@@ -352,8 +353,8 @@ public class ChartFragment extends Fragment implements TrackDataListener {
|
||||
}
|
||||
|
||||
/**
|
||||
* Resumes the trackDataHub. Needs to be synchronized because trackDataHub can
|
||||
* be accessed by multiple threads.
|
||||
* Resumes the trackDataHub.
|
||||
* Needs to be synchronized because trackDataHub can be accessed by multiple threads.
|
||||
*/
|
||||
private synchronized void resumeTrackDataHub() {
|
||||
trackDataHub = ((TrackDetailActivity) getActivity()).getTrackDataHub();
|
||||
@@ -363,8 +364,8 @@ public class ChartFragment extends Fragment implements TrackDataListener {
|
||||
}
|
||||
|
||||
/**
|
||||
* Pauses the trackDataHub. Needs to be synchronized because trackDataHub can
|
||||
* be accessed by multiple threads.
|
||||
* Pauses the trackDataHub.
|
||||
* Needs to be synchronized because trackDataHub can be accessed by multiple threads.
|
||||
*/
|
||||
private synchronized void pauseTrackDataHub() {
|
||||
trackDataHub.unregisterTrackDataListener(this);
|
||||
@@ -372,16 +373,16 @@ public class ChartFragment extends Fragment implements TrackDataListener {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the selected track is recording. Needs to be synchronized
|
||||
* because trackDataHub can be accessed by multiple threads.
|
||||
* Returns true if the selected track is recording.
|
||||
* Needs to be synchronized because trackDataHub can be accessed by multiple threads.
|
||||
*/
|
||||
private synchronized boolean isSelectedTrackRecording() {
|
||||
return trackDataHub != null && trackDataHub.isSelectedTrackRecording();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reloads the trackDataHub. Needs to be synchronized because trackDataHub can
|
||||
* be accessed by multiple threads.
|
||||
* Reloads the trackDataHub.
|
||||
* Needs to be synchronized because trackDataHub can be accessed by multiple threads.
|
||||
*/
|
||||
private synchronized void reloadTrackDataHub() {
|
||||
if (trackDataHub != null) {
|
||||
@@ -389,18 +390,12 @@ public class ChartFragment extends Fragment implements TrackDataListener {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* To zoom in.
|
||||
*/
|
||||
private void zoomIn() {
|
||||
chartView.zoomIn();
|
||||
zoomControls.setIsZoomInEnabled(chartView.canZoomIn());
|
||||
zoomControls.setIsZoomOutEnabled(chartView.canZoomOut());
|
||||
}
|
||||
|
||||
/**
|
||||
* To zoom out.
|
||||
*/
|
||||
private void zoomOut() {
|
||||
chartView.zoomOut();
|
||||
zoomControls.setIsZoomInEnabled(chartView.canZoomIn());
|
||||
@@ -438,9 +433,6 @@ public class ChartFragment extends Fragment implements TrackDataListener {
|
||||
double elevation = Double.NaN;
|
||||
double speed = Double.NaN;
|
||||
double pace = Double.NaN;
|
||||
double heartRate = Double.NaN;
|
||||
double cadence = Double.NaN;
|
||||
double power = Double.NaN;
|
||||
|
||||
if (tripStatisticsUpdater != null) {
|
||||
tripStatisticsUpdater.addLocation(location, recordingDistanceInterval);
|
||||
@@ -466,6 +458,10 @@ public class ChartFragment extends Fragment implements TrackDataListener {
|
||||
}
|
||||
pace = speed == 0 ? 0.0 : 60.0 / speed;
|
||||
}
|
||||
|
||||
double heartRate = Double.NaN;
|
||||
double cadence = Double.NaN;
|
||||
double power = Double.NaN;
|
||||
if (location instanceof SensorDataSetLocation && ((SensorDataSetLocation) location).getSensorDataSet() != null) {
|
||||
SensorDataSet sensorDataSet = ((SensorDataSetLocation) location).getSensorDataSet();
|
||||
if (sensorDataSet.hasHeartRate()) {
|
||||
@@ -479,6 +475,7 @@ public class ChartFragment extends Fragment implements TrackDataListener {
|
||||
}
|
||||
}
|
||||
|
||||
//TODO: Is related to ChartView.ELEVATION_SERIES etc.
|
||||
if (data != null) {
|
||||
data[0] = timeOrDistance;
|
||||
data[1] = elevation;
|
||||
|
||||
@@ -1,34 +1,17 @@
|
||||
/*
|
||||
* Copyright 2013 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
|
||||
package de.dennisguse.opentracks.fragments;
|
||||
|
||||
import android.app.AlertDialog;
|
||||
import android.app.Dialog;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.AdapterView.OnItemClickListener;
|
||||
import android.widget.GridView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
@@ -38,56 +21,13 @@ import de.dennisguse.opentracks.util.TrackIconUtils;
|
||||
|
||||
/**
|
||||
* A DialogFragment to choose an activity type.
|
||||
*
|
||||
* @author apoorvn
|
||||
*/
|
||||
public class ChooseActivityTypeDialogFragment extends DialogFragment {
|
||||
public class ChooseActivityTypeDialogFragment extends DialogFragment implements AdapterView.OnItemClickListener {
|
||||
|
||||
public static final String CHOOSE_ACTIVITY_TYPE_DIALOG_TAG = "chooseActivityType";
|
||||
private static final String KEY_CATEGORY = "category";
|
||||
private ChooseActivityTypeCaller caller;
|
||||
private static final String CHOOSE_ACTIVITY_TYPE_DIALOG_TAG = "chooseActivityType";
|
||||
|
||||
public static ChooseActivityTypeDialogFragment newInstance(String category) {
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putString(KEY_CATEGORY, category);
|
||||
|
||||
ChooseActivityTypeDialogFragment fragment = new ChooseActivityTypeDialogFragment();
|
||||
fragment.setArguments(bundle);
|
||||
return fragment;
|
||||
}
|
||||
|
||||
public static Dialog getDialog(final Context context, final String category, final ChooseActivityTypeCaller caller) {
|
||||
View view = LayoutInflater.from(context).inflate(R.layout.choose_activity_type, null);
|
||||
GridView gridView = view.findViewById(R.id.choose_activity_type_grid_view);
|
||||
|
||||
List<Integer> imageIds = new ArrayList<>();
|
||||
for (String iconValue : TrackIconUtils.getAllIconValues()) {
|
||||
imageIds.add(TrackIconUtils.getIconDrawable(iconValue));
|
||||
}
|
||||
|
||||
final ChooseActivityTypeImageAdapter imageAdapter = new ChooseActivityTypeImageAdapter(imageIds);
|
||||
gridView.setAdapter(imageAdapter);
|
||||
|
||||
final AlertDialog alertDialog = new AlertDialog.Builder(context).setTitle(R.string.track_edit_activity_type_hint).setView(view).create();
|
||||
alertDialog.setOnShowListener(new DialogInterface.OnShowListener() {
|
||||
@Override
|
||||
public void onShow(DialogInterface dialog) {
|
||||
int position = getPosition(context, category);
|
||||
if (position != -1) {
|
||||
imageAdapter.setSelected(position);
|
||||
imageAdapter.notifyDataSetChanged();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
gridView.setOnItemClickListener(new OnItemClickListener() {
|
||||
@Override
|
||||
public void onItemClick(AdapterView<?> parent, View v, int position, long id) {
|
||||
caller.onChooseActivityTypeDone(TrackIconUtils.getAllIconValues().get(position));
|
||||
alertDialog.dismiss();
|
||||
}
|
||||
});
|
||||
return alertDialog;
|
||||
public static void showDialog(FragmentManager fragmentManager, String preselectedCategory) {
|
||||
new ChooseActivityTypeDialogFragment(preselectedCategory).show(fragmentManager, ChooseActivityTypeDialogFragment.CHOOSE_ACTIVITY_TYPE_DIALOG_TAG);
|
||||
}
|
||||
|
||||
private static int getPosition(Context context, String category) {
|
||||
@@ -99,32 +39,66 @@ public class ChooseActivityTypeDialogFragment extends DialogFragment {
|
||||
return TrackIconUtils.getAllIconValues().indexOf(iconValue);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttach(@NonNull Context context) {
|
||||
super.onAttach(context);
|
||||
try {
|
||||
caller = (ChooseActivityTypeCaller) context;
|
||||
} catch (ClassCastException e) {
|
||||
throw new ClassCastException(context + " must implement " + ChooseActivityTypeCaller.class.getSimpleName());
|
||||
}
|
||||
private String preselectedCategory;
|
||||
|
||||
private ChooseActivityTypeCaller chooseActivityTypeCaller;
|
||||
|
||||
private ChooseActivityTypeDialogFragment(String preselectedCategory) {
|
||||
this.preselectedCategory = preselectedCategory;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||
return getDialog(getActivity(), getArguments().getString(KEY_CATEGORY), caller);
|
||||
Dialog dialog = super.onCreateDialog(savedInstanceState);
|
||||
dialog.setTitle(R.string.track_edit_activity_type_hint);
|
||||
return dialog;
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
View view = inflater.inflate(R.layout.choose_activity_type, container);
|
||||
GridView gridView = view.findViewById(R.id.choose_activity_type_grid_view);
|
||||
|
||||
List<Integer> imageIds = new ArrayList<>();
|
||||
for (String iconValue : TrackIconUtils.getAllIconValues()) {
|
||||
imageIds.add(TrackIconUtils.getIconDrawable(iconValue));
|
||||
}
|
||||
|
||||
final ChooseActivityTypeImageAdapter imageAdapter = new ChooseActivityTypeImageAdapter(imageIds);
|
||||
gridView.setAdapter(imageAdapter);
|
||||
|
||||
int position = getPosition(getContext(), preselectedCategory);
|
||||
if (position != -1) {
|
||||
imageAdapter.setSelected(position);
|
||||
imageAdapter.notifyDataSetChanged();
|
||||
}
|
||||
|
||||
gridView.setOnItemClickListener(this);
|
||||
return view;
|
||||
}
|
||||
|
||||
public void onAttach(@NonNull Context context) {
|
||||
super.onAttach(context);
|
||||
try {
|
||||
chooseActivityTypeCaller = (ChooseActivityTypeCaller) context;
|
||||
} catch (ClassCastException e) {
|
||||
throw new ClassCastException(context + " must implement " + ChooseActivityTypeCaller.class.getSimpleName());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void onItemClick(AdapterView<?> adapterView, View view, int position, long id) {
|
||||
chooseActivityTypeCaller.onChooseActivityTypeDone(TrackIconUtils.getAllIconValues().get(position));
|
||||
dismiss();
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface for caller of this dialog fragment.
|
||||
*
|
||||
* @author apoorvn
|
||||
* Interface for chooseActivityTypeCaller of this dialog fragment.
|
||||
*/
|
||||
public interface ChooseActivityTypeCaller {
|
||||
|
||||
/**
|
||||
* Called when choose activity type is done.
|
||||
*/
|
||||
void onChooseActivityTypeDone(String iconValue);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.util.DialogUtils;
|
||||
@@ -34,22 +35,21 @@ import de.dennisguse.opentracks.util.DialogUtils;
|
||||
*/
|
||||
public class ConfirmDeleteDialogFragment extends DialogFragment {
|
||||
|
||||
public static final String CONFIRM_DELETE_DIALOG_TAG = "confirmDeleteDialog";
|
||||
private static final String KEY_TRACK_IDS = "trackIds";
|
||||
private static final String CONFIRM_DELETE_DIALOG_TAG = "confirmDeleteDialog";
|
||||
private ConfirmDeleteCaller caller;
|
||||
private long[] trackIds;
|
||||
|
||||
public ConfirmDeleteDialogFragment(long[] trackIds) {
|
||||
this.trackIds = trackIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new instance.
|
||||
*
|
||||
* @param trackIds list of track ids to delete. To delete all, set to size 1 with trackIds[0] == -1L
|
||||
*/
|
||||
public static ConfirmDeleteDialogFragment newInstance(long[] trackIds) {
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putLongArray(KEY_TRACK_IDS, trackIds);
|
||||
|
||||
ConfirmDeleteDialogFragment deleteTrackDialogFragment = new ConfirmDeleteDialogFragment();
|
||||
deleteTrackDialogFragment.setArguments(bundle);
|
||||
return deleteTrackDialogFragment;
|
||||
public static void showDialog(FragmentManager fragmentManager, long[] trackIds) {
|
||||
new ConfirmDeleteDialogFragment(trackIds).show(fragmentManager, CONFIRM_DELETE_DIALOG_TAG);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -62,11 +62,9 @@ public class ConfirmDeleteDialogFragment extends DialogFragment {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
@NonNull
|
||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||
final long[] trackIds = getArguments().getLongArray(KEY_TRACK_IDS);
|
||||
int titleId;
|
||||
int messageId;
|
||||
titleId = trackIds.length > 1 ? R.string.generic_delete_selected_confirm_title : R.string.track_delete_one_confirm_title;
|
||||
|
||||
@@ -24,6 +24,7 @@ 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.content.ContentProviderUtils;
|
||||
@@ -37,17 +38,17 @@ import de.dennisguse.opentracks.util.DialogUtils;
|
||||
*/
|
||||
public class DeleteMarkerDialogFragment extends DialogFragment {
|
||||
|
||||
public static final String DELETE_MARKER_DIALOG_TAG = "deleteMarkerDialog";
|
||||
private static final String KEY_MARKER_IDS = "markerIds";
|
||||
private static final String DELETE_MARKER_DIALOG_TAG = "deleteMarkerDialog";
|
||||
private long[] markerIds;
|
||||
|
||||
private DeleteMarkerCaller caller;
|
||||
|
||||
public static DeleteMarkerDialogFragment newInstance(long[] markerIds) {
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putLongArray(KEY_MARKER_IDS, markerIds);
|
||||
public DeleteMarkerDialogFragment(long[] markerIds) {
|
||||
this.markerIds = markerIds;
|
||||
}
|
||||
|
||||
DeleteMarkerDialogFragment deleteMarkerDialogFragment = new DeleteMarkerDialogFragment();
|
||||
deleteMarkerDialogFragment.setArguments(bundle);
|
||||
return deleteMarkerDialogFragment;
|
||||
public static void showDialog(FragmentManager fragmentManager, long[] markerIds) {
|
||||
new DeleteMarkerDialogFragment(markerIds).show(fragmentManager, DELETE_MARKER_DIALOG_TAG);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -64,7 +65,6 @@ public class DeleteMarkerDialogFragment extends DialogFragment {
|
||||
@NonNull
|
||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||
final FragmentActivity fragmentActivity = getActivity();
|
||||
final long[] markerIds = getArguments().getLongArray(KEY_MARKER_IDS);
|
||||
int titleId;
|
||||
int messageId;
|
||||
if (markerIds.length == 1 && markerIds[0] == -1L) {
|
||||
@@ -81,8 +81,7 @@ public class DeleteMarkerDialogFragment extends DialogFragment {
|
||||
new Thread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
ContentProviderUtils contentProviderUtils = ContentProviderUtils.Factory.get(
|
||||
fragmentActivity);
|
||||
ContentProviderUtils contentProviderUtils = ContentProviderUtils.Factory.get(fragmentActivity);
|
||||
for (long markerId : markerIds) {
|
||||
contentProviderUtils.deleteWaypoint(fragmentActivity,
|
||||
markerId, new DescriptionGeneratorImpl(fragmentActivity));
|
||||
|
||||
@@ -25,6 +25,7 @@ import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
@@ -39,19 +40,19 @@ import de.dennisguse.opentracks.util.FileUtils;
|
||||
*/
|
||||
public class FileTypeDialogFragment extends DialogFragment {
|
||||
|
||||
public static final String FILE_TYPE_DIALOG_TAG = "fileType";
|
||||
private static final String KEY_TITLE_ID = "titleId";
|
||||
private static final String KEY_OPTION_ID = "optionId";
|
||||
private static final String FILE_TYPE_DIALOG_TAG = "fileType";
|
||||
private int titleId;
|
||||
|
||||
private FileTypeCaller caller;
|
||||
private int optionId;
|
||||
|
||||
public static FileTypeDialogFragment newInstance(int titleId, int optionId) {
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putInt(KEY_TITLE_ID, titleId);
|
||||
bundle.putInt(KEY_OPTION_ID, optionId);
|
||||
public FileTypeDialogFragment(int titleId, int optionId) {
|
||||
this.titleId = titleId;
|
||||
this.optionId = optionId;
|
||||
}
|
||||
|
||||
FileTypeDialogFragment fileTypeDialogFragment = new FileTypeDialogFragment();
|
||||
fileTypeDialogFragment.setArguments(bundle);
|
||||
return fileTypeDialogFragment;
|
||||
public static void showDialog(FragmentManager fragmentManager, int titleId, int optionId) {
|
||||
new FileTypeDialogFragment(titleId, optionId).show(fragmentManager, FILE_TYPE_DIALOG_TAG);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -67,8 +68,6 @@ public class FileTypeDialogFragment extends DialogFragment {
|
||||
@Override
|
||||
@NonNull
|
||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||
int optionId = getArguments().getInt(KEY_OPTION_ID);
|
||||
final int titleId = getArguments().getInt(KEY_TITLE_ID);
|
||||
final TrackFileFormat[] trackFileFormats = {TrackFileFormat.KMZ_WITH_TRACKDETAIL_AND_SENSORDATA_AND_PICTURES, TrackFileFormat.KML_WITH_TRACKDETAIL_AND_SENSORDATA, TrackFileFormat.GPX};
|
||||
String[] choices = new String[trackFileFormats.length];
|
||||
for (int i = 0; i < choices.length; i++) {
|
||||
@@ -96,7 +95,7 @@ public class FileTypeDialogFragment extends DialogFragment {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCancel(DialogInterface dialog) {
|
||||
public void onCancel(@NonNull DialogInterface dialog) {
|
||||
onDismissed();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,109 +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 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 {
|
||||
|
||||
public static final String FREQUENCY_DIALOG_TAG = "frequencyDialog";
|
||||
|
||||
private static final String KEY_PREFERENCE_ID = "preferenceId";
|
||||
private static final String KEY_DEFAULT_VALUE = "defaultValue";
|
||||
private static final String KEY_TITLE_ID = "titleId";
|
||||
|
||||
public static FrequencyDialogFragment newInstance(int preferenceId, int defaultValue, int titleId) {
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putInt(KEY_PREFERENCE_ID, preferenceId);
|
||||
bundle.putInt(KEY_DEFAULT_VALUE, defaultValue);
|
||||
bundle.putInt(KEY_TITLE_ID, titleId);
|
||||
|
||||
FrequencyDialogFragment frequencyDialogFragment = new FrequencyDialogFragment();
|
||||
frequencyDialogFragment.setArguments(bundle);
|
||||
return frequencyDialogFragment;
|
||||
}
|
||||
|
||||
@Override
|
||||
@NonNull
|
||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||
FragmentActivity fragmentActivity = getActivity();
|
||||
|
||||
final int preferenceId = getArguments().getInt(KEY_PREFERENCE_ID);
|
||||
int defaultValue = getArguments().getInt(KEY_DEFAULT_VALUE);
|
||||
int titleId = getArguments().getInt(KEY_TITLE_ID);
|
||||
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]);
|
||||
}
|
||||
}
|
||||
@@ -192,8 +192,7 @@ public class MarkerDetailFragment extends Fragment {
|
||||
startActivity(intent);
|
||||
return true;
|
||||
case R.id.marker_detail_delete:
|
||||
DeleteMarkerDialogFragment.newInstance(new long[]{markerId})
|
||||
.show(getChildFragmentManager(), DeleteMarkerDialogFragment.DELETE_MARKER_DIALOG_TAG);
|
||||
DeleteMarkerDialogFragment.showDialog(getChildFragmentManager(), new long[]{markerId});
|
||||
return true;
|
||||
default:
|
||||
return super.onOptionsItemSelected(item);
|
||||
|
||||
@@ -43,10 +43,8 @@ import de.dennisguse.opentracks.content.Waypoint;
|
||||
import de.dennisguse.opentracks.content.sensor.SensorDataSet;
|
||||
import de.dennisguse.opentracks.services.ITrackRecordingService;
|
||||
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
|
||||
import de.dennisguse.opentracks.services.sensors.RemoteSensorManager;
|
||||
import de.dennisguse.opentracks.stats.TripStatistics;
|
||||
import de.dennisguse.opentracks.util.LocationUtils;
|
||||
import de.dennisguse.opentracks.util.PreferencesUtils;
|
||||
import de.dennisguse.opentracks.util.StatsUtils;
|
||||
import de.dennisguse.opentracks.util.TrackIconUtils;
|
||||
import de.dennisguse.opentracks.util.UnitConversions;
|
||||
@@ -59,7 +57,7 @@ import de.dennisguse.opentracks.util.UnitConversions;
|
||||
*/
|
||||
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 Handler handlerUpdateUI;
|
||||
@@ -68,7 +66,6 @@ public class StatsFragment extends Fragment implements TrackDataListener {
|
||||
|
||||
private Location lastLocation = null;
|
||||
private TripStatistics lastTripStatistics = null;
|
||||
private SensorDataSet sensorDataSet = null;
|
||||
|
||||
private final Runnable updateUIeachSecond = new Runnable() {
|
||||
public void run() {
|
||||
@@ -83,7 +80,7 @@ public class StatsFragment extends Fragment implements TrackDataListener {
|
||||
}
|
||||
};
|
||||
private String category = "";
|
||||
private int recordingGpsAccuracy = PreferencesUtils.RECORDING_GPS_ACCURACY_DEFAULT;
|
||||
private int recordingGpsAccuracy;
|
||||
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
@@ -93,9 +90,10 @@ public class StatsFragment extends Fragment implements TrackDataListener {
|
||||
@Override
|
||||
public void onActivityCreated(Bundle savedInstanceState) {
|
||||
super.onActivityCreated(savedInstanceState);
|
||||
|
||||
recordingGpsAccuracy = Integer.parseInt(getContext().getResources().getString(R.string.recording_gps_accuracy_default));
|
||||
|
||||
handlerUpdateUI = new Handler();
|
||||
trackRecordingServiceConnection = new TrackRecordingServiceConnection(getContext(), null);
|
||||
trackRecordingServiceConnection.startConnection(getContext());
|
||||
|
||||
Spinner activityTypeIcon = getView().findViewById(R.id.stats_activity_type_icon);
|
||||
activityTypeIcon.setAdapter(TrackIconUtils.getIconSpinnerAdapter(getActivity(), ""));
|
||||
@@ -123,6 +121,10 @@ public class StatsFragment extends Fragment implements TrackDataListener {
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
resumeTrackDataHub();
|
||||
|
||||
trackRecordingServiceConnection = new TrackRecordingServiceConnection(getContext(), null);
|
||||
trackRecordingServiceConnection.startConnection(getContext());
|
||||
|
||||
updateUi(getActivity());
|
||||
if (isSelectedTrackRecording()) {
|
||||
handlerUpdateUI.post(updateUIeachSecond);
|
||||
@@ -140,6 +142,7 @@ public class StatsFragment extends Fragment implements TrackDataListener {
|
||||
public void onStop() {
|
||||
super.onStop();
|
||||
trackRecordingServiceConnection.unbind();
|
||||
trackRecordingServiceConnection = null;
|
||||
}
|
||||
|
||||
@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() {
|
||||
ITrackRecordingService trackRecordingService = trackRecordingServiceConnection.getServiceIfBound();
|
||||
|
||||
SensorDataSet sensorDataSet = null;
|
||||
if (trackRecordingService == null) {
|
||||
Log.d(STATS_FRAGMENT_TAG, "Cannot get the track recording service.");
|
||||
sensorDataSet = null;
|
||||
} else {
|
||||
//TODO sensorState = trackRecordingService.getSensorState();
|
||||
sensorDataSet = trackRecordingService.getSensorData();
|
||||
|
||||
@@ -84,9 +84,7 @@ public class ExportActivity extends FragmentActivity implements FileTypeDialogFr
|
||||
}
|
||||
|
||||
private void fileTypeDialogStart() {
|
||||
FileTypeDialogFragment
|
||||
.newInstance(R.string.export_all_title, R.string.export_all_option)
|
||||
.show(getSupportFragmentManager(), FileTypeDialogFragment.FILE_TYPE_DIALOG_TAG);
|
||||
FileTypeDialogFragment.showDialog(getSupportFragmentManager(), R.string.export_all_title, R.string.export_all_option);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -27,7 +27,6 @@ import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.content.ContentProviderUtils;
|
||||
import de.dennisguse.opentracks.content.Track;
|
||||
import de.dennisguse.opentracks.content.TracksColumns;
|
||||
@@ -99,8 +98,8 @@ public class ExportAsyncTask extends AsyncTask<Void, Integer, Boolean> {
|
||||
protected Boolean doInBackground(Void... params) {
|
||||
try {
|
||||
Thread.currentThread().setPriority(Thread.MAX_PRIORITY);
|
||||
boolean isRecording = PreferencesUtils.getLong(exportActivity, R.string.recording_track_id_key) != PreferencesUtils.RECORDING_TRACK_ID_DEFAULT;
|
||||
boolean isPaused = PreferencesUtils.getBoolean(exportActivity, R.string.recording_track_paused_key, PreferencesUtils.RECORDING_TRACK_PAUSED_DEFAULT);
|
||||
boolean isRecording = PreferencesUtils.isRecording(exportActivity);
|
||||
boolean isPaused = PreferencesUtils.isRecordingTrackPaused(exportActivity);
|
||||
// Get the wake lock if not recording or paused
|
||||
if (!isRecording || isPaused) {
|
||||
wakeLock = SystemUtils.acquireWakeLock(exportActivity, wakeLock);
|
||||
|
||||
@@ -49,9 +49,10 @@ public class KmlTrackWriter implements TrackWriter {
|
||||
private static final String END_STYLE = "end";
|
||||
private static final String TRACK_STYLE = "track";
|
||||
private static final String SCHEMA_ID = "schema";
|
||||
private static final String CADENCE = "cadence";
|
||||
private static final String HEART_RATE = "heart_rate";
|
||||
private static final String POWER = "power";
|
||||
|
||||
private static final String SENSOR_TYPE_CADENCE = "cadence";
|
||||
private static final String SENSOR_TYPE_HEART_RATE = "heart_rate";
|
||||
private static final String SENSOR_TYPE_POWER = "power";
|
||||
|
||||
private static final String WAYPOINT_ICON = "http://maps.google.com/mapfiles/kml/pushpin/blue-pushpin.png";
|
||||
private static final String STATISTICS_ICON = "http://maps.google.com/mapfiles/kml/pushpin/ylw-pushpin.png";
|
||||
@@ -71,9 +72,6 @@ public class KmlTrackWriter implements TrackWriter {
|
||||
private List<Float> powerList = new ArrayList<>();
|
||||
private List<Float> cadenceList = new ArrayList<>();
|
||||
private List<Float> heartRateList = new ArrayList<>();
|
||||
private boolean hasPower;
|
||||
private boolean hasCadence;
|
||||
private boolean hasHeartRate;
|
||||
|
||||
private Location startLocation;
|
||||
|
||||
@@ -132,9 +130,9 @@ public class KmlTrackWriter implements TrackWriter {
|
||||
printWriter.println("<Schema id=\"" + SCHEMA_ID + "\">");
|
||||
|
||||
if (exportSensorData) {
|
||||
writeSensorStyle(POWER, context.getString(R.string.description_sensor_power));
|
||||
writeSensorStyle(CADENCE, context.getString(R.string.description_sensor_cadence));
|
||||
writeSensorStyle(HEART_RATE, context.getString(R.string.description_sensor_heart_rate));
|
||||
writeSensorStyle(SENSOR_TYPE_POWER, context.getString(R.string.description_sensor_power));
|
||||
writeSensorStyle(SENSOR_TYPE_CADENCE, context.getString(R.string.description_sensor_cadence));
|
||||
writeSensorStyle(SENSOR_TYPE_HEART_RATE, context.getString(R.string.description_sensor_heart_rate));
|
||||
}
|
||||
printWriter.println("</Schema>");
|
||||
}
|
||||
@@ -235,9 +233,6 @@ public class KmlTrackWriter implements TrackWriter {
|
||||
public void writeOpenSegment() {
|
||||
if (printWriter != null) {
|
||||
printWriter.println("<gx:Track>");
|
||||
hasPower = false;
|
||||
hasCadence = false;
|
||||
hasHeartRate = false;
|
||||
powerList.clear();
|
||||
cadenceList.clear();
|
||||
heartRateList.clear();
|
||||
@@ -250,14 +245,14 @@ public class KmlTrackWriter implements TrackWriter {
|
||||
printWriter.println("<ExtendedData>");
|
||||
printWriter.println("<SchemaData schemaUrl=\"#" + SCHEMA_ID + "\">");
|
||||
if (exportSensorData) {
|
||||
if (hasPower) {
|
||||
writeSensorData(powerList, POWER);
|
||||
if (powerList.size() > 0) {
|
||||
writeSensorData(powerList, SENSOR_TYPE_POWER);
|
||||
}
|
||||
if (hasCadence) {
|
||||
writeSensorData(cadenceList, CADENCE);
|
||||
if (cadenceList.size() > 0) {
|
||||
writeSensorData(cadenceList, SENSOR_TYPE_CADENCE);
|
||||
}
|
||||
if (hasHeartRate) {
|
||||
writeSensorData(heartRateList, HEART_RATE);
|
||||
if (heartRateList.size() > 0) {
|
||||
writeSensorData(heartRateList, SENSOR_TYPE_HEART_RATE);
|
||||
}
|
||||
}
|
||||
printWriter.println("</SchemaData>");
|
||||
@@ -466,11 +461,11 @@ public class KmlTrackWriter implements TrackWriter {
|
||||
* Writes a sensor style.
|
||||
*
|
||||
* @param name the name of the sesnor
|
||||
* @param displayName the sensor display name
|
||||
* @param sensorType the sensor display name
|
||||
*/
|
||||
private void writeSensorStyle(String name, String displayName) {
|
||||
printWriter.println("<gx:SimpleArrayField name=\"" + name + "\" type=\"int\">");
|
||||
printWriter.println("<displayName>" + StringUtils.formatCData(displayName) + "</displayName>");
|
||||
private void writeSensorStyle(String name, String sensorType) {
|
||||
printWriter.println("<gx:SimpleArrayField name=\"" + name + "\" type=\"float\">");
|
||||
printWriter.println("<displayName>" + StringUtils.formatCData(sensorType) + "</displayName>");
|
||||
printWriter.println("</gx:SimpleArrayField>");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,7 +102,7 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
|
||||
this.context = context;
|
||||
this.importTrackId = importTrackId;
|
||||
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<>();
|
||||
waypoints = new ArrayList<>();
|
||||
}
|
||||
@@ -384,8 +384,7 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
|
||||
* @param message the message
|
||||
*/
|
||||
protected String createErrorMessage(String message) {
|
||||
return String.format(Locale.US, "Parsing error at line: %d column: %d. %s",
|
||||
locator.getLineNumber(), locator.getColumnNumber(), message);
|
||||
return String.format(Locale.US, "Parsing error at line: %d column: %d. %s", locator.getLineNumber(), locator.getColumnNumber(), message);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -85,9 +85,7 @@ public class ImportActivity extends FragmentActivity implements FileTypeDialogFr
|
||||
}
|
||||
|
||||
private void fileTypeDialogStart() {
|
||||
FileTypeDialogFragment
|
||||
.newInstance(R.string.import_selection_title, R.string.import_selection_option)
|
||||
.show(getSupportFragmentManager(), FileTypeDialogFragment.FILE_TYPE_DIALOG_TAG);
|
||||
FileTypeDialogFragment.showDialog(getSupportFragmentManager(), R.string.import_selection_title, R.string.import_selection_option);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -28,7 +28,6 @@ import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.content.ContentProviderUtils;
|
||||
import de.dennisguse.opentracks.content.Track;
|
||||
import de.dennisguse.opentracks.io.file.TrackFileFormat;
|
||||
@@ -103,8 +102,8 @@ public class ImportAsyncTask extends AsyncTask<Void, Integer, Boolean> {
|
||||
try {
|
||||
Thread.currentThread().setPriority(Thread.MAX_PRIORITY);
|
||||
// Get the wake lock if not recording or paused
|
||||
boolean isRecording = PreferencesUtils.getLong(importActivity, R.string.recording_track_id_key) != PreferencesUtils.RECORDING_TRACK_ID_DEFAULT;
|
||||
boolean isPaused = PreferencesUtils.getBoolean(importActivity, R.string.recording_track_paused_key, PreferencesUtils.RECORDING_TRACK_PAUSED_DEFAULT);
|
||||
boolean isRecording = PreferencesUtils.isRecording(importActivity);
|
||||
boolean isPaused = PreferencesUtils.isRecordingTrackPaused(importActivity);
|
||||
if (!isRecording || isPaused) {
|
||||
wakeLock = SystemUtils.acquireWakeLock(importActivity, wakeLock);
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ package de.dennisguse.opentracks.io.file.importer;
|
||||
import android.content.Context;
|
||||
import android.location.Location;
|
||||
import android.net.Uri;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
|
||||
@@ -39,6 +40,8 @@ import de.dennisguse.opentracks.content.sensor.SensorDataSet;
|
||||
*/
|
||||
public class KmlFileTrackImporter extends AbstractFileTrackImporter {
|
||||
|
||||
private static final String TAG = KmlFileTrackImporter.class.getSimpleName();
|
||||
|
||||
private static final String CADENCE = "cadence";
|
||||
private static final String HEART_RATE = "heart_rate";
|
||||
private static final String POWER = "power";
|
||||
@@ -65,11 +68,11 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter {
|
||||
private static final String ATTRIBUTE_NAME = "name";
|
||||
|
||||
private boolean trackStarted = false;
|
||||
private String sensorName;
|
||||
private String sensorType;
|
||||
private ArrayList<Location> locationList;
|
||||
private ArrayList<Integer> cadenceList;
|
||||
private ArrayList<Integer> heartRateList;
|
||||
private ArrayList<Integer> powerList;
|
||||
private ArrayList<Float> cadenceList;
|
||||
private ArrayList<Float> heartRateList;
|
||||
private ArrayList<Float> powerList;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
@@ -179,6 +182,10 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter {
|
||||
* On waypoint end.
|
||||
*/
|
||||
private void onWaypointEnd() throws SAXException {
|
||||
if (waypointType == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Add a waypoint if the waypointType matches
|
||||
WaypointType type;
|
||||
switch (waypointType) {
|
||||
@@ -229,20 +236,30 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter {
|
||||
*/
|
||||
private void onTrackSegmentEnd() {
|
||||
// Close a track segment by inserting the segment locations
|
||||
boolean hasHeartRate = heartRateList.size() == locationList.size();
|
||||
boolean hasCadence = cadenceList.size() == locationList.size();
|
||||
boolean hasPower = powerList.size() == locationList.size();
|
||||
|
||||
for (int i = 0; i < locationList.size(); i++) {
|
||||
Location location = locationList.get(i);
|
||||
|
||||
if (!hasPower && !hasCadence && !hasHeartRate) {
|
||||
boolean hasSensorData = false;
|
||||
float heartrate = SensorDataSet.DATA_UNAVAILABLE;
|
||||
float cadence = SensorDataSet.DATA_UNAVAILABLE;
|
||||
float power = SensorDataSet.DATA_UNAVAILABLE;
|
||||
|
||||
if (i < heartRateList.size()) {
|
||||
heartrate = heartRateList.get(i);
|
||||
hasSensorData = true;
|
||||
}
|
||||
if (i < cadenceList.size()) {
|
||||
cadence = cadenceList.get(i);
|
||||
hasSensorData = true;
|
||||
}
|
||||
if (i < powerList.size()) {
|
||||
power = powerList.get(i);
|
||||
hasSensorData = true;
|
||||
}
|
||||
|
||||
if (!hasSensorData) {
|
||||
insertTrackPoint(location);
|
||||
} else {
|
||||
float heartrate = hasHeartRate ? heartRateList.get(i) : SensorDataSet.DATA_UNAVAILABLE;
|
||||
float cadence = hasHeartRate ? cadenceList.get(i) : SensorDataSet.DATA_UNAVAILABLE;
|
||||
float power = hasHeartRate ? powerList.get(i) : SensorDataSet.DATA_UNAVAILABLE;
|
||||
|
||||
SensorDataSetLocation sensorDataSetLocation = new SensorDataSetLocation(location, new SensorDataSet(heartrate, cadence, power, SensorDataSet.DATA_UNAVAILABLE, location.getTime()));
|
||||
insertTrackPoint(sensorDataSetLocation);
|
||||
}
|
||||
@@ -277,7 +294,7 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter {
|
||||
* On sensor data start. gx:SimpleArrayData start tag.
|
||||
*/
|
||||
private void onSensorDataStart(Attributes attributes) {
|
||||
sensorName = attributes.getValue(ATTRIBUTE_NAME);
|
||||
sensorType = attributes.getValue(ATTRIBUTE_NAME);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -291,18 +308,24 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter {
|
||||
if (content.equals("")) {
|
||||
return;
|
||||
}
|
||||
int value;
|
||||
float value;
|
||||
try {
|
||||
value = Integer.parseInt(content);
|
||||
value = Float.valueOf(content);
|
||||
} catch (NumberFormatException e) {
|
||||
throw new SAXException(createErrorMessage("Unable to parse gx:value:" + content), e);
|
||||
}
|
||||
if (POWER.equals(sensorName)) {
|
||||
powerList.add(value);
|
||||
} else if (HEART_RATE.equals(sensorName)) {
|
||||
heartRateList.add(value);
|
||||
} else if (CADENCE.equals(sensorName)) {
|
||||
cadenceList.add(value);
|
||||
switch (sensorType) {
|
||||
case POWER:
|
||||
powerList.add(value);
|
||||
break;
|
||||
case HEART_RATE:
|
||||
heartRateList.add(value);
|
||||
break;
|
||||
case CADENCE:
|
||||
cadenceList.add(value);
|
||||
break;
|
||||
default:
|
||||
Log.w(TAG, "Data from sensor " + sensorType + " is not (yet) supported.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,7 +101,7 @@ public class KmzTrackImporter implements TrackImporter {
|
||||
* @param trackId the trackId
|
||||
*/
|
||||
private void cleanImport(long trackId) {
|
||||
if (trackId != PreferencesUtils.RECORDING_TRACK_ID_DEFAULT) {
|
||||
if (PreferencesUtils.isRecording(trackId)) {
|
||||
ContentProviderUtils contentProviderUtils = ContentProviderUtils.Factory.get(context);
|
||||
contentProviderUtils.deleteTrack(context, trackId);
|
||||
}
|
||||
|
||||
@@ -57,8 +57,7 @@ import de.dennisguse.opentracks.content.Waypoint;
|
||||
import de.dennisguse.opentracks.content.Waypoint.WaypointType;
|
||||
import de.dennisguse.opentracks.content.WaypointCreationRequest;
|
||||
import de.dennisguse.opentracks.content.sensor.SensorDataSet;
|
||||
import de.dennisguse.opentracks.services.sensors.RemoteSensorManager;
|
||||
import de.dennisguse.opentracks.services.sensors.RemoteSensorManagerFactory;
|
||||
import de.dennisguse.opentracks.services.sensors.BluetoothRemoteSensorManager;
|
||||
import de.dennisguse.opentracks.services.tasks.AnnouncementPeriodicTaskFactory;
|
||||
import de.dennisguse.opentracks.services.tasks.PeriodicTaskExecutor;
|
||||
import de.dennisguse.opentracks.services.tasks.SplitPeriodicTaskFactory;
|
||||
@@ -118,59 +117,55 @@ public class TrackRecordingService extends Service {
|
||||
private final OnSharedPreferenceChangeListener sharedPreferenceChangeListener = new OnSharedPreferenceChangeListener() {
|
||||
@Override
|
||||
public void onSharedPreferenceChanged(SharedPreferences preferences, String key) {
|
||||
if (key == null || key.equals(PreferencesUtils.getKey(context, R.string.recording_track_id_key))) {
|
||||
long trackId = PreferencesUtils.getLong(context, R.string.recording_track_id_key);
|
||||
if (PreferencesUtils.isKey(context, R.string.recording_track_id_key, key)) {
|
||||
// Only through the TrackRecordingService can one stop a recording and set the recordingTrackId to -1L.
|
||||
if (trackId != PreferencesUtils.RECORDING_TRACK_ID_DEFAULT) {
|
||||
recordingTrackId = trackId;
|
||||
if (PreferencesUtils.isRecording(context)) {
|
||||
recordingTrackId = PreferencesUtils.getRecordingTrackId(context);
|
||||
}
|
||||
}
|
||||
if (key == null || key.equals(PreferencesUtils.getKey(context, R.string.recording_track_paused_key))) {
|
||||
recordingTrackPaused = PreferencesUtils.getBoolean(context, R.string.recording_track_paused_key, PreferencesUtils.RECORDING_TRACK_PAUSED_DEFAULT);
|
||||
if (PreferencesUtils.isKey(context, R.string.recording_track_paused_key, key)) {
|
||||
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);
|
||||
voiceExecutor.setMetricUnits(metricUnits);
|
||||
splitExecutor.setMetricUnits(metricUnits);
|
||||
}
|
||||
if (key == null || key.equals(PreferencesUtils.getKey(context, R.string.voice_frequency_key))) {
|
||||
voiceExecutor.setTaskFrequency(PreferencesUtils.getInt(context, R.string.voice_frequency_key, PreferencesUtils.VOICE_FREQUENCY_DEFAULT));
|
||||
if (PreferencesUtils.isKey(context, R.string.voice_frequency_key, key)) {
|
||||
voiceExecutor.setTaskFrequency(PreferencesUtils.getVoiceFrequency(context));
|
||||
}
|
||||
if (key == null || key.equals(PreferencesUtils.getKey(context, R.string.split_frequency_key))) {
|
||||
splitExecutor.setTaskFrequency(PreferencesUtils.getInt(context, R.string.split_frequency_key, PreferencesUtils.SPLIT_FREQUENCY_DEFAULT));
|
||||
if (PreferencesUtils.isKey(context, R.string.split_frequency_key, key)) {
|
||||
splitExecutor.setTaskFrequency(PreferencesUtils.getSplitFrequency(context));
|
||||
}
|
||||
if (key == null || key.equals(PreferencesUtils.getKey(context, R.string.min_recording_interval_key))) {
|
||||
int minRecordingInterval = PreferencesUtils.getInt(context, R.string.min_recording_interval_key, PreferencesUtils.MIN_RECORDING_INTERVAL_DEFAULT);
|
||||
switch (minRecordingInterval) {
|
||||
case PreferencesUtils.MIN_RECORDING_INTERVAL_ADAPT_BATTERY_LIFE:
|
||||
// Choose battery life over moving time accuracy.
|
||||
locationListenerPolicy = new AdaptiveLocationListenerPolicy(30 * UnitConversions.ONE_SECOND, 5 * ONE_MINUTE, 5);
|
||||
break;
|
||||
case PreferencesUtils.MIN_RECORDING_INTERVAL_ADAPT_ACCURACY:
|
||||
// Get all the updates.
|
||||
locationListenerPolicy = new AdaptiveLocationListenerPolicy(UnitConversions.ONE_SECOND, 30 * UnitConversions.ONE_SECOND, 0);
|
||||
break;
|
||||
default:
|
||||
locationListenerPolicy = new AbsoluteLocationListenerPolicy(minRecordingInterval * UnitConversions.ONE_SECOND);
|
||||
if (PreferencesUtils.isKey(context, R.string.min_recording_interval_key, key)) {
|
||||
int minRecordingInterval = PreferencesUtils.getMinRecordingInterval(context);
|
||||
if (minRecordingInterval == PreferencesUtils.getMinRecordingIntervalAdaptBatteryLife(context)) {
|
||||
// Choose battery life over moving time accuracy.
|
||||
locationListenerPolicy = new AdaptiveLocationListenerPolicy(30 * UnitConversions.ONE_SECOND, 5 * ONE_MINUTE, 5);
|
||||
} else if (minRecordingInterval == PreferencesUtils.getMinRecordingIntervalAdaptAccuracy(context)) {
|
||||
// Get all the updates.
|
||||
locationListenerPolicy = new AdaptiveLocationListenerPolicy(UnitConversions.ONE_SECOND, 30 * UnitConversions.ONE_SECOND, 0);
|
||||
} else {
|
||||
locationListenerPolicy = new AbsoluteLocationListenerPolicy(minRecordingInterval * UnitConversions.ONE_SECOND);
|
||||
}
|
||||
}
|
||||
if (key == null || key.equals(PreferencesUtils.getKey(context, R.string.recording_distance_interval_key))) {
|
||||
recordingDistanceInterval = PreferencesUtils.getInt(context, R.string.recording_distance_interval_key, PreferencesUtils.RECORDING_DISTANCE_INTERVAL_DEFAULT);
|
||||
if (PreferencesUtils.isKey(context, R.string.recording_distance_interval_key, key)) {
|
||||
recordingDistanceInterval = PreferencesUtils.getRecordingDistanceInterval(context);
|
||||
}
|
||||
if (key == null || key.equals(PreferencesUtils.getKey(context, R.string.max_recording_distance_key))) {
|
||||
maxRecordingDistance = PreferencesUtils.getInt(context, R.string.max_recording_distance_key, PreferencesUtils.MAX_RECORDING_DISTANCE_DEFAULT);
|
||||
if (PreferencesUtils.isKey(context, R.string.max_recording_distance_key, key)) {
|
||||
maxRecordingDistance = PreferencesUtils.getMaxRecordingDistance(context);
|
||||
}
|
||||
if (key == null || key.equals(PreferencesUtils.getKey(context, R.string.recording_gps_accuracy_key))) {
|
||||
recordingGpsAccuracy = PreferencesUtils.getInt(context, R.string.recording_gps_accuracy_key, PreferencesUtils.RECORDING_GPS_ACCURACY_DEFAULT);
|
||||
if (PreferencesUtils.isKey(context, R.string.recording_gps_accuracy_key, key)) {
|
||||
recordingGpsAccuracy = PreferencesUtils.getRecordingGPSAccuracy(context);
|
||||
}
|
||||
if (key == null || key.equals(PreferencesUtils.getKey(context, R.string.auto_resume_track_timeout_key))) {
|
||||
autoResumeTrackTimeout = PreferencesUtils.getInt(context, R.string.auto_resume_track_timeout_key, PreferencesUtils.AUTO_RESUME_TRACK_TIMEOUT_DEFAULT);
|
||||
if (PreferencesUtils.isKey(context, R.string.auto_resume_track_timeout_key, key)) {
|
||||
autoResumeTrackTimeout = PreferencesUtils.getAutoResumeTrackTimeout(context);
|
||||
}
|
||||
}
|
||||
};
|
||||
private TripStatisticsUpdater markerTripStatisticsUpdater;
|
||||
private WakeLock wakeLock;
|
||||
private RemoteSensorManager remoteSensorManager;
|
||||
private BluetoothRemoteSensorManager remoteSensorManager;
|
||||
private Location lastLocation;
|
||||
private boolean currentSegmentHasLocation;
|
||||
private boolean isIdle; // true if idle
|
||||
@@ -267,7 +262,7 @@ public class TrackRecordingService extends Service {
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
if (remoteSensorManager != null) {
|
||||
RemoteSensorManagerFactory.releaseSystemSensorManager();
|
||||
remoteSensorManager.stop();
|
||||
remoteSensorManager = null;
|
||||
}
|
||||
|
||||
@@ -310,7 +305,7 @@ public class TrackRecordingService extends Service {
|
||||
* Returns true if the service is recording.
|
||||
*/
|
||||
public boolean isRecording() {
|
||||
return recordingTrackId != PreferencesUtils.RECORDING_TRACK_ID_DEFAULT;
|
||||
return PreferencesUtils.isRecording(recordingTrackId);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -447,7 +442,8 @@ public class TrackRecordingService extends Service {
|
||||
*/
|
||||
private void handleStartCommand(Intent intent, int startId) {
|
||||
// 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.");
|
||||
updateRecordingState(PreferencesUtils.RECORDING_TRACK_ID_DEFAULT, true);
|
||||
stopSelfResult(startId);
|
||||
@@ -465,17 +461,19 @@ public class TrackRecordingService extends Service {
|
||||
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) {
|
||||
Log.d(TAG, "Not resuming. Exceeded maximum retry attempts.");
|
||||
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(context.getResources().getString(R.string.auto_resume_track_timeout_never))) {
|
||||
Log.d(TAG, "Not resuming. Auto-resume track timeout set to never.");
|
||||
return false;
|
||||
} else if (autoResumeTrackTimeout == PreferencesUtils.AUTO_RESUME_TRACK_TIMEOUT_ALWAYS) {
|
||||
}
|
||||
|
||||
if (autoResumeTrackTimeout == Integer.parseInt(context.getResources().getString(R.string.auto_resume_track_timeout_always))) {
|
||||
Log.d(TAG, "Resuming. Auto-resume track timeout set to always.");
|
||||
return true;
|
||||
}
|
||||
@@ -509,13 +507,13 @@ public class TrackRecordingService extends Service {
|
||||
|
||||
// Update shared preferences
|
||||
updateRecordingState(trackId, false);
|
||||
PreferencesUtils.setInt(this, R.string.auto_resume_track_current_retry_key, 0);
|
||||
PreferencesUtils.resetAutoResumeTrackCurrentRetryDefault(this);
|
||||
|
||||
// Update database
|
||||
track.setId(trackId);
|
||||
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.setIcon(TrackIconUtils.getIconValue(this, category));
|
||||
track.setTripStatistics(trackTripStatisticsUpdater.getTripStatistics());
|
||||
@@ -595,7 +593,8 @@ public class TrackRecordingService extends Service {
|
||||
*/
|
||||
private void startRecording(boolean trackStarted) {
|
||||
// Update instance variables
|
||||
remoteSensorManager = RemoteSensorManagerFactory.getSystemSensorManager(this);
|
||||
remoteSensorManager = new BluetoothRemoteSensorManager(this);
|
||||
remoteSensorManager.start();
|
||||
lastLocation = null;
|
||||
currentSegmentHasLocation = false;
|
||||
isIdle = false;
|
||||
@@ -693,7 +692,7 @@ public class TrackRecordingService extends Service {
|
||||
|
||||
// Update instance variables
|
||||
if (remoteSensorManager != null) {
|
||||
RemoteSensorManagerFactory.releaseSystemSensorManager();
|
||||
remoteSensorManager.stop();
|
||||
remoteSensorManager = null;
|
||||
}
|
||||
lastLocation = null;
|
||||
@@ -854,7 +853,7 @@ public class TrackRecordingService extends Service {
|
||||
*/
|
||||
private void insertLocation(Track track, Location location, Location lastValidTrackPoint) {
|
||||
if (location == null) {
|
||||
Log.w(TAG, "Ignore insertLocation. loation is null.");
|
||||
Log.w(TAG, "Ignore insertLocation. location is null.");
|
||||
return;
|
||||
}
|
||||
// Do not insert if inserted already
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
|
||||
package de.dennisguse.opentracks.services;
|
||||
|
||||
import android.app.ActivityManager;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
@@ -29,14 +28,13 @@ import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import de.dennisguse.opentracks.BuildConfig;
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.TrackEditActivity;
|
||||
import de.dennisguse.opentracks.content.WaypointCreationRequest;
|
||||
import de.dennisguse.opentracks.util.IntentUtils;
|
||||
import de.dennisguse.opentracks.util.PreferencesUtils;
|
||||
import de.dennisguse.opentracks.util.ServiceUtils;
|
||||
|
||||
/**
|
||||
* Wrapper for the track recording service.
|
||||
@@ -154,7 +152,7 @@ public class TrackRecordingServiceConnection implements ServiceConnection, Death
|
||||
return;
|
||||
}
|
||||
|
||||
if (!startIfNeeded && !isRecordingServiceRunning(context)) {
|
||||
if (!startIfNeeded && !ServiceUtils.isTrackRecordingServiceRunning(context)) {
|
||||
Log.d(TAG, "Service is not started. Not binding it.");
|
||||
return;
|
||||
}
|
||||
@@ -169,31 +167,6 @@ public class TrackRecordingServiceConnection implements ServiceConnection, Death
|
||||
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.
|
||||
*
|
||||
@@ -201,7 +174,7 @@ public class TrackRecordingServiceConnection implements ServiceConnection, Death
|
||||
*/
|
||||
public void startConnection(@NonNull Context context) {
|
||||
bindIfStarted();
|
||||
if (!isRecordingServiceRunning(context)) {
|
||||
if (!ServiceUtils.isTrackRecordingServiceRunning(context)) {
|
||||
resetRecordingState(context);
|
||||
}
|
||||
}
|
||||
@@ -226,37 +199,14 @@ public class TrackRecordingServiceConnection implements ServiceConnection, Death
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops the recording.
|
||||
*
|
||||
* @param context the context
|
||||
* @param showEditor true to show the editor
|
||||
*/
|
||||
public void stopRecording(@NonNull Context context, boolean showEditor) {
|
||||
ITrackRecordingService trackRecordingService = getServiceIfBound();
|
||||
if (trackRecordingService == null) {
|
||||
resetRecordingState(context);
|
||||
} else {
|
||||
try {
|
||||
if (showEditor) {
|
||||
// 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);
|
||||
trackRecordingService.endCurrentTrack();
|
||||
if (recordingTrackId != PreferencesUtils.RECORDING_TRACK_ID_DEFAULT) {
|
||||
Intent intent = IntentUtils.newIntent(context, TrackEditActivity.class)
|
||||
.putExtra(TrackEditActivity.EXTRA_TRACK_ID, recordingTrackId)
|
||||
.putExtra(TrackEditActivity.EXTRA_NEW_TRACK, true);
|
||||
context.startActivity(intent);
|
||||
}
|
||||
} else {
|
||||
trackRecordingService.endCurrentTrack();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
//TODO What exception are we catching here? Should be removed...
|
||||
Log.e(TAG, "Unable to stop recording.", e);
|
||||
}
|
||||
private static void resetRecordingState(Context context) {
|
||||
if (PreferencesUtils.isRecording(context)) {
|
||||
PreferencesUtils.setLong(context, R.string.recording_track_id_key, PreferencesUtils.RECORDING_TRACK_ID_DEFAULT);
|
||||
}
|
||||
boolean recordingTrackPaused = PreferencesUtils.isRecordingTrackPaused(context);
|
||||
if (!recordingTrackPaused) {
|
||||
PreferencesUtils.defaultRecordingTrackPaused(context);
|
||||
}
|
||||
unbindAndStop();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -284,14 +234,36 @@ public class TrackRecordingServiceConnection implements ServiceConnection, Death
|
||||
return -1L;
|
||||
}
|
||||
|
||||
private static void resetRecordingState(Context context) {
|
||||
long recordingTrackId = PreferencesUtils.getLong(context, R.string.recording_track_id_key);
|
||||
if (recordingTrackId != 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);
|
||||
if (!recordingTrackPaused) {
|
||||
PreferencesUtils.setBoolean(context, R.string.recording_track_paused_key, PreferencesUtils.RECORDING_TRACK_PAUSED_DEFAULT);
|
||||
/**
|
||||
* Stops the recording.
|
||||
*
|
||||
* @param context the context
|
||||
* @param showEditor true to show the editor
|
||||
*/
|
||||
public void stopRecording(@NonNull Context context, boolean showEditor) {
|
||||
ITrackRecordingService trackRecordingService = getServiceIfBound();
|
||||
if (trackRecordingService == null) {
|
||||
resetRecordingState(context);
|
||||
} else {
|
||||
try {
|
||||
if (showEditor) {
|
||||
// Need to remember the recordingTrackId before calling endCurrentTrack() as endCurrentTrack() sets the value to -1L.
|
||||
long recordingTrackId = PreferencesUtils.getRecordingTrackId(context);
|
||||
trackRecordingService.endCurrentTrack();
|
||||
if (PreferencesUtils.isRecording(context)) {
|
||||
Intent intent = IntentUtils.newIntent(context, TrackEditActivity.class)
|
||||
.putExtra(TrackEditActivity.EXTRA_TRACK_ID, recordingTrackId)
|
||||
.putExtra(TrackEditActivity.EXTRA_NEW_TRACK, true);
|
||||
context.startActivity(intent);
|
||||
}
|
||||
} else {
|
||||
trackRecordingService.endCurrentTrack();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
//TODO What exception are we catching here? Should be removed...
|
||||
Log.e(TAG, "Unable to stop recording.", e);
|
||||
}
|
||||
}
|
||||
unbindAndStop();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,11 +21,10 @@ import android.bluetooth.BluetoothGatt;
|
||||
import android.bluetooth.BluetoothGattCallback;
|
||||
import android.bluetooth.BluetoothGattCharacteristic;
|
||||
import android.bluetooth.BluetoothGattDescriptor;
|
||||
import android.bluetooth.BluetoothGattService;
|
||||
import android.bluetooth.BluetoothProfile;
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Message;
|
||||
import android.util.Log;
|
||||
|
||||
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.SensorState;
|
||||
import de.dennisguse.opentracks.util.BluetoothUtils;
|
||||
|
||||
/**
|
||||
* Manages connection to Bluetooth LE heart rate monitor.
|
||||
* TODO: shutdown connection.
|
||||
*/
|
||||
public class BluetoothConnectionManager {
|
||||
|
||||
// Message types sent to handler
|
||||
static final int MESSAGE_DEVICE_NAME = 1;
|
||||
static final int MESSAGE_READ = 2;
|
||||
static final int MESSAGE_CONNECTING = 1;
|
||||
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 CLIENT_CHARACTERISTIC_CONFIG_UUID = new UUID(0x290200001000L, 0x800000805f9b34fbL);
|
||||
|
||||
@@ -58,30 +56,52 @@ public class BluetoothConnectionManager {
|
||||
|
||||
private SensorState sensorState;
|
||||
|
||||
private BluetoothGatt bluetoothGatt;
|
||||
private BluetoothDevice bluetoothDevice;
|
||||
|
||||
private BluetoothGattCallback connectCallback = new BluetoothGattCallback() {
|
||||
@Override
|
||||
public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
|
||||
if (newState == BluetoothProfile.STATE_CONNECTED) {
|
||||
gatt.discoverServices();
|
||||
setState(SensorState.CONNECTED);
|
||||
switch (newState) {
|
||||
case BluetoothProfile.STATE_CONNECTING:
|
||||
Log.d(TAG, "Connecting to sensor: " + gatt.getDevice());
|
||||
setState(SensorState.CONNECTING);
|
||||
|
||||
//Inform about status change
|
||||
Message message = handler.obtainMessage(MESSAGE_DEVICE_NAME);
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putString(KEY_DEVICE_NAME, gatt.getDevice().getName());
|
||||
message.setData(bundle);
|
||||
handler.sendMessage(message);
|
||||
return;
|
||||
handler.obtainMessage(MESSAGE_CONNECTING, gatt.getDevice().getName()).sendToTarget();
|
||||
case BluetoothProfile.STATE_CONNECTED:
|
||||
Log.d(TAG, "Connected to sensor: " + gatt.getDevice());
|
||||
setState(SensorState.CONNECTED);
|
||||
|
||||
gatt.discoverServices();
|
||||
|
||||
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
|
||||
public void onServicesDiscovered(@NonNull BluetoothGatt gatt, int status) {
|
||||
BluetoothGattCharacteristic characteristic = gatt
|
||||
.getService(HEART_RATE_SERVICE_UUID)
|
||||
.getCharacteristic(HEART_RATE_MEASUREMENT_CHAR_UUID);
|
||||
BluetoothGattService service = gatt.getService(BluetoothUtils.HEART_RATE_SERVICE_UUID);
|
||||
if (service == null) {
|
||||
Log.e(TAG, "Could not get heart rate service for " + gatt.getDevice().getAddress());
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
BluetoothGattCharacteristic characteristic = service.getCharacteristic(HEART_RATE_MEASUREMENT_CHAR_UUID);
|
||||
if (characteristic == null) {
|
||||
Log.e(TAG, "Could not get BluetoothCharacteristic for " + gatt.getDevice().getAddress());
|
||||
return;
|
||||
}
|
||||
gatt.setCharacteristicNotification(characteristic, true);
|
||||
|
||||
BluetoothGattDescriptor descriptor = characteristic.getDescriptor(CLIENT_CHARACTERISTIC_CONFIG_UUID);
|
||||
@@ -91,10 +111,11 @@ public class BluetoothConnectionManager {
|
||||
|
||||
@Override
|
||||
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);
|
||||
SensorDataSet sensorDataSet = new SensorDataSet(heartRate, gatt.getDevice().getName(), gatt.getDevice().getAddress());
|
||||
Log.d(TAG, "Received heart beat rate " + deviceName + ": " + heartRate);
|
||||
SensorDataSet sensorDataSet = new SensorDataSet(heartRate, deviceName, gatt.getDevice().getAddress());
|
||||
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
|
||||
*/
|
||||
BluetoothConnectionManager(Context context, Handler handler) {
|
||||
BluetoothConnectionManager(@NonNull Context context, @NonNull BluetoothDevice bluetoothDevice, @NonNull Handler handler) {
|
||||
this.context = context;
|
||||
this.bluetoothDevice = bluetoothDevice;
|
||||
this.handler = handler;
|
||||
this.sensorState = SensorState.NONE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the sensor state.
|
||||
*/
|
||||
public synchronized void connect() {
|
||||
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() {
|
||||
return sensorState;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the sensor state.
|
||||
*
|
||||
* @param sensorState the sensor state
|
||||
*/
|
||||
private synchronized void setState(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.BluetoothDevice;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.os.Message;
|
||||
import android.util.Log;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import android.widget.Toast;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
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.UnitConversions;
|
||||
|
||||
/**
|
||||
* Bluetooth sensor manager.
|
||||
* TODO: Handle a BluetoothGatt.STATE_DISCONNECTED
|
||||
* Bluetooth LE sensor manager.
|
||||
* Should only be instantiated once!
|
||||
*
|
||||
* @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 BluetoothAdapter bluetoothAdapter = getDefaultBluetoothAdapter();
|
||||
private static final BluetoothAdapter bluetoothAdapter = BluetoothUtils.getDefaultBluetoothAdapter(TAG);
|
||||
|
||||
private final Context context;
|
||||
|
||||
private final BluetoothConnectionManager bluetoothConnectionManager;
|
||||
|
||||
private SensorDataSet sensorDataSet = null;
|
||||
private final SharedPreferences sharedPreferences;
|
||||
|
||||
// Handler that gets information back from the bluetoothConnectionManager
|
||||
private final Handler messageHandler = new Handler(Looper.getMainLooper()) {
|
||||
@Override
|
||||
public void handleMessage(Message message) {
|
||||
String toastMessage;
|
||||
switch (message.what) {
|
||||
case BluetoothConnectionManager.MESSAGE_DEVICE_NAME:
|
||||
String deviceName = message.getData().getString(BluetoothConnectionManager.KEY_DEVICE_NAME);
|
||||
case BluetoothConnectionManager.MESSAGE_CONNECTING:
|
||||
//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;
|
||||
case BluetoothConnectionManager.MESSAGE_READ:
|
||||
if (!(message.obj instanceof SensorDataSet)) {
|
||||
@@ -66,98 +72,109 @@ public class BluetoothRemoteSensorManager extends RemoteSensorManager {
|
||||
sensorDataSet = (SensorDataSet) message.obj;
|
||||
}
|
||||
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:
|
||||
Log.e(TAG, "Got an undefined case. Please check.");
|
||||
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
|
||||
*/
|
||||
BluetoothRemoteSensorManager(Context context) {
|
||||
public BluetoothRemoteSensorManager(Context context) {
|
||||
this.context = context;
|
||||
bluetoothConnectionManager = new BluetoothConnectionManager(context, messageHandler);
|
||||
sharedPreferences = PreferencesUtils.getSharedPreferences(context);
|
||||
}
|
||||
|
||||
private static BluetoothAdapter getDefaultBluetoothAdapter() {
|
||||
// If from the main application thread, return directly
|
||||
if (Thread.currentThread().equals(Looper.getMainLooper().getThread())) {
|
||||
return BluetoothAdapter.getDefaultAdapter();
|
||||
}
|
||||
|
||||
// Get the default adapter from the main application thread
|
||||
final ArrayList<BluetoothAdapter> adapters = new ArrayList<>(1);
|
||||
final Object mutex = new Object();
|
||||
|
||||
Handler handler = new Handler(Looper.getMainLooper());
|
||||
handler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
adapters.add(BluetoothAdapter.getDefaultAdapter());
|
||||
synchronized (mutex) {
|
||||
mutex.notify();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
while (adapters.isEmpty()) {
|
||||
synchronized (mutex) {
|
||||
try {
|
||||
mutex.wait(UnitConversions.ONE_SECOND);
|
||||
} catch (InterruptedException e) {
|
||||
Log.e(TAG, "Interrupted while waiting for default bluetooth adapter", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (adapters.get(0) == null) {
|
||||
Log.w(TAG, "No bluetooth adapter found.");
|
||||
}
|
||||
return adapters.get(0);
|
||||
public void start() {
|
||||
sharedPreferences.registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
|
||||
startCurrentSensor();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
disconnect();
|
||||
sharedPreferences.unregisterOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
|
||||
}
|
||||
|
||||
|
||||
public boolean isEnabled() {
|
||||
return bluetoothAdapter != null && bluetoothAdapter.isEnabled();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setUpChannel() {
|
||||
public SensorDataSet getSensorDataSet() {
|
||||
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()) {
|
||||
Log.w(TAG, "Bluetooth not enabled.");
|
||||
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)) {
|
||||
Log.w(TAG, "No bluetooth address.");
|
||||
return;
|
||||
}
|
||||
Log.w(TAG, "Connecting to bluetooth address: " + address);
|
||||
Log.i(TAG, "Connecting to bluetooth address: " + address);
|
||||
|
||||
BluetoothDevice device;
|
||||
try {
|
||||
device = bluetoothAdapter.getRemoteDevice(address);
|
||||
} 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;
|
||||
}
|
||||
bluetoothConnectionManager.connect(device);
|
||||
|
||||
disconnect();
|
||||
|
||||
bluetoothConnectionManager = new BluetoothConnectionManager(context, device, messageHandler);
|
||||
bluetoothConnectionManager.connect();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void tearDownChannel() {
|
||||
bluetoothConnectionManager.reset();
|
||||
}
|
||||
|
||||
@Override
|
||||
public SensorState getSensorState() {
|
||||
return bluetoothConnectionManager.getSensorState();
|
||||
}
|
||||
|
||||
@Override
|
||||
public SensorDataSet getSensorDataSet() {
|
||||
return sensorDataSet;
|
||||
private void disconnect() {
|
||||
if (bluetoothConnectionManager != null) {
|
||||
bluetoothConnectionManager.disconnect();
|
||||
bluetoothConnectionManager = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -17,9 +17,9 @@ package de.dennisguse.opentracks.services.tasks;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.services.TrackRecordingService;
|
||||
import de.dennisguse.opentracks.stats.TripStatistics;
|
||||
import de.dennisguse.opentracks.util.PreferencesUtils;
|
||||
import de.dennisguse.opentracks.util.UnitConversions;
|
||||
|
||||
/**
|
||||
@@ -35,13 +35,14 @@ public class PeriodicTaskExecutor {
|
||||
private final TrackRecordingService trackRecordingService;
|
||||
private final PeriodicTaskFactory periodicTaskFactory;
|
||||
|
||||
private final int TASK_FREQUENCY_OFF;
|
||||
/**
|
||||
* The task frequency.
|
||||
* A positive value is a time frequency (minutes).
|
||||
* A negative value is a distance frequency (km or mi).
|
||||
* A zero value is to turn off periodic task.
|
||||
*/
|
||||
private int taskFrequency = PreferencesUtils.FREQUENCY_OFF;
|
||||
private int taskFrequency;
|
||||
|
||||
private PeriodicTask periodicTask;
|
||||
|
||||
@@ -56,6 +57,9 @@ public class PeriodicTaskExecutor {
|
||||
public PeriodicTaskExecutor(TrackRecordingService trackRecordingService, PeriodicTaskFactory periodicTaskFactory) {
|
||||
this.trackRecordingService = trackRecordingService;
|
||||
this.periodicTaskFactory = periodicTaskFactory;
|
||||
|
||||
TASK_FREQUENCY_OFF = Integer.parseInt(trackRecordingService.getBaseContext().getResources().getString(R.string.frequency_off));
|
||||
taskFrequency = TASK_FREQUENCY_OFF;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -71,7 +75,7 @@ public class PeriodicTaskExecutor {
|
||||
timerTaskExecutor.shutdown();
|
||||
timerTaskExecutor = null;
|
||||
}
|
||||
if (taskFrequency == PreferencesUtils.FREQUENCY_OFF) {
|
||||
if (taskFrequency == TASK_FREQUENCY_OFF) {
|
||||
Log.d(TAG, "Task frequency is off.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -17,8 +17,7 @@
|
||||
package de.dennisguse.opentracks.settings;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Parcelable;
|
||||
import android.preference.DialogPreference;
|
||||
import android.os.Bundle;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.MotionEvent;
|
||||
@@ -28,7 +27,12 @@ import android.widget.ArrayAdapter;
|
||||
import android.widget.AutoCompleteTextView;
|
||||
import android.widget.Spinner;
|
||||
|
||||
import androidx.preference.DialogPreference;
|
||||
import androidx.preference.PreferenceDialogFragmentCompat;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.fragments.ChooseActivityTypeDialogFragment;
|
||||
import de.dennisguse.opentracks.util.HackUtils;
|
||||
import de.dennisguse.opentracks.util.PreferencesUtils;
|
||||
import de.dennisguse.opentracks.util.TrackIconUtils;
|
||||
|
||||
@@ -39,106 +43,118 @@ import de.dennisguse.opentracks.util.TrackIconUtils;
|
||||
*/
|
||||
public class ActivityTypePreference extends DialogPreference {
|
||||
|
||||
private RecordingSettingsActivity recordingSettingsActivity;
|
||||
private AutoCompleteTextView textView;
|
||||
private Spinner spinner;
|
||||
|
||||
public ActivityTypePreference(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
setDialogLayoutResource(R.layout.activity_type_preference);
|
||||
setDialogLayoutResource(R.layout.preference_activity_type);
|
||||
setPositiveButtonText(android.R.string.ok);
|
||||
setNegativeButtonText(android.R.string.cancel);
|
||||
setDialogIcon(null);
|
||||
setPersistent(false);
|
||||
setPersistent(true);
|
||||
|
||||
SummaryProvider<DialogPreference> summaryProvider = new SummaryProvider<DialogPreference>() {
|
||||
@Override
|
||||
public CharSequence provideSummary(DialogPreference preference) {
|
||||
return PreferencesUtils.getDefaultActivity(ActivityTypePreference.this.getContext());
|
||||
}
|
||||
};
|
||||
setSummaryProvider(summaryProvider);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected View onCreateDialogView() {
|
||||
View view = super.onCreateDialogView();
|
||||
textView = view.findViewById(R.id.activity_type_preference_text_view);
|
||||
spinner = view.findViewById(R.id.activity_type_preference_spinner);
|
||||
public int getDialogLayoutResource() {
|
||||
return R.layout.preference_activity_type;
|
||||
}
|
||||
|
||||
String category = PreferencesUtils.getString(
|
||||
getContext(), R.string.default_activity_key, PreferencesUtils.DEFAULT_ACTIVITY_DEFAULT);
|
||||
textView.setText(category);
|
||||
public static class ActivityPreferenceDialog extends PreferenceDialogFragmentCompat {
|
||||
|
||||
ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(getContext(), R.array.activity_types, android.R.layout.simple_dropdown_item_1line);
|
||||
textView.setAdapter(adapter);
|
||||
textView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
|
||||
@Override
|
||||
public void onItemClick(AdapterView<?> parent, View v, int position, long id) {
|
||||
String iconValue = TrackIconUtils.getIconValue(getContext(), (String) textView.getAdapter().getItem(position));
|
||||
TrackIconUtils.setIconSpinner(spinner, iconValue);
|
||||
}
|
||||
});
|
||||
textView.setOnFocusChangeListener(new View.OnFocusChangeListener() {
|
||||
@Override
|
||||
public void onFocusChange(View v, boolean hasFocus) {
|
||||
if (!hasFocus) {
|
||||
String iconValue = TrackIconUtils.getIconValue(getContext(), textView.getText().toString());
|
||||
private AutoCompleteTextView textView;
|
||||
private Spinner spinner;
|
||||
|
||||
static ActivityPreferenceDialog newInstance(String preferenceKey) {
|
||||
ActivityTypePreference.ActivityPreferenceDialog dialog = new ActivityTypePreference.ActivityPreferenceDialog();
|
||||
final Bundle bundle = new Bundle(1);
|
||||
bundle.putString(PreferenceDialogFragmentCompat.ARG_KEY, preferenceKey);
|
||||
dialog.setArguments(bundle);
|
||||
|
||||
return dialog;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onBindDialogView(View view) {
|
||||
super.onBindDialogView(view);
|
||||
|
||||
final Context context = getActivity();
|
||||
|
||||
textView = view.findViewById(R.id.activity_type_preference_text_view);
|
||||
String category = PreferencesUtils.getDefaultActivity(context);
|
||||
textView.setText(category);
|
||||
ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(context, R.array.activity_types, android.R.layout.simple_dropdown_item_1line);
|
||||
textView.setAdapter(adapter);
|
||||
textView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
|
||||
@Override
|
||||
public void onItemClick(AdapterView<?> parent, View v, int position, long id) {
|
||||
String iconValue = TrackIconUtils.getIconValue(context, (String) textView.getAdapter().getItem(position));
|
||||
TrackIconUtils.setIconSpinner(spinner, iconValue);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
String iconValue = TrackIconUtils.getIconValue(getContext(), category);
|
||||
spinner.setAdapter(TrackIconUtils.getIconSpinnerAdapter(getContext(), iconValue));
|
||||
spinner.setOnTouchListener(new View.OnTouchListener() {
|
||||
@Override
|
||||
public boolean onTouch(View v, MotionEvent event) {
|
||||
if (event.getAction() == MotionEvent.ACTION_UP) {
|
||||
recordingSettingsActivity.showChooseActivityTypeDialog();
|
||||
});
|
||||
textView.setOnFocusChangeListener(new View.OnFocusChangeListener() {
|
||||
@Override
|
||||
public void onFocusChange(View v, boolean hasFocus) {
|
||||
if (!hasFocus) {
|
||||
String iconValue = TrackIconUtils.getIconValue(context, textView.getText().toString());
|
||||
TrackIconUtils.setIconSpinner(spinner, iconValue);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
});
|
||||
spinner.setOnKeyListener(new View.OnKeyListener() {
|
||||
@Override
|
||||
public boolean onKey(View v, int keyCode, KeyEvent event) {
|
||||
if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) {
|
||||
recordingSettingsActivity.showChooseActivityTypeDialog();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
});
|
||||
return view;
|
||||
}
|
||||
});
|
||||
|
||||
@Override
|
||||
protected void onDialogClosed(boolean positiveResult) {
|
||||
if (positiveResult) {
|
||||
String value = textView.getText().toString();
|
||||
if (callChangeListener(value)) {
|
||||
PreferencesUtils.setString(getContext(), R.string.default_activity_key, value);
|
||||
String iconValue = TrackIconUtils.getIconValue(context, category);
|
||||
spinner = view.findViewById(R.id.activity_type_preference_spinner);
|
||||
spinner.setAdapter(TrackIconUtils.getIconSpinnerAdapter(context, iconValue));
|
||||
spinner.setOnTouchListener(new View.OnTouchListener() {
|
||||
@Override
|
||||
public boolean onTouch(View v, MotionEvent event) {
|
||||
if (event.getAction() == MotionEvent.ACTION_UP) {
|
||||
showIconSelectDialog();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
});
|
||||
spinner.setOnKeyListener(new View.OnKeyListener() {
|
||||
@Override
|
||||
public boolean onKey(View v, int keyCode, KeyEvent event) {
|
||||
if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) {
|
||||
showIconSelectDialog();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void showIconSelectDialog() {
|
||||
String category = PreferencesUtils.getDefaultActivity(getActivity());
|
||||
ChooseActivityTypeDialogFragment.showDialog(getActivity().getSupportFragmentManager(), category);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDialogClosed(boolean positiveResult) {
|
||||
if (positiveResult) {
|
||||
String newDefaultActivity = textView.getText().toString();
|
||||
if (getPreference().callChangeListener(newDefaultActivity)) {
|
||||
PreferencesUtils.setDefaultActivity(getActivity(), newDefaultActivity);
|
||||
HackUtils.invalidatePreference(getPreference());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onRestoreInstanceState(Parcelable state) {
|
||||
super.onRestoreInstanceState(state);
|
||||
String iconValue = TrackIconUtils.getIconValue(getContext(), textView.getText().toString());
|
||||
TrackIconUtils.setIconSpinner(spinner, iconValue);
|
||||
textView.clearFocus();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the recording settings activity.
|
||||
*
|
||||
* @param activity the activity
|
||||
*/
|
||||
void setRecordingSettingsActivity(RecordingSettingsActivity activity) {
|
||||
recordingSettingsActivity = activity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the value of the dialog.
|
||||
*
|
||||
* @param iconValue the icon value
|
||||
*/
|
||||
void updateValue(String iconValue) {
|
||||
TrackIconUtils.setIconSpinner(spinner, iconValue);
|
||||
textView.setText(recordingSettingsActivity.getString(TrackIconUtils.getIconActivityType(iconValue)));
|
||||
textView.clearFocus();
|
||||
/**
|
||||
* Updates the value of the dialog.
|
||||
*
|
||||
* @param iconValue the icon value
|
||||
*/
|
||||
public void updateUI(String iconValue) {
|
||||
TrackIconUtils.setIconSpinner(spinner, iconValue);
|
||||
textView.setText(getActivity().getString(TrackIconUtils.getIconActivityType(iconValue)));
|
||||
textView.clearFocus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 (!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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
package de.dennisguse.opentracks.settings;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.util.PreferencesUtils;
|
||||
import de.dennisguse.opentracks.util.UnitConversions;
|
||||
|
||||
final class PreferenceHelper {
|
||||
|
||||
static String[] getMinRecordingIntervalEntries(Context context) {
|
||||
String[] entryValues = context.getResources().getStringArray(R.array.min_recording_interval_values);
|
||||
String[] entries = new String[entryValues.length];
|
||||
for (int i = 0; i < entryValues.length; i++) {
|
||||
int value = Integer.parseInt(entryValues[i]);
|
||||
|
||||
if (value == PreferencesUtils.getMinRecordingIntervalAdaptAccuracy(context)) {
|
||||
entries[i] = context.getString(R.string.value_adapt_accuracy);
|
||||
} else if (value == PreferencesUtils.getMinRecordingIntervalAdaptBatteryLife(context)) {
|
||||
entries[i] = context.getString(R.string.value_adapt_battery_life);
|
||||
} else if (value == PreferencesUtils.getMinRecordingIntervalDefault(context)) {
|
||||
entries[i] = context.getString(R.string.value_smallest_recommended);
|
||||
} else {
|
||||
entries[i] = value < 60 ? context.getString(R.string.value_integer_second, value) : context.getString(R.string.value_integer_minute, value / 60);
|
||||
}
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
static String[] getRecordingDistanceIntervalEntries(Context context, boolean metricUnits) {
|
||||
String[] entryValues = context.getResources().getStringArray(R.array.recording_distance_interval_values);
|
||||
String[] entries = new String[entryValues.length];
|
||||
|
||||
final int recordingDistanceIntervalDefault = PreferencesUtils.getRecordingDistanceIntervalDefault(context);
|
||||
|
||||
for (int i = 0; i < entryValues.length; i++) {
|
||||
int value = Integer.parseInt(entryValues[i]);
|
||||
String displayValue;
|
||||
if (metricUnits) {
|
||||
displayValue = context.getString(R.string.value_integer_meter, value);
|
||||
if (value == recordingDistanceIntervalDefault) {
|
||||
entries[i] = context.getString(R.string.value_integer_meter_recommended, value);
|
||||
} else {
|
||||
entries[i] = displayValue;
|
||||
}
|
||||
} else {
|
||||
int feet = (int) (value * UnitConversions.M_TO_FT);
|
||||
displayValue = context.getString(R.string.value_integer_feet, feet);
|
||||
if (value == recordingDistanceIntervalDefault) {
|
||||
entries[i] = context.getString(R.string.value_integer_feet_recommended, feet);
|
||||
} else {
|
||||
entries[i] = displayValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
static String[] getMaxRecordingDistanceEntries(Context context, boolean metricUnits) {
|
||||
String[] entryValues = context.getResources().getStringArray(R.array.max_recording_distance_values);
|
||||
String[] entries = new String[entryValues.length];
|
||||
|
||||
final int maxRecordingDistanceDefault = Integer.parseInt(context.getResources().getString(R.string.max_recording_distance_default));
|
||||
|
||||
for (int i = 0; i < entryValues.length; i++) {
|
||||
int value = Integer.parseInt(entryValues[i]);
|
||||
String displayValue;
|
||||
if (metricUnits) {
|
||||
displayValue = context.getString(R.string.value_integer_meter, value);
|
||||
if (value == maxRecordingDistanceDefault) {
|
||||
entries[i] = context.getString(R.string.value_integer_meter_recommended, value);
|
||||
} else {
|
||||
entries[i] = displayValue;
|
||||
}
|
||||
} else {
|
||||
int feet = (int) (value * UnitConversions.M_TO_FT);
|
||||
if (feet < 2000) {
|
||||
displayValue = context.getString(R.string.value_integer_feet, feet);
|
||||
if (value == maxRecordingDistanceDefault) {
|
||||
entries[i] = context.getString(R.string.value_integer_feet_recommended, feet);
|
||||
} else {
|
||||
entries[i] = displayValue;
|
||||
}
|
||||
} else {
|
||||
double mile = feet * UnitConversions.FT_TO_MI;
|
||||
displayValue = context.getString(R.string.value_float_mile, mile);
|
||||
entries[i] = displayValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
static String[] getRecordingGpsAccuracyEntries(Context context, boolean metricUnits) {
|
||||
String[] entryValues = context.getResources().getStringArray(R.array.recording_gps_accuracy_values);
|
||||
String[] entries = new String[entryValues.length];
|
||||
|
||||
final int recordingGPSAccuracyDefault = Integer.parseInt(context.getResources().getString(R.string.recording_gps_accuracy_default));
|
||||
final int recordingGPSAccuracyExcellent = Integer.parseInt(context.getResources().getString(R.string.recording_gps_accuracy_excellent));
|
||||
final int recordingGPSAccuracyPoor = Integer.parseInt(context.getResources().getString(R.string.recording_gps_accuracy_poor));
|
||||
|
||||
for (int i = 0; i < entryValues.length; i++) {
|
||||
int value = Integer.parseInt(entryValues[i]);
|
||||
String displayValue;
|
||||
if (metricUnits) {
|
||||
displayValue = context.getString(R.string.value_integer_meter, value);
|
||||
if (value == recordingGPSAccuracyDefault) {
|
||||
entries[i] = context.getString(R.string.value_integer_meter_recommended, value);
|
||||
} else if (value == recordingGPSAccuracyExcellent) {
|
||||
entries[i] = context.getString(R.string.value_integer_meter_excellent_gps, value);
|
||||
} else if (value == recordingGPSAccuracyPoor) {
|
||||
entries[i] = context.getString(R.string.value_integer_meter_poor_gps, value);
|
||||
} else {
|
||||
entries[i] = displayValue;
|
||||
}
|
||||
} else {
|
||||
int feet = (int) (value * UnitConversions.M_TO_FT);
|
||||
if (feet < 2000) {
|
||||
displayValue = context.getString(R.string.value_integer_feet, feet);
|
||||
|
||||
if (value == recordingGPSAccuracyDefault) {
|
||||
entries[i] = context.getString(R.string.value_integer_feet_recommended, feet);
|
||||
} else if (value == recordingGPSAccuracyExcellent) {
|
||||
entries[i] = context.getString(R.string.value_integer_feet_excellent_gps, feet);
|
||||
} else {
|
||||
entries[i] = displayValue;
|
||||
}
|
||||
} else {
|
||||
double mile = feet * UnitConversions.FT_TO_MI;
|
||||
displayValue = context.getString(R.string.value_float_mile, mile);
|
||||
if (value == recordingGPSAccuracyPoor) {
|
||||
entries[i] = context.getString(R.string.value_float_mile_poor_gps, mile);
|
||||
} else {
|
||||
entries[i] = displayValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
static String[] getAutoResumeTrackTimeoutEntries(Context context) {
|
||||
String[] entryValues = context.getResources().getStringArray(R.array.auto_resume_track_timeout_values);
|
||||
String[] entries = new String[entryValues.length];
|
||||
|
||||
for (int i = 0; i < entryValues.length; i++) {
|
||||
int value = Integer.parseInt(entryValues[i]);
|
||||
if (value == Integer.parseInt(context.getResources().getString(R.string.auto_resume_track_timeout_never))) {
|
||||
entries[i] = context.getString(R.string.value_never);
|
||||
} else if (value == Integer.parseInt(context.getResources().getString(R.string.auto_resume_track_timeout_always))) {
|
||||
entries[i] = context.getString(R.string.value_always);
|
||||
} else {
|
||||
entries[i] = context.getString(R.string.value_integer_minute, value);
|
||||
}
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
}
|
||||
@@ -1,303 +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.settings;
|
||||
|
||||
import android.app.Dialog;
|
||||
import android.os.Bundle;
|
||||
import android.preference.ListPreference;
|
||||
import android.preference.Preference;
|
||||
import android.preference.PreferenceActivity;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.fragments.ChooseActivityTypeDialogFragment;
|
||||
import de.dennisguse.opentracks.fragments.ChooseActivityTypeDialogFragment.ChooseActivityTypeCaller;
|
||||
import de.dennisguse.opentracks.util.PreferencesUtils;
|
||||
import de.dennisguse.opentracks.util.StringUtils;
|
||||
import de.dennisguse.opentracks.util.UnitConversions;
|
||||
|
||||
/**
|
||||
* An activity for accessing recording settings.
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
public class RecordingSettingsActivity extends PreferenceActivity implements ChooseActivityTypeCaller {
|
||||
|
||||
private static final int DIALOG_CHOOSE_ACTIVITY = 0;
|
||||
|
||||
private ActivityTypePreference activityTypePreference;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle bundle) {
|
||||
super.onCreate(bundle);
|
||||
addPreferencesFromResource(R.xml.settings_recording);
|
||||
|
||||
boolean metricUnits = PreferencesUtils.isMetricUnits(this);
|
||||
|
||||
configFrequencyPreference(R.string.voice_frequency_key, metricUnits);
|
||||
configFrequencyPreference(R.string.split_frequency_key, metricUnits);
|
||||
configDefaultActivity();
|
||||
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);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Dialog onCreateDialog(int id, Bundle bundle) {
|
||||
if (id != DIALOG_CHOOSE_ACTIVITY) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String category = PreferencesUtils.getString(
|
||||
this, R.string.default_activity_key, PreferencesUtils.DEFAULT_ACTIVITY_DEFAULT);
|
||||
return ChooseActivityTypeDialogFragment.getDialog(this, category, this);
|
||||
}
|
||||
|
||||
private void configFrequencyPreference(int key, boolean metricUnits) {
|
||||
ListPreference preference = (ListPreference) findPreference(getString(key));
|
||||
|
||||
String[] options = StringUtils.getFrequencyOptions(this, metricUnits);
|
||||
preference.setEntries(options);
|
||||
}
|
||||
|
||||
private void configDefaultActivity() {
|
||||
activityTypePreference = (ActivityTypePreference) findPreference(getString(R.string.default_activity_key));
|
||||
String defaultActivity = PreferencesUtils.getString(
|
||||
this, R.string.default_activity_key, PreferencesUtils.DEFAULT_ACTIVITY_DEFAULT);
|
||||
activityTypePreference.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
|
||||
@Override
|
||||
public boolean onPreferenceChange(Preference pref, Object newValue) {
|
||||
String stringValue = (String) newValue;
|
||||
pref.setSummary(stringValue != null && !stringValue.equals(PreferencesUtils.DEFAULT_ACTIVITY_DEFAULT) ? stringValue : getString(R.string.value_unknown));
|
||||
return true;
|
||||
}
|
||||
});
|
||||
activityTypePreference.setSummary(defaultActivity != null && !defaultActivity.equals(PreferencesUtils.DEFAULT_ACTIVITY_DEFAULT) ? defaultActivity : getString(R.string.value_unknown));
|
||||
activityTypePreference.setRecordingSettingsActivity(this);
|
||||
}
|
||||
|
||||
private void configListPreference(int key, int valueArray, boolean metricUnits) {
|
||||
ListPreference preference = (ListPreference) findPreference(getString(key));
|
||||
|
||||
@Deprecated //TODO Can we make values an int array?
|
||||
String[] values = getResources().getStringArray(valueArray);
|
||||
String[] options = new String[values.length];
|
||||
switch (key) {
|
||||
case R.string.min_recording_interval_key:
|
||||
setMinRecordingIntervalOptions(options, values);
|
||||
break;
|
||||
case R.string.recording_distance_interval_key:
|
||||
setRecordingDistanceIntervalOptions(options, values, metricUnits);
|
||||
break;
|
||||
case R.string.max_recording_distance_key:
|
||||
setMaxRecordingDistanceOptions(options, values, metricUnits);
|
||||
break;
|
||||
case R.string.recording_gps_accuracy_key:
|
||||
setRecordingGpsAccuracyOptions(options, values, metricUnits);
|
||||
break;
|
||||
case R.string.auto_resume_track_timeout_key:
|
||||
setAutoResumeTrackTimeoutOptions(options, values);
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
preference.setEntries(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the min recording interval options.
|
||||
*
|
||||
* @param options the options
|
||||
* @param values the values
|
||||
*/
|
||||
private void setMinRecordingIntervalOptions(String[] options, String[] values) {
|
||||
for (int i = 0; i < values.length; i++) {
|
||||
int value = Integer.parseInt(values[i]);
|
||||
switch (value) {
|
||||
case PreferencesUtils.MIN_RECORDING_INTERVAL_ADAPT_BATTERY_LIFE:
|
||||
options[i] = getString(R.string.value_adapt_battery_life);
|
||||
break;
|
||||
case PreferencesUtils.MIN_RECORDING_INTERVAL_ADAPT_ACCURACY:
|
||||
options[i] = getString(R.string.value_adapt_accuracy);
|
||||
break;
|
||||
case PreferencesUtils.MIN_RECORDING_INTERVAL_DEFAULT:
|
||||
options[i] = getString(R.string.value_smallest_recommended);
|
||||
break;
|
||||
default:
|
||||
options[i] = value < 60 ? getString(R.string.value_integer_second, value)
|
||||
: getString(R.string.value_integer_minute, value / 60);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the recording distance interval options.
|
||||
*
|
||||
* @param options the options
|
||||
* @param values the values
|
||||
* @param metricUnits true for metric units
|
||||
*/
|
||||
private void setRecordingDistanceIntervalOptions(String[] options, String[] values, boolean metricUnits) {
|
||||
for (int i = 0; i < values.length; i++) {
|
||||
int value = Integer.parseInt(values[i]);
|
||||
String displayValue;
|
||||
if (metricUnits) {
|
||||
displayValue = getString(R.string.value_integer_meter, value);
|
||||
if (value == PreferencesUtils.RECORDING_DISTANCE_INTERVAL_DEFAULT) {
|
||||
options[i] = getString(R.string.value_integer_meter_recommended, value);
|
||||
} else {
|
||||
options[i] = displayValue;
|
||||
}
|
||||
} else {
|
||||
int feet = (int) (value * UnitConversions.M_TO_FT);
|
||||
displayValue = getString(R.string.value_integer_feet, feet);
|
||||
if (value == PreferencesUtils.RECORDING_DISTANCE_INTERVAL_DEFAULT) {
|
||||
options[i] = getString(R.string.value_integer_feet_recommended, feet);
|
||||
} else {
|
||||
options[i] = displayValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the max recording distance options.
|
||||
*
|
||||
* @param options the options
|
||||
* @param values the values
|
||||
* @param metricUnits true for metric units
|
||||
*/
|
||||
private void setMaxRecordingDistanceOptions(String[] options, String[] values, boolean metricUnits) {
|
||||
for (int i = 0; i < values.length; i++) {
|
||||
int value = Integer.parseInt(values[i]);
|
||||
String displayValue;
|
||||
if (metricUnits) {
|
||||
displayValue = getString(R.string.value_integer_meter, value);
|
||||
if (value == PreferencesUtils.MAX_RECORDING_DISTANCE_DEFAULT) {
|
||||
options[i] = getString(R.string.value_integer_meter_recommended, value);
|
||||
} else {
|
||||
options[i] = displayValue;
|
||||
}
|
||||
} else {
|
||||
int feet = (int) (value * UnitConversions.M_TO_FT);
|
||||
if (feet < 2000) {
|
||||
displayValue = getString(R.string.value_integer_feet, feet);
|
||||
if (value == PreferencesUtils.MAX_RECORDING_DISTANCE_DEFAULT) {
|
||||
options[i] = getString(R.string.value_integer_feet_recommended, feet);
|
||||
} else {
|
||||
options[i] = displayValue;
|
||||
}
|
||||
} else {
|
||||
double mile = feet * UnitConversions.FT_TO_MI;
|
||||
displayValue = getString(R.string.value_float_mile, mile);
|
||||
options[i] = displayValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the recording gps accuracy options.
|
||||
*
|
||||
* @param options the options
|
||||
* @param values the values
|
||||
* @param metricUnits true for metric units
|
||||
*/
|
||||
private void setRecordingGpsAccuracyOptions(String[] options, String[] values, boolean metricUnits) {
|
||||
for (int i = 0; i < values.length; i++) {
|
||||
int value = Integer.parseInt(values[i]);
|
||||
String displayValue;
|
||||
if (metricUnits) {
|
||||
displayValue = getString(R.string.value_integer_meter, value);
|
||||
switch (value) {
|
||||
case PreferencesUtils.RECORDING_GPS_ACCURACY_DEFAULT:
|
||||
options[i] = getString(R.string.value_integer_meter_recommended, value);
|
||||
break;
|
||||
case PreferencesUtils.RECORDING_GPS_ACCURACY_EXCELLENT:
|
||||
options[i] = getString(R.string.value_integer_meter_excellent_gps, value);
|
||||
break;
|
||||
case PreferencesUtils.RECORDING_GPS_ACCURACY_POOR:
|
||||
options[i] = getString(R.string.value_integer_meter_poor_gps, value);
|
||||
break;
|
||||
default:
|
||||
options[i] = displayValue;
|
||||
}
|
||||
} else {
|
||||
int feet = (int) (value * UnitConversions.M_TO_FT);
|
||||
if (feet < 2000) {
|
||||
displayValue = getString(R.string.value_integer_feet, feet);
|
||||
switch (value) {
|
||||
case PreferencesUtils.RECORDING_GPS_ACCURACY_DEFAULT:
|
||||
options[i] = getString(R.string.value_integer_feet_recommended, feet);
|
||||
break;
|
||||
case PreferencesUtils.RECORDING_GPS_ACCURACY_EXCELLENT:
|
||||
options[i] = getString(R.string.value_integer_feet_excellent_gps, feet);
|
||||
break;
|
||||
default:
|
||||
options[i] = displayValue;
|
||||
}
|
||||
} else {
|
||||
double mile = feet * UnitConversions.FT_TO_MI;
|
||||
displayValue = getString(R.string.value_float_mile, mile);
|
||||
if (value == PreferencesUtils.RECORDING_GPS_ACCURACY_POOR) {
|
||||
options[i] = getString(R.string.value_float_mile_poor_gps, mile);
|
||||
} else {
|
||||
options[i] = displayValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the auto resume track timeout options.
|
||||
*
|
||||
* @param options the options
|
||||
* @param values the values
|
||||
*/
|
||||
private void setAutoResumeTrackTimeoutOptions(String[] options, String[] values) {
|
||||
for (int i = 0; i < values.length; i++) {
|
||||
int value = Integer.parseInt(values[i]);
|
||||
switch (value) {
|
||||
case PreferencesUtils.AUTO_RESUME_TRACK_TIMEOUT_NEVER:
|
||||
options[i] = getString(R.string.value_never);
|
||||
break;
|
||||
case PreferencesUtils.AUTO_RESUME_TRACK_TIMEOUT_ALWAYS:
|
||||
options[i] = getString(R.string.value_always);
|
||||
break;
|
||||
default:
|
||||
options[i] = getString(R.string.value_integer_minute, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void showChooseActivityTypeDialog() {
|
||||
try {
|
||||
removeDialog(DIALOG_CHOOSE_ACTIVITY);
|
||||
} catch (Exception e) {
|
||||
// Can safely ignore.
|
||||
}
|
||||
showDialog(DIALOG_CHOOSE_ACTIVITY);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChooseActivityTypeDone(String iconValue) {
|
||||
activityTypePreference.updateValue(iconValue);
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,14 @@
|
||||
package de.dennisguse.opentracks.settings;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.preference.DialogPreference;
|
||||
import android.os.Bundle;
|
||||
import android.util.AttributeSet;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
import androidx.preference.DialogPreference;
|
||||
import androidx.preference.PreferenceDialogFragmentCompat;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.util.PreferencesUtils;
|
||||
|
||||
@@ -15,13 +18,33 @@ public class ResetDialogPreference extends DialogPreference {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
super.onClick(dialog, which);
|
||||
if (which == DialogInterface.BUTTON_POSITIVE) {
|
||||
PreferencesUtils.resetPreferences(getContext(), true);
|
||||
interface ResetCallback {
|
||||
void onReset();
|
||||
}
|
||||
|
||||
Toast.makeText(getContext(), R.string.settings_reset_done, Toast.LENGTH_SHORT).show();
|
||||
public static class ResetPreferenceDialog extends PreferenceDialogFragmentCompat {
|
||||
|
||||
static PreferenceDialogFragmentCompat newInstance(String preferenceKey) {
|
||||
ResetPreferenceDialog dialog = new ResetPreferenceDialog();
|
||||
final Bundle bundle = new Bundle(1);
|
||||
bundle.putString(PreferenceDialogFragmentCompat.ARG_KEY, preferenceKey);
|
||||
dialog.setArguments(bundle);
|
||||
|
||||
return dialog;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDialogClosed(boolean positiveResult) {
|
||||
if (positiveResult) {
|
||||
FragmentActivity activity = getActivity();
|
||||
|
||||
PreferencesUtils.resetPreferences(activity, true);
|
||||
Toast.makeText(activity, R.string.settings_reset_done, Toast.LENGTH_SHORT).show();
|
||||
|
||||
if (activity instanceof ResetCallback) {
|
||||
((ResetCallback) activity).onReset();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,159 +1,195 @@
|
||||
/*
|
||||
* Copyright 2008 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
|
||||
package de.dennisguse.opentracks.settings;
|
||||
|
||||
import android.bluetooth.BluetoothAdapter;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Bundle;
|
||||
import android.preference.ListPreference;
|
||||
import android.preference.Preference;
|
||||
import android.preference.Preference.OnPreferenceClickListener;
|
||||
import android.preference.PreferenceActivity;
|
||||
import android.provider.Settings;
|
||||
import android.widget.Toast;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
import androidx.preference.ListPreference;
|
||||
import androidx.preference.Preference;
|
||||
import androidx.preference.PreferenceFragmentCompat;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.util.BluetoothDeviceUtils;
|
||||
import de.dennisguse.opentracks.util.IntentUtils;
|
||||
import de.dennisguse.opentracks.fragments.ChooseActivityTypeDialogFragment;
|
||||
import de.dennisguse.opentracks.util.BluetoothUtils;
|
||||
import de.dennisguse.opentracks.util.HackUtils;
|
||||
import de.dennisguse.opentracks.util.PreferencesUtils;
|
||||
import de.dennisguse.opentracks.util.StringUtils;
|
||||
|
||||
/**
|
||||
* An activity for accessing settings.
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
public class SettingsActivity extends PreferenceActivity {
|
||||
public class SettingsActivity extends AppCompatActivity implements ChooseActivityTypeDialogFragment.ChooseActivityTypeCaller, ResetDialogPreference.ResetCallback {
|
||||
|
||||
private long recordingTrackId = PreferencesUtils.RECORDING_TRACK_ID_DEFAULT;
|
||||
private static final String TAG = SettingsActivity.class.getSimpleName();
|
||||
|
||||
/*
|
||||
* Note that sharedPreferenceChangeListenr cannot be an anonymous inner class.
|
||||
* Anonymous inner class will get garbage collected.
|
||||
*/
|
||||
private final SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener = new SharedPreferences.OnSharedPreferenceChangeListener() {
|
||||
@Override
|
||||
public void onSharedPreferenceChanged(SharedPreferences preferences, String key) {
|
||||
if (key != null && key.equals(PreferencesUtils.getKey(SettingsActivity.this, R.string.recording_track_id_key))) {
|
||||
recordingTrackId = PreferencesUtils.getLong(SettingsActivity.this, R.string.recording_track_id_key);
|
||||
}
|
||||
|
||||
//TODO Should only be called if something meaningful happens.
|
||||
runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
updateUI();
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
private PrefsFragment prefsFragment;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle bundle) {
|
||||
super.onCreate(bundle);
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
addPreferencesFromResource(R.xml.settings);
|
||||
setContentView(R.layout.settings);
|
||||
|
||||
configPreference(R.string.settings_stats_key, StatsSettingsActivity.class);
|
||||
configPreference(R.string.settings_recording_key, RecordingSettingsActivity.class);
|
||||
Toolbar toolbar = findViewById(R.id.toolbar);
|
||||
toolbar.setTitle(R.string.menu_settings);
|
||||
setSupportActionBar(toolbar);
|
||||
|
||||
findPreference(getString(R.string.settings_sensor_bluetooth_pairing_key))
|
||||
.setOnPreferenceClickListener(new OnPreferenceClickListener() {
|
||||
public boolean onPreferenceClick(Preference preference) {
|
||||
Intent settingsIntent = new Intent(Settings.ACTION_BLUETOOTH_SETTINGS);
|
||||
startActivity(settingsIntent);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
onReset();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
PreferencesUtils.getSharedPreferences(this).registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
|
||||
recordingTrackId = PreferencesUtils.getLong(this, R.string.recording_track_id_key);
|
||||
updateUI();
|
||||
|
||||
configBluetoothSensor();
|
||||
public void onChooseActivityTypeDone(String iconValue) {
|
||||
prefsFragment.setDefaultActivity(iconValue);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPause() {
|
||||
super.onPause();
|
||||
PreferencesUtils.getSharedPreferences(this).unregisterOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
|
||||
public void onReset() {
|
||||
prefsFragment = new PrefsFragment();
|
||||
getSupportFragmentManager().beginTransaction().replace(R.id.settings_fragment, prefsFragment).commit();
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures a preference by starting a new activity when it is clicked.
|
||||
*
|
||||
* @param key the preference key
|
||||
* @param cl the class to start the new activity
|
||||
*/
|
||||
private void configPreference(int key, final Class<?> cl) {
|
||||
Preference preference = findPreference(getString(key));
|
||||
preference.setOnPreferenceClickListener(new OnPreferenceClickListener() {
|
||||
public static class PrefsFragment extends PreferenceFragmentCompat {
|
||||
|
||||
/*
|
||||
* Note that sharedPreferenceChangeListener cannot be an anonymous inner class.
|
||||
* Anonymous inner class will get garbage collected.
|
||||
*/
|
||||
private final SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener = new SharedPreferences.OnSharedPreferenceChangeListener() {
|
||||
@Override
|
||||
public boolean onPreferenceClick(Preference pref) {
|
||||
Intent intent = IntentUtils.newIntent(SettingsActivity.this, cl);
|
||||
startActivity(intent);
|
||||
return true;
|
||||
public void onSharedPreferenceChanged(SharedPreferences preferences, String key) {
|
||||
if (PreferencesUtils.isKey(getActivity(), R.string.recording_track_id_key, key)) {
|
||||
getActivity().runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
updateReset();
|
||||
}
|
||||
});
|
||||
}
|
||||
if (PreferencesUtils.isKey(getActivity(), R.string.stats_units_key, key)) {
|
||||
getActivity().runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
updateUnits();
|
||||
}
|
||||
});
|
||||
}
|
||||
if (PreferencesUtils.isKey(getActivity(), R.string.stats_rate_key, key)) {
|
||||
getActivity().runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
updateSpeed();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Configures the bluetooth sensor.
|
||||
*/
|
||||
private void configBluetoothSensor() {
|
||||
ListPreference preference = (ListPreference) findPreference(getString(R.string.bluetooth_sensor_key));
|
||||
String value = PreferencesUtils.getString(this, 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);
|
||||
// Used to forward update from ChooseActivityTypeDialogFragment; TODO Could be replaced with LiveData.
|
||||
private ActivityTypePreference.ActivityPreferenceDialog activityPreferenceDialog;
|
||||
|
||||
@Override
|
||||
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
|
||||
try {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
// Was the previously configured device unpaired? Then forget it.
|
||||
if (!devicesAddressList.contains(value)) {
|
||||
value = PreferencesUtils.BLUETOOTH_SENSOR_DEFAULT;
|
||||
PreferencesUtils.setString(this, R.string.bluetooth_sensor_key, value);
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
PreferencesUtils.getSharedPreferences(getActivity()).registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
|
||||
updateSpeed();
|
||||
updateUnits();
|
||||
updateReset();
|
||||
updateBluetooth();
|
||||
}
|
||||
|
||||
devicesNameList.add(0, getString(R.string.value_none));
|
||||
devicesAddressList.add(0, PreferencesUtils.BLUETOOTH_SENSOR_DEFAULT);
|
||||
@Override
|
||||
public void onPause() {
|
||||
super.onPause();
|
||||
PreferencesUtils.getSharedPreferences(getActivity()).unregisterOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
|
||||
}
|
||||
|
||||
String[] values = devicesAddressList.toArray(new String[0]);
|
||||
preference.setEntryValues(values);
|
||||
@Override
|
||||
public void onDisplayPreferenceDialog(Preference preference) {
|
||||
DialogFragment dialogFragment = null;
|
||||
if (preference instanceof ResetDialogPreference) {
|
||||
dialogFragment = ResetDialogPreference.ResetPreferenceDialog.newInstance(preference.getKey());
|
||||
} else if (preference instanceof ActivityTypePreference) {
|
||||
activityPreferenceDialog = ActivityTypePreference.ActivityPreferenceDialog.newInstance(preference.getKey());
|
||||
dialogFragment = activityPreferenceDialog;
|
||||
} else if (preference instanceof BluetoothLePreference) {
|
||||
dialogFragment = BluetoothLePreference.BluetoothLePreferenceDialog.newInstance(preference.getKey());
|
||||
}
|
||||
|
||||
String[] options = devicesNameList.toArray(new String[0]);
|
||||
preference.setEntries(options);
|
||||
}
|
||||
if (dialogFragment != null) {
|
||||
dialogFragment.setTargetFragment(this, 0);
|
||||
dialogFragment.show(getFragmentManager(), getClass().getSimpleName());
|
||||
return;
|
||||
}
|
||||
|
||||
private void updateUI() {
|
||||
//TODO Remove the following if recordingTrackId is replaced by direct communication rather than via preferences.
|
||||
boolean isRecording = recordingTrackId != PreferencesUtils.RECORDING_TRACK_ID_DEFAULT;
|
||||
Preference resetPreference = findPreference(getString(R.string.settings_reset_key));
|
||||
resetPreference.setSummary(isRecording ? getString(R.string.settings_not_while_recording) : "");
|
||||
resetPreference.setEnabled(!isRecording);
|
||||
super.onDisplayPreferenceDialog(preference);
|
||||
}
|
||||
|
||||
Preference speedCheckBoxPreference = findPreference(getString(R.string.chart_show_speed_key));
|
||||
speedCheckBoxPreference.setTitle(PreferencesUtils.isReportSpeed(this) ? R.string.stats_speed : R.string.stats_pace);
|
||||
public void setDefaultActivity(String iconValue) {
|
||||
if (activityPreferenceDialog != null) {
|
||||
activityPreferenceDialog.updateUI(iconValue);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateReset() {
|
||||
final boolean isRecording = PreferencesUtils.isRecording(getActivity());
|
||||
Preference resetPreference = findPreference(getString(R.string.settings_reset_key));
|
||||
resetPreference.setSummary(isRecording ? getString(R.string.settings_not_while_recording) : "");
|
||||
resetPreference.setEnabled(!isRecording);
|
||||
}
|
||||
|
||||
private void updateSpeed() {
|
||||
Preference speedCheckBoxPreference = findPreference(getString(R.string.chart_show_speed_key));
|
||||
speedCheckBoxPreference.setTitle(PreferencesUtils.isReportSpeed(getActivity()) ? R.string.stats_speed : R.string.stats_pace);
|
||||
}
|
||||
|
||||
private void updateBluetooth() {
|
||||
// Disable Bluetooth preference if device does not have Bluetooth
|
||||
BluetoothLePreference bluetoothPreference = findPreference(getString(R.string.settings_sensor_bluetooth_heart_rate_key));
|
||||
bluetoothPreference.setVisible(BluetoothUtils.hasBluetooth(TAG));
|
||||
}
|
||||
|
||||
private void updateUnits() {
|
||||
boolean metricUnits = PreferencesUtils.isMetricUnits(getActivity());
|
||||
|
||||
ListPreference voiceFrequency = findPreference(getString(R.string.voice_frequency_key));
|
||||
voiceFrequency.setEntries(StringUtils.getFrequencyOptions(getActivity(), metricUnits));
|
||||
|
||||
ListPreference splitFrequency = findPreference(getString(R.string.split_frequency_key));
|
||||
splitFrequency.setEntries(StringUtils.getFrequencyOptions(getActivity(), metricUnits));
|
||||
|
||||
ListPreference minRecordingInterval = findPreference(getString(R.string.min_recording_interval_key));
|
||||
minRecordingInterval.setEntries(PreferenceHelper.getMinRecordingIntervalEntries(getActivity()));
|
||||
|
||||
ListPreference recordingDistanceInterval = findPreference(getString(R.string.recording_distance_interval_key));
|
||||
recordingDistanceInterval.setEntries(PreferenceHelper.getRecordingDistanceIntervalEntries(getActivity(), metricUnits));
|
||||
|
||||
ListPreference maxRecordingDistance = findPreference(getString(R.string.max_recording_distance_key));
|
||||
maxRecordingDistance.setEntries(PreferenceHelper.getMaxRecordingDistanceEntries(getActivity(), metricUnits));
|
||||
|
||||
ListPreference recordingGpsAccuracy = findPreference(getString(R.string.recording_gps_accuracy_key));
|
||||
recordingGpsAccuracy.setEntries(PreferenceHelper.getRecordingGpsAccuracyEntries(getActivity(), metricUnits));
|
||||
|
||||
ListPreference 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,78 +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.settings;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.preference.ListPreference;
|
||||
import android.preference.Preference;
|
||||
import android.preference.Preference.OnPreferenceChangeListener;
|
||||
import android.preference.PreferenceActivity;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.util.PreferencesUtils;
|
||||
|
||||
/**
|
||||
* An activity for accessing stats settings.
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
public class StatsSettingsActivity extends PreferenceActivity {
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle bundle) {
|
||||
super.onCreate(bundle);
|
||||
addPreferencesFromResource(R.xml.settings_statistics);
|
||||
|
||||
configUnitsListPreference();
|
||||
boolean metricUnits = PreferencesUtils.STATS_UNITS_DEFAULT.equals(PreferencesUtils.getString(this, R.string.stats_units_key, ""));
|
||||
configRateListPreference(metricUnits);
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the preferred units list preference.
|
||||
*/
|
||||
private void configUnitsListPreference() {
|
||||
ListPreference listPreference = (ListPreference) findPreference(getString(R.string.stats_units_key));
|
||||
final OnPreferenceChangeListener listener = new OnPreferenceChangeListener() {
|
||||
|
||||
@Override
|
||||
public boolean onPreferenceChange(Preference pref, Object newValue) {
|
||||
boolean metricUnits = PreferencesUtils.STATS_UNITS_DEFAULT.equals(newValue);
|
||||
configRateListPreference(metricUnits);
|
||||
return true;
|
||||
}
|
||||
};
|
||||
listPreference.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
|
||||
@Override
|
||||
public boolean onPreferenceChange(Preference pref, Object newValue) {
|
||||
return listener.onPreferenceChange(pref, newValue);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the preferred rate list preference.
|
||||
*
|
||||
* @param metricUnits true if metric units
|
||||
*/
|
||||
private void configRateListPreference(boolean metricUnits) {
|
||||
ListPreference listPreference = (ListPreference) findPreference(getString(R.string.stats_rate_key));
|
||||
String[] options = getResources().getStringArray(metricUnits ? R.array.stats_rate_metric_options : R.array.stats_rate_imperial_options);
|
||||
|
||||
listPreference.setEntries(options);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
package de.dennisguse.opentracks.util;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.SearchManager;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
import android.util.SparseBooleanArray;
|
||||
import android.view.ActionMode;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.widget.AbsListView;
|
||||
import android.widget.ListView;
|
||||
|
||||
import androidx.appcompat.widget.SearchView;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
import de.dennisguse.opentracks.ContextualActionModeCallback;
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.SearchListActivity;
|
||||
import de.dennisguse.opentracks.TrackController;
|
||||
|
||||
public class ActivityUtils {
|
||||
|
||||
private static final String TAG = ActivityUtils.class.getSimpleName();
|
||||
|
||||
public static void configureListViewContextualMenu(final ListView listView, final ContextualActionModeCallback contextualActionModeCallback) {
|
||||
listView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE_MODAL);
|
||||
listView.setMultiChoiceModeListener(new AbsListView.MultiChoiceModeListener() {
|
||||
|
||||
@Override
|
||||
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
|
||||
mode.getMenuInflater().inflate(R.menu.list_context_menu, menu);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
|
||||
contextualActionModeCallback.onPrepare(menu, getCheckedPositions(listView), listView.getCheckedItemIds(), true);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyActionMode(ActionMode mode) {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemCheckedStateChanged(ActionMode mode, int position, long id, boolean checked) {
|
||||
mode.invalidate();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
|
||||
if (contextualActionModeCallback.onClick(item.getItemId(), getCheckedPositions(listView), listView.getCheckedItemIds())) {
|
||||
mode.finish();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the checked positions in a list view.
|
||||
*
|
||||
* @param list the list view
|
||||
*/
|
||||
private int[] getCheckedPositions(ListView list) {
|
||||
SparseBooleanArray positions = list.getCheckedItemPositions();
|
||||
ArrayList<Integer> arrayList = new ArrayList<>();
|
||||
for (int i = 0; i < positions.size(); i++) {
|
||||
int key = positions.keyAt(i);
|
||||
if (positions.valueAt(i)) {
|
||||
arrayList.add(key);
|
||||
}
|
||||
}
|
||||
int[] result = new int[arrayList.size()];
|
||||
for (int i = 0; i < arrayList.size(); i++) {
|
||||
result[i] = arrayList.get(i);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static void configureSearchWidget(Activity activity, final MenuItem menuItem, final TrackController trackController) {
|
||||
final SearchView searchView = (SearchView) menuItem.getActionView();
|
||||
SearchManager searchManager = (SearchManager) activity.getSystemService(Context.SEARCH_SERVICE);
|
||||
if (searchManager != null) {
|
||||
//NOTE: for some reason activity.getComponentName() did not trigger the SearchListActivity
|
||||
searchView.setSearchableInfo(searchManager.getSearchableInfo(new ComponentName(activity, SearchListActivity.class)));
|
||||
} else {
|
||||
Log.w(TAG, "Could not retrieve SearchManager.");
|
||||
}
|
||||
searchView.setQueryRefinementEnabled(true);
|
||||
searchView.setSubmitButtonEnabled(true);
|
||||
searchView.setOnQueryTextFocusChangeListener(new View.OnFocusChangeListener() {
|
||||
|
||||
@Override
|
||||
public void onFocusChange(View v, boolean hasFocus) {
|
||||
// Hide and show trackController when searchable widget has focus/no focus
|
||||
if (trackController != null) {
|
||||
if (hasFocus) {
|
||||
trackController.hide();
|
||||
} else {
|
||||
trackController.show();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
|
||||
@Override
|
||||
public boolean onQueryTextSubmit(String query) {
|
||||
menuItem.collapseActionView();
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onQueryTextChange(String newText) {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
searchView.setOnSuggestionListener(new SearchView.OnSuggestionListener() {
|
||||
@Override
|
||||
public boolean onSuggestionSelect(int position) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onSuggestionClick(int position) {
|
||||
menuItem.collapseActionView();
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
@@ -70,18 +71,30 @@ public class IntentUtils {
|
||||
* @param trackIds the track ids
|
||||
*/
|
||||
public static Intent newShareFileIntent(Context context, long[] trackIds) {
|
||||
if (trackIds.length == 0) {
|
||||
throw new RuntimeException("Need to share at least one track.");
|
||||
}
|
||||
String trackDescription = "";
|
||||
if (trackIds.length == 1) {
|
||||
Track track = ContentProviderUtils.Factory.get(context).getTrack(trackIds[0]);
|
||||
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)
|
||||
.putExtra(Intent.EXTRA_STREAM, uriAndMime.first)
|
||||
String action = trackIds.length == 1 ? Intent.ACTION_SEND : Intent.ACTION_SEND_MULTIPLE;
|
||||
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)
|
||||
.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))
|
||||
.setType(uriAndMime.second)
|
||||
.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uris)
|
||||
.setType(mime)
|
||||
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||
}
|
||||
|
||||
|
||||
@@ -29,7 +29,6 @@ import de.dennisguse.opentracks.content.Track;
|
||||
*
|
||||
* @author Leif Hendrik Wilden
|
||||
*/
|
||||
//TODO Cleanup: rename variables
|
||||
public class LocationUtils {
|
||||
|
||||
private static final String TAG = LocationUtils.class.getSimpleName();
|
||||
|
||||
@@ -19,7 +19,9 @@ package de.dennisguse.opentracks.util;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.SharedPreferences.Editor;
|
||||
import android.preference.PreferenceManager;
|
||||
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
|
||||
@@ -30,67 +32,8 @@ import de.dennisguse.opentracks.R;
|
||||
*/
|
||||
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 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() {
|
||||
}
|
||||
|
||||
@@ -98,16 +41,60 @@ public class PreferencesUtils {
|
||||
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
|
||||
*
|
||||
* @param context the context
|
||||
* @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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
@@ -179,9 +166,9 @@ public class PreferencesUtils {
|
||||
* @param context the context
|
||||
* @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);
|
||||
return sharedPreferences.getLong(getKey(context, keyId), -1L);
|
||||
return sharedPreferences.getLong(getKey(context, keyId), defaultValue);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -230,7 +217,8 @@ public class PreferencesUtils {
|
||||
* @param context the 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));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -240,21 +228,146 @@ public class PreferencesUtils {
|
||||
* @param context the 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));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if chart x axis is by distance, false if by time.
|
||||
*
|
||||
* @param context the context
|
||||
*/
|
||||
public static int getAutoResumeTrackTimeout(Context context) {
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
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 isChartByDistance(Context context) {
|
||||
final String CHART_X_AXIS_DEFAULT = context.getString(R.string.chart_x_axis_default);
|
||||
return CHART_X_AXIS_DEFAULT.equals(getString(context, R.string.chart_x_axis_key, CHART_X_AXIS_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) {
|
||||
long recordingTrackId = PreferencesUtils.getRecordingTrackId(context);
|
||||
return recordingTrackId != PreferencesUtils.RECORDING_TRACK_ID_DEFAULT;
|
||||
}
|
||||
|
||||
public static boolean isRecording(long recordingTrackId) {
|
||||
return recordingTrackId != PreferencesUtils.RECORDING_TRACK_ID_DEFAULT;
|
||||
}
|
||||
|
||||
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_recording, readAgain);
|
||||
PreferenceManager.setDefaultValues(context, R.xml.settings_statistics, readAgain);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
package de.dennisguse.opentracks.util;
|
||||
|
||||
import android.app.ActivityManager;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import de.dennisguse.opentracks.services.TrackRecordingService;
|
||||
|
||||
public class ServiceUtils {
|
||||
|
||||
private ServiceUtils() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the recording service is running.
|
||||
*
|
||||
* @param context the current context
|
||||
*/
|
||||
@Deprecated
|
||||
public static boolean isTrackRecordingServiceRunning(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;
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,6 @@
|
||||
package de.dennisguse.opentracks.util;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.location.Location;
|
||||
import android.util.Pair;
|
||||
import android.view.View;
|
||||
@@ -26,7 +25,7 @@ import android.widget.TextView;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
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.stats.TripStatistics;
|
||||
|
||||
/**
|
||||
@@ -71,13 +70,13 @@ public class StatsUtils {
|
||||
}
|
||||
|
||||
// Set elevation
|
||||
boolean showGradeElevation = PreferencesUtils.getBoolean(activity, R.string.stats_show_grade_elevation_key, PreferencesUtils.STATS_SHOW_ELEVATION_DEFAULT) && isRecording;
|
||||
boolean showGradeElevation = isRecording && PreferencesUtils.isShowStatsGradeElevation(activity);
|
||||
View elevationContainer = activity.findViewById(R.id.stats_elevation);
|
||||
elevationContainer.setVisibility(showGradeElevation ? View.VISIBLE : View.GONE);
|
||||
|
||||
if (showGradeElevation) {
|
||||
double altitude = location != null && location.hasAltitude() ? location.getAltitude() : Double.NaN;
|
||||
Pair<String, String> parts = formatElevation(activity, altitude, metricUnits);
|
||||
Pair<String, String> parts = StringUtils.formatElevation(activity, altitude, metricUnits);
|
||||
|
||||
TextView elevationValue = activity.findViewById(R.id.stats_elevation_current_value);
|
||||
elevationValue.setText(parts.first);
|
||||
@@ -86,7 +85,7 @@ public class StatsUtils {
|
||||
}
|
||||
|
||||
// Set coordinate
|
||||
boolean showCoordinate = isRecording && PreferencesUtils.getBoolean(activity, R.string.stats_show_coordinate_key, PreferencesUtils.STATS_SHOW_COORDINATE_DEFAULT);
|
||||
boolean showCoordinate = isRecording && PreferencesUtils.isStatsShowCoordinate(activity);
|
||||
|
||||
View coordinateSeparator = activity.findViewById(R.id.stats_coordinate_separator);
|
||||
coordinateSeparator.setVisibility(showCoordinate ? View.VISIBLE : View.GONE);
|
||||
@@ -109,7 +108,7 @@ public class StatsUtils {
|
||||
public static void setSensorData(Activity activity, SensorDataSet sensorDataSet, boolean isRecording) {
|
||||
// heart rate
|
||||
int isVisible = View.VISIBLE;
|
||||
if (!isRecording || PreferencesUtils.BLUETOOTH_SENSOR_DEFAULT.equals(PreferencesUtils.getString(activity, R.string.bluetooth_sensor_key, PreferencesUtils.BLUETOOTH_SENSOR_DEFAULT))) {
|
||||
if (!isRecording || PreferencesUtils.isBluetoothHeartRateSensorAddressDefault(activity)) {
|
||||
isVisible = View.INVISIBLE;
|
||||
}
|
||||
activity.findViewById(R.id.stats_sensor_container).setVisibility(isVisible);
|
||||
@@ -119,16 +118,25 @@ public class StatsUtils {
|
||||
TextView heartRateSensor = activity.findViewById(R.id.stats_sensor_heart_rate_sensor_value);
|
||||
String heartRate = activity.getString(R.string.value_unknown);
|
||||
String sensor = activity.getString(R.string.value_unknown);
|
||||
if (sensorDataSet != null && sensorDataSet.hasHeartRate() && sensorDataSet.isRecent(RemoteSensorManager.MAX_SENSOR_DATE_SET_AGE)) {
|
||||
heartRate = StringUtils.formatDecimal(sensorDataSet.getHeartRate(), 0);
|
||||
if (sensorDataSet != null && sensorDataSet.isRecent(BluetoothRemoteSensorManager.MAX_SENSOR_DATE_SET_AGE_MS)) {
|
||||
sensor = sensorDataSet.getSensorName();
|
||||
if (sensorDataSet.hasHeartRate()) {
|
||||
heartRate = StringUtils.formatDecimal(sensorDataSet.getHeartRate(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
heartRateValue.setText(heartRate);
|
||||
setBluetoothHeartRateSensorName(activity, sensor, isRecording);
|
||||
heartRateSensor.setText(sensor);
|
||||
}
|
||||
}
|
||||
|
||||
public static void setBluetoothHeartRateSensorName(Activity activity, String sensorName, boolean isRecording) {
|
||||
if (isRecording) {
|
||||
TextView heartRateSensor = activity.findViewById(R.id.stats_sensor_heart_rate_sensor_value);
|
||||
heartRateSensor.setText(sensorName);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the total time value.
|
||||
*
|
||||
@@ -150,6 +158,7 @@ public class StatsUtils {
|
||||
public static void setTripStatisticsValues(Activity activity, TripStatistics tripStatistics, String trackIconValue) {
|
||||
boolean metricUnits = PreferencesUtils.isMetricUnits(activity);
|
||||
boolean reportSpeed = PreferencesUtils.isReportSpeed(activity);
|
||||
boolean isRecording = PreferencesUtils.isRecording(activity);
|
||||
|
||||
// Set total distance
|
||||
{
|
||||
@@ -172,6 +181,7 @@ public class StatsUtils {
|
||||
if (trackIconValue != null) {
|
||||
TrackIconUtils.setIconSpinner(spinner, trackIconValue);
|
||||
}
|
||||
spinner.setEnabled(isRecording);
|
||||
}
|
||||
|
||||
// Set time
|
||||
@@ -240,7 +250,7 @@ public class StatsUtils {
|
||||
|
||||
// Set elevation
|
||||
{
|
||||
boolean showElevation = PreferencesUtils.getBoolean(activity, R.string.stats_show_grade_elevation_key, PreferencesUtils.STATS_SHOW_ELEVATION_DEFAULT);
|
||||
boolean showElevation = PreferencesUtils.isShowStatsGradeElevation(activity);
|
||||
View gradeElevationSeparator = activity.findViewById(R.id.stats_elevation_separator);
|
||||
gradeElevationSeparator.setVisibility(showElevation ? View.VISIBLE : View.GONE);
|
||||
|
||||
@@ -250,7 +260,7 @@ public class StatsUtils {
|
||||
if (showElevation) {
|
||||
{
|
||||
double elevation = tripStatistics == null ? Double.NaN : tripStatistics.getMinElevation();
|
||||
Pair<String, String> parts = formatElevation(activity, elevation, metricUnits);
|
||||
Pair<String, String> parts = StringUtils.formatElevation(activity, elevation, metricUnits);
|
||||
|
||||
TextView elevationValue = activity.findViewById(R.id.stats_elevation_min_value);
|
||||
elevationValue.setText(parts.first);
|
||||
@@ -260,7 +270,7 @@ public class StatsUtils {
|
||||
|
||||
{
|
||||
double elevation = tripStatistics == null ? Double.NaN : tripStatistics.getMaxElevation();
|
||||
Pair<String, String> parts = formatElevation(activity, elevation, metricUnits);
|
||||
Pair<String, String> parts = StringUtils.formatElevation(activity, elevation, metricUnits);
|
||||
|
||||
TextView elevationValue = activity.findViewById(R.id.stats_elevation_max_value);
|
||||
elevationValue.setText(parts.first);
|
||||
@@ -270,26 +280,4 @@ public class StatsUtils {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets an elevation value.
|
||||
*
|
||||
* @param context the context
|
||||
* @param elevation the elevation in meters
|
||||
* @param metricUnits true if metric units
|
||||
* @return the formatted elevation (or null) and it's unit as {@link Pair}
|
||||
*/
|
||||
// TODO Move to StringUtils
|
||||
private static Pair<String, String> formatElevation(Context context, double elevation, boolean metricUnits) {
|
||||
String value = context.getString(R.string.value_unknown);
|
||||
String unit = context.getString(metricUnits ? R.string.unit_meter : R.string.unit_feet);
|
||||
if (!Double.isNaN(elevation) && !Double.isInfinite(elevation)) {
|
||||
if (metricUnits) {
|
||||
value = StringUtils.formatDecimal(elevation);
|
||||
} else {
|
||||
value = StringUtils.formatDecimal(elevation * UnitConversions.M_TO_FT);
|
||||
}
|
||||
}
|
||||
return new Pair<>(value, unit);
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,7 @@ import android.text.TextUtils;
|
||||
import android.text.format.DateUtils;
|
||||
import android.util.Pair;
|
||||
|
||||
import java.text.DecimalFormat;
|
||||
import java.text.ParsePosition;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
@@ -124,13 +125,20 @@ public class StringUtils {
|
||||
}
|
||||
}
|
||||
|
||||
static String formatDecimal(double value) {
|
||||
private static String formatDecimal(double value) {
|
||||
return StringUtils.formatDecimal(value, 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a decimal number while removing trailing zeros of the decimal part (if present).
|
||||
*/
|
||||
static String formatDecimal(double value, int decimalPlaces) {
|
||||
String result = String.format(Locale.getDefault(), "%1$,." + decimalPlaces + "f", value);
|
||||
return result.replaceAll("[0]*$", "").replaceAll("\\.$", "");
|
||||
if (decimalPlaces < 1) {
|
||||
return Long.toString(Math.round(value));
|
||||
}
|
||||
|
||||
String format = "#." + new String(new char[decimalPlaces]).replace("\0", "#");
|
||||
return new DecimalFormat(format).format(value);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -351,7 +359,7 @@ public class StringUtils {
|
||||
String[] options = new String[values.length];
|
||||
for (int i = 0; i < values.length; i++) {
|
||||
int value = Integer.parseInt(values[i]);
|
||||
if (value == PreferencesUtils.FREQUENCY_OFF) {
|
||||
if (context.getString(R.string.frequency_off).equals(values[i])) {
|
||||
options[i] = context.getString(R.string.value_off);
|
||||
} else if (value < 0) {
|
||||
options[i] = context.getString(metricUnits ? R.string.value_integer_kilometer : R.string.value_integer_mile, Math.abs(value));
|
||||
@@ -361,4 +369,25 @@ public class StringUtils {
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets an elevation value.
|
||||
*
|
||||
* @param context the context
|
||||
* @param elevation the elevation in meters
|
||||
* @param metricUnits true if metric units
|
||||
* @return the formatted elevation (or null) and it's unit as {@link Pair}
|
||||
*/
|
||||
public static Pair<String, String> formatElevation(Context context, double elevation, boolean metricUnits) {
|
||||
String value = context.getString(R.string.value_unknown);
|
||||
String unit = context.getString(metricUnits ? R.string.unit_meter : R.string.unit_feet);
|
||||
if (!Double.isNaN(elevation) && !Double.isInfinite(elevation)) {
|
||||
if (metricUnits) {
|
||||
value = StringUtils.formatDecimal(elevation);
|
||||
} else {
|
||||
value = StringUtils.formatDecimal(elevation * UnitConversions.M_TO_FT);
|
||||
}
|
||||
}
|
||||
return new Pair<>(value, unit);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ public class TrackIconUtils {
|
||||
private static final LinkedHashMap<String, Pair<Integer, Integer>> MAP = new LinkedHashMap<>();
|
||||
|
||||
static {
|
||||
//Reflects order in ChooseActivityTypeDialogFragment
|
||||
//Reflects order in ChooseActivityTypeDialogFragmentActivity
|
||||
MAP.put(UNKNOWN, new Pair<>(R.string.activity_type_unknown, ACTIVITY_UNKNOWN_LOGO));
|
||||
MAP.put(RUN, new Pair<>(R.string.activity_type_running, R.drawable.ic_activity_run_24dp));
|
||||
MAP.put(WALK, new Pair<>(R.string.activity_type_walking, R.drawable.ic_activity_walk_24dp));
|
||||
|
||||
@@ -47,7 +47,7 @@ public class TrackNameUtils {
|
||||
* @param startTime the track start time
|
||||
*/
|
||||
public static String getTrackName(Context context, long trackId, long startTime) {
|
||||
String trackName = PreferencesUtils.getString(context, R.string.track_name_key, PreferencesUtils.TRACK_NAME_DEFAULT);
|
||||
String trackName = PreferencesUtils.getString(context, R.string.track_name_key, context.getString(R.string.track_name_default));
|
||||
|
||||
if (trackName.equals(context.getString(R.string.settings_recording_track_name_date_local_value))) {
|
||||
return StringUtils.formatDateTime(context, startTime);
|
||||
|
||||
|
Before Width: | Height: | Size: 199 B |
|
Before Width: | Height: | Size: 154 B |
|
Before Width: | Height: | Size: 260 B |
@@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="#FFFFFF"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M18,16.08c-0.76,0 -1.44,0.3 -1.96,0.77L8.91,12.7c0.05,-0.23 0.09,-0.46 0.09,-0.7s-0.04,-0.47 -0.09,-0.7l7.05,-4.11c0.54,0.5 1.25,0.81 2.04,0.81 1.66,0 3,-1.34 3,-3s-1.34,-3 -3,-3 -3,1.34 -3,3c0,0.24 0.04,0.47 0.09,0.7L8.04,9.81C7.5,9.31 6.79,9 6,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3c0.79,0 1.5,-0.31 2.04,-0.81l7.12,4.16c-0.05,0.21 -0.08,0.43 -0.08,0.65 0,1.61 1.31,2.92 2.92,2.92 1.61,0 2.92,-1.31 2.92,-2.92s-1.31,-2.92 -2.92,-2.92z" />
|
||||
</vector>
|
||||
@@ -16,7 +16,10 @@ limitations under the License.
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:baselineAligned="false">
|
||||
android:baselineAligned="false"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<include layout="@layout/toolbar" />
|
||||
|
||||
<include layout="@layout/track_controller" />
|
||||
|
||||
|
||||