Compare commits

..

25 Commits

Author SHA1 Message Date
Dennis Guse 1feb5cbb65 Release v3.8.1 2020-08-03 08:03:13 +02:00
Dennis Guse 1d2fde6e69 DashboardAPI: report if track is recording and start Dashboard on same task.
Necessary for Picture-in-Picture mode.
2020-08-03 07:59:17 +02:00
Dennis Guse 20423121ee Dashboard API: only forward fullscreen setting, when recording. 2020-08-03 07:59:17 +02:00
Dennis Guse ebf7b0a0c3 TrackRecordingActivity: added setting for fullscreen.
Fixes #342.
2020-08-03 07:59:17 +02:00
Dennis Guse 1189a07794 TrackController: center time. 2020-08-02 16:36:03 +02:00
Román 63e9041922 Orientation change when edit marker has to update photo uri in waypoint for delete button update (hide/show). 2020-07-31 20:29:56 +02:00
Dennis Guse 699be450b0 Fixed too optimistic intent-filter for imports.
Fixes #340.
2020-07-29 23:19:32 +02:00
Dennis Guse 66b175c56c Updated dependencies. 2020-07-26 17:39:30 +02:00
Dennis Guse 81a7aca4a0 Release v3.8.0 2020-07-24 22:22:48 +02:00
Dennis Guse 065f42fbef GPX import/export: speed via TrackPointExtension/v2.
Fixed data type for heartrate/speed (no decimals) allowed.
Fixes #335.
Fixes #181.

