Compare commits

...

46 Commits

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

Fixed #41.
2019-10-26 14:11:55 +02:00
Dennis Guse e9d5718495 Cleanup. 2019-10-23 18:47:32 +02:00
Dennis Guse 0dd7f74a11 Removed temporary sensor manager (was not used anymore). 2019-10-22 21:15:11 +02:00
Dennis Guse 9bd4ba489c Release: v3.2.2 2019-10-21 23:00:13 +02:00
Dennis Guse dd55b50dad Re-added actionbar (actually toolbar) to SettingsActivity. 2019-10-21 22:54:23 +02:00
Dennis Guse 1719d21a5b Bugfix: export now really includes sensor data (as double) and import is done as float. 2019-10-21 22:34:33 +02:00
Dennis Guse 75ef8d99d4 SensorDataSet: data unavailable is now NaN. 2019-10-21 22:34:28 +02:00
Dennis Guse ae06fd1680 Bugfix: export sensor data if available and allowed. 2019-10-20 18:39:31 +02:00
Dennis Guse 2b03f7dea8 Use LinearLayout for SearchActivity (show toolbar correctly). 2019-10-20 17:05:09 +02:00
Dennis Guse 7591b012b4 Extracted ShareActionProvider setup code into a separate class. 2019-10-20 16:55:28 +02:00
Dennis Guse 0e089f49f3 Re-added application logo to material design toolbar. 2019-10-19 12:27:11 +02:00
Dennis Guse 5ad1856b13 Removed context menu from ListActivities. 2019-10-17 21:32:55 +02:00
Dennis Guse 965dbbe829 Removed custom action bar style. 2019-10-17 21:26:31 +02:00
Dennis Guse 262c9886df Removed aggregated stats activity. 2019-10-17 21:22:07 +02:00
Dennis Guse ac5a9ea65d Use Androidx compat's toolbar instead of actionbars.
ListView's multiselect still uses actionbar.
2019-10-17 21:19:14 +02:00
Dennis Guse ed9b4c8f3d Add workaround for Compat's ShareActionProvider, which is not available when inflating in a AbsListView.MultiChoiceModeListener. 2019-10-17 18:33:45 +02:00
Dennis Guse 209aa6a8a5 Hide share action from SearchActivity as it is not (yet) functional. 2019-10-17 07:58:11 +02:00
Dennis Guse 81ee6ac1fa Do not use custom style for AlertDialogs, so PreferenceDialogs and AlertDialogs look the same. 2019-10-17 06:38:05 +02:00
Dennis Guse 024dd603cb Migration to Androidx PreferenceCompat. 2019-10-17 06:00:47 +02:00
Dennis Guse 50654c6baf Release v3.2.1 2019-10-15 18:36:40 +02:00
Dennis Guse 8c9bf60fc6 Update Settings if the units (metric/imperial) are changed. 2019-10-15 17:29:59 +02:00
Dennis Guse a83eb0b983 Added de-DE description. 2019-10-13 12:19:27 +02:00
Dennis Guse ab3cbfea0f Metadata: adjust to fastlane layout. 2019-10-13 12:19:27 +02:00
Dennis Guse c93e5ca1a6 Settings: moved to Androidx Compat and use one screen only. 2019-10-12 18:00:51 +02:00
Dennis Guse c2841092da Settings: moved setup into helper. 2019-09-19 22:30:28 +02:00
Dennis Guse 8b11e1d611 DialogFragments: do not save state. 2019-09-19 22:30:28 +02:00
Dennis Guse 6375a851b2 Moved ChooseActivityTypeDialog to be a real DialogFragment.
Note: This breaks RecordingsSettingsActivity.
2019-09-19 22:28:04 +02:00
105 changed files with 1657 additions and 2446 deletions
+38 -5
View File
@@ -1,12 +1,45 @@
#See https://docs.travis-ci.com/user/languages/android/
#See https://github.com/ankidroid/Anki-Android/blob/master/.travis.yml
language: android language: android
dist: trusty
env:
global:
- ADB_INSTALL_TIMEOUT=8
- ABI=armeabi-v7a
- EMU_FLAVOR=default
# PATH order is incredibly important. e.g. the 'emulator' script exists in more than one place!
- ANDROID_HOME=/usr/local/android-sdk
- TOOLS=${ANDROID_HOME}/tools
- PATH=${ANDROID_HOME}:${ANDROID_HOME}/emulator:${TOOLS}:${TOOLS}/bin:${ANDROID_HOME}/platform-tools:${PATH}
matrix:
- API=27 ABI=x86_64
android: android:
components: components:
# installing tools to start, then use `sdkmanager` below to get the rest
- tools
- build-tools-28.0.3 - build-tools-28.0.3
- android-29 - android-28
# Emulator Management: Create, Start and Wait
install:
- echo y | sdkmanager "platform-tools"
- echo y | sdkmanager --channel=4 "emulator"
- echo y | sdkmanager "system-images;android-$API;$EMU_FLAVOR;$ABI"
- echo no | avdmanager create avd --force -n test -k "system-images;android-$API;$EMU_FLAVOR;$ABI" -c 10M
- emulator -verbose -avd test -no-accel -no-snapshot -no-window -camera-back none -camera-front none -selinux permissive -qemu -m 2048 &
- android-wait-for-emulator
- adb shell input keyevent 82 &
script: script:
- ./gradlew build - ./gradlew clean build
#TODO: When all tests are passing, CI should also execute them. - ./gradlew connectedCheck
#- ./gradlew connectedCheck
sbefore_cache:
- rm -f $HOME/.gradle/caches/modules-2/modules-2.lock
- rm -fr $HOME/.gradle/caches/*/plugin-resolution/
cache:
directories:
- $HOME/.gradle/caches/
- $HOME/.gradle/wrapper/
- $HOME/.android/build-cache
+2 -2
View File
@@ -8,5 +8,5 @@
4. Create changelog (`_version code_.txt`) 4. Create changelog (`_version code_.txt`)
5. Create commit with all changes 5. Create commit with all changes
6. Push commits 6. Push commits
7. Tag newly create commit with _version name_ 7. Tag newly create commit with _version name_ (e.g., v3.2.1)
8. Push the tagged to public repository 8. Push the tagged commit to public repository
+1
View File
@@ -83,6 +83,7 @@ android {
dependencies { dependencies {
implementation 'androidx.appcompat:appcompat:1.1.0' implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'androidx.preference:preference:1.1.0'
androidTestImplementation 'androidx.test:core:1.2.0' androidTestImplementation 'androidx.test:core:1.2.0'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
@@ -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: Changes:
- Removed Widget - 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
@@ -3,6 +3,6 @@ OpenTracks is a sport tracking application that completely respects your privacy
Features: Features:
* Sport tracking: track your sport and outdoor activities * Sport tracking: track your sport and outdoor activities
* Photos and markers: mark interesting locations while tracking * Photos and markers: mark interesting locations while tracking
* Heart rate: supports bluetooth LE sensors * Heart rate: supports Bluetooth LE sensors
* Export data: Export tracks either as KML or GPX * Export data: export tracks either as KML or GPX
* No Internet access: Internet is not used * No Internet access: Internet is not used

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Before

Width:  |  Height:  |  Size: 129 KiB

After

Width:  |  Height:  |  Size: 129 KiB

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 76 KiB

Before

Width:  |  Height:  |  Size: 85 KiB

After

Width:  |  Height:  |  Size: 85 KiB

@@ -0,0 +1 @@
OpenTracks
@@ -40,7 +40,6 @@ import de.dennisguse.opentracks.content.ContentProviderUtils.LocationIterator;
import de.dennisguse.opentracks.content.Waypoint.WaypointType; import de.dennisguse.opentracks.content.Waypoint.WaypointType;
import de.dennisguse.opentracks.stats.TripStatistics; import de.dennisguse.opentracks.stats.TripStatistics;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
/** /**
@@ -214,7 +213,6 @@ public class CustomContentProviderUtilsImplTest {
Track track = providerUtils.createTrack(cursorMock); Track track = providerUtils.createTrack(cursorMock);
Assert.assertEquals(trackId, track.getId()); Assert.assertEquals(trackId, track.getId());
Assert.assertEquals(name, track.getName()); Assert.assertEquals(name, track.getName());
verify(cursorMock);
} }
/** /**
@@ -414,7 +412,6 @@ public class CustomContentProviderUtilsImplTest {
Assert.assertEquals(id, waypoint.getId()); Assert.assertEquals(id, waypoint.getId());
Assert.assertEquals(name, waypoint.getName()); Assert.assertEquals(name, waypoint.getName());
Assert.assertEquals(trackId, waypoint.getTrackId()); Assert.assertEquals(trackId, waypoint.getTrackId());
verify(cursorMock);
} }
/** /**
@@ -709,7 +706,6 @@ public class CustomContentProviderUtilsImplTest {
Assert.assertEquals((double) latitude, location.getLatitude(), 0.01); Assert.assertEquals((double) latitude, location.getLatitude(), 0.01);
Assert.assertEquals(time, location.getTime(), 0.01); Assert.assertEquals(time, location.getTime(), 0.01);
Assert.assertEquals(speed, location.getSpeed(), 0.01); Assert.assertEquals(speed, location.getSpeed(), 0.01);
verify(cursorMock);
} }
/** /**
@@ -112,7 +112,7 @@ public abstract class AbstractTestFileTrackImporter {
* @param lastTrack true if it is the last track in the gpx * @param lastTrack true if it is the last track in the gpx
* @param trackId the track id * @param trackId the track id
*/ */
protected void expectUpdateTrack(ArgumentCaptor<Track> trackCaptor, boolean lastTrack, long trackId) { protected void expectTrackUpdate(ArgumentCaptor<Track> trackCaptor, boolean lastTrack, long trackId) {
contentProviderUtils.updateTrack(trackCaptor.capture()); contentProviderUtils.updateTrack(trackCaptor.capture());
when(contentProviderUtils.insertWaypoint((Waypoint) any())).thenReturn(WAYPOINT_ID_O_URI); when(contentProviderUtils.insertWaypoint((Waypoint) any())).thenReturn(WAYPOINT_ID_O_URI);
@@ -34,6 +34,7 @@ import de.dennisguse.opentracks.util.PreferencesUtils;
import static org.mockito.Mockito.any; import static org.mockito.Mockito.any;
import static org.mockito.Mockito.anyInt; import static org.mockito.Mockito.anyInt;
import static org.mockito.Mockito.anyLong; import static org.mockito.Mockito.anyLong;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.eq; import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
@@ -84,8 +85,7 @@ public class GpxFileTrackImporterTest extends AbstractTestFileTrackImporter {
@Test @Test
public void testOneTrackOneSegment() throws Exception { public void testOneTrackOneSegment() throws Exception {
ArgumentCaptor<Track> trackCaptor = ArgumentCaptor.forClass(Track.class); // given
Location location0 = createLocation(0, DATE_FORMAT_0.parse(TRACK_TIME_0).getTime()); Location location0 = createLocation(0, DATE_FORMAT_0.parse(TRACK_TIME_0).getTime());
Location location1 = createLocation(1, DATE_FORMAT_1.parse(TRACK_TIME_1).getTime()); Location location1 = createLocation(1, DATE_FORMAT_1.parse(TRACK_TIME_1).getTime());
@@ -96,24 +96,27 @@ public class GpxFileTrackImporterTest extends AbstractTestFileTrackImporter {
when(contentProviderUtils.bulkInsertTrackPoint((Location[]) any(), eq(1), eq(TRACK_ID_0))).thenReturn(1); when(contentProviderUtils.bulkInsertTrackPoint((Location[]) any(), eq(1), eq(TRACK_ID_0))).thenReturn(1);
when(contentProviderUtils.getLastTrackPointId(TRACK_ID_0)).thenReturn(TRACK_POINT_ID_1); when(contentProviderUtils.getLastTrackPointId(TRACK_ID_0)).thenReturn(TRACK_POINT_ID_1);
when(contentProviderUtils.getTrack(PreferencesUtils.getLong(context, R.string.recording_track_id_key))).thenReturn(null); when(contentProviderUtils.getTrack(PreferencesUtils.getLong(context, R.string.recording_track_id_key))).thenReturn(null);
expectUpdateTrack(trackCaptor, true, TRACK_ID_0); ArgumentCaptor<Track> trackCaptor = ArgumentCaptor.forClass(Track.class);
expectTrackUpdate(trackCaptor, true, TRACK_ID_0);
// when
InputStream inputStream = new ByteArrayInputStream(VALID_ONE_TRACK_ONE_SEGMENT_GPX.getBytes()); InputStream inputStream = new ByteArrayInputStream(VALID_ONE_TRACK_ONE_SEGMENT_GPX.getBytes());
GpxFileTrackImporter gpxFileTrackImporter = new GpxFileTrackImporter(context, contentProviderUtils); GpxFileTrackImporter gpxFileTrackImporter = new GpxFileTrackImporter(context, contentProviderUtils);
long trackId = gpxFileTrackImporter.importFile(inputStream); long trackId = gpxFileTrackImporter.importFile(inputStream);
// then
Assert.assertEquals(TRACK_ID_0, trackId); Assert.assertEquals(TRACK_ID_0, trackId);
verify(contentProviderUtils, atLeastOnce()).updateTrack(trackCaptor.capture());
long time0 = DATE_FORMAT_0.parse(TRACK_TIME_0).getTime(); long time0 = DATE_FORMAT_0.parse(TRACK_TIME_0).getTime();
long time1 = DATE_FORMAT_1.parse(TRACK_TIME_1).getTime(); long time1 = DATE_FORMAT_1.parse(TRACK_TIME_1).getTime();
Assert.assertEquals(time1 - time0, trackCaptor.getValue().getTripStatistics().getTotalTime()); Assert.assertEquals(time1 - time0, trackCaptor.getValue().getTripStatistics().getTotalTime());
verify(contentProviderUtils);
verifyTrack(trackCaptor.getValue(), TRACK_NAME_0, TRACK_DESCRIPTION_0, time0); verifyTrack(trackCaptor.getValue(), TRACK_NAME_0, TRACK_DESCRIPTION_0, time0);
} }
@Test @Test
public void testOneTrackTwoSegments() throws Exception { public void testOneTrackTwoSegments() throws Exception {
ArgumentCaptor<Track> trackCaptor = ArgumentCaptor.forClass(Track.class); // given
Location location0 = createLocation(0, DATE_FORMAT_0.parse(TRACK_TIME_0).getTime()); Location location0 = createLocation(0, DATE_FORMAT_0.parse(TRACK_TIME_0).getTime());
when(contentProviderUtils.insertTrack((Track) any())).thenReturn(TRACK_ID_0_URI); when(contentProviderUtils.insertTrack((Track) any())).thenReturn(TRACK_ID_0_URI);
@@ -122,11 +125,17 @@ public class GpxFileTrackImporterTest extends AbstractTestFileTrackImporter {
when(contentProviderUtils.bulkInsertTrackPoint((Location[]) any(), eq(5), eq(TRACK_ID_0))).thenReturn(5); when(contentProviderUtils.bulkInsertTrackPoint((Location[]) any(), eq(5), eq(TRACK_ID_0))).thenReturn(5);
when(contentProviderUtils.getLastTrackPointId(TRACK_ID_0)).thenReturn(TRACK_POINT_ID_3); when(contentProviderUtils.getLastTrackPointId(TRACK_ID_0)).thenReturn(TRACK_POINT_ID_3);
when(contentProviderUtils.getTrack(PreferencesUtils.getLong(context, R.string.recording_track_id_key))).thenReturn(null); when(contentProviderUtils.getTrack(PreferencesUtils.getLong(context, R.string.recording_track_id_key))).thenReturn(null);
expectUpdateTrack(trackCaptor, true, TRACK_ID_0);
ArgumentCaptor<Track> trackCaptor = ArgumentCaptor.forClass(Track.class);
expectTrackUpdate(trackCaptor, true, TRACK_ID_0);
// when
InputStream inputStream = new ByteArrayInputStream(VALID_ONE_TRACK_TWO_SEGMENTS_GPX.getBytes()); InputStream inputStream = new ByteArrayInputStream(VALID_ONE_TRACK_TWO_SEGMENTS_GPX.getBytes());
GpxFileTrackImporter gpxFileTrackImporter = new GpxFileTrackImporter(context, contentProviderUtils); GpxFileTrackImporter gpxFileTrackImporter = new GpxFileTrackImporter(context, contentProviderUtils);
long trackId = gpxFileTrackImporter.importFile(inputStream); long trackId = gpxFileTrackImporter.importFile(inputStream);
// then
verify(contentProviderUtils, atLeastOnce()).updateTrack(trackCaptor.capture());
Assert.assertEquals(TRACK_ID_0, trackId); Assert.assertEquals(TRACK_ID_0, trackId);
long time0 = DATE_FORMAT_0.parse(TRACK_TIME_0).getTime(); long time0 = DATE_FORMAT_0.parse(TRACK_TIME_0).getTime();
@@ -135,14 +144,12 @@ public class GpxFileTrackImporterTest extends AbstractTestFileTrackImporter {
long time3 = DATE_FORMAT_1.parse(TRACK_TIME_3).getTime(); long time3 = DATE_FORMAT_1.parse(TRACK_TIME_3).getTime();
Assert.assertEquals(time1 - time0 + time3 - time2, trackCaptor.getValue().getTripStatistics().getTotalTime()); Assert.assertEquals(time1 - time0 + time3 - time2, trackCaptor.getValue().getTripStatistics().getTotalTime());
verify(contentProviderUtils);
verifyTrack(trackCaptor.getValue(), TRACK_NAME_0, TRACK_DESCRIPTION_0, DATE_FORMAT_0.parse(TRACK_TIME_0).getTime()); verifyTrack(trackCaptor.getValue(), TRACK_NAME_0, TRACK_DESCRIPTION_0, DATE_FORMAT_0.parse(TRACK_TIME_0).getTime());
} }
@Test @Test
public void testOneTrackTwoSegmentsNoTime() { public void testOneTrackTwoSegmentsNoTime() {
ArgumentCaptor<Track> trackCaptor = ArgumentCaptor.forClass(Track.class); // given
when(contentProviderUtils.insertTrack((Track) any())).thenReturn(TRACK_ID_0_URI); when(contentProviderUtils.insertTrack((Track) any())).thenReturn(TRACK_ID_0_URI);
expectFirstTrackPoint(null, TRACK_ID_0, TRACK_POINT_ID_0); expectFirstTrackPoint(null, TRACK_ID_0, TRACK_POINT_ID_0);
@@ -150,15 +157,20 @@ public class GpxFileTrackImporterTest extends AbstractTestFileTrackImporter {
when(contentProviderUtils.bulkInsertTrackPoint((Location[]) any(), eq(5), eq(TRACK_ID_0))).thenReturn(5); when(contentProviderUtils.bulkInsertTrackPoint((Location[]) any(), eq(5), eq(TRACK_ID_0))).thenReturn(5);
when(contentProviderUtils.getLastTrackPointId(TRACK_ID_0)).thenReturn(TRACK_POINT_ID_3); when(contentProviderUtils.getLastTrackPointId(TRACK_ID_0)).thenReturn(TRACK_POINT_ID_3);
when(contentProviderUtils.getTrack(PreferencesUtils.getLong(context, R.string.recording_track_id_key))).thenReturn(null); when(contentProviderUtils.getTrack(PreferencesUtils.getLong(context, R.string.recording_track_id_key))).thenReturn(null);
expectUpdateTrack(trackCaptor, true, TRACK_ID_0);
ArgumentCaptor<Track> trackCaptor = ArgumentCaptor.forClass(Track.class);
expectTrackUpdate(trackCaptor, true, TRACK_ID_0);
// when
InputStream inputStream = new ByteArrayInputStream(VALID_ONE_TRACK_TWO_SEGMENTS_NO_TIME_GPX.getBytes()); InputStream inputStream = new ByteArrayInputStream(VALID_ONE_TRACK_TWO_SEGMENTS_NO_TIME_GPX.getBytes());
GpxFileTrackImporter gpxFileTrackImporter = new GpxFileTrackImporter(context, contentProviderUtils); GpxFileTrackImporter gpxFileTrackImporter = new GpxFileTrackImporter(context, contentProviderUtils);
long trackId = gpxFileTrackImporter.importFile(inputStream); long trackId = gpxFileTrackImporter.importFile(inputStream);
Assert.assertEquals(TRACK_ID_0, trackId);
Assert.assertEquals(0, trackCaptor.getValue().getTripStatistics().getTotalTime());
verify(contentProviderUtils); // then
verify(contentProviderUtils, atLeastOnce()).updateTrack(trackCaptor.capture());
Assert.assertEquals(TRACK_ID_0, trackId);
Assert.assertEquals(0, trackCaptor.getValue().getTripStatistics().getTotalTime());
verifyTrack(trackCaptor.getValue(), TRACK_NAME_0, TRACK_DESCRIPTION_0, -1L); verifyTrack(trackCaptor.getValue(), TRACK_NAME_0, TRACK_DESCRIPTION_0, -1L);
} }
@@ -32,6 +32,7 @@ import de.dennisguse.opentracks.content.Track;
import de.dennisguse.opentracks.util.PreferencesUtils; import de.dennisguse.opentracks.util.PreferencesUtils;
import static org.mockito.Mockito.any; import static org.mockito.Mockito.any;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.eq; import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
@@ -68,8 +69,7 @@ public class KmlFileTrackImporterTest extends AbstractTestFileTrackImporter {
@Test @Test
public void testOneTrackOneSegment() throws Exception { public void testOneTrackOneSegment() throws Exception {
ArgumentCaptor<Track> trackCaptor = ArgumentCaptor.forClass(Track.class); // given
Location location0 = createLocation(0, DATE_FORMAT_0.parse(TRACK_TIME_0).getTime()); Location location0 = createLocation(0, DATE_FORMAT_0.parse(TRACK_TIME_0).getTime());
Location location1 = createLocation(1, DATE_FORMAT_1.parse(TRACK_TIME_1).getTime()); Location location1 = createLocation(1, DATE_FORMAT_1.parse(TRACK_TIME_1).getTime());
@@ -77,27 +77,31 @@ public class KmlFileTrackImporterTest extends AbstractTestFileTrackImporter {
expectFirstTrackPoint(location0, TRACK_ID_0, TRACK_POINT_ID_0); expectFirstTrackPoint(location0, TRACK_ID_0, TRACK_POINT_ID_0);
// A flush happens at the end // A flush happens at the end
when(contentProviderUtils.bulkInsertTrackPoint(new Location[]{location1}, eq(1), eq(TRACK_ID_0))).thenReturn(1); when(contentProviderUtils.bulkInsertTrackPoint((Location[]) any(), eq(1), eq(TRACK_ID_0))).thenReturn(1);
when(contentProviderUtils.getLastTrackPointId(TRACK_ID_0)).thenReturn(TRACK_POINT_ID_1); when(contentProviderUtils.getLastTrackPointId(TRACK_ID_0)).thenReturn(TRACK_POINT_ID_1);
when(contentProviderUtils.getTrack(PreferencesUtils.getLong(context, R.string.recording_track_id_key))).thenReturn(null); when(contentProviderUtils.getTrack(PreferencesUtils.getLong(context, R.string.recording_track_id_key))).thenReturn(null);
expectUpdateTrack(trackCaptor, true, TRACK_ID_0);
ArgumentCaptor<Track> trackCaptor = ArgumentCaptor.forClass(Track.class);
expectTrackUpdate(trackCaptor, true, TRACK_ID_0);
// when
InputStream inputStream = new ByteArrayInputStream(VALID_ONE_TRACK_ONE_SEGMENT_GPX.getBytes()); InputStream inputStream = new ByteArrayInputStream(VALID_ONE_TRACK_ONE_SEGMENT_GPX.getBytes());
KmlFileTrackImporter kmlFileTrackImporter = new KmlFileTrackImporter(context, TRACK_ID_0, contentProviderUtils); KmlFileTrackImporter kmlFileTrackImporter = new KmlFileTrackImporter(context, TRACK_ID_0, contentProviderUtils);
long trackId = kmlFileTrackImporter.importFile(inputStream); long trackId = kmlFileTrackImporter.importFile(inputStream);
// then
verify(contentProviderUtils, atLeastOnce()).updateTrack(trackCaptor.capture());
Assert.assertEquals(TRACK_ID_0, trackId); Assert.assertEquals(TRACK_ID_0, trackId);
long time0 = DATE_FORMAT_0.parse(TRACK_TIME_0).getTime(); long time0 = DATE_FORMAT_0.parse(TRACK_TIME_0).getTime();
long time1 = DATE_FORMAT_1.parse(TRACK_TIME_1).getTime(); long time1 = DATE_FORMAT_1.parse(TRACK_TIME_1).getTime();
Assert.assertEquals(time1 - time0, trackCaptor.getValue().getTripStatistics().getTotalTime()); Assert.assertEquals(time1 - time0, trackCaptor.getValue().getTripStatistics().getTotalTime());
verify(contentProviderUtils);
verifyTrack(trackCaptor.getValue(), TRACK_NAME_0, TRACK_DESCRIPTION_0, time0); verifyTrack(trackCaptor.getValue(), TRACK_NAME_0, TRACK_DESCRIPTION_0, time0);
} }
@Test @Test
public void testOneTrackTwoSegments() throws Exception { public void testOneTrackTwoSegments() throws Exception {
ArgumentCaptor<Track> trackCaptor = ArgumentCaptor.forClass(Track.class); // given
Location location0 = createLocation(0, DATE_FORMAT_0.parse(TRACK_TIME_0).getTime()); Location location0 = createLocation(0, DATE_FORMAT_0.parse(TRACK_TIME_0).getTime());
contentProviderUtils.clearTrack(context, TRACK_ID_0); contentProviderUtils.clearTrack(context, TRACK_ID_0);
@@ -107,11 +111,17 @@ public class KmlFileTrackImporterTest extends AbstractTestFileTrackImporter {
when(contentProviderUtils.bulkInsertTrackPoint((Location[]) any(), eq(5), eq(TRACK_ID_0))).thenReturn(5); when(contentProviderUtils.bulkInsertTrackPoint((Location[]) any(), eq(5), eq(TRACK_ID_0))).thenReturn(5);
when(contentProviderUtils.getLastTrackPointId(TRACK_ID_0)).thenReturn(TRACK_POINT_ID_3); when(contentProviderUtils.getLastTrackPointId(TRACK_ID_0)).thenReturn(TRACK_POINT_ID_3);
when(contentProviderUtils.getTrack(PreferencesUtils.getLong(context, R.string.recording_track_id_key))).thenReturn(null); when(contentProviderUtils.getTrack(PreferencesUtils.getLong(context, R.string.recording_track_id_key))).thenReturn(null);
expectUpdateTrack(trackCaptor, true, TRACK_ID_0);
ArgumentCaptor<Track> trackCaptor = ArgumentCaptor.forClass(Track.class);
expectTrackUpdate(trackCaptor, true, TRACK_ID_0);
// when
InputStream inputStream = new ByteArrayInputStream(VALID_ONE_TRACK_TWO_SEGMENTS_GPX.getBytes()); InputStream inputStream = new ByteArrayInputStream(VALID_ONE_TRACK_TWO_SEGMENTS_GPX.getBytes());
KmlFileTrackImporter kmlFileTrackImporter = new KmlFileTrackImporter(context, TRACK_ID_0, contentProviderUtils); KmlFileTrackImporter kmlFileTrackImporter = new KmlFileTrackImporter(context, TRACK_ID_0, contentProviderUtils);
long trackId = kmlFileTrackImporter.importFile(inputStream); long trackId = kmlFileTrackImporter.importFile(inputStream);
// then
verify(contentProviderUtils, atLeastOnce()).updateTrack(trackCaptor.capture());
Assert.assertEquals(TRACK_ID_0, trackId); Assert.assertEquals(TRACK_ID_0, trackId);
long time0 = DATE_FORMAT_0.parse(TRACK_TIME_0).getTime(); long time0 = DATE_FORMAT_0.parse(TRACK_TIME_0).getTime();
@@ -120,7 +130,6 @@ public class KmlFileTrackImporterTest extends AbstractTestFileTrackImporter {
long time3 = DATE_FORMAT_1.parse(TRACK_TIME_3).getTime(); long time3 = DATE_FORMAT_1.parse(TRACK_TIME_3).getTime();
Assert.assertEquals(time1 - time0 + time3 - time2, trackCaptor.getValue().getTripStatistics().getTotalTime()); Assert.assertEquals(time1 - time0 + time3 - time2, trackCaptor.getValue().getTripStatistics().getTotalTime());
verify(contentProviderUtils);
verifyTrack(trackCaptor.getValue(), TRACK_NAME_0, TRACK_DESCRIPTION_0, DATE_FORMAT_0.parse(TRACK_TIME_0).getTime()); verifyTrack(trackCaptor.getValue(), TRACK_NAME_0, TRACK_DESCRIPTION_0, DATE_FORMAT_0.parse(TRACK_TIME_0).getTime());
} }
} }
@@ -15,14 +15,12 @@
*/ */
package de.dennisguse.opentracks.services; package de.dennisguse.opentracks.services;
import android.content.BroadcastReceiver;
import android.content.ContentProvider; import android.content.ContentProvider;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.location.Location; import android.location.Location;
import android.os.IBinder; import android.os.IBinder;
import android.util.Log;
import androidx.test.core.app.ApplicationProvider; import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -38,7 +36,6 @@ import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Random; import java.util.Random;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@@ -77,9 +74,7 @@ public class TrackRecordingServiceTest {
private long trackId = Math.abs(new Random().nextLong()); private long trackId = Math.abs(new Random().nextLong());
static Intent createStartIntent(Context context) { static Intent createStartIntent(Context context) {
Intent startIntent = new Intent(); return new Intent(context, TrackRecordingService.class);
startIntent.setClass(context, TrackRecordingService.class);
return startIntent;
} }
static void updateAutoResumePrefs(Context context, int attempts, int timeoutMins) { static void updateAutoResumePrefs(Context context, int attempts, int timeoutMins) {
@@ -145,7 +140,10 @@ public class TrackRecordingServiceTest {
// Start the service in "resume" mode (simulates the on-reboot action). // Start the service in "resume" mode (simulates the on-reboot action).
Intent startIntent = createStartIntent(context); Intent startIntent = createStartIntent(context);
startIntent.putExtra(TrackRecordingService.RESUME_TRACK_EXTRA_NAME, true); startIntent.putExtra(TrackRecordingService.RESUME_TRACK_EXTRA_NAME, true);
mServiceRule.startService(startIntent);
ITrackRecordingService service = ((ITrackRecordingService) mServiceRule.bindService(startIntent)); ITrackRecordingService service = ((ITrackRecordingService) mServiceRule.bindService(startIntent));
// then
Assert.assertNotNull(service); Assert.assertNotNull(service);
// We expect to resume the previous track. // We expect to resume the previous track.
@@ -168,12 +166,14 @@ public class TrackRecordingServiceTest {
Assert.assertEquals(id, PreferencesUtils.getLong(context, R.string.recording_track_id_key)); Assert.assertEquals(id, PreferencesUtils.getLong(context, R.string.recording_track_id_key));
// Start the service in "resume" mode (simulates the on-reboot action). // Start the service in "resume" mode (simulates the on-reboot action).
Intent startIntent = createStartIntent(context); Intent startIntent = createStartIntent(context);
startIntent.putExtra(TrackRecordingService.RESUME_TRACK_EXTRA_NAME, true); startIntent.putExtra(TrackRecordingService.RESUME_TRACK_EXTRA_NAME, true);
mServiceRule.startService(startIntent); mServiceRule.startService(startIntent);
service = ((ITrackRecordingService) mServiceRule.bindService(createStartIntent(context))); service = ((ITrackRecordingService) mServiceRule.bindService(createStartIntent(context)));
Assert.assertNotNull(service);
// then
Assert.assertNotNull(service);
Assert.assertTrue(service.isRecording()); Assert.assertTrue(service.isRecording());
} }
@@ -189,7 +189,10 @@ public class TrackRecordingServiceTest {
// Start the service in "resume" mode (simulates the on-reboot action). // Start the service in "resume" mode (simulates the on-reboot action).
Intent startIntent = createStartIntent(context); Intent startIntent = createStartIntent(context);
startIntent.putExtra(TrackRecordingService.RESUME_TRACK_EXTRA_NAME, true); startIntent.putExtra(TrackRecordingService.RESUME_TRACK_EXTRA_NAME, true);
mServiceRule.startService(startIntent);
ITrackRecordingService service = ((ITrackRecordingService) mServiceRule.bindService(startIntent)); ITrackRecordingService service = ((ITrackRecordingService) mServiceRule.bindService(startIntent));
// then
Assert.assertNotNull(service); Assert.assertNotNull(service);
// We don't expect to resume the previous track, because it was stopped. // We don't expect to resume the previous track, because it was stopped.
@@ -201,7 +204,7 @@ public class TrackRecordingServiceTest {
@Test @Test
public void testResumeAfterReboot_expiredTrack() throws Exception { public void testResumeAfterReboot_expiredTrack() throws Exception {
// Insert a dummy track last updated 20 min ago. // Insert a dummy track last updated 20 min ago.
createDummyTrack(trackId, System.currentTimeMillis() - 20 * 60 * 1000, true); createDummyTrack(trackId, System.currentTimeMillis() - 1500 * 60 * 1000, true);
// Clear the number of attempts and set the timeout to 10 min. // Clear the number of attempts and set the timeout to 10 min.
updateAutoResumePrefs(context, PreferencesUtils.AUTO_RESUME_TRACK_CURRENT_RETRY_DEFAULT, PreferencesUtils.AUTO_RESUME_TRACK_TIMEOUT_DEFAULT); updateAutoResumePrefs(context, PreferencesUtils.AUTO_RESUME_TRACK_CURRENT_RETRY_DEFAULT, PreferencesUtils.AUTO_RESUME_TRACK_TIMEOUT_DEFAULT);
@@ -209,7 +212,10 @@ public class TrackRecordingServiceTest {
// Start the service in "resume" mode (simulates the on-reboot action). // Start the service in "resume" mode (simulates the on-reboot action).
Intent startIntent = createStartIntent(context); Intent startIntent = createStartIntent(context);
startIntent.putExtra(TrackRecordingService.RESUME_TRACK_EXTRA_NAME, true); startIntent.putExtra(TrackRecordingService.RESUME_TRACK_EXTRA_NAME, true);
mServiceRule.startService(startIntent);
ITrackRecordingService service = ((ITrackRecordingService) mServiceRule.bindService(startIntent)); ITrackRecordingService service = ((ITrackRecordingService) mServiceRule.bindService(startIntent));
// then
Assert.assertNotNull(service); Assert.assertNotNull(service);
// We don't expect to resume the previous track, because it has expired. // We don't expect to resume the previous track, because it has expired.
@@ -229,11 +235,10 @@ public class TrackRecordingServiceTest {
// Start the service in "resume" mode (simulates the on-reboot action). // Start the service in "resume" mode (simulates the on-reboot action).
Intent startIntent = createStartIntent(context); Intent startIntent = createStartIntent(context);
startIntent.putExtra(TrackRecordingService.RESUME_TRACK_EXTRA_NAME, true); startIntent.putExtra(TrackRecordingService.RESUME_TRACK_EXTRA_NAME, true);
mServiceRule.startService(startIntent);
//Explicit start service, so `startCommand()` is executed - which would be called by BootReceiver.
context.startService(startIntent);
ITrackRecordingService service = ((ITrackRecordingService) mServiceRule.bindService(startIntent)); ITrackRecordingService service = ((ITrackRecordingService) mServiceRule.bindService(startIntent));
//then
Assert.assertNotNull(service); Assert.assertNotNull(service);
// We don't expect to resume the previous track, because there were already too many attempts. // We don't expect to resume the previous track, because there were already too many attempts.
@@ -247,7 +252,10 @@ public class TrackRecordingServiceTest {
List<Track> tracks = providerUtils.getAllTracks(); List<Track> tracks = providerUtils.getAllTracks();
Assert.assertTrue(tracks.isEmpty()); Assert.assertTrue(tracks.isEmpty());
ITrackRecordingService service = ((ITrackRecordingService) mServiceRule.bindService(createStartIntent(context))); Intent startIntent = createStartIntent(context);
mServiceRule.startService(startIntent);
ITrackRecordingService service = ((ITrackRecordingService) mServiceRule.bindService(startIntent));
// Test if we start in no-recording mode by default. // Test if we start in no-recording mode by default.
Assert.assertFalse(service.isRecording()); Assert.assertFalse(service.isRecording());
Assert.assertEquals(-1L, service.getRecordingTrackId()); Assert.assertEquals(-1L, service.getRecordingTrackId());
@@ -266,11 +274,14 @@ public class TrackRecordingServiceTest {
@MediumTest @MediumTest
@Test @Test
public void testRecording_orphanedRecordingTrack() throws Exception { public void testRecording_orphanedRecordingTrack() throws Exception {
Intent startIntent = createStartIntent(context);
ITrackRecordingService service = ((ITrackRecordingService) mServiceRule.bindService(startIntent));
// Just set recording track to a bogus value. // Just set recording track to a bogus value.
// Make sure that the service will not start recording and will clear the bogus track.
PreferencesUtils.setLong(context, R.string.recording_track_id_key, 123L); PreferencesUtils.setLong(context, R.string.recording_track_id_key, 123L);
// Make sure that the service will not start recording and will clear the bogus track. // then
ITrackRecordingService service = ((ITrackRecordingService) mServiceRule.bindService(createStartIntent(context)));
Assert.assertFalse(service.isRecording()); Assert.assertFalse(service.isRecording());
Assert.assertEquals(PreferencesUtils.RECORDING_TRACK_ID_DEFAULT, service.getRecordingTrackId()); Assert.assertEquals(PreferencesUtils.RECORDING_TRACK_ID_DEFAULT, service.getRecordingTrackId());
} }
@@ -405,45 +416,4 @@ public class TrackRecordingServiceTest {
Assert.assertNotNull(wpt.getLocation()); Assert.assertNotNull(wpt.getLocation());
Assert.assertNull(wpt.getTripStatistics()); Assert.assertNull(wpt.getTripStatistics());
} }
/**
* Synchronous/waitable broadcast receiver to be used in testing.
*/
private class BlockingBroadcastReceiver extends BroadcastReceiver {
private static final long MAX_WAIT_TIME_MS = 3000;
private final List<Intent> receivedIntents = new ArrayList<>();
public List<Intent> getReceivedIntents() {
return receivedIntents;
}
@Override
public void onReceive(Context ctx, Intent intent) {
Log.d("Test", "Got broadcast: " + intent);
synchronized (receivedIntents) {
receivedIntents.add(intent);
receivedIntents.notifyAll();
}
}
public boolean waitUntilReceived(int receiveCount) {
long deadline = System.currentTimeMillis() + MAX_WAIT_TIME_MS;
synchronized (receivedIntents) {
while (receivedIntents.size() < receiveCount) {
try {
// Wait releases synchronized lock until it returns
receivedIntents.wait(500);
} catch (InterruptedException e) {
// Do nothing
}
if (System.currentTimeMillis() > deadline) {
return false;
}
}
}
return true;
}
}
} }
@@ -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.3", 2010, 5, 4, 3, 2, 1, 300);
assertGetTime("2010-05-04T03:02:01.35", 2010, 5, 4, 3, 2, 1, 350); assertGetTime("2010-05-04T03:02:01.35", 2010, 5, 4, 3, 2, 1, 350);
assertGetTime("2010-05-04T03:02:01.352Z", 2010, 5, 4, 3, 2, 1, 352); assertGetTime("2010-05-04T03:02:01.352Z", 2010, 5, 4, 3, 2, 1, 352);
assertGetTime("2010-05-04T03:02:01.3525Z", 2010, 5, 4, 3, 2, 1, 352); assertGetTime("2010-05-04T03:02:01.3529Z", 2010, 5, 4, 3, 2, 1, 353);
} }
/** /**
@@ -149,8 +149,7 @@ public class StringUtilsTest {
} }
/** /**
* Tests {@link StringUtils#getTime(String)} with fractional seconds and time * Tests {@link StringUtils#getTime(String)} with fractional seconds and time zone.
* zone.
*/ */
@Test @Test
public void testGetTime_fractionalAndTimezone() { public void testGetTime_fractionalAndTimezone() {
@@ -183,7 +182,7 @@ public class StringUtilsTest {
* Tests {@link StringUtils#getTimeParts(long)} with a positive number. * Tests {@link StringUtils#getTimeParts(long)} with a positive number.
*/ */
@Test @Test
public void testGetTimeParts_postive() { public void testGetTimeParts_positive() {
int[] parts = StringUtils.getTimeParts(61000); int[] parts = StringUtils.getTimeParts(61000);
Assert.assertEquals(1, parts[0]); Assert.assertEquals(1, parts[0]);
Assert.assertEquals(1, parts[1]); Assert.assertEquals(1, parts[1]);
@@ -200,4 +199,24 @@ public class StringUtilsTest {
Assert.assertEquals(-1, parts[1]); Assert.assertEquals(-1, parts[1]);
Assert.assertEquals(0, parts[2]); Assert.assertEquals(0, parts[2]);
} }
@Test
public void testFormatDecimal() {
Assert.assertEquals("0", StringUtils.formatDecimal(0.0, 0));
Assert.assertEquals("0", StringUtils.formatDecimal(0.1, 0));
Assert.assertEquals("1", StringUtils.formatDecimal(1.1, 0));
Assert.assertEquals("10", StringUtils.formatDecimal(10, 0));
Assert.assertEquals("10", StringUtils.formatDecimal(10.1, 0));
Assert.assertEquals("0", StringUtils.formatDecimal(-0.1, 0));
Assert.assertEquals("0", StringUtils.formatDecimal(0.0, 2));
Assert.assertEquals("0.1", StringUtils.formatDecimal(0.1, 2));
Assert.assertEquals("1.1", StringUtils.formatDecimal(1.1, 2));
Assert.assertEquals("10", StringUtils.formatDecimal(10, 2));
Assert.assertEquals("10.1", StringUtils.formatDecimal(10.1, 2));
Assert.assertEquals("10.11", StringUtils.formatDecimal(10.111, 2));
Assert.assertEquals("-0.1", StringUtils.formatDecimal(-0.1, 2));
Assert.assertEquals("1", StringUtils.formatDecimal(0.99, 1));
}
} }
+2 -11
View File
@@ -16,8 +16,8 @@ limitations under the License.
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="de.dennisguse.opentracks" package="de.dennisguse.opentracks"
android:installLocation="auto" android:installLocation="auto"
android:versionCode="3054" android:versionCode="3101"
android:versionName="v3.2.0"> <!-- For FDroid. --> android:versionName="v3.2.3"> <!-- For FDroid. -->
<!-- Permission to initialize services --> <!-- Permission to initialize services -->
<application <application
android:name=".Startup" android:name=".Startup"
@@ -30,10 +30,6 @@ limitations under the License.
<!-- Activities --> <!-- Activities -->
<activity android:name=".AboutActivity" /> <activity android:name=".AboutActivity" />
<activity
android:name=".AggregatedStatsActivity"
android:label="@string/menu_aggregated_statistics" />
<activity android:name=".DeleteActivity" /> <activity android:name=".DeleteActivity" />
<activity <activity
@@ -97,12 +93,8 @@ limitations under the License.
android:name=".io.file.exporter.ExportActivity" android:name=".io.file.exporter.ExportActivity"
android:theme="@style/ThemeCustomTransparent" /> android:theme="@style/ThemeCustomTransparent" />
<activity android:name=".settings.RecordingSettingsActivity" />
<activity android:name=".settings.SettingsActivity" /> <activity android:name=".settings.SettingsActivity" />
<activity android:name=".settings.StatsSettingsActivity" />
<!-- Services --> <!-- Services -->
<provider <provider
android:name=".content.ShareContentProvider" android:name=".content.ShareContentProvider"
@@ -128,7 +120,6 @@ limitations under the License.
</receiver> </receiver>
<!-- Providers --> <!-- Providers -->
<service <service
android:name=".services.TrackRecordingService" android:name=".services.TrackRecordingService"
android:enabled="true" android:enabled="true"
@@ -19,12 +19,13 @@ package de.dennisguse.opentracks;
import android.os.Bundle; import android.os.Bundle;
import android.speech.tts.TextToSpeech; import android.speech.tts.TextToSpeech;
import androidx.fragment.app.FragmentActivity; import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
/** /**
* @author Jimmy Shih * @author Jimmy Shih
*/ */
public abstract class AbstractActivity extends FragmentActivity { public abstract class AbstractActivity extends AppCompatActivity {
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
@@ -34,6 +35,9 @@ public abstract class AbstractActivity extends FragmentActivity {
setVolumeControlStream(TextToSpeech.Engine.DEFAULT_STREAM); setVolumeControlStream(TextToSpeech.Engine.DEFAULT_STREAM);
setContentView(getLayoutResId()); setContentView(getLayoutResId());
Toolbar toolbar = findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
} }
/** /**
@@ -16,24 +16,7 @@
package de.dennisguse.opentracks; 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.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;
import de.dennisguse.opentracks.fragments.ConfirmDeleteDialogFragment.ConfirmDeleteCaller; 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; protected static final int GPS_REQUEST_CODE = 6;
private static final int DELETE_REQUEST_CODE = 3; private static final int DELETE_REQUEST_CODE = 3;
public static void configureListViewContextualMenu(final ListView listView, final ContextualActionModeCallback contextualActionModeCallback) {
listView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE_MODAL);
listView.setMultiChoiceModeListener(new AbsListView.MultiChoiceModeListener() {
@Override
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
mode.getMenuInflater().inflate(R.menu.list_context_menu, menu);
return true;
}
@Override
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
contextualActionModeCallback.onPrepare(menu, getCheckedPositions(listView), listView.getCheckedItemIds(), true);
return true;
}
@Override
public void onDestroyActionMode(ActionMode mode) {
// Do nothing
}
@Override
public void onItemCheckedStateChanged(ActionMode mode, int position, long id, boolean checked) {
mode.invalidate();
}
@Override
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
if (contextualActionModeCallback.onClick(item.getItemId(), getCheckedPositions(listView), listView.getCheckedItemIds())) {
mode.finish();
}
return true;
}
/**
* Gets the checked positions in a list view.
*
* @param list the list view
*/
private int[] getCheckedPositions(ListView list) {
SparseBooleanArray positions = list.getCheckedItemPositions();
ArrayList<Integer> arrayList = new ArrayList<>();
for (int i = 0; i < positions.size(); i++) {
int key = positions.keyAt(i);
if (positions.valueAt(i)) {
arrayList.add(key);
}
}
int[] result = new int[arrayList.size()];
for (int i = 0; i < arrayList.size(); i++) {
result[i] = arrayList.get(i);
}
return result;
}
});
}
public static void configureSearchWidget(Activity activity, final MenuItem menuItem, final TrackController trackController) {
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 @Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) { protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == DELETE_REQUEST_CODE) { if (requestCode == DELETE_REQUEST_CODE) {
@@ -182,8 +56,7 @@ public abstract class AbstractListActivity extends AbstractActivity implements C
* @param trackIds the track ids * @param trackIds the track ids
*/ */
protected void deleteTracks(long[] trackIds) { protected void deleteTracks(long[] trackIds) {
ConfirmDeleteDialogFragment.newInstance(trackIds) ConfirmDeleteDialogFragment.showDialog(getSupportFragmentManager(), trackIds);
.show(getSupportFragmentManager(), ConfirmDeleteDialogFragment.CONFIRM_DELETE_DIALOG_TAG);
} }
@Override @Override
@@ -209,10 +82,9 @@ public abstract class AbstractListActivity extends AbstractActivity implements C
} }
/** /**
* Gets the track recording service connection. For stopping the current * Gets the track recording service connection.
* recording if need to delete the current recording track. * For stopping the current recording if need to delete the current recording track.
*/ */
@NonNull
abstract protected TrackRecordingServiceConnection getTrackRecordingServiceConnection(); 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 * Sets chart by distance.
* will be reloaded. * It is expected that after changing this value, data will be reloaded.
* *
* @param value true for by distance, false for by time * @param value true for by distance, false for by time
*/ */
@@ -22,14 +22,11 @@ import android.content.SharedPreferences;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener; import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
import android.database.Cursor; import android.database.Cursor;
import android.os.Bundle; import android.os.Bundle;
import android.view.ContextMenu;
import android.view.ContextMenu.ContextMenuInfo;
import android.view.KeyEvent; import android.view.KeyEvent;
import android.view.Menu; import android.view.Menu;
import android.view.MenuItem; import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.widget.AdapterView; import android.widget.AdapterView;
import android.widget.AdapterView.AdapterContextMenuInfo;
import android.widget.AdapterView.OnItemClickListener; import android.widget.AdapterView.OnItemClickListener;
import android.widget.ListView; import android.widget.ListView;
@@ -46,6 +43,7 @@ import de.dennisguse.opentracks.content.Waypoint.WaypointType;
import de.dennisguse.opentracks.content.WaypointsColumns; import de.dennisguse.opentracks.content.WaypointsColumns;
import de.dennisguse.opentracks.fragments.DeleteMarkerDialogFragment; import de.dennisguse.opentracks.fragments.DeleteMarkerDialogFragment;
import de.dennisguse.opentracks.fragments.DeleteMarkerDialogFragment.DeleteMarkerCaller; import de.dennisguse.opentracks.fragments.DeleteMarkerDialogFragment.DeleteMarkerCaller;
import de.dennisguse.opentracks.util.ActivityUtils;
import de.dennisguse.opentracks.util.IntentUtils; import de.dennisguse.opentracks.util.IntentUtils;
import de.dennisguse.opentracks.util.ListItemUtils; import de.dennisguse.opentracks.util.ListItemUtils;
import de.dennisguse.opentracks.util.PreferencesUtils; import de.dennisguse.opentracks.util.PreferencesUtils;
@@ -175,7 +173,7 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
} }
}; };
listView.setAdapter(resourceCursorAdapter); listView.setAdapter(resourceCursorAdapter);
AbstractListActivity.configureListViewContextualMenu(listView, contextualActionModeCallback); ActivityUtils.configureListViewContextualMenu(listView, contextualActionModeCallback);
LoaderManager.getInstance(this).initLoader(0, null, new LoaderCallbacks<Cursor>() { LoaderManager.getInstance(this).initLoader(0, null, new LoaderCallbacks<Cursor>() {
@NonNull @NonNull
@@ -234,7 +232,7 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
insertMarkerMenuItem = menu.findItem(R.id.marker_list_insert_marker); insertMarkerMenuItem = menu.findItem(R.id.marker_list_insert_marker);
searchMenuItem = menu.findItem(R.id.marker_list_search); searchMenuItem = menu.findItem(R.id.marker_list_search);
AbstractListActivity.configureSearchWidget(this, searchMenuItem, null); ActivityUtils.configureSearchWidget(this, searchMenuItem, null);
return super.onCreateOptionsMenu(menu); return super.onCreateOptionsMenu(menu);
} }
@@ -256,24 +254,6 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
return super.onOptionsItemSelected(item); return super.onOptionsItemSelected(item);
} }
@Override
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
super.onCreateContextMenu(menu, v, menuInfo);
getMenuInflater().inflate(R.menu.list_context_menu, menu);
AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo;
contextualActionModeCallback.onPrepare(menu, new int[]{info.position}, new long[]{info.id}, false);
}
@Override
public boolean onContextItemSelected(MenuItem item) {
AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo();
if (handleContextItem(item.getItemId(), new long[]{info.id})) {
return true;
}
return super.onContextItemSelected(item);
}
/** /**
* Handles a context item selection. * Handles a context item selection.
* *
@@ -300,8 +280,7 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
if (markerIds.length > 1 && markerIds.length == listView.getCount()) { if (markerIds.length > 1 && markerIds.length == listView.getCount()) {
markerIds = new long[]{-1L}; markerIds = new long[]{-1L};
} }
DeleteMarkerDialogFragment.newInstance(markerIds) DeleteMarkerDialogFragment.showDialog(getSupportFragmentManager(), markerIds);
.show(getSupportFragmentManager(), DeleteMarkerDialogFragment.DELETE_MARKER_DIALOG_TAG);
return true; return true;
case R.id.list_context_menu_select_all: case R.id.list_context_menu_select_all:
int size = listView.getCount(); int size = listView.getCount();
@@ -22,14 +22,11 @@ import android.content.SharedPreferences;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener; import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
import android.os.Bundle; import android.os.Bundle;
import android.util.Log; import android.util.Log;
import android.view.ContextMenu;
import android.view.ContextMenu.ContextMenuInfo;
import android.view.KeyEvent; import android.view.KeyEvent;
import android.view.Menu; import android.view.Menu;
import android.view.MenuItem; import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.widget.AdapterView; import android.widget.AdapterView;
import android.widget.AdapterView.AdapterContextMenuInfo;
import android.widget.AdapterView.OnItemClickListener; import android.widget.AdapterView.OnItemClickListener;
import android.widget.ArrayAdapter; import android.widget.ArrayAdapter;
import android.widget.ListView; import android.widget.ListView;
@@ -55,6 +52,7 @@ import de.dennisguse.opentracks.fragments.DeleteMarkerDialogFragment;
import de.dennisguse.opentracks.fragments.DeleteMarkerDialogFragment.DeleteMarkerCaller; import de.dennisguse.opentracks.fragments.DeleteMarkerDialogFragment.DeleteMarkerCaller;
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection; import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
import de.dennisguse.opentracks.stats.TripStatistics; import de.dennisguse.opentracks.stats.TripStatistics;
import de.dennisguse.opentracks.util.ActivityUtils;
import de.dennisguse.opentracks.util.IntentUtils; import de.dennisguse.opentracks.util.IntentUtils;
import de.dennisguse.opentracks.util.ListItemUtils; import de.dennisguse.opentracks.util.ListItemUtils;
import de.dennisguse.opentracks.util.PreferencesUtils; import de.dennisguse.opentracks.util.PreferencesUtils;
@@ -109,28 +107,29 @@ public class SearchListActivity extends AbstractListActivity implements DeleteMa
private ContextualActionModeCallback contextualActionModeCallback = new ContextualActionModeCallback() { private ContextualActionModeCallback contextualActionModeCallback = new ContextualActionModeCallback() {
@Override @Override
public void onPrepare(Menu menu, int[] positions, long[] ids, boolean showSelectAll) { public void onPrepare(Menu menu, int[] positions, long[] ids, boolean showSelectAll) {
boolean isRecording = recordingTrackId != PreferencesUtils.RECORDING_TRACK_ID_DEFAULT; boolean isRecording = PreferencesUtils.isRecording(recordingTrackId);
boolean isSingleSelection = positions.length == 1; boolean isSingleSelection = positions.length == 1;
boolean isSingleSelectionTrack; boolean isSingleSelectionTrack;
if (isSingleSelection) { if (isSingleSelection) {
Map<String, Object> item = arrayAdapter.getItem(positions[0]); Map<String, Object> item = arrayAdapter.getItem(positions[0]);
Long trackId = (Long) item.get(TRACK_ID_FIELD);
Track track = contentProviderUtils.getTrack(trackId);
isSingleSelectionTrack = item.get(MARKER_ID_FIELD) == null; isSingleSelectionTrack = item.get(MARKER_ID_FIELD) == null;
} else { } else {
isSingleSelectionTrack = false; isSingleSelectionTrack = false;
} }
// Not recording, one item, item is a track // Not recording, one item, item is a track
menu.findItem(R.id.list_context_menu_share).setVisible(!isRecording && isSingleSelection && isSingleSelectionTrack); MenuItem shareMenuItem = menu.findItem(R.id.list_context_menu_share);
//TODO Setup shareIntent. if (isSingleSelectionTrack) {
shareMenuItem.setVisible(!isRecording);
}
// One item, item is a marker // One item, item is a marker
menu.findItem(R.id.list_context_menu_show_on_map).setVisible(isSingleSelection && !isSingleSelectionTrack); menu.findItem(R.id.list_context_menu_show_on_map).setVisible(isSingleSelection && !isSingleSelectionTrack);
// One item, can be a track or a marker // One item, can be a track or a marker
menu.findItem(R.id.list_context_menu_edit).setVisible(isSingleSelection); menu.findItem(R.id.list_context_menu_edit).setVisible(isSingleSelection);
// One item. If track, no restriction. // One item. If track, no restriction.
menu.findItem(R.id.list_context_menu_delete).setVisible(isSingleSelection && (isSingleSelectionTrack)); menu.findItem(R.id.list_context_menu_delete).setVisible(isSingleSelection && isSingleSelectionTrack);
// Disable select all, no action is available for multiple selection // Disable select all, no action is available for multiple selection
menu.findItem(R.id.list_context_menu_select_all).setVisible(false); menu.findItem(R.id.list_context_menu_select_all).setVisible(false);
} }
@@ -222,7 +221,7 @@ public class SearchListActivity extends AbstractListActivity implements DeleteMa
startActivity(intent); startActivity(intent);
} }
}); });
AbstractListActivity.configureListViewContextualMenu(listView, contextualActionModeCallback); ActivityUtils.configureListViewContextualMenu(listView, contextualActionModeCallback);
handleIntent(getIntent()); handleIntent(getIntent());
} }
@@ -259,24 +258,6 @@ public class SearchListActivity extends AbstractListActivity implements DeleteMa
handleIntent(intent); 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 @Override
public boolean onKeyUp(int keyCode, KeyEvent event) { public boolean onKeyUp(int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_SEARCH) { if (keyCode == KeyEvent.KEYCODE_SEARCH) {
@@ -301,13 +282,14 @@ public class SearchListActivity extends AbstractListActivity implements DeleteMa
Long markerId = (Long) item.get(MARKER_ID_FIELD); Long markerId = (Long) item.get(MARKER_ID_FIELD);
Intent intent; Intent intent;
switch (itemId) { switch (itemId) {
case R.id.list_context_menu_share:
//TODO
Log.e(TAG, "Not implemented");
return true;
case R.id.list_context_menu_show_on_map: case R.id.list_context_menu_show_on_map:
IntentUtils.showCoordinateOnMap(this, (double) item.get(MARKER_LATITUDE_FIELD), (double) item.get(MARKER_LONGITUDE_FIELD), item.get(NAME_FIELD) + ""); IntentUtils.showCoordinateOnMap(this, (double) item.get(MARKER_LATITUDE_FIELD), (double) item.get(MARKER_LONGITUDE_FIELD), item.get(NAME_FIELD) + "");
return true; return true;
case R.id.list_context_menu_share:
intent = IntentUtils.newShareFileIntent(this, new long[]{trackId});
intent = Intent.createChooser(intent, null);
startActivity(intent);
return true;
case R.id.list_context_menu_edit: case R.id.list_context_menu_edit:
if (markerId != null) { if (markerId != null) {
intent = IntentUtils.newIntent(this, MarkerEditActivity.class) intent = IntentUtils.newIntent(this, MarkerEditActivity.class)
@@ -323,7 +305,7 @@ public class SearchListActivity extends AbstractListActivity implements DeleteMa
return true; return true;
case R.id.list_context_menu_delete: case R.id.list_context_menu_delete:
if (markerId != null) { if (markerId != null) {
DeleteMarkerDialogFragment.newInstance(new long[]{markerId}).show(getSupportFragmentManager(), DeleteMarkerDialogFragment.DELETE_MARKER_DIALOG_TAG); DeleteMarkerDialogFragment.showDialog(getSupportFragmentManager(), new long[]{markerId});
} else { } else {
deleteTracks(new long[]{trackId}); deleteTracks(new long[]{trackId});
} }
@@ -130,11 +130,11 @@ public class TrackController {
handlerUpdateTotalTime.removeCallbacks(updateTotalTimeRunnable); handlerUpdateTotalTime.removeCallbacks(updateTotalTimeRunnable);
} }
void hide() { public void hide() {
containerView.setVisibility(View.GONE); containerView.setVisibility(View.GONE);
} }
void show() { public void show() {
containerView.setVisibility(View.VISIBLE); containerView.setVisibility(View.VISIBLE);
} }
@@ -31,7 +31,6 @@ import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.view.View.OnClickListener; import android.view.View.OnClickListener;
import android.view.WindowManager; import android.view.WindowManager;
import android.widget.ShareActionProvider;
import android.widget.TabHost; import android.widget.TabHost;
import android.widget.TabHost.TabSpec; import android.widget.TabHost.TabSpec;
import android.widget.Toast; import android.widget.Toast;
@@ -48,7 +47,6 @@ import de.dennisguse.opentracks.content.Waypoint.WaypointType;
import de.dennisguse.opentracks.content.WaypointCreationRequest; import de.dennisguse.opentracks.content.WaypointCreationRequest;
import de.dennisguse.opentracks.fragments.ChartFragment; import de.dennisguse.opentracks.fragments.ChartFragment;
import de.dennisguse.opentracks.fragments.ChooseActivityTypeDialogFragment; import de.dennisguse.opentracks.fragments.ChooseActivityTypeDialogFragment;
import de.dennisguse.opentracks.fragments.ChooseActivityTypeDialogFragment.ChooseActivityTypeCaller;
import de.dennisguse.opentracks.fragments.ConfirmDeleteDialogFragment; import de.dennisguse.opentracks.fragments.ConfirmDeleteDialogFragment;
import de.dennisguse.opentracks.fragments.FrequencyDialogFragment; import de.dennisguse.opentracks.fragments.FrequencyDialogFragment;
import de.dennisguse.opentracks.fragments.StatsFragment; import de.dennisguse.opentracks.fragments.StatsFragment;
@@ -66,7 +64,7 @@ import de.dennisguse.opentracks.util.TrackUtils;
* @author Leif Hendrik Wilden * @author Leif Hendrik Wilden
* @author Rodrigo Damazio * @author Rodrigo Damazio
*/ */
public class TrackDetailActivity extends AbstractListActivity implements ChooseActivityTypeCaller, ConfirmDeleteDialogFragment.ConfirmDeleteCaller { public class TrackDetailActivity extends AbstractListActivity implements ChooseActivityTypeDialogFragment.ChooseActivityTypeCaller, ConfirmDeleteDialogFragment.ConfirmDeleteCaller {
public static final String EXTRA_TRACK_ID = "track_id"; public static final String EXTRA_TRACK_ID = "track_id";
public static final String EXTRA_MARKER_ID = "marker_id"; public static final String EXTRA_MARKER_ID = "marker_id";
@@ -317,11 +315,7 @@ public class TrackDetailActivity extends AbstractListActivity implements ChooseA
insertMarkerMenuItem = menu.findItem(R.id.track_detail_insert_marker); insertMarkerMenuItem = menu.findItem(R.id.track_detail_insert_marker);
insertPhotoMenuItem = menu.findItem(R.id.track_detail_insert_photo); insertPhotoMenuItem = menu.findItem(R.id.track_detail_insert_photo);
insertPhotoMenuItem.setVisible(new Intent(MediaStore.ACTION_IMAGE_CAPTURE).resolveActivity(getPackageManager()) != null); insertPhotoMenuItem.setVisible(new Intent(MediaStore.ACTION_IMAGE_CAPTURE).resolveActivity(getPackageManager()) != null);
shareMenuItem = menu.findItem(R.id.track_detail_share); shareMenuItem = menu.findItem(R.id.track_detail_share);
ShareActionProvider shareActionProvider = (ShareActionProvider) shareMenuItem.getActionProvider();
shareActionProvider.setShareIntent(IntentUtils.newShareFileIntent(this, new long[]{trackId}));
voiceFrequencyMenuItem = menu.findItem(R.id.track_detail_voice_frequency); voiceFrequencyMenuItem = menu.findItem(R.id.track_detail_voice_frequency);
splitFrequencyMenuItem = menu.findItem(R.id.track_detail_split_frequency); splitFrequencyMenuItem = menu.findItem(R.id.track_detail_split_frequency);
markerListMenuItem = menu.findItem(R.id.track_detail_markers); markerListMenuItem = menu.findItem(R.id.track_detail_markers);
@@ -345,6 +339,11 @@ public class TrackDetailActivity extends AbstractListActivity implements ChooseA
.putExtra(MarkerEditActivity.EXTRA_TRACK_ID, trackId); .putExtra(MarkerEditActivity.EXTRA_TRACK_ID, trackId);
startActivity(intent); startActivity(intent);
return true; return true;
case R.id.track_detail_share:
intent = IntentUtils.newShareFileIntent(this, new long[]{trackId});
intent = Intent.createChooser(intent, null);
startActivity(intent);
return true;
case R.id.track_detail_insert_photo: case R.id.track_detail_insert_photo:
if (!FileUtils.isExternalStorageWriteable() || ActivityCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) { if (!FileUtils.isExternalStorageWriteable() || ActivityCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) {
ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, EXTERNAL_STORAGE_PERMISSION_REQUEST_CODE); ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, EXTERNAL_STORAGE_PERMISSION_REQUEST_CODE);
@@ -361,12 +360,10 @@ public class TrackDetailActivity extends AbstractListActivity implements ChooseA
startActivity(intent); startActivity(intent);
return true; return true;
case R.id.track_detail_voice_frequency: case R.id.track_detail_voice_frequency:
FrequencyDialogFragment.newInstance(R.string.voice_frequency_key, PreferencesUtils.VOICE_FREQUENCY_DEFAULT, R.string.menu_voice_frequency) FrequencyDialogFragment.showDialog(getSupportFragmentManager(), R.string.voice_frequency_key, PreferencesUtils.VOICE_FREQUENCY_DEFAULT, R.string.menu_voice_frequency);
.show(getSupportFragmentManager(), FrequencyDialogFragment.FREQUENCY_DIALOG_TAG);
return true; return true;
case R.id.track_detail_split_frequency: case R.id.track_detail_split_frequency:
FrequencyDialogFragment.newInstance(R.string.split_frequency_key, PreferencesUtils.SPLIT_FREQUENCY_DEFAULT, R.string.menu_split_frequency) FrequencyDialogFragment.showDialog(getSupportFragmentManager(), R.string.split_frequency_key, PreferencesUtils.SPLIT_FREQUENCY_DEFAULT, R.string.menu_split_frequency);
.show(getSupportFragmentManager(), FrequencyDialogFragment.FREQUENCY_DIALOG_TAG);
return true; return true;
case R.id.track_detail_edit: case R.id.track_detail_edit:
intent = IntentUtils.newIntent(this, TrackEditActivity.class) intent = IntentUtils.newIntent(this, TrackEditActivity.class)
@@ -395,6 +392,7 @@ public class TrackDetailActivity extends AbstractListActivity implements ChooseA
} }
return; return;
} }
super.onRequestPermissionsResult(requestCode, permissions, grantResults); super.onRequestPermissionsResult(requestCode, permissions, grantResults);
} }
@@ -487,7 +485,7 @@ public class TrackDetailActivity extends AbstractListActivity implements ChooseA
} }
public void chooseActivityType(String category) { public void chooseActivityType(String category) {
ChooseActivityTypeDialogFragment.newInstance(category).show(getSupportFragmentManager(), ChooseActivityTypeDialogFragment.CHOOSE_ACTIVITY_TYPE_DIALOG_TAG); ChooseActivityTypeDialogFragment.showDialog(getSupportFragmentManager(), category);
} }
@Override @Override
@@ -28,10 +28,11 @@ import android.widget.Button;
import android.widget.EditText; import android.widget.EditText;
import android.widget.Spinner; import android.widget.Spinner;
import androidx.annotation.NonNull;
import de.dennisguse.opentracks.content.ContentProviderUtils; import de.dennisguse.opentracks.content.ContentProviderUtils;
import de.dennisguse.opentracks.content.Track; import de.dennisguse.opentracks.content.Track;
import de.dennisguse.opentracks.fragments.ChooseActivityTypeDialogFragment; import de.dennisguse.opentracks.fragments.ChooseActivityTypeDialogFragment;
import de.dennisguse.opentracks.fragments.ChooseActivityTypeDialogFragment.ChooseActivityTypeCaller;
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection; import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
import de.dennisguse.opentracks.util.TrackIconUtils; import de.dennisguse.opentracks.util.TrackIconUtils;
import de.dennisguse.opentracks.util.TrackUtils; import de.dennisguse.opentracks.util.TrackUtils;
@@ -41,7 +42,7 @@ import de.dennisguse.opentracks.util.TrackUtils;
* *
* @author Leif Hendrik Wilden * @author Leif Hendrik Wilden
*/ */
public class TrackEditActivity extends AbstractActivity implements ChooseActivityTypeCaller { public class TrackEditActivity extends AbstractActivity implements ChooseActivityTypeDialogFragment.ChooseActivityTypeCaller {
public static final String EXTRA_TRACK_ID = "track_id"; public static final String EXTRA_TRACK_ID = "track_id";
public static final String EXTRA_NEW_TRACK = "new_track"; public static final String EXTRA_NEW_TRACK = "new_track";
@@ -119,9 +120,7 @@ public class TrackEditActivity extends AbstractActivity implements ChooseActivit
@Override @Override
public boolean onTouch(View v, MotionEvent event) { public boolean onTouch(View v, MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_UP) { if (event.getAction() == MotionEvent.ACTION_UP) {
ChooseActivityTypeDialogFragment.newInstance(activityType.getText().toString()).show( ChooseActivityTypeDialogFragment.showDialog(getSupportFragmentManager(), activityType.getText().toString());
getSupportFragmentManager(),
ChooseActivityTypeDialogFragment.CHOOSE_ACTIVITY_TYPE_DIALOG_TAG);
} }
return true; return true;
} }
@@ -130,9 +129,7 @@ public class TrackEditActivity extends AbstractActivity implements ChooseActivit
@Override @Override
public boolean onKey(View v, int keyCode, KeyEvent event) { public boolean onKey(View v, int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) { if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) {
ChooseActivityTypeDialogFragment.newInstance(activityType.getText().toString()).show( ChooseActivityTypeDialogFragment.showDialog(getSupportFragmentManager(), activityType.getText().toString());
getSupportFragmentManager(),
ChooseActivityTypeDialogFragment.CHOOSE_ACTIVITY_TYPE_DIALOG_TAG);
} }
return true; return true;
} }
@@ -182,7 +179,7 @@ public class TrackEditActivity extends AbstractActivity implements ChooseActivit
} }
@Override @Override
public void onSaveInstanceState(Bundle outState) { public void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState); super.onSaveInstanceState(outState);
outState.putString(ICON_VALUE_KEY, iconValue); outState.putString(ICON_VALUE_KEY, iconValue);
} }
@@ -28,8 +28,6 @@ import android.location.LocationManager;
import android.os.Bundle; import android.os.Bundle;
import android.provider.Settings; import android.provider.Settings;
import android.util.Log; import android.util.Log;
import android.view.ContextMenu;
import android.view.ContextMenu.ContextMenuInfo;
import android.view.Gravity; import android.view.Gravity;
import android.view.KeyEvent; import android.view.KeyEvent;
import android.view.Menu; import android.view.Menu;
@@ -37,14 +35,12 @@ import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.view.View.OnClickListener; import android.view.View.OnClickListener;
import android.widget.AdapterView; import android.widget.AdapterView;
import android.widget.AdapterView.AdapterContextMenuInfo;
import android.widget.AdapterView.OnItemClickListener; import android.widget.AdapterView.OnItemClickListener;
import android.widget.ListView; import android.widget.ListView;
import android.widget.SearchView;
import android.widget.ShareActionProvider;
import android.widget.Toast; import android.widget.Toast;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.appcompat.widget.SearchView;
import androidx.core.app.ActivityCompat; import androidx.core.app.ActivityCompat;
import androidx.cursoradapter.widget.ResourceCursorAdapter; import androidx.cursoradapter.widget.ResourceCursorAdapter;
import androidx.loader.app.LoaderManager; import androidx.loader.app.LoaderManager;
@@ -60,9 +56,11 @@ import de.dennisguse.opentracks.fragments.ConfirmDeleteDialogFragment;
import de.dennisguse.opentracks.services.ITrackRecordingService; import de.dennisguse.opentracks.services.ITrackRecordingService;
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection; import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
import de.dennisguse.opentracks.settings.SettingsActivity; import de.dennisguse.opentracks.settings.SettingsActivity;
import de.dennisguse.opentracks.util.ActivityUtils;
import de.dennisguse.opentracks.util.IntentUtils; import de.dennisguse.opentracks.util.IntentUtils;
import de.dennisguse.opentracks.util.ListItemUtils; import de.dennisguse.opentracks.util.ListItemUtils;
import de.dennisguse.opentracks.util.PreferencesUtils; import de.dennisguse.opentracks.util.PreferencesUtils;
import de.dennisguse.opentracks.util.ServiceUtils;
import de.dennisguse.opentracks.util.StringUtils; import de.dennisguse.opentracks.util.StringUtils;
import de.dennisguse.opentracks.util.TrackIconUtils; import de.dennisguse.opentracks.util.TrackIconUtils;
import de.dennisguse.opentracks.util.TrackUtils; 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) { public void onPrepare(Menu menu, int[] positions, long[] trackIds, boolean showSelectAll) {
boolean isSingleSelection = trackIds.length == 1; boolean isSingleSelection = trackIds.length == 1;
MenuItem shareMenuItem = menu.findItem(R.id.list_context_menu_share);
ShareActionProvider shareActionProvider = (ShareActionProvider) shareMenuItem.getActionProvider();
shareActionProvider.setShareIntent(trackIds.length == 0 ? null : IntentUtils.newShareFileIntent(TrackListActivity.this, trackIds));
menu.findItem(R.id.list_context_menu_edit).setVisible(isSingleSelection); menu.findItem(R.id.list_context_menu_edit).setVisible(isSingleSelection);
menu.findItem(R.id.list_context_menu_select_all).setVisible(showSelectAll); menu.findItem(R.id.list_context_menu_select_all).setVisible(showSelectAll);
} }
@@ -148,7 +142,7 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
PreferencesUtils.getKey(TrackListActivity.this, R.string.recording_track_id_key))) { PreferencesUtils.getKey(TrackListActivity.this, R.string.recording_track_id_key))) {
recordingTrackId = PreferencesUtils.getLong( recordingTrackId = PreferencesUtils.getLong(
TrackListActivity.this, R.string.recording_track_id_key); TrackListActivity.this, R.string.recording_track_id_key);
if (key != null && recordingTrackId != PreferencesUtils.RECORDING_TRACK_ID_DEFAULT) { if (key != null && PreferencesUtils.isRecording(recordingTrackId)) {
trackRecordingServiceConnection.startAndBind(); trackRecordingServiceConnection.startAndBind();
} }
} }
@@ -164,7 +158,7 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
public void run() { public void run() {
TrackListActivity.this.invalidateOptionsMenu(); TrackListActivity.this.invalidateOptionsMenu();
LoaderManager.getInstance(TrackListActivity.this).restartLoader(0, null, loaderCallbacks); LoaderManager.getInstance(TrackListActivity.this).restartLoader(0, null, loaderCallbacks);
boolean isRecording = recordingTrackId != PreferencesUtils.RECORDING_TRACK_ID_DEFAULT; boolean isRecording = PreferencesUtils.isRecording(recordingTrackId);
trackController.update(isRecording, recordingTrackPaused); trackController.update(isRecording, recordingTrackPaused);
} }
}); });
@@ -175,7 +169,6 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
// Menu items // Menu items
private MenuItem searchMenuItem; private MenuItem searchMenuItem;
private MenuItem startGpsMenuItem; private MenuItem startGpsMenuItem;
private MenuItem aggregatedStatisticsMenuItem;
private MenuItem deleteAllMenuItem; private MenuItem deleteAllMenuItem;
private final OnClickListener stopListener = new OnClickListener() { private final OnClickListener stopListener = new OnClickListener() {
@@ -200,7 +193,7 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
runOnUiThread(new Runnable() { runOnUiThread(new Runnable() {
@Override @Override
public void run() { public void run() {
trackController.update(recordingTrackId != PreferencesUtils.RECORDING_TRACK_ID_DEFAULT, recordingTrackPaused); trackController.update(PreferencesUtils.isRecording(recordingTrackId), recordingTrackPaused);
} }
}); });
@@ -317,7 +310,8 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
} }
}; };
listView.setAdapter(resourceCursorAdapter); listView.setAdapter(resourceCursorAdapter);
AbstractListActivity.configureListViewContextualMenu(listView, contextualActionModeCallback);
ActivityUtils.configureListViewContextualMenu(listView, contextualActionModeCallback);
LoaderManager.getInstance(this).initLoader(0, null, loaderCallbacks); LoaderManager.getInstance(this).initLoader(0, null, loaderCallbacks);
showStartupDialogs(); showStartupDialogs();
@@ -339,8 +333,7 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
// Update UI // Update UI
this.invalidateOptionsMenu(); this.invalidateOptionsMenu();
LoaderManager.getInstance(this).restartLoader(0, null, loaderCallbacks); LoaderManager.getInstance(this).restartLoader(0, null, loaderCallbacks);
boolean isRecording = recordingTrackId != PreferencesUtils.RECORDING_TRACK_ID_DEFAULT; trackController.onResume(PreferencesUtils.isRecording(recordingTrackId), recordingTrackPaused);
trackController.onResume(isRecording, recordingTrackPaused);
} }
@Override @Override
@@ -380,10 +373,9 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
getMenuInflater().inflate(R.menu.track_list, menu); getMenuInflater().inflate(R.menu.track_list, menu);
searchMenuItem = menu.findItem(R.id.track_list_search); searchMenuItem = menu.findItem(R.id.track_list_search);
AbstractListActivity.configureSearchWidget(this, searchMenuItem, trackController); ActivityUtils.configureSearchWidget(this, searchMenuItem, trackController);
startGpsMenuItem = menu.findItem(R.id.track_list_start_gps); startGpsMenuItem = menu.findItem(R.id.track_list_start_gps);
aggregatedStatisticsMenuItem = menu.findItem(R.id.track_list_aggregated_statistics);
deleteAllMenuItem = menu.findItem(R.id.track_list_delete_all); deleteAllMenuItem = menu.findItem(R.id.track_list_delete_all);
return super.onCreateOptionsMenu(menu); return super.onCreateOptionsMenu(menu);
@@ -391,14 +383,13 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
@Override @Override
public boolean onPrepareOptionsMenu(Menu menu) { public boolean onPrepareOptionsMenu(Menu menu) {
boolean isGpsStarted = TrackRecordingServiceConnection.isRecordingServiceRunning(this); boolean isGpsStarted = ServiceUtils.isTrackRecordingServiceRunning(this);
boolean isRecording = recordingTrackId != PreferencesUtils.RECORDING_TRACK_ID_DEFAULT; boolean isRecording = PreferencesUtils.isRecording(recordingTrackId);
updateMenuItems(isGpsStarted, isRecording); updateMenuItems(isGpsStarted, isRecording);
View searchView = searchMenuItem.getActionView(); SearchView searchView = (SearchView) searchMenuItem.getActionView();
if (searchView instanceof SearchView) { searchView.setQuery("", false);
((SearchView) searchView).setQuery("", false);
}
return super.onPrepareOptionsMenu(menu); return super.onPrepareOptionsMenu(menu);
} }
@@ -412,7 +403,7 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
intent = new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS); intent = new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS);
startActivity(intent); startActivity(intent);
} else { } else {
startGps = !TrackRecordingServiceConnection.isRecordingServiceRunning(this); startGps = !ServiceUtils.isTrackRecordingServiceRunning(this);
// Show toast // Show toast
Toast toast = Toast.makeText(this, startGps ? R.string.gps_starting : R.string.gps_stopping, Toast.LENGTH_SHORT); Toast toast = Toast.makeText(this, startGps ? R.string.gps_starting : R.string.gps_stopping, Toast.LENGTH_SHORT);
@@ -439,10 +430,6 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
intent = IntentUtils.newIntent(this, MarkerListActivity.class); intent = IntentUtils.newIntent(this, MarkerListActivity.class);
startActivity(intent); startActivity(intent);
return true; return true;
case R.id.track_list_aggregated_statistics:
intent = IntentUtils.newIntent(this, AggregatedStatsActivity.class);
startActivity(intent);
return true;
case R.id.track_list_delete_all: case R.id.track_list_delete_all:
deleteTracks(new long[]{-1L}); deleteTracks(new long[]{-1L});
return true; return true;
@@ -454,24 +441,6 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
return super.onOptionsItemSelected(item); return super.onOptionsItemSelected(item);
} }
@Override
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
super.onCreateContextMenu(menu, v, menuInfo);
getMenuInflater().inflate(R.menu.list_context_menu, menu);
AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo;
contextualActionModeCallback.onPrepare(menu, new int[]{info.position}, new long[]{info.id}, false);
}
@Override
public boolean onContextItemSelected(MenuItem item) {
AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo();
if (handleContextItem(item.getItemId(), new long[]{info.id})) {
return true;
}
return super.onContextItemSelected(item);
}
@Override @Override
public boolean onKeyUp(int keyCode, KeyEvent event) { public boolean onKeyUp(int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_SEARCH && searchMenuItem != null) { if (keyCode == KeyEvent.KEYCODE_SEARCH && searchMenuItem != null) {
@@ -531,9 +500,6 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
startGpsMenuItem.setIcon(isGpsStarted ? R.drawable.ic_gps_fixed_24dp : R.drawable.ic_gps_off_24dp); startGpsMenuItem.setIcon(isGpsStarted ? R.drawable.ic_gps_fixed_24dp : R.drawable.ic_gps_off_24dp);
} }
} }
if (aggregatedStatisticsMenuItem != null) {
aggregatedStatisticsMenuItem.setVisible(hasTrack);
}
if (deleteAllMenuItem != null) { if (deleteAllMenuItem != null) {
deleteAllMenuItem.setVisible(hasTrack && !isRecording); deleteAllMenuItem.setVisible(hasTrack && !isRecording);
} }
@@ -562,12 +528,18 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
* @return true if handled. * @return true if handled.
*/ */
private boolean handleContextItem(int itemId, long[] trackIds) { private boolean handleContextItem(int itemId, long[] trackIds) {
Intent intent;
switch (itemId) { switch (itemId) {
case R.id.list_context_menu_show_on_map: case R.id.list_context_menu_show_on_map:
IntentUtils.showTrackOnMap(this, trackIds); IntentUtils.showTrackOnMap(this, trackIds);
return true; return true;
case R.id.list_context_menu_share:
intent = IntentUtils.newShareFileIntent(this, trackIds);
intent = Intent.createChooser(intent, null);
startActivity(intent);
return true;
case R.id.list_context_menu_edit: case R.id.list_context_menu_edit:
Intent intent = IntentUtils.newIntent(this, TrackEditActivity.class) intent = IntentUtils.newIntent(this, TrackEditActivity.class)
.putExtra(TrackEditActivity.EXTRA_TRACK_ID, trackIds[0]); .putExtra(TrackEditActivity.EXTRA_TRACK_ID, trackIds[0]);
startActivity(intent); startActivity(intent);
return true; return true;
@@ -208,7 +208,7 @@ public class TrackDataHub implements DataSourceListener {
* Returns true if the selected track is recording. * Returns true if the selected track is recording.
*/ */
public boolean isSelectedTrackRecording() { public boolean isSelectedTrackRecording() {
return selectedTrackId == recordingTrackId && recordingTrackId != PreferencesUtils.RECORDING_TRACK_ID_DEFAULT; return selectedTrackId == recordingTrackId && PreferencesUtils.isRecording(recordingTrackId);
} }
/** /**
@@ -2,7 +2,7 @@ package de.dennisguse.opentracks.content.sensor;
public final class SensorDataSet { public final class SensorDataSet {
public static final float DATA_UNAVAILABLE = Integer.MAX_VALUE; public static final float DATA_UNAVAILABLE = Float.NaN;
//TODO It might be necessary to consider: sensor if sensor was connected as well. //TODO It might be necessary to consider: sensor if sensor was connected as well.
public String sensorName; public String sensorName;
@@ -40,7 +40,7 @@ public final class SensorDataSet {
} }
public boolean hasHeartRate() { public boolean hasHeartRate() {
return heartRate != DATA_UNAVAILABLE; return !Float.isNaN(heartRate) && heartRate > 0;
} }
public float getHeartRate() { public float getHeartRate() {
@@ -48,7 +48,7 @@ public final class SensorDataSet {
} }
public boolean hasCadence() { public boolean hasCadence() {
return cadence != DATA_UNAVAILABLE; return !Float.isNaN(cadence);
} }
public float getCadence() { public float getCadence() {
@@ -56,7 +56,7 @@ public final class SensorDataSet {
} }
public boolean hasPower() { public boolean hasPower() {
return power != DATA_UNAVAILABLE; return !Float.isNaN(power);
} }
public float getPower() { public float getPower() {
@@ -77,7 +77,7 @@ public final class SensorDataSet {
} }
public boolean hasBatteryLevel() { public boolean hasBatteryLevel() {
return batteryLevel != DATA_UNAVAILABLE; return !Float.isNaN(batteryLevel);
} }
public float getBatteryLevel() { public float getBatteryLevel() {
@@ -8,6 +8,7 @@ public enum SensorState {
NONE, NONE,
CONNECTING, CONNECTING,
CONNECTED, CONNECTED,
DISCONNECTING,
DISCONNECTED; DISCONNECTED;
public static String getStateAsString(SensorState state, Context c) { public static String getStateAsString(SensorState state, Context c) {
@@ -18,6 +19,8 @@ public enum SensorState {
return c.getString(R.string.sensor_state_connecting); return c.getString(R.string.sensor_state_connecting);
case CONNECTED: case CONNECTED:
return c.getString(R.string.sensor_state_connected); return c.getString(R.string.sensor_state_connected);
case DISCONNECTING:
return c.getString(R.string.sensor_state_disconnected);
case DISCONNECTED: case DISCONNECTED:
return c.getString(R.string.sensor_state_disconnected); return c.getString(R.string.sensor_state_disconnected);
default: default:
@@ -352,8 +352,8 @@ public class ChartFragment extends Fragment implements TrackDataListener {
} }
/** /**
* Resumes the trackDataHub. Needs to be synchronized because trackDataHub can * Resumes the trackDataHub.
* be accessed by multiple threads. * Needs to be synchronized because trackDataHub can be accessed by multiple threads.
*/ */
private synchronized void resumeTrackDataHub() { private synchronized void resumeTrackDataHub() {
trackDataHub = ((TrackDetailActivity) getActivity()).getTrackDataHub(); trackDataHub = ((TrackDetailActivity) getActivity()).getTrackDataHub();
@@ -363,8 +363,8 @@ public class ChartFragment extends Fragment implements TrackDataListener {
} }
/** /**
* Pauses the trackDataHub. Needs to be synchronized because trackDataHub can * Pauses the trackDataHub.
* be accessed by multiple threads. * Needs to be synchronized because trackDataHub can be accessed by multiple threads.
*/ */
private synchronized void pauseTrackDataHub() { private synchronized void pauseTrackDataHub() {
trackDataHub.unregisterTrackDataListener(this); trackDataHub.unregisterTrackDataListener(this);
@@ -372,16 +372,16 @@ public class ChartFragment extends Fragment implements TrackDataListener {
} }
/** /**
* Returns true if the selected track is recording. Needs to be synchronized * Returns true if the selected track is recording.
* because trackDataHub can be accessed by multiple threads. * Needs to be synchronized because trackDataHub can be accessed by multiple threads.
*/ */
private synchronized boolean isSelectedTrackRecording() { private synchronized boolean isSelectedTrackRecording() {
return trackDataHub != null && trackDataHub.isSelectedTrackRecording(); return trackDataHub != null && trackDataHub.isSelectedTrackRecording();
} }
/** /**
* Reloads the trackDataHub. Needs to be synchronized because trackDataHub can * Reloads the trackDataHub.
* be accessed by multiple threads. * Needs to be synchronized because trackDataHub can be accessed by multiple threads.
*/ */
private synchronized void reloadTrackDataHub() { private synchronized void reloadTrackDataHub() {
if (trackDataHub != null) { if (trackDataHub != null) {
@@ -389,18 +389,12 @@ public class ChartFragment extends Fragment implements TrackDataListener {
} }
} }
/**
* To zoom in.
*/
private void zoomIn() { private void zoomIn() {
chartView.zoomIn(); chartView.zoomIn();
zoomControls.setIsZoomInEnabled(chartView.canZoomIn()); zoomControls.setIsZoomInEnabled(chartView.canZoomIn());
zoomControls.setIsZoomOutEnabled(chartView.canZoomOut()); zoomControls.setIsZoomOutEnabled(chartView.canZoomOut());
} }
/**
* To zoom out.
*/
private void zoomOut() { private void zoomOut() {
chartView.zoomOut(); chartView.zoomOut();
zoomControls.setIsZoomInEnabled(chartView.canZoomIn()); zoomControls.setIsZoomInEnabled(chartView.canZoomIn());
@@ -438,9 +432,6 @@ public class ChartFragment extends Fragment implements TrackDataListener {
double elevation = Double.NaN; double elevation = Double.NaN;
double speed = Double.NaN; double speed = Double.NaN;
double pace = Double.NaN; double pace = Double.NaN;
double heartRate = Double.NaN;
double cadence = Double.NaN;
double power = Double.NaN;
if (tripStatisticsUpdater != null) { if (tripStatisticsUpdater != null) {
tripStatisticsUpdater.addLocation(location, recordingDistanceInterval); tripStatisticsUpdater.addLocation(location, recordingDistanceInterval);
@@ -466,6 +457,10 @@ public class ChartFragment extends Fragment implements TrackDataListener {
} }
pace = speed == 0 ? 0.0 : 60.0 / speed; pace = speed == 0 ? 0.0 : 60.0 / speed;
} }
double heartRate = Double.NaN;
double cadence = Double.NaN;
double power = Double.NaN;
if (location instanceof SensorDataSetLocation && ((SensorDataSetLocation) location).getSensorDataSet() != null) { if (location instanceof SensorDataSetLocation && ((SensorDataSetLocation) location).getSensorDataSet() != null) {
SensorDataSet sensorDataSet = ((SensorDataSetLocation) location).getSensorDataSet(); SensorDataSet sensorDataSet = ((SensorDataSetLocation) location).getSensorDataSet();
if (sensorDataSet.hasHeartRate()) { if (sensorDataSet.hasHeartRate()) {
@@ -479,6 +474,7 @@ public class ChartFragment extends Fragment implements TrackDataListener {
} }
} }
//TODO: Is related to ChartView.ELEVATION_SERIES etc.
if (data != null) { if (data != null) {
data[0] = timeOrDistance; data[0] = timeOrDistance;
data[1] = elevation; data[1] = elevation;
@@ -1,34 +1,18 @@
/*
* Copyright 2013 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package de.dennisguse.opentracks.fragments; package de.dennisguse.opentracks.fragments;
import android.app.AlertDialog; import android.app.Activity;
import android.app.Dialog; import android.app.Dialog;
import android.content.Context; import android.content.Context;
import android.content.DialogInterface;
import android.os.Bundle; import android.os.Bundle;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView; import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.GridView; import android.widget.GridView;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.fragment.app.DialogFragment; import androidx.fragment.app.DialogFragment;
import androidx.fragment.app.FragmentManager;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@@ -38,56 +22,13 @@ import de.dennisguse.opentracks.util.TrackIconUtils;
/** /**
* A DialogFragment to choose an activity type. * A DialogFragment to choose an activity type.
*
* @author apoorvn
*/ */
public class ChooseActivityTypeDialogFragment extends DialogFragment { public class ChooseActivityTypeDialogFragment extends DialogFragment implements AdapterView.OnItemClickListener {
public static final String CHOOSE_ACTIVITY_TYPE_DIALOG_TAG = "chooseActivityType"; private static final String CHOOSE_ACTIVITY_TYPE_DIALOG_TAG = "chooseActivityType";
private static final String KEY_CATEGORY = "category";
private ChooseActivityTypeCaller caller;
public static ChooseActivityTypeDialogFragment newInstance(String category) { public static void showDialog(FragmentManager fragmentManager, String preselectedCategory) {
Bundle bundle = new Bundle(); new ChooseActivityTypeDialogFragment(preselectedCategory).show(fragmentManager, ChooseActivityTypeDialogFragment.CHOOSE_ACTIVITY_TYPE_DIALOG_TAG);
bundle.putString(KEY_CATEGORY, category);
ChooseActivityTypeDialogFragment fragment = new ChooseActivityTypeDialogFragment();
fragment.setArguments(bundle);
return fragment;
}
public static Dialog getDialog(final 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;
} }
private static int getPosition(Context context, String category) { private static int getPosition(Context context, String category) {
@@ -99,32 +40,79 @@ public class ChooseActivityTypeDialogFragment extends DialogFragment {
return TrackIconUtils.getAllIconValues().indexOf(iconValue); return TrackIconUtils.getAllIconValues().indexOf(iconValue);
} }
@Override private String preselectedCategory;
public void onAttach(@NonNull Context context) {
super.onAttach(context); private ChooseActivityTypeCaller chooseActivityTypeCaller;
try {
caller = (ChooseActivityTypeCaller) context; private ChooseActivityTypeDialogFragment(String preselectedCategory) {
} catch (ClassCastException e) { this.preselectedCategory = preselectedCategory;
throw new ClassCastException(context + " must implement " + ChooseActivityTypeCaller.class.getSimpleName());
}
} }
@NonNull @NonNull
@Override @Override
public Dialog onCreateDialog(Bundle savedInstanceState) { 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;
} }
/** /**
* Interface for caller of this dialog fragment. * TODO: Only used by obsolete Androidx preferences. Remove when onAttach(Context) is called.
* */
* @author apoorvn @Override
public void onAttach(@NonNull Activity activity) {
super.onAttach(activity);
try {
chooseActivityTypeCaller = (ChooseActivityTypeCaller) activity;
} catch (ClassCastException e) {
throw new ClassCastException(activity + " must implement " + ChooseActivityTypeCaller.class.getSimpleName());
}
}
public void onAttach(@NonNull Context context) {
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 chooseActivityTypeCaller of this dialog fragment.
*/ */
public interface ChooseActivityTypeCaller { public interface ChooseActivityTypeCaller {
/**
* Called when choose activity type is done.
*/
void onChooseActivityTypeDone(String iconValue); void onChooseActivityTypeDone(String iconValue);
} }
} }
@@ -23,6 +23,7 @@ import android.os.Bundle;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.fragment.app.DialogFragment; import androidx.fragment.app.DialogFragment;
import androidx.fragment.app.FragmentManager;
import de.dennisguse.opentracks.R; import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.util.DialogUtils; import de.dennisguse.opentracks.util.DialogUtils;
@@ -34,22 +35,21 @@ import de.dennisguse.opentracks.util.DialogUtils;
*/ */
public class ConfirmDeleteDialogFragment extends DialogFragment { public class ConfirmDeleteDialogFragment extends DialogFragment {
public static final String CONFIRM_DELETE_DIALOG_TAG = "confirmDeleteDialog"; private static final String CONFIRM_DELETE_DIALOG_TAG = "confirmDeleteDialog";
private static final String KEY_TRACK_IDS = "trackIds";
private ConfirmDeleteCaller caller; private ConfirmDeleteCaller caller;
private long[] trackIds;
public ConfirmDeleteDialogFragment(long[] trackIds) {
this.trackIds = trackIds;
}
/** /**
* Create a new instance. * Create a new instance.
* *
* @param trackIds list of track ids to delete. To delete all, set to size 1 with trackIds[0] == -1L * @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) { public static void showDialog(FragmentManager fragmentManager, long[] trackIds) {
Bundle bundle = new Bundle(); new ConfirmDeleteDialogFragment(trackIds).show(fragmentManager, CONFIRM_DELETE_DIALOG_TAG);
bundle.putLongArray(KEY_TRACK_IDS, trackIds);
ConfirmDeleteDialogFragment deleteTrackDialogFragment = new ConfirmDeleteDialogFragment();
deleteTrackDialogFragment.setArguments(bundle);
return deleteTrackDialogFragment;
} }
@Override @Override
@@ -62,11 +62,9 @@ public class ConfirmDeleteDialogFragment extends DialogFragment {
} }
} }
@Override @Override
@NonNull @NonNull
public Dialog onCreateDialog(Bundle savedInstanceState) { public Dialog onCreateDialog(Bundle savedInstanceState) {
final long[] trackIds = getArguments().getLongArray(KEY_TRACK_IDS);
int titleId; int titleId;
int messageId; int messageId;
titleId = trackIds.length > 1 ? R.string.generic_delete_selected_confirm_title : R.string.track_delete_one_confirm_title; 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.annotation.NonNull;
import androidx.fragment.app.DialogFragment; import androidx.fragment.app.DialogFragment;
import androidx.fragment.app.FragmentActivity; import androidx.fragment.app.FragmentActivity;
import androidx.fragment.app.FragmentManager;
import de.dennisguse.opentracks.R; import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.ContentProviderUtils; import de.dennisguse.opentracks.content.ContentProviderUtils;
@@ -37,17 +38,17 @@ import de.dennisguse.opentracks.util.DialogUtils;
*/ */
public class DeleteMarkerDialogFragment extends DialogFragment { public class DeleteMarkerDialogFragment extends DialogFragment {
public static final String DELETE_MARKER_DIALOG_TAG = "deleteMarkerDialog"; private static final String DELETE_MARKER_DIALOG_TAG = "deleteMarkerDialog";
private static final String KEY_MARKER_IDS = "markerIds"; private long[] markerIds;
private DeleteMarkerCaller caller; private DeleteMarkerCaller caller;
public static DeleteMarkerDialogFragment newInstance(long[] markerIds) { public DeleteMarkerDialogFragment(long[] markerIds) {
Bundle bundle = new Bundle(); this.markerIds = markerIds;
bundle.putLongArray(KEY_MARKER_IDS, markerIds); }
DeleteMarkerDialogFragment deleteMarkerDialogFragment = new DeleteMarkerDialogFragment(); public static void showDialog(FragmentManager fragmentManager, long[] markerIds) {
deleteMarkerDialogFragment.setArguments(bundle); new DeleteMarkerDialogFragment(markerIds).show(fragmentManager, DELETE_MARKER_DIALOG_TAG);
return deleteMarkerDialogFragment;
} }
@Override @Override
@@ -64,7 +65,6 @@ public class DeleteMarkerDialogFragment extends DialogFragment {
@NonNull @NonNull
public Dialog onCreateDialog(Bundle savedInstanceState) { public Dialog onCreateDialog(Bundle savedInstanceState) {
final FragmentActivity fragmentActivity = getActivity(); final FragmentActivity fragmentActivity = getActivity();
final long[] markerIds = getArguments().getLongArray(KEY_MARKER_IDS);
int titleId; int titleId;
int messageId; int messageId;
if (markerIds.length == 1 && markerIds[0] == -1L) { if (markerIds.length == 1 && markerIds[0] == -1L) {
@@ -81,8 +81,7 @@ public class DeleteMarkerDialogFragment extends DialogFragment {
new Thread(new Runnable() { new Thread(new Runnable() {
@Override @Override
public void run() { public void run() {
ContentProviderUtils contentProviderUtils = ContentProviderUtils.Factory.get( ContentProviderUtils contentProviderUtils = ContentProviderUtils.Factory.get(fragmentActivity);
fragmentActivity);
for (long markerId : markerIds) { for (long markerId : markerIds) {
contentProviderUtils.deleteWaypoint(fragmentActivity, contentProviderUtils.deleteWaypoint(fragmentActivity,
markerId, new DescriptionGeneratorImpl(fragmentActivity)); markerId, new DescriptionGeneratorImpl(fragmentActivity));
@@ -25,6 +25,7 @@ import android.os.Bundle;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.fragment.app.DialogFragment; import androidx.fragment.app.DialogFragment;
import androidx.fragment.app.FragmentManager;
import java.util.Locale; import java.util.Locale;
@@ -39,19 +40,19 @@ import de.dennisguse.opentracks.util.FileUtils;
*/ */
public class FileTypeDialogFragment extends DialogFragment { public class FileTypeDialogFragment extends DialogFragment {
public static final String FILE_TYPE_DIALOG_TAG = "fileType"; private static final String FILE_TYPE_DIALOG_TAG = "fileType";
private static final String KEY_TITLE_ID = "titleId"; private int titleId;
private static final String KEY_OPTION_ID = "optionId";
private FileTypeCaller caller; private FileTypeCaller caller;
private int optionId;
public static FileTypeDialogFragment newInstance(int titleId, int optionId) { public FileTypeDialogFragment(int titleId, int optionId) {
Bundle bundle = new Bundle(); this.titleId = titleId;
bundle.putInt(KEY_TITLE_ID, titleId); this.optionId = optionId;
bundle.putInt(KEY_OPTION_ID, optionId); }
FileTypeDialogFragment fileTypeDialogFragment = new FileTypeDialogFragment(); public static void showDialog(FragmentManager fragmentManager, int titleId, int optionId) {
fileTypeDialogFragment.setArguments(bundle); new FileTypeDialogFragment(titleId, optionId).show(fragmentManager, FILE_TYPE_DIALOG_TAG);
return fileTypeDialogFragment;
} }
@Override @Override
@@ -67,8 +68,6 @@ public class FileTypeDialogFragment extends DialogFragment {
@Override @Override
@NonNull @NonNull
public Dialog onCreateDialog(Bundle savedInstanceState) { 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}; 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]; String[] choices = new String[trackFileFormats.length];
for (int i = 0; i < choices.length; i++) { for (int i = 0; i < choices.length; i++) {
@@ -96,7 +95,7 @@ public class FileTypeDialogFragment extends DialogFragment {
} }
@Override @Override
public void onCancel(DialogInterface dialog) { public void onCancel(@NonNull DialogInterface dialog) {
onDismissed(); onDismissed();
} }
@@ -24,6 +24,7 @@ import android.os.Bundle;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.fragment.app.DialogFragment; import androidx.fragment.app.DialogFragment;
import androidx.fragment.app.FragmentActivity; import androidx.fragment.app.FragmentActivity;
import androidx.fragment.app.FragmentManager;
import de.dennisguse.opentracks.R; import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.util.PreferencesUtils; import de.dennisguse.opentracks.util.PreferencesUtils;
@@ -36,21 +37,19 @@ import de.dennisguse.opentracks.util.StringUtils;
*/ */
public class FrequencyDialogFragment extends DialogFragment { public class FrequencyDialogFragment extends DialogFragment {
public static final String FREQUENCY_DIALOG_TAG = "frequencyDialog"; private static final String FREQUENCY_DIALOG_TAG = "frequencyDialog";
private int preferenceId;
private int defaultValue;
private int titleId;
private static final String KEY_PREFERENCE_ID = "preferenceId"; public FrequencyDialogFragment(int preferenceId, int defaultValue, int titleId) {
private static final String KEY_DEFAULT_VALUE = "defaultValue"; this.preferenceId = preferenceId;
private static final String KEY_TITLE_ID = "titleId"; this.defaultValue = defaultValue;
this.titleId = titleId;
}
public static FrequencyDialogFragment newInstance(int preferenceId, int defaultValue, int titleId) { public static void showDialog(FragmentManager fragmentManager, int preferenceId, int defaultValue, int titleId) {
Bundle bundle = new Bundle(); new FrequencyDialogFragment(preferenceId, defaultValue, titleId).show(fragmentManager, FREQUENCY_DIALOG_TAG);
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 @Override
@@ -58,9 +57,6 @@ public class FrequencyDialogFragment extends DialogFragment {
public Dialog onCreateDialog(Bundle savedInstanceState) { public Dialog onCreateDialog(Bundle savedInstanceState) {
FragmentActivity fragmentActivity = getActivity(); 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); int frequencyValue = PreferencesUtils.getInt(fragmentActivity, preferenceId, defaultValue);
return new AlertDialog.Builder(fragmentActivity).setPositiveButton( return new AlertDialog.Builder(fragmentActivity).setPositiveButton(
@@ -192,8 +192,7 @@ public class MarkerDetailFragment extends Fragment {
startActivity(intent); startActivity(intent);
return true; return true;
case R.id.marker_detail_delete: case R.id.marker_detail_delete:
DeleteMarkerDialogFragment.newInstance(new long[]{markerId}) DeleteMarkerDialogFragment.showDialog(getChildFragmentManager(), new long[]{markerId});
.show(getChildFragmentManager(), DeleteMarkerDialogFragment.DELETE_MARKER_DIALOG_TAG);
return true; return true;
default: default:
return super.onOptionsItemSelected(item); return super.onOptionsItemSelected(item);
@@ -43,7 +43,6 @@ import de.dennisguse.opentracks.content.Waypoint;
import de.dennisguse.opentracks.content.sensor.SensorDataSet; import de.dennisguse.opentracks.content.sensor.SensorDataSet;
import de.dennisguse.opentracks.services.ITrackRecordingService; import de.dennisguse.opentracks.services.ITrackRecordingService;
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection; import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
import de.dennisguse.opentracks.services.sensors.RemoteSensorManager;
import de.dennisguse.opentracks.stats.TripStatistics; import de.dennisguse.opentracks.stats.TripStatistics;
import de.dennisguse.opentracks.util.LocationUtils; import de.dennisguse.opentracks.util.LocationUtils;
import de.dennisguse.opentracks.util.PreferencesUtils; import de.dennisguse.opentracks.util.PreferencesUtils;
@@ -59,7 +58,7 @@ import de.dennisguse.opentracks.util.UnitConversions;
*/ */
public class StatsFragment extends Fragment implements TrackDataListener { public class StatsFragment extends Fragment implements TrackDataListener {
public static final String STATS_FRAGMENT_TAG = "statsFragment"; public static final String STATS_FRAGMENT_TAG = StatsFragment.class.getSimpleName();
private TrackDataHub trackDataHub; private TrackDataHub trackDataHub;
private Handler handlerUpdateUI; private Handler handlerUpdateUI;
@@ -94,8 +93,6 @@ public class StatsFragment extends Fragment implements TrackDataListener {
public void onActivityCreated(Bundle savedInstanceState) { public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState); super.onActivityCreated(savedInstanceState);
handlerUpdateUI = new Handler(); handlerUpdateUI = new Handler();
trackRecordingServiceConnection = new TrackRecordingServiceConnection(getContext(), null);
trackRecordingServiceConnection.startConnection(getContext());
Spinner activityTypeIcon = getView().findViewById(R.id.stats_activity_type_icon); Spinner activityTypeIcon = getView().findViewById(R.id.stats_activity_type_icon);
activityTypeIcon.setAdapter(TrackIconUtils.getIconSpinnerAdapter(getActivity(), "")); activityTypeIcon.setAdapter(TrackIconUtils.getIconSpinnerAdapter(getActivity(), ""));
@@ -123,6 +120,10 @@ public class StatsFragment extends Fragment implements TrackDataListener {
public void onResume() { public void onResume() {
super.onResume(); super.onResume();
resumeTrackDataHub(); resumeTrackDataHub();
trackRecordingServiceConnection = new TrackRecordingServiceConnection(getContext(), null);
trackRecordingServiceConnection.startConnection(getContext());
updateUi(getActivity()); updateUi(getActivity());
if (isSelectedTrackRecording()) { if (isSelectedTrackRecording()) {
handlerUpdateUI.post(updateUIeachSecond); handlerUpdateUI.post(updateUIeachSecond);
@@ -140,6 +141,7 @@ public class StatsFragment extends Fragment implements TrackDataListener {
public void onStop() { public void onStop() {
super.onStop(); super.onStop();
trackRecordingServiceConnection.unbind(); trackRecordingServiceConnection.unbind();
trackRecordingServiceConnection = null;
} }
@Override @Override
@@ -298,7 +300,7 @@ public class StatsFragment extends Fragment implements TrackDataListener {
} }
/** /**
* Tries to fetch most recent {@link SensorDataSet} {@link RemoteSensorManager}. * Tries to fetch most recent {@link SensorDataSet} {@link de.dennisguse.opentracks.services.sensors.BluetoothRemoteSensorManager}.
*/ */
private void updateSensorDataUI() { private void updateSensorDataUI() {
ITrackRecordingService trackRecordingService = trackRecordingServiceConnection.getServiceIfBound(); ITrackRecordingService trackRecordingService = trackRecordingServiceConnection.getServiceIfBound();
@@ -84,9 +84,7 @@ public class ExportActivity extends FragmentActivity implements FileTypeDialogFr
} }
private void fileTypeDialogStart() { private void fileTypeDialogStart() {
FileTypeDialogFragment FileTypeDialogFragment.showDialog(getSupportFragmentManager(), R.string.export_all_title, R.string.export_all_option);
.newInstance(R.string.export_all_title, R.string.export_all_option)
.show(getSupportFragmentManager(), FileTypeDialogFragment.FILE_TYPE_DIALOG_TAG);
} }
@Override @Override
@@ -99,7 +99,7 @@ public class ExportAsyncTask extends AsyncTask<Void, Integer, Boolean> {
protected Boolean doInBackground(Void... params) { protected Boolean doInBackground(Void... params) {
try { try {
Thread.currentThread().setPriority(Thread.MAX_PRIORITY); Thread.currentThread().setPriority(Thread.MAX_PRIORITY);
boolean isRecording = PreferencesUtils.getLong(exportActivity, R.string.recording_track_id_key) != PreferencesUtils.RECORDING_TRACK_ID_DEFAULT; boolean isRecording = PreferencesUtils.isRecording(exportActivity);
boolean isPaused = PreferencesUtils.getBoolean(exportActivity, R.string.recording_track_paused_key, PreferencesUtils.RECORDING_TRACK_PAUSED_DEFAULT); boolean isPaused = PreferencesUtils.getBoolean(exportActivity, R.string.recording_track_paused_key, PreferencesUtils.RECORDING_TRACK_PAUSED_DEFAULT);
// Get the wake lock if not recording or paused // Get the wake lock if not recording or paused
if (!isRecording || isPaused) { if (!isRecording || isPaused) {
@@ -49,9 +49,10 @@ public class KmlTrackWriter implements TrackWriter {
private static final String END_STYLE = "end"; private static final String END_STYLE = "end";
private static final String TRACK_STYLE = "track"; private static final String TRACK_STYLE = "track";
private static final String SCHEMA_ID = "schema"; 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 SENSOR_TYPE_CADENCE = "cadence";
private static final String POWER = "power"; 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 WAYPOINT_ICON = "http://maps.google.com/mapfiles/kml/pushpin/blue-pushpin.png";
private static final String STATISTICS_ICON = "http://maps.google.com/mapfiles/kml/pushpin/ylw-pushpin.png"; private static final String 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> powerList = new ArrayList<>();
private List<Float> cadenceList = new ArrayList<>(); private List<Float> cadenceList = new ArrayList<>();
private List<Float> heartRateList = new ArrayList<>(); private List<Float> heartRateList = new ArrayList<>();
private boolean hasPower;
private boolean hasCadence;
private boolean hasHeartRate;
private Location startLocation; private Location startLocation;
@@ -132,9 +130,9 @@ public class KmlTrackWriter implements TrackWriter {
printWriter.println("<Schema id=\"" + SCHEMA_ID + "\">"); printWriter.println("<Schema id=\"" + SCHEMA_ID + "\">");
if (exportSensorData) { if (exportSensorData) {
writeSensorStyle(POWER, context.getString(R.string.description_sensor_power)); writeSensorStyle(SENSOR_TYPE_POWER, context.getString(R.string.description_sensor_power));
writeSensorStyle(CADENCE, context.getString(R.string.description_sensor_cadence)); writeSensorStyle(SENSOR_TYPE_CADENCE, context.getString(R.string.description_sensor_cadence));
writeSensorStyle(HEART_RATE, context.getString(R.string.description_sensor_heart_rate)); writeSensorStyle(SENSOR_TYPE_HEART_RATE, context.getString(R.string.description_sensor_heart_rate));
} }
printWriter.println("</Schema>"); printWriter.println("</Schema>");
} }
@@ -235,9 +233,6 @@ public class KmlTrackWriter implements TrackWriter {
public void writeOpenSegment() { public void writeOpenSegment() {
if (printWriter != null) { if (printWriter != null) {
printWriter.println("<gx:Track>"); printWriter.println("<gx:Track>");
hasPower = false;
hasCadence = false;
hasHeartRate = false;
powerList.clear(); powerList.clear();
cadenceList.clear(); cadenceList.clear();
heartRateList.clear(); heartRateList.clear();
@@ -250,14 +245,14 @@ public class KmlTrackWriter implements TrackWriter {
printWriter.println("<ExtendedData>"); printWriter.println("<ExtendedData>");
printWriter.println("<SchemaData schemaUrl=\"#" + SCHEMA_ID + "\">"); printWriter.println("<SchemaData schemaUrl=\"#" + SCHEMA_ID + "\">");
if (exportSensorData) { if (exportSensorData) {
if (hasPower) { if (powerList.size() > 0) {
writeSensorData(powerList, POWER); writeSensorData(powerList, SENSOR_TYPE_POWER);
} }
if (hasCadence) { if (cadenceList.size() > 0) {
writeSensorData(cadenceList, CADENCE); writeSensorData(cadenceList, SENSOR_TYPE_CADENCE);
} }
if (hasHeartRate) { if (heartRateList.size() > 0) {
writeSensorData(heartRateList, HEART_RATE); writeSensorData(heartRateList, SENSOR_TYPE_HEART_RATE);
} }
} }
printWriter.println("</SchemaData>"); printWriter.println("</SchemaData>");
@@ -466,11 +461,11 @@ public class KmlTrackWriter implements TrackWriter {
* Writes a sensor style. * Writes a sensor style.
* *
* @param name the name of the sesnor * @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) { private void writeSensorStyle(String name, String sensorType) {
printWriter.println("<gx:SimpleArrayField name=\"" + name + "\" type=\"int\">"); printWriter.println("<gx:SimpleArrayField name=\"" + name + "\" type=\"float\">");
printWriter.println("<displayName>" + StringUtils.formatCData(displayName) + "</displayName>"); printWriter.println("<displayName>" + StringUtils.formatCData(sensorType) + "</displayName>");
printWriter.println("</gx:SimpleArrayField>"); printWriter.println("</gx:SimpleArrayField>");
} }
} }
@@ -384,8 +384,7 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
* @param message the message * @param message the message
*/ */
protected String createErrorMessage(String message) { protected String createErrorMessage(String message) {
return String.format(Locale.US, "Parsing error at line: %d column: %d. %s", return String.format(Locale.US, "Parsing error at line: %d column: %d. %s", locator.getLineNumber(), locator.getColumnNumber(), message);
locator.getLineNumber(), locator.getColumnNumber(), message);
} }
/** /**
@@ -85,9 +85,7 @@ public class ImportActivity extends FragmentActivity implements FileTypeDialogFr
} }
private void fileTypeDialogStart() { private void fileTypeDialogStart() {
FileTypeDialogFragment FileTypeDialogFragment.showDialog(getSupportFragmentManager(), R.string.import_selection_title, R.string.import_selection_option);
.newInstance(R.string.import_selection_title, R.string.import_selection_option)
.show(getSupportFragmentManager(), FileTypeDialogFragment.FILE_TYPE_DIALOG_TAG);
} }
@Override @Override
@@ -103,7 +103,7 @@ public class ImportAsyncTask extends AsyncTask<Void, Integer, Boolean> {
try { try {
Thread.currentThread().setPriority(Thread.MAX_PRIORITY); Thread.currentThread().setPriority(Thread.MAX_PRIORITY);
// Get the wake lock if not recording or paused // 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 isRecording = PreferencesUtils.isRecording(importActivity);
boolean isPaused = PreferencesUtils.getBoolean(importActivity, R.string.recording_track_paused_key, PreferencesUtils.RECORDING_TRACK_PAUSED_DEFAULT); boolean isPaused = PreferencesUtils.getBoolean(importActivity, R.string.recording_track_paused_key, PreferencesUtils.RECORDING_TRACK_PAUSED_DEFAULT);
if (!isRecording || isPaused) { if (!isRecording || isPaused) {
wakeLock = SystemUtils.acquireWakeLock(importActivity, wakeLock); wakeLock = SystemUtils.acquireWakeLock(importActivity, wakeLock);
@@ -19,6 +19,7 @@ package de.dennisguse.opentracks.io.file.importer;
import android.content.Context; import android.content.Context;
import android.location.Location; import android.location.Location;
import android.net.Uri; import android.net.Uri;
import android.util.Log;
import androidx.annotation.VisibleForTesting; import androidx.annotation.VisibleForTesting;
@@ -39,6 +40,8 @@ import de.dennisguse.opentracks.content.sensor.SensorDataSet;
*/ */
public class KmlFileTrackImporter extends AbstractFileTrackImporter { public class KmlFileTrackImporter extends AbstractFileTrackImporter {
private static final String TAG = KmlFileTrackImporter.class.getSimpleName();
private static final String CADENCE = "cadence"; private static final String CADENCE = "cadence";
private static final String HEART_RATE = "heart_rate"; private static final String HEART_RATE = "heart_rate";
private static final String POWER = "power"; private static final String POWER = "power";
@@ -65,11 +68,11 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter {
private static final String ATTRIBUTE_NAME = "name"; private static final String ATTRIBUTE_NAME = "name";
private boolean trackStarted = false; private boolean trackStarted = false;
private String sensorName; private String sensorType;
private ArrayList<Location> locationList; private ArrayList<Location> locationList;
private ArrayList<Integer> cadenceList; private ArrayList<Float> cadenceList;
private ArrayList<Integer> heartRateList; private ArrayList<Float> heartRateList;
private ArrayList<Integer> powerList; private ArrayList<Float> powerList;
/** /**
* Constructor. * Constructor.
@@ -179,6 +182,10 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter {
* On waypoint end. * On waypoint end.
*/ */
private void onWaypointEnd() throws SAXException { private void onWaypointEnd() throws SAXException {
if (waypointType == null) {
return;
}
// Add a waypoint if the waypointType matches // Add a waypoint if the waypointType matches
WaypointType type; WaypointType type;
switch (waypointType) { switch (waypointType) {
@@ -229,20 +236,30 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter {
*/ */
private void onTrackSegmentEnd() { private void onTrackSegmentEnd() {
// Close a track segment by inserting the segment locations // 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++) { for (int i = 0; i < locationList.size(); i++) {
Location location = locationList.get(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); insertTrackPoint(location);
} else { } 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())); SensorDataSetLocation sensorDataSetLocation = new SensorDataSetLocation(location, new SensorDataSet(heartrate, cadence, power, SensorDataSet.DATA_UNAVAILABLE, location.getTime()));
insertTrackPoint(sensorDataSetLocation); insertTrackPoint(sensorDataSetLocation);
} }
@@ -277,7 +294,7 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter {
* On sensor data start. gx:SimpleArrayData start tag. * On sensor data start. gx:SimpleArrayData start tag.
*/ */
private void onSensorDataStart(Attributes attributes) { 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("")) { if (content.equals("")) {
return; return;
} }
int value; float value;
try { try {
value = Integer.parseInt(content); value = Float.valueOf(content);
} catch (NumberFormatException e) { } catch (NumberFormatException e) {
throw new SAXException(createErrorMessage("Unable to parse gx:value:" + content), e); throw new SAXException(createErrorMessage("Unable to parse gx:value:" + content), e);
} }
if (POWER.equals(sensorName)) { switch (sensorType) {
case POWER:
powerList.add(value); powerList.add(value);
} else if (HEART_RATE.equals(sensorName)) { break;
case HEART_RATE:
heartRateList.add(value); heartRateList.add(value);
} else if (CADENCE.equals(sensorName)) { break;
case CADENCE:
cadenceList.add(value); cadenceList.add(value);
break;
default:
Log.w(TAG, "Data from sensor " + sensorType + " is not (yet) supported.");
} }
} }
} }
@@ -57,7 +57,7 @@ import de.dennisguse.opentracks.content.Waypoint;
import de.dennisguse.opentracks.content.Waypoint.WaypointType; import de.dennisguse.opentracks.content.Waypoint.WaypointType;
import de.dennisguse.opentracks.content.WaypointCreationRequest; import de.dennisguse.opentracks.content.WaypointCreationRequest;
import de.dennisguse.opentracks.content.sensor.SensorDataSet; import de.dennisguse.opentracks.content.sensor.SensorDataSet;
import de.dennisguse.opentracks.services.sensors.RemoteSensorManager; import de.dennisguse.opentracks.services.sensors.BluetoothRemoteSensorManager;
import de.dennisguse.opentracks.services.sensors.RemoteSensorManagerFactory; import de.dennisguse.opentracks.services.sensors.RemoteSensorManagerFactory;
import de.dennisguse.opentracks.services.tasks.AnnouncementPeriodicTaskFactory; import de.dennisguse.opentracks.services.tasks.AnnouncementPeriodicTaskFactory;
import de.dennisguse.opentracks.services.tasks.PeriodicTaskExecutor; import de.dennisguse.opentracks.services.tasks.PeriodicTaskExecutor;
@@ -119,10 +119,9 @@ public class TrackRecordingService extends Service {
@Override @Override
public void onSharedPreferenceChanged(SharedPreferences preferences, String key) { public void onSharedPreferenceChanged(SharedPreferences preferences, String key) {
if (key == null || key.equals(PreferencesUtils.getKey(context, R.string.recording_track_id_key))) { 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);
// Only through the TrackRecordingService can one stop a recording and set the recordingTrackId to -1L. // Only through the TrackRecordingService can one stop a recording and set the recordingTrackId to -1L.
if (trackId != PreferencesUtils.RECORDING_TRACK_ID_DEFAULT) { if (PreferencesUtils.isRecording(context)) {
recordingTrackId = trackId; recordingTrackId = PreferencesUtils.getLong(context, R.string.recording_track_id_key);
} }
} }
if (key == null || key.equals(PreferencesUtils.getKey(context, R.string.recording_track_paused_key))) { if (key == null || key.equals(PreferencesUtils.getKey(context, R.string.recording_track_paused_key))) {
@@ -170,7 +169,7 @@ public class TrackRecordingService extends Service {
}; };
private TripStatisticsUpdater markerTripStatisticsUpdater; private TripStatisticsUpdater markerTripStatisticsUpdater;
private WakeLock wakeLock; private WakeLock wakeLock;
private RemoteSensorManager remoteSensorManager; private BluetoothRemoteSensorManager remoteSensorManager;
private Location lastLocation; private Location lastLocation;
private boolean currentSegmentHasLocation; private boolean currentSegmentHasLocation;
private boolean isIdle; // true if idle private boolean isIdle; // true if idle
@@ -310,7 +309,7 @@ public class TrackRecordingService extends Service {
* Returns true if the service is recording. * Returns true if the service is recording.
*/ */
public boolean isRecording() { public boolean isRecording() {
return recordingTrackId != PreferencesUtils.RECORDING_TRACK_ID_DEFAULT; return PreferencesUtils.isRecording(recordingTrackId);
} }
/** /**
@@ -447,7 +446,8 @@ public class TrackRecordingService extends Service {
*/ */
private void handleStartCommand(Intent intent, int startId) { private void handleStartCommand(Intent intent, int startId) {
// Check if the service is called to resume track (from phone reboot) // Check if the service is called to resume track (from phone reboot)
if (intent != null && intent.getBooleanExtra(RESUME_TRACK_EXTRA_NAME, false) && !shouldResumeTrack()) { boolean resumeTrackRequested = intent.getBooleanExtra(RESUME_TRACK_EXTRA_NAME, false);
if (resumeTrackRequested && !shouldResumeTrack()) {
Log.i(TAG, "Stop resume track."); Log.i(TAG, "Stop resume track.");
updateRecordingState(PreferencesUtils.RECORDING_TRACK_ID_DEFAULT, true); updateRecordingState(PreferencesUtils.RECORDING_TRACK_ID_DEFAULT, true);
stopSelfResult(startId); stopSelfResult(startId);
@@ -854,7 +854,7 @@ public class TrackRecordingService extends Service {
*/ */
private void insertLocation(Track track, Location location, Location lastValidTrackPoint) { private void insertLocation(Track track, Location location, Location lastValidTrackPoint) {
if (location == null) { if (location == null) {
Log.w(TAG, "Ignore insertLocation. loation is null."); Log.w(TAG, "Ignore insertLocation. location is null.");
return; return;
} }
// Do not insert if inserted already // Do not insert if inserted already
@@ -16,7 +16,6 @@
package de.dennisguse.opentracks.services; package de.dennisguse.opentracks.services;
import android.app.ActivityManager;
import android.content.ComponentName; import android.content.ComponentName;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
@@ -29,14 +28,13 @@ import android.widget.Toast;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import java.util.List;
import de.dennisguse.opentracks.BuildConfig; import de.dennisguse.opentracks.BuildConfig;
import de.dennisguse.opentracks.R; import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.TrackEditActivity; import de.dennisguse.opentracks.TrackEditActivity;
import de.dennisguse.opentracks.content.WaypointCreationRequest; import de.dennisguse.opentracks.content.WaypointCreationRequest;
import de.dennisguse.opentracks.util.IntentUtils; import de.dennisguse.opentracks.util.IntentUtils;
import de.dennisguse.opentracks.util.PreferencesUtils; import de.dennisguse.opentracks.util.PreferencesUtils;
import de.dennisguse.opentracks.util.ServiceUtils;
/** /**
* Wrapper for the track recording service. * Wrapper for the track recording service.
@@ -154,7 +152,7 @@ public class TrackRecordingServiceConnection implements ServiceConnection, Death
return; return;
} }
if (!startIfNeeded && !isRecordingServiceRunning(context)) { if (!startIfNeeded && !ServiceUtils.isTrackRecordingServiceRunning(context)) {
Log.d(TAG, "Service is not started. Not binding it."); Log.d(TAG, "Service is not started. Not binding it.");
return; return;
} }
@@ -169,31 +167,6 @@ public class TrackRecordingServiceConnection implements ServiceConnection, Death
context.bindService(new Intent(context, TrackRecordingService.class), this, flags); context.bindService(new Intent(context, TrackRecordingService.class), this, flags);
} }
/**
* Returns true if the recording service is running.
*
* @param context the current context
*/
@Deprecated
public static boolean isRecordingServiceRunning(Context context) {
ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
if (activityManager == null) {
return false;
}
//TODO This approach is deprecated as of API level 26 and should be replaced.
List<ActivityManager.RunningServiceInfo> services = activityManager.getRunningServices(Integer.MAX_VALUE);
for (ActivityManager.RunningServiceInfo serviceInfo : services) {
ComponentName componentName = serviceInfo.service;
String serviceName = componentName.getClassName();
if (TrackRecordingService.class.getName().equals(serviceName)) {
return true;
}
}
return false;
}
/** /**
* Resumes the track recording service connection. * Resumes the track recording service connection.
* *
@@ -201,7 +174,7 @@ public class TrackRecordingServiceConnection implements ServiceConnection, Death
*/ */
public void startConnection(@NonNull Context context) { public void startConnection(@NonNull Context context) {
bindIfStarted(); bindIfStarted();
if (!isRecordingServiceRunning(context)) { if (!ServiceUtils.isTrackRecordingServiceRunning(context)) {
resetRecordingState(context); resetRecordingState(context);
} }
} }
@@ -226,37 +199,14 @@ public class TrackRecordingServiceConnection implements ServiceConnection, Death
} }
} }
/** private static void resetRecordingState(Context context) {
* Stops the recording. if (PreferencesUtils.isRecording(context)) {
* PreferencesUtils.setLong(context, R.string.recording_track_id_key, PreferencesUtils.RECORDING_TRACK_ID_DEFAULT);
* @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 { boolean recordingTrackPaused = PreferencesUtils.getBoolean(context, R.string.recording_track_paused_key, PreferencesUtils.RECORDING_TRACK_PAUSED_DEFAULT);
trackRecordingService.endCurrentTrack(); if (!recordingTrackPaused) {
PreferencesUtils.setBoolean(context, R.string.recording_track_paused_key, PreferencesUtils.RECORDING_TRACK_PAUSED_DEFAULT);
} }
} catch (Exception e) {
//TODO What exception are we catching here? Should be removed...
Log.e(TAG, "Unable to stop recording.", e);
}
}
unbindAndStop();
} }
/** /**
@@ -284,14 +234,36 @@ public class TrackRecordingServiceConnection implements ServiceConnection, Death
return -1L; return -1L;
} }
private static void resetRecordingState(Context context) { /**
* 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); long recordingTrackId = PreferencesUtils.getLong(context, R.string.recording_track_id_key);
if (recordingTrackId != PreferencesUtils.RECORDING_TRACK_ID_DEFAULT) { trackRecordingService.endCurrentTrack();
PreferencesUtils.setLong(context, R.string.recording_track_id_key, PreferencesUtils.RECORDING_TRACK_ID_DEFAULT); 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);
} }
boolean recordingTrackPaused = PreferencesUtils.getBoolean(context, R.string.recording_track_paused_key, PreferencesUtils.RECORDING_TRACK_PAUSED_DEFAULT); } else {
if (!recordingTrackPaused) { trackRecordingService.endCurrentTrack();
PreferencesUtils.setBoolean(context, R.string.recording_track_paused_key, PreferencesUtils.RECORDING_TRACK_PAUSED_DEFAULT); }
} catch (Exception e) {
//TODO What exception are we catching here? Should be removed...
Log.e(TAG, "Unable to stop recording.", e);
} }
} }
unbindAndStop();
}
} }
@@ -23,9 +23,7 @@ import android.bluetooth.BluetoothGattCharacteristic;
import android.bluetooth.BluetoothGattDescriptor; import android.bluetooth.BluetoothGattDescriptor;
import android.bluetooth.BluetoothProfile; import android.bluetooth.BluetoothProfile;
import android.content.Context; import android.content.Context;
import android.os.Bundle;
import android.os.Handler; import android.os.Handler;
import android.os.Message;
import android.util.Log; import android.util.Log;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
@@ -34,6 +32,7 @@ import java.util.UUID;
import de.dennisguse.opentracks.content.sensor.SensorDataSet; import de.dennisguse.opentracks.content.sensor.SensorDataSet;
import de.dennisguse.opentracks.content.sensor.SensorState; import de.dennisguse.opentracks.content.sensor.SensorState;
import de.dennisguse.opentracks.util.BluetoothUtils;
/** /**
* Manages connection to Bluetooth LE heart rate monitor. * Manages connection to Bluetooth LE heart rate monitor.
@@ -42,10 +41,10 @@ import de.dennisguse.opentracks.content.sensor.SensorState;
public class BluetoothConnectionManager { public class BluetoothConnectionManager {
// Message types sent to handler // Message types sent to handler
static final int MESSAGE_DEVICE_NAME = 1; static final int MESSAGE_CONNECTING = 1;
static final int MESSAGE_READ = 2; static final int MESSAGE_CONNECTED = 2;
static final int MESSAGE_READ = 3;
static final String KEY_DEVICE_NAME = "device_name"; static final int MESSAGE_DISCONNECTED = 4;
private static final UUID HEART_RATE_SERVICE_UUID = new UUID(0x180D00001000L, 0x800000805f9b34fbL); private static final UUID HEART_RATE_SERVICE_UUID = new UUID(0x180D00001000L, 0x800000805f9b34fbL);
private static final UUID HEART_RATE_MEASUREMENT_CHAR_UUID = new UUID(0x2A3700001000L, 0x800000805f9b34fbL); private static final UUID HEART_RATE_MEASUREMENT_CHAR_UUID = new UUID(0x2A3700001000L, 0x800000805f9b34fbL);
@@ -58,22 +57,36 @@ public class BluetoothConnectionManager {
private SensorState sensorState; private SensorState sensorState;
private BluetoothGatt bluetoothGatt;
private BluetoothDevice bluetoothDevice;
private BluetoothGattCallback connectCallback = new BluetoothGattCallback() { private BluetoothGattCallback connectCallback = new BluetoothGattCallback() {
@Override @Override
public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) { public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
if (newState == BluetoothProfile.STATE_CONNECTED) { switch (newState) {
gatt.discoverServices(); case BluetoothProfile.STATE_CONNECTING:
Log.d(TAG, "Connecting to sensor: " + gatt.getDevice());
setState(SensorState.CONNECTING);
handler.obtainMessage(MESSAGE_CONNECTING, gatt.getDevice().getName()).sendToTarget();
case BluetoothProfile.STATE_CONNECTED:
Log.d(TAG, "Connected to sensor: " + gatt.getDevice());
setState(SensorState.CONNECTED); setState(SensorState.CONNECTED);
//Inform about status change gatt.discoverServices();
Message message = handler.obtainMessage(MESSAGE_DEVICE_NAME);
Bundle bundle = new Bundle(); handler.obtainMessage(MESSAGE_CONNECTED, gatt.getDevice().getName()).sendToTarget();
bundle.putString(KEY_DEVICE_NAME, gatt.getDevice().getName()); break;
message.setData(bundle); case BluetoothProfile.STATE_DISCONNECTING:
handler.sendMessage(message); Log.d(TAG, "Disconnecting from sensor: " + gatt.getDevice());
return; setState(SensorState.DISCONNECTING);
case BluetoothProfile.STATE_DISCONNECTED:
Log.d(TAG, "Disconnected from sensor: " + gatt.getDevice());
setState(SensorState.DISCONNECTED);
handler.obtainMessage(MESSAGE_DISCONNECTED, gatt.getDevice().getName()).sendToTarget();
} }
Log.d(TAG, "Could not connect to bluetooth sensor: " + gatt.getDevice());
} }
@Override @Override
@@ -91,10 +104,11 @@ public class BluetoothConnectionManager {
@Override @Override
public void onCharacteristicChanged(BluetoothGatt gatt, @NonNull BluetoothGattCharacteristic characteristic) { public void onCharacteristicChanged(BluetoothGatt gatt, @NonNull BluetoothGattCharacteristic characteristic) {
int heartRate = BluetoothLEUtils.parseHeartRate(characteristic); int heartRate = BluetoothUtils.parseHeartRate(characteristic);
String deviceName = gatt.getDevice().getName();
Log.d(TAG, "Received heart beat rate: " + heartRate); Log.d(TAG, "Received heart beat rate " + deviceName + ": " + heartRate);
SensorDataSet sensorDataSet = new SensorDataSet(heartRate, gatt.getDevice().getName(), gatt.getDevice().getAddress()); SensorDataSet sensorDataSet = new SensorDataSet(heartRate, deviceName, gatt.getDevice().getAddress());
handler.obtainMessage(MESSAGE_READ, sensorDataSet).sendToTarget(); handler.obtainMessage(MESSAGE_READ, sensorDataSet).sendToTarget();
} }
}; };
@@ -104,45 +118,42 @@ public class BluetoothConnectionManager {
* *
* @param handler a handler for sending messages back to the UI activity * @param handler a handler for sending messages back to the UI activity
*/ */
BluetoothConnectionManager(Context context, Handler handler) { BluetoothConnectionManager(@NonNull Context context, @NonNull BluetoothDevice bluetoothDevice, @NonNull Handler handler) {
this.context = context; this.context = context;
this.bluetoothDevice = bluetoothDevice;
this.handler = handler; this.handler = handler;
this.sensorState = SensorState.NONE; this.sensorState = SensorState.NONE;
} }
/** public synchronized void connect() {
* Gets the sensor state. if (bluetoothGatt != null) {
*/ Log.w(TAG, "Already connected; ignoring.");
}
Log.d(TAG, "Connecting to: " + bluetoothDevice);
setState(SensorState.CONNECTING);
bluetoothGatt = bluetoothDevice.connectGatt(this.context, true, this.connectCallback);
}
public synchronized void disconnect() {
if (bluetoothGatt == null) {
Log.w(TAG, "Cannot disconnect if not connected.");
return;
}
bluetoothGatt.close();
bluetoothGatt = null;
}
public synchronized boolean isSameBluetoothDevice(String address) {
return this.bluetoothDevice.getAddress().equals(address);
}
synchronized SensorState getSensorState() { synchronized SensorState getSensorState() {
return sensorState; return sensorState;
} }
/**
* Sets the sensor state.
*
* @param sensorState the sensor state
*/
private synchronized void setState(SensorState sensorState) { private synchronized void setState(SensorState sensorState) {
this.sensorState = sensorState; this.sensorState = sensorState;
} }
/**
* Resets the bluetooth connection manager.
*/
public synchronized void reset() {
//TODO Disconnect
setState(SensorState.NONE);
}
/**
* Connects to a bluetooth device.
*
* @param bluetoothDevice the bluetooth device
*/
public synchronized void connect(BluetoothDevice bluetoothDevice) {
Log.d(TAG, "connect to: " + bluetoothDevice);
bluetoothDevice.connectGatt(this.context, false, this.connectCallback);
setState(SensorState.CONNECTING);
}
} }
@@ -1,17 +0,0 @@
package de.dennisguse.opentracks.services.sensors;
import android.bluetooth.BluetoothGattCharacteristic;
final class BluetoothLEUtils {
private BluetoothLEUtils() {
}
static int parseHeartRate(BluetoothGattCharacteristic characteristic) {
//DOCUMENTATION https://www.bluetooth.com/specifications/gatt/characteristics/
byte[] raw = characteristic.getValue();
int index = ((raw[0] & 0x1) == 1) ? 2 : 1;
int format = (index == 1) ? BluetoothGattCharacteristic.FORMAT_UINT8 : BluetoothGattCharacteristic.FORMAT_UINT16;
return characteristic.getIntValue(format, index);
}
}
@@ -19,26 +19,28 @@ package de.dennisguse.opentracks.services.sensors;
import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothDevice;
import android.content.Context; import android.content.Context;
import android.content.SharedPreferences;
import android.os.Handler; import android.os.Handler;
import android.os.Looper; import android.os.Looper;
import android.os.Message; import android.os.Message;
import android.util.Log; import android.util.Log;
import android.widget.Toast;
import java.util.ArrayList; import java.util.ArrayList;
import de.dennisguse.opentracks.R; import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.sensor.SensorDataSet; import de.dennisguse.opentracks.content.sensor.SensorDataSet;
import de.dennisguse.opentracks.content.sensor.SensorState;
import de.dennisguse.opentracks.util.PreferencesUtils; import de.dennisguse.opentracks.util.PreferencesUtils;
import de.dennisguse.opentracks.util.UnitConversions; import de.dennisguse.opentracks.util.UnitConversions;
/** /**
* Bluetooth sensor manager. * Bluetooth sensor manager.
* TODO: Handle a BluetoothGatt.STATE_DISCONNECTED
* *
* @author Sandor Dornbush * @author Sandor Dornbush
*/ */
public class BluetoothRemoteSensorManager extends RemoteSensorManager { public class BluetoothRemoteSensorManager {
public static final long MAX_SENSOR_DATE_SET_AGE_MS = 5000;
private static final String TAG = BluetoothConnectionManager.class.getSimpleName(); private static final String TAG = BluetoothConnectionManager.class.getSimpleName();
@@ -46,17 +48,21 @@ public class BluetoothRemoteSensorManager extends RemoteSensorManager {
private final Context context; private final Context context;
private final BluetoothConnectionManager bluetoothConnectionManager; private final SharedPreferences sharedPreferences;
private SensorDataSet sensorDataSet = null;
// Handler that gets information back from the bluetoothConnectionManager // Handler that gets information back from the bluetoothConnectionManager
private final Handler messageHandler = new Handler(Looper.getMainLooper()) { private final Handler messageHandler = new Handler(Looper.getMainLooper()) {
@Override @Override
public void handleMessage(Message message) { public void handleMessage(Message message) {
String toastMessage;
switch (message.what) { switch (message.what) {
case BluetoothConnectionManager.MESSAGE_DEVICE_NAME: case BluetoothConnectionManager.MESSAGE_CONNECTING:
String deviceName = message.getData().getString(BluetoothConnectionManager.KEY_DEVICE_NAME); //Ignore for now.
toastMessage = context.getString(R.string.settings_sensor_connecting, message.obj);
Toast.makeText(context, toastMessage, Toast.LENGTH_LONG).show();
break;
case BluetoothConnectionManager.MESSAGE_CONNECTED:
toastMessage = context.getString(R.string.settings_sensor_connected, message.obj);
Toast.makeText(context, toastMessage, Toast.LENGTH_LONG).show();
break; break;
case BluetoothConnectionManager.MESSAGE_READ: case BluetoothConnectionManager.MESSAGE_READ:
if (!(message.obj instanceof SensorDataSet)) { if (!(message.obj instanceof SensorDataSet)) {
@@ -66,18 +72,44 @@ public class BluetoothRemoteSensorManager extends RemoteSensorManager {
sensorDataSet = (SensorDataSet) message.obj; sensorDataSet = (SensorDataSet) message.obj;
} }
break; break;
case BluetoothConnectionManager.MESSAGE_DISCONNECTED:
toastMessage = context.getString(R.string.settings_sensor_disconnected, message.obj);
Toast.makeText(context, toastMessage, Toast.LENGTH_LONG).show();
break;
default: default:
Log.e(TAG, "Got an undefined case. Please check.");
break; break;
} }
} }
}; };
private final SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener = new SharedPreferences.OnSharedPreferenceChangeListener() {
@Override
public void onSharedPreferenceChanged(SharedPreferences preferences, String key) {
if (bluetoothConnectionManager != null && key == null || key.equals(PreferencesUtils.getKey(context, R.string.bluetooth_sensor_key))) {
String address = PreferencesUtils.getString(context, R.string.bluetooth_sensor_key, PreferencesUtils.BLUETOOTH_SENSOR_DEFAULT);
if (address.equals(PreferencesUtils.BLUETOOTH_SENSOR_DEFAULT)) {
stop();
return;
}
if (bluetoothConnectionManager.isSameBluetoothDevice(address)) {
return;
}
disconnect();
startCurrentSensor();
}
}
};
private SensorDataSet sensorDataSet = null;
private BluetoothConnectionManager bluetoothConnectionManager;
/** /**
* @param context the context * @param context the context
*/ */
BluetoothRemoteSensorManager(Context context) { BluetoothRemoteSensorManager(Context context) {
this.context = context; this.context = context;
bluetoothConnectionManager = new BluetoothConnectionManager(context, messageHandler); sharedPreferences = PreferencesUtils.getSharedPreferences(context);
} }
private static BluetoothAdapter getDefaultBluetoothAdapter() { private static BluetoothAdapter getDefaultBluetoothAdapter() {
@@ -117,13 +149,34 @@ public class BluetoothRemoteSensorManager extends RemoteSensorManager {
return adapters.get(0); return adapters.get(0);
} }
@Override public void start() {
sharedPreferences.registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
startCurrentSensor();
}
public void stop() {
disconnect();
sharedPreferences.unregisterOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
}
public boolean isEnabled() { public boolean isEnabled() {
return bluetoothAdapter != null && bluetoothAdapter.isEnabled(); return bluetoothAdapter != null && bluetoothAdapter.isEnabled();
} }
@Override public SensorDataSet getSensorDataSet() {
protected void setUpChannel() { return sensorDataSet;
}
public boolean isSensorDataSetValid() {
SensorDataSet sensorDataSet = getSensorDataSet();
if (sensorDataSet == null) {
return false;
}
return sensorDataSet.isRecent(MAX_SENSOR_DATE_SET_AGE_MS);
}
private void startCurrentSensor() {
if (!isEnabled()) { if (!isEnabled()) {
Log.w(TAG, "Bluetooth not enabled."); Log.w(TAG, "Bluetooth not enabled.");
return; return;
@@ -134,30 +187,30 @@ public class BluetoothRemoteSensorManager extends RemoteSensorManager {
Log.w(TAG, "No bluetooth address."); Log.w(TAG, "No bluetooth address.");
return; return;
} }
Log.w(TAG, "Connecting to bluetooth address: " + address); Log.i(TAG, "Connecting to bluetooth address: " + address);
BluetoothDevice device; BluetoothDevice device;
try { try {
device = bluetoothAdapter.getRemoteDevice(address); device = bluetoothAdapter.getRemoteDevice(address);
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
Log.d(TAG, "Unable to get remote device for: " + address, e); Log.w(TAG, "Unable to get remote device for: " + address, e);
String toastMessage = context.getString(R.string.sensor_not_known, address);
Toast.makeText(context, toastMessage, Toast.LENGTH_LONG).show();
return; return;
} }
bluetoothConnectionManager.connect(device);
disconnect();
bluetoothConnectionManager = new BluetoothConnectionManager(context, device, messageHandler);
bluetoothConnectionManager.connect();
} }
@Override private void disconnect() {
protected void tearDownChannel() { if (bluetoothConnectionManager != null) {
bluetoothConnectionManager.reset(); bluetoothConnectionManager.disconnect();
} bluetoothConnectionManager = null;
}
@Override
public SensorState getSensorState() {
return bluetoothConnectionManager.getSensorState();
}
@Override
public SensorDataSet getSensorDataSet() {
return sensorDataSet;
} }
} }
@@ -1,135 +0,0 @@
/*
* Copyright 2010 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package de.dennisguse.opentracks.services.sensors;
import android.util.Log;
import java.util.Timer;
import java.util.TimerTask;
import de.dennisguse.opentracks.content.sensor.SensorDataSet;
import de.dennisguse.opentracks.content.sensor.SensorState;
/**
* Manage the connection to a remote sensor.
*
* @author Sandor Dornbush
*/
public abstract class RemoteSensorManager {
public static final long MAX_SENSOR_DATE_SET_AGE = 5000;
public static final long MAX_SENSOR_STATE_AGE = 20000;
private static final String TAG = RemoteSensorManager.class.getSimpleName();
private static final int RETRY_PERIOD = 20000;
private SensorState sensorState = SensorState.NONE;
private long sensorStateTimestamp = System.currentTimeMillis();
private TimerTask timerTask;
private Timer timer;
/**
* Returns true if the sensor is enabled.
*/
public abstract boolean isEnabled();
/**
* Sets up the sensor channel.
*/
protected abstract void setUpChannel();
/**
* Tears down the sensor channel.
*/
protected abstract void tearDownChannel();
/**
* Gets the sensor data set.
*/
public abstract SensorDataSet getSensorDataSet();
/**
* Starts the sensor.
*/
public void startSensor() {
setUpChannel();
timerTask = new TimerTask() {
@Override
public void run() {
switch (getSensorState()) {
case CONNECTING:
if (System.currentTimeMillis() - sensorStateTimestamp > MAX_SENSOR_STATE_AGE) {
Log.i(TAG, "Retry setUpChannel");
setUpChannel();
}
break;
case DISCONNECTED:
setUpChannel();
break;
default: // NONE, CONNECTED or SENDING
}
}
};
timer = new Timer(RemoteSensorManager.class.getSimpleName());
timer.schedule(timerTask, RETRY_PERIOD, RETRY_PERIOD);
}
/**
* Stops the sensor.
*/
public void stopSensor() {
if (timerTask != null) {
timerTask.cancel();
timerTask = null;
}
if (timer != null) {
timer.cancel();
timer.purge();
timer = null;
}
tearDownChannel();
}
/**
* Gets the sensor state.
*/
public SensorState getSensorState() {
return sensorState;
}
/**
* Sets the sensor state.
*
* @param sensorState the sensor state
*/
public void setSensorState(SensorState sensorState) {
sensorStateTimestamp = System.currentTimeMillis();
this.sensorState = sensorState;
}
/**
* Returns true if the sensor data set is valid.
*/
public boolean isSensorDataSetValid() {
SensorDataSet sensorDataSet = getSensorDataSet();
if (sensorDataSet == null) {
return false;
}
return sensorDataSet.isRecent(MAX_SENSOR_DATE_SET_AGE);
}
}
@@ -19,16 +19,14 @@ package de.dennisguse.opentracks.services.sensors;
import android.content.Context; import android.content.Context;
/** /**
* A factory of {@link RemoteSensorManager}. * A factory of {@link BluetoothRemoteSensorManager}.
* *
* @author Sandor Dornbush * @author Sandor Dornbush
*/ */
public class RemoteSensorManagerFactory { public class RemoteSensorManagerFactory {
private static RemoteSensorManager remoteSensorManagerSystem = null; private static BluetoothRemoteSensorManager remoteSensorManagerSystem = null;
//TODO Check if still needed? Is there a missing features?
private static RemoteSensorManager remoteSensorManagerTemporary = null;
private RemoteSensorManagerFactory() { private RemoteSensorManagerFactory() {
} }
@@ -38,11 +36,10 @@ public class RemoteSensorManagerFactory {
* *
* @param context the context * @param context the context
*/ */
public static RemoteSensorManager getSystemSensorManager(Context context) { public static BluetoothRemoteSensorManager getSystemSensorManager(Context context) {
releaseSensorManagerTemporary();
releaseSystemSensorManager(); releaseSystemSensorManager();
remoteSensorManagerSystem = getSensorManager(context); remoteSensorManagerSystem = getSensorManager(context);
remoteSensorManagerSystem.startSensor(); remoteSensorManagerSystem.start();
return remoteSensorManagerSystem; return remoteSensorManagerSystem;
} }
@@ -51,40 +48,17 @@ public class RemoteSensorManagerFactory {
*/ */
public static void releaseSystemSensorManager() { public static void releaseSystemSensorManager() {
if (remoteSensorManagerSystem != null) { if (remoteSensorManagerSystem != null) {
remoteSensorManagerSystem.stopSensor(); remoteSensorManagerSystem.stop();
} }
remoteSensorManagerSystem = null; 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. * Gets the sensor manager.
* *
* @param context the context * @param context the context
*/ */
private static RemoteSensorManager getSensorManager(Context context) { private static BluetoothRemoteSensorManager getSensorManager(Context context) {
return new BluetoothRemoteSensorManager(context); return new BluetoothRemoteSensorManager(context);
} }
} }
@@ -17,8 +17,7 @@
package de.dennisguse.opentracks.settings; package de.dennisguse.opentracks.settings;
import android.content.Context; import android.content.Context;
import android.os.Parcelable; import android.os.Bundle;
import android.preference.DialogPreference;
import android.util.AttributeSet; import android.util.AttributeSet;
import android.view.KeyEvent; import android.view.KeyEvent;
import android.view.MotionEvent; import android.view.MotionEvent;
@@ -28,7 +27,11 @@ import android.widget.ArrayAdapter;
import android.widget.AutoCompleteTextView; import android.widget.AutoCompleteTextView;
import android.widget.Spinner; import android.widget.Spinner;
import androidx.preference.DialogPreference;
import androidx.preference.PreferenceDialogFragmentCompat;
import de.dennisguse.opentracks.R; import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.fragments.ChooseActivityTypeDialogFragment;
import de.dennisguse.opentracks.util.PreferencesUtils; import de.dennisguse.opentracks.util.PreferencesUtils;
import de.dennisguse.opentracks.util.TrackIconUtils; import de.dennisguse.opentracks.util.TrackIconUtils;
@@ -39,35 +42,59 @@ import de.dennisguse.opentracks.util.TrackIconUtils;
*/ */
public class ActivityTypePreference extends DialogPreference { public class ActivityTypePreference extends DialogPreference {
private RecordingSettingsActivity recordingSettingsActivity; private SummaryProvider<DialogPreference> summaryProvider = new SummaryProvider<DialogPreference>() {
private AutoCompleteTextView textView; @Override
private Spinner spinner; public CharSequence provideSummary(DialogPreference preference) {
String defaultActivity = PreferencesUtils.getString(ActivityTypePreference.this.getContext(), R.string.default_activity_key, PreferencesUtils.DEFAULT_ACTIVITY_DEFAULT);
return defaultActivity != null && !defaultActivity.equals(PreferencesUtils.DEFAULT_ACTIVITY_DEFAULT) ? defaultActivity : getContext().getString(R.string.value_unknown);
}
};
public ActivityTypePreference(Context context, AttributeSet attrs) { public ActivityTypePreference(Context context, AttributeSet attrs) {
super(context, attrs); super(context, attrs);
setDialogLayoutResource(R.layout.activity_type_preference); setDialogLayoutResource(R.layout.preference_activity_type);
setPositiveButtonText(android.R.string.ok); setPositiveButtonText(android.R.string.ok);
setNegativeButtonText(android.R.string.cancel); setNegativeButtonText(android.R.string.cancel);
setDialogIcon(null); setDialogIcon(null);
setPersistent(false); setPersistent(true);
//TODO: for some reason the summary is not updated; after changing it. Enable summaryprovider, when fixed.
// setSummaryProvider(summaryProvider);
} }
@Override @Override
protected View onCreateDialogView() { public int getDialogLayoutResource() {
View view = super.onCreateDialogView(); return R.layout.preference_activity_type;
}
public static class ActivityPreferenceDialog extends PreferenceDialogFragmentCompat {
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); textView = view.findViewById(R.id.activity_type_preference_text_view);
spinner = view.findViewById(R.id.activity_type_preference_spinner); String category = PreferencesUtils.getString(context, R.string.default_activity_key, PreferencesUtils.DEFAULT_ACTIVITY_DEFAULT);
String category = PreferencesUtils.getString(
getContext(), R.string.default_activity_key, PreferencesUtils.DEFAULT_ACTIVITY_DEFAULT);
textView.setText(category); textView.setText(category);
ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(context, R.array.activity_types, android.R.layout.simple_dropdown_item_1line);
ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(getContext(), R.array.activity_types, android.R.layout.simple_dropdown_item_1line);
textView.setAdapter(adapter); textView.setAdapter(adapter);
textView.setOnItemClickListener(new AdapterView.OnItemClickListener() { textView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override @Override
public void onItemClick(AdapterView<?> parent, View v, int position, long id) { public void onItemClick(AdapterView<?> parent, View v, int position, long id) {
String iconValue = TrackIconUtils.getIconValue(getContext(), (String) textView.getAdapter().getItem(position)); String iconValue = TrackIconUtils.getIconValue(context, (String) textView.getAdapter().getItem(position));
TrackIconUtils.setIconSpinner(spinner, iconValue); TrackIconUtils.setIconSpinner(spinner, iconValue);
} }
}); });
@@ -75,19 +102,20 @@ public class ActivityTypePreference extends DialogPreference {
@Override @Override
public void onFocusChange(View v, boolean hasFocus) { public void onFocusChange(View v, boolean hasFocus) {
if (!hasFocus) { if (!hasFocus) {
String iconValue = TrackIconUtils.getIconValue(getContext(), textView.getText().toString()); String iconValue = TrackIconUtils.getIconValue(context, textView.getText().toString());
TrackIconUtils.setIconSpinner(spinner, iconValue); TrackIconUtils.setIconSpinner(spinner, iconValue);
} }
} }
}); });
String iconValue = TrackIconUtils.getIconValue(getContext(), category); String iconValue = TrackIconUtils.getIconValue(context, category);
spinner.setAdapter(TrackIconUtils.getIconSpinnerAdapter(getContext(), iconValue)); spinner = view.findViewById(R.id.activity_type_preference_spinner);
spinner.setAdapter(TrackIconUtils.getIconSpinnerAdapter(context, iconValue));
spinner.setOnTouchListener(new View.OnTouchListener() { spinner.setOnTouchListener(new View.OnTouchListener() {
@Override @Override
public boolean onTouch(View v, MotionEvent event) { public boolean onTouch(View v, MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_UP) { if (event.getAction() == MotionEvent.ACTION_UP) {
recordingSettingsActivity.showChooseActivityTypeDialog(); showIconSelectDialog();
} }
return true; return true;
} }
@@ -96,49 +124,37 @@ public class ActivityTypePreference extends DialogPreference {
@Override @Override
public boolean onKey(View v, int keyCode, KeyEvent event) { public boolean onKey(View v, int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) { if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) {
recordingSettingsActivity.showChooseActivityTypeDialog(); showIconSelectDialog();
} }
return true; return true;
} }
}); });
return view; }
private void showIconSelectDialog() {
String category = PreferencesUtils.getString(getActivity(), R.string.default_activity_key, PreferencesUtils.DEFAULT_ACTIVITY_DEFAULT);
ChooseActivityTypeDialogFragment.showDialog(getActivity().getSupportFragmentManager(), category);
} }
@Override @Override
protected void onDialogClosed(boolean positiveResult) { public void onDialogClosed(boolean positiveResult) {
if (positiveResult) { if (positiveResult) {
String value = textView.getText().toString(); String newDefaultActivity = textView.getText().toString();
if (callChangeListener(value)) { if (getPreference().callChangeListener(newDefaultActivity)) {
PreferencesUtils.setString(getContext(), R.string.default_activity_key, value); PreferencesUtils.setString(getActivity(), R.string.default_activity_key, newDefaultActivity);
} }
} }
} }
@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. * Updates the value of the dialog.
* *
* @param iconValue the icon value * @param iconValue the icon value
*/ */
void updateValue(String iconValue) { public void updateUI(String iconValue) {
TrackIconUtils.setIconSpinner(spinner, iconValue); TrackIconUtils.setIconSpinner(spinner, iconValue);
textView.setText(recordingSettingsActivity.getString(TrackIconUtils.getIconActivityType(iconValue))); textView.setText(getActivity().getString(TrackIconUtils.getIconActivityType(iconValue)));
textView.clearFocus(); textView.clearFocus();
} }
} }
}
@@ -0,0 +1,213 @@
package de.dennisguse.opentracks.settings;
import android.bluetooth.BluetoothAdapter;
import android.content.Context;
import androidx.preference.ListPreference;
import java.util.ArrayList;
import java.util.List;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.util.BluetoothUtils;
import de.dennisguse.opentracks.util.PreferencesUtils;
import de.dennisguse.opentracks.util.UnitConversions;
final class PreferenceHelper {
/**
* Sets the min recording interval options.
*
* @param options the options
* @param values the values
*/
static void setMinRecordingIntervalOptions(Context context, String[] options, String[] values) {
for (int i = 0; i < values.length; i++) {
int value = Integer.parseInt(values[i]);
switch (value) {
case PreferencesUtils.MIN_RECORDING_INTERVAL_ADAPT_BATTERY_LIFE:
options[i] = context.getString(R.string.value_adapt_battery_life);
break;
case PreferencesUtils.MIN_RECORDING_INTERVAL_ADAPT_ACCURACY:
options[i] = context.getString(R.string.value_adapt_accuracy);
break;
case PreferencesUtils.MIN_RECORDING_INTERVAL_DEFAULT:
options[i] = context.getString(R.string.value_smallest_recommended);
break;
default:
options[i] = value < 60 ? context.getString(R.string.value_integer_second, value) : context.getString(R.string.value_integer_minute, value / 60);
}
}
}
/**
* Sets the recording distance interval options.
*
* @param options the options
* @param values the values
* @param metricUnits true for metric units
*/
static void setRecordingDistanceIntervalOptions(Context context, String[] options, String[] values, boolean metricUnits) {
for (int i = 0; i < values.length; i++) {
int value = Integer.parseInt(values[i]);
String displayValue;
if (metricUnits) {
displayValue = context.getString(R.string.value_integer_meter, value);
if (value == PreferencesUtils.RECORDING_DISTANCE_INTERVAL_DEFAULT) {
options[i] = context.getString(R.string.value_integer_meter_recommended, value);
} else {
options[i] = displayValue;
}
} else {
int feet = (int) (value * UnitConversions.M_TO_FT);
displayValue = context.getString(R.string.value_integer_feet, feet);
if (value == PreferencesUtils.RECORDING_DISTANCE_INTERVAL_DEFAULT) {
options[i] = context.getString(R.string.value_integer_feet_recommended, feet);
} else {
options[i] = displayValue;
}
}
}
}
/**
* Sets the max recording distance options.
*
* @param options the options
* @param values the values
* @param metricUnits true for metric units
*/
static void setMaxRecordingDistanceOptions(Context context, String[] options, String[] values, boolean metricUnits) {
for (int i = 0; i < values.length; i++) {
int value = Integer.parseInt(values[i]);
String displayValue;
if (metricUnits) {
displayValue = context.getString(R.string.value_integer_meter, value);
if (value == PreferencesUtils.MAX_RECORDING_DISTANCE_DEFAULT) {
options[i] = context.getString(R.string.value_integer_meter_recommended, value);
} else {
options[i] = displayValue;
}
} else {
int feet = (int) (value * UnitConversions.M_TO_FT);
if (feet < 2000) {
displayValue = context.getString(R.string.value_integer_feet, feet);
if (value == PreferencesUtils.MAX_RECORDING_DISTANCE_DEFAULT) {
options[i] = context.getString(R.string.value_integer_feet_recommended, feet);
} else {
options[i] = displayValue;
}
} else {
double mile = feet * UnitConversions.FT_TO_MI;
displayValue = context.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
*/
static void setRecordingGpsAccuracyOptions(Context context, String[] options, String[] values, boolean metricUnits) {
for (int i = 0; i < values.length; i++) {
int value = Integer.parseInt(values[i]);
String displayValue;
if (metricUnits) {
displayValue = context.getString(R.string.value_integer_meter, value);
switch (value) {
case PreferencesUtils.RECORDING_GPS_ACCURACY_DEFAULT:
options[i] = context.getString(R.string.value_integer_meter_recommended, value);
break;
case PreferencesUtils.RECORDING_GPS_ACCURACY_EXCELLENT:
options[i] = context.getString(R.string.value_integer_meter_excellent_gps, value);
break;
case PreferencesUtils.RECORDING_GPS_ACCURACY_POOR:
options[i] = context.getString(R.string.value_integer_meter_poor_gps, value);
break;
default:
options[i] = displayValue;
}
} else {
int feet = (int) (value * UnitConversions.M_TO_FT);
if (feet < 2000) {
displayValue = context.getString(R.string.value_integer_feet, feet);
switch (value) {
case PreferencesUtils.RECORDING_GPS_ACCURACY_DEFAULT:
options[i] = context.getString(R.string.value_integer_feet_recommended, feet);
break;
case PreferencesUtils.RECORDING_GPS_ACCURACY_EXCELLENT:
options[i] = context.getString(R.string.value_integer_feet_excellent_gps, feet);
break;
default:
options[i] = displayValue;
}
} else {
double mile = feet * UnitConversions.FT_TO_MI;
displayValue = context.getString(R.string.value_float_mile, mile);
if (value == PreferencesUtils.RECORDING_GPS_ACCURACY_POOR) {
options[i] = context.getString(R.string.value_float_mile_poor_gps, mile);
} else {
options[i] = displayValue;
}
}
}
}
}
/**
* Sets the auto resume track timeout options.
*
* @param options the options
* @param values the values
*/
static void setAutoResumeTrackTimeoutOptions(Context context, String[] options, String[] values) {
for (int i = 0; i < values.length; i++) {
int value = Integer.parseInt(values[i]);
switch (value) {
case PreferencesUtils.AUTO_RESUME_TRACK_TIMEOUT_NEVER:
options[i] = context.getString(R.string.value_never);
break;
case PreferencesUtils.AUTO_RESUME_TRACK_TIMEOUT_ALWAYS:
options[i] = context.getString(R.string.value_always);
break;
default:
options[i] = context.getString(R.string.value_integer_minute, value);
}
}
}
/**
* Configures the bluetooth sensor.
*/
static void configureBluetoothSensorList(ListPreference preference) {
Context context = preference.getContext();
String value = PreferencesUtils.getString(context, R.string.bluetooth_sensor_key, PreferencesUtils.BLUETOOTH_SENSOR_DEFAULT);
List<String> devicesNameList = new ArrayList<>();
List<String> devicesAddressList = new ArrayList<>();
BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
if (bluetoothAdapter != null) {
BluetoothUtils.populateDeviceLists(bluetoothAdapter, devicesNameList, devicesAddressList);
}
// Was the previously configured device unpaired? Then forget it.
if (!devicesAddressList.contains(value)) {
value = PreferencesUtils.BLUETOOTH_SENSOR_DEFAULT;
PreferencesUtils.setString(context, R.string.bluetooth_sensor_key, value);
}
devicesNameList.add(0, context.getString(R.string.value_none));
devicesAddressList.add(0, PreferencesUtils.BLUETOOTH_SENSOR_DEFAULT);
String[] values = devicesAddressList.toArray(new String[0]);
preference.setEntryValues(values);
String[] options = devicesNameList.toArray(new String[0]);
preference.setEntries(options);
}
}
@@ -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,13 @@
package de.dennisguse.opentracks.settings; package de.dennisguse.opentracks.settings;
import android.content.Context; import android.content.Context;
import android.content.DialogInterface; import android.os.Bundle;
import android.preference.DialogPreference;
import android.util.AttributeSet; import android.util.AttributeSet;
import android.widget.Toast; import android.widget.Toast;
import androidx.preference.DialogPreference;
import androidx.preference.PreferenceDialogFragmentCompat;
import de.dennisguse.opentracks.R; import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.util.PreferencesUtils; import de.dennisguse.opentracks.util.PreferencesUtils;
@@ -15,13 +17,24 @@ public class ResetDialogPreference extends DialogPreference {
super(context, attrs); super(context, attrs);
} }
@Override public static class ResetPreferenceDialog extends PreferenceDialogFragmentCompat {
public void onClick(DialogInterface dialog, int which) {
super.onClick(dialog, which);
if (which == DialogInterface.BUTTON_POSITIVE) {
PreferencesUtils.resetPreferences(getContext(), true);
Toast.makeText(getContext(), R.string.settings_reset_done, Toast.LENGTH_SHORT).show(); 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) {
PreferencesUtils.resetPreferences(getActivity(), true);
Toast.makeText(getActivity(), R.string.settings_reset_done, Toast.LENGTH_SHORT).show();
}
} }
} }
} }
@@ -1,159 +1,182 @@
/*
* 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; package de.dennisguse.opentracks.settings;
import android.bluetooth.BluetoothAdapter;
import android.content.Intent;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.os.Bundle; 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 java.util.ArrayList; import androidx.appcompat.app.AppCompatActivity;
import java.util.List; 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.R;
import de.dennisguse.opentracks.util.BluetoothDeviceUtils; import de.dennisguse.opentracks.fragments.ChooseActivityTypeDialogFragment;
import de.dennisguse.opentracks.util.IntentUtils;
import de.dennisguse.opentracks.util.PreferencesUtils; import de.dennisguse.opentracks.util.PreferencesUtils;
import de.dennisguse.opentracks.util.StringUtils;
/** public class SettingsActivity extends AppCompatActivity implements ChooseActivityTypeDialogFragment.ChooseActivityTypeCaller {
* An activity for accessing settings.
*
* @author Jimmy Shih
*/
public class SettingsActivity extends PreferenceActivity {
private long recordingTrackId = PreferencesUtils.RECORDING_TRACK_ID_DEFAULT; private PrefsFragment prefsFragment = new PrefsFragment();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.settings);
Toolbar toolbar = findViewById(R.id.toolbar);
toolbar.setTitle(R.string.menu_settings);
setSupportActionBar(toolbar);
getSupportFragmentManager().beginTransaction().replace(R.id.settings_fragment, prefsFragment).commit();
}
@Override
public void onChooseActivityTypeDone(String iconValue) {
prefsFragment.setDefaultActivity(iconValue);
}
public static class PrefsFragment extends PreferenceFragmentCompat {
/* /*
* Note that sharedPreferenceChangeListenr cannot be an anonymous inner class. * Note that sharedPreferenceChangeListener cannot be an anonymous inner class.
* Anonymous inner class will get garbage collected. * Anonymous inner class will get garbage collected.
*/ */
private final SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener = new SharedPreferences.OnSharedPreferenceChangeListener() { private final SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener = new SharedPreferences.OnSharedPreferenceChangeListener() {
@Override @Override
public void onSharedPreferenceChanged(SharedPreferences preferences, String key) { public void onSharedPreferenceChanged(SharedPreferences preferences, String key) {
if (key != null && key.equals(PreferencesUtils.getKey(SettingsActivity.this, R.string.recording_track_id_key))) { getActivity().runOnUiThread(new Runnable() {
recordingTrackId = PreferencesUtils.getLong(SettingsActivity.this, R.string.recording_track_id_key);
}
//TODO Should only be called if something meaningful happens.
runOnUiThread(new Runnable() {
@Override @Override
public void run() { public void run() {
updateUI(); updateUI();
} }
}); });
if (key == null || key.equals(PreferencesUtils.getKey(getActivity(), R.string.stats_units_key))) {
getActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
updateUnits();
}
});
}
} }
}; };
// Used to forward update from ChooseActivityTypeDialogFragment; TODO Could be replaced with LiveData.
private ActivityTypePreference.ActivityPreferenceDialog activityPreferenceDialog;
@Override @Override
protected void onCreate(Bundle bundle) { public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
super.onCreate(bundle); setPreferencesFromResource(R.xml.settings, rootKey);
updateUnits();
addPreferencesFromResource(R.xml.settings);
configPreference(R.string.settings_stats_key, StatsSettingsActivity.class);
configPreference(R.string.settings_recording_key, RecordingSettingsActivity.class);
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;
}
});
} }
@Override @Override
protected void onResume() { public void onResume() {
super.onResume(); super.onResume();
PreferencesUtils.getSharedPreferences(this).registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener); PreferencesUtils.getSharedPreferences(getActivity()).registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
recordingTrackId = PreferencesUtils.getLong(this, R.string.recording_track_id_key);
updateUI(); updateUI();
configBluetoothSensor();
} }
@Override @Override
protected void onPause() { public void onPause() {
super.onPause(); super.onPause();
PreferencesUtils.getSharedPreferences(this).unregisterOnSharedPreferenceChangeListener(sharedPreferenceChangeListener); PreferencesUtils.getSharedPreferences(getActivity()).unregisterOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
} }
/** private void configFrequencyPreference(int key, boolean metricUnits) {
* Configures a preference by starting a new activity when it is clicked. ListPreference preference = findPreference(getString(key));
*
* @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() {
@Override
public boolean onPreferenceClick(Preference pref) {
Intent intent = IntentUtils.newIntent(SettingsActivity.this, cl);
startActivity(intent);
return true;
}
});
}
/** String[] options = StringUtils.getFrequencyOptions(getActivity(), metricUnits);
* 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);
}
// 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);
}
devicesNameList.add(0, getString(R.string.value_none));
devicesAddressList.add(0, PreferencesUtils.BLUETOOTH_SENSOR_DEFAULT);
String[] values = devicesAddressList.toArray(new String[0]);
preference.setEntryValues(values);
String[] options = devicesNameList.toArray(new String[0]);
preference.setEntries(options); preference.setEntries(options);
} }
private void configListPreference(int key, int valueArray, boolean metricUnits) {
//TODO Can we make values an int array?
String[] values = getResources().getStringArray(valueArray);
final String[] options = new String[values.length];
switch (key) {
case R.string.min_recording_interval_key:
PreferenceHelper.setMinRecordingIntervalOptions(getActivity(), options, values);
break;
case R.string.recording_distance_interval_key:
PreferenceHelper.setRecordingDistanceIntervalOptions(getActivity(), options, values, metricUnits);
break;
case R.string.max_recording_distance_key:
PreferenceHelper.setMaxRecordingDistanceOptions(getActivity(), options, values, metricUnits);
break;
case R.string.recording_gps_accuracy_key:
PreferenceHelper.setRecordingGpsAccuracyOptions(getActivity(), options, values, metricUnits);
break;
case R.string.auto_resume_track_timeout_key:
PreferenceHelper.setAutoResumeTrackTimeoutOptions(getActivity(), options, values);
break;
default:
return;
}
final ListPreference listPreference = findPreference(getString(key));
listPreference.setEntries(options);
}
@Override
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;
}
if (dialogFragment != null) {
dialogFragment.setTargetFragment(this, 0);
dialogFragment.show(getFragmentManager(), getClass().getSimpleName());
return;
}
super.onDisplayPreferenceDialog(preference);
}
public void setDefaultActivity(String iconValue) {
if (activityPreferenceDialog != null) {
activityPreferenceDialog.updateUI(iconValue);
}
}
private void updateUI() { private void updateUI() {
//TODO Remove the following if recordingTrackId is replaced by direct communication rather than via preferences. final boolean isRecording = PreferencesUtils.isRecording(getActivity());
boolean isRecording = recordingTrackId != PreferencesUtils.RECORDING_TRACK_ID_DEFAULT;
Preference resetPreference = findPreference(getString(R.string.settings_reset_key)); Preference resetPreference = findPreference(getString(R.string.settings_reset_key));
resetPreference.setSummary(isRecording ? getString(R.string.settings_not_while_recording) : ""); resetPreference.setSummary(isRecording ? getString(R.string.settings_not_while_recording) : "");
resetPreference.setEnabled(!isRecording); resetPreference.setEnabled(!isRecording);
Preference speedCheckBoxPreference = findPreference(getString(R.string.chart_show_speed_key)); Preference speedCheckBoxPreference = findPreference(getString(R.string.chart_show_speed_key));
speedCheckBoxPreference.setTitle(PreferencesUtils.isReportSpeed(this) ? R.string.stats_speed : R.string.stats_pace); speedCheckBoxPreference.setTitle(PreferencesUtils.isReportSpeed(getActivity()) ? R.string.stats_speed : R.string.stats_pace);
ListPreference bluetoothPreference = findPreference(getString(R.string.bluetooth_sensor_key));
PreferenceHelper.configureBluetoothSensorList(bluetoothPreference);
}
private void updateUnits() {
boolean metricUnits = PreferencesUtils.isMetricUnits(getActivity());
configFrequencyPreference(R.string.voice_frequency_key, metricUnits);
configFrequencyPreference(R.string.split_frequency_key, metricUnits);
configListPreference(R.string.min_recording_interval_key, R.array.min_recording_interval_values, metricUnits);
configListPreference(R.string.recording_distance_interval_key, R.array.recording_distance_interval_values, metricUnits);
configListPreference(R.string.max_recording_distance_key, R.array.max_recording_distance_values, metricUnits);
configListPreference(R.string.recording_gps_accuracy_key, R.array.recording_gps_accuracy_values, metricUnits);
configListPreference(R.string.auto_resume_track_timeout_key, R.array.auto_resume_track_timeout_values, metricUnits);
configListPreference(R.string.auto_resume_track_timeout_key, R.array.auto_resume_track_timeout_values, metricUnits);
final ListPreference statsRatePreferences = findPreference(getString(R.string.stats_rate_key));
String[] options = getResources().getStringArray(metricUnits ? R.array.stats_rate_metric_options : R.array.stats_rate_imperial_options);
statsRatePreferences.setEntries(options);
//TODO This is a hack!!! Need to manually updated the summary as otherwise it will only be updated after scrolling down and up again (bring the object out of view).
//Check if this is still needed after upgraded PreferenceFragment.
statsRatePreferences.setSummary(statsRatePreferences.getEntry());
}
} }
} }
@@ -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;
}
});
}
}
@@ -18,6 +18,7 @@ package de.dennisguse.opentracks.util;
import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothClass; import android.bluetooth.BluetoothClass;
import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothGattCharacteristic;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
@@ -27,9 +28,9 @@ import java.util.Set;
* *
* @author Rodrigo Damazio * @author Rodrigo Damazio
*/ */
public class BluetoothDeviceUtils { public class BluetoothUtils {
private BluetoothDeviceUtils() { private BluetoothUtils() {
} }
/** /**
@@ -53,4 +54,13 @@ public class BluetoothDeviceUtils {
} }
} }
} }
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);
}
} }
@@ -29,6 +29,7 @@ import androidx.core.content.FileProvider;
import java.io.File; import java.io.File;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date; import java.util.Date;
import de.dennisguse.opentracks.R; import de.dennisguse.opentracks.R;
@@ -69,19 +70,32 @@ public class IntentUtils {
* @param context the context * @param context the context
* @param trackIds the track ids * @param trackIds the track ids
*/ */
//TODO Share mulitple tracks in different files!
public static Intent newShareFileIntent(Context context, long[] trackIds) { public static Intent newShareFileIntent(Context context, long[] trackIds) {
if (trackIds.length == 0) {
throw new RuntimeException("Need to share at least one track.");
}
String trackDescription = ""; String trackDescription = "";
if (trackIds.length == 1) { if (trackIds.length == 1) {
Track track = ContentProviderUtils.Factory.get(context).getTrack(trackIds[0]); Track track = ContentProviderUtils.Factory.get(context).getTrack(trackIds[0]);
trackDescription = track == null ? "" : new DescriptionGeneratorImpl(context).generateTrackDescription(track, false); trackDescription = track == null ? "" : new DescriptionGeneratorImpl(context).generateTrackDescription(track, false);
} }
Pair<Uri, String> uriAndMime = ShareContentProvider.createURI(trackIds, TrackFileFormat.KMZ_WITH_TRACKDETAIL_AND_SENSORDATA_AND_PICTURES);
return new Intent(Intent.ACTION_SEND) String action = trackIds.length == 1 ? Intent.ACTION_SEND : Intent.ACTION_SEND_MULTIPLE;
.putExtra(Intent.EXTRA_STREAM, uriAndMime.first) String mime = "";
ArrayList<Uri> uris = new ArrayList<>();
for (long trackId : trackIds) {
Pair<Uri, String> uriAndMime = ShareContentProvider.createURI(new long[]{trackId}, TrackFileFormat.KMZ_WITH_TRACKDETAIL_AND_SENSORDATA_AND_PICTURES);
uris.add(uriAndMime.first);
mime = uriAndMime.second;
}
return new Intent(action)
.putExtra(Intent.EXTRA_SUBJECT, context.getString(R.string.share_track_subject)) .putExtra(Intent.EXTRA_SUBJECT, context.getString(R.string.share_track_subject))
.putExtra(Intent.EXTRA_TEXT, context.getString(R.string.share_track_share_file_body, trackDescription)) .putExtra(Intent.EXTRA_TEXT, context.getString(R.string.share_track_share_file_body, trackDescription))
.setType(uriAndMime.second) .putParcelableArrayListExtra(Intent.EXTRA_STREAM, uris)
.setType(mime)
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
} }
@@ -19,7 +19,8 @@ package de.dennisguse.opentracks.util;
import android.content.Context; import android.content.Context;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor; import android.content.SharedPreferences.Editor;
import android.preference.PreferenceManager;
import androidx.preference.PreferenceManager;
import de.dennisguse.opentracks.R; import de.dennisguse.opentracks.R;
@@ -252,9 +253,16 @@ public class PreferencesUtils {
return CHART_X_AXIS_DEFAULT.equals(getString(context, R.string.chart_x_axis_key, 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 isRecording(Context context) {
long recordingTrackId = PreferencesUtils.getLong(context, R.string.recording_track_id_key);
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) { public static void resetPreferences(Context context, boolean readAgain) {
PreferenceManager.setDefaultValues(context, R.xml.settings, readAgain); 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; package de.dennisguse.opentracks.util;
import android.app.Activity; import android.app.Activity;
import android.content.Context;
import android.location.Location; import android.location.Location;
import android.util.Pair; import android.util.Pair;
import android.view.View; import android.view.View;
@@ -26,7 +25,7 @@ import android.widget.TextView;
import de.dennisguse.opentracks.R; import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.sensor.SensorDataSet; import de.dennisguse.opentracks.content.sensor.SensorDataSet;
import de.dennisguse.opentracks.services.sensors.RemoteSensorManager; import de.dennisguse.opentracks.services.sensors.BluetoothRemoteSensorManager;
import de.dennisguse.opentracks.stats.TripStatistics; import de.dennisguse.opentracks.stats.TripStatistics;
/** /**
@@ -77,7 +76,7 @@ public class StatsUtils {
if (showGradeElevation) { if (showGradeElevation) {
double altitude = location != null && location.hasAltitude() ? location.getAltitude() : Double.NaN; 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); TextView elevationValue = activity.findViewById(R.id.stats_elevation_current_value);
elevationValue.setText(parts.first); elevationValue.setText(parts.first);
@@ -119,9 +118,11 @@ public class StatsUtils {
TextView heartRateSensor = activity.findViewById(R.id.stats_sensor_heart_rate_sensor_value); TextView heartRateSensor = activity.findViewById(R.id.stats_sensor_heart_rate_sensor_value);
String heartRate = activity.getString(R.string.value_unknown); String heartRate = activity.getString(R.string.value_unknown);
String sensor = 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)) { if (sensorDataSet != null && sensorDataSet.isRecent(BluetoothRemoteSensorManager.MAX_SENSOR_DATE_SET_AGE_MS)) {
heartRate = StringUtils.formatDecimal(sensorDataSet.getHeartRate(), 0);
sensor = sensorDataSet.getSensorName(); sensor = sensorDataSet.getSensorName();
if (sensorDataSet.hasHeartRate()) {
heartRate = StringUtils.formatDecimal(sensorDataSet.getHeartRate(), 0);
}
} }
heartRateValue.setText(heartRate); heartRateValue.setText(heartRate);
@@ -250,7 +251,7 @@ public class StatsUtils {
if (showElevation) { if (showElevation) {
{ {
double elevation = tripStatistics == null ? Double.NaN : tripStatistics.getMinElevation(); 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); TextView elevationValue = activity.findViewById(R.id.stats_elevation_min_value);
elevationValue.setText(parts.first); elevationValue.setText(parts.first);
@@ -260,7 +261,7 @@ public class StatsUtils {
{ {
double elevation = tripStatistics == null ? Double.NaN : tripStatistics.getMaxElevation(); 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); TextView elevationValue = activity.findViewById(R.id.stats_elevation_max_value);
elevationValue.setText(parts.first); elevationValue.setText(parts.first);
@@ -270,26 +271,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.text.format.DateUtils;
import android.util.Pair; import android.util.Pair;
import java.text.DecimalFormat;
import java.text.ParsePosition; import java.text.ParsePosition;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.Date; 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); 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) { static String formatDecimal(double value, int decimalPlaces) {
String result = String.format(Locale.getDefault(), "%1$,." + decimalPlaces + "f", value); if (decimalPlaces < 1) {
return result.replaceAll("[0]*$", "").replaceAll("\\.$", ""); return Long.toString(Math.round(value));
}
String format = "#." + new String(new char[decimalPlaces]).replace("\0", "#");
return new DecimalFormat(format).format(value);
} }
/** /**
@@ -361,4 +369,25 @@ public class StringUtils {
} }
return options; 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<>(); private static final LinkedHashMap<String, Pair<Integer, Integer>> MAP = new LinkedHashMap<>();
static { static {
//Reflects order in ChooseActivityTypeDialogFragment //Reflects order in ChooseActivityTypeDialogFragmentActivity
MAP.put(UNKNOWN, new Pair<>(R.string.activity_type_unknown, ACTIVITY_UNKNOWN_LOGO)); 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(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)); MAP.put(WALK, new Pair<>(R.string.activity_type_walking, R.drawable.ic_activity_walk_24dp));
Binary file not shown.

Before

Width:  |  Height:  |  Size: 199 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 154 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 260 B

+10
View File
@@ -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>
+4 -1
View File
@@ -16,7 +16,10 @@ limitations under the License.
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="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" /> <include layout="@layout/track_controller" />
+2
View File
@@ -4,6 +4,8 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:orientation="vertical"> android:orientation="vertical">
<include layout="@layout/toolbar" />
<ImageView <ImageView
android:id="@+id/imageView" android:id="@+id/imageView"
android:layout_width="match_parent" android:layout_width="match_parent"
@@ -17,6 +17,8 @@ limitations under the License.
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
<include layout="@layout/toolbar" />
<androidx.viewpager.widget.ViewPager <androidx.viewpager.widget.ViewPager
android:id="@+id/maker_detail_activity_view_pager" android:id="@+id/maker_detail_activity_view_pager"
android:layout_width="match_parent" android:layout_width="match_parent"
+2
View File
@@ -18,6 +18,8 @@ limitations under the License.
android:layout_height="match_parent" android:layout_height="match_parent"
android:orientation="vertical"> android:orientation="vertical">
<include layout="@layout/toolbar" />
<ScrollView style="@style/ScrollView.WithBottomButtons"> <ScrollView style="@style/ScrollView.WithBottomButtons">
<LinearLayout <LinearLayout
+6 -3
View File
@@ -13,9 +13,12 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
--> -->
<merge xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent"
android:orientation="vertical">
<include layout="@layout/toolbar" />
<ListView <ListView
android:id="@+id/marker_list" android:id="@+id/marker_list"
@@ -26,4 +29,4 @@ limitations under the License.
android:id="@+id/marker_list_empty" android:id="@+id/marker_list_empty"
style="@style/ListEmptyTextView" style="@style/ListEmptyTextView"
android:text="@string/marker_list_empty_message" /> android:text="@string/marker_list_empty_message" />
</merge> </LinearLayout>
+6 -3
View File
@@ -13,9 +13,12 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
--> -->
<merge xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent"
android:orientation="vertical">
<include layout="@layout/toolbar" />
<ListView <ListView
android:id="@+id/search_list" android:id="@+id/search_list"
@@ -26,4 +29,4 @@ limitations under the License.
android:id="@+id/search_list_empty" android:id="@+id/search_list_empty"
style="@style/ListEmptyTextView" style="@style/ListEmptyTextView"
android:text="@string/search_list_empty_message" /> android:text="@string/search_list_empty_message" />
</merge> </LinearLayout>
+14
View File
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<include layout="@layout/toolbar" />
<FrameLayout
android:id="@+id/settings_fragment"
android:layout_width="fill_parent"
android:layout_height="wrap_content" />
</LinearLayout>
+12
View File
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/colorPrimary"
android:minHeight="?attr/actionBarSize"
app:navigationIcon="@drawable/ic_logo_color_24dp" />
</merge>
+2
View File
@@ -18,6 +18,8 @@ limitations under the License.
android:layout_height="match_parent" android:layout_height="match_parent"
android:orientation="vertical"> android:orientation="vertical">
<include layout="@layout/toolbar" />
<TabHost <TabHost
android:id="@+id/tackdetail_tabhost" android:id="@+id/tackdetail_tabhost"
android:layout_width="match_parent" android:layout_width="match_parent"
+2
View File
@@ -18,6 +18,8 @@ limitations under the License.
android:layout_height="match_parent" android:layout_height="match_parent"
android:orientation="vertical"> android:orientation="vertical">
<include layout="@layout/toolbar" />
<ScrollView style="@style/ScrollView.WithBottomButtons"> <ScrollView style="@style/ScrollView.WithBottomButtons">
<LinearLayout <LinearLayout
+2
View File
@@ -18,6 +18,8 @@ limitations under the License.
android:layout_height="match_parent" android:layout_height="match_parent"
android:orientation="vertical"> android:orientation="vertical">
<include layout="@layout/toolbar" />
<ListView <ListView
android:id="@+id/track_list" android:id="@+id/track_list"
android:layout_width="match_parent" android:layout_width="match_parent"
+13 -12
View File
@@ -13,30 +13,31 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
--> -->
<menu xmlns:android="http://schemas.android.com/apk/res/android"> <menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item <item
android:id="@+id/list_context_menu_share" android:id="@+id/list_context_menu_share"
android:actionProviderClass="android.widget.ShareActionProvider" android:icon="@drawable/ic_share_24dp"
android:showAsAction="ifRoom" android:title="@string/menu_share"
android:title="@string/menu_share" /> app:showAsAction="ifRoom" />
<item <item
android:id="@+id/list_context_menu_show_on_map" android:id="@+id/list_context_menu_show_on_map"
android:icon="@drawable/ic_map_24dp" android:icon="@drawable/ic_map_24dp"
android:showAsAction="ifRoom" android:title="@string/menu_show_on_map"
android:title="@string/menu_show_on_map" /> app:showAsAction="ifRoom" />
<item <item
android:id="@+id/list_context_menu_edit" android:id="@+id/list_context_menu_edit"
android:icon="@drawable/ic_edit_24dp" android:icon="@drawable/ic_edit_24dp"
android:showAsAction="ifRoom" android:title="@string/menu_edit"
android:title="@string/menu_edit" /> app:showAsAction="ifRoom" />
<item <item
android:id="@+id/list_context_menu_delete" android:id="@+id/list_context_menu_delete"
android:icon="@drawable/ic_delete_forever_24dp" android:icon="@drawable/ic_delete_forever_24dp"
android:showAsAction="ifRoom" android:title="@string/menu_delete"
android:title="@string/menu_delete" /> app:showAsAction="ifRoom" />
<item <item
android:id="@+id/list_context_menu_select_all" android:id="@+id/list_context_menu_select_all"
android:icon="@drawable/ic_select_all_24dp" android:icon="@drawable/ic_select_all_24dp"
android:showAsAction="ifRoom" android:title="@string/menu_select_all"
android:title="@string/menu_select_all" /> app:showAsAction="ifRoom" />
</menu> </menu>
+8 -7
View File
@@ -13,20 +13,21 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
--> -->
<menu xmlns:android="http://schemas.android.com/apk/res/android"> <menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item <item
android:id="@+id/marker_detail_show_on_map" android:id="@+id/marker_detail_show_on_map"
android:icon="@drawable/ic_map_24dp" android:icon="@drawable/ic_map_24dp"
android:showAsAction="ifRoom" android:title="@string/menu_show_on_map"
android:title="@string/menu_show_on_map" /> app:showAsAction="ifRoom" />
<item <item
android:id="@+id/marker_detail_edit" android:id="@+id/marker_detail_edit"
android:icon="@drawable/ic_edit_24dp" android:icon="@drawable/ic_edit_24dp"
android:showAsAction="ifRoom" android:title="@string/menu_edit"
android:title="@string/menu_edit" /> app:showAsAction="ifRoom" />
<item <item
android:id="@+id/marker_detail_delete" android:id="@+id/marker_detail_delete"
android:icon="@drawable/ic_delete_forever_24dp" android:icon="@drawable/ic_delete_forever_24dp"
android:showAsAction="ifRoom" android:title="@string/menu_delete"
android:title="@string/menu_delete" /> app:showAsAction="ifRoom" />
</menu> </menu>
+7 -6
View File
@@ -13,15 +13,16 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
--> -->
<menu xmlns:android="http://schemas.android.com/apk/res/android"> <menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item <item
android:id="@+id/marker_list_insert_marker" android:id="@+id/marker_list_insert_marker"
android:icon="@drawable/ic_marker_show_24dp" android:icon="@drawable/ic_marker_show_24dp"
android:showAsAction="ifRoom" android:title="@string/menu_insert_marker"
android:title="@string/menu_insert_marker" /> app:showAsAction="ifRoom" />
<item <item
android:id="@+id/marker_list_search" android:id="@+id/marker_list_search"
android:actionViewClass="android.widget.SearchView" android:title="@string/menu_search"
android:showAsAction="ifRoom" app:actionViewClass="androidx.appcompat.widget.SearchView"
android:title="@string/menu_search" /> app:showAsAction="ifRoom" />
</menu> </menu>
+7 -6
View File
@@ -13,29 +13,30 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
--> -->
<menu xmlns:android="http://schemas.android.com/apk/res/android"> <menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item <item
android:id="@+id/track_detail_insert_marker" android:id="@+id/track_detail_insert_marker"
android:icon="@drawable/ic_marker_add_24dp" android:icon="@drawable/ic_marker_add_24dp"
android:orderInCategory="1" android:orderInCategory="1"
android:showAsAction="ifRoom" app:showAsAction="ifRoom"
android:title="@string/menu_insert_marker" /> android:title="@string/menu_insert_marker" />
<item <item
android:id="@+id/track_detail_insert_photo" android:id="@+id/track_detail_insert_photo"
android:icon="@drawable/ic_camera_24dp" android:icon="@drawable/ic_camera_24dp"
android:orderInCategory="2" android:orderInCategory="2"
android:showAsAction="ifRoom" app:showAsAction="ifRoom"
android:title="@string/menu_insert_photo" /> android:title="@string/menu_insert_photo" />
<item <item
android:id="@+id/track_detail_menu_show_on_map" android:id="@+id/track_detail_menu_show_on_map"
android:icon="@drawable/ic_map_24dp" android:icon="@drawable/ic_map_24dp"
android:showAsAction="ifRoom" app:showAsAction="ifRoom"
android:title="@string/menu_show_on_map" /> android:title="@string/menu_show_on_map" />
<item <item
android:id="@+id/track_detail_share" android:id="@+id/track_detail_share"
android:actionProviderClass="android.widget.ShareActionProvider" android:icon="@drawable/ic_share_24dp"
android:orderInCategory="4" android:orderInCategory="4"
android:showAsAction="ifRoom" app:showAsAction="ifRoom"
android:title="@string/menu_share" /> android:title="@string/menu_share" />
<item <item
android:id="@+id/track_detail_markers" android:id="@+id/track_detail_markers"
+9 -12
View File
@@ -13,27 +13,24 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
--> -->
<menu xmlns:android="http://schemas.android.com/apk/res/android"> <menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item <item
android:id="@+id/track_list_search" android:id="@+id/track_list_search"
android:actionViewClass="android.widget.SearchView" android:title="@string/menu_search"
android:showAsAction="ifRoom" app:actionViewClass="androidx.appcompat.widget.SearchView"
android:title="@string/menu_search" /> app:showAsAction="ifRoom" />
<!-- Icon and title is overwritten by TrackListActivity --> <!-- Icon and title is overwritten by TrackListActivity -->
<item <item
android:id="@+id/track_list_start_gps" android:id="@+id/track_list_start_gps"
android:icon="@drawable/ic_gps_off_24dp" android:icon="@drawable/ic_gps_off_24dp"
android:showAsAction="ifRoom" android:title="@string/menu_start_gps"
android:title="@string/menu_start_gps" /> app:showAsAction="ifRoom" />
<item <item
android:id="@+id/track_list_markers" android:id="@+id/track_list_markers"
android:icon="@drawable/ic_marker_show_24dp" android:icon="@drawable/ic_marker_show_24dp"
android:showAsAction="ifRoom" android:title="@string/menu_markers"
android:title="@string/menu_markers" /> app:showAsAction="ifRoom" />
<item
android:id="@+id/track_list_aggregated_statistics"
android:icon="@drawable/ic_statistics_24dp"
android:title="@string/menu_aggregated_statistics" />
<item <item
android:id="@+id/track_list_delete_all" android:id="@+id/track_list_delete_all"
android:icon="@drawable/ic_delete_forever_24dp" android:icon="@drawable/ic_delete_forever_24dp"
+13 -2
View File
@@ -1054,7 +1054,9 @@ limitations under the License.
sending data."> sending data.">
Sending Sending
</string> </string>
<string name="sensor_not_known" translation_description="Error message if sensor is not available in Android (e.g., pairing was removed in Bluetooth Settings).">
Sensor %1$s is not known. Please check the settings.
</string>
<!-- Settings --> <!-- Settings -->
<string name="settings_not_while_recording" translation_description="In settings, the summary for an option that is not available during a <string name="settings_not_while_recording" translation_description="In settings, the summary for an option that is not available during a
track recording."> track recording.">
@@ -1271,11 +1273,20 @@ limitations under the License.
Character limit: ~23 (1 line)"> Character limit: ~23 (1 line)">
Bluetooth sensor Bluetooth sensor
</string> </string>
<string name="settings_sensor_connected" translation_description="A message when a sensor is connected. <string name="settings_sensor_connecting" translation_description="A message when a sensor is connecting.
%1$s sensor name. E.g., 'Polar Bluetooth'."> %1$s sensor name. E.g., 'Polar Bluetooth'.">
Connected to %1$s Connected to %1$s
</string> </string>
<string name="settings_sensor_connected" translation_description="A message when a sensor is connected.
%1$s sensor name. E.g., 'Polar Bluetooth'.">
Connected to %1$s
</string>
<string name="settings_sensor_disconnected" translation_description="A message when a sensor is disconnected.
%1$s sensor name. E.g., 'Polar Bluetooth'.">
Disconnected from %1$s
</string>
<!-- Settings Stats --> <!-- Settings Stats -->
<string name="settings_stats_coordinate" translation_description="In the 'Stats settings, the option to show latitude and longitude. <string name="settings_stats_coordinate" translation_description="In the 'Stats settings, the option to show latitude and longitude.

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