GPXTrackWriter: set Trackpointv2 header correctly and export time in correct sequence.
2020-07-24 22:06:58 +02:00
ronajon c92371b36a GPX import/export: add heartrate and cadence via TrackPointExtension/v1. 2020-07-24 22:06:58 +02:00
Dennis Guse 97c46183c3 TrackRecordingServiceConnection: always bind (and start) the TrackRecordingService.
Seemed to be a memory optimization.
2020-07-21 19:44:39 +02:00
Dennis Guse c02b8fab53 HandlerServer: use executor service to send data to TrackRecordingService.
Fixes #329.
2020-07-21 19:43:51 +02:00
Dennis Guse 6540913d59 Moved R.string.stats_unit_key initialization into Startup. 2020-07-20 20:46:41 +02:00
Dennis Guse b70c3be5af Cleanup: static code analysis using AndroidStudio. 2020-07-20 20:05:45 +02:00
Dennis Guse 461f1c246b MarkerEditActivity: do not overwrite new photoUri with previous one. (#332)
Fixes #330.

Tested and it works 👍
2020-07-20 19:30:23 +02:00
Dennis Guse 6d091dfa81 Fixes NPE on MarkerEditActivity when taking a photo and trigger an orientation change. (#327)
Fixes #325.
2020-07-18 22:52:19 +02:00
Peter Storch 97ad6bc2f8 expose waypoints to dashboard
Necessary for https://github.com/OpenTracksApp/OSMDashboard/issues/48
2020-07-18 18:56:53 +02:00
Dennis Guse 93ee8a53ed Import from intent: also match file suffix as fallback. 2020-07-18 18:55:30 +02:00
Dennis Guse bf9b6d6627 Import: individual files chosen by an external app. 2020-07-18 18:55:30 +02:00
Dennis Guse 5f637952aa Replaced blue pushpin icon (png) with with orange icon (svg).
Fixes #115.
2020-07-18 18:51:34 +02:00
Román Martínez 17de8cea6f Service refactoring: move location management to the LocationHandle.
Tests refactoring: location and service tests re-worked.
2020-07-17 12:29:30 +02:00
Deus Figendi afea79476f united the gradiants in ic_marker_orange_pushpin 2020-07-16 22:57:44 +02:00
Deus Figendi 7215a2def0 Added an orange version of the ic_marker pushpin 2020-07-16 22:57:44 +02:00
Román b8d2d14d68 Preferred rate is used for aggregated stats. Fixes #321.
Lambda expression for Observer.
2020-07-15 21:43:55 +02:00
84 changed files with 1241 additions and 1015 deletions
+1 -1
View File
@@ -11,7 +11,7 @@ For the time being, the OpenTracks Data API can be used to implement mainly cust
## Implementation
OpenTracks by default does not expose any data to 3rd-party applications.
On the request of the user (i.e., pressing a button), an Intent is created that exposes the `Track` data (it's summary) as well as the `TrackPoint`s of one or more selected tracks.
On the request of the user (i.e., pressing a button), an Intent is created that exposes the `Track` data (it's summary), the `TrackPoint`s, and the `Waypoint`s of one or more selected tracks.
For each, one URI is provided.
These URIs notify observers if on content changes (e.g., a new `TrackPoint` was added).
+4 -24
View File
@@ -6,7 +6,7 @@ buildscript {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:4.0.0'
classpath 'com.android.tools.build:gradle:4.0.1'
}
}
@@ -17,20 +17,6 @@ allprojects {
}
}
def getVersionCode = { ->
try {
def stdout = new ByteArrayOutputStream()
exec {
commandLine 'git', 'rev-list', 'HEAD', '--count'
standardOutput = stdout
}
return Integer.parseInt(stdout.toString().trim())
}
catch (ignored) {
return -1
}
}
def getVersionName = { ->
try {
def stdout = new ByteArrayOutputStream()
@@ -64,9 +50,8 @@ android {
applicationName: "@string/app_name"
]
versionCode 3449
//versionCode getVersionCode() //Useful for debugging.
versionName "v3.7.6"
versionCode 3474
versionName "v3.8.1"
buildConfigField "String", "VERSION_NAME_FULL", "\"${getVersionName()}\""
@@ -111,13 +96,8 @@ dependencies {
implementation 'com.android.support.constraint:constraint-layout:1.1.3'
androidTestImplementation 'androidx.test:core:1.2.0'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
androidTestImplementation 'androidx.test.espresso:espresso-intents:3.2.0'
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
androidTestImplementation 'androidx.test:rules:1.2.0'
androidTestImplementation 'androidx.test:runner:1.2.0'
androidTestImplementation 'org.mockito:mockito-android:3.0.0'
testImplementation 'androidx.test:core:1.2.0'
testImplementation 'org.mockito:mockito-core:3.0.0'
androidTestImplementation 'org.mockito:mockito-android:3.4.4'
}
@@ -0,0 +1,116 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="210mm"
height="297mm"
viewBox="0 0 210 297"
version="1.1"
id="svg8"
inkscape:version="1.0 (4035a4fb49, 2020-05-01)"
sodipodi:docname="ic_marker_orange_pushpin_with_shadow.svg">
<defs
id="defs2">
<linearGradient
inkscape:collect="always"
id="linearGradient871"
x1="91.712517"
y1="266.48581"
x2="95.902588"
y2="266.72339"
gradientUnits="userSpaceOnUse"
gradientTransform="rotate(22.027833,208.46275,168.26892)"
>
<stop
style="stop-color:#ffffff;stop-opacity:1"
offset="0"
id="stop867" />
<stop
style="stop-color:#898d8d;stop-opacity:1"
offset="1"
id="stop869" />
</linearGradient>
</defs>
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="0.49497475"
inkscape:cx="298.98541"
inkscape:cy="561.0427"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
inkscape:document-rotation="0"
showgrid="false"
inkscape:window-width="1920"
inkscape:window-height="1012"
inkscape:window-x="0"
inkscape:window-y="1080"
inkscape:window-maximized="1" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Ebene 1"
inkscape:groupmode="layer"
id="layer1">
<path
d="m 108.52826,213.29755 a 56.035402,7.0061195 5.281088 0 0 -22.425508,2.5321 56.035402,7.0061195 5.281088 0 0 -0.233576,0.17697 l -0.206706,-0.0237 c -0.04471,0.11554 -0.07454,0.2314 -0.09612,0.34752 a 56.035402,7.0061195 5.281088 0 0 0.337963,1.58159 c 0.980726,1.70884 3.760629,3.46326 6.583579,5.2188 l -22.744347,15.50009 a 67.064653,11.556784 4.2275244 0 0 -39.967588,5.26867 67.064653,11.556784 4.2275244 0 0 -0.729154,0.57564 67.064653,11.556784 4.2275244 0 0 -2.754353,1.36358 67.064653,11.556784 4.2275244 0 0 38.908736,16.04324 l -32.176847,27.0646 52.228334,-24.74551 a 67.064653,11.556784 4.2275244 0 0 71.411147,-3.8186 67.064653,11.556784 4.2275244 0 0 0.72812,-0.57507 67.064653,11.556784 4.2275244 0 0 2.75539,-1.36415 67.064653,11.556784 4.2275244 0 0 -12.38374,-10.59818 40.033569,5.0733922 5.2502179 0 0 0.0563,-0.0369 40.033569,5.0733922 5.2502179 0 0 0.37672,-0.34325 l 22.45186,-15.30075 c 8.23597,-0.65161 17.65581,-1.47811 22.94433,-3.00957 a 56.035402,7.0061195 5.281088 0 0 2.65514,-1.03772 56.035402,7.0061195 5.281088 0 0 -46.08401,-12.26953 56.035402,7.0061195 5.281088 0 0 -41.6357,-2.54991 z"
style="fill:#333333;fill-opacity:0.49421525;stroke:#000000;stroke-width:0.138931px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.265501"
id="path839" />
<path
style="fill:url(#linearGradient871);fill-opacity:1;stroke:#000000;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.265501"
d="M 70.603317,174.30832 33.024148,288.94657 94.566672,183.19473 Z"
id="path865"
sodipodi:nodetypes="cccc" />
<ellipse
cx="144.42586"
cy="113.06232"
rx="70.318817"
ry="39.974442"
id="ellipse26"
style="fill:#d45500;fill-opacity:1;fill-rule:evenodd;stroke-width:0.264583"
transform="rotate(22.027833)" />
<ellipse
style="fill:#ff6600;fill-opacity:1;fill-rule:evenodd;stroke-width:0.264583"
id="ellipse30"
ry="39.974442"
rx="70.318817"
cy="105.23625"
cx="145.017"
transform="rotate(22.027833)" />
<path
id="rect28"
style="fill:#d45500;fill-opacity:1;fill-rule:evenodd;stroke-width:0.264583"
d="m 85.662408,24.322061 c -0.890996,8.350369 2.928035,17.133472 6.825491,25.923906 l -23.333902,57.672763 -0.730686,1.80598 0.248621,0.10059 a 42.43035,17.360788 22.027833 0 0 33.303948,30.10067 42.43035,17.360788 22.027833 0 0 44.86507,1.52585 l 0.13892,0.0562 0.24519,-0.60599 a 42.43035,17.360788 22.027833 0 0 0.59472,-1.15581 42.43035,17.360788 22.027833 0 0 0.37668,-1.24514 l 22.45185,-55.492655 c 9.28986,-2.66566 20.09069,-6.13552 24.76397,-13.17485 z" />
<ellipse
style="fill:#ff6600;fill-opacity:1;fill-rule:evenodd;stroke-width:0.264583"
id="path24"
ry="23.966745"
rx="59.409279"
cy="-10.262156"
cx="148.14029"
transform="rotate(22.027833)" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.0 KiB

@@ -0,0 +1,16 @@
v3.8.0: OpenTracks
Breaking change:
- GPX import/export speed, cadence, and heartrate via TrackPointExtension/v2
Changes:
- Dashboard API: provide access to markers (incl. callback to show in OpenTracks)
- New marker icon (orange push pin, SVG)
- Files to import can be selected via file manager
Bugfixes:
- Aggregated stats UI respects preferred rate
- Editing a marker failed on orientation change
Developer:
- Refactored TrackRecordingService
@@ -0,0 +1,8 @@
v3.8.1: OpenTracks
Changes:
- While recording: add option for fullscreen
- Dashboard is started on the same task
Bugfixes:
- Intent filters for importing files were far too optimistic
@@ -58,7 +58,6 @@ public class SearchEngineTest {
private final Context context = ApplicationProvider.getApplicationContext();
@Before
public void setUp() {
providerUtils = new ContentProviderUtils(context);
@@ -206,7 +206,7 @@ public class CustomContentProviderUtilsTest {
public void testDeleteAllTracks() {
// Insert track, points and waypoint at first.
long trackId = System.currentTimeMillis();
Track track = TestDataUtil.createTrackAndInsert(contentProviderUtils, trackId, 10);
TestDataUtil.createTrackAndInsert(contentProviderUtils, trackId, 10);
Waypoint waypoint = new Waypoint(contentProviderUtils.getLastValidTrackPoint(trackId));
contentProviderUtils.insertWaypoint(waypoint);
@@ -103,8 +103,7 @@ public class ExportImportTest {
// given
Track track = contentProviderUtils.getTrack(trackId);
TrackFileFormat trackFileFormat = TrackFileFormat.KML_WITH_TRACKDETAIL;
TrackExporter trackExporter = trackFileFormat.newTrackExporter(context, new Track[]{track});
TrackExporter trackExporter = TrackFileFormat.KML_WITH_TRACKDETAIL.newTrackExporter(context, new Track[]{track});
// when
// 1. export
@@ -138,8 +137,7 @@ public class ExportImportTest {
// given
Track track = contentProviderUtils.getTrack(trackId);
TrackFileFormat trackFileFormat = TrackFileFormat.KML_WITH_TRACKDETAIL_AND_SENSORDATA;
TrackExporter trackExporter = trackFileFormat.newTrackExporter(context, new Track[]{track});
TrackExporter trackExporter = TrackFileFormat.KML_WITH_TRACKDETAIL_AND_SENSORDATA.newTrackExporter(context, new Track[]{track});
// when
// 1. export
@@ -201,8 +199,7 @@ public class ExportImportTest {
// given
Track track = contentProviderUtils.getTrack(trackId);
TrackFileFormat trackFileFormat = TrackFileFormat.GPX;
TrackExporter trackExporter = trackFileFormat.newTrackExporter(context, new Track[]{track});
TrackExporter trackExporter = TrackFileFormat.GPX.newTrackExporter(context, new Track[]{track});
// when
// 1. export
@@ -229,7 +226,7 @@ public class ExportImportTest {
assertWaypoints();
// 3. trackpoints
assertTrackpoints(false, false, false);
assertTrackpoints(false, true, true);
}
private void assertWaypoints() {
@@ -21,6 +21,8 @@ import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import de.dennisguse.opentracks.services.handlers.AdaptiveLocationListenerPolicy;
/**
* Tests the {@link AdaptiveLocationListenerPolicy}.
*
@@ -172,7 +172,7 @@ public class TrackRecordingServiceTest {
// given
createDummyTrack(trackId, -1L, true);
TrackRecordingServiceInterface service = ((TrackRecordingServiceInterface) mServiceRule.bindService(createStartIntent(context)));
insertLocation(service);
newTrackPoint(service);
// when
service.pauseCurrentTrack();
@@ -182,7 +182,7 @@ public class TrackRecordingServiceTest {
//when
service.resumeTrack(trackId);
insertLocation(service);
newTrackPoint(service);
// then
Assert.assertTrue(service.isRecording());
@@ -201,14 +201,14 @@ public class TrackRecordingServiceTest {
// given
createDummyTrack(trackId, -1L, true);
TrackRecordingServiceInterface service = ((TrackRecordingServiceInterface) mServiceRule.bindService(createStartIntent(context)));
insertLocation(service);
newTrackPoint(service);
service.endCurrentTrack();
Assert.assertEquals(1, contentProviderUtils.getTrackPoints(trackId).size());
// when
service.resumeTrack(trackId);
insertLocation(service);
newTrackPoint(service);
// then
Assert.assertTrue(service.isRecording());
@@ -294,7 +294,7 @@ public class TrackRecordingServiceTest {
TrackRecordingServiceInterface service = ((TrackRecordingServiceInterface) mServiceRule.bindService(createStartIntent(context)));
service.startNewTrack();
Assert.assertTrue(service.isRecording());
insertLocation(service);
newTrackPoint(service);
long trackId = service.getRecordingTrackId();
// when
@@ -331,18 +331,18 @@ public class TrackRecordingServiceTest {
addTrack(dummyTrack, isRecording);
}
private static void insertLocation(TrackRecordingServiceInterface trackRecordingService) throws InterruptedException {
insertLocation(trackRecordingService, 45.0f, 35f, 5, 10, System.currentTimeMillis());
private static void newTrackPoint(TrackRecordingServiceInterface trackRecordingService) throws InterruptedException {
newTrackPoint(trackRecordingService, 45.0f, 35f, 5, 10, System.currentTimeMillis());
}
static void insertLocation(TrackRecordingServiceInterface trackRecordingService, double latitude, double longitude, float accuracy, long speed) throws InterruptedException {
insertLocation(trackRecordingService, latitude, longitude, accuracy, speed, System.currentTimeMillis());
static void newTrackPoint(TrackRecordingServiceInterface trackRecordingService, double latitude, double longitude, float accuracy, long speed) throws InterruptedException {
newTrackPoint(trackRecordingService, latitude, longitude, accuracy, speed, System.currentTimeMillis());
}
/**
* Inserts a location and waits for 200ms.
*/
private static void insertLocation(TrackRecordingServiceInterface trackRecordingService, double latitude, double longitude, float accuracy, long speed, long time) throws InterruptedException {
private static void newTrackPoint(TrackRecordingServiceInterface trackRecordingService, double latitude, double longitude, float accuracy, long speed, long time) throws InterruptedException {
Location location = new Location("gps");
location.setLongitude(longitude);
location.setLatitude(latitude);
@@ -350,7 +350,9 @@ public class TrackRecordingServiceTest {
location.setSpeed(speed);
location.setTime(time);
location.setBearing(3.0f);
trackRecordingService.insertLocation(location);
TrackPoint trackPoint = new TrackPoint(location);
int prefAccuracy = PreferencesUtils.getRecordingGPSAccuracy(ApplicationProvider.getApplicationContext());
trackRecordingService.newTrackPoint(trackPoint, prefAccuracy);
//TODO Needed?
Thread.sleep(200);
@@ -72,14 +72,11 @@ public class TrackRecordingServiceTestLocation {
sharedPreferences.edit().clear().commit();
service = ((TrackRecordingServiceInterface) mServiceRule.bindService(TrackRecordingServiceTest.createStartIntent(context)));
//Disable executorService to not insert locations from GPS via LocationManager
service.enableLocationExecutor(false);
}
@After
public void tearDown() {
// Reset service (if some previous test failed)
service.enableLocationExecutor(true);
if (service.isRecording() || service.isPaused()) {
service.endCurrentTrack();
}
@@ -95,12 +92,12 @@ public class TrackRecordingServiceTestLocation {
long trackId = service.startNewTrack();
// when
TrackRecordingServiceTest.insertLocation(service, 45.0, 35.0, 5, 15);
TrackRecordingServiceTest.insertLocation(service, 45.0001, 35.0, 5, 15);
TrackRecordingServiceTest.insertLocation(service, 45.0002, 35.0, 5, 15);
TrackRecordingServiceTest.insertLocation(service, 45.0003, 35.0, 5, 15);
TrackRecordingServiceTest.insertLocation(service, 45.0004, 35.0, 5, 15);
TrackRecordingServiceTest.insertLocation(service, 45.0005, 35.0, 5, 15);
TrackRecordingServiceTest.newTrackPoint(service, 45.0, 35.0, 5, 15);
TrackRecordingServiceTest.newTrackPoint(service, 45.0001, 35.0, 5, 15);
TrackRecordingServiceTest.newTrackPoint(service, 45.0002, 35.0, 5, 15);
TrackRecordingServiceTest.newTrackPoint(service, 45.0003, 35.0, 5, 15);
TrackRecordingServiceTest.newTrackPoint(service, 45.0004, 35.0, 5, 15);
TrackRecordingServiceTest.newTrackPoint(service, 45.0005, 35.0, 5, 15);
service.endCurrentTrack();
@@ -112,29 +109,6 @@ public class TrackRecordingServiceTestLocation {
Assert.assertEquals(45.0005, trackPoints.get(5).getLatitude(), 0.01);
}
@MediumTest
@Test
public void testOnLocationChangedAsync_movingInaccurate() throws Exception {
// given
long trackId = service.startNewTrack();
// when
TrackRecordingServiceTest.insertLocation(service, 45.0, 35.0, 5, 15);
TrackRecordingServiceTest.insertLocation(service, 45.1, 35.0, Long.MAX_VALUE, 15);
TrackRecordingServiceTest.insertLocation(service, 45.2, 35.0, Long.MAX_VALUE, 15);
TrackRecordingServiceTest.insertLocation(service, 45.3, 35.0, Long.MAX_VALUE, 15);
TrackRecordingServiceTest.insertLocation(service, 99.0, 35.0, Long.MAX_VALUE, 15);
service.endCurrentTrack();
// then
Assert.assertFalse(service.isRecording());
List<TrackPoint> trackPoints = contentProviderUtils.getTrackPoints(trackId);
Assert.assertEquals(1, trackPoints.size());
Assert.assertEquals(45.0, trackPoints.get(0).getLatitude(), 0.01);
}
@MediumTest
@Test
public void testOnLocationChangedAsync_slowMovingAccurate() throws Exception {
@@ -142,12 +116,12 @@ public class TrackRecordingServiceTestLocation {
long trackId = service.startNewTrack();
// when
TrackRecordingServiceTest.insertLocation(service, 45.0, 35.0, 5, 15);
TrackRecordingServiceTest.insertLocation(service, 45.000001, 35.0, 5, 15);
TrackRecordingServiceTest.insertLocation(service, 45.000002, 35.0, 5, 15);
TrackRecordingServiceTest.insertLocation(service, 45.000003, 35.0, 5, 15);
TrackRecordingServiceTest.insertLocation(service, 45.000004, 35.0, 5, 15);
TrackRecordingServiceTest.insertLocation(service, 45.000005, 35.0, 5, 15);
TrackRecordingServiceTest.newTrackPoint(service, 45.0, 35.0, 5, 15);
TrackRecordingServiceTest.newTrackPoint(service, 45.000001, 35.0, 5, 15);
TrackRecordingServiceTest.newTrackPoint(service, 45.000002, 35.0, 5, 15);
TrackRecordingServiceTest.newTrackPoint(service, 45.000003, 35.0, 5, 15);
TrackRecordingServiceTest.newTrackPoint(service, 45.000004, 35.0, 5, 15);
TrackRecordingServiceTest.newTrackPoint(service, 45.000005, 35.0, 5, 15);
service.endCurrentTrack();
@@ -184,12 +158,12 @@ public class TrackRecordingServiceTestLocation {
long trackId = service.startNewTrack();
// when
TrackRecordingServiceTest.insertLocation(service, 45.0, 35.0, 1, 0);
TrackRecordingServiceTest.insertLocation(service, 45.0, 35.0, 2, 0);
TrackRecordingServiceTest.insertLocation(service, 45.0, 35.0, 3, 0);
TrackRecordingServiceTest.insertLocation(service, 45.0, 35.0, 4, 0);
TrackRecordingServiceTest.insertLocation(service, 45.0, 35.0, 5, 0);
TrackRecordingServiceTest.insertLocation(service, 45.0, 35.0, 6, 0);
TrackRecordingServiceTest.newTrackPoint(service, 45.0, 35.0, 1, 0);
TrackRecordingServiceTest.newTrackPoint(service, 45.0, 35.0, 2, 0);
TrackRecordingServiceTest.newTrackPoint(service, 45.0, 35.0, 3, 0);
TrackRecordingServiceTest.newTrackPoint(service, 45.0, 35.0, 4, 0);
TrackRecordingServiceTest.newTrackPoint(service, 45.0, 35.0, 5, 0);
TrackRecordingServiceTest.newTrackPoint(service, 45.0, 35.0, 6, 0);
service.endCurrentTrack();
@@ -210,12 +184,12 @@ public class TrackRecordingServiceTestLocation {
long trackId = service.startNewTrack();
// when
TrackRecordingServiceTest.insertLocation(service, 45.0, 35.0, 1, 15);
TrackRecordingServiceTest.insertLocation(service, 45.0, 35.0, 2, 0);
TrackRecordingServiceTest.insertLocation(service, 45.0, 35.0, 3, 0);
TrackRecordingServiceTest.insertLocation(service, 45.0, 35.0, 4, 0);
TrackRecordingServiceTest.insertLocation(service, 45.0, 35.0, 5, 0);
TrackRecordingServiceTest.insertLocation(service, 45.0, 35.0, 6, 15);
TrackRecordingServiceTest.newTrackPoint(service, 45.0, 35.0, 1, 15);
TrackRecordingServiceTest.newTrackPoint(service, 45.0, 35.0, 2, 0);
TrackRecordingServiceTest.newTrackPoint(service, 45.0, 35.0, 3, 0);
TrackRecordingServiceTest.newTrackPoint(service, 45.0, 35.0, 4, 0);
TrackRecordingServiceTest.newTrackPoint(service, 45.0, 35.0, 5, 0);
TrackRecordingServiceTest.newTrackPoint(service, 45.0, 35.0, 6, 15);
service.endCurrentTrack();
@@ -253,12 +227,12 @@ public class TrackRecordingServiceTestLocation {
});
// when
TrackRecordingServiceTest.insertLocation(service, 45.0, 35.0, 0, 0);
TrackRecordingServiceTest.insertLocation(service, 45.0, 35.0, 1, 0);
TrackRecordingServiceTest.insertLocation(service, 45.0, 35.0, 2, 0);
TrackRecordingServiceTest.insertLocation(service, 45.0, 35.0, 3, 0);
TrackRecordingServiceTest.insertLocation(service, 45.0, 35.0, 4, 0);
TrackRecordingServiceTest.insertLocation(service, 45.0, 35.0, 5, 0);
TrackRecordingServiceTest.newTrackPoint(service, 45.0, 35.0, 0, 0);
TrackRecordingServiceTest.newTrackPoint(service, 45.0, 35.0, 1, 0);
TrackRecordingServiceTest.newTrackPoint(service, 45.0, 35.0, 2, 0);
TrackRecordingServiceTest.newTrackPoint(service, 45.0, 35.0, 3, 0);
TrackRecordingServiceTest.newTrackPoint(service, 45.0, 35.0, 4, 0);
TrackRecordingServiceTest.newTrackPoint(service, 45.0, 35.0, 5, 0);
service.endCurrentTrack();
@@ -282,11 +256,11 @@ public class TrackRecordingServiceTestLocation {
long trackId = service.startNewTrack();
// when
TrackRecordingServiceTest.insertLocation(service, 45.0, 35.0, 1, 0);
TrackRecordingServiceTest.insertLocation(service, 45.1, 35.0, 2, 0);
TrackRecordingServiceTest.insertLocation(service, 45.1, 35.0, 3, 0);
TrackRecordingServiceTest.insertLocation(service, 45.2, 35.0, 4, 0);
TrackRecordingServiceTest.insertLocation(service, 45.2, 35.0, 5, 0);
TrackRecordingServiceTest.newTrackPoint(service, 45.0, 35.0, 1, 0);
TrackRecordingServiceTest.newTrackPoint(service, 45.1, 35.0, 2, 0);
TrackRecordingServiceTest.newTrackPoint(service, 45.1, 35.0, 3, 0);
TrackRecordingServiceTest.newTrackPoint(service, 45.2, 35.0, 4, 0);
TrackRecordingServiceTest.newTrackPoint(service, 45.2, 35.0, 5, 0);
service.endCurrentTrack();
@@ -303,25 +277,4 @@ public class TrackRecordingServiceTestLocation {
Assert.assertEquals(4, trackPoints.get(5).getAccuracy(), 0.01);
Assert.assertEquals(5, trackPoints.get(6).getAccuracy(), 0.01);
}
@MediumTest
@Test
public void testOnLocationChangedAsync_firstTrackPointInvalid() throws Exception {
// given
long trackId = service.startNewTrack();
// when
service.insertLocation(TrackPoint.createPause().getLocation());
TrackRecordingServiceTest.insertLocation(service, 45.0, 35.0, 0, 0);
service.insertLocation(TrackPoint.createPause().getLocation());
service.endCurrentTrack();
// then
Assert.assertFalse(service.isRecording());
List<TrackPoint> trackPoints = contentProviderUtils.getTrackPoints(trackId);
Assert.assertEquals(1, trackPoints.size());
Assert.assertEquals(0, trackPoints.get(0).getAccuracy(), 0.01);
}
}
@@ -26,6 +26,7 @@ import java.util.concurrent.TimeoutException;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
import de.dennisguse.opentracks.content.provider.CustomContentProvider;
import de.dennisguse.opentracks.stats.TrackStatistics;
@@ -215,7 +216,9 @@ public class TrackRecordingServiceTestLooper {
location.setSpeed(10);
location.setTime(startTime + i * 10000);
location.setBearing(3.0f);
service.insertLocation(location);
TrackPoint trackPoint = new TrackPoint(location);
int prefAccuracy = PreferencesUtils.getRecordingGPSAccuracy(context);
service.newTrackPoint(trackPoint, prefAccuracy);
if (i % 7 == 0) {
service.insertWaypoint(null, null, null, null);
@@ -0,0 +1,63 @@
package de.dennisguse.opentracks.services.handlers;
import android.content.Context;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import de.dennisguse.opentracks.content.data.TrackPoint;
import static org.mockito.Mockito.verify;
@RunWith(MockitoJUnitRunner.class)
public class HandlerServerTest {
@Mock
private Context context;
@Mock
private HandlerServer.HandlerServerInterface server;
@Mock
private LocationHandler locationHandler;
private HandlerServer subject;
@Before
public void setUp() {
subject = new HandlerServer(locationHandler, server);
subject.start(context);
}
@After
public void tearDown() {
subject.stop(context);
}
@Test
public void onSharedPreferenceChanged() {
// when
subject.onSharedPreferenceChanged(null, null, null);
// then
verify(locationHandler).onSharedPreferenceChanged(null, null, null);
}
@Test
public void sendTrackPoint() throws InterruptedException {
// given
TrackPoint trackPoint = new TrackPoint();
int accuracy = 50;
// when
subject.sendTrackPoint(trackPoint, accuracy);
// then
Thread.sleep(10); // Wait for executor service
verify(server).newTrackPoint(trackPoint, accuracy);
}
}
@@ -0,0 +1,117 @@
package de.dennisguse.opentracks.services.handlers;
import android.content.Context;
import android.content.SharedPreferences;
import android.location.Location;
import androidx.test.core.app.ApplicationProvider;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.util.PreferencesUtils;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
@RunWith(MockitoJUnitRunner.class)
public class LocationHandlerTest {
private final Context context = ApplicationProvider.getApplicationContext();
@Mock
private HandlerServer handlerServer;
@InjectMocks
private LocationHandler locationHandler;
@Before
public void setUp() {
// Let's use default values.
SharedPreferences sharedPreferences = PreferencesUtils.getSharedPreferences(context);
sharedPreferences.edit().clear().commit();
locationHandler.onSharedPreferenceChanged(context, PreferencesUtils.getSharedPreferences(context), context.getString(R.string.recording_gps_accuracy_key));
locationHandler.onSharedPreferenceChanged(context, PreferencesUtils.getSharedPreferences(context), context.getString(R.string.min_recording_interval_key));
locationHandler.onStart(context);
}
/**
* When a valid location changed in LocationHandler -> newTrackPoint service method is called.
*/
@Test
public void testOnLocationChanged_okay() {
// when
locationHandler.onLocationChanged(createLocation(45f, 35f, 3, 5, System.currentTimeMillis()));
// then
verify(handlerServer, times(1)).sendTrackPoint(any(TrackPoint.class), any(Integer.class));
}
/**
* When location changed in LocationHandler with bad location -> newTrackPoint service method is not called.
*/
@Test
public void testOnLocationChanged_badLocation() {
// given
float latitude = 91f;
// when
// bad latitude
locationHandler.onLocationChanged(createLocation(latitude, 35f, 3, 5, System.currentTimeMillis()));
// then
verify(handlerServer, times(0)).sendTrackPoint(any(TrackPoint.class), any(Integer.class));
}
/**
* When location changed in LocationHandler with poor accuracy -> newTrackPoint service method is not called.
*/
@Test
public void testOnLocationChanged_poorAccuracy() {
// given
int prefAccuracy = PreferencesUtils.getRecordingGPSAccuracy(context);
// when
// poor latitude
locationHandler.onLocationChanged(createLocation(45f, 35f, prefAccuracy + 1, 5, System.currentTimeMillis()));
// then
// no newTrackPoint called
verify(handlerServer, times(0)).sendTrackPoint(any(TrackPoint.class), any(Integer.class));
}
@Test
public void testOnLocationChanged_movingInaccurate() {
// when
locationHandler.onLocationChanged(createLocation(45.0, 35.0, 5, 15, System.currentTimeMillis()));
locationHandler.onLocationChanged(createLocation(45.1, 35.0, Long.MAX_VALUE, 15, System.currentTimeMillis()));
locationHandler.onLocationChanged(createLocation(45.2, 35.0, Long.MAX_VALUE, 15, System.currentTimeMillis()));
locationHandler.onLocationChanged(createLocation(45.3, 35.0, Long.MAX_VALUE, 15, System.currentTimeMillis()));
locationHandler.onLocationChanged(createLocation(99.0, 35.0, Long.MAX_VALUE, 15, System.currentTimeMillis()));
// then
verify(handlerServer, times(1)).sendTrackPoint(any(TrackPoint.class), any(Integer.class));
}
/**
* Creates a location with parameters and returns the Location object.
*/
private static Location createLocation(double latitude, double longitude, float accuracy, long speed, long time) {
Location location = new Location("gps");
location.setLongitude(longitude);
location.setLatitude(latitude);
location.setAccuracy(accuracy);
location.setSpeed(speed);
location.setTime(time);
location.setBearing(3.0f);
return location;
}
}
@@ -60,7 +60,7 @@ public class AggregatedStatisticsTest {
// then
Assert.assertEquals(1, aggregatedStatistics.getCount());
Assert.assertTrue(aggregatedStatistics.get(biking) != null);
Assert.assertNotNull(aggregatedStatistics.get(biking));
Assert.assertEquals(1, aggregatedStatistics.get(biking).getCountTracks());
TrackStatistics statistics2 = aggregatedStatistics.get(biking).getTrackStatistics();
@@ -82,7 +82,7 @@ public class AggregatedStatisticsTest {
aggregatedStatistics.aggregate(track);
// then
Assert.assertTrue(aggregatedStatistics.get(mountainBiking) != null);
Assert.assertNotNull(aggregatedStatistics.get(mountainBiking));
}
@Test
@@ -99,7 +99,7 @@ public class AggregatedStatisticsTest {
aggregatedStatistics.aggregate(track);
// then
Assert.assertTrue(aggregatedStatistics.get(trailRunning) != null);
Assert.assertNotNull(aggregatedStatistics.get(trailRunning));
}
@Test
@@ -121,7 +121,7 @@ public class AggregatedStatisticsTest {
// then
Assert.assertEquals(1, aggregatedStatistics.getCount());
Assert.assertTrue(aggregatedStatistics.get(biking) != null);
Assert.assertNotNull(aggregatedStatistics.get(biking));
Assert.assertEquals(2, aggregatedStatistics.get(biking).getCountTracks());
TrackStatistics statistics2 = aggregatedStatistics.get(biking).getTrackStatistics();
@@ -153,9 +153,9 @@ public class AggregatedStatisticsTest {
// then
Assert.assertEquals(3, aggregatedStatistics.getCount());
Assert.assertTrue(aggregatedStatistics.get(biking) != null);
Assert.assertTrue(aggregatedStatistics.get(running) != null);
Assert.assertTrue(aggregatedStatistics.get(walking) != null);
Assert.assertNotNull(aggregatedStatistics.get(biking));
Assert.assertNotNull(aggregatedStatistics.get(running));
Assert.assertNotNull(aggregatedStatistics.get(walking));
Assert.assertEquals(1, aggregatedStatistics.get(biking).getCountTracks());
Assert.assertEquals(1, aggregatedStatistics.get(running).getCountTracks());
Assert.assertEquals(1, aggregatedStatistics.get(walking).getCountTracks());
@@ -215,10 +215,10 @@ public class AggregatedStatisticsTest {
Assert.assertEquals(4, aggregatedStatistics.getCount());
// There is a map for every sport.
Assert.assertTrue(aggregatedStatistics.get(biking) != null);
Assert.assertTrue(aggregatedStatistics.get(running) != null);
Assert.assertTrue(aggregatedStatistics.get(walking) != null);
Assert.assertTrue(aggregatedStatistics.get(driving) != null);
Assert.assertNotNull(aggregatedStatistics.get(biking));
Assert.assertNotNull(aggregatedStatistics.get(running));
Assert.assertNotNull(aggregatedStatistics.get(walking));
Assert.assertNotNull(aggregatedStatistics.get(driving));
// Number of tracks by sport.
Assert.assertEquals(5, aggregatedStatistics.get(biking).getCountTracks()); // Biking.
+56 -34
View File
@@ -14,6 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="de.dennisguse.opentracks"
android:installLocation="auto">
@@ -24,6 +25,28 @@ limitations under the License.
android:smallScreens="true"
android:xlargeScreens="true" />
<uses-feature
android:name="android.hardware.location.gps"
android:required="true" />
<uses-feature
android:name="android.hardware.android.hardware.bluetooth_le"
android:required="false" />
<uses-feature
android:name="android.hardware.camera"
android:required="false" />
<uses-feature
android:name="android.hardware.sensor.compass"
android:required="false" />
<uses-feature
android:name="android.hardware.touchscreen"
android:required="false" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<application
android:name=".Startup"
android:allowBackup="false"
@@ -33,7 +56,6 @@ limitations under the License.
android:label="${applicationName}"
android:theme="@style/ThemeCustom">
<!-- Activities -->
<activity android:name=".AboutActivity" />
<activity
@@ -49,22 +71,45 @@ limitations under the License.
<activity
android:name=".io.file.importer.ImportActivity"
android:theme="@style/ThemeCustomTransparentActivity">
<intent-filter>
<action android:name="android.intent.action.ATTACH_DATA" />
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.BROWSABLE" />
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="application/gpx" />
<data android:mimeType="application/gpx+xml" />
<data android:mimeType="application/vnd.google-earth.gpx" />
<data android:mimeType="application/vnd.google-earth.gpx+xml" />
<data android:scheme="file" />
<data android:mimeType="application/vnd.google-earth.kmz" />
<data
android:scheme="content"
android:pathPattern="*kmz" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="application/vnd.google-earth.kml+xml" />
<data
android:scheme="content"
android:pathPattern="*kml" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="application/gpx+xml" />
<data
android:scheme="content"
android:pathPattern="*gpx" />
</intent-filter>
</activity>
<activity android:name=".MarkerDetailActivity" />
<activity android:name=".MarkerDetailActivity">
<intent-filter>
<action android:name="de.dennisguse.opentracks.MarkerDetails"/>
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
<activity android:name=".MarkerEditActivity" />
@@ -150,27 +195,4 @@ limitations under the License.
android:icon="@drawable/ic_logo_color_24dp"
android:label="@string/recording_service" />
</application>
<!-- Permissions to record locations -->
<uses-feature
android:name="android.hardware.location.gps"
android:required="true" />
<uses-feature
android:name="android.hardware.android.hardware.bluetooth_le"
android:required="false" />
<uses-feature
android:name="android.hardware.camera"
android:required="false" />
<uses-feature
android:name="android.hardware.sensor.compass"
android:required="false" />
<uses-feature
android:name="android.hardware.touchscreen"
android:required="false" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
</manifest>
@@ -3,11 +3,9 @@ package de.dennisguse.opentracks;
import android.os.Bundle;
import android.widget.ListView;
import androidx.lifecycle.Observer;
import androidx.lifecycle.ViewModelProvider;
import de.dennisguse.opentracks.adapters.AggregatedStatisticsAdapter;
import de.dennisguse.opentracks.viewmodels.AggregatedStatistics;
import de.dennisguse.opentracks.viewmodels.AggregatedStatisticsModel;
public class AggregatedStatisticsActivity extends AbstractActivity {
@@ -23,15 +21,12 @@ public class AggregatedStatisticsActivity extends AbstractActivity {
listView.setEmptyView(findViewById(R.id.aggregated_stats_empty_view));
final AggregatedStatisticsModel viewModel = new ViewModelProvider(this).get(AggregatedStatisticsModel.class);
viewModel.getAggregatedStats().observe(this, new Observer<AggregatedStatistics>() {
@Override
public void onChanged(AggregatedStatistics aggregatedStatistics) {
if (aggregatedStatistics != null) {
adapter = new AggregatedStatisticsAdapter(getApplicationContext(), aggregatedStatistics);
listView.setAdapter(adapter);
}
adapter.notifyDataSetChanged();
viewModel.getAggregatedStats().observe(this, aggregatedStatistics -> {
if (aggregatedStatistics != null) {
adapter = new AggregatedStatisticsAdapter(getApplicationContext(), aggregatedStatistics);
listView.setAdapter(adapter);
}
adapter.notifyDataSetChanged();
});
}
@@ -120,7 +120,7 @@ public class MarkerDetailActivity extends AbstractActivity implements DeleteMark
private class MarkerDetailPagerAdapter extends FragmentStatePagerAdapter {
MarkerDetailPagerAdapter(FragmentManager fragmentManager) {
super(fragmentManager);
super(fragmentManager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT);
}
@Override
@@ -29,7 +29,6 @@ import android.util.Pair;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.ArrayAdapter;
import android.widget.AutoCompleteTextView;
import android.widget.Button;
@@ -59,13 +58,14 @@ public class MarkerEditActivity extends AbstractActivity {
public static final String EXTRA_TRACK_ID = "track_id";
public static final String EXTRA_MARKER_ID = "marker_id";
private static final String BUNDLE_PHOTO_URI = "photo_uri";
private static final int CAMERA_REQUEST_CODE = 5;
private static final int GALLERY_IMG_REQUEST_CODE = 7;
private static final String TAG = MarkerEditActivity.class.getSimpleName();
private long trackId;
private long markerId;
private TrackRecordingServiceConnection trackRecordingServiceConnection;
private TrackRecordingServiceConnection trackRecordingServiceConnection = new TrackRecordingServiceConnection();
private Waypoint waypoint;
private MenuItem insertPhotoMenuItem;
@@ -80,15 +80,13 @@ public class MarkerEditActivity extends AbstractActivity {
private EditText waypointDescription;
private ImageView waypointPhoto;
private ImageView waypointDeletePhotoBtn;
private Button done;
@Override
protected void onCreate(Bundle bundle) {
super.onCreate(bundle);
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
trackId = getIntent().getLongExtra(EXTRA_TRACK_ID, -1L);
markerId = getIntent().getLongExtra(EXTRA_MARKER_ID, -1L);
trackRecordingServiceConnection = new TrackRecordingServiceConnection(null);
long markerId = getIntent().getLongExtra(EXTRA_MARKER_ID, -1L);
hasCamera = getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY);
@@ -101,26 +99,65 @@ public class MarkerEditActivity extends AbstractActivity {
waypointPhoto = findViewById(R.id.marker_edit_waypoint_photo);
waypointDeletePhotoBtn = findViewById(R.id.marker_edit_waypoint_photo_delete);
waypointDeletePhotoBtn.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if (waypoint != null && waypoint.hasPhoto())
waypoint.setPhotoUrl(null);
waypointPhoto.setImageBitmap(null);
photoUri = null;
hideAndShowOptions();
waypointDeletePhotoBtn.setOnClickListener(v -> {
if (waypoint != null && waypoint.hasPhoto()) {
waypoint.setPhotoUrl(null);
}
waypointPhoto.setImageBitmap(null);
photoUri = null;
hideAndShowOptions();
});
Button cancel = findViewById(R.id.marker_edit_cancel);
cancel.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
finish();
cancel.setOnClickListener(v -> finish());
final boolean isNewMarker = markerId == -1L;
setTitle(isNewMarker ? R.string.menu_insert_marker : R.string.menu_edit);
Button done = findViewById(R.id.marker_edit_done);
done.setText(isNewMarker ? R.string.generic_add : R.string.generic_save);
done.setOnClickListener(v -> {
if (isNewMarker) {
addMarker();
} else {
saveMarker();
}
finish();
});
done = findViewById(R.id.marker_edit_done);
updateUiByMarkerId();
if (isNewMarker) {
int nextWaypointNumber = trackId == -1L ? -1 : new ContentProviderUtils(this).getNextWaypointNumber(trackId);
if (nextWaypointNumber == -1) {
nextWaypointNumber = 0;
}
waypointName.setText(getString(R.string.marker_name_format, nextWaypointNumber));
waypointName.selectAll();
waypointMarkerType.setText("");
waypointDescription.setText("");
} else {
waypoint = new ContentProviderUtils(this).getWaypoint(markerId);
if (waypoint == null) {
Log.d(TAG, "waypoint is null");
finish();
return;
}
waypointName.setText(waypoint.getName());
waypointMarkerType.setText(waypoint.getCategory());
waypointDescription.setText(waypoint.getDescription());
if (waypoint.hasPhoto()) {
photoUri = waypoint.getPhotoURI();
}
}
if (savedInstanceState != null) {
photoUri = savedInstanceState.getParcelable(BUNDLE_PHOTO_URI);
waypoint.setPhotoUrl(photoUri != null ? photoUri.toString() : null);
}
if (photoUri != null) {
setWaypointImageView(photoUri);
}
hideAndShowOptions();
}
@Override
@@ -129,6 +166,12 @@ public class MarkerEditActivity extends AbstractActivity {
trackRecordingServiceConnection.startConnection(this);
}
@Override
protected void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
outState.putParcelable(BUNDLE_PHOTO_URI, photoUri);
}
@Override
protected void onStop() {
super.onStop();
@@ -198,54 +241,6 @@ public class MarkerEditActivity extends AbstractActivity {
super.onActivityResult(requestCode, resultCode, data);
}
/**
* Updates the UI based on the marker id.
*/
private void updateUiByMarkerId() {
final boolean newMarker = markerId == -1L;
setTitle(newMarker ? R.string.menu_insert_marker : R.string.menu_edit);
done.setText(newMarker ? R.string.generic_add : R.string.generic_save);
done.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (newMarker) {
addMarker();
} else {
saveMarker();
}
finish();
}
});
if (newMarker) {
int nextWaypointNumber = trackId == -1L ? -1 : new ContentProviderUtils(this).getNextWaypointNumber(trackId);
if (nextWaypointNumber == -1) {
nextWaypointNumber = 0;
}
waypointName.setText(getString(R.string.marker_name_format, nextWaypointNumber));
waypointName.selectAll();
waypointMarkerType.setText("");
waypointDescription.setText("");
} else {
waypoint = new ContentProviderUtils(this).getWaypoint(markerId);
if (waypoint == null) {
Log.d(TAG, "waypoint is null");
finish();
return;
}
waypointName.setText(waypoint.getName());
waypointMarkerType.setText(waypoint.getCategory());
waypointDescription.setText(waypoint.getDescription());
if (waypoint.hasPhoto()) {
photoUri = waypoint.getPhotoURI();
setWaypointImageView(photoUri);
}
}
hideAndShowOptions();
}
/**
* Checks and hide/shows all buttons/options about marker photo options.
*
@@ -272,13 +267,13 @@ public class MarkerEditActivity extends AbstractActivity {
*
* @param uri the uri photo.
*/
private void setWaypointImageView(Uri uri) {
private void setWaypointImageView(@NonNull Uri uri) {
try (ParcelFileDescriptor pfd = getContentResolver().openFileDescriptor(uri, "r")) {
FileDescriptor fd = pfd.getFileDescriptor();
Bitmap bitmap = BitmapFactory.decodeFileDescriptor(fd);
waypointPhoto.setImageBitmap(bitmap);
hideAndShowOptions();
} catch(IOException e) {
} catch (IOException e) {
Log.e(TAG, e.getMessage());
Toast.makeText(this, R.string.marker_add_photo_canceled, Toast.LENGTH_LONG).show();
}
@@ -295,9 +290,6 @@ public class MarkerEditActivity extends AbstractActivity {
startActivityForResult(intent, GALLERY_IMG_REQUEST_CODE);
}
/**
* Adds a marker.
*/
private void addMarker() {
trackRecordingServiceConnection.addMarker(this,
waypointName.getText().toString(),
@@ -306,9 +298,6 @@ public class MarkerEditActivity extends AbstractActivity {
photoUri != null ? photoUri.toString() : null);
}
/**
* Saves a marker.
*/
private void saveMarker() {
waypoint.setName(waypointName.getText().toString());
waypoint.setCategory(waypointMarkerType.getText().toString());
@@ -26,8 +26,6 @@ import android.view.KeyEvent;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.ListView;
import androidx.annotation.NonNull;
@@ -82,7 +80,7 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
recordingTrackPaused = PreferencesUtils.isRecordingTrackPaused(MarkerListActivity.this);
}
if (key != null) {
runOnUiThread(() -> MarkerListActivity.this.invalidateOptionsMenu());
runOnUiThread(MarkerListActivity.this::invalidateOptionsMenu);
}
}
};
@@ -129,13 +127,10 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
listView = findViewById(R.id.marker_list);
listView.setEmptyView(findViewById(R.id.marker_list_empty));
listView.setOnItemClickListener(new OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
Intent intent = IntentUtils.newIntent(MarkerListActivity.this, MarkerDetailActivity.class)
.putExtra(MarkerDetailActivity.EXTRA_MARKER_ID, id);
startActivity(intent);
}
listView.setOnItemClickListener((parent, view, position, id) -> {
Intent intent = IntentUtils.newIntent(MarkerListActivity.this, MarkerDetailActivity.class)
.putExtra(MarkerDetailActivity.EXTRA_MARKER_ID, id);
startActivity(intent);
});
resourceCursorAdapter = new ResourceCursorAdapter(this, R.layout.list_item, null, 0) {
@Override
@@ -272,8 +267,7 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
DeleteMarkerDialogFragment.showDialog(getSupportFragmentManager(), markerIds);
return true;
case R.id.list_context_menu_select_all:
int size = listView.getCount();
for (int i = 0; i < size; i++) {
for (int i = 0; i < listView.getCount(); i++) {
listView.setItemChecked(i, true);
}
return false;
@@ -26,8 +26,6 @@ import android.view.KeyEvent;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.ArrayAdapter;
import android.widget.ListView;
@@ -203,20 +201,17 @@ public class SearchListActivity extends AbstractListActivity implements DeleteMa
ListView listView = findViewById(R.id.search_list);
listView.setAdapter(arrayAdapter);
listView.setEmptyView(findViewById(R.id.search_list_empty));
listView.setOnItemClickListener(new OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
Map<String, Object> item = arrayAdapter.getItem(position);
Long trackId = (Long) item.get(TRACK_ID_FIELD);
Long markerId = (Long) item.get(MARKER_ID_FIELD);
Intent intent = IntentUtils.newIntent(SearchListActivity.this, TrackRecordedActivity.class);
if (markerId != null) {
intent = intent.putExtra(TrackRecordedActivity.EXTRA_MARKER_ID, markerId);
} else {
intent = intent.putExtra(TrackRecordedActivity.EXTRA_TRACK_ID, trackId);
}
startActivity(intent);
listView.setOnItemClickListener((parent, view, position, id) -> {
Map<String, Object> item = arrayAdapter.getItem(position);
Long trackId = (Long) item.get(TRACK_ID_FIELD);
Long markerId = (Long) item.get(MARKER_ID_FIELD);
Intent intent = IntentUtils.newIntent(SearchListActivity.this, TrackRecordedActivity.class);
if (markerId != null) {
intent = intent.putExtra(TrackRecordedActivity.EXTRA_MARKER_ID, markerId);
} else {
intent = intent.putExtra(TrackRecordedActivity.EXTRA_TRACK_ID, trackId);
}
startActivity(intent);
});
ActivityUtils.configureListViewContextualMenu(listView, contextualActionModeCallback);
handleIntent(getIntent());
@@ -18,7 +18,7 @@ import de.dennisguse.opentracks.util.IntentDashboardUtils;
*/
public abstract class ShowOnMapProxyActivity extends AppCompatActivity {
private TrackFileFormat trackFileFormat;
private final TrackFileFormat trackFileFormat;
protected ShowOnMapProxyActivity(TrackFileFormat trackFileFormat) {
this.trackFileFormat = trackFileFormat;
@@ -4,6 +4,8 @@ import android.app.Application;
import android.os.StrictMode;
import android.util.Log;
import java.util.Locale;
import de.dennisguse.opentracks.util.PreferencesUtils;
/**
@@ -25,6 +27,10 @@ public class Startup extends Application {
//Set default values of preferences on first start.
PreferencesUtils.resetPreferences(this, false);
if (PreferencesUtils.getString(this, R.string.stats_units_key, "").equals("")) {
String statsUnits = getString(Locale.US.equals(Locale.getDefault()) ? R.string.stats_units_imperial : R.string.stats_units_metric);
PreferencesUtils.setString(this, R.string.stats_units_key, statsUnits);
}
//In debug builds: show thread and VM warnings.
@@ -21,7 +21,6 @@ import android.util.Log;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.AutoCompleteTextView;
import android.widget.Button;
@@ -89,20 +88,12 @@ public class TrackEditActivity extends AbstractActivity implements ChooseActivit
ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(this, R.array.activity_types, android.R.layout.simple_dropdown_item_1line);
activityType.setAdapter(adapter);
activityType.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
activityType.setOnItemClickListener((parent, view, position, id) -> setActivityTypeIcon(TrackIconUtils.getIconValue(
TrackEditActivity.this, (String) activityType.getAdapter().getItem(position))));
activityType.setOnFocusChangeListener((v, hasFocus) -> {
if (!hasFocus) {
setActivityTypeIcon(TrackIconUtils.getIconValue(
TrackEditActivity.this, (String) activityType.getAdapter().getItem(position)));
}
});
activityType.setOnFocusChangeListener(new View.OnFocusChangeListener() {
@Override
public void onFocusChange(View v, boolean hasFocus) {
if (!hasFocus) {
setActivityTypeIcon(TrackIconUtils.getIconValue(
TrackEditActivity.this, activityType.getText().toString()));
}
TrackEditActivity.this, activityType.getText().toString()));
}
});
@@ -116,37 +107,28 @@ public class TrackEditActivity extends AbstractActivity implements ChooseActivit
activityTypeIcon = findViewById(R.id.track_edit_activity_type_icon);
activityTypeIcon.setAdapter(TrackIconUtils.getIconSpinnerAdapter(this, iconValue));
activityTypeIcon.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_UP) {
ChooseActivityTypeDialogFragment.showDialog(getSupportFragmentManager(), activityType.getText().toString());
}
return true;
activityTypeIcon.setOnTouchListener((v, event) -> {
if (event.getAction() == MotionEvent.ACTION_UP) {
ChooseActivityTypeDialogFragment.showDialog(getSupportFragmentManager(), activityType.getText().toString());
}
return true;
});
activityTypeIcon.setOnKeyListener(new View.OnKeyListener() {
@Override
public boolean onKey(View v, int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) {
ChooseActivityTypeDialogFragment.showDialog(getSupportFragmentManager(), activityType.getText().toString());
}
return true;
activityTypeIcon.setOnKeyListener((v, keyCode, event) -> {
if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) {
ChooseActivityTypeDialogFragment.showDialog(getSupportFragmentManager(), activityType.getText().toString());
}
return true;
});
description = findViewById(R.id.track_edit_description);
description.setText(track.getDescription());
Button saveButton = findViewById(R.id.track_edit_save);
saveButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
TrackUtils.updateTrack(TrackEditActivity.this, track, nameEditText.getText().toString(),
activityType.getText().toString(), description.getText().toString(),
contentProviderUtils);
finish();
}
saveButton.setOnClickListener(v -> {
TrackUtils.updateTrack(TrackEditActivity.this, track, nameEditText.getText().toString(),
activityType.getText().toString(), description.getText().toString(),
contentProviderUtils);
finish();
});
Button cancel = findViewById(R.id.track_edit_cancel);
@@ -155,12 +137,7 @@ public class TrackEditActivity extends AbstractActivity implements ChooseActivit
cancel.setVisibility(View.GONE);
} else {
setTitle(R.string.menu_edit);
cancel.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
finish();
}
});
cancel.setOnClickListener(v -> finish());
cancel.setVisibility(View.VISIBLE);
}
}
@@ -33,8 +33,6 @@ import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.ListView;
import android.widget.Toast;
@@ -47,8 +45,6 @@ import androidx.loader.app.LoaderManager.LoaderCallbacks;
import androidx.loader.content.CursorLoader;
import androidx.loader.content.Loader;
import java.util.Locale;
import de.dennisguse.opentracks.content.data.TracksColumns;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
import de.dennisguse.opentracks.fragments.ConfirmDeleteDialogFragment;
@@ -229,31 +225,23 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
// Show trackController when search dialog is dismissed
SearchManager searchManager = (SearchManager) getSystemService(SEARCH_SERVICE);
if (searchManager != null) {
searchManager.setOnDismissListener(new SearchManager.OnDismissListener() {
@Override
public void onDismiss() {
trackController.show();
}
});
searchManager.setOnDismissListener(() -> trackController.show());
}
listView = findViewById(R.id.track_list);
listView.setEmptyView(findViewById(R.id.track_list_empty_view));
listView.setOnItemClickListener(new OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long trackId) {
Intent newIntent;
if (trackId == recordingTrackId) {
// Is recording -> open record activity.
newIntent = IntentUtils.newIntent(TrackListActivity.this, TrackRecordingActivity.class)
.putExtra(TrackRecordedActivity.EXTRA_TRACK_ID, trackId);
} else {
// Not recording -> open detail activity.
newIntent = IntentUtils.newIntent(TrackListActivity.this, TrackRecordedActivity.class)
.putExtra(TrackRecordedActivity.EXTRA_TRACK_ID, trackId);
}
startActivity(newIntent);
listView.setOnItemClickListener((parent, view, position, trackId) -> {
Intent newIntent;
if (trackId == recordingTrackId) {
// Is recording -> open record activity.
newIntent = IntentUtils.newIntent(TrackListActivity.this, TrackRecordingActivity.class)
.putExtra(TrackRecordedActivity.EXTRA_TRACK_ID, trackId);
} else {
// Not recording -> open detail activity.
newIntent = IntentUtils.newIntent(TrackListActivity.this, TrackRecordedActivity.class)
.putExtra(TrackRecordedActivity.EXTRA_TRACK_ID, trackId);
}
startActivity(newIntent);
});
resourceCursorAdapter = new ResourceCursorAdapter(this, R.layout.list_item, null, 0) {
@@ -290,7 +278,8 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
ActivityUtils.configureListViewContextualMenu(listView, contextualActionModeCallback);
LoaderManager.getInstance(this).initLoader(0, null, loaderCallbacks);
showStartupDialogs();
requestGPSPermissions();
}
@Override
@@ -436,19 +425,6 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
// Do nothing
}
/**
* Shows start up dialogs.
*/
private void showStartupDialogs() {
// If stats_units_key is undefined, set it
if (PreferencesUtils.getString(this, R.string.stats_units_key, "").equals("")) {
String statsUnits = getString(Locale.US.equals(Locale.getDefault()) ? R.string.stats_units_imperial : R.string.stats_units_metric);
PreferencesUtils.setString(this, R.string.stats_units_key, statsUnits);
}
requestGPSPermissions();
}
private void requestGPSPermissions() {
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.ACCESS_FINE_LOCATION}, GPS_REQUEST_CODE);
@@ -482,7 +458,7 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
Intent intent;
switch (itemId) {
case R.id.list_context_menu_show_on_map:
IntentDashboardUtils.startDashboard(this, trackIds);
IntentDashboardUtils.startDashboard(this, trackIds, false);
return true;
case R.id.list_context_menu_share:
intent = IntentUtils.newShareFileIntent(this, trackIds);
@@ -187,7 +187,7 @@ public class TrackRecordedActivity extends AbstractListActivity implements Choos
startActivity(intent);
return true;
case R.id.track_detail_menu_show_on_map:
IntentDashboardUtils.startDashboard(this, trackId);
IntentDashboardUtils.startDashboard(this, new long[]{trackId}, false);
return true;
case R.id.track_detail_markers:
intent = IntentUtils.newIntent(this, MarkerListActivity.class)
@@ -110,6 +110,9 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
if (PreferencesUtils.isKey(TrackRecordingActivity.this, R.string.stats_keep_screen_on_while_recording_key, key)) {
setScreenOnPolicy();
}
if (PreferencesUtils.isKey(TrackRecordingActivity.this, R.string.stats_fullscreen_while_recording_key, key)) {
setFullscreenPolicy();
}
if (key == null) return;
@@ -190,12 +193,12 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
public void onAttachedToWindow() {
setLockscreenPolicy();
setScreenOnPolicy();
setFullscreenPolicy();
super.onAttachedToWindow();
}
private void setLockscreenPolicy() {
boolean showOnLockScreen = PreferencesUtils.shouldShowStatsOnLockscreen(TrackRecordingActivity.this)
&& PreferencesUtils.isRecording(this);
boolean showOnLockScreen = PreferencesUtils.shouldShowStatsOnLockscreen(TrackRecordingActivity.this);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
setShowWhenLocked(showOnLockScreen);
@@ -207,8 +210,7 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
}
private void setScreenOnPolicy() {
boolean keepScreenOn = PreferencesUtils.shouldKeepScreenOn(TrackRecordingActivity.this)
&& PreferencesUtils.isRecording(this);
boolean keepScreenOn = PreferencesUtils.shouldKeepScreenOn(TrackRecordingActivity.this);
if (keepScreenOn) {
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
@@ -217,6 +219,16 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
}
}
private void setFullscreenPolicy() {
boolean keepScreenOn = PreferencesUtils.shouldUseFullscreen(TrackRecordingActivity.this);
if (keepScreenOn) {
getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
} else {
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
}
}
@Override
protected void onStart() {
super.onStart();
@@ -302,7 +314,7 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
startActivity(intent);
return true;
case R.id.track_detail_menu_show_on_map:
IntentDashboardUtils.startDashboard(this, trackId);
IntentDashboardUtils.startDashboard(this, new long[]{trackId}, true);
return true;
case R.id.track_detail_markers:
intent = IntentUtils.newIntent(this, MarkerListActivity.class)
@@ -17,8 +17,8 @@ import de.dennisguse.opentracks.viewmodels.AggregatedStatistics;
public class AggregatedStatisticsAdapter extends BaseAdapter {
private AggregatedStatistics aggregatedStatistics;
private Context context;
private final AggregatedStatistics aggregatedStatistics;
private final Context context;
public AggregatedStatisticsAdapter(Context context, AggregatedStatistics aggregatedStatistics) {
this.context = context;
@@ -83,12 +83,12 @@ public class AggregatedStatisticsAdapter extends BaseAdapter {
protected TextView distanceUnit;
protected TextView time;
protected boolean metricsUnits;
public ViewHolder() {
metricsUnits = PreferencesUtils.isMetricUnits(context);
}
protected boolean reportSpeed;
public void setValues(View view, int iconDrawable, String name, AggregatedStatistics.AggregatedStatistic aggregatedStats) {
reportSpeed = PreferencesUtils.isReportSpeed(context, name);
metricsUnits = PreferencesUtils.isMetricUnits(context);
sportIcon = view.findViewById(R.id.aggregated_stats_sport_icon);
typeLabel = view.findViewById(R.id.aggregated_stats_type_label);
numTracks = view.findViewById(R.id.aggregated_stats_num_tracks);
@@ -128,14 +128,14 @@ public class AggregatedStatisticsAdapter extends BaseAdapter {
maxSpeedLabel = view.findViewById(R.id.aggregated_stats_max_rate_label);
{
Pair<String, String> parts = StringUtils.getSpeedParts(context, aggregatedStats.getTrackStatistics().getAverageMovingSpeed(), metricsUnits, true);
Pair<String, String> parts = StringUtils.getSpeedParts(context, aggregatedStats.getTrackStatistics().getAverageMovingSpeed(), metricsUnits, reportSpeed);
avgSpeed.setText(parts.first);
avgSpeedUnit.setText(parts.second);
avgSpeedLabel.setText(context.getString(R.string.stats_average_moving_speed));
}
{
Pair<String, String> parts = StringUtils.getSpeedParts(context, aggregatedStats.getTrackStatistics().getMaxSpeed(), metricsUnits, true);
Pair<String, String> parts = StringUtils.getSpeedParts(context, aggregatedStats.getTrackStatistics().getMaxSpeed(), metricsUnits, reportSpeed);
maxSpeed.setText(parts.first);
maxSpeedUnit.setText(parts.second);
maxSpeedLabel.setText(context.getString(R.string.stats_max_speed));
@@ -164,14 +164,14 @@ public class AggregatedStatisticsAdapter extends BaseAdapter {
maxPaceLabel = view.findViewById(R.id.aggregated_stats_max_rate_label);
{
Pair<String, String> parts = StringUtils.getSpeedParts(context, aggregatedStats.getTrackStatistics().getAverageMovingSpeed(), metricsUnits, false);
Pair<String, String> parts = StringUtils.getSpeedParts(context, aggregatedStats.getTrackStatistics().getAverageMovingSpeed(), metricsUnits, reportSpeed);
avgPace.setText(parts.first);
avgPaceUnit.setText(parts.second);
avgPaceLabel.setText(R.string.stats_average_moving_pace);
}
{
Pair<String, String> parts = StringUtils.getSpeedParts(context, aggregatedStats.getTrackStatistics().getMaxSpeed(), metricsUnits, false);
Pair<String, String> parts = StringUtils.getSpeedParts(context, aggregatedStats.getTrackStatistics().getMaxSpeed(), metricsUnits, reportSpeed);
maxPace.setText(parts.first);
maxPaceUnit.setText(parts.second);
maxPaceLabel.setText(R.string.stats_fastest_pace);
@@ -108,28 +108,25 @@ public class SearchEngine {
/**
* Comparator for scored results.
*/
private static final Comparator<ScoredResult> SCORED_RESULT_COMPARATOR = new Comparator<ScoredResult>() {
@Override
public int compare(ScoredResult r1, ScoredResult r2) {
// Score ordering.
int scoreDiff = Double.compare(r2.score, r1.score);
if (scoreDiff != 0) {
return scoreDiff;
}
// Make tracks come before waypoints.
if (r1.waypoint != null && r2.track != null) {
return 1;
} else if (r1.track != null && r2.waypoint != null) {
return -1;
}
// Finally, use arbitrary ordering, by ID.
long id1 = r1.track != null ? r1.track.getId() : r1.waypoint.getId();
long id2 = r2.track != null ? r2.track.getId() : r2.waypoint.getId();
long idDiff = id2 - id1;
return Long.signum(idDiff);
private static final Comparator<ScoredResult> SCORED_RESULT_COMPARATOR = (r1, r2) -> {
// Score ordering.
int scoreDiff = Double.compare(r2.score, r1.score);
if (scoreDiff != 0) {
return scoreDiff;
}
// Make tracks come before waypoints.
if (r1.waypoint != null && r2.track != null) {
return 1;
} else if (r1.track != null && r2.waypoint != null) {
return -1;
}
// Finally, use arbitrary ordering, by ID.
long id1 = r1.track != null ? r1.track.getId() : r1.waypoint.getId();
long id2 = r2.track != null ? r2.track.getId() : r2.waypoint.getId();
long idDiff = id2 - id1;
return Long.signum(idDiff);
};
private final ContentProviderUtils providerUtils;
@@ -39,6 +39,7 @@ public final class Waypoint {
private double length = 0.0;
private long duration = 0;
private Location location;
@Deprecated //TODO Make an URI instead of String
private String photoUrl = "";
@VisibleForTesting
@@ -30,6 +30,7 @@ public interface WaypointsColumns extends BaseColumns {
String TABLE_NAME = "waypoints";
Uri CONTENT_URI = Uri.parse(ContentProviderUtils.CONTENT_BASE_URI + "/" + TABLE_NAME);
Uri CONTENT_URI_BY_TRACKID = Uri.parse(ContentProviderUtils.CONTENT_BASE_URI + "/" + TABLE_NAME + "/trackid");
String CONTENT_TYPE = "vnd.android.cursor.dir/vnd.de.dennisguse.waypoint";
String CONTENT_ITEMTYPE = "vnd.android.cursor.item/vnd.de.dennisguse.waypoint";
String DEFAULT_SORT_ORDER = "_id";
@@ -63,6 +63,7 @@ public abstract class CustomContentProvider extends ContentProvider {
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, WaypointsColumns.CONTENT_URI.getPath(), UrlType.WAYPOINTS.ordinal());
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, WaypointsColumns.CONTENT_URI.getPath() + "/#", UrlType.WAYPOINTS_BY_ID.ordinal());
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, WaypointsColumns.CONTENT_URI_BY_TRACKID.getPath() + "/*", UrlType.WAYPOINTS_BY_TRACKID.ordinal());
}
@Override
@@ -140,6 +141,7 @@ public abstract class CustomContentProvider extends ContentProvider {
case WAYPOINTS:
return WaypointsColumns.CONTENT_TYPE;
case WAYPOINTS_BY_ID:
case WAYPOINTS_BY_TRACKID:
return WaypointsColumns.CONTENT_ITEMTYPE;
default:
throw new IllegalArgumentException("Unknown URL " + url);
@@ -219,6 +221,10 @@ public abstract class CustomContentProvider extends ContentProvider {
queryBuilder.setTables(WaypointsColumns.TABLE_NAME);
queryBuilder.appendWhere(WaypointsColumns._ID + "=" + ContentUris.parseId(url));
break;
case WAYPOINTS_BY_TRACKID:
queryBuilder.setTables(WaypointsColumns.TABLE_NAME);
queryBuilder.appendWhere(WaypointsColumns.TRACKID + " IN (" + TextUtils.join(SQL_LIST_DELIMITER, ContentProviderUtils.parseTrackIdsFromUri(url)) + ")");
break;
default:
throw new IllegalArgumentException("Unknown url " + url);
}
@@ -382,6 +388,7 @@ public abstract class CustomContentProvider extends ContentProvider {
TRACKS,
TRACKS_BY_ID,
WAYPOINTS,
WAYPOINTS_BY_ID
WAYPOINTS_BY_ID,
WAYPOINTS_BY_TRACKID
}
}
@@ -6,7 +6,6 @@ import android.content.pm.ProviderInfo;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.net.Uri;
import android.os.Bundle;
import android.os.ParcelFileDescriptor;
import android.provider.OpenableColumns;
import android.util.Log;
@@ -211,14 +210,11 @@ public class ShareContentProvider extends CustomContentProvider implements ICont
final TrackExporter trackExporter = getTrackFileFormat(uri).newTrackExporter(getContext(), tracks);
PipeDataWriter pipeDataWriter = new PipeDataWriter<String>() {
@Override
public void writeDataToPipe(@NonNull ParcelFileDescriptor output, @NonNull Uri uri, @NonNull String mimeType, @Nullable Bundle opts, @Nullable String args) {
try (FileOutputStream fileOutputStream = new FileOutputStream(output.getFileDescriptor())) {
trackExporter.writeTrack(getContext(), fileOutputStream);
} catch (IOException e) {
Log.w(TAG, "there occurred an error while sharing a file: " + e);
}
PipeDataWriter<String> pipeDataWriter = (PipeDataWriter<String>) (output, uri1, mimeType, opts, args) -> {
try (FileOutputStream fileOutputStream = new FileOutputStream(output.getFileDescriptor())) {
trackExporter.writeTrack(getContext(), fileOutputStream);
} catch (IOException e) {
Log.w(TAG, "there occurred an error while sharing a file: " + e);
}
};
@@ -100,13 +100,4 @@ public class TrackPointIterator implements Iterator<TrackPoint>, AutoCloseable {
public void remove() {
throw new UnsupportedOperationException();
}
public int getCount() {
if (cursor == null) {
return -1;
}
return cursor.getCount();
}
}
@@ -6,10 +6,10 @@ import de.dennisguse.opentracks.services.sensors.BluetoothRemoteSensorManager;
public class SensorData {
private String sensorAddress;
private String sensorName;
private final String sensorAddress;
private final String sensorName;
private long timestamp_ms;
private final long timestamp_ms;
SensorData(String sensorAddress, String sensorName) {
this(sensorAddress, sensorName, System.currentTimeMillis());
@@ -25,8 +25,8 @@ public final class SensorDataCycling {
public static class Cadence extends SensorData {
private long crankRevolutionsCount; // UINT32
private int crankRevolutionsTime; // UINT16; 1/1024s
private final long crankRevolutionsCount; // UINT32
private final int crankRevolutionsTime; // UINT16; 1/1024s
private float cadence_rpm = INVALID_VALUE_FLOAT;
public Cadence(String sensorAddress, String sensorName, long crankRevolutionsCount, int crankRevolutionsTime) {
@@ -93,8 +93,8 @@ public final class SensorDataCycling {
public static class Speed extends SensorData {
private int wheelRevolutionsCount; // UINT16
private int wheelRevolutionsTime; // UINT16; 1/1024s
private final int wheelRevolutionsCount; // UINT16
private final int wheelRevolutionsTime; // UINT16; 1/1024s
private float speed_mps = INVALID_VALUE_FLOAT;
public Speed(String sensorAddress, String sensorName, int wheelRevolutionsCount, int wheelRevolutionsTime) {
@@ -154,8 +154,8 @@ public final class SensorDataCycling {
public static class CadenceAndSpeed extends SensorData {
private Cadence cadence;
private Speed speed;
private final Cadence cadence;
private final Speed speed;
public CadenceAndSpeed(String sensorAddress, String sensorName, @NonNull Cadence cadence, @NonNull Speed speed) {
super(sensorAddress, sensorName);
@@ -4,7 +4,7 @@ import androidx.annotation.NonNull;
public class SensorDataHeartRate extends SensorData {
private float heartRate_bpm;
private final float heartRate_bpm;
public SensorDataHeartRate(String name, String address, float heartRate_bpm) {
super(name, address);
@@ -18,7 +18,6 @@ package de.dennisguse.opentracks.fragments;
import android.app.Dialog;
import android.content.Context;
import android.content.DialogInterface;
import android.os.Bundle;
import androidx.annotation.NonNull;
@@ -77,18 +76,13 @@ public class DeleteMarkerDialogFragment extends DialogFragment {
messageId = markerIds.length > 1 ? R.string.marker_delete_multiple_confirm_message : R.string.marker_delete_one_confirm_message;
}
return DialogUtils.createConfirmationDialog(
fragmentActivity, titleId, getString(messageId), new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
new Thread(() -> {
ContentProviderUtils contentProviderUtils = new ContentProviderUtils(fragmentActivity);
for (long markerId : markerIds) {
contentProviderUtils.deleteWaypoint(getContext(), markerId);
}
caller.onDeleteMarkerDone();
}).start();
fragmentActivity, titleId, getString(messageId), (dialog, which) -> new Thread(() -> {
ContentProviderUtils contentProviderUtils = new ContentProviderUtils(fragmentActivity);
for (long markerId : markerIds) {
contentProviderUtils.deleteWaypoint(getContext(), markerId);
}
});
caller.onDeleteMarkerDone();
}).start());
}
/**
@@ -166,9 +166,9 @@ public class ExportProgressDialogFragment extends DialogFragment {
private class ExportThread extends Thread {
private TrackFileFormat trackFileFormat;
private final TrackFileFormat trackFileFormat;
private DocumentFile directory;
private final DocumentFile directory;
ExportThread(TrackFileFormat trackFileFormat, DocumentFile directory) {
this.directory = directory;
@@ -44,7 +44,9 @@ import de.dennisguse.opentracks.util.FileUtils;
public class ImportProgressDialogFragment extends DialogFragment {
private static final String EXTRA_DIRECTORY_URI_KEY = "directory_uri";
private static final String EXTRA_IS_TREE_KEY = "tree_uri";
private static final String EXTRA_DATA_URI_KEY = "data_uri";
private static final String TAG = ImportProgressDialogFragment.class.getSimpleName();
@@ -59,12 +61,10 @@ public class ImportProgressDialogFragment extends DialogFragment {
private int trackImportSuccessCount;
private int fileCount;
/**
* Create a new instance.
*/
public static void showDialog(FragmentManager fragmentManager, Uri documentFileUri) {
public static void showDialog(FragmentManager fragmentManager, Uri documentFileUri, boolean isDirectory) {
Bundle bundle = new Bundle();
bundle.putParcelable(EXTRA_DIRECTORY_URI_KEY, documentFileUri);
bundle.putParcelable(EXTRA_DATA_URI_KEY, documentFileUri);
bundle.putBoolean(EXTRA_IS_TREE_KEY, isDirectory);
ImportProgressDialogFragment dialogFragment = new ImportProgressDialogFragment();
dialogFragment.setArguments(bundle);
@@ -79,9 +79,13 @@ public class ImportProgressDialogFragment extends DialogFragment {
// This fragment uses a background thread, so it should not be recreated during activity re-creation.
setRetainInstance(true);
Uri directoryUri = getArguments().getParcelable(EXTRA_DIRECTORY_URI_KEY);
DocumentFile documentFile = DocumentFile.fromTreeUri(getContext(), directoryUri);
Uri directoryUri = getArguments().getParcelable(EXTRA_DATA_URI_KEY);
DocumentFile documentFile;
if (getArguments().getBoolean(EXTRA_IS_TREE_KEY)) {
documentFile = DocumentFile.fromTreeUri(getContext(), directoryUri);
} else {
documentFile = DocumentFile.fromSingleUri(getContext(), directoryUri);
}
directoryDisplayName = FileUtils.getPath(documentFile);
importThread = new ImportThread(documentFile);
@@ -162,7 +166,7 @@ public class ImportProgressDialogFragment extends DialogFragment {
private class ImportThread extends Thread {
private DocumentFile file;
private final DocumentFile file;
ImportThread(DocumentFile file) {
this.file = file;
@@ -109,7 +109,7 @@ public class MarkerDetailFragment extends Fragment {
long markerId = getArguments().getLong(KEY_MARKER_ID);
if (markerId == -1L) {
Log.d(TAG, "invalid marker id");
getFragmentManager().popBackStack();
getParentFragmentManager().popBackStack();
return;
}
contentProviderUtils = new ContentProviderUtils(getActivity());
@@ -125,16 +125,13 @@ public class MarkerDetailFragment extends Fragment {
textGradient = view.findViewById(R.id.marker_detail_waypoint_text_gradient);
waypointInfo = view.findViewById(R.id.marker_detail_waypoint_info);
photoView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
handler.removeCallbacks(hideText);
int visibility = waypointInfo.getVisibility() == View.GONE ? View.VISIBLE : View.GONE;
textGradient.setVisibility(visibility);
waypointInfo.setVisibility(visibility);
if (visibility == View.VISIBLE) {
handler.postDelayed(hideText, HIDE_TEXT_DELAY);
}
photoView.setOnClickListener(v -> {
handler.removeCallbacks(hideText);
int visibility = waypointInfo.getVisibility() == View.GONE ? View.VISIBLE : View.GONE;
textGradient.setVisibility(visibility);
waypointInfo.setVisibility(visibility);
if (visibility == View.VISIBLE) {
handler.postDelayed(hideText, HIDE_TEXT_DELAY);
}
});
return view;
@@ -222,7 +219,7 @@ public class MarkerDetailFragment extends Fragment {
waypoint = contentProviderUtils.getWaypoint(getArguments().getLong(KEY_MARKER_ID));
if (waypoint == null) {
Log.d(TAG, "waypoint is null");
getFragmentManager().popBackStack();
getParentFragmentManager().popBackStack();
}
}
}
@@ -54,17 +54,14 @@ public class StatisticsRecordedFragment extends Fragment implements TrackDataLis
private TrackStatistics trackStatistics = null;
private String category = "";
private final SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener = new SharedPreferences.OnSharedPreferenceChangeListener() {
@Override
public void onSharedPreferenceChanged(SharedPreferences preferences, String key) {
if (PreferencesUtils.isKey(getContext(), R.string.stats_units_key, key) || PreferencesUtils.isKey(getContext(), R.string.stats_rate_key, key)) {
if (isResumed()) {
getActivity().runOnUiThread(() -> {
if (isResumed()) {
updateUI();
}
});
}
private final SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener = (preferences, key) -> {
if (PreferencesUtils.isKey(getContext(), R.string.stats_units_key, key) || PreferencesUtils.isKey(getContext(), R.string.stats_rate_key, key)) {
if (isResumed()) {
getActivity().runOnUiThread(() -> {
if (isResumed()) {
updateUI();
}
});
}
}
};
@@ -124,23 +121,17 @@ public class StatisticsRecordedFragment extends Fragment implements TrackDataLis
Spinner activityTypeIcon = getView().findViewById(R.id.stats_activity_type_icon);
activityTypeIcon.setAdapter(TrackIconUtils.getIconSpinnerAdapter(getActivity(), ""));
activityTypeIcon.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_UP) {
((TrackRecordedActivity) getActivity()).chooseActivityType(category);
}
return true;
activityTypeIcon.setOnTouchListener((v, event) -> {
if (event.getAction() == MotionEvent.ACTION_UP) {
((TrackRecordedActivity) getActivity()).chooseActivityType(category);
}
return true;
});
activityTypeIcon.setOnKeyListener(new View.OnKeyListener() {
@Override
public boolean onKey(View v, int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) {
((TrackRecordedActivity) getActivity()).chooseActivityType(category);
}
return true;
activityTypeIcon.setOnKeyListener((v, keyCode, event) -> {
if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) {
((TrackRecordedActivity) getActivity()).chooseActivityType(category);
}
return true;
});
}
@@ -203,23 +203,17 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
Spinner activityTypeIcon = getView().findViewById(R.id.stats_activity_type_icon);
activityTypeIcon.setAdapter(TrackIconUtils.getIconSpinnerAdapter(getActivity(), ""));
activityTypeIcon.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_UP) {
((TrackRecordingActivity) getActivity()).chooseActivityType(category);
}
return true;
activityTypeIcon.setOnTouchListener((v, event) -> {
if (event.getAction() == MotionEvent.ACTION_UP) {
((TrackRecordingActivity) getActivity()).chooseActivityType(category);
}
return true;
});
activityTypeIcon.setOnKeyListener(new View.OnKeyListener() {
@Override
public boolean onKey(View v, int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) {
((TrackRecordingActivity) getActivity()).chooseActivityType(category);
}
return true;
activityTypeIcon.setOnKeyListener((v, keyCode, event) -> {
if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) {
((TrackRecordingActivity) getActivity()).chooseActivityType(category);
}
return true;
});
}
@@ -63,9 +63,7 @@ public class ExportActivity extends FragmentActivity implements ExportProgressDi
@Override
public void onExportFinished(final String directoryDisplayName, final int successCount, final int fileCount) {
runOnUiThread(() -> {
ExportFinishedDialogFragment.showDialog(getSupportFragmentManager(), directoryDisplayName, successCount, fileCount);
});
runOnUiThread(() -> ExportFinishedDialogFragment.showDialog(getSupportFragmentManager(), directoryDisplayName, successCount, fileCount));
}
@Override
@@ -33,11 +33,13 @@ import de.dennisguse.opentracks.util.StringUtils;
*
* @author Sandor Dornbush
*/
//TODO Can we export SensorData in GPX?
public class GpxTrackWriter implements TrackWriter {
private static final NumberFormat ELEVATION_FORMAT = NumberFormat.getInstance(Locale.US);
private static final NumberFormat COORDINATE_FORMAT = NumberFormat.getInstance(Locale.US);
private static final NumberFormat SPEED_FORMAT = NumberFormat.getInstance(Locale.US);
private static final NumberFormat HEARTRATE_FORMAT = NumberFormat.getInstance(Locale.US);
private static final NumberFormat CADENCE_FORMAT = NumberFormat.getInstance(Locale.US);
static {
/*
@@ -50,6 +52,15 @@ public class GpxTrackWriter implements TrackWriter {
COORDINATE_FORMAT.setMaximumFractionDigits(6);
COORDINATE_FORMAT.setMaximumIntegerDigits(3);
COORDINATE_FORMAT.setGroupingUsed(false);
SPEED_FORMAT.setMaximumFractionDigits(2);
SPEED_FORMAT.setGroupingUsed(false);
HEARTRATE_FORMAT.setMaximumFractionDigits(0);
HEARTRATE_FORMAT.setGroupingUsed(false);
CADENCE_FORMAT.setMaximumFractionDigits(0);
CADENCE_FORMAT.setGroupingUsed(false);
}
private final String creator;
@@ -82,11 +93,12 @@ public class GpxTrackWriter implements TrackWriter {
printWriter.println("xmlns=\"http://www.topografix.com/GPX/1/1\"");
printWriter.println("xmlns:topografix=\"http://www.topografix.com/GPX/Private/TopoGrafix/0/1\"");
printWriter.println("xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"");
printWriter.println("xsi:schemaLocation=\"http://www.topografix.com/GPX/1/1"
+ " http://www.topografix.com/GPX/1/1/gpx.xsd"
+ " http://www.topografix.com/GPX/Private/TopoGrafix/0/1"
+ " http://www.topografix.com/GPX/Private/TopoGrafix/0/1/topografix.xsd\">");
printWriter.println("xmlns:gpxtpx=\"http://www.garmin.com/xmlschemes/TrackPointExtension/v2\"");
printWriter.println("xsi:schemaLocation=\"http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd"
+ " http://www.topografix.com/GPX/Private/TopoGrafix/0/1 http://www.topografix.com/GPX/Private/TopoGrafix/0/1/topografix.xsd"
+ " http://www.garmin.com/xmlschemas/TrackPointExtension/v2 https://www8.garmin.com/xmlschemas/TrackPointExtensionv2.xsd\">");
printWriter.println("<metadata>");
Track track = tracks[0];
printWriter.println("<name>" + StringUtils.formatCData(track.getName()) + "</name>");
printWriter.println("<desc>" + StringUtils.formatCData(track.getDescription()) + "</desc>");
@@ -174,9 +186,28 @@ public class GpxTrackWriter implements TrackWriter {
if (trackPoint.hasAltitude()) {
printWriter.println("<ele>" + ELEVATION_FORMAT.format(trackPoint.getAltitude()) + "</ele>");
}
printWriter.println(
"<time>" + StringUtils.formatDateTimeIso8601(trackPoint.getTime()) + "</time>");
printWriter.println("<speed>" + trackPoint.getSpeed() + "</speed>");
printWriter.println("<time>" + StringUtils.formatDateTimeIso8601(trackPoint.getTime()) + "</time>");
if (trackPoint.hasSpeed() || trackPoint.hasHeartRate() || trackPoint.hasCyclingCadence()) {
printWriter.println("<extensions><gpxtpx:TrackPointExtension>");
if (trackPoint.hasSpeed()) {
printWriter.println("<gpxtpx:speed>" + SPEED_FORMAT.format(trackPoint.getSpeed()) + "</gpxtpx:speed>");
}
if (trackPoint.hasHeartRate()) {
printWriter.println("<gpxtpx:hr>" + HEARTRATE_FORMAT.format(trackPoint.getHeartRate_bpm()) + "</gpxtpx:hr>");
}
if (trackPoint.hasCyclingCadence()) {
printWriter.println("<gpxtpx:cad>" + HEARTRATE_FORMAT.format(trackPoint.getCyclingCadence_rpm()) + "</gpxtpx:cad>");
}
printWriter.println("</gpxtpx:TrackPointExtension></extensions>");
}
printWriter.println("</trkpt>");
}
}
@@ -83,6 +83,9 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
protected String altitude;
protected String time;
protected String speed;
protected String heartrate;
protected String cadence;
protected String power;
protected String waypointType;
protected String photoUrl;
protected String uuid;
@@ -248,14 +251,12 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
trackData.track.setName(name);
}
UUID uuidParsed;
try {
uuidParsed = UUID.fromString(uuid);
trackData.track.setUuid(UUID.fromString(uuid));
} catch (IllegalArgumentException | NullPointerException e) {
Log.w(TAG, "could not parse Track UUID, generating a new one.");
uuidParsed = UUID.randomUUID();
trackData.track.setUuid(UUID.randomUUID());
}
trackData.track.setUuid(uuidParsed);
if (description != null) {
trackData.track.setDescription(description);
@@ -416,45 +417,56 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
if (latitude == null || longitude == null) {
return null;
}
double latitudeValue;
double longitudeValue;
TrackPoint trackPoint = new TrackPoint();
try {
latitudeValue = Double.parseDouble(latitude);
longitudeValue = Double.parseDouble(longitude);
trackPoint.setLatitude(Double.parseDouble(latitude));
trackPoint.setLongitude(Double.parseDouble(longitude));
} catch (NumberFormatException e) {
throw new SAXException(createErrorMessage(String.format(Locale.US, "Unable to parse latitude longitude: %s %s", latitude, longitude)), e);
}
Double altitudeValue = null;
if (altitude != null) {
try {
altitudeValue = Double.parseDouble(altitude);
} catch (NumberFormatException e) {
throw new SAXException(createErrorMessage(String.format(Locale.US, "Unable to parse altitude: %s", altitude)), e);
}
}
long timeValue;
if (time == null) {
timeValue = trackData.importTime;
trackPoint.setTime(trackData.importTime);
} else {
try {
timeValue = StringUtils.parseTime(time);
trackPoint.setTime(StringUtils.parseTime(time));
} catch (Exception e) {
throw new SAXException(createErrorMessage(String.format(Locale.US, "Unable to parse time: %s", time)), e);
}
}
TrackPoint trackPoint = new TrackPoint(latitudeValue, longitudeValue, altitudeValue, timeValue);
if (altitude != null) {
try {
trackPoint.setAltitude(Double.parseDouble(altitude));
} catch (NumberFormatException e) {
throw new SAXException(createErrorMessage(String.format(Locale.US, "Unable to parse altitude: %s", altitude)), e);
}
}
float speedValue;
if (speed != null) {
try {
speedValue = Float.valueOf(speed);
trackPoint.setSpeed(speedValue);
trackPoint.setSpeed(Float.parseFloat(speed));
} catch (Exception e) {
throw new SAXException(createErrorMessage(String.format(Locale.US, "Unable to parse speed: %s", speed)), e);
}
}
if (heartrate != null) {
try {
trackPoint.setHeartRate_bpm(Float.parseFloat(heartrate));
} catch (Exception e) {
throw new SAXException(createErrorMessage(String.format(Locale.US, "Unable to parse heart rate: %s", heartrate)), e);
}
}
if (cadence != null) {
try {
trackPoint.setCyclingCadence_rpm(Float.parseFloat(cadence));
} catch (Exception e) {
throw new SAXException(createErrorMessage(String.format(Locale.US, "Unable to parse cadence: %s", cadence)), e);
}
}
return trackPoint;
}
@@ -33,7 +33,6 @@ public abstract class DirectoryChooserActivity extends AppCompatActivity {
if (resultCode == Activity.RESULT_OK) {
Uri directoryUri = resultData.getData();
startActivity(createIntent(directoryUri));
}
finish();
@@ -28,6 +28,7 @@ import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
/**
* Imports a GPX file.
* Uses https://www8.garmin.com/xmlschemas/TrackPointExtensionv2.xsd
*
* @author Jimmy Shih
*/
@@ -39,7 +40,6 @@ public class GpxFileTrackImporter extends AbstractFileTrackImporter {
private static final String TAG_GPX = "gpx";
private static final String TAG_NAME = "name";
private static final String TAG_TIME = "time";
private static final String TAG_SPEED = "speed";
private static final String TAG_TRACK = "trk";
private static final String TAG_TRACK_POINT = "trkpt";
private static final String TAG_TRACK_SEGMENT = "trkseg";
@@ -49,6 +49,10 @@ public class GpxFileTrackImporter extends AbstractFileTrackImporter {
private static final String ATTRIBUTE_LAT = "lat";
private static final String ATTRIBUTE_LON = "lon";
private static final String TAG_EXTENSION_SPEED = "gpxtpx:speed";
private static final String TAG_EXTENSION_HEARTRATE = "gpxtpx:hr";
private static final String TAG_EXTENSION_CADENCE = "gpxtpx:cad";
/**
* Constructor.
*
@@ -116,11 +120,6 @@ public class GpxFileTrackImporter extends AbstractFileTrackImporter {
time = content.trim();
}
break;
case TAG_SPEED:
if (content != null) {
speed = content.trim();
}
break;
case TAG_ELEVATION:
if (content != null) {
altitude = content.trim();
@@ -131,6 +130,20 @@ public class GpxFileTrackImporter extends AbstractFileTrackImporter {
waypointType = content.trim();
}
break;
case TAG_EXTENSION_SPEED:
if (content != null) {
speed = content.trim();
}
case TAG_EXTENSION_HEARTRATE:
if (content != null) {
heartrate = content.trim();
}
break;
case TAG_EXTENSION_CADENCE:
if (content != null) {
cadence = content.trim();
}
break;
}
// Reset element content
@@ -16,7 +16,6 @@
package de.dennisguse.opentracks.io.file.importer;
import android.net.Uri;
import android.os.Bundle;
import androidx.annotation.NonNull;
@@ -43,8 +42,12 @@ public class ImportActivity extends FragmentActivity implements ImportProgressDi
super.onCreate(savedInstanceState);
if (savedInstanceState == null || !savedInstanceState.getBoolean(BUNDLE_ACTIVITY_RECREATED, false)) {
Uri directoryUri = getIntent().getParcelableExtra(EXTRA_DIRECTORY_URI_KEY);
ImportProgressDialogFragment.showDialog(getSupportFragmentManager(), directoryUri);
if (getIntent().getData() != null) {
ImportProgressDialogFragment.showDialog(getSupportFragmentManager(), getIntent().getData(), false);
} else {
// Started from DirectoryChooserActivity
ImportProgressDialogFragment.showDialog(getSupportFragmentManager(), getIntent().getParcelableExtra(EXTRA_DIRECTORY_URI_KEY), true);
}
}
}
@@ -61,9 +64,7 @@ public class ImportActivity extends FragmentActivity implements ImportProgressDi
@Override
public void onImportFinished(final String directoryDisplayName, final int successCount, final int fileCount) {
runOnUiThread(() -> {
ImportFinishedDialogFragment.showDialog(getSupportFragmentManager(), directoryDisplayName, successCount, fileCount);
});
runOnUiThread(() -> ImportFinishedDialogFragment.showDialog(getSupportFragmentManager(), directoryDisplayName, successCount, fileCount));
}
@Override
@@ -53,7 +53,7 @@ public class KmzTrackImporter implements TrackImporter {
private final Context context;
private final long importTrackId;
private Uri uriKmzFile;
private final Uri uriKmzFile;
/**
* @param context the context
@@ -24,11 +24,7 @@ import android.content.Intent;
import android.content.SharedPreferences;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
import android.database.sqlite.SQLiteException;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.net.Uri;
import android.os.Bundle;
import android.os.IBinder;
import android.os.PowerManager.WakeLock;
import android.util.Log;
@@ -37,9 +33,6 @@ import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import androidx.core.app.TaskStackBuilder;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.TrackListActivity;
import de.dennisguse.opentracks.TrackRecordingActivity;
@@ -50,6 +43,7 @@ import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
import de.dennisguse.opentracks.content.provider.CustomContentProvider;
import de.dennisguse.opentracks.content.provider.TrackPointIterator;
import de.dennisguse.opentracks.content.sensor.SensorDataSet;
import de.dennisguse.opentracks.services.handlers.HandlerServer;
import de.dennisguse.opentracks.services.sensors.BluetoothRemoteSensorManager;
import de.dennisguse.opentracks.services.tasks.AnnouncementPeriodicTaskFactory;
import de.dennisguse.opentracks.services.tasks.PeriodicTaskExecutor;
@@ -62,7 +56,6 @@ import de.dennisguse.opentracks.util.SystemUtils;
import de.dennisguse.opentracks.util.TrackIconUtils;
import de.dennisguse.opentracks.util.TrackNameUtils;
import de.dennisguse.opentracks.util.TrackPointUtils;
import de.dennisguse.opentracks.util.UnitConversions;
/**
* A background service that registers a location listener and records track points.
@@ -70,25 +63,19 @@ import de.dennisguse.opentracks.util.UnitConversions;
*
* @author Leif Hendrik Wilden
*/
public class TrackRecordingService extends Service {
public class TrackRecordingService extends Service implements HandlerServer.HandlerServerInterface {
private static final String TAG = TrackRecordingService.class.getSimpleName();
// The following variables are set in onCreate:
@Deprecated //TODO Should not be necessary
private ExecutorService locationExecutorService; // Enforces order of location changes.
private ContentProviderUtils contentProviderUtils;
private LocationManager locationManager;
private PeriodicTaskExecutor voiceExecutor;
private TrackRecordingServiceNotificationManager notificationManager;
private LocationListenerPolicy locationListenerPolicy;
private long recordingTrackId;
private boolean recordingTrackPaused;
private int recordingDistanceInterval;
private int maxRecordingDistance;
private int recordingGpsAccuracy;
private long currentRecordingInterval;
private final OnSharedPreferenceChangeListener sharedPreferenceChangeListener = new OnSharedPreferenceChangeListener() {
@Override
@@ -110,27 +97,14 @@ public class TrackRecordingService extends Service {
if (PreferencesUtils.isKey(context, R.string.voice_frequency_key, key)) {
voiceExecutor.setTaskFrequency(PreferencesUtils.getVoiceFrequency(context));
}
if (PreferencesUtils.isKey(context, R.string.min_recording_interval_key, key)) {
int minRecordingInterval = PreferencesUtils.getMinRecordingInterval(context);
if (minRecordingInterval == PreferencesUtils.getMinRecordingIntervalAdaptBatteryLife(context)) {
// Choose battery life over moving time accuracy.
locationListenerPolicy = new AdaptiveLocationListenerPolicy(30 * UnitConversions.ONE_SECOND_MS, 5 * UnitConversions.ONE_MINUTE_MS, 5);
} else if (minRecordingInterval == PreferencesUtils.getMinRecordingIntervalAdaptAccuracy(context)) {
// Get all the updates.
locationListenerPolicy = new AdaptiveLocationListenerPolicy(UnitConversions.ONE_SECOND_MS, 30 * UnitConversions.ONE_SECOND_MS, 0);
} else {
locationListenerPolicy = new AbsoluteLocationListenerPolicy(minRecordingInterval * UnitConversions.ONE_SECOND_MS);
}
}
if (PreferencesUtils.isKey(context, R.string.recording_distance_interval_key, key)) {
recordingDistanceInterval = PreferencesUtils.getRecordingDistanceInterval(context);
}
if (PreferencesUtils.isKey(context, R.string.max_recording_distance_key, key)) {
maxRecordingDistance = PreferencesUtils.getMaxRecordingDistance(context);
}
if (PreferencesUtils.isKey(context, R.string.recording_gps_accuracy_key, key)) {
recordingGpsAccuracy = PreferencesUtils.getRecordingGPSAccuracy(context);
}
handlerServer.onSharedPreferenceChanged(context, preferences, key);
}
};
@@ -143,38 +117,16 @@ public class TrackRecordingService extends Service {
private boolean isIdle;
private TrackRecordingServiceBinder binder = new TrackRecordingServiceBinder(this);
private final LocationListener locationListener = new LocationListener() {
@Override
public void onLocationChanged(final Location location) {
if (locationExecutorService == null || locationExecutorService.isShutdown() || locationExecutorService.isTerminated()) {
return;
}
locationExecutorService.submit(() -> onLocationChangedAsync(location));
}
@Override
public void onStatusChanged(String provider, int status, Bundle extras) {
Log.w(TAG, "LocationListener.onStatusChanged(): is not implemented.");
}
@Override
public void onProviderEnabled(String provider) {
Log.w(TAG, "LocationListener.onProviderEnabled(): is not implemented.");
}
@Override
public void onProviderDisabled(String provider) {
Log.w(TAG, "LocationListener.onProviderDisabled(): is not implemented.");
}
};
private HandlerServer handlerServer;
@Override
public void onCreate() {
super.onCreate();
locationExecutorService = Executors.newSingleThreadExecutor();
handlerServer = new HandlerServer(this);
contentProviderUtils = new ContentProviderUtils(this);
locationManager = (LocationManager) getSystemService(Context.LOCATION_SERVICE);
voiceExecutor = new PeriodicTaskExecutor(this, new AnnouncementPeriodicTaskFactory());
notificationManager = new TrackRecordingServiceNotificationManager(this);
@@ -200,6 +152,8 @@ public class TrackRecordingService extends Service {
@Override
public void onDestroy() {
handlerServer.stop(this);
if (remoteSensorManager != null) {
remoteSensorManager.stop();
remoteSensorManager = null;
@@ -208,9 +162,6 @@ public class TrackRecordingService extends Service {
// Reverse order from onCreate
showNotification(false); //TODO Why?
unregisterLocationListener();
locationManager = null;
PreferencesUtils.unregister(this, sharedPreferenceChangeListener);
try {
@@ -227,8 +178,6 @@ public class TrackRecordingService extends Service {
// This should be the next to last operation
wakeLock = SystemUtils.releaseWakeLock(wakeLock);
// Shutdown the executorService last to avoid sending events to a dead executor.
locationExecutorService.shutdown();
super.onDestroy();
}
@@ -428,7 +377,7 @@ public class TrackRecordingService extends Service {
private void startGps() {
wakeLock = SystemUtils.acquireWakeLock(this, wakeLock);
registerLocationListener();
handlerServer.start(this);
showNotification(true);
}
@@ -498,6 +447,8 @@ public class TrackRecordingService extends Service {
}
lastTrackPoint = null;
handlerServer.stop(this);
stopGps(trackStopped);
}
@@ -509,7 +460,7 @@ public class TrackRecordingService extends Service {
void stopGps(boolean shutdown) {
if (!isRecording()) return;
unregisterLocationListener();
handlerServer.stop(this);
showNotification(false);
wakeLock = SystemUtils.releaseWakeLock(wakeLock);
if (shutdown) {
@@ -548,46 +499,27 @@ public class TrackRecordingService extends Service {
PreferencesUtils.setBoolean(this, R.string.recording_track_paused_key, recordingTrackPaused);
}
void onLocationChangedAsync(Location location) {
@Override
public void newTrackPoint(TrackPoint trackPoint, int recordingGpsAccuracy) {
if (!isRecording() || isPaused()) {
Log.w(TAG, "Ignore onLocationChangedAsync. Not recording or paused.");
Log.w(TAG, "Ignore newTrackPoint. Not recording or paused.");
return;
}
Track track = contentProviderUtils.getTrack(recordingTrackId);
if (track == null) {
Log.w(TAG, "Ignore onLocationChangedAsync. No track.");
Log.w(TAG, "Ignore newTrackPoint. No track.");
return;
}
if (!LocationUtils.isValidLocation(location)) {
Log.w(TAG, "Ignore onLocationChangedAsync. location is invalid.");
return;
}
TrackPoint trackPoint = new TrackPoint(location);
fillWithSensorDataSet(trackPoint);
notificationManager.updateTrackPoint(this, trackPoint, recordingGpsAccuracy);
if (!TrackPointUtils.fulfillsAccuracy(trackPoint, recordingGpsAccuracy)) {
Log.d(TAG, "Ignore onLocationChangedAsync. Poor accuracy.");
return;
}
TrackPointUtils.fixTime(trackPoint);
//TODO Figure out how to avoid loading the lastValidTrackPoint from the database
TrackPoint lastValidTrackPoint = getLastValidTrackPointInCurrentSegment(track.getId());
long idleTime = 0L;
if (TrackPointUtils.after(trackPoint, lastValidTrackPoint)) {
idleTime = trackPoint.getTime() - lastValidTrackPoint.getTime();
}
locationListenerPolicy.updateIdleTime(idleTime);
if (currentRecordingInterval != locationListenerPolicy.getDesiredPollingInterval()) {
registerLocationListener();
}
//Storing trackPoint
@@ -598,7 +530,7 @@ public class TrackRecordingService extends Service {
return;
}
if (!LocationUtils.isValidLocation(lastValidTrackPoint.getLocation())) {
if (lastValidTrackPoint == null || !LocationUtils.isValidLocation(lastValidTrackPoint.getLocation())) {
// For some reason the previous first trackPoint was not stored, but currentSegmentHasLocation set true.
// Should not happen. The current segment should have a location. Just insert the current location.
insertTrackPoint(track, trackPoint);
@@ -714,28 +646,6 @@ public class TrackRecordingService extends Service {
}
}
private void registerLocationListener() {
if (locationManager == null) {
Log.e(TAG, "locationManager is null.");
return;
}
try {
long interval = locationListenerPolicy.getDesiredPollingInterval();
locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, interval, locationListenerPolicy.getMinDistance_m(), locationListener);
currentRecordingInterval = interval;
} catch (SecurityException e) {
Log.e(TAG, "Could not register location listener; permissions not granted.", e);
}
}
private void unregisterLocationListener() {
if (locationManager == null) {
Log.e(TAG, "locationManager is null.");
return;
}
locationManager.removeUpdates(locationListener);
}
private void showNotification(boolean isGpsStarted) {
if (isRecording()) {
Intent intent = IntentUtils.newIntent(this, TrackRecordingActivity.class)
@@ -765,19 +675,6 @@ public class TrackRecordingService extends Service {
}
}
/**
* Disables processing of location updates from {@link android.location.LocationManager}.
*/
@VisibleForTesting
public void enableLocationExecutor(boolean enable) {
if (enable) {
locationExecutorService = Executors.newSingleThreadExecutor();
} else {
locationExecutorService.shutdownNow();
locationExecutorService = null;
}
}
@VisibleForTesting
public void setRemoteSensorManager(BluetoothRemoteSensorManager remoteSensorManager) {
this.remoteSensorManager = remoteSensorManager;
@@ -1,9 +1,8 @@
package de.dennisguse.opentracks.services;
import android.location.Location;
import androidx.annotation.VisibleForTesting;
import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.content.sensor.SensorDataSet;
import de.dennisguse.opentracks.services.sensors.BluetoothRemoteSensorManager;
@@ -82,23 +81,11 @@ class TrackRecordingServiceBinder extends android.os.Binder implements TrackReco
return trackRecordingService.insertWaypoint(name, category, description, photoUrl);
}
@VisibleForTesting
@Override
public void insertLocation(Location location) {
trackRecordingService.onLocationChangedAsync(location);
}
@Override
public SensorDataSet getSensorData() {
return trackRecordingService.getSensorDataSet();
}
@VisibleForTesting
@Override
public void enableLocationExecutor(boolean enable) {
trackRecordingService.enableLocationExecutor(enable);
}
@VisibleForTesting
@Override
public void setRemoteSensorManager(BluetoothRemoteSensorManager remoteSensorManager) {
@@ -112,4 +99,10 @@ class TrackRecordingServiceBinder extends android.os.Binder implements TrackReco
void detachFromService() {
trackRecordingService = null;
}
@VisibleForTesting
@Override
public void newTrackPoint(TrackPoint trackPoint, int recordingGpsAccuracy) {
trackRecordingService.newTrackPoint(trackPoint, recordingGpsAccuracy);
}
}
@@ -33,7 +33,6 @@ import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.TrackEditActivity;
import de.dennisguse.opentracks.util.IntentUtils;
import de.dennisguse.opentracks.util.PreferencesUtils;
import de.dennisguse.opentracks.util.ServiceUtils;
/**
* Wrapper for the track recording service.
@@ -66,14 +65,43 @@ public class TrackRecordingServiceConnection implements ServiceConnection, Death
* Starts and binds the service.
*/
public void startAndBind(Context context) {
bindService(context, true);
if (trackRecordingService != null) {
// Service is already started and bound.
return;
}
Log.i(TAG, "Starting the service.");
context.startService(new Intent(context, TrackRecordingService.class));
startConnection(context);
}
/**
* Binds the service if it is started.
* Resumes the track recording service connection.
*
* @param context the context
*/
private void bindIfStarted(Context context) {
bindService(context, false);
public void startConnection(@NonNull Context context) {
if (trackRecordingService != null) {
// Service is already started and bound.
return;
}
Log.i(TAG, "Binding the service.");
int flags = BuildConfig.DEBUG ? Context.BIND_DEBUG_UNBIND : 0;
context.bindService(new Intent(context, TrackRecordingService.class), this, flags);
}
/**
* Unbinds the service (but leave it running).
*/
public void unbind(Context context) {
try {
context.unbindService(this);
} catch (IllegalArgumentException e) {
// Means not bound to the service. OK to ignore.
}
setTrackRecordingService(null);
}
/**
@@ -91,7 +119,6 @@ public class TrackRecordingServiceConnection implements ServiceConnection, Death
return trackRecordingService;
}
/**
* Sets the trackRecordingService.
*
@@ -104,18 +131,6 @@ public class TrackRecordingServiceConnection implements ServiceConnection, Death
}
}
/**
* Unbinds the service (but leave it running).
*/
public void unbind(Context context) {
try {
context.unbindService(this);
} catch (IllegalArgumentException e) {
// Means not bound to the service. OK to ignore.
}
setTrackRecordingService(null);
}
@Override
public void onServiceConnected(ComponentName className, IBinder service) {
Log.i(TAG, "Connected to the service.");
@@ -139,44 +154,6 @@ public class TrackRecordingServiceConnection implements ServiceConnection, Death
setTrackRecordingService(null);
}
/**
* Binds the service if it is started.
*
* @param startIfNeeded start the service if needed
*/
private void bindService(Context context, boolean startIfNeeded) {
if (trackRecordingService != null) {
// Service is already started and bound.
return;
}
if (!startIfNeeded && !ServiceUtils.isTrackRecordingServiceRunning(context)) {
Log.d(TAG, "Service is not started. Not binding it.");
return;
}
if (startIfNeeded) {
Log.i(TAG, "Starting the service.");
context.startService(new Intent(context, TrackRecordingService.class));
}
Log.i(TAG, "Binding the service.");
int flags = BuildConfig.DEBUG ? Context.BIND_DEBUG_UNBIND : 0;
context.bindService(new Intent(context, TrackRecordingService.class), this, flags);
}
/**
* Resumes the track recording service connection.
*
* @param context the context
*/
public void startConnection(@NonNull Context context) {
bindIfStarted(context);
if (!ServiceUtils.isTrackRecordingServiceRunning(context)) {
resetRecordingState(context);
}
}
/**
* Resumes the recording track.
*/
@@ -197,16 +174,6 @@ public class TrackRecordingServiceConnection implements ServiceConnection, Death
}
}
private static void resetRecordingState(Context context) {
if (PreferencesUtils.isRecording(context)) {
PreferencesUtils.setLong(context, R.string.recording_track_id_key, PreferencesUtils.RECORDING_TRACK_ID_DEFAULT);
}
boolean recordingTrackPaused = PreferencesUtils.isRecordingTrackPaused(context);
if (!recordingTrackPaused) {
PreferencesUtils.defaultRecordingTrackPaused(context);
}
}
/**
* Adds a marker.
*
@@ -241,7 +208,7 @@ public class TrackRecordingServiceConnection implements ServiceConnection, Death
public void stopRecording(@NonNull Context context, boolean showEditor) {
TrackRecordingServiceInterface trackRecordingService = getServiceIfBound();
if (trackRecordingService == null) {
resetRecordingState(context);
Log.e(TAG, "TrackRecordingService not connected.");
} else {
try {
if (showEditor) {
@@ -15,10 +15,9 @@
*/
package de.dennisguse.opentracks.services;
import android.location.Location;
import androidx.annotation.VisibleForTesting;
import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.content.sensor.SensorDataSet;
import de.dennisguse.opentracks.services.sensors.BluetoothRemoteSensorManager;
@@ -99,23 +98,16 @@ public interface TrackRecordingServiceInterface {
*/
SensorDataSet getSensorData();
/**
* Inserts a location in the current recording track.
* <p>
* When recording a track, GPS locations are automatically inserted.
* This is used for inserting special track points or for testing.
*
* @param location the location to be inserted
*/
@VisibleForTesting
void insertLocation(Location location);
/**
* Disables processing of location updates from {@link android.location.LocationManager}.
*/
@VisibleForTesting
void enableLocationExecutor(boolean enable);
@VisibleForTesting
void setRemoteSensorManager(BluetoothRemoteSensorManager remoteSensorManager);
/**
* Inserts a track point in the current recording track.
* This is used for inserting special track points or for testing.
*
* @param trackPoint the track point object to be inserted.
* @param recordingGpsAccuracy recording GPS accuracy.
*/
@VisibleForTesting
void newTrackPoint(TrackPoint trackPoint, int recordingGpsAccuracy);
}
@@ -14,7 +14,7 @@
* the License.
*/
package de.dennisguse.opentracks.services;
package de.dennisguse.opentracks.services.handlers;
/**
* This is a simple location listener policy that will always dictate the same polling interval.
@@ -14,7 +14,7 @@
* the License.
*/
package de.dennisguse.opentracks.services;
package de.dennisguse.opentracks.services.handlers;
/**
* A {@link LocationListenerPolicy} that will change based on how long the user has been stationary.
@@ -0,0 +1,70 @@
package de.dennisguse.opentracks.services.handlers;
import android.content.Context;
import android.content.SharedPreferences;
import androidx.annotation.VisibleForTesting;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import de.dennisguse.opentracks.content.data.TrackPoint;
public class HandlerServer {
private static String TAG = HandlerServer.class.getSimpleName();
private final LocationHandler locationHandler;
private final HandlerServerInterface service;
private ExecutorService serviceExecutor;
public HandlerServer(HandlerServerInterface service) {
this.locationHandler = new LocationHandler(this);
this.service = service;
}
@VisibleForTesting
HandlerServer(LocationHandler locationHandler, HandlerServerInterface service) {
this.locationHandler = locationHandler;
this.service = service;
}
public void start(Context context) {
serviceExecutor = Executors.newSingleThreadExecutor();
locationHandler.onStart(context);
locationHandler.onSharedPreferenceChanged(context, null, null);
}
public void stop(Context context) {
locationHandler.onStop(context);
if (serviceExecutor != null) {
serviceExecutor.shutdownNow();
}
serviceExecutor = null;
}
public void onSharedPreferenceChanged(Context context, SharedPreferences preferences, String key) {
locationHandler.onSharedPreferenceChanged(context, preferences, key);
}
public void sendTrackPoint(TrackPoint trackPoint, int recordingGpsAccuracy) {
if (serviceExecutor == null || serviceExecutor.isTerminated() || serviceExecutor.isShutdown()) {
return;
}
serviceExecutor.execute(() -> service.newTrackPoint(trackPoint, recordingGpsAccuracy));
}
public interface HandlerServerInterface {
void newTrackPoint(TrackPoint trackPoint, int gpsAccuracy);
}
public interface Handler {
void onStart(Context context);
void onStop(Context context);
void onSharedPreferenceChanged(Context context, SharedPreferences preferences, String key);
}
}
@@ -0,0 +1,137 @@
package de.dennisguse.opentracks.services.handlers;
import android.content.Context;
import android.content.SharedPreferences;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.os.Bundle;
import android.util.Log;
import androidx.annotation.NonNull;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.util.LocationUtils;
import de.dennisguse.opentracks.util.PreferencesUtils;
import de.dennisguse.opentracks.util.TrackPointUtils;
import de.dennisguse.opentracks.util.UnitConversions;
class LocationHandler implements HandlerServer.Handler, LocationListener {
private final String TAG = LocationHandler.class.getSimpleName();
private LocationManager locationManager;
private final HandlerServer handlerServer;
private LocationListenerPolicy locationListenerPolicy;
private long currentRecordingInterval;
private int recordingGpsAccuracy;
private TrackPoint lastValidTrackPoint;
public LocationHandler(HandlerServer handlerServer) {
this.handlerServer = handlerServer;
}
@Override
public void onStart(Context context) {
locationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE);
registerLocationListener();
}
@Override
public void onStop(Context context) {
locationManager = null;
unregisterLocationListener();
}
@Override
public void onSharedPreferenceChanged(Context context, SharedPreferences preferences, String key) {
if (PreferencesUtils.isKey(context, R.string.min_recording_interval_key, key)) {
int minRecordingInterval = PreferencesUtils.getMinRecordingInterval(context);
if (minRecordingInterval == PreferencesUtils.getMinRecordingIntervalAdaptBatteryLife(context)) {
// Choose battery life over moving time accuracy.
locationListenerPolicy = new AdaptiveLocationListenerPolicy(30 * UnitConversions.ONE_SECOND_MS, 5 * UnitConversions.ONE_MINUTE_MS, 5);
} else if (minRecordingInterval == PreferencesUtils.getMinRecordingIntervalAdaptAccuracy(context)) {
// Get all the updates.
locationListenerPolicy = new AdaptiveLocationListenerPolicy(UnitConversions.ONE_SECOND_MS, 30 * UnitConversions.ONE_SECOND_MS, 0);
} else {
locationListenerPolicy = new AbsoluteLocationListenerPolicy(minRecordingInterval * UnitConversions.ONE_SECOND_MS);
}
if (locationManager != null) {
registerLocationListener();
}
}
if (PreferencesUtils.isKey(context, R.string.recording_gps_accuracy_key, key)) {
recordingGpsAccuracy = PreferencesUtils.getRecordingGPSAccuracy(context);
}
}
/**
* Checks if location is valid and builds a track point that will be send through HandlerServer.
*
* @param location {@link Location} object.
*/
@Override
public void onLocationChanged(@NonNull Location location) {
if (!LocationUtils.isValidLocation(location)) {
Log.w(TAG, "Ignore newTrackPoint. location is invalid.");
return;
}
TrackPoint trackPoint = new TrackPoint(location);
if (!TrackPointUtils.fulfillsAccuracy(trackPoint, recordingGpsAccuracy)) {
Log.d(TAG, "Ignore newTrackPoint. Poor accuracy.");
return;
}
long idleTime = 0L;
if (TrackPointUtils.after(trackPoint, lastValidTrackPoint)) {
idleTime = trackPoint.getTime() - lastValidTrackPoint.getTime();
}
locationListenerPolicy.updateIdleTime(idleTime);
if (currentRecordingInterval != locationListenerPolicy.getDesiredPollingInterval()) {
registerLocationListener();
}
lastValidTrackPoint = trackPoint;
handlerServer.sendTrackPoint(trackPoint, recordingGpsAccuracy);
}
@Override
public void onStatusChanged(String provider, int status, Bundle extras) {
}
@Override
public void onProviderEnabled(@NonNull String provider) {
}
@Override
public void onProviderDisabled(@NonNull String provider) {
}
private void registerLocationListener() {
if (locationManager == null) {
Log.e(TAG, "locationManager is null.");
return;
}
try {
long interval = locationListenerPolicy.getDesiredPollingInterval();
currentRecordingInterval = interval;
locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, interval, locationListenerPolicy.getMinDistance_m(), this);
} catch (SecurityException e) {
Log.e(TAG, "Could not register location listener; permissions not granted.", e);
}
}
private void unregisterLocationListener() {
if (locationManager == null) {
Log.e(TAG, "locationManager is null.");
return;
}
locationManager.removeUpdates(this);
locationManager = null;
}
}
@@ -14,7 +14,7 @@
* the License.
*/
package de.dennisguse.opentracks.services;
package de.dennisguse.opentracks.services.handlers;
/**
* This is an interface for classes that will manage the location listener policy.
@@ -19,7 +19,6 @@ package de.dennisguse.opentracks.services.tasks;
import android.content.Context;
import android.media.AudioManager;
import android.speech.tts.TextToSpeech;
import android.speech.tts.TextToSpeech.OnInitListener;
import android.speech.tts.UtteranceProgressListener;
import android.util.Log;
@@ -50,7 +49,7 @@ public class AnnouncementPeriodicTask implements PeriodicTask {
private final AudioManager audioManager;
private ContentProviderUtils contentProviderUtils;
private final ContentProviderUtils contentProviderUtils;
private final AudioManager.OnAudioFocusChangeListener audioFocusChangeListener = new AudioManager.OnAudioFocusChangeListener() {
@Override
@@ -116,12 +115,9 @@ public class AnnouncementPeriodicTask implements PeriodicTask {
Log.d(TAG, "Start");
if (tts == null) {
tts = new TextToSpeech(context, new OnInitListener() {
@Override
public void onInit(int status) {
Log.i(TAG, "TextToSpeech initialized with status " + status);
ttsInitStatus = status;
}
tts = new TextToSpeech(context, status -> {
Log.i(TAG, "TextToSpeech initialized with status " + status);
ttsInitStatus = status;
});
}
}
@@ -22,7 +22,6 @@ import android.util.AttributeSet;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.AutoCompleteTextView;
import android.widget.Spinner;
@@ -51,12 +50,7 @@ public class ActivityTypePreference extends DialogPreference {
setDialogIcon(null);
setPersistent(true);
SummaryProvider<DialogPreference> summaryProvider = new SummaryProvider<DialogPreference>() {
@Override
public CharSequence provideSummary(DialogPreference preference) {
return PreferencesUtils.getDefaultActivity(ActivityTypePreference.this.getContext());
}
};
SummaryProvider<DialogPreference> summaryProvider = preference -> PreferencesUtils.getDefaultActivity(ActivityTypePreference.this.getContext());
setSummaryProvider(summaryProvider);
}
@@ -90,43 +84,31 @@ public class ActivityTypePreference extends DialogPreference {
textView.setText(category);
ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(context, R.array.activity_types, android.R.layout.simple_dropdown_item_1line);
textView.setAdapter(adapter);
textView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View v, int position, long id) {
String iconValue = TrackIconUtils.getIconValue(context, (String) textView.getAdapter().getItem(position));
TrackIconUtils.setIconSpinner(spinner, iconValue);
}
textView.setOnItemClickListener((parent, v, position, id) -> {
String iconValue = TrackIconUtils.getIconValue(context, (String) textView.getAdapter().getItem(position));
TrackIconUtils.setIconSpinner(spinner, iconValue);
});
textView.setOnFocusChangeListener(new View.OnFocusChangeListener() {
@Override
public void onFocusChange(View v, boolean hasFocus) {
if (!hasFocus) {
String iconValue = TrackIconUtils.getIconValue(context, textView.getText().toString());
TrackIconUtils.setIconSpinner(spinner, iconValue);
}
textView.setOnFocusChangeListener((v, hasFocus) -> {
if (!hasFocus) {
String iconValue = TrackIconUtils.getIconValue(context, textView.getText().toString());
TrackIconUtils.setIconSpinner(spinner, iconValue);
}
});
String iconValue = TrackIconUtils.getIconValue(context, category);
spinner = view.findViewById(R.id.activity_type_preference_spinner);
spinner.setAdapter(TrackIconUtils.getIconSpinnerAdapter(context, iconValue));
spinner.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_UP) {
showIconSelectDialog();
}
return true;
spinner.setOnTouchListener((v, event) -> {
if (event.getAction() == MotionEvent.ACTION_UP) {
showIconSelectDialog();
}
return true;
});
spinner.setOnKeyListener(new View.OnKeyListener() {
@Override
public boolean onKey(View v, int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) {
showIconSelectDialog();
}
return true;
spinner.setOnKeyListener((v, keyCode, event) -> {
if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) {
showIconSelectDialog();
}
return true;
});
}
@@ -56,15 +56,12 @@ public class SettingsActivity extends AppCompatActivity implements ChooseActivit
public static class PrefsFragment extends PreferenceFragmentCompat {
private final SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener = new SharedPreferences.OnSharedPreferenceChangeListener() {
@Override
public void onSharedPreferenceChanged(SharedPreferences preferences, String key) {
if (PreferencesUtils.isKey(getActivity(), R.string.recording_track_id_key, key)) {
getActivity().runOnUiThread(() -> updateReset());
}
if (PreferencesUtils.isKey(getActivity(), R.string.stats_units_key, key)) {
getActivity().runOnUiThread(() -> updateUnits());
}
private final SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener = (preferences, key) -> {
if (PreferencesUtils.isKey(getActivity(), R.string.recording_track_id_key, key)) {
getActivity().runOnUiThread(this::updateReset);
}
if (PreferencesUtils.isKey(getActivity(), R.string.stats_units_key, key)) {
getActivity().runOnUiThread(this::updateUnits);
}
};
@@ -117,7 +114,7 @@ public class SettingsActivity extends AppCompatActivity implements ChooseActivit
if (dialogFragment != null) {
dialogFragment.setTargetFragment(this, 0);
dialogFragment.show(getFragmentManager(), getClass().getSimpleName());
dialogFragment.show(getParentFragmentManager(), getClass().getSimpleName());
return;
}
@@ -183,14 +183,11 @@ public abstract class BluetoothLeSensorPreference extends DialogPreference {
super.onPrepareDialogBuilder(builder);
builder.setSingleChoiceItems(listAdapter, selectedEntryIndex,
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
selectedEntryIndex = which;
(dialog, which) -> {
selectedEntryIndex = which;
BluetoothLeSensorPreferenceDialog.this.onClick(dialog, DialogInterface.BUTTON_POSITIVE);
dialog.dismiss();
}
BluetoothLeSensorPreferenceDialog.this.onClick(dialog, DialogInterface.BUTTON_POSITIVE);
dialog.dismiss();
});
builder.setIcon(android.R.drawable.stat_sys_data_bluetooth);
builder.setPositiveButton(null, null);
@@ -9,7 +9,6 @@ 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;
@@ -94,17 +93,13 @@ public class ActivityUtils {
}
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.setOnQueryTextFocusChangeListener((v, hasFocus) -> {
// Hide and show trackController when searchable widget has focus/no focus
if (trackController != null) {
if (hasFocus) {
trackController.hide();
} else {
trackController.show();
}
}
});
@@ -48,8 +48,6 @@ public class FileUtils {
*/
static final String FILEPROVIDER = BuildConfig.APPLICATION_ID + ".fileprovider";
public static final String EXPORT_DIR = "OpenTracks";
private static final String JPEG_EXTENSION = "jpeg";
/**
@@ -309,7 +307,10 @@ public class FileUtils {
public static ArrayList<DocumentFile> getFiles(DocumentFile file) {
ArrayList<DocumentFile> files = new ArrayList<>();
if (!file.isDirectory()) return files;
if (!file.isDirectory()) {
files.add(file);
return files;
}
for (DocumentFile candidate : file.listFiles()) {
if (!candidate.isDirectory()) {
@@ -11,6 +11,7 @@ import java.util.ArrayList;
import de.dennisguse.opentracks.content.data.TrackPointsColumns;
import de.dennisguse.opentracks.content.data.TracksColumns;
import de.dennisguse.opentracks.content.data.WaypointsColumns;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
/**
@@ -23,11 +24,14 @@ public class IntentDashboardUtils {
private static final String ACTION_DASHBOARD_PAYLOAD = ACTION_DASHBOARD + ".Payload";
private static final String EXTRAS_OPENTRACKS_IS_RECORDING_THIS_TRACK = "EXTRAS_OPENTRACKS_IS_RECORDING_THIS_TRACK";
private static final String EXTRAS_SHOULD_KEEP_SCREEN_ON = "EXTRAS_SHOULD_KEEP_SCREEN_ON";
private static final String EXTRAS_SHOW_WHEN_LOCKED = "EXTRAS_SHOULD_KEEP_SCREEN_ON";
private static final String EXTRAS_SHOW_FULLSCREEN = "EXTRAS_SHOULD_FULLSCREEN";
private static int TRACK_URI_INDEX = 0;
private static int TRACKPOINTS_URI_INDEX = 1;
private static final int TRACK_URI_INDEX = 0;
private static final int TRACKPOINTS_URI_INDEX = 1;
private static final int WAYPOINTS_URI_INDEX = 2;
private IntentDashboardUtils() {
}
@@ -38,7 +42,7 @@ public class IntentDashboardUtils {
* @param context the context
* @param trackIds the track ids
*/
public static void startDashboard(Context context, long[] trackIds) {
public static void startDashboard(Context context, long[] trackIds, boolean isRecording) {
if (trackIds.length == 0) {
return;
}
@@ -48,26 +52,27 @@ public class IntentDashboardUtils {
ArrayList<Uri> uris = new ArrayList<>();
uris.add(TRACK_URI_INDEX, Uri.withAppendedPath(TracksColumns.CONTENT_URI, trackIdList));
uris.add(TRACKPOINTS_URI_INDEX, Uri.withAppendedPath(TrackPointsColumns.CONTENT_URI_BY_TRACKID, trackIdList));
uris.add(WAYPOINTS_URI_INDEX, Uri.withAppendedPath(WaypointsColumns.CONTENT_URI_BY_TRACKID, trackIdList));
Intent intent = new Intent(ACTION_DASHBOARD);
intent.putParcelableArrayListExtra(ACTION_DASHBOARD_PAYLOAD, uris);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.putExtra(EXTRAS_SHOULD_KEEP_SCREEN_ON, PreferencesUtils.shouldKeepScreenOn(context));
intent.putExtra(EXTRAS_SHOW_WHEN_LOCKED, PreferencesUtils.shouldShowStatsOnLockscreen(context));
intent.putExtra(EXTRAS_OPENTRACKS_IS_RECORDING_THIS_TRACK, isRecording);
if (isRecording) {
intent.putExtra(EXTRAS_SHOW_FULLSCREEN, PreferencesUtils.shouldUseFullscreen(context));
}
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
ClipData clipData = ClipData.newRawUri(null, uris.get(TRACK_URI_INDEX));
clipData.addItem(new ClipData.Item(uris.get(TRACKPOINTS_URI_INDEX)));
clipData.addItem(new ClipData.Item(uris.get(WAYPOINTS_URI_INDEX)));
intent.setClipData(clipData);
context.startActivity(intent);
}
public static void startDashboard(Context context, long trackId) {
startDashboard(context, new long[]{trackId});
}
public static long[] extractTrackIdsFromIntent(@NonNull Intent intent) {
final ArrayList<Uri> uris = intent.getParcelableArrayListExtra(ACTION_DASHBOARD_PAYLOAD);
final Uri tracksUri = uris.get(TRACK_URI_INDEX);
@@ -16,13 +16,6 @@
package de.dennisguse.opentracks.util;
import android.location.Location;
import android.util.Log;
import java.util.ArrayList;
import java.util.List;
import java.util.Stack;
import de.dennisguse.opentracks.content.data.TrackPoint;
/**
* Utility class for decimating tracks at a given level of precision.
@@ -39,112 +32,6 @@ public class LocationUtils {
private LocationUtils() {
}
/**
* Computes the distance on the two sphere between the point c0 and the line segment c1 to c2.
*
* @param c0 the first coordinate
* @param c1 the beginning of the line segment
* @param c2 the end of the lone segment
* @return the distance in m (assuming spherical earth)
*/
private static double distance(final Location c0, final Location c1, final Location c2) {
if (c1.equals(c2)) {
return c2.distanceTo(c0);
}
final double s0lat = c0.getLatitude() * UnitConversions.DEG_TO_RAD;
final double s0lng = c0.getLongitude() * UnitConversions.DEG_TO_RAD;
final double s1lat = c1.getLatitude() * UnitConversions.DEG_TO_RAD;
final double s1lng = c1.getLongitude() * UnitConversions.DEG_TO_RAD;
final double s2lat = c2.getLatitude() * UnitConversions.DEG_TO_RAD;
final double s2lng = c2.getLongitude() * UnitConversions.DEG_TO_RAD;
double s2s1lat = s2lat - s1lat;
double s2s1lng = s2lng - s1lng;
final double u = ((s0lat - s1lat) * s2s1lat + (s0lng - s1lng) * s2s1lng)
/ (s2s1lat * s2s1lat + s2s1lng * s2s1lng);
if (u <= 0) {
return c0.distanceTo(c1);
}
if (u >= 1) {
return c0.distanceTo(c2);
}
Location sa = new Location("");
sa.setLatitude(c0.getLatitude() - c1.getLatitude());
sa.setLongitude(c0.getLongitude() - c1.getLongitude());
Location sb = new Location("");
sb.setLatitude(u * (c2.getLatitude() - c1.getLatitude()));
sb.setLongitude(u * (c2.getLongitude() - c1.getLongitude()));
return sa.distanceTo(sb);
}
/**
* Decimates the given trackPoints for a given zoom level.
* This uses a Douglas-Peucker decimation algorithm.
*
* @param tolerance in meters
* @param trackPoints input
*/
//TODO What was it used for? Sharing data with other apps?
private static List<TrackPoint> decimate(double tolerance, List<TrackPoint> trackPoints) {
List<TrackPoint> decimated = new ArrayList<>();
final int n = trackPoints.size();
if (n < 1) {
return null;
}
int idx;
int maxIdx = 0;
Stack<int[]> stack = new Stack<>();
double[] dists = new double[n];
dists[0] = 1;
dists[n - 1] = 1;
double maxDist;
double dist;
int[] current;
if (n > 2) {
int[] stackVal = new int[]{0, (n - 1)};
stack.push(stackVal);
while (stack.size() > 0) {
current = stack.pop();
maxDist = 0;
for (idx = current[0] + 1; idx < current[1]; ++idx) {
dist = LocationUtils.distance(trackPoints.get(idx).getLocation(), trackPoints.get(current[0]).getLocation(), trackPoints.get(current[1]).getLocation());
if (dist > maxDist) {
maxDist = dist;
maxIdx = idx;
}
}
if (maxDist > tolerance) {
dists[maxIdx] = maxDist;
int[] stackValCurMax = {current[0], maxIdx};
stack.push(stackValCurMax);
int[] stackValMaxCur = {maxIdx, current[1]};
stack.push(stackValMaxCur);
}
}
}
int i = 0;
idx = 0;
decimated.clear();
for (TrackPoint l : trackPoints) {
if (dists[idx] != 0) {
decimated.add(l);
i++;
}
idx++;
}
Log.d(TAG, "Decimating " + n + " points to " + i + " w/ tolerance = " + tolerance);
return decimated;
}
/**
* Checks if a given location is a valid (i.e. physically possible) location on Earth.
* Note: The special separator locations (which have latitude = 100) will not qualify as valid.
@@ -9,7 +9,7 @@ import de.dennisguse.opentracks.R;
public class MarkerUtils {
public static final int ICON_ID = R.drawable.ic_marker_blue_pushpin;
public static final int ICON_ID = R.drawable.ic_marker_orange_pushpin_with_shadow;
private MarkerUtils() {
}
@@ -53,6 +53,7 @@ public class PreferencesUtils {
//NOTE: This was also used to recover from a service restart, but this data should not be exposed to the whole application.
public static final long RECORDING_TRACK_ID_DEFAULT = -1L;
@Deprecated //Use the TrackRecordingService
public static long getRecordingTrackId(Context context) {
SharedPreferences sharedPreferences = getSharedPreferences(context);
return sharedPreferences.getLong(getKey(context, R.string.recording_track_id_key), RECORDING_TRACK_ID_DEFAULT);
@@ -208,14 +209,17 @@ public class PreferencesUtils {
return currentStatsRate.equals(context.getString(R.string.stats_rate_speed));
}
@Deprecated //Use TrackRecordingService
public static boolean isRecordingTrackPaused(Context context) {
return getBoolean(context, R.string.recording_track_paused_key, isRecordingTrackPausedDefault(context));
}
@Deprecated //Use TrackRecordingService
public static boolean isRecordingTrackPausedDefault(Context context) {
return context.getResources().getBoolean(R.bool.recording_track_paused_default);
}
@Deprecated //Use TrackRecordingService
public static void defaultRecordingTrackPaused(Context context) {
final boolean RECORDING_TRACK_PAUSED = context.getResources().getBoolean(R.bool.recording_track_paused_default);
setBoolean(context, R.string.recording_track_paused_key, RECORDING_TRACK_PAUSED);
@@ -271,6 +275,11 @@ public class PreferencesUtils {
return getBoolean(context, R.string.stats_keep_screen_on_while_recording_key, DEFAULT);
}
public static boolean shouldUseFullscreen(Context context) {
final boolean DEFAULT = context.getResources().getBoolean(R.bool.stats_fullscreen_while_recording_default);
return getBoolean(context, R.string.stats_fullscreen_while_recording_key, DEFAULT);
}
public static boolean isShowStatsElevation(Context context) {
final boolean STATS_SHOW_ELEVATION = context.getResources().getBoolean(R.bool.stats_show_elevation_default);
return getBoolean(context, R.string.stats_show_grade_elevation_key, STATS_SHOW_ELEVATION);
@@ -331,6 +340,7 @@ public class PreferencesUtils {
}
}
@Deprecated //Use TrackRecordingService
public static boolean isRecording(Context context) {
long recordingTrackId = getRecordingTrackId(context);
return isRecording(recordingTrackId);
@@ -18,7 +18,7 @@ public class ServiceUtils {
*
* @param context the current context
*/
@Deprecated
@Deprecated //TODO This method must be removed as the service will nowadays always be started
public static boolean isTrackRecordingServiceRunning(Context context) {
ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
if (activityManager == null) {
@@ -2,8 +2,8 @@ package de.dennisguse.opentracks.util;
public class UintUtils {
public static int UINT16_MAX = 0xFFFF;
public static long UINT32_MAX = 0xFFFFFFFFL;
public static final int UINT16_MAX = 0xFFFF;
public static final long UINT32_MAX = 0xFFFFFFFFL;
private UintUtils() {
}
@@ -1,18 +1,16 @@
package de.dennisguse.opentracks.viewmodels;
import android.content.Context;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.util.TrackIconUtils;
public class AggregatedStatistics {
private HashMap<String, AggregatedStatistic> aggregatedStatistics = new HashMap<>();
private final Map<String, AggregatedStatistic> aggregatedStatistics = new HashMap<>();
public void aggregate(List<Track> tracks) {
for (Track track : tracks) {
@@ -50,7 +48,7 @@ public class AggregatedStatistics {
}
public static class AggregatedStatistic {
private TrackStatistics trackStatistics;
private final TrackStatistics trackStatistics;
private int countTracks;
public AggregatedStatistic(TrackStatistics trackStatistics) {
Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

@@ -0,0 +1,59 @@
<vector android:height="34dp"
android:viewportHeight="297"
android:viewportWidth="210"
android:width="24.040403dp"
xmlns:aapt="http://schemas.android.com/aapt"
xmlns:android="http://schemas.android.com/apk/res/android">
<path
android:fillAlpha="0.49421525"
android:fillColor="#333333"
android:pathData="m108.528,213.298a56.035,7.006 5.281,0 0,-22.426 2.532,56.035 7.006,5.281 0,0 -0.234,0.177l-0.207,-0.024c-0.045,0.116 -0.075,0.231 -0.096,0.348a56.035,7.006 5.281,0 0,0.338 1.582c0.981,1.709 3.761,3.463 6.584,5.219l-22.744,15.5a67.065,11.557 4.228,0 0,-39.968 5.269,67.065 11.557,4.228 0,0 -0.729,0.576 67.065,11.557 4.228,0 0,-2.754 1.364,67.065 11.557,4.228 0,0 38.909,16.043l-32.177,27.065 52.228,-24.746a67.065,11.557 4.228,0 0,71.411 -3.819,67.065 11.557,4.228 0,0 0.728,-0.575 67.065,11.557 4.228,0 0,2.755 -1.364,67.065 11.557,4.228 0,0 -12.384,-10.598 40.034,5.073 5.25,0 0,0.056 -0.037,40.034 5.073,5.25 0,0 0.377,-0.343l22.452,-15.301c8.236,-0.652 17.656,-1.478 22.944,-3.01a56.035,7.006 5.281,0 0,2.655 -1.038,56.035 7.006,5.281 0,0 -46.084,-12.27 56.035,7.006 5.281,0 0,-41.636 -2.55z"
android:strokeAlpha="0.265501"
android:strokeColor="#000000"
android:strokeLineCap="butt"
android:strokeLineJoin="miter"
android:strokeWidth="0.138931" />
<path
android:pathData="M70.603,174.308 L33.024,288.947 94.567,183.195Z"
android:strokeAlpha="0.265501"
android:strokeColor="#000000"
android:strokeLineCap="butt"
android:strokeLineJoin="miter"
android:strokeWidth="0.264583">
<aapt:attr name="android:fillColor">
<gradient
android:endX="67.193245"
android:endY="217.31992"
android:startX="63.39815"
android:startY="215.52817"
android:type="linear">
<item
android:color="#FFFFFFFF"
android:offset="0" />
<item
android:color="#FF898D8D"
android:offset="1" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#d45500"
android:fillType="evenOdd"
android:pathData="M26.293,132.603a39.974,70.319 112.028,1 0,130.371 52.747a39.974,70.319 112.028,1 0,-130.371 -52.747z"
android:strokeWidth="0.264583" />
<path
android:fillColor="#ff6600"
android:fillType="evenOdd"
android:pathData="M29.776,125.57a39.974,70.319 112.028,1 0,130.371 52.747a39.974,70.319 112.028,1 0,-130.371 -52.747z"
android:strokeWidth="0.264583" />
<path
android:fillColor="#d45500"
android:fillType="evenOdd"
android:pathData="m85.662,24.322c-0.891,8.35 2.928,17.133 6.825,25.924l-23.334,57.673 -0.731,1.806 0.249,0.101a42.43,17.361 22.028,0 0,33.304 30.101,42.43 17.361,22.028 0,0 44.865,1.526l0.139,0.056 0.245,-0.606a42.43,17.361 22.028,0 0,0.595 -1.156,42.43 17.361,22.028 0,0 0.377,-1.245l22.452,-55.493c9.29,-2.666 20.091,-6.136 24.764,-13.175z"
android:strokeWidth="0.264583" />
<path
android:fillColor="#ff6600"
android:fillType="evenOdd"
android:pathData="M86.103,23.766a23.967,59.409 112.028,1 0,110.145 44.564a23.967,59.409 112.028,1 0,-110.145 -44.564z"
android:strokeWidth="0.264583" />
</vector>
@@ -24,11 +24,6 @@ limitations under the License.
android:paddingLeft="8dp"
android:paddingRight="8dp">
<View
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
<ImageButton
android:id="@+id/track_controller_record"
android:layout_width="wrap_content"
@@ -69,9 +64,5 @@ limitations under the License.
android:contentDescription="@string/image_stop"
android:src="@drawable/ic_button_stop_disabled" />
<View
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
</LinearLayout>
</merge>
+1 -1
View File
@@ -40,7 +40,7 @@ limitations under the License.
<EditText
android:id="@+id/marker_edit_waypoint_name"
style="@style/EditNameMarker"
android:drawableLeft="@drawable/ic_marker_blue_pushpin"
android:drawableStart="@drawable/ic_marker_orange_pushpin_with_shadow"
android:hint="@string/generic_name"
android:imeOptions="actionNext"
android:importantForAutofill="no" />
+5 -12
View File
@@ -23,11 +23,6 @@ limitations under the License.
android:paddingTop="8dp"
android:paddingBottom="8dp">
<View
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1" />
<ImageButton
android:id="@+id/track_controller_record"
android:layout_width="0dp"
@@ -39,14 +34,16 @@ limitations under the License.
<RelativeLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="6">
android:layout_weight="6"
android:gravity="center"
android:layout_gravity="center">
<TextView
android:id="@+id/track_controller_total_time"
style="@style/TextSmall"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_centerVertical="true"
android:textColor="@android:color/white" />
@@ -56,7 +53,7 @@ limitations under the License.
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_above="@id/track_controller_total_time"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:singleLine="true" />
</RelativeLayout>
@@ -69,9 +66,5 @@ limitations under the License.
android:contentDescription="@string/image_stop"
android:src="@drawable/ic_button_stop_disabled" />
<View
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1" />
</LinearLayout>
</merge>
+2 -2
View File
@@ -69,7 +69,7 @@ Si está parado o en el interior, no se registrarán los datos del sensor (pero
<string name="help_gps_troubleshooting_title">OpenTracks no está grabando las localizaciones?</string>
<string name="help_gps_troubleshooting_content">
Lo que puedes hacer...
Lo que puedes hacer…
a) Comprobar si el GPS está activado.
b) mejorar la recepción del GPS (por ejemplo, ir al exterior),\n
c) comprueba si con <a href="https://f-droid.org/en/packages/com.android.gpstest.osmdroid/">GPSTest</a> app, y\n
@@ -79,7 +79,7 @@ Si todo no ayuda, por favor informe de un error.</string>
<string name="help_support_title">¿Cómo puedo apoyar OpenTracks?</string>
<string name="help_support_content">
Puedes...
Puedes…
a) difundir la palabra sobre OpenTracks,...
b) <a href="https://github.com/OpenTracksApp/OpenTracks/issues">proponer características</a>,\n
c) <a href="https://github.com/OpenTracksApp/OpenTracks/issues">report bugs</a>,\n
+2 -2
View File
@@ -25,7 +25,7 @@ limitations under the License.
<string name="help_feature_title">OpenTracks heeft de volgende kenmerken:</string>
<string name="help_feature_content">
* creëer GPS tracks
* gebruik een hartslagsensor (alleen Bluetooth LE)...
* gebruik een hartslagsensor (alleen Bluetooth LE)…
* spraakaankondigingen (op tijd of afstand)...
* creëer markers (incl. fotos)
* werkt volledig offline
@@ -79,7 +79,7 @@ Als alles niet helpt, meldt u dan alstublieft een bug.</string>
<string name="help_support_title">Hoe kan ik OpenTracks ondersteunen?</string>
<string name="help_support_content">
Je kunt...
Je kunt…
a) het woord verspreiden over OpenTracks, en...
b) <a href="https://github.com/OpenTracksApp/OpenTracks/issues">voorstellen van functies</a>,\n
c) <a href="https://github.com/OpenTracksApp/OpenTracks/issues">melding van bugs</a>,\n
+3
View File
@@ -13,6 +13,9 @@
<string name="stats_keep_screen_on_while_recording_key" translatable="false">trackdetail_keep_screen_on_while_recording</string>
<bool name="stats_keep_screen_on_while_recording_default" translatable="false">false</bool>
<string name="stats_fullscreen_while_recording_key" translatable="false">trackdetail_fullscreen_while_recording</string>
<bool name="stats_fullscreen_while_recording_default" translatable="false">false</bool>
<string name="settings_reset_key" translatable="false">settingsReset</string>
<string name="settings_sensor_bluetooth_heart_rate_key" translatable="false">bluetoothSensor</string>
+2
View File
@@ -349,6 +349,8 @@ limitations under the License.
<string name="settings_recording_show_on_lockscreen_while_recording_summary">While recording, show statistics without unlocking the device.</string>
<string name="settings_recording_keep_screen_on_while_recording_title">Keep screen on</string>
<string name="settings_recording_keep_screen_on_while_recording_summary">While recording, keep the screen on.</string>
<string name="settings_recording_fullscreen_on_while_recording_title">Fullscreen</string>
<string name="settings_recording_fullscreen_on_while_recording_summary">While recording, present in fullscreen.</string>
<string name="settings_recording">Recording</string>
<string name="settings_recording_auto_resume_track_timeout_always_summary">Always resume a recording after reboot</string>
<string name="settings_recording_auto_resume_track_timeout_never_summary">Never resume a recording after reboot</string>
+5
View File
@@ -67,6 +67,11 @@ limitations under the License.
android:key="@string/stats_keep_screen_on_while_recording_key"
android:summary="@string/settings_recording_keep_screen_on_while_recording_summary"
android:title="@string/settings_recording_keep_screen_on_while_recording_title" />
<SwitchPreferenceCompat
android:defaultValue="@bool/stats_fullscreen_while_recording_default"
android:key="@string/stats_fullscreen_while_recording_key"
android:summary="@string/settings_recording_fullscreen_on_while_recording_summary"
android:title="@string/settings_recording_fullscreen_on_while_recording_title" />
<ListPreference
android:defaultValue="@string/voice_frequency_default"
android:entryValues="@array/frequency_values"